From cb2ad27d1cac6c17d896a16ff6026d66a7b930cd Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Wed, 21 May 2025 17:03:35 -0700 Subject: [PATCH 1/3] updating csp and test --- app/__init__.py | 22 +++++++++------------- app/config.py | 1 - tests/app/main/views/test_headers.py | 23 +++++++---------------- 3 files changed, 16 insertions(+), 30 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 0b2006689..b5360b22c 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -141,9 +141,8 @@ navigation = { def _csp(config): asset_domain = config["ASSET_DOMAIN"] logo_domain = config["LOGO_CDN_DOMAIN"] - api_public_url = config["API_PUBLIC_URL"] - csp = { + return { "default-src": ["'self'", asset_domain], "frame-src": [ "https://www.youtube.com", @@ -167,22 +166,19 @@ def _csp(config): "'self'", "https://gov-bam.nr-data.net", "https://www.google-analytics.com", + "http://localhost:6011", + "ws://localhost:6011", + "https://notify-api-staging.app.cloud.gov", + "wss://notify-api-staging.app.cloud.gov", + "https://notify-api-demo.app.cloud.gov", + "wss://notify-api-demo.app.cloud.gov", + "https://notify-api-production.app.cloud.gov", + "wss://notify-api-production.app.cloud.gov", ], "style-src": ["'self'", asset_domain], "img-src": ["'self'", asset_domain, logo_domain], } - if api_public_url: - csp["connect-src"].append(api_public_url) - # this is for web socket - if api_public_url.startswith("http://"): - ws_url = api_public_url.replace("http://", "ws://") - csp["connect-src"].append(ws_url) - elif api_public_url.startswith("https://"): - ws_url = api_public_url.replace("https://", "wss://") - csp["connect-src"].append(ws_url) - return csp - def create_app(application): @application.after_request diff --git a/app/config.py b/app/config.py index 42bba9b94..d123cdfc3 100644 --- a/app/config.py +++ b/app/config.py @@ -134,7 +134,6 @@ class Test(Development): ASSET_PATH = "https://static.example.com/" API_HOST_NAME = "http://you-forgot-to-mock-an-api-call-to" - API_PUBLIC_URL = "http://you-forgot-to-mock-an-api-call-to" REDIS_URL = "redis://you-forgot-to-mock-a-redis-call-to" LOGO_CDN_DOMAIN = "static-logos.test.com" diff --git a/tests/app/main/views/test_headers.py b/tests/app/main/views/test_headers.py index c051224f9..a999bca12 100644 --- a/tests/app/main/views/test_headers.py +++ b/tests/app/main/views/test_headers.py @@ -27,21 +27,12 @@ def test_owasp_useful_headers_set( csp, ) assert search(r"'nonce-[^']+';", csp) - assert search( - r"connect-src 'self' https:\/\/gov-bam\.nr-data\.net https:\/\/www\.google-analytics\.", - csp, - ) + assert search(r"connect-src", csp) + assert search(r"https:\/\/gov-bam\.nr-data\.net", csp) + assert search(r"https:\/\/www\.google-analytics\.com", csp) + assert search(r"http:\/\/localhost:6011", csp) + assert search(r"ws:\/\/localhost:6011", csp) + assert search(r"https:\/\/notify-api-.*\.app\.cloud\.gov", csp) + assert search(r"wss:\/\/notify-api-.*\.app\.cloud\.gov", csp) assert search(r"style-src 'self' static\.example\.com 'nonce-.*';", csp) assert search(r"img-src 'self' static\.example\.com static-logos\.test\.com", csp) - api_public_url = current_app.config.get("API_PUBLIC_URL") - assert api_public_url is not None, f"API_PUBLIC_URL: {api_public_url} — is missing" - - assert api_public_url in csp - if api_public_url.startswith("http://"): - assert api_public_url.replace("http://", "ws://") in csp - elif api_public_url.startswith("https://"): - assert api_public_url.replace("https://", "wss://") in csp - else: - raise AssertionError( - f"Unexpected API_PUBLIC_URL format: {api_public_url} — must start with 'http://' or 'https://'" - ) From 444800fbdad824f93693ef65c8423a1093e23d3f Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Wed, 21 May 2025 17:23:30 -0700 Subject: [PATCH 2/3] flake8 --- tests/app/main/views/test_headers.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/app/main/views/test_headers.py b/tests/app/main/views/test_headers.py index a999bca12..8ba559499 100644 --- a/tests/app/main/views/test_headers.py +++ b/tests/app/main/views/test_headers.py @@ -1,7 +1,5 @@ from re import search -from flask import current_app - def test_owasp_useful_headers_set( client_request, From 790fe2d6f94e4d15ebde27c291d4a69243a60dcc Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Thu, 22 May 2025 12:03:18 -0700 Subject: [PATCH 3/3] update csp using variables and testing --- .ds.baseline | 4 ++-- .github/workflows/deploy-demo.yml | 2 ++ .github/workflows/deploy-prod.yml | 2 ++ .github/workflows/deploy.yml | 2 ++ app/__init__.py | 12 ++++------- app/assets/javascripts/socketio.js | 8 ++++++++ app/config.py | 5 +++-- deploy-config/demo.yml | 1 + deploy-config/production.yml | 1 + deploy-config/staging.yml | 1 + manifest.yml | 2 +- tests/app/main/views/test_headers.py | 30 +++++++++++++++++++++------- 12 files changed, 50 insertions(+), 20 deletions(-) diff --git a/.ds.baseline b/.ds.baseline index fd261c5cf..5c24d852f 100644 --- a/.ds.baseline +++ b/.ds.baseline @@ -171,7 +171,7 @@ "filename": "app/config.py", "hashed_secret": "577a4c667e4af8682ca431857214b3a920883efc", "is_verified": false, - "line_number": 122, + "line_number": 123, "is_secret": false } ], @@ -644,5 +644,5 @@ } ] }, - "generated_at": "2025-05-19T21:41:23Z" + "generated_at": "2025-05-22T19:03:11Z" } diff --git a/.github/workflows/deploy-demo.yml b/.github/workflows/deploy-demo.yml index e6371a124..b16607c61 100644 --- a/.github/workflows/deploy-demo.yml +++ b/.github/workflows/deploy-demo.yml @@ -64,6 +64,7 @@ jobs: LOGIN_DOT_GOV_INITIAL_SIGNIN_URL: "https://secure.login.gov/openid_connect/authorize?acr_values=http%3A%2F%2Fidmanagement.gov%2Fns%2Fassurance%2Fial%2F1&client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&nonce=NONCE&prompt=select_account&redirect_uri=https://notify-demo.app.cloud.gov/sign-in&response_type=code&scope=openid+email&state=STATE" LOGIN_DOT_GOV_CERTS_URL: "https://secure.login.gov/api/openid_connect/certs" API_PUBLIC_URL: ${{ secrets.API_PUBLIC_URL }} + API_PUBLIC_WS_URL: ${{ secrets.API_PUBLIC_WS_URL }} with: cf_username: ${{ secrets.CLOUDGOV_USERNAME }} cf_password: ${{ secrets.CLOUDGOV_PASSWORD }} @@ -89,6 +90,7 @@ jobs: --var LOGIN_DOT_GOV_CERTS_URL="$LOGIN_DOT_GOV_CERTS_URL" --var LOGIN_PEM="$LOGIN_PEM" --var API_PUBLIC_URL="$API_PUBLIC_URL" + --var API_PUBLIC_WS_URL="$API_PUBLIC_WS_URL" --strategy rolling - name: Deploy egress proxy diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index 3954b30f0..05c7ccf26 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -64,6 +64,7 @@ jobs: LOGIN_DOT_GOV_INITIAL_SIGNIN_URL: "https://secure.login.gov/openid_connect/authorize?acr_values=http%3A%2F%2Fidmanagement.gov%2Fns%2Fassurance%2Fial%2F1&client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&nonce=NONCE&prompt=select_account&redirect_uri=https://beta.notify.gov/sign-in&response_type=code&scope=openid+email&state=STATE" LOGIN_DOT_GOV_CERTS_URL: "https://secure.login.gov/api/openid_connect/certs" API_PUBLIC_URL: ${{ secrets.API_PUBLIC_URL }} + API_PUBLIC_WS_URL: ${{ secrets.API_PUBLIC_WS_URL }} with: cf_username: ${{ secrets.CLOUDGOV_USERNAME }} cf_password: ${{ secrets.CLOUDGOV_PASSWORD }} @@ -89,6 +90,7 @@ jobs: --var LOGIN_DOT_GOV_CERTS_URL="$LOGIN_DOT_GOV_CERTS_URL" --var LOGIN_PEM="$LOGIN_PEM" --var API_PUBLIC_URL="$API_PUBLIC_URL" + --var API_PUBLIC_WS_URL="$API_PUBLIC_WS_URL" --strategy rolling - name: Deploy egress proxy diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9b3bf1d91..1a95d7ba1 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -70,6 +70,7 @@ jobs: LOGIN_DOT_GOV_INITIAL_SIGNIN_URL: "https://secure.login.gov/openid_connect/authorize?acr_values=http%3A%2F%2Fidmanagement.gov%2Fns%2Fassurance%2Fial%2F1&client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&nonce=NONCE&prompt=select_account&redirect_uri=https://notify-staging.app.cloud.gov/sign-in&response_type=code&scope=openid+email&state=STATE" LOGIN_DOT_GOV_CERTS_URL: "https://secure.login.gov/api/openid_connect/certs" API_PUBLIC_URL: ${{ secrets.API_PUBLIC_URL }} + API_PUBLIC_WS_URL: ${{ secrets.API_PUBLIC_WS_URL }} with: cf_username: ${{ secrets.CLOUDGOV_USERNAME }} cf_password: ${{ secrets.CLOUDGOV_PASSWORD }} @@ -95,6 +96,7 @@ jobs: --var LOGIN_DOT_GOV_CERTS_URL="$LOGIN_DOT_GOV_CERTS_URL" --var LOGIN_PEM="$LOGIN_PEM" --var API_PUBLIC_URL="$API_PUBLIC_URL" + --var API_PUBLIC_WS_URL="$API_PUBLIC_WS_URL" --strategy rolling diff --git a/app/__init__.py b/app/__init__.py index b5360b22c..a6a4a615b 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -141,6 +141,8 @@ navigation = { def _csp(config): asset_domain = config["ASSET_DOMAIN"] logo_domain = config["LOGO_CDN_DOMAIN"] + api_public_url = config["API_PUBLIC_URL"] + api_public_ws_url = config["API_PUBLIC_WS_URL"] return { "default-src": ["'self'", asset_domain], @@ -166,14 +168,8 @@ def _csp(config): "'self'", "https://gov-bam.nr-data.net", "https://www.google-analytics.com", - "http://localhost:6011", - "ws://localhost:6011", - "https://notify-api-staging.app.cloud.gov", - "wss://notify-api-staging.app.cloud.gov", - "https://notify-api-demo.app.cloud.gov", - "wss://notify-api-demo.app.cloud.gov", - "https://notify-api-production.app.cloud.gov", - "wss://notify-api-production.app.cloud.gov", + f"{api_public_url}", + f"{api_public_ws_url}", ], "style-src": ["'self'", asset_domain], "img-src": ["'self'", asset_domain, logo_domain], diff --git a/app/assets/javascripts/socketio.js b/app/assets/javascripts/socketio.js index 46d18c7d4..754ea5c01 100644 --- a/app/assets/javascripts/socketio.js +++ b/app/assets/javascripts/socketio.js @@ -20,6 +20,14 @@ document.addEventListener('DOMContentLoaded', function () { if (featureEnabled) { const socket = io(apiHost); + socket.on('connect_error', (err) => { + console.error('Socket connect_error:', err); + }); + + socket.on('error', (err) => { + console.error('Socket error:', err); + }); + socket.on('connect', () => { socket.emit('join', { room: `job-${jobId}` }); }); diff --git a/app/config.py b/app/config.py index d123cdfc3..fa8ade775 100644 --- a/app/config.py +++ b/app/config.py @@ -13,6 +13,7 @@ class Config(object): NOTIFY_ENVIRONMENT = getenv("NOTIFY_ENVIRONMENT", "development") API_HOST_NAME = getenv("API_HOST_NAME", "localhost") API_PUBLIC_URL = getenv("API_PUBLIC_URL", "localhost") + API_PUBLIC_WS_URL = getenv("API_PUBLIC_WS_URL", "localhost") ADMIN_BASE_URL = getenv("ADMIN_BASE_URL", "http://localhost:6012") HEADER_COLOUR = ( @@ -132,9 +133,9 @@ class Test(Development): WTF_CSRF_ENABLED = False ASSET_DOMAIN = "static.example.com" ASSET_PATH = "https://static.example.com/" - API_HOST_NAME = "http://you-forgot-to-mock-an-api-call-to" - + API_PUBLIC_URL = "http://you-forgot-to-mock-an-api-call-to" + API_PUBLIC_WS_URL = "ws://you-forgot-to-mock-an-api-call-to" REDIS_URL = "redis://you-forgot-to-mock-a-redis-call-to" LOGO_CDN_DOMAIN = "static-logos.test.com" diff --git a/deploy-config/demo.yml b/deploy-config/demo.yml index 08d29d096..5e34827ec 100644 --- a/deploy-config/demo.yml +++ b/deploy-config/demo.yml @@ -9,3 +9,4 @@ nr_agent_id: '1134302465' nr_app_id: '1083160688' FEATURE_SOCKET_ENABLED: true API_PUBLIC_URL: https://notify-api-demo.app.cloud.gov +API_PUBLIC_WS_URL: wss://notify-api-demo.app.cloud.gov diff --git a/deploy-config/production.yml b/deploy-config/production.yml index 7d3a14bee..2655d4b5a 100644 --- a/deploy-config/production.yml +++ b/deploy-config/production.yml @@ -9,3 +9,4 @@ nr_agent_id: '1050708682' nr_app_id: '1050708682' FEATURE_SOCKET_ENABLED: false API_PUBLIC_URL: https://notify-api-production.app.cloud.gov +API_PUBLIC_WS_URL: wss://notify-api-production.app.cloud.gov diff --git a/deploy-config/staging.yml b/deploy-config/staging.yml index 36e6ad78d..260bc334d 100644 --- a/deploy-config/staging.yml +++ b/deploy-config/staging.yml @@ -9,3 +9,4 @@ nr_agent_id: '1134291385' nr_app_id: '1031640326' FEATURE_SOCKET_ENABLED: false API_PUBLIC_URL: https://notify-api-staging.app.cloud.gov +API_PUBLIC_WS_URL: wss://notify-api-staging.app.cloud.gov diff --git a/manifest.yml b/manifest.yml index da4538c8d..2322bb376 100644 --- a/manifest.yml +++ b/manifest.yml @@ -62,6 +62,6 @@ applications: LOGIN_DOT_GOV_CERTS_URL: ((LOGIN_DOT_GOV_CERTS_URL)) API_PUBLIC_URL: ((API_PUBLIC_URL)) - + API_PUBLIC_WS_URL: ((API_PUBLIC_WS_URL)) # feature flagging FEATURE_SOCKET_ENABLED: ((FEATURE_SOCKET_ENABLED)) diff --git a/tests/app/main/views/test_headers.py b/tests/app/main/views/test_headers.py index 8ba559499..5551d9035 100644 --- a/tests/app/main/views/test_headers.py +++ b/tests/app/main/views/test_headers.py @@ -25,12 +25,28 @@ def test_owasp_useful_headers_set( csp, ) assert search(r"'nonce-[^']+';", csp) - assert search(r"connect-src", csp) - assert search(r"https:\/\/gov-bam\.nr-data\.net", csp) - assert search(r"https:\/\/www\.google-analytics\.com", csp) - assert search(r"http:\/\/localhost:6011", csp) - assert search(r"ws:\/\/localhost:6011", csp) - assert search(r"https:\/\/notify-api-.*\.app\.cloud\.gov", csp) - assert search(r"wss:\/\/notify-api-.*\.app\.cloud\.gov", csp) + connect_src = next( + ( + directive + for directive in csp.split(";") + if directive.strip().startswith("connect-src") + ), + None, + ) + assert connect_src is not None, "connect-src directive is missing" + from flask import current_app + + config = current_app.config + expected_sources = { + "'self'", + "https://gov-bam.nr-data.net", + "https://www.google-analytics.com", + config["API_PUBLIC_URL"], + config["API_PUBLIC_WS_URL"], + } + actual_sources = set(connect_src.strip().split()[1:]) + assert ( + expected_sources <= actual_sources + ), f"Missing sources in connect-src: {expected_sources - actual_sources}" assert search(r"style-src 'self' static\.example\.com 'nonce-.*';", csp) assert search(r"img-src 'self' static\.example\.com static-logos\.test\.com", csp)