• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mozilla / blurts-server / 4040bc25-7f30-464b-958e-746a756a4e44

pending completion
4040bc25-7f30-464b-958e-746a756a4e44

push

circleci

GitHub
Merge pull request #2790 from mozilla/MNTOR-1056-Migrate-breach-alert-email

282 of 1375 branches covered (20.51%)

Branch coverage included in aggregate %.

174 of 174 new or added lines in 17 files covered. (100.0%)

959 of 3709 relevant lines covered (25.86%)

4.2 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/controllers/hibp.js
1
/* This Source Code Form is subject to the terms of the Mozilla Public
2
 * License, v. 2.0. If a copy of the MPL was not distributed with this
3
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4

5
import { acceptedLanguages, negotiateLanguages } from 'fluent-langneg'
6
import AppConstants from '../app-constants.js'
7

8
import { getSubscribersByHashes } from '../db/tables/subscribers.js'
9
import { getEmailAddressesByHashes } from '../db/tables/email_addresses.js'
10
import { getTemplate } from '../views/email-2022.js'
11
import { breachAlertEmailPartial } from '../views/partials/email-breach-alert.js'
12

13
import {
14
  EmailTemplateType,
15
  getEmailCtaHref,
16
  getUnsubscribeUrl,
17
  sendEmail
18
} from '../utils/email.js'
19
import { getMessage } from '../utils/fluent.js'
20
import {
21
  getAddressesAndLanguageForEmail,
22
  getBreachByName,
23
  loadBreachesIntoApp
24
} from '../utils/hibp.js'
25
import { UnauthorizedError, UserInputError } from '../utils/error.js'
26
import mozlog from '../utils/log.js'
27

28
const log = mozlog('controllers.hibp')
×
29

30
/**
31
 * Whenever a breach is detected on the HIBP side, HIBP sends a request to this endpoint.
32
 * This function attempts to retrieve the breach info from the local cache, if not found
33
 * it retrieves it from the database
34
 * A breach notification contains the following parameters:
35
 * - breachName
36
 * - hashPrefix
37
 * - hashSuffixes
38
 * More about how account identities are anonymized: https://blog.mozilla.org/security/2018/06/25/scanning-breached-accounts-k-anonymity/
39
 */
