Skip to content

Commit

Permalink
Merge pull request #2961 from mozilla/MNTOR-1504-Add-links-to-websites
Browse files Browse the repository at this point in the history
Add links to company websites for breach resolution
  • Loading branch information
flozia committed Apr 19, 2023
2 parents 99a6884 + 94152f5 commit 268df94
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 26 deletions.
2 changes: 2 additions & 0 deletions .env-dist
Expand Up @@ -58,6 +58,8 @@ HIBP_THROTTLE_DELAY=2000
HIBP_THROTTLE_MAX_TRIES=5
# Authorization token for HIBP to present to /hibp/notify endpoint
HIBP_NOTIFY_TOKEN=unsafe-default-token-for-dev
# Domains we prefer to not link to
HIBP_BREACH_DOMAIN_BLOCKLIST=a-blocked-domain.com,another-blocked-domain.org

# OneRep API for exposure scanning
ONEREP_API_KEY=
Expand Down
14 changes: 8 additions & 6 deletions locales/en/breaches.ftl
Expand Up @@ -67,11 +67,12 @@ breach-checklist-link-mozilla-vpn = { -brand-mozilla-vpn }
## Prompts the user for changes when there is a breach detected of password

# { $breachedCompanyLink } will link to the website of the company where the breach occurred
breach-checklist-pw-header-2 = Go to the company’s website to change your password and enable two-factor authentication (2FA).
breach-checklist-pw-header-text = Update your passwords and enable two-factor authentication (2FA).
# The `breached-company-link` tags will be replaced with link tags or stripped if no link is available.
# Variables:
# $passwordManagerLink (string) - a link to the password manager documentation, with { -breach-checklist-link-password-manager } as the label
breach-checklist-pw-body-2 = Make sure your password is unique and hard to guess. If this password is used on any other accounts, be sure to change it there too. { $passwordManagerLink } can help you securely keep track of all of your passwords.
breach-checklist-pw-body-text = In most cases, we’d recommend that you change your password on the company’s website. But <b>their website may be down or contain malicious content</b>, so use caution if you <breached-company-link>visit the site</breached-company-link>. For added protection, make sure you’re using unique passwords for all accounts, so that any leaked passwords can’t be used to access other accounts. { $passwordManagerLink } can help you securely keep track of all of your passwords.
## Prompts the user for changes when there is a breach detected of email

Expand Down Expand Up @@ -135,9 +136,10 @@ breach-checklist-phone-header-2 = Protect your phone number with a masking servi
## Prompts the user for changes when there is a breach detected of security questions

# { $breachedCompanyLink } will link to the website of the company where the breach occurred
breach-checklist-sq-header-2 = Update your security questions on the company’s website.
breach-checklist-sq-body = Use long, random answers, and store them somewhere safe. Do this anywhere else you’ve used the same security questions.
breach-checklist-sq-header-text = Update your security questions.
# The `breached-company-link` tags will be replaced with link tags or stripped if no link is available.
breach-checklist-sq-body-text = In most cases, we’d recommend that you update your security questions on the company’s website. But <b>their website may be down or contain malicious content</b>, so use caution if you <breached-company-link>visit the site</breached-company-link>. For added protection, update these security questions on any important accounts where you’ve used them, and create unique passwords for all accounts.
## Prompts the user for changes when there is a breach detected of historical password

Expand Down
5 changes: 3 additions & 2 deletions src/appConstants.js
Expand Up @@ -43,12 +43,13 @@ const requiredEnvVars = [
]

