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.
Files changed (135) hide show
  1. package/README.md +109 -40
  2. package/dist/cli.js +85 -7
  3. package/dist/commands/scan-command.d.ts +6 -1
  4. package/dist/commands/scan-command.js +1 -1
  5. package/dist/commands/scan-content-command.d.ts +16 -0
  6. package/dist/commands/scan-content-command.js +65 -0
  7. package/dist/config/inline-ignore.d.ts +11 -0
  8. package/dist/config/inline-ignore.js +57 -0
  9. package/dist/config/suppression-policy.d.ts +22 -0
  10. package/dist/config/suppression-policy.js +146 -0
  11. package/dist/config.d.ts +23 -0
  12. package/dist/config.js +188 -3
  13. package/dist/layer2-static/action/parser.d.ts +3 -0
  14. package/dist/layer2-static/action/parser.js +165 -0
  15. package/dist/layer2-static/action/types.d.ts +39 -0
  16. package/dist/layer2-static/action/types.js +1 -0
  17. package/dist/layer2-static/advisories/agent-components.json +62 -0
  18. package/dist/layer2-static/advisories/cache.d.ts +6 -0
  19. package/dist/layer2-static/advisories/cache.js +44 -0
  20. package/dist/layer2-static/advisories/cache.ts +62 -0
  21. package/dist/layer2-static/advisories/gha-advisory-client.d.ts +7 -0
  22. package/dist/layer2-static/advisories/gha-advisory-client.js +4 -0
  23. package/dist/layer2-static/advisories/gha-advisory-client.ts +13 -0
  24. package/dist/layer2-static/advisories/gha-known-vulnerable-actions.json +4 -0
  25. package/dist/layer2-static/audits/registry.d.ts +3 -0
  26. package/dist/layer2-static/audits/registry.js +29 -0
  27. package/dist/layer2-static/audits/types.d.ts +12 -0
  28. package/dist/layer2-static/audits/types.js +1 -0
  29. package/dist/layer2-static/dependabot/parser.d.ts +3 -0
  30. package/dist/layer2-static/dependabot/parser.js +189 -0
  31. package/dist/layer2-static/dependabot/types.d.ts +50 -0
  32. package/dist/layer2-static/dependabot/types.js +1 -0
  33. package/dist/layer2-static/detectors/advisory-intelligence.d.ts +7 -0
  34. package/dist/layer2-static/detectors/advisory-intelligence.js +170 -0
  35. package/dist/layer2-static/detectors/command-exec.js +6 -0
  36. package/dist/layer2-static/detectors/dependabot-cooldown.d.ts +7 -0
  37. package/dist/layer2-static/detectors/dependabot-cooldown.js +54 -0
  38. package/dist/layer2-static/detectors/dependabot-execution.d.ts +7 -0
  39. package/dist/layer2-static/detectors/dependabot-execution.js +68 -0
  40. package/dist/layer2-static/detectors/rule-file.js +5 -0
  41. package/dist/layer2-static/detectors/workflow-anonymous-definition.d.ts +7 -0
  42. package/dist/layer2-static/detectors/workflow-anonymous-definition.js +48 -0
  43. package/dist/layer2-static/detectors/workflow-archived-uses.d.ts +7 -0
  44. package/dist/layer2-static/detectors/workflow-archived-uses.js +147 -0
  45. package/dist/layer2-static/detectors/workflow-artipacked.d.ts +7 -0
  46. package/dist/layer2-static/detectors/workflow-artipacked.js +62 -0
  47. package/dist/layer2-static/detectors/workflow-bot-conditions.d.ts +7 -0
  48. package/dist/layer2-static/detectors/workflow-bot-conditions.js +108 -0
  49. package/dist/layer2-static/detectors/workflow-cache-poisoning.d.ts +7 -0
  50. package/dist/layer2-static/detectors/workflow-cache-poisoning.js +67 -0
  51. package/dist/layer2-static/detectors/workflow-concurrency-limits.d.ts +7 -0
  52. package/dist/layer2-static/detectors/workflow-concurrency-limits.js +76 -0
  53. package/dist/layer2-static/detectors/workflow-dangerous-triggers.d.ts +6 -0
  54. package/dist/layer2-static/detectors/workflow-dangerous-triggers.js +38 -0
  55. package/dist/layer2-static/detectors/workflow-excessive-permissions.d.ts +6 -0
  56. package/dist/layer2-static/detectors/workflow-excessive-permissions.js +65 -0
  57. package/dist/layer2-static/detectors/workflow-forbidden-uses.d.ts +12 -0
  58. package/dist/layer2-static/detectors/workflow-forbidden-uses.js +148 -0
  59. package/dist/layer2-static/detectors/workflow-github-env.d.ts +7 -0
  60. package/dist/layer2-static/detectors/workflow-github-env.js +49 -0
  61. package/dist/layer2-static/detectors/workflow-hardcoded-container-credentials.d.ts +7 -0
  62. package/dist/layer2-static/detectors/workflow-hardcoded-container-credentials.js +90 -0
  63. package/dist/layer2-static/detectors/workflow-impostor-commit.d.ts +9 -0
  64. package/dist/layer2-static/detectors/workflow-impostor-commit.js +127 -0
  65. package/dist/layer2-static/detectors/workflow-insecure-commands.d.ts +7 -0
  66. package/dist/layer2-static/detectors/workflow-insecure-commands.js +72 -0
  67. package/dist/layer2-static/detectors/workflow-known-vuln-action.d.ts +8 -0
  68. package/dist/layer2-static/detectors/workflow-known-vuln-action.js +70 -0
  69. package/dist/layer2-static/detectors/workflow-misfeature.d.ts +7 -0
  70. package/dist/layer2-static/detectors/workflow-misfeature.js +101 -0
  71. package/dist/layer2-static/detectors/workflow-obfuscation.d.ts +7 -0
  72. package/dist/layer2-static/detectors/workflow-obfuscation.js +79 -0
  73. package/dist/layer2-static/detectors/workflow-overprovisioned-secrets.d.ts +7 -0
  74. package/dist/layer2-static/detectors/workflow-overprovisioned-secrets.js +81 -0
  75. package/dist/layer2-static/detectors/workflow-ref-confusion.d.ts +7 -0
  76. package/dist/layer2-static/detectors/workflow-ref-confusion.js +69 -0
  77. package/dist/layer2-static/detectors/workflow-ref-version-mismatch.d.ts +9 -0
  78. package/dist/layer2-static/detectors/workflow-ref-version-mismatch.js +179 -0
  79. package/dist/layer2-static/detectors/workflow-secrets-inherit.d.ts +7 -0
  80. package/dist/layer2-static/detectors/workflow-secrets-inherit.js +59 -0
  81. package/dist/layer2-static/detectors/workflow-secrets-outside-env.d.ts +7 -0
  82. package/dist/layer2-static/detectors/workflow-secrets-outside-env.js +99 -0
  83. package/dist/layer2-static/detectors/workflow-self-hosted-runner.d.ts +7 -0
  84. package/dist/layer2-static/detectors/workflow-self-hosted-runner.js +67 -0
  85. package/dist/layer2-static/detectors/workflow-stale-action-refs.d.ts +7 -0
  86. package/dist/layer2-static/detectors/workflow-stale-action-refs.js +178 -0
  87. package/dist/layer2-static/detectors/workflow-superfluous-actions.d.ts +7 -0
  88. package/dist/layer2-static/detectors/workflow-superfluous-actions.js +70 -0
  89. package/dist/layer2-static/detectors/workflow-template-injection.d.ts +6 -0
  90. package/dist/layer2-static/detectors/workflow-template-injection.js +94 -0
  91. package/dist/layer2-static/detectors/workflow-undocumented-permissions.d.ts +7 -0
  92. package/dist/layer2-static/detectors/workflow-undocumented-permissions.js +101 -0
  93. package/dist/layer2-static/detectors/workflow-unpinned-images.d.ts +7 -0
  94. package/dist/layer2-static/detectors/workflow-unpinned-images.js +108 -0
  95. package/dist/layer2-static/detectors/workflow-unpinned-uses.d.ts +7 -0
  96. package/dist/layer2-static/detectors/workflow-unpinned-uses.js +60 -0
  97. package/dist/layer2-static/detectors/workflow-unredacted-secrets.d.ts +7 -0
  98. package/dist/layer2-static/detectors/workflow-unredacted-secrets.js +92 -0
  99. package/dist/layer2-static/detectors/workflow-unsound-condition.d.ts +7 -0
  100. package/dist/layer2-static/detectors/workflow-unsound-condition.js +90 -0
  101. package/dist/layer2-static/detectors/workflow-unsound-contains.d.ts +7 -0
  102. package/dist/layer2-static/detectors/workflow-unsound-contains.js +88 -0
  103. package/dist/layer2-static/detectors/workflow-use-trusted-publishing.d.ts +7 -0
  104. package/dist/layer2-static/detectors/workflow-use-trusted-publishing.js +66 -0
  105. package/dist/layer2-static/engine.d.ts +13 -2
  106. package/dist/layer2-static/engine.js +578 -41
  107. package/dist/layer2-static/github/cache.d.ts +8 -0
  108. package/dist/layer2-static/github/cache.js +41 -0
  109. package/dist/layer2-static/github/client.d.ts +17 -0
  110. package/dist/layer2-static/github/client.js +43 -0
  111. package/dist/layer2-static/rule-engine.d.ts +1 -1
  112. package/dist/layer2-static/rule-engine.js +1 -13
  113. package/dist/layer2-static/rule-pack-loader.d.ts +10 -0
  114. package/dist/layer2-static/rule-pack-loader.js +187 -0
  115. package/dist/layer2-static/workflow/injection-sinks.json +5 -0
  116. package/dist/layer2-static/workflow/parser.d.ts +3 -0
  117. package/dist/layer2-static/workflow/parser.js +88 -0
  118. package/dist/layer2-static/workflow/types.d.ts +15 -0
  119. package/dist/layer2-static/workflow/types.js +1 -0
  120. package/dist/layer3-dynamic/toxic-flow.js +6 -0
  121. package/dist/pipeline.d.ts +1 -1
  122. package/dist/pipeline.js +11 -10
  123. package/dist/report/finding-fingerprint.d.ts +5 -0
  124. package/dist/report/finding-fingerprint.js +47 -0
  125. package/dist/reporter/markdown.js +25 -3
  126. package/dist/reporter/sarif.js +25 -5
  127. package/dist/reporter/terminal.js +37 -0
  128. package/dist/scan-target/fetch-plan.d.ts +8 -0
  129. package/dist/scan-target/fetch-plan.js +30 -0
  130. package/dist/scan-target/staging.js +60 -5
  131. package/dist/scan.d.ts +7 -1
  132. package/dist/scan.js +157 -40
  133. package/dist/types/finding.d.ts +10 -1
  134. package/dist/types/finding.js +5 -0
  135. 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 declare function loadRulePacks(baseDir?: string): DetectionRule[];
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 function loadRulePacks(baseDir = rulesDir) {
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,5 @@
1
+ {
2
+ "actions/github-script": ["script"],
3
+ "azure/powershell": ["inlineScript"],
4
+ "addnab/docker-run-action": ["run", "options"]
5
+ }
@@ -0,0 +1,3 @@
1
+ import type { WorkflowFacts } from "./types.js";
2
+ export declare function isGitHubWorkflowPath(path: string): boolean;
3
+ export declare function extractWorkflowFacts(parsed: unknown): WorkflowFacts | null;
@@ -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
  }
@@ -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
- export function runStaticPipeline(input) {
7
- const findings = runStaticEngine({
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,5 @@
1
+ import type { Finding } from "../types/finding.js";
2
+ export declare function buildFindingFingerprint(finding: Finding): string;
3
+ export declare function withFindingFingerprint<T extends Finding>(finding: T): T & {
4
+ fingerprint: string;
5
+ };
@@ -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
  }
@@ -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 finding.location.line === "number") {
13
- region.startLine = finding.location.line;
18
+ if (typeof input.location?.line === "number") {
19
+ region.startLine = input.location.line;
14
20
  }
15
- if (typeof finding.location.column === "number") {
16
- region.startColumn = finding.location.column;
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: finding.file_path,
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
  };