codegate-ai 0.1.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 (147) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +390 -0
  3. package/dist/cli-prompts.d.ts +6 -0
  4. package/dist/cli-prompts.js +94 -0
  5. package/dist/cli.d.ts +64 -0
  6. package/dist/cli.js +443 -0
  7. package/dist/commands/run-policy.d.ts +27 -0
  8. package/dist/commands/run-policy.js +39 -0
  9. package/dist/commands/scan-command/helpers.d.ts +28 -0
  10. package/dist/commands/scan-command/helpers.js +233 -0
  11. package/dist/commands/scan-command.d.ts +90 -0
  12. package/dist/commands/scan-command.js +403 -0
  13. package/dist/commands/undo.d.ts +5 -0
  14. package/dist/commands/undo.js +14 -0
  15. package/dist/config.d.ts +50 -0
  16. package/dist/config.js +187 -0
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.js +1 -0
  19. package/dist/knowledge-base/claude-code.json +152 -0
  20. package/dist/knowledge-base/cline.json +224 -0
  21. package/dist/knowledge-base/codex.json +162 -0
  22. package/dist/knowledge-base/copilot.json +132 -0
  23. package/dist/knowledge-base/cursor.json +134 -0
  24. package/dist/knowledge-base/gemini-cli.json +112 -0
  25. package/dist/knowledge-base/jetbrains-junie.json +208 -0
  26. package/dist/knowledge-base/kiro.json +102 -0
  27. package/dist/knowledge-base/opencode.json +128 -0
  28. package/dist/knowledge-base/roo-code.json +116 -0
  29. package/dist/knowledge-base/schema.json +77 -0
  30. package/dist/knowledge-base/windsurf.json +80 -0
  31. package/dist/knowledge-base/zed.json +88 -0
  32. package/dist/layer1-discovery/config-parser.d.ts +12 -0
  33. package/dist/layer1-discovery/config-parser.js +52 -0
  34. package/dist/layer1-discovery/file-walker.d.ts +13 -0
  35. package/dist/layer1-discovery/file-walker.js +77 -0
  36. package/dist/layer1-discovery/knowledge-base.d.ts +36 -0
  37. package/dist/layer1-discovery/knowledge-base.js +58 -0
  38. package/dist/layer1-discovery/tool-detector.d.ts +20 -0
  39. package/dist/layer1-discovery/tool-detector.js +138 -0
  40. package/dist/layer2-static/detectors/command-exec.d.ts +11 -0
  41. package/dist/layer2-static/detectors/command-exec.js +343 -0
  42. package/dist/layer2-static/detectors/consent-bypass.d.ts +8 -0
  43. package/dist/layer2-static/detectors/consent-bypass.js +330 -0
  44. package/dist/layer2-static/detectors/env-override.d.ts +8 -0
  45. package/dist/layer2-static/detectors/env-override.js +132 -0
  46. package/dist/layer2-static/detectors/git-hooks.d.ts +11 -0
  47. package/dist/layer2-static/detectors/git-hooks.js +61 -0
  48. package/dist/layer2-static/detectors/ide-settings.d.ts +8 -0
  49. package/dist/layer2-static/detectors/ide-settings.js +66 -0
  50. package/dist/layer2-static/detectors/plugin-manifest.d.ts +9 -0
  51. package/dist/layer2-static/detectors/plugin-manifest.js +1943 -0
  52. package/dist/layer2-static/detectors/rule-file.d.ts +7 -0
  53. package/dist/layer2-static/detectors/rule-file.js +299 -0
  54. package/dist/layer2-static/detectors/symlink.d.ts +9 -0
  55. package/dist/layer2-static/detectors/symlink.js +45 -0
  56. package/dist/layer2-static/engine.d.ts +28 -0
  57. package/dist/layer2-static/engine.js +83 -0
  58. package/dist/layer2-static/evidence.d.ts +12 -0
  59. package/dist/layer2-static/evidence.js +128 -0
  60. package/dist/layer2-static/rule-engine.d.ts +24 -0
  61. package/dist/layer2-static/rule-engine.js +138 -0
  62. package/dist/layer2-static/state/scan-state.d.ts +32 -0
  63. package/dist/layer2-static/state/scan-state.js +296 -0
  64. package/dist/layer3-dynamic/command-builder.d.ts +15 -0
  65. package/dist/layer3-dynamic/command-builder.js +39 -0
  66. package/dist/layer3-dynamic/local-text-analysis.d.ts +19 -0
  67. package/dist/layer3-dynamic/local-text-analysis.js +73 -0
  68. package/dist/layer3-dynamic/meta-agent.d.ts +17 -0
  69. package/dist/layer3-dynamic/meta-agent.js +33 -0
  70. package/dist/layer3-dynamic/prompt-templates/local-text-analysis.md +32 -0
  71. package/dist/layer3-dynamic/prompt-templates/security-analysis.md +13 -0
  72. package/dist/layer3-dynamic/prompt-templates/tool-poisoning.md +15 -0
  73. package/dist/layer3-dynamic/resource-fetcher.d.ts +25 -0
  74. package/dist/layer3-dynamic/resource-fetcher.js +119 -0
  75. package/dist/layer3-dynamic/sandbox.d.ts +13 -0
  76. package/dist/layer3-dynamic/sandbox.js +40 -0
  77. package/dist/layer3-dynamic/tool-description-acquisition.d.ts +22 -0
  78. package/dist/layer3-dynamic/tool-description-acquisition.js +76 -0
  79. package/dist/layer3-dynamic/tool-description-scanner.d.ts +11 -0
  80. package/dist/layer3-dynamic/tool-description-scanner.js +53 -0
  81. package/dist/layer3-dynamic/toxic-flow.d.ts +12 -0
  82. package/dist/layer3-dynamic/toxic-flow.js +57 -0
  83. package/dist/layer4-remediation/actions/quarantine.d.ts +1 -0
  84. package/dist/layer4-remediation/actions/quarantine.js +8 -0
  85. package/dist/layer4-remediation/actions/remove-field.d.ts +5 -0
  86. package/dist/layer4-remediation/actions/remove-field.js +53 -0
  87. package/dist/layer4-remediation/actions/replace-value.d.ts +5 -0
  88. package/dist/layer4-remediation/actions/replace-value.js +26 -0
  89. package/dist/layer4-remediation/actions/strip-unicode.d.ts +5 -0
  90. package/dist/layer4-remediation/actions/strip-unicode.js +8 -0
  91. package/dist/layer4-remediation/backup-manager.d.ts +32 -0
  92. package/dist/layer4-remediation/backup-manager.js +138 -0
  93. package/dist/layer4-remediation/diff-generator.d.ts +6 -0
  94. package/dist/layer4-remediation/diff-generator.js +29 -0
  95. package/dist/layer4-remediation/remediation-runner.d.ts +36 -0
  96. package/dist/layer4-remediation/remediation-runner.js +230 -0
  97. package/dist/layer4-remediation/remediator.d.ts +36 -0
  98. package/dist/layer4-remediation/remediator.js +117 -0
  99. package/dist/path-display.d.ts +1 -0
  100. package/dist/path-display.js +20 -0
  101. package/dist/pipeline.d.ts +34 -0
  102. package/dist/pipeline.js +259 -0
  103. package/dist/report-summary.d.ts +6 -0
  104. package/dist/report-summary.js +48 -0
  105. package/dist/reporter/html.d.ts +2 -0
  106. package/dist/reporter/html.js +103 -0
  107. package/dist/reporter/json.d.ts +2 -0
  108. package/dist/reporter/json.js +3 -0
  109. package/dist/reporter/markdown.d.ts +2 -0
  110. package/dist/reporter/markdown.js +52 -0
  111. package/dist/reporter/sarif.d.ts +2 -0
  112. package/dist/reporter/sarif.js +84 -0
  113. package/dist/reporter/terminal.d.ts +5 -0
  114. package/dist/reporter/terminal.js +94 -0
  115. package/dist/runtime/signal-handlers.d.ts +10 -0
  116. package/dist/runtime/signal-handlers.js +17 -0
  117. package/dist/scan-target/helpers.d.ts +20 -0
  118. package/dist/scan-target/helpers.js +268 -0
  119. package/dist/scan-target/staging.d.ts +5 -0
  120. package/dist/scan-target/staging.js +114 -0
  121. package/dist/scan-target/types.d.ts +18 -0
  122. package/dist/scan-target/types.js +1 -0
  123. package/dist/scan-target.d.ts +3 -0
  124. package/dist/scan-target.js +31 -0
  125. package/dist/scan.d.ts +54 -0
  126. package/dist/scan.js +593 -0
  127. package/dist/tui/app.d.ts +10 -0
  128. package/dist/tui/app.js +21 -0
  129. package/dist/tui/theme.d.ts +8 -0
  130. package/dist/tui/theme.js +7 -0
  131. package/dist/tui/views/dashboard.d.ts +6 -0
  132. package/dist/tui/views/dashboard.js +8 -0
  133. package/dist/tui/views/deep-scan-consent.d.ts +5 -0
  134. package/dist/tui/views/deep-scan-consent.js +6 -0
  135. package/dist/tui/views/progress.d.ts +4 -0
  136. package/dist/tui/views/progress.js +6 -0
  137. package/dist/tui/views/summary.d.ts +5 -0
  138. package/dist/tui/views/summary.js +16 -0
  139. package/dist/types/discovery.d.ts +12 -0
  140. package/dist/types/discovery.js +1 -0
  141. package/dist/types/finding.d.ts +46 -0
  142. package/dist/types/finding.js +15 -0
  143. package/dist/types/report.d.ts +25 -0
  144. package/dist/types/report.js +23 -0
  145. package/dist/wrapper.d.ts +35 -0
  146. package/dist/wrapper.js +220 -0
  147. package/package.json +97 -0
