Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add links to company websites for breach resolution #2961

Merged
merged 26 commits into from
Apr 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
fdac319
chore: Add links to company websites for breach resolution
flozia Mar 31, 2023
b075643
chore: Add list of links with their status generated by https://githu…
flozia Apr 4, 2023
8944a11
chore: Add variable breach recommendation strings for passwords and s…
flozia Apr 5, 2023
e930adb
chore: Only show breached company links if they are not on our block …
flozia Apr 5, 2023
58f3d2a
merge: main -> MNTOR-1504-Add-links-to-websites
flozia Apr 5, 2023
f5d2da1
chore: Update tsconfig module target
flozia Apr 5, 2023
1b25b2c
fix: Breach resolution test
flozia Apr 5, 2023
aa00a5a
chore: Use different string id for headers with links
flozia Apr 6, 2023
47c7c51
fix: Linter
flozia Apr 6, 2023
838e01e
chore: Use more semantic header string id
flozia Apr 6, 2023
25d4ff7
chore: Use a preset domain blocklist
flozia Apr 13, 2023
c0a14d0
Merge branch 'main' into MNTOR-1504-Add-links-to-websites
flozia Apr 18, 2023
2d1f4ca
chore: Update breach resolution strings for password and security que…
flozia Apr 18, 2023
5a26bbb
fix: Update breach resolution test
flozia Apr 18, 2023
5b539ab
chore: Update comment
flozia Apr 18, 2023
3f7097b
chore: Split domain list and check for exact matches
flozia Apr 18, 2023
2724dca
chore: Remove async
flozia Apr 18, 2023
d88ab70
chore: Revert changes in tsconfig.json
flozia Apr 18, 2023
91dfced
Merge branch 'MNTOR-1504-Add-links-to-websites' of github.com:mozilla…
flozia Apr 18, 2023
32f95cf
chore: Revert changes in tsconfig.json
flozia Apr 18, 2023
54ce192
fix: Remove JSDoc for hideBreachLink
flozia Apr 18, 2023
95f2f3e
chore: Remove unused strings
flozia Apr 18, 2023
d574bee
chore: Replace not only in passwords and security questions
flozia Apr 18, 2023
c1eb5ec
Merge branch 'main' into MNTOR-1504-Add-links-to-websites
flozia Apr 18, 2023
e05ea1a
chore: Add notes on 2FA and Firefox Password Manager back in
flozia Apr 18, 2023
94152f5
chore: Update breach resolution test
flozia Apr 18, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env-dist
Original file line number Diff line number Diff line change
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a limit how long an ENV var can be?
It feels like this could be REALLY long if we end up blocking over 20-30 domains.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to SRE, there is no limit that we would hit.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if it's better as an ENV var that has to be coordinated w/ SRE, versus maybe some JSON file that lives in the repo that is a single source of truth that we can audit occasionally. (unless there are reasons to keep it as a secret/ENV). 🤷
Although I guess I could technically recreate the blocklist locally by scraping the 650 breaches on the Monitor site and see if the outbound link is a link or not.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m still not sure about that as well, but one good argument for handling the list in the env is that we would be able to make adjustments without a release. Especially in the beginning, when we might need to test and audit the sites manually.


# OneRep API for exposure scanning
ONEREP_API_KEY=
Expand Down
14 changes: 8 additions & 6 deletions locales/en/breaches.ftl
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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'
flozia marked this conversation as resolved.
Show resolved Hide resolved
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}` : '',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aside: in my personal testing, I think http:// had better results than https:// (somewhere between 5-10% more 2xx/3xx results).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, thanks for the note. With us trying to be cautious where we link out to I think I’d feel more comfortable linking out to https://.

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:
flozia marked this conversation as resolved.
Show resolved Hide resolved
(![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
Original file line number Diff line number Diff line change
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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know how easy it is to mock AppConstants, but if it's easy (but only then - you've been working on this PR for long enough), maybe a test for the blocklist would be a good addition?

Copy link
Collaborator Author

@flozia flozia Apr 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! I create an issue for this and will address this in a follow-up in order to not block this PR.

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