codeproof 1.0.0 → 1.0.2
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/bin/codeproof.js +19 -1
- package/commands/apply.js +32 -0
- package/commands/ignore.js +32 -0
- package/commands/init.js +36 -1
- package/commands/moveSecret.js +2 -20
- package/commands/reportDashboard.js +13 -34
- package/commands/run.js +40 -32
- package/commands/whoami.js +19 -0
- package/core/enforcement.js +51 -0
- package/core/identity.js +78 -0
- package/hooks/preCommit.js +28 -2
- package/package.json +5 -2
- package/reporting/reportBuilder.js +6 -3
- package/reporting/reportReader.js +49 -0
- package/reporting/reportWriter.js +53 -10
- package/utils/fileScanner.js +40 -0
- package/utils/gitIgnore.js +55 -0
package/bin/codeproof.js
CHANGED
|
@@ -3,13 +3,16 @@ import { runInit } from "../commands/init.js";
|
|
|
3
3
|
import { runCli } from "../commands/run.js";
|
|
4
4
|
import { runReportDashboard } from "../commands/reportDashboard.js";
|
|
5
5
|
import { runMoveSecret } from "../commands/moveSecret.js";
|
|
6
|
+
import { runWhoAmI } from "../commands/whoami.js";
|
|
7
|
+
import { runIgnore } from "../commands/ignore.js";
|
|
8
|
+
import { runApply } from "../commands/apply.js";
|
|
6
9
|
import { logError, logInfo } from "../utils/logger.js";
|
|
7
10
|
|
|
8
11
|
const [, , command, ...args] = process.argv;
|
|
9
12
|
|
|
10
13
|
async function main() {
|
|
11
14
|
if (!command || command === "-h" || command === "--help") {
|
|
12
|
-
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");
|
|
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");
|
|
13
16
|
process.exit(0);
|
|
14
17
|
}
|
|
15
18
|
|
|
@@ -33,6 +36,21 @@ async function main() {
|
|
|
33
36
|
return;
|
|
34
37
|
}
|
|
35
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
|
+
|
|
49
|
+
if (command === "whoami") {
|
|
50
|
+
await runWhoAmI();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
36
54
|
logError(`Unknown command: ${command}`);
|
|
37
55
|
process.exit(1);
|
|
38
56
|
}
|
|
@@ -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
|
@@ -5,11 +5,15 @@ import { logInfo, logSuccess, logWarn } from "../utils/logger.js";
|
|
|
5
5
|
import { detectProjectType } from "../utils/projectType.js";
|
|
6
6
|
import { installPreCommitHook } from "../hooks/preCommit.js";
|
|
7
7
|
import { showWelcomeScreen } from "../ui/welcomeScreen.js";
|
|
8
|
+
import { getClientId } from "../core/identity.js";
|
|
9
|
+
import { randomUUID } from "crypto";
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
export async function runInit({ cwd }) {
|
|
12
14
|
logInfo("Initializing CodeProof...");
|
|
15
|
+
|
|
16
|
+
getClientId();
|
|
13
17
|
|
|
14
18
|
ensureGitRepo(cwd);
|
|
15
19
|
logSuccess("Git repository detected.");
|
|
@@ -23,11 +27,30 @@ export async function runInit({ cwd }) {
|
|
|
23
27
|
const configPath = path.join(gitRoot, "codeproof.config.json");
|
|
24
28
|
// Avoid overwriting user configuration to keep init idempotent.
|
|
25
29
|
if (fs.existsSync(configPath)) {
|
|
26
|
-
|
|
30
|
+
let updated = false;
|
|
31
|
+
try {
|
|
32
|
+
const raw = fs.readFileSync(configPath, "utf8");
|
|
33
|
+
const existing = JSON.parse(raw);
|
|
34
|
+
if (!existing.projectId) {
|
|
35
|
+
existing.projectId = randomUUID();
|
|
36
|
+
updated = true;
|
|
37
|
+
fs.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
logWarn("Config already exists but could not be updated.");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (updated) {
|
|
44
|
+
logSuccess("Added projectId to codeproof.config.json");
|
|
45
|
+
} else {
|
|
46
|
+
logWarn("Config already exists. Skipping creation.");
|
|
47
|
+
}
|
|
27
48
|
} else {
|
|
28
49
|
const config = {
|
|
50
|
+
projectId: randomUUID(),
|
|
29
51
|
projectType,
|
|
30
52
|
scanMode: "staged",
|
|
53
|
+
enforcement: "enabled",
|
|
31
54
|
features: {
|
|
32
55
|
reporting: true,
|
|
33
56
|
integration: false,
|
|
@@ -48,6 +71,18 @@ export async function runInit({ cwd }) {
|
|
|
48
71
|
logSuccess("Created codeproof.config.json");
|
|
49
72
|
}
|
|
50
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
|
+
|
|
51
86
|
installPreCommitHook(gitRoot);
|
|
52
87
|
logSuccess("Pre-commit hook installed.");
|
|
53
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.");
|
|
@@ -5,6 +5,7 @@ import { logError, logInfo, logWarn } from "../utils/logger.js";
|
|
|
5
5
|
import { sendReportToServer } from "../utils/apiClient.js";
|
|
6
6
|
import { resolveFeatureFlags, isVerbose } from "../core/featureFlags.js";
|
|
7
7
|
import { reportFeatureDisabled, withFailOpenIntegration } from "../core/safetyGuards.js";
|
|
8
|
+
import { readLatestReport } from "../reporting/reportReader.js";
|
|
8
9
|
|
|
9
10
|
function readConfig(configPath) {
|
|
10
11
|
if (!fs.existsSync(configPath)) {
|
|
@@ -21,24 +22,6 @@ function readConfig(configPath) {
|
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
function readLatestReport(reportPath) {
|
|
25
|
-
if (!fs.existsSync(reportPath)) {
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const content = fs.readFileSync(reportPath, "utf8");
|
|
30
|
-
const lines = content.split(/\r?\n/).filter(Boolean);
|
|
31
|
-
if (lines.length === 0) {
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
return JSON.parse(lines[lines.length - 1]);
|
|
37
|
-
} catch {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
25
|
export async function runReportDashboard({ cwd }) {
|
|
43
26
|
// Boundary: CLI orchestration only. Avoid importing this module in lower layers.
|
|
44
27
|
ensureGitRepo(cwd);
|
|
@@ -49,30 +32,26 @@ export async function runReportDashboard({ cwd }) {
|
|
|
49
32
|
const verbose = isVerbose(config);
|
|
50
33
|
|
|
51
34
|
if (features.reporting) {
|
|
52
|
-
const
|
|
53
|
-
const latestReport =
|
|
35
|
+
const latestEntry = readLatestReport(gitRoot);
|
|
36
|
+
const latestReport = latestEntry?.report || null;
|
|
54
37
|
|
|
55
38
|
if (!latestReport) {
|
|
56
|
-
logWarn("No reports found. Run
|
|
39
|
+
logWarn("No reports found. Run `codeproof run` first.");
|
|
57
40
|
} else {
|
|
58
41
|
const integration = config?.integration || {};
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
enabled: true,
|
|
65
|
-
endpointUrl: integration.endpointUrl
|
|
66
|
-
});
|
|
42
|
+
// Integrations are fail-open: never throw on network errors.
|
|
43
|
+
withFailOpenIntegration(() => {
|
|
44
|
+
sendReportToServer(latestReport, {
|
|
45
|
+
enabled: true,
|
|
46
|
+
endpointUrl: integration.endpointUrl
|
|
67
47
|
});
|
|
68
|
-
}
|
|
69
|
-
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (latestReport?.projectId) {
|
|
51
|
+
logInfo(`View dashboard: https://dashboard.codeproof.dev/project/${latestReport.projectId}`);
|
|
70
52
|
}
|
|
71
53
|
}
|
|
72
54
|
} else {
|
|
73
55
|
reportFeatureDisabled("Reporting", verbose, logInfo);
|
|
74
56
|
}
|
|
75
|
-
|
|
76
|
-
const projectId = encodeURIComponent(path.basename(gitRoot) || "project");
|
|
77
|
-
logInfo(`Dashboard: https://dashboard.codeproof.dev/project/${projectId}`);
|
|
78
57
|
}
|
package/commands/run.js
CHANGED
|
@@ -2,15 +2,17 @@ import fs from "fs";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { ensureGitRepo, getGitRoot, getStagedFiles } from "../utils/git.js";
|
|
4
4
|
import { logError, logInfo, logSuccess, logWarn } from "../utils/logger.js";
|
|
5
|
-
import {
|
|
5
|
+
import { buildScanTargets } from "../utils/fileScanner.js";
|
|
6
6
|
import { runRuleEngine } from "../engine/ruleEngine.js";
|
|
7
7
|
import { analyze } from "../engine/aiAnalyzer.js";
|
|
8
8
|
import { mergeDecisions } from "../engine/decisionMerger.js";
|
|
9
9
|
import { buildAiInputs, buildProjectContext } from "../engine/contextBuilder.js";
|
|
10
10
|
import { buildReport } from "../reporting/reportBuilder.js";
|
|
11
|
-
import {
|
|
11
|
+
import { writeReport } from "../reporting/reportWriter.js";
|
|
12
12
|
import { sendReportToServer } from "../utils/apiClient.js";
|
|
13
13
|
import { resolveFeatureFlags, isVerbose } from "../core/featureFlags.js";
|
|
14
|
+
import { getClientId } from "../core/identity.js";
|
|
15
|
+
import { getEnforcementState } from "../core/enforcement.js";
|
|
14
16
|
import {
|
|
15
17
|
reportFeatureDisabled,
|
|
16
18
|
warnExperimentalOnce,
|
|
@@ -18,6 +20,7 @@ import {
|
|
|
18
20
|
withFailOpenIntegration,
|
|
19
21
|
withFailOpenReporting
|
|
20
22
|
} from "../core/safetyGuards.js";
|
|
23
|
+
import { randomUUID } from "crypto";
|
|
21
24
|
|
|
22
25
|
function readConfig(configPath) {
|
|
23
26
|
if (!fs.existsSync(configPath)) {
|
|
@@ -34,23 +37,7 @@ function readConfig(configPath) {
|
|
|
34
37
|
}
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
function
|
|
38
|
-
return files
|
|
39
|
-
.map((filePath) => (path.isAbsolute(filePath) ? filePath : path.join(gitRoot, filePath)))
|
|
40
|
-
.filter((filePath) => {
|
|
41
|
-
try {
|
|
42
|
-
return fs.statSync(filePath).isFile();
|
|
43
|
-
} catch {
|
|
44
|
-
return false;
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function filterBinaryFiles(files) {
|
|
50
|
-
return files.filter((filePath) => !isBinaryFile(filePath));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export async function runCli({ cwd }) {
|
|
40
|
+
export async function runCli({ args = [], cwd }) {
|
|
54
41
|
// Boundary: CLI orchestration only. Avoid importing this module in lower layers.
|
|
55
42
|
logInfo("CodeProof run started.");
|
|
56
43
|
|
|
@@ -60,6 +47,20 @@ export async function runCli({ cwd }) {
|
|
|
60
47
|
const config = readConfig(configPath);
|
|
61
48
|
const features = resolveFeatureFlags(config);
|
|
62
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
|
+
}
|
|
63
64
|
|
|
64
65
|
if (!config.scanMode) {
|
|
65
66
|
logError("Config missing scanMode. Expected 'staged' or 'full'.");
|
|
@@ -71,27 +72,30 @@ export async function runCli({ cwd }) {
|
|
|
71
72
|
|
|
72
73
|
if (scanMode === "staged") {
|
|
73
74
|
logInfo("Scan mode: staged");
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
targets = buildScanTargets({
|
|
76
|
+
gitRoot,
|
|
77
|
+
scanMode,
|
|
78
|
+
stagedFiles: getStagedFiles(gitRoot)
|
|
79
|
+
});
|
|
76
80
|
} else if (scanMode === "full") {
|
|
77
81
|
logInfo("Scan mode: full");
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
targets = buildScanTargets({
|
|
83
|
+
gitRoot,
|
|
84
|
+
scanMode,
|
|
85
|
+
stagedFiles: []
|
|
86
|
+
});
|
|
81
87
|
} else {
|
|
82
88
|
logError("Invalid scanMode. Expected 'staged' or 'full'.");
|
|
83
89
|
process.exit(1);
|
|
84
90
|
}
|
|
85
91
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (filtered.length === 0) {
|
|
92
|
+
if (targets.length === 0) {
|
|
89
93
|
logWarn("No relevant files found. Exiting.");
|
|
90
94
|
// Exit code 0 allows the Git commit to continue.
|
|
91
95
|
process.exit(0);
|
|
92
96
|
}
|
|
93
97
|
|
|
94
|
-
const { findings, escalations } = runRuleEngine({ files:
|
|
98
|
+
const { findings, escalations } = runRuleEngine({ files: targets });
|
|
95
99
|
const projectContext = buildProjectContext({ gitRoot, config });
|
|
96
100
|
let aiInputs = [];
|
|
97
101
|
if (features.aiEscalation) {
|
|
@@ -116,18 +120,22 @@ export async function runCli({ cwd }) {
|
|
|
116
120
|
if (features.reporting) {
|
|
117
121
|
withFailOpenReporting(() => {
|
|
118
122
|
const timestamp = new Date().toISOString();
|
|
119
|
-
const
|
|
123
|
+
const reportId = randomUUID();
|
|
124
|
+
const projectId = config.projectId || "";
|
|
125
|
+
const clientId = getClientId();
|
|
120
126
|
const report = buildReport({
|
|
121
127
|
projectRoot: gitRoot,
|
|
128
|
+
projectId,
|
|
129
|
+
clientId,
|
|
130
|
+
reportId,
|
|
122
131
|
scanMode,
|
|
123
|
-
filesScannedCount:
|
|
132
|
+
filesScannedCount: targets.length,
|
|
124
133
|
baselineFindings: [...findings, ...escalations],
|
|
125
134
|
aiReviewed,
|
|
126
|
-
runId,
|
|
127
135
|
timestamp
|
|
128
136
|
});
|
|
129
137
|
// Reporting is fail-open: never block commits if logging fails.
|
|
130
|
-
|
|
138
|
+
writeReport({ projectRoot: gitRoot, report });
|
|
131
139
|
|
|
132
140
|
const integration = config?.integration || {};
|
|
133
141
|
const integrationEnabled = features.integration && Boolean(integration.enabled);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { readClientId, getGlobalConfigPath } from "../core/identity.js";
|
|
2
|
+
import { logError, logInfo } from "../utils/logger.js";
|
|
3
|
+
|
|
4
|
+
export async function runWhoAmI() {
|
|
5
|
+
const clientId = readClientId();
|
|
6
|
+
|
|
7
|
+
if (!clientId) {
|
|
8
|
+
logError("CodeProof not initialized. Run codeproof init first.");
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
logInfo("CodeProof Client ID:");
|
|
13
|
+
logInfo(clientId);
|
|
14
|
+
logInfo("");
|
|
15
|
+
logInfo("Use this ID to log in at:");
|
|
16
|
+
logInfo("https://your-dashboard-url.com/login");
|
|
17
|
+
logInfo("");
|
|
18
|
+
logInfo(`Config: ${getGlobalConfigPath()}`);
|
|
19
|
+
}
|
|
@@ -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/identity.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { randomUUID } from "crypto";
|
|
5
|
+
|
|
6
|
+
const CONFIG_DIR_NAME = ".codeproof";
|
|
7
|
+
const CONFIG_FILE_NAME = "config.json";
|
|
8
|
+
|
|
9
|
+
function getConfigDir() {
|
|
10
|
+
return path.join(os.homedir(), CONFIG_DIR_NAME);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getConfigPath() {
|
|
14
|
+
return path.join(getConfigDir(), CONFIG_FILE_NAME);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ensureConfigDir() {
|
|
18
|
+
const dir = getConfigDir();
|
|
19
|
+
if (!fs.existsSync(dir)) {
|
|
20
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
fs.chmodSync(dir, 0o700);
|
|
24
|
+
} catch {
|
|
25
|
+
// Best-effort on platforms that ignore chmod.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function writeConfig(config) {
|
|
30
|
+
const filePath = getConfigPath();
|
|
31
|
+
const payload = JSON.stringify(config, null, 2) + "\n";
|
|
32
|
+
fs.writeFileSync(filePath, payload, { encoding: "utf8", mode: 0o600 });
|
|
33
|
+
try {
|
|
34
|
+
fs.chmodSync(filePath, 0o600);
|
|
35
|
+
} catch {
|
|
36
|
+
// Best-effort on platforms that ignore chmod.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readConfig() {
|
|
41
|
+
const filePath = getConfigPath();
|
|
42
|
+
if (!fs.existsSync(filePath)) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
48
|
+
return JSON.parse(raw);
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function ensureClientId() {
|
|
55
|
+
ensureConfigDir();
|
|
56
|
+
|
|
57
|
+
const existing = readConfig();
|
|
58
|
+
if (existing?.clientId) {
|
|
59
|
+
return existing.clientId;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const clientId = randomUUID();
|
|
63
|
+
writeConfig({ clientId });
|
|
64
|
+
return clientId;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getClientId() {
|
|
68
|
+
return ensureClientId();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function readClientId() {
|
|
72
|
+
const existing = readConfig();
|
|
73
|
+
return existing?.clientId ?? null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getGlobalConfigPath() {
|
|
77
|
+
return getConfigPath();
|
|
78
|
+
}
|
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');try{const c=JSON.parse(fs.readFileSync('$CONFIG_PATH','utf8'));console.log((c.enforcement||'enabled').toLowerCase());}catch(e){console.log('enabled');}\")",
|
|
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');try{const c=JSON.parse(fs.readFileSync('$CONFIG_PATH','utf8'));console.log((c.enforcement||'enabled').toLowerCase());}catch(e){console.log('enabled');}\")",
|
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeproof",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "CodeProof CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,5 +9,8 @@
|
|
|
9
9
|
"engines": {
|
|
10
10
|
"node": ">=18"
|
|
11
11
|
},
|
|
12
|
-
"license": "MIT"
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"uuid": "^13.0.0"
|
|
15
|
+
}
|
|
13
16
|
}
|
|
@@ -45,11 +45,13 @@ function normalizeSeverity(value) {
|
|
|
45
45
|
|
|
46
46
|
export function buildReport({
|
|
47
47
|
projectRoot,
|
|
48
|
+
projectId,
|
|
49
|
+
clientId,
|
|
50
|
+
reportId,
|
|
48
51
|
scanMode,
|
|
49
52
|
filesScannedCount,
|
|
50
53
|
baselineFindings,
|
|
51
54
|
aiReviewed,
|
|
52
|
-
runId,
|
|
53
55
|
timestamp
|
|
54
56
|
}) {
|
|
55
57
|
const aiById = new Map(aiReviewed.map((entry) => [entry.finding.findingId, entry.decision]));
|
|
@@ -84,9 +86,10 @@ export function buildReport({
|
|
|
84
86
|
: "allowed";
|
|
85
87
|
|
|
86
88
|
return {
|
|
87
|
-
|
|
89
|
+
reportId,
|
|
88
90
|
timestamp,
|
|
89
|
-
|
|
91
|
+
projectId,
|
|
92
|
+
clientId,
|
|
90
93
|
scanMode,
|
|
91
94
|
summary: {
|
|
92
95
|
totalFilesScanned: filesScannedCount,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
const REPORT_DIR_NAME = "codeproof-reports";
|
|
5
|
+
const REPORT_PATTERN = /^report-(\d+)\.json$/;
|
|
6
|
+
|
|
7
|
+
function getReportDir(projectRoot) {
|
|
8
|
+
return path.join(projectRoot, REPORT_DIR_NAME);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getReportNumbers(files) {
|
|
12
|
+
return files
|
|
13
|
+
.map((file) => {
|
|
14
|
+
const match = REPORT_PATTERN.exec(file);
|
|
15
|
+
return match ? Number.parseInt(match[1], 10) : null;
|
|
16
|
+
})
|
|
17
|
+
.filter((value) => Number.isInteger(value));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function readLatestReport(projectRoot) {
|
|
21
|
+
const reportDir = getReportDir(projectRoot);
|
|
22
|
+
if (!fs.existsSync(reportDir)) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let files = [];
|
|
27
|
+
try {
|
|
28
|
+
files = fs.readdirSync(reportDir);
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const numbers = getReportNumbers(files).sort((a, b) => b - a);
|
|
34
|
+
if (numbers.length === 0) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const number of numbers) {
|
|
39
|
+
const reportPath = path.join(reportDir, `report-${number}.json`);
|
|
40
|
+
try {
|
|
41
|
+
const raw = fs.readFileSync(reportPath, "utf8");
|
|
42
|
+
return { report: JSON.parse(raw), reportPath };
|
|
43
|
+
} catch {
|
|
44
|
+
// Skip unreadable or invalid JSON reports instead of crashing.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
@@ -1,19 +1,62 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
|
-
import os from "os";
|
|
3
2
|
import path from "path";
|
|
4
3
|
|
|
4
|
+
const REPORT_DIR_NAME = "codeproof-reports";
|
|
5
|
+
const REPORT_PREFIX = "report-";
|
|
6
|
+
const REPORT_SUFFIX = ".json";
|
|
7
|
+
const REPORT_PATTERN = /^report-(\d+)\.json$/;
|
|
8
|
+
|
|
5
9
|
// Boundary: reporting storage only. Must not import rule logic, AI logic, or integrations.
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
function getReportDir(projectRoot) {
|
|
12
|
+
return path.join(projectRoot, REPORT_DIR_NAME);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function ensureReportDir(reportDir) {
|
|
16
|
+
if (!fs.existsSync(reportDir)) {
|
|
17
|
+
fs.mkdirSync(reportDir, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
10
20
|
|
|
11
|
-
|
|
12
|
-
|
|
21
|
+
function getNextReportNumber(reportDir) {
|
|
22
|
+
let files = [];
|
|
13
23
|
try {
|
|
14
|
-
fs.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
24
|
+
files = fs.readdirSync(reportDir);
|
|
25
|
+
} catch {
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const numbers = files
|
|
30
|
+
.map((file) => {
|
|
31
|
+
const match = REPORT_PATTERN.exec(file);
|
|
32
|
+
return match ? Number.parseInt(match[1], 10) : null;
|
|
33
|
+
})
|
|
34
|
+
.filter((value) => Number.isInteger(value));
|
|
35
|
+
|
|
36
|
+
if (numbers.length === 0) {
|
|
37
|
+
return 1;
|
|
18
38
|
}
|
|
39
|
+
|
|
40
|
+
return Math.max(...numbers) + 1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function writeReport({ projectRoot, report }) {
|
|
44
|
+
const reportDir = getReportDir(projectRoot);
|
|
45
|
+
ensureReportDir(reportDir);
|
|
46
|
+
|
|
47
|
+
// Per-run JSON keeps every audit entry immutable and easy to archive.
|
|
48
|
+
let reportNumber = getNextReportNumber(reportDir);
|
|
49
|
+
let reportPath = path.join(reportDir, `${REPORT_PREFIX}${reportNumber}${REPORT_SUFFIX}`);
|
|
50
|
+
while (fs.existsSync(reportPath)) {
|
|
51
|
+
reportNumber += 1;
|
|
52
|
+
reportPath = path.join(reportDir, `${REPORT_PREFIX}${reportNumber}${REPORT_SUFFIX}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Use numeric sequencing over timestamps to avoid collisions in fast CI runs.
|
|
56
|
+
const tempPath = path.join(reportDir, `.tmp-${process.pid}-${Date.now()}.json`);
|
|
57
|
+
const payload = JSON.stringify(report, null, 2) + "\n";
|
|
58
|
+
fs.writeFileSync(tempPath, payload, "utf8");
|
|
59
|
+
fs.renameSync(tempPath, reportPath);
|
|
60
|
+
|
|
61
|
+
return reportPath;
|
|
19
62
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { filterIgnoredFiles } from "./gitIgnore.js";
|
|
4
|
+
import { getDefaultExcludes, isBinaryFile, listFilesRecursive } from "./files.js";
|
|
5
|
+
|
|
6
|
+
function normalizeScopeFiles(files, gitRoot) {
|
|
7
|
+
return files
|
|
8
|
+
.map((filePath) => (path.isAbsolute(filePath) ? filePath : path.join(gitRoot, filePath)))
|
|
9
|
+
.filter((filePath) => {
|
|
10
|
+
try {
|
|
11
|
+
return fs.statSync(filePath).isFile();
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isExcludedByDefault(filePath, gitRoot, excludes) {
|
|
19
|
+
const relative = path.relative(gitRoot, filePath);
|
|
20
|
+
const segments = relative.split(path.sep);
|
|
21
|
+
return segments.some((segment) => excludes.has(segment));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function buildScanTargets({ gitRoot, scanMode, stagedFiles }) {
|
|
25
|
+
const excludes = getDefaultExcludes();
|
|
26
|
+
|
|
27
|
+
let scopeFiles = [];
|
|
28
|
+
if (scanMode === "staged") {
|
|
29
|
+
const normalized = normalizeScopeFiles(stagedFiles, gitRoot);
|
|
30
|
+
scopeFiles = normalized.filter((filePath) => !isExcludedByDefault(filePath, gitRoot, excludes));
|
|
31
|
+
} else {
|
|
32
|
+
scopeFiles = normalizeScopeFiles(listFilesRecursive(gitRoot, excludes), gitRoot);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Respect .gitignore via `git check-ignore` so scans only touch tracked sources.
|
|
36
|
+
const notIgnored = filterIgnoredFiles({ gitRoot, filePaths: scopeFiles });
|
|
37
|
+
|
|
38
|
+
// Exclude binary files to avoid false positives and wasted I/O.
|
|
39
|
+
return notIgnored.filter((filePath) => !isBinaryFile(filePath));
|
|
40
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { spawnSync } from "child_process";
|
|
3
|
+
|
|
4
|
+
function normalizePathForGit(filePath, gitRoot) {
|
|
5
|
+
const relative = path.isAbsolute(filePath)
|
|
6
|
+
? path.relative(gitRoot, filePath)
|
|
7
|
+
: filePath;
|
|
8
|
+
|
|
9
|
+
if (!relative || relative.startsWith("..")) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return relative.replace(/\\/g, "/");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function filterIgnoredFiles({ gitRoot, filePaths }) {
|
|
17
|
+
if (!Array.isArray(filePaths) || filePaths.length === 0) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const normalized = new Map();
|
|
22
|
+
for (const filePath of filePaths) {
|
|
23
|
+
const relative = normalizePathForGit(filePath, gitRoot);
|
|
24
|
+
if (relative) {
|
|
25
|
+
normalized.set(relative, filePath);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (normalized.size === 0) {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Use `git check-ignore` to respect .gitignore rules quickly and accurately.
|
|
34
|
+
const input = `${Array.from(normalized.keys()).join("\u0000")}\u0000`;
|
|
35
|
+
const result = spawnSync("git", ["check-ignore", "--stdin", "-z"], {
|
|
36
|
+
cwd: gitRoot,
|
|
37
|
+
input,
|
|
38
|
+
encoding: "utf8"
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (result.error || (result.status && result.status > 1)) {
|
|
42
|
+
return Array.from(normalized.values());
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const ignored = new Set(
|
|
46
|
+
String(result.stdout)
|
|
47
|
+
.split("\u0000")
|
|
48
|
+
.map((entry) => entry.trim())
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
return Array.from(normalized.entries())
|
|
53
|
+
.filter(([relative]) => !ignored.has(relative))
|
|
54
|
+
.map(([, original]) => original);
|
|
55
|
+
}
|