pr-checkmate 3.2.3 → 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.
Files changed (174) hide show
  1. package/.eslintrc.js +3 -9
  2. package/LICENSE +8 -3
  3. package/README.md +54 -5
  4. package/dist/cjs/agent/hook.d.ts +37 -0
  5. package/dist/cjs/agent/hook.js +175 -0
  6. package/dist/cjs/agent/registration.d.ts +10 -0
  7. package/dist/cjs/agent/registration.js +28 -0
  8. package/dist/cjs/agent/setup.d.ts +20 -0
  9. package/dist/cjs/agent/setup.js +229 -0
  10. package/dist/cjs/cli.js +101 -0
  11. package/dist/cjs/config/constants.d.ts +2 -0
  12. package/dist/cjs/config/constants.js +5 -1
  13. package/dist/cjs/config/pr-checkmate.template.json +11 -1
  14. package/dist/cjs/core/checks/dependencies/grype-scan.js +18 -0
  15. package/dist/cjs/core/checks/findings.js +33 -7
  16. package/dist/cjs/core/checks/git/large-files.d.ts +1 -0
  17. package/dist/cjs/core/checks/git/large-files.js +3 -3
  18. package/dist/cjs/core/checks/languages/lint.js +12 -0
  19. package/dist/cjs/core/checks/languages/ruff-shared.js +6 -1
  20. package/dist/cjs/core/checks/pr/commitlint.js +4 -2
  21. package/dist/cjs/core/checks/pr/pr-size-check.d.ts +2 -0
  22. package/dist/cjs/core/checks/pr/pr-size-check.js +5 -5
  23. package/dist/cjs/core/checks/quality/package-size.d.ts +23 -1
  24. package/dist/cjs/core/checks/quality/package-size.js +20 -4
  25. package/dist/cjs/core/checks/quality/spellcheck.js +8 -5
  26. package/dist/cjs/core/checks/security/agent-instructions.d.ts +4 -0
  27. package/dist/cjs/core/checks/security/agent-instructions.js +149 -0
  28. package/dist/cjs/core/checks/security/diff-security.d.ts +7 -0
  29. package/dist/cjs/core/checks/security/diff-security.js +21 -1
  30. package/dist/cjs/core/checks/security/security.d.ts +0 -13
  31. package/dist/cjs/core/checks/security/security.js +27 -0
  32. package/dist/cjs/core/checks/security/workflow-security.js +33 -0
  33. package/dist/cjs/core/commands.d.ts +6 -0
  34. package/dist/cjs/core/commands.js +11 -1
  35. package/dist/cjs/core/finding-store.d.ts +16 -0
  36. package/dist/cjs/core/finding-store.js +73 -0
  37. package/dist/cjs/core/install/doctor.js +12 -0
  38. package/dist/cjs/core/install/grype-db.d.ts +25 -0
  39. package/dist/cjs/core/install/grype-db.js +45 -0
  40. package/dist/cjs/core/install/profiles.d.ts +1 -1
  41. package/dist/cjs/core/install/profiles.js +1 -1
  42. package/dist/cjs/core/install/reuse.js +6 -3
  43. package/dist/cjs/core/registry.js +2 -0
  44. package/dist/cjs/core/reporters.js +9 -0
  45. package/dist/cjs/core/run-checks.js +15 -5
  46. package/dist/cjs/core/sarif.d.ts +13 -0
  47. package/dist/cjs/core/sarif.js +60 -0
  48. package/dist/cjs/core/types.d.ts +7 -2
  49. package/dist/cjs/index.d.ts +2 -1
  50. package/dist/cjs/index.js +4 -1
  51. package/dist/cjs/init.d.ts +0 -8
  52. package/dist/cjs/init.js +12 -2
  53. package/dist/cjs/interfaces/index.d.ts +12 -0
  54. package/dist/cjs/mcp/config-tools.d.ts +30 -0
  55. package/dist/cjs/mcp/config-tools.js +320 -0
  56. package/dist/cjs/mcp/dependency.d.ts +33 -0
  57. package/dist/cjs/mcp/dependency.js +407 -0
  58. package/dist/cjs/mcp/protocol.d.ts +97 -0
  59. package/dist/cjs/mcp/protocol.js +181 -0
  60. package/dist/cjs/mcp/review.d.ts +63 -0
  61. package/dist/cjs/mcp/review.js +150 -0
  62. package/dist/cjs/mcp/serve.d.ts +8 -0
  63. package/dist/cjs/mcp/serve.js +49 -0
  64. package/dist/cjs/mcp/settings.d.ts +13 -0
  65. package/dist/cjs/mcp/settings.js +50 -0
  66. package/dist/cjs/mcp/tools.d.ts +23 -0
  67. package/dist/cjs/mcp/tools.js +633 -0
  68. package/dist/cjs/utils/execa.d.ts +7 -0
  69. package/dist/cjs/utils/execa.js +41 -1
  70. package/dist/cjs/utils/own-deps.js +8 -1
  71. package/dist/cjs/utils/resolve-bin.d.ts +2 -0
  72. package/dist/cjs/utils/resolve-bin.js +5 -0
  73. package/dist/cjs/utils/result-collector.d.ts +17 -1
  74. package/dist/cjs/utils/result-collector.js +7 -2
  75. package/dist/esm/agent/hook.d.ts +37 -0
  76. package/dist/esm/agent/hook.js +167 -0
  77. package/dist/esm/agent/registration.d.ts +10 -0
  78. package/dist/esm/agent/registration.js +21 -0
  79. package/dist/esm/agent/setup.d.ts +20 -0
  80. package/dist/esm/agent/setup.js +220 -0
  81. package/dist/esm/cli.js +100 -2
  82. package/dist/esm/config/constants.d.ts +2 -0
  83. package/dist/esm/config/constants.js +4 -0
  84. package/dist/esm/config/pr-checkmate.template.json +11 -1
  85. package/dist/esm/core/checks/dependencies/dependency-check.js +2 -2
  86. package/dist/esm/core/checks/dependencies/grype-scan.js +18 -0
  87. package/dist/esm/core/checks/dependencies/license-check.js +2 -2
  88. package/dist/esm/core/checks/dependencies/npm-audit.js +2 -2
  89. package/dist/esm/core/checks/findings.js +33 -7
  90. package/dist/esm/core/checks/git/diff-smell.js +1 -1
  91. package/dist/esm/core/checks/git/large-files.d.ts +1 -0
  92. package/dist/esm/core/checks/git/large-files.js +1 -1
  93. package/dist/esm/core/checks/languages/cpp-format.js +1 -1
  94. package/dist/esm/core/checks/languages/lint.js +12 -3
  95. package/dist/esm/core/checks/languages/prettier.js +3 -3
  96. package/dist/esm/core/checks/languages/python-format.js +2 -2
  97. package/dist/esm/core/checks/languages/python-lint.js +2 -2
  98. package/dist/esm/core/checks/languages/ruff-shared.js +6 -1
  99. package/dist/esm/core/checks/languages/typecheck.js +1 -1
  100. package/dist/esm/core/checks/pr/commitlint.js +4 -2
  101. package/dist/esm/core/checks/pr/pr-body.js +1 -1
  102. package/dist/esm/core/checks/pr/pr-size-check.d.ts +2 -0
  103. package/dist/esm/core/checks/pr/pr-size-check.js +3 -3
  104. package/dist/esm/core/checks/pr/pr-title-lint.js +1 -1
  105. package/dist/esm/core/checks/quality/dead-code.js +1 -1
  106. package/dist/esm/core/checks/quality/duplicate-check.js +1 -1
  107. package/dist/esm/core/checks/quality/markdown-lint.js +1 -1
  108. package/dist/esm/core/checks/quality/package-size.d.ts +23 -1
  109. package/dist/esm/core/checks/quality/package-size.js +19 -4
  110. package/dist/esm/core/checks/quality/spellcheck.js +11 -8
  111. package/dist/esm/core/checks/security/agent-instructions.d.ts +4 -0
  112. package/dist/esm/core/checks/security/agent-instructions.js +146 -0
  113. package/dist/esm/core/checks/security/diff-security.d.ts +7 -0
  114. package/dist/esm/core/checks/security/diff-security.js +21 -1
  115. package/dist/esm/core/checks/security/security.d.ts +0 -13
  116. package/dist/esm/core/checks/security/security.js +28 -1
  117. package/dist/esm/core/checks/security/workflow-security.js +33 -0
  118. package/dist/esm/core/commands.d.ts +6 -0
  119. package/dist/esm/core/commands.js +10 -0
  120. package/dist/esm/core/finding-store.d.ts +16 -0
  121. package/dist/esm/core/finding-store.js +67 -0
  122. package/dist/esm/core/install/doctor.js +12 -0
  123. package/dist/esm/core/install/grype-db.d.ts +25 -0
  124. package/dist/esm/core/install/grype-db.js +41 -0
  125. package/dist/esm/core/install/prepare.js +1 -1
  126. package/dist/esm/core/install/profiles.d.ts +1 -1
  127. package/dist/esm/core/install/profiles.js +1 -1
  128. package/dist/esm/core/install/reuse.js +6 -3
  129. package/dist/esm/core/registry.js +2 -0
  130. package/dist/esm/core/reporters.js +9 -0
  131. package/dist/esm/core/run-checks.js +17 -7
  132. package/dist/esm/core/sarif.d.ts +13 -0
  133. package/dist/esm/core/sarif.js +57 -0
  134. package/dist/esm/core/types.d.ts +7 -2
  135. package/dist/esm/index.d.ts +2 -1
  136. package/dist/esm/index.js +2 -0
  137. package/dist/esm/init.d.ts +0 -8
  138. package/dist/esm/init.js +15 -4
  139. package/dist/esm/interfaces/index.d.ts +12 -0
  140. package/dist/esm/mcp/config-tools.d.ts +30 -0
  141. package/dist/esm/mcp/config-tools.js +310 -0
  142. package/dist/esm/mcp/dependency.d.ts +33 -0
  143. package/dist/esm/mcp/dependency.js +402 -0
  144. package/dist/esm/mcp/protocol.d.ts +97 -0
  145. package/dist/esm/mcp/protocol.js +175 -0
  146. package/dist/esm/mcp/review.d.ts +63 -0
  147. package/dist/esm/mcp/review.js +139 -0
  148. package/dist/esm/mcp/serve.d.ts +8 -0
  149. package/dist/esm/mcp/serve.js +43 -0
  150. package/dist/esm/mcp/settings.d.ts +13 -0
  151. package/dist/esm/mcp/settings.js +43 -0
  152. package/dist/esm/mcp/tools.d.ts +23 -0
  153. package/dist/esm/mcp/tools.js +626 -0
  154. package/dist/esm/utils/execa.d.ts +7 -0
  155. package/dist/esm/utils/execa.js +40 -1
  156. package/dist/esm/utils/own-deps.js +8 -1
  157. package/dist/esm/utils/resolve-bin.d.ts +2 -0
  158. package/dist/esm/utils/resolve-bin.js +4 -0
  159. package/dist/esm/utils/result-collector.d.ts +17 -1
  160. package/dist/esm/utils/result-collector.js +7 -2
  161. package/eslint-default-rules.json +2 -8
  162. package/eslint.config.mjs +37 -8
  163. package/install-manifests/commitlint/package-lock.json +5 -4
  164. package/install-manifests/core/package-lock.json +5 -3
  165. package/install-manifests/js/package-lock.json +250 -571
  166. package/install-manifests/js/package.json +10 -8
  167. package/install-manifests/markdown/package-lock.json +16 -13
  168. package/install-manifests/markdown/package.json +3 -0
  169. package/install-manifests/spellcheck/package-lock.json +3 -3
  170. package/package.json +24 -16
  171. package/pr-checkmate.schema.json +35 -0
  172. package/src/config/.eslintrc.js +3 -9
  173. package/src/config/eslint-default-rules.json +2 -8
  174. package/src/config/pr-checkmate.schema.json +35 -0
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.agentInstructionsCheck = exports.AGENT_PATTERNS = void 0;
4
+ // cspell:ignore windsurfrules
5
+ const index_js_1 = require("../../../utils/index.js");
6
+ const targets_js_1 = require("../../targets.js");
7
+ const types_js_1 = require("../../types.js");
8
+ const findings_js_1 = require("../findings.js");
9
+ /**
10
+ * Files that tell an AI agent what to do, or what it may do.
11
+ *
12
+ * They are code in every sense that matters: an agent reads them and acts, with
13
+ * the developer's shell and the developer's credentials. A pull request that edits
14
+ * one changes what every contributor's agent will do on its next run, and the diff
15
+ * is prose — which review tends to skim. That makes this the cheapest place in a
16
+ * repository to plant an instruction, and the least reviewed.
17
+ */
18
+ const INSTRUCTION_GLOBS = [
19
+ 'AGENTS.md',
20
+ '**/AGENTS.md',
21
+ 'CLAUDE.md',
22
+ '**/CLAUDE.md',
23
+ 'GEMINI.md',
24
+ '.cursorrules',
25
+ '.windsurfrules',
26
+ '.cursor/rules/**',
27
+ '.github/copilot-instructions.md',
28
+ '.claude/commands/**',
29
+ '.claude/agents/**',
30
+ '.claude/skills/**',
31
+ '.claude/settings.json',
32
+ '.mcp.json',
33
+ ];
34
+ /** Dot-directories the file walk drops by default and this check lives in. */
35
+ const KEEP_DIRS = ['.claude', '.cursor', '.github'];
36
+ /**
37
+ * Characters a person cannot see and a model reads anyway: zero-width characters,
38
+ * bidirectional overrides and the Unicode "tag" block. Each has been used to hide
39
+ * an instruction inside text that looks harmless on screen. A byte-order mark is
40
+ * left out on purpose — editors put one at the top of ordinary files.
41
+ */
42
+ const INVISIBLE = '[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069]|[\\u{E0000}-\\u{E007F}]';
43
+ exports.AGENT_PATTERNS = [
44
+ {
45
+ pattern: new RegExp(`^\\+(?!\\+).*(?:${INVISIBLE})`, 'u'),
46
+ label: 'invisible Unicode characters (can hide instructions from a human reviewer)',
47
+ severity: 'error',
48
+ },
49
+ {
50
+ pattern:
51
+ // "Do not add X without telling the user" asks the agent to check in, so a
52
+ // `without` that follows a negation in the same sentence is not a match.
53
+ /^\+(?!\+)(?![^\n]*\b(?:do not|don't|never)\b[^.\n]*\bwithout\b).*\b(?:do not|don't|never|without)\b[^.\n]{0,30}\b(?:tell|telling|inform|informing|mention|show|reveal|notify)\b[^.\n]{0,20}\b(?:the )?(?:user|developer|human)\b/i,
54
+ label: 'tells the agent to hide something from the user',
55
+ severity: 'error',
56
+ },
57
+ {
58
+ pattern: /^\+(?!\+).*\b(?:send|post|upload|curl|wget|fetch|exfiltrate|email)\b[^\n]{0,80}(?:\.env\b|credentials?|secrets?|api[_ -]?keys?|\btokens?\b|\bssh\b|id_rsa)[^\n]{0,80}https?:\/\//i,
59
+ label: 'sends credentials or secrets to a URL',
60
+ severity: 'error',
61
+ },
62
+ {
63
+ pattern: /^\+(?!\+).*\b(?:ignore|disregard|forget|override)\b[^.\n]{0,40}\b(?:previous|prior|above|earlier|all|any)\b[^.\n]{0,30}\b(?:instructions?|rules|prompts?|guidelines)\b/i,
64
+ label: 'prompt-injection phrasing ("ignore previous instructions")',
65
+ severity: 'warn',
66
+ },
67
+ {
68
+ pattern: /^\+(?!\+).*\b(?:curl|wget)\b[^|\n]*\|\s*(?:sudo\s+)?(?:sh|bash|zsh)\b/i,
69
+ label: 'pipes a download straight into a shell (an agent will run what it is told to run)',
70
+ severity: 'warn',
71
+ },
72
+ {
73
+ pattern: /^\+(?!\+).*(?:--no-verify|--dangerously-skip-permissions|--no-gpg-sign)\b/,
74
+ label: 'instructs the agent to skip a safety check',
75
+ severity: 'warn',
76
+ },
77
+ {
78
+ pattern: /^\+(?!\+)\s*"defaultMode"\s*:\s*"bypassPermissions"/,
79
+ label: 'bypassPermissions as the project default (every contributor’s agent runs without asking)',
80
+ severity: 'error',
81
+ },
82
+ {
83
+ pattern: /^\+(?!\+)\s*"enableAllProjectMcpServers"\s*:\s*true/,
84
+ label: 'auto-approves every MCP server this repository declares',
85
+ severity: 'warn',
86
+ },
87
+ {
88
+ pattern: /^\+(?!\+)\s*"(?:Bash|Write|Edit)(?:\((?:\*|:\*)\))?"\s*,?\s*$/,
89
+ label: 'permission granted for every shell command or file write',
90
+ severity: 'warn',
91
+ },
92
+ {
93
+ pattern: /^\+(?!\+).*"url"\s*:\s*"http:\/\/(?!localhost|127\.0\.0\.1|\[::1\])/,
94
+ label: 'MCP server reached over plain HTTP',
95
+ severity: 'warn',
96
+ },
97
+ {
98
+ // `"args": ["-y", "pkg"]` with no @version: whatever is published next runs on
99
+ // the machine of everyone who opens the repository.
100
+ pattern: /^\+(?!\+).*"args"\s*:\s*\[\s*"-y"\s*,\s*"(?:@[^/"@]+\/)?[^"@]+"/,
101
+ label: 'MCP server fetched with npx and no pinned version (the next publish runs on every contributor’s machine)',
102
+ severity: 'warn',
103
+ },
104
+ ];
105
+ exports.agentInstructionsCheck = {
106
+ name: 'Agent Instructions',
107
+ phase: 'informational',
108
+ // No `requiresDiff`: a hidden instruction is as live in a file that arrived last
109
+ // year as in one added today, so a whole-repository run reads them too.
110
+ applies(ctx) {
111
+ return ctx.config.agentInstructions?.enabled !== false;
112
+ },
113
+ async run(ctx) {
114
+ try {
115
+ const cfg = ctx.config.agentInstructions ?? {};
116
+ const found = (0, findings_js_1.emptyBuckets)();
117
+ const scanInto = (stdout) => {
118
+ // `comments: 'off'`: these are Markdown and JSON, where a leading `#` is a
119
+ // heading and `//` is part of a URL — stripping them as comments would
120
+ // blank exactly the lines worth reading.
121
+ (0, findings_js_1.mergeSeverityBuckets)(found, (0, findings_js_1.scanDiffPatterns)(stdout, exports.AGENT_PATTERNS, {
122
+ comments: 'off',
123
+ ignorePaths: cfg.ignorePaths,
124
+ }));
125
+ };
126
+ if (ctx.baseSha) {
127
+ scanInto(await (0, findings_js_1.diffAddedLines)(ctx, INSTRUCTION_GLOBS));
128
+ }
129
+ else if (ctx.config.run?.fullScanBehaviour !== 'skip') {
130
+ const files = await (0, targets_js_1.listTrackedFiles)(ctx, INSTRUCTION_GLOBS, ['.'], {
131
+ keepDirs: KEEP_DIRS,
132
+ });
133
+ if (files === null)
134
+ return (0, types_js_1.skip)('git unavailable');
135
+ await (0, findings_js_1.scanTrackedFiles)(ctx, files, scanInto);
136
+ }
137
+ return (0, findings_js_1.reportSeverityBuckets)(found.errors, found.warnings, {
138
+ prefix: 'Agent Instructions',
139
+ errorSummary: n => `${n} issue(s)`,
140
+ warningSummary: n => `${n} warning(s)`,
141
+ ...(0, findings_js_1.clientFindingOptions)(ctx, cfg),
142
+ });
143
+ }
144
+ catch (err) {
145
+ index_js_1.logger.warn(`[Agent Instructions]: ⚠️ Could not analyze agent files\n${err}`);
146
+ return (0, types_js_1.skip)('agent files unavailable');
147
+ }
148
+ },
149
+ };
@@ -1,2 +1,9 @@
1
1
  import { Check } from '../../types.js';
2
+ import { DiffPattern } from '../findings.js';
3
+ /**
4
+ * npm supply-chain entry points, matched in `package.json`. Both are how a
5
+ * compromised package reaches a developer machine or CI runner, so they are
6
+ * worth a human look whenever one is added — neither is wrong by itself.
7
+ */
8
+ export declare const PACKAGE_JSON_PATTERNS: DiffPattern[];
2
9
  export declare const diffSecurityCheck: Check;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.diffSecurityCheck = void 0;
3
+ exports.diffSecurityCheck = exports.PACKAGE_JSON_PATTERNS = void 0;
4
4
  const index_js_1 = require("../../../config/index.js");
5
5
  const index_js_2 = require("../../../utils/index.js");
6
6
  const types_js_1 = require("../../types.js");
@@ -235,6 +235,25 @@ const KOTLIN_SECURITY_PATTERNS = [
235
235
  },
236
236
  ];
