pr-checkmate 1.23.2 → 1.23.4
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 +5 -4
- package/cspell.json +963 -453
- package/dist/config/constants.js +1 -0
- package/dist/config/loader.d.ts +1 -1
- package/dist/config/loader.js +7 -3
- package/dist/core/checks/dependencies/grype-scan.d.ts +17 -0
- package/dist/core/checks/dependencies/grype-scan.js +90 -0
- package/dist/core/registry.js +2 -0
- package/dist/init.js +4 -1
- package/dist/interfaces/index.d.ts +5 -0
- package/dist/utils/direct-deps.d.ts +21 -0
- package/dist/utils/direct-deps.js +225 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/package.json +3 -3
- package/pr-checkmate.schema.json +11 -0
- package/src/config/cspell.json +963 -453
- package/src/config/pr-checkmate.schema.json +11 -0
package/dist/config/constants.js
CHANGED
package/dist/config/loader.d.ts
CHANGED
|
@@ -3,4 +3,4 @@ import { PRCheckMateConfig } from '../interfaces';
|
|
|
3
3
|
* Load project configuration from pr-checkmate.json
|
|
4
4
|
* Falls back to defaults if not found
|
|
5
5
|
*/
|
|
6
|
-
export declare function loadProjectConfig(): PRCheckMateConfig;
|
|
6
|
+
export declare function loadProjectConfig(cwd?: string): PRCheckMateConfig;
|
package/dist/config/loader.js
CHANGED
|
@@ -12,9 +12,8 @@ const constants_1 = require("./constants");
|
|
|
12
12
|
* Load project configuration from pr-checkmate.json
|
|
13
13
|
* Falls back to defaults if not found
|
|
14
14
|
*/
|
|
15
|
-
function loadProjectConfig() {
|
|
16
|
-
const
|
|
17
|
-
const configPath = node_path_1.default.join(projectRoot, 'pr-checkmate.json');
|
|
15
|
+
function loadProjectConfig(cwd = process.cwd()) {
|
|
16
|
+
const configPath = node_path_1.default.join(cwd, 'pr-checkmate.json');
|
|
18
17
|
if (node_fs_1.default.existsSync(configPath)) {
|
|
19
18
|
try {
|
|
20
19
|
const content = node_fs_1.default.readFileSync(configPath, 'utf-8');
|
|
@@ -33,6 +32,11 @@ function loadProjectConfig() {
|
|
|
33
32
|
function mergeWithDefaults(userConfig) {
|
|
34
33
|
const defaults = getDefaultConfig();
|
|
35
34
|
return {
|
|
35
|
+
// Preserve every key the user set — including opt-in checks (vulnScan,
|
|
36
|
+
// grypeScan) and controls with no default entry below (severity, customRules,
|
|
37
|
+
// prSize, caseCollision, go/rust/ruby/php, …). The explicitly-merged keys
|
|
38
|
+
// that follow override this spread with their default-merged values.
|
|
39
|
+
...userConfig,
|
|
36
40
|
sourcePath: userConfig.sourcePath || defaults.sourcePath,
|
|
37
41
|
// Replace (not merge): the client sees the full default list in their
|
|
38
42
|
// generated config and edits it directly, so their list is authoritative.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Check } from '../../types';
|
|
2
|
+
/**
|
|
3
|
+
* Dependency vulnerability scan via Anchore's `grype`. Non-blocking and ON by
|
|
4
|
+
* default — runs unless `grypeScan.enabled` is explicitly `false`. grype is a
|
|
5
|
+
* runner-dependency: it must be on the runner, and the check skips gracefully
|
|
6
|
+
* (not fails) when it is absent.
|
|
7
|
+
*
|
|
8
|
+
* grype resolves versions from lockfiles, so its raw output includes the entire
|
|
9
|
+
* transitive tree — and, because pr-checkmate installs into the consumer's
|
|
10
|
+
* node_modules, its own bundled dependencies too. Neither is something the
|
|
11
|
+
* consumer can fix. We therefore report ONLY vulnerabilities in DIRECT
|
|
12
|
+
* dependencies (those declared in the project's manifests, see
|
|
13
|
+
* `collectDirectDeps`): the set the maintainer actually controls. Findings whose
|
|
14
|
+
* ecosystem has no manifest we can parse are suppressed too, and the count of
|
|
15
|
+
* everything dropped is logged so nothing is hidden silently.
|
|
16
|
+
*/
|
|
17
|
+
export declare const grypeScanCheck: Check;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.grypeScanCheck = void 0;
|
|
4
|
+
const utils_1 = require("../../../utils");
|
|
5
|
+
const execa_1 = require("../../../utils/execa");
|
|
6
|
+
const types_1 = require("../../types");
|
|
7
|
+
const SEVERITY_ORDER = ['negligible', 'low', 'medium', 'high', 'critical'];
|
|
8
|
+
/** grype artifact `type` → the ecosystem we can resolve direct deps for. */
|
|
9
|
+
const TYPE_TO_ECOSYSTEM = {
|
|
10
|
+
npm: 'npm',
|
|
11
|
+
python: 'python',
|
|
12
|
+
'go-module': 'go',
|
|
13
|
+
'rust-crate': 'rust',
|
|
14
|
+
gem: 'ruby',
|
|
15
|
+
'php-composer': 'php',
|
|
16
|
+
};
|
|
17
|
+
function severityRank(severity) {
|
|
18
|
+
return SEVERITY_ORDER.indexOf((severity ?? '').toLowerCase());
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Dependency vulnerability scan via Anchore's `grype`. Non-blocking and ON by
|
|
22
|
+
* default — runs unless `grypeScan.enabled` is explicitly `false`. grype is a
|
|
23
|
+
* runner-dependency: it must be on the runner, and the check skips gracefully
|
|
24
|
+
* (not fails) when it is absent.
|
|
25
|
+
*
|
|
26
|
+
* grype resolves versions from lockfiles, so its raw output includes the entire
|
|
27
|
+
* transitive tree — and, because pr-checkmate installs into the consumer's
|
|
28
|
+
* node_modules, its own bundled dependencies too. Neither is something the
|
|
29
|
+
* consumer can fix. We therefore report ONLY vulnerabilities in DIRECT
|
|
30
|
+
* dependencies (those declared in the project's manifests, see
|
|
31
|
+
* `collectDirectDeps`): the set the maintainer actually controls. Findings whose
|
|
32
|
+
* ecosystem has no manifest we can parse are suppressed too, and the count of
|
|
33
|
+
* everything dropped is logged so nothing is hidden silently.
|
|
34
|
+
*/
|
|
35
|
+
exports.grypeScanCheck = {
|
|
36
|
+
name: 'Grype Scan',
|
|
37
|
+
phase: 'informational',
|
|
38
|
+
applies(ctx) {
|
|
39
|
+
return ctx.config.grypeScan?.enabled !== false;
|
|
40
|
+
},
|
|
41
|
+
async run(ctx) {
|
|
42
|
+
const minSeverity = (ctx.config.grypeScan?.minSeverity ?? 'medium').toLowerCase();
|
|
43
|
+
const minRank = severityRank(minSeverity);
|
|
44
|
+
const ignore = (ctx.config.grypeScan?.ignore ?? []).map(s => s.toLowerCase());
|
|
45
|
+
const result = await (0, execa_1.execa)('grype', ['dir:.', '-o', 'json', '-q'], {
|
|
46
|
+
cwd: ctx.cwd,
|
|
47
|
+
reject: false,
|
|
48
|
+
});
|
|
49
|
+
// execa v9 with reject:false resolves (not throws) with `code: 'ENOENT'`
|
|
50
|
+
// when the binary is absent — detect the missing scanner on the result.
|
|
51
|
+
if (result.code === 'ENOENT')
|
|
52
|
+
return (0, types_1.skip)('grype not installed');
|
|
53
|
+
let report;
|
|
54
|
+
try {
|
|
55
|
+
report = JSON.parse(result.stdout);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return (0, types_1.warn)('grype produced no parseable output');
|
|
59
|
+
}
|
|
60
|
+
const matches = report.matches ?? [];
|
|
61
|
+
const direct = (0, utils_1.collectDirectDeps)(ctx.cwd);
|
|
62
|
+
// Keep only: severity >= threshold, a direct dependency, and not ignored.
|
|
63
|
+
const isDirect = (m) => {
|
|
64
|
+
const ecosystem = TYPE_TO_ECOSYSTEM[m.artifact?.type ?? ''];
|
|
65
|
+
const names = ecosystem && direct.get(ecosystem);
|
|
66
|
+
if (!names)
|
|
67
|
+
return false; // unknown/unparseable ecosystem → not our concern
|
|
68
|
+
return names.has((0, utils_1.normalizeName)(ecosystem, m.artifact?.name ?? ''));
|
|
69
|
+
};
|
|
70
|
+
const isIgnored = (m) => ignore.includes((m.vulnerability?.id ?? '').toLowerCase()) ||
|
|
71
|
+
ignore.includes((m.artifact?.name ?? '').toLowerCase());
|
|
72
|
+
const atSeverity = matches.filter(m => severityRank(m.vulnerability?.severity) >= minRank);
|
|
73
|
+
const findings = atSeverity.filter(m => isDirect(m) && !isIgnored(m));
|
|
74
|
+
const suppressed = atSeverity.length - findings.length;
|
|
75
|
+
if (suppressed > 0) {
|
|
76
|
+
utils_1.logger.info(`[Grype Scan]: suppressed ${suppressed} finding(s) — transitive/third-party (not directly fixable) or ignored`);
|
|
77
|
+
}
|
|
78
|
+
if (findings.length === 0) {
|
|
79
|
+
utils_1.logger.info('[Grype Scan]: ✅ No known vulnerabilities in direct dependencies');
|
|
80
|
+
return (0, types_1.pass)();
|
|
81
|
+
}
|
|
82
|
+
utils_1.logger.warn(`[Grype Scan]: ⚠️ vulnerabilities in direct dependencies (>= ${minSeverity}):`);
|
|
83
|
+
findings
|
|
84
|
+
.slice(0, 10)
|
|
85
|
+
.forEach(m => utils_1.logger.warn(` ${m.vulnerability?.id ?? 'unknown'} — ${m.artifact?.name ?? '?'}@${m.artifact?.version ?? '?'} (${m.vulnerability?.severity ?? '?'})`));
|
|
86
|
+
if (findings.length > 10)
|
|
87
|
+
utils_1.logger.warn(` ... and ${findings.length - 10} more`);
|
|
88
|
+
return (0, types_1.warn)(`${findings.length} direct-dependency vulnerability(ies) at "${minSeverity}"+ (grype)`);
|
|
89
|
+
},
|
|
90
|
+
};
|
package/dist/core/registry.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.REGISTRY = void 0;
|
|
|
4
4
|
const banned_imports_1 = require("./checks/dependencies/banned-imports");
|
|
5
5
|
const circular_deps_1 = require("./checks/dependencies/circular-deps");
|
|
6
6
|
const dependency_check_1 = require("./checks/dependencies/dependency-check");
|
|
7
|
+
const grype_scan_1 = require("./checks/dependencies/grype-scan");
|
|
7
8
|
const license_check_1 = require("./checks/dependencies/license-check");
|
|
8
9
|
const npm_audit_1 = require("./checks/dependencies/npm-audit");
|
|
9
10
|
const outdated_deps_1 = require("./checks/dependencies/outdated-deps");
|
|
@@ -71,6 +72,7 @@ exports.REGISTRY = [
|
|
|
71
72
|
dockerfile_security_1.dockerfileSecurityCheck,
|
|
72
73
|
case_collision_1.caseCollisionCheck,
|
|
73
74
|
vuln_scan_1.vulnScanCheck,
|
|
75
|
+
grype_scan_1.grypeScanCheck,
|
|
74
76
|
custom_rules_1.customRulesCheck,
|
|
75
77
|
banned_imports_1.bannedImportsCheck,
|
|
76
78
|
license_header_1.licenseHeaderCheck,
|
package/dist/init.js
CHANGED
|
@@ -31,6 +31,9 @@ const DEFAULT_CONFIG = {
|
|
|
31
31
|
// Points editors (VS Code, etc.) at the bundled JSON schema for autocomplete
|
|
32
32
|
// and inline validation of this file.
|
|
33
33
|
$schema: './node_modules/pr-checkmate/pr-checkmate.schema.json',
|
|
34
|
+
// Available CLI commands — kept at the very top as a quick reference.
|
|
35
|
+
// Informational only; PR CheckMate ignores this at runtime.
|
|
36
|
+
commands: config_1.DEFAULT_COMMANDS,
|
|
34
37
|
// Human-readable hints. JSON has no comments, so these `//`-prefixed keys are
|
|
35
38
|
// just informational strings — PR CheckMate ignores them at runtime.
|
|
36
39
|
'// reporting': [
|
|
@@ -44,7 +47,6 @@ const DEFAULT_CONFIG = {
|
|
|
44
47
|
// Directories excluded from every check (matched on any path segment). Edit
|
|
45
48
|
// freely — add your own generated/vendored dirs or remove ones you want scanned.
|
|
46
49
|
ignoreDirs: config_1.IGNORED_DIRS,
|
|
47
|
-
commands: config_1.DEFAULT_COMMANDS,
|
|
48
50
|
// ESLint — the full default rule set is written out so every rule is visible
|
|
49
51
|
// and editable. Change any value, or set a rule to "off" to disable it.
|
|
50
52
|
eslint: {
|
|
@@ -134,6 +136,7 @@ const DEFAULT_CONFIG = {
|
|
|
134
136
|
dockerfileSecurity: { enabled: true }, // Dockerfile: :latest, USER root, curl|bash, …
|
|
135
137
|
caseCollision: { enabled: true }, // files differing only by case
|
|
136
138
|
vulnScan: { enabled: false }, // cross-language dep vulns (osv-scanner, runner-dependency)
|
|
139
|
+
grypeScan: { enabled: true, minSeverity: 'medium' }, // dep vulns (grype, non-blocking, runner-dependency; skips if grype absent)
|
|
137
140
|
// Test-coverage presence. Opt-in: enable once your CI produces a coverage
|
|
138
141
|
// report (lcov/cobertura/jacoco/…); warns if none is found.
|
|
139
142
|
coverage: { enabled: false },
|
|
@@ -199,6 +199,11 @@ export interface PRCheckMateConfig {
|
|
|
199
199
|
vulnScan?: {
|
|
200
200
|
enabled?: boolean;
|
|
201
201
|
};
|
|
202
|
+
grypeScan?: {
|
|
203
|
+
enabled?: boolean;
|
|
204
|
+
minSeverity?: 'negligible' | 'low' | 'medium' | 'high' | 'critical';
|
|
205
|
+
ignore?: string[];
|
|
206
|
+
};
|
|
202
207
|
bannedImports?: Array<{
|
|
203
208
|
name: string;
|
|
204
209
|
message?: string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package ecosystems for which we can determine a project's DIRECT (declared)
|
|
3
|
+
* dependencies from a manifest — the set a maintainer actually controls and can
|
|
4
|
+
* bump. Transitive packages (resolved only in a lockfile) are deliberately not
|
|
5
|
+
* here: the consumer cannot fix them directly, so a vuln scan must not flag them.
|
|
6
|
+
*/
|
|
7
|
+
export type Ecosystem = 'npm' | 'python' | 'go' | 'rust' | 'ruby' | 'php';
|
|
8
|
+
/** Canonical key for name comparison within an ecosystem. */
|
|
9
|
+
export declare function normalizeName(ecosystem: Ecosystem, name: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Build the set of DIRECT (declared) dependency names for `cwd`, keyed by
|
|
12
|
+
* ecosystem, by parsing manifests — not lockfiles. Used to filter vulnerability
|
|
13
|
+
* scanner output down to what the consumer actually controls: transitive
|
|
14
|
+
* dependencies (present only in lockfiles) and the scanning tool's own bundled
|
|
15
|
+
* dependencies are absent from these manifests and thus never reported.
|
|
16
|
+
*
|
|
17
|
+
* Dependency-free and best-effort: unparseable/absent manifests contribute
|
|
18
|
+
* nothing rather than throwing. An ecosystem with no manifest yields no entry
|
|
19
|
+
* (callers should treat "no entry" as "cannot classify" — see grype-scan).
|
|
20
|
+
*/
|
|
21
|
+
export declare function collectDirectDeps(cwd: string): Map<Ecosystem, Set<string>>;
|
|
@@ -0,0 +1,225 @@
|
|
|
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.normalizeName = normalizeName;
|
|
7
|
+
exports.collectDirectDeps = collectDirectDeps;
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
/** Directories never worth descending into when locating manifests. */
|
|
11
|
+
const SKIP_DIRS = new Set([
|
|
12
|
+
'node_modules',
|
|
13
|
+
'.git',
|
|
14
|
+
'dist',
|
|
15
|
+
'build',
|
|
16
|
+
'vendor',
|
|
17
|
+
'.venv',
|
|
18
|
+
'venv',
|
|
19
|
+
'target',
|
|
20
|
+
]);
|
|
21
|
+
/** PEP 503 normalisation, applied to both manifest and scanner names for Python. */
|
|
22
|
+
function normalizePython(name) {
|
|
23
|
+
return name
|
|
24
|
+
.trim()
|
|
25
|
+
.toLowerCase()
|
|
26
|
+
.replace(/[-_.]+/g, '-');
|
|
27
|
+
}
|
|
28
|
+
/** Canonical key for name comparison within an ecosystem. */
|
|
29
|
+
function normalizeName(ecosystem, name) {
|
|
30
|
+
return ecosystem === 'python' ? normalizePython(name) : name.trim().toLowerCase();
|
|
31
|
+
}
|
|
32
|
+
function readJson(file) {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(node_fs_1.default.readFileSync(file, 'utf-8'));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function readText(file) {
|
|
41
|
+
try {
|
|
42
|
+
return node_fs_1.default.readFileSync(file, 'utf-8');
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Collect direct dep names from a package.json's four dependency maps. */
|
|
49
|
+
function addNpm(file, out) {
|
|
50
|
+
const pkg = readJson(file);
|
|
51
|
+
if (!pkg)
|
|
52
|
+
return;
|
|
53
|
+
for (const key of [
|
|
54
|
+
'dependencies',
|
|
55
|
+
'devDependencies',
|
|
56
|
+
'peerDependencies',
|
|
57
|
+
'optionalDependencies',
|
|
58
|
+
]) {
|
|
59
|
+
const map = pkg[key];
|
|
60
|
+
if (map && typeof map === 'object') {
|
|
61
|
+
for (const name of Object.keys(map))
|
|
62
|
+
out.add(normalizeName('npm', name));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Extract the bare package name from a Python requirement specifier line. */
|
|
67
|
+
function pythonReqName(line) {
|
|
68
|
+
const stripped = line.split('#')[0].trim();
|
|
69
|
+
if (!stripped || stripped.startsWith('-'))
|
|
70
|
+
return null; // skip options like -r / -e
|
|
71
|
+
const match = /^([A-Za-z0-9._-]+)/.exec(stripped);
|
|
72
|
+
return match ? normalizePython(match[1]) : null;
|
|
73
|
+
}
|
|
74
|
+
function addPython(cwd, out, manifests) {
|
|
75
|
+
for (const file of manifests) {
|
|
76
|
+
const base = node_path_1.default.basename(file);
|
|
77
|
+
if (base.startsWith('requirements') && base.endsWith('.txt')) {
|
|
78
|
+
const text = readText(file);
|
|
79
|
+
if (text)
|
|
80
|
+
text.split('\n').forEach(l => {
|
|
81
|
+
const n = pythonReqName(l);
|
|
82
|
+
if (n)
|
|
83
|
+
out.add(n);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
else if (base === 'pyproject.toml' || base === 'Pipfile') {
|
|
87
|
+
const text = readText(file);
|
|
88
|
+
if (!text)
|
|
89
|
+
continue;
|
|
90
|
+
// Best-effort, parser-free: PEP 621 `dependencies = ["pkg>=1", ...]`,
|
|
91
|
+
// Poetry/Pipfile `pkg = "^1.2"` table entries. Section-agnostic on purpose.
|
|
92
|
+
for (const m of text.matchAll(/["']([A-Za-z0-9._-]+)\s*(?:[<>=!~;)\[]|["'])/g)) {
|
|
93
|
+
const n = normalizePython(m[1]);
|
|
94
|
+
if (n && n !== 'python')
|
|
95
|
+
out.add(n);
|
|
96
|
+
}
|
|
97
|
+
for (const m of text.matchAll(/^\s*([A-Za-z0-9._-]+)\s*=/gm)) {
|
|
98
|
+
const n = normalizePython(m[1]);
|
|
99
|
+
if (n && n !== 'python')
|
|
100
|
+
out.add(n);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function addGo(file, out) {
|
|
106
|
+
const text = readText(file);
|
|
107
|
+
if (!text)
|
|
108
|
+
return;
|
|
109
|
+
// `require ( ... )` block and single-line `require x v...`; skip `// indirect`.
|
|
110
|
+
for (const raw of text.split('\n')) {
|
|
111
|
+
const line = raw.trim();
|
|
112
|
+
if (line.includes('// indirect'))
|
|
113
|
+
continue;
|
|
114
|
+
const m = /^(?:require\s+)?([a-zA-Z0-9._~/-]+\.[a-zA-Z0-9._~/-]+)\s+v/.exec(line);
|
|
115
|
+
if (m)
|
|
116
|
+
out.add(m[1].toLowerCase());
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function addRust(file, out) {
|
|
120
|
+
const text = readText(file);
|
|
121
|
+
if (!text)
|
|
122
|
+
return;
|
|
123
|
+
// Keys under [dependencies] / [dev-dependencies] / [build-dependencies].
|
|
124
|
+
let inDeps = false;
|
|
125
|
+
for (const raw of text.split('\n')) {
|
|
126
|
+
const line = raw.trim();
|
|
127
|
+
if (line.startsWith('[')) {
|
|
128
|
+
inDeps = /^\[(?:.*\.)?(?:dev-|build-)?dependencies\]$/.test(line);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (!inDeps)
|
|
132
|
+
continue;
|
|
133
|
+
const m = /^([A-Za-z0-9._-]+)\s*=/.exec(line);
|
|
134
|
+
if (m)
|
|
135
|
+
out.add(m[1].toLowerCase());
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function addRuby(file, out) {
|
|
139
|
+
const text = readText(file);
|
|
140
|
+
if (!text)
|
|
141
|
+
return;
|
|
142
|
+
for (const m of text.matchAll(/^\s*gem\s+["']([A-Za-z0-9._-]+)["']/gm)) {
|
|
143
|
+
out.add(m[1].toLowerCase());
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function addPhp(file, out) {
|
|
147
|
+
const pkg = readJson(file);
|
|
148
|
+
if (!pkg)
|
|
149
|
+
return;
|
|
150
|
+
for (const key of ['require', 'require-dev']) {
|
|
151
|
+
const map = pkg[key];
|
|
152
|
+
if (map && typeof map === 'object') {
|
|
153
|
+
for (const name of Object.keys(map)) {
|
|
154
|
+
if (name === 'php' || name.startsWith('ext-'))
|
|
155
|
+
continue;
|
|
156
|
+
out.add(name.toLowerCase());
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** Recursively list files matching any of `names`, skipping vendored dirs. */
|
|
162
|
+
function findManifests(cwd, names, matchPrefix) {
|
|
163
|
+
const found = [];
|
|
164
|
+
const walk = (dir) => {
|
|
165
|
+
let entries;
|
|
166
|
+
try {
|
|
167
|
+
entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
for (const entry of entries) {
|
|
173
|
+
if (entry.isDirectory()) {
|
|
174
|
+
if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
|
|
175
|
+
walk(node_path_1.default.join(dir, entry.name));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else if (names.has(entry.name) || (matchPrefix && matchPrefix.test(entry.name))) {
|
|
179
|
+
found.push(node_path_1.default.join(dir, entry.name));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
walk(cwd);
|
|
184
|
+
return found;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Build the set of DIRECT (declared) dependency names for `cwd`, keyed by
|
|
188
|
+
* ecosystem, by parsing manifests — not lockfiles. Used to filter vulnerability
|
|
189
|
+
* scanner output down to what the consumer actually controls: transitive
|
|
190
|
+
* dependencies (present only in lockfiles) and the scanning tool's own bundled
|
|
191
|
+
* dependencies are absent from these manifests and thus never reported.
|
|
192
|
+
*
|
|
193
|
+
* Dependency-free and best-effort: unparseable/absent manifests contribute
|
|
194
|
+
* nothing rather than throwing. An ecosystem with no manifest yields no entry
|
|
195
|
+
* (callers should treat "no entry" as "cannot classify" — see grype-scan).
|
|
196
|
+
*/
|
|
197
|
+
function collectDirectDeps(cwd) {
|
|
198
|
+
const result = new Map();
|
|
199
|
+
const npm = new Set();
|
|
200
|
+
findManifests(cwd, new Set(['package.json'])).forEach(f => addNpm(f, npm));
|
|
201
|
+
if (npm.size)
|
|
202
|
+
result.set('npm', npm);
|
|
203
|
+
const python = new Set();
|
|
204
|
+
const pyManifests = findManifests(cwd, new Set(['pyproject.toml', 'Pipfile']), /^requirements.*\.txt$/);
|
|
205
|
+
addPython(cwd, python, pyManifests);
|
|
206
|
+
if (python.size)
|
|
207
|
+
result.set('python', python);
|
|
208
|
+
const go = new Set();
|
|
209
|
+
findManifests(cwd, new Set(['go.mod'])).forEach(f => addGo(f, go));
|
|
210
|
+
if (go.size)
|
|
211
|
+
result.set('go', go);
|
|
212
|
+
const rust = new Set();
|
|
213
|
+
findManifests(cwd, new Set(['Cargo.toml'])).forEach(f => addRust(f, rust));
|
|
214
|
+
if (rust.size)
|
|
215
|
+
result.set('rust', rust);
|
|
216
|
+
const ruby = new Set();
|
|
217
|
+
findManifests(cwd, new Set(['Gemfile'])).forEach(f => addRuby(f, ruby));
|
|
218
|
+
if (ruby.size)
|
|
219
|
+
result.set('ruby', ruby);
|
|
220
|
+
const php = new Set();
|
|
221
|
+
findManifests(cwd, new Set(['composer.json'])).forEach(f => addPhp(f, php));
|
|
222
|
+
if (php.size)
|
|
223
|
+
result.set('php', php);
|
|
224
|
+
return result;
|
|
225
|
+
}
|
package/dist/utils/index.d.ts
CHANGED
package/dist/utils/index.js
CHANGED
|
@@ -24,6 +24,7 @@ __exportStar(require("./logger"), exports);
|
|
|
24
24
|
__exportStar(require("./result-collector"), exports);
|
|
25
25
|
__exportStar(require("./resolve-bin"), exports);
|
|
26
26
|
__exportStar(require("./own-deps"), exports);
|
|
27
|
+
__exportStar(require("./direct-deps"), exports);
|
|
27
28
|
__exportStar(require("./detect-language"), exports);
|
|
28
29
|
__exportStar(require("./glob"), exports);
|
|
29
30
|
__exportStar(require("./update-notifier"), exports);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pr-checkmate",
|
|
3
|
-
"version": "1.23.
|
|
3
|
+
"version": "1.23.4",
|
|
4
4
|
"description": "Automated PR quality checks: linting, formatting, dependency analysis, and spellcheck",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"github-actions",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
],
|
|
42
42
|
"scripts": {
|
|
43
43
|
"copy-configs": "ts-node src/scripts/copy-configs.ts",
|
|
44
|
-
"build": "npm run clean && npm run copy-configs && tsc && npm run fix-shebang",
|
|
44
|
+
"build": "npm run clean && npm run copy-configs && tsc -p tsconfig.build.json && npm run fix-shebang",
|
|
45
45
|
"fix-shebang": "node -e \"const fs=require('fs'); const path='dist/cli.js'; let content=fs.readFileSync(path,'utf8'); if(!content.startsWith('#!')){content='#!/usr/bin/env node\\n'+content; fs.writeFileSync(path,content);} fs.chmodSync(path,'755');\"",
|
|
46
46
|
"check": "npm run build && npm run lint && npm run prettier && npm run spellcheck",
|
|
47
47
|
"prepublishOnly": "npm run build",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"spellcheck": "cspell '**/*.{ts,tsx,js,jsx,md,json}' --no-progress",
|
|
51
51
|
"docs": "typedoc",
|
|
52
52
|
"scan:secrets": "gitleaks-secret-scanner --diff-mode ci --html-report gitleaks-report.html",
|
|
53
|
-
"test": "
|
|
53
|
+
"test": "node --require ts-node/register --test \"src/**/*.test.ts\"",
|
|
54
54
|
"security-scan": "npx pr-checkmate security",
|
|
55
55
|
"clean": "rm -rf dist",
|
|
56
56
|
"release": "standard-version"
|
package/pr-checkmate.schema.json
CHANGED
|
@@ -89,6 +89,17 @@
|
|
|
89
89
|
"type": "object",
|
|
90
90
|
"properties": { "enabled": { "type": "boolean" } }
|
|
91
91
|
},
|
|
92
|
+
"grypeScan": {
|
|
93
|
+
"type": "object",
|
|
94
|
+
"properties": {
|
|
95
|
+
"enabled": { "type": "boolean" },
|
|
96
|
+
"minSeverity": {
|
|
97
|
+
"type": "string",
|
|
98
|
+
"enum": ["negligible", "low", "medium", "high", "critical"]
|
|
99
|
+
},
|
|
100
|
+
"ignore": { "type": "array", "items": { "type": "string" } }
|
|
101
|
+
}
|
|
102
|
+
},
|
|
92
103
|
"mergeConflict": { "$ref": "#/$defs/toggleSeverity" },
|
|
93
104
|
"leftoverDebug": { "$ref": "#/$defs/toggleSeverity" },
|
|
94
105
|
"lockfileDrift": { "$ref": "#/$defs/toggleSeverity" },
|