Add alerts, fix dark mode

main
Elbert Alias 5 years ago
parent d9c504a19c
commit cb1668fb04

@ -15,6 +15,7 @@
"termsAccept": { "message": "Acceptar" },
"termsContent": { "message": "Aquesta extensió envia informació anònima sobre els llocs web que visiteu, inclosos el nom de domini i les tecnologies identificades a <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. Això pot desactivar-se a Opcions." },
"privacyPolicy": { "message": "Política de privadesa" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Taulers de missatgeria" },
"categoryName3": { "message": "Gestor de bases de dades" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Immer Icon anzeigen" },
"termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Nachrichten Board" },
"categoryName3": { "message": "Datenbankverwaltung" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Always show icon" },
"termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Διαδικτυακό Φόρουμ" },
"categoryName3": { "message": "Διαχειριστής Βάσης Δεδομένων" },

@ -15,6 +15,7 @@
"termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Message boards" },
"categoryName3": { "message": "Database managers" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Always show icon" },
"termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "Gestor de Contenido" },
"categoryName2": { "message": "Foro" },
"categoryName3": { "message": "Gestor de Bases de Datos" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "همیشه نماد را نشان بده" },
"termsAccept": { "message": "قبول" },
"termsContent": { "message": "این افزونه اطلاعات وب‌سایت‌های بازدید شده توسط شما را به صورت ناشناس ارسال می‌کند، مانند آدرس سایت و تکنولوژی‌های استفاده شده در آن سایت را ارسال می‌کند. اطلاعات بیشتر در <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. شما می‌توانید این افزونه را غیرفعال کنید." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "سیستم مدیریت محتوا" },
"categoryName2": { "message": "انجمن پیام" },
"categoryName3": { "message": "مدیریت پایگاه داده" },

@ -14,6 +14,8 @@
"categoryPin": { "message": " Toujours afficher l'icône" },
"termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Forum" },
"categoryName3": { "message": "Gestionnaire de base de données" },