237
237
  const JS_GLOBS = ['*.ts', '*.tsx', '*.js', '*.jsx', '*.mjs', '*.cjs'];
238
+ const PACKAGE_JSON_GLOBS = ['package.json', '*/package.json'];
239
+ /**
240
+ * npm supply-chain entry points, matched in `package.json`. Both are how a
241
+ * compromised package reaches a developer machine or CI runner, so they are
242
+ * worth a human look whenever one is added — neither is wrong by itself.
243
+ */
244
+ exports.PACKAGE_JSON_PATTERNS = [
245
+ {
246
+ pattern: /^\+(?!\+)\s*"(?:preinstall|install|postinstall)"\s*:/,
247
+ label: 'npm install lifecycle script (runs on every consumer install)',
248
+ severity: 'warn',
249
+ },
250
+ {
251
+ // `repository.url` is the one key that legitimately holds a `git+https:` value.
252
+ pattern: /^\+(?!\+)\s*"(?!url")[^"]+"\s*:\s*"(?:git\+[a-z]+:|git:\/\/|git@|github:|https?:\/\/[^"]+\.(?:tgz|tar\.gz)")/,
253
+ label: 'dependency from a git or tarball URL (bypasses registry integrity and audit)',
254
+ severity: 'warn',
255
+ },
256
+ ];
238
257
  /**
239
258
  * Compile the client's `extraPatterns` into the same shape as the built-ins, so
240
259
  * a project rule gets identical treatment: comment stripping, the code/raw view,
@@ -323,6 +342,7 @@ exports.diffSecurityCheck = {
323
342
  // Client `extraPatterns` run against every scanned language, since a
324
343
  // project rule is not tied to the built-in language split.
325
344
  const sets = [[JS_GLOBS, [...SECURITY_PATTERNS, ...extra]]];
345
+ sets.push([PACKAGE_JSON_GLOBS, exports.PACKAGE_JSON_PATTERNS]);
326
346
  if (ctx.languages.includes('python')) {
327
347
  sets.push([['*.py'], [...PYTHON_SECURITY_PATTERNS, ...extra]]);
328
348
  }
@@ -30,19 +30,6 @@ import { Check } from '../../types.js';
30
30
  * the caller keeps its old wording rather than claiming a number it does not have.
31
31
  */
