Download and show favicons for breached companies

This commit is contained in:
Vincent 2023-02-22 17:11:11 +01:00 коммит произвёл Vincent
Родитель 7df1a763b7
Коммит 370e541b44
6 изменённых файлов: 91 добавлений и 4 удалений

1
.gitignore поставляемый
Просмотреть файл

@ -17,3 +17,4 @@ dist
**/playwright/.cache/ **/playwright/.cache/
**/storageState.json **/storageState.json
state.json state.json
src/client/images/logo_cache/

Просмотреть файл

@ -101,7 +101,7 @@
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
overflow: hidden; overflow: hidden;
align-items: baseline; align-items: center;
gap: var(--gap); gap: var(--gap);
} }
@ -215,6 +215,25 @@
display: none; display: none;
} }
.breach-row .breach-company {
display: flex;
align-items: center;
gap: var(--padding-sm);
text-decoration: none;
}
.breach-row a.breach-company:hover {
text-decoration: underline;
}
.breach-row .breach-company .breach-logo {
height: 1.5rem;
}
.breach-row .breach-company span.breach-logo {
width: 1.5rem;
border-radius: 1.5rem;
}
.breaches-table details > article { .breaches-table details > article {
padding: var(--padding-md); padding: var(--padding-md);
border-top: 1px solid var(--gray-10); border-top: 1px solid var(--gray-10);

Просмотреть файл

@ -0,0 +1,12 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_718_17887)">
<path d="M12.2925 12.0802L13.9325 9.75023C14.0025 9.64022 14.1525 9.61023 14.2625 9.68023L15.7625 10.5802C16.1525 10.8102 16.6525 10.7502 16.9825 10.4302C17.0125 10.3902 17.0525 10.3502 17.0825 10.3002L19.7625 6.56023C20.0925 6.11023 20.0025 5.49023 19.5625 5.15023C19.1125 4.81023 18.4925 4.90023 18.1525 5.34023C18.1425 5.35023 18.1325 5.36023 18.1225 5.37023L16.1025 8.17023C16.0225 8.27023 15.8825 8.30022 15.7625 8.23022L14.2325 7.32023C13.7825 7.05023 13.2025 7.17023 12.8925 7.59023L10.5825 10.8502H10.5815C10.2615 11.3002 10.3615 11.9202 10.8115 12.2402C11.2615 12.5502 11.8815 12.4502 12.2015 12.0002L12.2925 12.0802Z" fill="#321C64"/>
<path d="M24.0001 18.5005V4.41047C23.9901 3.87047 23.7801 3.37047 23.4101 2.99047L20.9901 0.570469C20.6101 0.190469 20.1001 -0.0195312 19.5701 -0.0195312H7.98012C6.87012 -0.0195312 5.98012 0.870469 5.98012 1.98047V9.79047C5.97012 9.92047 6.08012 10.0305 6.21012 10.0405V10.0395C6.69012 10.0595 7.18012 10.1295 7.65012 10.2495C7.78012 10.2895 7.92012 10.2095 7.96012 10.0795C7.96012 10.0495 7.97012 10.0295 7.97012 9.99947V2.45047C7.97012 2.17047 8.19012 1.95047 8.47012 1.95047H19.3501C19.4801 1.94047 19.6101 2.00047 19.7001 2.09047L21.8201 4.21047C21.9101 4.30047 21.9601 4.43047 21.9601 4.56047V17.9305C21.9601 18.2005 21.7301 18.4305 21.4601 18.4305H13.1101C12.9701 18.4305 12.8601 18.5405 12.8601 18.6805C12.8601 18.7205 12.8601 18.7505 12.8801 18.7905L13.6601 20.2905C13.7001 20.3705 13.7801 20.4205 13.8801 20.4205H21.9301C23.0301 20.4205 23.9301 19.5205 23.9301 18.4205L24.0001 18.5005Z" fill="#321C64"/>
<path d="M6.50036 11.5008C5.96036 11.4908 5.46036 11.7908 5.22036 12.2708L0.160362 21.8908C-0.079638 22.3308 -0.059638 22.8708 0.200362 23.2908C0.460362 23.7108 0.930362 23.9708 1.43036 23.9808H11.5414C12.0414 23.9708 12.5014 23.7108 12.7714 23.2908C13.0314 22.8608 13.0514 22.3208 12.8114 21.8808L7.75136 12.2468C7.50136 11.7668 7.00136 11.4668 6.47136 11.4668L6.50036 11.5008ZM5.74036 16.0008C5.74036 15.5808 6.07036 15.2508 6.49036 15.2508C6.90036 15.2508 7.24036 15.5808 7.24036 16.0008V19.0008C7.24036 19.4108 6.90036 19.7508 6.49036 19.7508C6.07036 19.7508 5.74036 19.4108 5.74036 19.0008V16.0008ZM6.49036 22.2508C5.93036 22.2508 5.49036 21.8008 5.49036 21.2508C5.49036 20.6908 5.93036 20.2508 6.49036 20.2508C7.04036 20.2508 7.49036 20.6908 7.49036 21.2508C7.49036 21.8008 7.04036 22.2408 6.49036 22.2508Z" fill="#321C64"/>
</g>
<defs>
<clipPath id="clip0_718_17887">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

После

Ширина:  |  Высота:  |  Размер: 2.6 KiB

Просмотреть файл

@ -2,6 +2,10 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { access } from 'node:fs/promises'
import { constants } from 'node:fs'
import { mainLayout } from '../views/mainLayout.js' import { mainLayout } from '../views/mainLayout.js'
import { breaches } from '../views/partials/breaches.js' import { breaches } from '../views/partials/breaches.js'
import { setBreachResolution, updateBreachStats } from '../db/tables/subscribers.js' import { setBreachResolution, updateBreachStats } from '../db/tables/subscribers.js'
@ -22,6 +26,7 @@ async function breachesPage (req, res) {
const data = { const data = {
breachesData, breachesData,
breachLogos: await getBreachLogos(req.app.locals.breaches),
emailVerifiedCount, emailVerifiedCount,
emailTotalCount, emailTotalCount,
selectedEmailIndex, selectedEmailIndex,
@ -34,6 +39,21 @@ async function breachesPage (req, res) {
res.send(mainLayout(data)) res.send(mainLayout(data))
} }
async function getBreachLogos (breaches) {
const breachLogos = new Map()
await Promise.all(breaches.map(async breach => {
const logoPath = resolve(dirname(fileURLToPath(import.meta.url)), '../client/images/logo_cache/', breach.Domain + '.ico')
try {
await access(logoPath, constants.F_OK)
breachLogos.set(breach.Domain, `/images/logo_cache/${breach.Domain}.ico`)
} catch {
// Do nothing; we don't have a cached version of the logo,
// so we don't add it to the Map.
}
}))
return breachLogos
}
/** /**
* Get breaches from the database and return a JSON object * Get breaches from the database and return a JSON object
* TODO: Takes in additional query parameters: * TODO: Takes in additional query parameters:

Просмотреть файл

@ -2,10 +2,15 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { get } from 'node:https'
import { createWriteStream } from 'node:fs'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import mozlog from './log.js' import mozlog from './log.js'
import AppConstants from '../app-constants.js' import AppConstants from '../app-constants.js'
import { fluentError } from './fluent.js' import { fluentError } from './fluent.js'
import { getAllBreaches, upsertBreaches } from '../db/tables/breaches.js' import { getAllBreaches, upsertBreaches } from '../db/tables/breaches.js'
import { mkdir } from 'node:fs/promises'
const { HIBP_THROTTLE_MAX_TRIES, HIBP_THROTTLE_DELAY, HIBP_API_ROOT, HIBP_KANON_API_ROOT, HIBP_KANON_API_TOKEN } = AppConstants const { HIBP_THROTTLE_MAX_TRIES, HIBP_THROTTLE_DELAY, HIBP_API_ROOT, HIBP_KANON_API_ROOT, HIBP_KANON_API_TOKEN } = AppConstants
// TODO: fix hardcode // TODO: fix hardcode
@ -141,6 +146,7 @@ async function loadBreachesIntoApp (app) {
// sync the "breaches" table with the latest from HIBP // sync the "breaches" table with the latest from HIBP
await upsertBreaches(breaches) await upsertBreaches(breaches)
} }
downloadBreachIcons(breaches)
app.locals.breaches = breaches app.locals.breaches = breaches
app.locals.breachesLoadedDateTime = Date.now() app.locals.breachesLoadedDateTime = Date.now()
} catch (error) { } catch (error) {
@ -149,6 +155,29 @@ async function loadBreachesIntoApp (app) {
log.info('done-loading-breaches', 'great success 👍') log.info('done-loading-breaches', 'great success 👍')
} }
async function downloadBreachIcons (breaches) {
const breachDomains = breaches.map(breach => breach.Domain)
const logoFolder = resolve(dirname(fileURLToPath(import.meta.url)), '../client/images/logo_cache/')
try {
await mkdir(logoFolder)
} catch {
// Do nothing; if the directory already exists, that's fine.
}
breachDomains.forEach(breachDomain => {
get(`https://icons.duckduckgo.com/ip3/${breachDomain}.ico`, (response) => {
if (response.statusCode !== 200) {
return
}
const file = createWriteStream(resolve(logoFolder, breachDomain.toLowerCase() + '.ico'))
response.pipe(file)
file.on('finish', () => {
file.close()
})
})
})
}
/** /**
* Get addresses and language from either subscribers or email_addresses fields: * Get addresses and language from either subscribers or email_addresses fields:
* *

Просмотреть файл

@ -12,7 +12,7 @@ function createEmailOptions (data, selectedEmailIndex) {
return optionElements.join('') return optionElements.join('')
} }
function createBreachRows (data) { function createBreachRows (data, logos) {
const locale = getLocale() const locale = getLocale()
const shortDate = new Intl.DateTimeFormat(locale, { year: 'numeric', month: '2-digit', day: '2-digit', timeZone: 'UTC' }) const shortDate = new Intl.DateTimeFormat(locale, { year: 'numeric', month: '2-digit', day: '2-digit', timeZone: 'UTC' })
const shortList = new Intl.ListFormat(locale, { style: 'narrow' }) const shortList = new Intl.ListFormat(locale, { style: 'narrow' })
@ -31,11 +31,17 @@ function createBreachRows (data) {
addedDate: longDate.format(addedDate), addedDate: longDate.format(addedDate),
dataClasses: longList.format(dataClassesTranslated) dataClasses: longList.format(dataClassesTranslated)
}) })
const logoPath = logos.get(breach.Domain) ?? '/images/fallback-logo.svg'
return ` return `
<details class='breach-row' data-status=${status} data-email=${account.email} data-classes='${dataClassesTranslated}' ${isHidden ? 'hidden' : ''}> <details class='breach-row' data-status=${status} data-email=${account.email} data-classes='${dataClassesTranslated}' ${isHidden ? 'hidden' : ''}>
<summary> <summary>
<span>${breach.Title}</span> <span>
<a href='https://${breach.Domain}' target='_blank' class='breach-company'>
<img src='${logoPath}' alt='' class='breach-logo' height='32' />
${breach.Title}
</a>
</span>
<span>${shortList.format(dataClassesTranslated)}</span> <span>${shortList.format(dataClassesTranslated)}</span>
<span> <span>
<span class='resolution-badge is-resolved'>${getMessage('column-status-badge-resolved')}</span> <span class='resolution-badge is-resolved'>${getMessage('column-status-badge-resolved')}</span>
@ -107,7 +113,7 @@ export const breaches = data => `
} }
<span>${getMessage('column-detected')}</span> <span>${getMessage('column-detected')}</span>
</header> </header>
${createBreachRows(data.breachesData)} ${createBreachRows(data.breachesData, data.breachLogos)}
<template class='no-breaches'> <template class='no-breaches'>
<div class="zero-state no-breaches-message"> <div class="zero-state no-breaches-message">
<img src='/images/breaches-none.svg' alt='' width="136" height="102" /> <img src='/images/breaches-none.svg' alt='' width="136" height="102" />