@ -15,6 +15,7 @@
"termsAccept": { "message": "Aceptar" },
"termsContent": { "message": "Esta extensión envía anonimamente información acerca das webs que visitas, incluindo dominio e aplicativos identificados, a <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. Isto pode ser desactivado nas preferencias." },
"privacyPolicy": { "message": "Política de privacidade" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Taboleiro de mensaxes" },
"categoryName3": { "message": "Xestor de base de datos" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Always show icon" },
"termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Διαδικτυακό Φόρουμ" },
"categoryName3": { "message": "Διαχειριστής Βάσης Δεδομένων" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Always show icon" },
"termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "Sistem Pengelola Konten" },
"categoryName2": { "message": "Papan Pesan" },
"categoryName3": { "message": "Pengelola Basis Data" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Always show icon" },
"termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Forum" },
"categoryName3": { "message": "Gestore di Database" },

@ -15,6 +15,7 @@
"termsAccept": { "message": "受諾する" },
"termsContent": { "message": "この拡張機能は、ドメイン名や特定された技術など、アクセスしたWebサイトに関する匿名情報を<a href='https://www.wappalyzer.com'>wappalyzer.com</a>に送信します。これは設定で無効にできます。" },
"privacyPolicy": { "message": "プライバシーポリシー" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" },
"categoryName2": { "message": "メッセージボード" },
"categoryName3": { "message": "データベースマネージャー" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Zawsze pokazuj tą ikonę" },
"termsAccept": { "message": "Akceptuj" },
"termsContent": { "message": "To rozszerzenie wysyła anonimowe informacje o stronach, które odwiedzasz, uwzględniając nazwy domen i zidentyfikowane technologie do <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. Opcja może zostać wyłączona w ustawieniach." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "System zarządzania treścią" },
"categoryName2": { "message": "Forum" },
"categoryName3": { "message": "Menedżer baz danych" },

@ -15,6 +15,7 @@
"termsAccept": { "message": "Aceitar" },
"termsContent": { "message": "Esta extensão envia informações anónimas sobre os sites que visitas, incluindo o nome de domínio e as tecnologias identificadas, para o <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. Isso pode ser desativado nas configurações." },
"privacyPolicy": { "message": "Políticas de Privacidade" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Fórum" },
"categoryName3": { "message": "Gestor de Base de Dados" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Sempre mostrar ícone" },
"termsAccept": { "message": "Aceitar" },
"termsContent": { "message": "Esta extensão envia informações anônimas sobre os sites que você visita, incluindo domínio e tecnologias identificadas para <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. Este comportamento pode ser desativado nas configurações." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Fórum" },
"categoryName3": { "message": "Gestão de Banco de Dados" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Afișează icon tot timpul" },
"termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Forum de discuții" },
"categoryName3": { "message": "Manager baze de date" },

@ -67,6 +67,8 @@
"categoryPin": { "message": "Always show icon" },
"termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName54": { "message": "SEO" },
"categoryName55": { "message": "Бухгалтерский учёт" },
"categoryName56": { "message": "Криптомайнер" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Always show icon" },
"termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Message Board" },
"categoryName3": { "message": "Správca databáz" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Her zaman bu kategorinin ikonunu kullan" },
"termsAccept": { "message": "Kabul Ediyorum" },
"termsContent": { "message": "Bu eklenti, ziyaret ettiğiniz web site bilgilerini, alan adları ve tespit edilen teknolojiler ile beraber anonim olarak <a href='https://www.wappalyzer.com'>wappalyzer.com</a>'a gönderir. Bunu, eklenti ayarlarından değiştirebilirsiniz." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "İçerik Yönetim Sistemi" },
"categoryName2": { "message": "Mesaj Tahtası" },
"categoryName3": { "message": "Veritabanı Yöneticisi" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Always show icon" },
"termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Форум" },
"categoryName3": { "message": "Менеджер БД" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Always show icon" },
"termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS (KBT)" },
"categoryName2": { "message": "Forum" },
"categoryName3": { "message": "MB boshqaruvi" },

@ -15,6 +15,7 @@
"termsAccept": { "message": "接受" },
"termsContent": { "message": "此扩展程序发送关于您访问的网站的匿名信息至 <a href='https://www.wappalyzer.com'>wappalyzer.com</a>,包含域名和检测到的技术。这可以在设置中禁用。" },
"privacyPolicy": { "message": "隐私政策" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "内容管理系统CMS" },
"categoryName2": { "message": "消息板" },
"categoryName3": { "message": "数据库管理器" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "永遠顯示圖示" },
"termsAccept": { "message": "接受" },
"termsContent": { "message": "這個擴充功能將你所造訪網站的網域名稱和識別到的技術等資訊,匿名傳送至 <a href='https://www.wappalyzer.com'>wappalyzer.com</a>。你可以在選項中停用。" },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "內容管理系統CMS" },
"categoryName2": { "message": "留言板/討論區" },
"categoryName3": { "message": "資料庫管理" },

@ -31,6 +31,24 @@ body {
display: none;
}
.footer {
align-items: center;
border-top: 1px solid #dbdbdb;
height: 3rem;
display: flex;
padding: 0 1.5rem;
}
.footer__link {
color: #4608ad;
text-decoration: none;
}
.footer__link:hover, .footer__link:active {
color: #4608ad;
text-decoration: underline;
}
.container {
min-height: 5rem;
padding: 1rem 1.5rem 0rem 1.5rem;
@ -202,13 +220,14 @@ body {
margin-top: 1rem;
}
@media (prefers-color-scheme: dark) {
/* Add alternative color palette for Dark mode theme. */
body.theme-mode-sync {
background: linear-gradient(160deg, #32067c, #150233);
}
.theme-mode-sync .header {
border-bottom: 1px solid #000;
border-bottom: 1px solid rgba(255, 255, 255, .2);
}
.theme-mode-sync .header__logo--dark {
@ -219,12 +238,28 @@ body.theme-mode-sync {
display: none;
}
.theme-mode-sync .footer {
border-top: 1px solid rgba(255, 255, 255, .2);
}
.theme-mode-sync .footer__link {
color: rgba(255, 255, 255, .8);
}
.theme-mode-sync .footer__link:hover, .theme-mode-sync .footer__link:active {
color: rgba(255, 255, 255, .8);
}
.theme-mode-sync .container {
color: white;
}
.theme-mode-sync .detected__category-link, .theme-mode-sync .detected__app {
color: white;
.theme-mode-sync .detected__category-link {
color: #fff;
}
.theme-mode-sync .detected__app {
color: rgba(255, 255, 255, .8);
}
.theme-mode-sync .detected__category-link:hover {
@ -249,3 +284,4 @@ body.theme-mode-sync {
.theme-mode-sync .terms__privacy {
color: white;
}
}

@ -14,8 +14,8 @@
<body>
<div class="header">
<a href="https://www.wappalyzer.com/" class="header__link" target="_blank">
<img class="header__logo header__logo--light" src="../images/logo-purple.svg">
<img class="header__logo header__logo--dark" src="../images/logo-white.svg">
<img alt="" class="header__logo header__logo--light" src="../images/logo-purple.svg">
<img alt="" class="header__logo header__logo--dark" src="../images/logo-white.svg">
</a>
</div>
@ -24,11 +24,15 @@
<div class="terms">
<div class="terms__content" data-i18n="termsContent"></div>
<button class="terms__accept" data-i18n="termsAccept"></button>
<button class="terms__accept" data-i18n="termsAccept" />
<a class="terms__privacy" href="https://www.wappalyzer.com/privacy" data-i18n="privacyPolicy"></a>
</div>
</div>
</div>
<div class="footer">
<a class="footer__link" href="https://www.wappalyzer.com/alerts/manage" data-i18n="createAlert"></a>
</div>
</body>
</html>

@ -10,29 +10,29 @@
/** global: fetch */
/** global: Wappalyzer */
const wappalyzer = new Wappalyzer();
const wappalyzer = new Wappalyzer()
const tabCache = {};
const robotsTxtQueue = {};
const tabCache = {}
const robotsTxtQueue = {}
let categoryOrder = [];
let categoryOrder = []
browser.tabs.onRemoved.addListener((tabId) => {
tabCache[tabId] = null;
});
tabCache[tabId] = null
})
function userAgent() {
const url = chrome.extension.getURL('/');
const url = chrome.extension.getURL('/')
if (url.match(/^moz-/)) {
return 'firefox';
return 'firefox'
}
if (url.match(/^ms-browser-/)) {
return 'edge';
return 'edge'
}
return 'chrome';
return 'chrome'
}
/**
@ -40,22 +40,22 @@ function userAgent() {
*/
function getOption(name, defaultValue = null) {
return new Promise(async (resolve, reject) => {
let value = defaultValue;
let value = defaultValue
try {
const option = await browser.storage.local.get(name);
const option = await browser.storage.local.get(name)
if (option[name] !== undefined) {
value = option[name];
value = option[name]
}
} catch (error) {
wappalyzer.log(error.message, 'driver', 'error');
wappalyzer.log(error.message, 'driver', 'error')
return reject(error.message);
return reject(error.message)
}
return resolve(value);
});
return resolve(value)
})
}
/**
@ -64,15 +64,15 @@ function getOption(name, defaultValue = null) {
function setOption(name, value) {
return new Promise(async (resolve, reject) => {
try {
await browser.storage.local.set({ [name]: value });
await browser.storage.local.set({ [name]: value })
} catch (error) {
wappalyzer.log(error.message, 'driver', 'error');
wappalyzer.log(error.message, 'driver', 'error')
return reject(error.message);
return reject(error.message)
}
return resolve();
});
return resolve()
})
}
/**
@ -81,8 +81,8 @@ function setOption(name, value) {
function openTab(args) {
browser.tabs.create({
url: args.url,
active: args.background === undefined || !args.background,
});
active: args.background === undefined || !args.background
})
}
/**
@ -92,123 +92,138 @@ async function post(url, body) {
try {
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify(body),
});
body: JSON.stringify(body)
})
wappalyzer.log(`POST ${url}: ${response.status}`, 'driver');
wappalyzer.log(`POST ${url}: ${response.status}`, 'driver')
} catch (error) {
wappalyzer.log(`POST ${url}: ${error}`, 'driver', 'error');
wappalyzer.log(`POST ${url}: ${error}`, 'driver', 'error')
}
}
// Capture response headers
browser.webRequest.onCompleted.addListener(async (request) => {
const headers = {};
browser.webRequest.onCompleted.addListener(
async (request) => {
const headers = {}
if (request.responseHeaders) {
const url = wappalyzer.parseUrl(request.url);
const url = wappalyzer.parseUrl(request.url)
let tab;
let tab
try {
[tab] = await browser.tabs.query({ url: [url.href] });
;[tab] = await browser.tabs.query({ url: [url.href] })
} catch (error) {
wappalyzer.log(error, 'driver', 'error');
wappalyzer.log(error, 'driver', 'error')
}
if (tab) {
request.responseHeaders.forEach((header) => {
const name = header.name.toLowerCase();
const name = header.name.toLowerCase()
headers[name] = headers[name] || [];
headers[name] = headers[name] || []
headers[name].push((header.value || header.binaryValue || '').toString());
});
headers[name].push(
(header.value || header.binaryValue || '').toString()
)
})
if (headers['content-type'] && /\/x?html/.test(headers['content-type'][0])) {
wappalyzer.analyze(url, { headers }, { tab });
if (
headers['content-type'] &&
/\/x?html/.test(headers['content-type'][0])
) {
wappalyzer.analyze(url, { headers }, { tab })
}
}
}
}, { urls: ['http://*/*', 'https://*/*'], types: ['main_frame'] }, ['responseHeaders']);
},
{ urls: ['http://*/*', 'https://*/*'], types: ['main_frame'] },
['responseHeaders']
)
browser.runtime.onConnect.addListener((port) => {
port.onMessage.addListener(async (message) => {
if (message.id === undefined) {
return;
return
}
if (message.id !== 'log') {
wappalyzer.log(`Message from ${port.name}: ${message.id}`, 'driver');
wappalyzer.log(`Message from ${port.name}: ${message.id}`, 'driver')
}
const pinnedCategory = await getOption('pinnedCategory');
const pinnedCategory = await getOption('pinnedCategory')
const url = wappalyzer.parseUrl(port.sender.tab ? port.sender.tab.url : '');
const url = wappalyzer.parseUrl(port.sender.tab ? port.sender.tab.url : '')
const cookies = await browser.cookies.getAll({ domain: `.${url.hostname}`, firstPartyDomain: null });
const cookies = await browser.cookies.getAll({
domain: `.${url.hostname}`,
firstPartyDomain: null
})
let response;
let response
switch (message.id) {
case 'log':
wappalyzer.log(message.subject, message.source);
wappalyzer.log(message.subject, message.source)
break;
break
case 'init':
wappalyzer.analyze(url, { cookies }, { tab: port.sender.tab });
wappalyzer.analyze(url, { cookies }, { tab: port.sender.tab })
break;
break
case 'analyze':
if (message.subject.html) {
browser.i18n.detectLanguage(message.subject.html)
browser.i18n
.detectLanguage(message.subject.html)
.then(({ languages }) => {
const language = languages
.filter(({ percentage }) => percentage >= 75)
.map(({ language: lang }) => lang)[0];
.map(({ language: lang }) => lang)[0]
message.subject.language = language;
message.subject.language = language
wappalyzer.analyze(url, message.subject, { tab: port.sender.tab });
});
wappalyzer.analyze(url, message.subject, { tab: port.sender.tab })
})
} else {
wappalyzer.analyze(url, message.subject, { tab: port.sender.tab });
wappalyzer.analyze(url, message.subject, { tab: port.sender.tab })
}
await setOption('hostnameCache', wappalyzer.hostnameCache);
await setOption('hostnameCache', wappalyzer.hostnameCache)
break;
break
case 'ad_log':
wappalyzer.cacheDetectedAds(message.subject);
wappalyzer.cacheDetectedAds(message.subject)
break;
break
case 'get_apps':
response = {
tabCache: tabCache[message.tab.id],
apps: wappalyzer.apps,
categories: wappalyzer.categories,
pinnedCategory,
termsAccepted: userAgent() === 'chrome' || await getOption('termsAccepted', false),
};
termsAccepted:
userAgent() === 'chrome' ||
(await getOption('termsAccepted', false))
}
break;
break
case 'set_option':
await setOption(message.key, message.value);
await setOption(message.key, message.value)
break;
break
case 'get_js_patterns':
response = {
patterns: wappalyzer.jsPatterns,
};
patterns: wappalyzer.jsPatterns
}
break;
break
case 'update_theme_mode':
// Sync theme mode to popup.
response = {
themeMode: await getOption('themeMode', false),
};
themeMode: await getOption('themeMode', false)
}
break;
break
default:
// Do nothing
}
@ -216,197 +231,207 @@ browser.runtime.onConnect.addListener((port) => {
if (response) {
port.postMessage({
id: message.id,
response,
});
response
})
}
});
});
})
})
wappalyzer.driver.document = document;
wappalyzer.driver.document = document
/**
* Log messages to console
*/
wappalyzer.driver.log = (message, source, type) => {
const log = ['warn', 'error'].indexOf(type) !== -1 ? type : 'log';
const log = ['warn', 'error'].includes(type) ? type : 'log'
console[log](`[wappalyzer ${type}]`, `[${source}]`, message); // eslint-disable-line no-console
};
console[log](`[wappalyzer ${type}]`, `[${source}]`, message) // eslint-disable-line no-console
}
/**
* Display apps
*/
wappalyzer.driver.displayApps = async (detected, meta, context) => {
const { tab } = context;
const { tab } = context
if (tab === undefined) {
return;
return
}
tabCache[tab.id] = tabCache[tab.id] || {
detected: [],
};
detected: []
}
tabCache[tab.id].detected = detected;
tabCache[tab.id].detected = detected
const pinnedCategory = await getOption('pinnedCategory');
const dynamicIcon = await getOption('dynamicIcon', true);
const pinnedCategory = await getOption('pinnedCategory')
const dynamicIcon = await getOption('dynamicIcon', true)
let found = false;
let found = false
// Find the main application to display
[pinnedCategory].concat(categoryOrder).forEach((match) => {
;[pinnedCategory].concat(categoryOrder).forEach((match) => {
Object.keys(detected).forEach((appName) => {
const app = detected[appName];
const app = detected[appName]
app.props.cats.forEach((category) => {
if (category === match && !found) {
let icon = app.props.icon && dynamicIcon ? app.props.icon : 'default.svg';
let icon =
app.props.icon && dynamicIcon ? app.props.icon : 'default.svg'
if (/\.svg$/i.test(icon)) {
icon = `converted/${icon.replace(/\.svg$/, '.png')}`;
icon = `converted/${icon.replace(/\.svg$/, '.png')}`
}
try {
browser.pageAction.setIcon({
tabId: tab.id,
path: `../images/icons/${icon}`,
});
path: `../images/icons/${icon}`
})
} catch (e) {
// Firefox for Android does not support setIcon see https://bugzilla.mozilla.org/show_bug.cgi?id=1331746
}
found = true;
found = true
}
});
});
});
})
})
})
browser.pageAction.show(tab.id);
};
browser.pageAction.show(tab.id)
}
/**
* Fetch and cache robots.txt for host
*/
wappalyzer.driver.getRobotsTxt = async (host, secure = false) => {
if (robotsTxtQueue[host]) {
return robotsTxtQueue[host];
return robotsTxtQueue[host]
}
const tracking = await getOption('tracking', true);
const robotsTxtCache = await getOption('robotsTxtCache', {});
const tracking = await getOption('tracking', true)
const robotsTxtCache = await getOption('robotsTxtCache', {})
robotsTxtQueue[host] = new Promise(async (resolve) => {
if (!tracking) {
return resolve([]);
return resolve([])
}
if (host in robotsTxtCache) {
return resolve(robotsTxtCache[host]);
return resolve(robotsTxtCache[host])
}
const timeout = setTimeout(() => resolve([]), 3000);
const timeout = setTimeout(() => resolve([]), 3000)
let response;
let response
try {
response = await fetch(`http${secure ? 's' : ''}://${host}/robots.txt`, { redirect: 'follow', mode: 'no-cors' });
response = await fetch(`http${secure ? 's' : ''}://${host}/robots.txt`, {
redirect: 'follow',
mode: 'no-cors'
})
} catch (error) {
wappalyzer.log(error, 'driver', 'error');
wappalyzer.log(error, 'driver', 'error')
return resolve([]);
return resolve([])
}
clearTimeout(timeout);
clearTimeout(timeout)
const robotsTxt = response.ok ? await response.text() : '';
const robotsTxt = response.ok ? await response.text() : ''
robotsTxtCache[host] = Wappalyzer.parseRobotsTxt(robotsTxt);
robotsTxtCache[host] = Wappalyzer.parseRobotsTxt(robotsTxt)
await setOption('robotsTxtCache', robotsTxtCache);
await setOption('robotsTxtCache', robotsTxtCache)
delete robotsTxtQueue[host];
delete robotsTxtQueue[host]
return resolve(robotsTxtCache[host]);
});
return resolve(robotsTxtCache[host])
})
return robotsTxtQueue[host];
};
return robotsTxtQueue[host]
}
/**
* Anonymously track detected applications for research purposes
*/
wappalyzer.driver.ping = async (hostnameCache = {}, adCache = []) => {
const tracking = await getOption('tracking', true);
const termsAccepted = userAgent() === 'chrome' || await getOption('termsAccepted', false);
const tracking = await getOption('tracking', true)
const termsAccepted =
userAgent() === 'chrome' || (await getOption('termsAccepted', false))
if (tracking && termsAccepted) {
if (Object.keys(hostnameCache).length) {
post('https://api.wappalyzer.com/ping/v1/', hostnameCache);
post('https://api.wappalyzer.com/ping/v1/', hostnameCache)
}
if (adCache.length) {
post('https://ad.wappalyzer.com/log/wp/', adCache);
post('https://ad.wappalyzer.com/log/wp/', adCache)
}
await setOption('robotsTxtCache', {});
await setOption('robotsTxtCache', {})
}
}
};
// Init
(async () => {
;(async () => {
// Technologies
try {
const response = await fetch('../apps.json');
const json = await response.json();
const response = await fetch('../apps.json')
const json = await response.json()
wappalyzer.apps = json.apps;
wappalyzer.categories = json.categories;
wappalyzer.apps = json.apps
wappalyzer.categories = json.categories
} catch (error) {
wappalyzer.log(`GET apps.json: ${error.message}`, 'driver', 'error');
wappalyzer.log(`GET apps.json: ${error.message}`, 'driver', 'error')
}
wappalyzer.parseJsPatterns();
wappalyzer.parseJsPatterns()
categoryOrder = Object.keys(wappalyzer.categories)
.map(categoryId => parseInt(categoryId, 10))
.sort((a, b) => wappalyzer.categories[a].priority - wappalyzer.categories[b].priority);
.map((categoryId) => parseInt(categoryId, 10))
.sort(
(a, b) =>
wappalyzer.categories[a].priority - wappalyzer.categories[b].priority
)
// Version check
const { version } = browser.runtime.getManifest();
const previousVersion = await getOption('version');
const upgradeMessage = await getOption('upgradeMessage', true);
const { version } = browser.runtime.getManifest()
const previousVersion = await getOption('version')
const upgradeMessage = await getOption('upgradeMessage', true)
if (previousVersion === null) {
openTab({
url: `${wappalyzer.config.websiteURL}installed`,
});
url: `${wappalyzer.config.websiteURL}installed`
})
} else if (version !== previousVersion && upgradeMessage) {
openTab({
url: `${wappalyzer.config.websiteURL}upgraded?v${version}`,
background: true,
});
background: true
})
}
await setOption('version', version);
await setOption('version', version)
// Hostname cache
wappalyzer.hostnameCache = await getOption('hostnameCache', {});
wappalyzer.hostnameCache = await getOption('hostnameCache', {})
// Run content script on all tabs
try {
const tabs = await browser.tabs.query({ url: ['http://*/*', 'https://*/*'] });
const tabs = await browser.tabs.query({
url: ['http://*/*', 'https://*/*']
})
tabs.forEach(async (tab) => {
try {
await browser.tabs.executeScript(tab.id, {
file: '../js/content.js',
});
file: '../js/content.js'
})
} catch (error) {
//
}
});
})
} catch (error) {
wappalyzer.log(error, 'driver', 'error');
wappalyzer.log(error, 'driver', 'error')
}
})();
})()