@@ -0,0 +1,138 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, } from "node:fs";
3
+ import { dirname, isAbsolute, join, resolve } from "node:path";
4
+ function isIgnorableBackupReadError(error) {
5
+ const code = error?.code;
6
+ return code === "ENOENT" || code === "ENOTDIR" || code === "EISDIR";
7
+ }
8
+ function readBackupSourceFile(sourcePath) {
9
+ try {
10
+ return readFileSync(sourcePath, "utf8");
11
+ }
12
+ catch (error) {
13
+ if (isIgnorableBackupReadError(error)) {
14
+ return null;
15
+ }
16
+ throw error;
17
+ }
18
+ }
19
+ function safeRelativePath(filePath) {
20
+ if (isAbsolute(filePath)) {
21
+ throw new Error(`Backup path must be relative: ${filePath}`);
22
+ }
23
+ if (filePath.includes("..")) {
24
+ throw new Error(`Backup path traversal is not allowed: ${filePath}`);
25
+ }
26
+ return filePath;
27
+ }
28
+ function hashContent(content) {
29
+ return createHash("sha256").update(content).digest("hex");
30
+ }
31
+ function ensureParentDirectory(path) {
32
+ mkdirSync(dirname(path), { recursive: true });
33
+ }
34
+ function backupRoot(projectRoot) {
35
+ return resolve(projectRoot, ".codegate-backup");
36
+ }
37
+ function sessionIdFromNow() {
38
+ const stamp = new Date().toISOString().replaceAll(":", "-").replaceAll(".", "-");
39
+ const nonce = randomBytes(3).toString("hex");
40
+ return `${stamp}-${nonce}`;
41
+ }
42
+ function manifestForSession(sessionDir) {
43
+ return join(sessionDir, ".manifest.json");
44
+ }
45
+ export function createBackupSession(input) {
46
+ const sessionId = sessionIdFromNow();
47
+ const root = backupRoot(input.projectRoot);
48
+ const sessionDir = join(root, sessionId);
49
+ mkdirSync(sessionDir, { recursive: true });
50
+ const entries = [];
51
+ for (const filePath of input.filePaths) {
52
+ const relativePath = safeRelativePath(filePath);
53
+ const sourcePath = resolve(input.projectRoot, relativePath);
54
+ const content = readBackupSourceFile(sourcePath);
55
+ if (content === null) {
56
+ continue;
57
+ }
58
+ const destinationPath = resolve(sessionDir, relativePath);
59
+ ensureParentDirectory(destinationPath);
60
+ writeFileSync(destinationPath, content, "utf8");
61
+ entries.push({
62
+ path: relativePath,
63
+ sha256: hashContent(content),
64
+ size: Buffer.byteLength(content, "utf8"),
65
+ });
66
+ }
67
+ const manifest = {
68
+ codegate_version: input.version,
69
+ created_at: new Date().toISOString(),
70
+ files: entries,
71
+ };
72
+ const manifestPath = manifestForSession(sessionDir);
73
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf8");
74
+ return {
75
+ sessionId,
76
+ sessionDir,
77
+ manifestPath,
78
+ manifest,
79
+ };
80
+ }
81
+ export function listBackupSessions(projectRoot) {
82
+ const root = backupRoot(projectRoot);
83
+ if (!existsSync(root)) {
84
+ return [];
85
+ }
86
+ return readdirSync(root)
87
+ .filter((entry) => entry !== "quarantine")
88
+ .filter((entry) => {
89
+ const fullPath = join(root, entry);
90
+ return statSync(fullPath).isDirectory();
91
+ })
92
+ .sort()
93
+ .reverse();
94
+ }
95
+ function readManifest(projectRoot, sessionId) {
96
+ const sessionDir = join(backupRoot(projectRoot), sessionId);
97
+ const manifestPath = manifestForSession(sessionDir);
98
+ if (!existsSync(manifestPath)) {
99
+ throw new Error(`Backup manifest missing for session ${sessionId}`);
100
+ }
101
+ const raw = readFileSync(manifestPath, "utf8");
102
+ const parsed = JSON.parse(raw);
103
+ if (!parsed || typeof parsed !== "object") {
104
+ throw new Error(`Invalid backup manifest for session ${sessionId}`);
105
+ }
106
+ const manifest = parsed;
107
+ if (!Array.isArray(manifest.files)) {
108
+ throw new Error(`Invalid backup manifest for session ${sessionId}`);
109
+ }
110
+ return manifest;
111
+ }
112
+ export function restoreBackupSession(input) {
113
+ const sessionDir = join(backupRoot(input.projectRoot), input.sessionId);
114
+ const manifest = readManifest(input.projectRoot, input.sessionId);
115
+ for (const entry of manifest.files) {
116
+ const backupPath = resolve(sessionDir, safeRelativePath(entry.path));
117
+ if (!existsSync(backupPath)) {
118
+ throw new Error(`Backup file missing: ${entry.path}`);
119
+ }
120
+ const content = readFileSync(backupPath, "utf8");
121
+ const actualHash = hashContent(content);
122
+ if (actualHash !== entry.sha256) {
123
+ throw new Error(`Backup manifest hash mismatch for ${entry.path}`);
124
+ }
125
+ }
126
+ for (const entry of manifest.files) {
127
+ const backupPath = resolve(sessionDir, safeRelativePath(entry.path));
128
+ const targetPath = resolve(input.projectRoot, safeRelativePath(entry.path));
129
+ const content = readFileSync(backupPath, "utf8");
130
+ ensureParentDirectory(targetPath);
131
+ writeFileSync(targetPath, content, "utf8");
132
+ }
133
+ rmSync(sessionDir, { recursive: true, force: true });
134
+ return {
135
+ restoredFiles: manifest.files.length,
136
+ sessionId: input.sessionId,
137
+ };
138
+ }
@@ -0,0 +1,6 @@
1
+ export interface DiffInput {
2
+ filePath: string;
3
+ before: string;
4
+ after: string;
5
+ }
6
+ export declare function generateUnifiedDiff(input: DiffInput): string;
@@ -0,0 +1,29 @@
1
+ export function generateUnifiedDiff(input) {
2
+ if (input.before === input.after) {
3
+ return "";
4
+ }
5
+ const beforeLines = input.before.split("\n");
6
+ const afterLines = input.after.split("\n");
7
+ const maxLines = Math.max(beforeLines.length, afterLines.length);
8
+ const lines = [];
9
+ lines.push(`--- a/${input.filePath}`);
10
+ lines.push(`+++ b/${input.filePath}`);
11
+ lines.push("@@");
12
+ for (let index = 0; index < maxLines; index += 1) {
13
+ const beforeLine = beforeLines[index];
14
+ const afterLine = afterLines[index];
15
+ if (beforeLine === afterLine) {
16
+ if (beforeLine !== undefined) {
17
+ lines.push(` ${beforeLine}`);
18
+ }
19
+ continue;
20
+ }
21
+ if (beforeLine !== undefined) {
22
+ lines.push(`-${beforeLine}`);
23
+ }
24
+ if (afterLine !== undefined) {
25
+ lines.push(`+${afterLine}`);
26
+ }
27
+ }
28
+ return lines.join("\n");
29
+ }
@@ -0,0 +1,36 @@
1
+ import { type CodeGateConfig } from "../config.js";
2
+ import type { CodeGateReport } from "../types/report.js";
3
+ import { type RemediationPlanItem } from "./remediator.js";
4
+ export interface RemediationFlags {
5
+ remediate?: boolean;
6
+ fixSafe?: boolean;
7
+ dryRun?: boolean;
8
+ patch?: boolean;
9
+ output?: string;
10
+ }
11
+ export interface RemediationRunnerInput {
12
+ scanTarget: string;
13
+ report: CodeGateReport;
14
+ config: CodeGateConfig;
15
+ flags: RemediationFlags;
16
+ isTTY: boolean;
17
+ }
18
+ export interface RemediationRunnerResult {
19
+ report: CodeGateReport;
20
+ plannedCount: number;
21
+ appliedCount: number;
22
+ plannedActions?: Array<{
23
+ findingId: string;
24
+ filePath: string;
25
+ action: RemediationPlanItem["action"]["type"];
26
+ }>;
27
+ appliedActions?: Array<{
28
+ findingId: string;
29
+ filePath: string;
30
+ action: RemediationPlanItem["action"]["type"];
31
+ }>;
32
+ backupSessionId?: string;
33
+ patchContent?: string;
34
+ patchPath?: string;
35
+ }
36
+ export declare function runRemediation(input: RemediationRunnerInput): RemediationRunnerResult;
@@ -0,0 +1,230 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { applyConfigPolicy } from "../config.js";
4
+ import { createBackupSession } from "./backup-manager.js";
5
+ import { generateUnifiedDiff } from "./diff-generator.js";
6
+ import { applyRemediationAction, planRemediation, } from "./remediator.js";
7
+ function inferFormat(filePath) {
8
+ if (filePath.endsWith(".json")) {
9
+ return "json";
10
+ }
11
+ if (filePath.endsWith(".jsonc")) {
12
+ return "jsonc";
13
+ }
14
+ if (filePath.endsWith(".toml")) {
15
+ return "toml";
16
+ }
17
+ if (filePath.endsWith(".yaml") || filePath.endsWith(".yml")) {
18
+ return "yaml";
19
+ }
20
+ if (filePath.endsWith(".env")) {
21
+ return "dotenv";
22
+ }
23
+ if (filePath.endsWith(".md") || filePath.endsWith(".mdc")) {
24
+ return "markdown";
25
+ }
26
+ return "text";
27
+ }
28
+ function loadRemediationFiles(scanTarget, report) {
29
+ const uniquePaths = Array.from(new Set(report.findings.flatMap((finding) => {
30
+ const paths = [finding.file_path];
31
+ if (finding.source_config?.file_path) {
32
+ paths.push(finding.source_config.file_path);
33
+ }
34
+ return paths;
35
+ })));
36
+ const files = [];
37
+ for (const relativePath of uniquePaths) {
38
+ const absolutePath = resolve(scanTarget, relativePath);
39
+ if (!existsSync(absolutePath)) {
40
+ continue;
41
+ }
42
+ files.push({
43
+ path: relativePath,
44
+ format: inferFormat(relativePath),
45
+ content: readFileSync(absolutePath, "utf8"),
46
+ });
47
+ }
48
+ return files;
49
+ }
50
+ function isSafeCriticalPlan(plan, report) {
51
+ const finding = report.findings.find((item) => item.finding_id === plan.findingId);
52
+ if (!finding || finding.severity !== "CRITICAL") {
53
+ return false;
54
+ }
55
+ return plan.action.type === "remove_field" || plan.action.type === "replace_value";
56
+ }
57
+ function choosePlans(input, plans) {
58
+ if (input.flags.fixSafe) {
59
+ return plans.filter((plan) => isSafeCriticalPlan(plan, input.report));
60
+ }
61
+ return plans;
62
+ }
63
+ function toPatchContent(plans) {
64
+ return plans
65
+ .map((plan) => plan.diff)
66
+ .filter((diff) => diff.length > 0)
67
+ .join("\n\n");
68
+ }
69
+ function toActionSummaries(plans) {
70
+ return plans.map((plan) => ({
71
+ findingId: plan.findingId,
72
+ filePath: plan.filePath,
73
+ action: plan.action.type,
74
+ }));
75
+ }
76
+ function findingById(report) {
77
+ return new Map(report.findings.map((finding) => [finding.finding_id, finding]));
78
+ }
79
+ function composeRemediationPlans(report, files, selectedPlans) {
80
+ const filesByPath = new Map(files.map((file) => [file.path, file]));
81
+ const findings = findingById(report);
82
+ const plansByFile = new Map();
83
+ for (const plan of selectedPlans) {
84
+ const grouped = plansByFile.get(plan.filePath);
85
+ if (grouped) {
86
+ grouped.push(plan);
87
+ continue;
88
+ }
89
+ plansByFile.set(plan.filePath, [plan]);
90
+ }
91
+ const composedPlans = [];
92
+ for (const [filePath, plans] of plansByFile.entries()) {
93
+ const file = filesByPath.get(filePath);
94
+ if (!file) {
95
+ continue;
96
+ }
97
+ const quarantinePlan = plans.find((plan) => plan.action.type === "quarantine");
98
+ if (quarantinePlan) {
99
+ const finding = findings.get(quarantinePlan.findingId);
100
+ if (!finding) {
101
+ continue;
102
+ }
103
+ const result = applyRemediationAction(quarantinePlan.action, file, finding);
104
+ if (!result.changed) {
105
+ continue;
106
+ }
107
+ composedPlans.push(...plans.map((plan) => ({
108
+ findingId: plan.findingId,
109
+ filePath,
110
+ action: { type: "quarantine" },
111
+ originalContent: file.content,
112
+ updatedContent: result.updatedContent,
113
+ diff: generateUnifiedDiff({
114
+ filePath,
115
+ before: file.content,
116
+ after: result.updatedContent,
117
+ }),
118
+ })));
119
+ continue;
120
+ }
121
+ let workingFile = { ...file };
122
+ const appliedPlans = [];
123
+ const structuredPlans = plans.filter((plan) => plan.action.type === "remove_field" || plan.action.type === "replace_value");
124
+ const textPlans = plans.filter((plan) => plan.action.type === "strip_unicode");
125
+ for (const plan of [...structuredPlans, ...textPlans]) {
126
+ const finding = findings.get(plan.findingId);
127
+ if (!finding) {
128
+ continue;
129
+ }
130
+ const result = applyRemediationAction(plan.action, workingFile, finding);
131
+ if (!result.changed) {
132
+ continue;
133
+ }
134
+ const before = workingFile.content;
135
+ workingFile = {
136
+ ...workingFile,
137
+ content: result.updatedContent,
138
+ };
139
+ appliedPlans.push({
140
+ findingId: plan.findingId,
141
+ filePath,
142
+ action: plan.action,
143
+ originalContent: before,
144
+ updatedContent: result.updatedContent,
145
+ diff: generateUnifiedDiff({
146
+ filePath,
147
+ before,
148
+ after: result.updatedContent,
149
+ }),
150
+ });
151
+ }
152
+ composedPlans.push(...appliedPlans);
153
+ }
154
+ return composedPlans;
155
+ }
156
+ export function runRemediation(input) {
157
+ const remediationEnabled = input.flags.remediate || input.flags.fixSafe || input.flags.patch || input.flags.dryRun;
158
+ if (!remediationEnabled) {
159
+ return {
160
+ report: input.report,
161
+ plannedCount: 0,
162
+ appliedCount: 0,
163
+ };
164
+ }
165
+ const files = loadRemediationFiles(input.scanTarget, input.report);
166
+ const plans = planRemediation({
167
+ findings: input.report.findings,
168
+ files,
169
+ });
170
+ const selectedPlans = choosePlans(input, plans);
171
+ const composedPlans = composeRemediationPlans(input.report, files, selectedPlans);
172
+ const patchContent = toPatchContent(composedPlans);
173
+ let patchPath;
174
+ if (input.flags.patch) {
175
+ if (input.flags.output) {
176
+ patchPath = resolve(input.scanTarget, input.flags.output);
177
+ writeFileSync(patchPath, patchContent, "utf8");
178
+ }
179
+ else if (input.isTTY) {
180
+ patchPath = resolve(input.scanTarget, "codegate-fixes.patch");
181
+ writeFileSync(patchPath, patchContent, "utf8");
182
+ }
183
+ }
184
+ if (input.flags.dryRun || (!input.flags.remediate && !input.flags.fixSafe)) {
185
+ return {
186
+ report: input.report,
187
+ plannedCount: composedPlans.length,
188
+ appliedCount: 0,
189
+ plannedActions: toActionSummaries(composedPlans),
190
+ appliedActions: [],
191
+ patchContent: input.flags.patch && !input.flags.output && !input.isTTY ? patchContent : undefined,
192
+ patchPath,
193
+ };
194
+ }
195
+ const filePlans = new Map();
196
+ for (const plan of composedPlans) {
197
+ if (!filePlans.has(plan.filePath)) {
198
+ filePlans.set(plan.filePath, plan);
199
+ continue;
200
+ }
201
+ filePlans.set(plan.filePath, {
202
+ ...plan,
203
+ originalContent: filePlans.get(plan.filePath)?.originalContent ?? plan.originalContent,
204
+ });
205
+ }
206
+ const session = createBackupSession({
207
+ projectRoot: input.scanTarget,
208
+ version: input.report.version,
209
+ filePaths: Array.from(filePlans.keys()),
210
+ });
211
+ for (const plan of filePlans.values()) {
212
+ writeFileSync(resolve(input.scanTarget, plan.filePath), plan.updatedContent, "utf8");
213
+ }
214
+ const appliedFindings = new Set(composedPlans.map((plan) => plan.findingId));
215
+ const remainingFindings = input.report.findings.filter((finding) => !appliedFindings.has(finding.finding_id));
216
+ const report = applyConfigPolicy({
217
+ ...input.report,
218
+ findings: remainingFindings,
219
+ }, input.config);
220
+ return {
221
+ report,
222
+ plannedCount: composedPlans.length,
223
+ appliedCount: composedPlans.length,
224
+ plannedActions: toActionSummaries(composedPlans),
225
+ appliedActions: toActionSummaries(composedPlans),
226
+ backupSessionId: session.sessionId,
227
+ patchContent: input.flags.patch && !input.flags.output && !input.isTTY ? patchContent : undefined,
228
+ patchPath,
229
+ };
230
+ }
@@ -0,0 +1,36 @@
1
+ import type { DiscoveryFormat } from "../types/discovery.js";
2
+ import type { Finding } from "../types/finding.js";
3
+ export interface RemediationFile {
4
+ path: string;
5
+ format: DiscoveryFormat;
6
+ content: string;
7
+ }
8
+ export type RemediationAction = {
9
+ type: "remove_field";
10
+ fieldPath: string;
11
+ } | {
12
+ type: "replace_value";
13
+ fieldPath: string;
14
+ value: unknown;
15
+ } | {
16
+ type: "strip_unicode";
17
+ } | {
18
+ type: "quarantine";
19
+ };
20
+ export interface RemediationPlanItem {
21
+ findingId: string;
22
+ filePath: string;
23
+ action: RemediationAction;
24
+ originalContent: string;
25
+ updatedContent: string;
26
+ diff: string;
27
+ }
28
+ export interface RemediationPlanInput {
29
+ findings: Finding[];
30
+ files: RemediationFile[];
31
+ }
32
+ export declare function applyRemediationAction(action: RemediationAction, file: RemediationFile, finding: Finding): {
33
+ updatedContent: string;
34
+ changed: boolean;
35
+ };
36
+ export declare function planRemediation(input: RemediationPlanInput): RemediationPlanItem[];
@@ -0,0 +1,117 @@
1
+ import { parse as parseJsonc } from "jsonc-parser";
2
+ import { generateUnifiedDiff } from "./diff-generator.js";
3
+ import { buildQuarantinePlaceholder } from "./actions/quarantine.js";
4
+ import { removeField } from "./actions/remove-field.js";
5
+ import { replaceValue } from "./actions/replace-value.js";
6
+ import { stripInvisibleUnicode } from "./actions/strip-unicode.js";
7
+ function parseStructuredContent(format, content) {
8
+ if (format === "json") {
9
+ return JSON.parse(content);
10
+ }
11
+ if (format === "jsonc") {
12
+ return parseJsonc(content);
13
+ }
14
+ return null;
15
+ }
16
+ function serializeStructuredContent(format, value) {
17
+ if (format === "json" || format === "jsonc") {
18
+ return `${JSON.stringify(value, null, 2)}\n`;
19
+ }
20
+ return String(value ?? "");
21
+ }
22
+ function chooseAction(finding, fieldPath) {
23
+ if (finding.category === "ENV_OVERRIDE" && fieldPath) {
24
+ return { type: "remove_field", fieldPath };
25
+ }
26
+ if (finding.category === "CONSENT_BYPASS" && fieldPath) {
27
+ if (fieldPath === "enableAllProjectMcpServers") {
28
+ return {
29
+ type: "replace_value",
30
+ fieldPath,
31
+ value: false,
32
+ };
33
+ }
34
+ return { type: "remove_field", fieldPath };
35
+ }
36
+ if (finding.category === "RULE_INJECTION") {
37
+ return { type: "strip_unicode" };
38
+ }
39
+ if (finding.category === "COMMAND_EXEC" ||
40
+ finding.category === "SYMLINK_ESCAPE" ||
41
+ finding.category === "GIT_HOOK") {
42
+ return { type: "quarantine" };
43
+ }
44
+ return null;
45
+ }
46
+ export function applyRemediationAction(action, file, finding) {
47
+ if (action.type === "strip_unicode") {
48
+ const stripped = stripInvisibleUnicode(file.content);
49
+ return {
50
+ updatedContent: stripped.content,
51
+ changed: stripped.changed,
52
+ };
53
+ }
54
+ if (action.type === "quarantine") {
55
+ return {
56
+ updatedContent: buildQuarantinePlaceholder(file.path, finding.description, ".codegate-backup/quarantine"),
57
+ changed: true,
58
+ };
59
+ }
60
+ const parsed = parseStructuredContent(file.format, file.content);
61
+ if (parsed === null) {
62
+ return { updatedContent: file.content, changed: false };
63
+ }
64
+ if (action.type === "remove_field") {
65
+ const result = removeField(parsed, action.fieldPath);
66
+ return {
67
+ updatedContent: result.changed
68
+ ? serializeStructuredContent(file.format, result.value)
69
+ : file.content,
70
+ changed: result.changed,
71
+ };
72
+ }
73
+ const result = replaceValue(parsed, action.fieldPath, action.value);
74
+ return {
75
+ updatedContent: result.changed
76
+ ? serializeStructuredContent(file.format, result.value)
77
+ : file.content,
78
+ changed: result.changed,
79
+ };
80
+ }
81
+ export function planRemediation(input) {
82
+ const filesByPath = new Map(input.files.map((file) => [file.path, file]));
83
+ const plan = [];
84
+ for (const finding of input.findings) {
85
+ if (!finding.fixable || finding.suppressed) {
86
+ continue;
87
+ }
88
+ const targetPath = finding.source_config?.file_path ?? finding.file_path;
89
+ const fieldPath = finding.source_config?.field ?? finding.location.field;
90
+ const file = filesByPath.get(targetPath);
91
+ if (!file) {
92
+ continue;
93
+ }
94
+ const action = chooseAction(finding, fieldPath);
95
+ if (!action) {
96
+ continue;
97
+ }
98
+ const result = applyRemediationAction(action, file, finding);
99
+ if (!result.changed) {
100
+ continue;
101
+ }
102
+ const diff = generateUnifiedDiff({
103
+ filePath: file.path,
104
+ before: file.content,
105
+ after: result.updatedContent,
106
+ });
107
+ plan.push({
108
+ findingId: finding.finding_id,
109
+ filePath: file.path,
110
+ action,
111
+ originalContent: file.content,
112
+ updatedContent: result.updatedContent,
113
+ diff,
114
+ });
115
+ }
116
+ return plan;
117
+ }
@@ -0,0 +1 @@
1
+ export declare function toAbsoluteDisplayPath(scanTarget: string, filePath: string): string;
@@ -0,0 +1,20 @@
1
+ import { isAbsolute, resolve } from "node:path";
2
+ const URI_LIKE_PATTERN = /^[a-z][a-z0-9+.-]*:/iu;
3
+ export function toAbsoluteDisplayPath(scanTarget, filePath) {
4
+ if (filePath.length === 0) {
5
+ return filePath;
6
+ }
7
+ if (isAbsolute(filePath)) {
8
+ return filePath;
9
+ }
10
+ if (filePath === "~" || filePath.startsWith("~/")) {
11
+ return filePath;
12
+ }
13
+ if (!isAbsolute(scanTarget)) {
14
+ return filePath;
15
+ }
16
+ if (URI_LIKE_PATTERN.test(filePath)) {
17
+ return filePath;
18
+ }
19
+ return resolve(scanTarget, filePath);
20
+ }
@@ -0,0 +1,34 @@
1
+ import { type StaticEngineConfig, type StaticFileInput } from "./layer2-static/engine.js";
2
+ import { type CodeGateReport } from "./types/report.js";
3
+ import type { GitHookEntry } from "./layer2-static/detectors/git-hooks.js";
4
+ import type { SymlinkEscapeEntry } from "./layer2-static/detectors/symlink.js";
5
+ import type { ResourceFetchResult, ResourceRequest } from "./layer3-dynamic/resource-fetcher.js";
6
+ import type { Finding } from "./types/finding.js";
7
+ export interface StaticPipelineInput {
8
+ version: string;
9
+ kbVersion: string;
10
+ scanTarget: string;
11
+ toolsDetected: string[];
12
+ projectRoot: string;
13
+ files: StaticFileInput[];
14
+ symlinkEscapes: SymlinkEscapeEntry[];
15
+ hooks: GitHookEntry[];
16
+ config: StaticEngineConfig;
17
+ }
18
+ export interface DeepScanResource {
19
+ id: string;
20
+ request: ResourceRequest;
21
+ commandPreview: string;
22
+ }
23
+ export interface DeepScanOutcome {
24
+ resourceId: string;
25
+ approved: boolean;
26
+ status: "skipped_without_consent" | "ok" | "auth_failure" | "timeout" | "network_error" | "command_error";
27
+ result?: ResourceFetchResult;
28
+ }
29
+ export declare function runStaticPipeline(input: StaticPipelineInput): CodeGateReport;
30
+ export declare function layer3OutcomesToFindings(outcomes: DeepScanOutcome[], options?: {
31
+ unicodeAnalysis?: boolean;
32
+ }): Finding[];
33
+ export declare function mergeLayer3Findings(baseReport: CodeGateReport, layer3Findings: Finding[]): CodeGateReport;
34
+ export declare function runDeepScanWithConsent(resources: DeepScanResource[], requestConsent: (resource: DeepScanResource) => Promise<boolean> | boolean, execute: (resource: DeepScanResource) => Promise<ResourceFetchResult>): Promise<DeepScanOutcome[]>;