40
async function notify (req, res) {
41
  if (!req.token || req.token !== AppConstants.HIBP_NOTIFY_TOKEN) {
×
42
    const errorMessage = 'HIBP notify endpoint requires valid authorization token.'
×
43
    throw new UnauthorizedError(errorMessage)
×
44
  }
45
  if (!['breachName', 'hashPrefix', 'hashSuffixes'].every(req.body?.hasOwnProperty, req.body)) {
×
46
    throw new UserInputError('HIBP breach notification: requires breachName, hashPrefix, and hashSuffixes.')
×
47
  }
48

49
  const { breachName, hashPrefix, hashSuffixes } = req.body
×
50

51
  let breachAlert = getBreachByName(req.app.locals.breaches, breachName)
×
52

53
  if (!breachAlert) {
×
54
    // If breach isn't found, try to reload breaches from HIBP
55
    log.debug('notify', 'Breach is not found, reloading breaches...')
×
56
    await loadBreachesIntoApp(req.app)
×
57
    breachAlert = getBreachByName(req.app.locals.breaches, breachName)
×
58
    if (!breachAlert) {
×
59
      // If breach *still* isn't found, we have a real error
60
      throw new Error('Unrecognized breach: ' + breachName)
×
61
    }
62
  }
63

64
  const { IsVerified, Domain, IsFabricated, IsSpamList } = breachAlert
×
65

66
  // If any of the following conditions are not satisfied:
67
  // Do not send the breach alert email! The `logId`s are being used for
68
  // logging in case we decide to not send the alert.
69
  const emailDeliveryConditions = [
×
70
    {
71
      logId: 'isNotVerified',
72
      condition: !IsVerified
73
    },
74
    {
75
      logId: 'domainEmpty',
76
      condition: Domain === ''
77
    },
78
    {
79
      logId: 'isFabricated',
80
      condition: IsFabricated
81
    },
82
    {
83
      logId: 'isSpam',
84
      condition: IsSpamList
85
    }
86
  ]
87

88
  const unsatisfiedConditions = emailDeliveryConditions.filter(condition => (
×
89
    condition.condition
×
90
  ))
91

92
  const doNotSendEmail = unsatisfiedConditions.length > 0
×
93

94
  if (doNotSendEmail) {
×
95
    // Get a list of the failed condition `logId`s
96
    const conditionLogIds = unsatisfiedConditions
×
97
      .map(condition => condition.logId)
×
98
      .join(', ')
99

100
    log.info('Breach alert email was not sent.', {
×
101
      name: breachAlert.Name,
102
      reason: `The following conditions were not satisfied: ${conditionLogIds}.`
103
    })
104

105
    return res.status(200).json({
×
106
      info: 'Breach loaded into database. Subscribers not notified.'
107
    })
108
  }
109

110
  try {
×
111
    const reqHashPrefix = hashPrefix.toLowerCase()
×
112
    const hashes = hashSuffixes.map(suffix => reqHashPrefix + suffix.toLowerCase())
×
113
    const subscribers = await getSubscribersByHashes(hashes)
×
114
    const emailAddresses = await getEmailAddressesByHashes(hashes)
×
115
    const recipients = subscribers.concat(emailAddresses)
×
116

117
    log.info(EmailTemplateType.Notification, {
×
118
      breachAlertName: breachAlert.Name,
119
      length: recipients.length
120
    })
121

122
    const utmCampaignId = 'breach-alert'
×
123
    const notifiedRecipients = []
×
124

125
    for (const recipient of recipients) {
×
126
      log.info('notify', { recipient })
×
127

128
      // Get subscriber ID from:
129
      // - `subscriber_id`: if `email_addresses` record
130
      // - `id`: if `subscribers` record
131
      const subscriberId = recipient.subscriber_id ?? recipient.id
×
132
      const {
133
        recipientEmail,
134
        breachedEmail,
135
        signupLanguage
136
      } = getAddressesAndLanguageForEmail(recipient)
×
137

138
      const requestedLanguage = signupLanguage
×
139
        ? acceptedLanguages(signupLanguage)
140
        : []
141

142
      const availableLanguages = req.app.locals.AVAILABLE_LANGUAGES
×
143
      const supportedLocales = negotiateLanguages(
×
144
        requestedLanguage,
145
        availableLanguages,
146
        { defaultLocale: 'en' }
147
      )
148

149
      if (!notifiedRecipients.includes(breachedEmail)) {
×
150
        const data = {
×
151
          breachData: breachAlert,
152
          breachedEmail,
153
          ctaHref: getEmailCtaHref(utmCampaignId, 'dashboard-cta'),
154
          heading: getMessage('email-spotted-new-breach'),
155
          // Override recipient if explicitly set in req
156
          recipientEmail: req.body.recipient ?? recipientEmail,
×
157
          subscriberId,
158
          supportedLocales,
159
          unsubscribeUrl: getUnsubscribeUrl(
160
            recipientEmail,
161
            'account-verification-email'
162
          ),
163
          utmCampaign: utmCampaignId
164
        }
165

166
        const emailTemplate = getTemplate(data, breachAlertEmailPartial)
×
167
        const subject = getMessage('breach-alert-subject')
×
168

169
        await sendEmail(data.recipientEmail, subject, emailTemplate)
×
170

171
        notifiedRecipients.push(breachedEmail)
×
172
      }
173
    }
174

175
    log.info('notified', { length: notifiedRecipients.length })
×
176

177
    res
×
178
      .status(200)
179
      .json({
180
        info: 'Notified subscribers of breach.'
181
      })
182
  } catch (error) {
183
    throw new Error(`Notifying subscribers of breach failed: ${error}`)
×
184
  }
185
}
186

187
export { notify }
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc