commit-cop 1.0.0 → 1.0.1
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 +31 -15
- 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 +4 -4
- package/src/checks/binaryFileCheck.ts +64 -0
- package/src/checks/debuggerCheck.ts +32 -0
- package/src/checks/generatedFolderCheck.ts +11 -3
- package/src/checks/junkFileCheck.ts +39 -0
- package/src/checks/lockfileDriftCheck.ts +38 -0
- package/src/checks/mergeConflictCheck.ts +40 -0
- package/src/checks/secretCheck.ts +22 -6
- package/src/checks/sensitiveFilenameCheck.ts +50 -0
- package/src/checks/utils.ts +62 -0
- package/src/index.ts +1 -1
- package/src/scanner.ts +15 -3
package/README.md
CHANGED
|
@@ -1,36 +1,52 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Commit Cop
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Commit Cop is a pre-commit safety checker that scans your **staged files** and warns you about common mistakes before they get pushed to GitHub.
|
|
4
4
|
|
|
5
5
|
Built for students, hackathons, and dev teams who want practical guardrails—not just formatting checks.
|
|
6
6
|
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install commit-cop
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Run it on staged changes:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx commit-cop
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Treat warnings as errors:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx commit-cop --strict
|
|
23
|
+
```
|
|
24
|
+
|
|
7
25
|
## What it checks
|
|
8
26
|
|
|
9
27
|
| Check | Severity | What it catches |
|
|
10
28
|
| --- | --- | --- |
|
|
29
|
+
| Merge conflicts | Error | `<<<<<<<`, `=======`, `>>>>>>>` markers left in code |
|
|
11
30
|
| Environment files | Error | `.env`, `.env.local`, and other `.env.*` files |
|
|
12
|
-
|
|
|
13
|
-
|
|
|
31
|
+
| Sensitive filenames | Error | Keys, certs, `credentials.json`, `.npmrc`, and similar |
|
|
32
|
+
| Generated folders | Error | `node_modules/`, `dist/`, `build/`, `.next/`, `coverage/` (including nested paths) |
|
|
33
|
+
| Secrets | Error | API keys, GitHub/AWS/Stripe tokens, JWT secrets, database URLs |
|
|
14
34
|
| Focused tests | Error | `test.only`, `it.only`, `describe.only` left in test files |
|
|
15
35
|
| Debug logs | Warning | `console.log` in staged JS/TS code |
|
|
36
|
+
| Debugger statements | Warning | `debugger` in staged JS/TS code |
|
|
16
37
|
| Localhost URLs | Warning | Hardcoded `localhost` or `127.0.0.1` URLs |
|
|
38
|
+
| Junk files | Warning | `.DS_Store`, `Thumbs.db`, swap/backup files |
|
|
39
|
+
| Lockfile drift | Warning | `package.json` staged without `package-lock.json` (or vice versa) |
|
|
17
40
|
| Large files | Warning | Staged files over 5 MB |
|
|
41
|
+
| Binary files | Warning | `.zip`, `.exe`, `.mp4`, and other non-text files |
|
|
18
42
|
|
|
19
43
|
Errors block the commit. Warnings are reported but do not block unless you use `--strict`.
|
|
20
44
|
|
|
21
|
-
##
|
|
22
|
-
|
|
23
|
-
Run inside a Git repository with staged changes:
|
|
45
|
+
## Local development
|
|
24
46
|
|
|
25
47
|
```bash
|
|
26
48
|
npm install
|
|
27
|
-
npm run
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
Treat warnings as errors:
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
npm run commitclean -- --strict
|
|
49
|
+
npm run commit-cop
|
|
34
50
|
```
|
|
35
51
|
|
|
36
52
|
Build and run the compiled CLI:
|
|
@@ -59,4 +75,4 @@ Each check implements the same interface: receive staged files, return findings
|
|
|
59
75
|
1. Read staged file paths with `git diff --cached --name-only`
|
|
60
76
|
2. Run every check in `src/checks/`
|
|
61
77
|
3. Print a summary with file locations and fix suggestions
|
|
62
|
-
4. Exit with code `1` if errors are found (or warnings in strict mode)
|
|
78
|
+
4. Exit with code `1` if errors are found (or warnings in strict mode)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"binaryFileCheck.d.ts","sourceRoot":"","sources":["../../src/checks/binaryFileCheck.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAW,MAAM,aAAa,CAAC;AA0BlD,eAAO,MAAM,eAAe,EAAE,KAoC7B,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { getBaseName } from "./utils.js";
|
|
3
|
+
const binaryExtensions = new Set([
|
|
4
|
+
".zip",
|
|
5
|
+
".exe",
|
|
6
|
+
".dll",
|
|
7
|
+
".mp4",
|
|
8
|
+
".mov",
|
|
9
|
+
".sqlite",
|
|
10
|
+
".db",
|
|
11
|
+
]);
|
|
12
|
+
function hasNullBytes(file) {
|
|
13
|
+
const buffer = fs.readFileSync(file);
|
|
14
|
+
const sampleSize = Math.min(buffer.length, 8192);
|
|
15
|
+
for (let index = 0; index < sampleSize; index += 1) {
|
|
16
|
+
if (buffer[index] === 0) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
export const binaryFileCheck = {
|
|
23
|
+
name: "binary-file-check",
|
|
24
|
+
async run(context) {
|
|
25
|
+
const findings = [];
|
|
26
|
+
for (const file of context.stagedFiles) {
|
|
27
|
+
if (!fs.existsSync(file))
|
|
28
|
+
continue;
|
|
29
|
+
const baseName = getBaseName(file);
|
|
30
|
+
const extension = baseName.includes(".")
|
|
31
|
+
? baseName.slice(baseName.lastIndexOf(".")).toLowerCase()
|
|
32
|
+
: "";
|
|
33
|
+
let isBinary = binaryExtensions.has(extension);
|
|
34
|
+
try {
|
|
35
|
+
isBinary ||= hasNullBytes(file);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (isBinary) {
|
|
41
|
+
findings.push({
|
|
42
|
+
severity: "warning",
|
|
43
|
+
checkName: this.name,
|
|
44
|
+
file,
|
|
45
|
+
message: "Binary or non-text file is staged.",
|
|
46
|
+
suggestion: "Remove it from the commit or store it outside Git (e.g. Git LFS).",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return findings;
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
//# sourceMappingURL=binaryFileCheck.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"binaryFileCheck.js","sourceRoot":"","sources":["../../src/checks/binaryFileCheck.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAEzB,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEzC,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC;IAC/B,MAAM;IACN,MAAM;IACN,MAAM;IACN,MAAM;IACN,MAAM;IACN,SAAS;IACT,KAAK;CACN,CAAC,CAAC;AAEH,SAAS,YAAY,CAAC,IAAY;IAChC,MAAM,MAAM,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IACrC,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAEjD,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,UAAU,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QACnD,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;YACxB,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,CAAC,MAAM,eAAe,GAAU;IACpC,IAAI,EAAE,mBAAmB;IAEzB,KAAK,CAAC,GAAG,CAAC,OAAO;QACf,MAAM,QAAQ,GAAc,EAAE,CAAC;QAE/B,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;YACvC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC;gBAAE,SAAS;YAEnC,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;YACnC,MAAM,SAAS,GAAG,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC;gBACtC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE;gBACzD,CAAC,CAAC,EAAE,CAAC;YAEP,IAAI,QAAQ,GAAG,gBAAgB,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YAE/C,IAAI,CAAC;gBACH,QAAQ,KAAK,YAAY,CAAC,IAAI,CAAC,CAAC;YAClC,CAAC;YAAC,MAAM,CAAC;gBACP,SAAS;YACX,CAAC;YAED,IAAI,QAAQ,EAAE,CAAC;gBACb,QAAQ,CAAC,IAAI,CAAC;oBACZ,QAAQ,EAAE,SAAS;oBACnB,SAAS,EAAE,IAAI,CAAC,IAAI;oBACpB,IAAI;oBACJ,OAAO,EAAE,oCAAoC;oBAC7C,UAAU,EACR,mEAAmE;iBACtE,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"debuggerCheck.d.ts","sourceRoot":"","sources":["../../src/checks/debuggerCheck.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAW,MAAM,aAAa,CAAC;AAGlD,eAAO,MAAM,aAAa,EAAE,KA4B3B,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { isCodeFile, readFileLines } from "./utils.js";
|
|
2
|
+
export const debuggerCheck = {
|
|
3
|
+
name: "debugger-check",
|
|
4
|
+
async run(context) {
|
|
5
|
+
const findings = [];
|
|
6
|
+
for (const file of context.stagedFiles) {
|
|
7
|
+
if (!isCodeFile(file))
|
|
8
|
+
continue;
|
|
9
|
+
const lines = readFileLines(file);
|
|
10
|
+
if (!lines)
|
|
11
|
+
continue;
|
|
12
|
+
lines.forEach((line, index) => {
|
|
13
|
+
if (/\bdebugger\b/.test(line)) {
|
|
14
|
+
findings.push({
|
|
15
|
+
severity: "warning",
|
|
16
|
+
checkName: this.name,
|
|
17
|
+
file,
|
|
18
|
+
line: index + 1,
|
|
19
|
+
message: "debugger statement found in staged code.",
|
|
20
|
+
suggestion: "Remove debugger statements before committing.",
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return findings;
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=debuggerCheck.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"debuggerCheck.js","sourceRoot":"","sources":["../../src/checks/debuggerCheck.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEvD,MAAM,CAAC,MAAM,aAAa,GAAU;IAClC,IAAI,EAAE,gBAAgB;IAEtB,KAAK,CAAC,GAAG,CAAC,OAAO;QACf,MAAM,QAAQ,GAAc,EAAE,CAAC;QAE/B,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;YACvC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;gBAAE,SAAS;YAEhC,MAAM,KAAK,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;YAClC,IAAI,CAAC,KAAK;gBAAE,SAAS;YAErB,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;gBAC5B,IAAI,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;oBAC9B,QAAQ,CAAC,IAAI,CAAC;wBACZ,QAAQ,EAAE,SAAS;wBACnB,SAAS,EAAE,IAAI,CAAC,IAAI;wBACpB,IAAI;wBACJ,IAAI,EAAE,KAAK,GAAG,CAAC;wBACf,OAAO,EAAE,0CAA0C;wBACnD,UAAU,EAAE,+CAA+C;qBAC5D,CAAC,CAAC;gBACL,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generatedFolderCheck.d.ts","sourceRoot":"","sources":["../../src/checks/generatedFolderCheck.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAW,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"generatedFolderCheck.d.ts","sourceRoot":"","sources":["../../src/checks/generatedFolderCheck.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAW,MAAM,aAAa,CAAC;AAkBlD,eAAO,MAAM,oBAAoB,EAAE,KA0BlC,CAAC"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizePath } from "./utils.js";
|
|
1
2
|
const blockedFolders = [
|
|
2
3
|
"node_modules/",
|
|
3
4
|
"dist/",
|
|
@@ -5,13 +6,17 @@ const blockedFolders = [
|
|
|
5
6
|
".next/",
|
|
6
7
|
"coverage/",
|
|
7
8
|
];
|
|
9
|
+
function matchesBlockedFolder(normalizedPath, folder) {
|
|
10
|
+
return (normalizedPath.startsWith(folder) ||
|
|
11
|
+
normalizedPath.includes(`/${folder}`));
|
|
12
|
+
}
|
|
8
13
|
export const generatedFolderCheck = {
|
|
9
14
|
name: "generated-folder-check",
|
|
10
15
|
async run(context) {
|
|
11
16
|
const findings = [];
|
|
12
17
|
for (const file of context.stagedFiles) {
|
|
13
|
-
const normalized = file
|
|
14
|
-
const matchedFolder = blockedFolders.find((folder) => normalized
|
|
18
|
+
const normalized = normalizePath(file);
|
|
19
|
+
const matchedFolder = blockedFolders.find((folder) => matchesBlockedFolder(normalized, folder));
|
|
15
20
|
if (matchedFolder) {
|
|
16
21
|
findings.push({
|
|
17
22
|
severity: "error",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generatedFolderCheck.js","sourceRoot":"","sources":["../../src/checks/generatedFolderCheck.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"generatedFolderCheck.js","sourceRoot":"","sources":["../../src/checks/generatedFolderCheck.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C,MAAM,cAAc,GAAG;IACrB,eAAe;IACf,OAAO;IACP,QAAQ;IACR,QAAQ;IACR,WAAW;CACZ,CAAC;AAEF,SAAS,oBAAoB,CAAC,cAAsB,EAAE,MAAc;IAClE,OAAO,CACL,cAAc,CAAC,UAAU,CAAC,MAAM,CAAC;QACjC,cAAc,CAAC,QAAQ,CAAC,IAAI,MAAM,EAAE,CAAC,CACtC,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,oBAAoB,GAAU;IACzC,IAAI,EAAE,wBAAwB;IAE9B,KAAK,CAAC,GAAG,CAAC,OAAO;QACf,MAAM,QAAQ,GAAc,EAAE,CAAC;QAE/B,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;YACvC,MAAM,UAAU,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;YAEvC,MAAM,aAAa,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CACnD,oBAAoB,CAAC,UAAU,EAAE,MAAM,CAAC,CACzC,CAAC;YAEF,IAAI,aAAa,EAAE,CAAC;gBAClB,QAAQ,CAAC,IAAI,CAAC;oBACZ,QAAQ,EAAE,OAAO;oBACjB,SAAS,EAAE,IAAI,CAAC,IAAI;oBACpB,IAAI;oBACJ,OAAO,EAAE,GAAG,aAAa,kEAAkE;oBAC3F,UAAU,EAAE,OAAO,aAAa,gDAAgD,IAAI,EAAE;iBACvF,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"junkFileCheck.d.ts","sourceRoot":"","sources":["../../src/checks/junkFileCheck.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAW,MAAM,aAAa,CAAC;AAWlD,eAAO,MAAM,aAAa,EAAE,KA2B3B,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { getBaseName, matchesAnyPattern } from "./utils.js";
|
|
2
|
+
const junkExactNames = new Set([
|
|
3
|
+
".ds_store",
|
|
4
|
+
"thumbs.db",
|
|
5
|
+
"desktop.ini",
|
|
6
|
+
]);
|
|
7
|
+
const junkNamePatterns = [/\.swp$/i, /\.bak$/i, /\.tmp$/i, /~$/];
|
|
8
|
+
export const junkFileCheck = {
|
|
9
|
+
name: "junk-file-check",
|
|
10
|
+
async run(context) {
|
|
11
|
+
const findings = [];
|
|
12
|
+
for (const file of context.stagedFiles) {
|
|
13
|
+
const baseName = getBaseName(file);
|
|
14
|
+
const normalizedBaseName = baseName.toLowerCase();
|
|
15
|
+
const isJunk = junkExactNames.has(normalizedBaseName) ||
|
|
16
|
+
matchesAnyPattern(baseName, junkNamePatterns);
|
|
17
|
+
if (isJunk) {
|
|
18
|
+
findings.push({
|
|
19
|
+
severity: "warning",
|
|
20
|
+
checkName: this.name,
|
|
21
|
+
file,
|
|
22
|
+
message: "OS or editor junk file is staged.",
|
|
23
|
+
suggestion: `Run: git restore --staged ${file}`,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return findings;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=junkFileCheck.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"junkFileCheck.js","sourceRoot":"","sources":["../../src/checks/junkFileCheck.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAE5D,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC;IAC7B,WAAW;IACX,WAAW;IACX,aAAa;CACd,CAAC,CAAC;AAEH,MAAM,gBAAgB,GAAG,CAAC,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;AAEjE,MAAM,CAAC,MAAM,aAAa,GAAU;IAClC,IAAI,EAAE,iBAAiB;IAEvB,KAAK,CAAC,GAAG,CAAC,OAAO;QACf,MAAM,QAAQ,GAAc,EAAE,CAAC;QAE/B,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;YACvC,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;YACnC,MAAM,kBAAkB,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC;YAElD,MAAM,MAAM,GACV,cAAc,CAAC,GAAG,CAAC,kBAAkB,CAAC;gBACtC,iBAAiB,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC;YAEhD,IAAI,MAAM,EAAE,CAAC;gBACX,QAAQ,CAAC,IAAI,CAAC;oBACZ,QAAQ,EAAE,SAAS;oBACnB,SAAS,EAAE,IAAI,CAAC,IAAI;oBACpB,IAAI;oBACJ,OAAO,EAAE,mCAAmC;oBAC5C,UAAU,EAAE,6BAA6B,IAAI,EAAE;iBAChD,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lockfileDriftCheck.d.ts","sourceRoot":"","sources":["../../src/checks/lockfileDriftCheck.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAW,MAAM,aAAa,CAAC;AAOlD,eAAO,MAAM,kBAAkB,EAAE,KA8BhC,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { normalizePath } from "./utils.js";
|
|
2
|
+
function isStaged(stagedFiles, target) {
|
|
3
|
+
return stagedFiles.some((file) => normalizePath(file) === target);
|
|
4
|
+
}
|
|
5
|
+
export const lockfileDriftCheck = {
|
|
6
|
+
name: "lockfile-drift-check",
|
|
7
|
+
async run(context) {
|
|
8
|
+
const findings = [];
|
|
9
|
+
const packageJsonStaged = isStaged(context.stagedFiles, "package.json");
|
|
10
|
+
const lockfileStaged = isStaged(context.stagedFiles, "package-lock.json");
|
|
11
|
+
if (packageJsonStaged && !lockfileStaged) {
|
|
12
|
+
findings.push({
|
|
13
|
+
severity: "warning",
|
|
14
|
+
checkName: this.name,
|
|
15
|
+
file: "package.json",
|
|
16
|
+
message: "package.json is staged but package-lock.json is not.",
|
|
17
|
+
suggestion: "Run npm install and stage package-lock.json.",
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
if (lockfileStaged && !packageJsonStaged) {
|
|
21
|
+
findings.push({
|
|
22
|
+
severity: "warning",
|
|
23
|
+
checkName: this.name,
|
|
24
|
+
file: "package-lock.json",
|
|
25
|
+
message: "package-lock.json is staged but package.json is not.",
|
|
26
|
+
suggestion: "Stage package.json or unstage the lockfile.",
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return findings;
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=lockfileDriftCheck.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lockfileDriftCheck.js","sourceRoot":"","sources":["../../src/checks/lockfileDriftCheck.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C,SAAS,QAAQ,CAAC,WAAqB,EAAE,MAAc;IACrD,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,MAAM,CAAC,CAAC;AACpE,CAAC;AAED,MAAM,CAAC,MAAM,kBAAkB,GAAU;IACvC,IAAI,EAAE,sBAAsB;IAE5B,KAAK,CAAC,GAAG,CAAC,OAAO;QACf,MAAM,QAAQ,GAAc,EAAE,CAAC;QAC/B,MAAM,iBAAiB,GAAG,QAAQ,CAAC,OAAO,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC;QACxE,MAAM,cAAc,GAAG,QAAQ,CAAC,OAAO,CAAC,WAAW,EAAE,mBAAmB,CAAC,CAAC;QAE1E,IAAI,iBAAiB,IAAI,CAAC,cAAc,EAAE,CAAC;YACzC,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,SAAS;gBACnB,SAAS,EAAE,IAAI,CAAC,IAAI;gBACpB,IAAI,EAAE,cAAc;gBACpB,OAAO,EAAE,sDAAsD;gBAC/D,UAAU,EAAE,8CAA8C;aAC3D,CAAC,CAAC;QACL,CAAC;QAED,IAAI,cAAc,IAAI,CAAC,iBAAiB,EAAE,CAAC;YACzC,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,SAAS;gBACnB,SAAS,EAAE,IAAI,CAAC,IAAI;gBACpB,IAAI,EAAE,mBAAmB;gBACzB,OAAO,EAAE,sDAAsD;gBAC/D,UAAU,EAAE,6CAA6C;aAC1D,CAAC,CAAC;QACL,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mergeConflictCheck.d.ts","sourceRoot":"","sources":["../../src/checks/mergeConflictCheck.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAW,MAAM,aAAa,CAAC;AAKlD,eAAO,MAAM,kBAAkB,EAAE,KAkChC,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readFileLines, shouldSkipContentScan } from "./utils.js";
|
|
2
|
+
const conflictMarkers = ["<<<<<<<", "=======", ">>>>>>>"];
|
|
3
|
+
export const mergeConflictCheck = {
|
|
4
|
+
name: "merge-conflict-check",
|
|
5
|
+
async run(context) {
|
|
6
|
+
const findings = [];
|
|
7
|
+
for (const file of context.stagedFiles) {
|
|
8
|
+
if (shouldSkipContentScan(file))
|
|
9
|
+
continue;
|
|
10
|
+
const lines = readFileLines(file);
|
|
11
|
+
if (!lines)
|
|
12
|
+
continue;
|
|
13
|
+
lines.forEach((line, index) => {
|
|
14
|
+
const trimmed = line.trim();
|
|
15
|
+
for (const marker of conflictMarkers) {
|
|
16
|
+
if (trimmed.startsWith(marker)) {
|
|
17
|
+
findings.push({
|
|
18
|
+
severity: "error",
|
|
19
|
+
checkName: this.name,
|
|
20
|
+
file,
|
|
21
|
+
line: index + 1,
|
|
22
|
+
message: "Merge conflict marker found in staged file.",
|
|
23
|
+
suggestion: "Resolve the conflict, remove the markers, and restage the file.",
|
|
24
|
+
});
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return findings;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=mergeConflictCheck.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mergeConflictCheck.js","sourceRoot":"","sources":["../../src/checks/mergeConflictCheck.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAElE,MAAM,eAAe,GAAG,CAAC,SAAS,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC;AAE1D,MAAM,CAAC,MAAM,kBAAkB,GAAU;IACvC,IAAI,EAAE,sBAAsB;IAE5B,KAAK,CAAC,GAAG,CAAC,OAAO;QACf,MAAM,QAAQ,GAAc,EAAE,CAAC;QAE/B,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;YACvC,IAAI,qBAAqB,CAAC,IAAI,CAAC;gBAAE,SAAS;YAE1C,MAAM,KAAK,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;YAClC,IAAI,CAAC,KAAK;gBAAE,SAAS;YAErB,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;gBAC5B,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;gBAE5B,KAAK,MAAM,MAAM,IAAI,eAAe,EAAE,CAAC;oBACrC,IAAI,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;wBAC/B,QAAQ,CAAC,IAAI,CAAC;4BACZ,QAAQ,EAAE,OAAO;4BACjB,SAAS,EAAE,IAAI,CAAC,IAAI;4BACpB,IAAI;4BACJ,IAAI,EAAE,KAAK,GAAG,CAAC;4BACf,OAAO,EAAE,6CAA6C;4BACtD,UAAU,EACR,iEAAiE;yBACpE,CAAC,CAAC;wBACH,OAAO;oBACT,CAAC;gBACH,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secretCheck.d.ts","sourceRoot":"","sources":["../../src/checks/secretCheck.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"secretCheck.d.ts","sourceRoot":"","sources":["../../src/checks/secretCheck.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAW,MAAM,aAAa,CAAC;AAyBlD,eAAO,MAAM,WAAW,EAAE,KAkCzB,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { isCommentLine, isPlaceholderValue, readFileLines, shouldSkipContentScan, } from "./utils.js";
|
|
2
2
|
const secretPatterns = [
|
|
3
3
|
/OPENAI_API_KEY\s*=/i,
|
|
4
4
|
/DATABASE_URL\s*=/i,
|
|
@@ -7,17 +7,27 @@ const secretPatterns = [
|
|
|
7
7
|
/PRIVATE_KEY\s*=/i,
|
|
8
8
|
/SECRET_KEY\s*=/i,
|
|
9
9
|
/sk-[A-Za-z0-9_-]{20,}/,
|
|
10
|
+
/ghp_[A-Za-z0-9]{36,}/,
|
|
11
|
+
/github_pat_[A-Za-z0-9_]+/,
|
|
12
|
+
/AKIA[0-9A-Z]{16}/,
|
|
13
|
+
/sk_live_[A-Za-z0-9]+/,
|
|
14
|
+
/AIza[0-9A-Za-z_-]{35}/,
|
|
15
|
+
/hooks\.slack\.com\/services\//,
|
|
16
|
+
/password\s*=\s*['"][^'"]{8,}['"]/i,
|
|
10
17
|
];
|
|
11
18
|
export const secretCheck = {
|
|
12
19
|
name: "secret-check",
|
|
13
20
|
async run(context) {
|
|
14
21
|
const findings = [];
|
|
15
22
|
for (const file of context.stagedFiles) {
|
|
16
|
-
if (
|
|
23
|
+
if (shouldSkipContentScan(file))
|
|
24
|
+
continue;
|
|
25
|
+
const lines = readFileLines(file);
|
|
26
|
+
if (!lines)
|
|
17
27
|
continue;
|
|
18
|
-
const content = fs.readFileSync(file, "utf-8");
|
|
19
|
-
const lines = content.split("\n");
|
|
20
28
|
lines.forEach((line, index) => {
|
|
29
|
+
if (isCommentLine(line) || isPlaceholderValue(line))
|
|
30
|
+
return;
|
|
21
31
|
for (const pattern of secretPatterns) {
|
|
22
32
|
if (pattern.test(line)) {
|
|
23
33
|
findings.push({
|
|
@@ -28,6 +38,7 @@ export const secretCheck = {
|
|
|
28
38
|
message: "Possible secret found in staged file.",
|
|
29
39
|
suggestion: "Remove the secret and rotate it if it was already pushed.",
|
|
30
40
|
});
|
|
41
|
+
return;
|
|
31
42
|
}
|
|
32
43
|
}
|
|
33
44
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secretCheck.js","sourceRoot":"","sources":["../../src/checks/secretCheck.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"secretCheck.js","sourceRoot":"","sources":["../../src/checks/secretCheck.ts"],"names":[],"mappings":"AACA,OAAO,EACL,aAAa,EACb,kBAAkB,EAClB,aAAa,EACb,qBAAqB,GACtB,MAAM,YAAY,CAAC;AAEpB,MAAM,cAAc,GAAG;IACrB,qBAAqB;IACrB,mBAAmB;IACnB,iBAAiB;IACjB,kBAAkB;IAClB,kBAAkB;IAClB,iBAAiB;IACjB,uBAAuB;IACvB,sBAAsB;IACtB,0BAA0B;IAC1B,kBAAkB;IAClB,sBAAsB;IACtB,uBAAuB;IACvB,+BAA+B;IAC/B,mCAAmC;CACpC,CAAC;AAEF,MAAM,CAAC,MAAM,WAAW,GAAU;IAChC,IAAI,EAAE,cAAc;IAEpB,KAAK,CAAC,GAAG,CAAC,OAAO;QACf,MAAM,QAAQ,GAAc,EAAE,CAAC;QAE/B,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;YACvC,IAAI,qBAAqB,CAAC,IAAI,CAAC;gBAAE,SAAS;YAE1C,MAAM,KAAK,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;YAClC,IAAI,CAAC,KAAK;gBAAE,SAAS;YAErB,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;gBAC5B,IAAI,aAAa,CAAC,IAAI,CAAC,IAAI,kBAAkB,CAAC,IAAI,CAAC;oBAAE,OAAO;gBAE5D,KAAK,MAAM,OAAO,IAAI,cAAc,EAAE,CAAC;oBACrC,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;wBACvB,QAAQ,CAAC,IAAI,CAAC;4BACZ,QAAQ,EAAE,OAAO;4BACjB,SAAS,EAAE,IAAI,CAAC,IAAI;4BACpB,IAAI;4BACJ,IAAI,EAAE,KAAK,GAAG,CAAC;4BACf,OAAO,EAAE,uCAAuC;4BAChD,UAAU,EACR,2DAA2D;yBAC9D,CAAC,CAAC;wBACH,OAAO;oBACT,CAAC;gBACH,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sensitiveFilenameCheck.d.ts","sourceRoot":"","sources":["../../src/checks/sensitiveFilenameCheck.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAW,MAAM,aAAa,CAAC;AAmBlD,eAAO,MAAM,sBAAsB,EAAE,KA8BpC,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { getBaseName, matchesAnyPattern, normalizePath } from "./utils.js";
|
|
2
|
+
const sensitiveExactNames = new Set([
|
|
3
|
+
"id_rsa",
|
|
4
|
+
"id_ed25519",
|
|
5
|
+
"credentials.json",
|
|
6
|
+
"serviceaccountkey.json",
|
|
7
|
+
".npmrc",
|
|
8
|
+
".pypirc",
|
|
9
|
+
]);
|
|
10
|
+
const sensitiveNamePatterns = [
|
|
11
|
+
/\.pem$/i,
|
|
12
|
+
/\.p12$/i,
|
|
13
|
+
/\.key$/i,
|
|
14
|
+
/^firebase-adminsdk.*\.json$/i,
|
|
15
|
+
];
|
|
16
|
+
export const sensitiveFilenameCheck = {
|
|
17
|
+
name: "sensitive-filename-check",
|
|
18
|
+
async run(context) {
|
|
19
|
+
const findings = [];
|
|
20
|
+
for (const file of context.stagedFiles) {
|
|
21
|
+
const baseName = getBaseName(file);
|
|
22
|
+
const normalizedBaseName = baseName.toLowerCase();
|
|
23
|
+
const normalizedPath = normalizePath(file).toLowerCase();
|
|
24
|
+
const isSensitive = sensitiveExactNames.has(normalizedBaseName) ||
|
|
25
|
+
matchesAnyPattern(baseName, sensitiveNamePatterns) ||
|
|
26
|
+
normalizedPath.endsWith("/credentials.json") ||
|
|
27
|
+
normalizedPath.includes("serviceaccountkey.json");
|
|
28
|
+
if (isSensitive) {
|
|
29
|
+
findings.push({
|
|
30
|
+
severity: "error",
|
|
31
|
+
checkName: this.name,
|
|
32
|
+
file,
|
|
33
|
+
message: "Sensitive credential file is staged.",
|
|
34
|
+
suggestion: `Run: git restore --staged ${file}`,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return findings;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=sensitiveFilenameCheck.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sensitiveFilenameCheck.js","sourceRoot":"","sources":["../../src/checks/sensitiveFilenameCheck.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3E,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAC;IAClC,QAAQ;IACR,YAAY;IACZ,kBAAkB;IAClB,wBAAwB;IACxB,QAAQ;IACR,SAAS;CACV,CAAC,CAAC;AAEH,MAAM,qBAAqB,GAAG;IAC5B,SAAS;IACT,SAAS;IACT,SAAS;IACT,8BAA8B;CAC/B,CAAC;AAEF,MAAM,CAAC,MAAM,sBAAsB,GAAU;IAC3C,IAAI,EAAE,0BAA0B;IAEhC,KAAK,CAAC,GAAG,CAAC,OAAO;QACf,MAAM,QAAQ,GAAc,EAAE,CAAC;QAE/B,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;YACvC,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;YACnC,MAAM,kBAAkB,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC;YAClD,MAAM,cAAc,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;YAEzD,MAAM,WAAW,GACf,mBAAmB,CAAC,GAAG,CAAC,kBAAkB,CAAC;gBAC3C,iBAAiB,CAAC,QAAQ,EAAE,qBAAqB,CAAC;gBAClD,cAAc,CAAC,QAAQ,CAAC,mBAAmB,CAAC;gBAC5C,cAAc,CAAC,QAAQ,CAAC,wBAAwB,CAAC,CAAC;YAEpD,IAAI,WAAW,EAAE,CAAC;gBAChB,QAAQ,CAAC,IAAI,CAAC;oBACZ,QAAQ,EAAE,OAAO;oBACjB,SAAS,EAAE,IAAI,CAAC,IAAI;oBACpB,IAAI;oBACJ,OAAO,EAAE,sCAAsC;oBAC/C,UAAU,EAAE,6BAA6B,IAAI,EAAE;iBAChD,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function normalizePath(file: string): string;
|
|
2
|
+
export declare function getBaseName(file: string): string;
|
|
3
|
+
export declare function isCodeFile(file: string): boolean;
|
|
4
|
+
export declare function shouldSkipContentScan(file: string): boolean;
|
|
5
|
+
export declare function readFileLines(file: string): string[] | null;
|
|
6
|
+
export declare function isCommentLine(line: string): boolean;
|
|
7
|
+
export declare function isPlaceholderValue(line: string): boolean;
|
|
8
|
+
export declare function matchesAnyPattern(value: string, patterns: RegExp[]): boolean;
|
|
9
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/checks/utils.ts"],"names":[],"mappings":"AAgBA,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEhD;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAQ3D;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAQnD;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAExD;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAAE,GACjB,OAAO,CAET"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
const CODE_EXTENSIONS = [".js", ".jsx", ".ts", ".tsx"];
|
|
3
|
+
const SKIP_CONTENT_EXTENSIONS = [".md", ".example", ".sample"];
|
|
4
|
+
const PLACEHOLDER_PATTERNS = [
|
|
5
|
+
/your-api-key/i,
|
|
6
|
+
/changeme/i,
|
|
7
|
+
/xxx+/i,
|
|
8
|
+
/TODO/i,
|
|
9
|
+
/placeholder/i,
|
|
10
|
+
/example/i,
|
|
11
|
+
/replace-me/i,
|
|
12
|
+
];
|
|
13
|
+
export function normalizePath(file) {
|
|
14
|
+
return file.replaceAll("\\", "/");
|
|
15
|
+
}
|
|
16
|
+
export function getBaseName(file) {
|
|
17
|
+
return normalizePath(file).split("/").pop() ?? "";
|
|
18
|
+
}
|
|
19
|
+
export function isCodeFile(file) {
|
|
20
|
+
return CODE_EXTENSIONS.some((ext) => file.endsWith(ext));
|
|
21
|
+
}
|
|
22
|
+
export function shouldSkipContentScan(file) {
|
|
23
|
+
return SKIP_CONTENT_EXTENSIONS.some((ext) => file.endsWith(ext));
|
|
24
|
+
}
|
|
25
|
+
export function readFileLines(file) {
|
|
26
|
+
if (!fs.existsSync(file))
|
|
27
|
+
return null;
|
|
28
|
+
try {
|
|
29
|
+
return fs.readFileSync(file, "utf-8").split("\n");
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function isCommentLine(line) {
|
|
36
|
+
const trimmed = line.trim();
|
|
37
|
+
return (trimmed.startsWith("//") ||
|
|
38
|
+
trimmed.startsWith("#") ||
|
|
39
|
+
trimmed.startsWith("*") ||
|
|
40
|
+
trimmed.startsWith("/*"));
|
|
41
|
+
}
|
|
42
|
+
export function isPlaceholderValue(line) {
|
|
43
|
+
return PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(line));
|
|
44
|
+
}
|
|
45
|
+
export function matchesAnyPattern(value, patterns) {
|
|
46
|
+
return patterns.some((pattern) => pattern.test(value));
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/checks/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAEzB,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;AAEvD,MAAM,uBAAuB,GAAG,CAAC,KAAK,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC;AAE/D,MAAM,oBAAoB,GAAG;IAC3B,eAAe;IACf,WAAW;IACX,OAAO;IACP,OAAO;IACP,cAAc;IACd,UAAU;IACV,aAAa;CACd,CAAC;AAEF,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAO,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,aAAa,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC;AACpD,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,eAAe,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,IAAY;IAChD,OAAO,uBAAuB,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;AACnE,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAEtC,IAAI,CAAC;QACH,OAAO,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAC5B,OAAO,CACL,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC;QACxB,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;QACvB,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;QACvB,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,CACzB,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,OAAO,oBAAoB,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AACpE,CAAC;AAED,MAAM,UAAU,iBAAiB,CAC/B,KAAa,EACb,QAAkB;IAElB,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;AACzD,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { runChecks } from "./scanner.js";
|
|
|
5
5
|
import { printReport } from "./reporter.js";
|
|
6
6
|
const program = new Command();
|
|
7
7
|
program
|
|
8
|
-
.name("
|
|
8
|
+
.name("commit-cop")
|
|
9
9
|
.description("Pre-commit safety checker for staged files")
|
|
10
10
|
.option("--strict", "Treat warnings as errors")
|
|
11
11
|
.action(async (options) => {
|
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,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "commit-cop",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "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
14
|
},
|
|
15
15
|
"repository": {
|
|
16
16
|
"type": "git",
|
|
@@ -38,4 +38,4 @@
|
|
|
38
38
|
"chalk": "^5.6.2",
|
|
39
39
|
"commander": "^14.0.3"
|
|
40
40
|
}
|
|
41
|
-
}
|
|
41
|
+
}
|
|
@@ -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: "Binary or non-text file is staged.",
|
|
56
|
+
suggestion:
|
|
57
|
+
"Remove it from the commit or store it outside Git (e.g. Git LFS).",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return findings;
|
|
63
|
+
},
|
|
64
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
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: "debugger statement found in staged code.",
|
|
24
|
+
suggestion: "Remove debugger statements before committing.",
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return findings;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
@@ -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) {
|
|
@@ -34,4 +42,4 @@ export const generatedFolderCheck: Check = {
|
|
|
34
42
|
|
|
35
43
|
return findings;
|
|
36
44
|
},
|
|
37
|
-
};
|
|
45
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
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: "OS or editor junk file is staged.",
|
|
32
|
+
suggestion: `Run: git restore --staged ${file}`,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return findings;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
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: "package.json is staged but package-lock.json is not.",
|
|
22
|
+
suggestion: "Run npm install and stage package-lock.json.",
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (lockfileStaged && !packageJsonStaged) {
|
|
27
|
+
findings.push({
|
|
28
|
+
severity: "warning",
|
|
29
|
+
checkName: this.name,
|
|
30
|
+
file: "package-lock.json",
|
|
31
|
+
message: "package-lock.json is staged but package.json is not.",
|
|
32
|
+
suggestion: "Stage package.json or unstage the lockfile.",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return findings;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
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: "Merge conflict marker found in staged file.",
|
|
29
|
+
suggestion:
|
|
30
|
+
"Resolve the conflict, remove the markers, and restage the file.",
|
|
31
|
+
});
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return findings;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
@@ -1,5 +1,10 @@
|
|
|
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
9
|
const secretPatterns = [
|
|
5
10
|
/OPENAI_API_KEY\s*=/i,
|
|
@@ -9,6 +14,13 @@ const secretPatterns = [
|
|
|
9
14
|
/PRIVATE_KEY\s*=/i,
|
|
10
15
|
/SECRET_KEY\s*=/i,
|
|
11
16
|
/sk-[A-Za-z0-9_-]{20,}/,
|
|
17
|
+
/ghp_[A-Za-z0-9]{36,}/,
|
|
18
|
+
/github_pat_[A-Za-z0-9_]+/,
|
|
19
|
+
/AKIA[0-9A-Z]{16}/,
|
|
20
|
+
/sk_live_[A-Za-z0-9]+/,
|
|
21
|
+
/AIza[0-9A-Za-z_-]{35}/,
|
|
22
|
+
/hooks\.slack\.com\/services\//,
|
|
23
|
+
/password\s*=\s*['"][^'"]{8,}['"]/i,
|
|
12
24
|
];
|
|
13
25
|
|
|
14
26
|
export const secretCheck: Check = {
|
|
@@ -18,12 +30,14 @@ 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) => {
|
|
39
|
+
if (isCommentLine(line) || isPlaceholderValue(line)) return;
|
|
40
|
+
|
|
27
41
|
for (const pattern of secretPatterns) {
|
|
28
42
|
if (pattern.test(line)) {
|
|
29
43
|
findings.push({
|
|
@@ -32,8 +46,10 @@ export const secretCheck: Check = {
|
|
|
32
46
|
file,
|
|
33
47
|
line: index + 1,
|
|
34
48
|
message: "Possible secret found in staged file.",
|
|
35
|
-
suggestion:
|
|
49
|
+
suggestion:
|
|
50
|
+
"Remove the secret and rotate it if it was already 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,50 @@
|
|
|
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: "Sensitive credential file is staged.",
|
|
43
|
+
suggestion: `Run: git restore --staged ${file}`,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return findings;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
@@ -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
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { printReport } from "./reporter.js";
|
|
|
8
8
|
const program = new Command();
|
|
9
9
|
|
|
10
10
|
program
|
|
11
|
-
.name("
|
|
11
|
+
.name("commit-cop")
|
|
12
12
|
.description("Pre-commit safety checker for staged files")
|
|
13
13
|
.option("--strict", "Treat warnings as errors")
|
|
14
14
|
.action(async (options) => {
|
package/src/scanner.ts
CHANGED
|
@@ -1,20 +1,32 @@
|
|
|
1
1
|
import type { CheckContext, Finding } from "./types.js";
|
|
2
2
|
import { envFileCheck } from "./checks/envFileCheck.js";
|
|
3
3
|
import { generatedFolderCheck } from "./checks/generatedFolderCheck.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { mergeConflictCheck } from "./checks/mergeConflictCheck.ts";
|
|
5
|
+
import { sensitiveFilenameCheck } from "./checks/sensitiveFilenameCheck.ts";
|
|
6
|
+
import { secretCheck } from "./checks/secretCheck.ts";
|
|
5
7
|
import { focusedTestCheck } from "./checks/focusedTestCheck.ts";
|
|
8
|
+
import { consoleLogCheck } from "./checks/consoleLogCheck.ts";
|
|
9
|
+
import { debuggerCheck } from "./checks/debuggerCheck.ts";
|
|
6
10
|
import { localHostCheck } from "./checks/localHostCheck.ts";
|
|
11
|
+
import { junkFileCheck } from "./checks/junkFileCheck.ts";
|
|
12
|
+
import { lockfileDriftCheck } from "./checks/lockfileDriftCheck.ts";
|
|
7
13
|
import { largeFileCheck } from "./checks/largeFileCheck.ts";
|
|
8
|
-
import {
|
|
14
|
+
import { binaryFileCheck } from "./checks/binaryFileCheck.ts";
|
|
9
15
|
|
|
10
16
|
const checks = [
|
|
17
|
+
mergeConflictCheck,
|
|
11
18
|
envFileCheck,
|
|
19
|
+
sensitiveFilenameCheck,
|
|
12
20
|
generatedFolderCheck,
|
|
13
21
|
secretCheck,
|
|
14
22
|
focusedTestCheck,
|
|
15
23
|
consoleLogCheck,
|
|
24
|
+
debuggerCheck,
|
|
16
25
|
localHostCheck,
|
|
26
|
+
junkFileCheck,
|
|
27
|
+
lockfileDriftCheck,
|
|
17
28
|
largeFileCheck,
|
|
29
|
+
binaryFileCheck,
|
|
18
30
|
];
|
|
19
31
|
|
|
20
32
|
export async function runChecks(context: CheckContext): Promise<Finding[]> {
|
|
@@ -26,4 +38,4 @@ export async function runChecks(context: CheckContext): Promise<Finding[]> {
|
|
|
26
38
|
}
|
|
27
39
|
|
|
28
40
|
return allFindings;
|
|
29
|
-
}
|
|
41
|
+
}
|