pr-checkmate 1.22.8 → 1.23.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 +101 -333
- package/cspell.json +24 -1
- package/dist/cli.js +40 -2
- package/dist/config/constants.d.ts +9 -0
- package/dist/config/constants.js +68 -1
- package/dist/core/checks/dependencies/banned-imports.d.ts +2 -0
- package/dist/core/checks/dependencies/banned-imports.js +71 -0
- package/dist/core/checks/{circular-deps.d.ts → dependencies/circular-deps.d.ts} +1 -1
- package/dist/core/checks/{circular-deps.js → dependencies/circular-deps.js} +5 -5
- package/dist/core/checks/{dependency-check.d.ts → dependencies/dependency-check.d.ts} +1 -1
- package/dist/core/checks/{dependency-check.js → dependencies/dependency-check.js} +3 -3
- package/dist/core/checks/{license-check.d.ts → dependencies/license-check.d.ts} +1 -1
- package/dist/core/checks/{license-check.js → dependencies/license-check.js} +3 -3
- package/dist/core/checks/{npm-audit.d.ts → dependencies/npm-audit.d.ts} +1 -1
- package/dist/core/checks/{npm-audit.js → dependencies/npm-audit.js} +3 -3
- package/dist/core/checks/{outdated-deps.d.ts → dependencies/outdated-deps.d.ts} +1 -1
- package/dist/core/checks/{outdated-deps.js → dependencies/outdated-deps.js} +17 -4
- package/dist/core/checks/dependencies/vuln-scan.d.ts +9 -0
- package/dist/core/checks/dependencies/vuln-scan.js +41 -0
- package/dist/core/checks/findings.d.ts +37 -1
- package/dist/core/checks/findings.js +80 -3
- package/dist/core/checks/git/custom-rules.d.ts +2 -0
- package/dist/core/checks/git/custom-rules.js +102 -0
- package/dist/core/checks/{diff-smell.d.ts → git/diff-smell.d.ts} +1 -1
- package/dist/core/checks/{diff-smell.js → git/diff-smell.js} +3 -3
- package/dist/core/checks/git/large-files.d.ts +2 -0
- package/dist/core/checks/git/large-files.js +106 -0
- package/dist/core/checks/git/leftover-debug.d.ts +2 -0
- package/dist/core/checks/git/leftover-debug.js +77 -0
- package/dist/core/checks/git/lockfile-drift.d.ts +2 -0
- package/dist/core/checks/git/lockfile-drift.js +114 -0
- package/dist/core/checks/git/merge-conflict.d.ts +2 -0
- package/dist/core/checks/git/merge-conflict.js +109 -0
- package/dist/core/checks/git/missing-tests.d.ts +2 -0
- package/dist/core/checks/git/missing-tests.js +92 -0
- package/dist/core/checks/git/todo-fixme.d.ts +7 -0
- package/dist/core/checks/git/todo-fixme.js +74 -0
- package/dist/core/checks/{cpp-format.d.ts → languages/cpp-format.d.ts} +1 -1
- package/dist/core/checks/{cpp-format.js → languages/cpp-format.js} +3 -3
- package/dist/core/checks/languages/csharp-format.d.ts +13 -0
- package/dist/core/checks/languages/csharp-format.js +52 -0
- package/dist/core/checks/languages/go-format.d.ts +8 -0
- package/dist/core/checks/languages/go-format.js +48 -0
- package/dist/core/checks/languages/go-lint.d.ts +8 -0
- package/dist/core/checks/languages/go-lint.js +48 -0
- package/dist/core/checks/{kotlin-lint.d.ts → languages/kotlin-lint.d.ts} +1 -1
- package/dist/core/checks/{kotlin-lint.js → languages/kotlin-lint.js} +13 -17
- package/dist/core/checks/{lint.d.ts → languages/lint.d.ts} +1 -1
- package/dist/core/checks/{lint.js → languages/lint.js} +5 -5
- package/dist/core/checks/languages/php-format.d.ts +9 -0
- package/dist/core/checks/languages/php-format.js +49 -0
- package/dist/core/checks/{prettier.d.ts → languages/prettier.d.ts} +1 -1
- package/dist/core/checks/{prettier.js → languages/prettier.js} +5 -5
- package/dist/core/checks/{python-format.d.ts → languages/python-format.d.ts} +1 -1
- package/dist/core/checks/{python-format.js → languages/python-format.js} +3 -3
- package/dist/core/checks/{python-lint.d.ts → languages/python-lint.d.ts} +1 -1
- package/dist/core/checks/{python-lint.js → languages/python-lint.js} +3 -3
- package/dist/core/checks/languages/python-typecheck.d.ts +9 -0
- package/dist/core/checks/languages/python-typecheck.js +66 -0
- package/dist/core/checks/languages/ruby-lint.d.ts +8 -0
- package/dist/core/checks/languages/ruby-lint.js +50 -0
- package/dist/core/checks/languages/rust-format.d.ts +9 -0
- package/dist/core/checks/languages/rust-format.js +45 -0
- package/dist/core/checks/languages/rust-lint.d.ts +8 -0
- package/dist/core/checks/languages/rust-lint.js +50 -0
- package/dist/core/checks/{swift-lint.d.ts → languages/swift-lint.d.ts} +1 -1
- package/dist/core/checks/{swift-lint.js → languages/swift-lint.js} +13 -17
- package/dist/core/checks/{typecheck.d.ts → languages/typecheck.d.ts} +1 -1
- package/dist/core/checks/{typecheck.js → languages/typecheck.js} +3 -3
- package/dist/core/checks/{commitlint.d.ts → pr/commitlint.d.ts} +1 -1
- package/dist/core/checks/{commitlint.js → pr/commitlint.js} +4 -4
- package/dist/core/checks/{pr-body.d.ts → pr/pr-body.d.ts} +1 -1
- package/dist/core/checks/{pr-body.js → pr/pr-body.js} +2 -2
- package/dist/core/checks/{pr-size-check.d.ts → pr/pr-size-check.d.ts} +1 -1
- package/dist/core/checks/{pr-size-check.js → pr/pr-size-check.js} +13 -11
- package/dist/core/checks/{pr-title-lint.d.ts → pr/pr-title-lint.d.ts} +1 -1
- package/dist/core/checks/{pr-title-lint.js → pr/pr-title-lint.js} +2 -2
- package/dist/core/checks/quality/case-collision.d.ts +8 -0
- package/dist/core/checks/quality/case-collision.js +40 -0
- package/dist/core/checks/quality/config-validation.d.ts +8 -0
- package/dist/core/checks/quality/config-validation.js +63 -0
- package/dist/core/checks/quality/coverage.d.ts +2 -0
- package/dist/core/checks/quality/coverage.js +63 -0
- package/dist/core/checks/{dead-code.d.ts → quality/dead-code.d.ts} +1 -1
- package/dist/core/checks/{dead-code.js → quality/dead-code.js} +4 -4
- package/dist/core/checks/{duplicate-check.d.ts → quality/duplicate-check.d.ts} +1 -1
- package/dist/core/checks/{duplicate-check.js → quality/duplicate-check.js} +6 -6
- package/dist/core/checks/quality/license-header.d.ts +7 -0
- package/dist/core/checks/quality/license-header.js +74 -0
- package/dist/core/checks/{markdown-lint.d.ts → quality/markdown-lint.d.ts} +1 -1
- package/dist/core/checks/{markdown-lint.js → quality/markdown-lint.js} +5 -5
- package/dist/core/checks/{spellcheck.d.ts → quality/spellcheck.d.ts} +1 -1
- package/dist/core/checks/{spellcheck.js → quality/spellcheck.js} +5 -5
- package/dist/core/checks/{diff-security.d.ts → security/diff-security.d.ts} +1 -1
- package/dist/core/checks/security/diff-security.js +262 -0
- package/dist/core/checks/security/dockerfile-security.d.ts +2 -0
- package/dist/core/checks/security/dockerfile-security.js +92 -0
- package/dist/core/checks/security/migration-safety.d.ts +2 -0
- package/dist/core/checks/security/migration-safety.js +110 -0
- package/dist/core/checks/{security.d.ts → security/security.d.ts} +1 -1
- package/dist/core/checks/{security.js → security/security.js} +10 -5
- package/dist/core/checks/{sensitive-file-guard.d.ts → security/sensitive-file-guard.d.ts} +1 -1
- package/dist/core/checks/{sensitive-file-guard.js → security/sensitive-file-guard.js} +4 -4
- package/dist/core/checks/security/workflow-security.d.ts +2 -0
- package/dist/core/checks/security/workflow-security.js +75 -0
- package/dist/core/context.js +14 -0
- package/dist/core/registry.js +73 -28
- package/dist/core/reporters.d.ts +4 -3
- package/dist/core/reporters.js +106 -4
- package/dist/core/run-checks.js +21 -3
- package/dist/core/targets.d.ts +4 -1
- package/dist/core/targets.js +16 -3
- package/dist/core/types.d.ts +13 -1
- package/dist/init.js +82 -0
- package/dist/interfaces/index.d.ts +101 -0
- package/dist/scripts/copy-configs.js +1 -0
- package/dist/utils/detect-language.d.ts +1 -1
- package/dist/utils/detect-language.js +32 -3
- package/dist/utils/glob.d.ts +10 -0
- package/dist/utils/glob.js +25 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/update-notifier.d.ts +10 -0
- package/dist/utils/update-notifier.js +60 -0
- package/package.json +2 -1
- package/pr-checkmate.schema.json +166 -0
- package/src/config/cspell.json +24 -1
- package/src/config/pr-checkmate.schema.json +166 -0
- package/dist/core/checks/diff-security.js +0 -145
- /package/dist/core/checks/{ruff-shared.d.ts → languages/ruff-shared.d.ts} +0 -0
- /package/dist/core/checks/{ruff-shared.js → languages/ruff-shared.js} +0 -0
package/dist/config/constants.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.CONFIG_FILE = exports.documentation = exports.DEFAULT_ALLOW_PACKAGES = exports.DEFAULT_COMMANDS = exports.SOURCE_EXTENSIONS = exports.DUPLICATE_CODE_FORMATS = exports.CONFIG_FILE_PATTERNS = exports.DEAD_CODE_IGNORE_DEFAULTS = exports.DUPLICATE_IGNORE_DEFAULTS = exports.IGNORE_PATTERNS = exports.IGNORED_DIRS = void 0;
|
|
6
|
+
exports.KNOWN_CONFIG_KEYS = exports.CONFIG_FILE = exports.documentation = exports.DEFAULT_ALLOW_PACKAGES = exports.DEFAULT_COMMANDS = exports.SOURCE_EXTENSIONS = exports.DUPLICATE_CODE_FORMATS = exports.CONFIG_FILE_PATTERNS = exports.DEAD_CODE_IGNORE_DEFAULTS = exports.DUPLICATE_IGNORE_DEFAULTS = exports.IGNORE_PATTERNS = exports.IGNORED_DIRS = void 0;
|
|
7
7
|
const node_path_1 = __importDefault(require("node:path"));
|
|
8
8
|
/**
|
|
9
9
|
* Build output, dependency, tooling, and report directories that are never
|
|
@@ -214,3 +214,70 @@ exports.documentation = {
|
|
|
214
214
|
cspell: 'https://cspell.org/configuration/',
|
|
215
215
|
};
|
|
216
216
|
exports.CONFIG_FILE = node_path_1.default.resolve(process.cwd(), 'pr-checkmate.json');
|
|
217
|
+
/**
|
|
218
|
+
* Every top-level key PR CheckMate recognises in pr-checkmate.json — the union
|
|
219
|
+
* of the `PRCheckMateConfig` interface fields and the keys the `init` generator
|
|
220
|
+
* writes (which use `eslint`/`cspell`/`markdownlint` rather than the interface's
|
|
221
|
+
* `lint`/`spellcheck`). The Config Validation check warns on any key not in this
|
|
222
|
+
* set (typos), ignoring `//`-prefixed comment keys and `$schema`. Keep in sync
|
|
223
|
+
* when adding a new configurable check.
|
|
224
|
+
*/
|
|
225
|
+
exports.KNOWN_CONFIG_KEYS = new Set([
|
|
226
|
+
// core
|
|
227
|
+
'sourcePath',
|
|
228
|
+
'ignoreDirs',
|
|
229
|
+
'languages',
|
|
230
|
+
'severity',
|
|
231
|
+
'customRules',
|
|
232
|
+
'$schema',
|
|
233
|
+
'commands',
|
|
234
|
+
'documentation',
|
|
235
|
+
'configValidation',
|
|
236
|
+
// linters/formatters (interface + init aliases)
|
|
237
|
+
'lint',
|
|
238
|
+
'eslint',
|
|
239
|
+
'prettier',
|
|
240
|
+
'spellcheck',
|
|
241
|
+
'cspell',
|
|
242
|
+
'markdownlint',
|
|
243
|
+
'typecheck',
|
|
244
|
+
// security
|
|
245
|
+
'security',
|
|
246
|
+
'diffSecurity',
|
|
247
|
+
'workflowSecurity',
|
|
248
|
+
'dockerfileSecurity',
|
|
249
|
+
'sensitiveFileGuard',
|
|
250
|
+
'migrationSafety',
|
|
251
|
+
// dependencies
|
|
252
|
+
'dependency',
|
|
253
|
+
'outdatedDeps',
|
|
254
|
+
'licenseCheck',
|
|
255
|
+
'vulnScan',
|
|
256
|
+
'bannedImports',
|
|
257
|
+
// git-diff / PR
|
|
258
|
+
'mergeConflict',
|
|
259
|
+
'diffSmell',
|
|
260
|
+
'leftoverDebug',
|
|
261
|
+
'todoFixme',
|
|
262
|
+
'lockfileDrift',
|
|
263
|
+
'largeFiles',
|
|
264
|
+
'missingTests',
|
|
265
|
+
'coverage',
|
|
266
|
+
'caseCollision',
|
|
267
|
+
'licenseHeader',
|
|
268
|
+
'duplicate',
|
|
269
|
+
'deadCode',
|
|
270
|
+
'prBody',
|
|
271
|
+
'prSize',
|
|
272
|
+
'commitlint',
|
|
273
|
+
// languages
|
|
274
|
+
'cpp',
|
|
275
|
+
'python',
|
|
276
|
+
'swift',
|
|
277
|
+
'kotlin',
|
|
278
|
+
'go',
|
|
279
|
+
'rust',
|
|
280
|
+
'csharp',
|
|
281
|
+
'ruby',
|
|
282
|
+
'php',
|
|
283
|
+
]);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.bannedImportsCheck = void 0;
|
|
4
|
+
const utils_1 = require("../../../utils");
|
|
5
|
+
const types_1 = require("../../types");
|
|
6
|
+
const findings_1 = require("../findings");
|
|
7
|
+
const PREFIX = 'Banned Imports';
|
|
8
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
9
|
+
/**
|
|
10
|
+
* Compile `bannedImports` into matchers that catch a banned package via an ES
|
|
11
|
+
* import, a CommonJS require, or its addition to a manifest dependency list.
|
|
12
|
+
*/
|
|
13
|
+
function compileBans(ctx) {
|
|
14
|
+
const list = ctx.config.bannedImports ?? [];
|
|
15
|
+
return list
|
|
16
|
+
.filter(b => b?.name)
|
|
17
|
+
.map(b => {
|
|
18
|
+
const n = escapeRe(b.name);
|
|
19
|
+
const re = new RegExp(`from\\s+['"]${n}(?:/[^'"]*)?['"]|require\\(\\s*['"]${n}(?:/[^'"]*)?['"]|import\\s+['"]${n}['"]|["']${n}["']\\s*:`);
|
|
20
|
+
return {
|
|
21
|
+
message: b.message ?? `banned dependency: ${b.name}`,
|
|
22
|
+
re,
|
|
23
|
+
severity: b.severity === 'error' ? 'error' : 'warn',
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
exports.bannedImportsCheck = {
|
|
28
|
+
name: 'Banned Imports',
|
|
29
|
+
phase: 'informational',
|
|
30
|
+
applies(ctx) {
|
|
31
|
+
return (ctx.config.bannedImports?.length ?? 0) > 0 && Boolean(ctx.baseSha);
|
|
32
|
+
},
|
|
33
|
+
async run(ctx) {
|
|
34
|
+
var _a;
|
|
35
|
+
const bans = compileBans(ctx);
|
|
36
|
+
if (bans.length === 0)
|
|
37
|
+
return (0, types_1.pass)('no banned imports configured');
|
|
38
|
+
try {
|
|
39
|
+
const stdout = await (0, findings_1.diffAddedLines)(ctx, ['*']);
|
|
40
|
+
const errors = {};
|
|
41
|
+
const warnings = {};
|
|
42
|
+
for (const raw of stdout.split('\n')) {
|
|
43
|
+
if (!raw.startsWith('+') || raw.startsWith('+++'))
|
|
44
|
+
continue;
|
|
45
|
+
const content = raw.slice(1);
|
|
46
|
+
for (const ban of bans) {
|
|
47
|
+
if (ban.re.test(content)) {
|
|
48
|
+
const bucket = ban.severity === 'error' ? errors : warnings;
|
|
49
|
+
(bucket[_a = ban.message] ?? (bucket[_a] = [])).push(content.trim().substring(0, 100));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const totalErrors = (0, findings_1.bucketTotal)(errors);
|
|
54
|
+
const totalWarnings = (0, findings_1.bucketTotal)(warnings);
|
|
55
|
+
if (totalErrors === 0 && totalWarnings === 0)
|
|
56
|
+
return (0, types_1.pass)();
|
|
57
|
+
(0, findings_1.logFindings)(errors, { prefix: PREFIX, isError: true, sampleLimit: 3 });
|
|
58
|
+
(0, findings_1.logFindings)(warnings, { prefix: PREFIX, sampleLimit: 3 });
|
|
59
|
+
const parts = [];
|
|
60
|
+
if (totalErrors > 0)
|
|
61
|
+
parts.push(`${totalErrors} banned (error)`);
|
|
62
|
+
if (totalWarnings > 0)
|
|
63
|
+
parts.push(`${totalWarnings} banned (warn)`);
|
|
64
|
+
return totalErrors > 0 ? (0, types_1.fail)(parts.join(', ')) : (0, types_1.warn)(parts.join(', '));
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
utils_1.logger.warn(`[Banned Imports]: ⚠️ Could not analyze diff\n${err}`);
|
|
68
|
+
return (0, types_1.skip)('diff unavailable');
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Check } from '
|
|
1
|
+
import { Check } from '../../types';
|
|
2
2
|
export declare const circularDepsCheck: Check;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.circularDepsCheck = void 0;
|
|
4
|
-
const config_1 = require("
|
|
5
|
-
const utils_1 = require("
|
|
6
|
-
const execa_1 = require("
|
|
7
|
-
const targets_1 = require("
|
|
8
|
-
const types_1 = require("
|
|
4
|
+
const config_1 = require("../../../config");
|
|
5
|
+
const utils_1 = require("../../../utils");
|
|
6
|
+
const execa_1 = require("../../../utils/execa");
|
|
7
|
+
const targets_1 = require("../../targets");
|
|
8
|
+
const types_1 = require("../../types");
|
|
9
9
|
exports.circularDepsCheck = {
|
|
10
10
|
name: 'Circular Deps',
|
|
11
11
|
phase: 'blocking',
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Check } from '
|
|
1
|
+
import { Check } from '../../types';
|
|
2
2
|
export declare const dependencyCheck: Check;
|
|
@@ -6,9 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.dependencyCheck = void 0;
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
-
const utils_1 = require("
|
|
10
|
-
const execa_1 = require("
|
|
11
|
-
const types_1 = require("
|
|
9
|
+
const utils_1 = require("../../../utils");
|
|
10
|
+
const execa_1 = require("../../../utils/execa");
|
|
11
|
+
const types_1 = require("../../types");
|
|
12
12
|
exports.dependencyCheck = {
|
|
13
13
|
name: 'Dependencies',
|
|
14
14
|
phase: 'blocking',
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Check } from '
|
|
1
|
+
import { Check } from '../../types';
|
|
2
2
|
export declare const licenseCheck: Check;
|
|
@@ -6,9 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.licenseCheck = void 0;
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
-
const utils_1 = require("
|
|
10
|
-
const execa_1 = require("
|
|
11
|
-
const types_1 = require("
|
|
9
|
+
const utils_1 = require("../../../utils");
|
|
10
|
+
const execa_1 = require("../../../utils/execa");
|
|
11
|
+
const types_1 = require("../../types");
|
|
12
12
|
const FORBIDDEN_LICENSES = ['GPL-2.0', 'GPL-3.0', 'AGPL-3.0', 'LGPL-2.0', 'LGPL-2.1', 'LGPL-3.0'];
|
|
13
13
|
/** Strip the trailing "@version" to get the bare package name (scoped-safe). */
|
|
14
14
|
function bareName(pkgKey) {
|
|
@@ -6,9 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.npmAuditCheck = void 0;
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
-
const utils_1 = require("
|
|
10
|
-
const execa_1 = require("
|
|
11
|
-
const types_1 = require("
|
|
9
|
+
const utils_1 = require("../../../utils");
|
|
10
|
+
const execa_1 = require("../../../utils/execa");
|
|
11
|
+
const types_1 = require("../../types");
|
|
12
12
|
const SEVERITY_ORDER = ['info', 'low', 'moderate', 'high', 'critical'];
|
|
13
13
|
function isSeverityAtLeast(severity, level) {
|
|
14
14
|
const sevIdx = SEVERITY_ORDER.indexOf(severity);
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Check } from '
|
|
1
|
+
import { Check } from '../../types';
|
|
2
2
|
export declare const outdatedDepsCheck: Check;
|
|
@@ -6,9 +6,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.outdatedDepsCheck = void 0;
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
-
const utils_1 = require("
|
|
10
|
-
const execa_1 = require("
|
|
11
|
-
const types_1 = require("
|
|
9
|
+
const utils_1 = require("../../../utils");
|
|
10
|
+
const execa_1 = require("../../../utils/execa");
|
|
11
|
+
const types_1 = require("../../types");
|
|
12
|
+
const DEFAULT_TIMEOUT_MS = 30000;
|
|
12
13
|
exports.outdatedDepsCheck = {
|
|
13
14
|
name: 'Outdated Deps',
|
|
14
15
|
phase: 'informational',
|
|
@@ -18,8 +19,20 @@ exports.outdatedDepsCheck = {
|
|
|
18
19
|
return node_fs_1.default.existsSync(node_path_1.default.join(ctx.cwd, 'package.json'));
|
|
19
20
|
},
|
|
20
21
|
async run(ctx) {
|
|
22
|
+
// `npm outdated` queries the registry for every dependency and can take a
|
|
23
|
+
// long time on big dependency trees — cap it so an advisory check never
|
|
24
|
+
// holds up CI. On timeout, skip rather than fail.
|
|
25
|
+
const timeout = ctx.config.outdatedDeps?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
21
26
|
// npm outdated exits 1 when outdated packages exist — use reject:false.
|
|
22
|
-
const result = await (0, execa_1.execa)('npm', ['outdated', '--json'], {
|
|
27
|
+
const result = await (0, execa_1.execa)('npm', ['outdated', '--json'], {
|
|
28
|
+
cwd: ctx.cwd,
|
|
29
|
+
reject: false,
|
|
30
|
+
timeout,
|
|
31
|
+
});
|
|
32
|
+
if (result.timedOut) {
|
|
33
|
+
utils_1.logger.warn(`[Outdated Deps]: ⚠️ npm outdated timed out after ${timeout}ms — skipping`);
|
|
34
|
+
return (0, types_1.skip)('npm outdated timed out');
|
|
35
|
+
}
|
|
23
36
|
if (!result.stdout?.trim())
|
|
24
37
|
return (0, types_1.pass)();
|
|
25
38
|
let outdated;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Check } from '../../types';
|
|
2
|
+
/**
|
|
3
|
+
* Cross-language dependency vulnerability scan via osv-scanner, which reads
|
|
4
|
+
* lockfiles across ecosystems (npm, PyPI, Go, crates, Maven, …) against the OSV
|
|
5
|
+
* database. Runner-dependency and opt-in (`vulnScan.enabled: true`): osv-scanner
|
|
6
|
+
* must be on the runner, and the check skips gracefully when it is absent.
|
|
7
|
+
* Complements the JS-only `npm-audit` with coverage for every other ecosystem.
|
|
8
|
+
*/
|
|
9
|
+
export declare const vulnScanCheck: Check;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.vulnScanCheck = void 0;
|
|
4
|
+
const utils_1 = require("../../../utils");
|
|
5
|
+
const execa_1 = require("../../../utils/execa");
|
|
6
|
+
const types_1 = require("../../types");
|
|
7
|
+
/**
|
|
8
|
+
* Cross-language dependency vulnerability scan via osv-scanner, which reads
|
|
9
|
+
* lockfiles across ecosystems (npm, PyPI, Go, crates, Maven, …) against the OSV
|
|
10
|
+
* database. Runner-dependency and opt-in (`vulnScan.enabled: true`): osv-scanner
|
|
11
|
+
* must be on the runner, and the check skips gracefully when it is absent.
|
|
12
|
+
* Complements the JS-only `npm-audit` with coverage for every other ecosystem.
|
|
13
|
+
*/
|
|
14
|
+
exports.vulnScanCheck = {
|
|
15
|
+
name: 'Vuln Scan',
|
|
16
|
+
phase: 'informational',
|
|
17
|
+
applies(ctx) {
|
|
18
|
+
return ctx.config.vulnScan?.enabled === true;
|
|
19
|
+
},
|
|
20
|
+
async run(ctx) {
|
|
21
|
+
const result = await (0, execa_1.execa)('osv-scanner', ['-r', '.'], { cwd: ctx.cwd, reject: false });
|
|
22
|
+
// execa v9 with reject:false resolves (not throws) with `code: 'ENOENT'`
|
|
23
|
+
// when the binary is absent — detect the missing scanner on the result.
|
|
24
|
+
if (result.code === 'ENOENT')
|
|
25
|
+
return (0, types_1.skip)('osv-scanner not installed');
|
|
26
|
+
if (result.exitCode === 0) {
|
|
27
|
+
utils_1.logger.info('[Vuln Scan]: ✅ No known vulnerabilities');
|
|
28
|
+
return (0, types_1.pass)();
|
|
29
|
+
}
|
|
30
|
+
const lines = [result.stdout, result.stderr]
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
.join('\n')
|
|
33
|
+
.split('\n')
|
|
34
|
+
.filter(l => /\b(CVE-|GHSA-|vulnerab|advisor)/i.test(l));
|
|
35
|
+
utils_1.logger.warn('[Vuln Scan]: ⚠️ known vulnerabilities found:');
|
|
36
|
+
lines.slice(0, 10).forEach(l => utils_1.logger.warn(` ${l.trim()}`));
|
|
37
|
+
if (lines.length > 10)
|
|
38
|
+
utils_1.logger.warn(` ... and ${lines.length - 10} more`);
|
|
39
|
+
return (0, types_1.warn)(`${lines.length || 'unknown'} vulnerability finding(s) (osv-scanner)`);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -1,4 +1,26 @@
|
|
|
1
1
|
import { CheckContext, CheckOutcome } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Client-configurable severity for a check's findings, read from that check's
|
|
4
|
+
* config block (e.g. `mergeConflict.severity`). `error` fails the run, `warn`
|
|
5
|
+
* only advises. Every check declares a natural default; the client overrides it
|
|
6
|
+
* per repo in pr-checkmate.json — promoting an advisory check to a gate, or
|
|
7
|
+
* downgrading a blocking one, with no code change.
|
|
8
|
+
*/
|
|
9
|
+
export type Severity = 'error' | 'warn';
|
|
10
|
+
export interface SeverityEmitter {
|
|
11
|
+
/** Log at the resolved level (`logger.error` for error, `logger.warn` for warn). */
|
|
12
|
+
log: (msg: string) => void;
|
|
13
|
+
/** Prefix icon matching the level. */
|
|
14
|
+
icon: string;
|
|
15
|
+
/** Build the outcome at the resolved level (`fail` for error, `warn` for warn). */
|
|
16
|
+
emit: (details: string) => CheckOutcome;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Resolve a check's effective severity (`configured` from config, else its
|
|
20
|
+
* `fallback`) into a matched logger, icon, and outcome constructor — so a check
|
|
21
|
+
* logs and returns consistently at whatever level the client chose.
|
|
22
|
+
*/
|
|
23
|
+
export declare function severityEmitter(configured: Severity | undefined, fallback: Severity): SeverityEmitter;
|
|
2
24
|
/** Findings grouped by label → matching sample strings. */
|
|
3
25
|
export type FindingBucket = Record<string, string[]>;
|
|
4
26
|
/**
|
|
@@ -6,6 +28,14 @@ export type FindingBucket = Record<string, string[]>;
|
|
|
6
28
|
* (zero context, added/copied/modified/renamed files) for added-line scanning.
|
|
7
29
|
*/
|
|
8
30
|
export declare function diffAddedLines(ctx: CheckContext, globs: string[]): Promise<string>;
|
|
31
|
+
/**
|
|
32
|
+
* Strip a trailing line comment (`//` or `#`) from a line, tracking quote state
|
|
33
|
+
* so a marker inside a string literal (or a `#` in `"#fff"`, a `//` in a quoted
|
|
34
|
+
* URL) is preserved. Heuristic, not a tokenizer: block comments and regex
|
|
35
|
+
* literals aren't handled. Used by code-targeting checks so `# print(x)` or
|
|
36
|
+
* `// eval(x)` in a comment isn't flagged as real code.
|
|
37
|
+
*/
|
|
38
|
+
export declare function stripInlineComment(line: string): string;
|
|
9
39
|
/** Total number of findings across all labels in a bucket. */
|
|
10
40
|
export declare function bucketTotal(bucket: FindingBucket): number;
|
|
11
41
|
interface LogOptions {
|
|
@@ -31,10 +61,16 @@ interface SeverityReportOptions {
|
|
|
31
61
|
showCount?: boolean;
|
|
32
62
|
/** Max samples printed per label; omit to print all. */
|
|
33
63
|
sampleLimit?: number;
|
|
64
|
+
/**
|
|
65
|
+
* Finding labels to suppress (client `<check>.ignore`). A label is dropped
|
|
66
|
+
* when it contains any ignore entry (case-insensitive), so a repo can mute a
|
|
67
|
+
* single built-in finding without disabling the whole check.
|
|
68
|
+
*/
|
|
69
|
+
ignore?: string[];
|
|
34
70
|
}
|
|
35
71
|
/**
|
|
36
72
|
* Report error/warning finding buckets: log both, then produce the outcome.
|
|
37
73
|
* Returns pass() when both buckets are empty, otherwise warn() with a summary.
|
|
38
74
|
*/
|
|
39
|
-
export declare function reportSeverityBuckets(
|
|
75
|
+
export declare function reportSeverityBuckets(errorsIn: FindingBucket, warningsIn: FindingBucket, opts: SeverityReportOptions): CheckOutcome;
|
|
40
76
|
export {};
|
|
@@ -1,19 +1,81 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.severityEmitter = severityEmitter;
|
|
3
4
|
exports.diffAddedLines = diffAddedLines;
|
|
5
|
+
exports.stripInlineComment = stripInlineComment;
|
|
4
6
|
exports.bucketTotal = bucketTotal;
|
|
5
7
|
exports.logFindings = logFindings;
|
|
6
8
|
exports.reportSeverityBuckets = reportSeverityBuckets;
|
|
7
9
|
const utils_1 = require("../../utils");
|
|
8
10
|
const execa_1 = require("../../utils/execa");
|
|
9
11
|
const types_1 = require("../types");
|
|
12
|
+
/**
|
|
13
|
+
* Resolve a check's effective severity (`configured` from config, else its
|
|
14
|
+
* `fallback`) into a matched logger, icon, and outcome constructor — so a check
|
|
15
|
+
* logs and returns consistently at whatever level the client chose.
|
|
16
|
+
*/
|
|
17
|
+
function severityEmitter(configured, fallback) {
|
|
18
|
+
const isError = (configured ?? fallback) === 'error';
|
|
19
|
+
return {
|
|
20
|
+
log: (isError ? utils_1.logger.error : utils_1.logger.warn).bind(utils_1.logger),
|
|
21
|
+
icon: isError ? '❌' : '⚠️',
|
|
22
|
+
emit: (details) => (isError ? (0, types_1.fail)(details) : (0, types_1.warn)(details)),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
10
25
|
/**
|
|
11
26
|
* Run `git diff` over the given file globs, returning the raw unified diff
|
|
12
27
|
* (zero context, added/copied/modified/renamed files) for added-line scanning.
|
|
13
28
|
*/
|
|
14
29
|
async function diffAddedLines(ctx, globs) {
|
|
15
|
-
const
|
|
16
|
-
|
|
30
|
+
const args = [
|
|
31
|
+
'diff',
|
|
32
|
+
'--unified=0',
|
|
33
|
+
'--diff-filter=ACMR',
|
|
34
|
+
ctx.baseSha,
|
|
35
|
+
ctx.headSha ?? 'HEAD',
|
|
36
|
+
'--',
|
|
37
|
+
...globs,
|
|
38
|
+
];
|
|
39
|
+
const cacheKey = `d:${args.join('')}`;
|
|
40
|
+
let stdout = ctx.cache?.get(cacheKey);
|
|
41
|
+
if (stdout === undefined) {
|
|
42
|
+
stdout = (await (0, execa_1.execa)('git', args, { cwd: ctx.cwd })).stdout;
|
|
43
|
+
ctx.cache?.set(cacheKey, stdout);
|
|
44
|
+
}
|
|
45
|
+
// Universal inline suppression: strip added lines opted out with the
|
|
46
|
+
// `pr-checkmate-ignore` directive, so every content check that scans this
|
|
47
|
+
// diff honours it without repeating the check itself.
|
|
48
|
+
return stdout
|
|
49
|
+
.split('\n')
|
|
50
|
+
.filter(line => !(line.startsWith('+') && line.includes('pr-checkmate-ignore')))
|
|
51
|
+
.join('\n');
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Strip a trailing line comment (`//` or `#`) from a line, tracking quote state
|
|
55
|
+
* so a marker inside a string literal (or a `#` in `"#fff"`, a `//` in a quoted
|
|
56
|
+
* URL) is preserved. Heuristic, not a tokenizer: block comments and regex
|
|
57
|
+
* literals aren't handled. Used by code-targeting checks so `# print(x)` or
|
|
58
|
+
* `// eval(x)` in a comment isn't flagged as real code.
|
|
59
|
+
*/
|
|
60
|
+
function stripInlineComment(line) {
|
|
61
|
+
let quote = '';
|
|
62
|
+
for (let i = 0; i < line.length; i++) {
|
|
63
|
+
const c = line[i];
|
|
64
|
+
if (quote) {
|
|
65
|
+
if (c === quote && line[i - 1] !== '\\')
|
|
66
|
+
quote = '';
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
70
|
+
quote = c;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (c === '#')
|
|
74
|
+
return line.slice(0, i);
|
|
75
|
+
if (c === '/' && line[i + 1] === '/')
|
|
76
|
+
return line.slice(0, i);
|
|
77
|
+
}
|
|
78
|
+
return line;
|
|
17
79
|
}
|
|
18
80
|
/** Total number of findings across all labels in a bucket. */
|
|
19
81
|
function bucketTotal(bucket) {
|
|
@@ -34,11 +96,26 @@ function logFindings(bucket, opts) {
|
|
|
34
96
|
}
|
|
35
97
|
}
|
|
36
98
|
}
|
|
99
|
+
/** Remove bucket entries whose label matches any ignore substring. */
|
|
100
|
+
function dropIgnored(bucket, ignore) {
|
|
101
|
+
if (ignore.length === 0)
|
|
102
|
+
return bucket;
|
|
103
|
+
const lowered = ignore.map(i => i.toLowerCase());
|
|
104
|
+
const kept = {};
|
|
105
|
+
for (const [label, items] of Object.entries(bucket)) {
|
|
106
|
+
if (!lowered.some(i => label.toLowerCase().includes(i)))
|
|
107
|
+
kept[label] = items;
|
|
108
|
+
}
|
|
109
|
+
return kept;
|
|
110
|
+
}
|
|
37
111
|
/**
|
|
38
112
|
* Report error/warning finding buckets: log both, then produce the outcome.
|
|
39
113
|
* Returns pass() when both buckets are empty, otherwise warn() with a summary.
|
|
40
114
|
*/
|
|
41
|
-
function reportSeverityBuckets(
|
|
115
|
+
function reportSeverityBuckets(errorsIn, warningsIn, opts) {
|
|
116
|
+
const ignore = opts.ignore ?? [];
|
|
117
|
+
const errors = dropIgnored(errorsIn, ignore);
|
|
118
|
+
const warnings = dropIgnored(warningsIn, ignore);
|
|
42
119
|
const totalErrors = bucketTotal(errors);
|
|
43
120
|
const totalWarnings = bucketTotal(warnings);
|
|
44
121
|
if (totalErrors === 0 && totalWarnings === 0)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.customRulesCheck = void 0;
|
|
4
|
+
const utils_1 = require("../../../utils");
|
|
5
|
+
const execa_1 = require("../../../utils/execa");
|
|
6
|
+
const types_1 = require("../../types");
|
|
7
|
+
const findings_1 = require("../findings");
|
|
8
|
+
/**
|
|
9
|
+
* Compile the user's `customRules` into ready-to-run matchers. A rule with an
|
|
10
|
+
* invalid regex is skipped with a warning rather than crashing the run. `g`/`y`
|
|
11
|
+
* flags are stripped because `.test()` on a sticky/global regex is stateful and
|
|
12
|
+
* would skip matches across lines.
|
|
13
|
+
*/
|
|
14
|
+
function compileRules(ctx) {
|
|
15
|
+
const rules = ctx.config.customRules ?? [];
|
|
16
|
+
const compiled = [];
|
|
17
|
+
for (const r of rules) {
|
|
18
|
+
if (!r?.name || !r?.pattern)
|
|
19
|
+
continue;
|
|
20
|
+
let re;
|
|
21
|
+
try {
|
|
22
|
+
re = new RegExp(r.pattern, (r.flags ?? '').replace(/[gy]/g, ''));
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
utils_1.logger.warn(`[Custom Rules]: ⚠️ invalid regex in rule "${r.name}" — skipped\n${err}`);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
compiled.push({
|
|
29
|
+
message: r.message ?? r.name,
|
|
30
|
+
re,
|
|
31
|
+
severity: r.severity === 'error' ? 'error' : 'warn',
|
|
32
|
+
include: r.include,
|
|
33
|
+
exclude: r.exclude,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return compiled;
|
|
37
|
+
}
|
|
38
|
+
const PREFIX = 'Custom Rules';
|
|
39
|
+
/** Parse the diff, bucketing rule matches on added lines by severity. */
|
|
40
|
+
function scanDiff(stdout, rules) {
|
|
41
|
+
var _a;
|
|
42
|
+
const errors = {};
|
|
43
|
+
const warnings = {};
|
|
44
|
+
let file = '';
|
|
45
|
+
for (const raw of stdout.split('\n')) {
|
|
46
|
+
if (raw.startsWith('+++ ')) {
|
|
47
|
+
const p = raw.slice(4).trim();
|
|
48
|
+
file = p === '/dev/null' ? '' : p.replace(/^b\//, '');
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (!raw.startsWith('+') || raw.startsWith('+++'))
|
|
52
|
+
continue;
|
|
53
|
+
if (raw.includes('pr-checkmate-ignore'))
|
|
54
|
+
continue;
|
|
55
|
+
const content = raw.slice(1);
|
|
56
|
+
for (const rule of rules) {
|
|
57
|
+
if (rule.include && !(0, utils_1.matchesAnyGlob)(file, rule.include))
|
|
58
|
+
continue;
|
|
59
|
+
if (rule.exclude && (0, utils_1.matchesAnyGlob)(file, rule.exclude))
|
|
60
|
+
continue;
|
|
61
|
+
if (rule.re.test(content)) {
|
|
62
|
+
const bucket = rule.severity === 'error' ? errors : warnings;
|
|
63
|
+
(bucket[_a = rule.message] ?? (bucket[_a] = [])).push(`${file}: ${content.trim().substring(0, 100)}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { errors, warnings };
|
|
68
|
+
}
|
|
69
|
+
exports.customRulesCheck = {
|
|
70
|
+
name: 'Custom Rules',
|
|
71
|
+
phase: 'informational',
|
|
72
|
+
applies(ctx) {
|
|
73
|
+
return (ctx.config.customRules?.length ?? 0) > 0 && Boolean(ctx.baseSha);
|
|
74
|
+
},
|
|
75
|
+
async run(ctx) {
|
|
76
|
+
const rules = compileRules(ctx);
|
|
77
|
+
if (rules.length === 0)
|
|
78
|
+
return (0, types_1.pass)('no valid custom rules');
|
|
79
|
+
try {
|
|
80
|
+
const { stdout } = await (0, execa_1.execa)('git', ['diff', '--unified=0', '--diff-filter=ACMR', ctx.baseSha, ctx.headSha ?? 'HEAD'], { cwd: ctx.cwd });
|
|
81
|
+
const { errors, warnings } = scanDiff(stdout, rules);
|
|
82
|
+
const totalErrors = (0, findings_1.bucketTotal)(errors);
|
|
83
|
+
const totalWarnings = (0, findings_1.bucketTotal)(warnings);
|
|
84
|
+
if (totalErrors === 0 && totalWarnings === 0)
|
|
85
|
+
return (0, types_1.pass)();
|
|
86
|
+
(0, findings_1.logFindings)(errors, { prefix: PREFIX, isError: true, sampleLimit: 3 });
|
|
87
|
+
(0, findings_1.logFindings)(warnings, { prefix: PREFIX, sampleLimit: 3 });
|
|
88
|
+
const parts = [];
|
|
89
|
+
if (totalErrors > 0)
|
|
90
|
+
parts.push(`${totalErrors} error(s)`);
|
|
91
|
+
if (totalWarnings > 0)
|
|
92
|
+
parts.push(`${totalWarnings} warning(s)`);
|
|
93
|
+
const summary = parts.join(', ');
|
|
94
|
+
// A rule with severity "error" blocks the run (fail), regardless of phase.
|
|
95
|
+
return totalErrors > 0 ? (0, types_1.fail)(summary) : (0, types_1.warn)(summary);
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
utils_1.logger.warn(`[Custom Rules]: ⚠️ Could not analyze diff\n${err}`);
|
|
99
|
+
return (0, types_1.skip)('diff unavailable');
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Check } from '
|
|
1
|
+
import { Check } from '../../types';
|
|
2
2
|
export declare const diffSmellCheck: Check;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.diffSmellCheck = void 0;
|
|
4
|
-
const utils_1 = require("
|
|
5
|
-
const types_1 = require("
|
|
6
|
-
const findings_1 = require("
|
|
4
|
+
const utils_1 = require("../../../utils");
|
|
5
|
+
const types_1 = require("../../types");
|
|
6
|
+
const findings_1 = require("../findings");
|
|
7
7
|
const SMELL_PATTERNS = [
|
|
8
8
|
{
|
|
9
9
|
pattern: /^\+(?!\+)(?:[^/]|\/[^/]).*\bconsole\.(log|error|warn|debug|info)\s*\(/,
|