diff --git a/.ds.baseline b/.ds.baseline index c64d22627..56c3afc7d 100644 --- a/.ds.baseline +++ b/.ds.baseline @@ -161,7 +161,7 @@ "filename": "app/config.py", "hashed_secret": "577a4c667e4af8682ca431857214b3a920883efc", "is_verified": false, - "line_number": 125, + "line_number": 123, "is_secret": false } ], @@ -684,5 +684,5 @@ } ] }, - "generated_at": "2024-11-14T15:53:44Z" + "generated_at": "2024-11-21T23:08:45Z" } diff --git a/app/config.py b/app/config.py index f40b46dea..146230047 100644 --- a/app/config.py +++ b/app/config.py @@ -91,9 +91,7 @@ class Config(object): getenv("FEATURE_BEST_PRACTICES_ENABLED", "false") == "true" ) - FEATURE_ABOUT_PAGE_ENABLED = ( - getenv("FEATURE_ABOUT_PAGE_ENABLED", "false") == "true" - ) + FEATURE_ABOUT_PAGE_ENABLED = getenv("FEATURE_ABOUT_PAGE_ENABLED", "false") == "true" def _s3_credentials_from_env(bucket_prefix): diff --git a/app/main/views/index.py b/app/main/views/index.py index 9982a8b74..49eeae427 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -30,15 +30,13 @@ from notifications_utils.url_safe_token import generate_token # Hook to check for feature flags @main.before_request def check_feature_flags(): - if ( - request.path.startswith("/guides/best-practices") - and not current_app.config.get("FEATURE_BEST_PRACTICES_ENABLED", False) + if request.path.startswith("/guides/best-practices") and not current_app.config.get( + "FEATURE_BEST_PRACTICES_ENABLED", False ): abort(404) - if ( - request.path.startswith("/about") - and not current_app.config.get("FEATURE_ABOUT_PAGE_ENABLED", False) + if request.path.startswith("/about") and not current_app.config.get( + "FEATURE_ABOUT_PAGE_ENABLED", False ): abort(404) @@ -294,7 +292,6 @@ def benchmark_performance(): ) -@main.route("/using-notify/guidance") @main.route("/guides/using-notify/guidance") @user_is_logged_in def guidance_index(): @@ -315,6 +312,14 @@ def about_notify(): ) +@main.route("/about/security") +def about_security(): + return render_template( + "views/about/security.html", + navigation_links=about_notify_nav(), + ) + + @main.route("/about/why-text-messaging") def why_text_messaging(): return render_template( diff --git a/app/main/views/sub_navigation_dictionaries.py b/app/main/views/sub_navigation_dictionaries.py index 16aef0c17..603094cd1 100644 --- a/app/main/views/sub_navigation_dictionaries.py +++ b/app/main/views/sub_navigation_dictionaries.py @@ -1,3 +1,6 @@ +from flask import current_app + + def features_nav(): return [ { @@ -22,46 +25,20 @@ def features_nav(): def using_notify_nav(): - return [ - { - "name": "Get started", - "link": "main.get_started", - }, - { - "name": "Guides", - "link": "main.best_practices", - }, - { - "name": "Trial mode", - "link": "main.trial_mode_new", - }, - { - "name": "Tracking usage", - "link": "main.pricing", - }, - { - "name": "Delivery status", - "link": "main.message_status", - }, - { - "name": "Guidance", - "link": "main.guidance_index", - # "sub_navigation_items": [ - # { - # "name": "Formatting", - # "link": "main.edit_and_format_messages", - # }, - # { - # "name": "Send files by email", - # "link": "main.send_files_by_email", - # }, - # ] - # { - # "name": "API documentation", - # "link": "main.documentation", - # }, - }, + nav_items = [ + {"name": "Get started", "link": "main.get_started"}, + {"name": "Guides", "link": "main.best_practices"}, + {"name": "Trial mode", "link": "main.trial_mode_new"}, + {"name": "Tracking usage", "link": "main.pricing"}, + {"name": "Delivery Status", "link": "main.message_status"}, + {"name": "Guidance", "link": "main.guidance_index"}, ] + if not current_app.config.get("FEATURE_BEST_PRACTICES_ENABLED"): + nav_items = [ + item for item in nav_items if item["link"] != "main.best_practices" + ] + + return nav_items def best_practices_nav(): @@ -131,6 +108,10 @@ def about_notify_nav(): }, ], }, + { + "name": "Security", + "link": "main.about_security", + }, ], }, ] diff --git a/app/navigation.py b/app/navigation.py index a02df484d..271d6848b 100644 --- a/app/navigation.py +++ b/app/navigation.py @@ -53,7 +53,7 @@ class HeaderNavigation(Navigation): "establish_trust", "write_for_action", "multiple_languages", - "benchmark_performance" + "benchmark_performance", }, "using_notify": { "get_started", diff --git a/app/templates/views/about/security.html b/app/templates/views/about/security.html new file mode 100644 index 000000000..9ebc0420f --- /dev/null +++ b/app/templates/views/about/security.html @@ -0,0 +1,66 @@ +{% extends "base.html" %} + +{% set page_title = "Security" %} + +{% block per_page_title %} +{{page_title}} +{% endblock %} + +{% block content_column_content %} + +
+

