zuplo 7.13.4 → 7.13.5
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/docs/articles/api-lint-breaking-changes.mdx +211 -0
- package/docs/articles/api-lint-ci-cd.mdx +242 -0
- package/docs/articles/api-lint-configuration.mdx +459 -0
- package/docs/articles/api-lint-custom-rules.mdx +222 -0
- package/docs/articles/api-lint-rules.mdx +360 -0
- package/docs/articles/api-lint.mdx +267 -0
- package/docs/cli/lint.mdx +64 -0
- package/docs/cli/lint.partial.mdx +70 -0
- package/docs/dev-portal/zudoku/configuration/api-catalog.md +16 -0
- package/docs/dev-portal/zudoku/configuration/blog.md +217 -0
- package/package.json +5 -5
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Lint Configuration
|
|
3
|
+
sidebar_label: Configuration
|
|
4
|
+
description:
|
|
5
|
+
Configure `zuplo lint` with zuplo.lint.jsonc to pick rulesets, scope rule
|
|
6
|
+
severities, override rules per route, require policies, and detect breaking
|
|
7
|
+
changes.
|
|
8
|
+
applies_to:
|
|
9
|
+
product: gateway
|
|
10
|
+
hosting: all
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
`zuplo lint` reads its settings from a `zuplo.lint.jsonc` file in the root of
|
|
14
|
+
your project. The file is optional: without it, the linter runs the
|
|
15
|
+
`zuplo:recommended` preset and fails only on errors.
|
|
16
|
+
|
|
17
|
+
Start the file with `$schema` and `version`:
|
|
18
|
+
|
|
19
|
+
```jsonc title="zuplo.lint.jsonc"
|
|
20
|
+
{
|
|
21
|
+
"$schema": "https://cdn.zuplo.com/schemas/zuplo-lint.json",
|
|
22
|
+
"version": 2,
|
|
23
|
+
"extends": ["zuplo:recommended"],
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`$schema` gives your editor completion and inline errors for every setting on
|
|
28
|
+
this page. `"version"` is the version of the configuration format; set it to
|
|
29
|
+
`2`.
|
|
30
|
+
|
|
31
|
+
The linter looks for `zuplo.lint.jsonc` first, then `zuplo.lint.json`. To use a
|
|
32
|
+
file somewhere else, pass its path with `--config`. The path is relative to the
|
|
33
|
+
directory you run the command from, not to `--dir`:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx zuplo lint --config ./lint/strict.jsonc
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The file is JSON with comments (JSONC), so you can annotate why a rule is off or
|
|
40
|
+
a path is skipped. Trailing commas are allowed.
|
|
41
|
+
|
|
42
|
+
## Full example
|
|
43
|
+
|
|
44
|
+
```jsonc title="zuplo.lint.jsonc"
|
|
45
|
+
{
|
|
46
|
+
"$schema": "https://cdn.zuplo.com/schemas/zuplo-lint.json",
|
|
47
|
+
"version": 2,
|
|
48
|
+
|
|
49
|
+
// Rulesets to start from, applied in order
|
|
50
|
+
"extends": [
|
|
51
|
+
"zuplo:strict",
|
|
52
|
+
["spectral:owasp", "recommended"],
|
|
53
|
+
"./lint/team-ruleset.json",
|
|
54
|
+
],
|
|
55
|
+
|
|
56
|
+
"rules": {
|
|
57
|
+
// A severity: "error", "warn", "info", "hint", or "off"
|
|
58
|
+
"info-contact": "off",
|
|
59
|
+
|
|
60
|
+
// A severity scoped to some routes
|
|
61
|
+
"operation-description": {
|
|
62
|
+
"severity": "error",
|
|
63
|
+
"paths": ["/v1/**"],
|
|
64
|
+
"excludePaths": ["/v1/internal/**"],
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
// A custom rule: require an API key policy on /v1 routes
|
|
68
|
+
"require-api-key-v1": {
|
|
69
|
+
"message": "v1 routes need an API key",
|
|
70
|
+
"severity": "error",
|
|
71
|
+
"paths": ["/v1/**"],
|
|
72
|
+
"given": "$",
|
|
73
|
+
"then": {
|
|
74
|
+
"function": "zuplo:requirePolicy",
|
|
75
|
+
"functionOptions": { "policyTypes": ["api-key"] },
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
// Change rules for some routes or files; later entries win
|
|
81
|
+
"overrides": [
|
|
82
|
+
{
|
|
83
|
+
"paths": ["/health"],
|
|
84
|
+
"rules": { "zuplo/require-policy": "off", "operation-tags": "off" },
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"files": ["config/legacy.oas.json"],
|
|
88
|
+
"rules": { "operation-description": "warn" },
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
|
|
92
|
+
// The lowest severity that fails the run
|
|
93
|
+
"failOn": "error",
|
|
94
|
+
|
|
95
|
+
// Compare the OpenAPI files with a Git branch
|
|
96
|
+
"breakingChanges": { "enabled": true, "baseline": "origin/main" },
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## How settings combine
|
|
101
|
+
|
|
102
|
+
```txt
|
|
103
|
+
extends (left to right) → rules → overrides (in order) → --fail-on flag
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
1. `extends` builds the starting set of rules. When two rulesets define the same
|
|
107
|
+
rule, the later one wins.
|
|
108
|
+
2. `rules` changes the severity of those rules, limits them to some routes, or
|
|
109
|
+
adds new ones.
|
|
110
|
+
3. `overrides` changes severities for the routes or files they match. When
|
|
111
|
+
several overrides match a result, the last one wins.
|
|
112
|
+
4. `--fail-on` on the command line replaces [`failOn`](#failon) for one run.
|
|
113
|
+
|
|
114
|
+
## Severities
|
|
115
|
+
|
|
116
|
+
Every rule reports at one severity:
|
|
117
|
+
|
|
118
|
+
| Severity | Meaning |
|
|
119
|
+
| --------- | -------------------------------------------------------------------------- |
|
|
120
|
+
| `"error"` | A defect. Fails the run under the default `failOn`. |
|
|
121
|
+
| `"warn"` | A likely problem. Fails the run only when `failOn` is `"warn"`. |
|
|
122
|
+
| `"info"` | Worth knowing. Reported, counted under `info` in the summary, never fails. |
|
|
123
|
+
| `"hint"` | A suggestion. Reported like `info`, never fails. |
|
|
124
|
+
| `"off"` | The rule doesn't run, or doesn't report on the routes it's off for. |
|
|
125
|
+
|
|
126
|
+
## Options
|
|
127
|
+
|
|
128
|
+
### `extends`
|
|
129
|
+
|
|
130
|
+
The rulesets to start from. The default is `["zuplo:recommended"]`. An entry is
|
|
131
|
+
one of these:
|
|
132
|
+
|
|
133
|
+
**A Zuplo preset.** Each preset combines Zuplo's own checks with a reviewed set
|
|
134
|
+
of OpenAPI rules that catch real defects without flooding a typical project.
|
|
135
|
+
|
|
136
|
+
| Preset | Adds |
|
|
137
|
+
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
138
|
+
| `zuplo:recommended` | The core checks: `operationId`, descriptions, tags, path parameters, undefined policies, route labels, policy schemas, duplicate operations, unsafe Markdown, and more. |
|
|
139
|
+
| `zuplo:strict` | Everything in recommended, plus required authentication and rate limiting, CORS on every route, API info, error responses, and documentation completeness. |
|
|
140
|
+
| `zuplo:security` | Everything in recommended, plus required authentication and rate limiting, both as errors. |
|
|
141
|
+
|
|
142
|
+
**A vacuum or Spectral built-in ruleset.** These bring in the full catalog of
|
|
143
|
+
general OpenAPI rules:
|
|
144
|
+
|
|
145
|
+
| Ruleset | Contains |
|
|
146
|
+
| ---------------------------------- | ------------------------- |
|
|
147
|
+
| `vacuum:oas` or `spectral:oas` | General OpenAPI rules. |
|
|
148
|
+
| `vacuum:owasp` or `spectral:owasp` | OWASP API security rules. |
|
|
149
|
+
|
|
150
|
+
On their own, these sets turn on that set's recommended rules. Write the entry
|
|
151
|
+
as a pair to pick a mode instead, as in Spectral:
|
|
152
|
+
|
|
153
|
+
```jsonc
|
|
154
|
+
{
|
|
155
|
+
"version": 2,
|
|
156
|
+
"extends": [
|
|
157
|
+
"zuplo:recommended",
|
|
158
|
+
["spectral:owasp", "all"], // every OWASP rule
|
|
159
|
+
],
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
| Mode | Effect |
|
|
164
|
+
| --------------- | ---------------------------------------------------- |
|
|
165
|
+
| `"recommended"` | The set's recommended rules. The default. |
|
|
166
|
+
| `"all"` | Every rule in the set. |
|
|
167
|
+
| `"off"` | Removes the set's rules that an earlier entry added. |
|
|
168
|
+
|
|
169
|
+
Some general OpenAPI rules don't fit Zuplo projects. For example,
|
|
170
|
+
`oas3-api-servers` reports every spec because Zuplo supplies the servers, and
|
|
171
|
+
`paths-kebab-case` reports URLPattern paths such as `/users/:id`. Turn those off
|
|
172
|
+
in `rules` when you extend `vacuum:oas`, or start from a Zuplo preset, which
|
|
173
|
+
leaves them out.
|
|
174
|
+
|
|
175
|
+
**A local ruleset file.** A `.json` or `.jsonc` file in the Spectral ruleset
|
|
176
|
+
format, with a path relative to `zuplo.lint.jsonc`. Use it to share rules
|
|
177
|
+
between projects. See
|
|
178
|
+
[Custom lint rules](./api-lint-custom-rules.mdx#share-rules-with-a-ruleset-file).
|
|
179
|
+
|
|
180
|
+
Run `npx zuplo lint --list-rules` to see every rule and which rulesets include
|
|
181
|
+
it. The [rules reference](./api-lint-rules.mdx) describes the rules in the Zuplo
|
|
182
|
+
presets.
|
|
183
|
+
|
|
184
|
+
### `rules`
|
|
185
|
+
|
|
186
|
+
Rule settings, keyed by rule ID. A value is either a severity or an object.
|
|
187
|
+
|
|
188
|
+
A severity changes the rule everywhere:
|
|
189
|
+
|
|
190
|
+
```jsonc
|
|
191
|
+
{
|
|
192
|
+
"version": 2,
|
|
193
|
+
"rules": {
|
|
194
|
+
"operation-tags": "off",
|
|
195
|
+
"operation-description": "error",
|
|
196
|
+
},
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
An object changes the severity and limits which routes the rule reports on:
|
|
201
|
+
|
|
202
|
+
```jsonc
|
|
203
|
+
{
|
|
204
|
+
"version": 2,
|
|
205
|
+
"rules": {
|
|
206
|
+
"operation-description": {
|
|
207
|
+
"severity": "error",
|
|
208
|
+
"paths": ["/v1/**"],
|
|
209
|
+
"excludePaths": ["/v1/internal/**"],
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
| Field | Description |
|
|
216
|
+
| -------------- | ---------------------------------------------------------------------------- |
|
|
217
|
+
| `severity` | The rule's severity. Leave it out to keep the severity the rule already has. |
|
|
218
|
+
| `paths` | Report the rule only on these API paths. |
|
|
219
|
+
| `excludePaths` | Don't report the rule on these API paths. |
|
|
220
|
+
|
|
221
|
+
Path patterns work the same everywhere in this file:
|
|
222
|
+
|
|
223
|
+
- A plain path matches that route exactly, as written in the OpenAPI file:
|
|
224
|
+
`"/users/{userId}"`.
|
|
225
|
+
- A path ending in `/**` matches that path and every path beneath it.
|
|
226
|
+
`"/internal/**"` matches `/internal` and `/internal/jobs`, but not
|
|
227
|
+
`/internal2`.
|
|
228
|
+
|
|
229
|
+
A rule with `paths` doesn't report results that aren't tied to a route, such as
|
|
230
|
+
a missing `info.description` or a `policies.json` schema error.
|
|
231
|
+
|
|
232
|
+
An object with `given` and `then` defines a new rule. See
|
|
233
|
+
[Custom lint rules](./api-lint-custom-rules.mdx).
|
|
234
|
+
|
|
235
|
+
A rule ID that no ruleset in `extends` defines stops the run with exit code `2`,
|
|
236
|
+
and the error suggests the closest ID. To configure a rule, extend a ruleset
|
|
237
|
+
that includes it: `"zuplo/require-cors-policy"` is only available when you
|
|
238
|
+
extend `zuplo:strict`.
|
|
239
|
+
|
|
240
|
+
`zuplo/require-policy` is the rule that enforces
|
|
241
|
+
[`requiredPolicies`](#requiredpolicies). Setting it applies the severity to
|
|
242
|
+
every required policy, and `"off"` turns all of them off. Use the base ID here;
|
|
243
|
+
`zuplo/require-policy-warn`, which appears in results, isn't a rule you
|
|
244
|
+
configure.
|
|
245
|
+
|
|
246
|
+
### `overrides`
|
|
247
|
+
|
|
248
|
+
Change rule severities for some routes or OpenAPI files. Use an override when a
|
|
249
|
+
group of routes is deliberately different from the rest of the API: public
|
|
250
|
+
endpoints, a health check, a legacy spec you can't change yet.
|
|
251
|
+
|
|
252
|
+
```jsonc
|
|
253
|
+
{
|
|
254
|
+
"version": 2,
|
|
255
|
+
"overrides": [
|
|
256
|
+
{
|
|
257
|
+
"paths": ["/health", "/status"],
|
|
258
|
+
"rules": { "zuplo/require-policy": "off" },
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
"files": ["config/legacy.oas.json"],
|
|
262
|
+
"rules": {
|
|
263
|
+
"operation-description": "off",
|
|
264
|
+
"operation-tags": "off",
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
| Field | Description |
|
|
272
|
+
| ------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
273
|
+
| `paths` | API paths the override applies to. |
|
|
274
|
+
| `files` | OpenAPI files the override applies to, relative to the project directory. `*` matches within one folder: `config/*.oas.json`. |
|
|
275
|
+
| `rules` | Severities to apply, keyed by rule ID. |
|
|
276
|
+
|
|
277
|
+
An override needs `paths`, `files`, or both. With both, a result must match
|
|
278
|
+
both.
|
|
279
|
+
|
|
280
|
+
An override can also turn a rule on. With `"operation-tags": "off"` in `rules`
|
|
281
|
+
and `{ "paths": ["/v1/**"], "rules": { "operation-tags": "warn" } }` in
|
|
282
|
+
`overrides`, the rule reports only on `/v1` routes. An override works within a
|
|
283
|
+
rule's own `paths` and `excludePaths`, and can't turn a rule on for a path in
|
|
284
|
+
`ignorePaths`.
|
|
285
|
+
|
|
286
|
+
### `failOn`
|
|
287
|
+
|
|
288
|
+
The lowest severity that fails the run with exit code `1`.
|
|
289
|
+
|
|
290
|
+
| Value | The run fails when |
|
|
291
|
+
| --------- | ----------------------------------------------------------------------- |
|
|
292
|
+
| `"error"` | There is at least one error, or a breaking change reported as an error. |
|
|
293
|
+
| `"warn"` | There is at least one error, warning, or breaking change. |
|
|
294
|
+
| `"none"` | Never. The linter reports results and exits with `0`. |
|
|
295
|
+
|
|
296
|
+
The default is `"error"`. `info` and `hint` results never fail a run. Any other
|
|
297
|
+
value stops the run with exit code `2`. The `--fail-on` flag overrides this
|
|
298
|
+
value for a single run.
|
|
299
|
+
|
|
300
|
+
### `ignorePaths`
|
|
301
|
+
|
|
302
|
+
API paths whose route results the linter drops for every rule. An override can't
|
|
303
|
+
turn a rule back on for an ignored path.
|
|
304
|
+
|
|
305
|
+
```jsonc
|
|
306
|
+
{
|
|
307
|
+
"version": 2,
|
|
308
|
+
"ignorePaths": ["/health", "/internal/**"],
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
`ignorePaths` also applies to
|
|
313
|
+
[automatic fixes](./api-lint.mdx#fix-problems-automatically). Results that
|
|
314
|
+
aren't tied to a route still appear: `policies.json` schema results, `info`
|
|
315
|
+
rules, and [breaking changes](./api-lint-breaking-changes.mdx). To turn off only
|
|
316
|
+
some rules for a route, use [`overrides`](#overrides).
|
|
317
|
+
|
|
318
|
+
### `include`
|
|
319
|
+
|
|
320
|
+
The OpenAPI files to lint, relative to the project directory. By default, the
|
|
321
|
+
linter lints every file directly in `config/` that ends in `.oas.json`; it
|
|
322
|
+
doesn't look in subdirectories.
|
|
323
|
+
|
|
324
|
+
```jsonc
|
|
325
|
+
{
|
|
326
|
+
"version": 2,
|
|
327
|
+
"include": ["config/public-api.oas.json"],
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Every entry must end in `.oas.json`, must be a relative path, and must stay
|
|
332
|
+
inside the project directory. Glob patterns aren't supported, so list each file.
|
|
333
|
+
A file that doesn't exist fails the run with exit code `2`.
|
|
334
|
+
|
|
335
|
+
The CLI still requires at least one `.oas.json` file directly in `config/`, so
|
|
336
|
+
use `include` to narrow the files that are linted, not to point at files
|
|
337
|
+
elsewhere.
|
|
338
|
+
|
|
339
|
+
`include` controls which OpenAPI files are linted. The linter always validates
|
|
340
|
+
`config/policies.json` when it exists.
|
|
341
|
+
|
|
342
|
+
### `requiredPolicies`
|
|
343
|
+
|
|
344
|
+
Policies that every route must include. The `zuplo:strict` and `zuplo:security`
|
|
345
|
+
presets each define a default set; `requiredPolicies` replaces those defaults
|
|
346
|
+
with your own list.
|
|
347
|
+
|
|
348
|
+
```jsonc
|
|
349
|
+
{
|
|
350
|
+
"version": 2,
|
|
351
|
+
"extends": ["zuplo:recommended"],
|
|
352
|
+
"requiredPolicies": [
|
|
353
|
+
{
|
|
354
|
+
"id": "require-auth",
|
|
355
|
+
"policyTypes": ["api-key", "open-id-jwt-auth"],
|
|
356
|
+
"direction": "inbound",
|
|
357
|
+
"severity": "error",
|
|
358
|
+
"message": "Routes must have an authentication policy",
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
"id": "require-rate-limit",
|
|
362
|
+
"policyTypes": ["rate-limit"],
|
|
363
|
+
"severity": "warn",
|
|
364
|
+
"message": "Routes should have a rate limiting policy",
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Unlike other rules, `requiredPolicies` works with any preset, including
|
|
371
|
+
`zuplo:recommended`. Each requirement applies to every route; to require a
|
|
372
|
+
policy on only some routes, use [`overrides`](#overrides) or a
|
|
373
|
+
[custom rule with `paths`](./api-lint-custom-rules.mdx#require-a-policy-on-some-routes).
|
|
374
|
+
|
|
375
|
+
Each entry has these fields:
|
|
376
|
+
|
|
377
|
+
| Field | Description |
|
|
378
|
+
| ------------- | --------------------------------------------------------------------------------------------------------- |
|
|
379
|
+
| `id` | A name for the requirement. Use it to keep track of entries; results report under `zuplo/require-policy`. |
|
|
380
|
+
| `policyTypes` | Policy types that satisfy the requirement. A route needs at least one policy whose type matches. |
|
|
381
|
+
| `direction` | `"inbound"` or `"outbound"`. Defaults to `"inbound"`. |
|
|
382
|
+
| `severity` | `"error"` or `"warn"`. Defaults to `"error"`. |
|
|
383
|
+
| `message` | The message to report. The linter appends the method and path of the route, for example `: get /todos`. |
|
|
384
|
+
|
|
385
|
+
The linter matches each policy name in the route's `x-zuplo-route.policies` list
|
|
386
|
+
to its `policyType` in `config/policies.json`. An entry in `policyTypes` matches
|
|
387
|
+
that exact type and any type that starts with it followed by a dash, so
|
|
388
|
+
`"api-key"` matches `api-key-inbound`, and `"rate-limit"` matches
|
|
389
|
+
`rate-limit-inbound`. Matching starts at the beginning of the type, so
|
|
390
|
+
`"rate-limit"` doesn't match `complex-rate-limit-inbound`; list
|
|
391
|
+
`"complex-rate-limit"` separately if you use it.
|
|
392
|
+
|
|
393
|
+
Requirements with `"severity": "error"` report under the rule ID
|
|
394
|
+
`zuplo/require-policy`. Requirements with `"severity": "warn"` report under
|
|
395
|
+
`zuplo/require-policy-warn`.
|
|
396
|
+
|
|
397
|
+
To turn off the preset defaults without adding your own requirements, set an
|
|
398
|
+
empty list:
|
|
399
|
+
|
|
400
|
+
```jsonc
|
|
401
|
+
{
|
|
402
|
+
"version": 2,
|
|
403
|
+
"extends": ["zuplo:strict"],
|
|
404
|
+
"requiredPolicies": [],
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
The defaults in `zuplo:strict` and `zuplo:security` accept these policy types:
|
|
409
|
+
|
|
410
|
+
- **Authentication:** `api-key`, `auth0-jwt-auth`, `clerk-jwt-auth`,
|
|
411
|
+
`cognito-jwt-auth`, `curity-phantom-token`, `firebase-jwt-auth`, `jwt-auth`,
|
|
412
|
+
`oauth2-pkce-auth`, `okta-jwt-auth`, `open-id-jwt-auth`, `propel-auth`,
|
|
413
|
+
`stytch-jwt-auth`, `supabase-jwt-auth`
|
|
414
|
+
- **Rate limiting:** `rate-limit`, `complex-rate-limit`
|
|
415
|
+
|
|
416
|
+
### `breakingChanges`
|
|
417
|
+
|
|
418
|
+
Compares each OpenAPI file with the same file on a Git branch, tag, or commit,
|
|
419
|
+
and reports changes that can break existing API consumers, such as a removed
|
|
420
|
+
endpoint or a changed type.
|
|
421
|
+
|
|
422
|
+
```jsonc
|
|
423
|
+
{
|
|
424
|
+
"version": 2,
|
|
425
|
+
"breakingChanges": {
|
|
426
|
+
"enabled": true,
|
|
427
|
+
"baseline": "origin/main",
|
|
428
|
+
"severity": "error",
|
|
429
|
+
},
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
| Field | Description |
|
|
434
|
+
| ---------- | ------------------------------------------------------------------------ |
|
|
435
|
+
| `enabled` | Turns detection on. Defaults to `false`. |
|
|
436
|
+
| `baseline` | The branch, tag, or commit SHA to compare against. Defaults to `"main"`. |
|
|
437
|
+
| `severity` | `"error"` or `"warn"`. Defaults to `"error"`. |
|
|
438
|
+
|
|
439
|
+
See [Breaking change detection](./api-lint-breaking-changes.mdx) for how to
|
|
440
|
+
choose a baseline, what counts as a breaking change, and how to run it in CI.
|
|
441
|
+
|
|
442
|
+
## Command-line flags and the configuration file
|
|
443
|
+
|
|
444
|
+
Flags take precedence over the configuration file for a single run:
|
|
445
|
+
|
|
446
|
+
| Flag | Overrides |
|
|
447
|
+
| ----------- | --------------------------------- |
|
|
448
|
+
| `--config` | Which configuration file to read. |
|
|
449
|
+
| `--fail-on` | [`failOn`](#failon). |
|
|
450
|
+
|
|
451
|
+
All other settings come only from the configuration file. See the
|
|
452
|
+
[`zuplo lint` CLI reference](../cli/lint.mdx) for every flag.
|
|
453
|
+
|
|
454
|
+
## Next steps
|
|
455
|
+
|
|
456
|
+
- [Custom lint rules](./api-lint-custom-rules.mdx) — write your own rules and
|
|
457
|
+
share them with a ruleset file.
|
|
458
|
+
- [Lint rules reference](./api-lint-rules.mdx) — what each rule checks.
|
|
459
|
+
- [Lint in CI/CD](./api-lint-ci-cd.mdx) — run the linter on every pull request.
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Custom Lint Rules
|
|
3
|
+
sidebar_label: Custom rules
|
|
4
|
+
description:
|
|
5
|
+
Write your own `zuplo lint` rules in the Spectral format, check Zuplo routes
|
|
6
|
+
and policies with Zuplo functions, and share the rules with a ruleset file.
|
|
7
|
+
applies_to:
|
|
8
|
+
product: gateway
|
|
9
|
+
hosting: all
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
When the built-in rules don't cover a convention your team follows, add your own
|
|
13
|
+
in `zuplo.lint.jsonc`. Custom rules use the
|
|
14
|
+
[Spectral](https://docs.stoplight.io/docs/spectral/e5b9616d6d50c-rulesets)
|
|
15
|
+
ruleset format, so a rule written for Spectral or vacuum works here unchanged.
|
|
16
|
+
|
|
17
|
+
## Write a rule
|
|
18
|
+
|
|
19
|
+
A rule selects parts of your OpenAPI document with `given` and checks each one
|
|
20
|
+
with a function in `then`:
|
|
21
|
+
|
|
22
|
+
```jsonc title="zuplo.lint.jsonc"
|
|
23
|
+
{
|
|
24
|
+
"$schema": "https://cdn.zuplo.com/schemas/zuplo-lint.json",
|
|
25
|
+
"version": 2,
|
|
26
|
+
"extends": ["zuplo:recommended"],
|
|
27
|
+
"rules": {
|
|
28
|
+
"operation-summary-length": {
|
|
29
|
+
"description": "Summaries must fit in the developer portal sidebar",
|
|
30
|
+
"message": "Keep summaries to 60 characters or fewer",
|
|
31
|
+
"severity": "warn",
|
|
32
|
+
"given": "$.paths[*][*].summary",
|
|
33
|
+
"then": { "function": "length", "functionOptions": { "max": 60 } },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```txt
|
|
40
|
+
config/routes.oas.json
|
|
41
|
+
45:20 warn Keep summaries to 60 characters or fewer: 'List every todo item that belongs to the current user, sorted by the date it was created' must not be longer/greater than '60' operation-summary-length $.paths['/v1/todos'].get.summary
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The key, `operation-summary-length`, is the rule ID. It appears in results, and
|
|
45
|
+
you use it to change the rule's severity in
|
|
46
|
+
[`overrides`](./api-lint-configuration.mdx#overrides). IDs starting with
|
|
47
|
+
`zuplo/` are reserved for Zuplo's rules.
|
|
48
|
+
|
|
49
|
+
| Field | Description |
|
|
50
|
+
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
51
|
+
| `given` | A [JSONPath](https://datatracker.ietf.org/doc/html/rfc9535) expression, or a list of them, selecting the values to check. |
|
|
52
|
+
| `then` | The function to run on each selected value, or a list of functions. Each has `function`, and optionally `field` and `functionOptions`. |
|
|
53
|
+
| `severity` | `"error"`, `"warn"`, `"info"`, `"hint"`, or `"off"`. Defaults to `"warn"`. |
|
|
54
|
+
| `message` | The text each result starts with. Defaults to `description`, then to the rule ID. |
|
|
55
|
+
| `description` | What the rule checks. Used as the message when `message` isn't set. |
|
|
56
|
+
| `paths` | Report the rule only on these API paths, such as `["/v1/**"]`. |
|
|
57
|
+
| `excludePaths` | Don't report the rule on these API paths. |
|
|
58
|
+
| `formats` | Document formats the rule applies to, such as `["oas3"]`. |
|
|
59
|
+
|
|
60
|
+
`then.field` checks a property of each selected value instead of the value
|
|
61
|
+
itself, so this rule reports every operation without a `summary`:
|
|
62
|
+
|
|
63
|
+
```jsonc
|
|
64
|
+
{
|
|
65
|
+
"version": 2,
|
|
66
|
+
"rules": {
|
|
67
|
+
"operation-summary": {
|
|
68
|
+
"description": "Operations need a summary",
|
|
69
|
+
"given": "$.paths[*][*]",
|
|
70
|
+
"then": { "field": "summary", "function": "truthy" },
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Functions
|
|
77
|
+
|
|
78
|
+
These are the general-purpose functions, the same as Spectral's core functions:
|
|
79
|
+
|
|
80
|
+
| Function | Reports when the value... | Options |
|
|
81
|
+
| -------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------- |
|
|
82
|
+
| `truthy` | is missing, empty, `false`, or `0` | |
|
|
83
|
+
| `falsy` | is set to a truthy value | |
|
|
84
|
+
| `defined` | is missing | |
|
|
85
|
+
| `undefined` | is set | |
|
|
86
|
+
| `pattern` | doesn't match `match`, or matches `notMatch` | `match`, `notMatch` (regular expressions) |
|
|
87
|
+
| `casing` | isn't in the given case | `type`: `camel`, `pascal`, `kebab`, `snake`, `cobol`, `flat`, `macro` |
|
|
88
|
+
| `length` | is shorter than `min` or longer than `max` (characters, items, or keys) | `min`, `max` |
|
|
89
|
+
| `enumeration` | isn't one of `values` | `values` |
|
|
90
|
+
| `alphabetical` | isn't sorted | `keyedBy` for lists of objects |
|
|
91
|
+
| `schema` | doesn't match a JSON schema | `schema` |
|
|
92
|
+
| `xor` | has more or fewer than one of `properties` | `properties` |
|
|
93
|
+
|
|
94
|
+
A Zuplo route has settings, such as its policies, that the OpenAPI document
|
|
95
|
+
alone can't explain, because policies are defined in `config/policies.json`.
|
|
96
|
+
These Zuplo functions read both files:
|
|
97
|
+
|
|
98
|
+
| Function | Reports a route that... | Options |
|
|
99
|
+
| ------------------------------ | -------------------------------------------------------- | --------------------------------------------------------------- |
|
|
100
|
+
| `zuplo:requirePolicy` | doesn't have a policy of one of the given types | `policyTypes` (required), `direction` (`inbound` or `outbound`) |
|
|
101
|
+
| `zuplo:noUndefinedPolicies` | references a policy that isn't in `config/policies.json` | |
|
|
102
|
+
| `zuplo:noEmptyInboundPolicies` | has an empty `policies.inbound` list | |
|
|
103
|
+
| `zuplo:requireRouteLabel` | has no `label` in `x-zuplo-route` | |
|
|
104
|
+
| `zuplo:requireCorsPolicy` | has no `corsPolicy` in `x-zuplo-route` | |
|
|
105
|
+
|
|
106
|
+
A Zuplo function checks every route itself, so set `"given": "$"` and choose the
|
|
107
|
+
routes with `paths` and `excludePaths`. Only `zuplo:requirePolicy` uses your
|
|
108
|
+
`message`; the other Zuplo functions write their own.
|
|
109
|
+
|
|
110
|
+
Run `npx zuplo lint --list-rules` to see every function and rule available.
|
|
111
|
+
|
|
112
|
+
## Examples
|
|
113
|
+
|
|
114
|
+
### Require a policy on some routes
|
|
115
|
+
|
|
116
|
+
[`requiredPolicies`](./api-lint-configuration.mdx#requiredpolicies) applies to
|
|
117
|
+
every route. A custom rule with `zuplo:requirePolicy` applies to the routes you
|
|
118
|
+
choose:
|
|
119
|
+
|
|
120
|
+
```jsonc
|
|
121
|
+
{
|
|
122
|
+
"version": 2,
|
|
123
|
+
"rules": {
|
|
124
|
+
"require-api-key-v1": {
|
|
125
|
+
"message": "v1 routes need an API key",
|
|
126
|
+
"severity": "error",
|
|
127
|
+
"paths": ["/v1/**"],
|
|
128
|
+
"given": "$",
|
|
129
|
+
"then": {
|
|
130
|
+
"function": "zuplo:requirePolicy",
|
|
131
|
+
"functionOptions": { "policyTypes": ["api-key"] },
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```txt
|
|
139
|
+
16:14 error v1 routes need an API key: get /v1/admin require-api-key-v1 paths./v1/admin.get.x-zuplo-route.policies.inbound
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`policyTypes` matches the same way as in `requiredPolicies`: `"api-key"` matches
|
|
143
|
+
the `api-key-inbound` policy type. Add one rule per requirement, each with its
|
|
144
|
+
own ID and `paths`.
|
|
145
|
+
|
|
146
|
+
### Enforce a naming convention
|
|
147
|
+
|
|
148
|
+
```jsonc
|
|
149
|
+
{
|
|
150
|
+
"version": 2,
|
|
151
|
+
"rules": {
|
|
152
|
+
"operation-id-camel-case": {
|
|
153
|
+
"description": "operationIds use camelCase",
|
|
154
|
+
"severity": "error",
|
|
155
|
+
"given": "$.paths[*][*].operationId",
|
|
156
|
+
"then": { "function": "casing", "functionOptions": { "type": "camel" } },
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Keep internal headers out of the public API
|
|
163
|
+
|
|
164
|
+
```jsonc
|
|
165
|
+
{
|
|
166
|
+
"version": 2,
|
|
167
|
+
"rules": {
|
|
168
|
+
"no-x-internal-headers": {
|
|
169
|
+
"description": "Don't expose X-Internal-* headers",
|
|
170
|
+
"severity": "warn",
|
|
171
|
+
"given": "$.paths[*][*].parameters[?(@.in == 'header')].name",
|
|
172
|
+
"then": {
|
|
173
|
+
"function": "pattern",
|
|
174
|
+
"functionOptions": { "notMatch": "^X-Internal-" },
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Share rules with a ruleset file
|
|
182
|
+
|
|
183
|
+
To use the same rules in several projects, put them in a ruleset file and extend
|
|
184
|
+
it. A ruleset file is a `.json` or `.jsonc` file in the Spectral ruleset format:
|
|
185
|
+
|
|
186
|
+
```jsonc title="lint/team-ruleset.jsonc"
|
|
187
|
+
{
|
|
188
|
+
// Rules shared by every API team
|
|
189
|
+
"rules": {
|
|
190
|
+
"operation-id-camel-case": {
|
|
191
|
+
"description": "operationIds use camelCase",
|
|
192
|
+
"severity": "error",
|
|
193
|
+
"given": "$.paths[*][*].operationId",
|
|
194
|
+
"then": { "function": "casing", "functionOptions": { "type": "camel" } },
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
```jsonc title="zuplo.lint.jsonc"
|
|
201
|
+
{
|
|
202
|
+
"$schema": "https://cdn.zuplo.com/schemas/zuplo-lint.json",
|
|
203
|
+
"version": 2,
|
|
204
|
+
"extends": ["zuplo:recommended", "./lint/team-ruleset.jsonc"],
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The path is relative to `zuplo.lint.jsonc`. A ruleset file can extend the
|
|
209
|
+
built-in rulesets (`vacuum:oas`, `spectral:oas`, `vacuum:owasp`,
|
|
210
|
+
`spectral:owasp`) but not other files. Rulesets must be local files: the linter
|
|
211
|
+
doesn't download rulesets from a URL, so copy a shared ruleset into your
|
|
212
|
+
repository, or add it as a package, and reference that path. YAML rulesets
|
|
213
|
+
aren't supported; convert them to JSON.
|
|
214
|
+
|
|
215
|
+
`zuplo.lint.jsonc` can still change the severity or scope of a rule from a
|
|
216
|
+
ruleset file, the same as any other rule.
|
|
217
|
+
|
|
218
|
+
## Next steps
|
|
219
|
+
|
|
220
|
+
- [Lint configuration](./api-lint-configuration.mdx) — rulesets, severities, and
|
|
221
|
+
overrides.
|
|
222
|
+
- [Lint rules reference](./api-lint-rules.mdx) — the rules in the Zuplo presets.
|