codegate-ai 0.7.0 → 0.9.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 +109 -40
- package/dist/cli.js +85 -7
- package/dist/commands/scan-command.d.ts +6 -1
- package/dist/commands/scan-command.js +1 -1
- package/dist/commands/scan-content-command.d.ts +16 -0
- package/dist/commands/scan-content-command.js +65 -0
- package/dist/config/inline-ignore.d.ts +11 -0
- package/dist/config/inline-ignore.js +57 -0
- package/dist/config/suppression-policy.d.ts +22 -0
- package/dist/config/suppression-policy.js +146 -0
- package/dist/config.d.ts +23 -0
- package/dist/config.js +188 -3
- package/dist/layer2-static/action/parser.d.ts +3 -0
- package/dist/layer2-static/action/parser.js +165 -0
- package/dist/layer2-static/action/types.d.ts +39 -0
- package/dist/layer2-static/action/types.js +1 -0
- package/dist/layer2-static/advisories/agent-components.json +62 -0
- package/dist/layer2-static/advisories/cache.d.ts +6 -0
- package/dist/layer2-static/advisories/cache.js +44 -0
- package/dist/layer2-static/advisories/cache.ts +62 -0
- package/dist/layer2-static/advisories/gha-advisory-client.d.ts +7 -0
- package/dist/layer2-static/advisories/gha-advisory-client.js +4 -0
- package/dist/layer2-static/advisories/gha-advisory-client.ts +13 -0
- package/dist/layer2-static/advisories/gha-known-vulnerable-actions.json +4 -0
- package/dist/layer2-static/audits/registry.d.ts +3 -0
- package/dist/layer2-static/audits/registry.js +29 -0
- package/dist/layer2-static/audits/types.d.ts +12 -0
- package/dist/layer2-static/audits/types.js +1 -0
- package/dist/layer2-static/dependabot/parser.d.ts +3 -0
- package/dist/layer2-static/dependabot/parser.js +189 -0
- package/dist/layer2-static/dependabot/types.d.ts +50 -0
- package/dist/layer2-static/dependabot/types.js +1 -0
- package/dist/layer2-static/detectors/advisory-intelligence.d.ts +7 -0
- package/dist/layer2-static/detectors/advisory-intelligence.js +170 -0
- package/dist/layer2-static/detectors/command-exec.js +6 -0
- package/dist/layer2-static/detectors/dependabot-cooldown.d.ts +7 -0
- package/dist/layer2-static/detectors/dependabot-cooldown.js +54 -0
- package/dist/layer2-static/detectors/dependabot-execution.d.ts +7 -0
- package/dist/layer2-static/detectors/dependabot-execution.js +68 -0
- package/dist/layer2-static/detectors/rule-file.js +5 -0
- package/dist/layer2-static/detectors/workflow-anonymous-definition.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-anonymous-definition.js +48 -0
- package/dist/layer2-static/detectors/workflow-archived-uses.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-archived-uses.js +147 -0
- package/dist/layer2-static/detectors/workflow-artipacked.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-artipacked.js +62 -0
- package/dist/layer2-static/detectors/workflow-bot-conditions.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-bot-conditions.js +108 -0
- package/dist/layer2-static/detectors/workflow-cache-poisoning.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-cache-poisoning.js +67 -0
- package/dist/layer2-static/detectors/workflow-concurrency-limits.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-concurrency-limits.js +76 -0
- package/dist/layer2-static/detectors/workflow-dangerous-triggers.d.ts +6 -0
- package/dist/layer2-static/detectors/workflow-dangerous-triggers.js +38 -0
- package/dist/layer2-static/detectors/workflow-excessive-permissions.d.ts +6 -0
- package/dist/layer2-static/detectors/workflow-excessive-permissions.js +65 -0
- package/dist/layer2-static/detectors/workflow-forbidden-uses.d.ts +12 -0
- package/dist/layer2-static/detectors/workflow-forbidden-uses.js +148 -0
- package/dist/layer2-static/detectors/workflow-github-env.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-github-env.js +49 -0
- package/dist/layer2-static/detectors/workflow-hardcoded-container-credentials.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-hardcoded-container-credentials.js +90 -0
- package/dist/layer2-static/detectors/workflow-impostor-commit.d.ts +9 -0
- package/dist/layer2-static/detectors/workflow-impostor-commit.js +127 -0
- package/dist/layer2-static/detectors/workflow-insecure-commands.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-insecure-commands.js +72 -0
- package/dist/layer2-static/detectors/workflow-known-vuln-action.d.ts +8 -0
- package/dist/layer2-static/detectors/workflow-known-vuln-action.js +70 -0
- package/dist/layer2-static/detectors/workflow-misfeature.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-misfeature.js +101 -0
- package/dist/layer2-static/detectors/workflow-obfuscation.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-obfuscation.js +79 -0
- package/dist/layer2-static/detectors/workflow-overprovisioned-secrets.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-overprovisioned-secrets.js +81 -0
- package/dist/layer2-static/detectors/workflow-ref-confusion.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-ref-confusion.js +69 -0
- package/dist/layer2-static/detectors/workflow-ref-version-mismatch.d.ts +9 -0
- package/dist/layer2-static/detectors/workflow-ref-version-mismatch.js +179 -0
- package/dist/layer2-static/detectors/workflow-secrets-inherit.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-secrets-inherit.js +59 -0
- package/dist/layer2-static/detectors/workflow-secrets-outside-env.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-secrets-outside-env.js +99 -0
- package/dist/layer2-static/detectors/workflow-self-hosted-runner.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-self-hosted-runner.js +67 -0
- package/dist/layer2-static/detectors/workflow-stale-action-refs.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-stale-action-refs.js +178 -0
- package/dist/layer2-static/detectors/workflow-superfluous-actions.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-superfluous-actions.js +70 -0
- package/dist/layer2-static/detectors/workflow-template-injection.d.ts +6 -0
- package/dist/layer2-static/detectors/workflow-template-injection.js +94 -0
- package/dist/layer2-static/detectors/workflow-undocumented-permissions.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-undocumented-permissions.js +101 -0
- package/dist/layer2-static/detectors/workflow-unpinned-images.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-unpinned-images.js +108 -0
- package/dist/layer2-static/detectors/workflow-unpinned-uses.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-unpinned-uses.js +60 -0
- package/dist/layer2-static/detectors/workflow-unredacted-secrets.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-unredacted-secrets.js +92 -0
- package/dist/layer2-static/detectors/workflow-unsound-condition.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-unsound-condition.js +90 -0
- package/dist/layer2-static/detectors/workflow-unsound-contains.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-unsound-contains.js +88 -0
- package/dist/layer2-static/detectors/workflow-use-trusted-publishing.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-use-trusted-publishing.js +66 -0
- package/dist/layer2-static/engine.d.ts +13 -2
- package/dist/layer2-static/engine.js +578 -41
- package/dist/layer2-static/github/cache.d.ts +8 -0
- package/dist/layer2-static/github/cache.js +41 -0
- package/dist/layer2-static/github/client.d.ts +17 -0
- package/dist/layer2-static/github/client.js +43 -0
- package/dist/layer2-static/rule-engine.d.ts +1 -1
- package/dist/layer2-static/rule-engine.js +1 -13
- package/dist/layer2-static/rule-pack-loader.d.ts +10 -0
- package/dist/layer2-static/rule-pack-loader.js +187 -0
- package/dist/layer2-static/workflow/injection-sinks.json +5 -0
- package/dist/layer2-static/workflow/parser.d.ts +3 -0
- package/dist/layer2-static/workflow/parser.js +88 -0
- package/dist/layer2-static/workflow/types.d.ts +15 -0
- package/dist/layer2-static/workflow/types.js +1 -0
- package/dist/layer3-dynamic/toxic-flow.js +6 -0
- package/dist/pipeline.d.ts +1 -1
- package/dist/pipeline.js +11 -10
- package/dist/report/finding-fingerprint.d.ts +5 -0
- package/dist/report/finding-fingerprint.js +47 -0
- package/dist/reporter/markdown.js +25 -3
- package/dist/reporter/sarif.js +25 -5
- package/dist/reporter/terminal.js +37 -0
- package/dist/scan-target/fetch-plan.d.ts +8 -0
- package/dist/scan-target/fetch-plan.js +30 -0
- package/dist/scan-target/staging.js +60 -5
- package/dist/scan.d.ts +7 -1
- package/dist/scan.js +157 -40
- package/dist/types/finding.d.ts +10 -1
- package/dist/types/finding.js +5 -0
- package/package.json +1 -1
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
function normalizeString(value) {
|
|
2
|
+
if (typeof value !== "string") {
|
|
3
|
+
return undefined;
|
|
4
|
+
}
|
|
5
|
+
const trimmed = value.trim();
|
|
6
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
7
|
+
}
|
|
8
|
+
function escapeRegExp(value) {
|
|
9
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
10
|
+
}
|
|
11
|
+
function globToRegExp(glob) {
|
|
12
|
+
const pattern = normalizeString(glob)?.replaceAll("\\", "/");
|
|
13
|
+
if (!pattern) {
|
|
14
|
+
return /^$/;
|
|
15
|
+
}
|
|
16
|
+
let regex = "^";
|
|
17
|
+
for (let index = 0; index < pattern.length; index++) {
|
|
18
|
+
const char = pattern[index];
|
|
19
|
+
if (char === "*") {
|
|
20
|
+
if (pattern[index + 1] === "*") {
|
|
21
|
+
regex += ".*";
|
|
22
|
+
index++;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
regex += "[^/]*";
|
|
26
|
+
}
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (char === "?") {
|
|
30
|
+
regex += "[^/]";
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
regex += escapeRegExp(char);
|
|
34
|
+
}
|
|
35
|
+
regex += "$";
|
|
36
|
+
return new RegExp(regex);
|
|
37
|
+
}
|
|
38
|
+
function matchesGlob(value, glob) {
|
|
39
|
+
return globToRegExp(glob).test(value.replaceAll("\\", "/"));
|
|
40
|
+
}
|
|
41
|
+
function parseSuppressionLocation(value) {
|
|
42
|
+
const trimmed = normalizeString(value);
|
|
43
|
+
if (!trimmed) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const pieces = trimmed.split(":");
|
|
47
|
+
if (pieces.length === 0 || pieces.length > 3) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const [filePath, lineRaw, columnRaw] = pieces;
|
|
51
|
+
if (!filePath) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const line = lineRaw !== undefined ? Number.parseInt(lineRaw, 10) : undefined;
|
|
55
|
+
const column = columnRaw !== undefined ? Number.parseInt(columnRaw, 10) : undefined;
|
|
56
|
+
if (lineRaw !== undefined && (!Number.isFinite(line) || (line ?? 0) < 1)) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
if (columnRaw !== undefined && (!Number.isFinite(column) || (column ?? 0) < 1)) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
filePath: filePath.replaceAll("\\", "/"),
|
|
64
|
+
line,
|
|
65
|
+
column,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function matchesSuppressionRule(finding, rule) {
|
|
69
|
+
const ruleId = normalizeString(rule.rule_id);
|
|
70
|
+
if (ruleId && finding.rule_id !== ruleId) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
const filePath = normalizeString(rule.file_path);
|
|
74
|
+
if (filePath && !matchesGlob(finding.file_path, filePath)) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
const location = parseSuppressionLocation(rule.location ?? "");
|
|
78
|
+
if (location) {
|
|
79
|
+
const normalizedFindingPath = finding.file_path.replaceAll("\\", "/");
|
|
80
|
+
if (normalizedFindingPath !== location.filePath) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
if (typeof location.line === "number" &&
|
|
84
|
+
(typeof finding.location.line !== "number" || finding.location.line !== location.line)) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
if (typeof location.column === "number" &&
|
|
88
|
+
(typeof finding.location.column !== "number" || finding.location.column !== location.column)) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const severity = rule.severity;
|
|
93
|
+
if (severity && finding.severity !== severity) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
const category = normalizeString(rule.category);
|
|
97
|
+
if (category && finding.category !== category) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
const cwe = normalizeString(rule.cwe);
|
|
101
|
+
if (cwe && finding.cwe !== cwe) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
const fingerprint = normalizeString(rule.fingerprint);
|
|
105
|
+
if (fingerprint && finding.fingerprint !== fingerprint) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
function matchesRulePolicyIgnore(finding, location) {
|
|
111
|
+
const parsed = parseSuppressionLocation(location);
|
|
112
|
+
if (!parsed) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
const normalizedFindingPath = finding.file_path.replaceAll("\\", "/");
|
|
116
|
+
if (normalizedFindingPath !== parsed.filePath) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
if (typeof parsed.line === "number" &&
|
|
120
|
+
(typeof finding.location.line !== "number" || finding.location.line !== parsed.line)) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
if (typeof parsed.column === "number" &&
|
|
124
|
+
(typeof finding.location.column !== "number" || finding.location.column !== parsed.column)) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
export function applySuppressionPolicy(findings, policy) {
|
|
130
|
+
const legacySuppressions = new Set((policy.suppress_findings ?? [])
|
|
131
|
+
.map((findingId) => normalizeString(findingId))
|
|
132
|
+
.filter((findingId) => findingId !== undefined));
|
|
133
|
+
const rules = policy.suppression_rules ?? [];
|
|
134
|
+
const rulePolicies = policy.rule_policies ?? {};
|
|
135
|
+
return findings.map((finding) => {
|
|
136
|
+
const ruleMatch = rules.some((rule) => matchesSuppressionRule(finding, rule));
|
|
137
|
+
const legacyMatch = legacySuppressions.has(finding.finding_id);
|
|
138
|
+
const rulePolicy = rulePolicies[finding.rule_id];
|
|
139
|
+
const ruleDisabled = rulePolicy?.disable === true;
|
|
140
|
+
const ruleIgnoreMatch = rulePolicy?.ignore?.some((location) => matchesRulePolicyIgnore(finding, location)) ?? false;
|
|
141
|
+
return {
|
|
142
|
+
...finding,
|
|
143
|
+
suppressed: finding.suppressed || legacyMatch || ruleMatch || ruleDisabled || ruleIgnoreMatch,
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import type { Finding } from "./types/finding.js";
|
|
2
2
|
import type { CodeGateReport } from "./types/report.js";
|
|
3
|
+
import { type RulePolicyMap, type SuppressionRule } from "./config/suppression-policy.js";
|
|
3
4
|
export declare const OUTPUT_FORMATS: readonly ["terminal", "json", "sarif", "markdown", "html"];
|
|
4
5
|
export type OutputFormat = (typeof OUTPUT_FORMATS)[number];
|
|
6
|
+
export declare const SCAN_COLLECTION_MODES: readonly ["default", "project", "user", "explicit", "all"];
|
|
7
|
+
export type ScanCollectionMode = (typeof SCAN_COLLECTION_MODES)[number];
|
|
8
|
+
export declare const SCAN_COLLECTION_KINDS: readonly ["workflows", "actions", "dependabot"];
|
|
9
|
+
export type ScanCollectionKind = (typeof SCAN_COLLECTION_KINDS)[number];
|
|
10
|
+
export declare const PERSONAS: readonly ["regular", "pedantic", "auditor"];
|
|
11
|
+
export type AuditPersona = (typeof PERSONAS)[number];
|
|
12
|
+
export declare const RUNTIME_MODES: readonly ["offline", "online", "online-no-audits"];
|
|
13
|
+
export type RuntimeMode = (typeof RUNTIME_MODES)[number];
|
|
5
14
|
export declare const SEVERITY_THRESHOLDS: readonly ["critical", "high", "medium", "low", "info"];
|
|
6
15
|
export type SeverityThreshold = (typeof SEVERITY_THRESHOLDS)[number];
|
|
7
16
|
export interface TuiConfig {
|
|
@@ -14,6 +23,9 @@ export interface ToolDiscoveryConfig {
|
|
|
14
23
|
agent_paths: Record<string, string>;
|
|
15
24
|
skip_tools: string[];
|
|
16
25
|
}
|
|
26
|
+
export interface WorkflowAuditConfig {
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
}
|
|
17
29
|
export interface CodeGateConfig {
|
|
18
30
|
severity_threshold: SeverityThreshold;
|
|
19
31
|
auto_proceed_below_threshold: boolean;
|
|
@@ -32,7 +44,18 @@ export interface CodeGateConfig {
|
|
|
32
44
|
check_ide_settings: boolean;
|
|
33
45
|
owasp_mapping: boolean;
|
|
34
46
|
trusted_api_domains: string[];
|
|
47
|
+
rule_pack_paths?: string[];
|
|
48
|
+
allowed_rules?: string[];
|
|
49
|
+
skip_rules?: string[];
|
|
50
|
+
strict_collection?: boolean;
|
|
51
|
+
scan_collection_modes?: ScanCollectionMode[];
|
|
52
|
+
scan_collection_kinds?: ScanCollectionKind[];
|
|
53
|
+
rules?: RulePolicyMap;
|
|
54
|
+
persona?: AuditPersona;
|
|
55
|
+
runtime_mode?: RuntimeMode;
|
|
56
|
+
workflow_audits?: WorkflowAuditConfig;
|
|
35
57
|
suppress_findings: string[];
|
|
58
|
+
suppression_rules?: SuppressionRule[];
|
|
36
59
|
}
|
|
37
60
|
export interface CliConfigOverrides {
|
|
38
61
|
format?: OutputFormat;
|
package/dist/config.js
CHANGED
|
@@ -3,7 +3,12 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { parse as parseJsonc } from "jsonc-parser";
|
|
5
5
|
import { applyReportSummary, computeExitCode as computeReportExitCode } from "./report-summary.js";
|
|
6
|
+
import { applySuppressionPolicy, } from "./config/suppression-policy.js";
|
|
6
7
|
export const OUTPUT_FORMATS = ["terminal", "json", "sarif", "markdown", "html"];
|
|
8
|
+
export const SCAN_COLLECTION_MODES = ["default", "project", "user", "explicit", "all"];
|
|
9
|
+
export const SCAN_COLLECTION_KINDS = ["workflows", "actions", "dependabot"];
|
|
10
|
+
export const PERSONAS = ["regular", "pedantic", "auditor"];
|
|
11
|
+
export const RUNTIME_MODES = ["offline", "online", "online-no-audits"];
|
|
7
12
|
export const SEVERITY_THRESHOLDS = ["critical", "high", "medium", "low", "info"];
|
|
8
13
|
export const DEFAULT_CONFIG = {
|
|
9
14
|
severity_threshold: "high",
|
|
@@ -34,7 +39,16 @@ export const DEFAULT_CONFIG = {
|
|
|
34
39
|
check_ide_settings: true,
|
|
35
40
|
owasp_mapping: true,
|
|
36
41
|
trusted_api_domains: [],
|
|
42
|
+
rule_pack_paths: [],
|
|
43
|
+
allowed_rules: [],
|
|
44
|
+
skip_rules: [],
|
|
45
|
+
strict_collection: false,
|
|
46
|
+
scan_collection_modes: ["default"],
|
|
47
|
+
persona: "regular",
|
|
48
|
+
runtime_mode: "offline",
|
|
49
|
+
workflow_audits: { enabled: false },
|
|
37
50
|
suppress_findings: [],
|
|
51
|
+
suppression_rules: [],
|
|
38
52
|
};
|
|
39
53
|
function normalizeOutputFormat(value) {
|
|
40
54
|
if (!value) {
|
|
@@ -55,6 +69,146 @@ function normalizeOptionalPath(value) {
|
|
|
55
69
|
const trimmed = value.trim();
|
|
56
70
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
57
71
|
}
|
|
72
|
+
function normalizeCollectionMode(value) {
|
|
73
|
+
if (!value) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
return SCAN_COLLECTION_MODES.find((mode) => mode === value) ?? undefined;
|
|
77
|
+
}
|
|
78
|
+
function normalizeCollectionModes(values) {
|
|
79
|
+
if (!values) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
const normalized = [];
|
|
83
|
+
for (const value of values) {
|
|
84
|
+
const mode = normalizeCollectionMode(value);
|
|
85
|
+
if (!mode || normalized.includes(mode)) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
normalized.push(mode);
|
|
89
|
+
}
|
|
90
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
91
|
+
}
|
|
92
|
+
function normalizeCollectionKind(value) {
|
|
93
|
+
if (!value) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
return SCAN_COLLECTION_KINDS.find((kind) => kind === value) ?? undefined;
|
|
97
|
+
}
|
|
98
|
+
function normalizeCollectionKinds(values) {
|
|
99
|
+
if (!values) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
const normalized = [];
|
|
103
|
+
for (const value of values) {
|
|
104
|
+
const kind = normalizeCollectionKind(value);
|
|
105
|
+
if (!kind || normalized.includes(kind)) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
normalized.push(kind);
|
|
109
|
+
}
|
|
110
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
111
|
+
}
|
|
112
|
+
function isPlainObject(value) {
|
|
113
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
114
|
+
}
|
|
115
|
+
function normalizeRulePolicyConfig(value) {
|
|
116
|
+
if (!isPlainObject(value)) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
const disable = typeof value.disable === "boolean" ? value.disable : undefined;
|
|
120
|
+
const ignoreRaw = value.ignore;
|
|
121
|
+
const ignore = Array.isArray(ignoreRaw) && ignoreRaw.length > 0
|
|
122
|
+
? ignoreRaw
|
|
123
|
+
.filter((entry) => typeof entry === "string")
|
|
124
|
+
.map((entry) => entry.trim())
|
|
125
|
+
.filter((entry) => entry.length > 0)
|
|
126
|
+
: undefined;
|
|
127
|
+
const configRaw = value.config;
|
|
128
|
+
const config = isPlainObject(configRaw) ? { ...configRaw } : undefined;
|
|
129
|
+
if (disable === undefined && ignore === undefined && config === undefined) {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
disable,
|
|
134
|
+
ignore,
|
|
135
|
+
config,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function normalizeRulePolicyMap(rules) {
|
|
139
|
+
if (!rules || typeof rules !== "object") {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
const normalized = {};
|
|
143
|
+
for (const [ruleId, value] of Object.entries(rules)) {
|
|
144
|
+
const key = ruleId.trim();
|
|
145
|
+
if (key.length === 0) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const policy = normalizeRulePolicyConfig(value);
|
|
149
|
+
if (!policy) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
normalized[key] = policy;
|
|
153
|
+
}
|
|
154
|
+
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
|
155
|
+
}
|
|
156
|
+
function mergeRulePolicyConfig(current, incoming) {
|
|
157
|
+
if (!current) {
|
|
158
|
+
if (!incoming) {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
disable: incoming.disable,
|
|
163
|
+
ignore: incoming.ignore ? [...incoming.ignore] : undefined,
|
|
164
|
+
config: incoming.config ? { ...incoming.config } : undefined,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (!incoming) {
|
|
168
|
+
return {
|
|
169
|
+
disable: current.disable,
|
|
170
|
+
ignore: current.ignore ? [...current.ignore] : undefined,
|
|
171
|
+
config: current.config ? { ...current.config } : undefined,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
disable: incoming.disable ?? current.disable,
|
|
176
|
+
ignore: unique([
|
|
177
|
+
current.ignore,
|
|
178
|
+
incoming.ignore,
|
|
179
|
+
]),
|
|
180
|
+
config: current.config || incoming.config
|
|
181
|
+
? {
|
|
182
|
+
...(current.config ?? {}),
|
|
183
|
+
...(incoming.config ?? {}),
|
|
184
|
+
}
|
|
185
|
+
: undefined,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function mergeRulePolicyMaps(...maps) {
|
|
189
|
+
const merged = {};
|
|
190
|
+
for (const map of maps) {
|
|
191
|
+
if (!map) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
for (const [ruleId, policy] of Object.entries(map)) {
|
|
195
|
+
merged[ruleId] = mergeRulePolicyConfig(merged[ruleId], policy) ?? merged[ruleId];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
199
|
+
}
|
|
200
|
+
function normalizePersona(value) {
|
|
201
|
+
if (!value) {
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
return PERSONAS.find((persona) => persona === value) ?? undefined;
|
|
205
|
+
}
|
|
206
|
+
function normalizeRuntimeMode(value) {
|
|
207
|
+
if (!value) {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
return RUNTIME_MODES.find((mode) => mode === value) ?? undefined;
|
|
211
|
+
}
|
|
58
212
|
function unique(values) {
|
|
59
213
|
const merged = values.flatMap((entry) => entry ?? []);
|
|
60
214
|
const seen = new Set();
|
|
@@ -163,22 +317,53 @@ export function resolveEffectiveConfig(options) {
|
|
|
163
317
|
globalConfig.trusted_api_domains,
|
|
164
318
|
projectConfig.trusted_api_domains,
|
|
165
319
|
]),
|
|
320
|
+
rule_pack_paths: unique([
|
|
321
|
+
DEFAULT_CONFIG.rule_pack_paths,
|
|
322
|
+
globalConfig.rule_pack_paths,
|
|
323
|
+
projectConfig.rule_pack_paths,
|
|
324
|
+
]),
|
|
325
|
+
allowed_rules: unique([
|
|
326
|
+
DEFAULT_CONFIG.allowed_rules,
|
|
327
|
+
globalConfig.allowed_rules,
|
|
328
|
+
projectConfig.allowed_rules,
|
|
329
|
+
]),
|
|
330
|
+
skip_rules: unique([
|
|
331
|
+
DEFAULT_CONFIG.skip_rules,
|
|
332
|
+
globalConfig.skip_rules,
|
|
333
|
+
projectConfig.skip_rules,
|
|
334
|
+
]),
|
|
335
|
+
strict_collection: pickFirst(projectConfig.strict_collection, globalConfig.strict_collection, DEFAULT_CONFIG.strict_collection) ?? DEFAULT_CONFIG.strict_collection,
|
|
336
|
+
scan_collection_modes: pickFirst(normalizeCollectionModes(projectConfig.scan_collection_modes), normalizeCollectionModes(globalConfig.scan_collection_modes), DEFAULT_CONFIG.scan_collection_modes) ?? DEFAULT_CONFIG.scan_collection_modes,
|
|
337
|
+
scan_collection_kinds: pickFirst(normalizeCollectionKinds(projectConfig.scan_collection_kinds), normalizeCollectionKinds(globalConfig.scan_collection_kinds)) ?? undefined,
|
|
338
|
+
rules: mergeRulePolicyMaps(normalizeRulePolicyMap(globalConfig.rules), normalizeRulePolicyMap(projectConfig.rules)) ?? undefined,
|
|
339
|
+
persona: pickFirst(normalizePersona(projectConfig.persona), normalizePersona(globalConfig.persona), DEFAULT_CONFIG.persona) ?? DEFAULT_CONFIG.persona,
|
|
340
|
+
runtime_mode: pickFirst(normalizeRuntimeMode(projectConfig.runtime_mode), normalizeRuntimeMode(globalConfig.runtime_mode), DEFAULT_CONFIG.runtime_mode) ?? DEFAULT_CONFIG.runtime_mode,
|
|
341
|
+
workflow_audits: {
|
|
342
|
+
enabled: pickFirst(projectConfig.workflow_audits?.enabled, globalConfig.workflow_audits?.enabled, DEFAULT_CONFIG.workflow_audits?.enabled) ?? false,
|
|
343
|
+
},
|
|
166
344
|
suppress_findings: unique([
|
|
167
345
|
DEFAULT_CONFIG.suppress_findings,
|
|
168
346
|
globalConfig.suppress_findings,
|
|
169
347
|
projectConfig.suppress_findings,
|
|
170
348
|
]),
|
|
349
|
+
suppression_rules: [
|
|
350
|
+
...(DEFAULT_CONFIG.suppression_rules ?? []),
|
|
351
|
+
...(globalConfig.suppression_rules ?? []),
|
|
352
|
+
...(projectConfig.suppression_rules ?? []),
|
|
353
|
+
],
|
|
171
354
|
};
|
|
172
355
|
}
|
|
173
356
|
export function computeExitCode(findings, threshold) {
|
|
174
357
|
return computeReportExitCode(findings, threshold);
|
|
175
358
|
}
|
|
176
359
|
export function applyConfigPolicy(report, config) {
|
|
177
|
-
const
|
|
178
|
-
|
|
360
|
+
const findings = applySuppressionPolicy(report.findings, {
|
|
361
|
+
suppress_findings: config.suppress_findings,
|
|
362
|
+
suppression_rules: config.suppression_rules,
|
|
363
|
+
rule_policies: config.rules,
|
|
364
|
+
}).map((finding) => ({
|
|
179
365
|
...finding,
|
|
180
366
|
owasp: config.owasp_mapping ? finding.owasp : [],
|
|
181
|
-
suppressed: finding.suppressed || suppressionSet.has(finding.finding_id),
|
|
182
367
|
}));
|
|
183
368
|
return applyReportSummary({
|
|
184
369
|
...report,
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
function asRecord(value) {
|
|
2
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3
|
+
return null;
|
|
4
|
+
}
|
|
5
|
+
return value;
|
|
6
|
+
}
|
|
7
|
+
function asString(value) {
|
|
8
|
+
return typeof value === "string" ? value : undefined;
|
|
9
|
+
}
|
|
10
|
+
function asBoolean(value) {
|
|
11
|
+
return typeof value === "boolean" ? value : undefined;
|
|
12
|
+
}
|
|
13
|
+
function asStringRecord(value) {
|
|
14
|
+
const record = asRecord(value);
|
|
15
|
+
if (!record) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
const result = {};
|
|
19
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
20
|
+
if (typeof entry === "string") {
|
|
21
|
+
result[key] = entry;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
25
|
+
}
|
|
26
|
+
function asStringArray(value) {
|
|
27
|
+
if (!Array.isArray(value)) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
const result = value.filter((entry) => typeof entry === "string");
|
|
31
|
+
return result.length > 0 ? result : undefined;
|
|
32
|
+
}
|
|
33
|
+
function normalizeActionPath(value) {
|
|
34
|
+
return value.replaceAll("\\", "/");
|
|
35
|
+
}
|
|
36
|
+
export function isGitHubActionPath(path) {
|
|
37
|
+
return /(?:^|\/)action\.ya?ml$/iu.test(normalizeActionPath(path));
|
|
38
|
+
}
|
|
39
|
+
function extractStepFacts(step) {
|
|
40
|
+
const stepRecord = asRecord(step);
|
|
41
|
+
if (!stepRecord) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const withEntries = asStringRecord(stepRecord.with);
|
|
45
|
+
const envEntries = asStringRecord(stepRecord.env);
|
|
46
|
+
const stepFacts = {
|
|
47
|
+
id: asString(stepRecord.id),
|
|
48
|
+
name: asString(stepRecord.name),
|
|
49
|
+
uses: asString(stepRecord.uses),
|
|
50
|
+
run: asString(stepRecord.run),
|
|
51
|
+
if: asString(stepRecord.if),
|
|
52
|
+
shell: asString(stepRecord.shell),
|
|
53
|
+
workingDirectory: asString(stepRecord["working-directory"]) ?? asString(stepRecord.workingDirectory),
|
|
54
|
+
with: withEntries,
|
|
55
|
+
env: envEntries,
|
|
56
|
+
};
|
|
57
|
+
if (!stepFacts.uses && !stepFacts.run) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return stepFacts;
|
|
61
|
+
}
|
|
62
|
+
function extractInputs(value) {
|
|
63
|
+
const inputs = asRecord(value);
|
|
64
|
+
if (!inputs) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
const result = {};
|
|
68
|
+
for (const [name, inputValue] of Object.entries(inputs)) {
|
|
69
|
+
const inputRecord = asRecord(inputValue);
|
|
70
|
+
if (!inputRecord) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const entry = {
|
|
74
|
+
description: asString(inputRecord.description),
|
|
75
|
+
required: asBoolean(inputRecord.required),
|
|
76
|
+
default: asString(inputRecord.default),
|
|
77
|
+
deprecationMessage: asString(inputRecord.deprecationMessage) ?? asString(inputRecord.deprecation_message),
|
|
78
|
+
};
|
|
79
|
+
if (entry.description !== undefined ||
|
|
80
|
+
entry.required !== undefined ||
|
|
81
|
+
entry.default !== undefined ||
|
|
82
|
+
entry.deprecationMessage !== undefined) {
|
|
83
|
+
result[name] = entry;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
87
|
+
}
|
|
88
|
+
function extractOutputs(value) {
|
|
89
|
+
const outputs = asRecord(value);
|
|
90
|
+
if (!outputs) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
const result = {};
|
|
94
|
+
for (const [name, outputValue] of Object.entries(outputs)) {
|
|
95
|
+
const outputRecord = asRecord(outputValue);
|
|
96
|
+
if (!outputRecord) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const entry = {
|
|
100
|
+
description: asString(outputRecord.description),
|
|
101
|
+
value: asString(outputRecord.value),
|
|
102
|
+
};
|
|
103
|
+
if (entry.description !== undefined || entry.value !== undefined) {
|
|
104
|
+
result[name] = entry;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
108
|
+
}
|
|
109
|
+
function extractRuns(value) {
|
|
110
|
+
const runs = asRecord(value);
|
|
111
|
+
if (!runs) {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
const stepsRaw = Array.isArray(runs.steps) ? runs.steps : [];
|
|
115
|
+
const steps = stepsRaw
|
|
116
|
+
.map((step) => extractStepFacts(step))
|
|
117
|
+
.filter((step) => step !== null);
|
|
118
|
+
const result = {
|
|
119
|
+
using: asString(runs.using),
|
|
120
|
+
main: asString(runs.main),
|
|
121
|
+
pre: asString(runs.pre),
|
|
122
|
+
post: asString(runs.post),
|
|
123
|
+
image: asString(runs.image),
|
|
124
|
+
args: asStringArray(runs.args),
|
|
125
|
+
steps: steps.length > 0 ? steps : undefined,
|
|
126
|
+
};
|
|
127
|
+
if (result.using === undefined &&
|
|
128
|
+
result.main === undefined &&
|
|
129
|
+
result.pre === undefined &&
|
|
130
|
+
result.post === undefined &&
|
|
131
|
+
result.image === undefined &&
|
|
132
|
+
result.args === undefined &&
|
|
133
|
+
result.steps === undefined) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
export function extractActionFacts(parsed) {
|
|
139
|
+
const root = asRecord(parsed);
|
|
140
|
+
if (!root) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
const runs = extractRuns(root.runs);
|
|
144
|
+
const inputs = extractInputs(root.inputs);
|
|
145
|
+
const outputs = extractOutputs(root.outputs);
|
|
146
|
+
const branding = asRecord(root.branding) ?? undefined;
|
|
147
|
+
if (runs === undefined &&
|
|
148
|
+
inputs === undefined &&
|
|
149
|
+
outputs === undefined &&
|
|
150
|
+
branding === undefined &&
|
|
151
|
+
asString(root.name) === undefined &&
|
|
152
|
+
asString(root.description) === undefined &&
|
|
153
|
+
asString(root.author) === undefined) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
name: asString(root.name),
|
|
158
|
+
description: asString(root.description),
|
|
159
|
+
author: asString(root.author),
|
|
160
|
+
branding,
|
|
161
|
+
inputs,
|
|
162
|
+
outputs,
|
|
163
|
+
runs,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface ActionStepFacts {
|
|
2
|
+
id?: string;
|
|
3
|
+
name?: string;
|
|
4
|
+
uses?: string;
|
|
5
|
+
run?: string;
|
|
6
|
+
if?: string;
|
|
7
|
+
shell?: string;
|
|
8
|
+
workingDirectory?: string;
|
|
9
|
+
with?: Record<string, string>;
|
|
10
|
+
env?: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
export interface ActionRunsFacts {
|
|
13
|
+
using?: string;
|
|
14
|
+
main?: string;
|
|
15
|
+
pre?: string;
|
|
16
|
+
post?: string;
|
|
17
|
+
image?: string;
|
|
18
|
+
args?: string[];
|
|
19
|
+
steps?: ActionStepFacts[];
|
|
20
|
+
}
|
|
21
|
+
export interface ActionInputFacts {
|
|
22
|
+
description?: string;
|
|
23
|
+
required?: boolean;
|
|
24
|
+
default?: string;
|
|
25
|
+
deprecationMessage?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface ActionOutputFacts {
|
|
28
|
+
description?: string;
|
|
29
|
+
value?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface ActionFacts {
|
|
32
|
+
name?: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
author?: string;
|
|
35
|
+
branding?: Record<string, unknown>;
|
|
36
|
+
inputs?: Record<string, ActionInputFacts>;
|
|
37
|
+
outputs?: Record<string, ActionOutputFacts>;
|
|
38
|
+
runs?: ActionRunsFacts;
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|