pr-checkmate 3.3.0 → 4.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/LICENSE +8 -3
- package/README.md +53 -4
- package/dist/cjs/agent/hook.d.ts +37 -0
- package/dist/cjs/agent/hook.js +175 -0
- package/dist/cjs/agent/registration.d.ts +10 -0
- package/dist/cjs/agent/registration.js +28 -0
- package/dist/cjs/agent/setup.d.ts +20 -0
- package/dist/cjs/agent/setup.js +229 -0
- package/dist/cjs/cli.js +88 -0
- package/dist/cjs/config/constants.d.ts +2 -0
- package/dist/cjs/config/constants.js +5 -1
- package/dist/cjs/config/pr-checkmate.template.json +10 -0
- package/dist/cjs/core/checks/findings.js +33 -7
- package/dist/cjs/core/checks/git/large-files.d.ts +1 -0
- package/dist/cjs/core/checks/git/large-files.js +3 -3
- package/dist/cjs/core/checks/pr/pr-size-check.d.ts +2 -0
- package/dist/cjs/core/checks/pr/pr-size-check.js +5 -5
- package/dist/cjs/core/checks/security/agent-instructions.d.ts +4 -0
- package/dist/cjs/core/checks/security/agent-instructions.js +149 -0
- package/dist/cjs/core/checks/security/diff-security.d.ts +7 -0
- package/dist/cjs/core/checks/security/diff-security.js +21 -1
- package/dist/cjs/core/checks/security/security.d.ts +0 -13
- package/dist/cjs/core/checks/security/security.js +27 -0
- package/dist/cjs/core/checks/security/workflow-security.js +33 -0
- package/dist/cjs/core/commands.d.ts +6 -0
- package/dist/cjs/core/commands.js +11 -1
- package/dist/cjs/core/finding-store.d.ts +16 -0
- package/dist/cjs/core/finding-store.js +73 -0
- package/dist/cjs/core/registry.js +2 -0
- package/dist/cjs/core/reporters.js +9 -0
- package/dist/cjs/core/run-checks.js +15 -5
- package/dist/cjs/core/sarif.d.ts +13 -0
- package/dist/cjs/core/sarif.js +60 -0
- package/dist/cjs/core/types.d.ts +7 -2
- package/dist/cjs/index.d.ts +2 -1
- package/dist/cjs/index.js +4 -1
- package/dist/cjs/init.js +11 -0
- package/dist/cjs/interfaces/index.d.ts +12 -0
- package/dist/cjs/mcp/config-tools.d.ts +30 -0
- package/dist/cjs/mcp/config-tools.js +320 -0
- package/dist/cjs/mcp/dependency.d.ts +33 -0
- package/dist/cjs/mcp/dependency.js +407 -0
- package/dist/cjs/mcp/protocol.d.ts +97 -0
- package/dist/cjs/mcp/protocol.js +181 -0
- package/dist/cjs/mcp/review.d.ts +63 -0
- package/dist/cjs/mcp/review.js +150 -0
- package/dist/cjs/mcp/serve.d.ts +8 -0
- package/dist/cjs/mcp/serve.js +49 -0
- package/dist/cjs/mcp/settings.d.ts +13 -0
- package/dist/cjs/mcp/settings.js +50 -0
- package/dist/cjs/mcp/tools.d.ts +23 -0
- package/dist/cjs/mcp/tools.js +633 -0
- package/dist/cjs/utils/execa.d.ts +7 -0
- package/dist/cjs/utils/execa.js +41 -1
- package/dist/cjs/utils/result-collector.d.ts +17 -1
- package/dist/cjs/utils/result-collector.js +7 -2
- package/dist/esm/agent/hook.d.ts +37 -0
- package/dist/esm/agent/hook.js +167 -0
- package/dist/esm/agent/registration.d.ts +10 -0
- package/dist/esm/agent/registration.js +21 -0
- package/dist/esm/agent/setup.d.ts +20 -0
- package/dist/esm/agent/setup.js +220 -0
- package/dist/esm/cli.js +86 -1
- package/dist/esm/config/constants.d.ts +2 -0
- package/dist/esm/config/constants.js +4 -0
- package/dist/esm/config/pr-checkmate.template.json +10 -0
- package/dist/esm/core/checks/findings.js +33 -7
- package/dist/esm/core/checks/git/large-files.d.ts +1 -0
- package/dist/esm/core/checks/git/large-files.js +1 -1
- package/dist/esm/core/checks/pr/pr-size-check.d.ts +2 -0
- package/dist/esm/core/checks/pr/pr-size-check.js +2 -2
- package/dist/esm/core/checks/security/agent-instructions.d.ts +4 -0
- package/dist/esm/core/checks/security/agent-instructions.js +146 -0
- package/dist/esm/core/checks/security/diff-security.d.ts +7 -0
- package/dist/esm/core/checks/security/diff-security.js +20 -0
- package/dist/esm/core/checks/security/security.d.ts +0 -13
- package/dist/esm/core/checks/security/security.js +27 -0
- package/dist/esm/core/checks/security/workflow-security.js +33 -0
- package/dist/esm/core/commands.d.ts +6 -0
- package/dist/esm/core/commands.js +10 -0
- package/dist/esm/core/finding-store.d.ts +16 -0
- package/dist/esm/core/finding-store.js +67 -0
- package/dist/esm/core/registry.js +2 -0
- package/dist/esm/core/reporters.js +9 -0
- package/dist/esm/core/run-checks.js +15 -5
- package/dist/esm/core/sarif.d.ts +13 -0
- package/dist/esm/core/sarif.js +57 -0
- package/dist/esm/core/types.d.ts +7 -2
- package/dist/esm/index.d.ts +2 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/init.js +12 -1
- package/dist/esm/interfaces/index.d.ts +12 -0
- package/dist/esm/mcp/config-tools.d.ts +30 -0
- package/dist/esm/mcp/config-tools.js +310 -0
- package/dist/esm/mcp/dependency.d.ts +33 -0
- package/dist/esm/mcp/dependency.js +402 -0
- package/dist/esm/mcp/protocol.d.ts +97 -0
- package/dist/esm/mcp/protocol.js +175 -0
- package/dist/esm/mcp/review.d.ts +63 -0
- package/dist/esm/mcp/review.js +139 -0
- package/dist/esm/mcp/serve.d.ts +8 -0
- package/dist/esm/mcp/serve.js +43 -0
- package/dist/esm/mcp/settings.d.ts +13 -0
- package/dist/esm/mcp/settings.js +43 -0
- package/dist/esm/mcp/tools.d.ts +23 -0
- package/dist/esm/mcp/tools.js +626 -0
- package/dist/esm/utils/execa.d.ts +7 -0
- package/dist/esm/utils/execa.js +40 -1
- package/dist/esm/utils/result-collector.d.ts +17 -1
- package/dist/esm/utils/result-collector.js +7 -2
- package/package.json +6 -2
- package/pr-checkmate.schema.json +35 -0
- package/src/config/pr-checkmate.schema.json +35 -0
|
@@ -55,6 +55,39 @@ exports.WORKFLOW_PATTERNS = [
|
|
|
55
55
|
label: 'pull_request_target trigger (runs with repo secrets on untrusted PRs)',
|
|
56
56
|
severity: 'warn',
|
|
57
57
|
},
|
|
58
|
+
{
|
|
59
|
+
// The second half of the "pwn request": `pull_request_target` alone is harmless,
|
|
60
|
+
// checking out the PR's own code under it hands the attacker a privileged
|
|
61
|
+
// runner. One line cannot see the trigger, so this stays a warning.
|
|
62
|
+
pattern: /^\+(?!\+)\s*(?:ref|repository):\s*['"]?\$\{\{\s*github\.event\.pull_request\.head\.(?:sha|ref|repo\.full_name)\s*\}\}/,
|
|
63
|
+
label: 'checkout of the PR head (code execution with secrets under pull_request_target)',
|
|
64
|
+
severity: 'warn',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
pattern: /^\+(?!\+)\s*permissions:\s*write-all\b/,
|
|
68
|
+
label: 'permissions: write-all (grant the token only the scopes the job needs)',
|
|
69
|
+
severity: 'error',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
pattern: /^\+(?!\+)\s*secrets:\s*inherit\b/,
|
|
73
|
+
label: 'secrets: inherit (hands every repository secret to the called workflow)',
|
|
74
|
+
severity: 'warn',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
pattern: /^\+(?!\+)\s*ACTIONS_ALLOW_UNSECURE_COMMANDS:\s*['"]?true\b/i,
|
|
78
|
+
label: 'ACTIONS_ALLOW_UNSECURE_COMMANDS (re-enables the deprecated set-env / add-path injection vector)',
|
|
79
|
+
severity: 'error',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
pattern: /^\+(?!\+).*\b(?:curl|wget)\b[^|]*\|\s*(?:sudo\s+)?(?:sh|bash)\b/i,
|
|
83
|
+
label: 'piping a download straight to a shell (curl/wget … | sh)',
|
|
84
|
+
severity: 'warn',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
pattern: /^\+(?!\+)\s*runs-on:\s*\[?\s*['"]?self-hosted\b/,
|
|
88
|
+
label: 'self-hosted runner (a PR from a fork can run code on your own infrastructure)',
|
|
89
|
+
severity: 'warn',
|
|
90
|
+
},
|
|
58
91
|
];
|
|
59
92
|
exports.workflowSecurityCheck = {
|
|
60
93
|
name: 'Workflow Security',
|
|
@@ -22,6 +22,12 @@ export declare const COMMAND_TO_CHECK: Record<string, string>;
|
|
|
22
22
|
* and not a reading of our source.
|
|
23
23
|
*/
|
|
24
24
|
export declare const TOOLING_COMMANDS: readonly string[];
|
|
25
|
+
/**
|
|
26
|
+
* Commands that serve AI agents rather than review a diff. They share the CLI's
|
|
27
|
+
* entry point and nothing else: no scope flags apply, and `mcp` in particular owns
|
|
28
|
+
* stdout for the protocol, so none of them may be confused with a check name.
|
|
29
|
+
*/
|
|
30
|
+
export declare const AGENT_COMMANDS: readonly string[];
|
|
25
31
|
export declare const STANDALONE_COMMANDS: readonly string[];
|
|
26
32
|
/** Flags accepted anywhere on the command line, with what each one means. */
|
|
27
33
|
export declare const FLAGS: Record<string, string>;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* reports success having verified nothing at all.
|
|
13
13
|
*/
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.FLAGS = exports.STANDALONE_COMMANDS = exports.TOOLING_COMMANDS = exports.COMMAND_TO_CHECK = void 0;
|
|
15
|
+
exports.FLAGS = exports.STANDALONE_COMMANDS = exports.AGENT_COMMANDS = exports.TOOLING_COMMANDS = exports.COMMAND_TO_CHECK = void 0;
|
|
16
16
|
/** Maps short CLI command names to their registry check names. */
|
|
17
17
|
exports.COMMAND_TO_CHECK = {
|
|
18
18
|
// Every check that can fail a run is reachable on its own, so "why does this
|
|
@@ -44,6 +44,7 @@ exports.COMMAND_TO_CHECK = {
|
|
|
44
44
|
'license-header': 'License Header',
|
|
45
45
|
'config-validation': 'Config Validation',
|
|
46
46
|
'dockerfile-security': 'Dockerfile Security',
|
|
47
|
+
'agent-instructions': 'Agent Instructions',
|
|
47
48
|
'case-collision': 'Case Collision',
|
|
48
49
|
'vuln-scan': 'Vuln Scan',
|
|
49
50
|
'leftover-debug': 'Leftover Debug',
|
|
@@ -79,14 +80,23 @@ exports.COMMAND_TO_CHECK = {
|
|
|
79
80
|
* and not a reading of our source.
|
|
80
81
|
*/
|
|
81
82
|
exports.TOOLING_COMMANDS = ['install', 'doctor', 'cache-prune'];
|
|
83
|
+
/**
|
|
84
|
+
* Commands that serve AI agents rather than review a diff. They share the CLI's
|
|
85
|
+
* entry point and nothing else: no scope flags apply, and `mcp` in particular owns
|
|
86
|
+
* stdout for the protocol, so none of them may be confused with a check name.
|
|
87
|
+
*/
|
|
88
|
+
exports.AGENT_COMMANDS = ['mcp', 'hook', 'agent-setup'];
|
|
82
89
|
exports.STANDALONE_COMMANDS = [
|
|
83
90
|
'init',
|
|
84
91
|
'all',
|
|
85
92
|
'precommit',
|
|
86
93
|
...exports.TOOLING_COMMANDS,
|
|
94
|
+
...exports.AGENT_COMMANDS,
|
|
87
95
|
];
|
|
88
96
|
/** Flags accepted anywhere on the command line, with what each one means. */
|
|
89
97
|
exports.FLAGS = {
|
|
90
98
|
'--full': 'review every tracked file instead of the diff (the default outside CI)',
|
|
91
99
|
'--all': 'with "install": fetch every profile, not only the ones this repository needs',
|
|
100
|
+
'--sarif': 'write findings as SARIF 2.1.0 to pr-checkmate.sarif (check-running commands only)',
|
|
101
|
+
'--no-hook': 'with "agent-setup": skip the git-commit gate for Claude Code',
|
|
92
102
|
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Finding } from '../utils/result-collector.js';
|
|
2
|
+
/** Run `work` collecting every finding filed inside it. */
|
|
3
|
+
export declare function collectFindings<T>(work: () => Promise<T>): Promise<{
|
|
4
|
+
result: T;
|
|
5
|
+
findings: Finding[];
|
|
6
|
+
}>;
|
|
7
|
+
/** A scanner saw `sample` under `label` at `file:line`. Claimed later by {@link fileFindings}. */
|
|
8
|
+
export declare function noteLocation(label: string, sample: string, file: string, line: number): void;
|
|
9
|
+
/**
|
|
10
|
+
* Turn the findings that survived filtering into located ones. Each sample claims
|
|
11
|
+
* the oldest location noted for it, so the same line appearing twice resolves to
|
|
12
|
+
* two places rather than one place twice.
|
|
13
|
+
*/
|
|
14
|
+
export declare function fileFindings(bucket: Record<string, string[]>, severity: Finding['severity']): void;
|
|
15
|
+
/** File a finding directly, for scanners that already know exactly where it is. */
|
|
16
|
+
export declare function addFinding(finding: Finding): void;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.collectFindings = collectFindings;
|
|
4
|
+
exports.noteLocation = noteLocation;
|
|
5
|
+
exports.fileFindings = fileFindings;
|
|
6
|
+
exports.addFinding = addFinding;
|
|
7
|
+
/**
|
|
8
|
+
* Where a finding was, carried from the scanner that saw it to the report that
|
|
9
|
+
* names it.
|
|
10
|
+
*
|
|
11
|
+
* Scanners reduce what they find to strings in a bucket — `"debugger statement" →
|
|
12
|
+
* ["debugger;"]` — because the report only ever needed a count and a sample. An
|
|
13
|
+
* agent needs more: which file, which line. Rewriting forty checks to carry that
|
|
14
|
+
* would have been forty chances to disagree with each other, so the location is
|
|
15
|
+
* noted once, where the diff is walked, and attached once, where the buckets are
|
|
16
|
+
* filed.
|
|
17
|
+
*
|
|
18
|
+
* It is attached at filing time, not scan time, on purpose. `ignore` and
|
|
19
|
+
* `severityOverrides` run in between, and a finding the client has silenced must
|
|
20
|
+
* not reach an agent that would then dutifully "fix" it.
|
|
21
|
+
*
|
|
22
|
+
* The store is an `AsyncLocalStorage` scope per check. Checks of one phase run in
|
|
23
|
+
* parallel, so a module-level list would mix their findings; a scope cannot. Code
|
|
24
|
+
* running outside `collectFindings` (the library API, unit tests) finds no store
|
|
25
|
+
* and every call here does nothing.
|
|
26
|
+
*/
|
|
27
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
28
|
+
/** More than this and the list is noise to a reader and a cost to an agent. */
|
|
29
|
+
const MAX_FINDINGS_PER_CHECK = 200;
|
|
30
|
+
const scope = new node_async_hooks_1.AsyncLocalStorage();
|
|
31
|
+
const keyOf = (label, sample) => `${label}\u0000${sample}`;
|
|
32
|
+
/** Run `work` collecting every finding filed inside it. */
|
|
33
|
+
async function collectFindings(work) {
|
|
34
|
+
const store = { found: [], pending: new Map() };
|
|
35
|
+
const result = await scope.run(store, work);
|
|
36
|
+
return { result, findings: store.found };
|
|
37
|
+
}
|
|
38
|
+
/** A scanner saw `sample` under `label` at `file:line`. Claimed later by {@link fileFindings}. */
|
|
39
|
+
function noteLocation(label, sample, file, line) {
|
|
40
|
+
const store = scope.getStore();
|
|
41
|
+
if (!store)
|
|
42
|
+
return;
|
|
43
|
+
const key = keyOf(label, sample);
|
|
44
|
+
const queue = store.pending.get(key) ?? [];
|
|
45
|
+
queue.push({ file, line });
|
|
46
|
+
store.pending.set(key, queue);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Turn the findings that survived filtering into located ones. Each sample claims
|
|
50
|
+
* the oldest location noted for it, so the same line appearing twice resolves to
|
|
51
|
+
* two places rather than one place twice.
|
|
52
|
+
*/
|
|
53
|
+
function fileFindings(bucket, severity) {
|
|
54
|
+
const store = scope.getStore();
|
|
55
|
+
if (!store)
|
|
56
|
+
return;
|
|
57
|
+
for (const [label, samples] of Object.entries(bucket)) {
|
|
58
|
+
for (const sample of samples) {
|
|
59
|
+
if (store.found.length >= MAX_FINDINGS_PER_CHECK)
|
|
60
|
+
return;
|
|
61
|
+
const where = store.pending.get(keyOf(label, sample))?.shift();
|
|
62
|
+
if (!where)
|
|
63
|
+
continue;
|
|
64
|
+
store.found.push({ ...where, rule: label, snippet: sample, severity });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** File a finding directly, for scanners that already know exactly where it is. */
|
|
69
|
+
function addFinding(finding) {
|
|
70
|
+
const store = scope.getStore();
|
|
71
|
+
if (store && store.found.length < MAX_FINDINGS_PER_CHECK)
|
|
72
|
+
store.found.push(finding);
|
|
73
|
+
}
|
|
@@ -41,6 +41,7 @@ const package_size_js_1 = require("./checks/quality/package-size.js");
|
|
|
41
41
|
const spellcheck_js_1 = require("./checks/quality/spellcheck.js");
|
|
42
42
|
const symlinks_js_1 = require("./checks/quality/symlinks.js");
|
|
43
43
|
const yaml_lint_js_1 = require("./checks/quality/yaml-lint.js");
|
|
44
|
+
const agent_instructions_js_1 = require("./checks/security/agent-instructions.js");
|
|
44
45
|
const diff_security_js_1 = require("./checks/security/diff-security.js");
|
|
45
46
|
const dockerfile_security_js_1 = require("./checks/security/dockerfile-security.js");
|
|
46
47
|
const migration_safety_js_1 = require("./checks/security/migration-safety.js");
|
|
@@ -66,6 +67,7 @@ exports.REGISTRY = [
|
|
|
66
67
|
diff_security_js_1.diffSecurityCheck,
|
|
67
68
|
workflow_security_js_1.workflowSecurityCheck,
|
|
68
69
|
dockerfile_security_js_1.dockerfileSecurityCheck,
|
|
70
|
+
agent_instructions_js_1.agentInstructionsCheck,
|
|
69
71
|
case_collision_js_1.caseCollisionCheck,
|
|
70
72
|
vuln_scan_js_1.vulnScanCheck,
|
|
71
73
|
grype_scan_js_1.grypeScanCheck,
|
|
@@ -6,6 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.buildMarkdownReport = buildMarkdownReport;
|
|
7
7
|
exports.report = report;
|
|
8
8
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const registration_js_1 = require("../agent/registration.js");
|
|
10
|
+
const constants_js_1 = require("../config/constants.js");
|
|
11
|
+
const settings_js_1 = require("../mcp/settings.js");
|
|
9
12
|
const http_js_1 = require("../utils/http.js");
|
|
10
13
|
const index_js_1 = require("../utils/index.js");
|
|
11
14
|
const package_version_js_1 = require("../utils/package-version.js");
|
|
@@ -107,6 +110,12 @@ function printStdout(report, cfg) {
|
|
|
107
110
|
if (cfg.include?.docsLink !== false) {
|
|
108
111
|
// The docs cover every check and config key.
|
|
109
112
|
index_js_1.logger.print(' DOCUMENTATION: https://pr-checkmate-docs.pages.dev/');
|
|
113
|
+
// Said once per project, and never in CI, where nobody is there to act on it.
|
|
114
|
+
if (!process.env.CI &&
|
|
115
|
+
(0, settings_js_1.readMcpSettings)(process.cwd()).enabled &&
|
|
116
|
+
!(0, registration_js_1.isMcpRegistered)(process.cwd())) {
|
|
117
|
+
index_js_1.logger.print(` AI AGENT: ${constants_js_1.AGENT_SETUP_COMMAND} — your agent then writes code that passes.`);
|
|
118
|
+
}
|
|
110
119
|
}
|
|
111
120
|
}
|
|
112
121
|
/** Network settings shared by every platform call. */
|
|
@@ -9,6 +9,7 @@ const pool_js_1 = require("../utils/pool.js");
|
|
|
9
9
|
const resolve_bin_js_1 = require("../utils/resolve-bin.js");
|
|
10
10
|
const result_collector_js_1 = require("../utils/result-collector.js");
|
|
11
11
|
const context_js_1 = require("./context.js");
|
|
12
|
+
const finding_store_js_1 = require("./finding-store.js");
|
|
12
13
|
const prepare_js_1 = require("./install/prepare.js");
|
|
13
14
|
const registry_js_1 = require("./registry.js");
|
|
14
15
|
const reporters_js_1 = require("./reporters.js");
|
|
@@ -174,15 +175,24 @@ async function runOne(check, ctx, collector, heavy, tooling) {
|
|
|
174
175
|
}
|
|
175
176
|
const started = Date.now();
|
|
176
177
|
const timeoutMs = ctx.config.run?.checkTimeoutMs ?? 300000;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
178
|
+
// Locations noted while this check runs, collected per check: the phase runs
|
|
179
|
+
// its checks in parallel, so a shared list would mix their findings.
|
|
180
|
+
let located = [];
|
|
181
|
+
const execute = async () => {
|
|
182
|
+
const { result, findings } = await (0, finding_store_js_1.collectFindings)(() => (0, pool_js_1.withTimeout)(check.run(ctx), timeoutMs, () => {
|
|
183
|
+
index_js_1.logger.warn(`[runChecks]: ⏱️ ${check.name} exceeded ${timeoutMs}ms`);
|
|
184
|
+
return { status: 'warn', details: `timed out after ${timeoutMs}ms` };
|
|
185
|
+
}));
|
|
186
|
+
located = findings;
|
|
187
|
+
return result;
|
|
188
|
+
};
|
|
181
189
|
const outcome = check.weight === 'heavy' ? await heavy.run(execute) : await execute();
|
|
182
190
|
if (ctx.config.run?.timings) {
|
|
183
191
|
index_js_1.logger.info(`[runChecks]: ${check.name} took ${Date.now() - started}ms`);
|
|
184
192
|
}
|
|
185
|
-
collector.register(check.name, applySeverity(check.name, outcome.status, ctx), outcome.details
|
|
193
|
+
collector.register(check.name, applySeverity(check.name, outcome.status, ctx), outcome.details,
|
|
194
|
+
// A clean result has nothing to locate, even when `failOn: never` filed some.
|
|
195
|
+
outcome.status === 'pass' ? undefined : (outcome.findings ?? located));
|
|
186
196
|
}
|
|
187
197
|
catch (err) {
|
|
188
198
|
// An unexpected throw in a blocking check fails the run; in informational/format
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A report as SARIF 2.1.0 — the format GitHub code scanning, VS Code and most
|
|
3
|
+
* security dashboards read, so a finding shows up inline on the line it names
|
|
4
|
+
* instead of only in a summary table.
|
|
5
|
+
*
|
|
6
|
+
* Only what a consumer can use is written. A failed or warned check that located
|
|
7
|
+
* its problems yields one result per location; one that could not yields a single
|
|
8
|
+
* result with no location, because a finding with nowhere to point is still a
|
|
9
|
+
* finding. Passes and skips are not results.
|
|
10
|
+
*/
|
|
11
|
+
import type { CheckReport } from './types.js';
|
|
12
|
+
/** Convert a report to a SARIF log object, ready for `JSON.stringify`. */
|
|
13
|
+
export declare function toSarif(report: CheckReport, toolVersion: string): Record<string, unknown>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toSarif = toSarif;
|
|
4
|
+
const SARIF_SCHEMA = 'https://json.schemastore.org/sarif-2.1.0.json';
|
|
5
|
+
const ruleIdFor = (check, rule) => (rule ? `${check}/${rule}` : check);
|
|
6
|
+
function locatedResult(check, f) {
|
|
7
|
+
return {
|
|
8
|
+
ruleId: ruleIdFor(check.name, f.rule),
|
|
9
|
+
level: f.severity === 'error' ? 'error' : 'warning',
|
|
10
|
+
message: { text: f.snippet ? `${f.rule}: ${f.snippet}` : f.rule },
|
|
11
|
+
locations: [
|
|
12
|
+
{
|
|
13
|
+
physicalLocation: {
|
|
14
|
+
artifactLocation: { uri: f.file },
|
|
15
|
+
...(f.line !== undefined && { region: { startLine: f.line } }),
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function summaryResult(check) {
|
|
22
|
+
return {
|
|
23
|
+
ruleId: ruleIdFor(check.name),
|
|
24
|
+
level: check.status === 'fail' ? 'error' : 'warning',
|
|
25
|
+
message: { text: check.details ?? `${check.name} reported a problem` },
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** Convert a report to a SARIF log object, ready for `JSON.stringify`. */
|
|
29
|
+
function toSarif(report, toolVersion) {
|
|
30
|
+
const results = [];
|
|
31
|
+
const rules = new Map();
|
|
32
|
+
for (const check of report.results) {
|
|
33
|
+
if (check.status !== 'fail' && check.status !== 'warn')
|
|
34
|
+
continue;
|
|
35
|
+
const found = check.findings && check.findings.length > 0
|
|
36
|
+
? check.findings.map(f => locatedResult(check, f))
|
|
37
|
+
: [summaryResult(check)];
|
|
38
|
+
for (const r of found) {
|
|
39
|
+
results.push(r);
|
|
40
|
+
rules.set(r.ruleId, { id: r.ruleId, name: r.ruleId });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
$schema: SARIF_SCHEMA,
|
|
45
|
+
version: '2.1.0',
|
|
46
|
+
runs: [
|
|
47
|
+
{
|
|
48
|
+
tool: {
|
|
49
|
+
driver: {
|
|
50
|
+
name: 'pr-checkmate',
|
|
51
|
+
version: toolVersion,
|
|
52
|
+
informationUri: 'https://www.npmjs.com/package/pr-checkmate',
|
|
53
|
+
rules: [...rules.values()],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
results,
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
}
|
package/dist/cjs/core/types.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { PRCheckMateConfig, ReporterConfig } from '../interfaces/index.js';
|
|
2
2
|
import type { Language, LanguageUsage } from '../utils/detect-language.js';
|
|
3
|
-
import type { CheckResult, CheckStatus } from '../utils/result-collector.js';
|
|
4
|
-
export type { CheckResult, CheckStatus, Language, LanguageUsage };
|
|
3
|
+
import type { CheckResult, CheckStatus, Finding } from '../utils/result-collector.js';
|
|
4
|
+
export type { CheckResult, CheckStatus, Finding, Language, LanguageUsage };
|
|
5
5
|
/**
|
|
6
6
|
* Execution phases, run strictly in order. Within a phase checks run in parallel.
|
|
7
7
|
* `format` runs last because formatters (prettier) may write files.
|
|
@@ -67,6 +67,11 @@ export interface CheckContext {
|
|
|
67
67
|
export interface CheckOutcome {
|
|
68
68
|
status: CheckStatus;
|
|
69
69
|
details?: string;
|
|
70
|
+
/**
|
|
71
|
+
* Where the problems are. Left to the orchestrator for pattern-based checks,
|
|
72
|
+
* which note locations as they scan; a check that knows better may set it.
|
|
73
|
+
*/
|
|
74
|
+
findings?: Finding[];
|
|
70
75
|
}
|
|
71
76
|
/**
|
|
72
77
|
* A single quality gate. `applies` decides relevance (language/marker gating);
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* @module pr-checkmate
|
|
5
5
|
*/
|
|
6
6
|
export { runChecks, buildContext, REGISTRY } from './core/index.js';
|
|
7
|
+
export { toSarif } from './core/sarif.js';
|
|
7
8
|
export { pass, warn, fail, skip, defineCheck } from './core/index.js';
|
|
8
|
-
export type { Check, CheckContext, CheckOutcome, CheckReport, CheckResult, CheckStatus, Language, Phase, ReporterFn, ReporterKind, RunOptions, } from './core/index.js';
|
|
9
|
+
export type { Check, CheckContext, CheckOutcome, CheckReport, CheckResult, CheckStatus, Finding, Language, Phase, ReporterFn, ReporterKind, RunOptions, } from './core/index.js';
|
|
9
10
|
export { getSourcePath, getSourcePaths, getIgnoredDirs, getPackageConfigPath, hasLocalConfig, } from './config/index.js';
|
|
10
11
|
export type { PRCheckMateConfig } from './interfaces/index.js';
|
|
11
12
|
export { Logger } from './utils/logger.js';
|
package/dist/cjs/index.js
CHANGED
|
@@ -5,13 +5,16 @@
|
|
|
5
5
|
* @module pr-checkmate
|
|
6
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.Logger = exports.hasLocalConfig = exports.getPackageConfigPath = exports.getIgnoredDirs = exports.getSourcePaths = exports.getSourcePath = exports.defineCheck = exports.skip = exports.fail = exports.warn = exports.pass = exports.REGISTRY = exports.buildContext = exports.runChecks = void 0;
|
|
8
|
+
exports.Logger = exports.hasLocalConfig = exports.getPackageConfigPath = exports.getIgnoredDirs = exports.getSourcePaths = exports.getSourcePath = exports.defineCheck = exports.skip = exports.fail = exports.warn = exports.pass = exports.toSarif = exports.REGISTRY = exports.buildContext = exports.runChecks = void 0;
|
|
9
9
|
// Primary programmatic API: run checks and receive a structured report.
|
|
10
10
|
// Never throws for violations, never calls process.exit — the caller decides.
|
|
11
11
|
var index_js_1 = require("./core/index.js");
|
|
12
12
|
Object.defineProperty(exports, "runChecks", { enumerable: true, get: function () { return index_js_1.runChecks; } });
|
|
13
13
|
Object.defineProperty(exports, "buildContext", { enumerable: true, get: function () { return index_js_1.buildContext; } });
|
|
14
14
|
Object.defineProperty(exports, "REGISTRY", { enumerable: true, get: function () { return index_js_1.REGISTRY; } });
|
|
15
|
+
// SARIF 2.1.0 export of a report, for code-scanning dashboards and editors.
|
|
16
|
+
var sarif_js_1 = require("./core/sarif.js");
|
|
17
|
+
Object.defineProperty(exports, "toSarif", { enumerable: true, get: function () { return sarif_js_1.toSarif; } });
|
|
15
18
|
// Authoring helpers for consumers writing their own checks (plugins).
|
|
16
19
|
var index_js_2 = require("./core/index.js");
|
|
17
20
|
Object.defineProperty(exports, "pass", { enumerable: true, get: function () { return index_js_2.pass; } });
|
package/dist/cjs/init.js
CHANGED
|
@@ -419,6 +419,16 @@ async function createConfig(root) {
|
|
|
419
419
|
* update notice insisted `init` must not be skipped after an upgrade. Defaults
|
|
420
420
|
* now apply when the config is loaded, so there is nothing to top up.
|
|
421
421
|
*/
|
|
422
|
+
/** Adds the agent-setup command to a client's `commands` block when it is missing; nothing else in it is touched. */
|
|
423
|
+
function withAgentCommand(commands) {
|
|
424
|
+
if (commands === undefined)
|
|
425
|
+
return undefined;
|
|
426
|
+
if (typeof commands !== 'object' || commands === null || Array.isArray(commands))
|
|
427
|
+
return commands;
|
|
428
|
+
if (index_js_1.AGENT_SETUP_COMMAND in commands)
|
|
429
|
+
return commands;
|
|
430
|
+
return { ...commands, [index_js_1.AGENT_SETUP_COMMAND]: index_js_1.DEFAULT_COMMANDS[index_js_1.AGENT_SETUP_COMMAND] };
|
|
431
|
+
}
|
|
422
432
|
async function updateConfig(root) {
|
|
423
433
|
index_js_2.logger.info('[init]: pr-checkmate.json already exists — updating it.');
|
|
424
434
|
try {
|
|
@@ -449,6 +459,7 @@ async function updateConfig(root) {
|
|
|
449
459
|
// when missing; a client's own block is never touched.
|
|
450
460
|
grypeScan: json.grypeScan ?? structuredClone(GRYPE_SCAN_SEED),
|
|
451
461
|
...templateExtras({ ...json, grypeScan: true }),
|
|
462
|
+
commands: withAgentCommand(json.commands),
|
|
452
463
|
languages,
|
|
453
464
|
};
|
|
454
465
|
fs_1.default.writeFileSync(CONFIG_FILE, `${JSON.stringify(updated, null, 2)}\n`);
|
|
@@ -317,6 +317,18 @@ export interface PRCheckMateConfig {
|
|
|
317
317
|
severityOverrides?: Record<string, 'error' | 'warn' | 'off'>;
|
|
318
318
|
ignorePaths?: string[];
|
|
319
319
|
};
|
|
320
|
+
mcp?: {
|
|
321
|
+
enabled?: boolean;
|
|
322
|
+
disabledTools?: string[];
|
|
323
|
+
readOnly?: boolean;
|
|
324
|
+
};
|
|
325
|
+
agentInstructions?: {
|
|
326
|
+
enabled?: boolean;
|
|
327
|
+
ignore?: string[];
|
|
328
|
+
failOn?: 'error' | 'warn' | 'never';
|
|
329
|
+
severityOverrides?: Record<string, 'error' | 'warn' | 'off'>;
|
|
330
|
+
ignorePaths?: string[];
|
|
331
|
+
};
|
|
320
332
|
dockerfileSecurity?: {
|
|
321
333
|
enabled?: boolean;
|
|
322
334
|
ignore?: string[];
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ToolResult } from './protocol.js';
|
|
2
|
+
type Json = Record<string, unknown>;
|
|
3
|
+
/** What a key means, from the schema that ships with the package. */
|
|
4
|
+
export declare function explainConfig(key?: string): ToolResult;
|
|
5
|
+
/** Problems in a proposed config, phrased so the agent can correct them. Empty means it is acceptable. */
|
|
6
|
+
export declare function validateConfig(config: Json, cwd: string): string[];
|
|
7
|
+
/** Validate and write `config` into the repository's `pr-checkmate.json`. */
|
|
8
|
+
export declare function writeConfig(cwd: string, config: unknown, overwrite: boolean): ToolResult;
|
|
9
|
+
interface Suggestion {
|
|
10
|
+
proposed: Json;
|
|
11
|
+
notes: string[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Look at the repository and propose a config.
|
|
15
|
+
*
|
|
16
|
+
* The baseline is a whole-repository run of the fast checks. A check that already
|
|
17
|
+
* fails is proposed as `warn`: the gate is on from the first commit, the existing
|
|
18
|
+
* debt is visible, and nothing new can slip in unnoticed — then each check is
|
|
19
|
+
* promoted back to blocking as it reaches zero. Proposing `error` everywhere would
|
|
20
|
+
* make the first CI run red for reasons nobody on the pull request caused, and the
|
|
21
|
+
* usual response to that is to switch the whole tool off.
|
|
22
|
+
*/
|
|
23
|
+
export declare function suggestConfig(cwd: string): Promise<Suggestion & {
|
|
24
|
+
languages: string[];
|
|
25
|
+
}>;
|
|
26
|
+
/** `suggest_config` as the text an agent reads. */
|
|
27
|
+
export declare function formatSuggestion(s: Suggestion & {
|
|
28
|
+
languages: string[];
|
|
29
|
+
}): ToolResult;
|
|
30
|
+
export {};
|