api-quality-spectral-ruleset 1.6.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <!-- README.md -->
2
2
 
3
- [![Node.js CI](https://github.com/apiaddicts/apquality-spectral/actions/workflows/node.js.yml/badge.svg)](https://github.com/apiaddicts/apquality-spectral/actions/workflows/node.js.yml) ![Coverage](./coverage/badges.svg)
3
+ [![Node.js CI](https://github.com/apiaddicts/apiaddicts-style-guide-spectral/actions/workflows/node.js.yml/badge.svg)](https://github.com/apiaddicts/apiaddicts-style-guide-spectral/actions/workflows/node.js.yml) ![Coverage](./coverage/badges.svg)
4
4
 
5
5
  # APIAddicts Style Guide Spectral
6
6
 
@@ -26,19 +26,19 @@ The Spectral Ruleset requires Node version 14 or later.
26
26
 
27
27
  You can specify the ruleset directly on the command line:
28
28
 
29
- `spectral lint -r https://raw.githubusercontent.com/apiaddicts/apquality-spectral/refs/heads/main/apq-spectral.yaml <api definition file>`
29
+ `spectral lint -r https://raw.githubusercontent.com/apiaddicts/apiaddicts-style-guide-spectral/refs/heads/main/apq-spectral.yaml <api definition file>`
30
30
 
31
31
  Or you can create a Spectral configuration file (`.spectral.yaml`) that references the ruleset:
32
32
 
33
33
  ```yaml
34
34
  extends:
35
- - https://raw.githubusercontent.com/apiaddicts/apquality-spectral/refs/heads/main/apq-spectral.yaml
35
+ - https://raw.githubusercontent.com/apiaddicts/apiaddicts-style-guide-spectral/refs/heads/main/apq-spectral.yaml
36
36
  ```
37
37
 
38
38
  ### Example
39
39
 
40
40
  ```bash
41
- spectral lint -r https://raw.githubusercontent.com/apiaddicts/apquality-spectral/refs/heads/main/apq-spectral.yaml petstore.yaml
41
+ spectral lint -r https://raw.githubusercontent.com/apiaddicts/apiaddicts-style-guide-spectral/refs/heads/main/apq-spectral.yaml petstore.yaml
42
42
  ```
43
43
 
44
44
  ### Using the Spectral VSCode extension
package/apq-spectral.yaml CHANGED
@@ -40,7 +40,7 @@ functions:
40
40
  - apq-numeric-invalid-format
41
41
  - apq-numeric-missing-format
42
42
  - apq-numeric-well-defined-format
43
- - apq-string-parameter-integrity
43
+ - apq-string-parameter-schema-integrity
44
44
  - apq-example-schema-types
45
45
  - apq-standard-response-schema
46
46
  - apq-allowed-http-verbs
@@ -54,11 +54,14 @@ functions:
54
54
  - apq-response-content-required
55
55
  - apq-tags-consistency
56
56
  - apq-forbidden-title-pattern
57
+ - apq-security-flows-allowed
58
+ - apq-oauth-url-pattern
59
+ - apq-security-types-allowed
57
60
  rules:
58
61
  apiq:OAR001:
59
62
  description: "For security reasons and as a REST best practice, the HTTPS protocol is mandatory."
60
63
  message: "OAR001: HTTPS protocol is mandatory."
61
- documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR001.md"
64
+ documentationUrl: "https://github.com/apiaddicts/apiaddicts-style-guide-spectral/blob/main/docs/resources/OAR001.md"
62
65
  severity: "error"
63
66
  given: "$.servers[*].url"
64
67
  then:
@@ -68,7 +71,7 @@ rules:
68
71
  apiq:OAR002:
69
72
  description: "A wrong scope definition may cause problems to import the API definition into WSO2."
70
73
  message: "{{error}}"
71
- documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR002.md"
74
+ documentationUrl: "https://github.com/apiaddicts/apiaddicts-style-guide-spectral/blob/main/docs/resources/OAR002.md"
72
75
  severity: "error"
73
76
  given: "$.x-wso2-security.apim.x-wso2-scopes"
74
77
  then:
@@ -76,7 +79,7 @@ rules:
76
79
  apiq:OAR003:
77
80
  description: "A description can help other developers to understand the correct use of the scope."
78
81
  message: "OAR003: Scope must define a 'description' attribute."
79
- documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR003.md"
82
+ documentationUrl: "https://github.com/apiaddicts/apiaddicts-style-guide-spectral/blob/main/docs/resources/OAR003.md"
80
83
  severity: "error"
81
84
  given: "$.x-wso2-security.apim.x-wso2-scopes[*]"
82
85
  then:
@@ -86,7 +89,7 @@ rules:
86
89
  apiq:OAR004:
87
90
  description: "A role with forbidden characters may cause problems in some applications."
88
91
  message: "{{error}}"
89
- documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR004.md"
92
+ documentationUrl: "https://github.com/apiaddicts/apiaddicts-style-guide-spectral/blob/main/docs/resources/OAR004.md"
90
93
  severity: "error"
91
94
  given: "$.x-wso2-security.apim.x-wso2-scopes[*].roles"
92
95
  then:
@@ -96,7 +99,7 @@ rules:
96
99
  apiq:OAR005:
97
100
  description: "A wrong scope may cause problems to import the API definition into WSO2 or allow all users to call the endpoint."
98
101
  message: "{{error}}"
99
- documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR005.md"
102
+ documentationUrl: "https://github.com/apiaddicts/apiaddicts-style-guide-spectral/blob/main/docs/resources/OAR005.md"
100
103
  severity: "error"
101
104
  resolved: false
102
105
  given: "$"
@@ -106,7 +109,7 @@ rules:
106
109
  description: "Routes must define request media types supported by the API."
107
110
  message: "OAR006: Specify at least one Media Type in the content of the request body."
108
111
  severity: "error"
109
- documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR006.md"
112
+ documentationUrl: "https://github.com/apiaddicts/apiaddicts-style-guide-spectral/blob/main/docs/resources/OAR006.md"
110
113
  given: "$.paths[*][post,put,patch]"
111
114
  then:
112
115
  - field: "requestBody"
@@ -116,7 +119,7 @@ rules:
116
119
  apiq:OAR007:
117
120
  description: "Routes must define response media types supported by the API"
118
121
  message: "OAR007: Specify at least one Media Type in the content of the response body."
119
- documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR007.md"
122
+ documentationUrl: "https://github.com/apiaddicts/apiaddicts-style-guide-spectral/blob/main/docs/resources/OAR007.md"
120
123
  severity: "error"
121
124
  given:
122
125
  - "$.paths[*][post,put,patch]"
@@ -128,7 +131,7 @@ rules:
128
131
  apiq:OAR008:
129
132
  description: "HTTP verbs not encouraged."
130
133
  message: "{{error}}"
131
- documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR008.md"
134
+ documentationUrl: "https://github.com/apiaddicts/apiaddicts-style-guide-spectral/blob/main/docs/resources/OAR008.md"
132
135
  severity: error
133
136
  given: "$.paths[*]"
134
137
  then:
@@ -162,7 +165,7 @@ rules:
162
165
  apiq:OAR011:
163
166
  description: "URLs should follow the configured naming convention (default kebab-case): all literal path segments must comply."
164
167
  message: "{{error}}"
165
- documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR011.md"
168
+ documentationUrl: "https://github.com/apiaddicts/apiaddicts-style-guide-spectral/blob/main/docs/resources/OAR011.md"
166
169
  severity: error
167
170
  given:
168
171
  - "$.paths[*]~"
@@ -174,7 +177,7 @@ rules:
174
177
  apiq:OAR012:
175
178
  description: "Path params, query params, object names and property names should follow the configured naming convention. You can configure snake_case (default), kebab-case, camelCase or UpperCamelCase"
176
179
  message: "OAR012: Path params, query params, object names and property names must follow the configured naming convention."
177
- documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR012.md"
180
+ documentationUrl: "https://github.com/apiaddicts/apiaddicts-style-guide-spectral/blob/main/docs/resources/OAR012.md"
178
181
  severity: warn
179
182
  given:
180
183
  - "$.paths[*][*].parameters[?(@.in == 'path' || @.in == 'query')].name"
@@ -197,7 +200,7 @@ rules:
197
200
  apiq:OAR014:
198
201
  description: "Resources depth level should be below the non-suggested range."
199
202
  message: "{{error}}"
200
- documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR014.md"
203
+ documentationUrl: "https://github.com/apiaddicts/apiaddicts-style-guide-spectral/blob/main/docs/resources/OAR014.md"
201
204
  severity: warn
202
205
  given: "$.paths.*~"
203
206
  then:
@@ -221,7 +224,7 @@ rules:
221
224
  apiq:OAR016:
222
225
  description: "Numeric types requires a valid format."
223
226
  message: "OAR016: Numeric types must use a valid format for their type."
224
- documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR016.md"
227
+ documentationUrl: "https://github.com/apiaddicts/apiaddicts-style-guide-spectral/blob/main/docs/resources/OAR016.md"
225
228
  severity: error
226
229
  resolved: false
227
230
  given: "$..[?(@ && @.type)]"
@@ -230,7 +233,7 @@ rules:
230
233
  apiq:OAR017:
231
234
  description: "Resource path should alternate static and parametrized parts."
232
235
  message: "OAR017: Resource path should alternate static and parametrized parts"
233
- documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR017.md"
236
+ documentationUrl: "https://github.com/apiaddicts/apiaddicts-style-guide-spectral/blob/main/docs/resources/OAR017.md"
234
237
  severity: error
235
238
  given: "$.paths.*~"
236
239
  then:
@@ -240,7 +243,7 @@ rules:
240
243
  apiq:OAR018:
241
244
  description: Operation not recommended for resource path depending on HTTP verb
242
245
  message: "OAR018: Operation not recommended for resource path: {{path}} ({{verb}})"
243
- documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR018.md"
246
+ documentationUrl: "https://github.com/apiaddicts/apiaddicts-style-guide-spectral/blob/main/docs/resources/OAR018.md"
244
247
  severity: warn
245
248
  given: "$.paths"
246
249
  then:
@@ -322,7 +325,7 @@ rules:
322
325
  apiq:OAR022:
323
326
  description: "$orderby must be defined as a query parameter in the collection GET operations selected by the configured paths."
324
327
  message: "{{error}}"
325
- documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR022.md"
328
+ documentationUrl: "https://github.com/apiaddicts/apiaddicts-style-guide-spectral/blob/main/docs/resources/OAR022.md"
326
329
  severity: warn
327
330
  given: "$.paths"
328
331
  then:
@@ -350,7 +353,7 @@ rules:
350
353
  apiq:OAR025:
351
354
  description: "$limit must be defined as an integer query parameter in the collection GET operations selected by the configured paths."
352
355
  message: "{{error}}"
353
- documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR025.md"
356
+ documentationUrl: "https://github.com/apiaddicts/apiaddicts-style-guide-spectral/blob/main/docs/resources/OAR025.md"
354
357
  severity: error
355
358
  given: "$.paths"
356
359
  then:
@@ -439,7 +442,7 @@ rules:
439
442
  functionOptions:
440
443
  notMatch: "^(Accept|Content-Type|Authorization)$"
441
444
  apiq:OAR034:
442
- description: "GET collection responses must include a paging block that complies with the standard paging schema (required start, limit and links with self/previous/next; optional numPages and total as integers)."
445
+ description: "GET success JSON responses on collection paths must define an object schema with a paging block that complies with paging-schema (required start, limit and links with self/previous/next; optional numPages and total as integers)."
443
446
  message: "{{error}}"
444
447
  severity: error
445
448
  given: "$.paths[*].get.responses[*]"
@@ -447,6 +450,8 @@ rules:
447
450
  function: apq-paged-response-check
448
451
  functionOptions:
449
452
  paging-schema: '{"type":"object","properties":{"numPages":{"type":"integer"},"total":{"type":"integer"},"start":{"type":"integer"},"limit":{"type":"integer"},"links":{"type":"object","properties":{"next":{"type":"object","properties":{"href":{"type":"string"}}},"previous":{"type":"object","properties":{"href":{"type":"string"}}},"last":{"type":"object","properties":{"href":{"type":"string"}}},"self":{"type":"object","properties":{"href":{"type":"string"}}},"first":{"type":"object","properties":{"href":{"type":"string"}}}},"required":["self","previous","next"]}},"required":["start","limit","links"],"pagingPropertyName":"paging"}'
453
+ paths: "/status,/health,/ping,**/me"
454
+ pathValidationStrategy: "/exclude"
450
455
  apiq:OAR035:
451
456
  description: "Response code 401 must be defined for operations with security schemes defined."
452
457
  message: "{{error}}"
@@ -477,7 +482,7 @@ rules:
477
482
  then:
478
483
  function: apq-schema-format
479
484
  apiq:OAR038:
480
- description: "The 201 response schema of a POST operation must have properties named 'data' or 'error' with at least one sub-property."
485
+ description: "The 201 response schema of a POST operation must only have the allowed top-level properties ('data' or 'error' by default), each with at least one sub-property, and every required one."
481
486
  message: "{{error}}"
482
487
  severity: error
483
488
  given: "$.paths.*.post.responses['201']"
@@ -485,10 +490,14 @@ rules:
485
490
  function: apq-valid-response-schema
486
491
  functionOptions:
487
492
  data-property: data
493
+ allowed-properties: ""
494
+ required-properties: ""
495
+ paths: "/me,/health,/ping,/status"
496
+ pathValidationStrategy: "/exclude"
488
497
  apiq:OAR039:
489
498
  description: "Response codes must be defined according to the standard depending on the HTTP verb and resource path."
490
499
  message: "{{error}}"
491
- documentationUrl: https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR039.md
500
+ documentationUrl: https://github.com/apiaddicts/apiaddicts-style-guide-spectral/blob/main/docs/resources/OAR039.md
492
501
  severity: error
493
502
  given: "$.paths[*][*]"
494
503
  then:
@@ -533,7 +542,7 @@ rules:
533
542
  apiq:OAR041:
534
543
  description: "A WSO2 x-scope on an operation always requires an x-auth-type definition."
535
544
  message: "{{error}}"
536
- documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR041.md"
545
+ documentationUrl: "https://github.com/apiaddicts/apiaddicts-style-guide-spectral/blob/main/docs/resources/OAR041.md"
537
546
  severity: "error"
538
547
  resolved: false
539
548
  given: "$"
@@ -553,7 +562,7 @@ rules:
553
562
  apiq:OAR043:
554
563
  description: "OpenAPI definition contains structural errors that would be detected by a strict parser or validator."
555
564
  message: "{{error}}"
556
- documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR043.md"
565
+ documentationUrl: "https://github.com/apiaddicts/apiaddicts-style-guide-spectral/blob/main/docs/resources/OAR043.md"
557
566
  severity: error
558
567
  given:
559
568
  - $.paths.*.*.parameters.*.in
@@ -602,7 +611,7 @@ rules:
602
611
  - "$.paths[*].patch"
603
612
  - "$.paths[*].delete"
604
613
  then:
605
- function: schema
614
+ function: schema
606
615
  functionOptions:
607
616
  schema:
608
617
  type: object
@@ -621,7 +630,7 @@ rules:
621
630
  apiq:OAR048:
622
631
  description: APIs must define at most one body parameter.
623
632
  message: "OAR048: An operation can have at most one body parameter"
624
- documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR048.md"
633
+ documentationUrl: "https://github.com/apiaddicts/apiaddicts-style-guide-spectral/blob/main/docs/resources/OAR048.md"
625
634
  severity: error
626
635
  given: "$.paths[*][*]"
627
636
  then:
@@ -657,7 +666,7 @@ rules:
657
666
  apiq:OAR052:
658
667
  description: "Numeric schema types must define a format."
659
668
  message: "OAR052: Numeric types requires a format"
660
- documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR052.md"
669
+ documentationUrl: "https://github.com/apiaddicts/apiaddicts-style-guide-spectral/blob/main/docs/resources/OAR052.md"
661
670
  severity: warn
662
671
  resolved: false
663
672
  given: "$..[?(@ && @.type)]"
@@ -666,7 +675,7 @@ rules:
666
675
  apiq:OAR053:
667
676
  description: "Response headers for API observability and tracing must be defined (excluding 204 responses and health endpoints)."
668
677
  message: "OAR053: Response must include mandatory headers and exclude forbidden headers."
669
- documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR053.md"
678
+ documentationUrl: "https://github.com/apiaddicts/apiaddicts-style-guide-spectral/blob/main/docs/resources/OAR053.md"
670
679
  severity: error
671
680
  given: "$.paths[*][*].responses[*]"
672
681
  then:
@@ -843,8 +852,8 @@ rules:
843
852
  then:
844
853
  function: apq-numeric-parameter-integrity
845
854
  apiq:OAR075:
846
- description: "String parameters should have minLength, maxLength, pattern (regular expression), or enum restriction."
847
- message: "OAR075: String parameters should have minLength, maxLength, pattern, or enum restriction."
855
+ description: "String parameter schemas should have minLength, maxLength, pattern (regular expression), or enum restriction."
856
+ message: "OAR075: String parameter schemas should have minLength, maxLength, pattern, or enum restriction."
848
857
  severity: error
849
858
  resolved: false
850
859
  given:
@@ -857,13 +866,13 @@ rules:
857
866
  - "$.components.parameters[*]"
858
867
  - "$.parameters[*]"
859
868
  then:
860
- function: apq-string-parameter-integrity
869
+ function: apq-string-parameter-schema-integrity
861
870
  functionOptions:
862
871
  parameter_integrity: "minLength,maxLength,pattern,enum"
863
872
  apiq:OAR076:
864
873
  description: "Schema should use well-defined type and format."
865
874
  message: "OAR076: Schema should use well-defined type and format."
866
- documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR076.md"
875
+ documentationUrl: "https://github.com/apiaddicts/apiaddicts-style-guide-spectral/blob/main/docs/resources/OAR076.md"
867
876
  severity: error
868
877
  resolved: false
869
878
  given: "$..[?(@ && @.type)]"
@@ -884,7 +893,7 @@ rules:
884
893
  apiq:OAR078:
885
894
  description: "All API methods must have security defined."
886
895
  message: "OAR078: Operation must have security defined."
887
- documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR078.md"
896
+ documentationUrl: "https://github.com/apiaddicts/apiaddicts-style-guide-spectral/blob/main/docs/resources/OAR078.md"
888
897
  severity: error
889
898
  given: "$"
890
899
  then:
@@ -892,7 +901,7 @@ rules:
892
901
  apiq:OAR079:
893
902
  description: "Operations with path parameters should include a 404 Not Found response."
894
903
  message: "OAR079: Path parameter present, but missing 404 Not Found response."
895
- documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR079.md"
904
+ documentationUrl: "https://github.com/apiaddicts/apiaddicts-style-guide-spectral/blob/main/docs/resources/OAR079.md"
896
905
  severity: "warn"
897
906
  given: "$.paths[*][get,post,put,patch,delete]"
898
907
  then:
@@ -1010,7 +1019,7 @@ rules:
1010
1019
  apiq:OAR091:
1011
1020
  description: "Parameters must contain only $ref references."
1012
1021
  message: "OAR091: Parameters must contain only $ref references."
1013
- documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR091.md"
1022
+ documentationUrl: "https://github.com/apiaddicts/apiaddicts-style-guide-spectral/blob/main/docs/resources/OAR091.md"
1014
1023
  severity: error
1015
1024
  resolved: false
1016
1025
  given: "$.paths[*][get,post,put,patch,delete].parameters[*]"
@@ -1176,7 +1185,7 @@ rules:
1176
1185
  apiq:OAR113:
1177
1186
  description: "Field or extension must be at the assigned location"
1178
1187
  message: "OAR113: Field or extension x-custom-example must be at the assigned location"
1179
- documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR113.md"
1188
+ documentationUrl: "https://github.com/apiaddicts/apiaddicts-style-guide-spectral/blob/main/docs/resources/OAR113.md"
1180
1189
  severity: warn
1181
1190
  given: "$"
1182
1191
  then:
@@ -1187,7 +1196,7 @@ rules:
1187
1196
  apiq:OAR114:
1188
1197
  description: "Response headers for API security and key management must be defined (excluding 204 responses)."
1189
1198
  message: "OAR114: Response must include mandatory headers and exclude forbidden headers."
1190
- documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR114.md"
1199
+ documentationUrl: "https://github.com/apiaddicts/apiaddicts-style-guide-spectral/blob/main/docs/resources/OAR114.md"
1191
1200
  severity: "error"
1192
1201
  given: "$.paths[*][*].responses[*]"
1193
1202
  then:
@@ -1200,7 +1209,7 @@ rules:
1200
1209
  apiq:OAR115:
1201
1210
  description: "All fields listed in the required array must be defined in the schema properties."
1202
1211
  message: "OAR115: All fields in the required array must be defined in schema properties."
1203
- documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR115.md"
1212
+ documentationUrl: "https://github.com/apiaddicts/apiaddicts-style-guide-spectral/blob/main/docs/resources/OAR115.md"
1204
1213
  severity: warn
1205
1214
  resolved: false
1206
1215
  given: "$..[?(@ && @.required)]"
@@ -1209,7 +1218,7 @@ rules:
1209
1218
  apiq:OAR116:
1210
1219
  description: "Every API path must match the configured regular expression."
1211
1220
  message: "{{error}}"
1212
- documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR116.md"
1221
+ documentationUrl: "https://github.com/apiaddicts/apiaddicts-style-guide-spectral/blob/main/docs/resources/OAR116.md"
1213
1222
  severity: error
1214
1223
  given: "$.paths.*~"
1215
1224
  then:
@@ -1219,10 +1228,60 @@ rules:
1219
1228
  apiq:OAR117:
1220
1229
  description: "The API title must not match the configured forbidden regular expression."
1221
1230
  message: "{{error}}"
1222
- documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR117.md"
1231
+ documentationUrl: "https://github.com/apiaddicts/apiaddicts-style-guide-spectral/blob/main/docs/resources/OAR117.md"
1223
1232
  severity: error
1224
1233
  given: "$.info.title"
1225
1234
  then:
1226
1235
  function: apq-forbidden-title-pattern
1227
1236
  functionOptions:
1228
- forbidden-pattern: "(?!)"
1237
+ forbidden-pattern: "(?!)"
1238
+ apiq:OAR118:
1239
+ description: "OAuth2 security schemes must only declare the flows allowed by the organization (authorizationCode and clientCredentials by default)."
1240
+ message: "{{error}}"
1241
+ documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR118.md"
1242
+ severity: error
1243
+ given: "$"
1244
+ then:
1245
+ function: apq-security-flows-allowed
1246
+ functionOptions:
1247
+ allowed-flows: "authorizationCode,clientCredentials"
1248
+ paths: "/me,/health,/ping,/status"
1249
+ pathValidationStrategy: "/exclude"
1250
+ apiq:OAR119:
1251
+ description: "The authorizationUrl of every OAuth2 flow must match the pattern allowed by the organization (an HTTPS URL on a domain ending in /authorize by default)."
1252
+ message: "{{error}}"
1253
+ documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR119.md"
1254
+ severity: error
1255
+ given: "$"
1256
+ then:
1257
+ function: apq-oauth-url-pattern
1258
+ functionOptions:
1259
+ field: authorizationUrl
1260
+ pattern: '^https://[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)+(:[0-9]+)?(/[^/?#]+)*/authorize$'
1261
+ paths: "/me,/health,/ping,/status"
1262
+ pathValidationStrategy: "/exclude"
1263
+ apiq:OAR120:
1264
+ description: "The tokenUrl of every OAuth2 flow must match the pattern allowed by the organization (an HTTPS URL on a domain ending in /token by default)."
1265
+ message: "{{error}}"
1266
+ documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR120.md"
1267
+ severity: error
1268
+ given: "$"
1269
+ then:
1270
+ function: apq-oauth-url-pattern
1271
+ functionOptions:
1272
+ field: tokenUrl
1273
+ pattern: '^https://[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)+(:[0-9]+)?(/[^/?#]+)*/token$'
1274
+ paths: "/me,/health,/ping,/status"
1275
+ pathValidationStrategy: "/exclude"
1276
+ apiq:OAR121:
1277
+ description: "Security schemes must only use the types allowed by the organization (oauth2 and HTTP bearer by default)."
1278
+ message: "{{error}}"
1279
+ documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR121.md"
1280
+ severity: error
1281
+ given: "$"
1282
+ then:
1283
+ function: apq-security-types-allowed
1284
+ functionOptions:
1285
+ allowed-security-types: "oauth2,bearer"
1286
+ paths: "/me,/health,/ping,/status"
1287
+ pathValidationStrategy: "/exclude"
@@ -0,0 +1,205 @@
1
+ const { pathsIncludeExclude } = require('./paths-include-exclude.js');
2
+
3
+ const DEFAULT_PATTERNS = {
4
+ authorizationUrl: '^https://[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)+(:[0-9]+)?(/[^/?#]+)*/authorize$',
5
+ tokenUrl: '^https://[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)+(:[0-9]+)?(/[^/?#]+)*/token$',
6
+ };
7
+ const DEFAULT_PATHS = '/me,/health,/ping,/status';
8
+ const INLINE_FLAGS_REGEX = /^\(\?([a-zA-Z]+)\)/;
9
+ const SUPPORTED_INLINE_FLAGS = 'imsu';
10
+
11
+ const VERBS_V2 = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch'];
12
+ const VERBS_V3 = VERBS_V2.concat(['trace']);
13
+ const VERBS_V32 = VERBS_V3.concat(['query']);
14
+
15
+ const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
16
+ const has = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
17
+
18
+ const documentVersion = (root) => {
19
+ if (has(root, 'swagger')) return 'v2';
20
+ if (!has(root, 'openapi')) return null;
21
+ const openapi = String(root.openapi).trim();
22
+ if (openapi.startsWith('3.2')) return 'v32';
23
+ if (openapi.startsWith('3.1')) return 'v31';
24
+ return 'v3';
25
+ };
26
+
27
+ const buildRegex = (raw) => {
28
+ if (typeof raw !== 'string' || raw.trim() === '') return null;
29
+ const text = raw.trim();
30
+ let source = text;
31
+ let flags = '';
32
+ const inline = INLINE_FLAGS_REGEX.exec(source);
33
+ if (inline) {
34
+ const requested = inline[1].toLowerCase();
35
+ if ([...requested].every((flag) => SUPPORTED_INLINE_FLAGS.includes(flag))) {
36
+ flags = [...new Set(requested)].join('');
37
+ source = source.slice(inline[0].length);
38
+ }
39
+ }
40
+ if (source === '') return null;
41
+ try {
42
+ return { regex: new RegExp(source, flags), text };
43
+ } catch (err) {
44
+ return null;
45
+ }
46
+ };
47
+
48
+ const pointerTarget = (root, ref) => {
49
+ let fragment;
50
+ try {
51
+ fragment = decodeURIComponent(ref.slice(1));
52
+ } catch (err) {
53
+ return undefined;
54
+ }
55
+ if (!fragment.startsWith('/')) return undefined;
56
+ const segments = fragment.slice(1).split('/').map((segment) => segment.replace(/~1/g, '/').replace(/~0/g, '~'));
57
+ let value = root;
58
+ for (const segment of segments) {
59
+ if (!isObject(value) || !has(value, segment)) return undefined;
60
+ value = value[segment];
61
+ }
62
+ return isObject(value) ? { value, segments } : undefined;
63
+ };
64
+
65
+ const collectOperations = (root, version) => {
66
+ const verbs = version === 'v32' ? VERBS_V32 : (version === 'v2' ? VERBS_V2 : VERBS_V3);
67
+ const operations = [];
68
+ const visitPathItem = (pathItem, pathKey, ancestors) => {
69
+ if (!isObject(pathItem) || ancestors.has(pathItem)) return;
70
+ const chain = new Set(ancestors).add(pathItem);
71
+ const ops = verbs.filter((verb) => has(pathItem, verb)).map((verb) => pathItem[verb]);
72
+ if (version === 'v32' && isObject(pathItem.additionalOperations)) ops.push(...Object.values(pathItem.additionalOperations));
73
+ ops.filter(isObject).forEach((op) => {
74
+ operations.push({ op, pathKey });
75
+ if (version === 'v2' || !isObject(op.callbacks)) return;
76
+ Object.values(op.callbacks).filter(isObject).forEach((callback) => {
77
+ Object.keys(callback)
78
+ .filter((expression) => !expression.startsWith('x-'))
79
+ .forEach((expression) => visitPathItem(callback[expression], pathKey, chain));
80
+ });
81
+ });
82
+ };
83
+ if (isObject(root.paths)) {
84
+ Object.keys(root.paths)
85
+ .filter((pathKey) => pathKey.startsWith('/'))
86
+ .forEach((pathKey) => visitPathItem(root.paths[pathKey], pathKey, new Set()));
87
+ }
88
+ if ((version === 'v31' || version === 'v32') && isObject(root.webhooks)) {
89
+ Object.values(root.webhooks).forEach((pathItem) => visitPathItem(pathItem, null, new Set()));
90
+ }
91
+ return operations;
92
+ };
93
+
94
+ const definitionsBase = (version) => (version === 'v2' ? ['securityDefinitions'] : ['components', 'securitySchemes']);
95
+
96
+ const lookup = (root, segments) => segments.reduce((value, segment) => (isObject(value) ? value[segment] : undefined), root);
97
+
98
+ /**
99
+ * @param {object} root - The resolved document (given: "$")
100
+ * @param {object} options
101
+ * @param {string} options.field - "authorizationUrl" or "tokenUrl", the URL of the OAuth2 flows that is checked
102
+ * @param {string} options.pattern - regular expression the URL must match (unanchored, an optional leading "(?i)" style
103
+ * group sets the i, m, s and u flags); blank or invalid falls back to "https://host.domain/.../authorize" for
104
+ * authorizationUrl and "https://host.domain/.../token" for tokenUrl
105
+ * @param {string|string[]} options.paths - comma-separated path patterns matched segment by segment; "*" matches one segment
106
+ * and "**" zero or more (default "/me,/health,/ping,/status")
107
+ * @param {string} options.pathValidationStrategy - "/exclude" (default) skips the security schemes applied only to matching
108
+ * paths, "/include" only evaluates the security schemes applied to them; webhooks have no path, so they are only evaluated
109
+ * with "/exclude", and callbacks follow the path of their operation
110
+ * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
111
+ * @returns {Array<{message: string, path: Array<string>}>} one result per URL that does not match, on the URL itself
112
+ */
113
+ module.exports = (root, options, context) => {
114
+ if (!isObject(root)) return [];
115
+ const version = documentVersion(root);
116
+ if (version === null) return [];
117
+
118
+ const opts = options || {};
119
+ const field = opts.field;
120
+ if (!has(DEFAULT_PATTERNS, field)) return [];
121
+ const matcher = buildRegex(opts.pattern) || buildRegex(DEFAULT_PATTERNS[field]);
122
+ const { include, isEvaluatedPath } = pathsIncludeExclude(opts, DEFAULT_PATHS);
123
+
124
+ const base = definitionsBase(version);
125
+ const definitions = lookup(root, base);
126
+ const rawDefinitions = lookup(context.document && context.document.data, base);
127
+ const schemes = new Map();
128
+ if (isObject(definitions)) {
129
+ Object.keys(definitions).forEach((name) => {
130
+ const scheme = definitions[name];
131
+ if (!isObject(scheme)) return;
132
+ const rawEntry = isObject(rawDefinitions) ? rawDefinitions[name] : undefined;
133
+ const direct = isObject(rawEntry) && !has(rawEntry, '$ref');
134
+ const known = schemes.get(scheme);
135
+ if (!known || (!known.direct && direct)) schemes.set(scheme, { name, path: [...base, name], direct });
136
+ });
137
+ }
138
+
139
+ const schemeFor = (key) => {
140
+ if (isObject(definitions) && has(definitions, key)) return isObject(definitions[key]) ? definitions[key] : undefined;
141
+ if (version !== 'v32') return undefined;
142
+ const self = typeof root.$self === 'string' ? root.$self.trim() : '';
143
+ const fragment = self !== '' && key.startsWith(`${self}#`) ? key.slice(self.length) : key;
144
+ if (!fragment.startsWith('#')) return undefined;
145
+ const target = pointerTarget(root, fragment);
146
+ if (!target) return undefined;
147
+ if (!schemes.has(target.value)) {
148
+ schemes.set(target.value, { name: target.segments[target.segments.length - 1], path: target.segments, direct: true });
149
+ }
150
+ return target.value;
151
+ };
152
+
153
+ const usage = new Map();
154
+ const rootSecurity = Array.isArray(root.security) ? root.security : [];
155
+ collectOperations(root, version).forEach(({ op, pathKey }) => {
156
+ const security = Array.isArray(op.security) ? op.security : rootSecurity;
157
+ const inScope = pathKey === null ? !include : isEvaluatedPath(pathKey);
158
+ security.filter(isObject).forEach((requirement) => {
159
+ Object.keys(requirement).forEach((key) => {
160
+ const scheme = schemeFor(key);
161
+ if (scheme) usage.set(scheme, usage.get(scheme) === true || inScope);
162
+ });
163
+ });
164
+ });
165
+
166
+ const inventory = context.documentInventory;
167
+ const isLocatable = (path) => {
168
+ const item = inventory.findAssociatedItemForPath(path, true);
169
+ if (!item || !item.document) return true;
170
+ return item.document.getRangeForJsonPath(item.path, false) !== undefined;
171
+ };
172
+ const locatable = (path) => {
173
+ if (!inventory || typeof inventory.findAssociatedItemForPath !== 'function') return path;
174
+ for (let length = path.length; length > 0; length -= 1) {
175
+ if (isLocatable(path.slice(0, length))) return path.slice(0, length);
176
+ }
177
+ return path;
178
+ };
179
+
180
+ const ruleCode = context.rule && context.rule.name ? context.rule.name.split(':').pop() : (field === 'tokenUrl' ? 'OAR120' : 'OAR119');
181
+ const results = [];
182
+ const check = (url, flow, name, path) => {
183
+ if (typeof url !== 'string' || matcher.regex.test(url)) return;
184
+ const flowText = typeof flow === 'string' && flow.trim() !== '' ? ` of flow '${flow}'` : '';
185
+ results.push({
186
+ message: `${ruleCode}: ${field} '${url}'${flowText} in security scheme '${name}' does not match the allowed pattern: ${matcher.text}`,
187
+ path: locatable(path),
188
+ });
189
+ };
190
+
191
+ schemes.forEach((entry, scheme) => {
192
+ if (usage.has(scheme) ? !usage.get(scheme) : include) return;
193
+ if (typeof scheme.type !== 'string' || scheme.type.trim().toLowerCase() !== 'oauth2') return;
194
+ if (version === 'v2') {
195
+ if (has(scheme, field)) check(scheme[field], scheme.flow, entry.name, [...entry.path, field]);
196
+ return;
197
+ }
198
+ if (!isObject(scheme.flows)) return;
199
+ Object.keys(scheme.flows)
200
+ .filter((flow) => !flow.startsWith('x-') && flow.trim() !== '' && isObject(scheme.flows[flow]) && has(scheme.flows[flow], field))
201
+ .forEach((flow) => check(scheme.flows[flow][field], flow, entry.name, [...entry.path, 'flows', flow, field]));
202
+ });
203
+
204
+ return results;
205
+ };