@codacy/verity-cli 0.29.1 → 0.29.2-experimental.88e4b5c
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/bin/verity.js
CHANGED
|
@@ -10492,7 +10492,7 @@ var SECURITY_PATTERNS = [
|
|
|
10492
10492
|
/Dockerfile/
|
|
10493
10493
|
];
|
|
10494
10494
|
var PROD_SERVICE_URL = "https://ofcamwrjwrkazqvdchko.supabase.co/functions/v1";
|
|
10495
|
-
var DEFAULT_SERVICE_URL = "".length > 0 ? "" : PROD_SERVICE_URL;
|
|
10495
|
+
var DEFAULT_SERVICE_URL = "https://wukeddyzpijoegyajtnc.supabase.co/functions/v1".length > 0 ? "https://wukeddyzpijoegyajtnc.supabase.co/functions/v1" : PROD_SERVICE_URL;
|
|
10496
10496
|
var GITHUB_CLIENT_ID = "Iv23li88HxAi3ZrbYzWh";
|
|
10497
10497
|
var GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
|
|
10498
10498
|
var GITHUB_ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
|
|
@@ -16693,12 +16693,33 @@ function isCodacyAvailable() {
|
|
|
16693
16693
|
return false;
|
|
16694
16694
|
}
|
|
16695
16695
|
}
|
|
16696
|
-
function
|
|
16697
|
-
|
|
16698
|
-
|
|
16699
|
-
|
|
16700
|
-
|
|
16696
|
+
function buildAnalyzerArgv(files) {
|
|
16697
|
+
return [
|
|
16698
|
+
"analyze",
|
|
16699
|
+
"--install-dependencies",
|
|
16700
|
+
"--files",
|
|
16701
|
+
...files,
|
|
16702
|
+
"--output-format",
|
|
16703
|
+
"json",
|
|
16704
|
+
"--log-level",
|
|
16705
|
+
"error",
|
|
16706
|
+
"--parallel-tools",
|
|
16707
|
+
"3"
|
|
16708
|
+
];
|
|
16709
|
+
}
|
|
16710
|
+
var EMPTY_RESULT = {
|
|
16711
|
+
tool: "@codacy/analysis-cli",
|
|
16712
|
+
findings: [],
|
|
16713
|
+
summary: { total_findings: 0, by_severity: {}, tools_run: [] }
|
|
16714
|
+
};
|
|
16715
|
+
function withFailure(kind, detail) {
|
|
16716
|
+
return {
|
|
16717
|
+
...EMPTY_RESULT,
|
|
16718
|
+
summary: { ...EMPTY_RESULT.summary, failure: { kind, detail: detail.slice(0, 300) } }
|
|
16701
16719
|
};
|
|
16720
|
+
}
|
|
16721
|
+
function runCodacyAnalysis(files) {
|
|
16722
|
+
const empty = EMPTY_RESULT;
|
|
16702
16723
|
if (files.length === 0) return empty;
|
|
16703
16724
|
const existingFiles = files.filter((f) => {
|
|
16704
16725
|
try {
|
|
@@ -16708,22 +16729,30 @@ function runCodacyAnalysis(files) {
|
|
|
16708
16729
|
}
|
|
16709
16730
|
});
|
|
16710
16731
|
if (existingFiles.length === 0) return empty;
|
|
16711
|
-
const
|
|
16712
|
-
|
|
16713
|
-
|
|
16714
|
-
|
|
16715
|
-
|
|
16716
|
-
|
|
16732
|
+
const proc = (0, import_node_child_process7.spawnSync)("codacy-analysis", buildAnalyzerArgv(existingFiles), {
|
|
16733
|
+
encoding: "utf-8",
|
|
16734
|
+
maxBuffer: 10 * 1024 * 1024
|
|
16735
|
+
});
|
|
16736
|
+
return interpretAnalyzerRun({
|
|
16737
|
+
stdout: proc.stdout ?? "",
|
|
16738
|
+
status: proc.status,
|
|
16739
|
+
stderr: proc.stderr ?? "",
|
|
16740
|
+
spawnError: proc.error?.message
|
|
16741
|
+
});
|
|
16742
|
+
}
|
|
16743
|
+
function interpretAnalyzerRun(run) {
|
|
16744
|
+
const output = run.stdout ?? "";
|
|
16745
|
+
if (!output.trim()) {
|
|
16746
|
+
return withFailure(
|
|
16747
|
+
run.spawnError ? "spawn_failed" : "no_output",
|
|
16748
|
+
run.spawnError ?? run.stderr ?? `exit ${run.status}`
|
|
16717
16749
|
);
|
|
16718
|
-
} catch {
|
|
16719
|
-
return empty;
|
|
16720
16750
|
}
|
|
16721
|
-
if (!output.trim()) return empty;
|
|
16722
16751
|
let parsed;
|
|
16723
16752
|
try {
|
|
16724
16753
|
parsed = JSON.parse(output);
|
|
16725
16754
|
} catch {
|
|
16726
|
-
return
|
|
16755
|
+
return withFailure("unparseable_output", output);
|
|
16727
16756
|
}
|
|
16728
16757
|
const issues = parsed.issues ?? [];
|
|
16729
16758
|
const findings = issues.map((issue) => ({
|
|
@@ -16746,6 +16775,22 @@ function runCodacyAnalysis(files) {
|
|
|
16746
16775
|
(parsed.capability?.ready ?? []).map((r) => r.toolId).filter((id) => id != null)
|
|
16747
16776
|
)
|
|
16748
16777
|
);
|
|
16778
|
+
const analyzerErrors = parsed.errors ?? [];
|
|
16779
|
+
let failure;
|
|
16780
|
+
if (findings.length === 0) {
|
|
16781
|
+
if (analyzerErrors.length > 0) {
|
|
16782
|
+
const first = analyzerErrors[0];
|
|
16783
|
+
failure = {
|
|
16784
|
+
kind: first.kind ?? "analyzer_error",
|
|
16785
|
+
detail: (first.message ?? "").slice(0, 300)
|
|
16786
|
+
};
|
|
16787
|
+
} else if (toolsRun.length === 0) {
|
|
16788
|
+
failure = {
|
|
16789
|
+
kind: "no_tools_ran",
|
|
16790
|
+
detail: "The analyzer reported no ready tools. Usually a missing or invalid .codacy/codacy.config.json \u2014 pattern ids that match no rule leave the tool configured out of existence (VRT-105)."
|
|
16791
|
+
};
|
|
16792
|
+
}
|
|
16793
|
+
}
|
|
16749
16794
|
const totalFindings = findings.length;
|
|
16750
16795
|
findings.sort(
|
|
16751
16796
|
(a, b) => (SEVERITY_ORDER[a.severity] ?? 3) - (SEVERITY_ORDER[b.severity] ?? 3)
|
|
@@ -16758,7 +16803,8 @@ function runCodacyAnalysis(files) {
|
|
|
16758
16803
|
total_findings: totalFindings,
|
|
16759
16804
|
by_severity: bySeverity,
|
|
16760
16805
|
tools_run: toolsRun,
|
|
16761
|
-
capped: totalFindings > MAX_FINDINGS
|
|
16806
|
+
capped: totalFindings > MAX_FINDINGS,
|
|
16807
|
+
...failure && { failure }
|
|
16762
16808
|
}
|
|
16763
16809
|
};
|
|
16764
16810
|
}
|
|
@@ -16919,7 +16965,7 @@ function resolveTaskContext(opts) {
|
|
|
16919
16965
|
// src/lib/cli-version.ts
|
|
16920
16966
|
function cliVersion() {
|
|
16921
16967
|
try {
|
|
16922
|
-
return true ? "0.29.
|
|
16968
|
+
return true ? "0.29.2-experimental.88e4b5c" : "dev";
|
|
16923
16969
|
} catch {
|
|
16924
16970
|
return "dev";
|
|
16925
16971
|
}
|
|
@@ -21518,7 +21564,7 @@ function registerTelemetryCommands(program2) {
|
|
|
21518
21564
|
}
|
|
21519
21565
|
|
|
21520
21566
|
// src/cli.ts
|
|
21521
|
-
program.name("verity").description("CLI for Verity quality gate service").version("0.29.
|
|
21567
|
+
program.name("verity").description("CLI for Verity quality gate service").version("0.29.2-experimental.88e4b5c").option("--token <token>", "Override authentication token").option("--service-url <url>", "Override service URL").option("--verbose", "Log HTTP requests/responses to stderr").hook("preAction", async () => {
|
|
21522
21568
|
setUserNamedServiceUrl(program.opts().serviceUrl);
|
|
21523
21569
|
try {
|
|
21524
21570
|
await foldLegacyLocalCredential();
|
|
@@ -160,7 +160,7 @@ Write the file to `.verity/standard.yaml`. Show the user a summary:
|
|
|
160
160
|
> - Security patterns: 7 (3 critical, 4 high)
|
|
161
161
|
> - Custom patterns: 3 (auth-middleware, rls-policy, error-boundary)
|
|
162
162
|
> - Analysis mode: balanced
|
|
163
|
-
> - Tools: ESLint9,
|
|
163
|
+
> - Tools: ESLint9, Trivy
|
|
164
164
|
|
|
165
165
|
Ask for confirmation before proceeding.
|
|
166
166
|
|
|
@@ -179,17 +179,62 @@ Ask for confirmation before proceeding.
|
|
|
179
179
|
|
|
180
180
|
**NEVER use `"patterns": []`.** Always populate patterns with specific patternId entries from `patterns-reference.yaml` section 8 (`curated_patterns`). This is the single most important step for keeping analysis fast and token-efficient.
|
|
181
181
|
|
|
182
|
+
### Pattern ID format — a wrong ID disables the tool SILENTLY
|
|
183
|
+
|
|
184
|
+
Every ID is `<toolId>_<ruleId>`, with `/` replaced by `_`:
|
|
185
|
+
|
|
186
|
+
| Rule as documented upstream | patternId to write |
|
|
187
|
+
|---|---|
|
|
188
|
+
| `no-eval` | `ESLint9_no-eval` |
|
|
189
|
+
| `@typescript-eslint/no-explicit-any` | `ESLint9_@typescript-eslint_no-explicit-any` |
|
|
190
|
+
| `F401` | `Ruff_F401` |
|
|
191
|
+
| `SC2086` | `shellcheck_SC2086` |
|
|
192
|
+
|
|
193
|
+
An unrecognised ID produces **no error**. The adapter builds `enabledPatterns` from
|
|
194
|
+
the config, matches nothing, and returns 0 issues — indistinguishable from clean
|
|
195
|
+
code. Note the perverse asymmetry: `patterns: []` (size 0) means "run all defaults"
|
|
196
|
+
and works, so a *wrong* list is strictly worse than *no* list.
|
|
197
|
+
|
|
198
|
+
Never hand-author or guess an ID. Copy it from `patterns-reference.yaml`, or derive
|
|
199
|
+
it from the installed adapter. Then **always validate** (next section).
|
|
200
|
+
|
|
182
201
|
### How to build the config
|
|
183
202
|
|
|
184
|
-
1.
|
|
185
|
-
|
|
186
|
-
|
|
203
|
+
1. **Generate the umbrella-pattern tools mechanically.** Trivy's catalogue is small,
|
|
204
|
+
fixed, and severity-partitioned, so never copy it by hand — emit it from the
|
|
205
|
+
installed adapter metadata:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
# balanced/thorough: critical + high vulns, secrets, malicious packages
|
|
209
|
+
node <skill-dir>/validate-patterns.mjs --emit Trivy \
|
|
210
|
+
'^Trivy_(secret|malicious_packages|vulnerability_(critical|high))$'
|
|
211
|
+
|
|
212
|
+
# lightweight: drop malicious_packages if you want the minimum
|
|
213
|
+
# thorough: add |medium to the regex to widen severity
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Paste the emitted array straight into the Trivy `patterns` field. `--list Trivy`
|
|
217
|
+
shows all six available patterns if you need to choose a different subset.
|
|
218
|
+
|
|
219
|
+
2. Read `patterns-reference.yaml` → `curated_patterns` section for the rule-level
|
|
220
|
+
tools (ESLint9, Ruff, shellcheck, …), where the curation is an editorial choice
|
|
221
|
+
about signal rather than a complete enumeration. Copy the pattern list for each
|
|
222
|
+
tool selected for this mode. `--list <toolId> <regex>` will confirm any single ID
|
|
223
|
+
or let you derive additions.
|
|
224
|
+
3. For tools with an **existing local config** (e.g., `eslint.config.js`): use `localConfigurationFile` to point to it. When `localConfigurationFile` is set, the tool uses its native config and the `patterns` array is ignored — but you still MUST include at least one pattern in the array (the CLI crashes without it). Use a single placeholder: `[{ "patternId": "ESLint9_no-eval" }]` for ESLint, etc.
|
|
187
225
|
4. For tools **without** a local config: populate the full curated pattern list from `patterns-reference.yaml`
|
|
226
|
+
5. **Validate** (next section). This is not optional — it is the only thing that
|
|
227
|
+
catches a stale or mistyped ID, and the failure mode is silent.
|
|
188
228
|
|
|
189
229
|
### Template — write this file exactly
|
|
190
230
|
|
|
191
231
|
Delete `.codacy/codacy.config.json` and write a new one. This example is for TypeScript **balanced** mode. Adapt the tool list and patterns for the detected languages and mode.
|
|
192
232
|
|
|
233
|
+
> The `Trivy` block below shows what `--emit Trivy` produces for balanced mode — it is
|
|
234
|
+
> illustrative output, **not** a list to copy. Run the command (step 1) and paste its
|
|
235
|
+
> actual result, so the config tracks the installed adapter rather than this document.
|
|
236
|
+
> `patterns-reference.yaml` deliberately stores no Trivy list for the same reason.
|
|
237
|
+
|
|
193
238
|
```json
|
|
194
239
|
{
|
|
195
240
|
"version": 1,
|
|
@@ -202,53 +247,40 @@ Delete `.codacy/codacy.config.json` and write a new one. This example is for Typ
|
|
|
202
247
|
"toolId": "ESLint9",
|
|
203
248
|
"localConfigurationFile": "./eslint.config.js",
|
|
204
249
|
"patterns": [
|
|
205
|
-
{ "patternId": "
|
|
206
|
-
{ "patternId": "
|
|
207
|
-
{ "patternId": "
|
|
208
|
-
{ "patternId": "
|
|
209
|
-
{ "patternId": "
|
|
210
|
-
{ "patternId": "
|
|
211
|
-
{ "patternId": "
|
|
212
|
-
{ "patternId": "
|
|
213
|
-
{ "patternId": "
|
|
214
|
-
{ "patternId": "
|
|
215
|
-
{ "patternId": "
|
|
216
|
-
{ "patternId": "
|
|
217
|
-
{ "patternId": "
|
|
218
|
-
{ "patternId": "
|
|
219
|
-
{ "patternId": "
|
|
220
|
-
{ "patternId": "
|
|
221
|
-
{ "patternId": "
|
|
222
|
-
{ "patternId": "@typescript-
|
|
223
|
-
{ "patternId": "@typescript-
|
|
224
|
-
{ "patternId": "@typescript-
|
|
225
|
-
{ "patternId": "@typescript-
|
|
226
|
-
{ "patternId": "@typescript-
|
|
227
|
-
{ "patternId": "
|
|
228
|
-
{ "patternId": "
|
|
229
|
-
{ "patternId": "
|
|
230
|
-
]
|
|
231
|
-
},
|
|
232
|
-
{
|
|
233
|
-
"toolId": "Semgrep",
|
|
234
|
-
"patterns": [
|
|
235
|
-
{ "patternId": "javascript.lang.security.audit.sqli.node-sequelize-sqli" },
|
|
236
|
-
{ "patternId": "javascript.lang.security.audit.sqli.node-knex-sqli" },
|
|
237
|
-
{ "patternId": "typescript.lang.security.audit.sqli.node-sequelize-sqli" },
|
|
238
|
-
{ "patternId": "javascript.lang.security.audit.command-injection" },
|
|
239
|
-
{ "patternId": "javascript.lang.security.audit.unsafe-html" },
|
|
240
|
-
{ "patternId": "generic.secrets.security.detected-generic-api-key" },
|
|
241
|
-
{ "patternId": "javascript.express.security.audit.express-jwt-not-revoked" },
|
|
242
|
-
{ "patternId": "javascript.jsonwebtoken.security.jwt-hardcode" }
|
|
250
|
+
{ "patternId": "ESLint9_no-eval" },
|
|
251
|
+
{ "patternId": "ESLint9_no-implied-eval" },
|
|
252
|
+
{ "patternId": "ESLint9_no-new-func" },
|
|
253
|
+
{ "patternId": "ESLint9_no-script-url" },
|
|
254
|
+
{ "patternId": "ESLint9_no-unused-vars" },
|
|
255
|
+
{ "patternId": "ESLint9_no-undef" },
|
|
256
|
+
{ "patternId": "ESLint9_no-unreachable" },
|
|
257
|
+
{ "patternId": "ESLint9_no-constant-condition" },
|
|
258
|
+
{ "patternId": "ESLint9_no-dupe-keys" },
|
|
259
|
+
{ "patternId": "ESLint9_no-duplicate-case" },
|
|
260
|
+
{ "patternId": "ESLint9_no-fallthrough" },
|
|
261
|
+
{ "patternId": "ESLint9_no-self-assign" },
|
|
262
|
+
{ "patternId": "ESLint9_no-self-compare" },
|
|
263
|
+
{ "patternId": "ESLint9_use-isnan" },
|
|
264
|
+
{ "patternId": "ESLint9_valid-typeof" },
|
|
265
|
+
{ "patternId": "ESLint9_no-loss-of-precision" },
|
|
266
|
+
{ "patternId": "ESLint9_no-unsafe-optional-chaining" },
|
|
267
|
+
{ "patternId": "ESLint9_@typescript-eslint_no-explicit-any" },
|
|
268
|
+
{ "patternId": "ESLint9_@typescript-eslint_no-unused-vars" },
|
|
269
|
+
{ "patternId": "ESLint9_@typescript-eslint_no-unsafe-assignment" },
|
|
270
|
+
{ "patternId": "ESLint9_@typescript-eslint_no-unsafe-call" },
|
|
271
|
+
{ "patternId": "ESLint9_@typescript-eslint_no-unsafe-return" },
|
|
272
|
+
{ "patternId": "ESLint9_eqeqeq" },
|
|
273
|
+
{ "patternId": "ESLint9_no-var" },
|
|
274
|
+
{ "patternId": "ESLint9_prefer-const" }
|
|
243
275
|
]
|
|
244
276
|
},
|
|
245
277
|
{
|
|
246
278
|
"toolId": "Trivy",
|
|
247
279
|
"patterns": [
|
|
248
|
-
{ "patternId": "
|
|
249
|
-
{ "patternId": "
|
|
250
|
-
{ "patternId": "
|
|
251
|
-
{ "patternId": "
|
|
280
|
+
{ "patternId": "Trivy_vulnerability_critical" },
|
|
281
|
+
{ "patternId": "Trivy_vulnerability_high" },
|
|
282
|
+
{ "patternId": "Trivy_secret" },
|
|
283
|
+
{ "patternId": "Trivy_malicious_packages" }
|
|
252
284
|
]
|
|
253
285
|
}
|
|
254
286
|
],
|
|
@@ -263,20 +295,51 @@ Delete `.codacy/codacy.config.json` and write a new one. This example is for Typ
|
|
|
263
295
|
}
|
|
264
296
|
```
|
|
265
297
|
|
|
298
|
+
### Validate the config — REQUIRED, do not skip
|
|
299
|
+
|
|
300
|
+
Immediately after writing `.codacy/codacy.config.json`, run the validator that ships
|
|
301
|
+
alongside this skill. It checks every `patternId` against the IDs the installed
|
|
302
|
+
adapters actually define, and exits non-zero on any that does not exist:
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
node "$(dirname "$0")/validate-patterns.mjs" # or: node <skill-dir>/validate-patterns.mjs
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Expected output:
|
|
309
|
+
|
|
310
|
+
```
|
|
311
|
+
ESLint9: 25/25 valid
|
|
312
|
+
Trivy: 4/4 valid
|
|
313
|
+
shellcheck: 14/14 valid
|
|
314
|
+
|
|
315
|
+
PASS: every patternId resolves.
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
**If it reports FAIL, fix the IDs before continuing.** Do not report setup as
|
|
319
|
+
complete with a failing validation — every listed pattern is silently disabled, so
|
|
320
|
+
the project would appear to have static analysis while enforcing nothing. The
|
|
321
|
+
validator prints the conventional `<toolId>_<rule>` form as a suggested fix.
|
|
322
|
+
|
|
266
323
|
**Tool selection by language** — ONLY include tools that apply to the detected languages:
|
|
267
324
|
|
|
268
325
|
| Language | Balanced mode tools |
|
|
269
326
|
|----------|-------------------|
|
|
270
|
-
| TypeScript/JavaScript | ESLint9 +
|
|
271
|
-
| Python | Ruff +
|
|
272
|
-
| Go |
|
|
273
|
-
| Java | PMD7 +
|
|
274
|
-
| Kotlin | detekt +
|
|
327
|
+
| TypeScript/JavaScript | ESLint9 + Trivy |
|
|
328
|
+
| Python | Ruff + Trivy |
|
|
329
|
+
| Go | Trivy |
|
|
330
|
+
| Java | PMD7 + Trivy |
|
|
331
|
+
| Kotlin | detekt + Trivy |
|
|
275
332
|
| Shell | shellcheck + Trivy |
|
|
276
333
|
| C/C++ | cppcheck + flawfinder + Trivy |
|
|
277
334
|
| Dockerfile | Hadolint + Trivy |
|
|
278
335
|
|
|
279
|
-
|
|
336
|
+
> **Semgrep is deliberately absent.** Its curated pattern IDs have not been derived
|
|
337
|
+
> yet (see the note in `patterns-reference.yaml`), and it additionally requires the
|
|
338
|
+
> Opengrep binary, which `--install-dependencies` does not fetch. Omit the Semgrep
|
|
339
|
+
> tool block entirely rather than emitting `patterns: []`, which would enable all
|
|
340
|
+
> 2523 default rules. Re-add it once IDs are derived and the validator passes.
|
|
341
|
+
|
|
342
|
+
Use curated patterns from `patterns-reference.yaml` for each tool. For Python, use the Ruff patterns. For TypeScript, use the ESLint9 patterns.
|
|
280
343
|
|
|
281
344
|
**Do NOT include tools for languages not in the project.** For example:
|
|
282
345
|
- Do NOT add ESLint9 to a Python project
|
|
@@ -297,16 +360,34 @@ Use curated patterns from `patterns-reference.yaml` for each tool. For Python, u
|
|
|
297
360
|
| Mode | Tools | Approximate pattern count |
|
|
298
361
|
|------|-------|--------------------------|
|
|
299
362
|
| lightweight | Trivy only | ~4 patterns |
|
|
300
|
-
| balanced | Language linter +
|
|
363
|
+
| balanced | Language linter + Trivy | ~25 + 4 = ~29 patterns |
|
|
301
364
|
| thorough | All applicable + Lizard | ~60-80 patterns |
|
|
302
365
|
|
|
303
366
|
### Verify
|
|
304
367
|
|
|
368
|
+
First confirm every pattern ID resolves (see "Validate the config" above) — a silently
|
|
369
|
+
disabled tool produces the same zero findings as clean code, so a quiet run proves
|
|
370
|
+
nothing on its own:
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
node <skill-dir>/validate-patterns.mjs
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Then confirm the volume is sane:
|
|
377
|
+
|
|
305
378
|
```bash
|
|
306
379
|
codacy-analysis analyze --install-dependencies --files src/some-small-file.ts --log-level error --output-format json 2>/dev/null
|
|
307
380
|
```
|
|
308
381
|
|
|
309
|
-
Expected: single-digit findings per file, not hundreds. If you see 50+ issues from one
|
|
382
|
+
Expected: single-digit findings per file, not hundreds. If you see 50+ issues from one
|
|
383
|
+
file, you likely have `"patterns": []` somewhere — fix it.
|
|
384
|
+
|
|
385
|
+
Check the `capability.ready` array in the JSON to see which tools actually ran.
|
|
386
|
+
`--install-dependencies` handles tools the CLI can install itself (ESLint9 is bundled,
|
|
387
|
+
shellcheck it downloads, Trivy it finds on PATH) but it does **not** fetch every
|
|
388
|
+
binary — Semgrep needs Opengrep installed separately, and at `--log-level error` the
|
|
389
|
+
"Tool unavailable" warning is suppressed, so the omission is invisible. Use
|
|
390
|
+
`--log-level warning` or `--inspect` when a tool seems to report nothing.
|
|
310
391
|
|
|
311
392
|
---
|
|
312
393
|
|
|
@@ -395,7 +395,20 @@ analysis_modes:
|
|
|
395
395
|
# 8. Curated Patterns Per Tool
|
|
396
396
|
# IMPORTANT: patterns: [] means ALL defaults (thousands of rules, massive
|
|
397
397
|
# token consumption and slow runs). Always use these curated lists instead.
|
|
398
|
-
#
|
|
398
|
+
#
|
|
399
|
+
# PATTERN ID FORMAT — get this wrong and the tool fails SILENTLY.
|
|
400
|
+
# Every ID is `<toolId>_<ruleId>`, with `/` replaced by `_`:
|
|
401
|
+
# no-eval → ESLint9_no-eval
|
|
402
|
+
# @typescript-eslint/no-explicit-any → ESLint9_@typescript-eslint_no-explicit-any
|
|
403
|
+
# F401 → Ruff_F401
|
|
404
|
+
# SC2086 → shellcheck_SC2086
|
|
405
|
+
# An unrecognised ID is not an error. The adapter builds `enabledPatterns` from
|
|
406
|
+
# the config, matches nothing, and reports 0 issues — identical to clean code.
|
|
407
|
+
# Perversely, `patterns: []` (size 0) means "all defaults" and DOES work, so a
|
|
408
|
+
# wrong list is strictly worse than no list.
|
|
409
|
+
#
|
|
410
|
+
# ALWAYS run `node validate-patterns.mjs` after writing codacy.config.json.
|
|
411
|
+
# Never hand-author an ID; derive it from the installed adapter metadata.
|
|
399
412
|
# =============================================================================
|
|
400
413
|
|
|
401
414
|
curated_patterns:
|
|
@@ -405,101 +418,116 @@ curated_patterns:
|
|
|
405
418
|
description: "Security + error-prone + TypeScript strictness. Skips style/formatting."
|
|
406
419
|
patterns:
|
|
407
420
|
# Security (MUST have)
|
|
408
|
-
- patternId: "
|
|
409
|
-
- patternId: "
|
|
410
|
-
- patternId: "
|
|
411
|
-
- patternId: "
|
|
421
|
+
- patternId: "ESLint9_no-eval"
|
|
422
|
+
- patternId: "ESLint9_no-implied-eval"
|
|
423
|
+
- patternId: "ESLint9_no-new-func"
|
|
424
|
+
- patternId: "ESLint9_no-script-url"
|
|
412
425
|
# Error-prone (high value)
|
|
413
|
-
- patternId: "
|
|
414
|
-
- patternId: "
|
|
415
|
-
- patternId: "
|
|
416
|
-
- patternId: "
|
|
417
|
-
- patternId: "
|
|
418
|
-
- patternId: "
|
|
419
|
-
- patternId: "
|
|
420
|
-
- patternId: "
|
|
421
|
-
- patternId: "
|
|
422
|
-
- patternId: "
|
|
423
|
-
- patternId: "
|
|
424
|
-
- patternId: "
|
|
425
|
-
- patternId: "
|
|
426
|
+
- patternId: "ESLint9_no-unused-vars"
|
|
427
|
+
- patternId: "ESLint9_no-undef"
|
|
428
|
+
- patternId: "ESLint9_no-unreachable"
|
|
429
|
+
- patternId: "ESLint9_no-constant-condition"
|
|
430
|
+
- patternId: "ESLint9_no-dupe-keys"
|
|
431
|
+
- patternId: "ESLint9_no-duplicate-case"
|
|
432
|
+
- patternId: "ESLint9_no-fallthrough"
|
|
433
|
+
- patternId: "ESLint9_no-self-assign"
|
|
434
|
+
- patternId: "ESLint9_no-self-compare"
|
|
435
|
+
- patternId: "ESLint9_use-isnan"
|
|
436
|
+
- patternId: "ESLint9_valid-typeof"
|
|
437
|
+
- patternId: "ESLint9_no-loss-of-precision"
|
|
438
|
+
- patternId: "ESLint9_no-unsafe-optional-chaining"
|
|
426
439
|
# TypeScript (if applicable)
|
|
427
|
-
- patternId: "@typescript-
|
|
428
|
-
- patternId: "@typescript-
|
|
429
|
-
- patternId: "@typescript-
|
|
430
|
-
- patternId: "@typescript-
|
|
431
|
-
- patternId: "@typescript-
|
|
440
|
+
- patternId: "ESLint9_@typescript-eslint_no-explicit-any"
|
|
441
|
+
- patternId: "ESLint9_@typescript-eslint_no-unused-vars"
|
|
442
|
+
- patternId: "ESLint9_@typescript-eslint_no-unsafe-assignment"
|
|
443
|
+
- patternId: "ESLint9_@typescript-eslint_no-unsafe-call"
|
|
444
|
+
- patternId: "ESLint9_@typescript-eslint_no-unsafe-return"
|
|
432
445
|
# Best practice
|
|
433
|
-
- patternId: "
|
|
434
|
-
- patternId: "
|
|
435
|
-
- patternId: "
|
|
436
|
-
|
|
437
|
-
# --- Semgrep / Opengrep (
|
|
438
|
-
#
|
|
446
|
+
- patternId: "ESLint9_eqeqeq"
|
|
447
|
+
- patternId: "ESLint9_no-var"
|
|
448
|
+
- patternId: "ESLint9_prefer-const"
|
|
449
|
+
|
|
450
|
+
# --- Semgrep / Opengrep (2523 available) — PATTERN LIST NOT YET DERIVED ---
|
|
451
|
+
#
|
|
452
|
+
# DO NOT emit a Semgrep block into codacy.config.json until the IDs below are
|
|
453
|
+
# derived and `validate-patterns.mjs` passes. The 17 IDs previously listed here
|
|
454
|
+
# were invented and matched nothing, so Semgrep silently reported zero findings.
|
|
455
|
+
#
|
|
456
|
+
# Real IDs repeat the trailing segment and carry the tool prefix, e.g.
|
|
457
|
+
# Semgrep_javascript.jsonwebtoken.security.jwt-hardcode.hardcoded-jwt-secret
|
|
458
|
+
# Semgrep_generic.secrets.security.detected-artifactory-token.detected-artifactory-token
|
|
459
|
+
# Derive them from the installed adapter, never by hand:
|
|
460
|
+
# node -e "const fs=require('fs');const d=process.env.CODACY_TOOLS_DIR+'/tools-opengrep-1/dist';\
|
|
461
|
+
# let b='';for(const f of fs.readdirSync(d))if(f.endsWith('.js'))b+=fs.readFileSync(d+'/'+f,'utf8');\
|
|
462
|
+
# console.log([...new Set(b.match(/Semgrep_[A-Za-z0-9_.-]+/g))].sort().join('\n'))"
|
|
463
|
+
#
|
|
464
|
+
# Also note Semgrep needs the Opengrep binary, which `--install-dependencies`
|
|
465
|
+
# does NOT fetch: curl -fsSL https://raw.githubusercontent.com/opengrep/opengrep/main/install.sh | bash
|
|
466
|
+
#
|
|
467
|
+
# Categories to cover when curating (counts of real candidates in parentheses):
|
|
468
|
+
# SQL injection, JS/TS (8) · XSS / unsafe HTML (15) · generic secrets (49)
|
|
469
|
+
# hardcoded JWT secret (4) · Python deserialization / pickle (6)
|
|
470
|
+
# NOTE: no rule matches "command-injection" for JavaScript — the old entry was fiction.
|
|
439
471
|
Semgrep:
|
|
440
472
|
description: "Security-only: injection, XSS, secrets, auth. No style rules."
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
- patternId: "generic.secrets.security.detected-generic-api-key"
|
|
458
|
-
- patternId: "generic.secrets.security.detected-aws-account-id"
|
|
459
|
-
# Deserialization
|
|
460
|
-
- patternId: "python.lang.security.deserialization.avoid-pickle"
|
|
461
|
-
- patternId: "python.lang.security.deserialization.avoid-yaml-load"
|
|
462
|
-
# Auth
|
|
463
|
-
- patternId: "javascript.express.security.audit.express-jwt-not-revoked"
|
|
464
|
-
- patternId: "javascript.jsonwebtoken.security.jwt-hardcode"
|
|
465
|
-
|
|
466
|
-
# --- Trivy (6 umbrella patterns → use all, already minimal) ---
|
|
473
|
+
status: unverified # omit from generated config until derived + validated
|
|
474
|
+
patterns: [] # intentionally empty — see note above, do NOT copy into config
|
|
475
|
+
|
|
476
|
+
# --- Trivy — DERIVED, NOT HAND-MAINTAINED ---
|
|
477
|
+
#
|
|
478
|
+
# Trivy's catalogue is a fixed set of 6 umbrella patterns, so the config block is
|
|
479
|
+
# GENERATED from adapter metadata rather than copied from here. The list below is
|
|
480
|
+
# a cached snapshot for reading convenience only; the generator must emit it:
|
|
481
|
+
#
|
|
482
|
+
# node validate-patterns.mjs --emit Trivy \
|
|
483
|
+
# '^Trivy_(secret|malicious_packages|vulnerability_(critical|high))$'
|
|
484
|
+
#
|
|
485
|
+
# If this snapshot ever disagrees with `--list Trivy`, the adapter wins — the
|
|
486
|
+
# snapshot is stale and should be regenerated. Severity is fixed per pattern, so
|
|
487
|
+
# severity filtering IS pattern selection. There is NO misconfig/config or license
|
|
488
|
+
# pattern; the old `trivy_config`/`trivy_license` entries were fiction.
|
|
467
489
|
Trivy:
|
|
468
|
-
description: "
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
490
|
+
description: "Dependency vulnerabilities, secrets, malicious packages. 6 patterns total."
|
|
491
|
+
source: derived
|
|
492
|
+
# NO pattern list is stored here ON PURPOSE. There is exactly one source of truth
|
|
493
|
+
# — the installed adapter — so there is nothing here to copy, mistype, or let go
|
|
494
|
+
# stale. Run `--list Trivy` to see all six, `--emit Trivy '<regex>'` to generate
|
|
495
|
+
# the config block.
|
|
496
|
+
#
|
|
497
|
+
# Selecting by severity IS selecting patterns (severity is fixed per pattern):
|
|
498
|
+
# lightweight '^Trivy_(secret|vulnerability_critical)$'
|
|
499
|
+
# balanced '^Trivy_(secret|malicious_packages|vulnerability_(critical|high))$'
|
|
500
|
+
# thorough '^Trivy_(secret|malicious_packages|vulnerability_(critical|high|medium))$'
|
|
501
|
+
patterns: derive # sentinel: not a list — see above. Never emit this value.
|
|
474
502
|
|
|
475
503
|
# --- Ruff (773 available → ~20 high-signal) ---
|
|
476
504
|
Ruff:
|
|
477
505
|
description: "Python errors + security. Skips style/formatting (use formatter instead)."
|
|
478
506
|
patterns:
|
|
479
507
|
# Pyflakes — error-prone
|
|
480
|
-
- patternId: "
|
|
481
|
-
- patternId: "
|
|
482
|
-
- patternId: "
|
|
483
|
-
- patternId: "
|
|
508
|
+
- patternId: "Ruff_F401" # unused import
|
|
509
|
+
- patternId: "Ruff_F811" # redefined unused name
|
|
510
|
+
- patternId: "Ruff_F841" # unused variable
|
|
511
|
+
- patternId: "Ruff_F821" # undefined name
|
|
484
512
|
# Bugbear — likely bugs
|
|
485
|
-
- patternId: "
|
|
486
|
-
- patternId: "
|
|
487
|
-
- patternId: "
|
|
513
|
+
- patternId: "Ruff_B006" # mutable default argument
|
|
514
|
+
- patternId: "Ruff_B007" # unused loop variable
|
|
515
|
+
- patternId: "Ruff_B018" # useless expression
|
|
488
516
|
# Security
|
|
489
|
-
- patternId: "
|
|
490
|
-
- patternId: "
|
|
491
|
-
- patternId: "
|
|
492
|
-
- patternId: "
|
|
493
|
-
- patternId: "
|
|
494
|
-
- patternId: "
|
|
495
|
-
- patternId: "
|
|
496
|
-
- patternId: "
|
|
497
|
-
- patternId: "
|
|
498
|
-
- patternId: "
|
|
499
|
-
- patternId: "
|
|
517
|
+
- patternId: "Ruff_S101" # assert used (not for prod)
|
|
518
|
+
- patternId: "Ruff_S102" # exec used
|
|
519
|
+
- patternId: "Ruff_S103" # bad file permissions
|
|
520
|
+
- patternId: "Ruff_S104" # hardcoded bind all interfaces
|
|
521
|
+
- patternId: "Ruff_S105" # hardcoded password string
|
|
522
|
+
- patternId: "Ruff_S106" # hardcoded password argument
|
|
523
|
+
- patternId: "Ruff_S107" # hardcoded password default
|
|
524
|
+
- patternId: "Ruff_S108" # hardcoded temp file
|
|
525
|
+
- patternId: "Ruff_S110" # try-except-pass
|
|
526
|
+
- patternId: "Ruff_S301" # pickle usage
|
|
527
|
+
- patternId: "Ruff_S608" # SQL injection via string formatting
|
|
500
528
|
# Type annotations (if desired)
|
|
501
|
-
- patternId: "
|
|
502
|
-
- patternId: "
|
|
529
|
+
- patternId: "Ruff_ANN001" # missing type annotation for function argument
|
|
530
|
+
- patternId: "Ruff_ANN201" # missing return type annotation
|
|
503
531
|
|
|
504
532
|
# --- ShellCheck (491 available → ~15 high-signal) ---
|
|
505
533
|
shellcheck:
|
|
@@ -0,0 +1,345 @@
|
|
|
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)` : ''}.`)
|