├── .editorconfig
├── .github
└── workflows
│ └── test.yaml
├── .gitignore
├── .vscode
├── extensions.json
└── settings.json
├── LICENSE
├── Makefile
├── README.md
├── docs
└── policies.txt
├── example
├── conftest
│ ├── Makefile
│ └── data.yaml
├── inputs
│ └── openapi.json
├── opa
│ ├── Makefile
│ ├── data.yaml
│ └── server-configuration.yaml
└── spectral
│ ├── .gitignore
│ ├── README.md
│ ├── build.sh
│ ├── build
│ └── .gitkeep
│ ├── example
│ ├── .spectral.yaml
│ └── openapi.json
│ ├── functions
│ └── opa.js
│ ├── package-lock.json
│ ├── package.json
│ ├── policies.js
│ ├── ruleset.js
│ └── wasm.js
└── src
├── .manifest
└── openapi
├── lib
└── lib.rego
├── main
├── conftest.rego
├── main.rego
├── main_test.rego
├── recommended.rego
└── ruleset.rego
└── policies
├── contact-properties
├── contact_properties.rego
└── contact_properties_test.rego
├── duplicated-entry-in-enum
├── duplicated_entry_in_enum.rego
└── duplicated_entry_in_enum_test.rego
├── info-contact
├── info_contact.rego
└── info_contact_test.rego
├── info-description
├── info_description.rego
└── info_description_test.rego
├── info-license
├── info_license.rego
└── info_license_test.rego
├── license-url
├── license_url.rego
└── license_url_test.rego
├── no-eval-in-markdown
├── no_eval_in_markdown.rego
└── no_eval_in_markdown_test.rego
├── no-script-tags-in-markdown
├── no_script_tags_in_markdown.rego
└── no_script_tags_in_markdown_test.rego
├── openapi-tags-uniqueness
├── openapi_tags_uniqueness.rego
└── openapi_tags_uniqueness_test.rego
├── openapi-tags
├── openapi_tags.rego
└── openapi_tags_test.rego
├── operation-description
├── operation_description.rego
└── operation_description_test.rego
├── operation-operationId-unique
├── operation_operationid_unique.rego
└── operation_operationid_unique_test.rego
├── operation-operationId-valid-in-url
├── operation_operationid_valid_in_url.rego
└── operation_operationid_valid_in_url_test.rego
├── operation-operationId
├── operation_operationid.rego
└── operation_operationid_test.rego
├── operation-parameters
├── operation_parameters.rego
└── operation_parameters_test.rego
├── operation-singular-tag
├── operation_singular_tag.rego
└── operation_singular_tag_test.rego
├── operation-success-response
├── operation_success_response.rego
└── operation_success_response_test.rego
├── operation-tag-defined
├── operation_tag_defined.rego
└── operation_tag_defined_test.rego
├── operation-tags
├── operation_tags.rego
└── operation_tags_test.rego
├── path-declarations-must-exist
├── path_declarations_must_exist.rego
└── path_declarations_must_exist_test.rego
├── path-keys-no-trailing-slash
├── path_keys_no_trailing_slash.rego
└── path_keys_no_trailing_slash_test.rego
├── path-not-include-query
├── path_not_include_query.rego
└── path_not_include_query_test.rego
├── path-params
├── lib
│ ├── duplicate_path_param_definition_results.rego
│ ├── duplicate_var_name_in_path_results.rego
│ ├── lib.rego
│ ├── param_asymmetry_results.rego
│ ├── path_collision_results.rego
│ ├── path_param_missing_required_results.rego
│ └── path_regex.rego
├── path_params.rego
└── path_params_test.rego
└── tag-description
├── tag_description.rego
└── tag_description_test.rego
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.rego]
2 | end_of_line = lf
3 | insert_final_newline = true
4 | charset = utf-8
5 | indent_style = tab
6 | indent_size = 4
--------------------------------------------------------------------------------
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
1 | name: bundle
2 | on:
3 | push:
4 | branches: [main]
5 |
6 | jobs:
7 | test-policies:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: checkout repository
11 | uses: actions/checkout@v3
12 | - name: opa fmt
13 | uses: docker://openpolicyagent/opa:0.45.0
14 | with:
15 | args: fmt --list --fail ./src
16 | - name: opa check
17 | uses: docker://openpolicyagent/opa:0.45.0
18 | with:
19 | args: check --strict --bundle ./src
20 | - name: opa test
21 | uses: docker://openpolicyagent/opa:0.45.0
22 | with:
23 | args: test --bundle ./src --verbose
24 | build-and-push:
25 | needs: test-policies
26 | runs-on: ubuntu-latest
27 | permissions:
28 | packages: write
29 | steps:
30 | - name: checkout repository
31 | uses: actions/checkout@v3
32 | - name: build bundle
33 | run: tar -czvf bundle.tar.gz -C ./src .
34 | - name: oras login and push
35 | env:
36 | GITHUB_OWNER: ${{ github.repository_owner }}
37 | GITHUB_REPO: ${{ github.repository }}
38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
39 | run: |
40 | echo $GITHUB_TOKEN | oras login ghcr.io -u $GITHUB_OWNER --password-stdin
41 | echo '{}' > manifest-config.json
42 | oras push ghcr.io/$GITHUB_REPO:latest \
43 | --config manifest-config.json:application/vnd.oci.image.config.v1+json \
44 | bundle.tar.gz:application/vnd.oci.image.layer.v1.tar+gzip
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | bundle.tar.gz
2 | example/conftest/policy
3 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["tsandall.opa"]
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "opa.checkOnSave": true,
3 | "opa.strictMode": true,
4 | "opa.roots": ["${workspaceFolder}/src"]
5 | }
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | check:
2 | opa check --strict ./src
3 | fmt:
4 | opa fmt --list --fail ./src
5 | test:
6 | opa test -b ./src
7 | bundle:
8 | tar -czvf bundle.tar.gz -C ./src .
9 | push:
10 | oras push ghcr.io/kevinswiber/spego:latest --config manifest-config.json:application/vnd.oci.image.config.v1+json bundle.tar.gz:application/vnd.oci.image.layer.v1.tar+gzip
11 | docs:
12 | opa inspect -a ./src > docs/policies.txt
13 | clean:
14 | rm bundle.tar.gz
15 |
16 | .PHONY: check fmt test bundle push docs clean
17 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # spego
2 |
3 | [](https://github.com/kevinswiber/spego/actions/workflows/test.yaml)
4 |
5 | A set of policies for Open Policy Agent to validate OpenAPI definitions.
6 |
7 | - Use `opa eval` to execute policies against OpenAPI documents
8 | - Built-in support for [Conftest](https://conftest.dev)
9 | - Based on the rules used by [Spectral](https://github.com/stoplightio/spectral).
10 |
11 | ## Usage
12 |
13 | Spego may be used as an Open Policy Agent (OPA) bundle. First, be sure to follow the [instructions to install OPA](https://www.openpolicyagent.org/docs/latest/#1-download-opa).
14 |
15 | ```sh
16 | git clone git@github.com:kevinswiber/spego.git
17 | opa eval \
18 | --bundle ./spego/src \
19 | --format pretty \
20 | --input ./spego/example/inputs/openapi.json \
21 | "data.openapi.main.results"
22 | ```
23 |
24 | Spego can also be used with Conftest. First, be sure to follow the [instructions to download Conftest](https://www.conftest.dev/install/).
25 |
26 | ```sh
27 | conftest pull git::https://github.com/kevinswiber/spego//src
28 | conftest test -n "openapi.main" ./openapi.json
29 | ```
30 |
31 | Note: Policy execution can be configured by adding a data file named `data.openapi.ruleset` as defined in the [Configuration](#configuration) section.
32 |
33 | ## Configuration
34 |
35 | - Supports a subset of Spectral custom rulesets, including [Changing Rule Severity](https://meta.stoplight.io/docs/spectral/e5b9616d6d50c-custom-rulesets#changing-rule-severity), [Recommended or All](https://meta.stoplight.io/docs/spectral/e5b9616d6d50c-custom-rulesets#recommended-or-all), [Disabling Rules](https://meta.stoplight.io/docs/spectral/e5b9616d6d50c-custom-rulesets#disabling-rules), and [Enabling Rules](https://meta.stoplight.io/docs/spectral/e5b9616d6d50c-custom-rulesets#enabling-rules).
36 | - Included as data in Open Policy Agent, under `data.openapi.ruleset`.
37 | - Configuration is optional. The recommended policies are executed by default.
38 |
39 | ### Example configuration
40 |
41 | ```yaml
42 | openapi:
43 | ruleset:
44 | extends: [['spego:oas', 'recommended']]
45 | rules:
46 | operation-success-response: error
47 | openapi-tags: true
48 | ```
49 |
50 | ## Policies (Rules)
51 |
52 | ### contact-properties
53 |
54 | Contact object must have "name", "url" and "email".
55 |
56 | ### duplicated-entry-in-enum
57 |
58 | Recommended: true
59 |
60 | Enum values must not have duplicate entry.
61 |
62 | ### info-contact
63 |
64 | Recommended: true
65 |
66 | Info object must have "contact" object.
67 |
68 | ### info-description
69 |
70 | Recommended: true
71 |
72 | Info "description" must be present and non-empty string.
73 |
74 | ### info-license
75 |
76 | Info object must have "license" object.
77 |
78 | ### license-url
79 |
80 | License object must include "url".
81 |
82 | ### no-eval-in-markdown
83 |
84 | Recommended: true
85 |
86 | Markdown descriptions must not have "eval(".
87 |
88 | ### no-script-tags-in-markdown
89 |
90 | Recommended: true
91 |
92 | Markdown descriptions must not have " friend",
22 | "operationId": "createUser",
23 | "responses": {
24 | "400": {}
25 | },
26 | "parameters": [
27 | { "name": "a", "in": "body" },
28 | { "name": "a", "in": "formData" }
29 | ],
30 | "tags": ["user"]
31 | },
32 | "get": {
33 | "summary": "Sample endpoint: Returns details about a particular user",
34 | "operationId": "listUser",
35 | "tags": ["user"],
36 | "parameters": [
37 | {
38 | "name": "id",
39 | "in": "query",
40 | "description": "ID of the user",
41 | "required": true,
42 | "schema": {
43 | "type": "integer",
44 | "format": "int32"
45 | }
46 | }
47 | ],
48 | "responses": {
49 | "400": {
50 | "description": "Sample response: Details about a user by ID",
51 | "headers": {
52 | "x-next": {
53 | "description": "A link to the next page of responses",
54 | "schema": {
55 | "type": "string"
56 | }
57 | }
58 | },
59 | "content": {
60 | "application/json": {
61 | "schema": {
62 | "$ref": "#/components/schemas/Error"
63 | }
64 | }
65 | }
66 | },
67 | "default": {
68 | "description": "Unexpected error",
69 | "content": {
70 | "application/json": {
71 | "schema": {
72 | "$ref": "#/components/schemas/Error"
73 | }
74 | }
75 | }
76 | }
77 | }
78 | }
79 | }
80 | },
81 | "components": {
82 | "schemas": {
83 | "User": {
84 | "type": "object",
85 | "properties": {
86 | "id": {
87 | "type": "string"
88 | }
89 | }
90 | },
91 | "Error": {
92 | "type": "object",
93 | "required": ["code", "message"],
94 | "properties": {
95 | "_id": {
96 | "type": "string"
97 | },
98 | "code": {
99 | "type": "integer",
100 | "format": "int32"
101 | },
102 | "message": {
103 | "type": "string"
104 | }
105 | }
106 | }
107 | }
108 | },
109 | "security": [
110 | {
111 | "BasicAuth": []
112 | }
113 | ]
114 | }
115 |
--------------------------------------------------------------------------------
/example/opa/Makefile:
--------------------------------------------------------------------------------
1 | test:
2 | opa eval -b ../../src -d ./data.yaml -i ../inputs/openapi.json -f pretty "data.openapi.main.problems"
3 |
4 | .PHONY: test
5 |
--------------------------------------------------------------------------------
/example/opa/data.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | openapi:
3 | ruleset:
4 | extends: [spego:oas]
5 | rules:
6 | operation-success-response: warn
7 |
--------------------------------------------------------------------------------
/example/opa/server-configuration.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | github-registry:
3 | url: https://ghcr.io
4 | type: oci
5 | credentials:
6 | bearer:
7 | schema: 'Bearer'
8 | token: '${PKG_PAT}'
9 |
10 | bundles:
11 | openapi:
12 | service: github-registry
13 | resource: ghcr.io/kevinswiber/spego:latest
14 | persist: true
15 | polling:
16 | min_delay_seconds: 30
17 | max_delay_seconds: 120
18 |
--------------------------------------------------------------------------------
/example/spectral/.gitignore:
--------------------------------------------------------------------------------
1 | policy.wasm
2 | policies.json
3 | node_modules
4 |
--------------------------------------------------------------------------------
/example/spectral/README.md:
--------------------------------------------------------------------------------
1 | # spego-spectral-example
2 |
3 | This is an example of using Spectral as a front-end for executing Spego policies. One benefit is that Spectral will resolve any JSON References and also point to line and character information showing where problems have occurred.
4 |
5 | Feel free to use this as a template for executing your own policies.
6 |
7 | ## Usage
8 |
9 | You can run this specific ruleset via a URL.
10 |
11 | ```sh
12 | spectral lint \
13 | -r https://raw.githubusercontent.com/kevinswiber/spego/main/example/spectral/ruleset.js \
14 | openapi.json
15 | ```
16 |
17 | But more than likely, you'll want to bundle your own policies and can use this as a template.
18 |
19 | Be sure to follow the [Spectral install instructions](https://meta.stoplight.io/docs/spectral/b8391e051b7d8-installation).
20 |
21 | ```sh
22 | git clone git@github.com:kevinswiber/spego.git
23 | cd ./spego/example/spectral
24 | npm install
25 | npm run build
26 | spectral lint -r ./ruleset.js ../inputs/openapi.json
27 | ```
28 |
29 | Furthermore, this ruleset can be extended. See the `example` directory. Example extended Spectral ruleset:
30 |
31 | ```yaml
32 | extends: [['../ruleset.js', 'recommended']]
33 | rules:
34 | no-script-tags-in-markdown: error
35 | no-eval-in-markdown: error
36 | path-params: false
37 | ```
38 |
--------------------------------------------------------------------------------
/example/spectral/build.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | echo "1. Building OPA bundle..."
4 | opa build \
5 | --target wasm \
6 | --bundle ../../src \
7 | --entrypoint "openapi/main/problems" \
8 | --output ./build/bundle.tar.gz
9 |
10 | echo "2. Extracting policy.wasm from bundle..."
11 | tar -xzf ./build/bundle.tar.gz -C ./build /policy.wasm 2>&1 | grep -v "Removing leading"
12 |
13 | echo "3. Converting Wasm file into a base64-decoded string: ./wasm.js"
14 | wasm=$(base64 ./build/policy.wasm)
15 | echo "exports.policyWasmBuffer = Buffer.from(\`$wasm\`, 'base64');" >./wasm.js
16 |
17 | echo "4. Extracting annotations: ./policies.js"
18 | policies=$(opa inspect --annotations --format json ../../src)
19 | echo "module.exports = $policies;" >./policies.js
20 |
21 | echo "Done!"
22 |
--------------------------------------------------------------------------------
/example/spectral/build/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kevinswiber/spego/5d9c41a4d3eb1d988a880377d7963cd09eb05f62/example/spectral/build/.gitkeep
--------------------------------------------------------------------------------
/example/spectral/example/.spectral.yaml:
--------------------------------------------------------------------------------
1 | extends: [['../ruleset.js', 'recommended']]
2 | rules:
3 | no-script-tags-in-markdown: error
4 | no-eval-in-markdown: error
5 | path-params: false
6 |
--------------------------------------------------------------------------------
/example/spectral/example/openapi.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.0.3",
3 | "info": {
4 | "title": "abcd eval('hi') abcd",
5 | "version": "1.0.0",
6 | "description": "Echo Service",
7 | "license": {
8 | "name": "MIT"
9 | },
10 | "contact": {}
11 | },
12 | "servers": [
13 | {
14 | "url": "localhost:3000"
15 | }
16 | ],
17 | "tags": [{ "name": "users" }],
18 | "paths": {
19 | "/user/{userId}": {
20 | "post": {
21 | "description": "hi friend",
22 | "operationId": "createUser",
23 | "responses": {
24 | "400": {}
25 | },
26 | "parameters": [
27 | { "name": "a", "in": "body" },
28 | { "name": "a", "in": "formData" }
29 | ],
30 | "tags": ["user"]
31 | },
32 | "get": {
33 | "summary": "Sample endpoint: Returns details about a particular user",
34 | "operationId": "listUser",
35 | "tags": ["user"],
36 | "parameters": [
37 | {
38 | "name": "id",
39 | "in": "query",
40 | "description": "ID of the user",
41 | "required": true,
42 | "schema": {
43 | "type": "integer",
44 | "format": "int32"
45 | }
46 | }
47 | ],
48 | "responses": {
49 | "400": {
50 | "description": "Sample response: Details about a user by ID",
51 | "headers": {
52 | "x-next": {
53 | "description": "A link to the next page of responses",
54 | "schema": {
55 | "type": "string"
56 | }
57 | }
58 | },
59 | "content": {
60 | "application/json": {
61 | "schema": {
62 | "$ref": "#/components/schemas/Error"
63 | }
64 | }
65 | }
66 | },
67 | "default": {
68 | "description": "Unexpected error",
69 | "content": {
70 | "application/json": {
71 | "schema": {
72 | "$ref": "#/components/schemas/Error"
73 | }
74 | }
75 | }
76 | }
77 | }
78 | }
79 | }
80 | },
81 | "components": {
82 | "schemas": {
83 | "User": {
84 | "type": "object",
85 | "properties": {
86 | "id": {
87 | "type": "string"
88 | }
89 | }
90 | },
91 | "Error": {
92 | "type": "object",
93 | "required": ["code", "message"],
94 | "properties": {
95 | "_id": {
96 | "type": "string"
97 | },
98 | "code": {
99 | "type": "integer",
100 | "format": "int32"
101 | },
102 | "message": {
103 | "type": "string"
104 | }
105 | }
106 | }
107 | }
108 | },
109 | "security": [
110 | {
111 | "BasicAuth": []
112 | }
113 | ]
114 | }
115 |
--------------------------------------------------------------------------------
/example/spectral/functions/opa.js:
--------------------------------------------------------------------------------
1 | export async function opa(input, { code, policy }) {
2 | policy.setData({
3 | openapi: {
4 | ruleset: {
5 | extends: [['spego:oas', 'off']],
6 | rules: { [code]: true },
7 | },
8 | },
9 | });
10 |
11 | try {
12 | const result = policy.evaluate(input);
13 | return result[0].result;
14 | } catch (err) {
15 | console.error('Error executing OPA policy:', err);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/example/spectral/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "spego-spectral-example",
3 | "private": true,
4 | "version": "1.0.0",
5 | "description": "An example of using Spego Wasm bundles in Spectral",
6 | "main": "ruleset.js",
7 | "scripts": {
8 | "build": "./build.sh",
9 | "clean": "rimraf ./build/bundle.tar.gz ./build/policy.wasm"
10 | },
11 | "keywords": [],
12 | "author": "Kevin Swiber ",
13 | "license": "Apache-2.0",
14 | "devDependencies": {
15 | "rimraf": "^3.0.2"
16 | },
17 | "dependencies": {
18 | "@stoplight/spectral-formats": "^1.4.0"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/example/spectral/policies.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "manifest": {
3 | "revision": "0.1.0",
4 | "roots": [
5 | "openapi/lib",
6 | "openapi/main",
7 | "openapi/policies"
8 | ]
9 | },
10 | "signatures_config": {},
11 | "namespaces": {
12 | "data.openapi.lib": [
13 | "../../src/openapi/lib/lib.rego"
14 | ],
15 | "data.openapi.main": [
16 | "../../src/openapi/main/conftest.rego",
17 | "../../src/openapi/main/main.rego",
18 | "../../src/openapi/main/main_test.rego",
19 | "../../src/openapi/main/recommended.rego",
20 | "../../src/openapi/main/ruleset.rego"
21 | ],
22 | "data.openapi.policies[\"contact-properties\"]": [
23 | "../../src/openapi/policies/contact-properties/contact_properties.rego",
24 | "../../src/openapi/policies/contact-properties/contact_properties_test.rego"
25 | ],
26 | "data.openapi.policies[\"duplicated-entry-in-enum\"]": [
27 | "../../src/openapi/policies/duplicated-entry-in-enum/duplicated_entry_in_enum.rego",
28 | "../../src/openapi/policies/duplicated-entry-in-enum/duplicated_entry_in_enum_test.rego"
29 | ],
30 | "data.openapi.policies[\"info-contact\"]": [
31 | "../../src/openapi/policies/info-contact/info_contact.rego",
32 | "../../src/openapi/policies/info-contact/info_contact_test.rego"
33 | ],
34 | "data.openapi.policies[\"info-description\"]": [
35 | "../../src/openapi/policies/info-description/info_description.rego",
36 | "../../src/openapi/policies/info-description/info_description_test.rego"
37 | ],
38 | "data.openapi.policies[\"info-license\"]": [
39 | "../../src/openapi/policies/info-license/info_license.rego",
40 | "../../src/openapi/policies/info-license/info_license_test.rego"
41 | ],
42 | "data.openapi.policies[\"license-url\"]": [
43 | "../../src/openapi/policies/license-url/license_url.rego",
44 | "../../src/openapi/policies/license-url/license_url_test.rego"
45 | ],
46 | "data.openapi.policies[\"no-eval-in-markdown\"]": [
47 | "../../src/openapi/policies/no-eval-in-markdown/no_eval_in_markdown.rego",
48 | "../../src/openapi/policies/no-eval-in-markdown/no_eval_in_markdown_test.rego"
49 | ],
50 | "data.openapi.policies[\"no-script-tags-in-markdown\"]": [
51 | "../../src/openapi/policies/no-script-tags-in-markdown/no_script_tags_in_markdown.rego",
52 | "../../src/openapi/policies/no-script-tags-in-markdown/no_script_tags_in_markdown_test.rego"
53 | ],
54 | "data.openapi.policies[\"openapi-tags\"]": [
55 | "../../src/openapi/policies/openapi-tags/openapi_tags.rego",
56 | "../../src/openapi/policies/openapi-tags/openapi_tags_test.rego"
57 | ],
58 | "data.openapi.policies[\"openapi-tags-uniqueness\"]": [
59 | "../../src/openapi/policies/openapi-tags-uniqueness/openapi_tags_uniqueness.rego",
60 | "../../src/openapi/policies/openapi-tags-uniqueness/openapi_tags_uniqueness_test.rego"
61 | ],
62 | "data.openapi.policies[\"operation-description\"]": [
63 | "../../src/openapi/policies/operation-description/operation_description.rego",
64 | "../../src/openapi/policies/operation-description/operation_description_test.rego"
65 | ],
66 | "data.openapi.policies[\"operation-operationId\"]": [
67 | "../../src/openapi/policies/operation-operationId/operation_operationid.rego",
68 | "../../src/openapi/policies/operation-operationId/operation_operationid_test.rego"
69 | ],
70 | "data.openapi.policies[\"operation-operationId-unique\"]": [
71 | "../../src/openapi/policies/operation-operationId-unique/operation_operationid_unique.rego",
72 | "../../src/openapi/policies/operation-operationId-unique/operation_operationid_unique_test.rego"
73 | ],
74 | "data.openapi.policies[\"operation-operationId-valid-in-url\"]": [
75 | "../../src/openapi/policies/operation-operationId-valid-in-url/operation_operationid_valid_in_url.rego",
76 | "../../src/openapi/policies/operation-operationId-valid-in-url/operation_operationid_valid_in_url_test.rego"
77 | ],
78 | "data.openapi.policies[\"operation-parameters\"]": [
79 | "../../src/openapi/policies/operation-parameters/operation_parameters.rego",
80 | "../../src/openapi/policies/operation-parameters/operation_parameters_test.rego"
81 | ],
82 | "data.openapi.policies[\"operation-singular-tag\"]": [
83 | "../../src/openapi/policies/operation-singular-tag/operation_singular_tag.rego",
84 | "../../src/openapi/policies/operation-singular-tag/operation_singular_tag_test.rego"
85 | ],
86 | "data.openapi.policies[\"operation-success-response\"]": [
87 | "../../src/openapi/policies/operation-success-response/operation_success_response.rego",
88 | "../../src/openapi/policies/operation-success-response/operation_success_response_test.rego"
89 | ],
90 | "data.openapi.policies[\"operation-tag-defined\"]": [
91 | "../../src/openapi/policies/operation-tag-defined/operation_tag_defined.rego",
92 | "../../src/openapi/policies/operation-tag-defined/operation_tag_defined_test.rego"
93 | ],
94 | "data.openapi.policies[\"operation-tags\"]": [
95 | "../../src/openapi/policies/operation-tags/operation_tags.rego",
96 | "../../src/openapi/policies/operation-tags/operation_tags_test.rego"
97 | ],
98 | "data.openapi.policies[\"path-declarations-must-exist\"]": [
99 | "../../src/openapi/policies/path-declarations-must-exist/path_declarations_must_exist.rego",
100 | "../../src/openapi/policies/path-declarations-must-exist/path_declarations_must_exist_test.rego"
101 | ],
102 | "data.openapi.policies[\"path-keys-no-trailing-slash\"]": [
103 | "../../src/openapi/policies/path-keys-no-trailing-slash/path_keys_no_trailing_slash.rego",
104 | "../../src/openapi/policies/path-keys-no-trailing-slash/path_keys_no_trailing_slash_test.rego"
105 | ],
106 | "data.openapi.policies[\"path-not-include-query\"]": [
107 | "../../src/openapi/policies/path-not-include-query/path_not_include_query.rego",
108 | "../../src/openapi/policies/path-not-include-query/path_not_include_query_test.rego"
109 | ],
110 | "data.openapi.policies[\"path-params\"]": [
111 | "../../src/openapi/policies/path-params/path_params.rego",
112 | "../../src/openapi/policies/path-params/path_params_test.rego"
113 | ],
114 | "data.openapi.policies[\"path-params\"].lib": [
115 | "../../src/openapi/policies/path-params/lib/duplicate_path_param_definition_results.rego",
116 | "../../src/openapi/policies/path-params/lib/duplicate_var_name_in_path_results.rego",
117 | "../../src/openapi/policies/path-params/lib/lib.rego",
118 | "../../src/openapi/policies/path-params/lib/param_asymmetry_results.rego",
119 | "../../src/openapi/policies/path-params/lib/path_collision_results.rego",
120 | "../../src/openapi/policies/path-params/lib/path_param_missing_required_results.rego",
121 | "../../src/openapi/policies/path-params/lib/path_regex.rego"
122 | ],
123 | "data.openapi.policies[\"tag-description\"]": [
124 | "../../src/openapi/policies/tag-description/tag_description.rego",
125 | "../../src/openapi/policies/tag-description/tag_description_test.rego"
126 | ]
127 | },
128 | "annotations": [
129 | {
130 | "location": {
131 | "file": "../../src/openapi/main/main.rego",
132 | "row": 9,
133 | "col": 1
134 | },
135 | "path": [
136 | {
137 | "type": "var",
138 | "value": "data"
139 | },
140 | {
141 | "type": "string",
142 | "value": "openapi"
143 | },
144 | {
145 | "type": "string",
146 | "value": "main"
147 | },
148 | {
149 | "type": "string",
150 | "value": "problems"
151 | }
152 | ],
153 | "annotations": {
154 | "scope": "rule",
155 | "title": "problems",
156 | "description": "Returns all non-successful rule validation results."
157 | }
158 | },
159 | {
160 | "location": {
161 | "file": "../../src/openapi/main/main.rego",
162 | "row": 28,
163 | "col": 1
164 | },
165 | "path": [
166 | {
167 | "type": "var",
168 | "value": "data"
169 | },
170 | {
171 | "type": "string",
172 | "value": "openapi"
173 | },
174 | {
175 | "type": "string",
176 | "value": "main"
177 | },
178 | {
179 | "type": "string",
180 | "value": "successes"
181 | }
182 | ],
183 | "annotations": {
184 | "scope": "rule",
185 | "title": "successes",
186 | "description": "Returns all successful rule validation results."
187 | }
188 | },
189 | {
190 | "location": {
191 | "file": "../../src/openapi/main/conftest.rego",
192 | "row": 12,
193 | "col": 1
194 | },
195 | "path": [
196 | {
197 | "type": "var",
198 | "value": "data"
199 | },
200 | {
201 | "type": "string",
202 | "value": "openapi"
203 | },
204 | {
205 | "type": "string",
206 | "value": "main"
207 | },
208 | {
209 | "type": "string",
210 | "value": "warn"
211 | }
212 | ],
213 | "annotations": {
214 | "scope": "rule",
215 | "title": "warn",
216 | "description": "Conftest-compatible warn rule."
217 | }
218 | },
219 | {
220 | "location": {
221 | "file": "../../src/openapi/main/main.rego",
222 | "row": 39,
223 | "col": 1
224 | },
225 | "path": [
226 | {
227 | "type": "var",
228 | "value": "data"
229 | },
230 | {
231 | "type": "string",
232 | "value": "openapi"
233 | },
234 | {
235 | "type": "string",
236 | "value": "main"
237 | },
238 | {
239 | "type": "string",
240 | "value": "results"
241 | }
242 | ],
243 | "annotations": {
244 | "scope": "rule",
245 | "title": "results",
246 | "description": "Returns all successful and non-successful rule validation results."
247 | }
248 | },
249 | {
250 | "location": {
251 | "file": "../../src/openapi/main/conftest.rego",
252 | "row": 30,
253 | "col": 1
254 | },
255 | "path": [
256 | {
257 | "type": "var",
258 | "value": "data"
259 | },
260 | {
261 | "type": "string",
262 | "value": "openapi"
263 | },
264 | {
265 | "type": "string",
266 | "value": "main"
267 | },
268 | {
269 | "type": "string",
270 | "value": "violation"
271 | }
272 | ],
273 | "annotations": {
274 | "scope": "rule",
275 | "title": "violation",
276 | "description": "Conftest-compatible violation rule."
277 | }
278 | },
279 | {
280 | "location": {
281 | "file": "../../src/openapi/policies/contact-properties/contact_properties.rego",
282 | "row": 10,
283 | "col": 1
284 | },
285 | "path": [
286 | {
287 | "type": "var",
288 | "value": "data"
289 | },
290 | {
291 | "type": "string",
292 | "value": "openapi"
293 | },
294 | {
295 | "type": "string",
296 | "value": "policies"
297 | },
298 | {
299 | "type": "string",
300 | "value": "contact-properties"
301 | },
302 | {
303 | "type": "string",
304 | "value": "results"
305 | }
306 | ],
307 | "annotations": {
308 | "scope": "rule",
309 | "title": "contact-properties",
310 | "description": "Contact object must have \"name\", \"url\" and \"email\".",
311 | "custom": {
312 | "recommended": false
313 | }
314 | }
315 | },
316 | {
317 | "location": {
318 | "file": "../../src/openapi/policies/duplicated-entry-in-enum/duplicated_entry_in_enum.rego",
319 | "row": 9,
320 | "col": 1
321 | },
322 | "path": [
323 | {
324 | "type": "var",
325 | "value": "data"
326 | },
327 | {
328 | "type": "string",
329 | "value": "openapi"
330 | },
331 | {
332 | "type": "string",
333 | "value": "policies"
334 | },
335 | {
336 | "type": "string",
337 | "value": "duplicated-entry-in-enum"
338 | },
339 | {
340 | "type": "string",
341 | "value": "results"
342 | }
343 | ],
344 | "annotations": {
345 | "scope": "rule",
346 | "title": "duplicated-entry-in-enum",
347 | "description": "Enum values must not have duplicate entry."
348 | }
349 | },
350 | {
351 | "location": {
352 | "file": "../../src/openapi/policies/info-contact/info_contact.rego",
353 | "row": 8,
354 | "col": 1
355 | },
356 | "path": [
357 | {
358 | "type": "var",
359 | "value": "data"
360 | },
361 | {
362 | "type": "string",
363 | "value": "openapi"
364 | },
365 | {
366 | "type": "string",
367 | "value": "policies"
368 | },
369 | {
370 | "type": "string",
371 | "value": "info-contact"
372 | },
373 | {
374 | "type": "string",
375 | "value": "results"
376 | }
377 | ],
378 | "annotations": {
379 | "scope": "rule",
380 | "title": "info-contact",
381 | "description": "Info object must have \"contact\" object."
382 | }
383 | },
384 | {
385 | "location": {
386 | "file": "../../src/openapi/policies/info-description/info_description.rego",
387 | "row": 8,
388 | "col": 1
389 | },
390 | "path": [
391 | {
392 | "type": "var",
393 | "value": "data"
394 | },
395 | {
396 | "type": "string",
397 | "value": "openapi"
398 | },
399 | {
400 | "type": "string",
401 | "value": "policies"
402 | },
403 | {
404 | "type": "string",
405 | "value": "info-description"
406 | },
407 | {
408 | "type": "string",
409 | "value": "results"
410 | }
411 | ],
412 | "annotations": {
413 | "scope": "rule",
414 | "title": "info-description",
415 | "description": "Info \"description\" must be present and non-empty string."
416 | }
417 | },
418 | {
419 | "location": {
420 | "file": "../../src/openapi/policies/info-license/info_license.rego",
421 | "row": 10,
422 | "col": 1
423 | },
424 | "path": [
425 | {
426 | "type": "var",
427 | "value": "data"
428 | },
429 | {
430 | "type": "string",
431 | "value": "openapi"
432 | },
433 | {
434 | "type": "string",
435 | "value": "policies"
436 | },
437 | {
438 | "type": "string",
439 | "value": "info-license"
440 | },
441 | {
442 | "type": "string",
443 | "value": "results"
444 | }
445 | ],
446 | "annotations": {
447 | "scope": "rule",
448 | "title": "info-license",
449 | "description": "Info object must have \"license\" object.",
450 | "custom": {
451 | "recommended": false
452 | }
453 | }
454 | },
455 | {
456 | "location": {
457 | "file": "../../src/openapi/policies/license-url/license_url.rego",
458 | "row": 10,
459 | "col": 1
460 | },
461 | "path": [
462 | {
463 | "type": "var",
464 | "value": "data"
465 | },
466 | {
467 | "type": "string",
468 | "value": "openapi"
469 | },
470 | {
471 | "type": "string",
472 | "value": "policies"
473 | },
474 | {
475 | "type": "string",
476 | "value": "license-url"
477 | },
478 | {
479 | "type": "string",
480 | "value": "results"
481 | }
482 | ],
483 | "annotations": {
484 | "scope": "rule",
485 | "title": "license-url",
486 | "description": "License object must include \"url\".",
487 | "custom": {
488 | "recommended": false
489 | }
490 | }
491 | },
492 | {
493 | "location": {
494 | "file": "../../src/openapi/policies/no-eval-in-markdown/no_eval_in_markdown.rego",
495 | "row": 9,
496 | "col": 1
497 | },
498 | "path": [
499 | {
500 | "type": "var",
501 | "value": "data"
502 | },
503 | {
504 | "type": "string",
505 | "value": "openapi"
506 | },
507 | {
508 | "type": "string",
509 | "value": "policies"
510 | },
511 | {
512 | "type": "string",
513 | "value": "no-eval-in-markdown"
514 | },
515 | {
516 | "type": "string",
517 | "value": "results"
518 | }
519 | ],
520 | "annotations": {
521 | "scope": "rule",
522 | "title": "no-eval-in-markdown",
523 | "description": "Markdown descriptions must not have \"eval(\"."
524 | }
525 | },
526 | {
527 | "location": {
528 | "file": "../../src/openapi/policies/no-script-tags-in-markdown/no_script_tags_in_markdown.rego",
529 | "row": 9,
530 | "col": 1
531 | },
532 | "path": [
533 | {
534 | "type": "var",
535 | "value": "data"
536 | },
537 | {
538 | "type": "string",
539 | "value": "openapi"
540 | },
541 | {
542 | "type": "string",
543 | "value": "policies"
544 | },
545 | {
546 | "type": "string",
547 | "value": "no-script-tags-in-markdown"
548 | },
549 | {
550 | "type": "string",
551 | "value": "results"
552 | }
553 | ],
554 | "annotations": {
555 | "scope": "rule",
556 | "title": "no-script-tags-in-markdown",
557 | "description": "Markdown descriptions must not have \"\u003cscript\u003e\" tags."
558 | }
559 | },
560 | {
561 | "location": {
562 | "file": "../../src/openapi/policies/openapi-tags-uniqueness/openapi_tags_uniqueness.rego",
563 | "row": 9,
564 | "col": 1
565 | },
566 | "path": [
567 | {
568 | "type": "var",
569 | "value": "data"
570 | },
571 | {
572 | "type": "string",
573 | "value": "openapi"
574 | },
575 | {
576 | "type": "string",
577 | "value": "policies"
578 | },
579 | {
580 | "type": "string",
581 | "value": "openapi-tags-uniqueness"
582 | },
583 | {
584 | "type": "string",
585 | "value": "results"
586 | }
587 | ],
588 | "annotations": {
589 | "scope": "rule",
590 | "title": "openapi-tags-uniqueness",
591 | "description": "Each tag must have a unique name."
592 | }
593 | },
594 | {
595 | "location": {
596 | "file": "../../src/openapi/policies/openapi-tags/openapi_tags.rego",
597 | "row": 10,
598 | "col": 1
599 | },
600 | "path": [
601 | {
602 | "type": "var",
603 | "value": "data"
604 | },
605 | {
606 | "type": "string",
607 | "value": "openapi"
608 | },
609 | {
610 | "type": "string",
611 | "value": "policies"
612 | },
613 | {
614 | "type": "string",
615 | "value": "openapi-tags"
616 | },
617 | {
618 | "type": "string",
619 | "value": "results"
620 | }
621 | ],
622 | "annotations": {
623 | "scope": "rule",
624 | "title": "openapi-tags",
625 | "description": "OpenAPI object must have non-empty \"tags\" array.",
626 | "custom": {
627 | "recommended": false
628 | }
629 | }
630 | },
631 | {
632 | "location": {
633 | "file": "../../src/openapi/policies/operation-description/operation_description.rego",
634 | "row": 8,
635 | "col": 1
636 | },
637 | "path": [
638 | {
639 | "type": "var",
640 | "value": "data"
641 | },
642 | {
643 | "type": "string",
644 | "value": "openapi"
645 | },
646 | {
647 | "type": "string",
648 | "value": "policies"
649 | },
650 | {
651 | "type": "string",
652 | "value": "operation-description"
653 | },
654 | {
655 | "type": "string",
656 | "value": "results"
657 | }
658 | ],
659 | "annotations": {
660 | "scope": "rule",
661 | "title": "operation-description",
662 | "description": "Operation \"description\" must be present and non-empty string."
663 | }
664 | },
665 | {
666 | "location": {
667 | "file": "../../src/openapi/policies/operation-operationId/operation_operationid.rego",
668 | "row": 8,
669 | "col": 1
670 | },
671 | "path": [
672 | {
673 | "type": "var",
674 | "value": "data"
675 | },
676 | {
677 | "type": "string",
678 | "value": "openapi"
679 | },
680 | {
681 | "type": "string",
682 | "value": "policies"
683 | },
684 | {
685 | "type": "string",
686 | "value": "operation-operationId"
687 | },
688 | {
689 | "type": "string",
690 | "value": "results"
691 | }
692 | ],
693 | "annotations": {
694 | "scope": "rule",
695 | "title": "operation-operationId",
696 | "description": "Operation must have \"operationId\"."
697 | }
698 | },
699 | {
700 | "location": {
701 | "file": "../../src/openapi/policies/operation-operationId-unique/operation_operationid_unique.rego",
702 | "row": 15,
703 | "col": 1
704 | },
705 | "path": [
706 | {
707 | "type": "var",
708 | "value": "data"
709 | },
710 | {
711 | "type": "string",
712 | "value": "openapi"
713 | },
714 | {
715 | "type": "string",
716 | "value": "policies"
717 | },
718 | {
719 | "type": "string",
720 | "value": "operation-operationId-unique"
721 | },
722 | {
723 | "type": "string",
724 | "value": "results"
725 | }
726 | ],
727 | "annotations": {
728 | "scope": "rule",
729 | "title": "operation-operationId-unique",
730 | "description": "Every operation must have unique \"operationId\".",
731 | "custom": {
732 | "message": "operationId must be unique",
733 | "severity": "error"
734 | }
735 | }
736 | },
737 | {
738 | "location": {
739 | "file": "../../src/openapi/policies/operation-operationId-valid-in-url/operation_operationid_valid_in_url.rego",
740 | "row": 8,
741 | "col": 1
742 | },
743 | "path": [
744 | {
745 | "type": "var",
746 | "value": "data"
747 | },
748 | {
749 | "type": "string",
750 | "value": "openapi"
751 | },
752 | {
753 | "type": "string",
754 | "value": "policies"
755 | },
756 | {
757 | "type": "string",
758 | "value": "operation-operationId-valid-in-url"
759 | },
760 | {
761 | "type": "string",
762 | "value": "results"
763 | }
764 | ],
765 | "annotations": {
766 | "scope": "rule",
767 | "title": "operation-operationId-valid-in-url",
768 | "description": "operationId must not have characters that are invalid when used in URL."
769 | }
770 | },
771 | {
772 | "location": {
773 | "file": "../../src/openapi/policies/operation-parameters/operation_parameters.rego",
774 | "row": 8,
775 | "col": 1
776 | },
777 | "path": [
778 | {
779 | "type": "var",
780 | "value": "data"
781 | },
782 | {
783 | "type": "string",
784 | "value": "openapi"
785 | },
786 | {
787 | "type": "string",
788 | "value": "policies"
789 | },
790 | {
791 | "type": "string",
792 | "value": "operation-parameters"
793 | },
794 | {
795 | "type": "string",
796 | "value": "results"
797 | }
798 | ],
799 | "annotations": {
800 | "scope": "rule",
801 | "title": "operation-parameters",
802 | "description": "Operation parameters are unique and non-repeating."
803 | }
804 | },
805 | {
806 | "location": {
807 | "file": "../../src/openapi/policies/operation-singular-tag/operation_singular_tag.rego",
808 | "row": 10,
809 | "col": 1
810 | },
811 | "path": [
812 | {
813 | "type": "var",
814 | "value": "data"
815 | },
816 | {
817 | "type": "string",
818 | "value": "openapi"
819 | },
820 | {
821 | "type": "string",
822 | "value": "policies"
823 | },
824 | {
825 | "type": "string",
826 | "value": "operation-singular-tag"
827 | },
828 | {
829 | "type": "string",
830 | "value": "results"
831 | }
832 | ],
833 | "annotations": {
834 | "scope": "rule",
835 | "title": "operation-singular-tag",
836 | "description": "Operation must not have more than a singular tag.",
837 | "custom": {
838 | "recommended": false
839 | }
840 | }
841 | },
842 | {
843 | "location": {
844 | "file": "../../src/openapi/policies/operation-success-response/operation_success_response.rego",
845 | "row": 8,
846 | "col": 1
847 | },
848 | "path": [
849 | {
850 | "type": "var",
851 | "value": "data"
852 | },
853 | {
854 | "type": "string",
855 | "value": "openapi"
856 | },
857 | {
858 | "type": "string",
859 | "value": "policies"
860 | },
861 | {
862 | "type": "string",
863 | "value": "operation-success-response"
864 | },
865 | {
866 | "type": "string",
867 | "value": "results"
868 | }
869 | ],
870 | "annotations": {
871 | "scope": "rule",
872 | "title": "operation-success-response",
873 | "description": "Operation must have at least one \"2xx\" or \"3xx\" response."
874 | }
875 | },
876 | {
877 | "location": {
878 | "file": "../../src/openapi/policies/operation-tag-defined/operation_tag_defined.rego",
879 | "row": 9,
880 | "col": 1
881 | },
882 | "path": [
883 | {
884 | "type": "var",
885 | "value": "data"
886 | },
887 | {
888 | "type": "string",
889 | "value": "openapi"
890 | },
891 | {
892 | "type": "string",
893 | "value": "policies"
894 | },
895 | {
896 | "type": "string",
897 | "value": "operation-tag-defined"
898 | },
899 | {
900 | "type": "string",
901 | "value": "results"
902 | }
903 | ],
904 | "annotations": {
905 | "scope": "rule",
906 | "title": "operation-tag-defined",
907 | "description": "Operation tags must be defined in global tags."
908 | }
909 | },
910 | {
911 | "location": {
912 | "file": "../../src/openapi/policies/operation-tags/operation_tags.rego",
913 | "row": 8,
914 | "col": 1
915 | },
916 | "path": [
917 | {
918 | "type": "var",
919 | "value": "data"
920 | },
921 | {
922 | "type": "string",
923 | "value": "openapi"
924 | },
925 | {
926 | "type": "string",
927 | "value": "policies"
928 | },
929 | {
930 | "type": "string",
931 | "value": "operation-tags"
932 | },
933 | {
934 | "type": "string",
935 | "value": "results"
936 | }
937 | ],
938 | "annotations": {
939 | "scope": "rule",
940 | "title": "operation-tags",
941 | "description": "Operation must have non-empty \"tags\" array."
942 | }
943 | },
944 | {
945 | "location": {
946 | "file": "../../src/openapi/policies/path-declarations-must-exist/path_declarations_must_exist.rego",
947 | "row": 8,
948 | "col": 1
949 | },
950 | "path": [
951 | {
952 | "type": "var",
953 | "value": "data"
954 | },
955 | {
956 | "type": "string",
957 | "value": "openapi"
958 | },
959 | {
960 | "type": "string",
961 | "value": "policies"
962 | },
963 | {
964 | "type": "string",
965 | "value": "path-declarations-must-exist"
966 | },
967 | {
968 | "type": "string",
969 | "value": "results"
970 | }
971 | ],
972 | "annotations": {
973 | "scope": "rule",
974 | "title": "path-declarations-must-exist",
975 | "description": "Path parameter declarations must not be empty, ex.\"/given/{}\" is invalid."
976 | }
977 | },
978 | {
979 | "location": {
980 | "file": "../../src/openapi/policies/path-keys-no-trailing-slash/path_keys_no_trailing_slash.rego",
981 | "row": 8,
982 | "col": 1
983 | },
984 | "path": [
985 | {
986 | "type": "var",
987 | "value": "data"
988 | },
989 | {
990 | "type": "string",
991 | "value": "openapi"
992 | },
993 | {
994 | "type": "string",
995 | "value": "policies"
996 | },
997 | {
998 | "type": "string",
999 | "value": "path-keys-no-trailing-slash"
1000 | },
1001 | {
1002 | "type": "string",
1003 | "value": "results"
1004 | }
1005 | ],
1006 | "annotations": {
1007 | "scope": "rule",
1008 | "title": "path-keys-no-trailing-slash",
1009 | "description": "Path must not end with slash."
1010 | }
1011 | },
1012 | {
1013 | "location": {
1014 | "file": "../../src/openapi/policies/path-not-include-query/path_not_include_query.rego",
1015 | "row": 8,
1016 | "col": 1
1017 | },
1018 | "path": [
1019 | {
1020 | "type": "var",
1021 | "value": "data"
1022 | },
1023 | {
1024 | "type": "string",
1025 | "value": "openapi"
1026 | },
1027 | {
1028 | "type": "string",
1029 | "value": "policies"
1030 | },
1031 | {
1032 | "type": "string",
1033 | "value": "path-not-include-query"
1034 | },
1035 | {
1036 | "type": "string",
1037 | "value": "results"
1038 | }
1039 | ],
1040 | "annotations": {
1041 | "scope": "rule",
1042 | "title": "path-not-include-query",
1043 | "description": "Path must not include query string."
1044 | }
1045 | },
1046 | {
1047 | "location": {
1048 | "file": "../../src/openapi/policies/path-params/path_params.rego",
1049 | "row": 11,
1050 | "col": 1
1051 | },
1052 | "path": [
1053 | {
1054 | "type": "var",
1055 | "value": "data"
1056 | },
1057 | {
1058 | "type": "string",
1059 | "value": "openapi"
1060 | },
1061 | {
1062 | "type": "string",
1063 | "value": "policies"
1064 | },
1065 | {
1066 | "type": "string",
1067 | "value": "path-params"
1068 | },
1069 | {
1070 | "type": "string",
1071 | "value": "results"
1072 | }
1073 | ],
1074 | "annotations": {
1075 | "scope": "rule",
1076 | "title": "path-params",
1077 | "description": "Path parameters must be defined and valid.",
1078 | "custom": {
1079 | "severity": "error"
1080 | }
1081 | }
1082 | },
1083 | {
1084 | "location": {
1085 | "file": "../../src/openapi/policies/tag-description/tag_description.rego",
1086 | "row": 10,
1087 | "col": 1
1088 | },
1089 | "path": [
1090 | {
1091 | "type": "var",
1092 | "value": "data"
1093 | },
1094 | {
1095 | "type": "string",
1096 | "value": "openapi"
1097 | },
1098 | {
1099 | "type": "string",
1100 | "value": "policies"
1101 | },
1102 | {
1103 | "type": "string",
1104 | "value": "tag-description"
1105 | },
1106 | {
1107 | "type": "string",
1108 | "value": "results"
1109 | }
1110 | ],
1111 | "annotations": {
1112 | "scope": "rule",
1113 | "title": "tag-description",
1114 | "description": "Tag object must have \"description\".",
1115 | "custom": {
1116 | "recommended": false
1117 | }
1118 | }
1119 | }
1120 | ]
1121 | };
1122 |
--------------------------------------------------------------------------------
/example/spectral/ruleset.js:
--------------------------------------------------------------------------------
1 | import { oas2, oas3 } from '@stoplight/spectral-formats';
2 | import opaWasm from 'https://cdn.skypack.dev/@open-policy-agent/opa-wasm';
3 | import { opa } from './functions/opa.js';
4 | import { annotations } from './policies.js';
5 | import { policyWasmBuffer } from './wasm.js';
6 |
7 | const policy = opaWasm.loadPolicySync(policyWasmBuffer);
8 |
9 | const policyAnnotations = annotations
10 | .filter(
11 | (policy) => policy.location.file.includes('policies') && policy.annotations
12 | )
13 | .map((policy) => policy.annotations);
14 |
15 | const rules = {};
16 |
17 | for (const { title, description, custom } of policyAnnotations) {
18 | rules[title] = {
19 | description,
20 | severity: custom?.severity,
21 | recommended: custom?.recommended,
22 | formats: [oas2, oas3],
23 | given: '$',
24 | then: {
25 | function: opa,
26 | functionOptions: {
27 | code: title,
28 | policy,
29 | },
30 | },
31 | };
32 | }
33 |
34 | export default {
35 | rules,
36 | };
37 |
--------------------------------------------------------------------------------
/src/.manifest:
--------------------------------------------------------------------------------
1 | {
2 | "revision": "0.1.0",
3 | "roots": [
4 | "openapi/lib",
5 | "openapi/main",
6 | "openapi/policies"
7 | ],
8 | "required_builtins": [
9 | "regex"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/src/openapi/lib/lib.rego:
--------------------------------------------------------------------------------
1 | package openapi.lib
2 |
3 | is_method_valid(method) {
4 | methods := {"get", "put", "post", "delete", "options", "head", "patch", "trace", "query"}
5 | methods[_] == method
6 | }
7 |
8 | format(meta, path) := result {
9 | result := merge_custom(
10 | meta,
11 | {
12 | "code": meta.title,
13 | "path": path,
14 | "message": extract_message(meta),
15 | },
16 | )
17 | }
18 |
19 | format_msg(meta, path, message) := result {
20 | result := merge_custom(meta, {
21 | "code": meta.title,
22 | "path": path,
23 | "message": message,
24 | })
25 | }
26 |
27 | merge_custom(meta, obj) := merged {
28 | not meta.custom
29 | merged := obj
30 | }
31 |
32 | merge_custom(meta, obj) := merged {
33 | custom := object.remove(meta.custom, ["recommended"])
34 | merged := object.union(custom, obj)
35 | }
36 |
37 | extract_message(meta) := msg {
38 | not meta.custom.message
39 | msg := meta.description
40 | }
41 |
42 | extract_message(meta) := msg {
43 | msg := meta.custom.message
44 | }
45 |
46 | msg(code, p_map) := msgs {
47 | p_map[code]
48 | msgs := {msg |
49 | p_map[code][result]
50 | msg := {
51 | "code": code,
52 | "message": result.message,
53 | "path": result.path,
54 | "severity": result.severity,
55 | "status": "failure",
56 | }
57 | }
58 | }
59 |
60 | msg(code, p_map) := {message} {
61 | not p_map[code]
62 | message := {
63 | "code": code,
64 | "status": "success",
65 | }
66 | }
67 |
68 | resolve_result_severity(result, _default) := severity {
69 | not result.severity
70 | severity := _default
71 | }
72 |
73 | resolve_result_severity(result, _default) := severity {
74 | severity := result.severity
75 | }
76 |
77 | resolve_rule_severity(policy_ref, _default, severity_overrides) := severity {
78 | not severity_overrides[policy_ref]
79 | severity := _default
80 | }
81 |
82 | resolve_rule_severity(policy_ref, _default, severity_overrides) := severity {
83 | severity := severity_overrides[policy_ref]
84 | }
85 |
--------------------------------------------------------------------------------
/src/openapi/main/conftest.rego:
--------------------------------------------------------------------------------
1 | package openapi.main
2 |
3 | import future.keywords.in
4 |
5 | escape(s) := t {
6 | t := replace(s, "/", "~1")
7 | }
8 |
9 | # METADATA
10 | # title: warn
11 | # description: Conftest-compatible warn rule.
12 | warn[msg] {
13 | problems[result]
14 | result.severity == "warn"
15 | escaped := [segment | some s in result.path; segment := escape(s)]
16 | pointer := sprintf("%s", [concat("/", escaped)])
17 | msg := {
18 | "msg": sprintf("%s - %s [%s]", [result.code, result.message, pointer]),
19 | "details": {
20 | "code": result.code,
21 | "severity": result.severity,
22 | "path": result.path,
23 | },
24 | }
25 | }
26 |
27 | # METADATA
28 | # title: violation
29 | # description: Conftest-compatible violation rule.
30 | violation[msg] {
31 | problems[result]
32 | result.severity == "error"
33 | escaped := [segment | some s in result.path; segment := escape(s)]
34 | pointer := sprintf("%s", [concat("/", escaped)])
35 | msg := {
36 | "msg": sprintf("%s - %s [%s]", [result.code, result.message, pointer]),
37 | "details": {
38 | "code": result.code,
39 | "severity": result.severity,
40 | "path": result.path,
41 | },
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/openapi/main/main.rego:
--------------------------------------------------------------------------------
1 | package openapi.main
2 |
3 | import data.openapi.lib
4 | import future.keywords.in
5 |
6 | # METADATA
7 | # title: problems
8 | # description: Returns all non-successful rule validation results.
9 | problems[msg] {
10 | policy_refs[policy_ref]
11 | data.openapi.policies[policy_ref].results[result]
12 | result.code == policy_ref
13 |
14 | severity_default := lib.resolve_result_severity(result, "warn")
15 | severity := lib.resolve_rule_severity(policy_ref, severity_default, severity_overrides)
16 |
17 | msg := {
18 | "code": result.code,
19 | "message": result.message,
20 | "path": result.path,
21 | "severity": severity,
22 | }
23 | }
24 |
25 | # METADATA
26 | # title: successes
27 | # description: Returns all successful rule validation results.
28 | successes[msg] {
29 | ref_codes := {code | some code in policy_refs}
30 | p_codes := {code | some problem in problems; code := problem.code}
31 | success_codes := ref_codes - p_codes
32 | success_codes[code]
33 | msg := {"code": code}
34 | }
35 |
36 | # METADATA
37 | # title: results
38 | # description: Returns all successful and non-successful rule validation results.
39 | results[message] {
40 | ref_codes := {code | some code in policy_refs}
41 | p_map := {code: results |
42 | some result in problems
43 | code := result.code
44 | results := {inner_result |
45 | problems[inner_result]
46 | inner_result.code == code
47 | }
48 | }
49 |
50 | ref_codes[code]
51 | messages := lib.msg(code, p_map)
52 | messages[message]
53 | }
54 |
--------------------------------------------------------------------------------
/src/openapi/main/main_test.rego:
--------------------------------------------------------------------------------
1 | package openapi.main
2 |
3 | test_recommended_policies_execute_by_default {
4 | a := results with input as {}
5 | count(a) == count(recommended_policy_refs)
6 | }
7 |
8 | test_recommended_policies_with_ruleset_by_default {
9 | ruleset := {"extends": ["spego:oas"]}
10 | a := results with input as {} with data.openapi.ruleset as ruleset
11 | count(a) == count(recommended_policy_refs)
12 | }
13 |
14 | test_recommended_policies_with_ruleset {
15 | ruleset := {"extends": [["spego:oas", "recommended"]]}
16 | a := results with input as {} with data.openapi.ruleset as ruleset
17 | count(a) == count(recommended_policy_refs)
18 | }
19 |
20 | test_all_policies_with_ruleset {
21 | ruleset := {"extends": [["spego:oas", "all"]]}
22 | a := results with input as {} with data.openapi.ruleset as ruleset
23 | count(a) == count(all_policy_refs)
24 | }
25 |
26 | test_ruleset_specifier_is_spectral_compatible {
27 | ruleset := {"extends": [["spectral:oas", "all"]]}
28 | a := results with input as {} with data.openapi.ruleset as ruleset
29 | count(a) == count(all_policy_refs)
30 | }
31 |
32 | test_off_policies_with_ruleset {
33 | ruleset := {"extends": [["spego:oas", "off"]]}
34 | a := results with input as {} with data.openapi.ruleset as ruleset
35 | count(a) == 0
36 | }
37 |
38 | test_enabled_policies_with_ruleset {
39 | ruleset := {
40 | "extends": [["spego:oas", "off"]],
41 | "rules": {"openapi-tags": true},
42 | }
43 |
44 | a := results with input as {} with data.openapi.ruleset as ruleset
45 | count(a) == 1
46 | }
47 |
48 | test_disabled_policies_with_ruleset {
49 | ruleset := {
50 | "extends": [["spego:oas", "all"]],
51 | "rules": {"openapi-tags": "off"},
52 | }
53 |
54 | a := results with input as {} with data.openapi.ruleset as ruleset
55 | count(a) == count(all_policy_refs) - 1
56 | }
57 |
58 | test_enabled_and_disabled_policies_with_ruleset {
59 | ruleset := {
60 | "extends": [["spego:oas", "recommended"]],
61 | "rules": {"openapi-tags": true, "path-params": "off"},
62 | }
63 |
64 | a := results with input as {} with data.openapi.ruleset as ruleset
65 | count(a) == count(recommended_policy_refs)
66 | }
67 |
68 | test_severity_override_with_ruleset {
69 | ruleset := {
70 | "extends": [["spego:oas", "all"]],
71 | "rules": {"openapi-tags": "info"},
72 | }
73 |
74 | result := {
75 | "code": "openapi-tags",
76 | "path": ["tags"],
77 | "message": "OpenAPI object must have non-empty \"tags\" array.",
78 | "severity": "info",
79 | "status": "failure",
80 | }
81 | results[result] with input as {} with data.openapi.ruleset as ruleset
82 | }
83 |
84 | test_results_includes_both_successes_and_problems {
85 | mock_input := {"paths": {"/users": {"get": {"responses": {"400": {}}}}}}
86 | ruleset := {"extends": ["spego:oas"]}
87 |
88 | problem_count := count(problems) with input as mock_input with data.openapi.ruleset as ruleset
89 | success_count := count(successes) with input as mock_input with data.openapi.ruleset as ruleset
90 | results_count := count(results) with input as mock_input with data.openapi.ruleset as ruleset
91 |
92 | problem_count > 0
93 | success_count > 0
94 | results_count == problem_count + success_count
95 | }
96 |
--------------------------------------------------------------------------------
/src/openapi/main/recommended.rego:
--------------------------------------------------------------------------------
1 | package openapi.main
2 |
3 | recommended_policy_refs := {
4 | "operation-success-response",
5 | "operation-operationId-unique",
6 | "operation-parameters",
7 | "operation-tag-defined",
8 | "path-params",
9 | "duplicated-entry-in-enum",
10 | "info-contact",
11 | "info-description",
12 | "no-eval-in-markdown",
13 | "no-script-tags-in-markdown",
14 | "openapi-tags-uniqueness",
15 | "operation-description",
16 | "operation-operationId",
17 | "operation-operationId-valid-in-url",
18 | "operation-tags",
19 | "path-declarations-must-exist",
20 | "path-keys-no-trailing-slash",
21 | "path-not-include-query",
22 | }
23 |
--------------------------------------------------------------------------------
/src/openapi/main/ruleset.rego:
--------------------------------------------------------------------------------
1 | package openapi.main
2 |
3 | severity_list := {"error", "warn", "info", "hint"}
4 |
5 | all_policy_refs := {code | data.openapi.policies[code]}
6 |
7 | policy_refs := refs {
8 | not data.openapi.ruleset
9 | refs := recommended_policy_refs
10 | }
11 |
12 | policy_refs := refs {
13 | ruleset := data.openapi.ruleset
14 | refs := policy_refs_from_ruleset(ruleset)
15 | }
16 |
17 | ruleset_specifiers := {"spego:oas", "spectral:oas"}
18 |
19 | policy_refs_from_ruleset(ruleset) := refs {
20 | is_array(ruleset.extends)
21 | specifier := ruleset.extends[0]
22 | ruleset_specifiers[specifier]
23 | rec_refs := recommended_policy_refs
24 | enabled := enabled_ruleset_overrides(ruleset)
25 | disabled := disabled_ruleset_overrides(ruleset)
26 | refs := (rec_refs | enabled) - disabled
27 | }
28 |
29 | policy_refs_from_ruleset(ruleset) := refs {
30 | is_array(ruleset.extends)
31 | is_array(ruleset.extends[0])
32 | [specifier, subset] := ruleset.extends[0]
33 | ruleset_specifiers[specifier]
34 |
35 | subset_map := {
36 | "recommended": recommended_policy_refs,
37 | "all": all_policy_refs,
38 | "off": set(),
39 | }
40 |
41 | subset_refs := subset_map[subset]
42 | enabled := enabled_ruleset_overrides(ruleset)
43 | disabled := disabled_ruleset_overrides(ruleset)
44 | refs := (subset_refs | enabled) - disabled
45 | }
46 |
47 | enabled_ruleset_overrides(ruleset) := policy_refs {
48 | not ruleset.rules
49 | policy_refs := set()
50 | }
51 |
52 | enabled_ruleset_overrides(ruleset) := policy_refs {
53 | rules := ruleset.rules
54 | valid_values := {true, "on"}
55 | policy_refs := {ref |
56 | v := rules[ref]
57 | valid_values[v]
58 | }
59 | }
60 |
61 | disabled_ruleset_overrides(ruleset) := policy_refs {
62 | not ruleset.rules
63 | policy_refs := set()
64 | }
65 |
66 | disabled_ruleset_overrides(ruleset) := policy_refs {
67 | rules := ruleset.rules
68 | valid_values := {false, "off"}
69 | policy_refs := {ref |
70 | v := rules[ref]
71 | valid_values[v]
72 | }
73 | }
74 |
75 | severity_overrides := {code: sev |
76 | rules := data.openapi.ruleset.rules
77 | sev := rules[code]
78 | severity_list[sev]
79 | }
80 |
--------------------------------------------------------------------------------
/src/openapi/policies/contact-properties/contact_properties.rego:
--------------------------------------------------------------------------------
1 | package openapi.policies["contact-properties"]
2 |
3 | import data.openapi.lib
4 |
5 | # METADATA
6 | # title: contact-properties
7 | # description: Contact object must have "name", "url" and "email".
8 | # custom:
9 | # recommended: false
10 | results[lib.format(rego.metadata.rule(), path)] {
11 | path := ["info", "contact"]
12 | contact := input.info.contact
13 |
14 | required := {"name", "email", "url"}
15 | present := {x | required[x]; contact[x]}
16 |
17 | count(present) < count(required)
18 | }
19 |
--------------------------------------------------------------------------------
/src/openapi/policies/contact-properties/contact_properties_test.rego:
--------------------------------------------------------------------------------
1 | package openapi.policies["contact-properties"]
2 |
3 | test_contact_missing_name_fails {
4 | result := {
5 | "code": "contact-properties",
6 | "path": ["info", "contact"],
7 | "message": "Contact object must have \"name\", \"url\" and \"email\".",
8 | }
9 |
10 | results[result] with input as {"info": {"contact": {
11 | "url": "https://example.org",
12 | "email": "web@example.org",
13 | }}}
14 | }
15 |
16 | test_contact_missing_email_fails {
17 | result := {
18 | "code": "contact-properties",
19 | "path": ["info", "contact"],
20 | "message": "Contact object must have \"name\", \"url\" and \"email\".",
21 | }
22 |
23 | results[result] with input as {"info": {"contact": {
24 | "name": "Example",
25 | "url": "https://example.org",
26 | }}}
27 | }
28 |
29 | test_contact_missing_url_fails {
30 | result := {
31 | "code": "contact-properties",
32 | "path": ["info", "contact"],
33 | "message": "Contact object must have \"name\", \"url\" and \"email\".",
34 | }
35 |
36 | results[result] with input as {"info": {"contact": {
37 | "name": "Example",
38 | "email": "web@example.org",
39 | }}}
40 | }
41 |
42 | test_contact_with_all_fields_succeeds {
43 | a := results with input as {"info": {"contact": {
44 | "name": "Example",
45 | "email": "web@example.org",
46 | "url": "https://example.com",
47 | }}}
48 |
49 | count(a) == 0
50 | }
51 |
--------------------------------------------------------------------------------
/src/openapi/policies/duplicated-entry-in-enum/duplicated_entry_in_enum.rego:
--------------------------------------------------------------------------------
1 | package openapi.policies["duplicated-entry-in-enum"]
2 |
3 | import data.openapi.lib
4 | import future.keywords.in
5 |
6 | # METADATA
7 | # title: duplicated-entry-in-enum
8 | # description: Enum values must not have duplicate entry.
9 | results[lib.format_msg(rego.metadata.rule(), path, message)] {
10 | enums := {[current_path, enum] |
11 | some value
12 | [current_path, value] = walk(input)
13 | enum = value.enum
14 | }
15 |
16 | some [found_path, enum] in enums
17 |
18 | some i1, i2
19 | enum[i1] == enum[i2]
20 |
21 | not i1 == i2
22 |
23 | path := array.concat(found_path, ["enum", sprintf("%d", [i1])])
24 | message := sprintf("Enum has duplicate value, \"%s\".", [enum[i1]])
25 | }
26 |
--------------------------------------------------------------------------------
/src/openapi/policies/duplicated-entry-in-enum/duplicated_entry_in_enum_test.rego:
--------------------------------------------------------------------------------
1 | package openapi.policies["duplicated-entry-in-enum"]
2 |
3 | test_duplicate_entry_in_enum_fails {
4 | result1 := {
5 | "code": "duplicated-entry-in-enum",
6 | "path": ["components", "schemas", "a", "properties", "b", "items", "enum", "0"],
7 | "message": "Enum has duplicate value, \"ALPHA\".",
8 | }
9 |
10 | result2 := {
11 | "code": "duplicated-entry-in-enum",
12 | "path": ["components", "schemas", "a", "properties", "b", "items", "enum", "2"],
13 | "message": "Enum has duplicate value, \"ALPHA\".",
14 | }
15 |
16 | res := results with input as {"components": {"schemas": {"a": {
17 | "type": "object",
18 | "properties": {"b": {
19 | "type": "array",
20 | "items": {
21 | "type": "string",
22 | "enum": ["ALPHA", "BETA", "ALPHA"],
23 | },
24 | }},
25 | }}}}
26 |
27 | res[result1]
28 | res[result2]
29 | }
30 |
--------------------------------------------------------------------------------
/src/openapi/policies/info-contact/info_contact.rego:
--------------------------------------------------------------------------------
1 | package openapi.policies["info-contact"]
2 |
3 | import data.openapi.lib
4 |
5 | # METADATA
6 | # title: info-contact
7 | # description: Info object must have "contact" object.
8 | results[lib.format(rego.metadata.rule(), path)] {
9 | path := ["info", "contact"]
10 | contact := object.get(input, path, true)
11 | not is_object(contact)
12 | }
13 |
--------------------------------------------------------------------------------
/src/openapi/policies/info-contact/info_contact_test.rego:
--------------------------------------------------------------------------------
1 | package openapi.policies["info-contact"]
2 |
3 | test_contact_missing_fails {
4 | result := {
5 | "code": "info-contact",
6 | "path": ["info", "contact"],
7 | "message": "Info object must have \"contact\" object.",
8 | }
9 |
10 | results[result] with input as {"info": {}}
11 | }
12 |
13 | test_contact_non_object_fails {
14 | result := {
15 | "code": "info-contact",
16 | "path": ["info", "contact"],
17 | "message": "Info object must have \"contact\" object.",
18 | }
19 |
20 | results[result] with input as {"info": {"contact": true}}
21 | }
22 |
23 | test_contact_exists_succeeds {
24 | a := results with input as {"info": {"contact": {}}}
25 | count(a) == 0
26 | }
27 |
--------------------------------------------------------------------------------
/src/openapi/policies/info-description/info_description.rego:
--------------------------------------------------------------------------------
1 | package openapi.policies["info-description"]
2 |
3 | import data.openapi.lib
4 |
5 | # METADATA
6 | # title: info-description
7 | # description: Info "description" must be present and non-empty string.
8 | results[lib.format(rego.metadata.rule(), path)] {
9 | path := ["info", "description"]
10 | description := object.get(input, path, true)
11 |
12 | checks := {
13 | is_string(description) == false,
14 | description == "",
15 | }
16 |
17 | checks[true]
18 | }
19 |
--------------------------------------------------------------------------------
/src/openapi/policies/info-description/info_description_test.rego:
--------------------------------------------------------------------------------
1 | package openapi.policies["info-description"]
2 |
3 | test_description_missing_fails {
4 | result := {
5 | "code": "info-description",
6 | "path": ["info", "description"],
7 | "message": "Info \"description\" must be present and non-empty string.",
8 | }
9 |
10 | results[result] with input as {"info": {}}
11 | }
12 |
13 | test_description_non_string_fails {
14 | result := {
15 | "code": "info-description",
16 | "path": ["info", "description"],
17 | "message": "Info \"description\" must be present and non-empty string.",
18 | }
19 |
20 | results[result] with input as {"info": {"description": true}}
21 | }
22 |
23 | test_description_empty_fails {
24 | result := {
25 | "code": "info-description",
26 | "path": ["info", "description"],
27 | "message": "Info \"description\" must be present and non-empty string.",
28 | }
29 |
30 | results[result] with input as {"info": {"description": ""}}
31 | }
32 |
33 | test_description_exists_succeeds {
34 | a := results with input as {"info": {"description": "hello"}}
35 | count(a) == 0
36 | }
37 |
--------------------------------------------------------------------------------
/src/openapi/policies/info-license/info_license.rego:
--------------------------------------------------------------------------------
1 | package openapi.policies["info-license"]
2 |
3 | import data.openapi.lib
4 |
5 | # METADATA
6 | # title: info-license
7 | # description: Info object must have "license" object.
8 | # custom:
9 | # recommended: false
10 | results[lib.format(rego.metadata.rule(), path)] {
11 | path := ["info", "license"]
12 | license := object.get(input, path, true)
13 | not is_object(license)
14 | }
15 |
--------------------------------------------------------------------------------
/src/openapi/policies/info-license/info_license_test.rego:
--------------------------------------------------------------------------------
1 | package openapi.policies["info-license"]
2 |
3 | test_license_missing_fails {
4 | result := {
5 | "code": "info-license",
6 | "path": ["info", "license"],
7 | "message": "Info object must have \"license\" object.",
8 | }
9 |
10 | results[result] with input as {"info": {}}
11 | }
12 |
13 | test_license_non_object_fails {
14 | result := {
15 | "code": "info-license",
16 | "path": ["info", "license"],
17 | "message": "Info object must have \"license\" object.",
18 | }
19 |
20 | results[result] with input as {"info": {"license": true}}
21 | }
22 |
23 | test_license_exists_succeeds {
24 | a := results with input as {"info": {"license": {}}}
25 | count(a) == 0
26 | }
27 |
--------------------------------------------------------------------------------
/src/openapi/policies/license-url/license_url.rego:
--------------------------------------------------------------------------------
1 | package openapi.policies["license-url"]
2 |
3 | import data.openapi.lib
4 |
5 | # METADATA
6 | # title: license-url
7 | # description: License object must include "url".
8 | # custom:
9 | # recommended: false
10 | results[lib.format(rego.metadata.rule(), path)] {
11 | path := ["info", "license", "url"]
12 | url := object.get(input, path, true)
13 |
14 | checks := {
15 | is_string(url) == false,
16 | url == "",
17 | }
18 |
19 | checks[true]
20 | }
21 |
--------------------------------------------------------------------------------
/src/openapi/policies/license-url/license_url_test.rego:
--------------------------------------------------------------------------------
1 | package openapi.policies["license-url"]
2 |
3 | test_url_missing_fails {
4 | result := {
5 | "code": "license-url",
6 | "path": ["info", "license", "url"],
7 | "message": "License object must include \"url\".",
8 | }
9 |
10 | results[result] with input as {"info": {"license": {}}}
11 | }
12 |
13 | test_url_non_string_fails {
14 | result := {
15 | "code": "license-url",
16 | "path": ["info", "license", "url"],
17 | "message": "License object must include \"url\".",
18 | }
19 |
20 | results[result] with input as {"info": {"license": {"url": true}}}
21 | }
22 |
23 | test_url_empty_fails {
24 | result := {
25 | "code": "license-url",
26 | "path": ["info", "license", "url"],
27 | "message": "License object must include \"url\".",
28 | }
29 |
30 | results[result] with input as {"info": {"license": {"url": ""}}}
31 | }
32 |
33 | test_url_exists_succeeds {
34 | a := results with input as {"info": {"license": {"url": "hello"}}}
35 | count(a) == 0
36 | }
37 |
--------------------------------------------------------------------------------
/src/openapi/policies/no-eval-in-markdown/no_eval_in_markdown.rego:
--------------------------------------------------------------------------------
1 | package openapi.policies["no-eval-in-markdown"]
2 |
3 | import data.openapi.lib
4 | import future.keywords.in
5 |
6 | # METADATA
7 | # title: no-eval-in-markdown
8 | # description: Markdown descriptions must not have "eval(".
9 | results[lib.format(rego.metadata.rule(), path)] {
10 | valid_props := {"title", "description"}
11 | titles_and_descriptions := {[md_path, md_val] |
12 | [current_path, value] := walk(input)
13 |
14 | some key
15 | valid_props[key]
16 |
17 | md_val := value[key]
18 | md_path = array.concat(current_path, [key])
19 | }
20 |
21 | some [path, val] in titles_and_descriptions
22 | contains(lower(val), "eval(")
23 | }
24 |
--------------------------------------------------------------------------------
/src/openapi/policies/no-eval-in-markdown/no_eval_in_markdown_test.rego:
--------------------------------------------------------------------------------
1 | package openapi.policies["no-eval-in-markdown"]
2 |
3 | test_eval_in_markdown_title_fails {
4 | result := {
5 | "code": "no-eval-in-markdown",
6 | "path": ["info", "title"],
7 | "message": "Markdown descriptions must not have \"eval(\".",
8 | }
9 |
10 | results[result] with input as {"info": {"title": "hi eval(\"alert('hi')\") there"}}
11 | }
12 |
13 | test_eval_in_markdown_description_fails {
14 | result := {
15 | "code": "no-eval-in-markdown",
16 | "path": ["info", "description"],
17 | "message": "Markdown descriptions must not have \"eval(\".",
18 | }
19 |
20 | results[result] with input as {"info": {"description": "hi eval(\"alert('hi')\") there"}}
21 | }
22 |
23 | test_eval_in_markdown_title_and_description_fails {
24 | mock_input := {"info": {
25 | "title": "hi eval(\"alert('hi')\") there",
26 | "description": "hi eval(\"alert('hi')\") there",
27 | }}
28 |
29 | result1 := {
30 | "code": "no-eval-in-markdown",
31 | "path": ["info", "title"],
32 | "message": "Markdown descriptions must not have \"eval(\".",
33 | }
34 |
35 | result2 := {
36 | "code": "no-eval-in-markdown",
37 | "path": ["info", "description"],
38 | "message": "Markdown descriptions must not have \"eval(\".",
39 | }
40 |
41 | res := results with input as mock_input
42 | res[result1]
43 | res[result2]
44 | }
45 |
46 | test_no_eval_in_markdown_title_succeeds {
47 | a := results with input as {"info": {"title": "hi"}}
48 | count(a) == 0
49 | }
50 |
51 | test_no_eval_in_markdown_description_succeeds {
52 | a := results with input as {"info": {"description": "hi"}}
53 | count(a) == 0
54 | }
55 |
--------------------------------------------------------------------------------
/src/openapi/policies/no-script-tags-in-markdown/no_script_tags_in_markdown.rego:
--------------------------------------------------------------------------------
1 | package openapi.policies["no-script-tags-in-markdown"]
2 |
3 | import data.openapi.lib
4 | import future.keywords.in
5 |
6 | # METADATA
7 | # title: no-script-tags-in-markdown
8 | # description: Markdown descriptions must not have " there"}}
11 | }
12 |
13 | test_script_tag_in_markdown_description_fails {
14 | result := {
15 | "code": "no-script-tags-in-markdown",
16 | "path": ["info", "description"],
17 | "message": "Markdown descriptions must not have \" there"}}
21 | }
22 |
23 | test_script_tag_in_markdown_title_and_description_fails {
24 | mock_input := {"info": {
25 | "title": "hi there",
26 | "description": "hi there",
27 | }}
28 |
29 | result1 := {
30 | "code": "no-script-tags-in-markdown",
31 | "path": ["info", "title"],
32 | "message": "Markdown descriptions must not have \"