codeproof 1.0.0 → 1.0.2

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 CHANGED
@@ -3,13 +3,16 @@ import { runInit } from "../commands/init.js";
3
3
  import { runCli } from "../commands/run.js";
4
4
  import { runReportDashboard } from "../commands/reportDashboard.js";
5
5
  import { runMoveSecret } from "../commands/moveSecret.js";
6
+ import { runWhoAmI } from "../commands/whoami.js";
7
+ import { runIgnore } from "../commands/ignore.js";
8
+ import { runApply } from "../commands/apply.js";
6
9
  import { logError, logInfo } from "../utils/logger.js";
7
10
 
8
11
  const [, , command, ...args] = process.argv;
9
12
 
10
13
  async function main() {
11
14
  if (!command || command === "-h" || command === "--help") {
12
- logInfo("Usage: codeproof <command>\n\nCommands:\n init Initialize CodeProof in a Git repository\n run Run CodeProof checks (stub)\n report@dashboard Send latest report and show dashboard link\n move-secret Move high-confidence secrets to .env");
15
+ logInfo("Usage: codeproof <command>\n\nCommands:\n init Initialize CodeProof in a Git repository\n run Run CodeProof checks (stub)\n report@dashboard Send latest report and show dashboard link\n move-secret Move high-confidence secrets to .env\n ignore Temporarily disable commit enforcement\n apply Re-enable commit enforcement\n whoami Show the local CodeProof client ID");
13
16
  process.exit(0);
14
17
  }
15
18
 
@@ -33,6 +36,21 @@ async function main() {
33
36
  return;
34
37
  }
35
38
 
39
+ if (command === "ignore") {
40
+ await runIgnore({ args, cwd: process.cwd() });
41
+ return;
42
+ }
43
+
44
+ if (command === "apply") {
45
+ await runApply({ args, cwd: process.cwd() });
46
+ return;
47
+ }
48
+
49
+ if (command === "whoami") {
50
+ await runWhoAmI();
51
+ return;
52
+ }
53
+
36
54
  logError(`Unknown command: ${command}`);
37
55
  process.exit(1);
38
56
  }
@@ -0,0 +1,32 @@
1
+ import { ensureGitRepo, getGitRoot } from "../utils/git.js";
2
+ import { logError, logInfo, logSuccess, logWarn } from "../utils/logger.js";
3
+ import { getEnforcementState, setEnforcementState } from "../core/enforcement.js";
4
+
5
+ export async function runApply({ cwd }) {
6
+ // Re-enable enforcement explicitly to restore pre-commit blocking.
7
+ ensureGitRepo(cwd);
8
+ const gitRoot = getGitRoot(cwd);
9
+
10
+ let current = "enabled";
11
+ try {
12
+ current = getEnforcementState(gitRoot);
13
+ } catch (error) {
14
+ logError(error?.message || "Unable to read codeproof.config.json.");
15
+ process.exit(1);
16
+ }
17
+
18
+ if (current === "enabled") {
19
+ logWarn("CodeProof enforcement is already enabled.");
20
+ return;
21
+ }
22
+
23
+ try {
24
+ setEnforcementState(gitRoot, "enabled");
25
+ } catch (error) {
26
+ logError(error?.message || "Unable to update codeproof.config.json.");
27
+ process.exit(1);
28
+ }
29
+
30
+ logSuccess("CodeProof enforcement re-enabled.");
31
+ logInfo("Pre-commit protection active.");
32
+ }
@@ -0,0 +1,32 @@
1
+ import { ensureGitRepo, getGitRoot } from "../utils/git.js";
2
+ import { logError, logInfo, logSuccess, logWarn } from "../utils/logger.js";
3
+ import { getEnforcementState, setEnforcementState } from "../core/enforcement.js";
4
+
5
+ export async function runIgnore({ cwd }) {
6
+ // Controlled bypass: disabling enforcement is explicit and project-scoped.
7
+ ensureGitRepo(cwd);
8
+ const gitRoot = getGitRoot(cwd);
9
+
10
+ let current = "enabled";
11
+ try {
12
+ current = getEnforcementState(gitRoot);
13
+ } catch (error) {
14
+ logError(error?.message || "Unable to read codeproof.config.json.");
15
+ process.exit(1);
16
+ }
17
+
18
+ if (current === "disabled") {
19
+ logWarn("CodeProof enforcement is already disabled.");
20
+ return;
21
+ }
22
+
23
+ try {
24
+ setEnforcementState(gitRoot, "disabled");
25
+ } catch (error) {
26
+ logError(error?.message || "Unable to update codeproof.config.json.");
27
+ process.exit(1);
28
+ }
29
+
30
+ logSuccess("CodeProof enforcement disabled.");
31
+ logInfo("Commits will not be blocked until `codeproof apply` is run.");
32
+ }
package/commands/init.js CHANGED
@@ -5,11 +5,15 @@ 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 { getClientId } from "../core/identity.js";
9
+ import { randomUUID } from "crypto";
8
10
 