32
32
  export declare function countFindings(reportPath: string): number | null;
33
- /**
34
- * How the outcome names what was found.
35
- *
36
- * Its own function so both halves are reachable without running the scanner. The
37
- * corpus for this check deliberately does not: the wrapper downloads a gitleaks
38
- * binary on first use, and a unit suite that reaches the network is the thing the
39
- * `installer` seam in `ensure.ts` exists to avoid. What broke here was the wording
40
- * — a row that read the same for one secret and for thirty-nine — and the wording
41
- * is testable on its own.
42
- *
43
- * `null` keeps the old sentence rather than inventing a number: a report we could
44
- * not read is not a reason to claim a count.
45
- */
46
33
  export declare function secretsSummary(found: number | null): string;
47
34
  /**
48
35
  * Whether the wrapper died instead of reporting, judged from its stderr.
@@ -13,6 +13,7 @@ const node_path_1 = __importDefault(require("node:path"));
13
13
  const index_js_1 = require("../../../config/index.js");
14
14
  const execa_js_1 = require("../../../utils/execa.js");
15
15
  const index_js_2 = require("../../../utils/index.js");
16
+ const finding_store_js_1 = require("../../finding-store.js");
16
17
  const types_js_1 = require("../../types.js");
17
18
  /**
18
19
  * A HOME the scanner can actually write to.
@@ -66,6 +67,31 @@ function countFindings(reportPath) {
66
67
  * `null` keeps the old sentence rather than inventing a number: a report we could
67
68
  * not read is not a reason to claim a count.
68
69
  */
