Merge branch 'refactoring'

main
Elbert Alias 4 years ago
commit e2c507b2d1

@ -16,6 +16,6 @@ echo "Validating icons..."
./bin/validate-icons ./bin/validate-icons
echo "Running tests..." # echo "Running tests..."
#
yarn run test # yarn run test

@ -2963,7 +2963,6 @@
], ],
"cpe": "cpe:/a:docker:engine", "cpe": "cpe:/a:docker:engine",
"icon": "Docker.svg", "icon": "Docker.svg",
"implies": "Linux",
"html": "<!-- This comment is expected by the docker HEALTHCHECK -->", "html": "<!-- This comment is expected by the docker HEALTHCHECK -->",
"website": "https://www.docker.com/" "website": "https://www.docker.com/"
}, },

@ -238,6 +238,8 @@ class Site {
this.listeners = {} this.listeners = {}
this.headers = {} this.headers = {}
this.pages = []
} }
async init() {} async init() {}
@ -285,6 +287,8 @@ class Site {
const page = await this.browser.newPage() const page = await this.browser.newPage()
this.pages.push(page)
page.setDefaultTimeout(this.options.maxWait) page.setDefaultTimeout(this.options.maxWait)
await page.setRequestInterception(true) await page.setRequestInterception(true)
@ -402,6 +406,10 @@ class Site {
// Validate response // Validate response
if (!this.analyzedUrls[url.href].status) { if (!this.analyzedUrls[url.href].status) {
await page.close()
this.log('Page closed')
throw new Error('NO_RESPONSE') throw new Error('NO_RESPONSE')
} }
@ -452,6 +460,10 @@ class Site {
[] []
) )
await page.close()
this.log('Page closed')
this.emit('goto', url) this.emit('goto', url)
return reducedLinks return reducedLinks
@ -537,6 +549,24 @@ class Site {
} }
}) })
} }
async destroy() {
await Promise.all(
this.pages.map(async (page) => {
if (page) {
try {
await page.close()
this.log('Page closed')
} catch (error) {
// Continue
}
}
})
)
this.log('Site closed')
}
} }
module.exports = Driver module.exports = Driver

@ -13,7 +13,7 @@
"software" "software"
], ],
"homepage": "https://www.wappalyzer.com", "homepage": "https://www.wappalyzer.com",
"version": "5.10.3", "version": "6.0.3",
"author": "Wappalyzer", "author": "Wappalyzer",
"license": "MIT", "license": "MIT",
"repository": { "repository": {

@ -8,7 +8,7 @@
"optionUpgradeMessage": { "message": "Tell me about upgrades" }, "optionUpgradeMessage": { "message": "Tell me about upgrades" },
"optionDynamicIcon": { "message": "Use technology icon instead of Wappalyzer logo" }, "optionDynamicIcon": { "message": "Use technology icon instead of Wappalyzer logo" },
"optionTracking": { "message": "Anonymously send identified technologies to wappalyzer.com" }, "optionTracking": { "message": "Anonymously send identified technologies to wappalyzer.com" },
"optionThemeMode": { "message": "Enable dark mode compatibility." }, "optionThemeMode": { "message": "Enable dark mode compatibility" },
"nothingToDo": { "message": "Nothing to do here." }, "nothingToDo": { "message": "Nothing to do here." },
"noAppsDetected": { "message": "No technologies detected." }, "noAppsDetected": { "message": "No technologies detected." },
"categoryPin": { "message": "Always show icon" }, "categoryPin": { "message": "Always show icon" },

@ -1,108 +0,0 @@
body {
color: #303942;
cursor: default;
direction: __MSG_@@bidi_dir__;
font-family: Helvetica, Arial, sans-serif;
font-size: .8rem;
line-height: 1.4rem;
margin: 0;
}
p {
margin: 0 0 1rem 0;
}
h1, h2, h3 {
font-weight: normal;
line-height: 1;
}
h1 {
border-bottom: 1px solid #dbdbdb;
font-size: 1.5rem;
margin: 0 0 1.5rem 0;
padding: 1rem 0 1.5rem 0;
}
h2 {
font-size: 1.3em;
margin-bottom: 0.4em;
}
h3 {
color: black;
font-size: 1.2em;
margin-bottom: 0.5em;
}
a {
color: rgb(17, 85, 204);
text-decoration: underline;
}
label {
display: block;
}
button {
background: #4608ad;
border: none;
border-radius: .2rem;
color: white;
font-size: inherit;
padding: 0 .6rem;
line-height: 1.8rem;
}
a:active {
color: rgb(5, 37, 119);
}
.hero {
background: linear-gradient(160deg, #32067c, #150233);
padding: 1.5rem 1.5rem 1rem 1.5rem;
}
.hero img {
height: 3rem;
}
.container {
margin: 0 auto;
max-width: 800px;
}
.content {
padding: 1.5rem;
}
#options-saved {
display: none;
margin-left: .5rem;
-webkit-animation: fadeout 2s;
}
#about {
border-top: 1px solid #dbdbdb;
margin-top: 1.5rem;
padding: 1.5rem 0 0 0;
}
#about img {
margin-right: .2rem;
vertical-align: middle;
}
#about button {
background: white;
border: 1px solid #dbdbdb;
cursor: pointer;
color: #303942;
margin-bottom: .5rem;
margin-inline-end: 1rem;
}
@-webkit-keyframes fadeout {
from { opacity: 1; }
to { opacity: 0; }
}

@ -1,287 +0,0 @@
body {
background: #fff;
direction: __MSG_@@bidi_dir__;
font-family: Helvetica, Arial, sans-serif;
font-size: .8rem;
margin: 0;
min-width: 30rem;
}
.header {
align-items: center;
border-bottom: 1px solid #dbdbdb;
height: 4rem;
display: flex;
}
.header__link:focus {
outline: none;
}
.header__logo {
display: inline-block;
margin: .2rem 1.5rem 0 1.5rem;
-webkit-backface-visibility: hidden;
-webkit-transform: translateZ(0) scale(1.0, 1.0);
transform: translateZ(0);
height: 2rem;
}
.header__logo--dark {
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;
}
.detected {
columns: 2;
column-gap: 1.5rem;
line-height: 1.4rem;
}
.detected__category {
page-break-inside: avoid;
break-inside: avoid-column;
padding-bottom: 1rem;
}
.detected__category-name {
display: block;
}
.detected__category-link {
color: #4608ad;
font-weight: bold;
line-height: 2rem;
text-decoration: none;
}
.detected__category-link:hover {
color: #4a4a4a;
}
.detected__category-pin-wrapper {
margin-left: .2rem;
margin-right: .2rem;
}
.detected__category-pin {
cursor: pointer;
display: none;
height: 16px;
margin-left: .2rem;
width: 16px;
vertical-align: middle;
}
.detected__category:hover .detected__category-pin--inactive {
display: inline-block;
}
.detected__category-pin-wrapper--active .detected__category-pin--inactive,
.detected__category-pin-wrapper:hover .detected__category-pin--inactive {
display: none !important;
}
.detected__category-pin-wrapper--active .detected__category-pin--active,
.detected__category-pin-wrapper:hover .detected__category-pin--active {
display: inline-block;
}
.detected__app {
color: #4a4a4a;
display: block;
line-height: 1.7rem;
text-decoration: none;
}
.detected__app:focus {
display: block;
outline: 0;
}
.detected__app-icon {
display: inline-block;
height: 16px;
margin-inline-end: .5rem;
vertical-align: -.2rem;
width: 16px;
}
.detected__app-name {
}
.detected__app-version, .detected__app-confidence {
background: #eee;
border-radius: 3px;
font-size: .7rem;
margin-left: .3rem;
padding: .1rem .2rem;
}
.detected__app:hover .detected__app-name {
border-bottom: 1px solid #4a4a4a;
}
.detected__app:hover .detected__app-version,
.detected__app:hover .detected__app-confidence {
border-bottom: 1px solid white;
}
.detected-app {
padding: 7px 0;
}
.detected-app:first-child {
padding-top: 0;
}
.detected-app:last-child {
border: none;
padding-bottom: 0;
}
.empty {
display: flex;
height: 5rem;
margin-bottom: 1rem;
align-items: center;
justify-content: center;
}
.empty__text {
}
.terms {
align-items: center;
display: flex;
flex-direction: column;
justify-content: center;
margin-bottom: 1rem;
height: 12rem;
width: 36rem;
}
.terms__wrapper {
display: none;
height: 100%;
width: 100%;
}
.terms__wrapper--active {
display: block;
}
.terms__content {
font-size: .9rem;
line-height: 150%;
text-align: center;
margin-bottom: 1rem;
width: 80%;
}
.terms__accept {
background-color: #4608ad;
border: none;
border-radius: 3px;
color: white;
cursor: pointer;
font-size: .9rem;
padding: .8rem 3rem;
}
.terms__accept:hover {
background-color: #4107a1;
}
.terms__privacy {
color: #4608ad;
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 rgba(255, 255, 255, .2);
}
.theme-mode-sync .header__logo--dark {
display: inline-block;
}
.theme-mode-sync .header__logo--light {
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 {
color: #fff;
}
.theme-mode-sync .detected__app {
color: rgba(255, 255, 255, .8);
}
.theme-mode-sync .detected__category-link:hover {
color: white;
border-bottom: 1px solid white;
}
.theme-mode-sync .detected__app-version, .theme-mode-sync .detected__app-confidence {
background-color: #4608ad;
}
.theme-mode-sync .detected__app:hover .detected__app-name {
border-bottom: 1px solid white;
}
.theme-mode-sync .detected__app:hover .theme-mode-sync .detected__app-version,
.theme-mode-sync .detected__app:hover .theme-mode-sync .detected__app-confidence {
border-bottom: none;
}
.theme-mode-sync .terms__accept,
.theme-mode-sync .terms__privacy {
color: white;
}
}

@ -0,0 +1,271 @@
:root {
--color-primary: #4608ad;
--color-primary-darken: #32067c;
--color-secondary: #e0e0e0;
--color-secondary-dark: rgba(255, 255, 255, .2);
--color-text: #4a4a4a;
--color-text-dark: rgba(255, 255, 255, .8);
--color-light-grey: #fafafa;
}
body {
background: #fff;
color: var(--color-text);
direction: __MSG_@@bidi_dir__;
font-family: Helvetica, Arial, sans-serif;
font-size: .9rem;
line-height: 1.5rem;
margin: 0;
min-width: 35rem;
}
a {
color: var(--color-primary);
outline: none;
text-decoration: none;
}
a:focus {
outline: none;
}
a:hover {
text-decoration: underline;
}
.header {
align-items: center;
border-bottom: 1px solid var(--color-secondary);
display: flex;
height: 4.5rem;
}
.header__logo {
height: 2.5rem;
margin: .5rem 1.5rem 0 1.5rem;
}
.header__logo--dark {
display: none;
}
.footer {
align-items: center;
justify-content: space-between;
border-top: 1px solid var(--color-secondary);
height: 3rem;
display: flex;
padding: 0 1.5rem;
}
.alerts__icon {
color: var(--color-primary);
height: 1.1rem;
margin-right: .5rem;
vertical-align: text-bottom;
width: 1.1rem;
}
.footer__settings {
color: var(--color-primary);
cursor: pointer;
height: 1.1rem;
vertical-align: middle;
width: 1.1rem;
}
.detections {
columns: 2;
column-gap: 1.5rem;
padding: 1.5rem 1.5rem .5rem 1.5rem;
}
.empty {
opacity: .3;
padding: 3rem 1.5rem .5rem 1.5rem;
text-align: center;
}
.category {
page-break-inside: avoid;
break-inside: avoid-column;
padding-bottom: 1rem;
}
.category__link {
font-weight: bold;
line-height: 2rem;
text-decoration: none;
}
.category__pin {
color: var(--color-primary);
cursor: pointer;
display: none;
visibility: hidden;
height: 1.1rem;
vertical-align: middle;
width: 1.1rem;
}
.category__pin--outline {
display: inline;
}
.category__pin--active {
visibility: visible;
}
.category__pin.category__pin--active {
display: inline
}
.category__pin--outline.category__pin--active {
display: none
}
.category__heading:hover .category__pin {
visibility: visible;
}
.technology {
display: block;
line-height: 1.7rem;
}
.technology__heading {
display: flex;
align-items: center;
margin-bottom: .2rem;
}
.technology__icon {
height: 16px;
margin-right: .5rem;
width: 16px;
vertical-align: middle;
}
.technology__link {
color: var(--color-text);
}
.technology__confidence {
opacity: .5;
font-size: .7rem;
margin-left: .2rem;
}
.technology__version {
background: var(--color-secondary);
border-radius: 3px;
font-size: .7rem;
padding: .1rem .3rem;
margin-left: .4rem;
vertical-align: middle;
}
.terms {
align-items: center;
display: flex;
flex-direction: column;
justify-content: center;
margin-bottom: 1rem;
height: 12rem;
width: 36rem;
}
.terms__wrapper {
display: none;
height: 100%;
width: 100%;
}
.terms__wrapper--active {
display: block;
}
.terms__content {
font-size: .9rem;
line-height: 150%;
text-align: center;
margin-bottom: 1rem;
width: 80%;
}
.terms__accept {
background-color: #4608ad;
border: none;
border-radius: 3px;
color: white;
cursor: pointer;
font-size: .9rem;
padding: .8rem 3rem;
}
.terms__accept:hover {
background-color: #4107a1;
}
.terms__privacy {
margin-top: 1rem;
}
.options {
padding: 1.5rem 1.5rem 1rem 1.5rem;
}
.options__label {
display: block;
margin-bottom: .5rem;
}
@media (prefers-color-scheme: dark) {
body.theme-mode {
background: var(--color-primary-darken);
color: var(--color-text-dark);
}
.theme-mode a {
color: var(--color-text-dark);
}
.theme-mode .header {
border-color: var(--color-secondary-dark)
}
.theme-mode .header__logo {
display: none;
}
.theme-mode .header__logo--dark {
display: inline-block;
}
.theme-mode .category__link {
color: #fff;
}
.theme-mode .category__pin {
color: #fff
}
.theme-mode .technology__confidence {
}
.theme-mode .technology__version {
background: var(--color-primary);
}
.theme-mode .footer {
border-color: var(--color-secondary-dark)
}
.theme-mode .footer__settings {
color: var(--color-text-dark);
}
.theme-mode .alerts__icon {
color:var(--color-text-dark);
}
}

