codeproof 1.0.2 → 1.0.4
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 +1 -0
- package/commands/reportDashboard.js +13 -5
- package/commands/run.js +54 -24
- package/core/safetyGuards.js +13 -6
- package/engine/aiAnalyzer.js +119 -27
- package/hooks/preCommit.js +4 -4
- package/package.json +1 -1
- package/reporting/reportBuilder.js +17 -8
- package/reporting/reportWriter.js +31 -2
- package/utils/apiClient.js +78 -38
- package/utils/git.js +17 -0
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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],
|
|
@@ -118,14 +118,18 @@ export async function runCli({ args = [], cwd }) {
|
|
|
118
118
|
});
|
|
119
119
|
|
|
120
120
|
if (features.reporting) {
|
|
121
|
-
withFailOpenReporting(() => {
|
|
121
|
+
await withFailOpenReporting(async () => {
|
|
122
122
|
const timestamp = new Date().toISOString();
|
|
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,
|
|
@@ -134,62 +138,88 @@ export async function runCli({ args = [], cwd }) {
|
|
|
134
138
|
aiReviewed,
|
|
135
139
|
timestamp
|
|
136
140
|
});
|
|
137
|
-
//
|
|
141
|
+
// Report is saved to file and sent to server regardless of findings
|
|
142
|
+
logInfo("Saving report to file...");
|
|
138
143
|
writeReport({ projectRoot: gitRoot, report });
|
|
144
|
+
logSuccess("Report saved locally.");
|
|
139
145
|
|
|
140
146
|
const integration = config?.integration || {};
|
|
141
147
|
const integrationEnabled = features.integration && Boolean(integration.enabled);
|
|
148
|
+
|
|
149
|
+
// Always send to server in pre-commit or manual mode
|
|
142
150
|
if (integrationEnabled) {
|
|
143
|
-
|
|
151
|
+
logInfo("Syncing report to server...");
|
|
152
|
+
await withFailOpenIntegration(async () => {
|
|
144
153
|
// Network calls are fail-open; never affect exit codes.
|
|
145
|
-
sendReportToServer(report, {
|
|
154
|
+
return await sendReportToServer(report, {
|
|
146
155
|
enabled: true,
|
|
147
156
|
endpointUrl: integration.endpointUrl
|
|
148
157
|
});
|
|
149
158
|
});
|
|
159
|
+
logSuccess("Report synced to server.");
|
|
150
160
|
} else {
|
|
151
161
|
reportFeatureDisabled("Integration", verbose, logInfo);
|
|
152
162
|
}
|
|
153
163
|
}, () => {
|
|
154
|
-
logWarn("Failed to
|
|
164
|
+
logWarn("Failed to process report. Continuing without blocking.");
|
|
155
165
|
});
|
|
156
166
|
} else {
|
|
157
167
|
reportFeatureDisabled("Reporting", verbose, logInfo);
|
|
158
168
|
}
|
|
159
169
|
|
|
160
170
|
if (blockFindings.length > 0) {
|
|
161
|
-
logError(
|
|
171
|
+
logError(`\n❌ CRITICAL ISSUES FOUND (${blockFindings.length}):\n`);
|
|
162
172
|
for (const finding of blockFindings) {
|
|
163
173
|
const relative = path.relative(gitRoot, finding.filePath) || finding.filePath;
|
|
164
|
-
logError(
|
|
165
|
-
|
|
166
|
-
);
|
|
167
|
-
|
|
174
|
+
logError(` • ${finding.ruleId.toUpperCase()}`);
|
|
175
|
+
logError(` File: ${relative}:${finding.line}`);
|
|
176
|
+
logError(` Issue: ${finding.message}`);
|
|
177
|
+
// console.log(` Code: ${finding.snippet}`);
|
|
178
|
+
logError("");
|
|
168
179
|
}
|
|
169
180
|
}
|
|
170
181
|
|
|
171
182
|
if (warnFindings.length > 0) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
logWarn(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
183
|
+
// Filter to show only HIGH risk warnings
|
|
184
|
+
const highRiskWarnings = warnFindings.filter(f => f.confidence === "high");
|
|
185
|
+
if (highRiskWarnings.length > 0) {
|
|
186
|
+
logWarn(`\n⚠️ HIGH RISK WARNINGS (${highRiskWarnings.length}):\n`);
|
|
187
|
+
for (const finding of highRiskWarnings) {
|
|
188
|
+
const relative = path.relative(gitRoot, finding.filePath) || finding.filePath;
|
|
189
|
+
logWarn(` • ${finding.ruleId.toUpperCase()}`);
|
|
190
|
+
logWarn(` File: ${relative}:${finding.line}`);
|
|
191
|
+
logWarn(` Issue: ${finding.message}`);
|
|
192
|
+
// console.log(` Code: ${finding.snippet}`);
|
|
193
|
+
logWarn("");
|
|
194
|
+
}
|
|
179
195
|
}
|
|
196
|
+
// comment out low risk warnings
|
|
197
|
+
// const lowRiskWarnings = warnFindings.filter(f => f.confidence !== "high");
|
|
198
|
+
// if (lowRiskWarnings.length > 0) {
|
|
199
|
+
// logWarn(`Baseline warnings (${lowRiskWarnings.length}):`);
|
|
200
|
+
// for (const finding of lowRiskWarnings) {
|
|
201
|
+
// const relative = path.relative(gitRoot, finding.filePath) || finding.filePath;
|
|
202
|
+
// logWarn(
|
|
203
|
+
// `${finding.ruleId} [${finding.severity}/${finding.confidence}] ${relative}:${finding.line} ${finding.message}`
|
|
204
|
+
// );
|
|
205
|
+
// logWarn(` ${finding.snippet}`);
|
|
206
|
+
// }
|
|
207
|
+
// }
|
|
180
208
|
}
|
|
181
209
|
|
|
182
210
|
if (aiReviewed.length > 0) {
|
|
183
|
-
logWarn(
|
|
211
|
+
logWarn(`\n🤖 AI-REVIEWED FINDINGS (${aiReviewed.length}):\n`);
|
|
184
212
|
for (const entry of aiReviewed) {
|
|
185
213
|
const { finding, decision } = entry;
|
|
186
214
|
const relative = path.relative(gitRoot, finding.filePath) || finding.filePath;
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
);
|
|
215
|
+
const verdict = decision.verdict === "block" ? "BLOCKED" : "WARNING";
|
|
216
|
+
logWarn(` • ${finding.ruleId.toUpperCase()} [${verdict}]`);
|
|
217
|
+
logWarn(` File: ${relative}:${finding.line}`);
|
|
218
|
+
logWarn(` Analysis: ${decision.explanation}`);
|
|
190
219
|
if (decision.suggestedFix) {
|
|
191
|
-
logWarn(`
|
|
220
|
+
logWarn(` Fix: ${decision.suggestedFix}`);
|
|
192
221
|
}
|
|
222
|
+
logWarn("");
|
|
193
223
|
}
|
|
194
224
|
}
|
|
195
225
|
|
package/core/safetyGuards.js
CHANGED
|
@@ -18,9 +18,13 @@ export function reportFeatureDisabled(name, verbose, logInfo) {
|
|
|
18
18
|
logInfo(`${name} disabled by feature flag.`);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export function withFailOpenReporting(action, onError) {
|
|
21
|
+
export async function withFailOpenReporting(action, onError) {
|
|
22
22
|
try {
|
|
23
|
-
|
|
23
|
+
const result = action();
|
|
24
|
+
if (result && typeof result.then === 'function') {
|
|
25
|
+
return await result;
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
24
28
|
} catch {
|
|
25
29
|
if (onError) {
|
|
26
30
|
onError();
|
|
@@ -29,21 +33,24 @@ export function withFailOpenReporting(action, onError) {
|
|
|
29
33
|
}
|
|
30
34
|
}
|
|
31
35
|
|
|
32
|
-
export function withFailOpenIntegration(action) {
|
|
36
|
+
export async function withFailOpenIntegration(action) {
|
|
33
37
|
try {
|
|
34
|
-
action();
|
|
38
|
+
const result = action();
|
|
39
|
+
if (result && typeof result.then === 'function') {
|
|
40
|
+
await result;
|
|
41
|
+
}
|
|
35
42
|
} catch {
|
|
36
43
|
// Integration failures are ignored to avoid affecting commits.
|
|
37
44
|
}
|
|
38
45
|
}
|
|
39
46
|
|
|
40
|
-
export function withFailOpenAiEscalation(enabled, action) {
|
|
47
|
+
export async function withFailOpenAiEscalation(enabled, action) {
|
|
41
48
|
if (!enabled) {
|
|
42
49
|
return [];
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
try {
|
|
46
|
-
return action();
|
|
53
|
+
return await action();
|
|
47
54
|
} catch {
|
|
48
55
|
// AI failures downgrade to warnings by returning no decisions.
|
|
49
56
|
return [];
|
package/engine/aiAnalyzer.js
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
122
|
+
export async function analyze(findings, projectContext) {
|
|
123
|
+
void projectContext;
|
|
35
124
|
|
|
36
|
-
if (!
|
|
37
|
-
return
|
|
125
|
+
if (!Array.isArray(findings) || findings.length === 0) {
|
|
126
|
+
return [];
|
|
38
127
|
}
|
|
39
128
|
|
|
40
|
-
return
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
package/hooks/preCommit.js
CHANGED
|
@@ -18,14 +18,14 @@ 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(
|
|
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.\"",
|
|
25
25
|
" exit 0",
|
|
26
26
|
" fi",
|
|
27
27
|
"fi",
|
|
28
|
-
"CODEPROOF_PRECOMMIT=1 codeproof run --precommit",
|
|
28
|
+
"CODEPROOF_PRECOMMIT=1 npx codeproof run --precommit",
|
|
29
29
|
"RESULT=$?",
|
|
30
30
|
"if [ $RESULT -ne 0 ]; then",
|
|
31
31
|
" echo \"CodeProof checks failed. Commit blocked.\"",
|
|
@@ -55,14 +55,14 @@ 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(
|
|
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.\"",
|
|
62
62
|
" exit 0",
|
|
63
63
|
" fi",
|
|
64
64
|
"fi",
|
|
65
|
-
"CODEPROOF_PRECOMMIT=1 codeproof run --precommit",
|
|
65
|
+
"CODEPROOF_PRECOMMIT=1 npx codeproof run --precommit",
|
|
66
66
|
"RESULT=$?",
|
|
67
67
|
"if [ $RESULT -ne 0 ]; then",
|
|
68
68
|
" echo \"CodeProof checks failed. Commit blocked.\"",
|
package/package.json
CHANGED
|
@@ -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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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/apiClient.js
CHANGED
|
@@ -4,11 +4,17 @@ import https from "https";
|
|
|
4
4
|
// Boundary: integration layer only. Must not import CLI, rule engine, or reporting.
|
|
5
5
|
// Network calls are fail-open to avoid impacting commits or developer flow.
|
|
6
6
|
|
|
7
|
-
const DEFAULT_ENDPOINT = "
|
|
7
|
+
const DEFAULT_ENDPOINT = "http://127.0.0.1:4000/api/reports";
|
|
8
8
|
|
|
9
|
-
export function sendReportToServer(report, options = {}) {
|
|
9
|
+
export async function sendReportToServer(report, options = {}) {
|
|
10
10
|
const enabled = Boolean(options.enabled);
|
|
11
|
+
|
|
12
|
+
// console.log("[API Client] sendReportToServer called");
|
|
13
|
+
// console.log("[API Client] enabled:", enabled);
|
|
14
|
+
// console.log("[API Client] options:", JSON.stringify(options, null, 2));
|
|
15
|
+
|
|
11
16
|
if (!enabled) {
|
|
17
|
+
// console.log("[API Client] Integration disabled, skipping");
|
|
12
18
|
return;
|
|
13
19
|
}
|
|
14
20
|
|
|
@@ -16,41 +22,75 @@ export function sendReportToServer(report, options = {}) {
|
|
|
16
22
|
? options.endpointUrl.trim()
|
|
17
23
|
: DEFAULT_ENDPOINT;
|
|
18
24
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
// console.log("[API Client] Target endpoint:", endpointUrl);
|
|
26
|
+
// console.log("[API Client] Report summary:", {
|
|
27
|
+
// projectId: report.projectId,
|
|
28
|
+
// clientId: report.clientId,
|
|
29
|
+
// findingsCount: report.findings?.length || 0
|
|
30
|
+
// });
|
|
31
|
+
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
try {
|
|
34
|
+
const url = new URL(endpointUrl);
|
|
35
|
+
const payload = JSON.stringify(report);
|
|
36
|
+
const transport = url.protocol === "http:" ? http : https;
|
|
37
|
+
|
|
38
|
+
// console.log("[API Client] URL parsed - protocol:", url.protocol, "hostname:", url.hostname, "port:", url.port, "pathname:", url.pathname);
|
|
39
|
+
// console.log("[API Client] Payload size:", Buffer.byteLength(payload), "bytes");
|
|
40
|
+
// console.log("[API Client] Payload preview:", payload.substring(0, 200) + "...");
|
|
41
|
+
|
|
42
|
+
const portNumber = url.port ? parseInt(url.port, 10) : (url.protocol === "http:" ? 80 : 443);
|
|
43
|
+
|
|
44
|
+
// console.log("[API Client] Sending POST to:", `${url.protocol}//${url.hostname}:${portNumber}${url.pathname}`);
|
|
45
|
+
|
|
46
|
+
const request = transport.request(
|
|
47
|
+
{
|
|
48
|
+
method: "POST",
|
|
49
|
+
hostname: url.hostname,
|
|
50
|
+
port: portNumber,
|
|
51
|
+
path: `${url.pathname}${url.search}`,
|
|
52
|
+
headers: {
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
"Content-Length": Buffer.byteLength(payload)
|
|
55
|
+
},
|
|
56
|
+
timeout: 5000
|
|
33
57
|
},
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
58
|
+
(res) => {
|
|
59
|
+
// console.log("[API Client] Response received:", res.statusCode);
|
|
60
|
+
let body = "";
|
|
61
|
+
res.on("data", (chunk) => {
|
|
62
|
+
body += chunk;
|
|
63
|
+
});
|
|
64
|
+
res.on("end", () => {
|
|
65
|
+
// console.log("[API Client] Response body:", body);
|
|
66
|
+
if (res.statusCode === 201) {
|
|
67
|
+
// Report sent successfully
|
|
68
|
+
} else {
|
|
69
|
+
console.error("[API Client] Server returned status:", res.statusCode);
|
|
70
|
+
}
|
|
71
|
+
resolve();
|
|
72
|
+
});
|
|
73
|
+
res.resume();
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
request.on("timeout", () => {
|
|
78
|
+
console.error("[API Client] Request timeout");
|
|
79
|
+
request.destroy();
|
|
80
|
+
resolve();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
request.on("error", (err) => {
|
|
84
|
+
console.error("[API Client] Request error:", err.message);
|
|
85
|
+
resolve();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
request.write(payload);
|
|
89
|
+
request.end();
|
|
90
|
+
// console.log("[API Client] Request sent");
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.error("[API Client] Exception:", err.message);
|
|
93
|
+
resolve();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
56
96
|
}
|
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
|
+
}
|