9
11
 
10
12
 
11
13
  export async function runInit({ cwd }) {
12
14
  logInfo("Initializing CodeProof...");
15
+
16
+ getClientId();
13
17
 
14
18
  ensureGitRepo(cwd);
15
19
  logSuccess("Git repository detected.");
@@ -23,11 +27,30 @@ export async function runInit({ cwd }) {
23
27
  const configPath = path.join(gitRoot, "codeproof.config.json");
24
28
  // Avoid overwriting user configuration to keep init idempotent.
25
29
  if (fs.existsSync(configPath)) {
26
- logWarn("Config already exists. Skipping creation.");
30
+ let updated = false;
31
+ try {
32
+ const raw = fs.readFileSync(configPath, "utf8");
33
+ const existing = JSON.parse(raw);
34
+ if (!existing.projectId) {
35
+ existing.projectId = randomUUID();
36
+ updated = true;
37
+ fs.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
38
+ }
39
+ } catch {
40
+ logWarn("Config already exists but could not be updated.");
41
+ }
42
+
43
+ if (updated) {
44
+ logSuccess("Added projectId to codeproof.config.json");
45
+ } else {
46
+ logWarn("Config already exists. Skipping creation.");
47
+ }
27
48
  } else {
28
49
  const config = {
50
+ projectId: randomUUID(),
29
51
  projectType,
30
52
  scanMode: "staged",
53
+ enforcement: "enabled",
31
54
  features: {
32
55
  reporting: true,
33
56
  integration: false,
@@ -48,6 +71,18 @@ export async function runInit({ cwd }) {
48
71
  logSuccess("Created codeproof.config.json");
49
72
  }
50
73
 
74
+ try {
75
+ const raw = fs.readFileSync(configPath, "utf8");
76
+ const existing = JSON.parse(raw);
77
+ if (!existing.enforcement) {
78
+ existing.enforcement = "enabled";
79
+ fs.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
80
+ logSuccess("Added enforcement=enabled to codeproof.config.json");
81
+ }
82
+ } catch {
83
+ logWarn("Unable to update enforcement in codeproof.config.json.");
84
+ }
85
+
51
86
  installPreCommitHook(gitRoot);
52
87
  logSuccess("Pre-commit hook installed.");
53
88
 
@@ -8,6 +8,7 @@ import { ensureEnvFile, readEnvKeys, appendEnvEntries } from "../utils/envManage
8
8
  import { backupFileOnce, extractSecretValueFromLine, replaceSecretInFile } from "../utils/fileRewriter.js";
9
9
  import { resolveFeatureFlags, isVerbose } from "../core/featureFlags.js";
10
10
  import { reportFeatureDisabled, warnExperimentalOnce } from "../core/safetyGuards.js";
11
+ import { readLatestReport } from "../reporting/reportReader.js";
11
12
 
12
13
  const TEST_PATH_HINTS = [
13
14
  "test",
@@ -37,24 +38,6 @@ function isIgnoredPath(filePath, excludes) {
37
38
  return false;
38
39
  }
39
40
 
40
- function readLatestReport(reportPath) {
41
- if (!fs.existsSync(reportPath)) {
42
- return null;
43
- }
44
-
45
- const content = fs.readFileSync(reportPath, "utf8");
46
- const lines = content.split(/\r?\n/).filter(Boolean);
47
- if (lines.length === 0) {
48
- return null;
49
- }
50
-
51
- try {
52
- return JSON.parse(lines[lines.length - 1]);
53
- } catch {
54
- return null;
55
- }
56
- }
57
-
58
41
  function confirmProceed(message) {
59
42
  return new Promise((resolve) => {
60
43
  const rl = readline.createInterface({
@@ -73,7 +56,6 @@ export async function runMoveSecret({ cwd }) {
73
56
  // Boundary: remediation reads reports only and must not depend on analysis state.
74
57
  ensureGitRepo(cwd);
75
58
  const gitRoot = getGitRoot(cwd);
76
- const reportPath = path.join(gitRoot, "codeproof-report.log");
77
59
  const configPath = path.join(gitRoot, "codeproof.config.json");
78
60
  let config = {};
79
61
  try {
@@ -92,7 +74,7 @@ export async function runMoveSecret({ cwd }) {
92
74
  }
93
75
 
94
76
  warnExperimentalOnce("Experimental feature enabled: move-secret.", logWarn);
95
- const latestReport = readLatestReport(reportPath);
77
+ const latestReport = readLatestReport(gitRoot)?.report || null;
96
78
 
97
79
  if (!latestReport || !Array.isArray(latestReport.findings)) {
98
80
  logWarn("No reports found. Run 'codeproof run' first.");
@@ -5,6 +5,7 @@ import { logError, logInfo, logWarn } from "../utils/logger.js";
5
5
  import { sendReportToServer } from "../utils/apiClient.js";
6
6
  import { resolveFeatureFlags, isVerbose } from "../core/featureFlags.js";
7
7
  import { reportFeatureDisabled, withFailOpenIntegration } from "../core/safetyGuards.js";
8
+ import { readLatestReport } from "../reporting/reportReader.js";
8
9
 
9
10
  function readConfig(configPath) {
10
11
  if (!fs.existsSync(configPath)) {
@@ -21,24 +22,6 @@ function readConfig(configPath) {
21
22
  }
22
23
  }
23
24
 
24
- function readLatestReport(reportPath) {
25
- if (!fs.existsSync(reportPath)) {
26
- return null;
27
- }
28
-
29
- const content = fs.readFileSync(reportPath, "utf8");
30
- const lines = content.split(/\r?\n/).filter(Boolean);
31
- if (lines.length === 0) {
32
- return null;
33
- }
34
-
35
- try {
36
- return JSON.parse(lines[lines.length - 1]);
37
- } catch {
38
- return null;
39
- }
40
- }
41
-
42
25
  export async function runReportDashboard({ cwd }) {
43
26
  // Boundary: CLI orchestration only. Avoid importing this module in lower layers.
44
27
  ensureGitRepo(cwd);
@@ -49,30 +32,26 @@ export async function runReportDashboard({ cwd }) {
49
32
  const verbose = isVerbose(config);
50
33
 
51
34
  if (features.reporting) {
52
- const reportPath = path.join(gitRoot, "codeproof-report.log");
53
- const latestReport = readLatestReport(reportPath);
35
+ const latestEntry = readLatestReport(gitRoot);
36
+ const latestReport = latestEntry?.report || null;
54
37
 
55
38
  if (!latestReport) {
56
- logWarn("No reports found. Run 'codeproof run' first.");
39
+ logWarn("No reports found. Run `codeproof run` first.");
57
40
  } else {
58
41
  const integration = config?.integration || {};
59
- const integrationEnabled = features.integration && Boolean(integration.enabled);
60
- if (integrationEnabled) {
61
- // Integrations are fail-open: skip silently when disabled, never throw on network errors.
62
- withFailOpenIntegration(() => {
63
- sendReportToServer(latestReport, {
64
- enabled: true,
65
- endpointUrl: integration.endpointUrl
66
- });
42
+ // Integrations are fail-open: never throw on network errors.
43
+ withFailOpenIntegration(() => {
44
+ sendReportToServer(latestReport, {
45
+ enabled: true,
46
+ endpointUrl: integration.endpointUrl
67
47
  });
68
- } else {
69
- reportFeatureDisabled("Integration", verbose, logInfo);
48
+ });
49
+
50
+ if (latestReport?.projectId) {
51
+ logInfo(`View dashboard: https://dashboard.codeproof.dev/project/${latestReport.projectId}`);
70
52
  }
71
53
  }
72
54
  } else {
73
55
  reportFeatureDisabled("Reporting", verbose, logInfo);
74
56
  }
75
-
76
- const projectId = encodeURIComponent(path.basename(gitRoot) || "project");
77
- logInfo(`Dashboard: https://dashboard.codeproof.dev/project/${projectId}`);
78
57
  }
package/commands/run.js CHANGED
@@ -2,15 +2,17 @@ import fs from "fs";
2
2
  import path from "path";
3
3
  import { ensureGitRepo, getGitRoot, getStagedFiles } from "../utils/git.js";
4
4
  import { logError, logInfo, logSuccess, logWarn } from "../utils/logger.js";
5
- import { getDefaultExcludes, isBinaryFile, listFilesRecursive } from "../utils/files.js";
5
+ import { buildScanTargets } from "../utils/fileScanner.js";
6
6
  import { runRuleEngine } from "../engine/ruleEngine.js";
7
7
  import { analyze } from "../engine/aiAnalyzer.js";
8
8
  import { mergeDecisions } from "../engine/decisionMerger.js";
9
9
  import { buildAiInputs, buildProjectContext } from "../engine/contextBuilder.js";
10
10
  import { buildReport } from "../reporting/reportBuilder.js";
11
- import { appendReport } from "../reporting/reportWriter.js";
11
+ import { writeReport } from "../reporting/reportWriter.js";
12
12
  import { sendReportToServer } from "../utils/apiClient.js";
13
13
  import { resolveFeatureFlags, isVerbose } from "../core/featureFlags.js";
14
+ import { getClientId } from "../core/identity.js";
15
+ import { getEnforcementState } from "../core/enforcement.js";
14
16
  import {
15
17
  reportFeatureDisabled,
16
18
  warnExperimentalOnce,
@@ -18,6 +20,7 @@ import {
18
20
  withFailOpenIntegration,
19
21
  withFailOpenReporting
20
22
  } from "../core/safetyGuards.js";
23
+ import { randomUUID } from "crypto";
21
24
 
22
25
  function readConfig(configPath) {
23
26
  if (!fs.existsSync(configPath)) {
@@ -34,23 +37,7 @@ function readConfig(configPath) {
34
37
  }
35
38
  }
36
39
 
37
- function normalizeScopeFiles(files, gitRoot) {
38
- return files
39
- .map((filePath) => (path.isAbsolute(filePath) ? filePath : path.join(gitRoot, filePath)))
40
- .filter((filePath) => {
41
- try {
42
- return fs.statSync(filePath).isFile();
43
- } catch {
44
- return false;
45
- }
46
- });
47
- }
48
-
49
- function filterBinaryFiles(files) {
50
- return files.filter((filePath) => !isBinaryFile(filePath));
51
- }
52
-
53
- export async function runCli({ cwd }) {
40
+ export async function runCli({ args = [], cwd }) {
54
41
  // Boundary: CLI orchestration only. Avoid importing this module in lower layers.
55
42
  logInfo("CodeProof run started.");
56
43
 
@@ -60,6 +47,20 @@ export async function runCli({ cwd }) {
60
47
  const config = readConfig(configPath);
61
48
  const features = resolveFeatureFlags(config);
62
49
  const verbose = isVerbose(config);
50
+ let enforcement = "enabled";
51
+ try {
52
+ enforcement = getEnforcementState(gitRoot);
53
+ } catch (error) {
54
+ logError(error?.message || "Unable to read enforcement state.");
55
+ process.exit(1);
56
+ }
57
+ const isPreCommit = args.includes("--precommit") || Boolean(process.env.CODEPROOF_PRECOMMIT);
58
+
59
+ if (isPreCommit && enforcement === "disabled") {
60
+ logWarn("CodeProof enforcement is temporarily disabled.");
61
+ logInfo("Commit allowed.");
62
+ process.exit(0);
63
+ }
63
64
 
64
65
  if (!config.scanMode) {
65
66
  logError("Config missing scanMode. Expected 'staged' or 'full'.");
@@ -71,27 +72,30 @@ export async function runCli({ cwd }) {
71
72
 
72
73
  if (scanMode === "staged") {
73
74
  logInfo("Scan mode: staged");
74
- const staged = getStagedFiles(gitRoot);
75
- targets = normalizeScopeFiles(staged, gitRoot);
75
+ targets = buildScanTargets({
76
+ gitRoot,
77
+ scanMode,
78
+ stagedFiles: getStagedFiles(gitRoot)
79
+ });
76
80
  } else if (scanMode === "full") {
77
81
  logInfo("Scan mode: full");
78
- const excludes = getDefaultExcludes();
79
- const allFiles = listFilesRecursive(gitRoot, excludes);
80
- targets = normalizeScopeFiles(allFiles, gitRoot);
82
+ targets = buildScanTargets({
83
+ gitRoot,
84
+ scanMode,
85
+ stagedFiles: []
86
+ });
81
87
  } else {
82
88
  logError("Invalid scanMode. Expected 'staged' or 'full'.");
83
89
  process.exit(1);
84
90
  }
85
91
 
86
- const filtered = filterBinaryFiles(targets);
87
-
88
- if (filtered.length === 0) {
92
+ if (targets.length === 0) {
89
93
  logWarn("No relevant files found. Exiting.");
90
94
  // Exit code 0 allows the Git commit to continue.
91
95
  process.exit(0);
92
96
  }
93
97
 
94
- const { findings, escalations } = runRuleEngine({ files: filtered });
98
+ const { findings, escalations } = runRuleEngine({ files: targets });
95
99
  const projectContext = buildProjectContext({ gitRoot, config });
96
100
  let aiInputs = [];
97
101
  if (features.aiEscalation) {
@@ -116,18 +120,22 @@ export async function runCli({ cwd }) {
116
120
  if (features.reporting) {
117
121
  withFailOpenReporting(() => {
118
122
  const timestamp = new Date().toISOString();
119
- const runId = `${Date.now()}-${process.pid}`;
123
+ const reportId = randomUUID();
124
+ const projectId = config.projectId || "";
125
+ const clientId = getClientId();
120
126
  const report = buildReport({
121
127
  projectRoot: gitRoot,
128
+ projectId,
129
+ clientId,
130
+ reportId,
122
131
  scanMode,
123
- filesScannedCount: filtered.length,
132
+ filesScannedCount: targets.length,
124
133
  baselineFindings: [...findings, ...escalations],
125
134
  aiReviewed,
126
- runId,
127
135
  timestamp
128
136
  });
129
137
  // Reporting is fail-open: never block commits if logging fails.
130
- appendReport({ projectRoot: gitRoot, report });
138
+ writeReport({ projectRoot: gitRoot, report });
131
139
 
132
140
  const integration = config?.integration || {};
133
141
  const integrationEnabled = features.integration && Boolean(integration.enabled);
@@ -0,0 +1,19 @@
1
+ import { readClientId, getGlobalConfigPath } from "../core/identity.js";
2
+ import { logError, logInfo } from "../utils/logger.js";
3
+
4
+ export async function runWhoAmI() {
5
+ const clientId = readClientId();
6
+
7
+ if (!clientId) {
8
+ logError("CodeProof not initialized. Run codeproof init first.");
9
+ process.exit(1);
10
+ }
11
+
12
+ logInfo("CodeProof Client ID:");
13
+ logInfo(clientId);
14
+ logInfo("");
15
+ logInfo("Use this ID to log in at:");
16
+ logInfo("https://your-dashboard-url.com/login");
17
+ logInfo("");
18
+ logInfo(`Config: ${getGlobalConfigPath()}`);
19
+ }
@@ -0,0 +1,51 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ const ENFORCEMENT_ENABLED = "enabled";
5
+ const ENFORCEMENT_DISABLED = "disabled";
6
+
7
+ function getConfigPath(gitRoot) {
8
+ return path.join(gitRoot, "codeproof.config.json");
9
+ }
10
+
11
+ function readConfig(gitRoot) {
12
+ const configPath = getConfigPath(gitRoot);
13
+ if (!fs.existsSync(configPath)) {
14
+ const error = new Error("Missing codeproof.config.json. Run codeproof init first.");
15
+ error.code = "CODEPROOF_CONFIG_MISSING";
16
+ throw error;
17
+ }
18
+
19
+ try {
20
+ const raw = fs.readFileSync(configPath, "utf8");
21
+ return JSON.parse(raw);
22
+ } catch {
23
+ const error = new Error("Invalid codeproof.config.json. Please fix the file.");
24
+ error.code = "CODEPROOF_CONFIG_INVALID";
25
+ throw error;
26
+ }
27
+ }
28
+
29
+ function writeConfig(gitRoot, config) {
30
+ const configPath = getConfigPath(gitRoot);
31
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
32
+ }
33
+
34
+ export function getEnforcementState(gitRoot) {
35
+ const config = readConfig(gitRoot);
36
+ const enforcement = String(config.enforcement || ENFORCEMENT_ENABLED).toLowerCase();
37
+ return enforcement === ENFORCEMENT_DISABLED ? ENFORCEMENT_DISABLED : ENFORCEMENT_ENABLED;
38
+ }
39
+
40
+ export function setEnforcementState(gitRoot, nextState) {
41
+ const config = readConfig(gitRoot);
42
+ const normalized = String(nextState || "").toLowerCase();
43
+ const enforcement = normalized === ENFORCEMENT_DISABLED
44
+ ? ENFORCEMENT_DISABLED
45
+ : ENFORCEMENT_ENABLED;
46
+
47
+ // Security: explicit state keeps this a reversible bypass, not a silent disable.
48
+ const updated = { ...config, enforcement };
49
+ writeConfig(gitRoot, updated);
50
+ return enforcement;
51
+ }
@@ -0,0 +1,78 @@
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+ import { randomUUID } from "crypto";
5
+
6
+ const CONFIG_DIR_NAME = ".codeproof";
7
+ const CONFIG_FILE_NAME = "config.json";
8
+
9
+ function getConfigDir() {
10
+ return path.join(os.homedir(), CONFIG_DIR_NAME);
11
+ }
12
+
13
+ function getConfigPath() {
14
+ return path.join(getConfigDir(), CONFIG_FILE_NAME);
15
+ }
16
+
17
+ function ensureConfigDir() {
18
+ const dir = getConfigDir();
19
+ if (!fs.existsSync(dir)) {
20
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
21
+ }
22
+ try {
23
+ fs.chmodSync(dir, 0o700);
24
+ } catch {
25
+ // Best-effort on platforms that ignore chmod.
26
+ }
27
+ }
28
+
29
+ function writeConfig(config) {
30
+ const filePath = getConfigPath();
31
+ const payload = JSON.stringify(config, null, 2) + "\n";
32
+ fs.writeFileSync(filePath, payload, { encoding: "utf8", mode: 0o600 });
33
+ try {
34
+ fs.chmodSync(filePath, 0o600);
35
+ } catch {
36
+ // Best-effort on platforms that ignore chmod.
37
+ }
38
+ }
39
+
40
+ function readConfig() {
41
+ const filePath = getConfigPath();
42
+ if (!fs.existsSync(filePath)) {
43
+ return null;
44
+ }
45
+
46
+ try {
47
+ const raw = fs.readFileSync(filePath, "utf8");
48
+ return JSON.parse(raw);
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ function ensureClientId() {
55
+ ensureConfigDir();
56
+
57
+ const existing = readConfig();
58
+ if (existing?.clientId) {
59
+ return existing.clientId;
60
+ }
61
+
62
+ const clientId = randomUUID();
63
+ writeConfig({ clientId });
64
+ return clientId;
65
+ }
66
+
67
+ export function getClientId() {
68
+ return ensureClientId();
69
+ }
70
+
71
+ export function readClientId() {
72
+ const existing = readConfig();
73
+ return existing?.clientId ?? null;
74
+ }
75
+
76
+ export function getGlobalConfigPath() {
77
+ return getConfigPath();
78
+ }
@@ -12,7 +12,20 @@ function getHookBlock() {
12
12
  return [
13
13
  "",
14
14
  HOOK_MARKER,
15
- "codeproof run",
15
+ "GIT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)",
16
+ "if [ -n \"$GIT_ROOT\" ]; then",
17
+ " cd \"$GIT_ROOT\" || exit 1",
18
+ "fi",
19
+ "CONFIG_PATH=\"$GIT_ROOT/codeproof.config.json\"",
20
+ "if [ -f \"$CONFIG_PATH\" ]; then",
21
+ " ENFORCEMENT=$(node -e \"const fs=require('fs');try{const c=JSON.parse(fs.readFileSync('$CONFIG_PATH','utf8'));console.log((c.enforcement||'enabled').toLowerCase());}catch(e){console.log('enabled');}\")",
22
+ " if [ \"$ENFORCEMENT\" = \"disabled\" ]; then",
23
+ " echo \"CodeProof enforcement is temporarily disabled.\"",
24
+ " echo \"Commit allowed.\"",
25
+ " exit 0",
26
+ " fi",
27
+ "fi",
28
+ "CODEPROOF_PRECOMMIT=1 codeproof run --precommit",
16
29
  "RESULT=$?",
17
30
  "if [ $RESULT -ne 0 ]; then",
18
31
  " echo \"CodeProof checks failed. Commit blocked.\"",
@@ -36,7 +49,20 @@ export function installPreCommitHook(gitRoot) {
36
49
  "#!/bin/sh",
37
50
  "",
38
51
  "# Auto-generated by CodeProof",
39
- "codeproof run",
52
+ "GIT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)",
53
+ "if [ -n \"$GIT_ROOT\" ]; then",
54
+ " cd \"$GIT_ROOT\" || exit 1",
55
+ "fi",
56
+ "CONFIG_PATH=\"$GIT_ROOT/codeproof.config.json\"",
57
+ "if [ -f \"$CONFIG_PATH\" ]; then",
58
+ " ENFORCEMENT=$(node -e \"const fs=require('fs');try{const c=JSON.parse(fs.readFileSync('$CONFIG_PATH','utf8'));console.log((c.enforcement||'enabled').toLowerCase());}catch(e){console.log('enabled');}\")",
59
+ " if [ \"$ENFORCEMENT\" = \"disabled\" ]; then",
60
+ " echo \"CodeProof enforcement is temporarily disabled.\"",
61
+ " echo \"Commit allowed.\"",
62
+ " exit 0",
63
+ " fi",
64
+ "fi",
65
+ "CODEPROOF_PRECOMMIT=1 codeproof run --precommit",
40
66
  "RESULT=$?",
41
67
  "if [ $RESULT -ne 0 ]; then",
42
68
  " echo \"CodeProof checks failed. Commit blocked.\"",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeproof",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "CodeProof CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,5 +9,8 @@
9
9
  "engines": {
10
10
  "node": ">=18"
11
11
  },
12
- "license": "MIT"
12
+ "license": "MIT",
13
+ "dependencies": {
14
+ "uuid": "^13.0.0"
15
+ }
13
16
  }
@@ -45,11 +45,13 @@ function normalizeSeverity(value) {
45
45
 
46
46
  export function buildReport({
47
47
  projectRoot,
48
+ projectId,
49
+ clientId,
50
+ reportId,
48
51
  scanMode,
49
52
  filesScannedCount,
50
53
  baselineFindings,
51
54
  aiReviewed,
52
- runId,
53
55
  timestamp
54
56
  }) {
55
57
  const aiById = new Map(aiReviewed.map((entry) => [entry.finding.findingId, entry.decision]));
@@ -84,9 +86,10 @@ export function buildReport({
84
86
  : "allowed";
85
87
 
86
88
  return {
87
- runId,
89
+ reportId,
88
90
  timestamp,
89
- projectRoot,
91
+ projectId,
92
+ clientId,
90
93
  scanMode,
91
94
  summary: {
92
95
  totalFilesScanned: filesScannedCount,
@@ -0,0 +1,49 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ const REPORT_DIR_NAME = "codeproof-reports";
5
+ const REPORT_PATTERN = /^report-(\d+)\.json$/;
6
+
7
+ function getReportDir(projectRoot) {
8
+ return path.join(projectRoot, REPORT_DIR_NAME);
9
+ }
10
+
11
+ function getReportNumbers(files) {
12
+ return files
13
+ .map((file) => {
14
+ const match = REPORT_PATTERN.exec(file);
15
+ return match ? Number.parseInt(match[1], 10) : null;
16
+ })
17
+ .filter((value) => Number.isInteger(value));
18
+ }
19
+
20
+ export function readLatestReport(projectRoot) {
21
+ const reportDir = getReportDir(projectRoot);
22
+ if (!fs.existsSync(reportDir)) {
23
+ return null;
24
+ }
25
+
26
+ let files = [];
27
+ try {
28
+ files = fs.readdirSync(reportDir);
29
+ } catch {
30
+ return null;
31
+ }
32
+
33
+ const numbers = getReportNumbers(files).sort((a, b) => b - a);
34
+ if (numbers.length === 0) {
35
+ return null;
36
+ }
37
+
38
+ for (const number of numbers) {
39
+ const reportPath = path.join(reportDir, `report-${number}.json`);
40
+ try {
41
+ const raw = fs.readFileSync(reportPath, "utf8");
42
+ return { report: JSON.parse(raw), reportPath };
43
+ } catch {
44
+ // Skip unreadable or invalid JSON reports instead of crashing.
45
+ }
46
+ }
47
+
48
+ return null;
49
+ }
@@ -1,19 +1,62 @@
1
1
  import fs from "fs";
2
- import os from "os";
3
2
  import path from "path";
4
3
 
4
+ const REPORT_DIR_NAME = "codeproof-reports";
5
+ const REPORT_PREFIX = "report-";
6
+ const REPORT_SUFFIX = ".json";
7
+ const REPORT_PATTERN = /^report-(\d+)\.json$/;
8
+
5
9
  // Boundary: reporting storage only. Must not import rule logic, AI logic, or integrations.
6
10
 
7
- export function appendReport({ projectRoot, report }) {
8
- const reportPath = path.join(projectRoot, "codeproof-report.log");
9
- const line = JSON.stringify(report) + os.EOL;
11
+ function getReportDir(projectRoot) {
12
+ return path.join(projectRoot, REPORT_DIR_NAME);
13
+ }
14
+
15
+ function ensureReportDir(reportDir) {
16
+ if (!fs.existsSync(reportDir)) {
17
+ fs.mkdirSync(reportDir, { recursive: true });
18
+ }
19
+ }
10
20
 
11
- // Append-only to preserve an immutable audit trail of every run.
12
- const fileHandle = fs.openSync(reportPath, "a");
21
+ function getNextReportNumber(reportDir) {
22
+ let files = [];
13
23
  try {
14
- fs.writeSync(fileHandle, line, null, "utf8");
15
- fs.fsyncSync(fileHandle);
16
- } finally {
17
- fs.closeSync(fileHandle);
24
+ files = fs.readdirSync(reportDir);
25
+ } catch {
26
+ return 1;
27
+ }
28
+
29
+ const numbers = files
30
+ .map((file) => {
31
+ const match = REPORT_PATTERN.exec(file);
32
+ return match ? Number.parseInt(match[1], 10) : null;
33
+ })
34
+ .filter((value) => Number.isInteger(value));
35
+
36
+ if (numbers.length === 0) {
37
+ return 1;
18
38
  }
39
+
40
+ return Math.max(...numbers) + 1;
41
+ }
42
+
43
+ export function writeReport({ projectRoot, report }) {
44
+ const reportDir = getReportDir(projectRoot);
45
+ ensureReportDir(reportDir);
46
+
47
+ // Per-run JSON keeps every audit entry immutable and easy to archive.
48
+ let reportNumber = getNextReportNumber(reportDir);
49
+ let reportPath = path.join(reportDir, `${REPORT_PREFIX}${reportNumber}${REPORT_SUFFIX}`);
50
+ while (fs.existsSync(reportPath)) {
51
+ reportNumber += 1;
52
+ reportPath = path.join(reportDir, `${REPORT_PREFIX}${reportNumber}${REPORT_SUFFIX}`);
53
+ }
54
+
55
+ // Use numeric sequencing over timestamps to avoid collisions in fast CI runs.
56
+ const tempPath = path.join(reportDir, `.tmp-${process.pid}-${Date.now()}.json`);
57
+ const payload = JSON.stringify(report, null, 2) + "\n";
58
+ fs.writeFileSync(tempPath, payload, "utf8");
59
+ fs.renameSync(tempPath, reportPath);
60
+
61
+ return reportPath;
19
62
  }
@@ -0,0 +1,40 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { filterIgnoredFiles } from "./gitIgnore.js";
4
+ import { getDefaultExcludes, isBinaryFile, listFilesRecursive } from "./files.js";
5
+
6
+ function normalizeScopeFiles(files, gitRoot) {
7
+ return files
8
+ .map((filePath) => (path.isAbsolute(filePath) ? filePath : path.join(gitRoot, filePath)))
9
+ .filter((filePath) => {
10
+ try {
11
+ return fs.statSync(filePath).isFile();
12
+ } catch {
13
+ return false;
14
+ }
15
+ });
16
+ }
17
+
18
+ function isExcludedByDefault(filePath, gitRoot, excludes) {
19
+ const relative = path.relative(gitRoot, filePath);
20
+ const segments = relative.split(path.sep);
21
+ return segments.some((segment) => excludes.has(segment));
22
+ }
23
+
24
+ export function buildScanTargets({ gitRoot, scanMode, stagedFiles }) {
25
+ const excludes = getDefaultExcludes();
26
+
27
+ let scopeFiles = [];
28
+ if (scanMode === "staged") {
29
+ const normalized = normalizeScopeFiles(stagedFiles, gitRoot);
30
+ scopeFiles = normalized.filter((filePath) => !isExcludedByDefault(filePath, gitRoot, excludes));
31
+ } else {
32
+ scopeFiles = normalizeScopeFiles(listFilesRecursive(gitRoot, excludes), gitRoot);
33
+ }
34
+
35
+ // Respect .gitignore via `git check-ignore` so scans only touch tracked sources.
36
+ const notIgnored = filterIgnoredFiles({ gitRoot, filePaths: scopeFiles });
37
+
38
+ // Exclude binary files to avoid false positives and wasted I/O.
39
+ return notIgnored.filter((filePath) => !isBinaryFile(filePath));
40
+ }
@@ -0,0 +1,55 @@
1
+ import path from "path";
2
+ import { spawnSync } from "child_process";
3
+
4
+ function normalizePathForGit(filePath, gitRoot) {
5
+ const relative = path.isAbsolute(filePath)
6
+ ? path.relative(gitRoot, filePath)
7
+ : filePath;
8
+
9
+ if (!relative || relative.startsWith("..")) {
10
+ return null;
11
+ }
12
+
13
+ return relative.replace(/\\/g, "/");
14
+ }
15
+
16
+ export function filterIgnoredFiles({ gitRoot, filePaths }) {
17
+ if (!Array.isArray(filePaths) || filePaths.length === 0) {
18
+ return [];
19
+ }
20
+
21
+ const normalized = new Map();
22
+ for (const filePath of filePaths) {
23
+ const relative = normalizePathForGit(filePath, gitRoot);
24
+ if (relative) {
25
+ normalized.set(relative, filePath);
26
+ }
27
+ }
28
+
29
+ if (normalized.size === 0) {
30
+ return [];
31
+ }
32
+
33
+ // Use `git check-ignore` to respect .gitignore rules quickly and accurately.
34
+ const input = `${Array.from(normalized.keys()).join("\u0000")}\u0000`;
35
+ const result = spawnSync("git", ["check-ignore", "--stdin", "-z"], {
36
+ cwd: gitRoot,
37
+ input,
38
+ encoding: "utf8"
39
+ });
40
+
41
+ if (result.error || (result.status && result.status > 1)) {
42
+ return Array.from(normalized.values());
43
+ }
44
+
45
+ const ignored = new Set(
46
+ String(result.stdout)
47
+ .split("\u0000")
48
+ .map((entry) => entry.trim())
49
+ .filter(Boolean)
50
+ );
51
+
52
+ return Array.from(normalized.entries())
53
+ .filter(([relative]) => !ignored.has(relative))
54
+ .map(([, original]) => original);
55
+ }