codegate-ai 0.10.0 → 0.12.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.
- package/README.md +39 -6
- package/dist/cli.js +24 -0
- package/dist/commands/clawhub-wrapper.d.ts +8 -1
- package/dist/commands/clawhub-wrapper.js +106 -4
- package/dist/commands/skills-wrapper.d.ts +8 -1
- package/dist/commands/skills-wrapper.js +106 -4
- package/dist/layer2-static/advisories/gha-known-vulnerable-actions.json +3 -1
- package/dist/layer2-static/detectors/workflow-floating-action-version.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-floating-action-version.js +63 -0
- package/dist/layer2-static/detectors/workflow-forbidden-uses.js +33 -0
- package/dist/layer2-static/detectors/workflow-github-env.js +89 -28
- package/dist/layer2-static/detectors/workflow-known-vuln-action.js +99 -1
- package/dist/layer2-static/detectors/workflow-ref-confusion.js +30 -0
- package/dist/layer2-static/detectors/workflow-run-untrusted-artifact.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-run-untrusted-artifact.js +126 -0
- package/dist/layer2-static/detectors/workflow-template-injection.js +102 -13
- package/dist/layer2-static/detectors/workflow-unpinned-uses.js +34 -0
- package/dist/layer2-static/detectors/workflow-unsafe-checkout-ref.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-unsafe-checkout-ref.js +102 -0
- package/dist/layer2-static/engine.js +33 -0
- package/dist/layer2-static/github/client.js +13 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -135,10 +135,12 @@ See the [Configuration](#configuration) section for full settings and examples.
|
|
|
135
135
|
| `--force` | Skip interactive confirmations. |
|
|
136
136
|
| `--include-user-scope` | Force-enable user/home AI tool config paths for this run (useful if config disables user-scope scanning). |
|
|
137
137
|
| `--collect <mode>` | Collection scope mode (`default`, `project`, `user`, `explicit`, `all`). Repeatable. |
|
|
138
|
+
| `--collect-kind <kind>` | Restrict collection to specific artifact kinds (`workflows`, `actions`, `dependabot`). Repeatable. |
|
|
138
139
|
| `--strict-collection` | Treat parse failures in collected inputs as high-severity findings. |
|
|
139
140
|
| `--persona <type>` | Audit sensitivity (`regular`, `pedantic`, `auditor`). |
|
|
140
141
|
| `--runtime-mode <mode>` | Runtime mode for optional online audits (`offline`, `online`, `online-no-audits`). |
|
|
141
142
|
| `--workflow-audits` | Enable CI/CD audit pack for GitHub workflow, action, and Dependabot inputs. |
|
|
143
|
+
| `--skill <name>` | Select one skill directory when scanning a skills-index repo URL that contains multiple skills. |
|
|
142
144
|
| `--reset-state` | Clear persisted scan-state history and exit. |
|
|
143
145
|
|
|
144
146
|
Examples:
|
|
@@ -154,7 +156,9 @@ codegate scan . --remediate
|
|
|
154
156
|
codegate scan . --fix-safe
|
|
155
157
|
codegate scan . --remediate --dry-run --patch
|
|
156
158
|
codegate scan . --workflow-audits --collect project --persona auditor --runtime-mode online
|
|
159
|
+
codegate scan . --workflow-audits --collect project --collect-kind workflows
|
|
157
160
|
codegate scan . --workflow-audits --strict-collection
|
|
161
|
+
codegate scan https://github.com/owner/repo --skill security-review
|
|
158
162
|
codegate scan . --reset-state
|
|
159
163
|
```
|
|
160
164
|
|
|
@@ -259,12 +263,27 @@ Behavior:
|
|
|
259
263
|
|
|
260
264
|
Wrapper flags (consumed by CodeGate, not forwarded):
|
|
261
265
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
266
|
+
| Flag | Purpose |
|
|
267
|
+
| -------------------------- | ----------------------------------------------------------------------------------------- |
|
|
268
|
+
| `--cg-force` | Continue install when preflight scan fails or returns blocking findings. |
|
|
269
|
+
| `--cg-deep` | Enable Layer 3 deep analysis during wrapper preflight scan. |
|
|
270
|
+
| `--cg-no-tui` | Disable TUI and interactive prompts for wrapper preflight scan. |
|
|
271
|
+
| `--cg-verbose` | Enable extended terminal output during wrapper preflight scan. |
|
|
272
|
+
| `--cg-include-user-scope` | Include user/home config surfaces in wrapper preflight scan. |
|
|
273
|
+
| `--cg-collect <mode>` | Preflight collection mode (`default`, `project`, `user`, `explicit`, `all`). Repeatable. |
|
|
274
|
+
| `--cg-collect-kind <kind>` | Preflight collection kind (`workflows`, `actions`, `dependabot`). Repeatable. |
|
|
275
|
+
| `--cg-strict-collection` | Treat parse failures in preflight-collected inputs as high-severity findings. |
|
|
276
|
+
| `--cg-persona <type>` | Preflight audit persona (`regular`, `pedantic`, `auditor`). |
|
|
277
|
+
| `--cg-runtime-mode <mode>` | Preflight runtime mode (`offline`, `online`, `online-no-audits`). |
|
|
278
|
+
| `--cg-workflow-audits` | Enable workflow audit pack for preflight scans. |
|
|
279
|
+
| `--cg-format <type>` | Set preflight output format: `terminal`, `json`, `sarif`, `markdown`, `html`. |
|
|
280
|
+
| `--cg-config <path>` | Use a specific CodeGate config file for wrapper preflight scan. |
|
|
281
|
+
| `--` | Stop wrapper-option parsing and pass all following args to the wrapped installer command. |
|
|
282
|
+
|
|
283
|
+
Inspect command help for complete usage:
|
|
284
|
+
|
|
285
|
+
- `codegate skills --help`
|
|
286
|
+
- `codegate clawhub --help`
|
|
268
287
|
|
|
269
288
|
Examples:
|
|
270
289
|
|
|
@@ -272,9 +291,23 @@ Examples:
|
|
|
272
291
|
codegate skills add https://github.com/vercel-labs/skills --skill find-skills
|
|
273
292
|
codegate skills add https://github.com/owner/repo --skill security-review --cg-force
|
|
274
293
|
codegate skills add https://github.com/owner/repo --skill security-review --cg-deep
|
|
294
|
+
codegate skills add https://github.com/owner/repo --skill security-review --cg-workflow-audits --cg-collect project
|
|
295
|
+
codegate skills add https://github.com/owner/repo --skill security-review --cg-collect-kind workflows
|
|
296
|
+
codegate skills add https://github.com/owner/repo --skill security-review --cg-strict-collection --cg-persona auditor
|
|
297
|
+
codegate skills add https://github.com/owner/repo --skill security-review --cg-runtime-mode online
|
|
298
|
+
codegate skills add https://github.com/owner/repo --skill security-review --cg-format json
|
|
299
|
+
codegate skills add https://github.com/owner/repo --skill security-review --cg-config ~/.codegate/config.json
|
|
300
|
+
codegate skills add https://github.com/owner/repo --skill security-review -- --registry custom
|
|
275
301
|
codegate clawhub install security-auditor
|
|
276
302
|
codegate clawhub install security-auditor --version 1.0.0
|
|
277
303
|
codegate clawhub install security-auditor --cg-deep
|
|
304
|
+
codegate clawhub install security-auditor --cg-workflow-audits --cg-collect project
|
|
305
|
+
codegate clawhub install security-auditor --cg-collect-kind workflows
|
|
306
|
+
codegate clawhub install security-auditor --cg-strict-collection --cg-persona auditor
|
|
307
|
+
codegate clawhub install security-auditor --cg-runtime-mode online
|
|
308
|
+
codegate clawhub install security-auditor --cg-no-tui --cg-format json
|
|
309
|
+
codegate clawhub install security-auditor --cg-config ~/.codegate/config.json
|
|
310
|
+
codegate clawhub install security-auditor -- --registry https://registry.clawhub.ai
|
|
278
311
|
codegate clawhub search security
|
|
279
312
|
```
|
|
280
313
|
|
package/dist/cli.js
CHANGED
|
@@ -31,6 +31,26 @@ function isNoTuiEnabled(options) {
|
|
|
31
31
|
function renderExampleHelp(lines) {
|
|
32
32
|
return ["", "Examples:", ...lines.map((line) => ` ${line}`)].join("\n");
|
|
33
33
|
}
|
|
34
|
+
function renderWrapperOptionsHelp() {
|
|
35
|
+
return [
|
|
36
|
+
"",
|
|
37
|
+
"CodeGate wrapper options (consumed by CodeGate, not forwarded to the wrapped CLI):",
|
|
38
|
+
" --cg-force continue install when preflight is warning/error",
|
|
39
|
+
" --cg-deep enable Layer 3 deep analysis during preflight",
|
|
40
|
+
" --cg-no-tui disable TUI and interactive prompts for preflight",
|
|
41
|
+
" --cg-verbose show extended terminal output during preflight",
|
|
42
|
+
" --cg-include-user-scope include user/home config paths in preflight scan",
|
|
43
|
+
" --cg-collect <mode> preflight collection mode (repeatable): default|project|user|explicit|all",
|
|
44
|
+
" --cg-collect-kind <kind> preflight collection kind (repeatable): workflows|actions|dependabot",
|
|
45
|
+
" --cg-strict-collection treat preflight parse failures as high severity",
|
|
46
|
+
" --cg-persona <type> preflight audit sensitivity: regular|pedantic|auditor",
|
|
47
|
+
" --cg-runtime-mode <mode> preflight runtime mode: offline|online|online-no-audits",
|
|
48
|
+
" --cg-workflow-audits enable workflow audit pack during preflight",
|
|
49
|
+
" --cg-format <type> preflight report format: terminal|json|sarif|markdown|html",
|
|
50
|
+
" --cg-config <path> use a specific CodeGate config file for preflight",
|
|
51
|
+
" -- pass subsequent args through to wrapped CLI unchanged",
|
|
52
|
+
].join("\n");
|
|
53
|
+
}
|
|
34
54
|
export function isDirectCliInvocation(importMetaUrl, argv1, deps = {}) {
|
|
35
55
|
if (!argv1) {
|
|
36
56
|
return false;
|
|
@@ -460,9 +480,11 @@ function addSkillsCommand(program, version, deps) {
|
|
|
460
480
|
.description("Wrap npx skills with CodeGate preflight scanning for installs")
|
|
461
481
|
.allowUnknownOption(true)
|
|
462
482
|
.allowExcessArguments(true)
|
|
483
|
+
.addHelpText("after", renderWrapperOptionsHelp())
|
|
463
484
|
.addHelpText("after", renderExampleHelp([
|
|
464
485
|
"codegate skills add vercel-labs/skills --skill find-skills",
|
|
465
486
|
"codegate skills add https://github.com/owner/repo --skill security-review",
|
|
487
|
+
"codegate skills add owner/repo --skill demo --cg-workflow-audits --cg-collect project",
|
|
466
488
|
"codegate skills add owner/repo --skill demo --cg-deep",
|
|
467
489
|
"codegate skills find security",
|
|
468
490
|
"codegate skills add owner/repo --skill demo --cg-force",
|
|
@@ -504,9 +526,11 @@ function addClawhubCommand(program, version, deps) {
|
|
|
504
526
|
.description("Wrap npx clawhub with CodeGate preflight scanning for installs")
|
|
505
527
|
.allowUnknownOption(true)
|
|
506
528
|
.allowExcessArguments(true)
|
|
529
|
+
.addHelpText("after", renderWrapperOptionsHelp())
|
|
507
530
|
.addHelpText("after", renderExampleHelp([
|
|
508
531
|
"codegate clawhub install security-auditor",
|
|
509
532
|
"codegate clawhub install security-auditor --version 1.0.0",
|
|
533
|
+
"codegate clawhub install security-auditor --cg-workflow-audits --cg-collect project",
|
|
510
534
|
"codegate clawhub install security-auditor --cg-deep",
|
|
511
535
|
"codegate clawhub search security",
|
|
512
536
|
"codegate clawhub install security-auditor --cg-force",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CodeGateConfig, type OutputFormat, type ResolveConfigOptions } from "../config.js";
|
|
1
|
+
import { type AuditPersona, type CodeGateConfig, type OutputFormat, type ResolveConfigOptions, type RuntimeMode, type ScanCollectionKind, type ScanCollectionMode } from "../config.js";
|
|
2
2
|
import { type ResolvedScanTarget } from "../scan-target.js";
|
|
3
3
|
import type { CodeGateReport } from "../types/report.js";
|
|
4
4
|
import { type ScanAnalysisDeps, type ScanRunnerInput } from "./scan-command.js";
|
|
@@ -10,7 +10,14 @@ export interface ClawhubWrapperRuntimeOptions {
|
|
|
10
10
|
force: boolean;
|
|
11
11
|
deep: boolean;
|
|
12
12
|
noTui: boolean;
|
|
13
|
+
verbose: boolean;
|
|
13
14
|
includeUserScope: boolean;
|
|
15
|
+
strictCollection: boolean;
|
|
16
|
+
workflowAudits: boolean;
|
|
17
|
+
collect: ScanCollectionMode[];
|
|
18
|
+
collectKinds: ScanCollectionKind[];
|
|
19
|
+
persona?: AuditPersona;
|
|
20
|
+
runtimeMode?: RuntimeMode;
|
|
14
21
|
format?: OutputFormat;
|
|
15
22
|
configPath?: string;
|
|
16
23
|
}
|
|
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
|
-
import { OUTPUT_FORMATS, } from "../config.js";
|
|
6
|
+
import { PERSONAS, RUNTIME_MODES, SCAN_COLLECTION_KINDS, SCAN_COLLECTION_MODES, OUTPUT_FORMATS, } from "../config.js";
|
|
7
7
|
import { resolveScanTarget } from "../scan-target.js";
|
|
8
8
|
import { runScanAnalysis } from "./scan-command.js";
|
|
9
9
|
import { renderByFormat, summarizeRequestedTargetFindings } from "./scan-command/helpers.js";
|
|
@@ -40,6 +40,38 @@ function parseOutputFormat(value) {
|
|
|
40
40
|
}
|
|
41
41
|
return matched;
|
|
42
42
|
}
|
|
43
|
+
function parseCollectionMode(value) {
|
|
44
|
+
const normalized = value.trim().toLowerCase();
|
|
45
|
+
const matched = SCAN_COLLECTION_MODES.find((mode) => mode === normalized);
|
|
46
|
+
if (!matched) {
|
|
47
|
+
throw new Error(`Unsupported --cg-collect value "${value}". Valid values: ${SCAN_COLLECTION_MODES.join(", ")}.`);
|
|
48
|
+
}
|
|
49
|
+
return matched;
|
|
50
|
+
}
|
|
51
|
+
function parseCollectionKind(value) {
|
|
52
|
+
const normalized = value.trim().toLowerCase();
|
|
53
|
+
const matched = SCAN_COLLECTION_KINDS.find((kind) => kind === normalized);
|
|
54
|
+
if (!matched) {
|
|
55
|
+
throw new Error(`Unsupported --cg-collect-kind value "${value}". Valid values: ${SCAN_COLLECTION_KINDS.join(", ")}.`);
|
|
56
|
+
}
|
|
57
|
+
return matched;
|
|
58
|
+
}
|
|
59
|
+
function parsePersona(value) {
|
|
60
|
+
const normalized = value.trim().toLowerCase();
|
|
61
|
+
const matched = PERSONAS.find((persona) => persona === normalized);
|
|
62
|
+
if (!matched) {
|
|
63
|
+
throw new Error(`Unsupported --cg-persona value "${value}". Valid values: ${PERSONAS.join(", ")}.`);
|
|
64
|
+
}
|
|
65
|
+
return matched;
|
|
66
|
+
}
|
|
67
|
+
function parseRuntimeMode(value) {
|
|
68
|
+
const normalized = value.trim().toLowerCase();
|
|
69
|
+
const matched = RUNTIME_MODES.find((mode) => mode === normalized);
|
|
70
|
+
if (!matched) {
|
|
71
|
+
throw new Error(`Unsupported --cg-runtime-mode value "${value}". Valid values: ${RUNTIME_MODES.join(", ")}.`);
|
|
72
|
+
}
|
|
73
|
+
return matched;
|
|
74
|
+
}
|
|
43
75
|
function isLikelyHttpUrl(value) {
|
|
44
76
|
return /^https?:\/\//iu.test(value);
|
|
45
77
|
}
|
|
@@ -383,7 +415,14 @@ export function parseClawhubInvocation(rawArgs, context) {
|
|
|
383
415
|
force: false,
|
|
384
416
|
deep: false,
|
|
385
417
|
noTui: false,
|
|
418
|
+
verbose: false,
|
|
386
419
|
includeUserScope: false,
|
|
420
|
+
strictCollection: false,
|
|
421
|
+
workflowAudits: false,
|
|
422
|
+
collect: [],
|
|
423
|
+
collectKinds: [],
|
|
424
|
+
persona: undefined,
|
|
425
|
+
runtimeMode: undefined,
|
|
387
426
|
format: undefined,
|
|
388
427
|
configPath: undefined,
|
|
389
428
|
};
|
|
@@ -409,10 +448,22 @@ export function parseClawhubInvocation(rawArgs, context) {
|
|
|
409
448
|
wrapper.noTui = true;
|
|
410
449
|
continue;
|
|
411
450
|
}
|
|
451
|
+
if (token === "--cg-verbose") {
|
|
452
|
+
wrapper.verbose = true;
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
412
455
|
if (token === "--cg-include-user-scope") {
|
|
413
456
|
wrapper.includeUserScope = true;
|
|
414
457
|
continue;
|
|
415
458
|
}
|
|
459
|
+
if (token === "--cg-strict-collection") {
|
|
460
|
+
wrapper.strictCollection = true;
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (token === "--cg-workflow-audits") {
|
|
464
|
+
wrapper.workflowAudits = true;
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
416
467
|
if (token === "--cg-format" || token.startsWith("--cg-format=")) {
|
|
417
468
|
const [value, consumedIndex] = parseWrapperOptionValue(rawArgs, index, "--cg-format");
|
|
418
469
|
wrapper.format = parseOutputFormat(value);
|
|
@@ -425,6 +476,36 @@ export function parseClawhubInvocation(rawArgs, context) {
|
|
|
425
476
|
index = consumedIndex;
|
|
426
477
|
continue;
|
|
427
478
|
}
|
|
479
|
+
if (token === "--cg-collect" || token.startsWith("--cg-collect=")) {
|
|
480
|
+
const [value, consumedIndex] = parseWrapperOptionValue(rawArgs, index, "--cg-collect");
|
|
481
|
+
const mode = parseCollectionMode(value);
|
|
482
|
+
if (!wrapper.collect.includes(mode)) {
|
|
483
|
+
wrapper.collect.push(mode);
|
|
484
|
+
}
|
|
485
|
+
index = consumedIndex;
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
if (token === "--cg-collect-kind" || token.startsWith("--cg-collect-kind=")) {
|
|
489
|
+
const [value, consumedIndex] = parseWrapperOptionValue(rawArgs, index, "--cg-collect-kind");
|
|
490
|
+
const kind = parseCollectionKind(value);
|
|
491
|
+
if (!wrapper.collectKinds.includes(kind)) {
|
|
492
|
+
wrapper.collectKinds.push(kind);
|
|
493
|
+
}
|
|
494
|
+
index = consumedIndex;
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
if (token === "--cg-persona" || token.startsWith("--cg-persona=")) {
|
|
498
|
+
const [value, consumedIndex] = parseWrapperOptionValue(rawArgs, index, "--cg-persona");
|
|
499
|
+
wrapper.persona = parsePersona(value);
|
|
500
|
+
index = consumedIndex;
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
if (token === "--cg-runtime-mode" || token.startsWith("--cg-runtime-mode=")) {
|
|
504
|
+
const [value, consumedIndex] = parseWrapperOptionValue(rawArgs, index, "--cg-runtime-mode");
|
|
505
|
+
wrapper.runtimeMode = parseRuntimeMode(value);
|
|
506
|
+
index = consumedIndex;
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
428
509
|
if (token.startsWith("--cg-")) {
|
|
429
510
|
throw new Error(`Unknown CodeGate wrapper option: ${token}`);
|
|
430
511
|
}
|
|
@@ -529,9 +610,24 @@ export async function executeClawhubWrapper(input, deps) {
|
|
|
529
610
|
scanTarget: resolvedTarget.scanTarget,
|
|
530
611
|
cli: cliConfig,
|
|
531
612
|
});
|
|
532
|
-
const config =
|
|
533
|
-
|
|
534
|
-
: baseConfig
|
|
613
|
+
const config = {
|
|
614
|
+
...baseConfig,
|
|
615
|
+
scan_user_scope: parsed.wrapper.includeUserScope === true ? true : (baseConfig.scan_user_scope ?? false),
|
|
616
|
+
scan_collection_modes: parsed.wrapper.collect.length > 0
|
|
617
|
+
? parsed.wrapper.collect
|
|
618
|
+
: baseConfig.scan_collection_modes,
|
|
619
|
+
scan_collection_kinds: parsed.wrapper.collectKinds.length > 0
|
|
620
|
+
? parsed.wrapper.collectKinds
|
|
621
|
+
: baseConfig.scan_collection_kinds,
|
|
622
|
+
strict_collection: parsed.wrapper.strictCollection === true ? true : (baseConfig.strict_collection ?? false),
|
|
623
|
+
persona: parsed.wrapper.persona ?? baseConfig.persona,
|
|
624
|
+
runtime_mode: parsed.wrapper.runtimeMode ?? baseConfig.runtime_mode,
|
|
625
|
+
workflow_audits: {
|
|
626
|
+
enabled: parsed.wrapper.workflowAudits === true
|
|
627
|
+
? true
|
|
628
|
+
: (baseConfig.workflow_audits?.enabled ?? false),
|
|
629
|
+
},
|
|
630
|
+
};
|
|
535
631
|
const { report, deepScanNotes } = await runScanAnalysis({
|
|
536
632
|
version: input.version,
|
|
537
633
|
scanTarget: resolvedTarget.scanTarget,
|
|
@@ -541,8 +637,14 @@ export async function executeClawhubWrapper(input, deps) {
|
|
|
541
637
|
options: {
|
|
542
638
|
noTui,
|
|
543
639
|
format: parsed.wrapper.format,
|
|
640
|
+
verbose: parsed.wrapper.verbose,
|
|
544
641
|
force: parsed.wrapper.force,
|
|
545
642
|
includeUserScope: parsed.wrapper.includeUserScope,
|
|
643
|
+
collect: parsed.wrapper.collect.length > 0 ? parsed.wrapper.collect : undefined,
|
|
644
|
+
strictCollection: parsed.wrapper.strictCollection,
|
|
645
|
+
persona: parsed.wrapper.persona,
|
|
646
|
+
runtimeMode: parsed.wrapper.runtimeMode,
|
|
647
|
+
workflowAudits: parsed.wrapper.workflowAudits,
|
|
546
648
|
deep: parsed.wrapper.deep,
|
|
547
649
|
},
|
|
548
650
|
}, {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CodeGateConfig, type OutputFormat, type ResolveConfigOptions } from "../config.js";
|
|
1
|
+
import { type AuditPersona, type CodeGateConfig, type OutputFormat, type ResolveConfigOptions, type RuntimeMode, type ScanCollectionKind, type ScanCollectionMode } from "../config.js";
|
|
2
2
|
import { type ResolvedScanTarget } from "../scan-target.js";
|
|
3
3
|
import { type ScanAnalysisDeps, type ScanRunnerInput } from "./scan-command.js";
|
|
4
4
|
import type { CodeGateReport } from "../types/report.js";
|
|
@@ -6,7 +6,14 @@ export interface SkillsWrapperRuntimeOptions {
|
|
|
6
6
|
force: boolean;
|
|
7
7
|
deep: boolean;
|
|
8
8
|
noTui: boolean;
|
|
9
|
+
verbose: boolean;
|
|
9
10
|
includeUserScope: boolean;
|
|
11
|
+
strictCollection: boolean;
|
|
12
|
+
workflowAudits: boolean;
|
|
13
|
+
collect: ScanCollectionMode[];
|
|
14
|
+
collectKinds: ScanCollectionKind[];
|
|
15
|
+
persona?: AuditPersona;
|
|
16
|
+
runtimeMode?: RuntimeMode;
|
|
10
17
|
format?: OutputFormat;
|
|
11
18
|
configPath?: string;
|
|
12
19
|
}
|
|
@@ -2,7 +2,7 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { createInterface } from "node:readline/promises";
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
|
-
import { OUTPUT_FORMATS, } from "../config.js";
|
|
5
|
+
import { PERSONAS, RUNTIME_MODES, SCAN_COLLECTION_KINDS, SCAN_COLLECTION_MODES, OUTPUT_FORMATS, } from "../config.js";
|
|
6
6
|
import { renderByFormat, summarizeRequestedTargetFindings } from "./scan-command/helpers.js";
|
|
7
7
|
import { resolveScanTarget } from "../scan-target.js";
|
|
8
8
|
import { runScanAnalysis } from "./scan-command.js";
|
|
@@ -33,6 +33,38 @@ function parseOutputFormat(value) {
|
|
|
33
33
|
}
|
|
34
34
|
return matched;
|
|
35
35
|
}
|
|
36
|
+
function parseCollectionMode(value) {
|
|
37
|
+
const normalized = value.trim().toLowerCase();
|
|
38
|
+
const matched = SCAN_COLLECTION_MODES.find((mode) => mode === normalized);
|
|
39
|
+
if (!matched) {
|
|
40
|
+
throw new Error(`Unsupported --cg-collect value "${value}". Valid values: ${SCAN_COLLECTION_MODES.join(", ")}.`);
|
|
41
|
+
}
|
|
42
|
+
return matched;
|
|
43
|
+
}
|
|
44
|
+
function parseCollectionKind(value) {
|
|
45
|
+
const normalized = value.trim().toLowerCase();
|
|
46
|
+
const matched = SCAN_COLLECTION_KINDS.find((kind) => kind === normalized);
|
|
47
|
+
if (!matched) {
|
|
48
|
+
throw new Error(`Unsupported --cg-collect-kind value "${value}". Valid values: ${SCAN_COLLECTION_KINDS.join(", ")}.`);
|
|
49
|
+
}
|
|
50
|
+
return matched;
|
|
51
|
+
}
|
|
52
|
+
function parsePersona(value) {
|
|
53
|
+
const normalized = value.trim().toLowerCase();
|
|
54
|
+
const matched = PERSONAS.find((persona) => persona === normalized);
|
|
55
|
+
if (!matched) {
|
|
56
|
+
throw new Error(`Unsupported --cg-persona value "${value}". Valid values: ${PERSONAS.join(", ")}.`);
|
|
57
|
+
}
|
|
58
|
+
return matched;
|
|
59
|
+
}
|
|
60
|
+
function parseRuntimeMode(value) {
|
|
61
|
+
const normalized = value.trim().toLowerCase();
|
|
62
|
+
const matched = RUNTIME_MODES.find((mode) => mode === normalized);
|
|
63
|
+
if (!matched) {
|
|
64
|
+
throw new Error(`Unsupported --cg-runtime-mode value "${value}". Valid values: ${RUNTIME_MODES.join(", ")}.`);
|
|
65
|
+
}
|
|
66
|
+
return matched;
|
|
67
|
+
}
|
|
36
68
|
function isLikelyGitSshSource(value) {
|
|
37
69
|
return /^git@[^:]+:.+/iu.test(value) || /^ssh:\/\//iu.test(value);
|
|
38
70
|
}
|
|
@@ -179,7 +211,14 @@ export function parseSkillsInvocation(rawArgs, context) {
|
|
|
179
211
|
force: false,
|
|
180
212
|
deep: false,
|
|
181
213
|
noTui: false,
|
|
214
|
+
verbose: false,
|
|
182
215
|
includeUserScope: false,
|
|
216
|
+
strictCollection: false,
|
|
217
|
+
workflowAudits: false,
|
|
218
|
+
collect: [],
|
|
219
|
+
collectKinds: [],
|
|
220
|
+
persona: undefined,
|
|
221
|
+
runtimeMode: undefined,
|
|
183
222
|
format: undefined,
|
|
184
223
|
configPath: undefined,
|
|
185
224
|
};
|
|
@@ -205,10 +244,22 @@ export function parseSkillsInvocation(rawArgs, context) {
|
|
|
205
244
|
wrapper.noTui = true;
|
|
206
245
|
continue;
|
|
207
246
|
}
|
|
247
|
+
if (token === "--cg-verbose") {
|
|
248
|
+
wrapper.verbose = true;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
208
251
|
if (token === "--cg-include-user-scope") {
|
|
209
252
|
wrapper.includeUserScope = true;
|
|
210
253
|
continue;
|
|
211
254
|
}
|
|
255
|
+
if (token === "--cg-strict-collection") {
|
|
256
|
+
wrapper.strictCollection = true;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (token === "--cg-workflow-audits") {
|
|
260
|
+
wrapper.workflowAudits = true;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
212
263
|
if (token === "--cg-format" || token.startsWith("--cg-format=")) {
|
|
213
264
|
const [value, consumedIndex] = parseWrapperOptionValue(rawArgs, index, "--cg-format");
|
|
214
265
|
wrapper.format = parseOutputFormat(value);
|
|
@@ -221,6 +272,36 @@ export function parseSkillsInvocation(rawArgs, context) {
|
|
|
221
272
|
index = consumedIndex;
|
|
222
273
|
continue;
|
|
223
274
|
}
|
|
275
|
+
if (token === "--cg-collect" || token.startsWith("--cg-collect=")) {
|
|
276
|
+
const [value, consumedIndex] = parseWrapperOptionValue(rawArgs, index, "--cg-collect");
|
|
277
|
+
const mode = parseCollectionMode(value);
|
|
278
|
+
if (!wrapper.collect.includes(mode)) {
|
|
279
|
+
wrapper.collect.push(mode);
|
|
280
|
+
}
|
|
281
|
+
index = consumedIndex;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (token === "--cg-collect-kind" || token.startsWith("--cg-collect-kind=")) {
|
|
285
|
+
const [value, consumedIndex] = parseWrapperOptionValue(rawArgs, index, "--cg-collect-kind");
|
|
286
|
+
const kind = parseCollectionKind(value);
|
|
287
|
+
if (!wrapper.collectKinds.includes(kind)) {
|
|
288
|
+
wrapper.collectKinds.push(kind);
|
|
289
|
+
}
|
|
290
|
+
index = consumedIndex;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (token === "--cg-persona" || token.startsWith("--cg-persona=")) {
|
|
294
|
+
const [value, consumedIndex] = parseWrapperOptionValue(rawArgs, index, "--cg-persona");
|
|
295
|
+
wrapper.persona = parsePersona(value);
|
|
296
|
+
index = consumedIndex;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (token === "--cg-runtime-mode" || token.startsWith("--cg-runtime-mode=")) {
|
|
300
|
+
const [value, consumedIndex] = parseWrapperOptionValue(rawArgs, index, "--cg-runtime-mode");
|
|
301
|
+
wrapper.runtimeMode = parseRuntimeMode(value);
|
|
302
|
+
index = consumedIndex;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
224
305
|
if (token.startsWith("--cg-")) {
|
|
225
306
|
throw new Error(`Unknown CodeGate wrapper option: ${token}`);
|
|
226
307
|
}
|
|
@@ -322,9 +403,24 @@ export async function executeSkillsWrapper(input, deps) {
|
|
|
322
403
|
scanTarget: resolvedTarget.scanTarget,
|
|
323
404
|
cli: cliConfig,
|
|
324
405
|
});
|
|
325
|
-
const config =
|
|
326
|
-
|
|
327
|
-
: baseConfig
|
|
406
|
+
const config = {
|
|
407
|
+
...baseConfig,
|
|
408
|
+
scan_user_scope: parsed.wrapper.includeUserScope === true ? true : (baseConfig.scan_user_scope ?? false),
|
|
409
|
+
scan_collection_modes: parsed.wrapper.collect.length > 0
|
|
410
|
+
? parsed.wrapper.collect
|
|
411
|
+
: baseConfig.scan_collection_modes,
|
|
412
|
+
scan_collection_kinds: parsed.wrapper.collectKinds.length > 0
|
|
413
|
+
? parsed.wrapper.collectKinds
|
|
414
|
+
: baseConfig.scan_collection_kinds,
|
|
415
|
+
strict_collection: parsed.wrapper.strictCollection === true ? true : (baseConfig.strict_collection ?? false),
|
|
416
|
+
persona: parsed.wrapper.persona ?? baseConfig.persona,
|
|
417
|
+
runtime_mode: parsed.wrapper.runtimeMode ?? baseConfig.runtime_mode,
|
|
418
|
+
workflow_audits: {
|
|
419
|
+
enabled: parsed.wrapper.workflowAudits === true
|
|
420
|
+
? true
|
|
421
|
+
: (baseConfig.workflow_audits?.enabled ?? false),
|
|
422
|
+
},
|
|
423
|
+
};
|
|
328
424
|
const { report, deepScanNotes } = await runScanAnalysis({
|
|
329
425
|
version: input.version,
|
|
330
426
|
scanTarget: resolvedTarget.scanTarget,
|
|
@@ -334,8 +430,14 @@ export async function executeSkillsWrapper(input, deps) {
|
|
|
334
430
|
options: {
|
|
335
431
|
noTui,
|
|
336
432
|
format: parsed.wrapper.format,
|
|
433
|
+
verbose: parsed.wrapper.verbose,
|
|
337
434
|
force: parsed.wrapper.force,
|
|
338
435
|
includeUserScope: parsed.wrapper.includeUserScope,
|
|
436
|
+
collect: parsed.wrapper.collect.length > 0 ? parsed.wrapper.collect : undefined,
|
|
437
|
+
strictCollection: parsed.wrapper.strictCollection,
|
|
438
|
+
persona: parsed.wrapper.persona,
|
|
439
|
+
runtimeMode: parsed.wrapper.runtimeMode,
|
|
440
|
+
workflowAudits: parsed.wrapper.workflowAudits,
|
|
339
441
|
skill: preferredSkill,
|
|
340
442
|
deep: parsed.wrapper.deep,
|
|
341
443
|
},
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Finding } from "../../types/finding.js";
|
|
2
|
+
export interface WorkflowFloatingActionVersionInput {
|
|
3
|
+
filePath: string;
|
|
4
|
+
parsed: unknown;
|
|
5
|
+
textContent: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function detectWorkflowFloatingActionVersion(input: WorkflowFloatingActionVersionInput): Finding[];
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { buildFindingEvidence } from "../evidence.js";
|
|
2
|
+
import { extractWorkflowFacts, isGitHubWorkflowPath } from "../workflow/parser.js";
|
|
3
|
+
const FLOATING_VERSION_VALUES = new Set(["latest", "stable", "edge", "nightly", "main", "master"]);
|
|
4
|
+
const VERSION_KEY_PATTERN = /version/iu;
|
|
5
|
+
function isRepositoryUses(value) {
|
|
6
|
+
if (typeof value !== "string") {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
return /^[a-z0-9._-]+\/[a-z0-9._-]+(?:\/[^@]+)?@[^\s]+$/iu.test(value.trim());
|
|
10
|
+
}
|
|
11
|
+
function isFloatingVersionValue(value) {
|
|
12
|
+
const normalized = value.trim().toLowerCase();
|
|
13
|
+
return FLOATING_VERSION_VALUES.has(normalized);
|
|
14
|
+
}
|
|
15
|
+
export function detectWorkflowFloatingActionVersion(input) {
|
|
16
|
+
if (!isGitHubWorkflowPath(input.filePath)) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
const facts = extractWorkflowFacts(input.parsed);
|
|
20
|
+
if (!facts) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
const findings = [];
|
|
24
|
+
facts.jobs.forEach((job, jobIndex) => {
|
|
25
|
+
job.steps.forEach((step, stepIndex) => {
|
|
26
|
+
if (!isRepositoryUses(step.uses) || !step.with) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
for (const [withKey, withValue] of Object.entries(step.with)) {
|
|
30
|
+
if (!VERSION_KEY_PATTERN.test(withKey) || !isFloatingVersionValue(withValue)) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const evidence = buildFindingEvidence({
|
|
34
|
+
textContent: input.textContent,
|
|
35
|
+
searchTerms: [step.uses ?? "", withKey, withValue],
|
|
36
|
+
fallbackValue: `${withKey}: ${withValue}`,
|
|
37
|
+
});
|
|
38
|
+
findings.push({
|
|
39
|
+
rule_id: "workflow-floating-action-version",
|
|
40
|
+
finding_id: `WORKFLOW_FLOATING_ACTION_VERSION-${input.filePath}-${jobIndex}-${stepIndex}-${withKey}`,
|
|
41
|
+
severity: "MEDIUM",
|
|
42
|
+
category: "CI_SUPPLY_CHAIN",
|
|
43
|
+
layer: "L2",
|
|
44
|
+
file_path: input.filePath,
|
|
45
|
+
location: { field: `jobs.${job.id}.steps[${stepIndex}].with.${withKey}` },
|
|
46
|
+
description: "Action input uses a floating version selector, which can pull unexpected releases over time",
|
|
47
|
+
affected_tools: ["github-actions"],
|
|
48
|
+
cve: null,
|
|
49
|
+
owasp: ["ASI02"],
|
|
50
|
+
cwe: "CWE-1104",
|
|
51
|
+
confidence: "HIGH",
|
|
52
|
+
fixable: false,
|
|
53
|
+
remediation_actions: [
|
|
54
|
+
"Pin action input versions to explicit releases instead of mutable selectors like latest",
|
|
55
|
+
],
|
|
56
|
+
evidence: evidence?.evidence ?? null,
|
|
57
|
+
suppressed: false,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
return findings;
|
|
63
|
+
}
|
|
@@ -104,6 +104,39 @@ export function detectWorkflowForbiddenUses(input) {
|
|
|
104
104
|
}
|
|
105
105
|
const findings = [];
|
|
106
106
|
facts.jobs.forEach((job, jobIndex) => {
|
|
107
|
+
const jobUses = job.uses?.trim();
|
|
108
|
+
if (jobUses && isForbidden(jobUses, policy)) {
|
|
109
|
+
const evidence = buildFindingEvidence({
|
|
110
|
+
textContent: input.textContent,
|
|
111
|
+
searchTerms: [jobUses],
|
|
112
|
+
fallbackValue: `uses: ${jobUses}`,
|
|
113
|
+
});
|
|
114
|
+
findings.push({
|
|
115
|
+
rule_id: "workflow-forbidden-uses",
|
|
116
|
+
finding_id: `WORKFLOW_FORBIDDEN_USES-JOB-${input.filePath}-${jobIndex}`,
|
|
117
|
+
severity: "HIGH",
|
|
118
|
+
category: "CI_SUPPLY_CHAIN",
|
|
119
|
+
layer: "L2",
|
|
120
|
+
file_path: input.filePath,
|
|
121
|
+
location: { field: `jobs.${job.id}.uses` },
|
|
122
|
+
description: policy.mode === "allow"
|
|
123
|
+
? "Workflow uses repository action outside the configured allowlist"
|
|
124
|
+
: "Workflow uses repository action matching the configured denylist",
|
|
125
|
+
affected_tools: ["github-actions"],
|
|
126
|
+
cve: null,
|
|
127
|
+
owasp: ["ASI02"],
|
|
128
|
+
cwe: "CWE-829",
|
|
129
|
+
confidence: "HIGH",
|
|
130
|
+
fixable: false,
|
|
131
|
+
remediation_actions: [
|
|
132
|
+
policy.mode === "allow"
|
|
133
|
+
? "Add the action to the allowlist only if it is explicitly trusted"
|
|
134
|
+
: "Remove the action or move it to an allowlist-only policy if it is trusted",
|
|
135
|
+
],
|
|
136
|
+
evidence: evidence?.evidence ?? null,
|
|
137
|
+
suppressed: false,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
107
140
|
job.steps.forEach((step, stepIndex) => {
|
|
108
141
|
const uses = step.uses?.trim();
|
|
109
142
|
if (!uses) {
|