const optionalEnvVars = [
'EMAIL_TEST_RECIPIENT',
'FX_REMOTE_SETTINGS_WRITER_PASS',
'FX_REMOTE_SETTINGS_WRITER_SERVER',
'FX_REMOTE_SETTINGS_WRITER_USER',
'EMAIL_TEST_RECIPIENT',
'GA4_MEASUREMENT_ID',
'GA4_DEBUG_MODE',
'GA4_MEASUREMENT_ID',
'HIBP_BREACH_DOMAIN_BLOCKLIST',
'LIVE_RELOAD',
'PORT',
'SENTRY_DSN_LEGACY'
Expand Down
64 changes: 51 additions & 13 deletions src/utils/breachResolution.js
Expand Up @@ -2,6 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import AppConstants from '../appConstants.js'
import { getMessage } from './fluent.js'

/**
Expand Down Expand Up @@ -34,8 +35,8 @@ const BreachDataTypes = {
const breachResolutionDataTypes = {
[BreachDataTypes.Passwords]: {
priority: 1,
header: 'breach-checklist-pw-header-2',
body: 'breach-checklist-pw-body-2'
header: 'breach-checklist-pw-header-text',
body: 'breach-checklist-pw-body-text'
},
[BreachDataTypes.Email]: {
priority: 2,
Expand Down Expand Up @@ -85,8 +86,8 @@ const breachResolutionDataTypes = {
},
[BreachDataTypes.SecurityQuestions]: {
priority: 11,
header: 'breach-checklist-sq-header-2',
body: 'breach-checklist-sq-body'
header: 'breach-checklist-sq-header-text',
body: 'breach-checklist-sq-body-text'
},
[BreachDataTypes.HistoricalPasswords]: {
priority: 12,
Expand All @@ -109,19 +110,24 @@ const breachResolutionDataTypes = {
*/
function appendBreachResolutionChecklist (userBreachData, options = {}) {
const { verifiedEmails } = userBreachData

for (const { breaches } of verifiedEmails) {
breaches.forEach(b => {
const dataClasses = b.DataClasses
const blockList = (AppConstants.HIBP_BREACH_DOMAIN_BLOCKLIST ?? '').split(',')
const showLink = b.Domain && !blockList.includes(b.Domain)

const args = {
companyName: b.Name,
breachedCompanyLink: showLink ? `https://${b.Domain}` : '',
firefoxRelayLink: `<a href="https://relay.firefox.com/?utm_medium=mozilla-websites&utm_source=monitor&utm_campaign=&utm_content=breach-resolution" target="_blank">${getMessage('breach-checklist-link-firefox-relay')}</a>`,
passwordManagerLink: `<a href="https://www.mozilla.org/firefox/features/password-manager/?utm_medium=mozilla-websites&utm_source=monitor&utm_campaign=&utm_content=breach-resolution" target="_blank">${getMessage('breach-checklist-link-password-manager')}</a>`,
mozillaVpnLink: `<a href="https://www.mozilla.org/products/vpn/?utm_medium=mozilla-websites&utm_source=monitor&utm_campaign=&utm_content=breach-resolution" target="_blank">${getMessage('breach-checklist-link-mozilla-vpn')}</a>`,
equifaxLink: '<a href="https://www.equifax.com/personal/credit-report-services/credit-freeze/" target="_blank">Equifax</a>',
experianLink: '<a href="https://www.experian.com/freeze/center.html" target="_blank">Experian</a>',
transUnionLink: '<a href="https://www.transunion.com/credit-freeze" target="_blank">TransUnion</a>'
}
b.breachChecklist = getResolutionRecsPerBreach(dataClasses, args, { ...options, hideBreachLink: b.Domain.length <= 0 })
b.breachChecklist = getResolutionRecsPerBreach(dataClasses, args, options)
})
}
}
Expand All @@ -134,39 +140,71 @@ function appendBreachResolutionChecklist (userBreachData, options = {}) {
* @param {object} args contains necessary variables for the fluent file
* - companyName
* - breachedCompanyUrl
* @param {Partial<{ countryCode: string, hideBreachLink: boolean }>} options
* @param {Partial<{ countryCode: string }>} options
* @returns {Map} map of relevant breach resolution recommendations
*/
function getResolutionRecsPerBreach (dataTypes, args, options = {}) {
const filteredBreachRecs = {}

// filter breachResolutionDataTypes based on relevant data types passed in
for (const [key, value] of Object.entries(breachResolutionDataTypes)) {
for (const resolution of Object.entries(breachResolutionDataTypes)) {
const [key, value] = resolution
if (
dataTypes.includes(key) &&
// Hide the security question or password resolution if we can't link to the breached site:
(![BreachDataTypes.Passwords, BreachDataTypes.SecurityQuestions].includes(key) || !options.hideBreachLink) &&
// Hide resolutions that apply to other countries than the user's:
(!options.countryCode || !Array.isArray(value.applicableCountryCodes) || value.applicableCountryCodes.includes(options.countryCode.toLowerCase()))
) {
filteredBreachRecs[key] = getRecommendationFromResolution(value, args)
filteredBreachRecs[key] = getRecommendationFromResolution(resolution, args)
}
}

// If we did not have any recommendations, add a generic recommendation:
if (Object.keys(filteredBreachRecs).length === 0) {
filteredBreachRecs[BreachDataTypes.General] = getRecommendationFromResolution(breachResolutionDataTypes[BreachDataTypes.General], args)
const resolutionTypeGeneral = BreachDataTypes.General
filteredBreachRecs[resolutionTypeGeneral] = getRecommendationFromResolution(
[
resolutionTypeGeneral,
breachResolutionDataTypes[resolutionTypeGeneral]
],
args
)
}

// loop through the breach recs
return filteredBreachRecs
}

/**
* Get the fluent string for the body
*
* @param {string} body for the fluent body string
* @param {object} args
* @returns {string} body string
*/
function getBodyMessage (body, args) {
const { stringArgs } = args

const companyLink = stringArgs.breachedCompanyLink
return getMessage(body, stringArgs)
.replace(
'<breached-company-link>',
companyLink ? `<a href="${companyLink}" target="_blank">` : ''
)
.replace(
'</breached-company-link>',
companyLink ? '</a>' : ''
)
}

// find fluent text based on fluent ids
function getRecommendationFromResolution (resolution, args) {
let { header, body, priority } = resolution
const [resolutionType, resolutionContent] = resolution
let { header, body, priority } = resolutionContent

header = header ? getMessage(header, args) : ''
body = body ? getMessage(body, args) : ''
body = body
? getBodyMessage(body, { resolutionType, stringArgs: args })
: ''
return { header, body, priority }
}

Expand Down
21 changes: 16 additions & 5 deletions src/utils/breachResolution.test.js
Expand Up @@ -33,7 +33,9 @@ test('appendBreachResolutionChecklist: two data classes', t => {
appendBreachResolutionChecklist(userBreachData)
t.truthy(userBreachData.verifiedEmails[0].breaches[0].breachChecklist)
t.is(userBreachData.verifiedEmails[0].breaches[0].breachChecklist[BreachDataTypes.Passwords].header,
'Go to the company’s website to change your password and enable two-factor authentication (2FA).')
'Update your passwords and enable two-factor authentication (2FA).')
t.is(userBreachData.verifiedEmails[0].breaches[0].breachChecklist[BreachDataTypes.Passwords].body,
'In most cases, we’d recommend that you change your password on the company’s website. But <b>their website may be down or contain malicious content</b>, so use caution if you <a href="https://companyName.com" target="_blank">visit the site</a>. For added protection, make sure you’re using unique passwords for all accounts, so that any leaked passwords can’t be used to access other accounts. <a href="https://www.mozilla.org/firefox/features/password-manager/?utm_medium=mozilla-websites&utm_source=monitor&utm_campaign=&utm_content=breach-resolution" target="_blank">Firefox Password Manager</a> can help you securely keep track of all of your passwords.')
t.is(userBreachData.verifiedEmails[0].breaches[0].breachChecklist[BreachDataTypes.Passwords].priority, 1)
})

Expand Down Expand Up @@ -147,13 +149,22 @@ test('appendBreachResolutionChecklist: data class with a resolution referring to
unverifiedEmails: []
}
appendBreachResolutionChecklist(userBreachData)
// There should only be a resolution for `BreachDataTypes.Phone`, as
// `BreachDataTypes.Passwords` and `BreachDataTypes.SecurityQuestions` refer
// to the breached company's domain, which we don't know:
// There should be a resolution for `BreachDataTypes.Phone`,
// `BreachDataTypes.Passwords` and `BreachDataTypes.SecurityQuestions`.
// The last two should fallback to a more generic header string that does not
// include the breached company's domain, which we don't know:
t.deepEqual(
Object.keys(userBreachData.verifiedEmails[0].breaches[0].breachChecklist),
[BreachDataTypes.Phone]
[BreachDataTypes.Passwords, BreachDataTypes.Phone, BreachDataTypes.SecurityQuestions]
)
t.is(userBreachData.verifiedEmails[0].breaches[0].breachChecklist[BreachDataTypes.Passwords].header,
'Update your passwords and enable two-factor authentication (2FA).')
t.is(userBreachData.verifiedEmails[0].breaches[0].breachChecklist[BreachDataTypes.Passwords].body,
'In most cases, we’d recommend that you change your password on the company’s website. But <b>their website may be down or contain malicious content</b>, so use caution if you visit the site. For added protection, make sure you’re using unique passwords for all accounts, so that any leaked passwords can’t be used to access other accounts. <a href="https://www.mozilla.org/firefox/features/password-manager/?utm_medium=mozilla-websites&utm_source=monitor&utm_campaign=&utm_content=breach-resolution" target="_blank">Firefox Password Manager</a> can help you securely keep track of all of your passwords.')
t.is(userBreachData.verifiedEmails[0].breaches[0].breachChecklist[BreachDataTypes.SecurityQuestions].header,
'Update your security questions.')
t.is(userBreachData.verifiedEmails[0].breaches[0].breachChecklist[BreachDataTypes.SecurityQuestions].body,
'In most cases, we’d recommend that you update your security questions on the company’s website. But <b>their website may be down or contain malicious content</b>, so use caution if you visit the site. For added protection, update these security questions on any important accounts where you’ve used them, and create unique passwords for all accounts.')
})

test('appendBreachResolutionChecklist: data class with a resolution referring to the breach\'s domain, which is available', t => {
Expand Down

0 comments on commit 268df94

Please sign in to comment.