{{page_title}}

+

Notify.gov is built for the needs of government agencies with fundamental system + security processes in place to: +

+ +

+ Notify.gov operates under a full three-year Authority-to-Operate (ATO). This + federal security authorization process leverages security + controls provided by National Institute of Standards and Technology (NIST). +

+ +

+ Our infrastructure runs on cloud.gov and utilizes several + services through Amazon Web + Services (AWS), including + AWS SNS for sending SMS + messages. +

+

For more information about the Notify.gov infrastructure, contact us at notify-support@gsa.gov.

+

Data

+

+ On Notify.gov, data is encrypted both in transit and at rest. To send a message, agencies upload a spreadsheet of + phone numbers and other necessary data from their existing data management system. +

+

+ Notify.gov is not a system of record, so it does not have a System of Records Notice (SORN). Agencies are + responsible for managing their data outside of Notify.gov. +

+

Data retention

+

+ Any data uploads that have recipient data are held for seven calendar days; personally identifiable information + (PII) is never stored in Notify’s database. +

+

Multi-Factor Authentication

+

+ Notify.gov uses Login.gov for enhanced security. + Login.gov is an extra layer of security created by the government that uses multi-factor authentication and stronger + passwords to protect your account. +

+

+ To access Notify.gov, users will use a Login.gov account associated with their agency (.gov) email with one of the + multi-factor authentication + methods offered through Login.gov. +

