pr-checkmate 2.1.0 → 3.0.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 +63 -267
- package/cspell.json +155 -133
- package/dist/config/constants.d.ts +13 -1
- package/dist/config/constants.js +20 -7
- package/dist/config/index.d.ts +23 -11
- package/dist/config/index.js +27 -33
- package/dist/core/checks/dependencies/banned-imports.js +4 -1
- package/dist/core/checks/dependencies/circular-deps.js +14 -5
- package/dist/core/checks/dependencies/dependency-check.js +2 -2
- package/dist/core/checks/dependencies/grype-scan.js +2 -1
- package/dist/core/checks/dependencies/license-check.js +1 -6
- package/dist/core/checks/dependencies/npm-audit.js +1 -1
- package/dist/core/checks/dependencies/outdated-deps.js +1 -6
- package/dist/core/checks/dependencies/vuln-scan.js +2 -1
- package/dist/core/checks/findings.d.ts +46 -2
- package/dist/core/checks/findings.js +140 -2
- package/dist/core/checks/git/custom-rules.js +2 -0
- package/dist/core/checks/git/diff-smell.js +6 -1
- package/dist/core/checks/git/leftover-debug.js +8 -2
- package/dist/core/checks/git/lockfile-drift.js +6 -1
- package/dist/core/checks/git/merge-conflict.js +1 -1
- package/dist/core/checks/git/missing-tests.js +1 -1
- package/dist/core/checks/git/todo-fixme.js +10 -2
- package/dist/core/checks/languages/cpp-format.js +19 -42
- package/dist/core/checks/languages/external-checks.d.ts +77 -0
- package/dist/core/checks/languages/external-checks.js +200 -0
- package/dist/core/checks/languages/external-runner.d.ts +119 -0
- package/dist/core/checks/languages/external-runner.js +143 -0
- package/dist/core/checks/languages/go-format.d.ts +4 -4
- package/dist/core/checks/languages/go-format.js +151 -30
- package/dist/core/checks/languages/lint.js +19 -19
- package/dist/core/checks/languages/prettier.js +2 -7
- package/dist/core/checks/languages/python-typecheck.d.ts +10 -5
- package/dist/core/checks/languages/python-typecheck.js +161 -37
- package/dist/core/checks/languages/typecheck.js +10 -8
- package/dist/core/checks/pr/commitlint.js +7 -1
- package/dist/core/checks/quality/case-collision.js +6 -4
- package/dist/core/checks/quality/config-validation.d.ts +0 -7
- package/dist/core/checks/quality/config-validation.js +30 -0
- package/dist/core/checks/quality/duplicate-check.js +86 -28
- package/dist/core/checks/quality/license-header.js +7 -4
- package/dist/core/checks/quality/markdown-lint.js +1 -6
- package/dist/core/checks/quality/spellcheck.js +34 -5
- package/dist/core/checks/quality/symlinks.js +6 -4
- package/dist/core/checks/security/diff-security.js +26 -18
- package/dist/core/checks/security/dockerfile-security.js +7 -1
- package/dist/core/checks/security/migration-safety.js +32 -4
- package/dist/core/checks/security/security.d.ts +56 -0
- package/dist/core/checks/security/security.js +135 -11
- package/dist/core/checks/security/sensitive-file-guard.js +40 -6
- package/dist/core/checks/security/workflow-security.js +7 -1
- package/dist/core/checks/tool-install.d.ts +102 -0
- package/dist/core/checks/tool-install.js +69 -0
- package/dist/core/context.js +9 -1
- package/dist/core/install/cache.js +42 -11
- package/dist/core/install/doctor.d.ts +2 -2
- package/dist/core/install/doctor.js +17 -16
- package/dist/core/install/ensure.js +22 -3
- package/dist/core/install/lock.d.ts +2 -1
- package/dist/core/install/prepare.d.ts +75 -0
- package/dist/core/install/prepare.js +151 -0
- package/dist/core/install/profiles.d.ts +17 -3
- package/dist/core/install/profiles.js +27 -0
- package/dist/core/install/reuse.d.ts +41 -0
- package/dist/core/install/reuse.js +107 -0
- package/dist/core/registry.js +10 -18
- package/dist/core/reporters.js +8 -2
- package/dist/core/run-checks.js +74 -3
- package/dist/core/targets.d.ts +116 -5
- package/dist/core/targets.js +101 -16
- package/dist/core/types.d.ts +26 -2
- package/dist/core/types.js +28 -0
- package/dist/init.js +21 -1
- package/dist/interfaces/index.d.ts +1 -0
- package/dist/utils/detect-language.js +11 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/package-version.d.ts +4 -0
- package/dist/utils/package-version.js +41 -0
- package/dist/utils/resolve-bin.d.ts +56 -6
- package/dist/utils/resolve-bin.js +136 -26
- package/dist/utils/update-notifier.js +5 -7
- package/gitleaks.toml +14 -0
- package/install-manifests/go/package-lock.json +24 -0
- package/install-manifests/go/package.json +8 -0
- package/install-manifests/pytypes/package-lock.json +45 -0
- package/install-manifests/pytypes/package.json +8 -0
- package/package.json +4 -2
- package/pr-checkmate.schema.json +8 -2
- package/src/config/cspell.json +155 -133
- package/src/config/pr-checkmate.schema.json +8 -2
- package/dist/core/checks/languages/csharp-format.d.ts +0 -13
- package/dist/core/checks/languages/csharp-format.js +0 -52
- package/dist/core/checks/languages/go-lint.d.ts +0 -8
- package/dist/core/checks/languages/go-lint.js +0 -48
- package/dist/core/checks/languages/kotlin-lint.d.ts +0 -2
- package/dist/core/checks/languages/kotlin-lint.js +0 -42
- package/dist/core/checks/languages/php-format.d.ts +0 -9
- package/dist/core/checks/languages/php-format.js +0 -49
- package/dist/core/checks/languages/ruby-lint.d.ts +0 -8
- package/dist/core/checks/languages/ruby-lint.js +0 -50
- package/dist/core/checks/languages/rust-format.d.ts +0 -9
- package/dist/core/checks/languages/rust-format.js +0 -45
- package/dist/core/checks/languages/rust-lint.d.ts +0 -8
- package/dist/core/checks/languages/rust-lint.js +0 -50
- package/dist/core/checks/languages/shellcheck.d.ts +0 -8
- package/dist/core/checks/languages/shellcheck.js +0 -55
- package/dist/core/checks/languages/swift-lint.d.ts +0 -2
- package/dist/core/checks/languages/swift-lint.js +0 -46
|
@@ -112,6 +112,35 @@ function validateFailOn(json) {
|
|
|
112
112
|
* checks what the user actually wrote, not the merged defaults. `//`-comment keys
|
|
113
113
|
* and `$schema` are ignored.
|
|
114
114
|
*/
|
|
115
|
+
/**
|
|
116
|
+
* Report a `sourcePath` that points nowhere.
|
|
117
|
+
*
|
|
118
|
+
* Every scoped check restricts itself to these directories, so a path that does
|
|
119
|
+
* not exist silences them — and silences them as a PASS. Measured on a fixture
|
|
120
|
+
* with `"sourcePath": "src"` and the code in `app/`: a real conflict marker in
|
|
121
|
+
* `app/conflict.py` went unreported while Merge Conflict, a blocking check,
|
|
122
|
+
* printed ✅. Nothing else in the run mentioned that most of the repository had
|
|
123
|
+
* not been looked at.
|
|
124
|
+
*
|
|
125
|
+
* `init` defaults to `.` and recommends it, so this is not the common case — it
|
|
126
|
+
* is what happens when someone picks `src` at setup, or moves the code later, and
|
|
127
|
+
* that is precisely when nobody is looking for it.
|
|
128
|
+
*/
|
|
129
|
+
function validateSourcePath(json, cwd) {
|
|
130
|
+
const raw = json.sourcePath;
|
|
131
|
+
if (raw === undefined)
|
|
132
|
+
return [];
|
|
133
|
+
const paths = (Array.isArray(raw) ? raw : [raw]).filter((p) => typeof p === 'string' && p.trim().length > 0);
|
|
134
|
+
return paths
|
|
135
|
+
.filter(p => {
|
|
136
|
+
const normalised = p.replace(/^\.\/?/, '').replace(/\/+$/, '');
|
|
137
|
+
// `.` and `./` mean the whole repository and always exist.
|
|
138
|
+
if (normalised === '')
|
|
139
|
+
return false;
|
|
140
|
+
return !node_fs_1.default.existsSync(node_path_1.default.join(cwd, normalised));
|
|
141
|
+
})
|
|
142
|
+
.map(p => `sourcePath "${p}" does not exist — every scoped check silently reviews nothing, and reports a pass`);
|
|
143
|
+
}
|
|
115
144
|
exports.configValidationCheck = {
|
|
116
145
|
name: 'Config Validation',
|
|
117
146
|
phase: 'informational',
|
|
@@ -153,6 +182,7 @@ exports.configValidationCheck = {
|
|
|
153
182
|
issues.push(...validateSeverity(json, checkNames));
|
|
154
183
|
}
|
|
155
184
|
issues.push(...validateFailOn(json));
|
|
185
|
+
issues.push(...validateSourcePath(json, ctx.cwd));
|
|
156
186
|
if (issues.length === 0)
|
|
157
187
|
return (0, types_1.pass)();
|
|
158
188
|
// Advisory by default: a config problem is worth knowing about, but a repo
|
|
@@ -1,33 +1,65 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.duplicateCheck = void 0;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
4
10
|
const config_1 = require("../../../config");
|
|
5
11
|
const constants_1 = require("../../../config/constants");
|
|
6
12
|
const utils_1 = require("../../../utils");
|
|
7
13
|
const execa_1 = require("../../../utils/execa");
|
|
8
14
|
const targets_1 = require("../../targets");
|
|
9
15
|
const types_1 = require("../../types");
|
|
16
|
+
const findings_1 = require("../findings");
|
|
17
|
+
/**
|
|
18
|
+
* The files to hand jscpd, per language.
|
|
19
|
+
*
|
|
20
|
+
* This map is why the check reads `ctx.languages` at all. It used to pass a fixed
|
|
21
|
+
* `*.ts,*.tsx,*.js,*.jsx` and gate itself on TypeScript/JavaScript, so a Python
|
|
22
|
+
* repository got `⏭️ Duplicate Code: no relevant files` — not because it had no
|
|
23
|
+
* duplication, but because nothing ever looked. The formats list in `constants.ts`
|
|
24
|
+
* named Python and C++ all along, which made the report's own claim untrue.
|
|
25
|
+
*
|
|
26
|
+
* Extensions mirror `detect-language.ts` so a language that decides the check runs
|
|
27
|
+
* is a language whose files it actually scans, with one deliberate addition: `.c`
|
|
28
|
+
* and `.h` under `cpp`, since a C project detects as `cpp` and the `c` format is
|
|
29
|
+
* one jscpd supports.
|
|
30
|
+
*/
|
|
31
|
+
const LANGUAGE_GLOBS = {
|
|
32
|
+
typescript: ['*.ts', '*.tsx'],
|
|
33
|
+
javascript: ['*.js', '*.jsx', '*.mjs', '*.cjs'],
|
|
34
|
+
python: ['*.py'],
|
|
35
|
+
cpp: ['*.cpp', '*.cc', '*.cxx', '*.c++', '*.hpp', '*.hh', '*.hxx', '*.c', '*.h'],
|
|
36
|
+
swift: ['*.swift'],
|
|
37
|
+
kotlin: ['*.kt', '*.kts', '*.java'],
|
|
38
|
+
go: ['*.go'],
|
|
39
|
+
rust: ['*.rs'],
|
|
40
|
+
csharp: ['*.cs'],
|
|
41
|
+
ruby: ['*.rb'],
|
|
42
|
+
php: ['*.php'],
|
|
43
|
+
};
|
|
44
|
+
/** The scan globs for one run: every detected language jscpd can tokenise. */
|
|
45
|
+
function globsFor(languages) {
|
|
46
|
+
return [...new Set(languages.flatMap(lang => LANGUAGE_GLOBS[lang] ?? []))];
|
|
47
|
+
}
|
|
10
48
|
// Always excluded regardless of client config — scanning these never yields
|
|
11
49
|
// meaningful production duplication. Config files (eslint/*.config.*) are data,
|
|
12
50
|
// not logic, and are commonly shipped in two formats (e.g. .eslintrc + flat
|
|
13
51
|
// config), so their repetition is expected rather than a code smell.
|
|
14
52
|
const BASE_IGNORE = ['node_modules/**', 'dist/**', '.git/**', ...constants_1.CONFIG_FILE_PATTERNS];
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
'
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
'
|
|
24
|
-
|
|
25
|
-
function stripJscpdPromo(text) {
|
|
26
|
-
return text
|
|
27
|
-
.split('\n')
|
|
28
|
-
.filter(line => !JSCPD_PROMO_MARKERS.some(marker => line.includes(marker)))
|
|
29
|
-
.join('\n')
|
|
30
|
-
.trimEnd();
|
|
53
|
+
/** `<path>:<start>-<end>`, repo-relative, for one end of a clone. */
|
|
54
|
+
function locate(cwd, at) {
|
|
55
|
+
const name = at?.name ?? '';
|
|
56
|
+
const rel = name ? node_path_1.default.relative(cwd, name) || node_path_1.default.basename(name) : '<unknown file>';
|
|
57
|
+
return `${rel}:${at?.start ?? '?'}-${at?.end ?? '?'}`;
|
|
58
|
+
}
|
|
59
|
+
/** One reportable line per clone: both ends, and how big it is. */
|
|
60
|
+
function describe(cwd, clone) {
|
|
61
|
+
const size = clone.lines === undefined ? '' : ` (${clone.lines} lines)`;
|
|
62
|
+
return `${locate(cwd, clone.firstFile)} ↔ ${locate(cwd, clone.secondFile)}${size}`;
|
|
31
63
|
}
|
|
32
64
|
exports.duplicateCheck = {
|
|
33
65
|
name: 'Duplicate Code',
|
|
@@ -36,12 +68,12 @@ exports.duplicateCheck = {
|
|
|
36
68
|
applies(ctx) {
|
|
37
69
|
if (ctx.config.duplicate?.enabled === false)
|
|
38
70
|
return false;
|
|
39
|
-
return ctx.languages.
|
|
71
|
+
return globsFor(ctx.languages).length > 0;
|
|
40
72
|
},
|
|
41
73
|
async run(ctx) {
|
|
42
74
|
const minLines = ctx.config.duplicate?.minLines ?? 5;
|
|
43
75
|
const minTokens = ctx.config.duplicate?.minTokens ?? 50;
|
|
44
|
-
const targetPaths = await (0, targets_1.resolveScopedTargetFiles)(ctx,
|
|
76
|
+
const targetPaths = await (0, targets_1.resolveScopedTargetFiles)(ctx, globsFor(ctx.languages), (0, config_1.getSourcePaths)(ctx.cwd));
|
|
45
77
|
if (targetPaths === null)
|
|
46
78
|
return (0, types_1.skip)('git unavailable');
|
|
47
79
|
if (targetPaths.length === 0)
|
|
@@ -55,7 +87,16 @@ exports.duplicateCheck = {
|
|
|
55
87
|
// defaulting to source-code formats so data files (yaml/json/markdown) are
|
|
56
88
|
// not scanned. Clients can add them back explicitly.
|
|
57
89
|
const formats = (ctx.config.duplicate?.formats ?? constants_1.DUPLICATE_CODE_FORMATS).join(',');
|
|
90
|
+
// The JSON reporter writes a file, so it needs somewhere to write it. Outside
|
|
91
|
+
// the repository deliberately: with no `--output`, jscpd creates `report/`
|
|
92
|
+
// in the client's working tree, and a check that leaves a directory behind is
|
|
93
|
+
// a check that shows up in the next commit.
|
|
94
|
+
const reportDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), 'pr-checkmate-jscpd-'));
|
|
58
95
|
try {
|
|
96
|
+
// `--silent` keeps jscpd's own console output away from the client entirely.
|
|
97
|
+
// That is what retired `stripJscpdPromo` — six string markers matching the
|
|
98
|
+
// funding lines jscpd v5 prints after its report, each good until the next
|
|
99
|
+
// release of somebody else's package.
|
|
59
100
|
const result = await (0, execa_1.execa)(process.execPath, [
|
|
60
101
|
jscpdBin,
|
|
61
102
|
'--min-lines',
|
|
@@ -63,27 +104,44 @@ exports.duplicateCheck = {
|
|
|
63
104
|
'--min-tokens',
|
|
64
105
|
String(minTokens),
|
|
65
106
|
'--reporters',
|
|
66
|
-
'
|
|
107
|
+
'json',
|
|
108
|
+
'--output',
|
|
109
|
+
reportDir,
|
|
110
|
+
'--absolute',
|
|
111
|
+
'--silent',
|
|
67
112
|
'--format',
|
|
68
113
|
formats,
|
|
69
114
|
'--ignore',
|
|
70
115
|
ignore,
|
|
71
116
|
...targetPaths,
|
|
72
117
|
], { cwd: ctx.cwd, reject: false });
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return (0, types_1.
|
|
118
|
+
const reportPath = node_path_1.default.join(reportDir, 'jscpd-report.json');
|
|
119
|
+
if (!node_fs_1.default.existsSync(reportPath)) {
|
|
120
|
+
// No report and nothing to read it from: jscpd did not get as far as
|
|
121
|
+
// writing one. Its stderr is the only thing that can say why, and a skip
|
|
122
|
+
// is the honest outcome — an empty report would read as "no duplication".
|
|
123
|
+
utils_1.logger.warn(`[Duplicate Code]: ⚠️ jscpd wrote no report (exit ${result.exitCode ?? 'killed'})` +
|
|
124
|
+
`${result.stderr ? `\n${result.stderr}` : ''}`);
|
|
125
|
+
return (0, types_1.skip)('jscpd produced no report');
|
|
81
126
|
}
|
|
82
|
-
|
|
127
|
+
const report = JSON.parse(node_fs_1.default.readFileSync(reportPath, 'utf-8'));
|
|
128
|
+
const clones = report.duplicates ?? [];
|
|
129
|
+
// jscpd's own count, not a regex over its prose. `output.match(/(\d+)\s+clone/i)`
|
|
130
|
+
// read the number out of an English sentence, so a reworded summary upstream
|
|
131
|
+
// would have reported zero duplication on a repository full of it — and
|
|
132
|
+
// reported it as a pass.
|
|
133
|
+
const cloneCount = report.statistics?.total?.clones ?? clones.length;
|
|
134
|
+
if (cloneCount === 0)
|
|
135
|
+
return (0, types_1.pass)();
|
|
136
|
+
(0, findings_1.logSample)('Duplicate Code', `${cloneCount} duplicate block(s):`, clones.map(clone => describe(ctx.cwd, clone)));
|
|
137
|
+
return (0, types_1.warn)(`${cloneCount} duplicate block(s)`);
|
|
83
138
|
}
|
|
84
139
|
catch (err) {
|
|
85
140
|
utils_1.logger.warn(`[Duplicate Code]: ⚠️ Could not run duplicate check\n${err}`);
|
|
86
141
|
return (0, types_1.skip)('jscpd unavailable');
|
|
87
142
|
}
|
|
143
|
+
finally {
|
|
144
|
+
node_fs_1.default.rmSync(reportDir, { recursive: true, force: true });
|
|
145
|
+
}
|
|
88
146
|
},
|
|
89
147
|
};
|
|
@@ -8,6 +8,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
9
|
const utils_1 = require("../../../utils");
|
|
10
10
|
const execa_1 = require("../../../utils/execa");
|
|
11
|
+
const targets_1 = require("../../targets");
|
|
11
12
|
const types_1 = require("../../types");
|
|
12
13
|
/** Source extensions a license header is expected on. */
|
|
13
14
|
const SOURCE_EXT = /\.(ts|tsx|js|jsx|mjs|cjs|py|kt|java|go|rs|cs|rb|php|swift|c|cc|cpp|h|hpp)$/i;
|
|
@@ -49,10 +50,12 @@ exports.licenseHeaderCheck = {
|
|
|
49
50
|
? ['diff', '--name-only', '--diff-filter=A', ctx.baseSha, ctx.headSha ?? 'HEAD']
|
|
50
51
|
: ['ls-files'];
|
|
51
52
|
const { stdout } = await (0, execa_1.execa)('git', args, { cwd: ctx.cwd });
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
// Its own git call rather than `resolveTargets`, and for one reason:
|
|
54
|
+
// `--diff-filter=A`. This check is about files a PR ADDS, and the shared
|
|
55
|
+
// resolver asks for ACMR — routing through it would start demanding a
|
|
56
|
+
// header on every modified file too, which is a different check. So the
|
|
57
|
+
// client's `excludePaths` is applied here explicitly instead (ticket 080).
|
|
58
|
+
added = (0, targets_1.excludeClientPaths)(stdout.split('\n').filter(Boolean), ctx.cwd).filter(f => SOURCE_EXT.test(f));
|
|
56
59
|
}
|
|
57
60
|
catch (err) {
|
|
58
61
|
utils_1.logger.warn(`[License Header]: ⚠️ Could not analyze diff\n${err}`);
|
|
@@ -47,12 +47,7 @@ exports.markdownLintCheck = {
|
|
|
47
47
|
const userRules = userConfig.rules ?? {};
|
|
48
48
|
let tempConfigFile = null;
|
|
49
49
|
let configArg;
|
|
50
|
-
if ((0, config_1.hasLocalConfig)([
|
|
51
|
-
'.markdownlint.json',
|
|
52
|
-
'.markdownlint.jsonc',
|
|
53
|
-
'.markdownlint.yaml',
|
|
54
|
-
'.markdownlint.yml',
|
|
55
|
-
])) {
|
|
50
|
+
if ((0, config_1.hasLocalConfig)(['.markdownlint.json', '.markdownlint.jsonc', '.markdownlint.yaml', '.markdownlint.yml'], ctx.cwd)) {
|
|
56
51
|
configArg = [];
|
|
57
52
|
}
|
|
58
53
|
else {
|
|
@@ -188,19 +188,48 @@ function dropMissingLanguages(language, missing) {
|
|
|
188
188
|
// to its own default, which is not necessarily what was configured here.
|
|
189
189
|
return kept.length > 0 ? kept.join(',') : 'en';
|
|
190
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Resolve a package id from the repository under review first, then from our own
|
|
193
|
+
* tree.
|
|
194
|
+
*
|
|
195
|
+
* The default `require.resolve` searches from THIS module, which is only the
|
|
196
|
+
* client's tree by accident. Installed as a dependency we sit in their
|
|
197
|
+
* `node_modules` and npm hoists `@cspell/dict-*` above us, so walking up finds
|
|
198
|
+
* it. Run through `npx` we sit in the npx cache and their `node_modules` is not
|
|
199
|
+
* on the path at all — so `npm i -D @cspell/dict-ru_ru`, which this file's own
|
|
200
|
+
* docstring promises is all it takes, did nothing and said nothing (ticket 081).
|
|
201
|
+
*
|
|
202
|
+
* Same policy as tool resolution for binaries: the client's copy first, ours as
|
|
203
|
+
* the fallback.
|
|
204
|
+
*/
|
|
205
|
+
function resolveFrom(cwd) {
|
|
206
|
+
return id => {
|
|
207
|
+
try {
|
|
208
|
+
return require.resolve(id, { paths: [cwd] });
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return require.resolve(id);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
}
|
|
191
215
|
function createTempCspellConfig(cwd) {
|
|
192
216
|
const packageConfigPath = (0, config_1.getPackageConfigPath)('cspell.json');
|
|
193
217
|
const packageConfigDir = node_path_1.default.dirname(packageConfigPath);
|
|
194
218
|
const defaultConfig = JSON.parse(node_fs_1.default.readFileSync(packageConfigPath, 'utf-8'));
|
|
195
219
|
const userConfig = loadUserCspellConfig(cwd);
|
|
196
220
|
const merged = mergeCspellConfig(defaultConfig, userConfig);
|
|
197
|
-
const
|
|
198
|
-
const
|
|
221
|
+
const resolve = resolveFrom(cwd);
|
|
222
|
+
const { resolved, missing } = resolveImports(merged.import ?? [], packageConfigDir, resolve);
|
|
223
|
+
const available = availableDictionaries(resolve);
|
|
199
224
|
for (const { pkg } of missing) {
|
|
200
225
|
utils_1.logger.info(`[Spellcheck]: dictionary ${pkg} is not installed and was skipped. Add it with \`npm i -D ${pkg}\`.`);
|
|
201
226
|
}
|
|
202
227
|
if (available.absent.length > 0) {
|
|
203
|
-
|
|
228
|
+
// `info`, not `debug`. A client who installed a dictionary and saw no change
|
|
229
|
+
// had nothing to read: the one line that would have explained it was behind a
|
|
230
|
+
// flag. Which dictionaries are in play decides which findings appear, so it
|
|
231
|
+
// belongs in the ordinary log (ticket 081).
|
|
232
|
+
utils_1.logger.info(`[Spellcheck]: optional dictionaries not installed: ${available.absent.join(', ')}. ` +
|
|
204
233
|
`Install any of them to spellcheck that language (\`npm i -D ${available.absent[0]}\`).`);
|
|
205
234
|
}
|
|
206
235
|
// A language the client asked for whose dictionary is absent is dropped rather
|
|
@@ -230,7 +259,7 @@ exports.spellcheckCheck = {
|
|
|
230
259
|
return ctx.config.cspell?.enabled !== false && ctx.config.spellcheck?.enabled !== false;
|
|
231
260
|
},
|
|
232
261
|
async run(ctx) {
|
|
233
|
-
const targets = await (0, targets_1.resolveScopedTargetFiles)(ctx, ['*.ts', '*.tsx', '*.js', '*.jsx', '*.md', '*.mdx', '*.json'], (0, config_1.getSourcePaths)());
|
|
262
|
+
const targets = await (0, targets_1.resolveScopedTargetFiles)(ctx, ['*.ts', '*.tsx', '*.js', '*.jsx', '*.md', '*.mdx', '*.json'], (0, config_1.getSourcePaths)(ctx.cwd));
|
|
234
263
|
if (targets === null)
|
|
235
264
|
return (0, types_1.skip)('git unavailable');
|
|
236
265
|
// Committed build output and test reports are dropped here, in JS, against
|
|
@@ -246,7 +275,7 @@ exports.spellcheckCheck = {
|
|
|
246
275
|
const files = targets.filter(f => !(0, utils_1.matchesAnyGlob)(f, config_1.IGNORE_PATTERNS));
|
|
247
276
|
if (files.length === 0)
|
|
248
277
|
return (0, types_1.pass)('no files to spellcheck');
|
|
249
|
-
const hasLocal = (0, config_1.hasLocalConfig)(['cspell.json', '.cspell.json', 'cspell.config.json']);
|
|
278
|
+
const hasLocal = (0, config_1.hasLocalConfig)(['cspell.json', '.cspell.json', 'cspell.config.json'], ctx.cwd);
|
|
250
279
|
let tempConfigPath = null;
|
|
251
280
|
const cspellBin = (0, utils_1.resolvePackageBin)('cspell', 'bin.mjs');
|
|
252
281
|
// `--no-must-find-files`: cspell exits 1 when it checked nothing, which the
|
|
@@ -7,7 +7,6 @@ exports.symlinksCheck = void 0;
|
|
|
7
7
|
exports.isBrokenSymlink = isBrokenSymlink;
|
|
8
8
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
-
const config_1 = require("../../../config");
|
|
11
10
|
const utils_1 = require("../../../utils");
|
|
12
11
|
const targets_1 = require("../../targets");
|
|
13
12
|
const types_1 = require("../../types");
|
|
@@ -42,9 +41,12 @@ exports.symlinksCheck = {
|
|
|
42
41
|
return ctx.config.symlinks?.enabled !== false;
|
|
43
42
|
},
|
|
44
43
|
async run(ctx) {
|
|
45
|
-
const files = await (0, targets_1.
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
const files = await (0, targets_1.resolveTargets)(ctx, ['*'], {
|
|
45
|
+
wholeTree: true,
|
|
46
|
+
onEmpty: { skip: 'no tracked files' },
|
|
47
|
+
});
|
|
48
|
+
if (!Array.isArray(files))
|
|
49
|
+
return files;
|
|
48
50
|
const ignore = ctx.config.symlinks?.ignore ?? [];
|
|
49
51
|
const broken = [];
|
|
50
52
|
for (const file of files) {
|
|
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.diffSecurityCheck = void 0;
|
|
4
4
|
const config_1 = require("../../../config");
|
|
5
5
|
const utils_1 = require("../../../utils");
|
|
6
|
-
const targets_1 = require("../../targets");
|
|
7
6
|
const types_1 = require("../../types");
|
|
8
7
|
const findings_1 = require("../findings");
|
|
9
8
|
const MD5_LABEL = 'MD5 (weak hash — use sha256 or stronger)';
|
|
@@ -298,6 +297,9 @@ exports.diffSecurityCheck = {
|
|
|
298
297
|
// to them, matching `duplicate.ignore` and `deadCode.ignoreFiles`. A client
|
|
299
298
|
// who wants their tests scanned says so by listing something else here.
|
|
300
299
|
ignorePaths: cfg.ignorePaths ?? config_1.DIFF_SECURITY_IGNORE_DEFAULTS,
|
|
300
|
+
// This check owns the cross-line heuristic, so it is the one that turns it
|
|
301
|
+
// on. `0` from the client switches it off.
|
|
302
|
+
dataflowWindow: cfg.dataflowWindow ?? findings_1.DEFAULT_DATAFLOW_WINDOW,
|
|
301
303
|
};
|
|
302
304
|
const extra = compileExtraPatterns(ctx);
|
|
303
305
|
const disabled = cfg.disablePatterns ?? [];
|
|
@@ -306,33 +308,39 @@ exports.diffSecurityCheck = {
|
|
|
306
308
|
};
|
|
307
309
|
/**
|
|
308
310
|
* Feed one language's sources through the patterns, from the diff when
|
|
309
|
-
* there is one and from the files themselves otherwise.
|
|
310
|
-
*
|
|
311
|
-
*
|
|
311
|
+
* there is one and from the files themselves otherwise.
|
|
312
|
+
*
|
|
313
|
+
* Through {@link reviewLines}, like the four other checks that need both
|
|
314
|
+
* modes. This used to be its body copied inline, on the assumption that a
|
|
315
|
+
* closure was needed to carry `patterns` — it is not, that is what `onChunk`
|
|
316
|
+
* is for, and the copy meant `fullScanBehaviour` and the `null`/`[]`
|
|
317
|
+
* distinction lived in two places, one of them a blocking security check
|
|
318
|
+
* (ticket 079).
|
|
312
319
|
*/
|
|
313
|
-
const review =
|
|
314
|
-
if (ctx.baseSha) {
|
|
315
|
-
scanInto(await (0, findings_1.diffAddedLines)(ctx, globs), patterns);
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
if (ctx.config.run?.fullScanBehaviour === 'skip')
|
|
319
|
-
return;
|
|
320
|
-
const files = await (0, targets_1.listTrackedFiles)(ctx, globs, ['.']);
|
|
321
|
-
await (0, findings_1.scanTrackedFiles)(ctx, files, asDiff => scanInto(asDiff, patterns));
|
|
322
|
-
};
|
|
320
|
+
const review = (globs, patterns) => (0, findings_1.reviewLines)(ctx, globs, stdout => scanInto(stdout, patterns));
|
|
323
321
|
// JS/TS patterns always run; language-specific sets scan their own globs
|
|
324
322
|
// only when that language is present, so a Python repo gets Python rules.
|
|
325
323
|
// Client `extraPatterns` run against every scanned language, since a
|
|
326
324
|
// project rule is not tied to the built-in language split.
|
|
327
|
-
|
|
325
|
+
const sets = [[JS_GLOBS, [...SECURITY_PATTERNS, ...extra]]];
|
|
328
326
|
if (ctx.languages.includes('python')) {
|
|
329
|
-
|
|
327
|
+
sets.push([['*.py'], [...PYTHON_SECURITY_PATTERNS, ...extra]]);
|
|
330
328
|
}
|
|
331
329
|
if (ctx.languages.includes('go')) {
|
|
332
|
-
|
|
330
|
+
sets.push([['*.go'], [...GO_SECURITY_PATTERNS, ...extra]]);
|
|
333
331
|
}
|
|
334
332
|
if (ctx.languages.includes('kotlin')) {
|
|
335
|
-
|
|
333
|
+
sets.push([
|
|
334
|
+
['*.kt', '*.kts', '*.java'],
|
|
335
|
+
[...KOTLIN_SECURITY_PATTERNS, ...extra],
|
|
336
|
+
]);
|
|
337
|
+
}
|
|
338
|
+
for (const [globs, patterns] of sets) {
|
|
339
|
+
// One failed set fails the check. A partial answer is worse than none
|
|
340
|
+
// here: three of five language sets scanned, reported as a pass, reads
|
|
341
|
+
// as a clean repository (ticket 072).
|
|
342
|
+
if (!(await review(globs, patterns)))
|
|
343
|
+
return (0, types_1.skip)('git unavailable');
|
|
336
344
|
}
|
|
337
345
|
return (0, findings_1.reportSeverityBuckets)(found.errors, found.warnings, {
|
|
338
346
|
prefix: 'Diff Security',
|
|
@@ -30,7 +30,13 @@ const DOCKER_PATTERNS = [
|
|
|
30
30
|
severity: 'warn',
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
|
-
|
|
33
|
+
// The `_FILE`/`_PATH` exclusion is not a loophole, it is the thing this rule
|
|
34
|
+
// is trying to steer people towards: `ENV DB_PASSWORD_FILE=/run/secrets/db`
|
|
35
|
+
// names a location, holds no secret, and is how Docker and Kubernetes tell
|
|
36
|
+
// you to do it. Flagging the correct practice alongside the wrong one is how
|
|
37
|
+
// a rule gets switched off — and it teaches obscuring the variable name,
|
|
38
|
+
// which makes the next real secret harder to spot, not easier.
|
|
39
|
+
pattern: /^\+(?!\+)\s*(?:ENV|ARG)\s+\w*(?:PASSWORD|SECRET|TOKEN|API_?KEY|ACCESS_KEY)\w*(?<!_FILE)(?<!_PATH)(?<!_FILEPATH)\s*[=\s]/i,
|
|
34
40
|
label: 'secret baked into ENV/ARG (use build secrets / runtime injection)',
|
|
35
41
|
severity: 'warn',
|
|
36
42
|
},
|
|
@@ -48,17 +48,45 @@ function isAddNotNullNoDefault(line) {
|
|
|
48
48
|
/\bNOT\s+NULL\b/i.test(line) &&
|
|
49
49
|
!/\bDEFAULT\b/i.test(line));
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Drop the comment from an added migration line, keeping the leading `+` so the
|
|
53
|
+
* patterns stay anchored on it.
|
|
54
|
+
*
|
|
55
|
+
* This check does its own matching rather than going through `scanDiffPatterns`,
|
|
56
|
+
* and it inherited none of the comment handling — so a commented-out statement
|
|
57
|
+
* was reported as destructive. `-- DROP TABLE users;` does nothing, and calling
|
|
58
|
+
* it "irreversible data loss" is noise on the most alarming label the product
|
|
59
|
+
* prints. Found by ticket 062's corpus.
|
|
60
|
+
*
|
|
61
|
+
* Only the two markers that are unambiguous in the files this check looks at:
|
|
62
|
+
* `--` in SQL and `#` in Python/Ruby migrations. `--` is required to be preceded
|
|
63
|
+
* by whitespace or the line start, which keeps it away from an identifier like
|
|
64
|
+
* `a--b` and from most string contents; `stripInlineComment` handles `#` with the
|
|
65
|
+
* string awareness it already has.
|
|
66
|
+
*/
|
|
67
|
+
function withoutComment(line) {
|
|
68
|
+
// The diff marker is split off first: `+-- DROP TABLE` puts `--` immediately
|
|
69
|
+
// after the `+`, where a boundary rule looking for whitespace or line-start
|
|
70
|
+
// does not see it — which is exactly the shape a commented-out statement takes.
|
|
71
|
+
const marker = line.startsWith('+') ? '+' : '';
|
|
72
|
+
const body = (0, findings_1.stripInlineComment)(line.slice(marker.length), 'hash').replace(/(^|\s)--.*$/, '$1');
|
|
73
|
+
return marker + body;
|
|
74
|
+
}
|
|
51
75
|
/** Bucket dangerous added lines by risk label, honouring the opt-out directive. */
|
|
52
76
|
function scan(stdout, findings) {
|
|
53
77
|
for (const line of stdout.split('\n')) {
|
|
54
78
|
if (line.includes('pr-checkmate-ignore'))
|
|
55
79
|
continue;
|
|
80
|
+
// The sample keeps the ORIGINAL line: the reader needs to see what was
|
|
81
|
+
// written, comment and all, even though the match ran on the code.
|
|
82
|
+
const code = withoutComment(line);
|
|
83
|
+
const sample = () => line.slice(1).trim().substring(0, 120);
|
|
56
84
|
for (const { pattern, label } of DDL_PATTERNS) {
|
|
57
|
-
if (pattern.test(
|
|
58
|
-
(findings[label] ?? (findings[label] = [])).push(
|
|
85
|
+
if (pattern.test(code))
|
|
86
|
+
(findings[label] ?? (findings[label] = [])).push(sample());
|
|
59
87
|
}
|
|
60
|
-
if (isAddNotNullNoDefault(
|
|
61
|
-
(findings[NOT_NULL_NO_DEFAULT] ?? (findings[NOT_NULL_NO_DEFAULT] = [])).push(
|
|
88
|
+
if (isAddNotNullNoDefault(code)) {
|
|
89
|
+
(findings[NOT_NULL_NO_DEFAULT] ?? (findings[NOT_NULL_NO_DEFAULT] = [])).push(sample());
|
|
62
90
|
}
|
|
63
91
|
}
|
|
64
92
|
}
|
|
@@ -1,4 +1,60 @@
|
|
|
1
1
|
import { Check } from '../../types';
|
|
2
|
+
/**
|
|
3
|
+
* A HOME the scanner can actually write to.
|
|
4
|
+
*
|
|
5
|
+
* The bundled wrapper caches the gitleaks binary in `os.homedir()/.gitleaks-cache`
|
|
6
|
+
* and offers no way to move it — the path is hardcoded. So in any environment
|
|
7
|
+
* where HOME is not writable it crashes before scanning a single commit. That is
|
|
8
|
+
* not exotic: it is what the container does under `--user "$(id -u):$(id -g)"`,
|
|
9
|
+
* the invocation our own entrypoint and docs recommend, where HOME is `/`.
|
|
10
|
+
*
|
|
11
|
+
* Redirecting to the temp directory costs nothing that was available anyway — if
|
|
12
|
+
* HOME cannot be written to, there was no `~/.gitconfig` of ours to lose either.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* How many secrets the scan actually found.
|
|
16
|
+
*
|
|
17
|
+
* The row used to read `❌ Security Scan: potential secrets detected` whether the
|
|
18
|
+
* number was one or thirty-nine, and that is not a cosmetic gap. It is the reason
|
|
19
|
+
* a real regression nearly shipped: an allowlist entry removed six genuine
|
|
20
|
+
* `Generic API Key` findings from a client repository, 39 → 33, and the A/B sweep
|
|
21
|
+
* that compares report rows across 28 repositories saw two identical lines
|
|
22
|
+
* (ticket 085). A check that finds things has to say how many, or nothing can
|
|
23
|
+
* tell whether it started finding fewer.
|
|
24
|
+
*
|
|
25
|
+
* Read from gitleaks' JSON report rather than counted out of its console output —
|
|
26
|
+
* the same lesson as ticket 078, where the clone count was parsed from an English
|
|
27
|
+
* sentence and would have reported zero on a repository full of duplication.
|
|
28
|
+
*
|
|
29
|
+
* @returns The count, or `null` when the report could not be read — in which case
|
|
30
|
+
* the caller keeps its old wording rather than claiming a number it does not have.
|
|
31
|
+
*/
|
|
32
|
+
export declare function countFindings(reportPath: string): number | null;
|
|
33
|
+
/**
|
|
34
|
+
* How the outcome names what was found.
|
|
35
|
+
*
|
|
36
|
+
* Its own function so both halves are reachable without running the scanner. The
|
|
37
|
+
* corpus for this check deliberately does not: the wrapper downloads a gitleaks
|
|
38
|
+
* binary on first use, and a unit suite that reaches the network is the thing the
|
|
39
|
+
* `installer` seam in `ensure.ts` exists to avoid. What broke here was the wording
|
|
40
|
+
* — a row that read the same for one secret and for thirty-nine — and the wording
|
|
41
|
+
* is testable on its own.
|
|
42
|
+
*
|
|
43
|
+
* `null` keeps the old sentence rather than inventing a number: a report we could
|
|
44
|
+
* not read is not a reason to claim a count.
|
|
45
|
+
*/
|
|
46
|
+
export declare function secretsSummary(found: number | null): string;
|
|
47
|
+
/**
|
|
48
|
+
* Whether the wrapper died instead of reporting, judged from its stderr.
|
|
49
|
+
*
|
|
50
|
+
* The wrapper is a Node script, and Node exits **1** on an unhandled exception —
|
|
51
|
+
* the same code gitleaks itself uses for "secrets found". Left undistinguished,
|
|
52
|
+
* a scanner that never ran is reported as `potential secrets detected`, pointing
|
|
53
|
+
* at a report file that was never written. A gate accusing a clean repository is
|
|
54
|
+
* the same class of failure as one that stays quiet about a real leak, and this
|
|
55
|
+
* is the only signal available to tell them apart.
|
|
56
|
+
*/
|
|
57
|
+
export declare function looksLikeCrash(stderr: string): boolean;
|
|
2
58
|
/**
|
|
3
59
|
* Secret scanning via the bundled gitleaks package. Language-agnostic — runs on
|
|
4
60
|
* every repo (Node, Python, C++, mobile). Secrets found → fail (blocking).
|