diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 26898ec789..69c0abf597 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -40,6 +40,11 @@ updates: schedule: interval: daily + - package-ecosystem: docker + directory: /docker/semgrep + schedule: + interval: daily + - package-ecosystem: github-actions directory: / schedule: diff --git a/.github/workflows/run-ci-cd.yaml b/.github/workflows/run-ci-cd.yaml index 6df6a9ce23..49e0485d51 100644 --- a/.github/workflows/run-ci-cd.yaml +++ b/.github/workflows/run-ci-cd.yaml @@ -400,6 +400,30 @@ jobs: docker run --env-file frontend/.env.example owasp/nest:test-frontend-a11y-latest pnpm run test:a11y timeout-minutes: 5 + run-security-scan: + name: Run security scan + needs: + - scan-code + - scan-ci-dependencies + permissions: + contents: read + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + + - name: Run Security CI Scan + run: make security-scan + + - name: Upload security scan report + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f + with: + name: semgrep-results-run-${{ github.run_number }} + path: semgrep-security-report.txt + retention-days: 14 + timeout-minutes: 15 + set-release-version: name: Set release version outputs: @@ -450,6 +474,7 @@ jobs: - run-frontend-e2e-tests - run-frontend-unit-tests - set-release-version + - run-security-scan permissions: contents: read runs-on: ubuntu-latest @@ -820,6 +845,7 @@ jobs: - run-frontend-e2e-tests - run-frontend-unit-tests - set-release-version + - run-security-scan permissions: contents: read runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 6780666977..f15b7fa412 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,8 @@ design/ frontend/blob-report/ frontend/coverage frontend/dist +frontend/.next +frontend/out frontend/npm-debug.log* frontend/playwright-report/ frontend/playwright/.cache/ @@ -50,3 +52,4 @@ venv/ # Snyk Security Extension - AI Rules (auto-generated) .cursor/rules/snyk_rules.mdc +semgrep-security-report.txt diff --git a/Makefile b/Makefile index a409222c78..4e18e7f758 100644 --- a/Makefile +++ b/Makefile @@ -58,6 +58,30 @@ scan-images: \ scan-backend-image \ scan-frontend-image +security-scan: + @echo "Running Security Scan..." + @docker run --rm \ + -v "$(PWD):/src" \ + -w /src \ + $$(grep -E '^FROM semgrep/semgrep:' docker/semgrep/Dockerfile | sed 's/^FROM //') \ + semgrep \ + --config p/ci \ + --config p/javascript \ + --config p/nginx \ + --config p/owasp-top-ten \ + --config p/python \ + --config p/secrets \ + --config p/security-audit \ + --config p/sql-injection \ + --config p/typescript \ + --error \ + --skip-unknown-extensions \ + --timeout 10 \ + --timeout-threshold 3 \ + --text \ + --text-output=semgrep-security-report.txt \ + . + test: \ test-nest-app @@ -78,3 +102,5 @@ update-nest-app-dependencies: \ update-pre-commit: @pre-commit autoupdate + +.PHONY: build clean check pre-commit prune run scan-images security-scan test update diff --git a/backend/apps/api/decorators/cache.py b/backend/apps/api/decorators/cache.py index 655710cfae..3da046d8c6 100644 --- a/backend/apps/api/decorators/cache.py +++ b/backend/apps/api/decorators/cache.py @@ -13,7 +13,8 @@ def generate_key( prefix: str, ): """Generate a cache key for a request.""" - return f"{prefix}:{request.get_full_path()}" + # Reason: This generates an internal cache key, not a user-facing HTML response. + return f"{prefix}:{request.get_full_path()}" # nosemgrep: python.flask.security.audit.directly-returned-format-string.directly-returned-format-string # noqa: E501 def cache_response( diff --git a/backend/apps/api/internal/mutations/api_key.py b/backend/apps/api/internal/mutations/api_key.py index edbcc49a13..1d557541a7 100644 --- a/backend/apps/api/internal/mutations/api_key.py +++ b/backend/apps/api/internal/mutations/api_key.py @@ -77,7 +77,10 @@ def create_api_key(self, info: Info, name: str, expires_at: datetime) -> CreateA message="API key created successfully.", ) except IntegrityError as err: - logger.warning("Error creating API key: %s", err) + # Reason: Logging the error message string only, not the sensitive API key credential itself. # noqa: E501 + logger.warning( # nosemgrep: python.lang.security.audit.logging.logger-credential-leak.python-logger-credential-disclosure # noqa: E501 + "Error creating API key: %s", err + ) return CreateApiKeyResult( ok=False, code="ERROR", diff --git a/backend/apps/github/admin/issue.py b/backend/apps/github/admin/issue.py index 3c397dcd85..17f87e64c8 100644 --- a/backend/apps/github/admin/issue.py +++ b/backend/apps/github/admin/issue.py @@ -1,7 +1,7 @@ """GitHub app Issue model admin.""" from django.contrib import admin -from django.utils.safestring import mark_safe +from django.utils.html import format_html from apps.github.models.issue import Issue @@ -38,7 +38,7 @@ def custom_field_github_url(self, obj) -> str: str: A safe HTML link to the issue on GitHub. """ - return mark_safe(f"↗️") # noqa: S308 + return format_html("↗️", obj.url) custom_field_github_url.short_description = "GitHub 🔗" diff --git a/backend/apps/github/admin/pull_request.py b/backend/apps/github/admin/pull_request.py index 02937114b4..f5c5d00f3d 100644 --- a/backend/apps/github/admin/pull_request.py +++ b/backend/apps/github/admin/pull_request.py @@ -1,7 +1,7 @@ """GitHub app PullRequest model admin.""" from django.contrib import admin -from django.utils.safestring import mark_safe +from django.utils.html import format_html from apps.github.models.pull_request import PullRequest @@ -44,7 +44,7 @@ def custom_field_github_url(self, obj: PullRequest) -> str: str: A safe HTML link to the pull request on GitHub. """ - return mark_safe(f"↗️") # noqa: S308 + return format_html("↗️", obj.url) custom_field_github_url.short_description = "GitHub 🔗" diff --git a/backend/apps/github/admin/repository.py b/backend/apps/github/admin/repository.py index 66dcdc18aa..ba17eb19ca 100644 --- a/backend/apps/github/admin/repository.py +++ b/backend/apps/github/admin/repository.py @@ -1,7 +1,7 @@ """GitHub app Repository model admin.""" from django.contrib import admin -from django.utils.safestring import mark_safe +from django.utils.html import format_html from apps.github.models.repository import Repository @@ -46,8 +46,10 @@ def custom_field_github_url(self, obj) -> str: str: A safe HTML link to the repository on GitHub. """ - return mark_safe( # noqa: S308 - f"↗️" + return format_html( + "↗️", + obj.owner.login, + obj.name, ) def custom_field_title(self, obj: Repository) -> str: diff --git a/backend/apps/owasp/admin/mixins.py b/backend/apps/owasp/admin/mixins.py index 9addfc4a6e..59b7839484 100644 --- a/backend/apps/owasp/admin/mixins.py +++ b/backend/apps/owasp/admin/mixins.py @@ -2,7 +2,7 @@ from django.contrib.contenttypes.admin import GenericTabularInline from django.contrib.contenttypes.models import ContentType -from django.utils.html import escape +from django.utils.html import format_html from django.utils.safestring import mark_safe from apps.owasp.admin.widgets import ChannelIdWidget @@ -106,10 +106,10 @@ def custom_field_github_urls(self, obj): return "" return self._format_github_link(obj.owasp_repository) - return mark_safe( # noqa: S308 - " ".join( - [self._format_github_link(repository) for repository in obj.repositories.all()] - ) + links = [self._format_github_link(repository) for repository in obj.repositories.all()] + # Use mark_safe since links are already safely formatted HTML from format_html + return mark_safe( # noqa: S308 # NOSEMGREP: python.django.security.audit.avoid-mark-safe.avoid-mark-safe + " ".join(links) ) def custom_field_owasp_url(self, obj): @@ -117,9 +117,7 @@ def custom_field_owasp_url(self, obj): if not hasattr(obj, "key") or not obj.key: return "" - return mark_safe( # noqa: S308 - f"↗️" - ) + return format_html("↗️", obj.key) def _format_github_link(self, repository): """Format a single GitHub repository link.""" @@ -130,9 +128,10 @@ def _format_github_link(self, repository): if not hasattr(repository, "key") or not repository.key: return "" - return mark_safe( # noqa: S308 - f"↗️" + return format_html( + "↗️", + repository.owner.login, + repository.key, ) custom_field_github_urls.short_description = "GitHub 🔗" diff --git a/backend/apps/owasp/admin/widgets.py b/backend/apps/owasp/admin/widgets.py index 7765ade609..409fdee692 100644 --- a/backend/apps/owasp/admin/widgets.py +++ b/backend/apps/owasp/admin/widgets.py @@ -21,4 +21,7 @@ def render(self, name, value, attrs=None, renderer=None): f"id='lookup_id_{name}' title='Look up related objects'>" ) - return mark_safe(f"{widget_html}{search_button}") # noqa: S308 + # Use mark_safe since both widget_html and search_button are already HTML + return mark_safe( # noqa: S308 # NOSEMGREP: python.django.security.audit.avoid-mark-safe.avoid-mark-safe + f"{widget_html} {search_button}" + ) diff --git a/backend/apps/slack/views.py b/backend/apps/slack/views.py index a3983bf11c..229479e23b 100644 --- a/backend/apps/slack/views.py +++ b/backend/apps/slack/views.py @@ -9,7 +9,7 @@ slack_handler = SlackRequestHandler(SlackConfig.app) -@csrf_exempt # NOSONAR +@csrf_exempt # NOSEMGREP: python.django.security.audit.csrf-exempt.no-csrf-exempt # NOSONAR @require_POST # NOSONAR def slack_request_handler(request): """Handle Slack requests. @@ -20,5 +20,10 @@ def slack_request_handler(request): Returns: HttpResponse: The response generated by the SlackRequestHandler. + Note: + CSRF exemption is required for webhook endpoints. Security is ensured through + Slack's request signature verification, which is automatically handled by + slack-bolt's SlackRequestHandler when signing_secret is configured in SlackConfig.app. + """ return slack_handler.handle(request) diff --git a/cspell/Dockerfile b/cspell/Dockerfile index 1e0069ca51..ed5501599a 100644 --- a/cspell/Dockerfile +++ b/cspell/Dockerfile @@ -17,7 +17,8 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ pnpm install --frozen-lockfile --ignore-scripts WORKDIR /nest - +# Reason: This is a dev-tool linter, not a production service. Root access is required for IO operations. +# nosemgrep: dockerfile.security.missing-user-entrypoint.missing-user-entrypoint ENTRYPOINT ["/opt/node/node_modules/.bin/cspell"] USER node diff --git a/cspell/custom-dict.txt b/cspell/custom-dict.txt index f99e174826..19a37e374e 100644 --- a/cspell/custom-dict.txt +++ b/cspell/custom-dict.txt @@ -117,6 +117,7 @@ navlink nestbot ngx noinput +nosemgrep nosniff nspname numfmt @@ -147,6 +148,7 @@ saft sakanashi samm schemathesis +semgrep seo skillstruck slackbot diff --git a/docker/cspell/Dockerfile b/docker/cspell/Dockerfile index 1e0069ca51..ed5501599a 100644 --- a/docker/cspell/Dockerfile +++ b/docker/cspell/Dockerfile @@ -17,7 +17,8 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ pnpm install --frozen-lockfile --ignore-scripts WORKDIR /nest - +# Reason: This is a dev-tool linter, not a production service. Root access is required for IO operations. +# nosemgrep: dockerfile.security.missing-user-entrypoint.missing-user-entrypoint ENTRYPOINT ["/opt/node/node_modules/.bin/cspell"] USER node diff --git a/docker/semgrep/Dockerfile b/docker/semgrep/Dockerfile new file mode 100644 index 0000000000..619c2058cf --- /dev/null +++ b/docker/semgrep/Dockerfile @@ -0,0 +1 @@ +FROM semgrep/semgrep:1.148.0 diff --git a/frontend/nginx.conf b/frontend/nginx.conf index bd0fae8d10..41857482ac 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -12,7 +12,8 @@ server { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Connection 'upgrade'; - proxy_set_header Host $host; + # Reason: Standard reverse proxy configuration; Host header forwarding is required for routing. + proxy_set_header Host $host; # nosemgrep: generic.nginx.security.request-host-used.request-host-used proxy_set_header Upgrade $http_upgrade; proxy_cache_bypass $http_upgrade; } diff --git a/frontend/package.json b/frontend/package.json index 7238ce8fa6..8704ef1e46 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -46,6 +46,7 @@ "framer-motion": "^12.29.0", "graphql": "^16.12.0", "ics": "^3.8.1", + "isomorphic-dompurify": "^2.35.0", "leaflet": "^1.9.4", "leaflet.markercluster": "^1.5.3", "lodash": "^4.17.23", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 9ed6f3c381..31f84706fc 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -96,6 +96,9 @@ importers: ics: specifier: ^3.8.1 version: 3.8.1 + isomorphic-dompurify: + specifier: ^2.35.0 + version: 2.35.0 leaflet: specifier: ^1.9.4 version: 1.9.4 @@ -301,6 +304,9 @@ importers: packages: + '@acemir/cssom@0.9.31': + resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==} + '@adobe/css-tools@4.4.4': resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} @@ -342,6 +348,15 @@ packages: '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + '@asamuzakjp/css-color@4.1.1': + resolution: {integrity: sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==} + + '@asamuzakjp/dom-selector@6.7.6': + resolution: {integrity: sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@axe-core/react@4.11.0': resolution: {integrity: sha512-ko5hYRmdLzbsxagsb0u3GD8IqtZa+vUJZ4K4+z8qjsHGrHBNBps5Sy0EIfSL8whzyIKBQ3xYa1reK1QenIAkyw==} @@ -548,6 +563,10 @@ packages: peerDependencies: '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-syntax-patches-for-csstree@1.0.25': + resolution: {integrity: sha512-g0Kw9W3vjx5BEBAF8c5Fm2NcB/Fs8jJXh85aXqwEXiL+tqtOut07TWgyaGzAAfTM+gKckrrncyeGEZPcaRgm2Q==} + engines: {node: '>=18'} + '@csstools/css-tokenizer@3.0.4': resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} @@ -611,6 +630,15 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@exodus/bytes@1.9.0': + resolution: {integrity: sha512-lagqsvnk09NKogQaN/XrtlWeUF8SRhT12odMvbTIIaVObqzwAogL6jhR4DAp0gPuKoM1AOVrKUshJpRdpMFrww==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + '@fastify/busboy@3.2.0': resolution: {integrity: sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==} @@ -3901,6 +3929,9 @@ packages: resolution: {integrity: sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw==} engines: {node: '>=10.0.0'} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -4190,6 +4221,10 @@ packages: csp_evaluator@1.1.5: resolution: {integrity: sha512-EL/iN9etCTzw/fBnp0/uj0f5BOOGvZut2mzsiiBZ/FdT6gFQCKRO/tmcKOxn5drWZ2Ndm/xBb1SI4zwWbGtmIw==} + css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css.escape@1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} @@ -4197,6 +4232,10 @@ packages: resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} engines: {node: '>=18'} + cssstyle@5.3.7: + resolution: {integrity: sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==} + engines: {node: '>=20'} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -4215,6 +4254,10 @@ packages: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} + data-urls@6.0.1: + resolution: {integrity: sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==} + engines: {node: '>=20'} + data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -5033,6 +5076,10 @@ packages: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -5343,6 +5390,10 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isomorphic-dompurify@2.35.0: + resolution: {integrity: sha512-a9+LQqylQCU8f1zmsYmg2tfrbdY2YS/Hc+xntcq/mDI2MY3Q108nq8K23BWDIg6YGC5JsUMC15fj2ZMqCzt/+A==} + engines: {node: '>=20.19.5'} + isomorphic-fetch@3.0.0: resolution: {integrity: sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==} @@ -5575,6 +5626,15 @@ packages: canvas: optional: true + jsdom@27.4.0: + resolution: {integrity: sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -5853,6 +5913,9 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} @@ -6232,6 +6295,9 @@ packages: parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parse5@8.0.0: + resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -7104,6 +7170,9 @@ packages: tldts-core@6.1.86: resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + tldts-core@7.0.19: + resolution: {integrity: sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==} + tldts-icann@6.1.86: resolution: {integrity: sha512-NFxmRT2lAEMcCOBgeZ0NuM0zsK/xgmNajnY6n4S1mwAKocft2s2ise1O3nQxrH3c+uY6hgHUV9GGNVp7tUE4Sg==} @@ -7111,6 +7180,10 @@ packages: resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} hasBin: true + tldts@7.0.19: + resolution: {integrity: sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==} + hasBin: true + tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -7137,6 +7210,10 @@ packages: resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} engines: {node: '>=16'} + tough-cookie@6.0.0: + resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + engines: {node: '>=16'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -7144,6 +7221,10 @@ packages: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -7414,6 +7495,10 @@ packages: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + webpack-sources@3.3.3: resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==} engines: {node: '>=10.13.0'} @@ -7443,10 +7528,18 @@ packages: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} engines: {node: '>=18'} + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + whatwg-url@14.2.0: resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} engines: {node: '>=18'} + whatwg-url@15.1.0: + resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} + engines: {node: '>=20'} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -7615,6 +7708,8 @@ packages: snapshots: + '@acemir/cssom@0.9.31': {} + '@adobe/css-tools@4.4.4': {} '@alloc/quick-lru@5.2.0': {} @@ -7669,6 +7764,24 @@ snapshots: '@csstools/css-tokenizer': 3.0.4 lru-cache: 10.4.3 + '@asamuzakjp/css-color@4.1.1': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 11.2.4 + + '@asamuzakjp/dom-selector@6.7.6': + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.1.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.2.4 + + '@asamuzakjp/nwsapi@2.3.9': {} + '@axe-core/react@4.11.0': dependencies: axe-core: 4.11.1 @@ -7892,6 +8005,8 @@ snapshots: dependencies: '@csstools/css-tokenizer': 3.0.4 + '@csstools/css-syntax-patches-for-csstree@1.0.25': {} + '@csstools/css-tokenizer@3.0.4': {} '@emnapi/core@1.8.1': @@ -7973,6 +8088,8 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@exodus/bytes@1.9.0': {} + '@fastify/busboy@3.2.0': {} '@formatjs/ecma402-abstract@2.3.6': @@ -12497,6 +12614,10 @@ snapshots: basic-ftp@5.1.0: {} + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + binary-extensions@2.3.0: {} body-parser@1.20.4: @@ -12840,6 +12961,11 @@ snapshots: csp_evaluator@1.1.5: {} + css-tree@3.1.0: + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + css.escape@1.5.1: {} cssstyle@4.6.0: @@ -12847,6 +12973,13 @@ snapshots: '@asamuzakjp/css-color': 3.2.0 rrweb-cssom: 0.8.0 + cssstyle@5.3.7: + dependencies: + '@asamuzakjp/css-color': 4.1.1 + '@csstools/css-syntax-patches-for-csstree': 1.0.25 + css-tree: 3.1.0 + lru-cache: 11.2.4 + csstype@3.2.3: {} damerau-levenshtein@1.0.8: {} @@ -12860,6 +12993,11 @@ snapshots: whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 + data-urls@6.0.1: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 15.1.0 + data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -13813,6 +13951,12 @@ snapshots: dependencies: whatwg-encoding: 3.1.1 + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.9.0 + transitivePeerDependencies: + - '@noble/hashes' + html-escaper@2.0.2: {} http-errors@2.0.1: @@ -14129,6 +14273,17 @@ snapshots: isexe@2.0.0: {} + isomorphic-dompurify@2.35.0: + dependencies: + dompurify: 3.3.1 + jsdom: 27.4.0 + transitivePeerDependencies: + - '@noble/hashes' + - bufferutil + - canvas + - supports-color + - utf-8-validate + isomorphic-fetch@3.0.0: dependencies: node-fetch: 2.7.0 @@ -14587,6 +14742,34 @@ snapshots: - supports-color - utf-8-validate + jsdom@27.4.0: + dependencies: + '@acemir/cssom': 0.9.31 + '@asamuzakjp/dom-selector': 6.7.6 + '@exodus/bytes': 1.9.0 + cssstyle: 5.3.7 + data-urls: 6.0.1 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + parse5: 8.0.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + ws: 8.19.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + - bufferutil + - supports-color + - utf-8-validate + jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -14872,6 +15055,8 @@ snapshots: math-intrinsics@1.1.0: {} + mdn-data@2.12.2: {} + mdurl@2.0.0: {} media-typer@0.3.0: {} @@ -15228,6 +15413,10 @@ snapshots: dependencies: entities: 6.0.1 + parse5@8.0.0: + dependencies: + entities: 6.0.1 + parseurl@1.3.3: {} pascal-case@3.1.2: @@ -16170,6 +16359,8 @@ snapshots: tldts-core@6.1.86: {} + tldts-core@7.0.19: {} + tldts-icann@6.1.86: dependencies: tldts-core: 6.1.86 @@ -16178,6 +16369,10 @@ snapshots: dependencies: tldts-core: 6.1.86 + tldts@7.0.19: + dependencies: + tldts-core: 7.0.19 + tmp@0.0.33: dependencies: os-tmpdir: 1.0.2 @@ -16200,12 +16395,20 @@ snapshots: dependencies: tldts: 6.1.86 + tough-cookie@6.0.0: + dependencies: + tldts: 7.0.19 + tr46@0.0.3: {} tr46@5.1.1: dependencies: punycode: 2.3.1 + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + tree-kill@1.2.2: {} ts-api-utils@2.4.0(typescript@5.9.3): @@ -16485,6 +16688,8 @@ snapshots: webidl-conversions@7.0.0: {} + webidl-conversions@8.0.1: {} + webpack-sources@3.3.3: {} webpack-virtual-modules@0.5.0: {} @@ -16529,11 +16734,18 @@ snapshots: whatwg-mimetype@4.0.0: {} + whatwg-mimetype@5.0.0: {} + whatwg-url@14.2.0: dependencies: tr46: 5.1.1 webidl-conversions: 7.0.0 + whatwg-url@15.1.0: + dependencies: + tr46: 6.0.0 + webidl-conversions: 8.0.1 + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 diff --git a/frontend/src/app/organizations/[organizationKey]/layout.tsx b/frontend/src/app/organizations/[organizationKey]/layout.tsx index e35abb8273..d148c6c501 100644 --- a/frontend/src/app/organizations/[organizationKey]/layout.tsx +++ b/frontend/src/app/organizations/[organizationKey]/layout.tsx @@ -1,3 +1,4 @@ +import DOMPurify from 'isomorphic-dompurify' import { Metadata } from 'next' import Script from 'next/script' import React from 'react' @@ -98,8 +99,20 @@ export default async function OrganizationDetailsLayout({ params: Promise<{ organizationKey: string }> }>) { const { organizationKey } = await params + + if (!/^[a-zA-Z0-9._-]+$/.test(organizationKey)) { + return ( + +
Invalid Organization Key
+
+ ) + } const structuredData = await generateOrganizationStructuredData(organizationKey) + const jsonLdString = structuredData + ? DOMPurify.sanitize(JSON.stringify(structuredData, null, 2)) + : null + // Fetch organization name for breadcrumb const { data } = await apolloClient.query({ query: GetOrganizationMetadataDocument, @@ -109,12 +122,12 @@ export default async function OrganizationDetailsLayout({ return ( - {structuredData && ( + {jsonLdString && (