codeproof 1.0.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.
@@ -0,0 +1,51 @@
1
+ // Centralized safety controls. These helpers must stay dependency-light.
2
+ // They enforce fail-open behavior without coupling to specific subsystems.
3
+
4
+ let warnedExperimental = false;
5
+
6
+ export function warnExperimentalOnce(message, logWarn) {
7
+ if (warnedExperimental) {
8
+ return;
9
+ }
10
+ warnedExperimental = true;
11
+ logWarn(message);
12
+ }
13
+
14
+ export function reportFeatureDisabled(name, verbose, logInfo) {
15
+ if (!verbose) {
16
+ return;
17
+ }
18
+ logInfo(`${name} disabled by feature flag.`);
19
+ }
20
+
21
+ export function withFailOpenReporting(action, onError) {
22
+ try {
23
+ return action();
24
+ } catch {
25
+ if (onError) {
26
+ onError();
27
+ }
28
+ return null;
29
+ }
30
+ }
31
+
32
+ export function withFailOpenIntegration(action) {
33
+ try {
34
+ action();
35
+ } catch {
36
+ // Integration failures are ignored to avoid affecting commits.
37
+ }
38
+ }
39
+
40
+ export function withFailOpenAiEscalation(enabled, action) {
41
+ if (!enabled) {
42
+ return [];
43
+ }
44
+
45
+ try {
46
+ return action();
47
+ } catch {
48
+ // AI failures downgrade to warnings by returning no decisions.
49
+ return [];
50
+ }
51
+ }
@@ -0,0 +1,51 @@
1
+ // AI contextual analysis layer. Only low-confidence findings reach this stage.
2
+ // Regex-first keeps the fast baseline deterministic; AI is a cautious fallback.
3
+
4
+ function callModel(payload) {
5
+ void payload;
6
+ // Stubbed: No provider hardcoded. Return null to trigger safe fallback.
7
+ return null;
8
+ }
9
+
10
+ function fallbackDecision(finding) {
11
+ return {
12
+ findingId: finding.findingId,
13
+ verdict: "warn",
14
+ confidence: 0.35,
15
+ explanation: "AI unavailable; defaulting to warn for manual review.",
16
+ suggestedFix: "Review the value and move secrets to environment variables."
17
+ };
18
+ }
19
+
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
+ }))
32
+ };
33
+
34
+ const response = callModel(payload);
35
+
36
+ if (!response || !Array.isArray(response.decisions)) {
37
+ return findings.map(fallbackDecision);
38
+ }
39
+
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
+ }));
47
+ }
48
+
49
+
50
+
51
+
@@ -0,0 +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
+ }
@@ -0,0 +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
+ }
@@ -0,0 +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
+ }
@@ -0,0 +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
+ }
@@ -0,0 +1,67 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { logInfo, logWarn } from "../utils/logger.js";
4
+
5
+ const HOOK_MARKER = "# CodeProof pre-commit hook";
6
+
7
+ function getHookPath(gitRoot) {
8
+ return path.join(gitRoot, ".git", "hooks", "pre-commit");
9
+ }
10
+
11
+ function getHookBlock() {
12
+ return [
13
+ "",
14
+ HOOK_MARKER,
15
+ "codeproof run",
16
+ "RESULT=$?",
17
+ "if [ $RESULT -ne 0 ]; then",
18
+ " echo \"CodeProof checks failed. Commit blocked.\"",
19
+ " exit $RESULT",
20
+ "fi",
21
+ ""
22
+ ].join("\n");
23
+ }
24
+
25
+ export function installPreCommitHook(gitRoot) {
26
+ const hookPath = getHookPath(gitRoot);
27
+ const hookDir = path.dirname(hookPath);
28
+
29
+ if (!fs.existsSync(hookDir)) {
30
+ fs.mkdirSync(hookDir, { recursive: true });
31
+ }
32
+
33
+ if (!fs.existsSync(hookPath)) {
34
+ // Use a POSIX shell hook for cross-platform Git compatibility (Git Bash on Windows).
35
+ const content = [
36
+ "#!/bin/sh",
37
+ "",
38
+ "# Auto-generated by CodeProof",
39
+ "codeproof run",
40
+ "RESULT=$?",
41
+ "if [ $RESULT -ne 0 ]; then",
42
+ " echo \"CodeProof checks failed. Commit blocked.\"",
43
+ " exit $RESULT",
44
+ "fi",
45
+ ""
46
+ ].join("\n");
47
+ fs.writeFileSync(hookPath, content, "utf8");
48
+ logInfo("Created new pre-commit hook.");
49
+ } else {
50
+ const existing = fs.readFileSync(hookPath, "utf8");
51
+ if (existing.includes("codeproof run") || existing.includes(HOOK_MARKER)) {
52
+ logWarn("Pre-commit hook already references CodeProof. Skipping append.");
53
+ } else {
54
+ // Append instead of overwrite to avoid breaking existing hook logic.
55
+ fs.appendFileSync(hookPath, getHookBlock(), "utf8");
56
+ logInfo("Appended CodeProof to existing pre-commit hook.");
57
+ }
58
+ }
59
+
60
+ try {
61
+ fs.chmodSync(hookPath, 0o755);
62
+ } catch {
63
+ // Best-effort: Windows may not honor chmod, but Git still runs hooks.
64
+ }
65
+ }
66
+
67
+
package/package.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "codeproof",
3
+ "version": "1.0.0",
4
+ "description": "CodeProof CLI",
5
+ "type": "module",
6
+ "bin": {
7
+ "codeproof": "./bin/codeproof.js"
8
+ },
9
+ "engines": {
10
+ "node": ">=18"
11
+ },
12
+ "license": "MIT"
13
+ }
@@ -0,0 +1,100 @@
1
+ import path from "path";
2
+
3
+ // Boundary: reporting only. Must not import rule logic or AI logic.
4
+ // Reports are built from provided inputs to keep dependencies one-directional.
5
+
6
+ function toRelativePath(projectRoot, filePath) {
7
+ if (!filePath) {
8
+ return "";
9
+ }
10
+ const relative = path.relative(projectRoot, filePath);
11
+ return relative || filePath;
12
+ }
13
+
14
+ function trimSnippet(snippet, maxLength = 200) {
15
+ if (!snippet) {
16
+ return "";
17
+ }
18
+ const trimmed = String(snippet).trim();
19
+ if (trimmed.length <= maxLength) {
20
+ return trimmed;
21
+ }
22
+ return trimmed.slice(0, maxLength) + "...";
23
+ }
24
+
25
+ function redactSecretSnippet(ruleId, snippet) {
26
+ if (!snippet) {
27
+ return "";
28
+ }
29
+ if (String(ruleId || "").startsWith("secret.")) {
30
+ return "***";
31
+ }
32
+ return snippet;
33
+ }
34
+
35
+ function mapDecisionConfidence(value) {
36
+ if (typeof value !== "number") {
37
+ return "low";
38
+ }
39
+ return value >= 0.75 ? "high" : "low";
40
+ }
41
+
42
+ function normalizeSeverity(value) {
43
+ return value === "block" ? "block" : "warn";
44
+ }
45
+
46
+ export function buildReport({
47
+ projectRoot,
48
+ scanMode,
49
+ filesScannedCount,
50
+ baselineFindings,
51
+ aiReviewed,
52
+ runId,
53
+ timestamp
54
+ }) {
55
+ const aiById = new Map(aiReviewed.map((entry) => [entry.finding.findingId, entry.decision]));
56
+
57
+ const findings = baselineFindings.map((finding) => {
58
+ const decision = aiById.get(finding.findingId);
59
+ const severity = normalizeSeverity(decision ? decision.verdict : finding.severity);
60
+ const confidence = decision
61
+ ? mapDecisionConfidence(decision.confidence)
62
+ : finding.confidence === "high"
63
+ ? "high"
64
+ : "low";
65
+
66
+ return {
67
+ ruleId: finding.ruleId,
68
+ severity,
69
+ confidence,
70
+ filePath: toRelativePath(projectRoot, finding.filePath),
71
+ lineNumber: finding.line || null,
72
+ codeSnippet: trimSnippet(redactSecretSnippet(finding.ruleId, finding.snippet)),
73
+ explanation: decision?.explanation || finding.message || ""
74
+ };
75
+ });
76
+
77
+ const blocksCount = findings.filter((finding) => finding.severity === "block").length;
78
+ const warningsCount = findings.filter((finding) => finding.severity === "warn").length;
79
+
80
+ const finalVerdict = blocksCount > 0
81
+ ? "blocked"
82
+ : warningsCount > 0
83
+ ? "allowed_with_warnings"
84
+ : "allowed";
85
+
86
+ return {
87
+ runId,
88
+ timestamp,
89
+ projectRoot,
90
+ scanMode,
91
+ summary: {
92
+ totalFilesScanned: filesScannedCount,
93
+ totalFindings: findings.length,
94
+ blocksCount,
95
+ warningsCount
96
+ },
97
+ findings,
98
+ finalVerdict
99
+ };
100
+ }
@@ -0,0 +1,19 @@
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+
5
+ // Boundary: reporting storage only. Must not import rule logic, AI logic, or integrations.
6
+
7
+ export function appendReport({ projectRoot, report }) {
8
+ const reportPath = path.join(projectRoot, "codeproof-report.log");
9
+ const line = JSON.stringify(report) + os.EOL;
10
+
11
+ // Append-only to preserve an immutable audit trail of every run.
12
+ const fileHandle = fs.openSync(reportPath, "a");
13
+ try {
14
+ fs.writeSync(fileHandle, line, null, "utf8");
15
+ fs.fsyncSync(fileHandle);
16
+ } finally {
17
+ fs.closeSync(fileHandle);
18
+ }
19
+ }
@@ -0,0 +1,11 @@
1
+ import { dangerousUsagePatterns } from "./regexPatterns.js";
2
+ import { extractFindings } from "./ruleUtils.js";
3
+
4
+ export function evaluateDangerousUsageRule({ content, filePath, lineStarts }) {
5
+ return extractFindings({
6
+ patterns: dangerousUsagePatterns,
7
+ content,
8
+ filePath,
9
+ lineStarts
10
+ });
11
+ }
@@ -0,0 +1,11 @@
1
+ import { insecureConfigPatterns } from "./regexPatterns.js";
2
+ import { extractFindings } from "./ruleUtils.js";
3
+
4
+ export function evaluateInsecureConfigRule({ content, filePath, lineStarts }) {
5
+ return extractFindings({
6
+ patterns: insecureConfigPatterns,
7
+ content,
8
+ filePath,
9
+ lineStarts
10
+ });
11
+ }
@@ -0,0 +1,53 @@
1
+ // Centralized regex patterns for deterministic rules.
2
+ // Regex-first keeps scans fast and predictable; AI is used only for ambiguous cases.
3
+
4
+ export const secretPatterns = [
5
+ {
6
+ id: "secret.aws_access_key",
7
+ regex: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g,
8
+ severity: "block",
9
+ confidence: "high",
10
+ message: "Possible AWS access key detected."
11
+ },
12
+ {
13
+ id: "secret.generic_api_key",
14
+ regex: /\bapi[_-]?key\s*[:=]\s*['"][A-Za-z0-9\-_]{8,}['"]/gi,
15
+ severity: "block",
16
+ confidence: "low",
17
+ message: "Possible API key detected."
18
+ },
19
+ {
20
+ id: "secret.generic_token",
21
+ regex: /\b(token|password|secret)\s*[:=]\s*['"][^'"\n]{6,}['"]/gi,
22
+ severity: "block",
23
+ confidence: "low",
24
+ message: "Possible secret material detected."
25
+ }
26
+ ];
27
+
28
+ export const dangerousUsagePatterns = [
29
+ {
30
+ id: "code.dangerous_eval",
31
+ regex: /\b(eval|exec)\s*\(/g,
32
+ severity: "warn",
33
+ confidence: "high",
34
+ message: "Dangerous function usage detected."
35
+ }
36
+ ];
37
+
38
+ export const insecureConfigPatterns = [
39
+ {
40
+ id: "config.debug_true",
41
+ regex: /\bDEBUG\s*=\s*true\b/gi,
42
+ severity: "warn",
43
+ confidence: "high",
44
+ message: "Insecure DEBUG flag enabled."
45
+ },
46
+ {
47
+ id: "config.node_env_development",
48
+ regex: /\bNODE_ENV\s*=\s*development\b/gi,
49
+ severity: "warn",
50
+ confidence: "high",
51
+ message: "NODE_ENV set to development."
52
+ }
53
+ ];
@@ -0,0 +1,58 @@
1
+ // Shared helpers for regex-based rule evaluation.
2
+
3
+ export function buildLineIndex(content) {
4
+ const starts = [0];
5
+ for (let i = 0; i < content.length; i += 1) {
6
+ if (content[i] === "\n") {
7
+ starts.push(i + 1);
8
+ }
9
+ }
10
+ return starts;
11
+ }
12
+
13
+ function getLineInfo(content, index, lineStarts) {
14
+ let low = 0;
15
+ let high = lineStarts.length - 1;
16
+ let lineNumber = 1;
17
+
18
+ while (low <= high) {
19
+ const mid = Math.floor((low + high) / 2);
20
+ if (lineStarts[mid] <= index) {
21
+ lineNumber = mid + 1;
22
+ low = mid + 1;
23
+ } else {
24
+ high = mid - 1;
25
+ }
26
+ }
27
+
28
+ const lineStart = lineStarts[lineNumber - 1] ?? 0;
29
+ const lineEnd = content.indexOf("\n", lineStart);
30
+ const rawLine = content.slice(lineStart, lineEnd === -1 ? content.length : lineEnd);
31
+ const snippet = rawLine.trim().slice(0, 160);
32
+
33
+ return { lineNumber, snippet };
34
+ }
35
+
36
+ export function extractFindings({ patterns, content, filePath, lineStarts }) {
37
+ const findings = [];
38
+
39
+ for (const pattern of patterns) {
40
+ const regex = new RegExp(pattern.regex.source, pattern.regex.flags);
41
+ let match = regex.exec(content);
42
+ while (match) {
43
+ const { lineNumber, snippet } = getLineInfo(content, match.index, lineStarts);
44
+ findings.push({
45
+ ruleId: pattern.id,
46
+ severity: pattern.severity,
47
+ confidence: pattern.confidence,
48
+ message: pattern.message,
49
+ filePath,
50
+ line: lineNumber,
51
+ snippet: snippet || match[0]
52
+ });
53
+ match = regex.exec(content);
54
+ }
55
+ }
56
+
57
+ return findings;
58
+ }
@@ -0,0 +1,21 @@
1
+ import { secretPatterns } from "./regexPatterns.js";
2
+ import { extractFindings } from "./ruleUtils.js";
3
+
4
+ // Regex-first for secrets ensures fast, deterministic detection.
5
+ // Low-confidence matches are escalated for future AI context review.
6
+ export function evaluateSecretRule({ content, filePath, lineStarts }) {
7
+ const findings = extractFindings({ patterns: secretPatterns, content, filePath, lineStarts });
8
+
9
+ const highConfidence = [];
10
+ const lowConfidence = [];
11
+
12
+ for (const finding of findings) {
13
+ if (finding.confidence === "high") {
14
+ highConfidence.push(finding);
15
+ } else {
16
+ lowConfidence.push(finding);
17
+ }
18
+ }
19
+
20
+ return { findings: highConfidence, escalations: lowConfidence };
21
+ }
@@ -0,0 +1,42 @@
1
+ function padRight(text, width) {
2
+ const safeText = text ?? "";
3
+ const padding = Math.max(0, width - safeText.length);
4
+ return safeText + " ".repeat(padding);
5
+ }
6
+
7
+ function buildBox(lines) {
8
+ const width = Math.max(...lines.map((line) => line.length), 0);
9
+ const top = "─".repeat(width + 4);
10
+ const bottom = "─".repeat(width + 4);
11
+ const body = lines.map((line) => `│ ${padRight(line, width)} │`);
12
+ return [top, ...body, bottom].join("\n");
13
+ }
14
+
15
+ export function buildWelcomeScreen({ projectType, scanMode, configPath }) {
16
+ // UX: use a clean box with minimal symbols so the message is readable and calm in any terminal.
17
+ const lines = [
18
+ "✓ CodeProof initialized successfully.",
19
+ "",
20
+ "What it does: Scans for secrets and risky patterns before you commit.",
21
+ "When it runs: On pre-commit, or when you run it manually.",
22
+ "Commit impact: Blocks commits only on high-confidence blockers.",
23
+ "",
24
+ `Project type: ${projectType}`,
25
+ `Scan mode: ${scanMode}`,
26
+ `Config file: ${configPath}`,
27
+ "",
28
+ "Commands:",
29
+ "→ codeproof run Manually run checks",
30
+ "→ codeproof status Show current configuration",
31
+ "→ codeproof report@dashboard View reports (placeholder)",
32
+ "→ codeproof move-secret Safely move detected secrets (experimental)"
33
+ ];
34
+
35
+ return buildBox(lines);
36
+ }
37
+
38
+ export function showWelcomeScreen(details) {
39
+ // UX: print once after init to reassure users without changing command behavior elsewhere.
40
+ const message = buildWelcomeScreen(details);
41
+ console.log(message);
42
+ }