pr-checkmate 1.22.8 → 1.23.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 (129) hide show
  1. package/README.md +101 -333
  2. package/cspell.json +9 -1
  3. package/dist/cli.js +32 -2
  4. package/dist/config/constants.d.ts +9 -0
  5. package/dist/config/constants.js +68 -1
  6. package/dist/core/checks/dependencies/banned-imports.d.ts +2 -0
  7. package/dist/core/checks/dependencies/banned-imports.js +71 -0
  8. package/dist/core/checks/{circular-deps.d.ts → dependencies/circular-deps.d.ts} +1 -1
  9. package/dist/core/checks/{circular-deps.js → dependencies/circular-deps.js} +5 -5
  10. package/dist/core/checks/{dependency-check.d.ts → dependencies/dependency-check.d.ts} +1 -1
  11. package/dist/core/checks/{dependency-check.js → dependencies/dependency-check.js} +3 -3
  12. package/dist/core/checks/{license-check.d.ts → dependencies/license-check.d.ts} +1 -1
  13. package/dist/core/checks/{license-check.js → dependencies/license-check.js} +3 -3
  14. package/dist/core/checks/{npm-audit.d.ts → dependencies/npm-audit.d.ts} +1 -1
  15. package/dist/core/checks/{npm-audit.js → dependencies/npm-audit.js} +3 -3
  16. package/dist/core/checks/{outdated-deps.d.ts → dependencies/outdated-deps.d.ts} +1 -1
  17. package/dist/core/checks/{outdated-deps.js → dependencies/outdated-deps.js} +3 -3
  18. package/dist/core/checks/dependencies/vuln-scan.d.ts +9 -0
  19. package/dist/core/checks/dependencies/vuln-scan.js +41 -0
  20. package/dist/core/checks/findings.d.ts +37 -1
  21. package/dist/core/checks/findings.js +80 -3
  22. package/dist/core/checks/git/custom-rules.d.ts +2 -0
  23. package/dist/core/checks/git/custom-rules.js +102 -0
  24. package/dist/core/checks/{diff-smell.d.ts → git/diff-smell.d.ts} +1 -1
  25. package/dist/core/checks/{diff-smell.js → git/diff-smell.js} +3 -3
  26. package/dist/core/checks/git/large-files.d.ts +2 -0
  27. package/dist/core/checks/git/large-files.js +83 -0
  28. package/dist/core/checks/git/leftover-debug.d.ts +2 -0
  29. package/dist/core/checks/git/leftover-debug.js +77 -0
  30. package/dist/core/checks/git/lockfile-drift.d.ts +2 -0
  31. package/dist/core/checks/git/lockfile-drift.js +114 -0
  32. package/dist/core/checks/git/merge-conflict.d.ts +2 -0
  33. package/dist/core/checks/git/merge-conflict.js +109 -0
  34. package/dist/core/checks/git/missing-tests.d.ts +2 -0
  35. package/dist/core/checks/git/missing-tests.js +92 -0
  36. package/dist/core/checks/git/todo-fixme.d.ts +7 -0
  37. package/dist/core/checks/git/todo-fixme.js +74 -0
  38. package/dist/core/checks/{cpp-format.d.ts → languages/cpp-format.d.ts} +1 -1
  39. package/dist/core/checks/{cpp-format.js → languages/cpp-format.js} +3 -3
  40. package/dist/core/checks/languages/csharp-format.d.ts +13 -0
  41. package/dist/core/checks/languages/csharp-format.js +52 -0
  42. package/dist/core/checks/languages/go-format.d.ts +8 -0
  43. package/dist/core/checks/languages/go-format.js +48 -0
  44. package/dist/core/checks/languages/go-lint.d.ts +8 -0
  45. package/dist/core/checks/languages/go-lint.js +48 -0
  46. package/dist/core/checks/{kotlin-lint.d.ts → languages/kotlin-lint.d.ts} +1 -1
  47. package/dist/core/checks/{kotlin-lint.js → languages/kotlin-lint.js} +13 -17
  48. package/dist/core/checks/{lint.d.ts → languages/lint.d.ts} +1 -1
  49. package/dist/core/checks/{lint.js → languages/lint.js} +5 -5
  50. package/dist/core/checks/languages/php-format.d.ts +9 -0
  51. package/dist/core/checks/languages/php-format.js +49 -0
  52. package/dist/core/checks/{prettier.d.ts → languages/prettier.d.ts} +1 -1
  53. package/dist/core/checks/{prettier.js → languages/prettier.js} +5 -5
  54. package/dist/core/checks/{python-format.d.ts → languages/python-format.d.ts} +1 -1
  55. package/dist/core/checks/{python-format.js → languages/python-format.js} +3 -3
  56. package/dist/core/checks/{python-lint.d.ts → languages/python-lint.d.ts} +1 -1
  57. package/dist/core/checks/{python-lint.js → languages/python-lint.js} +3 -3
  58. package/dist/core/checks/languages/python-typecheck.d.ts +9 -0
  59. package/dist/core/checks/languages/python-typecheck.js +66 -0
  60. package/dist/core/checks/languages/ruby-lint.d.ts +8 -0
  61. package/dist/core/checks/languages/ruby-lint.js +50 -0
  62. package/dist/core/checks/languages/rust-format.d.ts +9 -0
  63. package/dist/core/checks/languages/rust-format.js +45 -0
  64. package/dist/core/checks/languages/rust-lint.d.ts +8 -0
  65. package/dist/core/checks/languages/rust-lint.js +50 -0
  66. package/dist/core/checks/{swift-lint.d.ts → languages/swift-lint.d.ts} +1 -1
  67. package/dist/core/checks/{swift-lint.js → languages/swift-lint.js} +13 -17
  68. package/dist/core/checks/{typecheck.d.ts → languages/typecheck.d.ts} +1 -1
  69. package/dist/core/checks/{typecheck.js → languages/typecheck.js} +3 -3
  70. package/dist/core/checks/{commitlint.d.ts → pr/commitlint.d.ts} +1 -1
  71. package/dist/core/checks/{commitlint.js → pr/commitlint.js} +4 -4
  72. package/dist/core/checks/{pr-body.d.ts → pr/pr-body.d.ts} +1 -1
  73. package/dist/core/checks/{pr-body.js → pr/pr-body.js} +2 -2
  74. package/dist/core/checks/{pr-size-check.d.ts → pr/pr-size-check.d.ts} +1 -1
  75. package/dist/core/checks/{pr-size-check.js → pr/pr-size-check.js} +13 -11
  76. package/dist/core/checks/{pr-title-lint.d.ts → pr/pr-title-lint.d.ts} +1 -1
  77. package/dist/core/checks/{pr-title-lint.js → pr/pr-title-lint.js} +2 -2
  78. package/dist/core/checks/quality/case-collision.d.ts +8 -0
  79. package/dist/core/checks/quality/case-collision.js +40 -0
  80. package/dist/core/checks/quality/config-validation.d.ts +8 -0
  81. package/dist/core/checks/quality/config-validation.js +63 -0
  82. package/dist/core/checks/quality/coverage.d.ts +2 -0
  83. package/dist/core/checks/quality/coverage.js +63 -0
  84. package/dist/core/checks/{dead-code.d.ts → quality/dead-code.d.ts} +1 -1
  85. package/dist/core/checks/{dead-code.js → quality/dead-code.js} +4 -4
  86. package/dist/core/checks/{duplicate-check.d.ts → quality/duplicate-check.d.ts} +1 -1
  87. package/dist/core/checks/{duplicate-check.js → quality/duplicate-check.js} +6 -6
  88. package/dist/core/checks/quality/license-header.d.ts +7 -0
  89. package/dist/core/checks/quality/license-header.js +74 -0
  90. package/dist/core/checks/{markdown-lint.d.ts → quality/markdown-lint.d.ts} +1 -1
  91. package/dist/core/checks/{markdown-lint.js → quality/markdown-lint.js} +5 -5
  92. package/dist/core/checks/{spellcheck.d.ts → quality/spellcheck.d.ts} +1 -1
  93. package/dist/core/checks/{spellcheck.js → quality/spellcheck.js} +5 -5
  94. package/dist/core/checks/{diff-security.d.ts → security/diff-security.d.ts} +1 -1
  95. package/dist/core/checks/security/diff-security.js +262 -0
  96. package/dist/core/checks/security/dockerfile-security.d.ts +2 -0
  97. package/dist/core/checks/security/dockerfile-security.js +92 -0
  98. package/dist/core/checks/security/migration-safety.d.ts +2 -0
  99. package/dist/core/checks/security/migration-safety.js +110 -0
  100. package/dist/core/checks/{security.d.ts → security/security.d.ts} +1 -1
  101. package/dist/core/checks/{security.js → security/security.js} +4 -4
  102. package/dist/core/checks/{sensitive-file-guard.d.ts → security/sensitive-file-guard.d.ts} +1 -1
  103. package/dist/core/checks/{sensitive-file-guard.js → security/sensitive-file-guard.js} +4 -4
  104. package/dist/core/checks/security/workflow-security.d.ts +2 -0
  105. package/dist/core/checks/security/workflow-security.js +75 -0
  106. package/dist/core/context.js +14 -0
  107. package/dist/core/registry.js +73 -28
  108. package/dist/core/reporters.d.ts +4 -3
  109. package/dist/core/reporters.js +104 -4
  110. package/dist/core/run-checks.js +21 -3
  111. package/dist/core/targets.d.ts +4 -1
  112. package/dist/core/targets.js +16 -3
  113. package/dist/core/types.d.ts +13 -1
  114. package/dist/init.js +63 -0
  115. package/dist/interfaces/index.d.ts +99 -0
  116. package/dist/scripts/copy-configs.js +1 -0
  117. package/dist/utils/detect-language.d.ts +1 -1
  118. package/dist/utils/detect-language.js +32 -3
  119. package/dist/utils/glob.d.ts +10 -0
  120. package/dist/utils/glob.js +25 -0
  121. package/dist/utils/index.d.ts +1 -0
  122. package/dist/utils/index.js +1 -0
  123. package/package.json +2 -1
  124. package/pr-checkmate.schema.json +166 -0
  125. package/src/config/cspell.json +9 -1
  126. package/src/config/pr-checkmate.schema.json +166 -0
  127. package/dist/core/checks/diff-security.js +0 -145
  128. /package/dist/core/checks/{ruff-shared.d.ts → languages/ruff-shared.d.ts} +0 -0
  129. /package/dist/core/checks/{ruff-shared.js → languages/ruff-shared.js} +0 -0
