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

mozilla / blurts-server / #12632

pending completion
#12632

push

circleci

web-flow
Merge pull request #2854 from mozilla/MNTOR-741

MNTOR-741

282 of 1416 branches covered (19.92%)

Branch coverage included in aggregate %.

107 of 107 new or added lines in 9 files covered. (100.0%)

959 of 3912 relevant lines covered (24.51%)

2.04 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/emails/email-2022.js'
11
import { breachAlertEmailPartial } from '../views/emails/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
 * @param req
41
 * @param res
42
 */
43
async function notify (req, res) {
44
  if (!req.token || req.token !== AppConstants.HIBP_NOTIFY_TOKEN) {
×
45
    const errorMessage = 'HIBP notify endpoint requires valid authorization token.'
×
46
    throw new UnauthorizedError(errorMessage)
×
47
  }
48
  if (!['breachName', 'hashPrefix', 'hashSuffixes'].every(req.body?.hasOwnProperty, req.body)) {
×
49
    throw new UserInputError('HIBP breach notification: requires breachName, hashPrefix, and hashSuffixes.')
×
50
  }
51

52
  const { breachName, hashPrefix, hashSuffixes } = req.body
×
53

54
  let breachAlert = getBreachByName(req.app.locals.breaches, breachName)
×
55

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

67
  const { IsVerified, Domain, IsFabricated, IsSpamList } = breachAlert
×
68

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

91
  const unsatisfiedConditions = emailDeliveryConditions.filter(condition => (
×
92
    condition.condition
×
93
  ))
94

95
  const doNotSendEmail = unsatisfiedConditions.length > 0
×
96

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

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

108
    return res.status(200).json({
×
109
      info: 'Breach loaded into database. Subscribers not notified.'
110
    })
111
  }
112

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

120
    log.info(EmailTemplateType.Notification, {
×
121
      breachAlertName: breachAlert.Name,
122
      length: recipients.length
123
    })
124

125
    const utmCampaignId = 'breach-alert'
×
126
    const notifiedRecipients = []
×
127

128
    for (const recipient of recipients) {
×
129
      log.info('notify', { recipient })
×
130

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

141
      const requestedLanguage = signupLanguage
×
142
        ? acceptedLanguages(signupLanguage)
143
        : []
144

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

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

169
        const emailTemplate = getTemplate(data, breachAlertEmailPartial)
×
170
        const subject = getMessage('breach-alert-subject')
×
171

172
        await sendEmail(data.recipientEmail, subject, emailTemplate)
×
173

174
        notifiedRecipients.push(breachedEmail)
×
175
      }
176
    }
177

178
    log.info('notified', { length: notifiedRecipients.length })
×
179

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

190
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