@ -3,29 +3,29 @@
/* globals browser Wappalyzer */
/* eslint-env browser */
const wappalyzer = new Wappalyzer();
const wappalyzer = new Wappalyzer()
/**
* Get a value from localStorage
*/
function getOption(name, defaultValue = null) {
return new Promise(async (resolve, reject) => {
let value = defaultValue;
let value = defaultValue
try {
const option = await browser.storage.local.get(name);
const option = await browser.storage.local.get(name)
if (option[name] !== undefined) {
value = option[name];
value = option[name]
}
} catch (error) {
wappalyzer.log(error.message, 'driver', 'error');
wappalyzer.log(error.message, 'driver', 'error')
return reject(error.message);
return reject(error.message)
}
return resolve(value);
});
return resolve(value)
})
}
/**
@ -34,72 +34,76 @@ function getOption(name, defaultValue = null) {
function setOption(name, value) {
return new Promise(async (resolve, reject) => {
try {
await browser.storage.local.set({ [name]: value });
await browser.storage.local.set({ [name]: value })
} catch (error) {
wappalyzer.log(error.message, 'driver', 'error');
wappalyzer.log(error.message, 'driver', 'error')
return reject(error.message);
return reject(error.message)
}
return resolve();
});
return resolve()
})
}
document.addEventListener('DOMContentLoaded', async () => {
const nodes = document.querySelectorAll('[data-i18n]');
const nodes = document.querySelectorAll('[data-i18n]')
Array.prototype.forEach.call(nodes, (node) => {
node.childNodes[0].nodeValue = browser.i18n.getMessage(node.dataset.i18n);
});
node.childNodes[0].nodeValue = browser.i18n.getMessage(node.dataset.i18n)
})
document.querySelector('#github').addEventListener('click', () => {
window.open(wappalyzer.config.githubURL);
});
window.open(wappalyzer.config.githubURL)
})
document.querySelector('#twitter').addEventListener('click', () => {
window.open(wappalyzer.config.twitterURL);
});
window.open(wappalyzer.config.twitterURL)
})
document.querySelector('#wappalyzer').addEventListener('click', () => {
window.open(wappalyzer.config.websiteURL);
});
window.open(wappalyzer.config.websiteURL)
})
let el;
let value;
let el
let value
// Upgrade message
value = await getOption('upgradeMessage', true);
value = await getOption('upgradeMessage', true)
el = document.querySelector('#option-upgrade-message');
el = document.querySelector('#option-upgrade-message')
el.checked = value;
el.checked = value
el.addEventListener('change', e => setOption('upgradeMessage', e.target.checked));
el.addEventListener('change', (e) =>
setOption('upgradeMessage', e.target.checked)
)
// Dynamic icon
value = await getOption('dynamicIcon', true);
value = await getOption('dynamicIcon', true)
el = document.querySelector('#option-dynamic-icon');
el = document.querySelector('#option-dynamic-icon')
el.checked = value;
el.checked = value
el.addEventListener('change', e => setOption('dynamicIcon', e.target.checked));
el.addEventListener('change', (e) =>
setOption('dynamicIcon', e.target.checked)
)
// Tracking
value = await getOption('tracking', true);
value = await getOption('tracking', true)
el = document.querySelector('#option-tracking');
el = document.querySelector('#option-tracking')
el.checked = value;
el.checked = value
el.addEventListener('change', e => setOption('tracking', e.target.checked));
el.addEventListener('change', (e) => setOption('tracking', e.target.checked))
// Theme Mode
value = await getOption('themeMode', false);
value = await getOption('themeMode', false)
el = document.querySelector('#option-theme-mode');
el = document.querySelector('#option-theme-mode')
el.checked = value;
el.checked = value
el.addEventListener('change', e => setOption('themeMode', e.target.checked));
});
el.addEventListener('change', (e) => setOption('themeMode', e.target.checked))
})

@ -4,217 +4,267 @@
/** global: browser */
/** global: jsonToDOM */
let pinnedCategory = null;
let termsAccepted = false;
let pinnedCategory = null
let termsAccepted = false
const port = browser.runtime.connect({
name: 'popup.js',
});
name: 'popup.js'
})
function slugify(string) {
return string.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/--+/g, '-').replace(/(?:^-|-$)/, '');
return string
.toLowerCase()
.replace(/[^a-z0-9-]/g, '-')
.replace(/--+/g, '-')
.replace(/(?:^-|-$)/, '')
}
function i18n() {
const nodes = document.querySelectorAll('[data-i18n]');
const nodes = document.querySelectorAll('[data-i18n]')
Array.prototype.forEach.call(nodes, (node) => {
node.innerHTML = browser.i18n.getMessage(node.dataset.i18n);
});
node.innerHTML = browser.i18n.getMessage(node.dataset.i18n)
})
}
function replaceDom(domTemplate) {
const container = document.getElementsByClassName('container')[0];
const container = document.getElementsByClassName('container')[0]
while (container.firstChild) {
container.removeChild(container.firstChild);
container.removeChild(container.firstChild)
}
container.appendChild(jsonToDOM(domTemplate, document, {}));
container.appendChild(jsonToDOM(domTemplate, document, {}))
i18n();
i18n()
Array.from(document.querySelectorAll('.detected__category-pin-wrapper')).forEach((pin) => {
Array.from(
document.querySelectorAll('.detected__category-pin-wrapper')
).forEach((pin) => {
pin.addEventListener('click', () => {
const categoryId = parseInt(pin.dataset.categoryId, 10);
const categoryId = parseInt(pin.dataset.categoryId, 10)
if (categoryId === pinnedCategory) {
pin.className = 'detected__category-pin-wrapper';
pin.className = 'detected__category-pin-wrapper'
pinnedCategory = null;
pinnedCategory = null
} else {
const active = document.querySelector('.detected__category-pin-wrapper--active');
const active = document.querySelector(
'.detected__category-pin-wrapper--active'
)
if (active) {
active.className = 'detected__category-pin-wrapper';
active.className = 'detected__category-pin-wrapper'
}
pin.className = 'detected__category-pin-wrapper detected__category-pin-wrapper--active';
pin.className =
'detected__category-pin-wrapper detected__category-pin-wrapper--active'
pinnedCategory = categoryId;
pinnedCategory = categoryId
}
port.postMessage({
id: 'set_option',
key: 'pinnedCategory',
value: pinnedCategory,
});
});
});
value: pinnedCategory
})
})
})
}
function replaceDomWhenReady(dom) {
if (/complete|interactive|loaded/.test(document.readyState)) {
replaceDom(dom);
replaceDom(dom)
} else {
document.addEventListener('DOMContentLoaded', () => {
replaceDom(dom);
});
replaceDom(dom)
})
}
}
function appsToDomTemplate(response) {
let template = [];
let template = []
if (response.tabCache && Object.keys(response.tabCache.detected).length > 0) {
const categories = {};
const categories = {}
// Group apps by category
for (const appName in response.tabCache.detected) {
response.apps[appName].cats.forEach((cat) => {
categories[cat] = categories[cat] || { apps: [] };
categories[cat] = categories[cat] || { apps: [] }
categories[cat].apps[appName] = appName;
});
categories[cat].apps[appName] = appName
})
}
for (const cat in categories) {
const apps = [];
const apps = []
for (const appName in categories[cat].apps) {
const { confidence, version } = response.tabCache.detected[appName];
const { confidence, version } = response.tabCache.detected[appName]
apps.push(
[
'a', {
apps.push([
'a',
{
class: 'detected__app',
target: '_blank',
href: `https://www.wappalyzer.com/technologies/${slugify(appName)}`,
}, [
'img', {
class: 'detected__app-icon',
src: `../images/icons/${response.apps[appName].icon || 'default.svg'}`,
href: `https://www.wappalyzer.com/technologies/${slugify(appName)}`
},
], [
'span', {
class: 'detected__app-name',
[
'img',
{
class: 'detected__app-icon',
src: `../images/icons/${response.apps[appName].icon ||
'default.svg'}`
}
],
[
'span',
{
class: 'detected__app-name'
},
appName,
], version ? [
'span', {
class: 'detected__app-version',
appName
],
version
? [
'span',
{
class: 'detected__app-version'
},
version,
] : null, confidence < 100 ? [
'span', {
class: 'detected__app-confidence',
version
]
: null,
confidence < 100
? [
'span',
{
class: 'detected__app-confidence'
},
`${confidence}% sure`,
] : null,
],
);
`${confidence}% sure`
]
: null
])
}
template.push(
template.push([
'div',
{
class: 'detected__category'
},
[
'div', {
class: 'detected__category',
}, [
'div', {
class: 'detected__category-name',
}, [
'a', {
'div',
{
class: 'detected__category-name'
},
[
'a',
{
class: 'detected__category-link',
target: '_blank',
href: `https://www.wappalyzer.com/categories/${slugify(response.categories[cat].name)}`,
href: `https://www.wappalyzer.com/categories/${slugify(
response.categories[cat].name
)}`
},
browser.i18n.getMessage(`categoryName${cat}`),
], [
'span', {
class: `detected__category-pin-wrapper${pinnedCategory == cat ? ' detected__category-pin-wrapper--active' : ''}`,
browser.i18n.getMessage(`categoryName${cat}`)
],
[
'span',
{
class: `detected__category-pin-wrapper${
pinnedCategory == cat
? ' detected__category-pin-wrapper--active'
: ''
}`,
'data-category-id': cat,
title: browser.i18n.getMessage('categoryPin'),
}, [
'img', {
class: 'detected__category-pin detected__category-pin--active',
src: '../images/pin-active.svg',
},
], [
'img', {
class: 'detected__category-pin detected__category-pin--inactive',
src: '../images/pin.svg',
title: browser.i18n.getMessage('categoryPin')
},
[
'img',
{
class: 'detected__category-pin detected__category-pin--active',
src: '../images/pin-active.svg'
}
],
[
'img',
{
class:
'detected__category-pin detected__category-pin--inactive',
src: '../images/pin.svg'
}
]
]
],
], [
'div', {
class: 'detected__apps',
[
'div',
{
class: 'detected__apps'
},
apps,
],
],
);
apps
]
])
}
template = [
'div', {
class: 'detected',
'div',
{
class: 'detected'
},
template,
];
template
]
} else {
template = [
'div', {
class: 'empty',
'div',
{
class: 'empty'
},
[
'span', {
class: 'empty__text',
'span',
{
class: 'empty__text'
},
browser.i18n.getMessage('noAppsDetected'),
],
];
browser.i18n.getMessage('noAppsDetected')
]
]
}
return template;
return template
}
async function getApps() {
try {
const tabs = await browser.tabs.query({
active: true,
currentWindow: true,
});
currentWindow: true
})
const url = new URL(tabs[0].url)
document.querySelector(
'.footer__link'
).href = `https://www.wappalyzer.com/alerts/manage?url=${encodeURIComponent(
`${url.protocol}//${url.hostname}`
)}`
port.postMessage({
id: 'get_apps',
tab: tabs[0],
});
tab: tabs[0]
})
} catch (error) {
console.error(error); // eslint-disable-line no-console
console.error(error) // eslint-disable-line no-console
}
}
/**
* Async function to update body class based on option.
*/
async function getThemeMode() {
function getThemeMode() {
try {
port.postMessage({
id: 'update_theme_mode',
});
id: 'update_theme_mode'
})
} catch (error) {
console.error(error); // eslint-disable-line no-console
console.error(error) // eslint-disable-line no-console
}
}
@ -224,50 +274,51 @@ async function getThemeMode() {
*/
function updateThemeMode(res) {
if (res.hasOwnProperty('themeMode') && res.themeMode !== false) {
document.body.classList.add('theme-mode-sync');
document.body.classList.add('theme-mode-sync')
}
}
function displayApps(response) {
pinnedCategory = response.pinnedCategory; // eslint-disable-line prefer-destructuring
termsAccepted = response.termsAccepted; // eslint-disable-line prefer-destructuring
pinnedCategory = response.pinnedCategory // eslint-disable-line prefer-destructuring
termsAccepted = response.termsAccepted // eslint-disable-line prefer-destructuring
if (termsAccepted) {
replaceDomWhenReady(appsToDomTemplate(response));
replaceDomWhenReady(appsToDomTemplate(response))
} else {
i18n();
i18n()
const wrapper = document.querySelector('.terms__wrapper');
const wrapper = document.querySelector('.terms__wrapper')
document.querySelector('.terms__accept').addEventListener('click', () => {
port.postMessage({
id: 'set_option',
key: 'termsAccepted',
value: true,
});
value: true
})
wrapper.classList.remove('terms__wrapper--active');
wrapper.classList.remove('terms__wrapper--active')
getApps();
});
getApps()
})
wrapper.classList.add('terms__wrapper--active');
wrapper.classList.add('terms__wrapper--active')
}
}
port.onMessage.addListener((message) => {
switch (message.id) {
case 'get_apps':
displayApps(message.response);
displayApps(message.response)
break;
break
case 'update_theme_mode':
updateThemeMode(message.response);
updateThemeMode(message.response)
break;
break
default:
// Do nothing
}
});
})
getThemeMode();
getApps();
getThemeMode()
getApps()

@ -8,130 +8,137 @@
const validation = {
hostname: /(www.)?((.+?)\.(([a-z]{2,3}\.)?[a-z]{2,6}))$/,
hostnameBlacklist: /((local|dev(elopment)?|stag(e|ing)?|test(ing)?|demo(shop)?|admin|google|cache)\.|\/admin|\.local)/,
};
hostnameBlacklist: /((local|dev(elopment)?|stag(e|ing)?|test(ing)?|demo(shop)?|admin|google|cache)\.|\/admin|\.local)/
}
/**
* Enclose string in array
*/
function asArray(value) {
return value instanceof Array ? value : [value];
return Array.isArray(value) ? value : [value]
}
/**
*
*/
function asyncForEach(iterable, iterator) {
return Promise.all((iterable || [])
.map(item => new Promise(resolve => setTimeout(() => resolve(iterator(item)), 1))));
return Promise.all(
(iterable || []).map(
(item) =>
new Promise((resolve) => setTimeout(() => resolve(iterator(item)), 1))
)
)
}
/**
* Mark application as detected, set confidence and version
*/
function addDetected(app, pattern, type, value, key) {
app.detected = true;
app.detected = true
// Set confidence level
app.confidence[`${type} ${key ? `${key} ` : ''}${pattern.regex}`] = pattern.confidence === undefined ? 100 : parseInt(pattern.confidence, 10);
app.confidence[`${type} ${key ? `${key} ` : ''}${pattern.regex}`] =
pattern.confidence === undefined ? 100 : parseInt(pattern.confidence, 10)
// Detect version number
if (pattern.version) {
const versions = [];
const matches = pattern.regex.exec(value);
const versions = []
const matches = pattern.regex.exec(value)
let { version } = pattern;
let { version } = pattern
if (matches) {
matches.forEach((match, i) => {
// Parse ternary operator
const ternary = new RegExp(`\\\\${i}\\?([^:]+):(.*)$`).exec(version);
const ternary = new RegExp(`\\\\${i}\\?([^:]+):(.*)$`).exec(version)
if (ternary && ternary.length === 3) {
version = version.replace(ternary[0], match ? ternary[1] : ternary[2]);
version = version.replace(ternary[0], match ? ternary[1] : ternary[2])
}
// Replace back references
version = version.trim().replace(new RegExp(`\\\\${i}`, 'g'), match || '');
});
version = version
.trim()
.replace(new RegExp(`\\\\${i}`, 'g'), match || '')
})
if (version && versions.indexOf(version) === -1) {
versions.push(version);
if (version && !versions.includes(version)) {
versions.push(version)
}
if (versions.length) {
// Use the longest detected version number
app.version = versions.reduce((a, b) => (a.length > b.length ? a : b));
app.version = versions.reduce((a, b) => (a.length > b.length ? a : b))
}
}
}
}
function resolveExcludes(apps, detected) {
const excludes = [];
const detectedApps = Object.assign({}, apps, detected);
const excludes = []
const detectedApps = Object.assign({}, apps, detected)
// Exclude app in detected apps only
Object.keys(detectedApps).forEach((appName) => {
const app = detectedApps[appName];
const app = detectedApps[appName]
if (app.props.excludes) {
asArray(app.props.excludes).forEach((excluded) => {
excludes.push(excluded);
});
excludes.push(excluded)
})
}
});
})
// Remove excluded applications
Object.keys(apps).forEach((appName) => {
if (excludes.indexOf(appName) > -1) {
delete apps[appName];
if (excludes.includes(appName)) {
delete apps[appName]
}
});
})
}
class Application {
constructor(name, props, detected) {
this.confidence = {};
this.confidenceTotal = 0;
this.detected = Boolean(detected);
this.excludes = [];
this.name = name;
this.props = props;
this.version = '';
this.confidence = {}
this.confidenceTotal = 0
this.detected = Boolean(detected)
this.excludes = []
this.name = name
this.props = props
this.version = ''
}
/**
* Calculate confidence total
*/
getConfidence() {
let total = 0;
let total = 0
Object.keys(this.confidence).forEach((id) => {
total += this.confidence[id];
});
total += this.confidence[id]
})
this.confidenceTotal = Math.min(total, 100);
this.confidenceTotal = Math.min(total, 100)
return this.confidenceTotal;
return this.confidenceTotal
}
}
class Wappalyzer {
constructor() {
this.apps = {};
this.categories = {};
this.driver = {};
this.jsPatterns = {};
this.detected = {};
this.hostnameCache = {};
this.adCache = [];
this.apps = {}
this.categories = {}
this.driver = {}
this.jsPatterns = {}
this.detected = {}
this.hostnameCache = {}
this.adCache = []
this.config = {
websiteURL: 'https://www.wappalyzer.com/',
twitterURL: 'https://twitter.com/Wappalyzer',
githubURL: 'https://github.com/AliasIO/Wappalyzer',
};
githubURL: 'https://github.com/AliasIO/Wappalyzer'
}
}
/**
@ -139,124 +146,135 @@ class Wappalyzer {
*/
log(message, source, type) {
if (this.driver.log) {
this.driver.log(message, source || '', type || 'debug');
this.driver.log(message, source || '', type || 'debug')
}
}
analyze(url, data, context) {
const apps = {};
const promises = [];
const startTime = new Date();
const {
scripts,
cookies,
headers,
js,
} = data;
let { html } = data;
const apps = {}
const promises = []
const startTime = new Date()
const { scripts, cookies, headers, js } = data
let { html } = data
if (this.detected[url.canonical] === undefined) {
this.detected[url.canonical] = {};
this.detected[url.canonical] = {}
}
const metaTags = [];
const metaTags = []
// Additional information
let language = null;
let language = null
if (html) {
if (typeof html !== 'string') {
html = '';
html = ''
}
let matches = data.html.match(new RegExp('<html[^>]*[: ]lang="([a-z]{2}((-|_)[A-Z]{2})?)"', 'i'));
let matches = data.html.match(
new RegExp('<html[^>]*[: ]lang="([a-z]{2}((-|_)[A-Z]{2})?)"', 'i')
)
language = matches && matches.length ? matches[1] : data.language || null;
language = matches && matches.length ? matches[1] : data.language || null
// Meta tags
const regex = /<meta[^>]+>/ig;
const regex = /<meta[^>]+>/gi
do {
matches = regex.exec(html);
matches = regex.exec(html)
if (!matches) {
break;
break
}
metaTags.push(matches[0]);
} while (matches);
metaTags.push(matches[0])
} while (matches)
}
Object.keys(this.apps).forEach((appName) => {
apps[appName] = this.detected[url.canonical] && this.detected[url.canonical][appName]
apps[appName] =
this.detected[url.canonical] && this.detected[url.canonical][appName]
? this.detected[url.canonical][appName]
: new Application(appName, this.apps[appName]);
: new Application(appName, this.apps[appName])
const app = apps[appName];
const app = apps[appName]
promises.push(this.analyzeUrl(app, url));
promises.push(this.analyzeUrl(app, url))
if (html) {
promises.push(this.analyzeHtml(app, html));
promises.push(this.analyzeMeta(app, metaTags));
promises.push(this.analyzeHtml(app, html))
promises.push(this.analyzeMeta(app, metaTags))
}
if (scripts) {
promises.push(this.analyzeScripts(app, scripts));
promises.push(this.analyzeScripts(app, scripts))
}
if (cookies) {
promises.push(this.analyzeCookies(app, cookies));
promises.push(this.analyzeCookies(app, cookies))
}
if (headers) {
promises.push(this.analyzeHeaders(app, headers));
promises.push(this.analyzeHeaders(app, headers))
}
});
})
if (js) {
Object.keys(js).forEach((appName) => {
if (typeof js[appName] !== 'function') {
promises.push(this.analyzeJs(apps[appName], js[appName]));
promises.push(this.analyzeJs(apps[appName], js[appName]))
}
});
})
}
return new Promise(async (resolve) => {
await Promise.all(promises);
await Promise.all(promises)
Object.keys(apps).forEach((appName) => {
const app = apps[appName];
const app = apps[appName]
if (!app.detected || !app.getConfidence()) {
delete apps[app.name];
delete apps[app.name]
}
});
})
resolveExcludes(apps, this.detected[url]);
this.resolveImplies(apps, url.canonical);
resolveExcludes(apps, this.detected[url])
this.resolveImplies(apps, url.canonical)
this.cacheDetectedApps(apps, url.canonical);
this.trackDetectedApps(apps, url, language);
this.cacheDetectedApps(apps, url.canonical)
this.trackDetectedApps(apps, url, language)
this.log(`Processing ${Object.keys(data).join(', ')} took ${((new Date() - startTime) / 1000).toFixed(2)}s (${url.hostname})`, 'core');
this.log(
`Processing ${Object.keys(data).join(', ')} took ${(
(new Date() - startTime) /
1000
).toFixed(2)}s (${url.hostname})`,
'core'
)
if (Object.keys(apps).length) {
this.log(`Identified ${Object.keys(apps).join(', ')} (${url.hostname})`, 'core');
this.log(
`Identified ${Object.keys(apps).join(', ')} (${url.hostname})`,
'core'
)
}
this.driver.displayApps(this.detected[url.canonical], { language }, context);
this.driver.displayApps(
this.detected[url.canonical],
{ language },
context
)
return resolve();
});
return resolve()
})
}
/**
* Cache detected ads
*/
cacheDetectedAds(ad) {
this.adCache.push(ad);
this.adCache.push(ad)
}
/**
@ -264,58 +282,65 @@ class Wappalyzer {
*/
robotsTxtAllows(url) {
return new Promise(async (resolve, reject) => {
const parsed = this.parseUrl(url);
const parsed = this.parseUrl(url)
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return reject();
return reject()
}
const robotsTxt = await this.driver.getRobotsTxt(parsed.host, parsed.protocol === 'https:');
const robotsTxt = await this.driver.getRobotsTxt(
parsed.host,
parsed.protocol === 'https:'
)
if (robotsTxt.some(disallowedPath => parsed.pathname.indexOf(disallowedPath) === 0)) {
return reject();
if (
robotsTxt.some(
(disallowedPath) => parsed.pathname.indexOf(disallowedPath) === 0
)
) {
return reject()
}
return resolve();
});
return resolve()
})
}
/**
* Parse a URL
*/
parseUrl(url) {
const a = this.driver.document.createElement('a');
const a = this.driver.document.createElement('a')
a.href = url;
a.href = url
a.canonical = `${a.protocol}//${a.host}${a.pathname}`;
a.canonical = `${a.protocol}//${a.host}${a.pathname}`
return a;
return a
}
/**
*
*/
static parseRobotsTxt(robotsTxt) {
const disallow = [];
const disallow = []
let userAgent;
let userAgent
robotsTxt.split('\n').forEach((line) => {
let matches = /^User-agent:\s*(.+)$/i.exec(line.trim());
let matches = /^User-agent:\s*(.+)$/i.exec(line.trim())
if (matches) {
userAgent = matches[1].toLowerCase();
userAgent = matches[1].toLowerCase()
} else if (userAgent === '*' || userAgent === 'wappalyzer') {
matches = /^Disallow:\s*(.+)$/i.exec(line.trim());
matches = /^Disallow:\s*(.+)$/i.exec(line.trim())
if (matches) {
disallow.push(matches[1]);
disallow.push(matches[1])
}
}
});
})
return disallow;
return disallow
}
/**
@ -323,15 +348,15 @@ class Wappalyzer {
*/
ping() {
if (Object.keys(this.hostnameCache).length > 50) {
this.driver.ping(this.hostnameCache);
this.driver.ping(this.hostnameCache)
this.hostnameCache = {};
this.hostnameCache = {}
}
if (this.adCache.length > 50) {
this.driver.ping({}, this.adCache);
this.driver.ping({}, this.adCache)
this.adCache = [];
this.adCache = []
}
}
@ -340,55 +365,55 @@ class Wappalyzer {
*/
parsePatterns(patterns) {
if (!patterns) {
return [];
return []
}
let parsed = {};
let parsed = {}
// Convert string to object containing array containing string
if (typeof patterns === 'string' || patterns instanceof Array) {
if (typeof patterns === 'string' || Array.isArray(patterns)) {
patterns = {
main: asArray(patterns),
};
main: asArray(patterns)
}
}
Object.keys(patterns).forEach((key) => {
parsed[key] = [];
parsed[key] = []
asArray(patterns[key]).forEach((pattern) => {
const attrs = {};
const attrs = {}
pattern.split('\\;').forEach((attr, i) => {
if (i) {
// Key value pairs
attr = attr.split(':');
attr = attr.split(':')
if (attr.length > 1) {
attrs[attr.shift()] = attr.join(':');
attrs[attr.shift()] = attr.join(':')
}
} else {
attrs.string = attr;
attrs.string = attr
try {
attrs.regex = new RegExp(attr.replace('/', '\/'), 'i'); // Escape slashes in regular expression
attrs.regex = new RegExp(attr.replace('/', '/'), 'i') // Escape slashes in regular expression
} catch (error) {
attrs.regex = new RegExp();
attrs.regex = new RegExp()
this.log(`${error.message}: ${attr}`, 'error', 'core');
this.log(`${error.message}: ${attr}`, 'error', 'core')
}
}
});
})
parsed[key].push(attrs);
});
});
parsed[key].push(attrs)
})
})
// Convert back to array if the original pattern list was an array (or string)
if ('main' in parsed) {
parsed = parsed.main;
parsed = parsed.main
}
return parsed;
return parsed
}
/**
@ -397,49 +422,60 @@ class Wappalyzer {
parseJsPatterns() {
Object.keys(this.apps).forEach((appName) => {
if (this.apps[appName].js) {
this.jsPatterns[appName] = this.parsePatterns(this.apps[appName].js);
this.jsPatterns[appName] = this.parsePatterns(this.apps[appName].js)
}
});
})
}
resolveImplies(apps, url) {
let checkImplies = true;
let checkImplies = true
const resolve = (appName) => {
const app = apps[appName];
const app = apps[appName]
if (app && app.props.implies) {
asArray(app.props.implies).forEach((implied) => {
[implied] = this.parsePatterns(implied);
;[implied] = this.parsePatterns(implied)
if (!this.apps[implied.string]) {
this.log(`Implied application ${implied.string} does not exist`, 'core', 'warn');
this.log(
`Implied application ${implied.string} does not exist`,
'core',
'warn'
)
return;
return
}
if (!(implied.string in apps)) {
apps[implied.string] = this.detected[url] && this.detected[url][implied.string]
apps[implied.string] =
this.detected[url] && this.detected[url][implied.string]
? this.detected[url][implied.string]
: new Application(implied.string, this.apps[implied.string], true);
: new Application(
implied.string,
this.apps[implied.string],
true
)
checkImplies = true;
checkImplies = true
}
// Apply app confidence to implied app
Object.keys(app.confidence).forEach((id) => {
apps[implied.string].confidence[`${id} implied by ${appName}`] = app.confidence[id] * (implied.confidence === undefined ? 1 : implied.confidence / 100);
});
});
apps[implied.string].confidence[`${id} implied by ${appName}`] =
app.confidence[id] *
(implied.confidence === undefined ? 1 : implied.confidence / 100)
})
})
}
}
};
// Implied applications
// Run several passes as implied apps may imply other apps
while (checkImplies) {
checkImplies = false;
checkImplies = false
Object.keys(apps).forEach(resolve);
Object.keys(apps).forEach(resolve)
}
}
@ -448,19 +484,18 @@ class Wappalyzer {
*/
cacheDetectedApps(apps, url) {
Object.keys(apps).forEach((appName) => {
const app = apps[appName];
const app = apps[appName]
// Per URL
this.detected[url][appName] = app;
this.detected[url][appName] = app
Object.keys(app.confidence)
.forEach((id) => {
this.detected[url][appName].confidence[id] = app.confidence[id];
});
});
Object.keys(app.confidence).forEach((id) => {
this.detected[url][appName].confidence[id] = app.confidence[id]
})
})
if (this.driver.ping instanceof Function) {
this.ping();
this.ping()
}
}
@ -469,204 +504,219 @@ class Wappalyzer {
*/
trackDetectedApps(apps, url, language) {
if (!(this.driver.ping instanceof Function)) {
return;
return
}
const hostname = `${url.protocol}//${url.hostname}`;
const hostname = `${url.protocol}//${url.hostname}`
Object.keys(apps).forEach((appName) => {
const app = apps[appName];
const app = apps[appName]
if (this.detected[url.canonical][appName].getConfidence() >= 100) {
if (
validation.hostname.test(url.hostname)
&& !validation.hostnameBlacklist.test(url.hostname)
validation.hostname.test(url.hostname) &&
!validation.hostnameBlacklist.test(url.hostname)
) {
if (!(hostname in this.hostnameCache)) {
this.hostnameCache[hostname] = {
applications: {},
meta: {},
};
meta: {}
}
}
if (!(appName in this.hostnameCache[hostname].applications)) {
this.hostnameCache[hostname].applications[appName] = {
hits: 0,
};
hits: 0
}
}
this.hostnameCache[hostname].applications[appName].hits += 1;
this.hostnameCache[hostname].applications[appName].hits += 1
if (apps[appName].version) {
this.hostnameCache[hostname].applications[appName].version = app.version;
this.hostnameCache[hostname].applications[appName].version =
app.version
}
}
}
});
})
if (hostname in this.hostnameCache) {
this.hostnameCache[hostname].meta.language = language;
this.hostnameCache[hostname].meta.language = language
}
this.ping();
this.ping()
}
/**
* Analyze URL
*/
analyzeUrl(app, url) {
const patterns = this.parsePatterns(app.props.url);
const patterns = this.parsePatterns(app.props.url)
if (!patterns.length) {
return Promise.resolve();
return Promise.resolve()
}
return asyncForEach(patterns, (pattern) => {
if (pattern.regex.test(url.canonical)) {
addDetected(app, pattern, 'url', url.canonical);
addDetected(app, pattern, 'url', url.canonical)
}
});
})
}
/**
* Analyze HTML
*/
analyzeHtml(app, html) {
const patterns = this.parsePatterns(app.props.html);
const patterns = this.parsePatterns(app.props.html)
if (!patterns.length) {
return Promise.resolve();
return Promise.resolve()
}
return asyncForEach(patterns, (pattern) => {
if (pattern.regex.test(html)) {
addDetected(app, pattern, 'html', html);
addDetected(app, pattern, 'html', html)
}
});
})
}
/**
* Analyze script tag
*/
analyzeScripts(app, scripts) {
const patterns = this.parsePatterns(app.props.script);
const patterns = this.parsePatterns(app.props.script)
if (!patterns.length) {
return Promise.resolve();
return Promise.resolve()
}
return asyncForEach(patterns, (pattern) => {
scripts.forEach((uri) => {
if (pattern.regex.test(uri)) {
addDetected(app, pattern, 'script', uri);
addDetected(app, pattern, 'script', uri)
}
});
});
})
})
}
/**
* Analyze meta tag
*/
analyzeMeta(app, metaTags) {
const patterns = this.parsePatterns(app.props.meta);
const promises = [];
const patterns = this.parsePatterns(app.props.meta)
const promises = []
if (!app.props.meta) {
return Promise.resolve();
return Promise.resolve()
}
metaTags.forEach((match) => {
Object.keys(patterns).forEach((meta) => {
const r = new RegExp(`(?:name|property)=["']${meta}["']`, 'i');
const r = new RegExp(`(?:name|property)=["']${meta}["']`, 'i')
if (r.test(match)) {
const content = match.match(/content=("|')([^"']+)("|')/i);
const content = match.match(/content=("|')([^"']+)("|')/i)
promises.push(asyncForEach(patterns[meta], (pattern) => {
if (content && content.length === 4 && pattern.regex.test(content[2])) {
addDetected(app, pattern, 'meta', content[2], meta);
promises.push(
asyncForEach(patterns[meta], (pattern) => {
if (
content &&
content.length === 4 &&
pattern.regex.test(content[2])
) {
addDetected(app, pattern, 'meta', content[2], meta)
}
}));
})
)
}
});
});
})
})
return Promise.all(promises);
return Promise.all(promises)
}
/**
* Analyze response headers
*/
analyzeHeaders(app, headers) {
const patterns = this.parsePatterns(app.props.headers);
const promises = [];
const patterns = this.parsePatterns(app.props.headers)
const promises = []
Object.keys(patterns).forEach((headerName) => {
if (typeof patterns[headerName] !== 'function') {
promises.push(asyncForEach(patterns[headerName], (pattern) => {
headerName = headerName.toLowerCase();
promises.push(
asyncForEach(patterns[headerName], (pattern) => {
headerName = headerName.toLowerCase()
if (headerName in headers) {
headers[headerName].forEach((headerValue) => {
if (pattern.regex.test(headerValue)) {
addDetected(app, pattern, 'headers', headerValue, headerName);
addDetected(app, pattern, 'headers', headerValue, headerName)
}
});
})
}
}));
})
)
}
});
})
return promises ? Promise.all(promises) : Promise.resolve();
return promises ? Promise.all(promises) : Promise.resolve()
}
/**
* Analyze cookies
*/
analyzeCookies(app, cookies) {
const patterns = this.parsePatterns(app.props.cookies);
const promises = [];
const patterns = this.parsePatterns(app.props.cookies)
const promises = []
Object.keys(patterns).forEach((cookieName) => {
if (typeof patterns[cookieName] !== 'function') {
const cookieNameLower = cookieName.toLowerCase();
const cookieNameLower = cookieName.toLowerCase()
promises.push(asyncForEach(patterns[cookieName], (pattern) => {
const cookie = cookies.find(_cookie => _cookie.name.toLowerCase() === cookieNameLower);
promises.push(
asyncForEach(patterns[cookieName], (pattern) => {
const cookie = cookies.find(
(_cookie) => _cookie.name.toLowerCase() === cookieNameLower
)
if (cookie && pattern.regex.test(cookie.value)) {
addDetected(app, pattern, 'cookies', cookie.value, cookieName);
addDetected(app, pattern, 'cookies', cookie.value, cookieName)
}
}));
})
)
}
});
})
return promises ? Promise.all(promises) : Promise.resolve();
return promises ? Promise.all(promises) : Promise.resolve()
}
/**
* Analyze JavaScript variables
*/
analyzeJs(app, results) {
const promises = [];
const promises = []
Object.keys(results).forEach((string) => {
if (typeof results[string] !== 'function') {
promises.push(asyncForEach(Object.keys(results[string]), (index) => {
const pattern = this.jsPatterns[app.name][string][index];
const value = results[string][index];
promises.push(
asyncForEach(Object.keys(results[string]), (index) => {
const pattern = this.jsPatterns[app.name][string][index]
const value = results[string][index]
if (pattern && pattern.regex.test(value)) {
addDetected(app, pattern, 'js', value, string);
addDetected(app, pattern, 'js', value, string)
}
}));
})
)
}
});
})
return promises ? Promise.all(promises) : Promise.resolve();
return promises ? Promise.all(promises) : Promise.resolve()
}
}
if (typeof module === 'object') {
module.exports = Wappalyzer;
module.exports = Wappalyzer
}

Loading…
Cancel
Save