codeproof 1.0.5 → 1.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.
- package/bin/codeproof.js +6 -4
- package/commands/help.js +34 -0
- package/commands/init.js +4 -0
- package/commands/moveSecret.js +67 -15
- package/commands/whoami.js +1 -1
- package/package.json +1 -1
- package/ui/banner.js +36 -0
- package/ui/formatting.js +76 -0
package/bin/codeproof.js
CHANGED
|
@@ -6,14 +6,16 @@ import { runMoveSecret } from "../commands/moveSecret.js";
|
|
|
6
6
|
import { runWhoAmI } from "../commands/whoami.js";
|
|
7
7
|
import { runIgnore } from "../commands/ignore.js";
|
|
8
8
|
import { runApply } from "../commands/apply.js";
|
|
9
|
-
import {
|
|
9
|
+
import { runHelp } from "../commands/help.js";
|
|
10
|
+
import { logError } from "../utils/logger.js";
|
|
10
11
|
|
|
11
12
|
const [, , command, ...args] = process.argv;
|
|
12
13
|
|
|
13
14
|
async function main() {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
// Show help for no command or explicit help flags
|
|
16
|
+
if (!command || command === "-h" || command === "--help" || command === "help") {
|
|
17
|
+
await runHelp();
|
|
18
|
+
return;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
if (command === "init") {
|
package/commands/help.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Help command implementation
|
|
2
|
+
// Displays banner and available commands
|
|
3
|
+
|
|
4
|
+
import { displayBanner } from "../ui/banner.js";
|
|
5
|
+
import { formatBlock } from "../ui/formatting.js";
|
|
6
|
+
|
|
7
|
+
const HELP_TEXT = `Usage: codeproof <command>
|
|
8
|
+
|
|
9
|
+
Commands:
|
|
10
|
+
init Initialize CodeProof in a Git repository
|
|
11
|
+
run Run CodeProof checks
|
|
12
|
+
report@dashboard Send latest report and show dashboard link
|
|
13
|
+
move-secret Move high-risk secrets to .env
|
|
14
|
+
ignore Temporarily disable commit enforcement
|
|
15
|
+
apply Re-enable commit enforcement
|
|
16
|
+
whoami Show the local CodeProof client ID
|
|
17
|
+
help Show this help menu
|
|
18
|
+
-h, --help Show this help menu
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
codeproof init
|
|
22
|
+
codeproof run
|
|
23
|
+
codeproof move-secret --yes
|
|
24
|
+
codeproof help
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Display help information with banner
|
|
29
|
+
*/
|
|
30
|
+
export async function runHelp() {
|
|
31
|
+
displayBanner();
|
|
32
|
+
formatBlock(HELP_TEXT);
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
package/commands/init.js
CHANGED
|
@@ -5,12 +5,16 @@ import { logInfo, logSuccess, logWarn } from "../utils/logger.js";
|
|
|
5
5
|
import { detectProjectType } from "../utils/projectType.js";
|
|
6
6
|
import { installPreCommitHook } from "../hooks/preCommit.js";
|
|
7
7
|
import { showWelcomeScreen } from "../ui/welcomeScreen.js";
|
|
8
|
+
import { displayBanner } from "../ui/banner.js";
|
|
8
9
|
import { getClientId } from "../core/identity.js";
|
|
9
10
|
import { randomUUID } from "crypto";
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
export async function runInit({ cwd }) {
|
|
15
|
+
// Display banner at the start of initialization
|
|
16
|
+
displayBanner();
|
|
17
|
+
|
|
14
18
|
logInfo("Initializing CodeProof...");
|
|
15
19
|
|
|
16
20
|
getClientId();
|
package/commands/moveSecret.js
CHANGED
|
@@ -52,7 +52,9 @@ function confirmProceed(message) {
|
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
export async function runMoveSecret({ cwd }) {
|
|
55
|
+
export async function runMoveSecret({ args, cwd }) {
|
|
56
|
+
// Check for --yes flag to skip confirmation (for testing/CI)
|
|
57
|
+
const autoConfirm = args?.includes("--yes") || args?.includes("-y");
|
|
56
58
|
// Boundary: remediation reads reports only and must not depend on analysis state.
|
|
57
59
|
ensureGitRepo(cwd);
|
|
58
60
|
const gitRoot = getGitRoot(cwd);
|
|
@@ -74,27 +76,49 @@ export async function runMoveSecret({ cwd }) {
|
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
warnExperimentalOnce("Experimental feature enabled: move-secret.", logWarn);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
|
|
80
|
+
// Safety: Read latest report with validation
|
|
81
|
+
const reportData = readLatestReport(gitRoot);
|
|
82
|
+
if (!reportData) {
|
|
80
83
|
logWarn("No reports found. Run 'codeproof run' first.");
|
|
81
84
|
process.exit(0);
|
|
82
85
|
}
|
|
83
86
|
|
|
87
|
+
// The reportReader returns { report: parsedJSON, reportPath }
|
|
88
|
+
// The parsedJSON has structure: { projectId, clientId, project, report: {...}, ...}
|
|
89
|
+
// So reportData.report.report contains the actual report with findings
|
|
90
|
+
const fullReport = reportData.report;
|
|
91
|
+
const latestReport = fullReport.report || {};
|
|
92
|
+
|
|
93
|
+
if (!Array.isArray(latestReport.findings)) {
|
|
94
|
+
logWarn("Report has no findings array. Invalid report format.");
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
|
|
84
98
|
const excludes = getDefaultExcludes();
|
|
85
99
|
|
|
86
100
|
const eligible = latestReport.findings.filter((finding) => {
|
|
87
|
-
|
|
101
|
+
// Debug logging
|
|
102
|
+
const isSecret = finding.ruleId?.startsWith("secret.");
|
|
103
|
+
if (!isSecret) {
|
|
104
|
+
if (process.env.DEBUG_MOVE_SECRET) logWarn(`Filtered (not secret): ${finding.ruleId}`);
|
|
88
105
|
return false;
|
|
89
106
|
}
|
|
90
|
-
|
|
107
|
+
|
|
108
|
+
// Process findings where severity is "block" OR "high"
|
|
109
|
+
const isHighRisk = finding.severity === "block" || finding.severity === "high";
|
|
110
|
+
if (!isHighRisk) {
|
|
111
|
+
if (process.env.DEBUG_MOVE_SECRET) logWarn(`Filtered (low severity): ${finding.ruleId} severity=${finding.severity}`);
|
|
91
112
|
return false;
|
|
92
113
|
}
|
|
114
|
+
|
|
93
115
|
if (!finding.filePath || !finding.lineNumber) {
|
|
116
|
+
if (process.env.DEBUG_MOVE_SECRET) logWarn(`Filtered (no path/line): ${finding.ruleId}`);
|
|
94
117
|
return false;
|
|
95
118
|
}
|
|
96
119
|
|
|
97
120
|
if (!finding.codeSnippet) {
|
|
121
|
+
if (process.env.DEBUG_MOVE_SECRET) logWarn(`Filtered (no snippet): ${finding.filePath}:${finding.lineNumber}`);
|
|
98
122
|
return false;
|
|
99
123
|
}
|
|
100
124
|
|
|
@@ -103,10 +127,12 @@ export async function runMoveSecret({ cwd }) {
|
|
|
103
127
|
: path.join(gitRoot, finding.filePath);
|
|
104
128
|
|
|
105
129
|
if (isTestLike(absolutePath)) {
|
|
130
|
+
if (process.env.DEBUG_MOVE_SECRET) logWarn(`Filtered (test-like): ${absolutePath}`);
|
|
106
131
|
return false;
|
|
107
132
|
}
|
|
108
133
|
|
|
109
134
|
if (isIgnoredPath(absolutePath, excludes)) {
|
|
135
|
+
if (process.env.DEBUG_MOVE_SECRET) logWarn(`Filtered (ignored path): ${absolutePath}`);
|
|
110
136
|
return false;
|
|
111
137
|
}
|
|
112
138
|
|
|
@@ -126,7 +152,7 @@ export async function runMoveSecret({ cwd }) {
|
|
|
126
152
|
}
|
|
127
153
|
logInfo(`Secrets to move: ${eligible.length}`);
|
|
128
154
|
|
|
129
|
-
const confirmed = await confirmProceed("Proceed with moving these secrets? (y/N): ");
|
|
155
|
+
const confirmed = autoConfirm || await confirmProceed("Proceed with moving these secrets? (y/N): ");
|
|
130
156
|
if (!confirmed) {
|
|
131
157
|
logInfo("No changes made.");
|
|
132
158
|
process.exit(0);
|
|
@@ -139,6 +165,7 @@ export async function runMoveSecret({ cwd }) {
|
|
|
139
165
|
let secretIndex = 1;
|
|
140
166
|
let secretsMoved = 0;
|
|
141
167
|
const modifiedFiles = new Set();
|
|
168
|
+
const errors = [];
|
|
142
169
|
|
|
143
170
|
for (const finding of eligible) {
|
|
144
171
|
const absolutePath = path.isAbsolute(finding.filePath)
|
|
@@ -150,17 +177,18 @@ export async function runMoveSecret({ cwd }) {
|
|
|
150
177
|
const content = fs.readFileSync(absolutePath, "utf8");
|
|
151
178
|
const lines = content.split(/\r?\n/);
|
|
152
179
|
lineContent = lines[finding.lineNumber - 1] || "";
|
|
153
|
-
} catch {
|
|
154
|
-
|
|
180
|
+
} catch (err) {
|
|
181
|
+
errors.push(`${finding.filePath}:${finding.lineNumber} - unable to read file: ${err.message}`);
|
|
155
182
|
continue;
|
|
156
183
|
}
|
|
157
184
|
|
|
158
185
|
const expectedSecretValue = extractSecretValueFromLine(lineContent);
|
|
159
186
|
if (!expectedSecretValue) {
|
|
160
|
-
|
|
187
|
+
errors.push(`${finding.filePath}:${finding.lineNumber} - unable to extract secret value from line`);
|
|
161
188
|
continue;
|
|
162
189
|
}
|
|
163
190
|
|
|
191
|
+
// Avoid key collisions
|
|
164
192
|
while (existingKeys.has(`CODEPROOF_SECRET_${secretIndex}`)) {
|
|
165
193
|
secretIndex += 1;
|
|
166
194
|
}
|
|
@@ -168,7 +196,12 @@ export async function runMoveSecret({ cwd }) {
|
|
|
168
196
|
const envKey = `CODEPROOF_SECRET_${secretIndex}`;
|
|
169
197
|
|
|
170
198
|
// Safety: keep an original copy before any rewrite.
|
|
171
|
-
|
|
199
|
+
try {
|
|
200
|
+
backupFileOnce(gitRoot, absolutePath, backedUp);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
errors.push(`${finding.filePath} - backup failed: ${err.message}`);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
172
205
|
|
|
173
206
|
const result = replaceSecretInFile({
|
|
174
207
|
filePath: absolutePath,
|
|
@@ -179,7 +212,7 @@ export async function runMoveSecret({ cwd }) {
|
|
|
179
212
|
});
|
|
180
213
|
|
|
181
214
|
if (!result.updated) {
|
|
182
|
-
|
|
215
|
+
errors.push(`${finding.filePath}:${finding.lineNumber} - ${result.reason}`);
|
|
183
216
|
continue;
|
|
184
217
|
}
|
|
185
218
|
|
|
@@ -190,13 +223,32 @@ export async function runMoveSecret({ cwd }) {
|
|
|
190
223
|
modifiedFiles.add(absolutePath);
|
|
191
224
|
|
|
192
225
|
const relative = path.relative(gitRoot, absolutePath) || absolutePath;
|
|
193
|
-
logInfo(
|
|
226
|
+
logInfo(`✓ Updated ${relative}:${finding.lineNumber} → process.env.${envKey}`);
|
|
194
227
|
}
|
|
195
228
|
|
|
196
|
-
|
|
229
|
+
// Append env entries atomically
|
|
230
|
+
try {
|
|
231
|
+
appendEnvEntries(envPath, newEntries);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
logWarn(`Failed to write .env entries: ${err.message}`);
|
|
234
|
+
errors.push(`env-write: ${err.message}`);
|
|
235
|
+
}
|
|
197
236
|
|
|
198
|
-
|
|
237
|
+
// Output summary
|
|
238
|
+
logInfo("");
|
|
239
|
+
logInfo("═══════════════════════════════════════════");
|
|
240
|
+
logInfo("Secret Move Summary");
|
|
241
|
+
logInfo("═══════════════════════════════════════════");
|
|
242
|
+
logInfo(`Secrets processed: ${eligible.length}`);
|
|
199
243
|
logInfo(`Secrets moved: ${secretsMoved}`);
|
|
200
244
|
logInfo(`Files modified: ${modifiedFiles.size}`);
|
|
201
245
|
logInfo(`Backup location: ${path.join(gitRoot, ".codeproof-backup")}`);
|
|
246
|
+
|
|
247
|
+
if (errors.length > 0) {
|
|
248
|
+
logInfo("");
|
|
249
|
+
logWarn(`Errors/Skipped (${errors.length}):`);
|
|
250
|
+
errors.forEach((err) => logWarn(` - ${err}`));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
logInfo("═══════════════════════════════════════════");
|
|
202
254
|
}
|
package/commands/whoami.js
CHANGED
|
@@ -13,7 +13,7 @@ export async function runWhoAmI() {
|
|
|
13
13
|
logInfo(clientId);
|
|
14
14
|
logInfo("");
|
|
15
15
|
logInfo("Use this ID to log in at:");
|
|
16
|
-
logInfo("
|
|
16
|
+
logInfo("http://localhost:3000/login");
|
|
17
17
|
logInfo("");
|
|
18
18
|
logInfo(`Config: ${getGlobalConfigPath()}`);
|
|
19
19
|
}
|
package/package.json
CHANGED
package/ui/banner.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// ASCII banner for CodeProof CLI
|
|
2
|
+
// Professional-grade banner displayed on init and help commands
|
|
3
|
+
|
|
4
|
+
const BANNER_TEXT = `
|
|
5
|
+
██████╗ ██████╗ ██████╗ ███████╗██████╗ ██████╗ ██████╗ ██████╗ ███████╗
|
|
6
|
+
██╔════╝██╔═══██╗██╔══██╗██╔════╝██╔══██╗██╔══██╗██╔═══██╗██╔════╝ ██╔════╝
|
|
7
|
+
██║ ██║ ██║██████╔╝█████╗ ██████╔╝██████╔╝██║ ██║██║ ███╗█████╗
|
|
8
|
+
██║ ██║ ██║██╔═══╝ ██╔══╝ ██╔═══╝ ██╔══██╗██║ ██║██║ ██║██╔══╝
|
|
9
|
+
╚██████╗╚██████╔╝██║ ███████╗██║ ██║ ██║╚██████╔╝╚██████╔╝███████╗
|
|
10
|
+
╚═════╝ ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝
|
|
11
|
+
`;
|
|
12
|
+
|
|
13
|
+
const TAGLINE = "AI-Powered Pre-Commit Security Enforcement";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Display the CodeProof banner with tagline.
|
|
17
|
+
* Used on init and help commands.
|
|
18
|
+
*/
|
|
19
|
+
export function displayBanner() {
|
|
20
|
+
console.log(BANNER_TEXT);
|
|
21
|
+
console.log(` ${TAGLINE}\n`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get the banner text for programmatic use.
|
|
26
|
+
*/
|
|
27
|
+
export function getBannerText() {
|
|
28
|
+
return BANNER_TEXT;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get the tagline for programmatic use.
|
|
33
|
+
*/
|
|
34
|
+
export function getTagline() {
|
|
35
|
+
return TAGLINE;
|
|
36
|
+
}
|
package/ui/formatting.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// CLI output formatting helpers
|
|
2
|
+
// Provides consistent, professional-grade output formatting
|
|
3
|
+
|
|
4
|
+
const DIVIDER = "─".repeat(50);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Log informational message
|
|
8
|
+
*/
|
|
9
|
+
function logInfo(message) {
|
|
10
|
+
console.log(`[codeproof] ${message}`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Log success message (for completed actions)
|
|
15
|
+
*/
|
|
16
|
+
function logSuccess(message) {
|
|
17
|
+
console.log(`[codeproof] ✓ ${message}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Log warning message
|
|
22
|
+
*/
|
|
23
|
+
function logWarning(message) {
|
|
24
|
+
console.warn(`[codeproof] ⚠ ${message}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Log error message
|
|
29
|
+
*/
|
|
30
|
+
function logError(message) {
|
|
31
|
+
console.error(`[codeproof] ✗ ${message}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Print a section header with dividers
|
|
36
|
+
* Usage: sectionHeader("Initializing CodeProof")
|
|
37
|
+
*/
|
|
38
|
+
function sectionHeader(title) {
|
|
39
|
+
console.log(DIVIDER);
|
|
40
|
+
console.log(title.padStart(title.length + Math.floor((DIVIDER.length - title.length) / 2)));
|
|
41
|
+
console.log(DIVIDER);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Print structured output block (like a code block)
|
|
46
|
+
*/
|
|
47
|
+
function formatBlock(content) {
|
|
48
|
+
const lines = content.split("\n");
|
|
49
|
+
lines.forEach((line) => {
|
|
50
|
+
if (line.trim()) {
|
|
51
|
+
console.log(` ${line}`);
|
|
52
|
+
} else {
|
|
53
|
+
console.log("");
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Print a key-value pair with alignment
|
|
60
|
+
*/
|
|
61
|
+
function logKeyValue(key, value, indent = 2) {
|
|
62
|
+
const spaces = " ".repeat(indent);
|
|
63
|
+
console.log(`${spaces}${key.padEnd(20)} ${value}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Print an unordered list
|
|
68
|
+
*/
|
|
69
|
+
function logList(items, indent = 2) {
|
|
70
|
+
const spaces = " ".repeat(indent);
|
|
71
|
+
items.forEach((item) => {
|
|
72
|
+
console.log(`${spaces}• ${item}`);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export { logInfo, logSuccess, logWarning, logError, sectionHeader, formatBlock, logKeyValue, logList, DIVIDER };
|