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 +1 -0
- package/commands/reportDashboard.js +13 -5
- package/commands/run.js +6 -2
- package/core/safetyGuards.js +2 -2
- package/engine/aiAnalyzer.js +119 -27
- package/hooks/preCommit.js +2 -2
- package/package.json +1 -1
- package/reporting/reportBuilder.js +17 -8
- package/reporting/reportWriter.js +31 -2
- 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],
|
|
@@ -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,
|
package/core/safetyGuards.js
CHANGED
|
@@ -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 [];
|
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,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(
|
|
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(
|
|
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
|
@@ -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/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
|
+
}
|