@ -4,8 +4,8 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<script src="../node_modules/webextension-polyfill/dist/browser-polyfill.js"></script>
<script src="../js/wappalyzer.js"></script> <script src="../js/wappalyzer.js"></script>
<script src="../js/utils.js"></script>
<script src="../js/driver.js"></script> <script src="../js/driver.js"></script>
<script src="../js/lib/network.js"></script> <script src="../js/lib/network.js"></script>
</head> </head>

@ -1,69 +1,41 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title data-i18n="options">Wappalyzer options</title> <title data-i18n="options"></title>
<link rel="icon" href="../images/icon_32.png">
<link rel="stylesheet" href="../css/options.css"> <link rel="stylesheet" href="../css/styles.css">
<script src="../node_modules/webextension-polyfill/dist/browser-polyfill.js"></script> <script src="../js/utils.js"></script>
<script src="../js/wappalyzer.js"></script>
<script src="../js/options.js"></script> <script src="../js/options.js"></script>
</head> </head>
<body> <body>
<div class="hero"> <div class="options">
<div class="container"> <label class="options__label">
<img src="../images/logo-white.svg"> <input class="options__checkbox" type="checkbox">
</div>
</div>
<div class="content">
<div class="container">
<h1 data-i18n="options">Options</h1>
<p> <span data-i18n="optionUpgradeMessage">&nbsp;</span>
<label for="option-upgrade-message">
<input id="option-upgrade-message" type="checkbox">
<span data-i18n="optionUpgradeMessage">Tell me about upgrades</span>
</label> </label>
<label for="option-dynamic-icon">
<input id="option-dynamic-icon" type="checkbox"> <label class="options__label">
<span data-i18n="optionDynamicIcon">Use application icon instead of Wappalyzer logo</span> <input class="options__checkbox" type="checkbox">
</label>
<label for="option-tracking"> <span data-i18n="optionDynamicIcon">&nbsp;</span>
<input id="option-tracking" type="checkbox">
<span data-i18n="optionTracking">Anonymously send reports on detected applications to wappalyzer.com for research</span>
</label> </label>
<label for="option-theme-mode">
<input id="option-theme-mode" type="checkbox"> <label class="options__label">
<span data-i18n="optionThemeMode">Enable dark mode compatibility</span> <input class="options__checkbox" type="checkbox">
<span data-i18n="optionTracking">&nbsp;</span>
</label> </label>
</p>
<div id="about"> <label class="options__label">
<p> <input class="options__checkbox" type="checkbox">
<button id="github">
<img src="../images/github.png" width="16" height="16" alt="GitHub icon" /> <span data-i18n="optionThemeMode">&nbsp;</span>
<span data-i18n="github">Fork Wappalyzer on GitHub!</span> </label>
</button>
<button id="twitter">
<img src="../images/twitter.png" width="16" height="16" alt="Twitter icon" />
<span data-i18n="twitter">Follow Wappalyzer on Twitter</span>
</button>
<button id="wappalyzer">
<img src="../images/icon_16.png" width="16" height="16" alt="Wappalyzer icon" />
<span data-i18n="website">Go to wappalyzer.com</span>
</button>
</p>
</div>
</div>
</div> </div>
</body> </body>
</html> </html>

@ -1,38 +1,80 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../css/popup.css"> <title data-i18n="options"></title>
<link rel="stylesheet" href="../css/styles.css">
<script src="../node_modules/webextension-polyfill/dist/browser-polyfill.js"></script> <script src="../js/utils.js"></script>
<script src="../js/lib/jsontodom.js"></script>
<script src="../js/popup.js"></script> <script src="../js/popup.js"></script>
</head> </head>
<body> <body>
<div class="header"> <div class="header">
<a href="https://www.wappalyzer.com/" class="header__link" target="_blank"> <a href="https://www.wappalyzer.com/" class="header__link">
<img alt="" class="header__logo header__logo--light" src="../images/logo-purple.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"> <img alt="" class="header__logo header__logo--dark" src="../images/logo-white.svg">
</a> </a>
</div> </div>
<div class="container">
<div class="terms__wrapper">
<div class="terms"> <div class="terms">
<div class="terms__content" data-i18n="termsContent"></div> <div class="terms__content" data-i18n="termsContent"></div>
<button class="terms__accept" data-i18n="termsAccept" /> <button class="terms__accept" data-i18n="termsAccept">&nbsp;</button>
<a class="terms__privacy" href="https://www.wappalyzer.com/privacy" data-i18n="privacyPolicy"></a> <a class="terms__privacy" href="https://www.wappalyzer.com/privacy" data-i18n="privacyPolicy"></a>
</div> </div>
<div class="empty" data-i18n="noAppsDetected"></div>
<div class="detections"></div>
<div data-template="category" class="category">
<div class="category__heading">
<a class="category__link" href="#"></a>
<svg class="category__pin category__pin--outline" viewBox="0 0 24 24">
<title data-i18n="categoryPin"></title>
<path fill="currentColor" d="M16,12V4H17V2H7V4H8V12L6,14V16H11.2V22H12.8V16H18V14L16,12M8.8,14L10,12.8V4H14V12.8L15.2,14H8.8Z" />
</svg>
<svg class="category__pin" viewBox="0 0 24 24">
<title data-i18n="categoryPin"></title>
<path fill="currentColor" d="M16,12V4H17V2H7V4H8V12L6,14V16H11.2V22H12.8V16H18V14L16,12Z" />
</svg>
</div>
<div class="technologies"></div>
</div>
<div data-template="technology" class="technology">
<div class="technology__heading">
<img class="technology__icon" alt="" src="../images/icons/default.svg" />
<a class="technology__link" href="#"></a>
<span>
<span class="technology__version">&nbsp;</span>
</span>
<span class="technology__confidence">&nbsp;</span>
</div> </div>
</div> </div>
<div class="footer"> <div class="footer">
<a class="footer__link" href="https://www.wappalyzer.com/alerts/manage" data-i18n="createAlert"></a> <div class="alerts">
<svg class="alerts__icon" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,8H4A2,2 0 0,0 2,10V14A2,2 0 0,0 4,16H5V20A1,1 0 0,0 6,21H8A1,1 0 0,0 9,20V16H12L17,20V4L12,8M21.5,12C21.5,13.71 20.54,15.26 19,16V8C20.53,8.75 21.5,10.3 21.5,12Z" />
</svg>
<a class="alerts__link" href="https://www.wappalyzer.com/alerts/manage" data-i18n="createAlert"></a>
</div>
<svg class="footer__settings" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z" />
</svg>
</div> </div>
</body> </body>
</html> </html>

