codeproof 1.0.4 → 1.1.0

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.
Files changed (42) hide show
  1. package/bin/codeproof.js +62 -60
  2. package/commands/apply.js +32 -32
  3. package/commands/help.js +34 -0
  4. package/commands/ignore.js +32 -32
  5. package/commands/init.js +110 -106
  6. package/commands/moveSecret.js +254 -202
  7. package/commands/reportDashboard.js +65 -65
  8. package/commands/run.js +234 -234
  9. package/commands/whoami.js +19 -19
  10. package/core/boundaries.md +26 -26
  11. package/core/enforcement.js +51 -51
  12. package/core/featureFlags.js +25 -25
  13. package/core/identity.js +78 -78
  14. package/core/safetyGuards.js +58 -58
  15. package/engine/aiAnalyzer.js +143 -143
  16. package/engine/aiEscalation.js +6 -6
  17. package/engine/contextBuilder.js +65 -65
  18. package/engine/decisionMerger.js +30 -30
  19. package/engine/ruleEngine.js +52 -52
  20. package/hooks/preCommit.js +93 -93
  21. package/package.json +16 -16
  22. package/reporting/reportBuilder.js +112 -112
  23. package/reporting/reportReader.js +49 -49
  24. package/reporting/reportWriter.js +104 -91
  25. package/rules/dangerousUsageRule.js +11 -11
  26. package/rules/insecureConfigRule.js +11 -11
  27. package/rules/regexPatterns.js +53 -53
  28. package/rules/ruleUtils.js +58 -58
  29. package/rules/secretRule.js +21 -21
  30. package/ui/banner.js +36 -0
  31. package/ui/formatting.js +76 -0
  32. package/ui/welcomeScreen.js +42 -42
  33. package/utils/apiClient.js +96 -96
  34. package/utils/envManager.js +48 -48
  35. package/utils/fileRewriter.js +145 -145
  36. package/utils/fileScanner.js +40 -40
  37. package/utils/files.js +50 -50
  38. package/utils/git.js +63 -63
  39. package/utils/gitIgnore.js +55 -55
  40. package/utils/logger.js +25 -25
  41. package/utils/projectType.js +20 -20
  42. package/.env +0 -1