+
+{% endblock %} diff --git a/app/utils/csv.py b/app/utils/csv.py index 4ed6d16b5..79e3535c8 100644 --- a/app/utils/csv.py +++ b/app/utils/csv.py @@ -103,6 +103,7 @@ def generate_notifications_csv(**kwargs): "Carrier Response", "Status", "Time", + "Carrier", ] for header in original_column_headers: if header.lower() != "phone number": @@ -118,6 +119,7 @@ def generate_notifications_csv(**kwargs): "Carrier Response", "Status", "Time", + "Carrier", ] yield ",".join(fieldnames) + "\n" @@ -140,6 +142,7 @@ def generate_notifications_csv(**kwargs): notification["provider_response"], notification["status"], preferred_tz_created_at, + notification["carrier"], ] for header in original_column_headers: if header.lower() != "phone number": @@ -158,6 +161,7 @@ def generate_notifications_csv(**kwargs): notification["provider_response"], notification["status"], preferred_tz_created_at, + notification["carrier"], ] yield Spreadsheet.from_rows([map(str, values)]).as_csv_data diff --git a/deploy-config/production.yml b/deploy-config/production.yml index 42a681cb4..9f5cffc89 100644 --- a/deploy-config/production.yml +++ b/deploy-config/production.yml @@ -1,6 +1,6 @@ env: production instances: 2 -memory: 1.5G +memory: 2G command: newrelic-admin run-program gunicorn -c /home/vcap/app/gunicorn_config.py application public_admin_route: beta.notify.gov cloud_dot_gov_route: notify.app.cloud.gov diff --git a/tests/app/test_navigation.py b/tests/app/test_navigation.py index f74a0cfea..f469f2586 100644 --- a/tests/app/test_navigation.py +++ b/tests/app/test_navigation.py @@ -18,6 +18,7 @@ EXCLUDED_ENDPOINTS = tuple( Navigation.get_endpoint_with_blueprint, { "about_notify", + "about_security", "accept_invite", "accept_org_invite", "accessibility_statement", diff --git a/tests/app/utils/test_csv.py b/tests/app/utils/test_csv.py index d603fcd0e..db4b6a0ec 100644 --- a/tests/app/utils/test_csv.py +++ b/tests/app/utils/test_csv.py @@ -58,6 +58,7 @@ def _get_notifications_csv( "to": recipient, "recipient": recipient, "client_reference": "ref 1234", + "carrier": "AT&T Mobility", } for i in range(rows) ], @@ -88,15 +89,15 @@ def get_notifications_csv_mock( ( None, [ - "Phone Number,Template,Sent by,Batch File,Carrier Response,Status,Time\n", - "8005555555,foo,,,Did not like it,Delivered,1943-04-19 08:00:00 AM US/Eastern\r\n", + "Phone Number,Template,Sent by,Batch File,Carrier Response,Status,Time,Carrier\n", + "8005555555,foo,,,Did not like it,Delivered,1943-04-19 08:00:00 AM US/Eastern,AT&T Mobility\r\n", ], ), ( "Anne Example", [ - "Phone Number,Template,Sent by,Batch File,Carrier Response,Status,Time\n", - "8005555555,foo,Anne Example,,Did not like it,Delivered,1943-04-19 08:00:00 AM US/Eastern\r\n", # noqa + "Phone Number,Template,Sent by,Batch File,Carrier Response,Status,Time,Carrier\n", + "8005555555,foo,Anne Example,,Did not like it,Delivered,1943-04-19 08:00:00 AM US/Eastern,AT&T Mobility\r\n", # noqa ], ), ], @@ -135,6 +136,7 @@ def test_generate_notifications_csv_without_job( "Carrier Response", "Status", "Time", + "Carrier", ], [ "8005555555", @@ -144,6 +146,7 @@ def test_generate_notifications_csv_without_job( "Did not like it", "Delivered", "1943-04-19 08:00:00 AM US/Eastern", + "AT&T Mobility", ], ), ( @@ -159,6 +162,7 @@ def test_generate_notifications_csv_without_job( "Carrier Response", "Status", "Time", + "Carrier", "a", "b", "c", @@ -171,6 +175,7 @@ def test_generate_notifications_csv_without_job( "Did not like it", "Delivered", "1943-04-19 08:00:00 AM US/Eastern", + "AT&T Mobility", "🐜", "🐝", "πŸ¦€", @@ -189,6 +194,7 @@ def test_generate_notifications_csv_without_job( "Carrier Response", "Status", "Time", + "Carrier", "a", "b", "c", @@ -201,6 +207,7 @@ def test_generate_notifications_csv_without_job( "Did not like it", "Delivered", "1943-04-19 08:00:00 AM US/Eastern", + "AT&T Mobility", "🐜,🐜", "🐝,🐝", "πŸ¦€", diff --git a/tests/end_to_end/test_best_practices_content_pages.py b/tests/end_to_end/test_best_practices_content_pages.py index 962100de3..031e4baef 100644 --- a/tests/end_to_end/test_best_practices_content_pages.py +++ b/tests/end_to_end/test_best_practices_content_pages.py @@ -11,7 +11,7 @@ E2E_TEST_URI = os.getenv("NOTIFY_E2E_TEST_URI") def test_best_practices_side_menu(authenticated_page): page = authenticated_page - page.goto(f"{E2E_TEST_URI}/best-practices") + page.goto(f"{E2E_TEST_URI}/guides/best-practices") page.wait_for_load_state("domcontentloaded") check_axe_report(page) @@ -58,7 +58,7 @@ def test_best_practices_side_menu(authenticated_page): def test_breadcrumbs_best_practices(authenticated_page): page = authenticated_page - page.goto(f"{E2E_TEST_URI}/best-practices") + page.goto(f"{E2E_TEST_URI}/guides/best-practices") page.wait_for_load_state("domcontentloaded") check_axe_report(page) diff --git a/urls.js b/urls.js index 0440430ec..2279b5a79 100644 --- a/urls.js +++ b/urls.js @@ -8,7 +8,7 @@ const sublinks = [ { label: 'Trial Mode', path: '/using-notify/trial-mode' }, { label: 'Pricing', path: '/using-notify/pricing' }, { label: 'Delivery Status', path: '/using-notify/delivery-status' }, - { label: 'Guidance', path: '/using-notify/guidance' }, + { label: 'Guidance', path: '/guides/using-notify/guidance' }, { label: 'Features', path: '/features' }, { label: 'Roadmap', path: '/features/roadmap' }, { label: 'Security', path: '/features/security' },