pr-checkmate 1.22.7 → 1.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -333
- package/cspell.json +9 -1
- package/dist/cli.js +32 -2
- package/dist/config/constants.d.ts +25 -1
- package/dist/config/constants.js +157 -38
- package/dist/config/index.d.ts +15 -2
- package/dist/config/index.js +28 -7
- package/dist/config/loader.js +5 -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} +9 -10
- 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} +3 -3
- 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 +83 -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} +25 -14
- 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} +10 -10
- 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} +11 -15
- 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} +8 -21
- package/dist/core/checks/{spellcheck.d.ts → quality/spellcheck.d.ts} +1 -1
- package/dist/core/checks/{spellcheck.js → quality/spellcheck.js} +11 -19
- 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} +4 -4
- 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 +54 -10
- package/dist/core/registry.js +73 -28
- package/dist/core/reporters.d.ts +4 -3
- package/dist/core/reporters.js +104 -4
- package/dist/core/run-checks.js +21 -3
- package/dist/core/targets.d.ts +17 -1
- package/dist/core/targets.js +74 -4
- package/dist/core/types.d.ts +13 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -1
- package/dist/init.js +86 -8
- package/dist/interfaces/index.d.ts +101 -1
- package/dist/scripts/copy-configs.js +6 -1
- 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 +1 -1
- package/dist/utils/index.js +1 -1
- package/eslint-default-rules.json +62 -0
- package/eslint.config.mjs +6 -66
- package/package.json +3 -1
- package/pr-checkmate.schema.json +166 -0
- package/src/config/cspell.json +9 -1
- package/src/config/eslint-default-rules.json +62 -0
- package/src/config/pr-checkmate.schema.json +166 -0
- package/dist/core/checks/diff-security.js +0 -145
- package/dist/utils/delta.d.ts +0 -5
- package/dist/utils/delta.js +0 -53
- package/src/config/eslint.config.mjs +0 -180
- /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
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Check } from '
|
|
1
|
+
import { Check } from '../../types';
|
|
2
2
|
export declare const duplicateCheck: Check;
|
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.duplicateCheck = void 0;
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const types_1 = require("../types");
|
|
4
|
+
const config_1 = require("../../../config");
|
|
5
|
+
const constants_1 = require("../../../config/constants");
|
|
6
|
+
const utils_1 = require("../../../utils");
|
|
7
|
+
const execa_1 = require("../../../utils/execa");
|
|
8
|
+
const targets_1 = require("../../targets");
|
|
9
|
+
const types_1 = require("../../types");
|
|
14
10
|
// Always excluded regardless of client config — scanning these never yields
|
|
15
11
|
// meaningful production duplication. Config files (eslint/*.config.*) are data,
|
|
16
12
|
// not logic, and are commonly shipped in two formats (e.g. .eslintrc + flat
|
|
@@ -44,11 +40,11 @@ exports.duplicateCheck = {
|
|
|
44
40
|
async run(ctx) {
|
|
45
41
|
const minLines = ctx.config.duplicate?.minLines ?? 5;
|
|
46
42
|
const minTokens = ctx.config.duplicate?.minTokens ?? 50;
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
49
|
-
return (0, types_1.skip)('
|
|
50
|
-
|
|
51
|
-
|
|
43
|
+
const targetPaths = await (0, targets_1.resolveScopedTargetFiles)(ctx, ['*.ts', '*.tsx', '*.js', '*.jsx'], (0, config_1.getSourcePaths)());
|
|
44
|
+
if (targetPaths === null)
|
|
45
|
+
return (0, types_1.skip)('git unavailable');
|
|
46
|
+
if (targetPaths.length === 0)
|
|
47
|
+
return (0, types_1.skip)('no relevant files');
|
|
52
48
|
const jscpdBin = (0, utils_1.resolvePackageBin)('jscpd', 'run-jscpd.js');
|
|
53
49
|
// Client-configurable exclusions (pr-checkmate.json `duplicate.ignore`),
|
|
54
50
|
// defaulting to test files/folders, appended to the always-on base ignores.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Check } from '../../types';
|
|
2
|
+
/**
|
|
3
|
+
* Requires a license/copyright header (a configured regex) in the first N lines
|
|
4
|
+
* of newly ADDED source files. Opt-in — needs `licenseHeader.pattern`; does
|
|
5
|
+
* nothing until a repo configures one. Advisory (warn).
|
|
6
|
+
*/
|
|
7
|
+
export declare const licenseHeaderCheck: Check;
|
|
@@ -0,0 +1,74 @@
|
|
|
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.licenseHeaderCheck = void 0;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const utils_1 = require("../../../utils");
|
|
10
|
+
const execa_1 = require("../../../utils/execa");
|
|
11
|
+
const types_1 = require("../../types");
|
|
12
|
+
/** Source extensions a license header is expected on. */
|
|
13
|
+
const SOURCE_EXT = /\.(ts|tsx|js|jsx|mjs|cjs|py|kt|java|go|rs|cs|rb|php|swift|c|cc|cpp|h|hpp)$/i;
|
|
14
|
+
const DEFAULT_HEADER_LINES = 10;
|
|
15
|
+
/**
|
|
16
|
+
* Requires a license/copyright header (a configured regex) in the first N lines
|
|
17
|
+
* of newly ADDED source files. Opt-in — needs `licenseHeader.pattern`; does
|
|
18
|
+
* nothing until a repo configures one. Advisory (warn).
|
|
19
|
+
*/
|
|
20
|
+
exports.licenseHeaderCheck = {
|
|
21
|
+
name: 'License Header',
|
|
22
|
+
phase: 'informational',
|
|
23
|
+
applies(ctx) {
|
|
24
|
+
return ctx.config.licenseHeader?.enabled === true && Boolean(ctx.baseSha);
|
|
25
|
+
},
|
|
26
|
+
async run(ctx) {
|
|
27
|
+
const patternSrc = ctx.config.licenseHeader?.pattern;
|
|
28
|
+
if (!patternSrc)
|
|
29
|
+
return (0, types_1.skip)('no licenseHeader.pattern configured');
|
|
30
|
+
let header;
|
|
31
|
+
try {
|
|
32
|
+
header = new RegExp(patternSrc);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
utils_1.logger.warn(`[License Header]: ⚠️ invalid pattern\n${err}`);
|
|
36
|
+
return (0, types_1.skip)('invalid licenseHeader.pattern');
|
|
37
|
+
}
|
|
38
|
+
const maxLines = ctx.config.licenseHeader?.lines ?? DEFAULT_HEADER_LINES;
|
|
39
|
+
let added;
|
|
40
|
+
try {
|
|
41
|
+
const { stdout } = await (0, execa_1.execa)('git', ['diff', '--name-only', '--diff-filter=A', ctx.baseSha, ctx.headSha ?? 'HEAD'], { cwd: ctx.cwd });
|
|
42
|
+
added = stdout
|
|
43
|
+
.split('\n')
|
|
44
|
+
.filter(Boolean)
|
|
45
|
+
.filter(f => SOURCE_EXT.test(f));
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
utils_1.logger.warn(`[License Header]: ⚠️ Could not analyze diff\n${err}`);
|
|
49
|
+
return (0, types_1.skip)('diff unavailable');
|
|
50
|
+
}
|
|
51
|
+
const missing = [];
|
|
52
|
+
for (const file of added) {
|
|
53
|
+
let head;
|
|
54
|
+
try {
|
|
55
|
+
head = node_fs_1.default
|
|
56
|
+
.readFileSync(node_path_1.default.join(ctx.cwd, file), 'utf-8')
|
|
57
|
+
.split('\n')
|
|
58
|
+
.slice(0, maxLines)
|
|
59
|
+
.join('\n');
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
continue; // removed/renamed between diff and read
|
|
63
|
+
}
|
|
64
|
+
if (!header.test(head))
|
|
65
|
+
missing.push(file);
|
|
66
|
+
}
|
|
67
|
+
if (missing.length === 0)
|
|
68
|
+
return (0, types_1.pass)();
|
|
69
|
+
missing.slice(0, 10).forEach(f => utils_1.logger.warn(`[License Header]: ⚠️ missing header: ${f}`));
|
|
70
|
+
if (missing.length > 10)
|
|
71
|
+
utils_1.logger.warn(`[License Header]: ⚠️ ... and ${missing.length - 10} more`);
|
|
72
|
+
return (0, types_1.warn)(`${missing.length} new source file(s) missing the license header`);
|
|
73
|
+
},
|
|
74
|
+
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Check } from '
|
|
1
|
+
import { Check } from '../../types';
|
|
2
2
|
export declare const markdownLintCheck: Check;
|
|
@@ -7,11 +7,11 @@ exports.markdownLintCheck = void 0;
|
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
8
|
const node_os_1 = __importDefault(require("node:os"));
|
|
9
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
-
const config_1 = require("
|
|
11
|
-
const utils_1 = require("
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const types_1 = require("
|
|
10
|
+
const config_1 = require("../../../config");
|
|
11
|
+
const utils_1 = require("../../../utils");
|
|
12
|
+
const execa_1 = require("../../../utils/execa");
|
|
13
|
+
const targets_1 = require("../../targets");
|
|
14
|
+
const types_1 = require("../../types");
|
|
15
15
|
function loadUserMarkdownConfig(cwd) {
|
|
16
16
|
const configPath = node_path_1.default.join(cwd, 'pr-checkmate.json');
|
|
17
17
|
if (!node_fs_1.default.existsSync(configPath))
|
|
@@ -31,21 +31,6 @@ function buildMergedConfig(bundledConfigPath, userRules) {
|
|
|
31
31
|
node_fs_1.default.writeFileSync(tmpFile, JSON.stringify(merged, null, 2));
|
|
32
32
|
return tmpFile;
|
|
33
33
|
}
|
|
34
|
-
async function resolveTargets(cwd) {
|
|
35
|
-
const changedFiles = await (0, delta_1.getChangedFiles)(['.md', '.mdx']);
|
|
36
|
-
if (changedFiles !== null)
|
|
37
|
-
return changedFiles;
|
|
38
|
-
try {
|
|
39
|
-
const { stdout } = await (0, execa_1.execa)('git', ['ls-files', '*.md', '**/*.md'], { cwd, reject: false });
|
|
40
|
-
return stdout
|
|
41
|
-
.split('\n')
|
|
42
|
-
.map(f => f.trim())
|
|
43
|
-
.filter(f => f.endsWith('.md'));
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
return [`${cwd}/**/*.md`];
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
34
|
exports.markdownLintCheck = {
|
|
50
35
|
name: 'Markdown',
|
|
51
36
|
phase: 'informational',
|
|
@@ -53,7 +38,9 @@ exports.markdownLintCheck = {
|
|
|
53
38
|
return true;
|
|
54
39
|
},
|
|
55
40
|
async run(ctx) {
|
|
56
|
-
const targets = await
|
|
41
|
+
const targets = await (0, targets_1.resolveTargetFiles)(ctx, ['*.md', '*.mdx']);
|
|
42
|
+
if (targets === null)
|
|
43
|
+
return (0, types_1.skip)('git unavailable');
|
|
57
44
|
if (targets.length === 0)
|
|
58
45
|
return (0, types_1.skip)('no markdown files');
|
|
59
46
|
const userConfig = loadUserMarkdownConfig(ctx.cwd);
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Check } from '
|
|
1
|
+
import { Check } from '../../types';
|
|
2
2
|
export declare const spellcheckCheck: Check;
|
|
@@ -6,11 +6,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.spellcheckCheck = void 0;
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
-
const config_1 = require("
|
|
10
|
-
const utils_1 = require("
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const types_1 = require("
|
|
9
|
+
const config_1 = require("../../../config");
|
|
10
|
+
const utils_1 = require("../../../utils");
|
|
11
|
+
const execa_1 = require("../../../utils/execa");
|
|
12
|
+
const targets_1 = require("../../targets");
|
|
13
|
+
const types_1 = require("../../types");
|
|
14
14
|
function loadUserCspellConfig(cwd) {
|
|
15
15
|
const configPath = node_path_1.default.join(cwd, 'pr-checkmate.json');
|
|
16
16
|
if (!node_fs_1.default.existsSync(configPath))
|
|
@@ -57,20 +57,12 @@ exports.spellcheckCheck = {
|
|
|
57
57
|
return true;
|
|
58
58
|
},
|
|
59
59
|
async run(ctx) {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
'.md',
|
|
67
|
-
'.mdx',
|
|
68
|
-
'.json',
|
|
69
|
-
]);
|
|
70
|
-
const patterns = changedFiles ?? [
|
|
71
|
-
node_path_1.default.join(ctx.cwd, sourcePath, '**/*.{ts,tsx,js,jsx,md,mdx,json}'),
|
|
72
|
-
...config_1.IGNORE_PATTERNS.map(p => `--exclude=${p}`),
|
|
73
|
-
];
|
|
60
|
+
const files = await (0, targets_1.resolveScopedTargetFiles)(ctx, ['*.ts', '*.tsx', '*.js', '*.jsx', '*.md', '*.mdx', '*.json'], (0, config_1.getSourcePaths)());
|
|
61
|
+
if (files === null)
|
|
62
|
+
return (0, types_1.skip)('git unavailable');
|
|
63
|
+
if (files.length === 0)
|
|
64
|
+
return (0, types_1.pass)('no files to spellcheck');
|
|
65
|
+
const patterns = [...files, ...config_1.IGNORE_PATTERNS.map(p => `--exclude=${p}`)];
|
|
74
66
|
const hasLocal = (0, config_1.hasLocalConfig)(['cspell.json', '.cspell.json', 'cspell.config.json']);
|
|
75
67
|
let tempConfigPath = null;
|
|
76
68
|
const cspellBin = (0, utils_1.resolvePackageBin)('cspell', 'bin.mjs');
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Check } from '
|
|
1
|
+
import { Check } from '../../types';
|
|
2
2
|
export declare const diffSecurityCheck: Check;
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.diffSecurityCheck = void 0;
|
|
4
|
+
const utils_1 = require("../../../utils");
|
|
5
|
+
const types_1 = require("../../types");
|
|
6
|
+
const findings_1 = require("../findings");
|
|
7
|
+
const MD5_LABEL = 'MD5 (weak hash — use sha256 or stronger)';
|
|
8
|
+
const SHA1_LABEL = 'SHA-1 (weak hash — use sha256 or stronger)';
|
|
9
|
+
const SECURITY_PATTERNS = [
|
|
10
|
+
{
|
|
11
|
+
pattern: /^\+(?!\+).*dangerouslySetInnerHTML\s*=/,
|
|
12
|
+
label: 'dangerouslySetInnerHTML (XSS risk)',
|
|
13
|
+
severity: 'error',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
pattern: /^\+(?!\+).*\.innerHTML\s*=/,
|
|
17
|
+
label: 'innerHTML assignment (XSS risk)',
|
|
18
|
+
severity: 'warn',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
pattern: /^\+(?!\+).*document\.write\s*\(/,
|
|
22
|
+
label: 'document.write() (XSS risk)',
|
|
23
|
+
severity: 'error',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
pattern: /^\+(?!\+)(?![^'"`]*['"`]).*\beval\s*\(/,
|
|
27
|
+
label: 'eval() (code injection risk)',
|
|
28
|
+
severity: 'error',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
pattern: /^\+(?!\+).*\bnew\s+Function\s*\(/,
|
|
32
|
+
label: 'new Function() (code injection risk)',
|
|
33
|
+
severity: 'error',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
pattern: /^\+(?!\+).*__proto__\s*[=:[]/,
|
|
37
|
+
label: '__proto__ mutation (prototype pollution)',
|
|
38
|
+
severity: 'error',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
pattern: /^\+(?!\+).*\.constructor\.prototype\b/,
|
|
42
|
+
label: 'constructor.prototype access (prototype pollution)',
|
|
43
|
+
severity: 'warn',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
pattern: /^\+(?!\+).*createHash\s*\(\s*['"]md5['"]/,
|
|
47
|
+
label: MD5_LABEL,
|
|
48
|
+
severity: 'warn',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
pattern: /^\+(?!\+).*createHash\s*\(\s*['"]sha1['"]/,
|
|
52
|
+
label: SHA1_LABEL,
|
|
53
|
+
severity: 'warn',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
pattern: /^\+(?!\+).*\bcreateDecipher\s*\(/,
|
|
57
|
+
label: 'deprecated createDecipher() — use createDecipheriv()',
|
|
58
|
+
severity: 'warn',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
pattern: /^\+(?!\+).*\bcreateCipher\s*\(/,
|
|
62
|
+
label: 'deprecated createCipher() — use createCipheriv()',
|
|
63
|
+
severity: 'warn',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
pattern: /^\+(?!\+).*cors\s*\(\s*\{[^}]*origin\s*:\s*['"]\*['"]/,
|
|
67
|
+
label: "CORS wildcard origin: '*' (allows any domain)",
|
|
68
|
+
severity: 'warn',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
pattern: /^\+(?!\+).*['"]Access-Control-Allow-Origin['"]\s*[,:]\s*['"]\*['"]/,
|
|
72
|
+
label: "Access-Control-Allow-Origin: '*' header (CORS wildcard)",
|
|
73
|
+
severity: 'warn',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
pattern: /^\+(?!\+).*(fetch|axios\.get|axios\.post|axios\.put|axios\.delete|http\.get|http\.request)\s*\(\s*['"]http:\/\//,
|
|
77
|
+
label: 'Insecure HTTP (non-HTTPS) in external call',
|
|
78
|
+
severity: 'warn',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
pattern: /^\+(?!\+).*(exec|execSync|spawn|spawnSync)\s*\(\s*`[^`]*\$\{/,
|
|
82
|
+
label: 'exec/spawn with template literal (command injection risk)',
|
|
83
|
+
severity: 'error',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
pattern: /^\+(?!\+).*(exec|execSync)\s*\(\s*['"][^'"]*['"\s]*\+/,
|
|
87
|
+
label: 'exec/spawn with string concatenation (command injection risk)',
|
|
88
|
+
severity: 'error',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
pattern: /^\+(?!\+).*\b(query|execute|raw)\s*\(\s*`[^`]*\$\{/,
|
|
92
|
+
label: 'SQL query with template literal interpolation (SQL injection risk)',
|
|
93
|
+
severity: 'error',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
// A quoted string that contains a whole SQL keyword, concatenated with a
|
|
97
|
+
// variable — the real injection shape. Word boundaries + the quote context
|
|
98
|
+
// stop identifiers like `insertions + deletions` from false-positiving.
|
|
99
|
+
pattern: /^\+(?!\+).*['"`][^'"`]*\b(SELECT|INSERT|UPDATE|DELETE|WHERE)\b[^'"`]*['"`]\s*\+\s*\w/i,
|
|
100
|
+
label: 'SQL string concatenation (SQL injection risk)',
|
|
101
|
+
severity: 'error',
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
/** Python-specific dangerous constructs, scanned in *.py diffs. */
|
|
105
|
+
const PYTHON_SECURITY_PATTERNS = [
|
|
106
|
+
{ pattern: /^\+(?!\+).*\beval\s*\(/, label: 'eval() (code injection risk)', severity: 'error' },
|
|
107
|
+
{ pattern: /^\+(?!\+).*\bexec\s*\(/, label: 'exec() (code injection risk)', severity: 'error' },
|
|
108
|
+
{
|
|
109
|
+
pattern: /^\+(?!\+).*\bos\.system\s*\(/,
|
|
110
|
+
label: 'os.system() (command injection risk)',
|
|
111
|
+
severity: 'error',
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
pattern: /^\+(?!\+).*\bsubprocess\.\w+\([^)]*shell\s*=\s*True/,
|
|
115
|
+
label: 'subprocess with shell=True (command injection risk)',
|
|
116
|
+
severity: 'error',
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
pattern: /^\+(?!\+).*\bpickle\.loads?\s*\(/,
|
|
120
|
+
label: 'pickle load (insecure deserialization)',
|
|
121
|
+
severity: 'error',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
pattern: /^\+(?!\+).*\byaml\.load\s*\((?![^)]*Safe)/,
|
|
125
|
+
label: 'yaml.load without SafeLoader (unsafe deserialization)',
|
|
126
|
+
severity: 'warn',
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
pattern: /^\+(?!\+).*\bhashlib\.md5\s*\(/,
|
|
130
|
+
label: MD5_LABEL,
|
|
131
|
+
severity: 'warn',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
pattern: /^\+(?!\+).*\bhashlib\.sha1\s*\(/,
|
|
135
|
+
label: SHA1_LABEL,
|
|
136
|
+
severity: 'warn',
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
pattern: /^\+(?!\+).*\bverify\s*=\s*False\b/,
|
|
140
|
+
label: 'TLS verification disabled (verify=False)',
|
|
141
|
+
severity: 'warn',
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
pattern: /^\+(?!\+).*['"][^'"]*\b(SELECT|INSERT|UPDATE|DELETE)\b[^'"]*['"]\s*%\s*/i,
|
|
145
|
+
label: 'SQL string formatting (SQL injection risk)',
|
|
146
|
+
severity: 'error',
|
|
147
|
+
},
|
|
148
|
+
];
|
|
149
|
+
/** Go-specific dangerous constructs, scanned in *.go diffs. */
|
|
150
|
+
const GO_SECURITY_PATTERNS = [
|
|
151
|
+
{
|
|
152
|
+
pattern: /^\+(?!\+).*\bmd5\.New\s*\(/,
|
|
153
|
+
label: MD5_LABEL,
|
|
154
|
+
severity: 'warn',
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
pattern: /^\+(?!\+).*\bsha1\.New\s*\(/,
|
|
158
|
+
label: SHA1_LABEL,
|
|
159
|
+
severity: 'warn',
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
pattern: /^\+(?!\+).*InsecureSkipVerify\s*:\s*true/,
|
|
163
|
+
label: 'InsecureSkipVerify: true (TLS verification disabled)',
|
|
164
|
+
severity: 'error',
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
pattern: /^\+(?!\+).*\bexec\.Command\s*\([^)]*\+/,
|
|
168
|
+
label: 'exec.Command with string concatenation (command injection risk)',
|
|
169
|
+
severity: 'warn',
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
pattern: /^\+(?!\+).*\bfmt\.Sprintf\s*\([^)]*\b(SELECT|INSERT|UPDATE|DELETE)\b/i,
|
|
173
|
+
label: 'SQL built with fmt.Sprintf (SQL injection risk)',
|
|
174
|
+
severity: 'warn',
|
|
175
|
+
},
|
|
176
|
+
{ pattern: /^\+(?!\+).*"crypto\/des"/, label: 'DES (weak cipher)', severity: 'warn' },
|
|
177
|
+
];
|
|
178
|
+
/** Kotlin/Java-specific dangerous constructs, scanned in *.kt/*.kts/*.java diffs. */
|
|
179
|
+
const KOTLIN_SECURITY_PATTERNS = [
|
|
180
|
+
{
|
|
181
|
+
pattern: /^\+(?!\+).*\bRuntime\.getRuntime\(\)\.exec\s*\(/,
|
|
182
|
+
label: 'Runtime.exec() (command injection risk)',
|
|
183
|
+
severity: 'error',
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
pattern: /^\+(?!\+).*\bMessageDigest\.getInstance\s*\(\s*"(MD5|SHA-1)"/,
|
|
187
|
+
label: 'weak hash (MD5/SHA-1 via MessageDigest — use SHA-256+)',
|
|
188
|
+
severity: 'warn',
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
pattern: /^\+(?!\+).*\bCipher\.getInstance\s*\(\s*"(DES|.*ECB)/,
|
|
192
|
+
label: 'weak cipher (DES / ECB mode)',
|
|
193
|
+
severity: 'warn',
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
pattern: /^\+(?!\+).*ALLOW_ALL_HOSTNAME_VERIFIER/,
|
|
197
|
+
label: 'ALLOW_ALL_HOSTNAME_VERIFIER (TLS hostname verification disabled)',
|
|
198
|
+
severity: 'error',
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
pattern: /^\+(?!\+).*\bObjectInputStream\b/,
|
|
202
|
+
label: 'ObjectInputStream (insecure deserialization risk)',
|
|
203
|
+
severity: 'warn',
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
pattern: /^\+(?!\+).*"[^"]*\b(SELECT|INSERT|UPDATE|DELETE)\b[^"]*"\s*\+\s*\w/i,
|
|
207
|
+
label: 'SQL string concatenation (SQL injection risk)',
|
|
208
|
+
severity: 'error',
|
|
209
|
+
},
|
|
210
|
+
];
|
|
211
|
+
const JS_GLOBS = ['*.ts', '*.tsx', '*.js', '*.jsx', '*.mjs', '*.cjs'];
|
|
212
|
+
/** Scan a diff with a pattern set, accumulating matches into severity buckets. */
|
|
213
|
+
function scanInto(stdout, patterns, errors, warnings) {
|
|
214
|
+
for (const line of stdout.split('\n')) {
|
|
215
|
+
// Match against the comment-stripped line so `eval(x)` in a comment isn't
|
|
216
|
+
// flagged, but report the original line for context.
|
|
217
|
+
const scanned = (0, findings_1.stripInlineComment)(line);
|
|
218
|
+
for (const { pattern, label, severity } of patterns) {
|
|
219
|
+
if (pattern.test(scanned)) {
|
|
220
|
+
const bucket = severity === 'error' ? errors : warnings;
|
|
221
|
+
(bucket[label] ?? (bucket[label] = [])).push(line.slice(1).trim().substring(0, 120));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
exports.diffSecurityCheck = {
|
|
227
|
+
name: 'Diff Security',
|
|
228
|
+
phase: 'informational',
|
|
229
|
+
applies(ctx) {
|
|
230
|
+
return ctx.config.diffSecurity?.enabled !== false && Boolean(ctx.baseSha);
|
|
231
|
+
},
|
|
232
|
+
async run(ctx) {
|
|
233
|
+
try {
|
|
234
|
+
const errors = {};
|
|
235
|
+
const warnings = {};
|
|
236
|
+
// JS/TS patterns always run; language-specific sets scan their own globs
|
|
237
|
+
// only when that language is present, so a Python repo gets Python rules.
|
|
238
|
+
scanInto(await (0, findings_1.diffAddedLines)(ctx, JS_GLOBS), SECURITY_PATTERNS, errors, warnings);
|
|
239
|
+
if (ctx.languages.includes('python')) {
|
|
240
|
+
scanInto(await (0, findings_1.diffAddedLines)(ctx, ['*.py']), PYTHON_SECURITY_PATTERNS, errors, warnings);
|
|
241
|
+
}
|
|
242
|
+
if (ctx.languages.includes('go')) {
|
|
243
|
+
scanInto(await (0, findings_1.diffAddedLines)(ctx, ['*.go']), GO_SECURITY_PATTERNS, errors, warnings);
|
|
244
|
+
}
|
|
245
|
+
if (ctx.languages.includes('kotlin')) {
|
|
246
|
+
const ktOut = await (0, findings_1.diffAddedLines)(ctx, ['*.kt', '*.kts', '*.java']);
|
|
247
|
+
scanInto(ktOut, KOTLIN_SECURITY_PATTERNS, errors, warnings);
|
|
248
|
+
}
|
|
249
|
+
return (0, findings_1.reportSeverityBuckets)(errors, warnings, {
|
|
250
|
+
prefix: 'Diff Security',
|
|
251
|
+
errorSummary: n => `${n} issue(s)`,
|
|
252
|
+
warningSummary: n => `${n} warning(s)`,
|
|
253
|
+
sampleLimit: 3,
|
|
254
|
+
ignore: ctx.config.diffSecurity?.ignore,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
utils_1.logger.warn(`[Diff Security]: ⚠️ Could not analyze diff\n${err}`);
|
|
259
|
+
return (0, types_1.skip)('diff unavailable');
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.dockerfileSecurityCheck = void 0;
|
|
4
|
+
const utils_1 = require("../../../utils");
|
|
5
|
+
const execa_1 = require("../../../utils/execa");
|
|
6
|
+
const targets_1 = require("../../targets");
|
|
7
|
+
const types_1 = require("../../types");
|
|
8
|
+
const findings_1 = require("../findings");
|
|
9
|
+
const DOCKERFILE_GLOBS = ['**/Dockerfile', '**/Dockerfile.*', '**/*.Dockerfile'];
|
|
10
|
+
/** Common Dockerfile security smells, matched on added diff lines. */
|
|
11
|
+
const DOCKER_PATTERNS = [
|
|
12
|
+
{
|
|
13
|
+
pattern: /^\+(?!\+)\s*FROM\s+\S+:latest\b/i,
|
|
14
|
+
label: 'FROM …:latest (pin the base image to a version/digest)',
|
|
15
|
+
severity: 'warn',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
pattern: /^\+(?!\+)\s*USER\s+root\b/i,
|
|
19
|
+
label: 'USER root (run the container as a non-root user)',
|
|
20
|
+
severity: 'warn',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
pattern: /^\+(?!\+).*\b(?:curl|wget)\b[^|]*\|\s*(?:sudo\s+)?(?:sh|bash)\b/i,
|
|
24
|
+
label: 'piping a download straight to a shell (curl/wget … | sh)',
|
|
25
|
+
severity: 'warn',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
pattern: /^\+(?!\+)\s*ADD\s+https?:\/\//i,
|
|
29
|
+
label: 'ADD from a URL (prefer COPY, or verify a checksum)',
|
|
30
|
+
severity: 'warn',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
pattern: /^\+(?!\+)\s*(?:ENV|ARG)\s+\w*(?:PASSWORD|SECRET|TOKEN|API_?KEY|ACCESS_KEY)\w*\s*[=\s]/i,
|
|
34
|
+
label: 'secret baked into ENV/ARG (use build secrets / runtime injection)',
|
|
35
|
+
severity: 'warn',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
pattern: /^\+(?!\+).*(?:--no-check-certificate|--insecure)\b/,
|
|
39
|
+
label: 'insecure download flag (--insecure / --no-check-certificate)',
|
|
40
|
+
severity: 'warn',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
pattern: /^\+(?!\+)\s*RUN\s+.*\bsudo\b/,
|
|
44
|
+
label: 'sudo inside RUN (unnecessary and risky in a container build)',
|
|
45
|
+
severity: 'warn',
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
function scanDiff(stdout) {
|
|
49
|
+
const errors = {};
|
|
50
|
+
const warnings = {};
|
|
51
|
+
for (const line of stdout.split('\n')) {
|
|
52
|
+
if (line.includes('pr-checkmate-ignore'))
|
|
53
|
+
continue;
|
|
54
|
+
for (const { pattern, label, severity } of DOCKER_PATTERNS) {
|
|
55
|
+
if (pattern.test(line)) {
|
|
56
|
+
const bucket = severity === 'error' ? errors : warnings;
|
|
57
|
+
(bucket[label] ?? (bucket[label] = [])).push(line.slice(1).trim().substring(0, 120));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { errors, warnings };
|
|
62
|
+
}
|
|
63
|
+
exports.dockerfileSecurityCheck = {
|
|
64
|
+
name: 'Dockerfile Security',
|
|
65
|
+
phase: 'informational',
|
|
66
|
+
applies(ctx) {
|
|
67
|
+
return ctx.config.dockerfileSecurity?.enabled !== false && Boolean(ctx.baseSha);
|
|
68
|
+
},
|
|
69
|
+
async run(ctx) {
|
|
70
|
+
const files = await (0, targets_1.resolveTargetFiles)(ctx, ['*']);
|
|
71
|
+
if (files === null)
|
|
72
|
+
return (0, types_1.skip)('git unavailable');
|
|
73
|
+
const dockerfiles = files.filter(f => (0, utils_1.matchesAnyGlob)(f, DOCKERFILE_GLOBS));
|
|
74
|
+
if (dockerfiles.length === 0)
|
|
75
|
+
return (0, types_1.pass)();
|
|
76
|
+
try {
|
|
77
|
+
const { stdout } = await (0, execa_1.execa)('git', ['diff', '--unified=0', ctx.baseSha, ctx.headSha ?? 'HEAD', '--', ...dockerfiles], { cwd: ctx.cwd });
|
|
78
|
+
const { errors, warnings } = scanDiff(stdout);
|
|
79
|
+
return (0, findings_1.reportSeverityBuckets)(errors, warnings, {
|
|
80
|
+
prefix: 'Dockerfile Security',
|
|
81
|
+
errorSummary: n => `${n} issue(s)`,
|
|
82
|
+
warningSummary: n => `${n} warning(s)`,
|
|
83
|
+
sampleLimit: 3,
|
|
84
|
+
ignore: ctx.config.dockerfileSecurity?.ignore,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
utils_1.logger.warn(`[Dockerfile Security]: ⚠️ Could not analyze diff\n${err}`);
|
|
89
|
+
return (0, types_1.skip)('diff unavailable');
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
};
|