commit-cop 1.0.0 → 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.
- package/README.md +98 -20
- package/dist/checks/binaryFileCheck.d.ts +3 -0
- package/dist/checks/binaryFileCheck.d.ts.map +1 -0
- package/dist/checks/binaryFileCheck.js +53 -0
- package/dist/checks/binaryFileCheck.js.map +1 -0
- package/dist/checks/debuggerCheck.d.ts +3 -0
- package/dist/checks/debuggerCheck.d.ts.map +1 -0
- package/dist/checks/debuggerCheck.js +28 -0
- package/dist/checks/debuggerCheck.js.map +1 -0
- package/dist/checks/generatedFolderCheck.d.ts.map +1 -1
- package/dist/checks/generatedFolderCheck.js +7 -2
- package/dist/checks/generatedFolderCheck.js.map +1 -1
- package/dist/checks/junkFileCheck.d.ts +3 -0
- package/dist/checks/junkFileCheck.d.ts.map +1 -0
- package/dist/checks/junkFileCheck.js +30 -0
- package/dist/checks/junkFileCheck.js.map +1 -0
- package/dist/checks/lockfileDriftCheck.d.ts +3 -0
- package/dist/checks/lockfileDriftCheck.d.ts.map +1 -0
- package/dist/checks/lockfileDriftCheck.js +32 -0
- package/dist/checks/lockfileDriftCheck.js.map +1 -0
- package/dist/checks/mergeConflictCheck.d.ts +3 -0
- package/dist/checks/mergeConflictCheck.d.ts.map +1 -0
- package/dist/checks/mergeConflictCheck.js +33 -0
- package/dist/checks/mergeConflictCheck.js.map +1 -0
- package/dist/checks/secretCheck.d.ts.map +1 -1
- package/dist/checks/secretCheck.js +15 -4
- package/dist/checks/secretCheck.js.map +1 -1
- package/dist/checks/sensitiveFilenameCheck.d.ts +3 -0
- package/dist/checks/sensitiveFilenameCheck.d.ts.map +1 -0
- package/dist/checks/sensitiveFilenameCheck.js +41 -0
- package/dist/checks/sensitiveFilenameCheck.js.map +1 -0
- package/dist/checks/utils.d.ts +9 -0
- package/dist/checks/utils.d.ts.map +1 -0
- package/dist/checks/utils.js +48 -0
- package/dist/checks/utils.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scanner.js +14 -2
- package/dist/scanner.js.map +1 -1
- package/package.json +6 -5
- package/src/brand.ts +3 -0
- package/src/checks/binaryFileCheck.ts +64 -0
- package/src/checks/consoleLogCheck.ts +3 -2
- package/src/checks/debuggerCheck.ts +33 -0
- package/src/checks/envFileCheck.ts +3 -2
- package/src/checks/focusedTestCheck.ts +2 -2
- package/src/checks/generatedFolderCheck.ts +13 -5
- package/src/checks/junkFileCheck.ts +40 -0
- package/src/checks/largeFileCheck.ts +2 -2
- package/src/checks/localHostCheck.ts +2 -2
- package/src/checks/lockfileDriftCheck.ts +40 -0
- package/src/checks/mergeConflictCheck.ts +41 -0
- package/src/checks/secretCheck.ts +33 -17
- package/src/checks/sensitiveFilenameCheck.ts +51 -0
- package/src/checks/utils.ts +62 -0
- package/src/fix/debugCode.ts +74 -0
- package/src/fix/focusedTests.ts +26 -0
- package/src/fix/gitignore.ts +108 -0
- package/src/fix/junkFiles.ts +16 -0
- package/src/fix/lockfile.ts +23 -0
- package/src/fix/matchers.ts +141 -0
- package/src/fix/runFix.ts +96 -0
- package/src/fix/unstage.ts +25 -0
- package/src/fix/utils.ts +50 -0
- package/src/git.ts +2 -1
- package/src/hook.ts +98 -0
- package/src/index.ts +45 -27
- package/src/reporter.ts +70 -30
- package/src/runScan.ts +35 -0
- package/src/scanner.ts +19 -4
- package/src/types.ts +5 -0
- package/test.ts +5 -1
- package/testing.ts +3 -0
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,YAAY,CAAC;KAClB,WAAW,CAAC,4CAA4C,CAAC;KACzD,MAAM,CAAC,UAAU,EAAE,0BAA0B,CAAC;KAC9C,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;IACxB,MAAM,WAAW,GAAG,cAAc,EAAE,CAAC;IAErC,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7B,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC;QACtC,OAAO;IACT,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC;QAC/B,WAAW;QACX,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC;KAChC,CAAC,CAAC;IAEH,WAAW,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAE1C,MAAM,SAAS,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC;IAC3E,MAAM,WAAW,GAAG,QAAQ,CAAC,IAAI,CAC/B,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,KAAK,SAAS,CAC5C,CAAC;IAEF,IAAI,SAAS,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,WAAW,CAAC,EAAE,CAAC;QACjD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC;AAEL,OAAO,CAAC,KAAK,EAAE,CAAC"}
|
package/dist/scanner.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../src/scanner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../src/scanner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AA+BxD,wBAAsB,SAAS,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CASzE"}
|
package/dist/scanner.js
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
import { envFileCheck } from "./checks/envFileCheck.js";
|
|
2
2
|
import { generatedFolderCheck } from "./checks/generatedFolderCheck.js";
|
|
3
|
-
import {
|
|
3
|
+
import { mergeConflictCheck } from "./checks/mergeConflictCheck.js";
|
|
4
|
+
import { sensitiveFilenameCheck } from "./checks/sensitiveFilenameCheck.js";
|
|
5
|
+
import { secretCheck } from "./checks/secretCheck.js";
|
|
4
6
|
import { focusedTestCheck } from "./checks/focusedTestCheck.js";
|
|
7
|
+
import { consoleLogCheck } from "./checks/consoleLogCheck.js";
|
|
8
|
+
import { debuggerCheck } from "./checks/debuggerCheck.js";
|
|
5
9
|
import { localHostCheck } from "./checks/localHostCheck.js";
|
|
10
|
+
import { junkFileCheck } from "./checks/junkFileCheck.js";
|
|
11
|
+
import { lockfileDriftCheck } from "./checks/lockfileDriftCheck.js";
|
|
6
12
|
import { largeFileCheck } from "./checks/largeFileCheck.js";
|
|
7
|
-
import {
|
|
13
|
+
import { binaryFileCheck } from "./checks/binaryFileCheck.js";
|
|
8
14
|
const checks = [
|
|
15
|
+
mergeConflictCheck,
|
|
9
16
|
envFileCheck,
|
|
17
|
+
sensitiveFilenameCheck,
|
|
10
18
|
generatedFolderCheck,
|
|
11
19
|
secretCheck,
|
|
12
20
|
focusedTestCheck,
|
|
13
21
|
consoleLogCheck,
|
|
22
|
+
debuggerCheck,
|
|
14
23
|
localHostCheck,
|
|
24
|
+
junkFileCheck,
|
|
25
|
+
lockfileDriftCheck,
|
|
15
26
|
largeFileCheck,
|
|
27
|
+
binaryFileCheck,
|
|
16
28
|
];
|
|
17
29
|
export async function runChecks(context) {
|
|
18
30
|
const allFindings = [];
|
package/dist/scanner.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scanner.js","sourceRoot":"","sources":["../src/scanner.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AACxE,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"scanner.js","sourceRoot":"","sources":["../src/scanner.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AACxE,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAE9D,MAAM,MAAM,GAAG;IACb,kBAAkB;IAClB,YAAY;IACZ,sBAAsB;IACtB,oBAAoB;IACpB,WAAW;IACX,gBAAgB;IAChB,eAAe;IACf,aAAa;IACb,cAAc;IACd,aAAa;IACb,kBAAkB;IAClB,cAAc;IACd,eAAe;CAChB,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,OAAqB;IACnD,MAAM,WAAW,GAAc,EAAE,CAAC;IAElC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC1C,WAAW,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,CAAC;IAChC,CAAC;IAED,OAAO,WAAW,CAAC;AACrB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "commit-cop",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Commit Cop — a pre-commit safety checker that scans staged files for risky commits.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"
|
|
7
|
+
"commit-cop": "./dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"dev": "tsx src/index.ts",
|
|
11
11
|
"build": "tsc",
|
|
12
12
|
"start": "node dist/index.js",
|
|
13
|
-
"
|
|
13
|
+
"commit-cop": "tsx src/index.ts",
|
|
14
|
+
"demo:setup": "node scripts/setup-demo.mjs"
|
|
14
15
|
},
|
|
15
16
|
"repository": {
|
|
16
17
|
"type": "git",
|
|
@@ -38,4 +39,4 @@
|
|
|
38
39
|
"chalk": "^5.6.2",
|
|
39
40
|
"commander": "^14.0.3"
|
|
40
41
|
}
|
|
41
|
-
}
|
|
42
|
+
}
|
package/src/brand.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import type { Check, Finding } from "../types.js";
|
|
3
|
+
import { getBaseName } from "./utils.js";
|
|
4
|
+
|
|
5
|
+
const binaryExtensions = new Set([
|
|
6
|
+
".zip",
|
|
7
|
+
".exe",
|
|
8
|
+
".dll",
|
|
9
|
+
".mp4",
|
|
10
|
+
".mov",
|
|
11
|
+
".sqlite",
|
|
12
|
+
".db",
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
function hasNullBytes(file: string): boolean {
|
|
16
|
+
const buffer = fs.readFileSync(file);
|
|
17
|
+
const sampleSize = Math.min(buffer.length, 8192);
|
|
18
|
+
|
|
19
|
+
for (let index = 0; index < sampleSize; index += 1) {
|
|
20
|
+
if (buffer[index] === 0) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const binaryFileCheck: Check = {
|
|
29
|
+
name: "binary-file-check",
|
|
30
|
+
|
|
31
|
+
async run(context) {
|
|
32
|
+
const findings: Finding[] = [];
|
|
33
|
+
|
|
34
|
+
for (const file of context.stagedFiles) {
|
|
35
|
+
if (!fs.existsSync(file)) continue;
|
|
36
|
+
|
|
37
|
+
const baseName = getBaseName(file);
|
|
38
|
+
const extension = baseName.includes(".")
|
|
39
|
+
? baseName.slice(baseName.lastIndexOf(".")).toLowerCase()
|
|
40
|
+
: "";
|
|
41
|
+
|
|
42
|
+
let isBinary = binaryExtensions.has(extension);
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
isBinary ||= hasNullBytes(file);
|
|
46
|
+
} catch {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (isBinary) {
|
|
51
|
+
findings.push({
|
|
52
|
+
severity: "warning",
|
|
53
|
+
checkName: this.name,
|
|
54
|
+
file,
|
|
55
|
+
message:
|
|
56
|
+
"Binary file detected — archives, executables, and media don't belong in source control.",
|
|
57
|
+
suggestion: "Remove it from the commit or store it with Git LFS.",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return findings;
|
|
63
|
+
},
|
|
64
|
+
};
|
|
@@ -27,8 +27,9 @@ export const consoleLogCheck: Check = {
|
|
|
27
27
|
checkName: this.name,
|
|
28
28
|
file,
|
|
29
29
|
line: index + 1,
|
|
30
|
-
message:
|
|
31
|
-
|
|
30
|
+
message:
|
|
31
|
+
"Debug log left in code — easy to miss and clutters production output.",
|
|
32
|
+
suggestion: "Delete the console.log before committing.",
|
|
32
33
|
});
|
|
33
34
|
}
|
|
34
35
|
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Check, Finding } from "../types.js";
|
|
2
|
+
import { isCodeFile, readFileLines } from "./utils.js";
|
|
3
|
+
|
|
4
|
+
export const debuggerCheck: Check = {
|
|
5
|
+
name: "debugger-check",
|
|
6
|
+
|
|
7
|
+
async run(context) {
|
|
8
|
+
const findings: Finding[] = [];
|
|
9
|
+
|
|
10
|
+
for (const file of context.stagedFiles) {
|
|
11
|
+
if (!isCodeFile(file)) continue;
|
|
12
|
+
|
|
13
|
+
const lines = readFileLines(file);
|
|
14
|
+
if (!lines) continue;
|
|
15
|
+
|
|
16
|
+
lines.forEach((line, index) => {
|
|
17
|
+
if (/\bdebugger\b/.test(line)) {
|
|
18
|
+
findings.push({
|
|
19
|
+
severity: "warning",
|
|
20
|
+
checkName: this.name,
|
|
21
|
+
file,
|
|
22
|
+
line: index + 1,
|
|
23
|
+
message:
|
|
24
|
+
"debugger statement left in code — pauses execution and breaks CI/headless runs.",
|
|
25
|
+
suggestion: "Delete the debugger statement before committing.",
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return findings;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -14,8 +14,9 @@ export const envFileCheck: Check = {
|
|
|
14
14
|
severity: "error",
|
|
15
15
|
checkName: this.name,
|
|
16
16
|
file,
|
|
17
|
-
message:
|
|
18
|
-
|
|
17
|
+
message:
|
|
18
|
+
".env file detected — these often hold API keys, passwords, and tokens.",
|
|
19
|
+
suggestion: `Unstage it: git restore --staged ${file}`,
|
|
19
20
|
});
|
|
20
21
|
}
|
|
21
22
|
}
|
|
@@ -28,8 +28,8 @@ export const focusedTestCheck: Check = {
|
|
|
28
28
|
checkName: this.name,
|
|
29
29
|
file,
|
|
30
30
|
line: index + 1,
|
|
31
|
-
message: `${pattern}
|
|
32
|
-
suggestion: `
|
|
31
|
+
message: `${pattern} detected — only that test will run, hiding failures in the rest of the suite.`,
|
|
32
|
+
suggestion: `Change ${pattern} back to ${pattern.replace(".only", "")}.`,
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
35
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Check, Finding } from "../types.js";
|
|
2
|
+
import { normalizePath } from "./utils.js";
|
|
2
3
|
|
|
3
4
|
const blockedFolders = [
|
|
4
5
|
"node_modules/",
|
|
@@ -8,6 +9,13 @@ const blockedFolders = [
|
|
|
8
9
|
"coverage/",
|
|
9
10
|
];
|
|
10
11
|
|
|
12
|
+
function matchesBlockedFolder(normalizedPath: string, folder: string): boolean {
|
|
13
|
+
return (
|
|
14
|
+
normalizedPath.startsWith(folder) ||
|
|
15
|
+
normalizedPath.includes(`/${folder}`)
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
11
19
|
export const generatedFolderCheck: Check = {
|
|
12
20
|
name: "generated-folder-check",
|
|
13
21
|
|
|
@@ -15,10 +23,10 @@ export const generatedFolderCheck: Check = {
|
|
|
15
23
|
const findings: Finding[] = [];
|
|
16
24
|
|
|
17
25
|
for (const file of context.stagedFiles) {
|
|
18
|
-
const normalized = file
|
|
26
|
+
const normalized = normalizePath(file);
|
|
19
27
|
|
|
20
28
|
const matchedFolder = blockedFolders.find((folder) =>
|
|
21
|
-
normalized
|
|
29
|
+
matchesBlockedFolder(normalized, folder)
|
|
22
30
|
);
|
|
23
31
|
|
|
24
32
|
if (matchedFolder) {
|
|
@@ -26,12 +34,12 @@ export const generatedFolderCheck: Check = {
|
|
|
26
34
|
severity: "error",
|
|
27
35
|
checkName: this.name,
|
|
28
36
|
file,
|
|
29
|
-
message:
|
|
30
|
-
suggestion: `Add ${matchedFolder} to .gitignore
|
|
37
|
+
message: `Generated folder (${matchedFolder}) — auto-built files bloat the repo and cause merge pain.`,
|
|
38
|
+
suggestion: `Add ${matchedFolder} to .gitignore, then: git restore --staged ${file}`,
|
|
31
39
|
});
|
|
32
40
|
}
|
|
33
41
|
}
|
|
34
42
|
|
|
35
43
|
return findings;
|
|
36
44
|
},
|
|
37
|
-
};
|
|
45
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Check, Finding } from "../types.js";
|
|
2
|
+
import { getBaseName, matchesAnyPattern } from "./utils.js";
|
|
3
|
+
|
|
4
|
+
const junkExactNames = new Set([
|
|
5
|
+
".ds_store",
|
|
6
|
+
"thumbs.db",
|
|
7
|
+
"desktop.ini",
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
const junkNamePatterns = [/\.swp$/i, /\.bak$/i, /\.tmp$/i, /~$/];
|
|
11
|
+
|
|
12
|
+
export const junkFileCheck: Check = {
|
|
13
|
+
name: "junk-file-check",
|
|
14
|
+
|
|
15
|
+
async run(context) {
|
|
16
|
+
const findings: Finding[] = [];
|
|
17
|
+
|
|
18
|
+
for (const file of context.stagedFiles) {
|
|
19
|
+
const baseName = getBaseName(file);
|
|
20
|
+
const normalizedBaseName = baseName.toLowerCase();
|
|
21
|
+
|
|
22
|
+
const isJunk =
|
|
23
|
+
junkExactNames.has(normalizedBaseName) ||
|
|
24
|
+
matchesAnyPattern(baseName, junkNamePatterns);
|
|
25
|
+
|
|
26
|
+
if (isJunk) {
|
|
27
|
+
findings.push({
|
|
28
|
+
severity: "warning",
|
|
29
|
+
checkName: this.name,
|
|
30
|
+
file,
|
|
31
|
+
message:
|
|
32
|
+
"OS or editor junk file — adds noise and has no place in the repo.",
|
|
33
|
+
suggestion: `Unstage it: git restore --staged ${file}`,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return findings;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
@@ -20,8 +20,8 @@ export const largeFileCheck: Check = {
|
|
|
20
20
|
severity: "warning",
|
|
21
21
|
checkName: this.name,
|
|
22
22
|
file,
|
|
23
|
-
message: `Large file
|
|
24
|
-
suggestion: "
|
|
23
|
+
message: `Large file (${sizeMb.toFixed(2)} MB) — slows clones and may hit GitHub's size limits.`,
|
|
24
|
+
suggestion: "Remove it from Git or use Git LFS for big assets.",
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
27
|
}
|
|
@@ -27,8 +27,8 @@ export const localHostCheck: Check = {
|
|
|
27
27
|
checkName: this.name,
|
|
28
28
|
file,
|
|
29
29
|
line: index + 1,
|
|
30
|
-
message: `Hardcoded local URL
|
|
31
|
-
suggestion: "
|
|
30
|
+
message: `Hardcoded local URL (${pattern}) — won't work in production or for teammates.`,
|
|
31
|
+
suggestion: "Move the URL to an environment variable (e.g. process.env.API_URL).",
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
34
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Check, Finding } from "../types.js";
|
|
2
|
+
import { normalizePath } from "./utils.js";
|
|
3
|
+
|
|
4
|
+
function isStaged(stagedFiles: string[], target: string): boolean {
|
|
5
|
+
return stagedFiles.some((file) => normalizePath(file) === target);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const lockfileDriftCheck: Check = {
|
|
9
|
+
name: "lockfile-drift-check",
|
|
10
|
+
|
|
11
|
+
async run(context) {
|
|
12
|
+
const findings: Finding[] = [];
|
|
13
|
+
const packageJsonStaged = isStaged(context.stagedFiles, "package.json");
|
|
14
|
+
const lockfileStaged = isStaged(context.stagedFiles, "package-lock.json");
|
|
15
|
+
|
|
16
|
+
if (packageJsonStaged && !lockfileStaged) {
|
|
17
|
+
findings.push({
|
|
18
|
+
severity: "warning",
|
|
19
|
+
checkName: this.name,
|
|
20
|
+
file: "package.json",
|
|
21
|
+
message:
|
|
22
|
+
"package.json changed without its lockfile — teammates may get different dependency versions.",
|
|
23
|
+
suggestion: "Run npm install, then stage package-lock.json.",
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (lockfileStaged && !packageJsonStaged) {
|
|
28
|
+
findings.push({
|
|
29
|
+
severity: "warning",
|
|
30
|
+
checkName: this.name,
|
|
31
|
+
file: "package-lock.json",
|
|
32
|
+
message:
|
|
33
|
+
"Lockfile changed without package.json — the lockfile may not match your declared dependencies.",
|
|
34
|
+
suggestion: "Stage package.json too, or unstage package-lock.json.",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return findings;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Check, Finding } from "../types.js";
|
|
2
|
+
import { readFileLines, shouldSkipContentScan } from "./utils.js";
|
|
3
|
+
|
|
4
|
+
const conflictMarkers = ["<<<<<<<", "=======", ">>>>>>>"];
|
|
5
|
+
|
|
6
|
+
export const mergeConflictCheck: Check = {
|
|
7
|
+
name: "merge-conflict-check",
|
|
8
|
+
|
|
9
|
+
async run(context) {
|
|
10
|
+
const findings: Finding[] = [];
|
|
11
|
+
|
|
12
|
+
for (const file of context.stagedFiles) {
|
|
13
|
+
if (shouldSkipContentScan(file)) continue;
|
|
14
|
+
|
|
15
|
+
const lines = readFileLines(file);
|
|
16
|
+
if (!lines) continue;
|
|
17
|
+
|
|
18
|
+
lines.forEach((line, index) => {
|
|
19
|
+
const trimmed = line.trim();
|
|
20
|
+
|
|
21
|
+
for (const marker of conflictMarkers) {
|
|
22
|
+
if (trimmed.startsWith(marker)) {
|
|
23
|
+
findings.push({
|
|
24
|
+
severity: "error",
|
|
25
|
+
checkName: this.name,
|
|
26
|
+
file,
|
|
27
|
+
line: index + 1,
|
|
28
|
+
message:
|
|
29
|
+
"Unresolved merge conflict — this file won't run correctly until fixed.",
|
|
30
|
+
suggestion:
|
|
31
|
+
"Resolve the conflict, remove the <<<<<<< / ======= / >>>>>>> markers, then restage.",
|
|
32
|
+
});
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return findings;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -1,14 +1,26 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
1
|
import type { Check, Finding } from "../types.js";
|
|
2
|
+
import {
|
|
3
|
+
isCommentLine,
|
|
4
|
+
isPlaceholderValue,
|
|
5
|
+
readFileLines,
|
|
6
|
+
shouldSkipContentScan,
|
|
7
|
+
} from "./utils.js";
|
|
3
8
|
|
|
4
|
-
const
|
|
5
|
-
/OPENAI_API_KEY\s*=/i,
|
|
6
|
-
/DATABASE_URL\s*=/i,
|
|
7
|
-
/JWT_SECRET\s*=/i,
|
|
8
|
-
/AUTH_SECRET\s*=/i,
|
|
9
|
-
/PRIVATE_KEY\s*=/i,
|
|
10
|
-
/SECRET_KEY\s*=/i,
|
|
11
|
-
/sk-[A-Za-z0-9_-]{20,}/,
|
|
9
|
+
const secretRules: { pattern: RegExp; label: string }[] = [
|
|
10
|
+
{ pattern: /OPENAI_API_KEY\s*=/i, label: "OpenAI API key" },
|
|
11
|
+
{ pattern: /DATABASE_URL\s*=/i, label: "database connection string" },
|
|
12
|
+
{ pattern: /JWT_SECRET\s*=/i, label: "JWT secret" },
|
|
13
|
+
{ pattern: /AUTH_SECRET\s*=/i, label: "auth secret" },
|
|
14
|
+
{ pattern: /PRIVATE_KEY\s*=/i, label: "private key" },
|
|
15
|
+
{ pattern: /SECRET_KEY\s*=/i, label: "secret key" },
|
|
16
|
+
{ pattern: /sk-[A-Za-z0-9_-]{20,}/, label: "API key" },
|
|
17
|
+
{ pattern: /ghp_[A-Za-z0-9]{36,}/, label: "GitHub personal access token" },
|
|
18
|
+
{ pattern: /github_pat_[A-Za-z0-9_]+/, label: "GitHub personal access token" },
|
|
19
|
+
{ pattern: /AKIA[0-9A-Z]{16}/, label: "AWS access key" },
|
|
20
|
+
{ pattern: /sk_live_[A-Za-z0-9]+/, label: "Stripe live secret key" },
|
|
21
|
+
{ pattern: /AIza[0-9A-Za-z_-]{35}/, label: "Google API key" },
|
|
22
|
+
{ pattern: /hooks\.slack\.com\/services\//, label: "Slack webhook URL" },
|
|
23
|
+
{ pattern: /password\s*=\s*['"][^'"]{8,}['"]/i, label: "hardcoded password" },
|
|
12
24
|
];
|
|
13
25
|
|
|
14
26
|
export const secretCheck: Check = {
|
|
@@ -18,22 +30,26 @@ export const secretCheck: Check = {
|
|
|
18
30
|
const findings: Finding[] = [];
|
|
19
31
|
|
|
20
32
|
for (const file of context.stagedFiles) {
|
|
21
|
-
if (
|
|
33
|
+
if (shouldSkipContentScan(file)) continue;
|
|
22
34
|
|
|
23
|
-
const
|
|
24
|
-
|
|
35
|
+
const lines = readFileLines(file);
|
|
36
|
+
if (!lines) continue;
|
|
25
37
|
|
|
26
38
|
lines.forEach((line, index) => {
|
|
27
|
-
|
|
28
|
-
|
|
39
|
+
if (isCommentLine(line) || isPlaceholderValue(line)) return;
|
|
40
|
+
|
|
41
|
+
for (const rule of secretRules) {
|
|
42
|
+
if (rule.pattern.test(line)) {
|
|
29
43
|
findings.push({
|
|
30
44
|
severity: "error",
|
|
31
45
|
checkName: this.name,
|
|
32
46
|
file,
|
|
33
47
|
line: index + 1,
|
|
34
|
-
message:
|
|
35
|
-
suggestion:
|
|
48
|
+
message: `Possible ${rule.label} — credentials pushed to GitHub can be scraped instantly.`,
|
|
49
|
+
suggestion:
|
|
50
|
+
"Remove it from the code, unstage the file, and rotate the credential if it was ever pushed.",
|
|
36
51
|
});
|
|
52
|
+
return;
|
|
37
53
|
}
|
|
38
54
|
}
|
|
39
55
|
});
|
|
@@ -41,4 +57,4 @@ export const secretCheck: Check = {
|
|
|
41
57
|
|
|
42
58
|
return findings;
|
|
43
59
|
},
|
|
44
|
-
};
|
|
60
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Check, Finding } from "../types.js";
|
|
2
|
+
import { getBaseName, matchesAnyPattern, normalizePath } from "./utils.js";
|
|
3
|
+
|
|
4
|
+
const sensitiveExactNames = new Set([
|
|
5
|
+
"id_rsa",
|
|
6
|
+
"id_ed25519",
|
|
7
|
+
"credentials.json",
|
|
8
|
+
"serviceaccountkey.json",
|
|
9
|
+
".npmrc",
|
|
10
|
+
".pypirc",
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
const sensitiveNamePatterns = [
|
|
14
|
+
/\.pem$/i,
|
|
15
|
+
/\.p12$/i,
|
|
16
|
+
/\.key$/i,
|
|
17
|
+
/^firebase-adminsdk.*\.json$/i,
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export const sensitiveFilenameCheck: Check = {
|
|
21
|
+
name: "sensitive-filename-check",
|
|
22
|
+
|
|
23
|
+
async run(context) {
|
|
24
|
+
const findings: Finding[] = [];
|
|
25
|
+
|
|
26
|
+
for (const file of context.stagedFiles) {
|
|
27
|
+
const baseName = getBaseName(file);
|
|
28
|
+
const normalizedBaseName = baseName.toLowerCase();
|
|
29
|
+
const normalizedPath = normalizePath(file).toLowerCase();
|
|
30
|
+
|
|
31
|
+
const isSensitive =
|
|
32
|
+
sensitiveExactNames.has(normalizedBaseName) ||
|
|
33
|
+
matchesAnyPattern(baseName, sensitiveNamePatterns) ||
|
|
34
|
+
normalizedPath.endsWith("/credentials.json") ||
|
|
35
|
+
normalizedPath.includes("serviceaccountkey.json");
|
|
36
|
+
|
|
37
|
+
if (isSensitive) {
|
|
38
|
+
findings.push({
|
|
39
|
+
severity: "error",
|
|
40
|
+
checkName: this.name,
|
|
41
|
+
file,
|
|
42
|
+
message:
|
|
43
|
+
"Credential or key file detected — private keys and auth config should not be in Git.",
|
|
44
|
+
suggestion: `Unstage it: git restore --staged ${file}`,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return findings;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
|
|
3
|
+
const CODE_EXTENSIONS = [".js", ".jsx", ".ts", ".tsx"];
|
|
4
|
+
|
|
5
|
+
const SKIP_CONTENT_EXTENSIONS = [".md", ".example", ".sample"];
|
|
6
|
+
|
|
7
|
+
const PLACEHOLDER_PATTERNS = [
|
|
8
|
+
/your-api-key/i,
|
|
9
|
+
/changeme/i,
|
|
10
|
+
/xxx+/i,
|
|
11
|
+
/TODO/i,
|
|
12
|
+
/placeholder/i,
|
|
13
|
+
/example/i,
|
|
14
|
+
/replace-me/i,
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export function normalizePath(file: string): string {
|
|
18
|
+
return file.replaceAll("\\", "/");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getBaseName(file: string): string {
|
|
22
|
+
return normalizePath(file).split("/").pop() ?? "";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isCodeFile(file: string): boolean {
|
|
26
|
+
return CODE_EXTENSIONS.some((ext) => file.endsWith(ext));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function shouldSkipContentScan(file: string): boolean {
|
|
30
|
+
return SKIP_CONTENT_EXTENSIONS.some((ext) => file.endsWith(ext));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function readFileLines(file: string): string[] | null {
|
|
34
|
+
if (!fs.existsSync(file)) return null;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
return fs.readFileSync(file, "utf-8").split("\n");
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function isCommentLine(line: string): boolean {
|
|
44
|
+
const trimmed = line.trim();
|
|
45
|
+
return (
|
|
46
|
+
trimmed.startsWith("//") ||
|
|
47
|
+
trimmed.startsWith("#") ||
|
|
48
|
+
trimmed.startsWith("*") ||
|
|
49
|
+
trimmed.startsWith("/*")
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isPlaceholderValue(line: string): boolean {
|
|
54
|
+
return PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(line));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function matchesAnyPattern(
|
|
58
|
+
value: string,
|
|
59
|
+
patterns: RegExp[]
|
|
60
|
+
): boolean {
|
|
61
|
+
return patterns.some((pattern) => pattern.test(value));
|
|
62
|
+
}
|