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,17 @@
|
|
|
1
|
+
import type { RuntimeMode } from "../../config.js";
|
|
2
|
+
import { type AdvisoryPayload } from "./cache.js";
|
|
3
|
+
export interface GithubMetadataClientOptions {
|
|
4
|
+
runtimeMode?: RuntimeMode;
|
|
5
|
+
cacheDir?: string;
|
|
6
|
+
cacheMaxAgeMs?: number;
|
|
7
|
+
now?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface GithubMetadataClient {
|
|
10
|
+
runtimeMode: RuntimeMode;
|
|
11
|
+
cacheDir: string;
|
|
12
|
+
cacheMaxAgeMs: number;
|
|
13
|
+
isOnlineEnabled(): boolean;
|
|
14
|
+
loadKnownVulnerableActions(bundle: Record<string, string[]>): AdvisoryPayload;
|
|
15
|
+
}
|
|
16
|
+
export declare function createGithubMetadataClient(options?: GithubMetadataClientOptions): GithubMetadataClient;
|
|
17
|
+
export declare function loadBundledGithubAdvisories(options?: GithubMetadataClientOptions): AdvisoryPayload;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import bundledAdvisories from "../advisories/gha-known-vulnerable-actions.json" with { type: "json" };
|
|
4
|
+
import { loadCachedAdvisoryPayload, saveCachedAdvisoryPayload, } from "./cache.js";
|
|
5
|
+
function normalizeAdvisoryMap(value) {
|
|
6
|
+
const normalized = {};
|
|
7
|
+
for (const [action, versions] of Object.entries(value)) {
|
|
8
|
+
normalized[action.toLowerCase()] = versions.map((version) => version.toLowerCase());
|
|
9
|
+
}
|
|
10
|
+
return normalized;
|
|
11
|
+
}
|
|
12
|
+
export function createGithubMetadataClient(options = {}) {
|
|
13
|
+
const runtimeMode = options.runtimeMode ?? "offline";
|
|
14
|
+
const cacheDir = options.cacheDir ?? join(homedir(), ".codegate", "cache");
|
|
15
|
+
const cacheMaxAgeMs = options.cacheMaxAgeMs ?? 24 * 60 * 60 * 1000;
|
|
16
|
+
const now = options.now ?? Date.now();
|
|
17
|
+
return {
|
|
18
|
+
runtimeMode,
|
|
19
|
+
cacheDir,
|
|
20
|
+
cacheMaxAgeMs,
|
|
21
|
+
isOnlineEnabled() {
|
|
22
|
+
return runtimeMode === "online";
|
|
23
|
+
},
|
|
24
|
+
loadKnownVulnerableActions(bundle) {
|
|
25
|
+
const payload = {
|
|
26
|
+
generatedAt: now,
|
|
27
|
+
advisories: normalizeAdvisoryMap(bundle),
|
|
28
|
+
};
|
|
29
|
+
if (!runtimeMode || runtimeMode !== "online") {
|
|
30
|
+
return payload;
|
|
31
|
+
}
|
|
32
|
+
const cached = loadCachedAdvisoryPayload(cacheDir, cacheMaxAgeMs, now);
|
|
33
|
+
if (cached) {
|
|
34
|
+
return cached;
|
|
35
|
+
}
|
|
36
|
+
saveCachedAdvisoryPayload(cacheDir, payload);
|
|
37
|
+
return payload;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function loadBundledGithubAdvisories(options = {}) {
|
|
42
|
+
return createGithubMetadataClient(options).loadKnownVulnerableActions(bundledAdvisories);
|
|
43
|
+
}
|
|
@@ -21,4 +21,4 @@ export interface RuleEvaluationInput {
|
|
|
21
21
|
textContent: string;
|
|
22
22
|
}
|
|
23
23
|
export declare function evaluateRule(rule: DetectionRule, input: RuleEvaluationInput): boolean;
|
|
24
|
-
export
|
|
24
|
+
export { loadRulePacks } from "./rule-pack-loader.js";
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
import { readdirSync, readFileSync } from "node:fs";
|
|
2
|
-
import { dirname, extname, join, resolve } from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
const rulesDir = resolve(dirname(fileURLToPath(import.meta.url)), "rules");
|
|
5
1
|
function escapeRegex(value) {
|
|
6
2
|
return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
7
3
|
}
|
|
@@ -127,12 +123,4 @@ export function evaluateRule(rule, input) {
|
|
|
127
123
|
}
|
|
128
124
|
return false;
|
|
129
125
|
}
|
|
130
|
-
export
|
|
131
|
-
const files = readdirSync(baseDir)
|
|
132
|
-
.filter((file) => extname(file) === ".json")
|
|
133
|
-
.sort();
|
|
134
|
-
return files.flatMap((file) => {
|
|
135
|
-
const raw = readFileSync(join(baseDir, file), "utf8");
|
|
136
|
-
return JSON.parse(raw);
|
|
137
|
-
});
|
|
138
|
-
}
|
|
126
|
+
export { loadRulePacks } from "./rule-pack-loader.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DetectionRule } from "./rule-engine.js";
|
|
2
|
+
export interface RulePackLoaderOptions {
|
|
3
|
+
baseDir?: string;
|
|
4
|
+
rule_pack_paths?: string[];
|
|
5
|
+
allowed_rules?: string[];
|
|
6
|
+
skip_rules?: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function loadRulePacks(): DetectionRule[];
|
|
9
|
+
export declare function loadRulePacks(baseDir: string): DetectionRule[];
|
|
10
|
+
export declare function loadRulePacks(options: RulePackLoaderOptions): DetectionRule[];
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { dirname, extname, join, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
const defaultRulesDir = resolve(dirname(fileURLToPath(import.meta.url)), "rules");
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const Ajv = require("ajv");
|
|
8
|
+
const RULE_SCHEMA = {
|
|
9
|
+
type: "object",
|
|
10
|
+
additionalProperties: true,
|
|
11
|
+
required: [
|
|
12
|
+
"id",
|
|
13
|
+
"severity",
|
|
14
|
+
"category",
|
|
15
|
+
"description",
|
|
16
|
+
"tool",
|
|
17
|
+
"file_pattern",
|
|
18
|
+
"query_type",
|
|
19
|
+
"query",
|
|
20
|
+
"condition",
|
|
21
|
+
"owasp",
|
|
22
|
+
"cwe",
|
|
23
|
+
],
|
|
24
|
+
properties: {
|
|
25
|
+
id: { type: "string", minLength: 1 },
|
|
26
|
+
severity: { type: "string", minLength: 1 },
|
|
27
|
+
category: { type: "string", minLength: 1 },
|
|
28
|
+
description: { type: "string", minLength: 1 },
|
|
29
|
+
tool: { type: "string", minLength: 1 },
|
|
30
|
+
file_pattern: { type: "string", minLength: 1 },
|
|
31
|
+
query_type: {
|
|
32
|
+
type: "string",
|
|
33
|
+
enum: ["json_path", "toml_path", "env_key", "text_pattern"],
|
|
34
|
+
},
|
|
35
|
+
query: { type: "string" },
|
|
36
|
+
condition: {
|
|
37
|
+
type: "string",
|
|
38
|
+
enum: [
|
|
39
|
+
"equals_true",
|
|
40
|
+
"equals_false",
|
|
41
|
+
"exists",
|
|
42
|
+
"not_empty",
|
|
43
|
+
"matches_regex",
|
|
44
|
+
"not_in_allowlist",
|
|
45
|
+
"regex_match",
|
|
46
|
+
"contains",
|
|
47
|
+
"line_length_exceeds",
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
cve: { type: "string" },
|
|
51
|
+
owasp: {
|
|
52
|
+
type: "array",
|
|
53
|
+
items: { type: "string" },
|
|
54
|
+
},
|
|
55
|
+
cwe: { type: "string", minLength: 1 },
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
const ruleValidator = new Ajv({ allErrors: true, strict: false }).compile(RULE_SCHEMA);
|
|
59
|
+
function normalizeRuleIds(values) {
|
|
60
|
+
const seen = new Set();
|
|
61
|
+
const normalized = [];
|
|
62
|
+
for (const value of values ?? []) {
|
|
63
|
+
const trimmed = value.trim();
|
|
64
|
+
if (trimmed.length === 0 || seen.has(trimmed)) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
seen.add(trimmed);
|
|
68
|
+
normalized.push(trimmed);
|
|
69
|
+
}
|
|
70
|
+
return normalized;
|
|
71
|
+
}
|
|
72
|
+
function toErrorMessage(errors) {
|
|
73
|
+
if (!errors || errors.length === 0) {
|
|
74
|
+
return "validation error";
|
|
75
|
+
}
|
|
76
|
+
return errors
|
|
77
|
+
.map((error) => {
|
|
78
|
+
const location = error.instancePath === "" ? "<root>" : error.instancePath;
|
|
79
|
+
return `${location}: ${error.message ?? "validation error"}`;
|
|
80
|
+
})
|
|
81
|
+
.join("; ");
|
|
82
|
+
}
|
|
83
|
+
function isPackDirectory(path) {
|
|
84
|
+
try {
|
|
85
|
+
return statSync(path).isDirectory();
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function isPackFile(path) {
|
|
92
|
+
try {
|
|
93
|
+
return statSync(path).isFile();
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function resolvePackPaths(path) {
|
|
100
|
+
const absolutePath = resolve(path);
|
|
101
|
+
if (!existsSync(absolutePath)) {
|
|
102
|
+
throw new Error(`Rule pack path does not exist: ${absolutePath}`);
|
|
103
|
+
}
|
|
104
|
+
if (isPackFile(absolutePath)) {
|
|
105
|
+
return [absolutePath];
|
|
106
|
+
}
|
|
107
|
+
if (!isPackDirectory(absolutePath)) {
|
|
108
|
+
throw new Error(`Rule pack path is not a file or directory: ${absolutePath}`);
|
|
109
|
+
}
|
|
110
|
+
return readdirSync(absolutePath)
|
|
111
|
+
.filter((file) => extname(file) === ".json")
|
|
112
|
+
.filter((file) => file !== "schema.json")
|
|
113
|
+
.sort()
|
|
114
|
+
.map((file) => join(absolutePath, file));
|
|
115
|
+
}
|
|
116
|
+
function loadRulesFromFile(path) {
|
|
117
|
+
let parsed;
|
|
118
|
+
try {
|
|
119
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
123
|
+
throw new Error(`Failed to parse rule pack ${path}: ${reason}`, { cause: error });
|
|
124
|
+
}
|
|
125
|
+
if (!Array.isArray(parsed)) {
|
|
126
|
+
throw new Error(`Invalid rule pack ${path}: expected a JSON array of rule objects`);
|
|
127
|
+
}
|
|
128
|
+
return parsed.map((candidate, index) => {
|
|
129
|
+
if (!ruleValidator(candidate)) {
|
|
130
|
+
const reasons = toErrorMessage(ruleValidator.errors);
|
|
131
|
+
throw new Error(`Invalid rule pack ${path} [${index}]: ${reasons}`);
|
|
132
|
+
}
|
|
133
|
+
return candidate;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function collectRulesFromPaths(paths) {
|
|
137
|
+
const collected = [];
|
|
138
|
+
for (const path of paths) {
|
|
139
|
+
for (const packPath of resolvePackPaths(path)) {
|
|
140
|
+
collected.push(...loadRulesFromFile(packPath));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return collected;
|
|
144
|
+
}
|
|
145
|
+
function dedupeByRuleId(rules) {
|
|
146
|
+
const deduped = new Map();
|
|
147
|
+
for (const rule of rules) {
|
|
148
|
+
deduped.set(rule.id, rule);
|
|
149
|
+
}
|
|
150
|
+
return Array.from(deduped.values());
|
|
151
|
+
}
|
|
152
|
+
function filterRules(rules, allowedRules, skipRules) {
|
|
153
|
+
const allowed = new Set(allowedRules);
|
|
154
|
+
const skipped = new Set(skipRules);
|
|
155
|
+
return rules.filter((rule) => {
|
|
156
|
+
if (skipped.has(rule.id)) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
if (allowed.size > 0 && !allowed.has(rule.id)) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
return true;
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function normalizeOptions(arg) {
|
|
166
|
+
if (typeof arg === "string") {
|
|
167
|
+
return {
|
|
168
|
+
baseDir: arg,
|
|
169
|
+
rulePackPaths: [],
|
|
170
|
+
allowedRules: [],
|
|
171
|
+
skipRules: [],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const options = arg ?? {};
|
|
175
|
+
return {
|
|
176
|
+
baseDir: options.baseDir ?? defaultRulesDir,
|
|
177
|
+
rulePackPaths: options.rule_pack_paths ?? [],
|
|
178
|
+
allowedRules: normalizeRuleIds(options.allowed_rules),
|
|
179
|
+
skipRules: normalizeRuleIds(options.skip_rules),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
export function loadRulePacks(arg) {
|
|
183
|
+
const options = normalizeOptions(arg);
|
|
184
|
+
const bundledRules = collectRulesFromPaths([options.baseDir]);
|
|
185
|
+
const externalRules = collectRulesFromPaths(options.rulePackPaths);
|
|
186
|
+
return filterRules(dedupeByRuleId([...bundledRules, ...externalRules]), options.allowedRules, options.skipRules);
|
|
187
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
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 normalizeWorkflowPath(value) {
|
|
11
|
+
return value.replaceAll("\\", "/");
|
|
12
|
+
}
|
|
13
|
+
export function isGitHubWorkflowPath(path) {
|
|
14
|
+
return /(?:^|\/)\.github\/workflows\/[^/]+\.ya?ml$/iu.test(normalizeWorkflowPath(path));
|
|
15
|
+
}
|
|
16
|
+
function extractTriggers(value) {
|
|
17
|
+
if (typeof value === "string") {
|
|
18
|
+
return [value];
|
|
19
|
+
}
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
return value.filter((entry) => typeof entry === "string");
|
|
22
|
+
}
|
|
23
|
+
const record = asRecord(value);
|
|
24
|
+
if (!record) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
return Object.keys(record);
|
|
28
|
+
}
|
|
29
|
+
function extractStepFacts(step) {
|
|
30
|
+
const stepRecord = asRecord(step);
|
|
31
|
+
if (!stepRecord) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const withValues = asRecord(stepRecord.with);
|
|
35
|
+
const withEntries = {};
|
|
36
|
+
if (withValues) {
|
|
37
|
+
for (const [key, value] of Object.entries(withValues)) {
|
|
38
|
+
if (typeof value === "string") {
|
|
39
|
+
withEntries[key] = value;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const stepFacts = {
|
|
44
|
+
uses: asString(stepRecord.uses),
|
|
45
|
+
run: asString(stepRecord.run),
|
|
46
|
+
with: Object.keys(withEntries).length > 0 ? withEntries : undefined,
|
|
47
|
+
};
|
|
48
|
+
if (!stepFacts.uses && !stepFacts.run) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return stepFacts;
|
|
52
|
+
}
|
|
53
|
+
function extractJobFacts(id, value) {
|
|
54
|
+
const jobRecord = asRecord(value);
|
|
55
|
+
if (!jobRecord) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const stepsRaw = Array.isArray(jobRecord.steps) ? jobRecord.steps : [];
|
|
59
|
+
const steps = stepsRaw
|
|
60
|
+
.map((step) => extractStepFacts(step))
|
|
61
|
+
.filter((step) => step !== null);
|
|
62
|
+
return {
|
|
63
|
+
id,
|
|
64
|
+
permissions: jobRecord.permissions,
|
|
65
|
+
steps,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export function extractWorkflowFacts(parsed) {
|
|
69
|
+
const root = asRecord(parsed);
|
|
70
|
+
if (!root) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const triggers = extractTriggers(root.on);
|
|
74
|
+
const jobsRecord = asRecord(root.jobs);
|
|
75
|
+
const jobs = jobsRecord
|
|
76
|
+
? Object.entries(jobsRecord)
|
|
77
|
+
.map(([id, value]) => extractJobFacts(id, value))
|
|
78
|
+
.filter((job) => job !== null)
|
|
79
|
+
: [];
|
|
80
|
+
if (triggers.length === 0 && jobs.length === 0) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
triggers,
|
|
85
|
+
workflowPermissions: root.permissions,
|
|
86
|
+
jobs,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface WorkflowStepFacts {
|
|
2
|
+
uses?: string;
|
|
3
|
+
run?: string;
|
|
4
|
+
with?: Record<string, string>;
|
|
5
|
+
}
|
|
6
|
+
export interface WorkflowJobFacts {
|
|
7
|
+
id: string;
|
|
8
|
+
permissions?: unknown;
|
|
9
|
+
steps: WorkflowStepFacts[];
|
|
10
|
+
}
|
|
11
|
+
export interface WorkflowFacts {
|
|
12
|
+
triggers: string[];
|
|
13
|
+
workflowPermissions?: unknown;
|
|
14
|
+
jobs: WorkflowJobFacts[];
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -42,6 +42,12 @@ function makeFinding(input, sourceTool, sensitiveTool, sinkTool) {
|
|
|
42
42
|
confidence: "HIGH",
|
|
43
43
|
fixable: false,
|
|
44
44
|
remediation_actions: [],
|
|
45
|
+
metadata: {
|
|
46
|
+
sources: [sourceTool],
|
|
47
|
+
sinks: [sinkTool],
|
|
48
|
+
risk_tags: ["toxic-flow"],
|
|
49
|
+
origin: "toxic-flow",
|
|
50
|
+
},
|
|
45
51
|
suppressed: false,
|
|
46
52
|
};
|
|
47
53
|
}
|
package/dist/pipeline.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export interface DeepScanOutcome {
|
|
|
26
26
|
status: "skipped_without_consent" | "ok" | "auth_failure" | "timeout" | "network_error" | "command_error";
|
|
27
27
|
result?: ResourceFetchResult;
|
|
28
28
|
}
|
|
29
|
-
export declare function runStaticPipeline(input: StaticPipelineInput): CodeGateReport
|
|
29
|
+
export declare function runStaticPipeline(input: StaticPipelineInput): Promise<CodeGateReport>;
|
|
30
30
|
export declare function layer3OutcomesToFindings(outcomes: DeepScanOutcome[], options?: {
|
|
31
31
|
unicodeAnalysis?: boolean;
|
|
32
32
|
}): Finding[];
|
package/dist/pipeline.js
CHANGED
|
@@ -3,14 +3,15 @@ import { createEmptyReport } from "./types/report.js";
|
|
|
3
3
|
import { scanToolDescriptions, } from "./layer3-dynamic/tool-description-scanner.js";
|
|
4
4
|
import { detectToxicFlows } from "./layer3-dynamic/toxic-flow.js";
|
|
5
5
|
import { applyReportSummary } from "./report-summary.js";
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
import { withFindingFingerprint } from "./report/finding-fingerprint.js";
|
|
7
|
+
export async function runStaticPipeline(input) {
|
|
8
|
+
const findings = (await runStaticEngine({
|
|
8
9
|
projectRoot: input.projectRoot,
|
|
9
10
|
files: input.files,
|
|
10
11
|
symlinkEscapes: input.symlinkEscapes,
|
|
11
12
|
hooks: input.hooks,
|
|
12
13
|
config: input.config,
|
|
13
|
-
});
|
|
14
|
+
})).map(withFindingFingerprint);
|
|
14
15
|
const report = createEmptyReport({
|
|
15
16
|
version: input.version,
|
|
16
17
|
kbVersion: input.kbVersion,
|
|
@@ -63,7 +64,7 @@ function parseLayer3Response(resourceId, metadata) {
|
|
|
63
64
|
.filter((item) => typeof item === "object" && item !== null)
|
|
64
65
|
.map((item, index) => {
|
|
65
66
|
const findingId = item.id ?? `L3-${resourceId}-${index}`;
|
|
66
|
-
return {
|
|
67
|
+
return withFindingFingerprint({
|
|
67
68
|
rule_id: item.id ?? "layer3-analysis-finding",
|
|
68
69
|
finding_id: findingId,
|
|
69
70
|
severity: parseSeverity(item.severity),
|
|
@@ -82,7 +83,7 @@ function parseLayer3Response(resourceId, metadata) {
|
|
|
82
83
|
remediation_actions: item.remediation_actions ?? [],
|
|
83
84
|
source_config: item.source_config ?? null,
|
|
84
85
|
suppressed: false,
|
|
85
|
-
};
|
|
86
|
+
});
|
|
86
87
|
});
|
|
87
88
|
}
|
|
88
89
|
function asRecord(value) {
|
|
@@ -172,17 +173,17 @@ function deriveLayer3ToolFindings(resourceId, metadata, options = {}) {
|
|
|
172
173
|
serverId: resourceId,
|
|
173
174
|
tools: toolDescriptions,
|
|
174
175
|
unicodeAnalysis: options.unicodeAnalysis,
|
|
175
|
-
}),
|
|
176
|
+
}).map(withFindingFingerprint),
|
|
176
177
|
...detectToxicFlows({
|
|
177
178
|
scopeId: resourceId,
|
|
178
179
|
tools: toolDescriptions,
|
|
179
180
|
knownClassifications,
|
|
180
|
-
}),
|
|
181
|
+
}).map(withFindingFingerprint),
|
|
181
182
|
];
|
|
182
183
|
}
|
|
183
184
|
function layer3ErrorFinding(resourceId, status, description) {
|
|
184
185
|
const severity = status === "timeout" ? "MEDIUM" : status === "skipped_without_consent" ? "INFO" : "LOW";
|
|
185
|
-
return {
|
|
186
|
+
return withFindingFingerprint({
|
|
186
187
|
rule_id: `layer3-${status}`,
|
|
187
188
|
finding_id: `L3-${status}-${resourceId}`,
|
|
188
189
|
severity,
|
|
@@ -199,7 +200,7 @@ function layer3ErrorFinding(resourceId, status, description) {
|
|
|
199
200
|
fixable: false,
|
|
200
201
|
remediation_actions: [],
|
|
201
202
|
suppressed: false,
|
|
202
|
-
};
|
|
203
|
+
});
|
|
203
204
|
}
|
|
204
205
|
function isRegistryMetadataResource(resourceId) {
|
|
205
206
|
return (resourceId.startsWith("npm:") || resourceId.startsWith("pypi:") || resourceId.startsWith("git:"));
|
|
@@ -232,7 +233,7 @@ export function layer3OutcomesToFindings(outcomes, options = {}) {
|
|
|
232
233
|
export function mergeLayer3Findings(baseReport, layer3Findings) {
|
|
233
234
|
return applyReportSummary({
|
|
234
235
|
...baseReport,
|
|
235
|
-
findings: [...baseReport.findings, ...layer3Findings],
|
|
236
|
+
findings: [...baseReport.findings, ...layer3Findings].map(withFindingFingerprint),
|
|
236
237
|
});
|
|
237
238
|
}
|
|
238
239
|
export async function runDeepScanWithConsent(resources, requestConsent, execute) {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
function normalizeLocation(location) {
|
|
3
|
+
const normalized = {};
|
|
4
|
+
if (typeof location.field === "string" && location.field.length > 0) {
|
|
5
|
+
normalized.field = location.field;
|
|
6
|
+
}
|
|
7
|
+
if (typeof location.line === "number") {
|
|
8
|
+
normalized.line = location.line;
|
|
9
|
+
}
|
|
10
|
+
if (typeof location.column === "number") {
|
|
11
|
+
normalized.column = location.column;
|
|
12
|
+
}
|
|
13
|
+
return normalized;
|
|
14
|
+
}
|
|
15
|
+
function normalizeSourceConfig(sourceConfig) {
|
|
16
|
+
if (!sourceConfig) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const normalized = {
|
|
20
|
+
file_path: sourceConfig.file_path,
|
|
21
|
+
};
|
|
22
|
+
if (typeof sourceConfig.field === "string" && sourceConfig.field.length > 0) {
|
|
23
|
+
normalized.field = sourceConfig.field;
|
|
24
|
+
}
|
|
25
|
+
return normalized;
|
|
26
|
+
}
|
|
27
|
+
function buildFingerprintPayload(finding) {
|
|
28
|
+
return {
|
|
29
|
+
rule_id: finding.rule_id,
|
|
30
|
+
category: finding.category,
|
|
31
|
+
layer: finding.layer,
|
|
32
|
+
file_path: finding.file_path,
|
|
33
|
+
location: normalizeLocation(finding.location),
|
|
34
|
+
source_config: normalizeSourceConfig(finding.source_config),
|
|
35
|
+
cwe: finding.cwe,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function buildFindingFingerprint(finding) {
|
|
39
|
+
const payload = JSON.stringify(buildFingerprintPayload(finding));
|
|
40
|
+
return `sha256:${createHash("sha256").update(payload).digest("hex")}`;
|
|
41
|
+
}
|
|
42
|
+
export function withFindingFingerprint(finding) {
|
|
43
|
+
return {
|
|
44
|
+
...finding,
|
|
45
|
+
fingerprint: buildFindingFingerprint(finding),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -14,6 +14,28 @@ function formatLocation(location) {
|
|
|
14
14
|
}
|
|
15
15
|
return parts.join(", ") || "-";
|
|
16
16
|
}
|
|
17
|
+
function formatMetadata(metadata) {
|
|
18
|
+
if (!metadata) {
|
|
19
|
+
return "-";
|
|
20
|
+
}
|
|
21
|
+
const parts = [];
|
|
22
|
+
if (metadata.sources && metadata.sources.length > 0) {
|
|
23
|
+
parts.push(`sources=${metadata.sources.join(", ")}`);
|
|
24
|
+
}
|
|
25
|
+
if (metadata.sinks && metadata.sinks.length > 0) {
|
|
26
|
+
parts.push(`sinks=${metadata.sinks.join(", ")}`);
|
|
27
|
+
}
|
|
28
|
+
if (metadata.referenced_secrets && metadata.referenced_secrets.length > 0) {
|
|
29
|
+
parts.push(`referenced_secrets=${metadata.referenced_secrets.join(", ")}`);
|
|
30
|
+
}
|
|
31
|
+
if (metadata.risk_tags && metadata.risk_tags.length > 0) {
|
|
32
|
+
parts.push(`risk_tags=${metadata.risk_tags.join(", ")}`);
|
|
33
|
+
}
|
|
34
|
+
if (metadata.origin) {
|
|
35
|
+
parts.push(`origin=${metadata.origin}`);
|
|
36
|
+
}
|
|
37
|
+
return parts.length > 0 ? parts.join("; ") : "-";
|
|
38
|
+
}
|
|
17
39
|
export function renderMarkdownReport(report) {
|
|
18
40
|
const lines = [];
|
|
19
41
|
lines.push("# CodeGate Report");
|
|
@@ -43,10 +65,10 @@ export function renderMarkdownReport(report) {
|
|
|
43
65
|
lines.push("No findings.");
|
|
44
66
|
return lines.join("\n");
|
|
45
67
|
}
|
|
46
|
-
lines.push("| Severity | Category | File | Location | Description |");
|
|
47
|
-
lines.push("| --- | --- | --- | --- | --- |");
|
|
68
|
+
lines.push("| Severity | Category | File | Location | Description | Fingerprint | Metadata |");
|
|
69
|
+
lines.push("| --- | --- | --- | --- | --- | --- | --- |");
|
|
48
70
|
for (const finding of report.findings) {
|
|
49
|
-
lines.push(`| ${finding.severity} | ${finding.category} | \`${escapePipes(finding.file_path)}\` | ${escapePipes(formatLocation(finding.location))} | ${escapePipes(finding.description)} |`);
|
|
71
|
+
lines.push(`| ${finding.severity} | ${finding.category} | \`${escapePipes(finding.file_path)}\` | ${escapePipes(formatLocation(finding.location))} | ${escapePipes(finding.description)} | ${escapePipes(finding.fingerprint ?? "-")} | ${escapePipes(formatMetadata(finding.metadata))} |`);
|
|
50
72
|
}
|
|
51
73
|
return lines.join("\n");
|
|
52
74
|
}
|
package/dist/reporter/sarif.js
CHANGED
|
@@ -8,22 +8,39 @@ function toSarifLevel(severity) {
|
|
|
8
8
|
return "note";
|
|
9
9
|
}
|
|
10
10
|
function findingToLocation(finding) {
|
|
11
|
+
return locationToSarif({
|
|
12
|
+
filePath: finding.file_path,
|
|
13
|
+
location: finding.location,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function locationToSarif(input) {
|
|
11
17
|
const region = {};
|
|
12
|
-
if (typeof
|
|
13
|
-
region.startLine =
|
|
18
|
+
if (typeof input.location?.line === "number") {
|
|
19
|
+
region.startLine = input.location.line;
|
|
14
20
|
}
|
|
15
|
-
if (typeof
|
|
16
|
-
region.startColumn =
|
|
21
|
+
if (typeof input.location?.column === "number") {
|
|
22
|
+
region.startColumn = input.location.column;
|
|
17
23
|
}
|
|
18
24
|
return {
|
|
19
25
|
physicalLocation: {
|
|
20
26
|
artifactLocation: {
|
|
21
|
-
uri:
|
|
27
|
+
uri: input.filePath,
|
|
22
28
|
},
|
|
23
29
|
region: Object.keys(region).length > 0 ? region : undefined,
|
|
24
30
|
},
|
|
25
31
|
};
|
|
26
32
|
}
|
|
33
|
+
function findingToRelatedLocations(finding) {
|
|
34
|
+
const related = finding.affected_locations ?? [];
|
|
35
|
+
if (related.length === 0) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
const locations = related.map((location) => locationToSarif({
|
|
39
|
+
filePath: location.file_path,
|
|
40
|
+
location: location.location,
|
|
41
|
+
}));
|
|
42
|
+
return locations.length > 0 ? locations : undefined;
|
|
43
|
+
}
|
|
27
44
|
function buildRules(findings) {
|
|
28
45
|
const byRuleId = new Map();
|
|
29
46
|
for (const finding of findings) {
|
|
@@ -47,8 +64,10 @@ function findingToResult(finding) {
|
|
|
47
64
|
level: toSarifLevel(finding.severity),
|
|
48
65
|
message: { text: finding.description },
|
|
49
66
|
locations: [findingToLocation(finding)],
|
|
67
|
+
relatedLocations: findingToRelatedLocations(finding),
|
|
50
68
|
properties: {
|
|
51
69
|
finding_id: finding.finding_id,
|
|
70
|
+
fingerprint: finding.fingerprint ?? null,
|
|
52
71
|
severity: finding.severity,
|
|
53
72
|
category: finding.category,
|
|
54
73
|
layer: finding.layer,
|
|
@@ -59,6 +78,7 @@ function findingToResult(finding) {
|
|
|
59
78
|
evidence: finding.evidence ?? null,
|
|
60
79
|
fixable: finding.fixable,
|
|
61
80
|
suppressed: finding.suppressed,
|
|
81
|
+
metadata: finding.metadata ?? null,
|
|
62
82
|
source_config: finding.source_config ?? null,
|
|
63
83
|
},
|
|
64
84
|
};
|