codeproof 1.0.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/bin/codeproof.js +43 -0
- package/commands/init.js +72 -0
- package/commands/moveSecret.js +220 -0
- package/commands/reportDashboard.js +78 -0
- package/commands/run.js +196 -0
- package/core/boundaries.md +26 -0
- package/core/featureFlags.js +25 -0
- package/core/safetyGuards.js +51 -0
- package/engine/aiAnalyzer.js +51 -0
- package/engine/aiEscalation.js +6 -0
- package/engine/contextBuilder.js +65 -0
- package/engine/decisionMerger.js +30 -0
- package/engine/ruleEngine.js +52 -0
- package/hooks/preCommit.js +67 -0
- package/package.json +13 -0
- package/reporting/reportBuilder.js +100 -0
- package/reporting/reportWriter.js +19 -0
- package/rules/dangerousUsageRule.js +11 -0
- package/rules/insecureConfigRule.js +11 -0
- package/rules/regexPatterns.js +53 -0
- package/rules/ruleUtils.js +58 -0
- package/rules/secretRule.js +21 -0
- package/ui/welcomeScreen.js +42 -0
- package/utils/apiClient.js +56 -0
- package/utils/envManager.js +48 -0
- package/utils/fileRewriter.js +145 -0
- package/utils/files.js +50 -0
- package/utils/git.js +46 -0
- package/utils/logger.js +25 -0
- package/utils/projectType.js +20 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import http from "http";
|
|
2
|
+
import https from "https";
|
|
3
|
+
|
|
4
|
+
// Boundary: integration layer only. Must not import CLI, rule engine, or reporting.
|
|
5
|
+
// Network calls are fail-open to avoid impacting commits or developer flow.
|
|
6
|
+
|
|
7
|
+
const DEFAULT_ENDPOINT = "https://api.codeproof.dev/report";
|
|
8
|
+
|
|
9
|
+
export function sendReportToServer(report, options = {}) {
|
|
10
|
+
const enabled = Boolean(options.enabled);
|
|
11
|
+
if (!enabled) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const endpointUrl = typeof options.endpointUrl === "string" && options.endpointUrl.trim()
|
|
16
|
+
? options.endpointUrl.trim()
|
|
17
|
+
: DEFAULT_ENDPOINT;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const url = new URL(endpointUrl);
|
|
21
|
+
const payload = JSON.stringify(report);
|
|
22
|
+
const transport = url.protocol === "http:" ? http : https;
|
|
23
|
+
|
|
24
|
+
const request = transport.request(
|
|
25
|
+
{
|
|
26
|
+
method: "POST",
|
|
27
|
+
hostname: url.hostname,
|
|
28
|
+
port: url.port || (url.protocol === "http:" ? 80 : 443),
|
|
29
|
+
path: `${url.pathname}${url.search}`,
|
|
30
|
+
headers: {
|
|
31
|
+
"Content-Type": "application/json",
|
|
32
|
+
"Content-Length": Buffer.byteLength(payload)
|
|
33
|
+
},
|
|
34
|
+
timeout: 2000
|
|
35
|
+
},
|
|
36
|
+
(res) => {
|
|
37
|
+
// UX: fail-open integrations never read or store server responses.
|
|
38
|
+
res.resume();
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
request.on("timeout", () => {
|
|
43
|
+
// Integrations are fail-open: timeout should not block or throw.
|
|
44
|
+
request.destroy();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
request.on("error", () => {
|
|
48
|
+
// Integrations are fail-open: network errors are ignored silently.
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
request.write(payload);
|
|
52
|
+
request.end();
|
|
53
|
+
} catch {
|
|
54
|
+
// Integrations are fail-open: invalid URLs or serialization issues are ignored silently.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
export function ensureEnvFile(projectRoot) {
|
|
6
|
+
const envPath = path.join(projectRoot, ".env");
|
|
7
|
+
if (!fs.existsSync(envPath)) {
|
|
8
|
+
fs.writeFileSync(envPath, "", "utf8");
|
|
9
|
+
}
|
|
10
|
+
return envPath;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function readEnvKeys(envPath) {
|
|
14
|
+
if (!fs.existsSync(envPath)) {
|
|
15
|
+
return new Set();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const content = fs.readFileSync(envPath, "utf8");
|
|
19
|
+
const keys = new Set();
|
|
20
|
+
const lines = content.split(/\r?\n/);
|
|
21
|
+
|
|
22
|
+
for (const line of lines) {
|
|
23
|
+
const trimmed = line.trim();
|
|
24
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const eqIndex = trimmed.indexOf("=");
|
|
28
|
+
if (eqIndex === -1) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
32
|
+
if (key) {
|
|
33
|
+
keys.add(key);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return keys;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function appendEnvEntries(envPath, entries) {
|
|
41
|
+
if (!entries.length) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// Safety: append only to avoid overwriting existing .env entries.
|
|
45
|
+
const lines = entries.map((entry) => `${entry.key}=${entry.value}`);
|
|
46
|
+
const content = lines.join(os.EOL) + os.EOL;
|
|
47
|
+
fs.appendFileSync(envPath, content, "utf8");
|
|
48
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
function detectLineEnding(content) {
|
|
5
|
+
return content.includes("\r\n") ? "\r\n" : "\n";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function ensureBackupDir(projectRoot) {
|
|
9
|
+
const backupRoot = path.join(projectRoot, ".codeproof-backup");
|
|
10
|
+
if (!fs.existsSync(backupRoot)) {
|
|
11
|
+
fs.mkdirSync(backupRoot, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
return backupRoot;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function backupFileOnce(projectRoot, filePath, backedUp) {
|
|
17
|
+
if (backedUp.has(filePath)) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const backupRoot = ensureBackupDir(projectRoot);
|
|
22
|
+
const relative = path.relative(projectRoot, filePath);
|
|
23
|
+
const backupPath = path.join(backupRoot, relative);
|
|
24
|
+
const backupDir = path.dirname(backupPath);
|
|
25
|
+
if (!fs.existsSync(backupDir)) {
|
|
26
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
fs.copyFileSync(filePath, backupPath);
|
|
29
|
+
backedUp.add(filePath);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function findValueSegment(line) {
|
|
33
|
+
const equalsIndex = line.indexOf("=");
|
|
34
|
+
const colonIndex = line.indexOf(":");
|
|
35
|
+
let separatorIndex = -1;
|
|
36
|
+
if (equalsIndex !== -1 && colonIndex !== -1) {
|
|
37
|
+
separatorIndex = Math.min(equalsIndex, colonIndex);
|
|
38
|
+
} else if (equalsIndex !== -1) {
|
|
39
|
+
separatorIndex = equalsIndex;
|
|
40
|
+
} else if (colonIndex !== -1) {
|
|
41
|
+
separatorIndex = colonIndex;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (separatorIndex === -1) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const right = line.slice(separatorIndex + 1);
|
|
49
|
+
const leadingMatch = right.match(/^\s*/);
|
|
50
|
+
const leading = leadingMatch ? leadingMatch[0] : "";
|
|
51
|
+
const startIndex = separatorIndex + 1 + leading.length;
|
|
52
|
+
|
|
53
|
+
const quoteChar = right[leading.length];
|
|
54
|
+
if (quoteChar === "'" || quoteChar === '"') {
|
|
55
|
+
const endIndex = right.indexOf(quoteChar, leading.length + 1);
|
|
56
|
+
if (endIndex === -1) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
separatorIndex,
|
|
61
|
+
valueStart: startIndex,
|
|
62
|
+
valueEnd: separatorIndex + 1 + endIndex + 1,
|
|
63
|
+
secretValue: right.slice(leading.length + 1, endIndex)
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const candidates = [];
|
|
68
|
+
const delimiters = [" ", "\t", ";", ",", ")"];
|
|
69
|
+
for (const delimiter of delimiters) {
|
|
70
|
+
const index = right.indexOf(delimiter, leading.length);
|
|
71
|
+
if (index !== -1) {
|
|
72
|
+
candidates.push(separatorIndex + 1 + index);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const commentIndex = right.indexOf("//", leading.length);
|
|
76
|
+
if (commentIndex !== -1) {
|
|
77
|
+
candidates.push(separatorIndex + 1 + commentIndex);
|
|
78
|
+
}
|
|
79
|
+
const hashIndex = right.indexOf("#", leading.length);
|
|
80
|
+
if (hashIndex !== -1) {
|
|
81
|
+
candidates.push(separatorIndex + 1 + hashIndex);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const valueEnd = candidates.length > 0 ? Math.min(...candidates) : line.length;
|
|
85
|
+
const rawValue = line.slice(startIndex, valueEnd).trim();
|
|
86
|
+
if (!rawValue) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
separatorIndex,
|
|
92
|
+
valueStart: startIndex,
|
|
93
|
+
valueEnd,
|
|
94
|
+
secretValue: rawValue
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function extractSecretValueFromLine(line) {
|
|
99
|
+
const segment = findValueSegment(line);
|
|
100
|
+
return segment?.secretValue ? segment.secretValue : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function replaceSecretInFile({ filePath, lineNumber, envKey, expectedSnippet, expectedSecretValue }) {
|
|
104
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
105
|
+
const eol = detectLineEnding(content);
|
|
106
|
+
const lines = content.split(/\r?\n/);
|
|
107
|
+
const index = lineNumber - 1;
|
|
108
|
+
|
|
109
|
+
if (index < 0 || index >= lines.length) {
|
|
110
|
+
return { updated: false, reason: "line_out_of_range" };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const originalLine = lines[index];
|
|
114
|
+
if (originalLine.includes(`process.env.${envKey}`) || originalLine.includes("process.env.CODEPROOF_SECRET_")) {
|
|
115
|
+
return { updated: false, reason: "already_moved" };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (expectedSnippet && !String(originalLine).includes(String(expectedSnippet))) {
|
|
119
|
+
return { updated: false, reason: "line_mismatch" };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Safety: only rewrite the specific line from the latest report; no regex rescanning.
|
|
123
|
+
const segment = findValueSegment(originalLine);
|
|
124
|
+
if (!segment || !segment.secretValue) {
|
|
125
|
+
return { updated: false, reason: "unable_to_extract" };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (expectedSecretValue && segment.secretValue !== expectedSecretValue) {
|
|
129
|
+
return { updated: false, reason: "value_mismatch" };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const before = originalLine.slice(0, segment.valueStart);
|
|
133
|
+
const after = originalLine.slice(segment.valueEnd);
|
|
134
|
+
const replacement = `process.env.${envKey}`;
|
|
135
|
+
// Preserve original formatting and line endings by replacing only the value segment.
|
|
136
|
+
const updatedLine = `${before}${replacement}${after}`;
|
|
137
|
+
|
|
138
|
+
lines[index] = updatedLine;
|
|
139
|
+
|
|
140
|
+
const hasTrailingNewline = content.endsWith(eol);
|
|
141
|
+
const newContent = lines.join(eol) + (hasTrailingNewline ? eol : "");
|
|
142
|
+
fs.writeFileSync(filePath, newContent, "utf8");
|
|
143
|
+
|
|
144
|
+
return { updated: true, secretValue: segment.secretValue };
|
|
145
|
+
}
|
package/utils/files.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_EXCLUDES = new Set([".git", "node_modules", ".venv", "dist", "build"]);
|
|
5
|
+
|
|
6
|
+
export function getDefaultExcludes() {
|
|
7
|
+
return DEFAULT_EXCLUDES;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function isBinaryFile(filePath) {
|
|
11
|
+
// Heuristic: if the first chunk contains a null byte, treat as binary.
|
|
12
|
+
try {
|
|
13
|
+
const fd = fs.openSync(filePath, "r");
|
|
14
|
+
const buffer = Buffer.alloc(8000);
|
|
15
|
+
const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, 0);
|
|
16
|
+
fs.closeSync(fd);
|
|
17
|
+
for (let i = 0; i < bytesRead; i += 1) {
|
|
18
|
+
if (buffer[i] === 0) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
} catch {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function listFilesRecursive(rootDir, excludes = DEFAULT_EXCLUDES) {
|
|
29
|
+
const results = [];
|
|
30
|
+
|
|
31
|
+
function walk(currentDir) {
|
|
32
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
if (excludes.has(entry.name)) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
39
|
+
|
|
40
|
+
if (entry.isDirectory()) {
|
|
41
|
+
walk(fullPath);
|
|
42
|
+
} else if (entry.isFile()) {
|
|
43
|
+
results.push(fullPath);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
walk(rootDir);
|
|
49
|
+
return results;
|
|
50
|
+
}
|
package/utils/git.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { spawnSync } from "child_process";
|
|
2
|
+
import { logError } from "./logger.js";
|
|
3
|
+
|
|
4
|
+
function runGit(args, cwd) {
|
|
5
|
+
const result = spawnSync("git", args, {
|
|
6
|
+
cwd,
|
|
7
|
+
stdio: "pipe",
|
|
8
|
+
encoding: "utf8"
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
if (result.error) {
|
|
12
|
+
throw result.error;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ensureGitRepo(cwd) {
|
|
19
|
+
const result = runGit(["rev-parse", "--is-inside-work-tree"], cwd);
|
|
20
|
+
if (result.status !== 0 || !String(result.stdout).trim().includes("true")) {
|
|
21
|
+
logError("Not a Git repository. Run this inside a Git repo.");
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getGitRoot(cwd) {
|
|
27
|
+
const result = runGit(["rev-parse", "--show-toplevel"], cwd);
|
|
28
|
+
if (result.status !== 0) {
|
|
29
|
+
logError("Failed to resolve Git root.");
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
return String(result.stdout).trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getStagedFiles(cwd) {
|
|
36
|
+
const result = runGit(["diff", "--cached", "--name-only"], cwd);
|
|
37
|
+
if (result.status !== 0) {
|
|
38
|
+
logError("Failed to read staged files.");
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return String(result.stdout)
|
|
43
|
+
.split(/\r?\n/)
|
|
44
|
+
.map((line) => line.trim())
|
|
45
|
+
.filter(Boolean);
|
|
46
|
+
}
|
package/utils/logger.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
function formatPrefix(level) {
|
|
2
|
+
const map = {
|
|
3
|
+
info: "[codeproof]",
|
|
4
|
+
success: "[codeproof]",
|
|
5
|
+
warn: "[codeproof]",
|
|
6
|
+
error: "[codeproof]"
|
|
7
|
+
};
|
|
8
|
+
return map[level] || "[codeproof]";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function logInfo(message) {
|
|
12
|
+
console.log(`${formatPrefix("info")} ${message}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function logSuccess(message) {
|
|
16
|
+
console.log(`${formatPrefix("success")} ${message}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function logWarn(message) {
|
|
20
|
+
console.warn(`${formatPrefix("warn")} ${message}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function logError(message) {
|
|
24
|
+
console.error(`${formatPrefix("error")} ${message}`);
|
|
25
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
export function detectProjectType(rootDir) {
|
|
5
|
+
const hasFile = (fileName) => fs.existsSync(path.join(rootDir, fileName));
|
|
6
|
+
|
|
7
|
+
if (hasFile("package.json")) {
|
|
8
|
+
return "Node";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (hasFile("requirements.txt") || hasFile("pyproject.toml")) {
|
|
12
|
+
return "Python";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (hasFile("pom.xml") || hasFile("build.gradle")) {
|
|
16
|
+
return "Java";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return "Unknown";
|
|
20
|
+
}
|