70
+ /**
71
+ * Tell the report where each leak is. The secret itself is never copied: the
72
+ * finding carries a file, a line and the rule that matched, which is all anyone
73
+ * needs to go and remove it — and all that is safe to hand to an agent's context.
74
+ */
75
+ function recordLeaks(reportPath, cwd) {
76
+ try {
77
+ const report = JSON.parse(node_fs_1.default.readFileSync(reportPath, 'utf-8'));
78
+ if (!Array.isArray(report))
79
+ return;
80
+ for (const leak of report) {
81
+ if (typeof leak?.File !== 'string')
82
+ continue;
83
+ (0, finding_store_js_1.addFinding)({
84
+ file: node_path_1.default.isAbsolute(leak.File) ? node_path_1.default.relative(cwd, leak.File) : leak.File,
85
+ line: typeof leak.StartLine === 'number' ? leak.StartLine : undefined,
86
+ rule: typeof leak.RuleID === 'string' ? leak.RuleID : 'secret',
87
+ severity: 'error',
88
+ });
89
+ }
90
+ }
91
+ catch {
92
+ // An unreadable report already shows up as an uncounted summary; no locations to add.
93
+ }
94
+ }
69
95
  function secretsSummary(found) {
70
96
  return found === null ? 'potential secrets detected' : `${found} potential secret(s)`;
71
97
  }