@ -1,20 +1,22 @@
/** global: browser */ 'use strict'
/** global: XMLSerializer */
/* global browser */
/* eslint-env browser */ /* eslint-env browser */
/* globals chrome */
const port = browser.runtime.connect({ const Content = {
name: 'content.js' async init() {
})
;(async function() {
if (typeof browser !== 'undefined' && typeof document.body !== 'undefined') {
await new Promise((resolve) => setTimeout(resolve, 1000)) await new Promise((resolve) => setTimeout(resolve, 1000))
try { Content.port = chrome.runtime.connect({ name: 'content.js' })
port.postMessage({ id: 'init' })
Content.port.onMessage.addListener(({ func, args }) => {
const onFunc = `on${func.charAt(0).toUpperCase() + func.slice(1)}`
if (Content[onFunc]) {
Content[onFunc](args)
}
})
try {
// HTML // HTML
let html = new XMLSerializer().serializeToString(document) let html = new XMLSerializer().serializeToString(document)
@ -23,9 +25,7 @@ const port = browser.runtime.connect({
const maxRows = 3000 const maxRows = 3000
const rows = html.length / maxCols const rows = html.length / maxCols
let i for (let i = 0; i < rows; i += 1) {
for (i = 0; i < rows; i += 1) {
if (i < maxRows / 2 || i > rows - maxRows / 2) { if (i < maxRows / 2 || i > rows - maxRows / 2) {
chunks.push(html.slice(i * maxCols, (i + 1) * maxCols)) chunks.push(html.slice(i * maxCols, (i + 1) * maxCols))
} }
@ -33,62 +33,83 @@ const port = browser.runtime.connect({
html = chunks.join('\n') html = chunks.join('\n')
// Scripts const language =
const scripts = Array.prototype.slice document.documentElement.getAttribute('lang') ||
.apply(document.scripts) document.documentElement.getAttribute('xml:lang') ||
.filter((script) => script.src) (await new Promise((resolve) =>
.map((script) => script.src) chrome.i18n.detectLanguage(html, ({ languages }) =>
resolve(
languages
.filter(({ percentage }) => percentage >= 75)
.map(({ language: lang }) => lang)[0]
)
)
))
// Script tags
const scripts = Array.from(document.scripts)
.filter(({ src }) => src)
.map(({ src }) => src)
.filter((script) => script.indexOf('data:text/javascript;') !== 0) .filter((script) => script.indexOf('data:text/javascript;') !== 0)
port.postMessage({ id: 'analyze', subject: { html, scripts } }) // Meta tags
const meta = Array.from(document.querySelectorAll('meta'))
.map((meta) => ({
key: meta.getAttribute('name') || meta.getAttribute('property'),
value: meta.getAttribute('content')
}))
.filter(({ value }) => value)
Content.port.postMessage({
func: 'onContentLoad',
args: [location.href, { html, scripts, meta }, language]
})
// JavaScript variables Content.port.postMessage({ func: 'getTechnologies' })
} catch (error) {
Content.port.postMessage({ func: 'error', args: [error, 'content.js'] })
}
},
onGetTechnologies(technologies) {
const script = document.createElement('script') const script = document.createElement('script')
script.onload = () => { script.onload = () => {
const onMessage = (event) => { const onMessage = ({ data }) => {
if (event.data.id !== 'js') { if (!data.wappalyzer || !data.wappalyzer.js) {
return return
} }
window.removeEventListener('message', onMessage) window.removeEventListener('message', onMessage)
port.postMessage({ id: 'analyze', subject: { js: event.data.js } }) Content.port.postMessage({
func: 'analyzeJs',
args: [location.href, data.wappalyzer.js]
})
script.remove() script.remove()
} }
window.addEventListener('message', onMessage) window.addEventListener('message', onMessage)
port.postMessage({ id: 'get_js_patterns' }) window.postMessage({
wappalyzer: {
technologies: technologies
.filter(({ js }) => Object.keys(js).length)
.filter(({ name }) => name === 'jQuery')
.map(({ name, js }) => ({ name, chains: Object.keys(js) }))
}
})
} }
script.setAttribute('src', browser.extension.getURL('js/inject.js')) script.setAttribute('src', chrome.extension.getURL('js/inject.js'))
document.body.appendChild(script) document.body.appendChild(script)
} catch (error) {
port.postMessage({ id: 'log', subject: error })
}
}
})()
port.onMessage.addListener((message) => {
switch (message.id) {
case 'get_js_patterns':
postMessage(
{
id: 'patterns',
patterns: message.response.patterns
},
window.location.href
)
break
default:
// Do nothing
} }
}) }
// https://stackoverflow.com/a/44774834 if (/complete|interactive|loaded/.test(document.readyState)) {
// https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/tabs/executeScript#Return_value Content.init()
undefined // eslint-disable-line no-unused-expressions } else {
document.addEventListener('DOMContentLoaded', Content.init)
}

@ -1,121 +1,160 @@
/** 'use strict'
* WebExtension driver
*/
/* eslint-env browser */ /* eslint-env browser */
/* global browser, chrome, Wappalyzer */ /* globals chrome, Wappalyzer, Utils */
/** global: browser */ const {
/** global: chrome */ setTechnologies,
/** global: fetch */ setCategories,
/** global: Wappalyzer */ analyze,
analyzeManyToMany,
resolve
} = Wappalyzer
const { agent, promisify, getOption, setOption } = Utils
const expiry = 1000 * 60 * 60 * 24
const Driver = {
lastPing: Date.now(),
async init() {
chrome.runtime.onConnect.addListener(Driver.onRuntimeConnect)
await Driver.loadTechnologies()
const hostnameCache = (await getOption('hostnames')) || {}
Driver.cache = {
hostnames: Object.keys(hostnameCache).reduce(
(cache, hostname) => ({
...cache,
[hostname]: {
...hostnameCache[hostname],
detections: hostnameCache[hostname].detections.map(
({
pattern: { regex, confidence, version },
match,
technology: name
}) => ({
pattern: {
regex: new RegExp(regex, 'i'),
confidence,
version
},
match,
technology: Wappalyzer.technologies.find(
({ name: _name }) => name === _name
)
})
)
}
}),
{}
),
tabs: {},
robots: (await getOption('robots')) || {},
ads: (await getOption('ads')) || []
}
const wappalyzer = new Wappalyzer() chrome.webRequest.onCompleted.addListener(
Driver.onWebRequestComplete,
{ urls: ['http://*/*', 'https://*/*'], types: ['main_frame'] },
['responseHeaders']
)
chrome.tabs.onRemoved.addListener((id) => (Driver.cache.tabs[id] = null))
},
const tabCache = {} log(message, source = 'driver', type = 'log') {
const robotsTxtQueue = {} // eslint-disable-next-line no-console
console[type](`wappalyzer | ${source} |`, message)
},
let categoryOrder = [] warn(message, source = 'driver') {
Driver.log(message, source, 'warn')
},
browser.tabs.onRemoved.addListener((tabId) => { error(error, source = 'driver') {
tabCache[tabId] = null Driver.log(error, source, 'error')
}) },
function userAgent() { open(url, active = true) {
const url = chrome.extension.getURL('/') chrome.tabs.create({ url, active })
},
if (url.startsWith('moz-')) { async loadTechnologies() {
return 'firefox' try {
} const { apps: technologies, categories } = await (
await fetch(chrome.extension.getURL('apps.json'))
).json()
if (url.startsWith('ms-browser')) { setTechnologies(technologies)
return 'edge' setCategories(categories)
} catch (error) {
Driver.error(error)
} }
},
return 'chrome' post(url, body) {
}
/**
* Get a value from localStorage
*/
function getOption(name, defaultValue = null) {
return new Promise(async (resolve, reject) => {
let value = defaultValue
try { try {
const option = await browser.storage.local.get(name) return fetch(url, {
method: 'POST',
if (option[name] !== undefined) { body: JSON.stringify(body)
value = option[name] })
}
} catch (error) { } catch (error) {
wappalyzer.log(error.message, 'driver', 'error') throw new Error(error.message || error.toString())
return reject(error.message)
} }
},
return resolve(value) async analyzeJs(href, js) {
}) const url = new URL(href)
}
await Driver.onDetect(
url,
Array.prototype.concat.apply(
[],
await Promise.all(
js.map(({ name, chain, value }) =>
analyzeManyToMany(
Wappalyzer.technologies.find(({ name: _name }) => name === _name),
'js',
{ [chain]: [value] }
)
)
)
)
)
},
/** onRuntimeConnect(port) {
* Set a value in localStorage Driver.log(`Connected to ${port.name}`)
*/
function setOption(name, value) {
return new Promise(async (resolve, reject) => {
try {
await browser.storage.local.set({ [name]: value })
} catch (error) {
wappalyzer.log(error.message, 'driver', 'error')
return reject(error.message) port.onMessage.addListener(async ({ func, args }) => {
if (!func) {
return
} }
return resolve() Driver.log({ port: port.name, func, args })
})
}
/**
* Open a tab
*/
function openTab(args) {
browser.tabs.create({
url: args.url,
active: args.background === undefined || !args.background
})
}
/** if (!Driver[func]) {
* Make a POST request Driver.error(new Error(`Method does not exist: Driver.${func}`))
*/
async function post(url, body) {
try {
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify(body)
})
wappalyzer.log(`POST ${url}: ${response.status}`, 'driver') return
} catch (error) {
wappalyzer.log(`POST ${url}: ${error}`, 'driver', 'error')
} }
}
// Capture response headers port.postMessage({
browser.webRequest.onCompleted.addListener( func,
async (request) => { args: await Driver[func].call(port.sender, ...(args || []))
const headers = {} })
})
},
async onWebRequestComplete(request) {
if (request.responseHeaders) { if (request.responseHeaders) {
const url = wappalyzer.parseUrl(request.url) const headers = {}
let tab
try { try {
;[tab] = await browser.tabs.query({ url: [url.href] }) const url = new URL(request.url)
} catch (error) {
wappalyzer.log(error, 'driver', 'error') const [tab] = await promisify(chrome.tabs, 'query', { url: [url.href] })
}
if (tab) { if (tab) {
request.responseHeaders.forEach((header) => { request.responseHeaders.forEach((header) => {
@ -132,314 +171,313 @@ browser.webRequest.onCompleted.addListener(
headers['content-type'] && headers['content-type'] &&
/\/x?html/.test(headers['content-type'][0]) /\/x?html/.test(headers['content-type'][0])
) { ) {
wappalyzer.analyze(url, { headers }, { tab }) await Driver.onDetect(
} url,
await analyze(url.href, { headers }, { tab })
)
} }
} }
}, } catch (error) {
{ urls: ['http://*/*', 'https://*/*'], types: ['main_frame'] }, Driver.error(error)
['responseHeaders']
)
browser.runtime.onConnect.addListener((port) => {
port.onMessage.addListener(async (message) => {
if (message.id === undefined) {
return
} }
if (message.id !== 'log') {
wappalyzer.log(`Message from ${port.name}: ${message.id}`, 'driver')
} }
},
const pinnedCategory = await getOption('pinnedCategory') async onContentLoad(href, items, language) {
try {
const url = wappalyzer.parseUrl(port.sender.tab ? port.sender.tab.url : '') const url = new URL(href)
const cookies = await browser.cookies.getAll({ items.cookies = await promisify(chrome.cookies, 'getAll', {
domain: `.${url.hostname}` domain: `.${url.hostname}`
}) })
let response await Driver.onDetect(url, await analyze(href, items), language, true)
} catch (error) {
switch (message.id) { Driver.error(error)
case 'log': }
wappalyzer.log(message.subject, message.source) },
break getTechnologies() {
case 'init': return Wappalyzer.technologies
wappalyzer.analyze(url, { cookies }, { tab: port.sender.tab }) },
break async onDetect(url, detections = [], language, incrementHits = false) {
case 'analyze': if (!detections.length) {
if (message.subject.html) { return
browser.i18n }
.detectLanguage(message.subject.html)
.then(({ languages }) => {
const language = languages
.filter(({ percentage }) => percentage >= 75)
.map(({ language: lang }) => lang)[0]
message.subject.language = language const { hostname, href } = url
wappalyzer.analyze(url, message.subject, { tab: port.sender.tab }) // Cache detections
const cache = (Driver.cache.hostnames[hostname] = {
...(Driver.cache.hostnames[hostname] || {
detections: [],
hits: 0
}),
dateTime: Date.now()
}) })
} else {
wappalyzer.analyze(url, message.subject, { tab: port.sender.tab })
}
await setOption('hostnameCache', wappalyzer.hostnameCache)
break // Remove duplicates
case 'ad_log': cache.detections = cache.detections = cache.detections.concat(detections)
wappalyzer.cacheDetectedAds(message.subject)
break cache.detections.filter(
case 'get_apps': ({ technology: { name }, pattern: { regex } }, index) =>
response = { cache.detections.findIndex(
tabCache: tabCache[message.tab.id], ({ technology: { name: _name }, pattern: { regex: _regex } }) =>
apps: wappalyzer.apps, name === _name && (!regex || regex.toString() === _regex.toString())
categories: wappalyzer.categories, ) === index
pinnedCategory, )
termsAccepted:
userAgent() === 'chrome' ||
(await getOption('termsAccepted', false))
}
break cache.hits += incrementHits ? 1 : 0
case 'set_option': cache.language = cache.language || language
await setOption(message.key, message.value)
break // Expire cache
case 'get_js_patterns': Driver.cache.hostnames = Object.keys(Driver.cache.hostnames).reduce(
response = { (hostnames, hostname) => {
patterns: wappalyzer.jsPatterns const cache = Driver.cache.hostnames[hostname]
}
break if (cache.dateTime > Date.now() - expiry) {
case 'update_theme_mode': hostnames[hostname] = cache
// Sync theme mode to popup.
response = {
themeMode: await getOption('themeMode', false)
} }
break return hostnames
default: },
// Do nothing {}
} )
if (response) { await setOption(
port.postMessage({ 'hostnames',
id: message.id, Object.keys(Driver.cache.hostnames).reduce(
response (cache, hostname) => ({
...cache,
[hostname]: {
...Driver.cache.hostnames[hostname],
detections: Driver.cache.hostnames[hostname].detections.map(
({
pattern: { regex, confidence, version },
match,
technology: { name: technology }
}) => ({
technology,
pattern: {
regex: regex.source,
confidence,
version
},
match
}) })
)
} }
}) }),
}) {}
)
)
wappalyzer.driver.document = document const resolved = resolve(Driver.cache.hostnames[hostname].detections)
/** await Driver.setIcon(url, resolved)
* Log messages to console
*/
wappalyzer.driver.log = (message, source, type) => {
const log = ['warn', 'error'].includes(type) ? type : 'log'
console[log](`[wappalyzer ${type}]`, `[${source}]`, message) // eslint-disable-line no-console const tabs = await promisify(chrome.tabs, 'query', { url: [href] })
}
/** tabs.forEach(({ id }) => (Driver.cache.tabs[id] = resolved))
* Display apps
*/
wappalyzer.driver.displayApps = async (detected, meta, context) => {
const { tab } = context
if (tab === undefined) { Driver.log({ hostname, technologies: resolved })
return
}
tabCache[tab.id] = tabCache[tab.id] || { await Driver.ping()
detected: [] },
}
async onAd(ad) {
Driver.cache.ads.push(ad)
tabCache[tab.id].detected = detected await setOption('ads', Driver.cache.ads)
},
const pinnedCategory = await getOption('pinnedCategory') async setIcon(url, technologies) {
const dynamicIcon = await getOption('dynamicIcon', true) const dynamicIcon = await getOption('dynamicIcon', true)
let found = false let icon = 'default.svg'
// Find the main application to display if (dynamicIcon) {
;[pinnedCategory].concat(categoryOrder).forEach((match) => { const pinnedCategory = parseInt(await getOption('pinnedCategory'), 10)
Object.keys(detected).forEach((appName) => {
const app = detected[appName]
app.props.cats.forEach((category) => { const pinned = technologies.find(({ categories }) =>
if (category === match && !found) { categories.some(({ id }) => id === pinnedCategory)
let icon = )
app.props.icon && dynamicIcon ? app.props.icon : 'default.svg'
if (/\.svg$/i.test(icon)) { ;({ icon } = pinned ||
icon = `converted/${icon.replace(/\.svg$/, '.png')}` technologies.sort(({ categories: a }, { categories: b }) => {
} const max = (value) =>
value.reduce((max, { priority }) => Math.max(max, priority))
try { return max(a) > max(b) ? -1 : 1
browser.pageAction.setIcon({ })[0] || { icon })
tabId: tab.id,
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 const tabs = await promisify(chrome.tabs, 'query', { url: [url.href] })
}
await Promise.all(
tabs.map(async ({ id: tabId }) => {
await promisify(chrome.pageAction, 'setIcon', {
tabId,
path: chrome.extension.getURL(
`../images/icons/${
/\.svg$/i.test(icon)
? `converted/${icon.replace(/\.svg$/, '.png')}`
: icon
}`
)
}) })
chrome.pageAction.show(tabId)
}) })
)
},
async getDetections() {
const [{ id }] = await promisify(chrome.tabs, 'query', {
active: true,
currentWindow: true
}) })
browser.pageAction.show(tab.id) return Driver.cache.tabs[id]
} },
/** async getRobots(hostname, secure = false) {
* Fetch and cache robots.txt for host if (!(await getOption('tracking', true))) {
*/ return
wappalyzer.driver.getRobotsTxt = async (host, secure = false) => {
if (robotsTxtQueue[host]) {
return robotsTxtQueue[host]
} }
const tracking = await getOption('tracking', true) if (typeof Driver.cache.robots[hostname] !== 'undefined') {
const robotsTxtCache = await getOption('robotsTxtCache', {}) return Driver.cache.robots[hostname]
}
robotsTxtQueue[host] = new Promise(async (resolve) => { try {
if (!tracking) { Driver.cache.robots[hostname] = await Promise.race([
return resolve([]) new Promise(async (resolve) => {
const response = await fetch(
`http${secure ? 's' : ''}://${hostname}/robots.txt`,
{
redirect: 'follow',
mode: 'no-cors'
} }
)
if (!response.ok) {
Driver.error(new Error(response.statusText))
if (host in robotsTxtCache) { resolve('')
return resolve(robotsTxtCache[host])
} }
const timeout = setTimeout(() => resolve([]), 3000) let agent
let response resolve(
(await response.text()).split('\n').reduce((disallows, line) => {
let matches = /^User-agent:\s*(.+)$/i.exec(line.trim())
try { if (matches) {
response = await fetch(`http${secure ? 's' : ''}://${host}/robots.txt`, { agent = matches[1].toLowerCase()
redirect: 'follow', } else if (agent === '*' || agent === 'wappalyzer') {
mode: 'no-cors' matches = /^Disallow:\s*(.+)$/i.exec(line.trim())
})
} catch (error) {
wappalyzer.log(error, 'driver', 'error')
return resolve([]) if (matches) {
disallows.push(matches[1])
}
} }
clearTimeout(timeout) return disallows
}, [])
)
}),
new Promise((resolve) => setTimeout(() => resolve(''), 5000))
])
Driver.cache.robots = Object.keys(Driver.cache.robots)
.slice(-50)
.reduce(
(cache, hostname) => ({
...cache,
[hostname]: Driver.cache.robots[hostname]
}),
{}
)
const robotsTxt = response.ok ? await response.text() : '' await setOption('robots', Driver.cache.robots)
robotsTxtCache[host] = Wappalyzer.parseRobotsTxt(robotsTxt) return Driver.cache.robots[hostname]
} catch (error) {
Driver.error(error)
}
},
await setOption('robotsTxtCache', robotsTxtCache) async checkRobots(href) {
const url = new URL(href)
delete robotsTxtQueue[host] if (url.protocol !== 'http:' && url.protocol !== 'https:') {
throw new Error('Invalid protocol')
}
return resolve(robotsTxtCache[host]) const robots = await Driver.getRobots(
}) url.hostname,
url.protocol === 'https:'
)
return robotsTxtQueue[host] if (robots.some((disallowed) => url.pathname.indexOf(disallowed) === 0)) {
} throw new Error('Disallowed')
}
},
/** async ping() {
* Anonymously track detected applications for research purposes
*/
wappalyzer.driver.ping = async (
hostnameCache = { expires: 0, hostnames: {} },
adCache = []
) => {
const tracking = await getOption('tracking', true) const tracking = await getOption('tracking', true)
const termsAccepted = const termsAccepted =
userAgent() === 'chrome' || (await getOption('termsAccepted', false)) agent === 'chrome' || (await getOption('termsAccepted', false))
if (tracking && termsAccepted) { if (tracking && termsAccepted) {
if ( const count = Object.keys(Driver.cache.hostnames).length
hostnameCache.hostnames &&
Object.keys(hostnameCache.hostnames).length
) {
post('https://api.wappalyzer.com/ping/v1/', hostnameCache.hostnames)
}
if (adCache.length) { if (count && (count >= 50 || Driver.lastPing < Date.now() - expiry)) {
post('https://ad.wappalyzer.com/log/wp/', adCache) await Driver.post(
} 'https://api.wappalyzer.com/ping/v1/',
Object.keys(Driver.cache.hostnames).reduce((hostnames, hostname) => {
// eslint-disable-next-line standard/computed-property-even-spacing
const { language, detections, hits } = Driver.cache.hostnames[
hostname
]
await setOption('robotsTxtCache', {}) hostnames[hostname] = hostnames[hostname] || {
applications: resolve(detections).reduce(
(technologies, { name, confidence, version }) => {
if (confidence === 100) {
technologies[name] = {
version,
hits
} }
}
// Init return technologies
;(async () => { }
// Technologies },
try { {}
const response = await fetch('../apps.json') ),
const json = await response.json() meta: {
language
wappalyzer.apps = json.apps }
wappalyzer.categories = json.categories
} catch (error) {
wappalyzer.log(`GET apps.json: ${error.message}`, 'driver', 'error')
} }
wappalyzer.parseJsPatterns() return hostnames
}, {})
categoryOrder = Object.keys(wappalyzer.categories)
.map((categoryId) => parseInt(categoryId, 10))
.sort(
(a, b) =>
wappalyzer.categories[a].priority - wappalyzer.categories[b].priority
) )
// Version check await setOption('hostnames', (Driver.cache.hostnames = {}))
const { version } = browser.runtime.getManifest()
const previousVersion = await getOption('version')
const upgradeMessage = await getOption('upgradeMessage', true)
if (previousVersion === null) { Driver.lastPing = Date.now()
openTab({
url: `${wappalyzer.config.websiteURL}installed`
})
} else if (version !== previousVersion && upgradeMessage) {
// openTab({
// url: `${wappalyzer.config.websiteURL}upgraded?v${version}`,
// background: true
// })
} }
await setOption('version', version) if (Driver.cache.ads.length > 50) {
await Driver.post('https://ad.wappalyzer.com/log/wp/', Driver.cache.ads)
// Hostname cache
wappalyzer.hostnameCache = await getOption('hostnameCache', {
expires: Date.now() + 1000 * 60 * 60 * 24,
hostnames: {}
})
// Run content script on all tabs
try {
const tabs = await browser.tabs.query({
url: ['http://*/*', 'https://*/*']
})
tabs.forEach(async (tab) => { await setOption('ads', (Driver.cache.ads = []))
try { }
await browser.tabs.executeScript(tab.id, {
file: '../js/content.js'
})
} catch (error) {
//
} }
})
} catch (error) {
wappalyzer.log(error, 'driver', 'error')
} }
})() }
Driver.init()

@ -1,62 +1,44 @@
/* eslint-env browser */ /* eslint-env browser */
/* eslint-disable no-restricted-globals, no-prototype-builtins */
;(() => { ;(function() {
try { try {
const detectJs = (chain) => { const onMessage = ({ data }) => {
const properties = chain.split('.') if (!data.wappalyzer) {
let value = properties.length ? window : null
for (let i = 0; i < properties.length; i += 1) {
const property = properties[i]
if (value && value.hasOwnProperty(property)) {
value = value[property]
} else {
value = null
break
}
}
return typeof value === 'string' || typeof value === 'number'
? value
: !!value
}
const onMessage = (event) => {
if (event.data.id !== 'patterns') {
return return
} }
removeEventListener('message', onMessage) const { technologies } = data.wappalyzer || {}
const patterns = event.data.patterns || {}
const js = {}
for (const appName in patterns) {
if (patterns.hasOwnProperty(appName)) {
js[appName] = {}
for (const chain in patterns[appName]) { removeEventListener('message', onMessage)
if (patterns[appName].hasOwnProperty(chain)) {
js[appName][chain] = {}
for (const index in patterns[appName][chain]) { postMessage({
const value = detectJs(chain) wappalyzer: {
js: technologies.reduce((results, { name, chains }) => {
chains.forEach((chain) => {
const value = chain
.split('.')
.reduce(
(value, method) =>
value && value.hasOwnProperty(method)
? value[method]
: undefined,
window
)
technologies.push({
name,
chain,
value:
typeof value === 'string' || typeof value === 'number'
? value
: !!value
})
})
if (value && patterns[appName][chain].hasOwnProperty(index)) { return technologies
js[appName][chain][index] = value }, [])
} }
} })
}
}
}
}
postMessage({ id: 'js', js }, window.location.href)
} }
addEventListener('message', onMessage) addEventListener('message', onMessage)

@ -120,17 +120,17 @@ var exports = {};
return dict; return dict;
}, },
sendToBackground: function(message, event, responseMessage) { sendToBackground: function(message, event, responseMessage) {
if ( typeof browser !== 'undefined' || typeof chrome !== 'undefined' ) { if ( typeof chrome !== 'undefined' ) {
var port = browser.runtime.connect({name:"adparser"}); var port = chrome.runtime.connect({name:"adparser"});
port.onMessage.addListener((message) => { port.onMessage.addListener((message) => {
if ( message && message.tracking_enabled ) { if ( message && typeof message.tracking_enabled !== 'undefined' ) {
if (message.tracking_enabled) {
utilCallback(); utilCallback();
} else { } else {
utilElseCallback(); utilElseCallback();
} }
}
}); });
port.postMessage(message); port.postMessage(message);
@ -1088,8 +1088,8 @@ var exports = {};
} }
function addBackgroundListener(event, callback) { function addBackgroundListener(event, callback) {
if ( typeof browser !== 'undefined' || typeof chrome !== 'undefined' ) { if ( typeof chrome !== 'undefined' ) {
browser.runtime.onMessage.addListener(function(msg) { chrome.runtime.onMessage.addListener(function(msg) {
if ( msg.event === event ) { if ( msg.event === event ) {
callback(msg); callback(msg);
} }
@ -1173,7 +1173,7 @@ if ( exports.utils.SCRIPT_IN_WINDOW_TOP ) {
})(window); })(window);
(function(adparser, pageUrl) { (function(adparser, pageUrl) {
function onAdFound(log) { function onAdFound(log) {
adparser.sendToBackground({ id: 'ad_log', subject: log }, 'ad_log', '', function(){}); adparser.sendToBackground({ func: 'onAd', args: [log] }, 'onAd', '', function(){});
} }
if ( adparser && adparser.inWindowTop ) { if ( adparser && adparser.inWindowTop ) {

@ -1,63 +0,0 @@
jsonToDOM.namespaces = {
html: 'http://www.w3.org/1999/xhtml',
xul: 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul',
};
jsonToDOM.defaultNamespace = jsonToDOM.namespaces.html;
function jsonToDOM(jsonTemplate, doc, nodes) {
function namespace(name) {
const reElemNameParts = /^(?:(.*):)?(.*)$/.exec(name);
return { namespace: jsonToDOM.namespaces[reElemNameParts[1]], shortName: reElemNameParts[2] };
}
// Note that 'elemNameOrArray' is: either the full element name (eg. [html:]div) or an array of elements in JSON notation
function tag(elemNameOrArray, elemAttr) {
// Array of elements? Parse each one...
if (Array.isArray(elemNameOrArray)) {
const frag = doc.createDocumentFragment();
Array.prototype.forEach.call(arguments, (thisElem) => {
frag.appendChild(tag(...thisElem));
});
return frag;
}
// Single element? Parse element namespace prefix (if none exists, default to defaultNamespace), and create element
const elemNs = namespace(elemNameOrArray);
const elem = doc.createElementNS(elemNs.namespace || jsonToDOM.defaultNamespace, elemNs.shortName);
// Set element's attributes and/or callback functions (eg. onclick)
for (const key in elemAttr) {
const val = elemAttr[key];
if (nodes && key == 'key') {
nodes[val] = elem;
continue;
}
const attrNs = namespace(key);
if (typeof val === 'function') {
// Special case for function attributes; don't just add them as 'on...' attributes, but as events, using addEventListener
elem.addEventListener(key.replace(/^on/, ''), val, false);
} else {
// Note that the default namespace for XML attributes is, and should be, blank (ie. they're not in any namespace)
elem.setAttributeNS(attrNs.namespace || '', attrNs.shortName, val);
}
}
// Create and append this element's children
const childElems = Array.prototype.slice.call(arguments, 2);
childElems.forEach((childElem) => {
if (childElem != null) {
elem.appendChild(
childElem instanceof doc.defaultView.Node ? childElem
: Array.isArray(childElem) ? tag(...childElem)
: doc.createTextNode(childElem),
);
}
});
return elem;
}
return tag(...jsonTemplate);
}

@ -1,19 +1,6 @@
(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
'use strict'; 'use strict';
(function() {
function isChrome() {
return (typeof chrome !== 'undefined' &&
window.navigator.userAgent.match(/Chrom(?:e|ium)\/([0-9\.]+)/));
}
var requiredBrowserApis = [
browser.webNavigation,
browser.tabs,
browser.webRequest,
browser.runtime
];
(function() {
var MIN_FF_MAJOR_VERSION = 51; var MIN_FF_MAJOR_VERSION = 51;
var areListenersRegistered = false; var areListenersRegistered = false;
@ -61,7 +48,7 @@
'washingtonpost.com' 'washingtonpost.com'
]; ];
var robotsTxtAllows = wappalyzer.robotsTxtAllows.bind(wappalyzer); var robotsTxtAllows = Driver.checkRobots;
if ( !String.prototype.endsWith ) { if ( !String.prototype.endsWith ) {
String.prototype.endsWith = function(searchString, position) { String.prototype.endsWith = function(searchString, position) {
var subjectString = this.toString(); var subjectString = this.toString();
@ -76,34 +63,7 @@
} }
function getFrame(getFrameDetails, callback) { function getFrame(getFrameDetails, callback) {
var gettingFrame = browser.webNavigation.getFrame(getFrameDetails); chrome.webNavigation.getFrame(getFrameDetails, callback);
gettingFrame.then(callback);
}
function ifBrowserValid(callback, elseCallback) {
if ( isChrome() ) {
callback();
} else if ( typeof browser !== 'undefined' ) {
try {
var gettingInfo = browser.runtime.getBrowserInfo();
gettingInfo.then(function(browserInfo) {
var browserVersion = parseInt(browserInfo.version.split('.')[0]);
if ( browserInfo.name === 'Firefox' &&
browserVersion >= MIN_FF_MAJOR_VERSION) {
callback();
} else {
elseCallback();
}
});
} catch (err) {
elseCallback();
}
} else {
elseCallback();
}
} }
function ifTrackingEnabled(details, ifCallback, elseCallback) { function ifTrackingEnabled(details, ifCallback, elseCallback) {
@ -112,24 +72,19 @@
allowedByRobotsTxt(details, ifCallback, elseCallback); allowedByRobotsTxt(details, ifCallback, elseCallback);
}; };
browser.storage.local.get('tracking').then(function(item) { Utils.getOption('tracking', true).then(function(tracking) {
if ( tracking ) {
if ( item.hasOwnProperty('tracking') ) {
if ( item.tracking ) {
fullIfCallback(); fullIfCallback();
} else { } else {
elseCallback(); elseCallback();
} }
} else {
fullIfCallback();
}
}); });
} }
function allowedByRobotsTxt(details, ifCallback, elseCallback) { function allowedByRobotsTxt(details, ifCallback, elseCallback) {
if ( details.url && !details.url.startsWith('chrome://') ) { if ( details.url && !details.url.startsWith('chrome://') ) {
robotsTxtAllows(details.url).then(ifCallback, elseCallback); Driver.checkRobots(details.url, details.url.startsWith('https:')).then(ifCallback).catch(elseCallback);
} else { } else {
elseCallback(); elseCallback();
} }
@ -268,7 +223,7 @@
PageNetworkTrafficCollector.prototype.sendLogMessageToTabConsole = function() { PageNetworkTrafficCollector.prototype.sendLogMessageToTabConsole = function() {
var logMessage = Array.from(arguments).join(' '); var logMessage = Array.from(arguments).join(' ');
var message = {message: logMessage, event: 'console-log-message'}; var message = {message: logMessage, event: 'console-log-message'};
browser.tabs.sendMessage(this.tabId, message); chrome.tabs.sendMessage(this.tabId, message);
}; };
PageNetworkTrafficCollector.prototype.sendToTab = function(assetReq, reqs, curPageUrl, adTrackingEvent) { PageNetworkTrafficCollector.prototype.sendToTab = function(assetReq, reqs, curPageUrl, adTrackingEvent) {
@ -298,7 +253,7 @@
msg.origUrl = curPageUrl; msg.origUrl = curPageUrl;
msg.displayAdFound = this.displayAdFound; msg.displayAdFound = this.displayAdFound;
browser.tabs.sendMessage(this.tabId, msg); chrome.tabs.sendMessage(this.tabId, msg);
}; };
PageNetworkTrafficCollector.prototype.getRedirKey = function(url, frameId) { PageNetworkTrafficCollector.prototype.getRedirKey = function(url, frameId) {
@ -615,7 +570,7 @@
var _this = this, var _this = this,
origPageUrl, msgAssetReq; origPageUrl, msgAssetReq;
msgAssetReq = this.msgsBeingSent[msgKey]; msgAssetReq = this.msgsBeingSent[msgKey];
browser.tabs.get(this.tabId).then(function(tab) { chrome.tabs.get(this.tabId, function(tab) {
origPageUrl = tab.url; origPageUrl = tab.url;
}); });
@ -697,83 +652,74 @@
function registerListeners() { function registerListeners() {
browser.webRequest.onBeforeRequest.addListener( chrome.webRequest.onBeforeRequest.addListener(
onBeforeRequestListener, onBeforeRequestListener,
{urls: ['http://*/*', 'https://*/*']}, {urls: ['http://*/*', 'https://*/*']},
[] []
); );
browser.webRequest.onSendHeaders.addListener( chrome.webRequest.onSendHeaders.addListener(
onSendHeadersListener, onSendHeadersListener,
{urls: ['http://*/*', 'https://*/*']}, {urls: ['http://*/*', 'https://*/*']},
['requestHeaders'] ['requestHeaders']
); );
browser.webRequest.onHeadersReceived.addListener( chrome.webRequest.onHeadersReceived.addListener(
onHeadersReceivedListener, onHeadersReceivedListener,
{urls: ['http://*/*', 'https://*/*']}, {urls: ['http://*/*', 'https://*/*']},
['responseHeaders'] ['responseHeaders']
); );
browser.webRequest.onBeforeRedirect.addListener( chrome.webRequest.onBeforeRedirect.addListener(
onBeforeRedirectListener, onBeforeRedirectListener,
{urls: ['http://*/*', 'https://*/*']}, {urls: ['http://*/*', 'https://*/*']},
[] []
); );
browser.webRequest.onResponseStarted.addListener( chrome.webRequest.onResponseStarted.addListener(
onResponseStartedListener, onResponseStartedListener,
{urls: ['http://*/*', 'https://*/*']}, {urls: ['http://*/*', 'https://*/*']},
['responseHeaders'] ['responseHeaders']
); );
browser.webNavigation.onCommitted.addListener(onCommittedListener); chrome.webNavigation.onCommitted.addListener(onCommittedListener);
browser.webNavigation.onCompleted.addListener(onCompletedListener); chrome.webNavigation.onCompleted.addListener(onCompletedListener);
browser.tabs.onRemoved.addListener(onRemovedListener); chrome.tabs.onRemoved.addListener(onRemovedListener);
browser.runtime.onMessage.addListener(onMessageListener); chrome.runtime.onMessage.addListener(onMessageListener);
areListenersRegistered = true; areListenersRegistered = true;
} }
function unregisterListeners() { function unregisterListeners() {
browser.webRequest.onBeforeRequest.removeListener( chrome.webRequest.onBeforeRequest.removeListener(
onBeforeRequestListener onBeforeRequestListener
); );
browser.webRequest.onSendHeaders.removeListener( chrome.webRequest.onSendHeaders.removeListener(
onSendHeadersListener onSendHeadersListener
); );
browser.webRequest.onHeadersReceived.removeListener( chrome.webRequest.onHeadersReceived.removeListener(
onHeadersReceivedListener onHeadersReceivedListener
); );
browser.webRequest.onBeforeRedirect.removeListener( chrome.webRequest.onBeforeRedirect.removeListener(
onBeforeRedirectListener onBeforeRedirectListener
); );
browser.webRequest.onResponseStarted.removeListener( chrome.webRequest.onResponseStarted.removeListener(
onResponseStartedListener onResponseStartedListener
); );
browser.webNavigation.onCommitted.removeListener(onCommittedListener); chrome.webNavigation.onCommitted.removeListener(onCommittedListener);
browser.webNavigation.onCompleted.removeListener(onCompletedListener); chrome.webNavigation.onCompleted.removeListener(onCompletedListener);
browser.tabs.onRemoved.removeListener(onRemovedListener); chrome.tabs.onRemoved.removeListener(onRemovedListener);
browser.runtime.onMessage.removeListener(onMessageListener); chrome.runtime.onMessage.removeListener(onMessageListener);
areListenersRegistered = false; areListenersRegistered = false;
} }
function areRequiredBrowserApisAvailable() { chrome.webNavigation.onBeforeNavigate.addListener(
return requiredBrowserApis.every(function(api) {
return typeof api !== 'undefined';
});
}
if ( areRequiredBrowserApisAvailable() ) {
ifBrowserValid(
function() {
browser.webNavigation.onBeforeNavigate.addListener(
function(details) { function(details) {
if ( details.frameId === 0 ) { if ( details.frameId === 0 ) {
globalPageContainer.onNewNavigation(details); globalPageContainer.onNewNavigation(details);
@ -783,24 +729,8 @@
url: [{urlMatches: 'http://*/*'}, {urlMatches: 'https://*/*'}] url: [{urlMatches: 'http://*/*'}, {urlMatches: 'https://*/*'}]
} }
); );
}, function() {
}
);
}
browser.runtime.onConnect.addListener((port) => {
port.onMessage.addListener((message) => {
if ( message === 'is_browser_valid' ) {
ifBrowserValid(
port.postMessage({'browser_valid': true}),
port.postMessage({'browser_valid': false})
);
}
});
});
browser.runtime.onConnect.addListener((port) => { chrome.runtime.onConnect.addListener((port) => {
port.onMessage.addListener((message) => { port.onMessage.addListener((message) => {
if ( message === 'is_tracking_enabled' ) { if ( message === 'is_tracking_enabled' ) {
ifTrackingEnabled( ifTrackingEnabled(
@ -816,7 +746,4 @@
return true; return true;
}); });
}); });
})(); })();
},{}]},{},[1]);

@ -1,109 +1,44 @@
/** global: browser */ 'use strict'
/** global: Wappalyzer */
/* globals browser Wappalyzer */
/* eslint-env browser */ /* eslint-env browser */
/* globals Utils */
const wappalyzer = new Wappalyzer() const { i18n, getOption, setOption } = Utils
/** const Options = {
* Get a value from localStorage async init() {
*/ // Theme mode
function getOption(name, defaultValue = null) { const themeMode = await getOption('themeMode', false)
return new Promise(async (resolve, reject) => {
let value = defaultValue
try { if (themeMode) {
const option = await browser.storage.local.get(name) document.querySelector('body').classList.add('theme-mode')
if (option[name] !== undefined) {
value = option[name]
}
} catch (error) {
wappalyzer.log(error.message, 'driver', 'error')
return reject(error.message)
}
return resolve(value)
})
}
/**
* Set a value in localStorage
*/
function setOption(name, value) {
return new Promise(async (resolve, reject) => {
try {
await browser.storage.local.set({ [name]: value })
} catch (error) {
wappalyzer.log(error.message, 'driver', 'error')
return reject(error.message)
} }
return resolve() ;[
}) ['upgradeMessage', true],
} ['dynamicIcon', true],
['tracking', true],
['themeMode', false]
].map(async ([option, defaultValue]) => {
const el = document
.querySelector(
`[data-i18n="option${option.charAt(0).toUpperCase() +
option.slice(1)}"]`
)
.parentNode.querySelector('input')
document.addEventListener('DOMContentLoaded', async () => { el.checked = !!(await getOption(option, defaultValue))
const nodes = document.querySelectorAll('[data-i18n]')
Array.prototype.forEach.call(nodes, (node) => { el.addEventListener('click', async () => {
node.childNodes[0].nodeValue = browser.i18n.getMessage(node.dataset.i18n) await setOption(option, !!el.checked)
}) })
document.querySelector('#github').addEventListener('click', () => {
window.open(wappalyzer.config.githubURL)
})
document.querySelector('#twitter').addEventListener('click', () => {
window.open(wappalyzer.config.twitterURL)
}) })
document.querySelector('#wappalyzer').addEventListener('click', () => { i18n()
window.open(wappalyzer.config.websiteURL) }
}) }
let el
let value
// Upgrade message
value = await getOption('upgradeMessage', true)
el = document.querySelector('#option-upgrade-message')
el.checked = value
el.addEventListener('change', (e) =>
setOption('upgradeMessage', e.target.checked)
)
// Dynamic icon
value = await getOption('dynamicIcon', true)
el = document.querySelector('#option-dynamic-icon')
el.checked = value
el.addEventListener('change', (e) =>
setOption('dynamicIcon', e.target.checked)
)
// Tracking
value = await getOption('tracking', true)
el = document.querySelector('#option-tracking')
el.checked = value
el.addEventListener('change', (e) => setOption('tracking', e.target.checked))
// Theme Mode
value = await getOption('themeMode', false)
el = document.querySelector('#option-theme-mode')
el.checked = value
el.addEventListener('change', (e) => setOption('themeMode', e.target.checked)) if (/complete|interactive|loaded/.test(document.readyState)) {
}) Options.init()
} else {
document.addEventListener('DOMContentLoaded', Options.init)
}

@ -1,335 +1,188 @@
'use strict'
/* eslint-env browser */ /* eslint-env browser */
/* global browser, jsonToDOM */ /* globals chrome, Utils */
/** global: browser */ const { agent, i18n, getOption, setOption, promisify } = Utils
/** global: jsonToDOM */
let pinnedCategory = null const Popup = {
let termsAccepted = false port: chrome.runtime.connect({ name: 'popup.js' }),
const port = browser.runtime.connect({ async init() {
name: 'popup.js' // Templates
}) Popup.templates = Array.from(
document.querySelectorAll('[data-template]')
function slugify(string) { ).reduce((templates, template) => {
return string templates[template.dataset.template] = template.cloneNode(true)
.toLowerCase()
.replace(/[^a-z0-9-]/g, '-')
.replace(/--+/g, '-')
.replace(/(?:^-|-$)/, '')
}
function i18n() { template.remove()
const nodes = document.querySelectorAll('[data-i18n]')
Array.prototype.forEach.call(nodes, (node) => { return templates
node.innerHTML = browser.i18n.getMessage(node.dataset.i18n) }, {})
})
}
function replaceDom(domTemplate) { // Theme mode
const container = document.getElementsByClassName('container')[0] const themeMode = await getOption('themeMode', false)
while (container.firstChild) { if (themeMode) {
container.removeChild(container.firstChild) document.querySelector('body').classList.add('theme-mode')
} }
container.appendChild(jsonToDOM(domTemplate, document, {})) // Terms
const termsAccepted =
agent === 'chrome' || (await getOption('termsAccepted', false))
i18n() if (termsAccepted) {
document.querySelector('.terms').style.display = 'none'
Array.from(
document.querySelectorAll('.detected__category-pin-wrapper')
).forEach((pin) => {
pin.addEventListener('click', () => {
const categoryId = parseInt(pin.dataset.categoryId, 10)
if (categoryId === pinnedCategory) {
pin.className = 'detected__category-pin-wrapper'
pinnedCategory = null Popup.driver('getDetections')
} else { } else {
const active = document.querySelector( document.querySelector('.detections').style.display = 'none'
'.detected__category-pin-wrapper--active'
)
if (active) { i18n()
active.className = 'detected__category-pin-wrapper'
} }
pin.className = // Alert
'detected__category-pin-wrapper detected__category-pin-wrapper--active' const [{ url }] = await promisify(chrome.tabs, 'query', {
active: true,
currentWindow: true
})
pinnedCategory = categoryId document.querySelector(
} '.alerts__link'
).href = `https://www.wappalyzer.com/alerts/manage?url=${encodeURIComponent(
`${url}`
)}`
port.postMessage({ document
id: 'set_option', .querySelector('.footer__settings')
key: 'pinnedCategory', .addEventListener('click', () => chrome.runtime.openOptionsPage())
value: pinnedCategory },
})
})
})
Array.from(document.querySelectorAll('a')).forEach((link) => { driver(func, ...args) {
link.addEventListener('click', () => { Popup.port.postMessage({ func, args })
browser.tabs.create({ url: link.href }) },
return false log(message) {
}) Popup.driver('log', message, 'popup.js')
}) },
}
function replaceDomWhenReady(dom) { categorise(technologies) {
if (/complete|interactive|loaded/.test(document.readyState)) { return Object.values(
replaceDom(dom) technologies.reduce((categories, technology) => {
} else { technology.categories.forEach((category) => {
document.addEventListener('DOMContentLoaded', () => { categories[category.id] = categories[category.id] || {
replaceDom(dom) ...category,
}) technologies: []
} }
}
function appsToDomTemplate(response) { categories[category.id].technologies.push(technology)
let template = [] })
if (response.tabCache && Object.keys(response.tabCache.detected).length > 0) { return categories
const categories = {} }, {})
)
},
// Group apps by category async onGetDetections(detections) {
for (const appName in response.tabCache.detected) { const pinnedCategory = await getOption('pinnedCategory')
response.apps[appName].cats.forEach((cat) => {
categories[cat] = categories[cat] || {
name: response.categories[cat].name,
apps: []
}
categories[cat].apps[appName] = appName if (detections.length) {
}) document.querySelector('.empty').remove()
} }
for (const cat in categories) { Popup.categorise(detections).forEach(
const apps = [] ({ id, name, slug: categorySlug, technologies }) => {
const categoryNode = Popup.templates.category.cloneNode(true)
for (const appName in categories[cat].apps) { const link = categoryNode.querySelector('.category__link')
const { confidenceTotal, version } = response.tabCache.detected[appName]
apps.push([ link.href = `https://www.wappalyzer.com/technologies/${categorySlug}`
'a', link.textContent = name
{
class: 'detected__app',
href: `https://www.wappalyzer.com/technologies/${slugify(
categories[cat].name
)}/${slugify(appName)}`
},
[
'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'
},
version
]
: null,
confidenceTotal < 100
? [
'span',
{
class: 'detected__app-confidence'
},
`${confidenceTotal}% sure`
]
: null
])
}
template.push([ const pins = categoryNode.querySelectorAll('.category__pin')
'div',
{ if (pinnedCategory === id) {
class: 'detected__category' pins.forEach((pin) => pin.classList.add('category__pin--active'))
},
[
'div',
{
class: 'detected__category-name'
},
[
'a',
{
class: 'detected__category-link',
href: `https://www.wappalyzer.com/categories/${slugify(
response.categories[cat].name
)}`
},
browser.i18n.getMessage(`categoryName${cat}`)
],
[
'span',
{
class: `detected__category-pin-wrapper${
parseInt(pinnedCategory, 10) === parseInt(cat, 10)
? ' 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'
}
]
]
],
[
'div',
{
class: 'detected__apps'
},
apps
]
])
} }
template = [ pins.forEach((pin) =>
'div', pin.addEventListener('click', async () => {
{ const pinnedCategory = await getOption('pinnedCategory')
class: 'detected'
}, Array.from(
template document.querySelectorAll('.category__pin--active')
] ).forEach((pin) => pin.classList.remove('category__pin--active'))
if (pinnedCategory === id) {
await setOption('pinnedCategory', null)
} else { } else {
template = [ await setOption('pinnedCategory', id)
'div',
{ pins.forEach((pin) => pin.classList.add('category__pin--active'))
class: 'empty'
},
[
'span',
{
class: 'empty__text'
},
browser.i18n.getMessage('noAppsDetected')
]
]
} }
})
)
return template technologies
} .filter(({ confidence }) => confidence)
.forEach(({ name, slug, confidence, version, icon, website }) => {
const technologyNode = Popup.templates.technology.cloneNode(true)
async function getApps() { const image = technologyNode.querySelector('.technology__icon')
try {
const tabs = await browser.tabs.query({
active: true,
currentWindow: true
})
const url = new URL(tabs[0].url) image.src = `../images/icons/${icon}`
document.querySelector( const link = technologyNode.querySelector('.technology__link')
'.footer__link'
).href = `https://www.wappalyzer.com/alerts/manage?url=${encodeURIComponent(
`${url.protocol}//${url.hostname}`
)}`
port.postMessage({ link.href = `https://www.wappalyzer.com/technologies/${categorySlug}/${slug}`
id: 'get_apps', link.textContent = name
tab: tabs[0]
})
} catch (error) {
console.error(error) // eslint-disable-line no-console
}
}
/** const confidenceNode = technologyNode.querySelector(
* Async function to update body class based on option. '.technology__confidence'
*/ )
function getThemeMode() {
try {
port.postMessage({
id: 'update_theme_mode'
})
} catch (error) {
console.error(error) // eslint-disable-line no-console
}
}
/** if (confidence < 100) {
* Update theme mode based on browser option. confidenceNode.textContent = `${confidence}% sure`
* @param {object} res Response from port listener. } else {
*/ confidenceNode.remove()
function updateThemeMode(res) {
if (res.hasOwnProperty('themeMode') && res.themeMode !== false) {
document.body.classList.add('theme-mode-sync')
} }
}
function displayApps(response) { const versionNode = technologyNode.querySelector(
pinnedCategory = response.pinnedCategory // eslint-disable-line prefer-destructuring '.technology__version'
termsAccepted = response.termsAccepted // eslint-disable-line prefer-destructuring )
if (termsAccepted) { if (version) {
replaceDomWhenReady(appsToDomTemplate(response)) versionNode.textContent = version
} else { } else {
i18n() versionNode.remove()
}
const wrapper = document.querySelector('.terms__wrapper')
document.querySelector('.terms__accept').addEventListener('click', () => { categoryNode
port.postMessage({ .querySelector('.technologies')
id: 'set_option', .appendChild(technologyNode)
key: 'termsAccepted',
value: true
}) })
wrapper.classList.remove('terms__wrapper--active') document.querySelector('.detections').appendChild(categoryNode)
}
)
getApps() Array.from(document.querySelectorAll('a')).forEach((a) =>
}) a.addEventListener('click', () => Popup.driver('open', a.href))
)
wrapper.classList.add('terms__wrapper--active') i18n()
} }
} }
port.onMessage.addListener((message) => { Popup.port.onMessage.addListener(({ func, args }) => {
switch (message.id) { const onFunc = `on${func.charAt(0).toUpperCase() + func.slice(1)}`
case 'get_apps':
displayApps(message.response)
break if (Popup[onFunc]) {
case 'update_theme_mode': Popup[onFunc](args)
updateThemeMode(message.response)
break
default:
// Do nothing
} }
}) })
getThemeMode() if (/complete|interactive|loaded/.test(document.readyState)) {
getApps() Popup.init()
} else {
document.addEventListener('DOMContentLoaded', Popup.init)
}

@ -0,0 +1,53 @@
'use strict'
/* eslint-env browser */
/* globals chrome */
const Utils = {
agent: chrome.extension.getURL('/').startsWith('moz-') ? 'firefox' : 'chrome',
promisify(context, method, ...args) {
return new Promise((resolve, reject) => {
context[method](...args, (...args) => {
if (chrome.runtime.lastError) {
return reject(chrome.runtime.lastError)
}
resolve(...args)
})
})
},
open(url, active = true) {
chrome.tabs.create({ url, active })
},
async getOption(name, defaultValue = null) {
try {
const option = await Utils.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 Utils.promisify(chrome.storage.local, 'set', {
[name]: value
})
} catch (error) {
throw new Error(error.message || error.toString())
}
},
i18n() {
Array.from(document.querySelectorAll('[data-i18n]')).forEach(
(node) => (node.innerHTML = chrome.i18n.getMessage(node.dataset.i18n))
)
}
}

@ -36,7 +36,6 @@
"https://*/*" "https://*/*"
], ],
"js": [ "js": [
"node_modules/webextension-polyfill/dist/browser-polyfill.js",
"js/content.js" "js/content.js"
], ],
"run_at": "document_idle" "run_at": "document_idle"
@ -46,12 +45,7 @@
"http://*/*", "http://*/*",
"https://*/*" "https://*/*"
], ],
"exclude_matches": [
"https://*.modirum.com/*",
"https://www.alphaecommerce.gr/*"
],
"js": [ "js": [
"node_modules/webextension-polyfill/dist/browser-polyfill.js",
"js/lib/iframe.js" "js/lib/iframe.js"
], ],
"run_at": "document_start", "run_at": "document_start",

@ -1,399 +1,261 @@
/** 'use strict'
* Wappalyzer v5
* const Wappalyzer = {
* Created by Elbert Alias <elbert@alias.io> technologies: [],
* categories: [],
* License: GPLv3 http://www.gnu.org/licenses/gpl-3.0.txt
*/ slugify(string) {
return string
const validation = { .toLowerCase()
hostname: /(www.)?((.+?)\.(([a-z]{2,3}\.)?[a-z]{2,6}))$/, .replace(/[^a-z0-9-]/g, '-')
hostnameBlacklist: /((local|dev(elopment)?|stag(e|ing)?|test(ing)?|demo(shop)?|admin|google|cache)\.|\/admin|\.local)/ .replace(/--+/g, '-')
} .replace(/(?:^-|-$)/, '')
},
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 }) => {
* Enclose string in array if (name === technology.name) {
*/ const versionValue = Wappalyzer.resolveVersion(pattern, match)
function asArray(value) {
return Array.isArray(value) ? value : [value]
}
/** confidence = Math.min(100, confidence + pattern.confidence)
* version =
*/ versionValue.length > version.length && versionValue.length <= 10
function asyncForEach(iterable, iterator) { ? versionValue
return Promise.all( : version
(iterable || []).map( }
(item) => })
new Promise((resolve) => setTimeout(() => resolve(iterator(item)), 1))
)
)
}
/** resolved.push({ technology, confidence, version })
* Mark application as detected, set confidence and version }
*/
function addDetected(app, pattern, type, value, key) {
app.detected = true
// Set confidence level return resolved
app.confidence[`${type} ${key ? `${key} ` : ''}${pattern.regex}`] = }, [])
pattern.confidence === undefined ? 100 : parseInt(pattern.confidence, 10)
// Detect version number Wappalyzer.resolveExcludes(resolved)
if (pattern.version) { Wappalyzer.resolveImplies(resolved)
const versions = []
const matches = pattern.regex.exec(value)
let { version } = pattern 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) { if (matches) {
matches.forEach((match, i) => { matches.forEach((match, index) => {
// Parse ternary operator // Parse ternary operator
const ternary = new RegExp(`\\\\${i}\\?([^:]+):(.*)$`).exec(version) const ternary = new RegExp(`\\\\${index}\\?([^:]+):(.*)$`).exec(
version
)
if (ternary && ternary.length === 3) { if (ternary && ternary.length === 3) {
version = version.replace(ternary[0], match ? ternary[1] : ternary[2]) resolved = version.replace(
ternary[0],
match ? ternary[1] : ternary[2]
)
} }
// Replace back references // Replace back references
version = version resolved = resolved
.trim() .trim()
.replace(new RegExp(`\\\\${i}`, 'g'), match || '') .replace(new RegExp(`\\\\${index}`, 'g'), match || '')
}) })
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))
} }
} }
}
}
function resolveExcludes(apps, detected) { return resolved
const excludes = [] },
const detectedApps = Object.assign({}, apps, detected)
// Exclude app in detected apps only resolveExcludes(resolved) {
Object.keys(detectedApps).forEach((appName) => { resolved.forEach(({ technology }) => {
const app = detectedApps[appName] technology.excludes.forEach((name) => {
const excluded = Wappalyzer.getTechnology(name)
if (app.props.excludes) { if (!excluded) {
asArray(app.props.excludes).forEach((excluded) => { throw new Error(`Excluded technology does not exist: ${name}`)
excludes.push(excluded)
})
} }
})
// Remove excluded applications const index = resolved.findIndex(({ name }) => name === excluded.name)
Object.keys(apps).forEach((appName) => {
if (excludes.includes(appName)) {
delete apps[appName]
}
})
}
class Application { if (index === -1) {
constructor(name, props, detected) { resolved.splice(index, 1)
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
Object.keys(this.confidence).forEach((id) => {
total += this.confidence[id]
}) })
})
},
this.confidenceTotal = Math.min(total, 100) resolveImplies(resolved) {
let done = false
return this.confidenceTotal
}
}
class Wappalyzer {
constructor() {
this.apps = {}
this.categories = {}
this.driver = {}
this.jsPatterns = {}
this.detected = {}
this.hostnameCache = {
expires: Date.now() + 1000 * 60 * 60 * 24,
hostnames: {}
}
this.adCache = []
this.config = {
websiteURL: 'https://www.wappalyzer.com/',
twitterURL: 'https://twitter.com/Wappalyzer',
githubURL: 'https://github.com/AliasIO/Wappalyzer'
}
}
/**
* Log messages to console
*/
log(message, source, type) {
if (this.driver.log) {
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
if (this.detected[url.canonical] === undefined) {
this.detected[url.canonical] = {}
}
const metaTags = []
// Additional information
let language = null
if (html) {
if (typeof html !== 'string') {
html = ''
}
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
// Meta tags
const regex = /<meta[^>]+>/gi
do {
matches = regex.exec(html)
if (!matches) {
break
}
metaTags.push(matches[0])
} while (matches)
}
Object.keys(this.apps).forEach((appName) => {
apps[appName] =
this.detected[url.canonical] && this.detected[url.canonical][appName]
? this.detected[url.canonical][appName]
: new Application(appName, this.apps[appName])
const app = apps[appName]
promises.push(this.analyzeUrl(app, url)) while (resolved.length && !done) {
resolved.forEach(({ technology, confidence }) => {
done = true
if (html) { technology.implies.forEach((name) => {
promises.push(this.analyzeHtml(app, html)) const implied = Wappalyzer.getTechnology(name)
promises.push(this.analyzeMeta(app, metaTags))
}
if (scripts) { if (!implied) {
promises.push(this.analyzeScripts(app, scripts)) throw new Error(`Implied technology does not exist: ${name}`)
} }
if (cookies) { if (
promises.push(this.analyzeCookies(app, cookies)) resolved.findIndex(
} ({ technology: { name } }) => name === implied.name
) === -1
) {
resolved.push({ technology: implied, confidence, version: '' })
if (headers) { done = false
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]))
}
}) })
} }
},
return new Promise(async (resolve) => { async analyze(url, { html, meta, headers, cookies, scripts }) {
await Promise.all(promises) const oo = Wappalyzer.analyzeOneToOne
const om = Wappalyzer.analyzeOneToMany
Object.keys(apps).forEach((appName) => { const mm = Wappalyzer.analyzeManyToMany
const app = apps[appName]
if (!app.detected || !app.getConfidence()) { const flatten = (array) => Array.prototype.concat.apply([], array)
delete apps[app.name]
}
})
resolveExcludes(apps, this.detected[url]) try {
this.resolveImplies(apps, url.canonical) const detections = flatten(
flatten(
this.cacheDetectedApps(apps, url.canonical) await Promise.all(
this.trackDetectedApps(apps, url, language) Wappalyzer.technologies.map((technology) =>
Promise.all([
this.log( oo(technology, 'url', url),
`Processing ${Object.keys(data).join(', ')} took ${( oo(technology, 'html', html),
(new Date() - startTime) / om(technology, 'meta', meta),
1000 mm(technology, 'headers', headers),
).toFixed(2)}s (${url.hostname})`, om(technology, 'cookies', cookies),
'core' om(technology, 'scripts', scripts)
) ])
if (Object.keys(apps).length) {
this.log(
`Identified ${Object.keys(apps).join(', ')} (${url.hostname})`,
'core'
) )
}
this.driver.displayApps(
this.detected[url.canonical],
{ language },
context
) )
return resolve()
})
}
/**
* Cache detected ads
*/
cacheDetectedAds(ad) {
this.adCache.push(ad)
}
/**
*
*/
robotsTxtAllows(url) {
return new Promise(async (resolve, reject) => {
const parsed = this.parseUrl(url)
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return reject()
}
const robotsTxt = await this.driver.getRobotsTxt(
parsed.host,
parsed.protocol === 'https:'
) )
).filter((technology) => technology)
if ( return detections
robotsTxt.some( } catch (error) {
(disallowedPath) => parsed.pathname.indexOf(disallowedPath) === 0 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,
js,
implies,
excludes,
icon,
website
} = data[name]
technologies.push({
name,
categories: cats || [],
slug: Wappalyzer.slugify(name),
url: transform(url),
headers: transform(
Object.keys(headers || {}).reduce(
(lcHeaders, header) => ({
...lcHeaders,
[header.toLowerCase()]: headers[header]
}),
{}
) )
) { ),
return reject() cookies: transform(cookies),
} html: transform(html),
meta: transform(meta),
return resolve() scripts: transform(script),
}) js: transform(js),
} implies: typeof implies === 'string' ? [implies] : implies || [],
excludes: typeof excludes === 'string' ? [excludes] : excludes || [],
/** icon: icon || 'default.svg',
* Parse a URL website: website || ''
*/ })
parseUrl(url) {
const a = this.driver.document.createElement('a') return technologies
}, [])
a.href = url },
a.canonical = `${a.protocol}//${a.host}${a.pathname}` setCategories(data) {
Wappalyzer.categories = Object.keys(data)
return a .reduce((categories, id) => {
} const category = data[id]
/** categories.push({
* id: parseInt(id, 10),
*/ slug: Wappalyzer.slugify(category.name),
static parseRobotsTxt(robotsTxt) { ...category
const disallow = [] })
let userAgent return categories
}, [])
robotsTxt.split('\n').forEach((line) => { .sort(({ priority: a }, { priority: b }) => (a > b ? -1 : 0))
let matches = /^User-agent:\s*(.+)$/i.exec(line.trim()) },
if (matches) { transformPatterns(patterns) {
userAgent = matches[1].toLowerCase()
} else if (userAgent === '*' || userAgent === 'wappalyzer') {
matches = /^Disallow:\s*(.+)$/i.exec(line.trim())
if (matches) {
disallow.push(matches[1])
}
}
})
return disallow
}
/**
*
*/
ping() {
if (
!this.hostnameCache.hostnames ||
Object.keys(this.hostnameCache.hostnames).length > 50 ||
this.hostnameCache.expires < Date.now()
) {
this.driver.ping(this.hostnameCache)
this.hostnameCache = {
expires: Date.now() + 1000 * 60 * 60 * 24,
hostnames: {}
}
}
if (this.adCache.length > 50) {
this.driver.ping(undefined, this.adCache)
this.adCache = []
}
}
/**
* Parse apps.json patterns
*/
parsePatterns(patterns) {
if (!patterns) { if (!patterns) {
return [] return []
} }
let parsed = {} const toArray = (value) => (Array.isArray(value) ? value : [value])
// Convert string to object containing array containing string
if (typeof patterns === 'string' || Array.isArray(patterns)) { if (typeof patterns === 'string' || Array.isArray(patterns)) {
patterns = { patterns = { main: patterns }
main: asArray(patterns)
} }
}
Object.keys(patterns).forEach((key) => {
parsed[key] = []
asArray(patterns[key]).forEach((pattern) => { const parsed = Object.keys(patterns).reduce((parsed, key) => {
const attrs = {} parsed[key] = toArray(patterns[key]).map((pattern) => {
const { regex, confidence, version } = pattern
pattern.split('\\;').forEach((attr, i) => { .split('\\;')
.reduce((attrs, attr, i) => {
if (i) { if (i) {
// Key value pairs // Key value pairs
attr = attr.split(':') attr = attr.split(':')
@ -402,334 +264,68 @@ class Wappalyzer {
attrs[attr.shift()] = attr.join(':') attrs[attr.shift()] = attr.join(':')
} }
} else { } else {
attrs.string = attr // Escape slashes in regular expression
attrs.regex = new RegExp(attr.replace(/\//g, '\\/'), 'i')
try {
attrs.regex = new RegExp(attr.replace('/', '/'), 'i') // Escape slashes in regular expression
} catch (error) {
attrs.regex = new RegExp()
this.log(`${error.message}: ${attr}`, 'error', 'core')
}
}
})
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
}
return parsed
}
/**
* Parse JavaScript patterns
*/
parseJsPatterns() {
Object.keys(this.apps).forEach((appName) => {
if (this.apps[appName].js) {
this.jsPatterns[appName] = this.parsePatterns(this.apps[appName].js)
}
})
}
resolveImplies(apps, url) {
let checkImplies = true
const resolve = (appName) => {
const app = apps[appName]
if (app && app.props.implies) {
asArray(app.props.implies).forEach((implied) => {
;[implied] = this.parsePatterns(implied)
if (!this.apps[implied.string]) {
this.log(
`Implied application ${implied.string} does not exist`,
'core',
'warn'
)
return
}
if (!(implied.string in apps)) {
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
)
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)
})
})
}
} }
// Implied applications return attrs
// Run several passes as implied apps may imply other apps }, {})
while (checkImplies) {
checkImplies = false
Object.keys(apps).forEach(resolve) return {
} regex,
confidence: parseInt(confidence || 100, 10),
version: version || ''
} }
/**
* Cache detected applications
*/
cacheDetectedApps(apps, url) {
Object.keys(apps).forEach((appName) => {
const app = apps[appName]
// Per URL
this.detected[url][appName] = app
Object.keys(app.confidence).forEach((id) => {
this.detected[url][appName].confidence[id] = app.confidence[id]
})
}) })
if (this.driver.ping instanceof Function) { return parsed
this.ping() }, {})
}
}
/**
* Track detected applications
*/
trackDetectedApps(apps, url, language) {
if (!(this.driver.ping instanceof Function)) {
return
}
const hostname = `${url.protocol}//${url.hostname}`
Object.keys(apps).forEach((appName) => {
const app = apps[appName]
if (this.detected[url.canonical][appName].getConfidence() >= 100) {
if (
validation.hostname.test(url.hostname) &&
!validation.hostnameBlacklist.test(url.hostname)
) {
if (!(hostname in this.hostnameCache.hostnames)) {
this.hostnameCache.hostnames[hostname] = {
applications: {},
meta: {}
}
}
if (
!(appName in this.hostnameCache.hostnames[hostname].applications)
) {
this.hostnameCache.hostnames[hostname].applications[appName] = {
hits: 0
}
}
this.hostnameCache.hostnames[hostname].applications[appName].hits += 1
if (apps[appName].version) {
this.hostnameCache.hostnames[hostname].applications[
appName
].version = app.version
}
}
}
})
if (hostname in this.hostnameCache.hostnames) {
this.hostnameCache.hostnames[hostname].meta.language = language
}
this.ping()
}
/**
* Analyze URL
*/
analyzeUrl(app, url) {
const patterns = this.parsePatterns(app.props.url)
if (!patterns.length) {
return Promise.resolve()
}
return asyncForEach(patterns, (pattern) => {
if (pattern.regex.test(url.canonical)) {
addDetected(app, pattern, 'url', url.canonical)
}
})
}
/**
* Analyze HTML
*/
analyzeHtml(app, html) {
const patterns = this.parsePatterns(app.props.html)
if (!patterns.length) {
return Promise.resolve()
}
return asyncForEach(patterns, (pattern) => {
if (pattern.regex.test(html)) {
addDetected(app, pattern, 'html', html)
}
})
}
/**
* Analyze script tag
*/
analyzeScripts(app, scripts) {
const patterns = this.parsePatterns(app.props.script)
if (!patterns.length) {
return Promise.resolve()
}
return asyncForEach(patterns, (pattern) => {
scripts.forEach((uri) => {
if (pattern.regex.test(uri)) {
addDetected(app, pattern, 'script', uri)
}
})
})
}
/** return 'main' in parsed ? parsed.main : parsed
* Analyze meta tag },
*/
analyzeMeta(app, metaTags) {
const patterns = this.parsePatterns(app.props.meta)
const promises = []
if (!app.props.meta) { analyzeOneToOne(technology, type, value) {
return Promise.resolve() return technology[type].reduce((technologies, pattern) => {
if (pattern.regex.test(value)) {
technologies.push({ technology, pattern, match: value })
} }
metaTags.forEach((match) => { return technologies
Object.keys(patterns).forEach((meta) => { }, [])
const r = new RegExp(`(?:name|property)=["']${meta}["']`, 'i') },
if (r.test(match)) { analyzeOneToMany(technology, type, items = []) {
const content = match.match(/content=("|')([^"']+)("|')/i) return items.reduce((technologies, { key, value }) => {
const patterns = technology[type][key] || []
promises.push( patterns.forEach((pattern) => {
asyncForEach(patterns[meta], (pattern) => { if (pattern.regex.test(value)) {
if ( technologies.push({ technology, pattern, match: value })
content &&
content.length === 4 &&
pattern.regex.test(content[2])
) {
addDetected(app, pattern, 'meta', content[2], meta)
}
})
)
} }
}) })
})
return Promise.all(promises) return technologies
} }, [])
},
/** analyzeManyToMany(technology, type, items = {}) {
* Analyze response headers return Object.keys(technology[type]).reduce((technologies, key) => {
*/ const patterns = technology[type][key] || []
analyzeHeaders(app, headers) { const values = items[key] || []
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()
if (headerName in headers) {
headers[headerName].forEach((headerValue) => {
if (pattern.regex.test(headerValue)) {
addDetected(app, pattern, 'headers', headerValue, headerName)
}
})
}
})
)
}
})
return promises ? Promise.all(promises) : Promise.resolve() patterns.forEach((pattern) => {
} values.forEach((value) => {
if (pattern.regex.test(value)) {
/** technologies.push({ technology, pattern, match: value })
* Analyze cookies
*/
analyzeCookies(app, cookies) {
const patterns = this.parsePatterns(app.props.cookies)
const promises = []
Object.keys(patterns).forEach((cookieName) => {
if (typeof patterns[cookieName] !== 'function') {
const cookieNameLower = cookieName.toLowerCase()
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)
}
})
)
} }
}) })
return promises ? Promise.all(promises) : Promise.resolve()
}
/**
* Analyze JavaScript variables
*/
analyzeJs(app, results) {
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]
if (pattern && pattern.regex.test(value)) {
addDetected(app, pattern, 'js', value, string)
}
})
)
}
}) })
return promises ? Promise.all(promises) : Promise.resolve() return technologies
}, [])
} }
} }
if (typeof module === 'object') { if (typeof module !== 'undefined') {
module.exports = Wappalyzer module.exports = Wappalyzer
} }

