diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5a2d59fa502..113ee609299 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -484,6 +484,31 @@ stop-review-app: include: - template: Jobs/SAST.gitlab-ci.yml - template: Jobs/Dependency-Scanning.gitlab-ci.yml + - template: Security/Secret-Detection.gitlab-ci.yml + +secret_detection: + allow_failure: false + variables: + SECRET_DETECTION_LOG_OPTIONS: origin/${CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME}..HEAD + SECRET_DETECTION_REPORT_FILE: "gl-secret-detection-report.json" + rules: + - if: $SECRET_DETECTION_DISABLED + when: never + - if: '$CI_COMMIT_BRANCH || $CI_COMMIT_TAG' + before_script: + - apk add --no-cache jq + script: + - /analyzer run + # check if '{ "vulnerabilities": [], ..' is empty in the report file if it exists + - | + if [ -f "$SECRET_DETECTION_REPORT_FILE" ]; then + if [ "$(jq ".vulnerabilities | length" $SECRET_DETECTION_REPORT_FILE)" -gt 0 ]; then + echo "Vulnerabilities detected. Please analyze the artifact $SECRET_DETECTION_REPORT_FILE produced by the 'secret-detection' job." + exit 80 + fi + else + echo "Artifact $SECRET_DETECTION_REPORT_FILE does not exist. The 'secret-detection' job likely didn't create one. Hence, no evaluation can be performed." + fi .container_scan_template: interruptible: true diff --git a/app/assets/images/alert/icon-lock-alert-important.svg b/app/assets/images/alert/icon-lock-alert-important.svg index 47e94193ac7..0a1190b3cd9 100644 --- a/app/assets/images/alert/icon-lock-alert-important.svg +++ b/app/assets/images/alert/icon-lock-alert-important.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/alert/success.svg b/app/assets/images/alert/success.svg index 44da527697a..3f25b860f7a 100644 --- a/app/assets/images/alert/success.svg +++ b/app/assets/images/alert/success.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/alert/unphishable.svg b/app/assets/images/alert/unphishable.svg index 2b557fef831..9d479e3413c 100644 --- a/app/assets/images/alert/unphishable.svg +++ b/app/assets/images/alert/unphishable.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/alert/warning.svg b/app/assets/images/alert/warning.svg index 6fa53a4d200..4ec85cd21d7 100644 --- a/app/assets/images/alert/warning.svg +++ b/app/assets/images/alert/warning.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/carat-right.svg b/app/assets/images/carat-right.svg index 64c1dd2d6e2..dc2a091c774 100644 --- a/app/assets/images/carat-right.svg +++ b/app/assets/images/carat-right.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/come-back.svg b/app/assets/images/come-back.svg index 38875a9104d..e21ef79649b 100644 --- a/app/assets/images/come-back.svg +++ b/app/assets/images/come-back.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/get-started/2FA.svg b/app/assets/images/get-started/2FA.svg index b7426c71062..032b46042d8 100644 --- a/app/assets/images/get-started/2FA.svg +++ b/app/assets/images/get-started/2FA.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/get-started/ID.svg b/app/assets/images/get-started/ID.svg index 4282205234a..90cc036a476 100644 --- a/app/assets/images/get-started/ID.svg +++ b/app/assets/images/get-started/ID.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/get-started/email-password.svg b/app/assets/images/get-started/email-password.svg index d15f937574a..4eba2101c0a 100644 --- a/app/assets/images/get-started/email-password.svg +++ b/app/assets/images/get-started/email-password.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/get-started/financial.svg b/app/assets/images/get-started/financial.svg index 9bd71dbbbb0..019514da824 100644 --- a/app/assets/images/get-started/financial.svg +++ b/app/assets/images/get-started/financial.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/get-started/personal-details.svg b/app/assets/images/get-started/personal-details.svg index 38be0aa5b65..20c495c201a 100644 --- a/app/assets/images/get-started/personal-details.svg +++ b/app/assets/images/get-started/personal-details.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/get-started/personal-key.svg b/app/assets/images/get-started/personal-key.svg index b375bd4479d..1793eb6355a 100644 --- a/app/assets/images/get-started/personal-key.svg +++ b/app/assets/images/get-started/personal-key.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/globe-blue.svg b/app/assets/images/globe-blue.svg index 111117296e7..6239302fac9 100644 --- a/app/assets/images/globe-blue.svg +++ b/app/assets/images/globe-blue.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/globe-white.svg b/app/assets/images/globe-white.svg index b834cfcbb49..0d680abd897 100644 --- a/app/assets/images/globe-white.svg +++ b/app/assets/images/globe-white.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/icon-https.svg b/app/assets/images/icon-https.svg index 0eb37008f18..a633e5e0094 100644 --- a/app/assets/images/icon-https.svg +++ b/app/assets/images/icon-https.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/icon-lock-alert-important.svg b/app/assets/images/icon-lock-alert-important.svg index 47e94193ac7..0a1190b3cd9 100644 --- a/app/assets/images/icon-lock-alert-important.svg +++ b/app/assets/images/icon-lock-alert-important.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/id-card.svg b/app/assets/images/id-card.svg index b1336c20c74..4b6cd53b6d9 100644 --- a/app/assets/images/id-card.svg +++ b/app/assets/images/id-card.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/idv/laptop-icon.svg b/app/assets/images/idv/laptop-icon.svg index 680054edaf3..434b1cc5039 100644 --- a/app/assets/images/idv/laptop-icon.svg +++ b/app/assets/images/idv/laptop-icon.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/idv/phone-icon.svg b/app/assets/images/idv/phone-icon.svg index b409f134cb6..d7323041d7f 100644 --- a/app/assets/images/idv/phone-icon.svg +++ b/app/assets/images/idv/phone-icon.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/idv/user-in-person.svg b/app/assets/images/idv/user-in-person.svg index e80083004bb..a48daa7ebf7 100644 --- a/app/assets/images/idv/user-in-person.svg +++ b/app/assets/images/idv/user-in-person.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/info-pin-map.svg b/app/assets/images/info-pin-map.svg index 5f31cbcfaca..a01258bf679 100644 --- a/app/assets/images/info-pin-map.svg +++ b/app/assets/images/info-pin-map.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/lock.svg b/app/assets/images/lock.svg index 6ca5c06bd4b..82489d82462 100644 --- a/app/assets/images/lock.svg +++ b/app/assets/images/lock.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/logo-white.svg b/app/assets/images/logo-white.svg index ad9bd07e677..1683396c7b1 100644 --- a/app/assets/images/logo-white.svg +++ b/app/assets/images/logo-white.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/logo.svg b/app/assets/images/logo.svg index 6577834188e..d6a1abd991b 100644 --- a/app/assets/images/logo.svg +++ b/app/assets/images/logo.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/mfa-options/auth_app.svg b/app/assets/images/mfa-options/auth_app.svg index 8c4279bf7d3..e62097008f4 100644 --- a/app/assets/images/mfa-options/auth_app.svg +++ b/app/assets/images/mfa-options/auth_app.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/mfa-options/backup_code.svg b/app/assets/images/mfa-options/backup_code.svg index 2f0567135d1..2ea2ce4042c 100644 --- a/app/assets/images/mfa-options/backup_code.svg +++ b/app/assets/images/mfa-options/backup_code.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/mfa-options/phone.svg b/app/assets/images/mfa-options/phone.svg index 5c812b76465..5bcae08c142 100644 --- a/app/assets/images/mfa-options/phone.svg +++ b/app/assets/images/mfa-options/phone.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/mfa-options/piv_cac.svg b/app/assets/images/mfa-options/piv_cac.svg index 61884ec3a80..f3341be00da 100644 --- a/app/assets/images/mfa-options/piv_cac.svg +++ b/app/assets/images/mfa-options/piv_cac.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/mfa-options/security-key-icon.svg b/app/assets/images/mfa-options/security-key-icon.svg index 1a9a2adb89c..57ba60b1150 100644 --- a/app/assets/images/mfa-options/security-key-icon.svg +++ b/app/assets/images/mfa-options/security-key-icon.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/mfa-options/webauthn.svg b/app/assets/images/mfa-options/webauthn.svg index 98a1a9828fe..9d2a1ae4f78 100644 --- a/app/assets/images/mfa-options/webauthn.svg +++ b/app/assets/images/mfa-options/webauthn.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/mfa-options/webauthn_platform.svg b/app/assets/images/mfa-options/webauthn_platform.svg index c95e42cb3b9..eca65bb36b6 100644 --- a/app/assets/images/mfa-options/webauthn_platform.svg +++ b/app/assets/images/mfa-options/webauthn_platform.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/personal-key/email.svg b/app/assets/images/personal-key/email.svg index 68801b1ab10..a4ebfd7d445 100644 --- a/app/assets/images/personal-key/email.svg +++ b/app/assets/images/personal-key/email.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/personal-key/pkey-block.svg b/app/assets/images/personal-key/pkey-block.svg index b37c230c647..3dc022e9f2e 100644 --- a/app/assets/images/personal-key/pkey-block.svg +++ b/app/assets/images/personal-key/pkey-block.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/personal-key/shield.svg b/app/assets/images/personal-key/shield.svg index 04b34cc9887..df9ebdbf1fe 100644 --- a/app/assets/images/personal-key/shield.svg +++ b/app/assets/images/personal-key/shield.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/personal-key/warning.svg b/app/assets/images/personal-key/warning.svg index 6fa53a4d200..4ec85cd21d7 100644 --- a/app/assets/images/personal-key/warning.svg +++ b/app/assets/images/personal-key/warning.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/platform-authenticator.svg b/app/assets/images/platform-authenticator.svg index 1bf063b51ba..9d324b09068 100644 --- a/app/assets/images/platform-authenticator.svg +++ b/app/assets/images/platform-authenticator.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/plus.svg b/app/assets/images/plus.svg index 41888145942..7d4cdc49e3c 100644 --- a/app/assets/images/plus.svg +++ b/app/assets/images/plus.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/security-key.svg b/app/assets/images/security-key.svg index d81a0ba67fa..d207e6b7db1 100644 --- a/app/assets/images/security-key.svg +++ b/app/assets/images/security-key.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/sign-in.svg b/app/assets/images/sign-in.svg index 3215afa4ec1..a7211e5d3f4 100644 --- a/app/assets/images/sign-in.svg +++ b/app/assets/images/sign-in.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/sp-logos/generic.svg b/app/assets/images/sp-logos/generic.svg index 89d24209556..66be92c3651 100644 --- a/app/assets/images/sp-logos/generic.svg +++ b/app/assets/images/sp-logos/generic.svg @@ -1 +1 @@ -GovernmentAgency NamePlaceholder \ No newline at end of file +GovernmentAgency NamePlaceholder \ No newline at end of file diff --git a/app/assets/images/sp-logos/square-gsa-dark.svg b/app/assets/images/sp-logos/square-gsa-dark.svg index 62fc5eeaba2..79d64578262 100644 --- a/app/assets/images/sp-logos/square-gsa-dark.svg +++ b/app/assets/images/sp-logos/square-gsa-dark.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/sp-logos/square-gsa.svg b/app/assets/images/sp-logos/square-gsa.svg index 92e87824489..4af7b9dfa2b 100644 --- a/app/assets/images/sp-logos/square-gsa.svg +++ b/app/assets/images/sp-logos/square-gsa.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/status/delete.svg b/app/assets/images/status/delete.svg index d4f411858ef..1b7b8fec5cf 100644 --- a/app/assets/images/status/delete.svg +++ b/app/assets/images/status/delete.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/status/error-lock.svg b/app/assets/images/status/error-lock.svg index b61bdbebcc8..3e94b7d262f 100644 --- a/app/assets/images/status/error-lock.svg +++ b/app/assets/images/status/error-lock.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/status/error.svg b/app/assets/images/status/error.svg index 8deea0a91da..131e4c967fa 100644 --- a/app/assets/images/status/error.svg +++ b/app/assets/images/status/error.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/status/info-question.svg b/app/assets/images/status/info-question.svg index 79c60dcfcf5..1ecfc8720cf 100644 --- a/app/assets/images/status/info-question.svg +++ b/app/assets/images/status/info-question.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/status/personal-key.svg b/app/assets/images/status/personal-key.svg index f67e726ae96..9840e70f095 100644 --- a/app/assets/images/status/personal-key.svg +++ b/app/assets/images/status/personal-key.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/status/warning.svg b/app/assets/images/status/warning.svg index 6fa53a4d200..4ec85cd21d7 100644 --- a/app/assets/images/status/warning.svg +++ b/app/assets/images/status/warning.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/us-flag.png b/app/assets/images/us-flag.png deleted file mode 100644 index 8ae515a4d7d..00000000000 Binary files a/app/assets/images/us-flag.png and /dev/null differ diff --git a/app/assets/images/us_flag.svg b/app/assets/images/us_flag.svg new file mode 100644 index 00000000000..275d57b9735 --- /dev/null +++ b/app/assets/images/us_flag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/user-access.svg b/app/assets/images/user-access.svg index e31ec27ee1d..163d5ab6b6d 100644 --- a/app/assets/images/user-access.svg +++ b/app/assets/images/user-access.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/user-signup-ial1.svg b/app/assets/images/user-signup-ial1.svg index d58709e6db3..7a39cef53a5 100644 --- a/app/assets/images/user-signup-ial1.svg +++ b/app/assets/images/user-signup-ial1.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/user-signup-ial2.svg b/app/assets/images/user-signup-ial2.svg index 3308435e5c4..2c49bc0a84b 100644 --- a/app/assets/images/user-signup-ial2.svg +++ b/app/assets/images/user-signup-ial2.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/user.svg b/app/assets/images/user.svg index d0de2f5793c..6074e80aeda 100644 --- a/app/assets/images/user.svg +++ b/app/assets/images/user.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/verified.svg b/app/assets/images/verified.svg index 391191ab826..ee412c8f683 100644 --- a/app/assets/images/verified.svg +++ b/app/assets/images/verified.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/webauthn-verified.svg b/app/assets/images/webauthn-verified.svg index cd44197f765..0c186a7827f 100644 --- a/app/assets/images/webauthn-verified.svg +++ b/app/assets/images/webauthn-verified.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/stylesheets/_uswds-core.scss b/app/assets/stylesheets/_uswds-core.scss index 3c4b46b3bbf..94b1aeb15da 100644 --- a/app/assets/stylesheets/_uswds-core.scss +++ b/app/assets/stylesheets/_uswds-core.scss @@ -1,8 +1,6 @@ @forward '@18f/identity-design-system/packages/uswds-core' with ( $theme-body-font-size: 'sm', $theme-font-path: '', - $theme-font-role-heading: 'sans', - $theme-font-type-serif: false, $theme-image-path: '', $theme-global-border-box-sizing: true, $theme-global-link-styles: true, diff --git a/app/assets/stylesheets/utilities/_typography.scss b/app/assets/stylesheets/utilities/_typography.scss index 5db68043ba7..522e0e0c508 100644 --- a/app/assets/stylesheets/utilities/_typography.scss +++ b/app/assets/stylesheets/utilities/_typography.scss @@ -1,10 +1,6 @@ @use 'uswds-core' as *; @use '../variables/app' as *; -body { - -webkit-font-smoothing: antialiased; -} - .text-wrap-anywhere { overflow-wrap: anywhere; } diff --git a/app/controllers/concerns/idv/verify_info_concern.rb b/app/controllers/concerns/idv/verify_info_concern.rb index 03b3dde0be9..3560c6c4224 100644 --- a/app/controllers/concerns/idv/verify_info_concern.rb +++ b/app/controllers/concerns/idv/verify_info_concern.rb @@ -332,8 +332,10 @@ def add_proofing_costs(results) add_cost(:lexis_nexis_resolution, transaction_id: hash[:transaction_id]) elsif stage == :state_id next if hash[:exception].present? + next if hash[:vendor_name] == 'UnsupportedJurisdiction' + # transaction_id comes from TransactionLocatorId add_cost(:aamva, transaction_id: hash[:transaction_id]) - track_aamva unless hash[:vendor_name] == 'UnsupportedJurisdiction' + track_aamva elsif stage == :threatmetrix # transaction_id comes from request_id tmx_id = hash[:transaction_id] diff --git a/app/controllers/concerns/second_mfa_reminder_concern.rb b/app/controllers/concerns/second_mfa_reminder_concern.rb index c24f3058026..ca7fb29e569 100644 --- a/app/controllers/concerns/second_mfa_reminder_concern.rb +++ b/app/controllers/concerns/second_mfa_reminder_concern.rb @@ -1,6 +1,5 @@ module SecondMfaReminderConcern def user_needs_second_mfa_reminder? - return false unless IdentityConfig.store.second_mfa_reminder_enabled return false if user_has_dismissed_second_mfa_reminder? return false if second_mfa_enrollment_may_downgrade_for_service_provider_mfa_requirement? return false if user_has_multiple_mfa_methods? diff --git a/app/controllers/idv/capture_doc_status_controller.rb b/app/controllers/idv/capture_doc_status_controller.rb index 9e294d79439..670721751a6 100644 --- a/app/controllers/idv/capture_doc_status_controller.rb +++ b/app/controllers/idv/capture_doc_status_controller.rb @@ -20,7 +20,8 @@ def status :too_many_requests elsif confirmed_barcode_attention_result? || user_has_establishing_in_person_enrollment? :ok - elsif session_result.blank? || pending_barcode_attention_confirmation? + elsif session_result.blank? || pending_barcode_attention_confirmation? || + redo_document_capture_pending? :accepted elsif !session_result.success? :unauthorized @@ -68,11 +69,13 @@ def user_has_establishing_in_person_enrollment? end def confirmed_barcode_attention_result? - had_barcode_attention_result? && !document_capture_session.ocr_confirmation_pending? + !redo_document_capture_pending? && had_barcode_attention_result? && + !document_capture_session.ocr_confirmation_pending? end def pending_barcode_attention_confirmation? - had_barcode_attention_result? && document_capture_session.ocr_confirmation_pending? + !redo_document_capture_pending? && had_barcode_attention_result? && + document_capture_session.ocr_confirmation_pending? end def had_barcode_attention_result? @@ -90,5 +93,12 @@ def idv_session service_provider: current_sp, ) end + + def redo_document_capture_pending? + return unless session_result&.dig(:captured_at) + return unless document_capture_session.requested_at + + document_capture_session.requested_at > session_result.captured_at + end end end diff --git a/app/controllers/idv/document_capture_controller.rb b/app/controllers/idv/document_capture_controller.rb index 2e4eb7c7879..6f24377a3e6 100644 --- a/app/controllers/idv/document_capture_controller.rb +++ b/app/controllers/idv/document_capture_controller.rb @@ -20,6 +20,9 @@ def show def update idv_session.redo_document_capture = nil # done with this redo + # Not used in standard flow, here for data consistency with hybrid flow. + document_capture_session.confirm_ocr + result = handle_stored_result analytics.idv_doc_auth_document_capture_submitted(**result.to_h.merge(analytics_arguments)) diff --git a/app/controllers/idv/hybrid_mobile/document_capture_controller.rb b/app/controllers/idv/hybrid_mobile/document_capture_controller.rb index 2241569439d..b6f9e2678d2 100644 --- a/app/controllers/idv/hybrid_mobile/document_capture_controller.rb +++ b/app/controllers/idv/hybrid_mobile/document_capture_controller.rb @@ -6,13 +6,9 @@ class DocumentCaptureController < ApplicationController before_action :check_valid_document_capture_session before_action :override_csp_to_allow_acuant + before_action :confirm_document_capture_needed, only: :show def show - if document_capture_session&.load_result&.success? - redirect_to idv_hybrid_mobile_capture_complete_url - return - end - analytics.idv_doc_auth_document_capture_visited(**analytics_arguments) Funnel::DocAuth::RegisterStep.new(document_capture_user.id, sp_session[:issuer]). @@ -22,6 +18,7 @@ def show end def update + document_capture_session.confirm_ocr result = handle_stored_result analytics.idv_doc_auth_document_capture_submitted(**result.to_h.merge(analytics_arguments)) @@ -69,6 +66,20 @@ def handle_stored_result failure(I18n.t('doc_auth.errors.general.network_error'), extra) end end + + def confirm_document_capture_needed + return unless stored_result&.success? + return if redo_document_capture_pending? + + redirect_to idv_hybrid_mobile_capture_complete_url + end + + def redo_document_capture_pending? + return unless stored_result&.dig(:captured_at) + return unless document_capture_session.requested_at + + document_capture_session.requested_at > stored_result.captured_at + end end end end diff --git a/app/controllers/idv/link_sent_controller.rb b/app/controllers/idv/link_sent_controller.rb index d0c239dea7e..622aec47f95 100644 --- a/app/controllers/idv/link_sent_controller.rb +++ b/app/controllers/idv/link_sent_controller.rb @@ -25,6 +25,7 @@ def update # The doc capture flow will have fetched the results already. We need # to fetch them again here to add the PII to this session handle_document_verification_success(document_capture_session_result) + idv_session.redo_document_capture = nil redirect_to idv_ssn_url end diff --git a/app/controllers/reactivate_account_controller.rb b/app/controllers/reactivate_account_controller.rb index 4ffe8267361..bc76afd778a 100644 --- a/app/controllers/reactivate_account_controller.rb +++ b/app/controllers/reactivate_account_controller.rb @@ -5,10 +5,12 @@ class ReactivateAccountController < ApplicationController before_action :confirm_password_reset_profile def index + analytics.reactivate_account_visit @personal_key_generated_at = current_user.personal_key_generated_at end def update + analytics.reactivate_account_submit reactivate_account_session.suspend redirect_to idv_url end diff --git a/app/javascript/packages/clipboard-button/package.json b/app/javascript/packages/clipboard-button/package.json index 917a4b4ccd3..8d62cf41a08 100644 --- a/app/javascript/packages/clipboard-button/package.json +++ b/app/javascript/packages/clipboard-button/package.json @@ -3,6 +3,6 @@ "version": "1.0.0", "private": true, "dependencies": { - "@18f/identity-design-system": "^7.1.0" + "@18f/identity-design-system": "^8.0.0" } } diff --git a/app/javascript/packages/document-capture/components/submission-complete.jsx b/app/javascript/packages/document-capture/components/submission-complete.tsx similarity index 55% rename from app/javascript/packages/document-capture/components/submission-complete.jsx rename to app/javascript/packages/document-capture/components/submission-complete.tsx index 3c7ca5801b5..6405308edf8 100644 --- a/app/javascript/packages/document-capture/components/submission-complete.jsx +++ b/app/javascript/packages/document-capture/components/submission-complete.tsx @@ -1,31 +1,27 @@ import { useState, useContext, useRef } from 'react'; import CallbackOnMount from './callback-on-mount'; import UploadContext from '../context/upload'; +import type { UploadSuccessResponse } from '../context/upload'; -/** @typedef {import('../context/upload').UploadSuccessResponse} UploadSuccessResponse */ - -/** - * @typedef Resource - * - * @prop {()=>T} read Resource reader. - * - * @template T - */ +interface Resource { + /** + * Resource reader. + */ + read: () => T; +} -/** - * @typedef SubmissionCompleteProps - * - * @prop {Resource} resource Resource object. - */ +interface SubmissionCompleteProps { + /** + * Resource object. + */ + resource: Resource; +} export class RetrySubmissionError extends Error {} -/** - * @param {SubmissionCompleteProps} props Props object. - */ -function SubmissionComplete({ resource }) { - const [, setRetryError] = useState(/** @type {Error=} */ (undefined)); - const sleepTimeout = useRef(/** @type {number=} */ (undefined)); +function SubmissionComplete({ resource }: SubmissionCompleteProps) { + const [, setRetryError] = useState(undefined); + const sleepTimeout = useRef(); const { statusPollInterval } = useContext(UploadContext); const response = resource.read(); @@ -39,8 +35,7 @@ function SubmissionComplete({ resource }) { }, statusPollInterval); } } else { - /** @type {HTMLFormElement?} */ - const form = document.querySelector('.js-document-capture-form'); + const form = document.querySelector('.js-document-capture-form'); form?.submit(); } diff --git a/app/jobs/reports/authentication_report.rb b/app/jobs/reports/authentication_report.rb new file mode 100644 index 00000000000..c530048f643 --- /dev/null +++ b/app/jobs/reports/authentication_report.rb @@ -0,0 +1,43 @@ +require 'reporting/authentication_report' + +module Reports + class AuthenticationReport < BaseReport + REPORT_NAME = 'authentication-report' + + attr_accessor :report_date + + def perform(report_date) + return unless IdentityConfig.store.s3_reports_enabled + + self.report_date = report_date + message = "Report: #{REPORT_NAME} #{report_date}" + subject = "Weekly Authentication Report - #{report_date}" + + report_configs.each do |report_hash| + tables = weekly_authentication_report_tables(report_hash['issuers']) + + report_hash['emails'].each do |email| + ReportMailer.tables_report( + email:, + subject:, + message:, + tables:, + ).deliver_now + end + end + end + + private + + def weekly_authentication_report_tables(issuers) + Reporting::AuthenticationReport.new( + issuers:, + time_range: report_date.all_week, + ).as_tables_with_options + end + + def report_configs + IdentityConfig.store.weekly_auth_funnel_report_config + end + end +end diff --git a/app/jobs/reports/base_report.rb b/app/jobs/reports/base_report.rb index e424ffc8f49..e2148679d4d 100644 --- a/app/jobs/reports/base_report.rb +++ b/app/jobs/reports/base_report.rb @@ -62,10 +62,11 @@ def upload_file_to_s3_timestamped_and_latest(report_name, body, extension) url end - def generate_s3_paths(name, extension, now: Time.zone.now) + def generate_s3_paths(name, extension, subname: nil, now: Time.zone.now) host_data_env = Identity::Hostdata.env - latest = "#{host_data_env}/#{name}/latest.#{name}.#{extension}" - [latest, "#{host_data_env}/#{name}/#{now.year}/#{now.strftime('%F')}.#{name}.#{extension}"] + name_subdir_ext = "#{name}#{subname ? '/' : ''}#{subname}.#{extension}" + latest = "#{host_data_env}/#{name}/latest.#{name_subdir_ext}" + [latest, "#{host_data_env}/#{name}/#{now.year}/#{now.strftime('%F')}.#{name_subdir_ext}"] end def logger diff --git a/app/jobs/reports/monthly_key_metrics_report.rb b/app/jobs/reports/monthly_key_metrics_report.rb index 7ba755a7862..8438089d1b6 100644 --- a/app/jobs/reports/monthly_key_metrics_report.rb +++ b/app/jobs/reports/monthly_key_metrics_report.rb @@ -6,9 +6,30 @@ class MonthlyKeyMetricsReport < BaseReport attr_reader :report_date - def perform(date) + def perform(date = Time.zone.today) @report_date = date - csv_for_email = monthly_key_metrics_report_array + + account_reuse_table = account_reuse_report.account_reuse_report + total_profiles_table = account_reuse_report.total_identities_report + + upload_to_s3(account_reuse_table, report_name: 'account_reuse') + upload_to_s3(total_profiles_table, report_name: 'total_profiles') + + email_tables = [ + [ + { + title: "IDV app reuse rate #{account_reuse_report.stats_month}", + float_as_percent: true, + precision: 4, + }, + *account_reuse_table, + ], + [ + { title: 'Total proofed identities' }, + *total_profiles_table, + ], + ] + email_message = "Report: #{REPORT_NAME} #{date}" email_addresses = emails.select(&:present?) @@ -17,7 +38,7 @@ def perform(date) email: email_addresses, subject: "Monthly Key Metrics Report - #{date}", message: email_message, - tables: csv_for_email, + tables: email_tables, ).deliver_now else Rails.logger.warn 'No email addresses received - Monthly Key Metrics Report NOT SENT' @@ -32,19 +53,29 @@ def emails emails end - def monthly_key_metrics_report_array - csv_array = [] + def account_reuse_report + @account_reuse_report ||= Reporting::AccountReuseAndTotalIdentitiesReport.new(report_date) + end - account_reuse_report_csv.each do |row| - csv_array << row - end + def upload_to_s3(report_body, report_name: nil) + _latest, path = generate_s3_paths(REPORT_NAME, 'csv', subname: report_name, now: report_date) - csv_array + if bucket_name.present? + upload_file_to_s3_bucket( + path: path, + body: csv_file(report_body), + content_type: 'text/csv', + bucket: bucket_name, + ) + end end - # Individual Key Metric Report - def account_reuse_report_csv - Reports::MonthlyAccountReuseReport.new(report_date).report_csv + def csv_file(report_array) + CSV.generate do |csv| + report_array.each do |row| + csv << row + end + end end end end diff --git a/app/models/document_capture_session.rb b/app/models/document_capture_session.rb index 95e0b2bf218..ac9bd7a8215 100644 --- a/app/models/document_capture_session.rb +++ b/app/models/document_capture_session.rb @@ -14,6 +14,7 @@ def store_result_from_response(doc_auth_response) ) session_result.success = doc_auth_response.success? session_result.pii = doc_auth_response.pii_from_doc + session_result.captured_at = Time.zone.now session_result.attention_with_barcode = doc_auth_response.attention_with_barcode? EncryptedRedisStructStorage.store( session_result, @@ -28,6 +29,7 @@ def store_failed_auth_image_fingerprint(front_image_fingerprint, back_image_fing id: generate_result_id, ) session_result.success = false + session_result.captured_at = Time.zone.now session_result.add_failed_front_image!(front_image_fingerprint) if front_image_fingerprint session_result.add_failed_back_image!(back_image_fingerprint) if back_image_fingerprint EncryptedRedisStructStorage.store( @@ -52,20 +54,6 @@ def create_doc_auth_session save! end - def store_doc_auth_result(result:, pii:) - EncryptedRedisStructStorage.store( - DocumentCaptureSessionAsyncResult.new( - id: result_id, - pii: pii, - result: result, - status: DocumentCaptureSessionAsyncResult::DONE, - ), - expires_in: IdentityConfig.store.async_wait_timeout_seconds, - ) - self.ocr_confirmation_pending = result[:attention_with_barcode] - save! - end - def load_proofing_result EncryptedRedisStructStorage.load(result_id, type: ProofingSessionAsyncResult) end @@ -99,6 +87,12 @@ def expired? Time.zone.now end + def confirm_ocr + return unless self.ocr_confirmation_pending + + update!(ocr_confirmation_pending: false) + end + private def generate_result_id diff --git a/app/presenters/two_factor_login_options_presenter.rb b/app/presenters/two_factor_login_options_presenter.rb index c9ddcdd710e..ed30b21e87f 100644 --- a/app/presenters/two_factor_login_options_presenter.rb +++ b/app/presenters/two_factor_login_options_presenter.rb @@ -1,8 +1,9 @@ class TwoFactorLoginOptionsPresenter < TwoFactorAuthCode::GenericDeliveryPresenter include ActionView::Helpers::TranslationHelper - attr_reader :user, :phishing_resistant_required, :piv_cac_required + attr_reader :user, :reauthentication_context, :phishing_resistant_required, :piv_cac_required + alias_method :reauthentication_context?, :reauthentication_context alias_method :phishing_resistant_required?, :phishing_resistant_required alias_method :piv_cac_required?, :piv_cac_required @@ -35,6 +36,8 @@ def info end def restricted_options_warning_text + return if reauthentication_context? + if piv_cac_required? t('two_factor_authentication.aal2_request.piv_cac_only_html', sp_name:) elsif phishing_resistant_required? @@ -46,9 +49,9 @@ def options return @options if defined?(@options) mfa = MfaContext.new(user) - if @piv_cac_required + if piv_cac_required? && !reauthentication_context? configurations = mfa.piv_cac_configurations - elsif @phishing_resistant_required + elsif phishing_resistant_required? && !reauthentication_context? configurations = mfa.phishing_resistant_configurations else configurations = mfa.two_factor_configurations diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 803c6351853..f4357a1a2b5 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -3506,6 +3506,16 @@ def rate_limit_triggered(type:, **extra) track_event('Rate Limit Triggered', type: type, **extra) end + # Account profile reactivation submitted + def reactivate_account_submit + track_event('Reactivate Account Submitted') + end + + # Account profile reactivation page visited + def reactivate_account_visit + track_event('Reactivate Account Visited') + end + # The result of a reCAPTCHA verification request was received # @param [Hash] recaptcha_result Full reCAPTCHA response body # @param [Float] score_threshold Minimum value for considering passing result diff --git a/app/services/document_capture_session_result.rb b/app/services/document_capture_session_result.rb index 6400dbcf592..1a5bdb794bc 100644 --- a/app/services/document_capture_session_result.rb +++ b/app/services/document_capture_session_result.rb @@ -8,9 +8,10 @@ :attention_with_barcode, :failed_front_image_fingerprints, :failed_back_image_fingerprints, + :captured_at, keyword_init: true, allowed_members: [:id, :success, :attention_with_barcode, :failed_front_image_fingerprints, - :failed_back_image_fingerprints], + :failed_back_image_fingerprints, :captured_at], ) do def self.redis_key_prefix 'dcs:result' diff --git a/app/services/encryption/contextless_kms_client.rb b/app/services/encryption/contextless_kms_client.rb index 1efb1dc644b..3a8bd8b5a4d 100644 --- a/app/services/encryption/contextless_kms_client.rb +++ b/app/services/encryption/contextless_kms_client.rb @@ -17,13 +17,13 @@ class ContextlessKmsClient }.freeze def encrypt(plaintext) - KmsLogger.log(:encrypt) + KmsLogger.log(:encrypt, key_id: IdentityConfig.store.aws_kms_key_id) return encrypt_kms(plaintext) if FeatureManagement.use_kms? encrypt_local(plaintext) end def decrypt(ciphertext) - KmsLogger.log(:decrypt) + KmsLogger.log(:decrypt, key_id: IdentityConfig.store.aws_kms_key_id) return decrypt_kms(ciphertext) if use_kms?(ciphertext) decrypt_local(ciphertext) end diff --git a/app/services/encryption/kms_client.rb b/app/services/encryption/kms_client.rb index 3e36aee1597..7d059229373 100644 --- a/app/services/encryption/kms_client.rb +++ b/app/services/encryption/kms_client.rb @@ -32,14 +32,14 @@ def initialize(kms_key_id: IdentityConfig.store.aws_kms_key_id) end def encrypt(plaintext, encryption_context) - KmsLogger.log(:encrypt, encryption_context) + KmsLogger.log(:encrypt, context: encryption_context, key_id: kms_key_id) return encrypt_kms(plaintext, encryption_context) if FeatureManagement.use_kms? encrypt_local(plaintext, encryption_context) end def decrypt(ciphertext, encryption_context) return decrypt_contextless_kms(ciphertext) if self.class.looks_like_contextless?(ciphertext) - KmsLogger.log(:decrypt, encryption_context) + KmsLogger.log(:decrypt, context: encryption_context, key_id: kms_key_id) return decrypt_kms(ciphertext, encryption_context) if use_kms?(ciphertext) decrypt_local(ciphertext, encryption_context) end diff --git a/app/services/encryption/kms_logger.rb b/app/services/encryption/kms_logger.rb index 1dfc538d345..75cd8766fee 100644 --- a/app/services/encryption/kms_logger.rb +++ b/app/services/encryption/kms_logger.rb @@ -1,11 +1,12 @@ module Encryption class KmsLogger LOG_FILENAME = 'kms.log' - def self.log(action, context = nil) + def self.log(action, key_id:, context: nil) output = { kms: { action: action, encryption_context: context, + key_id: key_id, }, log_filename: LOG_FILENAME, } diff --git a/app/jobs/reports/monthly_account_reuse_report.rb b/app/services/reporting/account_reuse_and_total_identities_report.rb similarity index 62% rename from app/jobs/reports/monthly_account_reuse_report.rb rename to app/services/reporting/account_reuse_and_total_identities_report.rb index ee02545ce3d..ca4966e6608 100644 --- a/app/jobs/reports/monthly_account_reuse_report.rb +++ b/app/services/reporting/account_reuse_and_total_identities_report.rb @@ -1,38 +1,76 @@ -require 'csv' - -module Reports - class MonthlyAccountReuseReport < BaseReport - REPORT_NAME = 'monthly-account-reuse-report' - +module Reporting + class AccountReuseAndTotalIdentitiesReport attr_reader :report_date def initialize(report_date = Time.zone.today) @report_date = report_date end - def perform(report_date = Time.zone.today) - @report_date = report_date - - _latest, path = generate_s3_paths(REPORT_NAME, 'json', now: report_date) + # Return array of arrays + def account_reuse_report + account_reuse_table = [] + account_reuse_table << ['Num. SPs', 'Num. users', 'Percentage'] - if bucket_name.present? - upload_file_to_s3_bucket( - path: path, - body: report_body, - content_type: 'text/csv', - bucket: bucket_name, - ) + total_reuse_report[:reuse_stats].each do |result_entry| + account_reuse_table << [ + result_entry['num_agencies'], + result_entry['num_users'], + result_entry['percentage'], + ] end + + account_reuse_table << [ + 'Total (all >1)', + total_reuse_report[:total_users], + total_reuse_report[:total_percentage], + ] + + account_reuse_table end - def first_day_of_report_month - report_date.beginning_of_month.strftime('%Y-%m-%d') + def total_identities_report + total_identities_table = [] + total_identities_table << ["Total proofed identities (#{stats_month})"] + total_identities_table << [total_reuse_report[:total_proofed]] + total_identities_table end - def params - { - query_date: first_day_of_report_month, - }.transform_values { |v| ActiveRecord::Base.connection.quote(v) } + def stats_month + report_date.prev_month(1).strftime('%b-%Y') + end + + private + + def total_reuse_report + return @total_reuse_report if defined?(@total_reuse_report) + reuse_stats = agency_reuse_results + + reuse_total_users = 0 + reuse_total_percentage = 0 + + total_proofed = num_active_profiles + + if !reuse_stats.empty? + reuse_stats.each do |result_entry| + reuse_total_users += result_entry['num_users'] + end + + if total_proofed > 0 + reuse_stats.each_with_index do |result_entry, index| + reuse_stats[index]['percentage'] = result_entry['num_users'] / total_proofed.to_f + + reuse_total_percentage += reuse_stats[index]['percentage'] + end + end + end + + # reuse_stats and total_stats + @total_reuse_report = { + reuse_stats: reuse_stats, + total_users: reuse_total_users, + total_percentage: reuse_total_percentage, + total_proofed: total_proofed, + } end def agency_reuse_results @@ -64,7 +102,7 @@ def agency_reuse_results num_agencies ASC SQL - agency_results = transaction_with_timeout do + agency_results = Reports::BaseReport.transaction_with_timeout do ActiveRecord::Base.connection.execute(agency_sql) end @@ -83,87 +121,21 @@ def num_active_profiles profiles.activated_at < %{query_date} SQL - proofed_results = transaction_with_timeout do + proofed_results = Reports::BaseReport.transaction_with_timeout do ActiveRecord::Base.connection.execute(proofed_sql) end proofed_results.first['num_proofed'] end - def stats_month - report_date.prev_month(1).strftime('%b-%Y') - end - - def total_reuse_report - reuse_stats = agency_reuse_results - - reuse_total_users = 0 - reuse_total_percentage = 0 - - total_proofed = num_active_profiles - - if !reuse_stats.empty? - reuse_stats.each do |result_entry| - reuse_total_users += result_entry['num_users'] - end - - if total_proofed > 0 - reuse_stats.each_with_index do |result_entry, index| - reuse_stats[index]['percentage'] = result_entry['num_users'] / total_proofed.to_f - - reuse_total_percentage += reuse_stats[index]['percentage'] - end - end - end - - # reuse_stats and total_stats - { reuse_stats: reuse_stats, - total_users: reuse_total_users, - total_percentage: reuse_total_percentage, - total_proofed: total_proofed } - end - - def report_csv - monthly_reuse_report = total_reuse_report - - tables_array = [] - reuse_rate_table = [] - reuse_rate_table << { - title: "IDV app reuse rate #{stats_month}", - float_as_percent: true, - precision: 4, - } - reuse_rate_table << ['Num. SPs', 'Num. users', 'Percentage'] - - monthly_reuse_report[:reuse_stats].each do |result_entry| - reuse_rate_table << [ - result_entry['num_agencies'], - result_entry['num_users'], - result_entry['percentage'], - ] - end - reuse_rate_table << [ - 'Total (all >1)', - monthly_reuse_report[:total_users], - monthly_reuse_report[:total_percentage], - ] - tables_array << reuse_rate_table - - total_proofed_identities_table = [] - total_proofed_identities_table << { title: 'Total proofed identities' } - total_proofed_identities_table << ["Total proofed identities (#{stats_month})"] - total_proofed_identities_table << [monthly_reuse_report[:total_proofed]] - tables_array << total_proofed_identities_table - - tables_array + def params + { + query_date: first_day_of_report_month, + }.transform_values { |v| ActiveRecord::Base.connection.quote(v) } end - def report_body - CSV.generate do |csv| - report_csv.each do |row| - csv << row - end - end + def first_day_of_report_month + report_date.beginning_of_month.strftime('%Y-%m-%d') end end end diff --git a/app/views/idv/agreement/show.html.erb b/app/views/idv/agreement/show.html.erb index ccf4418b788..e39fbc04049 100644 --- a/app/views/idv/agreement/show.html.erb +++ b/app/views/idv/agreement/show.html.erb @@ -25,7 +25,7 @@ <%= render ClickObserverComponent.new(event_name: 'IdV: consent checkbox toggled') do %> <%= render ValidatedFieldComponent.new( form: f, - name: :ial2_consent_given, + name: :idv_consent_given, as: :boolean, label: t('doc_auth.instructions.consent', app_name: APP_NAME), required: true, diff --git a/app/views/idv/getting_started/show.html.erb b/app/views/idv/getting_started/show.html.erb index 442cc502a75..30701e6c993 100644 --- a/app/views/idv/getting_started/show.html.erb +++ b/app/views/idv/getting_started/show.html.erb @@ -49,7 +49,7 @@ <%= render ClickObserverComponent.new(event_name: 'IdV: consent checkbox toggled') do %> <%= render ValidatedFieldComponent.new( form: f, - name: :ial2_consent_given, + name: :idv_consent_given, as: :boolean, label: t('doc_auth.getting_started.instructions.consent', app_name: APP_NAME), required: true, diff --git a/app/views/shared/_banner.html.erb b/app/views/shared/_banner.html.erb index 933c4451593..80f4665c1f2 100644 --- a/app/views/shared/_banner.html.erb +++ b/app/views/shared/_banner.html.erb @@ -5,10 +5,10 @@
<%= image_tag( - asset_url('us-flag.png'), + asset_url('us_flag.svg'), alt: t('image_description.us_flag'), - size: '18x12', - class: 'margin-right-1', + size: '16x11', + class: 'usa-banner__header-flag', ) %>

