pr-checkmate 3.2.3 → 3.3.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/.eslintrc.js +3 -9
- package/README.md +1 -1
- package/dist/cjs/cli.js +13 -0
- package/dist/cjs/config/pr-checkmate.template.json +1 -1
- package/dist/cjs/core/checks/dependencies/grype-scan.js +18 -0
- package/dist/cjs/core/checks/languages/lint.js +12 -0
- package/dist/cjs/core/checks/languages/ruff-shared.js +6 -1
- package/dist/cjs/core/checks/pr/commitlint.js +4 -2
- package/dist/cjs/core/checks/quality/package-size.d.ts +23 -1
- package/dist/cjs/core/checks/quality/package-size.js +20 -4
- package/dist/cjs/core/checks/quality/spellcheck.js +8 -5
- package/dist/cjs/core/install/doctor.js +12 -0
- package/dist/cjs/core/install/grype-db.d.ts +25 -0
- package/dist/cjs/core/install/grype-db.js +45 -0
- package/dist/cjs/core/install/profiles.d.ts +1 -1
- package/dist/cjs/core/install/profiles.js +1 -1
- package/dist/cjs/core/install/reuse.js +6 -3
- package/dist/cjs/init.d.ts +0 -8
- package/dist/cjs/init.js +1 -2
- package/dist/cjs/utils/own-deps.js +8 -1
- package/dist/cjs/utils/resolve-bin.d.ts +2 -0
- package/dist/cjs/utils/resolve-bin.js +5 -0
- package/dist/esm/cli.js +14 -1
- package/dist/esm/config/pr-checkmate.template.json +1 -1
- package/dist/esm/core/checks/dependencies/dependency-check.js +2 -2
- package/dist/esm/core/checks/dependencies/grype-scan.js +18 -0
- package/dist/esm/core/checks/dependencies/license-check.js +2 -2
- package/dist/esm/core/checks/dependencies/npm-audit.js +2 -2
- package/dist/esm/core/checks/git/diff-smell.js +1 -1
- package/dist/esm/core/checks/languages/cpp-format.js +1 -1
- package/dist/esm/core/checks/languages/lint.js +12 -3
- package/dist/esm/core/checks/languages/prettier.js +3 -3
- package/dist/esm/core/checks/languages/python-format.js +2 -2
- package/dist/esm/core/checks/languages/python-lint.js +2 -2
- package/dist/esm/core/checks/languages/ruff-shared.js +6 -1
- package/dist/esm/core/checks/languages/typecheck.js +1 -1
- package/dist/esm/core/checks/pr/commitlint.js +4 -2
- package/dist/esm/core/checks/pr/pr-body.js +1 -1
- package/dist/esm/core/checks/pr/pr-size-check.js +1 -1
- package/dist/esm/core/checks/pr/pr-title-lint.js +1 -1
- package/dist/esm/core/checks/quality/dead-code.js +1 -1
- package/dist/esm/core/checks/quality/duplicate-check.js +1 -1
- package/dist/esm/core/checks/quality/markdown-lint.js +1 -1
- package/dist/esm/core/checks/quality/package-size.d.ts +23 -1
- package/dist/esm/core/checks/quality/package-size.js +19 -4
- package/dist/esm/core/checks/quality/spellcheck.js +11 -8
- package/dist/esm/core/checks/security/diff-security.js +1 -1
- package/dist/esm/core/checks/security/security.js +1 -1
- package/dist/esm/core/install/doctor.js +12 -0
- package/dist/esm/core/install/grype-db.d.ts +25 -0
- package/dist/esm/core/install/grype-db.js +41 -0
- package/dist/esm/core/install/prepare.js +1 -1
- package/dist/esm/core/install/profiles.d.ts +1 -1
- package/dist/esm/core/install/profiles.js +1 -1
- package/dist/esm/core/install/reuse.js +6 -3
- package/dist/esm/core/run-checks.js +2 -2
- package/dist/esm/init.d.ts +0 -8
- package/dist/esm/init.js +4 -4
- package/dist/esm/utils/own-deps.js +8 -1
- package/dist/esm/utils/resolve-bin.d.ts +2 -0
- package/dist/esm/utils/resolve-bin.js +4 -0
- package/eslint-default-rules.json +2 -8
- package/eslint.config.mjs +37 -8
- package/install-manifests/commitlint/package-lock.json +5 -4
- package/install-manifests/core/package-lock.json +5 -3
- package/install-manifests/js/package-lock.json +250 -571
- package/install-manifests/js/package.json +10 -8
- package/install-manifests/markdown/package-lock.json +16 -13
- package/install-manifests/markdown/package.json +3 -0
- package/install-manifests/spellcheck/package-lock.json +3 -3
- package/package.json +19 -15
- package/src/config/.eslintrc.js +3 -9
- package/src/config/eslint-default-rules.json +2 -8
|
@@ -4,8 +4,8 @@ const __dirname = __dirnameFn(__fileURLToPath(import.meta.url));
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { execa } from '../../../utils/execa.js';
|
|
7
|
-
import {
|
|
8
|
-
import { isNpmProject, pass,
|
|
7
|
+
import { findOwnPackageRoot, logger } from '../../../utils/index.js';
|
|
8
|
+
import { fail, isNpmProject, pass, skip } from '../../types.js';
|
|
9
9
|
const SEVERITY_ORDER = ['info', 'low', 'moderate', 'high', 'critical'];
|
|
10
10
|
function isSeverityAtLeast(severity, level) {
|
|
11
11
|
const sevIdx = SEVERITY_ORDER.indexOf(severity);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { logger } from '../../../utils/index.js';
|
|
2
2
|
import { pass, skip } from '../../types.js';
|
|
3
|
-
import { bucketTotal, emptyBuckets, mergeSeverityBuckets, reportCountedFindings,
|
|
3
|
+
import { bucketTotal, emptyBuckets, mergeSeverityBuckets, reportCountedFindings, reviewLines, scanDiffPatterns, } from '../findings.js';
|
|
4
4
|
/**
|
|
5
5
|
* Smells that are only smells in live code.
|
|
6
6
|
*
|
|
@@ -4,7 +4,7 @@ import { getSourcePaths } from '../../../config/index.js';
|
|
|
4
4
|
import { execa } from '../../../utils/execa.js';
|
|
5
5
|
import { logger, resolvePackageBin } from '../../../utils/index.js';
|
|
6
6
|
import { resolveScopedTargetFiles } from '../../targets.js';
|
|
7
|
-
import { pass,
|
|
7
|
+
import { pass, skip, warn } from '../../types.js';
|
|
8
8
|
/**
|
|
9
9
|
* C/C++ source & header extensions clang-format understands. `.h` is intentionally
|
|
10
10
|
* included even though it is ambiguous with C — clang-format handles both.
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { getPackageConfigPath, getSourcePaths, hasLocalConfig, IGNORE_PATTERNS, } from '../../../config/index.js';
|
|
2
3
|
import { execa } from '../../../utils/execa.js';
|
|
3
|
-
import { logger, resolvePackageBin } from '../../../utils/index.js';
|
|
4
|
+
import { logger, registeredToolPaths, resolvePackageBin } from '../../../utils/index.js';
|
|
4
5
|
import { clientToolPaths } from '../../install/reuse.js';
|
|
5
6
|
import { resolveScopedTargetFiles } from '../../targets.js';
|
|
6
|
-
import { pass,
|
|
7
|
+
import { fail, pass, skip, warn } from '../../types.js';
|
|
7
8
|
/**
|
|
8
9
|
* Resolve the ESLint binary under the client's `install.reuseLocal` policy.
|
|
9
10
|
*
|
|
@@ -73,10 +74,18 @@ export const lintCheck = {
|
|
|
73
74
|
logger.info(`[ESLint]: Using pr-checkmate config (ESLint v${version}, ${configFile})`);
|
|
74
75
|
}
|
|
75
76
|
IGNORE_PATTERNS.forEach(p => args.push('--ignore-pattern', p));
|
|
77
|
+
// Our bundled config loads its plugins through these roots, and an ESLint
|
|
78
|
+
// older than 9 resolves legacy-config plugins relative to the path below — in
|
|
79
|
+
// both cases the plugins are in a fetched profile rather than beside us.
|
|
80
|
+
const toolRoots = registeredToolPaths();
|
|
81
|
+
if (!hasLocal && version < 9 && toolRoots.length > 0) {
|
|
82
|
+
args.push('--resolve-plugins-relative-to', toolRoots[0]);
|
|
83
|
+
}
|
|
76
84
|
const result = await execa(process.execPath, [eslintBin, ...args], {
|
|
77
85
|
cwd: ctx.cwd,
|
|
78
86
|
stdio: 'inherit',
|
|
79
87
|
reject: false,
|
|
88
|
+
env: { PR_CHECKMATE_TOOL_ROOTS: toolRoots.join(path.delimiter) },
|
|
80
89
|
});
|
|
81
90
|
if (result.exitCode === 0)
|
|
82
91
|
return pass();
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { getPackageConfigPath, hasLocalConfig, IGNORE_PATTERNS,
|
|
3
|
+
import { getPackageConfigPath, getSourcePaths, hasLocalConfig, IGNORE_PATTERNS, } from '../../../config/index.js';
|
|
4
4
|
import { execa } from '../../../utils/execa.js';
|
|
5
|
-
import {
|
|
5
|
+
import { cleanupTempFile, logger, resolvePackageBin } from '../../../utils/index.js';
|
|
6
6
|
import { resolveScopedTargetFiles } from '../../targets.js';
|
|
7
|
-
import { pass,
|
|
7
|
+
import { pass, skip, warn } from '../../types.js';
|
|
8
8
|
function loadUserPrettierConfig(cwd) {
|
|
9
9
|
const configPath = path.join(cwd, 'pr-checkmate.json');
|
|
10
10
|
if (!fs.existsSync(configPath))
|
|
@@ -2,8 +2,8 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { logger } from '../../../utils/index.js';
|
|
4
4
|
import { resolveTargetFiles } from '../../targets.js';
|
|
5
|
-
import { pass,
|
|
6
|
-
import {
|
|
5
|
+
import { pass, skip, warn } from '../../types.js';
|
|
6
|
+
import { createRuffWorkspace, PY_GLOBS } from './ruff-shared.js';
|
|
7
7
|
/**
|
|
8
8
|
* Formats changed Python files with Ruff's formatter (WASM, no Python/pip needed).
|
|
9
9
|
* Read-only in check mode (`warn` when files differ from formatted output); rewrites
|
|
@@ -2,8 +2,8 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { logger } from '../../../utils/index.js';
|
|
4
4
|
import { resolveTargetFiles } from '../../targets.js';
|
|
5
|
-
import { pass,
|
|
6
|
-
import {
|
|
5
|
+
import { pass, skip, warn } from '../../types.js';
|
|
6
|
+
import { createRuffWorkspace, PY_GLOBS } from './ruff-shared.js';
|
|
7
7
|
/**
|
|
8
8
|
* Lints changed Python files with Ruff, bundled as a self-contained WASM module
|
|
9
9
|
* (`@astral-sh/ruff-wasm-nodejs`) so it runs on a bare Node image with no Python
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from 'node:module';
|
|
2
2
|
const require = __createRequire(import.meta.url);
|
|
3
|
+
import { resolvePackageBin } from '../../../utils/index.js';
|
|
3
4
|
export const PY_GLOBS = ['*.py'];
|
|
4
5
|
/**
|
|
5
6
|
* The rules we lint Python with — pinned here rather than left to Ruff's defaults.
|
|
@@ -26,7 +27,11 @@ const SELECTED_RULES = ['E4', 'E7', 'E9', 'F'];
|
|
|
26
27
|
* singleton) so the WASM module loads only when a check runs.
|
|
27
28
|
*/
|
|
28
29
|
export function createRuffWorkspace() {
|
|
29
|
-
|
|
30
|
+
// Resolved rather than required by name: the package lives in the `python`
|
|
31
|
+
// profile, which is fetched into the cache directory when this repository
|
|
32
|
+
// needs it, and a bare specifier only searches our own tree.
|
|
33
|
+
const entry = resolvePackageBin('@astral-sh/ruff-wasm-nodejs', 'ruff_wasm.js');
|
|
34
|
+
const { Workspace, PositionEncoding } = require(entry);
|
|
30
35
|
const settings = {
|
|
31
36
|
...Workspace.defaultSettings(),
|
|
32
37
|
lint: { select: SELECTED_RULES },
|
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { execa } from '../../../utils/execa.js';
|
|
4
4
|
import { logger, resolvePackageBin } from '../../../utils/index.js';
|
|
5
5
|
import { clientToolPaths } from '../../install/reuse.js';
|
|
6
|
-
import { pass,
|
|
6
|
+
import { fail, pass, skip, warn } from '../../types.js';
|
|
7
7
|
/**
|
|
8
8
|
* Prefer the consumer project's own TypeScript so its tsconfig is compiled by the
|
|
9
9
|
* version it was written for; fall back to the compiler bundled with pr-checkmate.
|
|
@@ -34,8 +34,10 @@ const DEFAULT_IGNORES = ['^Release\\b', '^chore\\(release\\):'];
|
|
|
34
34
|
function buildTempConfig(types, ignores) {
|
|
35
35
|
// Emit a CJS module so `ignores` can hold functions — commitlint requires
|
|
36
36
|
// predicate functions there, which a JSON config cannot express.
|
|
37
|
-
//
|
|
38
|
-
|
|
37
|
+
// Resolved through the same roots as the binary, not by bare specifier: both
|
|
38
|
+
// live in the `commitlint` profile, which is fetched into the cache directory,
|
|
39
|
+
// and `require.resolve` alone only searches our own tree.
|
|
40
|
+
const configConventional = resolvePackageBin('@commitlint/config-conventional', 'lib/index.js');
|
|
39
41
|
const content = `const patterns = ${JSON.stringify(ignores)}.map((p) => new RegExp(p, 'i'));
|
|
40
42
|
module.exports = {
|
|
41
43
|
extends: [${JSON.stringify(configConventional)}],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { logger } from '../../../utils/index.js';
|
|
3
|
-
import { pass,
|
|
3
|
+
import { pass, skip, warn } from '../../types.js';
|
|
4
4
|
// Matches "Closes #123", "Fixes #45", "Resolves #7" or JIRA-style "PROJ-456".
|
|
5
5
|
const TICKET_PATTERN = /(?:closes?|fixes?|resolves?)\s+#\d+|[A-Z][A-Z0-9]+-\d+/i;
|
|
6
6
|
export const prBodyCheck = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execa } from '../../../utils/execa.js';
|
|
2
2
|
import { logger } from '../../../utils/index.js';
|
|
3
|
-
import { pass,
|
|
3
|
+
import { pass, skip, warn } from '../../types.js';
|
|
4
4
|
const DEFAULT_MAX_FILES = 50;
|
|
5
5
|
const DEFAULT_MAX_LINES = 1000;
|
|
6
6
|
export const prSizeCheck = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { logger } from '../../../utils/index.js';
|
|
3
|
-
import { pass,
|
|
3
|
+
import { pass, skip, warn } from '../../types.js';
|
|
4
4
|
const CONVENTIONAL_PREFIX = /^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\(.+\))?!?: .+/;
|
|
5
5
|
export const prTitleLintCheck = {
|
|
6
6
|
name: 'PR Title',
|
|
@@ -4,7 +4,7 @@ import { DEAD_CODE_IGNORE_DEFAULTS } from '../../../config/constants.js';
|
|
|
4
4
|
import { execa } from '../../../utils/execa.js';
|
|
5
5
|
import { logger, resolvePackageBin } from '../../../utils/index.js';
|
|
6
6
|
import { clientToolPaths } from '../../install/reuse.js';
|
|
7
|
-
import { pass,
|
|
7
|
+
import { pass, skip, warn } from '../../types.js';
|
|
8
8
|
/**
|
|
9
9
|
* Resolve ts-unused-exports, preferring the consumer project's own copy (so a
|
|
10
10
|
* repo pinned to a different major keeps its own behaviour) and falling back to
|
|
@@ -6,7 +6,7 @@ import { getSourcePaths } from '../../../config/index.js';
|
|
|
6
6
|
import { execa } from '../../../utils/execa.js';
|
|
7
7
|
import { logger, resolvePackageBin } from '../../../utils/index.js';
|
|
8
8
|
import { resolveScopedTargetFiles } from '../../targets.js';
|
|
9
|
-
import { pass,
|
|
9
|
+
import { pass, skip, warn } from '../../types.js';
|
|
10
10
|
import { logSample } from '../findings.js';
|
|
11
11
|
/**
|
|
12
12
|
* The files to hand jscpd, per language.
|
|
@@ -4,7 +4,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
4
4
|
import { GENERATED_DOC_FILES, getPackageConfigPath, hasLocalConfig, } from '../../../config/index.js';
|
|
5
5
|
import { logger, resolvePackageBin } from '../../../utils/index.js';
|
|
6
6
|
import { resolveTargetFiles } from '../../targets.js';
|
|
7
|
-
import { pass,
|
|
7
|
+
import { pass, skip, warn } from '../../types.js';
|
|
8
8
|
function loadUserMarkdownConfig(cwd) {
|
|
9
9
|
const configPath = path.join(cwd, 'pr-checkmate.json');
|
|
10
10
|
if (!fs.existsSync(configPath))
|
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
import { Check } from '../../types.js';
|
|
2
|
+
/** The slice of `npm pack --dry-run --json` this check reads. */
|
|
3
|
+
interface PackedFile {
|
|
4
|
+
path: string;
|
|
5
|
+
size: number;
|
|
6
|
+
}
|
|
7
|
+
interface PackManifest {
|
|
8
|
+
name: string;
|
|
9
|
+
version: string;
|
|
10
|
+
size: number;
|
|
11
|
+
unpackedSize: number;
|
|
12
|
+
entryCount: number;
|
|
13
|
+
files: PackedFile[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The manifest out of whatever `npm pack --json` printed.
|
|
17
|
+
*
|
|
18
|
+
* npm 10 prints `[ {manifest} ]`; npm 11 prints `{ "<name>": {manifest} }`. Reading
|
|
19
|
+
* only the first form turned every npm 11 runner into "could not describe this
|
|
20
|
+
* package" and the check into a permanent skip.
|
|
21
|
+
*/
|
|
22
|
+
export declare function pickManifest(parsed: unknown): PackManifest | null;
|
|
2
23
|
/** Why we could not read a published version. */
|
|
3
|
-
|
|
24
|
+
type QueryFailure = 'absent' | 'unauthenticated' | 'unreadable';
|
|
4
25
|
/**
|
|
5
26
|
* Tell "never published" apart from "we were not let in".
|
|
6
27
|
*
|
|
@@ -34,3 +55,4 @@ export declare function classifyQueryFailure(stderr: string): QueryFailure;
|
|
|
34
55
|
* { "Package Size": "error" }` makes it a gate for a repository that wants one.
|
|
35
56
|
*/
|
|
36
57
|
export declare const packageSizeCheck: Check;
|
|
58
|
+
export {};
|
|
@@ -8,6 +8,22 @@ import { logSample } from '../findings.js';
|
|
|
8
8
|
const DEFAULT_MAX_GROWTH = 20;
|
|
9
9
|
/** How many of the files behind the growth are named. */
|
|
10
10
|
const NAMED_FILES = 5;
|
|
11
|
+
/**
|
|
12
|
+
* The manifest out of whatever `npm pack --json` printed.
|
|
13
|
+
*
|
|
14
|
+
* npm 10 prints `[ {manifest} ]`; npm 11 prints `{ "<name>": {manifest} }`. Reading
|
|
15
|
+
* only the first form turned every npm 11 runner into "could not describe this
|
|
16
|
+
* package" and the check into a permanent skip.
|
|
17
|
+
*/
|
|
18
|
+
export function pickManifest(parsed) {
|
|
19
|
+
const entries = Array.isArray(parsed)
|
|
20
|
+
? parsed
|
|
21
|
+
: parsed !== null && typeof parsed === 'object'
|
|
22
|
+
? Object.values(parsed)
|
|
23
|
+
: [];
|
|
24
|
+
const first = entries[0];
|
|
25
|
+
return first !== null && typeof first === 'object' ? first : null;
|
|
26
|
+
}
|
|
11
27
|
/**
|
|
12
28
|
* Ask npm what a tarball would contain, without producing one.
|
|
13
29
|
*
|
|
@@ -41,10 +57,9 @@ async function packManifest(cwd, spec, registry) {
|
|
|
41
57
|
if (result.exitCode !== 0)
|
|
42
58
|
return { failure: classifyQueryFailure(result.stderr ?? '') };
|
|
43
59
|
try {
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
46
|
-
return { manifest
|
|
47
|
-
}
|
|
60
|
+
const manifest = pickManifest(JSON.parse(result.stdout));
|
|
61
|
+
if (manifest)
|
|
62
|
+
return { manifest };
|
|
48
63
|
}
|
|
49
64
|
catch {
|
|
50
65
|
// Fall through: npm answered with something that is not a manifest.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { getPackageConfigPath, hasLocalConfig, IGNORE_PATTERNS,
|
|
3
|
+
import { getPackageConfigPath, getSourcePaths, hasLocalConfig, IGNORE_PATTERNS, } from '../../../config/index.js';
|
|
4
4
|
import { execa } from '../../../utils/execa.js';
|
|
5
|
-
import {
|
|
5
|
+
import { cleanupTempFile, logger, matchesAnyGlob, registeredToolPaths, resolvePackageBin, } from '../../../utils/index.js';
|
|
6
6
|
import { resolveScopedTargetFiles } from '../../targets.js';
|
|
7
|
-
import { pass,
|
|
7
|
+
import { pass, skip, warn } from '../../types.js';
|
|
8
8
|
function loadUserCspellConfig(cwd) {
|
|
9
9
|
const configPath = path.join(cwd, 'pr-checkmate.json');
|
|
10
10
|
if (!fs.existsSync(configPath))
|
|
@@ -193,12 +193,15 @@ export function dropMissingLanguages(language, missing) {
|
|
|
193
193
|
*/
|
|
194
194
|
function resolveFrom(cwd) {
|
|
195
195
|
return id => {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
196
|
+
// The client's copy first, then the fetched `spellcheck` profile (cspell's own
|
|
197
|
+
// dictionaries live there), then our tree.
|
|
198
|
+
for (const root of [cwd, ...registeredToolPaths()]) {
|
|
199
|
+
try {
|
|
200
|
+
return require.resolve(id, { paths: [root] });
|
|
201
|
+
}
|
|
202
|
+
catch { }
|
|
201
203
|
}
|
|
204
|
+
return require.resolve(id);
|
|
202
205
|
};
|
|
203
206
|
}
|
|
204
207
|
function createTempCspellConfig(cwd) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DIFF_SECURITY_IGNORE_DEFAULTS } from '../../../config/index.js';
|
|
2
2
|
import { logger } from '../../../utils/index.js';
|
|
3
3
|
import { skip } from '../../types.js';
|
|
4
|
-
import { clientFindingOptions, emptyBuckets, mergeSeverityBuckets, reportSeverityBuckets,
|
|
4
|
+
import { clientFindingOptions, DEFAULT_DATAFLOW_WINDOW, emptyBuckets, mergeSeverityBuckets, reportSeverityBuckets, reviewLines, scanDiffPatterns, } from '../findings.js';
|
|
5
5
|
const MD5_LABEL = 'MD5 (weak hash — use sha256 or stronger)';
|
|
6
6
|
const SHA1_LABEL = 'SHA-1 (weak hash — use sha256 or stronger)';
|
|
7
7
|
const SECURITY_PATTERNS = [
|
|
@@ -4,7 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { getPackageConfigPath, hasLocalConfig } from '../../../config/index.js';
|
|
5
5
|
import { execa } from '../../../utils/execa.js';
|
|
6
6
|
import { logger, resolveGitRef, resolvePackageBin } from '../../../utils/index.js';
|
|
7
|
-
import { pass,
|
|
7
|
+
import { fail, pass, skip, warn } from '../../types.js';
|
|
8
8
|
/**
|
|
9
9
|
* A HOME the scanner can actually write to.
|
|
10
10
|
*
|
|
@@ -5,6 +5,7 @@ import { logger } from '../../utils/logger.js';
|
|
|
5
5
|
import { packageVersion } from '../../utils/package-version.js';
|
|
6
6
|
import { resolveCacheDir, stalePaths } from './cache.js';
|
|
7
7
|
import { cachedProfiles, ensureTooling } from './ensure.js';
|
|
8
|
+
import { grypeDbState } from './grype-db.js';
|
|
8
9
|
import { profileSatisfiedLocally } from './prepare.js';
|
|
9
10
|
import { planProfiles, PROFILES } from './profiles.js';
|
|
10
11
|
// Re-exported: `doctor` has been the home of this since ticket 064, and the
|
|
@@ -135,6 +136,17 @@ export async function runDoctor(cwd = process.cwd()) {
|
|
|
135
136
|
lines.push(` ${missing.length} profile(s) not installed yet. They are fetched on the next`);
|
|
136
137
|
lines.push(' run, or now with: npx pr-checkmate install');
|
|
137
138
|
}
|
|
139
|
+
if (config.grypeScan?.enabled !== false) {
|
|
140
|
+
const db = await grypeDbState(cwd);
|
|
141
|
+
lines.push('');
|
|
142
|
+
lines.push(' Grype database (shared by all projects on this machine)');
|
|
143
|
+
if (db.state === 'missing-tool')
|
|
144
|
+
lines.push(' — grype is not installed (brew install grype)');
|
|
145
|
+
else if (db.state === 'ready')
|
|
146
|
+
lines.push(` ✅ up to date${db.built ? ` (built ${db.built})` : ''}`);
|
|
147
|
+
else
|
|
148
|
+
lines.push(` ⬇️ stale${db.built ? ` (built ${db.built})` : ''} — refresh: npx pr-checkmate install`);
|
|
149
|
+
}
|
|
138
150
|
const stale = stalePaths(packageVersion(), config.install?.cacheDir);
|
|
139
151
|
if (stale.length > 0) {
|
|
140
152
|
lines.push('');
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
type GrypeDbState = {
|
|
2
|
+
state: 'missing-tool';
|
|
3
|
+
} | {
|
|
4
|
+
state: 'ready';
|
|
5
|
+
built?: string;
|
|
6
|
+
} | {
|
|
7
|
+
state: 'stale';
|
|
8
|
+
built?: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Is grype's vulnerability database usable right now? grype refuses one older than
|
|
12
|
+
* 5 days (exit code 1 from `db status`). The database lives in grype's own cache,
|
|
13
|
+
* shared by every project on the machine, so one refresh serves them all.
|
|
14
|
+
*/
|
|
15
|
+
export declare function grypeDbState(cwd: string): Promise<GrypeDbState>;
|
|
16
|
+
/**
|
|
17
|
+
* Download a fresh database when the current one is missing or stale.
|
|
18
|
+
*
|
|
19
|
+
* `timeoutMs` bounds the download: a check inside a run passes one so it can never
|
|
20
|
+
* look like a hung commit, `install` passes none because a person asked for exactly
|
|
21
|
+
* this and the download is the whole job. Returns why it is still unusable, or
|
|
22
|
+
* `null` when it is ready.
|
|
23
|
+
*/
|
|
24
|
+
export declare function ensureGrypeDb(cwd: string, timeoutMs?: number): Promise<string | null>;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { execa } from '../../utils/execa.js';
|
|
2
|
+
import { logger } from '../../utils/logger.js';
|
|
3
|
+
/** `Built:` line of `grype db status`, so `doctor` can say how old the data is. */
|
|
4
|
+
const builtOf = (stdout) => typeof stdout === 'string' ? /^Built:\s*(.+)$/m.exec(stdout)?.[1]?.trim() : undefined;
|
|
5
|
+
/**
|
|
6
|
+
* Is grype's vulnerability database usable right now? grype refuses one older than
|
|
7
|
+
* 5 days (exit code 1 from `db status`). The database lives in grype's own cache,
|
|
8
|
+
* shared by every project on the machine, so one refresh serves them all.
|
|
9
|
+
*/
|
|
10
|
+
export async function grypeDbState(cwd) {
|
|
11
|
+
const status = await execa('grype', ['db', 'status'], { cwd, reject: false, timeout: 30000 });
|
|
12
|
+
if (status.code === 'ENOENT')
|
|
13
|
+
return { state: 'missing-tool' };
|
|
14
|
+
const built = builtOf(status.stdout);
|
|
15
|
+
return status.exitCode === 0 ? { state: 'ready', built } : { state: 'stale', built };
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Download a fresh database when the current one is missing or stale.
|
|
19
|
+
*
|
|
20
|
+
* `timeoutMs` bounds the download: a check inside a run passes one so it can never
|
|
21
|
+
* look like a hung commit, `install` passes none because a person asked for exactly
|
|
22
|
+
* this and the download is the whole job. Returns why it is still unusable, or
|
|
23
|
+
* `null` when it is ready.
|
|
24
|
+
*/
|
|
25
|
+
export async function ensureGrypeDb(cwd, timeoutMs) {
|
|
26
|
+
const current = await grypeDbState(cwd);
|
|
27
|
+
if (current.state === 'missing-tool')
|
|
28
|
+
return 'ENOENT';
|
|
29
|
+
if (current.state === 'ready')
|
|
30
|
+
return null;
|
|
31
|
+
logger.info('[Grype Scan]: vulnerability database missing or stale — updating it');
|
|
32
|
+
const update = await execa('grype', ['db', 'update'], {
|
|
33
|
+
cwd,
|
|
34
|
+
reject: false,
|
|
35
|
+
...(timeoutMs ? { timeout: timeoutMs } : {}),
|
|
36
|
+
});
|
|
37
|
+
if (update.timedOut) {
|
|
38
|
+
return `database update exceeded ${timeoutMs}ms — run "grype db update" once, then retry`;
|
|
39
|
+
}
|
|
40
|
+
return update.exitCode === 0 ? null : 'database update failed (offline?) — run "grype db update"';
|
|
41
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { logger } from '../../utils/logger.js';
|
|
2
2
|
import { clearToolPaths, packagePresent, registerToolPaths } from '../../utils/resolve-bin.js';
|
|
3
3
|
import { ensureTooling } from './ensure.js';
|
|
4
|
-
import {
|
|
4
|
+
import { planProfiles, PROFILES } from './profiles.js';
|
|
5
5
|
/**
|
|
6
6
|
* Whether a profile needs fetching at all, or already ships inside the package.
|
|
7
7
|
*
|
|
@@ -3,7 +3,7 @@ import type { Language, LanguageUsage } from '../../utils/detect-language.js';
|
|
|
3
3
|
/**
|
|
4
4
|
* A named bundle of tooling, and the checks that stop working without it.
|
|
5
5
|
*
|
|
6
|
-
* The
|
|
6
|
+
* The tooling totals ~233 MB, and no repository needs all of it: a
|
|
7
7
|
* Python service pays for the whole JavaScript toolchain, a repository with
|
|
8
8
|
* Spellcheck switched off still pays for cspell. Profiles are how that stops —
|
|
9
9
|
* they name which slice of the tooling a given repository actually needs.
|
|
@@ -20,9 +20,9 @@ export const PROFILES = [
|
|
|
20
20
|
'typescript',
|
|
21
21
|
'eslint',
|
|
22
22
|
'eslint-config-prettier',
|
|
23
|
-
'eslint-plugin-import-x',
|
|
24
23
|
'eslint-plugin-jsdoc',
|
|
25
24
|
'eslint-plugin-prettier',
|
|
25
|
+
'eslint-plugin-simple-import-sort',
|
|
26
26
|
'eslint-plugin-sonarjs',
|
|
27
27
|
'@typescript-eslint/eslint-plugin',
|
|
28
28
|
'@typescript-eslint/parser',
|
|
@@ -5,15 +5,18 @@ import { ownPackageRoot } from '../../utils/resolve-bin.js';
|
|
|
5
5
|
/**
|
|
6
6
|
* The major version we declare for a tool, from our own manifest.
|
|
7
7
|
*
|
|
8
|
-
* Read from
|
|
8
|
+
* Read from the manifest rather than from the installed tree: what matters is
|
|
9
9
|
* the version our configuration was written against, and a hoisted install can
|
|
10
10
|
* resolve to something else entirely.
|
|
11
11
|
*/
|
|
12
12
|
function ownMajor(pkgName) {
|
|
13
13
|
try {
|
|
14
14
|
const manifest = path.join(ownPackageRoot(), 'package.json');
|
|
15
|
-
const
|
|
16
|
-
|
|
15
|
+
const parsed = JSON.parse(fs.readFileSync(manifest, 'utf-8'));
|
|
16
|
+
// Profile tooling is declared in `devDependencies` — it is fetched on demand
|
|
17
|
+
// rather than installed with us — but it is still the version our config was
|
|
18
|
+
// written against, and the manifest keeps both.
|
|
19
|
+
const range = parsed.dependencies?.[pkgName] ?? parsed.devDependencies?.[pkgName];
|
|
17
20
|
if (!range)
|
|
18
21
|
return null;
|
|
19
22
|
// `^10.6.0`, `>=3.0.0`, `~5.1` — the leading number is the major in every
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { isCiEnvironment, logger } from '../utils/index.js';
|
|
2
2
|
import { packageVersion } from '../utils/package-version.js';
|
|
3
|
-
import {
|
|
3
|
+
import { resolveConcurrency, runPool, Semaphore, withTimeout } from '../utils/pool.js';
|
|
4
4
|
import { clearToolPaths } from '../utils/resolve-bin.js';
|
|
5
5
|
import { ResultCollector } from '../utils/result-collector.js';
|
|
6
6
|
import { buildContext } from './context.js';
|
|
7
|
-
import {
|
|
7
|
+
import { noTooling, prepareTooling } from './install/prepare.js';
|
|
8
8
|
import { REGISTRY } from './registry.js';
|
|
9
9
|
import { report } from './reporters.js';
|
|
10
10
|
import { PHASES, } from './types.js';
|
package/dist/esm/init.d.ts
CHANGED
|
@@ -30,14 +30,6 @@ export declare function setupWorkflow(root: string, mode: GateMode): void;
|
|
|
30
30
|
* heart about what should block.
|
|
31
31
|
*/
|
|
32
32
|
export declare const BLOCKING_BY_DEFAULT: readonly ["Security Scan", "Sensitive Files", "Diff Security", "Workflow Security", "Merge Conflict", "NPM Audit", "License Check", "Dependencies", "TypeScript", "ESLint", "YAML Lint"];
|
|
33
|
-
/**
|
|
34
|
-
* Every option the template documents that the file does not already carry, at
|
|
35
|
-
* its default. Without this, options such as `vulnScan` (off by default) or
|
|
36
|
-
* `licenseCheck` exist only in the docs, and nobody edits what they cannot see.
|
|
37
|
-
* Comment keys travel with the option they describe, section headers with the
|
|
38
|
-
* first option after them.
|
|
39
|
-
*/
|
|
40
|
-
export declare function templateExtras(existing: Record<string, unknown>): Record<string, unknown>;
|
|
41
33
|
export declare function buildInitialConfig(sourcePath: string, languages: string[]): Record<string, unknown>;
|
|
42
34
|
/**
|
|
43
35
|
* Initialize (or refresh) pr-checkmate configuration in the current project.
|
package/dist/esm/init.js
CHANGED
|
@@ -2,11 +2,11 @@ import { fileURLToPath as __fileURLToPath } from 'node:url';
|
|
|
2
2
|
import { dirname as __dirnameFn } from 'node:path';
|
|
3
3
|
const __dirname = __dirnameFn(__fileURLToPath(import.meta.url));
|
|
4
4
|
import fs from 'fs';
|
|
5
|
-
import path from 'path';
|
|
6
5
|
import inquirer from 'inquirer';
|
|
7
|
-
import
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { DEAD_CODE_IGNORE_DEFAULTS, DEFAULT_COMMANDS, DIFF_SECURITY_IGNORE_DEFAULTS, DOCS_URL, documentation, DUPLICATE_IGNORE_DEFAULTS, FULL_CONFIG_URL, GENERATED_DOC_FILES, IGNORED_DIRS, } from './config/index.js';
|
|
8
8
|
import { DEFAULT_DATAFLOW_WINDOW } from './core/checks/findings.js';
|
|
9
|
-
import {
|
|
9
|
+
import { detectLanguages, findOwnPackageRoot, logger } from './utils/index.js';
|
|
10
10
|
const CONFIG_FILE = path.resolve(process.cwd(), 'pr-checkmate.json');
|
|
11
11
|
/**
|
|
12
12
|
* Points editors at the bundled JSON schema. With the generated config reduced
|
|
@@ -286,7 +286,7 @@ const GRYPE_SCAN_SEED = {
|
|
|
286
286
|
* Comment keys travel with the option they describe, section headers with the
|
|
287
287
|
* first option after them.
|
|
288
288
|
*/
|
|
289
|
-
|
|
289
|
+
function templateExtras(existing) {
|
|
290
290
|
const templatePath = path.join(__dirname, 'config', 'pr-checkmate.template.json');
|
|
291
291
|
if (!fs.existsSync(templatePath)) {
|
|
292
292
|
return {};
|
|
@@ -3,6 +3,7 @@ import { dirname as __dirnameFn } from 'node:path';
|
|
|
3
3
|
const __dirname = __dirnameFn(__fileURLToPath(import.meta.url));
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
import { PROFILES } from '../core/install/profiles.js';
|
|
6
7
|
const OWN_PACKAGE_NAME = 'pr-checkmate';
|
|
7
8
|
const PACKAGE_JSON = 'package.json';
|
|
8
9
|
let cachedClosure;
|
|
@@ -85,7 +86,13 @@ export function getOwnDependencyClosure() {
|
|
|
85
86
|
}
|
|
86
87
|
// BFS over the dependency graph, resolving each package relative to the parent
|
|
87
88
|
// that declared it so nested (non-hoisted) installs resolve correctly.
|
|
88
|
-
|
|
89
|
+
//
|
|
90
|
+
// Profile tooling is ours even though it no longer sits in `dependencies`: it is
|
|
91
|
+
// fetched on demand, so the roots are what we declare plus what the profiles
|
|
92
|
+
// name. Without the second half every fetched tool is "the client's" and its
|
|
93
|
+
// licence blocks the client for a dependency they cannot remove.
|
|
94
|
+
const roots = new Set([...readDependencyNames(root), ...PROFILES.flatMap(p => p.packages)]);
|
|
95
|
+
const queue = [...roots].map(name => ({
|
|
89
96
|
name,
|
|
90
97
|
fromDir: root,
|
|
91
98
|
}));
|
|
@@ -16,6 +16,8 @@ export interface ResolvedTool {
|
|
|
16
16
|
* to a run from before there was anything to fetch.
|
|
17
17
|
*/
|
|
18
18
|
export declare function registerToolPaths(dirs: string[]): void;
|
|
19
|
+
/** The roots registered for this run, for tools that load plugins by themselves. */
|
|
20
|
+
export declare function registeredToolPaths(): string[];
|
|
19
21
|
/** Drop every registered profile root. For tests, and for a second run in-process. */
|
|
20
22
|
export declare function clearToolPaths(): void;
|
|
21
23
|
/** The root of the installed pr-checkmate package — where our own tooling lives. */
|
|
@@ -25,6 +25,10 @@ let profileRoots = [];
|
|
|
25
25
|
export function registerToolPaths(dirs) {
|
|
26
26
|
profileRoots = [...dirs];
|
|
27
27
|
}
|
|
28
|
+
/** The roots registered for this run, for tools that load plugins by themselves. */
|
|
29
|
+
export function registeredToolPaths() {
|
|
30
|
+
return [...profileRoots];
|
|
31
|
+
}
|
|
28
32
|
/** Drop every registered profile root. For tests, and for a second run in-process. */
|
|
29
33
|
export function clearToolPaths() {
|
|
30
34
|
profileRoots = [];
|
|
@@ -26,14 +26,8 @@
|
|
|
26
26
|
"sonarjs/cognitive-complexity": ["warn", 30],
|
|
27
27
|
"sonarjs/no-duplicate-string": ["warn", { "threshold": 3 }],
|
|
28
28
|
"sonarjs/no-identical-functions": "warn",
|
|
29
|
-
"import/
|
|
30
|
-
|
|
31
|
-
{
|
|
32
|
-
"groups": ["builtin", "external", "internal", "parent", "sibling", "index"],
|
|
33
|
-
"alphabetize": { "order": "asc", "caseInsensitive": true }
|
|
34
|
-
}
|
|
35
|
-
],
|
|
36
|
-
"import/no-duplicates": "error",
|
|
29
|
+
"simple-import-sort/imports": "warn",
|
|
30
|
+
"no-duplicate-imports": ["error", { "allowSeparateTypeImports": true }],
|
|
37
31
|
"jsdoc/check-param-names": "warn",
|
|
38
32
|
"jsdoc/check-tag-names": "warn",
|
|
39
33
|
"jsdoc/check-types": "warn",
|