@ -1,51 +1,50 @@
/* eslint-env mocha */ /* eslint-env mocha */
const { assert, expect } = require('chai'); const { assert, expect } = require('chai')
const Wappalyzer = require('../src/wappalyzer'); const Wappalyzer = require('../src/wappalyzer')
const appsJson = { const appsJson = {
appUrl: { appUrl: {
url: 'test', url: 'test'
}, },
appCookies: { appCookies: {
cookies: { cookies: {
test: 'test', test: 'test'
}, }
}, },
appUppercaseCookies: { appUppercaseCookies: {
cookies: { cookies: {
Test: 'Test', Test: 'Test'
}, }
}, },
appHeaders: { appHeaders: {
headers: { headers: {
'X-Powered-By': 'test', 'X-Powered-By': 'test'
}, }
}, },
appHtml: { appHtml: {
html: 'test v(\\d)\\;confidence:50\\;version:\\1', html: 'test v(\\d)\\;confidence:50\\;version:\\1',
implies: 'appImplies', implies: 'appImplies',
excludes: 'appExcludes', excludes: 'appExcludes'
}, },
appMeta: { appMeta: {
meta: { meta: {
generator: 'test', generator: 'test'
}, }
}, },
appScript: { appScript: {
script: 'test', script: 'test'
}, },
appJs: { appJs: {
js: { js: {
key: 'value', key: 'value'
}, }
},
appImplies: {
}, },
appImplies: {},
appExcludes: { appExcludes: {
html: 'test', html: 'test'
}, }
}; }
const driverData = { const driverData = {
cookies: [ cookies: [
@ -53,92 +52,86 @@ const driverData = {
name: 'test', name: 'test',
value: 'test', value: 'test',
domain: '', domain: '',
path: '', path: ''
}, }
], ],
headers: { headers: {
'x-powered-by': [ 'x-powered-by': ['test']
'test',
],
}, },
html: '<meta name="generator" content="test"> html test v1', html: '<meta name="generator" content="test"> html test v1',
scripts: [ scripts: ['test'],
'test',
],
js: { js: {
appJs: { appJs: {
key: [ key: ['value']
'value', }
], }
}, }
},
};
describe('Wappalyzer', () => { describe('Wappalyzer', () => {
describe('#analyze()', () => { describe('#analyze()', () => {
let apps; let apps
before(async () => { before(async () => {
const wappalyzer = new Wappalyzer(); const wappalyzer = new Wappalyzer()
wappalyzer.apps = appsJson; wappalyzer.apps = appsJson
wappalyzer.parseJsPatterns(); wappalyzer.parseJsPatterns()
wappalyzer.driver.displayApps = (detected) => { wappalyzer.driver.displayApps = (detected) => {
apps = detected; apps = detected
}; }
await wappalyzer.analyze({ canonical: 'test' }, driverData); await wappalyzer.analyze({ canonical: 'test' }, driverData)
}); })
it('should identify technologies using URLs', () => { it('should identify technologies using URLs', () => {
expect(apps).to.have.any.keys('appUrl'); expect(apps).to.have.any.keys('appUrl')
}); })
it('should identify technologies using HTML', () => { it('should identify technologies using HTML', () => {
expect(apps).to.have.any.keys('appHtml'); expect(apps).to.have.any.keys('appHtml')
}); })
it('should identify technologies using meta tags', () => { it('should identify technologies using meta tags', () => {
expect(apps).to.have.any.keys('appMeta'); expect(apps).to.have.any.keys('appMeta')
}); })
it('should identify technologies using script URLs', () => { it('should identify technologies using script URLs', () => {
expect(apps).to.have.any.keys('appScript'); expect(apps).to.have.any.keys('appScript')
}); })
it('should identify technologies using headers', () => { it('should identify technologies using headers', () => {
expect(apps).to.have.any.keys('appHeaders'); expect(apps).to.have.any.keys('appHeaders')
}); })
it('should identify technologies using cookies', () => { it('should identify technologies using cookies', () => {
expect(apps).to.have.any.keys('appCookies'); expect(apps).to.have.any.keys('appCookies')
}); })
it('should identify technologies using uppercase named cookies', () => { it('should identify technologies using uppercase named cookies', () => {
expect(apps).to.have.any.keys('appUppercaseCookies'); expect(apps).to.have.any.keys('appUppercaseCookies')
}); })
it('should identify technologies using JavaScript', () => { it('should identify technologies using JavaScript', () => {
expect(apps).to.have.any.keys('appJs'); expect(apps).to.have.any.keys('appJs')
}); })
it('should return the implied technology', () => { it('should return the implied technology', () => {
expect(apps).to.have.any.keys('appImplies'); expect(apps).to.have.any.keys('appImplies')
}); })
it('should not return the excluded technology', () => { it('should not return the excluded technology', () => {
expect(apps).to.not.have.any.keys('appExcludes'); expect(apps).to.not.have.any.keys('appExcludes')
}); })
it('should return the confidence value', () => { it('should return the confidence value', () => {
assert.equal(apps.appHtml.confidenceTotal, 50); assert.equal(apps.appHtml.confidenceTotal, 50)
}); })
it('should return the version number', () => { it('should return the version number', () => {
assert.equal(apps.appHtml.version, '1'); assert.equal(apps.appHtml.version, '1')
}); })
it('should analyze html', async () => { it('should analyze html', async () => {
const html = ` const html = `
@ -156,123 +149,125 @@ describe('Wappalyzer', () => {
<!-- End Google Tag Manager --> <!-- End Google Tag Manager -->
</body> </body>
</html> </html>
`; `
const wappalyzer = new Wappalyzer(); const wappalyzer = new Wappalyzer()
wappalyzer.apps = { wappalyzer.apps = {
"Google Tag Manager": { 'Google Tag Manager': {
"html": [ html: [
"googletagmanager\\.com/ns\\.html[^>]+></iframe>", 'googletagmanager\\.com/ns\\.html[^>]+></iframe>',
"<!-- (?:End )?Google Tag Manager -->" '<!-- (?:End )?Google Tag Manager -->'
] ]
} }
}; }
var applications = null; let applications = null
wappalyzer.driver = { wappalyzer.driver = {
log () {}, log() {},
displayApps (detectedMap) { displayApps(detectedMap) {
applications = detectedMap; applications = detectedMap
}
} }
};
await wappalyzer.analyze({ canonical: 'example.com' }, { html }); await wappalyzer.analyze({ canonical: 'example.com' }, { html })
assert.equal(applications['Google Tag Manager'].name, 'Google Tag Manager'); assert.equal(
}); applications['Google Tag Manager'].name,
'Google Tag Manager'
)
})
it('should analyze scripts', async () => { it('should analyze scripts', async () => {
const scripts = [ const scripts = [
'http://www.google-analytics.com/analytics.js', 'http://www.google-analytics.com/analytics.js',
'http://example.com/assets/js/jquery.min.js' 'http://example.com/assets/js/jquery.min.js'
]; ]
const wappalyzer = new Wappalyzer(); const wappalyzer = new Wappalyzer()
wappalyzer.apps = { wappalyzer.apps = {
"Google Analytics": { 'Google Analytics': {
"cats": [ cats: [10],
10 script:
], 'google-analytics\\.com\\/(?:ga|urchin|(analytics))\\.js\\;version:\\1?UA:'
"script": "google-analytics\\.com\\/(?:ga|urchin|(analytics))\\.js\\;version:\\1?UA:"
}, },
"jQuery": { jQuery: {
"script": [ script: [
"jquery(?:\\-|\\.)([\\d.]*\\d)[^/]*\\.js\\;version:\\1", 'jquery(?:\\-|\\.)([\\d.]*\\d)[^/]*\\.js\\;version:\\1',
"/([\\d.]+)/jquery(?:\\.min)?\\.js\\;version:\\1", '/([\\d.]+)/jquery(?:\\.min)?\\.js\\;version:\\1',
"jquery.*\\.js(?:\\?ver(?:sion)?=([\\d.]+))?\\;version:\\1" 'jquery.*\\.js(?:\\?ver(?:sion)?=([\\d.]+))?\\;version:\\1'
] ]
} }
}; }
var applications = null; let applications = null
wappalyzer.driver = { wappalyzer.driver = {
log () {}, log() {},
displayApps (detectedMap) { displayApps(detectedMap) {
applications = detectedMap; applications = detectedMap
}
} }
};
await wappalyzer.analyze({ canonical: 'example.com' }, { scripts }); await wappalyzer.analyze({ canonical: 'example.com' }, { scripts })
assert.equal(applications['Google Analytics'].name, 'Google Analytics'); assert.equal(applications['Google Analytics'].name, 'Google Analytics')
assert.equal(applications['jQuery'].name, 'jQuery'); assert.equal(applications.jQuery.name, 'jQuery')
}); })
it('should analyze headers', async () => { it('should analyze headers', async () => {
const headers = { const headers = {
'date': [ 'Thu, 01 Feb 2018 11:34:18 GMT' ], date: ['Thu, 01 Feb 2018 11:34:18 GMT'],
'connection': [ 'keep-alive' ], connection: ['keep-alive'],
'x-powered-by': [ 'Express'], 'x-powered-by': ['Express'],
'etag': [ 'W/125-1jQLmiya7mfec43xR3Eb3pjdu64s' ], etag: ['W/125-1jQLmiya7mfec43xR3Eb3pjdu64s'],
'content-length': [ '293' ], 'content-length': ['293'],
'content-type': [ 'text/html; charset=utf-8' ] 'content-type': ['text/html; charset=utf-8']
}; }
const wappalyzer = new Wappalyzer(); const wappalyzer = new Wappalyzer()
wappalyzer.apps = { wappalyzer.apps = {
"Express": { Express: {
"headers": { headers: {
"X-Powered-By": "^Express$" 'X-Powered-By': '^Express$'
}
} }
} }
}; let applications = null
var applications = null;
wappalyzer.driver = { wappalyzer.driver = {
log () {}, log() {},
displayApps (detectedMap) { displayApps(detectedMap) {
applications = detectedMap; applications = detectedMap
}
} }
};
await wappalyzer.analyze({ canonical: 'example.com' }, { headers }); await wappalyzer.analyze({ canonical: 'example.com' }, { headers })
assert.equal(applications['Express'].name, 'Express'); assert.equal(applications.Express.name, 'Express')
}); })
it('should analyze js globals', async () => { it('should analyze js globals', async () => {
const js = { const js = {
'Moment.js': { 'moment': { '0': true } }, 'Moment.js': { moment: { '0': true } },
'Google Font API': { 'WebFonts': { '0': true } } 'Google Font API': { WebFonts: { '0': true } }
}; }
const wappalyzer = new Wappalyzer(); const wappalyzer = new Wappalyzer()
wappalyzer.apps = { wappalyzer.apps = {
"Moment.js": { 'Moment.js': {
"js": { js: {
"moment": "", moment: '',
"moment.version": "(.*)\\;version:\\1" 'moment.version': '(.*)\\;version:\\1'
} }
}, },
"Google Font API": { 'Google Font API': {
"js": { js: {
"WebFonts": "" WebFonts: ''
} }
} }
}; }
var applications = null; let applications = null
wappalyzer.driver = { wappalyzer.driver = {
log () {}, log() {},
displayApps (detectedMap) { displayApps(detectedMap) {
applications = detectedMap; applications = detectedMap
}
} }
};
wappalyzer.parseJsPatterns(); wappalyzer.parseJsPatterns()
await wappalyzer.analyze({ canonical: 'example.com' }, { js }); await wappalyzer.analyze({ canonical: 'example.com' }, { js })
assert.equal(applications['Google Font API'].name, 'Google Font API'); assert.equal(applications['Google Font API'].name, 'Google Font API')
assert.equal(applications['Moment.js'].name, 'Moment.js'); assert.equal(applications['Moment.js'].name, 'Moment.js')
}); })
}); })
}); })

Loading…
Cancel
Save