diff --git a/config/application.yml.default b/config/application.yml.default index f6d2983ead7..4c183ce2dd8 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -294,7 +294,6 @@ s3_public_reports_enabled: false s3_reports_enabled: false saml_secret_rotation_enabled: false second_mfa_reminder_account_age_in_days: 30 -second_mfa_reminder_enabled: true second_mfa_reminder_sign_in_count: 10 seed_agreements_data: true service_provider_request_ttl_hours: 24 @@ -355,6 +354,7 @@ voice_otp_pause_time: '0.5s' voice_otp_speech_rate: 'slow' voip_block: true voip_allowed_phones: '[]' +weekly_auth_funnel_report_config: '[]' development: aamva_private_key: 123abc @@ -476,7 +476,6 @@ production: s3_reports_enabled: true saml_endpoint_configs: '[]' scrypt_cost: 10000$8$1$ - second_mfa_reminder_enabled: false secret_key_base: seed_agreements_data: false session_encryption_key: diff --git a/config/initializers/job_configurations.rb b/config/initializers/job_configurations.rb index f1832446985..bdb591d68be 100644 --- a/config/initializers/job_configurations.rb +++ b/config/initializers/job_configurations.rb @@ -4,7 +4,6 @@ cron_24h = '0 0 * * *' gpo_cron_24h = '0 10 * * *' # 10am UTC is 5am EST/6am EDT cron_1w = '0 0 * * 0' -cron_1st_of_mo = '0 0 1 * *' if defined?(Rails::Console) Rails.logger.info 'job_configurations: console detected, skipping schedule' @@ -189,12 +188,6 @@ cron: cron_24h, args: -> { [14.days.ago] }, }, - # Monthly report describing account reuse - monthly_account_reuse_report: { - class: 'Reports::MonthlyAccountReuseReport', - cron: cron_1st_of_mo, - args: -> { [Time.zone.today] }, - }, # Monthly report checking in on key metrics monthly_key_metrics_report: { class: 'Reports::MonthlyKeyMetricsReport', @@ -219,6 +212,12 @@ statement_timeout: IdentityConfig.store.multi_region_kms_migration_jobs_user_timeout, }, }, + # Send weekly authentication reports to partners + weekly_authentication_report: { + class: 'Reports::AuthenticationReport', + cron: cron_1w, + args: -> { [Time.zone.now] }, + }, }.compact end # rubocop:enable Metrics/BlockLength diff --git a/lib/identity_config.rb b/lib/identity_config.rb index eab204cbf6f..5b47c4c8ac9 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -420,7 +420,6 @@ def self.build_store(config_map) config.add(:saml_secret_rotation_enabled, type: :boolean) config.add(:scrypt_cost, type: :string) config.add(:second_mfa_reminder_account_age_in_days, type: :integer) - config.add(:second_mfa_reminder_enabled, type: :boolean) config.add(:second_mfa_reminder_sign_in_count, type: :integer) config.add(:secret_key_base, type: :string) config.add(:seed_agreements_data, type: :boolean) @@ -484,6 +483,7 @@ def self.build_store(config_map) config.add(:voice_otp_speech_rate) config.add(:voip_allowed_phones, type: :json) config.add(:voip_block, type: :boolean) + config.add(:weekly_auth_funnel_report_config, type: :json) @key_types = config.key_types @store = RedactedStruct.new('IdentityConfig', *config.written_env.keys, keyword_init: true). diff --git a/lib/reporting/authentication_report.rb b/lib/reporting/authentication_report.rb index f13bfc14f0c..8b50947e271 100644 --- a/lib/reporting/authentication_report.rb +++ b/lib/reporting/authentication_report.rb @@ -54,58 +54,29 @@ def progress? @progress end - # rubocop:disable Metrics/BlockLength - def to_csv - CSV.generate do |csv| - csv << ['Report Timeframe', "#{time_range.begin} to #{time_range.end}"] - csv << ['Report Generated', Date.today.to_s] # rubocop:disable Rails/Date - csv << ['Issuer', issuers.join(', ')] - csv << [] - csv << ['Metric', 'Number of accounts', '% of total from start'] - csv << [ - 'New Users Started IAL1 Verification', - email_confirmation, - format_as_percent(numerator: email_confirmation, denominator: email_confirmation), - ] - - csv << [ - 'New Users Completed IAL1 Password Setup', - two_fa_setup_visited, - format_as_percent(numerator: two_fa_setup_visited, denominator: email_confirmation), - ] - - csv << [ - 'New Users Completed IAL1 MFA', - user_fully_registered, - format_as_percent(numerator: user_fully_registered, denominator: email_confirmation), - ] - csv << [ - 'New IAL1 Users Consented to Partner', - sp_redirect_initiated_new_users, - format_as_percent( - numerator: sp_redirect_initiated_new_users, - denominator: email_confirmation, - ), - ] - csv << [] - csv << ['Total # of IAL1 Users', sp_redirect_initiated_all] - csv << [] - csv << [ - 'AAL2 Authentication Requests from Partner', - oidc_auth_request, - format_as_percent(numerator: oidc_auth_request, denominator: oidc_auth_request), - ] - csv << [ - 'AAL2 Authenticated Requests', - sp_redirect_initiated_after_oidc, - format_as_percent( - numerator: sp_redirect_initiated_after_oidc, - denominator: oidc_auth_request, - ), - ] + def as_tables + [ + overview_table, + funnel_metrics_table, + ] + end + + def as_tables_with_options + [ + [{ title: 'Overview' }, *overview_table], + [{ title: 'Authentication Funnel Metrics' }, *funnel_metrics_table], + ] + end + + def to_csvs + as_tables.map do |table| + CSV.generate do |csv| + table.each do |row| + csv << row + end + end end end - # rubocop:enable Metrics/BlockLength # event name => set(user ids) # @return Hash> @@ -188,6 +159,57 @@ def cloudwatch_client ) end + def overview_table + [ + ['Report Timeframe', "#{time_range.begin} to #{time_range.end}"], + ['Report Generated', Date.today.to_s], # rubocop:disable Rails/Date + ['Issuer', issuers.join(', ')], + ['Total # of IAL1 Users', sp_redirect_initiated_all], + ] + end + + def funnel_metrics_table + [ + ['Metric', 'Number of accounts', '% of total from start'], + [ + 'New Users Started IAL1 Verification', + email_confirmation, + format_as_percent(numerator: email_confirmation, denominator: email_confirmation), + ], + [ + 'New Users Completed IAL1 Password Setup', + two_fa_setup_visited, + format_as_percent(numerator: two_fa_setup_visited, denominator: email_confirmation), + ], + [ + 'New Users Completed IAL1 MFA', + user_fully_registered, + format_as_percent(numerator: user_fully_registered, denominator: email_confirmation), + ], + [ + 'New IAL1 Users Consented to Partner', + sp_redirect_initiated_new_users, + format_as_percent( + numerator: sp_redirect_initiated_new_users, + denominator: email_confirmation, + ), + ], + [ + 'AAL2 Authentication Requests from Partner', + oidc_auth_request, + format_as_percent(numerator: oidc_auth_request, denominator: oidc_auth_request), + ], + [ + 'AAL2 Authenticated Requests', + sp_redirect_initiated_after_oidc, + format_as_percent( + numerator: sp_redirect_initiated_after_oidc, + denominator: oidc_auth_request, + ), + ], + ] + end + # @return [String] def format_as_percent(numerator:, denominator:) (100 * numerator.to_f / denominator.to_f).round(2).to_s + '%' @@ -199,6 +221,8 @@ def format_as_percent(numerator:, denominator:) if __FILE__ == $PROGRAM_NAME options = Reporting::CommandLineOptions.new.parse!(ARGV) - puts Reporting::AuthenticationReport.new(**options).to_csv + Reporting::AuthenticationReport.new(**options).to_csvs.each do |csv| + puts csv + end end # rubocop:enable Rails/Output diff --git a/package.json b/package.json index d3b7bf82e57..41ddb6f1302 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "build:css": "build-sass app/assets/stylesheets/*.css.scss app/components/*.scss --load-path=app/assets/stylesheets --out-dir=app/assets/builds" }, "dependencies": { - "@18f/identity-design-system": "^7.1.0", + "@18f/identity-design-system": "^8.0.0", "@babel/core": "^7.20.7", "@babel/preset-env": "^7.15.6", "@babel/preset-react": "^7.14.5", @@ -62,7 +62,7 @@ "@types/sinon": "^10.0.13", "@types/sinon-chai": "^3.2.8", "@typescript-eslint/eslint-plugin": "^5.38.1", - "@typescript-eslint/parser": "^5.38.1", + "@typescript-eslint/parser": "^6.7.4", "chai": "^4.3.6", "chai-as-promised": "^7.1.1", "clipboard-polyfill": "^3.0.3", @@ -86,9 +86,9 @@ "sinon": "^14.0.0", "sinon-chai": "^3.7.0", "stylelint": "^15.10.1", - "svgo": "^2.8.0", + "svgo": "^3.0.2", "swr": "^2.0.0", - "typescript": "^5.0.4", + "typescript": "^5.2.2", "webpack-dev-server": "^4.11.1" }, "resolutions": { diff --git a/public/images/logo-white.svg b/public/images/logo-white.svg index 521c6547e93..442f2f4d272 100644 --- a/public/images/logo-white.svg +++ b/public/images/logo-white.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/public/images/logo.svg b/public/images/logo.svg index 715ac538706..47db55a6fe2 100644 --- a/public/images/logo.svg +++ b/public/images/logo.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/public/safari-pinned-tab.svg b/public/safari-pinned-tab.svg index f19be19ff58..96ee84418c1 100644 --- a/public/safari-pinned-tab.svg +++ b/public/safari-pinned-tab.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/spec/controllers/concerns/second_mfa_reminder_concern_spec.rb b/spec/controllers/concerns/second_mfa_reminder_concern_spec.rb index b5bf49f719b..ea20f14b5f1 100644 --- a/spec/controllers/concerns/second_mfa_reminder_concern_spec.rb +++ b/spec/controllers/concerns/second_mfa_reminder_concern_spec.rb @@ -28,16 +28,6 @@ def initialize(current_user:, service_provider_mfa_policy:) describe '#user_needs_second_mfa_reminder?' do subject(:user_needs_second_mfa_reminder) { instance.user_needs_second_mfa_reminder? } - shared_examples 'second mfa reminder feature is disabled' do - before do - allow(IdentityConfig.store).to receive(:second_mfa_reminder_account_age_in_days). - and_return(10) - allow(IdentityConfig.store).to receive(:second_mfa_reminder_enabled).and_return(false) - end - - it { expect(user_needs_second_mfa_reminder).to eq(false) } - end - shared_examples 'second mfa reminder with phishing-resistant required request' do let(:phishing_resistant_required) { true } @@ -80,7 +70,6 @@ def initialize(current_user:, service_provider_mfa_policy:) it { expect(user_needs_second_mfa_reminder).to eq(true) } - it_behaves_like 'second mfa reminder feature is disabled' it_behaves_like 'second mfa reminder with phishing-resistant required request' it_behaves_like 'second mfa reminder with piv required request' end @@ -95,7 +84,6 @@ def initialize(current_user:, service_provider_mfa_policy:) it { expect(user_needs_second_mfa_reminder).to eq(true) } - it_behaves_like 'second mfa reminder feature is disabled' it_behaves_like 'second mfa reminder with phishing-resistant required request' it_behaves_like 'second mfa reminder with piv required request' end diff --git a/spec/controllers/idv/document_capture_controller_spec.rb b/spec/controllers/idv/document_capture_controller_spec.rb index 8de517414af..6792a7a059b 100644 --- a/spec/controllers/idv/document_capture_controller_spec.rb +++ b/spec/controllers/idv/document_capture_controller_spec.rb @@ -3,7 +3,16 @@ RSpec.describe Idv::DocumentCaptureController do include IdvHelper - let(:document_capture_session_uuid) { 'fd14e181-6fb1-4cdc-92e0-ef66dad0df4e' } + let(:document_capture_session_requested_at) { Time.zone.now } + + let!(:document_capture_session) do + DocumentCaptureSession.create!( + user: user, + requested_at: document_capture_session_requested_at, + ) + end + + let(:document_capture_session_uuid) { document_capture_session&.uuid } let(:user) { create(:user) } @@ -184,5 +193,16 @@ expect(user.reload.establishing_in_person_enrollment).to be_nil end end + + context 'ocr confirmation pending' do + before do + subject.document_capture_session.ocr_confirmation_pending = true + end + + it 'confirms ocr' do + put :update + expect(subject.document_capture_session.ocr_confirmation_pending).to be_falsey + end + end end end diff --git a/spec/controllers/idv/hybrid_mobile/document_capture_controller_spec.rb b/spec/controllers/idv/hybrid_mobile/document_capture_controller_spec.rb index fcacb00a32a..5f1ebbfbe4d 100644 --- a/spec/controllers/idv/hybrid_mobile/document_capture_controller_spec.rb +++ b/spec/controllers/idv/hybrid_mobile/document_capture_controller_spec.rb @@ -15,6 +15,8 @@ let(:document_capture_session_uuid) { document_capture_session&.uuid } let(:document_capture_session_requested_at) { Time.zone.now } + let(:document_capture_session_result_captured_at) { Time.zone.now + 1.second } + let(:document_capture_session_result_success) { true } let(:ab_test_args) do { sample_bucket1: :sample_value1, sample_bucket2: :sample_value2 } @@ -102,20 +104,42 @@ end end end + + context 'stored_result already exists' do + before do + stub_document_capture_session_result + end + + it 'redirects to document capture complete' do + get :show + expect(response).to redirect_to idv_hybrid_mobile_capture_complete_url + end + + context 'document capture re-requested' do + let(:document_capture_session_result_captured_at) do + document_capture_session_requested_at - 5.minutes + end + context 'with successful stored_result' do + it 'renders the show template' do + get :show + expect(response).to render_template :show + end + end + + context 'with failed stored_result' do + let(:document_capture_session_result_success) { false } + it 'renders the show template' do + get :show + expect(response).to render_template :show + end + end + end + end end describe '#update' do before do - allow_any_instance_of(DocumentCaptureSession).to receive(:load_result).and_return( - DocumentCaptureSessionResult.new( - id: 1234, - success: true, - pii: { - state: 'WA', - }, - attention_with_barcode: true, - ), - ) + stub_document_capture_session_result end context 'with no user id in session' do @@ -161,6 +185,17 @@ put :update expect(response).to redirect_to idv_hybrid_mobile_capture_complete_url end + + context 'ocr confirmation pending' do + before do + subject.document_capture_session.ocr_confirmation_pending = true + end + + it 'confirms ocr' do + put :update + expect(subject.document_capture_session.ocr_confirmation_pending).to be_falsey + end + end end end @@ -172,4 +207,18 @@ controller.extra_view_variables end end + + def stub_document_capture_session_result + allow_any_instance_of(DocumentCaptureSession).to receive(:load_result).and_return( + DocumentCaptureSessionResult.new( + id: 1234, + success: document_capture_session_result_success, + pii: { + state: 'WA', + }, + attention_with_barcode: true, + captured_at: document_capture_session_result_captured_at, + ), + ) + end end diff --git a/spec/controllers/idv/link_sent_controller_spec.rb b/spec/controllers/idv/link_sent_controller_spec.rb index 4745b5de372..5b1bcdceedc 100644 --- a/spec/controllers/idv/link_sent_controller_spec.rb +++ b/spec/controllers/idv/link_sent_controller_spec.rb @@ -140,14 +140,26 @@ allow(subject).to receive(:document_capture_session).and_return(document_capture_session) end - it 'redirects to ssn page when successful' do - put :update + context 'document capture session successful' do + it 'redirects to ssn page' do + put :update - expect(response).to redirect_to(idv_ssn_url) + expect(response).to redirect_to(idv_ssn_url) - pc = ProofingComponent.find_by(user_id: user.id) - expect(pc.document_check).to eq('mock') - expect(pc.document_type).to eq('state_id') + pc = ProofingComponent.find_by(user_id: user.id) + expect(pc.document_check).to eq('mock') + expect(pc.document_type).to eq('state_id') + end + + context 'redo document capture' do + before do + subject.idv_session.redo_document_capture = true + end + it 'resets redo_document capture to nil in idv_session' do + put :update + expect(subject.idv_session.redo_document_capture).to be_nil + end + end end context 'document capture session canceled' do diff --git a/spec/controllers/idv/verify_info_controller_spec.rb b/spec/controllers/idv/verify_info_controller_spec.rb index 632fc46fcd8..81cb0bd138c 100644 --- a/spec/controllers/idv/verify_info_controller_spec.rb +++ b/spec/controllers/idv/verify_info_controller_spec.rb @@ -341,8 +341,8 @@ let(:success) { true } let(:vendor_name) { 'UnsupportedJurisdiction' } - it 'considers the request billable' do - expect { put :show }.to change { SpCost.where(cost_type: 'aamva').count }.by(1) + it 'does not consider the request billable' do + expect { put :show }.to_not change { SpCost.where(cost_type: 'aamva').count } end end diff --git a/spec/controllers/reactivate_account_controller_spec.rb b/spec/controllers/reactivate_account_controller_spec.rb index a37f61eb43f..70d53c453eb 100644 --- a/spec/controllers/reactivate_account_controller_spec.rb +++ b/spec/controllers/reactivate_account_controller_spec.rb @@ -17,13 +17,16 @@ let(:profiles) { [create(:profile, :verified, :password_reset)] } it 'renders the index template' do + stub_analytics + expect(@analytics).to receive(:track_event).with('Reactivate Account Visited') + get :index expect(subject).to render_template(:index) end end - context 'wthout a password reset profile' do + context 'without a password reset profile' do let(:profiles) { [create(:profile, :active)] } it 'redirects to the root url' do get :index @@ -37,6 +40,8 @@ let(:profiles) { [create(:profile, :verified, :password_reset)] } it 'redirects user to idv_url' do + stub_analytics + expect(@analytics).to receive(:track_event).with('Reactivate Account Submitted') put :update expect(subject.user_session[:acknowledge_personal_key]).to be_nil diff --git a/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb b/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb index e48caf2b801..c336ef8f9ef 100644 --- a/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb +++ b/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb @@ -139,7 +139,7 @@ ) end - it 'it shows capture complete on mobile and error page on desktop', js: true do + it 'shows capture complete on mobile and error page on desktop', js: true do user = nil perform_in_browser(:desktop) do @@ -174,4 +174,134 @@ end end end + + context 'barcode read error on mobile (redo document capture)' do + it 'continues to ssn on desktop when user selects Continue', js: true do + user = nil + + perform_in_browser(:desktop) do + user = sign_in_and_2fa_user + complete_doc_auth_steps_before_hybrid_handoff_step + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link + + expect(page).to have_content(t('doc_auth.headings.text_message')) + end + + expect(@sms_link).to be_present + + perform_in_browser(:mobile) do + visit @sms_link + + mock_doc_auth_attention_with_barcode + attach_and_submit_images + click_idv_continue + + expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) + expect(page).to have_content(t('doc_auth.headings.capture_complete').tr(' ', ' ')) + expect(page).to have_text(t('doc_auth.instructions.switch_back')) + end + + perform_in_browser(:desktop) do + expect(page).to have_current_path(idv_ssn_path, wait: 10) + + fill_out_ssn_form_ok + click_idv_continue + + expect(page).to have_current_path(idv_verify_info_path, wait: 10) + + # verify pii is displayed + expect(page).to have_text('DAVID') + expect(page).to have_text('SAMPLE') + expect(page).to have_text('123 ABC AVE') + + warning_link_text = t('doc_auth.headings.capture_scan_warning_link') + click_link warning_link_text + + expect(current_path).to eq(idv_hybrid_handoff_path) + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link + end + + perform_in_browser(:mobile) do + visit @sms_link + + DocAuth::Mock::DocAuthMockClient.reset! + attach_and_submit_images + + expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) + end + + perform_in_browser(:desktop) do + expect(page).to have_current_path(idv_verify_info_path, wait: 10) + + # verify orig pii no longer displayed + expect(page).not_to have_text('DAVID') + expect(page).not_to have_text('SAMPLE') + expect(page).not_to have_text('123 ABC AVE') + # verify new pii from redo is displayed + expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:first_name]) + expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:last_name]) + expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:address1]) + + click_idv_continue + end + end + end + + context 'barcode read error on desktop, redo document capture on mobile' do + it 'continues to ssn on desktop when user selects Continue', js: true do + user = nil + + perform_in_browser(:desktop) do + user = sign_in_and_2fa_user + complete_doc_auth_steps_before_document_capture_step + mock_doc_auth_attention_with_barcode + attach_and_submit_images + click_idv_continue + expect(page).to have_current_path(idv_ssn_path, wait: 10) + + fill_out_ssn_form_ok + click_idv_continue + + expect(page).to have_current_path(idv_verify_info_path, wait: 10) + + # verify pii is displayed + expect(page).to have_text('DAVID') + expect(page).to have_text('SAMPLE') + expect(page).to have_text('123 ABC AVE') + + warning_link_text = t('doc_auth.headings.capture_scan_warning_link') + click_link warning_link_text + + expect(current_path).to eq(idv_hybrid_handoff_path) + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link + end + + perform_in_browser(:mobile) do + visit @sms_link + + DocAuth::Mock::DocAuthMockClient.reset! + attach_and_submit_images + + expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) + end + + perform_in_browser(:desktop) do + expect(page).to have_current_path(idv_verify_info_path, wait: 10) + + # verify orig pii no longer displayed + expect(page).not_to have_text('DAVID') + expect(page).not_to have_text('SAMPLE') + expect(page).not_to have_text('123 ABC AVE') + # verify new pii from redo is displayed + expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:first_name]) + expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:last_name]) + expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:address1]) + + click_idv_continue + end + end + end end diff --git a/spec/features/openid_connect/phishing_resistant_required_spec.rb b/spec/features/openid_connect/phishing_resistant_required_spec.rb index 9c1914b3236..bb9bdc75fc6 100644 --- a/spec/features/openid_connect/phishing_resistant_required_spec.rb +++ b/spec/features/openid_connect/phishing_resistant_required_spec.rb @@ -2,20 +2,60 @@ RSpec.describe 'Phishing-resistant authentication required in an OIDC context' do include OidcAuthHelper + include WebAuthnHelper - describe 'OpenID Connect requesting AAL3 authentication' do - context 'user does not have phishing-resistant auth configured' do - it 'sends user to set up phishing-resistant auth' do - user = user_with_2fa + shared_examples 'setting up phishing-resistant authenticator in an OIDC context' do + it 'sends user to set up phishing-resistant auth' do + sign_in_live_with_2fa(user) - visit_idp_from_ial1_oidc_sp_requesting_aal3(prompt: 'select_account') - sign_in_live_with_2fa(user) + expect(page).to have_current_path(authentication_methods_setup_path) + expect(page).to have_content(t('two_factor_authentication.two_factor_aal3_choice')) + expect(page).to have_xpath("//img[@alt='important alert icon']") + + # Validate that user is not allowed to continue without making a selection. + click_continue + expect(page).to have_current_path(authentication_methods_setup_path) + expect(page).to have_content(t('errors.two_factor_auth_setup.must_select_option')) + + # Regression (LG-11110): Ensure the user can reauthenticate with any existing configuration, + # not limited based on phishing-resistant requirement. + travel (IdentityConfig.store.reauthn_window + 1).seconds do + check t('two_factor_authentication.two_factor_choice_options.webauthn') + click_continue + + expect(page).to have_content(t('two_factor_authentication.login_options.sms')) + expect(page).to have_content(t('two_factor_authentication.login_options.voice')) + + choose t('two_factor_authentication.login_options.sms') + click_continue - expect(current_url).to eq(authentication_methods_setup_url) + fill_in_code_with_last_phone_otp + click_submit_default + + # LG-11193: Currently the user is redirected back to the MFA setup selection after + # reauthenticating. This should be improved to remember their original selection. + expect(page).to have_current_path(authentication_methods_setup_path) expect(page).to have_content(t('two_factor_authentication.two_factor_aal3_choice')) - expect(page).to have_xpath("//img[@alt='important alert icon']") + mock_webauthn_setup_challenge + check t('two_factor_authentication.two_factor_choice_options.webauthn') + click_continue + + fill_in_nickname_and_click_continue + mock_press_button_on_hardware_key_on_setup + + expect(page).to have_current_path(sign_up_completed_path) end end + end + + describe 'OpenID Connect requesting AAL3 authentication' do + context 'user does not have phishing-resistant auth configured' do + let(:user) { create(:user, :fully_registered, :with_phone) } + + before { visit_idp_from_ial1_oidc_sp_requesting_aal3(prompt: 'select_account') } + + it_behaves_like 'setting up phishing-resistant authenticator in an OIDC context' + end context 'user has phishing-resistant auth configured' do context 'with piv cac configured' do @@ -65,16 +105,11 @@ describe 'OpenID Connect requesting phishing-resistant authentication' do context 'user does not have phishing-resistant auth configured' do - it 'sends user to set up phishing-resistant auth' do - user = user_with_2fa + let(:user) { create(:user, :fully_registered, :with_phone) } - visit_idp_from_ial1_oidc_sp_requesting_phishing_resistant(prompt: 'select_account') - sign_in_live_with_2fa(user) + before { visit_idp_from_ial1_oidc_sp_requesting_phishing_resistant(prompt: 'select_account') } - expect(current_url).to eq(authentication_methods_setup_url) - expect(page).to have_content(t('two_factor_authentication.two_factor_aal3_choice')) - expect(page).to have_xpath("//img[@alt='important alert icon']") - end + it_behaves_like 'setting up phishing-resistant authenticator in an OIDC context' end context 'user has phishing-resistant auth configured' do @@ -125,31 +160,11 @@ describe 'ServiceProvider configured to default to AAL3 authentication' do context 'user does not have phishing-resistant auth configured' do - it 'sends user to set up phishing-resistant auth' do - user = user_with_2fa + let(:user) { create(:user, :fully_registered, :with_phone) } - visit_idp_from_ial1_oidc_sp_defaulting_to_aal3(prompt: 'select_account') - sign_in_live_with_2fa(user) - - expect(current_url).to eq(authentication_methods_setup_url) - expect(page).to have_content(t('two_factor_authentication.two_factor_aal3_choice')) - expect(page).to have_xpath("//img[@alt='important alert icon']") - end + before { visit_idp_from_ial1_oidc_sp_defaulting_to_aal3(prompt: 'select_account') } - it 'throws an error if user doesnt select phishing-resistant auth' do - user = user_with_2fa - - visit_idp_from_ial1_oidc_sp_defaulting_to_aal3(prompt: 'select_account') - sign_in_live_with_2fa(user) - - expect(current_url).to eq(authentication_methods_setup_url) - expect(page).to have_content(t('two_factor_authentication.two_factor_aal3_choice')) - expect(page).to have_xpath("//img[@alt='important alert icon']") - - click_continue - - expect(page).to have_content(t('errors.two_factor_auth_setup.must_select_option')) - end + it_behaves_like 'setting up phishing-resistant authenticator in an OIDC context' end context 'user has phishing-resistant auth configured' do diff --git a/spec/features/saml/phishing_resistant_required_spec.rb b/spec/features/saml/phishing_resistant_required_spec.rb index efd2c403447..414a5f84d31 100644 --- a/spec/features/saml/phishing_resistant_required_spec.rb +++ b/spec/features/saml/phishing_resistant_required_spec.rb @@ -2,22 +2,65 @@ RSpec.describe 'Phishing-resistant authentication required in an SAML context' do include SamlAuthHelper + include WebAuthnHelper + + shared_examples 'setting up phishing-resistant authenticator in an SAML context' do + it 'sends user to set up phishing-resistant auth' do + expect(page).to have_current_path(authentication_methods_setup_path) + expect(page).to have_content(t('two_factor_authentication.two_factor_aal3_choice')) + expect(page).to have_xpath("//img[@alt='important alert icon']") + + # Validate that user is not allowed to continue without making a selection. + click_continue + expect(page).to have_current_path(authentication_methods_setup_path) + expect(page).to have_content(t('errors.two_factor_auth_setup.must_select_option')) + + # Regression (LG-11110): Ensure the user can reauthenticate with any existing configuration, + # not limited based on phishing-resistant requirement. + travel (IdentityConfig.store.reauthn_window + 1).seconds do + check t('two_factor_authentication.two_factor_choice_options.webauthn') + click_continue + + expect(page).to have_content(t('two_factor_authentication.login_options.sms')) + expect(page).to have_content(t('two_factor_authentication.login_options.voice')) + + choose t('two_factor_authentication.login_options.sms') + click_continue + + fill_in_code_with_last_phone_otp + click_submit_default + + # LG-11193: Currently the user is redirected back to the MFA setup selection after + # reauthenticating. This should be improved to remember their original selection. + expect(page).to have_current_path(authentication_methods_setup_path) + expect(page).to have_content(t('two_factor_authentication.two_factor_aal3_choice')) + mock_webauthn_setup_challenge + check t('two_factor_authentication.two_factor_choice_options.webauthn') + click_continue + + fill_in_nickname_and_click_continue + mock_press_button_on_hardware_key_on_setup + + expect(page).to have_current_path(sign_up_completed_path) + end + end + end describe 'SAML ServiceProvider requesting phishing-resistant authentication' do context 'user does not have phishing-resistant auth configured' do - it 'sends user to set up phishing-resistant auth' do - sign_in_and_2fa_user(user_with_2fa) + let(:user) { create(:user, :proofed, :with_phone) } + + before do + sign_in_and_2fa_user(user) visit_saml_authn_request_url( overrides: { issuer: sp1_issuer, authn_context: Saml::Idp::Constants::AAL2_PHISHING_RESISTANT_AUTHN_CONTEXT_CLASSREF, }, ) - - expect(current_url).to eq(authentication_methods_setup_url) - expect(page).to have_content(t('two_factor_authentication.two_factor_aal3_choice')) - expect(page).to have_xpath("//img[@alt='important alert icon']") end + + it_behaves_like 'setting up phishing-resistant authenticator in an SAML context' end context 'user has phishing-resistant auth configured' do @@ -76,18 +119,18 @@ describe 'SAML ServiceProvider requesting AAL3 authentication' do context 'user does not have phishing-resistant auth configured' do - it 'sends user to set up phishing-resistant auth' do - sign_in_and_2fa_user(user_with_2fa) + let(:user) { create(:user, :proofed, :with_phone) } + + before do + sign_in_and_2fa_user(user) visit_saml_authn_request_url( overrides: { issuer: sp1_issuer, authn_context: Saml::Idp::Constants::AAL3_AUTHN_CONTEXT_CLASSREF }, ) - - expect(current_url).to eq(authentication_methods_setup_url) - expect(page).to have_content(t('two_factor_authentication.two_factor_aal3_choice')) - expect(page).to have_xpath("//img[@alt='important alert icon']") end + + it_behaves_like 'setting up phishing-resistant authenticator in an SAML context' end context 'user has phishing-resistant auth configured' do @@ -143,18 +186,18 @@ describe 'SAML ServiceProvider configured to default to AAL3 authentication' do context 'user does not have phishing-resistant auth configured' do - it 'sends user to set up phishing-resistant auth' do - sign_in_and_2fa_user(user_with_2fa) + let(:user) { create(:user, :proofed, :with_phone) } + + before do + sign_in_and_2fa_user(user) visit_saml_authn_request_url( overrides: { issuer: aal3_issuer, authn_context: nil }, ) - - expect(current_url).to eq(authentication_methods_setup_url) - expect(page).to have_content(t('two_factor_authentication.two_factor_aal3_choice')) - expect(page).to have_xpath("//img[@alt='important alert icon']") end + + it_behaves_like 'setting up phishing-resistant authenticator in an SAML context' end context 'user has phishing-resistant auth configured' do diff --git a/spec/jobs/reports/authentication_report_spec.rb b/spec/jobs/reports/authentication_report_spec.rb new file mode 100644 index 00000000000..5bc2d870aff --- /dev/null +++ b/spec/jobs/reports/authentication_report_spec.rb @@ -0,0 +1,70 @@ +require 'rails_helper' + +RSpec.describe Reports::AuthenticationReport do + let(:issuer) { 'issuer1' } + let(:issuers) { [issuer] } + let(:report_date) { Date.new(2023, 12, 25) } + let(:email) { 'partner.name@example.com' } + let(:name) { 'Partner Name' } + + let(:report_configs) do + [ + { + 'name' => name, + 'issuers' => issuers, + 'emails' => [email], + }, + ] + end + + before do + allow(IdentityConfig.store).to receive(:s3_reports_enabled).and_return(true) + allow(IdentityConfig.store).to receive(:weekly_auth_funnel_report_config) { report_configs } + end + + describe '#perform' do + let(:tables) do + [ + [ + { title: 'Overview' }, + ['Report Timeframe', '2023-10-01 00:00:00 UTC to 2023-10-01 23:59:59 UTC'], + ['Report Generated', '2023-10-02'], + ['Issuer', 'some:issuer'], + ['Total # of IAL1 Users', '75'], + ], + [ + { title: 'Authentication Metrics Report' }, + ['Metric', 'Number of accounts', '% of total from start'], + ['New Users Started IAL1 Verification', '100', '100%'], + ['New Users Completed IAL1 Password Setup', '85', '85%'], + ['New Users Completed IAL1 MFA', '80', '80%'], + ['New IAL1 Users Consented to Partner', '75', '75%'], + ['AAL2 Authentication Requests from Partner', '12', '12%'], + ['AAL2 Authenticated Requests', '50', '50%'], + ], + ] + end + + let(:weekly_report) { double(Reporting::AuthenticationReport, as_tables_with_options: tables) } + + before do + expect(Reporting::AuthenticationReport).to receive(:new).with( + issuers:, + time_range: report_date.all_week, + ) { weekly_report } + + allow(ReportMailer).to receive(:tables_report).and_call_original + end + + it 'emails the csv' do + expect(ReportMailer).to receive(:tables_report).with( + email:, + subject: "Weekly Authentication Report - #{report_date}", + message: "Report: authentication-report #{report_date}", + tables:, + ) + + subject.perform(report_date) + end + end +end diff --git a/spec/jobs/reports/monthly_key_metrics_report_spec.rb b/spec/jobs/reports/monthly_key_metrics_report_spec.rb index f6cbefc7309..bc0298d6ccd 100644 --- a/spec/jobs/reports/monthly_key_metrics_report_spec.rb +++ b/spec/jobs/reports/monthly_key_metrics_report_spec.rb @@ -7,12 +7,31 @@ let(:name) { 'monthly-key-metrics-report' } let(:agnes_email) { 'fake@agnes_email.com' } let(:feds_email) { 'fake@feds_email.com' } + let(:s3_report_bucket_prefix) { 'reports-bucket' } + let(:account_reuse_s3_path) do + 'int/monthly-key-metrics-report/2021/2021-03-02.monthly-key-metrics-report/account_reuse.csv' + end + let(:total_profiles_s3_path) do + 'int/monthly-key-metrics-report/2021/2021-03-02.monthly-key-metrics-report/total_profiles.csv' + end before do allow(IdentityConfig.store).to receive(:team_agnes_email). and_return(agnes_email) allow(IdentityConfig.store).to receive(:team_all_feds_email). and_return(feds_email) + + allow(Identity::Hostdata).to receive(:env).and_return('int') + allow(Identity::Hostdata).to receive(:aws_account_id).and_return('1234') + allow(Identity::Hostdata).to receive(:aws_region).and_return('us-west-1') + allow(IdentityConfig.store).to receive(:s3_report_bucket_prefix). + and_return(s3_report_bucket_prefix) + + Aws.config[:s3] = { + stub_responses: { + put_object: {}, + }, + } end it 'sends out a report to the email listed with one total user' do @@ -51,4 +70,22 @@ subject.perform(report_date) end + + it 'uploads a file to S3 based on the report date' do + expect(subject).to receive(:upload_file_to_s3_bucket).with( + path: account_reuse_s3_path, + body: anything, + content_type: 'text/csv', + bucket: 'reports-bucket.1234-us-west-1', + ).exactly(1).time.and_call_original + + expect(subject).to receive(:upload_file_to_s3_bucket).with( + path: total_profiles_s3_path, + body: anything, + content_type: 'text/csv', + bucket: 'reports-bucket.1234-us-west-1', + ).exactly(1).time.and_call_original + + subject.perform(report_date) + end end diff --git a/spec/lib/reporting/authentication_report_spec.rb b/spec/lib/reporting/authentication_report_spec.rb index ca0bdeb2988..ba53bd5216e 100644 --- a/spec/lib/reporting/authentication_report_spec.rb +++ b/spec/lib/reporting/authentication_report_spec.rb @@ -42,29 +42,32 @@ allow(report).to receive(:cloudwatch_client).and_return(cloudwatch_client) end - describe '#to_csv' do + describe '#as_tables' do + it 'generates the tabular csv data' do + expect(report.as_tables).to eq expected_tables + end + end + + describe '#as_tables_with_names' do + it 'adds a "first row" hash with a title for tables_report mailer' do + tables = report.as_tables_with_options + aggregate_failures do + tables.each do |table| + expect(table[0][:title]).to_not be nil + end + end + end + end + + describe '#to_csvs' do it 'generates a csv' do - csv = CSV.parse(report.to_csv, headers: false) + csv_string_list = report.to_csvs + expect(csv_string_list.count).to be 2 - expected_csv = [ - ['Report Timeframe', "#{time_range.begin} to #{time_range.end}"], - ['Report Generated', Date.today.to_s], # rubocop:disable Rails/Date - ['Issuer', issuer], - [], - ['Metric', 'Number of accounts', '% of total from start'], - ['New Users Started IAL1 Verification', '4', '100.0%'], - ['New Users Completed IAL1 Password Setup', '3', '75.0%'], - ['New Users Completed IAL1 MFA', '2', '50.0%'], - ['New IAL1 Users Consented to Partner', '1', '25.0%'], - [], - ['Total # of IAL1 Users', '2'], - [], - ['AAL2 Authentication Requests from Partner', '5', '100.0%'], - ['AAL2 Authenticated Requests', '2', '40.0%'], - ] + csvs = csv_string_list.map { |csv| CSV.parse(csv) } aggregate_failures do - csv.map(&:to_a).zip(expected_csv).each do |actual, expected| + csvs.map(&:to_a).zip(expected_tables(strings: true)).each do |actual, expected| expect(actual).to eq(expected) end end @@ -141,4 +144,24 @@ end end end + + def expected_tables(strings: false) + [ + [ + ['Report Timeframe', "#{time_range.begin} to #{time_range.end}"], + ['Report Generated', Date.today.to_s], # rubocop:disable Rails/Date + ['Issuer', issuer], + ['Total # of IAL1 Users', strings ? '2' : 2], + ], + [ + ['Metric', 'Number of accounts', '% of total from start'], + ['New Users Started IAL1 Verification', strings ? '4' : 4, '100.0%'], + ['New Users Completed IAL1 Password Setup', strings ? '3' : 3, '75.0%'], + ['New Users Completed IAL1 MFA', strings ? '2' : 2, '50.0%'], + ['New IAL1 Users Consented to Partner', strings ? '1' : 1, '25.0%'], + ['AAL2 Authentication Requests from Partner', strings ? '5' : 5, '100.0%'], + ['AAL2 Authenticated Requests', strings ? '2' : 2, '40.0%'], + ], + ] + end end diff --git a/spec/models/document_capture_session_spec.rb b/spec/models/document_capture_session_spec.rb index 6d61bd49392..f140962c30b 100644 --- a/spec/models/document_capture_session_spec.rb +++ b/spec/models/document_capture_session_spec.rb @@ -61,38 +61,6 @@ end end - describe '#store_doc_auth_result' do - it 'generates a result ID stores the result encrypted in redis' do - record = DocumentCaptureSession.new(result_id: SecureRandom.uuid) - - record.store_doc_auth_result( - result: doc_auth_response.to_h, - pii: doc_auth_response.pii_from_doc, - ) - - result_id = record.result_id - key = EncryptedRedisStructStorage.key(result_id, type: DocumentCaptureSessionAsyncResult) - data = REDIS_POOL.with { |client| client.get(key) } - expect(data).to be_a(String) - expect(data).to_not include('Testy') - expect(data).to_not include('Testerson') - expect(record.ocr_confirmation_pending).to eq(false) - end - - context 'with attention with barcode response' do - before { allow(doc_auth_response).to receive(:attention_with_barcode?).and_return(true) } - - it 'sets record as pending ocr confirmation' do - record = DocumentCaptureSession.new(result_id: SecureRandom.uuid) - record.store_doc_auth_result( - result: doc_auth_response.to_h, - pii: doc_auth_response.pii_from_doc, - ) - expect(record.ocr_confirmation_pending).to eq(true) - end - end - end - describe '#expired?' do before do allow(IdentityConfig.store).to receive(:doc_capture_request_valid_for_minutes).and_return(15) @@ -138,5 +106,27 @@ expect(result.failed_front_image?(nil)).to eq(false) expect(result.failed_back_image?(nil)).to eq(false) end + + it 'saves failed image finterprints' do + record = DocumentCaptureSession.new(result_id: SecureRandom.uuid) + + record.store_failed_auth_image_fingerprint( + 'fingerprint1', nil + ) + old_result = record.load_result + + record.store_failed_auth_image_fingerprint( + 'fingerprint2', 'fingerprint3' + ) + new_result = record.load_result + + expect(old_result.failed_front_image?('fingerprint1')).to eq(true) + expect(old_result.failed_front_image?('fingerprint2')).to eq(false) + expect(old_result.failed_back_image?('fingerprint3')).to eq(false) + + expect(new_result.failed_front_image?('fingerprint1')).to eq(true) + expect(new_result.failed_front_image?('fingerprint2')).to eq(true) + expect(new_result.failed_back_image?('fingerprint3')).to eq(true) + end end end diff --git a/spec/presenters/two_factor_login_options_presenter_spec.rb b/spec/presenters/two_factor_login_options_presenter_spec.rb index 926e6204f88..ce1ace484d0 100644 --- a/spec/presenters/two_factor_login_options_presenter_spec.rb +++ b/spec/presenters/two_factor_login_options_presenter_spec.rb @@ -61,18 +61,101 @@ ) end - context 'with multiple webauthn configurations' do - let(:user) { create(:user) } - before(:each) do - create_list(:webauthn_configuration, 2, user: user) - user.webauthn_configurations.reload + describe '#options' do + let(:user) do + create( + :user, + :fully_registered, + :with_webauthn, + :with_webauthn_platform, + :with_phone, + :with_piv_or_cac, + :with_personal_key, + :with_backup_code, + :with_authentication_app, + ) + end + + subject(:options) { presenter.options } + let(:options_classes) { options.map(&:class) } + + it 'returns classes for mfas associated with account' do + expect(options_classes).to eq( + [ + TwoFactorAuthentication::SmsSelectionPresenter, + TwoFactorAuthentication::VoiceSelectionPresenter, + TwoFactorAuthentication::WebauthnSelectionPresenter, + TwoFactorAuthentication::BackupCodeSelectionPresenter, + TwoFactorAuthentication::PivCacSelectionPresenter, + TwoFactorAuthentication::SignInAuthAppSelectionPresenter, + TwoFactorAuthentication::PersonalKeySelectionPresenter, + ], + ) end it 'has only one webauthn selection presenter' do - webauthn_selection_presenters = presenter.options.map(&:class).select do |klass| + webauthn_selection_presenter_count = options_classes.count do |klass| klass == TwoFactorAuthentication::WebauthnSelectionPresenter end - expect(webauthn_selection_presenters.count).to eq 1 + + expect(webauthn_selection_presenter_count).to eq 1 + end + + context 'piv cac required' do + let(:piv_cac_required) { true } + + it 'filters to piv method' do + expect(options_classes).to eq([TwoFactorAuthentication::PivCacSelectionPresenter]) + end + + context 'in reauthentication context' do + let(:reauthentication_context) { true } + + it 'returns all mfas associated with account' do + expect(options_classes).to eq( + [ + TwoFactorAuthentication::SmsSelectionPresenter, + TwoFactorAuthentication::VoiceSelectionPresenter, + TwoFactorAuthentication::WebauthnSelectionPresenter, + TwoFactorAuthentication::BackupCodeSelectionPresenter, + TwoFactorAuthentication::PivCacSelectionPresenter, + TwoFactorAuthentication::SignInAuthAppSelectionPresenter, + TwoFactorAuthentication::PersonalKeySelectionPresenter, + ], + ) + end + end + end + + context 'phishing resistant required' do + let(:phishing_resistant_required) { true } + + it 'filters to phishing resistant methods' do + expect(options_classes).to eq( + [ + TwoFactorAuthentication::WebauthnSelectionPresenter, + TwoFactorAuthentication::PivCacSelectionPresenter, + ], + ) + end + + context 'in reauthentication context' do + let(:reauthentication_context) { true } + + it 'returns all mfas associated with account' do + expect(options_classes).to eq( + [ + TwoFactorAuthentication::SmsSelectionPresenter, + TwoFactorAuthentication::VoiceSelectionPresenter, + TwoFactorAuthentication::WebauthnSelectionPresenter, + TwoFactorAuthentication::BackupCodeSelectionPresenter, + TwoFactorAuthentication::PivCacSelectionPresenter, + TwoFactorAuthentication::SignInAuthAppSelectionPresenter, + TwoFactorAuthentication::PersonalKeySelectionPresenter, + ], + ) + end + end end end @@ -90,29 +173,6 @@ ) end - context 'piv cac required' do - let(:piv_cac_required) { true } - - it 'returns piv cac required warning text for app' do - expect(restricted_options_warning_text).to eq( - t('two_factor_authentication.aal2_request.piv_cac_only_html', sp_name: APP_NAME), - ) - end - - context 'with sp' do - let(:service_provider) { build(:service_provider) } - - it 'returns piv cac required warning text for service provider' do - expect(restricted_options_warning_text).to eq( - t( - 'two_factor_authentication.aal2_request.piv_cac_only_html', - sp_name: service_provider.friendly_name, - ), - ) - end - end - end - context 'with sp' do let(:service_provider) { build(:service_provider) } @@ -125,6 +185,12 @@ ) end end + + context 'in reauthentication context' do + let(:reauthentication_context) { true } + + it { should be_nil } + end end context 'piv cac required' do @@ -148,6 +214,12 @@ ) end end + + context 'in reauthentication context' do + let(:reauthentication_context) { true } + + it { should be_nil } + end end end diff --git a/spec/services/encryption/contextless_kms_client_spec.rb b/spec/services/encryption/contextless_kms_client_spec.rb index 4de95fe3e2f..d2c1e65cc38 100644 --- a/spec/services/encryption/contextless_kms_client_spec.rb +++ b/spec/services/encryption/contextless_kms_client_spec.rb @@ -148,7 +148,10 @@ end it 'logs the encryption' do - expect(Encryption::KmsLogger).to receive(:log).with(:encrypt) + expect(Encryption::KmsLogger).to receive(:log).with( + :encrypt, + key_id: IdentityConfig.store.aws_kms_key_id, + ) subject.encrypt(long_kms_plaintext) end @@ -180,7 +183,10 @@ end it 'logs the decryption' do - expect(Encryption::KmsLogger).to receive(:log).with(:decrypt) + expect(Encryption::KmsLogger).to receive(:log).with( + :decrypt, + key_id: IdentityConfig.store.aws_kms_key_id, + ) subject.decrypt('KMSx' + kms_ciphertext) end diff --git a/spec/services/encryption/kms_client_spec.rb b/spec/services/encryption/kms_client_spec.rb index b2a515b451b..9e9575c2d44 100644 --- a/spec/services/encryption/kms_client_spec.rb +++ b/spec/services/encryption/kms_client_spec.rb @@ -111,7 +111,11 @@ end it 'logs the context' do - expect(Encryption::KmsLogger).to receive(:log).with(:encrypt, encryption_context) + expect(Encryption::KmsLogger).to receive(:log).with( + :encrypt, + context: encryption_context, + key_id: subject.kms_key_id, + ) subject.encrypt(plaintext, encryption_context) end @@ -163,7 +167,11 @@ end it 'logs the context' do - expect(Encryption::KmsLogger).to receive(:log).with(:decrypt, encryption_context) + expect(Encryption::KmsLogger).to receive(:log).with( + :decrypt, + context: encryption_context, + key_id: subject.kms_key_id, + ) subject.decrypt(kms_ciphertext, encryption_context) end end diff --git a/spec/services/encryption/kms_logger_spec.rb b/spec/services/encryption/kms_logger_spec.rb index fdb4841b292..3036a547fb4 100644 --- a/spec/services/encryption/kms_logger_spec.rb +++ b/spec/services/encryption/kms_logger_spec.rb @@ -8,13 +8,18 @@ kms: { action: 'encrypt', encryption_context: { context: 'pii-encryption', user_uuid: '1234-abc' }, + key_id: 'super-duper-aws-kms-key-id', }, log_filename: Encryption::KmsLogger::LOG_FILENAME, }.to_json expect(described_class.logger).to receive(:info).with(log) - described_class.log(:encrypt, context: 'pii-encryption', user_uuid: '1234-abc') + described_class.log( + :encrypt, + context: { context: 'pii-encryption', user_uuid: '1234-abc' }, + key_id: 'super-duper-aws-kms-key-id', + ) end end @@ -24,13 +29,14 @@ kms: { action: 'decrypt', encryption_context: nil, + key_id: 'super-duper-aws-kms-key-id', }, log_filename: Encryption::KmsLogger::LOG_FILENAME, }.to_json expect(described_class.logger).to receive(:info).with(log) - described_class.log(:decrypt) + described_class.log(:decrypt, key_id: 'super-duper-aws-kms-key-id') end end end diff --git a/spec/jobs/reports/monthly_account_reuse_report_spec.rb b/spec/services/reporting/account_reuse_and_total_identities_report_spec.rb similarity index 60% rename from spec/jobs/reports/monthly_account_reuse_report_spec.rb rename to spec/services/reporting/account_reuse_and_total_identities_report_spec.rb index e16f933d5eb..fad3350bc5e 100644 --- a/spec/jobs/reports/monthly_account_reuse_report_spec.rb +++ b/spec/services/reporting/account_reuse_and_total_identities_report_spec.rb @@ -1,45 +1,16 @@ require 'rails_helper' require 'csv' -RSpec.describe Reports::MonthlyAccountReuseReport do +RSpec.describe Reporting::AccountReuseAndTotalIdentitiesReport do let(:report_date) { Date.new(2021, 3, 1) } - subject(:report) { Reports::MonthlyAccountReuseReport.new } - - let(:s3_report_bucket_prefix) { 'reports-bucket' } - let(:s3_report_path) do - 'int/monthly-account-reuse-report/2021/2021-03-01.monthly-account-reuse-report.json' - end + subject(:report) { Reporting::AccountReuseAndTotalIdentitiesReport.new(report_date) } before do travel_to report_date - allow(Identity::Hostdata).to receive(:env).and_return('int') - allow(Identity::Hostdata).to receive(:aws_account_id).and_return('1234') - allow(Identity::Hostdata).to receive(:aws_region).and_return('us-west-1') - allow(IdentityConfig.store).to receive(:s3_report_bucket_prefix). - and_return(s3_report_bucket_prefix) - - Aws.config[:s3] = { - stub_responses: { - put_object: {}, - }, - } end describe '#perform' do - it 'uploads a file to S3 based on the report date' do - expect(report).to receive(:upload_file_to_s3_bucket).with( - path: s3_report_path, - body: anything, - content_type: 'text/csv', - bucket: 'reports-bucket.1234-us-west-1', - ).exactly(1).time.and_call_original - - expect(report).to receive(:report_body).and_call_original.once - - report.perform - end - context 'with data' do let(:in_query) { report_date - 12.days } let(:out_of_query) { report_date + 12.days } @@ -121,32 +92,16 @@ def create_identity(id, provider, verified_time) end end - it 'aggregates by issuer' do - expect(report).to receive(:upload_file_to_s3_bucket). - exactly(1).times do |path:, body:, content_type:, bucket:| - actual_csv = body - expected_csv = CSV.generate do |csv| - [ - [ - { title: 'IDV app reuse rate Feb-2021', float_as_percent: true, precision: 4 }, - ['Num. SPs', 'Num. users', 'Percentage'], - [2, 3, 0.3], - [3, 2, 0.2], - ['Total (all >1)', 5, 0.5], - ], - [ - { title: 'Total proofed identities' }, - ['Total proofed identities (Feb-2021)'], - [10], - ], - ].each do |row| - csv << row - end - end - expect(actual_csv).to eq(expected_csv) - end + it 'returns correct queries' do + actual_account_reuse_table = report.account_reuse_report + actual_total_profiles_table = report.total_identities_report + + expected_account_reuse_table = [['Num. SPs', 'Num. users', 'Percentage'], [2, 3, 0.3], + [3, 2, 0.2], ['Total (all >1)', 5, 0.5]] + expected_total_profiles_table = [['Total proofed identities (Feb-2021)'], [10]] - report.perform + expect(actual_account_reuse_table).to eq(expected_account_reuse_table) + expect(actual_total_profiles_table).to eq(expected_total_profiles_table) end end end diff --git a/spec/support/features/doc_auth_helper.rb b/spec/support/features/doc_auth_helper.rb index b89572cf6c5..c331efa85b7 100644 --- a/spec/support/features/doc_auth_helper.rb +++ b/spec/support/features/doc_auth_helper.rb @@ -273,17 +273,6 @@ def mock_doc_auth_acuant_error_unknown ) end - def set_up_document_capture_result(uuid:, idv_result:) - dcs = DocumentCaptureSession.where(uuid: uuid).first_or_create - dcs.create_doc_auth_session - if idv_result - dcs.store_doc_auth_result( - result: idv_result.except(:pii_from_doc), - pii: idv_result[:pii_from_doc], - ) - end - end - def verify_phone_otp choose_idv_otp_delivery_method_sms fill_in_code_with_last_phone_otp diff --git a/spec/views/idv/agreement/show.html.erb_spec.rb b/spec/views/idv/agreement/show.html.erb_spec.rb index d4aceb05d7f..25ba56fd5c8 100644 --- a/spec/views/idv/agreement/show.html.erb_spec.rb +++ b/spec/views/idv/agreement/show.html.erb_spec.rb @@ -14,7 +14,7 @@ it 'includes code to track clicks on the consent checkbox' do selector = [ 'lg-click-observer[event-name="IdV: consent checkbox toggled"]', - '[name="doc_auth[ial2_consent_given]"]', + '[name="doc_auth[idv_consent_given]"]', ].join ' ' expect(rendered).to have_css(selector) diff --git a/spec/views/idv/getting_started/show.html.erb_spec.rb b/spec/views/idv/getting_started/show.html.erb_spec.rb index 14b22b96648..722bc59513e 100644 --- a/spec/views/idv/getting_started/show.html.erb_spec.rb +++ b/spec/views/idv/getting_started/show.html.erb_spec.rb @@ -36,7 +36,7 @@ it 'includes code to track clicks on the consent checkbox' do selector = [ 'lg-click-observer[event-name="IdV: consent checkbox toggled"]', - '[name="doc_auth[ial2_consent_given]"]', + '[name="doc_auth[idv_consent_given]"]', ].join ' ' expect(rendered).to have_css(selector) diff --git a/svgo.config.js b/svgo.config.js index 5e87d040fc6..bbe5780705d 100644 --- a/svgo.config.js +++ b/svgo.config.js @@ -1,4 +1,5 @@ -module.exports = /** @type {import('svgo').OptimizeOptions} */ ({ +/** @type {import('svgo').Config} */ +const config = { multipass: true, plugins: [ { @@ -24,4 +25,6 @@ module.exports = /** @type {import('svgo').OptimizeOptions} */ ({ }, }, ], -}); +}; + +module.exports = config; diff --git a/tsconfig.json b/tsconfig.json index 08f6846bf42..1c56906d480 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,6 +22,7 @@ "app/javascript/packs", "spec/javascript/spec_helper.d.ts", "spec/javascript/**/*.ts", + "./*.js", "scripts" ], "exclude": [ diff --git a/yarn.lock b/yarn.lock index e68abcf6225..1524e115352 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,12 +2,12 @@ # yarn lockfile v1 -"@18f/identity-design-system@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@18f/identity-design-system/-/identity-design-system-7.1.0.tgz#ddde006a4ee1371fed75dda6cd97ef0cc929331c" - integrity sha512-/DChj/p9hju15HMQCtukId2POIhHo25SjZzKh11bklkTICLOBEpPc1GRIkWUIDv5WU/N5DtX1saFafqs+HKnEA== +"@18f/identity-design-system@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@18f/identity-design-system/-/identity-design-system-8.0.0.tgz#8fa0d2610d80d57653fab98aa79c241fc4265627" + integrity sha512-QNrtTf3pBuM7tNBqfN8wnm0QPr82FaCerD6V+qADn3fC6cfoQiC13mEtvn2yq4sdwsJETcMqs0KCcqBLuP8luQ== dependencies: - "@uswds/uswds" "^3.4.1" + "@uswds/uswds" "^3.6.1" "@aashutoshrathi/word-wrap@^1.2.3": version "1.2.6" @@ -1675,14 +1675,15 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/parser@^5.38.1": - version "5.38.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.38.1.tgz#c577f429f2c32071b92dff4af4f5fbbbd2414bd0" - integrity sha512-LDqxZBVFFQnQRz9rUZJhLmox+Ep5kdUmLatLQnCRR6523YV+XhRjfYzStQ4MheFA8kMAfUlclHSbu+RKdRwQKw== +"@typescript-eslint/parser@^6.7.4": + version "6.7.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.7.4.tgz#23d1dd4fe5d295c7fa2ab651f5406cd9ad0bd435" + integrity sha512-I5zVZFY+cw4IMZUeNCU7Sh2PO5O57F7Lr0uyhgCJmhN/BuTlnc55KxPonR4+EM3GBdfiCyGZye6DgMjtubQkmA== dependencies: - "@typescript-eslint/scope-manager" "5.38.1" - "@typescript-eslint/types" "5.38.1" - "@typescript-eslint/typescript-estree" "5.38.1" + "@typescript-eslint/scope-manager" "6.7.4" + "@typescript-eslint/types" "6.7.4" + "@typescript-eslint/typescript-estree" "6.7.4" + "@typescript-eslint/visitor-keys" "6.7.4" debug "^4.3.4" "@typescript-eslint/scope-manager@5.38.1": @@ -1693,6 +1694,14 @@ "@typescript-eslint/types" "5.38.1" "@typescript-eslint/visitor-keys" "5.38.1" +"@typescript-eslint/scope-manager@6.7.4": + version "6.7.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.7.4.tgz#a484a17aa219e96044db40813429eb7214d7b386" + integrity sha512-SdGqSLUPTXAXi7c3Ob7peAGVnmMoGzZ361VswK2Mqf8UOYcODiYvs8rs5ILqEdfvX1lE7wEZbLyELCW+Yrql1A== + dependencies: + "@typescript-eslint/types" "6.7.4" + "@typescript-eslint/visitor-keys" "6.7.4" + "@typescript-eslint/type-utils@5.38.1": version "5.38.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.38.1.tgz#7f038fcfcc4ade4ea76c7c69b2aa25e6b261f4c1" @@ -1708,6 +1717,11 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.38.1.tgz#74f9d6dcb8dc7c58c51e9fbc6653ded39e2e225c" integrity sha512-QTW1iHq1Tffp9lNfbfPm4WJabbvpyaehQ0SrvVK2yfV79SytD9XDVxqiPvdrv2LK7DGSFo91TB2FgWanbJAZXg== +"@typescript-eslint/types@6.7.4": + version "6.7.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.7.4.tgz#5d358484d2be986980c039de68e9f1eb62ea7897" + integrity sha512-o9XWK2FLW6eSS/0r/tgjAGsYasLAnOWg7hvZ/dGYSSNjCh+49k5ocPN8OmG5aZcSJ8pclSOyVKP2x03Sj+RrCA== + "@typescript-eslint/typescript-estree@5.38.1": version "5.38.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.38.1.tgz#657d858d5d6087f96b638ee383ee1cff52605a1e" @@ -1721,6 +1735,19 @@ semver "^7.3.7" tsutils "^3.21.0" +"@typescript-eslint/typescript-estree@6.7.4": + version "6.7.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.4.tgz#f2baece09f7bb1df9296e32638b2e1130014ef1a" + integrity sha512-ty8b5qHKatlNYd9vmpHooQz3Vki3gG+3PchmtsA4TgrZBKWHNjWfkQid7K7xQogBqqc7/BhGazxMD5vr6Ha+iQ== + dependencies: + "@typescript-eslint/types" "6.7.4" + "@typescript-eslint/visitor-keys" "6.7.4" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.5.4" + ts-api-utils "^1.0.1" + "@typescript-eslint/utils@5.38.1": version "5.38.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.38.1.tgz#e3ac37d7b33d1362bb5adf4acdbe00372fb813ef" @@ -1741,15 +1768,23 @@ "@typescript-eslint/types" "5.38.1" eslint-visitor-keys "^3.3.0" +"@typescript-eslint/visitor-keys@6.7.4": + version "6.7.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.4.tgz#80dfecf820fc67574012375859085f91a4dff043" + integrity sha512-pOW37DUhlTZbvph50x5zZCkFn3xzwkGtNoJHzIM3svpiSkJzwOYr/kVBaXmf+RAQiUDs1AHEZVNPg6UJCJpwRA== + dependencies: + "@typescript-eslint/types" "6.7.4" + eslint-visitor-keys "^3.4.1" + "@ungap/promise-all-settled@1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== -"@uswds/uswds@^3.4.1": - version "3.4.1" - resolved "https://registry.yarnpkg.com/@uswds/uswds/-/uswds-3.4.1.tgz#dd15644d791e1969dff2394d9d2ec2ea1e795128" - integrity sha512-eLYbWUqf9eWUa2P6CO3ckIjtQyM3AylrIOHxN5gYG3P62TDd3FzRDyoACfvOe6CNk0w0PqXWJnuPzxpNoOgWNA== +"@uswds/uswds@^3.6.1": + version "3.6.1" + resolved "https://registry.yarnpkg.com/@uswds/uswds/-/uswds-3.6.1.tgz#bfa64d0cd64f333bccb1cfb50be44c1d6b70e3a0" + integrity sha512-KDr3r4xvbvQ1X05tfacid42m/vLjAAt8N3q2/LDuujjrrBxEdHgK9ROftsesuSBoaD2Fss4lKxS0dPojLzdbbw== dependencies: classlist-polyfill "1.0.3" object-assign "4.1.1" @@ -2693,26 +2728,18 @@ css-functions-list@^3.1.0: resolved "https://registry.yarnpkg.com/css-functions-list/-/css-functions-list-3.1.0.tgz#cf5b09f835ad91a00e5959bcfc627cd498e1321b" integrity sha512-/9lCvYZaUbBGvYUgYGFJ4dcYiyqdhSjG7IPVluoV8A1ILjkF7ilmhp1OGUz8n+nmBcu0RNrQAzgD8B6FJbrt2w== -css-select@^4.1.3: - version "4.2.1" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.2.1.tgz#9e665d6ae4c7f9d65dbe69d0316e3221fb274cdd" - integrity sha512-/aUslKhzkTNCQUB2qTX84lVmfia9NyjP3WpDGtj/WxhwBzWBYUV3DgUpurHTme8UTPcPlAD1DJ+b0nN/t50zDQ== +css-select@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" + integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== dependencies: boolbase "^1.0.0" - css-what "^5.1.0" - domhandler "^4.3.0" - domutils "^2.8.0" + css-what "^6.1.0" + domhandler "^5.0.2" + domutils "^3.0.1" nth-check "^2.0.1" -css-tree@^1.1.2, css-tree@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" - integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== - dependencies: - mdn-data "2.0.14" - source-map "^0.6.1" - -css-tree@^2.3.1: +css-tree@^2.2.1, css-tree@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.3.1.tgz#10264ce1e5442e8572fc82fbe490644ff54b5c20" integrity sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw== @@ -2720,22 +2747,30 @@ css-tree@^2.3.1: mdn-data "2.0.30" source-map-js "^1.0.1" -css-what@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.1.0.tgz#3f7b707aadf633baf62c2ceb8579b545bb40f7fe" - integrity sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw== +css-tree@~2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.2.1.tgz#36115d382d60afd271e377f9c5f67d02bd48c032" + integrity sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA== + dependencies: + mdn-data "2.0.28" + source-map-js "^1.0.1" + +css-what@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== -csso@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" - integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== +csso@^5.0.5: + version "5.0.5" + resolved "https://registry.yarnpkg.com/csso/-/csso-5.0.5.tgz#f9b7fe6cc6ac0b7d90781bb16d5e9874303e2ca6" + integrity sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ== dependencies: - css-tree "^1.1.2" + css-tree "~2.2.0" cssstyle@^3.0.0: version "3.0.0" @@ -2967,19 +3002,19 @@ dom-accessibility-api@^0.5.14, dom-accessibility-api@^0.5.6, dom-accessibility-a resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.14.tgz#56082f71b1dc7aac69d83c4285eef39c15d93f56" integrity sha512-NMt+m9zFMPZe0JcY9gN224Qvk6qLIdqex29clBvc/y75ZBX9YA9wNK3frsYvu2DI1xcCIwxwnX+TlsJ2DSOADg== -dom-serializer@^1.0.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91" - integrity sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig== +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== dependencies: - domelementtype "^2.0.1" - domhandler "^4.2.0" - entities "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" -domelementtype@^2.0.1, domelementtype@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" - integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== +domelementtype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== domexception@^4.0.0: version "4.0.0" @@ -2988,21 +3023,21 @@ domexception@^4.0.0: dependencies: webidl-conversions "^7.0.0" -domhandler@^4.2.0, domhandler@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.0.tgz#16c658c626cf966967e306f966b431f77d4a5626" - integrity sha512-fC0aXNQXqKSFTr2wDNZDhsEYjCiYsDWl3D01kwt25hm1YIPyDGHvvi3rw+PLqHAl/m71MaiF7d5zvBr0p5UB2g== +domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== dependencies: - domelementtype "^2.2.0" + domelementtype "^2.3.0" -domutils@^2.8.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" - integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== +domutils@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" + integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== dependencies: - dom-serializer "^1.0.1" - domelementtype "^2.2.0" - domhandler "^4.2.0" + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" ee-first@1.1.1: version "1.1.1" @@ -3042,15 +3077,10 @@ enhanced-resolve@^5.10.0: graceful-fs "^4.2.4" tapable "^2.2.0" -entities@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f" - integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ== - -entities@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-4.4.0.tgz#97bdaba170339446495e653cfd2db78962900174" - integrity sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA== +entities@^4.2.0, entities@^4.4.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== envinfo@^7.7.3: version "7.8.1" @@ -4781,10 +4811,10 @@ mathml-tag-names@^2.1.3: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -mdn-data@2.0.14: - version "2.0.14" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" - integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== +mdn-data@2.0.28: + version "2.0.28" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.28.tgz#5ec48e7bef120654539069e1ae4ddc81ca490eba" + integrity sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g== mdn-data@2.0.30: version "2.0.30" @@ -6020,7 +6050,7 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.4, semver@^7.3.7: +semver@^7.3.4, semver@^7.3.7, semver@^7.5.4: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== @@ -6260,11 +6290,6 @@ spdy@^4.0.2: select-hose "^2.0.0" spdy-transport "^3.0.0" -stable@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" - integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== - statuses@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" @@ -6507,18 +6532,17 @@ svg-tags@^1.0.0: resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764" integrity sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q= -svgo@^2.8.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24" - integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg== +svgo@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-3.0.2.tgz#5e99eeea42c68ee0dc46aa16da093838c262fe0a" + integrity sha512-Z706C1U2pb1+JGP48fbazf3KxHrWOsLme6Rv7imFBn5EnuanDW1GPaA/P1/dvObE670JDePC3mnj0k0B7P0jjQ== dependencies: "@trysound/sax" "0.2.0" commander "^7.2.0" - css-select "^4.1.3" - css-tree "^1.1.3" - csso "^4.2.0" + css-select "^5.1.0" + css-tree "^2.2.1" + csso "^5.0.5" picocolors "^1.0.0" - stable "^0.1.8" swr@^2.0.0: version "2.0.0" @@ -6653,6 +6677,11 @@ trim-newlines@^4.0.2: resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-4.1.1.tgz#28c88deb50ed10c7ba6dc2474421904a00139125" integrity sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ== +ts-api-utils@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.3.tgz#f12c1c781d04427313dbac808f453f050e54a331" + integrity sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg== + tsconfig-paths@^3.14.1: version "3.14.2" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088" @@ -6729,10 +6758,10 @@ typed-array-length@^1.0.4: for-each "^0.3.3" is-typed-array "^1.1.9" -typescript@^5.0.4: - version "5.0.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b" - integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== +typescript@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" + integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== unbox-primitive@^1.0.2: version "1.0.2"