You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
195 lines
4.8 KiB
195 lines
4.8 KiB
#!/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 <url> [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)
|
|
}
|
|
})()
|