parent
ad853a3abd
commit
2aca8275c5
@ -0,0 +1,285 @@
|
|||||||
|
'use strict'
|
||||||
|
/* eslint-env browser */
|
||||||
|
/* globals chrome, Wappalyzer */
|
||||||
|
|
||||||
|
const { setTechnologies, setCategories, analyze, resolve, unique } = Wappalyzer
|
||||||
|
|
||||||
|
function promisify(context, method, ...args) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
context[method](...args, (...args) => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
return reject(chrome.runtime.lastError)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(...args)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const Driver = {
|
||||||
|
cache: {
|
||||||
|
hostnames: {},
|
||||||
|
robots: {}
|
||||||
|
},
|
||||||
|
|
||||||
|
agent: chrome.extension.getURL('/').startsWith('moz-') ? 'firefox' : 'chrome',
|
||||||
|
|
||||||
|
log(message, source = 'driver', type = 'log') {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console[type](`wappalyzer | ${source} |`, message)
|
||||||
|
},
|
||||||
|
|
||||||
|
warn(message, source = 'driver') {
|
||||||
|
Driver.log(message, source, 'warn')
|
||||||
|
},
|
||||||
|
|
||||||
|
error(error, source = 'driver') {
|
||||||
|
Driver.log(error, source, 'error')
|
||||||
|
},
|
||||||
|
|
||||||
|
open(url, active = true) {
|
||||||
|
chrome.tabs.create({ url, active })
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadTechnologies() {
|
||||||
|
try {
|
||||||
|
const { apps: technologies, categories } = await (
|
||||||
|
await fetch(chrome.extension.getURL('apps.json'))
|
||||||
|
).json()
|
||||||
|
|
||||||
|
setTechnologies(technologies)
|
||||||
|
setCategories(categories)
|
||||||
|
} catch (error) {
|
||||||
|
Driver.error(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
post(url, body) {
|
||||||
|
try {
|
||||||
|
return fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error.message || error.toString())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async getOption(name, defaultValue = null) {
|
||||||
|
try {
|
||||||
|
const option = await promisify(chrome.storage.local, 'get', name)
|
||||||
|
|
||||||
|
if (option[name] !== undefined) {
|
||||||
|
return option[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error.message || error.toString())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async setOption(name, value) {
|
||||||
|
try {
|
||||||
|
await promisify(chrome.storage.local, 'set', {
|
||||||
|
[name]: value
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error.message || error.toString())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onRuntimeConnect(port) {
|
||||||
|
port.onMessage.addListener(async (message) => {
|
||||||
|
const { func, args } = message
|
||||||
|
|
||||||
|
if (!func || !port.sender.tab) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Driver.log(`Message received from ${port.name}: ${func}`)
|
||||||
|
|
||||||
|
await Driver[func](...args)
|
||||||
|
|
||||||
|
/*
|
||||||
|
const pinnedCategory = await getOption('pinnedCategory')
|
||||||
|
|
||||||
|
const url = new URL(port.sender.tab.url)
|
||||||
|
|
||||||
|
const cookies = await browser.cookies.getAll({
|
||||||
|
domain: `.${url.hostname}`
|
||||||
|
})
|
||||||
|
|
||||||
|
let response
|
||||||
|
|
||||||
|
switch (message.id) {
|
||||||
|
case 'log':
|
||||||
|
wappalyzer.log(message.subject, message.source)
|
||||||
|
|
||||||
|
break
|
||||||
|
case 'analyze':
|
||||||
|
if (message.subject.html) {
|
||||||
|
browser.i18n
|
||||||
|
.detectLanguage(message.subject.html)
|
||||||
|
.then(({ languages }) => {
|
||||||
|
const language = languages
|
||||||
|
.filter(({ percentage }) => percentage >= 75)
|
||||||
|
.map(({ language: lang }) => lang)[0]
|
||||||
|
|
||||||
|
message.subject.language = language
|
||||||
|
|
||||||
|
wappalyzer.analyze(url, message.subject, {
|
||||||
|
tab: port.sender.tab
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
wappalyzer.analyze(url, message.subject, { tab: port.sender.tab })
|
||||||
|
}
|
||||||
|
|
||||||
|
await setOption('hostnameCache', wappalyzer.hostnameCache)
|
||||||
|
|
||||||
|
break
|
||||||
|
case 'ad_log':
|
||||||
|
wappalyzer.cacheDetectedAds(message.subject)
|
||||||
|
|
||||||
|
break
|
||||||
|
case 'get_apps':
|
||||||
|
response = {
|
||||||
|
tabCache: tabCache[message.tab.id],
|
||||||
|
apps: wappalyzer.apps,
|
||||||
|
categories: wappalyzer.categories,
|
||||||
|
pinnedCategory,
|
||||||
|
termsAccepted:
|
||||||
|
userAgent() === 'chrome' ||
|
||||||
|
(await getOption('termsAccepted', false))
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
case 'set_option':
|
||||||
|
await setOption(message.key, message.value)
|
||||||
|
|
||||||
|
break
|
||||||
|
case 'get_js_patterns':
|
||||||
|
response = {
|
||||||
|
patterns: wappalyzer.jsPatterns
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
case 'update_theme_mode':
|
||||||
|
// Sync theme mode to popup.
|
||||||
|
response = {
|
||||||
|
themeMode: await getOption('themeMode', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
port.postMessage({
|
||||||
|
id: message.id,
|
||||||
|
response
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
*/
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
async onWebRequestComplete(request) {
|
||||||
|
if (request.responseHeaders) {
|
||||||
|
const headers = {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = new URL(request.url)
|
||||||
|
|
||||||
|
const [tab] = await promisify(chrome.tabs, 'query', { url: [url.href] })
|
||||||
|
|
||||||
|
if (tab) {
|
||||||
|
request.responseHeaders.forEach((header) => {
|
||||||
|
const name = header.name.toLowerCase()
|
||||||
|
|
||||||
|
headers[name] = headers[name] || []
|
||||||
|
|
||||||
|
headers[name].push(
|
||||||
|
(header.value || header.binaryValue || '').toString()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (
|
||||||
|
headers['content-type'] &&
|
||||||
|
/\/x?html/.test(headers['content-type'][0])
|
||||||
|
) {
|
||||||
|
await Driver.onDetect(url, await analyze(url, { headers }, { tab }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Driver.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async onContentLoad(href, items) {
|
||||||
|
try {
|
||||||
|
const url = new URL(href)
|
||||||
|
|
||||||
|
items.cookies = await promisify(chrome.cookies, 'getAll', {
|
||||||
|
domain: `.${url.hostname}`
|
||||||
|
})
|
||||||
|
|
||||||
|
await Driver.onDetect(url, await analyze(url, items))
|
||||||
|
} catch (error) {
|
||||||
|
Driver.error(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async onDetect(url, detections = []) {
|
||||||
|
Driver.cache.hostnames[url.hostname] = unique([
|
||||||
|
...(Driver.cache.hostnames[url.hostname] || []),
|
||||||
|
...detections
|
||||||
|
])
|
||||||
|
|
||||||
|
const resolved = resolve(Driver.cache.hostnames[url.hostname])
|
||||||
|
|
||||||
|
const pinnedCategory = parseInt(
|
||||||
|
await Driver.getOption('pinnedCategory'),
|
||||||
|
10
|
||||||
|
)
|
||||||
|
|
||||||
|
const pinned = resolved.find(({ categories }) =>
|
||||||
|
categories.some(({ id }) => id === pinnedCategory)
|
||||||
|
)
|
||||||
|
|
||||||
|
const { icon } =
|
||||||
|
pinned ||
|
||||||
|
resolved.sort(({ categories: a }, { categories: b }) => {
|
||||||
|
const max = (value) =>
|
||||||
|
value.reduce((max, { priority }) => Math.max(max, priority))
|
||||||
|
|
||||||
|
return max(a) > max(b) ? -1 : 1
|
||||||
|
})[0]
|
||||||
|
|
||||||
|
const tabs = await promisify(chrome.tabs, 'query', { url: [url.href] })
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
tabs.map(({ id: tabId }) =>
|
||||||
|
promisify(chrome.pageAction, 'setIcon', {
|
||||||
|
tabId,
|
||||||
|
path: chrome.extension.getURL(`../images/icons/${icon}`)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
;(async function() {
|
||||||
|
await Driver.loadTechnologies()
|
||||||
|
|
||||||
|
chrome.runtime.onConnect.addListener(Driver.onRuntimeConnect)
|
||||||
|
chrome.webRequest.onCompleted.addListener(
|
||||||
|
Driver.onWebRequestComplete,
|
||||||
|
{ urls: ['http://*/*', 'https://*/*'], types: ['main_frame'] },
|
||||||
|
['responseHeaders']
|
||||||
|
)
|
||||||
|
})()
|
@ -0,0 +1,326 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const Wappalyzer = {
|
||||||
|
technologies: [],
|
||||||
|
categories: [],
|
||||||
|
|
||||||
|
slugify(string) {
|
||||||
|
return string
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9-]/g, '-')
|
||||||
|
.replace(/--+/g, '-')
|
||||||
|
.replace(/(?:^-|-$)/, '')
|
||||||
|
},
|
||||||
|
|
||||||
|
unique(detections) {
|
||||||
|
return detections.filter(
|
||||||
|
({ technology: { name }, pattern: { regex } }, index) => {
|
||||||
|
return (
|
||||||
|
detections.findIndex(
|
||||||
|
({ technology: { name: _name }, pattern: { regex: _regex } }) =>
|
||||||
|
name === _name && (!regex || regex === _regex)
|
||||||
|
) === index
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
getTechnology(name) {
|
||||||
|
return Wappalyzer.technologies.find(({ name: _name }) => name === _name)
|
||||||
|
},
|
||||||
|
|
||||||
|
getCategory(id) {
|
||||||
|
return Wappalyzer.categories.find(({ id: _id }) => id === _id)
|
||||||
|
},
|
||||||
|
|
||||||
|
resolve(detections) {
|
||||||
|
const resolved = detections.reduce((resolved, { technology }) => {
|
||||||
|
if (
|
||||||
|
resolved.findIndex(
|
||||||
|
({ technology: { name } }) => name === technology.name
|
||||||
|
) === -1
|
||||||
|
) {
|
||||||
|
let version = ''
|
||||||
|
let confidence = 0
|
||||||
|
|
||||||
|
detections.forEach(({ technology: { name }, pattern, match }) => {
|
||||||
|
if (name === technology.name) {
|
||||||
|
const versionValue = Wappalyzer.resolveVersion(pattern, match)
|
||||||
|
|
||||||
|
confidence = Math.min(100, confidence + pattern.confidence)
|
||||||
|
version =
|
||||||
|
versionValue.length > version.length && versionValue.length <= 10
|
||||||
|
? versionValue
|
||||||
|
: version
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
resolved.push({ technology, confidence, version })
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolved
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
Wappalyzer.resolveExcludes(resolved)
|
||||||
|
Wappalyzer.resolveImplies(resolved)
|
||||||
|
|
||||||
|
return resolved.map(
|
||||||
|
({
|
||||||
|
technology: { name, slug, categories, icon, website },
|
||||||
|
confidence,
|
||||||
|
version
|
||||||
|
}) => ({
|
||||||
|
name,
|
||||||
|
slug,
|
||||||
|
categories: categories.map((id) => Wappalyzer.getCategory(id)),
|
||||||
|
confidence,
|
||||||
|
version,
|
||||||
|
icon,
|
||||||
|
website
|
||||||
|
})
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
resolveVersion({ version, regex }, match) {
|
||||||
|
let resolved = version
|
||||||
|
|
||||||
|
if (version) {
|
||||||
|
const matches = regex.exec(match)
|
||||||
|
|
||||||
|
if (matches) {
|
||||||
|
matches.forEach((match, index) => {
|
||||||
|
// Parse ternary operator
|
||||||
|
const ternary = new RegExp(`\\\\${index}\\?([^:]+):(.*)$`).exec(
|
||||||
|
version
|
||||||
|
)
|
||||||
|
|
||||||
|
if (ternary && ternary.length === 3) {
|
||||||
|
resolved = version.replace(
|
||||||
|
ternary[0],
|
||||||
|
match ? ternary[1] : ternary[2]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace back references
|
||||||
|
resolved = resolved
|
||||||
|
.trim()
|
||||||
|
.replace(new RegExp(`\\\\${index}`, 'g'), match || '')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolved
|
||||||
|
},
|
||||||
|
|
||||||
|
resolveExcludes(resolved) {
|
||||||
|
resolved.forEach(({ technology }) => {
|
||||||
|
technology.excludes.forEach((name) => {
|
||||||
|
const excluded = Wappalyzer.getTechnology(name)
|
||||||
|
|
||||||
|
const index = resolved.findIndex(({ name }) => name === excluded.name)
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
resolved.splice(index, 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
resolveImplies(resolved) {
|
||||||
|
let done = false
|
||||||
|
|
||||||
|
while (!done) {
|
||||||
|
resolved.forEach(({ technology, confidence }) => {
|
||||||
|
done = true
|
||||||
|
|
||||||
|
technology.implies.forEach((name) => {
|
||||||
|
const implied = Wappalyzer.getTechnology(name)
|
||||||
|
|
||||||
|
if (
|
||||||
|
resolved.findIndex(
|
||||||
|
({ technology: { name } }) => name === implied.name
|
||||||
|
) === -1
|
||||||
|
) {
|
||||||
|
resolved.push({ technology: implied, confidence, version: '' })
|
||||||
|
|
||||||
|
done = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async analyze(url, { html, meta, headers, cookies, scripts, js }) {
|
||||||
|
const oo = Wappalyzer.analyzeOneToOne
|
||||||
|
const om = Wappalyzer.analyzeOneToMany
|
||||||
|
const mm = Wappalyzer.analyzeManyToMany
|
||||||
|
|
||||||
|
const flatten = (array) => Array.prototype.concat.apply([], array)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const detections = flatten(
|
||||||
|
flatten(
|
||||||
|
await Promise.all(
|
||||||
|
Wappalyzer.technologies.map((technology) =>
|
||||||
|
Promise.all([
|
||||||
|
oo(technology, 'url', url),
|
||||||
|
oo(technology, 'html', html),
|
||||||
|
om(technology, 'meta', meta),
|
||||||
|
mm(technology, 'headers', headers),
|
||||||
|
om(technology, 'cookies', cookies),
|
||||||
|
om(technology, 'scripts', scripts)
|
||||||
|
])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).filter((technology) => technology)
|
||||||
|
|
||||||
|
return detections
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error.message || error.toString())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setTechnologies(data) {
|
||||||
|
const transform = Wappalyzer.transformPatterns
|
||||||
|
|
||||||
|
Wappalyzer.technologies = Object.keys(data).reduce((technologies, name) => {
|
||||||
|
const {
|
||||||
|
cats,
|
||||||
|
url,
|
||||||
|
html,
|
||||||
|
meta,
|
||||||
|
headers,
|
||||||
|
cookies,
|
||||||
|
script,
|
||||||
|
implies,
|
||||||
|
excludes,
|
||||||
|
icon,
|
||||||
|
website
|
||||||
|
} = data[name]
|
||||||
|
|
||||||
|
technologies.push({
|
||||||
|
name,
|
||||||
|
categories: cats || [],
|
||||||
|
slug: Wappalyzer.slugify(name),
|
||||||
|
url: transform(url),
|
||||||
|
headers: transform(headers),
|
||||||
|
cookies: transform(cookies),
|
||||||
|
html: transform(html),
|
||||||
|
meta: transform(meta),
|
||||||
|
scripts: transform(script),
|
||||||
|
implies: typeof implies === 'string' ? [implies] : implies || [],
|
||||||
|
excludes: typeof excludes === 'string' ? [excludes] : excludes || [],
|
||||||
|
icon: icon || 'default.svg',
|
||||||
|
website: website || ''
|
||||||
|
})
|
||||||
|
|
||||||
|
return technologies
|
||||||
|
}, [])
|
||||||
|
},
|
||||||
|
|
||||||
|
setCategories(data) {
|
||||||
|
Wappalyzer.categories = Object.keys(data)
|
||||||
|
.reduce((categories, id) => {
|
||||||
|
const category = data[id]
|
||||||
|
|
||||||
|
categories.push({
|
||||||
|
id: parseInt(id, 10),
|
||||||
|
slug: Wappalyzer.slugify(category.name),
|
||||||
|
...category
|
||||||
|
})
|
||||||
|
|
||||||
|
return categories
|
||||||
|
}, [])
|
||||||
|
.sort(({ priority: a }, { priority: b }) => (a > b ? -1 : 0))
|
||||||
|
},
|
||||||
|
|
||||||
|
transformPatterns(patterns) {
|
||||||
|
if (!patterns) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const toArray = (value) => (Array.isArray(value) ? value : [value])
|
||||||
|
|
||||||
|
if (typeof patterns === 'string' || Array.isArray(patterns)) {
|
||||||
|
patterns = { main: patterns }
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = Object.keys(patterns).reduce((parsed, key) => {
|
||||||
|
parsed[key.toLowerCase()] = toArray(patterns[key]).map((pattern) => {
|
||||||
|
const { regex, confidence, version } = pattern
|
||||||
|
.split('\\;')
|
||||||
|
.reduce((attrs, attr, i) => {
|
||||||
|
if (i) {
|
||||||
|
// Key value pairs
|
||||||
|
attr = attr.split(':')
|
||||||
|
|
||||||
|
if (attr.length > 1) {
|
||||||
|
attrs[attr.shift()] = attr.join(':')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Escape slashes in regular expression
|
||||||
|
attrs.regex = new RegExp(attr.replace(/\//g, '\\/'), 'i')
|
||||||
|
}
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
return {
|
||||||
|
regex,
|
||||||
|
confidence: parseInt(confidence || 100, 10),
|
||||||
|
version: version || ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return parsed
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
return 'main' in parsed ? parsed.main : parsed
|
||||||
|
},
|
||||||
|
|
||||||
|
analyzeOneToOne(technology, type, value) {
|
||||||
|
return technology[type].reduce((technologies, pattern) => {
|
||||||
|
if (pattern.regex.test(value)) {
|
||||||
|
technologies.push({ technology, pattern, match: value })
|
||||||
|
}
|
||||||
|
|
||||||
|
return technologies
|
||||||
|
}, [])
|
||||||
|
},
|
||||||
|
|
||||||
|
analyzeOneToMany(technology, type, items = []) {
|
||||||
|
return items.reduce((technologies, { key, value }) => {
|
||||||
|
const patterns = technology[type][key] || []
|
||||||
|
|
||||||
|
patterns.forEach((pattern) => {
|
||||||
|
if (pattern.regex.test(value)) {
|
||||||
|
technologies.push({ technology, pattern, match: value })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return technologies
|
||||||
|
}, [])
|
||||||
|
},
|
||||||
|
|
||||||
|
analyzeManyToMany(technology, type, items = {}) {
|
||||||
|
return Object.keys(technology[type]).reduce((technologies, key) => {
|
||||||
|
const patterns = technology[type][key] || []
|
||||||
|
const values = items[key] || []
|
||||||
|
|
||||||
|
patterns.forEach((pattern) => {
|
||||||
|
values.forEach((value) => {
|
||||||
|
if (pattern.regex.test(value)) {
|
||||||
|
technologies.push({ technology, pattern, match: value })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return technologies
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof module !== 'undefined') {
|
||||||
|
module.exports = Wappalyzer
|
||||||
|
}
|
Loading…
Reference in new issue