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

mozilla / blurts-server / #13117

pending completion
#13117

push

circleci

Vinnl
Pretend HTMLElement.shadowRoot is always set

We generally only tend to access it after having called
this.attachShadow({ mode: 'open' }), and the error message about it
potentially being undefined at every location we access
this.shadowRoot is more noisy than helpful.

See also
https://github.com/mozilla/blurts-server/pull/2959#discussion_r1154023113
and
https://github.com/mozilla/blurts-server/pull/2959#discussion_r1154960095

282 of 1629 branches covered (17.31%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

959 of 4374 relevant lines covered (21.93%)

1.83 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
  sendEmail
17
} from '../utils/email.js'
18
import { getMessage } from '../utils/fluent.js'
19
import {
20
  getAddressesAndLanguageForEmail,
21
  getBreachByName,
22
  loadBreachesIntoApp
23
} from '../utils/hibp.js'
24
import { UnauthorizedError, UserInputError } from '../utils/error.js'
25
import mozlog from '../utils/log.js'
26

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

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

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

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

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

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

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

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

94
  const doNotSendEmail = unsatisfiedConditions.length > 0
×
95

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

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

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

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

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

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

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

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

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

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

151
      if (!notifiedRecipients.includes(breachedEmail)) {
×
152
        const data = {
×
153
          breachData: breachAlert,
154
          breachLogos: req.app.locals.breachLogoMap,
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
          utmCampaign: utmCampaignId
163
        }
164

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

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

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

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

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

186
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