diff --git a/.gitignore b/.gitignore index 97f83b858..fc4956cd9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ /build/* +/src/drivers/**/categories.json +/src/drivers/**/technologies/* +/src/drivers/**/wappalyzer.js /src/images/icons/converted/* /src/manifest.json /src/manifest.bak.json diff --git a/README.md b/README.md index e307ddf9d..65b453894 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,12 @@ yarn install ## Usage +### Command line + +```sh +node src/drivers/npm/cli.js https://example.com +``` + ### Chrome extension - Go to `about:extensions` @@ -507,4 +513,4 @@ Application version information can be obtained from a pattern using a capture g - + \ No newline at end of file diff --git a/bin/build.js b/bin/build.js index 04f62203f..d74c1bd73 100755 --- a/bin/build.js +++ b/bin/build.js @@ -5,18 +5,17 @@ const currentVersion = JSON.parse( fs.readFileSync('./src/manifest-v3.json') ).version -const version = process.argv[2] +version = process.argv[2] if (!version) { // eslint-disable-next-line no-console - console.error( - `No version number specified. Current version is ${currentVersion}.` + console.warn( + `No version number specified. Current version is ${currentVersion}: will it use it.` ) - - process.exit(1) + version = currentVersion } -;['./src/manifest-v2.json', './src/manifest-v3.json'].forEach((file) => { +;['./src/drivers/npm/package.json', './src/manifest-v2.json', './src/manifest-v3.json'].forEach((file) => { const json = JSON.parse(fs.readFileSync(file)) json.version = version diff --git a/bin/link.js b/bin/link.js new file mode 100644 index 000000000..fd0b336d5 --- /dev/null +++ b/bin/link.js @@ -0,0 +1,21 @@ +const fs = require('fs') + +const link = (src, dest) => { + if (fs.existsSync(dest)) { + fs.unlinkSync(dest) + } + + fs.linkSync(src, dest) +} + +link('./src/js/wappalyzer.js', './src/drivers/npm/wappalyzer.js') +link('./src/categories.json', './src/drivers/npm/categories.json') + +for (const index of Array(27).keys()) { + const character = index ? String.fromCharCode(index + 96) : '_' + + link( + `./src/technologies/${character}.json`, + `./src/drivers/npm/technologies/${character}.json` + ) +} diff --git a/package.json b/package.json index 29f3b0023..c90b5af94 100644 --- a/package.json +++ b/package.json @@ -17,13 +17,14 @@ "terminal-overwrite": "^2.0.1" }, "scripts": { + "link": "node ./bin/link.js; node ./bin/manifest.js v3", "lint": "eslint src/**/*.{js,json}", "lint:fix": "eslint --fix src/**/*.{js,json}", "validate": "yarn run lint && jsonlint -qV ./schema.json ./src/technologies/ && node ./bin/validate.js", "convert": "node --no-warnings ./bin/convert.js", "prettify": "jsonlint -si --trim-trailing-commas --enforce-double-quotes ./src/categories.json ./src/technologies/*.json", - "build": "yarn run validate && yarn run prettify && yarn run convert && node ./bin/build.js", - "build:safari": "xcrun safari-web-extension-converter --swift --project-location build --force src", + "build": "yarn run link && yarn run validate && yarn run prettify && yarn run convert && node ./bin/build.js", + "build:safari": "xcrun safari-web-extension-converter --swift --project-location build --force src/drivers/webextension", "manifest": "node ./bin/manifest.js" } } diff --git a/src/drivers/npm/Dockerfile b/src/drivers/npm/Dockerfile new file mode 100644 index 000000000..ebebe2237 --- /dev/null +++ b/src/drivers/npm/Dockerfile @@ -0,0 +1,31 @@ +FROM node:14-alpine + +MAINTAINER Wappalyzer + +ENV WAPPALYZER_ROOT /opt/wappalyzer +ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true +ENV CHROMIUM_BIN /usr/bin/chromium-browser + +RUN apk update && apk add -u --no-cache \ + nodejs \ + udev \ + chromium \ + ttf-freefont \ + yarn + +RUN mkdir -p "$WAPPALYZER_ROOT/browsers" + +WORKDIR "$WAPPALYZER_ROOT" + +COPY technologies ./technologies +COPY \ + cli.js \ + categories.json \ + driver.js \ + package.json \ + wappalyzer.js \ + yarn.lock ./ + +RUN yarn install + +ENTRYPOINT ["node", "cli.js"] diff --git a/src/drivers/npm/README.md b/src/drivers/npm/README.md new file mode 100644 index 000000000..f856e4b9d --- /dev/null +++ b/src/drivers/npm/README.md @@ -0,0 +1,155 @@ +# Wappalyzer + +[Wappalyzer](https://www.wappalyzer.com/) indentifies technologies on websites. + +*Note:* The [wappalyzer-core](https://www.npmjs.com/package/wappalyzer-core) package provides a low-level API without dependencies. + +## Command line + +### Installation + +```shell +$ npm i -g wappalyzer +``` + +### Usage + +``` +wappalyzer [options] +``` + +#### Options + +``` +-b, --batch-size=... Process links in batches +-d, --debug Output debug messages +-t, --delay=ms Wait for ms milliseconds between requests +-h, --help This text +-H, --header Extra header to send with requests +--html-max-cols=... Limit the number of HTML characters per line processed +--html-max-rows=... Limit the number of HTML lines processed +-D, --max-depth=... Don't analyse pages more than num levels deep +-m, --max-urls=... Exit when num URLs have been analysed +-w, --max-wait=... Wait no more than ms milliseconds for page resources to load +-p, --probe=[basic|full] Perform a deeper scan by performing additional requests and inspecting DNS records +-P, --pretty Pretty-print JSON output +--proxy=... Proxy URL, e.g. 'http://user:pass@proxy:8080' +-r, --recursive Follow links on pages (crawler) +-a, --user-agent=... Set the user agent string +-n, --no-scripts Disabled JavaScript on web pages +-N, --no-redirect Disable cross-domain redirects +-e, --extended Output additional information +--local-storage=... JSON object to use as local storage +--session-storage=... JSON object to use as session storage +--defer=ms Defer scan for ms milliseconds after page load + +``` + + +## Dependency + +### Installation + +```shell +$ npm i wappalyzer +``` + +### Usage + +```javascript +const Wappalyzer = require('wappalyzer') + +const url = 'https://www.wappalyzer.com' + +const options = { + debug: false, + delay: 500, + headers: {}, + maxDepth: 3, + maxUrls: 10, + maxWait: 5000, + recursive: true, + probe: true, + proxy: false, + userAgent: 'Wappalyzer', + htmlMaxCols: 2000, + htmlMaxRows: 2000, + noScripts: false, + noRedirect: false, +}; + +const wappalyzer = new Wappalyzer(options) + +;(async function() { + try { + await wappalyzer.init() + + // Optionally set additional request headers + const headers = {} + + // Optionally set local and/or session storage + const storage = { + local: {} + session: {} + } + + const site = await wappalyzer.open(url, headers, storage) + + // Optionally capture and output errors + site.on('error', console.error) + + const results = await site.analyze() + + console.log(JSON.stringify(results, null, 2)) + } catch (error) { + console.error(error) + } + + await wappalyzer.destroy() +})() +``` + +Multiple URLs can be processed in parallel: + +```javascript +const Wappalyzer = require('wappalyzer'); + +const urls = ['https://www.wappalyzer.com', 'https://www.example.com'] + +const wappalyzer = new Wappalyzer() + +;(async function() { + try { + await wappalyzer.init() + + const results = await Promise.all( + urls.map(async (url) => { + const site = await wappalyzer.open(url) + + const results = await site.analyze() + + return { url, results } + }) + ) + + console.log(JSON.stringify(results, null, 2)) + } catch (error) { + console.error(error) + } + + await wappalyzer.destroy() +})() +``` + +### Events + +Listen to events with `site.on(eventName, callback)`. Use the `page` parameter to access the Puppeteer page instance ([reference](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#class-page)). + +| Event | Parameters | Description | +|-------------|--------------------------------|------------------------------------------| +| `log` | `message`, `source` | Debug messages | +| `error` | `message`, `source` | Error messages | +| `request` | `page`, `request` | Emitted at the start of a request | +| `response` | `page`, `request` | Emitted upon receiving a server response | +| `goto` | `page`, `url`, `html`, `cookies`, `scriptsSrc`, `scripts`, `meta`, `js`, `language` `links` | Emitted after a page has been analysed | +| `analyze` | `urls`, `technologies`, `meta` | Emitted when the site has been analysed | diff --git a/src/drivers/npm/cli.js b/src/drivers/npm/cli.js new file mode 100644 index 000000000..ac2c377b0 --- /dev/null +++ b/src/drivers/npm/cli.js @@ -0,0 +1,194 @@ +#!/usr/bin/env node + +const Wappalyzer = require('./driver') + +const args = process.argv.slice(2) + +const options = {} + +let url +let arg + +const aliases = { + a: 'userAgent', + b: 'batchSize', + d: 'debug', + f: 'fast', + t: 'delay', + h: 'help', + H: 'header', + D: 'maxDepth', + m: 'maxUrls', + p: 'probe', + P: 'pretty', + r: 'recursive', + w: 'maxWait', + n: 'noScripts', + N: 'noRedirect', + e: 'extended', +} + +while (true) { + // eslint-disable-line no-constant-condition + arg = args.shift() + + if (!arg) { + break + } + + const matches = /^-?-([^=]+)(?:=(.+)?)?/.exec(arg) + + if (matches) { + const key = + aliases[matches[1]] || + matches[1].replace(/-\w/g, (_matches) => _matches[1].toUpperCase()) + // eslint-disable-next-line no-nested-ternary + const value = matches[2] + ? matches[2] + : args[0] && !args[0].startsWith('-') + ? args.shift() + : true + + if (options[key]) { + if (!Array.isArray(options[key])) { + options[key] = [options[key]] + } + + options[key].push(value) + } else { + options[key] = value + } + } else { + url = arg + } +} + +if (!url || options.help) { + process.stdout.write(`Usage: + wappalyzer [options] + +Examples: + wappalyzer https://www.example.com + node cli.js https://www.example.com -r -D 3 -m 50 -H "Cookie: username=admin" + docker wappalyzer/cli https://www.example.com --pretty + +Options: + -b, --batch-size=... Process links in batches + -d, --debug Output debug messages + -f, --fast Prioritise speed over accuracy + -t, --delay=ms Wait for ms milliseconds between requests + -h, --help This text + -H, --header Extra header to send with requests + --html-max-cols=... Limit the number of HTML characters per line processed + --html-max-rows=... Limit the number of HTML lines processed + -D, --max-depth=... Don't analyse pages more than num levels deep + -m, --max-urls=... Exit when num URLs have been analysed + -w, --max-wait=... Wait no more than ms milliseconds for page resources to load + -p, --probe=[basic|full] Perform a deeper scan by performing additional requests and inspecting DNS records + -P, --pretty Pretty-print JSON output + --proxy=... Proxy URL, e.g. 'http://user:pass@proxy:8080' + -r, --recursive Follow links on pages (crawler) + -a, --user-agent=... Set the user agent string + -n, --no-scripts Disabled JavaScript on web pages + -N, --no-redirect Disable cross-domain redirects + -e, --extended Output additional information + --local-storage=... JSON object to use as local storage + --session-storage=... JSON object to use as session storage + --defer=ms Defer scan for ms milliseconds after page load +`) + process.exit(options.help ? 0 : 1) +} + +try { + const { hostname } = new URL(url) + + if (!hostname) { + throw new Error('Invalid URL') + } +} catch (error) { + // eslint-disable-next-line no-console + console.log(error.message || error.toString()) + + process.exit(1) +} + +const headers = {} + +if (options.header) { + ;(Array.isArray(options.header) ? options.header : [options.header]).forEach( + (header) => { + const [key, value] = header.split(':') + + headers[key.trim()] = (value || '').trim() + } + ) +} + +const storage = { + local: {}, + session: {}, +} + +for (const type of Object.keys(storage)) { + if (options[`${type}Storage`]) { + try { + storage[type] = JSON.parse(options[`${type}Storage`]) + + if ( + !options[`${type}Storage`] || + !Object.keys(options[`${type}Storage`]).length + ) { + throw new Error('Object has no properties') + } + } catch (error) { + // eslint-disable-next-line no-console + console.log(`${type}Storage error: ${error.message || error}`) + + process.exit(1) + } + } +} + +;(async function () { + const wappalyzer = new Wappalyzer(options) + + try { + await wappalyzer.init() + + const site = await wappalyzer.open(url, headers, storage) + + await new Promise((resolve) => + setTimeout(resolve, parseInt(options.defer || 0, 10)) + ) + + const results = await site.analyze() + + process.stdout.write( + `${JSON.stringify(results, null, options.pretty ? 2 : null)}\n` + ) + + await wappalyzer.destroy() + + process.exit(0) + } catch (error) { + try { + await Promise.race([ + wappalyzer.destroy(), + new Promise((resolve, reject) => + setTimeout( + () => reject(new Error('Attempt to close the browser timed out')), + 3000 + ) + ), + ]) + } catch (error) { + // eslint-disable-next-line no-console + console.error(error.message || String(error)) + } + + // eslint-disable-next-line no-console + console.error(error.message || String(error)) + + process.exit(1) + } +})() diff --git a/src/drivers/npm/driver.js b/src/drivers/npm/driver.js new file mode 100644 index 000000000..abfbcada2 --- /dev/null +++ b/src/drivers/npm/driver.js @@ -0,0 +1,1481 @@ +const fs = require('fs') +const dns = require('dns').promises +const path = require('path') +const http = require('http') +const https = require('https') +const puppeteer = require('puppeteer') +const Wappalyzer = require('./wappalyzer') + +const { setTechnologies, setCategories, analyze, analyzeManyToMany, resolve } = + Wappalyzer + +const { CHROMIUM_BIN, CHROMIUM_DATA_DIR, CHROMIUM_WEBSOCKET, CHROMIUM_ARGS } = + process.env + +const chromiumArgs = CHROMIUM_ARGS + ? CHROMIUM_ARGS.split(' ') + : [ + '--headless', + '--single-process', + '--no-sandbox', + '--no-zygote', + '--disable-gpu', + '--ignore-certificate-errors', + '--allow-running-insecure-content', + '--disable-web-security', + `--user-data-dir=${CHROMIUM_DATA_DIR || '/tmp/chromium'}`, + ] + +const extensions = /^([^.]+$|\.(asp|aspx|cgi|htm|html|jsp|php)$)/ + +const categories = JSON.parse( + fs.readFileSync(path.resolve(`${__dirname}/categories.json`)) +) + +let technologies = {} + +for (const index of Array(27).keys()) { + const character = index ? String.fromCharCode(index + 96) : '_' + + technologies = { + ...technologies, + ...JSON.parse( + fs.readFileSync( + path.resolve(`${__dirname}/technologies/${character}.json`) + ) + ), + } +} + +setTechnologies(technologies) +setCategories(categories) + +const xhrDebounce = [] + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)) +} + +function getJs(page, technologies = Wappalyzer.technologies) { + return page.evaluate((technologies) => { + return technologies + .filter(({ js }) => Object.keys(js).length) + .map(({ name, js }) => ({ name, chains: Object.keys(js) })) + .reduce((technologies, { name, chains }) => { + chains.forEach((chain) => { + chain = chain.replace(/\[([^\]]+)\]/g, '.$1') + + const parts = chain.split('.') + + const root = /^[a-z_$][a-z0-9_$]*$/i.test(parts[0]) + ? // eslint-disable-next-line no-new-func + new Function( + `return typeof ${ + parts[0] + } === 'undefined' ? undefined : ${parts.shift()}` + )() + : window + + const value = parts.reduce( + (value, method) => + value && + value instanceof Object && + Object.prototype.hasOwnProperty.call(value, method) + ? value[method] + : '__UNDEFINED__', + root || '__UNDEFINED__' + ) + + if (value !== '__UNDEFINED__') { + technologies.push({ + name, + chain, + value: + typeof value === 'string' || typeof value === 'number' + ? value + : !!value, + }) + } + }) + + return technologies + }, []) + }, technologies) +} + +function analyzeJs(js, technologies = Wappalyzer.technologies) { + return js + .map(({ name, chain, value }) => { + return analyzeManyToMany( + technologies.find(({ name: _name }) => name === _name), + 'js', + { [chain]: [value] } + ) + }) + .flat() +} + +function getDom(page, technologies = Wappalyzer.technologies) { + return page.evaluate((technologies) => { + return technologies + .filter(({ dom }) => dom && dom.constructor === Object) + .reduce((technologies, { name, dom }) => { + const toScalar = (value) => + typeof value === 'string' || typeof value === 'number' + ? value + : !!value + + Object.keys(dom).forEach((selector) => { + let nodes = [] + + try { + nodes = document.querySelectorAll(selector) + } catch (error) { + // Continue + } + + if (!nodes.length) { + return + } + + dom[selector].forEach(({ exists, text, properties, attributes }) => { + nodes.forEach((node) => { + if ( + technologies.filter(({ name: _name }) => _name === name) + .length >= 50 + ) { + return + } + + if ( + exists && + technologies.findIndex( + ({ name: _name, selector: _selector, exists }) => + name === _name && selector === _selector && exists === '' + ) === -1 + ) { + technologies.push({ + name, + selector, + exists: '', + }) + } + + if (text) { + // eslint-disable-next-line unicorn/prefer-text-content + const value = ( + node.textContent ? node.textContent.trim() : '' + ).slice(0, 1000000) + + if ( + value && + technologies.findIndex( + ({ name: _name, selector: _selector, text }) => + name === _name && selector === _selector && text === value + ) === -1 + ) { + technologies.push({ + name, + selector, + text: value, + }) + } + } + + if (properties) { + Object.keys(properties).forEach((property) => { + if ( + Object.prototype.hasOwnProperty.call(node, property) && + technologies.findIndex( + ({ + name: _name, + selector: _selector, + property: _property, + value, + }) => + name === _name && + selector === _selector && + property === _property && + value === toScalar(value) + ) === -1 + ) { + const value = node[property] + + if (typeof value !== 'undefined') { + technologies.push({ + name, + selector, + property, + value: toScalar(value), + }) + } + } + }) + } + + if (attributes) { + Object.keys(attributes).forEach((attribute) => { + if ( + node.hasAttribute(attribute) && + technologies.findIndex( + ({ + name: _name, + selector: _selector, + attribute: _atrribute, + value, + }) => + name === _name && + selector === _selector && + attribute === _atrribute && + value === toScalar(value) + ) === -1 + ) { + const value = node.getAttribute(attribute) + + technologies.push({ + name, + selector, + attribute, + value: toScalar(value), + }) + } + }) + } + }) + }) + }) + + return technologies + }, []) + }, technologies) +} + +function analyzeDom(dom, technologies = Wappalyzer.technologies) { + return dom + .map(({ name, selector, exists, text, property, attribute, value }) => { + const technology = technologies.find(({ name: _name }) => name === _name) + + if (typeof exists !== 'undefined') { + return analyzeManyToMany(technology, 'dom.exists', { + [selector]: [''], + }) + } + + if (typeof text !== 'undefined') { + return analyzeManyToMany(technology, 'dom.text', { + [selector]: [text], + }) + } + + if (typeof property !== 'undefined') { + return analyzeManyToMany(technology, `dom.properties.${property}`, { + [selector]: [value], + }) + } + + if (typeof attribute !== 'undefined') { + return analyzeManyToMany(technology, `dom.attributes.${attribute}`, { + [selector]: [value], + }) + } + }) + .flat() +} + +function get(url, options = {}) { + const timeout = + options.timeout || + (this.options.fast + ? this.Math.min(this.options.maxWait, 3000) + : this.options.maxWait) + + if (['http:', 'https:'].includes(url.protocol)) { + const { get } = url.protocol === 'http:' ? http : https + + return new Promise((resolve, reject) => + get( + url, + { + rejectUnauthorized: false, + headers: { + 'User-Agent': options.userAgent, + }, + }, + (response) => { + if (response.statusCode >= 300) { + return reject( + new Error(`${response.statusCode} ${response.statusMessage}`) + ) + } + + response.setEncoding('utf8') + + let body = '' + + response.on('data', (data) => (body += data)) + response.on('error', (error) => reject(new Error(error.message))) + response.on('end', () => resolve(body)) + } + ) + .setTimeout(timeout, () => + reject(new Error(`Timeout (${url}, ${timeout}ms)`)) + ) + .on('error', (error) => reject(new Error(error.message))) + ) + } else { + throw new Error(`Invalid protocol: ${url.protocol}`) + } +} + +class Driver { + constructor(options = {}) { + this.options = { + batchSize: 5, + debug: false, + delay: 500, + htmlMaxCols: 2000, + htmlMaxRows: 3000, + maxDepth: 3, + maxUrls: 10, + maxWait: 30000, + recursive: false, + probe: false, + proxy: false, + noScripts: false, + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36', + extended: false, + ...options, + } + + this.options.debug = Boolean(+this.options.debug) + this.options.fast = Boolean(+this.options.fast) + this.options.recursive = Boolean(+this.options.recursive) + this.options.probe = + String(this.options.probe || '').toLowerCase() === 'basic' + ? 'basic' + : String(this.options.probe || '').toLowerCase() === 'full' + ? 'full' + : Boolean(+this.options.probe) && 'full' + this.options.delay = parseInt(this.options.delay, 10) + this.options.maxDepth = parseInt(this.options.maxDepth, 10) + this.options.maxUrls = parseInt(this.options.maxUrls, 10) + this.options.maxWait = parseInt(this.options.maxWait, 10) + this.options.htmlMaxCols = parseInt(this.options.htmlMaxCols, 10) + this.options.htmlMaxRows = parseInt(this.options.htmlMaxRows, 10) + this.options.noScripts = Boolean(+this.options.noScripts) + this.options.extended = Boolean(+this.options.extended) + + if (this.options.proxy) { + chromiumArgs.push(`--proxy-server=${this.options.proxy}`) + } + + this.destroyed = false + } + + async init() { + for (let attempt = 1; attempt <= 2; attempt++) { + this.log(`Launching browser (attempt ${attempt})...`) + + try { + if (CHROMIUM_WEBSOCKET) { + this.browser = await puppeteer.connect({ + ignoreHTTPSErrors: true, + acceptInsecureCerts: true, + browserWSEndpoint: CHROMIUM_WEBSOCKET, + }) + } else { + this.browser = await puppeteer.launch({ + ignoreHTTPSErrors: true, + acceptInsecureCerts: true, + args: chromiumArgs, + executablePath: CHROMIUM_BIN, + timeout: this.options.fast + ? Math.min(this.options.maxWait, 10000) + : this.options.maxWait, + }) + } + + break + } catch (error) { + this.log(error) + + if (attempt >= 2) { + throw new Error(error.message || error.toString()) + } + } + } + + this.browser.on('disconnected', () => { + this.browser = undefined + + this.log('Browser disconnected') + }) + } + + async destroy() { + if (this.browser) { + try { + await sleep(1) + + await this.browser.close() + + this.log('Browser closed') + } catch (error) { + throw new Error(error.toString()) + } + } + } + + async open(url, headers = {}, storage = {}) { + const site = new Site(url.split('#')[0], headers, this) + + if (storage.local || storage.session) { + this.log('Setting storage...') + + const page = await site.newPage(site.originalUrl) + + await page.setRequestInterception(true) + + page.on('request', (request) => + request.respond({ + status: 200, + contentType: 'text/plain', + body: 'ok', + }) + ) + + await page.goto(url) + + await page.evaluate((storage) => { + ;['local', 'session'].forEach((type) => { + Object.keys(storage[type] || {}).forEach((key) => { + window[`${type}Storage`].setItem(key, storage[type][key]) + }) + }) + }, storage) + + try { + await page.close() + } catch { + // Continue + } + } + + return site + } + + log(message, source = 'driver') { + if (this.options.debug) { + // eslint-disable-next-line no-console + console.log(`log | ${source} |`, message) + } + } +} + +class Site { + constructor(url, headers = {}, driver) { + ;({ + options: this.options, + browser: this.browser, + init: this.initDriver, + } = driver) + + this.options.headers = { + ...this.options.headers, + ...headers, + } + + this.driver = driver + + try { + this.originalUrl = new URL(url) + } catch (error) { + throw new Error(error.toString()) + } + + this.analyzedUrls = {} + this.analyzedXhr = {} + this.analyzedRequires = {} + this.detections = [] + + this.listeners = {} + + this.pages = [] + + this.cache = {} + + this.probed = false + } + + log(message, source = 'driver', type = 'log') { + if (this.options.debug) { + // eslint-disable-next-line no-console + console[type](`${type} | ${source} |`, message) + } + + this.emit(type, { message, source }) + } + + error(error, source = 'driver') { + this.log(error, source, 'error') + } + + on(event, callback) { + if (!this.listeners[event]) { + this.listeners[event] = [] + } + + this.listeners[event].push(callback) + } + + emit(event, params) { + if (this.listeners[event]) { + return Promise.allSettled( + this.listeners[event].map((listener) => listener(params)) + ) + } + } + + promiseTimeout( + promise, + fallback, + errorMessage = 'Operation took too long to complete', + maxWait = this.options.fast + ? Math.min(this.options.maxWait, 2000) + : this.options.maxWait + ) { + let timeout = null + + if (!(promise instanceof Promise)) { + return Promise.resolve(promise) + } + + return Promise.race([ + new Promise((resolve, reject) => { + timeout = setTimeout(() => { + clearTimeout(timeout) + + const error = new Error(errorMessage) + + error.code = 'PROMISE_TIMEOUT_ERROR' + + if (fallback !== undefined) { + this.error(error) + + resolve(fallback) + } else { + reject(error) + } + }, maxWait) + }), + promise.then((value) => { + clearTimeout(timeout) + + return value + }), + ]) + } + + async goto(url) { + // Return when the URL is a duplicate or maxUrls has been reached + if (this.analyzedUrls[url.href]) { + return [] + } + + this.log(`Navigate to ${url}`) + + this.analyzedUrls[url.href] = { + status: 0, + } + + const page = await this.newPage(url) + + await page.setRequestInterception(true) + + let responseReceived = false + + page.on('request', async (request) => { + try { + if (request.resourceType() === 'xhr') { + let hostname + + try { + ;({ hostname } = new URL(request.url())) + } catch (error) { + request.abort('blockedbyclient') + + return + } + + if (!xhrDebounce.includes(hostname)) { + xhrDebounce.push(hostname) + + setTimeout(async () => { + xhrDebounce.splice(xhrDebounce.indexOf(hostname), 1) + + this.analyzedXhr[url.hostname] = + this.analyzedXhr[url.hostname] || [] + + if (!this.analyzedXhr[url.hostname].includes(hostname)) { + this.analyzedXhr[url.hostname].push(hostname) + + await this.onDetect(url, analyze({ xhr: hostname })) + } + }, 1000) + } + } + + if ( + (responseReceived && request.isNavigationRequest()) || + request.frame() !== page.mainFrame() || + !['document', ...(this.options.noScripts ? [] : ['script'])].includes( + request.resourceType() + ) + ) { + request.abort('blockedbyclient') + } else { + await this.emit('request', { page, request }) + + if (Object.keys(this.options.headers).length) { + const headers = { + ...request.headers(), + ...this.options.headers, + } + + request.continue({ headers }) + } else { + request.continue() + } + } + } catch (error) { + error.message += ` (${url})` + + this.error(error) + } + }) + + page.on('response', async (response) => { + if (!page || page.__closed || page.isClosed()) { + return + } + + try { + if ( + response.status() < 300 && + response.frame().url() === url.href && + response.request().resourceType() === 'script' + ) { + const scripts = await response.text() + + await this.onDetect(response.url(), analyze({ scripts })) + } + } catch (error) { + if (error.constructor.name !== 'ProtocolError') { + error.message += ` (${url})` + + this.error(error) + } + } + + try { + if (response.url() === url.href) { + this.analyzedUrls[url.href] = { + status: response.status(), + } + + const rawHeaders = response.headers() + const headers = {} + + Object.keys(rawHeaders).forEach((key) => { + headers[key] = [ + ...(headers[key] || []), + ...(Array.isArray(rawHeaders[key]) + ? rawHeaders[key] + : [rawHeaders[key]]), + ] + }) + + // Prevent cross-domain redirects + if (response.status() >= 300 && response.status() < 400) { + if (headers.location) { + const _url = new URL(headers.location.slice(-1), url) + + const redirects = Object.keys(this.analyzedUrls).length - 1 + + if ( + _url.hostname.replace(/^www\./, '') === + this.originalUrl.hostname.replace(/^www\./, '') || + (redirects < 3 && !this.options.noRedirect) + ) { + url = _url + + return + } + } + } + + responseReceived = true + + const certIssuer = response.securityDetails() + ? response.securityDetails().issuer() + : '' + + await this.onDetect(url, analyze({ headers, certIssuer })) + + await this.emit('response', { page, response, headers, certIssuer }) + } + } catch (error) { + error.message += ` (${url})` + + this.error(error) + } + }) + + try { + await page.goto(url.href) + + if (page.url() === 'about:blank') { + const error = new Error(`The page failed to load (${url})`) + + error.code = 'WAPPALYZER_PAGE_EMPTY' + + throw error + } + + if (!this.options.noScripts) { + await sleep(this.options.fast ? 1000 : 3000) + } + + // page.on('console', (message) => this.log(message.text())) + + // Cookies + let cookies = [] + + try { + cookies = (await page.cookies()).reduce( + (cookies, { name, value }) => ({ + ...cookies, + [name.toLowerCase()]: [value], + }), + {} + ) + + // Change Google Analytics 4 cookie from _ga_XXXXXXXXXX to _ga_* + Object.keys(cookies).forEach((name) => { + if (/_ga_[A-Z0-9]+/.test(name)) { + cookies['_ga_*'] = cookies[name] + + delete cookies[name] + } + }) + } catch (error) { + error.message += ` (${url})` + + this.error(error) + } + + // HTML + let html = await this.promiseTimeout(page.content(), '', 'Timeout (html)') + + if (this.options.htmlMaxCols && this.options.htmlMaxRows) { + const batches = [] + const rows = html.length / this.options.htmlMaxCols + + for (let i = 0; i < rows; i += 1) { + if ( + i < this.options.htmlMaxRows / 2 || + i > rows - this.options.htmlMaxRows / 2 + ) { + batches.push( + html.slice( + i * this.options.htmlMaxCols, + (i + 1) * this.options.htmlMaxCols + ) + ) + } + } + + html = batches.join('\n') + } + + let links = [] + let text = '' + let css = '' + let scriptSrc = [] + let scripts = [] + let meta = [] + let js = [] + let dom = [] + + if (html) { + await Promise.all([ + (async () => { + // Links + links = !this.options.recursive + ? [] + : await this.promiseTimeout( + ( + await this.promiseTimeout( + page.evaluateHandle(() => + Array.from(document.getElementsByTagName('a')).map( + ({ + hash, + hostname, + href, + pathname, + protocol, + rel, + }) => ({ + hash, + hostname, + href, + pathname, + protocol, + rel, + }) + ) + ), + { jsonValue: () => [] }, + 'Timeout (links)' + ) + ).jsonValue(), + [], + 'Timeout (links)' + ) + })(), + (async () => { + // Text + text = await this.promiseTimeout( + ( + await this.promiseTimeout( + page.evaluateHandle( + () => + // eslint-disable-next-line unicorn/prefer-text-content + document.body && document.body.innerText + ), + { jsonValue: () => '' }, + 'Timeout (text)' + ) + ).jsonValue(), + '', + 'Timeout (text)' + ) + })(), + (async () => { + // CSS + css = await this.promiseTimeout( + ( + await this.promiseTimeout( + page.evaluateHandle((maxRows) => { + const css = [] + + try { + if (!document.styleSheets.length) { + return '' + } + + for (const sheet of Array.from(document.styleSheets)) { + for (const rules of Array.from(sheet.cssRules)) { + css.push(rules.cssText) + + if (css.length >= maxRows) { + break + } + } + } + } catch (error) { + return '' + } + + return css.join('\n') + }, this.options.htmlMaxRows), + { jsonValue: () => '' }, + 'Timeout (css)' + ) + ).jsonValue(), + '', + 'Timeout (css)' + ) + })(), + (async () => { + // Script tags + ;[scriptSrc, scripts] = await this.promiseTimeout( + ( + await this.promiseTimeout( + page.evaluateHandle(() => { + const nodes = Array.from( + document.getElementsByTagName('script') + ) + + return [ + nodes + .filter( + ({ src }) => + src && !src.startsWith('data:text/javascript;') + ) + .map(({ src }) => src), + nodes + .map((node) => node.textContent) + .filter((script) => script), + ] + }), + { jsonValue: () => [] }, + 'Timeout (scripts)' + ) + ).jsonValue(), + [], + 'Timeout (scripts)' + ) + })(), + (async () => { + // Meta tags + meta = await this.promiseTimeout( + ( + await this.promiseTimeout( + page.evaluateHandle(() => + Array.from(document.querySelectorAll('meta')).reduce( + (metas, meta) => { + const key = + meta.getAttribute('name') || + meta.getAttribute('property') + + if (key) { + metas[key.toLowerCase()] = + metas[key.toLowerCase()] || [] + + metas[key.toLowerCase()].push( + meta.getAttribute('content') + ) + } + + return metas + }, + {} + ) + ), + { jsonValue: () => [] }, + 'Timeout (meta)' + ) + ).jsonValue(), + [], + 'Timeout (meta)' + ) + })(), + (async () => { + // JavaScript + js = this.options.noScripts + ? [] + : await this.promiseTimeout(getJs(page), [], 'Timeout (js)') + })(), + (async () => { + // DOM + dom = await this.promiseTimeout(getDom(page), [], 'Timeout (dom)') + })(), + ]) + } + + this.cache[url.href] = { + page, + html, + text, + cookies, + scripts, + scriptSrc, + meta, + } + + await this.onDetect( + url, + [ + analyzeDom(dom), + analyzeJs(js), + analyze({ + url, + cookies, + html, + text, + css, + scripts, + scriptSrc, + meta, + }), + ].flat() + ) + + const reducedLinks = Array.prototype.reduce.call( + links, + (results, link) => { + if ( + results && + Object.prototype.hasOwnProperty.call( + Object.getPrototypeOf(results), + 'push' + ) && + link.protocol && + link.protocol.match(/https?:/) && + link.hostname === url.hostname && + extensions.test(link.pathname.slice(-5)) + ) { + results.push(new URL(link.href.split('#')[0])) + } + + return results + }, + [] + ) + + await this.emit('goto', { + page, + url, + links: reducedLinks, + ...this.cache[url.href], + }) + + page.__closed = true + + try { + await page.close() + + this.log(`Page closed (${url})`) + } catch (error) { + // Continue + } + + return reducedLinks + } catch (error) { + page.__closed = true + + try { + await page.close() + + this.log(`Page closed (${url})`) + } catch (error) { + // Continue + } + + if (error.message.includes('net::ERR_NAME_NOT_RESOLVED')) { + const newError = new Error( + `Hostname could not be resolved (${url.hostname})` + ) + + newError.code = 'WAPPALYZER_DNS_ERROR' + + throw newError + } + + if ( + error.constructor.name === 'TimeoutError' || + error.code === 'PROMISE_TIMEOUT_ERROR' + ) { + error.code = 'WAPPALYZER_TIMEOUT_ERROR' + } + + error.message += ` (${url})` + + throw error + } + } + + async newPage(url) { + if (!this.browser) { + await this.initDriver() + + if (!this.browser) { + throw new Error('Browser closed') + } + } + + let page + + try { + page = await this.browser.newPage() + + if (!page || page.isClosed()) { + throw new Error('Page did not open') + } + } catch (error) { + error.message += ` (${url})` + + this.error(error) + + await this.initDriver() + + page = await this.browser.newPage() + } + + this.pages.push(page) + + page.setJavaScriptEnabled(!this.options.noScripts) + + page.setDefaultTimeout(this.options.maxWait) + + await page.setUserAgent(this.options.userAgent) + + page.on('dialog', (dialog) => dialog.dismiss()) + + page.on('error', (error) => { + error.message += ` (${url})` + + this.error(error) + }) + + return page + } + + async analyze(url = this.originalUrl, index = 1, depth = 1) { + if (this.options.recursive) { + await sleep(this.options.delay * index) + } + + await Promise.allSettled([ + (async () => { + try { + const links = ((await this.goto(url)) || []).filter( + ({ href }) => !this.analyzedUrls[href] + ) + + if ( + links.length && + this.options.recursive && + Object.keys(this.analyzedUrls).length < this.options.maxUrls && + depth < this.options.maxDepth + ) { + await this.batch( + links.slice( + 0, + this.options.maxUrls - Object.keys(this.analyzedUrls).length + ), + depth + 1 + ) + } + } catch (error) { + this.analyzedUrls[url.href] = { + status: this.analyzedUrls[url.href]?.status || 0, + error: error.message || error.toString(), + } + + error.message += ` (${url})` + + this.error(error) + } + })(), + (async () => { + if (this.options.probe && !this.probed) { + this.probed = true + + await this.probe(url) + } + })(), + ]) + + const patterns = this.options.extended + ? this.detections.reduce( + ( + patterns, + { + technology: { name, implies, excludes }, + pattern: { regex, value, match, confidence, type, version }, + } + ) => { + patterns[name] = patterns[name] || [] + + patterns[name].push({ + type, + regex: regex.source, + value: String(value).length <= 250 ? value : null, + match: match.length <= 250 ? match : null, + confidence, + version, + implies: implies.map(({ name }) => name), + excludes: excludes.map(({ name }) => name), + }) + + return patterns + }, + {} + ) + : undefined + + const results = { + urls: this.analyzedUrls, + technologies: resolve(this.detections).map( + ({ + slug, + name, + description, + confidence, + version, + icon, + website, + cpe, + categories, + rootPath, + }) => ({ + slug, + name, + description, + confidence, + version: version || null, + icon, + website, + cpe, + categories: categories.map(({ id, slug, name }) => ({ + id, + slug, + name, + })), + rootPath, + }) + ), + patterns, + } + + await this.emit('analyze', results) + + return results + } + + async probe(url) { + const paths = [ + { + type: 'robots', + path: '/robots.txt', + }, + ] + + if (this.options.probe === 'full') { + Wappalyzer.technologies + .filter(({ probe }) => Object.keys(probe).length) + .forEach((technology) => { + paths.push( + ...Object.keys(technology.probe).map((path) => ({ + type: 'probe', + path, + technology, + })) + ) + }) + } + + // DNS + const records = {} + const resolveDns = (func, hostname) => { + return this.promiseTimeout( + func(hostname).catch((error) => { + if (error.code !== 'ENODATA') { + error.message += ` (${url})` + + this.error(error) + } + + return [] + }), + [], + 'Timeout (dns)', + this.options.fast + ? Math.min(this.options.maxWait, 15000) + : this.options.maxWait + ) + } + + const domain = url.hostname.replace(/^www\./, '') + + await Promise.allSettled([ + // Static files + ...paths.map(async ({ type, path, technology }, index) => { + try { + await sleep(this.options.delay * index) + + const body = await get(new URL(path, url.href), { + userAgent: this.options.userAgent, + timeout: Math.min(this.options.maxWait, 3000), + }) + + this.log(`Probe ok (${path})`) + + const text = body.slice(0, 100000) + + await this.onDetect( + url, + analyze( + { + [type]: path ? { [path]: [text] } : text, + }, + technology && [technology] + ) + ) + } catch (error) { + this.error(`Probe failed (${path}): ${error.message || error}`) + } + }), + // DNS + // eslint-disable-next-line no-async-promise-executor + new Promise(async (resolve, reject) => { + ;[records.cname, records.ns, records.mx, records.txt, records.soa] = + await Promise.all([ + resolveDns(dns.resolveCname, url.hostname), + resolveDns(dns.resolveNs, domain), + resolveDns(dns.resolveMx, domain), + resolveDns(dns.resolveTxt, domain), + resolveDns(dns.resolveSoa, domain), + ]) + + const dnsRecords = Object.keys(records).reduce((dns, type) => { + dns[type] = dns[type] || [] + + Array.prototype.push.apply( + dns[type], + Array.isArray(records[type]) + ? records[type].map((value) => { + return typeof value === 'object' + ? Object.values(value).join(' ') + : value + }) + : [Object.values(records[type]).join(' ')] + ) + + return dns + }, {}) + + this.log( + `Probe DNS ok: (${Object.values(dnsRecords).flat().length} records)` + ) + + await this.onDetect(url, analyze({ dns: dnsRecords })) + + resolve() + }), + ]) + } + + async batch(links, depth, batch = 0) { + if (links.length === 0) { + return + } + + const batched = links.splice(0, this.options.batchSize) + + await Promise.allSettled( + batched.map((link, index) => this.analyze(link, index, depth)) + ) + + await this.batch(links, depth, batch + 1) + } + + async onDetect(url, detections = []) { + this.detections = this.detections + .concat(detections) + .filter( + ( + { technology: { name }, pattern: { regex }, version }, + index, + detections + ) => + detections.findIndex( + ({ + technology: { name: _name }, + pattern: { regex: _regex }, + version: _version, + }) => + name === _name && + version === _version && + (!regex || regex.toString() === _regex.toString()) + ) === index + ) + + // Track if technology was identified on website's root path + detections.forEach(({ technology: { name } }) => { + const detection = this.detections.find( + ({ technology: { name: _name } }) => name === _name + ) + + detection.rootPath = detection.rootPath || url.pathname === '/' + }) + + if (this.cache[url.href]) { + const resolved = resolve(this.detections) + + const requires = [ + ...Wappalyzer.requires.filter(({ name }) => + resolved.some(({ name: _name }) => _name === name) + ), + ...Wappalyzer.categoryRequires.filter(({ categoryId }) => + resolved.some(({ categories }) => + categories.some(({ id }) => id === categoryId) + ) + ), + ] + + await Promise.allSettled( + requires.map(async ({ name, categoryId, technologies }) => { + const id = categoryId + ? `category:${categoryId}` + : `technology:${name}` + + this.analyzedRequires[url.href] = + this.analyzedRequires[url.href] || [] + + if (!this.analyzedRequires[url.href].includes(id)) { + this.analyzedRequires[url.href].push(id) + + const { page, cookies, html, text, css, scripts, scriptSrc, meta } = + this.cache[url.href] + + const js = await this.promiseTimeout( + getJs(page, technologies), + [], + 'Timeout (js)' + ) + const dom = await this.promiseTimeout( + getDom(page, technologies), + [], + 'Timeout (dom)' + ) + + await this.onDetect( + url, + [ + analyzeDom(dom, technologies), + analyzeJs(js, technologies), + await analyze( + { + url, + cookies, + html, + text, + css, + scripts, + scriptSrc, + meta, + }, + technologies + ), + ].flat() + ) + } + }) + ) + } + } + + async destroy() { + await Promise.allSettled( + this.pages.map(async (page) => { + if (page) { + page.__closed = true + + try { + await page.close() + } catch (error) { + // Continue + } + } + }) + ) + + this.log('Site closed') + } +} + +module.exports = Driver diff --git a/src/drivers/npm/package.json b/src/drivers/npm/package.json new file mode 100644 index 000000000..f9721e5b3 --- /dev/null +++ b/src/drivers/npm/package.json @@ -0,0 +1,46 @@ +{ + "name": "wappalyzer", + "description": "Identify technology on websites", + "keywords": [ + "analyze", + "identify", + "detect", + "detector", + "technology", + "cms", + "framework", + "library", + "software" + ], + "homepage": "https://www.wappalyzer.com/", + "version": "6.10.66", + "author": "Wappalyzer", + "license": "GPL-3.0", + "repository": { + "type": "git", + "url": "https://github.com/wappalyzer/wappalyzer" + }, + "funding": [ + { + "url": "https://github.com/sponsors/aliasio" + } + ], + "main": "driver.js", + "files": [ + "cli.js", + "categories.json", + "driver.js", + "index.js", + "technologies/*", + "wappalyzer.js" + ], + "bin": { + "wappalyzer": "./cli.js" + }, + "dependencies": { + "puppeteer": "~19.7.0" + }, + "engines": { + "node": ">=16" + } +} \ No newline at end of file diff --git a/src/drivers/npm/yarn.lock b/src/drivers/npm/yarn.lock new file mode 100644 index 000000000..11397257c --- /dev/null +++ b/src/drivers/npm/yarn.lock @@ -0,0 +1,525 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.0.0": + version "7.21.4" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.21.4.tgz#d0fa9e4413aca81f2b23b9442797bda1826edb39" + integrity sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g== + dependencies: + "@babel/highlight" "^7.18.6" + +"@babel/helper-validator-identifier@^7.18.6": + version "7.19.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" + integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== + +"@babel/highlight@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" + integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== + dependencies: + "@babel/helper-validator-identifier" "^7.18.6" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@types/node@*": + version "18.15.11" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.11.tgz#b3b790f09cb1696cffcec605de025b088fa4225f" + integrity sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q== + +"@types/yauzl@^2.9.1": + version "2.10.0" + resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.0.tgz#b3248295276cf8c6f153ebe6a9aba0c988cb2599" + integrity sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw== + dependencies: + "@types/node" "*" + +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +bl@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== + +buffer@^5.2.1, buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +chalk@^2.0.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chownr@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + +chromium-bidi@0.4.5: + version "0.4.5" + resolved "https://registry.yarnpkg.com/chromium-bidi/-/chromium-bidi-0.4.5.tgz#a352e755536dde609bd2c77e4b1f0906bff8784e" + integrity sha512-rkav9YzRfAshSTG3wNXF7P7yNiI29QAo1xBXElPoCoSQR5n20q3cOyVhDv6S7+GlF/CJ/emUxlQiR0xOPurkGg== + dependencies: + mitt "3.0.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +cosmiconfig@8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.1.0.tgz#947e174c796483ccf0a48476c24e4fefb7e1aea8" + integrity sha512-0tLZ9URlPGU7JsKq0DQOQ3FoRsYX8xDZ7xMiATQfaiGMz7EHowNkbU9u1coAOmnh9p/1ySpm0RB3JNWRXM5GCg== + dependencies: + import-fresh "^3.2.1" + js-yaml "^4.1.0" + parse-json "^5.0.0" + path-type "^4.0.0" + +cross-fetch@3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" + integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== + dependencies: + node-fetch "2.6.7" + +debug@4, debug@4.3.4, debug@^4.1.1: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +devtools-protocol@0.0.1094867: + version "0.0.1094867" + resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1094867.tgz#2ab93908e9376bd85d4e0604aa2651258f13e374" + integrity sha512-pmMDBKiRVjh0uKK6CT1WqZmM3hBVSgD+N2MrgyV1uNizAZMw4tx6i/RTc+/uCsKSCmg0xXx7arCP/OFcIwTsiQ== + +end-of-stream@^1.1.0, end-of-stream@^1.4.1: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +extract-zip@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" + integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== + dependencies: + debug "^4.1.1" + get-stream "^5.1.0" + yauzl "^2.10.0" + optionalDependencies: + "@types/yauzl" "^2.9.1" + +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g== + dependencies: + pend "~1.2.0" + +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +get-stream@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + +glob@^9.2.0: + version "9.3.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-9.3.4.tgz#e75dee24891a80c25cc7ee1dd327e126b98679af" + integrity sha512-qaSc49hojMOv1EPM4EuyITjDSgSKI0rthoHnvE81tcOi1SCVndHko7auqxdQ14eiQG2NDBJBE86+2xIrbIvrbA== + dependencies: + fs.realpath "^1.0.0" + minimatch "^8.0.2" + minipass "^4.2.4" + path-scurry "^1.6.1" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +https-proxy-agent@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +inherits@^2.0.3, inherits@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +lru-cache@^7.14.1: + version "7.18.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" + integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== + +minimatch@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-8.0.2.tgz#ba35f8afeb255a4cbad4b6677b46132f3278c469" + integrity sha512-ikHGF67ODxj7vS5NKU2wvTsFLbExee+KXVCnBWh8Cg2hVJfBMQIrlo50qru/09E0EifjnU8dZhJ/iHhyXJM6Mw== + dependencies: + brace-expansion "^2.0.1" + +minipass@^4.0.2, minipass@^4.2.4: + version "4.2.5" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.5.tgz#9e0e5256f1e3513f8c34691dd68549e85b2c8ceb" + integrity sha512-+yQl7SX3bIT83Lhb4BVorMAHVuqsskxRdlmO9kTpyukp8vsm2Sn/fUOV9xlnG8/a5JsypJzap21lz/y3FBMJ8Q== + +mitt@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.0.tgz#69ef9bd5c80ff6f57473e8d89326d01c414be0bd" + integrity sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ== + +mkdirp-classic@^0.5.2: + version "0.5.3" + resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +node-fetch@2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" + +once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +path-scurry@^1.6.1: + version "1.6.3" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.6.3.tgz#4eba7183d64ef88b63c7d330bddc3ba279dc6c40" + integrity sha512-RAmB+n30SlN+HnNx6EbcpoDy9nwdpcGPnEKrJnu6GZoDWBdIjo1UQMVtW2ybtC7LC2oKLcMq8y5g8WnKLiod9g== + dependencies: + lru-cache "^7.14.1" + minipass "^4.0.2" + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== + +progress@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +proxy-from-env@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +puppeteer-core@19.7.5: + version "19.7.5" + resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-19.7.5.tgz#cedc8eb7862fe7a8aa2a25ed167c0f1230de72b2" + integrity sha512-EJuNha+SxPfaYFbkoWU80H3Wb1SiQH5fFyb2xdbWda0ziax5mhV63UMlqNfPeTDIWarwtR4OIcq/9VqY8HPOsg== + dependencies: + chromium-bidi "0.4.5" + cross-fetch "3.1.5" + debug "4.3.4" + devtools-protocol "0.0.1094867" + extract-zip "2.0.1" + https-proxy-agent "5.0.1" + proxy-from-env "1.1.0" + rimraf "4.4.0" + tar-fs "2.1.1" + unbzip2-stream "1.4.3" + ws "8.12.1" + +puppeteer@~19.7.0: + version "19.7.5" + resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-19.7.5.tgz#d7db0dfcc80ca2cdf8eb0100bae1ce888a841389" + integrity sha512-UqD8K+yaZa6/hwzP54AATCiHrEYGGxzQcse9cZzrtsVGd8wT0llCdYhsBp8n+zvnb1ofY0YFgI3TYZ/MiX5uXQ== + dependencies: + cosmiconfig "8.1.0" + https-proxy-agent "5.0.1" + progress "2.0.3" + proxy-from-env "1.1.0" + puppeteer-core "19.7.5" + +readable-stream@^3.1.1, readable-stream@^3.4.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +rimraf@4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-4.4.0.tgz#c7a9f45bb2ec058d2e60ef9aca5167974313d605" + integrity sha512-X36S+qpCUR0HjXlkDe4NAOhS//aHH0Z+h8Ckf2auGJk3PTnx5rLmrHkwNdbVQuCSUhOyFrlRvFEllZOYE+yZGQ== + dependencies: + glob "^9.2.0" + +safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +tar-fs@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" + integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.1.4" + +tar-stream@^2.1.4: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== + dependencies: + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + +through@^2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +unbzip2-stream@1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" + integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== + dependencies: + buffer "^5.2.1" + through "^2.3.8" + +util-deprecate@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +ws@8.12.1: + version "8.12.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.12.1.tgz#c51e583d79140b5e42e39be48c934131942d4a8f" + integrity sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew== + +yauzl@^2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0" diff --git a/src/drivers/webextension/images/icons/PandaCSS.svg b/src/images/icons/PandaCSS.svg similarity index 100% rename from src/drivers/webextension/images/icons/PandaCSS.svg rename to src/images/icons/PandaCSS.svg diff --git a/src/technologies/_.json b/src/technologies/_.json index 475da8fa6..412177d09 100644 --- a/src/technologies/_.json +++ b/src/technologies/_.json @@ -106,10 +106,10 @@ 6 ], "description": "42stores is a French SaaS ecommerce solution that was established in 2008. It offers a range of features such as monitoring, customer support, and regular updates. The platform is known for its flexibility and modularity, making it possible to integrate with various ERP systems.", - "icon": "42stores.svg", "headers": { "Powered-By": "^42stores$" }, + "icon": "42stores.svg", "pricing": [ "poa", "recurring" diff --git a/src/technologies/a.json b/src/technologies/a.json index 51bc0e842..0691742f4 100644 --- a/src/technologies/a.json +++ b/src/technologies/a.json @@ -1543,8 +1543,8 @@ ], "cpe": "cpe:2.3:a:adobe:experience_manager:*:*:*:*:*:*:*:*", "description": "Adobe Experience Manager Franklin, also known as Project Helix or Composability, is a new way to publish AEM pages using Google Drive or Microsoft Office via Sharepoint. Instead of components, Franklin uses blocks to build pages. Blocks are pieces of a document that will be transformed into web page content.", - "icon": "Adobe Experience Manager Franklin.svg", "excludes": "Adobe Experience Manager", + "icon": "Adobe Experience Manager Franklin.svg", "scriptSrc": "^.+/scripts/lib-franklin\\.js$", "website": "https://www.hlx.live" }, @@ -4213,7 +4213,6 @@ ], "description": "Assertive Yield is a SaaS company that specialises in helping SSPs (Supply-Side Platforms), publishers, and ad networks optimise their advertising revenue through real-time attribution and yield optimisation strategies.", "icon": "Assertive Yield.svg", - "saas": true, "js": { "assertive.predict": "" }, @@ -4221,6 +4220,7 @@ "payg", "poa" ], + "saas": true, "website": "https://www.assertiveyield.com" }, "Astra": { @@ -4671,8 +4671,8 @@ 87 ], "description": "Automatic.css is a CSS framework for WordPress page builders.", - "icon": "Automatic.css.png", "dom": "link[href*='/wp-content/uploads/automatic-css/']", + "icon": "Automatic.css.png", "pricing": [ "low", "recurring" diff --git a/src/technologies/f.json b/src/technologies/f.json index c44d45cc4..04a685879 100644 --- a/src/technologies/f.json +++ b/src/technologies/f.json @@ -881,8 +881,8 @@ 18, 22 ], - "description": "Flask is a Python micro web framework ideal for rapidly constructing web applications, offering minimalism, flexibility, and modularity.", "cpe": "cpe:2.3:a:palletsprojects:flask:*:*:*:*:*:*:*:*", + "description": "Flask is a Python micro web framework ideal for rapidly constructing web applications, offering minimalism, flexibility, and modularity.", "headers": { "Server": "Werkzeug/?([\\d\\.]+)?\\;version:\\1" }, diff --git a/src/technologies/g.json b/src/technologies/g.json index e71548497..3f30fc626 100644 --- a/src/technologies/g.json +++ b/src/technologies/g.json @@ -1570,8 +1570,8 @@ 36 ], "description": "Google AdSense is a program run by Google through which website publishers serve advertisements that are targeted to the site content and audience.", - "icon": "Google AdSense.svg", "dom": "amp-ad[type='adsense']", + "icon": "Google AdSense.svg", "js": { "Goog_AdSense_": "", "Goog_AdSense_OsdAdapter": "", diff --git a/src/technologies/i.json b/src/technologies/i.json index 814380b9f..ee58e2c74 100644 --- a/src/technologies/i.json +++ b/src/technologies/i.json @@ -1208,6 +1208,18 @@ ], "website": "https://isotope.metafizzy.co" }, + "Isso": { + "cats": [ + 15 + ], + "description": "Isso is a lightweight commenting server written in Python and JavaScript, referred to as \"Ich schrei sonst\" in German.", + "implies": "Python", + "js": { + "Isso.fetchComments": "" + }, + "oss": true, + "website": "https://github.com/posativ/isso/" + }, "Issuu": { "cats": [ 19, @@ -1229,18 +1241,6 @@ "scriptSrc": "\\.issuu\\.com/", "website": "https://issuu.com" }, - "Isso": { - "cats": [ - 15 - ], - "description": "Isso is a lightweight commenting server written in Python and JavaScript, referred to as \"Ich schrei sonst\" in German.", - "js": { - "Isso.fetchComments": "" - }, - "implies": "Python", - "oss": true, - "website": "https://github.com/posativ/isso/" - }, "Iterable": { "cats": [ 32 diff --git a/src/technologies/k.json b/src/technologies/k.json index 1d2d7858f..03ffda0d3 100644 --- a/src/technologies/k.json +++ b/src/technologies/k.json @@ -644,12 +644,12 @@ "cats": [ 53 ], - "description": "Kicksite is a gym and martial arts member management software with attendance tracking, automated billing, free texting, lead capture forms and more.", - "icon": "Kicksite.png", "cookies": { "_kicksite_session": "" }, - "dom":"iframe[src*='.kicksite.net/']", + "description": "Kicksite is a gym and martial arts member management software with attendance tracking, automated billing, free texting, lead capture forms and more.", + "dom": "iframe[src*='.kicksite.net/']", + "icon": "Kicksite.png", "pricing": [ "mid", "recurring" diff --git a/src/technologies/l.json b/src/technologies/l.json index ab4209107..83735bb1e 100644 --- a/src/technologies/l.json +++ b/src/technologies/l.json @@ -456,7 +456,6 @@ 87 ], "description": "LearnDash is a WordPress plugin that enables the creation and management of online courses, quizzes, and educational content within a website.", - "icon": "LearnDash.svg", "dom": { "link[href*='/wp-content/plugins/sfwd-lms/']": { "attributes": { @@ -464,12 +463,13 @@ } } }, - "requires": "WordPress", + "icon": "LearnDash.svg", "pricing": [ "low", "onetime", "recurring" ], + "requires": "WordPress", "website": "https://www.learndash.com" }, "LearnWorlds": { @@ -1519,23 +1519,6 @@ "scriptSrc": "lodash.*\\.js", "website": "https://www.lodash.com" }, - "Loglib": { - "cats": [ - 10 - ], - "description": "Loglib is a Open Source and Privacy-First web analytics that aims to provide simple yet can be powerful based on your needs.", - "icon": "Loglib.svg", - "js": { - "lli": "", - "llc": "" - }, - "pricing": [ - "freemium" - ], - "oss": true, - "saas": true, - "website": "https://www.loglib.io" - }, "LogRocket": { "cats": [ 10 @@ -1627,6 +1610,23 @@ ], "website": "https://www.loginradius.com" }, + "Loglib": { + "cats": [ + 10 + ], + "description": "Loglib is a Open Source and Privacy-First web analytics that aims to provide simple yet can be powerful based on your needs.", + "icon": "Loglib.svg", + "js": { + "llc": "", + "lli": "" + }, + "oss": true, + "pricing": [ + "freemium" + ], + "saas": true, + "website": "https://www.loglib.io" + }, "LogoiX": { "cats": [ 99 @@ -2095,4 +2095,4 @@ "oss": true, "website": "https://github.com/paulirish/lite-youtube-embed" } -} +} \ No newline at end of file diff --git a/src/technologies/o.json b/src/technologies/o.json index 0d2ea6533..62406882b 100644 --- a/src/technologies/o.json +++ b/src/technologies/o.json @@ -1993,4 +1993,4 @@ }, "website": "https://owncloud.org" } -} +} \ No newline at end of file diff --git a/src/technologies/p.json b/src/technologies/p.json index ca90c3330..ded79c82d 100644 --- a/src/technologies/p.json +++ b/src/technologies/p.json @@ -3380,20 +3380,6 @@ "oss": true, "website": "https://github.com/mroderick/PubSubJS" }, - "Public CMS": { - "cats": [ - 1 - ], - "cookies": { - "PUBLICCMS_USER": "" - }, - "headers": { - "X-Powered-PublicCMS": "^(.+)$\\;version:\\1" - }, - "icon": "Public CMS.png", - "implies": "Java", - "website": "https://www.publiccms.com" - }, "PubTech": { "cats": [ 67 @@ -3408,6 +3394,20 @@ ], "website": "https://www.pubtech.ai/" }, + "Public CMS": { + "cats": [ + 1 + ], + "cookies": { + "PUBLICCMS_USER": "" + }, + "headers": { + "X-Powered-PublicCMS": "^(.+)$\\;version:\\1" + }, + "icon": "Public CMS.png", + "implies": "Java", + "website": "https://www.publiccms.com" + }, "Pulse Secure": { "cats": [ 46 @@ -3924,4 +3924,4 @@ }, "website": "https://punbb.informer.com" } -} +} \ No newline at end of file diff --git a/src/technologies/s.json b/src/technologies/s.json index 6b303a21c..0cdaef522 100644 --- a/src/technologies/s.json +++ b/src/technologies/s.json @@ -795,8 +795,8 @@ ], "description": "Salla is an ecommerce platform specifically tailored to serve businesses and customers in Saudi Arabia.", "headers": { - "x-powered-by": "^Salla$", - "X-Frame-Options": "\\.salla\\.sa" + "X-Frame-Options": "\\.salla\\.sa", + "x-powered-by": "^Salla$" }, "icon": "Salla.svg", "js": { @@ -7082,7 +7082,7 @@ ], "description": "Swiper is a JavaScript library that creates modern touch sliders with hardware-accelerated transitions.", "dom": [ - "div[data-swiper-slide-index]", + "div[data-swiper-slide-index]", "swiper-container", "swiper-slide", "div.swiper-wrapper", @@ -7430,4 +7430,4 @@ }, "website": "https://styled-components.com" } -} +} \ No newline at end of file