pr-checkmate 1.21.5 → 1.21.7
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 +6 -2
- package/cspell.json +7 -0
- package/dist/config/loader.js +17 -0
- package/dist/scripts/commitlint.js +110 -0
- package/dist/scripts/diff-security.js +199 -0
- package/dist/scripts/diff-smell.js +4 -1
- package/dist/scripts/duplicate-check.js +3 -2
- package/dist/scripts/index.js +19 -1
- package/dist/scripts/lint.js +6 -7
- package/dist/scripts/markdown-lint.js +29 -10
- package/dist/scripts/pr-title-lint.js +3 -3
- package/dist/scripts/prettier-autoformat.js +4 -3
- package/dist/scripts/security.js +12 -17
- package/dist/scripts/sensitive-file-guard.js +145 -0
- package/dist/scripts/spellcheck.js +8 -3
- package/dist/scripts/typecheck.js +2 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/resolve-bin.js +46 -0
- package/gitleaks.toml +10 -0
- package/package.json +4 -5
- package/src/config/cspell.json +7 -0
package/README.md
CHANGED
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
* ✅ Diff code smells — scans PR diff for `console.*()` calls, `debugger` statements, and `TODO/FIXME/HACK/XXX` comments left in added lines.
|
|
27
27
|
* ✅ PR description check — validates the PR body has at least 20 characters and includes a ticket reference (`Closes #123` or `PROJ-456` style).
|
|
28
28
|
* ✅ Duplicate code detection — uses `jscpd` in delta mode (changed files in PR, or `src/` otherwise) with a 5-line / 50-token minimum threshold.
|
|
29
|
+
* ✅ Diff security scan — detects XSS vectors, `eval()`/`new Function()`, prototype pollution, weak crypto, CORS wildcards, insecure HTTP calls, command injection, and SQL injection patterns in added PR lines (warning-only, delta mode).
|
|
30
|
+
* ✅ Sensitive file guard — warns when the PR touches `.env` files, private keys/certificates, CI/CD pipeline configs, auth middleware, infrastructure files, or lockfiles (warning-only, PR context only).
|
|
29
31
|
* ✅ PR summary comment — after `npx pr-checkmate all`, posts a summary table to the CI log and upserts a comment on the PR. No spam on repeated pushes.
|
|
30
32
|
|
|
31
33
|
<br>
|
|
@@ -120,7 +122,7 @@ npx pr-checkmate <job>
|
|
|
120
122
|
|
|
121
123
|
| Job | Description |
|
|
122
124
|
|----------------|-------------|
|
|
123
|
-
| `all` | Run all checks in parallel (lint, deps, audit, security, circular, license, then spellcheck, pr-size, pr-title, outdated, markdown, typecheck, diff-smell, pr-body, duplicate, then prettier). Posts a PR summary comment when run in a GitHub Actions PR context. |
|
|
125
|
+
| `all` | Run all checks in parallel (lint, deps, audit, security, circular, license, then spellcheck, pr-size, pr-title, outdated, markdown, typecheck, diff-smell, pr-body, duplicate, diff-security, sensitive-files, then prettier). Posts a PR summary comment when run in a GitHub Actions PR context. |
|
|
124
126
|
| `lint` | Lint code using ESLint |
|
|
125
127
|
| `prettier` | Format code using Prettier |
|
|
126
128
|
| `deps` | Check unused / missing dependencies via depcheck |
|
|
@@ -137,12 +139,14 @@ npx pr-checkmate <job>
|
|
|
137
139
|
| `diff-smell` | Scan PR diff for `console.*()`, `debugger`, and `TODO/FIXME/HACK/XXX` in added lines (warning-only, delta mode) |
|
|
138
140
|
| `pr-body` | Check PR description for minimum length (20 chars) and a ticket reference (warning-only) |
|
|
139
141
|
| `duplicate` | Detect duplicate code with `jscpd` — min 5 lines / 50 tokens (warning-only, delta mode in PR context) |
|
|
142
|
+
| `diff-security` | Scan PR diff for XSS vectors, `eval()`/`new Function()`, prototype pollution, weak crypto (MD5/SHA-1), CORS wildcards, insecure HTTP, command injection, and SQL injection in added lines (warning-only, delta mode, PR context only) |
|
|
143
|
+
| `sensitive-files` | Detect PR changes to `.env` files, private keys/certificates, CI/CD configs, auth middleware, infrastructure files, and lockfiles (warning-only, PR context only) |
|
|
140
144
|
|
|
141
145
|
<br>
|
|
142
146
|
|
|
143
147
|
### ⚡ Delta Mode
|
|
144
148
|
|
|
145
|
-
In a GitHub Actions PR context, `lint`, `prettier`, `spellcheck`, `markdown`, `diff-smell`, and `
|
|
149
|
+
In a GitHub Actions PR context, `lint`, `prettier`, `spellcheck`, `markdown`, `diff-smell`, `duplicate`, and `diff-security` automatically switch to **delta mode** — only the files changed in the PR are checked instead of the entire project. This is transparent and requires no configuration.
|
|
146
150
|
|
|
147
151
|
Outside of a PR context (local runs, push workflows) the checks fall back to the full project scan.
|
|
148
152
|
|
package/cspell.json
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
"./node_modules/@cspell/dict-es-es/cspell-ext.json"
|
|
10
10
|
],
|
|
11
11
|
"words": [
|
|
12
|
+
"ACMR",
|
|
13
|
+
"ACMRD",
|
|
12
14
|
"accona",
|
|
13
15
|
"Accona",
|
|
14
16
|
"acepto",
|
|
@@ -41,6 +43,7 @@
|
|
|
41
43
|
"Catrice",
|
|
42
44
|
"Cerrar",
|
|
43
45
|
"checkmate",
|
|
46
|
+
"circleci",
|
|
44
47
|
"coincidan",
|
|
45
48
|
"como",
|
|
46
49
|
"comprobante",
|
|
@@ -85,6 +88,7 @@
|
|
|
85
88
|
"gastos",
|
|
86
89
|
"Gastos",
|
|
87
90
|
"Gestión",
|
|
91
|
+
"gitlab",
|
|
88
92
|
"gitleaks",
|
|
89
93
|
"givatayim",
|
|
90
94
|
"Givatayim",
|
|
@@ -113,9 +117,11 @@
|
|
|
113
117
|
"israel",
|
|
114
118
|
"jerusalem",
|
|
115
119
|
"kfar",
|
|
120
|
+
"kubeconfig",
|
|
116
121
|
"Kfar",
|
|
117
122
|
"Kolomiets",
|
|
118
123
|
"leído",
|
|
124
|
+
"lockfiles",
|
|
119
125
|
"manera",
|
|
120
126
|
"mavox",
|
|
121
127
|
"Mavox",
|
|
@@ -182,6 +188,7 @@
|
|
|
182
188
|
"sistema",
|
|
183
189
|
"sultansadikov",
|
|
184
190
|
"telaviv",
|
|
191
|
+
"travis",
|
|
185
192
|
"Términos",
|
|
186
193
|
"Teudat",
|
|
187
194
|
"Tiempo",
|
package/dist/config/loader.js
CHANGED
|
@@ -58,6 +58,7 @@ function mergeWithDefaults(userConfig) {
|
|
|
58
58
|
diffSmell: { ...defaults.diffSmell, ...userConfig.diffSmell },
|
|
59
59
|
prBody: { ...defaults.prBody, ...userConfig.prBody },
|
|
60
60
|
duplicate: { ...defaults.duplicate, ...userConfig.duplicate },
|
|
61
|
+
commitlint: { ...defaults.commitlint, ...userConfig.commitlint },
|
|
61
62
|
};
|
|
62
63
|
}
|
|
63
64
|
/**
|
|
@@ -118,6 +119,22 @@ function getDefaultConfig() {
|
|
|
118
119
|
diffSmell: { enabled: true },
|
|
119
120
|
prBody: { enabled: true, minLength: 20, requireTicket: true },
|
|
120
121
|
duplicate: { enabled: true, minLines: 5, minTokens: 50 },
|
|
122
|
+
commitlint: {
|
|
123
|
+
enabled: true,
|
|
124
|
+
types: [
|
|
125
|
+
'feat',
|
|
126
|
+
'fix',
|
|
127
|
+
'docs',
|
|
128
|
+
'chore',
|
|
129
|
+
'ci',
|
|
130
|
+
'refactor',
|
|
131
|
+
'perf',
|
|
132
|
+
'style',
|
|
133
|
+
'revert',
|
|
134
|
+
'test',
|
|
135
|
+
'build',
|
|
136
|
+
],
|
|
137
|
+
},
|
|
121
138
|
};
|
|
122
139
|
}
|
|
123
140
|
/**
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runCommitlint = runCommitlint;
|
|
7
|
+
const execa_1 = require("execa");
|
|
8
|
+
const utils_1 = require("../utils");
|
|
9
|
+
const loader_1 = require("../config/loader");
|
|
10
|
+
const config_1 = require("../config");
|
|
11
|
+
const result_collector_1 = require("../utils/result-collector");
|
|
12
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
13
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
14
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
15
|
+
const LOCAL_CONFIG_FILES = [
|
|
16
|
+
'commitlint.config.cjs',
|
|
17
|
+
'commitlint.config.js',
|
|
18
|
+
'commitlint.config.mjs',
|
|
19
|
+
'commitlint.config.ts',
|
|
20
|
+
'.commitlintrc',
|
|
21
|
+
'.commitlintrc.json',
|
|
22
|
+
'.commitlintrc.yaml',
|
|
23
|
+
'.commitlintrc.yml',
|
|
24
|
+
'.commitlintrc.js',
|
|
25
|
+
'.commitlintrc.cjs',
|
|
26
|
+
];
|
|
27
|
+
const DEFAULT_TYPES = [
|
|
28
|
+
'feat',
|
|
29
|
+
'fix',
|
|
30
|
+
'docs',
|
|
31
|
+
'chore',
|
|
32
|
+
'ci',
|
|
33
|
+
'refactor',
|
|
34
|
+
'perf',
|
|
35
|
+
'style',
|
|
36
|
+
'revert',
|
|
37
|
+
'test',
|
|
38
|
+
'build',
|
|
39
|
+
];
|
|
40
|
+
function buildTempConfig(types) {
|
|
41
|
+
const config = {
|
|
42
|
+
extends: [(0, utils_1.resolvePackageBin)('@commitlint/config-conventional', 'index.js')],
|
|
43
|
+
rules: {
|
|
44
|
+
'type-enum': [2, 'always', types],
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
const tmpFile = node_path_1.default.join(node_os_1.default.tmpdir(), `.commitlint-${process.pid}.json`);
|
|
48
|
+
node_fs_1.default.writeFileSync(tmpFile, JSON.stringify(config, null, 2));
|
|
49
|
+
return tmpFile;
|
|
50
|
+
}
|
|
51
|
+
async function getCommitRange() {
|
|
52
|
+
const eventName = process.env.GITHUB_EVENT_NAME;
|
|
53
|
+
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
54
|
+
if (eventName === 'pull_request' && eventPath && node_fs_1.default.existsSync(eventPath)) {
|
|
55
|
+
try {
|
|
56
|
+
const event = JSON.parse(node_fs_1.default.readFileSync(eventPath, 'utf-8'));
|
|
57
|
+
const baseSha = event?.pull_request?.base?.sha;
|
|
58
|
+
const headSha = event?.pull_request?.head?.sha;
|
|
59
|
+
if (baseSha && headSha) {
|
|
60
|
+
return { from: baseSha, to: headSha };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
utils_1.logger.warn(`[runCommitlint]: ⚠️ Could not read GitHub event: ${err}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { from: 'HEAD~1', to: 'HEAD' };
|
|
68
|
+
}
|
|
69
|
+
async function runCommitlint() {
|
|
70
|
+
const cfg = (0, loader_1.loadProjectConfig)();
|
|
71
|
+
if (cfg.commitlint?.enabled === false) {
|
|
72
|
+
utils_1.logger.info('[runCommitlint]: Disabled in pr-checkmate.json, skipping');
|
|
73
|
+
result_collector_1.resultCollector.register('Commitlint', 'skip');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
utils_1.logger.info('[runCommitlint]: 📝 Running commitlint...');
|
|
77
|
+
const commitlintBin = (0, utils_1.resolvePackageBin)('@commitlint/cli', 'cli.js');
|
|
78
|
+
const hasLocal = (0, config_1.hasLocalConfig)(LOCAL_CONFIG_FILES);
|
|
79
|
+
let tempConfigFile = null;
|
|
80
|
+
const { from, to } = await getCommitRange();
|
|
81
|
+
const args = ['--from', from, '--to', to, '--verbose'];
|
|
82
|
+
try {
|
|
83
|
+
if (hasLocal) {
|
|
84
|
+
utils_1.logger.info('[runCommitlint]: Using local commitlint config');
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
const types = cfg.commitlint?.types ?? DEFAULT_TYPES;
|
|
88
|
+
tempConfigFile = buildTempConfig(types);
|
|
89
|
+
utils_1.logger.info(`[runCommitlint]: Using pr-checkmate commitlint config (types: ${types.join(', ')})`);
|
|
90
|
+
args.push('--config', tempConfigFile);
|
|
91
|
+
}
|
|
92
|
+
const result = await (0, execa_1.execa)(process.execPath, [commitlintBin, ...args], {
|
|
93
|
+
reject: false,
|
|
94
|
+
stdio: 'inherit',
|
|
95
|
+
});
|
|
96
|
+
if (result.exitCode === 0) {
|
|
97
|
+
utils_1.logger.info('[runCommitlint]: ✅ All commits follow the convention');
|
|
98
|
+
result_collector_1.resultCollector.register('Commitlint', 'pass');
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
utils_1.logger.warn('[runCommitlint]: ⚠️ Some commits do not follow the convention (see above)');
|
|
102
|
+
result_collector_1.resultCollector.register('Commitlint', 'warn', 'commit message violations found');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
if (tempConfigFile && node_fs_1.default.existsSync(tempConfigFile)) {
|
|
107
|
+
node_fs_1.default.unlinkSync(tempConfigFile);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runDiffSecurity = runDiffSecurity;
|
|
7
|
+
const execa_1 = require("execa");
|
|
8
|
+
const utils_1 = require("../utils");
|
|
9
|
+
const result_collector_1 = require("../utils/result-collector");
|
|
10
|
+
const loader_1 = require("../config/loader");
|
|
11
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
|
+
const SECURITY_PATTERNS = [
|
|
13
|
+
// XSS vectors
|
|
14
|
+
{
|
|
15
|
+
pattern: /^\+(?!\+).*dangerouslySetInnerHTML\s*=/,
|
|
16
|
+
label: 'dangerouslySetInnerHTML (XSS risk)',
|
|
17
|
+
severity: 'error',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
pattern: /^\+(?!\+).*\.innerHTML\s*=/,
|
|
21
|
+
label: 'innerHTML assignment (XSS risk)',
|
|
22
|
+
severity: 'warn',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
pattern: /^\+(?!\+).*document\.write\s*\(/,
|
|
26
|
+
label: 'document.write() (XSS risk)',
|
|
27
|
+
severity: 'error',
|
|
28
|
+
},
|
|
29
|
+
// Code injection
|
|
30
|
+
{
|
|
31
|
+
pattern: /^\+(?!\+)(?![^'"`]*['"`]).*\beval\s*\(/,
|
|
32
|
+
label: 'eval() (code injection risk)',
|
|
33
|
+
severity: 'error',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
pattern: /^\+(?!\+).*\bnew\s+Function\s*\(/,
|
|
37
|
+
label: 'new Function() (code injection risk)',
|
|
38
|
+
severity: 'error',
|
|
39
|
+
},
|
|
40
|
+
// Prototype pollution
|
|
41
|
+
{
|
|
42
|
+
pattern: /^\+(?!\+).*__proto__\s*[=:[]/,
|
|
43
|
+
label: '__proto__ mutation (prototype pollution)',
|
|
44
|
+
severity: 'error',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
pattern: /^\+(?!\+).*\.constructor\.prototype\b/,
|
|
48
|
+
label: 'constructor.prototype access (prototype pollution)',
|
|
49
|
+
severity: 'warn',
|
|
50
|
+
},
|
|
51
|
+
// Weak crypto (Node.js built-ins)
|
|
52
|
+
{
|
|
53
|
+
pattern: /^\+(?!\+).*createHash\s*\(\s*['"]md5['"]/,
|
|
54
|
+
label: 'MD5 (weak hash — use sha256 or stronger)',
|
|
55
|
+
severity: 'warn',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
pattern: /^\+(?!\+).*createHash\s*\(\s*['"]sha1['"]/,
|
|
59
|
+
label: 'SHA-1 (weak hash — use sha256 or stronger)',
|
|
60
|
+
severity: 'warn',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
pattern: /^\+(?!\+).*\bcreateDecipher\s*\(/,
|
|
64
|
+
label: 'deprecated createDecipher() — use createDecipheriv()',
|
|
65
|
+
severity: 'warn',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
pattern: /^\+(?!\+).*\bcreateCipher\s*\(/,
|
|
69
|
+
label: 'deprecated createCipher() — use createCipheriv()',
|
|
70
|
+
severity: 'warn',
|
|
71
|
+
},
|
|
72
|
+
// CORS misconfiguration
|
|
73
|
+
{
|
|
74
|
+
pattern: /^\+(?!\+).*cors\s*\(\s*\{[^}]*origin\s*:\s*['"]\*['"]/,
|
|
75
|
+
label: "CORS wildcard origin: '*' (allows any domain)",
|
|
76
|
+
severity: 'warn',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
pattern: /^\+(?!\+).*['"]Access-Control-Allow-Origin['"]\s*[,:]\s*['"]\*['"]/,
|
|
80
|
+
label: "Access-Control-Allow-Origin: '*' header (CORS wildcard)",
|
|
81
|
+
severity: 'warn',
|
|
82
|
+
},
|
|
83
|
+
// Insecure HTTP (non-HTTPS external calls)
|
|
84
|
+
{
|
|
85
|
+
pattern: /^\+(?!\+).*(fetch|axios\.get|axios\.post|axios\.put|axios\.delete|http\.get|http\.request)\s*\(\s*['"]http:\/\//,
|
|
86
|
+
label: 'Insecure HTTP (non-HTTPS) in external call',
|
|
87
|
+
severity: 'warn',
|
|
88
|
+
},
|
|
89
|
+
// Command injection risk — exec/spawn with string concatenation
|
|
90
|
+
{
|
|
91
|
+
pattern: /^\+(?!\+).*(exec|execSync|spawn|spawnSync)\s*\(\s*`[^`]*\$\{/,
|
|
92
|
+
label: 'exec/spawn with template literal (command injection risk)',
|
|
93
|
+
severity: 'error',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
pattern: /^\+(?!\+).*(exec|execSync)\s*\(\s*['"][^'"]*['"\s]*\+/,
|
|
97
|
+
label: 'exec/spawn with string concatenation (command injection risk)',
|
|
98
|
+
severity: 'error',
|
|
99
|
+
},
|
|
100
|
+
// SQL injection — template literals in query strings
|
|
101
|
+
{
|
|
102
|
+
pattern: /^\+(?!\+).*\b(query|execute|raw)\s*\(\s*`[^`]*\$\{/,
|
|
103
|
+
label: 'SQL query with template literal interpolation (SQL injection risk)',
|
|
104
|
+
severity: 'error',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
pattern: /^\+(?!\+).*(SELECT|INSERT|UPDATE|DELETE|WHERE)[^'"]*['"\s]*\+\s*\w/i,
|
|
108
|
+
label: 'SQL string concatenation (SQL injection risk)',
|
|
109
|
+
severity: 'error',
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
function readBaseSha() {
|
|
113
|
+
const eventName = process.env.GITHUB_EVENT_NAME;
|
|
114
|
+
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
115
|
+
if (eventName !== 'pull_request' || !eventPath || !node_fs_1.default.existsSync(eventPath)) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const event = JSON.parse(node_fs_1.default.readFileSync(eventPath, 'utf-8'));
|
|
120
|
+
return event?.pull_request?.base?.sha ?? null;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function runDiffSecurity() {
|
|
127
|
+
const cfg = (0, loader_1.loadProjectConfig)();
|
|
128
|
+
if (cfg.diffSecurity?.enabled === false) {
|
|
129
|
+
utils_1.logger.info('[runDiffSecurity]: Disabled in pr-checkmate.json, skipping');
|
|
130
|
+
result_collector_1.resultCollector.register('Diff Security', 'skip');
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
utils_1.logger.info('[runDiffSecurity]: Scanning diff for security issues...');
|
|
134
|
+
const baseSha = readBaseSha();
|
|
135
|
+
if (!baseSha) {
|
|
136
|
+
utils_1.logger.info('[runDiffSecurity]: Not in GitHub PR context, skipping');
|
|
137
|
+
result_collector_1.resultCollector.register('Diff Security', 'skip');
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const { stdout } = await (0, execa_1.execa)('git', [
|
|
142
|
+
'diff',
|
|
143
|
+
'--unified=0',
|
|
144
|
+
'--diff-filter=ACMR',
|
|
145
|
+
baseSha,
|
|
146
|
+
'HEAD',
|
|
147
|
+
'--',
|
|
148
|
+
'*.ts',
|
|
149
|
+
'*.tsx',
|
|
150
|
+
'*.js',
|
|
151
|
+
'*.jsx',
|
|
152
|
+
'*.mjs',
|
|
153
|
+
'*.cjs',
|
|
154
|
+
]);
|
|
155
|
+
const lines = stdout.split('\n');
|
|
156
|
+
const errors = {};
|
|
157
|
+
const warnings = {};
|
|
158
|
+
for (const line of lines) {
|
|
159
|
+
for (const { pattern, label, severity } of SECURITY_PATTERNS) {
|
|
160
|
+
if (pattern.test(line)) {
|
|
161
|
+
const bucket = severity === 'error' ? errors : warnings;
|
|
162
|
+
if (!bucket[label])
|
|
163
|
+
bucket[label] = [];
|
|
164
|
+
bucket[label].push(line.slice(1).trim().substring(0, 120));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const totalErrors = Object.values(errors).reduce((s, a) => s + a.length, 0);
|
|
169
|
+
const totalWarnings = Object.values(warnings).reduce((s, a) => s + a.length, 0);
|
|
170
|
+
if (totalErrors === 0 && totalWarnings === 0) {
|
|
171
|
+
utils_1.logger.info('[runDiffSecurity]: ✅ No security issues detected in diff');
|
|
172
|
+
result_collector_1.resultCollector.register('Diff Security', 'pass');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
for (const [label, occurrences] of Object.entries(errors)) {
|
|
176
|
+
utils_1.logger.error(`[runDiffSecurity]: ❌ ${label}: ${occurrences.length} occurrence(s)`);
|
|
177
|
+
occurrences.slice(0, 3).forEach(line => utils_1.logger.error(` ${line}`));
|
|
178
|
+
if (occurrences.length > 3)
|
|
179
|
+
utils_1.logger.error(` ... and ${occurrences.length - 3} more`);
|
|
180
|
+
}
|
|
181
|
+
for (const [label, occurrences] of Object.entries(warnings)) {
|
|
182
|
+
utils_1.logger.warn(`[runDiffSecurity]: ⚠️ ${label}: ${occurrences.length} occurrence(s)`);
|
|
183
|
+
occurrences.slice(0, 3).forEach(line => utils_1.logger.warn(` ${line}`));
|
|
184
|
+
if (occurrences.length > 3)
|
|
185
|
+
utils_1.logger.warn(` ... and ${occurrences.length - 3} more`);
|
|
186
|
+
}
|
|
187
|
+
const parts = [];
|
|
188
|
+
if (totalErrors > 0)
|
|
189
|
+
parts.push(`${totalErrors} issue(s)`);
|
|
190
|
+
if (totalWarnings > 0)
|
|
191
|
+
parts.push(`${totalWarnings} warning(s)`);
|
|
192
|
+
const summary = parts.join(', ');
|
|
193
|
+
result_collector_1.resultCollector.register('Diff Security', 'warn', summary);
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
utils_1.logger.warn(`[runDiffSecurity]: ⚠️ Could not analyze diff\n${err}`);
|
|
197
|
+
result_collector_1.resultCollector.register('Diff Security', 'skip');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -11,7 +11,10 @@ const loader_1 = require("../config/loader");
|
|
|
11
11
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
12
|
const SMELL_PATTERNS = [
|
|
13
13
|
// console.* calls added in diff (not in comments)
|
|
14
|
-
{
|
|
14
|
+
{
|
|
15
|
+
pattern: /^\+(?!\+)(?:[^/]|\/[^/]).*\bconsole\.(log|error|warn|debug|info)\s*\(/,
|
|
16
|
+
label: 'console.*()',
|
|
17
|
+
},
|
|
15
18
|
{ pattern: /^\+(?!\+).*\bdebugger\b/, label: 'debugger statement' },
|
|
16
19
|
{ pattern: /^\+\s*\/\/\s*TODO\b/i, label: 'TODO comment' },
|
|
17
20
|
{ pattern: /^\+\s*\/\/\s*FIXME\b/i, label: 'FIXME comment' },
|
|
@@ -32,9 +32,10 @@ async function runDuplicateCheck() {
|
|
|
32
32
|
// Fall back to src/ or project root when not in delta mode
|
|
33
33
|
const srcPath = node_path_1.default.join(projectRoot, 'src');
|
|
34
34
|
const targetPaths = changedFiles ?? (node_fs_1.default.existsSync(srcPath) ? [srcPath] : [projectRoot]);
|
|
35
|
+
const jscpdBin = (0, utils_1.resolvePackageBin)('jscpd', 'bin/jscpd');
|
|
35
36
|
try {
|
|
36
|
-
const result = await (0, execa_1.execa)(
|
|
37
|
-
|
|
37
|
+
const result = await (0, execa_1.execa)(process.execPath, [
|
|
38
|
+
jscpdBin,
|
|
38
39
|
'--min-lines',
|
|
39
40
|
String(minLines),
|
|
40
41
|
'--min-tokens',
|
package/dist/scripts/index.js
CHANGED
|
@@ -30,6 +30,9 @@ const typecheck_1 = require("./typecheck");
|
|
|
30
30
|
const diff_smell_1 = require("./diff-smell");
|
|
31
31
|
const pr_body_1 = require("./pr-body");
|
|
32
32
|
const duplicate_check_1 = require("./duplicate-check");
|
|
33
|
+
const commitlint_1 = require("./commitlint");
|
|
34
|
+
const diff_security_1 = require("./diff-security");
|
|
35
|
+
const sensitive_file_guard_1 = require("./sensitive-file-guard");
|
|
33
36
|
const pr_summary_1 = require("./pr-summary");
|
|
34
37
|
const utils_1 = require("../utils");
|
|
35
38
|
const result_collector_1 = require("../utils/result-collector");
|
|
@@ -49,6 +52,9 @@ __exportStar(require("./typecheck"), exports);
|
|
|
49
52
|
__exportStar(require("./diff-smell"), exports);
|
|
50
53
|
__exportStar(require("./pr-body"), exports);
|
|
51
54
|
__exportStar(require("./duplicate-check"), exports);
|
|
55
|
+
__exportStar(require("./commitlint"), exports);
|
|
56
|
+
__exportStar(require("./diff-security"), exports);
|
|
57
|
+
__exportStar(require("./sensitive-file-guard"), exports);
|
|
52
58
|
__exportStar(require("./pr-summary"), exports);
|
|
53
59
|
/**
|
|
54
60
|
* Wraps a blocking check: registers 'pass' on success, 'fail' on throw (and re-throws).
|
|
@@ -101,8 +107,11 @@ async function runJob(jobName) {
|
|
|
101
107
|
trackSoftCheck('Markdown', markdown_lint_1.runMarkdownLint),
|
|
102
108
|
trackSoftCheck('Spellcheck', spellcheck_1.runSpellcheck),
|
|
103
109
|
(0, diff_smell_1.runDiffSmell)(),
|
|
110
|
+
(0, diff_security_1.runDiffSecurity)(),
|
|
111
|
+
(0, sensitive_file_guard_1.runSensitiveFileGuard)(),
|
|
104
112
|
(0, pr_body_1.runPrBodyCheck)(),
|
|
105
113
|
(0, duplicate_check_1.runDuplicateCheck)(),
|
|
114
|
+
(0, commitlint_1.runCommitlint)(),
|
|
106
115
|
]);
|
|
107
116
|
// Phase 3: prettier last — may write files, then auto-commit in CI
|
|
108
117
|
await trackSoftCheck('Prettier', prettier_autoformat_1.runPrettier);
|
|
@@ -158,8 +167,17 @@ async function runJob(jobName) {
|
|
|
158
167
|
case 'duplicate':
|
|
159
168
|
await (0, duplicate_check_1.runDuplicateCheck)();
|
|
160
169
|
break;
|
|
170
|
+
case 'commitlint':
|
|
171
|
+
await (0, commitlint_1.runCommitlint)();
|
|
172
|
+
break;
|
|
173
|
+
case 'diff-security':
|
|
174
|
+
await (0, diff_security_1.runDiffSecurity)();
|
|
175
|
+
break;
|
|
176
|
+
case 'sensitive-files':
|
|
177
|
+
await (0, sensitive_file_guard_1.runSensitiveFileGuard)();
|
|
178
|
+
break;
|
|
161
179
|
default:
|
|
162
|
-
utils_1.logger.warn(`[runJob]: Unknown job "${jobName}". Available: all, lint, deps, npm-audit, prettier, spellcheck, security, circular, license, pr-size, pr-title, outdated, markdown, typecheck, diff-smell, pr-body, duplicate`);
|
|
180
|
+
utils_1.logger.warn(`[runJob]: Unknown job "${jobName}". Available: all, lint, deps, npm-audit, prettier, spellcheck, security, circular, license, pr-size, pr-title, outdated, markdown, typecheck, diff-smell, diff-security, sensitive-files, pr-body, duplicate, commitlint`);
|
|
163
181
|
}
|
|
164
182
|
}
|
|
165
183
|
catch (err) {
|
package/dist/scripts/lint.js
CHANGED
|
@@ -9,12 +9,10 @@ const utils_1 = require("../utils");
|
|
|
9
9
|
const delta_1 = require("../utils/delta");
|
|
10
10
|
const execa_1 = require("execa");
|
|
11
11
|
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
-
/**
|
|
13
|
-
* Detect ESLint major version to choose appropriate config format
|
|
14
|
-
*/
|
|
15
12
|
async function getESLintMajorVersion() {
|
|
16
13
|
try {
|
|
17
|
-
const
|
|
14
|
+
const eslintBin = (0, utils_1.resolvePackageBin)('eslint', 'bin/eslint.js');
|
|
15
|
+
const { stdout } = await (0, execa_1.execa)(process.execPath, [eslintBin, '--version']);
|
|
18
16
|
const match = stdout.match(/v?(\d+)\./);
|
|
19
17
|
return match ? parseInt(match[1], 10) : 8;
|
|
20
18
|
}
|
|
@@ -24,6 +22,7 @@ async function getESLintMajorVersion() {
|
|
|
24
22
|
}
|
|
25
23
|
async function runLint() {
|
|
26
24
|
utils_1.logger.info('[runLint]: 🔍 Running ESLint...');
|
|
25
|
+
const eslintBin = (0, utils_1.resolvePackageBin)('eslint', 'bin/eslint.js');
|
|
27
26
|
const projectRoot = process.cwd();
|
|
28
27
|
const sourcePath = (0, config_1.getSourcePath)();
|
|
29
28
|
const sourceFullPath = node_path_1.default.join(projectRoot, sourcePath);
|
|
@@ -39,7 +38,7 @@ async function runLint() {
|
|
|
39
38
|
let args;
|
|
40
39
|
if (hasLocal) {
|
|
41
40
|
utils_1.logger.info(`[runLint]: Using local ESLint config (checking: ${sourcePath})`);
|
|
42
|
-
args = [
|
|
41
|
+
args = [...targets, '--ext', '.ts,.tsx,.js,.jsx', '--fix'];
|
|
43
42
|
}
|
|
44
43
|
else {
|
|
45
44
|
utils_1.logger.info(`[runLint]: Using pr-checkmate ESLint config (checking: ${sourcePath})`);
|
|
@@ -48,11 +47,11 @@ async function runLint() {
|
|
|
48
47
|
const configFile = eslintVersion >= 9 ? 'eslint.config.mjs' : '.eslintrc.js';
|
|
49
48
|
const configPath = (0, config_1.getPackageConfigPath)(configFile);
|
|
50
49
|
utils_1.logger.info(`[runLint]: Detected ESLint v${eslintVersion}, using ${configFile}`);
|
|
51
|
-
args = [
|
|
50
|
+
args = [...targets, '--ext', '.ts,.tsx,.js,.jsx', '--fix', '--config', configPath];
|
|
52
51
|
}
|
|
53
52
|
config_1.IGNORE_PATTERNS.forEach(p => args.push('--ignore-pattern', p));
|
|
54
53
|
try {
|
|
55
|
-
await (0, execa_1.execa)(
|
|
54
|
+
await (0, execa_1.execa)(process.execPath, [eslintBin, ...args], { stdio: 'inherit' });
|
|
56
55
|
utils_1.logger.info('[runLint]: ✅ ESLint passed');
|
|
57
56
|
}
|
|
58
57
|
catch (err) {
|
|
@@ -43,8 +43,14 @@ async function runMarkdownLint() {
|
|
|
43
43
|
else {
|
|
44
44
|
// Use git ls-files to get only tracked .md files — naturally excludes .gitignore and untracked dirs
|
|
45
45
|
try {
|
|
46
|
-
const { stdout } = await (0, execa_1.execa)('git', ['ls-files', '*.md', '**/*.md'], {
|
|
47
|
-
|
|
46
|
+
const { stdout } = await (0, execa_1.execa)('git', ['ls-files', '*.md', '**/*.md'], {
|
|
47
|
+
cwd: root,
|
|
48
|
+
reject: false,
|
|
49
|
+
});
|
|
50
|
+
const tracked = stdout
|
|
51
|
+
.split('\n')
|
|
52
|
+
.map(f => f.trim())
|
|
53
|
+
.filter(f => f.endsWith('.md'));
|
|
48
54
|
targets = tracked.length > 0 ? tracked : [];
|
|
49
55
|
}
|
|
50
56
|
catch {
|
|
@@ -60,7 +66,12 @@ async function runMarkdownLint() {
|
|
|
60
66
|
const hasUserRules = Object.keys(userRules).length > 0;
|
|
61
67
|
let configArg;
|
|
62
68
|
let tempConfigFile = null;
|
|
63
|
-
if ((0, config_1.hasLocalConfig)([
|
|
69
|
+
if ((0, config_1.hasLocalConfig)([
|
|
70
|
+
'.markdownlint.json',
|
|
71
|
+
'.markdownlint.jsonc',
|
|
72
|
+
'.markdownlint.yaml',
|
|
73
|
+
'.markdownlint.yml',
|
|
74
|
+
])) {
|
|
64
75
|
utils_1.logger.info('[runMarkdownLint]: 📋 Using local .markdownlint config');
|
|
65
76
|
configArg = [];
|
|
66
77
|
}
|
|
@@ -79,21 +90,29 @@ async function runMarkdownLint() {
|
|
|
79
90
|
const extraIgnores = (userConfig.ignores ?? []).flatMap(p => ['--ignore', p]);
|
|
80
91
|
const gitignorePath = node_path_1.default.join(root, '.gitignore');
|
|
81
92
|
const ignorePath = node_fs_1.default.existsSync(gitignorePath) ? ['--ignore-path', gitignorePath] : [];
|
|
93
|
+
const markdownlintBin = (0, utils_1.resolvePackageBin)('markdownlint-cli', 'markdownlint.js');
|
|
82
94
|
const args = [
|
|
83
|
-
'markdownlint',
|
|
84
95
|
'--fix',
|
|
85
96
|
...targets,
|
|
86
97
|
...configArg,
|
|
87
98
|
...ignorePath,
|
|
88
|
-
'--ignore',
|
|
89
|
-
'
|
|
90
|
-
'--ignore',
|
|
91
|
-
'
|
|
92
|
-
'--ignore',
|
|
99
|
+
'--ignore',
|
|
100
|
+
'node_modules',
|
|
101
|
+
'--ignore',
|
|
102
|
+
'dist',
|
|
103
|
+
'--ignore',
|
|
104
|
+
'coverage',
|
|
105
|
+
'--ignore',
|
|
106
|
+
'graphify-out',
|
|
107
|
+
'--ignore',
|
|
108
|
+
'.claude',
|
|
93
109
|
...extraIgnores,
|
|
94
110
|
];
|
|
95
111
|
try {
|
|
96
|
-
const result = await (0, execa_1.execa)(
|
|
112
|
+
const result = await (0, execa_1.execa)(process.execPath, [markdownlintBin, ...args], {
|
|
113
|
+
reject: false,
|
|
114
|
+
stdio: 'inherit',
|
|
115
|
+
});
|
|
97
116
|
if (result.exitCode === 0) {
|
|
98
117
|
utils_1.logger.info('[runMarkdownLint]: ✅ Markdown lint passed');
|
|
99
118
|
}
|
|
@@ -29,10 +29,10 @@ async function runPrTitleLint() {
|
|
|
29
29
|
}
|
|
30
30
|
const title = pr.title;
|
|
31
31
|
if (!CONVENTIONAL_PREFIX.test(title)) {
|
|
32
|
-
utils_1.logger.warn(
|
|
32
|
+
utils_1.logger.warn("[runPrTitleLint]: ⚠️ PR title doesn't follow Conventional Commits");
|
|
33
33
|
utils_1.logger.warn(` Got: "${title}"`);
|
|
34
|
-
utils_1.logger.warn(
|
|
35
|
-
utils_1.logger.warn(
|
|
34
|
+
utils_1.logger.warn(' Expected: type(scope): description');
|
|
35
|
+
utils_1.logger.warn(' Types: feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert');
|
|
36
36
|
// warning only — teams may not use Conventional Commits
|
|
37
37
|
return;
|
|
38
38
|
}
|
|
@@ -87,18 +87,19 @@ async function runPrettier() {
|
|
|
87
87
|
]);
|
|
88
88
|
let args;
|
|
89
89
|
let tempConfig = null;
|
|
90
|
+
const prettierBin = (0, utils_1.resolvePackageBin)('prettier', 'bin/prettier.cjs');
|
|
90
91
|
if (hasLocal) {
|
|
91
92
|
utils_1.logger.info(`[runPrettier]: Using local Prettier config (checking: ${sourcePath})`);
|
|
92
|
-
args = ['
|
|
93
|
+
args = ['--write', ...targets, '--ignore-path', ignorePath];
|
|
93
94
|
}
|
|
94
95
|
else {
|
|
95
96
|
utils_1.logger.info(`[runPrettier]: Using pr-checkmate Prettier config (checking: ${sourcePath})`);
|
|
96
97
|
// Create temporary config with merged settings
|
|
97
98
|
tempConfig = createTempPrettierConfig();
|
|
98
|
-
args = ['
|
|
99
|
+
args = ['--write', ...targets, '--config', tempConfig, '--ignore-path', ignorePath];
|
|
99
100
|
}
|
|
100
101
|
try {
|
|
101
|
-
await (0, execa_1.execa)(
|
|
102
|
+
await (0, execa_1.execa)(process.execPath, [prettierBin, ...args], { stdio: 'inherit' });
|
|
102
103
|
utils_1.logger.info('[runPrettier]: ✅ Prettier completed');
|
|
103
104
|
}
|
|
104
105
|
catch (err) {
|
package/dist/scripts/security.js
CHANGED
|
@@ -6,8 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.runSecurityScan = runSecurityScan;
|
|
7
7
|
const execa_1 = require("execa");
|
|
8
8
|
const utils_1 = require("../utils");
|
|
9
|
+
const config_1 = require("../config");
|
|
9
10
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
11
11
|
/**
|
|
12
12
|
* Get HEAD SHA from git
|
|
13
13
|
*/
|
|
@@ -49,18 +49,6 @@ async function getBaseSha() {
|
|
|
49
49
|
return null;
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
-
/**
|
|
53
|
-
* Check if gitleaks-secret-scanner is installed locally
|
|
54
|
-
*/
|
|
55
|
-
function getGitleaksCommand() {
|
|
56
|
-
const localPath = node_path_1.default.join(process.cwd(), 'node_modules', '.bin', 'gitleaks-secret-scanner');
|
|
57
|
-
if (node_fs_1.default.existsSync(localPath)) {
|
|
58
|
-
utils_1.logger.info('[getGitleaksCommand]: Using local gitleaks-secret-scanner');
|
|
59
|
-
return localPath;
|
|
60
|
-
}
|
|
61
|
-
utils_1.logger.info('[getGitleaksCommand]: Using npx gitleaks-secret-scanner');
|
|
62
|
-
return 'npx';
|
|
63
|
-
}
|
|
64
52
|
/**
|
|
65
53
|
* Run security scan for secrets
|
|
66
54
|
*/
|
|
@@ -80,11 +68,18 @@ async function runSecurityScan() {
|
|
|
80
68
|
...(headSha && { HEAD_SHA: headSha }),
|
|
81
69
|
...(baseSha && { BASE_SHA: baseSha }),
|
|
82
70
|
};
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
|
|
71
|
+
const gitleaksBin = (0, utils_1.resolvePackageBin)('gitleaks-secret-scanner-fixed', 'bin/cli.js');
|
|
72
|
+
const hasClientConfig = (0, config_1.hasLocalConfig)([
|
|
73
|
+
'.gitleaks.toml',
|
|
74
|
+
'gitleaks.toml',
|
|
75
|
+
'.gitleaks/config.toml',
|
|
76
|
+
]);
|
|
77
|
+
const gitleaksArgs = ['--diff-mode', 'ci'];
|
|
78
|
+
if (!hasClientConfig) {
|
|
79
|
+
gitleaksArgs.push('--config', (0, config_1.getPackageConfigPath)('gitleaks.toml'));
|
|
80
|
+
}
|
|
86
81
|
utils_1.logger.info(`HEAD_SHA=${env.HEAD_SHA || 'auto-detect'}, BASE_SHA=${env.BASE_SHA || 'auto-detect'}`);
|
|
87
|
-
await (0, execa_1.execa)(
|
|
82
|
+
await (0, execa_1.execa)(process.execPath, [gitleaksBin, ...gitleaksArgs], {
|
|
88
83
|
stdio: 'inherit',
|
|
89
84
|
env,
|
|
90
85
|
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runSensitiveFileGuard = runSensitiveFileGuard;
|
|
7
|
+
const execa_1 = require("execa");
|
|
8
|
+
const utils_1 = require("../utils");
|
|
9
|
+
const result_collector_1 = require("../utils/result-collector");
|
|
10
|
+
const loader_1 = require("../config/loader");
|
|
11
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
|
+
const SENSITIVE_CATEGORIES = [
|
|
13
|
+
{
|
|
14
|
+
label: 'Environment files (.env)',
|
|
15
|
+
patterns: [/^\.env$/, /^\.env\..+/, /\/.env$/, /\/.env\..+/],
|
|
16
|
+
severity: 'error',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
label: 'Private keys and certificates',
|
|
20
|
+
patterns: [/\.(pem|key|p12|pfx|crt|cer|der|jks|keystore)$/i],
|
|
21
|
+
severity: 'error',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
label: 'CI/CD pipeline config',
|
|
25
|
+
patterns: [
|
|
26
|
+
/^\.github\/workflows\//,
|
|
27
|
+
/^\.gitlab-ci\.yml$/,
|
|
28
|
+
/^\.circleci\//,
|
|
29
|
+
/^\.travis\.yml$/,
|
|
30
|
+
/^Jenkinsfile$/,
|
|
31
|
+
/^\.buildkite\//,
|
|
32
|
+
],
|
|
33
|
+
severity: 'warn',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
label: 'Authentication / authorization middleware',
|
|
37
|
+
patterns: [
|
|
38
|
+
/\/(middleware|guards?|interceptors?)\/(auth|jwt|session|token|oauth|rbac)/i,
|
|
39
|
+
/\/(auth|jwt|session)\.(ts|js|mjs|cjs)$/i,
|
|
40
|
+
],
|
|
41
|
+
severity: 'warn',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
label: 'Infrastructure and deployment config',
|
|
45
|
+
patterns: [
|
|
46
|
+
/^(docker-compose|compose)\.(yml|yaml)$/,
|
|
47
|
+
/^Dockerfile(\..+)?$/,
|
|
48
|
+
/^terraform\//,
|
|
49
|
+
/\.tf$/,
|
|
50
|
+
/^\.?kubeconfig$/,
|
|
51
|
+
/^helm\//,
|
|
52
|
+
],
|
|
53
|
+
severity: 'warn',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
label: 'Package manager lockfiles (dependency supply chain)',
|
|
57
|
+
patterns: [/^package-lock\.json$/, /^yarn\.lock$/, /^pnpm-lock\.yaml$/],
|
|
58
|
+
severity: 'warn',
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
function readBaseSha() {
|
|
62
|
+
const eventName = process.env.GITHUB_EVENT_NAME;
|
|
63
|
+
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
64
|
+
if (eventName !== 'pull_request' || !eventPath || !node_fs_1.default.existsSync(eventPath)) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const event = JSON.parse(node_fs_1.default.readFileSync(eventPath, 'utf-8'));
|
|
69
|
+
return event?.pull_request?.base?.sha ?? null;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function matchesCategory(filePath, category) {
|
|
76
|
+
return category.patterns.some(p => p.test(filePath));
|
|
77
|
+
}
|
|
78
|
+
async function runSensitiveFileGuard() {
|
|
79
|
+
const cfg = (0, loader_1.loadProjectConfig)();
|
|
80
|
+
if (cfg.sensitiveFileGuard?.enabled === false) {
|
|
81
|
+
utils_1.logger.info('[runSensitiveFileGuard]: Disabled in pr-checkmate.json, skipping');
|
|
82
|
+
result_collector_1.resultCollector.register('Sensitive Files', 'skip');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
utils_1.logger.info('[runSensitiveFileGuard]: Checking for sensitive file changes...');
|
|
86
|
+
const baseSha = readBaseSha();
|
|
87
|
+
if (!baseSha) {
|
|
88
|
+
utils_1.logger.info('[runSensitiveFileGuard]: Not in GitHub PR context, skipping');
|
|
89
|
+
result_collector_1.resultCollector.register('Sensitive Files', 'skip');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const { stdout } = await (0, execa_1.execa)('git', [
|
|
94
|
+
'diff',
|
|
95
|
+
'--name-only',
|
|
96
|
+
'--diff-filter=ACMRD',
|
|
97
|
+
baseSha,
|
|
98
|
+
'HEAD',
|
|
99
|
+
]);
|
|
100
|
+
const changedFiles = stdout.split('\n').filter(Boolean);
|
|
101
|
+
if (changedFiles.length === 0) {
|
|
102
|
+
utils_1.logger.info('[runSensitiveFileGuard]: ✅ No file changes to inspect');
|
|
103
|
+
result_collector_1.resultCollector.register('Sensitive Files', 'pass');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const errors = {};
|
|
107
|
+
const warnings = {};
|
|
108
|
+
for (const filePath of changedFiles) {
|
|
109
|
+
for (const category of SENSITIVE_CATEGORIES) {
|
|
110
|
+
if (matchesCategory(filePath, category)) {
|
|
111
|
+
const bucket = category.severity === 'error' ? errors : warnings;
|
|
112
|
+
if (!bucket[category.label])
|
|
113
|
+
bucket[category.label] = [];
|
|
114
|
+
bucket[category.label].push(filePath);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const totalErrors = Object.values(errors).reduce((s, a) => s + a.length, 0);
|
|
119
|
+
const totalWarnings = Object.values(warnings).reduce((s, a) => s + a.length, 0);
|
|
120
|
+
if (totalErrors === 0 && totalWarnings === 0) {
|
|
121
|
+
utils_1.logger.info('[runSensitiveFileGuard]: ✅ No sensitive file changes detected');
|
|
122
|
+
result_collector_1.resultCollector.register('Sensitive Files', 'pass');
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
for (const [label, files] of Object.entries(errors)) {
|
|
126
|
+
utils_1.logger.error(`[runSensitiveFileGuard]: ❌ ${label}:`);
|
|
127
|
+
files.forEach(f => utils_1.logger.error(` ${f}`));
|
|
128
|
+
}
|
|
129
|
+
for (const [label, files] of Object.entries(warnings)) {
|
|
130
|
+
utils_1.logger.warn(`[runSensitiveFileGuard]: ⚠️ ${label}:`);
|
|
131
|
+
files.forEach(f => utils_1.logger.warn(` ${f}`));
|
|
132
|
+
}
|
|
133
|
+
const parts = [];
|
|
134
|
+
if (totalErrors > 0)
|
|
135
|
+
parts.push(`${totalErrors} sensitive file(s) changed`);
|
|
136
|
+
if (totalWarnings > 0)
|
|
137
|
+
parts.push(`${totalWarnings} notable file(s) changed`);
|
|
138
|
+
const summary = parts.join(', ');
|
|
139
|
+
result_collector_1.resultCollector.register('Sensitive Files', 'warn', summary);
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
utils_1.logger.warn(`[runSensitiveFileGuard]: ⚠️ Could not check file changes\n${err}`);
|
|
143
|
+
result_collector_1.resultCollector.register('Sensitive Files', 'skip');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -32,11 +32,15 @@ function loadUserCspellConfig() {
|
|
|
32
32
|
*/
|
|
33
33
|
function createTempCspellConfig() {
|
|
34
34
|
const packageConfigPath = (0, config_1.getPackageConfigPath)('cspell.json');
|
|
35
|
+
const packageConfigDir = node_path_1.default.dirname(packageConfigPath);
|
|
35
36
|
const defaultConfig = JSON.parse(node_fs_1.default.readFileSync(packageConfigPath, 'utf-8'));
|
|
36
37
|
const userConfig = loadUserCspellConfig();
|
|
38
|
+
// Resolve relative import paths to absolute so they work from any client cwd
|
|
39
|
+
const resolvedImports = (defaultConfig.import ?? []).map(p => p.startsWith('.') ? node_path_1.default.resolve(packageConfigDir, p) : p);
|
|
37
40
|
// Merge configurations
|
|
38
41
|
const mergedConfig = {
|
|
39
42
|
...defaultConfig,
|
|
43
|
+
import: resolvedImports,
|
|
40
44
|
words: [...defaultConfig.words, ...(userConfig.words || [])],
|
|
41
45
|
ignorePaths: [...defaultConfig.ignorePaths, ...(userConfig.ignorePaths || [])],
|
|
42
46
|
ignoreRegExpList: [...defaultConfig.ignoreRegExpList, ...(userConfig.ignoreRegExpList || [])],
|
|
@@ -81,18 +85,19 @@ async function runSpellcheck() {
|
|
|
81
85
|
const hasLocal = (0, config_1.hasLocalConfig)(['cspell.json', '.cspell.json', 'cspell.config.json']);
|
|
82
86
|
let args;
|
|
83
87
|
let tempConfigPath = null;
|
|
88
|
+
const cspellBin = (0, utils_1.resolvePackageBin)('cspell', 'bin.mjs');
|
|
84
89
|
if (hasLocal) {
|
|
85
90
|
utils_1.logger.info(`📋 Using local cspell config (checking: ${sourcePath})`);
|
|
86
|
-
args = [
|
|
91
|
+
args = [...patterns, '--no-progress'];
|
|
87
92
|
}
|
|
88
93
|
else {
|
|
89
94
|
utils_1.logger.info(`[runSpellcheck]: Using pr-checkmate cspell config (checking: ${sourcePath})`);
|
|
90
95
|
// Create temporary config with merged settings
|
|
91
96
|
tempConfigPath = createTempCspellConfig();
|
|
92
|
-
args = [
|
|
97
|
+
args = [...patterns, '--config', tempConfigPath, '--no-progress'];
|
|
93
98
|
}
|
|
94
99
|
try {
|
|
95
|
-
await (0, execa_1.execa)(
|
|
100
|
+
await (0, execa_1.execa)(process.execPath, [cspellBin, ...args], { stdio: 'inherit' });
|
|
96
101
|
utils_1.logger.info('[runSpellcheck]: ✅ Spellcheck passed');
|
|
97
102
|
}
|
|
98
103
|
catch (err) {
|
|
@@ -21,7 +21,8 @@ async function runTypecheck() {
|
|
|
21
21
|
utils_1.logger.info('[runTypecheck]: No tsconfig.json found, skipping');
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
24
|
-
const
|
|
24
|
+
const tscBin = (0, utils_1.resolvePackageBin)('typescript', 'bin/tsc');
|
|
25
|
+
const result = await (0, execa_1.execa)(process.execPath, [tscBin, '--noEmit'], { reject: false });
|
|
25
26
|
if (result.exitCode !== 0) {
|
|
26
27
|
const output = [result.stdout, result.stderr].filter(Boolean).join('\n');
|
|
27
28
|
const errorCount = (output.match(/error TS\d+/g) ?? []).length;
|
package/dist/utils/index.js
CHANGED
|
@@ -23,6 +23,7 @@ const logger_1 = require("./logger");
|
|
|
23
23
|
__exportStar(require("./logger"), exports);
|
|
24
24
|
__exportStar(require("./delta"), exports);
|
|
25
25
|
__exportStar(require("./result-collector"), exports);
|
|
26
|
+
__exportStar(require("./resolve-bin"), exports);
|
|
26
27
|
function cleanupTempFile(filePath) {
|
|
27
28
|
try {
|
|
28
29
|
if (node_fs_1.default.existsSync(filePath)) {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resolvePackageBin = resolvePackageBin;
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
/**
|
|
10
|
+
* Resolves a binary path for a bundled package, bypassing exports map restrictions.
|
|
11
|
+
*
|
|
12
|
+
* Strategy (tries each in order):
|
|
13
|
+
* 1. Direct subpath — fast path for packages that explicitly export their bin
|
|
14
|
+
* 2. Via package.json — for packages that export ./package.json but not the bin subpath (e.g. eslint)
|
|
15
|
+
* 3. Walk up from main entry — for packages that export neither (e.g. cspell)
|
|
16
|
+
*/
|
|
17
|
+
function resolvePackageBin(pkgName, binRelPath) {
|
|
18
|
+
// 1. Direct subpath
|
|
19
|
+
try {
|
|
20
|
+
return require.resolve(`${pkgName}/${binRelPath}`);
|
|
21
|
+
}
|
|
22
|
+
catch { }
|
|
23
|
+
// 2. Via package.json
|
|
24
|
+
try {
|
|
25
|
+
const pkgJsonPath = require.resolve(`${pkgName}/package.json`);
|
|
26
|
+
return node_path_1.default.join(node_path_1.default.dirname(pkgJsonPath), binRelPath);
|
|
27
|
+
}
|
|
28
|
+
catch { }
|
|
29
|
+
// 3. Walk up from main entry to find package root by matching package name
|
|
30
|
+
const main = require.resolve(pkgName);
|
|
31
|
+
let dir = node_path_1.default.dirname(main);
|
|
32
|
+
while (dir !== node_path_1.default.dirname(dir)) {
|
|
33
|
+
const pkgJsonPath = node_path_1.default.join(dir, 'package.json');
|
|
34
|
+
if (node_fs_1.default.existsSync(pkgJsonPath)) {
|
|
35
|
+
try {
|
|
36
|
+
const pkg = JSON.parse(node_fs_1.default.readFileSync(pkgJsonPath, 'utf-8'));
|
|
37
|
+
if (pkg.name === pkgName) {
|
|
38
|
+
return node_path_1.default.join(dir, binRelPath);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch { }
|
|
42
|
+
}
|
|
43
|
+
dir = node_path_1.default.dirname(dir);
|
|
44
|
+
}
|
|
45
|
+
throw new Error(`Cannot resolve binary "${binRelPath}" for package "${pkgName}"`);
|
|
46
|
+
}
|
package/gitleaks.toml
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pr-checkmate",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.7",
|
|
4
4
|
"description": "Automated PR quality checks: linting, formatting, dependency analysis, and spellcheck",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"github-actions",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"cspell.json",
|
|
28
28
|
"src/config/pr-checkmate-workflow.yml",
|
|
29
29
|
".markdownlint.json",
|
|
30
|
+
"gitleaks.toml",
|
|
30
31
|
"LICENSE"
|
|
31
32
|
],
|
|
32
33
|
"scripts": {
|
|
@@ -59,6 +60,8 @@
|
|
|
59
60
|
"author": "Artur Polishchuk",
|
|
60
61
|
"license": "LicenseRef-Proprietary",
|
|
61
62
|
"dependencies": {
|
|
63
|
+
"@commitlint/cli": "^21.2.0",
|
|
64
|
+
"@commitlint/config-conventional": "^21.2.0",
|
|
62
65
|
"@cspell/dict-es-es": "^3.0.8",
|
|
63
66
|
"@cspell/dict-fr-fr": "^2.2.2",
|
|
64
67
|
"@cspell/dict-he": "^4.0.2",
|
|
@@ -83,9 +86,5 @@
|
|
|
83
86
|
},
|
|
84
87
|
"devDependencies": {
|
|
85
88
|
"standard-version": "^9.5.0"
|
|
86
|
-
},
|
|
87
|
-
"peerDependencies": {
|
|
88
|
-
"eslint": "^9.26.0",
|
|
89
|
-
"prettier": ">=3.0.0"
|
|
90
89
|
}
|
|
91
90
|
}
|
package/src/config/cspell.json
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
"./node_modules/@cspell/dict-es-es/cspell-ext.json"
|
|
10
10
|
],
|
|
11
11
|
"words": [
|
|
12
|
+
"ACMR",
|
|
13
|
+
"ACMRD",
|
|
12
14
|
"accona",
|
|
13
15
|
"Accona",
|
|
14
16
|
"acepto",
|
|
@@ -41,6 +43,7 @@
|
|
|
41
43
|
"Catrice",
|
|
42
44
|
"Cerrar",
|
|
43
45
|
"checkmate",
|
|
46
|
+
"circleci",
|
|
44
47
|
"coincidan",
|
|
45
48
|
"como",
|
|
46
49
|
"comprobante",
|
|
@@ -85,6 +88,7 @@
|
|
|
85
88
|
"gastos",
|
|
86
89
|
"Gastos",
|
|
87
90
|
"Gestión",
|
|
91
|
+
"gitlab",
|
|
88
92
|
"gitleaks",
|
|
89
93
|
"givatayim",
|
|
90
94
|
"Givatayim",
|
|
@@ -113,9 +117,11 @@
|
|
|
113
117
|
"israel",
|
|
114
118
|
"jerusalem",
|
|
115
119
|
"kfar",
|
|
120
|
+
"kubeconfig",
|
|
116
121
|
"Kfar",
|
|
117
122
|
"Kolomiets",
|
|
118
123
|
"leído",
|
|
124
|
+
"lockfiles",
|
|
119
125
|
"manera",
|
|
120
126
|
"mavox",
|
|
121
127
|
"Mavox",
|
|
@@ -182,6 +188,7 @@
|
|
|
182
188
|
"sistema",
|
|
183
189
|
"sultansadikov",
|
|
184
190
|
"telaviv",
|
|
191
|
+
"travis",
|
|
185
192
|
"Términos",
|
|
186
193
|
"Teudat",
|
|
187
194
|
"Tiempo",
|