@codacy/verity-cli 0.34.3 → 0.34.5-experimental.7268b07
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -60
- package/bin/verity.js +11967 -9643
- package/data/skills/verity-analyze/SKILL.md +2 -2
- package/data/skills/verity-init/SKILL.md +162 -139
- package/data/skills/verity-setup/SKILL.md +35 -367
- package/data/skills/verity-setup/patterns-reference.yaml +4 -330
- package/data/skills/verity-setup/standard-template.yaml +2 -9
- package/package.json +2 -9
- package/data/skills/verity-setup/validate-patterns.mjs +0 -345
|
@@ -1,345 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Derive and validate codacy-analysis patternIds against the pattern catalogue the
|
|
4
|
-
* installed @codacy/analysis-cli tool adapters actually define.
|
|
5
|
-
*
|
|
6
|
-
* WHY THIS EXISTS
|
|
7
|
-
* ---------------
|
|
8
|
-
* A wrong patternId fails SILENTLY. Each adapter builds `enabledPatterns` from the
|
|
9
|
-
* config and then either skips work entirely or filters every result away:
|
|
10
|
-
*
|
|
11
|
-
* runVulnScan = enabledPatterns.size === 0 || vulnPatternIds.some(id => enabledPatterns.has(id))
|
|
12
|
-
*
|
|
13
|
-
* So a tool with an unrecognised pattern list reports "ready", routes files, and
|
|
14
|
-
* returns 0 issues — indistinguishable from clean code. Worse, `size === 0` means
|
|
15
|
-
* "run ALL defaults", so an empty list works while a wrong list silently disables
|
|
16
|
-
* the tool. Verity shipped `trivy_vuln`, bare `no-eval`, and bare `F401` for months;
|
|
17
|
-
* ESLint, Semgrep, Trivy and Ruff were all dead and nothing surfaced it.
|
|
18
|
-
*
|
|
19
|
-
* The ID format is `<toolId>_<ruleId>`, with `/` replaced by `_` in namespaced
|
|
20
|
-
* rules (`@typescript-eslint/no-explicit-any` → `ESLint9_@typescript-eslint_no-explicit-any`).
|
|
21
|
-
*
|
|
22
|
-
* MODES
|
|
23
|
-
* validate [config] Check every patternId in the config resolves.
|
|
24
|
-
* Exit 1 if any does not. Default: .codacy/codacy.config.json
|
|
25
|
-
* --list <toolId> [regex] Print the adapter's real pattern IDs, one per line.
|
|
26
|
-
* --emit <toolId> [regex] Print a ready-to-paste JSON "patterns" array.
|
|
27
|
-
*
|
|
28
|
-
* Use --emit to GENERATE config blocks so IDs are never hand-authored, e.g. Trivy:
|
|
29
|
-
* node validate-patterns.mjs --emit Trivy '^Trivy_(secret|malicious_packages|vulnerability_(critical|high))$'
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
import { readFileSync, readdirSync, existsSync, realpathSync, statSync, writeSync } from 'node:fs'
|
|
33
|
-
import { join, dirname, delimiter } from 'node:path'
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Locate the @codacy adapter packages shipped with the installed analysis CLI.
|
|
37
|
-
* Resolved without shelling out, so this works on Windows as well as POSIX.
|
|
38
|
-
*/
|
|
39
|
-
function findToolsDir() {
|
|
40
|
-
if (process.env.CODACY_TOOLS_DIR) return process.env.CODACY_TOOLS_DIR
|
|
41
|
-
|
|
42
|
-
// Walk PATH ourselves rather than calling `which`/`where`, which differ per platform.
|
|
43
|
-
const exts = process.platform === 'win32'
|
|
44
|
-
? (process.env.PATHEXT ?? '.EXE;.CMD;.BAT').split(';')
|
|
45
|
-
: ['']
|
|
46
|
-
const candidates = []
|
|
47
|
-
for (const dir of (process.env.PATH ?? '').split(delimiter)) {
|
|
48
|
-
if (!dir) continue
|
|
49
|
-
for (const ext of exts) {
|
|
50
|
-
candidates.push(join(dir, 'codacy-analysis' + ext.toLowerCase()))
|
|
51
|
-
if (ext) candidates.push(join(dir, 'codacy-analysis' + ext))
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
for (const candidate of candidates) {
|
|
56
|
-
let resolved
|
|
57
|
-
try {
|
|
58
|
-
if (!statSync(candidate).isFile()) continue
|
|
59
|
-
resolved = realpathSync(candidate)
|
|
60
|
-
} catch {
|
|
61
|
-
continue
|
|
62
|
-
}
|
|
63
|
-
// resolved is <pkg>/dist/index.js (POSIX symlink) or a shim dir (Windows).
|
|
64
|
-
// Walk up looking for node_modules/@codacy.
|
|
65
|
-
let dir = dirname(resolved)
|
|
66
|
-
for (let i = 0; i < 6; i++) {
|
|
67
|
-
const tools = join(dir, 'node_modules', '@codacy')
|
|
68
|
-
if (existsSync(tools)) return tools
|
|
69
|
-
const parent = dirname(dir)
|
|
70
|
-
if (parent === dir) break
|
|
71
|
-
dir = parent
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return null
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Harvest the pattern IDs an adapter defines. Adapters ship as bundled JS with the
|
|
79
|
-
* pattern catalogue inlined, so we scan for `<toolId>_<rule>` literals. This is a
|
|
80
|
-
* heuristic over a build artifact, not a public API — it can only ever produce false
|
|
81
|
-
* ALARMS (an ID we fail to find), never false confidence, which is the safe direction.
|
|
82
|
-
*/
|
|
83
|
-
/** The characters a pattern id may contain after its `<toolId>_` prefix. */
|
|
84
|
-
const ID_CHAR = /[A-Za-z0-9_@./-]/
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Scan for `<toolId>_<rule>` literals WITHOUT building a regex from `toolId`.
|
|
88
|
-
*
|
|
89
|
-
* The previous form compiled `new RegExp(`${escaped}_[...]+`)` per call. Escaping
|
|
90
|
-
* made it correct, but a RegExp built from a runtime value is a ReDoS surface by
|
|
91
|
-
* construction and reads as one to any reviewer or scanner. An indexOf scan has
|
|
92
|
-
* neither problem, needs no escaping to be right, and is what the code was always
|
|
93
|
-
* expressing: find the prefix, then take the id characters that follow it.
|
|
94
|
-
*/
|
|
95
|
-
function harvestIds(toolsDir, toolId) {
|
|
96
|
-
const ids = new Set()
|
|
97
|
-
const prefix = toolId + '_'
|
|
98
|
-
const scan = (text) => {
|
|
99
|
-
let i = text.indexOf(prefix)
|
|
100
|
-
while (i !== -1) {
|
|
101
|
-
let end = i + prefix.length
|
|
102
|
-
while (end < text.length && ID_CHAR.test(text[end])) end++
|
|
103
|
-
// A bare prefix with nothing after it is not an id.
|
|
104
|
-
if (end > i + prefix.length) ids.add(text.slice(i, end))
|
|
105
|
-
i = text.indexOf(prefix, end > i ? end : i + 1)
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
for (const pkg of readdirSync(toolsDir)) {
|
|
109
|
-
if (!pkg.startsWith('tools-')) continue
|
|
110
|
-
const distDir = join(toolsDir, pkg, 'dist')
|
|
111
|
-
if (!existsSync(distDir)) continue
|
|
112
|
-
for (const file of readdirSync(distDir)) {
|
|
113
|
-
if (!file.endsWith('.js')) continue
|
|
114
|
-
scan(readFileSync(join(distDir, file), 'utf-8'))
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return ids
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const toolsDir = findToolsDir()
|
|
121
|
-
if (!toolsDir) {
|
|
122
|
-
console.error('SKIP: could not locate @codacy tool adapters.')
|
|
123
|
-
console.error(' Install the CLI (npm i -g @codacy/analysis-cli) or set CODACY_TOOLS_DIR.')
|
|
124
|
-
process.exit(1)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Write to fd 1 synchronously, looping until every byte is gone.
|
|
129
|
-
*
|
|
130
|
-
* `console.log` / `process.stdout.write` are ASYNCHRONOUS when stdout is a pipe,
|
|
131
|
-
* and this script exits in the same tick — so the output was cut at whatever the
|
|
132
|
-
* pipe accepted. A write callback does not help either: this is a top-level
|
|
133
|
-
* block, so execution falls through to the validate path and exits 1 before the
|
|
134
|
-
* callback can run (observed: 65536 bytes, status 1).
|
|
135
|
-
*
|
|
136
|
-
* The loop matters. A single writeSync to a pipe may report a SHORT write, and
|
|
137
|
-
* dropping the remainder would reintroduce the same bug in a quieter form.
|
|
138
|
-
*/
|
|
139
|
-
function writeStdoutSync(text) {
|
|
140
|
-
const buf = Buffer.from(text, 'utf8')
|
|
141
|
-
let off = 0
|
|
142
|
-
while (off < buf.length) {
|
|
143
|
-
try {
|
|
144
|
-
off += writeSync(1, buf, off, buf.length - off)
|
|
145
|
-
} catch (err) {
|
|
146
|
-
if (err.code === 'EAGAIN') continue // non-blocking pipe not ready; retry
|
|
147
|
-
if (err.code === 'EPIPE') return // consumer went away (e.g. `| head`)
|
|
148
|
-
throw err
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const argv = process.argv.slice(2)
|
|
154
|
-
const mode = argv[0] === '--list' || argv[0] === '--emit' ? argv[0] : 'validate'
|
|
155
|
-
|
|
156
|
-
// ---------------------------------------------------------------------------
|
|
157
|
-
// --list / --emit : derive IDs straight from adapter metadata
|
|
158
|
-
// ---------------------------------------------------------------------------
|
|
159
|
-
if (mode !== 'validate') {
|
|
160
|
-
const toolId = argv[1]
|
|
161
|
-
if (!toolId) {
|
|
162
|
-
console.error(`Usage: validate-patterns.mjs ${mode} <toolId> [regex]`)
|
|
163
|
-
process.exit(1)
|
|
164
|
-
}
|
|
165
|
-
// An invalid pattern here is a typo in a hand-typed argument, not an error worth a
|
|
166
|
-
// stack trace — report it with the offending input so it can be fixed at a glance.
|
|
167
|
-
let filter = null
|
|
168
|
-
if (argv[2]) {
|
|
169
|
-
try {
|
|
170
|
-
// The filter IS a regex — that is the documented interface
|
|
171
|
-
// (`--list Trivy '^Trivy_secret$'`) and two tests pin the behaviour, so it
|
|
172
|
-
// cannot become a substring match. Unlike the harvest above, there is no
|
|
173
|
-
// way to express this without compiling a runtime value.
|
|
174
|
-
//
|
|
175
|
-
// It is also not a security boundary: this is a local developer CLI, the
|
|
176
|
-
// pattern comes from the operator's own shell, and the only thing a
|
|
177
|
-
// catastrophic regex can stall is the operator's own terminal. Nothing
|
|
178
|
-
// untrusted reaches this line — `argv[2]` is typed by the person running it.
|
|
179
|
-
// Suppressions for both engines Codacy may raise this under. The ESLint
|
|
180
|
-
// form alone did not clear it, so the finding comes from the Semgrep side;
|
|
181
|
-
// `nosemgrep` is rule-agnostic, which is deliberate here — pinning a rule id
|
|
182
|
-
// guessed from a message string is how the first attempt failed.
|
|
183
|
-
// eslint-disable-next-line security/detect-non-literal-regexp -- operator-supplied filter, local CLI, self-inflicted at worst
|
|
184
|
-
// nosemgrep: operator-supplied filter on a local CLI; see the reasoning above
|
|
185
|
-
filter = new RegExp(argv[2])
|
|
186
|
-
} catch (err) {
|
|
187
|
-
console.error(`Invalid regex: ${argv[2]}`)
|
|
188
|
-
console.error(` ${err.message}`)
|
|
189
|
-
console.error(` Note: quote the argument so the shell does not expand it, e.g. '^Trivy_secret$'`)
|
|
190
|
-
process.exit(1)
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
const ids = [...harvestIds(toolsDir, toolId)].sort().filter((id) => !filter || filter.test(id))
|
|
194
|
-
if (ids.length === 0) {
|
|
195
|
-
console.error(`No pattern IDs found for toolId "${toolId}"${filter ? ' matching ' + argv[2] : ''}.`)
|
|
196
|
-
console.error('Check the toolId spelling (case-sensitive: ESLint9, Semgrep, Trivy, Ruff, shellcheck).')
|
|
197
|
-
process.exit(1)
|
|
198
|
-
}
|
|
199
|
-
// ⚠ NEVER `process.exit()` IN THE SAME TICK AS A WRITE TO STDOUT.
|
|
200
|
-
//
|
|
201
|
-
// When stdout is a PIPE, Node's writes are asynchronous. `process.exit()`
|
|
202
|
-
// terminates before the buffer drains, truncating output at whatever the pipe
|
|
203
|
-
// accepted — about 64 KB. Redirecting to a FILE hides it completely, because
|
|
204
|
-
// file writes are synchronous. So it looks fine every time you check by hand,
|
|
205
|
-
// and is broken for every caller that reads the output programmatically.
|
|
206
|
-
//
|
|
207
|
-
// Measured on the ESLint9 catalogue (2936 ids), five runs each:
|
|
208
|
-
// node ... > file -> 2936, 2936, 2936, 2936, 2936 (whole)
|
|
209
|
-
// node ... | cat -> 1812, 1812, 1812, 1812, 1812 (cut at the buffer)
|
|
210
|
-
//
|
|
211
|
-
// The harvest is deterministic. Only the DELIVERY was not, and it cost a real
|
|
212
|
-
// misdiagnosis: `ESLint9_use-isnan` sorts at line 2608, past the cut, so every
|
|
213
|
-
// consumer reading through a pipe — including the review that flagged it —
|
|
214
|
-
// concluded a perfectly valid pattern id did not exist.
|
|
215
|
-
//
|
|
216
|
-
// `process.exitCode = 0` alone is NOT enough here: this is a top-level block,
|
|
217
|
-
// so execution would fall through into the validate path below and exit there,
|
|
218
|
-
// truncating exactly as before. The write callback fires once the data has been
|
|
219
|
-
// handed to the OS, which is the point at which exiting is safe.
|
|
220
|
-
const out =
|
|
221
|
-
mode === '--list'
|
|
222
|
-
? ids.join('\n')
|
|
223
|
-
: JSON.stringify(ids.map((patternId) => ({ patternId })), null, 2)
|
|
224
|
-
writeStdoutSync(out + '\n')
|
|
225
|
-
process.exit(0)
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// ---------------------------------------------------------------------------
|
|
229
|
-
// validate : every patternId in the config must resolve
|
|
230
|
-
// ---------------------------------------------------------------------------
|
|
231
|
-
const configPath = argv[0] ?? '.codacy/codacy.config.json'
|
|
232
|
-
|
|
233
|
-
if (!existsSync(configPath)) {
|
|
234
|
-
console.error(`SKIP: ${configPath} not found — write the config first.`)
|
|
235
|
-
process.exit(1)
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
let config
|
|
239
|
-
try {
|
|
240
|
-
config = JSON.parse(readFileSync(configPath, 'utf-8'))
|
|
241
|
-
} catch (err) {
|
|
242
|
-
console.error(`FAIL: could not parse ${configPath}`)
|
|
243
|
-
console.error(` ${err.message}`)
|
|
244
|
-
process.exit(1)
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const tools = config?.tools ?? []
|
|
248
|
-
if (!Array.isArray(tools)) {
|
|
249
|
-
console.error(`FAIL: ${configPath} has a "tools" field that is not an array.`)
|
|
250
|
-
process.exit(1)
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (tools.length === 0) {
|
|
254
|
-
console.log('OK: "tools": [] — every tool runs its default pattern set. Nothing to validate.')
|
|
255
|
-
process.exit(0)
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
let invalidTotal = 0
|
|
259
|
-
let emptyTotal = 0
|
|
260
|
-
let unverifiableTotal = 0
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Reject a malformed tools[] entry with a located, actionable message. A generator
|
|
264
|
-
* bug or a half-written file must fail closed here rather than throw somewhere
|
|
265
|
-
* further down, where the stack trace would say nothing about which entry is bad.
|
|
266
|
-
*/
|
|
267
|
-
function readToolEntry(tool, index) {
|
|
268
|
-
const at = `tools[${index}]`
|
|
269
|
-
if (tool === null || typeof tool !== 'object' || Array.isArray(tool)) {
|
|
270
|
-
console.error(`FAIL: ${at} is not an object.`)
|
|
271
|
-
process.exit(1)
|
|
272
|
-
}
|
|
273
|
-
const { toolId, patterns } = tool
|
|
274
|
-
if (typeof toolId !== 'string' || toolId.trim() === '') {
|
|
275
|
-
console.error(`FAIL: ${at} has no valid "toolId" (expected a non-empty string).`)
|
|
276
|
-
process.exit(1)
|
|
277
|
-
}
|
|
278
|
-
if (patterns !== undefined && !Array.isArray(patterns)) {
|
|
279
|
-
console.error(`FAIL: ${toolId}: "patterns" must be an array, or omitted for tool defaults.`)
|
|
280
|
-
process.exit(1)
|
|
281
|
-
}
|
|
282
|
-
const patternIds = []
|
|
283
|
-
for (const [i, entry] of (patterns ?? []).entries()) {
|
|
284
|
-
if (entry === null || typeof entry !== 'object' || typeof entry.patternId !== 'string') {
|
|
285
|
-
console.error(
|
|
286
|
-
`FAIL: ${toolId}: patterns[${i}] must be an object with a string "patternId".`,
|
|
287
|
-
)
|
|
288
|
-
process.exit(1)
|
|
289
|
-
}
|
|
290
|
-
patternIds.push(entry.patternId)
|
|
291
|
-
}
|
|
292
|
-
return { toolId, patternIds }
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
for (const [index, rawTool] of tools.entries()) {
|
|
296
|
-
const { toolId, patternIds } = readToolEntry(rawTool, index)
|
|
297
|
-
if (patternIds.length === 0) {
|
|
298
|
-
console.log(` ${toolId}: patterns: [] → all defaults enabled (valid, but noisy)`)
|
|
299
|
-
emptyTotal++
|
|
300
|
-
continue
|
|
301
|
-
}
|
|
302
|
-
const valid = harvestIds(toolsDir, toolId)
|
|
303
|
-
// FAIL CLOSED. "Cannot verify" is not "verified". This whole script exists because
|
|
304
|
-
// a silently-disabled tool looks identical to clean code, so reporting PASS on an
|
|
305
|
-
// unverifiable tool would reproduce the exact failure it is meant to catch.
|
|
306
|
-
if (valid.size === 0) {
|
|
307
|
-
unverifiableTotal++
|
|
308
|
-
console.log(
|
|
309
|
-
` ${toolId}: UNVERIFIABLE — no pattern IDs harvested. Adapter missing, or the` +
|
|
310
|
-
` toolId is misspelled (case-sensitive).`,
|
|
311
|
-
)
|
|
312
|
-
continue
|
|
313
|
-
}
|
|
314
|
-
const bad = patternIds.filter((id) => !valid.has(id))
|
|
315
|
-
if (bad.length === 0) {
|
|
316
|
-
console.log(` ${toolId}: ${patternIds.length}/${patternIds.length} valid`)
|
|
317
|
-
} else {
|
|
318
|
-
invalidTotal += bad.length
|
|
319
|
-
console.log(
|
|
320
|
-
` ${toolId}: ${patternIds.length - bad.length}/${patternIds.length} valid — ${bad.length} INVALID:`,
|
|
321
|
-
)
|
|
322
|
-
for (const id of bad) {
|
|
323
|
-
// Suggest the conventional form so the fix is obvious.
|
|
324
|
-
const guess = `${toolId}_${id.replace(/\//g, '_')}`
|
|
325
|
-
const hint = valid.has(guess)
|
|
326
|
-
? `did you mean "${guess}"?`
|
|
327
|
-
: `no close match — regenerate with: --emit ${toolId}`
|
|
328
|
-
console.log(` ✗ ${id} — ${hint}`)
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
console.log('')
|
|
334
|
-
if (invalidTotal > 0) {
|
|
335
|
-
console.error(`FAIL: ${invalidTotal} patternId(s) do not exist. Those rules are SILENTLY DISABLED.`)
|
|
336
|
-
console.error(' Regenerate the list with --emit <toolId>, or use "patterns": [] for tool defaults.')
|
|
337
|
-
process.exit(1)
|
|
338
|
-
}
|
|
339
|
-
if (unverifiableTotal > 0) {
|
|
340
|
-
console.error(`FAIL: ${unverifiableTotal} tool(s) could not be verified — this is NOT a pass.`)
|
|
341
|
-
console.error(' Check the toolId spelling, reinstall @codacy/analysis-cli, or point')
|
|
342
|
-
console.error(' CODACY_TOOLS_DIR at the directory holding the tools-* adapter packages.')
|
|
343
|
-
process.exit(1)
|
|
344
|
-
}
|
|
345
|
-
console.log(`PASS: every patternId resolves${emptyTotal ? ` (${emptyTotal} tool(s) on defaults)` : ''}.`)
|