@@ -1,143 +1,143 @@
1
- import http from "http";
2
- import https from "https";
3
- import { logWarn } from "../utils/logger.js";
4
-
5
- // AI contextual analysis layer. Only low-confidence findings reach this stage.
6
- // Regex-first keeps the fast baseline deterministic; AI is a cautious fallback.
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 };
14
- }
15
-
16
- function fallbackDecision(finding, reason) {
17
- if (reason) {
18
- logWarn(`AI escalation failed: ${reason}`);
19
- }
20
-
21
- return {
22
- findingId: finding.findingId,
23
- verdict: "warn",
24
- confidence: 0.35,
25
- explanation: "AI unavailable; defaulting to warn for manual review.",
26
- suggestedFix: "Review the value and move secrets to environment variables."
27
- };
28
- }
29
-
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
119
- };
120
- }
121
-
122
- export async function analyze(findings, projectContext) {
123
- void projectContext;
124
-
125
- if (!Array.isArray(findings) || findings.length === 0) {
126
- return [];
127
- }
128
-
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
- );
139
- }
140
-
141
-
142
-
143
-
1
+ import http from "http";
2
+ import https from "https";
3
+ import { logWarn } from "../utils/logger.js";
4
+
5
+ // AI contextual analysis layer. Only low-confidence findings reach this stage.
6
+ // Regex-first keeps the fast baseline deterministic; AI is a cautious fallback.
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 };
14
+ }
15
+
16
+ function fallbackDecision(finding, reason) {
17
+ if (reason) {
18
+ logWarn(`AI escalation failed: ${reason}`);
19
+ }
20
+
21
+ return {
22
+ findingId: finding.findingId,
23
+ verdict: "warn",
24
+ confidence: 0.35,
25
+ explanation: "AI unavailable; defaulting to warn for manual review.",
26
+ suggestedFix: "Review the value and move secrets to environment variables."
27
+ };
28
+ }
29
+
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
119
+ };
120
+ }
121
+
122
+ export async function analyze(findings, projectContext) {
123
+ void projectContext;
124
+
125
+ if (!Array.isArray(findings) || findings.length === 0) {
126
+ return [];
127
+ }
128
+
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
+ );
139
+ }
140
+
141
+
142
+
143
+
@@ -1,6 +1,6 @@
1
- // Stub interface for future AI contextual review.
2
- // This keeps the AI boundary clear and avoids network calls at this stage.
3
- export function aiReview(findings, context) {
4
- void context;
5
- return findings;
6
- }
1
+ // Stub interface for future AI contextual review.
2
+ // This keeps the AI boundary clear and avoids network calls at this stage.
3
+ export function aiReview(findings, context) {
4
+ void context;
5
+ return findings;
6
+ }
@@ -1,65 +1,65 @@
1
- import fs from "fs";
2
- import path from "path";
3
-
4
- const TEST_PATH_HINTS = [
5
- "test",
6
- "tests",
7
- "__tests__",
8
- "spec",
9
- "example",
10
- "examples",
11
- "sample",
12
- "samples",
13
- "mock",
14
- "mocks"
15
- ];
16
-
17
- function isTestLike(filePath) {
18
- const normalized = filePath.toLowerCase();
19
- return TEST_PATH_HINTS.some((hint) => normalized.includes(path.sep + hint));
20
- }
21
-
22
- function getFileType(filePath) {
23
- const ext = path.extname(filePath).toLowerCase();
24
- return ext ? ext.slice(1) : "unknown";
25
- }
26
-
27
- function getSnippetAroundLine(content, lineNumber, padding = 2, maxChars = 800) {
28
- const lines = content.split("\n");
29
- const start = Math.max(0, lineNumber - 1 - padding);
30
- const end = Math.min(lines.length, lineNumber + padding);
31
- const snippet = lines.slice(start, end).join("\n");
32
- return snippet.length > maxChars ? snippet.slice(0, maxChars) + "..." : snippet;
33
- }
34
-
35
- export function buildProjectContext({ gitRoot, config }) {
36
- return {
37
- projectType: config?.projectType || "Unknown",
38
- scanMode: config?.scanMode || "staged"
39
- };
40
- }
41
-
42
- export function buildAiInputs(findings, projectContext) {
43
- return findings.map((finding) => {
44
- let content = "";
45
- try {
46
- content = fs.readFileSync(finding.filePath, "utf8");
47
- } catch {
48
- content = "";
49
- }
50
-
51
- const snippet = content
52
- ? getSnippetAroundLine(content, finding.line || 1)
53
- : finding.snippet || "";
54
-
55
- return {
56
- findingId: finding.findingId,
57
- ruleId: finding.ruleId,
58
- filePath: finding.filePath,
59
- fileType: getFileType(finding.filePath),
60
- isTestLike: isTestLike(finding.filePath),
61
- snippet,
62
- projectContext
63
- };
64
- });
65
- }
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ const TEST_PATH_HINTS = [
5
+ "test",
6
+ "tests",
7
+ "__tests__",
8
+ "spec",
9
+ "example",
10
+ "examples",
11
+ "sample",
12
+ "samples",
13
+ "mock",
14
+ "mocks"
15
+ ];
16
+
17
+ function isTestLike(filePath) {
18
+ const normalized = filePath.toLowerCase();
19
+ return TEST_PATH_HINTS.some((hint) => normalized.includes(path.sep + hint));
20
+ }
21
+
22
+ function getFileType(filePath) {
23
+ const ext = path.extname(filePath).toLowerCase();
24
+ return ext ? ext.slice(1) : "unknown";
25
+ }
26
+
27
+ function getSnippetAroundLine(content, lineNumber, padding = 2, maxChars = 800) {
28
+ const lines = content.split("\n");
29
+ const start = Math.max(0, lineNumber - 1 - padding);
30
+ const end = Math.min(lines.length, lineNumber + padding);
31
+ const snippet = lines.slice(start, end).join("\n");
32
+ return snippet.length > maxChars ? snippet.slice(0, maxChars) + "..." : snippet;
33
+ }
34
+
35
+ export function buildProjectContext({ gitRoot, config }) {
36
+ return {
37
+ projectType: config?.projectType || "Unknown",
38
+ scanMode: config?.scanMode || "staged"
39
+ };
40
+ }
41
+
42
+ export function buildAiInputs(findings, projectContext) {
43
+ return findings.map((finding) => {
44
+ let content = "";
45
+ try {
46
+ content = fs.readFileSync(finding.filePath, "utf8");
47
+ } catch {
48
+ content = "";
49
+ }
50
+
51
+ const snippet = content
52
+ ? getSnippetAroundLine(content, finding.line || 1)
53
+ : finding.snippet || "";
54
+
55
+ return {
56
+ findingId: finding.findingId,
57
+ ruleId: finding.ruleId,
58
+ filePath: finding.filePath,
59
+ fileType: getFileType(finding.filePath),
60
+ isTestLike: isTestLike(finding.filePath),
61
+ snippet,
62
+ projectContext
63
+ };
64
+ });
65
+ }
@@ -1,30 +1,30 @@
1
- // Combine baseline results with AI decisions without overriding blocks.
2
-
3
- export function mergeDecisions({ baselineFindings, aiDecisions }) {
4
- const blockFindings = baselineFindings.filter(
5
- (finding) => finding.severity === "block" && finding.confidence === "high"
6
- );
7
- const warnFindings = baselineFindings.filter(
8
- (finding) => finding.severity === "warn"
9
- );
10
-
11
- const aiById = new Map(aiDecisions.map((decision) => [decision.findingId, decision]));
12
- const aiReviewed = baselineFindings
13
- .filter((finding) => finding.confidence === "low")
14
- .map((finding) => ({
15
- finding,
16
- decision: aiById.get(finding.findingId)
17
- }))
18
- .filter((entry) => entry.decision);
19
-
20
- const aiBlocks = aiReviewed.filter((entry) => entry.decision.verdict === "block");
21
-
22
- const exitCode = blockFindings.length > 0 || aiBlocks.length > 0 ? 1 : 0;
23
-
24
- return {
25
- blockFindings,
26
- warnFindings,
27
- aiReviewed,
28
- exitCode
29
- };
30
- }
1
+ // Combine baseline results with AI decisions without overriding blocks.
2
+
3
+ export function mergeDecisions({ baselineFindings, aiDecisions }) {
4
+ const blockFindings = baselineFindings.filter(
5
+ (finding) => finding.severity === "block" && finding.confidence === "high"
6
+ );
7
+ const warnFindings = baselineFindings.filter(
8
+ (finding) => finding.severity === "warn"
9
+ );
10
+
11
+ const aiById = new Map(aiDecisions.map((decision) => [decision.findingId, decision]));
12
+ const aiReviewed = baselineFindings
13
+ .filter((finding) => finding.confidence === "low")
14
+ .map((finding) => ({
15
+ finding,
16
+ decision: aiById.get(finding.findingId)
17
+ }))
18
+ .filter((entry) => entry.decision);
19
+
20
+ const aiBlocks = aiReviewed.filter((entry) => entry.decision.verdict === "block");
21
+
22
+ const exitCode = blockFindings.length > 0 || aiBlocks.length > 0 ? 1 : 0;
23
+
24
+ return {
25
+ blockFindings,
26
+ warnFindings,
27
+ aiReviewed,
28
+ exitCode
29
+ };
30
+ }
@@ -1,52 +1,52 @@
1
- import fs from "fs";
2
- import { buildLineIndex } from "../rules/ruleUtils.js";
3
- import { evaluateSecretRule } from "../rules/secretRule.js";
4
- import { evaluateDangerousUsageRule } from "../rules/dangerousUsageRule.js";
5
- import { evaluateInsecureConfigRule } from "../rules/insecureConfigRule.js";
6
-
7
- // Boundary: rule engine must never import reporting, integration, or remediation.
8
- // Rationale: regex-first keeps scans fast and deterministic; AI is a cautious fallback.
9
-
10
- // The engine aggregates deterministic regex rules and prepares low-confidence items
11
- // for AI escalation. This keeps the baseline fast and predictable.
12
- export function runRuleEngine({ files }) {
13
- const findings = [];
14
- const escalations = [];
15
- let counter = 0;
16
-
17
- for (const filePath of files) {
18
- let content = "";
19
- try {
20
- content = fs.readFileSync(filePath, "utf8");
21
- } catch {
22
- continue;
23
- }
24
-
25
- const lineStarts = buildLineIndex(content);
26
-
27
- const secretResult = evaluateSecretRule({ content, filePath, lineStarts });
28
- findings.push(...secretResult.findings.map((finding) => ({
29
- ...finding,
30
- findingId: `f-${counter++}`
31
- })));
32
- escalations.push(...secretResult.escalations.map((finding) => ({
33
- ...finding,
34
- findingId: `f-${counter++}`
35
- })));
36
-
37
- findings.push(
38
- ...evaluateDangerousUsageRule({ content, filePath, lineStarts }).map((finding) => ({
39
- ...finding,
40
- findingId: `f-${counter++}`
41
- }))
42
- );
43
- findings.push(
44
- ...evaluateInsecureConfigRule({ content, filePath, lineStarts }).map((finding) => ({
45
- ...finding,
46
- findingId: `f-${counter++}`
47
- }))
48
- );
49
- }
50
-
51
- return { findings, escalations };
52
- }
1
+ import fs from "fs";
2
+ import { buildLineIndex } from "../rules/ruleUtils.js";
3
+ import { evaluateSecretRule } from "../rules/secretRule.js";
4
+ import { evaluateDangerousUsageRule } from "../rules/dangerousUsageRule.js";
5
+ import { evaluateInsecureConfigRule } from "../rules/insecureConfigRule.js";
6
+
7
+ // Boundary: rule engine must never import reporting, integration, or remediation.
8
+ // Rationale: regex-first keeps scans fast and deterministic; AI is a cautious fallback.
9
+
10
+ // The engine aggregates deterministic regex rules and prepares low-confidence items
11
+ // for AI escalation. This keeps the baseline fast and predictable.
12
+ export function runRuleEngine({ files }) {
13
+ const findings = [];
14
+ const escalations = [];
15
+ let counter = 0;
16
+
17
+ for (const filePath of files) {
18
+ let content = "";
19
+ try {
20
+ content = fs.readFileSync(filePath, "utf8");
21
+ } catch {
22
+ continue;
23
+ }
24
+
25
+ const lineStarts = buildLineIndex(content);
26
+
27
+ const secretResult = evaluateSecretRule({ content, filePath, lineStarts });
28
+ findings.push(...secretResult.findings.map((finding) => ({
29
+ ...finding,
30
+ findingId: `f-${counter++}`
31
+ })));
32
+ escalations.push(...secretResult.escalations.map((finding) => ({
33
+ ...finding,
34
+ findingId: `f-${counter++}`
35
+ })));
36
+
37
+ findings.push(
38
+ ...evaluateDangerousUsageRule({ content, filePath, lineStarts }).map((finding) => ({
39
+ ...finding,
40
+ findingId: `f-${counter++}`
41
+ }))
42
+ );
43
+ findings.push(
44
+ ...evaluateInsecureConfigRule({ content, filePath, lineStarts }).map((finding) => ({
45
+ ...finding,
46
+ findingId: `f-${counter++}`
47
+ }))
48
+ );
49
+ }
50
+
51
+ return { findings, escalations };
52
+ }