@@ -186,6 +212,7 @@ exports.securityCheck = {
186
212
  if (stderr.trim())
187
213
  index_js_2.logger.warn(stderr.trimEnd());
188
214
  const found = countFindings(reportPath);
215
+ recordLeaks(reportPath, ctx.cwd);
189
216
  node_fs_1.default.rmSync(reportDir, { recursive: true, force: true });
190
217
  if (result.exitCode === 0)
191
218
  return (0, types_js_1.pass)();
@@ -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
+ }
@@ -16,6 +16,7 @@ const package_version_js_1 = require("../../utils/package-version.js");
16
16
  Object.defineProperty(exports, "packageVersion", { enumerable: true, get: function () { return package_version_js_1.packageVersion; } });
17
17
  const cache_js_1 = require("./cache.js");
18
18
  const ensure_js_1 = require("./ensure.js");
19
+ const grype_db_js_1 = require("./grype-db.js");
19
20
  const prepare_js_1 = require("./prepare.js");
20
21
  const profiles_js_1 = require("./profiles.js");
21
22
  /**
@@ -142,6 +143,17 @@ async function runDoctor(cwd = process.cwd()) {
142
143
  lines.push(` ${missing.length} profile(s) not installed yet. They are fetched on the next`);
143
144
  lines.push(' run, or now with: npx pr-checkmate install');
144
145
  }
146
+ if (config.grypeScan?.enabled !== false) {
147
+ const db = await (0, grype_db_js_1.grypeDbState)(cwd);
148
+ lines.push('');
149
+ lines.push(' Grype database (shared by all projects on this machine)');
150
+ if (db.state === 'missing-tool')
151
+ lines.push(' — grype is not installed (brew install grype)');
152
+ else if (db.state === 'ready')
153
+ lines.push(` ✅ up to date${db.built ? ` (built ${db.built})` : ''}`);
154
+ else
155
+ lines.push(` ⬇️ stale${db.built ? ` (built ${db.built})` : ''} — refresh: npx pr-checkmate install`);
156
+ }
145
157
  const stale = (0, cache_js_1.stalePaths)((0, package_version_js_1.packageVersion)(), config.install?.cacheDir);
146
158
  if (stale.length > 0) {
147
159
  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,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.grypeDbState = grypeDbState;
4
+ exports.ensureGrypeDb = ensureGrypeDb;
5
+ const execa_js_1 = require("../../utils/execa.js");
6
+ const logger_js_1 = require("../../utils/logger.js");
7
+ /** `Built:` line of `grype db status`, so `doctor` can say how old the data is. */
8
+ const builtOf = (stdout) => typeof stdout === 'string' ? /^Built:\s*(.+)$/m.exec(stdout)?.[1]?.trim() : undefined;
9
+ /**
10
+ * Is grype's vulnerability database usable right now? grype refuses one older than
11
+ * 5 days (exit code 1 from `db status`). The database lives in grype's own cache,
12
+ * shared by every project on the machine, so one refresh serves them all.
13
+ */
14
+ async function grypeDbState(cwd) {
15
+ const status = await (0, execa_js_1.execa)('grype', ['db', 'status'], { cwd, reject: false, timeout: 30000 });
16
+ if (status.code === 'ENOENT')
17
+ return { state: 'missing-tool' };
18
+ const built = builtOf(status.stdout);
19
+ return status.exitCode === 0 ? { state: 'ready', built } : { state: 'stale', built };
20
+ }
21
+ /**
22
+ * Download a fresh database when the current one is missing or stale.
23
+ *
24
+ * `timeoutMs` bounds the download: a check inside a run passes one so it can never
25
+ * look like a hung commit, `install` passes none because a person asked for exactly
26
+ * this and the download is the whole job. Returns why it is still unusable, or
27
+ * `null` when it is ready.
28
+ */
29
+ async function ensureGrypeDb(cwd, timeoutMs) {
30
+ const current = await grypeDbState(cwd);
31
+ if (current.state === 'missing-tool')
32
+ return 'ENOENT';
33
+ if (current.state === 'ready')
34
+ return null;
35
+ logger_js_1.logger.info('[Grype Scan]: vulnerability database missing or stale — updating it');
36
+ const update = await (0, execa_js_1.execa)('grype', ['db', 'update'], {
37
+ cwd,
38
+ reject: false,
39
+ ...(timeoutMs ? { timeout: timeoutMs } : {}),
40
+ });
41
+ if (update.timedOut) {
42
+ return `database update exceeded ${timeoutMs}ms — run "grype db update" once, then retry`;
43
+ }
44
+ return update.exitCode === 0 ? null : 'database update failed (offline?) — run "grype db update"';
45
+ }
@@ -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 package ships ~233 MB of tooling, and no repository needs all of it: a
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.
@@ -25,9 +25,9 @@ exports.PROFILES = [
25
25
  'typescript',
26
26
  'eslint',
27
27
  'eslint-config-prettier',
28
- 'eslint-plugin-import-x',
29
28
  'eslint-plugin-jsdoc',
30
29
  'eslint-plugin-prettier',
30
+ 'eslint-plugin-simple-import-sort',
31
31
  'eslint-plugin-sonarjs',
32
32
  '@typescript-eslint/eslint-plugin',
33
33
  '@typescript-eslint/parser',
@@ -12,15 +12,18 @@ const resolve_bin_js_1 = require("../../utils/resolve-bin.js");
12
12
  /**
13
13
  * The major version we declare for a tool, from our own manifest.
14
14
  *
15
- * Read from `dependencies` rather than from the installed tree: what matters is
15
+ * Read from the manifest rather than from the installed tree: what matters is
16
16
  * the version our configuration was written against, and a hoisted install can
17
17
  * resolve to something else entirely.
18
18
  */
19
19
  function ownMajor(pkgName) {
20
20
  try {
21
21
  const manifest = node_path_1.default.join((0, resolve_bin_js_1.ownPackageRoot)(), 'package.json');
22
- const deps = JSON.parse(node_fs_1.default.readFileSync(manifest, 'utf-8')).dependencies;
23
- const range = deps?.[pkgName];
22
+ const parsed = JSON.parse(node_fs_1.default.readFileSync(manifest, 'utf-8'));
23
+ // Profile tooling is declared in `devDependencies` — it is fetched on demand
24
+ // rather than installed with us — but it is still the version our config was
25
+ // written against, and the manifest keeps both.
26
+ const range = parsed.dependencies?.[pkgName] ?? parsed.devDependencies?.[pkgName];
24
27
  if (!range)
25
28
  return null;
26
29
  // `^10.6.0`, `>=3.0.0`, `~5.1` — the leading number is the major in every
@@ -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. */