diff --git a/.github/workflows/generate-markdown.yml b/.github/workflows/generate-markdown.yml index a59c944..7d9aca0 100644 --- a/.github/workflows/generate-markdown.yml +++ b/.github/workflows/generate-markdown.yml @@ -13,8 +13,8 @@ jobs: auto-update-readme: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v5 - name: Markdown autodocs - uses: dineshsonachalam/markdown-autodocs@v1.0.4 + uses: dineshsonachalam/markdown-autodocs@v1.0.7 with: output_file_paths: '[./README.md, ./docs/*.md]' diff --git a/.github/workflows/go_legacy.yml b/.github/workflows/go-oldstable.yml similarity index 77% rename from .github/workflows/go_legacy.yml rename to .github/workflows/go-oldstable.yml index a86368d..75fd280 100644 --- a/.github/workflows/go_legacy.yml +++ b/.github/workflows/go-oldstable.yml @@ -1,4 +1,4 @@ -name: Go +name: Go Test (oldstable) on: push: @@ -12,10 +12,10 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v6 with: go-version: 'oldstable' diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 95ee8c7..a9cdcf2 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -12,12 +12,13 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@v5 - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v6 with: - go-version: 'stable' + go-version: "stable" - name: Build run: go build -v ./cmd/... @@ -26,15 +27,36 @@ jobs: run: go vet ./... - name: gofmt - uses: Jerome1337/gofmt-action@v1.0.4 + uses: Jerome1337/gofmt-action@v1.0.5 with: gofmt-flags: "-l -d" - - name: golint - uses: Jerome1337/golint-action@v1.0.2 - - name: Revive Action - uses: morphy2k/revive-action@v2.5.1 + uses: morphy2k/revive-action@v2 - name: Tests run: go test -v ./... + + run_modver: + runs-on: ubuntu-latest + needs: build # Only run when build job was successful + if: ${{ github.event_name == 'pull_request' && success() }} + permissions: + contents: read # Modver needs to read the repo content + pull-requests: write # Modver needs to write comments/status on PRs + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 # Modver needs full history for comparison + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: "stable" + + - name: Modver + uses: bobg/modver@v2.12.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + pull_request_url: https://github.com/${{ github.repository }}/pull/${{ github.event.number }} diff --git a/.github/workflows/itest.yml b/.github/workflows/itest.yml index eff11c2..878d1a3 100644 --- a/.github/workflows/itest.yml +++ b/.github/workflows/itest.yml @@ -5,19 +5,19 @@ jobs: build: runs-on: ubuntu-latest steps: + - name: Checkout + uses: actions/checkout@v5 - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v6 with: - go-version: 1.21.0 + go-version-file: "go.mod" + check-latest: true - name: Set up Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v6 with: - node-version: 16 - - - name: Checkout - uses: actions/checkout@v3 + node-version: 24 - name: Execute the scripts run: | @@ -25,7 +25,7 @@ jobs: sudo apt install -y make nginx fcgiwrap gnutls-bin cp -r $GITHUB_WORKSPACE ~ cd ~ - cd csaf_distribution/docs/scripts/ + cd csaf/docs/scripts/ # keep in sync with docs/scripts/Readme.md export FOLDERNAME=devca1 ORGANAME="CSAF Tools Development (internal)" source ./TLSConfigsForITest.sh @@ -36,10 +36,10 @@ jobs: shell: bash - name: Upload test results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: checker-results - path: | - ~/checker-results.html - ~/checker-results-no-clientcert.json - if-no-files-found: error + name: checker-results + path: | + ~/checker-results.html + ~/checker-results-no-clientcert.json + if-no-files-found: error diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 739f45c..52406e8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,22 +7,26 @@ on: jobs: releases-matrix: name: Release Go binaries - runs-on: ubuntu-20.04 + # use oldest available ubuntu to be compatible with more libc.so revs. + runs-on: ubuntu-22.04 + permissions: + contents: write steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v5 - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v6 with: - go-version: '^1.21.0' + go-version: '^1.24.9' + check-latest: true - name: Build run: make dist - name: Upload release assets - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: files: | - dist/csaf_distribution-*.zip - dist/csaf_distribution-*.tar.gz + dist/csaf-*.zip + dist/csaf-*.tar.gz diff --git a/LICENSE-Apache-2.0.txt b/LICENSE-Apache-2.0.txt new file mode 100644 index 0000000..137069b --- /dev/null +++ b/LICENSE-Apache-2.0.txt @@ -0,0 +1,73 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSES/Apache-2.0.txt b/LICENSES/Apache-2.0.txt new file mode 100644 index 0000000..137069b --- /dev/null +++ b/LICENSES/Apache-2.0.txt @@ -0,0 +1,73 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSES/BSD-3-Clause.txt b/LICENSES/BSD-3-Clause.txt new file mode 100644 index 0000000..ea890af --- /dev/null +++ b/LICENSES/BSD-3-Clause.txt @@ -0,0 +1,11 @@ +Copyright (c) . + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/LICENSES/LicenseRef-Go119-BSD-Patentgrant.txt b/LICENSES/LicenseRef-Go119-BSD-Patentgrant.txt deleted file mode 100644 index fa1aad8..0000000 --- a/LICENSES/LicenseRef-Go119-BSD-Patentgrant.txt +++ /dev/null @@ -1,51 +0,0 @@ -Copyright (c) 2009 The Go Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -Additional IP Rights Grant (Patents) - -"This implementation" means the copyrightable works distributed by -Google as part of the Go project. - -Google hereby grants to You a perpetual, worldwide, non-exclusive, -no-charge, royalty-free, irrevocable (except as stated in this section) -patent license to make, have made, use, offer to sell, sell, import, -transfer and otherwise run, modify and propagate the contents of this -implementation of Go, where such license applies only to those patent -claims, both currently owned or controlled by Google and acquired in -the future, licensable by Google that are necessarily infringed by this -implementation of Go. This grant does not include claims that would be -infringed only as a consequence of further modification of this -implementation. If you or your agent or exclusive licensee institute or -order or agree to the institution of patent litigation against any -entity (including a cross-claim or counterclaim in a lawsuit) alleging -that this implementation of Go or any code incorporated within this -implementation of Go constitutes direct or contributory patent -infringement, or inducement of patent infringement, then any patent -rights granted to you under this License for this implementation of Go -shall terminate as of the date such litigation is filed. diff --git a/Makefile b/Makefile index 19e31c7..7bb8ef9 100644 --- a/Makefile +++ b/Makefile @@ -1,26 +1,26 @@ -# This file is Free Software under the MIT License -# without warranty, see README.md and LICENSES/MIT.txt for details. +# This file is Free Software under the Apache-2.0 License +# without warranty, see README.md and LICENSES/Apache-2.0.txt for details. # -# SPDX-License-Identifier: MIT +# SPDX-License-Identifier: Apache-2.0 # # SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) # Software-Engineering: 2021 Intevation GmbH # -# Makefile to build csaf_distribution components +# Makefile to build csaf components SHELL = /bin/bash BUILD = go build MKDIR = mkdir -p -.PHONY: build build_linux build_win build_mac_amd64 build_mac_arm64 tag_checked_out mostlyclean +.PHONY: build build_linux build_linux_arm64 build_win build_win_arm64 build_mac_amd64 build_mac_arm64 tag_checked_out mostlyclean all: - @echo choose a target from: build build_linux build_win build_mac_amd64 build_mac_arm64 mostlyclean + @echo choose a target from: build build_linux build_linux_arm64 build_win build_win_arm64 build_mac_amd64 build_mac_arm64 mostlyclean @echo prepend \`make BUILDTAG=1\` to checkout the highest git tag before building @echo or set BUILDTAG to a specific tag # Build all binaries -build: build_linux build_win build_mac_amd64 build_mac_arm64 +build: build_linux build_linux_arm64 build_win build_win_arm64 build_mac_amd64 build_mac_arm64 # if BUILDTAG == 1 set it to the highest git tag ifeq ($(strip $(BUILDTAG)),1) @@ -29,7 +29,7 @@ endif ifdef BUILDTAG # add the git tag checkout to the requirements of our build targets -build_linux build_win build_mac_amd64 build_mac_arm64: tag_checked_out +build_linux build_linux_arm64 build_win build_win_arm64 build_mac_amd64 build_mac_arm64: tag_checked_out endif tag_checked_out: @@ -41,54 +41,77 @@ tag_checked_out: # into a semver version. For this we increase the PATCH number, so that # any commit after a tag is considered newer than the semver from the tag # without an optional 'v' -# Note we need `--tags` because github release only creates lightweight tags +# Note we need `--tags` because github releases only create lightweight tags # (see feature request https://github.com/github/feedback/discussions/4924). # We use `--always` in case of being run as github action with shallow clone. # In this case we might in some situations see an error like # `/bin/bash: line 1: 2b55bbb: value too great for base (error token is "2b55bbb")` # which can be ignored. -GITDESC := $(shell git describe --tags --always) +GITDESC := $(shell git describe --tags --always --dirty=-modified 2>/dev/null || true) +CURRENT_FOLDER_NAME := $(notdir $(CURDIR)) +ifeq ($(strip $(GITDESC)),) +SEMVER := $(CURRENT_FOLDER_NAME) +else GITDESCPATCH := $(shell echo '$(GITDESC)' | sed -E 's/v?[0-9]+\.[0-9]+\.([0-9]+)[-+]?.*/\1/') SEMVERPATCH := $(shell echo $$(( $(GITDESCPATCH) + 1 ))) -# Hint: The regexp in the next line only matches if there is a hyphen (`-`) -# followed by a number, by which we assume that git describe -# has added a string after the tag -SEMVER := $(shell echo '$(GITDESC)' | sed -E 's/v?([0-9]+\.[0-9]+\.)([0-9]+)(-[1-9].*)/\1$(SEMVERPATCH)\3/' ) +# Hint: The second regexp in the next line only matches +# if there is a hyphen (`-`) followed by a number, +# by which we assume that git describe has added a string after the tag +SEMVER := $(shell echo '$(GITDESC)' | sed -E -e 's/^v//' -e 's/([0-9]+\.[0-9]+\.)([0-9]+)(-[1-9].*)/\1$(SEMVERPATCH)\3/' ) +endif testsemver: @echo from \'$(GITDESC)\' transformed to \'$(SEMVER)\' # Set -ldflags parameter to pass the semversion. -LDFLAGS = -ldflags "-X github.com/csaf-poc/csaf_distribution/v3/util.SemVersion=$(SEMVER)" +LDFLAGS = -ldflags "-X github.com/gocsaf/csaf/v3/util.SemVersion=$(SEMVER)" # Build binaries and place them under bin-$(GOOS)-$(GOARCH) # Using 'Target-specific Variable Values' to specify the build target system -GOARCH = amd64 -build_linux: GOOS = linux -build_win: GOOS = windows -build_mac_amd64: GOOS = darwin +build_linux: GOOS=linux +build_linux: GOARCH=amd64 -build_mac_arm64: GOARCH = arm64 -build_mac_arm64: GOOS = darwin +build_win: GOOS=windows +build_win: GOARCH=amd64 -build_linux build_win build_mac_amd64 build_mac_arm64: +build_mac_amd64: GOOS=darwin +build_mac_amd64: GOARCH=amd64 + +build_mac_arm64: GOOS=darwin +build_mac_arm64: GOARCH=arm64 + +build_linux_arm64: GOOS=linux +build_linux_arm64: GOARCH=arm64 + +build_win_arm64: GOOS=windows +build_win_arm64: GOARCH=arm64 + +build_linux build_linux_arm64 build_win build_win_arm64 build_mac_amd64 build_mac_arm64: $(eval BINDIR = bin-$(GOOS)-$(GOARCH)/ ) $(MKDIR) $(BINDIR) env GOARCH=$(GOARCH) GOOS=$(GOOS) $(BUILD) -o $(BINDIR) $(LDFLAGS) -v ./cmd/... -DISTDIR := csaf_distribution-$(SEMVER) -dist: build_linux build_win build_mac_amd64 build_mac_arm64 +DISTDIR := csaf-$(SEMVER) +dist: build_linux build_linux_arm64 build_win build_win_arm64 build_mac_amd64 build_mac_arm64 mkdir -p dist mkdir -p dist/$(DISTDIR)-windows-amd64/bin-windows-amd64 + mkdir -p dist/$(DISTDIR)-windows-arm64/bin-windows-arm64 cp README.md dist/$(DISTDIR)-windows-amd64 + cp README.md dist/$(DISTDIR)-windows-arm64 cp bin-windows-amd64/csaf_uploader.exe bin-windows-amd64/csaf_validator.exe \ bin-windows-amd64/csaf_checker.exe bin-windows-amd64/csaf_downloader.exe \ dist/$(DISTDIR)-windows-amd64/bin-windows-amd64/ + cp bin-windows-arm64/csaf_uploader.exe bin-windows-arm64/csaf_validator.exe \ + bin-windows-arm64/csaf_checker.exe bin-windows-arm64/csaf_downloader.exe \ + dist/$(DISTDIR)-windows-arm64/bin-windows-arm64/ mkdir -p dist/$(DISTDIR)-windows-amd64/docs + mkdir -p dist/$(DISTDIR)-windows-arm64/docs cp docs/csaf_uploader.md docs/csaf_validator.md docs/csaf_checker.md \ docs/csaf_downloader.md dist/$(DISTDIR)-windows-amd64/docs + cp docs/csaf_uploader.md docs/csaf_validator.md docs/csaf_checker.md \ + docs/csaf_downloader.md dist/$(DISTDIR)-windows-arm64/docs mkdir -p dist/$(DISTDIR)-macos/bin-darwin-amd64 \ dist/$(DISTDIR)-macos/bin-darwin-arm64 \ dist/$(DISTDIR)-macos/docs @@ -98,9 +121,20 @@ dist: build_linux build_win build_mac_amd64 build_mac_arm64 cp docs/$${f}.md dist/$(DISTDIR)-macos/docs ; \ done mkdir dist/$(DISTDIR)-gnulinux-amd64 - cp -r README.md docs bin-linux-amd64 dist/$(DISTDIR)-gnulinux-amd64 + mkdir dist/$(DISTDIR)-gnulinux-arm64 + cp -r README.md bin-linux-amd64 dist/$(DISTDIR)-gnulinux-amd64 + cp -r README.md bin-linux-arm64 dist/$(DISTDIR)-gnulinux-arm64 + # adjust which docs to copy + mkdir -p dist/tmp_docs + cp -r docs/examples dist/tmp_docs + cp docs/*.md dist/tmp_docs + cp -r dist/tmp_docs dist/$(DISTDIR)-gnulinux-amd64/docs + cp -r dist/tmp_docs dist/$(DISTDIR)-gnulinux-arm64/docs + rm -rf dist/tmp_docs cd dist/ ; zip -r $(DISTDIR)-windows-amd64.zip $(DISTDIR)-windows-amd64/ + cd dist/ ; zip -r $(DISTDIR)-windows-arm64.zip $(DISTDIR)-windows-arm64/ cd dist/ ; tar -cvmlzf $(DISTDIR)-gnulinux-amd64.tar.gz $(DISTDIR)-gnulinux-amd64/ + cd dist/ ; tar -cvmlzf $(DISTDIR)-gnulinux-arm64.tar.gz $(DISTDIR)-gnulinux-arm64/ cd dist/ ; tar -cvmlzf $(DISTDIR)-macos.tar.gz $(DISTDIR)-macos # Remove bin-*-* and dist directories diff --git a/README.md b/README.md index 54daf87..54543a7 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,20 @@ -# csaf_distribution + + + + +# csaf + +Implements a [CSAF](https://oasis-open.github.io/csaf-documentation/) +([specification v2.0](https://docs.oasis-open.org/csaf/csaf/v2.0/os/csaf-v2.0-os.html) +and its [errata](https://docs.oasis-open.org/csaf/csaf/v2.0/csaf-v2.0.html)) trusted provider, checker, aggregator and downloader. Includes an uploader command line tool for the trusted provider. @@ -28,13 +41,20 @@ is a tool for testing a CSAF Trusted Provider according to [Section 7 of the CSA ### [csaf_aggregator](docs/csaf_aggregator.md) is a CSAF Aggregator, to list or mirror providers. -## Other stuff + +## Use as go library + +The modules of this repository can be used as library by other Go applications. [ISDuBA](https://github.com/ISDuBA/ISDuBA) does so, for example. +But there is only limited support and thus it is _not officially supported_. +There are plans to change this without a concrete schedule within a future major release, e.g. see [#367](https://github.com/gocsaf/csaf/issues/367). + +Initially envisioned as a toolbox, it was not constructed as a library, +and to name one issue, exposes too many functions. +This leads to problems like [#634](https://github.com/gocsaf/csaf/issues/634), where we have to accept that with 3.2.0 there was an unintended API change. ### [examples](./examples/README.md) -are small examples of how to use `github.com/csaf-poc/csaf_distribution` -as an API. Currently this is a work in progress, as usage of this repository -as a library to access is _not officially supported_, e.g. -see https://github.com/csaf-poc/csaf_distribution/issues/367 . +are small examples of how to use `github.com/gocsaf/csaf` as an API. Currently this is a work in progress. + ## Setup Binaries for the server side are only available and tested @@ -58,9 +78,10 @@ Download the binaries from the most recent release assets on Github. ### Build from sources -- A recent version of **Go** (1.20+) should be installed. [Go installation](https://go.dev/doc/install) +- Needs a [supported version](docs/Development.md) of **Go** to be installed. + [Go installation](https://go.dev/doc/install) -- Clone the repository `git clone https://github.com/csaf-poc/csaf_distribution.git ` +- Clone the repository `git clone https://github.com/gocsaf/csaf.git ` - Build Go components Makefile supplies the following targets: - Build for GNU/Linux system: `make build_linux` @@ -86,10 +107,18 @@ Binaries will be placed in directories named like `bin-linux-amd64/` and `bin-wi For further details of the development process consult our [development page](./docs/Development.md). +## Previous repo URLs + +> [!NOTE] +> To avoid future breakage, if you have `csaf-poc` in some of your URLs: +> 1. Adjust your HTML links. +> 2. Adjust your go module paths, see [#579](https://github.com/gocsaf/csaf/issues/579#issuecomment-2497244379). +> +> (This repository was moved here from https://github.com/csaf-poc/csaf_distribution on 2024-10-28. The old one is deprecated and redirection will be switched off sometime in 2025.) ## License -- `csaf_distribution` is licensed as Free Software under MIT License. +- `csaf` is licensed as Free Software under the terms of the [Apache License, Version 2.0](./LICENSES/Apache-2.0.txt). - See the specific source files for details, the license itself can be found in the directory `LICENSES/`. diff --git a/cmd/csaf_aggregator/client.go b/cmd/csaf_aggregator/client.go index deb108a..abd475c 100644 --- a/cmd/csaf_aggregator/client.go +++ b/cmd/csaf_aggregator/client.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) // Software-Engineering: 2022 Intevation GmbH @@ -10,23 +10,25 @@ package main import ( "errors" + "fmt" "io" "net/http" - "github.com/csaf-poc/csaf_distribution/v3/util" + "github.com/gocsaf/csaf/v3/util" ) var errNotFound = errors.New("not found") func downloadJSON(c util.Client, url string, found func(io.Reader) error) error { res, err := c.Get(url) - if err != nil || res.StatusCode != http.StatusOK || + if err != nil { + return fmt.Errorf("not found: %w", err) + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK || res.Header.Get("Content-Type") != "application/json" { // ignore this as it is expected. return errNotFound } - return func() error { - defer res.Body.Close() - return found(res.Body) - }() + return found(res.Body) } diff --git a/cmd/csaf_aggregator/client_test.go b/cmd/csaf_aggregator/client_test.go new file mode 100644 index 0000000..3617ce6 --- /dev/null +++ b/cmd/csaf_aggregator/client_test.go @@ -0,0 +1,67 @@ +// This file is Free Software under the MIT License +// without warranty, see README.md and LICENSES/MIT.txt for details. +// +// SPDX-License-Identifier: MIT +// +// SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) +// Software-Engineering: 2022 Intevation GmbH + +package main + +import ( + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gocsaf/csaf/v3/util" +) + +func Test_downloadJSON(t *testing.T) { + tests := []struct { + name string + statusCode int + contentType string + wantErr error + }{ + { + name: "status ok, application/json", + statusCode: http.StatusOK, + contentType: "application/json", + wantErr: nil, + }, + { + name: "status found, application/json", + statusCode: http.StatusFound, + contentType: "application/json", + wantErr: errNotFound, + }, + { + name: "status ok, application/xml", + statusCode: http.StatusOK, + contentType: "application/xml", + wantErr: errNotFound, + }, + } + + t.Parallel() + for _, testToRun := range tests { + test := testToRun + t.Run(test.name, func(tt *testing.T) { + tt.Parallel() + found := func(_ io.Reader) error { + return nil + } + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Add("Content-Type", test.contentType) + w.WriteHeader(test.statusCode) + })) + defer server.Close() + hClient := http.Client{} + client := util.Client(&hClient) + if gotErr := downloadJSON(client, server.URL, found); gotErr != test.wantErr { + t.Errorf("downloadJSON: Expected %q but got %q.", test.wantErr, gotErr) + } + }) + } +} diff --git a/cmd/csaf_aggregator/config.go b/cmd/csaf_aggregator/config.go index edb1fd9..55a7193 100644 --- a/cmd/csaf_aggregator/config.go +++ b/cmd/csaf_aggregator/config.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) // Software-Engineering: 2022 Intevation GmbH @@ -12,7 +12,7 @@ import ( "crypto/tls" "errors" "fmt" - "log" + "log/slog" "net/http" "os" "runtime" @@ -20,12 +20,12 @@ import ( "time" "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/csaf-poc/csaf_distribution/v3/csaf" - "github.com/csaf-poc/csaf_distribution/v3/internal/certs" - "github.com/csaf-poc/csaf_distribution/v3/internal/filter" - "github.com/csaf-poc/csaf_distribution/v3/internal/models" - "github.com/csaf-poc/csaf_distribution/v3/internal/options" - "github.com/csaf-poc/csaf_distribution/v3/util" + "github.com/gocsaf/csaf/v3/csaf" + "github.com/gocsaf/csaf/v3/internal/certs" + "github.com/gocsaf/csaf/v3/internal/filter" + "github.com/gocsaf/csaf/v3/internal/models" + "github.com/gocsaf/csaf/v3/internal/options" + "github.com/gocsaf/csaf/v3/util" "golang.org/x/time/rate" ) @@ -178,9 +178,11 @@ func (p *provider) ageAccept(c *config) func(time.Time) bool { } if c.Verbose { - log.Printf( - "Setting up filter to accept advisories within time range %s to %s\n", - r[0].Format(time.RFC3339), r[1].Format(time.RFC3339)) + slog.Debug( + "Setting up filter to accept advisories within time range", + "from", r[0].Format(time.RFC3339), + "to", r[1].Format(time.RFC3339), + ) } return r.Contains } @@ -262,8 +264,14 @@ func (c *config) privateOpenPGPKey() (*crypto.Key, error) { return c.key, c.keyErr } -func (c *config) httpClient(p *provider) util.Client { +// httpLog does structured logging in a [util.LoggingClient]. +func httpLog(method, url string) { + slog.Debug("http", + "method", method, + "url", url) +} +func (c *config) httpClient(p *provider) util.Client { hClient := http.Client{} var tlsConfig tls.Config @@ -282,6 +290,7 @@ func (c *config) httpClient(p *provider) util.Client { hClient.Transport = &http.Transport{ TLSClientConfig: &tlsConfig, + Proxy: http.ProxyFromEnvironment, } client := util.Client(&hClient) @@ -299,10 +308,18 @@ func (c *config) httpClient(p *provider) util.Client { Client: client, Header: c.ExtraHeader, } + default: + client = &util.HeaderClient{ + Client: client, + Header: http.Header{}, + } } if c.Verbose { - client = &util.LoggingClient{Client: client} + client = &util.LoggingClient{ + Client: client, + Log: httpLog, + } } if p.Rate == nil && c.Rate == nil { @@ -323,7 +340,6 @@ func (c *config) httpClient(p *provider) util.Client { } func (c *config) checkProviders() error { - if !c.AllowSingleProvider && len(c.Providers) < 2 { return errors.New("need at least two providers") } @@ -393,6 +409,17 @@ func (c *config) setDefaults() { } } +// prepareLogging sets up the structured logging. +func (c *config) prepareLogging() error { + ho := slog.HandlerOptions{ + Level: slog.LevelDebug, + } + handler := slog.NewTextHandler(os.Stdout, &ho) + logger := slog.New(handler) + slog.SetDefault(logger) + return nil +} + // compileIgnorePatterns compiles the configured patterns to be ignored. func (p *provider) compileIgnorePatterns() error { pm, err := filter.NewPatternMatcher(p.IgnorePattern) @@ -452,7 +479,6 @@ func (c *config) prepareCertificates() error { // prepare prepares internal state of a loaded configuration. func (c *config) prepare() error { - if len(c.Providers) == 0 { return errors.New("no providers given in configuration") } diff --git a/cmd/csaf_aggregator/files.go b/cmd/csaf_aggregator/files.go index adf04aa..18ccbb6 100644 --- a/cmd/csaf_aggregator/files.go +++ b/cmd/csaf_aggregator/files.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) // Software-Engineering: 2022 Intevation GmbH diff --git a/cmd/csaf_aggregator/full.go b/cmd/csaf_aggregator/full.go index 600c650..e71d7b6 100644 --- a/cmd/csaf_aggregator/full.go +++ b/cmd/csaf_aggregator/full.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) // Software-Engineering: 2022 Intevation GmbH @@ -11,15 +11,15 @@ package main import ( "errors" "fmt" - "log" + "log/slog" "os" "path/filepath" "strings" "sync" "time" - "github.com/csaf-poc/csaf_distribution/v3/csaf" - "github.com/csaf-poc/csaf_distribution/v3/util" + "github.com/gocsaf/csaf/v3/csaf" + "github.com/gocsaf/csaf/v3/util" ) type fullJob struct { @@ -29,11 +29,13 @@ type fullJob struct { err error } -// setupProviderFull fetches the provider-metadate.json for a specific provider. +// setupProviderFull fetches the provider-metadata.json for a specific provider. func (w *worker) setupProviderFull(provider *provider) error { - log.Printf("worker #%d: %s (%s)\n", - w.num, provider.Name, provider.Domain) - + w.log.Info("Setting up provider", + "provider", slog.GroupValue( + slog.String("name", provider.Name), + slog.String("domain", provider.Domain), + )) w.dir = "" w.provider = provider @@ -55,7 +57,7 @@ func (w *worker) setupProviderFull(provider *provider) error { "provider-metadata.json has %d validation issues", len(errors)) } - log.Printf("provider-metadata: %s\n", w.loc) + w.log.Info("Using provider-metadata", "url", w.loc) return nil } @@ -79,7 +81,7 @@ func (w *worker) fullWork(wg *sync.WaitGroup, jobs <-chan *fullJob) { func (p *processor) full() error { if p.cfg.runAsMirror() { - log.Println("Running in aggregator mode") + p.log.Info("Running in aggregator mode") // check if we need to setup a remote validator if p.cfg.RemoteValidatorOptions != nil { @@ -96,16 +98,18 @@ func (p *processor) full() error { }() } } else { - log.Println("Running in lister mode") + p.log.Info("Running in lister mode") } queue := make(chan *fullJob) var wg sync.WaitGroup - log.Printf("Starting %d workers.\n", p.cfg.Workers) + p.log.Info("Starting workers...", "num", p.cfg.Workers) + for i := 1; i <= p.cfg.Workers; i++ { wg.Add(1) w := newWorker(i, p) + go w.fullWork(&wg, queue) } @@ -135,12 +139,22 @@ func (p *processor) full() error { for i := range jobs { j := &jobs[i] if j.err != nil { - log.Printf("error: '%s' failed: %v\n", j.provider.Name, j.err) + p.log.Error("Job execution failed", + slog.Group("job", + slog.Group("provider"), + "name", j.provider.Name, + ), + "err", j.err, + ) continue } if j.aggregatorProvider == nil { - log.Printf( - "error: '%s' does not produce any result.\n", j.provider.Name) + p.log.Error("Job did not produce any result", + slog.Group("job", + slog.Group("provider"), + "name", j.provider.Name, + ), + ) continue } diff --git a/cmd/csaf_aggregator/indices.go b/cmd/csaf_aggregator/indices.go index 69954bd..976d9a3 100644 --- a/cmd/csaf_aggregator/indices.go +++ b/cmd/csaf_aggregator/indices.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) // Software-Engineering: 2022 Intevation GmbH @@ -12,7 +12,6 @@ import ( "bufio" "encoding/csv" "fmt" - "log" "os" "path/filepath" "sort" @@ -20,8 +19,8 @@ import ( "strings" "time" - "github.com/csaf-poc/csaf_distribution/v3/csaf" - "github.com/csaf-poc/csaf_distribution/v3/util" + "github.com/gocsaf/csaf/v3/csaf" + "github.com/gocsaf/csaf/v3/util" ) const ( @@ -184,19 +183,26 @@ func (w *worker) writeROLIENoSummaries(label string) error { fname := "csaf-feed-tlp-" + labelFolder + ".json" - feedURL := w.processor.cfg.Domain + "/.well-known/csaf-aggregator/" + - w.provider.Name + "/" + labelFolder + "/" + fname + feedURL, err := w.getProviderBaseURL() + if err != nil { + return err + } + feedURL = feedURL.JoinPath(labelFolder, fname) links := []csaf.Link{{ Rel: "self", - HRef: feedURL, + HRef: feedURL.String(), }} if w.provider.serviceDocument(w.processor.cfg) { + serviceURL, err := w.getProviderBaseURL() + if err != nil { + return err + } + serviceURL = serviceURL.JoinPath("service.json") links = append(links, csaf.Link{ - Rel: "service", - HRef: w.processor.cfg.Domain + "/.well-known/csaf-aggregator/" + - w.provider.Name + "/service.json", + Rel: "service", + HRef: serviceURL.String(), }) } @@ -224,8 +230,11 @@ func (w *worker) writeROLIE(label string, summaries []summary) error { fname := "csaf-feed-tlp-" + labelFolder + ".json" - feedURL := w.processor.cfg.Domain + "/.well-known/csaf-aggregator/" + - w.provider.Name + "/" + labelFolder + "/" + fname + feedURL, err := w.getProviderBaseURL() + if err != nil { + return err + } + feedURL = feedURL.JoinPath(labelFolder, fname) entries := make([]*csaf.Entry, len(summaries)) @@ -237,10 +246,13 @@ func (w *worker) writeROLIE(label string, summaries []summary) error { for i := range summaries { s := &summaries[i] - csafURL := w.processor.cfg.Domain + "/.well-known/csaf-aggregator/" + - w.provider.Name + "/" + label + "/" + - strconv.Itoa(s.summary.InitialReleaseDate.Year()) + "/" + - s.filename + csafURL, err := w.getProviderBaseURL() + if err != nil { + return err + } + csafURLString := csafURL.JoinPath(label, + strconv.Itoa(s.summary.InitialReleaseDate.Year()), + s.filename).String() entries[i] = &csaf.Entry{ ID: s.summary.ID, @@ -248,15 +260,15 @@ func (w *worker) writeROLIE(label string, summaries []summary) error { Published: csaf.TimeStamp(s.summary.InitialReleaseDate), Updated: csaf.TimeStamp(s.summary.CurrentReleaseDate), Link: []csaf.Link{ - {Rel: "self", HRef: csafURL}, - {Rel: "hash", HRef: csafURL + ".sha256"}, - {Rel: "hash", HRef: csafURL + ".sha512"}, - {Rel: "signature", HRef: csafURL + ".asc"}, + {Rel: "self", HRef: csafURLString}, + {Rel: "hash", HRef: csafURLString + ".sha256"}, + {Rel: "hash", HRef: csafURLString + ".sha512"}, + {Rel: "signature", HRef: csafURLString + ".asc"}, }, Format: format, Content: csaf.Content{ Type: "application/json", - Src: csafURL, + Src: csafURLString, }, } if s.summary.Summary != "" { @@ -268,14 +280,18 @@ func (w *worker) writeROLIE(label string, summaries []summary) error { links := []csaf.Link{{ Rel: "self", - HRef: feedURL, + HRef: feedURL.String(), }} if w.provider.serviceDocument(w.processor.cfg) { + serviceURL, err := w.getProviderBaseURL() + if err != nil { + return err + } + serviceURL = serviceURL.JoinPath("service.json") links = append(links, csaf.Link{ - Rel: "service", - HRef: w.processor.cfg.Domain + "/.well-known/csaf-aggregator/" + - w.provider.Name + "/service.json", + Rel: "service", + HRef: serviceURL.String(), }) } @@ -345,12 +361,15 @@ func (w *worker) writeService() error { for _, ts := range labels { feedName := "csaf-feed-tlp-" + ts + ".json" - href := w.processor.cfg.Domain + "/.well-known/csaf-aggregator/" + - w.provider.Name + "/" + ts + "/" + feedName + hrefURL, err := w.getProviderBaseURL() + if err != nil { + return err + } + hrefURL = hrefURL.JoinPath(ts, feedName) collection := csaf.ROLIEServiceWorkspaceCollection{ Title: "CSAF feed (TLP:" + strings.ToUpper(ts) + ")", - HRef: href, + HRef: hrefURL.String(), Categories: categories, } collections = append(collections, collection) @@ -377,7 +396,7 @@ func (w *worker) writeIndices() error { } for label, summaries := range w.summaries { - log.Printf("%s: %d\n", label, len(summaries)) + w.log.Debug("Writing indices", "label", label, "summaries.num", len(summaries)) if err := w.writeInterims(label, summaries); err != nil { return err } diff --git a/cmd/csaf_aggregator/interim.go b/cmd/csaf_aggregator/interim.go index bdd5ebc..8805fdb 100644 --- a/cmd/csaf_aggregator/interim.go +++ b/cmd/csaf_aggregator/interim.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) // Software-Engineering: 2022 Intevation GmbH @@ -13,11 +13,9 @@ import ( "crypto/sha256" "crypto/sha512" "encoding/csv" - "encoding/json" "errors" "fmt" "io" - "log" "net/http" "os" "path/filepath" @@ -25,8 +23,9 @@ import ( "sync" "time" - "github.com/csaf-poc/csaf_distribution/v3/csaf" - "github.com/csaf-poc/csaf_distribution/v3/util" + "github.com/gocsaf/csaf/v3/csaf" + "github.com/gocsaf/csaf/v3/internal/misc" + "github.com/gocsaf/csaf/v3/util" ) type interimJob struct { @@ -82,7 +81,7 @@ func (w *worker) checkInterims( if err := func() error { defer res.Body.Close() tee := io.TeeReader(res.Body, hasher) - return json.NewDecoder(tee).Decode(&doc) + return misc.StrictJSONParse(tee, &doc) }(); err != nil { return nil, err } @@ -102,12 +101,12 @@ func (w *worker) checkInterims( // XXX: Should we return an error here? for _, e := range errors { - log.Printf("validation error: %s: %v\n", url, e) + w.log.Error("validation error", "url", url, "err", e) } // We need to write the changed content. - // This will start the transcation if not already started. + // This will start the transaction if not already started. dst, err := tx.Dst() if err != nil { return nil, err @@ -159,8 +158,7 @@ func (w *worker) checkInterims( // setupProviderInterim prepares the worker for a specific provider. func (w *worker) setupProviderInterim(provider *provider) { - log.Printf("worker #%d: %s (%s)\n", - w.num, provider.Name, provider.Domain) + w.log.Info("Setting up worker", provider.Name, provider.Domain) w.dir = "" w.provider = provider @@ -262,7 +260,7 @@ func (p *processor) interim() error { queue := make(chan *interimJob) var wg sync.WaitGroup - log.Printf("Starting %d workers.\n", p.cfg.Workers) + p.log.Info("Starting workers...", "num", p.cfg.Workers) for i := 1; i <= p.cfg.Workers; i++ { wg.Add(1) w := newWorker(i, p) diff --git a/cmd/csaf_aggregator/lazytransaction.go b/cmd/csaf_aggregator/lazytransaction.go index a2b1e94..af36ee2 100644 --- a/cmd/csaf_aggregator/lazytransaction.go +++ b/cmd/csaf_aggregator/lazytransaction.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) // Software-Engineering: 2022 Intevation GmbH @@ -9,11 +9,11 @@ package main import ( - "log" + "log/slog" "os" "path/filepath" - "github.com/csaf-poc/csaf_distribution/v3/util" + "github.com/gocsaf/csaf/v3/util" ) type lazyTransaction struct { @@ -85,7 +85,8 @@ func (lt *lazyTransaction) commit() error { os.RemoveAll(lt.dst) return err } - log.Printf("Move %q -> %q\n", symlink, lt.src) + + slog.Debug("Moving directory", "from", symlink, "to", lt.src) if err := os.Rename(symlink, lt.src); err != nil { os.RemoveAll(lt.dst) return err diff --git a/cmd/csaf_aggregator/lister.go b/cmd/csaf_aggregator/lister.go index a3bfd29..7e1fb58 100644 --- a/cmd/csaf_aggregator/lister.go +++ b/cmd/csaf_aggregator/lister.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) // Software-Engineering: 2022 Intevation GmbH @@ -11,8 +11,8 @@ package main import ( "fmt" - "github.com/csaf-poc/csaf_distribution/v3/csaf" - "github.com/csaf-poc/csaf_distribution/v3/util" + "github.com/gocsaf/csaf/v3/csaf" + "github.com/gocsaf/csaf/v3/util" ) // mirrorAllowed checks if mirroring is allowed. diff --git a/cmd/csaf_aggregator/main.go b/cmd/csaf_aggregator/main.go index 74a9670..2056e84 100644 --- a/cmd/csaf_aggregator/main.go +++ b/cmd/csaf_aggregator/main.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) // Software-Engineering: 2022 Intevation GmbH @@ -11,10 +11,12 @@ package main import ( "fmt" + "log/slog" "os" "path/filepath" - "github.com/csaf-poc/csaf_distribution/v3/internal/options" + "github.com/gocsaf/csaf/v3/internal/options" + "github.com/gofrs/flock" ) @@ -44,8 +46,9 @@ func lock(lockFile *string, fn func() error) error { func main() { _, cfg, err := parseArgsConfig() - options.ErrorCheck(err) - options.ErrorCheck(cfg.prepare()) - p := processor{cfg: cfg} - options.ErrorCheck(lock(cfg.LockFile, p.process)) + cfg.prepareLogging() + options.ErrorCheckStructured(err) + options.ErrorCheckStructured(cfg.prepare()) + p := processor{cfg: cfg, log: slog.Default()} + options.ErrorCheckStructured(lock(cfg.LockFile, p.process)) } diff --git a/cmd/csaf_aggregator/mirror.go b/cmd/csaf_aggregator/mirror.go index 3acb48e..9653ea9 100644 --- a/cmd/csaf_aggregator/mirror.go +++ b/cmd/csaf_aggregator/mirror.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) // Software-Engineering: 2022 Intevation GmbH @@ -13,10 +13,9 @@ import ( "crypto/sha256" "crypto/sha512" "encoding/hex" - "encoding/json" "fmt" "io" - "log" + "log/slog" "net/http" "net/url" "os" @@ -30,8 +29,9 @@ import ( "github.com/ProtonMail/gopenpgp/v2/constants" "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/csaf-poc/csaf_distribution/v3/csaf" - "github.com/csaf-poc/csaf_distribution/v3/util" + "github.com/gocsaf/csaf/v3/csaf" + "github.com/gocsaf/csaf/v3/internal/misc" + "github.com/gocsaf/csaf/v3/util" ) // mirrorAllowed checks if mirroring is allowed. @@ -47,7 +47,7 @@ func (w *worker) mirror() (*csaf.AggregatorCSAFProvider, error) { if err != nil && w.dir != "" { // If something goes wrong remove the debris. if err := os.RemoveAll(w.dir); err != nil { - log.Printf("error: %v\n", err) + w.log.Error("Could not remove directory", "path", w.dir, "err", err) } } return result, err @@ -67,7 +67,7 @@ func (w *worker) mirrorInternal() (*csaf.AggregatorCSAFProvider, error) { // Collecting the categories per label. w.categories = map[string]util.Set[string]{} - base, err := url.Parse(w.loc) + pmdURL, err := url.Parse(w.loc) if err != nil { return nil, err } @@ -76,7 +76,7 @@ func (w *worker) mirrorInternal() (*csaf.AggregatorCSAFProvider, error) { w.client, w.expr, w.metadataProvider, - base) + pmdURL) afp.AgeAccept = w.provider.ageAccept(w.processor.cfg) @@ -103,9 +103,13 @@ func (w *worker) mirrorInternal() (*csaf.AggregatorCSAFProvider, error) { } // Add us as a mirror. + mirror, err := w.getProviderBaseURL() + if err != nil { + return nil, err + } mirrorURL := csaf.ProviderURL( - fmt.Sprintf("%s/.well-known/csaf-aggregator/%s/provider-metadata.json", - w.processor.cfg.Domain, w.provider.Name)) + mirror.JoinPath("provider-metadata.json").String(), + ) acp.Mirrors = []csaf.ProviderURL{ mirrorURL, @@ -128,8 +132,12 @@ func (w *worker) writeProviderMetadata() error { fname := filepath.Join(w.dir, "provider-metadata.json") + prefixURL, err := w.getProviderBaseURL() + if err != nil { + return err + } pm := csaf.NewProviderMetadataPrefix( - w.processor.cfg.Domain+"/.well-known/csaf-aggregator/"+w.provider.Name, + prefixURL.String(), w.labelsFromSummaries()) // Fill in directory URLs if needed. @@ -139,9 +147,8 @@ func (w *worker) writeProviderMetadata() error { labels = append(labels, label) } sort.Strings(labels) - prefix := w.processor.cfg.Domain + "/.well-known/csaf-aggregator/" + w.provider.Name + "/" for _, label := range labels { - pm.AddDirectoryDistribution(prefix + label) + pm.AddDirectoryDistribution(prefixURL.JoinPath(label).String()) } } @@ -166,7 +173,7 @@ func (w *worker) writeProviderMetadata() error { {Expr: `$.public_openpgp_keys`, Action: util.ReMarshalMatcher(&pm.PGPKeys)}, }, w.metadataProvider); err != nil { // only log the errors - log.Printf("extracting data from orignal provider failed: %v\n", err) + w.log.Error("Extracting data from original provider failed", "err", err) } // We are mirroring the remote public keys, too. @@ -188,19 +195,22 @@ func (w *worker) mirrorPGPKeys(pm *csaf.ProviderMetadata) error { return err } + keyURL, err := w.getProviderBaseURL() + if err != nil { + return err + } localKeyURL := func(fingerprint string) string { - return fmt.Sprintf("%s/.well-known/csaf-aggregator/%s/openpgp/%s.asc", - w.processor.cfg.Domain, w.provider.Name, fingerprint) + return keyURL.JoinPath("openpgp", (fingerprint + ".asc")).String() } for i := range pm.PGPKeys { pgpKey := &pm.PGPKeys[i] if pgpKey.URL == nil { - log.Printf("ignoring PGP key without URL: %s\n", pgpKey.Fingerprint) + w.log.Warn("Ignoring PGP key without URL", "fingerprint", pgpKey.Fingerprint) continue } if _, err := hex.DecodeString(string(pgpKey.Fingerprint)); err != nil { - log.Printf("ignoring PGP with invalid fingerprint: %s\n", *pgpKey.URL) + w.log.Warn("Ignoring PGP key with invalid fingerprint", "url", *pgpKey.URL) continue } @@ -240,8 +250,8 @@ func (w *worker) mirrorPGPKeys(pm *csaf.ProviderMetadata) error { } // replace the URL - url := localKeyURL(fingerprint) - pgpKey.URL = &url + u := localKeyURL(fingerprint) + pgpKey.URL = &u } // If we have public key configured copy it into the new folder @@ -308,7 +318,7 @@ func (w *worker) createAggregatorProvider() (*csaf.AggregatorCSAFProvider, error var ( lastUpdated = csaf.TimeStamp(lastUpdatedT) role = csaf.MetadataRole(roleS) - url = csaf.ProviderURL(urlS) + providerURL = csaf.ProviderURL(urlS) ) return &csaf.AggregatorCSAFProvider{ @@ -316,7 +326,7 @@ func (w *worker) createAggregatorProvider() (*csaf.AggregatorCSAFProvider, error LastUpdated: &lastUpdated, Publisher: &pub, Role: &role, - URL: &url, + URL: &providerURL, }, }, nil } @@ -344,7 +354,7 @@ func (w *worker) doMirrorTransaction() error { // Check if there is a sysmlink already. target := filepath.Join(w.processor.cfg.Folder, w.provider.Name) - log.Printf("target: '%s'\n", target) + w.log.Debug("Checking for path existance", "path", target) exists, err := util.PathExists(target) if err != nil { @@ -359,7 +369,7 @@ func (w *worker) doMirrorTransaction() error { } } - log.Printf("sym link: %s -> %s\n", w.dir, target) + w.log.Debug("Creating sym link", "from", w.dir, "to", target) // Create a new symlink if err := os.Symlink(w.dir, target); err != nil { @@ -368,7 +378,7 @@ func (w *worker) doMirrorTransaction() error { } // Move the symlink - log.Printf("Move: %s -> %s\n", target, webTarget) + w.log.Debug("Moving sym link", "from", target, "to", webTarget) if err := os.Rename(target, webTarget); err != nil { os.RemoveAll(w.dir) return err @@ -462,8 +472,9 @@ func (w *worker) extractCategories(label string, advisory any) error { expr := cat[len(exprPrefix):] // Compile first to check that the expression is okay. if _, err := w.expr.Compile(expr); err != nil { - fmt.Printf("Compiling category expression %q failed: %v\n", - expr, err) + slog.Error("Compiling category expression failed", + "expr", expr, + "err", err) continue } // Ignore errors here as they result from not matching. @@ -499,14 +510,14 @@ func (w *worker) mirrorFiles(tlpLabel csaf.TLPLabel, files []csaf.AdvisoryFile) u, err := url.Parse(file.URL()) if err != nil { - log.Printf("error: %s\n", err) + w.log.Error("Could not parse advisory file URL", "err", err) continue } // Should we ignore this advisory? if w.provider.ignoreURL(file.URL(), w.processor.cfg) { if w.processor.cfg.Verbose { - log.Printf("Ignoring %s: %q\n", w.provider.Name, file.URL()) + w.log.Info("Ignoring advisory", slog.Group("provider", "name", w.provider.Name), "file", file) } continue } @@ -514,7 +525,7 @@ func (w *worker) mirrorFiles(tlpLabel csaf.TLPLabel, files []csaf.AdvisoryFile) // Ignore not conforming filenames. filename := filepath.Base(u.Path) if !util.ConformingFileName(filename) { - log.Printf("Not conforming filename %q. Ignoring.\n", filename) + w.log.Warn("Ignoring advisory because of non-conforming filename", "filename", filename) continue } @@ -527,23 +538,22 @@ func (w *worker) mirrorFiles(tlpLabel csaf.TLPLabel, files []csaf.AdvisoryFile) download := func(r io.Reader) error { tee := io.TeeReader(r, hasher) - return json.NewDecoder(tee).Decode(&advisory) + return misc.StrictJSONParse(tee, &advisory) } if err := downloadJSON(w.client, file.URL(), download); err != nil { - log.Printf("error: %v\n", err) + w.log.Error("Error while downloading JSON", "err", err) continue } // Check against CSAF schema. errors, err := csaf.ValidateCSAF(advisory) if err != nil { - log.Printf("error: %s: %v", file, err) + w.log.Error("Error while validating CSAF schema", "err", err) continue } if len(errors) > 0 { - log.Printf("CSAF file %s has %d validation errors.\n", - file, len(errors)) + w.log.Error("CSAF file has validation errors", "num.errors", len(errors), "file", file) continue } @@ -551,29 +561,27 @@ func (w *worker) mirrorFiles(tlpLabel csaf.TLPLabel, files []csaf.AdvisoryFile) if rmv := w.processor.remoteValidator; rmv != nil { rvr, err := rmv.Validate(advisory) if err != nil { - log.Printf("Calling remote validator failed: %s\n", err) + w.log.Error("Calling remote validator failed", "err", err) continue } if !rvr.Valid { - log.Printf( - "CSAF file %s does not validate remotely.\n", file) + w.log.Error("CSAF file does not validate remotely", "file", file.URL()) continue } } sum, err := csaf.NewAdvisorySummary(w.expr, advisory) if err != nil { - log.Printf("error: %s: %v\n", file, err) + w.log.Error("Error while creating new advisory", "file", file, "err", err) continue } if util.CleanFileName(sum.ID) != filename { - log.Printf("ID %q does not match filename %s", - sum.ID, filename) + w.log.Error("ID mismatch", "id", sum.ID, "filename", filename) } if err := w.extractCategories(label, advisory); err != nil { - log.Printf("error: %s: %v\n", file, err) + w.log.Error("Could not extract categories", "file", file, "err", err) continue } @@ -591,12 +599,10 @@ func (w *worker) mirrorFiles(tlpLabel csaf.TLPLabel, files []csaf.AdvisoryFile) if err := os.MkdirAll(yearDir, 0755); err != nil { return err } - //log.Printf("created %s\n", yearDir) yearDirs[year] = yearDir } fname := filepath.Join(yearDir, filename) - //log.Printf("write: %s\n", fname) data := content.Bytes() if err := writeFileHashes( fname, filename, @@ -621,10 +627,9 @@ func (w *worker) mirrorFiles(tlpLabel csaf.TLPLabel, files []csaf.AdvisoryFile) // If this fails it creates a signature itself with the configured key. func (w *worker) downloadSignatureOrSign(url, fname string, data []byte) error { sig, err := w.downloadSignature(url) - if err != nil { if err != errNotFound { - log.Printf("error: %s: %v\n", url, err) + w.log.Error("Could not find signature URL", "url", url, "err", err) } // Sign it our self. if sig, err = w.sign(data); err != nil { diff --git a/cmd/csaf_aggregator/processor.go b/cmd/csaf_aggregator/processor.go index ccd5062..0d41df8 100644 --- a/cmd/csaf_aggregator/processor.go +++ b/cmd/csaf_aggregator/processor.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) // Software-Engineering: 2022 Intevation GmbH @@ -10,14 +10,15 @@ package main import ( "fmt" - "log" + "log/slog" + "net/url" "os" "path/filepath" - "github.com/ProtonMail/gopenpgp/v2/crypto" + "github.com/gocsaf/csaf/v3/csaf" + "github.com/gocsaf/csaf/v3/util" - "github.com/csaf-poc/csaf_distribution/v3/csaf" - "github.com/csaf-poc/csaf_distribution/v3/util" + "github.com/ProtonMail/gopenpgp/v2/crypto" ) type processor struct { @@ -26,6 +27,9 @@ type processor struct { // remoteValidator is a globally configured remote validator. remoteValidator csaf.RemoteValidator + + // log is the structured logger for the whole processor. + log *slog.Logger } type summary struct { @@ -48,6 +52,7 @@ type worker struct { dir string // Directory to store data to. summaries map[string][]summary // the summaries of the advisories. categories map[string]util.Set[string] // the categories per label. + log *slog.Logger // the structured logger, supplied with the worker number. } func newWorker(num int, processor *processor) *worker { @@ -55,6 +60,7 @@ func newWorker(num int, processor *processor) *worker { num: num, processor: processor, expr: util.NewPathEval(), + log: processor.log.With(slog.Int("worker", num)), } } @@ -84,16 +90,21 @@ func (w *worker) locateProviderMetadata(domain string) error { lpmd := loader.Load(domain) - if w.processor.cfg.Verbose { - for i := range lpmd.Messages { - log.Printf( - "Loading provider-metadata.json of %q: %s\n", - domain, lpmd.Messages[i].Message) - } - } - if !lpmd.Valid() { + for i := range lpmd.Messages { + w.log.Error( + "Loading provider-metadata.json", + "domain", domain, + "message", lpmd.Messages[i].Message) + } return fmt.Errorf("no valid provider-metadata.json found for '%s'", domain) + } else if w.processor.cfg.Verbose { + for i := range lpmd.Messages { + w.log.Debug( + "Loading provider-metadata.json", + "domain", domain, + "message", lpmd.Messages[i].Message) + } } w.metadataProvider = lpmd.Document @@ -102,6 +113,18 @@ func (w *worker) locateProviderMetadata(domain string) error { return nil } +// getProviderBaseURL returns the base URL for the provider. +func (w *worker) getProviderBaseURL() (*url.URL, error) { + baseURL, err := url.Parse(w.processor.cfg.Domain) + if err != nil { + return nil, err + } + baseURL = baseURL.JoinPath(".well-known", + "csaf-aggregator", + w.provider.Name) + return baseURL, nil +} + // removeOrphans removes the directories that are not in the providers list. func (p *processor) removeOrphans() error { @@ -141,7 +164,7 @@ func (p *processor) removeOrphans() error { fi, err := entry.Info() if err != nil { - log.Printf("error: %v\n", err) + p.log.Error("Could not retrieve file info", "err", err) continue } @@ -153,13 +176,13 @@ func (p *processor) removeOrphans() error { d := filepath.Join(path, entry.Name()) r, err := filepath.EvalSymlinks(d) if err != nil { - log.Printf("error: %v\n", err) + p.log.Error("Could not evaluate symlink", "err", err) continue } fd, err := os.Stat(r) if err != nil { - log.Printf("error: %v\n", err) + p.log.Error("Could not retrieve file stats", "err", err) continue } @@ -169,18 +192,18 @@ func (p *processor) removeOrphans() error { } // Remove the link. - log.Printf("removing link %s -> %s\n", d, r) + p.log.Info("Removing link", "path", fmt.Sprintf("%s -> %s", d, r)) if err := os.Remove(d); err != nil { - log.Printf("error: %v\n", err) + p.log.Error("Could not remove symlink", "err", err) continue } // Only remove directories which are in our folder. if rel, err := filepath.Rel(prefix, r); err == nil && rel == filepath.Base(r) { - log.Printf("removing directory %s\n", r) + p.log.Info("Remove directory", "path", r) if err := os.RemoveAll(r); err != nil { - log.Printf("error: %v\n", err) + p.log.Error("Could not remove directory", "err", err) } } } diff --git a/cmd/csaf_checker/config.go b/cmd/csaf_checker/config.go index 3502443..3ea1840 100644 --- a/cmd/csaf_checker/config.go +++ b/cmd/csaf_checker/config.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2023 German Federal Office for Information Security (BSI) // Software-Engineering: 2023 Intevation GmbH @@ -13,10 +13,10 @@ import ( "fmt" "net/http" - "github.com/csaf-poc/csaf_distribution/v3/internal/certs" - "github.com/csaf-poc/csaf_distribution/v3/internal/filter" - "github.com/csaf-poc/csaf_distribution/v3/internal/models" - "github.com/csaf-poc/csaf_distribution/v3/internal/options" + "github.com/gocsaf/csaf/v3/internal/certs" + "github.com/gocsaf/csaf/v3/internal/filter" + "github.com/gocsaf/csaf/v3/internal/models" + "github.com/gocsaf/csaf/v3/internal/options" ) type outputFormat string diff --git a/cmd/csaf_checker/links.go b/cmd/csaf_checker/links.go index 5784489..4eed5f9 100644 --- a/cmd/csaf_checker/links.go +++ b/cmd/csaf_checker/links.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) // Software-Engineering: 2022 Intevation GmbH @@ -13,9 +13,11 @@ import ( "net/http" "net/url" + "github.com/gocsaf/csaf/v3/internal/misc" + "github.com/PuerkitoBio/goquery" - "github.com/csaf-poc/csaf_distribution/v3/util" + "github.com/gocsaf/csaf/v3/util" ) type ( @@ -93,7 +95,12 @@ func (pgs pages) listed( return err } // Links may be relative - abs := baseURL.ResolveReference(u).String() + var abs string + if u.IsAbs() { + abs = u.String() + } else { + abs = misc.JoinURL(baseURL, u).String() + } content.links.Add(abs) return nil }) diff --git a/cmd/csaf_checker/links_test.go b/cmd/csaf_checker/links_test.go index 3229511..6baccf8 100644 --- a/cmd/csaf_checker/links_test.go +++ b/cmd/csaf_checker/links_test.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) // Software-Engineering: 2022 Intevation GmbH @@ -10,8 +10,12 @@ package main import ( "fmt" + "net/http" + "net/http/httptest" "strings" "testing" + + "github.com/gocsaf/csaf/v3/util" ) const page0 = ` @@ -31,7 +35,6 @@ const page0 = ` ` func TestLinksOnPage(t *testing.T) { - var links []string err := linksOnPage( @@ -58,3 +61,78 @@ func TestLinksOnPage(t *testing.T) { } } } + +func Test_listed(t *testing.T) { + tests := []struct { + name string + badDirs util.Set[string] + path string + want bool + }{ + { + name: "listed path", + badDirs: util.Set[string]{}, + path: "/white/avendor-advisory-0004.json", + want: true, + }, + { + name: "badDirs contains path", + badDirs: util.Set[string]{"/white/": {}}, + path: "/white/avendor-advisory-0004.json", + want: false, + }, + { + name: "not found", + badDirs: util.Set[string]{}, + path: "/not-found/resource.json", + want: false, + }, + { + name: "badDirs does not contain path", + badDirs: util.Set[string]{"/bad-dir/": {}}, + path: "/white/avendor-advisory-0004.json", + want: true, + }, + { + name: "unlisted path", + badDirs: util.Set[string]{}, + path: "/white/avendor-advisory-0004-not-listed.json", + want: false, + }, + } + + t.Parallel() + for _, testToRun := range tests { + test := testToRun + t.Run(test.name, func(tt *testing.T) { + tt.Parallel() + serverURL := "" + fs := http.FileServer(http.Dir("../../testdata/simple-directory-provider")) + server := httptest.NewTLSServer(fs) + defer server.Close() + + serverURL = server.URL + + hClient := server.Client() + client := util.Client(hClient) + + pgs := pages{} + cfg := config{RemoteValidator: "", RemoteValidatorCache: ""} + p, err := newProcessor(&cfg) + if err != nil { + t.Error(err) + } + p.client = client + + badDirs := util.Set[string]{} + for dir := range test.badDirs { + badDirs.Add(serverURL + dir) + } + + got, _ := pgs.listed(serverURL+test.path, p, badDirs) + if got != test.want { + t.Errorf("%q: Expected %t but got %t.", test.name, test.want, got) + } + }) + } +} diff --git a/cmd/csaf_checker/main.go b/cmd/csaf_checker/main.go index 73a5cce..4efb351 100644 --- a/cmd/csaf_checker/main.go +++ b/cmd/csaf_checker/main.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) // Software-Engineering: 2022 Intevation GmbH @@ -12,7 +12,7 @@ package main import ( "log" - "github.com/csaf-poc/csaf_distribution/v3/internal/options" + "github.com/gocsaf/csaf/v3/internal/options" ) // run uses a processor to check all the given domains or direct urls diff --git a/cmd/csaf_checker/processor.go b/cmd/csaf_checker/processor.go index 7eaefef..584684c 100644 --- a/cmd/csaf_checker/processor.go +++ b/cmd/csaf_checker/processor.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) // Software-Engineering: 2021 Intevation GmbH @@ -15,7 +15,6 @@ import ( "crypto/sha512" "crypto/tls" "encoding/csv" - "encoding/json" "errors" "fmt" "io" @@ -29,11 +28,13 @@ import ( "strings" "time" + "github.com/gocsaf/csaf/v3/internal/misc" + "github.com/ProtonMail/gopenpgp/v2/crypto" "golang.org/x/time/rate" - "github.com/csaf-poc/csaf_distribution/v3/csaf" - "github.com/csaf-poc/csaf_distribution/v3/util" + "github.com/gocsaf/csaf/v3/csaf" + "github.com/gocsaf/csaf/v3/util" ) // topicMessages stores the collected topicMessages for a specific topic. @@ -53,6 +54,8 @@ type processor struct { pmd any keys *crypto.KeyRing labelChecker labelChecker + timesChanges map[string]time.Time + timesAdv map[string]time.Time invalidAdvisories topicMessages badFilenames topicMessages @@ -83,10 +86,8 @@ type reporter interface { report(*processor, *Domain) } -var ( - // errContinue indicates that the current check should continue. - errContinue = errors.New("continue") -) +// errContinue indicates that the current check should continue. +var errContinue = errors.New("continue") type whereType byte @@ -138,7 +139,7 @@ func (m *topicMessages) info(format string, args ...any) { m.add(InfoType, format, args...) } -// use signals that we going to use this topic. +// use signals that we're going to use this topic. func (m *topicMessages) use() { if *m == nil { *m = []Message{} @@ -164,9 +165,8 @@ func (m *topicMessages) hasErrors() bool { return false } -// newProcessor returns an initilaized processor. +// newProcessor returns an initialized processor. func newProcessor(cfg *config) (*processor, error) { - var validator csaf.RemoteValidator if cfg.RemoteValidator != "" { @@ -191,6 +191,9 @@ func newProcessor(cfg *config) (*processor, error) { advisories: map[csaf.TLPLabel]util.Set[string]{}, whiteAdvisories: map[identifier]bool{}, }, + timesAdv: map[string]time.Time{}, + timesChanges: map[string]time.Time{}, + noneTLS: util.Set[string]{}, }, nil } @@ -202,17 +205,17 @@ func (p *processor) close() { } } -// clean clears the fields values of the given processor. -func (p *processor) clean() { +// reset clears the fields values of the given processor. +func (p *processor) reset() { p.redirects = nil - p.noneTLS = nil - for k := range p.alreadyChecked { - delete(p.alreadyChecked, k) - } p.pmdURL = "" p.pmd256 = nil p.pmd = nil p.keys = nil + clear(p.alreadyChecked) + clear(p.noneTLS) + clear(p.timesAdv) + clear(p.timesChanges) p.invalidAdvisories.reset() p.badFilenames.reset() @@ -239,7 +242,6 @@ func (p *processor) clean() { // Then it calls the report method on each report from the given "reporters" parameter for each domain. // It returns a pointer to the report and nil, otherwise an error. func (p *processor) run(domains []string) (*Report, error) { - report := Report{ Date: ReportTime{Time: time.Now().UTC()}, Version: util.SemVersion, @@ -247,15 +249,17 @@ func (p *processor) run(domains []string) (*Report, error) { } for _, d := range domains { + p.reset() + if !p.checkProviderMetadata(d) { - // We cannot build a report if the provider metadata cannot be parsed. - log.Printf("Could not parse the Provider-Metadata.json of: %s\n", d) - continue + // We need to fail the domain if the PMD cannot be parsed. + p.badProviderMetadata.use() + p.badProviderMetadata.error("Could not parse the Provider-Metadata.json of: %s", d) + } if err := p.checkDomain(d); err != nil { - log.Printf("Failed to find valid provider-metadata.json for domain %s: %v. "+ - "Continuing with next domain.", d, err) - continue + p.badProviderMetadata.use() + p.badProviderMetadata.error("Failed to find valid provider-metadata.json for domain %s: %v. ", d, err) } domain := &Domain{Name: d} @@ -266,8 +270,10 @@ func (p *processor) run(domains []string) (*Report, error) { } if domain.Role == nil { - log.Printf("No role found in meta data. Ignoring domain %q\n", d) - continue + log.Printf("No role found in meta data for domain %q\n", d) + // Assume trusted provider to continue report generation + role := csaf.MetadataRoleTrustedProvider + domain.Role = &role } rules := roleRequirements(*domain.Role) @@ -287,7 +293,6 @@ func (p *processor) run(domains []string) (*Report, error) { domain.Passed = rules.eval(p) report.Domains = append(report.Domains, domain) - p.clean() } return &report, nil @@ -295,7 +300,6 @@ func (p *processor) run(domains []string) (*Report, error) { // fillMeta fills the report with extra informations from provider metadata. func (p *processor) fillMeta(domain *Domain) error { - if p.pmd == nil { return nil } @@ -321,7 +325,6 @@ func (p *processor) fillMeta(domain *Domain) error { // domainChecks compiles a list of checks which should be performed // for a given domain. func (p *processor) domainChecks(domain string) []func(*processor, string) error { - // If we have a direct domain url we dont need to // perform certain checks. direct := strings.HasPrefix(domain, "https://") @@ -376,9 +379,6 @@ func (p *processor) checkDomain(domain string) error { // checkTLS parses the given URL to check its schema, as a result it sets // the value of "noneTLS" field if it is not HTTPS. func (p *processor) checkTLS(u string) { - if p.noneTLS == nil { - p.noneTLS = util.Set[string]{} - } if x, err := url.Parse(u); err == nil && x.Scheme != "https" { p.noneTLS.Add(u) } @@ -391,7 +391,6 @@ func (p *processor) markChecked(s string, mask whereType) bool { } func (p *processor) checkRedirect(r *http.Request, via []*http.Request) error { - url := r.URL.String() p.checkTLS(url) if p.redirects == nil { @@ -429,16 +428,15 @@ func (p *processor) fullClient() util.Client { hClient.Transport = &http.Transport{ TLSClientConfig: &tlsConfig, + Proxy: http.ProxyFromEnvironment, } client := util.Client(&hClient) // Add extra headers. - if len(p.cfg.ExtraHeader) > 0 { - client = &util.HeaderClient{ - Client: client, - Header: p.cfg.ExtraHeader, - } + client = &util.HeaderClient{ + Client: client, + Header: p.cfg.ExtraHeader, } // Add optional URL logging. @@ -461,6 +459,7 @@ func (p *processor) basicClient() *http.Client { if p.cfg.Insecure { tr := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + Proxy: http.ProxyFromEnvironment, } return &http.Client{Transport: tr} } @@ -493,7 +492,6 @@ func (p *processor) usedAuthorizedClient() bool { // rolieFeedEntries loads the references to the advisory files for a given feed. func (p *processor) rolieFeedEntries(feed string) ([]csaf.AdvisoryFile, error) { - client := p.httpClient() res, err := client.Get(feed) p.badDirListings.use() @@ -518,7 +516,7 @@ func (p *processor) rolieFeedEntries(feed string) ([]csaf.AdvisoryFile, error) { return nil, nil, fmt.Errorf("%s: %v", feed, err) } var rolieDoc any - err = json.NewDecoder(bytes.NewReader(all)).Decode(&rolieDoc) + err = misc.StrictJSONParse(bytes.NewReader(all), &rolieDoc) return rfeed, rolieDoc, err }() if err != nil { @@ -536,7 +534,7 @@ func (p *processor) rolieFeedEntries(feed string) ([]csaf.AdvisoryFile, error) { if len(errors) > 0 { p.badProviderMetadata.error("%s: Validating against JSON schema failed:", feed) for _, msg := range errors { - p.badProviderMetadata.error(strings.ReplaceAll(msg, `%`, `%%`)) + p.badProviderMetadata.error("%s", strings.ReplaceAll(msg, `%`, `%%`)) } } @@ -544,7 +542,6 @@ func (p *processor) rolieFeedEntries(feed string) ([]csaf.AdvisoryFile, error) { var files []csaf.AdvisoryFile rfeed.Entries(func(entry *csaf.Entry) { - // Filter if we have date checking. if accept := p.cfg.Range; accept != nil { if t := time.Time(entry.Updated); !t.IsZero() && !accept.Contains(t) { @@ -593,11 +590,17 @@ func (p *processor) rolieFeedEntries(feed string) ([]csaf.AdvisoryFile, error) { var file csaf.AdvisoryFile - if sha256 != "" || sha512 != "" || sign != "" { - file = csaf.HashedAdvisoryFile{url, sha256, sha512, sign} - } else { - file = csaf.PlainAdvisoryFile(url) + switch { + case sha256 == "" && sha512 != "": + p.badROLIEFeed.info("%s has no sha256 hash file listed", url) + case sha256 != "" && sha512 == "": + p.badROLIEFeed.info("%s has no sha512 hash file listed", url) + case sha256 == "" && sha512 == "": + p.badROLIEFeed.error("No hash listed on ROLIE feed %s", url) + case sign == "": + p.badROLIEFeed.error("No signature listed on ROLIE feed %s", url) } + file = csaf.PlainAdvisoryFile{Path: url, SHA256: sha256, SHA512: sha512, Sign: sign} files = append(files, file) }) @@ -619,17 +622,13 @@ func makeAbsolute(base *url.URL) func(*url.URL) *url.URL { var yearFromURL = regexp.MustCompile(`.*/(\d{4})/[^/]+$`) +// integrity checks several csaf.AdvisoryFiles for formal +// mistakes, from conforming filenames to invalid advisories. func (p *processor) integrity( files []csaf.AdvisoryFile, - base string, mask whereType, lg func(MessageType, string, ...any), ) error { - b, err := url.Parse(base) - if err != nil { - return err - } - makeAbs := makeAbsolute(b) client := p.httpClient() var data bytes.Buffer @@ -640,9 +639,8 @@ func (p *processor) integrity( lg(ErrorType, "Bad URL %s: %v", f, err) continue } - fp = makeAbs(fp) - u := b.ResolveReference(fp).String() + u := fp.String() // Should this URL be ignored? if p.cfg.ignoreURL(u) { @@ -680,9 +678,9 @@ func (p *processor) integrity( continue } - // Warn if we do not get JSON. + // Error if we do not get JSON. if ct := res.Header.Get("Content-Type"); ct != "application/json" { - lg(WarnType, + lg(ErrorType, "The content type of %s should be 'application/json' but is '%s'", u, ct) } @@ -697,7 +695,7 @@ func (p *processor) integrity( if err := func() error { defer res.Body.Close() tee := io.TeeReader(res.Body, hasher) - return json.NewDecoder(tee).Decode(&doc) + return misc.StrictJSONParse(tee, &doc) }(); err != nil { lg(ErrorType, "Reading %s failed: %v", u, err) continue @@ -734,50 +732,59 @@ func (p *processor) integrity( // Check if file is in the right folder. p.badFolders.use() - if date, err := p.expr.Eval( - `$.document.tracking.initial_release_date`, doc); err != nil { - p.badFolders.error( - "Extracting 'initial_release_date' from %s failed: %v", u, err) - } else if text, ok := date.(string); !ok { - p.badFolders.error("'initial_release_date' is not a string in %s", u) - } else if d, err := time.Parse(time.RFC3339, text); err != nil { - p.badFolders.error( - "Parsing 'initial_release_date' as RFC3339 failed in %s: %v", u, err) - } else if folderYear == nil { + switch date, fault := p.extractTime(doc, `initial_release_date`, u); { + case fault != "": + p.badFolders.error("%s", fault) + case folderYear == nil: p.badFolders.error("No year folder found in %s", u) - } else if d.UTC().Year() != *folderYear { - p.badFolders.error("%s should be in folder %d", u, d.UTC().Year()) + case date.UTC().Year() != *folderYear: + p.badFolders.error("%s should be in folder %d", u, date.UTC().Year()) + } + current, fault := p.extractTime(doc, `current_release_date`, u) + if fault != "" { + p.badChanges.error("%s", fault) + } else { + p.timesAdv[f.URL()] = current } // Check hashes p.badIntegrities.use() - for _, x := range []struct { + type hash struct { ext string url func() string hash []byte - }{ - {"SHA256", f.SHA256URL, s256.Sum(nil)}, - {"SHA512", f.SHA512URL, s512.Sum(nil)}, - } { + } + hashes := []hash{} + if f.SHA256URL() != "" { + hashes = append(hashes, hash{"SHA256", f.SHA256URL, s256.Sum(nil)}) + } + if f.SHA512URL() != "" { + hashes = append(hashes, hash{"SHA512", f.SHA512URL, s512.Sum(nil)}) + } + + couldFetchHash := false + hashFetchErrors := []string{} + + for _, x := range hashes { hu, err := url.Parse(x.url()) if err != nil { lg(ErrorType, "Bad URL %s: %v", x.url(), err) continue } - hu = makeAbs(hu) - hashFile := b.ResolveReference(hu).String() + hashFile := hu.String() p.checkTLS(hashFile) if res, err = client.Get(hashFile); err != nil { - p.badIntegrities.error("Fetching %s failed: %v.", hashFile, err) + hashFetchErrors = append(hashFetchErrors, fmt.Sprintf("Fetching %s failed: %v.", hashFile, err)) continue } if res.StatusCode != http.StatusOK { - p.badIntegrities.error("Fetching %s failed: Status code %d (%s)", - hashFile, res.StatusCode, res.Status) + hashFetchErrors = append(hashFetchErrors, fmt.Sprintf("Fetching %s failed: Status code %d (%s)", + hashFile, res.StatusCode, res.Status)) continue } + couldFetchHash = true h, err := func() ([]byte, error) { defer res.Body.Close() return util.HashFromReader(res.Body) @@ -795,14 +802,26 @@ func (p *processor) integrity( x.ext, u, hashFile) } } + + msgType := ErrorType + // Log only as warning, if the other hash could be fetched + if couldFetchHash { + msgType = WarnType + } + if f.IsDirectory() { + msgType = InfoType + } + for _, fetchError := range hashFetchErrors { + p.badIntegrities.add(msgType, "%s", fetchError) + } + // Check signature su, err := url.Parse(f.SignURL()) if err != nil { lg(ErrorType, "Bad URL %s: %v", f.SignURL(), err) continue } - su = makeAbs(su) - sigFile := b.ResolveReference(su).String() + sigFile := su.String() p.checkTLS(sigFile) p.badSignatures.use() @@ -840,9 +859,48 @@ func (p *processor) integrity( } } + // If we tested an existing changes.csv + if len(p.timesAdv) > 0 && p.badChanges.used() { + // Iterate over all files again + for _, f := range files { + // If there was no previous error when extracting times from advisories and we have a valid time + if timeAdv, ok := p.timesAdv[f.URL()]; ok { + // If there was no previous error when extracting times from changes and the file was listed in changes.csv + if timeCha, ok := p.timesChanges[f.URL()]; ok { + // check if the time matches + if !timeAdv.Equal(timeCha) { + // if not, give an error and remove the pair so it isn't reported multiple times should integrity be called again + p.badChanges.error("Current release date in changes.csv and %s is not identical.", f.URL()) + delete(p.timesAdv, f.URL()) + delete(p.timesChanges, f.URL()) + } + } + } + } + } + return nil } +// extractTime extracts a time.Time value from a json document and returns it and an empty string or zero time alongside +// a string representing the error message that prevented obtaining the proper time value. +func (p *processor) extractTime(doc any, value string, u any) (time.Time, string) { + filter := "$.document.tracking." + value + date, err := p.expr.Eval(filter, doc) + if err != nil { + return time.Time{}, fmt.Sprintf("Extracting '%s' from %s failed: %v", value, u, err) + } + text, ok := date.(string) + if !ok { + return time.Time{}, fmt.Sprintf("'%s' is not a string in %s", value, u) + } + d, err := time.Parse(time.RFC3339, text) + if err != nil { + return time.Time{}, fmt.Sprintf("Parsing '%s' as RFC3339 failed in %s: %v", value, u, err) + } + return d, "" +} + // checkIndex fetches the "index.txt" and calls "checkTLS" method for HTTPS checks. // It extracts the file names from the file and passes them to "integrity" function. // It returns error if fetching/reading the file(s) fails, otherwise nil. @@ -883,11 +941,13 @@ func (p *processor) checkIndex(base string, mask whereType) error { scanner := bufio.NewScanner(res.Body) for line := 1; scanner.Scan(); line++ { u := scanner.Text() - if _, err := url.Parse(u); err != nil { + up, err := url.Parse(u) + if err != nil { p.badIntegrities.error("index.txt contains invalid URL %q in line %d", u, line) continue } - files = append(files, csaf.PlainAdvisoryFile(u)) + + files = append(files, csaf.DirectoryAdvisoryFile{Path: misc.JoinURL(bu, up).String()}) } return files, scanner.Err() }() @@ -902,7 +962,7 @@ func (p *processor) checkIndex(base string, mask whereType) error { // Block rolie checks. p.labelChecker.feedLabel = "" - return p.integrity(files, base, mask, p.badIndices.add) + return p.integrity(files, mask, p.badIndices.add) } // checkChanges fetches the "changes.csv" and calls the "checkTLS" method for HTTPs checks. @@ -910,7 +970,6 @@ func (p *processor) checkIndex(base string, mask whereType) error { // of the fields' values and if they are sorted properly. Then it passes the files to the // "integrity" functions. It returns error if some test fails, otherwise nil. func (p *processor) checkChanges(base string, mask whereType) error { - bu, err := url.Parse(base) if err != nil { return err @@ -969,9 +1028,15 @@ func (p *processor) checkChanges(base string, mask whereType) error { continue } path := r[pathColumn] - times, files = - append(times, t), - append(files, csaf.PlainAdvisoryFile(path)) + + pathURL, err := url.Parse(path) + if err != nil { + return nil, nil, err + } + + times, files = append(times, t), + append(files, csaf.DirectoryAdvisoryFile{Path: misc.JoinURL(bu, pathURL).String()}) + p.timesChanges[path] = t } return times, files, nil }() @@ -985,7 +1050,7 @@ func (p *processor) checkChanges(base string, mask whereType) error { if p.cfg.Range != nil { filtered = " (maybe filtered out by time interval)" } - p.badChanges.warn("no entries in changes.csv found" + filtered) + p.badChanges.warn("%s", "no entries in changes.csv found"+filtered) } if !sort.SliceIsSorted(times, func(i, j int) bool { @@ -997,7 +1062,7 @@ func (p *processor) checkChanges(base string, mask whereType) error { // Block rolie checks. p.labelChecker.feedLabel = "" - return p.integrity(files, base, mask, p.badChanges.add) + return p.integrity(files, mask, p.badChanges.add) } // empty checks if list of strings contains at least one none empty string. @@ -1143,7 +1208,6 @@ func (p *processor) checkMissing(string) error { // checkInvalid goes over all found adivisories URLs and checks // if file name conforms to standard. func (p *processor) checkInvalid(string) error { - p.badDirListings.use() var invalids []string @@ -1165,7 +1229,6 @@ func (p *processor) checkInvalid(string) error { // checkListing goes over all found adivisories URLs and checks // if their parent directory is listable. func (p *processor) checkListing(string) error { - p.badDirListings.use() pgs := pages{} @@ -1200,7 +1263,6 @@ func (p *processor) checkListing(string) error { // checkWhitePermissions checks if the TLP:WHITE advisories are // available with unprotected access. func (p *processor) checkWhitePermissions(string) error { - var ids []string for id, open := range p.labelChecker.whiteAdvisories { if !open { @@ -1226,7 +1288,6 @@ func (p *processor) checkWhitePermissions(string) error { // According to the result, the respective error messages added to // badProviderMetadata. func (p *processor) checkProviderMetadata(domain string) bool { - p.badProviderMetadata.use() client := p.httpClient() @@ -1237,8 +1298,8 @@ func (p *processor) checkProviderMetadata(domain string) bool { for i := range lpmd.Messages { p.badProviderMetadata.warn( - "Unexpected situation while loading provider-metadata.json: " + - lpmd.Messages[i].Message) + "Unexpected situation while loading provider-metadata.json: %s", + lpmd.Messages[i].Message) } if !lpmd.Valid() { @@ -1273,7 +1334,6 @@ func (p *processor) checkSecurity(domain string, legacy bool) (int, string) { // checkSecurityFolder checks the security.txt in a given folder. func (p *processor) checkSecurityFolder(folder string) string { - client := p.httpClient() path := folder + "security.txt" res, err := client.Get(path) @@ -1303,17 +1363,11 @@ func (p *processor) checkSecurityFolder(folder string) string { } // Try to load - up, err := url.Parse(u) + _, err = url.Parse(u) if err != nil { return fmt.Sprintf("CSAF URL '%s' invalid: %v", u, err) } - base, err := url.Parse(folder) - if err != nil { - return err.Error() - } - - u = base.ResolveReference(up).String() p.checkTLS(u) if res, err = client.Get(u); err != nil { return fmt.Sprintf("Cannot fetch %s from security.txt: %v", u, err) @@ -1338,50 +1392,52 @@ func (p *processor) checkSecurityFolder(folder string) string { // checkDNS checks if the "csaf.data.security.domain.tld" DNS record is available // and serves the "provider-metadata.json". -// It returns an empty string if all checks are passed, otherwise the errormessage. -func (p *processor) checkDNS(domain string) string { - +func (p *processor) checkDNS(domain string) { + p.badDNSPath.use() client := p.httpClient() path := "https://csaf.data.security." + domain res, err := client.Get(path) if err != nil { - return fmt.Sprintf("Fetching %s failed: %v", path, err) + p.badDNSPath.add(ErrorType, + "Fetching %s failed: %v", path, err) + return } if res.StatusCode != http.StatusOK { - return fmt.Sprintf("Fetching %s failed. Status code %d (%s)", + p.badDNSPath.add(ErrorType, "Fetching %s failed. Status code %d (%s)", path, res.StatusCode, res.Status) - } hash := sha256.New() defer res.Body.Close() content, err := io.ReadAll(res.Body) if err != nil { - return fmt.Sprintf("Error while reading the response from %s", path) + p.badDNSPath.add(ErrorType, + "Error while reading the response from %s", path) } hash.Write(content) if !bytes.Equal(hash.Sum(nil), p.pmd256) { - return fmt.Sprintf("%s does not serve the same provider-metadata.json as previously found", path) + p.badDNSPath.add(ErrorType, + "%s does not serve the same provider-metadata.json as previously found", + path) } - return "" } -// checkWellknownMetadataReporter checks if the provider-metadata.json file is -// available under the /.well-known/csaf/ directory. Returns the errormessage if -// an error was encountered, or an empty string otherwise -func (p *processor) checkWellknown(domain string) string { - +// checkWellknown checks if the provider-metadata.json file is +// available under the /.well-known/csaf/ directory. +func (p *processor) checkWellknown(domain string) { + p.badWellknownMetadata.use() client := p.httpClient() path := "https://" + domain + "/.well-known/csaf/provider-metadata.json" res, err := client.Get(path) if err != nil { - return fmt.Sprintf("Fetching %s failed: %v", path, err) + p.badWellknownMetadata.add(ErrorType, + "Fetching %s failed: %v", path, err) + return } if res.StatusCode != http.StatusOK { - return fmt.Sprintf("Fetching %s failed. Status code %d (%s)", + p.badWellknownMetadata.add(ErrorType, "Fetching %s failed. Status code %d (%s)", path, res.StatusCode, res.Status) } - return "" } // checkWellknownSecurityDNS @@ -1393,66 +1449,49 @@ func (p *processor) checkWellknown(domain string) string { // 4. Finally it checks if the "csaf.data.security.domain.tld" DNS record // is available and serves the "provider-metadata.json". // -// / -// If all three checks fail, errors are given, -// otherwise warnings for all failed checks. -// The function returns nil, unless errors outside the checks were found. -// In that case, errors are returned. +// For the security.txt checks, it first checks the default location. +// Should this lookup fail, a warning is will be given and a lookup +// for the legacy location will be made. If this fails as well, then an +// error is given. func (p *processor) checkWellknownSecurityDNS(domain string) error { - - warningsW := p.checkWellknown(domain) + p.checkWellknown(domain) // Security check for well known (default) and legacy location - warningsS, sDMessage := p.checkSecurity(domain, false) + warnings, sDMessage := p.checkSecurity(domain, false) // if the security.txt under .well-known was not okay // check for a security.txt within its legacy location sLMessage := "" - if warningsS == 1 { - warningsS, sLMessage = p.checkSecurity(domain, true) + if warnings == 1 { + warnings, sLMessage = p.checkSecurity(domain, true) } - warningsD := p.checkDNS(domain) - p.badWellknownMetadata.use() p.badSecurity.use() - p.badDNSPath.use() - var kind MessageType - if warningsS != 1 || warningsD == "" || warningsW == "" { - kind = WarnType - } else { - kind = ErrorType + // Report about Securitytxt: + // Only report about default location if it was succesful (0). + // Report default and legacy as errors if neither was succesful (1). + // Warn about missing security in the default position if not found + // but found in the legacy location, and inform about finding it there (2). + switch warnings { + case 0: + p.badSecurity.add(InfoType, "%s", sDMessage) + case 1: + p.badSecurity.add(ErrorType, "%s", sDMessage) + p.badSecurity.add(ErrorType, "%s", sLMessage) + case 2: + p.badSecurity.add(WarnType, "%s", sDMessage) + p.badSecurity.add(InfoType, "%s", sLMessage) } - // Info, Warning or Error depending on kind and warningS - kindSD := kind - if warningsS == 0 { - kindSD = InfoType - } - kindSL := kind - if warningsS == 2 { - kindSL = InfoType - } + p.checkDNS(domain) - if warningsW != "" { - p.badWellknownMetadata.add(kind, warningsW) - } - p.badSecurity.add(kindSD, sDMessage) - // only if the well-known security.txt was not successful: - // report about the legacy location - if warningsS != 0 { - p.badSecurity.add(kindSL, sLMessage) - } - if warningsD != "" { - p.badDNSPath.add(kind, warningsD) - } return nil } // checkPGPKeys checks if the OpenPGP keys are available and valid, fetches -// the the remotely keys and compares the fingerprints. -// As a result of these a respective error messages are passed to badPGP method -// in case of errors. It returns nil if all checks are passed. +// the remote pubkeys and compares the fingerprints. +// As a result of these checks respective error messages are passed +// to badPGP methods. It returns nil if all checks are passed. func (p *processor) checkPGPKeys(_ string) error { - p.badPGPs.use() src, err := p.expr.Eval("$.public_openpgp_keys", p.pmd) @@ -1476,11 +1515,6 @@ func (p *processor) checkPGPKeys(_ string) error { client := p.httpClient() - base, err := url.Parse(p.pmdURL) - if err != nil { - return err - } - for i := range keys { key := &keys[i] if key.URL == nil { @@ -1493,10 +1527,11 @@ func (p *processor) checkPGPKeys(_ string) error { continue } - u := base.ResolveReference(up).String() + // Todo: refactor all methods to directly accept *url.URL + u := up.String() p.checkTLS(u) - res, err := client.Get(u) + res, err := client.Get(*key.URL) if err != nil { p.badPGPs.error("Fetching public OpenPGP key %s failed: %v.", u, err) continue @@ -1511,14 +1546,13 @@ func (p *processor) checkPGPKeys(_ string) error { defer res.Body.Close() return crypto.NewKeyFromArmoredReader(res.Body) }() - if err != nil { p.badPGPs.error("Reading public OpenPGP key %s failed: %v", u, err) continue } if !strings.EqualFold(ckey.GetFingerprint(), string(key.Fingerprint)) { - p.badPGPs.error("Fingerprint of public OpenPGP key %s does not match remotely loaded.", u) + p.badPGPs.error("Given Fingerprint (%q) of public OpenPGP key %q does not match remotely loaded (%q).", string(key.Fingerprint), u, ckey.GetFingerprint()) continue } if p.keys == nil { diff --git a/cmd/csaf_checker/processor_test.go b/cmd/csaf_checker/processor_test.go new file mode 100644 index 0000000..4d13908 --- /dev/null +++ b/cmd/csaf_checker/processor_test.go @@ -0,0 +1,259 @@ +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. +// +// SPDX-License-Identifier: Apache-2.0 +// +// SPDX-FileCopyrightText: 2023 German Federal Office for Information Security (BSI) +// Software-Engineering: 2023 Intevation GmbH + +package main + +import ( + "bytes" + "encoding/json" + "net/http/httptest" + "os" + "reflect" + "slices" + "strings" + "testing" + "text/template" + + "github.com/gocsaf/csaf/v3/internal/testutil" + "github.com/gocsaf/csaf/v3/util" +) + +func getRequirementTestData(t *testing.T, params testutil.ProviderParams, directoryProvider bool) []Requirement { + path := "../../testdata/processor-requirements/" + if params.EnableSha256 { + path += "sha256-" + } + if params.EnableSha512 { + path += "sha512-" + } + if params.ForbidSha256 { + path += "forbid-sha256-" + } + if params.ForbidSha512 { + path += "forbid-sha512-" + } + if directoryProvider { + path += "directory" + } else { + path += "rolie" + } + path += ".json" + + content, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + + tmplt, err := template.New("base").Parse(string(content)) + if err != nil { + t.Fatal(err) + } + + var output bytes.Buffer + err = tmplt.Execute(&output, params) + if err != nil { + t.Fatal(err) + } + var requirement []Requirement + err = json.Unmarshal(output.Bytes(), &requirement) + if err != nil { + t.Fatal(err) + } + return requirement +} + +func TestContentTypeReport(t *testing.T) { + serverURL := "" + params := testutil.ProviderParams{ + URL: "", + EnableSha256: true, + EnableSha512: true, + ForbidSha256: true, + ForbidSha512: true, + JSONContentType: "application/json; charset=utf-8", + } + server := httptest.NewTLSServer(testutil.ProviderHandler(¶ms, false)) + defer server.Close() + + serverURL = server.URL + params.URL = server.URL + + hClient := server.Client() + client := util.Client(hClient) + + cfg := config{} + err := cfg.prepare() + if err != nil { + t.Fatalf("SHA marking config failed: %v", err) + } + p, err := newProcessor(&cfg) + if err != nil { + t.Fatalf("could not init downloader: %v", err) + } + p.client = client + + report, err := p.run([]string{serverURL + "/provider-metadata.json"}) + if err != nil { + t.Errorf("Content-Type-Report: Expected no error, got: %v", err) + } + + got := report.Domains[0].Requirements + idx := slices.IndexFunc(got, func(e *Requirement) bool { + return e.Num == 7 + }) + if idx == -1 { + t.Error("Content-Type-Report: Could not find requirement") + } else { + message := got[idx].Messages[0] + if message.Type != ErrorType || !strings.Contains(message.Text, "should be 'application/json'") { + t.Errorf("Content-Type-Report: Content Type Error, got %v", message) + } + } + + p.close() +} + +func TestShaMarking(t *testing.T) { + tests := []struct { + name string + directoryProvider bool + enableSha256 bool + enableSha512 bool + forbidSha256 bool + forbidSha512 bool + }{ + { + name: "deliver sha256 and sha512", + directoryProvider: false, + enableSha256: true, + enableSha512: true, + }, + { + name: "enable sha256 and sha512, forbid fetching", + directoryProvider: false, + enableSha256: true, + enableSha512: true, + forbidSha256: true, + forbidSha512: true, + }, + { + name: "enable sha256 and sha512, forbid sha256", + directoryProvider: false, + enableSha256: true, + enableSha512: true, + forbidSha256: true, + forbidSha512: false, + }, + { + name: "enable sha256 and sha512, forbid sha512", + directoryProvider: false, + enableSha256: true, + enableSha512: true, + forbidSha256: false, + forbidSha512: true, + }, + { + name: "only deliver sha256", + directoryProvider: false, + enableSha256: true, + enableSha512: false, + }, + { + name: "only deliver sha512", + directoryProvider: false, + enableSha256: false, + enableSha512: true, + }, + { + name: "deliver sha256 and sha512, directory provider", + directoryProvider: true, + enableSha256: true, + enableSha512: true, + }, + { + name: "only deliver sha256, directory provider", + directoryProvider: true, + enableSha256: true, + enableSha512: false, + }, + { + name: "only deliver sha512, directory provider", + directoryProvider: true, + enableSha256: false, + enableSha512: true, + }, + { + name: "no hash", + directoryProvider: false, + enableSha256: false, + enableSha512: false, + }, + { + name: "no hash, directory provider", + directoryProvider: true, + enableSha256: false, + enableSha512: false, + }, + } + + t.Parallel() + for _, testToRun := range tests { + test := testToRun + t.Run(test.name, func(tt *testing.T) { + tt.Parallel() + serverURL := "" + params := testutil.ProviderParams{ + URL: "", + EnableSha256: test.enableSha256, + EnableSha512: test.enableSha512, + ForbidSha256: test.forbidSha256, + ForbidSha512: test.forbidSha512, + } + server := httptest.NewTLSServer(testutil.ProviderHandler(¶ms, test.directoryProvider)) + defer server.Close() + + serverURL = server.URL + params.URL = server.URL + + hClient := server.Client() + client := util.Client(hClient) + + cfg := config{} + err := cfg.prepare() + if err != nil { + t.Fatalf("SHA marking config failed: %v", err) + } + p, err := newProcessor(&cfg) + if err != nil { + t.Fatalf("could not init downloader: %v", err) + } + p.client = client + + report, err := p.run([]string{serverURL + "/provider-metadata.json"}) + if err != nil { + t.Errorf("SHA marking %v: Expected no error, got: %v", test.name, err) + } + expected := getRequirementTestData(t, + testutil.ProviderParams{ + URL: serverURL, + EnableSha256: test.enableSha256, + EnableSha512: test.enableSha512, + ForbidSha256: test.forbidSha256, + ForbidSha512: test.forbidSha512, + }, + test.directoryProvider) + for i, got := range report.Domains[0].Requirements { + if !reflect.DeepEqual(expected[i], *got) { + t.Errorf("SHA marking %v: Expected %v, got %v", test.name, expected[i], *got) + } + } + + p.close() + }) + } +} diff --git a/cmd/csaf_checker/report.go b/cmd/csaf_checker/report.go index 2b53bb2..58ed25a 100644 --- a/cmd/csaf_checker/report.go +++ b/cmd/csaf_checker/report.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) // Software-Engineering: 2021 Intevation GmbH @@ -18,8 +18,8 @@ import ( "os" "time" - "github.com/csaf-poc/csaf_distribution/v3/csaf" - "github.com/csaf-poc/csaf_distribution/v3/internal/models" + "github.com/gocsaf/csaf/v3/csaf" + "github.com/gocsaf/csaf/v3/internal/models" ) // MessageType is the kind of the message. diff --git a/cmd/csaf_checker/reporters.go b/cmd/csaf_checker/reporters.go index c707a14..9cd3fc8 100644 --- a/cmd/csaf_checker/reporters.go +++ b/cmd/csaf_checker/reporters.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) // Software-Engineering: 2022 Intevation GmbH @@ -13,7 +13,7 @@ import ( "sort" "strings" - "github.com/csaf-poc/csaf_distribution/v3/util" + "github.com/gocsaf/csaf/v3/util" ) type ( @@ -178,7 +178,7 @@ func (r *tlpAmberRedReporter) report(p *processor, domain *Domain) { return } if len(p.badAmberRedPermissions) == 0 { - req.message(InfoType, "All tested advisories labeled TLP:WHITE or TLP:RED were access-protected.") + req.message(InfoType, "All tested advisories labeled TLP:AMBER or TLP:RED were access-protected.") return } req.Messages = p.badAmberRedPermissions diff --git a/cmd/csaf_checker/roliecheck.go b/cmd/csaf_checker/roliecheck.go index 94b1c2f..f510992 100644 --- a/cmd/csaf_checker/roliecheck.go +++ b/cmd/csaf_checker/roliecheck.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2023 German Federal Office for Information Security (BSI) // Software-Engineering: 2023 Intevation GmbH @@ -15,8 +15,8 @@ import ( "sort" "strings" - "github.com/csaf-poc/csaf_distribution/v3/csaf" - "github.com/csaf-poc/csaf_distribution/v3/util" + "github.com/gocsaf/csaf/v3/csaf" + "github.com/gocsaf/csaf/v3/util" ) // identifier consist of document/tracking/id and document/publisher/namespace, @@ -216,11 +216,6 @@ func defaults[T any](p *T, def T) T { // processROLIEFeeds goes through all ROLIE feeds and checks their // integrity and completeness. func (p *processor) processROLIEFeeds(feeds [][]csaf.Feed) error { - - base, err := url.Parse(p.pmdURL) - if err != nil { - return err - } p.badROLIEFeed.use() advisories := map[*csaf.Feed][]csaf.AdvisoryFile{} @@ -232,12 +227,11 @@ func (p *processor) processROLIEFeeds(feeds [][]csaf.Feed) error { if feed.URL == nil { continue } - up, err := url.Parse(string(*feed.URL)) + feedBase, err := url.Parse(string(*feed.URL)) if err != nil { p.badProviderMetadata.error("Invalid URL %s in feed: %v.", *feed.URL, err) continue } - feedBase := base.ResolveReference(up) feedURL := feedBase.String() p.checkTLS(feedURL) @@ -264,13 +258,12 @@ func (p *processor) processROLIEFeeds(feeds [][]csaf.Feed) error { continue } - up, err := url.Parse(string(*feed.URL)) + feedURL, err := url.Parse(string(*feed.URL)) if err != nil { p.badProviderMetadata.error("Invalid URL %s in feed: %v.", *feed.URL, err) continue } - feedURL := base.ResolveReference(up) feedBase, err := util.BaseURL(feedURL) if err != nil { p.badProviderMetadata.error("Bad base path: %v", err) @@ -290,7 +283,7 @@ func (p *processor) processROLIEFeeds(feeds [][]csaf.Feed) error { // TODO: Issue a warning if we want check AMBER+ without an // authorizing client. - if err := p.integrity(files, feedBase, rolieMask, p.badProviderMetadata.add); err != nil { + if err := p.integrity(files, rolieMask, p.badProviderMetadata.add); err != nil { if err != errContinue { return err } @@ -319,13 +312,12 @@ func (p *processor) processROLIEFeeds(feeds [][]csaf.Feed) error { continue } - up, err := url.Parse(string(*feed.URL)) + feedBase, err := url.Parse(string(*feed.URL)) if err != nil { p.badProviderMetadata.error("Invalid URL %s in feed: %v.", *feed.URL, err) continue } - feedBase := base.ResolveReference(up) makeAbs := makeAbsolute(feedBase) label := defaults(feed.TLPLabel, csaf.TLPLabelUnlabeled) diff --git a/cmd/csaf_checker/rules.go b/cmd/csaf_checker/rules.go index 6981b6b..e04388d 100644 --- a/cmd/csaf_checker/rules.go +++ b/cmd/csaf_checker/rules.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2023 German Federal Office for Information Security (BSI) // Software-Engineering: 2023 Intevation GmbH @@ -12,7 +12,7 @@ import ( "fmt" "sort" - "github.com/csaf-poc/csaf_distribution/v3/csaf" + "github.com/gocsaf/csaf/v3/csaf" ) type ruleCondition int diff --git a/cmd/csaf_downloader/config.go b/cmd/csaf_downloader/config.go index 367780f..da911d7 100644 --- a/cmd/csaf_downloader/config.go +++ b/cmd/csaf_downloader/config.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) // Software-Engineering: 2022 Intevation GmbH @@ -13,17 +13,16 @@ import ( "fmt" "io" "log" + "log/slog" "net/http" "os" "path/filepath" "time" - "golang.org/x/exp/slog" - - "github.com/csaf-poc/csaf_distribution/v3/internal/certs" - "github.com/csaf-poc/csaf_distribution/v3/internal/filter" - "github.com/csaf-poc/csaf_distribution/v3/internal/models" - "github.com/csaf-poc/csaf_distribution/v3/internal/options" + "github.com/gocsaf/csaf/v3/internal/certs" + "github.com/gocsaf/csaf/v3/internal/filter" + "github.com/gocsaf/csaf/v3/internal/models" + "github.com/gocsaf/csaf/v3/internal/options" ) const ( @@ -42,6 +41,13 @@ const ( validationUnsafe = validationMode("unsafe") ) +type hashAlgorithm string + +const ( + algSha256 = hashAlgorithm("sha256") + algSha512 = hashAlgorithm("sha512") +) + type config struct { Directory string `short:"d" long:"directory" description:"DIRectory to store the downloaded files in" value-name:"DIR" toml:"directory"` Insecure bool `long:"insecure" description:"Do not check TLS certificates from provider" toml:"insecure"` @@ -58,6 +64,8 @@ type config struct { IgnorePattern []string `long:"ignore_pattern" short:"i" description:"Do not download files if their URLs match any of the given PATTERNs" value-name:"PATTERN" toml:"ignore_pattern"` ExtraHeader http.Header `long:"header" short:"H" description:"One or more extra HTTP header fields" toml:"header"` + EnumeratePMDOnly bool `long:"enumerate_pmd_only" description:"If this flag is set to true, the downloader will only enumerate valid provider metadata files, but not download documents" toml:"enumerate_pmd_only"` + RemoteValidator string `long:"validator" description:"URL to validate documents remotely" value-name:"URL" toml:"validator"` RemoteValidatorCache string `long:"validator_cache" description:"FILE to cache remote validations" value-name:"FILE" toml:"validator_cache"` RemoteValidatorPresets []string `long:"validator_preset" description:"One or more PRESETS to validate remotely" value-name:"PRESETS" toml:"validator_preset"` @@ -78,6 +86,9 @@ type config struct { clientCerts []tls.Certificate ignorePattern filter.PatternMatcher + + //lint:ignore SA5008 We are using choice or than once: sha256, sha512 + PreferredHash hashAlgorithm `long:"preferred_hash" choice:"sha256" choice:"sha512" value-name:"HASH" description:"HASH to prefer" toml:"preferred_hash"` } // configPaths are the potential file locations of the config file. @@ -219,7 +230,7 @@ func (cfg *config) prepareLogging() error { w = f } ho := slog.HandlerOptions{ - //AddSource: true, + // AddSource: true, Level: cfg.LogLevel.Level, ReplaceAttr: dropSubSeconds, } diff --git a/cmd/csaf_downloader/downloader.go b/cmd/csaf_downloader/downloader.go index 7fa0c7c..4edd724 100644 --- a/cmd/csaf_downloader/downloader.go +++ b/cmd/csaf_downloader/downloader.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2022, 2023 German Federal Office for Information Security (BSI) // Software-Engineering: 2022, 2023 Intevation GmbH @@ -19,29 +19,37 @@ import ( "fmt" "hash" "io" + "log/slog" "net/http" "net/url" "os" "path" "path/filepath" + "slices" "strconv" "strings" "sync" "time" - "golang.org/x/exp/slog" - "github.com/ProtonMail/gopenpgp/v2/crypto" "golang.org/x/time/rate" - "github.com/csaf-poc/csaf_distribution/v3/csaf" - "github.com/csaf-poc/csaf_distribution/v3/util" + "github.com/gocsaf/csaf/v3/csaf" + "github.com/gocsaf/csaf/v3/internal/misc" + "github.com/gocsaf/csaf/v3/util" ) +type hashFetchInfo struct { + url string + preferred bool + warn bool + hashType hashAlgorithm +} + type downloader struct { cfg *config + client *util.Client // Used for testing keys *crypto.KeyRing - eval *util.PathEval validator csaf.RemoteValidator forwarder *forwarder mkdirMu sync.Mutex @@ -55,7 +63,6 @@ type downloader struct { const failedValidationDir = "failed_validation" func newDownloader(cfg *config) (*downloader, error) { - var validator csaf.RemoteValidator if cfg.RemoteValidator != "" { @@ -74,7 +81,6 @@ func newDownloader(cfg *config) (*downloader, error) { return &downloader{ cfg: cfg, - eval: util.NewPathEval(), validator: validator, }, nil } @@ -106,7 +112,6 @@ func logRedirect(req *http.Request, via []*http.Request) error { } func (d *downloader) httpClient() util.Client { - hClient := http.Client{} if d.cfg.verbose() { @@ -124,16 +129,20 @@ func (d *downloader) httpClient() util.Client { hClient.Transport = &http.Transport{ TLSClientConfig: &tlsConfig, + Proxy: http.ProxyFromEnvironment, } client := util.Client(&hClient) + // Overwrite for testing purposes + if d.client != nil { + client = *d.client + } + // Add extra headers. - if len(d.cfg.ExtraHeader) > 0 { - client = &util.HeaderClient{ - Client: client, - Header: d.cfg.ExtraHeader, - } + client = &util.HeaderClient{ + Client: client, + Header: d.cfg.ExtraHeader, } // Add optional URL logging. @@ -165,6 +174,36 @@ func httpLog(who string) func(string, string) { } } +func (d *downloader) enumerate(domain string) error { + client := d.httpClient() + + loader := csaf.NewProviderMetadataLoader(client) + lpmd := loader.Enumerate(domain) + + docs := []any{} + + for _, pmd := range lpmd { + if d.cfg.verbose() { + for i := range pmd.Messages { + slog.Debug("Enumerating provider-metadata.json", + "domain", domain, + "message", pmd.Messages[i].Message) + } + } + + docs = append(docs, pmd.Document) + } + + // print the results + doc, err := json.MarshalIndent(docs, "", " ") + if err != nil { + slog.Error("Couldn't marshal PMD document json") + } + fmt.Println(string(doc)) + + return nil +} + func (d *downloader) download(ctx context.Context, domain string) error { client := d.httpClient() @@ -172,7 +211,14 @@ func (d *downloader) download(ctx context.Context, domain string) error { lpmd := loader.Load(domain) - if d.cfg.verbose() { + if !lpmd.Valid() { + for i := range lpmd.Messages { + slog.Error("Loading provider-metadata.json", + "domain", domain, + "message", lpmd.Messages[i].Message) + } + return fmt.Errorf("no valid provider-metadata.json found for '%s'", domain) + } else if d.cfg.verbose() { for i := range lpmd.Messages { slog.Debug("Loading provider-metadata.json", "domain", domain, @@ -180,28 +226,26 @@ func (d *downloader) download(ctx context.Context, domain string) error { } } - if !lpmd.Valid() { - return fmt.Errorf("no valid provider-metadata.json found for '%s'", domain) - } - - base, err := url.Parse(lpmd.URL) + pmdURL, err := url.Parse(lpmd.URL) if err != nil { return fmt.Errorf("invalid URL '%s': %v", lpmd.URL, err) } + expr := util.NewPathEval() + if err := d.loadOpenPGPKeys( client, lpmd.Document, - base, + expr, ); err != nil { return err } afp := csaf.NewAdvisoryFileProcessor( client, - d.eval, + expr, lpmd.Document, - base) + pmdURL) // Do we need time range based filtering? if d.cfg.Range != nil { @@ -220,7 +264,6 @@ func (d *downloader) downloadFiles( label csaf.TLPLabel, files []csaf.AdvisoryFile, ) error { - var ( advisoryCh = make(chan csaf.AdvisoryFile) errorCh = make(chan error) @@ -267,10 +310,9 @@ allFiles: func (d *downloader) loadOpenPGPKeys( client util.Client, doc any, - base *url.URL, + expr *util.PathEval, ) error { - - src, err := d.eval.Eval("$.public_openpgp_keys", doc) + src, err := expr.Eval("$.public_openpgp_keys", doc) if err != nil { // no keys. return nil @@ -292,7 +334,7 @@ func (d *downloader) loadOpenPGPKeys( if key.URL == nil { continue } - up, err := url.Parse(*key.URL) + u, err := url.Parse(*key.URL) if err != nil { slog.Warn("Invalid URL", "url", *key.URL, @@ -300,9 +342,7 @@ func (d *downloader) loadOpenPGPKeys( continue } - u := base.ResolveReference(up).String() - - res, err := client.Get(u) + res, err := client.Get(u.String()) if err != nil { slog.Warn( "Fetching public OpenPGP key failed", @@ -323,7 +363,6 @@ func (d *downloader) loadOpenPGPKeys( defer res.Body.Close() return crypto.NewKeyFromArmoredReader(res.Body) }() - if err != nil { slog.Warn( "Reading public OpenPGP key failed", @@ -335,7 +374,7 @@ func (d *downloader) loadOpenPGPKeys( if !strings.EqualFold(ckey.GetFingerprint(), string(key.Fingerprint)) { slog.Warn( "Fingerprint of public OpenPGP key does not match remotely loaded", - "url", u) + "url", u, "fingerprint", key.Fingerprint, "remote-fingerprint", ckey.GetFingerprint()) continue } if d.keys == nil { @@ -375,6 +414,321 @@ func (d *downloader) logValidationIssues(url string, errors []string, err error) } } +// downloadContext stores the common context of a downloader. +type downloadContext struct { + d *downloader + client util.Client + data bytes.Buffer + lastDir string + initialReleaseDate time.Time + dateExtract func(any) error + lower string + stats stats + expr *util.PathEval +} + +func newDownloadContext(d *downloader, label csaf.TLPLabel) *downloadContext { + dc := &downloadContext{ + d: d, + client: d.httpClient(), + lower: strings.ToLower(string(label)), + expr: util.NewPathEval(), + } + dc.dateExtract = util.TimeMatcher(&dc.initialReleaseDate, time.RFC3339) + return dc +} + +func (dc *downloadContext) downloadAdvisory( + file csaf.AdvisoryFile, + errorCh chan<- error, +) error { + u, err := url.Parse(file.URL()) + if err != nil { + dc.stats.downloadFailed++ + slog.Warn("Ignoring invalid URL", + "url", file.URL(), + "error", err) + return nil + } + + if dc.d.cfg.ignoreURL(file.URL()) { + slog.Debug("Ignoring URL", "url", file.URL()) + return nil + } + + // Ignore not conforming filenames. + filename := filepath.Base(u.Path) + if !util.ConformingFileName(filename) { + dc.stats.filenameFailed++ + slog.Warn("Ignoring none conforming filename", + "filename", filename) + return nil + } + + resp, err := dc.client.Get(file.URL()) + if err != nil { + dc.stats.downloadFailed++ + slog.Warn("Cannot GET", + "url", file.URL(), + "error", err) + return nil + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + dc.stats.downloadFailed++ + slog.Warn("Cannot load", + "url", file.URL(), + "status", resp.Status, + "status_code", resp.StatusCode) + return nil + } + + // Warn if we do not get JSON. + if ct := resp.Header.Get("Content-Type"); ct != "application/json" { + slog.Warn("Content type is not 'application/json'", + "url", file.URL(), + "content_type", ct) + } + + var ( + writers []io.Writer + s256, s512 hash.Hash + s256Data, s512Data []byte + remoteSHA256, remoteSHA512 []byte + signData []byte + ) + + hashToFetch := []hashFetchInfo{} + if file.SHA512URL() != "" { + hashToFetch = append(hashToFetch, hashFetchInfo{ + url: file.SHA512URL(), + warn: true, + hashType: algSha512, + preferred: strings.EqualFold(string(dc.d.cfg.PreferredHash), string(algSha512)), + }) + } else { + slog.Info("SHA512 not present") + } + if file.SHA256URL() != "" { + hashToFetch = append(hashToFetch, hashFetchInfo{ + url: file.SHA256URL(), + warn: true, + hashType: algSha256, + preferred: strings.EqualFold(string(dc.d.cfg.PreferredHash), string(algSha256)), + }) + } else { + slog.Info("SHA256 not present") + } + if file.IsDirectory() { + for i := range hashToFetch { + hashToFetch[i].warn = false + } + } + + remoteSHA256, s256Data, remoteSHA512, s512Data = loadHashes(dc.client, hashToFetch) + if remoteSHA512 != nil { + s512 = sha512.New() + writers = append(writers, s512) + } + if remoteSHA256 != nil { + s256 = sha256.New() + writers = append(writers, s256) + } + + // Remember the data as we need to store it to file later. + dc.data.Reset() + writers = append(writers, &dc.data) + + // Download the advisory and hash it. + hasher := io.MultiWriter(writers...) + + var doc any + + tee := io.TeeReader(resp.Body, hasher) + + if err := misc.StrictJSONParse(tee, &doc); err != nil { + dc.stats.downloadFailed++ + slog.Warn("Downloading failed", + "url", file.URL(), + "error", err) + return nil + } + + // Compare the checksums. + s256Check := func() error { + if s256 != nil && !bytes.Equal(s256.Sum(nil), remoteSHA256) { + dc.stats.sha256Failed++ + return fmt.Errorf("SHA256 checksum of %s does not match", file.URL()) + } + return nil + } + + s512Check := func() error { + if s512 != nil && !bytes.Equal(s512.Sum(nil), remoteSHA512) { + dc.stats.sha512Failed++ + return fmt.Errorf("SHA512 checksum of %s does not match", file.URL()) + } + return nil + } + + // Validate OpenPGP signature. + keysCheck := func() error { + // Only check signature if we have loaded keys. + if dc.d.keys == nil { + return nil + } + var sign *crypto.PGPSignature + sign, signData, err = loadSignature(dc.client, file.SignURL()) + if err != nil { + slog.Warn("Downloading signature failed", + "url", file.SignURL(), + "error", err) + } + if sign != nil { + if err := dc.d.checkSignature(dc.data.Bytes(), sign); err != nil { + if !dc.d.cfg.IgnoreSignatureCheck { + dc.stats.signatureFailed++ + return fmt.Errorf("cannot verify signature for %s: %v", file.URL(), err) + } + } + } + return nil + } + + // Validate against CSAF schema. + schemaCheck := func() error { + if errors, err := csaf.ValidateCSAF(doc); err != nil || len(errors) > 0 { + dc.stats.schemaFailed++ + dc.d.logValidationIssues(file.URL(), errors, err) + return fmt.Errorf("schema validation for %q failed", file.URL()) + } + return nil + } + + // Validate if filename is conforming. + filenameCheck := func() error { + if err := util.IDMatchesFilename(dc.expr, doc, filename); err != nil { + dc.stats.filenameFailed++ + return fmt.Errorf("filename not conforming %s: %s", file.URL(), err) + } + return nil + } + + // Validate against remote validator. + remoteValidatorCheck := func() error { + if dc.d.validator == nil { + return nil + } + rvr, err := dc.d.validator.Validate(doc) + if err != nil { + errorCh <- fmt.Errorf( + "calling remote validator on %q failed: %w", + file.URL(), err) + return nil + } + if !rvr.Valid { + dc.stats.remoteFailed++ + return fmt.Errorf("remote validation of %q failed", file.URL()) + } + return nil + } + + // Run all the validations. + valStatus := notValidatedValidationStatus + for _, check := range []func() error{ + s256Check, + s512Check, + keysCheck, + schemaCheck, + filenameCheck, + remoteValidatorCheck, + } { + if err := check(); err != nil { + slog.Error("Validation check failed", "error", err) + valStatus.update(invalidValidationStatus) + if dc.d.cfg.ValidationMode == validationStrict { + return nil + } + } + } + valStatus.update(validValidationStatus) + + // Send to forwarder + if dc.d.forwarder != nil { + dc.d.forwarder.forward( + filename, dc.data.String(), + valStatus, + string(s256Data), + string(s512Data)) + } + + if dc.d.cfg.NoStore { + // Do not write locally. + if valStatus == validValidationStatus { + dc.stats.succeeded++ + } + return nil + } + + if err := dc.expr.Extract( + `$.document.tracking.initial_release_date`, dc.dateExtract, false, doc, + ); err != nil { + slog.Warn("Cannot extract initial_release_date from advisory", + "url", file.URL()) + dc.initialReleaseDate = time.Now() + } + dc.initialReleaseDate = dc.initialReleaseDate.UTC() + + // Advisories that failed validation are stored in a special folder. + var newDir string + if valStatus != validValidationStatus { + newDir = path.Join(dc.d.cfg.Directory, failedValidationDir) + } else { + newDir = dc.d.cfg.Directory + } + + // Do we have a configured destination folder? + if dc.d.cfg.Folder != "" { + newDir = path.Join(newDir, dc.d.cfg.Folder) + } else { + newDir = path.Join(newDir, dc.lower, strconv.Itoa(dc.initialReleaseDate.Year())) + } + + if newDir != dc.lastDir { + if err := dc.d.mkdirAll(newDir, 0755); err != nil { + errorCh <- err + return nil + } + dc.lastDir = newDir + } + + // Write advisory to file + path := filepath.Join(dc.lastDir, filename) + + // Write data to disk. + for _, x := range []struct { + p string + d []byte + }{ + {path, dc.data.Bytes()}, + {path + ".sha256", s256Data}, + {path + ".sha512", s512Data}, + {path + ".asc", signData}, + } { + if x.d != nil { + if err := os.WriteFile(x.p, x.d, 0644); err != nil { + errorCh <- err + return nil + } + } + } + + dc.stats.succeeded++ + slog.Info("Written advisory", "path", path) + return nil +} + func (d *downloader) downloadWorker( ctx context.Context, wg *sync.WaitGroup, @@ -384,20 +738,11 @@ func (d *downloader) downloadWorker( ) { defer wg.Done() - var ( - client = d.httpClient() - data bytes.Buffer - lastDir string - initialReleaseDate time.Time - dateExtract = util.TimeMatcher(&initialReleaseDate, time.RFC3339) - lower = strings.ToLower(string(label)) - stats = stats{} - ) + dc := newDownloadContext(d, label) // Add collected stats back to total. - defer d.addStats(&stats) + defer d.addStats(&dc.stats) -nextAdvisory: for { var file csaf.AdvisoryFile var ok bool @@ -409,274 +754,10 @@ nextAdvisory: case <-ctx.Done(): return } - - u, err := url.Parse(file.URL()) - if err != nil { - stats.downloadFailed++ - slog.Warn("Ignoring invalid URL", - "url", file.URL(), - "error", err) - continue + if err := dc.downloadAdvisory(file, errorCh); err != nil { + slog.Error("download terminated", "error", err) + return } - - if d.cfg.ignoreURL(file.URL()) { - slog.Debug("Ignoring URL", "url", file.URL()) - continue - } - - // Ignore not conforming filenames. - filename := filepath.Base(u.Path) - if !util.ConformingFileName(filename) { - stats.filenameFailed++ - slog.Warn("Ignoring none conforming filename", - "filename", filename) - continue - } - - resp, err := client.Get(file.URL()) - if err != nil { - stats.downloadFailed++ - slog.Warn("Cannot GET", - "url", file.URL(), - "error", err) - continue - } - - if resp.StatusCode != http.StatusOK { - stats.downloadFailed++ - slog.Warn("Cannot load", - "url", file.URL(), - "status", resp.Status, - "status_code", resp.StatusCode) - continue - } - - // Warn if we do not get JSON. - if ct := resp.Header.Get("Content-Type"); ct != "application/json" { - slog.Warn("Content type is not 'application/json'", - "url", file.URL(), - "content_type", ct) - } - - var ( - writers []io.Writer - s256, s512 hash.Hash - s256Data, s512Data []byte - remoteSHA256, remoteSHA512 []byte - signData []byte - ) - - // Only hash when we have a remote counter part we can compare it with. - if remoteSHA256, s256Data, err = loadHash(client, file.SHA256URL()); err != nil { - slog.Warn("Cannot fetch SHA256", - "url", file.SHA256URL(), - "error", err) - } else { - s256 = sha256.New() - writers = append(writers, s256) - } - - if remoteSHA512, s512Data, err = loadHash(client, file.SHA512URL()); err != nil { - slog.Warn("Cannot fetch SHA512", - "url", file.SHA512URL(), - "error", err) - } else { - s512 = sha512.New() - writers = append(writers, s512) - } - - // Remember the data as we need to store it to file later. - data.Reset() - writers = append(writers, &data) - - // Download the advisory and hash it. - hasher := io.MultiWriter(writers...) - - var doc any - - if err := func() error { - defer resp.Body.Close() - tee := io.TeeReader(resp.Body, hasher) - return json.NewDecoder(tee).Decode(&doc) - }(); err != nil { - stats.downloadFailed++ - slog.Warn("Downloading failed", - "url", file.URL(), - "error", err) - continue - } - - // Compare the checksums. - s256Check := func() error { - if s256 != nil && !bytes.Equal(s256.Sum(nil), remoteSHA256) { - stats.sha256Failed++ - return fmt.Errorf("SHA256 checksum of %s does not match", file.URL()) - } - return nil - } - - s512Check := func() error { - if s512 != nil && !bytes.Equal(s512.Sum(nil), remoteSHA512) { - stats.sha512Failed++ - return fmt.Errorf("SHA512 checksum of %s does not match", file.URL()) - } - return nil - } - - // Validate OpenPGP signature. - keysCheck := func() error { - // Only check signature if we have loaded keys. - if d.keys == nil { - return nil - } - var sign *crypto.PGPSignature - sign, signData, err = loadSignature(client, file.SignURL()) - if err != nil { - slog.Warn("Downloading signature failed", - "url", file.SignURL(), - "error", err) - } - if sign != nil { - if err := d.checkSignature(data.Bytes(), sign); err != nil { - if !d.cfg.IgnoreSignatureCheck { - stats.signatureFailed++ - return fmt.Errorf("cannot verify signature for %s: %v", file.URL(), err) - } - } - } - return nil - } - - // Validate against CSAF schema. - schemaCheck := func() error { - if errors, err := csaf.ValidateCSAF(doc); err != nil || len(errors) > 0 { - stats.schemaFailed++ - d.logValidationIssues(file.URL(), errors, err) - return fmt.Errorf("schema validation for %q failed", file.URL()) - } - return nil - } - - // Validate if filename is conforming. - filenameCheck := func() error { - if err := util.IDMatchesFilename(d.eval, doc, filename); err != nil { - stats.filenameFailed++ - return fmt.Errorf("filename not conforming %s: %s", file.URL(), err) - } - return nil - } - - // Validate against remote validator. - remoteValidatorCheck := func() error { - if d.validator == nil { - return nil - } - rvr, err := d.validator.Validate(doc) - if err != nil { - errorCh <- fmt.Errorf( - "calling remote validator on %q failed: %w", - file.URL(), err) - return nil - } - if !rvr.Valid { - stats.remoteFailed++ - return fmt.Errorf("remote validation of %q failed", file.URL()) - } - return nil - } - - // Run all the validations. - valStatus := notValidatedValidationStatus - for _, check := range []func() error{ - s256Check, - s512Check, - keysCheck, - schemaCheck, - filenameCheck, - remoteValidatorCheck, - } { - if err := check(); err != nil { - slog.Error("Validation check failed", "error", err) - valStatus.update(invalidValidationStatus) - if d.cfg.ValidationMode == validationStrict { - continue nextAdvisory - } - } - } - valStatus.update(validValidationStatus) - - // Send to forwarder - if d.forwarder != nil { - d.forwarder.forward( - filename, data.String(), - valStatus, - string(s256Data), - string(s512Data)) - } - - if d.cfg.NoStore { - // Do not write locally. - if valStatus == validValidationStatus { - stats.succeeded++ - } - continue - } - - if err := d.eval.Extract( - `$.document.tracking.initial_release_date`, dateExtract, false, doc, - ); err != nil { - slog.Warn("Cannot extract initial_release_date from advisory", - "url", file.URL()) - initialReleaseDate = time.Now() - } - initialReleaseDate = initialReleaseDate.UTC() - - // Advisories that failed validation are stored in a special folder. - var newDir string - if valStatus != validValidationStatus { - newDir = path.Join(d.cfg.Directory, failedValidationDir) - } else { - newDir = d.cfg.Directory - } - - // Do we have a configured destination folder? - if d.cfg.Folder != "" { - newDir = path.Join(newDir, d.cfg.Folder) - } else { - newDir = path.Join(newDir, lower, strconv.Itoa(initialReleaseDate.Year())) - } - - if newDir != lastDir { - if err := d.mkdirAll(newDir, 0755); err != nil { - errorCh <- err - continue - } - lastDir = newDir - } - - // Write advisory to file - path := filepath.Join(lastDir, filename) - - // Write data to disk. - for _, x := range []struct { - p string - d []byte - }{ - {path, data.Bytes()}, - {path + ".sha256", s256Data}, - {path + ".sha512", s512Data}, - {path + ".asc", signData}, - } { - if x.d != nil { - if err := os.WriteFile(x.p, x.d, 0644); err != nil { - errorCh <- err - continue nextAdvisory - } - } - } - - stats.succeeded++ - slog.Info("Written advisory", "path", path) } } @@ -697,11 +778,11 @@ func loadSignature(client util.Client, p string) (*crypto.PGPSignature, []byte, if err != nil { return nil, nil, err } + defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, nil, fmt.Errorf( "fetching signature from '%s' failed: %s (%d)", p, resp.Status, resp.StatusCode) } - defer resp.Body.Close() data, err := io.ReadAll(resp.Body) if err != nil { return nil, nil, err @@ -713,16 +794,60 @@ func loadSignature(client util.Client, p string) (*crypto.PGPSignature, []byte, return sign, data, nil } +func loadHashes(client util.Client, hashes []hashFetchInfo) ([]byte, []byte, []byte, []byte) { + var remoteSha256, remoteSha512, sha256Data, sha512Data []byte + + // Load preferred hashes first + slices.SortStableFunc(hashes, func(a, b hashFetchInfo) int { + if a.preferred == b.preferred { + return 0 + } + if a.preferred && !b.preferred { + return -1 + } + return 1 + }) + for _, h := range hashes { + if remote, data, err := loadHash(client, h.url); err != nil { + if h.warn { + slog.Warn("Cannot fetch hash", + "hash", h.hashType, + "url", h.url, + "error", err) + } else { + slog.Info("Hash not present", "hash", h.hashType, "file", h.url) + } + } else { + switch h.hashType { + case algSha512: + { + remoteSha512 = remote + sha512Data = data + } + case algSha256: + { + remoteSha256 = remote + sha256Data = data + } + } + if h.preferred { + break + } + } + } + return remoteSha256, sha256Data, remoteSha512, sha512Data +} + func loadHash(client util.Client, p string) ([]byte, []byte, error) { resp, err := client.Get(p) if err != nil { return nil, nil, err } + defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, nil, fmt.Errorf( "fetching hash from '%s' failed: %s (%d)", p, resp.Status, resp.StatusCode) } - defer resp.Body.Close() var data bytes.Buffer tee := io.TeeReader(resp.Body, &data) hash, err := util.HashFromReader(tee) @@ -742,3 +867,14 @@ func (d *downloader) run(ctx context.Context, domains []string) error { } return nil } + +// runEnumerate performs the enumeration of PMDs for all the given domains. +func (d *downloader) runEnumerate(domains []string) error { + defer d.stats.log() + for _, domain := range domains { + if err := d.enumerate(domain); err != nil { + return err + } + } + return nil +} diff --git a/cmd/csaf_downloader/downloader_test.go b/cmd/csaf_downloader/downloader_test.go new file mode 100644 index 0000000..1485ec9 --- /dev/null +++ b/cmd/csaf_downloader/downloader_test.go @@ -0,0 +1,160 @@ +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. +// +// SPDX-License-Identifier: Apache-2.0 +// +// SPDX-FileCopyrightText: 2023 German Federal Office for Information Security (BSI) +// Software-Engineering: 2023 Intevation GmbH + +package main + +import ( + "context" + "errors" + "log/slog" + "net/http/httptest" + "os" + "testing" + + "github.com/gocsaf/csaf/v3/internal/options" + "github.com/gocsaf/csaf/v3/internal/testutil" + "github.com/gocsaf/csaf/v3/util" +) + +func checkIfFileExists(path string, t *testing.T) bool { + if _, err := os.Stat(path); err == nil { + return true + } else if !errors.Is(err, os.ErrNotExist) { + t.Fatalf("Failed to check if file exists: %v", err) + } + return false +} + +func TestShaMarking(t *testing.T) { + tests := []struct { + name string + directoryProvider bool + wantSha256 bool + wantSha512 bool + enableSha256 bool + enableSha512 bool + preferredHash hashAlgorithm + }{ + { + name: "want sha256 and sha512", + directoryProvider: false, + wantSha256: true, + wantSha512: true, + enableSha256: true, + enableSha512: true, + }, + { + name: "only want sha256", + directoryProvider: false, + wantSha256: true, + wantSha512: false, + enableSha256: true, + enableSha512: true, + preferredHash: algSha256, + }, + { + name: "only want sha512", + directoryProvider: false, + wantSha256: false, + wantSha512: true, + enableSha256: true, + enableSha512: true, + preferredHash: algSha512, + }, + { + name: "only want sha512", + directoryProvider: false, + wantSha256: false, + wantSha512: true, + enableSha256: true, + enableSha512: true, + preferredHash: algSha512, + }, + + { + name: "only deliver sha256", + directoryProvider: false, + wantSha256: true, + wantSha512: false, + enableSha256: true, + enableSha512: false, + preferredHash: algSha512, + }, + { + name: "only want sha256, directory provider", + directoryProvider: true, + wantSha256: true, + wantSha512: false, + enableSha256: true, + enableSha512: true, + preferredHash: algSha256, + }, + { + name: "only want sha512, directory provider", + directoryProvider: true, + wantSha256: false, + wantSha512: true, + enableSha256: true, + enableSha512: true, + preferredHash: algSha512, + }, + } + + t.Parallel() + for _, testToRun := range tests { + test := testToRun + t.Run(test.name, func(tt *testing.T) { + tt.Parallel() + serverURL := "" + params := testutil.ProviderParams{ + URL: "", + EnableSha256: test.enableSha256, + EnableSha512: test.enableSha512, + } + server := httptest.NewTLSServer(testutil.ProviderHandler(¶ms, test.directoryProvider)) + defer server.Close() + + serverURL = server.URL + params.URL = server.URL + + hClient := server.Client() + client := util.Client(hClient) + + tempDir := t.TempDir() + cfg := config{LogLevel: &options.LogLevel{Level: slog.LevelDebug}, Directory: tempDir, PreferredHash: test.preferredHash} + err := cfg.prepare() + if err != nil { + t.Fatalf("SHA marking config failed: %v", err) + } + d, err := newDownloader(&cfg) + if err != nil { + t.Fatalf("could not init downloader: %v", err) + } + d.client = &client + + ctx := context.Background() + err = d.run(ctx, []string{serverURL + "/provider-metadata.json"}) + if err != nil { + t.Errorf("SHA marking %v: Expected no error, got: %v", test.name, err) + } + d.close() + + // Check for downloaded hashes + sha256Exists := checkIfFileExists(tempDir+"/white/2020/avendor-advisory-0004.json.sha256", t) + sha512Exists := checkIfFileExists(tempDir+"/white/2020/avendor-advisory-0004.json.sha512", t) + + if sha256Exists != test.wantSha256 { + t.Errorf("%v: expected sha256 hash present to be %v, got: %v", test.name, test.wantSha256, sha256Exists) + } + + if sha512Exists != test.wantSha512 { + t.Errorf("%v: expected sha512 hash present to be %v, got: %v", test.name, test.wantSha512, sha512Exists) + } + }) + } +} diff --git a/cmd/csaf_downloader/forwarder.go b/cmd/csaf_downloader/forwarder.go index eda6595..ac2c336 100644 --- a/cmd/csaf_downloader/forwarder.go +++ b/cmd/csaf_downloader/forwarder.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2023 German Federal Office for Information Security (BSI) // Software-Engineering: 2023 Intevation GmbH @@ -12,16 +12,15 @@ import ( "bytes" "crypto/tls" "io" + "log/slog" "mime/multipart" "net/http" "os" "path/filepath" "strings" - "golang.org/x/exp/slog" - - "github.com/csaf-poc/csaf_distribution/v3/internal/misc" - "github.com/csaf-poc/csaf_distribution/v3/util" + "github.com/gocsaf/csaf/v3/internal/misc" + "github.com/gocsaf/csaf/v3/util" ) // failedForwardDir is the name of the special sub folder @@ -107,16 +106,15 @@ func (f *forwarder) httpClient() util.Client { hClient.Transport = &http.Transport{ TLSClientConfig: &tlsConfig, + Proxy: http.ProxyFromEnvironment, } client := util.Client(&hClient) // Add extra headers. - if len(f.cfg.ForwardHeader) > 0 { - client = &util.HeaderClient{ - Client: client, - Header: f.cfg.ForwardHeader, - } + client = &util.HeaderClient{ + Client: client, + Header: f.cfg.ForwardHeader, } // Add optional URL logging. @@ -226,12 +224,12 @@ func (f *forwarder) storeFailed(filename, doc, sha256, sha512 string) { // limitedString reads max bytes from reader and returns it as a string. // Longer strings are indicated by "..." as a suffix. -func limitedString(r io.Reader, max int) (string, error) { +func limitedString(r io.Reader, maxLength int) (string, error) { var msg strings.Builder - if _, err := io.Copy(&msg, io.LimitReader(r, int64(max))); err != nil { + if _, err := io.Copy(&msg, io.LimitReader(r, int64(maxLength))); err != nil { return "", err } - if msg.Len() >= max { + if msg.Len() >= maxLength { msg.WriteString("...") } return msg.String(), nil diff --git a/cmd/csaf_downloader/forwarder_test.go b/cmd/csaf_downloader/forwarder_test.go index dc515ad..25f0f1f 100644 --- a/cmd/csaf_downloader/forwarder_test.go +++ b/cmd/csaf_downloader/forwarder_test.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2023 German Federal Office for Information Security (BSI) // Software-Engineering: 2023 Intevation GmbH @@ -14,6 +14,7 @@ import ( "encoding/json" "errors" "io" + "log/slog" "mime" "mime/multipart" "net/http" @@ -22,10 +23,8 @@ import ( "strings" "testing" - "golang.org/x/exp/slog" - - "github.com/csaf-poc/csaf_distribution/v3/internal/options" - "github.com/csaf-poc/csaf_distribution/v3/util" + "github.com/gocsaf/csaf/v3/internal/options" + "github.com/gocsaf/csaf/v3/util" ) func TestValidationStatusUpdate(t *testing.T) { diff --git a/cmd/csaf_downloader/main.go b/cmd/csaf_downloader/main.go index daff163..fe6efd1 100644 --- a/cmd/csaf_downloader/main.go +++ b/cmd/csaf_downloader/main.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) // Software-Engineering: 2022 Intevation GmbH @@ -11,12 +11,11 @@ package main import ( "context" + "log/slog" "os" "os/signal" - "golang.org/x/exp/slog" - - "github.com/csaf-poc/csaf_distribution/v3/internal/options" + "github.com/gocsaf/csaf/v3/internal/options" ) func run(cfg *config, domains []string) error { @@ -41,6 +40,11 @@ func run(cfg *config, domains []string) error { d.forwarder = f } + // If the enumerate-only flag is set, enumerate found PMDs, + // else use the normal load method + if cfg.EnumeratePMDOnly { + return d.runEnumerate(domains) + } return d.run(ctx, domains) } diff --git a/cmd/csaf_downloader/stats.go b/cmd/csaf_downloader/stats.go index 237420a..d85feb3 100644 --- a/cmd/csaf_downloader/stats.go +++ b/cmd/csaf_downloader/stats.go @@ -1,14 +1,14 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2023 German Federal Office for Information Security (BSI) // Software-Engineering: 2023 Intevation GmbH package main -import "golang.org/x/exp/slog" +import "log/slog" // stats contains counters of the downloads. type stats struct { diff --git a/cmd/csaf_downloader/stats_test.go b/cmd/csaf_downloader/stats_test.go index b3ab914..033fd7b 100644 --- a/cmd/csaf_downloader/stats_test.go +++ b/cmd/csaf_downloader/stats_test.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2023 German Federal Office for Information Security (BSI) // Software-Engineering: 2023 Intevation GmbH @@ -11,9 +11,8 @@ package main import ( "bytes" "encoding/json" + "log/slog" "testing" - - "golang.org/x/exp/slog" ) func TestStatsAdd(t *testing.T) { diff --git a/cmd/csaf_provider/actions.go b/cmd/csaf_provider/actions.go index 54d4e24..1862983 100644 --- a/cmd/csaf_provider/actions.go +++ b/cmd/csaf_provider/actions.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) // Software-Engineering: 2022 Intevation GmbH @@ -26,8 +26,8 @@ import ( "github.com/ProtonMail/gopenpgp/v2/constants" "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/csaf-poc/csaf_distribution/v3/csaf" - "github.com/csaf-poc/csaf_distribution/v3/util" + "github.com/gocsaf/csaf/v3/csaf" + "github.com/gocsaf/csaf/v3/util" ) const dateFormat = time.RFC3339 diff --git a/cmd/csaf_provider/config.go b/cmd/csaf_provider/config.go index af99cc1..5d29b61 100644 --- a/cmd/csaf_provider/config.go +++ b/cmd/csaf_provider/config.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) // Software-Engineering: 2021 Intevation GmbH @@ -11,6 +11,7 @@ package main import ( "fmt" "io" + "net/url" "os" "strings" @@ -18,7 +19,7 @@ import ( "github.com/ProtonMail/gopenpgp/v2/crypto" "golang.org/x/crypto/bcrypt" - "github.com/csaf-poc/csaf_distribution/v3/csaf" + "github.com/gocsaf/csaf/v3/csaf" ) const ( @@ -262,6 +263,14 @@ func loadConfig() (*config, error) { if cfg.CanonicalURLPrefix == "" { cfg.CanonicalURLPrefix = "https://" + os.Getenv("SERVER_NAME") } + // Check if canonical url prefix is invalid + parsedURL, err := url.ParseRequestURI(cfg.CanonicalURLPrefix) + if err != nil { + return nil, err + } + if parsedURL.Scheme != "https" && parsedURL.Scheme != "http" { + return nil, fmt.Errorf("invalid canonical URL: %q", cfg.CanonicalURLPrefix) + } if cfg.TLPs == nil { cfg.TLPs = []tlp{tlpCSAF, tlpWhite, tlpGreen, tlpAmber, tlpRed} diff --git a/cmd/csaf_provider/controller.go b/cmd/csaf_provider/controller.go index c8680ff..f04b7bd 100644 --- a/cmd/csaf_provider/controller.go +++ b/cmd/csaf_provider/controller.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) // Software-Engineering: 2021 Intevation GmbH @@ -174,7 +174,7 @@ func (c *controller) web( // writeJSON sets the header for the response and writes the JSON encoding of the given "content". // It logs out an error message in case of an error. func writeJSON(rw http.ResponseWriter, content any, code int) { - rw.Header().Set("Content-type", "application/json; charset=utf-8") + rw.Header().Set("Content-type", "application/json") rw.Header().Set("X-Content-Type-Options", "nosniff") rw.WriteHeader(code) if err := json.NewEncoder(rw).Encode(content); err != nil { diff --git a/cmd/csaf_provider/create.go b/cmd/csaf_provider/create.go index 8e882a5..11e0b7c 100644 --- a/cmd/csaf_provider/create.go +++ b/cmd/csaf_provider/create.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) // Software-Engineering: 2021 Intevation GmbH @@ -22,8 +22,8 @@ import ( "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/csaf-poc/csaf_distribution/v3/csaf" - "github.com/csaf-poc/csaf_distribution/v3/util" + "github.com/gocsaf/csaf/v3/csaf" + "github.com/gocsaf/csaf/v3/util" ) // ensureFolders initializes the paths and call functions to create diff --git a/cmd/csaf_provider/files.go b/cmd/csaf_provider/files.go index 0b3c5ed..3b99ff5 100644 --- a/cmd/csaf_provider/files.go +++ b/cmd/csaf_provider/files.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) // Software-Engineering: 2021 Intevation GmbH @@ -13,7 +13,7 @@ import ( "crypto/sha512" "os" - "github.com/csaf-poc/csaf_distribution/v3/util" + "github.com/gocsaf/csaf/v3/util" ) func writeHashedFile(fname, name string, data []byte, armored string) error { diff --git a/cmd/csaf_provider/indices.go b/cmd/csaf_provider/indices.go index a7ecd3b..a4eb97a 100644 --- a/cmd/csaf_provider/indices.go +++ b/cmd/csaf_provider/indices.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) // Software-Engineering: 2021 Intevation GmbH @@ -18,7 +18,7 @@ import ( "sort" "time" - "github.com/csaf-poc/csaf_distribution/v3/util" + "github.com/gocsaf/csaf/v3/util" ) func updateIndex(dir, fname string) error { diff --git a/cmd/csaf_provider/main.go b/cmd/csaf_provider/main.go index 2264676..3faebfe 100644 --- a/cmd/csaf_provider/main.go +++ b/cmd/csaf_provider/main.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) // Software-Engineering: 2021 Intevation GmbH @@ -18,7 +18,7 @@ import ( "github.com/jessevdk/go-flags" - "github.com/csaf-poc/csaf_distribution/v3/util" + "github.com/gocsaf/csaf/v3/util" ) type options struct { @@ -48,7 +48,7 @@ func main() { cfg, err := loadConfig() if err != nil { - cgi.Serve(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + cgi.Serve(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { http.Error(rw, "Something went wrong. Check server logs for more details", http.StatusInternalServerError) })) diff --git a/cmd/csaf_provider/mux.go b/cmd/csaf_provider/mux.go index 34b7e2e..021c074 100644 --- a/cmd/csaf_provider/mux.go +++ b/cmd/csaf_provider/mux.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) // Software-Engineering: 2021 Intevation GmbH diff --git a/cmd/csaf_provider/rolie.go b/cmd/csaf_provider/rolie.go index ea48480..d9717b1 100644 --- a/cmd/csaf_provider/rolie.go +++ b/cmd/csaf_provider/rolie.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) // Software-Engineering: 2021 Intevation GmbH @@ -15,8 +15,8 @@ import ( "strings" "time" - "github.com/csaf-poc/csaf_distribution/v3/csaf" - "github.com/csaf-poc/csaf_distribution/v3/util" + "github.com/gocsaf/csaf/v3/csaf" + "github.com/gocsaf/csaf/v3/util" ) // mergeCategories merges the given categories into the old ones. diff --git a/cmd/csaf_provider/tmpl/create.html b/cmd/csaf_provider/tmpl/create.html index 74fef6d..0b06f6f 100644 --- a/cmd/csaf_provider/tmpl/create.html +++ b/cmd/csaf_provider/tmpl/create.html @@ -1,8 +1,8 @@ + ```toml workers = 2 folder = "/var/csaf_aggregator" -lock_file = "/var/csaf_aggregator/run.lock" +lock_file = "/var/lock/csaf_aggregator/lock" web = "/var/csaf_aggregator/html" domain = "https://localhost:9443" rate = 10.0 @@ -187,6 +193,7 @@ insecure = true #interim_years = #passphrase = #write_indices = false +#time_range = # specification requires at least two providers (default), # to override for testing, enable: @@ -208,6 +215,7 @@ insecure = true create_service_document = true # rate = 1.5 # insecure = true +# time_range = [[providers]] name = "local-dev-provider2" @@ -217,8 +225,8 @@ insecure = true write_indices = true client_cert = "./../devca1/testclient1.crt" client_key = "./../devca1/testclient1-key.pem" -# client_passphrase = -# header = +# client_passphrase = # Limited and experimental, see downloader doc. +# header = [[providers]] name = "local-dev-provider3" @@ -226,16 +234,22 @@ insecure = true # rate = 1.8 # insecure = true write_indices = true - # If aggregator.category == "aggregator", set for an entry that should + # If aggregator.category == "aggreator", set for an entry that should # be listed in addition: category = "lister" -# ignore_pattern = [".*white.*", ".*red.*"] +# ignore_pattern = [".*white.*", ".*red.*"] ``` - + #### Publish others' advisories In case you want to provide CSAF advisories from others that only qualify as CSAF publishers, see [how to use the `csaf_aggregator` as "CSAF proxy provider"](proxy-provider-for-aggregator.md). + +Some providers may limit the rate of requests that may be sent to retrieve advisories. +This may cause issues with the aggregator. +In this case, the --rate option can be used to adjust the requests per second +sent by each worker of the aggregator to an acceptable rate. +(The rate that is considered acceptable depends on the provider.) diff --git a/docs/csaf_checker.md b/docs/csaf_checker.md index 58f77ca..5c812bd 100644 --- a/docs/csaf_checker.md +++ b/docs/csaf_checker.md @@ -30,9 +30,12 @@ Help Options: Will check all given _domains_, by trying each as a CSAF provider. +If no user agent is specified with `--header=user-agent:custom-agent/1.0` then the default agent in the form of `csaf_distribution/VERSION` is sent. + If a _domain_ starts with `https://` it is instead considered a direct URL to the `provider-metadata.json` and checking proceeds from there. If no config file is explictly given the follwing places are searched for a config file: + ``` ~/.config/csaf/checker.toml ~/.csaf_checker.toml @@ -41,6 +44,7 @@ csaf_checker.toml with `~` expanding to `$HOME` on unixoid systems and `%HOMEPATH` on Windows systems. Supported options in config files: + ``` output = "" format = "json" @@ -58,9 +62,10 @@ validator_preset = ["mandatory"] ``` Usage example: -` ./csaf_checker example.com -f html --rate=5.3 -H apikey:SECRET -o check-results.html` +`./csaf_checker example.com -f html --rate=5.3 -H apikey:SECRET -o check-results.html` Each performed check has a return type of either 0,1 or 2: + ``` type 0: success type 1: warning @@ -70,9 +75,15 @@ type 2: error The checker result is a success if no checks resulted in type 2, and a failure otherwise. The option `timerange` allows to only check advisories from a given time -interval. It can only be given once. See the +interval. It can only be given once. See the [downloader documentation](csaf_downloader.md#timerange-option) for details. +Some providers may limit the rate of requests that may be sent to retrieve advisories. +This may cause the checker to be unable to retrieve all advisories. In this case, +the --rate option can be used to adjust the requests per second +sent by the checker to an acceptable rate. +(The rate that is considered acceptable depends on the provider.) + You can ignore certain advisories while checking by specifying a list of regular expressions[^1] to match their URLs by using the `ignorepattern` @@ -80,6 +91,7 @@ option. E.g. `-i='.*white.*' -i='*.red.*'` will ignore files which URLs contain the sub strings **white** or **red**. In the config file this has to be noted as: + ``` ignorepattern = [".*white.*", ".*red.*"] ``` @@ -88,7 +100,7 @@ ignorepattern = [".*white.*", ".*red.*"] The `role` given in the `provider-metadata.json` is not yet considered to change the overall result, -see https://github.com/csaf-poc/csaf_distribution/issues/221 . +see . If a provider hosts one or more advisories with a TLP level of AMBER or RED, then these advisories must be access protected. To check these advisories, authorization can be given via custom headers or certificates. diff --git a/docs/csaf_downloader.md b/docs/csaf_downloader.md index fcf6634..123694e 100644 --- a/docs/csaf_downloader.md +++ b/docs/csaf_downloader.md @@ -1,4 +1,5 @@ ## csaf_downloader + A tool to download CSAF documents from CSAF providers. ### Usage @@ -21,6 +22,7 @@ Application Options: -f, --folder=FOLDER Download into a given subFOLDER -i, --ignore_pattern=PATTERN Do not download files if their URLs match any of the given PATTERNs -H, --header= One or more extra HTTP header fields + --enumerate_pmd_only If this flag is set to true, the downloader will only enumerate valid provider metadata files, but not download documents --validator=URL URL to validate documents remotely --validator_cache=FILE FILE to cache remote validations --validator_preset=PRESETS One or more PRESETS to validate remotely (default: [mandatory]) @@ -29,9 +31,10 @@ Application Options: --forward_header= One or more extra HTTP header fields used by forwarding --forward_queue=LENGTH Maximal queue LENGTH before forwarder (default: 5) --forward_insecure Do not check TLS certificates from forward endpoint - --logfile=FILE FILE to log downloading to (default: downloader.log) - --loglevel=LEVEL[debug|info|warn|error] LEVEL of logging details (default: info) + --log_file=FILE FILE to log downloading to (default: downloader.log) + --log_level=LEVEL[debug|info|warn|error] LEVEL of logging details (default: info) -c, --config=TOML-FILE Path to config TOML file + --preferred_hash=HASH[sha256|sha512] HASH to prefer Help Options: -h, --help Show this help message @@ -39,6 +42,8 @@ Help Options: Will download all CSAF documents for the given _domains_, by trying each as a CSAF provider. +If no user agent is specified with `--header=user-agent:custom-agent/1.0` then the default agent in the form of `csaf_distribution/VERSION` is sent. + If a _domain_ starts with `https://` it is instead considered a direct URL to the `provider-metadata.json` and downloading procedes from there. Increasing the number of workers opens more connections to the web servers @@ -46,7 +51,14 @@ to download more advisories at once. This may improve the overall speed of the d However, since this also increases the load on the servers, their administrators could have taken countermeasures to limit this. +For example, some providers may limit the rate of requests that may be sent to retrieve advisories. +This may cause the downloader to be unable to retrieve all advisories. +In this case, the --rate option can be used to adjust the requests per second +sent by the downloader to an acceptable rate. +(The rate that is considered acceptable depends on the provider.) + If no config file is explictly given the follwing places are searched for a config file: + ``` ~/.config/csaf/downloader.toml ~/.csaf_downloader.toml @@ -56,13 +68,14 @@ csaf_downloader.toml with `~` expanding to `$HOME` on unixoid systems and `%HOMEPATH` on Windows systems. Supported options in config files: + ``` # directory # not set by default insecure = false # client_cert # not set by default # client_key # not set by default # client_passphrase # not set by default -ignoresigcheck = false +ignore_sigcheck = false # rate # set to unlimited worker = 2 # time_range # not set by default @@ -90,14 +103,16 @@ option. E.g. `-i='.*white.*' -i='*.red.*'` will ignore files which URLs contain the sub strings **white** or **red**. In the config file this has to be noted as: + ``` ignorepattern = [".*white.*", ".*red.*"] ``` #### Timerange option -The `timerange` parameter enables downloading advisories which last changes falls -into a given intervall. There are three possible notations: +The `time_range` parameter enables downloading advisories +which last changes falls into a given intervall. +There are three possible notations: 1. Relative. If the given string follows the rules of a [Go duration](https://pkg.go.dev/time@go1.20.6#ParseDuration), @@ -106,16 +121,18 @@ into a given intervall. There are three possible notations: and 'y' for years are recognized. In these cases only integer values are accepted without any fractions. Some examples: + - `"3h"` means downloading the advisories that have changed in the last three hours. - - `"30m"` .. changed within the last thirty minutes. + - `"30m"` .. changed within the last thirty minutes. - `"3M2m"` .. changed within the last three months and two minutes. - - `"2y"` .. changed within the last two years. + - `"2y"` .. changed within the last two years. 2. Absolute. If the given string is an RFC 3339 date timestamp the time interval between this date and now is used. E.g. `"2006-01-02"` means that all files between 2006 January 2nd and now going to being downloaded. Accepted patterns are: + - `"2006-01-02T15:04:05Z"` - `"2006-01-02T15:04:05+07:00"` - `"2006-01-02T15:04:05-07:00"` @@ -134,6 +151,7 @@ into a given intervall. There are three possible notations: All interval boundaries are inclusive. #### Forwarding + The downloader is able to forward downloaded advisories and their checksums, OpenPGP signatures and validation results to an HTTP endpoint. The details of the implemented API are described [here](https://github.com/mfd2007/csaf_upload_interface). @@ -149,7 +167,7 @@ key protection mechanism based on RFC 1423, see Thus it considered experimental and most likely to be removed in a future release. Please only use this option, if you fully understand the security implications! -Note that for fully automated processes, it usually not make sense +Note that for fully automated processes, it usually does not make sense to protect the client certificate's private key with a passphrase. Because the passphrase has to be accessible to the process anyway to run unattented. In this situation the processing environment should be secured diff --git a/docs/csaf_provider.md b/docs/csaf_provider.md index b02165b..2fc5354 100644 --- a/docs/csaf_provider.md +++ b/docs/csaf_provider.md @@ -4,7 +4,7 @@ The [setup docs](../README.md#setup-trusted-provider) explain how to wire this up with nginx and where the config file lives. When installed, two endpoints are offered, -and you should use the [csaf_uploader](../docs/csaf_uploader) +and you should use the [csaf_uploader](../docs/csaf_uploader.md) to access them: ### /api/create @@ -58,7 +58,8 @@ The following example file documents all available configuration options: # The following shows an example of a manually set prefix: #canonical_url_prefix = "https://localhost" -# Require users to use a password and a valid Client Certificate for write access. +# Require users to use both +# (1) a password and (2) a valid Client Certificate for write access. #certificate_and_password = false # Allow the user to send the request without having to send a passphrase @@ -100,22 +101,12 @@ The following example file documents all available configuration options: #tlps = ["csaf", "white", "amber", "green", "red"] # Make the provider create a ROLIE service document. -#create_service_document = true +#create_service_document = false # Make the provider create a ROLIE category document from a list of strings. # If a list item starts with `expr:` # the rest of the string is used as a JsonPath expression # to extract a string from the incoming advisories. -# If the result of the expression is a string this string -# is used. If the result is an array each element of -# this array is tested if it is a string or an array. -# If this test fails the expression fails. If the -# test succeeds the rules are applied recursively to -# collect all strings in the result. -# Suggested expressions are: -# - vendor, product family and product names: "expr:$.product_tree..branches[?(@.category==\"vendor\" || @.category==\"product_family\" || @.category==\"product_name\")].name" -# - CVEs: "expr:$.vulnerabilities[*].cve" -# - CWEs: "expr:$.vulnerabilities[*].cwe.id" # Strings not starting with `expr:` are taken verbatim. # By default no category documents are created. # This example provides an overview over the syntax, @@ -151,5 +142,5 @@ contact_details = "Example Company can be reached at contact_us@example.com, or There is an experimental upload interface which works with a web browser. It is disabled by default, as there are known issues, notably: - * https://github.com/csaf-poc/csaf_distribution/issues/43 - * https://github.com/csaf-poc/csaf_distribution/issues/256 + * https://github.com/gocsaf/csaf/issues/43 + * https://github.com/gocsaf/csaf/issues/256 diff --git a/docs/csaf_uploader.md b/docs/csaf_uploader.md index 0e68aa9..76af99f 100644 --- a/docs/csaf_uploader.md +++ b/docs/csaf_uploader.md @@ -43,6 +43,12 @@ E.g. uploading a csaf-document which asks to enter a password interactively. +To upload an already signed document, use the `-x` option +```bash +# Note: The file CSAF-document-1.json.asc must exist +./csaf_uploader -x -a upload -I -t white -u https://localhost/cgi-bin/csaf_provider.go CSAF-document-1.json +``` + By default csaf_uploader will try to load a config file from the following places: diff --git a/docs/csaf_validator.md b/docs/csaf_validator.md index dfa0c9a..87ec831 100644 --- a/docs/csaf_validator.md +++ b/docs/csaf_validator.md @@ -2,6 +2,16 @@ is a tool to validate local advisories files against the JSON Schema and an optional remote validator. +### Exit codes + +If no fatal error occurs the program will exit with an exit code `n` with the following conditions: + +- `n == 0`: all valid +- `(n & 1) > 0`: a general error occurred, all other flags are unset (see logs for more information) +- `(n & 2) > 0`: schema validation failed +- `(n & 4) > 0`: no remote validator configured +- `(n & 8) > 0`: failure in remote validation + ### Usage ``` diff --git a/docs/development-ca.md b/docs/development-ca.md index 483732c..21f4ef4 100644 --- a/docs/development-ca.md +++ b/docs/development-ca.md @@ -55,7 +55,7 @@ signing_key encryption_key non_repudiation -dns_name = "*.local" +dns_name = "*.test" dns_name = "localhost" serial = 010 diff --git a/docs/examples/aggregator.toml b/docs/examples/aggregator.toml index ae1723d..8d4ee80 100644 --- a/docs/examples/aggregator.toml +++ b/docs/examples/aggregator.toml @@ -5,6 +5,7 @@ web = "/var/csaf_aggregator/html" domain = "https://localhost:9443" rate = 10.0 insecure = true +#verbose = false #openpgp_private_key = #openpgp_public_key = #interim_years = @@ -51,7 +52,7 @@ insecure = true # rate = 1.8 # insecure = true write_indices = true - # If aggregator.category == "aggreator", set for an entry that should + # If aggregator.category == "aggregator", set for an entry that should # be listed in addition: category = "lister" # ignore_pattern = [".*white.*", ".*red.*"] diff --git a/docs/provider-setup.md b/docs/provider-setup.md index 3f07fd0..d54268f 100644 --- a/docs/provider-setup.md +++ b/docs/provider-setup.md @@ -78,6 +78,9 @@ server { # directory listings autoindex on; + + # allow others web applications to get the static information + add_header Access-Control-Allow-Origin "*"; } # enable CGI @@ -115,7 +118,7 @@ sudo chmod g+r,o-rwx /etc/csaf/config.toml Here is a minimal example configuration, which you need to customize for a production setup, -see the [options of `csaf_provider`](https://github.com/csaf-poc/csaf_distribution/blob/main/docs/csaf_provider.md). +see the [options of `csaf_provider`](https://github.com/gocsaf/csaf/blob/main/docs/csaf_provider.md). @@ -141,7 +144,7 @@ on a GNU/Linux operating system. Create the folders: ```(shell) -curl https://192.168.56.102/cgi-bin/csaf_provider.go/create --cert-type p12 --cert {clientCertificat.p12} +curl https://192.168.56.102/cgi-bin/csaf_provider.go/api/create --cert-type p12 --cert {clientCertificat.p12} ``` Replace {clientCertificate.p12} with the client certificate file in pkcs12 format which includes the corresponding key as well. @@ -155,7 +158,7 @@ Again replacing `{clientCert.crt}` and `{clientKey.pem}` accordingly. To let nginx resolves the DNS record `csaf.data.security.domain.tld` to fulfill the [Requirement 10](https://docs.oasis-open.org/csaf/csaf/v2.0/cs01/csaf-v2.0-cs01.html#7110-requirement-10-dns-path) configure a new server block (virtual host) in a separated file under `/etc/nginx/available-sites/{DNSNAME}` like following: - + ```sh server { diff --git a/docs/proxy-provider-for-aggregator.md b/docs/proxy-provider-for-aggregator.md index f34d714..4148f52 100644 --- a/docs/proxy-provider-for-aggregator.md +++ b/docs/proxy-provider-for-aggregator.md @@ -5,7 +5,9 @@ calls it a *CSAF publisher*. After manually downloading the advisories from such a publisher, the tools here can be used to offer the CSAF files for automated downloading -as *CSAF aggregator*. (The construct is called *CSAF proxy provider*. See [Section 7.2.5](https://docs.oasis-open.org/csaf/csaf/v2.0/csaf-v2.0.html#725-role-csaf-aggregator) for more details.) +as *CSAF aggregator*. (The construct is called *CSAF proxy provider*. +See [Section 7.2.5](https://docs.oasis-open.org/csaf/csaf/v2.0/os/csaf-v2.0-os.html#725-role-csaf-aggregator) +for more details.) There are three necessary steps, easiest is to use one single virtual maschine (or container) per internal provider. diff --git a/docs/scripts/DNSConfigForItest.sh b/docs/scripts/DNSConfigForItest.sh index aa9a8ee..9196af3 100755 --- a/docs/scripts/DNSConfigForItest.sh +++ b/docs/scripts/DNSConfigForItest.sh @@ -1,9 +1,9 @@ #!/usr/bin/env bash # -# This file is Free Software under the MIT License -# without warranty, see README.md and LICENSES/MIT.txt for details. +# This file is Free Software under the Apache-2.0 License +# without warranty, see README.md and LICENSES/Apache-2.0.txt for details. # -# SPDX-License-Identifier: MIT +# SPDX-License-Identifier: Apache-2.0 # # SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) # Software-Engineering: 2022 Intevation GmbH @@ -28,6 +28,8 @@ echo " location = / { try_files /.well-known/csaf/provider-metadata.json =404; + # allow others web applications to get the static information + add_header Access-Control-Allow-Origin "*"; } access_log /var/log/nginx/dns-domain_access.log; diff --git a/docs/scripts/Readme.md b/docs/scripts/Readme.md index 65c5260..e0bc7c9 100644 --- a/docs/scripts/Readme.md +++ b/docs/scripts/Readme.md @@ -1,23 +1,24 @@ -Scripts for assisting the Integration tests. They are written on Ubuntu 20.04 TLS amd64. +Scripts for assisting the Integration tests. +They were written on Ubuntu 20.04 LTS amd64 and also tested with 24.04 LTS. -- `prepareUbunutForITest.sh` installs the required packages for the csaf_distribution integration tests on a naked ubuntu 20.04 LTS amd64. +- `prepareUbuntuInstanceForITests.sh` installs the required packages for the csaf integration tests on a naked Ubuntu LTS amd64. - `TLSConfigsForITest.sh` generates a root CA and webserver cert by running `createRootCAForITest.sh` and `createWebserverCertForITest.sh` and configures nginx for serving TLS connections. - `TLSClientConfigsForITest.sh` generates client certificates by calling `createCCForITest.sh` which uses the root certificate initialized before with `createRootCAForITest.sh`. It configures nginx to enable the authentication with client certificate. (This assumes that the same folder name is used to create the root certificate) -- `setupProviderForITest.sh` builds the csaf_provider, writes the required nginx configurations and create the initial folders. IT calls `uploadToProvider.sh` to upload some csaf example files to the provider. +- `setupProviderForITest.sh` builds the `csaf_provider`, writes the required nginx configurations and create the initial folders. IT calls `uploadToProvider.sh` to upload some csaf example files to the provider. -As creating the folders needs to authenticate with the csaf_provider, the configurations of TLS server and Client certificate authentication should be set. So it is recommended to call the scripts in this order: `TLSConfigsForITest.sh`, `TLSClientConfigsForITest.sh`, `setupProviderForITest.sh` +As creating the folders needs to authenticate with the `csaf_provider`, the configurations of TLS server and Client certificate authentication should be set. So it is recommended to call the scripts in this order: `TLSConfigsForITest.sh`, `TLSClientConfigsForITest.sh`, `setupProviderForITest.sh` -Calling example (as root): +Calling example (as user with sudo privileges): ``` bash - curl --fail -O https://raw.githubusercontent.com/csaf-poc/csaf_distribution/main/docs/scripts/prepareUbuntuInstanceForITests.sh - bash prepareUbuntuInstanceForITests.sh + curl --fail -O https://raw.githubusercontent.com/gocsaf/csaf/main/docs/scripts/prepareUbuntuInstanceForITests.sh + sudo bash prepareUbuntuInstanceForITests.sh - git clone https://github.com/csaf-poc/csaf_distribution.git # --branch - pushd csaf_distribution/docs/scripts/ + git clone https://github.com/gocsaf/csaf.git # --branch + pushd csaf/docs/scripts/ export FOLDERNAME=devca1 ORGANAME="CSAF Tools Development (internal)" source ./TLSConfigsForITest.sh diff --git a/docs/scripts/TLSClientConfigsForITest.sh b/docs/scripts/TLSClientConfigsForITest.sh index c9c64a9..830666f 100755 --- a/docs/scripts/TLSClientConfigsForITest.sh +++ b/docs/scripts/TLSClientConfigsForITest.sh @@ -1,9 +1,9 @@ #!/usr/bin/env bash -# This file is Free Software under the MIT License -# without warranty, see README.md and LICENSES/MIT.txt for details. +# This file is Free Software under the Apache-2.0 License +# without warranty, see README.md and LICENSES/Apache-2.0.txt for details. # -# SPDX-License-Identifier: MIT +# SPDX-License-Identifier: Apache-2.0 # # SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) # Software-Engineering: 2022 Intevation GmbH @@ -18,7 +18,7 @@ set -e NGINX_CONFIG_PATH=/etc/nginx/sites-available/default -cd ~/csaf_distribution/docs/scripts/ +cd ~/csaf/docs/scripts/ source ./createCCForITest.sh echo ' diff --git a/docs/scripts/TLSConfigsForITest.sh b/docs/scripts/TLSConfigsForITest.sh index 9b1f183..d7c06f9 100644 --- a/docs/scripts/TLSConfigsForITest.sh +++ b/docs/scripts/TLSConfigsForITest.sh @@ -1,7 +1,7 @@ -# This file is Free Software under the MIT License -# without warranty, see README.md and LICENSES/MIT.txt for details. +# This file is Free Software under the Apache-2.0 License +# without warranty, see README.md and LICENSES/Apache-2.0.txt for details. # -# SPDX-License-Identifier: MIT +# SPDX-License-Identifier: Apache-2.0 # # SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) # Software-Engineering: 2022 Intevation GmbH @@ -17,7 +17,7 @@ set -e NGINX_CONFIG_PATH=/etc/nginx/sites-available/default -cd ~/csaf_distribution/docs/scripts/ +cd ~/csaf/docs/scripts/ ## Create Root CA ./createRootCAForITest.sh diff --git a/docs/scripts/createCCForITest.sh b/docs/scripts/createCCForITest.sh index 091dad7..eaeb247 100644 --- a/docs/scripts/createCCForITest.sh +++ b/docs/scripts/createCCForITest.sh @@ -1,7 +1,7 @@ -# This file is Free Software under the MIT License -# without warranty, see README.md and LICENSES/MIT.txt for details. +# This file is Free Software under the Apache-2.0 License +# without warranty, see README.md and LICENSES/Apache-2.0.txt for details. # -# SPDX-License-Identifier: MIT +# SPDX-License-Identifier: Apache-2.0 # # SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) # Software-Engineering: 2022 Intevation GmbH diff --git a/docs/scripts/createRootCAForITest.sh b/docs/scripts/createRootCAForITest.sh index cf7cd15..47f3af7 100755 --- a/docs/scripts/createRootCAForITest.sh +++ b/docs/scripts/createRootCAForITest.sh @@ -1,9 +1,9 @@ #!/usr/bin/env bash -# This file is Free Software under the MIT License -# without warranty, see README.md and LICENSES/MIT.txt for details. +# This file is Free Software under the Apache-2.0 License +# without warranty, see README.md and LICENSES/Apache-2.0.txt for details. # -# SPDX-License-Identifier: MIT +# SPDX-License-Identifier: Apache-2.0 # # SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) # Software-Engineering: 2022 Intevation GmbH diff --git a/docs/scripts/createWebserverCertForITest.sh b/docs/scripts/createWebserverCertForITest.sh index 2cfabd4..781b68d 100644 --- a/docs/scripts/createWebserverCertForITest.sh +++ b/docs/scripts/createWebserverCertForITest.sh @@ -1,7 +1,7 @@ -# This file is Free Software under the MIT License -# without warranty, see README.md and LICENSES/MIT.txt for details. +# This file is Free Software under the Apache-2.0 License +# without warranty, see README.md and LICENSES/Apache-2.0.txt for details. # -# SPDX-License-Identifier: MIT +# SPDX-License-Identifier: Apache-2.0 # # SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) # Software-Engineering: 2022 Intevation GmbH diff --git a/docs/scripts/downloadExamples.sh b/docs/scripts/downloadExamples.sh index e8b3ac0..9d93959 100755 --- a/docs/scripts/downloadExamples.sh +++ b/docs/scripts/downloadExamples.sh @@ -2,10 +2,10 @@ # # Desc: Tries getting csaf 2.0 examples from api.github. Do not run too often! # -# This file is Free Software under the MIT License -# without warranty, see README.md and LICENSES/MIT.txt for details. +# This file is Free Software under the Apache-2.0 License +# without warranty, see README.md and LICENSES/Apache-2.0.txt for details. # -# SPDX-License-Identifier: MIT +# SPDX-License-Identifier: Apache-2.0 # # SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) # Software-Engineering: 2022 Intevation GmbH diff --git a/docs/scripts/prepareUbuntuInstanceForITests.sh b/docs/scripts/prepareUbuntuInstanceForITests.sh index f99bc26..fa2268e 100755 --- a/docs/scripts/prepareUbuntuInstanceForITests.sh +++ b/docs/scripts/prepareUbuntuInstanceForITests.sh @@ -1,12 +1,12 @@ #!/usr/bin/env bash set -e -# This script prepares a naked Ubuntu 20.04 LTS amd64 -# for the csaf_distribution integration tests +# This script prepares a naked Ubuntu LTS amd64 +# for the csaf integration tests # by installing the required packages. apt update -apt install -y make bash curl gnupg sed tar git nginx fcgiwrap gnutls-bin +apt install -y make bash curl gnupg sed tar git nginx fcgiwrap gnutls-bin zip # Install Go from binary distribution latest_go="$(curl https://go.dev/VERSION\?m=text| head -1).linux-amd64.tar.gz" @@ -14,19 +14,17 @@ curl -O https://dl.google.com/go/$latest_go rm -rf /usr/local/go # be sure that we do not have an old installation tar -C /usr/local -xzf $latest_go -# Install newer Node.js version from nodesource +# Install a current Node.js version from nodesource # as needed for https://github.com/secvisogram/csaf-validator-service # Instructions from # https://github.com/nodesource/distributions/blob/master/README.md#debmanual KEYRING=/usr/share/keyrings/nodesource.gpg -curl -fsSL https://deb.nodesource.com/gpgkey/nodesource.gpg.key | gpg --dearmor > "$KEYRING" +curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor > "$KEYRING" gpg --no-default-keyring --keyring "$KEYRING" --list-keys chmod a+r /usr/share/keyrings/nodesource.gpg -VERSION=node_16.x -DISTRO="$(lsb_release -s -c)" -echo "deb [signed-by=$KEYRING] https://deb.nodesource.com/$VERSION $DISTRO main" | tee /etc/apt/sources.list.d/nodesource.list -echo "deb-src [signed-by=$KEYRING] https://deb.nodesource.com/$VERSION $DISTRO main" | tee -a /etc/apt/sources.list.d/nodesource.list +NODE_MAJOR=20 +echo "deb [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list apt-get update apt-get install -y nodejs diff --git a/docs/scripts/setupProviderForITest.sh b/docs/scripts/setupProviderForITest.sh index 86cbe23..ae6c6fc 100755 --- a/docs/scripts/setupProviderForITest.sh +++ b/docs/scripts/setupProviderForITest.sh @@ -1,9 +1,9 @@ #!/usr/bin/env bash # -# This file is Free Software under the MIT License -# without warranty, see README.md and LICENSES/MIT.txt for details. +# This file is Free Software under the Apache-2.0 License +# without warranty, see README.md and LICENSES/Apache-2.0.txt for details. # -# SPDX-License-Identifier: MIT +# SPDX-License-Identifier: Apache-2.0 # # SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) # Software-Engineering: 2022 Intevation GmbH @@ -17,7 +17,7 @@ sudo chgrp -R www-data /var/www sudo chmod -R g+ws /var/www export NGINX_CONFIG_PATH=/etc/nginx/sites-available/default -export DNS_NAME=csaf.data.security.localhost +export DNS_NAME=csaf.data.security.test sudo cp /usr/share/doc/fcgiwrap/examples/nginx.conf /etc/nginx/fcgiwrap.conf @@ -61,6 +61,9 @@ echo " # directory listings autoindex on; + + # allow others web applications to get the static information + add_header Access-Control-Allow-Origin "*"; " > locationConfig.txt sudo sed -i "/^\s*location \/ {/r locationConfig.txt" $NGINX_CONFIG_PATH # Insert config inside location{} ./DNSConfigForItest.sh diff --git a/docs/scripts/setupValidationService.sh b/docs/scripts/setupValidationService.sh index a0e4f6e..4a7dfd7 100755 --- a/docs/scripts/setupValidationService.sh +++ b/docs/scripts/setupValidationService.sh @@ -1,9 +1,9 @@ #!/usr/bin/env bash # -# This file is Free Software under the MIT License -# without warranty, see README.md and LICENSES/MIT.txt for details. +# This file is Free Software under the Apache-2.0 License +# without warranty, see README.md and LICENSES/Apache-2.0.txt for details. # -# SPDX-License-Identifier: MIT +# SPDX-License-Identifier: Apache-2.0 # # SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) # Software-Engineering: 2022 Intevation GmbH @@ -21,7 +21,7 @@ echo ' remote_validator= { "url" = "http://localhost:8082", "presets" = ["mandatory"], "cache" = "/var/lib/csaf/validations.db" } ' | sudo tee --append /etc/csaf/config.toml -npm install pm2 -g +sudo npm install pm2 -g pushd ~ git clone https://github.com/secvisogram/csaf-validator-service.git diff --git a/docs/scripts/testAggregator.sh b/docs/scripts/testAggregator.sh index 13d1df0..f6322f6 100755 --- a/docs/scripts/testAggregator.sh +++ b/docs/scripts/testAggregator.sh @@ -1,9 +1,9 @@ #!/usr/bin/env bash -# This file is Free Software under the MIT License -# without warranty, see README.md and LICENSES/MIT.txt for details. +# This file is Free Software under the Apache-2.0 License +# without warranty, see README.md and LICENSES/Apache-2.0.txt for details. # -# SPDX-License-Identifier: MIT +# SPDX-License-Identifier: Apache-2.0 # # SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) # Software-Engineering: 2022 Intevation GmbH @@ -29,6 +29,6 @@ popd echo echo '=== run aggregator' -cd ~/csaf_distribution/ +cd ~/csaf/ sudo cp docs/examples/aggregator.toml /etc/csaf sudo ./bin-linux-amd64/csaf_aggregator -c /etc/csaf/aggregator.toml diff --git a/docs/scripts/testChecker.sh b/docs/scripts/testChecker.sh index 8c680d4..28474d0 100755 --- a/docs/scripts/testChecker.sh +++ b/docs/scripts/testChecker.sh @@ -1,9 +1,9 @@ #!/usr/bin/env bash -# This file is Free Software under the MIT License -# without warranty, see README.md and LICENSES/MIT.txt for details. +# This file is Free Software under the Apache-2.0 License +# without warranty, see README.md and LICENSES/Apache-2.0.txt for details. # -# SPDX-License-Identifier: MIT +# SPDX-License-Identifier: Apache-2.0 # # SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) # Software-Engineering: 2022 Intevation GmbH @@ -11,7 +11,7 @@ set -e # to exit if a command in the script fails echo '==== run checker (twice)' -cd ~/csaf_distribution +cd ~/csaf ./bin-linux-amd64/csaf_checker -f html -o ../checker-results.html --insecure \ --client_cert ~/devca1/testclient1.crt \ diff --git a/docs/scripts/testDownloader.sh b/docs/scripts/testDownloader.sh index 3596399..6326536 100755 --- a/docs/scripts/testDownloader.sh +++ b/docs/scripts/testDownloader.sh @@ -1,16 +1,16 @@ #!/usr/bin/env bash -# This file is Free Software under the MIT License -# without warranty, see README.md and LICENSES/MIT.txt for details. +# This file is Free Software under the Apache-2.0 License +# without warranty, see README.md and LICENSES/Apache-2.0.txt for details. # -# SPDX-License-Identifier: MIT +# SPDX-License-Identifier: Apache-2.0 # # SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) # Software-Engineering: 2022 Intevation GmbH set -e # to exit if a command in the script fails -cd ~/csaf_distribution +cd ~/csaf echo echo '==== run downloader (1)' diff --git a/docs/scripts/uploadToProvider.sh b/docs/scripts/uploadToProvider.sh index e3aac28..151201c 100755 --- a/docs/scripts/uploadToProvider.sh +++ b/docs/scripts/uploadToProvider.sh @@ -2,10 +2,10 @@ # # Desc: Call ./downloadExamples.sh and then try csaf_uploader. # -# This file is Free Software under the MIT License -# without warranty, see README.md and LICENSES/MIT.txt for details. +# This file is Free Software under the Apache-2.0 License +# without warranty, see README.md and LICENSES/Apache-2.0.txt for details. # -# SPDX-License-Identifier: MIT +# SPDX-License-Identifier: Apache-2.0 # # SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) # Software-Engineering: 2022 Intevation GmbH diff --git a/docs/test-keys/Readme.md b/docs/test-keys/Readme.md index 5b422fd..94c8d8f 100644 --- a/docs/test-keys/Readme.md +++ b/docs/test-keys/Readme.md @@ -1,6 +1,6 @@ OpenPGP key-pairs for testing only. -Note: as the keypairs wre fully public, **do not use them for production**. +Note: as the keypairs are fully public, **do not use them for production**. Create your own keypair(s) with the security properties and operational security you need. diff --git a/examples/README.md b/examples/README.md index a70ea09..c525e96 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,6 +1,6 @@ # API examples -An experimental example of how to use `github.com/csaf-poc/csaf_distribution` +An experimental example of how to use `github.com/gocsaf/csaf` as a library. As usage of the repository as an API is currently a _work in progress_, these examples are likely to be changed. diff --git a/examples/product_lister/main.go b/examples/product_lister/main.go new file mode 100644 index 0000000..5ad26a9 --- /dev/null +++ b/examples/product_lister/main.go @@ -0,0 +1,141 @@ +// Package main implements a simple demo program to +// work with the csaf library. +package main + +import ( + "encoding/json" + "flag" + "fmt" + "log" + "os" + + "github.com/gocsaf/csaf/v3/csaf" +) + +func main() { + flag.Usage = func() { + if _, err := fmt.Fprintf(flag.CommandLine.Output(), + "Usage:\n %s [OPTIONS] files...\n\nOptions:\n", os.Args[0]); err != nil { + log.Fatalf("error: %v\n", err) + } + flag.PrintDefaults() + } + printProductIdentHelper := flag.Bool("print_ident_helper", false, "print product helper mapping") + flag.Parse() + + files := flag.Args() + if len(files) == 0 { + log.Println("No files given.") + return + } + + var printer func(*csaf.Advisory) error + if *printProductIdentHelper { + printer = printProductIdentHelperMapping + } else { + printer = printProductIDMapping + } + + if err := run(files, printer); err != nil { + log.Fatalf("error: %v\n", err) + } +} + +// visitFullProductNames iterates all full product names in the advisory. +func visitFullProductNames( + adv *csaf.Advisory, + visit func(*csaf.FullProductName), +) { + // Iterate over all full product names + if fpns := adv.ProductTree.FullProductNames; fpns != nil { + for _, fpn := range *fpns { + if fpn != nil && fpn.ProductID != nil { + visit(fpn) + } + } + } + + // Iterate over branches recursively + var recBranch func(b *csaf.Branch) + recBranch = func(b *csaf.Branch) { + if b == nil { + return + } + if fpn := b.Product; fpn != nil && fpn.ProductID != nil { + visit(fpn) + + } + for _, c := range b.Branches { + recBranch(c) + } + } + for _, b := range adv.ProductTree.Branches { + recBranch(b) + } + + // Iterate over relationships + if rels := adv.ProductTree.RelationShips; rels != nil { + for _, rel := range *rels { + if rel != nil { + if fpn := rel.FullProductName; fpn != nil && fpn.ProductID != nil { + visit(fpn) + } + } + } + } +} + +// run applies fn to all loaded advisories. +func run(files []string, fn func(*csaf.Advisory) error) error { + for _, file := range files { + adv, err := csaf.LoadAdvisory(file) + if err != nil { + return fmt.Errorf("loading %q failed: %w", file, err) + } + if err := fn(adv); err != nil { + return err + } + } + return nil +} + +// printJSON serializes v as indented JSON to stdout. +func printJSON(v any) error { + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + return enc.Encode(v) +} + +// printProductIDMapping prints all product ids with their name and identification helper. +func printProductIDMapping(adv *csaf.Advisory) error { + type productNameHelperMapping struct { + FullProductName *csaf.FullProductName `json:"product"` + ProductIdentificationHelper *csaf.ProductIdentificationHelper `json:"product_identification_helper"` + } + + productIDMap := map[csaf.ProductID][]productNameHelperMapping{} + visitFullProductNames(adv, func(fpn *csaf.FullProductName) { + productIDMap[*fpn.ProductID] = append(productIDMap[*fpn.ProductID], productNameHelperMapping{ + FullProductName: fpn, + ProductIdentificationHelper: fpn.ProductIdentificationHelper, + }) + }) + return printJSON(productIDMap) +} + +// printProductIdentHelperMapping prints all product identifier helper with their product id. +func printProductIdentHelperMapping(adv *csaf.Advisory) error { + type productIdentIDMapping struct { + ProductNameHelperMapping csaf.ProductIdentificationHelper `json:"product_identification_helper"` + ProductID *csaf.ProductID `json:"product_id"` + } + + productIdentMap := []productIdentIDMapping{} + visitFullProductNames(adv, func(fpn *csaf.FullProductName) { + productIdentMap = append(productIdentMap, productIdentIDMapping{ + ProductNameHelperMapping: *fpn.ProductIdentificationHelper, + ProductID: fpn.ProductID, + }) + }) + return printJSON(productIdentMap) +} diff --git a/examples/purls_searcher/main.go b/examples/purls_searcher/main.go index c1ec3e1..72fb976 100644 --- a/examples/purls_searcher/main.go +++ b/examples/purls_searcher/main.go @@ -1,5 +1,5 @@ // Package main implements a simple demo program to -// work with the csaf_distribution library. +// work with the csaf library. package main import ( @@ -9,8 +9,8 @@ import ( "os" "strings" - "github.com/csaf-poc/csaf_distribution/v3/csaf" - "github.com/csaf-poc/csaf_distribution/v3/util" + "github.com/gocsaf/csaf/v3/csaf" + "github.com/gocsaf/csaf/v3/util" ) func main() { diff --git a/go.mod b/go.mod index 469c8a3..8f2cd62 100644 --- a/go.mod +++ b/go.mod @@ -1,32 +1,33 @@ -module github.com/csaf-poc/csaf_distribution/v3 +module github.com/gocsaf/csaf/v3 -go 1.20 +go 1.24.9 + +toolchain go1.25.3 require ( - github.com/BurntSushi/toml v1.3.2 + github.com/BurntSushi/toml v1.5.0 github.com/Intevation/gval v1.3.0 github.com/Intevation/jsonpath v0.2.1 - github.com/ProtonMail/gopenpgp/v2 v2.7.4 - github.com/PuerkitoBio/goquery v1.8.1 - github.com/gofrs/flock v0.8.1 - github.com/jessevdk/go-flags v1.5.0 + github.com/ProtonMail/gopenpgp/v2 v2.9.0 + github.com/PuerkitoBio/goquery v1.11.0 + github.com/gofrs/flock v0.13.0 + github.com/jessevdk/go-flags v1.6.1 github.com/mitchellh/go-homedir v1.1.0 - github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 - go.etcd.io/bbolt v1.3.8 - golang.org/x/crypto v0.14.0 - golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa - golang.org/x/term v0.13.0 - golang.org/x/time v0.3.0 + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 + go.etcd.io/bbolt v1.4.3 + golang.org/x/crypto v0.46.0 + golang.org/x/term v0.38.0 + golang.org/x/time v0.14.0 ) require ( - github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c // indirect + github.com/ProtonMail/go-crypto v1.3.0 // indirect github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect - github.com/andybalholm/cascadia v1.3.2 // indirect - github.com/cloudflare/circl v1.3.6 // indirect + github.com/andybalholm/cascadia v1.3.3 // indirect + github.com/cloudflare/circl v1.6.1 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/shopspring/decimal v1.3.1 // indirect - golang.org/x/net v0.17.0 // indirect - golang.org/x/sys v0.14.0 // indirect - golang.org/x/text v0.13.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect ) diff --git a/go.sum b/go.sum index 3a101d4..eeaa200 100644 --- a/go.sum +++ b/go.sum @@ -1,115 +1,126 @@ -github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= -github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/Intevation/gval v1.3.0 h1:+Ze5sft5MmGbZrHj06NVUbcxCb67l9RaPTLMNr37mjw= github.com/Intevation/gval v1.3.0/go.mod h1:xmGyGpP5be12EL0P12h+dqiYG8qn2j3PJxIgkoOHO5o= github.com/Intevation/jsonpath v0.2.1 h1:rINNQJ0Pts5XTFEG+zamtdL7l9uuE1z0FBA+r55Sw+A= github.com/Intevation/jsonpath v0.2.1/go.mod h1:WnZ8weMmwAx/fAO3SutjYFU+v7DFreNYnibV7CiaYIw= -github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= -github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE= -github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= +github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw= -github.com/ProtonMail/gopenpgp/v2 v2.7.4 h1:Vz/8+HViFFnf2A6XX8JOvZMrA6F5puwNvvF21O1mRlo= -github.com/ProtonMail/gopenpgp/v2 v2.7.4/go.mod h1:IhkNEDaxec6NyzSI0PlxapinnwPVIESk8/76da3Ct3g= -github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= -github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= -github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= -github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= -github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= -github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= -github.com/cloudflare/circl v1.3.6 h1:/xbKIqSHbZXHwkhbrhrt2YOHIwYJlXH94E3tI/gDlUg= -github.com/cloudflare/circl v1.3.6/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ProtonMail/gopenpgp/v2 v2.9.0 h1:ruLzBmwe4dR1hdnrsEJ/S7psSBmV15gFttFUPP/+/kE= +github.com/ProtonMail/gopenpgp/v2 v2.9.0/go.mod h1:IldDyh9Hv1ZCCYatTuuEt1XZJ0OPjxLpTarDfglih7s= +github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw= +github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ= +github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= +github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= -github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= +github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= -github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= +github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= -github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= -github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= -go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= +go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= -golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= -golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/certs/certs.go b/internal/certs/certs.go index dcbf7ef..ab31d8b 100644 --- a/internal/certs/certs.go +++ b/internal/certs/certs.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2023 German Federal Office for Information Security (BSI) // Software-Engineering: 2023 Intevation GmbH diff --git a/internal/certs/certs_test.go b/internal/certs/certs_test.go index e9a36cd..5bd7025 100644 --- a/internal/certs/certs_test.go +++ b/internal/certs/certs_test.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2023 German Federal Office for Information Security (BSI) // Software-Engineering: 2023 Intevation GmbH @@ -20,13 +20,13 @@ func TestLoadCertificates(t *testing.T) { passphrase = "qwer" missingCert = "data/testclientcert_missing.crt" missingTestkey = "data/testclientkey_missing.pem" - privateKey = "data/privated.pem" + privateKey = "data/private.pem" privateCert = "data/cert.crt" ) // Try to load cert that is not protected, expect success. if cert, err := LoadCertificate(&testCert, &testKey, nil); cert == nil || err != nil { - t.Errorf("Failure: Couldn't load supposedly valid certificate.") + t.Errorf("Failure: Couldn't load supposedly valid certificate. Got error: %v", err) } // Try to load no cert, expect error. if cert, err := LoadCertificate(nil, &testKey, nil); cert != nil || err == nil { @@ -46,7 +46,7 @@ func TestLoadCertificates(t *testing.T) { } // Try to load encrypted cert, expecting success. if cert, err := LoadCertificate(&privateCert, &privateKey, &passphrase); cert == nil || err != nil { - t.Errorf("Failure: Couldn't load supposedly valid encrypted certificate.") + t.Errorf("Failure: Couldn't load supposedly valid encrypted certificate. Got error: %v", err) } // Try to load wrong encrypted cert, expecting error. if cert, err := LoadCertificate(&testKey, &privateKey, &passphrase); cert != nil || err == nil { @@ -56,8 +56,8 @@ func TestLoadCertificates(t *testing.T) { if cert, err := LoadCertificate(&missingCert, &privateKey, &passphrase); cert != nil || err == nil { t.Errorf("Failure: No Failure while loading nonexistens certificate.") } - // Try to load nonexistent encrypted cert, expecting error. + // Try to load nonexistent encrypted cert, expecting success. if cert, err := LoadCertificate(nil, nil, nil); cert != nil || err != nil { - t.Errorf("Failure: Expected nil return.") + t.Errorf("Failure: Expected nil return. Got error: %v", err) } } diff --git a/internal/certs/createTestCerts.sh b/internal/certs/createTestCerts.sh new file mode 100755 index 0000000..084677e --- /dev/null +++ b/internal/certs/createTestCerts.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2025 German Federal Office for Information Security (BSI) +# Software-Engineering: 2025 Intevation GmbH + +# cab be used to generated the certificates for the go tests +# as the resulting files are in the repository, this script does not +# need to be run each time, its purpose is to document how the keys and +# certs were created + +set -e + +certtool --generate-privkey --outfile testserver-key.pem + + +echo ' +organization = "CSAF" +unit = "CSAF Distribution" +country = "DE" +cn = "csaf.test" + +dns_name = "csaf.test" +dns_name = "localhost" +dns_name = "*.csaf.test" +ip_address = "127.0.0.1" +ip_address = "::1" + +tls_www_server +tls_www_client +ocsp_signing_key +encryption_key +signing_key +expiration_days = 36500 +' > gnutls-certtool.testserver.template + +certtool --generate-self-signed --load-privkey testserver-key.pem --outfile cert.crt --template gnutls-certtool.testserver.template --stdout | head -1 + +# for testing legacy code path, we use openssl's traditional mode to +# create a password protected variant after RFC 1423 that still can be read +# by https://pkg.go.dev/crypto/x509#DecryptPEMBlock. Citation: +# Legacy PEM encryption as specified in RFC 1423 is insecure by design. +# Since it does not authenticate the ciphertext, it is vulnerable +# to padding oracle attacks that can let an attacker recover the plaintext. +openssl rsa -in testserver-key.pem -out private.pem -aes256 -passout pass:qwer -traditional + +echo ' +organization = "CSAF Tools Development (internal)" +country = "DE" +cn = "Tester" + +tls_www_client +encryption_key +signing_key + +expiration_days = 36500 +' > gnutls-certtool.testclientkey.template + +certtool --generate-privkey --bits 3072 --outfile testclientkey.pem +certtool --generate-self-signed --load-privkey testclientkey.pem --template gnutls-certtool.testclientkey.template --outfile testclient.crt diff --git a/internal/certs/data/cert.crt b/internal/certs/data/cert.crt index f80d61c..f814e3e 100644 --- a/internal/certs/data/cert.crt +++ b/internal/certs/data/cert.crt @@ -1,37 +1,28 @@ -----BEGIN CERTIFICATE----- -MIIGajCCBNKgAwIBAgIUGNi4GgCUssOOe3k0VuHf3R0+d54wDQYJKoZIhvcNAQEL -BQAwgY0xFDASBgNVBAMTC0NvbW1vbiBuYW1lMRMwEQYDVQQLEwppbnRldmF0aW9u -MRMwEQYDVQQKEwppbnRldmF0aW9uMRMwEQYDVQQHEwppbnRldmF0aW9uMRUwEwYD -VQQIEwxMb3dlciBTYXhvbnkxCzAJBgNVBAYTAkdFMRIwEAYKCZImiZPyLGQBGRYC -REMwHhcNMjMwOTE5MDcwMDA1WhcNMjYwNjE0MDcwMDA3WjCB8DEQMA4GA1UEAxMH -cmVxdWVzdDETMBEGA1UECxMKaW50ZXZhdGlvbjETMBEGA1UEChMKaW50ZXZhdGlv -bjETMBEGA1UEBxMKb3NuYWJydWVjazEVMBMGA1UECBMMbG93ZXIgc2F4b255MQsw -CQYDVQQGEwJHRTESMBAGCgmSJomT8ixkARkWAkRDMREwDwYKCZImiZPyLGQBGRYB -LjERMA8GCgmSJomT8ixkARkWAS4xETAPBgoJkiaJk/IsZAEZFgEuMRMwEQYKCZIm -iZPyLGQBGRYDd3d3MRcwFQYKCZImiZPyLGQBARMHbm8gaWRlYTCCAaIwDQYJKoZI -hvcNAQEBBQADggGPADCCAYoCggGBAN0vZbLXtRzd61rR8Hos0BGnqCaJXIwGARwx -JojMyxASFT+KeC4QDRkgRrK6OY4k/i7TEHuUGk/Bm754++554wmmhDqv1Q4+VhhR -1K/JAz/HVZNTAR1rPKwG82lyEpPxlRNZg/QtF9DqQSoSkL/fJLs+rq4zlKozXzRE -auZ5Be8So1dXRZfMVUMDgtk+IX8+iCeZisiWfv62ttQ0EiuiXLagd6ruEuoCSVi2 -tVswsC/Hp8AI2Ro56mmHiWthuae1H8yDWUFLSe9AQW65qC/xVUgo/nMpK2BYVFKb -70TMjl/dZM0Qn1tdiNyqCkbIhXjklZvZYhO+15TPkgDXDsqRUjpTrLZXLGrD6XIx -CRLZGY6YrUfsFTjUC6JrUrAR8zY7SLsYN5sUmFUSMpJnI+T/SD4p/0CXrKrbMOjW -Qqz6FX/WHPxvswGKHk5zHYGHrzx7OKmfVa6gzUgZSfOHj2xOOR2Un9DwNavIrmSC -WYXKZqig5qDyfzBvlXWEio/5GrDwgQIDAQABo4IBWzCCAVcwgcIGA1UdEQSBujCB -t4IrYSBkbnNOYW1lIG9mIHRoZSBzdWJqZWN0IG9mIHRoZSBjZXJ0aWZpY2F0ZYI3 -YW4gYWRkaXRpb25hbCBkbnNOYW1lIG9mIHRoZSBzdWJqZWN0IG9mIHRoZSBjZXJ0 -aWZpY2F0ZYIBLoIBLoIBLoIBLoIBLoIBLoIBLoIBLoIBLoIBLoIBLoIBLoIBLoIP -c2Vjb25kIGFkZGl0aW9ugg50aGlyZCBhZGRpdGlvboIHZG5zTmFtZTAMBgNVHRMB -Af8EAjAAMDEGA1UdJQQqMCgGCCsGAQUFBwMJBggrBgEFBQcDAgYIKwYBBQUHAwEG -CCsGAQUFBwMCMA8GA1UdDwEB/wQFAwMHsAAwHQYDVR0OBBYEFKrFhODjTKCopb+W -Qa29PsHR4HXgMB8GA1UdIwQYMBaAFCyZxCa1ZUHVy8LjikE8zumAiEgfMA0GCSqG -SIb3DQEBCwUAA4IBgQBTrAgh6d+qiLumEfmkNCmhewxKxDZp+Ni2nz9XRzNO2cQE -U0n8MdbnQInW3xJXng2sAcl1fQz0RN1hkyjDwi69mbbPgcTYmxJFvyt+zRYBe/Sq -4CGGkxEdPW94tMpQ6SrCn2mAMnvcq9A1pYBVYyPeUsaRHC5OUBFOMCevNy8JwNyY -MJ0H5HQCyCysbzA1d521pogGUs/tmbE+ym9zpV8vG0b6De1PexjVeGkTNYz6NCR2 -VZTQ+OJ5iE5pHPEC1Qif44LrR9Kdn/wu3RjTYyHeBOJFjK+DKgleNF4QVTcZQIPE -snN4H+/VSgTZQ3kgWbtpd1m5oRBJovEc2Qe+l+iDFCk8OA4z/x+fkvOeD3NUAl7D -9Pt3cP3UtWUJp4NJn2dvUljmQhB02HSqdNBhqKSg4/cf7l8Zo1ejvBUosrlgw3C3 -apDaC4/xk7woFKVYW25teH2ze+Gpz/YsLDtmL7Bri8CGVsqsN9yqO8SstwKBa3Rt -xQ2em6XnnanApT4iFX4= +MIIE2DCCA0CgAwIBAgIUT/9u6/HtTciy3NB6UGXu+U+UzT8wDQYJKoZIhvcNAQEL +BQAwTDELMAkGA1UEBhMCREUxDTALBgNVBAoTBENTQUYxGjAYBgNVBAsTEUNTQUYg +RGlzdHJpYnV0aW9uMRIwEAYDVQQDEwljc2FmLnRlc3QwIBcNMjUxMDE3MTAyMjM1 +WhgPMjEyNTA5MjMxMDIyMzVaMEwxCzAJBgNVBAYTAkRFMQ0wCwYDVQQKEwRDU0FG +MRowGAYDVQQLExFDU0FGIERpc3RyaWJ1dGlvbjESMBAGA1UEAxMJY3NhZi50ZXN0 +MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAwqJ45WlBG5CqW3Meewsf +Es1tqQRsHS/L6Hlz/aTZQHte/Co18qklnza0ZvK0mbPsQ8HLKXfU6Am5yw3u6vZj +XNfhWDW4QtsSk9f/y/fBADw17qYinoVyLpqZU5Z6kFRY5npY0C9bCtsAZd4qimx5 +yu/MhM8LHI9K2oKPSkFgRCTRKAo9sZ97o4wZmTxJIasOr0SPpmfMLs2sHSEqcK4d +/RxZ+OtYtd3pmE/WjxtSozCkdAccvrH+TSAuF3+/6oBiov8yX0KPNEBiiwuDXMUD +QWkjfcrxQZAswMWRo55JJYBbIjrinW8vldLooFo5trNEE2nukgRPhvLhiJdKKAeg ++A8jM/Bx7JgjRCPppIEmWdvXg+CS6L0hGj49pg3OcIiNNoufoXPRkFqmRh72n1Oj +2RC13W8H3C3SDYz20mqJhkbci+05vO/LgKj9te8xEs/xa4xCtv7ycuB2Etzf1cWS +zfz5LGXwwLI0rjpx3OAsr5i8Fukxe5maYLS9AUCTetTnAgMBAAGjga8wgawwDAYD +VR0TAQH/BAIwADAnBgNVHSUEIDAeBggrBgEFBQcDAgYIKwYBBQUHAwEGCCsGAQUF +BwMJMEQGA1UdEQQ9MDuCCWNzYWYudGVzdIIJbG9jYWxob3N0ggsqLmNzYWYudGVz +dIcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0O +BBYEFN2InaQvsu6hULCYeKc6pdE4VgVHMA0GCSqGSIb3DQEBCwUAA4IBgQBjPdXd +2xHzce3mi4RlANT4nOSdpELhl54xeJDgI9Evt70N8B4uTmOI5+F6JVICE25cnDs1 +c9SoHpWzh1ZuzfiBYa/cdQNUtaTfgHLi5GYtV1DzmKXVRUciBiNBWWxYMbTGvTOO +i3r6DEgOYuukeL4qj//EGOcTJEarHVSxPMuXTD/PoP/VpIdqRS9drEpFUC6lecZc +UJtUPAcyx0oD2vNmPmulDfYFMLLOPrIeNa0g7os4wgUl7+9wR1cPPRTXY0fW6Hoi +j+a8Qn80Q3PrOuEO/SZ4aHHpOk90bRqofyIhFjPwS0YN5w/Sn23uq1u2Dx+Zy+5K +6Cs9p5dJWu5/zU4ZdbQlpYIHXQVbido1TY92Z84skEsac2wVh7L2LMB3p3Gu9WYn +oKqFYCw5FICvRgyh1KG8QWhW59Em0Jxr8rTw6qyBQACdixKy6/1ok2ArMivTC8Gd +rEbefshgc6dnAZCAp1MjCU+tg9iYEymSSLdOtUKvHEIosUGO1p5ol0hReTQ= -----END CERTIFICATE----- diff --git a/internal/certs/data/private.pem b/internal/certs/data/private.pem new file mode 100644 index 0000000..483283c --- /dev/null +++ b/internal/certs/data/private.pem @@ -0,0 +1,42 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-256-CBC,3ACC7169D177F0159193ACAF3B3997A3 + +DjxOUO2dbAAeHUtP2jSW/7zpVTWeRaJi5Kce74r1TB2DJ4FYI8361ZZcOrjISGQJ +33f1Ic+8gv3P5ORzGAIfxzSmwQLk5y45da7of2dj69FXba+WoGNKgMS/KMmj+CvR +XylNJl4RE5zovePkPvk2JDvyjg+POMMu3UTOoxJzSTmifV6F7msuFTHMHhs3edSs +PUAHprSW7Qh5dYq3VK8tuqg9qdy3uLajpZkg9b9bBfaiku+SiRfwsdCjeAuubiJK +ctyPQclE5B1jEgJit6odjzsLENB9uCzkgq61UPoxbT6URZ0jJwhEZgh15UAr74QP +KAElD8Q7V2Z0w31vPhBcMIyrSaNlMr5p4teNFlMEZRa0lhNOXp7AY0DwBtioX2bR +VCxFTk409L/gVaweUnS0jzY0cj/pU1L1I5OWScDjCRkkj0Vk40S/zcy5esz85b5r +rGRxdRKqJIIZeb3r7WdvINFnNXL/KL/hxVruZcZse8cV3Na+w4rH+AHElMd51tZ+ +RKEBDqH0jlg3aelfAWXkV96pUtH/4lTSZ1+huQyHLUjTULll7L6BtxNGzY071buS +0CaTFyRcaipKYkXQjmrA49uTWQzrEgqiRZ4exh/gAaM/tEgVRfo/49Xo5wrTsGr0 +4Q0hBnUYAa+cVL7K8z2WAk1qerb1CsmiyjQZFI1S6z10ugS6zTDdB/kwW5ZvAzWB +/DXc9rJlgTFLbZK7Oty/IDayYkWD3BjfOV94oMeogK0eworAMxhvfIFkPxRHwhIp +9KfBw7xsa2gJECbi8BvrsV69PHn6EHmphn7NMpc8A3KmBFv1uOqWu9P7ef67+e+U +JprzVt2mUDoTUayzVkwQPy3rm5wWxVanHqtRXig3RN3pnreEv1AdfTKLfCxE2jvo +9fh6hNo3urgIL1KFXHjiXVRt03RGfpWfAI3JKqhkWOqZ7rVT19AuJ6On2J1dVMkm +TFelKdX97YlvMfNdKp1pkzOjZ2f4ehL5WCkMq88VgDrTmZv+CfcnrRslsLP6MSpX +scAMFDdkzSBUH3NyHxxkstcs5xQm1SuPN/omB7rpYgfhD6HwdgZNEAINtMNgIIoR +tW34hGkV6BhI+2y+pkIndm63JVikrbuLKiwTjwynFJWKTWgRBMR/BvJ1Bq/IfJNo +pC/hIpN95vUbHGzHRfmO9v5HiaAaBYGs59gL6WS0OlsyFXMr6a9ZmBDbZ7TD94Ax +IAhGhRE+5OpF/kWLfOriXMEbyY/oNoN1y7jdpMdmncq2/26/OhL8RFUKPlCbz0LN +5FUv7ouW8kvUgy5tGu78iPu6MNI+BzqLg+TrUu2bufajS+/VGAFo/2PX896n+2FJ +cP2DXlmFgC6udIeWsGNJI8Y50fC+YZxN+UthLOctiOgM4pGK1UDl8JQLbt0xRrJA +MI5XkbXJJYBdjHaqg8WGF260UgWhlD9sdJc7ntLX9S+3DoOboSwmYu4Y8p15e4Cg +8LHgW4NmnBFPX5/oyYMVCt7SWEnnwGEeebu+YgD9fbFAsag5TpE04zpx58rCW5bh +sJqRBCcZE5rqO9CUF1fYu0F24fv+E3LK9lujCMARVfJk8CLUg7VFL9dY2XWEfHsO +plZ0lmc5BntBoQ5r+xK/6TbK5nn1Fo+JPRjnDaE++QdVx9ZVjtT+a/wCD5NJr13k +dByZ3eCz5+mZUBGD1PWh5C+iyL3Wpq29b3EsHfSIMzOZsCpY2jkC8Jr90ADxhZcH +j8wFXHIWCe+Nn89Zim53gvbzumspRj8Yb08RATruqpvwj3M/K5K6P92Lt3uqt6UB +W+tAcChHNNWHFIT5CtCV/rltJYe2c9k9yG6BZJeLWPYgq90dFkIqbdkiz/pVpmKS +WMMzvkaK+LEcv+M9eMUQPdPYWhwv67wAlUsdLVWyQtxoYcLPUY3Io+Smn8eE+Qz7 +bxkSX+59QB3eCXrNGKTFsBiNDlxl+9YH6U9XhwIGyHlnBgN/79ts0ZutIpOibIWg +WPc9Cp5nkjjQl/4y8RSea8KSlkmM9YeTEo8cEL57XXOr1OO8UEPn/Ogoo5TI7JXL +jGh4evOcfWbiXZbn9kGshq1Kmv+lhN5IZ8QJY0s5Ze1eURnu0zlqKvFe3PxDxHV7 ++PaM8MneRkT5B8QgC7prh/yJ0KEI2MyIcYP73fw8cOLTXenw0bpmKLLfxu8mSx4M +VEDqeZJUb/XwsZTd9VT+42p4YT/6wRAe9eU3zA9wKh4Sr96vUGTPktXcpxCjoBre +3IaF/6aeyRQn91Ps9XmOc0/KSxZmHMxWv2btVc4oLHawnyRlLXXT7OSG4FFR7eE8 +IRoCCSip6YnIflp1v2n1f/07SzfKtrtVdiW1u2lbBJtwuzN/h8TtwRJan5bKWV/6 +-----END RSA PRIVATE KEY----- diff --git a/internal/certs/data/privated.pem b/internal/certs/data/privated.pem deleted file mode 100644 index 354598a..0000000 --- a/internal/certs/data/privated.pem +++ /dev/null @@ -1,42 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -Proc-Type: 4,ENCRYPTED -DEK-Info: DES-CBC,054A583F6C90570F - -tlGw8qlO25FaQdRLkai5L1JHWz/5fC4zd3qFISWssYH2FEnz8yfYsCoRLivVYhJB -fswOTj9h5b1RYRsWfIwCGfyNeOj8hkQrLwCW607vbhydGGJ4xc5RBF9MK0QCjSNT -r8myedNyfI4nm5enNVFDqYsqAc7cA3m1qw+QsAhPOrASDTp5svHR7g9+T6P5GDHm -B79nap02kfmodC7ytmWDBEclJ45Y19LOucN0+Nl6JgKkQEfWB/p2s2kGAGY1Of3X -/ERPOqeqZdFSdPDyX+mrzjGVhypgjBaz7XRh8OSeW8UP70rE+9aZKn9fIs2NyYMH -wwCElUmFV1Ye+/JtE4+Rcu6pG7NrX1rAC+pqPZaF8PT/kEuawiwrMuU0RP/8Y6mn -PRZZGZhXwBcfWPDN+JIj7e1NAXynwP/d4Pc4nb1O6EG3/Yip+F9NNaNbEfS4z9eV -Se7Gr/ySwxFhww9KhMtFYhkb6DVzy7StXpDqDmLhaF+qGCl86XRzZHho6EwQi+9r -c3VXbgogbjwIP8OgAKIZLuMxETZb0rvOr87sMAiqWRx+gRhryNniNr70anY8Vkpl -jcw6SJdqWuvOGaKjxWgdcHOzHdISEu/W6z8euTzMxX6/C7hBrKT8Edt71Jha26a5 -ZZNDH2XoqDphelfCbrARhw4P++KcnhPsY2da5cJ4021dfwXQGbGjcW1EAR3tCP/U -NKWc8Wm4dzuQSMqJERbWlXL8/UuvtyJR8VgNueg8EAHXCWBCS9i1i06gla9gPbdy -erhMDtUsJepFPDZVuqvm0dIjBaldl+74FHnPQ6+qFHXy6f71bGOmbonspnApqoeP -gc4zB65Nv+ws//XfdgwHhmtUkWS2ANPNQhU9o92l8XlqKicGC72dEEsR2TMS7fEW -K9/d06ZGu83FEXL43OXN79JmkpblonCWRgyVF7WPGufm+dtmR5zlIQruW2FJVwPZ -QmOioJYlSopOztyyBIuhZaNwVDQgoFtwHKRWAUseodzmHuPpvWCBjlL4hebJ7O0T -HGHGddqam3IPmyradhk0o1Qb54uk9rrzKWjcOEw850mJt3DnkHRNRgY96Gg0fA+m -+UxEOuGPvOudOMtC32vDKwAZ9eGgxAKea/kvaLFdPqwiq3B+IBetjSYGZ2kxVOAD -K8rHH6bnzrrasKHfOIBpw4MsiAG19sW1fFL61v5OXTcLOEQ/UVC8WinSj3JK894O -XjETyg8zvH+bYdlv9T2SGvAAzv1bJ3Iw9kb2VK0ZgwfwQgKpCDe6PEFLP7K2NNdF -zSw1GHOiDewsMD7VSfkmtevhzTOcQd/3uoyn/5ftcvcbqI4CGxP6kOxmul3NdfYl -insi95+IuhkSUQL02AdkI3SQhSnfmFRZSsy6JTXSN/7XOOzRFyMJcR1WlXOKFpt9 -G/bYGjVmfxtRqH4ZO7irCPiM+ZudXvPCl5VhZReBsJeEJcNuR36QTJIL3RQHyKTD -9Z12PegrgPXDgkSns1s8phTu+GygIEh67yLPbPYohYYbJUOkab7Il3JauihnuMSP -2BDDbwdvL1V7TQCmnopNb1srZj3q/1eWKmik2U1kvc78c3W03NC5wFETic2QCM9z -u/IaKAjO/kvSB8+ClSYaZDVLuBgUHf0DSG9cb5eoPqFt3t4zuWQhQjJR1YlLtQsJ -YSQFf0WqGj6sA2+AIy6Fv3oitlOPtRi/2seZ8ACSqxbwUFf3to8ZA3rJNoaYLvsT -sz++DrA8oHr4eDOiCoLeU6MLNiUvB6RGtjDwhQDh2LoJJyAdh9wB3vaAmEJ1u3o4 -cGyTCxbbkxRCWhMWW4NJbvdZORYhhhIu+TH5DaLgsZS1n+UF/amKQ0m8sj968Uo/ -w05QBNm/F3zg5dpzyW7uEfti8DaP/apDcf1dHSpk9ERkJ/QSIdgzGmrROQvh2tF/ -nvubXXMAex0tXFS6eyIZVgkT1S5eF001DsxIlp/jY6oFUYHquMcOQkyRAvUTvLO1 -pkexrPYrmx/alP71nNrBfixSTHMuPVb2jC38ElzllgxHfaaI5Q1hef4lVaErNaQ3 -m1hvE7dYkNomTt9fu/LHaxtw/P1eBlL44QcfqdqL67ROES+fB27d8vbajm1EQraw -QUoY+NM5KeQyKeRPWxDVQwAv02Lof/FSiB01yNqrzmRojtTykKB5VrnIA1DDP2vI -SoZjPZOSIJHh3qlDaKxlGOQD9Wp4OtIPLqxpBmRgGcq2AVtm57jRAF634nTGvB+N -7fvMpBay3EZy3sauM4MZk7bytJKK6huQjmER+GM/F/Wyw28L7rewK8ukPKx8Wybc -ljVLrduRPt97JH4WWejy+k5vv4LHWJLsGGU474YHGMXF2VE3kJ3JKj8Wm5gS6p/p ------END RSA PRIVATE KEY----- diff --git a/internal/certs/data/testclient.crt b/internal/certs/data/testclient.crt index f46f386..6cfd9fa 100644 --- a/internal/certs/data/testclient.crt +++ b/internal/certs/data/testclient.crt @@ -1,27 +1,26 @@ -----BEGIN CERTIFICATE----- -MIIEkDCCAvigAwIBAgIBFDANBgkqhkiG9w0BAQsFADBKMQ8wDQYDVQQDEwZUZXN0 -ZXIxKjAoBgNVBAoTIUNTQUYgVG9vbHMgRGV2ZWxvcG1lbnQgKGludGVybmFsKTEL -MAkGA1UEBhMCREUwHhcNMjMwOTA0MDcyMjAzWhcNMjMxMDI0MDcyMjAzWjBVMRow -GAYDVQQDExFUTFMgVGVzdCBDbGllbnQgMTEqMCgGA1UEChMhQ1NBRiBUb29scyBE -ZXZlbG9wbWVudCAoaW50ZXJuYWwpMQswCQYDVQQGEwJERTCCAaIwDQYJKoZIhvcN -AQEBBQADggGPADCCAYoCggGBAN0vZbLXtRzd61rR8Hos0BGnqCaJXIwGARwxJojM -yxASFT+KeC4QDRkgRrK6OY4k/i7TEHuUGk/Bm754++554wmmhDqv1Q4+VhhR1K/J -Az/HVZNTAR1rPKwG82lyEpPxlRNZg/QtF9DqQSoSkL/fJLs+rq4zlKozXzREauZ5 -Be8So1dXRZfMVUMDgtk+IX8+iCeZisiWfv62ttQ0EiuiXLagd6ruEuoCSVi2tVsw -sC/Hp8AI2Ro56mmHiWthuae1H8yDWUFLSe9AQW65qC/xVUgo/nMpK2BYVFKb70TM -jl/dZM0Qn1tdiNyqCkbIhXjklZvZYhO+15TPkgDXDsqRUjpTrLZXLGrD6XIxCRLZ -GY6YrUfsFTjUC6JrUrAR8zY7SLsYN5sUmFUSMpJnI+T/SD4p/0CXrKrbMOjWQqz6 -FX/WHPxvswGKHk5zHYGHrzx7OKmfVa6gzUgZSfOHj2xOOR2Un9DwNavIrmSCWYXK -Zqig5qDyfzBvlXWEio/5GrDwgQIDAQABo3YwdDAMBgNVHRMBAf8EAjAAMBMGA1Ud -JQQMMAoGCCsGAQUFBwMCMA8GA1UdDwEB/wQFAwMHoAAwHQYDVR0OBBYEFKrFhODj -TKCopb+WQa29PsHR4HXgMB8GA1UdIwQYMBaAFI6GhktAq9L2uRChC9LcXeedKiUg -MA0GCSqGSIb3DQEBCwUAA4IBgQAbUDaIkmubooDde7BpZQx742BsPg4IN68bIg9A -3jI9codx9c8l9ROvZ/7FeRNXzhYrQUwzcKpwtQ1mB7kM85oXaTLxrtnkZAO2fFSb -8RA6QjOrnOvewWaO3moCZaPnN1wWtlnUev2tD7D2Tz/f20dE2wbDV0BGb8bU4eGI -UVgzYrMh0MHaC8LKoXUWP97jp/p+9CG4D2S1CmpzP2Nm1dS03oj4UHIUtamjivYY -vOeoKATXmj59lgYqqoAVbTH6f4mZlZGmzUhRxK6hck7xBdiXAwfta72m4WzE7HRh -nHAgO5aVWb6zltvVDJhYumB9Itv+LI7uU8fF9Uyc65SZ2BevxgikoDNxTx0oNr+4 -hExQhJfKuPFF2NI1N2tPYJT53Cek/ZJfjX3TyBneqehthtRqoAIIEaF/QlXqzJIi -G66YFC3xFlLmaQh52DJkF2+hzcPhFTVQv3yCirGLUSS9Nm7vTO2wnnW5arZazSV+ -enRZb3oiVYFVDh0Hymz9g5VraMw= +MIIEeDCCAuCgAwIBAgIUTqTcNqmr8Ou/MpL1AUnM/3gcoUkwDQYJKoZIhvcNAQEL +BQAwSjELMAkGA1UEBhMCREUxKjAoBgNVBAoTIUNTQUYgVG9vbHMgRGV2ZWxvcG1l +bnQgKGludGVybmFsKTEPMA0GA1UEAxMGVGVzdGVyMCAXDTI1MTAxNzEwMjIzNloY +DzIxMjUwOTIzMTAyMjM2WjBKMQswCQYDVQQGEwJERTEqMCgGA1UEChMhQ1NBRiBU +b29scyBEZXZlbG9wbWVudCAoaW50ZXJuYWwpMQ8wDQYDVQQDEwZUZXN0ZXIwggGi +MA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQDBN4fIBbwuGJXjXoa6F7e4Zzin +Yd9EB4nt5TkNoMkRgQe0JIJ+t1/lS/xlI7ATxNjUdybnYwCrEfDvy8XGwN6te+Xh +dz6HKDWPijW+ritQW9kouxJJSpna95L8SqU4tjdfyL/2X9E/7j3VYw1//zcmhLJg +1Os0+JHPcPuj1vmwLa1v7eGTCNlt0K8DbrlhPlteJB3hWolNIoVDjRemZFmqwUeV +GZ/XJos7OTB07p08yCOFhLl9jXCgEDDkKmcnAil3YhjudlEGSjdzFLskVD4xrtQ5 +GsbdJHyHhcUdgh+vqX2bFSklwdwVil1qIUEHnxpcRMaluZQ4u1tCgNhKNQHrJzVQ +n1aRVAYdX1PxfoIb5wt0+25MiVw8y8EcrMH97Ss26eNAtLeHZNrY9alqx/Cs8gOi +I8wA2Nga138tZuCJRXsDOnom9RrtdPLajhSb7n33Iq8ZDhYVGEIm2pc5MJxaI53V +e2WhmemFPfYwUAtzdGgwrBoY9MechdtNLGZqHxECAwEAAaNUMFIwDAYDVR0TAQH/ +BAIwADATBgNVHSUEDDAKBggrBgEFBQcDAjAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0O +BBYEFBVaa/ovyPpbk/8nlmuISWB2/t8xMA0GCSqGSIb3DQEBCwUAA4IBgQC8EeDx +qipM7bAFxkAOmvhNAjodKXRCWKhatD8HryeINOPgWajzLlHj/PCnULulhaFO1viA ++iBBKbHb+7LImb/owlNVu8iYDh/xBXmLrOHyd12K8dyN471iTBrskQwSCnYd6e/p +4i0hhNj5JidOgA6swjt9j4X7/IgsvXexLIAhqgQDSsKQpPK17E9IB+d5p3UHU71w +Ob1mGIZ2j/GJnC6YmGFPqIZZ+cy3aVOypWf8RVZYPTFCz43ZuC70cP3kl2io75Rp +rWUNKXU+yUdBphHN6KJXUmlH4T9yqXKqnxK+9CnVC/CTlucF9VpktN7wfVxVPsrY +L79iys+FLPKrDkqcjpIJ2l/n/ugcUcXvN477qFCGbRY/3tB3Dmf4AvMPpTsStkXw +Ld+xAHog8upjVGsmXODX4sKjRMIFLIHbM01Iw0ECdKoKIMwjFGenwGmpBZA/Pfxe +AXBejd9KD0stCfHuKqx7Iu5N7Fg8BCLzmcSyoOmwJEo+Z3Z42IfSjOX8rQQ= -----END CERTIFICATE----- diff --git a/internal/certs/data/testclientkey.pem b/internal/certs/data/testclientkey.pem index ce2726e..4e8b564 100644 --- a/internal/certs/data/testclientkey.pem +++ b/internal/certs/data/testclientkey.pem @@ -3,180 +3,180 @@ Public Key Info: Key Security Level: High (3072 bits) modulus: - 00:dd:2f:65:b2:d7:b5:1c:dd:eb:5a:d1:f0:7a:2c:d0 - 11:a7:a8:26:89:5c:8c:06:01:1c:31:26:88:cc:cb:10 - 12:15:3f:8a:78:2e:10:0d:19:20:46:b2:ba:39:8e:24 - fe:2e:d3:10:7b:94:1a:4f:c1:9b:be:78:fb:ee:79:e3 - 09:a6:84:3a:af:d5:0e:3e:56:18:51:d4:af:c9:03:3f - c7:55:93:53:01:1d:6b:3c:ac:06:f3:69:72:12:93:f1 - 95:13:59:83:f4:2d:17:d0:ea:41:2a:12:90:bf:df:24 - bb:3e:ae:ae:33:94:aa:33:5f:34:44:6a:e6:79:05:ef - 12:a3:57:57:45:97:cc:55:43:03:82:d9:3e:21:7f:3e - 88:27:99:8a:c8:96:7e:fe:b6:b6:d4:34:12:2b:a2:5c - b6:a0:77:aa:ee:12:ea:02:49:58:b6:b5:5b:30:b0:2f - c7:a7:c0:08:d9:1a:39:ea:69:87:89:6b:61:b9:a7:b5 - 1f:cc:83:59:41:4b:49:ef:40:41:6e:b9:a8:2f:f1:55 - 48:28:fe:73:29:2b:60:58:54:52:9b:ef:44:cc:8e:5f - dd:64:cd:10:9f:5b:5d:88:dc:aa:0a:46:c8:85:78:e4 - 95:9b:d9:62:13:be:d7:94:cf:92:00:d7:0e:ca:91:52 - 3a:53:ac:b6:57:2c:6a:c3:e9:72:31:09:12:d9:19:8e - 98:ad:47:ec:15:38:d4:0b:a2:6b:52:b0:11:f3:36:3b - 48:bb:18:37:9b:14:98:55:12:32:92:67:23:e4:ff:48 - 3e:29:ff:40:97:ac:aa:db:30:e8:d6:42:ac:fa:15:7f - d6:1c:fc:6f:b3:01:8a:1e:4e:73:1d:81:87:af:3c:7b - 38:a9:9f:55:ae:a0:cd:48:19:49:f3:87:8f:6c:4e:39 - 1d:94:9f:d0:f0:35:ab:c8:ae:64:82:59:85:ca:66:a8 - a0:e6:a0:f2:7f:30:6f:95:75:84:8a:8f:f9:1a:b0:f0 - 81: + 00:c1:37:87:c8:05:bc:2e:18:95:e3:5e:86:ba:17:b7 + b8:67:38:a7:61:df:44:07:89:ed:e5:39:0d:a0:c9:11 + 81:07:b4:24:82:7e:b7:5f:e5:4b:fc:65:23:b0:13:c4 + d8:d4:77:26:e7:63:00:ab:11:f0:ef:cb:c5:c6:c0:de + ad:7b:e5:e1:77:3e:87:28:35:8f:8a:35:be:ae:2b:50 + 5b:d9:28:bb:12:49:4a:99:da:f7:92:fc:4a:a5:38:b6 + 37:5f:c8:bf:f6:5f:d1:3f:ee:3d:d5:63:0d:7f:ff:37 + 26:84:b2:60:d4:eb:34:f8:91:cf:70:fb:a3:d6:f9:b0 + 2d:ad:6f:ed:e1:93:08:d9:6d:d0:af:03:6e:b9:61:3e + 5b:5e:24:1d:e1:5a:89:4d:22:85:43:8d:17:a6:64:59 + aa:c1:47:95:19:9f:d7:26:8b:3b:39:30:74:ee:9d:3c + c8:23:85:84:b9:7d:8d:70:a0:10:30:e4:2a:67:27:02 + 29:77:62:18:ee:76:51:06:4a:37:73:14:bb:24:54:3e + 31:ae:d4:39:1a:c6:dd:24:7c:87:85:c5:1d:82:1f:af + a9:7d:9b:15:29:25:c1:dc:15:8a:5d:6a:21:41:07:9f + 1a:5c:44:c6:a5:b9:94:38:bb:5b:42:80:d8:4a:35:01 + eb:27:35:50:9f:56:91:54:06:1d:5f:53:f1:7e:82:1b + e7:0b:74:fb:6e:4c:89:5c:3c:cb:c1:1c:ac:c1:fd:ed + 2b:36:e9:e3:40:b4:b7:87:64:da:d8:f5:a9:6a:c7:f0 + ac:f2:03:a2:23:cc:00:d8:d8:1a:d7:7f:2d:66:e0:89 + 45:7b:03:3a:7a:26:f5:1a:ed:74:f2:da:8e:14:9b:ee + 7d:f7:22:af:19:0e:16:15:18:42:26:da:97:39:30:9c + 5a:23:9d:d5:7b:65:a1:99:e9:85:3d:f6:30:50:0b:73 + 74:68:30:ac:1a:18:f4:c7:9c:85:db:4d:2c:66:6a:1f + 11: public exponent: 01:00:01: private exponent: - 14:ff:c0:f9:ff:bc:b4:26:e5:87:53:d3:2e:e6:3e:42 - ce:d6:0a:02:94:84:be:b5:30:46:02:50:8e:90:e0:cf - b6:b0:b7:a6:bd:48:cc:d5:8b:d8:ea:72:ff:af:dd:17 - 3c:be:d1:1b:ca:6d:cd:10:a6:86:a8:d9:d2:44:44:27 - d0:65:51:65:0c:27:34:07:dc:7b:38:64:10:03:7c:f4 - a1:cd:40:de:24:3a:e0:21:bc:ef:33:1d:9f:61:e8:57 - ac:e4:9c:c0:7b:df:7c:f8:20:83:ac:0b:8e:0b:d3:62 - eb:8a:8e:03:5b:a3:e5:08:ae:df:a7:fe:85:92:e8:a5 - ae:58:46:72:d6:fc:91:43:b1:7b:a4:c0:5f:51:c3:50 - 0d:e2:67:e8:af:51:13:41:a9:8d:ef:fb:a1:a4:e2:84 - 7c:2b:a0:50:c5:fe:ed:84:a5:25:83:86:4a:d3:0f:56 - 37:38:e6:1e:26:7d:45:22:0b:ba:22:35:be:f8:8b:1b - 72:90:13:c4:1f:c5:d1:34:b5:0e:b2:ee:f7:e1:b9:5e - a2:29:8d:f9:6e:23:4b:50:8f:35:c8:a9:f3:d2:1f:dd - ce:a0:96:50:2d:2e:af:cf:b5:e1:20:e7:e9:d2:49:ed - b5:0e:5b:3e:d1:4b:f1:fa:c2:73:3a:1b:51:34:7e:75 - 30:06:d2:47:d2:a8:2a:45:be:16:fb:8f:63:84:85:b7 - bf:f7:c4:c5:3d:95:56:8c:d1:02:7f:58:ac:4d:11:7b - c5:55:f3:c8:4e:d7:d9:aa:62:b0:e3:1e:04:5c:97:d1 - ca:e2:71:aa:8b:33:b4:34:e9:04:d4:70:7c:f4:cb:57 - 19:c1:03:23:f4:bc:4d:91:8f:b2:9a:99:1c:6c:81:2d - 4d:2d:e9:a1:e3:ce:e3:c9:62:52:89:1f:47:86:61:f1 - dd:bc:46:8d:79:0a:99:9d:aa:4b:a9:0a:72:54:db:dc - ae:48:be:60:4a:73:99:d8:3c:9e:07:78:05:df:87:39 + 70:0e:fd:af:d3:2b:ad:6c:52:d9:f8:43:99:00:12:6c + 5f:69:2b:22:87:33:54:4f:f9:69:fc:e9:db:7b:61:ac + 7c:c4:4c:7c:66:73:81:a9:61:a5:73:1e:fc:8a:aa:9a + ba:b6:94:18:94:81:99:b5:a1:0f:e2:15:c5:4c:ac:98 + df:07:96:f8:ea:89:c6:97:31:b5:8d:b0:16:21:46:cc + ce:28:62:3e:9b:c5:29:70:26:2f:d8:24:8e:a8:52:7d + d1:0e:83:ce:a7:09:9b:d3:57:87:3f:98:5f:c8:ab:ba + aa:31:2e:19:ae:84:1d:39:ab:9e:b2:42:f6:75:ff:68 + ae:73:00:fa:d7:a4:c5:3d:7c:4f:54:65:4e:1c:88:e6 + c2:b5:9d:a2:ca:38:61:45:09:17:01:68:5a:f7:4e:4d + cb:24:f1:e3:57:a1:97:58:1e:b3:ef:57:91:e0:1d:95 + 51:8c:a9:4a:4e:f7:cd:fe:f7:04:f3:ff:67:ad:e7:01 + 14:dc:7e:e4:00:c0:38:51:2f:04:db:39:6c:f1:1b:a4 + a5:f1:b4:5a:c3:17:d2:41:1a:5a:b5:f3:69:3b:b8:ba + 7b:59:96:d7:b2:c2:2c:9a:dd:e9:42:ce:fb:c8:22:fc + c5:33:97:6d:68:89:cd:e5:bc:2e:cc:9d:23:65:18:04 + 0c:83:b6:35:7e:16:09:96:d1:48:61:31:b1:ce:f8:50 + f0:14:ba:57:2f:02:1b:61:9c:bc:81:c1:ef:b3:bf:2f + fb:36:af:18:8c:90:40:55:5a:fd:a7:d4:ed:3b:94:a6 + df:ab:eb:6c:d2:bc:e3:80:7e:d5:06:21:28:9b:04:65 + b5:cc:04:b2:44:e9:2d:3b:7d:de:24:90:8d:fb:90:2d + 40:17:51:cf:a7:fa:ee:54:89:8f:c0:f4:e4:c2:bd:44 + 94:1d:8d:fc:b7:d7:05:4d:46:dc:63:1f:7f:d8:b4:8b + 11:db:37:be:4d:e9:2b:33:b9:6b:8c:a7:f0:43:56:c5 prime1: - 00:e9:63:0f:d7:49:31:27:a8:36:fe:95:bd:8d:05:c1 - 35:48:2e:03:4f:a6:57:54:3a:a4:95:3f:8e:9f:28:7c - d2:df:af:54:36:9e:7c:9f:c3:b9:64:8f:c0:b0:96:3c - aa:01:f6:9a:be:83:e2:85:20:0d:33:de:88:97:af:6f - be:3f:53:5a:a3:77:02:fd:81:17:91:3b:b2:2d:ab:78 - db:d9:43:db:04:69:82:61:30:e4:96:ac:88:8b:f6:3f - 56:c4:49:fd:d5:e5:8c:9d:30:ad:cf:d9:8d:5c:87:b5 - 27:4b:09:8e:19:ed:e2:11:3f:69:b2:47:be:70:39:11 - 41:a3:db:bb:b9:0e:e4:7b:50:d0:d2:c2:89:81:36:b9 - 6b:a6:fe:94:5b:06:66:e6:ed:86:52:42:5e:a9:0e:18 - db:18:f9:14:21:3d:e0:3c:8d:79:c3:f5:d2:cc:51:65 - fb:1c:49:ed:0a:d5:33:99:34:16:f9:1d:68:4a:78:da - 5f: + 00:f0:57:25:fd:aa:7e:98:13:08:28:99:16:eb:af:2e + 22:f6:e6:d7:bd:df:49:57:17:71:bf:21:ba:bf:75:54 + 5a:38:92:64:8c:4a:10:d4:4f:77:18:44:c2:79:f0:9d + 72:26:2e:9a:27:5d:e7:41:0b:c6:65:cb:fa:89:6d:9b + fb:87:78:e2:87:22:d4:92:21:f5:3a:57:fa:b0:bf:bb + 66:a2:bf:43:af:e8:58:b4:e2:a1:ed:97:62:09:0d:49 + ca:4c:99:a2:f4:f3:31:df:80:8e:56:be:64:9d:72:59 + ef:e9:db:4d:a3:e2:cf:79:1e:99:89:b2:f1:e3:2d:bc + 8f:a0:2a:2f:a6:f0:21:18:2d:f1:57:20:55:c1:c9:18 + c1:64:c6:9c:00:df:b2:54:55:8d:fe:d3:46:a0:5c:2e + f8:f7:10:b6:27:3a:4a:79:a1:14:b1:0c:c3:72:5b:2b + 66:d6:85:2c:7e:58:72:eb:33:62:73:34:e5:38:87:2e + 17: prime2: - 00:f2:9d:ae:5f:bd:b7:a3:87:a7:8d:30:46:06:8b:15 - a9:e5:a9:58:1c:2b:3a:7e:78:35:36:56:31:42:df:46 - 87:e8:57:0d:6e:99:de:cf:fb:a8:72:16:71:4b:b3:ad - ed:74:07:cb:cf:7d:2b:12:89:66:c4:0f:8a:ea:e3:37 - 17:2c:75:92:11:7a:a6:da:29:24:33:9b:69:c2:64:68 - 03:db:31:de:fe:1d:a2:4d:9d:91:9f:f0:50:b8:8f:d0 - 22:11:b9:b0:95:98:5e:65:bf:45:97:9b:35:f2:98:27 - 46:7c:b2:86:eb:7b:8b:57:f2:c3:49:47:7d:01:4a:9a - b0:e6:67:05:e5:61:7a:ab:63:c8:cb:d8:44:69:88:72 - a5:a9:60:89:60:df:e6:d9:4d:16:2b:35:7b:20:00:f3 - 3c:d1:78:f9:22:eb:48:c3:7f:78:63:e6:34:60:48:30 - 66:02:bb:38:c2:94:2e:b9:86:b2:2f:9a:4f:17:7f:e1 - 1f: + 00:cd:ce:5d:fb:04:16:34:f4:de:02:7d:00:07:3e:b0 + 94:8c:f4:3a:62:05:37:1a:4f:d8:40:2e:31:11:07:77 + 09:8b:bd:76:6e:85:b9:43:df:3f:86:cb:db:6d:fe:c6 + 4c:ca:e1:16:ce:5c:0e:e1:b1:10:0d:8d:48:99:d7:43 + 7f:6c:b6:20:b2:cd:0c:56:26:02:18:81:e1:67:e5:cd + b3:66:1e:77:dc:49:6a:5d:8c:9c:0e:24:14:3e:a1:4a + 7e:cf:72:e6:e4:03:e6:38:41:fa:2b:91:71:6c:33:b0 + ec:07:3a:be:5b:f8:74:f5:e4:1f:9c:c4:d0:d4:75:a8 + 35:09:05:0f:7f:54:4e:2a:bc:cc:92:de:1e:f4:74:8a + 56:36:e0:b1:37:cf:b3:9c:57:05:76:59:69:c3:03:de + c2:33:0c:c4:a1:4f:2a:b8:3c:20:63:c9:58:96:1a:e2 + 62:ce:bf:fb:a9:51:b0:66:99:35:d6:d2:60:59:72:bd + 17: coefficient: - 00:93:3e:7c:b9:ea:87:52:37:fa:d5:0a:36:fb:e1:d0 - fc:62:4d:00:0b:ad:a8:fb:bd:34:53:96:c2:6c:a1:6a - 49:b7:a0:24:33:16:95:79:14:ac:bb:75:8d:78:e9:10 - fa:be:44:60:58:94:4a:9c:ba:64:1d:86:27:8b:7f:51 - 4d:80:b0:ff:7a:91:c0:4d:a4:aa:d1:f1:79:7d:8f:71 - 49:12:73:d4:44:5f:0c:2e:55:a6:d9:13:b8:3b:e5:dc - e1:14:98:7e:eb:5b:60:ad:d7:4b:da:c0:d8:3f:bf:70 - 92:53:8c:31:6a:8b:61:5e:a3:7d:ff:84:2c:7d:ed:9f - 74:29:9a:e7:14:fb:c3:ab:8e:9f:60:6a:98:ab:86:0b - ea:fb:ff:20:2f:3b:a7:76:03:3a:55:bb:b2:c6:9c:b5 - 66:36:b8:1c:7f:9b:b6:62:89:ff:6a:d6:35:58:0b:f0 - 55:27:01:f0:67:8d:88:3f:74:48:3d:bf:8c:fc:05:62 - 47: + 33:6a:05:3e:1e:46:46:58:e2:61:38:6a:c2:8f:77:a2 + 27:b7:19:38:75:40:d6:8c:87:bc:65:a6:24:c3:97:e5 + ef:70:1b:2c:4e:9c:08:ca:1d:eb:97:11:74:14:bb:99 + de:22:a1:6e:bc:6c:c6:25:98:8a:8e:17:f4:f9:4d:a3 + 1d:01:5e:26:0e:b4:e8:1c:aa:06:7c:66:b1:89:5a:b4 + 82:65:d1:bf:20:cb:b2:57:a8:af:7f:00:07:00:7c:5e + d4:09:60:0c:0a:6e:a8:e1:16:1b:04:95:b1:bc:2b:35 + ad:80:78:0a:0a:1d:5f:c9:cc:24:3a:5e:20:03:50:44 + b8:b0:f3:f1:17:ff:41:b8:5d:56:9b:1c:f1:e6:2b:c6 + ba:a2:8c:18:25:8c:d5:90:f1:28:66:29:bb:40:3d:b2 + f9:65:99:2e:b7:1b:e3:d0:d2:1a:d7:96:70:cc:f6:74 + c5:2e:bf:f5:c9:60:c0:ff:38:f8:a8:db:1a:7d:6a:4e + exp1: - 00:99:16:2d:91:dd:a4:ac:8a:9e:68:27:f8:89:c4:38 - 93:a6:a0:e7:f3:1a:fd:35:76:b1:f6:64:16:3d:37:e5 - 88:bc:c8:d8:c8:6a:f4:fc:26:fa:38:88:42:b0:92:1b - 80:b8:80:f5:c7:f9:e2:5f:c8:42:60:bf:9b:81:43:c6 - 5c:58:55:68:a2:c8:b1:e1:6f:07:f2:6f:e1:d4:2b:21 - bf:b3:a7:da:c5:ee:1f:63:79:1a:b7:ea:bc:36:72:73 - e1:8a:27:ae:a4:db:49:7c:e2:2d:60:a5:27:20:86:b3 - c0:ee:6b:7a:16:6f:ff:55:a8:ee:bf:ce:67:90:5d:1e - 80:9b:e6:ca:1f:fd:30:c9:e2:9c:d7:62:5b:a7:b2:29 - b5:ff:78:06:00:1f:16:e8:6a:ed:2c:8f:f4:5f:97:ab - 9e:2b:a7:56:18:e7:e9:6a:4e:b2:8c:63:76:be:26:b6 - 6a:1c:88:31:40:65:d0:ce:b1:68:50:47:85:dd:33:a0 - a9: + 5c:1b:49:f7:f9:0b:23:04:c8:2f:a6:db:dd:de:f8:f3 + 75:63:ea:72:5d:cc:21:90:5e:8b:3d:45:f0:71:ea:ad + d8:d8:61:a8:52:0a:39:13:6b:34:e5:c5:12:2e:60:68 + 8a:b1:79:6a:74:d6:57:5b:47:e1:63:56:d4:ac:29:07 + 30:57:e7:98:9a:84:94:ac:66:ea:c1:24:d5:ef:e4:c5 + e4:c1:20:13:9e:1b:c0:d6:c9:ef:e0:00:36:2f:dd:83 + a5:ef:8b:40:0c:a3:a4:60:04:2c:c2:32:95:14:69:db + 43:e8:43:cc:f6:f3:44:1b:b2:03:cf:8c:5b:df:ff:4f + 9b:b6:0f:25:0f:09:df:d6:5b:93:64:54:f9:3b:34:3d + 89:7d:83:f3:e1:c6:da:03:1f:b3:f5:0c:30:10:a3:ff + cd:cf:9d:bf:52:db:8f:d9:67:b0:a2:8f:94:97:d3:fe + 49:60:28:39:13:74:97:26:ce:28:10:b1:78:04:76:69 + exp2: - 00:8d:b1:5f:7c:94:ed:62:39:40:b6:a9:a1:cc:02:80 - c5:77:d6:9e:19:dd:79:4d:11:61:6a:79:8e:4d:92:de - bb:53:0b:3c:52:02:d5:69:3c:7d:95:1b:dc:51:2d:00 - 00:35:0a:b4:92:5a:74:c4:5f:b0:c0:02:9f:cc:2c:a5 - 29:08:93:25:9a:c5:ba:1a:a1:7a:7e:15:5e:ff:e3:ea - 07:8e:85:a2:c9:60:7f:40:bb:2c:a8:6f:0e:85:ab:a0 - 0f:b5:b0:70:1b:fe:1f:eb:66:78:fb:60:ef:71:de:40 - d9:de:cb:d9:16:40:52:12:2c:3a:b7:5a:63:fc:54:18 - e2:05:bd:d7:68:ae:b4:98:d2:2f:1c:36:13:46:5b:25 - 31:f1:28:eb:32:c3:b1:2b:e9:e4:6f:99:cd:6d:d4:80 - 3a:5d:d0:3c:18:93:b7:2c:4e:0e:fe:b1:1c:97:ba:b1 - 61:72:68:eb:6e:60:62:a5:81:b0:21:33:0a:cc:1b:a8 - 5b: + 6e:6d:c5:d5:b3:8a:aa:dd:9c:e6:5e:e6:0d:fd:20:48 + 85:1d:62:da:47:8c:1a:8d:2f:2e:b8:da:51:15:dd:54 + 7c:eb:ab:49:80:6d:39:32:e7:e6:4f:2a:2d:6a:20:43 + 02:35:26:c4:91:76:d6:b8:e8:31:2d:57:00:5d:15:f5 + a0:82:55:27:3b:88:dc:0c:c6:e1:19:87:b5:f5:03:9b + b8:36:ae:ff:bf:50:d8:63:63:34:df:3d:11:a1:ff:d3 + ed:41:ed:0b:f9:df:a4:de:19:fb:18:ae:70:6d:88:08 + 0d:95:02:a1:5c:be:7d:55:eb:74:75:d2:cb:bd:5a:05 + 23:12:d9:0e:ec:50:88:f4:07:1c:e3:1c:5e:f4:cd:69 + 97:46:97:30:a8:3c:ea:ad:72:db:de:fc:35:cc:b4:d1 + 25:0d:3b:d0:86:27:18:f6:02:37:28:c9:64:b9:86:31 + 98:58:41:13:c8:26:4b:d6:f7:a1:8d:fe:6e:e0:76:ff + Public Key PIN: - pin-sha256:iFdBnKP/7hZCLdj7qqTtdNPFjpZGka259fSYvv3X02U= + pin-sha256:Zv2mSFRUYM7ofg5obMJJxhZpnuvO7gkCOlqfDK1gzks= Public Key ID: - sha256:8857419ca3ffee16422dd8fbaaa4ed74d3c58e964691adb9f5f498befdd7d365 - sha1:aac584e0e34ca0a8a5bf9641adbd3ec1d1e075e0 + sha256:66fda648545460cee87e0e686cc249c616699eebceee09023a5a9f0cad60ce4b + sha1:155a6bfa2fc8fa5b93ff27966b88496076fedf31 -----BEGIN RSA PRIVATE KEY----- -MIIG5QIBAAKCAYEA3S9lste1HN3rWtHweizQEaeoJolcjAYBHDEmiMzLEBIVP4p4 -LhANGSBGsro5jiT+LtMQe5QaT8Gbvnj77nnjCaaEOq/VDj5WGFHUr8kDP8dVk1MB -HWs8rAbzaXISk/GVE1mD9C0X0OpBKhKQv98kuz6urjOUqjNfNERq5nkF7xKjV1dF -l8xVQwOC2T4hfz6IJ5mKyJZ+/ra21DQSK6JctqB3qu4S6gJJWLa1WzCwL8enwAjZ -GjnqaYeJa2G5p7UfzINZQUtJ70BBbrmoL/FVSCj+cykrYFhUUpvvRMyOX91kzRCf -W12I3KoKRsiFeOSVm9liE77XlM+SANcOypFSOlOstlcsasPpcjEJEtkZjpitR+wV -ONQLomtSsBHzNjtIuxg3mxSYVRIykmcj5P9IPin/QJesqtsw6NZCrPoVf9Yc/G+z -AYoeTnMdgYevPHs4qZ9VrqDNSBlJ84ePbE45HZSf0PA1q8iuZIJZhcpmqKDmoPJ/ -MG+VdYSKj/kasPCBAgMBAAECggGAFP/A+f+8tCblh1PTLuY+Qs7WCgKUhL61MEYC -UI6Q4M+2sLemvUjM1YvY6nL/r90XPL7RG8ptzRCmhqjZ0kREJ9BlUWUMJzQH3Hs4 -ZBADfPShzUDeJDrgIbzvMx2fYehXrOScwHvffPggg6wLjgvTYuuKjgNbo+UIrt+n -/oWS6KWuWEZy1vyRQ7F7pMBfUcNQDeJn6K9RE0Gpje/7oaTihHwroFDF/u2EpSWD -hkrTD1Y3OOYeJn1FIgu6IjW++IsbcpATxB/F0TS1DrLu9+G5XqIpjfluI0tQjzXI -qfPSH93OoJZQLS6vz7XhIOfp0knttQ5bPtFL8frCczobUTR+dTAG0kfSqCpFvhb7 -j2OEhbe/98TFPZVWjNECf1isTRF7xVXzyE7X2apisOMeBFyX0cricaqLM7Q06QTU -cHz0y1cZwQMj9LxNkY+ympkcbIEtTS3poePO48liUokfR4Zh8d28Ro15Cpmdqkup -CnJU29yuSL5gSnOZ2DyeB3gF34c5AoHBAOljD9dJMSeoNv6VvY0FwTVILgNPpldU -OqSVP46fKHzS369UNp58n8O5ZI/AsJY8qgH2mr6D4oUgDTPeiJevb74/U1qjdwL9 -gReRO7Itq3jb2UPbBGmCYTDklqyIi/Y/VsRJ/dXljJ0wrc/ZjVyHtSdLCY4Z7eIR -P2myR75wORFBo9u7uQ7ke1DQ0sKJgTa5a6b+lFsGZubthlJCXqkOGNsY+RQhPeA8 -jXnD9dLMUWX7HEntCtUzmTQW+R1oSnjaXwKBwQDyna5fvbejh6eNMEYGixWp5alY -HCs6fng1NlYxQt9Gh+hXDW6Z3s/7qHIWcUuzre10B8vPfSsSiWbED4rq4zcXLHWS -EXqm2ikkM5tpwmRoA9sx3v4dok2dkZ/wULiP0CIRubCVmF5lv0WXmzXymCdGfLKG -63uLV/LDSUd9AUqasOZnBeVheqtjyMvYRGmIcqWpYIlg3+bZTRYrNXsgAPM80Xj5 -IutIw394Y+Y0YEgwZgK7OMKULrmGsi+aTxd/4R8CgcEAmRYtkd2krIqeaCf4icQ4 -k6ag5/Ma/TV2sfZkFj035Yi8yNjIavT8Jvo4iEKwkhuAuID1x/niX8hCYL+bgUPG -XFhVaKLIseFvB/Jv4dQrIb+zp9rF7h9jeRq36rw2cnPhiieupNtJfOItYKUnIIaz -wO5rehZv/1Wo7r/OZ5BdHoCb5sof/TDJ4pzXYlunsim1/3gGAB8W6GrtLI/0X5er -niunVhjn6WpOsoxjdr4mtmociDFAZdDOsWhQR4XdM6CpAoHBAI2xX3yU7WI5QLap -ocwCgMV31p4Z3XlNEWFqeY5Nkt67Uws8UgLVaTx9lRvcUS0AADUKtJJadMRfsMAC -n8wspSkIkyWaxboaoXp+FV7/4+oHjoWiyWB/QLssqG8OhaugD7WwcBv+H+tmePtg -73HeQNney9kWQFISLDq3WmP8VBjiBb3XaK60mNIvHDYTRlslMfEo6zLDsSvp5G+Z -zW3UgDpd0DwYk7csTg7+sRyXurFhcmjrbmBipYGwITMKzBuoWwKBwQCTPny56odS -N/rVCjb74dD8Yk0AC62o+700U5bCbKFqSbegJDMWlXkUrLt1jXjpEPq+RGBYlEqc -umQdhieLf1FNgLD/epHATaSq0fF5fY9xSRJz1ERfDC5VptkTuDvl3OEUmH7rW2Ct -10vawNg/v3CSU4wxaothXqN9/4Qsfe2fdCma5xT7w6uOn2BqmKuGC+r7/yAvO6d2 -AzpVu7LGnLVmNrgcf5u2Yon/atY1WAvwVScB8GeNiD90SD2/jPwFYkc= +MIIG4gIBAAKCAYEAwTeHyAW8LhiV416Guhe3uGc4p2HfRAeJ7eU5DaDJEYEHtCSC +frdf5Uv8ZSOwE8TY1Hcm52MAqxHw78vFxsDerXvl4Xc+hyg1j4o1vq4rUFvZKLsS +SUqZ2veS/EqlOLY3X8i/9l/RP+491WMNf/83JoSyYNTrNPiRz3D7o9b5sC2tb+3h +kwjZbdCvA265YT5bXiQd4VqJTSKFQ40XpmRZqsFHlRmf1yaLOzkwdO6dPMgjhYS5 +fY1woBAw5CpnJwIpd2IY7nZRBko3cxS7JFQ+Ma7UORrG3SR8h4XFHYIfr6l9mxUp +JcHcFYpdaiFBB58aXETGpbmUOLtbQoDYSjUB6yc1UJ9WkVQGHV9T8X6CG+cLdPtu +TIlcPMvBHKzB/e0rNunjQLS3h2Ta2PWpasfwrPIDoiPMANjYGtd/LWbgiUV7Azp6 +JvUa7XTy2o4Um+599yKvGQ4WFRhCJtqXOTCcWiOd1XtloZnphT32MFALc3RoMKwa +GPTHnIXbTSxmah8RAgMBAAECggGAcA79r9MrrWxS2fhDmQASbF9pKyKHM1RP+Wn8 +6dt7Yax8xEx8ZnOBqWGlcx78iqqauraUGJSBmbWhD+IVxUysmN8HlvjqicaXMbWN +sBYhRszOKGI+m8UpcCYv2CSOqFJ90Q6DzqcJm9NXhz+YX8iruqoxLhmuhB05q56y +QvZ1/2iucwD616TFPXxPVGVOHIjmwrWdoso4YUUJFwFoWvdOTcsk8eNXoZdYHrPv +V5HgHZVRjKlKTvfN/vcE8/9nrecBFNx+5ADAOFEvBNs5bPEbpKXxtFrDF9JBGlq1 +82k7uLp7WZbXssIsmt3pQs77yCL8xTOXbWiJzeW8LsydI2UYBAyDtjV+FgmW0Uhh +MbHO+FDwFLpXLwIbYZy8gcHvs78v+zavGIyQQFVa/afU7TuUpt+r62zSvOOAftUG +ISibBGW1zASyROktO33eJJCN+5AtQBdRz6f67lSJj8D05MK9RJQdjfy31wVNRtxj +H3/YtIsR2ze+TekrM7lrjKfwQ1bFAoHBAPBXJf2qfpgTCCiZFuuvLiL25te930lX +F3G/Ibq/dVRaOJJkjEoQ1E93GETCefCdciYumidd50ELxmXL+oltm/uHeOKHItSS +IfU6V/qwv7tmor9Dr+hYtOKh7ZdiCQ1JykyZovTzMd+Ajla+ZJ1yWe/p202j4s95 +HpmJsvHjLbyPoCovpvAhGC3xVyBVwckYwWTGnADfslRVjf7TRqBcLvj3ELYnOkp5 +oRSxDMNyWytm1oUsflhy6zNiczTlOIcuFwKBwQDNzl37BBY09N4CfQAHPrCUjPQ6 +YgU3Gk/YQC4xEQd3CYu9dm6FuUPfP4bL223+xkzK4RbOXA7hsRANjUiZ10N/bLYg +ss0MViYCGIHhZ+XNs2Yed9xJal2MnA4kFD6hSn7PcubkA+Y4QforkXFsM7DsBzq+ +W/h09eQfnMTQ1HWoNQkFD39UTiq8zJLeHvR0ilY24LE3z7OcVwV2WWnDA97CMwzE +oU8quDwgY8lYlhriYs6/+6lRsGaZNdbSYFlyvRcCgcBcG0n3+QsjBMgvptvd3vjz +dWPqcl3MIZBeiz1F8HHqrdjYYahSCjkTazTlxRIuYGiKsXlqdNZXW0fhY1bUrCkH +MFfnmJqElKxm6sEk1e/kxeTBIBOeG8DWye/gADYv3YOl74tADKOkYAQswjKVFGnb +Q+hDzPbzRBuyA8+MW9//T5u2DyUPCd/WW5NkVPk7ND2JfYPz4cbaAx+z9QwwEKP/ +zc+dv1Lbj9lnsKKPlJfT/klgKDkTdJcmzigQsXgEdmkCgcBubcXVs4qq3ZzmXuYN +/SBIhR1i2keMGo0vLrjaURXdVHzrq0mAbTky5+ZPKi1qIEMCNSbEkXbWuOgxLVcA +XRX1oIJVJzuI3AzG4RmHtfUDm7g2rv+/UNhjYzTfPRGh/9PtQe0L+d+k3hn7GK5w +bYgIDZUCoVy+fVXrdHXSy71aBSMS2Q7sUIj0BxzjHF70zWmXRpcwqDzqrXLb3vw1 +zLTRJQ070IYnGPYCNyjJZLmGMZhYQRPIJkvW96GN/m7gdv8CgcAzagU+HkZGWOJh +OGrCj3eiJ7cZOHVA1oyHvGWmJMOX5e9wGyxOnAjKHeuXEXQUu5neIqFuvGzGJZiK +jhf0+U2jHQFeJg606ByqBnxmsYlatIJl0b8gy7JXqK9/AAcAfF7UCWAMCm6o4RYb +BJWxvCs1rYB4CgodX8nMJDpeIANQRLiw8/EX/0G4XVabHPHmK8a6oowYJYzVkPEo +Zim7QD2y+WWZLrcb49DSGteWcMz2dMUuv/XJYMD/OPio2xp9ak4= -----END RSA PRIVATE KEY----- diff --git a/internal/filter/filter.go b/internal/filter/filter.go index bdc6afb..daccdb0 100644 --- a/internal/filter/filter.go +++ b/internal/filter/filter.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2023 German Federal Office for Information Security (BSI) // Software-Engineering: 2023 Intevation GmbH diff --git a/internal/filter/filter_test.go b/internal/filter/filter_test.go index 14bcc59..bc34440 100644 --- a/internal/filter/filter_test.go +++ b/internal/filter/filter_test.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2023 German Federal Office for Information Security (BSI) // Software-Engineering: 2023 Intevation GmbH diff --git a/internal/misc/doc.go b/internal/misc/doc.go index 1fec00a..1101add 100644 --- a/internal/misc/doc.go +++ b/internal/misc/doc.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2023 German Federal Office for Information Security (BSI) // Software-Engineering: 2023 Intevation GmbH diff --git a/internal/misc/json.go b/internal/misc/json.go new file mode 100644 index 0000000..2888302 --- /dev/null +++ b/internal/misc/json.go @@ -0,0 +1,37 @@ +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. +// +// SPDX-License-Identifier: Apache-2.0 +// +// SPDX-FileCopyrightText: 2025 German Federal Office for Information Security (BSI) +// Software-Engineering: 2025 Intevation GmbH + +package misc + +import ( + "encoding/json" + "fmt" + "io" +) + +// StrictJSONParse creates a JSON decoder that decodes an interface +// while not allowing trailing data +func StrictJSONParse(jsonData io.Reader, target any) error { + decoder := json.NewDecoder(jsonData) + // Don't allow unknown fields + decoder.DisallowUnknownFields() + + if err := decoder.Decode(target); err != nil { + return fmt.Errorf("JSON decoding error: %w", err) + } + + // Check for any trailing data after the main JSON structure + if _, err := decoder.Token(); err != io.EOF { + if err != nil { + return fmt.Errorf("error reading trailing data: %w", err) + } + return fmt.Errorf("unexpected trailing data after JSON object") + } + + return nil +} diff --git a/internal/misc/mime.go b/internal/misc/mime.go index 0e699a3..5bb36d0 100644 --- a/internal/misc/mime.go +++ b/internal/misc/mime.go @@ -1,12 +1,12 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2023 German Federal Office for Information Security (BSI) // Software-Engineering: 2023 Intevation GmbH -package misc +package misc //revive:disable-line:var-naming import ( "fmt" diff --git a/internal/misc/mime_test.go b/internal/misc/mime_test.go index bd5a31c..19f9475 100644 --- a/internal/misc/mime_test.go +++ b/internal/misc/mime_test.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2023 German Federal Office for Information Security (BSI) // Software-Engineering: 2023 Intevation GmbH diff --git a/internal/misc/url.go b/internal/misc/url.go new file mode 100644 index 0000000..2256a94 --- /dev/null +++ b/internal/misc/url.go @@ -0,0 +1,21 @@ +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. +// +// SPDX-License-Identifier: Apache-2.0 +// +// SPDX-FileCopyrightText: 2025 German Federal Office for Information Security (BSI) +// Software-Engineering: 2025 Intevation GmbH + +package misc + +import "net/url" + +// JoinURL joins the two URLs while preserving the query and fragment part of the latter. +func JoinURL(baseURL *url.URL, relativeURL *url.URL) *url.URL { + u := baseURL.JoinPath(relativeURL.Path) + u.RawQuery = relativeURL.RawQuery + u.RawFragment = relativeURL.RawFragment + // Enforce https, this is required if the base url was only a domain + u.Scheme = "https" + return u +} diff --git a/internal/models/models.go b/internal/models/models.go index 520cd9c..b743d18 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2023 German Federal Office for Information Security (BSI) // Software-Engineering: 2023 Intevation GmbH diff --git a/internal/models/models_test.go b/internal/models/models_test.go index a40100f..48cd02d 100644 --- a/internal/models/models_test.go +++ b/internal/models/models_test.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2023 German Federal Office for Information Security (BSI) // Software-Engineering: 2023 Intevation GmbH @@ -81,7 +81,7 @@ func TestUnmarshalText(t *testing.T) { byteSlice := []byte{'3', 'h'} var emptySlice []byte if testTimeRange.UnmarshalText(byteSlice) != nil { - t.Errorf(testTimeRange.UnmarshalText(byteSlice).Error()) + t.Error(testTimeRange.UnmarshalText(byteSlice).Error()) } if testTimeRange.UnmarshalText(emptySlice) == nil { t.Errorf("Failure: UnmarshalText succeeded on invalid slice of bytes.") @@ -104,10 +104,10 @@ func TestUnmarshalFlag(t *testing.T) { time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC), time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC)) if err := testTimeRange.UnmarshalFlag("3h"); err != nil { - t.Errorf(err.Error()) + t.Error(err.Error()) } if err := testTimeRange.UnmarshalFlag("2006-01-02T15:04:05"); err != nil { - t.Errorf(err.Error()) + t.Error(err.Error()) } if err := testTimeRange.UnmarshalFlag("2006-01-02T15:04:05a"); err == nil { t.Errorf("Failure: Extracted time from invalid string") @@ -119,7 +119,7 @@ func TestUnmarshalFlag(t *testing.T) { t.Errorf("Failure: Extracted time from invalid string") } if err := testTimeRange.UnmarshalFlag("2006-01-02T15:04:05, 2007-01-02T15:04:05"); err != nil { - t.Errorf(err.Error()) + t.Error(err.Error()) } } diff --git a/internal/options/log.go b/internal/options/log.go index 226072e..bd18c65 100644 --- a/internal/options/log.go +++ b/internal/options/log.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2023 German Federal Office for Information Security (BSI) // Software-Engineering: 2023 Intevation GmbH @@ -9,9 +9,8 @@ package options import ( + "log/slog" "strings" - - "golang.org/x/exp/slog" ) // LogLevel implements a helper type to be used in configurations. diff --git a/internal/options/log_test.go b/internal/options/log_test.go index 2272f0f..a99c598 100644 --- a/internal/options/log_test.go +++ b/internal/options/log_test.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2023 German Federal Office for Information Security (BSI) // Software-Engineering: 2023 Intevation GmbH @@ -9,9 +9,8 @@ package options import ( + "log/slog" "testing" - - "golang.org/x/exp/slog" ) func TestMarshalFlag(t *testing.T) { diff --git a/internal/options/options.go b/internal/options/options.go index 961b4b4..38b5bd4 100644 --- a/internal/options/options.go +++ b/internal/options/options.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2023 German Federal Office for Information Security (BSI) // Software-Engineering: 2023 Intevation GmbH @@ -12,13 +12,14 @@ package options import ( "fmt" "log" + "log/slog" "os" + "github.com/gocsaf/csaf/v3/util" + "github.com/BurntSushi/toml" "github.com/jessevdk/go-flags" "github.com/mitchellh/go-homedir" - - "github.com/csaf-poc/csaf_distribution/v3/util" ) // Parser helps parsing command line arguments and loading @@ -45,7 +46,6 @@ type Parser[C any] struct { // If a config file was specified it is loaded. // Returns the arguments and the configuration. func (p *Parser[C]) Parse() ([]string, *C, error) { - var cmdLineOpts C if p.SetDefaults != nil { p.SetDefaults(&cmdLineOpts) @@ -81,6 +81,7 @@ func (p *Parser[C]) Parse() ([]string, *C, error) { // No config file -> We are good. if path == "" { + slog.Warn("No config file found. Maybe you want to specify one or store it in a respective default location", "locations", p.DefaultConfigLocations) return args, &cmdLineOpts, nil } @@ -147,3 +148,13 @@ func ErrorCheck(err error) { log.Fatalf("error: %v\n", err) } } + +// ErrorCheckStructured checks if err is not nil and terminates the program if +// so. This is similar to [ErrorCheck], but uses [slog] instead of the +// non-structured Go logging. +func ErrorCheckStructured(err error) { + if err != nil { + slog.Error("Error while executing program", "err", err) + os.Exit(1) + } +} diff --git a/internal/options/options_test.go b/internal/options/options_test.go index 3feba93..6e96838 100644 --- a/internal/options/options_test.go +++ b/internal/options/options_test.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2023 German Federal Office for Information Security (BSI) // Software-Engineering: 2023 Intevation GmbH @@ -37,10 +37,10 @@ func TestParse(t *testing.T) { }, Usage: "[OPTIONS] domain...", HasVersion: func(cfg *config) bool { return cfg.Version }, - SetDefaults: func(cfg *config) { + SetDefaults: func(_ *config) { }, // Re-establish default values if not set. - EnsureDefaults: func(cfg *config) { + EnsureDefaults: func(_ *config) { }, } @@ -90,7 +90,7 @@ func TestParse(t *testing.T) { cmd.Env = append(os.Environ(), "TEST_HELP=1") err := cmd.Run() if err != nil { - t.Fatalf(err.Error()) + t.Fatal(err.Error()) } // test the version flag @@ -104,7 +104,7 @@ func TestParse(t *testing.T) { cmd.Env = append(os.Environ(), "TEST_VERSION=1") err = cmd.Run() if err != nil { - t.Fatalf(err.Error()) + t.Fatal(err.Error()) } } @@ -140,7 +140,7 @@ func TestLoadToml(t *testing.T) { t.Errorf("Failure: Succeeded in parsing nonexistant parameter") } if err := loadTOML(&cfg, "data/config.toml"); err != nil { - t.Errorf(err.Error()) + t.Error(err.Error()) } } @@ -157,7 +157,6 @@ func TestErrorCheck(t *testing.T) { return } t.Fatalf("process ran with err %v, want exit status 1", err) - } // TestSecondPassCommandlineParsing checks if the second pass @@ -168,7 +167,7 @@ func TestSecondPassCommandlineParsing(t *testing.T) { os.Args = []string{"cmd"} p := Parser[config]{ - ConfigLocation: func(cfg *config) string { + ConfigLocation: func(_ *config) string { // This is a bit stupid. os.Args = []string{"cmd", "--invalid"} return "data/empty.toml" @@ -188,7 +187,7 @@ func TestSecondPassCommandlineHelp(t *testing.T) { os.Args = []string{"cmd"} p := Parser[config]{ - ConfigLocation: func(cfg *config) string { + ConfigLocation: func(_ *config) string { // This is a bit stupid. os.Args = []string{"cmd", "--help"} return "data/empty.toml" diff --git a/internal/testutil/testutil.go b/internal/testutil/testutil.go new file mode 100644 index 0000000..a8186a4 --- /dev/null +++ b/internal/testutil/testutil.go @@ -0,0 +1,87 @@ +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. +// +// SPDX-License-Identifier: Apache-2.0 +// +// SPDX-FileCopyrightText: 2023 German Federal Office for Information Security (BSI) +// Software-Engineering: 2023 Intevation GmbH + +// Package testutil contains shared helper functions for testing the application. +package testutil + +import ( + "html/template" + "net/http" + "os" + "strings" +) + +// ProviderParams configures the test provider. +type ProviderParams struct { + URL string + EnableSha256 bool + EnableSha512 bool + ForbidSha256 bool + ForbidSha512 bool + JSONContentType string +} + +// ProviderHandler returns a test provider handler with the specified configuration. +func ProviderHandler(params *ProviderParams, directoryProvider bool) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + path := "../../testdata/" + if directoryProvider { + path += "simple-directory-provider" + } else { + path += "simple-rolie-provider" + } + + jsonContenType := "application/json" + if params.JSONContentType != "" { + jsonContenType = params.JSONContentType + } + + path += r.URL.Path + + if strings.HasSuffix(r.URL.Path, "/") { + path += "index.html" + } + + content, err := os.ReadFile(path) + if err != nil { + w.WriteHeader(http.StatusNotFound) + return + } + switch { + case strings.HasSuffix(path, ".html"): + w.Header().Add("Content-Type", "text/html") + case strings.HasSuffix(path, ".json"): + w.Header().Add("Content-Type", jsonContenType) + case (strings.HasSuffix(path, ".sha256")) && params.ForbidSha256: + w.WriteHeader(http.StatusForbidden) + return + case strings.HasSuffix(path, ".sha512") && params.ForbidSha512: + w.WriteHeader(http.StatusForbidden) + return + case strings.HasSuffix(path, ".sha256") && directoryProvider && !params.EnableSha256: + w.WriteHeader(http.StatusNotFound) + return + case strings.HasSuffix(path, ".sha512") && directoryProvider && !params.EnableSha512: + w.WriteHeader(http.StatusNotFound) + return + default: + w.Header().Add("Content-Type", "text/plain") + } + + tmplt, err := template.New("base").Parse(string(content)) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + err = tmplt.Execute(w, params) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + } +} diff --git a/testdata/csaf-documents/trailing-garbage-data/avendor-advisory-0004.json b/testdata/csaf-documents/trailing-garbage-data/avendor-advisory-0004.json new file mode 100644 index 0000000..2131136 --- /dev/null +++ b/testdata/csaf-documents/trailing-garbage-data/avendor-advisory-0004.json @@ -0,0 +1,171 @@ +{ + "document": { + "category": "csaf_vex", + "csaf_version": "2.0", + "distribution": { + "tlp": { + "label": "WHITE", + "url": "https://www.first.org/tlp/v1/" + } + }, + "notes": [ + { + "category": "summary", + "title": "Test document summary", + "text": "Auto generated test CSAF document" + } + ], + "publisher": { + "category": "vendor", + "name": "ACME Inc.", + "namespace": "https://www.example.com" + }, + "title": "Test CSAF document", + "tracking": { + "current_release_date": "2020-01-01T00:00:00Z", + "generator": { + "date": "2020-01-01T00:00:00Z", + "engine": { + "name": "csaf-tool", + "version": "0.3.2" + } + }, + "id": "Avendor-advisory-0004", + "initial_release_date": "2020-01-01T00:00:00Z", + "revision_history": [ + { + "date": "2020-01-01T00:00:00Z", + "number": "1", + "summary": "Initial version" + } + ], + "status": "final", + "version": "1" + } + }, + "product_tree": { + "branches": [ + { + "category": "vendor", + "name": "AVendor", + "branches": [ + { + "category": "product_name", + "name": "product_1", + "branches": [ + { + "category": "product_version", + "name": "1.1", + "product": { + "name": "AVendor product_1 1.1", + "product_id": "CSAFPID_0001" + } + }, + { + "category": "product_version", + "name": "1.2", + "product": { + "name": "AVendor product_1 1.2", + "product_id": "CSAFPID_0002" + } + }, + { + "category": "product_version", + "name": "2.0", + "product": { + "name": "AVendor product_1 2.0", + "product_id": "CSAFPID_0003" + } + } + ] + } + ] + }, + { + "category": "vendor", + "name": "AVendor1", + "branches": [ + { + "category": "product_name", + "name": "product_2", + "branches": [ + { + "category": "product_version", + "name": "1", + "product": { + "name": "AVendor1 product_2 1", + "product_id": "CSAFPID_0004" + } + } + ] + } + ] + }, + { + "category": "vendor", + "name": "AVendor", + "branches": [ + { + "category": "product_name", + "name": "product_3", + "branches": [ + { + "category": "product_version", + "name": "2022H2", + "product": { + "name": "AVendor product_3 2022H2", + "product_id": "CSAFPID_0005" + } + } + ] + } + ] + } + ] + }, + "vulnerabilities": [ + { + "cve": "CVE-2020-1234", + "notes": [ + { + "category": "description", + "title": "CVE description", + "text": "https://nvd.nist.gov/vuln/detail/CVE-2020-1234" + } + ], + "product_status": { + "under_investigation": ["CSAFPID_0001"] + }, + "threats": [ + { + "category": "impact", + "details": "Customers should upgrade to the latest version of the product", + "date": "2020-01-01T00:00:00Z", + "product_ids": ["CSAFPID_0001"] + } + ] + }, + { + "cve": "CVE-2020-9876", + "notes": [ + { + "category": "description", + "title": "CVE description", + "text": "https://nvd.nist.gov/vuln/detail/CVE-2020-9876" + } + ], + "product_status": { + "under_investigation": ["CSAFPID_0001"] + }, + "threats": [ + { + "category": "impact", + "details": "Still under investigation", + "date": "2020-01-01T00:00:00Z", + "product_ids": ["CSAFPID_0001"] + } + ] + } + ] +} +invalid data diff --git a/testdata/csaf-documents/valid/advisory-tracking-generator-no-version.json b/testdata/csaf-documents/valid/advisory-tracking-generator-no-version.json new file mode 100644 index 0000000..47c9907 --- /dev/null +++ b/testdata/csaf-documents/valid/advisory-tracking-generator-no-version.json @@ -0,0 +1,169 @@ +{ + "document": { + "category": "csaf_vex", + "csaf_version": "2.0", + "distribution": { + "tlp": { + "label": "WHITE", + "url": "https://www.first.org/tlp/v1/" + } + }, + "notes": [ + { + "category": "summary", + "title": "Test document summary", + "text": "Auto generated test CSAF document" + } + ], + "publisher": { + "category": "vendor", + "name": "ACME Inc.", + "namespace": "https://www.example.com" + }, + "title": "Test CSAF document", + "tracking": { + "current_release_date": "2020-01-01T00:00:00Z", + "generator": { + "date": "2020-01-01T00:00:00Z", + "engine": { + "name": "csaf-tool" + } + }, + "id": "Avendor-advisory-0004", + "initial_release_date": "2020-01-01T00:00:00Z", + "revision_history": [ + { + "date": "2020-01-01T00:00:00Z", + "number": "1", + "summary": "Initial version" + } + ], + "status": "final", + "version": "1" + } + }, + "product_tree": { + "branches": [ + { + "category": "vendor", + "name": "AVendor", + "branches": [ + { + "category": "product_name", + "name": "product_1", + "branches": [ + { + "category": "product_version", + "name": "1.1", + "product": { + "name": "AVendor product_1 1.1", + "product_id": "CSAFPID_0001" + } + }, + { + "category": "product_version", + "name": "1.2", + "product": { + "name": "AVendor product_1 1.2", + "product_id": "CSAFPID_0002" + } + }, + { + "category": "product_version", + "name": "2.0", + "product": { + "name": "AVendor product_1 2.0", + "product_id": "CSAFPID_0003" + } + } + ] + } + ] + }, + { + "category": "vendor", + "name": "AVendor1", + "branches": [ + { + "category": "product_name", + "name": "product_2", + "branches": [ + { + "category": "product_version", + "name": "1", + "product": { + "name": "AVendor1 product_2 1", + "product_id": "CSAFPID_0004" + } + } + ] + } + ] + }, + { + "category": "vendor", + "name": "AVendor", + "branches": [ + { + "category": "product_name", + "name": "product_3", + "branches": [ + { + "category": "product_version", + "name": "2022H2", + "product": { + "name": "AVendor product_3 2022H2", + "product_id": "CSAFPID_0005" + } + } + ] + } + ] + } + ] + }, + "vulnerabilities": [ + { + "cve": "CVE-2020-1234", + "notes": [ + { + "category": "description", + "title": "CVE description", + "text": "https://nvd.nist.gov/vuln/detail/CVE-2020-1234" + } + ], + "product_status": { + "under_investigation": ["CSAFPID_0001"] + }, + "threats": [ + { + "category": "impact", + "details": "Customers should upgrade to the latest version of the product", + "date": "2020-01-01T00:00:00Z", + "product_ids": ["CSAFPID_0001"] + } + ] + }, + { + "cve": "CVE-2020-9876", + "notes": [ + { + "category": "description", + "title": "CVE description", + "text": "https://nvd.nist.gov/vuln/detail/CVE-2020-9876" + } + ], + "product_status": { + "under_investigation": ["CSAFPID_0001"] + }, + "threats": [ + { + "category": "impact", + "details": "Still under investigation", + "date": "2020-01-01T00:00:00Z", + "product_ids": ["CSAFPID_0001"] + } + ] + } + ] +} diff --git a/testdata/csaf-documents/valid/avendor-advisory-0004.json b/testdata/csaf-documents/valid/avendor-advisory-0004.json new file mode 100644 index 0000000..0e194e9 --- /dev/null +++ b/testdata/csaf-documents/valid/avendor-advisory-0004.json @@ -0,0 +1,170 @@ +{ + "document": { + "category": "csaf_vex", + "csaf_version": "2.0", + "distribution": { + "tlp": { + "label": "WHITE", + "url": "https://www.first.org/tlp/v1/" + } + }, + "notes": [ + { + "category": "summary", + "title": "Test document summary", + "text": "Auto generated test CSAF document" + } + ], + "publisher": { + "category": "vendor", + "name": "ACME Inc.", + "namespace": "https://www.example.com" + }, + "title": "Test CSAF document", + "tracking": { + "current_release_date": "2020-01-01T00:00:00Z", + "generator": { + "date": "2020-01-01T00:00:00Z", + "engine": { + "name": "csaf-tool", + "version": "0.3.2" + } + }, + "id": "Avendor-advisory-0004", + "initial_release_date": "2020-01-01T00:00:00Z", + "revision_history": [ + { + "date": "2020-01-01T00:00:00Z", + "number": "1", + "summary": "Initial version" + } + ], + "status": "final", + "version": "1" + } + }, + "product_tree": { + "branches": [ + { + "category": "vendor", + "name": "AVendor", + "branches": [ + { + "category": "product_name", + "name": "product_1", + "branches": [ + { + "category": "product_version", + "name": "1.1", + "product": { + "name": "AVendor product_1 1.1", + "product_id": "CSAFPID_0001" + } + }, + { + "category": "product_version", + "name": "1.2", + "product": { + "name": "AVendor product_1 1.2", + "product_id": "CSAFPID_0002" + } + }, + { + "category": "product_version", + "name": "2.0", + "product": { + "name": "AVendor product_1 2.0", + "product_id": "CSAFPID_0003" + } + } + ] + } + ] + }, + { + "category": "vendor", + "name": "AVendor1", + "branches": [ + { + "category": "product_name", + "name": "product_2", + "branches": [ + { + "category": "product_version", + "name": "1", + "product": { + "name": "AVendor1 product_2 1", + "product_id": "CSAFPID_0004" + } + } + ] + } + ] + }, + { + "category": "vendor", + "name": "AVendor", + "branches": [ + { + "category": "product_name", + "name": "product_3", + "branches": [ + { + "category": "product_version", + "name": "2022H2", + "product": { + "name": "AVendor product_3 2022H2", + "product_id": "CSAFPID_0005" + } + } + ] + } + ] + } + ] + }, + "vulnerabilities": [ + { + "cve": "CVE-2020-1234", + "notes": [ + { + "category": "description", + "title": "CVE description", + "text": "https://nvd.nist.gov/vuln/detail/CVE-2020-1234" + } + ], + "product_status": { + "under_investigation": ["CSAFPID_0001"] + }, + "threats": [ + { + "category": "impact", + "details": "Customers should upgrade to the latest version of the product", + "date": "2020-01-01T00:00:00Z", + "product_ids": ["CSAFPID_0001"] + } + ] + }, + { + "cve": "CVE-2020-9876", + "notes": [ + { + "category": "description", + "title": "CVE description", + "text": "https://nvd.nist.gov/vuln/detail/CVE-2020-9876" + } + ], + "product_status": { + "under_investigation": ["CSAFPID_0001"] + }, + "threats": [ + { + "category": "impact", + "details": "Still under investigation", + "date": "2020-01-01T00:00:00Z", + "product_ids": ["CSAFPID_0001"] + } + ] + } + ] +} diff --git a/testdata/processor-requirements/directory.json b/testdata/processor-requirements/directory.json new file mode 100644 index 0000000..ed61fcc --- /dev/null +++ b/testdata/processor-requirements/directory.json @@ -0,0 +1,210 @@ +[ + { + "num": 1, + "description": "Valid CSAF documents", + "messages": [ + { + "type": 1, + "text": "No remote validator configured" + }, + { + "type": 0, + "text": "All advisories validated fine against the schema." + } + ] + }, + { + "num": 2, + "description": "Filename", + "messages": [ + { + "type": 0, + "text": "All found filenames are conforming." + } + ] + }, + { + "num": 3, + "description": "TLS", + "messages": [ + { + "type": 0, + "text": "All tested URLs were HTTPS." + } + ] + }, + { + "num": 4, + "description": "TLP:WHITE", + "messages": [ + { + "type": 0, + "text": "All advisories labeled TLP:WHITE were freely accessible." + } + ] + }, + { + "num": 5, + "description": "TLP:AMBER and TLP:RED", + "messages": [ + { + "type": 0, + "text": "No advisories labeled TLP:AMBER or TLP:RED tested for accessibility." + } + ] + }, + { + "num": 6, + "description": "Redirects", + "messages": [ + { + "type": 0, + "text": "No redirections found." + } + ] + }, + { + "num": 7, + "description": "provider-metadata.json", + "messages": [ + { + "type": 0, + "text": "Found good provider metadata." + } + ] + }, + { + "num": 8, + "description": "security.txt", + "messages": [ + { + "type": 0, + "text": "Performed no test of security.txt since the direct url of the provider-metadata.json was used." + } + ] + }, + { + "num": 9, + "description": "/.well-known/csaf/provider-metadata.json", + "messages": [ + { + "type": 0, + "text": "Performed no test on whether the provider-metadata.json is available under the .well-known path since the direct url of the provider-metadata.json was used." + } + ] + }, + { + "num": 10, + "description": "DNS path", + "messages": [ + { + "type": 0, + "text": "Performed no test on the contents of https://csaf.data.security.DOMAIN since the direct url of the provider-metadata.json was used." + } + ] + }, + { + "num": 11, + "description": "One folder per year", + "messages": [ + { + "type": 2, + "text": "No year folder found in {{.URL}}/white/avendor-advisory-0004.json" + } + ] + }, + { + "num": 12, + "description": "index.txt", + "messages": [ + { + "type": 0, + "text": "Found {{.URL}}/white/index.txt" + } + ] + }, + { + "num": 13, + "description": "changes.csv", + "messages": [ + { + "type": 0, + "text": "Found {{.URL}}/white/changes.csv" + } + ] + }, + { + "num": 14, + "description": "Directory listings", + "messages": [ + { + "type": 0, + "text": "All directory listings are valid." + } + ] + }, + { + "num": 15, + "description": "ROLIE feed", + "messages": [ + { + "type": 2, + "text": "ROLIE feed based distribution was not used." + } + ] + }, + { + "num": 16, + "description": "ROLIE service document", + "messages": [ + { + "type": 1, + "text": "No ROLIE service document found." + } + ] + }, + { + "num": 17, + "description": "ROLIE category document", + "messages": [ + { + "type": 1, + "text": "No ROLIE category document found." + } + ] + }, + { + "num": 18, + "description": "Integrity", + "messages": [ + { + "type": 0, + "text": "Fetching {{.URL}}/white/avendor-advisory-0004.json.sha256 failed: Status code 404 (404 Not Found)" + }, + { + "type": 0, + "text": "Fetching {{.URL}}/white/avendor-advisory-0004.json.sha512 failed: Status code 404 (404 Not Found)" + } + ] + }, + { + "num": 19, + "description": "Signatures", + "messages": [ + { + "type": 0, + "text": "All signatures verified." + } + ] + }, + { + "num": 20, + "description": "Public OpenPGP Key", + "messages": [ + { + "type": 0, + "text": "1 public OpenPGP key(s) loaded." + } + ] + } +] \ No newline at end of file diff --git a/testdata/processor-requirements/rolie.json b/testdata/processor-requirements/rolie.json new file mode 100644 index 0000000..cd65a7e --- /dev/null +++ b/testdata/processor-requirements/rolie.json @@ -0,0 +1,210 @@ +[ + { + "num": 1, + "description": "Valid CSAF documents", + "messages": [ + { + "type": 1, + "text": "No remote validator configured" + }, + { + "type": 0, + "text": "All advisories validated fine against the schema." + } + ] + }, + { + "num": 2, + "description": "Filename", + "messages": [ + { + "type": 0, + "text": "All found filenames are conforming." + } + ] + }, + { + "num": 3, + "description": "TLS", + "messages": [ + { + "type": 0, + "text": "All tested URLs were HTTPS." + } + ] + }, + { + "num": 4, + "description": "TLP:WHITE", + "messages": [ + { + "type": 0, + "text": "All advisories labeled TLP:WHITE were freely accessible." + } + ] + }, + { + "num": 5, + "description": "TLP:AMBER and TLP:RED", + "messages": [ + { + "type": 0, + "text": "No advisories labeled TLP:AMBER or TLP:RED tested for accessibility." + } + ] + }, + { + "num": 6, + "description": "Redirects", + "messages": [ + { + "type": 0, + "text": "No redirections found." + } + ] + }, + { + "num": 7, + "description": "provider-metadata.json", + "messages": [ + { + "type": 0, + "text": "Found good provider metadata." + } + ] + }, + { + "num": 8, + "description": "security.txt", + "messages": [ + { + "type": 0, + "text": "Performed no test of security.txt since the direct url of the provider-metadata.json was used." + } + ] + }, + { + "num": 9, + "description": "/.well-known/csaf/provider-metadata.json", + "messages": [ + { + "type": 0, + "text": "Performed no test on whether the provider-metadata.json is available under the .well-known path since the direct url of the provider-metadata.json was used." + } + ] + }, + { + "num": 10, + "description": "DNS path", + "messages": [ + { + "type": 0, + "text": "Performed no test on the contents of https://csaf.data.security.DOMAIN since the direct url of the provider-metadata.json was used." + } + ] + }, + { + "num": 11, + "description": "One folder per year", + "messages": [ + { + "type": 2, + "text": "No year folder found in {{.URL}}/white/avendor-advisory-0004.json" + } + ] + }, + { + "num": 12, + "description": "index.txt", + "messages": [ + { + "type": 2, + "text": "Fetching index.txt failed: {{.URL}}/index.txt not found." + } + ] + }, + { + "num": 13, + "description": "changes.csv", + "messages": [ + { + "type": 2, + "text": "Fetching changes.csv failed: {{.URL}}/changes.csv not found." + } + ] + }, + { + "num": 14, + "description": "Directory listings", + "messages": [ + { + "type": 2, + "text": "Fetching {{.URL}}/white/ failed. Status code 404 (404 Not Found)" + }, + { + "type": 2, + "text": "Not listed advisories: {{.URL}}/white/avendor-advisory-0004.json" + } + ] + }, + { + "num": 15, + "description": "ROLIE feed", + "messages": [ + { + "type": 2, + "text": "No hash listed on ROLIE feed {{.URL}}/white/avendor-advisory-0004.json" + } + ] + }, + { + "num": 16, + "description": "ROLIE service document", + "messages": [ + { + "type": 0, + "text": "ROLIE service document validated fine." + } + ] + }, + { + "num": 17, + "description": "ROLIE category document", + "messages": [ + { + "type": 1, + "text": "Fetching {{.URL}}/white/category-white.json failed. Status code 404 (404 Not Found)" + } + ] + }, + { + "num": 18, + "description": "Integrity", + "messages": [ + { + "type": 0, + "text": "All checksums match." + } + ] + }, + { + "num": 19, + "description": "Signatures", + "messages": [ + { + "type": 0, + "text": "All signatures verified." + } + ] + }, + { + "num": 20, + "description": "Public OpenPGP Key", + "messages": [ + { + "type": 0, + "text": "1 public OpenPGP key(s) loaded." + } + ] + } +] diff --git a/testdata/processor-requirements/sha256-directory.json b/testdata/processor-requirements/sha256-directory.json new file mode 100644 index 0000000..46b4049 --- /dev/null +++ b/testdata/processor-requirements/sha256-directory.json @@ -0,0 +1,206 @@ +[ + { + "num": 1, + "description": "Valid CSAF documents", + "messages": [ + { + "type": 1, + "text": "No remote validator configured" + }, + { + "type": 0, + "text": "All advisories validated fine against the schema." + } + ] + }, + { + "num": 2, + "description": "Filename", + "messages": [ + { + "type": 0, + "text": "All found filenames are conforming." + } + ] + }, + { + "num": 3, + "description": "TLS", + "messages": [ + { + "type": 0, + "text": "All tested URLs were HTTPS." + } + ] + }, + { + "num": 4, + "description": "TLP:WHITE", + "messages": [ + { + "type": 0, + "text": "All advisories labeled TLP:WHITE were freely accessible." + } + ] + }, + { + "num": 5, + "description": "TLP:AMBER and TLP:RED", + "messages": [ + { + "type": 0, + "text": "No advisories labeled TLP:AMBER or TLP:RED tested for accessibility." + } + ] + }, + { + "num": 6, + "description": "Redirects", + "messages": [ + { + "type": 0, + "text": "No redirections found." + } + ] + }, + { + "num": 7, + "description": "provider-metadata.json", + "messages": [ + { + "type": 0, + "text": "Found good provider metadata." + } + ] + }, + { + "num": 8, + "description": "security.txt", + "messages": [ + { + "type": 0, + "text": "Performed no test of security.txt since the direct url of the provider-metadata.json was used." + } + ] + }, + { + "num": 9, + "description": "/.well-known/csaf/provider-metadata.json", + "messages": [ + { + "type": 0, + "text": "Performed no test on whether the provider-metadata.json is available under the .well-known path since the direct url of the provider-metadata.json was used." + } + ] + }, + { + "num": 10, + "description": "DNS path", + "messages": [ + { + "type": 0, + "text": "Performed no test on the contents of https://csaf.data.security.DOMAIN since the direct url of the provider-metadata.json was used." + } + ] + }, + { + "num": 11, + "description": "One folder per year", + "messages": [ + { + "type": 2, + "text": "No year folder found in {{.URL}}/white/avendor-advisory-0004.json" + } + ] + }, + { + "num": 12, + "description": "index.txt", + "messages": [ + { + "type": 0, + "text": "Found {{.URL}}/white/index.txt" + } + ] + }, + { + "num": 13, + "description": "changes.csv", + "messages": [ + { + "type": 0, + "text": "Found {{.URL}}/white/changes.csv" + } + ] + }, + { + "num": 14, + "description": "Directory listings", + "messages": [ + { + "type": 0, + "text": "All directory listings are valid." + } + ] + }, + { + "num": 15, + "description": "ROLIE feed", + "messages": [ + { + "type": 2, + "text": "ROLIE feed based distribution was not used." + } + ] + }, + { + "num": 16, + "description": "ROLIE service document", + "messages": [ + { + "type": 1, + "text": "No ROLIE service document found." + } + ] + }, + { + "num": 17, + "description": "ROLIE category document", + "messages": [ + { + "type": 1, + "text": "No ROLIE category document found." + } + ] + }, + { + "num": 18, + "description": "Integrity", + "messages": [ + { + "type": 0, + "text": "Fetching {{.URL}}/white/avendor-advisory-0004.json.sha512 failed: Status code 404 (404 Not Found)" + } + ] + }, + { + "num": 19, + "description": "Signatures", + "messages": [ + { + "type": 0, + "text": "All signatures verified." + } + ] + }, + { + "num": 20, + "description": "Public OpenPGP Key", + "messages": [ + { + "type": 0, + "text": "1 public OpenPGP key(s) loaded." + } + ] + } +] \ No newline at end of file diff --git a/testdata/processor-requirements/sha256-rolie.json b/testdata/processor-requirements/sha256-rolie.json new file mode 100644 index 0000000..4ed47f1 --- /dev/null +++ b/testdata/processor-requirements/sha256-rolie.json @@ -0,0 +1,210 @@ +[ + { + "num": 1, + "description": "Valid CSAF documents", + "messages": [ + { + "type": 1, + "text": "No remote validator configured" + }, + { + "type": 0, + "text": "All advisories validated fine against the schema." + } + ] + }, + { + "num": 2, + "description": "Filename", + "messages": [ + { + "type": 0, + "text": "All found filenames are conforming." + } + ] + }, + { + "num": 3, + "description": "TLS", + "messages": [ + { + "type": 0, + "text": "All tested URLs were HTTPS." + } + ] + }, + { + "num": 4, + "description": "TLP:WHITE", + "messages": [ + { + "type": 0, + "text": "All advisories labeled TLP:WHITE were freely accessible." + } + ] + }, + { + "num": 5, + "description": "TLP:AMBER and TLP:RED", + "messages": [ + { + "type": 0, + "text": "No advisories labeled TLP:AMBER or TLP:RED tested for accessibility." + } + ] + }, + { + "num": 6, + "description": "Redirects", + "messages": [ + { + "type": 0, + "text": "No redirections found." + } + ] + }, + { + "num": 7, + "description": "provider-metadata.json", + "messages": [ + { + "type": 0, + "text": "Found good provider metadata." + } + ] + }, + { + "num": 8, + "description": "security.txt", + "messages": [ + { + "type": 0, + "text": "Performed no test of security.txt since the direct url of the provider-metadata.json was used." + } + ] + }, + { + "num": 9, + "description": "/.well-known/csaf/provider-metadata.json", + "messages": [ + { + "type": 0, + "text": "Performed no test on whether the provider-metadata.json is available under the .well-known path since the direct url of the provider-metadata.json was used." + } + ] + }, + { + "num": 10, + "description": "DNS path", + "messages": [ + { + "type": 0, + "text": "Performed no test on the contents of https://csaf.data.security.DOMAIN since the direct url of the provider-metadata.json was used." + } + ] + }, + { + "num": 11, + "description": "One folder per year", + "messages": [ + { + "type": 2, + "text": "No year folder found in {{.URL}}/white/avendor-advisory-0004.json" + } + ] + }, + { + "num": 12, + "description": "index.txt", + "messages": [ + { + "type": 2, + "text": "Fetching index.txt failed: {{.URL}}/index.txt not found." + } + ] + }, + { + "num": 13, + "description": "changes.csv", + "messages": [ + { + "type": 2, + "text": "Fetching changes.csv failed: {{.URL}}/changes.csv not found." + } + ] + }, + { + "num": 14, + "description": "Directory listings", + "messages": [ + { + "type": 2, + "text": "Fetching {{.URL}}/white/ failed. Status code 404 (404 Not Found)" + }, + { + "type": 2, + "text": "Not listed advisories: {{.URL}}/white/avendor-advisory-0004.json" + } + ] + }, + { + "num": 15, + "description": "ROLIE feed", + "messages": [ + { + "type": 0, + "text": "{{.URL}}/white/avendor-advisory-0004.json has no sha512 hash file listed" + } + ] + }, + { + "num": 16, + "description": "ROLIE service document", + "messages": [ + { + "type": 0, + "text": "ROLIE service document validated fine." + } + ] + }, + { + "num": 17, + "description": "ROLIE category document", + "messages": [ + { + "type": 1, + "text": "Fetching {{.URL}}/white/category-white.json failed. Status code 404 (404 Not Found)" + } + ] + }, + { + "num": 18, + "description": "Integrity", + "messages": [ + { + "type": 0, + "text": "All checksums match." + } + ] + }, + { + "num": 19, + "description": "Signatures", + "messages": [ + { + "type": 0, + "text": "All signatures verified." + } + ] + }, + { + "num": 20, + "description": "Public OpenPGP Key", + "messages": [ + { + "type": 0, + "text": "1 public OpenPGP key(s) loaded." + } + ] + } +] diff --git a/testdata/processor-requirements/sha256-sha512-directory.json b/testdata/processor-requirements/sha256-sha512-directory.json new file mode 100644 index 0000000..3e30b9a --- /dev/null +++ b/testdata/processor-requirements/sha256-sha512-directory.json @@ -0,0 +1,206 @@ +[ + { + "num": 1, + "description": "Valid CSAF documents", + "messages": [ + { + "type": 1, + "text": "No remote validator configured" + }, + { + "type": 0, + "text": "All advisories validated fine against the schema." + } + ] + }, + { + "num": 2, + "description": "Filename", + "messages": [ + { + "type": 0, + "text": "All found filenames are conforming." + } + ] + }, + { + "num": 3, + "description": "TLS", + "messages": [ + { + "type": 0, + "text": "All tested URLs were HTTPS." + } + ] + }, + { + "num": 4, + "description": "TLP:WHITE", + "messages": [ + { + "type": 0, + "text": "All advisories labeled TLP:WHITE were freely accessible." + } + ] + }, + { + "num": 5, + "description": "TLP:AMBER and TLP:RED", + "messages": [ + { + "type": 0, + "text": "No advisories labeled TLP:AMBER or TLP:RED tested for accessibility." + } + ] + }, + { + "num": 6, + "description": "Redirects", + "messages": [ + { + "type": 0, + "text": "No redirections found." + } + ] + }, + { + "num": 7, + "description": "provider-metadata.json", + "messages": [ + { + "type": 0, + "text": "Found good provider metadata." + } + ] + }, + { + "num": 8, + "description": "security.txt", + "messages": [ + { + "type": 0, + "text": "Performed no test of security.txt since the direct url of the provider-metadata.json was used." + } + ] + }, + { + "num": 9, + "description": "/.well-known/csaf/provider-metadata.json", + "messages": [ + { + "type": 0, + "text": "Performed no test on whether the provider-metadata.json is available under the .well-known path since the direct url of the provider-metadata.json was used." + } + ] + }, + { + "num": 10, + "description": "DNS path", + "messages": [ + { + "type": 0, + "text": "Performed no test on the contents of https://csaf.data.security.DOMAIN since the direct url of the provider-metadata.json was used." + } + ] + }, + { + "num": 11, + "description": "One folder per year", + "messages": [ + { + "type": 2, + "text": "No year folder found in {{.URL}}/white/avendor-advisory-0004.json" + } + ] + }, + { + "num": 12, + "description": "index.txt", + "messages": [ + { + "type": 0, + "text": "Found {{.URL}}/white/index.txt" + } + ] + }, + { + "num": 13, + "description": "changes.csv", + "messages": [ + { + "type": 0, + "text": "Found {{.URL}}/white/changes.csv" + } + ] + }, + { + "num": 14, + "description": "Directory listings", + "messages": [ + { + "type": 0, + "text": "All directory listings are valid." + } + ] + }, + { + "num": 15, + "description": "ROLIE feed", + "messages": [ + { + "type": 2, + "text": "ROLIE feed based distribution was not used." + } + ] + }, + { + "num": 16, + "description": "ROLIE service document", + "messages": [ + { + "type": 1, + "text": "No ROLIE service document found." + } + ] + }, + { + "num": 17, + "description": "ROLIE category document", + "messages": [ + { + "type": 1, + "text": "No ROLIE category document found." + } + ] + }, + { + "num": 18, + "description": "Integrity", + "messages": [ + { + "type": 0, + "text": "All checksums match." + } + ] + }, + { + "num": 19, + "description": "Signatures", + "messages": [ + { + "type": 0, + "text": "All signatures verified." + } + ] + }, + { + "num": 20, + "description": "Public OpenPGP Key", + "messages": [ + { + "type": 0, + "text": "1 public OpenPGP key(s) loaded." + } + ] + } +] diff --git a/testdata/processor-requirements/sha256-sha512-forbid-sha256-forbid-sha512-rolie.json b/testdata/processor-requirements/sha256-sha512-forbid-sha256-forbid-sha512-rolie.json new file mode 100644 index 0000000..03359f0 --- /dev/null +++ b/testdata/processor-requirements/sha256-sha512-forbid-sha256-forbid-sha512-rolie.json @@ -0,0 +1,214 @@ +[ + { + "num": 1, + "description": "Valid CSAF documents", + "messages": [ + { + "type": 1, + "text": "No remote validator configured" + }, + { + "type": 0, + "text": "All advisories validated fine against the schema." + } + ] + }, + { + "num": 2, + "description": "Filename", + "messages": [ + { + "type": 0, + "text": "All found filenames are conforming." + } + ] + }, + { + "num": 3, + "description": "TLS", + "messages": [ + { + "type": 0, + "text": "All tested URLs were HTTPS." + } + ] + }, + { + "num": 4, + "description": "TLP:WHITE", + "messages": [ + { + "type": 0, + "text": "All advisories labeled TLP:WHITE were freely accessible." + } + ] + }, + { + "num": 5, + "description": "TLP:AMBER and TLP:RED", + "messages": [ + { + "type": 0, + "text": "No advisories labeled TLP:AMBER or TLP:RED tested for accessibility." + } + ] + }, + { + "num": 6, + "description": "Redirects", + "messages": [ + { + "type": 0, + "text": "No redirections found." + } + ] + }, + { + "num": 7, + "description": "provider-metadata.json", + "messages": [ + { + "type": 0, + "text": "Found good provider metadata." + } + ] + }, + { + "num": 8, + "description": "security.txt", + "messages": [ + { + "type": 0, + "text": "Performed no test of security.txt since the direct url of the provider-metadata.json was used." + } + ] + }, + { + "num": 9, + "description": "/.well-known/csaf/provider-metadata.json", + "messages": [ + { + "type": 0, + "text": "Performed no test on whether the provider-metadata.json is available under the .well-known path since the direct url of the provider-metadata.json was used." + } + ] + }, + { + "num": 10, + "description": "DNS path", + "messages": [ + { + "type": 0, + "text": "Performed no test on the contents of https://csaf.data.security.DOMAIN since the direct url of the provider-metadata.json was used." + } + ] + }, + { + "num": 11, + "description": "One folder per year", + "messages": [ + { + "type": 2, + "text": "No year folder found in {{.URL}}/white/avendor-advisory-0004.json" + } + ] + }, + { + "num": 12, + "description": "index.txt", + "messages": [ + { + "type": 2, + "text": "Fetching index.txt failed: {{.URL}}/index.txt not found." + } + ] + }, + { + "num": 13, + "description": "changes.csv", + "messages": [ + { + "type": 2, + "text": "Fetching changes.csv failed: {{.URL}}/changes.csv not found." + } + ] + }, + { + "num": 14, + "description": "Directory listings", + "messages": [ + { + "type": 2, + "text": "Fetching {{.URL}}/white/ failed. Status code 404 (404 Not Found)" + }, + { + "type": 2, + "text": "Not listed advisories: {{.URL}}/white/avendor-advisory-0004.json" + } + ] + }, + { + "num": 15, + "description": "ROLIE feed", + "messages": [ + { + "type": 0, + "text": "All checked ROLIE feeds validated fine." + } + ] + }, + { + "num": 16, + "description": "ROLIE service document", + "messages": [ + { + "type": 0, + "text": "ROLIE service document validated fine." + } + ] + }, + { + "num": 17, + "description": "ROLIE category document", + "messages": [ + { + "type": 1, + "text": "Fetching {{.URL}}/white/category-white.json failed. Status code 404 (404 Not Found)" + } + ] + }, + { + "num": 18, + "description": "Integrity", + "messages": [ + { + "type": 2, + "text": "Fetching {{.URL}}/white/avendor-advisory-0004.json.sha256 failed: Status code 403 (403 Forbidden)" + }, + { + "type": 2, + "text": "Fetching {{.URL}}/white/avendor-advisory-0004.json.sha512 failed: Status code 403 (403 Forbidden)" + } + ] + }, + { + "num": 19, + "description": "Signatures", + "messages": [ + { + "type": 0, + "text": "All signatures verified." + } + ] + }, + { + "num": 20, + "description": "Public OpenPGP Key", + "messages": [ + { + "type": 0, + "text": "1 public OpenPGP key(s) loaded." + } + ] + } +] diff --git a/testdata/processor-requirements/sha256-sha512-forbid-sha256-rolie.json b/testdata/processor-requirements/sha256-sha512-forbid-sha256-rolie.json new file mode 100644 index 0000000..72a173a --- /dev/null +++ b/testdata/processor-requirements/sha256-sha512-forbid-sha256-rolie.json @@ -0,0 +1,210 @@ +[ + { + "num": 1, + "description": "Valid CSAF documents", + "messages": [ + { + "type": 1, + "text": "No remote validator configured" + }, + { + "type": 0, + "text": "All advisories validated fine against the schema." + } + ] + }, + { + "num": 2, + "description": "Filename", + "messages": [ + { + "type": 0, + "text": "All found filenames are conforming." + } + ] + }, + { + "num": 3, + "description": "TLS", + "messages": [ + { + "type": 0, + "text": "All tested URLs were HTTPS." + } + ] + }, + { + "num": 4, + "description": "TLP:WHITE", + "messages": [ + { + "type": 0, + "text": "All advisories labeled TLP:WHITE were freely accessible." + } + ] + }, + { + "num": 5, + "description": "TLP:AMBER and TLP:RED", + "messages": [ + { + "type": 0, + "text": "No advisories labeled TLP:AMBER or TLP:RED tested for accessibility." + } + ] + }, + { + "num": 6, + "description": "Redirects", + "messages": [ + { + "type": 0, + "text": "No redirections found." + } + ] + }, + { + "num": 7, + "description": "provider-metadata.json", + "messages": [ + { + "type": 0, + "text": "Found good provider metadata." + } + ] + }, + { + "num": 8, + "description": "security.txt", + "messages": [ + { + "type": 0, + "text": "Performed no test of security.txt since the direct url of the provider-metadata.json was used." + } + ] + }, + { + "num": 9, + "description": "/.well-known/csaf/provider-metadata.json", + "messages": [ + { + "type": 0, + "text": "Performed no test on whether the provider-metadata.json is available under the .well-known path since the direct url of the provider-metadata.json was used." + } + ] + }, + { + "num": 10, + "description": "DNS path", + "messages": [ + { + "type": 0, + "text": "Performed no test on the contents of https://csaf.data.security.DOMAIN since the direct url of the provider-metadata.json was used." + } + ] + }, + { + "num": 11, + "description": "One folder per year", + "messages": [ + { + "type": 2, + "text": "No year folder found in {{.URL}}/white/avendor-advisory-0004.json" + } + ] + }, + { + "num": 12, + "description": "index.txt", + "messages": [ + { + "type": 2, + "text": "Fetching index.txt failed: {{.URL}}/index.txt not found." + } + ] + }, + { + "num": 13, + "description": "changes.csv", + "messages": [ + { + "type": 2, + "text": "Fetching changes.csv failed: {{.URL}}/changes.csv not found." + } + ] + }, + { + "num": 14, + "description": "Directory listings", + "messages": [ + { + "type": 2, + "text": "Fetching {{.URL}}/white/ failed. Status code 404 (404 Not Found)" + }, + { + "type": 2, + "text": "Not listed advisories: {{.URL}}/white/avendor-advisory-0004.json" + } + ] + }, + { + "num": 15, + "description": "ROLIE feed", + "messages": [ + { + "type": 0, + "text": "All checked ROLIE feeds validated fine." + } + ] + }, + { + "num": 16, + "description": "ROLIE service document", + "messages": [ + { + "type": 0, + "text": "ROLIE service document validated fine." + } + ] + }, + { + "num": 17, + "description": "ROLIE category document", + "messages": [ + { + "type": 1, + "text": "Fetching {{.URL}}/white/category-white.json failed. Status code 404 (404 Not Found)" + } + ] + }, + { + "num": 18, + "description": "Integrity", + "messages": [ + { + "type": 1, + "text": "Fetching {{.URL}}/white/avendor-advisory-0004.json.sha256 failed: Status code 403 (403 Forbidden)" + } + ] + }, + { + "num": 19, + "description": "Signatures", + "messages": [ + { + "type": 0, + "text": "All signatures verified." + } + ] + }, + { + "num": 20, + "description": "Public OpenPGP Key", + "messages": [ + { + "type": 0, + "text": "1 public OpenPGP key(s) loaded." + } + ] + } +] diff --git a/testdata/processor-requirements/sha256-sha512-forbid-sha512-rolie.json b/testdata/processor-requirements/sha256-sha512-forbid-sha512-rolie.json new file mode 100644 index 0000000..1ab8f1e --- /dev/null +++ b/testdata/processor-requirements/sha256-sha512-forbid-sha512-rolie.json @@ -0,0 +1,210 @@ +[ + { + "num": 1, + "description": "Valid CSAF documents", + "messages": [ + { + "type": 1, + "text": "No remote validator configured" + }, + { + "type": 0, + "text": "All advisories validated fine against the schema." + } + ] + }, + { + "num": 2, + "description": "Filename", + "messages": [ + { + "type": 0, + "text": "All found filenames are conforming." + } + ] + }, + { + "num": 3, + "description": "TLS", + "messages": [ + { + "type": 0, + "text": "All tested URLs were HTTPS." + } + ] + }, + { + "num": 4, + "description": "TLP:WHITE", + "messages": [ + { + "type": 0, + "text": "All advisories labeled TLP:WHITE were freely accessible." + } + ] + }, + { + "num": 5, + "description": "TLP:AMBER and TLP:RED", + "messages": [ + { + "type": 0, + "text": "No advisories labeled TLP:AMBER or TLP:RED tested for accessibility." + } + ] + }, + { + "num": 6, + "description": "Redirects", + "messages": [ + { + "type": 0, + "text": "No redirections found." + } + ] + }, + { + "num": 7, + "description": "provider-metadata.json", + "messages": [ + { + "type": 0, + "text": "Found good provider metadata." + } + ] + }, + { + "num": 8, + "description": "security.txt", + "messages": [ + { + "type": 0, + "text": "Performed no test of security.txt since the direct url of the provider-metadata.json was used." + } + ] + }, + { + "num": 9, + "description": "/.well-known/csaf/provider-metadata.json", + "messages": [ + { + "type": 0, + "text": "Performed no test on whether the provider-metadata.json is available under the .well-known path since the direct url of the provider-metadata.json was used." + } + ] + }, + { + "num": 10, + "description": "DNS path", + "messages": [ + { + "type": 0, + "text": "Performed no test on the contents of https://csaf.data.security.DOMAIN since the direct url of the provider-metadata.json was used." + } + ] + }, + { + "num": 11, + "description": "One folder per year", + "messages": [ + { + "type": 2, + "text": "No year folder found in {{.URL}}/white/avendor-advisory-0004.json" + } + ] + }, + { + "num": 12, + "description": "index.txt", + "messages": [ + { + "type": 2, + "text": "Fetching index.txt failed: {{.URL}}/index.txt not found." + } + ] + }, + { + "num": 13, + "description": "changes.csv", + "messages": [ + { + "type": 2, + "text": "Fetching changes.csv failed: {{.URL}}/changes.csv not found." + } + ] + }, + { + "num": 14, + "description": "Directory listings", + "messages": [ + { + "type": 2, + "text": "Fetching {{.URL}}/white/ failed. Status code 404 (404 Not Found)" + }, + { + "type": 2, + "text": "Not listed advisories: {{.URL}}/white/avendor-advisory-0004.json" + } + ] + }, + { + "num": 15, + "description": "ROLIE feed", + "messages": [ + { + "type": 0, + "text": "All checked ROLIE feeds validated fine." + } + ] + }, + { + "num": 16, + "description": "ROLIE service document", + "messages": [ + { + "type": 0, + "text": "ROLIE service document validated fine." + } + ] + }, + { + "num": 17, + "description": "ROLIE category document", + "messages": [ + { + "type": 1, + "text": "Fetching {{.URL}}/white/category-white.json failed. Status code 404 (404 Not Found)" + } + ] + }, + { + "num": 18, + "description": "Integrity", + "messages": [ + { + "type": 1, + "text": "Fetching {{.URL}}/white/avendor-advisory-0004.json.sha512 failed: Status code 403 (403 Forbidden)" + } + ] + }, + { + "num": 19, + "description": "Signatures", + "messages": [ + { + "type": 0, + "text": "All signatures verified." + } + ] + }, + { + "num": 20, + "description": "Public OpenPGP Key", + "messages": [ + { + "type": 0, + "text": "1 public OpenPGP key(s) loaded." + } + ] + } +] diff --git a/testdata/processor-requirements/sha256-sha512-rolie.json b/testdata/processor-requirements/sha256-sha512-rolie.json new file mode 100644 index 0000000..5875174 --- /dev/null +++ b/testdata/processor-requirements/sha256-sha512-rolie.json @@ -0,0 +1,210 @@ +[ + { + "num": 1, + "description": "Valid CSAF documents", + "messages": [ + { + "type": 1, + "text": "No remote validator configured" + }, + { + "type": 0, + "text": "All advisories validated fine against the schema." + } + ] + }, + { + "num": 2, + "description": "Filename", + "messages": [ + { + "type": 0, + "text": "All found filenames are conforming." + } + ] + }, + { + "num": 3, + "description": "TLS", + "messages": [ + { + "type": 0, + "text": "All tested URLs were HTTPS." + } + ] + }, + { + "num": 4, + "description": "TLP:WHITE", + "messages": [ + { + "type": 0, + "text": "All advisories labeled TLP:WHITE were freely accessible." + } + ] + }, + { + "num": 5, + "description": "TLP:AMBER and TLP:RED", + "messages": [ + { + "type": 0, + "text": "No advisories labeled TLP:AMBER or TLP:RED tested for accessibility." + } + ] + }, + { + "num": 6, + "description": "Redirects", + "messages": [ + { + "type": 0, + "text": "No redirections found." + } + ] + }, + { + "num": 7, + "description": "provider-metadata.json", + "messages": [ + { + "type": 0, + "text": "Found good provider metadata." + } + ] + }, + { + "num": 8, + "description": "security.txt", + "messages": [ + { + "type": 0, + "text": "Performed no test of security.txt since the direct url of the provider-metadata.json was used." + } + ] + }, + { + "num": 9, + "description": "/.well-known/csaf/provider-metadata.json", + "messages": [ + { + "type": 0, + "text": "Performed no test on whether the provider-metadata.json is available under the .well-known path since the direct url of the provider-metadata.json was used." + } + ] + }, + { + "num": 10, + "description": "DNS path", + "messages": [ + { + "type": 0, + "text": "Performed no test on the contents of https://csaf.data.security.DOMAIN since the direct url of the provider-metadata.json was used." + } + ] + }, + { + "num": 11, + "description": "One folder per year", + "messages": [ + { + "type": 2, + "text": "No year folder found in {{.URL}}/white/avendor-advisory-0004.json" + } + ] + }, + { + "num": 12, + "description": "index.txt", + "messages": [ + { + "type": 2, + "text": "Fetching index.txt failed: {{.URL}}/index.txt not found." + } + ] + }, + { + "num": 13, + "description": "changes.csv", + "messages": [ + { + "type": 2, + "text": "Fetching changes.csv failed: {{.URL}}/changes.csv not found." + } + ] + }, + { + "num": 14, + "description": "Directory listings", + "messages": [ + { + "type": 2, + "text": "Fetching {{.URL}}/white/ failed. Status code 404 (404 Not Found)" + }, + { + "type": 2, + "text": "Not listed advisories: {{.URL}}/white/avendor-advisory-0004.json" + } + ] + }, + { + "num": 15, + "description": "ROLIE feed", + "messages": [ + { + "type": 0, + "text": "All checked ROLIE feeds validated fine." + } + ] + }, + { + "num": 16, + "description": "ROLIE service document", + "messages": [ + { + "type": 0, + "text": "ROLIE service document validated fine." + } + ] + }, + { + "num": 17, + "description": "ROLIE category document", + "messages": [ + { + "type": 1, + "text": "Fetching {{.URL}}/white/category-white.json failed. Status code 404 (404 Not Found)" + } + ] + }, + { + "num": 18, + "description": "Integrity", + "messages": [ + { + "type": 0, + "text": "All checksums match." + } + ] + }, + { + "num": 19, + "description": "Signatures", + "messages": [ + { + "type": 0, + "text": "All signatures verified." + } + ] + }, + { + "num": 20, + "description": "Public OpenPGP Key", + "messages": [ + { + "type": 0, + "text": "1 public OpenPGP key(s) loaded." + } + ] + } +] diff --git a/testdata/processor-requirements/sha512-directory.json b/testdata/processor-requirements/sha512-directory.json new file mode 100644 index 0000000..5102fab --- /dev/null +++ b/testdata/processor-requirements/sha512-directory.json @@ -0,0 +1,207 @@ +[ + { + "num": 1, + "description": "Valid CSAF documents", + "messages": [ + { + "type": 1, + "text": "No remote validator configured" + }, + { + "type": 0, + "text": "All advisories validated fine against the schema." + } + ] + }, + { + "num": 2, + "description": "Filename", + "messages": [ + { + "type": 0, + "text": "All found filenames are conforming." + } + ] + }, + { + "num": 3, + "description": "TLS", + "messages": [ + { + "type": 0, + "text": "All tested URLs were HTTPS." + } + ] + }, + { + "num": 4, + "description": "TLP:WHITE", + "messages": [ + { + "type": 0, + "text": "All advisories labeled TLP:WHITE were freely accessible." + } + ] + }, + { + "num": 5, + "description": "TLP:AMBER and TLP:RED", + "messages": [ + { + "type": 0, + "text": "No advisories labeled TLP:AMBER or TLP:RED tested for accessibility." + } + ] + }, + { + "num": 6, + "description": "Redirects", + "messages": [ + { + "type": 0, + "text": "No redirections found." + } + ] + }, + { + "num": 7, + "description": "provider-metadata.json", + "messages": [ + { + "type": 0, + "text": "Found good provider metadata." + } + ] + }, + { + "num": 8, + "description": "security.txt", + "messages": [ + { + "type": 0, + "text": "Performed no test of security.txt since the direct url of the provider-metadata.json was used." + } + ] + }, + { + "num": 9, + "description": "/.well-known/csaf/provider-metadata.json", + "messages": [ + { + "type": 0, + "text": "Performed no test on whether the provider-metadata.json is available under the .well-known path since the direct url of the provider-metadata.json was used." + } + ] + }, + { + "num": 10, + "description": "DNS path", + "messages": [ + { + "type": 0, + "text": "Performed no test on the contents of https://csaf.data.security.DOMAIN since the direct url of the provider-metadata.json was used." + } + ] + }, + { + "num": 11, + "description": "One folder per year", + "messages": [ + { + "type": 2, + "text": "No year folder found in {{.URL}}/white/avendor-advisory-0004.json" + } + ] + }, + { + "num": 12, + "description": "index.txt", + "messages": [ + { + "type": 0, + "text": "Found {{.URL}}/white/index.txt" + } + ] + }, + { + "num": 13, + "description": "changes.csv", + "messages": [ + { + "type": 0, + "text": "Found {{.URL}}/white/changes.csv" + } + ] + }, + { + "num": 14, + "description": "Directory listings", + "messages": [ + { + "type": 0, + "text": "All directory listings are valid." + } + ] + }, + { + "num": 15, + "description": "ROLIE feed", + "messages": [ + { + "type": 2, + "text": "ROLIE feed based distribution was not used." + } + ] + }, + { + "num": 16, + "description": "ROLIE service document", + "messages": [ + { + "type": 1, + "text": "No ROLIE service document found." + } + ] + }, + { + "num": 17, + "description": "ROLIE category document", + "messages": [ + { + "type": 1, + "text": "No ROLIE category document found." + } + ] + }, + { + "num": 18, + "description": "Integrity", + "messages": [ + { + "type": 0, + "text": "Fetching {{.URL}}/white/avendor-advisory-0004.json.sha256 failed: Status code 404 (404 Not Found)" + } + ] + }, + { + "num": 19, + "description": "Signatures", + "messages": [ + { + "type": 0, + "text": "All signatures verified." + } + ] + }, + { + "num": 20, + "description": "Public OpenPGP Key", + "messages": [ + { + "type": 0, + "text": "1 public OpenPGP key(s) loaded." + } + ] + } +] + diff --git a/testdata/processor-requirements/sha512-rolie.json b/testdata/processor-requirements/sha512-rolie.json new file mode 100644 index 0000000..a2a195d --- /dev/null +++ b/testdata/processor-requirements/sha512-rolie.json @@ -0,0 +1,210 @@ +[ + { + "num": 1, + "description": "Valid CSAF documents", + "messages": [ + { + "type": 1, + "text": "No remote validator configured" + }, + { + "type": 0, + "text": "All advisories validated fine against the schema." + } + ] + }, + { + "num": 2, + "description": "Filename", + "messages": [ + { + "type": 0, + "text": "All found filenames are conforming." + } + ] + }, + { + "num": 3, + "description": "TLS", + "messages": [ + { + "type": 0, + "text": "All tested URLs were HTTPS." + } + ] + }, + { + "num": 4, + "description": "TLP:WHITE", + "messages": [ + { + "type": 0, + "text": "All advisories labeled TLP:WHITE were freely accessible." + } + ] + }, + { + "num": 5, + "description": "TLP:AMBER and TLP:RED", + "messages": [ + { + "type": 0, + "text": "No advisories labeled TLP:AMBER or TLP:RED tested for accessibility." + } + ] + }, + { + "num": 6, + "description": "Redirects", + "messages": [ + { + "type": 0, + "text": "No redirections found." + } + ] + }, + { + "num": 7, + "description": "provider-metadata.json", + "messages": [ + { + "type": 0, + "text": "Found good provider metadata." + } + ] + }, + { + "num": 8, + "description": "security.txt", + "messages": [ + { + "type": 0, + "text": "Performed no test of security.txt since the direct url of the provider-metadata.json was used." + } + ] + }, + { + "num": 9, + "description": "/.well-known/csaf/provider-metadata.json", + "messages": [ + { + "type": 0, + "text": "Performed no test on whether the provider-metadata.json is available under the .well-known path since the direct url of the provider-metadata.json was used." + } + ] + }, + { + "num": 10, + "description": "DNS path", + "messages": [ + { + "type": 0, + "text": "Performed no test on the contents of https://csaf.data.security.DOMAIN since the direct url of the provider-metadata.json was used." + } + ] + }, + { + "num": 11, + "description": "One folder per year", + "messages": [ + { + "type": 2, + "text": "No year folder found in {{.URL}}/white/avendor-advisory-0004.json" + } + ] + }, + { + "num": 12, + "description": "index.txt", + "messages": [ + { + "type": 2, + "text": "Fetching index.txt failed: {{.URL}}/index.txt not found." + } + ] + }, + { + "num": 13, + "description": "changes.csv", + "messages": [ + { + "type": 2, + "text": "Fetching changes.csv failed: {{.URL}}/changes.csv not found." + } + ] + }, + { + "num": 14, + "description": "Directory listings", + "messages": [ + { + "type": 2, + "text": "Fetching {{.URL}}/white/ failed. Status code 404 (404 Not Found)" + }, + { + "type": 2, + "text": "Not listed advisories: {{.URL}}/white/avendor-advisory-0004.json" + } + ] + }, + { + "num": 15, + "description": "ROLIE feed", + "messages": [ + { + "type": 0, + "text": "{{.URL}}/white/avendor-advisory-0004.json has no sha256 hash file listed" + } + ] + }, + { + "num": 16, + "description": "ROLIE service document", + "messages": [ + { + "type": 0, + "text": "ROLIE service document validated fine." + } + ] + }, + { + "num": 17, + "description": "ROLIE category document", + "messages": [ + { + "type": 1, + "text": "Fetching {{.URL}}/white/category-white.json failed. Status code 404 (404 Not Found)" + } + ] + }, + { + "num": 18, + "description": "Integrity", + "messages": [ + { + "type": 0, + "text": "All checksums match." + } + ] + }, + { + "num": 19, + "description": "Signatures", + "messages": [ + { + "type": 0, + "text": "All signatures verified." + } + ] + }, + { + "num": 20, + "description": "Public OpenPGP Key", + "messages": [ + { + "type": 0, + "text": "1 public OpenPGP key(s) loaded." + } + ] + } +] diff --git a/testdata/simple-directory-provider/openpgp/info.txt b/testdata/simple-directory-provider/openpgp/info.txt new file mode 100644 index 0000000..3a159f6 --- /dev/null +++ b/testdata/simple-directory-provider/openpgp/info.txt @@ -0,0 +1,2 @@ +The GPG key was generated with no passphrase and this command: +`gpg --default-new-key-algo "ed25519/cert,sign+cv25519/encr" --quick-generate-key security@example.com"` diff --git a/testdata/simple-directory-provider/openpgp/privkey.asc b/testdata/simple-directory-provider/openpgp/privkey.asc new file mode 100644 index 0000000..816f309 --- /dev/null +++ b/testdata/simple-directory-provider/openpgp/privkey.asc @@ -0,0 +1,15 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +lFgEZtsQNxYJKwYBBAHaRw8BAQdASr3y4zW+4XGqUlvRJ7stRCUHv8HB4ZoMoTtU +KLgnHr4AAQD5G5xy/yTN5b+lvV5Ahrbz1qOZ/wmKTieGOH9GZb6JwhHwtBRzZWN1 +cml0eUBleGFtcGxlLmNvbYiZBBMWCgBBFiEEqJFMovEROcammgAY+zzZsV3mFZYF +AmbbEDcCGwMFCQWjmoAFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQ+zzZ +sV3mFZZskQEAg5Dttqm6TA7MtLxz7VSlklx95LQr9d5jm4jcOaqlGT0A/1mAAlUq +SDySFGI6DFQLcaZaUd9Yl+1b0Icr0tUiOaQHnF0EZtsQNxIKKwYBBAGXVQEFAQEH +QOTHP4FkopIGJMWXTYsaeQ1Dugd+yNYWB357vRYq6QsiAwEIBwAA/0RIazq1s8Oe +23jvNaZGb/adDYnRrkCMXXTBKsuA6WOAEhKIeAQYFgoAIBYhBKiRTKLxETnGppoA +GPs82bFd5hWWBQJm2xA3AhsMAAoJEPs82bFd5hWWDKABAOl+NoM6FBhKAvckUXDR +MLZ4k778N4Vy9VHbectjRKj1AQCO3JOmON+U6/mjohXrc2bwzKzt2yGiLP2HMxDx +uzMXBQ== +=4XHC +-----END PGP PRIVATE KEY BLOCK----- diff --git a/testdata/simple-directory-provider/openpgp/pubkey.asc b/testdata/simple-directory-provider/openpgp/pubkey.asc new file mode 100644 index 0000000..88cb720 --- /dev/null +++ b/testdata/simple-directory-provider/openpgp/pubkey.asc @@ -0,0 +1,13 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEZtsQNxYJKwYBBAHaRw8BAQdASr3y4zW+4XGqUlvRJ7stRCUHv8HB4ZoMoTtU +KLgnHr60FHNlY3VyaXR5QGV4YW1wbGUuY29tiJkEExYKAEEWIQSokUyi8RE5xqaa +ABj7PNmxXeYVlgUCZtsQNwIbAwUJBaOagAULCQgHAgIiAgYVCgkICwIEFgIDAQIe +BwIXgAAKCRD7PNmxXeYVlmyRAQCDkO22qbpMDsy0vHPtVKWSXH3ktCv13mObiNw5 +qqUZPQD/WYACVSpIPJIUYjoMVAtxplpR31iX7VvQhyvS1SI5pAe4OARm2xA3Egor +BgEEAZdVAQUBAQdA5Mc/gWSikgYkxZdNixp5DUO6B37I1hYHfnu9FirpCyIDAQgH +iHgEGBYKACAWIQSokUyi8RE5xqaaABj7PNmxXeYVlgUCZtsQNwIbDAAKCRD7PNmx +XeYVlgygAQDpfjaDOhQYSgL3JFFw0TC2eJO+/DeFcvVR23nLY0So9QEAjtyTpjjf +lOv5o6IV63Nm8Mys7dshoiz9hzMQ8bszFwU= +=rhGT +-----END PGP PUBLIC KEY BLOCK----- diff --git a/testdata/simple-directory-provider/provider-metadata.json b/testdata/simple-directory-provider/provider-metadata.json new file mode 100644 index 0000000..792afd3 --- /dev/null +++ b/testdata/simple-directory-provider/provider-metadata.json @@ -0,0 +1,25 @@ +{ + "canonical_url": "{{.URL}}/provider-metadata.json", + "distributions": [ + { + "directory_url": "{{.URL}}/white/" + } + ], + "last_updated": "2020-01-01T00:00:00Z", + "list_on_CSAF_aggregators": true, + "metadata_version": "2.0", + "mirror_on_CSAF_aggregators": true, + "public_openpgp_keys": [ + { + "fingerprint": "A8914CA2F11139C6A69A0018FB3CD9B15DE61596", + "url": "{{.URL}}/openpgp/pubkey.asc" + } + ], + "publisher": { + "category": "vendor", + "name": "ACME Inc", + "namespace": "https://example.com", + "contact_details": "mailto:security@example.com" + }, + "role": "csaf_trusted_provider" +} diff --git a/testdata/simple-directory-provider/security.txt b/testdata/simple-directory-provider/security.txt new file mode 100644 index 0000000..0ae943d --- /dev/null +++ b/testdata/simple-directory-provider/security.txt @@ -0,0 +1,2 @@ +CSAF: /provider-metadata.json + diff --git a/testdata/simple-directory-provider/white/avendor-advisory-0004-not-listed.json b/testdata/simple-directory-provider/white/avendor-advisory-0004-not-listed.json new file mode 100644 index 0000000..0e194e9 --- /dev/null +++ b/testdata/simple-directory-provider/white/avendor-advisory-0004-not-listed.json @@ -0,0 +1,170 @@ +{ + "document": { + "category": "csaf_vex", + "csaf_version": "2.0", + "distribution": { + "tlp": { + "label": "WHITE", + "url": "https://www.first.org/tlp/v1/" + } + }, + "notes": [ + { + "category": "summary", + "title": "Test document summary", + "text": "Auto generated test CSAF document" + } + ], + "publisher": { + "category": "vendor", + "name": "ACME Inc.", + "namespace": "https://www.example.com" + }, + "title": "Test CSAF document", + "tracking": { + "current_release_date": "2020-01-01T00:00:00Z", + "generator": { + "date": "2020-01-01T00:00:00Z", + "engine": { + "name": "csaf-tool", + "version": "0.3.2" + } + }, + "id": "Avendor-advisory-0004", + "initial_release_date": "2020-01-01T00:00:00Z", + "revision_history": [ + { + "date": "2020-01-01T00:00:00Z", + "number": "1", + "summary": "Initial version" + } + ], + "status": "final", + "version": "1" + } + }, + "product_tree": { + "branches": [ + { + "category": "vendor", + "name": "AVendor", + "branches": [ + { + "category": "product_name", + "name": "product_1", + "branches": [ + { + "category": "product_version", + "name": "1.1", + "product": { + "name": "AVendor product_1 1.1", + "product_id": "CSAFPID_0001" + } + }, + { + "category": "product_version", + "name": "1.2", + "product": { + "name": "AVendor product_1 1.2", + "product_id": "CSAFPID_0002" + } + }, + { + "category": "product_version", + "name": "2.0", + "product": { + "name": "AVendor product_1 2.0", + "product_id": "CSAFPID_0003" + } + } + ] + } + ] + }, + { + "category": "vendor", + "name": "AVendor1", + "branches": [ + { + "category": "product_name", + "name": "product_2", + "branches": [ + { + "category": "product_version", + "name": "1", + "product": { + "name": "AVendor1 product_2 1", + "product_id": "CSAFPID_0004" + } + } + ] + } + ] + }, + { + "category": "vendor", + "name": "AVendor", + "branches": [ + { + "category": "product_name", + "name": "product_3", + "branches": [ + { + "category": "product_version", + "name": "2022H2", + "product": { + "name": "AVendor product_3 2022H2", + "product_id": "CSAFPID_0005" + } + } + ] + } + ] + } + ] + }, + "vulnerabilities": [ + { + "cve": "CVE-2020-1234", + "notes": [ + { + "category": "description", + "title": "CVE description", + "text": "https://nvd.nist.gov/vuln/detail/CVE-2020-1234" + } + ], + "product_status": { + "under_investigation": ["CSAFPID_0001"] + }, + "threats": [ + { + "category": "impact", + "details": "Customers should upgrade to the latest version of the product", + "date": "2020-01-01T00:00:00Z", + "product_ids": ["CSAFPID_0001"] + } + ] + }, + { + "cve": "CVE-2020-9876", + "notes": [ + { + "category": "description", + "title": "CVE description", + "text": "https://nvd.nist.gov/vuln/detail/CVE-2020-9876" + } + ], + "product_status": { + "under_investigation": ["CSAFPID_0001"] + }, + "threats": [ + { + "category": "impact", + "details": "Still under investigation", + "date": "2020-01-01T00:00:00Z", + "product_ids": ["CSAFPID_0001"] + } + ] + } + ] +} diff --git a/testdata/simple-directory-provider/white/avendor-advisory-0004.json b/testdata/simple-directory-provider/white/avendor-advisory-0004.json new file mode 100644 index 0000000..0e194e9 --- /dev/null +++ b/testdata/simple-directory-provider/white/avendor-advisory-0004.json @@ -0,0 +1,170 @@ +{ + "document": { + "category": "csaf_vex", + "csaf_version": "2.0", + "distribution": { + "tlp": { + "label": "WHITE", + "url": "https://www.first.org/tlp/v1/" + } + }, + "notes": [ + { + "category": "summary", + "title": "Test document summary", + "text": "Auto generated test CSAF document" + } + ], + "publisher": { + "category": "vendor", + "name": "ACME Inc.", + "namespace": "https://www.example.com" + }, + "title": "Test CSAF document", + "tracking": { + "current_release_date": "2020-01-01T00:00:00Z", + "generator": { + "date": "2020-01-01T00:00:00Z", + "engine": { + "name": "csaf-tool", + "version": "0.3.2" + } + }, + "id": "Avendor-advisory-0004", + "initial_release_date": "2020-01-01T00:00:00Z", + "revision_history": [ + { + "date": "2020-01-01T00:00:00Z", + "number": "1", + "summary": "Initial version" + } + ], + "status": "final", + "version": "1" + } + }, + "product_tree": { + "branches": [ + { + "category": "vendor", + "name": "AVendor", + "branches": [ + { + "category": "product_name", + "name": "product_1", + "branches": [ + { + "category": "product_version", + "name": "1.1", + "product": { + "name": "AVendor product_1 1.1", + "product_id": "CSAFPID_0001" + } + }, + { + "category": "product_version", + "name": "1.2", + "product": { + "name": "AVendor product_1 1.2", + "product_id": "CSAFPID_0002" + } + }, + { + "category": "product_version", + "name": "2.0", + "product": { + "name": "AVendor product_1 2.0", + "product_id": "CSAFPID_0003" + } + } + ] + } + ] + }, + { + "category": "vendor", + "name": "AVendor1", + "branches": [ + { + "category": "product_name", + "name": "product_2", + "branches": [ + { + "category": "product_version", + "name": "1", + "product": { + "name": "AVendor1 product_2 1", + "product_id": "CSAFPID_0004" + } + } + ] + } + ] + }, + { + "category": "vendor", + "name": "AVendor", + "branches": [ + { + "category": "product_name", + "name": "product_3", + "branches": [ + { + "category": "product_version", + "name": "2022H2", + "product": { + "name": "AVendor product_3 2022H2", + "product_id": "CSAFPID_0005" + } + } + ] + } + ] + } + ] + }, + "vulnerabilities": [ + { + "cve": "CVE-2020-1234", + "notes": [ + { + "category": "description", + "title": "CVE description", + "text": "https://nvd.nist.gov/vuln/detail/CVE-2020-1234" + } + ], + "product_status": { + "under_investigation": ["CSAFPID_0001"] + }, + "threats": [ + { + "category": "impact", + "details": "Customers should upgrade to the latest version of the product", + "date": "2020-01-01T00:00:00Z", + "product_ids": ["CSAFPID_0001"] + } + ] + }, + { + "cve": "CVE-2020-9876", + "notes": [ + { + "category": "description", + "title": "CVE description", + "text": "https://nvd.nist.gov/vuln/detail/CVE-2020-9876" + } + ], + "product_status": { + "under_investigation": ["CSAFPID_0001"] + }, + "threats": [ + { + "category": "impact", + "details": "Still under investigation", + "date": "2020-01-01T00:00:00Z", + "product_ids": ["CSAFPID_0001"] + } + ] + } + ] +} diff --git a/testdata/simple-directory-provider/white/avendor-advisory-0004.json.asc b/testdata/simple-directory-provider/white/avendor-advisory-0004.json.asc new file mode 100644 index 0000000..9dff47b --- /dev/null +++ b/testdata/simple-directory-provider/white/avendor-advisory-0004.json.asc @@ -0,0 +1,7 @@ +-----BEGIN PGP SIGNATURE----- + +iHUEABYKAB0WIQSokUyi8RE5xqaaABj7PNmxXeYVlgUCZukv9QAKCRD7PNmxXeYV +ljq0AP9n/rTgoNCJzSTZzNrrMy28ZR+Ppp1MSPWGFUzsx6qLJgD/d8cu0lokMsXf +y0uc9k7hrla/ajFUzNt3AVvT+CPFtAo= +=7E66 +-----END PGP SIGNATURE----- diff --git a/testdata/simple-directory-provider/white/avendor-advisory-0004.json.sha256 b/testdata/simple-directory-provider/white/avendor-advisory-0004.json.sha256 new file mode 100644 index 0000000..851b27c --- /dev/null +++ b/testdata/simple-directory-provider/white/avendor-advisory-0004.json.sha256 @@ -0,0 +1 @@ +cb263bf1beab18b893de63f2966d0d8c5f38d60101c24d3fd7a5feebaad02c3b avendor-advisory-0004.json diff --git a/testdata/simple-directory-provider/white/avendor-advisory-0004.json.sha512 b/testdata/simple-directory-provider/white/avendor-advisory-0004.json.sha512 new file mode 100644 index 0000000..6703550 --- /dev/null +++ b/testdata/simple-directory-provider/white/avendor-advisory-0004.json.sha512 @@ -0,0 +1 @@ +39476e1d08a0871d166091c90de259544382a3599eebda118a93468499a30fd034286086c461a97d3d5298e093b0be3868e8d89d8a6a255c4aa6adb81ebbfcad avendor-advisory-0004.json diff --git a/testdata/simple-directory-provider/white/changes.csv b/testdata/simple-directory-provider/white/changes.csv new file mode 100644 index 0000000..4acdb29 --- /dev/null +++ b/testdata/simple-directory-provider/white/changes.csv @@ -0,0 +1 @@ +"avendor-advisory-0004.json","2020-01-01T00:00:00+00:00" diff --git a/testdata/simple-directory-provider/white/index.html b/testdata/simple-directory-provider/white/index.html new file mode 100644 index 0000000..bcfabd9 --- /dev/null +++ b/testdata/simple-directory-provider/white/index.html @@ -0,0 +1,6 @@ + + + + avendor-advisory-0004 + + diff --git a/testdata/simple-directory-provider/white/index.txt b/testdata/simple-directory-provider/white/index.txt new file mode 100644 index 0000000..d19d30f --- /dev/null +++ b/testdata/simple-directory-provider/white/index.txt @@ -0,0 +1 @@ +avendor-advisory-0004.json diff --git a/testdata/simple-rolie-provider/openpgp/info.txt b/testdata/simple-rolie-provider/openpgp/info.txt new file mode 100644 index 0000000..3a159f6 --- /dev/null +++ b/testdata/simple-rolie-provider/openpgp/info.txt @@ -0,0 +1,2 @@ +The GPG key was generated with no passphrase and this command: +`gpg --default-new-key-algo "ed25519/cert,sign+cv25519/encr" --quick-generate-key security@example.com"` diff --git a/testdata/simple-rolie-provider/openpgp/privkey.asc b/testdata/simple-rolie-provider/openpgp/privkey.asc new file mode 100644 index 0000000..816f309 --- /dev/null +++ b/testdata/simple-rolie-provider/openpgp/privkey.asc @@ -0,0 +1,15 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +lFgEZtsQNxYJKwYBBAHaRw8BAQdASr3y4zW+4XGqUlvRJ7stRCUHv8HB4ZoMoTtU +KLgnHr4AAQD5G5xy/yTN5b+lvV5Ahrbz1qOZ/wmKTieGOH9GZb6JwhHwtBRzZWN1 +cml0eUBleGFtcGxlLmNvbYiZBBMWCgBBFiEEqJFMovEROcammgAY+zzZsV3mFZYF +AmbbEDcCGwMFCQWjmoAFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQ+zzZ +sV3mFZZskQEAg5Dttqm6TA7MtLxz7VSlklx95LQr9d5jm4jcOaqlGT0A/1mAAlUq +SDySFGI6DFQLcaZaUd9Yl+1b0Icr0tUiOaQHnF0EZtsQNxIKKwYBBAGXVQEFAQEH +QOTHP4FkopIGJMWXTYsaeQ1Dugd+yNYWB357vRYq6QsiAwEIBwAA/0RIazq1s8Oe +23jvNaZGb/adDYnRrkCMXXTBKsuA6WOAEhKIeAQYFgoAIBYhBKiRTKLxETnGppoA +GPs82bFd5hWWBQJm2xA3AhsMAAoJEPs82bFd5hWWDKABAOl+NoM6FBhKAvckUXDR +MLZ4k778N4Vy9VHbectjRKj1AQCO3JOmON+U6/mjohXrc2bwzKzt2yGiLP2HMxDx +uzMXBQ== +=4XHC +-----END PGP PRIVATE KEY BLOCK----- diff --git a/testdata/simple-rolie-provider/openpgp/pubkey.asc b/testdata/simple-rolie-provider/openpgp/pubkey.asc new file mode 100644 index 0000000..88cb720 --- /dev/null +++ b/testdata/simple-rolie-provider/openpgp/pubkey.asc @@ -0,0 +1,13 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEZtsQNxYJKwYBBAHaRw8BAQdASr3y4zW+4XGqUlvRJ7stRCUHv8HB4ZoMoTtU +KLgnHr60FHNlY3VyaXR5QGV4YW1wbGUuY29tiJkEExYKAEEWIQSokUyi8RE5xqaa +ABj7PNmxXeYVlgUCZtsQNwIbAwUJBaOagAULCQgHAgIiAgYVCgkICwIEFgIDAQIe +BwIXgAAKCRD7PNmxXeYVlmyRAQCDkO22qbpMDsy0vHPtVKWSXH3ktCv13mObiNw5 +qqUZPQD/WYACVSpIPJIUYjoMVAtxplpR31iX7VvQhyvS1SI5pAe4OARm2xA3Egor +BgEEAZdVAQUBAQdA5Mc/gWSikgYkxZdNixp5DUO6B37I1hYHfnu9FirpCyIDAQgH +iHgEGBYKACAWIQSokUyi8RE5xqaaABj7PNmxXeYVlgUCZtsQNwIbDAAKCRD7PNmx +XeYVlgygAQDpfjaDOhQYSgL3JFFw0TC2eJO+/DeFcvVR23nLY0So9QEAjtyTpjjf +lOv5o6IV63Nm8Mys7dshoiz9hzMQ8bszFwU= +=rhGT +-----END PGP PUBLIC KEY BLOCK----- diff --git a/testdata/simple-rolie-provider/provider-metadata.json b/testdata/simple-rolie-provider/provider-metadata.json new file mode 100644 index 0000000..7abb316 --- /dev/null +++ b/testdata/simple-rolie-provider/provider-metadata.json @@ -0,0 +1,33 @@ +{ + "canonical_url": "{{.URL}}/provider-metadata.json", + "distributions": [ + { + "rolie": { + "feeds": [ + { + "summary": "TLP:WHITE advisories", + "tlp_label": "WHITE", + "url": "{{.URL}}/white/white-feed.json" + } + ] + } + } + ], + "last_updated": "2020-01-01T00:00:00Z", + "list_on_CSAF_aggregators": true, + "metadata_version": "2.0", + "mirror_on_CSAF_aggregators": true, + "public_openpgp_keys": [ + { + "fingerprint": "A8914CA2F11139C6A69A0018FB3CD9B15DE61596", + "url": "{{.URL}}/openpgp/pubkey.asc" + } + ], + "publisher": { + "category": "vendor", + "name": "ACME Inc", + "namespace": "https://example.com", + "contact_details": "mailto:security@example.com" + }, + "role": "csaf_trusted_provider" +} diff --git a/testdata/simple-rolie-provider/security.txt b/testdata/simple-rolie-provider/security.txt new file mode 100644 index 0000000..0ae943d --- /dev/null +++ b/testdata/simple-rolie-provider/security.txt @@ -0,0 +1,2 @@ +CSAF: /provider-metadata.json + diff --git a/testdata/simple-rolie-provider/service.json b/testdata/simple-rolie-provider/service.json new file mode 100644 index 0000000..a398a40 --- /dev/null +++ b/testdata/simple-rolie-provider/service.json @@ -0,0 +1,23 @@ +{ + "service": { + "workspace": [ + { + "title": "CSAF feeds", + "collection": [ + { + "title": "CSAF feed (TLP:WHITE)", + "href": "{{.URL}}/white/white-feed.json", + "categories": { + "category": [ + { + "scheme": "urn:ietf:params:rolie:category:information-type", + "term": "csaf" + } + ] + } + } + ] + } + ] + } +} diff --git a/testdata/simple-rolie-provider/white/avendor-advisory-0004.json b/testdata/simple-rolie-provider/white/avendor-advisory-0004.json new file mode 100644 index 0000000..0e194e9 --- /dev/null +++ b/testdata/simple-rolie-provider/white/avendor-advisory-0004.json @@ -0,0 +1,170 @@ +{ + "document": { + "category": "csaf_vex", + "csaf_version": "2.0", + "distribution": { + "tlp": { + "label": "WHITE", + "url": "https://www.first.org/tlp/v1/" + } + }, + "notes": [ + { + "category": "summary", + "title": "Test document summary", + "text": "Auto generated test CSAF document" + } + ], + "publisher": { + "category": "vendor", + "name": "ACME Inc.", + "namespace": "https://www.example.com" + }, + "title": "Test CSAF document", + "tracking": { + "current_release_date": "2020-01-01T00:00:00Z", + "generator": { + "date": "2020-01-01T00:00:00Z", + "engine": { + "name": "csaf-tool", + "version": "0.3.2" + } + }, + "id": "Avendor-advisory-0004", + "initial_release_date": "2020-01-01T00:00:00Z", + "revision_history": [ + { + "date": "2020-01-01T00:00:00Z", + "number": "1", + "summary": "Initial version" + } + ], + "status": "final", + "version": "1" + } + }, + "product_tree": { + "branches": [ + { + "category": "vendor", + "name": "AVendor", + "branches": [ + { + "category": "product_name", + "name": "product_1", + "branches": [ + { + "category": "product_version", + "name": "1.1", + "product": { + "name": "AVendor product_1 1.1", + "product_id": "CSAFPID_0001" + } + }, + { + "category": "product_version", + "name": "1.2", + "product": { + "name": "AVendor product_1 1.2", + "product_id": "CSAFPID_0002" + } + }, + { + "category": "product_version", + "name": "2.0", + "product": { + "name": "AVendor product_1 2.0", + "product_id": "CSAFPID_0003" + } + } + ] + } + ] + }, + { + "category": "vendor", + "name": "AVendor1", + "branches": [ + { + "category": "product_name", + "name": "product_2", + "branches": [ + { + "category": "product_version", + "name": "1", + "product": { + "name": "AVendor1 product_2 1", + "product_id": "CSAFPID_0004" + } + } + ] + } + ] + }, + { + "category": "vendor", + "name": "AVendor", + "branches": [ + { + "category": "product_name", + "name": "product_3", + "branches": [ + { + "category": "product_version", + "name": "2022H2", + "product": { + "name": "AVendor product_3 2022H2", + "product_id": "CSAFPID_0005" + } + } + ] + } + ] + } + ] + }, + "vulnerabilities": [ + { + "cve": "CVE-2020-1234", + "notes": [ + { + "category": "description", + "title": "CVE description", + "text": "https://nvd.nist.gov/vuln/detail/CVE-2020-1234" + } + ], + "product_status": { + "under_investigation": ["CSAFPID_0001"] + }, + "threats": [ + { + "category": "impact", + "details": "Customers should upgrade to the latest version of the product", + "date": "2020-01-01T00:00:00Z", + "product_ids": ["CSAFPID_0001"] + } + ] + }, + { + "cve": "CVE-2020-9876", + "notes": [ + { + "category": "description", + "title": "CVE description", + "text": "https://nvd.nist.gov/vuln/detail/CVE-2020-9876" + } + ], + "product_status": { + "under_investigation": ["CSAFPID_0001"] + }, + "threats": [ + { + "category": "impact", + "details": "Still under investigation", + "date": "2020-01-01T00:00:00Z", + "product_ids": ["CSAFPID_0001"] + } + ] + } + ] +} diff --git a/testdata/simple-rolie-provider/white/avendor-advisory-0004.json.asc b/testdata/simple-rolie-provider/white/avendor-advisory-0004.json.asc new file mode 100644 index 0000000..9dff47b --- /dev/null +++ b/testdata/simple-rolie-provider/white/avendor-advisory-0004.json.asc @@ -0,0 +1,7 @@ +-----BEGIN PGP SIGNATURE----- + +iHUEABYKAB0WIQSokUyi8RE5xqaaABj7PNmxXeYVlgUCZukv9QAKCRD7PNmxXeYV +ljq0AP9n/rTgoNCJzSTZzNrrMy28ZR+Ppp1MSPWGFUzsx6qLJgD/d8cu0lokMsXf +y0uc9k7hrla/ajFUzNt3AVvT+CPFtAo= +=7E66 +-----END PGP SIGNATURE----- diff --git a/testdata/simple-rolie-provider/white/avendor-advisory-0004.json.sha256 b/testdata/simple-rolie-provider/white/avendor-advisory-0004.json.sha256 new file mode 100644 index 0000000..851b27c --- /dev/null +++ b/testdata/simple-rolie-provider/white/avendor-advisory-0004.json.sha256 @@ -0,0 +1 @@ +cb263bf1beab18b893de63f2966d0d8c5f38d60101c24d3fd7a5feebaad02c3b avendor-advisory-0004.json diff --git a/testdata/simple-rolie-provider/white/avendor-advisory-0004.json.sha512 b/testdata/simple-rolie-provider/white/avendor-advisory-0004.json.sha512 new file mode 100644 index 0000000..6703550 --- /dev/null +++ b/testdata/simple-rolie-provider/white/avendor-advisory-0004.json.sha512 @@ -0,0 +1 @@ +39476e1d08a0871d166091c90de259544382a3599eebda118a93468499a30fd034286086c461a97d3d5298e093b0be3868e8d89d8a6a255c4aa6adb81ebbfcad avendor-advisory-0004.json diff --git a/testdata/simple-rolie-provider/white/white-feed.json b/testdata/simple-rolie-provider/white/white-feed.json new file mode 100644 index 0000000..923a492 --- /dev/null +++ b/testdata/simple-rolie-provider/white/white-feed.json @@ -0,0 +1,61 @@ +{ + "feed": { + "id": "csaf-feed-tlp-white", + "title": "CSAF feed (TLP:WHITE)", + "link": [ + { + "rel": "self", + "href": "{{.URL}}/white/csaf-feed-tlp-white.json" + }, + { + "rel": "service", + "href": "{{.URL}}/service.json" + } + ], + "category": [ + { + "scheme": "urn:ietf:params:rolie:category:information-type", + "term": "csaf" + } + ], + "updated": "2020-01-01T00:00:00Z", + "entry": [ + { + "id": "Avendor-advisory-0004", + "title": "Test CSAF document", + "link": [ + { + "rel": "self", + "href": "{{.URL}}/white/avendor-advisory-0004.json" + }, + {{if .EnableSha256}} + { + "rel": "hash", + "href": "{{.URL}}/white/avendor-advisory-0004.json.sha256" + }, + {{end}} + {{if .EnableSha512}} + { + "rel": "hash", + "href": "{{.URL}}/white/avendor-advisory-0004.json.sha512" + }, + {{end}} + { + "rel": "signature", + "href": "{{.URL}}/white/avendor-advisory-0004.json.asc" + } + ], + "published": "2020-01-01T00:00:00Z", + "updated": "2020-01-01T00:00:00Z", + "content": { + "type": "application/json", + "src": "{{.URL}}/avendor-advisory-0004.json" + }, + "format": { + "schema": "https://docs.oasis-open.org/csaf/csaf/v2.0/csaf_json_schema.json", + "version": "2.0" + } + } + ] + } +} diff --git a/util/client.go b/util/client.go index 239f8ec..b82bc54 100644 --- a/util/client.go +++ b/util/client.go @@ -1,12 +1,12 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) // Software-Engineering: 2022 Intevation GmbH -package util +package util //revive:disable-line:var-naming import ( "context" @@ -61,6 +61,11 @@ func (hc *HeaderClient) Do(req *http.Request) (*http.Response, error) { req.Header.Add(key, v) } } + + // Use default user agent if none is set + if userAgent := hc.Header.Get("User-Agent"); userAgent == "" { + req.Header.Add("User-Agent", "csaf_distribution/"+SemVersion) + } return hc.Client.Do(req) } diff --git a/util/csv.go b/util/csv.go index aee0e6d..cffaf52 100644 --- a/util/csv.go +++ b/util/csv.go @@ -1,12 +1,12 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) // Software-Engineering: 2022 Intevation GmbH -package util +package util //revive:disable-line:var-naming import ( "bufio" diff --git a/util/csv_test.go b/util/csv_test.go new file mode 100644 index 0000000..68b5a3e --- /dev/null +++ b/util/csv_test.go @@ -0,0 +1,38 @@ +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. +// +// SPDX-License-Identifier: Apache-2.0 +// +// SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) +// Software-Engineering: 2022 Intevation GmbH + +package util //revive:disable-line:var-naming + +import ( + "bytes" + "testing" +) + +func TestCSV(t *testing.T) { + buf := new(bytes.Buffer) + csvWriter := NewFullyQuotedCSWWriter(buf) + for _, x := range [][]string{{"a", "b", "c"}, {"d", "e", "f"}} { + if err := csvWriter.Write(x); err != nil { + t.Error(err) + } + } + + csvWriter.Flush() + if err := csvWriter.Error(); err != nil { + t.Error(err) + } + for _, want := range []string{`"a","b","c"`, `"d","e","f"`} { + got, err := buf.ReadString('\n') + if err != nil { + t.Error(err) + } + if got[:len(got)-1] != want { + t.Errorf("FullyQuotedCSWWriter: Expected %q but got %q.", want, got) + } + } +} diff --git a/util/doc.go b/util/doc.go index df6809e..2caf4e3 100644 --- a/util/doc.go +++ b/util/doc.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2023 German Federal Office for Information Security (BSI) // Software-Engineering: 2023 Intevation GmbH diff --git a/util/file.go b/util/file.go index bff7110..85bda0a 100644 --- a/util/file.go +++ b/util/file.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) // Software-Engineering: 2022 Intevation GmbH diff --git a/util/file_test.go b/util/file_test.go index aafce6d..ab2a208 100644 --- a/util/file_test.go +++ b/util/file_test.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) // Software-Engineering: 2022 Intevation GmbH @@ -10,6 +10,8 @@ package util import ( "bytes" + "os" + "path/filepath" "testing" ) @@ -55,8 +57,51 @@ func TestConformingFileName(t *testing.T) { } } -func TestNWriter(t *testing.T) { +func TestIDMatchesFilename(t *testing.T) { + pathEval := NewPathEval() + doc := make(map[string]any) + doc["document"] = map[string]any{ + "tracking": map[string]any{ + "id": "valid.json", + }, + } + + if err := IDMatchesFilename(pathEval, doc, "valid.json"); err != nil { + t.Errorf("IDMatchesFilename: Expected nil, got %q", err) + } + + if err := IDMatchesFilename(pathEval, doc, "different_file_name.json"); err == nil { + t.Error("IDMatchesFilename: Expected error, got nil") + } + + doc["document"] = map[string]any{ + "tracking": map[string]any{}, + } + if err := IDMatchesFilename(pathEval, doc, "valid.json"); err == nil { + t.Error("IDMatchesFilename: Expected error, got nil") + } +} + +func TestPathExists(t *testing.T) { + got, err := PathExists("/this/path/does/not/exist") + if err != nil { + t.Error(err) + } + if got != false { + t.Error("PathExists: Expected false, got true") + } + dir := t.TempDir() + got, err = PathExists(dir) + if err != nil { + t.Error(err) + } + if got != true { + t.Error("PathExists: Expected true, got false") + } +} + +func TestNWriter(t *testing.T) { msg := []byte("Gruß!\n") first, second := msg[:len(msg)/2], msg[len(msg)/2:] @@ -78,3 +123,93 @@ func TestNWriter(t *testing.T) { t.Errorf("Expected %q, but got %q", msg, out) } } + +func TestWriteToFile(t *testing.T) { + filename := filepath.Join(t.TempDir(), "test_file") + wt := bytes.NewBufferString("test_data") + if err := WriteToFile(filename, wt); err != nil { + t.Error(err) + } + fileData, err := os.ReadFile(filename) + if err != nil { + t.Error(err) + } + if !bytes.Equal(fileData, []byte("test_data")) { + t.Errorf("DeepCopy: Expected test_data, got %v", fileData) + } +} + +func TestMakeUniqFile(t *testing.T) { + dir := t.TempDir() + _, file, err := MakeUniqFile(dir) + if err != nil { + t.Error(err) + } + if _, err = file.Write([]byte("test_data")); err != nil { + t.Error(err) + } + if err = file.Close(); err != nil { + t.Error(err) + } +} + +func Test_mkUniq(t *testing.T) { + dir := t.TempDir() + name, err := mkUniq(dir+"/", func(_ string) error { + return nil + }) + if err != nil { + t.Error(err) + } + firstTime := true + name1, err := mkUniq(dir+"/", func(_ string) error { + if firstTime { + firstTime = false + return os.ErrExist + } + return nil + }) + if err != nil { + t.Error(err) + } + if name == name1 { + t.Errorf("mkUniq: Expected unique names, got %v and %v", name, name1) + } +} + +func TestDeepCopy(t *testing.T) { + dir := t.TempDir() + if err := os.MkdirAll(filepath.Join(dir, "src/folder0"), 0755); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Join(dir, "dst"), 0755); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Join(dir, "dst1"), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "src/folder0/test_file"), []byte("test_data"), 0755); err != nil { + t.Fatal(err) + } + + if err := DeepCopy(filepath.Join(dir, "dst"), filepath.Join(dir, "src")); err != nil { + t.Error(err) + } + + fileData, err := os.ReadFile(filepath.Join(dir, "dst/folder0/test_file")) + if err != nil { + t.Error(err) + } + + if !bytes.Equal(fileData, []byte("test_data")) { + t.Errorf("DeepCopy: Expected test_data, got %v", fileData) + } + + if err = DeepCopy("/path/does/not/exist", filepath.Join(dir, "src")); err == nil { + t.Error("DeepCopy: Expected error, got nil") + } + + if err = DeepCopy(filepath.Join(dir, "dst1"), "/path/does/not/exist"); err == nil { + t.Error("DeepCopy: Expected error, got nil") + } +} diff --git a/util/hash.go b/util/hash.go index 84cfa08..b5dcaa9 100644 --- a/util/hash.go +++ b/util/hash.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) // Software-Engineering: 2022 Intevation GmbH diff --git a/util/hash_test.go b/util/hash_test.go new file mode 100644 index 0000000..d690891 --- /dev/null +++ b/util/hash_test.go @@ -0,0 +1,107 @@ +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. +// +// SPDX-License-Identifier: Apache-2.0 +// +// SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) +// Software-Engineering: 2022 Intevation GmbH + +package util + +import ( + "hash" + "os" + "path/filepath" + "reflect" + "strings" + "testing" +) + +func TestHashFromReader(t *testing.T) { + r := strings.NewReader("deadbeef") + want := []byte{0xde, 0xad, 0xbe, 0xef} + if got, err := HashFromReader(r); !reflect.DeepEqual(want, got) { + if err != nil { + t.Error(err) + } + t.Errorf("HashFromReader: Expected %v, got %v", want, got) + } +} + +func TestHashFromFile(t *testing.T) { + dir := t.TempDir() + filePath := filepath.Join(dir, "test_file") + testFile, err := os.Create(filePath) + if err != nil { + t.Error(err) + } + + testFile.WriteString("deadbeef") + want := []byte{0xde, 0xad, 0xbe, 0xef} + + testFile.Close() + + if got, err := HashFromFile(filePath); !reflect.DeepEqual(want, got) { + if err != nil { + t.Error(err) + } + t.Errorf("HashFromFile: Expected %v, got %v", want, got) + } +} + +type deadbeefHash struct { + hash.Hash +} + +func (deadbeefHash) Write(p []byte) (int, error) { return len(p), nil } +func (deadbeefHash) Sum(_ []byte) []byte { return []byte{0xde, 0xad, 0xbe, 0xef} } + +func TestWriteHashToFile(t *testing.T) { + dir := t.TempDir() + filePath := filepath.Join(dir, "test_file") + + hashArg := deadbeefHash{} + nameArg := "name" + want := "deadbeef " + nameArg + "\n" + + if err := WriteHashToFile(filePath, nameArg, hashArg, []byte{}); err != nil { + t.Error(err) + } + testFile, err := os.Open(filePath) + if err != nil { + t.Error(err) + } + defer testFile.Close() + fileContent, err := os.ReadFile(filePath) + if err != nil { + t.Error(err) + } + if got := string(fileContent); got != want { + t.Errorf("WriteHashToFile: Expected %v, got %v", want, got) + } +} + +func TestWriteHashSumToFile(t *testing.T) { + dir := t.TempDir() + filePath := filepath.Join(dir, "test_file") + + sum := []byte{0xde, 0xad, 0xbe, 0xef} + nameArg := "name" + want := "deadbeef " + nameArg + "\n" + + if err := WriteHashSumToFile(filePath, nameArg, sum); err != nil { + t.Error(err) + } + testFile, err := os.Open(filePath) + if err != nil { + t.Error(err) + } + defer testFile.Close() + fileContent, err := os.ReadFile(filePath) + if err != nil { + t.Error(err) + } + if got := string(fileContent); got != want { + t.Errorf("WriteHashSumToFile: Expected %v, got %v", want, got) + } +} diff --git a/util/json.go b/util/json.go index 851974b..f66ab86 100644 --- a/util/json.go +++ b/util/json.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) // Software-Engineering: 2021 Intevation GmbH diff --git a/util/json_test.go b/util/json_test.go new file mode 100644 index 0000000..ba18171 --- /dev/null +++ b/util/json_test.go @@ -0,0 +1,196 @@ +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. +// +// SPDX-License-Identifier: Apache-2.0 +// +// SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) +// Software-Engineering: 2022 Intevation GmbH + +package util + +import ( + "context" + "reflect" + "testing" + "time" +) + +func TestPathEval_Compile(t *testing.T) { + pathEval := NewPathEval() + eval, err := pathEval.Compile("foo") + if err != nil { + t.Error(err) + } + + // Check caching + eval1, err := pathEval.Compile("foo") + if err != nil { + t.Error(err) + } + if reflect.ValueOf(eval).Pointer() != reflect.ValueOf(eval1).Pointer() { + t.Error("PathEval_Compile: Expected cached eval") + } + + got, err := eval.EvalInt(context.Background(), map[string]any{"foo": 5}) + if err != nil { + t.Error(err) + } + if got != 5 { + t.Errorf("PathEval_Compile: Expected 5, got %v", got) + } +} + +func TestPathEval_Eval(t *testing.T) { + pathEval := NewPathEval() + _, err := pathEval.Eval("foo", nil) + if err == nil { + t.Error("PathEval_Eval: Expected error, got nil") + } + got, err := pathEval.Eval("foo", map[string]any{"foo": 5}) + if err != nil { + t.Error(err) + } + if got != 5 { + t.Errorf("PathEval_Compile: Expected 5, got %v", got) + } +} + +func TestReMarshalMatcher(t *testing.T) { + var intDst int + var uintSrc uint = 2 + remarshalFunc := ReMarshalMatcher(&intDst) + if err := remarshalFunc(uintSrc); err != nil { + t.Error(err) + } + if intDst != 2 { + t.Errorf("ReMarshalMatcher: Expected %v, got %v", uintSrc, intDst) + } +} + +func TestBoolMatcher(t *testing.T) { + var boolDst bool + boolFunc := BoolMatcher(&boolDst) + if err := boolFunc(true); err != nil { + t.Error(err) + } + + if boolDst != true { + t.Error("BoolMatcher: Expected true got false") + } + + if err := boolFunc(1); err == nil { + t.Error("BoolMatcher: Expected error, got nil") + } +} + +func TestStringMatcher(t *testing.T) { + var stringDst string + stringFunc := StringMatcher(&stringDst) + if err := stringFunc("test"); err != nil { + t.Error(err) + } + + if stringDst != "test" { + t.Errorf("StringMatcher: Expected test, got %v", stringDst) + } + + if err := stringFunc(1); err == nil { + t.Error("StringMatcher: Expected error, got nil") + } +} + +func TestStringTreeMatcher(t *testing.T) { + var stringTreeDst []string + stringTreeFunc := StringTreeMatcher(&stringTreeDst) + if err := stringTreeFunc([]any{"a", "a", "b"}); err != nil { + t.Error(err) + } + + wantAnySlice := []any{"a", "b"} + if reflect.DeepEqual(stringTreeDst, wantAnySlice) { + t.Errorf("StringTreeMatcher: Expected %v, got %v", wantAnySlice, stringTreeDst) + } + + if err := stringTreeFunc([]string{"a", "a", "b"}); err == nil { + t.Error("StringTreeMatcher: Expected error, got nil") + } + + if err := stringTreeFunc(1); err == nil { + t.Error("StringTreeMatcher: Expected error, got nil") + } +} + +func TestTimeMatcher(t *testing.T) { + var timeDst time.Time + timeFunc := TimeMatcher(&timeDst, time.RFC3339) + if err := timeFunc("2024-03-18T12:57:48.236Z"); err != nil { + t.Error(err) + } + wantTime := time.Date(2024, time.March, 18, 12, 57, 48, 236_000_000, time.UTC) + if timeDst != wantTime { + t.Errorf("TimeMatcher: Expected %v, got %v", wantTime, timeDst) + } + + if err := timeFunc(""); err == nil { + t.Error("TimeMatcher: Expected error, got nil") + } + + if err := timeFunc(1); err == nil { + t.Error("TimeMatcher: Expected error, got nil") + } +} + +func TestPathEval_Extract(t *testing.T) { + pathEval := NewPathEval() + var result string + matcher := StringMatcher(&result) + if err := pathEval.Extract("foo", matcher, true, map[string]any{"foo": "bar"}); err != nil { + t.Error(err) + } + if result != "bar" { + t.Errorf("PathEval_Extract: Expected bar, got %v", result) + } +} + +func TestPathEval_Match(t *testing.T) { + var got string + doc := map[string]any{"foo": "bar"} + + pe := NewPathEval() + pem := PathEvalMatcher{Expr: "foo", Action: StringMatcher(&got)} + + if err := pe.Match([]PathEvalMatcher{pem}, doc); err != nil { + t.Error(err) + } + if got != "bar" { + t.Errorf("PathEval_Match: Expected bar, got %v", got) + } +} + +func TestPathEval_Strings(t *testing.T) { + pe := NewPathEval() + doc := map[string]any{"foo": "bar"} + want := []string{"bar"} + + got, err := pe.Strings([]string{"foo"}, true, doc) + if err != nil { + t.Error(err) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("PathEval_Strings: Expected %v, got %v", want, got) + } +} + +func TestAsStrings(t *testing.T) { + arg := []any{"foo", "bar"} + want := []string{"foo", "bar"} + + got, valid := AsStrings(arg) + if !valid { + t.Error("AsStrings: Expected true, got false") + } + if !reflect.DeepEqual(got, want) { + t.Errorf("AsStrings: Expected %v, got %v", want, got) + } +} diff --git a/util/set.go b/util/set.go index 0df693d..1a625da 100644 --- a/util/set.go +++ b/util/set.go @@ -1,12 +1,12 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2023 German Federal Office for Information Security (BSI) // Software-Engineering: 2023 Intevation GmbH -package util +package util //revive:disable-line:var-naming // Set is a simple set type. type Set[K comparable] map[K]struct{} diff --git a/util/set_test.go b/util/set_test.go new file mode 100644 index 0000000..a28878e --- /dev/null +++ b/util/set_test.go @@ -0,0 +1,65 @@ +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. +// +// SPDX-License-Identifier: Apache-2.0 +// +// SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) +// Software-Engineering: 2022 Intevation GmbH + +package util + +import ( + "reflect" + "sort" + "testing" +) + +func TestSet(t *testing.T) { + s := Set[int]{} + if s.Contains(0) { + t.Error("Set.Contains: Expected false got true") + } + s.Add(0) + if !s.Contains(0) { + t.Error("Set.Contains: Expected true got false") + } + + s0 := Set[int]{} + s1 := Set[int]{} + + s0.Add(0) + s0.Add(1) + + s1.Add(0) + s1.Add(1) + s1.Add(2) + + diff0 := s0.Difference(s1) + diff1 := s1.Difference(s0) + + if reflect.DeepEqual(diff0, diff1) { + t.Errorf("Set.Difference: %q and %q are different", diff0, diff1) + } + + if s0.ContainsAll(s1) { + t.Error("Set.ContainsAll: Expected false got true") + } + + if !s1.ContainsAll(s0) { + t.Error("Set.ContainsAll: Expected true got false") + } + + s2 := Set[int]{} + s2.Add(0) + s2.Add(1) + s2.Add(2) + s2.Add(3) + + wantKeys := []int{0, 1, 2, 3} + gotKeys := s2.Keys() + sort.Ints(gotKeys) + + if !reflect.DeepEqual(wantKeys, gotKeys) { + t.Errorf("Set.Keys: Expected %q got %q", wantKeys, gotKeys) + } +} diff --git a/util/url.go b/util/url.go index fb454f4..f59c078 100644 --- a/util/url.go +++ b/util/url.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) // Software-Engineering: 2022 Intevation GmbH diff --git a/util/url_test.go b/util/url_test.go new file mode 100644 index 0000000..bcf219e --- /dev/null +++ b/util/url_test.go @@ -0,0 +1,36 @@ +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. +// +// SPDX-License-Identifier: Apache-2.0 +// +// SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) +// Software-Engineering: 2022 Intevation GmbH + +package util //revive:disable-line:var-naming + +import ( + "net/url" + "testing" +) + +func TestBaseUrl(t *testing.T) { + for _, x := range [][2]string{ + {`http://example.com`, `http://example.com/`}, + {`scheme://example.com`, `scheme://example.com/`}, + {`https://example.com`, `https://example.com/`}, + {`https://example.com:8080/`, `https://example.com:8080/`}, + {`https://user@example.com:8080/`, `https://user@example.com:8080/`}, + {`https://user@example.com:8080/resource`, `https://user@example.com:8080/`}, + {`https://user@example.com:8080/resource/`, `https://user@example.com:8080/resource/`}, + {`https://user@example.com:8080/resource/#fragment`, `https://user@example.com:8080/resource/`}, + {`https://user@example.com:8080/resource/?query=test#fragment`, `https://user@example.com:8080/resource/`}, + } { + url, _ := url.Parse(x[0]) + if got, err := BaseURL(url); got != x[1] { + if err != nil { + t.Error(err) + } + t.Errorf("%q: Expected %q but got %q.", x[0], x[1], got) + } + } +} diff --git a/util/version.go b/util/version.go index 45d60b8..010f8cd 100644 --- a/util/version.go +++ b/util/version.go @@ -1,7 +1,7 @@ -// This file is Free Software under the MIT License -// without warranty, see README.md and LICENSES/MIT.txt for details. +// This file is Free Software under the Apache-2.0 License +// without warranty, see README.md and LICENSES/Apache-2.0.txt for details. // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // // SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) // Software-Engineering: 2022 Intevation GmbH