codeproof 1.0.2 → 1.0.3

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/.env ADDED
@@ -0,0 +1 @@
1
+ AI_API_URL = "https://api-risk-fgef.onrender.com/predict"
@@ -37,8 +37,13 @@ export async function runReportDashboard({ cwd }) {
37
37
 
38
38
  if (!latestReport) {
39
39
  logWarn("No reports found. Run `codeproof run` first.");
40
- } else {
41
- const integration = config?.integration || {};
40
+ return;
41
+ }
42
+
43
+ const integration = config?.integration || {};
44
+ const integrationEnabled = features.integration && Boolean(integration.enabled);
45
+
46
+ if (integrationEnabled) {
42
47
  // Integrations are fail-open: never throw on network errors.
43
48
  withFailOpenIntegration(() => {
44
49
  sendReportToServer(latestReport, {
@@ -46,10 +51,13 @@ export async function runReportDashboard({ cwd }) {
46
51
  endpointUrl: integration.endpointUrl
47
52
  });
48
53
  });
54
+ logInfo("Report sent to server.");
55
+ } else {
56
+ reportFeatureDisabled("Integration", verbose, logInfo);
57
+ }
49
58
 
50
- if (latestReport?.projectId) {
51
- logInfo(`View dashboard: https://dashboard.codeproof.dev/project/${latestReport.projectId}`);
52
- }
59
+ if (latestReport?.projectId) {
60
+ logInfo(`View dashboard: https://dashboard.codeproof.dev/project/${latestReport.projectId}`);
53
61
  }
54
62
  } else {
55
63
  reportFeatureDisabled("Reporting", verbose, logInfo);
package/commands/run.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
- import { ensureGitRepo, getGitRoot, getStagedFiles } from "../utils/git.js";
3
+ import { ensureGitRepo, getGitRoot, getStagedFiles, getRepoIdentifier, getProjectName } from "../utils/git.js";
4
4
  import { logError, logInfo, logSuccess, logWarn } from "../utils/logger.js";
5
5
  import { buildScanTargets } from "../utils/fileScanner.js";
6
6
  import { runRuleEngine } from "../engine/ruleEngine.js";
@@ -110,7 +110,7 @@ export async function runCli({ args = [], cwd }) {
110
110
  }
111
111
 
112
112
  const aiDecisions = aiInputs.length > 0
113
- ? withFailOpenAiEscalation(features.aiEscalation, () => analyze(aiInputs, projectContext))
113
+ ? await withFailOpenAiEscalation(features.aiEscalation, () => analyze(aiInputs, projectContext))
114
114
  : [];
115
115
  const { blockFindings, warnFindings, aiReviewed, exitCode } = mergeDecisions({
116
116
  baselineFindings: [...findings, ...escalations],
@@ -123,9 +123,13 @@ export async function runCli({ args = [], cwd }) {
123
123
  const reportId = randomUUID();
124
124
  const projectId = config.projectId || "";
125
125
  const clientId = getClientId();
126
+ const projectName = getProjectName(gitRoot);
127
+ const repoIdentifier = getRepoIdentifier(gitRoot);
126
128
  const report = buildReport({
127
129
  projectRoot: gitRoot,
128
130
  projectId,
131
+ projectName,
132
+ repoIdentifier,
129
133
  clientId,
130
134
  reportId,
131
135
  scanMode,
@@ -37,13 +37,13 @@ export function withFailOpenIntegration(action) {
37
37
  }
38
38
  }
39
39
 
40
- export function withFailOpenAiEscalation(enabled, action) {
40
+ export async function withFailOpenAiEscalation(enabled, action) {
41
41
  if (!enabled) {
42
42
  return [];
43
43
  }
44
44
 
45
45
  try {
46
- return action();
46
+ return await action();
47
47
  } catch {
48
48
  // AI failures downgrade to warnings by returning no decisions.
49
49
  return [];
@@ -1,13 +1,23 @@
1
+ import http from "http";
2
+ import https from "https";
3
+ import { logWarn } from "../utils/logger.js";
4
+
1
5
  // AI contextual analysis layer. Only low-confidence findings reach this stage.
2
6
  // Regex-first keeps the fast baseline deterministic; AI is a cautious fallback.
3
7
 
4
- function callModel(payload) {
5
- void payload;
6
- // Stubbed: No provider hardcoded. Return null to trigger safe fallback.
7
- return null;
8
+ const DEFAULT_TIMEOUT_MS = 5000;
9
+
10
+ function getAiConfig() {
11
+ const apiUrl = process.env.AI_API_URL || "";
12
+ const timeoutMs = Number(process.env.AI_TIMEOUT_MS) || DEFAULT_TIMEOUT_MS;
13
+ return { apiUrl, timeoutMs };
8
14
  }
9
15
 
10
- function fallbackDecision(finding) {
16
+ function fallbackDecision(finding, reason) {
17
+ if (reason) {
18
+ logWarn(`AI escalation failed: ${reason}`);
19
+ }
20
+
11
21
  return {
12
22
  findingId: finding.findingId,
13
23
  verdict: "warn",
@@ -17,33 +27,115 @@ function fallbackDecision(finding) {
17
27
  };
18
28
  }
19
29
 
20
- export function analyze(findings, projectContext) {
21
- const payload = {
22
- version: 1,
23
- projectContext,
24
- findings: findings.map((finding) => ({
25
- findingId: finding.findingId,
26
- ruleId: finding.ruleId,
27
- filePath: finding.filePath,
28
- fileType: finding.fileType,
29
- isTestLike: finding.isTestLike,
30
- snippet: finding.snippet
31
- }))
30
+ function postJsonWithTimeout({ url, payload, timeoutMs }) {
31
+ return new Promise((resolve, reject) => {
32
+ let parsedUrl;
33
+ try {
34
+ parsedUrl = new URL(url);
35
+ } catch {
36
+ reject(new Error("Invalid AI_API_URL"));
37
+ return;
38
+ }
39
+
40
+ const data = JSON.stringify(payload);
41
+ const transport = parsedUrl.protocol === "http:" ? http : https;
42
+
43
+ const request = transport.request(
44
+ {
45
+ method: "POST",
46
+ hostname: parsedUrl.hostname,
47
+ port: parsedUrl.port || (parsedUrl.protocol === "http:" ? 80 : 443),
48
+ path: `${parsedUrl.pathname}${parsedUrl.search}`,
49
+ headers: {
50
+ "Content-Type": "application/json",
51
+ "Content-Length": Buffer.byteLength(data)
52
+ },
53
+ timeout: timeoutMs
54
+ },
55
+ (res) => {
56
+ let body = "";
57
+ res.setEncoding("utf8");
58
+ res.on("data", (chunk) => {
59
+ body += chunk;
60
+ });
61
+ res.on("end", () => {
62
+ if (res.statusCode && res.statusCode >= 400) {
63
+ reject(new Error(`AI API responded with ${res.statusCode}`));
64
+ return;
65
+ }
66
+ try {
67
+ resolve(JSON.parse(body));
68
+ } catch {
69
+ reject(new Error("AI API returned invalid JSON"));
70
+ }
71
+ });
72
+ }
73
+ );
74
+
75
+ request.on("timeout", () => {
76
+ request.destroy(new Error("AI request timed out"));
77
+ });
78
+
79
+ request.on("error", (err) => {
80
+ reject(err);
81
+ });
82
+
83
+ request.write(data);
84
+ request.end();
85
+ });
86
+ }
87
+
88
+ async function callModel(finding) {
89
+ const { apiUrl, timeoutMs } = getAiConfig();
90
+
91
+ if (!apiUrl) {
92
+ throw new Error("AI_API_URL is not configured");
93
+ }
94
+
95
+ const response = await postJsonWithTimeout({
96
+ url: apiUrl,
97
+ timeoutMs,
98
+ payload: {
99
+ code: finding.snippet || ""
100
+ }
101
+ });
102
+
103
+ if (!response || typeof response.found !== "boolean") {
104
+ throw new Error("AI API returned invalid payload");
105
+ }
106
+
107
+ const verdict = response.found && response.risk === "Critical" ? "block" : "warn";
108
+ const confidence = response.found ? 0.85 : 0.35;
109
+ const explanation = response.found
110
+ ? `AI detected ${response.risk} risk: ${response.secret || "secret value"}`
111
+ : "AI did not detect risk in this code snippet.";
112
+
113
+ return {
114
+ findingId: finding.findingId,
115
+ verdict,
116
+ confidence,
117
+ explanation,
118
+ suggestedFix: response.found ? "Move secrets to environment variables." : undefined
32
119
  };
120
+ }
33
121
 
34
- const response = callModel(payload);
122
+ export async function analyze(findings, projectContext) {
123
+ void projectContext;
35
124
 
36
- if (!response || !Array.isArray(response.decisions)) {
37
- return findings.map(fallbackDecision);
125
+ if (!Array.isArray(findings) || findings.length === 0) {
126
+ return [];
38
127
  }
39
128
 
40
- return response.decisions.map((decision) => ({
41
- findingId: decision.findingId,
42
- verdict: decision.verdict || "warn",
43
- confidence: typeof decision.confidence === "number" ? decision.confidence : 0.5,
44
- explanation: decision.explanation || "AI decision provided.",
45
- suggestedFix: decision.suggestedFix
46
- }));
129
+ return Promise.all(
130
+ findings.map(async (finding) => {
131
+ try {
132
+ return await callModel(finding);
133
+ } catch (error) {
134
+ const reason = error instanceof Error ? error.message : "AI call failed";
135
+ return fallbackDecision(finding, reason);
136
+ }
137
+ })
138
+ );
47
139
  }
48
140
 
49
141
 
@@ -18,7 +18,7 @@ function getHookBlock() {
18
18
  "fi",
19
19
  "CONFIG_PATH=\"$GIT_ROOT/codeproof.config.json\"",
20
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');}\")",
21
+ " ENFORCEMENT=$(node -e \"const fs=require('fs');const path=process.argv[1];try{const c=JSON.parse(fs.readFileSync(path,'utf8'));console.log((c.enforcement||'enabled').toLowerCase());}catch(e){console.log('enabled');}\" \"$CONFIG_PATH\")",
22
22
  " if [ \"$ENFORCEMENT\" = \"disabled\" ]; then",
23
23
  " echo \"CodeProof enforcement is temporarily disabled.\"",
24
24
  " echo \"Commit allowed.\"",
@@ -55,7 +55,7 @@ export function installPreCommitHook(gitRoot) {
55
55
  "fi",
56
56
  "CONFIG_PATH=\"$GIT_ROOT/codeproof.config.json\"",
57
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');}\")",
58
+ " ENFORCEMENT=$(node -e \"const fs=require('fs');const path=process.argv[1];try{const c=JSON.parse(fs.readFileSync(path,'utf8'));console.log((c.enforcement||'enabled').toLowerCase());}catch(e){console.log('enabled');}\" \"$CONFIG_PATH\")",
59
59
  " if [ \"$ENFORCEMENT\" = \"disabled\" ]; then",
60
60
  " echo \"CodeProof enforcement is temporarily disabled.\"",
61
61
  " echo \"Commit allowed.\"",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeproof",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "CodeProof CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -46,6 +46,8 @@ function normalizeSeverity(value) {
46
46
  export function buildReport({
47
47
  projectRoot,
48
48
  projectId,
49
+ projectName,
50
+ repoIdentifier,
49
51
  clientId,
50
52
  reportId,
51
53
  scanMode,
@@ -85,17 +87,24 @@ export function buildReport({
85
87
  ? "allowed_with_warnings"
86
88
  : "allowed";
87
89
 
90
+ // Server-compatible format
88
91
  return {
89
- reportId,
90
- timestamp,
91
92
  projectId,
92
93
  clientId,
93
- scanMode,
94
- summary: {
95
- totalFilesScanned: filesScannedCount,
96
- totalFindings: findings.length,
97
- blocksCount,
98
- warningsCount
94
+ project: {
95
+ name: projectName || "Unknown Project",
96
+ repoIdentifier: repoIdentifier || projectRoot
97
+ },
98
+ report: {
99
+ timestamp,
100
+ scanMode,
101
+ summary: {
102
+ filesScanned: filesScannedCount,
103
+ findings: findings.length,
104
+ blocks: blocksCount,
105
+ warnings: warningsCount,
106
+ finalVerdict
107
+ }
99
108
  },
100
109
  findings,
101
110
  finalVerdict
@@ -44,6 +44,22 @@ export function writeReport({ projectRoot, report }) {
44
44
  const reportDir = getReportDir(projectRoot);
45
45
  ensureReportDir(reportDir);
46
46
 
47
+ // Cleanup old temp files before creating new ones
48
+ try {
49
+ const files = fs.readdirSync(reportDir);
50
+ for (const file of files) {
51
+ if (file.startsWith('.tmp-')) {
52
+ try {
53
+ fs.unlinkSync(path.join(reportDir, file));
54
+ } catch {
55
+ // Ignore errors on cleanup
56
+ }
57
+ }
58
+ }
59
+ } catch {
60
+ // Ignore cleanup errors
61
+ }
62
+
47
63
  // Per-run JSON keeps every audit entry immutable and easy to archive.
48
64
  let reportNumber = getNextReportNumber(reportDir);
49
65
  let reportPath = path.join(reportDir, `${REPORT_PREFIX}${reportNumber}${REPORT_SUFFIX}`);
@@ -55,8 +71,21 @@ export function writeReport({ projectRoot, report }) {
55
71
  // Use numeric sequencing over timestamps to avoid collisions in fast CI runs.
56
72
  const tempPath = path.join(reportDir, `.tmp-${process.pid}-${Date.now()}.json`);
57
73
  const payload = JSON.stringify(report, null, 2) + "\n";
58
- fs.writeFileSync(tempPath, payload, "utf8");
59
- fs.renameSync(tempPath, reportPath);
74
+
75
+ try {
76
+ fs.writeFileSync(tempPath, payload, "utf8");
77
+ fs.renameSync(tempPath, reportPath);
78
+ } catch (err) {
79
+ // Cleanup temp file on error
80
+ try {
81
+ if (fs.existsSync(tempPath)) {
82
+ fs.unlinkSync(tempPath);
83
+ }
84
+ } catch {
85
+ // Ignore cleanup errors
86
+ }
87
+ throw err;
88
+ }
60
89
 
61
90
  return reportPath;
62
91
  }
package/utils/git.js CHANGED
@@ -44,3 +44,20 @@ export function getStagedFiles(cwd) {
44
44
  .map((line) => line.trim())
45
45
  .filter(Boolean);
46
46
  }
47
+
48
+ export function getRepoIdentifier(gitRoot) {
49
+ try {
50
+ const result = runGit(["config", "--get", "remote.origin.url"], gitRoot);
51
+ if (result.status === 0) {
52
+ return String(result.stdout).trim() || gitRoot;
53
+ }
54
+ } catch {
55
+ // Fallback to directory name
56
+ }
57
+ return gitRoot;
58
+ }
59
+
60
+ export function getProjectName(gitRoot) {
61
+ const parts = gitRoot.split(/[\\/]/);
62
+ return parts[parts.length - 1] || "Unknown";
63
+ }