@@ -0,0 +1,262 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.diffSecurityCheck = void 0;
4
+ const utils_1 = require("../../../utils");
5
+ const types_1 = require("../../types");
6
+ const findings_1 = require("../findings");
7
+ const MD5_LABEL = 'MD5 (weak hash — use sha256 or stronger)';
8
+ const SHA1_LABEL = 'SHA-1 (weak hash — use sha256 or stronger)';
9
+ const SECURITY_PATTERNS = [
10
+ {
11
+ pattern: /^\+(?!\+).*dangerouslySetInnerHTML\s*=/,
12
+ label: 'dangerouslySetInnerHTML (XSS risk)',
13
+ severity: 'error',
14
+ },
15
+ {
16
+ pattern: /^\+(?!\+).*\.innerHTML\s*=/,
17
+ label: 'innerHTML assignment (XSS risk)',
18
+ severity: 'warn',
19
+ },
20
+ {
21
+ pattern: /^\+(?!\+).*document\.write\s*\(/,
22
+ label: 'document.write() (XSS risk)',
23
+ severity: 'error',
24
+ },
25
+ {
26
+ pattern: /^\+(?!\+)(?![^'"`]*['"`]).*\beval\s*\(/,
27
+ label: 'eval() (code injection risk)',
28
+ severity: 'error',
29
+ },
30
+ {
31
+ pattern: /^\+(?!\+).*\bnew\s+Function\s*\(/,
32
+ label: 'new Function() (code injection risk)',
33
+ severity: 'error',
34
+ },
35
+ {
36
+ pattern: /^\+(?!\+).*__proto__\s*[=:[]/,
37
+ label: '__proto__ mutation (prototype pollution)',
38
+ severity: 'error',
39
+ },
40
+ {
41
+ pattern: /^\+(?!\+).*\.constructor\.prototype\b/,
42
+ label: 'constructor.prototype access (prototype pollution)',
43
+ severity: 'warn',
44
+ },
45
+ {
46
+ pattern: /^\+(?!\+).*createHash\s*\(\s*['"]md5['"]/,
47
+ label: MD5_LABEL,
48
+ severity: 'warn',
49
+ },
50
+ {
51
+ pattern: /^\+(?!\+).*createHash\s*\(\s*['"]sha1['"]/,
52
+ label: SHA1_LABEL,
53
+ severity: 'warn',
54
+ },
55
+ {
56
+ pattern: /^\+(?!\+).*\bcreateDecipher\s*\(/,
57
+ label: 'deprecated createDecipher() — use createDecipheriv()',
58
+ severity: 'warn',
59
+ },
60
+ {
61
+ pattern: /^\+(?!\+).*\bcreateCipher\s*\(/,
62
+ label: 'deprecated createCipher() — use createCipheriv()',
63
+ severity: 'warn',
64
+ },
65
+ {
66
+ pattern: /^\+(?!\+).*cors\s*\(\s*\{[^}]*origin\s*:\s*['"]\*['"]/,
67
+ label: "CORS wildcard origin: '*' (allows any domain)",
68
+ severity: 'warn',
69
+ },
70
+ {
71
+ pattern: /^\+(?!\+).*['"]Access-Control-Allow-Origin['"]\s*[,:]\s*['"]\*['"]/,
72
+ label: "Access-Control-Allow-Origin: '*' header (CORS wildcard)",
73
+ severity: 'warn',
74
+ },
75
+ {
76
+ pattern: /^\+(?!\+).*(fetch|axios\.get|axios\.post|axios\.put|axios\.delete|http\.get|http\.request)\s*\(\s*['"]http:\/\//,
77
+ label: 'Insecure HTTP (non-HTTPS) in external call',
78
+ severity: 'warn',
79
+ },
80
+ {
81
+ pattern: /^\+(?!\+).*(exec|execSync|spawn|spawnSync)\s*\(\s*`[^`]*\$\{/,
82
+ label: 'exec/spawn with template literal (command injection risk)',
83
+ severity: 'error',
84
+ },
85
+ {
86
+ pattern: /^\+(?!\+).*(exec|execSync)\s*\(\s*['"][^'"]*['"\s]*\+/,
87
+ label: 'exec/spawn with string concatenation (command injection risk)',
88
+ severity: 'error',
89
+ },
90
+ {
91
+ pattern: /^\+(?!\+).*\b(query|execute|raw)\s*\(\s*`[^`]*\$\{/,
92
+ label: 'SQL query with template literal interpolation (SQL injection risk)',
93
+ severity: 'error',
94
+ },
95
+ {
96
+ // A quoted string that contains a whole SQL keyword, concatenated with a
97
+ // variable — the real injection shape. Word boundaries + the quote context
98
+ // stop identifiers like `insertions + deletions` from false-positiving.
99
+ pattern: /^\+(?!\+).*['"`][^'"`]*\b(SELECT|INSERT|UPDATE|DELETE|WHERE)\b[^'"`]*['"`]\s*\+\s*\w/i,
100
+ label: 'SQL string concatenation (SQL injection risk)',
101
+ severity: 'error',
102
+ },
103
+ ];
104
+ /** Python-specific dangerous constructs, scanned in *.py diffs. */
105
+ const PYTHON_SECURITY_PATTERNS = [
106
+ { pattern: /^\+(?!\+).*\beval\s*\(/, label: 'eval() (code injection risk)', severity: 'error' },
107
+ { pattern: /^\+(?!\+).*\bexec\s*\(/, label: 'exec() (code injection risk)', severity: 'error' },
108
+ {
109
+ pattern: /^\+(?!\+).*\bos\.system\s*\(/,
110
+ label: 'os.system() (command injection risk)',
111
+ severity: 'error',
112
+ },
113
+ {
114
+ pattern: /^\+(?!\+).*\bsubprocess\.\w+\([^)]*shell\s*=\s*True/,
115
+ label: 'subprocess with shell=True (command injection risk)',
116
+ severity: 'error',
117
+ },
118
+ {
119
+ pattern: /^\+(?!\+).*\bpickle\.loads?\s*\(/,
120
+ label: 'pickle load (insecure deserialization)',
121
+ severity: 'error',
122
+ },
123
+ {
124
+ pattern: /^\+(?!\+).*\byaml\.load\s*\((?![^)]*Safe)/,
125
+ label: 'yaml.load without SafeLoader (unsafe deserialization)',
126
+ severity: 'warn',
127
+ },
128
+ {
129
+ pattern: /^\+(?!\+).*\bhashlib\.md5\s*\(/,
130
+ label: MD5_LABEL,
131
+ severity: 'warn',
132
+ },
133
+ {
134
+ pattern: /^\+(?!\+).*\bhashlib\.sha1\s*\(/,
135
+ label: SHA1_LABEL,
136
+ severity: 'warn',
137
+ },
138
+ {
139
+ pattern: /^\+(?!\+).*\bverify\s*=\s*False\b/,
140
+ label: 'TLS verification disabled (verify=False)',
141
+ severity: 'warn',
142
+ },
143
+ {
144
+ pattern: /^\+(?!\+).*['"][^'"]*\b(SELECT|INSERT|UPDATE|DELETE)\b[^'"]*['"]\s*%\s*/i,
145
+ label: 'SQL string formatting (SQL injection risk)',
146
+ severity: 'error',
147
+ },
148
+ ];
149
+ /** Go-specific dangerous constructs, scanned in *.go diffs. */
150
+ const GO_SECURITY_PATTERNS = [
151
+ {
152
+ pattern: /^\+(?!\+).*\bmd5\.New\s*\(/,
153
+ label: MD5_LABEL,
154
+ severity: 'warn',
155
+ },
156
+ {
157
+ pattern: /^\+(?!\+).*\bsha1\.New\s*\(/,
158
+ label: SHA1_LABEL,
159
+ severity: 'warn',
160
+ },
161
+ {
162
+ pattern: /^\+(?!\+).*InsecureSkipVerify\s*:\s*true/,
163
+ label: 'InsecureSkipVerify: true (TLS verification disabled)',
164
+ severity: 'error',
165
+ },
166
+ {
167
+ pattern: /^\+(?!\+).*\bexec\.Command\s*\([^)]*\+/,
168
+ label: 'exec.Command with string concatenation (command injection risk)',
169
+ severity: 'warn',
170
+ },
171
+ {
172
+ pattern: /^\+(?!\+).*\bfmt\.Sprintf\s*\([^)]*\b(SELECT|INSERT|UPDATE|DELETE)\b/i,
173
+ label: 'SQL built with fmt.Sprintf (SQL injection risk)',
174
+ severity: 'warn',
175
+ },
176
+ { pattern: /^\+(?!\+).*"crypto\/des"/, label: 'DES (weak cipher)', severity: 'warn' },
177
+ ];
178
+ /** Kotlin/Java-specific dangerous constructs, scanned in *.kt/*.kts/*.java diffs. */
179
+ const KOTLIN_SECURITY_PATTERNS = [
180
+ {
181
+ pattern: /^\+(?!\+).*\bRuntime\.getRuntime\(\)\.exec\s*\(/,
182
+ label: 'Runtime.exec() (command injection risk)',
183
+ severity: 'error',
184
+ },
185
+ {
186
+ pattern: /^\+(?!\+).*\bMessageDigest\.getInstance\s*\(\s*"(MD5|SHA-1)"/,
187
+ label: 'weak hash (MD5/SHA-1 via MessageDigest — use SHA-256+)',
188
+ severity: 'warn',
189
+ },
190
+ {
191
+ pattern: /^\+(?!\+).*\bCipher\.getInstance\s*\(\s*"(DES|.*ECB)/,
192
+ label: 'weak cipher (DES / ECB mode)',
193
+ severity: 'warn',
194
+ },
195
+ {
196
+ pattern: /^\+(?!\+).*ALLOW_ALL_HOSTNAME_VERIFIER/,
197
+ label: 'ALLOW_ALL_HOSTNAME_VERIFIER (TLS hostname verification disabled)',
198
+ severity: 'error',
199
+ },
200
+ {
201
+ pattern: /^\+(?!\+).*\bObjectInputStream\b/,
202
+ label: 'ObjectInputStream (insecure deserialization risk)',
203
+ severity: 'warn',
204
+ },
205
+ {
206
+ pattern: /^\+(?!\+).*"[^"]*\b(SELECT|INSERT|UPDATE|DELETE)\b[^"]*"\s*\+\s*\w/i,
207
+ label: 'SQL string concatenation (SQL injection risk)',
208
+ severity: 'error',
209
+ },
210
+ ];
211
+ const JS_GLOBS = ['*.ts', '*.tsx', '*.js', '*.jsx', '*.mjs', '*.cjs'];
212
+ /** Scan a diff with a pattern set, accumulating matches into severity buckets. */
213
+ function scanInto(stdout, patterns, errors, warnings) {
214
+ for (const line of stdout.split('\n')) {
215
+ // Match against the comment-stripped line so `eval(x)` in a comment isn't
216
+ // flagged, but report the original line for context.
217
+ const scanned = (0, findings_1.stripInlineComment)(line);
218
+ for (const { pattern, label, severity } of patterns) {
219
+ if (pattern.test(scanned)) {
220
+ const bucket = severity === 'error' ? errors : warnings;
221
+ (bucket[label] ?? (bucket[label] = [])).push(line.slice(1).trim().substring(0, 120));
222
+ }
223
+ }
224
+ }
225
+ }
226
+ exports.diffSecurityCheck = {
227
+ name: 'Diff Security',
228
+ phase: 'informational',
229
+ applies(ctx) {
230
+ return ctx.config.diffSecurity?.enabled !== false && Boolean(ctx.baseSha);
231
+ },
232
+ async run(ctx) {
233
+ try {
234
+ const errors = {};
235
+ const warnings = {};
236
+ // JS/TS patterns always run; language-specific sets scan their own globs
237
+ // only when that language is present, so a Python repo gets Python rules.
238
+ scanInto(await (0, findings_1.diffAddedLines)(ctx, JS_GLOBS), SECURITY_PATTERNS, errors, warnings);
239
+ if (ctx.languages.includes('python')) {
240
+ scanInto(await (0, findings_1.diffAddedLines)(ctx, ['*.py']), PYTHON_SECURITY_PATTERNS, errors, warnings);
241
+ }
242
+ if (ctx.languages.includes('go')) {
243
+ scanInto(await (0, findings_1.diffAddedLines)(ctx, ['*.go']), GO_SECURITY_PATTERNS, errors, warnings);
244
+ }
245
+ if (ctx.languages.includes('kotlin')) {
246
+ const ktOut = await (0, findings_1.diffAddedLines)(ctx, ['*.kt', '*.kts', '*.java']);
247
+ scanInto(ktOut, KOTLIN_SECURITY_PATTERNS, errors, warnings);
248
+ }
249
+ return (0, findings_1.reportSeverityBuckets)(errors, warnings, {
250
+ prefix: 'Diff Security',
251
+ errorSummary: n => `${n} issue(s)`,
252
+ warningSummary: n => `${n} warning(s)`,
253
+ sampleLimit: 3,
254
+ ignore: ctx.config.diffSecurity?.ignore,
255
+ });
256
+ }
257
+ catch (err) {
258
+ utils_1.logger.warn(`[Diff Security]: ⚠️ Could not analyze diff\n${err}`);
259
+ return (0, types_1.skip)('diff unavailable');
260
+ }
261
+ },
262
+ };
@@ -0,0 +1,2 @@
1
+ import { Check } from '../../types';
2
+ export declare const dockerfileSecurityCheck: Check;
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.dockerfileSecurityCheck = void 0;
4
+ const utils_1 = require("../../../utils");
5
+ const execa_1 = require("../../../utils/execa");
6
+ const targets_1 = require("../../targets");
7
+ const types_1 = require("../../types");
8
+ const findings_1 = require("../findings");
9
+ const DOCKERFILE_GLOBS = ['**/Dockerfile', '**/Dockerfile.*', '**/*.Dockerfile'];
10
+ /** Common Dockerfile security smells, matched on added diff lines. */
11
+ const DOCKER_PATTERNS = [
12
+ {
13
+ pattern: /^\+(?!\+)\s*FROM\s+\S+:latest\b/i,
14
+ label: 'FROM …:latest (pin the base image to a version/digest)',
15
+ severity: 'warn',
16
+ },
17
+ {
18
+ pattern: /^\+(?!\+)\s*USER\s+root\b/i,
19
+ label: 'USER root (run the container as a non-root user)',
20
+ severity: 'warn',
21
+ },
22
+ {
23
+ pattern: /^\+(?!\+).*\b(?:curl|wget)\b[^|]*\|\s*(?:sudo\s+)?(?:sh|bash)\b/i,
24
+ label: 'piping a download straight to a shell (curl/wget … | sh)',
25
+ severity: 'warn',
26
+ },
27
+ {
28
+ pattern: /^\+(?!\+)\s*ADD\s+https?:\/\//i,
29
+ label: 'ADD from a URL (prefer COPY, or verify a checksum)',
30
+ severity: 'warn',
31
+ },
32
+ {
33
+ pattern: /^\+(?!\+)\s*(?:ENV|ARG)\s+\w*(?:PASSWORD|SECRET|TOKEN|API_?KEY|ACCESS_KEY)\w*\s*[=\s]/i,
34
+ label: 'secret baked into ENV/ARG (use build secrets / runtime injection)',
35
+ severity: 'warn',
36
+ },
37
+ {
38
+ pattern: /^\+(?!\+).*(?:--no-check-certificate|--insecure)\b/,
39
+ label: 'insecure download flag (--insecure / --no-check-certificate)',
40
+ severity: 'warn',
41
+ },
42
+ {
43
+ pattern: /^\+(?!\+)\s*RUN\s+.*\bsudo\b/,
44
+ label: 'sudo inside RUN (unnecessary and risky in a container build)',
45
+ severity: 'warn',
46
+ },
47
+ ];
48
+ function scanDiff(stdout) {
49
+ const errors = {};
50
+ const warnings = {};
51
+ for (const line of stdout.split('\n')) {
52
+ if (line.includes('pr-checkmate-ignore'))
53
+ continue;
54
+ for (const { pattern, label, severity } of DOCKER_PATTERNS) {
55
+ if (pattern.test(line)) {
56
+ const bucket = severity === 'error' ? errors : warnings;
57
+ (bucket[label] ?? (bucket[label] = [])).push(line.slice(1).trim().substring(0, 120));
58
+ }
59
+ }
60
+ }
61
+ return { errors, warnings };
62
+ }
63
+ exports.dockerfileSecurityCheck = {
64
+ name: 'Dockerfile Security',
65
+ phase: 'informational',
66
+ applies(ctx) {
67
+ return ctx.config.dockerfileSecurity?.enabled !== false && Boolean(ctx.baseSha);
68
+ },
69
+ async run(ctx) {
70
+ const files = await (0, targets_1.resolveTargetFiles)(ctx, ['*']);
71
+ if (files === null)
72
+ return (0, types_1.skip)('git unavailable');
73
+ const dockerfiles = files.filter(f => (0, utils_1.matchesAnyGlob)(f, DOCKERFILE_GLOBS));
74
+ if (dockerfiles.length === 0)
75
+ return (0, types_1.pass)();
76
+ try {
77
+ const { stdout } = await (0, execa_1.execa)('git', ['diff', '--unified=0', ctx.baseSha, ctx.headSha ?? 'HEAD', '--', ...dockerfiles], { cwd: ctx.cwd });
78
+ const { errors, warnings } = scanDiff(stdout);
79
+ return (0, findings_1.reportSeverityBuckets)(errors, warnings, {
80
+ prefix: 'Dockerfile Security',
81
+ errorSummary: n => `${n} issue(s)`,
82
+ warningSummary: n => `${n} warning(s)`,
83
+ sampleLimit: 3,
84
+ ignore: ctx.config.dockerfileSecurity?.ignore,
85
+ });
86
+ }
87
+ catch (err) {
88
+ utils_1.logger.warn(`[Dockerfile Security]: ⚠️ Could not analyze diff\n${err}`);
89
+ return (0, types_1.skip)('diff unavailable');
90
+ }
91
+ },
92
+ };
@@ -0,0 +1,2 @@
1
+ import { Check } from '../../types';
2
+ export declare const migrationSafetyCheck: Check;
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.migrationSafetyCheck = void 0;
4
+ const utils_1 = require("../../../utils");
5
+ const execa_1 = require("../../../utils/execa");
6
+ const targets_1 = require("../../targets");
7
+ const types_1 = require("../../types");
8
+ const findings_1 = require("../findings");
9
+ /**
10
+ * Paths treated as database migrations. A client overrides via
11
+ * `migrationSafety.paths` (REPLACES this list).
12
+ */
13
+ const DEFAULT_MIGRATION_GLOBS = [
14
+ '**/migrations/**',
15
+ '**/migrate/**',
16
+ '**/db/migrate/**', // Rails
17
+ '**/versions/**', // Alembic
18
+ '**/*.sql',
19
+ ];
20
+ /** Destructive / locking operations, matched on added diff lines. */
21
+ const DDL_PATTERNS = [
22
+ { pattern: /^\+(?!\+).*\bDROP\s+TABLE\b/i, label: 'DROP TABLE — irreversible data loss' },
23
+ { pattern: /^\+(?!\+).*\bDROP\s+COLUMN\b/i, label: 'DROP COLUMN — irreversible data loss' },
24
+ { pattern: /^\+(?!\+).*\bTRUNCATE\b/i, label: 'TRUNCATE — irreversible data loss' },
25
+ {
26
+ pattern: /^\+(?!\+).*\bRENAME\s+(?:COLUMN|TABLE)\b/i,
27
+ label: 'RENAME — breaks code still reading the old name',
28
+ },
29
+ { pattern: /^\+(?!\+).*\bDROP\s+(?:CONSTRAINT|INDEX)\b/i, label: 'DROP CONSTRAINT/INDEX' },
30
+ {
31
+ pattern: /^\+(?!\+).*\bALTER\s+COLUMN\b.*\bSET\s+NOT\s+NULL\b/i,
32
+ label: 'SET NOT NULL — locks the table, rejects existing NULL rows',
33
+ },
34
+ // Alembic (Python) operations.
35
+ { pattern: /^\+(?!\+).*\bop\.drop_column\b/, label: 'op.drop_column — irreversible data loss' },
36
+ { pattern: /^\+(?!\+).*\bop\.drop_table\b/, label: 'op.drop_table — irreversible data loss' },
37
+ { pattern: /^\+(?!\+).*\bop\.drop_constraint\b/, label: 'op.drop_constraint' },
38
+ ];
39
+ const NOT_NULL_NO_DEFAULT = 'ADD COLUMN … NOT NULL without DEFAULT — fails/locks on a non-empty table';
40
+ /**
41
+ * A new NOT NULL column with no DEFAULT: the classic migration that breaks on a
42
+ * populated table. Split out because it's a conjunction (ADD COLUMN + NOT NULL,
43
+ * and NO DEFAULT on the line), not a single keyword.
44
+ */
45
+ function isAddNotNullNoDefault(line) {
46
+ return (/^\+(?!\+)/.test(line) &&
47
+ /\bADD\s+COLUMN\b/i.test(line) &&
48
+ /\bNOT\s+NULL\b/i.test(line) &&
49
+ !/\bDEFAULT\b/i.test(line));
50
+ }
51
+ /** Bucket dangerous added lines by risk label, honouring the opt-out directive. */
52
+ function scan(stdout, findings) {
53
+ for (const line of stdout.split('\n')) {
54
+ if (line.includes('pr-checkmate-ignore'))
55
+ continue;
56
+ for (const { pattern, label } of DDL_PATTERNS) {
57
+ if (pattern.test(line))
58
+ (findings[label] ?? (findings[label] = [])).push(line.slice(1).trim().substring(0, 120));
59
+ }
60
+ if (isAddNotNullNoDefault(line)) {
61
+ (findings[NOT_NULL_NO_DEFAULT] ?? (findings[NOT_NULL_NO_DEFAULT] = [])).push(line.slice(1).trim().substring(0, 120));
62
+ }
63
+ }
64
+ }
65
+ exports.migrationSafetyCheck = {
66
+ name: 'Migration Safety',
67
+ phase: 'informational',
68
+ applies(ctx) {
69
+ return ctx.config.migrationSafety?.enabled !== false && Boolean(ctx.baseSha);
70
+ },
71
+ async run(ctx) {
72
+ // Not sourcePath-scoped: migrations commonly live outside `src` (db/, alembic/).
73
+ // ignoreDirs still applies via resolveTargetFiles.
74
+ const files = await (0, targets_1.resolveTargetFiles)(ctx, ['*']);
75
+ if (files === null)
76
+ return (0, types_1.skip)('git unavailable');
77
+ const globs = ctx.config.migrationSafety?.paths ?? DEFAULT_MIGRATION_GLOBS;
78
+ const migrationFiles = files.filter(f => (0, utils_1.matchesAnyGlob)(f, globs));
79
+ if (migrationFiles.length === 0)
80
+ return (0, types_1.pass)();
81
+ try {
82
+ const { stdout } = await (0, execa_1.execa)('git', [
83
+ 'diff',
84
+ '--unified=0',
85
+ ctx.baseSha,
86
+ ctx.headSha ?? 'HEAD',
87
+ '--',
88
+ ...migrationFiles,
89
+ ], { cwd: ctx.cwd });
90
+ const findings = {};
91
+ scan(stdout, findings);
92
+ if ((0, findings_1.bucketTotal)(findings) === 0)
93
+ return (0, types_1.pass)();
94
+ const sev = (0, findings_1.severityEmitter)(ctx.config.migrationSafety?.severity, 'warn');
95
+ (0, findings_1.logFindings)(findings, {
96
+ prefix: 'Migration Safety',
97
+ sampleLimit: 2,
98
+ isError: sev.icon === '❌',
99
+ });
100
+ const summary = Object.entries(findings)
101
+ .map(([label, arr]) => `${arr.length}× ${label.split(' — ')[0]}`)
102
+ .join(', ');
103
+ return sev.emit(summary);
104
+ }
105
+ catch (err) {
106
+ utils_1.logger.warn(`[Migration Safety]: ⚠️ Could not analyze diff\n${err}`);
107
+ return (0, types_1.skip)('diff unavailable');
108
+ }
109
+ },
110
+ };
@@ -1,4 +1,4 @@
1
- import { Check } from '../types';
1
+ import { Check } from '../../types';
2
2
  /**
3
3
  * Secret scanning via the bundled gitleaks package. Language-agnostic — runs on
4
4
  * every repo (Node, Python, C++, mobile). Secrets found → fail (blocking).
@@ -1,10 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.securityCheck = void 0;
4
- const config_1 = require("../../config");
5
- const utils_1 = require("../../utils");
6
- const execa_1 = require("../../utils/execa");
7
- const types_1 = require("../types");
4
+ const config_1 = require("../../../config");
5
+ const utils_1 = require("../../../utils");
6
+ const execa_1 = require("../../../utils/execa");
7
+ const types_1 = require("../../types");
8
8
  /**
9
9
  * Secret scanning via the bundled gitleaks package. Language-agnostic — runs on
10
10
  * every repo (Node, Python, C++, mobile). Secrets found → fail (blocking).
@@ -1,2 +1,2 @@
1
- import { Check } from '../types';
1
+ import { Check } from '../../types';
2
2
  export declare const sensitiveFileGuardCheck: Check;
@@ -1,10 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.sensitiveFileGuardCheck = void 0;
4
- const utils_1 = require("../../utils");
5
- const execa_1 = require("../../utils/execa");
6
- const types_1 = require("../types");
7
- const findings_1 = require("./findings");
4
+ const utils_1 = require("../../../utils");
5
+ const execa_1 = require("../../../utils/execa");
6
+ const types_1 = require("../../types");
7
+ const findings_1 = require("../findings");
8
8
  const SENSITIVE_CATEGORIES = [
9
9
  {
10
10
  label: 'Environment files (.env)',
@@ -0,0 +1,2 @@
1
+ import { Check } from '../../types';
2
+ export declare const workflowSecurityCheck: Check;
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.workflowSecurityCheck = void 0;
4
+ const utils_1 = require("../../../utils");
5
+ const types_1 = require("../../types");
6
+ const findings_1 = require("../findings");
7
+ /**
8
+ * GitHub Actions workflow risks, matched on added diff lines of workflow files.
9
+ * These are the high-frequency supply-chain / injection footguns.
10
+ */
11
+ const WORKFLOW_PATTERNS = [
12
+ {
13
+ // Untrusted PR-controlled input interpolated into an expression — the classic
14
+ // `run:` script-injection vector when it reaches a shell.
15
+ pattern: /^\+(?!\+).*\$\{\{\s*github\.event\.(?:issue|pull_request|comment|review|discussion|head_commit|commits)[^}]*\.(?:title|body|head_ref|ref|label|name|email|message|url)/,
16
+ label: 'untrusted github.event input in expression (script injection risk)',
17
+ severity: 'error',
18
+ },
19
+ {
20
+ pattern: /^\+(?!\+).*\$\{\{\s*github\.head_ref\s*\}\}/,
21
+ label: 'github.head_ref interpolation (injection risk — use an env var)',
22
+ severity: 'warn',
23
+ },
24
+ {
25
+ // Third-party action not pinned to a full 40-char commit SHA.
26
+ pattern: /^\+(?!\+).*\buses:\s*['"]?[\w.-]+\/[\w.-]+@(?![0-9a-fA-F]{40}\b)[\w./-]+/,
27
+ label: 'action not pinned to a full commit SHA (supply-chain risk)',
28
+ severity: 'warn',
29
+ },
30
+ {
31
+ pattern: /^\+(?!\+).*\bpull_request_target\b/,
32
+ label: 'pull_request_target trigger (runs with repo secrets on untrusted PRs)',
33
+ severity: 'warn',
34
+ },
35
+ ];
36
+ function scanDiff(stdout) {
37
+ const errors = {};
38
+ const warnings = {};
39
+ for (const line of stdout.split('\n')) {
40
+ for (const { pattern, label, severity } of WORKFLOW_PATTERNS) {
41
+ if (pattern.test(line)) {
42
+ const bucket = severity === 'error' ? errors : warnings;
43
+ (bucket[label] ?? (bucket[label] = [])).push(line.slice(1).trim().substring(0, 120));
44
+ }
45
+ }
46
+ }
47
+ return { errors, warnings };
48
+ }
49
+ exports.workflowSecurityCheck = {
50
+ name: 'Workflow Security',
51
+ phase: 'informational',
52
+ applies(ctx) {
53
+ return ctx.config.workflowSecurity?.enabled !== false && Boolean(ctx.baseSha);
54
+ },
55
+ async run(ctx) {
56
+ try {
57
+ const stdout = await (0, findings_1.diffAddedLines)(ctx, [
58
+ '.github/workflows/*.yml',
59
+ '.github/workflows/*.yaml',
60
+ ]);
61
+ const { errors, warnings } = scanDiff(stdout);
62
+ return (0, findings_1.reportSeverityBuckets)(errors, warnings, {
63
+ prefix: 'Workflow Security',
64
+ errorSummary: n => `${n} issue(s)`,
65
+ warningSummary: n => `${n} warning(s)`,
66
+ sampleLimit: 3,
67
+ ignore: ctx.config.workflowSecurity?.ignore,
68
+ });
69
+ }
70
+ catch (err) {
71
+ utils_1.logger.warn(`[Workflow Security]: ⚠️ Could not analyze diff\n${err}`);
72
+ return (0, types_1.skip)('diff unavailable');
73
+ }
74
+ },
75
+ };
@@ -30,6 +30,19 @@ async function resolveRef(cwd, ref) {
30
30
  return stdout.trim() || undefined;
31
31
  }
32
32
  async function resolveDiffRange(cwd, opts) {
33
+ // Pre-commit mode: diff HEAD against the current index. `git write-tree`
34
+ // materialises the staged index as a real tree object, so every check that
35
+ // diffs baseSha..headSha sees exactly the staged changes — no per-check code.
36
+ if (opts.staged) {
37
+ try {
38
+ const head = (await (0, execa_1.execa)('git', ['rev-parse', 'HEAD'], { cwd })).stdout.trim();
39
+ const tree = (await (0, execa_1.execa)('git', ['write-tree'], { cwd })).stdout.trim();
40
+ return { baseSha: head, headSha: tree };
41
+ }
42
+ catch {
43
+ return {};
44
+ }
45
+ }
33
46
  if (opts.baseSha || opts.headSha) {
34
47
  return { baseSha: opts.baseSha, headSha: opts.headSha };
35
48
  }
@@ -81,5 +94,6 @@ async function buildContext(opts = {}) {
81
94
  write: opts.write ?? false,
82
95
  baseSha,
83
96
  headSha,
97
+ cache: new Map(),
84
98
  };
85
99
  }