diff --git a/.github/workflows/documentation-build.yml b/.github/workflows/documentation-build.yml index c0d8090ba1..c53c87573f 100644 --- a/.github/workflows/documentation-build.yml +++ b/.github/workflows/documentation-build.yml @@ -43,7 +43,7 @@ jobs: distribution: temurin - name: Set up Gradle - uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 + uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 with: cache-read-only: false @@ -102,7 +102,7 @@ jobs: # ----------------------------------------- # Create pull request for documentation update # ----------------------------------------- - - name: Update documentation - Create pull request + - name: Update website and documentation - Create pull request if: (inputs.publish-documentation != '') && (github.ref_name == env.ACTIONS_SECHUB_DOC_RELEASE_BRANCH) id: pr_release_documentation uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f @@ -110,9 +110,9 @@ jobs: branch: release-documentation branch-suffix: short-commit-hash delete-branch: true - title: '1 - Release documentation [auto-generated]' + title: '1 - Release website and documentation [auto-generated]' body: | - Release of SecHub documentation + Release of SecHub website and documentation -> Please review before merge. diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 7e0e6c295f..3a2a60682c 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -44,7 +44,7 @@ jobs: distribution: temurin - name: Set up Gradle - uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 + uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 with: cache-read-only: false diff --git a/.github/workflows/publish-libraries.yml b/.github/workflows/publish-libraries.yml deleted file mode 100644 index 620ca4a50b..0000000000 --- a/.github/workflows/publish-libraries.yml +++ /dev/null @@ -1,104 +0,0 @@ -# SPDX-License-Identifier: MIT -name: Publish libraries - -on: - workflow_dispatch: - inputs: - libraries-version: - description: Libraries Version (e.g. 1.0.0) - required: true - milestone-number: - description: Milestone number for release(s) - default: 20 - required: true -jobs: - release-version: - name: Build, publish and release - runs-on: ubuntu-latest - steps: - - name: Checkout master - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - ref: master - # Create temporary local tags, so we build documentation for this tag... - # The final tag on git server side will be automatically done by the release step at the end - # automatically. - - name: "Temporary tag libraries version: v${{ github.event.inputs.libraries-version }}-libraries" - run: git tag v${{ github.event.inputs.libraries-version }}-libraries - - # Build - - name: Set up JDK 17 - uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b - with: - java-version: 17 - distribution: temurin - - - name: Set up Gradle - uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 - with: - cache-read-only: false - - - name: Build - run: ./gradlew clean build -x :sechub-integrationtest:test -x :sechub-cli:build - - - name: Create combined test report - if: always() - run: ./gradlew createCombinedTestReport - - # To identifiy parts not in git history and leading to "-dirty-$commitId" markers - - name: Inspect GIT status - if: always() - run: git status > build/reports/git-status.txt - - # ----------------------------------------- - # Upload Build Artifacts (only test output) - # ----------------------------------------- - - name: Archive combined test report - if: always() - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 - with: - name: combined-sechub-testreport - path: build/reports/combined-report - retention-days: 14 - - name: Archive GIT status - if: always() - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 - with: - name: git-status.txt - path: build/reports/git-status.txt - retention-days: 14 - - # ----------------------------------------- - # Assert releaseable, so no dirty flags on releases - # even when all artifact creation parts are done! - # ----------------------------------------- - - name: Assert releasable - run: ./gradlew assertReleaseable - - # ************************************************** - # Now let's create + publish a new LIBRARIES release - # ************************************************** - - # Publish to github packages - - name: Publish - run: ./gradlew publish - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token - - # Create release - - name: Create libraries release - id: create_libraries_release - uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token - with: - tag_name: v${{ github.event.inputs.libraries-version }}-libraries - commitish: master - release_name: Libraries Version ${{ github.event.inputs.libraries-version }} - body: | - New library artifacts can be found at https://github.com/mercedes-benz/sechub/packages - - For details about changes look at [Milestone ${{github.event.inputs.milestone-number}}]( https://github.com/mercedes-benz/sechub/milestone/${{github.event.inputs.milestone-number}}?closed=1) - draft: false - prerelease: false - diff --git a/.github/workflows/release-client-server-pds.yml b/.github/workflows/release-client-server-pds.yml index 1a8ffe5646..d509fe4877 100644 --- a/.github/workflows/release-client-server-pds.yml +++ b/.github/workflows/release-client-server-pds.yml @@ -100,7 +100,7 @@ jobs: distribution: temurin - name: Set up Gradle - uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 + uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 with: cache-read-only: false @@ -282,9 +282,9 @@ jobs: retention-days: 14 # ----------------------------------------- - # Update and commit release documentation for https://mercedes-benz.github.io/sechub/ + # Update and commit website and release documentation for https://mercedes-benz.github.io/sechub/ # ----------------------------------------- - - name: Update release documentation + - name: Update website and release documentation run: | git reset --hard sechub-doc/helperscripts/publish+git-add-releasedocs.sh @@ -293,16 +293,16 @@ jobs: # ----------------------------------------- # Create pull request for release documentation # ----------------------------------------- - - name: Create pull request for release documentation + - name: Create pull request for website and release documentation id: pr_release_documentation uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f with: branch: release-documentation branch-suffix: short-commit-hash delete-branch: true - title: '1 - Release documentation [auto-generated]' + title: '1 - Release website and documentation [auto-generated]' body: | - Release of SecHub documentation + Release of SecHub website and documentation -> Please review and merge **before** publishing the release. @@ -323,135 +323,53 @@ jobs: # ****************************************** # S E R V E R release # ****************************************** - - name: Create server release ${{ inputs.server-version }} - id: create_server_release - if: inputs.server-version != '' - uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token - with: - tag_name: v${{ inputs.server-version }}-server - commitish: master - release_name: Server Version ${{ inputs.server-version }} - body: | - Changes in this Release - - Some minor changes on SecHub server implementation - - For more details please look at [Milestone ${{inputs.server-milestone-number}}]( https://github.com/mercedes-benz/sechub/milestone/${{inputs.server-milestone-number}}?closed=1) - draft: true - prerelease: false - - - name: Create sha256 checksum file for SecHub server jar + - name: Prepare server ${{ inputs.server-version }} release artifacts if: inputs.server-version != '' + shell: bash run: | - cd sechub-server/build/libs - sha256sum sechub-server-${{ inputs.server-version }}.jar > sechub-server-${{ inputs.server-version }}.jar.sha256sum + mkdir server-release-artifacts + # Collect release artifacts + cp sechub-server/build/libs/sechub-server-${{ inputs.server-version }}.jar \ + sechub-developertools/build/libs/sechub-developer-admin-ui-${{ inputs.server-version }}.jar \ + server-release-artifacts/ + cp sechub-doc/build/docs/asciidoc/sechub-architecture.pdf \ + server-release-artifacts/sechub-architecture-${{ inputs.server-version }}.pdf + cp sechub-doc/build/docs/asciidoc/sechub-developer-quickstart-guide.pdf \ + server-release-artifacts/sechub-developer-quickstart-guide-${{ inputs.server-version }}.pdf + cp sechub-doc/build/docs/asciidoc/sechub-operations.pdf \ + server-release-artifacts/sechub-operations-${{ inputs.server-version }}.pdf + cp sechub-doc/build/docs/asciidoc/sechub-restapi.pdf \ + server-release-artifacts/sechub-restapi-${{ inputs.server-version }}.pdf + cp sechub-doc/build/api-spec/openapi3.json \ + server-release-artifacts/sechub-openapi3-${{ inputs.server-version }}.json + # Compute sha256 checksums for .jar files + cd server-release-artifacts + for i in *.jar ; do + sha256sum "$i" > "$i.sha256sum" + done - - name: Create sha256 checksum files for SecHub developer tools jars + - name: Create server ${{ inputs.server-version }} release draft if: inputs.server-version != '' + shell: bash run: | - cd sechub-developertools/build/libs/ - sha256sum sechub-developer-admin-ui-${{ inputs.server-version }}.jar > sechub-developer-admin-ui-${{ inputs.server-version }}.jar.sha256sum - - - name: Upload Server release asset sechub-server-${{ inputs.server-version }}.jar - if: inputs.server-version != '' - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_server_release.outputs.upload_url }} - asset_path: sechub-server/build/libs/sechub-server-${{ inputs.server-version }}.jar - asset_name: sechub-server-${{ inputs.server-version }}.jar - asset_content_type: application/zip - - - name: Upload Server release asset sechub-server-${{ inputs.server-version }}.jar.sha256sum - if: inputs.server-version != '' - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_server_release.outputs.upload_url }} - asset_path: sechub-server/build/libs/sechub-server-${{ inputs.server-version }}.jar.sha256sum - asset_name: sechub-server-${{ inputs.server-version }}.jar.sha256sum - asset_content_type: text/plain - - - name: Upload SecHub release asset sechub-developer-admin-ui-${{ inputs.server-version }}.jar - if: inputs.server-version != '' - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_server_release.outputs.upload_url }} - asset_path: sechub-developertools/build/libs/sechub-developer-admin-ui-${{ inputs.server-version }}.jar - asset_name: sechub-developer-admin-ui-${{ inputs.server-version }}.jar - asset_content_type: application/zip - - - name: Upload Server release asset sechub-developer-admin-ui-${{ inputs.server-version }}.jar.sha256sum - if: inputs.server-version != '' - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_server_release.outputs.upload_url }} - asset_path: sechub-developertools/build/libs/sechub-developer-admin-ui-${{ inputs.server-version }}.jar.sha256sum - asset_name: sechub-developer-admin-ui-${{ inputs.server-version }}.jar.sha256sum - asset_content_type: text/plain - - # Server documentation: - - name: Upload sechub-architecture.pdf release asset - if: inputs.server-version != '' - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_server_release.outputs.upload_url }} - asset_path: ./sechub-doc/build/docs/asciidoc/sechub-architecture.pdf - asset_name: sechub-architecture-${{ inputs.server-version }}.pdf - asset_content_type: application/pdf - - - name: Upload sechub-operations.pdf release asset - if: inputs.server-version != '' - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_server_release.outputs.upload_url }} - asset_path: ./sechub-doc/build/docs/asciidoc/sechub-operations.pdf - asset_name: sechub-operations-${{ inputs.server-version }}.pdf - asset_content_type: application/pdf - - - name: Upload sechub-developer-quickstart-guide.pdf release asset - if: inputs.server-version != '' - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_server_release.outputs.upload_url }} - asset_path: ./sechub-doc/build/docs/asciidoc/sechub-developer-quickstart-guide.pdf - asset_name: sechub-developer-quickstart-guide-${{ inputs.server-version }}.pdf - asset_content_type: application/pdf - - - name: Upload sechub-restapi.pdf release asset - if: inputs.server-version != '' - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_server_release.outputs.upload_url }} - asset_path: ./sechub-doc/build/docs/asciidoc/sechub-restapi.pdf - asset_name: sechub-restapi-${{ inputs.server-version }}.pdf - asset_content_type: application/pdf - - - name: Upload sechub-openapi3.json release asset - if: inputs.server-version != '' - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 + assets=() + echo "# Adding Server binaries and docs" + cd server-release-artifacts/ + for asset in * ; do + filename=`basename "$asset"` + echo "# - $filename" + assets+=("-a" "${asset}#${filename}") + done + # Define release data + tag_name="v${{ inputs.server-version }}-server" + release_title="Server Version ${{ inputs.server-version }}" + release_message="Changes in this Release + - Some minor changes on SecHub server implementation" + release_footer="For more details please look at [Milestone ${{inputs.server-milestone-number}}]( https://github.com/mercedes-benz/sechub/milestone/${{inputs.server-milestone-number}}?closed=1)" + echo "# Create release draft \"$release_title\" on github" + hub release create --draft "${assets[@]}" -m "$release_title" -m "$release_message" -m "$release_footer" "$tag_name" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_server_release.outputs.upload_url }} - asset_path: ./sechub-doc/build/api-spec/openapi3.json - asset_name: sechub-openapi3-${{ inputs.server-version }}.json - asset_content_type: text/plain - name: Create Server ${{ inputs.server-version }} release issue if: inputs.server-version != '' @@ -499,8 +417,8 @@ jobs: if: inputs.client-version != '' run: | cd sechub-cli/build/go - zip -r sechub-cli.zip platform - sha256sum sechub-cli.zip > sechub-cli.zip.sha256 + zip -r sechub-cli-${{ inputs.client-version }}.zip platform + sha256sum sechub-cli-${{ inputs.client-version }}.zip > sechub-cli-${{ inputs.client-version }}.zip.sha256 - name: Create client Debian packages if: inputs.client-version != '' @@ -512,16 +430,13 @@ jobs: shell: bash run: | assets=() - echo "# Add Client binaries sechub-cli-${{ inputs.client-version }}.zip + checksum" - assets+=("-a" "sechub-cli/build/go/sechub-cli.zip#sechub-cli-${{ inputs.client-version }}.zip") - assets+=("-a" "sechub-cli/build/go/sechub-cli.zip.sha256#sechub-cli-${{ inputs.client-version }}.zip.sha256") - echo "# Add Debian packages" - for asset in sechub-cli/build/deb-build/*.deb ; do + cp "sechub-doc/build/docs/asciidoc/sechub-client.pdf" sechub-client-${{ inputs.client-version }}.pdf + echo "# Adding Client binaries, docs and Debian packages" + for asset in sechub-cli/build/go/sechub-cli-${{ inputs.client-version }}.zip* sechub-client-${{ inputs.client-version }}.pdf sechub-cli/build/deb-build/*.deb ; do filename=`basename "$asset"` + echo "# - $filename" assets+=("-a" "${asset}#${filename}") done - echo "# Add Client documentation sechub-client-${{ inputs.client-version }}.pdf" - assets+=("-a" "sechub-doc/build/docs/asciidoc/sechub-client.pdf#sechub-client-${{ inputs.client-version }}.pdf") # Define release data tag_name="v${{ inputs.client-version }}-client" release_title="Client Version ${{ inputs.client-version }}" @@ -549,63 +464,44 @@ jobs: # ****************************************** # P D S release # ****************************************** - - name: Create PDS release ${{ inputs.pds-version }} - id: create_pds_release - if: inputs.pds-version != '' - uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token - with: - tag_name: v${{ inputs.pds-version }}-pds - commitish: master - release_name: PDS Version ${{ inputs.pds-version }} - body: | - Changes in this Release - - Some minor changes on PDS server implementation - - For more details please look at [Milestone ${{inputs.pds-milestone-number}}]( https://github.com/mercedes-benz/sechub/milestone/${{inputs.pds-milestone-number}}?closed=1) - draft: true - prerelease: false - - - name: Create sha256 checksum file for PDS jar + - name: Prepare PDS ${{ inputs.pds-version }} release artifacts if: inputs.pds-version != '' + shell: bash run: | - cd sechub-pds/build/libs/ - sha256sum sechub-pds-${{ inputs.pds-version }}.jar > sechub-pds-${{ inputs.pds-version }}.jar.sha256sum - - - name: Upload PDS release asset sechub-pds-${{ inputs.pds-version }}.jar - if: inputs.pds-version != '' - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_pds_release.outputs.upload_url }} - asset_path: sechub-pds/build/libs/sechub-pds-${{ inputs.pds-version }}.jar - asset_name: sechub-pds-${{ inputs.pds-version }}.jar - asset_content_type: application/zip - - - name: Upload PDS release asset sechub-pds-${{ inputs.pds-version }}.jar.sha256sum - if: inputs.pds-version != '' - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_pds_release.outputs.upload_url }} - asset_path: sechub-pds/build/libs/sechub-pds-${{ inputs.pds-version }}.jar.sha256sum - asset_name: sechub-pds-${{ inputs.pds-version }}.jar.sha256sum - asset_content_type: text/plain + mkdir pds-release-artifacts + # Collect release artifacts + cp sechub-pds/build/libs/sechub-pds-${{ inputs.pds-version }}.jar \ + pds-release-artifacts/ + cp sechub-doc/build/docs/asciidoc/sechub-product-delegation-server.pdf \ + pds-release-artifacts/sechub-product-delegation-server-${{ inputs.pds-version }}.pdf + # Compute sha256 checksums for .jar files + cd pds-release-artifacts + for i in *.jar ; do + sha256sum "$i" > "$i.sha256sum" + done - # sechub-product-delegation-server.pdf - - name: Upload PDS release asset sechub-product-delegation-server-${{ inputs.pds-version }}.pdf + - name: Create PDS ${{ inputs.pds-version }} release draft if: inputs.pds-version != '' - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 + shell: bash + run: | + assets=() + echo "# Adding PDS binaries and docs" + cd pds-release-artifacts/ + for asset in * ; do + filename=`basename "$asset"` + echo "# - $filename" + assets+=("-a" "${asset}#${filename}") + done + # Define release data + tag_name="v${{ inputs.pds-version }}-pds" + release_title="PDS Version ${{ inputs.pds-version }}" + release_message="Changes in this Release + - Some minor changes on PDS server implementation" + release_footer="For more details please look at [Milestone ${{inputs.pds-milestone-number}}]( https://github.com/mercedes-benz/sechub/milestone/${{inputs.pds-milestone-number}}?closed=1)" + echo "# Create release draft \"$release_title\" on github" + hub release create --draft "${assets[@]}" -m "$release_title" -m "$release_message" -m "$release_footer" "$tag_name" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_pds_release.outputs.upload_url }} - asset_path: ./sechub-doc/build/docs/asciidoc/sechub-product-delegation-server.pdf - asset_name: /sechub-product-delegation-server-${{ inputs.pds-version }}.pdf - asset_content_type: application/pdf - name: Create PDS ${{ inputs.pds-version }} release issue if: inputs.pds-version != '' diff --git a/.github/workflows/release-github-action.yml b/.github/workflows/release-github-action.yml index 779506d6d8..ec283598ad 100644 --- a/.github/workflows/release-github-action.yml +++ b/.github/workflows/release-github-action.yml @@ -48,6 +48,9 @@ jobs: # ---------------------- # Setup + Caching # ---------------------- + - name: Install required packages + run: sudo apt-get -y install hub + - name: Use Node.js uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af with: @@ -153,22 +156,19 @@ jobs: # ----------------------------------------- # Create draft release # ----------------------------------------- - - name: Create Github Action release - id: create_ghaction_release - uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e + - name: Create Github Action ${{ inputs.ghaction-version }} release draft + shell: bash + run: | + # Define release data + tag_name="v${{ inputs.ghaction-version }}-gha" + release_title="Github Action Version ${{ inputs.ghaction-version }}" + release_message="Changes in this Release + - Some minor changes on Github Action implementation" + release_footer="For more details please look at [Milestone ${{inputs.ghaction-milestone-number}}]( https://github.com/mercedes-benz/sechub/milestone/${{inputs.ghaction-milestone-number}}?closed=1)" + echo "# Create release draft \"$release_title\" on github" + hub release create --draft -m "$release_title" -m "$release_message" -m "$release_footer" "$tag_name" env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token - with: - tag_name: v${{ inputs.ghaction-version }}-gha - commitish: master - release_name: Github Action Version ${{ inputs.ghaction-version }} - body: | - Changes in this Release - - Some minor changes on Github Action implementation - - For more details please look at [Milestone ${{inputs.ghaction-milestone-number}}]( https://github.com/mercedes-benz/sechub/milestone/${{inputs.ghaction-milestone-number}}?closed=1) - draft: true - prerelease: false + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # ----------------------------------------- # Create a pull request for merging back `master` into `develop` diff --git a/.github/workflows/release-pds-tools.yml b/.github/workflows/release-pds-tools.yml index 6e3f89c9d3..f453c5c8fb 100644 --- a/.github/workflows/release-pds-tools.yml +++ b/.github/workflows/release-pds-tools.yml @@ -39,13 +39,15 @@ jobs: # Create temporary local tags, so we build documentation for this tag... # The final tag on git server side will be done by the release when the draft is saved as "real" release # automatically. - - name: "Temporary tag server version: v${{ inputs.pds-tools-version }}-pds-tools - if defined" - if: inputs.pds-tools-version != '' + - name: "Temporary tag server version: v${{ inputs.pds-tools-version }}-pds-tools" run: git tag v${{ inputs.pds-tools-version }}-pds-tools # ---------------------- # Setup + Caching # ---------------------- + - name: Install required packages + run: sudo apt-get -y install hub + - name: Set up JDK 17 uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b with: @@ -53,7 +55,7 @@ jobs: distribution: temurin - name: Set up Gradle - uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 + uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 with: cache-read-only: false @@ -91,7 +93,7 @@ jobs: echo "Pull Request URL - ${{ steps.pr_spdx_headers.outputs.pull-request-url }}" # ---------------------- - # SecHub PDS-Tools + # Build SecHub PDS-Tools # ---------------------- - name: Build Server, DAUI and generate OpenAPI file run: ./gradlew ensureLocalhostCertificate build generateOpenapi buildDeveloperAdminUI -x :sechub-integrationtest:test -x :sechub-cli:build @@ -135,51 +137,43 @@ jobs: git status ./gradlew assertReleaseable - - name: Create PDS-Tools release - id: create_pds_tools_release - if: inputs.pds-tools-version != '' - uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token - with: - tag_name: v${{ inputs.pds-tools-version }}-pds-tools - commitish: master - release_name: PDS-Tools Version ${{ inputs.pds-tools-version }} - body: | - Changes in this Release - - Some minor changes on PDS-Tools implementation - - For more details please look at [Milestone ${{inputs.pds-tools-milestone-number}}]( https://github.com/mercedes-benz/sechub/milestone/${{inputs.pds-tools-milestone-number}}?closed=1) - draft: true - prerelease: false - - - name: Create sha256 checksum file for PDS-Tools cli jar - if: inputs.pds-tools-version != '' + # ****************************************** + # P D S - T o o l s release + # ****************************************** + - name: Prepare PDS-Tools ${{ inputs.pds-tools-version }} release artifacts + shell: bash run: | - cd sechub-pds-tools/build/libs - sha256sum sechub-pds-tools-cli-${{ inputs.pds-tools-version }}.jar > sechub-pds-tools-cli-${{ inputs.pds-tools-version }}.jar.sha256sum - - - name: Upload PDS-Tools release asset sechub-pds-tools-cli-${{ inputs.pds-tools-version }}.jar - if: inputs.pds-tools-version != '' - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_pds_tools_release.outputs.upload_url }} - asset_path: sechub-pds-tools/build/libs/sechub-pds-tools-cli-${{ inputs.pds-tools-version }}.jar - asset_name: sechub-pds-tools-cli-${{ inputs.pds-tools-version }}.jar - asset_content_type: application/zip - - - name: Upload PDS-Tools release asset sechub-pds-tools-cli-${{ inputs.pds-tools-version }}.jar.sha256sum - if: inputs.pds-tools-version != '' - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 + mkdir pds-tools-release-artifacts + # Collect release artifacts + cp sechub-pds-tools/build/libs/sechub-pds-tools-cli-${{ inputs.pds-tools-version }}.jar \ + pds-tools-release-artifacts/ + # Compute sha256 checksums for .jar files + cd pds-tools-release-artifacts + for i in *.jar ; do + sha256sum "$i" > "$i.sha256sum" + done + + - name: Create PDS-Tools ${{ inputs.pds-tools-version }} release draft + shell: bash + run: | + assets=() + echo "# Adding PDS binaries and docs" + cd pds-tools-release-artifacts/ + for asset in * ; do + filename=`basename "$asset"` + echo "# - $filename" + assets+=("-a" "${asset}#${filename}") + done + # Define release data + tag_name="v${{ inputs.pds-tools-version }}-pds-tools" + release_title="PDS-Tools Version ${{ inputs.pds-tools-version }}" + release_message="Changes in this Release + - Some minor changes on PDS-Tools implementation" + release_footer="For more details please look at [Milestone ${{inputs.pds-tools-milestone-number}}]( https://github.com/mercedes-benz/sechub/milestone/${{inputs.pds-tools-milestone-number}}?closed=1)" + echo "# Create release draft \"$release_title\" on github" + hub release create --draft "${assets[@]}" -m "$release_title" -m "$release_message" -m "$release_footer" "$tag_name" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_pds_tools_release.outputs.upload_url }} - asset_path: sechub-pds-tools/build/libs/sechub-pds-tools-cli-${{ inputs.pds-tools-version }}.jar.sha256sum - asset_name: sechub-pds-tools-cli-${{ inputs.pds-tools-version }}.jar.sha256sum - asset_content_type: text/plain # ----------------------------------------- # Create release issue diff --git a/.github/workflows/release-web-server.yml b/.github/workflows/release-web-server.yml index cf8a939cdb..6ae3a0e0dc 100644 --- a/.github/workflows/release-web-server.yml +++ b/.github/workflows/release-web-server.yml @@ -1,6 +1,11 @@ # SPDX-License-Identifier: MIT name: Release Web Server +############################### +# D E P R E C A T E D ! ! +# +# web-server functionality will be integrated into SecHub server +############################### on: workflow_dispatch: inputs: @@ -64,7 +69,7 @@ jobs: distribution: temurin - name: Set up Gradle - uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 + uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 with: cache-read-only: false diff --git a/.github/workflows/release-web-ui.yml b/.github/workflows/release-web-ui.yml index d9e433f452..47a7a6bc53 100644 --- a/.github/workflows/release-web-ui.yml +++ b/.github/workflows/release-web-ui.yml @@ -37,18 +37,14 @@ jobs: echo "actor-email: '${{ inputs.actor-email }}'" echo "Web-UI '${{ inputs.web-ui-version }}' - Milestone '${{ inputs.web-ui-milestone-number }}'" - # Check inputs: - - name: "Verify Input for Web-UI release" - if: (inputs.web-ui-version == '') || (inputs.web-ui-milestone-number == '') - run: | - echo "For Web-UI release, web-ui-version and web-ui-milestone-number must be provided!" - exit 1 - - name: Checkout master uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: ref: master + - name: Install required packages + run: sudo apt-get -y install hub + # Create temporary local tag, so we build for this tag... # The final tag on git server side will be done automatically by the release when the draft is saved as "real" release - name: "Temporary tag server version: v${{ inputs.web-ui-version }}-web-ui" @@ -140,45 +136,33 @@ jobs: path: sechub-web-ui/dist/ retention-days: 7 - - name: Create Web-UI release - id: create_web-ui_release - uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own - with: - tag_name: v${{ inputs.web-ui-version }}-web-ui - commitish: master - release_name: web-ui frontend Version ${{ inputs.web-ui-version }} - body: | - Changes in this release: - - New shiny Web-UI features - - For more details please look at [Milestone ${{inputs.web-ui-milestone-number}}]( https://github.com/mercedes-benz/sechub/milestone/${{inputs.web-ui-milestone-number}}?closed=1) - draft: true - prerelease: false - - - name: Create sha256 checksum file for Web-UI zip file + # ****************************************** + # W e b - U I release + # ****************************************** + - name: Prepare PDS-Tools ${{ inputs.pds-tools-version }} release artifacts + shell: bash run: sha256sum $WEB_UI_RELEASE_ZIPFILE > $WEB_UI_RELEASE_ZIPFILE.sha256sum - - name: Upload Web-UI release asset ${{ env.WEB_UI_RELEASE_ZIPFILE }} - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_web-ui_release.outputs.upload_url }} - asset_path: ${{ env.WEB_UI_RELEASE_ZIPFILE }} - asset_name: ${{ env.WEB_UI_RELEASE_ZIPFILE }} - asset_content_type: application/zip - - - name: Upload Web-UI release asset ${{ env.WEB_UI_RELEASE_ZIPFILE }}.sha256sum - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 + - name: Create PDS-Tools ${{ inputs.pds-tools-version }} release draft + shell: bash + run: | + assets=() + echo "# Adding web-ui content" + for asset in $WEB_UI_RELEASE_ZIPFILE $WEB_UI_RELEASE_ZIPFILE.sha256sum ; do + filename=`basename "$asset"` + echo "# - $filename" + assets+=("-a" "${asset}#${filename}") + done + # Define release data + tag_name="v${{ inputs.web-ui-version }}-web-ui" + release_title="web-ui frontend Version ${{ inputs.web-ui-version }}" + release_message="Changes in this Release + - New shiny Web-UI features" + release_footer="For more details please look at [Milestone ${{inputs.web-ui-milestone-number}}]( https://github.com/mercedes-benz/sechub/milestone/${{inputs.web-ui-milestone-number}}?closed=1)" + echo "# Create release draft \"$release_title\" on github" + hub release create --draft "${assets[@]}" -m "$release_title" -m "$release_message" -m "$release_footer" "$tag_name" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_web-ui_release.outputs.upload_url }} - asset_path: ${{ env.WEB_UI_RELEASE_ZIPFILE }}.sha256sum - asset_name: ${{ env.WEB_UI_RELEASE_ZIPFILE }}.sha256sum - asset_content_type: text/plain # ----------------------------------------- # Create release issue diff --git a/.github/workflows/release-wrapper-checkmarx.yml b/.github/workflows/release-wrapper-checkmarx.yml index cc34a01bc2..f20e6f83f8 100644 --- a/.github/workflows/release-wrapper-checkmarx.yml +++ b/.github/workflows/release-wrapper-checkmarx.yml @@ -36,6 +36,9 @@ jobs: # ---------------------- # Setup + Caching # ---------------------- + - name: Install required packages + run: sudo apt-get -y install hub + - name: Set up JDK 17 uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b with: @@ -43,7 +46,7 @@ jobs: distribution: temurin - name: Set up Gradle - uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 + uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 with: cache-read-only: false @@ -124,50 +127,36 @@ jobs: - name: Assert releasable run: ./gradlew assertReleaseable - - name: Create Checkmarx Wrapper release - id: create_checkmarx-wrapper_release - uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token - with: - tag_name: v${{ inputs.checkmarx-wrapper-version }}-checkmarx-wrapper - commitish: master - release_name: Checkmarx Wrapper Version ${{ inputs.checkmarx-wrapper-version }} - body: | - Changes in this Release - - Some minor changes on Checkmarx Wrapper implementation - - For more details please look at [Milestone ${{inputs.checkmarx-wrapper-milestone-number}}]( https://github.com/mercedes-benz/sechub/milestone/${{inputs.checkmarx-wrapper-milestone-number}}?closed=1) - draft: true - prerelease: false - - # ----------------------------------------- - # Upload release artifacts - # ----------------------------------------- - - name: Create files and sha256 checksum for Checkmarx Wrapper jar + # ****************************************** + # Checkmarx Wrapper release + # ****************************************** + - name: Prepare Checkmarx Wrapper ${{ inputs.checkmarx-wrapper-version }} release artifacts + shell: bash run: | cd sechub-wrapper-checkmarx/build/libs/ sha256sum sechub-wrapper-checkmarx-${{ inputs.checkmarx-wrapper-version }}.jar > sechub-wrapper-checkmarx-${{ inputs.checkmarx-wrapper-version }}.jar.sha256sum - - name: Upload asset sechub-wrapper-checkmarx-${{ inputs.checkmarx-wrapper-version }}.jar - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_checkmarx-wrapper_release.outputs.upload_url }} - asset_path: sechub-wrapper-checkmarx/build/libs/sechub-wrapper-checkmarx-${{ inputs.checkmarx-wrapper-version }}.jar - asset_name: sechub-wrapper-checkmarx-${{ inputs.checkmarx-wrapper-version }}.jar - asset_content_type: application/zip - - - name: Upload asset sechub-wrapper-checkmarx-${{ inputs.checkmarx-wrapper-version }}.jar.sha256sum - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 + - name: Create Checkmarx Wrapper ${{ inputs.checkmarx-wrapper-version }} release draft + shell: bash + run: | + assets=() + echo "# Adding release artifacts" + cd sechub-wrapper-checkmarx/build/libs/ + for asset in sechub-wrapper-checkmarx-${{ inputs.checkmarx-wrapper-version }}.jar* ; do + filename=`basename "$asset"` + echo "# - $filename" + assets+=("-a" "${asset}#${filename}") + done + # Define release data + tag_name="v${{ inputs.checkmarx-wrapper-version }}-checkmarx-wrapper" + release_title="Checkmarx Wrapper Version ${{ inputs.checkmarx-wrapper-version }}" + release_message="Changes in this Release + - Some minor changes on Checkmarx Wrapper implementation" + release_footer="For more details please look at [Milestone ${{inputs.checkmarx-wrapper-milestone-number}}]( https://github.com/mercedes-benz/sechub/milestone/${{inputs.checkmarx-wrapper-milestone-number}}?closed=1)" + echo "# Create release draft \"$release_title\" on github" + hub release create --draft "${assets[@]}" -m "$release_title" -m "$release_message" -m "$release_footer" "$tag_name" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_checkmarx-wrapper_release.outputs.upload_url }} - asset_path: sechub-wrapper-checkmarx/build/libs/sechub-wrapper-checkmarx-${{ inputs.checkmarx-wrapper-version }}.jar.sha256sum - asset_name: sechub-wrapper-checkmarx-${{ inputs.checkmarx-wrapper-version }}.jar.sha256sum - asset_content_type: text/plain # ----------------------------------------- # Create release issue diff --git a/.github/workflows/release-wrapper-owaspzap.yml b/.github/workflows/release-wrapper-owaspzap.yml index a686be2f69..337c58c756 100644 --- a/.github/workflows/release-wrapper-owaspzap.yml +++ b/.github/workflows/release-wrapper-owaspzap.yml @@ -37,6 +37,9 @@ jobs: # ---------------------- # Setup + Caching # ---------------------- + - name: Install required packages + run: sudo apt-get -y install hub + - name: Set up JDK 17 uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b with: @@ -44,7 +47,7 @@ jobs: distribution: temurin - name: Set up Gradle - uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 + uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 with: cache-read-only: false @@ -125,50 +128,36 @@ jobs: - name: Assert releasable run: ./gradlew assertReleaseable - - name: Create OWASP-ZAP Wrapper release - id: create_owaspzap-wrapper_release - uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token - with: - tag_name: v${{ inputs.owaspzap-wrapper-version }}-owaspzap-wrapper - commitish: master - release_name: OWASP-ZAP Wrapper Version ${{ inputs.owaspzap-wrapper-version }} - body: | - Changes in this Release - - Some minor changes on OWASP-ZAP Wrapper implementation - - For more details please look at [Milestone ${{inputs.owaspzap-wrapper-milestone-number}}]( https://github.com/mercedes-benz/sechub/milestone/${{inputs.owaspzap-wrapper-milestone-number}}?closed=1) - draft: true - prerelease: false - - # ----------------------------------------- - # Upload release artifacts - # ----------------------------------------- - - name: Create files and sha256 checksum for OWASP-ZAP Wrapper jar + # ****************************************** + # OWASP-ZAP Wrapper release + # ****************************************** + - name: Prepare OWASP-ZAP Wrapper ${{ inputs.owaspzap-wrapper-version }} release artifacts + shell: bash run: | cd sechub-wrapper-owasp-zap/build/libs/ sha256sum sechub-pds-wrapperowaspzap-${{ inputs.owaspzap-wrapper-version }}.jar > sechub-pds-wrapperowaspzap-${{ inputs.owaspzap-wrapper-version }}.jar.sha256sum - - name: Upload asset sechub-pds-wrapperowaspzap-${{ inputs.owaspzap-wrapper-version }}.jar - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_owaspzap-wrapper_release.outputs.upload_url }} - asset_path: sechub-wrapper-owasp-zap/build/libs/sechub-pds-wrapperowaspzap-${{ inputs.owaspzap-wrapper-version }}.jar - asset_name: sechub-pds-wrapperowaspzap-${{ inputs.owaspzap-wrapper-version }}.jar - asset_content_type: application/zip - - - name: Upload asset sechub-pds-wrapperowaspzap-${{ inputs.owaspzap-wrapper-version }}.jar.sha256sum - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 + - name: Create OWASP-ZAP Wrapper ${{ inputs.owaspzap-wrapper-version }} release draft + shell: bash + run: | + assets=() + echo "# Adding release artifacts" + cd sechub-wrapper-owasp-zap/build/libs/ + for asset in sechub-pds-wrapperowaspzap-${{ inputs.owaspzap-wrapper-version }}.jar* ; do + filename=`basename "$asset"` + echo "# - $filename" + assets+=("-a" "${asset}#${filename}") + done + # Define release data + tag_name="v${{ inputs.owaspzap-wrapper-version }}-owaspzap-wrapper" + release_title="OWASP-ZAP Wrapper Version ${{ inputs.owaspzap-wrapper-version }}" + release_message="Changes in this Release + - Some minor changes on OWASP-ZAP Wrapper implementation" + release_footer="For more details please look at [Milestone ${{inputs.owaspzap-wrapper-milestone-number}}]( https://github.com/mercedes-benz/sechub/milestone/${{inputs.owaspzap-wrapper-milestone-number}}?closed=1)" + echo "# Create release draft \"$release_title\" on github" + hub release create --draft "${assets[@]}" -m "$release_title" -m "$release_message" -m "$release_footer" "$tag_name" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_owaspzap-wrapper_release.outputs.upload_url }} - asset_path: sechub-wrapper-owasp-zap/build/libs/sechub-pds-wrapperowaspzap-${{ inputs.owaspzap-wrapper-version }}.jar.sha256sum - asset_name: sechub-pds-wrapperowaspzap-${{ inputs.owaspzap-wrapper-version }}.jar.sha256sum - asset_content_type: text/plain # ----------------------------------------- # Create release issue diff --git a/.github/workflows/release-wrapper-prepare.yml b/.github/workflows/release-wrapper-prepare.yml index efba3f78d4..7dce62eba9 100644 --- a/.github/workflows/release-wrapper-prepare.yml +++ b/.github/workflows/release-wrapper-prepare.yml @@ -36,6 +36,9 @@ jobs: # ---------------------- # Setup + Caching # ---------------------- + - name: Install required packages + run: sudo apt-get -y install hub + - name: Set up JDK 17 uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b with: @@ -43,7 +46,7 @@ jobs: distribution: temurin - name: Set up Gradle - uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 + uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 with: cache-read-only: false @@ -124,50 +127,36 @@ jobs: - name: Assert releasable run: ./gradlew assertReleaseable - - name: Create Prepare Wrapper release - id: create_prepare-wrapper_release - uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token - with: - tag_name: v${{ inputs.prepare-wrapper-version }}-prepare-wrapper - commitish: master - release_name: Prepare Wrapper Version ${{ inputs.prepare-wrapper-version }} - body: | - Changes in this Release - - Some minor changes on Prepare Wrapper implementation - - For more details please look at [Milestone ${{inputs.prepare-wrapper-milestone-number}}]( https://github.com/mercedes-benz/sechub/milestone/${{inputs.prepare-wrapper-milestone-number}}?closed=1) - draft: true - prerelease: false - - # ----------------------------------------- - # Upload release artifacts - # ----------------------------------------- - - name: Create files and sha256 checksum for Prepare Wrapper jar + # ****************************************** + # Prepare Wrapper release + # ****************************************** + - name: Prepare Prepare Wrapper ${{ inputs.prepare-wrapper-version }} release artifacts + shell: bash run: | cd sechub-wrapper-prepare/build/libs/ sha256sum sechub-wrapper-prepare-${{ inputs.prepare-wrapper-version }}.jar > sechub-wrapper-prepare-${{ inputs.prepare-wrapper-version }}.jar.sha256sum - - name: Upload asset sechub-wrapper-prepare-${{ inputs.prepare-wrapper-version }}.jar - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_prepare-wrapper_release.outputs.upload_url }} - asset_path: sechub-wrapper-prepare/build/libs/sechub-wrapper-prepare-${{ inputs.prepare-wrapper-version }}.jar - asset_name: sechub-wrapper-prepare-${{ inputs.prepare-wrapper-version }}.jar - asset_content_type: application/zip - - - name: Upload asset sechub-wrapper-prepare-${{ inputs.prepare-wrapper-version }}.jar.sha256sum - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 + - name: Create Prepare Wrapper ${{ inputs.prepare-wrapper-version }} release draft + shell: bash + run: | + assets=() + echo "# Adding release artifacts" + cd sechub-wrapper-prepare/build/libs/ + for asset in sechub-wrapper-prepare-${{ inputs.prepare-wrapper-version }}.jar* ; do + filename=`basename "$asset"` + echo "# - $filename" + assets+=("-a" "${asset}#${filename}") + done + # Define release data + tag_name="v${{ inputs.prepare-wrapper-version }}-prepare-wrapper" + release_title="Prepare Wrapper Version ${{ inputs.prepare-wrapper-version }}" + release_message="Changes in this Release + - Some minor changes on Prepare Wrapper implementation" + release_footer="For more details please look at [Milestone ${{inputs.prepare-wrapper-milestone-number}}]( https://github.com/mercedes-benz/sechub/milestone/${{inputs.prepare-wrapper-milestone-number}}?closed=1)" + echo "# Create release draft \"$release_title\" on github" + hub release create --draft "${assets[@]}" -m "$release_title" -m "$release_message" -m "$release_footer" "$tag_name" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_prepare-wrapper_release.outputs.upload_url }} - asset_path: sechub-wrapper-prepare/build/libs/sechub-wrapper-prepare-${{ inputs.prepare-wrapper-version }}.jar.sha256sum - asset_name: sechub-wrapper-prepare-${{ inputs.prepare-wrapper-version }}.jar.sha256sum - asset_content_type: text/plain # ----------------------------------------- # Create release issue diff --git a/.github/workflows/release-wrapper-validation.yml b/.github/workflows/release-wrapper-validation.yml index 6db7e94a58..c4cca4d5ce 100644 --- a/.github/workflows/release-wrapper-validation.yml +++ b/.github/workflows/release-wrapper-validation.yml @@ -36,6 +36,9 @@ jobs: # ---------------------- # Setup + Caching # ---------------------- + - name: Install required packages + run: sudo apt-get -y install hub + - name: Set up JDK 17 uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b with: @@ -43,7 +46,7 @@ jobs: distribution: temurin - name: Set up Gradle - uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 + uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 with: cache-read-only: false @@ -124,50 +127,36 @@ jobs: - name: Assert releasable run: ./gradlew assertReleaseable - - name: Create Secret-Validation Wrapper release - id: create_secretvalidation-wrapper_release - uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token - with: - tag_name: v${{ inputs.secretvalidation-wrapper-version }}-secretvalidation-wrapper - commitish: master - release_name: Secret-Validation Wrapper Version ${{ inputs.secretvalidation-wrapper-version }} - body: | - Changes in this Release - - Some minor changes on Secret-Validation Wrapper implementation - - For more details please look at [Milestone ${{inputs.secretvalidation-wrapper-milestone-number}}]( https://github.com/mercedes-benz/sechub/milestone/${{inputs.secretvalidation-wrapper-milestone-number}}?closed=1) - draft: true - prerelease: false - - # ----------------------------------------- - # Upload release artifacts - # ----------------------------------------- - - name: Create files and sha256 checksum for Secret-Validation Wrapper jar + # ****************************************** + # Secret-Validation Wrapper release + # ****************************************** + - name: Prepare Secret-Validation Wrapper ${{ inputs.secretvalidation-wrapper-version }} release artifacts + shell: bash run: | cd sechub-wrapper-secretvalidation/build/libs/ sha256sum sechub-wrapper-secretvalidation-${{ inputs.secretvalidation-wrapper-version }}.jar > sechub-wrapper-secretvalidation-${{ inputs.secretvalidation-wrapper-version }}.jar.sha256sum - - name: Upload asset sechub-wrapper-secretvalidation-${{ inputs.secretvalidation-wrapper-version }}.jar - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_secretvalidation-wrapper_release.outputs.upload_url }} - asset_path: sechub-wrapper-secretvalidation/build/libs/sechub-wrapper-secretvalidation-${{ inputs.secretvalidation-wrapper-version }}.jar - asset_name: sechub-wrapper-secretvalidation-${{ inputs.secretvalidation-wrapper-version }}.jar - asset_content_type: application/zip - - - name: Upload asset sechub-wrapper-secretvalidation-${{ inputs.secretvalidation-wrapper-version }}.jar.sha256sum - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 + - name: Create Secret-Validation Wrapper ${{ inputs.secretvalidation-wrapper-version }} release draft + shell: bash + run: | + assets=() + echo "# Adding release artifacts" + cd sechub-wrapper-secretvalidation/build/libs/ + for asset in sechub-wrapper-secretvalidation-${{ inputs.secretvalidation-wrapper-version }}.jar* ; do + filename=`basename "$asset"` + echo "# - $filename" + assets+=("-a" "${asset}#${filename}") + done + # Define release data + tag_name="v${{ inputs.secretvalidation-wrapper-version }}-secretvalidation-wrapper" + release_title="Secret-Validation Wrapper Version ${{ inputs.secretvalidation-wrapper-version }}" + release_message="Changes in this Release + - Some minor changes on Secret-Validation Wrapper implementation" + release_footer="For more details please look at [Milestone ${{inputs.secretvalidation-wrapper-milestone-number}}]( https://github.com/mercedes-benz/sechub/milestone/${{inputs.secretvalidation-wrapper-milestone-number}}?closed=1)" + echo "# Create release draft \"$release_title\" on github" + hub release create --draft "${assets[@]}" -m "$release_title" -m "$release_message" -m "$release_footer" "$tag_name" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_secretvalidation-wrapper_release.outputs.upload_url }} - asset_path: sechub-wrapper-secretvalidation/build/libs/sechub-wrapper-secretvalidation-${{ inputs.secretvalidation-wrapper-version }}.jar.sha256sum - asset_name: sechub-wrapper-secretvalidation-${{ inputs.secretvalidation-wrapper-version }}.jar.sha256sum - asset_content_type: text/plain # ----------------------------------------- # Create release issue diff --git a/.github/workflows/release-wrapper-xray.yml b/.github/workflows/release-wrapper-xray.yml index c1eb53443f..8fe2274d15 100644 --- a/.github/workflows/release-wrapper-xray.yml +++ b/.github/workflows/release-wrapper-xray.yml @@ -36,6 +36,9 @@ jobs: # ---------------------- # Setup + Caching # ---------------------- + - name: Install required packages + run: sudo apt-get -y install hub + - name: Set up JDK 17 uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b with: @@ -43,7 +46,7 @@ jobs: distribution: temurin - name: Set up Gradle - uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 + uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 with: cache-read-only: false @@ -124,50 +127,36 @@ jobs: - name: Assert releasable run: ./gradlew assertReleaseable - - name: Create Xray Wrapper release - id: create_xray-wrapper_release - uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token - with: - tag_name: v${{ inputs.xray-wrapper-version }}-xray-wrapper - commitish: master - release_name: Xray Wrapper Version ${{ inputs.xray-wrapper-version }} - body: | - Changes in this Release - - Some minor changes on Xray Wrapper implementation - - For more details please look at [Milestone ${{inputs.xray-wrapper-milestone-number}}]( https://github.com/mercedes-benz/sechub/milestone/${{inputs.xray-wrapper-milestone-number}}?closed=1) - draft: true - prerelease: false - - # ----------------------------------------- - # Upload release artifacts - # ----------------------------------------- - - name: Create files and sha256 checksum for Xray Wrapper jar + # ****************************************** + # Xray Wrapper release + # ****************************************** + - name: Prepare Xray Wrapper ${{ inputs.xray-wrapper-version }} release artifacts + shell: bash run: | cd sechub-wrapper-xray/build/libs/ sha256sum sechub-pds-wrapper-xray-${{ inputs.xray-wrapper-version }}.jar > sechub-pds-wrapper-xray-${{ inputs.xray-wrapper-version }}.jar.sha256sum - - name: Upload asset sechub-pds-wrapper-xray-${{ inputs.xray-wrapper-version }}.jar - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_xray-wrapper_release.outputs.upload_url }} - asset_path: sechub-wrapper-xray/build/libs/sechub-pds-wrapper-xray-${{ inputs.xray-wrapper-version }}.jar - asset_name: sechub-pds-wrapper-xray-${{ inputs.xray-wrapper-version }}.jar - asset_content_type: application/zip - - - name: Upload asset sechub-pds-wrapper-xray-${{ inputs.xray-wrapper-version }}.jar.sha256sum - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 + - name: Create Xray Wrapper ${{ inputs.xray-wrapper-version }} release draft + shell: bash + run: | + assets=() + echo "# Adding release artifacts" + cd sechub-wrapper-xray/build/libs/ + for asset in sechub-pds-wrapper-xray-${{ inputs.xray-wrapper-version }}.jar* ; do + filename=`basename "$asset"` + echo "# - $filename" + assets+=("-a" "${asset}#${filename}") + done + # Define release data + tag_name="v${{ inputs.xray-wrapper-version }}-xray-wrapper" + release_title="Xray Wrapper Version ${{ inputs.xray-wrapper-version }}" + release_message="Changes in this Release + - Some minor changes on Xray Wrapper implementation" + release_footer="For more details please look at [Milestone ${{inputs.xray-wrapper-milestone-number}}]( https://github.com/mercedes-benz/sechub/milestone/${{inputs.xray-wrapper-milestone-number}}?closed=1)" + echo "# Create release draft \"$release_title\" on github" + hub release create --draft "${assets[@]}" -m "$release_title" -m "$release_message" -m "$release_footer" "$tag_name" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_xray-wrapper_release.outputs.upload_url }} - asset_path: sechub-wrapper-xray/build/libs/sechub-pds-wrapper-xray-${{ inputs.xray-wrapper-version }}.jar.sha256sum - asset_name: sechub-pds-wrapper-xray-${{ inputs.xray-wrapper-version }}.jar.sha256sum - asset_content_type: text/plain # ----------------------------------------- # Create release issue diff --git a/docs/200.html b/docs/200.html new file mode 100644 index 0000000000..767e5b16cc --- /dev/null +++ b/docs/200.html @@ -0,0 +1,12 @@ + + +SecHub + + + + + + + +
+ \ No newline at end of file diff --git a/docs/404.html b/docs/404.html new file mode 100644 index 0000000000..767e5b16cc --- /dev/null +++ b/docs/404.html @@ -0,0 +1,12 @@ + + +SecHub + + + + + + + +
+ \ No newline at end of file diff --git a/docs/_nuxt/B3oZ3Xfb.js b/docs/_nuxt/B3oZ3Xfb.js new file mode 100644 index 0000000000..f6120d65d5 --- /dev/null +++ b/docs/_nuxt/B3oZ3Xfb.js @@ -0,0 +1,23 @@ +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./CdjxlzT3.js","./error-404.DYxFu4PM.css","./CxV5zgZb.js","./error-500.PGmg907S.css"])))=>i.map(i=>d[i]); +/** +* @vue/shared v3.4.36 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**//*! #__NO_SIDE_EFFECTS__ */function _s(e,t){const n=new Set(e.split(","));return r=>n.has(r)}const ce={},Lt=[],Le=()=>{},Ha=()=>!1,dn=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),vs=e=>e.startsWith("onUpdate:"),_e=Object.assign,bs=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},La=Object.prototype.hasOwnProperty,Q=(e,t)=>La.call(e,t),K=Array.isArray,Nt=e=>or(e)==="[object Map]",Ei=e=>or(e)==="[object Set]",Z=e=>typeof e=="function",fe=e=>typeof e=="string",ht=e=>typeof e=="symbol",ie=e=>e!==null&&typeof e=="object",Ti=e=>(ie(e)||Z(e))&&Z(e.then)&&Z(e.catch),Si=Object.prototype.toString,or=e=>Si.call(e),Na=e=>or(e).slice(8,-1),Ci=e=>or(e)==="[object Object]",ws=e=>fe(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,jt=_s(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),ir=e=>{const t=Object.create(null);return n=>t[n]||(t[n]=e(n))},ja=/-(\w)/g,De=ir(e=>e.replace(ja,(t,n)=>n?n.toUpperCase():"")),Fa=/\B([A-Z])/g,kt=ir(e=>e.replace(Fa,"-$1").toLowerCase()),lr=ir(e=>e.charAt(0).toUpperCase()+e.slice(1)),Ar=ir(e=>e?`on${lr(e)}`:""),ft=(e,t)=>!Object.is(e,t),Rr=(e,...t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:r,value:n})},Da=e=>{const t=parseFloat(e);return isNaN(t)?e:t},Ri=e=>{const t=fe(e)?Number(e):NaN;return isNaN(t)?e:t};let ao;const Pi=()=>ao||(ao=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function ar(e){if(K(e)){const t={};for(let n=0;n{if(n){const r=n.split(Ba);r.length>1&&(t[r[0].trim()]=r[1].trim())}}),t}function cr(e){let t="";if(fe(e))t=e;else if(K(e))for(let n=0;n!!(e&&e.__v_isRef===!0),Je=e=>fe(e)?e:e==null?"":K(e)||ie(e)&&(e.toString===Si||!Z(e.toString))?Ii(e)?Je(e.value):JSON.stringify(e,Mi,2):String(e),Mi=(e,t)=>Ii(t)?Mi(e,t.value):Nt(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[r,s],o)=>(n[Pr(r,o)+" =>"]=s,n),{})}:Ei(t)?{[`Set(${t.size})`]:[...t.values()].map(n=>Pr(n))}:ht(t)?Pr(t):ie(t)&&!K(t)&&!Ci(t)?String(t):t,Pr=(e,t="")=>{var n;return ht(e)?`Symbol(${(n=e.description)!=null?n:t})`:e};/** +* @vue/reactivity v3.4.36 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let Fe;class Oi{constructor(t=!1){this.detached=t,this._active=!0,this.effects=[],this.cleanups=[],this.parent=Fe,!t&&Fe&&(this.index=(Fe.scopes||(Fe.scopes=[])).push(this)-1)}get active(){return this._active}run(t){if(this._active){const n=Fe;try{return Fe=this,t()}finally{Fe=n}}}on(){Fe=this}off(){Fe=this.parent}stop(t){if(this._active){let n,r;for(n=0,r=this.effects.length;n=4))break}this._dirtyLevel===1&&(this._dirtyLevel=0),gt()}return this._dirtyLevel>=4}set dirty(t){this._dirtyLevel=t?4:0}run(){if(this._dirtyLevel=0,!this.active)return this.fn();let t=ct,n=Ct;try{return ct=!0,Ct=this,this._runnings++,co(this),this.fn()}finally{uo(this),this._runnings--,Ct=n,ct=t}}stop(){this.active&&(co(this),uo(this),this.onStop&&this.onStop(),this.active=!1)}}function Ja(e){return e.value}function co(e){e._trackId++,e._depsLength=0}function uo(e){if(e.deps.length>e._depsLength){for(let t=e._depsLength;t{const n=new Map;return n.cleanup=e,n.computed=t,n},Wn=new WeakMap,At=Symbol(""),qr=Symbol("");function Pe(e,t,n){if(ct&&Ct){let r=Wn.get(e);r||Wn.set(e,r=new Map);let s=r.get(n);s||r.set(n,s=Fi(()=>r.delete(n))),Ni(Ct,s)}}function Ye(e,t,n,r,s,o){const i=Wn.get(e);if(!i)return;let l=[];if(t==="clear")l=[...i.values()];else if(n==="length"&&K(e)){const a=Number(r);i.forEach((f,u)=>{(u==="length"||!ht(u)&&u>=a)&&l.push(f)})}else switch(n!==void 0&&l.push(i.get(n)),t){case"add":K(e)?ws(n)&&l.push(i.get("length")):(l.push(i.get(At)),Nt(e)&&l.push(i.get(qr)));break;case"delete":K(e)||(l.push(i.get(At)),Nt(e)&&l.push(i.get(qr)));break;case"set":Nt(e)&&l.push(i.get(At));break}Es();for(const a of l)a&&ji(a,4);Ts()}function Xa(e,t){const n=Wn.get(e);return n&&n.get(t)}const Ya=_s("__proto__,__v_isRef,__isVue"),Di=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(ht)),fo=Qa();function Qa(){const e={};return["includes","indexOf","lastIndexOf"].forEach(t=>{e[t]=function(...n){const r=ee(this);for(let o=0,i=this.length;o{e[t]=function(...n){pt(),Es();const r=ee(this)[t].apply(this,n);return Ts(),gt(),r}}),e}function ec(e){ht(e)||(e=String(e));const t=ee(this);return Pe(t,"has",e),t.hasOwnProperty(e)}class Ui{constructor(t=!1,n=!1){this._isReadonly=t,this._isShallow=n}get(t,n,r){const s=this._isReadonly,o=this._isShallow;if(n==="__v_isReactive")return!s;if(n==="__v_isReadonly")return s;if(n==="__v_isShallow")return o;if(n==="__v_raw")return r===(s?o?hc:Ki:o?Wi:Vi).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(r)?t:void 0;const i=K(t);if(!s){if(i&&Q(fo,n))return Reflect.get(fo,n,r);if(n==="hasOwnProperty")return ec}const l=Reflect.get(t,n,r);return(ht(n)?Di.has(n):Ya(n))||(s||Pe(t,"get",n),o)?l:Ee(l)?i&&ws(n)?l:l.value:ie(l)?s?qi(l):Pt(l):l}}class Bi extends Ui{constructor(t=!1){super(!1,t)}set(t,n,r,s){let o=t[n];if(!this._isShallow){const a=dt(o);if(!Wt(r)&&!dt(r)&&(o=ee(o),r=ee(r)),!K(t)&&Ee(o)&&!Ee(r))return a?!1:(o.value=r,!0)}const i=K(t)&&ws(n)?Number(n)e,ur=e=>Reflect.getPrototypeOf(e);function bn(e,t,n=!1,r=!1){e=e.__v_raw;const s=ee(e),o=ee(t);n||(ft(t,o)&&Pe(s,"get",t),Pe(s,"get",o));const{has:i}=ur(s),l=r?Ss:n?Rs:sn;if(i.call(s,t))return l(e.get(t));if(i.call(s,o))return l(e.get(o));e!==s&&e.get(t)}function wn(e,t=!1){const n=this.__v_raw,r=ee(n),s=ee(e);return t||(ft(e,s)&&Pe(r,"has",e),Pe(r,"has",s)),e===s?n.has(e):n.has(e)||n.has(s)}function xn(e,t=!1){return e=e.__v_raw,!t&&Pe(ee(e),"iterate",At),Reflect.get(e,"size",e)}function ho(e,t=!1){!t&&!Wt(e)&&!dt(e)&&(e=ee(e));const n=ee(this);return ur(n).has.call(n,e)||(n.add(e),Ye(n,"add",e,e)),this}function po(e,t,n=!1){!n&&!Wt(t)&&!dt(t)&&(t=ee(t));const r=ee(this),{has:s,get:o}=ur(r);let i=s.call(r,e);i||(e=ee(e),i=s.call(r,e));const l=o.call(r,e);return r.set(e,t),i?ft(t,l)&&Ye(r,"set",e,t):Ye(r,"add",e,t),this}function go(e){const t=ee(this),{has:n,get:r}=ur(t);let s=n.call(t,e);s||(e=ee(e),s=n.call(t,e)),r&&r.call(t,e);const o=t.delete(e);return s&&Ye(t,"delete",e,void 0),o}function mo(){const e=ee(this),t=e.size!==0,n=e.clear();return t&&Ye(e,"clear",void 0,void 0),n}function En(e,t){return function(r,s){const o=this,i=o.__v_raw,l=ee(i),a=t?Ss:e?Rs:sn;return!e&&Pe(l,"iterate",At),i.forEach((f,u)=>r.call(s,a(f),a(u),o))}}function Tn(e,t,n){return function(...r){const s=this.__v_raw,o=ee(s),i=Nt(o),l=e==="entries"||e===Symbol.iterator&&i,a=e==="keys"&&i,f=s[e](...r),u=n?Ss:t?Rs:sn;return!t&&Pe(o,"iterate",a?qr:At),{next(){const{value:c,done:d}=f.next();return d?{value:c,done:d}:{value:l?[u(c[0]),u(c[1])]:u(c),done:d}},[Symbol.iterator](){return this}}}}function nt(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function oc(){const e={get(o){return bn(this,o)},get size(){return xn(this)},has:wn,add:ho,set:po,delete:go,clear:mo,forEach:En(!1,!1)},t={get(o){return bn(this,o,!1,!0)},get size(){return xn(this)},has:wn,add(o){return ho.call(this,o,!0)},set(o,i){return po.call(this,o,i,!0)},delete:go,clear:mo,forEach:En(!1,!0)},n={get(o){return bn(this,o,!0)},get size(){return xn(this,!0)},has(o){return wn.call(this,o,!0)},add:nt("add"),set:nt("set"),delete:nt("delete"),clear:nt("clear"),forEach:En(!0,!1)},r={get(o){return bn(this,o,!0,!0)},get size(){return xn(this,!0)},has(o){return wn.call(this,o,!0)},add:nt("add"),set:nt("set"),delete:nt("delete"),clear:nt("clear"),forEach:En(!0,!0)};return["keys","values","entries",Symbol.iterator].forEach(o=>{e[o]=Tn(o,!1,!1),n[o]=Tn(o,!0,!1),t[o]=Tn(o,!1,!0),r[o]=Tn(o,!0,!0)}),[e,n,t,r]}const[ic,lc,ac,cc]=oc();function Cs(e,t){const n=t?e?cc:ac:e?lc:ic;return(r,s,o)=>s==="__v_isReactive"?!e:s==="__v_isReadonly"?e:s==="__v_raw"?r:Reflect.get(Q(n,s)&&s in r?n:r,s,o)}const uc={get:Cs(!1,!1)},fc={get:Cs(!1,!0)},dc={get:Cs(!0,!1)};const Vi=new WeakMap,Wi=new WeakMap,Ki=new WeakMap,hc=new WeakMap;function pc(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function gc(e){return e.__v_skip||!Object.isExtensible(e)?0:pc(Na(e))}function Pt(e){return dt(e)?e:As(e,!1,nc,uc,Vi)}function $t(e){return As(e,!1,sc,fc,Wi)}function qi(e){return As(e,!0,rc,dc,Ki)}function As(e,t,n,r,s){if(!ie(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;const o=s.get(e);if(o)return o;const i=gc(e);if(i===0)return e;const l=new Proxy(e,i===2?r:n);return s.set(e,l),l}function Ft(e){return dt(e)?Ft(e.__v_raw):!!(e&&e.__v_isReactive)}function dt(e){return!!(e&&e.__v_isReadonly)}function Wt(e){return!!(e&&e.__v_isShallow)}function Gi(e){return e?!!e.__v_raw:!1}function ee(e){const t=e&&e.__v_raw;return t?ee(t):e}function mc(e){return Object.isExtensible(e)&&Ai(e,"__v_skip",!0),e}const sn=e=>ie(e)?Pt(e):e,Rs=e=>ie(e)?qi(e):e;class Zi{constructor(t,n,r,s){this.getter=t,this._setter=n,this.dep=void 0,this.__v_isRef=!0,this.__v_isReadonly=!1,this.effect=new xs(()=>t(this._value),()=>Mn(this,this.effect._dirtyLevel===2?2:3)),this.effect.computed=this,this.effect.active=this._cacheable=!s,this.__v_isReadonly=r}get value(){const t=ee(this);return(!t._cacheable||t.effect.dirty)&&ft(t._value,t._value=t.effect.run())&&Mn(t,4),zi(t),t.effect._dirtyLevel>=2&&Mn(t,2),t._value}set value(t){this._setter(t)}get _dirty(){return this.effect.dirty}set _dirty(t){this.effect.dirty=t}}function yc(e,t,n=!1){let r,s;const o=Z(e);return o?(r=e,s=Le):(r=e.get,s=e.set),new Zi(r,s,o||!s,n)}function zi(e){var t;ct&&Ct&&(e=ee(e),Ni(Ct,(t=e.dep)!=null?t:e.dep=Fi(()=>e.dep=void 0,e instanceof Zi?e:void 0)))}function Mn(e,t=4,n,r){e=ee(e);const s=e.dep;s&&ji(s,t)}function Ee(e){return!!(e&&e.__v_isRef===!0)}function ae(e){return Ji(e,!1)}function yo(e){return Ji(e,!0)}function Ji(e,t){return Ee(e)?e:new _c(e,t)}class _c{constructor(t,n){this.__v_isShallow=n,this.dep=void 0,this.__v_isRef=!0,this._rawValue=n?t:ee(t),this._value=n?t:sn(t)}get value(){return zi(this),this._value}set value(t){const n=this.__v_isShallow||Wt(t)||dt(t);t=n?t:ee(t),ft(t,this._rawValue)&&(this._rawValue,this._rawValue=t,this._value=n?t:sn(t),Mn(this,4))}}function X(e){return Ee(e)?e.value:e}const vc={get:(e,t,n)=>X(Reflect.get(e,t,n)),set:(e,t,n,r)=>{const s=e[t];return Ee(s)&&!Ee(n)?(s.value=n,!0):Reflect.set(e,t,n,r)}};function Xi(e){return Ft(e)?e:new Proxy(e,vc)}class bc{constructor(t,n,r){this._object=t,this._key=n,this._defaultValue=r,this.__v_isRef=!0}get value(){const t=this._object[this._key];return t===void 0?this._defaultValue:t}set value(t){this._object[this._key]=t}get dep(){return Xa(ee(this._object),this._key)}}class wc{constructor(t){this._getter=t,this.__v_isRef=!0,this.__v_isReadonly=!0}get value(){return this._getter()}}function xc(e,t,n){return Ee(e)?e:Z(e)?new wc(e):ie(e)&&arguments.length>1?Ec(e,t,n):ae(e)}function Ec(e,t,n){const r=e[t];return Ee(r)?r:new bc(e,t,n)}/** +* @vue/runtime-core v3.4.36 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function ut(e,t,n,r){try{return r?e(...r):e()}catch(s){Gt(s,t,n)}}function Ne(e,t,n,r){if(Z(e)){const s=ut(e,t,n,r);return s&&Ti(s)&&s.catch(o=>{Gt(o,t,n)}),s}if(K(e)){const s=[];for(let o=0;o>>1,s=xe[r],o=ln(s);oKe&&xe.splice(t,1)}function Zr(e){K(e)?Dt.push(...e):(!it||!it.includes(e,e.allowRecurse?Et+1:Et))&&Dt.push(e),Qi()}function _o(e,t,n=on?Ke+1:0){for(;nln(n)-ln(r));if(Dt.length=0,it){it.push(...t);return}for(it=t,Et=0;Ete.id==null?1/0:e.id,Ac=(e,t)=>{const n=ln(e)-ln(t);if(n===0){if(e.pre&&!t.pre)return-1;if(t.pre&&!e.pre)return 1}return n};function el(e){Gr=!1,on=!0,xe.sort(Ac);try{for(Ke=0;Ke{r._d&&Mo(-1);const o=qn(t);let i;try{i=e(...s)}finally{qn(o),r._d&&Mo(1)}return i};return r._n=!0,r._c=!0,r._d=!0,r}function We(e,t,n,r){const s=e.dirs,o=t&&t.dirs;for(let i=0;i{e.isMounted=!0}),pr(()=>{e.isUnmounting=!0}),e}const $e=[Function,Array],tl={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:$e,onEnter:$e,onAfterEnter:$e,onEnterCancelled:$e,onBeforeLeave:$e,onLeave:$e,onAfterLeave:$e,onLeaveCancelled:$e,onBeforeAppear:$e,onAppear:$e,onAfterAppear:$e,onAppearCancelled:$e},nl=e=>{const t=e.subTree;return t.component?nl(t.component):t},Pc={name:"BaseTransition",props:tl,setup(e,{slots:t}){const n=Ns(),r=Rc();return()=>{const s=t.default&&sl(t.default(),!0);if(!s||!s.length)return;let o=s[0];if(s.length>1){for(const d of s)if(d.type!==Ae){o=d;break}}const i=ee(e),{mode:l}=i;if(r.isLeaving)return kr(o);const a=vo(o);if(!a)return kr(o);let f=zr(a,i,r,n,d=>f=d);Gn(a,f);const u=n.subTree,c=u&&vo(u);if(c&&c.type!==Ae&&!qe(a,c)&&nl(n).type!==Ae){const d=zr(c,i,r,n);if(Gn(c,d),l==="out-in"&&a.type!==Ae)return r.isLeaving=!0,d.afterLeave=()=>{r.isLeaving=!1,n.update.active!==!1&&(n.effect.dirty=!0,n.update())},kr(o);l==="in-out"&&a.type!==Ae&&(d.delayLeave=(p,m,_)=>{const A=rl(r,c);A[String(c.key)]=c,p[lt]=()=>{m(),p[lt]=void 0,delete f.delayedLeave},f.delayedLeave=_})}return o}}},kc=Pc;function rl(e,t){const{leavingVNodes:n}=e;let r=n.get(t.type);return r||(r=Object.create(null),n.set(t.type,r)),r}function zr(e,t,n,r,s){const{appear:o,mode:i,persisted:l=!1,onBeforeEnter:a,onEnter:f,onAfterEnter:u,onEnterCancelled:c,onBeforeLeave:d,onLeave:p,onAfterLeave:m,onLeaveCancelled:_,onBeforeAppear:A,onAppear:P,onAfterAppear:H,onAppearCancelled:y}=t,S=String(e.key),T=rl(n,e),w=(O,I)=>{O&&Ne(O,r,9,I)},B=(O,I)=>{const q=I[1];w(O,I),K(O)?O.every(R=>R.length<=1)&&q():O.length<=1&&q()},G={mode:i,persisted:l,beforeEnter(O){let I=a;if(!n.isMounted)if(o)I=A||a;else return;O[lt]&&O[lt](!0);const q=T[S];q&&qe(e,q)&&q.el[lt]&&q.el[lt](),w(I,[O])},enter(O){let I=f,q=u,R=c;if(!n.isMounted)if(o)I=P||f,q=H||u,R=y||c;else return;let V=!1;const te=O[Sn]=oe=>{V||(V=!0,oe?w(R,[O]):w(q,[O]),G.delayedLeave&&G.delayedLeave(),O[Sn]=void 0)};I?B(I,[O,te]):te()},leave(O,I){const q=String(e.key);if(O[Sn]&&O[Sn](!0),n.isUnmounting)return I();w(d,[O]);let R=!1;const V=O[lt]=te=>{R||(R=!0,I(),te?w(_,[O]):w(m,[O]),O[lt]=void 0,T[q]===e&&delete T[q])};T[q]=e,p?B(p,[O,V]):V()},clone(O){const I=zr(O,t,n,r,s);return s&&s(I),I}};return G}function kr(e){if(hn(e))return e=et(e),e.children=null,e}function vo(e){if(!hn(e))return e;const{shapeFlag:t,children:n}=e;if(n){if(t&16)return n[0];if(t&32&&Z(n.default))return n.default()}}function Gn(e,t){e.shapeFlag&6&&e.component?Gn(e.component.subTree,t):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function sl(e,t=!1,n){let r=[],s=0;for(let o=0;o1)for(let o=0;o!!e.type.__asyncLoader;/*! #__NO_SIDE_EFFECTS__ */function bo(e){Z(e)&&(e={loader:e});const{loader:t,loadingComponent:n,errorComponent:r,delay:s=200,timeout:o,suspensible:i=!0,onError:l}=e;let a=null,f,u=0;const c=()=>(u++,a=null,d()),d=()=>{let p;return a||(p=a=t().catch(m=>{if(m=m instanceof Error?m:new Error(String(m)),l)return new Promise((_,A)=>{l(m,()=>_(c()),()=>A(m),u+1)});throw m}).then(m=>p!==a&&a?a:(m&&(m.__esModule||m[Symbol.toStringTag]==="Module")&&(m=m.default),f=m,m)))};return Ue({name:"AsyncComponentWrapper",__asyncLoader:d,get __asyncResolved(){return f},setup(){const p=ge;if(f)return()=>Ir(f,p);const m=H=>{a=null,Gt(H,p,13,!r)};if(i&&p.suspense||mn)return d().then(H=>()=>Ir(H,p)).catch(H=>(m(H),()=>r?F(r,{error:H}):null));const _=ae(!1),A=ae(),P=ae(!!s);return s&&setTimeout(()=>{P.value=!1},s),o!=null&&setTimeout(()=>{if(!_.value&&!A.value){const H=new Error(`Async component timed out after ${o}ms.`);m(H),A.value=H}},o),d().then(()=>{_.value=!0,p.parent&&hn(p.parent.vnode)&&(p.parent.effect.dirty=!0,fr(p.parent.update))}).catch(H=>{m(H),A.value=H}),()=>{if(_.value&&f)return Ir(f,p);if(A.value&&r)return F(r,{error:A.value});if(n&&!P.value)return F(n)}}})}function Ir(e,t){const{ref:n,props:r,children:s,ce:o}=t.vnode,i=F(e,r,s);return i.ref=n,i.ce=o,delete t.vnode.ce,i}const hn=e=>e.type.__isKeepAlive;function ol(e,t){ll(e,"a",t)}function il(e,t){ll(e,"da",t)}function ll(e,t,n=ge){const r=e.__wdc||(e.__wdc=()=>{let s=n;for(;s;){if(s.isDeactivated)return;s=s.parent}return e()});if(hr(t,r,n),n){let s=n.parent;for(;s&&s.parent;)hn(s.parent.vnode)&&Ic(r,t,n,s),s=s.parent}}function Ic(e,t,n,r){const s=hr(t,e,r,!0);ks(()=>{bs(r[t],s)},n)}function hr(e,t,n=ge,r=!1){if(n){const s=n[e]||(n[e]=[]),o=t.__weh||(t.__weh=(...i)=>{pt();const l=gn(n),a=Ne(t,n,e,i);return l(),gt(),a});return r?s.unshift(o):s.push(o),o}}const tt=e=>(t,n=ge)=>{(!mn||e==="sp")&&hr(e,(...r)=>t(...r),n)},Mc=tt("bm"),pn=tt("m"),Oc=tt("bu"),$c=tt("u"),pr=tt("bum"),ks=tt("um"),Hc=tt("sp"),Lc=tt("rtg"),Nc=tt("rtc");function al(e,t=ge){hr("ec",e,t)}const cl="components";function wo(e,t){return dl(cl,e,!0,t)||e}const ul=Symbol.for("v-ndc");function fl(e){return fe(e)?dl(cl,e,!1)||e:e||ul}function dl(e,t,n=!0,r=!1){const s=Me||ge;if(s){const o=s.type;{const l=Mu(o,!1);if(l&&(l===t||l===De(t)||l===lr(De(t))))return o}const i=xo(s[e]||o[e],t)||xo(s.appContext[e],t);return!i&&r?o:i}}function xo(e,t){return e&&(e[t]||e[De(t)]||e[lr(De(t))])}function Ht(e,t,n,r){let s;const o=n;if(K(e)||fe(e)){s=new Array(e.length);for(let i=0,l=e.length;it(i,l,void 0,o));else{const i=Object.keys(e);s=new Array(i.length);for(let l=0,a=i.length;le?Nl(e)?js(e):Jr(e.parent):null,en=_e(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>Jr(e.parent),$root:e=>Jr(e.root),$emit:e=>e.emit,$options:e=>Is(e),$forceUpdate:e=>e.f||(e.f=()=>{e.effect.dirty=!0,fr(e.update)}),$nextTick:e=>e.n||(e.n=Ge.bind(e.proxy)),$watch:e=>au.bind(e)}),Mr=(e,t)=>e!==ce&&!e.__isScriptSetup&&Q(e,t),jc={get({_:e},t){if(t==="__v_skip")return!0;const{ctx:n,setupState:r,data:s,props:o,accessCache:i,type:l,appContext:a}=e;let f;if(t[0]!=="$"){const p=i[t];if(p!==void 0)switch(p){case 1:return r[t];case 2:return s[t];case 4:return n[t];case 3:return o[t]}else{if(Mr(r,t))return i[t]=1,r[t];if(s!==ce&&Q(s,t))return i[t]=2,s[t];if((f=e.propsOptions[0])&&Q(f,t))return i[t]=3,o[t];if(n!==ce&&Q(n,t))return i[t]=4,n[t];Xr&&(i[t]=0)}}const u=en[t];let c,d;if(u)return t==="$attrs"&&Pe(e.attrs,"get",""),u(e);if((c=l.__cssModules)&&(c=c[t]))return c;if(n!==ce&&Q(n,t))return i[t]=4,n[t];if(d=a.config.globalProperties,Q(d,t))return d[t]},set({_:e},t,n){const{data:r,setupState:s,ctx:o}=e;return Mr(s,t)?(s[t]=n,!0):r!==ce&&Q(r,t)?(r[t]=n,!0):Q(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(o[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:r,appContext:s,propsOptions:o}},i){let l;return!!n[i]||e!==ce&&Q(e,i)||Mr(t,i)||(l=o[0])&&Q(l,i)||Q(r,i)||Q(en,i)||Q(s.config.globalProperties,i)},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:Q(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};function Eo(e){return K(e)?e.reduce((t,n)=>(t[n]=null,t),{}):e}let Xr=!0;function Fc(e){const t=Is(e),n=e.proxy,r=e.ctx;Xr=!1,t.beforeCreate&&To(t.beforeCreate,e,"bc");const{data:s,computed:o,methods:i,watch:l,provide:a,inject:f,created:u,beforeMount:c,mounted:d,beforeUpdate:p,updated:m,activated:_,deactivated:A,beforeDestroy:P,beforeUnmount:H,destroyed:y,unmounted:S,render:T,renderTracked:w,renderTriggered:B,errorCaptured:G,serverPrefetch:O,expose:I,inheritAttrs:q,components:R,directives:V,filters:te}=t;if(f&&Dc(f,r,null),i)for(const J in i){const W=i[J];Z(W)&&(r[J]=W.bind(n))}if(s){const J=s.call(n,n);ie(J)&&(e.data=Pt(J))}if(Xr=!0,o)for(const J in o){const W=o[J],ve=Z(W)?W.bind(n,n):Z(W.get)?W.get.bind(n,n):Le,_n=!Z(W)&&Z(W.set)?W.set.bind(n):Le,_t=pe({get:ve,set:_n});Object.defineProperty(r,J,{enumerable:!0,configurable:!0,get:()=>_t.value,set:Be=>_t.value=Be})}if(l)for(const J in l)hl(l[J],r,n,J);if(a){const J=Z(a)?a.call(n):a;Reflect.ownKeys(J).forEach(W=>{gr(W,J[W])})}u&&To(u,e,"c");function j(J,W){K(W)?W.forEach(ve=>J(ve.bind(n))):W&&J(W.bind(n))}if(j(Mc,c),j(pn,d),j(Oc,p),j($c,m),j(ol,_),j(il,A),j(al,G),j(Nc,w),j(Lc,B),j(pr,H),j(ks,S),j(Hc,O),K(I))if(I.length){const J=e.exposed||(e.exposed={});I.forEach(W=>{Object.defineProperty(J,W,{get:()=>n[W],set:ve=>n[W]=ve})})}else e.exposed||(e.exposed={});T&&e.render===Le&&(e.render=T),q!=null&&(e.inheritAttrs=q),R&&(e.components=R),V&&(e.directives=V)}function Dc(e,t,n=Le){K(e)&&(e=Yr(e));for(const r in e){const s=e[r];let o;ie(s)?"default"in s?o=Qe(s.from||r,s.default,!0):o=Qe(s.from||r):o=Qe(s),Ee(o)?Object.defineProperty(t,r,{enumerable:!0,configurable:!0,get:()=>o.value,set:i=>o.value=i}):t[r]=o}}function To(e,t,n){Ne(K(e)?e.map(r=>r.bind(t.proxy)):e.bind(t.proxy),t,n)}function hl(e,t,n,r){const s=r.includes(".")?Pl(n,r):()=>n[r];if(fe(e)){const o=t[e];Z(o)&&On(s,o)}else if(Z(e))On(s,e.bind(n));else if(ie(e))if(K(e))e.forEach(o=>hl(o,t,n,r));else{const o=Z(e.handler)?e.handler.bind(n):t[e.handler];Z(o)&&On(s,o,e)}}function Is(e){const t=e.type,{mixins:n,extends:r}=t,{mixins:s,optionsCache:o,config:{optionMergeStrategies:i}}=e.appContext,l=o.get(t);let a;return l?a=l:!s.length&&!n&&!r?a=t:(a={},s.length&&s.forEach(f=>Zn(a,f,i,!0)),Zn(a,t,i)),ie(t)&&o.set(t,a),a}function Zn(e,t,n,r=!1){const{mixins:s,extends:o}=t;o&&Zn(e,o,n,!0),s&&s.forEach(i=>Zn(e,i,n,!0));for(const i in t)if(!(r&&i==="expose")){const l=Uc[i]||n&&n[i];e[i]=l?l(e[i],t[i]):t[i]}return e}const Uc={data:So,props:Co,emits:Co,methods:Yt,computed:Yt,beforeCreate:Se,created:Se,beforeMount:Se,mounted:Se,beforeUpdate:Se,updated:Se,beforeDestroy:Se,beforeUnmount:Se,destroyed:Se,unmounted:Se,activated:Se,deactivated:Se,errorCaptured:Se,serverPrefetch:Se,components:Yt,directives:Yt,watch:Vc,provide:So,inject:Bc};function So(e,t){return t?e?function(){return _e(Z(e)?e.call(this,this):e,Z(t)?t.call(this,this):t)}:t:e}function Bc(e,t){return Yt(Yr(e),Yr(t))}function Yr(e){if(K(e)){const t={};for(let n=0;n1)return n&&Z(t)?t.call(r&&r.proxy):t}}function gl(){return!!(ge||Me||Ut)}const ml={},yl=()=>Object.create(ml),_l=e=>Object.getPrototypeOf(e)===ml;function qc(e,t,n,r=!1){const s={},o=yl();e.propsDefaults=Object.create(null),vl(e,t,s,o);for(const i in e.propsOptions[0])i in s||(s[i]=void 0);n?e.props=r?s:$t(s):e.type.props?e.props=s:e.props=o,e.attrs=o}function Gc(e,t,n,r){const{props:s,attrs:o,vnode:{patchFlag:i}}=e,l=ee(s),[a]=e.propsOptions;let f=!1;if((r||i>0)&&!(i&16)){if(i&8){const u=e.vnode.dynamicProps;for(let c=0;c{a=!0;const[d,p]=bl(c,t,!0);_e(i,d),p&&l.push(...p)};!n&&t.mixins.length&&t.mixins.forEach(u),e.extends&&u(e.extends),e.mixins&&e.mixins.forEach(u)}if(!o&&!a)return ie(e)&&r.set(e,Lt),Lt;if(K(o))for(let u=0;ue[0]==="_"||e==="$stable",Ms=e=>K(e)?e.map(Ie):[Ie(e)],zc=(e,t,n)=>{if(t._n)return t;const r=se((...s)=>Ms(t(...s)),n);return r._c=!1,r},xl=(e,t,n)=>{const r=e._ctx;for(const s in e){if(wl(s))continue;const o=e[s];if(Z(o))t[s]=zc(s,o,r);else if(o!=null){const i=Ms(o);t[s]=()=>i}}},El=(e,t)=>{const n=Ms(t);e.slots.default=()=>n},Tl=(e,t,n)=>{for(const r in t)(n||r!=="_")&&(e[r]=t[r])},Jc=(e,t,n)=>{const r=e.slots=yl();if(e.vnode.shapeFlag&32){const s=t._;s?(Tl(r,t,n),n&&Ai(r,"_",s,!0)):xl(t,r)}else t&&El(e,t)},Xc=(e,t,n)=>{const{vnode:r,slots:s}=e;let o=!0,i=ce;if(r.shapeFlag&32){const l=t._;l?n&&l===1?o=!1:Tl(s,t,n):(o=!t.$stable,xl(t,s)),i=t}else t&&(El(e,t),i={default:1});if(o)for(const l in s)!wl(l)&&i[l]==null&&delete s[l]};function zn(e,t,n,r,s=!1){if(K(e)){e.forEach((d,p)=>zn(d,t&&(K(t)?t[p]:t),n,r,s));return}if(Qt(r)&&!s)return;const o=r.shapeFlag&4?js(r.component):r.el,i=s?null:o,{i:l,r:a}=e,f=t&&t.r,u=l.refs===ce?l.refs={}:l.refs,c=l.setupState;if(f!=null&&f!==a&&(fe(f)?(u[f]=null,Q(c,f)&&(c[f]=null)):Ee(f)&&(f.value=null)),Z(a))ut(a,l,12,[i,u]);else{const d=fe(a),p=Ee(a);if(d||p){const m=()=>{if(e.f){const _=d?Q(c,a)?c[a]:u[a]:a.value;s?K(_)&&bs(_,o):K(_)?_.includes(o)||_.push(o):d?(u[a]=[o],Q(c,a)&&(c[a]=u[a])):(a.value=[o],e.k&&(u[e.k]=a.value))}else d?(u[a]=i,Q(c,a)&&(c[a]=i)):p&&(a.value=i,e.k&&(u[e.k]=i))};i?(m.id=-1,Ce(m,n)):m()}}}const Yc=Symbol("_vte"),Qc=e=>e.__isTeleport;let Ro=!1;const Ot=()=>{Ro||(console.error("Hydration completed but contains mismatches."),Ro=!0)},eu=e=>e.namespaceURI.includes("svg")&&e.tagName!=="foreignObject",tu=e=>e.namespaceURI.includes("MathML"),Cn=e=>{if(eu(e))return"svg";if(tu(e))return"mathml"},An=e=>e.nodeType===8;function nu(e){const{mt:t,p:n,o:{patchProp:r,createText:s,nextSibling:o,parentNode:i,remove:l,insert:a,createComment:f}}=e,u=(y,S)=>{if(!S.hasChildNodes()){n(null,y,S),Kn(),S._vnode=y;return}c(S.firstChild,y,null,null,null),Kn(),S._vnode=y},c=(y,S,T,w,B,G=!1)=>{G=G||!!S.dynamicChildren;const O=An(y)&&y.data==="[",I=()=>_(y,S,T,w,B,O),{type:q,ref:R,shapeFlag:V,patchFlag:te}=S;let oe=y.nodeType;S.el=y,te===-2&&(G=!1,S.dynamicChildren=null);let j=null;switch(q){case Rt:oe!==3?S.children===""?(a(S.el=s(""),i(y),y),j=y):j=I():(y.data!==S.children&&(Ot(),y.data=S.children),j=o(y));break;case Ae:H(y)?(j=o(y),P(S.el=y.content.firstChild,y,T)):oe!==8||O?j=I():j=o(y);break;case tn:if(O&&(y=o(y),oe=y.nodeType),oe===1||oe===3){j=y;const J=!S.children.length;for(let W=0;W{G=G||!!S.dynamicChildren;const{type:O,props:I,patchFlag:q,shapeFlag:R,dirs:V,transition:te}=S,oe=O==="input"||O==="option";if(oe||q!==-1){V&&We(S,null,T,"created");let j=!1;if(H(y)){j=Cl(w,te)&&T&&T.vnode.props&&T.vnode.props.appear;const W=y.content.firstChild;j&&te.beforeEnter(W),P(W,y,T),S.el=y=W}if(R&16&&!(I&&(I.innerHTML||I.textContent))){let W=p(y.firstChild,S,y,T,w,B,G);for(;W;){Ot();const ve=W;W=W.nextSibling,l(ve)}}else R&8&&y.textContent!==S.children&&(Ot(),y.textContent=S.children);if(I){if(oe||!G||q&48){const W=y.tagName.includes("-");for(const ve in I)(oe&&(ve.endsWith("value")||ve==="indeterminate")||dn(ve)&&!jt(ve)||ve[0]==="."||W)&&r(y,ve,null,I[ve],void 0,T)}else if(I.onClick)r(y,"onClick",null,I.onClick,void 0,T);else if(q&4&&Ft(I.style))for(const W in I.style)I.style[W]}let J;(J=I&&I.onVnodeBeforeMount)&&He(J,T,S),V&&We(S,null,T,"beforeMount"),((J=I&&I.onVnodeMounted)||V||j)&&Ml(()=>{J&&He(J,T,S),j&&te.enter(y),V&&We(S,null,T,"mounted")},w)}return y.nextSibling},p=(y,S,T,w,B,G,O)=>{O=O||!!S.dynamicChildren;const I=S.children,q=I.length;for(let R=0;R{const{slotScopeIds:O}=S;O&&(B=B?B.concat(O):O);const I=i(y),q=p(o(y),S,I,T,w,B,G);return q&&An(q)&&q.data==="]"?o(S.anchor=q):(Ot(),a(S.anchor=f("]"),I,q),q)},_=(y,S,T,w,B,G)=>{if(Ot(),S.el=null,G){const q=A(y);for(;;){const R=o(y);if(R&&R!==q)l(R);else break}}const O=o(y),I=i(y);return l(y),n(null,S,I,O,T,w,Cn(I),B),O},A=(y,S="[",T="]")=>{let w=0;for(;y;)if(y=o(y),y&&An(y)&&(y.data===S&&w++,y.data===T)){if(w===0)return o(y);w--}return y},P=(y,S,T)=>{const w=S.parentNode;w&&w.replaceChild(y,S);let B=T;for(;B;)B.vnode.el===S&&(B.vnode.el=B.subTree.el=y),B=B.parent},H=y=>y.nodeType===1&&y.tagName.toLowerCase()==="template";return[u,c]}const Ce=Ml;function ru(e){return Sl(e)}function su(e){return Sl(e,nu)}function Sl(e,t){const n=Pi();n.__VUE__=!0;const{insert:r,remove:s,patchProp:o,createElement:i,createText:l,createComment:a,setText:f,setElementText:u,parentNode:c,nextSibling:d,setScopeId:p=Le,insertStaticContent:m}=e,_=(h,g,v,E=null,b=null,C=null,$=void 0,k=null,M=!!g.dynamicChildren)=>{if(h===g)return;h&&!qe(h,g)&&(E=vn(h),Be(h,b,C,!0),h=null),g.patchFlag===-2&&(M=!1,g.dynamicChildren=null);const{type:x,ref:L,shapeFlag:U}=g;switch(x){case Rt:A(h,g,v,E);break;case Ae:P(h,g,v,E);break;case tn:h==null&&H(g,v,E,$);break;case he:R(h,g,v,E,b,C,$,k,M);break;default:U&1?T(h,g,v,E,b,C,$,k,M):U&6?V(h,g,v,E,b,C,$,k,M):(U&64||U&128)&&x.process(h,g,v,E,b,C,$,k,M,It)}L!=null&&b&&zn(L,h&&h.ref,C,g||h,!g)},A=(h,g,v,E)=>{if(h==null)r(g.el=l(g.children),v,E);else{const b=g.el=h.el;g.children!==h.children&&f(b,g.children)}},P=(h,g,v,E)=>{h==null?r(g.el=a(g.children||""),v,E):g.el=h.el},H=(h,g,v,E)=>{[h.el,h.anchor]=m(h.children,g,v,E,h.el,h.anchor)},y=({el:h,anchor:g},v,E)=>{let b;for(;h&&h!==g;)b=d(h),r(h,v,E),h=b;r(g,v,E)},S=({el:h,anchor:g})=>{let v;for(;h&&h!==g;)v=d(h),s(h),h=v;s(g)},T=(h,g,v,E,b,C,$,k,M)=>{g.type==="svg"?$="svg":g.type==="math"&&($="mathml"),h==null?w(g,v,E,b,C,$,k,M):O(h,g,b,C,$,k,M)},w=(h,g,v,E,b,C,$,k)=>{let M,x;const{props:L,shapeFlag:U,transition:D,dirs:z}=h;if(M=h.el=i(h.type,C,L&&L.is,L),U&8?u(M,h.children):U&16&&G(h.children,M,null,E,b,Or(h,C),$,k),z&&We(h,null,E,"created"),B(M,h,h.scopeId,$,E),L){for(const le in L)le!=="value"&&!jt(le)&&o(M,le,null,L[le],C,E);"value"in L&&o(M,"value",null,L.value,C),(x=L.onVnodeBeforeMount)&&He(x,E,h)}z&&We(h,null,E,"beforeMount");const Y=Cl(b,D);Y&&D.beforeEnter(M),r(M,g,v),((x=L&&L.onVnodeMounted)||Y||z)&&Ce(()=>{x&&He(x,E,h),Y&&D.enter(M),z&&We(h,null,E,"mounted")},b)},B=(h,g,v,E,b)=>{if(v&&p(h,v),E)for(let C=0;C{for(let x=M;x{const k=g.el=h.el;let{patchFlag:M,dynamicChildren:x,dirs:L}=g;M|=h.patchFlag&16;const U=h.props||ce,D=g.props||ce;let z;if(v&&vt(v,!1),(z=D.onVnodeBeforeUpdate)&&He(z,v,g,h),L&&We(g,h,v,"beforeUpdate"),v&&vt(v,!0),(U.innerHTML&&D.innerHTML==null||U.textContent&&D.textContent==null)&&u(k,""),x?I(h.dynamicChildren,x,k,v,E,Or(g,b),C):$||W(h,g,k,null,v,E,Or(g,b),C,!1),M>0){if(M&16)q(k,U,D,v,b);else if(M&2&&U.class!==D.class&&o(k,"class",null,D.class,b),M&4&&o(k,"style",U.style,D.style,b),M&8){const Y=g.dynamicProps;for(let le=0;le{z&&He(z,v,g,h),L&&We(g,h,v,"updated")},E)},I=(h,g,v,E,b,C,$)=>{for(let k=0;k{if(g!==v){if(g!==ce)for(const C in g)!jt(C)&&!(C in v)&&o(h,C,g[C],null,b,E);for(const C in v){if(jt(C))continue;const $=v[C],k=g[C];$!==k&&C!=="value"&&o(h,C,k,$,b,E)}"value"in v&&o(h,"value",g.value,v.value,b)}},R=(h,g,v,E,b,C,$,k,M)=>{const x=g.el=h?h.el:l(""),L=g.anchor=h?h.anchor:l("");let{patchFlag:U,dynamicChildren:D,slotScopeIds:z}=g;z&&(k=k?k.concat(z):z),h==null?(r(x,v,E),r(L,v,E),G(g.children||[],v,L,b,C,$,k,M)):U>0&&U&64&&D&&h.dynamicChildren?(I(h.dynamicChildren,D,v,b,C,$,k),(g.key!=null||b&&g===b.subTree)&&Al(h,g,!0)):W(h,g,v,L,b,C,$,k,M)},V=(h,g,v,E,b,C,$,k,M)=>{g.slotScopeIds=k,h==null?g.shapeFlag&512?b.ctx.activate(g,v,E,$,M):te(g,v,E,b,C,$,M):oe(h,g,M)},te=(h,g,v,E,b,C,$)=>{const k=h.component=Au(h,E,b);if(hn(h)&&(k.ctx.renderer=It),Ru(k,!1,$),k.asyncDep){if(b&&b.registerDep(k,j,$),!h.el){const M=k.subTree=F(Ae);P(null,M,g,v)}}else j(k,h,g,v,b,C,$)},oe=(h,g,v)=>{const E=g.component=h.component;if(pu(h,g,v))if(E.asyncDep&&!E.asyncResolved){J(E,g,v);return}else E.next=g,Cc(E.update),E.effect.dirty=!0,E.update();else g.el=h.el,E.vnode=g},j=(h,g,v,E,b,C,$)=>{const k=()=>{if(h.isMounted){let{next:L,bu:U,u:D,parent:z,vnode:Y}=h;{const Mt=Rl(h);if(Mt){L&&(L.el=Y.el,J(h,L,$)),Mt.asyncDep.then(()=>{h.isUnmounted||k()});return}}let le=L,re;vt(h,!1),L?(L.el=Y.el,J(h,L,$)):L=Y,U&&Rr(U),(re=L.props&&L.props.onVnodeBeforeUpdate)&&He(re,z,L,Y),vt(h,!0);const ye=$r(h),je=h.subTree;h.subTree=ye,_(je,ye,c(je.el),vn(je),h,b,C),L.el=ye.el,le===null&&$s(h,ye.el),D&&Ce(D,b),(re=L.props&&L.props.onVnodeUpdated)&&Ce(()=>He(re,z,L,Y),b)}else{let L;const{el:U,props:D}=g,{bm:z,m:Y,parent:le}=h,re=Qt(g);if(vt(h,!1),z&&Rr(z),!re&&(L=D&&D.onVnodeBeforeMount)&&He(L,le,g),vt(h,!0),U&&Cr){const ye=()=>{h.subTree=$r(h),Cr(U,h.subTree,h,b,null)};re?g.type.__asyncLoader().then(()=>!h.isUnmounted&&ye()):ye()}else{const ye=h.subTree=$r(h);_(null,ye,v,E,h,b,C),g.el=ye.el}if(Y&&Ce(Y,b),!re&&(L=D&&D.onVnodeMounted)){const ye=g;Ce(()=>He(L,le,ye),b)}(g.shapeFlag&256||le&&Qt(le.vnode)&&le.vnode.shapeFlag&256)&&h.a&&Ce(h.a,b),h.isMounted=!0,g=v=E=null}},M=h.effect=new xs(k,Le,()=>fr(x),h.scope),x=h.update=()=>{M.dirty&&M.run()};x.i=h,x.id=h.uid,vt(h,!0),x()},J=(h,g,v)=>{g.component=h;const E=h.vnode.props;h.vnode=g,h.next=null,Gc(h,g.props,E,v),Xc(h,g.children,v),pt(),_o(h),gt()},W=(h,g,v,E,b,C,$,k,M=!1)=>{const x=h&&h.children,L=h?h.shapeFlag:0,U=g.children,{patchFlag:D,shapeFlag:z}=g;if(D>0){if(D&128){_n(x,U,v,E,b,C,$,k,M);return}else if(D&256){ve(x,U,v,E,b,C,$,k,M);return}}z&8?(L&16&&zt(x,b,C),U!==x&&u(v,U)):L&16?z&16?_n(x,U,v,E,b,C,$,k,M):zt(x,b,C,!0):(L&8&&u(v,""),z&16&&G(U,v,E,b,C,$,k,M))},ve=(h,g,v,E,b,C,$,k,M)=>{h=h||Lt,g=g||Lt;const x=h.length,L=g.length,U=Math.min(x,L);let D;for(D=0;DL?zt(h,b,C,!0,!1,U):G(g,v,E,b,C,$,k,M,U)},_n=(h,g,v,E,b,C,$,k,M)=>{let x=0;const L=g.length;let U=h.length-1,D=L-1;for(;x<=U&&x<=D;){const z=h[x],Y=g[x]=M?at(g[x]):Ie(g[x]);if(qe(z,Y))_(z,Y,v,null,b,C,$,k,M);else break;x++}for(;x<=U&&x<=D;){const z=h[U],Y=g[D]=M?at(g[D]):Ie(g[D]);if(qe(z,Y))_(z,Y,v,null,b,C,$,k,M);else break;U--,D--}if(x>U){if(x<=D){const z=D+1,Y=zD)for(;x<=U;)Be(h[x],b,C,!0),x++;else{const z=x,Y=x,le=new Map;for(x=Y;x<=D;x++){const ke=g[x]=M?at(g[x]):Ie(g[x]);ke.key!=null&&le.set(ke.key,x)}let re,ye=0;const je=D-Y+1;let Mt=!1,oo=0;const Jt=new Array(je);for(x=0;x=je){Be(ke,b,C,!0);continue}let Ve;if(ke.key!=null)Ve=le.get(ke.key);else for(re=Y;re<=D;re++)if(Jt[re-Y]===0&&qe(ke,g[re])){Ve=re;break}Ve===void 0?Be(ke,b,C,!0):(Jt[Ve-Y]=x+1,Ve>=oo?oo=Ve:Mt=!0,_(ke,g[Ve],v,null,b,C,$,k,M),ye++)}const io=Mt?ou(Jt):Lt;for(re=io.length-1,x=je-1;x>=0;x--){const ke=Y+x,Ve=g[ke],lo=ke+1{const{el:C,type:$,transition:k,children:M,shapeFlag:x}=h;if(x&6){_t(h.component.subTree,g,v,E);return}if(x&128){h.suspense.move(g,v,E);return}if(x&64){$.move(h,g,v,It);return}if($===he){r(C,g,v);for(let U=0;Uk.enter(C),b);else{const{leave:U,delayLeave:D,afterLeave:z}=k,Y=()=>r(C,g,v),le=()=>{U(C,()=>{Y(),z&&z()})};D?D(C,Y,le):le()}else r(C,g,v)},Be=(h,g,v,E=!1,b=!1)=>{const{type:C,props:$,ref:k,children:M,dynamicChildren:x,shapeFlag:L,patchFlag:U,dirs:D,cacheIndex:z}=h;if(U===-2&&(b=!1),k!=null&&zn(k,null,v,h,!0),z!=null&&(g.renderCache[z]=void 0),L&256){g.ctx.deactivate(h);return}const Y=L&1&&D,le=!Qt(h);let re;if(le&&(re=$&&$.onVnodeBeforeUnmount)&&He(re,g,h),L&6)$a(h.component,v,E);else{if(L&128){h.suspense.unmount(v,E);return}Y&&We(h,null,g,"beforeUnmount"),L&64?h.type.remove(h,g,v,It,E):x&&!x.hasOnce&&(C!==he||U>0&&U&64)?zt(x,g,v,!1,!0):(C===he&&U&384||!b&&L&16)&&zt(M,g,v),E&&ro(h)}(le&&(re=$&&$.onVnodeUnmounted)||Y)&&Ce(()=>{re&&He(re,g,h),Y&&We(h,null,g,"unmounted")},v)},ro=h=>{const{type:g,el:v,anchor:E,transition:b}=h;if(g===he){Oa(v,E);return}if(g===tn){S(h);return}const C=()=>{s(v),b&&!b.persisted&&b.afterLeave&&b.afterLeave()};if(h.shapeFlag&1&&b&&!b.persisted){const{leave:$,delayLeave:k}=b,M=()=>$(v,C);k?k(h.el,C,M):M()}else C()},Oa=(h,g)=>{let v;for(;h!==g;)v=d(h),s(h),h=v;s(g)},$a=(h,g,v)=>{const{bum:E,scope:b,update:C,subTree:$,um:k,m:M,a:x}=h;Po(M),Po(x),E&&Rr(E),b.stop(),C&&(C.active=!1,Be($,h,g,v)),k&&Ce(k,g),Ce(()=>{h.isUnmounted=!0},g),g&&g.pendingBranch&&!g.isUnmounted&&h.asyncDep&&!h.asyncResolved&&h.suspenseId===g.pendingId&&(g.deps--,g.deps===0&&g.resolve())},zt=(h,g,v,E=!1,b=!1,C=0)=>{for(let $=C;${if(h.shapeFlag&6)return vn(h.component.subTree);if(h.shapeFlag&128)return h.suspense.next();const g=d(h.anchor||h.el),v=g&&g[Yc];return v?d(v):g};let Tr=!1;const so=(h,g,v)=>{h==null?g._vnode&&Be(g._vnode,null,null,!0):_(g._vnode||null,h,g,null,null,null,v),Tr||(Tr=!0,_o(),Kn(),Tr=!1),g._vnode=h},It={p:_,um:Be,m:_t,r:ro,mt:te,mc:G,pc:W,pbc:I,n:vn,o:e};let Sr,Cr;return t&&([Sr,Cr]=t(It)),{render:so,hydrate:Sr,createApp:Kc(so,Sr)}}function Or({type:e,props:t},n){return n==="svg"&&e==="foreignObject"||n==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:n}function vt({effect:e,update:t},n){e.allowRecurse=t.allowRecurse=n}function Cl(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function Al(e,t,n=!1){const r=e.children,s=t.children;if(K(r)&&K(s))for(let o=0;o>1,e[n[l]]0&&(t[r]=n[o-1]),n[o]=r)}}for(o=n.length,i=n[o-1];o-- >0;)n[o]=i,i=t[i];return n}function Rl(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:Rl(t)}function Po(e){if(e)for(let t=0;tQe(iu);function Zt(e,t){return Os(e,null,t)}const Rn={};function On(e,t,n){return Os(e,t,n)}function Os(e,t,{immediate:n,deep:r,flush:s,once:o,onTrack:i,onTrigger:l}=ce){if(t&&o){const w=t;t=(...B)=>{w(...B),T()}}const a=ge,f=w=>r===!0?w:Tt(w,r===!1?1:void 0);let u,c=!1,d=!1;if(Ee(e)?(u=()=>e.value,c=Wt(e)):Ft(e)?(u=()=>f(e),c=!0):K(e)?(d=!0,c=e.some(w=>Ft(w)||Wt(w)),u=()=>e.map(w=>{if(Ee(w))return w.value;if(Ft(w))return f(w);if(Z(w))return ut(w,a,2)})):Z(e)?t?u=()=>ut(e,a,2):u=()=>(p&&p(),Ne(e,a,3,[m])):u=Le,t&&r){const w=u;u=()=>Tt(w())}let p,m=w=>{p=y.onStop=()=>{ut(w,a,4),p=y.onStop=void 0}},_;if(mn)if(m=Le,t?n&&Ne(t,a,3,[u(),d?[]:void 0,m]):u(),s==="sync"){const w=lu();_=w.__watcherHandles||(w.__watcherHandles=[])}else return Le;let A=d?new Array(e.length).fill(Rn):Rn;const P=()=>{if(!(!y.active||!y.dirty))if(t){const w=y.run();(r||c||(d?w.some((B,G)=>ft(B,A[G])):ft(w,A)))&&(p&&p(),Ne(t,a,3,[w,A===Rn?void 0:d&&A[0]===Rn?[]:A,m]),A=w)}else y.run()};P.allowRecurse=!!t;let H;s==="sync"?H=P:s==="post"?H=()=>Ce(P,a&&a.suspense):(P.pre=!0,a&&(P.id=a.uid),H=()=>fr(P));const y=new xs(u,Le,H),S=$i(),T=()=>{y.stop(),S&&bs(S.effects,y)};return t?n?P():A=y.run():s==="post"?Ce(y.run.bind(y),a&&a.suspense):y.run(),_&&_.push(T),T}function au(e,t,n){const r=this.proxy,s=fe(e)?e.includes(".")?Pl(r,e):()=>r[e]:e.bind(r,r);let o;Z(t)?o=t:(o=t.handler,n=t);const i=gn(this),l=Os(s,o.bind(r),n);return i(),l}function Pl(e,t){const n=t.split(".");return()=>{let r=e;for(let s=0;s{Tt(r,t,n)});else if(Ci(e)){for(const r in e)Tt(e[r],t,n);for(const r of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,r)&&Tt(e[r],t,n)}return e}const cu=(e,t)=>t==="modelValue"||t==="model-value"?e.modelModifiers:e[`${t}Modifiers`]||e[`${De(t)}Modifiers`]||e[`${kt(t)}Modifiers`];function uu(e,t,...n){if(e.isUnmounted)return;const r=e.vnode.props||ce;let s=n;const o=t.startsWith("update:"),i=o&&cu(r,t.slice(7));i&&(i.trim&&(s=n.map(u=>fe(u)?u.trim():u)),i.number&&(s=n.map(Da)));let l,a=r[l=Ar(t)]||r[l=Ar(De(t))];!a&&o&&(a=r[l=Ar(kt(t))]),a&&Ne(a,e,6,s);const f=r[l+"Once"];if(f){if(!e.emitted)e.emitted={};else if(e.emitted[l])return;e.emitted[l]=!0,Ne(f,e,6,s)}}function kl(e,t,n=!1){const r=t.emitsCache,s=r.get(e);if(s!==void 0)return s;const o=e.emits;let i={},l=!1;if(!Z(e)){const a=f=>{const u=kl(f,t,!0);u&&(l=!0,_e(i,u))};!n&&t.mixins.length&&t.mixins.forEach(a),e.extends&&a(e.extends),e.mixins&&e.mixins.forEach(a)}return!o&&!l?(ie(e)&&r.set(e,null),null):(K(o)?o.forEach(a=>i[a]=null):_e(i,o),ie(e)&&r.set(e,i),i)}function mr(e,t){return!e||!dn(t)?!1:(t=t.slice(2).replace(/Once$/,""),Q(e,t[0].toLowerCase()+t.slice(1))||Q(e,kt(t))||Q(e,t))}function $r(e){const{type:t,vnode:n,proxy:r,withProxy:s,propsOptions:[o],slots:i,attrs:l,emit:a,render:f,renderCache:u,props:c,data:d,setupState:p,ctx:m,inheritAttrs:_}=e,A=qn(e);let P,H;try{if(n.shapeFlag&4){const S=s||r,T=S;P=Ie(f.call(T,S,u,c,p,d,m)),H=l}else{const S=t;P=Ie(S.length>1?S(c,{attrs:l,slots:i,emit:a}):S(c,null)),H=t.props?l:du(l)}}catch(S){nn.length=0,Gt(S,e,1),P=F(Ae)}let y=P;if(H&&_!==!1){const S=Object.keys(H),{shapeFlag:T}=y;S.length&&T&7&&(o&&S.some(vs)&&(H=hu(H,o)),y=et(y,H,!1,!0))}return n.dirs&&(y=et(y,null,!1,!0),y.dirs=y.dirs?y.dirs.concat(n.dirs):n.dirs),n.transition&&(y.transition=n.transition),P=y,qn(A),P}function fu(e,t=!0){let n;for(let r=0;r{let t;for(const n in e)(n==="class"||n==="style"||dn(n))&&((t||(t={}))[n]=e[n]);return t},hu=(e,t)=>{const n={};for(const r in e)(!vs(r)||!(r.slice(9)in t))&&(n[r]=e[r]);return n};function pu(e,t,n){const{props:r,children:s,component:o}=e,{props:i,children:l,patchFlag:a}=t,f=o.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&a>=0){if(a&1024)return!0;if(a&16)return r?ko(r,i,f):!!i;if(a&8){const u=t.dynamicProps;for(let c=0;ce.__isSuspense;let es=0;const mu={name:"Suspense",__isSuspense:!0,process(e,t,n,r,s,o,i,l,a,f){if(e==null)_u(t,n,r,s,o,i,l,a,f);else{if(o&&o.deps>0&&!e.suspense.isInFallback){t.suspense=e.suspense,t.suspense.vnode=t,t.el=e.el;return}vu(e,t,n,r,s,i,l,a,f)}},hydrate:bu,normalize:wu},yu=mu;function an(e,t){const n=e.props&&e.props[t];Z(n)&&n()}function _u(e,t,n,r,s,o,i,l,a){const{p:f,o:{createElement:u}}=a,c=u("div"),d=e.suspense=Il(e,s,r,t,c,n,o,i,l,a);f(null,d.pendingBranch=e.ssContent,c,null,r,d,o,i),d.deps>0?(an(e,"onPending"),an(e,"onFallback"),f(null,e.ssFallback,t,n,r,null,o,i),Bt(d,e.ssFallback)):d.resolve(!1,!0)}function vu(e,t,n,r,s,o,i,l,{p:a,um:f,o:{createElement:u}}){const c=t.suspense=e.suspense;c.vnode=t,t.el=e.el;const d=t.ssContent,p=t.ssFallback,{activeBranch:m,pendingBranch:_,isInFallback:A,isHydrating:P}=c;if(_)c.pendingBranch=d,qe(d,_)?(a(_,d,c.hiddenContainer,null,s,c,o,i,l),c.deps<=0?c.resolve():A&&(P||(a(m,p,n,r,s,null,o,i,l),Bt(c,p)))):(c.pendingId=es++,P?(c.isHydrating=!1,c.activeBranch=_):f(_,s,c),c.deps=0,c.effects.length=0,c.hiddenContainer=u("div"),A?(a(null,d,c.hiddenContainer,null,s,c,o,i,l),c.deps<=0?c.resolve():(a(m,p,n,r,s,null,o,i,l),Bt(c,p))):m&&qe(d,m)?(a(m,d,n,r,s,c,o,i,l),c.resolve(!0)):(a(null,d,c.hiddenContainer,null,s,c,o,i,l),c.deps<=0&&c.resolve()));else if(m&&qe(d,m))a(m,d,n,r,s,c,o,i,l),Bt(c,d);else if(an(t,"onPending"),c.pendingBranch=d,d.shapeFlag&512?c.pendingId=d.component.suspenseId:c.pendingId=es++,a(null,d,c.hiddenContainer,null,s,c,o,i,l),c.deps<=0)c.resolve();else{const{timeout:H,pendingId:y}=c;H>0?setTimeout(()=>{c.pendingId===y&&c.fallback(p)},H):H===0&&c.fallback(p)}}function Il(e,t,n,r,s,o,i,l,a,f,u=!1){const{p:c,m:d,um:p,n:m,o:{parentNode:_,remove:A}}=f;let P;const H=xu(e);H&&t&&t.pendingBranch&&(P=t.pendingId,t.deps++);const y=e.props?Ri(e.props.timeout):void 0,S=o,T={vnode:e,parent:t,parentComponent:n,namespace:i,container:r,hiddenContainer:s,deps:0,pendingId:es++,timeout:typeof y=="number"?y:-1,activeBranch:null,pendingBranch:null,isInFallback:!u,isHydrating:u,isUnmounted:!1,effects:[],resolve(w=!1,B=!1){const{vnode:G,activeBranch:O,pendingBranch:I,pendingId:q,effects:R,parentComponent:V,container:te}=T;let oe=!1;T.isHydrating?T.isHydrating=!1:w||(oe=O&&I.transition&&I.transition.mode==="out-in",oe&&(O.transition.afterLeave=()=>{q===T.pendingId&&(d(I,te,o===S?m(O):o,0),Zr(R))}),O&&(_(O.el)!==T.hiddenContainer&&(o=m(O)),p(O,V,T,!0)),oe||d(I,te,o,0)),Bt(T,I),T.pendingBranch=null,T.isInFallback=!1;let j=T.parent,J=!1;for(;j;){if(j.pendingBranch){j.effects.push(...R),J=!0;break}j=j.parent}!J&&!oe&&Zr(R),T.effects=[],H&&t&&t.pendingBranch&&P===t.pendingId&&(t.deps--,t.deps===0&&!B&&t.resolve()),an(G,"onResolve")},fallback(w){if(!T.pendingBranch)return;const{vnode:B,activeBranch:G,parentComponent:O,container:I,namespace:q}=T;an(B,"onFallback");const R=m(G),V=()=>{T.isInFallback&&(c(null,w,I,R,O,null,q,l,a),Bt(T,w))},te=w.transition&&w.transition.mode==="out-in";te&&(G.transition.afterLeave=V),T.isInFallback=!0,p(G,O,null,!0),te||V()},move(w,B,G){T.activeBranch&&d(T.activeBranch,w,B,G),T.container=w},next(){return T.activeBranch&&m(T.activeBranch)},registerDep(w,B,G){const O=!!T.pendingBranch;O&&T.deps++;const I=w.vnode.el;w.asyncDep.catch(q=>{Gt(q,w,0)}).then(q=>{if(w.isUnmounted||T.isUnmounted||T.pendingId!==w.suspenseId)return;w.asyncResolved=!0;const{vnode:R}=w;ns(w,q,!1),I&&(R.el=I);const V=!I&&w.subTree.el;B(w,R,_(I||w.subTree.el),I?null:m(w.subTree),T,i,G),V&&A(V),$s(w,R.el),O&&--T.deps===0&&T.resolve()})},unmount(w,B){T.isUnmounted=!0,T.activeBranch&&p(T.activeBranch,n,w,B),T.pendingBranch&&p(T.pendingBranch,n,w,B)}};return T}function bu(e,t,n,r,s,o,i,l,a){const f=t.suspense=Il(t,r,n,e.parentNode,document.createElement("div"),null,s,o,i,l,!0),u=a(e,f.pendingBranch=t.ssContent,n,f,o,i);return f.deps===0&&f.resolve(!1,!0),u}function wu(e){const{shapeFlag:t,children:n}=e,r=t&32;e.ssContent=Io(r?n.default:n),e.ssFallback=r?Io(n.fallback):F(Ae)}function Io(e){let t;if(Z(e)){const n=Kt&&e._c;n&&(e._d=!1,ne()),e=e(),n&&(e._d=!0,t=Re,Ol())}return K(e)&&(e=fu(e)),e=Ie(e),t&&!e.dynamicChildren&&(e.dynamicChildren=t.filter(n=>n!==e)),e}function Ml(e,t){t&&t.pendingBranch?K(e)?t.effects.push(...e):t.effects.push(e):Zr(e)}function Bt(e,t){e.activeBranch=t;const{vnode:n,parentComponent:r}=e;let s=t.el;for(;!s&&t.component;)t=t.component.subTree,s=t.el;n.el=s,r&&r.subTree===n&&(r.vnode.el=s,$s(r,s))}function xu(e){const t=e.props&&e.props.suspensible;return t!=null&&t!==!1}const he=Symbol.for("v-fgt"),Rt=Symbol.for("v-txt"),Ae=Symbol.for("v-cmt"),tn=Symbol.for("v-stc"),nn=[];let Re=null;function ne(e=!1){nn.push(Re=e?null:[])}function Ol(){nn.pop(),Re=nn[nn.length-1]||null}let Kt=1;function Mo(e){Kt+=e,e<0&&Re&&(Re.hasOnce=!0)}function $l(e){return e.dynamicChildren=Kt>0?Re||Lt:null,Ol(),Kt>0&&Re&&Re.push(e),e}function ue(e,t,n,r,s,o){return $l(N(e,t,n,r,s,o,!0))}function St(e,t,n,r,s){return $l(F(e,t,n,r,s,!0))}function Jn(e){return e?e.__v_isVNode===!0:!1}function qe(e,t){return e.type===t.type&&e.key===t.key}const Hl=({key:e})=>e??null,$n=({ref:e,ref_key:t,ref_for:n})=>(typeof e=="number"&&(e=""+e),e!=null?fe(e)||Ee(e)||Z(e)?{i:Me,r:e,k:t,f:!!n}:e:null);function N(e,t=null,n=null,r=0,s=null,o=e===he?0:1,i=!1,l=!1){const a={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&Hl(t),ref:t&&$n(t),scopeId:dr,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:o,patchFlag:r,dynamicProps:s,dynamicChildren:null,appContext:null,ctx:Me};return l?(Ls(a,n),o&128&&e.normalize(a)):n&&(a.shapeFlag|=fe(n)?8:16),Kt>0&&!i&&Re&&(a.patchFlag>0||o&6)&&a.patchFlag!==32&&Re.push(a),a}const F=Eu;function Eu(e,t=null,n=null,r=0,s=null,o=!1){if((!e||e===ul)&&(e=Ae),Jn(e)){const l=et(e,t,!0);return n&&Ls(l,n),Kt>0&&!o&&Re&&(l.shapeFlag&6?Re[Re.indexOf(e)]=l:Re.push(l)),l.patchFlag=-2,l}if(Ou(e)&&(e=e.__vccOpts),t){t=Ll(t);let{class:l,style:a}=t;l&&!fe(l)&&(t.class=cr(l)),ie(a)&&(Gi(a)&&!K(a)&&(a=_e({},a)),t.style=ar(a))}const i=fe(e)?1:gu(e)?128:Qc(e)?64:ie(e)?4:Z(e)?2:0;return N(e,t,n,r,s,i,o,!0)}function Ll(e){return e?Gi(e)||_l(e)?_e({},e):e:null}function et(e,t,n=!1,r=!1){const{props:s,ref:o,patchFlag:i,children:l,transition:a}=e,f=t?Tu(s||{},t):s,u={__v_isVNode:!0,__v_skip:!0,type:e.type,props:f,key:f&&Hl(f),ref:t&&t.ref?n&&o?K(o)?o.concat($n(t)):[o,$n(t)]:$n(t):o,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:l,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==he?i===-1?16:i|16:i,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:a,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&et(e.ssContent),ssFallback:e.ssFallback&&et(e.ssFallback),el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return a&&r&&Gn(u,a.clone(u)),u}function we(e=" ",t=0){return F(Rt,null,e,t)}function Hs(e,t){const n=F(tn,null,e);return n.staticCount=t,n}function Ie(e){return e==null||typeof e=="boolean"?F(Ae):K(e)?F(he,null,e.slice()):typeof e=="object"?at(e):F(Rt,null,String(e))}function at(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:et(e)}function Ls(e,t){let n=0;const{shapeFlag:r}=e;if(t==null)t=null;else if(K(t))n=16;else if(typeof t=="object")if(r&65){const s=t.default;s&&(s._c&&(s._d=!1),Ls(e,s()),s._c&&(s._d=!0));return}else{n=32;const s=t._;!s&&!_l(t)?t._ctx=Me:s===3&&Me&&(Me.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else Z(t)?(t={default:t,_ctx:Me},n=32):(t=String(t),r&64?(n=16,t=[we(t)]):n=8);e.children=t,e.shapeFlag|=n}function Tu(...e){const t={};for(let n=0;nge||Me;let Xn,ts;{const e=Pi(),t=(n,r)=>{let s;return(s=e[n])||(s=e[n]=[]),s.push(r),o=>{s.length>1?s.forEach(i=>i(o)):s[0](o)}};Xn=t("__VUE_INSTANCE_SETTERS__",n=>ge=n),ts=t("__VUE_SSR_SETTERS__",n=>mn=n)}const gn=e=>{const t=ge;return Xn(e),e.scope.on(),()=>{e.scope.off(),Xn(t)}},Oo=()=>{ge&&ge.scope.off(),Xn(null)};function Nl(e){return e.vnode.shapeFlag&4}let mn=!1;function Ru(e,t=!1,n=!1){t&&ts(t);const{props:r,children:s}=e.vnode,o=Nl(e);qc(e,r,o,t),Jc(e,s,n);const i=o?Pu(e,t):void 0;return t&&ts(!1),i}function Pu(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,jc);const{setup:r}=n;if(r){const s=e.setupContext=r.length>1?Iu(e):null,o=gn(e);pt();const i=ut(r,e,0,[e.props,s]);if(gt(),o(),Ti(i)){if(i.then(Oo,Oo),t)return i.then(l=>{ns(e,l,t)}).catch(l=>{Gt(l,e,0)});e.asyncDep=i}else ns(e,i,t)}else jl(e,t)}function ns(e,t,n){Z(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:ie(t)&&(e.setupState=Xi(t)),jl(e,n)}let $o;function jl(e,t,n){const r=e.type;if(!e.render){if(!t&&$o&&!r.render){const s=r.template||Is(e).template;if(s){const{isCustomElement:o,compilerOptions:i}=e.appContext.config,{delimiters:l,compilerOptions:a}=r,f=_e(_e({isCustomElement:o,delimiters:l},i),a);r.render=$o(s,f)}}e.render=r.render||Le}{const s=gn(e);pt();try{Fc(e)}finally{gt(),s()}}}const ku={get(e,t){return Pe(e,"get",""),e[t]}};function Iu(e){const t=n=>{e.exposed=n||{}};return{attrs:new Proxy(e.attrs,ku),slots:e.slots,emit:e.emit,expose:t}}function js(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(Xi(mc(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in en)return en[n](e)},has(t,n){return n in t||n in en}})):e.proxy}function Mu(e,t=!0){return Z(e)?e.displayName||e.name:e.name||t&&e.__name}function Ou(e){return Z(e)&&"__vccOpts"in e}const pe=(e,t)=>yc(e,t,mn);function cn(e,t,n){const r=arguments.length;return r===2?ie(t)&&!K(t)?Jn(t)?F(e,null,[t]):F(e,t):F(e,null,t):(r>3?n=Array.prototype.slice.call(arguments,2):r===3&&Jn(n)&&(n=[n]),F(e,t,n))}const Fl="3.4.36";/** +* @vue/runtime-dom v3.4.36 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/const $u="http://www.w3.org/2000/svg",Hu="http://www.w3.org/1998/Math/MathML",ze=typeof document<"u"?document:null,Ho=ze&&ze.createElement("template"),Lu={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,r)=>{const s=t==="svg"?ze.createElementNS($u,e):t==="mathml"?ze.createElementNS(Hu,e):n?ze.createElement(e,{is:n}):ze.createElement(e);return e==="select"&&r&&r.multiple!=null&&s.setAttribute("multiple",r.multiple),s},createText:e=>ze.createTextNode(e),createComment:e=>ze.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>ze.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,r,s,o){const i=n?n.previousSibling:t.lastChild;if(s&&(s===o||s.nextSibling))for(;t.insertBefore(s.cloneNode(!0),n),!(s===o||!(s=s.nextSibling)););else{Ho.innerHTML=r==="svg"?`${e}`:r==="mathml"?`${e}`:e;const l=Ho.content;if(r==="svg"||r==="mathml"){const a=l.firstChild;for(;a.firstChild;)l.appendChild(a.firstChild);l.removeChild(a)}t.insertBefore(l,n)}return[i?i.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},rt="transition",Xt="animation",un=Symbol("_vtc"),Vt=(e,{slots:t})=>cn(kc,Nu(e),t);Vt.displayName="Transition";const Dl={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String};Vt.props=_e({},tl,Dl);const bt=(e,t=[])=>{K(e)?e.forEach(n=>n(...t)):e&&e(...t)},Lo=e=>e?K(e)?e.some(t=>t.length>1):e.length>1:!1;function Nu(e){const t={};for(const R in e)R in Dl||(t[R]=e[R]);if(e.css===!1)return t;const{name:n="v",type:r,duration:s,enterFromClass:o=`${n}-enter-from`,enterActiveClass:i=`${n}-enter-active`,enterToClass:l=`${n}-enter-to`,appearFromClass:a=o,appearActiveClass:f=i,appearToClass:u=l,leaveFromClass:c=`${n}-leave-from`,leaveActiveClass:d=`${n}-leave-active`,leaveToClass:p=`${n}-leave-to`}=e,m=ju(s),_=m&&m[0],A=m&&m[1],{onBeforeEnter:P,onEnter:H,onEnterCancelled:y,onLeave:S,onLeaveCancelled:T,onBeforeAppear:w=P,onAppear:B=H,onAppearCancelled:G=y}=t,O=(R,V,te)=>{wt(R,V?u:l),wt(R,V?f:i),te&&te()},I=(R,V)=>{R._isLeaving=!1,wt(R,c),wt(R,p),wt(R,d),V&&V()},q=R=>(V,te)=>{const oe=R?B:H,j=()=>O(V,R,te);bt(oe,[V,j]),No(()=>{wt(V,R?a:o),st(V,R?u:l),Lo(oe)||jo(V,r,_,j)})};return _e(t,{onBeforeEnter(R){bt(P,[R]),st(R,o),st(R,i)},onBeforeAppear(R){bt(w,[R]),st(R,a),st(R,f)},onEnter:q(!1),onAppear:q(!0),onLeave(R,V){R._isLeaving=!0;const te=()=>I(R,V);st(R,c),st(R,d),Uu(),No(()=>{R._isLeaving&&(wt(R,c),st(R,p),Lo(S)||jo(R,r,A,te))}),bt(S,[R,te])},onEnterCancelled(R){O(R,!1),bt(y,[R])},onAppearCancelled(R){O(R,!0),bt(G,[R])},onLeaveCancelled(R){I(R),bt(T,[R])}})}function ju(e){if(e==null)return null;if(ie(e))return[Hr(e.enter),Hr(e.leave)];{const t=Hr(e);return[t,t]}}function Hr(e){return Ri(e)}function st(e,t){t.split(/\s+/).forEach(n=>n&&e.classList.add(n)),(e[un]||(e[un]=new Set)).add(t)}function wt(e,t){t.split(/\s+/).forEach(r=>r&&e.classList.remove(r));const n=e[un];n&&(n.delete(t),n.size||(e[un]=void 0))}function No(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let Fu=0;function jo(e,t,n,r){const s=e._endId=++Fu,o=()=>{s===e._endId&&r()};if(n)return setTimeout(o,n);const{type:i,timeout:l,propCount:a}=Du(e,t);if(!i)return r();const f=i+"end";let u=0;const c=()=>{e.removeEventListener(f,d),o()},d=p=>{p.target===e&&++u>=a&&c()};setTimeout(()=>{u(n[m]||"").split(", "),s=r(`${rt}Delay`),o=r(`${rt}Duration`),i=Fo(s,o),l=r(`${Xt}Delay`),a=r(`${Xt}Duration`),f=Fo(l,a);let u=null,c=0,d=0;t===rt?i>0&&(u=rt,c=i,d=o.length):t===Xt?f>0&&(u=Xt,c=f,d=a.length):(c=Math.max(i,f),u=c>0?i>f?rt:Xt:null,d=u?u===rt?o.length:a.length:0);const p=u===rt&&/\b(transform|all)(,|$)/.test(r(`${rt}Property`).toString());return{type:u,timeout:c,propCount:d,hasTransform:p}}function Fo(e,t){for(;e.lengthDo(n)+Do(e[r])))}function Do(e){return e==="auto"?0:Number(e.slice(0,-1).replace(",","."))*1e3}function Uu(){return document.body.offsetHeight}function Bu(e,t,n){const r=e[un];r&&(t=(t?[t,...r]:[...r]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const Uo=Symbol("_vod"),Vu=Symbol("_vsh"),Wu=Symbol(""),Ku=/(^|;)\s*display\s*:/;function qu(e,t,n){const r=e.style,s=fe(n);let o=!1;if(n&&!s){if(t)if(fe(t))for(const i of t.split(";")){const l=i.slice(0,i.indexOf(":")).trim();n[l]==null&&Hn(r,l,"")}else for(const i in t)n[i]==null&&Hn(r,i,"");for(const i in n)i==="display"&&(o=!0),Hn(r,i,n[i])}else if(s){if(t!==n){const i=r[Wu];i&&(n+=";"+i),r.cssText=n,o=Ku.test(n)}}else t&&e.removeAttribute("style");Uo in e&&(e[Uo]=o?r.display:"",e[Vu]&&(r.display="none"))}const Bo=/\s*!important$/;function Hn(e,t,n){if(K(n))n.forEach(r=>Hn(e,t,r));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const r=Gu(e,t);Bo.test(n)?e.setProperty(kt(r),n.replace(Bo,""),"important"):e[r]=n}}const Vo=["Webkit","Moz","ms"],Lr={};function Gu(e,t){const n=Lr[t];if(n)return n;let r=De(t);if(r!=="filter"&&r in e)return Lr[t]=r;r=lr(r);for(let s=0;sNr||(Qu.then(()=>Nr=0),Nr=Date.now());function tf(e,t){const n=r=>{if(!r._vts)r._vts=Date.now();else if(r._vts<=n.attached)return;Ne(nf(r,n.value),t,5,[r])};return n.value=e,n.attached=ef(),n}function nf(e,t){if(K(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(r=>s=>!s._stopped&&r&&r(s))}else return t}const Zo=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,rf=(e,t,n,r,s,o)=>{const i=s==="svg";t==="class"?Bu(e,r,i):t==="style"?qu(e,n,r):dn(t)?vs(t)||Xu(e,t,n,r,o):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):sf(e,t,r,i))?(Zu(e,t,r),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&Ko(e,t,r,i,o,t!=="value")):(t==="true-value"?e._trueValue=r:t==="false-value"&&(e._falseValue=r),Ko(e,t,r,i))};function sf(e,t,n,r){if(r)return!!(t==="innerHTML"||t==="textContent"||t in e&&Zo(t)&&Z(n));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const s=e.tagName;if(s==="IMG"||s==="VIDEO"||s==="CANVAS"||s==="SOURCE")return!1}return Zo(t)&&fe(n)?!1:t in e}const Ul=_e({patchProp:rf},Lu);let rn,zo=!1;function of(){return rn||(rn=ru(Ul))}function lf(){return rn=zo?rn:su(Ul),zo=!0,rn}const af=(...e)=>{const t=of().createApp(...e),{mount:n}=t;return t.mount=r=>{const s=Vl(r);if(!s)return;const o=t._component;!Z(o)&&!o.render&&!o.template&&(o.template=s.innerHTML),s.innerHTML="";const i=n(s,!1,Bl(s));return s instanceof Element&&(s.removeAttribute("v-cloak"),s.setAttribute("data-v-app","")),i},t},cf=(...e)=>{const t=lf().createApp(...e),{mount:n}=t;return t.mount=r=>{const s=Vl(r);if(s)return n(s,!0,Bl(s))},t};function Bl(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function Vl(e){return fe(e)?document.querySelector(e):e}const uf=/"(?:_|\\u0{2}5[Ff]){2}(?:p|\\u0{2}70)(?:r|\\u0{2}72)(?:o|\\u0{2}6[Ff])(?:t|\\u0{2}74)(?:o|\\u0{2}6[Ff])(?:_|\\u0{2}5[Ff]){2}"\s*:/,ff=/"(?:c|\\u0063)(?:o|\\u006[Ff])(?:n|\\u006[Ee])(?:s|\\u0073)(?:t|\\u0074)(?:r|\\u0072)(?:u|\\u0075)(?:c|\\u0063)(?:t|\\u0074)(?:o|\\u006[Ff])(?:r|\\u0072)"\s*:/,df=/^\s*["[{]|^\s*-?\d{1,16}(\.\d{1,17})?([Ee][+-]?\d+)?\s*$/;function hf(e,t){if(e==="__proto__"||e==="constructor"&&t&&typeof t=="object"&&"prototype"in t){pf(e);return}return t}function pf(e){console.warn(`[destr] Dropping "${e}" key to prevent prototype pollution.`)}function Yn(e,t={}){if(typeof e!="string")return e;const n=e.trim();if(e[0]==='"'&&e.endsWith('"')&&!e.includes("\\"))return n.slice(1,-1);if(n.length<=9){const r=n.toLowerCase();if(r==="true")return!0;if(r==="false")return!1;if(r==="undefined")return;if(r==="null")return null;if(r==="nan")return Number.NaN;if(r==="infinity")return Number.POSITIVE_INFINITY;if(r==="-infinity")return Number.NEGATIVE_INFINITY}if(!df.test(e)){if(t.strict)throw new SyntaxError("[destr] Invalid JSON");return e}try{if(uf.test(e)||ff.test(e)){if(t.strict)throw new Error("[destr] Possible prototype pollution");return JSON.parse(e,hf)}return JSON.parse(e)}catch(r){if(t.strict)throw r;return e}}const gf=/#/g,mf=/&/g,yf=/\//g,_f=/=/g,Fs=/\+/g,vf=/%5e/gi,bf=/%60/gi,wf=/%7c/gi,xf=/%20/gi;function Ef(e){return encodeURI(""+e).replace(wf,"|")}function rs(e){return Ef(typeof e=="string"?e:JSON.stringify(e)).replace(Fs,"%2B").replace(xf,"+").replace(gf,"%23").replace(mf,"%26").replace(bf,"`").replace(vf,"^").replace(yf,"%2F")}function jr(e){return rs(e).replace(_f,"%3D")}function Qn(e=""){try{return decodeURIComponent(""+e)}catch{return""+e}}function Tf(e){return Qn(e.replace(Fs," "))}function Sf(e){return Qn(e.replace(Fs," "))}function Ds(e=""){const t={};e[0]==="?"&&(e=e.slice(1));for(const n of e.split("&")){const r=n.match(/([^=]+)=?(.*)/)||[];if(r.length<2)continue;const s=Tf(r[1]);if(s==="__proto__"||s==="constructor")continue;const o=Sf(r[2]||"");t[s]===void 0?t[s]=o:Array.isArray(t[s])?t[s].push(o):t[s]=[t[s],o]}return t}function Cf(e,t){return(typeof t=="number"||typeof t=="boolean")&&(t=String(t)),t?Array.isArray(t)?t.map(n=>`${jr(e)}=${rs(n)}`).join("&"):`${jr(e)}=${rs(t)}`:jr(e)}function Wl(e){return Object.keys(e).filter(t=>e[t]!==void 0).map(t=>Cf(t,e[t])).filter(Boolean).join("&")}const Af=/^[\s\w\0+.-]{2,}:([/\\]{1,2})/,Rf=/^[\s\w\0+.-]{2,}:([/\\]{2})?/,Pf=/^([/\\]\s*){2,}[^/\\]/,kf=/^[\s\0]*(blob|data|javascript|vbscript):$/i,If=/\/$|\/\?|\/#/,Mf=/^\.?\//;function mt(e,t={}){return typeof t=="boolean"&&(t={acceptRelative:t}),t.strict?Af.test(e):Rf.test(e)||(t.acceptRelative?Pf.test(e):!1)}function Of(e){return!!e&&kf.test(e)}function ss(e="",t){return t?If.test(e):e.endsWith("/")}function yr(e="",t){if(!t)return(ss(e)?e.slice(0,-1):e)||"/";if(!ss(e,!0))return e||"/";let n=e,r="";const s=e.indexOf("#");s>=0&&(n=e.slice(0,s),r=e.slice(s));const[o,...i]=n.split("?");return((o.endsWith("/")?o.slice(0,-1):o)||"/")+(i.length>0?`?${i.join("?")}`:"")+r}function er(e="",t){if(!t)return e.endsWith("/")?e:e+"/";if(ss(e,!0))return e||"/";let n=e,r="";const s=e.indexOf("#");if(s>=0&&(n=e.slice(0,s),r=e.slice(s),!n))return r;const[o,...i]=n.split("?");return o+"/"+(i.length>0?`?${i.join("?")}`:"")+r}function $f(e=""){return e.startsWith("/")}function Jo(e=""){return $f(e)?e:"/"+e}function Hf(e,t){if(ql(t)||mt(e))return e;const n=yr(t);return e.startsWith(n)?e:yn(n,e)}function Lf(e,t){if(ql(t))return e;const n=yr(t);if(!e.startsWith(n))return e;const r=e.slice(n.length);return r[0]==="/"?r:"/"+r}function Kl(e,t){const n=Ff(e),r={...Ds(n.search),...t};return n.search=Wl(r),zl(n)}function ql(e){return!e||e==="/"}function Nf(e){return e&&e!=="/"}function yn(e,...t){let n=e||"";for(const r of t.filter(s=>Nf(s)))if(n){const s=r.replace(Mf,"");n=er(n)+s}else n=r;return n}function Gl(...e){var i,l,a,f;const t=/\/(?!\/)/,n=e.filter(Boolean),r=[];let s=0;for(const u of n)if(!(!u||u==="/")){for(const[c,d]of u.split(t).entries())if(!(!d||d===".")){if(d===".."){if(r.length===1&&mt(r[0]))continue;r.pop(),s--;continue}if(c===1&&((i=r[r.length-1])!=null&&i.endsWith(":/"))){r[r.length-1]+="/"+d;continue}r.push(d),s++}}let o=r.join("/");return s>=0?(l=n[0])!=null&&l.startsWith("/")&&!o.startsWith("/")?o="/"+o:(a=n[0])!=null&&a.startsWith("./")&&!o.startsWith("./")&&(o="./"+o):o="../".repeat(-1*s)+o,(f=n[n.length-1])!=null&&f.endsWith("/")&&!o.endsWith("/")&&(o+="/"),o}function jf(e,t,n={}){return n.trailingSlash||(e=er(e),t=er(t)),n.leadingSlash||(e=Jo(e),t=Jo(t)),n.encoding||(e=Qn(e),t=Qn(t)),e===t}const Zl=Symbol.for("ufo:protocolRelative");function Ff(e="",t){const n=e.match(/^[\s\0]*(blob:|data:|javascript:|vbscript:)(.*)/i);if(n){const[,c,d=""]=n;return{protocol:c.toLowerCase(),pathname:d,href:c+d,auth:"",host:"",search:"",hash:""}}if(!mt(e,{acceptRelative:!0}))return Xo(e);const[,r="",s,o=""]=e.replace(/\\/g,"/").match(/^[\s\0]*([\w+.-]{2,}:)?\/\/([^/@]+@)?(.*)/)||[];let[,i="",l=""]=o.match(/([^#/?]*)(.*)?/)||[];r==="file:"&&(l=l.replace(/\/(?=[A-Za-z]:)/,""));const{pathname:a,search:f,hash:u}=Xo(l);return{protocol:r.toLowerCase(),auth:s?s.slice(0,Math.max(0,s.length-1)):"",host:i,pathname:a,search:f,hash:u,[Zl]:!r}}function Xo(e=""){const[t="",n="",r=""]=(e.match(/([^#?]*)(\?[^#]*)?(#.*)?/)||[]).splice(1);return{pathname:t,search:n,hash:r}}function zl(e){const t=e.pathname||"",n=e.search?(e.search.startsWith("?")?"":"?")+e.search:"",r=e.hash||"",s=e.auth?e.auth+"@":"",o=e.host||"";return(e.protocol||e[Zl]?(e.protocol||"")+"//":"")+s+o+t+n+r}class Df extends Error{constructor(t,n){super(t,n),this.name="FetchError",n!=null&&n.cause&&!this.cause&&(this.cause=n.cause)}}function Uf(e){var a,f,u,c,d;const t=((a=e.error)==null?void 0:a.message)||((f=e.error)==null?void 0:f.toString())||"",n=((u=e.request)==null?void 0:u.method)||((c=e.options)==null?void 0:c.method)||"GET",r=((d=e.request)==null?void 0:d.url)||String(e.request)||"/",s=`[${n}] ${JSON.stringify(r)}`,o=e.response?`${e.response.status} ${e.response.statusText}`:"",i=`${s}: ${o}${t?` ${t}`:""}`,l=new Df(i,e.error?{cause:e.error}:void 0);for(const p of["request","options","response"])Object.defineProperty(l,p,{get(){return e[p]}});for(const[p,m]of[["data","_data"],["status","status"],["statusCode","status"],["statusText","statusText"],["statusMessage","statusText"]])Object.defineProperty(l,p,{get(){return e.response&&e.response[m]}});return l}const Bf=new Set(Object.freeze(["PATCH","POST","PUT","DELETE"]));function Yo(e="GET"){return Bf.has(e.toUpperCase())}function Vf(e){if(e===void 0)return!1;const t=typeof e;return t==="string"||t==="number"||t==="boolean"||t===null?!0:t!=="object"?!1:Array.isArray(e)?!0:e.buffer?!1:e.constructor&&e.constructor.name==="Object"||typeof e.toJSON=="function"}const Wf=new Set(["image/svg","application/xml","application/xhtml","application/html"]),Kf=/^application\/(?:[\w!#$%&*.^`~-]*\+)?json(;.+)?$/i;function qf(e=""){if(!e)return"json";const t=e.split(";").shift()||"";return Kf.test(t)?"json":Wf.has(t)||t.startsWith("text/")?"text":"blob"}function Gf(e,t,n=globalThis.Headers){const r={...t,...e};if(t!=null&&t.params&&(e!=null&&e.params)&&(r.params={...t==null?void 0:t.params,...e==null?void 0:e.params}),t!=null&&t.query&&(e!=null&&e.query)&&(r.query={...t==null?void 0:t.query,...e==null?void 0:e.query}),t!=null&&t.headers&&(e!=null&&e.headers)){r.headers=new n((t==null?void 0:t.headers)||{});for(const[s,o]of new n((e==null?void 0:e.headers)||{}))r.headers.set(s,o)}return r}const Zf=new Set([408,409,425,429,500,502,503,504]),zf=new Set([101,204,205,304]);function Jl(e={}){const{fetch:t=globalThis.fetch,Headers:n=globalThis.Headers,AbortController:r=globalThis.AbortController}=e;async function s(l){const a=l.error&&l.error.name==="AbortError"&&!l.options.timeout||!1;if(l.options.retry!==!1&&!a){let u;typeof l.options.retry=="number"?u=l.options.retry:u=Yo(l.options.method)?0:1;const c=l.response&&l.response.status||500;if(u>0&&(Array.isArray(l.options.retryStatusCodes)?l.options.retryStatusCodes.includes(c):Zf.has(c))){const d=l.options.retryDelay||0;return d>0&&await new Promise(p=>setTimeout(p,d)),o(l.request,{...l.options,retry:u-1})}}const f=Uf(l);throw Error.captureStackTrace&&Error.captureStackTrace(f,o),f}const o=async function(a,f={}){var p;const u={request:a,options:Gf(f,e.defaults,n),response:void 0,error:void 0};u.options.method=(p=u.options.method)==null?void 0:p.toUpperCase(),u.options.onRequest&&await u.options.onRequest(u),typeof u.request=="string"&&(u.options.baseURL&&(u.request=Hf(u.request,u.options.baseURL)),(u.options.query||u.options.params)&&(u.request=Kl(u.request,{...u.options.params,...u.options.query}))),u.options.body&&Yo(u.options.method)&&(Vf(u.options.body)?(u.options.body=typeof u.options.body=="string"?u.options.body:JSON.stringify(u.options.body),u.options.headers=new n(u.options.headers||{}),u.options.headers.has("content-type")||u.options.headers.set("content-type","application/json"),u.options.headers.has("accept")||u.options.headers.set("accept","application/json")):("pipeTo"in u.options.body&&typeof u.options.body.pipeTo=="function"||typeof u.options.body.pipe=="function")&&("duplex"in u.options||(u.options.duplex="half")));let c;if(!u.options.signal&&u.options.timeout){const m=new r;c=setTimeout(()=>m.abort(),u.options.timeout),u.options.signal=m.signal}try{u.response=await t(u.request,u.options)}catch(m){return u.error=m,u.options.onRequestError&&await u.options.onRequestError(u),await s(u)}finally{c&&clearTimeout(c)}if(u.response.body&&!zf.has(u.response.status)&&u.options.method!=="HEAD"){const m=(u.options.parseResponse?"json":u.options.responseType)||qf(u.response.headers.get("content-type")||"");switch(m){case"json":{const _=await u.response.text(),A=u.options.parseResponse||Yn;u.response._data=A(_);break}case"stream":{u.response._data=u.response.body;break}default:u.response._data=await u.response[m]()}}return u.options.onResponse&&await u.options.onResponse(u),!u.options.ignoreResponseError&&u.response.status>=400&&u.response.status<600?(u.options.onResponseError&&await u.options.onResponseError(u),await s(u)):u.response},i=async function(a,f){return(await o(a,f))._data};return i.raw=o,i.native=(...l)=>t(...l),i.create=(l={})=>Jl({...e,defaults:{...e.defaults,...l}}),i}const Us=function(){if(typeof globalThis<"u")return globalThis;if(typeof self<"u")return self;if(typeof window<"u")return window;if(typeof global<"u")return global;throw new Error("unable to locate global object")}(),Jf=Us.fetch||(()=>Promise.reject(new Error("[ofetch] global.fetch is not supported!"))),Xf=Us.Headers,Yf=Us.AbortController,Qf=Jl({fetch:Jf,Headers:Xf,AbortController:Yf}),ed=Qf,td=()=>{var e;return((e=window==null?void 0:window.__NUXT__)==null?void 0:e.config)||{}},tr=td().app,nd=()=>tr.baseURL,rd=()=>tr.buildAssetsDir,Bs=(...e)=>Gl(_r(),rd(),...e),_r=(...e)=>{const t=tr.cdnURL||tr.baseURL;return e.length?Gl(t,...e):t};globalThis.__buildAssetsURL=Bs,globalThis.__publicAssetsURL=_r;globalThis.$fetch||(globalThis.$fetch=ed.create({baseURL:nd()}));function os(e,t={},n){for(const r in e){const s=e[r],o=n?`${n}:${r}`:r;typeof s=="object"&&s!==null?os(s,t,o):typeof s=="function"&&(t[o]=s)}return t}const sd={run:e=>e()},od=()=>sd,Xl=typeof console.createTask<"u"?console.createTask:od;function id(e,t){const n=t.shift(),r=Xl(n);return e.reduce((s,o)=>s.then(()=>r.run(()=>o(...t))),Promise.resolve())}function ld(e,t){const n=t.shift(),r=Xl(n);return Promise.all(e.map(s=>r.run(()=>s(...t))))}function Fr(e,t){for(const n of[...e])n(t)}class ad{constructor(){this._hooks={},this._before=void 0,this._after=void 0,this._deprecatedMessages=void 0,this._deprecatedHooks={},this.hook=this.hook.bind(this),this.callHook=this.callHook.bind(this),this.callHookWith=this.callHookWith.bind(this)}hook(t,n,r={}){if(!t||typeof n!="function")return()=>{};const s=t;let o;for(;this._deprecatedHooks[t];)o=this._deprecatedHooks[t],t=o.to;if(o&&!r.allowDeprecated){let i=o.message;i||(i=`${s} hook has been deprecated`+(o.to?`, please use ${o.to}`:"")),this._deprecatedMessages||(this._deprecatedMessages=new Set),this._deprecatedMessages.has(i)||(console.warn(i),this._deprecatedMessages.add(i))}if(!n.name)try{Object.defineProperty(n,"name",{get:()=>"_"+t.replace(/\W+/g,"_")+"_hook_cb",configurable:!0})}catch{}return this._hooks[t]=this._hooks[t]||[],this._hooks[t].push(n),()=>{n&&(this.removeHook(t,n),n=void 0)}}hookOnce(t,n){let r,s=(...o)=>(typeof r=="function"&&r(),r=void 0,s=void 0,n(...o));return r=this.hook(t,s),r}removeHook(t,n){if(this._hooks[t]){const r=this._hooks[t].indexOf(n);r!==-1&&this._hooks[t].splice(r,1),this._hooks[t].length===0&&delete this._hooks[t]}}deprecateHook(t,n){this._deprecatedHooks[t]=typeof n=="string"?{to:n}:n;const r=this._hooks[t]||[];delete this._hooks[t];for(const s of r)this.hook(t,s)}deprecateHooks(t){Object.assign(this._deprecatedHooks,t);for(const n in t)this.deprecateHook(n,t[n])}addHooks(t){const n=os(t),r=Object.keys(n).map(s=>this.hook(s,n[s]));return()=>{for(const s of r.splice(0,r.length))s()}}removeHooks(t){const n=os(t);for(const r in n)this.removeHook(r,n[r])}removeAllHooks(){for(const t in this._hooks)delete this._hooks[t]}callHook(t,...n){return n.unshift(t),this.callHookWith(id,t,...n)}callHookParallel(t,...n){return n.unshift(t),this.callHookWith(ld,t,...n)}callHookWith(t,n,...r){const s=this._before||this._after?{name:n,args:r,context:{}}:void 0;this._before&&Fr(this._before,s);const o=t(n in this._hooks?[...this._hooks[n]]:[],r);return o instanceof Promise?o.finally(()=>{this._after&&s&&Fr(this._after,s)}):(this._after&&s&&Fr(this._after,s),o)}beforeEach(t){return this._before=this._before||[],this._before.push(t),()=>{if(this._before!==void 0){const n=this._before.indexOf(t);n!==-1&&this._before.splice(n,1)}}}afterEach(t){return this._after=this._after||[],this._after.push(t),()=>{if(this._after!==void 0){const n=this._after.indexOf(t);n!==-1&&this._after.splice(n,1)}}}}function Yl(){return new ad}function cd(e={}){let t,n=!1;const r=i=>{if(t&&t!==i)throw new Error("Context conflict")};let s;if(e.asyncContext){const i=e.AsyncLocalStorage||globalThis.AsyncLocalStorage;i?s=new i:console.warn("[unctx] `AsyncLocalStorage` is not provided.")}const o=()=>{if(s&&t===void 0){const i=s.getStore();if(i!==void 0)return i}return t};return{use:()=>{const i=o();if(i===void 0)throw new Error("Context is not available");return i},tryUse:()=>o(),set:(i,l)=>{l||r(i),t=i,n=!0},unset:()=>{t=void 0,n=!1},call:(i,l)=>{r(i),t=i;try{return s?s.run(i,l):l()}finally{n||(t=void 0)}},async callAsync(i,l){t=i;const a=()=>{t=i},f=()=>t===i?a:void 0;is.add(f);try{const u=s?s.run(i,l):l();return n||(t=void 0),await u}finally{is.delete(f)}}}}function ud(e={}){const t={};return{get(n,r={}){return t[n]||(t[n]=cd({...e,...r})),t[n],t[n]}}}const nr=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof global<"u"?global:typeof window<"u"?window:{},Qo="__unctx__",fd=nr[Qo]||(nr[Qo]=ud()),dd=(e,t={})=>fd.get(e,t),ei="__unctx_async_handlers__",is=nr[ei]||(nr[ei]=new Set);function Ql(e){const t=[];for(const s of is){const o=s();o&&t.push(o)}const n=()=>{for(const s of t)s()};let r=e();return r&&typeof r=="object"&&"catch"in r&&(r=r.catch(s=>{throw n(),s})),[r,n]}const hd={componentName:"NuxtLink"},pd=null,gd="#__nuxt",ea="nuxt-app",ti=36e5;function ta(e=ea){return dd(e,{asyncContext:!1})}const md="__nuxt_plugin";function yd(e){let t=0;const n={_name:ea,_scope:Za(),provide:void 0,globalName:"nuxt",versions:{get nuxt(){return"3.12.4"},get vue(){return n.vueApp.version}},payload:$t({data:$t({}),state:Pt({}),once:new Set,_errors:$t({})}),static:{data:{}},runWithContext(s){return n._scope.active&&!$i()?n._scope.run(()=>ni(n,s)):ni(n,s)},isHydrating:!0,deferHydration(){if(!n.isHydrating)return()=>{};t++;let s=!1;return()=>{if(!s&&(s=!0,t--,t===0))return n.isHydrating=!1,n.callHook("app:suspense:resolve")}},_asyncDataPromises:{},_asyncData:$t({}),_payloadRevivers:{},...e};if(window.__NUXT__)for(const s in window.__NUXT__)switch(s){case"data":case"state":case"_errors":Object.assign(n.payload[s],window.__NUXT__[s]);break;default:n.payload[s]=window.__NUXT__[s]}n.hooks=Yl(),n.hook=n.hooks.hook,n.callHook=n.hooks.callHook,n.provide=(s,o)=>{const i="$"+s;Pn(n,i,o),Pn(n.vueApp.config.globalProperties,i,o)},Pn(n.vueApp,"$nuxt",n),Pn(n.vueApp.config.globalProperties,"$nuxt",n);{window.addEventListener("nuxt.preloadError",o=>{n.callHook("app:chunkError",{error:o.payload})}),window.useNuxtApp=window.useNuxtApp||me;const s=n.hook("app:error",(...o)=>{console.error("[nuxt] error caught during app initialization",...o)});n.hook("app:mounted",s)}const r=n.payload.config;return n.provide("config",r),n}function _d(e,t){t.hooks&&e.hooks.addHooks(t.hooks)}async function vd(e,t){if(typeof t=="function"){const{provide:n}=await e.runWithContext(()=>t(e))||{};if(n&&typeof n=="object")for(const r in n)e.provide(r,n[r])}}async function bd(e,t){const n=[],r=[],s=[],o=[];let i=0;async function l(a){var u;const f=((u=a.dependsOn)==null?void 0:u.filter(c=>t.some(d=>d._name===c)&&!n.includes(c)))??[];if(f.length>0)r.push([new Set(f),a]);else{const c=vd(e,a).then(async()=>{a._name&&(n.push(a._name),await Promise.all(r.map(async([d,p])=>{d.has(a._name)&&(d.delete(a._name),d.size===0&&(i++,await l(p)))})))});a.parallel?s.push(c.catch(d=>o.push(d))):await c}}for(const a of t)_d(e,a);for(const a of t)await l(a);if(await Promise.all(s),i)for(let a=0;a{}),e,{[md]:!0,_name:t})}function ni(e,t,n){const r=()=>t();return ta(e._name).set(e),e.vueApp.runWithContext(r)}function wd(e){var n;let t;return gl()&&(t=(n=Ns())==null?void 0:n.appContext.app.$nuxt),t=t||ta(e).tryUse(),t||null}function me(e){const t=wd(e);if(!t)throw new Error("[nuxt] instance unavailable");return t}function qt(e){return me().$config}function Pn(e,t,n){Object.defineProperty(e,t,{get:()=>n})}function xd(e,t){return{ctx:{table:e},matchAll:n=>ra(n,e)}}function na(e){const t={};for(const n in e)t[n]=n==="dynamic"?new Map(Object.entries(e[n]).map(([r,s])=>[r,na(s)])):new Map(Object.entries(e[n]));return t}function Ed(e){return xd(na(e))}function ra(e,t,n){e.endsWith("/")&&(e=e.slice(0,-1)||"/");const r=[];for(const[o,i]of ri(t.wildcard))(e===o||e.startsWith(o+"/"))&&r.push(i);for(const[o,i]of ri(t.dynamic))if(e.startsWith(o+"/")){const l="/"+e.slice(o.length).split("/").splice(2).join("/");r.push(...ra(l,i))}const s=t.static.get(e);return s&&r.push(s),r.filter(Boolean)}function ri(e){return[...e.entries()].sort((t,n)=>t[0].length-n[0].length)}function Dr(e){if(e===null||typeof e!="object")return!1;const t=Object.getPrototypeOf(e);return t!==null&&t!==Object.prototype&&Object.getPrototypeOf(t)!==null||Symbol.iterator in e?!1:Symbol.toStringTag in e?Object.prototype.toString.call(e)==="[object Module]":!0}function ls(e,t,n=".",r){if(!Dr(t))return ls(e,{},n,r);const s=Object.assign({},t);for(const o in e){if(o==="__proto__"||o==="constructor")continue;const i=e[o];i!=null&&(r&&r(s,o,i,n)||(Array.isArray(i)&&Array.isArray(s[o])?s[o]=[...i,...s[o]]:Dr(i)&&Dr(s[o])?s[o]=ls(i,s[o],(n?`${n}.`:"")+o.toString(),r):s[o]=i))}return s}function Td(e){return(...t)=>t.reduce((n,r)=>ls(n,r,"",e),{})}const Sd=Td();function Cd(e,t){try{return t in e}catch{return!1}}var Ad=Object.defineProperty,Rd=(e,t,n)=>t in e?Ad(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,xt=(e,t,n)=>(Rd(e,typeof t!="symbol"?t+"":t,n),n);class as extends Error{constructor(t,n={}){super(t,n),xt(this,"statusCode",500),xt(this,"fatal",!1),xt(this,"unhandled",!1),xt(this,"statusMessage"),xt(this,"data"),xt(this,"cause"),n.cause&&!this.cause&&(this.cause=n.cause)}toJSON(){const t={message:this.message,statusCode:cs(this.statusCode,500)};return this.statusMessage&&(t.statusMessage=sa(this.statusMessage)),this.data!==void 0&&(t.data=this.data),t}}xt(as,"__h3_error__",!0);function Pd(e){if(typeof e=="string")return new as(e);if(kd(e))return e;const t=new as(e.message??e.statusMessage??"",{cause:e.cause||e});if(Cd(e,"stack"))try{Object.defineProperty(t,"stack",{get(){return e.stack}})}catch{try{t.stack=e.stack}catch{}}if(e.data&&(t.data=e.data),e.statusCode?t.statusCode=cs(e.statusCode,t.statusCode):e.status&&(t.statusCode=cs(e.status,t.statusCode)),e.statusMessage?t.statusMessage=e.statusMessage:e.statusText&&(t.statusMessage=e.statusText),t.statusMessage){const n=t.statusMessage;sa(t.statusMessage)!==n&&console.warn("[h3] Please prefer using `message` for longer error messages instead of `statusMessage`. In the future, `statusMessage` will be sanitized by default.")}return e.fatal!==void 0&&(t.fatal=e.fatal),e.unhandled!==void 0&&(t.unhandled=e.unhandled),t}function kd(e){var t;return((t=e==null?void 0:e.constructor)==null?void 0:t.__h3_error__)===!0}const Id=/[^\u0009\u0020-\u007E]/g;function sa(e=""){return e.replace(Id,"")}function cs(e,t=200){return!e||(typeof e=="string"&&(e=Number.parseInt(e,10)),e<100||e>999)?t:e}const oa=Symbol("route"),Ze=()=>{var e;return(e=me())==null?void 0:e.$router},ia=()=>gl()?Qe(oa,me()._route):me()._route;const Md=()=>{try{if(me()._processingMiddleware)return!0}catch{return!1}return!1},la=(e,t)=>{e||(e="/");const n=typeof e=="string"?e:"path"in e?us(e):Ze().resolve(e).href;if(t!=null&&t.open){const{target:a="_blank",windowFeatures:f={}}=t.open,u=Object.entries(f).filter(([c,d])=>d!==void 0).map(([c,d])=>`${c.toLowerCase()}=${d}`).join(", ");return open(n,a,u),Promise.resolve()}const r=mt(n,{acceptRelative:!0}),s=(t==null?void 0:t.external)||r;if(s){if(!(t!=null&&t.external))throw new Error("Navigating to an external URL is not allowed by default. Use `navigateTo(url, { external: true })`.");const{protocol:a}=new URL(n,window.location.href);if(a&&Of(a))throw new Error(`Cannot navigate to a URL with '${a}' protocol.`)}const o=Md();if(!s&&o)return e;const i=Ze(),l=me();return s?(l._scope.stop(),t!=null&&t.replace?location.replace(n):location.href=n,o?l.isHydrating?new Promise(()=>{}):!1:Promise.resolve()):t!=null&&t.replace?i.replace(e):i.push(e)};function us(e){return Kl(e.path||"",e.query||{})+(e.hash||"")}const aa="__nuxt_error",Vs=()=>xc(me().payload,"error"),Od=e=>{const t=Ws(e);try{const n=me(),r=Vs();n.hooks.callHook("app:error",t),r.value=r.value||t}catch{throw t}return t},$d=async(e={})=>{const t=me(),n=Vs();t.callHook("app:error:cleared",e),e.redirect&&await Ze().replace(e.redirect),n.value=pd},Hd=e=>!!e&&typeof e=="object"&&aa in e,Ws=e=>{const t=Pd(e);return Object.defineProperty(t,aa,{value:!0,configurable:!1,writable:!1}),t},Ld=-1,Nd=-2,jd=-3,Fd=-4,Dd=-5,Ud=-6;function Bd(e,t){return Vd(JSON.parse(e),t)}function Vd(e,t){if(typeof e=="number")return s(e,!0);if(!Array.isArray(e)||e.length===0)throw new Error("Invalid input");const n=e,r=Array(n.length);function s(o,i=!1){if(o===Ld)return;if(o===jd)return NaN;if(o===Fd)return 1/0;if(o===Dd)return-1/0;if(o===Ud)return-0;if(i)throw new Error("Invalid input");if(o in r)return r[o];const l=n[o];if(!l||typeof l!="object")r[o]=l;else if(Array.isArray(l))if(typeof l[0]=="string"){const a=l[0],f=t==null?void 0:t[a];if(f)return r[o]=f(s(l[1]));switch(a){case"Date":r[o]=new Date(l[1]);break;case"Set":const u=new Set;r[o]=u;for(let p=1;p>>9)+65536).toString(16).substring(1,8).toLowerCase()}function si(e){return e._h||Ks(e._d?e._d:`${e.tag}:${e.textContent||e.innerHTML||""}:${Object.entries(e.props).map(([t,n])=>`${t}:${String(n)}`).join(",")}`)}function ua(e,t){const{props:n,tag:r}=e;if(Gd.includes(r))return r;if(r==="link"&&n.rel==="canonical")return"canonical";if(n.charset)return"charset";const s=["id"];r==="meta"&&s.push("name","property","http-equiv");for(const o of s)if(typeof n[o]<"u"){const i=String(n[o]);return`${r}:${o}:${i}`}return!1}function oi(e,t){return e==null?t||null:typeof e=="function"?e(t):e}function fa(e,t){const n=[],r=t.resolveKeyData||(o=>o.key),s=t.resolveValueData||(o=>o.value);for(const[o,i]of Object.entries(e))n.push(...(Array.isArray(i)?i:[i]).map(l=>{const a={key:o,value:l},f=s(a);return typeof f=="object"?fa(f,t):Array.isArray(f)?f:{[typeof t.key=="function"?t.key(a):t.key]:r(a),[typeof t.value=="function"?t.value(a):t.value]:f}}).flat());return n}function da(e,t){return Object.entries(e).map(([n,r])=>{if(typeof r=="object"&&(r=da(r,t)),t.resolve){const s=t.resolve({key:n,value:r});if(typeof s<"u")return s}return typeof r=="number"&&(r=r.toString()),typeof r=="string"&&t.wrapValue&&(r=r.replace(new RegExp(t.wrapValue,"g"),`\\${t.wrapValue}`),r=`${t.wrapValue}${r}${t.wrapValue}`),`${n}${t.keyValueSeparator||""}${r}`}).join(t.entrySeparator||"")}const Te=e=>({keyValue:e,metaKey:"property"}),Ur=e=>({keyValue:e}),qs={appleItunesApp:{unpack:{entrySeparator:", ",resolve({key:e,value:t}){return`${Xe(e)}=${t}`}}},articleExpirationTime:Te("article:expiration_time"),articleModifiedTime:Te("article:modified_time"),articlePublishedTime:Te("article:published_time"),bookReleaseDate:Te("book:release_date"),charset:{metaKey:"charset"},contentSecurityPolicy:{unpack:{entrySeparator:"; ",resolve({key:e,value:t}){return`${Xe(e)} ${t}`}},metaKey:"http-equiv"},contentType:{metaKey:"http-equiv"},defaultStyle:{metaKey:"http-equiv"},fbAppId:Te("fb:app_id"),msapplicationConfig:Ur("msapplication-Config"),msapplicationTileColor:Ur("msapplication-TileColor"),msapplicationTileImage:Ur("msapplication-TileImage"),ogAudioSecureUrl:Te("og:audio:secure_url"),ogAudioUrl:Te("og:audio"),ogImageSecureUrl:Te("og:image:secure_url"),ogImageUrl:Te("og:image"),ogSiteName:Te("og:site_name"),ogVideoSecureUrl:Te("og:video:secure_url"),ogVideoUrl:Te("og:video"),profileFirstName:Te("profile:first_name"),profileLastName:Te("profile:last_name"),profileUsername:Te("profile:username"),refresh:{metaKey:"http-equiv",unpack:{entrySeparator:";",resolve({key:e,value:t}){if(e==="seconds")return`${t}`}}},robots:{unpack:{entrySeparator:", ",resolve({key:e,value:t}){return typeof t=="boolean"?`${Xe(e)}`:`${Xe(e)}:${t}`}}},xUaCompatible:{metaKey:"http-equiv"}},ha=["og","book","article","profile"];function pa(e){var n;const t=Xe(e).split(":")[0];return ha.includes(t)?"property":((n=qs[e])==null?void 0:n.metaKey)||"name"}function zd(e){var t;return((t=qs[e])==null?void 0:t.keyValue)||Xe(e)}function Xe(e){const t=e.replace(/([A-Z])/g,"-$1").toLowerCase(),n=t.split("-")[0];return ha.includes(n)||n==="twitter"?e.replace(/([A-Z])/g,":$1").toLowerCase():t}function fs(e){if(Array.isArray(e))return e.map(n=>fs(n));if(typeof e!="object"||Array.isArray(e))return e;const t={};for(const[n,r]of Object.entries(e))t[Xe(n)]=fs(r);return t}function Jd(e,t){const n=qs[t];return t==="refresh"?`${e.seconds};url=${e.url}`:da(fs(e),{keyValueSeparator:"=",entrySeparator:", ",resolve({value:r,key:s}){if(r===null)return"";if(typeof r=="boolean")return`${s}`},...n==null?void 0:n.unpack})}const ga=["og:image","og:video","og:audio","twitter:image"];function ma(e){const t={};return Object.entries(e).forEach(([n,r])=>{String(r)!=="false"&&n&&(t[n]=r)}),t}function ii(e,t){const n=ma(t),r=Xe(e),s=pa(r);if(ga.includes(r)){const o={};return Object.entries(n).forEach(([i,l])=>{o[`${e}${i==="url"?"":`${i.charAt(0).toUpperCase()}${i.slice(1)}`}`]=l}),Gs(o).sort((i,l)=>{var a,f;return(((a=i[s])==null?void 0:a.length)||0)-(((f=l[s])==null?void 0:f.length)||0)})}return[{[s]:r,...n}]}function Gs(e){const t=[],n={};Object.entries(e).forEach(([s,o])=>{if(!Array.isArray(o)){if(typeof o=="object"&&o){if(ga.includes(Xe(s))){t.push(...ii(s,o));return}n[s]=ma(o)}else n[s]=o;return}o.forEach(i=>{t.push(...typeof i=="string"?Gs({[s]:i}):ii(s,i))})});const r=fa(n,{key({key:s}){return pa(s)},value({key:s}){return s==="charset"?"charset":"content"},resolveKeyData({key:s}){return zd(s)},resolveValueData({value:s,key:o}){return s===null?"_null":typeof s=="object"?Jd(s,o):typeof s=="number"?s.toString():s}});return[...t,...r].map(s=>(s.content==="_null"&&(s.content=null),s))}async function Xd(e,t,n){const r={tag:e,props:await ya(typeof t=="object"&&typeof t!="function"&&!(t instanceof Promise)?{...t}:{[["script","noscript","style"].includes(e)?"innerHTML":"textContent"]:t},["templateParams","titleTemplate"].includes(e))};return ca.forEach(s=>{const o=typeof r.props[s]<"u"?r.props[s]:n[s];typeof o<"u"&&((!["innerHTML","textContent","children"].includes(s)||Kd.includes(r.tag))&&(r[s==="children"?"innerHTML":s]=o),delete r.props[s])}),r.props.body&&(r.tagPosition="bodyClose",delete r.props.body),r.tag==="script"&&typeof r.innerHTML=="object"&&(r.innerHTML=JSON.stringify(r.innerHTML),r.props.type=r.props.type||"application/json"),Array.isArray(r.props.content)?r.props.content.map(s=>({...r,props:{...r.props,content:s}})):r}function Yd(e,t){var r;const n=e==="class"?" ":";";return typeof t=="object"&&!Array.isArray(t)&&(t=Object.entries(t).filter(([,s])=>s).map(([s,o])=>e==="style"?`${s}:${o}`:s)),(r=String(Array.isArray(t)?t.join(n):t))==null?void 0:r.split(n).filter(s=>s.trim()).filter(Boolean).join(n)}async function ya(e,t){for(const n of Object.keys(e)){if(["class","style"].includes(n)){e[n]=Yd(n,e[n]);continue}if(e[n]instanceof Promise&&(e[n]=await e[n]),!t&&!ca.includes(n)){const r=String(e[n]),s=n.startsWith("data-");r==="true"||r===""?e[n]=s?"true":!0:e[n]||(s&&r==="false"?e[n]="false":delete e[n])}}return e}const Qd=10;async function eh(e){const t=[];return Object.entries(e.resolvedInput).filter(([n,r])=>typeof r<"u"&&qd.includes(n)).forEach(([n,r])=>{const s=Wd(r);t.push(...s.map(o=>Xd(n,o,e)).flat())}),(await Promise.all(t)).flat().filter(Boolean).map((n,r)=>(n._e=e._i,e.mode&&(n._m=e.mode),n._p=(e._i<a&&a[f]||void 0,t):l=t[i],typeof l<"u"?(l||"").replace(/"/g,'\\"'):!1}let s=e;try{s=decodeURI(e)}catch{}return(s.match(/%(\w+\.+\w+)|%(\w+)/g)||[]).sort().reverse().forEach(i=>{const l=r(i.slice(1));typeof l=="string"&&(e=e.replace(new RegExp(`\\${i}(\\W|$)`,"g"),(a,f)=>`${l}${f}`).trim())}),e.includes(ot)&&(e.endsWith(ot)&&(e=e.slice(0,-ot.length).trim()),e.startsWith(ot)&&(e=e.slice(ot.length).trim()),e=e.replace(new RegExp(`\\${ot}\\s*\\${ot}`,"g"),ot),e=Nn(e,{separator:n},n)),e}async function _a(e,t={}){var u;const n=t.document||e.resolvedOptions.document;if(!n||!e.dirty)return;const r={shouldRender:!0,tags:[]};if(await e.hooks.callHook("dom:beforeRender",r),!r.shouldRender)return;const s=(await e.resolveTags()).map(c=>({tag:c,id:Ln.includes(c.tag)?si(c):c.tag,shouldRender:!0}));let o=e._dom;if(!o){o={elMap:{htmlAttrs:n.documentElement,bodyAttrs:n.body}};for(const c of["body","head"]){const d=(u=n[c])==null?void 0:u.children,p=[];for(const m of[...d].filter(_=>Ln.includes(_.tagName.toLowerCase()))){const _={tag:m.tagName.toLowerCase(),props:await ya(m.getAttributeNames().reduce((H,y)=>({...H,[y]:m.getAttribute(y)}),{})),innerHTML:m.innerHTML};let A=1,P=ua(_);for(;P&&p.find(H=>H._d===P);)P=`${P}:${A++}`;_._d=P||void 0,p.push(_),o.elMap[m.getAttribute("data-hid")||si(_)]=m}}}o.pendingSideEffects={...o.sideEffects||{}},o.sideEffects={};function i(c,d,p){const m=`${c}:${d}`;o.sideEffects[m]=p,delete o.pendingSideEffects[m]}function l({id:c,$el:d,tag:p}){const m=p.tag.endsWith("Attrs");o.elMap[c]=d,m||(["textContent","innerHTML"].forEach(_=>{p[_]&&p[_]!==d[_]&&(d[_]=p[_])}),i(c,"el",()=>{var _;(_=o.elMap[c])==null||_.remove(),delete o.elMap[c]}));for(const[_,A]of Object.entries(p._eventHandlers||{}))d.getAttribute(`data-${_}`)!==""&&((p.tag==="bodyAttrs"?n.defaultView:d).addEventListener(_.replace("on",""),A.bind(d)),d.setAttribute(`data-${_}`,""));Object.entries(p.props).forEach(([_,A])=>{const P=`attr:${_}`;if(_==="class")for(const H of(A||"").split(" ").filter(Boolean))m&&i(c,`${P}:${H}`,()=>d.classList.remove(H)),!d.classList.contains(H)&&d.classList.add(H);else if(_==="style")for(const H of(A||"").split(";").filter(Boolean)){const[y,...S]=H.split(":").map(T=>T.trim());i(c,`${P}:${y}`,()=>{d.style.removeProperty(y)}),d.style.setProperty(y,S.join(":"))}else d.getAttribute(_)!==A&&d.setAttribute(_,A===!0?"":String(A)),m&&i(c,P,()=>d.removeAttribute(_))})}const a=[],f={bodyClose:void 0,bodyOpen:void 0,head:void 0};for(const c of s){const{tag:d,shouldRender:p,id:m}=c;if(p){if(d.tag==="title"){n.title=d.textContent;continue}c.$el=c.$el||o.elMap[m],c.$el?l(c):Ln.includes(d.tag)&&a.push(c)}}for(const c of a){const d=c.tag.tagPosition||"head";c.$el=n.createElement(c.tag.tag),l(c),f[d]=f[d]||n.createDocumentFragment(),f[d].appendChild(c.$el)}for(const c of s)await e.hooks.callHook("dom:renderTag",c,n,i);f.head&&n.head.appendChild(f.head),f.bodyOpen&&n.body.insertBefore(f.bodyOpen,n.body.firstChild),f.bodyClose&&n.body.appendChild(f.bodyClose),Object.values(o.pendingSideEffects).forEach(c=>c()),e._dom=o,e.dirty=!1,await e.hooks.callHook("dom:rendered",{renders:s})}async function nh(e,t={}){const n=t.delayFn||(r=>setTimeout(r,10));return e._domUpdatePromise=e._domUpdatePromise||new Promise(r=>n(async()=>{await _a(e,t),delete e._domUpdatePromise,r()}))}function rh(e){return t=>{var r,s;const n=((s=(r=t.resolvedOptions.document)==null?void 0:r.head.querySelector('script[id="unhead:payload"]'))==null?void 0:s.innerHTML)||!1;return n&&t.push(JSON.parse(n)),{mode:"client",hooks:{"entries:updated":function(o){nh(o,e)}}}}}const sh=["templateParams","htmlAttrs","bodyAttrs"],oh={hooks:{"tag:normalise":function({tag:e}){["hid","vmid","key"].forEach(r=>{e.props[r]&&(e.key=e.props[r],delete e.props[r])});const n=ua(e)||(e.key?`${e.tag}:${e.key}`:!1);n&&(e._d=n)},"tags:resolve":function(e){const t={};e.tags.forEach(r=>{const s=(r.key?`${r.tag}:${r.key}`:r._d)||r._p,o=t[s];if(o){let l=r==null?void 0:r.tagDuplicateStrategy;if(!l&&sh.includes(r.tag)&&(l="merge"),l==="merge"){const a=o.props;["class","style"].forEach(f=>{a[f]&&(r.props[f]?(f==="style"&&!a[f].endsWith(";")&&(a[f]+=";"),r.props[f]=`${a[f]} ${r.props[f]}`):r.props[f]=a[f])}),t[s].props={...a,...r.props};return}else if(r._e===o._e){o._duped=o._duped||[],r._d=`${o._d}:${o._duped.length+1}`,o._duped.push(r);return}else if(rr(r)>rr(o))return}const i=Object.keys(r.props).length+(r.innerHTML?1:0)+(r.textContent?1:0);if(Ln.includes(r.tag)&&i===0){delete t[s];return}t[s]=r});const n=[];Object.values(t).forEach(r=>{const s=r._duped;delete r._duped,n.push(r),s&&n.push(...s)}),e.tags=n,e.tags=e.tags.filter(r=>!(r.tag==="meta"&&(r.props.name||r.props.property)&&!r.props.content))}}},ih={mode:"server",hooks:{"tags:resolve":function(e){const t={};e.tags.filter(n=>["titleTemplate","templateParams","title"].includes(n.tag)&&n._m==="server").forEach(n=>{t[n.tag]=n.tag.startsWith("title")?n.textContent:n.props}),Object.keys(t).length&&e.tags.push({tag:"script",innerHTML:JSON.stringify(t),props:{id:"unhead:payload",type:"application/json"}})}}},lh=["script","link","bodyAttrs"],ah=e=>({hooks:{"tags:resolve":function(t){for(const n of t.tags.filter(r=>lh.includes(r.tag)))Object.entries(n.props).forEach(([r,s])=>{r.startsWith("on")&&typeof s=="function"&&(e.ssr&&ci.includes(r)?n.props[r]=`this.dataset.${r}fired = true`:delete n.props[r],n._eventHandlers=n._eventHandlers||{},n._eventHandlers[r]=s)}),e.ssr&&n._eventHandlers&&(n.props.src||n.props.href)&&(n.key=n.key||Ks(n.props.src||n.props.href))},"dom:renderTag":function({$el:t,tag:n}){var r,s;for(const o of Object.keys((t==null?void 0:t.dataset)||{}).filter(i=>ci.some(l=>`${l}fired`===i))){const i=o.replace("fired","");(s=(r=n._eventHandlers)==null?void 0:r[i])==null||s.call(t,new Event(i.replace("on","")))}}}}),ch=["link","style","script","noscript"],uh={hooks:{"tag:normalise":({tag:e})=>{e.key&&ch.includes(e.tag)&&(e.props["data-hid"]=e._h=Ks(e.key))}}},fh={hooks:{"tags:resolve":e=>{const t=n=>{var r;return(r=e.tags.find(s=>s._d===n))==null?void 0:r._p};for(const{prefix:n,offset:r}of th)for(const s of e.tags.filter(o=>typeof o.tagPriority=="string"&&o.tagPriority.startsWith(n))){const o=t(s.tagPriority.replace(n,""));typeof o<"u"&&(s._p=o+r)}e.tags.sort((n,r)=>n._p-r._p).sort((n,r)=>rr(n)-rr(r))}}},dh={meta:"content",link:"href",htmlAttrs:"lang"},hh=e=>({hooks:{"tags:resolve":t=>{var l;const{tags:n}=t,r=(l=n.find(a=>a.tag==="title"))==null?void 0:l.textContent,s=n.findIndex(a=>a.tag==="templateParams"),o=s!==-1?n[s].props:{},i=o.separator||"|";delete o.separator,o.pageTitle=Nn(o.pageTitle||r||"",o,i);for(const a of n.filter(f=>f.processTemplateParams!==!1)){const f=dh[a.tag];f&&typeof a.props[f]=="string"?a.props[f]=Nn(a.props[f],o,i):(a.processTemplateParams===!0||["titleTemplate","title"].includes(a.tag))&&["innerHTML","textContent"].forEach(u=>{typeof a[u]=="string"&&(a[u]=Nn(a[u],o,i))})}e._templateParams=o,e._separator=i,t.tags=n.filter(a=>a.tag!=="templateParams")}}}),ph={hooks:{"tags:resolve":e=>{const{tags:t}=e;let n=t.findIndex(s=>s.tag==="titleTemplate");const r=t.findIndex(s=>s.tag==="title");if(r!==-1&&n!==-1){const s=oi(t[n].textContent,t[r].textContent);s!==null?t[r].textContent=s||t[r].textContent:delete t[r]}else if(n!==-1){const s=oi(t[n].textContent);s!==null&&(t[n].textContent=s,t[n].tag="title",n=-1)}n!==-1&&delete t[n],e.tags=t.filter(Boolean)}}},gh={hooks:{"tags:afterResolve":function(e){for(const t of e.tags)typeof t.innerHTML=="string"&&(t.innerHTML&&["application/ld+json","application/json"].includes(t.props.type)?t.innerHTML=t.innerHTML.replace(/{l.dirty=!0,t.callHook("entries:updated",l)};let s=0,o=[];const i=[],l={plugins:i,dirty:!1,resolvedOptions:e,hooks:t,headEntries(){return o},use(a){const f=typeof a=="function"?a(l):a;(!f.key||!i.some(u=>u.key===f.key))&&(i.push(f),ui(f.mode,n)&&t.addHooks(f.hooks||{}))},push(a,f){f==null||delete f.head;const u={_i:s++,input:a,...f};return ui(u.mode,n)&&(o.push(u),r()),{dispose(){o=o.filter(c=>c._i!==u._i),t.callHook("entries:updated",l),r()},patch(c){o=o.map(d=>(d._i===u._i&&(d.input=u.input=c),d)),r()}}},async resolveTags(){const a={tags:[],entries:[...o]};await t.callHook("entries:resolve",a);for(const f of a.entries){const u=f.resolvedInput||f.input;if(f.resolvedInput=await(f.transform?f.transform(u):u),f.resolvedInput)for(const c of await eh(f)){const d={tag:c,entry:f,resolvedOptions:l.resolvedOptions};await t.callHook("tag:normalise",d),a.tags.push(d.tag)}}return await t.callHook("tags:beforeResolve",a),await t.callHook("tags:resolve",a),await t.callHook("tags:afterResolve",a),a.tags},ssr:n};return[oh,ih,ah,uh,fh,hh,ph,gh,...(e==null?void 0:e.plugins)||[]].forEach(a=>l.use(a)),l.hooks.callHook("init",l),l}function _h(){return va}const vh=Fl.startsWith("3");function bh(e){return typeof e=="function"?e():X(e)}function sr(e,t=""){if(e instanceof Promise)return e;const n=bh(e);return!e||!n?n:Array.isArray(n)?n.map(r=>sr(r,t)):typeof n=="object"?Object.fromEntries(Object.entries(n).map(([r,s])=>r==="titleTemplate"||r.startsWith("on")?[r,X(s)]:[r,sr(s,r)])):n}const wh={hooks:{"entries:resolve":function(e){for(const t of e.entries)t.resolvedInput=sr(t.input)}}},ba="usehead";function xh(e){return{install(n){vh&&(n.config.globalProperties.$unhead=e,n.config.globalProperties.$head=e,n.provide(ba,e))}}.install}function Eh(e={}){e.domDelayFn=e.domDelayFn||(n=>Ge(()=>setTimeout(()=>n(),0)));const t=mh(e);return t.use(wh),t.install=xh(t),t}const ds=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{},hs="__unhead_injection_handler__";function Th(e){ds[hs]=e}function Sh(){if(hs in ds)return ds[hs]();const e=Qe(ba);return e||_h()}function Ch(e,t={}){const n=t.head||Sh();if(n)return n.ssr?n.push(e,t):Ah(n,e,t)}function Ah(e,t,n={}){const r=ae(!1),s=ae({});Zt(()=>{s.value=r.value?{}:sr(t)});const o=e.push(s.value,n);return On(s,l=>{o.patch(l)}),Ns()&&(pr(()=>{o.dispose()}),il(()=>{r.value=!0}),ol(()=>{r.value=!1})),o}function Rh(e,t){const{title:n,titleTemplate:r,...s}=e;return Ch({title:n,titleTemplate:r,_flatMeta:s},{...t,transform(o){const i=Gs({...o._flatMeta});return delete o._flatMeta,{...o,meta:i}}})}let jn,Fn;function Ph(){return jn=$fetch(Bs(`builds/meta/${qt().app.buildId}.json`),{responseType:"json"}),jn.then(e=>{Fn=Ed(e.matcher)}).catch(e=>{console.error("[nuxt] Error fetching app manifest.",e)}),jn}function vr(){return jn||Ph()}async function Zs(e){if(await vr(),!Fn)return console.error("[nuxt] Error creating app manifest matcher.",Fn),{};try{return Sd({},...Fn.matchAll(e).reverse())}catch(t){return console.error("[nuxt] Error matching route rules.",t),{}}}async function fi(e,t={}){const n=await Ih(e,t),r=me(),s=r._payloadCache=r._payloadCache||{};return n in s||(s[n]=xa(e).then(o=>o?wa(n).then(i=>i||(delete s[n],null)):(s[n]=null,null))),s[n]}const kh="_payload.json";async function Ih(e,t={}){const n=new URL(e,"http://localhost");if(n.host!=="localhost"||mt(n.pathname,{acceptRelative:!0}))throw new Error("Payload URL must not include hostname: "+e);const r=qt(),s=t.hash||(t.fresh?Date.now():r.app.buildId),o=r.app.cdnURL,i=o&&await xa(e)?o:r.app.baseURL;return yn(i,n.pathname,kh+(s?`?${s}`:""))}async function wa(e){const t=fetch(e).then(n=>n.text().then(Ea));try{return await t}catch(n){console.warn("[nuxt] Cannot load payload ",e,n)}return null}async function xa(e=ia().path){if(e=yr(e),(await vr()).prerendered.includes(e))return!0;const n=await Zs(e);return!!n.prerender&&!n.redirect}let kn=null;async function Mh(){if(kn)return kn;const e=document.getElementById("__NUXT_DATA__");if(!e)return{};const t=await Ea(e.textContent||""),n=e.dataset.src?await wa(e.dataset.src):void 0;return kn={...t,...n,...window.__NUXT__},kn}async function Ea(e){return await Bd(e,me()._payloadRevivers)}function Oh(e,t){me()._payloadRevivers[e]=t}const di={NuxtError:e=>Ws(e),EmptyShallowRef:e=>yo(e==="_"?void 0:e==="0n"?BigInt(0):Yn(e)),EmptyRef:e=>ae(e==="_"?void 0:e==="0n"?BigInt(0):Yn(e)),ShallowRef:e=>yo(e),ShallowReactive:e=>$t(e),Ref:e=>ae(e),Reactive:e=>Pt(e)},$h=yt({name:"nuxt:revive-payload:client",order:-30,async setup(e){let t,n;for(const r in di)Oh(r,di[r]);Object.assign(e.payload,([t,n]=Ql(()=>e.runWithContext(Mh)),t=await t,n(),t)),window.__NUXT__=e.payload}}),Hh=[],Lh=yt({name:"nuxt:head",enforce:"pre",setup(e){const t=Eh({plugins:Hh});Th(()=>me().vueApp._context.provides.usehead),e.vueApp.use(t);{let n=!0;const r=async()=>{n=!1,await _a(t)};t.hooks.hook("dom:beforeRender",s=>{s.shouldRender=!n}),e.hooks.hook("page:start",()=>{n=!0}),e.hooks.hook("page:finish",()=>{e.isHydrating||r()}),e.hooks.hook("app:error",r),e.hooks.hook("app:suspense:resolve",r)}}}),Nh=async e=>{let t,n;const r=([t,n]=Ql(()=>Zs(e.path)),t=await t,n(),t);if(r.redirect)return mt(r.redirect,{acceptRelative:!0})?(window.location.href=r.redirect,!1):r.redirect},jh=[Nh];function Br(e){typeof e=="object"&&(e=zl({pathname:e.path||"",search:Wl(e.query||{}),hash:e.hash||""}));const t=new URL(e.toString(),window.location.href);return{path:t.pathname,fullPath:e,query:Ds(t.search),hash:t.hash,params:{},name:void 0,matched:[],redirectedFrom:void 0,meta:{},href:e}}const Fh=yt({name:"nuxt:router",enforce:"pre",setup(e){const t=Lf(window.location.pathname,qt().app.baseURL)+window.location.search+window.location.hash,n=[],r={"navigate:before":[],"resolve:before":[],"navigate:after":[],error:[]},s=(c,d)=>(r[c].push(d),()=>r[c].splice(r[c].indexOf(d),1)),o=qt().app.baseURL,i=Pt(Br(t));async function l(c,d){try{const p=Br(c);for(const m of r["navigate:before"]){const _=await m(p,i);if(_===!1||_ instanceof Error)return;if(typeof _=="string"&&_.length)return l(_,!0)}for(const m of r["resolve:before"])await m(p,i);Object.assign(i,p),window.history[d?"replaceState":"pushState"]({},"",yn(o,p.fullPath)),e.isHydrating||await e.runWithContext($d);for(const m of r["navigate:after"])await m(p,i)}catch(p){for(const m of r.error)await m(p)}}const f={currentRoute:pe(()=>i),isReady:()=>Promise.resolve(),options:{},install:()=>Promise.resolve(),push:c=>l(c,!1),replace:c=>l(c,!0),back:()=>window.history.go(-1),go:c=>window.history.go(c),forward:()=>window.history.go(1),beforeResolve:c=>s("resolve:before",c),beforeEach:c=>s("navigate:before",c),afterEach:c=>s("navigate:after",c),onError:c=>s("error",c),resolve:Br,addRoute:(c,d)=>{n.push(d)},getRoutes:()=>n,hasRoute:c=>n.some(d=>d.name===c),removeRoute:c=>{const d=n.findIndex(p=>p.name===c);d!==-1&&n.splice(d,1)}};e.vueApp.component("RouterLink",Ue({functional:!0,props:{to:{type:String,required:!0},custom:Boolean,replace:Boolean,activeClass:String,exactActiveClass:String,ariaCurrentValue:String},setup:(c,{slots:d})=>{const p=()=>l(c.to,c.replace);return()=>{var _;const m=f.resolve(c.to);return c.custom?(_=d.default)==null?void 0:_.call(d,{href:c.to,navigate:p,route:m}):cn("a",{href:c.to,onClick:A=>(A.preventDefault(),p())},d)}}})),window.addEventListener("popstate",c=>{const d=c.target.location;f.replace(d.href.replace(d.origin,""))}),e._route=i,e._middleware=e._middleware||{global:[],named:{}};const u=e.payload.state._layout;return e.hooks.hookOnce("app:created",async()=>{f.beforeEach(async(c,d)=>{c.meta=Pt(c.meta||{}),e.isHydrating&&u&&!dt(c.meta.layout)&&(c.meta.layout=u),e._processingMiddleware=!0;{const p=new Set([...jh,...e._middleware.global]);{const m=await e.runWithContext(()=>Zs(c.path));if(m.appMiddleware)for(const _ in m.appMiddleware){const A=e._middleware.named[_];if(!A)return;m.appMiddleware[_]?p.add(A):p.delete(A)}}for(const m of p){const _=await e.runWithContext(()=>m(c,d));if(_!==!0&&(_||_===!1))return _}}}),f.afterEach(()=>{delete e._processingMiddleware}),await f.replace(t),jf(i.fullPath,t)||await e.runWithContext(()=>la(i.fullPath))}),{provide:{route:i,router:f}}}}),ps=globalThis.requestIdleCallback||(e=>{const t=Date.now(),n={didTimeout:!1,timeRemaining:()=>Math.max(0,50-(Date.now()-t))};return setTimeout(()=>{e(n)},1)}),Dh=globalThis.cancelIdleCallback||(e=>{clearTimeout(e)}),br=e=>{const t=me();t.isHydrating?t.hooks.hookOnce("app:suspense:resolve",()=>{ps(()=>e())}):ps(()=>e())},Uh=yt({name:"nuxt:payload",setup(e){Ze().beforeResolve(async(t,n)=>{if(t.path===n.path)return;const r=await fi(t.path);r&&Object.assign(e.static.data,r.data)}),br(()=>{var t;e.hooks.hook("link:prefetch",async n=>{const{hostname:r}=new URL(n,window.location.href);r===window.location.hostname&&await fi(n)}),((t=navigator.connection)==null?void 0:t.effectiveType)!=="slow-2g"&&setTimeout(vr,1e3)})}}),Bh=yt(()=>{const e=Ze();br(()=>{e.beforeResolve(async()=>{await new Promise(t=>{setTimeout(t,100),requestAnimationFrame(()=>{setTimeout(t,0)})})})})}),Vh=yt(e=>{let t;async function n(){const r=await vr();t&&clearTimeout(t),t=setTimeout(n,ti);try{const s=await $fetch(Bs("builds/latest.json")+`?${Date.now()}`);s.id!==r.id&&e.hooks.callHook("app:manifest:update",s)}catch{}}br(()=>{t=setTimeout(n,ti)})});function Wh(e={}){const t=e.path||window.location.pathname;let n={};try{n=Yn(sessionStorage.getItem("nuxt:reload")||"{}")}catch{}if(e.force||(n==null?void 0:n.path)!==t||(n==null?void 0:n.expires){r.clear()}),e.hook("app:chunkError",({error:o})=>{r.add(o)});function s(o){const l="href"in o&&o.href[0]==="#"?n.app.baseURL+o.href:yn(n.app.baseURL,o.fullPath);Wh({path:l,persistState:!0})}e.hook("app:manifest:update",()=>{t.beforeResolve(s)}),t.onError((o,i)=>{r.has(o)&&s(i)})}}),qh=yt({name:"nuxt:global-components"}),Gh=[$h,Lh,Fh,Uh,Bh,Vh,Kh,qh],Zh="modulepreload",zh=function(e,t){return new URL(e,t).href},hi={},Jh=function(t,n,r){let s=Promise.resolve();if(n&&n.length>0){const i=document.getElementsByTagName("link"),l=document.querySelector("meta[property=csp-nonce]"),a=(l==null?void 0:l.nonce)||(l==null?void 0:l.getAttribute("nonce"));s=Promise.allSettled(n.map(f=>{if(f=zh(f,r),f in hi)return;hi[f]=!0;const u=f.endsWith(".css"),c=u?'[rel="stylesheet"]':"";if(!!r)for(let m=i.length-1;m>=0;m--){const _=i[m];if(_.href===f&&(!u||_.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${f}"]${c}`))return;const p=document.createElement("link");if(p.rel=u?"stylesheet":Zh,u||(p.as="script"),p.crossOrigin="",p.href=f,a&&p.setAttribute("nonce",a),document.head.appendChild(p),u)return new Promise((m,_)=>{p.addEventListener("load",m),p.addEventListener("error",()=>_(new Error(`Unable to preload CSS for ${f}`)))})}))}function o(i){const l=new Event("vite:preloadError",{cancelable:!0});if(l.payload=i,window.dispatchEvent(l),!l.defaultPrevented)throw i}return s.then(i=>{for(const l of i||[])l.status==="rejected"&&o(l.reason);return t().catch(o)})},pi=(...e)=>Jh(...e).catch(t=>{const n=new Event("nuxt.preloadError");throw n.payload=t,window.dispatchEvent(n),t});async function Ta(e,t=Ze()){const{path:n,matched:r}=t.resolve(e);if(!r.length||(t._routePreloaded||(t._routePreloaded=new Set),t._routePreloaded.has(n)))return;const s=t._preloadPromises=t._preloadPromises||[];if(s.length>4)return Promise.all(s).then(()=>Ta(e,t));t._routePreloaded.add(n);const o=r.map(i=>{var l;return(l=i.components)==null?void 0:l.default}).filter(i=>typeof i=="function");for(const i of o){const l=Promise.resolve(i()).catch(()=>{}).finally(()=>s.splice(s.indexOf(l)));s.push(l)}await Promise.all(s)}const Xh=(...e)=>e.find(t=>t!==void 0);function Yh(e){const t=e.componentName||"NuxtLink";function n(s,o){if(!s||e.trailingSlash!=="append"&&e.trailingSlash!=="remove")return s;if(typeof s=="string")return gi(s,e.trailingSlash);const i="path"in s&&s.path!==void 0?s.path:o(s).path;return{...s,name:void 0,path:gi(i,e.trailingSlash)}}function r(s){const o=Ze(),i=qt(),l=pe(()=>!!s.target&&s.target!=="_self"),a=pe(()=>{const _=s.to||s.href||"";return typeof _=="string"&&mt(_,{acceptRelative:!0})}),f=wo("RouterLink"),u=f&&typeof f!="string"?f.useLink:void 0,c=pe(()=>{if(s.external)return!0;const _=s.to||s.href||"";return typeof _=="object"?!1:_===""||a.value}),d=pe(()=>{const _=s.to||s.href||"";return c.value?_:n(_,o.resolve)}),p=c.value||u==null?void 0:u({...s,to:d}),m=pe(()=>{var _;if(!d.value||a.value)return d.value;if(c.value){const A=typeof d.value=="object"&&"path"in d.value?us(d.value):d.value,P=typeof A=="object"?o.resolve(A).href:A;return n(P,o.resolve)}return typeof d.value=="object"?((_=o.resolve(d.value))==null?void 0:_.href)??null:n(yn(i.app.baseURL,d.value),o.resolve)});return{to:d,hasTarget:l,isAbsoluteUrl:a,isExternal:c,href:m,isActive:(p==null?void 0:p.isActive)??pe(()=>d.value===o.currentRoute.value.path),isExactActive:(p==null?void 0:p.isExactActive)??pe(()=>d.value===o.currentRoute.value.path),route:(p==null?void 0:p.route)??pe(()=>o.resolve(d.value)),async navigate(){await la(m.value,{replace:s.replace,external:c.value||l.value})}}}return Ue({name:t,props:{to:{type:[String,Object],default:void 0,required:!1},href:{type:[String,Object],default:void 0,required:!1},target:{type:String,default:void 0,required:!1},rel:{type:String,default:void 0,required:!1},noRel:{type:Boolean,default:void 0,required:!1},prefetch:{type:Boolean,default:void 0,required:!1},noPrefetch:{type:Boolean,default:void 0,required:!1},activeClass:{type:String,default:void 0,required:!1},exactActiveClass:{type:String,default:void 0,required:!1},prefetchedClass:{type:String,default:void 0,required:!1},replace:{type:Boolean,default:void 0,required:!1},ariaCurrentValue:{type:String,default:void 0,required:!1},external:{type:Boolean,default:void 0,required:!1},custom:{type:Boolean,default:void 0,required:!1}},useLink:r,setup(s,{slots:o}){const i=Ze(),{to:l,href:a,navigate:f,isExternal:u,hasTarget:c,isAbsoluteUrl:d}=r(s),p=ae(!1),m=ae(null),_=A=>{var P;m.value=s.custom?(P=A==null?void 0:A.$el)==null?void 0:P.nextElementSibling:A==null?void 0:A.$el};if(s.prefetch!==!1&&s.noPrefetch!==!0&&s.target!=="_blank"&&!ep()){const P=me();let H,y=null;pn(()=>{const S=Qh();br(()=>{H=ps(()=>{var T;(T=m==null?void 0:m.value)!=null&&T.tagName&&(y=S.observe(m.value,async()=>{y==null||y(),y=null;const w=typeof l.value=="string"?l.value:u.value?us(l.value):i.resolve(l.value).fullPath;await Promise.all([P.hooks.callHook("link:prefetch",w).catch(()=>{}),!u.value&&!c.value&&Ta(l.value,i).catch(()=>{})]),p.value=!0}))})})}),pr(()=>{H&&Dh(H),y==null||y(),y=null})}return()=>{var H;if(!u.value&&!c.value){const y={ref:_,to:l.value,activeClass:s.activeClass||e.activeClass,exactActiveClass:s.exactActiveClass||e.exactActiveClass,replace:s.replace,ariaCurrentValue:s.ariaCurrentValue,custom:s.custom};return s.custom||(p.value&&(y.class=s.prefetchedClass||e.prefetchedClass),y.rel=s.rel||void 0),cn(wo("RouterLink"),y,o.default)}const A=s.target||null,P=Xh(s.noRel?"":s.rel,e.externalRelAttribute,d.value||c.value?"noopener noreferrer":"")||null;return s.custom?o.default?o.default({href:a.value,navigate:f,get route(){if(!a.value)return;const y=new URL(a.value,window.location.href);return{path:y.pathname,fullPath:y.pathname,get query(){return Ds(y.search)},hash:y.hash,params:{},name:void 0,matched:[],redirectedFrom:void 0,meta:{},href:a.value}},rel:P,target:A,isExternal:u.value||c.value,isActive:!1,isExactActive:!1}):null:cn("a",{ref:m,href:a.value||null,rel:P,target:A},(H=o.default)==null?void 0:H.call(o))}}})}const zs=Yh(hd);function gi(e,t){const n=t==="append"?er:yr;return mt(e)&&!e.startsWith("http")?e:n(e,!0)}function Qh(){const e=me();if(e._observer)return e._observer;let t=null;const n=new Map,r=(o,i)=>(t||(t=new IntersectionObserver(l=>{for(const a of l){const f=n.get(a.target);(a.isIntersecting||a.intersectionRatio>0)&&f&&f()}})),n.set(o,i),t.observe(o),()=>{n.delete(o),t.unobserve(o),n.size===0&&(t.disconnect(),t=null)});return e._observer={observe:r}}function ep(){const e=navigator.connection;return!!(e&&(e.saveData||/2g/.test(e.effectiveType)))}const Js=_r("/logo.svg"),tp=_r("/github.svg");let np=Symbol("headlessui.useid"),rp=0;function Xs(){return Qe(np,()=>`${++rp}`)()}function de(e){var t;if(e==null||e.value==null)return null;let n=(t=e.value.$el)!=null?t:e.value;return n instanceof Node?n:null}function wr(e,t,...n){if(e in t){let s=t[e];return typeof s=="function"?s(...n):s}let r=new Error(`Tried to handle "${e}" but there is no handler defined. Only defined handlers are: ${Object.keys(t).map(s=>`"${s}"`).join(", ")}.`);throw Error.captureStackTrace&&Error.captureStackTrace(r,wr),r}var sp=Object.defineProperty,op=(e,t,n)=>t in e?sp(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,mi=(e,t,n)=>(op(e,typeof t!="symbol"?t+"":t,n),n);let ip=class{constructor(){mi(this,"current",this.detect()),mi(this,"currentId",0)}set(t){this.current!==t&&(this.currentId=0,this.current=t)}reset(){this.set(this.detect())}nextId(){return++this.currentId}get isServer(){return this.current==="server"}get isClient(){return this.current==="client"}detect(){return typeof window>"u"||typeof document>"u"?"server":"client"}},Ys=new ip;function Qs(e){if(Ys.isServer)return null;if(e instanceof Node)return e.ownerDocument;if(e!=null&&e.hasOwnProperty("value")){let t=de(e);if(t)return t.ownerDocument}return document}let gs=["[contentEditable=true]","[tabindex]","a[href]","area[href]","button:not([disabled])","iframe","input:not([disabled])","select:not([disabled])","textarea:not([disabled])"].map(e=>`${e}:not([tabindex='-1'])`).join(",");var ms=(e=>(e[e.First=1]="First",e[e.Previous=2]="Previous",e[e.Next=4]="Next",e[e.Last=8]="Last",e[e.WrapAround=16]="WrapAround",e[e.NoScroll=32]="NoScroll",e))(ms||{}),lp=(e=>(e[e.Error=0]="Error",e[e.Overflow=1]="Overflow",e[e.Success=2]="Success",e[e.Underflow=3]="Underflow",e))(lp||{}),ap=(e=>(e[e.Previous=-1]="Previous",e[e.Next=1]="Next",e))(ap||{});function Sa(e=document.body){return e==null?[]:Array.from(e.querySelectorAll(gs)).sort((t,n)=>Math.sign((t.tabIndex||Number.MAX_SAFE_INTEGER)-(n.tabIndex||Number.MAX_SAFE_INTEGER)))}var eo=(e=>(e[e.Strict=0]="Strict",e[e.Loose=1]="Loose",e))(eo||{});function to(e,t=0){var n;return e===((n=Qs(e))==null?void 0:n.body)?!1:wr(t,{0(){return e.matches(gs)},1(){let r=e;for(;r!==null;){if(r.matches(gs))return!0;r=r.parentElement}return!1}})}function Ca(e){let t=Qs(e);Ge(()=>{t&&!to(t.activeElement,0)&&up(e)})}var cp=(e=>(e[e.Keyboard=0]="Keyboard",e[e.Mouse=1]="Mouse",e))(cp||{});typeof window<"u"&&typeof document<"u"&&(document.addEventListener("keydown",e=>{e.metaKey||e.altKey||e.ctrlKey||(document.documentElement.dataset.headlessuiFocusVisible="")},!0),document.addEventListener("click",e=>{e.detail===1?delete document.documentElement.dataset.headlessuiFocusVisible:e.detail===0&&(document.documentElement.dataset.headlessuiFocusVisible="")},!0));function up(e){e==null||e.focus({preventScroll:!0})}let fp=["textarea","input"].join(",");function dp(e){var t,n;return(n=(t=e==null?void 0:e.matches)==null?void 0:t.call(e,fp))!=null?n:!1}function Aa(e,t=n=>n){return e.slice().sort((n,r)=>{let s=t(n),o=t(r);if(s===null||o===null)return 0;let i=s.compareDocumentPosition(o);return i&Node.DOCUMENT_POSITION_FOLLOWING?-1:i&Node.DOCUMENT_POSITION_PRECEDING?1:0})}function hp(e,t){return pp(Sa(),t,{relativeTo:e})}function pp(e,t,{sorted:n=!0,relativeTo:r=null,skipElements:s=[]}={}){var o;let i=(o=Array.isArray(e)?e.length>0?e[0].ownerDocument:document:e==null?void 0:e.ownerDocument)!=null?o:document,l=Array.isArray(e)?n?Aa(e):e:Sa(e);s.length>0&&l.length>1&&(l=l.filter(m=>!s.includes(m))),r=r??i.activeElement;let a=(()=>{if(t&5)return 1;if(t&10)return-1;throw new Error("Missing Focus.First, Focus.Previous, Focus.Next or Focus.Last")})(),f=(()=>{if(t&1)return 0;if(t&2)return Math.max(0,l.indexOf(r))-1;if(t&4)return Math.max(0,l.indexOf(r))+1;if(t&8)return l.length-1;throw new Error("Missing Focus.First, Focus.Previous, Focus.Next or Focus.Last")})(),u=t&32?{preventScroll:!0}:{},c=0,d=l.length,p;do{if(c>=d||c+d<=0)return 0;let m=f+c;if(t&16)m=(m+d)%d;else{if(m<0)return 3;if(m>=d)return 1}p=l[m],p==null||p.focus(u),c+=a}while(p!==i.activeElement);return t&6&&dp(p)&&p.select(),2}function gp(){return/iPhone/gi.test(window.navigator.platform)||/Mac/gi.test(window.navigator.platform)&&window.navigator.maxTouchPoints>0}function mp(){return/Android/gi.test(window.navigator.userAgent)}function yp(){return gp()||mp()}function In(e,t,n){Ys.isServer||Zt(r=>{document.addEventListener(e,t,n),r(()=>document.removeEventListener(e,t,n))})}function _p(e,t,n){Ys.isServer||Zt(r=>{window.addEventListener(e,t,n),r(()=>window.removeEventListener(e,t,n))})}function vp(e,t,n=pe(()=>!0)){function r(o,i){if(!n.value||o.defaultPrevented)return;let l=i(o);if(l===null||!l.getRootNode().contains(l))return;let a=function f(u){return typeof u=="function"?f(u()):Array.isArray(u)||u instanceof Set?u:[u]}(e);for(let f of a){if(f===null)continue;let u=f instanceof HTMLElement?f:de(f);if(u!=null&&u.contains(l)||o.composed&&o.composedPath().includes(u))return}return!to(l,eo.Loose)&&l.tabIndex!==-1&&o.preventDefault(),t(o,l)}let s=ae(null);In("pointerdown",o=>{var i,l;n.value&&(s.value=((l=(i=o.composedPath)==null?void 0:i.call(o))==null?void 0:l[0])||o.target)},!0),In("mousedown",o=>{var i,l;n.value&&(s.value=((l=(i=o.composedPath)==null?void 0:i.call(o))==null?void 0:l[0])||o.target)},!0),In("click",o=>{yp()||s.value&&(r(o,()=>s.value),s.value=null)},!0),In("touchend",o=>r(o,()=>o.target instanceof HTMLElement?o.target:null),!0),_p("blur",o=>r(o,()=>window.document.activeElement instanceof HTMLIFrameElement?window.document.activeElement:null),!0)}function yi(e,t){if(e)return e;let n=t??"button";if(typeof n=="string"&&n.toLowerCase()==="button")return"button"}function bp(e,t){let n=ae(yi(e.value.type,e.value.as));return pn(()=>{n.value=yi(e.value.type,e.value.as)}),Zt(()=>{var r;n.value||de(t)&&de(t)instanceof HTMLButtonElement&&!((r=de(t))!=null&&r.hasAttribute("type"))&&(n.value="button")}),n}function _i(e){return[e.screenX,e.screenY]}function wp(){let e=ae([-1,-1]);return{wasMoved(t){let n=_i(t);return e.value[0]===n[0]&&e.value[1]===n[1]?!1:(e.value=n,!0)},update(t){e.value=_i(t)}}}function xp({container:e,accept:t,walk:n,enabled:r}){Zt(()=>{let s=e.value;if(!s||r!==void 0&&!r.value)return;let o=Qs(e);if(!o)return;let i=Object.assign(a=>t(a),{acceptNode:t}),l=o.createTreeWalker(s,NodeFilter.SHOW_ELEMENT,i,!1);for(;l.nextNode();)n(l.currentNode)})}var ys=(e=>(e[e.None=0]="None",e[e.RenderStrategy=1]="RenderStrategy",e[e.Static=2]="Static",e))(ys||{}),Ep=(e=>(e[e.Unmount=0]="Unmount",e[e.Hidden=1]="Hidden",e))(Ep||{});function xr({visible:e=!0,features:t=0,ourProps:n,theirProps:r,...s}){var o;let i=Pa(r,n),l=Object.assign(s,{props:i});if(e||t&2&&i.static)return Vr(l);if(t&1){let a=(o=i.unmount)==null||o?0:1;return wr(a,{0(){return null},1(){return Vr({...s,props:{...i,hidden:!0,style:{display:"none"}}})}})}return Vr(l)}function Vr({props:e,attrs:t,slots:n,slot:r,name:s}){var o,i;let{as:l,...a}=Tp(e,["unmount","static"]),f=(o=n.default)==null?void 0:o.call(n,r),u={};if(r){let c=!1,d=[];for(let[p,m]of Object.entries(r))typeof m=="boolean"&&(c=!0),m===!0&&d.push(p);c&&(u["data-headlessui-state"]=d.join(" "))}if(l==="template"){if(f=Ra(f??[]),Object.keys(a).length>0||Object.keys(t).length>0){let[c,...d]=f??[];if(!Sp(c)||d.length>0)throw new Error(['Passing props on "template"!',"",`The current component <${s} /> is rendering a "template".`,"However we need to passthrough the following props:",Object.keys(a).concat(Object.keys(t)).map(_=>_.trim()).filter((_,A,P)=>P.indexOf(_)===A).sort((_,A)=>_.localeCompare(A)).map(_=>` - ${_}`).join(` +`),"","You can apply a few solutions:",['Add an `as="..."` prop, to ensure that we render an actual element instead of a "template".',"Render a single element as the child so that we can forward the props onto that element."].map(_=>` - ${_}`).join(` +`)].join(` +`));let p=Pa((i=c.props)!=null?i:{},a,u),m=et(c,p,!0);for(let _ in p)_.startsWith("on")&&(m.props||(m.props={}),m.props[_]=p[_]);return m}return Array.isArray(f)&&f.length===1?f[0]:f}return cn(l,Object.assign({},a,u),{default:()=>f})}function Ra(e){return e.flatMap(t=>t.type===he?Ra(t.children):[t])}function Pa(...e){if(e.length===0)return{};if(e.length===1)return e[0];let t={},n={};for(let r of e)for(let s in r)s.startsWith("on")&&typeof r[s]=="function"?(n[s]!=null||(n[s]=[]),n[s].push(r[s])):t[s]=r[s];if(t.disabled||t["aria-disabled"])return Object.assign(t,Object.fromEntries(Object.keys(n).map(r=>[r,void 0])));for(let r in n)Object.assign(t,{[r](s,...o){let i=n[r];for(let l of i){if(s instanceof Event&&s.defaultPrevented)return;l(s,...o)}}});return t}function Tp(e,t=[]){let n=Object.assign({},e);for(let r of t)r in n&&delete n[r];return n}function Sp(e){return e==null?!1:typeof e.type=="string"||typeof e.type=="object"||typeof e.type=="function"}let ka=Symbol("Context");var fn=(e=>(e[e.Open=1]="Open",e[e.Closed=2]="Closed",e[e.Closing=4]="Closing",e[e.Opening=8]="Opening",e))(fn||{});function Cp(){return Qe(ka,null)}function Ap(e){gr(ka,e)}var be=(e=>(e.Space=" ",e.Enter="Enter",e.Escape="Escape",e.Backspace="Backspace",e.Delete="Delete",e.ArrowLeft="ArrowLeft",e.ArrowUp="ArrowUp",e.ArrowRight="ArrowRight",e.ArrowDown="ArrowDown",e.Home="Home",e.End="End",e.PageUp="PageUp",e.PageDown="PageDown",e.Tab="Tab",e))(be||{});function Rp(e){throw new Error("Unexpected object: "+e)}var Oe=(e=>(e[e.First=0]="First",e[e.Previous=1]="Previous",e[e.Next=2]="Next",e[e.Last=3]="Last",e[e.Specific=4]="Specific",e[e.Nothing=5]="Nothing",e))(Oe||{});function Pp(e,t){let n=t.resolveItems();if(n.length<=0)return null;let r=t.resolveActiveIndex(),s=r??-1;switch(e.focus){case 0:{for(let o=0;o=0;--o)if(!t.resolveDisabled(n[o],o,n))return o;return r}case 2:{for(let o=s+1;o=0;--o)if(!t.resolveDisabled(n[o],o,n))return o;return r}case 4:{for(let o=0;o{let o=document.getElementById(s);if(o){let i=o.getAttribute("aria-label");return typeof i=="string"?i.trim():bi(o).trim()}return null}).filter(Boolean);if(r.length>0)return r.join(", ")}return bi(e).trim()}function Ip(e){let t=ae(""),n=ae("");return()=>{let r=de(e);if(!r)return"";let s=r.innerText;if(t.value===s)return n.value;let o=kp(r).trim().toLowerCase();return t.value=s,n.value=o,o}}var Mp=(e=>(e[e.Open=0]="Open",e[e.Closed=1]="Closed",e))(Mp||{}),Op=(e=>(e[e.Pointer=0]="Pointer",e[e.Other=1]="Other",e))(Op||{});function $p(e){requestAnimationFrame(()=>requestAnimationFrame(e))}let Ia=Symbol("MenuContext");function Er(e){let t=Qe(Ia,null);if(t===null){let n=new Error(`<${e} /> is missing a parent component.`);throw Error.captureStackTrace&&Error.captureStackTrace(n,Er),n}return t}let Dn=Ue({name:"Menu",props:{as:{type:[Object,String],default:"template"}},setup(e,{slots:t,attrs:n}){let r=ae(1),s=ae(null),o=ae(null),i=ae([]),l=ae(""),a=ae(null),f=ae(1);function u(d=p=>p){let p=a.value!==null?i.value[a.value]:null,m=Aa(d(i.value.slice()),A=>de(A.dataRef.domRef)),_=p?m.indexOf(p):null;return _===-1&&(_=null),{items:m,activeItemIndex:_}}let c={menuState:r,buttonRef:s,itemsRef:o,items:i,searchQuery:l,activeItemIndex:a,activationTrigger:f,closeMenu:()=>{r.value=1,a.value=null},openMenu:()=>r.value=0,goToItem(d,p,m){let _=u(),A=Pp(d===Oe.Specific?{focus:Oe.Specific,id:p}:{focus:d},{resolveItems:()=>_.items,resolveActiveIndex:()=>_.activeItemIndex,resolveId:P=>P.id,resolveDisabled:P=>P.dataRef.disabled});l.value="",a.value=A,f.value=m??1,i.value=_.items},search(d){let p=l.value!==""?0:1;l.value+=d.toLowerCase();let m=(a.value!==null?i.value.slice(a.value+p).concat(i.value.slice(0,a.value+p)):i.value).find(A=>A.dataRef.textValue.startsWith(l.value)&&!A.dataRef.disabled),_=m?i.value.indexOf(m):-1;_===-1||_===a.value||(a.value=_,f.value=1)},clearSearch(){l.value=""},registerItem(d,p){let m=u(_=>[..._,{id:d,dataRef:p}]);i.value=m.items,a.value=m.activeItemIndex,f.value=1},unregisterItem(d){let p=u(m=>{let _=m.findIndex(A=>A.id===d);return _!==-1&&m.splice(_,1),m});i.value=p.items,a.value=p.activeItemIndex,f.value=1}};return vp([s,o],(d,p)=>{var m;c.closeMenu(),to(p,eo.Loose)||(d.preventDefault(),(m=de(s))==null||m.focus())},pe(()=>r.value===0)),gr(Ia,c),Ap(pe(()=>wr(r.value,{0:fn.Open,1:fn.Closed}))),()=>{let d={open:r.value===0,close:c.closeMenu};return xr({ourProps:{},theirProps:e,slot:d,slots:t,attrs:n,name:"Menu"})}}}),Un=Ue({name:"MenuButton",props:{disabled:{type:Boolean,default:!1},as:{type:[Object,String],default:"button"},id:{type:String,default:null}},setup(e,{attrs:t,slots:n,expose:r}){var s;let o=(s=e.id)!=null?s:`headlessui-menu-button-${Xs()}`,i=Er("MenuButton");r({el:i.buttonRef,$el:i.buttonRef});function l(c){switch(c.key){case be.Space:case be.Enter:case be.ArrowDown:c.preventDefault(),c.stopPropagation(),i.openMenu(),Ge(()=>{var d;(d=de(i.itemsRef))==null||d.focus({preventScroll:!0}),i.goToItem(Oe.First)});break;case be.ArrowUp:c.preventDefault(),c.stopPropagation(),i.openMenu(),Ge(()=>{var d;(d=de(i.itemsRef))==null||d.focus({preventScroll:!0}),i.goToItem(Oe.Last)});break}}function a(c){switch(c.key){case be.Space:c.preventDefault();break}}function f(c){e.disabled||(i.menuState.value===0?(i.closeMenu(),Ge(()=>{var d;return(d=de(i.buttonRef))==null?void 0:d.focus({preventScroll:!0})})):(c.preventDefault(),i.openMenu(),$p(()=>{var d;return(d=de(i.itemsRef))==null?void 0:d.focus({preventScroll:!0})})))}let u=bp(pe(()=>({as:e.as,type:t.type})),i.buttonRef);return()=>{var c;let d={open:i.menuState.value===0},{...p}=e,m={ref:i.buttonRef,id:o,type:u.value,"aria-haspopup":"menu","aria-controls":(c=de(i.itemsRef))==null?void 0:c.id,"aria-expanded":i.menuState.value===0,onKeydown:l,onKeyup:a,onClick:f};return xr({ourProps:m,theirProps:p,slot:d,attrs:t,slots:n,name:"MenuButton"})}}}),Bn=Ue({name:"MenuItems",props:{as:{type:[Object,String],default:"div"},static:{type:Boolean,default:!1},unmount:{type:Boolean,default:!0},id:{type:String,default:null}},setup(e,{attrs:t,slots:n,expose:r}){var s;let o=(s=e.id)!=null?s:`headlessui-menu-items-${Xs()}`,i=Er("MenuItems"),l=ae(null);r({el:i.itemsRef,$el:i.itemsRef}),xp({container:pe(()=>de(i.itemsRef)),enabled:pe(()=>i.menuState.value===0),accept(d){return d.getAttribute("role")==="menuitem"?NodeFilter.FILTER_REJECT:d.hasAttribute("role")?NodeFilter.FILTER_SKIP:NodeFilter.FILTER_ACCEPT},walk(d){d.setAttribute("role","none")}});function a(d){var p;switch(l.value&&clearTimeout(l.value),d.key){case be.Space:if(i.searchQuery.value!=="")return d.preventDefault(),d.stopPropagation(),i.search(d.key);case be.Enter:if(d.preventDefault(),d.stopPropagation(),i.activeItemIndex.value!==null){let m=i.items.value[i.activeItemIndex.value];(p=de(m.dataRef.domRef))==null||p.click()}i.closeMenu(),Ca(de(i.buttonRef));break;case be.ArrowDown:return d.preventDefault(),d.stopPropagation(),i.goToItem(Oe.Next);case be.ArrowUp:return d.preventDefault(),d.stopPropagation(),i.goToItem(Oe.Previous);case be.Home:case be.PageUp:return d.preventDefault(),d.stopPropagation(),i.goToItem(Oe.First);case be.End:case be.PageDown:return d.preventDefault(),d.stopPropagation(),i.goToItem(Oe.Last);case be.Escape:d.preventDefault(),d.stopPropagation(),i.closeMenu(),Ge(()=>{var m;return(m=de(i.buttonRef))==null?void 0:m.focus({preventScroll:!0})});break;case be.Tab:d.preventDefault(),d.stopPropagation(),i.closeMenu(),Ge(()=>hp(de(i.buttonRef),d.shiftKey?ms.Previous:ms.Next));break;default:d.key.length===1&&(i.search(d.key),l.value=setTimeout(()=>i.clearSearch(),350));break}}function f(d){switch(d.key){case be.Space:d.preventDefault();break}}let u=Cp(),c=pe(()=>u!==null?(u.value&fn.Open)===fn.Open:i.menuState.value===0);return()=>{var d,p;let m={open:i.menuState.value===0},{..._}=e,A={"aria-activedescendant":i.activeItemIndex.value===null||(d=i.items.value[i.activeItemIndex.value])==null?void 0:d.id,"aria-labelledby":(p=de(i.buttonRef))==null?void 0:p.id,id:o,onKeydown:a,onKeyup:f,role:"menu",tabIndex:0,ref:i.itemsRef};return xr({ourProps:A,theirProps:_,slot:m,attrs:t,slots:n,features:ys.RenderStrategy|ys.Static,visible:c.value,name:"MenuItems"})}}}),Vn=Ue({name:"MenuItem",inheritAttrs:!1,props:{as:{type:[Object,String],default:"template"},disabled:{type:Boolean,default:!1},id:{type:String,default:null}},setup(e,{slots:t,attrs:n,expose:r}){var s;let o=(s=e.id)!=null?s:`headlessui-menu-item-${Xs()}`,i=Er("MenuItem"),l=ae(null);r({el:l,$el:l});let a=pe(()=>i.activeItemIndex.value!==null?i.items.value[i.activeItemIndex.value].id===o:!1),f=Ip(l),u=pe(()=>({disabled:e.disabled,get textValue(){return f()},domRef:l}));pn(()=>i.registerItem(o,u)),ks(()=>i.unregisterItem(o)),Zt(()=>{i.menuState.value===0&&a.value&&i.activationTrigger.value!==0&&Ge(()=>{var P,H;return(H=(P=de(l))==null?void 0:P.scrollIntoView)==null?void 0:H.call(P,{block:"nearest"})})});function c(P){if(e.disabled)return P.preventDefault();i.closeMenu(),Ca(de(i.buttonRef))}function d(){if(e.disabled)return i.goToItem(Oe.Nothing);i.goToItem(Oe.Specific,o)}let p=wp();function m(P){p.update(P)}function _(P){p.wasMoved(P)&&(e.disabled||a.value||i.goToItem(Oe.Specific,o,0))}function A(P){p.wasMoved(P)&&(e.disabled||a.value&&i.goToItem(Oe.Nothing))}return()=>{let{disabled:P}=e,H={active:a.value,disabled:P,close:i.closeMenu},{...y}=e;return xr({ourProps:{id:o,ref:l,role:"menuitem",tabIndex:P===!0?void 0:-1,"aria-disabled":P===!0?!0:void 0,disabled:void 0,onClick:c,onFocus:d,onPointerenter:m,onMouseenter:m,onPointermove:_,onMousemove:_,onPointerleave:A,onMouseleave:A},theirProps:{...n,...y},slot:H,attrs:n,slots:t,name:"MenuItem"})}}});function Hp(e,t){return ne(),ue("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20",fill:"currentColor","aria-hidden":"true","data-slot":"icon"},[N("path",{"fill-rule":"evenodd",d:"M2 4.75A.75.75 0 0 1 2.75 4h14.5a.75.75 0 0 1 0 1.5H2.75A.75.75 0 0 1 2 4.75ZM2 10a.75.75 0 0 1 .75-.75h14.5a.75.75 0 0 1 0 1.5H2.75A.75.75 0 0 1 2 10Zm0 5.25a.75.75 0 0 1 .75-.75h14.5a.75.75 0 0 1 0 1.5H2.75a.75.75 0 0 1-.75-.75Z","clip-rule":"evenodd"})])}function Lp(e,t){return ne(),ue("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20",fill:"currentColor","aria-hidden":"true","data-slot":"icon"},[N("path",{d:"M10.75 16.82A7.462 7.462 0 0 1 15 15.5c.71 0 1.396.098 2.046.282A.75.75 0 0 0 18 15.06v-11a.75.75 0 0 0-.546-.721A9.006 9.006 0 0 0 15 3a8.963 8.963 0 0 0-4.25 1.065V16.82ZM9.25 4.065A8.963 8.963 0 0 0 5 3c-.85 0-1.673.118-2.454.339A.75.75 0 0 0 2 4.06v11a.75.75 0 0 0 .954.721A7.506 7.506 0 0 1 5 15.5c1.579 0 3.042.487 4.25 1.32V4.065Z"})])}function Np(e,t){return ne(),ue("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20",fill:"currentColor","aria-hidden":"true","data-slot":"icon"},[N("path",{"fill-rule":"evenodd",d:"M16.403 12.652a3 3 0 0 0 0-5.304 3 3 0 0 0-3.75-3.751 3 3 0 0 0-5.305 0 3 3 0 0 0-3.751 3.75 3 3 0 0 0 0 5.305 3 3 0 0 0 3.75 3.751 3 3 0 0 0 5.305 0 3 3 0 0 0 3.751-3.75Zm-2.546-4.46a.75.75 0 0 0-1.214-.883l-3.483 4.79-1.88-1.88a.75.75 0 1 0-1.06 1.061l2.5 2.5a.75.75 0 0 0 1.137-.089l4-5.5Z","clip-rule":"evenodd"})])}function Ma(e,t){return ne(),ue("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20",fill:"currentColor","aria-hidden":"true","data-slot":"icon"},[N("path",{"fill-rule":"evenodd",d:"M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z","clip-rule":"evenodd"})])}function jp(e,t){return ne(),ue("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20",fill:"currentColor","aria-hidden":"true","data-slot":"icon"},[N("path",{d:"M14 6H6v8h8V6Z"}),N("path",{"fill-rule":"evenodd",d:"M9.25 3V1.75a.75.75 0 0 1 1.5 0V3h1.5V1.75a.75.75 0 0 1 1.5 0V3h.5A2.75 2.75 0 0 1 17 5.75v.5h1.25a.75.75 0 0 1 0 1.5H17v1.5h1.25a.75.75 0 0 1 0 1.5H17v1.5h1.25a.75.75 0 0 1 0 1.5H17v.5A2.75 2.75 0 0 1 14.25 17h-.5v1.25a.75.75 0 0 1-1.5 0V17h-1.5v1.25a.75.75 0 0 1-1.5 0V17h-1.5v1.25a.75.75 0 0 1-1.5 0V17h-.5A2.75 2.75 0 0 1 3 14.25v-.5H1.75a.75.75 0 0 1 0-1.5H3v-1.5H1.75a.75.75 0 0 1 0-1.5H3v-1.5H1.75a.75.75 0 0 1 0-1.5H3v-.5A2.75 2.75 0 0 1 5.75 3h.5V1.75a.75.75 0 0 1 1.5 0V3h1.5ZM4.5 5.75c0-.69.56-1.25 1.25-1.25h8.5c.69 0 1.25.56 1.25 1.25v8.5c0 .69-.56 1.25-1.25 1.25h-8.5c-.69 0-1.25-.56-1.25-1.25v-8.5Z","clip-rule":"evenodd"})])}function Fp(e,t){return ne(),ue("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20",fill:"currentColor","aria-hidden":"true","data-slot":"icon"},[N("path",{d:"M12 4.467c0-.405.262-.75.559-1.027.276-.257.441-.584.441-.94 0-.828-.895-1.5-2-1.5s-2 .672-2 1.5c0 .362.171.694.456.953.29.265.544.6.544.994a.968.968 0 0 1-1.024.974 39.655 39.655 0 0 1-3.014-.306.75.75 0 0 0-.847.847c.14.993.242 1.999.306 3.014A.968.968 0 0 1 4.447 10c-.393 0-.729-.253-.994-.544C3.194 9.17 2.862 9 2.5 9 1.672 9 1 9.895 1 11s.672 2 1.5 2c.356 0 .683-.165.94-.441.276-.297.622-.559 1.027-.559a.997.997 0 0 1 1.004 1.03 39.747 39.747 0 0 1-.319 3.734.75.75 0 0 0 .64.842c1.05.146 2.111.252 3.184.318A.97.97 0 0 0 10 16.948c0-.394-.254-.73-.545-.995C9.171 15.693 9 15.362 9 15c0-.828.895-1.5 2-1.5s2 .672 2 1.5c0 .356-.165.683-.441.94-.297.276-.559.622-.559 1.027a.998.998 0 0 0 1.03 1.005c1.337-.05 2.659-.162 3.961-.337a.75.75 0 0 0 .644-.644c.175-1.302.288-2.624.337-3.961A.998.998 0 0 0 16.967 12c-.405 0-.75.262-1.027.559-.257.276-.584.441-.94.441-.828 0-1.5-.895-1.5-2s.672-2 1.5-2c.362 0 .694.17.953.455.265.291.601.545.995.545a.97.97 0 0 0 .976-1.024 41.159 41.159 0 0 0-.318-3.184.75.75 0 0 0-.842-.64c-1.228.164-2.473.271-3.734.319A.997.997 0 0 1 12 4.467Z"})])}function Dp(e,t){return ne(),ue("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20",fill:"currentColor","aria-hidden":"true","data-slot":"icon"},[N("path",{d:"M4.464 3.162A2 2 0 0 1 6.28 2h7.44a2 2 0 0 1 1.816 1.162l1.154 2.5c.067.145.115.291.145.438A3.508 3.508 0 0 0 16 6H4c-.288 0-.568.035-.835.1.03-.147.078-.293.145-.438l1.154-2.5Z"}),N("path",{"fill-rule":"evenodd",d:"M2 9.5a2 2 0 0 1 2-2h12a2 2 0 1 1 0 4H4a2 2 0 0 1-2-2Zm13.24 0a.75.75 0 0 1 .75-.75H16a.75.75 0 0 1 .75.75v.01a.75.75 0 0 1-.75.75h-.01a.75.75 0 0 1-.75-.75V9.5Zm-2.25-.75a.75.75 0 0 0-.75.75v.01c0 .414.336.75.75.75H13a.75.75 0 0 0 .75-.75V9.5a.75.75 0 0 0-.75-.75h-.01ZM2 15a2 2 0 0 1 2-2h12a2 2 0 1 1 0 4H4a2 2 0 0 1-2-2Zm13.24 0a.75.75 0 0 1 .75-.75H16a.75.75 0 0 1 .75.75v.01a.75.75 0 0 1-.75.75h-.01a.75.75 0 0 1-.75-.75V15Zm-2.25-.75a.75.75 0 0 0-.75.75v.01c0 .414.336.75.75.75H13a.75.75 0 0 0 .75-.75V15a.75.75 0 0 0-.75-.75h-.01Z","clip-rule":"evenodd"})])}function Up(e,t){return ne(),ue("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20",fill:"currentColor","aria-hidden":"true","data-slot":"icon"},[N("path",{"fill-rule":"evenodd",d:"M14.5 10a4.5 4.5 0 0 0 4.284-5.882c-.105-.324-.51-.391-.752-.15L15.34 6.66a.454.454 0 0 1-.493.11 3.01 3.01 0 0 1-1.618-1.616.455.455 0 0 1 .11-.494l2.694-2.692c.24-.241.174-.647-.15-.752a4.5 4.5 0 0 0-5.873 4.575c.055.873-.128 1.808-.8 2.368l-7.23 6.024a2.724 2.724 0 1 0 3.837 3.837l6.024-7.23c.56-.672 1.495-.855 2.368-.8.096.007.193.01.291.01ZM5 16a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z","clip-rule":"evenodd"}),N("path",{d:"M14.5 11.5c.173 0 .345-.007.514-.022l3.754 3.754a2.5 2.5 0 0 1-3.536 3.536l-4.41-4.41 2.172-2.607c.052-.063.147-.138.342-.196.202-.06.469-.087.777-.067.128.008.257.012.387.012ZM6 4.586l2.33 2.33a.452.452 0 0 1-.08.09L6.8 8.214 4.586 6H3.309a.5.5 0 0 1-.447-.276l-1.7-3.402a.5.5 0 0 1 .093-.577l.49-.49a.5.5 0 0 1 .577-.094l3.402 1.7A.5.5 0 0 1 6 3.31v1.277Z"})])}const Bp={class:"relative z-50 mx-auto flex max-w-7xl justify-between px-4 py-4 sm:px-6 lg:px-8"},Vp={class:"relative z-10 flex items-center gap-16"},Wp=N("img",{src:Js,class:"h-12 w-10 object-contain",alt:"SecHub Logo"},null,-1),Kp={class:"hidden items-center lg:flex lg:gap-10"},qp={class:"flex items-center gap-6"},Gp={class:"lg:hidden"},Zp={class:"flex items-center"},zp={class:"lg:hidden"},Jp={class:"flex items-center"},Xp=N("span",{class:"sr-only"},"Open main menu",-1),Yp=N("a",{href:"https://github.com/mercedes-benz/sechub",target:"_blank"},[N("img",{src:tp,class:"size-6 object-contain",alt:"GitHub Logo"})],-1),Qp=Ue({__name:"Header",setup(e){const t=[{title:"Getting Started",href:"https://mercedes-benz.github.io/sechub/latest/sechub-getting-started.html"},{title:"Client",href:"https://mercedes-benz.github.io/sechub/latest/sechub-client.html"},{title:"Rest API",href:"https://mercedes-benz.github.io/sechub/latest/sechub-restapi.html"},{title:"Product Delegation Server (PDS)",href:"https://mercedes-benz.github.io/sechub/latest/sechub-product-delegation-server.html"},{title:"Operations",href:"https://mercedes-benz.github.io/sechub/latest/sechub-operations.html"},{title:"Developer - Quickstart Guide",href:"https://mercedes-benz.github.io/sechub/latest/sechub-developer-quickstart-guide.html"},{title:"Developer - Architecture",href:"https://mercedes-benz.github.io/sechub/latest/sechub-architecture.html"},{title:"Developer - Technical",href:"https://mercedes-benz.github.io/sechub/latest/sechub-techdoc.html"}],n=[{title:"Downloads",href:"#download"},{title:"Collaboration",href:"https://github.com/mercedes-benz/sechub/blob/develop/CONTRIBUTING.md"}];return(r,s)=>{const o=zs;return ne(),ue("header",null,[N("nav",null,[N("div",Bp,[N("div",Vp,[F(o,{"aria-label":"Home",to:"/"},{default:se(()=>[Wp]),_:1}),N("div",Kp,[F(X(Dn),{as:"div",class:"relative inline-block"},{default:se(()=>[N("div",null,[F(X(Un),{class:"menu-item"},{default:se(()=>[we(" Documentation ")]),_:1})]),F(Vt,{"enter-active-class":"transition duration-100 ease-out","enter-from-class":"transform scale-95 opacity-0","enter-to-class":"transform scale-100 opacity-100","leave-active-class":"transition duration-75 ease-in","leave-from-class":"transform scale-100 opacity-100","leave-to-class":"transform scale-95 opacity-0"},{default:se(()=>[F(X(Bn),{class:"absolute left-0 mt-2 w-56 origin-top-left divide-y divide-gray-100 rounded-md bg-white shadow-lg ring-1 ring-black/5 focus:outline-none"},{default:se(()=>[(ne(),ue(he,null,Ht(t,i=>F(X(Vn),{key:i.title},{default:se(()=>[F(o,{to:i.href,target:"_blank",class:"group flex w-full items-center rounded-md px-2 py-2 text-sm text-gray-700 transition-colors duration-300 hover:bg-fern-500 hover:text-white"},{default:se(()=>[we(Je(i.title),1)]),_:2},1032,["to"])]),_:2},1024)),64))]),_:1})]),_:1})]),_:1}),(ne(),ue(he,null,Ht(n,i=>F(o,{key:i.title,to:i.href,class:"menu-item",target:i.href.startsWith("https")?"_blank":void 0},{default:se(()=>[we(Je(i.title),1)]),_:2},1032,["to","target"])),64))])]),N("div",qp,[N("div",Gp,[F(X(Dn),{as:"div",class:"relative"},{default:se(()=>[N("div",Zp,[F(X(Un),{class:"menu-item flex"},{default:se(()=>[we(" Docs "),F(X(Ma),{class:"ml-1 h-5 w-5","aria-hidden":"true"})]),_:1})]),F(Vt,{"enter-active-class":"transition duration-100 ease-out","enter-from-class":"transform scale-95 opacity-0","enter-to-class":"transform scale-100 opacity-100","leave-active-class":"transition duration-75 ease-in","leave-from-class":"transform scale-100 opacity-100","leave-to-class":"transform scale-95 opacity-0"},{default:se(()=>[F(X(Bn),{class:"absolute right-0 mt-2 w-56 origin-top-right divide-y divide-gray-100 rounded-md bg-white shadow-lg ring-1 ring-black/5 focus:outline-none"},{default:se(()=>[(ne(),ue(he,null,Ht(t,i=>F(X(Vn),{key:i.title},{default:se(()=>[F(o,{to:i.href,target:"_blank",class:"group flex w-full items-center rounded-md px-2 py-2 text-sm text-gray-700 transition-colors duration-300 hover:bg-fern-500 hover:text-white"},{default:se(()=>[we(Je(i.title),1)]),_:2},1032,["to"])]),_:2},1024)),64))]),_:1})]),_:1})]),_:1})]),N("div",zp,[F(X(Dn),{as:"div",class:"relative"},{default:se(()=>[N("div",Jp,[F(X(Un),null,{default:se(()=>[Xp,F(X(Hp),{class:"size-6"})]),_:1})]),F(Vt,{"enter-active-class":"transition duration-100 ease-out","enter-from-class":"transform scale-95 opacity-0","enter-to-class":"transform scale-100 opacity-100","leave-active-class":"transition duration-75 ease-in","leave-from-class":"transform scale-100 opacity-100","leave-to-class":"transform scale-95 opacity-0"},{default:se(()=>[F(X(Bn),{class:"absolute right-0 mt-2 w-56 origin-top-right divide-y divide-gray-100 rounded-md bg-white shadow-lg ring-1 ring-black/5 focus:outline-none"},{default:se(()=>[(ne(),ue(he,null,Ht(n,i=>F(X(Vn),{key:i.title},{default:se(()=>[F(o,{to:i.href,class:"group flex w-full items-center rounded-md px-2 py-2 text-sm text-gray-700 transition-colors duration-300 hover:bg-fern-500 hover:text-white",target:i.href.startsWith("https")?"_blank":void 0},{default:se(()=>[we(Je(i.title),1)]),_:2},1032,["to","target"])]),_:2},1024)),64))]),_:1})]),_:1})]),_:1})]),Yp])])])])}}}),no=(e,t)=>{const n=e.__vccOpts||e;for(const[r,s]of t)n[r]=s;return n},eg={},tg={class:"absolute left-1/2 top-4 h-[1026px] w-[1026px] -translate-x-1/3 stroke-gray-300/70 [mask-image:linear-gradient(to_bottom,white_20%,transparent_75%)] sm:top-16 sm:-translate-x-1/2 lg:-top-16 lg:ml-12 xl:-top-14 xl:ml-0"},ng=Hs('',2),rg=[ng];function sg(e,t){return ne(),ue("div",tg,rg)}const og=no(eg,[["render",sg]]),ig={class:"overflow-hidden pt-16 lg:pb-32 xl:pb-36"},lg={class:"container"},ag={class:"lg:grid lg:grid-cols-12 lg:gap-x-8 lg:gap-y-0"},cg=N("div",{class:"relative z-10 mx-auto max-w-2xl lg:col-span-7 lg:max-w-none lg:pt-6 xl:col-span-6"},[N("h1",{class:"text-4xl font-medium tracking-tight text-gray-900"},"SecHub - One API to secure them all"),N("p",{class:"mt-6 text-lg text-gray-600"}," The free and open-source security platform SecHub, provides a central API to test software with different security tools. "),N("p",{class:"mt-6 text-lg text-gray-600"}," SecHub supports many free and open-source as well as proprietary security tools covering SAST (Static Application Security Testing), DAST (Dynamic Application Security Testing), Secret scanners, Infrastructure scanners, License scanners and more… ")],-1),ug={class:"relative mt-10 sm:mt-20 lg:col-span-5 lg:row-span-2 lg:mt-0 xl:col-span-6"},fg=N("div",{class:"pb-10 [mask-image:linear-gradient(to_bottom,white_60%,transparent)] sm:mx-0 lg:absolute lg:-inset-x-10 lg:-bottom-20 lg:-top-10 lg:h-auto lg:px-0 lg:pt-10 xl:-bottom-32"},[N("div",{class:"relative mx-auto max-w-64 lg:max-w-80"},[N("img",{src:Js,alt:"SecHub Logo"})])],-1),dg=Ue({__name:"Hero",setup(e){return(t,n)=>{const r=og;return ne(),ue("section",ig,[N("div",lg,[N("div",ag,[cg,N("div",ug,[F(r),fg])])])])}}}),hg={"aria-label":"Features of SecHub",class:"bg-fern-600 py-32"},pg={class:"container"},gg=N("div",{class:"mx-auto max-w-2xl sm:text-center"},[N("h2",{class:"text-3xl font-medium tracking-tight text-white"},"Your Security Guardian."),N("p",{class:"mt-2 text-lg text-white"},[we(" SecHub orchestrates different security tools by one API layer. "),N("br"),we(" Users interact with the SecHub Server, eliminating the need for projects to integrate vendor plugins for each security tool. ")])],-1),mg={role:"list",class:"mx-auto mt-16 grid max-w-2xl grid-cols-1 gap-6 text-sm sm:mt-20 sm:grid-cols-2 md:gap-y-10 lg:max-w-none lg:grid-cols-3"},yg={class:"mt-6 font-semibold text-white"},_g={class:"mt-2 text-white"},vg=Ue({__name:"Features",setup(e){const t=[{title:"API Orchestration",description:"SecHub orchestrates tools via an unified API, simplifying integration for projects without the need for different vendor plugins.",icon:Dp},{title:"Easy to use Client",description:"The SecHub client is written in Go and easy to use on your system of choice.",icon:Fp},{title:"License Flexibility",description:"SecHub is MIT licensed. This ensures freedom in use, modification and distribution, fostering collaboration and adoption across all kind of projects.",icon:Np},{title:"Comprehensive Documentation",description:"Extensive documentation covers Getting Started, Architecture, Client, RestAPI, Operations and more. Available on our documentation.",icon:Lp},{title:"Integration with Build Systems and IDEs",description:"Seamless integration with every build system and multiple developer IDEs adds security to your code without additional complexity.",icon:Up},{title:"Product Delegation Server (PDS)",description:"Explore SecHub's PDS, a vital component providing you the choice of many existing securtiy tools for use with SecHub. It also allows an easy integration of new securtiy tools.",icon:jp}];return(n,r)=>(ne(),ue("section",hg,[N("div",pg,[gg,N("ul",mg,[(ne(),ue(he,null,Ht(t,s=>N("li",{key:s.title,class:"rounded-2xl border border-fern-300 p-8 transition-colors duration-300 hover:bg-fern-500"},[(ne(),St(fl(s.icon),{class:"size-10 text-white"})),N("h3",yg,Je(s.title),1),N("p",_g,Je(s.description),1)])),64))])])]))}}),bg={},wg={class:"absolute left-20 top-1/2 -translate-y-1/2 sm:left-1/2 sm:-translate-x-1/2"},xg=Hs('',1),Eg=[xg];function Tg(e,t){return ne(),ue("div",wg,Eg)}const Sg=no(bg,[["render",Tg]]),Cg={"aria-label":"Download Sechub",id:"download",class:"relative bg-gray-100 py-32"},Ag={class:"container"},Rg={class:"mx-auto max-w-md sm:text-center"},Pg=N("h2",{class:"text-3xl font-medium tracking-tight text-gray-900 sm:text-4xl"},"Start Using Today!",-1),kg=N("p",{class:"mt-4 text-lg text-gray-700"},[we(" Download SecHub for seamless security integration."),N("br"),we("Orchestrates security tools through an unified API."),N("br"),we("MIT License. ")],-1),Ig={__name:"Download",setup(e){const t=[{name:"Client",href:"https://mercedes-benz.github.io/sechub/latest/client-download.html"},{name:"Server",href:"https://mercedes-benz.github.io/sechub/latest/server-download.html"},{name:"Product Delegation Server",href:"https://mercedes-benz.github.io/sechub/latest/pds-download.html"},{name:"Kubernetes Images and Charts",href:"https://github.com/mercedes-benz/sechub/packages"}];return(n,r)=>{const s=Sg,o=zs;return ne(),ue("section",Cg,[F(s),N("div",Ag,[N("div",Rg,[Pg,kg,F(X(Dn),{as:"div",class:"relative mt-8 inline-block text-left"},{default:se(()=>[N("div",null,[F(X(Un),{class:"button"},{default:se(()=>[we(" Download "),F(X(Ma),{class:"-mr-1 ml-2 h-5 w-5 text-white hover:text-fern-100","aria-hidden":"true"})]),_:1})]),F(Vt,{"enter-active-class":"transition duration-100 ease-out","enter-from-class":"transform scale-95 opacity-0","enter-to-class":"transform scale-100 opacity-100","leave-active-class":"transition duration-75 ease-in","leave-from-class":"transform scale-100 opacity-100","leave-to-class":"transform scale-95 opacity-0"},{default:se(()=>[F(X(Bn),{class:"absolute right-0 mt-2 w-56 origin-top-right divide-y divide-gray-100 rounded-md bg-white shadow-lg ring-1 ring-black/5 focus:outline-none"},{default:se(()=>[(ne(),ue(he,null,Ht(t,i=>F(X(Vn),{key:i.name},{default:se(({active:l})=>[F(o,{to:i.href,target:"_blank",class:"group flex w-full rounded-md px-2 py-2 text-sm text-gray-700 transition-colors duration-300 hover:bg-fern-500 hover:text-white"},{default:se(()=>[we(Je(i.name),1)]),_:2},1032,["to"])]),_:2},1024)),64))]),_:1})]),_:1})]),_:1})])])])}}},Mg={},Og={class:"border-t border-gray-200"},$g={class:"container"},Hg={class:"flex flex-col items-start justify-between gap-y-12 pb-6 pt-16 lg:flex-row lg:items-center lg:py-16"},Lg=Hs('
SecHub Logo

SecHub

Your security guardian.

',1),Ng={class:"mt-6 text-sm text-gray-500"},jg=N("a",{href:"https://www.mercedes-benz-techinnovation.com/en/imprint/"},"Impressum",-1),Fg=N("div",{class:"lg:w-64"},[N("p",{class:"text-base font-semibold text-gray-900"},"You want to innovate SecHub?"),N("p",{class:"mt-1 text-sm text-gray-700"}," We attach great importance to open and transparent communication for all parts of the community. ")],-1);function Dg(e,t){const n=zs;return ne(),ue("footer",Og,[N("div",$g,[N("div",Hg,[N("div",null,[Lg,N("p",Ng,[we("© "+Je(new Date().getFullYear())+" Mercedes-Benz Tech Innovation GmbH - ",1),jg,we(".")])]),F(n,{class:"group relative -mx-4 flex items-center self-stretch p-4 transition-colors hover:bg-gray-100 sm:self-auto sm:rounded-2xl lg:mx-0 lg:self-auto lg:p-6",to:"https://github.com/mercedes-benz/sechub/blob/develop/CONTRIBUTING.md",target:"_blank"},{default:se(()=>[Fg]),_:1})])])])}const Ug=no(Mg,[["render",Dg]]),Bg=Ue({__name:"app",setup(e){return Rh({title:"SecHub | Your security guardian.",description:"SecHub is a free and open-source security platform that provides a central API to test software with different security tools.",ogImage:"/og.jpg"}),(t,n)=>{const r=Qp,s=dg,o=vg,i=Ig,l=Ug;return ne(),ue(he,null,[F(r),N("main",null,[F(s),F(o),F(i)]),F(l)],64)}}}),Vg={__name:"nuxt-error-page",props:{error:Object},setup(e){const n=e.error;n.stack&&n.stack.split(` +`).splice(1).map(c=>({text:c.replace("webpack:/","").replace(".vue",".js").trim(),internal:c.includes("node_modules")&&!c.includes(".cache")||c.includes("internal")||c.includes("new Promise")})).map(c=>`${c.text}`).join(` +`);const r=Number(n.statusCode||500),s=r===404,o=n.statusMessage??(s?"Page Not Found":"Internal Server Error"),i=n.message||n.toString(),l=void 0,u=s?bo(()=>pi(()=>import("./CdjxlzT3.js"),__vite__mapDeps([0,1]),import.meta.url).then(c=>c.default||c)):bo(()=>pi(()=>import("./CxV5zgZb.js"),__vite__mapDeps([2,3]),import.meta.url).then(c=>c.default||c));return(c,d)=>(ne(),St(X(u),Ka(Ll({statusCode:X(r),statusMessage:X(o),description:X(i),stack:X(l)})),null,16))}},Wg={key:0},wi={__name:"nuxt-root",setup(e){const t=()=>null,n=me(),r=n.deferHydration();if(n.isHydrating){const a=n.hooks.hookOnce("app:error",r);Ze().beforeEach(a)}const s=!1;gr(oa,ia()),n.hooks.callHookWith(a=>a.map(f=>f()),"vue:setup");const o=Vs(),i=!1;al((a,f,u)=>{if(n.hooks.callHook("vue:error",a,f,u).catch(c=>console.error("[nuxt] Error in `vue:error` hook",c)),Hd(a)&&(a.fatal||a.unhandled))return n.runWithContext(()=>Od(a)),!1});const l=!1;return(a,f)=>(ne(),St(yu,{onResolve:X(r)},{default:se(()=>[X(i)?(ne(),ue("div",Wg)):X(o)?(ne(),St(X(Vg),{key:1,error:X(o)},null,8,["error"])):X(l)?(ne(),St(X(t),{key:2,context:X(l)},null,8,["context"])):X(s)?(ne(),St(fl(X(s)),{key:3})):(ne(),St(X(Bg),{key:4}))]),_:1},8,["onResolve"]))}};let xi;{let e;xi=async function(){var i,l;if(e)return e;const r=!!((i=window.__NUXT__)!=null&&i.serverRendered||((l=document.getElementById("__NUXT_DATA__"))==null?void 0:l.dataset.ssr)==="true")?cf(wi):af(wi),s=yd({vueApp:r});async function o(a){await s.callHook("app:error",a),s.payload.error=s.payload.error||Ws(a)}r.config.errorHandler=o;try{await bd(s,Gh)}catch(a){o(a)}try{await s.hooks.callHook("app:created",r),await s.hooks.callHook("app:beforeMount",r),r.mount(gd),await s.hooks.callHook("app:mounted",r),await Ge()}catch(a){o(a)}return r.config.errorHandler===o&&(r.config.errorHandler=void 0),r},e=xi().catch(t=>{throw console.error("Error while mounting app:",t),t})}export{no as _,N as a,F as b,ue as c,we as d,zs as e,qg as f,ne as o,Kg as p,Je as t,Ch as u,se as w}; diff --git a/docs/_nuxt/CdjxlzT3.js b/docs/_nuxt/CdjxlzT3.js new file mode 100644 index 0000000000..7cb23e4fc0 --- /dev/null +++ b/docs/_nuxt/CdjxlzT3.js @@ -0,0 +1 @@ +import{_ as r,u as s,o as i,c as u,a as e,t as o,b as c,w as d,d as l,e as p,p as h,f as b}from"./B3oZ3Xfb.js";const f=t=>(h("data-v-922baad2"),t=t(),b(),t),g={class:"font-sans antialiased bg-white dark:bg-black text-black dark:text-white grid min-h-screen place-content-center overflow-hidden"},x=f(()=>e("div",{class:"fixed left-0 right-0 spotlight z-10"},null,-1)),m={class:"max-w-520px text-center z-20"},y=["textContent"],_=["textContent"],k={class:"w-full flex items-center justify-center"},w={__name:"error-404",props:{appName:{type:String,default:"Nuxt"},version:{type:String,default:""},statusCode:{type:Number,default:404},statusMessage:{type:String,default:"Not Found"},description:{type:String,default:"Sorry, the page you are looking for could not be found."},backHome:{type:String,default:"Go back home"}},setup(t){const n=t;return s({title:`${n.statusCode} - ${n.statusMessage} | ${n.appName}`,script:[],style:[{children:'*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:var(--un-default-border-color, #e5e7eb)}:before,:after{--un-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}h1{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}h1,p{margin:0}*,:before,:after{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / .5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: }'}]}),(z,S)=>{const a=p;return i(),u("div",g,[x,e("div",m,[e("h1",{class:"text-8xl sm:text-10xl font-medium mb-8",textContent:o(t.statusCode)},null,8,y),e("p",{class:"text-xl px-8 sm:px-0 sm:text-4xl font-light mb-16 leading-tight",textContent:o(t.description)},null,8,_),e("div",k,[c(a,{to:"/",class:"gradient-border text-md sm:text-xl py-2 px-4 sm:py-3 sm:px-6 cursor-pointer"},{default:d(()=>[l(o(t.backHome),1)]),_:1})])])])}}},C=r(w,[["__scopeId","data-v-922baad2"]]);export{C as default}; diff --git a/docs/_nuxt/CxV5zgZb.js b/docs/_nuxt/CxV5zgZb.js new file mode 100644 index 0000000000..ebfd7ce0f9 --- /dev/null +++ b/docs/_nuxt/CxV5zgZb.js @@ -0,0 +1 @@ +import{_ as a,u as o,o as s,c as i,a as e,t as r,p as u,f as c}from"./B3oZ3Xfb.js";const l=t=>(u("data-v-1e3620c9"),t=t(),c(),t),d={class:"font-sans antialiased bg-white dark:bg-black text-black dark:text-white grid min-h-screen place-content-center overflow-hidden"},p=l(()=>e("div",{class:"fixed -bottom-1/2 left-0 right-0 h-1/2 spotlight"},null,-1)),h={class:"max-w-520px text-center"},g=["textContent"],b=["textContent"],f={__name:"error-500",props:{appName:{type:String,default:"Nuxt"},version:{type:String,default:""},statusCode:{type:Number,default:500},statusMessage:{type:String,default:"Server error"},description:{type:String,default:"This page is temporarily unavailable."}},setup(t){const n=t;return o({title:`${n.statusCode} - ${n.statusMessage} | ${n.appName}`,script:[],style:[{children:'*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:var(--un-default-border-color, #e5e7eb)}:before,:after{--un-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}h1{font-size:inherit;font-weight:inherit}h1,p{margin:0}*,:before,:after{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / .5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: }'}]}),(m,x)=>(s(),i("div",d,[p,e("div",h,[e("h1",{class:"text-8xl sm:text-10xl font-medium mb-8",textContent:r(t.statusCode)},null,8,g),e("p",{class:"text-xl px-8 sm:px-0 sm:text-4xl font-light mb-16 leading-tight",textContent:r(t.description)},null,8,b)])]))}},k=a(f,[["__scopeId","data-v-1e3620c9"]]);export{k as default}; diff --git a/docs/_nuxt/builds/latest.json b/docs/_nuxt/builds/latest.json new file mode 100644 index 0000000000..8e669e8017 --- /dev/null +++ b/docs/_nuxt/builds/latest.json @@ -0,0 +1 @@ +{"id":"3d0297fe-7927-480d-bf98-ca001666f7a0","timestamp":1733914960560} \ No newline at end of file diff --git a/docs/_nuxt/builds/meta/3d0297fe-7927-480d-bf98-ca001666f7a0.json b/docs/_nuxt/builds/meta/3d0297fe-7927-480d-bf98-ca001666f7a0.json new file mode 100644 index 0000000000..9d406e7b4e --- /dev/null +++ b/docs/_nuxt/builds/meta/3d0297fe-7927-480d-bf98-ca001666f7a0.json @@ -0,0 +1 @@ +{"id":"3d0297fe-7927-480d-bf98-ca001666f7a0","timestamp":1733914960560,"matcher":{"static":{},"wildcard":{},"dynamic":{}},"prerendered":["/"]} \ No newline at end of file diff --git a/docs/_nuxt/entry.BaEZ_Hj7.css b/docs/_nuxt/entry.BaEZ_Hj7.css new file mode 100644 index 0000000000..1121166ad3 --- /dev/null +++ b/docs/_nuxt/entry.BaEZ_Hj7.css @@ -0,0 +1 @@ +html{scroll-behavior:smooth}.button{border-radius:.375rem;display:inline-flex;justify-content:center;width:100%;--tw-bg-opacity:1;background-color:#467e3b;background-color:rgb(70 126 59/var(--tw-bg-opacity));font-size:.875rem;font-weight:500;line-height:1.25rem;padding:.5rem 1rem;--tw-text-opacity:1;color:#fff;color:rgb(255 255 255/var(--tw-text-opacity))}.button:hover{--tw-bg-opacity:1;background-color:#3a6431;background-color:rgb(58 100 49/var(--tw-bg-opacity))}.button:focus{outline:2px solid transparent;outline-offset:2px}.button:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color),var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) #ffffffbf,0 0 #0000;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-color:hsla(0,0%,100%,.75)}.button.is-outlined{border-width:1px;--tw-border-opacity:1;background-color:transparent;border-color:#467e3b;border-color:rgb(70 126 59/var(--tw-border-opacity));--tw-text-opacity:1;color:#467e3b;color:rgb(70 126 59/var(--tw-text-opacity))}.button.is-outlined:hover{--tw-bg-opacity:1;background-color:#f5faf3;background-color:rgb(245 250 243/var(--tw-bg-opacity))}.container{margin-left:auto;margin-right:auto;max-width:80rem;padding-left:1rem;padding-right:1rem}@media (min-width:640px){.container{padding-left:1.5rem;padding-right:1.5rem}}@media (min-width:1024px){.container{padding-left:2rem;padding-right:2rem}}.menu-item{border-radius:.5rem;font-size:.875rem;line-height:1.25rem;margin:-.5rem -.75rem;padding:.5rem .75rem;position:relative;--tw-text-opacity:1;color:#374151;color:rgb(55 65 81/var(--tw-text-opacity));transition-delay:.15s;transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.menu-item:hover{--tw-text-opacity:1;color:#467e3b;color:rgb(70 126 59/var(--tw-text-opacity));transition-delay:0s} diff --git a/docs/_nuxt/error-404.DYxFu4PM.css b/docs/_nuxt/error-404.DYxFu4PM.css new file mode 100644 index 0000000000..a84f24192c --- /dev/null +++ b/docs/_nuxt/error-404.DYxFu4PM.css @@ -0,0 +1 @@ +.spotlight[data-v-922baad2]{background:linear-gradient(45deg,#00dc82,#36e4da 50%,#0047e1);bottom:-30vh;filter:blur(20vh);height:40vh}.gradient-border[data-v-922baad2]{-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);border-radius:.5rem;position:relative}@media (prefers-color-scheme:light){.gradient-border[data-v-922baad2]{background-color:#ffffff4d}.gradient-border[data-v-922baad2]:before{background:linear-gradient(90deg,#e2e2e2,#e2e2e2 25%,#00dc82,#36e4da 75%,#0047e1)}}@media (prefers-color-scheme:dark){.gradient-border[data-v-922baad2]{background-color:#1414144d}.gradient-border[data-v-922baad2]:before{background:linear-gradient(90deg,#303030,#303030 25%,#00dc82,#36e4da 75%,#0047e1)}}.gradient-border[data-v-922baad2]:before{background-size:400% auto;border-radius:.5rem;bottom:0;content:"";left:0;-webkit-mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);-webkit-mask-composite:xor;mask-composite:exclude;opacity:.5;padding:2px;position:absolute;right:0;top:0;transition:background-position .3s ease-in-out,opacity .2s ease-in-out;width:100%}.gradient-border[data-v-922baad2]:hover:before{background-position:-50% 0;opacity:1}.fixed[data-v-922baad2]{position:fixed}.left-0[data-v-922baad2]{left:0}.right-0[data-v-922baad2]{right:0}.z-10[data-v-922baad2]{z-index:10}.z-20[data-v-922baad2]{z-index:20}.grid[data-v-922baad2]{display:grid}.mb-16[data-v-922baad2]{margin-bottom:4rem}.mb-8[data-v-922baad2]{margin-bottom:2rem}.max-w-520px[data-v-922baad2]{max-width:520px}.min-h-screen[data-v-922baad2]{min-height:100vh}.w-full[data-v-922baad2]{width:100%}.flex[data-v-922baad2]{display:flex}.cursor-pointer[data-v-922baad2]{cursor:pointer}.place-content-center[data-v-922baad2]{place-content:center}.items-center[data-v-922baad2]{align-items:center}.justify-center[data-v-922baad2]{justify-content:center}.overflow-hidden[data-v-922baad2]{overflow:hidden}.bg-white[data-v-922baad2]{--un-bg-opacity:1;background-color:#fff;background-color:rgb(255 255 255/var(--un-bg-opacity))}.px-4[data-v-922baad2]{padding-left:1rem;padding-right:1rem}.px-8[data-v-922baad2]{padding-left:2rem;padding-right:2rem}.py-2[data-v-922baad2]{padding-bottom:.5rem;padding-top:.5rem}.text-center[data-v-922baad2]{text-align:center}.text-8xl[data-v-922baad2]{font-size:6rem;line-height:1}.text-xl[data-v-922baad2]{font-size:1.25rem;line-height:1.75rem}.text-black[data-v-922baad2]{--un-text-opacity:1;color:#000;color:rgb(0 0 0/var(--un-text-opacity))}.font-light[data-v-922baad2]{font-weight:300}.font-medium[data-v-922baad2]{font-weight:500}.leading-tight[data-v-922baad2]{line-height:1.25}.font-sans[data-v-922baad2]{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.antialiased[data-v-922baad2]{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}@media (prefers-color-scheme:dark){.dark\:bg-black[data-v-922baad2]{--un-bg-opacity:1;background-color:#000;background-color:rgb(0 0 0/var(--un-bg-opacity))}.dark\:text-white[data-v-922baad2]{--un-text-opacity:1;color:#fff;color:rgb(255 255 255/var(--un-text-opacity))}}@media (min-width:640px){.sm\:px-0[data-v-922baad2]{padding-left:0;padding-right:0}.sm\:px-6[data-v-922baad2]{padding-left:1.5rem;padding-right:1.5rem}.sm\:py-3[data-v-922baad2]{padding-bottom:.75rem;padding-top:.75rem}.sm\:text-4xl[data-v-922baad2]{font-size:2.25rem;line-height:2.5rem}.sm\:text-xl[data-v-922baad2]{font-size:1.25rem;line-height:1.75rem}} diff --git a/docs/_nuxt/error-500.PGmg907S.css b/docs/_nuxt/error-500.PGmg907S.css new file mode 100644 index 0000000000..6e42251fa2 --- /dev/null +++ b/docs/_nuxt/error-500.PGmg907S.css @@ -0,0 +1 @@ +.spotlight[data-v-1e3620c9]{background:linear-gradient(45deg,#00dc82,#36e4da 50%,#0047e1);filter:blur(20vh)}.fixed[data-v-1e3620c9]{position:fixed}.-bottom-1\/2[data-v-1e3620c9]{bottom:-50%}.left-0[data-v-1e3620c9]{left:0}.right-0[data-v-1e3620c9]{right:0}.grid[data-v-1e3620c9]{display:grid}.mb-16[data-v-1e3620c9]{margin-bottom:4rem}.mb-8[data-v-1e3620c9]{margin-bottom:2rem}.h-1\/2[data-v-1e3620c9]{height:50%}.max-w-520px[data-v-1e3620c9]{max-width:520px}.min-h-screen[data-v-1e3620c9]{min-height:100vh}.place-content-center[data-v-1e3620c9]{place-content:center}.overflow-hidden[data-v-1e3620c9]{overflow:hidden}.bg-white[data-v-1e3620c9]{--un-bg-opacity:1;background-color:#fff;background-color:rgb(255 255 255/var(--un-bg-opacity))}.px-8[data-v-1e3620c9]{padding-left:2rem;padding-right:2rem}.text-center[data-v-1e3620c9]{text-align:center}.text-8xl[data-v-1e3620c9]{font-size:6rem;line-height:1}.text-xl[data-v-1e3620c9]{font-size:1.25rem;line-height:1.75rem}.text-black[data-v-1e3620c9]{--un-text-opacity:1;color:#000;color:rgb(0 0 0/var(--un-text-opacity))}.font-light[data-v-1e3620c9]{font-weight:300}.font-medium[data-v-1e3620c9]{font-weight:500}.leading-tight[data-v-1e3620c9]{line-height:1.25}.font-sans[data-v-1e3620c9]{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.antialiased[data-v-1e3620c9]{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}@media (prefers-color-scheme:dark){.dark\:bg-black[data-v-1e3620c9]{--un-bg-opacity:1;background-color:#000;background-color:rgb(0 0 0/var(--un-bg-opacity))}.dark\:text-white[data-v-1e3620c9]{--un-text-opacity:1;color:#fff;color:rgb(255 255 255/var(--un-text-opacity))}}@media (min-width:640px){.sm\:px-0[data-v-1e3620c9]{padding-left:0;padding-right:0}.sm\:text-4xl[data-v-1e3620c9]{font-size:2.25rem;line-height:2.5rem}} diff --git a/docs/_payload.json b/docs/_payload.json new file mode 100644 index 0000000000..351ad2ead5 --- /dev/null +++ b/docs/_payload.json @@ -0,0 +1 @@ +[{"data":1,"prerenderedAt":3},["ShallowReactive",2],{},1733914966097] \ No newline at end of file diff --git a/docs/favicon.ico b/docs/favicon.ico index 02fcae3958..62c71abf07 100644 Binary files a/docs/favicon.ico and b/docs/favicon.ico differ diff --git a/docs/github.svg b/docs/github.svg new file mode 100644 index 0000000000..37fa923df3 --- /dev/null +++ b/docs/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/index-old.html b/docs/index-old.html new file mode 100644 index 0000000000..f440fe5607 --- /dev/null +++ b/docs/index-old.html @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + SecHub | SecHub - one central and easy way to use different security tools with one API/Client + + + + + + + +
+
+ View on GitHub + +

SecHub

+

SecHub - one central and easy way to use different security tools with one API/Client +

+
+
+ +
+
+
+ + SecHub Logo + +

In a nutshell

+ +

SecHub represents a mechanism to integrate diverse security products like

+ + +

by just using one simple API/client.

+ +

You find the sources, issue tracker and more at GitHub. +

+
+ +
+

Documentation

+ + +
+ +
+

Downloads

+

Below, you find download links to the latest released versions of SecHub components:

+ +
+ +
+

IDE integration

+

These plugins for some major IDEs make it easy to navigate in the report tree to the code positions:

+ +
+ +
+

License

+ +

This project is licensed under the MIT + LICENSE.

+
+
+
+ +
+ +
+ + + \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index f440fe5607..78625e7e4e 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,119 +1,14 @@ - - - - - - - - - - - - - - - - - - SecHub | SecHub - one central and easy way to use different security tools with one API/Client - - - - - - - -
-
- View on GitHub - -

SecHub

-

SecHub - one central and easy way to use different security tools with one API/Client -

-
-
- -
-
-
- - SecHub Logo - -

In a nutshell

- -

SecHub represents a mechanism to integrate diverse security products like

- - -

by just using one simple API/client.

- -

You find the sources, issue tracker and more at GitHub. -

-
- -
-

Documentation

- - -
- -
-

Downloads

-

Below, you find download links to the latest released versions of SecHub components:

- -
- -
-

IDE integration

-

These plugins for some major IDEs make it easy to navigate in the report tree to the code positions:

- -
- -
-

License

- -

This project is licensed under the MIT - LICENSE.

-
-
-
- -
- -
- - - \ No newline at end of file + + +SecHub | Your security guardian. + + + + + + + + + +

SecHub - One API to secure them all

The free and open-source security platform SecHub, provides a central API to test software with different security tools.

SecHub supports many free and open-source as well as proprietary security tools covering SAST (Static Application Security Testing), DAST (Dynamic Application Security Testing), Secret scanners, Infrastructure scanners, License scanners and more…

SecHub Logo

Your Security Guardian.

SecHub orchestrates different security tools by one API layer.
Users interact with the SecHub Server, eliminating the need for projects to integrate vendor plugins for each security tool.

  • API Orchestration

    SecHub orchestrates tools via an unified API, simplifying integration for projects without the need for different vendor plugins.

  • Easy to use Client

    The SecHub client is written in Go and easy to use on your system of choice.

  • License Flexibility

    SecHub is MIT licensed. This ensures freedom in use, modification and distribution, fostering collaboration and adoption across all kind of projects.

  • Comprehensive Documentation

    Extensive documentation covers Getting Started, Architecture, Client, RestAPI, Operations and more. Available on our documentation.

  • Integration with Build Systems and IDEs

    Seamless integration with every build system and multiple developer IDEs adds security to your code without additional complexity.

  • Product Delegation Server (PDS)

    Explore SecHub's PDS, a vital component providing you the choice of many existing securtiy tools for use with SecHub. It also allows an easy integration of new securtiy tools.

Start Using Today!

Download SecHub for seamless security integration.
Orchestrates security tools through an unified API.
MIT License.

+ \ No newline at end of file diff --git a/docs/logo.svg b/docs/logo.svg new file mode 100644 index 0000000000..405711c6d4 --- /dev/null +++ b/docs/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/og.jpg b/docs/og.jpg new file mode 100644 index 0000000000..9038501d38 Binary files /dev/null and b/docs/og.jpg differ diff --git a/gradle/libraries.gradle b/gradle/libraries.gradle index 9094378c20..a83d0d988a 100644 --- a/gradle/libraries.gradle +++ b/gradle/libraries.gradle @@ -87,6 +87,9 @@ ext { /* Owasp Zap wrapper */ owaspzap_client_api: "1.14.0", jcommander: "1.82", + selenium_firefox_driver: "4.26.0", + selenium_support: "4.26.0", + groovy_jsr223: "4.0.24", thymeleaf_extras_springsecurity5: "3.1.2.RELEASE", @@ -97,16 +100,17 @@ ext { cycloneDX_core: "8.0.0", cyclonedx_gradle_plugin: "1.7.4", - /* Prepare wrapper */ - jgit_core: "6.9.0.202403050737-r", + /* Prepare wrapper */ + jgit_core: "6.9.0.202403050737-r", - /* ArchUnit */ - arch_unit: "1.3.0", + /* ArchUnit */ + arch_unit: "1.3.0", /* encryption */ // https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk18on - bouncy_castle_bcprov_jdk8: "1.78.1" + bouncy_castle_bcprov_jdk8: "1.78.1", + jakarta_servlet_api: "6.0.0" ] library = [ @@ -130,6 +134,8 @@ ext { springframework_restdocs: "org.springframework.restdocs:spring-restdocs-mockmvc", springframework_security_test: "org.springframework.security:spring-security-test", springframework_web: "org.springframework:spring-web", + springboot_test_autoconfigure: "org.springframework.boot:spring-boot-test-autoconfigure", + springframework_webmvc: "org.springframework:spring-webmvc", micrometer_prometheus: "io.micrometer:micrometer-registry-prometheus", @@ -195,6 +201,11 @@ ext { jcommander: "com.beust:jcommander:${libraryVersion.jcommander}", + selenium_firefox_driver: "org.seleniumhq.selenium:selenium-firefox-driver:${libraryVersion.selenium_firefox_driver}", + selenium_support: "org.seleniumhq.selenium:selenium-support:${libraryVersion.selenium_support}", + + groovy_jsr223: "org.apache.groovy:groovy-jsr223:${libraryVersion.groovy_jsr223}", + /* * Needed for Spring Boot WebFlux CSRF protection - see: https://stackoverflow.com/a/53305169 */ @@ -220,6 +231,8 @@ ext { javaxAnnotationApi: "javax.annotation:javax.annotation-api:${libraryVersion.javaxAnnotationApi}", findbugs: "com.google.code.findbugs:jsr305:${libraryVersion.findbugs}", httpmime: "org.apache.httpcomponents:httpmime:${libraryVersion.httpmime}", + + jakarta_servlet_api: "jakarta.servlet:jakarta.servlet-api:${libraryVersion.jakarta_servlet_api}", ] diff --git a/gradle/projects.gradle b/gradle/projects.gradle index c930a4f1d0..acadbd954c 100644 --- a/gradle/projects.gradle +++ b/gradle/projects.gradle @@ -41,6 +41,7 @@ projectType = [ project(':sechub-integrationtest'), project(':sechub-developertools'), project(':sechub-test'), + project(':sechub-commons-security-spring'), project(':sechub-testframework-spring'), project(':sechub-storage-sharedvolume-spring'), diff --git a/sechub-adapter-pds/src/main/java/com/mercedesbenz/sechub/adapter/pds/PDSAdapterV1.java b/sechub-adapter-pds/src/main/java/com/mercedesbenz/sechub/adapter/pds/PDSAdapterV1.java index 1661e211ac..97d51ba16b 100644 --- a/sechub-adapter-pds/src/main/java/com/mercedesbenz/sechub/adapter/pds/PDSAdapterV1.java +++ b/sechub-adapter-pds/src/main/java/com/mercedesbenz/sechub/adapter/pds/PDSAdapterV1.java @@ -565,23 +565,28 @@ private String createJobDataJSON(PDSContext context) throws AdapterException { } private PDSJobData createJobData(PDSContext context) { - PDSAdapterConfig config = context.getConfig(); - PDSAdapterConfigData data = config.getPDSAdapterConfigData(); - assertConfigDataNotNull(data); - Map parameters = data.getJobParameters(); - + PDSAdapterConfig adapterConfig = context.getConfig(); + PDSAdapterConfigData adapterConfigData = adapterConfig.getPDSAdapterConfigData(); + assertConfigDataNotNull(adapterConfigData); + Map adapterConfigDataJobParameters = adapterConfigData.getJobParameters(); + + /* + * convert adapter configuration to PDS job data that shall be sent to PDS as + * key value parameters: + */ PDSJobData jobData = new PDSJobData(); - for (String key : parameters.keySet()) { + + for (String key : adapterConfigDataJobParameters.keySet()) { PDSJobParameterEntry parameter = new PDSJobParameterEntry(); parameter.key = key; - parameter.value = parameters.get(key); + parameter.value = adapterConfigDataJobParameters.get(key); jobData.parameters.add(parameter); } - UUID secHubJobUUID = data.getSecHubJobUUID(); + UUID secHubJobUUID = adapterConfigData.getSecHubJobUUID(); jobData.sechubJobUUID = secHubJobUUID.toString(); - jobData.productId = data.getPdsProductIdentifier(); + jobData.productId = adapterConfigData.getPdsProductIdentifier(); return jobData; } diff --git a/sechub-adapter/src/main/java/com/mercedesbenz/sechub/adapter/AbstractAdapterConfigBuilder.java b/sechub-adapter/src/main/java/com/mercedesbenz/sechub/adapter/AbstractAdapterConfigBuilder.java index 392669ca59..6a66e98300 100644 --- a/sechub-adapter/src/main/java/com/mercedesbenz/sechub/adapter/AbstractAdapterConfigBuilder.java +++ b/sechub-adapter/src/main/java/com/mercedesbenz/sechub/adapter/AbstractAdapterConfigBuilder.java @@ -12,6 +12,7 @@ import org.slf4j.LoggerFactory; import com.mercedesbenz.sechub.adapter.support.URIShrinkSupport; +import com.mercedesbenz.sechub.commons.core.ConfigurationFailureException; import com.mercedesbenz.sechub.commons.core.security.CryptoAccess; /** @@ -69,10 +70,11 @@ protected URIShrinkSupport createURIShrinkSupport() { * * @param strategy * @return builder (configured by strategy) + * @throws ConfigurationFailureException */ @Override @SuppressWarnings("unchecked") - public final B configure(AdapterConfigurationStrategy strategy) { + public final B configure(AdapterConfigurationStrategy strategy) throws ConfigurationFailureException { strategy.configure((B) this); return (B) this; } diff --git a/sechub-adapter/src/main/java/com/mercedesbenz/sechub/adapter/AdapterConfigBuilder.java b/sechub-adapter/src/main/java/com/mercedesbenz/sechub/adapter/AdapterConfigBuilder.java index 1d4ea473f9..fc450ce2dd 100644 --- a/sechub-adapter/src/main/java/com/mercedesbenz/sechub/adapter/AdapterConfigBuilder.java +++ b/sechub-adapter/src/main/java/com/mercedesbenz/sechub/adapter/AdapterConfigBuilder.java @@ -3,6 +3,8 @@ import static com.mercedesbenz.sechub.adapter.TimeConstants.*; +import com.mercedesbenz.sechub.commons.core.ConfigurationFailureException; + public interface AdapterConfigBuilder { public static final int DEFAULT_SCAN_RESULT_CHECK_IN_MILLISECONDS = TIME_1_MINUTE_IN_MILLISECONDS; @@ -30,7 +32,7 @@ public interface AdapterConfigBuilder { * @param strategy * @return builder (configured by strategy) */ - AdapterConfigBuilder configure(AdapterConfigurationStrategy strategy); + AdapterConfigBuilder configure(AdapterConfigurationStrategy strategy) throws ConfigurationFailureException; /** * Set result check interval in minutes. diff --git a/sechub-adapter/src/main/java/com/mercedesbenz/sechub/adapter/AdapterConfigurationStrategy.java b/sechub-adapter/src/main/java/com/mercedesbenz/sechub/adapter/AdapterConfigurationStrategy.java index b7976bcfae..8f30c4c0a7 100644 --- a/sechub-adapter/src/main/java/com/mercedesbenz/sechub/adapter/AdapterConfigurationStrategy.java +++ b/sechub-adapter/src/main/java/com/mercedesbenz/sechub/adapter/AdapterConfigurationStrategy.java @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT package com.mercedesbenz.sechub.adapter; +import com.mercedesbenz.sechub.commons.core.ConfigurationFailureException; + /** * A configuration strategy is used to configure a given config adapter builder * @@ -16,6 +18,6 @@ public interface AdapterConfigurationStrategy { * * @param configBuilder */ - void configure(B configBuilder); + void configure(B configBuilder) throws ConfigurationFailureException; } diff --git a/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/AdministrationAPIConstants.java b/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/AdministrationAPIConstants.java index 86b2f04f01..aa31b4545c 100644 --- a/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/AdministrationAPIConstants.java +++ b/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/AdministrationAPIConstants.java @@ -108,6 +108,12 @@ private AdministrationAPIConstants() { public static final String API_CHANGE_PROJECT_ACCESSLEVEL = API_ADMINISTRATION + "project/{projectId}/accesslevel/{projectAccessLevel}"; + /* +-----------------------------------------------------------------------+ */ + /* +............................ Templates.................................+ */ + /* +-----------------------------------------------------------------------+ */ + public static final String API_ASSIGN_TEMPLATE_TO_PROJECT = API_ADMINISTRATION + "project/{projectId}/template/{templateId}"; + public static final String API_UNASSIGN_TEMPLATE_FROM_PROJECT = API_ADMINISTRATION + "project/{projectId}/template/{templateId}"; + /* +-----------------------------------------------------------------------+ */ /* +............................ Encryption................................+ */ /* +-----------------------------------------------------------------------+ */ diff --git a/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/project/Project.java b/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/project/Project.java index 890a2c01d3..0faa9b53d5 100644 --- a/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/project/Project.java +++ b/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/project/Project.java @@ -35,6 +35,7 @@ public class Project { public static final String TABLE_NAME_PROJECT_TO_METADATA = "ADM_PROJECT_TO_METADATA"; public static final String TABLE_NAME_PROJECT_WHITELIST_URI = "ADM_PROJECT_WHITELIST_URI"; public static final String TABLE_NAME_PROJECT_METADATA = "ADM_PROJECT_METADATA"; + public static final String TABLE_NAME_PROJECT_TEMPLATES = "ADM_PROJECT_TEMPLATES"; public static final String COLUMN_PROJECT_ID = "PROJECT_ID"; public static final String COLUMN_PROJECT_OWNER = "PROJECT_OWNER"; @@ -43,10 +44,12 @@ public class Project { public static final String COLUMN_METADATA = "METADATA_KEY"; public static final String COLUMN_PROJECT_ACCESS_LEVEL = "PROJECT_ACCESS_LEVEL"; + public static final String COLUMN_TEMPLATE_ID = "PROJECT_TEMPLATE_ID"; public static final String ASSOCIATE_PROJECT_TO_USER_COLUMN_PROJECT_ID = "PROJECTS_PROJECT_ID"; public static final String ASSOCIATE_PROJECT_TO_URI_COLUMN_PROJECT_ID = "PROJECT_PROJECT_ID"; public static final String ASSOCIATE_PROJECT_TO_METADATA_COLUMN_PROJECT_ID = "PROJECT_ID"; + public static final String ASSOCIATE_PROJECT_TO_TEMPLATE_COLUMN_PROJECT_ID = "PROJECT_PROJECT_ID"; /* +-----------------------------------------------------------------------+ */ /* +............................ JPQL .....................................+ */ @@ -88,6 +91,11 @@ public class Project { @OneToMany(cascade = { CascadeType.REFRESH }, fetch = FetchType.EAGER, mappedBy = ProjectMetaDataEntity.PROPERTY_PROJECT_ID) Set metaData = new HashSet<>(); + @Column(name = COLUMN_TEMPLATE_ID, nullable = false) + @ElementCollection(targetClass = String.class, fetch = FetchType.EAGER) + @CollectionTable(name = TABLE_NAME_PROJECT_TEMPLATES) + Set templateIds = new HashSet<>(); + @Version @Column(name = "VERSION") Integer version; @@ -127,6 +135,10 @@ public ProjectAccessLevel getAccessLevel() { return accessLevel; } + public Set getTemplateIds() { + return templateIds; + } + @Override public int hashCode() { final int prime = 31; diff --git a/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/project/ProjectAdministrationRestController.java b/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/project/ProjectAdministrationRestController.java index 9b4cd6561b..ad377d6d3c 100644 --- a/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/project/ProjectAdministrationRestController.java +++ b/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/project/ProjectAdministrationRestController.java @@ -28,6 +28,8 @@ import com.mercedesbenz.sechub.sharedkernel.Step; import com.mercedesbenz.sechub.sharedkernel.project.ProjectAccessLevel; import com.mercedesbenz.sechub.sharedkernel.security.RoleConstants; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminAssignsTemplateToProject; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminUnassignsTemplateFromProject; import com.mercedesbenz.sechub.sharedkernel.usecases.admin.project.UseCaseAdminChangesProjectAccessLevel; import com.mercedesbenz.sechub.sharedkernel.usecases.admin.project.UseCaseAdminChangesProjectDescription; import com.mercedesbenz.sechub.sharedkernel.usecases.admin.project.UseCaseAdminCreatesProject; @@ -86,6 +88,9 @@ public class ProjectAdministrationRestController { @Autowired ListProjectsService listProjectsService; + @Autowired + ProjectTemplateService projectTemplateService; + /* @formatter:off */ @UseCaseAdminCreatesProject( @Step( @@ -180,6 +185,22 @@ public void changeProjectAccessLevel(@PathVariable(name = "projectId") String pr projectAccessLevelChangeService.changeProjectAccessLevel(projectId, projectAccessLevel); } + /* @formatter:off */ + @UseCaseAdminAssignsTemplateToProject(@Step(number = 1, name = "Rest call", description = "Admin does call REST API to assign a template to project", needsRestDoc = true)) + @RequestMapping(path = AdministrationAPIConstants.API_ASSIGN_TEMPLATE_TO_PROJECT, method = RequestMethod.PUT, produces = {MediaType.APPLICATION_JSON_VALUE}) + public void assignTemplateToProject(@PathVariable(name = "projectId") String projectId, @PathVariable(name = "templateId") String templateId) { + /* @formatter:on */ + projectTemplateService.assignTemplateToProject(templateId, projectId); + } + + /* @formatter:off */ + @UseCaseAdminUnassignsTemplateFromProject(@Step(number = 1, name = "Rest call", description = "Admin does call REST API to unassign a template from project", needsRestDoc = true)) + @RequestMapping(path = AdministrationAPIConstants.API_UNASSIGN_TEMPLATE_FROM_PROJECT, method = RequestMethod.DELETE, produces = {MediaType.APPLICATION_JSON_VALUE}) + public void unassignTemplateFromProject(@PathVariable(name = "projectId") String projectId, @PathVariable(name = "templateId") String templateId) { + /* @formatter:on */ + projectTemplateService.unassignTemplateFromProject(templateId, projectId); + } + @InitBinder protected void initBinder(WebDataBinder binder) { binder.setValidator(validator); diff --git a/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/project/ProjectDetailInformation.java b/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/project/ProjectDetailInformation.java index f33b0c50d1..bdccd79606 100644 --- a/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/project/ProjectDetailInformation.java +++ b/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/project/ProjectDetailInformation.java @@ -17,16 +17,22 @@ public class ProjectDetailInformation { public static final String PROPERTY_OWNER = "owner"; public static final String PROPERTY_ACCESSLEVEL = "accessLevel"; public static final String PROPERTY_DESCRIPTION = "description"; + public static final String PROPERTY_TEMPLATE_IDS = "templateIds"; private String projectId; private List users = new ArrayList<>(); private List whitelist = new ArrayList<>(); + private List templateIds = new ArrayList<>(); private Map metaData = new HashMap<>(); private String owner; private String description; private String accessLevel; + ProjectDetailInformation() { + /* for JSON */ + } + public ProjectDetailInformation(Project project) { this.projectId = project.getId(); @@ -38,6 +44,8 @@ public ProjectDetailInformation(Project project) { project.getMetaData().forEach(entry -> this.metaData.put(entry.key, entry.value)); + project.getTemplateIds().forEach(templateid -> this.templateIds.add(templateid)); + this.owner = project.getOwner().getName(); this.description = project.getDescription(); @@ -72,4 +80,8 @@ public String getDescription() { public String getAccessLevel() { return accessLevel; } + + public List getTemplateIds() { + return templateIds; + } } diff --git a/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/project/ProjectRepositoryImpl.java b/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/project/ProjectRepositoryImpl.java index 152164be2b..8b7b7fffe2 100644 --- a/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/project/ProjectRepositoryImpl.java +++ b/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/project/ProjectRepositoryImpl.java @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT package com.mercedesbenz.sechub.domain.administration.project; +import static com.mercedesbenz.sechub.domain.administration.project.Project.*; + import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import jakarta.persistence.Query; @@ -9,14 +11,17 @@ public class ProjectRepositoryImpl implements ProjectRepositoryCustom { @PersistenceContext private EntityManager em; +/* @formatter:off */ + private static final String QUERY_DELETE_PROJECT_TO_USER = "delete from " + TABLE_NAME_PROJECT_TO_USER + " p2u where p2u." + ASSOCIATE_PROJECT_TO_USER_COLUMN_PROJECT_ID + " = ?1"; + + private static final String QUERY_DELETE_PROJECT_TO_URI = "delete from " + TABLE_NAME_PROJECT_WHITELIST_URI + " p2w where p2w." + Project.ASSOCIATE_PROJECT_TO_URI_COLUMN_PROJECT_ID + " = ?1"; + + private static final String QUERY_DELETE_PROJECT_TO_METADATA = "delete from " + TABLE_NAME_PROJECT_METADATA + " p2w where p2w." + Project.ASSOCIATE_PROJECT_TO_METADATA_COLUMN_PROJECT_ID + " = ?1"; - private static final String QUERY_DELETE_PROJECT_TO_USER = "delete from " + Project.TABLE_NAME_PROJECT_TO_USER + " p2u where p2u." - + Project.ASSOCIATE_PROJECT_TO_USER_COLUMN_PROJECT_ID + " = ?1"; - private static final String QUERY_DELETE_PROJECT_TO_URI = "delete from " + Project.TABLE_NAME_PROJECT_WHITELIST_URI + " p2w where p2w." - + Project.ASSOCIATE_PROJECT_TO_URI_COLUMN_PROJECT_ID + " = ?1"; - private static final String QUERY_DELETE_PROJECT_TO_METADATA = "delete from " + Project.TABLE_NAME_PROJECT_METADATA + " p2w where p2w." - + Project.ASSOCIATE_PROJECT_TO_METADATA_COLUMN_PROJECT_ID + " = ?1"; - private static final String QUERY_DELETE_PROJECT = "delete from " + Project.TABLE_NAME + " p where p." + Project.COLUMN_PROJECT_ID + " = ?1"; + private static final String QUERY_DELETE_PROJECT_TO_TEMPLATE = "delete from " + TABLE_NAME_PROJECT_TEMPLATES + " p2w where p2w." + Project.ASSOCIATE_PROJECT_TO_TEMPLATE_COLUMN_PROJECT_ID + " = ?1"; + + private static final String QUERY_DELETE_PROJECT = "delete from " + TABLE_NAME + " p where p." + Project.COLUMN_PROJECT_ID + " = ?1"; + /* @formatter:on */ @Override public void deleteProjectWithAssociations(String projectId) { @@ -32,6 +37,10 @@ public void deleteProjectWithAssociations(String projectId) { deleteProjectToMetaData.setParameter(1, projectId); deleteProjectToMetaData.executeUpdate(); + Query deleteProjectToTemplate = em.createNativeQuery(QUERY_DELETE_PROJECT_TO_TEMPLATE); + deleteProjectToTemplate.setParameter(1, projectId); + deleteProjectToTemplate.executeUpdate(); + Query deleteProject = em.createNativeQuery(QUERY_DELETE_PROJECT); deleteProject.setParameter(1, projectId); deleteProject.executeUpdate(); diff --git a/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/project/ProjectTemplateService.java b/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/project/ProjectTemplateService.java new file mode 100644 index 0000000000..d930cdf142 --- /dev/null +++ b/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/project/ProjectTemplateService.java @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.administration.project; + +import java.util.List; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.mercedesbenz.sechub.sharedkernel.Step; +import com.mercedesbenz.sechub.sharedkernel.error.NotAcceptableException; +import com.mercedesbenz.sechub.sharedkernel.messaging.DomainMessage; +import com.mercedesbenz.sechub.sharedkernel.messaging.DomainMessageService; +import com.mercedesbenz.sechub.sharedkernel.messaging.DomainMessageSynchronousResult; +import com.mercedesbenz.sechub.sharedkernel.messaging.IsSendingSyncMessage; +import com.mercedesbenz.sechub.sharedkernel.messaging.MessageDataKeys; +import com.mercedesbenz.sechub.sharedkernel.messaging.MessageID; +import com.mercedesbenz.sechub.sharedkernel.security.RoleConstants; +import com.mercedesbenz.sechub.sharedkernel.template.SecHubProjectTemplateData; +import com.mercedesbenz.sechub.sharedkernel.template.SecHubProjectToTemplate; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminAssignsTemplateToProject; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminUnassignsTemplateFromProject; +import com.mercedesbenz.sechub.sharedkernel.validation.UserInputAssertion; + +import jakarta.annotation.security.RolesAllowed; + +@Service +@RolesAllowed(RoleConstants.ROLE_SUPERADMIN) +public class ProjectTemplateService { + + private static final Logger LOG = LoggerFactory.getLogger(ProjectTemplateService.class); + + @Autowired + DomainMessageService eventBus; + + @Autowired + ProjectRepository projectRepository; + + @Autowired + ProjectTransactionService projectTansactionService; + + @Autowired + UserInputAssertion assertion; + + /* @formatter:off */ + @UseCaseAdminAssignsTemplateToProject( + @Step( + number = 2, + name = "service assigns template to project", + description = "The service will request the template assignment in domain 'scan' via synchronous event and updates mapping in domain 'administration' afterwards")) + /* @formatter:on */ + public void assignTemplateToProject(String templateId, String projectId) { + changeTemplateAssignment(templateId, projectId, (t, p) -> fetchAssignRequestResult(t, p), "assigned to"); + } + + /* @formatter:off */ + @UseCaseAdminUnassignsTemplateFromProject( + @Step( + number = 2, + name = "service unassigns template from project", + description = "The service will request the template unassignment in domain 'scan' via synchronous event and updates mapping in domain 'administration' afterwards")) + /* @formatter:on */ + public void unassignTemplateFromProject(String templateId, String projectId) { + changeTemplateAssignment(templateId, projectId, (t, p) -> fetchUnassignmentRequestResult(t, p), "unassigned from"); + } + + private void changeTemplateAssignment(String templateId, String projectId, TemplateChangeResultFetcher fetcher, String assignOrUnassignInfo) { + assertion.assertIsValidTemplateId(templateId); + assertion.assertIsValidProjectId(projectId); + + Project project = projectRepository.findOrFailProject(projectId); + Set templateIds = project.getTemplateIds(); + LOG.debug("Project '{}' has following template ids: {}", projectId, templateIds); + + SecHubProjectTemplateData result = fetcher.fetchTemplateAssignmentChangeResult(templateId, projectId); + List newTemplateIds = result.getTemplateIds(); + templateIds.clear(); + templateIds.addAll(newTemplateIds); + + projectTansactionService.saveInOwnTransaction(project); + LOG.info("Template '{}' has been {} project '{}'", templateId, assignOrUnassignInfo, projectId); + + LOG.debug("Project '{}' has following template ids: {}", projectId, templateIds); + } + + @IsSendingSyncMessage(MessageID.REQUEST_ASSIGN_TEMPLATE_TO_PROJECT) + private SecHubProjectTemplateData fetchAssignRequestResult(String templateId, String projectId) { + return sendSynchronousProjectTemplateChangeEvent(templateId, projectId, MessageID.REQUEST_ASSIGN_TEMPLATE_TO_PROJECT, + MessageID.RESULT_ASSIGN_TEMPLATE_TO_PROJECT); + + } + + @IsSendingSyncMessage(MessageID.REQUEST_UNASSIGN_TEMPLATE_FROM_PROJECT) + private SecHubProjectTemplateData fetchUnassignmentRequestResult(String templateId, String projectId) { + return sendSynchronousProjectTemplateChangeEvent(templateId, projectId, MessageID.REQUEST_UNASSIGN_TEMPLATE_FROM_PROJECT, + MessageID.RESULT_UNASSIGN_TEMPLATE_FROM_PROJECT); + } + + /* + * This method sends a synchronous event to event bus and waits that the + * assignment is done inside other domain (in this case we know it is inside the + * scan domain). When this is done, the change event was successful and the + * event result contains SecHubProjectTemplateData which can be used inside + * administration domain further. + */ + private SecHubProjectTemplateData sendSynchronousProjectTemplateChangeEvent(String templateId, String projectId, MessageID requestMessageId, + MessageID acceptedResultMessageId) { + + DomainMessage message = new DomainMessage(requestMessageId); + + SecHubProjectToTemplate mapping = new SecHubProjectToTemplate(); + mapping.setProjectId(projectId); + mapping.setTemplateId(templateId); + message.set(MessageDataKeys.PROJECT_TO_TEMPLATE, mapping); + + DomainMessageSynchronousResult result = eventBus.sendSynchron(message); + + if (result.hasFailed()) { + throw new NotAcceptableException("Was not able to change template to project assignment.\nReason: " + result.getErrorMessage()); + } + MessageID messageID = result.getMessageId(); + if (!(acceptedResultMessageId.equals(messageID))) { + throw new IllegalStateException("Result message id not supported: " + messageID); + } + return result.get(MessageDataKeys.PROJECT_TEMPLATES); + } + + private interface TemplateChangeResultFetcher { + public SecHubProjectTemplateData fetchTemplateAssignmentChangeResult(String templateId, String projectId); + } +} diff --git a/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/TestAdministrationSecurityConfiguration.java b/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/TestAdministrationSecurityConfiguration.java new file mode 100644 index 0000000000..87c7ff38ba --- /dev/null +++ b/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/TestAdministrationSecurityConfiguration.java @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.administration; + +import static org.mockito.Mockito.mock; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.web.client.RestTemplate; + +import com.mercedesbenz.sechub.sharedkernel.security.SecHubSecurityConfiguration; + +@Configuration +@Import(SecHubSecurityConfiguration.class) +public class TestAdministrationSecurityConfiguration { + + @Bean + RestTemplate restTemplate() { + return mock(); + } +} diff --git a/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/project/ProjectAdministrationRestControllerMockTest.java b/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/project/ProjectAdministrationRestControllerMockTest.java index a48b6b5f14..1a117259a6 100644 --- a/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/project/ProjectAdministrationRestControllerMockTest.java +++ b/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/project/ProjectAdministrationRestControllerMockTest.java @@ -29,11 +29,9 @@ import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.ActiveProfiles; @@ -42,18 +40,19 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.validation.Errors; +import com.mercedesbenz.sechub.domain.administration.TestAdministrationSecurityConfiguration; import com.mercedesbenz.sechub.domain.administration.project.ProjectJsonInput.ProjectMetaData; import com.mercedesbenz.sechub.domain.administration.user.User; import com.mercedesbenz.sechub.sharedkernel.Profiles; -import com.mercedesbenz.sechub.sharedkernel.security.AbstractSecHubAPISecurityConfiguration; import com.mercedesbenz.sechub.sharedkernel.security.RoleConstants; import com.mercedesbenz.sechub.test.TestPortProvider; @RunWith(SpringRunner.class) -@WebMvcTest(ProjectAdministrationRestController.class) -@ContextConfiguration(classes = { ProjectAdministrationRestController.class, ProjectAdministrationRestControllerMockTest.SimpleTestConfiguration.class }) +@WebMvcTest +@ContextConfiguration(classes = { ProjectAdministrationRestController.class }) @WithMockUser(roles = RoleConstants.ROLE_SUPERADMIN) @ActiveProfiles({ Profiles.TEST, Profiles.ADMIN_ACCESS }) +@Import(TestAdministrationSecurityConfiguration.class) public class ProjectAdministrationRestControllerMockTest { private static final int PORT_USED = TestPortProvider.DEFAULT_INSTANCE.getWebMVCTestHTTPSPort(); @@ -97,6 +96,9 @@ public class ProjectAdministrationRestControllerMockTest { @MockBean ProjectChangeAccessLevelService projectChangeAccessLevelService; + @MockBean + ProjectTemplateService projectTemplateService; + @Before public void before() { when(createProjectInputvalidator.supports(ProjectJsonInput.class)).thenReturn(true); @@ -240,12 +242,4 @@ public void when_admin_tries_to_change_project_description_but_request_body_is_m /* @formatter:on */ } - - @TestConfiguration - @Profile(Profiles.TEST) - @EnableAutoConfiguration - public static class SimpleTestConfiguration extends AbstractSecHubAPISecurityConfiguration { - - } - } diff --git a/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/project/ProjectDetailInformationTest.java b/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/project/ProjectDetailInformationTest.java new file mode 100644 index 0000000000..62fc101608 --- /dev/null +++ b/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/project/ProjectDetailInformationTest.java @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.administration.project; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.net.URI; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import com.mercedesbenz.sechub.domain.administration.user.User; +import com.mercedesbenz.sechub.sharedkernel.project.ProjectAccessLevel; + +class ProjectDetailInformationTest { + + @Test + void constructor_stores_data_from_project_into_fields() { + /* prepare */ + Project project = mock(Project.class); + + User owner = mock(User.class); + when(owner.getName()).thenReturn("owner1"); + + User user1 = mock(User.class); + when(user1.getName()).thenReturn("user1"); + + User user2 = mock(User.class); + when(user2.getName()).thenReturn("user2"); + + String projectId = "id1"; + ProjectMetaDataEntity metaDataEntity = new ProjectMetaDataEntity(projectId, "key1", "value1"); + ProjectAccessLevel accessLevel = ProjectAccessLevel.FULL; + + when(project.getWhiteList()).thenReturn(Set.of(URI.create("https://example.com"))); + when(project.getAccessLevel()).thenReturn(accessLevel); + when(project.getId()).thenReturn(projectId); + when(project.getOwner()).thenReturn(owner); + when(project.getMetaData()).thenReturn(Set.of(metaDataEntity)); + when(project.getUsers()).thenReturn(Set.of(user1, user2)); + when(project.getTemplateIds()).thenReturn(Set.of("template1", "template2")); + + /* execute */ + ProjectDetailInformation toTest = new ProjectDetailInformation(project); + + /* test */ + assertThat(toTest.getProjectId()).isEqualTo(projectId); + assertThat(toTest.getAccessLevel()).isEqualTo(accessLevel.getId()); + assertThat(toTest.getOwner()).isEqualTo("owner1"); + assertThat(toTest.getUsers()).contains("user1", "user2"); + assertThat(toTest.getWhiteList()).contains("https://example.com"); + assertThat(toTest.getTemplateIds()).contains("template1", "template2"); + assertThat(toTest.getMetaData()).containsEntry("key1", "value1"); + + } + +} diff --git a/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/project/ProjectTemplateServiceTest.java b/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/project/ProjectTemplateServiceTest.java new file mode 100644 index 0000000000..dd9ea02985 --- /dev/null +++ b/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/project/ProjectTemplateServiceTest.java @@ -0,0 +1,202 @@ +package com.mercedesbenz.sechub.domain.administration.project; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.EnumSource.Mode; +import org.mockito.ArgumentCaptor; + +import com.mercedesbenz.sechub.sharedkernel.error.NotAcceptableException; +import com.mercedesbenz.sechub.sharedkernel.messaging.DomainMessage; +import com.mercedesbenz.sechub.sharedkernel.messaging.DomainMessageService; +import com.mercedesbenz.sechub.sharedkernel.messaging.DomainMessageSynchronousResult; +import com.mercedesbenz.sechub.sharedkernel.messaging.MessageDataKeys; +import com.mercedesbenz.sechub.sharedkernel.messaging.MessageID; +import com.mercedesbenz.sechub.sharedkernel.template.SecHubProjectTemplateData; +import com.mercedesbenz.sechub.sharedkernel.template.SecHubProjectToTemplate; + +class ProjectTemplateServiceTest { + + private static final String CORRECT_ASSIGN_TEMPLATE_RESULT_MESSAGE_ID = "RESULT_ASSIGN_TEMPLATE_TO_PROJECT"; + private static final String CORRECT_UNASSIGN_TEMPLATE_RESULT_MESSAGE_ID = "RESULT_UNASSIGN_TEMPLATE_FROM_PROJECT"; + private static final String ASSIGNED_TEMPLATE_ID_A = "template-a"; + private static final String ASSIGNED_TEMPLATE_ID_B = "template-b"; + + private static final String ASSIGNED_TEMPLATE_ID_AFTER_CHANGE_1 = "template-after_change-1"; + private static final String ASSIGNED_TEMPLATE_ID_AFTER_CHANGE_2 = "template-after-change-2"; + + private static final String PROJECT_ID1 = "project1"; + private static final String TEMPLATE_ID1 = "templateId1"; + private ProjectTemplateService serviceToTest; + private DomainMessageService eventBus; + private ProjectRepository projectRepository; + private ProjectTransactionService projectTansactionService; + + @BeforeEach + void beforeEach() { + + eventBus = mock(); + projectRepository = mock(); + projectTansactionService = mock(); + + serviceToTest = new ProjectTemplateService(); + serviceToTest.assertion = mock(); + serviceToTest.eventBus = eventBus; + + serviceToTest.projectRepository = projectRepository; + serviceToTest.projectTansactionService = projectTansactionService; + } + + @Test + void assignTemplateToProject_sends_assign_request_synchronous_with_expected_data() { + + /* prepare */ + mockEventBusSynchronResultWithMessageId(MessageID.RESULT_ASSIGN_TEMPLATE_TO_PROJECT); + + /* execute */ + serviceToTest.assignTemplateToProject(TEMPLATE_ID1, PROJECT_ID1); + + /* test */ + ArgumentCaptor messageCaptor = ArgumentCaptor.captor(); + verify(eventBus).sendSynchron(messageCaptor.capture()); + + DomainMessage sentMessage = messageCaptor.getValue(); + assertThat(sentMessage).isNotNull(); + assertThat(sentMessage.getMessageId()).isEqualTo(MessageID.REQUEST_ASSIGN_TEMPLATE_TO_PROJECT); + SecHubProjectToTemplate sentMessageData = sentMessage.get(MessageDataKeys.PROJECT_TO_TEMPLATE); + assertThat(sentMessageData).isNotNull(); + assertThat(sentMessageData.getProjectId()).isEqualTo(PROJECT_ID1); + assertThat(sentMessageData.getTemplateId()).isEqualTo(TEMPLATE_ID1); + + } + + @Test + void assignTemplateToProject_updates_template_by_synchronous_event_result() { + + /* prepare */ + mockEventBusSynchronResultWithMessageId(MessageID.RESULT_ASSIGN_TEMPLATE_TO_PROJECT); + + /* execute */ + serviceToTest.assignTemplateToProject(TEMPLATE_ID1, PROJECT_ID1); + + /* test */ + ArgumentCaptor projectCaptor = ArgumentCaptor.forClass(Project.class); + verify(projectTansactionService).saveInOwnTransaction(projectCaptor.capture()); + Project projectSaved = projectCaptor.getValue(); + assertThat(projectSaved.getTemplateIds()).hasSize(2).describedAs("project templates must be changed by result data") + .contains(ASSIGNED_TEMPLATE_ID_AFTER_CHANGE_1).contains(ASSIGNED_TEMPLATE_ID_AFTER_CHANGE_2); + + } + + @Test + void unassignTemplateFromProject_sends_assign_request_synchronous_with_expected_data() { + + /* prepare */ + mockEventBusSynchronResultWithMessageId(MessageID.RESULT_UNASSIGN_TEMPLATE_FROM_PROJECT); + + /* execute */ + serviceToTest.unassignTemplateFromProject(TEMPLATE_ID1, PROJECT_ID1); + + /* test */ + ArgumentCaptor messageCaptor = ArgumentCaptor.captor(); + verify(eventBus).sendSynchron(messageCaptor.capture()); + + DomainMessage sentMessage = messageCaptor.getValue(); + assertThat(sentMessage).isNotNull(); + assertThat(sentMessage.getMessageId()).isEqualTo(MessageID.REQUEST_UNASSIGN_TEMPLATE_FROM_PROJECT); + SecHubProjectToTemplate sentMessageData = sentMessage.get(MessageDataKeys.PROJECT_TO_TEMPLATE); + assertThat(sentMessageData).isNotNull(); + assertThat(sentMessageData.getProjectId()).isEqualTo(PROJECT_ID1); + assertThat(sentMessageData.getTemplateId()).isEqualTo(TEMPLATE_ID1); + } + + @Test + void unassignTemplateFromProject_updates_template_by_synchronous_event_result() { + + /* prepare */ + mockEventBusSynchronResultWithMessageId(MessageID.RESULT_UNASSIGN_TEMPLATE_FROM_PROJECT); + + /* execute */ + serviceToTest.unassignTemplateFromProject(TEMPLATE_ID1, PROJECT_ID1); + + /* test */ + ArgumentCaptor projectCaptor = ArgumentCaptor.forClass(Project.class); + verify(projectTansactionService).saveInOwnTransaction(projectCaptor.capture()); + Project projectSaved = projectCaptor.getValue(); + assertThat(projectSaved.getTemplateIds()).hasSize(2).describedAs("project templates must be changed by result data") + .contains(ASSIGNED_TEMPLATE_ID_AFTER_CHANGE_1).contains(ASSIGNED_TEMPLATE_ID_AFTER_CHANGE_2); + + } + + @ParameterizedTest + @EnumSource(value = MessageID.class, mode = Mode.EXCLUDE, names = CORRECT_UNASSIGN_TEMPLATE_RESULT_MESSAGE_ID) + void unassignTemplateFromProject_when_synchronous_event_result_has_unsupported_message_throws_invalid_exception(MessageID wrongMessageId) { + /* prepare */ + mockEventBusSynchronResultWithMessageId(wrongMessageId); + + /* execute + test */ + assertThatThrownBy(() -> serviceToTest.unassignTemplateFromProject(TEMPLATE_ID1, PROJECT_ID1)).isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Result message id not supported"); + + } + + @ParameterizedTest + @EnumSource(value = MessageID.class, mode = Mode.EXCLUDE, names = CORRECT_ASSIGN_TEMPLATE_RESULT_MESSAGE_ID) + void assignTemplateToProject_when_synchronous_event_result_has_unsupported_message_throws_invalid_exception(MessageID wrongMessageId) { + mockEventBusSynchronResultWithMessageId(wrongMessageId); + + /* execute + test */ + assertThatThrownBy(() -> serviceToTest.assignTemplateToProject(TEMPLATE_ID1, PROJECT_ID1)).isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Result message id not supported"); + + } + + @Test + void assignTemplateToProject_when_event_result_failed_exception_is_thrown() { + /* prepare */ + DomainMessageSynchronousResult mockedResultMessage = mockEventBusSynchronResultWithMessageId( + MessageID.valueOf(CORRECT_UNASSIGN_TEMPLATE_RESULT_MESSAGE_ID)); + when(mockedResultMessage.hasFailed()).thenReturn(true); + + assertThatThrownBy(() -> serviceToTest.assignTemplateToProject(TEMPLATE_ID1, PROJECT_ID1)).isInstanceOf(NotAcceptableException.class) + .hasMessageContaining("Was not able to change template to project assignment"); + } + + @Test + void unassignTemplateFromProject_when_event_result_failed_exception_is_thrown() { + /* prepare */ + DomainMessageSynchronousResult mockedResultMessage = mockEventBusSynchronResultWithMessageId( + MessageID.valueOf(CORRECT_UNASSIGN_TEMPLATE_RESULT_MESSAGE_ID)); + when(mockedResultMessage.hasFailed()).thenReturn(true); + + assertThatThrownBy(() -> serviceToTest.unassignTemplateFromProject(TEMPLATE_ID1, PROJECT_ID1)).isInstanceOf(NotAcceptableException.class) + .hasMessageContaining("Was not able to change template to project assignment"); + } + + private DomainMessageSynchronousResult mockEventBusSynchronResultWithMessageId(MessageID resultMessageId) { + Project project1 = new Project(); + project1.getTemplateIds().addAll(Set.of(ASSIGNED_TEMPLATE_ID_A, ASSIGNED_TEMPLATE_ID_B)); + + when(projectRepository.findOrFailProject(PROJECT_ID1)).thenReturn(project1); + + SecHubProjectTemplateData mockedResultData = mock(); + when(mockedResultData.getProjectId()).thenReturn("result-project"); + when(mockedResultData.getTemplateIds()).thenReturn(List.of(ASSIGNED_TEMPLATE_ID_AFTER_CHANGE_1, ASSIGNED_TEMPLATE_ID_AFTER_CHANGE_2)); + DomainMessageSynchronousResult mockedResultMessage = mock(); + when(mockedResultMessage.getMessageId()).thenReturn(resultMessageId); + when(mockedResultMessage.get(MessageDataKeys.PROJECT_TEMPLATES)).thenReturn(mockedResultData); + + when(eventBus.sendSynchron(any(DomainMessage.class))).thenReturn(mockedResultMessage); + + return mockedResultMessage; + } + +} diff --git a/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/project/ProjectUpdateAdministrationRestControllerMockTest.java b/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/project/ProjectUpdateAdministrationRestControllerMockTest.java index a6bba1c49f..136b987a3b 100644 --- a/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/project/ProjectUpdateAdministrationRestControllerMockTest.java +++ b/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/project/ProjectUpdateAdministrationRestControllerMockTest.java @@ -19,11 +19,9 @@ import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.ActiveProfiles; @@ -32,16 +30,16 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.validation.Errors; +import com.mercedesbenz.sechub.domain.administration.TestAdministrationSecurityConfiguration; import com.mercedesbenz.sechub.domain.administration.project.ProjectJsonInput.ProjectMetaData; import com.mercedesbenz.sechub.sharedkernel.Profiles; -import com.mercedesbenz.sechub.sharedkernel.security.AbstractSecHubAPISecurityConfiguration; import com.mercedesbenz.sechub.sharedkernel.security.RoleConstants; import com.mercedesbenz.sechub.test.TestPortProvider; @RunWith(SpringRunner.class) -@WebMvcTest(ProjectUpdateAdministrationRestController.class) -@ContextConfiguration(classes = { ProjectUpdateAdministrationRestController.class, - ProjectUpdateAdministrationRestControllerMockTest.SimpleTestConfiguration.class }) +@WebMvcTest +@ContextConfiguration(classes = { ProjectUpdateAdministrationRestController.class }) +@Import(TestAdministrationSecurityConfiguration.class) @WithMockUser(roles = RoleConstants.ROLE_SUPERADMIN) @ActiveProfiles({ Profiles.TEST, Profiles.ADMIN_ACCESS }) public class ProjectUpdateAdministrationRestControllerMockTest { @@ -157,11 +155,4 @@ public Void answer(InvocationOnMock invocation) { verifyNoInteractions(mockedProjectUpdateMetaDataService); /* @formatter:on */ } - - @TestConfiguration - @Profile(Profiles.TEST) - @EnableAutoConfiguration - public static class SimpleTestConfiguration extends AbstractSecHubAPISecurityConfiguration { - - } } diff --git a/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/signup/AnonymousSignupRestControllerMockTest.java b/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/signup/AnonymousSignupRestControllerMockTest.java index e721db536d..52627b73ee 100644 --- a/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/signup/AnonymousSignupRestControllerMockTest.java +++ b/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/signup/AnonymousSignupRestControllerMockTest.java @@ -10,11 +10,9 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.ActiveProfiles; @@ -22,8 +20,8 @@ import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; +import com.mercedesbenz.sechub.domain.administration.TestAdministrationSecurityConfiguration; import com.mercedesbenz.sechub.sharedkernel.Profiles; -import com.mercedesbenz.sechub.sharedkernel.security.AbstractSecHubAPISecurityConfiguration; import com.mercedesbenz.sechub.sharedkernel.validation.ApiVersionValidationFactory; import com.mercedesbenz.sechub.sharedkernel.validation.EmailValidationImpl; import com.mercedesbenz.sechub.sharedkernel.validation.UserIdValidationImpl; @@ -38,9 +36,9 @@ SignupJsonInputValidator.class, UserIdValidationImpl.class, EmailValidationImpl.class, - ApiVersionValidationFactory.class, - AnonymousSignupRestControllerMockTest.SimpleTestConfiguration.class }) + ApiVersionValidationFactory.class }) /* @formatter:on */ +@Import(TestAdministrationSecurityConfiguration.class) @WithMockUser @ActiveProfiles(Profiles.TEST) public class AnonymousSignupRestControllerMockTest { @@ -152,13 +150,6 @@ public void calling_with_api_1_0_and_userid_set_but_NO_valid_email_returns_HTTP_ /* @formatter:on */ } - @TestConfiguration - @Profile(Profiles.TEST) - @EnableAutoConfiguration - public static class SimpleTestConfiguration extends AbstractSecHubAPISecurityConfiguration { - - } - private SignupJsonInput createUserSelfRegistration(String api, String email, String name) { SignupJsonInput created = new SignupJsonInput(); diff --git a/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/signup/SignupAdministrationRestControllerMockTest.java b/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/signup/SignupAdministrationRestControllerMockTest.java index eb5015f332..0588feebfc 100644 --- a/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/signup/SignupAdministrationRestControllerMockTest.java +++ b/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/signup/SignupAdministrationRestControllerMockTest.java @@ -13,25 +13,24 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.Import; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; +import com.mercedesbenz.sechub.domain.administration.TestAdministrationSecurityConfiguration; import com.mercedesbenz.sechub.sharedkernel.Profiles; -import com.mercedesbenz.sechub.sharedkernel.security.AbstractSecHubAPISecurityConfiguration; import com.mercedesbenz.sechub.sharedkernel.security.RoleConstants; import com.mercedesbenz.sechub.test.TestPortProvider; @RunWith(SpringRunner.class) -@WebMvcTest(SignupAdministrationRestController.class) -@ContextConfiguration(classes = { SignupAdministrationRestController.class, SignupAdministrationRestControllerMockTest.SimpleTestConfiguration.class }) +@WebMvcTest +@ContextConfiguration(classes = { SignupAdministrationRestController.class }) +@Import(TestAdministrationSecurityConfiguration.class) @WithMockUser(roles = RoleConstants.ROLE_SUPERADMIN) @ActiveProfiles({ Profiles.TEST, Profiles.ADMIN_ACCESS }) public class SignupAdministrationRestControllerMockTest { @@ -96,11 +95,4 @@ public void listUserSignups_results_in_a_filled_list_when_2_signups_exist() thro /* @formatter:on */ } - @TestConfiguration - @Profile(Profiles.TEST) - @EnableAutoConfiguration - public static class SimpleTestConfiguration extends AbstractSecHubAPISecurityConfiguration { - - } - } \ No newline at end of file diff --git a/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/user/UserAdministrationRestControllerMockTest.java b/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/user/UserAdministrationRestControllerMockTest.java index 2deaec8f2b..b42a3f6744 100644 --- a/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/user/UserAdministrationRestControllerMockTest.java +++ b/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/user/UserAdministrationRestControllerMockTest.java @@ -22,30 +22,29 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.Import; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; +import com.mercedesbenz.sechub.domain.administration.TestAdministrationSecurityConfiguration; import com.mercedesbenz.sechub.domain.administration.project.Project; import com.mercedesbenz.sechub.domain.administration.signup.AnonymousSignupCreateService; import com.mercedesbenz.sechub.domain.administration.signup.Signup; import com.mercedesbenz.sechub.domain.administration.signup.SignupRepository; import com.mercedesbenz.sechub.sharedkernel.Profiles; -import com.mercedesbenz.sechub.sharedkernel.security.AbstractSecHubAPISecurityConfiguration; import com.mercedesbenz.sechub.sharedkernel.security.RoleConstants; import com.mercedesbenz.sechub.test.TestPortProvider; @RunWith(SpringRunner.class) -@WebMvcTest(UserAdministrationRestController.class) -@ContextConfiguration(classes = { UserAdministrationRestController.class, UserAdministrationRestControllerMockTest.SimpleTestConfiguration.class }) +@WebMvcTest +@ContextConfiguration(classes = { UserAdministrationRestController.class }) @WithMockUser(roles = RoleConstants.ROLE_SUPERADMIN) +@Import(TestAdministrationSecurityConfiguration.class) @ActiveProfiles({ Profiles.TEST, Profiles.ADMIN_ACCESS }) public class UserAdministrationRestControllerMockTest { @@ -194,11 +193,4 @@ public void calling_with_api_1_0_and_valid_userid_and_email_returns_HTTP_200() t /* @formatter:on */ } - @TestConfiguration - @Profile(Profiles.TEST) - @EnableAutoConfiguration - public static class SimpleTestConfiguration extends AbstractSecHubAPISecurityConfiguration { - - } - } diff --git a/sechub-api-java/src/test/java/com/mercedesbenz/sechub/api/generator/Template.java b/sechub-api-java/src/test/java/com/mercedesbenz/sechub/api/generator/CodeTemplate.java similarity index 92% rename from sechub-api-java/src/test/java/com/mercedesbenz/sechub/api/generator/Template.java rename to sechub-api-java/src/test/java/com/mercedesbenz/sechub/api/generator/CodeTemplate.java index 1b115771d3..68e181d237 100644 --- a/sechub-api-java/src/test/java/com/mercedesbenz/sechub/api/generator/Template.java +++ b/sechub-api-java/src/test/java/com/mercedesbenz/sechub/api/generator/CodeTemplate.java @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT package com.mercedesbenz.sechub.api.generator; -class Template { +class CodeTemplate { StringBuilder sb = new StringBuilder(); String getCode() { diff --git a/sechub-api-java/src/test/java/com/mercedesbenz/sechub/api/generator/InternalAccessModelFileGenerator.java b/sechub-api-java/src/test/java/com/mercedesbenz/sechub/api/generator/InternalAccessModelFileGenerator.java index f45a5c1bdb..fe433832eb 100644 --- a/sechub-api-java/src/test/java/com/mercedesbenz/sechub/api/generator/InternalAccessModelFileGenerator.java +++ b/sechub-api-java/src/test/java/com/mercedesbenz/sechub/api/generator/InternalAccessModelFileGenerator.java @@ -41,7 +41,7 @@ private void generateAbstractModel(MapGenInfo info) throws Exception { LOG.debug("To : {}", genFile); LOG.debug(""); - Template template = new Template(); + CodeTemplate template = new CodeTemplate(); template.addLine("// SPDX-License-Identifier: MIT"); template.addLine("package " + context.getTargetAbstractModelPackage() + ";"); template.addLine(""); @@ -102,7 +102,7 @@ private void generateAbstractModel(MapGenInfo info) throws Exception { } - private void generateMethods(Template template, List methods) { + private void generateMethods(CodeTemplate template, List methods) { for (Method method : methods) { context.getSetterGetterSupport().generateMethod(method, template, "public", true, "delegate"); } diff --git a/sechub-api-java/src/test/java/com/mercedesbenz/sechub/api/generator/PublicModelFileGenerator.java b/sechub-api-java/src/test/java/com/mercedesbenz/sechub/api/generator/PublicModelFileGenerator.java index d22bdd8f89..907964c44a 100644 --- a/sechub-api-java/src/test/java/com/mercedesbenz/sechub/api/generator/PublicModelFileGenerator.java +++ b/sechub-api-java/src/test/java/com/mercedesbenz/sechub/api/generator/PublicModelFileGenerator.java @@ -46,7 +46,7 @@ private void generatetPublicModel(MapGenInfo info, boolean overwritePublicModelF String internalAccessClass = context.getTargetAbstractModelPackage() + "." + info.targetInternalAccessClassName; - Template template = new Template(); + CodeTemplate template = new CodeTemplate(); template.addLine("// SPDX-License-Identifier: MIT"); template.addLine("package " + context.getTargetModelPackage() + ";"); template.addLine(""); @@ -114,13 +114,13 @@ private void generatetPublicModel(MapGenInfo info, boolean overwritePublicModelF } - private void generatePublicSetterGetterMethods(MapGenInfo info, Template template) { + private void generatePublicSetterGetterMethods(MapGenInfo info, CodeTemplate template) { for (Method method : collectGettersAndSetters(info.fromGenclazz)) { context.getSetterGetterSupport().generateMethod(method, template, "public", false, "internalAccess"); } } - private void generateAdditionalWrapperMethods(MapGenInfo info, Template template) { + private void generateAdditionalWrapperMethods(MapGenInfo info, CodeTemplate template) { Map map = info.getReferenceMap(); for (String beanName : map.keySet()) { @@ -130,7 +130,7 @@ private void generateAdditionalWrapperMethods(MapGenInfo info, Template template } - private void generateMethodsToReferenceOtherWrapper(String beanName, BeanDataContainer other, Template template) { + private void generateMethodsToReferenceOtherWrapper(String beanName, BeanDataContainer other, CodeTemplate template) { String fieldName = asFieldName(beanName); if (other.isAsList()) { @@ -168,7 +168,7 @@ private void generateMethodsToReferenceOtherWrapper(String beanName, BeanDataCon } } - private void generateAdditionalWrapperFields(MapGenInfo info, Template template) { + private void generateAdditionalWrapperFields(MapGenInfo info, CodeTemplate template) { Map map = info.getReferenceMap(); for (String beanName : map.keySet()) { @@ -178,7 +178,7 @@ private void generateAdditionalWrapperFields(MapGenInfo info, Template template) } - private void generateAdditionalWrapperFieldsForOther(String beanName, BeanDataContainer other, Template template) { + private void generateAdditionalWrapperFieldsForOther(String beanName, BeanDataContainer other, CodeTemplate template) { template.addLine(" private " + other.asTargetTypeResult() + " " + asFieldName(beanName) + ";"); } } diff --git a/sechub-api-java/src/test/java/com/mercedesbenz/sechub/api/generator/SetterGetterGenerationSupport.java b/sechub-api-java/src/test/java/com/mercedesbenz/sechub/api/generator/SetterGetterGenerationSupport.java index 7baed8d71b..c6a14bd5e9 100644 --- a/sechub-api-java/src/test/java/com/mercedesbenz/sechub/api/generator/SetterGetterGenerationSupport.java +++ b/sechub-api-java/src/test/java/com/mercedesbenz/sechub/api/generator/SetterGetterGenerationSupport.java @@ -17,7 +17,7 @@ public SetterGetterGenerationSupport(ApiWrapperGenerationContext context) { this.context = context; } - public void generateMethod(Method method, Template template, String visibility, boolean handleNull, String callFieldName) { + public void generateMethod(Method method, CodeTemplate template, String visibility, boolean handleNull, String callFieldName) { List paramList = getParameters(method); String methodSignature = createMethodSignature(method, paramList, visibility); diff --git a/sechub-archunit-test/src/test/java/mercedesbenz/com/sechub/archunit/ArchUnitImportOptions.java b/sechub-archunit-test/src/test/java/mercedesbenz/com/sechub/archunit/ArchUnitImportOptions.java index 9e62666c42..7a7c37fcd2 100644 --- a/sechub-archunit-test/src/test/java/mercedesbenz/com/sechub/archunit/ArchUnitImportOptions.java +++ b/sechub-archunit-test/src/test/java/mercedesbenz/com/sechub/archunit/ArchUnitImportOptions.java @@ -1,17 +1,31 @@ // SPDX-License-Identifier: MIT package mercedesbenz.com.sechub.archunit; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + import com.tngtech.archunit.core.importer.ImportOption; public class ArchUnitImportOptions { + public static Path SECHUB_ROOT_PATH = resolveRoothPath(); - public static String SECHUB_ROOT_PATH = "../"; + private static Path resolveRoothPath() { + try { + return Paths.get("./../").toRealPath(); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } /* Ignore specific directories */ static ImportOption ignoreAllTests = location -> { return !location.contains("/test/"); // ignore any URI to sources that contains '/test/' }; + static List ignoreFolders = new ArchUnitRuntimeSupport().createImportOptionsIgnoreFolder(); // ignore specific folders e.g. build folders + static ImportOption ignoreSechubOpenAPIJava = location -> { return !location.contains("/sechub-openapi-java/"); // ignore any URI to sources that contains '/sechub-openapi-java/' }; diff --git a/sechub-archunit-test/src/test/java/mercedesbenz/com/sechub/archunit/ArchUnitRuntimeSupport.java b/sechub-archunit-test/src/test/java/mercedesbenz/com/sechub/archunit/ArchUnitRuntimeSupport.java new file mode 100644 index 0000000000..0278ab6b37 --- /dev/null +++ b/sechub-archunit-test/src/test/java/mercedesbenz/com/sechub/archunit/ArchUnitRuntimeSupport.java @@ -0,0 +1,27 @@ +package mercedesbenz.com.sechub.archunit; + +import java.util.*; + +import com.tngtech.archunit.core.importer.ImportOption; + +class ArchUnitRuntimeSupport { + + public List createImportOptionsIgnoreFolder() { + List importOptions = new ArrayList<>(); + + // comma seperated list of folders to ignore e.g. build folders from different + // builds + String folderToIgnore = System.getProperty("sechub.archunit.ignoreFolders"); + if (folderToIgnore == null || folderToIgnore.isBlank()) { + return importOptions; + } + + folderToIgnore = folderToIgnore.trim(); + String[] folders = folderToIgnore.split(","); + for (String folder : folders) { + importOptions.add(location -> !location.contains(folder)); + } + return importOptions; + } + +} \ No newline at end of file diff --git a/sechub-archunit-test/src/test/java/mercedesbenz/com/sechub/archunit/CodingRulesTest.java b/sechub-archunit-test/src/test/java/mercedesbenz/com/sechub/archunit/CodingRulesTest.java index 57c1272f0b..4e02386977 100644 --- a/sechub-archunit-test/src/test/java/mercedesbenz/com/sechub/archunit/CodingRulesTest.java +++ b/sechub-archunit-test/src/test/java/mercedesbenz/com/sechub/archunit/CodingRulesTest.java @@ -32,6 +32,7 @@ void classes_should_not_use_deprecated_members() { /* @formatter:off */ JavaClasses importedClasses = new ClassFileImporter() .withImportOption(ignoreAllTests) + .withImportOptions(ignoreFolders) .withImportOption(ignoreSechubOpenAPIJava) .withImportOption(ignoreNessusAdapter) .withImportOption(ignoreNessusProduct) @@ -75,6 +76,7 @@ void test_classes_should_be_in_the_same_package_as_implementation() { /* prepare */ /* @formatter:off */ JavaClasses importedClasses = new ClassFileImporter() + .withImportOptions(ignoreFolders) .withImportOption(ignoreSechubOpenAPIJava) .withImportOption(ignoreSechubApiJava) .withImportOption(ignoreDocGen) @@ -105,6 +107,7 @@ void classes_should_not_use_standard_streams() { /* @formatter:off */ JavaClasses importedClasses = new ClassFileImporter() .withImportOption(ignoreAllTests) + .withImportOptions(ignoreFolders) .withImportOption(ignoreSechubOpenAPIJava) .withImportOption(ignoreIntegrationTest) .withImportOption(ignoreDocGen) @@ -126,6 +129,7 @@ private JavaClasses ignoreTestGeneratedAndDeprecatedPackages() { /* @formatter:off */ return new ClassFileImporter() .withImportOption(ignoreAllTests) + .withImportOptions(ignoreFolders) .withImportOption(ignoreSechubOpenAPIJava) .withImportOption(ignoreNessusAdapter) .withImportOption(ignoreNessusProduct) diff --git a/sechub-archunit-test/src/test/java/mercedesbenz/com/sechub/archunit/DomainAccessRulesTest.java b/sechub-archunit-test/src/test/java/mercedesbenz/com/sechub/archunit/DomainAccessRulesTest.java index 0e7c36aaec..291bb06f21 100644 --- a/sechub-archunit-test/src/test/java/mercedesbenz/com/sechub/archunit/DomainAccessRulesTest.java +++ b/sechub-archunit-test/src/test/java/mercedesbenz/com/sechub/archunit/DomainAccessRulesTest.java @@ -46,6 +46,8 @@ void no_class_in_one_domain_communicate_with_another_domain(String domainToTest) /* prepare */ /* @formatter:off */ JavaClasses importedClasses = new ClassFileImporter() + .withImportOption(ignoreAllTests) + .withImportOptions(ignoreFolders) .withImportOption(ignoreDevelopertools) .withImportOption(ignoreJarFiles) .importPath(SECHUB_ROOT_PATH); diff --git a/sechub-archunit-test/src/test/java/mercedesbenz/com/sechub/archunit/NamingConventionTest.java b/sechub-archunit-test/src/test/java/mercedesbenz/com/sechub/archunit/NamingConventionTest.java index d212d12372..99d254ded1 100644 --- a/sechub-archunit-test/src/test/java/mercedesbenz/com/sechub/archunit/NamingConventionTest.java +++ b/sechub-archunit-test/src/test/java/mercedesbenz/com/sechub/archunit/NamingConventionTest.java @@ -10,6 +10,8 @@ import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.lang.syntax.ArchRuleDefinition; @AnalyzeClasses public class NamingConventionTest { @@ -19,6 +21,7 @@ void classes_in_test_packages_containing_test_or_assert_in_name() { /* prepare */ /* @formatter:off */ JavaClasses importedClasses = new ClassFileImporter() + .withImportOptions(ignoreFolders) .withImportOption(ignoreSechubOpenAPIJava) .withImportOption(ignoreSechubTestframework) .withImportOption(ignoreSharedkernelTest) @@ -27,17 +30,20 @@ void classes_in_test_packages_containing_test_or_assert_in_name() { .importPath(SECHUB_ROOT_PATH); /* execute + test */ - classes() + ArchRule rule = ArchRuleDefinition.classes() .that() .resideInAPackage("..test..") .should() .haveSimpleNameContaining("Test") .orShould() + .haveSimpleNameContaining("Assert") + .orShould() .haveNameMatching(".*\\.Assert.*") // including inner classes .orShould() .haveNameMatching(".*Test\\$.*") // including inner classes - .because("Tests classes should contain 'Test' or 'Assert' in their name.") - .check(importedClasses); + .because("Tests classes should contain 'Test' or 'Assert' in their name."); + + rule.check(importedClasses); /* @formatter:on */ } @@ -46,6 +52,7 @@ void service_annotated_classes_contain_service_or_executor_in_name() { /* prepare */ /* @formatter:off */ JavaClasses importedClasses = new ClassFileImporter() + .withImportOptions(ignoreFolders) .withImportOption(ignoreAllTests) .withImportOption(ignoreSechubOpenAPIJava) .withImportOption(ignoreJarFiles) diff --git a/sechub-authorization/src/main/java/com/mercedesbenz/sechub/domain/authorization/AuthUserDetailsService.java b/sechub-authorization/src/main/java/com/mercedesbenz/sechub/domain/authorization/AuthUserDetailsService.java index 9c27dc079a..73cae11e53 100644 --- a/sechub-authorization/src/main/java/com/mercedesbenz/sechub/domain/authorization/AuthUserDetailsService.java +++ b/sechub-authorization/src/main/java/com/mercedesbenz/sechub/domain/authorization/AuthUserDetailsService.java @@ -52,7 +52,7 @@ public class AuthUserDetailsService implements UserDetailsService { public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { /* @formatter:off */ return repository - .findByUserId(username) + .findByUserId(username.toLowerCase()) .map(AuthUserDetailsService::adoptUser) .orElseThrow(() -> new UsernameNotFoundException(username)); /* @formatter:on */ diff --git a/sechub-cli/script/build-debian-packages.sh b/sechub-cli/script/build-debian-packages.sh index 3bd7d52f8d..25f35bb2cf 100755 --- a/sechub-cli/script/build-debian-packages.sh +++ b/sechub-cli/script/build-debian-packages.sh @@ -5,7 +5,7 @@ set -e # Debian packaging data DEB_PACKAGE_NAME="sechub-client" DEB_SECTION="misc" -DEB_MAINTAINER="SecHub FOSS team " +DEB_MAINTAINER="SecHub FOSS team " DEB_HOMEPAGE="https://github.com/mercedes-benz/sechub" DEB_DESCRIPTION="The SecHub command line client. See $DEB_HOMEPAGE" DEB_BIN_PATH="usr/bin" # Where to place the SecHub client executable on install diff --git a/sechub-commons-archive/src/main/java/com/mercedesbenz/sechub/commons/archive/ArchiveSupport.java b/sechub-commons-archive/src/main/java/com/mercedesbenz/sechub/commons/archive/ArchiveSupport.java index e03c99e287..a57352dc9f 100644 --- a/sechub-commons-archive/src/main/java/com/mercedesbenz/sechub/commons/archive/ArchiveSupport.java +++ b/sechub-commons-archive/src/main/java/com/mercedesbenz/sechub/commons/archive/ArchiveSupport.java @@ -4,6 +4,7 @@ import static java.util.Objects.requireNonNull; import java.io.File; +import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; @@ -329,6 +330,28 @@ public void compressFolder(ArchiveType type, File folder, File targetArchiveFile } + /** + * Just extracts the given archive file to target folder, without any + * adjustments. + * + * @param archiveType archive type to use + * @param archiveFile file which shall be extracted + * @param targetFolder target folder where extraction shall be done + * @throws IOException + */ + public ArchiveExtractionResult extractFileAsIsToFolder(ArchiveType archiveType, File archiveFile, File targetFolder, + ArchiveExtractionConstraints archiveExtractionConstraints) throws IOException { + try (FileInputStream sourceInputStream = new FileInputStream(archiveFile)) { + String sourceLocation = archiveFile.getAbsolutePath(); + var result = switch (archiveType) { + case TAR -> extractTar(sourceInputStream, sourceLocation, targetFolder, null, archiveExtractionConstraints); + case ZIP -> extractZip(sourceInputStream, sourceLocation, targetFolder, null, archiveExtractionConstraints); + default -> throw new IllegalArgumentException("Archive type: " + archiveType + " is not supported!"); + }; + return result; + } + } + private void compressRecursively(String basePath, ArchiveOutputStream outputStream, File file, ArchiveType type, String pathAddition, CreationPathContext creationPathContext) throws IOException { @@ -407,13 +430,13 @@ private void compressRecursively(String basePath, ArchiveOutputStream outputStre } private ArchiveExtractionResult extractTar(InputStream sourceInputStream, String sourceLocation, File outputDir, - SecHubFileStructureDataProvider fileStructureProvider, ArchiveExtractionConstraints archiveExtractionConstraints) throws IOException { + SecHubFileStructureDataProvider fileStructurDataProvider, ArchiveExtractionConstraints archiveExtractionConstraints) throws IOException { try (ArchiveInputStream archiveInputStream = new ArchiveStreamFactory().createArchiveInputStream(ArchiveType.TAR.getType(), sourceInputStream)) { if (!(archiveInputStream instanceof TarArchiveInputStream)) { throw new IOException("Cannot extract: " + sourceLocation + " because it is not a tar tar"); } try (SafeArchiveInputStream safeArchiveInputStream = new SafeArchiveInputStream(archiveInputStream, archiveExtractionConstraints)) { - return extract(safeArchiveInputStream, sourceLocation, outputDir, fileStructureProvider); + return extract(safeArchiveInputStream, sourceLocation, outputDir, fileStructurDataProvider); } } catch (ArchiveException e) { throw new IOException("Was not able to extract tar:" + sourceLocation + " at " + outputDir, e); @@ -422,11 +445,11 @@ private ArchiveExtractionResult extractTar(InputStream sourceInputStream, String } private ArchiveExtractionResult extractZip(InputStream sourceInputStream, String sourceLocation, File outputDir, - SecHubFileStructureDataProvider configuration, ArchiveExtractionConstraints archiveExtractionConstraints) throws IOException { + SecHubFileStructureDataProvider fileStructurDataProvider, ArchiveExtractionConstraints archiveExtractionConstraints) throws IOException { try (ArchiveInputStream archiveInputStream = new ArchiveStreamFactory().createArchiveInputStream(ArchiveType.ZIP.getType(), sourceInputStream); SafeArchiveInputStream safeArchiveInputStream = new SafeArchiveInputStream(archiveInputStream, archiveExtractionConstraints)) { - return extract(safeArchiveInputStream, sourceLocation, outputDir, configuration); + return extract(safeArchiveInputStream, sourceLocation, outputDir, fileStructurDataProvider); } catch (ArchiveException e) { throw new IOException("Was not able to extract tar:" + sourceLocation + " at " + outputDir, e); @@ -460,7 +483,7 @@ public boolean isZipFileStream(InputStream inputStream) { } private ArchiveExtractionResult extract(SafeArchiveInputStream safeArchiveInputStream, String sourceLocation, File outputDir, - SecHubFileStructureDataProvider fileStructureProvider) throws ArchiveException, IOException { + SecHubFileStructureDataProvider fileStructurDataProvider) throws ArchiveException, IOException { ArchiveExtractionResult result = new ArchiveExtractionResult(); result.targetLocation = outputDir.getAbsolutePath(); @@ -473,7 +496,7 @@ private ArchiveExtractionResult extract(SafeArchiveInputStream safeArchiveInputS throw new IllegalStateException("Entry path is null - cannot be handled!"); } - ArchiveTransformationData data = createTransformationData(fileStructureProvider, name); + ArchiveTransformationData data = createTransformationData(fileStructurDataProvider, name); if (data == null) { continue; } diff --git a/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/ConfigurationFailureException.java b/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/ConfigurationFailureException.java new file mode 100644 index 0000000000..c089e29f3d --- /dev/null +++ b/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/ConfigurationFailureException.java @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.core; + +public class ConfigurationFailureException extends Exception { + + private static final long serialVersionUID = -384180667154600386L; + + public ConfigurationFailureException(String message) { + super(message); + } + + public ConfigurationFailureException(String message, Throwable cause) { + super(message, cause); + } + +} \ No newline at end of file diff --git a/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/util/SecHubStorageUtil.java b/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/util/SecHubStorageUtil.java index 988636685c..6279b686c0 100644 --- a/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/util/SecHubStorageUtil.java +++ b/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/util/SecHubStorageUtil.java @@ -13,4 +13,8 @@ public class SecHubStorageUtil { public static String createStoragePathForProject(String projectId) { return "jobstorage/" + projectId; } + + public static String createAssetStoragePath() { + return "assets"; + } } diff --git a/sechub-commons-model-testframework/src/main/java/com/mercedesbenz/sechub/commons/model/login/TestWebLoginConfigurationBuilder.java b/sechub-commons-model-testframework/src/main/java/com/mercedesbenz/sechub/commons/model/login/TestWebLoginConfigurationBuilder.java index d55646140f..1cabf1592c 100644 --- a/sechub-commons-model-testframework/src/main/java/com/mercedesbenz/sechub/commons/model/login/TestWebLoginConfigurationBuilder.java +++ b/sechub-commons-model-testframework/src/main/java/com/mercedesbenz/sechub/commons/model/login/TestWebLoginConfigurationBuilder.java @@ -41,12 +41,14 @@ public ScriptPageEntryBuilder formScripted(String user, String login) { return builder; } - public TestWebLoginConfigurationBuilder totp(String seed, int validityInSeconds, TOTPHashAlgorithm hashAlgorithm, int tokenLength) { + public TestWebLoginConfigurationBuilder totp(String seed, int validityInSeconds, TOTPHashAlgorithm hashAlgorithm, int tokenLength, + EncodingType encodingType) { WebLoginTOTPConfiguration totp = new WebLoginTOTPConfiguration(); totp.setSeed(seed); totp.setValidityInSeconds(validityInSeconds); totp.setHashAlgorithm(hashAlgorithm); totp.setTokenLength(tokenLength); + totp.setEncodingType(encodingType); loginConfig.setTotp(totp); return this; diff --git a/sechub-commons-model/build.gradle b/sechub-commons-model/build.gradle index f0e7b9d98b..e8b8741516 100644 --- a/sechub-commons-model/build.gradle +++ b/sechub-commons-model/build.gradle @@ -17,4 +17,5 @@ dependencies{ testImplementation spring_boot_dependency.mockito_core testImplementation spring_boot_dependency.hamcrest + testImplementation spring_boot_dependency.assertj_core } \ No newline at end of file diff --git a/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/login/EncodingType.java b/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/login/EncodingType.java new file mode 100644 index 0000000000..9365f94976 --- /dev/null +++ b/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/login/EncodingType.java @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.model.login; + +import static com.fasterxml.jackson.annotation.JsonFormat.Feature.*; + +import com.fasterxml.jackson.annotation.JsonFormat; + +public enum EncodingType { + + @JsonFormat(with = ACCEPT_CASE_INSENSITIVE_PROPERTIES) + AUTODETECT, + + @JsonFormat(with = ACCEPT_CASE_INSENSITIVE_PROPERTIES) + HEX, + + @JsonFormat(with = ACCEPT_CASE_INSENSITIVE_PROPERTIES) + BASE32, + + @JsonFormat(with = ACCEPT_CASE_INSENSITIVE_PROPERTIES) + BASE64, + + @JsonFormat(with = ACCEPT_CASE_INSENSITIVE_PROPERTIES) + PLAIN, + + ; + +} diff --git a/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/login/WebLoginConfiguration.java b/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/login/WebLoginConfiguration.java index 3284dd9875..2809810f41 100644 --- a/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/login/WebLoginConfiguration.java +++ b/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/login/WebLoginConfiguration.java @@ -5,6 +5,7 @@ import java.util.Optional; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.mercedesbenz.sechub.commons.model.template.TemplateData; @JsonIgnoreProperties(ignoreUnknown = true) public class WebLoginConfiguration { @@ -19,6 +20,8 @@ public class WebLoginConfiguration { private WebLoginTOTPConfiguration totp; + private TemplateData templateData; + public URL getUrl() { return url; } @@ -43,4 +46,12 @@ public void setTotp(WebLoginTOTPConfiguration totp) { this.totp = totp; } + public TemplateData getTemplateData() { + return templateData; + } + + public void setTemplateData(TemplateData templateData) { + this.templateData = templateData; + } + } \ No newline at end of file diff --git a/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/login/WebLoginTOTPConfiguration.java b/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/login/WebLoginTOTPConfiguration.java index 9435499eee..9ddd3a5533 100644 --- a/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/login/WebLoginTOTPConfiguration.java +++ b/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/login/WebLoginTOTPConfiguration.java @@ -13,20 +13,24 @@ public class WebLoginTOTPConfiguration { public static final String PROPERTY_VALIDITY_IN_SECONDS = "validityInSeconds"; public static final String PROPERTY_TOKEN_LENGTH = "tokenLength"; public static final String PROPERTY_HASH_ALGORITHM = "hashAlgorithm"; + public static final String PROPERTY_ENCODING_TYPE = "encodingType"; public static final int DEFAULT_VALIDITY_IN_SECONDS = 30; public static final int DEFAULT_TOKEN_LENGTH = 6; public static final TOTPHashAlgorithm DEFAULT_HASH_ALGORITHM = TOTPHashAlgorithm.HMAC_SHA1; + public static final EncodingType DEFAULT_ENCODING_TYPE = EncodingType.AUTODETECT; private SealedObject seed; private int validityInSeconds; private int tokenLength; private TOTPHashAlgorithm hashAlgorithm; + private EncodingType encodingType; public WebLoginTOTPConfiguration() { this.validityInSeconds = DEFAULT_VALIDITY_IN_SECONDS; this.tokenLength = DEFAULT_TOKEN_LENGTH; this.hashAlgorithm = DEFAULT_HASH_ALGORITHM; + this.encodingType = DEFAULT_ENCODING_TYPE; } public String getSeed() { @@ -61,4 +65,12 @@ public void setHashAlgorithm(TOTPHashAlgorithm hashAlgorithm) { this.hashAlgorithm = hashAlgorithm; } + public EncodingType getEncodingType() { + return encodingType; + } + + public void setEncodingType(EncodingType encodingType) { + this.encodingType = encodingType; + } + } diff --git a/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/template/TemplateData.java b/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/template/TemplateData.java new file mode 100644 index 0000000000..a0f2216ef4 --- /dev/null +++ b/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/template/TemplateData.java @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.model.template; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Template data for SecHub configuration model. Here users can define user + * specific template data - e.g. variables like "username", "password" + * + * @author Albert Tregnaghi + * + */ +public class TemplateData { + + private Map variables = new LinkedHashMap<>(); + + public Map getVariables() { + return variables; + } + +} diff --git a/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/template/TemplateDataResolver.java b/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/template/TemplateDataResolver.java new file mode 100644 index 0000000000..7da6386f33 --- /dev/null +++ b/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/template/TemplateDataResolver.java @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.model.template; + +import java.util.Optional; + +import com.mercedesbenz.sechub.commons.model.SecHubConfigurationModel; +import com.mercedesbenz.sechub.commons.model.SecHubWebScanConfiguration; +import com.mercedesbenz.sechub.commons.model.login.WebLoginConfiguration; + +public class TemplateDataResolver { + + public TemplateData resolveTemplateData(TemplateType type, SecHubConfigurationModel configuration) { + if (type == null) { + return null; + } + if (configuration == null) { + return null; + } + switch (type) { + case WEBSCAN_LOGIN: + return resolveWebScanLoginTemplateData(configuration); + default: + break; + } + return null; + } + + private TemplateData resolveWebScanLoginTemplateData(SecHubConfigurationModel configuration) { + Optional webScanOpt = configuration.getWebScan(); + if (webScanOpt.isEmpty()) { + return null; + } + SecHubWebScanConfiguration webScan = webScanOpt.get(); + Optional loginOpt = webScan.getLogin(); + if (loginOpt.isEmpty()) { + return null; + } + WebLoginConfiguration login = loginOpt.get(); + return login.getTemplateData(); + + } +} diff --git a/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/template/TemplateDefinition.java b/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/template/TemplateDefinition.java new file mode 100644 index 0000000000..b8296dbb29 --- /dev/null +++ b/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/template/TemplateDefinition.java @@ -0,0 +1,239 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.model.template; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.mercedesbenz.sechub.commons.model.JSONable; + +@JsonPropertyOrder({ "id", "type", "variables", "assets" }) +public class TemplateDefinition implements JSONable { + + private static TemplateDefinition IMPORTER = new TemplateDefinition(); + + public static final String PROPERTY_TYPE = "type"; + public static final String PROPERTY_ID = "id"; + public static final String PROPERTY_ASSET_ID = "assetId"; + public static final String PROPERTY_VARIABLES = "variables"; + + private TemplateType type; + + private String assetId; + private List variables = new ArrayList<>(); + + private String id; + + public TemplateDefinition() { + } + + public static TemplateDefinitionBuilder builder() { + return new TemplateDefinitionBuilder(); + } + + public static TemplateDefinition from(String json) { + return IMPORTER.fromJSON(json); + } + + public void setId(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + public void setAssetId(String assetId) { + this.assetId = assetId; + } + + public String getAssetId() { + return assetId; + } + + public List getVariables() { + return variables; + } + + public void setType(TemplateType type) { + this.type = type; + } + + public TemplateType getType() { + return type; + } + + @Override + public Class getJSONTargetClass() { + return TemplateDefinition.class; + } + + @Override + public int hashCode() { + return Objects.hash(assetId, id, type, variables); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + TemplateDefinition other = (TemplateDefinition) obj; + return Objects.equals(assetId, other.assetId) && Objects.equals(id, other.id) && type == other.type && Objects.equals(variables, other.variables); + } + + public static class TemplateDefinitionBuilder { + + private String assetId; + private String templateId; + private TemplateType templateType; + + private TemplateDefinitionBuilder() { + + } + + public TemplateDefinitionBuilder assetId(String assetId) { + this.assetId = assetId; + return this; + } + + public TemplateDefinitionBuilder templateId(String templateId) { + this.templateId = templateId; + return this; + } + + public TemplateDefinitionBuilder templateType(TemplateType templateType) { + this.templateType = templateType; + return this; + } + + public TemplateDefinition build() { + if (assetId == null) { + throw new IllegalStateException("assetId not defined"); + } + if (templateId == null) { + throw new IllegalStateException("templateId not defined"); + } + if (templateType == null) { + throw new IllegalStateException("templateType not defined"); + } + TemplateDefinition def = new TemplateDefinition(); + def.id = templateId; + def.type = templateType; + def.assetId = assetId; + + return def; + } + + } + + public static class TemplateVariable { + public static final String PROPERTY_NAME = "name"; + public static final String PROPERTY_OPTIONAL = "optional"; + public static final String PROPERTY_VALIDATION = "validation"; + + private String name; + private boolean optional; + private TemplateVariableValidation validation; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isOptional() { + return optional; + } + + public void setOptional(boolean optional) { + this.optional = optional; + } + + public TemplateVariableValidation getValidation() { + return validation; + } + + public void setValidation(TemplateVariableValidation validation) { + this.validation = validation; + } + + @Override + public int hashCode() { + return Objects.hash(name, optional, validation); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + TemplateVariable other = (TemplateVariable) obj; + return Objects.equals(name, other.name) && optional == other.optional && Objects.equals(validation, other.validation); + } + + } + + public static class TemplateVariableValidation { + + public static final String PROPERTY_MIN_LENGTH = "minLength"; + public static final String PROPERTY_MAX_LENGTH = "maxLength"; + public static final String PROPERTY_REGULAR_EXPRESSION = "regularExpression"; + + private Integer minLength; + private Integer maxLength; + private String regularExpression; + + public Integer getMinLength() { + return minLength; + } + + public void setMinLength(Integer minLength) { + this.minLength = minLength; + } + + public Integer getMaxLength() { + return maxLength; + } + + public void setMaxLength(Integer maxLength) { + this.maxLength = maxLength; + } + + public String getRegularExpression() { + return regularExpression; + } + + public void setRegularExpression(String regularExpression) { + this.regularExpression = regularExpression; + } + + @Override + public int hashCode() { + return Objects.hash(maxLength, minLength, regularExpression); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + TemplateVariableValidation other = (TemplateVariableValidation) obj; + return Objects.equals(maxLength, other.maxLength) && Objects.equals(minLength, other.minLength) + && Objects.equals(regularExpression, other.regularExpression); + } + } + +} diff --git a/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/template/TemplateIdenifierConstants.java b/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/template/TemplateIdenifierConstants.java new file mode 100644 index 0000000000..56e15f48ca --- /dev/null +++ b/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/template/TemplateIdenifierConstants.java @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.model.template; + +import com.mercedesbenz.sechub.commons.core.MustBeKeptStable; + +/** + * Template constants - used as identifier inside database, json files but also + * for path handling inside PDS solutions. So NEVER CHANGE the content of this + * identifiers!!!! + * + * @author Albert Tregnaghi + * + */ +@MustBeKeptStable +public class TemplateIdenifierConstants { + + public static final String ID_WEBSCAN_LOGIN = "webscan-login"; + +} diff --git a/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/template/TemplateType.java b/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/template/TemplateType.java new file mode 100644 index 0000000000..c2460d27c6 --- /dev/null +++ b/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/template/TemplateType.java @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.model.template; + +import static com.mercedesbenz.sechub.commons.model.template.TemplateIdenifierConstants.*; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.mercedesbenz.sechub.commons.core.MustBeKeptStable; + +/** + * Defines the template type + * + * @author Albert Tregnaghi + * + */ +@MustBeKeptStable +public enum TemplateType { + + @JsonAlias({ ID_WEBSCAN_LOGIN }) + WEBSCAN_LOGIN(ID_WEBSCAN_LOGIN); + + private String id; + + TemplateType(String id) { + this.id = id; + } + + public String getId() { + return id; + } +} diff --git a/sechub-commons-model/src/test/java/com/mercedesbenz/sechub/commons/model/login/WebLoginTOTPConfigurationTest.java b/sechub-commons-model/src/test/java/com/mercedesbenz/sechub/commons/model/login/WebLoginTOTPConfigurationTest.java index 877b334bae..8f6b8df019 100644 --- a/sechub-commons-model/src/test/java/com/mercedesbenz/sechub/commons/model/login/WebLoginTOTPConfigurationTest.java +++ b/sechub-commons-model/src/test/java/com/mercedesbenz/sechub/commons/model/login/WebLoginTOTPConfigurationTest.java @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT package com.mercedesbenz.sechub.commons.model.login; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.Test; @@ -19,6 +19,7 @@ void default_values_are_as_expected() { assertEquals(WebLoginTOTPConfiguration.DEFAULT_VALIDITY_IN_SECONDS, defaultConfig.getValidityInSeconds()); assertEquals(WebLoginTOTPConfiguration.DEFAULT_TOKEN_LENGTH, defaultConfig.getTokenLength()); assertEquals(WebLoginTOTPConfiguration.DEFAULT_HASH_ALGORITHM, defaultConfig.getHashAlgorithm()); + assertEquals(WebLoginTOTPConfiguration.DEFAULT_ENCODING_TYPE, defaultConfig.getEncodingType()); } @Test @@ -35,6 +36,7 @@ void default_values_are_used_correctly_during_json_serialization_and_deserializa assertEquals(config.getValidityInSeconds(), expectedConfig.getValidityInSeconds()); assertEquals(config.getTokenLength(), expectedConfig.getTokenLength()); assertEquals(config.getHashAlgorithm(), expectedConfig.getHashAlgorithm()); + assertEquals(config.getEncodingType(), expectedConfig.getEncodingType()); } @Test @@ -45,6 +47,7 @@ void custom_values_are_used_correctly_during_json_serialization_and_deserializat expectedConfig.setValidityInSeconds(45); expectedConfig.setTokenLength(9); expectedConfig.setHashAlgorithm(TOTPHashAlgorithm.HMAC_SHA512); + expectedConfig.setEncodingType(EncodingType.BASE64); /* execute */ String json = JSONConverter.get().toJSON(expectedConfig); @@ -55,6 +58,7 @@ void custom_values_are_used_correctly_during_json_serialization_and_deserializat assertEquals(config.getValidityInSeconds(), expectedConfig.getValidityInSeconds()); assertEquals(config.getTokenLength(), expectedConfig.getTokenLength()); assertEquals(config.getHashAlgorithm(), expectedConfig.getHashAlgorithm()); + assertEquals(config.getEncodingType(), expectedConfig.getEncodingType()); } } diff --git a/sechub-commons-model/src/test/java/com/mercedesbenz/sechub/commons/model/template/TemplateDataResolverTest.java b/sechub-commons-model/src/test/java/com/mercedesbenz/sechub/commons/model/template/TemplateDataResolverTest.java new file mode 100644 index 0000000000..cbf6cf798d --- /dev/null +++ b/sechub-commons-model/src/test/java/com/mercedesbenz/sechub/commons/model/template/TemplateDataResolverTest.java @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.model.template; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullSource; + +import com.mercedesbenz.sechub.commons.model.JSONConverter; +import com.mercedesbenz.sechub.commons.model.SecHubConfigurationModel; + +class TemplateDataResolverTest { + + TemplateDataResolver resolverToTest; + + @BeforeEach + void beforeEach() { + resolverToTest = new TemplateDataResolver(); + } + + @Test + void webscan_login_template_data_can_be_resolved_when_defined_in_model() { + + /* prepare */ + String json = """ + { + "webScan" : { + "login" : { + + "templateData" : { + "variables" : { + "username" : "the-user", + "password" : "the-password" + } + } + } + } + } + """; + SecHubConfigurationModel model = JSONConverter.get().fromJSON(SecHubConfigurationModel.class, json); + + /* execute */ + TemplateData result = resolverToTest.resolveTemplateData(TemplateType.WEBSCAN_LOGIN, model); + + /* test */ + assertThat(result).isNotNull(); + assertThat(result.getVariables()).containsEntry("username", "the-user").containsEntry("password", "the-password"); + } + + @Test + void webscan_login_template_data_cannot_be_resolved_when_not_defined_in_model() { + + /* prepare */ + String json = """ + { + "webScan" : { + + } + } + """; + SecHubConfigurationModel model = JSONConverter.get().fromJSON(SecHubConfigurationModel.class, json); + + /* execute */ + TemplateData result = resolverToTest.resolveTemplateData(TemplateType.WEBSCAN_LOGIN, model); + + /* test */ + assertThat(result).isNull(); + } + + @ParameterizedTest + @NullSource + @EnumSource(TemplateType.class) + void template_data_is_always_null_when_model_is_empty(TemplateType templateType) { + + /* prepare */ + String json = """ + { + } + """; + SecHubConfigurationModel model = JSONConverter.get().fromJSON(SecHubConfigurationModel.class, json); + + /* execute */ + TemplateData result = resolverToTest.resolveTemplateData(templateType, model); + + /* test */ + assertThat(result).isNull(); + } + + @ParameterizedTest + @NullSource + @EnumSource(TemplateType.class) + void template_data_is_always_null_when_model_is_null(TemplateType templateType) { + + /* prepare */ + SecHubConfigurationModel model = null; + + /* execute */ + TemplateData result = resolverToTest.resolveTemplateData(templateType, model); + + /* test */ + assertThat(result).isNull(); + } + +} diff --git a/sechub-commons-model/src/test/java/com/mercedesbenz/sechub/commons/model/template/TemplateDefinitionTest.java b/sechub-commons-model/src/test/java/com/mercedesbenz/sechub/commons/model/template/TemplateDefinitionTest.java new file mode 100644 index 0000000000..073460e9e4 --- /dev/null +++ b/sechub-commons-model/src/test/java/com/mercedesbenz/sechub/commons/model/template/TemplateDefinitionTest.java @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.model.template; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import com.mercedesbenz.sechub.commons.model.template.TemplateDefinition.TemplateVariable; +import com.mercedesbenz.sechub.commons.model.template.TemplateDefinition.TemplateVariableValidation; +import com.mercedesbenz.sechub.test.TestFileReader; + +class TemplateDefinitionTest { + + @Test + public void json_to_from_works() throws Exception { + + /* prepare */ + + TemplateDefinition definition = TemplateDefinition.builder().templateId("identifier").templateType(TemplateType.WEBSCAN_LOGIN).assetId("asset1") + .build(); + + TemplateVariable variable1 = new TemplateVariable(); + variable1.setName("variable1"); + variable1.setOptional(true); + + TemplateVariableValidation validation = new TemplateVariableValidation(); + validation.setMinLength(1); + validation.setMaxLength(100); + validation.setRegularExpression("[a-z].*"); + variable1.setValidation(validation); + definition.getVariables().add(variable1); + + /* execute */ + String json = definition.toFormattedJSON(); + + /* test */ + TemplateDefinition deserialized = TemplateDefinition.from(json); + assertThat(deserialized.getId()).isEqualTo("identifier"); + assertThat(deserialized.getType()).isEqualTo(TemplateType.WEBSCAN_LOGIN); + assertThat(deserialized.getAssetId()).isEqualTo("asset1"); + assertThatList(deserialized.getVariables()).hasSize(1); + + TemplateVariable var1 = deserialized.getVariables().iterator().next(); + assertThat(var1).describedAs("variable1").hasNoNullFieldsOrProperties(); + assertThat(var1.getName()).isEqualTo("variable1"); + assertThat(var1.isOptional()).isTrue(); + assertThat(var1.getValidation()).isNotNull(); + + TemplateVariableValidation var1Validation = var1.getValidation(); + assertThat(var1Validation).isNotNull(); + assertThat(var1Validation.getMinLength()).isEqualTo(1); + assertThat(var1Validation.getMaxLength()).isEqualTo(100); + assertThat(var1Validation.getRegularExpression()).isEqualTo("[a-z].*"); + } + + @Test + public void example1_with_type_defined_by_id_and_not_enum_name_can_be_loaded() throws Exception { + /* prepare */ + String json = TestFileReader.readTextFromFile("./src/test/resources/template/template-definition-example1.json"); + + /* execute */ + TemplateDefinition deserialized = TemplateDefinition.from(json); + + /* test */ + assertThat(deserialized.getId()).isEqualTo("identifier"); + assertThat(deserialized.getType()).isEqualTo(TemplateType.WEBSCAN_LOGIN); + assertThat(deserialized.getAssetId()).isEqualTo("asset0815"); + assertThatList(deserialized.getVariables()).hasSize(1); + + TemplateVariable var1 = deserialized.getVariables().iterator().next(); + assertThat(var1).describedAs("variable1").hasNoNullFieldsOrProperties(); + assertThat(var1.getName()).isEqualTo("variable1"); + assertThat(var1.isOptional()).isTrue(); + assertThat(var1.getValidation()).isNotNull(); + + TemplateVariableValidation var1Validation = var1.getValidation(); + assertThat(var1Validation).isNotNull(); + assertThat(var1Validation.getMinLength()).isEqualTo(1); + assertThat(var1Validation.getMaxLength()).isEqualTo(100); + assertThat(var1Validation.getRegularExpression()).isEqualTo("[a-z].*"); + + } + +} diff --git a/sechub-commons-model/src/test/resources/template/template-definition-example1.json b/sechub-commons-model/src/test/resources/template/template-definition-example1.json new file mode 100644 index 0000000000..d07f391a88 --- /dev/null +++ b/sechub-commons-model/src/test/resources/template/template-definition-example1.json @@ -0,0 +1,14 @@ +{ + "type" : "webscan-login", + "assetId" : "asset0815", + "variables" : [ { + "name" : "variable1", + "optional" : true, + "validation" : { + "minLength" : 1, + "maxLength" : 100, + "regularExpression" : "[a-z].*" + } + } ], + "id" : "identifier" +} diff --git a/sechub-commons-pds/src/main/java/com/mercedesbenz/sechub/commons/pds/AbstractPDSKey.java b/sechub-commons-pds/src/main/java/com/mercedesbenz/sechub/commons/pds/AbstractPDSKey.java index 8fbaa33bd7..417e1735a8 100644 --- a/sechub-commons-pds/src/main/java/com/mercedesbenz/sechub/commons/pds/AbstractPDSKey.java +++ b/sechub-commons-pds/src/main/java/com/mercedesbenz/sechub/commons/pds/AbstractPDSKey.java @@ -61,7 +61,7 @@ public T markMandatory() { return (T) this; } - /* + /** * Mark this key as generated, means it will be automatically created and sent * on PDS calls */ diff --git a/sechub-commons-pds/src/main/java/com/mercedesbenz/sechub/commons/pds/PDSConfigDataKeyProvider.java b/sechub-commons-pds/src/main/java/com/mercedesbenz/sechub/commons/pds/PDSConfigDataKeyProvider.java index acfbc84ac7..77d7e9fc3d 100644 --- a/sechub-commons-pds/src/main/java/com/mercedesbenz/sechub/commons/pds/PDSConfigDataKeyProvider.java +++ b/sechub-commons-pds/src/main/java/com/mercedesbenz/sechub/commons/pds/PDSConfigDataKeyProvider.java @@ -188,7 +188,13 @@ public enum PDSConfigDataKeyProvider implements PDSKeyProvider */ PDS_MOCKING_DISABLED(new ExecutionPDSKey(PDSDefaultParameterKeyConstants.PARAM_KEY_PDS_MOCKING_DISABLED, "When 'true' any PDS adapter call will use real PDS adapter and not a mocked variant.").markForTestingOnly().markSendToPDS() - .markDefaultRecommended().withDefault(true)); + .markDefaultRecommended().withDefault(true)), + + PDS_CONFIG_TEMPLATE_META_DATA_LIST(new ExecutionPDSKey(PDSDefaultParameterKeyConstants.PARAM_KEY_PDS_CONFIG_TEMPLATE_META_DATA_LIST, """ + Contains a list of template meta data entries (json). + """).markGenerated()), + + ; private ExecutionPDSKey key; diff --git a/sechub-commons-pds/src/main/java/com/mercedesbenz/sechub/commons/pds/PDSDefaultParameterKeyConstants.java b/sechub-commons-pds/src/main/java/com/mercedesbenz/sechub/commons/pds/PDSDefaultParameterKeyConstants.java index 310c9382d6..62981afb4e 100644 --- a/sechub-commons-pds/src/main/java/com/mercedesbenz/sechub/commons/pds/PDSDefaultParameterKeyConstants.java +++ b/sechub-commons-pds/src/main/java/com/mercedesbenz/sechub/commons/pds/PDSDefaultParameterKeyConstants.java @@ -6,7 +6,7 @@ /** * All default parameter keys supported by PDS. A PDS can support optional * parameters (via its configuration) but these ones are always supported and be - * available at runtime inside PDS scripts.
+ * available at runtime inside PDS.
*
* * Wrappers can use these constants as spring boot values. @@ -78,6 +78,8 @@ public class PDSDefaultParameterKeyConstants { public static final String PARAM_KEY_PDS_CONFIG_JOBSTORAGE_READ_RESILIENCE_RETRY_WAIT_SECONDS = "pds.config.jobstorage.read.resilience.retry.wait.seconds"; + public static final String PARAM_KEY_PDS_CONFIG_TEMPLATE_META_DATA_LIST = "pds.config.template.metadata.list"; + /* ---------------------- */ /* Integration tests only */ /* ---------------------- */ diff --git a/sechub-commons-pds/src/main/java/com/mercedesbenz/sechub/commons/pds/PDSDefaultRuntimeKeyConstants.java b/sechub-commons-pds/src/main/java/com/mercedesbenz/sechub/commons/pds/PDSDefaultRuntimeKeyConstants.java index f45b60e0e1..44fcd89635 100644 --- a/sechub-commons-pds/src/main/java/com/mercedesbenz/sechub/commons/pds/PDSDefaultRuntimeKeyConstants.java +++ b/sechub-commons-pds/src/main/java/com/mercedesbenz/sechub/commons/pds/PDSDefaultRuntimeKeyConstants.java @@ -30,6 +30,8 @@ public class PDSDefaultRuntimeKeyConstants { public static final String RT_KEY_PDS_JOB_BINARIES_TAR_FILE = "pds.job.binaries.tar.file"; + public static final String RT_KEY_PDS_JOB_EXTRACTED_ASSETS_FOLDER = "pds.job.extracted.assets.folder"; + public static final String RT_KEY_PDS_JOB_EXTRACTED_SOURCES_FOLDER = "pds.job.extracted.sources.folder"; public static final String RT_KEY_PDS_JOB_HAS_EXTRACTED_BINARIES = "pds.job.has.extracted.binaries"; diff --git a/sechub-commons-pds/src/main/java/com/mercedesbenz/sechub/commons/pds/PDSLauncherScriptEnvironmentConstants.java b/sechub-commons-pds/src/main/java/com/mercedesbenz/sechub/commons/pds/PDSLauncherScriptEnvironmentConstants.java index cb8b2aa650..cf2232f0ea 100644 --- a/sechub-commons-pds/src/main/java/com/mercedesbenz/sechub/commons/pds/PDSLauncherScriptEnvironmentConstants.java +++ b/sechub-commons-pds/src/main/java/com/mercedesbenz/sechub/commons/pds/PDSLauncherScriptEnvironmentConstants.java @@ -27,6 +27,7 @@ public class PDSLauncherScriptEnvironmentConstants { @Deprecated public static final String PDS_JOB_SOURCECODE_UNZIPPED_FOLDER = "PDS_JOB_SOURCECODE_UNZIPPED_FOLDER"; + public static final String PDS_JOB_EXTRACTED_ASSETS_FOLDER = "PDS_JOB_EXTRACTED_ASSETS_FOLDER"; public static final String PDS_JOB_EXTRACTED_SOURCES_FOLDER = "PDS_JOB_EXTRACTED_SOURCES_FOLDER"; public static final String PDS_JOB_HAS_EXTRACTED_SOURCES = "PDS_JOB_HAS_EXTRACTED_SOURCES"; public static final String PDS_JOB_SOURCECODE_ZIP_FILE = "PDS_JOB_SOURCECODE_ZIP_FILE"; diff --git a/sechub-commons-pds/src/main/java/com/mercedesbenz/sechub/commons/pds/data/PDSTemplateMetaData.java b/sechub-commons-pds/src/main/java/com/mercedesbenz/sechub/commons/pds/data/PDSTemplateMetaData.java new file mode 100644 index 0000000000..8d1712ff90 --- /dev/null +++ b/sechub-commons-pds/src/main/java/com/mercedesbenz/sechub/commons/pds/data/PDSTemplateMetaData.java @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.pds.data; + +import java.util.Objects; + +import com.mercedesbenz.sechub.commons.model.template.TemplateType; + +public class PDSTemplateMetaData { + + private String templateId; + private TemplateType templateType; + private PDSAssetData assetData; + + public PDSAssetData getAssetData() { + return assetData; + } + + public String getTemplateId() { + return templateId; + } + + public TemplateType getTemplateType() { + return templateType; + } + + public void setTemplateType(TemplateType type) { + this.templateType = type; + } + + public void setTemplateId(String templateId) { + this.templateId = templateId; + } + + public void setAssetData(PDSAssetData assetData) { + this.assetData = assetData; + } + + @Override + public int hashCode() { + return Objects.hash(assetData, templateId, templateType); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + PDSTemplateMetaData other = (PDSTemplateMetaData) obj; + return Objects.equals(assetData, other.assetData) && Objects.equals(templateId, other.templateId) && templateType == other.templateType; + } + + @Override + public String toString() { + return "PDSTemplateMetaData [" + (templateId != null ? "template=" + templateId + ", " : "") + + (templateType != null ? "type=" + templateType + ", " : "") + (assetData != null ? "assetData=" + assetData : "") + "]"; + } + + public static class PDSAssetData { + private String assetId; + private String fileName; + private String checksum; + + public String getAssetId() { + return assetId; + } + + public void setAssetId(String assetId) { + this.assetId = assetId; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public String getChecksum() { + return checksum; + } + + public void setChecksum(String checksum) { + this.checksum = checksum; + } + + @Override + public int hashCode() { + return Objects.hash(assetId, checksum, fileName); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + PDSAssetData other = (PDSAssetData) obj; + return Objects.equals(assetId, other.assetId) && Objects.equals(checksum, other.checksum) && Objects.equals(fileName, other.fileName); + } + + @Override + public String toString() { + return "PDSAssetData [" + (assetId != null ? "assetId=" + assetId + ", " : "") + (fileName != null ? "fileName=" + fileName + ", " : "") + + (checksum != null ? "checksum=" + checksum : "") + "]"; + } + + } +} diff --git a/sechub-commons-pds/src/test/java/com/mercedesbenz/sechub/commons/pds/data/PDSTemplateMetaDataTest.java b/sechub-commons-pds/src/test/java/com/mercedesbenz/sechub/commons/pds/data/PDSTemplateMetaDataTest.java new file mode 100644 index 0000000000..c752a997f7 --- /dev/null +++ b/sechub-commons-pds/src/test/java/com/mercedesbenz/sechub/commons/pds/data/PDSTemplateMetaDataTest.java @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.pds.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import com.mercedesbenz.sechub.commons.model.JSONConverter; +import com.mercedesbenz.sechub.commons.pds.data.PDSTemplateMetaData.PDSAssetData; +import com.mercedesbenz.sechub.test.TestFileReader; + +class PDSTemplateMetaDataTest { + + @ParameterizedTest + @ValueSource(strings = { "pds-param-template-metadata-example1.json", "pds-param-template-metadata-syntax.json" }) + void examples_in_doc_are_valid(String fileName) { + /* prepare */ + String json = TestFileReader.readTextFromFile("./../sechub-doc/src/docs/asciidoc/documents/shared/snippet/" + fileName); + + /* execute */ + List result = JSONConverter.get().fromJSONtoListOf(PDSTemplateMetaData.class, json); + + /* test */ + assertEquals(1, result.size()); + PDSTemplateMetaData entry = result.iterator().next(); + assertNotNull(entry.getTemplateId()); + PDSAssetData assetData = entry.getAssetData(); + assertNotNull(assetData); + assertNotNull(assetData.getAssetId()); + assertNotNull(assetData.getChecksum()); + assertNotNull(assetData.getFileName()); + } + +} diff --git a/sechub-commons-security-spring/build.gradle b/sechub-commons-security-spring/build.gradle new file mode 100644 index 0000000000..792807d703 --- /dev/null +++ b/sechub-commons-security-spring/build.gradle @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +/*============================================================================ +* Build file for subproject +* +* Root build file: "${rootProject.projectDir}/build.gradle" +* ============================================================================ +*/ +dependencies { + + implementation project(':sechub-commons-core') + implementation project(':sechub-testframework-spring') + implementation library.springboot_starter_security + implementation library.springboot_starter_oauth2_resource_server + + testImplementation library.springframework_web + testImplementation library.springframework_webmvc + testImplementation library.springboot_test_autoconfigure + testImplementation library.springboot_starter_test + testImplementation library.springframework_security_test + testImplementation library.jakarta_servlet_api +} diff --git a/sechub-commons-security-spring/src/main/java/com/mercedesbenz/sechub/spring/security/AbstractSecurityConfiguration.java b/sechub-commons-security-spring/src/main/java/com/mercedesbenz/sechub/spring/security/AbstractSecurityConfiguration.java new file mode 100644 index 0000000000..05b5f89349 --- /dev/null +++ b/sechub-commons-security-spring/src/main/java/com/mercedesbenz/sechub/spring/security/AbstractSecurityConfiguration.java @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.spring.security; + +import org.springframework.beans.BeanInstantiationException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; +import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; +import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.client.RestTemplate; + +/** + * Abstract class that provides a base configuration for securing stateless web + * applications inside the SecHub project using Spring Security. + * + *

+ * Using this class will enable Basic Authentication by username and password. + *

+ * + *

+ * This configuration also supports OAuth2-based authentication, either using + * JWT tokens or opaque tokens. It integrates with the + * {@link UserDetailsService} to provide user details and roles for + * authorization. + *

+ * + *

+ * To enable OAuth2 authentication in JWT mode, you must set the following + * property {@code sechub.security.oauth2.jwt.enabled=true} in the application + * properties. For opaque token mode, set + * {@code sechub.security.oauth2.opaque-token.enabled=true}. Note: This + * configuration requires exactly one of the two modes to be enabled. + *

+ * + *

+ * Subclasses must implement the {@link #isOAuth2Enabled()} method to indicate + * whether OAuth2 is enabled and the {@link #authorizeHttpRequests()} method to + * configure API access permissions. + *

+ * + * @see org.springframework.security.config.annotation.web.builders.HttpSecurity + * @see org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer + * @see org.springframework.security.oauth2.jwt.JwtDecoder + * @see org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector + * @see org.springframework.security.oauth2.server.resource.web.BearerTokenResolver + * @see org.springframework.security.web.SecurityFilterChain + * + * @author hamidonos + */ +public abstract class AbstractSecurityConfiguration { + + static final String OAUTH2_PROPERTIES_PREFIX = "sechub.security.oauth2";; + static final String OAUTH2_PROPERTIES_MODE = "mode"; + + /* @formatter:off */ + @Bean + SecurityFilterChain filterChain(HttpSecurity httpSecurity, + @Autowired(required = false) UserDetailsService userDetailsService, + RestTemplate restTemplate, + @Autowired(required = false) OAuth2JwtProperties oAuth2JwtProperties, + @Autowired(required = false) OAuth2OpaqueTokenProperties oAuth2OpaqueTokenProperties, + @Autowired(required = false) JwtDecoder jwtDecoder) throws Exception { + + httpSecurity.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(authorizeHttpRequests()) + .csrf(AbstractHttpConfigurer::disable) // CSRF protection disabled. The CookieServerCsrfTokenRepository does + // not work since Spring Boot 3 + .httpBasic(Customizer.withDefaults()).headers((headers) -> headers + .contentSecurityPolicy((csp) -> csp.policyDirectives("default-src 'none'; style-src 'unsafe-inline'"))); + + if (isOAuth2Enabled()) { + if (userDetailsService == null) { + throw new NoSuchBeanDefinitionException(UserDetailsService.class); + } + + if ((oAuth2JwtProperties == null && oAuth2OpaqueTokenProperties == null) || (oAuth2JwtProperties != null && oAuth2OpaqueTokenProperties != null)) { + String exMsg = "Either JWT or opaque token mode must be enabled by setting the '%s.%s' property to either '%s' or '%s'".formatted( + OAUTH2_PROPERTIES_PREFIX, + OAUTH2_PROPERTIES_MODE, + OAuth2JwtPropertiesConfiguration.MODE, + OAuth2OpaqueTokenPropertiesConfiguration.MODE + ); + + throw new BeanInstantiationException(SecurityFilterChain.class, exMsg); + } + + if (oAuth2JwtProperties != null) { + if (jwtDecoder == null) { + throw new NoSuchBeanDefinitionException(JwtDecoder.class); + } + BearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver(); + AuthenticationProvider authenticationProvider = new OAuth2JwtAuthenticationProvider(userDetailsService, jwtDecoder); + + httpSecurity + .oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer + .jwt(jwt -> jwt.decoder(jwtDecoder)) + .bearerTokenResolver(bearerTokenResolver)) + .authenticationProvider(authenticationProvider); + } + + if (oAuth2OpaqueTokenProperties != null) { + OpaqueTokenIntrospector opaqueTokenIntrospector = new Base64OAuth2OpaqueTokenIntrospector( + restTemplate, + oAuth2OpaqueTokenProperties.getIntrospectionUri(), + oAuth2OpaqueTokenProperties.getClientId(), + oAuth2OpaqueTokenProperties.getClientSecret(), + userDetailsService); + + httpSecurity + .oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer + .opaqueToken(opaqueToken -> opaqueToken.introspector(opaqueTokenIntrospector))); + } + } + /* @formatter:on */ + + return httpSecurity.build(); + } + + protected abstract boolean isOAuth2Enabled(); + + protected abstract Customizer.AuthorizationManagerRequestMatcherRegistry> authorizeHttpRequests(); + +} diff --git a/sechub-commons-security-spring/src/main/java/com/mercedesbenz/sechub/spring/security/Base64OAuth2OpaqueTokenIntrospector.java b/sechub-commons-security-spring/src/main/java/com/mercedesbenz/sechub/spring/security/Base64OAuth2OpaqueTokenIntrospector.java new file mode 100644 index 0000000000..5c18d1b23a --- /dev/null +++ b/sechub-commons-security-spring/src/main/java/com/mercedesbenz/sechub/spring/security/Base64OAuth2OpaqueTokenIntrospector.java @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.spring.security; + +import static java.util.Objects.requireNonNull; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import javax.crypto.SealedObject; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames; +import org.springframework.security.oauth2.server.resource.introspection.BadOpaqueTokenException; +import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionAuthenticatedPrincipal; +import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.server.ResponseStatusException; + +import com.mercedesbenz.sechub.commons.core.security.CryptoAccess; + +/** + *

+ * The Base64OAuth2OpaqueTokenIntrospector class is responsible for + * introspecting opaque OAuth2 tokens. It sends the opaque token to an + * introspection endpoint and retrieves the token's details. + *

+ * + *

+ * This class integrates with the {@link UserDetailsService} to load user + * details based on the token's subject. The user details are then used to + * create an {@link OAuth2IntrospectionAuthenticatedPrincipal} which contains + * the authenticated principal's information and authorities. + *

+ * + *

+ * The class also handles the encoding of client credentials using Base64 and + * includes them in the authorization header of the introspection request. + *

+ * + * @see org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector + * @see org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal + * @see org.springframework.security.core.userdetails.UserDetailsService + * @see org.springframework.web.client.RestTemplate + * + * @author hamidonos + */ +class Base64OAuth2OpaqueTokenIntrospector implements OpaqueTokenIntrospector { + + private static final Logger LOG = LoggerFactory.getLogger(Base64OAuth2OpaqueTokenIntrospector.class); + private static final CryptoAccess CRYPTO_STRING = CryptoAccess.CRYPTO_STRING; + private static final String TOKEN = "token"; + private static final String BASIC_AUTHORIZATION_HEADER_VALUE_FORMAT = "Basic %s"; + private static final String CLIENT_ID_CLIENT_SECRET_FORMAT = "%s:%s"; + private static final String TOKEN_TYPE_HINT = "token_type_hint"; + private static final String ACCESS_TOKEN = "access_token"; + private static final String TOKEN_TYPE_HINT_VALUE = ACCESS_TOKEN; + private static final int DEFAULT_EXPIRES_IN_SECONDS = 60 * 60 * 24; /* 1 day */ + + private final RestTemplate restTemplate; + private final String introspectionUri; + private final SealedObject clientIdSealed; + private final SealedObject clientSecretSealed; + private final UserDetailsService userDetailsService; + + /* @formatter:off */ + Base64OAuth2OpaqueTokenIntrospector(RestTemplate restTemplate, + String introspectionUri, + String clientId, + String clientSecret, + UserDetailsService userDetailsService) { + this.restTemplate = requireNonNull(restTemplate, "Parameter restTemplate must not be null"); + this.introspectionUri = requireNonNull(introspectionUri, "Parameter introspectionUri must not be null"); + this.clientIdSealed = CRYPTO_STRING.seal(requireNonNull(clientId, "Parameter clientId must not be null")); + this.clientSecretSealed = CRYPTO_STRING.seal(requireNonNull(clientSecret, "Parameter clientSecret must not be null")); + this.userDetailsService = requireNonNull(userDetailsService, "Parameter userDetailsService must not be null"); + } + /* @formatter:on */ + + @Override + public OAuth2AuthenticatedPrincipal introspect(String opaqueToken) throws OAuth2AuthenticationException { + if (opaqueToken == null || opaqueToken.isEmpty()) { + throw new BadOpaqueTokenException("Token is null or empty"); + } + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set(HttpHeaders.AUTHORIZATION, getBasicAuthHeaderValue()); + HttpEntity> entity = getRequestParameters(opaqueToken, headers); + + OpaqueTokenResponse opaqueTokenResponse; + try { + opaqueTokenResponse = restTemplate.postForObject(introspectionUri, entity, OpaqueTokenResponse.class); + + if (opaqueTokenResponse == null) { + throw new RestClientException("Response is null"); + } + } catch (RestClientException e) { + String errMsg = "Failed to perform token introspection"; + LOG.error(errMsg, e); + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, errMsg, e); + } + + Instant now = Instant.now(); + + if (!opaqueTokenResponse.isActive()) { + throw new BadOpaqueTokenException("Token is not active"); + } + + String subject = opaqueTokenResponse.getSubject(); + + if (subject == null || subject.isEmpty()) { + throw new BadOpaqueTokenException("Subject is null"); + } + + Map introspectionClaims = getIntrospectionClaims(now, opaqueTokenResponse); + UserDetails userDetails = userDetailsService.loadUserByUsername(subject); + Collection authorities = new ArrayList<>(userDetails.getAuthorities()); + return new OAuth2IntrospectionAuthenticatedPrincipal(subject, introspectionClaims, authorities); + } + + private static HttpEntity> getRequestParameters(String opaqueToken, HttpHeaders headers) { + MultiValueMap formParameters = new LinkedMultiValueMap<>(); + formParameters.add(TOKEN, opaqueToken); + formParameters.add(TOKEN_TYPE_HINT, TOKEN_TYPE_HINT_VALUE); + return new HttpEntity<>(formParameters, headers); + } + + private String getBasicAuthHeaderValue() { + String clientId = CRYPTO_STRING.unseal(clientIdSealed); + String clientSecret = CRYPTO_STRING.unseal(clientSecretSealed); + String clientIdClientSecret = CLIENT_ID_CLIENT_SECRET_FORMAT.formatted(clientId, clientSecret); + String clientIdClientSecretB64Encoded = Base64.getEncoder().encodeToString(clientIdClientSecret.getBytes()); + return BASIC_AUTHORIZATION_HEADER_VALUE_FORMAT.formatted(clientIdClientSecretB64Encoded); + } + + /* @formatter:off */ + private static Map getIntrospectionClaims(Instant issuedAt, OpaqueTokenResponse opaqueTokenResponse) { + Map map = new HashMap<>(); + map.put(OAuth2TokenIntrospectionClaimNames.ACTIVE, opaqueTokenResponse.isActive()); + map.put(OAuth2TokenIntrospectionClaimNames.SCOPE, opaqueTokenResponse.getScope()); + map.put(OAuth2TokenIntrospectionClaimNames.CLIENT_ID, opaqueTokenResponse.getClientId()); + map.put(OAuth2TokenIntrospectionClaimNames.USERNAME, opaqueTokenResponse.getUsername()); + map.put(OAuth2TokenIntrospectionClaimNames.TOKEN_TYPE, opaqueTokenResponse.getTokenType()); + map.put(OAuth2TokenIntrospectionClaimNames.IAT, issuedAt); + Instant expiresAt = opaqueTokenResponse.getExpiresAt(); + map.put(OAuth2TokenIntrospectionClaimNames.EXP, expiresAt == null ? issuedAt.plusSeconds(DEFAULT_EXPIRES_IN_SECONDS) : expiresAt); + map.put(OAuth2TokenIntrospectionClaimNames.SUB, opaqueTokenResponse.getSubject()); + map.put(OAuth2TokenIntrospectionClaimNames.AUD, opaqueTokenResponse.getAudience()); + return map; + } + /* @formatter:on */ +} \ No newline at end of file diff --git a/sechub-commons-security-spring/src/main/java/com/mercedesbenz/sechub/spring/security/JwtDecoderConfiguration.java b/sechub-commons-security-spring/src/main/java/com/mercedesbenz/sechub/spring/security/JwtDecoderConfiguration.java new file mode 100644 index 0000000000..5e57453e15 --- /dev/null +++ b/sechub-commons-security-spring/src/main/java/com/mercedesbenz/sechub/spring/security/JwtDecoderConfiguration.java @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.spring.security; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; + +@Configuration +@ConditionalOnProperty(value = "sechub.security.oauth2.token-type", havingValue = "JWT") +class JwtDecoderConfiguration { + + @Bean + JwtDecoder jwtDecoder(OAuth2JwtProperties oAuth2JwtProperties) { + /* + * @formatter:off + * The `NimbusJwtDecoder` is a `JwtDecoder` implementation that utilizes the Nimbus JOSE + JWT library to decode JSON Web Tokens (JWTs). + * It requires a JWK Set URI to fetch the public keys from the Identity Provider (IDP) to verify the JWT's signature. + */ + return NimbusJwtDecoder + .withJwkSetUri(oAuth2JwtProperties.getJwkSetUri()) + .build(); + /* @formatter:on */ + } +} diff --git a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/security/OAuth2AuthenticationProvider.java b/sechub-commons-security-spring/src/main/java/com/mercedesbenz/sechub/spring/security/OAuth2JwtAuthenticationProvider.java similarity index 75% rename from sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/security/OAuth2AuthenticationProvider.java rename to sechub-commons-security-spring/src/main/java/com/mercedesbenz/sechub/spring/security/OAuth2JwtAuthenticationProvider.java index 452f784565..dce0aa5016 100644 --- a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/security/OAuth2AuthenticationProvider.java +++ b/sechub-commons-security-spring/src/main/java/com/mercedesbenz/sechub/spring/security/OAuth2JwtAuthenticationProvider.java @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -package com.mercedesbenz.sechub.sharedkernel.security; +package com.mercedesbenz.sechub.spring.security; import static java.util.Objects.requireNonNull; @@ -16,12 +16,10 @@ /** *

- * This class integrates authentication and authorization in SecHub by combining - * OAuth2-based authentication with custom - * {@link com.mercedesbenz.sechub.domain.authorization.AuthUserDetailsService} - * for authorization. While OAuth2 manages the authentication process, our - * system fetches roles and permissions from the database to handle - * authorization. + * This class integrates security in SecHub by combining OAuth2-based + * authentication with authorization from the {@link UserDetailsService}. While + * OAuth2 manages the authentication process, the user details service is + * responsible for providing application specific user details and authorities. *

* *

@@ -34,22 +32,20 @@ * which encapsulates information about the authenticated user and their roles. *

* - * @see com.mercedesbenz.sechub.domain.authorization.AuthUserDetailsService * @see org.springframework.security.oauth2.jwt.JwtDecoder * @see org.springframework.security.core.userdetails.UserDetailsService * @see org.springframework.security.authentication.AuthenticationProvider * * @author hamidonos */ -@SuppressWarnings("JavadocReference") -class OAuth2AuthenticationProvider implements AuthenticationProvider { +public class OAuth2JwtAuthenticationProvider implements AuthenticationProvider { private final UserDetailsService userDetailsService; private final JwtDecoder jwtDecoder; - public OAuth2AuthenticationProvider(UserDetailsService userDetailsService, JwtDecoder jwtDecoder) { - this.userDetailsService = requireNonNull(userDetailsService, "Property userDetailsService must not be null"); - this.jwtDecoder = requireNonNull(jwtDecoder, "Property jwtDecoder must not be null"); + public OAuth2JwtAuthenticationProvider(UserDetailsService userDetailsService, JwtDecoder jwtDecoder) { + this.userDetailsService = requireNonNull(userDetailsService, "Parameter userDetailsService must not be null"); + this.jwtDecoder = requireNonNull(jwtDecoder, "Parameter jwtDecoder must not be null"); } @Override diff --git a/sechub-commons-security-spring/src/main/java/com/mercedesbenz/sechub/spring/security/OAuth2JwtProperties.java b/sechub-commons-security-spring/src/main/java/com/mercedesbenz/sechub/spring/security/OAuth2JwtProperties.java new file mode 100644 index 0000000000..64b021b63b --- /dev/null +++ b/sechub-commons-security-spring/src/main/java/com/mercedesbenz/sechub/spring/security/OAuth2JwtProperties.java @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.spring.security; + +import static java.util.Objects.requireNonNull; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(OAuth2JwtProperties.PREFIX) +public class OAuth2JwtProperties { + + static final String PREFIX = "sechub.security.oauth2.jwt"; + private final String jwkSetUri; + + OAuth2JwtProperties(String jwkSetUri) { + this.jwkSetUri = requireNonNull(jwkSetUri, "Property '%s.%s' must not be null".formatted(PREFIX, "jwk-set-uri")); + } + + public String getJwkSetUri() { + return jwkSetUri; + } +} \ No newline at end of file diff --git a/sechub-commons-security-spring/src/main/java/com/mercedesbenz/sechub/spring/security/OAuth2JwtPropertiesConfiguration.java b/sechub-commons-security-spring/src/main/java/com/mercedesbenz/sechub/spring/security/OAuth2JwtPropertiesConfiguration.java new file mode 100644 index 0000000000..45845db7aa --- /dev/null +++ b/sechub-commons-security-spring/src/main/java/com/mercedesbenz/sechub/spring/security/OAuth2JwtPropertiesConfiguration.java @@ -0,0 +1,12 @@ +package com.mercedesbenz.sechub.spring.security; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties(OAuth2JwtProperties.class) +@ConditionalOnProperty(prefix = AbstractSecurityConfiguration.OAUTH2_PROPERTIES_PREFIX, name = AbstractSecurityConfiguration.OAUTH2_PROPERTIES_MODE, havingValue = OAuth2JwtPropertiesConfiguration.MODE) +class OAuth2JwtPropertiesConfiguration { + static final String MODE = "JWT"; +} diff --git a/sechub-commons-security-spring/src/main/java/com/mercedesbenz/sechub/spring/security/OAuth2OpaqueTokenProperties.java b/sechub-commons-security-spring/src/main/java/com/mercedesbenz/sechub/spring/security/OAuth2OpaqueTokenProperties.java new file mode 100644 index 0000000000..8775bff51f --- /dev/null +++ b/sechub-commons-security-spring/src/main/java/com/mercedesbenz/sechub/spring/security/OAuth2OpaqueTokenProperties.java @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.spring.security; + +import static java.util.Objects.requireNonNull; + +import javax.crypto.SealedObject; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import com.mercedesbenz.sechub.commons.core.security.CryptoAccess; + +@ConfigurationProperties(OAuth2OpaqueTokenProperties.PREFIX) +public class OAuth2OpaqueTokenProperties { + + static final String PREFIX = "sechub.security.oauth2.opaque-token"; + private static final String ERR_MSG_FORMAT = "Property '%s.%s' must not be null"; + private static final CryptoAccess CRYPTO_STRING = CryptoAccess.CRYPTO_STRING; + + private final String introspectionUri; + private final SealedObject clientIdSealed; + private final SealedObject clientSecretSealed; + + /* @formatter:off */ + OAuth2OpaqueTokenProperties(String introspectionUri, + String clientId, + String clientSecret) { + this.introspectionUri = requireNonNull(introspectionUri, ERR_MSG_FORMAT.formatted(PREFIX, "introspection-uri")); + this.clientIdSealed = CRYPTO_STRING.seal(requireNonNull(clientId, ERR_MSG_FORMAT.formatted(PREFIX, "client-id"))); + this.clientSecretSealed = CRYPTO_STRING.seal(requireNonNull(clientSecret, ERR_MSG_FORMAT.formatted(PREFIX, "client-secret"))); + } + /* @formatter:on */ + + public String getIntrospectionUri() { + return introspectionUri; + } + + public String getClientId() { + return CRYPTO_STRING.unseal(clientIdSealed); + } + + public String getClientSecret() { + return CRYPTO_STRING.unseal(clientSecretSealed); + } +} \ No newline at end of file diff --git a/sechub-commons-security-spring/src/main/java/com/mercedesbenz/sechub/spring/security/OAuth2OpaqueTokenPropertiesConfiguration.java b/sechub-commons-security-spring/src/main/java/com/mercedesbenz/sechub/spring/security/OAuth2OpaqueTokenPropertiesConfiguration.java new file mode 100644 index 0000000000..bc88b467e0 --- /dev/null +++ b/sechub-commons-security-spring/src/main/java/com/mercedesbenz/sechub/spring/security/OAuth2OpaqueTokenPropertiesConfiguration.java @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.spring.security; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties(OAuth2OpaqueTokenProperties.class) +@ConditionalOnProperty(prefix = AbstractSecurityConfiguration.OAUTH2_PROPERTIES_PREFIX, name = AbstractSecurityConfiguration.OAUTH2_PROPERTIES_MODE, havingValue = OAuth2OpaqueTokenPropertiesConfiguration.MODE) +class OAuth2OpaqueTokenPropertiesConfiguration { + static final String MODE = "OPAQUE_TOKEN"; +} \ No newline at end of file diff --git a/sechub-commons-security-spring/src/main/java/com/mercedesbenz/sechub/spring/security/OpaqueTokenResponse.java b/sechub-commons-security-spring/src/main/java/com/mercedesbenz/sechub/spring/security/OpaqueTokenResponse.java new file mode 100644 index 0000000000..4311ac6d6c --- /dev/null +++ b/sechub-commons-security-spring/src/main/java/com/mercedesbenz/sechub/spring/security/OpaqueTokenResponse.java @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.spring.security; + +import static java.util.Objects.requireNonNull; + +import java.time.Instant; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * OpaqueTokenResponse represents the response from the OAuth2 + * opaque token introspection endpoint. It contains various properties related + * to the token, such as its active status, scope, client ID, client type, + * username, token type, expiration time, subject, audience, and group type. + * + *

+ * The active property is required and indicates whether the token + * is active or not. An inactive token should be treated as + * 401 Unauthorized. For active tokens the sub + * property is also required. It represents the subject of the authentication + * request. All other properties are optional and may be null. + *

+ * + * @author hamidonos + */ +class OpaqueTokenResponse { + + private static final String ERR_MSG_FORMAT = "Property '%s' must not be null"; + + private static final String JSON_PROPERTY_ACTIVE = "active"; + private static final String JSON_PROPERTY_SCOPE = "scope"; + private static final String JSON_PROPERTY_CLIENT_ID = "client_id"; + private static final String JSON_PROPERTY_CLIENT_TYPE = "client_type"; + private static final String JSON_PROPERTY_USERNAME = "username"; + private static final String JSON_PROPERTY_TOKEN_TYPE = "token_type"; + private static final String JSON_PROPERTY_EXP = "exp"; + private static final String JSON_PROPERTY_SUBJECT = "sub"; + private static final String JSON_PROPERTY_AUDIENCE = "aud"; + private static final String JSON_PROPERTY_GROUP_TYPE = "group_type"; + + private final Boolean active; + private final String scope; + private final String clientId; + private final String clientType; + private final String username; + private final String tokenType; + private final Instant expiresAt; + private final String subject; + private final String audience; + private final String groupType; + + /* @formatter:off */ + @JsonCreator + OpaqueTokenResponse(@JsonProperty(JSON_PROPERTY_ACTIVE) Boolean active, + @JsonProperty(JSON_PROPERTY_SCOPE) String scope, + @JsonProperty(JSON_PROPERTY_CLIENT_ID) String clientId, + @JsonProperty(JSON_PROPERTY_CLIENT_TYPE) String clientType, + @JsonProperty(JSON_PROPERTY_USERNAME) String username, + @JsonProperty(JSON_PROPERTY_TOKEN_TYPE) String tokenType, + @JsonProperty(JSON_PROPERTY_EXP) Long expiresAt, + @JsonProperty(JSON_PROPERTY_SUBJECT) String subject, + @JsonProperty(JSON_PROPERTY_AUDIENCE) String audience, + @JsonProperty(JSON_PROPERTY_GROUP_TYPE) String groupType) { + this.active = requireNonNull(active, ERR_MSG_FORMAT.formatted(JSON_PROPERTY_ACTIVE)); + this.scope = scope; + this.clientId = clientId; + this.clientType = clientType; + this.username = username; + this.tokenType = tokenType; + this.expiresAt = expiresAt != null ? Instant.ofEpochSecond(expiresAt) : null; + this.subject = active ? requireNonNull(subject, ERR_MSG_FORMAT.formatted(JSON_PROPERTY_SUBJECT)) : subject; + this.audience = audience; + this.groupType = groupType; + } + /* @formatter:on */ + + boolean isActive() { + return active; + } + + String getScope() { + return scope; + } + + String getClientId() { + return clientId; + } + + String getClientType() { + return clientType; + } + + String getUsername() { + return username; + } + + String getTokenType() { + return tokenType; + } + + Instant getExpiresAt() { + return expiresAt; + } + + String getSubject() { + return subject; + } + + String getAudience() { + return audience; + } + + String getGroupType() { + return groupType; + } +} diff --git a/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/Base64OAuth2OpaqueTokenIntrospectorTest.java b/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/Base64OAuth2OpaqueTokenIntrospectorTest.java new file mode 100644 index 0000000000..b95aac142d --- /dev/null +++ b/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/Base64OAuth2OpaqueTokenIntrospectorTest.java @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.spring.security; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; +import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames; +import org.springframework.security.oauth2.server.resource.introspection.BadOpaqueTokenException; +import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.server.ResponseStatusException; + +class Base64OAuth2OpaqueTokenIntrospectorTest { + + private static final String INTROSPECTION_URI = "https://example.org/introspection-uri"; + private static final RestTemplate restTemplate = mock(); + private static final String CLIENT_ID = "example-client-id"; + private static final String CLIENT_SECRET = "example-client-secret"; + private static final UserDetailsService userDetailsService = mock(); + private static final OpaqueTokenIntrospector introspectorToTest = new Base64OAuth2OpaqueTokenIntrospector(restTemplate, INTROSPECTION_URI, CLIENT_ID, + CLIENT_SECRET, userDetailsService); + private static final String OPAQUE_TOKEN = "opaque-token"; + private static final String SUBJECT = "sub"; + private static final int DEFAULT_EXPIRES_IN_SECONDS = 60 * 60 * 24; + + @BeforeEach + void beforeEach() { + reset(restTemplate, userDetailsService); + } + + /* @formatter:off */ + @ParameterizedTest + @ArgumentsSource(Base64OAuth2OpaqueTokenIntrospectorSingleNullArgumentProvider.class) + void construct_base64_o_auth_2_opaque_token_introspector_with_null_argument_fails(RestTemplate restTemplate, + String introspectionUri, + String clientId, + String clientSecret, + UserDetailsService userDetailsService, + String errMsg) { + assertThatThrownBy(() -> new Base64OAuth2OpaqueTokenIntrospector(restTemplate, introspectionUri, clientId, clientSecret, userDetailsService)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining(errMsg); + } + /* @formatter:on */ + + @ParameterizedTest + @NullAndEmptySource + void introspect_with_null_or_empty_opaque_token_fails(String opaqueToken) { + /* @formatter:off */ + assertThatThrownBy(() -> introspectorToTest.introspect(opaqueToken)) + .isInstanceOf(BadOpaqueTokenException.class) + .hasMessageContaining("Token is null or empty"); + /* @formatter:on */ + } + + @Test + void introspect_with_null_response_fails() { + /* prepare */ + when(restTemplate.postForObject(eq(INTROSPECTION_URI), any(), eq(OpaqueTokenResponse.class))).thenReturn(null); + + /* execute & assert */ + /* @formatter:off */ + assertThatThrownBy(() -> introspectorToTest.introspect(OPAQUE_TOKEN)) + .isInstanceOf(ResponseStatusException.class) + .hasMessageContaining("Failed to perform token introspection"); + /* @formatter:on */ + } + + @Test + void introspect_with_inactive_token_fails() { + /* prepare */ + OpaqueTokenResponse opaqueTokenResponse = createOpaqueTokenResponse(Boolean.FALSE, null); + when(restTemplate.postForObject(eq(INTROSPECTION_URI), any(), eq(OpaqueTokenResponse.class))).thenReturn(opaqueTokenResponse); + + /* execute & assert */ + /* @formatter:off */ + assertThatThrownBy(() -> introspectorToTest.introspect(OPAQUE_TOKEN)) + .isInstanceOf(BadOpaqueTokenException.class) + .hasMessageContaining("Token is not active"); + /* @formatter:on */ + } + + @Test + void introspect_with_valid_token_succeeds() { + /* prepare */ + long expiresAt = 3600L; + OpaqueTokenResponse opaqueTokenResponse = createOpaqueTokenResponse(Boolean.TRUE, expiresAt); + when(restTemplate.postForObject(eq(INTROSPECTION_URI), any(), eq(OpaqueTokenResponse.class))).thenReturn(opaqueTokenResponse); + Collection authorities = Set.of(new SimpleGrantedAuthority(TestRoles.USER)); + when(userDetailsService.loadUserByUsername(SUBJECT)).thenReturn(new TestUserDetails(authorities, SUBJECT)); + + /* execute */ + OAuth2AuthenticatedPrincipal principal = introspectorToTest.introspect(OPAQUE_TOKEN); + + /* assert */ + assertThat(principal.getName()).isEqualTo(SUBJECT); + Map attributes = principal.getAttributes(); + assertThat(attributes.get(OAuth2TokenIntrospectionClaimNames.ACTIVE)).isEqualTo(opaqueTokenResponse.isActive()); + assertThat(attributes.get(OAuth2TokenIntrospectionClaimNames.SCOPE)).isEqualTo(opaqueTokenResponse.getScope()); + assertThat(attributes.get(OAuth2TokenIntrospectionClaimNames.CLIENT_ID)).isEqualTo(opaqueTokenResponse.getClientId()); + assertThat(attributes.get(OAuth2TokenIntrospectionClaimNames.USERNAME)).isEqualTo(opaqueTokenResponse.getUsername()); + assertThat(attributes.get(OAuth2TokenIntrospectionClaimNames.TOKEN_TYPE)).isEqualTo(opaqueTokenResponse.getTokenType()); + assertThat((Instant) attributes.get(OAuth2TokenIntrospectionClaimNames.IAT)).isAfter(Instant.EPOCH); + assertThat((Instant) attributes.get(OAuth2TokenIntrospectionClaimNames.EXP)).isEqualTo(Instant.ofEpochSecond(expiresAt)); + assertThat(attributes.get(OAuth2TokenIntrospectionClaimNames.SUB)).isEqualTo(opaqueTokenResponse.getSubject()); + assertThat(attributes.get(OAuth2TokenIntrospectionClaimNames.AUD)).isEqualTo(opaqueTokenResponse.getAudience()); + } + + @Test + void introspect_with_null_expires_at_constructs_principal_with_default_expires_at() { + /* prepare */ + Instant now = Instant.now(); + OpaqueTokenResponse opaqueTokenResponse = createOpaqueTokenResponse(Boolean.TRUE, null); + when(restTemplate.postForObject(eq(INTROSPECTION_URI), any(), eq(OpaqueTokenResponse.class))).thenReturn(opaqueTokenResponse); + Collection authorities = Set.of(new SimpleGrantedAuthority(TestRoles.USER)); + when(userDetailsService.loadUserByUsername(SUBJECT)).thenReturn(new TestUserDetails(authorities, SUBJECT)); + + /* execute */ + OAuth2AuthenticatedPrincipal principal = introspectorToTest.introspect(OPAQUE_TOKEN); + + /* assert */ + Instant actual = (Instant) principal.getAttributes().get(OAuth2TokenIntrospectionClaimNames.EXP); + assertThat(actual).isNotNull(); + assertThat(actual).isAfterOrEqualTo(now.plusSeconds(DEFAULT_EXPIRES_IN_SECONDS)); + } + + /* @formatter:off */ + private static OpaqueTokenResponse createOpaqueTokenResponse(Boolean isActive, Long expiresAt) { + return new OpaqueTokenResponse( + isActive, + "scope", + "client-id", + "client-type", + SUBJECT, + "token-type", + expiresAt, + SUBJECT, + "aud", + "group-type" + ); + } + /* @formatter:on */ + + private static class Base64OAuth2OpaqueTokenIntrospectorSingleNullArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext extensionContext) throws Exception { + return Stream.of(Arguments.of(null, INTROSPECTION_URI, CLIENT_ID, CLIENT_SECRET, userDetailsService, "Parameter restTemplate must not be null"), + Arguments.of(restTemplate, null, CLIENT_ID, CLIENT_SECRET, userDetailsService, "Parameter introspectionUri must not be null"), + Arguments.of(restTemplate, INTROSPECTION_URI, null, CLIENT_SECRET, userDetailsService, "Parameter clientId must not be null"), + Arguments.of(restTemplate, INTROSPECTION_URI, CLIENT_ID, null, userDetailsService, "Parameter clientSecret must not be null"), + Arguments.of(restTemplate, INTROSPECTION_URI, CLIENT_ID, CLIENT_SECRET, null, "Parameter userDetailsService must not be null")); + } + } +} \ No newline at end of file diff --git a/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/OAuth2JwtIntegrationTest.java b/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/OAuth2JwtIntegrationTest.java new file mode 100644 index 0000000000..1635c03529 --- /dev/null +++ b/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/OAuth2JwtIntegrationTest.java @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.spring.security; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.Set; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpHeaders; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import com.mercedesbenz.sechub.testframework.spring.YamlPropertyLoaderFactory; + +/** + * This test class verifies the integration of Spring Security OAuth2 components + * in JWT mode. + * + *

+ * Unlike {@link SecurityConfigurationTest}, which primarily tests if a endpoint + * is secured on an abstract level, this class exercises the full OAuth2 flow + * with real OAuth2 mechanisms. We do that by relying on the + * {@link AbstractSecurityConfiguration}. + *

+ * + *

+ * In a typical setup, the + * {@link org.springframework.security.oauth2.jwt.JwtDecoder} decodes JWT tokens + * by integrating with a identity provider. With this configuration, however, we + * mock the identity provider to avoid external dependencies. Additionally, we + * mock the user's roles, which are otherwise fetched from the database. + *

+ * + *

+ * Note: This test class is not intended for verifying whether security + * is enabled on specific endpoints. For that, use + * {@link SecurityConfigurationTest}. + *

+ * + * @see AbstractSecurityConfiguration + * @see org.springframework.security.core.userdetails.UserDetailsService + * @see OAuth2JwtAuthenticationProvider + * @see org.springframework.security.oauth2.jwt.JwtDecoder + * @see SecurityConfigurationTest + * + * @author hamidonos + */ +@WebMvcTest +@TestPropertySource(locations = "classpath:application-jwt-test.yml", factory = YamlPropertyLoaderFactory.class) +@ActiveProfiles("oauth2") +class OAuth2JwtIntegrationTest { + + /** + * For this test we call the API endpoint /api/user. It is just a mock endpoint + * to test the OAuth2 integration. It could also be any other endpoint. + */ + private static final String API_USER = "/api/user"; + + private final MockMvc mockMvc; + + @Autowired + OAuth2JwtIntegrationTest(MockMvc mockMvc) { + this.mockMvc = mockMvc; + } + + @Test + void api_user_is_not_accessible_anonymously() throws Exception { + /* execute & test */ + /* @formatter:off */ + mockMvc + .perform(MockMvcRequestBuilders.get(API_USER)) + .andExpect(status().isUnauthorized()); + /* @formatter:on */ + } + + @Test + void api_user_is_accessible_as_superadmin() throws Exception { + /* prepare */ + String authHeader = TestOAuth2JwtSecurityConfiguration.createJwtAuthHeader(Set.of(TestRoles.SUPERADMIN)); + + /* execute & test */ + /* @formatter:off */ + mockMvc + .perform(MockMvcRequestBuilders.get(API_USER).header(HttpHeaders.AUTHORIZATION, authHeader)) + .andExpect(status().isOk()); + /* @formatter:on */ + } + + @Test + void api_user_is_accessible_as_superadmin_owner() throws Exception { + /* prepare */ + String authHeader = TestOAuth2JwtSecurityConfiguration.createJwtAuthHeader(Set.of(TestRoles.SUPERADMIN, TestRoles.OWNER)); + + /* execute & test */ + /* @formatter:off */ + mockMvc + .perform(MockMvcRequestBuilders.get(API_USER).header(HttpHeaders.AUTHORIZATION, authHeader)) + .andExpect(status().isOk()); + /* @formatter:on */ + } + + @Test + void api_user_is_not_accessible_as_owner() throws Exception { + /* prepare */ + String authHeader = TestOAuth2JwtSecurityConfiguration.createJwtAuthHeader(Set.of(TestRoles.OWNER)); + + /* execute & test */ + /* @formatter:off */ + mockMvc + .perform(MockMvcRequestBuilders.get(API_USER).header(HttpHeaders.AUTHORIZATION, authHeader)) + .andExpect(status().isForbidden()); + /* @formatter:on */ + } + + @Test + void api_user_is_accessible_as_user() throws Exception { + /* prepare */ + String authHeader = TestOAuth2JwtSecurityConfiguration.createJwtAuthHeader(Set.of(TestRoles.USER)); + + /* execute & test */ + /* @formatter:off */ + mockMvc + .perform(MockMvcRequestBuilders.get(API_USER).header(HttpHeaders.AUTHORIZATION, authHeader)) + .andExpect(status().isOk()); + /* @formatter:on */ + } + + @Configuration + @Import({ TestSecurityConfiguration.class, TestOAuth2JwtSecurityConfiguration.class, OAuth2JwtPropertiesConfiguration.class }) + static class TestConfig { + + @Bean + TestSecurityController testSecurityController() { + return new TestSecurityController(); + } + } +} diff --git a/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/OAuth2JwtPropertiesTest.java b/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/OAuth2JwtPropertiesTest.java new file mode 100644 index 0000000000..4c2dacf1f2 --- /dev/null +++ b/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/OAuth2JwtPropertiesTest.java @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.spring.security; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; + +import com.mercedesbenz.sechub.testframework.spring.YamlPropertyLoaderFactory; + +@SpringBootTest +@ActiveProfiles("oauth2") +@TestPropertySource(locations = "classpath:application-jwt-test.yml", factory = YamlPropertyLoaderFactory.class) +class OAuth2JwtPropertiesTest { + + private final OAuth2JwtProperties oAuth2JwtProperties; + + OAuth2JwtPropertiesTest(@Autowired OAuth2JwtProperties oAuth2JwtProperties) { + this.oAuth2JwtProperties = oAuth2JwtProperties; + } + + @Test + void construct_properties_with_jwt_enabled_succeeds() { + assertThat(oAuth2JwtProperties.getJwkSetUri()).isEqualTo("https://example.org/jwk-set-uri"); + } + + /* @formatter:off */ + @Test + void construct_properties_with_jwk_set_uri_null_fails() { + assertThatThrownBy(() -> new OAuth2JwtProperties(null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("Property 'sechub.security.oauth2.jwt.jwk-set-uri' must not be null"); + } + /* @formatter:on */ + + @Configuration + @Import({ OAuth2JwtPropertiesConfiguration.class, OAuth2OpaqueTokenPropertiesConfiguration.class }) + static class TestConfig { + } +} \ No newline at end of file diff --git a/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/OAuth2OpaqueTokenIntegrationTest.java b/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/OAuth2OpaqueTokenIntegrationTest.java new file mode 100644 index 0000000000..313cafbfb8 --- /dev/null +++ b/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/OAuth2OpaqueTokenIntegrationTest.java @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.spring.security; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.Set; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpHeaders; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import com.mercedesbenz.sechub.testframework.spring.YamlPropertyLoaderFactory; + +/** + * This test class verifies the integration of Spring Security OAuth2 components + * in opaque token mode. + * + *

+ * Unlike {@link SecurityConfigurationTest}, which primarily tests if a endpoint + * is secured on an abstract level, this class exercises the full OAuth2 flow + * with real OAuth2 mechanisms. We do that by relying on the + * {@link AbstractSecurityConfiguration}. + *

+ * + *

+ * In a typical setup, the + * {@link org.springframework.security.oauth2.jwt.JwtDecoder} decodes JWT tokens + * by integrating with a identity provider. With this configuration, however, we + * mock the identity provider to avoid external dependencies. Additionally, we + * mock the user's roles, which are otherwise fetched from the database. + *

+ * + *

+ * Note: This test class is not intended for verifying whether security + * is enabled on specific endpoints. For that, use + * {@link SecurityConfigurationTest}. + *

+ * + * @see AbstractSecurityConfiguration + * @see com.mercedesbenz.sechub.domain.authorization.AuthUserDetailsService + * @see OAuth2JwtAuthenticationProvider + * @see org.springframework.security.oauth2.jwt.JwtDecoder + * @see SecurityConfigurationTest + * + * @author hamidonos + */ +@SuppressWarnings("JavadocReference") +@WebMvcTest +@TestPropertySource(locations = "classpath:application-opaque-token-test.yml", factory = YamlPropertyLoaderFactory.class) +@ActiveProfiles("oauth2") +class OAuth2OpaqueTokenIntegrationTest { + + /** + * For this test we call the API endpoint /api/user. It is just a mock endpoint + * to test the OAuth2 integration. It could also be any other endpoint. + */ + private static final String API_USER = "/api/user"; + + private final MockMvc mockMvc; + + @Autowired + OAuth2OpaqueTokenIntegrationTest(MockMvc mockMvc) { + this.mockMvc = mockMvc; + } + + @Test + void api_user_is_not_accessible_anonymously() throws Exception { + /* execute & test */ + /* @formatter:off */ + mockMvc + .perform(MockMvcRequestBuilders.get(API_USER)) + .andExpect(status().isUnauthorized()); + /* @formatter:on */ + } + + @Test + void api_user_is_accessible_as_superadmin() throws Exception { + /* prepare */ + String authHeader = TestOAuth2OpaqueTokenSecurityConfiguration.createOpaqueTokenHeader(Set.of(TestRoles.SUPERADMIN)); + + /* execute & test */ + /* @formatter:off */ + mockMvc + .perform(MockMvcRequestBuilders.get(API_USER).header(HttpHeaders.AUTHORIZATION, authHeader)) + .andExpect(status().isOk()); + /* @formatter:on */ + } + + @Test + void api_user_is_accessible_as_superadmin_owner() throws Exception { + /* prepare */ + String authHeader = TestOAuth2OpaqueTokenSecurityConfiguration.createOpaqueTokenHeader(Set.of(TestRoles.SUPERADMIN, TestRoles.OWNER)); + + /* execute & test */ + /* @formatter:off */ + mockMvc + .perform(MockMvcRequestBuilders.get(API_USER).header(HttpHeaders.AUTHORIZATION, authHeader)) + .andExpect(status().isOk()); + /* @formatter:on */ + } + + @Test + void api_user_is_not_accessible_as_owner() throws Exception { + /* prepare */ + String authHeader = TestOAuth2OpaqueTokenSecurityConfiguration.createOpaqueTokenHeader(Set.of(TestRoles.OWNER)); + + /* execute & test */ + /* @formatter:off */ + mockMvc + .perform(MockMvcRequestBuilders.get(API_USER).header(HttpHeaders.AUTHORIZATION, authHeader)) + .andExpect(status().isForbidden()); + /* @formatter:on */ + } + + @Test + void api_user_is_accessible_as_user() throws Exception { + /* prepare */ + String authHeader = TestOAuth2OpaqueTokenSecurityConfiguration.createOpaqueTokenHeader(Set.of(TestRoles.USER)); + + /* execute & test */ + /* @formatter:off */ + mockMvc + .perform(MockMvcRequestBuilders.get(API_USER).header(HttpHeaders.AUTHORIZATION, authHeader)) + .andExpect(status().isOk()); + /* @formatter:on */ + } + + @Configuration + @Import({ TestSecurityConfiguration.class, TestOAuth2OpaqueTokenSecurityConfiguration.class, OAuth2OpaqueTokenPropertiesConfiguration.class }) + static class TestConfig { + + @Bean + TestSecurityController testSecurityController() { + return new TestSecurityController(); + } + } +} diff --git a/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/OAuth2OpaqueTokenPropertiesTest.java b/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/OAuth2OpaqueTokenPropertiesTest.java new file mode 100644 index 0000000000..08995e6aa0 --- /dev/null +++ b/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/OAuth2OpaqueTokenPropertiesTest.java @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.spring.security; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; + +import com.mercedesbenz.sechub.testframework.spring.YamlPropertyLoaderFactory; + +@SpringBootTest +@ActiveProfiles("oauth2") +@TestPropertySource(locations = "classpath:application-opaque-token-test.yml", factory = YamlPropertyLoaderFactory.class) +class OAuth2OpaqueTokenPropertiesTest { + + private final OAuth2OpaqueTokenProperties oAuth2OpaqueTokenProperties; + + OAuth2OpaqueTokenPropertiesTest(@Autowired OAuth2OpaqueTokenProperties oAuth2OpaqueTokenProperties) { + this.oAuth2OpaqueTokenProperties = oAuth2OpaqueTokenProperties; + } + + @Test + void construct_properties_with_opaque_token_enabled_succeeds() { + assertThat(oAuth2OpaqueTokenProperties.getIntrospectionUri()).isEqualTo("https://example.org/introspection-uri"); + assertThat(oAuth2OpaqueTokenProperties.getClientId()).isEqualTo("example-client-id"); + assertThat(oAuth2OpaqueTokenProperties.getClientSecret()).isEqualTo("example-client-secret"); + } + + /* @formatter:off */ + @ParameterizedTest + @ArgumentsSource(InvalidOAuth2OpaqueTokenPropertiesProvider.class) + void construct_properties_with_null_arguments_fails(String introspectionUri, + String clientId, + String clientSecret, + String errMsg) { + assertThatThrownBy(() -> new OAuth2OpaqueTokenProperties(introspectionUri, clientId, clientSecret)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining(errMsg); + } + /* @formatter:on */ + + @Configuration + @Import({ OAuth2OpaqueTokenPropertiesConfiguration.class, OAuth2JwtPropertiesConfiguration.class }) + static class TestConfig { + } + + private static class InvalidOAuth2OpaqueTokenPropertiesProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext extensionContext) { + /* @formatter:off */ + return Stream.of( + Arguments.of(null, "example-client-id", "example-client-secret", "Property 'sechub.security.oauth2.opaque-token.introspection-uri' must not be null"), + Arguments.of("https://example.org/introspection-uri", null, "example-client-secret", "Property 'sechub.security.oauth2.opaque-token.client-id' must not be null"), + Arguments.of("https://example.org/introspection-uri", "example-client-id", null, "Property 'sechub.security.oauth2.opaque-token.client-secret' must not be null") + ); + /* @formatter:on */ + } + } +} \ No newline at end of file diff --git a/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/OpaqueTokenResponseTest.java b/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/OpaqueTokenResponseTest.java new file mode 100644 index 0000000000..1b9e5935eb --- /dev/null +++ b/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/OpaqueTokenResponseTest.java @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.spring.security; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.time.Instant; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.opentest4j.TestAbortedException; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.exc.ValueInstantiationException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.jayway.jsonpath.JsonPath; + +class OpaqueTokenResponseTest { + + private static final String opaqueTokenResponseJson; + private static final ObjectMapper objectMapper = new ObjectMapper(); + + static { + try { + opaqueTokenResponseJson = Files.readString(Paths.get("src/test/resources/opaque-token-response.json")); + } catch (IOException e) { + throw new TestAbortedException("Failed to prepare test", e); + } + } + + @Test + void construct_opaque_token_response_from_valid_json_is_successful() throws JsonProcessingException { + /* prepare */ + Instant epoch = Instant.EPOCH; + + /* execute */ + OpaqueTokenResponse opaqueTokenResponse = objectMapper.readValue(opaqueTokenResponseJson, OpaqueTokenResponse.class); + + // assert + assertThat(opaqueTokenResponse).isNotNull(); + assertThat(opaqueTokenResponse.isActive()).isEqualTo(JsonPath.read(opaqueTokenResponseJson, "$.active")); + assertThat(opaqueTokenResponse.getScope()).isEqualTo(JsonPath.read(opaqueTokenResponseJson, "$.scope")); + assertThat(opaqueTokenResponse.getClientId()).isEqualTo(JsonPath.read(opaqueTokenResponseJson, "$.client_id")); + assertThat(opaqueTokenResponse.getClientType()).isEqualTo(JsonPath.read(opaqueTokenResponseJson, "$.client_type")); + assertThat(opaqueTokenResponse.getUsername()).isEqualTo(JsonPath.read(opaqueTokenResponseJson, "$.username")); + assertThat(opaqueTokenResponse.getTokenType()).isEqualTo(JsonPath.read(opaqueTokenResponseJson, "$.token_type")); + assertThat(opaqueTokenResponse.getExpiresAt()).isAfter(epoch); + assertThat(opaqueTokenResponse.getSubject()).isEqualTo(JsonPath.read(opaqueTokenResponseJson, "$.sub")); + assertThat(opaqueTokenResponse.getAudience()).isEqualTo(JsonPath.read(opaqueTokenResponseJson, "$.aud")); + assertThat(opaqueTokenResponse.getGroupType()).isEqualTo(JsonPath.read(opaqueTokenResponseJson, "$.group_type")); + } + + @ParameterizedTest + @ArgumentsSource(ValidOpaqueTokenResponseProvider.class) + void construct_opaque_token_response_from_valid_json_with_nullable_properties_is_successful(String validOpaqueTokenResponseJson) { + /* execute & test */ + assertDoesNotThrow(() -> objectMapper.readValue(validOpaqueTokenResponseJson, OpaqueTokenResponse.class)); + } + + @ParameterizedTest + @ArgumentsSource(InvalidOpaqueTokenResponseProvider.class) + void construct_opaque_token_response_from_invalid_json_fails(String invalidOpaqueTokenResponseJson, String errMsg) throws JsonProcessingException { + /* execute & test */ + + /* @formatter:off */ + assertThatThrownBy(() -> objectMapper.readValue(invalidOpaqueTokenResponseJson, OpaqueTokenResponse.class)) + .isInstanceOf(ValueInstantiationException.class) + .hasMessageContaining(errMsg); + /* @formatter:on */ + } + + private static String removeJsonKeyAndValue(String key) throws JsonProcessingException { + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode rootNode = objectMapper.readTree(opaqueTokenResponseJson); + + if (rootNode instanceof ObjectNode) { + ((ObjectNode) rootNode).remove(key); + } else { + throw new IllegalArgumentException("Invalid JSON"); + } + + return objectMapper.writeValueAsString(rootNode); + } + + /* @formatter:off */ + private static class ValidOpaqueTokenResponseProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext extensionContext) throws Exception { + return Stream.of( + Arguments.of(removeJsonKeyAndValue("scope")), + Arguments.of(removeJsonKeyAndValue("client_id")), + Arguments.of(removeJsonKeyAndValue("client_type")), + Arguments.of(removeJsonKeyAndValue("username")), + Arguments.of(removeJsonKeyAndValue("token_type")), + Arguments.of(removeJsonKeyAndValue("exp")), + Arguments.of(removeJsonKeyAndValue("aud")), + Arguments.of(removeJsonKeyAndValue("group_type")) + ); + } + } + + /* @formatter:off */ + private static class InvalidOpaqueTokenResponseProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext extensionContext) throws Exception { + return Stream.of( + Arguments.of(removeJsonKeyAndValue("active"), "Property 'active' must not be null"), + Arguments.of(removeJsonKeyAndValue("sub"), "Property 'sub' must not be null") + ); + } + } + /* @formatter:on */ +} diff --git a/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/SecurityConfigurationTest.java b/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/SecurityConfigurationTest.java new file mode 100644 index 0000000000..3e2dccb3f8 --- /dev/null +++ b/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/SecurityConfigurationTest.java @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.spring.security; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpStatus; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +/** + * This test class makes sure that the defined API security rules from + * {@link AbstractSecurityConfiguration} are working properly. + * + *

+ * Using {@link WithMockUser} to set up a mocked + * {@link org.springframework.security.core.context.SecurityContext}, we can + * test how the endpoints behave when accessed by different roles. + *

+ * + *

+ * Note: Here we don't test the integration of OAuth2 or Basic Auth. For + * that, see {@link OAuth2JwtIntegrationTest}. This test class is only concerned + * with verifying if the security rules are correctly applied on an abstract + * level. + *

+ * + * @see WithMockUser + * @see OAuth2JwtIntegrationTest + * @see AbstractSecurityConfiguration + * + * @author hamidonos + */ +@WebMvcTest +class SecurityConfigurationTest { + + private static final String SUPERADMIN = "SUPERADMIN"; + private static final String USER = "USER"; + private static final String OWNER = "OWNER"; + + private final MockMvc mockMvc; + + @Autowired + SecurityConfigurationTest(MockMvc mockMvc) { + this.mockMvc = mockMvc; + } + + /* Super Admin */ + + @Test + @WithMockUser(roles = SUPERADMIN) + void api_admin_is_accessible_with_superadmin_role() throws Exception { + getAndExpect("/api/admin", HttpStatus.OK); + } + + @Test + @WithMockUser(roles = SUPERADMIN) + void api_user_is_accessible_with_superadmin_role() throws Exception { + getAndExpect("/api/user", HttpStatus.OK); + } + + @Test + @WithMockUser(roles = SUPERADMIN) + void api_owner_is_accessible_with_superadmin_role() throws Exception { + getAndExpect("/api/owner", HttpStatus.OK); + } + + /* User */ + + @Test + @WithMockUser(roles = USER) + void api_admin_is_not_accessible_with_user_role() throws Exception { + getAndExpect("/api/admin", HttpStatus.FORBIDDEN); + } + + @Test + @WithMockUser(roles = USER) + void api_user_is_accessible_with_user_role() throws Exception { + getAndExpect("/api/user", HttpStatus.OK); + } + + @Test + @WithMockUser(roles = USER) + void api_owner_is_not_accessible_with_user_role() throws Exception { + getAndExpect("/api/owner", HttpStatus.FORBIDDEN); + } + + /* Owner */ + + @Test + @WithMockUser(roles = OWNER) + void api_admin_is_not_accessible_with_owner_role() throws Exception { + getAndExpect("/api/admin", HttpStatus.FORBIDDEN); + } + + @Test + @WithMockUser(roles = OWNER) + void api_user_is_not_accessible_with_owner_role() throws Exception { + getAndExpect("/api/user", HttpStatus.FORBIDDEN); + } + + @Test + @WithMockUser(roles = OWNER) + void api_owner_is_accessible_with_owner_role() throws Exception { + getAndExpect("/api/owner", HttpStatus.OK); + } + + /* Anonymous */ + + @Test + void api_admin_is_not_accessible_anonymously() throws Exception { + getAndExpect("/api/admin", HttpStatus.UNAUTHORIZED); + } + + @Test + void api_user_is_not_accessible_anonymously() throws Exception { + getAndExpect("/api/user", HttpStatus.UNAUTHORIZED); + } + + @Test + void api_owner_is_not_accessible_anonymously() throws Exception { + getAndExpect("/api/owner", HttpStatus.UNAUTHORIZED); + } + + @Test + void api_anonymous_is_accessible_anonymously() throws Exception { + getAndExpect("/api/anonymous", HttpStatus.OK); + } + + private void getAndExpect(String path, HttpStatus httpStatus) throws Exception { + /* @formatter:off */ + mockMvc + .perform(MockMvcRequestBuilders.get(path)) + .andExpect(status().is(httpStatus.value())); + /* @formatter:on */ + } + + @Configuration + @Import(TestSecurityConfiguration.class) + static class TestConfig { + + @Bean + TestSecurityController testSecurityController() { + return new TestSecurityController(); + } + } +} diff --git a/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/TestOAuth2JwtSecurityConfiguration.java b/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/TestOAuth2JwtSecurityConfiguration.java new file mode 100644 index 0000000000..06395eb7a0 --- /dev/null +++ b/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/TestOAuth2JwtSecurityConfiguration.java @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.spring.security; + +import static com.mercedesbenz.sechub.spring.security.TestRoles.OWNER; +import static com.mercedesbenz.sechub.spring.security.TestRoles.SUPERADMIN; +import static com.mercedesbenz.sechub.spring.security.TestRoles.USER; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +import org.opentest4j.TestAbortedException; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtException; + +/** + * This configuration class provides the necessary beans to test Springs OAuth2 + * integration with SecHub components in JWT mode. + * + * @author hamidonos + */ +@Configuration +public class TestOAuth2JwtSecurityConfiguration { + + public static final String BEARER_PREFIX = OAuth2AccessToken.TokenType.BEARER.getValue() + " "; + + private static final String ADMIN_JWT = "admin-jwt-token"; + private static final String ADMIN_OWNER_JWT = "admin-owner-jwt-token"; + private static final String ADMIN_USER_JWT = "admin-user-jwt-token"; + private static final String OWNER_JWT = "owner-jwt-token"; + private static final String OWNER_USER_JWT = "owner-user-jwt-token"; + private static final String USER_JWT = "user-jwt-token"; + + private static final String ADMIN_ID = UUID.randomUUID().toString(); + private static final String ADMIN_OWNER_ID = UUID.randomUUID().toString(); + private static final String ADMIN_USER_ID = UUID.randomUUID().toString(); + private static final String OWNER_ID = UUID.randomUUID().toString(); + private static final String OWNER_USER_ID = UUID.randomUUID().toString(); + private static final String USER_ID = UUID.randomUUID().toString(); + + private static final String ALGORITHM = "alg"; + private static final String ALGORITHM_NONE = "none"; + + /** + * This bean provides a {@link JwtDecoder} that decodes the JWT token and + * returns a {@link Jwt} object. The behaviour is completely mocked and the + * possible JWT tokens are pre-defined. Every possible JWT value is mapped to a + * specific subject (or user id). The subject will be returned as part of the + * JWT decode process. To keep testing as simple as possible, we map only ONE + * role to ONE user and provide here no combinations. + */ + @Bean + JwtDecoder jwtDecoder() { + JwtDecoder jwtDecoder = mock(); + when(jwtDecoder.decode(anyString())).thenAnswer(invocation -> { + String jwtTokenValue = invocation.getArgument(0); + Jwt.Builder builder = Jwt.withTokenValue(jwtTokenValue).header(ALGORITHM, ALGORITHM_NONE); + if (ADMIN_JWT.equals(jwtTokenValue)) { + return builder.subject(ADMIN_ID).build(); + } + if (ADMIN_OWNER_JWT.equals(jwtTokenValue)) { + return builder.subject(ADMIN_OWNER_ID).build(); + } + if (ADMIN_USER_JWT.equals(jwtTokenValue)) { + return builder.subject(ADMIN_USER_ID).build(); + } + if (OWNER_JWT.equals(jwtTokenValue)) { + return builder.subject(OWNER_ID).build(); + } + if (OWNER_USER_JWT.equals(jwtTokenValue)) { + return builder.subject(OWNER_USER_ID).build(); + } + if (USER_JWT.equals(jwtTokenValue)) { + return builder.subject(USER_ID).build(); + } + + throw new JwtException("Invalid JWT token"); + }); + return jwtDecoder; + } + + /** + * Here we mock the {@link UserDetailsService} to return a + * {@link TestUserDetails} object based on the user id (or subject). The subject + * is determined by the {@link TestOAuth2JwtSecurityConfiguration#jwtDecoder()} + * bean. Depending on the user id, the {@link TestUserDetails} object will + * contain the corresponding authorities. + */ + @Bean + UserDetailsService userDetailsService() { + UserDetailsService userDetailsService = mock(); + when(userDetailsService.loadUserByUsername(anyString())).thenAnswer(invocation -> { + String username = invocation.getArgument(0); + if (!Set.of(ADMIN_ID, ADMIN_OWNER_ID, ADMIN_USER_ID, OWNER_ID, OWNER_USER_ID, USER_ID).contains(username)) { + throw new UsernameNotFoundException("User %s not found".formatted(username)); + } + + Collection authorities = new HashSet<>(); + + if (ADMIN_ID.equals(username)) { + authorities.add(new SimpleGrantedAuthority(SUPERADMIN)); + } + if (ADMIN_OWNER_ID.equals(username)) { + authorities.add(new SimpleGrantedAuthority(SUPERADMIN)); + authorities.add(new SimpleGrantedAuthority(OWNER)); + } + if (ADMIN_USER_ID.equals(username)) { + authorities.add(new SimpleGrantedAuthority(SUPERADMIN)); + authorities.add(new SimpleGrantedAuthority(USER)); + } + if (OWNER_ID.equals(username)) { + authorities.add(new SimpleGrantedAuthority(OWNER)); + } + if (OWNER_USER_ID.equals(username)) { + authorities.add(new SimpleGrantedAuthority(OWNER)); + authorities.add(new SimpleGrantedAuthority(USER)); + } + if (USER_ID.equals(username)) { + authorities.add(new SimpleGrantedAuthority(USER)); + } + + return new TestUserDetails(authorities, username); + }); + + return userDetailsService; + } + + public static String createJwtAuthHeader(Set roles) { + if (roles.isEmpty()) { + throw new TestAbortedException("Roles cannot be empty"); + } + + if (roles.equals(Set.of(SUPERADMIN))) { + return BEARER_PREFIX + ADMIN_JWT; + } + + if (roles.equals(Set.of(SUPERADMIN, OWNER))) { + return BEARER_PREFIX + ADMIN_OWNER_JWT; + } + + if (roles.equals(Set.of(SUPERADMIN, USER))) { + return BEARER_PREFIX + ADMIN_USER_JWT; + } + + if (roles.equals(Set.of(OWNER))) { + return BEARER_PREFIX + OWNER_JWT; + } + + if (roles.equals(Set.of(OWNER, USER))) { + return BEARER_PREFIX + OWNER_USER_JWT; + } + + if (roles.equals(Set.of(USER))) { + return BEARER_PREFIX + USER_JWT; + } + + throw new TestAbortedException("Invalid roles"); + } + +} diff --git a/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/TestOAuth2OpaqueTokenSecurityConfiguration.java b/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/TestOAuth2OpaqueTokenSecurityConfiguration.java new file mode 100644 index 0000000000..ddfd8749a7 --- /dev/null +++ b/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/TestOAuth2OpaqueTokenSecurityConfiguration.java @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.spring.security; + +import static com.mercedesbenz.sechub.spring.security.TestRoles.OWNER; +import static com.mercedesbenz.sechub.spring.security.TestRoles.SUPERADMIN; +import static com.mercedesbenz.sechub.spring.security.TestRoles.USER; +import static java.util.Objects.requireNonNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +import org.opentest4j.TestAbortedException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpEntity; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +/** + * This configuration class provides the necessary beans to test Springs OAuth2 + * integration with SecHub components in opaque token mode. + * + * @author hamidonos + */ +@Configuration +public class TestOAuth2OpaqueTokenSecurityConfiguration { + + public static final String BEARER_PREFIX = OAuth2AccessToken.TokenType.BEARER.getValue() + " "; + + private static final String TOKEN = "token"; + private static final String ADMIN_OPAQUE_TOKEN = "admin-opaque-token"; + private static final String ADMIN_OWNER_OPAQUE_TOKEN = "admin-owner-opaque-token"; + private static final String ADMIN_USER_OPAQUE_TOKEN = "admin-user-opaque-token"; + private static final String OWNER_OPAQUE_TOKEN = "owner-opaque-token"; + private static final String OWNER_USER_OPAQUE_TOKEN = "owner-user-opaque-token"; + private static final String USER_OPAQUE_TOKEN = "user-opaque-token"; + + private static final String ADMIN_ID = UUID.randomUUID().toString(); + private static final String ADMIN_OWNER_ID = UUID.randomUUID().toString(); + private static final String ADMIN_USER_ID = UUID.randomUUID().toString(); + private static final String OWNER_ID = UUID.randomUUID().toString(); + private static final String OWNER_USER_ID = UUID.randomUUID().toString(); + private static final String USER_ID = UUID.randomUUID().toString(); + + /* @formatter:off */ + @Autowired + TestOAuth2OpaqueTokenSecurityConfiguration(RestTemplate restTemplate, + OAuth2OpaqueTokenProperties oAuth2OpaqueTokenProperties) { + + when(restTemplate.postForObject(eq(oAuth2OpaqueTokenProperties.getIntrospectionUri()), any(), eq(OpaqueTokenResponse.class))).thenAnswer(invocation -> { + HttpEntity> request = invocation.getArgument(1); + String token = requireNonNull(request.getBody()).getFirst(TOKEN); + boolean isActive = false; + String subject = ""; + + if (ADMIN_OPAQUE_TOKEN.equals(token)) { + isActive = true; + subject = ADMIN_ID; + } + if (ADMIN_OWNER_OPAQUE_TOKEN.equals(token)) { + isActive = true; + subject = ADMIN_OWNER_ID; + } + if (ADMIN_USER_OPAQUE_TOKEN.equals(token)) { + isActive = true; + subject = ADMIN_USER_ID; + } + if (OWNER_OPAQUE_TOKEN.equals(token)) { + isActive = true; + subject = OWNER_ID; + } + if (OWNER_USER_OPAQUE_TOKEN.equals(token)) { + isActive = true; + subject = OWNER_USER_ID; + } + if (USER_OPAQUE_TOKEN.equals(token)) { + isActive = true; + subject = USER_ID; + } + + return new OpaqueTokenResponse(isActive, + "scope", + "client-id", + "client-type", + subject, + "token-type", + Instant.now().plusSeconds(60L).getEpochSecond(), + subject, + "aud", + "group-type"); + }); + } + /* @formatter:on */ + + /** + * Here we mock the {@link UserDetailsService} to return a + * {@link TestUserDetails} object based on the user id (or subject). The subject + * is determined by the {@link Base64OAuth2OpaqueTokenIntrospector} component. + * Depending on the user id, the {@link TestUserDetails} object will contain the + * corresponding authorities. + */ + @Bean + UserDetailsService userDetailsService() { + UserDetailsService userDetailsService = mock(); + when(userDetailsService.loadUserByUsername(anyString())).thenAnswer(invocation -> { + String username = invocation.getArgument(0); + if (!Set.of(ADMIN_ID, ADMIN_OWNER_ID, ADMIN_USER_ID, OWNER_ID, OWNER_USER_ID, USER_ID).contains(username)) { + throw new UsernameNotFoundException("User %s not found".formatted(username)); + } + + Collection authorities = new HashSet<>(); + + if (ADMIN_ID.equals(username)) { + authorities.add(new SimpleGrantedAuthority(SUPERADMIN)); + } + if (ADMIN_OWNER_ID.equals(username)) { + authorities.add(new SimpleGrantedAuthority(SUPERADMIN)); + authorities.add(new SimpleGrantedAuthority(OWNER)); + } + if (ADMIN_USER_ID.equals(username)) { + authorities.add(new SimpleGrantedAuthority(SUPERADMIN)); + authorities.add(new SimpleGrantedAuthority(USER)); + } + if (OWNER_ID.equals(username)) { + authorities.add(new SimpleGrantedAuthority(OWNER)); + } + if (OWNER_USER_ID.equals(username)) { + authorities.add(new SimpleGrantedAuthority(OWNER)); + authorities.add(new SimpleGrantedAuthority(USER)); + } + if (USER_ID.equals(username)) { + authorities.add(new SimpleGrantedAuthority(USER)); + } + + return new TestUserDetails(authorities, username); + }); + + return userDetailsService; + } + + public static String createOpaqueTokenHeader(Set roles) { + if (roles.isEmpty()) { + throw new TestAbortedException("Roles cannot be empty"); + } + + if (roles.equals(Set.of(SUPERADMIN))) { + return BEARER_PREFIX + ADMIN_OPAQUE_TOKEN; + } + + if (roles.equals(Set.of(SUPERADMIN, OWNER))) { + return BEARER_PREFIX + ADMIN_OWNER_OPAQUE_TOKEN; + } + + if (roles.equals(Set.of(SUPERADMIN, USER))) { + return BEARER_PREFIX + ADMIN_USER_OPAQUE_TOKEN; + } + + if (roles.equals(Set.of(OWNER))) { + return BEARER_PREFIX + OWNER_OPAQUE_TOKEN; + } + + if (roles.equals(Set.of(OWNER, USER))) { + return BEARER_PREFIX + OWNER_USER_OPAQUE_TOKEN; + } + + if (roles.equals(Set.of(USER))) { + return BEARER_PREFIX + USER_OPAQUE_TOKEN; + } + + throw new TestAbortedException("Invalid roles"); + } +} diff --git a/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/TestRoles.java b/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/TestRoles.java new file mode 100644 index 0000000000..5d01a7a5d8 --- /dev/null +++ b/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/TestRoles.java @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.spring.security; + +class TestRoles { + static final String SUPERADMIN = "ROLE_SUPERADMIN"; + static final String USER = "ROLE_USER"; + static final String OWNER = "ROLE_OWNER"; +} diff --git a/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/TestSecurityConfiguration.java b/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/TestSecurityConfiguration.java new file mode 100644 index 0000000000..0b44ca475e --- /dev/null +++ b/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/TestSecurityConfiguration.java @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.spring.security; + +import static org.mockito.Mockito.mock; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; +import org.springframework.web.client.RestTemplate; + +/* @formatter:off */ +/** + * The TestSecurityConfiguration class extends the {@link AbstractSecurityConfiguration} and provides + * security configuration for testing purposes. It defines security rules for different test API paths. + * + *

+ * The following API paths are configured: + *

+ *
    + *
  • /api/admin/** - Accessible only to users having the SUPERADMIN authority.
  • + *
  • /api/user/** - Accessible to users having the USER or SUPERADMIN authority.
  • + *
  • /api/owner/** - Accessible to users with having the OWNER or SUPERADMIN authority.
  • + *
  • /api/anonymous/** - Accessible to all users without authentication.
  • + *
  • / - All other paths are denied access.
  • + *
+ * + * @see AbstractSecurityConfiguration + * + * @author hamidonos + */ +/* @formatter:on */ +@Configuration +class TestSecurityConfiguration extends AbstractSecurityConfiguration { + + private final Environment environment; + + TestSecurityConfiguration(Environment environment) { + this.environment = environment; + } + + @Bean + RestTemplate restTemplate() { + return mock(); + } + + @Override + protected boolean isOAuth2Enabled() { + return environment.matchesProfiles("oauth2"); + } + + @Override + protected Customizer.AuthorizationManagerRequestMatcherRegistry> authorizeHttpRequests() { + /* @formatter:off */ + return (auth) -> auth. + requestMatchers("/api/admin" + "/**").hasAnyAuthority(TestRoles.SUPERADMIN). + requestMatchers("/api/user"+ "/**").hasAnyAuthority(TestRoles.USER, TestRoles.SUPERADMIN). + requestMatchers("/api/owner"+ "/**").hasAnyAuthority(TestRoles.OWNER, TestRoles.SUPERADMIN). + requestMatchers("/api/anonymous"+ "/**").permitAll(). + requestMatchers("/**").denyAll(); + /* @formatter:on */ + } +} diff --git a/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/TestSecurityController.java b/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/TestSecurityController.java new file mode 100644 index 0000000000..64bdc01153 --- /dev/null +++ b/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/TestSecurityController.java @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.spring.security; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * This controller spins up a mock API for testing the + * {@link AbstractSecurityConfiguration} of the SecHub application. + * + *

+ * Note: The sechub-shared-kernel module is a library that does + * not contain the actual implementation of the endpoints, hence a mock + * controller is used here. + *

+ * + * @author hamidonos + */ +@RestController +class TestSecurityController { + + private static final String OK = HttpStatus.OK.getReasonPhrase(); + + @GetMapping("/api/admin") + String apiAdmin() { + return OK; + } + + @GetMapping("/api/user") + String apiUser() { + return OK; + } + + @GetMapping("/api/owner") + String apiOwner() { + return OK; + } + + @GetMapping("/api/anonymous") + String apiAnonymous() { + return OK; + } + + @GetMapping("/error") + String errorPage() { + return OK; + } + + @GetMapping("/actuator") + String actuator() { + return OK; + } +} diff --git a/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/TestUserDetails.java b/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/TestUserDetails.java new file mode 100644 index 0000000000..cbbae4a901 --- /dev/null +++ b/sechub-commons-security-spring/src/test/java/com/mercedesbenz/sechub/spring/security/TestUserDetails.java @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.spring.security; + +import java.util.Collection; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +/** + * Test implementation of {@link UserDetails} for use in Spring Security related + * tests. + * + *

+ * Defines an authenticated user that expects a username and a collection of + * granted authorities in the constructor. + *

+ * + *

+ * For Basic Auth purposes, the password can be set as well. Otherwise it will + * contain a default value. + *

+ * + * @author hamidonos + */ +class TestUserDetails implements UserDetails { + + private static final String DEFAULT_PASSWORD = ""; + private static final boolean DEFAULT_IS_ACCOUNT_NON_EXPIRED = true; + private static final boolean DEFAULT_IS_ACCOUNT_NON_LOCKED = true; + private static final boolean DEFAULT_IS_CREDENTIALS_NON_EXPIRED = true; + private static final boolean DEFAULT_IS_ENABLED = true; + + private final Collection authorities; + private final String username; + private final String password; + + TestUserDetails(Collection authorities, String username) { + this.authorities = authorities; + this.username = username; + this.password = DEFAULT_PASSWORD; + } + + TestUserDetails(Collection authorities, String username, String password) { + this.authorities = authorities; + this.username = username; + this.password = password; + } + + @Override + public Collection getAuthorities() { + return authorities; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getUsername() { + return username; + } + + @Override + public boolean isAccountNonExpired() { + return DEFAULT_IS_ACCOUNT_NON_EXPIRED; + } + + @Override + public boolean isAccountNonLocked() { + return DEFAULT_IS_ACCOUNT_NON_LOCKED; + } + + @Override + public boolean isCredentialsNonExpired() { + return DEFAULT_IS_CREDENTIALS_NON_EXPIRED; + } + + @Override + public boolean isEnabled() { + return DEFAULT_IS_ENABLED; + } +} diff --git a/sechub-commons-security-spring/src/test/resources/application-jwt-test.yml b/sechub-commons-security-spring/src/test/resources/application-jwt-test.yml new file mode 100644 index 0000000000..d8590ccf3c --- /dev/null +++ b/sechub-commons-security-spring/src/test/resources/application-jwt-test.yml @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: MIT + +sechub: + security: + oauth2: + mode: JWT + jwt: + jwk-set-uri: https://example.org/jwk-set-uri diff --git a/sechub-commons-security-spring/src/test/resources/application-opaque-token-test.yml b/sechub-commons-security-spring/src/test/resources/application-opaque-token-test.yml new file mode 100644 index 0000000000..fcaf903119 --- /dev/null +++ b/sechub-commons-security-spring/src/test/resources/application-opaque-token-test.yml @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: MIT + +sechub: + security: + oauth2: + mode: OPAQUE_TOKEN + opaque-token: + introspection-uri: https://example.org/introspection-uri + client-id: example-client-id + client-secret: example-client-secret diff --git a/sechub-commons-security-spring/src/test/resources/opaque-token-response.json b/sechub-commons-security-spring/src/test/resources/opaque-token-response.json new file mode 100644 index 0000000000..98890e105e --- /dev/null +++ b/sechub-commons-security-spring/src/test/resources/opaque-token-response.json @@ -0,0 +1,12 @@ +{ + "active": true, + "scope": "scope", + "client_id": "client_id", + "client_type": "client_type", + "username": "username", + "token_type": "token_type", + "exp": 1, + "sub": "sub", + "aud": "aud", + "group_type": "group_type" +} \ No newline at end of file diff --git a/sechub-developertools/build.gradle b/sechub-developertools/build.gradle index fbba4299a1..b7e3887037 100644 --- a/sechub-developertools/build.gradle +++ b/sechub-developertools/build.gradle @@ -44,12 +44,13 @@ task buildDeveloperAdminUI(type: Jar, dependsOn: build) { group 'sechub' description 'Builds the SecHub Developer Admin tool as standalone executable jar. Use launch-developer-admin-ui script to execute' archiveBaseName = 'sechub-developer-admin-ui' - + /* TODO: This is a 'dirty' fix for the standard archive entries limit of 64K. We should refactor this module to make it leaner */ + zip64 = true + manifest { attributes 'Main-Class': 'com.mercedesbenz.sechub.developertools.admin.ui.DeveloperAdministrationUI' } - from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) diff --git a/sechub-developertools/scripts/sdc.sh b/sechub-developertools/scripts/sdc.sh index fdbfe820a2..003ca50a2d 100755 --- a/sechub-developertools/scripts/sdc.sh +++ b/sechub-developertools/scripts/sdc.sh @@ -314,15 +314,7 @@ fi if [[ "$FORMAT_CODE_ALL" = "YES" ]]; then startJob "Format all sourcecode" - openApiFilePath="$SECHUB_ROOT_DIR/sechub-doc/build/api-spec/openapi3.json" - if [ -f "$openApiFilePath" ]; then - echo ">>> Open API file exists" - else - echo ">>> Open API file DOES NOT exist - must be generated." - # Problem detected: open api file must be generated to avoid problems with gradle configuration lifecycle for open api generator! - ./gradlew generateOpenapi - fi - ./gradlew spotlessApply -Dsechub.build.stage=all + ./gradlew spotlessApply fi diff --git a/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/DeveloperAdministration.java b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/DeveloperAdministration.java index 7e0304fcc5..5afb595157 100644 --- a/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/DeveloperAdministration.java +++ b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/DeveloperAdministration.java @@ -22,9 +22,11 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.mercedesbenz.sechub.commons.model.JSONConverter; +import com.mercedesbenz.sechub.commons.model.template.TemplateDefinition; import com.mercedesbenz.sechub.commons.pds.PDSDefaultParameterKeyConstants; import com.mercedesbenz.sechub.developertools.admin.ui.ConfigurationSetup; import com.mercedesbenz.sechub.developertools.admin.ui.UIContext; +import com.mercedesbenz.sechub.domain.scan.asset.AssetDetailData; import com.mercedesbenz.sechub.domain.scan.product.pds.PDSProductExecutorKeyConstants; import com.mercedesbenz.sechub.domain.scan.product.pds.SecHubProductExecutionPDSKeyProvider; import com.mercedesbenz.sechub.integrationtest.api.AsPDSUser; @@ -811,4 +813,49 @@ public SecHubEncryptionStatus fetchEncryptionStatus() { return asTestUser().fetchEncryptionStatus(); } + public TemplateDefinition fetchTemplateOrNull(String templateId) { + return asTestUser().fetchTemplateDefinitionOrNull(templateId); + + } + + public void createOrUpdateTemplate(String templateId, TemplateDefinition templateDefinition) { + asTestUser().createOrUpdateTemplate(templateId, templateDefinition); + } + + public void assignTemplateToProject(String templateId, String projectId) { + asTestUser().assignTemplateToProject(templateId, new FixedTestProject(projectId)); + } + + public void unassignTemplateFromProject(String templateId, String projectId) { + asTestUser().unassignTemplateFromProject(templateId, new FixedTestProject(projectId)); + } + + public List fetchAllTemplateIdentifiers() { + return asTestUser().fetchTemplateList(); + } + + public List fetchAllAssetIdentifiers() { + return asTestUser().fetchAllAssetIds(); + } + + public void uploadAssetFile(String assetId, File file) { + asTestUser().uploadAssetFile(assetId, file); + } + + public AssetDetailData fetchAssetDetails(String assetId) { + return asTestUser().fetchAssetDetails(assetId); + } + + public void deleteAsset(String assetId) { + asTestUser().deleteAsset(assetId); + } + + public void deleteAssetFile(String assetId, String fileName) { + asTestUser().deleteAssetFile(assetId, fileName); + } + + public File downloadAssetFile(String assetId, String fileName) { + return asTestUser().downloadAssetFile(assetId, fileName); + } + } diff --git a/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/CommandUI.java b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/CommandUI.java index 11a85dc139..cdbefba643 100644 --- a/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/CommandUI.java +++ b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/CommandUI.java @@ -20,6 +20,7 @@ import com.mercedesbenz.sechub.developertools.admin.ui.action.ActionSupport; import com.mercedesbenz.sechub.developertools.admin.ui.action.adapter.ShowProductExecutorTemplatesDialogAction; +import com.mercedesbenz.sechub.developertools.admin.ui.action.asset.ManageAssetsAction; import com.mercedesbenz.sechub.developertools.admin.ui.action.client.TriggerSecHubClientSynchronousScanAction; import com.mercedesbenz.sechub.developertools.admin.ui.action.config.ConfigureAutoCleanupAction; import com.mercedesbenz.sechub.developertools.admin.ui.action.config.ConfigurePDSAutoCleanupAction; @@ -96,6 +97,10 @@ import com.mercedesbenz.sechub.developertools.admin.ui.action.scheduler.RefreshSchedulerStatusAction; import com.mercedesbenz.sechub.developertools.admin.ui.action.status.CheckStatusAction; import com.mercedesbenz.sechub.developertools.admin.ui.action.status.ListStatusEntriesAction; +import com.mercedesbenz.sechub.developertools.admin.ui.action.template.AssignTemplateToProjectAction; +import com.mercedesbenz.sechub.developertools.admin.ui.action.template.CreateOrUpdateTemplateAction; +import com.mercedesbenz.sechub.developertools.admin.ui.action.template.FetchAllTemplateIdentifiersAction; +import com.mercedesbenz.sechub.developertools.admin.ui.action.template.UnassignTemplateFromProjectAction; import com.mercedesbenz.sechub.developertools.admin.ui.action.user.AcceptUserSignupAction; import com.mercedesbenz.sechub.developertools.admin.ui.action.user.AnonymousRequestNewAPITokenUserAction; import com.mercedesbenz.sechub.developertools.admin.ui.action.user.AnonymousSigninNewUserAction; @@ -250,6 +255,17 @@ public void createConfigMenu() { add(mappingsMenu, new UpdateGlobalMappingAction(context)); menu.add(new ConfigureAutoCleanupAction(context)); + + JMenu templatesMenu = new JMenu("Templates"); + menu.add(templatesMenu); + add(templatesMenu, new CreateOrUpdateTemplateAction(context)); + add(templatesMenu, new FetchAllTemplateIdentifiersAction(context)); + + add(templatesMenu, new AssignTemplateToProjectAction(context)); + add(templatesMenu, new UnassignTemplateFromProjectAction(context)); + + menu.add(new ManageAssetsAction(context)); + } public void createEncryptionMenu() { diff --git a/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/ManageAssetsDialogUI.java b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/ManageAssetsDialogUI.java new file mode 100644 index 0000000000..24abb248c4 --- /dev/null +++ b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/ManageAssetsDialogUI.java @@ -0,0 +1,280 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.developertools.admin.ui; + +import java.awt.BorderLayout; +import java.awt.event.ActionEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.io.File; +import java.util.List; +import java.util.Optional; + +import javax.swing.JFrame; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JScrollPane; +import javax.swing.JSplitPane; +import javax.swing.JTextArea; +import javax.swing.JToolBar; +import javax.swing.JTree; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; + +import com.mercedesbenz.sechub.developertools.admin.ui.action.AbstractUIAction; +import com.mercedesbenz.sechub.developertools.admin.ui.cache.InputCacheIdentifier; +import com.mercedesbenz.sechub.domain.scan.asset.AssetDetailData; +import com.mercedesbenz.sechub.domain.scan.asset.AssetFileData; + +public class ManageAssetsDialogUI { + + private JFrame frame; + private UIContext context; + private DefaultMutableTreeNode root; + private JTree tree; + private JTextArea textArea; + + public ManageAssetsDialogUI(UIContext context) { + this.context = context; + + frame = new JFrame(); + frame.setLayout(new BorderLayout()); + + createMenuBar(context); + + createToolBar(context); + + frame.setTitle("Manage assets"); + + frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + + frame.setSize(800, 600); + frame.setLocationRelativeTo(null); + + root = new DefaultMutableTreeNode(new AssetRootElement()); + DefaultTreeModel model = new DefaultTreeModel(root); + tree = new JTree(model); + + textArea = new JTextArea(); + JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, new JScrollPane(tree), new JScrollPane(textArea)); + frame.add(splitPane, BorderLayout.CENTER); + + tree.addMouseListener(new MouseAdapter() { + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() == 2) { + DefaultMutableTreeNode node = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent(); + if (node == null) { + return; + } + + Object userObject = node.getUserObject(); + if (userObject instanceof AssetRootElement) { + refreshModel(); + return; + } + if (userObject instanceof AssetElement) { + node.removeAllChildren(); + + AssetElement element = (AssetElement) userObject; + String assetId = element.assetId; + AssetDetailData detailData = context.getAdministration().fetchAssetDetails(assetId); + textArea.setText(detailData.toFormattedJSON()); + int added = 0; + for (AssetFileData info : detailData.getFiles()) { + node.add(new DefaultMutableTreeNode(new AssetFileElement(assetId, info.getFileName(), info.getChecksum()))); + added++; + } + element.info = added + " files"; + } + tree.repaint(); + } + } + }); + } + + private void createToolBar(UIContext context) { + JToolBar toolbar = new JToolBar(); + toolbar.add(new RefresAction(context)); + toolbar.addSeparator(); + toolbar.add(new UploadAssetFileAction(context)); + toolbar.add(new DownloadAssetFileAction(context)); + toolbar.addSeparator(); + toolbar.add(new DeleteAction(context)); + + frame.add(toolbar, BorderLayout.NORTH); + } + + private void createMenuBar(UIContext context) { + JMenuBar menuBar = new JMenuBar(); + JMenu menu1 = new JMenu("Actions"); + menuBar.add(menu1); + menu1.add(new RefresAction(context)); + menu1.addSeparator(); + menu1.add(new UploadAssetFileAction(context)); + menu1.add(new DownloadAssetFileAction(context)); + menu1.addSeparator(); + menu1.add(new DeleteAction(context)); + + frame.setJMenuBar(menuBar); + } + + public static void main(String[] args) { + new ManageAssetsDialogUI(null).show(); + } + + public void show() { + frame.setVisible(true); + } + + private void refreshModel() { + root.removeAllChildren(); + List assetIdentifiers = context.getAdministration().fetchAllAssetIdentifiers(); + textArea.setText("Loaded asset identifiers:\n" + assetIdentifiers); + AssetRootElement rootElement = (AssetRootElement) root.getUserObject(); + rootElement.info = "Loaded: " + assetIdentifiers.size(); + + for (String assetId : assetIdentifiers) { + root.add(new DefaultMutableTreeNode(new AssetElement(assetId))); + } + tree.setModel(new DefaultTreeModel(root)); + } + + private class AssetRootElement { + private String info = "Double click to load"; + + @Override + public String toString() { + return "assets (" + info + ")"; + } + } + + private class AssetElement { + private String assetId; + private String info = "Double click to load"; + + private AssetElement(String assetId) { + this.assetId = assetId; + } + + @Override + public String toString() { + return assetId + "(" + info + ")"; + } + + } + + private class AssetFileElement { + + private String checksum; + private String fileName; + private String assetId; + + public AssetFileElement(String assetId, String fileName, String checksum) { + this.assetId = assetId; + this.fileName = fileName; + this.checksum = checksum; + } + + @Override + public String toString() { + return fileName + " (" + checksum + ")"; + } + + } + + private class RefresAction extends AbstractUIAction { + + public RefresAction(UIContext context) { + super("Refresh", context); + } + + private static final long serialVersionUID = 7392018849800602872L; + + @Override + protected void execute(ActionEvent e) throws Exception { + refreshModel(); + } + + } + + private class UploadAssetFileAction extends AbstractUIAction { + + public UploadAssetFileAction(UIContext context) { + super("Upload", context); + } + + private static final long serialVersionUID = 7392018849800602872L; + + @Override + protected void execute(ActionEvent e) throws Exception { + + Optional assetIdOpt = getUserInput("Select asset id", InputCacheIdentifier.ASSET_ID); + if (assetIdOpt.isEmpty()) { + return; + } + File file = getContext().getDialogUI().selectFile(null); + if (file == null) { + return; + } + String assetId = assetIdOpt.get(); + getContext().getAdministration().uploadAssetFile(assetId, file); + } + + } + + private class DownloadAssetFileAction extends AbstractUIAction { + + public DownloadAssetFileAction(UIContext context) { + super("Download", context); + } + + private static final long serialVersionUID = 7392018849800602872L; + + @Override + protected void execute(ActionEvent e) throws Exception { + DefaultMutableTreeNode node = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent(); + Object userObject = node.getUserObject(); + + if (userObject instanceof AssetFileElement) { + AssetFileElement assetElement = (AssetFileElement) userObject; + File file = getContext().getAdministration().downloadAssetFile(assetElement.assetId, assetElement.fileName); + textArea.setText("Downloaded to:\n" + file.getAbsolutePath()); + } + } + + } + + private class DeleteAction extends AbstractUIAction { + + public DeleteAction(UIContext context) { + super("Delete", context); + } + + private static final long serialVersionUID = 7392018849800602872L; + + @Override + protected void execute(ActionEvent e) throws Exception { + DefaultMutableTreeNode node = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent(); + Object userObject = node.getUserObject(); + if (userObject instanceof AssetElement) { + AssetElement assetElement = (AssetElement) userObject; + String assetId = assetElement.assetId; + if (!getContext().getDialogUI().confirm("Do you really want to delete complete asset:" + assetId)) { + return; + } + getContext().getAdministration().deleteAsset(assetId); + + } else if (userObject instanceof AssetFileElement) { + AssetFileElement assetElement = (AssetFileElement) userObject; + String assetId = assetElement.assetId; + String fileName = assetElement.fileName; + + if (!getContext().getDialogUI().confirm("Do you really want to delete file:" + fileName + " from asset:" + assetId)) { + return; + } + getContext().getAdministration().deleteAssetFile(assetId, fileName); + } + } + + } + +} diff --git a/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/action/asset/ManageAssetsAction.java b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/action/asset/ManageAssetsAction.java new file mode 100644 index 0000000000..0368e97e66 --- /dev/null +++ b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/action/asset/ManageAssetsAction.java @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.developertools.admin.ui.action.asset; + +import java.awt.event.ActionEvent; + +import com.mercedesbenz.sechub.developertools.admin.ui.ManageAssetsDialogUI; +import com.mercedesbenz.sechub.developertools.admin.ui.UIContext; +import com.mercedesbenz.sechub.developertools.admin.ui.action.AbstractUIAction; + +public class ManageAssetsAction extends AbstractUIAction { + private static final long serialVersionUID = 1L; + + public ManageAssetsAction(UIContext context) { + super("Manage assets", context); + } + + @Override + public void execute(ActionEvent e) { + + ManageAssetsDialogUI ui = new ManageAssetsDialogUI(getContext()); + ui.show(); + + } + +} \ No newline at end of file diff --git a/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/action/template/AssignTemplateToProjectAction.java b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/action/template/AssignTemplateToProjectAction.java new file mode 100644 index 0000000000..10b63857a1 --- /dev/null +++ b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/action/template/AssignTemplateToProjectAction.java @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.developertools.admin.ui.action.template; + +import java.awt.event.ActionEvent; +import java.util.Optional; + +import com.mercedesbenz.sechub.commons.model.template.TemplateDefinition; +import com.mercedesbenz.sechub.developertools.admin.ui.UIContext; +import com.mercedesbenz.sechub.developertools.admin.ui.action.AbstractUIAction; +import com.mercedesbenz.sechub.developertools.admin.ui.cache.InputCacheIdentifier; + +public class AssignTemplateToProjectAction extends AbstractUIAction { + private static final long serialVersionUID = 1L; + + public AssignTemplateToProjectAction(UIContext context) { + super("Assign template to project", context); + } + + @Override + public void execute(ActionEvent e) { + Optional templateIdOpt = getUserInput("Please enter templateId", InputCacheIdentifier.TEMPLATE_ID); + if (!templateIdOpt.isPresent()) { + return; + } + String templateId = templateIdOpt.get(); + TemplateDefinition foundTemplate = getContext().getAdministration().fetchTemplateOrNull(templateId); + if (foundTemplate == null) { + error("The template " + templateId + " does not exist!"); + return; + } + Optional projectIdOpt = getUserInput("Please enter projectId", InputCacheIdentifier.PROJECT_ID); + if (!projectIdOpt.isPresent()) { + return; + } + String projectId = projectIdOpt.get(); + String projectInfo = getContext().getAdministration().fetchProjectInfo(projectId); + + if (projectInfo == null) { + error("The project " + projectId + " does not exist!"); + return; + } + + getContext().getAdministration().assignTemplateToProject(templateId, projectId); + + } + +} \ No newline at end of file diff --git a/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/action/template/CreateOrUpdateTemplateAction.java b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/action/template/CreateOrUpdateTemplateAction.java new file mode 100644 index 0000000000..3aa8002b15 --- /dev/null +++ b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/action/template/CreateOrUpdateTemplateAction.java @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.developertools.admin.ui.action.template; + +import java.awt.event.ActionEvent; +import java.util.Optional; + +import com.mercedesbenz.sechub.commons.model.template.TemplateDefinition; +import com.mercedesbenz.sechub.commons.model.template.TemplateDefinition.TemplateVariable; +import com.mercedesbenz.sechub.commons.model.template.TemplateDefinition.TemplateVariableValidation; +import com.mercedesbenz.sechub.commons.model.template.TemplateType; +import com.mercedesbenz.sechub.developertools.admin.ui.UIContext; +import com.mercedesbenz.sechub.developertools.admin.ui.action.AbstractUIAction; +import com.mercedesbenz.sechub.developertools.admin.ui.cache.InputCacheIdentifier; + +public class CreateOrUpdateTemplateAction extends AbstractUIAction { + private static final long serialVersionUID = 1L; + + public CreateOrUpdateTemplateAction(UIContext context) { + super("Create or update template", context); + } + + @Override + public void execute(ActionEvent e) { + Optional templateIdOpt = getUserInput("Please enter templateId", InputCacheIdentifier.TEMPLATE_ID); + if (!templateIdOpt.isPresent()) { + return; + } + + String dialogTitle = null; + String templateId = templateIdOpt.get(); + TemplateDefinition templateDefinition = getContext().getAdministration().fetchTemplateOrNull(templateId); + if (templateDefinition == null) { + /* we create an example here */ + + String title = "Create new template:" + templateId; + String message = "Please enter template type"; + Optional templateTypeOpt = getUserInputFromCombobox(title, TemplateType.WEBSCAN_LOGIN, message, TemplateType.values()); + if (!templateTypeOpt.isPresent()) { + return; + } + templateDefinition = TemplateDefinition.builder().templateId(templateId).templateType(templateTypeOpt.get()).assetId("example-asset-id").build(); + TemplateVariable exampleVariable = new TemplateVariable(); + exampleVariable.setName("example-variable"); + exampleVariable.setOptional(true); + TemplateVariableValidation validation = new TemplateVariableValidation(); + validation.setMinLength(2); + validation.setMaxLength(100); + validation.setRegularExpression("[0-9a-z].*"); + + exampleVariable.setValidation(validation); + templateDefinition.getVariables().add(exampleVariable); + + dialogTitle = "New Template:" + templateId + " (by example)"; + + } else { + dialogTitle = "Change existing template:" + templateId; + } + + Optional templateDefInputOpt = getUserInputFromTextArea(dialogTitle, templateDefinition.toFormattedJSON()); + if (templateDefInputOpt.isEmpty()) { + return; + } + + TemplateDefinition updatedTemplateDefinition = TemplateDefinition.from(templateDefInputOpt.get()); + + getContext().getAdministration().createOrUpdateTemplate(templateId, updatedTemplateDefinition); + } + +} \ No newline at end of file diff --git a/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/action/template/FetchAllTemplateIdentifiersAction.java b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/action/template/FetchAllTemplateIdentifiersAction.java new file mode 100644 index 0000000000..763a9b163b --- /dev/null +++ b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/action/template/FetchAllTemplateIdentifiersAction.java @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.developertools.admin.ui.action.template; + +import java.awt.event.ActionEvent; +import java.util.List; + +import com.mercedesbenz.sechub.developertools.admin.ui.UIContext; +import com.mercedesbenz.sechub.developertools.admin.ui.action.AbstractUIAction; + +public class FetchAllTemplateIdentifiersAction extends AbstractUIAction { + private static final long serialVersionUID = 1L; + + public FetchAllTemplateIdentifiersAction(UIContext context) { + super("Fetch all template identifiers", context); + } + + @Override + public void execute(ActionEvent e) { + List identifiers = getContext().getAdministration().fetchAllTemplateIdentifiers(); + output("Found template identiiers:\n" + identifiers.toString()); + } + +} \ No newline at end of file diff --git a/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/action/template/UnassignTemplateFromProjectAction.java b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/action/template/UnassignTemplateFromProjectAction.java new file mode 100644 index 0000000000..2ec9ef90c3 --- /dev/null +++ b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/action/template/UnassignTemplateFromProjectAction.java @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.developertools.admin.ui.action.template; + +import java.awt.event.ActionEvent; +import java.util.Optional; + +import com.mercedesbenz.sechub.developertools.admin.ui.UIContext; +import com.mercedesbenz.sechub.developertools.admin.ui.action.AbstractUIAction; +import com.mercedesbenz.sechub.developertools.admin.ui.cache.InputCacheIdentifier; + +public class UnassignTemplateFromProjectAction extends AbstractUIAction { + private static final long serialVersionUID = 1L; + + public UnassignTemplateFromProjectAction(UIContext context) { + super("Unassign template from project", context); + } + + @Override + public void execute(ActionEvent e) { + Optional templateIdOpt = getUserInput("Please enter templateId", InputCacheIdentifier.TEMPLATE_ID); + if (!templateIdOpt.isPresent()) { + return; + } + String templateId = templateIdOpt.get(); + Optional projectIdOpt = getUserInput("Please enter projectId", InputCacheIdentifier.PROJECT_ID); + if (!projectIdOpt.isPresent()) { + return; + } + String projectId = projectIdOpt.get(); + + getContext().getAdministration().unassignTemplateFromProject(templateId, projectId); + + } + +} \ No newline at end of file diff --git a/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/cache/InputCacheIdentifier.java b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/cache/InputCacheIdentifier.java index e6a9238208..931ff7b8d1 100644 --- a/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/cache/InputCacheIdentifier.java +++ b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/cache/InputCacheIdentifier.java @@ -50,4 +50,8 @@ public enum InputCacheIdentifier { PAGE_SIZE, + TEMPLATE_ID, + + ASSET_ID, + } \ No newline at end of file diff --git a/sechub-doc/build.gradle b/sechub-doc/build.gradle index b72cb03032..2a7a277fa1 100644 --- a/sechub-doc/build.gradle +++ b/sechub-doc/build.gradle @@ -17,6 +17,7 @@ dependencies { 'sechub-pds-tools', /* only pds tooling + avoid cycles */ 'sechub-api-java', /* the api project needs sechub-doc tests (and compile) for open api json files. So we may not have this as relation! */ 'sechub-systemtest', /* avoid cyclic dependency, see AdoptedSystemTestDefaultFallbacks javadoc for more information */ + 'sechub-wrapper-owasp-zap', ] /* fetch all sub projects, except unwanted and all only used for testing */ @@ -53,6 +54,7 @@ dependencies { for (String wantedProjectName: wanted){ implementation project(":${wantedProjectName}") } + implementation project(':sechub-shared-kernel') implementation library.apache_commons_io implementation spring_boot_dependency.jackson_core diff --git a/sechub-doc/helperscripts/publish+git-add-releasedocs.sh b/sechub-doc/helperscripts/publish+git-add-releasedocs.sh index 5358bb49b2..6a951e7e70 100755 --- a/sechub-doc/helperscripts/publish+git-add-releasedocs.sh +++ b/sechub-doc/helperscripts/publish+git-add-releasedocs.sh @@ -1,42 +1,97 @@ #!/bin/bash # SPDX-License-Identifier: MIT -FILE_LIST="" -SOURCE_DIR="build/docs/final-html" -DEST_DIR="../docs/latest" -IMAGE_DIR="images" GIT_RELEASE_BRANCH="master" +DOCS_FILE_LIST="" +DOCS_SOURCE_DIR="build/docs/final-html" +DEST_DIR="../docs" +DOCS_DEST_DIR="$DEST_DIR/latest" +DOCS_IMAGE_DIR="images" +WEBSITE_HOME="../sechub-website" +WEBSITE_BUILD_DIR="$WEBSITE_HOME/.output/public" +WEBSITE_FILE_LIST="" -function add_changed_images { - pushd "$SOURCE_DIR/" >/dev/null 2>&1 - local imagefiles=`ls $IMAGE_DIR/*` +function website_add_changed_files { + pushd "$WEBSITE_BUILD_DIR/" >/dev/null 2>&1 + local website_files=`/bin/ls | grep -v '_nuxt'` + popd >/dev/null 2>&1 + + echo -n "# Adding changed or new website files:" + for website_file in $website_files ; do + if ! cmp --silent "$WEBSITE_BUILD_DIR/$website_file" "$DEST_DIR/$website_file" ; then + echo -n " '$website_file'" + WEBSITE_FILE_LIST="$WEBSITE_FILE_LIST $website_file" + fi + done + + for file in $WEBSITE_FILE_LIST ; do + /bin/cp "$WEBSITE_BUILD_DIR/$file" "$DEST_DIR/$file" + echo "git add \"$DEST_DIR/$file\"" + git add "$DEST_DIR/$file" + done + echo +} + +function website_add_nuxt_files { + local nuxt_dir="$DEST_DIR/_nuxt" + + if [ -d "$nuxt_dir" ] ; then + echo "# Wiping _nuxt destination directory" + # because file names include checksums and thus change each time + git rm -rf "$nuxt_dir/" + fi + + echo "# Copying _nuxt directory" + /bin/cp -r "$WEBSITE_BUILD_DIR/_nuxt" "$DEST_DIR" + + pushd "$DEST_DIR/" >/dev/null 2>&1 + local nuxt_files=`find _nuxt/ -type f` + + for file in $nuxt_files ; do + echo "git add \"$file\"" + git add "$file" + done + popd >/dev/null 2>&1 +} + +function docs_collect_changed_images { + pushd "$DOCS_SOURCE_DIR/" >/dev/null 2>&1 + local imagefiles=`ls $DOCS_IMAGE_DIR/*` popd >/dev/null 2>&1 echo -n "# Adding changed or new image files:" for imagefile in $imagefiles ; do - if ! cmp --silent "$SOURCE_DIR/$imagefile" "$DEST_DIR/$imagefile" ; then + if ! cmp --silent "$DOCS_SOURCE_DIR/$imagefile" "$DOCS_DEST_DIR/$imagefile" ; then echo -n " '$imagefile'" - FILE_LIST="$FILE_LIST $imagefile" + DOCS_FILE_LIST="$DOCS_FILE_LIST $imagefile" fi done echo } -function add_changed_html_files { - pushd "$SOURCE_DIR/" >/dev/null 2>&1 +function docs_collect_changed_html_files { + pushd "$DOCS_SOURCE_DIR/" >/dev/null 2>&1 local htmlfiles=`ls *.html` popd >/dev/null 2>&1 echo -n "# Adding changed or new html files:" for htmlfile in $htmlfiles ; do - if ! cmp --silent "$SOURCE_DIR/$htmlfile" "$DEST_DIR/$htmlfile" ; then + if ! cmp --silent "$DOCS_SOURCE_DIR/$htmlfile" "$DOCS_DEST_DIR/$htmlfile" ; then echo -n " '$htmlfile'" - FILE_LIST="$FILE_LIST $htmlfile" + DOCS_FILE_LIST="$DOCS_FILE_LIST $htmlfile" fi done echo } +function docs_add_changed_files { + for file in $DOCS_FILE_LIST ; do + /bin/cp "$DOCS_SOURCE_DIR/$file" "$DOCS_DEST_DIR/$file" + echo "git add -f \"$DOCS_DEST_DIR/$file\"" + git add -f "$DOCS_DEST_DIR/$file" + done +} + ####################### cd "`dirname $0`/.." @@ -47,17 +102,23 @@ if [ "$BRANCH" != "$GIT_RELEASE_BRANCH" ] ; then exit 1 fi -# Update images directory (changed files only) -add_changed_images +echo "# SecHub website: Build and publish" +"$WEBSITE_HOME"/build-sechub-website.sh +echo + +# Get list of changes files +website_add_changed_files + +# Update _nuxt directory +website_add_nuxt_files + +# Docs: Update images directory (changed files only) +docs_collect_changed_images -# Add changed html files -add_changed_html_files +# Docs: Add changed html files +docs_collect_changed_html_files -# Copy files to destination and stage them for commit -for file in $FILE_LIST ; do - /bin/cp "$SOURCE_DIR/$file" "$DEST_DIR/$file" - echo "git add -f \"$DEST_DIR/$file\"" - git add -f "$DEST_DIR/$file" -done +# Copy docs files to destination and stage them for commit +docs_add_changed_files # Important: We do no git commit here - so everything the script does, can be undone. diff --git a/sechub-doc/src/docs/asciidoc/diagrams/diagram_templates-and-assets-big-picture.puml b/sechub-doc/src/docs/asciidoc/diagrams/diagram_templates-and-assets-big-picture.puml new file mode 100644 index 0000000000..559f69408c --- /dev/null +++ b/sechub-doc/src/docs/asciidoc/diagrams/diagram_templates-and-assets-big-picture.puml @@ -0,0 +1,39 @@ +@startuml + +skinparam linetype ortho + +actor user + +rectangle "SecHub configuration file" as config #lightgreen + +rectangle "Template data" as templateData #lightgreen + +rectangle "SecHub\nJob" as sechub + +rectangle "Access to extracted\nproduct specific template files\n(from asset)" as assetFile #darkorange + +rectangle "PDS\nJob" as pds +rectangle "PDS launcher script\nfor PDS product" as pdsLauncherScript + +rectangle "Template\ndefinition" as templateDefinition +rectangle "Project" as project +rectangle "Asset" as asset + +user->config +config ->sechub +config o--templateData + +pds ..> asset + +sechub ..> project +project ..> templateDefinition +sechub -> pds + +templateDefinition .> asset + +pds -> pdsLauncherScript + +pdsLauncherScript ..> assetFile + +@enduml + diff --git a/sechub-doc/src/docs/asciidoc/diagrams/diagram_templates-mapping-to-projects-events.puml b/sechub-doc/src/docs/asciidoc/diagrams/diagram_templates-mapping-to-projects-events.puml new file mode 100644 index 0000000000..509fbc4266 --- /dev/null +++ b/sechub-doc/src/docs/asciidoc/diagrams/diagram_templates-mapping-to-projects-events.puml @@ -0,0 +1,52 @@ +' SPDX-License-Identifier: MIT +@startuml + +'Hide empty parts: +hide empty fields +hide empty methods + +'You can find more examles at https://plantuml.com/class-diagram + + +node scan { + + class ScanProjectConfig { + String projectId + String key + String value + } +} + + +node administration{ + class Project { + String projectId + List templates + } + +} + +node eventBus as eventBus{ +} + +administration --> eventBus: REQUEST_ASSIGN_TEMPLATE_TO_PROJECT (1) + +eventBus--> scan: REQUEST_ASSIGN_TEMPLATE_TO_PROJECT (2) + +scan --> eventBus: RESULT_ASSIGN_TEMPLATE_TO_PROJECT (3) + + +note top of ScanProjectConfig +If the there exists already an entry for key "TEMPLATE_$templateType" +the config entry will be replaced. otherwise created. +end note + +note top of administration +After template mapping result is returned by scan domain, +the assigned template list will be updated. + +If a failure happend - e.g. template with given id does +not exist, the scan domain will return a failure +and this failure will be returned to caller side. +end note +@enduml diff --git a/sechub-doc/src/docs/asciidoc/diagrams/diagram_templates-mapping-to-projects.puml b/sechub-doc/src/docs/asciidoc/diagrams/diagram_templates-mapping-to-projects.puml new file mode 100644 index 0000000000..c88bb9438b --- /dev/null +++ b/sechub-doc/src/docs/asciidoc/diagrams/diagram_templates-mapping-to-projects.puml @@ -0,0 +1,61 @@ +' SPDX-License-Identifier: MIT +@startuml + +'Hide empty parts: +hide empty fields +hide empty methods + +'You can find more examles at https://plantuml.com/class-diagram + + +node scan { + + class Template { + String id + TemplateDefinition definition + } + + class TemplateDefinition { + TemplateType type + ... + + } + + class ScanProjectConfig { + String projectId + String key + String value + } +} + + +node administration{ + class Project { + String projectId + List templates + } + +} + +database "Scan\ndatabase" as DB1 { +} + +database "Administration\ndatabase" as DB2 { +} + +TemplateDefinition . Template +Template --> DB1 +ScanProjectConfig --> DB1 + +Project --> DB2 + + +note top of ScanProjectConfig +We use the existing concept of scan project +configuration to handle project related +template setup: + +The key will be always "TEMPLATE_$templateType" +and the value is the template identifier. +end note +@enduml diff --git a/sechub-doc/src/docs/asciidoc/documents/pds/pds_config.adoc b/sechub-doc/src/docs/asciidoc/documents/pds/pds_config.adoc index d4bc584562..9e798d1065 100644 --- a/sechub-doc/src/docs/asciidoc/documents/pds/pds_config.adoc +++ b/sechub-doc/src/docs/asciidoc/documents/pds/pds_config.adoc @@ -48,7 +48,7 @@ Either use system properties pds.techuser.userid pds.techuser.apitoken ---- -or env entries +or environment variables ---- PDS_TECHUSER_USERID PDS_TECHUSER_APITOKEN @@ -187,6 +187,12 @@ how the data is gathered. |PDS_JOB_SOURCECODE_ZIP_FILE | The absolute path to the uploaded "sourcecode.zip" file |PDS_JOB_EXTRACTED_SOURCES_FOLDER | When auto extracting is enabled (default) the uploaded source code is extracted to this folder |PDS_JOB_EXTRACTED_BINARIES_FOLDER | When auto extracting is enabled (default) the uploaded binaries are extracted to this folder +|PDS_JOB_EXTRACTED_ASSETS_FOLDER | The absolute path to the extracted assets. + + + + Files for template types are located in dedicated sub folders which are named by the id of the template type. + + + + + For example: WEBSCAN_LOGIN template type has id: `webscan-login`. To access the file `custom-login.groovy` a script + or a wrapper application can simply fetch the file via `$PDS_JOB_EXTRACTED_ASSETS_FOLDER/webscan-login/custom-login.groovy`. |PDS_SCAN_TARGET_URL | Target URL for current scan (e.g webscan). Will not be set in all scan types. E.g. for a code scan this environemnt variable will not be available |PDS_SCAN_CONFIGURATION | Contains the SecHub configuration as JSON _(but reduced to current scan type, so e.g. a web scan will have no code scan configuration data available)_ + + @@ -216,10 +222,15 @@ include::../gen/gen_pds_executor_config_parameters.adoc[] ==== File locations ===== Upload -`$PDS_JOB_WORKSPACE_LOCATION/upload/` +Content from uploaded user archives is extracted to: + +`PDS_JOB_EXTRACTED_SOURCES_FOLDER`, + +`PDS_JOB_EXTRACTED_BINARIES_FOLDER` + +Content from uploaded asset files is extracted to: + `PDS_JOB_EXTRACTED_ASSETS_FOLDER`, -Automatically unzipped content is available inside + -`$PDS_JOB_WORKSPACE_LOCATION/upload/unzipped` ===== Output diff --git a/sechub-doc/src/docs/asciidoc/documents/shared/concepts/concept_templates_and_assets.adoc b/sechub-doc/src/docs/asciidoc/documents/shared/concepts/concept_templates_and_assets.adoc index 2e9492cfa9..bd1361d3c7 100644 --- a/sechub-doc/src/docs/asciidoc/documents/shared/concepts/concept_templates_and_assets.adoc +++ b/sechub-doc/src/docs/asciidoc/documents/shared/concepts/concept_templates_and_assets.adoc @@ -7,70 +7,101 @@ and - depending on the product - very hard to provide a complete generic approac only. ===== Example situation -To clarify this abstract situation here an example: + +To clarify this abstract situation here an example: Think about a wide range of applications inside +a corporation which want to use a common single sign on with multi factor authentication and some +other specific parts. The login configuration in {sechub} configuration file by defining explicit steps, pages etc. +would become complex and error prone because : -Think about a wide range of applications inside a corporation which want to use a common -single sign on with multi factor authentication and some other specific parts. - -The login configuration would become complex and error prone because not every user would -understand exactly what to do. If the single-signon mechanism would change, every project/user -would need to change their {sechub} configuration files! When talking about hundreds of projects -this would be a great effort - for users and for support! +- not every user would understand exactly what to do +- if the single signon mechanism ever changes, every project/user would need to change their {sechub} configuration files! + When talking about hundreds of projects this would be a great effort - for users and for support! And let us assume the used product has hundreds of options which cannot be configured by a generic approach easily (e.g. form login definition in configuration): How could this product -be provided in a desired way then? - -And what if there are some specific parts which are not 100% same in every of these projects? +be provided in a desired way then? And what if there are some specific parts which are not 100% same +in every of these projects? This is the reason for the "templates and assets" concept which is described below. +===== Big picture +plantuml::./diagrams/diagram_templates-and-assets-big-picture.puml[format=svg, title="Big picture of templates and assets"] + ===== Templates -{sechub} can have multiple templates . Every template has a type and can contains a set of -assets and also a variable definition. It can be administrated via -REST end points: +{sechub} can have multiple templates. Every template has a type and can contains a set of +assets and also a variable definition. It can be administrated via REST end points. -``` -PUT /api/administration/template/$template-id -GET /api/administration/template/$template-id -DELETE /api/administration/template/$template-id -``` +====== Template id format +A template id -PUT will contain following body: +- has a minimum length of 3 +- has a maximum length of 40 +- can contain only `a-z` `A-Z`, `0-9` or `-` or `_` + +====== Template definition +The template definitions are hold inside {sechub} database. +A template is defined by following json syntax: +.Template definition syntax [source,json] ---- -{ - "type" : "webscan-login",//<1> - "assets" : ["asset-id-1"]//<2> - "variables" : {//<3> - "username" : "mandatory",//<4> - "password" : "mandatory", - "tip-of-the-day" : "optional"//<5> - } -} +include::../snippet/template-definition-syntax.json[] ---- -<1> The type of the template. Currently possible: `webscan-login` -<2> Array with asset identifiers assigned to the template -<3> Variable definitions as list of key and value pairs which can be mandatory or optional. +<1> The template identifier +<2> The type of the template. Must be set when a template is created. Will + be ignored on updates. Possible values are: `webscan-login` +<3> Asset identifier for the template +<4> Variable definitions as list of key and value pairs which can be mandatory or optional. Via the type {sechub} server is able to check if the configuration is valid and give response to users when job starts. +<5> Name of the variable +<6> Describes if the variable is optional. + + When `false` configuration must contain the variable inside template data. When `true` + the configuration is valid without the variable. The default is `false`. +<7> Variable content validation definition ((optional) +<8> Minimum length (optional) +<9> Maximum length (optional) +<10> Regular expression (optional). If defined, the content must match the given regular expression + to be valid. + +[CAUTION] +==== +The validation section inside the definition is only for a "first simple check" on {sechub} side, to +stop before any {pds} job is created. -===== Mapping templates to projects -``` -PUT /api/admin/project/$projectId/template/$templateid -DELETE /api/admin/project/$projectId/template/$templateid -GET /api/admin/project/$projectId/template/list -``` +*But {pds} solutions which are using templates must ensure,that the given user content +(variable) is correct and not some malicious injected data!* +==== +====== Mapping templates to projects -[IMPORTANT] +plantuml::./diagrams/diagram_templates-mapping-to-projects.puml[format=svg, title="Mapping of templates and projects"] + +As shown in the figure above, the template data is hold inside domain "scan". The reason is, that +template details are necessary inside the scan operation and no where else. + +The association between project and used templates is done in administration domain because the project +entity and its details are used here. At the `administration` domain only the template identifiers +used by this project are necessary. + +A mapping of templates to a project is dependent on the template type: Same type of a template cannot +be added twice to a project! Because the `administration` domain does not know about the template details, +a synchronous event to `scan` domain will be sent to assign the template to the project. + +The scan domain will update this information inside `ScanProjectConfig` entities and will drop +a former assigned template of same type from the config. + +After this is done as a syncron result the scan domain will return all assigned +templates, as shown in next figure: + +plantuml::./diagrams/diagram_templates-mapping-to-projects-events.puml[format=svg, title="Mapping of templates and projects (Events)"] + +[NOTE] ==== The mapping is stored with template type as part of the composite key: If two template definitions with same type are uploaded, the last one will overwrite the first one. ==== -===== SecHub configuration file +====== Templates in SecHub configuration file The users can now define in their {sechub} configuration file that they want to use the configured template for web scanning by defining a templateData section inside the login configuration. @@ -84,52 +115,29 @@ include::../configuration/sechub_config_example22_webscan_with_template.json[] is not able to define which template is used. <2> Setup template variables by a list of key and value pairs. -[IMPORTANT] -==== If the user has defined a `templateData` section in the configuration but no template of that type is assigned to the project, a dedicated error message must be returned to the user and the {sechub} job will always fail. -==== - -===== Asset storage -The assets are initially stored in {sechub} database, together with the asset file checksum. - -When a template is used inside a {sechub} configuration, the {sechub} start mechanism will -check if the file from database is already available at storage (S3/NFS) which represents -a cache for the assets. - -If available, the cached part will be used. Otherwise it will be uploaded -(see <> ) - - -[TIP] -==== -Main reason why we store always in DB and use storage (S3/NFS) as a cache only: - -1. Restore a DB backup on a fresh installation: + - Administrators can just apply the database backup and everything works again -2. Storage could be volatile/be deleted etc. (we rely to the database) -3. For having two clusters ( {sechub} and {pds} having not same database we need - a way to exchange. -==== - [IMPORTANT] ==== -For templates and assets we use the shared storage between {sechub} and {pds} . -solutions and not to provide own storage for {pds}. Diffeent storages are not supported here. +For templates and assets we must use shared storage between {sechub} and {pds}. {pds} solutions may +not to provide own storage for {pds} - different storages are not supported here. ==== -`/assets/$assetId/` +===== Assets +====== Asset id format +An asset id -An asset id has - -- maximum length of 40 +- has a minimum length of 3 +- has a maximum length of 40 - can contain only `a-z` `A-Z`, `0-9` or `-` or `_` -At the storage for each product (as named in the `pds-config.json` of the {pds} solution), there will be a ZIP file having the -product identifier as filename +====== {sechub} server uses product identifiers for asset ids +The {sechub} server will use the product identifiers from {pds} executor configurations +as file names when storing asset data inside database and storage. The product identifiers +are also defined inside the {pds} server configuration files. -For example: +Here an example for storage paths: ``` /assets/ asset-id-1/ @@ -138,39 +146,62 @@ For example: OTHER_PRODUCT_X.zip OTHER_PRODUCT_X.zip.sha256 ``` -[TIP] +[IMPORTANT] ==== -The content and the structure inside the ZIP files is absolute solution specific : -The PDS solution defines which kind of template data is necessary and how the -structure looks like! +The content and the structure inside the ZIP files is absolute solution specific: +Means the {pds} solution defines which kind of template data is necessary and how the +structure looks like. Because of this necessary template variables, file structure for +asset zip files etc. must be documented inside the {pds} solution! ==== -===== Administration REST end points for storage +====== Asset operations +*General* + +*Usecases* + +- Admin creates or updates asset +- + Over REST API Administrators will be able to -- list (ZIP) file names + - `GET /api/administration/asset/$assetId/list` -- upload asset ZIP files by REST API and create checksum file + - `PUT /api/administration/asset/$assetId/$productId` -- download asset files by REST API + - `GET /api/administration/asset/$assetId/$productId` -- delete single asset file by REST API + - `DELETE /api/administration/asset/$assetId/$productId` -- delete all asset files by REST API + - `DELETE /api/administration/asset/$assetId` +- upload asset ZIP files by REST API and create checksum file +- download asset files by REST API +- list asset details (filenames and checksums) +- delete single asset file by REST API +- delete all asset data by REST API The REST API will always create an audit log entry +*Upload* + +The upload and delete operations will always handle DB and storage (S3/NFS)! +If an asset ZIP file already exists, the operation will overwrite the file and the +old sha256 checksum in both locations. + +[TIP] +==== +Main reason why we store always in DB and use storage (S3/NFS) as a cache only: + +1. Restore a DB backup on a fresh installation: + + Administrators can just apply the database backup and everything works again +2. Storage could be volatile/be deleted etc. (we rely to the database) +3. For having multiple and also different cluster types ( {sechub} cluster and multiple {pds} clusters) + having not same database we need a common way to exchange data. + Each {pds} cluster and also the {sechub} cluster have their own + database - means cannot be used for exchange. +==== + + [IMPORTANT] ==== This works only when the storage is shared between {sechub} and {pds}. If the {pds} uses its own storage (which should NOT be done for production, but only -for PDS solution development) the assets must be uploaded directly to the PDS storage -location! +for PDS solution development) the assets would needed to be uploaded directly to the PDS storage +location with correct checksum etc. ==== -===== SecHub template and asset handling -====== Validation +===== SecHub runtime +====== Validate config uses valid template location When a {sechub} job starts and there is a template data definition inside the configuration, {sechub} will validate if the project has a template assigned for the location inside the configuration. e.g. templates with type `webscan-login` may only be defined inside @@ -179,51 +210,43 @@ web scan login configuration). If this validation fails, the complete {sechub} job will fail and stop processing. [[sechub-concept-asset-upload-lazy]] -====== Upload in storage as cache lazily -When validation did not fail, {sechub} will check if the current version of the asset is already uploaded to -storage (S3/NFS) already. +====== Validate config uses valid template location + +When validation did not fail, {sechub} will check if the current version of the +necessary product specific asset ZIP file is exsting in storage. -When not uploaded to storage, the file will be uploaded before the job is further processed. +When not uploaded to storage, the file will be uploaded before the job is +further processed. ====== Template PDS parameter calculation -If validation and lazy upload did not fail, {sechub} will calculate a -{pds} parameter : `pds.template.metadata` with JSON (see parameter syntax for details). +If the job configuration has valid template data and the template +is available in storage, {sechub} will calculate the +{pds} parameter : `pds.config.template.metadata.list` with JSON: -.PDS parameter syntax for template meta data +[[sechub-concept-template-metadata-example]] +.PDS parameter syntax for template meta data [source,json] ---- -{ - "template-metadata" : [ //<1> - { - "template" : "single-singon", //<2> - "type": "webscan-login",//<3> - "assets" : [ //<4> - { - "id" : "asset-id-1", //<5> - "sha256" : "434c6c6ec1b0ed9844149069d7d45ac18e72505b" //<6> - } - ] - }] - } -} - +include::../snippet/pds-param-template-metadata-syntax.json[] ---- <1> Meta data array <2> Template identifier - just as an information for logging etc. <3> Template type <4> Asset information array -<5> ID of the asset, necessary for storage download -<6> checksum of the asset ZIP file, necessary to check after download +<5> Asset identifier +<6> Name of file (inside asset) +<7> Checksum of the file (SHA256) -===== PDS asset handling +===== PDS runtime +====== Asset handling {sechub} calls {pds}, with {pds} parameter `pds.template.metadata` (syntax is described above). -The {pds} intance will fetch all wanted asset ZIP file for the current product -from storage (S3 or NFS) and extract it to `$workspaceFolder/assets/`. +The {pds} instance will fetch all defined files from storage (S3 or NFS) +and extract/copy it to `$workspaceFolder/assets/`. -Before extraction is done a checksum for the downloaded ZIP file is created and compared -with the checksum from template meta data. If it is different the {pds} job will fail -with a dedicated error message. +Before extraction is done a checksum for the downloaded file is created and compared +with the checksum from template meta data. The checksum algorithm is SHA256. +If it is different the {pds} job will fail with a dedicated error message. If the checksum is valid, the assets ZIP file will be unzipped below a subfolder with the template type: @@ -238,66 +261,74 @@ template type: An example: -The `MY_PRODUCT.zip` file contains +The `WEBSCAN_PRODUCT_ID.zip` file contains [source,text] ---- -/script.js +/script.groovy /development/ debug-settings.json ---- and the template meta data looks like this: -[source,text] +[source,json] ---- -{ - "template-metadata" : { - { - "type: "webscan-login", - "assets" : [ - { - "id" : "asset-id-1", - "sha256" : "434c6c6ec1b0ed9844149069d7d45ac18e72505b" - } - ] - } - } -} +include::../snippet/pds-param-template-metadata-example1.json[] ---- +The extraction is done into folder: +`$workspaceFolder/assets/$templateType/` which will be made available to launcher scripts (and wrapper +applications) by environmnet variable `PDS_JOB_EXTRACTED_ASSETS_FOLDER`. -It will be extracted the following way. +For the former example `WEBSCAN_PRODUCT_ID.zip` will be extracted the following way: [source,text] ---- `$workspaceFolder/ assets/ webscan-login/ - script.js - develoment/ + script.groovy + development/ debug-settings.json ---- -====== Pseudo code for usage inside PDS solutions/wrappers +====== Example code for usage inside PDS solutions/wrappers -Here an example (but pseudo code) how a product could use the assets inside: +Here an example how a product could use the assets inside +a wrapper application for a PDS solution: [source,java] ---- -WebScanTemplateData data = util.fetchWebScanTemplateData(sechubConfig); -if (data!=null){ - // folder=/$workspaceFolder/assets - File folder = util.getAssetFolder(); +import java.nio.file.*; +import com.mercedesbenz.sechub.commons.model.JSONConverter; +import com.mercedesbenz.sechub.commons.model.template.*; +// ... + +String sechubConfigAsJson = System.getenv("PDS_SCAN_CONFIGURATION"); +SecHubConfigurationModel sechubConfig = JSONConverter.get().fromJSON(SecHubConfigurationModel.class,sechubConfigAsJson); + +TemplateDataResolver resolver = new TemplateDataResolver(); + +TemplateData templateData = resolver.resolveTemplateData(TemplateType.WEBSCAN_LOGIN, sechubConfig); +if (templateData!=null){ + + Map variables = templateData.getVariables(); + + // read template script file from extracted folder + String assetExtractionFolder = System.getenv("PDS_JOB_EXTRACTED_ASSETS_FOLDER"); + Path path = Paths.get(assetExtractionFolder, TemplateType.WEBSCAN_LOGIN.getId(), "script.groovy"); + String scriptTemplate = Files.readString(path); - // in example: it is a web scan... - script = folder.getChild("webscan-login/script.js"); + // replace variable parts inside template + String script = scriptTemplate ; + script = script.replaceAll("#user", variales.get("user"); + script = script.replaceAll("#pwd", variales.get("password"); - script = replaceVariableInScript(data.getVariable("username"), script); - script = replaceVariableInScript(data.getVariable("password"), script); + // execute script + // ... - // use script.... } ---- diff --git a/sechub-doc/src/docs/asciidoc/documents/shared/configuration/sechub_config.adoc b/sechub-doc/src/docs/asciidoc/documents/shared/configuration/sechub_config.adoc index 39c0d463a6..1a0df3d6d2 100644 --- a/sechub-doc/src/docs/asciidoc/documents/shared/configuration/sechub_config.adoc +++ b/sechub-doc/src/docs/asciidoc/documents/shared/configuration/sechub_config.adoc @@ -328,6 +328,15 @@ The currently available hash algorithms are: - `HMAC_SHA1` - `HMAC_SHA256` - `HMAC_SHA512` +<6> The `encodingType` is an __optional__ field, representing the encoding of the __mandatory__ field `seed`. +{sechub} has a default configured if nothing is specified or the encoding type is not known. +The default value is `AUTODETECT` where {sechub} tries to detect the encoding of one of the four other available types. + The currently available encoding types for `seed` are, which are treated case-insensitive: +- `BASE64` +- `BASE32` +- `HEX` +- `PLAIN` +- `AUTODETECT` [[sechub-config-example-webscan-openapi]] ====== Example OpenAPI scan diff --git a/sechub-doc/src/docs/asciidoc/documents/shared/configuration/sechub_config_example21_webscan_login_form_with_totp.json b/sechub-doc/src/docs/asciidoc/documents/shared/configuration/sechub_config_example21_webscan_login_form_with_totp.json index 989a54b318..110468808a 100644 --- a/sechub-doc/src/docs/asciidoc/documents/shared/configuration/sechub_config_example21_webscan_login_form_with_totp.json +++ b/sechub-doc/src/docs/asciidoc/documents/shared/configuration/sechub_config_example21_webscan_login_form_with_totp.json @@ -27,7 +27,8 @@ "seed" : "example-seed", //<2> "validityInSeconds" : 60, //<3> "tokenLength" : 8, //<4> - "hashAlgorithm" : "HMAC_SHA256" //<5> + "hashAlgorithm" : "HMAC_SHA256", //<5> + "encodingType" : "base64" //<6> } } } diff --git a/sechub-doc/src/docs/asciidoc/documents/shared/snippet/pds-param-template-metadata-example1.json b/sechub-doc/src/docs/asciidoc/documents/shared/snippet/pds-param-template-metadata-example1.json new file mode 100644 index 0000000000..9f67da8c86 --- /dev/null +++ b/sechub-doc/src/docs/asciidoc/documents/shared/snippet/pds-param-template-metadata-example1.json @@ -0,0 +1,9 @@ +[ { + "templateId" : "single-singon", + "templateType" : "webscan-login", + "assetData" : { + "assetId" : "custom-webscan-setup", + "fileName" : "WEBSCAN_PRODUCT_ID.zip", + "checksum" : "434c6c6ec1b0ed9844149069d7d45ac18e72505b" + } +} ] \ No newline at end of file diff --git a/sechub-doc/src/docs/asciidoc/documents/shared/snippet/pds-param-template-metadata-syntax.json b/sechub-doc/src/docs/asciidoc/documents/shared/snippet/pds-param-template-metadata-syntax.json new file mode 100644 index 0000000000..85e400cf1b --- /dev/null +++ b/sechub-doc/src/docs/asciidoc/documents/shared/snippet/pds-param-template-metadata-syntax.json @@ -0,0 +1,13 @@ + [ //<1> + { + "templateId" : "templateId", //<2> + "templateType": "WEBSCAN_LOGIN", //<3> + + "assetData" : { //<4> + "assetId" : "assetId", //<5> + "fileName" : "fileName", //<6> + "checksum" : "fileChecksum" //<7> + } + + } + ] diff --git a/sechub-doc/src/docs/asciidoc/documents/shared/snippet/template-definition-syntax.json b/sechub-doc/src/docs/asciidoc/documents/shared/snippet/template-definition-syntax.json new file mode 100644 index 0000000000..b15de0e9fa --- /dev/null +++ b/sechub-doc/src/docs/asciidoc/documents/shared/snippet/template-definition-syntax.json @@ -0,0 +1,26 @@ +{ + "templateDefinition" : { + + "id" : "$templateId", //<1> + + "type" : "$templateType", //<2> + + "assetId" : "$assetId", //<3> + + "variables" : [ //<4> + { + "name" : "$variableName", // <5> + "optional": false, // <6> + + "validation" : { // <7> + + "minLength" : 1, // <8> + "maxLength" : 100, // <9> + + "regularExpression" : "$regularExpression" // <10> + } + } + ] + } + +} \ No newline at end of file diff --git a/sechub-doc/src/docs/asciidoc/sechub-getting-started.adoc b/sechub-doc/src/docs/asciidoc/sechub-getting-started.adoc index bfc703cf73..3cf24bcf26 100644 --- a/sechub-doc/src/docs/asciidoc/sechub-getting-started.adoc +++ b/sechub-doc/src/docs/asciidoc/sechub-getting-started.adoc @@ -186,9 +186,10 @@ Setup of GoSec complete: Now you are ready to do scans! -== Scan using SecHub client -=== Install SecHub client -The SecHub client is needed to scan. In later sections of this guide, the client is used to scan an example. The command below, will download the latest version and put it in your `/usr/local/bin` folder. +== Scan using SecHub Client +=== Install SecHub Client +The SecHub Client is needed to scan. In later sections of this guide, the client is used to scan an example. +The command below, will download the latest version and put it in your `/usr/local/bin` folder. [source, bash] -- @@ -196,24 +197,21 @@ The SecHub client is needed to scan. In later sections of this guide, the client CLIENT_VERSION=`curl -s https://mercedes-benz.github.io/sechub/latest/client-download.html | grep https://github.com/mercedes-benz/sechub/ | awk -F '-' '{print $NF}' | sed 's/.zip">//'` # Download the zipped binary -curl -L -o sechub-cli.zip https://github.com/mercedes-benz/sechub/releases/download/v$CLIENT_VERSION-client/sechub-cli-$CLIENT_VERSION.zip +wget https://github.com/mercedes-benz/sechub/releases/download/v$CLIENT_VERSION-client/sechub-cli-$CLIENT_VERSION.zip # Verify the binary -curl -L -o sechub-cli.zip.sha256 https://github.com/mercedes-benz/sechub/releases/download/v$CLIENT_VERSION-client/sechub-cli-$CLIENT_VERSION.zip.sha256 -sha256sum --check sechub-cli.zip.sha256 +wget https://github.com/mercedes-benz/sechub/releases/download/v$CLIENT_VERSION-client/sechub-cli-$CLIENT_VERSION.zip.sha256 +sha256sum --check sechub-cli-$CLIENT_VERSION.zip.sha256 # Extract -unzip sechub-cli.zip +unzip sechub-cli-$CLIENT_VERSION.zip # Depending on your architecture and OS, you will have to copy a different binary file: -# For linux x86-64 +# Example for Linux x86-64 sudo cp platform/linux-amd64/sechub /usr/local/bin -# For linux arm-64 -sudo cp platform/linux-arm64/sechub /usr/local/bin - # Cleanup -rm -rf sechub-cli.zip.sha256 sechub-cli.zip platform/ +rm -rf sechub-cli-$CLIENT_VERSION.zip sechub-cli-$CLIENT_VERSION.zip.sha256 platform/ -- === Scan @@ -249,10 +247,19 @@ Now you can do a scan, type `sechub scan`, this will create a file which contain If you want the report in HTML format instead, add `-reportformat html` as an option: `sechub -reportformat html scan` == Optional -=== Install SecHub's VSCodium Plugin (OPTIONAL) -SecHub's VSCodium Plugin helps you to work faster with the SecHub report. -You can go to the exact code line and fix the problem. +=== SecHub's IDE plugins +SecHub's IDE plugins help you to work faster with the SecHub report. +You can jump to the exact code lines and fix the problem. + +==== Eclipse Plugin +You can get the SecHub plugin the usual way from the https://marketplace.eclipse.org/content/sechub[Eclipse Marketplace]. + +==== IntelliJ Plugin +You can get the SecHub plugin the usual way from the https://plugins.jetbrains.com/plugin/23379-sechub[IntelliJ Marketplace]. + +==== VS-Codium / Visual Studio Code Plugin +You can get the plugin from the https://open-vsx.org/extension/mercedes-benz/sechub[OpenVSX Registry]. -You can get the plugin from https://open-vsx.org/extension/mercedes-benz/sechub[here]. +To install it, search for `sechub` in the Extensions manager and choose the one from "mercedes-benz". -And to install it, open VSCodium and in the `Command Palette` (Usually can be opened with `Ctrl+Shift+P`) type `install vsix`, and in the pop-up menu, choose the plugin. +For Visual Studio Code users: Download the Plugin and install it manually from file. diff --git a/sechub-doc/src/main/java/com/mercedesbenz/sechub/docgen/AsciidocGenerator.java b/sechub-doc/src/main/java/com/mercedesbenz/sechub/docgen/AsciidocGenerator.java index 58f90fed04..3d2019c74d 100644 --- a/sechub-doc/src/main/java/com/mercedesbenz/sechub/docgen/AsciidocGenerator.java +++ b/sechub-doc/src/main/java/com/mercedesbenz/sechub/docgen/AsciidocGenerator.java @@ -5,6 +5,8 @@ import java.io.IOException; import java.util.Map; import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; import org.apache.commons.io.FileUtils; import org.slf4j.LoggerFactory; @@ -347,10 +349,33 @@ public void fetchDomainMessagingParts() { } public void generateSecHubSystemPropertiesDescription(GenContext context) throws IOException { - String text = propertiesGenerator.generate(getCollector().fetchMustBeDocumentParts(), context.sechubEnvVariableRegistry); + String text = propertiesGenerator.generate(getCollector().fetchMustBeDocumentParts(), context.sechubEnvVariableRegistry, createCustomPropertiesMap()); writer.writeTextToFile(context.systemProperitesFile, text); } + /** + * Creates a map with custom properties for the system properties documentation + * This is a workaround for @ConfigurationProperties classes, since currently + * there is no support for auto-doc generation for these classes. + * + *

+ * TODO: This should be removed in the future + *

+ */ + private Map> createCustomPropertiesMap() { + /* @formatter:off */ + return Map.of("oauth2-jwt", new TreeSet<>( + Set.of( + new SystemPropertiesDescriptionGenerator.TableRow("sechub.security.oauth2.mode", null, "The OAuth2 mode to use for authentication. Must be either 'JWT' or 'OPAQUE_TOKEN'.", false), + new SystemPropertiesDescriptionGenerator.TableRow("sechub.security.oauth2.jwt.jwk-set-uri", null, "The URI to the JWK set of the identity provider. Required in 'JWT' mode.", false), + new SystemPropertiesDescriptionGenerator.TableRow("sechub.security.oauth2.opaque-token.introspection-uri", null, "Opaque token introspection endpoint of the identity provider. Required in 'OPAQUE_TOKEN' mode.", false), + new SystemPropertiesDescriptionGenerator.TableRow("sechub.security.oauth2.opaque-token.client-id", null, "Client ID to use when authenticating against the identity provider. Required in 'OPAQUE_TOKEN' mode.", false), + new SystemPropertiesDescriptionGenerator.TableRow("sechub.security.oauth2.opaque-token.client-secret", null, "Client secret to use when authenticating against the identity provider. Required in 'OPAQUE_TOKEN' mode.", false) + )) + ); + /* @formatter:on */ + } + public void generatePDSSystemPropertiesDescription(GenContext context) throws IOException { String text = propertiesGenerator.generate(getCollector().fetchPDSMustBeDocumentParts(), context.pdsEnvVariableRegistry); writer.writeTextToFile(context.pdsSystemProperitesFile, text); diff --git a/sechub-doc/src/main/java/com/mercedesbenz/sechub/docgen/spring/SystemPropertiesDescriptionGenerator.java b/sechub-doc/src/main/java/com/mercedesbenz/sechub/docgen/spring/SystemPropertiesDescriptionGenerator.java index aa05dbef87..5e261e76a2 100644 --- a/sechub-doc/src/main/java/com/mercedesbenz/sechub/docgen/spring/SystemPropertiesDescriptionGenerator.java +++ b/sechub-doc/src/main/java/com/mercedesbenz/sechub/docgen/spring/SystemPropertiesDescriptionGenerator.java @@ -25,16 +25,27 @@ public SystemPropertiesDescriptionGenerator() { } public String generate(List list, SecureEnvironmentVariableKeyValueRegistry registry) { - return generate(list, registry, AcceptAllSpringValueFilter.INSTANCE); + return generate(list, registry, AcceptAllSpringValueFilter.INSTANCE, null); } - protected String generate(List list, SecureEnvironmentVariableKeyValueRegistry registry, SpringValueFilter filter) { + public String generate(List list, SecureEnvironmentVariableKeyValueRegistry registry, + Map> customPropertiesMap) { + return generate(list, registry, AcceptAllSpringValueFilter.INSTANCE, customPropertiesMap); + } + + protected String generate(List list, SecureEnvironmentVariableKeyValueRegistry registry, SpringValueFilter filter, + Map> customPropertiesMap) { if (list == null || list.isEmpty()) { return ""; } StringBuilder sb = new StringBuilder(); Map> rowMap = new TreeMap<>(); + + if (customPropertiesMap != null) { + rowMap.putAll(customPropertiesMap); + } + for (DocAnnotationData data : list) { if (SimpleStringUtils.isEmpty(data.springValue)) { continue; @@ -94,12 +105,22 @@ private String buildTitle(String key) { return "Scope '" + key + "'"; } - class TableRow implements Comparable { + public static class TableRow implements Comparable { String propertyKey; String defaultValue; String description; boolean hasDefaultValue; + public TableRow() { + } + + public TableRow(String propertyKey, String defaultValue, String description, boolean hasDefaultValue) { + this.propertyKey = propertyKey; + this.defaultValue = defaultValue; + this.description = description; + this.hasDefaultValue = hasDefaultValue; + } + @Override public int compareTo(TableRow o) { return getPropertyKey().compareTo(o.getPropertyKey()); @@ -116,7 +137,6 @@ public String getPropertyKey() { public int hashCode() { final int prime = 31; int result = 1; - result = prime * result + getOuterType().hashCode(); result = prime * result + ((defaultValue == null) ? 0 : defaultValue.hashCode()); result = prime * result + ((description == null) ? 0 : description.hashCode()); result = prime * result + ((propertyKey == null) ? 0 : propertyKey.hashCode()); @@ -132,8 +152,6 @@ public boolean equals(Object obj) { if (getClass() != obj.getClass()) return false; TableRow other = (TableRow) obj; - if (!getOuterType().equals(other.getOuterType())) - return false; if (defaultValue == null) { if (other.defaultValue != null) return false; @@ -151,9 +169,5 @@ public boolean equals(Object obj) { return false; return true; } - - private SystemPropertiesDescriptionGenerator getOuterType() { - return SystemPropertiesDescriptionGenerator.this; - } } } diff --git a/sechub-doc/src/main/java/com/mercedesbenz/sechub/docgen/usecase/UseCaseRestDocModelDataCollector.java b/sechub-doc/src/main/java/com/mercedesbenz/sechub/docgen/usecase/UseCaseRestDocModelDataCollector.java index dd68855f7e..21c98cb548 100644 --- a/sechub-doc/src/main/java/com/mercedesbenz/sechub/docgen/usecase/UseCaseRestDocModelDataCollector.java +++ b/sechub-doc/src/main/java/com/mercedesbenz/sechub/docgen/usecase/UseCaseRestDocModelDataCollector.java @@ -192,8 +192,8 @@ private File scanForSpringRestDocGenFolder(UseCaseRestDocEntry entry) { Maybe you - forgot to implement the RESTDOC test for the usecase or for one of its variants - used two differet names for the variant inside your test (annotation + code in test method) - - forgot to add the documentation calls inside a RESTDOC test, or - - you accidently used another class when calling UseCaseRestDoc.Factory.createPath(...) or + - forgot to add the documentation calls inside a RESTDOC test + - accidently used another class when calling UseCaseRestDoc.Factory.createPath(...) - executed not `gradlew sechub-doc:test` before Details: diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/ExampleFilesValidTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/ExampleFilesValidTest.java index d7c8500f5c..eff696a547 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/ExampleFilesValidTest.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/ExampleFilesValidTest.java @@ -11,9 +11,32 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; -import com.mercedesbenz.sechub.commons.model.*; -import com.mercedesbenz.sechub.commons.model.login.*; +import com.mercedesbenz.sechub.commons.model.ClientCertificateConfiguration; +import com.mercedesbenz.sechub.commons.model.HTTPHeaderConfiguration; +import com.mercedesbenz.sechub.commons.model.JSONConverter; +import com.mercedesbenz.sechub.commons.model.JSONConverterException; +import com.mercedesbenz.sechub.commons.model.SecHubDataConfiguration; +import com.mercedesbenz.sechub.commons.model.SecHubScanConfiguration; +import com.mercedesbenz.sechub.commons.model.SecHubSourceDataConfiguration; +import com.mercedesbenz.sechub.commons.model.SecHubTimeUnit; +import com.mercedesbenz.sechub.commons.model.SecHubWebScanApiConfiguration; +import com.mercedesbenz.sechub.commons.model.SecHubWebScanApiType; +import com.mercedesbenz.sechub.commons.model.SecHubWebScanConfiguration; +import com.mercedesbenz.sechub.commons.model.WebScanDurationConfiguration; +import com.mercedesbenz.sechub.commons.model.login.Action; +import com.mercedesbenz.sechub.commons.model.login.ActionType; +import com.mercedesbenz.sechub.commons.model.login.BasicLoginConfiguration; +import com.mercedesbenz.sechub.commons.model.login.EncodingType; +import com.mercedesbenz.sechub.commons.model.login.FormLoginConfiguration; +import com.mercedesbenz.sechub.commons.model.login.Page; +import com.mercedesbenz.sechub.commons.model.login.Script; +import com.mercedesbenz.sechub.commons.model.login.TOTPHashAlgorithm; +import com.mercedesbenz.sechub.commons.model.login.WebLoginConfiguration; +import com.mercedesbenz.sechub.commons.model.login.WebLoginTOTPConfiguration; +import com.mercedesbenz.sechub.commons.model.template.TemplateType; import com.mercedesbenz.sechub.commons.pds.PDSDefaultParameterKeyConstants; +import com.mercedesbenz.sechub.commons.pds.data.PDSTemplateMetaData; +import com.mercedesbenz.sechub.commons.pds.data.PDSTemplateMetaData.PDSAssetData; import com.mercedesbenz.sechub.pds.commons.core.config.PDSProductParameterDefinition; import com.mercedesbenz.sechub.pds.commons.core.config.PDSProductParameterSetup; import com.mercedesbenz.sechub.pds.commons.core.config.PDSProductSetup; @@ -63,8 +86,8 @@ void check_pds_config_example1_can_be_loaded_and_is_valid() throws Exception { } @ParameterizedTest - @EnumSource(ExampleFile.class) - void every_sechub_config_file_is_valid(ExampleFile file) { + @EnumSource(SecHubConfigExampleFile.class) + void every_sechub_config_file_is_valid(SecHubConfigExampleFile file) { /* prepare */ String json = TestFileReader.readTextFromFile(file.getPath()); SecHubScanConfiguration config = null; @@ -81,10 +104,10 @@ void every_sechub_config_file_is_valid(ExampleFile file) { } @ParameterizedTest - @EnumSource(value = ExampleFile.class, names = { "WEBSCAN_ANONYMOUS", "WEBSCAN_BASIC_AUTH", "WEBSCAN_FORM_BASED_SCRIPT_AUTH", + @EnumSource(value = SecHubConfigExampleFile.class, names = { "WEBSCAN_ANONYMOUS", "WEBSCAN_BASIC_AUTH", "WEBSCAN_FORM_BASED_SCRIPT_AUTH", "WEBSCAN_OPENAPI_WITH_DATA_REFERENCE", "WEBSCAN_HEADER_SCAN", "WEBSCAN_CLIENT_CERTIFICATE", "WEBSCAN_FORM_BASED_SCRIPT_AUTH_WITH_TOTP" }, mode = EnumSource.Mode.INCLUDE) - void every_sechub_config_webscan_file_is_valid_and_has_a_target_uri(ExampleFile file) { + void every_sechub_config_webscan_file_is_valid_and_has_a_target_uri(SecHubConfigExampleFile file) { /* prepare */ String json = TestFileReader.readTextFromFile(file.getPath()); @@ -102,7 +125,7 @@ void every_sechub_config_webscan_file_is_valid_and_has_a_target_uri(ExampleFile @Test void webscan_anonymous_can_be_read_and_contains_expected_config() { /* prepare */ - String json = TestFileReader.readTextFromFile(ExampleFile.WEBSCAN_ANONYMOUS.getPath()); + String json = TestFileReader.readTextFromFile(SecHubConfigExampleFile.WEBSCAN_ANONYMOUS.getPath()); /* execute */ SecHubScanConfiguration config = SecHubScanConfiguration.createFromJSON(json); @@ -121,7 +144,7 @@ void webscan_anonymous_can_be_read_and_contains_expected_config() { @Test void webscan_basic_auth_can_be_read_and_contains_expected_config() { /* prepare */ - String json = TestFileReader.readTextFromFile(ExampleFile.WEBSCAN_BASIC_AUTH.getPath()); + String json = TestFileReader.readTextFromFile(SecHubConfigExampleFile.WEBSCAN_BASIC_AUTH.getPath()); /* execute */ SecHubScanConfiguration config = SecHubScanConfiguration.createFromJSON(json); @@ -144,7 +167,7 @@ void webscan_basic_auth_can_be_read_and_contains_expected_config() { @Test void webscan_form_based_script_auth_can_be_read_and_contains_expected_config() { /* prepare */ - String json = TestFileReader.readTextFromFile(ExampleFile.WEBSCAN_FORM_BASED_SCRIPT_AUTH.getPath()); + String json = TestFileReader.readTextFromFile(SecHubConfigExampleFile.WEBSCAN_FORM_BASED_SCRIPT_AUTH.getPath()); /* execute */ SecHubScanConfiguration config = SecHubScanConfiguration.createFromJSON(json); @@ -168,7 +191,7 @@ void webscan_form_based_script_auth_can_be_read_and_contains_expected_config() { @Test void webscan_openapi_with_data_reference_can_be_read_and_contains_expected_config() { /* prepare */ - String json = TestFileReader.readTextFromFile(ExampleFile.WEBSCAN_OPENAPI_WITH_DATA_REFERENCE.getPath()); + String json = TestFileReader.readTextFromFile(SecHubConfigExampleFile.WEBSCAN_OPENAPI_WITH_DATA_REFERENCE.getPath()); /* execute */ SecHubScanConfiguration config = SecHubScanConfiguration.createFromJSON(json); @@ -185,7 +208,7 @@ void webscan_openapi_with_data_reference_can_be_read_and_contains_expected_confi @Test void webscan_client_certificate_with_data_reference_can_be_read_and_contains_expected_config() { /* prepare */ - String json = TestFileReader.readTextFromFile(ExampleFile.WEBSCAN_CLIENT_CERTIFICATE_WITH_OPENAPI.getPath()); + String json = TestFileReader.readTextFromFile(SecHubConfigExampleFile.WEBSCAN_CLIENT_CERTIFICATE_WITH_OPENAPI.getPath()); /* execute */ SecHubScanConfiguration config = SecHubScanConfiguration.createFromJSON(json); @@ -205,7 +228,7 @@ void webscan_client_certificate_with_data_reference_can_be_read_and_contains_exp @Test void webscan_client_certificate_with_openapi_can_be_read_and_contains_expected_config() { /* prepare */ - String json = TestFileReader.readTextFromFile(ExampleFile.WEBSCAN_CLIENT_CERTIFICATE.getPath()); + String json = TestFileReader.readTextFromFile(SecHubConfigExampleFile.WEBSCAN_CLIENT_CERTIFICATE.getPath()); /* execute */ SecHubScanConfiguration config = SecHubScanConfiguration.createFromJSON(json); @@ -222,7 +245,7 @@ void webscan_client_certificate_with_openapi_can_be_read_and_contains_expected_c @Test void webscan_header_scan_can_be_read_and_contains_expected_config() { /* prepare */ - String json = TestFileReader.readTextFromFile(ExampleFile.WEBSCAN_HEADER_SCAN.getPath()); + String json = TestFileReader.readTextFromFile(SecHubConfigExampleFile.WEBSCAN_HEADER_SCAN.getPath()); /* execute */ SecHubScanConfiguration config = SecHubScanConfiguration.createFromJSON(json); @@ -250,7 +273,7 @@ void webscan_header_scan_can_be_read_and_contains_expected_config() { @Test void webscan_header_from_data_reference_can_be_read_and_contains_expected_config() { /* prepare */ - String json = TestFileReader.readTextFromFile(ExampleFile.WEBSCAN_HEADER_FROM_DATA_REFERENCE.getPath()); + String json = TestFileReader.readTextFromFile(SecHubConfigExampleFile.WEBSCAN_HEADER_FROM_DATA_REFERENCE.getPath()); /* execute */ SecHubScanConfiguration config = SecHubScanConfiguration.createFromJSON(json); @@ -283,7 +306,7 @@ void webscan_header_from_data_reference_can_be_read_and_contains_expected_config @Test void webscan_form_based_script_auth_with_totp_can_be_read_and_contains_expected_config() { /* prepare */ - String json = TestFileReader.readTextFromFile(ExampleFile.WEBSCAN_FORM_BASED_SCRIPT_AUTH_WITH_TOTP.getPath()); + String json = TestFileReader.readTextFromFile(SecHubConfigExampleFile.WEBSCAN_FORM_BASED_SCRIPT_AUTH_WITH_TOTP.getPath()); /* execute */ SecHubScanConfiguration config = SecHubScanConfiguration.createFromJSON(json); @@ -307,6 +330,29 @@ void webscan_form_based_script_auth_with_totp_can_be_read_and_contains_expected_ assertEquals(60, totp.getValidityInSeconds()); assertEquals(8, totp.getTokenLength()); assertEquals(TOTPHashAlgorithm.HMAC_SHA256, totp.getHashAlgorithm()); + assertEquals(EncodingType.BASE64, totp.getEncodingType()); + } + + @Test + void pds_param_template_metadata_array_syntax_example_is_valid() { + /* prepare */ + String json = TestFileReader.readTextFromFile(PDSDataExampleFile.PDS_PARAM_TEMPLATE_META_DATA_SYNTAX.getPath()); + + /* execute */ + List result = JSONConverter.get().fromJSONtoListOf(PDSTemplateMetaData.class, json); + + /* test */ + assertEquals(1, result.size()); + PDSTemplateMetaData data = result.iterator().next(); + assertEquals("templateId", data.getTemplateId()); + assertEquals(TemplateType.WEBSCAN_LOGIN, data.getTemplateType()); + + PDSAssetData assetData = data.getAssetData(); + assertNotNull(assetData); + assertEquals("assetId", assetData.getAssetId()); + assertEquals("fileChecksum", assetData.getChecksum()); + assertEquals("fileName", assetData.getFileName()); + } private void assertDefaultValue(PDSProductSetup setup, boolean isMandatory, String parameterKey, String expectedDefault) { diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/PDSDataExampleFile.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/PDSDataExampleFile.java new file mode 100644 index 0000000000..b4dc4be87f --- /dev/null +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/PDSDataExampleFile.java @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub; + +public enum PDSDataExampleFile { + + PDS_PARAM_TEMPLATE_META_DATA_SYNTAX("src/docs/asciidoc/documents/shared/snippet/pds-param-template-metadata-syntax.json"); + ; + + private String path; + + private PDSDataExampleFile(String path) { + this.path = path; + } + + public String getPath() { + return path; + } +} diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/ExampleFile.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/SecHubConfigExampleFile.java similarity index 96% rename from sechub-doc/src/test/java/com/mercedesbenz/sechub/ExampleFile.java rename to sechub-doc/src/test/java/com/mercedesbenz/sechub/SecHubConfigExampleFile.java index 36bac1cf81..95ae922289 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/ExampleFile.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/SecHubConfigExampleFile.java @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT package com.mercedesbenz.sechub; -public enum ExampleFile { +public enum SecHubConfigExampleFile { DATA_SECTION_EXAMPLE_1("src/docs/asciidoc/documents/shared/configuration/sechub_config_data_section_general_example1.json"), @@ -45,7 +45,7 @@ public enum ExampleFile { private String path; - private ExampleFile(String path) { + private SecHubConfigExampleFile(String path) { this.path = path; } diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/docgen/AsciidocGeneratorTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/docgen/AsciidocGeneratorTest.java index e45ad83beb..c5afc81a6f 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/docgen/AsciidocGeneratorTest.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/docgen/AsciidocGeneratorTest.java @@ -29,6 +29,7 @@ public void before() throws Exception { generatorToTest.writer = mock(DocGenTextFileWriter.class); when(generatorToTest.propertiesGenerator.generate(any(), any())).thenReturn("properties-test"); + when(generatorToTest.propertiesGenerator.generate(any(), any(), any())).thenReturn("properties-test"); when(generatorToTest.scheduleDescriptionGenerator.generate(generatorToTest.collector)).thenReturn("schedule-test"); } @@ -69,7 +70,7 @@ public void calls_properties_generator_and_saves() throws Exception { generatorToTest.generateSecHubSystemPropertiesDescription(genContext); /* test */ - verify(generatorToTest.propertiesGenerator).generate(any(), any()); + verify(generatorToTest.propertiesGenerator).generate(any(), any(), any()); verify(generatorToTest.writer).writeTextToFile(genContext.systemProperitesFile, "properties-test"); } diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/AdminShowsScanLogsForProjectRestDocTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/AdminShowsScanLogsForProjectRestDocTest.java index 819cefd01e..756e76590f 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/AdminShowsScanLogsForProjectRestDocTest.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/AdminShowsScanLogsForProjectRestDocTest.java @@ -22,11 +22,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.ActiveProfiles; @@ -41,7 +40,6 @@ import com.mercedesbenz.sechub.domain.scan.log.ProjectScanLogSummary; import com.mercedesbenz.sechub.domain.scan.log.ScanLogRestController; import com.mercedesbenz.sechub.sharedkernel.Profiles; -import com.mercedesbenz.sechub.sharedkernel.security.AbstractSecHubAPISecurityConfiguration; import com.mercedesbenz.sechub.sharedkernel.security.RoleConstants; import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseRestDoc; import com.mercedesbenz.sechub.sharedkernel.usecases.admin.project.UseCaseAdminShowsScanLogsForProject; @@ -50,8 +48,9 @@ import com.mercedesbenz.sechub.test.TestPortProvider; @RunWith(SpringRunner.class) -@WebMvcTest(ProjectAdministrationRestController.class) -@ContextConfiguration(classes = { ScanLogRestController.class, AdminShowsScanLogsForProjectRestDocTest.SimpleTestConfiguration.class }) +@WebMvcTest +@ContextConfiguration(classes = { ProjectAdministrationRestController.class, ScanLogRestController.class }) +@Import(TestRestDocSecurityConfiguration.class) @WithMockUser(roles = RoleConstants.ROLE_SUPERADMIN) @ActiveProfiles(Profiles.TEST) @AutoConfigureRestDocs(uriScheme = "https", uriHost = ExampleConstants.URI_SECHUB_SERVER, uriPort = 443) @@ -123,10 +122,4 @@ public void restdoc_admin_downloads_scan_logs_for_project() throws Exception { /* @formatter:on */ } - @Profile(Profiles.TEST) - @EnableAutoConfiguration - public static class SimpleTestConfiguration extends AbstractSecHubAPISecurityConfiguration { - - } - } diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/AnonymousCheckAliveRestDocTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/AnonymousCheckAliveRestDocTest.java index cc759dc18f..51b2097c3d 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/AnonymousCheckAliveRestDocTest.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/AnonymousCheckAliveRestDocTest.java @@ -11,11 +11,9 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.Import; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; @@ -25,7 +23,6 @@ import com.mercedesbenz.sechub.docgen.util.RestDocFactory; import com.mercedesbenz.sechub.server.core.AnonymousCheckAliveRestController; import com.mercedesbenz.sechub.sharedkernel.Profiles; -import com.mercedesbenz.sechub.sharedkernel.security.AbstractSecHubAPISecurityConfiguration; import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseRestDoc; import com.mercedesbenz.sechub.sharedkernel.usecases.anonymous.UseCaseAnonymousCheckAlive; import com.mercedesbenz.sechub.test.ExampleConstants; @@ -33,8 +30,9 @@ import com.mercedesbenz.sechub.test.TestPortProvider; @RunWith(SpringRunner.class) -@WebMvcTest(AnonymousCheckAliveRestController.class) -@ContextConfiguration(classes = { AnonymousCheckAliveRestController.class, AnonymousCheckAliveRestDocTest.SimpleTestConfiguration.class }) +@WebMvcTest +@ContextConfiguration(classes = { AnonymousCheckAliveRestController.class }) +@Import(TestRestDocSecurityConfiguration.class) @WithMockUser @ActiveProfiles(Profiles.TEST) @AutoConfigureRestDocs(uriScheme = "https", uriHost = ExampleConstants.URI_SECHUB_SERVER, uriPort = 443) @@ -91,11 +89,4 @@ public void calling_check_alive_get_returns_HTTP_200() throws Exception { /* @formatter:on */ } - - @TestConfiguration - @Profile(Profiles.TEST) - @EnableAutoConfiguration - public static class SimpleTestConfiguration extends AbstractSecHubAPISecurityConfiguration { - - } } diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/AnonymousSignupRestControllerRestDocTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/AnonymousSignupRestControllerRestDocTest.java index 368506db41..baa234e2e5 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/AnonymousSignupRestControllerRestDocTest.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/AnonymousSignupRestControllerRestDocTest.java @@ -12,12 +12,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.ActiveProfiles; @@ -30,7 +28,6 @@ import com.mercedesbenz.sechub.domain.administration.signup.AnonymousSignupRestController; import com.mercedesbenz.sechub.domain.administration.signup.SignupJsonInputValidator; import com.mercedesbenz.sechub.sharedkernel.Profiles; -import com.mercedesbenz.sechub.sharedkernel.security.AbstractSecHubAPISecurityConfiguration; import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseRestDoc; import com.mercedesbenz.sechub.sharedkernel.usecases.user.UseCaseUserSignup; import com.mercedesbenz.sechub.sharedkernel.validation.ApiVersionValidationFactory; @@ -41,9 +38,10 @@ import com.mercedesbenz.sechub.test.TestPortProvider; @RunWith(SpringRunner.class) -@WebMvcTest(AnonymousSignupRestController.class) +@WebMvcTest @ContextConfiguration(classes = { AnonymousSignupRestController.class, SignupJsonInputValidator.class, UserIdValidationImpl.class, - ApiVersionValidationFactory.class, EmailValidationImpl.class, AnonymousSignupRestControllerRestDocTest.SimpleTestConfiguration.class }) + ApiVersionValidationFactory.class, EmailValidationImpl.class }) +@Import(TestRestDocSecurityConfiguration.class) @WithMockUser @ActiveProfiles(Profiles.TEST) @AutoConfigureRestDocs(uriScheme = "https", uriHost = ExampleConstants.URI_SECHUB_SERVER, uriPort = 443) @@ -93,11 +91,4 @@ public void calling_with_api_1_0_and_valid_userid_and_email_returns_HTTP_200() t /* @formatter:on */ } - @TestConfiguration - @Profile(Profiles.TEST) - @EnableAutoConfiguration - public static class SimpleTestConfiguration extends AbstractSecHubAPISecurityConfiguration { - - } - } diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/AnonymousUserGetAPITokenByOneTimeTokenRestControllerRestDocTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/AnonymousUserGetAPITokenByOneTimeTokenRestControllerRestDocTest.java index 2308ffa3b3..7350f9c36c 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/AnonymousUserGetAPITokenByOneTimeTokenRestControllerRestDocTest.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/AnonymousUserGetAPITokenByOneTimeTokenRestControllerRestDocTest.java @@ -15,11 +15,10 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithAnonymousUser; import org.springframework.test.context.ActiveProfiles; @@ -31,7 +30,6 @@ import com.mercedesbenz.sechub.domain.administration.user.AnonymousUserGetAPITokenByOneTimeTokenService; import com.mercedesbenz.sechub.domain.administration.user.AnonymousUserGetApiTokenByOneTimeTokenRestController; import com.mercedesbenz.sechub.sharedkernel.Profiles; -import com.mercedesbenz.sechub.sharedkernel.security.AbstractSecHubAPISecurityConfiguration; import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseRestDoc; import com.mercedesbenz.sechub.sharedkernel.usecases.user.UseCaseUserClicksLinkToGetNewAPIToken; import com.mercedesbenz.sechub.test.ExampleConstants; @@ -39,9 +37,9 @@ import com.mercedesbenz.sechub.test.TestPortProvider; @ExtendWith(SpringExtension.class) -@WebMvcTest(AnonymousUserGetApiTokenByOneTimeTokenRestController.class) -@ContextConfiguration(classes = { AnonymousUserGetApiTokenByOneTimeTokenRestController.class, - AnonymousUserGetAPITokenByOneTimeTokenRestControllerRestDocTest.SimpleTestConfiguration.class }) +@WebMvcTest +@ContextConfiguration(classes = { AnonymousUserGetApiTokenByOneTimeTokenRestController.class }) +@Import(TestRestDocSecurityConfiguration.class) @ActiveProfiles(Profiles.TEST) @AutoConfigureRestDocs(uriScheme = "https", uriHost = ExampleConstants.URI_SECHUB_SERVER, uriPort = 443) public class AnonymousUserGetAPITokenByOneTimeTokenRestControllerRestDocTest implements TestIsNecessaryForDocumentation { @@ -86,10 +84,4 @@ public void restdoc_user_clicks_link_to_get_NewApiToken() throws Exception { /* @formatter:on */ } - @Profile(Profiles.TEST) - @EnableAutoConfiguration - public static class SimpleTestConfiguration extends AbstractSecHubAPISecurityConfiguration { - - } - } diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/AnonymousUserRequestsNewApiTokenRestDocTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/AnonymousUserRequestsNewApiTokenRestDocTest.java index 30d6ae44be..9b790faf86 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/AnonymousUserRequestsNewApiTokenRestDocTest.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/AnonymousUserRequestsNewApiTokenRestDocTest.java @@ -13,12 +13,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.ActiveProfiles; @@ -30,7 +28,6 @@ import com.mercedesbenz.sechub.domain.administration.user.AnonymousUserRequestNewApiTokenRestController; import com.mercedesbenz.sechub.domain.administration.user.AnonymousUserRequestsNewApiTokenService; import com.mercedesbenz.sechub.sharedkernel.Profiles; -import com.mercedesbenz.sechub.sharedkernel.security.AbstractSecHubAPISecurityConfiguration; import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseRestDoc; import com.mercedesbenz.sechub.sharedkernel.usecases.user.UseCaseUserRequestsNewApiToken; import com.mercedesbenz.sechub.test.ExampleConstants; @@ -38,9 +35,9 @@ import com.mercedesbenz.sechub.test.TestPortProvider; @RunWith(SpringRunner.class) -@WebMvcTest(AnonymousUserRequestNewApiTokenRestController.class) -@ContextConfiguration(classes = { AnonymousUserRequestNewApiTokenRestController.class, - AnonymousUserRequestsNewApiTokenRestDocTest.SimpleTestConfiguration.class }) +@WebMvcTest +@ContextConfiguration(classes = { AnonymousUserRequestNewApiTokenRestController.class }) +@Import(TestRestDocSecurityConfiguration.class) @WithMockUser @ActiveProfiles(Profiles.TEST) @AutoConfigureRestDocs(uriScheme = "https", uriHost = ExampleConstants.URI_SECHUB_SERVER, uriPort = 443) @@ -82,11 +79,4 @@ public void calling_with_api_1_0_and_valid_userid_and_email_returns_HTTP_200() t /* @formatter:on */ } - @TestConfiguration - @Profile(Profiles.TEST) - @EnableAutoConfiguration - public static class SimpleTestConfiguration extends AbstractSecHubAPISecurityConfiguration { - - } - } diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/AssetRestControllerRestDocTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/AssetRestControllerRestDocTest.java new file mode 100644 index 0000000000..34fb89c4bc --- /dev/null +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/AssetRestControllerRestDocTest.java @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.restdoc; + +import static com.mercedesbenz.sechub.commons.core.CommonConstants.*; +import static com.mercedesbenz.sechub.restdoc.RestDocumentation.*; +import static com.mercedesbenz.sechub.test.RestDocPathParameter.*; +import static com.mercedesbenz.sechub.test.SecHubTestURLBuilder.*; +import static org.mockito.Mockito.*; +import static org.springframework.restdocs.headers.HeaderDocumentation.*; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.restdocs.request.RequestDocumentation.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; + +import com.mercedesbenz.sechub.docgen.util.RestDocFactory; +import com.mercedesbenz.sechub.docgen.util.RestDocTestFileSupport; +import com.mercedesbenz.sechub.domain.scan.asset.AssetDetailData; +import com.mercedesbenz.sechub.domain.scan.asset.AssetFileData; +import com.mercedesbenz.sechub.domain.scan.asset.AssetFileRepository; +import com.mercedesbenz.sechub.domain.scan.asset.AssetRestController; +import com.mercedesbenz.sechub.domain.scan.asset.AssetService; +import com.mercedesbenz.sechub.sharedkernel.Profiles; +import com.mercedesbenz.sechub.sharedkernel.logging.AuditLogService; +import com.mercedesbenz.sechub.sharedkernel.logging.LogSanitizer; +import com.mercedesbenz.sechub.sharedkernel.security.RoleConstants; +import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseRestDoc; +import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseRestDoc.SpringRestDocOutput; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminDeletesAssetCompletely; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminDeletesOneFileFromAsset; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminDownloadsAssetFile; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminFetchesAssetDetails; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminFetchesAssetIds; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminUploadsAssetFile; +import com.mercedesbenz.sechub.test.ExampleConstants; +import com.mercedesbenz.sechub.test.TestIsNecessaryForDocumentation; +import com.mercedesbenz.sechub.test.TestPortProvider; + +@RunWith(SpringRunner.class) +@WebMvcTest +@ActiveProfiles({ Profiles.TEST, Profiles.ADMIN_ACCESS }) +@WithMockUser(roles = RoleConstants.ROLE_SUPERADMIN) +@ContextConfiguration(classes = { AssetRestController.class, AssetRestControllerRestDocTest.class }) +@Import(TestRestDocSecurityConfiguration.class) +@AutoConfigureRestDocs(uriScheme = "https", uriHost = ExampleConstants.URI_SECHUB_SERVER, uriPort = 443) +public class AssetRestControllerRestDocTest implements TestIsNecessaryForDocumentation { + + private static final String TEST_CHECKSUM1 = "c6965634c4ec8e4f5e72dffd36ea725860e8b485216260264a0973073805e422"; + + private static final int PORT_USED = TestPortProvider.DEFAULT_INSTANCE.getRestDocTestPort(); + + @Autowired + private MockMvc mockMvc; + + @MockBean + private AssetFileRepository assetFileRepository; + + @MockBean + AssetService assetService; + + @MockBean + AuditLogService auditLogService; + + @MockBean + LogSanitizer logSanitizer; + + private static final String TEST_ASSET_ID1 = "asset-1"; + private static final String TEST_ASSET_ID2 = "asset-2"; + + private static final String TEST_FILENAME1 = "PRODUCT1.zip"; + + @Before + public void before() { + } + + @Test + @UseCaseRestDoc(useCase = UseCaseAdminDeletesOneFileFromAsset.class) + public void restdoc_admin_deletes_one_file_from_asset() throws Exception { + /* prepare */ + String apiEndpoint = https(PORT_USED).buildAdminDeletesAssetFile(ASSET_ID.pathElement(), FILE_NAME.pathElement()); + Class useCase = UseCaseAdminDeletesOneFileFromAsset.class; + + /* execute + test @formatter:off */ + this.mockMvc.perform( + delete(apiEndpoint, TEST_ASSET_ID1, TEST_FILENAME1). + header(AuthenticationHelper.HEADER_NAME, AuthenticationHelper.getHeaderValue()) + ). + andExpect(status().isOk()). + andDo(defineRestService(). + with(). + useCaseData(useCase). + tag(RestDocFactory.extractTag(apiEndpoint)). + requestSchema(OpenApiSchema.ASSETS.getSchema()). + and(). + document( + pathParameters( + parameterWithName(ASSET_ID.paramName()).description("The asset identifier"), + parameterWithName(FILE_NAME.paramName()).description("The name of the file to delete inside the asset") + ) + )); + + /* @formatter:on */ + } + + @Test + @UseCaseRestDoc(useCase = UseCaseAdminDeletesAssetCompletely.class) + public void restdoc_admin_deletes_asset_completely() throws Exception { + /* prepare */ + String apiEndpoint = https(PORT_USED).buildAdminDeletesAsset(ASSET_ID.pathElement()); + Class useCase = UseCaseAdminDeletesAssetCompletely.class; + + /* execute + test @formatter:off */ + this.mockMvc.perform( + delete(apiEndpoint, TEST_ASSET_ID1). + contentType(MediaType.APPLICATION_JSON_VALUE). + header(AuthenticationHelper.HEADER_NAME, AuthenticationHelper.getHeaderValue()) + ). + andExpect(status().isOk()). + andDo(defineRestService(). + with(). + useCaseData(useCase). + tag(RestDocFactory.extractTag(apiEndpoint)). + requestSchema(OpenApiSchema.ASSETS.getSchema()). + and(). + document( + pathParameters( + parameterWithName(ASSET_ID.paramName()).description("The asset identifier for the asset which shall be deleted completely") + ) + )); + + /* @formatter:on */ + } + + @Test + @UseCaseRestDoc(useCase = UseCaseAdminFetchesAssetIds.class) + public void restdoc_admin_fetches_all_asset_ids() throws Exception { + /* prepare */ + when(assetService.fetchAllAssetIds()).thenReturn(List.of(TEST_ASSET_ID1, TEST_ASSET_ID2)); + + String apiEndpoint = https(PORT_USED).buildAdminFetchesAllAssetIds(); + Class useCase = UseCaseAdminFetchesAssetIds.class; + + /* execute + test @formatter:off */ + this.mockMvc.perform( + get(apiEndpoint). + header(AuthenticationHelper.HEADER_NAME, AuthenticationHelper.getHeaderValue()) + ). + andExpect(status().isOk()). + andDo(defineRestService(). + with(). + useCaseData(useCase). + tag(RestDocFactory.extractTag(apiEndpoint)). + requestSchema(OpenApiSchema.ASSETS.getSchema()). + and(). + document( + responseFields( + fieldWithPath("[]").description("Array contains all existing asset identifiers") + ) + )); + + /* @formatter:on */ + } + + @Test + @UseCaseRestDoc(useCase = UseCaseAdminFetchesAssetDetails.class) + public void restdoc_admin_fetches_asset_details() throws Exception { + AssetDetailData asset1Details = new AssetDetailData(); + asset1Details.setAssetId(TEST_ASSET_ID1); + AssetFileData fileInfo = new AssetFileData(); + fileInfo.setChecksum(TEST_CHECKSUM1); + fileInfo.setFileName(TEST_FILENAME1); + asset1Details.getFiles().add(fileInfo); + /* prepare */ + when(assetService.fetchAssetDetails(TEST_ASSET_ID1)).thenReturn(asset1Details); + + String apiEndpoint = https(PORT_USED).buildAdminFetchesAssetDetails(ASSET_ID.pathElement()); + Class useCase = UseCaseAdminFetchesAssetDetails.class; + + /* execute + test @formatter:off */ + this.mockMvc.perform( + get(apiEndpoint, TEST_ASSET_ID1). + contentType(MediaType.APPLICATION_JSON_VALUE). + header(AuthenticationHelper.HEADER_NAME, AuthenticationHelper.getHeaderValue()) + ). + andExpect(status().isOk()). + andDo(defineRestService(). + with(). + useCaseData(useCase). + tag(RestDocFactory.extractTag(apiEndpoint)). + requestSchema(OpenApiSchema.ASSETS.getSchema()). + and(). + document( + responseFields( + fieldWithPath("assetId").description("The asset identifier"), + fieldWithPath("files[]").description("Array containing data about files from asset"), + fieldWithPath("files[].fileName").description("Name of file"), + fieldWithPath("files[].checksum").description("Checksum for file") + ) + )); + + /* @formatter:on */ + } + + @Test + @UseCaseRestDoc(useCase = UseCaseAdminUploadsAssetFile.class) + public void restDoc_admin_uploads_assetfile() throws Exception { + /* prepare */ + String apiEndpoint = https(PORT_USED).buildAdminUploadsAssetFile(ASSET_ID.pathElement()); + Class useCase = UseCaseAdminUploadsAssetFile.class; + + InputStream inputStreamTo = RestDocTestFileSupport.getTestfileSupport().getInputStreamTo("upload/zipfile_contains_only_test1.txt.zip"); + MockMultipartFile file1 = new MockMultipartFile("file", inputStreamTo); + /* execute + test @formatter:off */ + this.mockMvc.perform( + multipart(apiEndpoint, TEST_ASSET_ID1). + file(file1). + queryParam(MULTIPART_CHECKSUM, TEST_CHECKSUM1) + ). + andExpect(status().isOk()). + andDo(defineRestService(). + with(). + useCaseData(useCase). + tag(RestDocFactory.extractTag(apiEndpoint)). + and(). + document( + requestHeaders( + ), + pathParameters( + parameterWithName(ASSET_ID.paramName()).description("The id of the asset to which the uploaded file belongs to") + ), + queryParameters( + parameterWithName(MULTIPART_CHECKSUM).description("A sha256 checksum for file upload validation") + ), + requestParts( + partWithName(MULTIPART_FILE).description("The asset file to upload") + ) + )); + + /* @formatter:on */ + } + + @Test + @UseCaseRestDoc(useCase = UseCaseAdminDownloadsAssetFile.class, wanted = { SpringRestDocOutput.PATH_PARAMETERS, SpringRestDocOutput.REQUEST_FIELDS, + SpringRestDocOutput.CURL_REQUEST }) + public void restdoc_admin_downloads_assetfile() throws Exception { + /* prepare */ + String apiEndpoint = https(PORT_USED).buildAdminDownloadsAssetFile(ASSET_ID.pathElement(), FILE_NAME.pathElement()); + Class useCase = UseCaseAdminDownloadsAssetFile.class; + + /* execute + test @formatter:off */ + this.mockMvc.perform( + get(apiEndpoint,TEST_ASSET_ID1, TEST_FILENAME1). + contentType(MediaType.APPLICATION_JSON_VALUE). + header(AuthenticationHelper.HEADER_NAME, AuthenticationHelper.getHeaderValue()) + ). + andExpect(status().isOk()). + andDo(defineRestService(). + with(). + useCaseData(useCase). + tag(RestDocFactory.extractTag(apiEndpoint)). + responseSchema(OpenApiSchema.ASSETS.getSchema()). + and(). + document( + requestHeaders( + + ), + pathParameters( + parameterWithName(ASSET_ID.paramName()).description("The asset identifier"), + parameterWithName(FILE_NAME.paramName()).description("The name of the file to download from asset") + ) + )); + + /* @formatter:on */ + } + +} diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ConfigAdministrationRestControllerRestDocTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ConfigAdministrationRestControllerRestDocTest.java index 1ad8676f10..bef9b8e805 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ConfigAdministrationRestControllerRestDocTest.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ConfigAdministrationRestControllerRestDocTest.java @@ -14,11 +14,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.ActiveProfiles; @@ -30,9 +29,7 @@ import com.mercedesbenz.sechub.domain.administration.autocleanup.AdministrationAutoCleanupConfig; import com.mercedesbenz.sechub.domain.administration.config.AdministrationConfigService; import com.mercedesbenz.sechub.domain.administration.config.ConfigAdministrationRestController; -import com.mercedesbenz.sechub.domain.administration.scheduler.SchedulerAdministrationRestController; import com.mercedesbenz.sechub.sharedkernel.Profiles; -import com.mercedesbenz.sechub.sharedkernel.security.AbstractSecHubAPISecurityConfiguration; import com.mercedesbenz.sechub.sharedkernel.security.RoleConstants; import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseRestDoc; import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminFetchesAutoCleanupConfiguration; @@ -42,8 +39,9 @@ import com.mercedesbenz.sechub.test.TestPortProvider; @RunWith(SpringRunner.class) -@WebMvcTest(SchedulerAdministrationRestController.class) -@ContextConfiguration(classes = { ConfigAdministrationRestController.class, ConfigAdministrationRestControllerRestDocTest.SimpleTestConfiguration.class }) +@WebMvcTest +@ContextConfiguration(classes = { ConfigAdministrationRestController.class }) +@Import(TestRestDocSecurityConfiguration.class) @WithMockUser(roles = RoleConstants.ROLE_SUPERADMIN) @ActiveProfiles({ Profiles.TEST, Profiles.ADMIN_ACCESS }) @AutoConfigureRestDocs(uriScheme = "https", uriHost = ExampleConstants.URI_SECHUB_SERVER, uriPort = 443) @@ -122,10 +120,4 @@ public void restdoc_admin_fetches_auto_cleanup_configuration() throws Exception /* @formatter:on */ } - @Profile(Profiles.TEST) - @EnableAutoConfiguration - public static class SimpleTestConfiguration extends AbstractSecHubAPISecurityConfiguration { - - } - } diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/DownloadsFullScanDataForJobRestDocTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/DownloadsFullScanDataForJobRestDocTest.java index a853ca63dc..4f579de8b5 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/DownloadsFullScanDataForJobRestDocTest.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/DownloadsFullScanDataForJobRestDocTest.java @@ -17,11 +17,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.ActiveProfiles; @@ -30,7 +29,6 @@ import org.springframework.test.web.servlet.MockMvc; import com.mercedesbenz.sechub.docgen.util.RestDocFactory; -import com.mercedesbenz.sechub.domain.administration.project.ProjectAdministrationRestController; import com.mercedesbenz.sechub.domain.scan.admin.FullScanData; import com.mercedesbenz.sechub.domain.scan.admin.FullScanDataRestController; import com.mercedesbenz.sechub.domain.scan.admin.FullScanDataService; @@ -39,7 +37,6 @@ import com.mercedesbenz.sechub.sharedkernel.Profiles; import com.mercedesbenz.sechub.sharedkernel.logging.AuditLogService; import com.mercedesbenz.sechub.sharedkernel.logging.LogSanitizer; -import com.mercedesbenz.sechub.sharedkernel.security.AbstractSecHubAPISecurityConfiguration; import com.mercedesbenz.sechub.sharedkernel.security.RoleConstants; import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseRestDoc; import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseRestDoc.SpringRestDocOutput; @@ -49,8 +46,9 @@ import com.mercedesbenz.sechub.test.TestPortProvider; @RunWith(SpringRunner.class) -@WebMvcTest(ProjectAdministrationRestController.class) -@ContextConfiguration(classes = { FullScanDataRestController.class, DownloadsFullScanDataForJobRestDocTest.SimpleTestConfiguration.class, LogSanitizer.class }) +@WebMvcTest +@ContextConfiguration(classes = { FullScanDataRestController.class, LogSanitizer.class }) +@Import(TestRestDocSecurityConfiguration.class) @WithMockUser(roles = RoleConstants.ROLE_SUPERADMIN) @ActiveProfiles(Profiles.TEST) @AutoConfigureRestDocs(uriScheme = "https", uriHost = ExampleConstants.URI_SECHUB_SERVER, uriPort = 443) @@ -123,10 +121,4 @@ public void restdoc_admin_downloads_fullscan_data_for_job() throws Exception { /* @formatter:on */ } - @Profile(Profiles.TEST) - @EnableAutoConfiguration - public static class SimpleTestConfiguration extends AbstractSecHubAPISecurityConfiguration { - - } - } diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/EncryptionAdministrationRestControllerRestDocTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/EncryptionAdministrationRestControllerRestDocTest.java index e85a5223b3..7ce2d0dd7e 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/EncryptionAdministrationRestControllerRestDocTest.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/EncryptionAdministrationRestControllerRestDocTest.java @@ -22,6 +22,7 @@ import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.ActiveProfiles; @@ -43,7 +44,6 @@ import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubEncryptionDataValidator; import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubEncryptionStatus; import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubPasswordSource; -import com.mercedesbenz.sechub.sharedkernel.security.AbstractSecHubAPISecurityConfiguration; import com.mercedesbenz.sechub.sharedkernel.security.RoleConstants; import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseRestDoc; import com.mercedesbenz.sechub.sharedkernel.usecases.encryption.UseCaseAdminFetchesEncryptionStatus; @@ -54,10 +54,11 @@ @RunWith(SpringRunner.class) @WebMvcTest(JobAdministrationRestController.class) -@ContextConfiguration(classes = { EncryptionAdministrationRestController.class, SecHubEncryptionDataValidator.class, - EncryptionAdministrationRestControllerRestDocTest.SimpleTestConfiguration.class }) +@ContextConfiguration(classes = { EncryptionAdministrationRestController.class, SecHubEncryptionDataValidator.class }) +@Import(TestRestDocSecurityConfiguration.class) @WithMockUser(roles = RoleConstants.ROLE_SUPERADMIN) @ActiveProfiles({ Profiles.TEST, Profiles.ADMIN_ACCESS }) +@EnableAutoConfiguration @AutoConfigureRestDocs(uriScheme = "https", uriHost = ExampleConstants.URI_SECHUB_SERVER, uriPort = 443) public class EncryptionAdministrationRestControllerRestDocTest implements TestIsNecessaryForDocumentation { @@ -94,22 +95,22 @@ public void restdoc_admin_starts_encryption_rotation() throws Exception { /* execute + test @formatter:off */ this.mockMvc.perform( - post(apiEndpoint). - contentType(MediaType.APPLICATION_JSON_VALUE). - content(data.toFormattedJSON()). - header(AuthenticationHelper.HEADER_NAME, AuthenticationHelper.getHeaderValue()) + post(apiEndpoint). + contentType(MediaType.APPLICATION_JSON_VALUE). + content(data.toFormattedJSON()). + header(AuthenticationHelper.HEADER_NAME, AuthenticationHelper.getHeaderValue()) ). - andExpect(status().isOk()). - andDo(defineRestService(). - with(). - useCaseData(useCase). - tag(extractTag(apiEndpoint)). - and(). - document( - requestHeaders( - - ) - )); + andExpect(status().isOk()). + andDo(defineRestService(). + with(). + useCaseData(useCase). + tag(extractTag(apiEndpoint)). + and(). + document( + requestHeaders( + + ) + )); /* @formatter:on */ } @@ -131,37 +132,37 @@ public void restdoc_admin_fetches_encryption_status() throws Exception { String domains = SecHubEncryptionStatus.PROPERTY_DOMAINS+"[]."; String domainData = domains+SecHubDomainEncryptionStatus.PROPERTY_DATA+"[]."; - this.mockMvc.perform( - get(apiEndpoint). - contentType(MediaType.APPLICATION_JSON_VALUE). - header(AuthenticationHelper.HEADER_NAME, AuthenticationHelper.getHeaderValue()) - ). - andExpect(status().isOk()). - andDo(defineRestService(). - with(). - useCaseData(useCase). - tag(extractTag(apiEndpoint)). - responseSchema(OpenApiSchema.ENCRYPTION_STATUS.getSchema()). - and(). - document( - requestHeaders( - - ), - responseFields( - fieldWithPath(SecHubEncryptionStatus.PROPERTY_TYPE).description("The type description of the json content"), - fieldWithPath(domains+SecHubDomainEncryptionStatus.PROPERTY_NAME).description("Name of the domain which will provide this encryption data elements"), - fieldWithPath(domainData+SecHubDomainEncryptionData.PROPERTY_ID).description("Unique identifier"), - fieldWithPath(domainData+SecHubDomainEncryptionData.PROPERTY_ALGORITHM).description("Algorithm used for encryption"), - fieldWithPath(domainData+SecHubDomainEncryptionData.PROPERTY_PASSWORDSOURCE+"."+ SecHubPasswordSource.PROPERTY_TYPE).description("Type of password source. Can be "+List.of(SecHubCipherPasswordSourceType.values())), - fieldWithPath(domainData+SecHubDomainEncryptionData.PROPERTY_PASSWORDSOURCE+"."+ SecHubPasswordSource.PROPERTY_DATA).description("Data for password source. If type is "+SecHubCipherPasswordSourceType.ENVIRONMENT_VARIABLE+" then it is the the name of the environment variable."), - fieldWithPath(domainData+SecHubDomainEncryptionData.PROPERTY_USAGE).description("Map containing information about usage of this encryption"), - fieldWithPath(domainData+SecHubDomainEncryptionData.PROPERTY_USAGE+".*").description("Key value data"), - fieldWithPath(domainData+SecHubDomainEncryptionData.PROPERTY_CREATED).description("Creation timestamp"), - fieldWithPath(domainData+SecHubDomainEncryptionData.PROPERTY_CREATED_FROM).description("User id of admin who created the encryption entry") - ) - )); - - /* @formatter:on */ + this.mockMvc.perform( + get(apiEndpoint). + contentType(MediaType.APPLICATION_JSON_VALUE). + header(AuthenticationHelper.HEADER_NAME, AuthenticationHelper.getHeaderValue()) + ). + andExpect(status().isOk()). + andDo(defineRestService(). + with(). + useCaseData(useCase). + tag(extractTag(apiEndpoint)). + responseSchema(OpenApiSchema.ENCRYPTION_STATUS.getSchema()). + and(). + document( + requestHeaders( + + ), + responseFields( + fieldWithPath(SecHubEncryptionStatus.PROPERTY_TYPE).description("The type description of the json content"), + fieldWithPath(domains+SecHubDomainEncryptionStatus.PROPERTY_NAME).description("Name of the domain which will provide this encryption data elements"), + fieldWithPath(domainData+SecHubDomainEncryptionData.PROPERTY_ID).description("Unique identifier"), + fieldWithPath(domainData+SecHubDomainEncryptionData.PROPERTY_ALGORITHM).description("Algorithm used for encryption"), + fieldWithPath(domainData+SecHubDomainEncryptionData.PROPERTY_PASSWORDSOURCE+"."+ SecHubPasswordSource.PROPERTY_TYPE).description("Type of password source. Can be "+List.of(SecHubCipherPasswordSourceType.values())), + fieldWithPath(domainData+SecHubDomainEncryptionData.PROPERTY_PASSWORDSOURCE+"."+ SecHubPasswordSource.PROPERTY_DATA).description("Data for password source. If type is "+SecHubCipherPasswordSourceType.ENVIRONMENT_VARIABLE+" then it is the the name of the environment variable."), + fieldWithPath(domainData+SecHubDomainEncryptionData.PROPERTY_USAGE).description("Map containing information about usage of this encryption"), + fieldWithPath(domainData+SecHubDomainEncryptionData.PROPERTY_USAGE+".*").description("Key value data"), + fieldWithPath(domainData+SecHubDomainEncryptionData.PROPERTY_CREATED).description("Creation timestamp"), + fieldWithPath(domainData+SecHubDomainEncryptionData.PROPERTY_CREATED_FROM).description("User id of admin who created the encryption entry") + ) + )); + + /* @formatter:on */ } private SecHubEncryptionStatus createEncryptionStatusExample() { @@ -188,9 +189,4 @@ private SecHubEncryptionStatus createEncryptionStatusExample() { return status; } - @EnableAutoConfiguration - public static class SimpleTestConfiguration extends AbstractSecHubAPISecurityConfiguration { - - } - } diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/FalsePositiveRestControllerRestDocTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/FalsePositiveRestControllerRestDocTest.java index 755823b43c..a77d3c65d9 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/FalsePositiveRestControllerRestDocTest.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/FalsePositiveRestControllerRestDocTest.java @@ -25,11 +25,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.security.test.context.support.WithMockUser; @@ -45,7 +44,6 @@ import com.mercedesbenz.sechub.domain.scan.project.*; import com.mercedesbenz.sechub.domain.scan.report.ScanReportRepository; import com.mercedesbenz.sechub.sharedkernel.Profiles; -import com.mercedesbenz.sechub.sharedkernel.security.AbstractSecHubAPISecurityConfiguration; import com.mercedesbenz.sechub.sharedkernel.security.RoleConstants; import com.mercedesbenz.sechub.sharedkernel.security.UserContextService; import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseRestDoc; @@ -59,8 +57,9 @@ import com.mercedesbenz.sechub.test.TestPortProvider; @RunWith(SpringRunner.class) -@WebMvcTest(FalsePositiveRestController.class) -@ContextConfiguration(classes = { FalsePositiveRestController.class, FalsePositiveRestControllerRestDocTest.SimpleTestConfiguration.class }) +@WebMvcTest +@ContextConfiguration(classes = { FalsePositiveRestController.class }) +@Import(TestRestDocSecurityConfiguration.class) @WithMockUser(roles = RoleConstants.ROLE_SUPERADMIN) @ActiveProfiles({ Profiles.TEST, Profiles.ADMIN_ACCESS }) @AutoConfigureRestDocs(uriScheme = "https", uriHost = ExampleConstants.URI_SECHUB_SERVER, uriPort = 443) @@ -158,7 +157,7 @@ public void restdoc_mark_false_positives() throws Exception { fieldWithPath(PROPERTY_JOBDATA+"[]."+ PROPERTY_FINDINGID).description("SecHub finding identifier - identifies problem inside the job which shall be markeda as a false positive."), fieldWithPath(PROPERTY_JOBDATA+"[]."+ FalsePositiveJobData.PROPERTY_COMMENT).optional().description("A comment describing why this is a false positive"), - fieldWithPath(PROPERTY_PROJECTDATA).description("Porject data list containing false positive setup for the project"), + fieldWithPath(PROPERTY_PROJECTDATA).description("Project data list containing false positive setup for the project"), fieldWithPath(PROPERTY_PROJECTDATA+"[]."+ PROPERTY_ID).description("Identifier which is used to update or remove the respective false positive entry."), fieldWithPath(PROPERTY_PROJECTDATA+"[]."+ FalsePositiveProjectData.PROPERTY_COMMENT).optional().description("A comment describing why this is a false positive."), fieldWithPath(PROPERTY_PROJECTDATA+"[]."+ PROPERTY_WEBSCAN).optional().description("Defines a section for false positives which occur during webscans."), @@ -353,7 +352,7 @@ public void user_fetches_false_positive_configuration() throws Exception { fieldWithPath(PROPERTY_FALSE_POSITIVES+"[]."+FalsePositiveEntry.PROPERTY_JOBDATA+"."+PROPERTY_FINDINGID).description("SecHub finding identifier - identifies problem inside the job which shall be markeda as a false positive. *ATTENTION*: at the moment only code scan false positive handling is supported. Infra and web scan findings will lead to a non accepted error!"), fieldWithPath(PROPERTY_FALSE_POSITIVES+"[]."+FalsePositiveEntry.PROPERTY_JOBDATA+"."+FalsePositiveJobData.PROPERTY_COMMENT).optional().description("A comment from author describing why this was marked as a false positive"), - fieldWithPath(PROPERTY_FALSE_POSITIVES+"[]."+PROPERTY_PROJECTDATA).optional().description("Porject data list containing false positive setup for the project."), + fieldWithPath(PROPERTY_FALSE_POSITIVES+"[]."+PROPERTY_PROJECTDATA).optional().description("Project data list containing false positive setup for the project."), fieldWithPath(PROPERTY_FALSE_POSITIVES+"[]."+PROPERTY_PROJECTDATA+"."+ PROPERTY_ID).description("Identifier which is used to update or remove the respective false positive entry."), fieldWithPath(PROPERTY_FALSE_POSITIVES+"[]."+PROPERTY_PROJECTDATA+"."+ FalsePositiveProjectData.PROPERTY_COMMENT).optional().description("A comment describing why this is a false positive."), fieldWithPath(PROPERTY_FALSE_POSITIVES+"[]."+PROPERTY_PROJECTDATA+"."+ PROPERTY_WEBSCAN).optional().description("Defines a section for false positives which occur during webscans."), @@ -370,10 +369,4 @@ public void user_fetches_false_positive_configuration() throws Exception { /* @formatter:on */ } - @Profile(Profiles.TEST) - @EnableAutoConfiguration - public static class SimpleTestConfiguration extends AbstractSecHubAPISecurityConfiguration { - - } - } diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/JobAdministrationRestControllerRestDocTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/JobAdministrationRestControllerRestDocTest.java index 9f1e1bc123..79791039db 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/JobAdministrationRestControllerRestDocTest.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/JobAdministrationRestControllerRestDocTest.java @@ -21,10 +21,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.ActiveProfiles; @@ -40,7 +40,6 @@ import com.mercedesbenz.sechub.domain.administration.job.JobRestartRequestService; import com.mercedesbenz.sechub.domain.administration.job.JobStatus; import com.mercedesbenz.sechub.sharedkernel.Profiles; -import com.mercedesbenz.sechub.sharedkernel.security.AbstractSecHubAPISecurityConfiguration; import com.mercedesbenz.sechub.sharedkernel.security.RoleConstants; import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseRestDoc; import com.mercedesbenz.sechub.sharedkernel.usecases.job.UseCaseAdminCancelsJob; @@ -52,8 +51,9 @@ import com.mercedesbenz.sechub.test.TestPortProvider; @RunWith(SpringRunner.class) -@WebMvcTest(JobAdministrationRestController.class) -@ContextConfiguration(classes = { JobAdministrationRestController.class, JobAdministrationRestControllerRestDocTest.SimpleTestConfiguration.class }) +@WebMvcTest +@ContextConfiguration(classes = { JobAdministrationRestController.class }) +@Import(TestRestDocSecurityConfiguration.class) @WithMockUser(roles = RoleConstants.ROLE_SUPERADMIN) @ActiveProfiles({ Profiles.TEST, Profiles.ADMIN_ACCESS }) @AutoConfigureRestDocs(uriScheme = "https", uriHost = ExampleConstants.URI_SECHUB_SERVER, uriPort = 443) @@ -230,9 +230,4 @@ private static String inArray(String field) { return "[]." + field; } - @EnableAutoConfiguration - public static class SimpleTestConfiguration extends AbstractSecHubAPISecurityConfiguration { - - } - } diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/MappingAdministrationRestControllerRestDocTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/MappingAdministrationRestControllerRestDocTest.java index f421f8b05f..d512589bce 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/MappingAdministrationRestControllerRestDocTest.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/MappingAdministrationRestControllerRestDocTest.java @@ -18,11 +18,9 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Profile; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.ActiveProfiles; @@ -36,7 +34,6 @@ import com.mercedesbenz.sechub.domain.administration.status.StatusAdministrationRestController; import com.mercedesbenz.sechub.domain.administration.status.StatusEntry; import com.mercedesbenz.sechub.sharedkernel.Profiles; -import com.mercedesbenz.sechub.sharedkernel.security.AbstractSecHubAPISecurityConfiguration; import com.mercedesbenz.sechub.sharedkernel.security.RoleConstants; import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseRestDoc; import com.mercedesbenz.sechub.sharedkernel.usecases.admin.status.UseCaseAdminListsStatusInformation; @@ -45,8 +42,8 @@ import com.mercedesbenz.sechub.test.TestPortProvider; @RunWith(SpringRunner.class) -@WebMvcTest(StatusAdministrationRestController.class) -@ContextConfiguration(classes = { StatusAdministrationRestController.class, MappingAdministrationRestControllerRestDocTest.SimpleTestConfiguration.class }) +@WebMvcTest +@ContextConfiguration(classes = { StatusAdministrationRestController.class }) @WithMockUser(roles = RoleConstants.ROLE_SUPERADMIN) @ActiveProfiles({ Profiles.TEST, Profiles.ADMIN_ACCESS }) @AutoConfigureRestDocs(uriScheme = "https", uriHost = ExampleConstants.URI_SECHUB_SERVER, uriPort = 443) @@ -121,10 +118,4 @@ public void restdoc_admin_lists_status_information() throws Exception { /* @formatter:on */ } - @Profile(Profiles.TEST) - @EnableAutoConfiguration - public static class SimpleTestConfiguration extends AbstractSecHubAPISecurityConfiguration { - - } - } diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/OpenApiSchema.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/OpenApiSchema.java index 70ac1df00c..b2ddd75914 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/OpenApiSchema.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/OpenApiSchema.java @@ -64,6 +64,10 @@ enum OpenApiSchema { ENCRYPTION_STATUS("EncryptionStatus"), + TEMPLATES("Templates"), + + ASSETS("Assets"), + ; private final Schema schema; diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ProductExecutionProfileRestControllerRestDocTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ProductExecutionProfileRestControllerRestDocTest.java index c9885de327..124a0565bc 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ProductExecutionProfileRestControllerRestDocTest.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ProductExecutionProfileRestControllerRestDocTest.java @@ -18,12 +18,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.ActiveProfiles; @@ -50,7 +48,6 @@ import com.mercedesbenz.sechub.sharedkernel.ProductIdentifier; import com.mercedesbenz.sechub.sharedkernel.Profiles; import com.mercedesbenz.sechub.sharedkernel.logging.AuditLogService; -import com.mercedesbenz.sechub.sharedkernel.security.AbstractSecHubAPISecurityConfiguration; import com.mercedesbenz.sechub.sharedkernel.security.RoleConstants; import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseRestDoc; import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminAssignsExecutionProfileToProject; @@ -70,8 +67,9 @@ import com.mercedesbenz.sechub.test.executorconfig.TestExecutorSetupJobParam; @RunWith(SpringRunner.class) -@WebMvcTest(ProductExecutionProfileRestController.class) -@ContextConfiguration(classes = { ProductExecutionProfileRestController.class, ProductExecutionProfileRestControllerRestDocTest.SimpleTestConfiguration.class }) +@WebMvcTest +@ContextConfiguration(classes = { ProductExecutionProfileRestController.class }) +@Import(TestRestDocSecurityConfiguration.class) @WithMockUser(roles = RoleConstants.ROLE_SUPERADMIN) @ActiveProfiles({ Profiles.TEST, Profiles.ADMIN_ACCESS }) @AutoConfigureRestDocs(uriScheme = "https", uriHost = ExampleConstants.URI_SECHUB_SERVER, uriPort = 443) @@ -437,10 +435,4 @@ public void restDoc_admin_fetches_profiles_list() throws Exception { /* @formatter:on */ } - @TestConfiguration - @Profile(Profiles.TEST) - @EnableAutoConfiguration - public static class SimpleTestConfiguration extends AbstractSecHubAPISecurityConfiguration { - - } } diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ProductExecutorConfigRestControllerRestDocTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ProductExecutorConfigRestControllerRestDocTest.java index 3e168c113a..b42fbd925c 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ProductExecutorConfigRestControllerRestDocTest.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ProductExecutorConfigRestControllerRestDocTest.java @@ -19,12 +19,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.ActiveProfiles; @@ -51,7 +49,6 @@ import com.mercedesbenz.sechub.sharedkernel.ProductIdentifier; import com.mercedesbenz.sechub.sharedkernel.Profiles; import com.mercedesbenz.sechub.sharedkernel.logging.AuditLogService; -import com.mercedesbenz.sechub.sharedkernel.security.AbstractSecHubAPISecurityConfiguration; import com.mercedesbenz.sechub.sharedkernel.security.RoleConstants; import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseRestDoc; import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminCreatesExecutorConfiguration; @@ -66,8 +63,9 @@ import com.mercedesbenz.sechub.test.executorconfig.TestExecutorSetupJobParam; @RunWith(SpringRunner.class) -@WebMvcTest(ProductExecutorConfigRestController.class) -@ContextConfiguration(classes = { ProductExecutorConfigRestController.class, ProductExecutorConfigRestControllerRestDocTest.SimpleTestConfiguration.class }) +@WebMvcTest +@ContextConfiguration(classes = { ProductExecutorConfigRestController.class }) +@Import(TestRestDocSecurityConfiguration.class) @WithMockUser(roles = RoleConstants.ROLE_SUPERADMIN) @ActiveProfiles({ Profiles.TEST, Profiles.ADMIN_ACCESS }) @AutoConfigureRestDocs(uriScheme = "https", uriHost = ExampleConstants.URI_SECHUB_SERVER, uriPort = 443) @@ -365,11 +363,4 @@ public void restDoc_admin_fetches_executor_config_list() throws Exception { /* @formatter:on */ } - - @TestConfiguration - @Profile(Profiles.TEST) - @EnableAutoConfiguration - public static class SimpleTestConfiguration extends AbstractSecHubAPISecurityConfiguration { - - } } diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ProjectAdministrationRestControllerRestDocTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ProjectAdministrationRestControllerRestDocTest.java index 1a30bb2ff3..bcf9ba4b80 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ProjectAdministrationRestControllerRestDocTest.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ProjectAdministrationRestControllerRestDocTest.java @@ -1,25 +1,17 @@ // SPDX-License-Identifier: MIT package com.mercedesbenz.sechub.restdoc; -import static com.mercedesbenz.sechub.restdoc.RestDocumentation.defineRestService; -import static com.mercedesbenz.sechub.test.RestDocPathParameter.PROJECT_ACCESS_LEVEL; -import static com.mercedesbenz.sechub.test.RestDocPathParameter.PROJECT_ID; -import static com.mercedesbenz.sechub.test.RestDocPathParameter.USER_ID; -import static com.mercedesbenz.sechub.test.SecHubTestURLBuilder.https; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; -import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; -import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; -import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static com.mercedesbenz.sechub.restdoc.RestDocumentation.*; +import static com.mercedesbenz.sechub.test.RestDocPathParameter.*; +import static com.mercedesbenz.sechub.test.SecHubTestURLBuilder.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.springframework.restdocs.headers.HeaderDocumentation.*; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.restdocs.request.RequestDocumentation.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import java.lang.annotation.Annotation; import java.net.URI; @@ -34,11 +26,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.ActiveProfiles; @@ -63,15 +54,17 @@ import com.mercedesbenz.sechub.domain.administration.project.ProjectJsonInput.ProjectWhiteList; import com.mercedesbenz.sechub.domain.administration.project.ProjectMetaDataEntity; import com.mercedesbenz.sechub.domain.administration.project.ProjectRepository; +import com.mercedesbenz.sechub.domain.administration.project.ProjectTemplateService; import com.mercedesbenz.sechub.domain.administration.project.ProjectUnassignUserService; import com.mercedesbenz.sechub.domain.administration.project.ProjectUpdateWhitelistService; import com.mercedesbenz.sechub.domain.administration.user.User; import com.mercedesbenz.sechub.server.SecHubWebMvcConfigurer; import com.mercedesbenz.sechub.sharedkernel.Profiles; import com.mercedesbenz.sechub.sharedkernel.project.ProjectAccessLevel; -import com.mercedesbenz.sechub.sharedkernel.security.AbstractSecHubAPISecurityConfiguration; import com.mercedesbenz.sechub.sharedkernel.security.RoleConstants; import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseRestDoc; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminAssignsTemplateToProject; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminUnassignsTemplateFromProject; import com.mercedesbenz.sechub.sharedkernel.usecases.admin.project.UseCaseAdminChangesProjectAccessLevel; import com.mercedesbenz.sechub.sharedkernel.usecases.admin.project.UseCaseAdminChangesProjectDescription; import com.mercedesbenz.sechub.sharedkernel.usecases.admin.project.UseCaseAdminCreatesProject; @@ -86,9 +79,9 @@ import com.mercedesbenz.sechub.test.TestPortProvider; @RunWith(SpringRunner.class) -@WebMvcTest(ProjectAdministrationRestController.class) -@ContextConfiguration(classes = { ProjectAdministrationRestController.class, ProjectAdministrationRestControllerRestDocTest.SimpleTestConfiguration.class, - SecHubWebMvcConfigurer.class }) +@WebMvcTest +@ContextConfiguration(classes = { ProjectAdministrationRestController.class, SecHubWebMvcConfigurer.class }) +@Import(TestRestDocSecurityConfiguration.class) @WithMockUser(roles = RoleConstants.ROLE_SUPERADMIN) @ActiveProfiles({ Profiles.TEST, Profiles.ADMIN_ACCESS }) @AutoConfigureRestDocs(uriScheme = "https", uriHost = ExampleConstants.URI_SECHUB_SERVER, uriPort = 443) @@ -135,6 +128,9 @@ public class ProjectAdministrationRestControllerRestDocTest implements TestIsNec @MockBean ProjectChangeAccessLevelService projectChangeAccessLevelService; + @MockBean + ProjectTemplateService projectTemplateService; + @Before public void before() { when(createProjectInputvalidator.supports(ProjectJsonInput.class)).thenReturn(true); @@ -246,7 +242,7 @@ public void restdoc_delete_project() throws Exception { ), pathParameters( - parameterWithName(PROJECT_ID.paramName()).description("The id for project to delete") + parameterWithName(PROJECT_ID.paramName()).description("The project id to delete") ) )); /* @formatter:on */ @@ -276,7 +272,7 @@ public void restdoc_change_project_owner() throws Exception { ), pathParameters( - parameterWithName(PROJECT_ID.paramName()).description("The id for project"), + parameterWithName(PROJECT_ID.paramName()).description("The project id"), parameterWithName(USER_ID.paramName()).description("The user id of the user to assign to project as the owner") ) )); @@ -324,7 +320,7 @@ public void restdoc_change_project_access_level() throws Exception { ), pathParameters( - parameterWithName(PROJECT_ID.paramName()).description("The id for project"), + parameterWithName(PROJECT_ID.paramName()).description("The project id"), parameterWithName(PROJECT_ACCESS_LEVEL.paramName()).description("The new project access level. "+acceptedValues) ) )); @@ -356,7 +352,7 @@ public void restdoc_assign_user2project() throws Exception { ), pathParameters( - parameterWithName(PROJECT_ID.paramName()).description("The id for project"), + parameterWithName(PROJECT_ID.paramName()).description("The project id"), parameterWithName(USER_ID.paramName()).description("The user id of the user to assign to project") ) )); @@ -388,7 +384,7 @@ public void restdoc_unassign_userFromProject() throws Exception { ), pathParameters( - parameterWithName(PROJECT_ID.paramName()).description("The id for project"), + parameterWithName(PROJECT_ID.paramName()).description("The project id"), parameterWithName(USER_ID.paramName()).description("The user id of the user to unassign from project") ) )); @@ -456,13 +452,14 @@ public void restdoc_show_project_details() throws Exception { ), pathParameters( - parameterWithName(PROJECT_ID.paramName()).description("The id for project to show details for") + parameterWithName(PROJECT_ID.paramName()).description("The project id to show details for") ), responseFields( fieldWithPath(ProjectDetailInformation.PROPERTY_PROJECT_ID).description("The name of the project"), fieldWithPath(ProjectDetailInformation.PROPERTY_USERS).description("A list of all users having access to the project"), fieldWithPath(ProjectDetailInformation.PROPERTY_OWNER).description("Username of the owner of this project. An owner is the person in charge."), fieldWithPath(ProjectDetailInformation.PROPERTY_WHITELIST).description("A list of all whitelisted URIs. Only these ones can be scanned for the project!"), + fieldWithPath(ProjectDetailInformation.PROPERTY_TEMPLATE_IDS).description("A list of all templates assigned to the project"), fieldWithPath(ProjectDetailInformation.PROPERTY_METADATA).description("An JSON object containing metadata key-value pairs defined for this project."), fieldWithPath(ProjectDetailInformation.PROPERTY_METADATA + ".key1").description("An arbitrary metadata key"), fieldWithPath(ProjectDetailInformation.PROPERTY_ACCESSLEVEL).description("The project access level"), @@ -535,15 +532,17 @@ public void restdoc_change_project_description() throws Exception { ), pathParameters( - parameterWithName(PROJECT_ID.paramName()).description("The id for project to change details for") + parameterWithName(PROJECT_ID.paramName()).description("The project id to change details for") ), responseFields( fieldWithPath(ProjectDetailInformation.PROPERTY_PROJECT_ID).description("The name of the project."), fieldWithPath(ProjectDetailInformation.PROPERTY_USERS).description("A list of all users having access to the project."), fieldWithPath(ProjectDetailInformation.PROPERTY_OWNER).description("Username of the owner of this project. An owner is the person in charge."), fieldWithPath(ProjectDetailInformation.PROPERTY_WHITELIST).description("A list of all whitelisted URIs. Only these ones can be scanned for the project!"), + fieldWithPath(ProjectDetailInformation.PROPERTY_TEMPLATE_IDS).description("A list of all templates assigned to the project"), fieldWithPath(ProjectDetailInformation.PROPERTY_METADATA).description("An JSON object containing metadata key-value pairs defined for this project."), - fieldWithPath(ProjectDetailInformation.PROPERTY_METADATA + ".key1").description("An arbitrary metadata key."), + fieldWithPath(ProjectDetailInformation.PROPERTY_METADATA + ".key1").description("An arbitrary metadata key"), + fieldWithPath(ProjectDetailInformation.PROPERTY_ACCESSLEVEL).description("The project access level"), fieldWithPath(ProjectDetailInformation.PROPERTY_ACCESSLEVEL).description("The project access level"), fieldWithPath(ProjectDetailInformation.PROPERTY_DESCRIPTION).description("The project description.") ) @@ -552,10 +551,68 @@ public void restdoc_change_project_description() throws Exception { /* @formatter:on */ } - @Profile(Profiles.TEST) - @EnableAutoConfiguration - public static class SimpleTestConfiguration extends AbstractSecHubAPISecurityConfiguration { + @Test + @UseCaseRestDoc(useCase = UseCaseAdminAssignsTemplateToProject.class) + public void restdoc_assign_template2project() throws Exception { + /* prepare */ + String apiEndpoint = https(PORT_USED).buildAdminAssignsTemplateToProjectUrl(TEMPLATE_ID.pathElement(), PROJECT_ID.pathElement()); + Class useCase = UseCaseAdminAssignsTemplateToProject.class; + + /* execute + test @formatter:off */ + mockMvc.perform( + put(apiEndpoint, "projectId1", "template1"). + contentType(MediaType.APPLICATION_JSON_VALUE). + header(AuthenticationHelper.HEADER_NAME, AuthenticationHelper.getHeaderValue()) + ). + andExpect(status().isOk()). + andDo(defineRestService(). + with(). + useCaseData(useCase). + tag(RestDocFactory.extractTag(apiEndpoint)). + and(). + document( + requestHeaders( + ), + pathParameters( + parameterWithName(PROJECT_ID.paramName()).description("The project id"), + parameterWithName(TEMPLATE_ID.paramName()).description("The id of the template to assign to project") + ) + )); + + /* @formatter:on */ + } + + @Test + @UseCaseRestDoc(useCase = UseCaseAdminUnassignsTemplateFromProject.class) + public void restdoc_unassign_templateFromproject() throws Exception { + /* prepare */ + String apiEndpoint = https(PORT_USED).buildAdminUnAssignsTemplateToProjectUrl(TEMPLATE_ID.pathElement(), PROJECT_ID.pathElement()); + Class useCase = UseCaseAdminUnassignsTemplateFromProject.class; + + /* execute + test @formatter:off */ + mockMvc.perform( + delete(apiEndpoint, "projectId1", "template1"). + contentType(MediaType.APPLICATION_JSON_VALUE). + header(AuthenticationHelper.HEADER_NAME, AuthenticationHelper.getHeaderValue()) + ). + andExpect(status().isOk()). + andDo(defineRestService(). + with(). + useCaseData(useCase). + tag(RestDocFactory.extractTag(apiEndpoint)). + and(). + document( + requestHeaders( + + ), + pathParameters( + parameterWithName(PROJECT_ID.paramName()).description("The project id"), + parameterWithName(TEMPLATE_ID.paramName()).description("The id of the template to unassign from project") + ) + )); + + /* @formatter:on */ } } diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ProjectRestControllerRestDocTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ProjectRestControllerRestDocTest.java index 0e19c77d5c..b03ded0b61 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ProjectRestControllerRestDocTest.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ProjectRestControllerRestDocTest.java @@ -43,7 +43,7 @@ import com.mercedesbenz.sechub.test.TestPortProvider; @RunWith(SpringRunner.class) -@WebMvcTest(ProjectRestController.class) +@WebMvcTest @ContextConfiguration(classes = { ProjectRestController.class }) @WithMockUser(roles = RoleConstants.ROLE_USER) @ExtendWith(RestDocumentationExtension.class) diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ProjectUpdateAdministrationRestControllerRestDocTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ProjectUpdateAdministrationRestControllerRestDocTest.java index 072c1a2c0f..492bfb9baf 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ProjectUpdateAdministrationRestControllerRestDocTest.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ProjectUpdateAdministrationRestControllerRestDocTest.java @@ -17,11 +17,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.ActiveProfiles; @@ -38,7 +37,6 @@ import com.mercedesbenz.sechub.domain.administration.project.ProjectUpdateWhitelistService; import com.mercedesbenz.sechub.domain.administration.project.UpdateProjectInputValidator; import com.mercedesbenz.sechub.sharedkernel.Profiles; -import com.mercedesbenz.sechub.sharedkernel.security.AbstractSecHubAPISecurityConfiguration; import com.mercedesbenz.sechub.sharedkernel.security.RoleConstants; import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseRestDoc; import com.mercedesbenz.sechub.sharedkernel.usecases.admin.project.UseCaseUpdateProjectMetaData; @@ -48,9 +46,9 @@ import com.mercedesbenz.sechub.test.TestPortProvider; @RunWith(SpringRunner.class) -@WebMvcTest(ProjectUpdateAdministrationRestController.class) -@ContextConfiguration(classes = { ProjectUpdateAdministrationRestController.class, - ProjectUpdateAdministrationRestControllerRestDocTest.SimpleTestConfiguration.class }) +@WebMvcTest +@ContextConfiguration(classes = { ProjectUpdateAdministrationRestController.class }) +@Import(TestRestDocSecurityConfiguration.class) @WithMockUser(roles = RoleConstants.ROLE_SUPERADMIN) @ActiveProfiles({ Profiles.TEST, Profiles.ADMIN_ACCESS }) @AutoConfigureRestDocs(uriScheme = "https", uriHost = ExampleConstants.URI_SECHUB_SERVER, uriPort = 443) @@ -154,10 +152,4 @@ public void restdoc_update_metadata_for_project() throws Exception { /* @formatter:on */ } - @Profile(Profiles.TEST) - @EnableAutoConfiguration - public static class SimpleTestConfiguration extends AbstractSecHubAPISecurityConfiguration { - - } - } diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ScanProjectMockDataRestControllerRestDocTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ScanProjectMockDataRestControllerRestDocTest.java index af47f1c69c..c3272643ae 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ScanProjectMockDataRestControllerRestDocTest.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ScanProjectMockDataRestControllerRestDocTest.java @@ -14,11 +14,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.ActiveProfiles; @@ -33,7 +32,6 @@ import com.mercedesbenz.sechub.domain.scan.project.ScanProjectMockDataConfigurationService; import com.mercedesbenz.sechub.domain.scan.project.ScanProjectMockDataRestController; import com.mercedesbenz.sechub.sharedkernel.Profiles; -import com.mercedesbenz.sechub.sharedkernel.security.AbstractSecHubAPISecurityConfiguration; import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseRestDoc; import com.mercedesbenz.sechub.sharedkernel.usecases.user.UseCaseUserDefinesProjectMockdata; import com.mercedesbenz.sechub.sharedkernel.usecases.user.UseCaseUserRetrievesProjectMockdata; @@ -43,8 +41,9 @@ import com.mercedesbenz.sechub.test.TestPortProvider; @RunWith(SpringRunner.class) -@WebMvcTest(ScanProjectMockDataRestController.class) -@ContextConfiguration(classes = { ScanProjectMockDataRestController.class, ScanProjectMockDataRestControllerRestDocTest.SimpleTestConfiguration.class }) +@WebMvcTest +@ContextConfiguration(classes = { ScanProjectMockDataRestController.class }) +@Import(TestRestDocSecurityConfiguration.class) @AutoConfigureRestDocs(uriScheme = "https", uriHost = ExampleConstants.URI_SECHUB_SERVER, uriPort = 443) @ActiveProfiles({ Profiles.MOCKED_PRODUCTS, Profiles.TEST }) public class ScanProjectMockDataRestControllerRestDocTest implements TestIsNecessaryForDocumentation { @@ -140,12 +139,6 @@ public void get_project_mock_configuration() throws Exception { /* @formatter:on */ } - @TestConfiguration - @EnableAutoConfiguration - public static class SimpleTestConfiguration extends AbstractSecHubAPISecurityConfiguration { - - } - @Before public void before() throws Exception { } diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/SchedulerAdministrationRestControllerRestDocTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/SchedulerAdministrationRestControllerRestDocTest.java index dcb5773924..f87d1588d1 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/SchedulerAdministrationRestControllerRestDocTest.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/SchedulerAdministrationRestControllerRestDocTest.java @@ -13,11 +13,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.ActiveProfiles; @@ -30,7 +29,6 @@ import com.mercedesbenz.sechub.domain.administration.scheduler.SwitchSchedulerJobProcessingService; import com.mercedesbenz.sechub.domain.administration.scheduler.TriggerSchedulerStatusRefreshService; import com.mercedesbenz.sechub.sharedkernel.Profiles; -import com.mercedesbenz.sechub.sharedkernel.security.AbstractSecHubAPISecurityConfiguration; import com.mercedesbenz.sechub.sharedkernel.security.RoleConstants; import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseRestDoc; import com.mercedesbenz.sechub.sharedkernel.usecases.admin.schedule.UseCaseAdminDisablesSchedulerJobProcessing; @@ -41,8 +39,9 @@ import com.mercedesbenz.sechub.test.TestPortProvider; @RunWith(SpringRunner.class) -@WebMvcTest(SchedulerAdministrationRestController.class) -@ContextConfiguration(classes = { SchedulerAdministrationRestController.class, SchedulerAdministrationRestControllerRestDocTest.SimpleTestConfiguration.class }) +@WebMvcTest +@ContextConfiguration(classes = { SchedulerAdministrationRestController.class }) +@Import(TestRestDocSecurityConfiguration.class) @WithMockUser(roles = RoleConstants.ROLE_SUPERADMIN) @ActiveProfiles({ Profiles.TEST, Profiles.ADMIN_ACCESS }) @AutoConfigureRestDocs(uriScheme = "https", uriHost = ExampleConstants.URI_SECHUB_SERVER, uriPort = 443) @@ -147,10 +146,4 @@ public void restdoc_admin_enables_scheduler_job_processing() throws Exception { /* @formatter:on */ } - @Profile(Profiles.TEST) - @EnableAutoConfiguration - public static class SimpleTestConfiguration extends AbstractSecHubAPISecurityConfiguration { - - } - } diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/SchedulerRestControllerRestDocTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/SchedulerRestControllerRestDocTest.java index 97c1cea3b8..3ec02f1493 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/SchedulerRestControllerRestDocTest.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/SchedulerRestControllerRestDocTest.java @@ -3,17 +3,17 @@ import static com.mercedesbenz.sechub.commons.core.CommonConstants.*; import static com.mercedesbenz.sechub.commons.model.SecHubConfigurationModel.*; -import static com.mercedesbenz.sechub.commons.model.TestSecHubConfigurationBuilder.*; -import static com.mercedesbenz.sechub.restdoc.RestDocumentation.*; +import static com.mercedesbenz.sechub.commons.model.TestSecHubConfigurationBuilder.configureSecHub; +import static com.mercedesbenz.sechub.restdoc.RestDocumentation.defineRestService; import static com.mercedesbenz.sechub.test.RestDocPathParameter.*; -import static com.mercedesbenz.sechub.test.SecHubTestURLBuilder.*; +import static com.mercedesbenz.sechub.test.SecHubTestURLBuilder.https; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import static org.springframework.restdocs.headers.HeaderDocumentation.*; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; import static org.springframework.restdocs.payload.PayloadDocumentation.*; import static org.springframework.restdocs.request.RequestDocumentation.*; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import java.io.InputStream; @@ -26,12 +26,10 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; import org.springframework.restdocs.RestDocumentationExtension; @@ -42,63 +40,30 @@ import org.springframework.util.StringUtils; import com.mercedesbenz.sechub.commons.core.CommonConstants; -import com.mercedesbenz.sechub.commons.model.ClientCertificateConfiguration; -import com.mercedesbenz.sechub.commons.model.HTTPHeaderConfiguration; -import com.mercedesbenz.sechub.commons.model.SecHubCodeScanConfiguration; -import com.mercedesbenz.sechub.commons.model.SecHubConfigurationMetaData; -import com.mercedesbenz.sechub.commons.model.SecHubDataConfiguration; -import com.mercedesbenz.sechub.commons.model.SecHubDataConfigurationUsageByName; -import com.mercedesbenz.sechub.commons.model.SecHubFileSystemConfiguration; -import com.mercedesbenz.sechub.commons.model.SecHubInfrastructureScanConfiguration; -import com.mercedesbenz.sechub.commons.model.SecHubSourceDataConfiguration; -import com.mercedesbenz.sechub.commons.model.SecHubTimeUnit; -import com.mercedesbenz.sechub.commons.model.SecHubWebScanApiConfiguration; -import com.mercedesbenz.sechub.commons.model.SecHubWebScanApiType; -import com.mercedesbenz.sechub.commons.model.SecHubWebScanConfiguration; -import com.mercedesbenz.sechub.commons.model.TrafficLight; -import com.mercedesbenz.sechub.commons.model.WebScanDurationConfiguration; +import com.mercedesbenz.sechub.commons.model.*; import com.mercedesbenz.sechub.commons.model.job.ExecutionResult; import com.mercedesbenz.sechub.commons.model.job.ExecutionState; -import com.mercedesbenz.sechub.commons.model.login.ActionType; -import com.mercedesbenz.sechub.commons.model.login.FormLoginConfiguration; -import com.mercedesbenz.sechub.commons.model.login.TOTPHashAlgorithm; -import com.mercedesbenz.sechub.commons.model.login.WebLoginConfiguration; -import com.mercedesbenz.sechub.commons.model.login.WebLoginTOTPConfiguration; +import com.mercedesbenz.sechub.commons.model.login.*; import com.mercedesbenz.sechub.docgen.util.RestDocFactory; import com.mercedesbenz.sechub.docgen.util.RestDocTestFileSupport; -import com.mercedesbenz.sechub.domain.schedule.ScheduleJobStatus; -import com.mercedesbenz.sechub.domain.schedule.SchedulerApproveJobService; -import com.mercedesbenz.sechub.domain.schedule.SchedulerBinariesUploadService; -import com.mercedesbenz.sechub.domain.schedule.SchedulerCreateJobService; -import com.mercedesbenz.sechub.domain.schedule.SchedulerGetJobStatusService; -import com.mercedesbenz.sechub.domain.schedule.SchedulerRestController; -import com.mercedesbenz.sechub.domain.schedule.SchedulerResult; -import com.mercedesbenz.sechub.domain.schedule.SchedulerSourcecodeUploadService; +import com.mercedesbenz.sechub.domain.schedule.*; import com.mercedesbenz.sechub.domain.schedule.access.ScheduleAccess; import com.mercedesbenz.sechub.domain.schedule.access.ScheduleAccess.ProjectAccessCompositeKey; import com.mercedesbenz.sechub.domain.schedule.access.ScheduleAccessRepository; -import com.mercedesbenz.sechub.domain.schedule.job.ScheduleSecHubJob; -import com.mercedesbenz.sechub.domain.schedule.job.SecHubJobInfoForUser; -import com.mercedesbenz.sechub.domain.schedule.job.SecHubJobInfoForUserListPage; -import com.mercedesbenz.sechub.domain.schedule.job.SecHubJobInfoForUserService; -import com.mercedesbenz.sechub.domain.schedule.job.SecHubJobRepository; +import com.mercedesbenz.sechub.domain.schedule.job.*; import com.mercedesbenz.sechub.sharedkernel.Profiles; import com.mercedesbenz.sechub.sharedkernel.configuration.SecHubConfiguration; import com.mercedesbenz.sechub.sharedkernel.configuration.SecHubConfigurationValidator; -import com.mercedesbenz.sechub.sharedkernel.security.AbstractSecHubAPISecurityConfiguration; import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseRestDoc; import com.mercedesbenz.sechub.sharedkernel.usecases.job.UseCaseUserListsJobsForProject; -import com.mercedesbenz.sechub.sharedkernel.usecases.user.execute.UseCaseUserApprovesJob; -import com.mercedesbenz.sechub.sharedkernel.usecases.user.execute.UseCaseUserChecksJobStatus; -import com.mercedesbenz.sechub.sharedkernel.usecases.user.execute.UseCaseUserCreatesNewJob; -import com.mercedesbenz.sechub.sharedkernel.usecases.user.execute.UseCaseUserUploadsBinaries; -import com.mercedesbenz.sechub.sharedkernel.usecases.user.execute.UseCaseUserUploadsSourceCode; +import com.mercedesbenz.sechub.sharedkernel.usecases.user.execute.*; import com.mercedesbenz.sechub.test.ExampleConstants; import com.mercedesbenz.sechub.test.TestIsNecessaryForDocumentation; import com.mercedesbenz.sechub.test.TestPortProvider; -@WebMvcTest(SchedulerRestController.class) -@ContextConfiguration(classes = { SchedulerRestController.class, SchedulerRestControllerRestDocTest.SimpleTestConfiguration.class }) +@WebMvcTest +@ContextConfiguration(classes = { SchedulerRestController.class }) +@Import(TestRestDocSecurityConfiguration.class) @WithMockUser @ExtendWith(RestDocumentationExtension.class) @ActiveProfiles(Profiles.TEST) @@ -755,7 +720,7 @@ public void restDoc_userCreatesNewJob_webScan_login_form_script_and_totp_as_seco webConfig(). addURI("https://localhost/mywebapp"). login("https://localhost/mywebapp/login"). - totp("example-seed", 30, TOTPHashAlgorithm.HMAC_SHA1, 6). + totp("example-seed", 30, TOTPHashAlgorithm.HMAC_SHA1, 6, EncodingType.BASE32). formScripted("username1","password1"). createPage(). createAction(). @@ -817,6 +782,7 @@ public void restDoc_userCreatesNewJob_webScan_login_form_script_and_totp_as_seco fieldWithPath(PROPERTY_WEB_SCAN+"."+SecHubWebScanConfiguration.PROPERTY_LOGIN+"."+WebLoginConfiguration.PROPERTY_TOTP+"."+WebLoginTOTPConfiguration.PROPERTY_VALIDITY_IN_SECONDS).description("The time in seconds the generated TOTP is valid. In most cases nothing is specified and the default of '"+WebLoginTOTPConfiguration.DEFAULT_VALIDITY_IN_SECONDS+"' seconds is used.").optional(), fieldWithPath(PROPERTY_WEB_SCAN+"."+SecHubWebScanConfiguration.PROPERTY_LOGIN+"."+WebLoginConfiguration.PROPERTY_TOTP+"."+WebLoginTOTPConfiguration.PROPERTY_TOKEN_LENGTH).description("The length of the generated TOTP. In most cases nothing is specified and the default length '"+WebLoginTOTPConfiguration.DEFAULT_TOKEN_LENGTH+"' is used.").optional(), fieldWithPath(PROPERTY_WEB_SCAN+"."+SecHubWebScanConfiguration.PROPERTY_LOGIN+"."+WebLoginConfiguration.PROPERTY_TOTP+"."+WebLoginTOTPConfiguration.PROPERTY_HASH_ALGORITHM).description("The hash algorithm to generate the TOTP. In most cases nothing is specified and the default hash algorithm '"+WebLoginTOTPConfiguration.DEFAULT_HASH_ALGORITHM+"' is used. Currently available values are: 'HMAC_SHA1', 'HMAC_SHA256', 'HMAC_SHA512'").optional(), + fieldWithPath(PROPERTY_WEB_SCAN+"."+SecHubWebScanConfiguration.PROPERTY_LOGIN+"."+WebLoginConfiguration.PROPERTY_TOTP+"."+WebLoginTOTPConfiguration.PROPERTY_ENCODING_TYPE).description("The encoding type of the 'seed'. The default value is '"+WebLoginTOTPConfiguration.DEFAULT_ENCODING_TYPE+"'. Currently available values are: 'BASE64', 'BASE32', 'HEX', 'PLAIN', 'AUTODETECT'").optional(), fieldWithPath(PROPERTY_WEB_SCAN+"."+SecHubWebScanConfiguration.PROPERTY_LOGIN+".url").description("Login URL").optional(), fieldWithPath(PROPERTY_WEB_SCAN+"."+SecHubWebScanConfiguration.PROPERTY_LOGIN+"."+FORM).description("form login definition").optional(), fieldWithPath(PROPERTY_WEB_SCAN+"."+SecHubWebScanConfiguration.PROPERTY_LOGIN+"."+FORM+"."+SCRIPT).description("script").optional(), @@ -1225,11 +1191,4 @@ public void before() { when(sechubConfigurationValidator.supports(SecHubConfiguration.class)).thenReturn(true); } - - @TestConfiguration - @Profile(Profiles.TEST) - @EnableAutoConfiguration - public static class SimpleTestConfiguration extends AbstractSecHubAPISecurityConfiguration { - - } } diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ServerInfoAdministrationRestControllerRestDocTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ServerInfoAdministrationRestControllerRestDocTest.java index 7b8451a531..8ad177272a 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ServerInfoAdministrationRestControllerRestDocTest.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ServerInfoAdministrationRestControllerRestDocTest.java @@ -15,11 +15,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.ActiveProfiles; @@ -32,7 +31,6 @@ import com.mercedesbenz.sechub.server.core.ServerInfoAdministrationRestController; import com.mercedesbenz.sechub.server.core.ServerRuntimeData; import com.mercedesbenz.sechub.sharedkernel.Profiles; -import com.mercedesbenz.sechub.sharedkernel.security.AbstractSecHubAPISecurityConfiguration; import com.mercedesbenz.sechub.sharedkernel.security.RoleConstants; import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseRestDoc; import com.mercedesbenz.sechub.sharedkernel.usecases.admin.status.UseCaseAdminFetchesServerRuntimeData; @@ -41,9 +39,9 @@ import com.mercedesbenz.sechub.test.TestPortProvider; @RunWith(SpringRunner.class) -@WebMvcTest(ServerInfoAdministrationRestController.class) -@ContextConfiguration(classes = { ServerInfoAdministrationRestController.class, - ServerInfoAdministrationRestControllerRestDocTest.SimpleTestConfiguration.class }) +@WebMvcTest +@ContextConfiguration(classes = { ServerInfoAdministrationRestController.class }) +@Import(TestRestDocSecurityConfiguration.class) @WithMockUser(roles = RoleConstants.ROLE_SUPERADMIN) @ActiveProfiles({ Profiles.TEST, Profiles.ADMIN_ACCESS }) @AutoConfigureRestDocs(uriScheme = "https", uriHost = ExampleConstants.URI_SECHUB_SERVER, uriPort = 443) @@ -95,9 +93,4 @@ public void restdoc_admin_get_server_version_as_Json() throws Exception { /* @formatter:on */ } - @Profile(Profiles.TEST) - @EnableAutoConfiguration - public static class SimpleTestConfiguration extends AbstractSecHubAPISecurityConfiguration { - - } } diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/SignupAdministrationRestControllerRestDocTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/SignupAdministrationRestControllerRestDocTest.java index d02576634e..db7eba9903 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/SignupAdministrationRestControllerRestDocTest.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/SignupAdministrationRestControllerRestDocTest.java @@ -19,11 +19,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.Import; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.ActiveProfiles; @@ -37,7 +36,6 @@ import com.mercedesbenz.sechub.domain.administration.signup.SignupDeleteService; import com.mercedesbenz.sechub.domain.administration.signup.SignupRepository; import com.mercedesbenz.sechub.sharedkernel.Profiles; -import com.mercedesbenz.sechub.sharedkernel.security.AbstractSecHubAPISecurityConfiguration; import com.mercedesbenz.sechub.sharedkernel.security.RoleConstants; import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseRestDoc; import com.mercedesbenz.sechub.sharedkernel.usecases.admin.signup.UseCaseAdminDeletesSignup; @@ -48,8 +46,9 @@ import com.mercedesbenz.sechub.test.TestPortProvider; @RunWith(SpringRunner.class) -@WebMvcTest(SignupAdministrationRestController.class) -@ContextConfiguration(classes = { SignupAdministrationRestController.class, SignupAdministrationRestControllerRestDocTest.SimpleTestConfiguration.class }) +@WebMvcTest +@ContextConfiguration(classes = { SignupAdministrationRestController.class }) +@Import(TestRestDocSecurityConfiguration.class) @WithMockUser(roles = RoleConstants.ROLE_SUPERADMIN) @ActiveProfiles({ Profiles.TEST, Profiles.ADMIN_ACCESS }) @AutoConfigureRestDocs(uriScheme = "https", uriHost = ExampleConstants.URI_SECHUB_SERVER, uriPort = 443) @@ -150,10 +149,4 @@ public void restdoc_delete_signup() throws Exception { /* @formatter:on */ } - @Profile(Profiles.TEST) - @EnableAutoConfiguration - public static class SimpleTestConfiguration extends AbstractSecHubAPISecurityConfiguration { - - } - } diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/StatusAdministrationRestControllerRestDocTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/StatusAdministrationRestControllerRestDocTest.java index b59d5ed4dc..3c4d476e66 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/StatusAdministrationRestControllerRestDocTest.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/StatusAdministrationRestControllerRestDocTest.java @@ -18,11 +18,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.ActiveProfiles; @@ -38,7 +37,6 @@ import com.mercedesbenz.sechub.domain.administration.mapping.UpdateMappingService; import com.mercedesbenz.sechub.sharedkernel.Profiles; import com.mercedesbenz.sechub.sharedkernel.mapping.MappingIdentifier; -import com.mercedesbenz.sechub.sharedkernel.security.AbstractSecHubAPISecurityConfiguration; import com.mercedesbenz.sechub.sharedkernel.security.RoleConstants; import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseRestDoc; import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdmiUpdatesMappingConfiguration; @@ -49,8 +47,9 @@ import com.mercedesbenz.sechub.test.TestPortProvider; @RunWith(SpringRunner.class) -@WebMvcTest(MappingAdministrationRestController.class) -@ContextConfiguration(classes = { MappingAdministrationRestController.class, MappingAdministrationRestControllerRestDocTest.SimpleTestConfiguration.class }) +@WebMvcTest +@ContextConfiguration(classes = { MappingAdministrationRestController.class }) +@Import(TestRestDocSecurityConfiguration.class) @WithMockUser(roles = RoleConstants.ROLE_SUPERADMIN) @ActiveProfiles({ Profiles.TEST, Profiles.ADMIN_ACCESS }) @AutoConfigureRestDocs(uriScheme = "https", uriHost = ExampleConstants.URI_SECHUB_SERVER, uriPort = 443) @@ -161,9 +160,4 @@ public void restdoc_admin_updates_mapping_configuration() throws Exception { /* @formatter:on */ } - @Profile(Profiles.TEST) - @EnableAutoConfiguration - public static class SimpleTestConfiguration extends AbstractSecHubAPISecurityConfiguration { - - } } diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/TemplateRestControllerRestDocTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/TemplateRestControllerRestDocTest.java new file mode 100644 index 0000000000..f9e84c6f30 --- /dev/null +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/TemplateRestControllerRestDocTest.java @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.restdoc; + +import static com.mercedesbenz.sechub.commons.model.template.TemplateDefinition.*; +import static com.mercedesbenz.sechub.restdoc.RestDocumentation.*; +import static com.mercedesbenz.sechub.test.RestDocPathParameter.*; +import static com.mercedesbenz.sechub.test.SecHubTestURLBuilder.*; +import static org.mockito.Mockito.*; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.restdocs.request.RequestDocumentation.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.lang.annotation.Annotation; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; + +import com.mercedesbenz.sechub.commons.model.template.TemplateDefinition; +import com.mercedesbenz.sechub.commons.model.template.TemplateDefinition.TemplateVariable; +import com.mercedesbenz.sechub.commons.model.template.TemplateDefinition.TemplateVariableValidation; +import com.mercedesbenz.sechub.commons.model.template.TemplateType; +import com.mercedesbenz.sechub.docgen.util.RestDocFactory; +import com.mercedesbenz.sechub.domain.scan.template.TemplateRepository; +import com.mercedesbenz.sechub.domain.scan.template.TemplateRestController; +import com.mercedesbenz.sechub.domain.scan.template.TemplateService; +import com.mercedesbenz.sechub.sharedkernel.Profiles; +import com.mercedesbenz.sechub.sharedkernel.logging.AuditLogService; +import com.mercedesbenz.sechub.sharedkernel.logging.LogSanitizer; +import com.mercedesbenz.sechub.sharedkernel.security.RoleConstants; +import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseRestDoc; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminCreatesOrUpdatesTemplate; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminDeletesTemplate; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminFetchesAllTemplateIds; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminFetchesTemplate; +import com.mercedesbenz.sechub.test.ExampleConstants; +import com.mercedesbenz.sechub.test.TestIsNecessaryForDocumentation; +import com.mercedesbenz.sechub.test.TestPortProvider; + +@RunWith(SpringRunner.class) +@WebMvcTest(TemplateRestController.class) +@ContextConfiguration(classes = { TemplateRestController.class, TemplateRestControllerRestDocTest.class }) +@WithMockUser(roles = RoleConstants.ROLE_SUPERADMIN) +@ActiveProfiles({ Profiles.TEST, Profiles.ADMIN_ACCESS }) +@Import(TestRestDocSecurityConfiguration.class) +@AutoConfigureRestDocs(uriScheme = "https", uriHost = ExampleConstants.URI_SECHUB_SERVER, uriPort = 443) +public class TemplateRestControllerRestDocTest implements TestIsNecessaryForDocumentation { + + private static final int PORT_USED = TestPortProvider.DEFAULT_INSTANCE.getRestDocTestPort(); + + @Autowired + private MockMvc mockMvc; + + @MockBean + private TemplateRepository templateRepository; + + @MockBean + TemplateService templateService; + + @MockBean + AuditLogService auditLogService; + + @MockBean + LogSanitizer logSanitizer; + + private TemplateDefinition definition; + + private TemplateVariable usernameVariable; + + private TemplateVariable passwordVariable; + + private static final String TEST_TEMPLATE_ID1 = "template1"; + private static final String TEST_TEMPLATE_ID2 = "template2"; + + @Before + public void before() { + definition = new TemplateDefinition(); + definition.setType(TemplateType.WEBSCAN_LOGIN); + definition.setAssetId("asset-id1"); + + usernameVariable = new TemplateVariable(); + usernameVariable.setName("username"); + TemplateVariableValidation usernameValidation = new TemplateVariableValidation(); + usernameValidation.setMinLength(3); + usernameValidation.setMaxLength(15); + usernameValidation.setRegularExpression("[a-zA-Z0-9_-].*"); + + usernameVariable.setValidation(usernameValidation); + + passwordVariable = new TemplateVariable(); + passwordVariable.setName("password"); + TemplateVariableValidation passwordValidation = new TemplateVariableValidation(); + passwordValidation.setMaxLength(20); + + passwordVariable.setValidation(passwordValidation); + + definition.getVariables().add(usernameVariable); + } + + @Test + @UseCaseRestDoc(useCase = UseCaseAdminCreatesOrUpdatesTemplate.class) + public void restdoc_admin_creates_or_updates_template() throws Exception { + /* prepare */ + String apiEndpoint = https(PORT_USED).buildAdminCreatesOrUpdatesTemplate(TEMPLATE_ID.pathElement()); + Class useCase = UseCaseAdminCreatesOrUpdatesTemplate.class; + + String content = definition.toFormattedJSON(); + + /* execute + test @formatter:off */ + this.mockMvc.perform( + put(apiEndpoint, TEST_TEMPLATE_ID1). + contentType(MediaType.APPLICATION_JSON_VALUE). + content(content). + header(AuthenticationHelper.HEADER_NAME, AuthenticationHelper.getHeaderValue()) + ). + andExpect(status().isOk()). + andDo(defineRestService(). + with(). + useCaseData(useCase). + tag(RestDocFactory.extractTag(apiEndpoint)). + requestSchema(OpenApiSchema.TEMPLATES.getSchema()). + and(). + document( + requestFields( + fieldWithPath(PROPERTY_TYPE).description("The template type. Must be be defined when a new template is created. An update will ignore changes of this property because the type is immutable! Currently supported types are: "+ TemplateType.values()), + + fieldWithPath(PROPERTY_ASSET_ID).description("The asset id used by the template"), + fieldWithPath(PROPERTY_VARIABLES+"[]."+ TemplateVariable.PROPERTY_NAME).description("The variable name"), + fieldWithPath(PROPERTY_VARIABLES+"[]."+ TemplateVariable.PROPERTY_OPTIONAL).optional().description("Defines if the variable is optional. The default is false"), + fieldWithPath(PROPERTY_VARIABLES+"[]."+ TemplateVariable.PROPERTY_VALIDATION).optional().description("Defines a simple validation segment."), + fieldWithPath(PROPERTY_VARIABLES+"[]."+ TemplateVariable.PROPERTY_VALIDATION+"."+ TemplateVariableValidation.PROPERTY_MIN_LENGTH).optional().description("The minimum content length of this variable"), + fieldWithPath(PROPERTY_VARIABLES+"[]."+ TemplateVariable.PROPERTY_VALIDATION+"."+ TemplateVariableValidation.PROPERTY_MAX_LENGTH).optional().description("The maximum content length of this variable"), + fieldWithPath(PROPERTY_VARIABLES+"[]."+ TemplateVariable.PROPERTY_VALIDATION+"."+ TemplateVariableValidation.PROPERTY_REGULAR_EXPRESSION).optional().description("A regular expression which must match to accept the user input inside the variable") + ), + pathParameters( + parameterWithName(TEMPLATE_ID.paramName()).description("The (unique) template id") + ) + )); + + /* @formatter:on */ + } + + @Test + @UseCaseRestDoc(useCase = UseCaseAdminDeletesTemplate.class) + public void restdoc_admin_deletes_template() throws Exception { + /* prepare */ + String apiEndpoint = https(PORT_USED).buildAdminDeletesTemplate(TEMPLATE_ID.pathElement()); + Class useCase = UseCaseAdminDeletesTemplate.class; + + /* execute + test @formatter:off */ + this.mockMvc.perform( + delete(apiEndpoint, TEST_TEMPLATE_ID1). + contentType(MediaType.APPLICATION_JSON_VALUE). + header(AuthenticationHelper.HEADER_NAME, AuthenticationHelper.getHeaderValue()) + ). + andExpect(status().isOk()). + andDo(defineRestService(). + with(). + useCaseData(useCase). + tag(RestDocFactory.extractTag(apiEndpoint)). + requestSchema(OpenApiSchema.TEMPLATES.getSchema()). + and(). + document( + pathParameters( + parameterWithName(TEMPLATE_ID.paramName()).description("The (unique) template id") + ) + )); + + /* @formatter:on */ + } + + @Test + @UseCaseRestDoc(useCase = UseCaseAdminFetchesTemplate.class) + public void restdoc_admin_fetches_template() throws Exception { + /* prepare */ + definition.setId(TEST_TEMPLATE_ID1); // to have this in result as well, for create/delete it was not necessary, but + // here we want it + when(templateService.fetchTemplateDefinition(TEST_TEMPLATE_ID1)).thenReturn(definition); + + String apiEndpoint = https(PORT_USED).buildAdminFetchesTemplate(TEMPLATE_ID.pathElement()); + Class useCase = UseCaseAdminFetchesTemplate.class; + + /* execute + test @formatter:off */ + this.mockMvc.perform( + get(apiEndpoint, TEST_TEMPLATE_ID1). + contentType(MediaType.APPLICATION_JSON_VALUE). + header(AuthenticationHelper.HEADER_NAME, AuthenticationHelper.getHeaderValue()) + ). + andExpect(status().isOk()). + andDo(defineRestService(). + with(). + useCaseData(useCase). + tag(RestDocFactory.extractTag(apiEndpoint)). + requestSchema(OpenApiSchema.TEMPLATES.getSchema()). + and(). + document( + responseFields( + fieldWithPath(PROPERTY_TYPE).description("The template type. Currently supported types are: "+ TemplateType.values()), + + fieldWithPath(PROPERTY_ID).description("The (unique) template id"), + fieldWithPath(PROPERTY_ASSET_ID).description("The asset id used by the template"), + fieldWithPath(PROPERTY_VARIABLES+"[]."+ TemplateVariable.PROPERTY_NAME).description("The variable name"), + fieldWithPath(PROPERTY_VARIABLES+"[]."+ TemplateVariable.PROPERTY_OPTIONAL).optional().description("Defines if the variable is optional. The default is false"), + fieldWithPath(PROPERTY_VARIABLES+"[]."+ TemplateVariable.PROPERTY_VALIDATION).optional().description("Defines a simple validation segment."), + fieldWithPath(PROPERTY_VARIABLES+"[]."+ TemplateVariable.PROPERTY_VALIDATION+"."+ TemplateVariableValidation.PROPERTY_MIN_LENGTH).optional().description("The minimum content length of this variable"), + fieldWithPath(PROPERTY_VARIABLES+"[]."+ TemplateVariable.PROPERTY_VALIDATION+"."+ TemplateVariableValidation.PROPERTY_MAX_LENGTH).optional().description("The maximum content length of this variable"), + fieldWithPath(PROPERTY_VARIABLES+"[]."+ TemplateVariable.PROPERTY_VALIDATION+"."+ TemplateVariableValidation.PROPERTY_REGULAR_EXPRESSION).optional().description("A regular expression which must match to accept the user input inside the variable") + ), + pathParameters( + parameterWithName(TEMPLATE_ID.paramName()).description("The (unique) template id") + ) + )); + + /* @formatter:on */ + } + + @Test + @UseCaseRestDoc(useCase = UseCaseAdminFetchesAllTemplateIds.class) + public void restdoc_admin_fetches_templatelist() throws Exception { + /* prepare */ + when(templateService.fetchAllTemplateIds()).thenReturn(List.of(TEST_TEMPLATE_ID1, TEST_TEMPLATE_ID2)); + + String apiEndpoint = https(PORT_USED).buildAdminFetchesTemplateList(); + Class useCase = UseCaseAdminFetchesAllTemplateIds.class; + + /* execute + test @formatter:off */ + this.mockMvc.perform( + get(apiEndpoint, TEST_TEMPLATE_ID1). + contentType(MediaType.APPLICATION_JSON_VALUE). + header(AuthenticationHelper.HEADER_NAME, AuthenticationHelper.getHeaderValue()) + ). + andExpect(status().isOk()). + andDo(defineRestService(). + with(). + useCaseData(useCase). + tag(RestDocFactory.extractTag(apiEndpoint)). + requestSchema(OpenApiSchema.TEMPLATES.getSchema()). + and(). + document( + responseFields( + fieldWithPath("[]").description("Array contains all existing template identifiers") + ) + )); + + /* @formatter:on */ + } +} diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/TestRestDocSecurityConfiguration.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/TestRestDocSecurityConfiguration.java new file mode 100644 index 0000000000..78f1d9c9a9 --- /dev/null +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/TestRestDocSecurityConfiguration.java @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.restdoc; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.web.client.RestTemplate; + +import com.mercedesbenz.sechub.sharedkernel.security.SecHubSecurityConfiguration; + +@Import(SecHubSecurityConfiguration.class) +class TestRestDocSecurityConfiguration { + + @Bean + RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/UserAdministrationRestControllerRestDocTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/UserAdministrationRestControllerRestDocTest.java index ded75b8140..15f074a33d 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/UserAdministrationRestControllerRestDocTest.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/UserAdministrationRestControllerRestDocTest.java @@ -21,11 +21,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.Import; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; @@ -46,7 +45,6 @@ import com.mercedesbenz.sechub.domain.administration.user.UserListService; import com.mercedesbenz.sechub.domain.administration.user.UserRevokeSuperAdminRightsService; import com.mercedesbenz.sechub.sharedkernel.Profiles; -import com.mercedesbenz.sechub.sharedkernel.security.AbstractSecHubAPISecurityConfiguration; import com.mercedesbenz.sechub.sharedkernel.security.RoleConstants; import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseRestDoc; import com.mercedesbenz.sechub.sharedkernel.usecases.admin.signup.UseCaseAdminAcceptsSignup; @@ -64,8 +62,9 @@ import com.mercedesbenz.sechub.test.TestPortProvider; @RunWith(SpringRunner.class) -@WebMvcTest(UserAdministrationRestController.class) -@ContextConfiguration(classes = { UserAdministrationRestController.class, UserAdministrationRestControllerRestDocTest.SimpleTestConfiguration.class }) +@WebMvcTest +@ContextConfiguration(classes = { UserAdministrationRestController.class }) +@Import(TestRestDocSecurityConfiguration.class) @WithMockUser(roles = RoleConstants.ROLE_SUPERADMIN) @ActiveProfiles({ Profiles.TEST, Profiles.ADMIN_ACCESS }) @AutoConfigureRestDocs(uriScheme = "https", uriHost = ExampleConstants.URI_SECHUB_SERVER, uriPort = 443) @@ -436,10 +435,4 @@ public void restdoc_show_user_details_for_email_address() throws Exception { /* @formatter:on */ } - @Profile(Profiles.TEST) - @EnableAutoConfiguration - public static class SimpleTestConfiguration extends AbstractSecHubAPISecurityConfiguration { - - } - } diff --git a/sechub-integrationtest/pds/product-scripts/integrationtest-webscan.sh b/sechub-integrationtest/pds/product-scripts/integrationtest-webscan.sh index e049d6a539..c89235bf74 100755 --- a/sechub-integrationtest/pds/product-scripts/integrationtest-webscan.sh +++ b/sechub-integrationtest/pds/product-scripts/integrationtest-webscan.sh @@ -16,6 +16,15 @@ info:PDS_SCAN_CONFIGURATION=$PDS_SCAN_CONFIGURATION dumpPDSVariables + +# We added pds.config.templates.metadata.list as optional parameter here for testing +# So we can dump the variable here - used in scenario12 integration test +dumpVariable "PDS_CONFIG_TEMPLATE_METADATA_LIST" + +ASSET_FILE1="$PDS_JOB_EXTRACTED_ASSETS_FOLDER/webscan-login/testfile1.txt" +TEST_CONTENT_FROM_ASSETFILE=$(cat $ASSET_FILE1) +# Afterwards TEST_CONTENT_FROM_ASSETFILE=i am "testfile1.txt" for scenario12 integration tests +dumpVariable "TEST_CONTENT_FROM_ASSETFILE" if [[ "$PDS_TEST_KEY_VARIANTNAME" = "a" ]]; then diff --git a/sechub-integrationtest/src/main/java/com/mercedesbenz/sechub/integrationtest/api/AsUser.java b/sechub-integrationtest/src/main/java/com/mercedesbenz/sechub/integrationtest/api/AsUser.java index 1e26c38bf5..dd5d0c08cd 100644 --- a/sechub-integrationtest/src/main/java/com/mercedesbenz/sechub/integrationtest/api/AsUser.java +++ b/sechub-integrationtest/src/main/java/com/mercedesbenz/sechub/integrationtest/api/AsUser.java @@ -24,7 +24,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; +import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RequestCallback; import org.springframework.web.client.ResponseExtractor; import org.springframework.web.client.RestClientException; @@ -37,6 +40,9 @@ import com.mercedesbenz.sechub.commons.model.JSONConverter; import com.mercedesbenz.sechub.commons.model.SecHubConfigurationModel; import com.mercedesbenz.sechub.commons.model.TrafficLight; +import com.mercedesbenz.sechub.commons.model.template.TemplateDefinition; +import com.mercedesbenz.sechub.domain.administration.project.ProjectDetailInformation; +import com.mercedesbenz.sechub.domain.scan.asset.AssetDetailData; import com.mercedesbenz.sechub.domain.scan.project.FalsePositiveProjectData; import com.mercedesbenz.sechub.integrationtest.JSONTestSupport; import com.mercedesbenz.sechub.integrationtest.internal.IntegrationTestContext; @@ -1425,4 +1431,111 @@ public SecHubEncryptionStatus fetchEncryptionStatus() { return SecHubEncryptionStatus.fromString(json); } + public AsUser createOrUpdateTemplate(String templateId, TemplateDefinition definition) { + String url = getUrlBuilder().buildAdminCreatesOrUpdatesTemplate(templateId); + getRestHelper().putJSON(url, definition.toFormattedJSON()); + return this; + } + + public TemplateDefinition fetchTemplateDefinitionOrNull(String templateId) { + String url = getUrlBuilder().buildAdminFetchesTemplate(templateId); + try { + String json = getRestHelper().getJSON(url); + return TemplateDefinition.from(json); + + } catch (HttpClientErrorException e) { + HttpStatusCode statusCode = e.getStatusCode(); + if (statusCode.equals(HttpStatus.NOT_FOUND)) { + return null; + } + throw e; + } + } + + public void assignTemplateToProject(String templateid, TestProject project) { + String url = getUrlBuilder().buildAdminAssignsTemplateToProjectUrl(templateid, project.getProjectId()); + getRestHelper().put(url); + } + + public void unassignTemplateFromProject(String templateid, TestProject project) { + String url = getUrlBuilder().buildAdminUnAssignsTemplateToProjectUrl(templateid, project.getProjectId()); + getRestHelper().delete(url); + } + + public ProjectDetailInformation fetchProjectDetailInformation(TestProject project) { + String url = getUrlBuilder().buildAdminFetchProjectInfoUrl(project.getProjectId()); + String json = getRestHelper().getJSON(url); + ProjectDetailInformation result = JSONConverter.get().fromJSON(ProjectDetailInformation.class, json); + return result; + } + + public void deleteTemplate(String templateId) { + String url = getUrlBuilder().buildAdminDeletesTemplate(templateId); + getRestHelper().delete(url); + } + + public List fetchTemplateList() { + String url = getUrlBuilder().buildAdminFetchesTemplateList(); + String json = getRestHelper().getJSON(url); + return JSONConverter.get().fromJSONtoListOf(String.class, json); + } + + public AsUser uploadAssetFile(String assetId, File file) { + String url = getUrlBuilder().buildAdminUploadsAssetFile(assetId); + String checkSum = TestAPI.createSHA256Of(file); + /* @formatter:off */ + autoDumper.execute(() -> getRestHelper().upload(url,file,checkSum) + ); + /* @formatter:on */ + return this; + } + + public AsUser uploadAssetFiles(String assetId, File... files) { + for (File file : files) { + uploadAssetFile(assetId, file); + } + return this; + } + + public File downloadAssetFile(String assetId, String fileName) { + String url = getUrlBuilder().buildAdminDownloadsAssetFile(assetId, fileName); + /* @formatter:off */ + RequestCallback requestCallback = request -> request.getHeaders().setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL)); + + ResponseExtractor responseExtractor = response -> { + Path path = TestUtil.createTempFileInBuildSubFolder("assets/"+assetId, fileName); + Files.copy(response.getBody(), path, StandardCopyOption.REPLACE_EXISTING); + if (TestUtil.isDeletingTempFiles()) { + path.toFile().deleteOnExit(); + } + return path.toFile(); + }; + RestTemplate template = getRestHelper().getTemplate(); + File downloadedAssetFile = template.execute(url, HttpMethod.GET, requestCallback, responseExtractor); + + return downloadedAssetFile; + } + + public List fetchAllAssetIds() { + String url = getUrlBuilder().buildAdminFetchesAllAssetIds(); + String json = getRestHelper().getJSON(url); + return JSONConverter.get().fromJSONtoListOf(String.class, json); + } + + public AssetDetailData fetchAssetDetails(String assetId) { + String url = getUrlBuilder().buildAdminFetchesAssetDetails(assetId); + String json = getRestHelper().getJSON(url); + return JSONConverter.get().fromJSON(AssetDetailData.class, json); + } + + public void deleteAssetFile(String assetId, String fileName) { + String url = getUrlBuilder().buildAdminDeletesAssetFile(assetId, fileName); + getRestHelper().delete(url); + } + + public void deleteAsset(String assetId) { + String url = getUrlBuilder().buildAdminDeletesAsset(assetId); + getRestHelper().delete(url); + } + } diff --git a/sechub-integrationtest/src/main/java/com/mercedesbenz/sechub/integrationtest/api/TestAPI.java b/sechub-integrationtest/src/main/java/com/mercedesbenz/sechub/integrationtest/api/TestAPI.java index f83e330099..08289ece95 100644 --- a/sechub-integrationtest/src/main/java/com/mercedesbenz/sechub/integrationtest/api/TestAPI.java +++ b/sechub-integrationtest/src/main/java/com/mercedesbenz/sechub/integrationtest/api/TestAPI.java @@ -42,6 +42,7 @@ import com.mercedesbenz.sechub.commons.model.SecHubMessagesList; import com.mercedesbenz.sechub.commons.pds.data.PDSJobStatusState; import com.mercedesbenz.sechub.domain.scan.admin.FullScanData; +import com.mercedesbenz.sechub.domain.scan.project.ScanProjectConfig; import com.mercedesbenz.sechub.integrationtest.internal.DefaultTestExecutionProfile; import com.mercedesbenz.sechub.integrationtest.internal.IntegrationTestContext; import com.mercedesbenz.sechub.integrationtest.internal.IntegrationTestDefaultProfiles; @@ -588,6 +589,17 @@ public static void executeUntilSuccessOrTimeout(TestExecutable testExecutable) { return; } + /** + * Tries to execute runnable with default maximum time and retry (4 times a 500 + * milliseconds) Shortcut for + * executeRunnableAndAcceptAssertionsMaximumTimes(4,runnable, 500); + * + * @param runnable + */ + public static void executeResilient(Runnable runnable) { + executeRunnableAndAcceptAssertionsMaximumTimes(4, runnable, 500); + } + public static void executeRunnableAndAcceptAssertionsMaximumTimes(int tries, Runnable runnable, int millisBeforeNextRetry) { executeCallableAndAcceptAssertionsMaximumTimes(tries, () -> { runnable.run(); @@ -1694,4 +1706,10 @@ public static boolean isSecHubTerminating() { return getSuperAdminRestHelper().getBooleanFromURL(url); } + public static List fetchScanProjectConfigurations(TestProject project) { + String url = getURLBuilder().buildIntegrationTestFetchScanProjectConfigurations(project.getProjectId()); + String json = getSuperAdminRestHelper().getJSON(url); + return JSONConverter.get().fromJSONtoListOf(ScanProjectConfig.class, json); + + } } diff --git a/sechub-integrationtest/src/main/resources/pds-config-integrationtest.json b/sechub-integrationtest/src/main/resources/pds-config-integrationtest.json index 9b4651eaf0..79b6966008 100644 --- a/sechub-integrationtest/src/main/resources/pds-config-integrationtest.json +++ b/sechub-integrationtest/src/main/resources/pds-config-integrationtest.json @@ -5,7 +5,7 @@ "id" : "PDS_INTTEST_PRODUCT_CODESCAN", "path" : "./../sechub-integrationtest/pds/product-scripts/integrationtest-codescan.sh", "scanType" : "codeScan", - "envWhitelist" : [ "INTEGRATIONTEST_SCRIPT_ENV_ACCEPTED"], + "envWhitelist" : [ "INTEGRATIONTEST_SCRIPT_ENV_ACCEPTED" ], "description" : "This is only a fake code scan - used by integration tests. The code scan will just return data from uploaded zip file", "parameters" : { "mandatory" : [ { @@ -71,6 +71,9 @@ "optional" : [ { "key" : "pds.test.key.variantname", "description" : "a parameter from configuration - will be different in each integration test config from sechub integration test server" + }, { + "key" : "pds.config.template.metadata.list", + "description" : "normally this parameter is NOT sent to script, but for testing we add this parameter, so we can check by TestAPI..." } ] } }, { @@ -209,7 +212,7 @@ "path" : "./../sechub-integrationtest/pds/product-scripts/integrationtest-prepare.sh", "scanType" : "prepare", "description" : "This is a fake prepare scan - used by integration tests.", - "envWhitelist" : [ "PDS_STORAGE_*"], + "envWhitelist" : [ "PDS_STORAGE_*" ], "parameters" : { "optional" : [ { "key" : "pds.test.key.variantname", diff --git a/sechub-integrationtest/src/test/java/com/mercedesbenz/sechub/integrationtest/scenario1/AssetScenario1IntTest.java b/sechub-integrationtest/src/test/java/com/mercedesbenz/sechub/integrationtest/scenario1/AssetScenario1IntTest.java new file mode 100644 index 0000000000..f7417b9579 --- /dev/null +++ b/sechub-integrationtest/src/test/java/com/mercedesbenz/sechub/integrationtest/scenario1/AssetScenario1IntTest.java @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.integrationtest.scenario1; + +import static com.mercedesbenz.sechub.integrationtest.api.TestAPI.*; +import static com.mercedesbenz.sechub.integrationtest.api.TestAPI.as; +import static org.assertj.core.api.Assertions.*; + +import java.io.File; +import java.util.List; +import java.util.UUID; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.Timeout; +import org.springframework.web.client.HttpClientErrorException.NotFound; + +import com.mercedesbenz.sechub.domain.scan.asset.AssetDetailData; +import com.mercedesbenz.sechub.domain.scan.asset.AssetFileData; +import com.mercedesbenz.sechub.integrationtest.api.IntegrationTestSetup; +import com.mercedesbenz.sechub.integrationtest.api.TestAPI; +import com.mercedesbenz.sechub.test.TestFileReader; + +public class AssetScenario1IntTest { + + @Rule + public IntegrationTestSetup setup = IntegrationTestSetup.forScenario(Scenario1.class); + + @Rule + public Timeout timeOut = Timeout.seconds(600); + + @Before + public void before() { + + } + + @Test + public void asset_crud_operation_working_as_expected() { + /* ------- */ + /* prepare */ + /* ------- */ + File uploadedFile1 = new File("./src/test/resources/asset/examples-1/asset1.txt"); + File uploadedFile2 = new File("./src/test/resources/asset/examples-1/asset2.txt"); + + String assetId = "crud" + UUID.randomUUID().toString(); + + /* --------------- */ + /* execute + test */ + /* --------------- */ + as(SUPER_ADMIN).uploadAssetFiles(assetId, uploadedFile1, uploadedFile2); + + // fetch all asset ids + List allAssetIds = as(SUPER_ADMIN).fetchAllAssetIds(); + assertThat(allAssetIds).contains(assetId); + + /* download files */ + File downloadedAssetFile1 = as(SUPER_ADMIN).downloadAssetFile(assetId, uploadedFile1.getName()); + + String output = TestFileReader.readTextFromFile(downloadedAssetFile1); + assertThat(output).isEqualTo("I am text file \"asset1.txt\""); + + /* fetch asset details and check content is as expected */ + AssetDetailData detailData = as(SUPER_ADMIN).fetchAssetDetails(assetId); + assertThat(detailData.getAssetId()).isEqualTo(assetId); + + String checksum1 = TestAPI.createSHA256Of(uploadedFile1); + AssetFileData expectedInfo1 = new AssetFileData(); + expectedInfo1.setChecksum(checksum1); + expectedInfo1.setFileName("asset1.txt"); + + String checksum2 = TestAPI.createSHA256Of(uploadedFile2); + AssetFileData expectedInfo2 = new AssetFileData(); + expectedInfo2.setChecksum(checksum2); + expectedInfo2.setFileName("asset2.txt"); + + assertThat(detailData.getFiles()).contains(expectedInfo1, expectedInfo2).hasSize(2); + + /* delete single file from asset */ + as(SUPER_ADMIN).deleteAssetFile(assetId, "asset1.txt"); + + /* check asset still exists in list and details contain only asset2.txt */ + assertThat(as(SUPER_ADMIN).fetchAllAssetIds()).contains(assetId); + assertThat(as(SUPER_ADMIN).fetchAssetDetails(assetId).getFiles()).containsOnly(expectedInfo2); + + /* + * Upload asset 2 again, but with different content - we use other file from + * examples-2 instead of examples-1. Will override existing asset file. + */ + File uploadedFile2changed = new File("./src/test/resources/asset/examples-2/asset2.txt"); + String checksum2changed = TestAPI.createSHA256Of(uploadedFile2changed); + assertThat(checksum2changed).as("precondition-check that files are different").isNotEqualTo(checksum2); + + as(SUPER_ADMIN).uploadAssetFile(assetId, uploadedFile2changed); + + AssetFileData expectedInfo2Canged = new AssetFileData(); + expectedInfo2Canged.setChecksum(checksum2changed); + expectedInfo2Canged.setFileName("asset2.txt"); + + assertThat(as(SUPER_ADMIN).fetchAssetDetails(assetId).getFiles()).containsOnly(expectedInfo2Canged); + + output = TestFileReader.readTextFromFile(as(SUPER_ADMIN).downloadAssetFile(assetId, "asset2.txt")); + assertThat(output).isEqualTo("I am text file \"asset2.txt\" - but from folder example-2"); + + /* delete complete asset */ + as(SUPER_ADMIN).deleteAsset(assetId); + + assertThat(as(SUPER_ADMIN).fetchAllAssetIds()).doesNotContain(assetId); + assertThatThrownBy(() -> as(SUPER_ADMIN).fetchAssetDetails(assetId)).isInstanceOf(NotFound.class); + } + +} diff --git a/sechub-integrationtest/src/test/java/com/mercedesbenz/sechub/integrationtest/scenario1/TemplateScenario1IntTest.java b/sechub-integrationtest/src/test/java/com/mercedesbenz/sechub/integrationtest/scenario1/TemplateScenario1IntTest.java new file mode 100644 index 0000000000..cc97b09d0c --- /dev/null +++ b/sechub-integrationtest/src/test/java/com/mercedesbenz/sechub/integrationtest/scenario1/TemplateScenario1IntTest.java @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.integrationtest.scenario1; + +import static com.mercedesbenz.sechub.integrationtest.api.TestAPI.*; +import static com.mercedesbenz.sechub.integrationtest.api.TestAPI.as; +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.Timeout; + +import com.mercedesbenz.sechub.commons.model.template.TemplateDefinition; +import com.mercedesbenz.sechub.commons.model.template.TemplateDefinition.TemplateVariable; +import com.mercedesbenz.sechub.commons.model.template.TemplateType; +import com.mercedesbenz.sechub.domain.scan.project.ScanProjectConfig; +import com.mercedesbenz.sechub.domain.scan.project.ScanProjectConfigID; +import com.mercedesbenz.sechub.integrationtest.api.IntegrationTestSetup; + +public class TemplateScenario1IntTest { + + @Rule + public IntegrationTestSetup setup = IntegrationTestSetup.forScenario(Scenario1.class); + + @Rule + public Timeout timeOut = Timeout.seconds(600); + + private String templateId; + + private TemplateDefinition createDefinition; + + private TemplateDefinition definitionWithId; + private TemplateDefinition updateDefinition; + + @Before + public void before() { + + templateId = "template-1_" + System.nanoTime(); + + /* @formatter:off */ + TemplateDefinition fullTemplateDefinition = TemplateDefinition.builder(). + templateId(templateId). + templateType(TemplateType.WEBSCAN_LOGIN). + assetId("asset1"). + build(); + /* @formatter:on */ + TemplateVariable usernameVariable = new TemplateVariable(); + usernameVariable.setName("username"); + + TemplateVariable passwordVariable = new TemplateVariable(); + passwordVariable.setName("password"); + + fullTemplateDefinition.getVariables().add(usernameVariable); + fullTemplateDefinition.getVariables().add(passwordVariable); + + String fullTemplateDefinitionJson = fullTemplateDefinition.toFormattedJSON(); + createDefinition = TemplateDefinition.from(fullTemplateDefinitionJson.replace(templateId, "does-not-matter-will-be-overriden")); + + definitionWithId = TemplateDefinition.from(fullTemplateDefinitionJson); + + updateDefinition = TemplateDefinition.from(fullTemplateDefinitionJson.replace(templateId, "will-not-be-changed-by-update")); + } + + @Test + public void template_crud_test() { + /* prepare */ + as(SUPER_ADMIN).createProject(Scenario1.PROJECT_1, SUPER_ADMIN); // not done in this scenario automatically + + /* check preconditions */ + assertTemplateNotInsideTemplateList(); + + /* execute + test */ + assertTemplateCanBeCreated(); + + assertTemplateCanBeUpdated(); + + assertTemplateCanBeAssignedToProject(); + + assertTemplateCanBeUnassignedFromProject(); + + assertTemplateCanBeAssignedToProject(); + + assertTemplateCanBeDeletedAndAssignmentIsPurged(); + + assertTemplateCanBeRecreatedWithSameId(); + + assertTemplateCanBeAssignedToProject(); + + assertProjectDeleteDoesPurgeTemplateAssignment(); + + assertTemplateExistsInTemplateListAndCanBeFetched(); + + /* + * cleanup - we remove the re-created template finally to have no garbage after + * test + */ + as(SUPER_ADMIN).deleteTemplate(templateId); + + // check cleanup worked + assertTemplateNotInsideTemplateList(); + + } + + private void assertTemplateNotInsideTemplateList() { + List templateIds = as(SUPER_ADMIN).fetchTemplateList(); + executeResilient(() -> assertThat(templateIds).doesNotContain(templateId)); + } + + private void assertTemplateExistsInTemplateListAndCanBeFetched() { + // check template list still contains the test template */ + executeResilient(() -> assertThat(as(SUPER_ADMIN).fetchTemplateList()).contains(templateId)); + executeResilient(() -> assertThat(as(SUPER_ADMIN).fetchTemplateDefinitionOrNull(templateId)).isNotNull()); + } + + private void assertProjectDeleteDoesPurgeTemplateAssignment() { + /* execute 7 - delete project */ + as(SUPER_ADMIN).deleteProject(Scenario1.PROJECT_1); + + /* test 7 - configuration for project is removed */ + executeResilient(() -> assertThat(fetchScanProjectConfigurations(Scenario1.PROJECT_1)).isEmpty()); + + } + + private void assertTemplateCanBeRecreatedWithSameId() { + /* execute 6 - create template with same id again */ + as(SUPER_ADMIN).createOrUpdateTemplate(templateId, createDefinition); + + /* test 6 - template is recreated */ + executeResilient(() -> assertThat(as(SUPER_ADMIN).fetchTemplateDefinitionOrNull(templateId)).isNotNull()); + } + + private void assertTemplateCanBeDeletedAndAssignmentIsPurged() { + /* execute 5 - delete template */ + as(SUPER_ADMIN).deleteTemplate(templateId); + + /* test 5.1 check delete unassigns template */ + executeResilient(() -> assertThat(as(SUPER_ADMIN).fetchProjectDetailInformation(Scenario1.PROJECT_1).getTemplateIds()).contains(templateId)); + + /* test 5.2 check template no longer exists */ + executeResilient(() -> assertThat(as(SUPER_ADMIN).fetchTemplateDefinitionOrNull(templateId)).isNull()); + } + + private void assertTemplateCanBeUnassignedFromProject() { + /* execute 4 - unassign */ + as(SUPER_ADMIN).unassignTemplateFromProject(templateId, Scenario1.PROJECT_1); + + /* test 4 - check assignment */ + executeResilient(() -> assertThat(as(SUPER_ADMIN).fetchProjectDetailInformation(Scenario1.PROJECT_1).getTemplateIds()).isEmpty()); + executeResilient(() -> assertThat(fetchScanProjectConfigurations(Scenario1.PROJECT_1)).isEmpty()); + } + + private void assertTemplateCanBeAssignedToProject() { + + /* execute 3- assign */ + as(SUPER_ADMIN).assignTemplateToProject(templateId, Scenario1.PROJECT_1); + + /* test 3.1 - check assignment by project details in domain administration */ + executeResilient(() -> assertThat(as(SUPER_ADMIN).fetchProjectDetailInformation(Scenario1.PROJECT_1).getTemplateIds()).contains(templateId)); + + /* test 3.2 - check project scan configuration in domain scan */ + executeResilient(() -> { + List configurations = fetchScanProjectConfigurations(Scenario1.PROJECT_1); + assertThat(configurations).isNotEmpty().hasSize(1) + .contains(new ScanProjectConfig(ScanProjectConfigID.TEMPLATE_WEBSCAN_LOGIN, Scenario1.PROJECT_1.getProjectId())); + assertThat(configurations.iterator().next().getData()).isEqualTo(templateId); + }); + } + + private void assertTemplateCanBeUpdated() { + /* prepare 2 - update */ + updateDefinition.setAssetId("asset2"); + + /* execute 2 - update */ + as(SUPER_ADMIN).createOrUpdateTemplate(templateId, updateDefinition); + + /* test 2 - update works */ + executeResilient(() -> { + TemplateDefinition loadedTemplate = as(SUPER_ADMIN).fetchTemplateDefinitionOrNull(templateId); + assertThat(loadedTemplate.getAssetId()).isEqualTo("asset2"); + assertThat(loadedTemplate.getType()).isEqualTo(TemplateType.WEBSCAN_LOGIN); + assertThat(loadedTemplate.getId()).isEqualTo(templateId); + }); + } + + private void assertTemplateCanBeCreated() { + /* execute 1 - create */ + as(SUPER_ADMIN).createOrUpdateTemplate(templateId, createDefinition); + + /* test 1 - created definition has content as expected and contains id */ + executeResilient( + () -> assertThat(as(SUPER_ADMIN).fetchTemplateDefinitionOrNull(templateId).toFormattedJSON()).isEqualTo(definitionWithId.toFormattedJSON())); + } + +} diff --git a/sechub-integrationtest/src/test/java/com/mercedesbenz/sechub/integrationtest/scenario12/PDSWebScanJobScenario12IntTest.java b/sechub-integrationtest/src/test/java/com/mercedesbenz/sechub/integrationtest/scenario12/PDSWebScanJobScenario12IntTest.java index 67a8317c98..f813483f04 100644 --- a/sechub-integrationtest/src/test/java/com/mercedesbenz/sechub/integrationtest/scenario12/PDSWebScanJobScenario12IntTest.java +++ b/sechub-integrationtest/src/test/java/com/mercedesbenz/sechub/integrationtest/scenario12/PDSWebScanJobScenario12IntTest.java @@ -2,12 +2,16 @@ package com.mercedesbenz.sechub.integrationtest.scenario12; import static com.mercedesbenz.sechub.integrationtest.api.TestAPI.*; +import static com.mercedesbenz.sechub.integrationtest.api.TestAPI.as; import static com.mercedesbenz.sechub.integrationtest.scenario12.Scenario12.*; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.*; +import java.io.File; import java.util.Arrays; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.UUID; @@ -15,7 +19,18 @@ import org.junit.Test; import org.junit.rules.Timeout; -import com.mercedesbenz.sechub.commons.model.*; +import com.mercedesbenz.sechub.commons.model.ClientCertificateConfiguration; +import com.mercedesbenz.sechub.commons.model.HTTPHeaderConfiguration; +import com.mercedesbenz.sechub.commons.model.JSONConverter; +import com.mercedesbenz.sechub.commons.model.SecHubMessageType; +import com.mercedesbenz.sechub.commons.model.SecHubScanConfiguration; +import com.mercedesbenz.sechub.commons.model.SecHubWebScanApiConfiguration; +import com.mercedesbenz.sechub.commons.model.SecHubWebScanApiType; +import com.mercedesbenz.sechub.commons.model.SecHubWebScanConfiguration; +import com.mercedesbenz.sechub.commons.model.Severity; +import com.mercedesbenz.sechub.commons.model.template.TemplateDefinition; +import com.mercedesbenz.sechub.commons.model.template.TemplateDefinition.TemplateVariable; +import com.mercedesbenz.sechub.commons.model.template.TemplateType; import com.mercedesbenz.sechub.integrationtest.api.IntegrationTestSetup; import com.mercedesbenz.sechub.integrationtest.api.TestProject; import com.mercedesbenz.sechub.integrationtest.internal.IntegrationTestFileSupport; @@ -35,18 +50,68 @@ public class PDSWebScanJobScenario12IntTest { @Rule public Timeout timeOut = Timeout.seconds(600); + /* @formatter:off + * + * This is test is a web scan integration test which + * tests multiple features. + * + * The test prepares + * - web scan in general with dedicated setup + * - uses a SecHub configuration with template data inside + * - creates an asset, creates a template which uses the asset, assigns template + * + * The tests checks following: + * + * - PDS web scan has expected info finding, with + * - given target URL + * - product level information + * - SecHub web configuration parts + * + * - PDS parameter for template meta data configuration is correct and transmitted to PDS + * The parameter "pds.config.template.metadata.list" is normally not available inside + * the scripts, but for testing we added the parameter inside server configuration so it + * will be added to script level and can be checked by TestAPI + * + * - PDS will download and extract the uploaded asset file automatically and the + * extracted content is available inside the test bash script (executed by PDS) + * + * + * @formatter:on + */ @Test - public void pds_web_scan_has_expected_info_finding_with_given_target_url_and_product2_level_information_and_sechub_web_config_parts() { + public void pds_web_scan_can_be_executed_and_works() throws Exception { /* @formatter:off */ /* prepare */ + String assetId="asset-s12-pds-inttest-webscan"; + File productZipFile = IntegrationTestFileSupport.getTestfileSupport().createFileFromResourcePath("/asset/scenario12/PDS_INTTEST_PRODUCT_WEBSCAN.zip"); + String configurationAsJson = IntegrationTestFileSupport.getTestfileSupport().loadTestFile("sechub-integrationtest-webscanconfig-all-options.json"); SecHubScanConfiguration configuration = SecHubScanConfiguration.createFromJSON(configurationAsJson); configuration.setProjectId("myTestProject"); TestProject project = PROJECT_1; String targetURL = configuration.getWebScan().get().getUrl().toString(); - as(SUPER_ADMIN).updateWhiteListForProject(project, Arrays.asList(targetURL)); + + TemplateVariable userNameVariable = new TemplateVariable(); + userNameVariable.setName("username"); + + TemplateVariable passwordVariable = new TemplateVariable(); + passwordVariable.setName("password"); + + TemplateDefinition templateDefinition = new TemplateDefinition(); + templateDefinition.setAssetId(assetId); + templateDefinition.setType(TemplateType.WEBSCAN_LOGIN); + templateDefinition.getVariables().add(userNameVariable); + templateDefinition.getVariables().add(passwordVariable); + + String templateId = "template-scenario12-1"; + as(SUPER_ADMIN). + updateWhiteListForProject(project, Arrays.asList(targetURL)). + uploadAssetFile(assetId, productZipFile). + createOrUpdateTemplate(templateId, templateDefinition). + assignTemplateToProject(templateId, project) + ; /* execute */ UUID jobUUID = as(USER_1).withSecHubClient().startAsynchronScanFor(project, configuration).getJobUUID(); @@ -101,13 +166,13 @@ public void pds_web_scan_has_expected_info_finding_with_given_target_url_and_pro assertTrue(includes.contains("/customer/<*>")); assertTrue(excludes.contains("<*>/admin/<*>")); - // config must contain the expected headers + // web configuration must contain the expected headers assertExpectedHeaders(webConfiguration); - // config must contain the expected client certificate + // web configuration must contain the expected client certificate assertExpectedClientCertificate(webConfiguration); - // config must contain the expected openApi definition + // web configuration must contain the expected openApi definition assertExpectedOpenApiDefinition(webConfiguration); /* additional testing : messages*/ @@ -119,6 +184,14 @@ public void pds_web_scan_has_expected_info_finding_with_given_target_url_and_pro hasMessage(SecHubMessageType.ERROR,"error from webscan by PDS for sechub job uuid: "+jobUUID). hasMessage(SecHubMessageType.INFO, "another-token.txtbearer-token.txtcertificate.p12openapi.json"); + UUID pdsJobUUID = waitForFirstPDSJobOfSecHubJobAndReturnPDSJobUUID(jobUUID); + Map variables = fetchPDSVariableTestOutputMap(pdsJobUUID); + + String expectedMetaDataListJson = """ + [{"templateId":"template-scenario12-1","templateType":"WEBSCAN_LOGIN","assetData":{"assetId":"asset-s12-pds-inttest-webscan","fileName":"PDS_INTTEST_PRODUCT_WEBSCAN.zip","checksum":"ff06430bfc2d8c698ab8effa41b914525b8cca1c1eecefa76d248b25cc598fba"}}] + """.trim(); + assertThat(variables.get("PDS_CONFIG_TEMPLATE_METADATA_LIST")).isEqualTo(expectedMetaDataListJson); + assertThat(variables.get("TEST_CONTENT_FROM_ASSETFILE")).isEqualTo("i am \"testfile1.txt\" for scenario12 integration tests"); /* @formatter:on */ } diff --git a/sechub-integrationtest/src/test/resources/asset/examples-1/asset1.txt b/sechub-integrationtest/src/test/resources/asset/examples-1/asset1.txt new file mode 100644 index 0000000000..c9f4187cc1 --- /dev/null +++ b/sechub-integrationtest/src/test/resources/asset/examples-1/asset1.txt @@ -0,0 +1 @@ +I am text file "asset1.txt" \ No newline at end of file diff --git a/sechub-integrationtest/src/test/resources/asset/examples-1/asset2.txt b/sechub-integrationtest/src/test/resources/asset/examples-1/asset2.txt new file mode 100644 index 0000000000..a47cb9bda8 --- /dev/null +++ b/sechub-integrationtest/src/test/resources/asset/examples-1/asset2.txt @@ -0,0 +1 @@ +I am text file "asset2.txt" \ No newline at end of file diff --git a/sechub-integrationtest/src/test/resources/asset/examples-2/asset2.txt b/sechub-integrationtest/src/test/resources/asset/examples-2/asset2.txt new file mode 100644 index 0000000000..7ccc0dabcc --- /dev/null +++ b/sechub-integrationtest/src/test/resources/asset/examples-2/asset2.txt @@ -0,0 +1 @@ +I am text file "asset2.txt" - but from folder example-2 \ No newline at end of file diff --git a/sechub-integrationtest/src/test/resources/asset/scenario12/PDS_INTTEST_PRODUCT_WEBSCAN.zip b/sechub-integrationtest/src/test/resources/asset/scenario12/PDS_INTTEST_PRODUCT_WEBSCAN.zip new file mode 100644 index 0000000000..1c1ce661bf Binary files /dev/null and b/sechub-integrationtest/src/test/resources/asset/scenario12/PDS_INTTEST_PRODUCT_WEBSCAN.zip differ diff --git a/sechub-integrationtest/src/test/resources/sechub-integrationtest-webscanconfig-all-options.json b/sechub-integrationtest/src/test/resources/sechub-integrationtest-webscanconfig-all-options.json index 6951487d61..894924e54f 100644 --- a/sechub-integrationtest/src/test/resources/sechub-integrationtest-webscanconfig-all-options.json +++ b/sechub-integrationtest/src/test/resources/sechub-integrationtest-webscanconfig-all-options.json @@ -25,8 +25,8 @@ }, "webScan" : { "url" : "https://demo.example.org/myapp", - "includes": [ "/portal/admin", "/abc.html", "/hidden", "/customer/<*>" ], - "excludes": [ "/public/media", "/contact.html", "/static", "<*>/admin/<*>" ], + "includes" : [ "/portal/admin", "/abc.html", "/hidden", "/customer/<*>" ], + "excludes" : [ "/public/media", "/contact.html", "/static", "<*>/admin/<*>" ], "maxScanDuration" : { "duration" : 35, "unit" : "minutes" @@ -39,7 +39,7 @@ "password" : "secret-password", "use" : [ "client-cert-api-file-reference" ] }, - "headers" : [{ + "headers" : [ { "name" : "Authorization", "use" : [ "header-file-ref-for-big-token" ] }, { @@ -50,9 +50,13 @@ }, { "name" : "Key", "use" : [ "another-header-file-ref-for-big-token" ] - }], + } ], "login" : { "url" : "https://demo.example.org/myapp/login", + "templateData" : { + "username" : "testuser", + "password" : "testpwd" + }, "basic" : { "realm" : "realm0", "user" : "user0", diff --git a/sechub-openapi-java/src/main/resources/openapi.yaml b/sechub-openapi-java/src/main/resources/openapi.yaml index 0f1ca5927d..89428aa834 100644 --- a/sechub-openapi-java/src/main/resources/openapi.yaml +++ b/sechub-openapi-java/src/main/resources/openapi.yaml @@ -150,6 +150,11 @@ components: items: type: string type: array + templates: + description: A list of all assigned templates + items: + type: string + type: array example: owner: owner metaData: @@ -163,6 +168,9 @@ components: users: - user1 - user2 + templates: + - template1 + - template2 ProjectWhitelist: title: ProjectWhitelist @@ -673,6 +681,8 @@ components: format: int32 hashAlgorithm: $ref: '#/components/schemas/TOTPHashAlgorithm' + encodingType: + $ref: '#/components/schemas/EncodingType' required: - seed @@ -684,6 +694,16 @@ components: - HmacSHA512 description: Representing the TOTP hash algorithms. default: HmacSHA1 + + EncodingType: + enum: + - AUTODETECT + - HEX + - BASE32 + - BASE64 + - PLAIN + description: Representing the encoding of the TOTP seed. + default: AUTODETECT WebLoginConfiguration: title: WebLoginConfiguration @@ -2005,16 +2025,81 @@ components: $ref: '#/components/schemas/SecHubJobInfoForUser' projectId: type: string - - + + TemplateType: + title: TemplateType + type: string + enum: + - WEBSCAN_LOGIN + + TemplateVariableValidation: + title: TemplateVariableValidation + type: object + properties: + minLength: + type: integer + format: int32 + maxLength: + type: integer + format: int32 + regularExpression: + type: string + + TemplateVariable: + title: TemplateVariable + type: object + properties: + name: + type: string + optional: + type: boolean + validation: + $ref: '#/components/schemas/TemplateVariableValidation' + + TemplateDefinition: + title: TemplateDefinition + type: object + properties: + type: + $ref: '#/components/schemas/TemplateType' + assets: + type: array + items: + type: string + variables: + type: array + items: + $ref: '#/components/schemas/TemplateVariable' + + AssetDetailData: + title: AssetDetailData + type: object + properties: + assetId: + type: string + files: + type: array + items: + type: object + $ref: '#/components/schemas/AssetFileData' + + AssetFileData: + title: AssetFileData + type: object + properties: + fileName: + type: string + checksum: + type: string + security: - basicAuth: [ ] paths: - ############# + ############ ## System ## - ############# + ############ /api/anonymous/check/alive: get: @@ -2535,7 +2620,60 @@ paths: description: "Not acceptable" tags: - Project Administration - + /api/admin/project/{projectId}/template/{templateId}: + put: + summary: Admin assigns template to project + description: An administrator assigns a template to a project + operationId: adminAssignTemplateToProject + parameters: + - name: projectId + description: The id for project + in: path + required: true + schema: + type: string + - name: templateId + description: The id of the template to assign to project + in: path + required: true + schema: + type: string + responses: + "200": + description: "Ok" + "404": + description: "Not found" + "406": + description: "Not acceptable" + tags: + - Project Administration + delete: + summary: Admin unassigns template from project + description: An administrator unassigns a template from a project + operationId: adminUnassignTemplateFromProject + parameters: + - name: projectId + description: The id for project + in: path + required: true + schema: + type: string + - name: templateId + description: The id of the template to assign to project + in: path + required: true + schema: + type: string + responses: + "200": + description: "Ok" + "404": + description: "Not found" + "406": + description: "Not acceptable" + tags: + - Project Administration + ################### ## User Profile ## ################### @@ -3777,4 +3915,256 @@ paths: description: "Not acceptable" x-content-type: application/json tags: - - Other \ No newline at end of file + - Other + + /api/admin/template/{templateId}: + put: + summary: Admin creates or updates a template + description: An administrator wants to create a new template or to update a template definition + operationId: adminCreateOrUpdateTemplate + parameters: + - name: templateId + description: The template id + in: path + required: true + schema: + type: string + responses: + "200": + description: "Ok" + "404": + description: "Not found" + "406": + description: "Not acceptable" + tags: + - Configuration + + delete: + summary: Admin deletes a template + description: An administrator wants to delete an existing template + operationId: adminDeleteTemplate + parameters: + - name: templateId + description: The template id + in: path + required: true + schema: + type: string + responses: + "200": + description: "Ok" + "404": + description: "Not found" + tags: + - Configuration + + get: + summary: Admin fetches template + description: An administrator wants to fetch the template definition by template id + operationId: adminFetchTemplate + parameters: + - name: templateId + description: The template id + in: path + required: true + schema: + type: string + responses: + "200": + description: "Ok" + content: + application/json: + schema: + $ref: '#/components/schemas/TemplateDefinition' + "404": + description: "Not found" + tags: + - Configuration + + /api/admin/templates: + get: + summary: Admin fetches template ids + description: An administrator wants to fetch a list containing all available template identifiers + operationId: adminFetchTemplateIds + parameters: + - name: templateId + description: The template id + in: path + required: true + schema: + type: string + responses: + "200": + description: "Ok" + content: + application/json;charset=UTF-8: + schema: + type: array + items: + type: string + "404": + description: "Not found" + tags: + - Configuration + + /api/admin/asset/ids: + get: + summary: Admin fetches asset ids + description: An administrator fetches all available asset ids. + operationId: adminFetchAssetIds + responses: + "200": + description: "Ok" + content: + application/json;charset=UTF-8: + schema: + type: array + items: + type: string + tags: + - Configuration + + /api/admin/asset/{assetId}/details: + get: + summary: Admin fetches asset details + description: "An administrator fetches details about an asset. For example: the result will contain names but also checksum of files." + operationId: adminFetchAssetDetails + parameters: + - name: assetId + description: The asset identifier + in: path + required: true + schema: + type: string + responses: + "200": + description: "Ok" + content: + application/json: + schema: + $ref: '#/components/schemas/AssetDetailData' + + "404": + description: "Not found" + tags: + - Configuration + + /api/admin/asset/{assetId}: + delete: + summary: Admin deletes asset comletely + description: An administrator deletes an asset completely. + operationId: adminDeletesAssetCompletely + parameters: + - name: assetId + description: TThe asset identifier for the asset which shall be deleted completely + in: path + required: true + schema: + type: string + responses: + "200": + description: "Ok" + + "404": + description: "Not found" + tags: + - Configuration + + /api/admin/asset/{assetId}/file: + post: + summary: Admin uploads an asset file + description: "An administrator uploads a file for an asset. If the file already exits, it will be overriden." + operationId: adminUploadsAssetFile + parameters: + - name: assetId + description: The id of the asset to which the uploaded file belongs to + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + description: The asset file to upload + checkSum: + type: string + description: A sha256 checksum for file upload validation + required: + - file + - checkSum + encoding: + file: + contentType: multipart/form-data + responses: + "200": + description: "Ok" + "406": + description: "Not acceptable" + x-content-type: multipart/form-data + tags: + - Configuration + + /api/admin/asset/{assetId}/file/{fileName}: + get: + summary: Admin downloads an asset file + description: An administrator downloads a file fom an asset. + operationId: adminDownloadsAssetFile + parameters: + - name: assetId + description: The asset identifier of the asset containing the file. + in: path + required: true + schema: + type: string + - name: fileName + description: The name of the file inside the asset which shall be downloaded. + in: path + required: true + schema: + type: string + responses: + "200": + content: + application/octet-stream: + schema: + type: string + format: binary + description: "Ok" + + "404": + description: "Not found" + tags: + - Configuration + + delete: + summary: Admin deletes an asset file + description: An administrator deletes a file fom an asset. + operationId: adminDeletesAssetFile + parameters: + - name: assetId + description: The asset identifier for the asset in which the file shall be deleted. + in: path + required: true + schema: + type: string + - name: fileName + description: The name of the file to delete inside the asset. + in: path + required: true + schema: + type: string + responses: + "200": + description: "Ok" + + "404": + description: "Not found" + tags: + - Configuration \ No newline at end of file diff --git a/sechub-pds-core/src/main/java/com/mercedesbenz/sechub/pds/job/PDSJobConfigurationSupport.java b/sechub-pds-core/src/main/java/com/mercedesbenz/sechub/pds/job/PDSJobConfigurationSupport.java index 8482876303..414831af46 100644 --- a/sechub-pds-core/src/main/java/com/mercedesbenz/sechub/pds/job/PDSJobConfigurationSupport.java +++ b/sechub-pds-core/src/main/java/com/mercedesbenz/sechub/pds/job/PDSJobConfigurationSupport.java @@ -13,12 +13,14 @@ import org.slf4j.LoggerFactory; import com.mercedesbenz.sechub.commons.core.util.SimpleStringUtils; +import com.mercedesbenz.sechub.commons.model.JSONConverter; import com.mercedesbenz.sechub.commons.model.SecHubConfigurationModel; import com.mercedesbenz.sechub.commons.model.SecHubDataConfigurationType; import com.mercedesbenz.sechub.commons.model.SecHubDataConfigurationTypeListParser; import com.mercedesbenz.sechub.commons.model.SecHubScanConfiguration; import com.mercedesbenz.sechub.commons.pds.PDSDefaultParameterKeyConstants; import com.mercedesbenz.sechub.commons.pds.PDSDefaultParameterValueConstants; +import com.mercedesbenz.sechub.commons.pds.data.PDSTemplateMetaData; import com.mercedesbenz.sechub.pds.execution.PDSExecutionParameterEntry; public class PDSJobConfigurationSupport { @@ -200,4 +202,13 @@ public int getJobStorageReadResiliencRetryWaitSeconds(int defaultValue) { return result; } + public List getTemplateMetaData() { + + String json = getStringParameterOrNull(PARAM_KEY_PDS_CONFIG_TEMPLATE_META_DATA_LIST); + if (json == null || json.isBlank()) { + return Collections.emptyList(); + } + return JSONConverter.get().fromJSONtoListOf(PDSTemplateMetaData.class, json); + } + } diff --git a/sechub-pds-core/src/main/java/com/mercedesbenz/sechub/pds/usecase/PDSUseCaseIdentifier.java b/sechub-pds-core/src/main/java/com/mercedesbenz/sechub/pds/usecase/PDSUseCaseIdentifier.java index 70de74439b..f59264d4dc 100644 --- a/sechub-pds-core/src/main/java/com/mercedesbenz/sechub/pds/usecase/PDSUseCaseIdentifier.java +++ b/sechub-pds-core/src/main/java/com/mercedesbenz/sechub/pds/usecase/PDSUseCaseIdentifier.java @@ -47,6 +47,8 @@ public enum PDSUseCaseIdentifier { UC_SYSTEM_SIGTERM_HANDLING(19, false), + UC_SYSTEM_JOB_EXECUTION(20, false), + ; /* +---------------------------------------------------------------------+ */ diff --git a/sechub-pds-core/src/main/java/com/mercedesbenz/sechub/pds/usecase/UseCaseSystemExecutesJob.java b/sechub-pds-core/src/main/java/com/mercedesbenz/sechub/pds/usecase/UseCaseSystemExecutesJob.java new file mode 100644 index 0000000000..d3be4feb73 --- /dev/null +++ b/sechub-pds-core/src/main/java/com/mercedesbenz/sechub/pds/usecase/UseCaseSystemExecutesJob.java @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.pds.usecase; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/* @formatter:off */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@PDSUseCaseDefinition( + id=PDSUseCaseIdentifier.UC_SYSTEM_JOB_EXECUTION, + group=PDSUseCaseGroup.JOB_EXECUTION, + title="System executes job", + description="The PDS does execute a PDS job.") +public @interface UseCaseSystemExecutesJob { + PDSStep value(); +} diff --git a/sechub-pds-solutions/checkmarx/helm/pds-checkmarx/Chart.yaml b/sechub-pds-solutions/checkmarx/helm/pds-checkmarx/Chart.yaml index 6aad19547e..1c7ddb568a 100644 --- a/sechub-pds-solutions/checkmarx/helm/pds-checkmarx/Chart.yaml +++ b/sechub-pds-solutions/checkmarx/helm/pds-checkmarx/Chart.yaml @@ -9,4 +9,4 @@ type: application # This is the chart version. # This version number should be incremented each time you make changes to the chart and its templates. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.4.0 +version: 1.5.0 diff --git a/sechub-pds-solutions/checkmarx/helm/pds-checkmarx/templates/deployment.yaml b/sechub-pds-solutions/checkmarx/helm/pds-checkmarx/templates/deployment.yaml index 8d0270b380..b0153e29b3 100644 --- a/sechub-pds-solutions/checkmarx/helm/pds-checkmarx/templates/deployment.yaml +++ b/sechub-pds-solutions/checkmarx/helm/pds-checkmarx/templates/deployment.yaml @@ -124,6 +124,8 @@ spec: value: "{{ .Values.storage.sharedVolume.upload.dir }}" {{- end}} ports: + - name: pds-health-port + containerPort: 10251 - name: pds-https-port containerPort: 8444 startupProbe: @@ -131,9 +133,19 @@ spec: scheme: HTTPS path: /api/anonymous/check/alive port: pds-https-port + initialDelaySeconds: 5 periodSeconds: 1 - failureThreshold: 300 - # probe every 1s x 300 = 5 mins before restart of container + # probe every 1s x 600 = 10 mins before restart of container (some PDS download huge files before startup) + failureThreshold: 600 + successThreshold: 1 + timeoutSeconds: 1 + readinessProbe: + httpGet: + scheme: HTTP + path: /actuator/health/readiness + port: pds-health-port + periodSeconds: 2 + failureThreshold: 2 successThreshold: 1 timeoutSeconds: 1 livenessProbe: @@ -144,7 +156,7 @@ spec: periodSeconds: 5 failureThreshold: 3 successThreshold: 1 - timeoutSeconds: 3 + timeoutSeconds: 5 volumeMounts: - mountPath: "/workspace" name: pds-workspace diff --git a/sechub-pds-solutions/findsecuritybugs/helm/pds-findsecuritybugs/Chart.yaml b/sechub-pds-solutions/findsecuritybugs/helm/pds-findsecuritybugs/Chart.yaml index cfbc141c95..f4c3486a26 100644 --- a/sechub-pds-solutions/findsecuritybugs/helm/pds-findsecuritybugs/Chart.yaml +++ b/sechub-pds-solutions/findsecuritybugs/helm/pds-findsecuritybugs/Chart.yaml @@ -9,4 +9,4 @@ type: application # This is the chart version. # This version number should be incremented each time you make changes to the chart and its templates. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.4.0 +version: 1.5.0 diff --git a/sechub-pds-solutions/findsecuritybugs/helm/pds-findsecuritybugs/templates/deployment.yaml b/sechub-pds-solutions/findsecuritybugs/helm/pds-findsecuritybugs/templates/deployment.yaml index 951448fd1a..e8db3fe28a 100644 --- a/sechub-pds-solutions/findsecuritybugs/helm/pds-findsecuritybugs/templates/deployment.yaml +++ b/sechub-pds-solutions/findsecuritybugs/helm/pds-findsecuritybugs/templates/deployment.yaml @@ -120,6 +120,8 @@ spec: value: "{{ .Values.storage.sharedVolume.upload.dir }}" {{- end}} ports: + - name: pds-health-port + containerPort: 10251 - name: pds-https-port containerPort: 8444 startupProbe: @@ -127,9 +129,19 @@ spec: scheme: HTTPS path: /api/anonymous/check/alive port: pds-https-port + initialDelaySeconds: 5 periodSeconds: 1 - failureThreshold: 300 - # probe every 1s x 300 = 5 mins before restart of container + # probe every 1s x 600 = 10 mins before restart of container (some PDS download huge files before startup) + failureThreshold: 600 + successThreshold: 1 + timeoutSeconds: 1 + readinessProbe: + httpGet: + scheme: HTTP + path: /actuator/health/readiness + port: pds-health-port + periodSeconds: 2 + failureThreshold: 2 successThreshold: 1 timeoutSeconds: 1 livenessProbe: @@ -140,7 +152,7 @@ spec: periodSeconds: 5 failureThreshold: 3 successThreshold: 1 - timeoutSeconds: 3 + timeoutSeconds: 5 volumeMounts: - mountPath: "/workspace" name: pds-fsb-workspace diff --git a/sechub-pds-solutions/gitleaks/helm/pds-gitleaks/Chart.yaml b/sechub-pds-solutions/gitleaks/helm/pds-gitleaks/Chart.yaml index 3694c52fac..f7919a2ec6 100644 --- a/sechub-pds-solutions/gitleaks/helm/pds-gitleaks/Chart.yaml +++ b/sechub-pds-solutions/gitleaks/helm/pds-gitleaks/Chart.yaml @@ -9,4 +9,4 @@ type: application # This is the chart version. # This version number should be incremented each time you make changes to the chart and its templates. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.4.0 +version: 1.5.0 diff --git a/sechub-pds-solutions/gitleaks/helm/pds-gitleaks/templates/deployment.yaml b/sechub-pds-solutions/gitleaks/helm/pds-gitleaks/templates/deployment.yaml index 5170250dbf..79b00453a2 100644 --- a/sechub-pds-solutions/gitleaks/helm/pds-gitleaks/templates/deployment.yaml +++ b/sechub-pds-solutions/gitleaks/helm/pds-gitleaks/templates/deployment.yaml @@ -136,6 +136,8 @@ spec: value: "{{ .Values.storage.sharedVolume.upload.dir }}" {{- end}} ports: + - name: pds-health-port + containerPort: 10251 - name: pds-https-port containerPort: 8444 startupProbe: @@ -143,9 +145,19 @@ spec: scheme: HTTPS path: /api/anonymous/check/alive port: pds-https-port + initialDelaySeconds: 5 periodSeconds: 1 - failureThreshold: 300 - # probe every 1s x 300 = 5 mins before restart of container + # probe every 1s x 600 = 10 mins before restart of container (some PDS download huge files before startup) + failureThreshold: 600 + successThreshold: 1 + timeoutSeconds: 1 + readinessProbe: + httpGet: + scheme: HTTP + path: /actuator/health/readiness + port: pds-health-port + periodSeconds: 2 + failureThreshold: 2 successThreshold: 1 timeoutSeconds: 1 livenessProbe: @@ -156,7 +168,7 @@ spec: periodSeconds: 5 failureThreshold: 3 successThreshold: 1 - timeoutSeconds: 3 + timeoutSeconds: 5 {{- if .Values.image.imagePullSecrets }} imagePullSecrets: {{ .Values.image.imagePullSecrets | indent 8 | trim }} diff --git a/sechub-pds-solutions/gosec/helm/pds-gosec/Chart.yaml b/sechub-pds-solutions/gosec/helm/pds-gosec/Chart.yaml index c8a70ba4e2..bbd921f00a 100644 --- a/sechub-pds-solutions/gosec/helm/pds-gosec/Chart.yaml +++ b/sechub-pds-solutions/gosec/helm/pds-gosec/Chart.yaml @@ -9,4 +9,4 @@ type: application # This is the chart version. # This version number should be incremented each time you make changes to the chart and its templates. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.4.0 +version: 1.5.0 diff --git a/sechub-pds-solutions/gosec/helm/pds-gosec/templates/deployment.yaml b/sechub-pds-solutions/gosec/helm/pds-gosec/templates/deployment.yaml index 8d0270b380..b0153e29b3 100644 --- a/sechub-pds-solutions/gosec/helm/pds-gosec/templates/deployment.yaml +++ b/sechub-pds-solutions/gosec/helm/pds-gosec/templates/deployment.yaml @@ -124,6 +124,8 @@ spec: value: "{{ .Values.storage.sharedVolume.upload.dir }}" {{- end}} ports: + - name: pds-health-port + containerPort: 10251 - name: pds-https-port containerPort: 8444 startupProbe: @@ -131,9 +133,19 @@ spec: scheme: HTTPS path: /api/anonymous/check/alive port: pds-https-port + initialDelaySeconds: 5 periodSeconds: 1 - failureThreshold: 300 - # probe every 1s x 300 = 5 mins before restart of container + # probe every 1s x 600 = 10 mins before restart of container (some PDS download huge files before startup) + failureThreshold: 600 + successThreshold: 1 + timeoutSeconds: 1 + readinessProbe: + httpGet: + scheme: HTTP + path: /actuator/health/readiness + port: pds-health-port + periodSeconds: 2 + failureThreshold: 2 successThreshold: 1 timeoutSeconds: 1 livenessProbe: @@ -144,7 +156,7 @@ spec: periodSeconds: 5 failureThreshold: 3 successThreshold: 1 - timeoutSeconds: 3 + timeoutSeconds: 5 volumeMounts: - mountPath: "/workspace" name: pds-workspace diff --git a/sechub-pds-solutions/iac/helm/pds-iac/Chart.yaml b/sechub-pds-solutions/iac/helm/pds-iac/Chart.yaml index 7be03c4e83..d443958aa1 100644 --- a/sechub-pds-solutions/iac/helm/pds-iac/Chart.yaml +++ b/sechub-pds-solutions/iac/helm/pds-iac/Chart.yaml @@ -9,4 +9,4 @@ type: application # This is the chart version. # This version number should be incremented each time you make changes to the chart and its templates. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.4.0 +version: 1.5.0 diff --git a/sechub-pds-solutions/iac/helm/pds-iac/templates/deployment.yaml b/sechub-pds-solutions/iac/helm/pds-iac/templates/deployment.yaml index 16b91bf72c..7f1d0a353f 100644 --- a/sechub-pds-solutions/iac/helm/pds-iac/templates/deployment.yaml +++ b/sechub-pds-solutions/iac/helm/pds-iac/templates/deployment.yaml @@ -124,6 +124,8 @@ spec: value: "{{ .Values.storage.sharedVolume.upload.dir }}" {{- end}} ports: + - name: pds-health-port + containerPort: 10251 - name: pds-https-port containerPort: 8444 startupProbe: @@ -131,9 +133,19 @@ spec: scheme: HTTPS path: /api/anonymous/check/alive port: pds-https-port + initialDelaySeconds: 5 periodSeconds: 1 - failureThreshold: 300 - # probe every 1s x 300 = 5 mins before restart of container + # probe every 1s x 600 = 10 mins before restart of container (some PDS download huge files before startup) + failureThreshold: 600 + successThreshold: 1 + timeoutSeconds: 1 + readinessProbe: + httpGet: + scheme: HTTP + path: /actuator/health/readiness + port: pds-health-port + periodSeconds: 2 + failureThreshold: 2 successThreshold: 1 timeoutSeconds: 1 livenessProbe: @@ -144,4 +156,4 @@ spec: periodSeconds: 5 failureThreshold: 3 successThreshold: 1 - timeoutSeconds: 3 + timeoutSeconds: 5 diff --git a/sechub-pds-solutions/loc/helm/pds-loc/Chart.yaml b/sechub-pds-solutions/loc/helm/pds-loc/Chart.yaml index ed91b6e04f..ddf86c9c61 100644 --- a/sechub-pds-solutions/loc/helm/pds-loc/Chart.yaml +++ b/sechub-pds-solutions/loc/helm/pds-loc/Chart.yaml @@ -9,4 +9,4 @@ type: application # This is the chart version. # This version number should be incremented each time you make changes to the chart and its templates. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.3.0 +version: 1.4.0 diff --git a/sechub-pds-solutions/loc/helm/pds-loc/templates/deployment.yaml b/sechub-pds-solutions/loc/helm/pds-loc/templates/deployment.yaml index 5c3396071d..9d29d500e5 100644 --- a/sechub-pds-solutions/loc/helm/pds-loc/templates/deployment.yaml +++ b/sechub-pds-solutions/loc/helm/pds-loc/templates/deployment.yaml @@ -124,6 +124,8 @@ spec: value: "{{ .Values.storage.sharedVolume.upload.dir }}" {{- end}} ports: + - name: pds-health-port + containerPort: 10251 - name: pds-https-port containerPort: 8444 startupProbe: @@ -131,9 +133,19 @@ spec: scheme: HTTPS path: /api/anonymous/check/alive port: pds-https-port + initialDelaySeconds: 5 periodSeconds: 1 - failureThreshold: 300 - # probe every 1s x 300 = 5 mins before restart of container + # probe every 1s x 600 = 10 mins before restart of container (some PDS download huge files before startup) + failureThreshold: 600 + successThreshold: 1 + timeoutSeconds: 1 + readinessProbe: + httpGet: + scheme: HTTP + path: /actuator/health/readiness + port: pds-health-port + periodSeconds: 2 + failureThreshold: 2 successThreshold: 1 timeoutSeconds: 1 livenessProbe: @@ -144,7 +156,7 @@ spec: periodSeconds: 5 failureThreshold: 3 successThreshold: 1 - timeoutSeconds: 3 + timeoutSeconds: 5 volumeMounts: - mountPath: "/workspace" name: pds-workspace diff --git a/sechub-pds-solutions/multi/helm/pds-multi/Chart.yaml b/sechub-pds-solutions/multi/helm/pds-multi/Chart.yaml index fdff571b9f..0777ce8585 100644 --- a/sechub-pds-solutions/multi/helm/pds-multi/Chart.yaml +++ b/sechub-pds-solutions/multi/helm/pds-multi/Chart.yaml @@ -9,4 +9,4 @@ type: application # This is the chart version. # This version number should be incremented each time you make changes to the chart and its templates. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.4.0 +version: 1.5.0 diff --git a/sechub-pds-solutions/multi/helm/pds-multi/templates/deployment.yaml b/sechub-pds-solutions/multi/helm/pds-multi/templates/deployment.yaml index 74d77f580b..587895f63d 100644 --- a/sechub-pds-solutions/multi/helm/pds-multi/templates/deployment.yaml +++ b/sechub-pds-solutions/multi/helm/pds-multi/templates/deployment.yaml @@ -124,6 +124,8 @@ spec: value: "{{ .Values.storage.sharedVolume.upload.dir }}" {{- end}} ports: + - name: pds-health-port + containerPort: 10251 - name: pds-https-port containerPort: 8444 startupProbe: @@ -131,9 +133,19 @@ spec: scheme: HTTPS path: /api/anonymous/check/alive port: pds-https-port + initialDelaySeconds: 5 periodSeconds: 1 - failureThreshold: 300 - # probe every 1s x 300 = 5 mins before restart of container + # probe every 1s x 600 = 10 mins before restart of container (some PDS download huge files before startup) + failureThreshold: 600 + successThreshold: 1 + timeoutSeconds: 1 + readinessProbe: + httpGet: + scheme: HTTP + path: /actuator/health/readiness + port: pds-health-port + periodSeconds: 2 + failureThreshold: 2 successThreshold: 1 timeoutSeconds: 1 livenessProbe: @@ -144,7 +156,7 @@ spec: periodSeconds: 5 failureThreshold: 3 successThreshold: 1 - timeoutSeconds: 3 + timeoutSeconds: 5 volumeMounts: - mountPath: "/workspace" name: pds-workspace diff --git a/sechub-pds-solutions/owaspzap/docker/Owasp-Zap-Debian.dockerfile b/sechub-pds-solutions/owaspzap/docker/Owasp-Zap-Debian.dockerfile index 3f360047fe..dc60a49cf6 100644 --- a/sechub-pds-solutions/owaspzap/docker/Owasp-Zap-Debian.dockerfile +++ b/sechub-pds-solutions/owaspzap/docker/Owasp-Zap-Debian.dockerfile @@ -62,9 +62,6 @@ RUN cd "$TOOL_FOLDER" && \ sha256sum --check sechub-pds-wrapperowaspzap-$OWASPZAP_WRAPPER_VERSION.jar.sha256sum && \ ln -s sechub-pds-wrapperowaspzap-$OWASPZAP_WRAPPER_VERSION.jar wrapperowaspzap.jar -# Copy default full ruleset file -COPY owasp-zap-full-ruleset-all-release-status.json ${TOOL_FOLDER}/owasp-zap-full-ruleset-all-release-status.json - # Copy zap addon download urls into container COPY zap-addons.txt "$TOOL_FOLDER/zap-addons.txt" diff --git a/sechub-pds-solutions/owaspzap/docker/owasp-zap-full-ruleset-all-release-status.json b/sechub-pds-solutions/owaspzap/docker/owasp-zap-full-ruleset-all-release-status.json deleted file mode 100644 index d08f86716d..0000000000 --- a/sechub-pds-solutions/owaspzap/docker/owasp-zap-full-ruleset-all-release-status.json +++ /dev/null @@ -1,1152 +0,0 @@ -{ - "timestamp" : "2024-10-07 07:29:08.282732", - "origin" : "https://www.zaproxy.org/docs/alerts/", - "rules" : { - "Directory-Browsing-0" : { - "id" : "0", - "name" : "Directory Browsing", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/0" - }, - "Private-IP-Disclosure-2" : { - "id" : "2", - "name" : "Private IP Disclosure", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/2" - }, - "Session-ID-in-URL-Rewrite-3" : { - "id" : "3", - "name" : "Session ID in URL Rewrite", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/3" - }, - "Path-Traversal-6" : { - "id" : "6", - "name" : "Path Traversal", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/6" - }, - "Remote-File-Inclusion-7" : { - "id" : "7", - "name" : "Remote File Inclusion", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/7" - }, - "Source-Code-Disclosure-Git-41" : { - "id" : "41", - "name" : "Source Code Disclosure - Git", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/41" - }, - "Source-Code-Disclosure-SVN-42" : { - "id" : "42", - "name" : "Source Code Disclosure - SVN", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/42" - }, - "Source-Code-Disclosure-File-Inclusion-43" : { - "id" : "43", - "name" : "Source Code Disclosure - File Inclusion", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/43" - }, - "Vulnerable-JS-Library-10003" : { - "id" : "10003", - "name" : "Vulnerable JS Library", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10003" - }, - "Tech-Detection-Passive-Scanner-10004" : { - "id" : "10004", - "name" : "Tech Detection Passive Scanner", - "type" : "tool", - "link" : "https://www.zaproxy.org/docs/alerts/10004" - }, - "In-Page-Banner-Information-Leak-10009" : { - "id" : "10009", - "name" : "In Page Banner Information Leak", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10009" - }, - "Cookie-No-HttpOnly-Flag-10010" : { - "id" : "10010", - "name" : "Cookie No HttpOnly Flag", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10010" - }, - "Cookie-Without-Secure-Flag-10011" : { - "id" : "10011", - "name" : "Cookie Without Secure Flag", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10011" - }, - "Re-examine-Cache-control-Directives-10015" : { - "id" : "10015", - "name" : "Re-examine Cache-control Directives", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10015" - }, - "Cross-Domain-JavaScript-Source-File-Inclusion-10017" : { - "id" : "10017", - "name" : "Cross-Domain JavaScript Source File Inclusion", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10017" - }, - "Content-Type-Header-Missing-10019" : { - "id" : "10019", - "name" : "Content-Type Header Missing", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10019" - }, - "Anti-clickjacking-Header-10020" : { - "id" : "10020", - "name" : "Anti-clickjacking Header", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10020" - }, - "X-Content-Type-Options-Header-Missing-10021" : { - "id" : "10021", - "name" : "X-Content-Type-Options Header Missing", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10021" - }, - "Information-Disclosure-Debug-Error-Messages-10023" : { - "id" : "10023", - "name" : "Information Disclosure - Debug Error Messages", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10023" - }, - "Information-Disclosure-Sensitive-Information-in-URL-10024" : { - "id" : "10024", - "name" : "Information Disclosure - Sensitive Information in URL", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10024" - }, - "Information-Disclosure-Sensitive-Information-in-HTTP-Referrer-Header-10025" : { - "id" : "10025", - "name" : "Information Disclosure - Sensitive Information in HTTP Referrer Header", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10025" - }, - "HTTP-Parameter-Override-10026" : { - "id" : "10026", - "name" : "HTTP Parameter Override", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10026" - }, - "Information-Disclosure-Suspicious-Comments-10027" : { - "id" : "10027", - "name" : "Information Disclosure - Suspicious Comments", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10027" - }, - "Open-Redirect-10028" : { - "id" : "10028", - "name" : "Open Redirect", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10028" - }, - "Cookie-Poisoning-10029" : { - "id" : "10029", - "name" : "Cookie Poisoning", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10029" - }, - "User-Controllable-Charset-10030" : { - "id" : "10030", - "name" : "User Controllable Charset", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10030" - }, - "User-Controllable-HTML-Element-Attribute-(Potential-XSS)-10031" : { - "id" : "10031", - "name" : "User Controllable HTML Element Attribute (Potential XSS)", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10031" - }, - "Viewstate-10032" : { - "id" : "10032", - "name" : "Viewstate", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10032" - }, - "Directory-Browsing-10033" : { - "id" : "10033", - "name" : "Directory Browsing", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10033" - }, - "Heartbleed-OpenSSL-Vulnerability-(Indicative)-10034" : { - "id" : "10034", - "name" : "Heartbleed OpenSSL Vulnerability (Indicative)", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10034" - }, - "Strict-Transport-Security-Header-10035" : { - "id" : "10035", - "name" : "Strict-Transport-Security Header", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10035" - }, - "HTTP-Server-Response-Header-10036" : { - "id" : "10036", - "name" : "HTTP Server Response Header", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10036" - }, - "Server-Leaks-Information-via-\"X-Powered-By\"-HTTP-Response-Header-Field(s)-10037" : { - "id" : "10037", - "name" : "Server Leaks Information via \"X-Powered-By\" HTTP Response Header Field(s)", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10037" - }, - "Content-Security-Policy-(CSP)-Header-Not-Set-10038" : { - "id" : "10038", - "name" : "Content Security Policy (CSP) Header Not Set", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10038" - }, - "X-Backend-Server-Header-Information-Leak-10039" : { - "id" : "10039", - "name" : "X-Backend-Server Header Information Leak", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10039" - }, - "Secure-Pages-Include-Mixed-Content-10040" : { - "id" : "10040", - "name" : "Secure Pages Include Mixed Content", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10040" - }, - "HTTP-to-HTTPS-Insecure-Transition-in-Form-Post-10041" : { - "id" : "10041", - "name" : "HTTP to HTTPS Insecure Transition in Form Post", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10041" - }, - "HTTPS-to-HTTP-Insecure-Transition-in-Form-Post-10042" : { - "id" : "10042", - "name" : "HTTPS to HTTP Insecure Transition in Form Post", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10042" - }, - "User-Controllable-JavaScript-Event-(XSS)-10043" : { - "id" : "10043", - "name" : "User Controllable JavaScript Event (XSS)", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10043" - }, - "Big-Redirect-Detected-(Potential-Sensitive-Information-Leak)-10044" : { - "id" : "10044", - "name" : "Big Redirect Detected (Potential Sensitive Information Leak)", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10044" - }, - "Source-Code-Disclosure-/WEB-INF-Folder-10045" : { - "id" : "10045", - "name" : "Source Code Disclosure - /WEB-INF Folder", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/10045" - }, - "HTTPS-Content-Available-via-HTTP-10047" : { - "id" : "10047", - "name" : "HTTPS Content Available via HTTP", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/10047" - }, - "Remote-Code-Execution-Shell-Shock-10048" : { - "id" : "10048", - "name" : "Remote Code Execution - Shell Shock", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/10048" - }, - "Content-Cacheability-10049" : { - "id" : "10049", - "name" : "Content Cacheability", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10049" - }, - "Retrieved-from-Cache-10050" : { - "id" : "10050", - "name" : "Retrieved from Cache", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10050" - }, - "Relative-Path-Confusion-10051" : { - "id" : "10051", - "name" : "Relative Path Confusion", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/10051" - }, - "X-ChromeLogger-Data-(XCOLD)-Header-Information-Leak-10052" : { - "id" : "10052", - "name" : "X-ChromeLogger-Data (XCOLD) Header Information Leak", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10052" - }, - "Cookie-without-SameSite-Attribute-10054" : { - "id" : "10054", - "name" : "Cookie without SameSite Attribute", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10054" - }, - "CSP-10055" : { - "id" : "10055", - "name" : "CSP", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10055" - }, - "X-Debug-Token-Information-Leak-10056" : { - "id" : "10056", - "name" : "X-Debug-Token Information Leak", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10056" - }, - "Username-Hash-Found-10057" : { - "id" : "10057", - "name" : "Username Hash Found", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10057" - }, - "GET-for-POST-10058" : { - "id" : "10058", - "name" : "GET for POST", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/10058" - }, - "X-AspNet-Version-Response-Header-10061" : { - "id" : "10061", - "name" : "X-AspNet-Version Response Header", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10061" - }, - "PII-Disclosure-10062" : { - "id" : "10062", - "name" : "PII Disclosure", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10062" - }, - "Permissions-Policy-Header-Not-Set-10063" : { - "id" : "10063", - "name" : "Permissions Policy Header Not Set", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10063" - }, - "Use-of-SAML-10070" : { - "id" : "10070", - "name" : "Use of SAML", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10070" - }, - "Base64-Disclosure-10094" : { - "id" : "10094", - "name" : "Base64 Disclosure", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10094" - }, - "Backup-File-Disclosure-10095" : { - "id" : "10095", - "name" : "Backup File Disclosure", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/10095" - }, - "Timestamp-Disclosure-Unix-10096" : { - "id" : "10096", - "name" : "Timestamp Disclosure - Unix", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10096" - }, - "Hash-Disclosure-MD4-/-MD5-10097" : { - "id" : "10097", - "name" : "Hash Disclosure - MD4 / MD5", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10097" - }, - "Cross-Domain-Misconfiguration-10098" : { - "id" : "10098", - "name" : "Cross-Domain Misconfiguration", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10098" - }, - "Source-Code-Disclosure-PHP-10099" : { - "id" : "10099", - "name" : "Source Code Disclosure - PHP", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10099" - }, - "Access-Control-Issue-Improper-Authentication-10101" : { - "id" : "10101", - "name" : "Access Control Issue - Improper Authentication", - "type" : "tool", - "link" : "https://www.zaproxy.org/docs/alerts/10101" - }, - "Access-Control-Issue-Improper-Authorization-10102" : { - "id" : "10102", - "name" : "Access Control Issue - Improper Authorization", - "type" : "tool", - "link" : "https://www.zaproxy.org/docs/alerts/10102" - }, - "Image-Exposes-Location-or-Privacy-Data-10103" : { - "id" : "10103", - "name" : "Image Exposes Location or Privacy Data", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10103" - }, - "User-Agent-Fuzzer-10104" : { - "id" : "10104", - "name" : "User Agent Fuzzer", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/10104" - }, - "Weak-Authentication-Method-10105" : { - "id" : "10105", - "name" : "Weak Authentication Method", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10105" - }, - "HTTP-Only-Site-10106" : { - "id" : "10106", - "name" : "HTTP Only Site", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/10106" - }, - "Httpoxy-Proxy-Header-Misuse-10107" : { - "id" : "10107", - "name" : "Httpoxy - Proxy Header Misuse", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/10107" - }, - "Reverse-Tabnabbing-10108" : { - "id" : "10108", - "name" : "Reverse Tabnabbing", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10108" - }, - "Modern-Web-Application-10109" : { - "id" : "10109", - "name" : "Modern Web Application", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10109" - }, - "Dangerous-JS-Functions-10110" : { - "id" : "10110", - "name" : "Dangerous JS Functions", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10110" - }, - "Authentication-Request-Identified-10111" : { - "id" : "10111", - "name" : "Authentication Request Identified", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10111" - }, - "Session-Management-Response-Identified-10112" : { - "id" : "10112", - "name" : "Session Management Response Identified", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10112" - }, - "Verification-Request-Identified-10113" : { - "id" : "10113", - "name" : "Verification Request Identified", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10113" - }, - "Script-Served-From-Malicious-Domain-(polyfill)-10115" : { - "id" : "10115", - "name" : "Script Served From Malicious Domain (polyfill)", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10115" - }, - "Absence-of-Anti-CSRF-Tokens-10202" : { - "id" : "10202", - "name" : "Absence of Anti-CSRF Tokens", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/10202" - }, - "Anti-CSRF-Tokens-Check-20012" : { - "id" : "20012", - "name" : "Anti-CSRF Tokens Check", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/20012" - }, - "HTTP-Parameter-Pollution-20014" : { - "id" : "20014", - "name" : "HTTP Parameter Pollution", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/20014" - }, - "Heartbleed-OpenSSL-Vulnerability-20015" : { - "id" : "20015", - "name" : "Heartbleed OpenSSL Vulnerability", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/20015" - }, - "Cross-Domain-Misconfiguration-20016" : { - "id" : "20016", - "name" : "Cross-Domain Misconfiguration", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/20016" - }, - "Source-Code-Disclosure-CVE-2012-1823-20017" : { - "id" : "20017", - "name" : "Source Code Disclosure - CVE-2012-1823", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/20017" - }, - "Remote-Code-Execution-CVE-2012-1823-20018" : { - "id" : "20018", - "name" : "Remote Code Execution - CVE-2012-1823", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/20018" - }, - "External-Redirect-20019" : { - "id" : "20019", - "name" : "External Redirect", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/20019" - }, - "Buffer-Overflow-30001" : { - "id" : "30001", - "name" : "Buffer Overflow", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/30001" - }, - "Format-String-Error-30002" : { - "id" : "30002", - "name" : "Format String Error", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/30002" - }, - "Integer-Overflow-Error-30003" : { - "id" : "30003", - "name" : "Integer Overflow Error", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/30003" - }, - "CRLF-Injection-40003" : { - "id" : "40003", - "name" : "CRLF Injection", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40003" - }, - "Parameter-Tampering-40008" : { - "id" : "40008", - "name" : "Parameter Tampering", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40008" - }, - "Server-Side-Include-40009" : { - "id" : "40009", - "name" : "Server Side Include", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40009" - }, - "Cross-Site-Scripting-(Reflected)-40012" : { - "id" : "40012", - "name" : "Cross Site Scripting (Reflected)", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40012" - }, - "Session-Fixation-40013" : { - "id" : "40013", - "name" : "Session Fixation", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40013" - }, - "Cross-Site-Scripting-(Persistent)-40014" : { - "id" : "40014", - "name" : "Cross Site Scripting (Persistent)", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40014" - }, - "LDAP-Injection-40015" : { - "id" : "40015", - "name" : "LDAP Injection", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40015" - }, - "Cross-Site-Scripting-(Persistent)-Prime-40016" : { - "id" : "40016", - "name" : "Cross Site Scripting (Persistent) - Prime", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40016" - }, - "Cross-Site-Scripting-(Persistent)-Spider-40017" : { - "id" : "40017", - "name" : "Cross Site Scripting (Persistent) - Spider", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40017" - }, - "SQL-Injection-40018" : { - "id" : "40018", - "name" : "SQL Injection", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40018" - }, - "SQL-Injection-MySQL-40019" : { - "id" : "40019", - "name" : "SQL Injection - MySQL", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40019" - }, - "SQL-Injection-Hypersonic-SQL-40020" : { - "id" : "40020", - "name" : "SQL Injection - Hypersonic SQL", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40020" - }, - "SQL-Injection-Oracle-40021" : { - "id" : "40021", - "name" : "SQL Injection - Oracle", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40021" - }, - "SQL-Injection-PostgreSQL-40022" : { - "id" : "40022", - "name" : "SQL Injection - PostgreSQL", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40022" - }, - "Possible-Username-Enumeration-40023" : { - "id" : "40023", - "name" : "Possible Username Enumeration", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40023" - }, - "SQL-Injection-SQLite-40024" : { - "id" : "40024", - "name" : "SQL Injection - SQLite", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40024" - }, - "Proxy-Disclosure-40025" : { - "id" : "40025", - "name" : "Proxy Disclosure", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40025" - }, - "Cross-Site-Scripting-(DOM-Based)-40026" : { - "id" : "40026", - "name" : "Cross Site Scripting (DOM Based)", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40026" - }, - "SQL-Injection-MsSQL-40027" : { - "id" : "40027", - "name" : "SQL Injection - MsSQL", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40027" - }, - "ELMAH-Information-Leak-40028" : { - "id" : "40028", - "name" : "ELMAH Information Leak", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40028" - }, - "Trace.axd-Information-Leak-40029" : { - "id" : "40029", - "name" : "Trace.axd Information Leak", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40029" - }, - "Out-of-Band-XSS-40031" : { - "id" : "40031", - "name" : "Out of Band XSS", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40031" - }, - ".htaccess-Information-Leak-40032" : { - "id" : "40032", - "name" : ".htaccess Information Leak", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40032" - }, - "NoSQL-Injection-MongoDB-40033" : { - "id" : "40033", - "name" : "NoSQL Injection - MongoDB", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40033" - }, - ".env-Information-Leak-40034" : { - "id" : "40034", - "name" : ".env Information Leak", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40034" - }, - "Hidden-File-Found-40035" : { - "id" : "40035", - "name" : "Hidden File Found", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40035" - }, - "JWT-Scan-Rule-40036" : { - "id" : "40036", - "name" : "JWT Scan Rule", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40036" - }, - "Bypassing-403-40038" : { - "id" : "40038", - "name" : "Bypassing 403", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40038" - }, - "Web-Cache-Deception-40039" : { - "id" : "40039", - "name" : "Web Cache Deception", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40039" - }, - "CORS-Header-40040" : { - "id" : "40040", - "name" : "CORS Header", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40040" - }, - "File-Upload-40041" : { - "id" : "40041", - "name" : "File Upload", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40041" - }, - "Spring-Actuator-Information-Leak-40042" : { - "id" : "40042", - "name" : "Spring Actuator Information Leak", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40042" - }, - "Log4Shell-40043" : { - "id" : "40043", - "name" : "Log4Shell", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40043" - }, - "Exponential-Entity-Expansion-(Billion-Laughs-Attack)-40044" : { - "id" : "40044", - "name" : "Exponential Entity Expansion (Billion Laughs Attack)", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40044" - }, - "Spring4Shell-40045" : { - "id" : "40045", - "name" : "Spring4Shell", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40045" - }, - "Server-Side-Request-Forgery-40046" : { - "id" : "40046", - "name" : "Server Side Request Forgery", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40046" - }, - "Text4shell-(CVE-2022-42889)-40047" : { - "id" : "40047", - "name" : "Text4shell (CVE-2022-42889)", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/40047" - }, - "ExtensionGraphQl-50007" : { - "id" : "50007", - "name" : "ExtensionGraphQl", - "type" : "tool", - "link" : "https://www.zaproxy.org/docs/alerts/50007" - }, - "Insecure-JSF-ViewState-90001" : { - "id" : "90001", - "name" : "Insecure JSF ViewState", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/90001" - }, - "Java-Serialization-Object-90002" : { - "id" : "90002", - "name" : "Java Serialization Object", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/90002" - }, - "Sub-Resource-Integrity-Attribute-Missing-90003" : { - "id" : "90003", - "name" : "Sub Resource Integrity Attribute Missing", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/90003" - }, - "Insufficient-Site-Isolation-Against-Spectre-Vulnerability-90004" : { - "id" : "90004", - "name" : "Insufficient Site Isolation Against Spectre Vulnerability", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/90004" - }, - "Fetch-Metadata-Request-Headers-90005" : { - "id" : "90005", - "name" : "Fetch Metadata Request Headers", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/90005" - }, - "Charset-Mismatch-90011" : { - "id" : "90011", - "name" : "Charset Mismatch", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/90011" - }, - "XSLT-Injection-90017" : { - "id" : "90017", - "name" : "XSLT Injection", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/90017" - }, - "Advanced-SQL-Injection-90018" : { - "id" : "90018", - "name" : "Advanced SQL Injection", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/90018" - }, - "Server-Side-Code-Injection-90019" : { - "id" : "90019", - "name" : "Server Side Code Injection", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/90019" - }, - "Remote-OS-Command-Injection-90020" : { - "id" : "90020", - "name" : "Remote OS Command Injection", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/90020" - }, - "XPath-Injection-90021" : { - "id" : "90021", - "name" : "XPath Injection", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/90021" - }, - "Application-Error-Disclosure-90022" : { - "id" : "90022", - "name" : "Application Error Disclosure", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/90022" - }, - "XML-External-Entity-Attack-90023" : { - "id" : "90023", - "name" : "XML External Entity Attack", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/90023" - }, - "Generic-Padding-Oracle-90024" : { - "id" : "90024", - "name" : "Generic Padding Oracle", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/90024" - }, - "Expression-Language-Injection-90025" : { - "id" : "90025", - "name" : "Expression Language Injection", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/90025" - }, - "SOAP-Action-Spoofing-90026" : { - "id" : "90026", - "name" : "SOAP Action Spoofing", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/90026" - }, - "Cookie-Slack-Detector-90027" : { - "id" : "90027", - "name" : "Cookie Slack Detector", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/90027" - }, - "Insecure-HTTP-Method-90028" : { - "id" : "90028", - "name" : "Insecure HTTP Method", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/90028" - }, - "SOAP-XML-Injection-90029" : { - "id" : "90029", - "name" : "SOAP XML Injection", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/90029" - }, - "WSDL-File-Detection-90030" : { - "id" : "90030", - "name" : "WSDL File Detection", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/90030" - }, - "Loosely-Scoped-Cookie-90033" : { - "id" : "90033", - "name" : "Loosely Scoped Cookie", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/90033" - }, - "Cloud-Metadata-Potentially-Exposed-90034" : { - "id" : "90034", - "name" : "Cloud Metadata Potentially Exposed", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/90034" - }, - "Server-Side-Template-Injection-90035" : { - "id" : "90035", - "name" : "Server Side Template Injection", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/90035" - }, - "Server-Side-Template-Injection-(Blind)-90036" : { - "id" : "90036", - "name" : "Server Side Template Injection (Blind)", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/90036" - }, - "NoSQL-Injection-MongoDB-(Time-Based)-90039" : { - "id" : "90039", - "name" : "NoSQL Injection - MongoDB (Time Based)", - "type" : "active", - "link" : "https://www.zaproxy.org/docs/alerts/90039" - }, - "Server-is-running-on-Clacks-GNU-Terry-Pratchett-100002" : { - "id" : "100002", - "name" : "Server is running on Clacks - GNU Terry Pratchett", - "type" : "script passive", - "link" : "https://www.zaproxy.org/docs/alerts/100002" - }, - "Cookie-Set-Without-HttpOnly-Flag-100003" : { - "id" : "100003", - "name" : "Cookie Set Without HttpOnly Flag", - "type" : "script passive", - "link" : "https://www.zaproxy.org/docs/alerts/100003" - }, - "Content-Security-Policy-Violations-Reporting-Enabled-100004" : { - "id" : "100004", - "name" : "Content Security Policy Violations Reporting Enabled", - "type" : "script passive", - "link" : "https://www.zaproxy.org/docs/alerts/100004" - }, - "SameSite-Cookie-Attribute-Protection-Used-100005" : { - "id" : "100005", - "name" : "SameSite Cookie Attribute Protection Used", - "type" : "script passive", - "link" : "https://www.zaproxy.org/docs/alerts/100005" - }, - "Information-Disclosure-IP-Exposed-via-F5-BIG-IP-Persistence-Cookie-100006" : { - "id" : "100006", - "name" : "Information Disclosure - IP Exposed via F5 BIG-IP Persistence Cookie", - "type" : "script passive", - "link" : "https://www.zaproxy.org/docs/alerts/100006" - }, - "Information-Disclosure-Base64-encoded-String-100007" : { - "id" : "100007", - "name" : "Information Disclosure - Base64-encoded String", - "type" : "script passive", - "link" : "https://www.zaproxy.org/docs/alerts/100007" - }, - "Information-Disclosure-Credit-Card-Number-100008" : { - "id" : "100008", - "name" : "Information Disclosure - Credit Card Number", - "type" : "script passive", - "link" : "https://www.zaproxy.org/docs/alerts/100008" - }, - "Information-Disclosure-Email-Addresses-100009" : { - "id" : "100009", - "name" : "Information Disclosure - Email Addresses", - "type" : "script passive", - "link" : "https://www.zaproxy.org/docs/alerts/100009" - }, - "Information-Disclosure-Hash-100010" : { - "id" : "100010", - "name" : "Information Disclosure - Hash", - "type" : "script passive", - "link" : "https://www.zaproxy.org/docs/alerts/100010" - }, - "Information-Disclosure-HTML-Comments-100011" : { - "id" : "100011", - "name" : "Information Disclosure - HTML Comments", - "type" : "script passive", - "link" : "https://www.zaproxy.org/docs/alerts/100011" - }, - "Information-Disclosure-IBAN-Numbers-100012" : { - "id" : "100012", - "name" : "Information Disclosure - IBAN Numbers", - "type" : "script passive", - "link" : "https://www.zaproxy.org/docs/alerts/100012" - }, - "Information-Disclosure-Private-IP-Address-100013" : { - "id" : "100013", - "name" : "Information Disclosure - Private IP Address", - "type" : "script passive", - "link" : "https://www.zaproxy.org/docs/alerts/100013" - }, - "Reflected-HTTP-GET-Parameter(s)-100014" : { - "id" : "100014", - "name" : "Reflected HTTP GET Parameter(s)", - "type" : "script passive", - "link" : "https://www.zaproxy.org/docs/alerts/100014" - }, - "HUNT-Methodology-100015" : { - "id" : "100015", - "name" : "HUNT Methodology", - "type" : "script passive", - "link" : "https://www.zaproxy.org/docs/alerts/100015" - }, - "Missing-Security-Headers-100016" : { - "id" : "100016", - "name" : "Missing Security Headers", - "type" : "script passive", - "link" : "https://www.zaproxy.org/docs/alerts/100016" - }, - "Non-Static-Site-Detected-100017" : { - "id" : "100017", - "name" : "Non Static Site Detected", - "type" : "script passive", - "link" : "https://www.zaproxy.org/docs/alerts/100017" - }, - "Relative-Path-Overwrite-100018" : { - "id" : "100018", - "name" : "Relative Path Overwrite", - "type" : "script passive", - "link" : "https://www.zaproxy.org/docs/alerts/100018" - }, - "Information-Disclosure-Server-Header-100019" : { - "id" : "100019", - "name" : "Information Disclosure - Server Header", - "type" : "script passive", - "link" : "https://www.zaproxy.org/docs/alerts/100019" - }, - "Information-Disclosure-SQL-Error-100020" : { - "id" : "100020", - "name" : "Information Disclosure - SQL Error", - "type" : "script passive", - "link" : "https://www.zaproxy.org/docs/alerts/100020" - }, - "Telerik-UI-for-ASP.NET-AJAX-Cryptographic-Weakness-(CVE-2017-9248)-100021" : { - "id" : "100021", - "name" : "Telerik UI for ASP.NET AJAX Cryptographic Weakness (CVE-2017-9248)", - "type" : "script passive", - "link" : "https://www.zaproxy.org/docs/alerts/100021" - }, - "Upload-Form-Discovered-100022" : { - "id" : "100022", - "name" : "Upload Form Discovered", - "type" : "script passive", - "link" : "https://www.zaproxy.org/docs/alerts/100022" - }, - "Information-Disclosure-X-Powered-By-Header-100023" : { - "id" : "100023", - "name" : "Information Disclosure - X-Powered-By Header", - "type" : "script passive", - "link" : "https://www.zaproxy.org/docs/alerts/100023" - }, - "Cross-Site-WebSocket-Hijacking-100025" : { - "id" : "100025", - "name" : "Cross-Site WebSocket Hijacking", - "type" : "script active", - "link" : "https://www.zaproxy.org/docs/alerts/100025" - }, - "JWT-None-Exploit-100026" : { - "id" : "100026", - "name" : "JWT None Exploit", - "type" : "script active", - "link" : "https://www.zaproxy.org/docs/alerts/100026" - }, - "File-Content-Disclosure-(CVE-2019-5418)-100029" : { - "id" : "100029", - "name" : "File Content Disclosure (CVE-2019-5418)", - "type" : "script active", - "link" : "https://www.zaproxy.org/docs/alerts/100029" - }, - "Backup-File-Detected-100030" : { - "id" : "100030", - "name" : "Backup File Detected", - "type" : "script active", - "link" : "https://www.zaproxy.org/docs/alerts/100030" - }, - "Information-Disclosure-Google-API-Key-100034" : { - "id" : "100034", - "name" : "Information Disclosure - Google API Key", - "type" : "script passive", - "link" : "https://www.zaproxy.org/docs/alerts/100034" - }, - "Information-Disclosure-Java-Stack-Trace-100035" : { - "id" : "100035", - "name" : "Information Disclosure - Java Stack Trace", - "type" : "script passive", - "link" : "https://www.zaproxy.org/docs/alerts/100035" - }, - "Information-Disclosure-Amazon-S3-Bucket-URL-100036" : { - "id" : "100036", - "name" : "Information Disclosure - Amazon S3 Bucket URL", - "type" : "script passive", - "link" : "https://www.zaproxy.org/docs/alerts/100036" - }, - "Application-Error-Disclosure-via-WebSockets-110001" : { - "id" : "110001", - "name" : "Application Error Disclosure via WebSockets", - "type" : "websocket passive", - "link" : "https://www.zaproxy.org/docs/alerts/110001" - }, - "Base64-Disclosure-in-WebSocket-message-110002" : { - "id" : "110002", - "name" : "Base64 Disclosure in WebSocket message", - "type" : "websocket passive", - "link" : "https://www.zaproxy.org/docs/alerts/110002" - }, - "Information-Disclosure-Debug-Error-Messages-via-WebSocket-110003" : { - "id" : "110003", - "name" : "Information Disclosure - Debug Error Messages via WebSocket", - "type" : "websocket passive", - "link" : "https://www.zaproxy.org/docs/alerts/110003" - }, - "Email-address-found-in-WebSocket-message-110004" : { - "id" : "110004", - "name" : "Email address found in WebSocket message", - "type" : "websocket passive", - "link" : "https://www.zaproxy.org/docs/alerts/110004" - }, - "Personally-Identifiable-Information-via-WebSocket-110005" : { - "id" : "110005", - "name" : "Personally Identifiable Information via WebSocket", - "type" : "websocket passive", - "link" : "https://www.zaproxy.org/docs/alerts/110005" - }, - "Private-IP-Disclosure-via-WebSocket-110006" : { - "id" : "110006", - "name" : "Private IP Disclosure via WebSocket", - "type" : "websocket passive", - "link" : "https://www.zaproxy.org/docs/alerts/110006" - }, - "Username-Hash-Found-in-WebSocket-message-110007" : { - "id" : "110007", - "name" : "Username Hash Found in WebSocket message", - "type" : "websocket passive", - "link" : "https://www.zaproxy.org/docs/alerts/110007" - }, - "Information-Disclosure-Suspicious-Comments-in-XML-via-WebSocket-110008" : { - "id" : "110008", - "name" : "Information Disclosure - Suspicious Comments in XML via WebSocket", - "type" : "websocket passive", - "link" : "https://www.zaproxy.org/docs/alerts/110008" - }, - "Full-Path-Disclosure-110009" : { - "id" : "110009", - "name" : "Full Path Disclosure", - "type" : "passive", - "link" : "https://www.zaproxy.org/docs/alerts/110009" - }, - "Information-Disclosure-Information-in-Browser-Storage-120000" : { - "id" : "120000", - "name" : "Information Disclosure - Information in Browser Storage", - "type" : "client passive", - "link" : "https://www.zaproxy.org/docs/alerts/120000" - }, - "Information-Disclosure-Sensitive-Information-in-Browser-Storage-120001" : { - "id" : "120001", - "name" : "Information Disclosure - Sensitive Information in Browser Storage", - "type" : "client passive", - "link" : "https://www.zaproxy.org/docs/alerts/120001" - }, - "Information-Disclosure-JWT-in-Browser-Storage-120002" : { - "id" : "120002", - "name" : "Information Disclosure - JWT in Browser Storage", - "type" : "client passive", - "link" : "https://www.zaproxy.org/docs/alerts/120002" - } - } -} \ No newline at end of file diff --git a/sechub-pds-solutions/owaspzap/docker/scripts/owasp-zap.sh b/sechub-pds-solutions/owaspzap/docker/scripts/owasp-zap.sh index 2168042335..fb3ae04865 100755 --- a/sechub-pds-solutions/owaspzap/docker/scripts/owasp-zap.sh +++ b/sechub-pds-solutions/owaspzap/docker/scripts/owasp-zap.sh @@ -125,14 +125,7 @@ echo "" echo "Start scanning" echo "" -if [ ! -z "$PDS_SCAN_CONFIGURATION" ] ; then - sechub_scan_configuration="$PDS_JOB_WORKSPACE_LOCATION/sechubScanConfiguration.json" - echo "Using configuration file: $sechub_scan_configuration" - echo "$PDS_SCAN_CONFIGURATION" > "$sechub_scan_configuration" - zap_options="$zap_options --sechubConfigfile $sechub_scan_configuration" -fi - -java -jar $options "$TOOL_FOLDER/wrapperowaspzap.jar" $zap_options --zapHost "$ZAP_HOST" --zapPort "$ZAP_PORT" --zapApiKey "$ZAP_API_KEY" --jobUUID "$SECHUB_JOB_UUID" --targetURL "$PDS_SCAN_TARGET_URL" --report "$PDS_JOB_RESULT_FILE" --fullRulesetfile "$TOOL_FOLDER/owasp-zap-full-ruleset-all-release-status.json" +java -jar $options "$TOOL_FOLDER/wrapperowaspzap.jar" $zap_options --zapHost "$ZAP_HOST" --zapPort "$ZAP_PORT" --zapApiKey "$ZAP_API_KEY" --jobUUID "$SECHUB_JOB_UUID" --targetURL "$PDS_SCAN_TARGET_URL" --report "$PDS_JOB_RESULT_FILE" # Shutdown OWASP-ZAP and cleanup after the scan echo "Shutdown OWASP-ZAP after scan" diff --git a/sechub-pds-solutions/owaspzap/helm/pds-owaspzap/Chart.yaml b/sechub-pds-solutions/owaspzap/helm/pds-owaspzap/Chart.yaml index a9d8e66502..a42eaafc8f 100644 --- a/sechub-pds-solutions/owaspzap/helm/pds-owaspzap/Chart.yaml +++ b/sechub-pds-solutions/owaspzap/helm/pds-owaspzap/Chart.yaml @@ -9,4 +9,4 @@ type: application # This is the chart version. # This version number should be incremented each time you make changes to the chart and its templates. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.5.0 +version: 1.6.0 diff --git a/sechub-pds-solutions/owaspzap/helm/pds-owaspzap/templates/deployment.yaml b/sechub-pds-solutions/owaspzap/helm/pds-owaspzap/templates/deployment.yaml index 5a5e2840e7..514696b6e0 100644 --- a/sechub-pds-solutions/owaspzap/helm/pds-owaspzap/templates/deployment.yaml +++ b/sechub-pds-solutions/owaspzap/helm/pds-owaspzap/templates/deployment.yaml @@ -125,6 +125,8 @@ spec: value: "{{ .Values.storage.sharedVolume.upload.dir }}" {{- end}} ports: + - name: pds-health-port + containerPort: 10251 - name: pds-https-port containerPort: 8444 startupProbe: @@ -132,9 +134,19 @@ spec: scheme: HTTPS path: /api/anonymous/check/alive port: pds-https-port + initialDelaySeconds: 5 periodSeconds: 1 - failureThreshold: 300 - # probe every 1s x 300 = 5 mins before restart of container + # probe every 1s x 600 = 10 mins before restart of container (some PDS download huge files before startup) + failureThreshold: 600 + successThreshold: 1 + timeoutSeconds: 1 + readinessProbe: + httpGet: + scheme: HTTP + path: /actuator/health/readiness + port: pds-health-port + periodSeconds: 2 + failureThreshold: 2 successThreshold: 1 timeoutSeconds: 1 livenessProbe: @@ -145,7 +157,7 @@ spec: periodSeconds: 5 failureThreshold: 3 successThreshold: 1 - timeoutSeconds: 3 + timeoutSeconds: 5 volumeMounts: {{- if .Values.pds.volumes.pdsWorkspace.enabled }} - mountPath: "/workspace" diff --git a/sechub-pds-solutions/pmd/helm/pds-pmd/Chart.yaml b/sechub-pds-solutions/pmd/helm/pds-pmd/Chart.yaml index 61249dea99..5cba443a75 100644 --- a/sechub-pds-solutions/pmd/helm/pds-pmd/Chart.yaml +++ b/sechub-pds-solutions/pmd/helm/pds-pmd/Chart.yaml @@ -9,4 +9,4 @@ type: application # This is the chart version. # This version number should be incremented each time you make changes to the chart and its templates. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.4.0 +version: 1.5.0 diff --git a/sechub-pds-solutions/pmd/helm/pds-pmd/templates/deployment.yaml b/sechub-pds-solutions/pmd/helm/pds-pmd/templates/deployment.yaml index 74d77f580b..587895f63d 100644 --- a/sechub-pds-solutions/pmd/helm/pds-pmd/templates/deployment.yaml +++ b/sechub-pds-solutions/pmd/helm/pds-pmd/templates/deployment.yaml @@ -124,6 +124,8 @@ spec: value: "{{ .Values.storage.sharedVolume.upload.dir }}" {{- end}} ports: + - name: pds-health-port + containerPort: 10251 - name: pds-https-port containerPort: 8444 startupProbe: @@ -131,9 +133,19 @@ spec: scheme: HTTPS path: /api/anonymous/check/alive port: pds-https-port + initialDelaySeconds: 5 periodSeconds: 1 - failureThreshold: 300 - # probe every 1s x 300 = 5 mins before restart of container + # probe every 1s x 600 = 10 mins before restart of container (some PDS download huge files before startup) + failureThreshold: 600 + successThreshold: 1 + timeoutSeconds: 1 + readinessProbe: + httpGet: + scheme: HTTP + path: /actuator/health/readiness + port: pds-health-port + periodSeconds: 2 + failureThreshold: 2 successThreshold: 1 timeoutSeconds: 1 livenessProbe: @@ -144,7 +156,7 @@ spec: periodSeconds: 5 failureThreshold: 3 successThreshold: 1 - timeoutSeconds: 3 + timeoutSeconds: 5 volumeMounts: - mountPath: "/workspace" name: pds-workspace diff --git a/sechub-pds-solutions/prepare/helm/pds-prepare/Chart.yaml b/sechub-pds-solutions/prepare/helm/pds-prepare/Chart.yaml index 9524db9f7b..3a978f803a 100644 --- a/sechub-pds-solutions/prepare/helm/pds-prepare/Chart.yaml +++ b/sechub-pds-solutions/prepare/helm/pds-prepare/Chart.yaml @@ -9,4 +9,4 @@ type: application # This is the chart version. # This version number should be incremented each time you make changes to the chart and its templates. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.4.0 +version: 1.5.0 diff --git a/sechub-pds-solutions/prepare/helm/pds-prepare/templates/deployment.yaml b/sechub-pds-solutions/prepare/helm/pds-prepare/templates/deployment.yaml index 7d3e8956f1..850d4e7e84 100644 --- a/sechub-pds-solutions/prepare/helm/pds-prepare/templates/deployment.yaml +++ b/sechub-pds-solutions/prepare/helm/pds-prepare/templates/deployment.yaml @@ -129,6 +129,8 @@ spec: - name: PDS_NO_PROXY value: {{ .Values.proxy.noProxy }} ports: + - name: pds-health-port + containerPort: 10251 - name: pds-https-port containerPort: 8444 startupProbe: @@ -136,9 +138,19 @@ spec: scheme: HTTPS path: /api/anonymous/check/alive port: pds-https-port + initialDelaySeconds: 5 periodSeconds: 1 - failureThreshold: 300 - # probe every 1s x 300 = 5 mins before restart of container + # probe every 1s x 600 = 10 mins before restart of container (some PDS download huge files before startup) + failureThreshold: 600 + successThreshold: 1 + timeoutSeconds: 1 + readinessProbe: + httpGet: + scheme: HTTP + path: /actuator/health/readiness + port: pds-health-port + periodSeconds: 2 + failureThreshold: 2 successThreshold: 1 timeoutSeconds: 1 livenessProbe: @@ -149,7 +161,7 @@ spec: periodSeconds: 5 failureThreshold: 3 successThreshold: 1 - timeoutSeconds: 3 + timeoutSeconds: 5 volumeMounts: - mountPath: "/workspace" name: pds-workspace diff --git a/sechub-pds-solutions/scancode/helm/pds-scancode/Chart.yaml b/sechub-pds-solutions/scancode/helm/pds-scancode/Chart.yaml index b8ec812e50..cd1797fc7f 100644 --- a/sechub-pds-solutions/scancode/helm/pds-scancode/Chart.yaml +++ b/sechub-pds-solutions/scancode/helm/pds-scancode/Chart.yaml @@ -9,4 +9,4 @@ type: application # This is the chart version. # This version number should be incremented each time you make changes to the chart and its templates. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.4.0 +version: 1.5.0 diff --git a/sechub-pds-solutions/scancode/helm/pds-scancode/templates/deployment.yaml b/sechub-pds-solutions/scancode/helm/pds-scancode/templates/deployment.yaml index 50280d8fd6..f3e66c12b6 100644 --- a/sechub-pds-solutions/scancode/helm/pds-scancode/templates/deployment.yaml +++ b/sechub-pds-solutions/scancode/helm/pds-scancode/templates/deployment.yaml @@ -124,6 +124,8 @@ spec: value: "{{ .Values.storage.sharedVolume.upload.dir }}" {{- end}} ports: + - name: pds-health-port + containerPort: 10251 - name: pds-https-port containerPort: 8444 startupProbe: @@ -131,9 +133,19 @@ spec: scheme: HTTPS path: /api/anonymous/check/alive port: pds-https-port + initialDelaySeconds: 5 periodSeconds: 1 - failureThreshold: 300 - # probe every 1s x 300 = 5 mins before restart of container + # probe every 1s x 600 = 10 mins before restart of container (some PDS download huge files before startup) + failureThreshold: 600 + successThreshold: 1 + timeoutSeconds: 1 + readinessProbe: + httpGet: + scheme: HTTP + path: /actuator/health/readiness + port: pds-health-port + periodSeconds: 2 + failureThreshold: 2 successThreshold: 1 timeoutSeconds: 1 livenessProbe: @@ -144,7 +156,7 @@ spec: periodSeconds: 5 failureThreshold: 3 successThreshold: 1 - timeoutSeconds: 3 + timeoutSeconds: 5 volumeMounts: - mountPath: "/workspace" name: pds-workspace diff --git a/sechub-pds-solutions/xray/helm/pds-xray/Chart.yaml b/sechub-pds-solutions/xray/helm/pds-xray/Chart.yaml index d230dd3449..6ce70cb0c1 100644 --- a/sechub-pds-solutions/xray/helm/pds-xray/Chart.yaml +++ b/sechub-pds-solutions/xray/helm/pds-xray/Chart.yaml @@ -9,4 +9,4 @@ type: application # This is the chart version. # This version number should be incremented each time you make changes to the chart and its templates. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.3.0 +version: 1.4.0 diff --git a/sechub-pds-solutions/xray/helm/pds-xray/templates/deployment.yaml b/sechub-pds-solutions/xray/helm/pds-xray/templates/deployment.yaml index 41ee7c0b79..6fde67ba1c 100644 --- a/sechub-pds-solutions/xray/helm/pds-xray/templates/deployment.yaml +++ b/sechub-pds-solutions/xray/helm/pds-xray/templates/deployment.yaml @@ -133,6 +133,8 @@ spec: - name: XRAY_PASSWORD value: {{ .Values.xrayWrapper.artifactory.password }} ports: + - name: pds-health-port + containerPort: 10251 - name: pds-https-port containerPort: 8444 startupProbe: @@ -140,9 +142,19 @@ spec: scheme: HTTPS path: /api/anonymous/check/alive port: pds-https-port + initialDelaySeconds: 5 periodSeconds: 1 - failureThreshold: 300 - # probe every 1s x 300 = 5 mins before restart of container + # probe every 1s x 600 = 10 mins before restart of container (some PDS download huge files before startup) + failureThreshold: 600 + successThreshold: 1 + timeoutSeconds: 1 + readinessProbe: + httpGet: + scheme: HTTP + path: /actuator/health/readiness + port: pds-health-port + periodSeconds: 2 + failureThreshold: 2 successThreshold: 1 timeoutSeconds: 1 livenessProbe: @@ -153,7 +165,7 @@ spec: periodSeconds: 5 failureThreshold: 3 successThreshold: 1 - timeoutSeconds: 3 + timeoutSeconds: 5 volumeMounts: - mountPath: "/workspace" name: pds-workspace diff --git a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/batch/PDSBatchTriggerService.java b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/batch/PDSBatchTriggerService.java index 5fa5765a36..6485696c34 100644 --- a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/batch/PDSBatchTriggerService.java +++ b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/batch/PDSBatchTriggerService.java @@ -16,6 +16,8 @@ import com.mercedesbenz.sechub.pds.execution.PDSExecutionService; import com.mercedesbenz.sechub.pds.job.PDSJobRepository; import com.mercedesbenz.sechub.pds.job.PDSJobTransactionService; +import com.mercedesbenz.sechub.pds.usecase.PDSStep; +import com.mercedesbenz.sechub.pds.usecase.UseCaseSystemExecutesJob; import jakarta.annotation.PostConstruct; @@ -59,6 +61,7 @@ protected void postConstruct() { // default 10 seconds delay and 5 seconds initial @Scheduled(initialDelayString = "${pds.config.trigger.nextjob.initialdelay:" + DEFAULT_INITIAL_DELAY_MILLIS + "}", fixedDelayString = "${pds.config.trigger.nextjob.delay:" + DEFAULT_FIXED_DELAY_MILLIS + "}") + @UseCaseSystemExecutesJob(@PDSStep(number = 1, name = "PDS trigger service fills queue", description = "Trigger service adds jobs to queue (when queue not full)")) public void triggerExecutionOfNextJob() { if (!schedulingEnabled) { LOG.trace("Trigger execution of next job canceled, because scheduling disabled."); @@ -86,8 +89,7 @@ public void triggerExecutionOfNextJob() { } catch (ObjectOptimisticLockingFailureException e) { /* * This can happen when PDS instances are started at same time, so the check for - * next jobs can lead to race condiitons - and optmistic locks will occurre - * here. + * next jobs can lead to race conditions - and optimistic locks will occur here. * * To avoid this to happen again, we wait a random time here. So next call on * this machine should normally not collide again. diff --git a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/execution/PDSExecutionCallable.java b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/execution/PDSExecutionCallable.java index 6dfb1085f2..61bac36ec3 100644 --- a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/execution/PDSExecutionCallable.java +++ b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/execution/PDSExecutionCallable.java @@ -39,6 +39,7 @@ import com.mercedesbenz.sechub.pds.usecase.UseCaseAdminFetchesJobErrorStream; import com.mercedesbenz.sechub.pds.usecase.UseCaseAdminFetchesJobMetaData; import com.mercedesbenz.sechub.pds.usecase.UseCaseAdminFetchesJobOutputStream; +import com.mercedesbenz.sechub.pds.usecase.UseCaseSystemExecutesJob; import com.mercedesbenz.sechub.pds.usecase.UseCaseSystemHandlesJobCancelRequests; import com.mercedesbenz.sechub.pds.util.PDSResilientRetryExecutor; import com.mercedesbenz.sechub.pds.util.PDSResilientRetryExecutor.ExceptionThrower; @@ -91,6 +92,7 @@ public void throwException(String message, Exception cause) throws IllegalStateE } @Override + @UseCaseSystemExecutesJob(@PDSStep(number = 3, name = "PDS execution call", description = "Central point of PDS job execution.")) public PDSExecutionResult call() throws Exception { LOG.info("Prepare execution of PDS job: {}", pdsJobUUID); PDSExecutionResult result = new PDSExecutionResult(); diff --git a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/execution/PDSExecutionEnvironmentService.java b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/execution/PDSExecutionEnvironmentService.java index 27fdcaa7eb..3896d68bcc 100644 --- a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/execution/PDSExecutionEnvironmentService.java +++ b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/execution/PDSExecutionEnvironmentService.java @@ -105,6 +105,10 @@ private void addPdsJobRelatedVariables(UUID pdsJobUUID, Map map) map.put(PDS_JOB_SOURCECODE_ZIP_FILE, locationData.getSourceCodeZipFileLocation()); map.put(PDS_JOB_BINARIES_TAR_FILE, locationData.getBinariesTarFileLocation()); + String extractedAssetsLocation = locationData.getExtractedAssetsLocation(); + + map.put(PDS_JOB_EXTRACTED_ASSETS_FOLDER, extractedAssetsLocation); + String extractedSourcesLocation = locationData.getExtractedSourcesLocation(); map.put(PDS_JOB_SOURCECODE_UNZIPPED_FOLDER, extractedSourcesLocation); diff --git a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/execution/PDSExecutionService.java b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/execution/PDSExecutionService.java index 71131c1981..28f57eb91e 100644 --- a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/execution/PDSExecutionService.java +++ b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/execution/PDSExecutionService.java @@ -37,6 +37,7 @@ import com.mercedesbenz.sechub.pds.job.PDSWorkspaceService; import com.mercedesbenz.sechub.pds.usecase.PDSStep; import com.mercedesbenz.sechub.pds.usecase.UseCaseAdminFetchesMonitoringStatus; +import com.mercedesbenz.sechub.pds.usecase.UseCaseSystemExecutesJob; import com.mercedesbenz.sechub.pds.usecase.UseCaseSystemHandlesJobCancelRequests; import com.mercedesbenz.sechub.pds.usecase.UseCaseSystemSigTermHandling; @@ -205,6 +206,7 @@ public boolean isQueueFull() { } @Async + @UseCaseSystemExecutesJob(@PDSStep(number = 2, name = "Add Job to queue", description = "PDS job is added to queue")) public void addToExecutionQueueAsynchron(UUID jobUUID) { Future former = null; synchronized (jobsInQueue) { diff --git a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/job/PDSWorkspaceService.java b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/job/PDSWorkspaceService.java index 329354db30..6fc14bbdfd 100644 --- a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/job/PDSWorkspaceService.java +++ b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/job/PDSWorkspaceService.java @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT package com.mercedesbenz.sechub.pds.job; -import static com.mercedesbenz.sechub.commons.core.CommonConstants.FILENAME_BINARIES_TAR; -import static com.mercedesbenz.sechub.commons.core.CommonConstants.FILENAME_SOURCECODE_ZIP; +import static com.mercedesbenz.sechub.commons.core.CommonConstants.*; +import static java.util.Objects.*; import java.io.File; import java.io.FileFilter; @@ -15,6 +15,7 @@ import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; import java.time.Duration; +import java.util.List; import java.util.Set; import java.util.UUID; @@ -34,10 +35,14 @@ import com.mercedesbenz.sechub.commons.archive.ArchiveSupport.ArchiveType; import com.mercedesbenz.sechub.commons.archive.FileSize; import com.mercedesbenz.sechub.commons.archive.SecHubFileStructureDataProvider; +import com.mercedesbenz.sechub.commons.core.security.CheckSumSupport; import com.mercedesbenz.sechub.commons.model.JSONConverter; import com.mercedesbenz.sechub.commons.model.JSONConverterException; import com.mercedesbenz.sechub.commons.model.ScanType; import com.mercedesbenz.sechub.commons.model.SecHubConfigurationModel; +import com.mercedesbenz.sechub.commons.model.template.TemplateType; +import com.mercedesbenz.sechub.commons.pds.data.PDSTemplateMetaData; +import com.mercedesbenz.sechub.commons.pds.data.PDSTemplateMetaData.PDSAssetData; import com.mercedesbenz.sechub.commons.pds.execution.ExecutionEventData; import com.mercedesbenz.sechub.commons.pds.execution.ExecutionEventDetailIdentifier; import com.mercedesbenz.sechub.commons.pds.execution.ExecutionEventType; @@ -47,18 +52,24 @@ import com.mercedesbenz.sechub.pds.config.PDSServerConfigurationService; import com.mercedesbenz.sechub.pds.storage.PDSMultiStorageService; import com.mercedesbenz.sechub.pds.storage.PDSStorageInfoCollector; +import com.mercedesbenz.sechub.pds.usecase.PDSStep; +import com.mercedesbenz.sechub.pds.usecase.UseCaseSystemExecutesJob; import com.mercedesbenz.sechub.pds.util.PDSArchiveSupportProvider; import com.mercedesbenz.sechub.pds.util.PDSResilientRetryExecutor; import com.mercedesbenz.sechub.pds.util.PDSResilientRetryExecutor.ExceptionThrower; +import com.mercedesbenz.sechub.storage.core.AssetStorage; import com.mercedesbenz.sechub.storage.core.JobStorage; +import com.mercedesbenz.sechub.storage.core.Storage; @Service public class PDSWorkspaceService { public static final String UPLOAD = "upload"; + public static final String ASSETS = "assets"; public static final String EXTRACTED = "extracted"; public static final String EXTRACTED_SOURCES = EXTRACTED + "/sources"; public static final String EXTRACTED_BINARIES = EXTRACTED + "/binaries"; + public static final String EXTRACTED_ASSETS = EXTRACTED + "/assets"; public static final String OUTPUT = "output"; public static final String MESSAGES = "messages"; @@ -120,6 +131,9 @@ public class PDSWorkspaceService { @Autowired PDSWorkspacePreparationResultCalculator preparationResultCalculator; + @Autowired + CheckSumSupport checksumSupport; + private static final ArchiveFilter TAR_FILE_FILTER = new TarFileFilter(); private static final ArchiveFilter ZIP_FILE_FILTER = new SourcecodeZipFileFilter(); @@ -151,6 +165,7 @@ public void throwException(String message, Exception cause) throws IOException { * @return {@link PDSWorkspacePreparationResult}, never null * @throws IOException */ + @UseCaseSystemExecutesJob(@PDSStep(number = 4, name = "PDS workspace preparation", description = "PDS job workspace is prepared here: Directories are created, files are downloaded and extracted etc.")) public PDSWorkspacePreparationResult prepare(UUID pdsJobUUID, PDSJobConfiguration config, String metaData) throws IOException { PDSJobConfigurationSupport configurationSupport = new PDSJobConfigurationSupport(config); @@ -162,6 +177,8 @@ public PDSWorkspacePreparationResult prepare(UUID pdsJobUUID, PDSJobConfiguratio writeMetaData(pdsJobUUID, metaData); + importAndExtractFilesFromAssetStorage(pdsJobUUID, config, configurationSupport, preparationContext); + importWantedFilesFromJobStorage(pdsJobUUID, config, configurationSupport, preparationContext); extractZipFileUploadsWhenConfigured(pdsJobUUID, config, preparationContext); @@ -188,7 +205,7 @@ private void importWantedFilesFromJobStorage(UUID pdsJobUUID, PDSJobConfiguratio PDSResilientRetryExecutor resilientStorageReadExecutor = createResilientReadExecutor(preparationContext); File jobFolder = getUploadFolder(pdsJobUUID); - JobStorage storage = fetchStorage(pdsJobUUID, config); + JobStorage storage = fetchJobStorage(pdsJobUUID, config); Set names = resilientStorageReadExecutor.execute(() -> storage.listNames(), "List storage names for job: " + pdsJobUUID.toString()); @@ -209,6 +226,79 @@ private void importWantedFilesFromJobStorage(UUID pdsJobUUID, PDSJobConfiguratio } finally { storage.close(); } + + } + + private void importAndExtractFilesFromAssetStorage(UUID pdsJobUUID, PDSJobConfiguration config, PDSJobConfigurationSupport configurationSupport, + PDSWorkspacePreparationContext preparationContext) throws IOException { + + List templateMetaDataList = configurationSupport.getTemplateMetaData(); + if (templateMetaDataList.isEmpty()) { + LOG.debug("No template meta data available - will skipp asset data import"); + return; + } + + PDSResilientRetryExecutor resilientStorageReadExecutor = createResilientReadExecutor(preparationContext); + + for (PDSTemplateMetaData metaData : templateMetaDataList) { + TemplateType templateType = requireNonNull(metaData.getTemplateType(), "Template type may not be null"); + PDSAssetData assetData = requireNonNull(metaData.getAssetData(), "Asset data may not be null"); + String fileName = requireNonBlank(assetData.getFileName(), "Filename must be defined"); + String assetId = requireNonBlank(assetData.getAssetId(), "Asset id must be defined"); + String checksumFromSecHub = requireNonBlank(assetData.getChecksum(), "Checksum must be defined."); + + File assetDownloadFile = getAssetFileFromUpload(pdsJobUUID, assetId, fileName); + String assetFilePath = assetId + "/" + fileName; + + resilientStorageReadExecutor.execute(() -> { + + try (AssetStorage storage = storageService.createAssetStorage(assetId)) { + InputStream storageStream = storage.fetch(fileName); + readAndCopyInputStreamToFileSystem(pdsJobUUID, fileName, assetDownloadFile, storageStream); + + String checksumAfterDownloadFromStorage = checksumSupport.createSha256Checksum(assetDownloadFile.toPath()); + + if (!checksumAfterDownloadFromStorage.equals(checksumFromSecHub)) { + throw new IOException("Checksum not as expected for asset file:" + assetFilePath + "\nSecHub checksum:" + checksumFromSecHub + + ", Download checksum:" + checksumAfterDownloadFromStorage); + } + } + }, "Read and copy asset file: " + assetFilePath + " for job: " + pdsJobUUID); + + File extractionFolder = getAssetExtractionFolder(pdsJobUUID, templateType); + if (fileName.toLowerCase().endsWith(".zip")) { + LOG.info("Start extraction of asset file '{}'", fileName); + /* extract ZIP file */ + ArchiveExtractionConstraints archiveExtractionConstraints = createExtractionContraints(); + archiveSupportProvider.getArchiveSupport().extractFileAsIsToFolder(ArchiveType.ZIP, assetDownloadFile, extractionFolder, + archiveExtractionConstraints); + } else { + /* + * This case will only happen in PDS solution development: + * + * At development time, it can happen that there is a need to directly upload + * and change asset files in storage to make things easier/faster turn around + * times. + * + * But SecHub will always use "$pdsProductIdentifier.zip" as the asset file name + * for the product (if there exists one in storage) and send this inside + * parameters! + */ + LOG.warn( + "Asset file name '{}' does not end with '.zip' - will just copy the file to extraction folder. This may ONLY happen at PDS solution development time, but not in production by SecHub!", + fileName); + FileUtils.copyFile(assetDownloadFile, new File(extractionFolder, fileName)); + } + + } + + } + + private static final String requireNonBlank(String target, String message) { + if (requireNonNull(target, message).isBlank()) { + throw new IllegalStateException(message); + } + return target; } private PDSResilientRetryExecutor createResilientReadExecutor(PDSWorkspacePreparationContext preparationContext) { @@ -220,30 +310,34 @@ private PDSResilientRetryExecutor createResilientReadExecutor(PDSWo return resilientExecutor; } - private void readAndCopyStorageToFileSystem(UUID jobUUID, File jobFolder, JobStorage storage, String name) throws IOException { + private void readAndCopyStorageToFileSystem(UUID jobUUID, File jobFolder, Storage storage, String fileName) throws IOException { - File uploadFile = new File(jobFolder, name); + File uploadFile = new File(jobFolder, fileName); - try (InputStream fetchedInputStream = storage.fetch(name)) { + try (InputStream fetchedInputStream = storage.fetch(fileName)) { - try { + readAndCopyInputStreamToFileSystem(jobUUID, fileName, uploadFile, fetchedInputStream); + } - FileUtils.copyInputStreamToFile(fetchedInputStream, uploadFile); + } - LOG.debug("Imported '{}' for job {} from storage to {}", name, jobUUID, uploadFile.getAbsolutePath()); + private void readAndCopyInputStreamToFileSystem(UUID jobUUID, String fileName, File uploadFile, InputStream fetchedInputStream) throws IOException { + try { - } catch (IOException e) { + FileUtils.copyInputStreamToFile(fetchedInputStream, uploadFile); - LOG.error("Was not able to copy stream of uploaded file: {} for job {}, reason: ", name, jobUUID, e.getMessage()); + LOG.debug("Imported '{}' for job {} from storage to {}", fileName, jobUUID, uploadFile.getAbsolutePath()); - if (uploadFile.exists()) { - boolean deleteSuccessful = uploadFile.delete(); - LOG.info("Uploaded file existed. Deleted successfully: {}", deleteSuccessful); - } - throw e; + } catch (IOException e) { + + LOG.error("Was not able to copy stream of uploaded file: {} for job {}, reason: ", fileName, jobUUID, e.getMessage()); + + if (uploadFile.exists()) { + boolean deleteSuccessful = uploadFile.delete(); + LOG.info("Uploaded file existed. Deleted successfully: {}", deleteSuccessful); } + throw e; } - } void extractZipFileUploadsWhenConfigured(UUID jobUUID, PDSJobConfiguration config, PDSWorkspacePreparationContext preparationContext) throws IOException { @@ -288,7 +382,7 @@ private boolean isWantedStorageContent(String name, PDSJobConfigurationSupport c return false; } - private JobStorage fetchStorage(UUID pdsJobUUID, PDSJobConfiguration config) { + private JobStorage fetchJobStorage(UUID pdsJobUUID, PDSJobConfiguration config) { UUID pdsOrSecHubJobUUID; String storagePath; @@ -325,6 +419,41 @@ public File getUploadFolder(UUID pdsJobUUID) { return file; } + /** + * Resolves assets folder - if not existing it will be created + * + * @param pdsJobUUID + * @return assets folder + */ + private Path getAssetUploadFolder(UUID pdsJobUUID) { + File file = new File(getUploadFolder(pdsJobUUID), ASSETS); + file.mkdirs(); + return file.toPath(); + } + + /** + * Resolves asset extraction folder - if not existing it will be created + * + * @param pdsJobUUID + * @param templateType + * @return asset extraction folder + */ + public File getAssetExtractionFolder(UUID pdsJobUUID, TemplateType templateType) { + Path parent = getUploadFolder(pdsJobUUID).toPath(); + Path assetsExtractedPath = parent.resolve(EXTRACTED_ASSETS); + Path templateTypeExtractedPath = assetsExtractedPath.resolve(templateType.getId()); + File file = templateTypeExtractedPath.toFile(); + file.mkdirs(); + return file; + } + + private File getAssetFileFromUpload(UUID pdsJobUUID, String assetId, String fileName) { + Path parent = getAssetUploadFolder(pdsJobUUID); + Path assetIdDownloadPath = parent.resolve(assetId); + assetIdDownloadPath.toFile().mkdirs(); + return assetIdDownloadPath.resolve(fileName).toFile(); + } + private Path getWorkspaceFolderPath(UUID jobUUID) { File workspaceFolder = getWorkspaceFolder(jobUUID); Path workspaceFolderPath = workspaceFolder.toPath(); @@ -418,8 +547,7 @@ private boolean extractArchives(UUID pdsJobUUID, boolean deleteOriginFiles, SecH } ArchiveSupport archiveSupport = archiveSupportProvider.getArchiveSupport(); - ArchiveExtractionConstraints archiveExtractionConstraints = new ArchiveExtractionConstraints(archiveExtractionMaxFileSizeUncompressed, - archiveExtractionMaxEntries, archiveExtractionMaxDirectoryDepth, archiveExtractionTimeout); + ArchiveExtractionConstraints archiveExtractionConstraints = createExtractionContraints(); for (File archiveFile : archiveFiles) { try (FileInputStream archiveFileInputStream = new FileInputStream(archiveFile)) { @@ -443,6 +571,12 @@ private boolean extractArchives(UUID pdsJobUUID, boolean deleteOriginFiles, SecH return true; } + private ArchiveExtractionConstraints createExtractionContraints() { + ArchiveExtractionConstraints archiveExtractionConstraints = new ArchiveExtractionConstraints(archiveExtractionMaxFileSizeUncompressed, + archiveExtractionMaxEntries, archiveExtractionMaxDirectoryDepth, archiveExtractionTimeout); + return archiveExtractionConstraints; + } + public void cleanup(UUID jobUUID, PDSJobConfiguration config) throws IOException { FileUtils.deleteDirectory(getWorkspaceFolder(jobUUID)); LOG.info("Removed workspace folder for job {}", jobUUID); @@ -453,7 +587,7 @@ public void cleanup(UUID jobUUID, PDSJobConfiguration config) throws IOException LOG.info("Removed NOT storage for PDS job {} because sechub storage and will be handled by sechub job {}", jobUUID, config.getSechubJobUUID()); } else { - JobStorage storage = fetchStorage(jobUUID, config); + JobStorage storage = fetchJobStorage(jobUUID, config); storage.deleteAll(); LOG.info("Removed storage for job {}", jobUUID); storage.close(); @@ -549,6 +683,7 @@ public WorkspaceLocationData createLocationData(UUID jobUUID) { locationData.extractedSourcesLocation = createExtractedSourcesLocation(workspaceFolderPath).toString(); locationData.extractedBinariesLocation = createExtractedBinariesLocation(workspaceFolderPath).toString(); + locationData.extractedAssetsLocation = createExtractedAssetsLocation(workspaceFolderPath).toString(); locationData.sourceCodeZipFileLocation = createSourceCodeZipFileLocation(workspaceFolderPath).toString(); locationData.binariesTarFileLocation = createBinariesTarFileLocation(workspaceFolderPath).toString(); @@ -679,6 +814,10 @@ private Path createExtractedSourcesLocation(Path workspaceFolderPath) { return createWorkspacePathAndEnsureParentDirectories(workspaceFolderPath, UPLOAD + "/" + EXTRACTED_SOURCES); } + private Path createExtractedAssetsLocation(Path workspaceFolderPath) { + return createWorkspacePathAndEnsureParentDirectories(workspaceFolderPath, UPLOAD + "/" + EXTRACTED_ASSETS); + } + private Path createWorkspacePathAndEnsureDirectory(Path workspaceLocation, String subPath) { Path path = createWorkspacePathAndEnsureParentDirectories(workspaceLocation, subPath); if (!Files.exists(path)) { diff --git a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/job/WorkspaceLocationData.java b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/job/WorkspaceLocationData.java index 6ee704ba75..36a386f261 100644 --- a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/job/WorkspaceLocationData.java +++ b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/job/WorkspaceLocationData.java @@ -11,6 +11,7 @@ public class WorkspaceLocationData { String userMessagesLocation; String metaDataFileLocation; String eventsLocation; + String extractedAssetsLocation; public String getWorkspaceLocation() { return workspaceLocation; @@ -47,4 +48,8 @@ public String getMetaDataFileLocation() { public String getEventsLocation() { return eventsLocation; } + + public String getExtractedAssetsLocation() { + return extractedAssetsLocation; + } } diff --git a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/storage/PDSMultiStorageService.java b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/storage/PDSMultiStorageService.java index 9ed628b700..efdbd681ef 100644 --- a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/storage/PDSMultiStorageService.java +++ b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/storage/PDSMultiStorageService.java @@ -9,6 +9,8 @@ import org.springframework.stereotype.Service; import com.mercedesbenz.sechub.pds.config.PDSServerConfigurationService; +import com.mercedesbenz.sechub.storage.core.AssetStorage; +import com.mercedesbenz.sechub.storage.core.AssetStorageFactory; import com.mercedesbenz.sechub.storage.core.JobStorage; import com.mercedesbenz.sechub.storage.core.JobStorageFactory; import com.mercedesbenz.sechub.storage.core.S3Setup; @@ -34,22 +36,30 @@ public class PDSMultiStorageService implements StorageService { private static final Logger LOG = LoggerFactory.getLogger(PDSMultiStorageService.class); private JobStorageFactory jobStorageFactory; + private AssetStorageFactory assetStorageFactory; @Autowired public PDSMultiStorageService(SharedVolumeSetup sharedVolumeSetup, S3Setup s3Setup) { if (s3Setup.isAvailable()) { - jobStorageFactory = new AwsS3JobStorageFactory(s3Setup); + AwsS3JobStorageFactory awsJobFactory = new AwsS3JobStorageFactory(s3Setup); + + jobStorageFactory = awsJobFactory; + assetStorageFactory = awsJobFactory; } else if (sharedVolumeSetup.isAvailable()) { - jobStorageFactory = new SharedVolumeJobStorageFactory(sharedVolumeSetup); + SharedVolumeJobStorageFactory sharedVolumeStorageFactory = new SharedVolumeJobStorageFactory(sharedVolumeSetup); + + jobStorageFactory = sharedVolumeStorageFactory; + assetStorageFactory = sharedVolumeStorageFactory; } - if (jobStorageFactory == null) { + if (jobStorageFactory == null || assetStorageFactory == null) { throw new IllegalStateException("Did not found any available storage setup! At least one must be set!"); } LOG.info("Created storage factory: {}", jobStorageFactory.getClass().getSimpleName()); + LOG.info("Created asset storage factory: {}", assetStorageFactory.getClass().getSimpleName()); } @@ -64,4 +74,9 @@ public JobStorage createJobStorageForPath(String storagePath, UUID jobUUID) { return jobStorage; } + @Override + public AssetStorage createAssetStorage(String assetId) { + return assetStorageFactory.createAssetStorage(assetId); + } + } \ No newline at end of file diff --git a/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/job/PDSWorkspaceServiceTest.java b/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/job/PDSWorkspaceServiceTest.java index d7791fd554..220567d65f 100644 --- a/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/job/PDSWorkspaceServiceTest.java +++ b/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/job/PDSWorkspaceServiceTest.java @@ -3,27 +3,37 @@ import static com.mercedesbenz.sechub.test.TestConstants.*; import static java.io.File.*; -import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Collections; +import java.util.List; import java.util.UUID; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import com.mercedesbenz.sechub.commons.core.security.CheckSumSupport; +import com.mercedesbenz.sechub.commons.model.JSONConverter; +import com.mercedesbenz.sechub.commons.model.template.TemplateType; import com.mercedesbenz.sechub.commons.pds.PDSDefaultParameterKeyConstants; +import com.mercedesbenz.sechub.commons.pds.data.PDSTemplateMetaData; +import com.mercedesbenz.sechub.commons.pds.data.PDSTemplateMetaData.PDSAssetData; import com.mercedesbenz.sechub.pds.commons.core.config.PDSProductSetup; import com.mercedesbenz.sechub.pds.config.PDSServerConfigurationService; import com.mercedesbenz.sechub.pds.execution.PDSExecutionParameterEntry; import com.mercedesbenz.sechub.pds.storage.PDSMultiStorageService; import com.mercedesbenz.sechub.pds.storage.PDSStorageInfoCollector; +import com.mercedesbenz.sechub.storage.core.AssetStorage; import com.mercedesbenz.sechub.storage.core.JobStorage; import com.mercedesbenz.sechub.test.TestFileReader; import com.mercedesbenz.sechub.test.TestUtil; @@ -41,6 +51,7 @@ class PDSWorkspaceServiceTest { private PDSWorkspacePreparationResultCalculator preparationResultCalculator; private PDSWorkspacePreparationContext preparationContext; private UUID jobUUID; + private CheckSumSupport checksumSupport; @BeforeAll static void beforeAll() throws IOException { @@ -51,14 +62,15 @@ static void beforeAll() throws IOException { void beforeEach() { jobUUID = UUID.randomUUID(); - storageService = mock(PDSMultiStorageService.class); - storage = mock(JobStorage.class); - storageInfoCollector = mock(PDSStorageInfoCollector.class); - preparationContextFactory = mock(PDSWorkspacePreparationContextFactory.class); - serverConfigService = mock(PDSServerConfigurationService.class); - preparationResultCalculator = mock(PDSWorkspacePreparationResultCalculator.class); + storageService = mock(); + storage = mock(); + storageInfoCollector = mock(); + preparationContextFactory = mock(); + serverConfigService = mock(); + preparationResultCalculator = mock(); + checksumSupport = mock(); - preparationContext = mock(PDSWorkspacePreparationContext.class); + preparationContext = mock(); when(preparationContextFactory.createPreparationContext(any())).thenReturn(preparationContext); PDSProductSetup setup = new PDSProductSetup(); @@ -73,6 +85,7 @@ void beforeEach() { serviceToTest.preparationContextFactory = preparationContextFactory; serviceToTest.serverConfigService = serverConfigService; serviceToTest.preparationResultCalculator = preparationResultCalculator; + serviceToTest.checksumSupport = checksumSupport; config = new PDSJobConfiguration(); @@ -145,7 +158,7 @@ void prepare_returns_result_from_calculator() throws Exception { PDSWorkspacePreparationResult result = serviceToTest.prepare(jobUUID, config, null); /* test */ - assertSame(expected, result); + assertThat(expected).isSameAs(result); } @Test @@ -156,7 +169,7 @@ void when_job_has_no_metadata_no_metadata_file_is_created_in_workspace() throws /* test */ File metaDataFile = serviceToTest.getMetaDataFile(jobUUID); - assertFalse(metaDataFile.exists()); + assertThat(metaDataFile.exists()).isFalse(); } @@ -168,8 +181,8 @@ void when_job_has_metadata_a_metadata_file_is_created_in_workspace_containing_co /* test */ File metaDataFile = serviceToTest.getMetaDataFile(jobUUID); - assertTrue(metaDataFile.exists()); - assertEquals("this is my metadata", TestFileReader.readTextFromFile(metaDataFile)); + assertThat(metaDataFile.exists()).isTrue(); + assertThat(TestFileReader.readTextFromFile(metaDataFile)).isEqualTo("this is my metadata"); } @Test @@ -182,14 +195,15 @@ void createLocationData_contains_expected_pathes_when_using_temp_directory_as_up String expectedWorspaceLocation = workspaceRootFolderPath + separatorChar + jobUUID; /* @formatter:off */ - assertEquals(expectedWorspaceLocation,result.getWorkspaceLocation()); - assertEquals(expectedWorspaceLocation+separatorChar+"output"+separatorChar+"result.txt",result.getResultFileLocation()); - assertEquals(expectedWorspaceLocation+separatorChar+"output"+separatorChar+"messages",result.getUserMessagesLocation()); - assertEquals(expectedWorspaceLocation+separatorChar+"metadata.txt",result.getMetaDataFileLocation()); - assertEquals(expectedWorspaceLocation+separatorChar+"upload"+separatorChar+SOURCECODE_ZIP,result.getSourceCodeZipFileLocation()); - assertEquals(expectedWorspaceLocation+separatorChar+"upload"+separatorChar+"extracted"+separatorChar+"sources",result.getExtractedSourcesLocation()); - assertEquals(expectedWorspaceLocation+separatorChar+"upload"+separatorChar+"extracted"+separatorChar+"binaries",result.getExtractedBinariesLocation()); - assertEquals(expectedWorspaceLocation+separatorChar+"events",result.getEventsLocation()); + assertThat(result.getWorkspaceLocation()).isEqualTo(expectedWorspaceLocation); + assertThat(result.getResultFileLocation()).isEqualTo(expectedWorspaceLocation+separatorChar+"output"+separatorChar+"result.txt"); + + assertThat(result.getUserMessagesLocation()).isEqualTo(expectedWorspaceLocation+separatorChar+"output"+separatorChar+"messages"); + assertThat(result.getMetaDataFileLocation()).isEqualTo(expectedWorspaceLocation+separatorChar+"metadata.txt"); + assertThat(result.getSourceCodeZipFileLocation()).isEqualTo(expectedWorspaceLocation+separatorChar+"upload"+separatorChar+SOURCECODE_ZIP); + assertThat(result.getExtractedSourcesLocation()).isEqualTo(expectedWorspaceLocation+separatorChar+"upload"+separatorChar+"extracted"+separatorChar+"sources"); + assertThat(result.getExtractedBinariesLocation()).isEqualTo(expectedWorspaceLocation+separatorChar+"upload"+separatorChar+"extracted"+separatorChar+"binaries"); + assertThat(result.getEventsLocation()).isEqualTo(expectedWorspaceLocation+separatorChar+"events"); /* @formatter:on */ } @@ -209,6 +223,69 @@ void when_configuration_tells_to_use_sechubstorage_sechub_storage_path_and_sechu verify(storageService).createJobStorageForPath("xyz/abc/project1", config.getSechubJobUUID()); } + @Test + void prepare_downloads_asset_and_stores_file_locally_when_parameter_contains_pds_template_metadata_no_checksum_failure() throws Exception { + /* prepare */ + PDSTemplateMetaData metaData = new PDSTemplateMetaData(); + metaData.setTemplateId("template1"); + metaData.setTemplateType(TemplateType.WEBSCAN_LOGIN); + PDSAssetData assetData = new PDSAssetData(); + assetData.setAssetId("asset1"); + assetData.setChecksum("checksum1"); + assetData.setFileName("file1.txt"); + metaData.setAssetData(assetData); + + String json = JSONConverter.get().toJSON(metaData, false); + config.getParameters().add(createEntry(PDSDefaultParameterKeyConstants.PARAM_KEY_PDS_CONFIG_TEMPLATE_META_DATA_LIST, json)); + + AssetStorage assetStorage = mock(); + when(storageService.createAssetStorage("asset1")).thenReturn(assetStorage); + when(assetStorage.fetch("file1.txt")).thenReturn(new ByteArrayInputStream("testdata".getBytes())); + when(checksumSupport.createSha256Checksum(any(Path.class))).thenReturn("checksum1"); + + /* execute */ + serviceToTest.prepare(jobUUID, config, null); + + /* test */ + ArgumentCaptor pathCaptor = ArgumentCaptor.captor(); + verify(storageService).createAssetStorage("asset1"); + verify(checksumSupport).createSha256Checksum(pathCaptor.capture()); + Path path = pathCaptor.getValue(); + + assertThat(path.getFileName().toString()).isEqualTo("file1.txt"); + + // check file is created + assertThat(Files.exists(path)).isTrue(); + List lines = Files.readAllLines(path); + assertThat(lines).contains("testdata").hasSize(1); + } + + @Test + void prepare_downloads_asset_and_stores_file_locally_when_parameter_contains_pds_template_metadata_checksum_failure() throws Exception { + /* prepare */ + PDSTemplateMetaData metaData = new PDSTemplateMetaData(); + metaData.setTemplateId("template1"); + metaData.setTemplateType(TemplateType.WEBSCAN_LOGIN); + PDSAssetData assetData = new PDSAssetData(); + assetData.setAssetId("asset1"); + assetData.setChecksum("checksum1"); + assetData.setFileName("file1.txt"); + metaData.setAssetData(assetData); + + String json = JSONConverter.get().toJSON(metaData, false); + config.getParameters().add(createEntry(PDSDefaultParameterKeyConstants.PARAM_KEY_PDS_CONFIG_TEMPLATE_META_DATA_LIST, json)); + + AssetStorage assetStorage = mock(); + when(storageService.createAssetStorage("asset1")).thenReturn(assetStorage); + when(assetStorage.fetch("file1.txt")).thenReturn(new ByteArrayInputStream("testdata".getBytes())); + when(checksumSupport.createSha256Checksum(any(Path.class))).thenReturn("checksum-other-means-failure"); + + /* execute + test */ + assertThatThrownBy(() -> serviceToTest.prepare(jobUUID, config, null)).cause().isInstanceOf(IOException.class) + .hasMessageStartingWith("Checksum not as expected"); + + } + private PDSExecutionParameterEntry createEntry(String key, String value) { PDSExecutionParameterEntry entry = new PDSExecutionParameterEntry(); entry.setKey(key); diff --git a/sechub-scan-product-checkmarx/src/main/java/com/mercedesbenz/sechub/domain/scan/product/checkmarx/CheckmarxExecutorConfigSuppport.java b/sechub-scan-product-checkmarx/src/main/java/com/mercedesbenz/sechub/domain/scan/product/checkmarx/CheckmarxExecutorConfigSuppport.java index 26cf7301f8..066af81fc3 100644 --- a/sechub-scan-product-checkmarx/src/main/java/com/mercedesbenz/sechub/domain/scan/product/checkmarx/CheckmarxExecutorConfigSuppport.java +++ b/sechub-scan-product-checkmarx/src/main/java/com/mercedesbenz/sechub/domain/scan/product/checkmarx/CheckmarxExecutorConfigSuppport.java @@ -9,6 +9,7 @@ import com.mercedesbenz.sechub.commons.core.environment.SystemEnvironmentVariableSupport; import com.mercedesbenz.sechub.commons.core.util.SimpleStringUtils; import com.mercedesbenz.sechub.commons.model.SecHubRuntimeException; +import com.mercedesbenz.sechub.domain.scan.product.ProductExecutorContext; import com.mercedesbenz.sechub.domain.scan.product.config.ProductExecutorConfig; import com.mercedesbenz.sechub.sharedkernel.error.NotAcceptableException; import com.mercedesbenz.sechub.sharedkernel.mapping.MappingIdentifier; @@ -27,14 +28,14 @@ public class CheckmarxExecutorConfigSuppport extends DefaultExecutorConfigSuppor * @return support * @throws NotAcceptableException when configuration is not valid */ - public static CheckmarxExecutorConfigSuppport createSupportAndAssertConfigValid(ProductExecutorConfig config, + public static CheckmarxExecutorConfigSuppport createSupportAndAssertConfigValid(ProductExecutorContext context, SystemEnvironmentVariableSupport variableSupport) { - return new CheckmarxExecutorConfigSuppport(config, variableSupport, new CheckmarxProductExecutorMinimumConfigValidation()); + return new CheckmarxExecutorConfigSuppport(context, variableSupport, new CheckmarxProductExecutorMinimumConfigValidation()); } - private CheckmarxExecutorConfigSuppport(ProductExecutorConfig config, SystemEnvironmentVariableSupport variableSupport, + private CheckmarxExecutorConfigSuppport(ProductExecutorContext context, SystemEnvironmentVariableSupport variableSupport, Validation validation) { - super(config, variableSupport, validation); + super(context, variableSupport, validation); } public boolean isAlwaysFullScanEnabled() { diff --git a/sechub-scan-product-checkmarx/src/main/java/com/mercedesbenz/sechub/domain/scan/product/checkmarx/CheckmarxProductExecutor.java b/sechub-scan-product-checkmarx/src/main/java/com/mercedesbenz/sechub/domain/scan/product/checkmarx/CheckmarxProductExecutor.java index b17cc43bc0..d1c640f636 100644 --- a/sechub-scan-product-checkmarx/src/main/java/com/mercedesbenz/sechub/domain/scan/product/checkmarx/CheckmarxProductExecutor.java +++ b/sechub-scan-product-checkmarx/src/main/java/com/mercedesbenz/sechub/domain/scan/product/checkmarx/CheckmarxProductExecutor.java @@ -109,8 +109,8 @@ protected List executeByAdapter(ProductExecutorData data) throws JobStorage storage = storageService.createJobStorageForProject(projectId, jobUUID); - CheckmarxExecutorConfigSuppport configSupport = CheckmarxExecutorConfigSuppport - .createSupportAndAssertConfigValid(data.getProductExecutorContext().getExecutorConfig(), systemEnvironmentVariableSupport); + CheckmarxExecutorConfigSuppport configSupport = CheckmarxExecutorConfigSuppport.createSupportAndAssertConfigValid(data.getProductExecutorContext(), + systemEnvironmentVariableSupport); AdapterMetaDataCallback metaDataCallback = data.getProductExecutorContext().getCallback(); diff --git a/sechub-scan-product-checkmarx/src/test/java/com/mercedesbenz/sechub/domain/scan/product/checkmarx/CheckmarxExecutorConfigSuppportTest.java b/sechub-scan-product-checkmarx/src/test/java/com/mercedesbenz/sechub/domain/scan/product/checkmarx/CheckmarxExecutorConfigSuppportTest.java index 7fee8f00c9..01b4215396 100644 --- a/sechub-scan-product-checkmarx/src/test/java/com/mercedesbenz/sechub/domain/scan/product/checkmarx/CheckmarxExecutorConfigSuppportTest.java +++ b/sechub-scan-product-checkmarx/src/test/java/com/mercedesbenz/sechub/domain/scan/product/checkmarx/CheckmarxExecutorConfigSuppportTest.java @@ -12,6 +12,7 @@ import com.mercedesbenz.sechub.adapter.checkmarx.CheckmarxConstants; import com.mercedesbenz.sechub.commons.core.environment.SystemEnvironmentVariableSupport; +import com.mercedesbenz.sechub.domain.scan.product.ProductExecutorContext; import com.mercedesbenz.sechub.domain.scan.product.config.ProductExecutorConfig; import com.mercedesbenz.sechub.domain.scan.product.config.ProductExecutorConfigSetup; import com.mercedesbenz.sechub.domain.scan.product.config.ProductExecutorConfigSetupJobParameter; @@ -23,12 +24,14 @@ public class CheckmarxExecutorConfigSuppportTest { private ProductExecutorConfigSetup setup; private List jobParameters; private SystemEnvironmentVariableSupport systemEnvironmentVariableSupport; + private ProductExecutorContext context; @Before public void before() throws Exception { - + context = mock(); systemEnvironmentVariableSupport = mock(SystemEnvironmentVariableSupport.class); config = mock(ProductExecutorConfig.class); + when(context.getExecutorConfig()).thenReturn(config); setup = mock(ProductExecutorConfigSetup.class); jobParameters = new ArrayList<>(); @@ -40,7 +43,7 @@ public void before() throws Exception { @Test public void client_secret_returns_default_when_not_configured() { /* prepare */ - supportToTest = CheckmarxExecutorConfigSuppport.createSupportAndAssertConfigValid(config, systemEnvironmentVariableSupport); + supportToTest = CheckmarxExecutorConfigSuppport.createSupportAndAssertConfigValid(context, systemEnvironmentVariableSupport); assertEquals(CheckmarxConstants.DEFAULT_CLIENT_SECRET, supportToTest.getClientSecret()); } @@ -49,7 +52,7 @@ public void client_secret_returns_default_when_not_configured() { public void client_secret_returns_configured_value_when_parameter_available() { /* prepare */ jobParameters.add(new ProductExecutorConfigSetupJobParameter(CheckmarxExecutorConfigParameterKeys.CHECKMARX_CLIENT_SECRET, "new.secret")); - supportToTest = CheckmarxExecutorConfigSuppport.createSupportAndAssertConfigValid(config, systemEnvironmentVariableSupport); + supportToTest = CheckmarxExecutorConfigSuppport.createSupportAndAssertConfigValid(context, systemEnvironmentVariableSupport); /* execute +test */ assertEquals("new.secret", supportToTest.getClientSecret()); @@ -59,7 +62,7 @@ public void client_secret_returns_configured_value_when_parameter_available() { public void client_secret_returns_an_empty_string_default_is_returned() { /* prepare */ jobParameters.add(new ProductExecutorConfigSetupJobParameter(CheckmarxExecutorConfigParameterKeys.CHECKMARX_CLIENT_SECRET, "")); - supportToTest = CheckmarxExecutorConfigSuppport.createSupportAndAssertConfigValid(config, systemEnvironmentVariableSupport); + supportToTest = CheckmarxExecutorConfigSuppport.createSupportAndAssertConfigValid(context, systemEnvironmentVariableSupport); /* execute +test */ assertEquals(CheckmarxConstants.DEFAULT_CLIENT_SECRET, supportToTest.getClientSecret()); @@ -68,7 +71,7 @@ public void client_secret_returns_an_empty_string_default_is_returned() { @Test public void engine_configuration_name_returns_default_when_not_configured() { /* prepare */ - supportToTest = CheckmarxExecutorConfigSuppport.createSupportAndAssertConfigValid(config, systemEnvironmentVariableSupport); + supportToTest = CheckmarxExecutorConfigSuppport.createSupportAndAssertConfigValid(context, systemEnvironmentVariableSupport); assertEquals(CheckmarxConstants.DEFAULT_CHECKMARX_ENGINECONFIGURATION_MULTILANGANGE_SCAN_NAME, supportToTest.getEngineConfigurationName()); } @@ -77,7 +80,7 @@ public void engine_configuration_name_returns_default_when_not_configured() { public void engine_configuration_name_returns_configured_value_when_parameter_available() { /* prepare */ jobParameters.add(new ProductExecutorConfigSetupJobParameter(CheckmarxExecutorConfigParameterKeys.CHECKMARX_ENGINE_CONFIGURATIONNAME, "test.engine")); - supportToTest = CheckmarxExecutorConfigSuppport.createSupportAndAssertConfigValid(config, systemEnvironmentVariableSupport); + supportToTest = CheckmarxExecutorConfigSuppport.createSupportAndAssertConfigValid(context, systemEnvironmentVariableSupport); /* execute +test */ assertEquals("test.engine", supportToTest.getEngineConfigurationName()); @@ -87,7 +90,7 @@ public void engine_configuration_name_returns_configured_value_when_parameter_av public void engine_configuration_name_returns_an_empty_string_default_is_returned() { /* prepare */ jobParameters.add(new ProductExecutorConfigSetupJobParameter(CheckmarxExecutorConfigParameterKeys.CHECKMARX_ENGINE_CONFIGURATIONNAME, "")); - supportToTest = CheckmarxExecutorConfigSuppport.createSupportAndAssertConfigValid(config, systemEnvironmentVariableSupport); + supportToTest = CheckmarxExecutorConfigSuppport.createSupportAndAssertConfigValid(context, systemEnvironmentVariableSupport); /* execute +test */ assertEquals(CheckmarxConstants.DEFAULT_CHECKMARX_ENGINECONFIGURATION_MULTILANGANGE_SCAN_NAME, supportToTest.getEngineConfigurationName()); @@ -97,7 +100,7 @@ public void engine_configuration_name_returns_an_empty_string_default_is_returne public void always_fullscan_enabled_true() { /* prepare */ jobParameters.add(new ProductExecutorConfigSetupJobParameter(CheckmarxExecutorConfigParameterKeys.CHECKMARX_FULLSCAN_ALWAYS, "true")); - supportToTest = CheckmarxExecutorConfigSuppport.createSupportAndAssertConfigValid(config, systemEnvironmentVariableSupport); + supportToTest = CheckmarxExecutorConfigSuppport.createSupportAndAssertConfigValid(context, systemEnvironmentVariableSupport); /* test */ assertEquals(true, supportToTest.isAlwaysFullScanEnabled()); @@ -107,7 +110,7 @@ public void always_fullscan_enabled_true() { public void always_fullscan_enabled_false() { /* prepare */ jobParameters.add(new ProductExecutorConfigSetupJobParameter(CheckmarxExecutorConfigParameterKeys.CHECKMARX_FULLSCAN_ALWAYS, "false")); - supportToTest = CheckmarxExecutorConfigSuppport.createSupportAndAssertConfigValid(config, systemEnvironmentVariableSupport); + supportToTest = CheckmarxExecutorConfigSuppport.createSupportAndAssertConfigValid(context, systemEnvironmentVariableSupport); /* test */ assertEquals(false, supportToTest.isAlwaysFullScanEnabled()); diff --git a/sechub-scan-product-checkmarx/src/test/java/com/mercedesbenz/sechub/domain/scan/product/checkmarx/CheckmarxProductExecutorMockTest.java b/sechub-scan-product-checkmarx/src/test/java/com/mercedesbenz/sechub/domain/scan/product/checkmarx/CheckmarxProductExecutorMockTest.java index 3b81bf864a..a06127bc77 100644 --- a/sechub-scan-product-checkmarx/src/test/java/com/mercedesbenz/sechub/domain/scan/product/checkmarx/CheckmarxProductExecutorMockTest.java +++ b/sechub-scan-product-checkmarx/src/test/java/com/mercedesbenz/sechub/domain/scan/product/checkmarx/CheckmarxProductExecutorMockTest.java @@ -18,12 +18,13 @@ import org.junit.runner.RunWith; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.client.RestTemplate; import com.amazonaws.util.StringInputStream; import com.mercedesbenz.sechub.adapter.AdapterException; @@ -49,11 +50,10 @@ import com.mercedesbenz.sechub.domain.scan.product.config.ProductExecutorConfigSetupJobParameter; import com.mercedesbenz.sechub.domain.scan.resolve.NetworkTargetResolver; import com.mercedesbenz.sechub.sharedkernel.ProductIdentifier; -import com.mercedesbenz.sechub.sharedkernel.Profiles; import com.mercedesbenz.sechub.sharedkernel.configuration.SecHubConfiguration; import com.mercedesbenz.sechub.sharedkernel.mapping.MappingIdentifier; import com.mercedesbenz.sechub.sharedkernel.metadata.DefaultMetaDataInspector; -import com.mercedesbenz.sechub.sharedkernel.security.AbstractSecHubAPISecurityConfiguration; +import com.mercedesbenz.sechub.sharedkernel.security.SecHubSecurityConfiguration; import com.mercedesbenz.sechub.sharedkernel.storage.SecHubStorageService; import com.mercedesbenz.sechub.storage.core.JobStorage; @@ -61,6 +61,7 @@ @RunWith(SpringRunner.class) @ContextConfiguration(classes = { CheckmarxProductExecutor.class, CheckmarxResilienceConsultant.class, CheckmarxProductExecutorMockTest.SimpleTestConfiguration.class, DefaultMetaDataInspector.class }) +@Import(SecHubSecurityConfiguration.class) public class CheckmarxProductExecutorMockTest { private static final String PROJECT_EXAMPLE = "projectIdxyz"; @@ -215,10 +216,12 @@ private SecHubExecutionContext createExecutionContextForPseudoCodeScan() { } @TestConfiguration - @Profile(Profiles.TEST) - @EnableAutoConfiguration - public static class SimpleTestConfiguration extends AbstractSecHubAPISecurityConfiguration { + static class SimpleTestConfiguration { + @Bean + RestTemplate restTemplate() { + return new RestTemplate(); + } } } diff --git a/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/AbstractPDSProductExecutor.java b/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/AbstractPDSProductExecutor.java index d12908f390..56041bb67a 100644 --- a/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/AbstractPDSProductExecutor.java +++ b/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/AbstractPDSProductExecutor.java @@ -75,8 +75,7 @@ protected final List executeByAdapter(ProductExecutorData data) t LOG.debug("Trigger PDS adapter execution for scan type: {} by: {}", getScanType(), getClass().getSimpleName()); ProductExecutorContext executorContext = data.getProductExecutorContext(); - PDSExecutorConfigSupport configSupport = PDSExecutorConfigSupport.createSupportAndAssertConfigValid(executorContext.getExecutorConfig(), - serviceCollection); + PDSExecutorConfigSupport configSupport = PDSExecutorConfigSupport.createSupportAndAssertConfigValid(executorContext, serviceCollection); SecHubExecutionContext context = data.getSechubExecutionContext(); diff --git a/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSAdapterConfigurationStrategy.java b/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSAdapterConfigurationStrategy.java index 7693ce64bd..ed5dbf7d78 100644 --- a/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSAdapterConfigurationStrategy.java +++ b/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSAdapterConfigurationStrategy.java @@ -13,6 +13,7 @@ import com.mercedesbenz.sechub.adapter.AdapterConfigurationStrategy; import com.mercedesbenz.sechub.adapter.pds.PDSAdapterConfigurator; import com.mercedesbenz.sechub.adapter.pds.PDSAdapterConfiguratorProvider; +import com.mercedesbenz.sechub.commons.core.ConfigurationFailureException; import com.mercedesbenz.sechub.commons.model.ScanType; import com.mercedesbenz.sechub.commons.model.SecHubRuntimeException; import com.mercedesbenz.sechub.domain.scan.DefaultAdapterConfigurationStrategy; @@ -136,7 +137,7 @@ private PDSAdapterConfigurationStrategy(PDSAdapterConfigurationStrategyConfig co } @Override - public void configure(B configBuilder) { + public void configure(B configBuilder) throws ConfigurationFailureException { PDSAdapterConfigurator pdsConfigurator = null; if (configBuilder instanceof PDSAdapterConfiguratorProvider) { PDSAdapterConfiguratorProvider provider = (PDSAdapterConfiguratorProvider) configBuilder; @@ -153,13 +154,11 @@ public void configure( handlePdsParts(pdsConfigurator); } - private void handlePdsParts(PDSAdapterConfigurator pdsConfigurable) { + private void handlePdsParts(PDSAdapterConfigurator pdsConfigurable) throws ConfigurationFailureException { PDSExecutorConfigSupport configSupport = strategyConfig.configSupport; SecHubExecutionContext context = strategyConfig.productExecutorData.getSechubExecutionContext(); - Map jobParametersToSend = configSupport.createJobParametersToSendToPDS(context.getConfiguration()); - pdsConfigurable.setJobParameters(jobParametersToSend); pdsConfigurable.setReusingSecHubStorage(configSupport.isReusingSecHubStorage()); pdsConfigurable.setScanType(strategyConfig.scanType); pdsConfigurable.setPdsProductIdentifier(configSupport.getPDSProductIdentifier()); @@ -170,7 +169,6 @@ private void handlePdsParts(PDSAdapterConfigurator pdsConfigurable) { pdsConfigurable.setBinaryTarFileInputStreamOrNull(strategyConfig.binariesTarFileInputStreamOrNull); pdsConfigurable.setSourceCodeZipFileRequired(strategyConfig.contentProvider.isSourceRequired()); pdsConfigurable.setBinaryTarFileRequired(strategyConfig.contentProvider.isBinaryRequired()); - pdsConfigurable.setResilienceMaxRetries(configSupport.getPDSAdapterResilienceMaxRetries()); pdsConfigurable.setResilienceTimeToWaitBeforeRetryInMilliseconds(configSupport.getPDSAdapterResilienceRetryWaitInMilliseconds()); @@ -179,6 +177,14 @@ private void handlePdsParts(PDSAdapterConfigurator pdsConfigurable) { handleBinariesChecksum(pdsConfigurable); handleBinariesFileSize(pdsConfigurable); + + handleJobParameters(pdsConfigurable, configSupport, context); + } + + private void handleJobParameters(PDSAdapterConfigurator pdsConfigurable, PDSExecutorConfigSupport configSupport, SecHubExecutionContext context) + throws ConfigurationFailureException { + Map jobParametersToSend = configSupport.createJobParametersToSendToPDS(context); + pdsConfigurable.setJobParameters(jobParametersToSend); } private void handleSourceCodeChecksum(PDSAdapterConfigurator pdsConfigurable) { @@ -237,7 +243,7 @@ private void handleBinariesFileSize(PDSAdapterConfigurator pdsConfigurable) { } } - private void handleCommonParts(B configBuilder) { + private void handleCommonParts(B configBuilder) throws ConfigurationFailureException { /* standard configuration */ /* @formatter:off */ diff --git a/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSExecutorConfigSupport.java b/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSExecutorConfigSupport.java index a6dc958cc0..d119bbd8e4 100644 --- a/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSExecutorConfigSupport.java +++ b/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSExecutorConfigSupport.java @@ -14,19 +14,27 @@ import org.slf4j.LoggerFactory; import com.mercedesbenz.sechub.adapter.DefaultExecutorConfigSupport; +import com.mercedesbenz.sechub.commons.core.ConfigurationFailureException; import com.mercedesbenz.sechub.commons.core.environment.SystemEnvironmentVariableSupport; import com.mercedesbenz.sechub.commons.core.util.SecHubStorageUtil; import com.mercedesbenz.sechub.commons.core.util.SimpleStringUtils; +import com.mercedesbenz.sechub.commons.model.JSONConverter; +import com.mercedesbenz.sechub.commons.model.ScanType; import com.mercedesbenz.sechub.commons.model.SecHubDataConfigurationType; import com.mercedesbenz.sechub.commons.model.SecHubDataConfigurationTypeListParser; +import com.mercedesbenz.sechub.commons.model.template.TemplateDefinition; import com.mercedesbenz.sechub.commons.pds.PDSConfigDataKeyProvider; import com.mercedesbenz.sechub.commons.pds.PDSDefaultParameterKeyConstants; import com.mercedesbenz.sechub.commons.pds.PDSKey; import com.mercedesbenz.sechub.commons.pds.PDSKeyProvider; +import com.mercedesbenz.sechub.commons.pds.data.PDSTemplateMetaData; import com.mercedesbenz.sechub.domain.scan.NetworkTargetProductServerDataProvider; import com.mercedesbenz.sechub.domain.scan.NetworkTargetType; +import com.mercedesbenz.sechub.domain.scan.SecHubExecutionContext; import com.mercedesbenz.sechub.domain.scan.config.ScanMapping; +import com.mercedesbenz.sechub.domain.scan.product.ProductExecutorContext; import com.mercedesbenz.sechub.domain.scan.product.config.ProductExecutorConfig; +import com.mercedesbenz.sechub.sharedkernel.ProductIdentifier; import com.mercedesbenz.sechub.sharedkernel.configuration.SecHubConfiguration; import com.mercedesbenz.sechub.sharedkernel.error.NotAcceptableException; import com.mercedesbenz.sechub.sharedkernel.validation.Validation; @@ -42,6 +50,8 @@ public class PDSExecutorConfigSupport extends DefaultExecutorConfigSupport imple private PDSExecutorConfigSuppportServiceCollection serviceCollection; private SecHubDataConfigurationTypeListParser parser = new SecHubDataConfigurationTypeListParser(); + PDSTemplateMetaDataService templateMetaDataTransformer = new PDSTemplateMetaDataService(); + static { List> allParameterProviders = new ArrayList<>(); allParameterProviders.addAll(Arrays.asList(SecHubProductExecutionPDSKeyProvider.values())); @@ -64,29 +74,63 @@ public static List> getUnmodifiableListOfParame * @return support * @throws NotAcceptableException when configuration is not valid */ - public static PDSExecutorConfigSupport createSupportAndAssertConfigValid(ProductExecutorConfig config, + public static PDSExecutorConfigSupport createSupportAndAssertConfigValid(ProductExecutorContext context, PDSExecutorConfigSuppportServiceCollection serviceCollection) { - return new PDSExecutorConfigSupport(config, serviceCollection, new PDSProductExecutorMinimumConfigValidation()); + PDSExecutorConfigSupport result = new PDSExecutorConfigSupport(context, serviceCollection, new PDSProductExecutorMinimumConfigValidation()); + return result; } - private PDSExecutorConfigSupport(ProductExecutorConfig config, PDSExecutorConfigSuppportServiceCollection serviceCollection, + private PDSExecutorConfigSupport(ProductExecutorContext context, PDSExecutorConfigSuppportServiceCollection serviceCollection, Validation validation) { - super(config, serviceCollection.getSystemEnvironmentVariableSupport(), validation); + super(context, serviceCollection.getSystemEnvironmentVariableSupport(), validation); this.serviceCollection = serviceCollection; } - public Map createJobParametersToSendToPDS(SecHubConfiguration secHubConfiguration) { + public Map createJobParametersToSendToPDS(SecHubExecutionContext context) throws ConfigurationFailureException { + if (context == null) { + throw new IllegalArgumentException("context may not be null!"); + } + SecHubConfiguration configuration = context.getConfiguration(); + if (configuration == null) { + throw new IllegalStateException("configuration may not be null inside context at this moment!"); + } + ProductIdentifier productIdentifier = config.getProductIdentifier(); + if (productIdentifier == null) { + throw new IllegalStateException("productIdentifier may not be null inside config at this moment!"); + } + ScanType scanType = productIdentifier.getType(); + if (scanType == null) { + throw new IllegalStateException("scanType may not be null inside productIdentifier:" + productIdentifier); + } Map parametersToSend = createParametersToSendByProviders(keyProvidersForSendingParametersToPDS); handleEnvironmentVariablesInJobParameters(parametersToSend); /* handle remaining parts without environment variable conversion */ - handleSecHubStorageIfNecessary(secHubConfiguration, parametersToSend); + handleSecHubStorageIfNecessary(configuration, parametersToSend); addMappingsAsJobParameter(parametersToSend); + addPdsTemplateMetaDataList(scanType, context, parametersToSend); return parametersToSend; } + private void addPdsTemplateMetaDataList(ScanType scanType, SecHubExecutionContext context, Map parametersToSend) + throws ConfigurationFailureException { + List templateDefinitions = context.getTemplateDefinitions(); + if (templateDefinitions == null) { + throw new IllegalStateException("template definitions may not be null inside context at this moment!"); + } + PDSTemplateMetaDataService templateMetaDataService = serviceCollection.getTemplateMetaDataService(); + + List pdsTemplateMetaDataList = templateMetaDataService.createTemplateMetaData(templateDefinitions, getPDSProductIdentifier(), + scanType, context.getConfiguration()); + + templateMetaDataService.ensureTemplateAssetFilesAreAvailableInStorage(pdsTemplateMetaDataList); + + String pdsTemplateMetaDataListAsJson = JSONConverter.get().toJSON(pdsTemplateMetaDataList, false); + parametersToSend.put(PDSDefaultParameterKeyConstants.PARAM_KEY_PDS_CONFIG_TEMPLATE_META_DATA_LIST, pdsTemplateMetaDataListAsJson); + } + private void handleSecHubStorageIfNecessary(SecHubConfiguration secHubConfiguration, Map parametersToSend) { if (isReusingSecHubStorage()) { String projectId = secHubConfiguration.getProjectId(); @@ -310,4 +354,5 @@ public boolean isGivenStorageSupportedByPDSProduct(PDSStorageContentProvider con public String getDataTypesSupportedByPDSAsString() { return getParameter(PDSConfigDataKeyProvider.PDS_CONFIG_SUPPORTED_DATATYPES); } + } diff --git a/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSExecutorConfigSuppportServiceCollection.java b/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSExecutorConfigSuppportServiceCollection.java index 5bd6854a7b..b36d5113a8 100644 --- a/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSExecutorConfigSuppportServiceCollection.java +++ b/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSExecutorConfigSuppportServiceCollection.java @@ -16,6 +16,9 @@ public class PDSExecutorConfigSuppportServiceCollection { @Autowired ScanMappingRepository scanMappingRepository; + @Autowired + PDSTemplateMetaDataService templateMetaDataService; + public SystemEnvironmentVariableSupport getSystemEnvironmentVariableSupport() { return systemEnvironment; } @@ -23,4 +26,8 @@ public SystemEnvironmentVariableSupport getSystemEnvironmentVariableSupport() { public ScanMappingRepository getScanMappingRepository() { return scanMappingRepository; } + + public PDSTemplateMetaDataService getTemplateMetaDataService() { + return templateMetaDataService; + } } diff --git a/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSInfraScanProductExecutor.java b/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSInfraScanProductExecutor.java index cb1192bd72..a20c32b386 100644 --- a/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSInfraScanProductExecutor.java +++ b/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSInfraScanProductExecutor.java @@ -112,8 +112,7 @@ protected void customize(ProductExecutorData data) { data.setNetworkLocationProvider(new InfraScanNetworkLocationProvider(secHubConfiguration)); ProductExecutorContext executorContext = data.getProductExecutorContext(); - PDSExecutorConfigSupport configSupport = PDSExecutorConfigSupport.createSupportAndAssertConfigValid(executorContext.getExecutorConfig(), - serviceCollection); + PDSExecutorConfigSupport configSupport = PDSExecutorConfigSupport.createSupportAndAssertConfigValid(executorContext, serviceCollection); data.setNetworkTargetDataProvider(configSupport); } diff --git a/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSTemplateMetaDataService.java b/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSTemplateMetaDataService.java new file mode 100644 index 0000000000..b0acdd59e3 --- /dev/null +++ b/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSTemplateMetaDataService.java @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.scan.product.pds; + +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.mercedesbenz.sechub.commons.core.ConfigurationFailureException; +import com.mercedesbenz.sechub.commons.model.ScanType; +import com.mercedesbenz.sechub.commons.model.SecHubConfigurationModel; +import com.mercedesbenz.sechub.commons.model.template.TemplateDefinition; +import com.mercedesbenz.sechub.commons.pds.data.PDSTemplateMetaData; +import com.mercedesbenz.sechub.commons.pds.data.PDSTemplateMetaData.PDSAssetData; +import com.mercedesbenz.sechub.domain.scan.asset.AssetDetailData; +import com.mercedesbenz.sechub.domain.scan.asset.AssetFileData; +import com.mercedesbenz.sechub.domain.scan.asset.AssetService; +import com.mercedesbenz.sechub.sharedkernel.error.NotFoundException; + +@Service +public class PDSTemplateMetaDataService { + + private static final Logger LOG = LoggerFactory.getLogger(PDSTemplateMetaDataService.class); + + @Autowired + AssetService assetService; + + @Autowired + RelevantScanTemplateDefinitionFilter filter; + + public List createTemplateMetaData(List templateDefinitions, String pdsProductId, ScanType scanType, + SecHubConfigurationModel configuration) throws ConfigurationFailureException { + + List result = new ArrayList<>(); + + List filteredDefinitions = filter.filter(templateDefinitions, scanType, configuration); + if (filteredDefinitions.isEmpty()) { + LOG.debug("Given {} template definitions, after filtering: {}", templateDefinitions.size(), filteredDefinitions.size()); + } + + resolveAssetFileDataAndAddToResult(result, pdsProductId, filteredDefinitions); + + return result; + } + + /** + * Ensures that asset files in given PDS template meta data is available in + * storage. + * + * @param metaDataList list containing template meta data + * @throws ConfigurationFailureException + */ + public void ensureTemplateAssetFilesAreAvailableInStorage(List metaDataList) throws ConfigurationFailureException { + for (PDSTemplateMetaData metaData : metaDataList) { + + PDSAssetData assetData = metaData.getAssetData(); + + String assetId = assetData.getAssetId(); + String fileName = assetData.getFileName(); + + assetService.ensureAssetFileInStorageAvailableAndHasSameChecksumAsInDatabase(fileName, assetId); + + } + } + + private void resolveAssetFileDataAndAddToResult(List result, String pdsProductId, List filteredDefinitions) + throws ConfigurationFailureException { + if (filteredDefinitions.isEmpty()) { + return; + } + + String filename = pdsProductId + ".zip"; + + for (TemplateDefinition definition : filteredDefinitions) { + String assetId = definition.getAssetId(); + AssetDetailData details = null; + try { + details = assetService.fetchAssetDetails(assetId); + } catch (NotFoundException e) { + /* asset does not exist */ + throw new ConfigurationFailureException("The asset " + assetId + " does not exist! Cannot transform", e); + } + + List files = details.getFiles(); + boolean fileForTemplateFound = false; + + for (AssetFileData fileData : files) { + if (!filename.equals(fileData.getFileName())) { + continue; + } + /* found */ + PDSAssetData assetData = new PDSAssetData(); + assetData.setAssetId(assetId); + assetData.setFileName(fileData.getFileName()); + assetData.setChecksum(fileData.getChecksum()); + + PDSTemplateMetaData metaData = new PDSTemplateMetaData(); + metaData.setTemplateId(definition.getId()); + metaData.setTemplateType(definition.getType()); + metaData.setAssetData(assetData); + + result.add(metaData); + + fileForTemplateFound = true; + break; + } + + if (!fileForTemplateFound) { + throw new ConfigurationFailureException("The asset " + assetId + " does not contain file '" + filename + "'"); + } + } + } +} diff --git a/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSWebScanProductExecutor.java b/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSWebScanProductExecutor.java index b11947eac7..4e4478da99 100644 --- a/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSWebScanProductExecutor.java +++ b/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSWebScanProductExecutor.java @@ -110,8 +110,7 @@ protected void customize(ProductExecutorData data) { data.setNetworkLocationProvider(new WebScanNetworkLocationProvider(secHubConfiguration)); ProductExecutorContext executorContext = data.getProductExecutorContext(); - PDSExecutorConfigSupport configSupport = PDSExecutorConfigSupport.createSupportAndAssertConfigValid(executorContext.getExecutorConfig(), - serviceCollection); + PDSExecutorConfigSupport configSupport = PDSExecutorConfigSupport.createSupportAndAssertConfigValid(executorContext, serviceCollection); data.setNetworkTargetDataProvider(configSupport); diff --git a/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/RelevantScanTemplateDefinitionFilter.java b/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/RelevantScanTemplateDefinitionFilter.java new file mode 100644 index 0000000000..724f720403 --- /dev/null +++ b/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/RelevantScanTemplateDefinitionFilter.java @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.scan.product.pds; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.mercedesbenz.sechub.commons.model.ScanType; +import com.mercedesbenz.sechub.commons.model.SecHubConfigurationModel; +import com.mercedesbenz.sechub.commons.model.template.TemplateData; +import com.mercedesbenz.sechub.commons.model.template.TemplateDataResolver; +import com.mercedesbenz.sechub.commons.model.template.TemplateDefinition; +import com.mercedesbenz.sechub.commons.model.template.TemplateType; + +@Component +/** + * Filters template definitions which are relevant for a scan + * + * @author Albert Tregnaghi + * + */ +public class RelevantScanTemplateDefinitionFilter { + + @Autowired + TemplateDataResolver templateDataResolver; + + /** + * Filters template definitions for only relevant definitions for given scan + * type and configuration. If the configuration does not contain a templateData + * definition, the list will always be empty. If configuration contains template + * data, but the scan type does not need/use them the result will also be empty + * + * @param templateDefinitions all given definitions + * @param scanType scan type for which relevant definitions shall be + * filtered + * @param configuration SecHub configuration which contains template data + * (or not) + * @return list of relevant template definitions for the scan, never + * null + */ + public List filter(List templateDefinitions, ScanType scanType, SecHubConfigurationModel configuration) { + List result = new ArrayList<>(); + + for (TemplateDefinition definition : templateDefinitions) { + + addDefinitionToResultIfNecessary(definition, scanType, configuration, result); + } + return result; + } + + private void addDefinitionToResultIfNecessary(TemplateDefinition definition, ScanType scanType, SecHubConfigurationModel configuration, + List result) { + if (scanType == null) { + return; + } + switch (scanType) { + case WEB_SCAN: + /* for web scan we accept WEBSCAN_LOGIN templates */ + if (TemplateType.WEBSCAN_LOGIN.equals(definition.getType())) { + TemplateData templateData = templateDataResolver.resolveTemplateData(TemplateType.WEBSCAN_LOGIN, configuration); + if (templateData != null) { + /* data available, so add to result */ + result.add(definition); + } + } + break; + default: + break; + } + + } + +} diff --git a/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/SecHubProductExecutionPDSKeyProvider.java b/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/SecHubProductExecutionPDSKeyProvider.java index 395e7fd580..2120e1a489 100644 --- a/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/SecHubProductExecutionPDSKeyProvider.java +++ b/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/SecHubProductExecutionPDSKeyProvider.java @@ -8,7 +8,7 @@ import com.mercedesbenz.sechub.domain.scan.NetworkTargetType; /** - * These providers/keys are used by sechub PDS product executors at runtime + * These providers/keys are used by SecHub PDS product executors at runtime * while communicating with PDS servers. * * @author Albert Tregnaghi diff --git a/sechub-scan-product-pds/src/test/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSExecutorConfigSupportTest.java b/sechub-scan-product-pds/src/test/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSExecutorConfigSupportTest.java index 32049385b1..987d59c304 100644 --- a/sechub-scan-product-pds/src/test/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSExecutorConfigSupportTest.java +++ b/sechub-scan-product-pds/src/test/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSExecutorConfigSupportTest.java @@ -1,10 +1,13 @@ // SPDX-License-Identifier: MIT package com.mercedesbenz.sechub.domain.scan.product.pds; +import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; @@ -24,16 +27,25 @@ import com.mercedesbenz.sechub.commons.mapping.MappingData; import com.mercedesbenz.sechub.commons.mapping.MappingEntry; import com.mercedesbenz.sechub.commons.model.JSONConverter; +import com.mercedesbenz.sechub.commons.model.ScanType; import com.mercedesbenz.sechub.commons.model.SecHubDataConfigurationType; +import com.mercedesbenz.sechub.commons.model.template.TemplateDefinition; import com.mercedesbenz.sechub.commons.pds.PDSConfigDataKeyProvider; +import com.mercedesbenz.sechub.commons.pds.PDSDefaultParameterKeyConstants; +import com.mercedesbenz.sechub.commons.pds.data.PDSTemplateMetaData; +import com.mercedesbenz.sechub.commons.pds.data.PDSTemplateMetaData.PDSAssetData; import com.mercedesbenz.sechub.domain.scan.NetworkTargetType; +import com.mercedesbenz.sechub.domain.scan.SecHubExecutionContext; import com.mercedesbenz.sechub.domain.scan.config.ScanMapping; import com.mercedesbenz.sechub.domain.scan.config.ScanMappingRepository; +import com.mercedesbenz.sechub.domain.scan.product.ProductExecutorContext; import com.mercedesbenz.sechub.domain.scan.product.config.ProductExecutorConfig; import com.mercedesbenz.sechub.domain.scan.product.config.ProductExecutorConfigSetup; import com.mercedesbenz.sechub.domain.scan.product.config.ProductExecutorConfigSetupCredentials; import com.mercedesbenz.sechub.domain.scan.product.config.ProductExecutorConfigSetupJobParameter; +import com.mercedesbenz.sechub.sharedkernel.ProductIdentifier; import com.mercedesbenz.sechub.sharedkernel.configuration.SecHubConfiguration; +import com.mercedesbenz.sechub.test.TestCanaryException; public class PDSExecutorConfigSupportTest { @@ -59,11 +71,27 @@ public class PDSExecutorConfigSupportTest { private List jobParameters; private ScanMappingRepository repository; private SystemEnvironmentVariableSupport systemEnvironmentVariableSupport; + private ProductExecutorContext context; + private SecHubExecutionContext sechubExecutionContext; + private SecHubConfiguration sechubConfiguration; + private PDSTemplateMetaDataService templateMetaDataService; @BeforeEach public void before() throws Exception { - config = mock(ProductExecutorConfig.class); - executorConfigSetup = mock(ProductExecutorConfigSetup.class); + sechubConfiguration = mock(); + sechubExecutionContext = mock(); + config = mock(); + context = mock(); + executorConfigSetup = mock(); + serviceCollection = mock(); + repository = mock(); + templateMetaDataService = mock(); + + when(sechubExecutionContext.getTemplateDefinitions()).thenReturn(Collections.emptyList()); + when(sechubExecutionContext.getConfiguration()).thenReturn(sechubConfiguration); + + when(config.getProductIdentifier()).thenReturn(ProductIdentifier.UNKNOWN);// the scan type unknown is used, because not really relevant + when(context.getExecutorConfig()).thenReturn(config); jobParameters = new ArrayList<>(); jobParameters.add(new ProductExecutorConfigSetupJobParameter(PDSConfigDataKeyProvider.PDS_CONFIG_PRODUCTIDENTIFIER.getKey().getId(), @@ -82,16 +110,14 @@ public void before() throws Exception { when(executorConfigSetup.getJobParameters()).thenReturn(jobParameters); - serviceCollection = mock(PDSExecutorConfigSuppportServiceCollection.class); - Answer defaultAnswerWithNoConversion = createAnswerWhichReturnsAlwaysJustTheOriginValue(); systemEnvironmentVariableSupport = mock(SystemEnvironmentVariableSupport.class, defaultAnswerWithNoConversion); - repository = mock(ScanMappingRepository.class); - when(serviceCollection.getScanMappingRepository()).thenReturn(repository); when(serviceCollection.getSystemEnvironmentVariableSupport()).thenReturn(systemEnvironmentVariableSupport); - supportToTest = PDSExecutorConfigSupport.createSupportAndAssertConfigValid(config, serviceCollection); + when(serviceCollection.getTemplateMetaDataService()).thenReturn(templateMetaDataService); + + supportToTest = PDSExecutorConfigSupport.createSupportAndAssertConfigValid(context, serviceCollection); } @EnumSource(SecHubDataConfigurationType.class) @@ -107,7 +133,7 @@ void isGivenStorageSupportedByPDSProduct_binary_and_source_required_from_model_a .add(new ProductExecutorConfigSetupJobParameter(PDSConfigDataKeyProvider.PDS_CONFIG_SUPPORTED_DATATYPES.getKey().getId(), type.toString())); // create support again (necessary to have new job parameters included) - supportToTest = PDSExecutorConfigSupport.createSupportAndAssertConfigValid(config, serviceCollection); + supportToTest = PDSExecutorConfigSupport.createSupportAndAssertConfigValid(context, serviceCollection); /* execute */ boolean result = supportToTest.isGivenStorageSupportedByPDSProduct(contentProvider); @@ -143,7 +169,7 @@ void isGivenStorageSupportedByPDSProduct_no_binary_in_model_required_but_product SecHubDataConfigurationType.BINARY.toString())); // create support again (necessary to have new job parameters included) - supportToTest = PDSExecutorConfigSupport.createSupportAndAssertConfigValid(config, serviceCollection); + supportToTest = PDSExecutorConfigSupport.createSupportAndAssertConfigValid(context, serviceCollection); /* execute */ boolean result = supportToTest.isGivenStorageSupportedByPDSProduct(contentProvider); @@ -164,7 +190,7 @@ void isGivenStorageSupportedByPDSProduct_only_source_in_model_required_but_produ jobParameters.add(new ProductExecutorConfigSetupJobParameter(PDSConfigDataKeyProvider.PDS_CONFIG_SUPPORTED_DATATYPES.getKey().getId(), typesAsString)); // create support again (necessary to have new job parameters included) - supportToTest = PDSExecutorConfigSupport.createSupportAndAssertConfigValid(config, serviceCollection); + supportToTest = PDSExecutorConfigSupport.createSupportAndAssertConfigValid(context, serviceCollection); /* execute */ boolean result = supportToTest.isGivenStorageSupportedByPDSProduct(contentProvider); @@ -185,7 +211,7 @@ void isGivenStorageSupportedByPDSProduct_only_binary_in_model_required_but_produ jobParameters.add(new ProductExecutorConfigSetupJobParameter(PDSConfigDataKeyProvider.PDS_CONFIG_SUPPORTED_DATATYPES.getKey().getId(), typesAsString)); // create support again (necessary to have new job parameters included) - supportToTest = PDSExecutorConfigSupport.createSupportAndAssertConfigValid(config, serviceCollection); + supportToTest = PDSExecutorConfigSupport.createSupportAndAssertConfigValid(context, serviceCollection); /* execute */ boolean result = supportToTest.isGivenStorageSupportedByPDSProduct(contentProvider); @@ -206,7 +232,7 @@ void isGivenStorageSupportedByPDSProduct_no_source_in_model_required_but_product SecHubDataConfigurationType.SOURCE.toString())); // create support again (necessary to have new job parameters included) - supportToTest = PDSExecutorConfigSupport.createSupportAndAssertConfigValid(config, serviceCollection); + supportToTest = PDSExecutorConfigSupport.createSupportAndAssertConfigValid(context, serviceCollection); /* execute */ boolean result = supportToTest.isGivenStorageSupportedByPDSProduct(contentProvider); @@ -227,7 +253,7 @@ void isGivenStorageSupportedByPDSProduct_no_source_or_binary_in_model_required_b SecHubDataConfigurationType.NONE.toString())); // create support again (necessary to have new job parameters included) - supportToTest = PDSExecutorConfigSupport.createSupportAndAssertConfigValid(config, serviceCollection); + supportToTest = PDSExecutorConfigSupport.createSupportAndAssertConfigValid(context, serviceCollection); /* execute */ boolean result = supportToTest.isGivenStorageSupportedByPDSProduct(contentProvider); @@ -249,7 +275,7 @@ void isGivenStorageSupportedByPDSProduct_no_source_or_binary_in_model_required_b .add(new ProductExecutorConfigSetupJobParameter(PDSConfigDataKeyProvider.PDS_CONFIG_SUPPORTED_DATATYPES.getKey().getId(), type.toString())); // create support again (necessary to have new job parameters included) - supportToTest = PDSExecutorConfigSupport.createSupportAndAssertConfigValid(config, serviceCollection); + supportToTest = PDSExecutorConfigSupport.createSupportAndAssertConfigValid(context, serviceCollection); /* execute */ boolean result = supportToTest.isGivenStorageSupportedByPDSProduct(contentProvider); @@ -272,7 +298,7 @@ void isGivenStorageSupportedByPDSProduct_no_source_or_binary_in_model_but_wrong_ jobParameters.add(new ProductExecutorConfigSetupJobParameter(PDSConfigDataKeyProvider.PDS_CONFIG_SUPPORTED_DATATYPES.getKey().getId(), typesAsString)); // create support again (necessary to have new job parameters included) - supportToTest = PDSExecutorConfigSupport.createSupportAndAssertConfigValid(config, serviceCollection); + supportToTest = PDSExecutorConfigSupport.createSupportAndAssertConfigValid(context, serviceCollection); /* execute */ boolean result = supportToTest.isGivenStorageSupportedByPDSProduct(contentProvider); @@ -297,7 +323,25 @@ void isTargetTypeForbidden_returns_false_for_target_type_requested_is_intranet_w } @Test - void createJobParametersToSendToPDS_environmentVariablesEntriesAreReplacedWithTheirContent() { + void createJobParametersToSendToPDS_null_list_of_template_meta_data_throws_illegal_argument_exception() { + assertThatThrownBy(() -> supportToTest.createJobParametersToSendToPDS(null)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void createJobParametersToSendToPDS_empty_template_list_is_accepted() throws Exception { + /* prepare */ + when(sechubExecutionContext.getTemplateDefinitions()).thenReturn(Collections.emptyList()); + + /* execute */ + Map result = supportToTest.createJobParametersToSendToPDS(sechubExecutionContext); + + /* test */ + String data = result.get(PDSDefaultParameterKeyConstants.PARAM_KEY_PDS_CONFIG_TEMPLATE_META_DATA_LIST); + assertThat(data).isEqualTo("[]"); + } + + @Test + void createJobParametersToSendToPDS_environmentVariablesEntriesAreReplacedWithTheirContent() throws Exception { /* prepare */ String parameterKey1 = "test.key1"; String parameterKey2 = "test.key2"; @@ -308,15 +352,13 @@ void createJobParametersToSendToPDS_environmentVariablesEntriesAreReplacedWithTh jobParameters.add(new ProductExecutorConfigSetupJobParameter(parameterKey3, "just-a-key-not-converted")); // create support again (necessary to have new job parameters included) - supportToTest = PDSExecutorConfigSupport.createSupportAndAssertConfigValid(config, serviceCollection); + supportToTest = PDSExecutorConfigSupport.createSupportAndAssertConfigValid(context, serviceCollection); when(systemEnvironmentVariableSupport.getValueOrVariableContent("env:A_TESTVARIABLE")).thenReturn("resolved-value"); when(systemEnvironmentVariableSupport.getValueOrVariableContent("env:A_NOT_EXISTING_VARIABLE")).thenReturn(null); - SecHubConfiguration sechubConfiguration = mock(SecHubConfiguration.class); - /* execute */ - Map parameterMap = supportToTest.createJobParametersToSendToPDS(sechubConfiguration); + Map parameterMap = supportToTest.createJobParametersToSendToPDS(sechubExecutionContext); /* test */ assertEquals("resolved-value", parameterMap.get(parameterKey1)); @@ -326,15 +368,13 @@ void createJobParametersToSendToPDS_environmentVariablesEntriesAreReplacedWithTh } @Test - void createJobParametersToSendToPDS_mapping_is_resolved() { + void createJobParametersToSendToPDS_mapping_is_resolved() throws Exception { /* prepare */ mockSecHubMappingInDatabase(); mockSecHubMappingId2InDatabase(); - SecHubConfiguration sechubConfiguration = new SecHubConfiguration(); - /* execute */ - Map parameters = supportToTest.createJobParametersToSendToPDS(sechubConfiguration); + Map parameters = supportToTest.createJobParametersToSendToPDS(sechubExecutionContext); /* test 1 */ String p1 = parameters.get(SECHUB_MAPPING_ID_1); @@ -385,6 +425,58 @@ void createJobParametersToSendToPDS_mapping_is_resolved() { verify(systemEnvironmentVariableSupport, times(1)).getValueOrVariableContent(any()); } + @Test + void createJobParametersToSendToPDS_pds_template_meta_data_is_created_by_result_from_template_metadata_service() throws Exception { + /* prepare */ + List templateDefinitions = mock(); + when(sechubExecutionContext.getTemplateDefinitions()).thenReturn(templateDefinitions); + + List templateMetaDataServiceResult = createTemplateMetaDataServiceExampleResult(); + + when(templateMetaDataService.createTemplateMetaData(eq(templateDefinitions), anyString(), any(ScanType.class), eq(sechubConfiguration))) + .thenReturn(templateMetaDataServiceResult); + /* execute */ + Map parameterMap = supportToTest.createJobParametersToSendToPDS(sechubExecutionContext); + + /* test */ + String expectedJson = JSONConverter.get().toJSON(templateMetaDataServiceResult, false); + String jsonFromConfigSupport = parameterMap.get(PDSDefaultParameterKeyConstants.PARAM_KEY_PDS_CONFIG_TEMPLATE_META_DATA_LIST); + + assertThat(jsonFromConfigSupport).isNotNull().isEqualTo(expectedJson).hasSizeGreaterThan(10); + + } + + @Test + void createJobParametersToSendToPDS_templateDataService_is_called_twice_in_correct_order_and_arguments() throws Exception { + /* prepare */ + List templateDefinitions = mock(); + when(sechubExecutionContext.getTemplateDefinitions()).thenReturn(templateDefinitions); + + List templateMetaDataServiceResult = createTemplateMetaDataServiceExampleResult(); + + when(templateMetaDataService.createTemplateMetaData(eq(templateDefinitions), anyString(), any(ScanType.class), eq(sechubConfiguration))) + .thenReturn(templateMetaDataServiceResult); + // next line does ensure that it is called with the former result. It also + // breaks further processing + doThrow(new TestCanaryException()).when(templateMetaDataService).ensureTemplateAssetFilesAreAvailableInStorage(templateMetaDataServiceResult); + + /* execute + test */ + assertThatThrownBy(() -> supportToTest.createJobParametersToSendToPDS(sechubExecutionContext)).isInstanceOf(TestCanaryException.class); + + } + + private List createTemplateMetaDataServiceExampleResult() { + List templateMetaDataServiceResult = new ArrayList<>(); + PDSTemplateMetaData pdsTemplateMetaData1 = new PDSTemplateMetaData(); + PDSAssetData assetData1 = new PDSAssetData(); + assetData1.setAssetId("asset-id-1"); + assetData1.setChecksum("checksum1"); + assetData1.setFileName("file1"); + pdsTemplateMetaData1.setAssetData(assetData1); + templateMetaDataServiceResult.add(pdsTemplateMetaData1); + return templateMetaDataServiceResult; + } + private void mockSecHubMappingId2InDatabase() { String mappingId = SECHUB_MAPPING_ID_2; diff --git a/sechub-scan-product-pds/src/test/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSTemplateMetaDataServiceTest.java b/sechub-scan-product-pds/src/test/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSTemplateMetaDataServiceTest.java new file mode 100644 index 0000000000..1d5d9ad1d1 --- /dev/null +++ b/sechub-scan-product-pds/src/test/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSTemplateMetaDataServiceTest.java @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.scan.product.pds; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.mercedesbenz.sechub.commons.core.ConfigurationFailureException; +import com.mercedesbenz.sechub.commons.model.ScanType; +import com.mercedesbenz.sechub.commons.model.SecHubConfigurationModel; +import com.mercedesbenz.sechub.commons.model.template.TemplateDefinition; +import com.mercedesbenz.sechub.commons.model.template.TemplateDefinition.TemplateVariable; +import com.mercedesbenz.sechub.commons.model.template.TemplateType; +import com.mercedesbenz.sechub.commons.pds.data.PDSTemplateMetaData; +import com.mercedesbenz.sechub.commons.pds.data.PDSTemplateMetaData.PDSAssetData; +import com.mercedesbenz.sechub.domain.scan.asset.AssetDetailData; +import com.mercedesbenz.sechub.domain.scan.asset.AssetFileData; +import com.mercedesbenz.sechub.domain.scan.asset.AssetService; +import com.mercedesbenz.sechub.sharedkernel.error.NotFoundException; + +class PDSTemplateMetaDataServiceTest { + private PDSTemplateMetaDataService serviceToTest; + private AssetService assetService; + private RelevantScanTemplateDefinitionFilter filter; + + @BeforeEach + void beforeEach() { + + filter = mock(); + assetService = mock(); + + serviceToTest = new PDSTemplateMetaDataService(); + serviceToTest.filter = filter; + serviceToTest.assetService = assetService; + } + + @Test + void createTemplateMetaData_returns_pds_template_meta_data_when_asset_is_available() throws Exception { + + /* prepare */ + ScanType exampleScanType = ScanType.WEB_SCAN; + TemplateType exampleTemplateType = TemplateType.WEBSCAN_LOGIN; + String exampleTemplateId1 = "template_id_1"; + String exampleChecksum1 = "checksum1"; + String exampleAssetId1 = "asset1"; + + SecHubConfigurationModel configuration = mock(); + + // prepare template data + List givenDefinitions = mock(); + List filteredDefinitions = new ArrayList<>(); + TemplateVariable variable1 = new TemplateVariable(); + variable1.setName("var1"); + + TemplateDefinition templateDefinition1 = new TemplateDefinition(); + templateDefinition1.setAssetId(exampleAssetId1); + templateDefinition1.setId(exampleTemplateId1); + templateDefinition1.setType(exampleTemplateType); + + templateDefinition1.getVariables().add(variable1); + filteredDefinitions.add(templateDefinition1); + + when(filter.filter(givenDefinitions, exampleScanType, configuration)).thenReturn(filteredDefinitions); + + // prepare asset data + AssetFileData assetFile1a = new AssetFileData(); + assetFile1a.setChecksum("other"); + assetFile1a.setFileName("other_product_id.zip"); + AssetDetailData assetDetailData1 = mock(); + + AssetFileData assetFile1b = new AssetFileData(); + assetFile1b.setChecksum(exampleChecksum1); + assetFile1b.setFileName("test_product_id.zip"); + + List assetfiles = new ArrayList<>(); + assetfiles.add(assetFile1a); + assetfiles.add(assetFile1b); + + when(assetDetailData1.getFiles()).thenReturn(assetfiles); + when(assetService.fetchAssetDetails(exampleAssetId1)).thenReturn(assetDetailData1); + + /* execute */ + List result = serviceToTest.createTemplateMetaData(givenDefinitions, "test_product_id", exampleScanType, configuration); + + /* test */ + PDSTemplateMetaData expectedTemplateMetaData1 = new PDSTemplateMetaData(); + expectedTemplateMetaData1.setTemplateId(exampleTemplateId1); + expectedTemplateMetaData1.setTemplateType(exampleTemplateType); + + PDSAssetData assetData1 = new PDSAssetData(); + assetData1.setAssetId(exampleAssetId1); + assetData1.setChecksum(exampleChecksum1); + assetData1.setFileName("test_product_id.zip"); + + expectedTemplateMetaData1.setAssetData(assetData1); + + assertThat(result).isNotNull().contains(expectedTemplateMetaData1).hasSize(1); + } + + @Test + void createTemplateMetaData_returns_empty_pds_template_meta_data_when_definition_list_is_empty() throws Exception { + + /* prepare */ + ScanType exampleScanType = ScanType.WEB_SCAN; + + SecHubConfigurationModel configuration = mock(); + + // prepare template data + List givenDefinitions = mock(); + List filteredDefinitions = new ArrayList<>(); + + when(filter.filter(givenDefinitions, exampleScanType, configuration)).thenReturn(filteredDefinitions); + + /* execute */ + List result = serviceToTest.createTemplateMetaData(givenDefinitions, "test_product_id", exampleScanType, configuration); + + /* test */ + assertThat(result).isNotNull().isEmpty(); + } + + @Test + void createTemplateMetaData_throws_exception_when_asset_not_found() throws Exception { + + /* prepare */ + ScanType exampleScanType = ScanType.WEB_SCAN; + TemplateType exampleTemplateType = TemplateType.WEBSCAN_LOGIN; + String exampleTemplateId1 = "template_id_1"; + String exampleAssetId1 = "asset1"; + + SecHubConfigurationModel configuration = mock(); + + // prepare template data + List givenDefinitions = mock(); + List filteredDefinitions = new ArrayList<>(); + TemplateVariable variable1 = new TemplateVariable(); + variable1.setName("var1"); + + TemplateDefinition templateDefinition1 = new TemplateDefinition(); + templateDefinition1.setAssetId(exampleAssetId1); + templateDefinition1.setId(exampleTemplateId1); + templateDefinition1.setType(exampleTemplateType); + + templateDefinition1.getVariables().add(variable1); + filteredDefinitions.add(templateDefinition1); + + when(filter.filter(givenDefinitions, exampleScanType, configuration)).thenReturn(filteredDefinitions); + + when(assetService.fetchAssetDetails(exampleAssetId1)).thenThrow(NotFoundException.class); + + /* execute + test */ + assertThatThrownBy(() -> serviceToTest.createTemplateMetaData(givenDefinitions, "test_product_id", exampleScanType, configuration)) + .isInstanceOf(ConfigurationFailureException.class).hasMessageContaining(exampleAssetId1 + " does not exist"); + } + + @Test + void createTemplateMetaData_throws_exception_when_asset_product_file_not_found() throws Exception { + + /* prepare */ + ScanType exampleScanType = ScanType.WEB_SCAN; + TemplateType exampleTemplateType = TemplateType.WEBSCAN_LOGIN; + String exampleTemplateId1 = "template_id_1"; + String exampleAssetId1 = "asset1"; + + SecHubConfigurationModel configuration = mock(); + + // prepare template data + List givenDefinitions = mock(); + List filteredDefinitions = new ArrayList<>(); + TemplateVariable variable1 = new TemplateVariable(); + variable1.setName("var1"); + + TemplateDefinition templateDefinition1 = new TemplateDefinition(); + templateDefinition1.setAssetId(exampleAssetId1); + templateDefinition1.setId(exampleTemplateId1); + templateDefinition1.setType(exampleTemplateType); + + templateDefinition1.getVariables().add(variable1); + filteredDefinitions.add(templateDefinition1); + + when(filter.filter(givenDefinitions, exampleScanType, configuration)).thenReturn(filteredDefinitions); + // prepare asset data + AssetFileData assetFile1a = new AssetFileData(); + assetFile1a.setChecksum("other"); + assetFile1a.setFileName("other_product_id.zip"); + AssetDetailData assetDetailData1 = mock(); + + List assetfiles = new ArrayList<>(); + assetfiles.add(assetFile1a); + + when(assetDetailData1.getFiles()).thenReturn(assetfiles); + when(assetService.fetchAssetDetails(exampleAssetId1)).thenReturn(assetDetailData1); + + /* execute + test */ + assertThatThrownBy(() -> serviceToTest.createTemplateMetaData(givenDefinitions, "test_product_id", exampleScanType, configuration)) + .isInstanceOf(ConfigurationFailureException.class).hasMessageContaining("does not contain file 'test_product_id.zip'"); + ; + } + + @Test + void ensureTemplateAssetFilesAreAvailableInStorage_calls_asste_service_for_each_metadata_assetfile() throws Exception { + + /* prepare */ + PDSAssetData assetData1 = new PDSAssetData(); + assetData1.setAssetId("asset1"); + assetData1.setFileName("file1.txt"); + + PDSTemplateMetaData templateMetaData1 = new PDSTemplateMetaData(); + templateMetaData1.setAssetData(assetData1); + + PDSAssetData assetData2 = new PDSAssetData(); + assetData2.setAssetId("asset2"); + assetData2.setFileName("file2.txt"); + + PDSTemplateMetaData templateMetaData2 = new PDSTemplateMetaData(); + templateMetaData2.setAssetData(assetData2); + + List metaDataList = new ArrayList<>(); + metaDataList.add(templateMetaData1); + metaDataList.add(templateMetaData2); + + /* execute */ + serviceToTest.ensureTemplateAssetFilesAreAvailableInStorage(metaDataList); + + /* test */ + verify(assetService).ensureAssetFileInStorageAvailableAndHasSameChecksumAsInDatabase("file1.txt", "asset1"); + verify(assetService).ensureAssetFileInStorageAvailableAndHasSameChecksumAsInDatabase("file2.txt", "asset2"); + } + +} diff --git a/sechub-scan-product-pds/src/test/java/com/mercedesbenz/sechub/domain/scan/product/pds/RelevantScanTemplateDefinitionFilterTest.java b/sechub-scan-product-pds/src/test/java/com/mercedesbenz/sechub/domain/scan/product/pds/RelevantScanTemplateDefinitionFilterTest.java new file mode 100644 index 0000000000..365d9ab202 --- /dev/null +++ b/sechub-scan-product-pds/src/test/java/com/mercedesbenz/sechub/domain/scan/product/pds/RelevantScanTemplateDefinitionFilterTest.java @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.scan.product.pds; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.EnumSource.Mode; +import org.junit.jupiter.params.provider.NullSource; + +import com.mercedesbenz.sechub.commons.model.ScanType; +import com.mercedesbenz.sechub.commons.model.SecHubConfigurationModel; +import com.mercedesbenz.sechub.commons.model.template.TemplateData; +import com.mercedesbenz.sechub.commons.model.template.TemplateDataResolver; +import com.mercedesbenz.sechub.commons.model.template.TemplateDefinition; +import com.mercedesbenz.sechub.commons.model.template.TemplateType; + +class RelevantScanTemplateDefinitionFilterTest { + private RelevantScanTemplateDefinitionFilter filterToTest; + private SecHubConfigurationModel configuration; + private TemplateDataResolver templateDataResolver; + private TemplateDefinition defininition1WebScanLogin; + private TemplateDefinition defininition2NoTemplateType; + private List templateDefinitions; + + @BeforeEach + void beforeEach() { + templateDataResolver = mock(); + configuration = mock(); + + filterToTest = new RelevantScanTemplateDefinitionFilter(); + filterToTest.templateDataResolver = templateDataResolver; + + defininition1WebScanLogin = new TemplateDefinition(); + defininition1WebScanLogin.setType(TemplateType.WEBSCAN_LOGIN); + + defininition2NoTemplateType = new TemplateDefinition(); + defininition2NoTemplateType.setType(null); + + templateDefinitions = new ArrayList<>(2); + templateDefinitions.add(defininition1WebScanLogin); + templateDefinitions.add(defininition2NoTemplateType); + + } + + @Test + void webscan_login_definition_inside_result_when_resolver_finds_template_data_and_scan_type_is_web_scan() { + /* prepare */ + TemplateData templateData = mock(); + + when(templateDataResolver.resolveTemplateData(TemplateType.WEBSCAN_LOGIN, configuration)).thenReturn(templateData); + + /* execute */ + List result = filterToTest.filter(templateDefinitions, ScanType.WEB_SCAN, configuration); + + /* test */ + assertThat(result).contains(defininition1WebScanLogin).doesNotContain(defininition2NoTemplateType).hasSize(1); + } + + @ParameterizedTest + @EnumSource(value = ScanType.class, mode = Mode.EXCLUDE, names = "WEB_SCAN") + @NullSource + void webscan_login_definition_not_inside_result_when_resolver_finds_template_data_but_scan_type_is_not_web_scan(ScanType scanType) { + /* prepare */ + TemplateData templateData = mock(); + + when(templateDataResolver.resolveTemplateData(TemplateType.WEBSCAN_LOGIN, configuration)).thenReturn(templateData); + + /* execute */ + List result = filterToTest.filter(templateDefinitions, scanType, configuration); + + /* test */ + assertThat(result).isEmpty(); + } + + @ParameterizedTest + @EnumSource(value = ScanType.class) + @NullSource + void no_login_definition_inside_result_when_resolver_does_not_find_template_data(ScanType scanType) { + /* prepare */ + when(templateDataResolver.resolveTemplateData(TemplateType.WEBSCAN_LOGIN, configuration)).thenReturn(null); + + /* execute */ + List result = filterToTest.filter(templateDefinitions, scanType, configuration); + + /* test */ + assertThat(result).isEmpty(); + } + +} diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/adapter/DefaultExecutorConfigSupport.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/adapter/DefaultExecutorConfigSupport.java index 63abcbeae6..ec6eac4871 100644 --- a/sechub-scan/src/main/java/com/mercedesbenz/sechub/adapter/DefaultExecutorConfigSupport.java +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/adapter/DefaultExecutorConfigSupport.java @@ -14,6 +14,7 @@ import com.mercedesbenz.sechub.commons.mapping.NamePatternIdProvider; import com.mercedesbenz.sechub.commons.mapping.NamePatternIdProviderFactory; import com.mercedesbenz.sechub.commons.model.SecHubRuntimeException; +import com.mercedesbenz.sechub.domain.scan.product.ProductExecutorContext; import com.mercedesbenz.sechub.domain.scan.product.config.ProductExecutorConfig; import com.mercedesbenz.sechub.domain.scan.product.config.ProductExecutorConfigSetupCredentials; import com.mercedesbenz.sechub.domain.scan.product.config.ProductExecutorConfigSetupJobParameter; @@ -41,12 +42,13 @@ public class DefaultExecutorConfigSupport { NamePatternIdProviderFactory providerFactory; - public DefaultExecutorConfigSupport(ProductExecutorConfig config, SystemEnvironmentVariableSupport variableSupport, + public DefaultExecutorConfigSupport(ProductExecutorContext context, SystemEnvironmentVariableSupport variableSupport, Validation validation) { - notNull(config, "config may not be null!"); + notNull(context, "context may not be null!"); + notNull(context.getExecutorConfig(), "executor config may not be null!"); notNull(variableSupport, "variableSupport may not be null!"); - this.config = config; + this.config = context.getExecutorConfig(); this.variableSupport = variableSupport; providerFactory = new NamePatternIdProviderFactory(); diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/DefaultAdapterConfigurationStrategy.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/DefaultAdapterConfigurationStrategy.java index 128cd891bf..3f97199e3f 100644 --- a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/DefaultAdapterConfigurationStrategy.java +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/DefaultAdapterConfigurationStrategy.java @@ -5,6 +5,7 @@ import com.mercedesbenz.sechub.adapter.AdapterConfigBuilder; import com.mercedesbenz.sechub.adapter.AdapterConfigurationStrategy; import com.mercedesbenz.sechub.adapter.DefaultExecutorConfigSupport; +import com.mercedesbenz.sechub.commons.core.ConfigurationFailureException; import com.mercedesbenz.sechub.commons.model.ScanType; import com.mercedesbenz.sechub.domain.scan.product.ProductExecutorData; @@ -46,7 +47,7 @@ public DefaultAdapterConfigurationStrategy(ProductExecutorData data, DefaultExec } @Override - public void configure(B configBuilder) { + public void configure(B configBuilder) throws ConfigurationFailureException { /* @formatter:off */ SecHubExecutionContext context = data.getSechubExecutionContext(); String projectId = context.getConfiguration().getProjectId(); diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/IntegrationTestScanRestController.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/IntegrationTestScanRestController.java index 2568951e77..d949f38dd4 100644 --- a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/IntegrationTestScanRestController.java +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/IntegrationTestScanRestController.java @@ -37,6 +37,8 @@ import com.mercedesbenz.sechub.domain.scan.product.config.ProductExecutionProfileRepository; import com.mercedesbenz.sechub.domain.scan.product.config.ProductExecutorConfigInfo; import com.mercedesbenz.sechub.domain.scan.product.config.WithoutProductExecutorConfigInfo; +import com.mercedesbenz.sechub.domain.scan.project.ScanProjectConfig; +import com.mercedesbenz.sechub.domain.scan.project.ScanProjectConfigRepository; import com.mercedesbenz.sechub.domain.scan.report.ScanReportCountService; import com.mercedesbenz.sechub.sharedkernel.ProductIdentifier; import com.mercedesbenz.sechub.sharedkernel.Profiles; @@ -92,6 +94,9 @@ public class IntegrationTestScanRestController { @Autowired private ScanConfigService scanConfigService; + @Autowired + private ScanProjectConfigRepository scanProjectConfigRepository; + @RequestMapping(path = APIConstants.API_ANONYMOUS + "integrationtest/autocleanup/inspection/scan/days", method = RequestMethod.GET, produces = { MediaType.APPLICATION_JSON_VALUE }) public long fetchScheduleAutoCleanupConfiguredDays() { @@ -262,4 +267,9 @@ public List getPDSJobUUIDSForSecHubJOob(@PathVariable("jobUUID") UUID sech return list; } + @RequestMapping(path = APIConstants.API_ANONYMOUS + "integrationtest/project-scanconfig/{projectId}", method = RequestMethod.GET) + public List fetchScanConfigValue(@PathVariable("projectId") String projectId) { + return scanProjectConfigRepository.findAllForProject(projectId); + } + } diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/ProjectDataDeleteService.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/ProjectDataDeleteService.java index 2580f42855..fc4bb67041 100644 --- a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/ProjectDataDeleteService.java +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/ProjectDataDeleteService.java @@ -59,6 +59,10 @@ public void deleteAllDataForProject(String projectId) { productResultRepository.deleteAllResultsForProject(projectId); scanReportRepository.deleteAllReportsForProject(projectId); scanLogRepository.deleteAllLogDataForProject(projectId); + /* + * next line deletes any project related configuration - this includes template + * assignment + */ scanProjectConfigRepository.deleteAllConfigurationsForProject(projectId); profileRepository.deleteAllProfileRelationsToProject(projectId); diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/ScanJobExecutionRunnable.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/ScanJobExecutionRunnable.java index c102279096..8595244d30 100644 --- a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/ScanJobExecutionRunnable.java +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/ScanJobExecutionRunnable.java @@ -14,6 +14,7 @@ import com.mercedesbenz.sechub.domain.scan.product.WebScanProductExecutionService; import com.mercedesbenz.sechub.sharedkernel.LogConstants; import com.mercedesbenz.sechub.sharedkernel.Step; +import com.mercedesbenz.sechub.sharedkernel.usecases.job.UseCaseSchedulerStartsJob; import com.mercedesbenz.sechub.sharedkernel.usecases.other.UseCaseSystemSuspendsJobsWhenSigTermReceived; /** @@ -36,6 +37,13 @@ public ScanJobRunnableData getRunnableData() { } @Override + @UseCaseSchedulerStartsJob(@Step(number = 3, name = "Runnable calls execution services", + + description = """ + The job execution runnable creates the execution context and calls dedicated execution services + (preparation, analytics, product execution, storage and reporting) synchronously for the job. + It is also responsible for cancelation and supsension of jobs. + """)) public void run() { /* runs in own thread so we set job uuid to MDC here ! */ try { diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/ScanMessageHandler.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/ScanMessageHandler.java index e2cbade215..0f7143a77c 100644 --- a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/ScanMessageHandler.java +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/ScanMessageHandler.java @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT package com.mercedesbenz.sechub.domain.scan; +import java.util.Set; import java.util.UUID; import org.slf4j.Logger; @@ -16,6 +17,7 @@ import com.mercedesbenz.sechub.domain.scan.config.UpdateScanMappingConfigurationService; import com.mercedesbenz.sechub.domain.scan.product.ProductResultService; import com.mercedesbenz.sechub.domain.scan.project.ScanProjectConfigAccessLevelService; +import com.mercedesbenz.sechub.domain.scan.template.TemplateService; import com.mercedesbenz.sechub.sharedkernel.Step; import com.mercedesbenz.sechub.sharedkernel.mapping.MappingIdentifier; import com.mercedesbenz.sechub.sharedkernel.mapping.MappingIdentifier.MappingType; @@ -33,6 +35,8 @@ import com.mercedesbenz.sechub.sharedkernel.messaging.SynchronMessageHandler; import com.mercedesbenz.sechub.sharedkernel.messaging.UserMessage; import com.mercedesbenz.sechub.sharedkernel.project.ProjectAccessLevel; +import com.mercedesbenz.sechub.sharedkernel.template.SecHubProjectTemplateData; +import com.mercedesbenz.sechub.sharedkernel.template.SecHubProjectToTemplate; import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdmiUpdatesMappingConfiguration; import com.mercedesbenz.sechub.sharedkernel.usecases.admin.project.UseCaseAdminChangesProjectAccessLevel; @@ -68,6 +72,9 @@ public class ScanMessageHandler implements AsynchronMessageHandler, SynchronMess @Autowired ScanConfigService configService; + @Autowired + TemplateService templateService; + @Override public void receiveAsyncMessage(DomainMessage request) { MessageID messageId = request.getMessageId(); @@ -114,12 +121,47 @@ public DomainMessageSynchronousResult receiveSynchronMessage(DomainMessage reque case REQUEST_PURGE_JOB_RESULTS: return handleJobRestartHardRequested(request); - + case REQUEST_ASSIGN_TEMPLATE_TO_PROJECT: + return handleAssignTemplateToProjectRequest(request); + case REQUEST_UNASSIGN_TEMPLATE_FROM_PROJECT: + return handleUnassignTemplateFromProjectRequest(request); default: throw new IllegalStateException("unhandled message id:" + messageId); } } + @IsRecevingSyncMessage(MessageID.REQUEST_ASSIGN_TEMPLATE_TO_PROJECT) + private DomainMessageSynchronousResult handleAssignTemplateToProjectRequest(DomainMessage request) { + SecHubProjectToTemplate projectToTemplate = request.get(MessageDataKeys.PROJECT_TO_TEMPLATE); + String templateId = projectToTemplate.getTemplateId(); + String projectId = projectToTemplate.getProjectId(); + + try { + templateService.assignTemplateToProject(templateId, projectId); + Set templateIds = templateService.fetchAssignedTemplateIdsForProject(projectId); + return templateAssignmentDone(projectId, templateIds); + } catch (Exception e) { + return templateAssignmentFailed(e); + } + + } + + @IsRecevingSyncMessage(MessageID.REQUEST_UNASSIGN_TEMPLATE_FROM_PROJECT) + private DomainMessageSynchronousResult handleUnassignTemplateFromProjectRequest(DomainMessage request) { + SecHubProjectToTemplate projectToTemplate = request.get(MessageDataKeys.PROJECT_TO_TEMPLATE); + String templateId = projectToTemplate.getTemplateId(); + String projectId = projectToTemplate.getProjectId(); + + try { + templateService.unassignTemplateFromProject(templateId, projectId); + Set templateIds = templateService.fetchAssignedTemplateIdsForProject(projectId); + return templateUnassignmentDone(projectId, templateIds); + } catch (Exception e) { + return templateUnassignmentFailed(e); + } + + } + @IsRecevingSyncMessage(MessageID.REQUEST_PURGE_JOB_RESULTS) private DomainMessageSynchronousResult handleJobRestartHardRequested(DomainMessage request) { UUID jobUUID = request.get(MessageDataKeys.SECHUB_JOB_UUID); @@ -147,6 +189,40 @@ private DomainMessageSynchronousResult purgeDone(UUID jobUUID) { return result; } + @IsSendingSyncMessageAnswer(value = MessageID.RESULT_ASSIGN_TEMPLATE_TO_PROJECT, answeringTo = MessageID.REQUEST_ASSIGN_TEMPLATE_TO_PROJECT, branchName = "success") + private DomainMessageSynchronousResult templateAssignmentDone(String projectId, Set assignedTemplates) { + DomainMessageSynchronousResult result = new DomainMessageSynchronousResult(MessageID.RESULT_ASSIGN_TEMPLATE_TO_PROJECT); + SecHubProjectTemplateData templates = new SecHubProjectTemplateData(); + templates.setProjectId(projectId); + templates.getTemplateIds().addAll(assignedTemplates); + + result.set(MessageDataKeys.PROJECT_TEMPLATES, templates); + return result; + } + + @IsSendingSyncMessageAnswer(value = MessageID.RESULT_ASSIGN_TEMPLATE_TO_PROJECT, answeringTo = MessageID.REQUEST_ASSIGN_TEMPLATE_TO_PROJECT, branchName = "failed") + private DomainMessageSynchronousResult templateAssignmentFailed(Exception failure) { + DomainMessageSynchronousResult result = new DomainMessageSynchronousResult(MessageID.RESULT_ASSIGN_TEMPLATE_TO_PROJECT, failure); + return result; + } + + @IsSendingSyncMessageAnswer(value = MessageID.RESULT_UNASSIGN_TEMPLATE_FROM_PROJECT, answeringTo = MessageID.REQUEST_UNASSIGN_TEMPLATE_FROM_PROJECT, branchName = "success") + private DomainMessageSynchronousResult templateUnassignmentDone(String projectId, Set assignedTemplates) { + DomainMessageSynchronousResult result = new DomainMessageSynchronousResult(MessageID.RESULT_UNASSIGN_TEMPLATE_FROM_PROJECT); + SecHubProjectTemplateData templates = new SecHubProjectTemplateData(); + templates.setProjectId(projectId); + templates.getTemplateIds().addAll(assignedTemplates); + + result.set(MessageDataKeys.PROJECT_TEMPLATES, templates); + return result; + } + + @IsSendingSyncMessageAnswer(value = MessageID.RESULT_UNASSIGN_TEMPLATE_FROM_PROJECT, answeringTo = MessageID.REQUEST_UNASSIGN_TEMPLATE_FROM_PROJECT, branchName = "failed") + private DomainMessageSynchronousResult templateUnassignmentFailed(Exception failure) { + DomainMessageSynchronousResult result = new DomainMessageSynchronousResult(MessageID.RESULT_UNASSIGN_TEMPLATE_FROM_PROJECT, failure); + return result; + } + @IsReceivingAsyncMessage(MessageID.MAPPING_CONFIGURATION_CHANGED) @UseCaseAdmiUpdatesMappingConfiguration(@Step(number = 3, name = "Event handler", description = "Receives mapping configuration change event")) private void handleMappingConfigurationChanged(DomainMessage request) { diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/ScanService.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/ScanService.java index 6ad21e23b4..98f2563fef 100644 --- a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/ScanService.java +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/ScanService.java @@ -26,6 +26,7 @@ import com.mercedesbenz.sechub.domain.scan.report.CreateScanReportService; import com.mercedesbenz.sechub.domain.scan.report.ScanReport; import com.mercedesbenz.sechub.domain.scan.report.ScanReportException; +import com.mercedesbenz.sechub.domain.scan.template.TemplateService; import com.mercedesbenz.sechub.sharedkernel.MustBeDocumented; import com.mercedesbenz.sechub.sharedkernel.ProgressStateFetcher; import com.mercedesbenz.sechub.sharedkernel.Step; @@ -80,6 +81,9 @@ public class ScanService implements SynchronMessageHandler { @Autowired ScanProgressStateFetcherFactory monitorFactory; + @Autowired + TemplateService templateService; + @MustBeDocumented("Define delay in milliseconds, for before next job cancellation check will be executed.") @Value("${sechub.config.check.canceljob.delay:" + DEFAULT_CHECK_CANCELJOB_DELAY_MILLIS + "}") private int millisecondsToWaitBeforeCancelCheck = DEFAULT_CHECK_CANCELJOB_DELAY_MILLIS; @@ -195,7 +199,7 @@ private void cleanupStorage(SecHubExecutionContext context) { } - private SecHubExecutionContext createExecutionContext(DomainMessage message) throws JSONConverterException { + SecHubExecutionContext createExecutionContext(DomainMessage message) throws JSONConverterException { UUID executionUUID = message.get(SECHUB_EXECUTION_UUID); UUID sechubJobUUID = message.get(SECHUB_JOB_UUID); @@ -206,7 +210,6 @@ private SecHubExecutionContext createExecutionContext(DomainMessage message) thr throw new IllegalStateException("SecHubConfiguration not found in message - so cannot execute!"); } SecHubExecutionContext executionContext = new SecHubExecutionContext(sechubJobUUID, configuration, executedBy, executionUUID); - buildOptions(executionContext); return executionContext; @@ -224,6 +227,10 @@ private void buildOptions(SecHubExecutionContext executionContext) { ScanProjectMockDataConfiguration mockDataConfig = ScanProjectMockDataConfiguration.fromString(data); executionContext.putData(ScanKey.PROJECT_MOCKDATA_CONFIGURATION, mockDataConfig); } + + /* append template definitions */ + executionContext.getTemplateDefinitions().addAll(templateService.fetchAllTemplateDefinitionsForProject(projectId)); + } @Override diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/SecHubExecutionContext.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/SecHubExecutionContext.java index 43f2b7621c..f93e6969d2 100644 --- a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/SecHubExecutionContext.java +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/SecHubExecutionContext.java @@ -2,13 +2,16 @@ package com.mercedesbenz.sechub.domain.scan; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.UUID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.mercedesbenz.sechub.commons.model.template.TemplateDefinition; import com.mercedesbenz.sechub.domain.scan.product.ProductExecutor; import com.mercedesbenz.sechub.domain.scan.product.ProductExecutorData; import com.mercedesbenz.sechub.sharedkernel.TypedKey; @@ -50,6 +53,8 @@ public class SecHubExecutionContext { private boolean suspended; + private List templateDefinitions = new ArrayList<>(); + public SecHubExecutionContext(UUID sechubJobUUID, SecHubConfiguration configuration, String executedBy, UUID executionUUID) { this(sechubJobUUID, configuration, executedBy, executionUUID, null); } @@ -191,4 +196,8 @@ public boolean isSuspended() { return suspended; } + public List getTemplateDefinitions() { + return templateDefinitions; + } + } diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/asset/AssetDetailData.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/asset/AssetDetailData.java new file mode 100644 index 0000000000..79942f30f8 --- /dev/null +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/asset/AssetDetailData.java @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.scan.asset; + +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.mercedesbenz.sechub.commons.model.JSONable; + +@JsonIgnoreProperties(ignoreUnknown = true) // we do ignore to avoid problems from wrong configured values! +public class AssetDetailData implements JSONable { + + private String assetId; + + private List files = new ArrayList<>(); + + public void setAssetId(String assetid) { + this.assetId = assetid; + } + + public String getAssetId() { + return assetId; + } + + public List getFiles() { + return files; + } + + @Override + public Class getJSONTargetClass() { + return AssetDetailData.class; + } +} diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/asset/AssetFile.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/asset/AssetFile.java new file mode 100644 index 0000000000..9d912b89e6 --- /dev/null +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/asset/AssetFile.java @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.scan.asset; + +import java.io.Serializable; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import jakarta.persistence.Version; + +/** + * Represents a template + * + * @author Albert Tregnaghi + * + */ +@Entity +@Table(name = AssetFile.TABLE_NAME) +public class AssetFile { + + /* +-----------------------------------------------------------------------+ */ + /* +............................ SQL ......................................+ */ + /* +-----------------------------------------------------------------------+ */ + public static final String TABLE_NAME = "SCAN_ASSET_FILE"; + + public static final String COLUMN_ASSET_ID = "ASSET_ID"; + public static final String COLUMN_FILE_NAME = "FILE_NAME"; + public static final String COLUMN_DATA = "DATA"; + public static final String COLUMN_CHECKSUM = "CHECKSUM"; + + /* +-----------------------------------------------------------------------+ */ + /* +............................ JPQL .....................................+ */ + /* +-----------------------------------------------------------------------+ */ + public static final String CLASS_NAME = "AssetFile"; + + @EmbeddedId + AssetFileCompositeKey key; + + @Column(name = COLUMN_CHECKSUM) + String checksum; + + @Column(name = COLUMN_DATA) + byte[] data; + + @Version + @Column(name = "VERSION") + @JsonIgnore + Integer version; + + AssetFile() { + // jpa only + } + + public AssetFile(AssetFileCompositeKey key) { + this.key = key; + } + + public AssetFileCompositeKey getKey() { + return key; + } + + public void setChecksum(String definition) { + this.checksum = definition; + } + + public String getChecksum() { + return checksum; + } + + public void setData(byte[] data) { + this.data = data; + } + + public byte[] getData() { + return data; + } + + /** + * Asset id and file name or only strings. To avoid confusion in constructor + * usage, this builder was introduced. + * + * @author Albert Tregnaghi + * + */ + public static class AssetFileCompositeKeyBuilder { + + private String assetId; + private String fileName; + + public AssetFileCompositeKey build() { + + if (assetId == null) { + throw new IllegalStateException("asset id not defined!"); + } + if (fileName == null) { + throw new IllegalStateException("file name not defined!"); + } + + AssetFileCompositeKey key = new AssetFileCompositeKey(); + key.setAssetId(assetId); + key.setFileName(fileName); + + return key; + } + + public AssetFileCompositeKeyBuilder assetId(String assetId) { + this.assetId = assetId; + return this; + } + + public AssetFileCompositeKeyBuilder fileName(String fileName) { + this.fileName = fileName; + return this; + } + } + + @Embeddable + public static class AssetFileCompositeKey implements Serializable { + + public static AssetFileCompositeKeyBuilder builder() { + return new AssetFileCompositeKeyBuilder(); + } + + private static final long serialVersionUID = 8753389792382752253L; + + @Column(name = COLUMN_ASSET_ID, nullable = false) + private String assetId; + + @Column(name = COLUMN_FILE_NAME, nullable = false) + private String fileName; + + AssetFileCompositeKey() { + // jpa only + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String userId) { + this.fileName = userId; + } + + public String getAssetId() { + return assetId; + } + + public void setAssetId(String projectId) { + this.assetId = projectId; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((assetId == null) ? 0 : assetId.hashCode()); + result = prime * result + ((fileName == null) ? 0 : fileName.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + AssetFileCompositeKey other = (AssetFileCompositeKey) obj; + if (assetId == null) { + if (other.assetId != null) + return false; + } else if (!assetId.equals(other.assetId)) + return false; + if (fileName == null) { + if (other.fileName != null) + return false; + } else if (!fileName.equals(other.fileName)) + return false; + return true; + } + } + + @Override + public int hashCode() { + return Objects.hash(key); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + AssetFile other = (AssetFile) obj; + return Objects.equals(key, other.key); + } + + @Override + public String toString() { + return "AssetFile [" + (key != null ? "key=" + key + ", " : "") + (checksum != null ? "checksum=" + checksum + ", " : "") + + (version != null ? "version=" + version : "") + "]"; + } +} diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/asset/AssetFileData.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/asset/AssetFileData.java new file mode 100644 index 0000000000..6e18327e57 --- /dev/null +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/asset/AssetFileData.java @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.scan.asset; + +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) // we do ignore to avoid problems from wrong configured values! +public class AssetFileData { + + private String fileName; + + private String checksum; + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public String getChecksum() { + return checksum; + } + + public void setChecksum(String checksum) { + this.checksum = checksum; + } + + @Override + public int hashCode() { + return Objects.hash(checksum, fileName); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + AssetFileData other = (AssetFileData) obj; + return Objects.equals(checksum, other.checksum) && Objects.equals(fileName, other.fileName); + } + + @Override + public String toString() { + return "AssetFileInformation [" + (checksum != null ? "checksum=" + checksum + ", " : "") + (fileName != null ? "fileName=" + fileName : "") + "]"; + } + +} diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/asset/AssetFileRepository.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/asset/AssetFileRepository.java new file mode 100644 index 0000000000..b6394bac30 --- /dev/null +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/asset/AssetFileRepository.java @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.scan.asset; + +import static com.mercedesbenz.sechub.domain.scan.asset.AssetFile.*; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import com.mercedesbenz.sechub.domain.scan.asset.AssetFile.AssetFileCompositeKey; + +public interface AssetFileRepository extends JpaRepository { + + @Query(value = "SELECT DISTINCT " + COLUMN_ASSET_ID + " FROM " + TABLE_NAME, nativeQuery = true) + List fetchAllAssetIds(); + + @Query(value = "SELECT * from " + TABLE_NAME + " WHERE " + COLUMN_ASSET_ID + "=:assetId", nativeQuery = true) + List fetchAllAssetFilesWithAssetId(@Param("assetId") String assetId); + + @Modifying + @Query(value = "DELETE from " + TABLE_NAME + " WHERE " + COLUMN_ASSET_ID + "=:assetId", nativeQuery = true) + void deleteAssetFilesHavingAssetId(@Param("assetId") String assetId); + +} diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/asset/AssetRestController.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/asset/AssetRestController.java new file mode 100644 index 0000000000..c4f0b1f83d --- /dev/null +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/asset/AssetRestController.java @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.scan.asset; + +import static com.mercedesbenz.sechub.commons.core.CommonConstants.*; + +import java.io.IOException; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import com.mercedesbenz.sechub.sharedkernel.Profiles; +import com.mercedesbenz.sechub.sharedkernel.Step; +import com.mercedesbenz.sechub.sharedkernel.logging.AuditLogService; +import com.mercedesbenz.sechub.sharedkernel.logging.LogSanitizer; +import com.mercedesbenz.sechub.sharedkernel.security.APIConstants; +import com.mercedesbenz.sechub.sharedkernel.security.RoleConstants; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminDeletesAssetCompletely; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminDeletesOneFileFromAsset; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminDownloadsAssetFile; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminFetchesAssetDetails; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminFetchesAssetIds; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminUploadsAssetFile; + +import jakarta.annotation.security.RolesAllowed; +import jakarta.servlet.http.HttpServletResponse; + +@RestController +@EnableAutoConfiguration +@RequestMapping(APIConstants.API_ADMINISTRATION) +@RolesAllowed({ RoleConstants.ROLE_SUPERADMIN }) +@Profile(Profiles.ADMIN_ACCESS) +public class AssetRestController { + + @Autowired + AssetService assetService; + + @Autowired + AuditLogService auditLogService; + + @Autowired + LogSanitizer logSanitizer; + + /* @formatter:off */ + @UseCaseAdminUploadsAssetFile(@Step(number = 1, next = 2, name = "REST API call to upload a file for an asset", needsRestDoc = true)) + @RequestMapping(path = "/asset/{assetId}/file", method = RequestMethod.POST) + @ResponseStatus(HttpStatus.OK) + /* @formatter:on */ + public void uploadAssetFile(@PathVariable("assetId") String assetId, @RequestPart(MULTIPART_FILE) MultipartFile file, + @RequestParam(MULTIPART_CHECKSUM) String checkSum) { + + auditLogService.log("starts upload of file:{} for asset: {}", logSanitizer.sanitize(file.getOriginalFilename(), 100), + logSanitizer.sanitize(assetId, 40)); + + assetService.uploadAssetFile(assetId, file, checkSum); + + } + + /* @formatter:off */ + @UseCaseAdminDownloadsAssetFile(@Step(number = 1, next = 2, name = "REST API call to download a file for an asset", needsRestDoc = true)) + @RequestMapping(path = "/asset/{assetId}/file/{fileName}", method = RequestMethod.GET, produces = {MediaType.APPLICATION_OCTET_STREAM_VALUE, MediaType.ALL_VALUE}) + @ResponseStatus(HttpStatus.OK) + /* @formatter:on */ + public void downloadAssetFile(@PathVariable("assetId") String assetId, @PathVariable("fileName") String fileName, HttpServletResponse response) + throws IOException { + + auditLogService.log("starts download of file:{} for asset: {}", logSanitizer.sanitize(fileName, 100), logSanitizer.sanitize(assetId, 40)); + + assetService.downloadAssetFile(assetId, fileName, response.getOutputStream()); + + } + + /* @formatter:off */ + @UseCaseAdminFetchesAssetIds(@Step(number = 1, next = 2, name = "REST API call to fetch all availbale asset ids", needsRestDoc = true)) + @RequestMapping(path = "/asset/ids", method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE}) + @ResponseStatus(HttpStatus.OK) + /* @formatter:on */ + public List fetchAllAssetIds() { + return assetService.fetchAllAssetIds(); + } + + /* @formatter:off */ + @UseCaseAdminFetchesAssetDetails(@Step(number = 1, next = 2, name = "REST API call to fetch details about asset", needsRestDoc = true)) + @RequestMapping(path = "/asset/{assetId}/details", method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE}) + @ResponseStatus(HttpStatus.OK) + /* @formatter:on */ + public AssetDetailData fetchAssetDetails(@PathVariable("assetId") String assetId) { + return assetService.fetchAssetDetails(assetId); + } + + /* @formatter:off */ + @UseCaseAdminDeletesOneFileFromAsset(@Step(number = 1, next = 2, name = "REST API call to delete an asset file", needsRestDoc = true)) + @RequestMapping(path = "/asset/{assetId}/file/{fileName}", method = RequestMethod.DELETE) + @ResponseStatus(HttpStatus.OK) + /* @formatter:on */ + public void deleteAssetFile(@PathVariable("assetId") String assetId, @PathVariable("fileName") String fileName) throws IOException { + assetService.deleteAssetFile(assetId, fileName); + } + + /* @formatter:off */ + @UseCaseAdminDeletesAssetCompletely(@Step(number = 1, next = 2, name = "REST API call to delete complete asset", needsRestDoc = true)) + @RequestMapping(path = "/asset/{assetId}", method = RequestMethod.DELETE) + @ResponseStatus(HttpStatus.OK) + /* @formatter:on */ + public void deleteAsset(@PathVariable("assetId") String assetId) throws IOException { + assetService.deleteAsset(assetId); + } +} diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/asset/AssetService.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/asset/AssetService.java new file mode 100644 index 0000000000..28965552cb --- /dev/null +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/asset/AssetService.java @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.scan.asset; + +import static com.mercedesbenz.sechub.commons.core.CommonConstants.*; +import static com.mercedesbenz.sechub.sharedkernel.util.Assert.*; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Optional; +import java.util.Scanner; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import com.amazonaws.util.StringInputStream; +import com.mercedesbenz.sechub.commons.core.ConfigurationFailureException; +import com.mercedesbenz.sechub.commons.core.security.CheckSumSupport; +import com.mercedesbenz.sechub.commons.model.SecHubRuntimeException; +import com.mercedesbenz.sechub.domain.scan.asset.AssetFile.AssetFileCompositeKey; +import com.mercedesbenz.sechub.sharedkernel.Step; +import com.mercedesbenz.sechub.sharedkernel.error.BadRequestException; +import com.mercedesbenz.sechub.sharedkernel.error.NotAcceptableException; +import com.mercedesbenz.sechub.sharedkernel.error.NotFoundException; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminDeletesAssetCompletely; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminDeletesOneFileFromAsset; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminDownloadsAssetFile; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminFetchesAssetDetails; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminFetchesAssetIds; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminUploadsAssetFile; +import com.mercedesbenz.sechub.sharedkernel.validation.UserInputAssertion; +import com.mercedesbenz.sechub.storage.core.AssetStorage; +import com.mercedesbenz.sechub.storage.core.StorageService; + +import jakarta.servlet.ServletOutputStream; + +@Service +public class AssetService { + + private static final Logger LOG = LoggerFactory.getLogger(AssetService.class); + + private final AssetFileRepository repository; + + private final UserInputAssertion inputAssertion; + + private final CheckSumSupport checkSumSupport; + + private final StorageService storageService; + + AssetService(AssetFileRepository repository, UserInputAssertion inputAssertion, CheckSumSupport checkSumSupport, StorageService storageService) { + this.repository = repository; + this.inputAssertion = inputAssertion; + this.checkSumSupport = checkSumSupport; + this.storageService = storageService; + } + + @UseCaseAdminDeletesAssetCompletely(@Step(number = 2, name = "Services deletes all asset parts")) + @Transactional + public void deleteAsset(String assetId) throws IOException { + inputAssertion.assertIsValidAssetId(assetId); + + repository.deleteAssetFilesHavingAssetId(assetId); + storageService.createAssetStorage(assetId).deleteAll(); + } + + @UseCaseAdminDeletesOneFileFromAsset(@Step(number = 2, name = "Services deletes file from asset")) + public void deleteAssetFile(String assetId, String fileName) throws IOException { + inputAssertion.assertIsValidAssetId(assetId); + inputAssertion.assertIsValidAssetFileName(fileName); + + repository.deleteById(AssetFileCompositeKey.builder().assetId(assetId).fileName(fileName).build()); + storageService.createAssetStorage(assetId).delete(fileName); + } + + @UseCaseAdminDownloadsAssetFile(@Step(number = 2, name = "Service downloads asset file from database")) + public void downloadAssetFile(String assetId, String fileName, ServletOutputStream outputStream) throws IOException { + inputAssertion.assertIsValidAssetId(assetId); + inputAssertion.assertIsValidAssetFileName(fileName); + + notNull(outputStream, "output stream may not be null!"); + + AssetFile assetFile = assertAssetFileFromDatabase(assetId, fileName); + outputStream.write(assetFile.getData()); + + } + + /** + * Ensures file for asset exists in database and and also in storage (with same + * checksum). If the file is not inside database a {@link NotFoundException} + * will be thrown. If the file is not available in storage, or the checksum in + * storage is different than the checksum from database, the file will stored + * again in storage (with data from database) + * + * @param fileName file name + * @param assetId asset identifier + * @throws ConfigurationFailureException if there are configuration problems + * @throws NotFoundException when the asset or the file is not found + * in database + * + */ + public void ensureAssetFileInStorageAvailableAndHasSameChecksumAsInDatabase(String fileName, String assetId) throws ConfigurationFailureException { + + try (AssetStorage assetStorage = storageService.createAssetStorage(assetId)) { + AssetFile assetFile = assertAssetFileFromDatabase(assetId, fileName); + String checksumFromDatabase = assetFile.getChecksum(); + + if (assetStorage.isExisting(fileName)) { + String checksumFileName = fileName + DOT_CHECKSUM; + if (assetStorage.isExisting(checksumFileName)) { + + String checksumFromStorage = null; + try (InputStream inputStream = assetStorage.fetch(checksumFileName); Scanner scanner = new Scanner(inputStream)) { + checksumFromStorage = scanner.hasNext() ? scanner.next() : ""; + } + if (checksumFromStorage.equals(checksumFromDatabase)) { + LOG.debug("Checksum for file '{}' in asset '{}' is '{}' in database and storage. Can be kept, no recration necessary", fileName, + assetId, checksumFromStorage, checksumFromDatabase); + return; + } + LOG.warn( + "Checksum for file '{}' in asset '{}' was '{}' instead of expected value from database '{}'. Will recreated file and checksum in storage.", + fileName, assetId, checksumFromStorage, checksumFromDatabase); + } else { + LOG.warn("Asset storage for file '{}' in asset '{}' did exist, but checksum did not exist. Will recreated file and checksum in storage.", + fileName, assetId); + } + } else { + LOG.info("Asset storage for file '{}' in asset '{}' does not exist and must be created.", fileName, assetId); + } + storeStream(fileName, checksumFromDatabase, assetStorage, assetFile.getData().length, new ByteArrayInputStream(assetFile.getData())); + + } catch (NotFoundException | IOException e) { + throw new ConfigurationFailureException("Was not able to ensure file " + fileName + " in asset " + assetId, e); + } + + } + + @UseCaseAdminFetchesAssetIds(@Step(number = 2, name = "Service fetches all asset ids from database")) + public List fetchAllAssetIds() { + return repository.fetchAllAssetIds(); + } + + /** + * Fetches asset details (from database) + * + * @param assetId asset identifier + * @return detail data + * @throws NotFoundException when no asset exists for given identifier + */ + @UseCaseAdminFetchesAssetDetails(@Step(number = 2, name = "Service fetches asset details for given asset id")) + public AssetDetailData fetchAssetDetails(String assetId) { + inputAssertion.assertIsValidAssetId(assetId); + + List assetFiles = repository.fetchAllAssetFilesWithAssetId(assetId); + if (assetFiles.isEmpty()) { + throw new NotFoundException("No asset data available for asset id:" + assetId); + } + + AssetDetailData data = new AssetDetailData(); + data.setAssetId(assetId); + for (AssetFile assetFile : assetFiles) { + AssetFileData information = new AssetFileData(); + information.setFileName(assetFile.getKey().getFileName()); + information.setChecksum(assetFile.getChecksum()); + data.getFiles().add(information); + } + + return data; + } + + @UseCaseAdminUploadsAssetFile(@Step(number = 2, name = "Service tries to upload file for asset", description = "Uploaded file will be stored in database and in storage")) + public void uploadAssetFile(String assetId, MultipartFile multipartFile, String checkSum) { + inputAssertion.assertIsValidAssetId(assetId); + + inputAssertion.assertIsValidSha256Checksum(checkSum); + + String fileName = assertAssetFile(multipartFile); + + handleChecksumValidation(fileName, multipartFile, checkSum, assetId); + + try { + /* now store */ + byte[] bytes = multipartFile.getBytes(); + persistFileAndChecksumInDatabase(fileName, bytes, checkSum, assetId); + + ensureAssetFileInStorageAvailableAndHasSameChecksumAsInDatabase(fileName, assetId); + + LOG.info("Successfully uploaded file '{}' for asset '{}'", fileName, assetId); + + } catch (IOException e) { + throw new SecHubRuntimeException("Was not able to upload file '" + fileName + "' for asset '" + assetId + "'", e); + } catch (ConfigurationFailureException e) { + throw new IllegalStateException("A configuration failure should not happen at this point!", e); + } + } + + private String assertAssetFile(MultipartFile file) { + notNull(file, "file may not be null!"); + String fileName = file.getOriginalFilename(); + + inputAssertion.assertIsValidAssetFileName(fileName); + + long fileSize = file.getSize(); + + if (fileSize <= 0) { + throw new BadRequestException("Uploaded asset file may not be empty!"); + } + return fileName; + } + + private AssetFile assertAssetFileFromDatabase(String assetId, String fileName) { + AssetFileCompositeKey key = AssetFileCompositeKey.builder().assetId(assetId).fileName(fileName).build(); + Optional result = repository.findById(key); + if (result.isEmpty()) { + throw new NotFoundException("For asset:" + assetId + " no file with name:" + fileName + " exists!"); + } + AssetFile assetFile = result.get(); + return assetFile; + } + + private void assertCheckSumCorrect(String checkSum, InputStream inputStream) { + if (!checkSumSupport.hasCorrectSha256Checksum(checkSum, inputStream)) { + LOG.error("Uploaded file has incorrect sha256 checksum! Something must have happened during the upload."); + throw new NotAcceptableException("Sourcecode checksum check failed"); + } + } + + private String createFileNameForChecksum(String fileName) { + return fileName + DOT_CHECKSUM; + } + + private void handleChecksumValidation(String fileName, MultipartFile file, String checkSum, String assetid) { + try (InputStream inputStream = file.getInputStream()) { + /* validate */ + assertCheckSumCorrect(checkSum, inputStream); + + } catch (IOException e) { + LOG.error("Was not able to validate uploaded file checksum for file '{}' in asset '{}'", fileName, assetid, e); + throw new SecHubRuntimeException("Was not able to validate uploaded asset checksum"); + } + } + + private void persistFileAndChecksumInDatabase(String fileName, byte[] bytes, String checkSum, String assetId) throws IOException { + /* delete if exists */ + AssetFileCompositeKey key = AssetFileCompositeKey.builder().assetId(assetId).fileName(fileName).build(); + repository.deleteById(key); + + AssetFile assetFile = new AssetFile(key); + assetFile.setChecksum(checkSum); + assetFile.setData(bytes); + + repository.save(assetFile); + } + + private void storeStream(String fileName, String checkSum, AssetStorage assetStorage, long fileSize, InputStream inputStream) throws IOException { + assetStorage.store(fileName, inputStream, fileSize); + + long checksumSizeInBytes = checkSum.getBytes().length; + assetStorage.store(createFileNameForChecksum(fileName), new StringInputStream(checkSum), checksumSizeInBytes); + } + +} diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/product/ProductExecutorContextFactory.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/product/ProductExecutorContextFactory.java index 2ff0b373ad..76e0f72049 100644 --- a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/product/ProductExecutorContextFactory.java +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/product/ProductExecutorContextFactory.java @@ -15,12 +15,12 @@ public class ProductExecutorContextFactory { @Autowired ProductResultTransactionService transactionService; - public ProductExecutorContext create(List formerResults, SecHubExecutionContext executionContext, ProductExecutor productExecutor, + public ProductExecutorContext create(List formerResults, SecHubExecutionContext sechubExecutionContext, ProductExecutor productExecutor, ProductExecutorConfig config) { ProductExecutorContext productExecutorContext = new ProductExecutorContext(config, formerResults); - ProductExecutorCallbackImpl callback = new ProductExecutorCallbackImpl(executionContext, productExecutorContext, transactionService); + ProductExecutorCallbackImpl callback = new ProductExecutorCallbackImpl(sechubExecutionContext, productExecutorContext, transactionService); productExecutorContext.callback = callback; productExecutorContext.useFirstFormerResult(); diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/ScanProjectConfig.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/ScanProjectConfig.java index b27366dc5f..ea46fe873e 100644 --- a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/ScanProjectConfig.java +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/ScanProjectConfig.java @@ -40,6 +40,11 @@ public class ScanProjectConfig { /* +-----------------------------------------------------------------------+ */ public static final String CLASS_NAME = ScanProjectConfig.class.getSimpleName(); + public static final String PROPERTY_KEY = "key"; + + public static final String QUERY_FIND_ALL_CONFIGURATIONS_FOR_PROJECT = "SELECT c FROM ScanProjectConfig c where c." + PROPERTY_KEY + "." + + ScanProjectConfigCompositeKey.PROPERTY_PROJECT_ID + " =:projectId"; + @EmbeddedId ScanProjectConfigCompositeKey key; @@ -98,6 +103,8 @@ public static class ScanProjectConfigCompositeKey implements Serializable { private static final long serialVersionUID = 8753389792382752253L; + public static final String PROPERTY_PROJECT_ID = "projectId"; + @Column(name = COLUMN_PROJECT_ID, nullable = false) private String projectId; @@ -169,6 +176,12 @@ public boolean equals(Object obj) { return false; return true; } + + @Override + public String toString() { + return "ScanProjectConfigCompositeKey [" + (projectId != null ? "projectId=" + projectId + ", " : "") + + (configId != null ? "configId=" + configId : "") + "]"; + } } @Override @@ -197,4 +210,9 @@ public boolean equals(Object obj) { return true; } + @Override + public String toString() { + return "ScanProjectConfig [" + (key != null ? "key=" + key + ", " : "") + (data != null ? "data=" + data : "") + "]"; + } + } \ No newline at end of file diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/ScanProjectConfigID.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/ScanProjectConfigID.java index 60a80ea7ac..e968f1bf20 100644 --- a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/ScanProjectConfigID.java +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/ScanProjectConfigID.java @@ -4,6 +4,7 @@ import static com.mercedesbenz.sechub.sharedkernel.util.Assert.*; import com.mercedesbenz.sechub.commons.core.MustBeKeptStable; +import com.mercedesbenz.sechub.commons.model.template.TemplateType; @MustBeKeptStable("You can rename enums, but do not change id parts, because used inside DB!") public enum ScanProjectConfigID { @@ -26,13 +27,15 @@ public enum ScanProjectConfigID { */ PROJECT_ACCESS_LEVEL("project_access_level"), + TEMPLATE_WEBSCAN_LOGIN("template_" + TemplateType.WEBSCAN_LOGIN.name().toLowerCase()), + ; private String id; ScanProjectConfigID(String id) { notNull(id, "config id may not be null!"); - maxLength(id, 20); // because in DB we got only 3x20 defined, so max is 20 + maxLength(id, 60); // in DB we got 3x20 defined, but we have only ascii chars allowed, so 60 is max this.id = id; } diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/ScanProjectConfigRepository.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/ScanProjectConfigRepository.java index 526bfd31b5..2b1deb1f57 100644 --- a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/ScanProjectConfigRepository.java +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/ScanProjectConfigRepository.java @@ -3,9 +3,13 @@ import static com.mercedesbenz.sechub.domain.scan.project.ScanProjectConfig.*; +import java.util.List; +import java.util.Set; + import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import com.mercedesbenz.sechub.domain.scan.project.ScanProjectConfig.ScanProjectConfigCompositeKey; @@ -15,4 +19,11 @@ public interface ScanProjectConfigRepository extends JpaRepository configIds, String value); + + @Query(value = ScanProjectConfig.QUERY_FIND_ALL_CONFIGURATIONS_FOR_PROJECT) + List findAllForProject(@Param("projectId") String projectId); + } diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/ScanProjectConfigService.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/ScanProjectConfigService.java index c15d69cb5d..4a785785f2 100644 --- a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/ScanProjectConfigService.java +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/ScanProjectConfigService.java @@ -2,9 +2,11 @@ package com.mercedesbenz.sechub.domain.scan.project; import java.util.Optional; +import java.util.Set; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import com.mercedesbenz.sechub.domain.scan.ScanAssertService; import com.mercedesbenz.sechub.domain.scan.project.ScanProjectConfig.ScanProjectConfigCompositeKey; @@ -74,10 +76,21 @@ public ScanProjectConfig get(String projectId, ScanProjectConfigID configId, boo } /** - * Set configuration for project (means will persist given change to config) + * Removes project configuration entry * - * @param projectId - * @param configId + * @param projectId project identifier + * @param configId configuration identifier + */ + public void unset(String projectId, ScanProjectConfigID configId) { + set(projectId, configId, null); + } + + /** + * Set configuration for project (means will persist given change to + * configuration) + * + * @param projectId project identifier + * @param configId configuration identifier * @param data when null existing entry will be deleted on * database */ @@ -107,4 +120,9 @@ public void set(String projectId, ScanProjectConfigID configId, String data) { } + @Transactional + public void deleteAllConfigurationsOfGivenConfigIdsAndValue(Set configIds, String value) { + repository.deleteAllConfigurationsOfGivenConfigIdsAndValue(configIds, value); + } + } diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/template/Template.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/template/Template.java new file mode 100644 index 0000000000..c885e3c20a --- /dev/null +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/template/Template.java @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.scan.template; + +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Version; + +/** + * Represents a template + * + * @author Albert Tregnaghi + * + */ +@Entity +@Table(name = Template.TABLE_NAME) +public class Template { + + /* +-----------------------------------------------------------------------+ */ + /* +............................ SQL ......................................+ */ + /* +-----------------------------------------------------------------------+ */ + public static final String TABLE_NAME = "SCAN_TEMPLATE"; + + public static final String COLUMN_TEMPLATE_ID = "TEMPLATE_ID"; + public static final String COLUMN_DEFINITION = "TEMPLATE_DEFINITION"; + + /* +-----------------------------------------------------------------------+ */ + /* +............................ JPQL .....................................+ */ + /* +-----------------------------------------------------------------------+ */ + public static final String CLASS_NAME = "Template"; + + public static final String PROPERTY_ID = "id"; + public static final String PROPERTY_DEFINITION = "definition"; + + public static final String QUERY_All_TEMPLATE_IDS = "select t.id from #{#entityName} t"; + + @Id + @Column(name = COLUMN_TEMPLATE_ID, updatable = false, nullable = false) + String id; + + @Column(name = COLUMN_DEFINITION) + String definition; + + @Version + @Column(name = "VERSION") + @JsonIgnore + Integer version; + + Template() { + // jpa only + } + + public Template(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + public void setDefinition(String definition) { + this.definition = definition; + } + + public String getDefinition() { + return definition; + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Template other = (Template) obj; + return Objects.equals(id, other.id); + } + + @Override + public String toString() { + return "Template [" + (id != null ? "id=" + id + ", " : "") + (definition != null ? "definition=" + definition : "") + "]"; + } + +} diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/template/TemplateProjectAssignment.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/template/TemplateProjectAssignment.java new file mode 100644 index 0000000000..32f5551374 --- /dev/null +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/template/TemplateProjectAssignment.java @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.scan.template; + +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Version; + +/** + * Represents a template + * + * @author Albert Tregnaghi + * + */ +@Entity +@Table(name = TemplateProjectAssignment.TABLE_NAME) +public class TemplateProjectAssignment { + + /* +-----------------------------------------------------------------------+ */ + /* +............................ SQL ......................................+ */ + /* +-----------------------------------------------------------------------+ */ + public static final String TABLE_NAME = "SCAN_TEMPLATE"; + + public static final String COLUMN_TEMPLATE_ID = "TEMPLATE_ID"; + public static final String COLUMN_DEFINITION = "TEMPLATE_DEFINITION"; + + /* +-----------------------------------------------------------------------+ */ + /* +............................ JPQL .....................................+ */ + /* +-----------------------------------------------------------------------+ */ + public static final String CLASS_NAME = "Template"; + + public static final String PROPERTY_ID = "id"; + public static final String PROPERTY_DEFINITION = "definition"; + + public static final String QUERY_All_TEMPLATE_IDS = "select t.id from #{#entityName} t"; + + @Id + @Column(name = COLUMN_TEMPLATE_ID, updatable = false, nullable = false) + String id; + + @Column(name = COLUMN_DEFINITION) + String definition; + + @Version + @Column(name = "VERSION") + @JsonIgnore + Integer version; + + TemplateProjectAssignment() { + // jpa only + } + + public TemplateProjectAssignment(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + public void setDefinition(String definition) { + this.definition = definition; + } + + public String getDefinition() { + return definition; + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + TemplateProjectAssignment other = (TemplateProjectAssignment) obj; + return Objects.equals(id, other.id); + } + + @Override + public String toString() { + return "Template [" + (id != null ? "id=" + id + ", " : "") + (definition != null ? "definition=" + definition : "") + "]"; + } + +} diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/template/TemplateRepository.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/template/TemplateRepository.java new file mode 100644 index 0000000000..3f5ceeaf88 --- /dev/null +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/template/TemplateRepository.java @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.scan.template; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface TemplateRepository extends JpaRepository { + + @Query(Template.QUERY_All_TEMPLATE_IDS) + List findAllTemplateIds(); +} diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/template/TemplateRestController.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/template/TemplateRestController.java new file mode 100644 index 0000000000..573bf7defd --- /dev/null +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/template/TemplateRestController.java @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.scan.template; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import com.mercedesbenz.sechub.commons.model.template.TemplateDefinition; +import com.mercedesbenz.sechub.sharedkernel.Profiles; +import com.mercedesbenz.sechub.sharedkernel.Step; +import com.mercedesbenz.sechub.sharedkernel.logging.AuditLogService; +import com.mercedesbenz.sechub.sharedkernel.logging.LogSanitizer; +import com.mercedesbenz.sechub.sharedkernel.security.APIConstants; +import com.mercedesbenz.sechub.sharedkernel.security.RoleConstants; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminCreatesOrUpdatesTemplate; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminDeletesTemplate; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminFetchesAllTemplateIds; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminFetchesTemplate; + +import jakarta.annotation.security.RolesAllowed; + +@RestController +@EnableAutoConfiguration +@RequestMapping(APIConstants.API_ADMINISTRATION) +@RolesAllowed({ RoleConstants.ROLE_SUPERADMIN }) +@Profile(Profiles.ADMIN_ACCESS) +public class TemplateRestController { + + @Autowired + TemplateService templateService; + + @Autowired + AuditLogService auditLogService; + + @Autowired + LogSanitizer logSanitizer; + + @UseCaseAdminCreatesOrUpdatesTemplate(@Step(number = 1, next = 2, name = "REST API call to create or update template", needsRestDoc = true)) + @RequestMapping(path = "/template/{templateId}", method = RequestMethod.PUT) + @ResponseStatus(HttpStatus.OK) + public void createOrUpdate(@RequestBody TemplateDefinition templateDefinition, @PathVariable("templateId") String templateId) { + + auditLogService.log("starts create/update of template: {}", logSanitizer.sanitize(templateId, -1)); + + templateService.createOrUpdateTemplate(templateId, templateDefinition); + + } + + @UseCaseAdminDeletesTemplate(@Step(number = 1, next = 2, name = "REST API call to delete a template", needsRestDoc = true)) + @RequestMapping(path = "/template/{templateId}", method = RequestMethod.DELETE) + @ResponseStatus(HttpStatus.OK) + public void delete(@PathVariable("templateId") String templateId) { + + auditLogService.log("starts delete of template: {}", logSanitizer.sanitize(templateId, -1)); + + templateService.deleteTemplate(templateId); + + } + + @UseCaseAdminFetchesTemplate(@Step(number = 1, next = 2, name = "REST API call to fetch template", needsRestDoc = true)) + @RequestMapping(path = "/template/{templateId}", method = RequestMethod.GET) + @ResponseStatus(HttpStatus.OK) + public TemplateDefinition fetchTemplate(@PathVariable("templateId") String templateId) { + + auditLogService.log("fetches template definition for template: {}", logSanitizer.sanitize(templateId, -1)); + + return templateService.fetchTemplateDefinition(templateId); + + } + + @UseCaseAdminFetchesAllTemplateIds(@Step(number = 1, next = 2, name = "REST API call to fetch template list", needsRestDoc = true)) + @RequestMapping(path = "/templates", method = RequestMethod.GET) + @ResponseStatus(HttpStatus.OK) + public List fetchAllTemplateIds() { + return templateService.fetchAllTemplateIds(); + + } +} diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/template/TemplateService.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/template/TemplateService.java new file mode 100644 index 0000000000..ad66b38d37 --- /dev/null +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/template/TemplateService.java @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.scan.template; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import com.mercedesbenz.sechub.commons.model.template.TemplateDefinition; +import com.mercedesbenz.sechub.commons.model.template.TemplateType; +import com.mercedesbenz.sechub.domain.scan.project.ScanProjectConfig; +import com.mercedesbenz.sechub.domain.scan.project.ScanProjectConfigID; +import com.mercedesbenz.sechub.domain.scan.project.ScanProjectConfigService; +import com.mercedesbenz.sechub.sharedkernel.Step; +import com.mercedesbenz.sechub.sharedkernel.error.NotFoundException; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminAssignsTemplateToProject; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminCreatesOrUpdatesTemplate; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminDeletesTemplate; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminFetchesAllTemplateIds; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminFetchesTemplate; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminUnassignsTemplateFromProject; +import com.mercedesbenz.sechub.sharedkernel.validation.UserInputAssertion; + +@Service +public class TemplateService { + + private static final Logger LOG = LoggerFactory.getLogger(TemplateService.class); + + private final TemplateRepository repository; + + private final ScanProjectConfigService configService; + + private final TemplateTypeScanConfigIdResolver resolver; + + private final UserInputAssertion inputAssertion; + + TemplateService(TemplateRepository repository, ScanProjectConfigService configService, UserInputAssertion inputAssertion, + TemplateTypeScanConfigIdResolver resolver) { + this.repository = repository; + this.configService = configService; + this.resolver = resolver; + this.inputAssertion = inputAssertion; + } + + @UseCaseAdminCreatesOrUpdatesTemplate(@Step(number = 2, name = "Service creates or updates template")) + public void createOrUpdateTemplate(String templateId, TemplateDefinition newTemplateDefinition) { + if (templateId == null) { + throw new IllegalArgumentException("Template id may not be null!"); + } + if (newTemplateDefinition == null) { + throw new IllegalArgumentException("Template definition may not be null!"); + } + + // first of all we always set template id, so we have never a clash here + // even when somebody copied an existing template definition. + newTemplateDefinition.setId(templateId); + + Optional