codeproof 1.0.1 → 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/bin/codeproof.js +13 -1
- package/commands/apply.js +32 -0
- package/commands/ignore.js +32 -0
- package/commands/init.js +13 -0
- package/commands/moveSecret.js +2 -20
- package/commands/reportDashboard.js +13 -5
- package/commands/run.js +22 -3
- package/core/enforcement.js +51 -0
- package/core/safetyGuards.js +2 -2
- package/engine/aiAnalyzer.js +119 -27
- package/hooks/preCommit.js +28 -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"
|
package/bin/codeproof.js
CHANGED
|
@@ -4,13 +4,15 @@ import { runCli } from "../commands/run.js";
|
|
|
4
4
|
import { runReportDashboard } from "../commands/reportDashboard.js";
|
|
5
5
|
import { runMoveSecret } from "../commands/moveSecret.js";
|
|
6
6
|
import { runWhoAmI } from "../commands/whoami.js";
|
|
7
|
+
import { runIgnore } from "../commands/ignore.js";
|
|
8
|
+
import { runApply } from "../commands/apply.js";
|
|
7
9
|
import { logError, logInfo } from "../utils/logger.js";
|
|
8
10
|
|
|
9
11
|
const [, , command, ...args] = process.argv;
|
|
10
12
|
|
|
11
13
|
async function main() {
|
|
12
14
|
if (!command || command === "-h" || command === "--help") {
|
|
13
|
-
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 whoami Show the local CodeProof client ID");
|
|
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");
|
|
14
16
|
process.exit(0);
|
|
15
17
|
}
|
|
16
18
|
|
|
@@ -34,6 +36,16 @@ async function main() {
|
|
|
34
36
|
return;
|
|
35
37
|
}
|
|
36
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
|
+
|
|
37
49
|
if (command === "whoami") {
|
|
38
50
|
await runWhoAmI();
|
|
39
51
|
return;
|
|
@@ -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
|
@@ -50,6 +50,7 @@ export async function runInit({ cwd }) {
|
|
|
50
50
|
projectId: randomUUID(),
|
|
51
51
|
projectType,
|
|
52
52
|
scanMode: "staged",
|
|
53
|
+
enforcement: "enabled",
|
|
53
54
|
features: {
|
|
54
55
|
reporting: true,
|
|
55
56
|
integration: false,
|
|
@@ -70,6 +71,18 @@ export async function runInit({ cwd }) {
|
|
|
70
71
|
logSuccess("Created codeproof.config.json");
|
|
71
72
|
}
|
|
72
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
|
+
|
|
73
86
|
installPreCommitHook(gitRoot);
|
|
74
87
|
logSuccess("Pre-commit hook installed.");
|
|
75
88
|
|
package/commands/moveSecret.js
CHANGED
|
@@ -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(
|
|
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.");
|
|
@@ -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";
|
|
@@ -12,6 +12,7 @@ import { writeReport } from "../reporting/reportWriter.js";
|
|
|
12
12
|
import { sendReportToServer } from "../utils/apiClient.js";
|
|
13
13
|
import { resolveFeatureFlags, isVerbose } from "../core/featureFlags.js";
|
|
14
14
|
import { getClientId } from "../core/identity.js";
|
|
15
|
+
import { getEnforcementState } from "../core/enforcement.js";
|
|
15
16
|
import {
|
|
16
17
|
reportFeatureDisabled,
|
|
17
18
|
warnExperimentalOnce,
|
|
@@ -36,7 +37,7 @@ function readConfig(configPath) {
|
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
export async function runCli({ cwd }) {
|
|
40
|
+
export async function runCli({ args = [], cwd }) {
|
|
40
41
|
// Boundary: CLI orchestration only. Avoid importing this module in lower layers.
|
|
41
42
|
logInfo("CodeProof run started.");
|
|
42
43
|
|
|
@@ -46,6 +47,20 @@ export async function runCli({ cwd }) {
|
|
|
46
47
|
const config = readConfig(configPath);
|
|
47
48
|
const features = resolveFeatureFlags(config);
|
|
48
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
|
+
}
|
|
49
64
|
|
|
50
65
|
if (!config.scanMode) {
|
|
51
66
|
logError("Config missing scanMode. Expected 'staged' or 'full'.");
|
|
@@ -95,7 +110,7 @@ export async function runCli({ cwd }) {
|
|
|
95
110
|
}
|
|
96
111
|
|
|
97
112
|
const aiDecisions = aiInputs.length > 0
|
|
98
|
-
? withFailOpenAiEscalation(features.aiEscalation, () => analyze(aiInputs, projectContext))
|
|
113
|
+
? await withFailOpenAiEscalation(features.aiEscalation, () => analyze(aiInputs, projectContext))
|
|
99
114
|
: [];
|
|
100
115
|
const { blockFindings, warnFindings, aiReviewed, exitCode } = mergeDecisions({
|
|
101
116
|
baselineFindings: [...findings, ...escalations],
|
|
@@ -108,9 +123,13 @@ export async function runCli({ cwd }) {
|
|
|
108
123
|
const reportId = randomUUID();
|
|
109
124
|
const projectId = config.projectId || "";
|
|
110
125
|
const clientId = getClientId();
|
|
126
|
+
const projectName = getProjectName(gitRoot);
|
|
127
|
+
const repoIdentifier = getRepoIdentifier(gitRoot);
|
|
111
128
|
const report = buildReport({
|
|
112
129
|
projectRoot: gitRoot,
|
|
113
130
|
projectId,
|
|
131
|
+
projectName,
|
|
132
|
+
repoIdentifier,
|
|
114
133
|
clientId,
|
|
115
134
|
reportId,
|
|
116
135
|
scanMode,
|
|
@@ -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
|
+
}
|
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
|
@@ -12,7 +12,20 @@ function getHookBlock() {
|
|
|
12
12
|
return [
|
|
13
13
|
"",
|
|
14
14
|
HOOK_MARKER,
|
|
15
|
-
"
|
|
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');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
|
+
" 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
|
-
"
|
|
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');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
|
+
" 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
|
@@ -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
|
+
}
|