diff --git a/.gitignore b/.gitignore index fac0153..4548555 100755 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ node_modules/ +*.db .* -!/.gitignore -package-lock.json \ No newline at end of file +!/.gitignore \ No newline at end of file diff --git a/README.md b/README.md index b4561af..a125802 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ It is based on [htcrawl](https://htcrawl.org), a node library powerful enough to - Handles XHR, fetch, JSONP and websockets requests - Supports cookies, proxy, custom headers, http auth and more - Scriptable login sequences +- Sequence recorder +- Postmessage fuzzer # GETTING STARTED ## Installation @@ -82,9 +84,11 @@ Each element of the list is an array where the first element is the name of the Actions are: - navigate <url> - write <selector> <text> +- select <selector> <value> - click <selector> - clickToNavigate <selector> - sleep <seconds> +- setTarget <selector> ## Example ``` diff --git a/chrome-extension/background.js b/chrome-extension/background.js new file mode 100644 index 0000000..d9cf6e7 --- /dev/null +++ b/chrome-extension/background.js @@ -0,0 +1,32 @@ +// Empty service_worker used as proxy between node and the UI (devtools panel) + +const messageQueue = []; +let lastPingTime = 0; +const sendMessage = (message) => { + messageQueue.push(message); +}; + + +setTimeout(async function loop(){ + try{ + if(messageQueue.length === 0 && (new Date()).getTime() - lastPingTime > 2000){ + // Keep the worker active + try{ + await chrome.runtime.sendMessage({ping: 1}); + }catch(e){} + lastPingTime = (new Date()).getTime(); + } + for(let i = messageQueue.length - 1; i >= 0; i--){ + try{ + await chrome.runtime.sendMessage({body: messageQueue[i]}); + }catch(e){ + // Failed to send message + break; + } + messageQueue.splice(i, 1); + } + }catch(e){ + console.error(`Exception from Service Worker: ${e}`) + } + setTimeout(loop, 200); +}, 200); \ No newline at end of file diff --git a/chrome-extension/manifest.json b/chrome-extension/manifest.json new file mode 100644 index 0000000..db11344 --- /dev/null +++ b/chrome-extension/manifest.json @@ -0,0 +1,24 @@ + { + "manifest_version": 3, + "name": "Domdig", + "version": "1.0", + "permissions": ["scripting", "debugger", "activeTab"], + "action": {}, + "commands": { + "open-panel": { + "description": "Open Panel", + "suggested_key": { + "default": "Ctrl+Shift+H", + "mac": "Ctrl+Shift+H" + } + } + }, + "devtools_page": "ui.html", + "background": { + "service_worker": "background.js" + }, + "host_permissions": [ + "" + ] + } + \ No newline at end of file diff --git a/chrome-extension/style.css b/chrome-extension/style.css new file mode 100644 index 0000000..fdda739 --- /dev/null +++ b/chrome-extension/style.css @@ -0,0 +1,234 @@ + +html, body { + height: 100%; + margin: 0; + padding: 0; + font-family: Arial, sans-serif; + background-color: #f4f4f9; + } + +.flex-end { + display: flex; + justify-content: flex-end; +} + +.header { + background-color: #6a1b9a; + color: #fff; + padding: 10px 20px; +} + +.container { + display: flex; + flex-direction: column; + height: calc(100% - 140px); + padding: 20px; +} + +.button-container { + margin-bottom: 40px; +} + +button { + background-color: #9c27b0; + color: white; + border: none; + padding: 10px 20px; + margin-right: 10px; + margin-bottom: 10px; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s; + font-size: 14px; + flex: 0; +} + +button:hover { + background-color: #7b1fa2; +} + + +.sequence-table { + width: 100%; + border-collapse: collapse; + text-align: left; +} + +.sequence-table th, .sequence-table td { + border: 1px solid #ddd; + padding: 8px; + vertical-align: top; +} + +.sequence-table tr:nth-child(odd) { + background-color: #f2f2f2; +} + +.sequence-table tbody tr:hover { + background-color: #ddd; +} + +.sequence-table td:nth-child(2), .sequence-table th:nth-child(2) { + width: 100%; +} + +.sequence-table td:nth-child(1), .sequence-table th:nth-child(1) { + min-width: 150px; +} + +.sequence-table td:nth-child(3) { + vertical-align: middle; +} + + +.delete-btn { + background-color: red; + font-size: 14px; +} + +input[type="text"], input[type="number"] { + width: 100%; + padding: 8px; + margin: 4px 0; + box-sizing: border-box; + border: 1px solid #ddd; + border-radius: 4px; + box-sizing: border-box; +} + +select { + width: 100%; + min-width: 130px; + padding: 8px; + margin: 4px 0; + border: 1px solid #ddd; + border-radius: 4px; + box-sizing: border-box; + -webkit-appearance: none; + appearance: none; + background-color: white; + cursor: pointer; + position: relative; + background-image: url('data:image/svg+xml;charset=US-ASCII,'); + background-repeat: no-repeat; + background-position: right 10px center; + background-size: 12px 12px; +} + +select:hover { + border-color: #bbb; +} + +select:focus { + outline: none; + border-color: #666; +} + +#selectModal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +#selectModal div { + color: white; + font-size: 24px; + text-align: center; +} + +.dropdown { + position: relative; + display: inline-block; + margin-right: 30px; + margin-top: 20px; + min-width: 120px; +} + +.dropdown-toggle { + padding: 10px; + font-size: 14px; + background-color: #9c27b0; + border-radius: 5px; + color: white; + cursor: pointer; + position: relative; + background-image: url('data:image/svg+xml;charset=US-ASCII,'); + background-repeat: no-repeat; + background-position: right 10px center; + width: 100%; + user-select: none; +} + + +.dropdown-menu { + display: none; + position: absolute; + background-color: #9c27b0; + color: white; + box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); + z-index: 999; + width: calc(100% + 20px); + padding-top:10px; + margin-top: -10px; +} + +.dropdown-menu .dropdown-item { + padding: 10px; + cursor: pointer; + transition: background-color 0.3s; +} +.dropdown-menu, .dropdown-item:last-child { + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; +} +.dropdown-menu .dropdown-item:hover { + background-color: #7b1fa2; +} + + +.tab-button { + background-color: #9c27b0; + color: #ffffffaa; + border: none; + margin-right: -1px; + position: relative; + top: 1px; + border-radius: 8px 8px 0 0; + padding: 10px 20px; + cursor: pointer; + font-size: 12px; + margin-bottom: 0; +} + +.tab-button.tab-active, .tab-button:hover { + background-color: #7b1fa2; + z-index: 1; +} +.tab-button.tab-active { + font-size: 14px; + color:white; +} + +.tab-button-container { + background-color: #f4f4f9; + border-bottom: 1px solid #7b1fa2; + padding: 0; +} + +.tab-content { + border: 1px solid #7b1fa2; + border-top: none; + padding: 20px; + display: none; +} + +.tab-content-active { + display: block; +} diff --git a/chrome-extension/ui-panel.html b/chrome-extension/ui-panel.html new file mode 100644 index 0000000..caf1d22 --- /dev/null +++ b/chrome-extension/ui-panel.html @@ -0,0 +1,78 @@ + + + + + + + +
+

DOMDIG Sequence Builder

+
+ +
+
+ + + +
+ +
+
+ + +
+
+ + + + + + + + + +
ActionParameters
+
+
+ + + + + + + + + +
ActionParameters
+
+
+ + + +
+ + + + diff --git a/chrome-extension/ui-panel.js b/chrome-extension/ui-panel.js new file mode 100644 index 0000000..91e3627 --- /dev/null +++ b/chrome-extension/ui-panel.js @@ -0,0 +1,179 @@ + +const Modal = class { + constructor(id, text){ + this.modal = document.createElement('div'); + this.modal.id = id; + const t = document.createElement('div'); + t.textContent = text; + this.modal.appendChild(t); + } + + show(){ + document.body.appendChild(this.modal); + } + hide(){ + try{ + document.body.removeChild(this.modal); + }catch(e){} + } +} + +const modal = new Modal("selectModal", 'Select an element on the page'); + +onCrawlerMessage( message => { + if(message.error){ + alert(message.error); + } else { + addRow(message.action, message.par1, message.par2); + } + modal.hide(); +}); + +document.querySelectorAll('.dropdown-toggle').forEach(e => e.addEventListener('click', function() { + this.nextElementSibling.style.display = this.nextElementSibling.style.display !== 'block' ? 'block' : 'none'; +})); + +document.querySelectorAll('.dropdown-item').forEach(e => e.addEventListener('click', function() { + this.parentNode.style.display = 'none'; +})); + +document.getElementById("finish-and-scan").addEventListener("click", () => { + pageEval(`UI.scan(${JSON.stringify(readTables())})`); +}); + +document.getElementById("finish").addEventListener("click", () => { + pageEval(`UI.end(${JSON.stringify(readTables())})`); +}); + +document.getElementById("finish-discart").addEventListener("click", () => { + if(window.confirm("Discart sequence and exit?")){ + pageEval(`UI.discart()`); + } +}); + +document.getElementById("add-action-click").addEventListener("click", () => { + modal.show(); + pageEval(`UI.selectElement("click")`); +}); + +document.getElementById("add-action-write").addEventListener("click", () => { + modal.show(); + pageEval(`UI.selectElement("write")`); +}); + +document.getElementById("add-action-select").addEventListener("click", () => { + modal.show(); + pageEval(`UI.selectElement("select")`); +}); + +document.getElementById("add-action-click-to-navigate").addEventListener("click", () => { + modal.show(); + pageEval(`UI.selectElement("clickToNavigate")`); +}); + +document.getElementById("add-action-set-target").addEventListener("click", () => { + modal.show(); + pageEval(`UI.selectElement("setTarget")`); +}); + +document.getElementById("add-action-sleep").addEventListener("click", () => { + addRow("sleep", 1); +}); + +document.getElementById("add-action-navigate").addEventListener("click", () => { + addRow("navigate"); +}); + + +function readTables() { + const ret = {start: [], runtime: []}; + for(t of Object.keys(ret)){ + for(let row of document.getElementById(`sequence-table-${t}`).tBodies[0].rows){ + ret[t].push( + [...row.querySelectorAll("input, select")].map(i => i.value || null) + .filter(v => v != null) + ); + } + } + return ret.runtime.length > 0 ? ret : ret.start; + } + +async function addRow(action, par1, par2) { + const table = document.querySelector(".tab-content-active table"); + const updateFields = () => { + text1.placeholder = ""; + text1.type = "text"; + text2.placeholder = ""; + text2.style.display = 'inline'; + + switch (actSelect.value) { + case "navigate": + text1.placeholder = "url"; + text2.style.display = 'none'; + break; + case "write": + text1.placeholder = "selector"; + text2.placeholder = "text"; + break; + case "click": + case "clickToNavigate": + case "setTarget": + text1.placeholder = "selector"; + text2.style.display = 'none'; + break; + case "sleep": + text1.placeholder = "seconds"; + text1.type = "number"; + text2.style.display = 'none'; + } + } + const row = table.tBodies[0].insertRow(); + + const actSelect = document.createElement("select"); + ["write", "select", "click", "clickToNavigate", "setTarget", "sleep", "navigate"].forEach(val => { + const option = document.createElement("option"); + option.value = val; + option.text = val; + actSelect.appendChild(option); + }); + row.insertCell(0).appendChild(actSelect); + if(action){ + actSelect.value = action; + } + const parsCell = row.insertCell(1); + const text1 = document.createElement("input"); + text1.type = "text"; + const text2 = document.createElement("input"); + text2.type = "text"; + if(par1){ + text1.value = par1; + } + if(par2){ + text2.value = par2; + } + + parsCell.appendChild(text1); + parsCell.appendChild(document.createElement("br")); + parsCell.appendChild(text2); + + actSelect.addEventListener('change', () => updateFields()); + updateFields(); + + const deleteButton = document.createElement("span"); + deleteButton.style = "color: red; cursor: pointer"; + deleteButton.textContent = "✕"; + deleteButton.onclick = function() { + table.deleteRow(row.rowIndex); + }; + row.insertCell(2).appendChild(deleteButton); +} + +document.querySelectorAll('.tab-button').forEach(b => { + b.addEventListener('click', () => { + const c = b.parentElement.parentElement; + c.querySelector('.tab-active')?.classList.remove('tab-active'); + c.querySelector('.tab-content-active')?.classList.remove('tab-content-active'); + c.querySelector(`[data-tab-name="${b.getAttribute('data-for')}"]`).classList.add("tab-content-active"); + b.classList.add('tab-active'); + }); +}); diff --git a/chrome-extension/ui.html b/chrome-extension/ui.html new file mode 100644 index 0000000..79c6b0e --- /dev/null +++ b/chrome-extension/ui.html @@ -0,0 +1,7 @@ + + + + DOMDIG DevTools Panel + + + diff --git a/chrome-extension/ui.js b/chrome-extension/ui.js new file mode 100644 index 0000000..d394bab --- /dev/null +++ b/chrome-extension/ui.js @@ -0,0 +1,10 @@ +let domdiglPanel; +chrome.devtools.panels.create( + "Domdig", + "", + "ui-panel.html", + function(panel) { + domdiglPanel = panel; + } + ); + \ No newline at end of file diff --git a/chrome-extension/utils.js b/chrome-extension/utils.js new file mode 100644 index 0000000..471f539 --- /dev/null +++ b/chrome-extension/utils.js @@ -0,0 +1,13 @@ +function pageEval(code, result){ + const wrapped = `(function(){const UI=window.__PROBE__.UI;return ${code}})();`; + chrome.devtools.inspectedWindow.eval(wrapped, result); +} + +function onCrawlerMessage(handler){ + chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) { + if(message.body){ + handler(message.body); + } + sendResponse({status: "ok"}); + }); +} \ No newline at end of file diff --git a/database.js b/database.js index 75a4ac0..15a23c7 100644 --- a/database.js +++ b/database.js @@ -34,6 +34,15 @@ const qryCreateTableVulnerability = ` ) `; +const qryCreateTableScanSettings = ` + CREATE TABLE scan_settings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + parameter TEXT, + value TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) +`; + exports.Database = class { constructor(dbName){ this.dbName = dbName; @@ -62,6 +71,7 @@ exports.Database = class { db.exec(qryCreateTableRequest); db.exec(qryCreateTableScanInfo); db.exec(qryCreateTableVulnerability); + db.exec(qryCreateTableScanSettings); db.close(); } @@ -83,4 +93,11 @@ exports.Database = class { const qry = "UPDATE vulnerability set confirmed=? where type=? and payload=? and element=? and url=?"; this.run(qry, [vulnerability.confirmed, vulnerability.type, vulnerability.payload, vulnerability.element, vulnerability.url]); } + + addScanArguments(args){ + const qry = "INSERT INTO scan_settings (parameter, value) values (?, ?)"; + for(const arg of args){ + this.run(qry, arg); + } + } } diff --git a/domdig.js b/domdig.js index 8886732..db9fa40 100755 --- a/domdig.js +++ b/domdig.js @@ -6,6 +6,8 @@ const utils = require('./utils'); const defpayloads = require('./payloads'); const URL = require('url').URL; const Database = require('./database').Database; +const SequenceBuilder = require('./sequence-builder').SequenceBuilder; +const SequenceExecutor = require('./sequence-executor').SequenceExecutor; const PAYLOADMAP = []; var PAYLOADMAP_I = 0; @@ -14,10 +16,12 @@ var VERBOSE = true; var DATABASE = null; var CRAWLER = null; var USE_SINGLE_BROWSER = false; +var TARGET_ELEMENT = null; +var SEQUENCE_EXECUTOR = null; -function getNewPayload(payload, element){ +function getNewPayload(payload, element, info){ const p = payload.replace("{0}", PAYLOADMAP_I); - PAYLOADMAP[PAYLOADMAP_I] = {payload:payload, element:element}; + PAYLOADMAP[PAYLOADMAP_I] = {payload:payload, element:element, info:JSON.stringify(info)}; PAYLOADMAP_I++; return p; } @@ -34,27 +38,6 @@ function getUrlMutations(url, payload){ return muts; } -async function scanAttributes(crawler){ - // use also 'srcdoc' since it can contain also esacped html: - // content can have a "timer" so maybe is not executed in time - const attrs = ["href", "action", "formaction", "srcdoc", "content"]; - for(let attr of attrs){ - const elems = await crawler.page().$$(`[${attr}]`); - for(let e of elems){ - // must use evaluate since puppetteer cannot get non-standard attributes - let val = await e.evaluate( (i,a) => i.getAttribute(a), attr); - if(val.startsWith(consts.SINKNAME) == false){ - continue; - } else { - let key = val.match(/\(([0-9]+)\)/)[1]; - let es = await utils.getElementSelector(e); - utils.addVulnerability(VULNSJAR, DATABASE, consts.VULNTYPE_WARNING, PAYLOADMAP[key], null, `Attribute '${attr}' of '${es}' set to payload`, VERBOSE); - break; - } - } - } -} - async function triggerOnpaste(crawler){ const elems = await crawler.page().$$('[onpaste]'); for(let e of elems){ @@ -66,42 +49,78 @@ async function triggerOnpaste(crawler){ } } -function sequenceError(message, seqline){ - if(seqline){ - message = "action " + seqline + ": " + message; +async function loadHtcrawl(targetUrl, options){ + if(!CRAWLER || !USE_SINGLE_BROWSER){ + // instantiate htcrawl + crawler = await htcrawl.launch(targetUrl, options); + CRAWLER = crawler; + } else { + crawler = CRAWLER; + firstRun = false; + await crawler.newPage(targetUrl); } - if(DATABASE){ - DATABASE.updateStatus(message, true); + if(options.localStorage){ + await crawler.page().evaluateOnNewDocument( (localStorage) => { + for(let l of localStorage){ + let fn = l.type == "L" ? window.localStorage : window.sessionStorage; + fn.setItem(l.key, l.val); + } + }, options.localStorage); + } + await crawler.page().setCacheEnabled(false); + return crawler; +} + +function fuzzObject(obj, payload) { + const copies = []; + + const createCopy = (original, path = []) => { + for (const key in original) { + if (original.hasOwnProperty(key)) { + const newPath = path.concat(key); + + if (typeof original[key] === 'object' && original[key] !== null) { + createCopy(original[key], newPath); + } else { + const newObject = structuredClone(obj); + let current = newObject; + + for (let i = 0; i < newPath.length - 1; i++) { + current = current[newPath[i]]; + } + + current[newPath[newPath.length - 1]] = payload; + copies.push(newObject); + } + } + } + }; + + createCopy(obj); + return copies; +} + +function isFuzzObject(obj) { + if (typeof obj === 'string' && obj.includes(consts.SINKNAME)) { + return true; } - console.error(chalk.red(message)); - process.exit(2); + if(typeof obj == 'object' && !!obj){ + for (let k in obj) { + if (isFuzzObject(obj[k])) { + return true; + } + } + } + return false; } + async function loadCrawler(vulntype, targetUrl, payload, setXSSSink, checkTplInj, options){ - // var hashSet = false; var loaded = false; var crawler; var retries = 4; - var firstRun = true; - //options.openChromeDevtoos = true; do{ - if(!CRAWLER || !USE_SINGLE_BROWSER){ - // instantiate htcrawl - crawler = await htcrawl.launch(targetUrl, options); - CRAWLER = crawler; - } else { - crawler = CRAWLER; - firstRun = false; - await crawler.newPage(targetUrl); - } - if(options.localStorage){ - await crawler.page().evaluateOnNewDocument( (localStorage) => { - for(let l of localStorage){ - let fn = l.type == "L" ? window.localStorage : window.sessionStorage; - fn.setItem(l.key, l.val); - } - }, options.localStorage); - } + crawler = await loadHtcrawl(targetUrl, options); const handleRequest = async function(e, crawler){ if(options.printRequests){ @@ -117,6 +136,54 @@ async function loadCrawler(vulntype, targetUrl, payload, setXSSSink, checkTplInj crawler.on("navigation", handleRequest); crawler.on("jsonp", handleRequest); crawler.on("websocket", handleRequest); + + crawler.page().exposeFunction("__domdig_on_postmessage__", async (message, origin, url) => { + // console.log(message, origin) + if(isFuzzObject(message)){ + return; + } + const p = getNewPayload(payload, `postMessage/${origin}`) + const fuzzMessages = fuzzObject(message, p); + + const frames = await crawler.page().frames(); + let src; + for(const frame of frames){ + // console.log(frame.url()) + const fu = new URL(frame.url()); + if(fu.origin == origin){ + src = frame; + } + } + src.evaluate( (dst, messages) => { + if(window.top.location.toString() == dst){ + for(let message of messages){ + window.top.postMessage(message, "*"); + } + } else { + window.top.document.querySelectorAll("iframe").forEach(frame => { + if(frame.contentWindow.document.location.toString() == dst){ + for(let message of messages){ + frame.contentWindow.postMessage(message, "*"); + } + } + }) + } + }, url, fuzzMessages); + }) + crawler.page().evaluateOnNewDocument(() => { + window.addEventListener("message", async event => { + await window.__domdig_on_postmessage__(event.data, event.origin, `${document.location}`); + }); + }); + crawler.page().on("frameattached", async frame => { + try{ + await frame.evaluate(() => { + window.addEventListener("message", async event => { + await window.__domdig_on_postmessage__(event.data, event.origin, `${document.location}`); + }); + }); + }catch(e){} + }); if(!options.dryRun){ if(setXSSSink){ crawler.page().exposeFunction(consts.SINKNAME, function(key) { @@ -136,31 +203,18 @@ async function loadCrawler(vulntype, targetUrl, payload, setXSSSink, checkTplInj const p = getNewPayload(payload, e.params.element); try{ await crawler.page().$eval(e.params.element, (i, p) => i.value = p, p); + + // return false to prevent element to be automatically filled with a random value + // we need to manually trigger angularjs 'input' event that won't be triggered by htcrawl (due to return false) + await crawler.page().$eval(e.params.element, el => { + const evt = document.createEvent('HTMLEvents'); + evt.initEvent("input", true, false); + el.dispatchEvent(evt); + }); }catch(e){} - // return false to prevent element to be automatically filled with a random value - // we need to manually trigger angularjs 'input' event that won't be triggered by htcrawl (due to return false) - crawler.page().$eval(e.params.element, el => { - const evt = document.createEvent('HTMLEvents'); - evt.initEvent("input", true, false); - el.dispatchEvent(evt); - }); return false; }); - // change page hash before the triggering of the first event - // to see if some code, during crawling, takes the hash and evaluates our payload - // It will result in a sort of assisted-XSS where the victim, after following the XSS URL, - // has to perform some actions. - // It's useless since the same (a better) test is performed by the Reflected XSS check. - // crawler.on("triggerevent", async function(e, crawler){ - // if(!hashSet){ - // const p = getNewPayload(payload, "hash"); - // await crawler.page().evaluate(p => document.location.hash = p, p); - // hashSet = true; - // PREVURL = crawler.page().url(); - // } - // }); - if(checkTplInj){ crawler.on("eventtriggered", async function(e, crawler){ var cont = await crawler.page().content(); @@ -193,70 +247,40 @@ async function loadCrawler(vulntype, targetUrl, payload, setXSSSink, checkTplInj } } while(!loaded); - if(options.initSequence && firstRun){ - ps(`Start initial sequence`); - let seqline = 1; - for(let seq of options.initSequence){ - switch(seq[0]){ - case "sleep": - ps(`Sleep for ${seq[1]} seconds`); - await sleep(seq[1] * 1000); - break; - case "write": - ps(`Filling input ${seq[1]} with "${seq[2]}"`); - try{ - await crawler.page().type(seq[1], seq[2]); - } catch(e){ - sequenceError("element not found", seqline); - } - break; - case "click": - ps(`Click ${seq[1]}`); - try{ - await crawler.page().click(seq[1]); - } catch(e){ - sequenceError("element not found", seqline); - } - await crawler.waitForRequestsCompletion(); - break; - case "clickToNavigate": - ps(`Click to navigate ${seq[1]} ${seq[2]}`); - try{ - await crawler.clickToNavigate(seq[1], seq[2]); - } catch(err){ - sequenceError(err, seqline); - } - break; - case "navigate": - ps(`Navigate ${seq[1]}`); - try{ - await crawler.navigate(seq[1]); - } catch(err){ - sequenceError(err, seqline); - } - break; - default: - sequenceError("action not found", seqline); + if(SEQUENCE_EXECUTOR){ + try{ + await SEQUENCE_EXECUTOR.run(crawler, "runtime"); + }catch(e){ + if(DATABASE){ + DATABASE.updateStatus(`${e}`, true); } - seqline++; + if(VERBOSE) utils.printError(`Runtime sequence error: ${e}`); + return null; } - ps(`Initial sequence finished`); } return crawler; } + + + async function scanDom(crawler, options){ let timeo = setTimeout(function(){ crawler.stop(); }, options.maxExecTime); - await crawler.start(); + let target = null; + if(TARGET_ELEMENT){ + ps(`Scanning ${TARGET_ELEMENT}`); + target = await crawler.page().$(TARGET_ELEMENT); + } + await crawler.start(target); clearTimeout(timeo); } async function close(crawler){ - await sleep(200); + await utils.sleep(200); try{ if(USE_SINGLE_BROWSER){ await crawler.page().close(); @@ -280,7 +304,6 @@ async function scanStored(url, options){ crawler.on("fillinput", () => true); await scanDom(crawler, options); await triggerOnpaste(crawler); - await scanAttributes(crawler); await close(crawler); ps("Stored XSS scan finshed"); } @@ -301,13 +324,6 @@ async function crawlDOM(crawler, options){ } } -function sleep(n){ - return new Promise(resolve => { - setTimeout(resolve, n); - }); -}; - - async function retryScan(retries, fnc){ while(true) try{ await fnc(); @@ -339,13 +355,6 @@ async function runDOMScan(payloads, targetUrl, isTplInj, options){ if(crawler == null)return; await scanDom(crawler, options); await triggerOnpaste(crawler); - await scanAttributes(crawler); - - // Last chance, let's try to change the hash - // await crawler.page().evaluate(p => document.location.hash = p, getNewPayload(payload, "hash")); - // await triggerOnpaste(crawler); - // await scanAttributes(crawler); - await close(crawler); if(options.scanStored){ @@ -374,7 +383,6 @@ async function runFuzzer(payloads, targetUrl, isTplInj, options){ await scanDom(crawler, options); } await triggerOnpaste(crawler); - await scanAttributes(crawler); await close(crawler); if(options.scanStored){ @@ -390,7 +398,7 @@ async function runFuzzer(payloads, targetUrl, isTplInj, options){ (async () => { var targetUrl, cnt, crawler; - const argv = require('minimist')(process.argv.slice(2), {boolean:["l", "J", "q", "T", "D", "r", "B", "S"]}); + const argv = require('minimist')(process.argv.slice(2), {boolean:["l", "J", "q", "T", "D", "r", "B", "S", "O"]}); if(argv.q)VERBOSE = false; if(VERBOSE)utils.banner(); if('h' in argv){ @@ -407,7 +415,11 @@ async function runFuzzer(payloads, targetUrl, isTplInj, options){ } catch(e){ utils.error(e); } - const options = utils.parseArgs(argv, targetUrl); + const {options, settings} = utils.parseArgs(argv, targetUrl); + if(argv.m){ + settings.push(["-m", argv.m]) + } + settings.push([null, targetUrl.href]); options.crawlmode = "random"; if(options.databaseFileName){ if(fs.existsSync(options.databaseFileName)){ @@ -416,6 +428,7 @@ async function runFuzzer(payloads, targetUrl, isTplInj, options){ } DATABASE = new Database(options.databaseFileName); DATABASE.init(); + DATABASE.addScanArguments(settings); } if(!options.maxExecTime) options.maxExecTime = consts.DEF_MAXEXECTIME; const allModes = [consts.MODE_DOMSCAN, consts.MODE_FUZZ]; @@ -444,6 +457,47 @@ async function runFuzzer(payloads, targetUrl, isTplInj, options){ process.on('SIGTERM', sigHandler); process.on('SIGINT', sigHandler); + if(options.sequenceBuilder){ + if(fs.existsSync(options.sequenceBuilder)){ + utils.printError(`${options.sequenceBuilder} already exists`); + process.exit(1); + } + ps("Running Sequence Builder, use Domdig's DevTools panel ..."); + const builder = new SequenceBuilder(targetUrl.href, options); + const builderResult = await builder.run(); + if(builderResult.discart){ + process.exit(0); + } + fs.writeFileSync(options.sequenceBuilder, JSON.stringify(builderResult.sequence)); + ps(`Sequence saved to ${options.sequenceBuilder}`); + if(builderResult.next == "scan"){ + options.initSequence = builderResult.sequence; + if(builderResult.targetUrl){ + targetUrl.href = builderResult.targetUrl; + } + } else { + process.exit(0); + } + } + + if(options.initSequence){ + try{ + SEQUENCE_EXECUTOR = new SequenceExecutor(options.initSequence, status => ps(status)); + if(SEQUENCE_EXECUTOR.sequence.start.length > 0){ + const seqCrawler = await loadHtcrawl(targetUrl.href, options); + await seqCrawler.load(); + await SEQUENCE_EXECUTOR.run(seqCrawler, "start"); + await seqCrawler.page().close(); + } + }catch(e){ + if(DATABASE){ + DATABASE.updateStatus(`${e}`, true); + } + console.error(chalk.red(`${e}`)); + process.exit(2); + } + } + ps(`Starting scan\n modes: ${modes.join(",")} scan stored: ${options.scanStored ? "yes" : "no"} check template injection: ${options.checkTemplateInj ? "yes" : "no"}`); if(options.dryRun){ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8d031a1 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,875 @@ +{ + "name": "domdig", + "version": "1.1.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "domdig", + "version": "1.1.0", + "license": "GPL-3.0", + "dependencies": { + "better-sqlite3": "^8.0.1", + "chalk": "^2.4.2", + "htcrawl": "^1.2.0", + "minimist": "^1.2.0" + } + }, + "../../htcrawl": { + "version": "1.2.0", + "license": "GPL-3.0", + "dependencies": { + "puppeteer": "^21.6.0", + "yargs": "^17.7.2" + }, + "bin": { + "htcrawl": "ui/cli/main.js" + }, + "devDependencies": { + "puppeteer": "^21.6.0" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/better-sqlite3": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-8.7.0.tgz", + "integrity": "sha512-99jZU4le+f3G6aIl6PmmV0cxUIWqKieHxsiF7G34CVFiE+/UabpYqkU0NJIkY/96mQKikHeBjtR27vFfs5JpEw==", + "hasInstallScript": true, + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "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" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/htcrawl": { + "resolved": "../../htcrawl", + "link": true + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + }, + "node_modules/node-abi": { + "version": "3.51.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.51.0.tgz", + "integrity": "sha512-SQkEP4hmNWjlniS5zdnfIXTk1x7Ome85RDzHlTbBtzE97Gfwz/Ipw4v/Ryk20DWIy3yCNVLVlGKApCnmvYoJbA==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "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" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "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" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "better-sqlite3": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-8.7.0.tgz", + "integrity": "sha512-99jZU4le+f3G6aIl6PmmV0cxUIWqKieHxsiF7G34CVFiE+/UabpYqkU0NJIkY/96mQKikHeBjtR27vFfs5JpEw==", + "requires": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "requires": { + "mimic-response": "^3.1.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, + "detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==" + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" + }, + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" + }, + "htcrawl": { + "version": "file:../../htcrawl", + "requires": { + "puppeteer": "^21.6.0", + "yargs": "^17.7.2" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + }, + "node-abi": { + "version": "3.51.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.51.0.tgz", + "integrity": "sha512-SQkEP4hmNWjlniS5zdnfIXTk1x7Ome85RDzHlTbBtzE97Gfwz/Ipw4v/Ryk20DWIy3yCNVLVlGKApCnmvYoJbA==", + "requires": { + "semver": "^7.3.5" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "prebuild-install": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", + "requires": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" + }, + "simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "requires": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/package.json b/package.json index ed911f3..db02dad 100755 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "domdig", - "version": "1.0.0", + "version": "1.1.0", "description": "DOM XSS scanner for Single Page Applications", "main": "domdig.js", "dependencies": { - "minimist": "^1.2.0", - "chalk": "^2.4.2", "better-sqlite3": "^8.0.1", - "htcrawl": "^1.0.7" + "chalk": "^2.4.2", + "htcrawl": "^1.2.0", + "minimist": "^1.2.0" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" diff --git a/payloads.js b/payloads.js index 7bbc275..7f11de9 100755 --- a/payloads.js +++ b/payloads.js @@ -53,6 +53,8 @@ exports.xss = [ "\\x3c/scrIpt\\x3e\\x3cscript\\x3ewindow.___xssSink({0})\\x3c/scrIpt\\x3e", "\\74/scrIpt\\76\\74script\\76window.___xssSink({0})\\74/scrIpt\\76", + "${window.___xssSink({0})}", + ]; // template injection. it will be rendered as [object Object]123456[object Object] diff --git a/sequence-builder.js b/sequence-builder.js new file mode 100644 index 0000000..109b080 --- /dev/null +++ b/sequence-builder.js @@ -0,0 +1,127 @@ +const fs = require('fs'); +const chalk = require('chalk'); +const consts = require("./consts"); +const htcrawl = require('htcrawl'); +const utils = require('./utils'); + + +exports.SequenceBuilder = class { + constructor(url, options){ + this.url = url; + this.options = options; + this.crawler = null; + this.targetUrl = null; + this._finished = false; + } + + run(){ + return new Promise((resolve, reject) => { + this.launch(resolve); + }); + } + async launch(resolve){ + this.crawler = await htcrawl.launch(this.url, { + ...this.options, + headlessChrome: false, + customUI: { + extensionPath: __dirname + '/chrome-extension', + UIMethods: UI => { + UI.selectElement = async action => { + const e = await UI.utils.selectElement(); + switch(action){ + case "click": + case "clickToNavigate": + case "setTarget": + UI.dispatch('selectElement', {action: action, par1: e.selector}); + break; + case "write": + case "select": + let inp = e.element; + if(!inp.matches("input, select, textarea")){ + inp = inp.querySelector("input, select, textarea"); + } + if(!inp){ + UI.dispatch('selectElement', {error: "Cannot find an input element"}); + } + + const inpRect = inp.getBoundingClientRect(); + const okBtn = UI.utils.createElement("button", { + position: 'absolute', + left: (inpRect.left + document.documentElement.scrollLeft + inpRect.width) + "px", + top: (inpRect.top + document.documentElement.scrollTop) + "px", + backgroundColor: '#9c27b0', + color: 'white', + border: 'none', + padding: '10px 20px', + borderRadius: '5px', + cursor: 'pointer', + zIndex: 2147483640 + }); + okBtn.textContent = "Apply"; + inp.focus(); + okBtn.onclick = function() { + this.parentNode.removeChild(this); + UI.dispatch('selectElement', { + action: action, + par1: UI.utils.getElementSelector(inp), + par2: inp.value + }); + }; + break; + } + }; + UI.end = sequence => { + UI.dispatch("end", {sequence: sequence}); + }; + + UI.scan = sequence => { + UI.dispatch("scan", {sequence: sequence}); + }; + + UI.discart = () => { + UI.dispatch("discart"); + }; + }, + events: { + selectElement: async e => { + this.crawler.sendToUI(e.params); + if(e.params.action == "click"){ + await this.crawler.page().click(e.params.par1); + } + if(e.params.action == "clickToNavigate"){ + try{ + this.crawler.on("navigation", e => { + this.targetUrl = e.params.request.url; + }); + await this.crawler.clickToNavigate(e.params.par1); + }catch(e){ + this.crawler.sendToUI({error: "Navigation timeout"}); + return; + } + } + }, + end: async e => { + this.crawler.browser().close(); + resolve({ + sequence: e.params.sequence, + next: null + }); + }, + scan: async e => { + this.crawler.browser().close(); + resolve({ + sequence: e.params.sequence, + next: "scan", + targetUrl: this.targetUrl, + }); + }, + discart: async e => { + resolve({discart: true}); + }, + } + }, + }); + await this.crawler.load(); + this.crawler.sendToUI({action: "navigate", par1: this.url}); + } +}; \ No newline at end of file diff --git a/sequence-executor.js b/sequence-executor.js new file mode 100644 index 0000000..a5edd0d --- /dev/null +++ b/sequence-executor.js @@ -0,0 +1,90 @@ +const fs = require('fs'); +const chalk = require('chalk'); +const consts = require("./consts"); +const htcrawl = require('htcrawl'); +const utils = require('./utils'); + + +exports.SequenceExecutor = class { + seqline; + result = {}; + constructor(sequence, statusChange){ + this.sequence = Array.isArray(sequence) ? {start: sequence, runtime: []} : sequence; + this.statusChange = statusChange; + } + + async run(crawler, sequenceType) { + this.seqline = 1; + this.result.targetElement = null; + const sequence = this.sequence[sequenceType]; + if(sequence.length == 0){ + return + } + this.statusChange(`Running sequence: ${sequenceType}`); + for(let seq of sequence){ + switch(seq[0]){ + case "sleep": + this.statusChange(`Sleep for ${seq[1]} seconds`); + await utils.sleep(seq[1] * 1000); + break; + case "write": + this.statusChange(`Filling input ${seq[1]} with "${seq[2]}"`); + try{ + await crawler.page().type(seq[1], seq[2]); + } catch(e){ + this.error("element not found"); + } + break; + case "select": + this.statusChange(`Selecting input ${seq[1]} with "${seq[2]}"`); + try{ + await crawler.page().select(seq[1], seq[2]); + } catch(e){ + this.error("element not found"); + } + break; + case "click": + this.statusChange(`Click ${seq[1]}`); + try{ + await crawler.page().click(seq[1]); + } catch(e){ + this.error("element not found"); + } + await crawler.waitForRequestsCompletion(); + break; + case "clickToNavigate": + this.statusChange(`Click to navigate ${seq[1]} ${seq[2] || ''}`); + try{ + await crawler.clickToNavigate(seq[1], seq[2], seq[3]); + } catch(err){ + this.error(err); + } + break; + case "navigate": + this.statusChange(`Navigate ${seq[1]}`); + try{ + await crawler.navigate(seq[1]); + } catch(err){ + this.error(err); + } + break; + case "setTarget": + this.statusChange(`Set Target element ${seq[1]}`); + this.result.targetElement = seq[1]; + break; + default: + this.error("action not found"); + } + this.seqline++; + } + this.statusChange(`Sequence finished: ${sequenceType}`); + } + + error(message){ + if(this.seqline){ + message = "action " + this.seqline + ": " + message; + } + throw message; + } + +}; \ No newline at end of file diff --git a/test/gentestdata.js b/test/gentestdata.js new file mode 100644 index 0000000..4014041 --- /dev/null +++ b/test/gentestdata.js @@ -0,0 +1,20 @@ +const fs = require('fs'); +const { spawn } = require('child_process'); + +const main = async page => { + + + const db = require('better-sqlite3')(`${__dirname}/testdata.db`); + const scanSettings = { + command: db.prepare("SELECT parameter, value FROM scan_settings where parameter is null or (parameter != '-P' and parameter != '-d')").all().map(p => + [p.parameter, p.value].filter(v => !!v) + ).flat(), + payloads: db.prepare('SELECT * FROM vulnerability').all().map(v => + v.payload.replace(/alert\(1\)/g, 'window.___xssSink({0})') + ) + } + + fs.writeFileSync(`${__dirname}/testdata/${page}.json`, JSON.stringify(scanSettings)); +} + +main(process.argv[2]) \ No newline at end of file diff --git a/test/run.sh b/test/run.sh new file mode 100755 index 0000000..d00c794 --- /dev/null +++ b/test/run.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +WD=$( cd "$(dirname "$(readlink $0 || echo $0)")" ; pwd -P ) +command -v tmux >/dev/null || { echo "tmux is required"; exit 1; } +TESTS="$1" +tmux kill-session -t domdigltest > /dev/null 2>&1 +tput setaf 2 +echo "Tests started at "`date` +tput sgr0 +echo -ne "" > $WD/test-results.log +tmux new-session -d -s domdigltest "python3 -m http.server 9092 -b 127.0.0.1 -d $WD/testpages" +sleep 1 + + +TEST_CMD="\ +node $WD/unit.js $TESTS > $WD/test-results.log 2>&1 ;\ +tmux kill-session -t domdigltest \ +" +tmux split-window -t domdigltest "$TEST_CMD" + +tmux a -t domdigltest > /dev/null 2>&1 +tput setaf 1 +cat $WD/test-results.log +tput setaf 2 +echo "Tests finished at "`date` +tput sgr0 +rm $WD/test-results.log +exit 0 \ No newline at end of file diff --git a/test/testdata/attributes.json b/test/testdata/attributes.json new file mode 100644 index 0000000..b07bf44 --- /dev/null +++ b/test/testdata/attributes.json @@ -0,0 +1 @@ +{"command":["-l","-T","-S","http://127.0.0.1:9092/attributes.html"],"payloads":["javascript:window.___xssSink({0})","java%0ascript:window.___xssSink({0})"]} \ No newline at end of file diff --git a/test/testdata/deep.json b/test/testdata/deep.json new file mode 100644 index 0000000..2a3afb2 --- /dev/null +++ b/test/testdata/deep.json @@ -0,0 +1 @@ +{"command":["-l","-T","-S","http://127.0.0.1:9092/deep.html"],"payloads":["","'>","\">","1 -->","]]>"]} \ No newline at end of file diff --git a/test/testdata/fetch.json b/test/testdata/fetch.json new file mode 100644 index 0000000..d02fb40 --- /dev/null +++ b/test/testdata/fetch.json @@ -0,0 +1 @@ +{"command":["-S","-T","-m","domscan","-l","http://127.0.0.1:9092/fetch.html"],"payloads":["","'>","\">","1 -->","]]>"]} \ No newline at end of file diff --git a/test/testdata/hidden.json b/test/testdata/hidden.json new file mode 100644 index 0000000..1b2b931 --- /dev/null +++ b/test/testdata/hidden.json @@ -0,0 +1 @@ +{"command":["-l","-T","-S","http://127.0.0.1:9092/hidden.html"],"payloads":["","'>","\">","1 -->","]]>"]} \ No newline at end of file diff --git a/test/testdata/href.json b/test/testdata/href.json new file mode 100644 index 0000000..e88bbd4 --- /dev/null +++ b/test/testdata/href.json @@ -0,0 +1 @@ +{"command":["-l","-T","-S","http://127.0.0.1:9092/href.html"],"payloads":["javascript:window.___xssSink({0})","java%0ascript:window.___xssSink({0})"]} \ No newline at end of file diff --git a/test/testdata/postmessage.json b/test/testdata/postmessage.json new file mode 100644 index 0000000..ab5b29f --- /dev/null +++ b/test/testdata/postmessage.json @@ -0,0 +1 @@ +{"command":["-l","-T","-S","-m","fuzz","http://127.0.0.1:9092/postmessage.html"],"payloads":["","'>","\">","1 -->","]]>"]} \ No newline at end of file diff --git a/test/testdata/xhr.json b/test/testdata/xhr.json new file mode 100644 index 0000000..3867f9b --- /dev/null +++ b/test/testdata/xhr.json @@ -0,0 +1 @@ +{"command":["-l","-T","-S","http://127.0.0.1:9092/xhr.html"],"payloads":["","'>","\">","1 -->","]]>"]} \ No newline at end of file diff --git a/test/testpages/attributes.html b/test/testpages/attributes.html new file mode 100644 index 0000000..0913bb0 --- /dev/null +++ b/test/testpages/attributes.html @@ -0,0 +1,21 @@ + + + + + + +
+ + + \ No newline at end of file diff --git a/test/testpages/common.js b/test/testpages/common.js new file mode 100644 index 0000000..1790a8c --- /dev/null +++ b/test/testpages/common.js @@ -0,0 +1,7 @@ +function getHash(){ + return decodeURIComponent(document.location.hash.substr(1)); +} + +function setHash(h){ + document.location.href = document.location.href.split("#")[0] + "#" +h; +} \ No newline at end of file diff --git a/test/testpages/data.json b/test/testpages/data.json new file mode 100644 index 0000000..6758433 --- /dev/null +++ b/test/testpages/data.json @@ -0,0 +1 @@ +{"test": "testdata"} \ No newline at end of file diff --git a/test/testpages/deep.html b/test/testpages/deep.html new file mode 100644 index 0000000..7fd2656 --- /dev/null +++ b/test/testpages/deep.html @@ -0,0 +1,59 @@ + + + + + + +
+
+ + + + + \ No newline at end of file diff --git a/test/testpages/fetch.html b/test/testpages/fetch.html new file mode 100644 index 0000000..216744b --- /dev/null +++ b/test/testpages/fetch.html @@ -0,0 +1,49 @@ + + + + + + +
+
+
+ + + + + \ No newline at end of file diff --git a/test/testpages/hidden.html b/test/testpages/hidden.html new file mode 100644 index 0000000..39432c1 --- /dev/null +++ b/test/testpages/hidden.html @@ -0,0 +1,40 @@ + + + + + + +
+
+ + + + + \ No newline at end of file diff --git a/test/testpages/href.html b/test/testpages/href.html new file mode 100644 index 0000000..33add49 --- /dev/null +++ b/test/testpages/href.html @@ -0,0 +1,16 @@ + + + + + + +
+ + + \ No newline at end of file diff --git a/test/testpages/postmessage-frame.html b/test/testpages/postmessage-frame.html new file mode 100644 index 0000000..a5e420b --- /dev/null +++ b/test/testpages/postmessage-frame.html @@ -0,0 +1,17 @@ + + + + + + +
+ + + \ No newline at end of file diff --git a/test/testpages/postmessage.html b/test/testpages/postmessage.html new file mode 100644 index 0000000..22be35e --- /dev/null +++ b/test/testpages/postmessage.html @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/test/testpages/simple.html b/test/testpages/simple.html new file mode 100644 index 0000000..1afd275 --- /dev/null +++ b/test/testpages/simple.html @@ -0,0 +1,13 @@ + + + + + + +
+ + + \ No newline at end of file diff --git a/test/testpages/tplstring.html b/test/testpages/tplstring.html new file mode 100644 index 0000000..8d6d0cc --- /dev/null +++ b/test/testpages/tplstring.html @@ -0,0 +1,13 @@ + + + + + + +
+ + + \ No newline at end of file diff --git a/test/testpages/xhr.html b/test/testpages/xhr.html new file mode 100644 index 0000000..ed75fff --- /dev/null +++ b/test/testpages/xhr.html @@ -0,0 +1,66 @@ + + + + + + +
+
+
+ + + + + \ No newline at end of file diff --git a/test/unit.js b/test/unit.js new file mode 100644 index 0000000..f89211c --- /dev/null +++ b/test/unit.js @@ -0,0 +1,60 @@ +const fs = require('fs'); + +const { spawn } = require('child_process'); + +function arrayCmp(arr1, arr2) { + if (arr1.length !== arr2.length) return false; + return arr1.slice().sort().every((value, index) => + value === arr2.slice().sort()[index] + ); +} + + +function runDomdig(cmd){ + return new Promise((resolve, reject) => { + const child = spawn('node', ['../domdig.js', ...cmd]); + child.stdout.on('data', (data) => { + // console.log(`${data}`); + }); + + child.stderr.on('data', (data) => { + reject(`Errore standard: ${data}`); + }); + + child.on('close', (code) => { + resolve(code); + }); + }) +}; + +tests = ["attributes","fetch","href","xhr","deep","hidden","postmessage"]; + +(async () => { + for(const test of tests){ + if(process.argv[2] && process.argv[2] != test){ + continue; + } + const testData = require(`${__dirname}/testdata/${test}.json`); + const dbFile = `${__dirname}/testout.db`; + const payloadsFile = `${__dirname}/payloads.json`; + try{ + fs.unlinkSync(dbFile); + }catch(e){} + fs.writeFileSync(payloadsFile, JSON.stringify(testData.payloads)) + const exitcode = await runDomdig([...testData.command, '-d', dbFile, '-P', payloadsFile]); + + + const db = require('better-sqlite3')(dbFile); + const payloads = db.prepare('SELECT * FROM vulnerability').all().map(v => + v.payload.replace(/alert\(1\)/g, 'window.___xssSink({0})') + ) + if(!arrayCmp(testData.payloads, payloads)){ + console.log(`Test failed: ${test}`) + console.log("Expected", testData.payloads); + console.log("Got ", payloads); + process.exit(1); + } + fs.unlinkSync(dbFile); + fs.unlinkSync(payloadsFile); + } +})(); \ No newline at end of file diff --git a/utils.js b/utils.js index 45e8ae9..75d951c 100755 --- a/utils.js +++ b/utils.js @@ -18,9 +18,9 @@ exports.writeJSON = writeJSON; exports.prettifyJson = prettifyJson; exports.loadPayloadsFromFile = loadPayloadsFromFile; exports.error = error; -exports.getElementSelector = getElementSelector; exports.Vulnerability = Vulnerability; exports.replaceSinkName = replaceSinkName; +exports.sleep = sleep; function Vulnerability(type, payload, element, url, message, confirmed){ @@ -144,13 +144,14 @@ function error(message){ } function banner(){ + const { version } = require(`${__dirname}/package.json`); console.log(chalk.yellow([ " ___ ____ __ ______ _", " / _ \\/ __ \\/ |/ / _ \\(_)__ _", " / // / /_/ / /|_/ / // / / _ `/", "/____/\\____/_/ /_/____/_/\\_, /" ].join("\n"))); - console.log(chalk.green(" ver 1.0.0 ") + chalk.yellow("/___/")); + console.log(chalk.green(` ver ${version} `) + chalk.yellow("/___/")); console.log(chalk.yellow("DOM XSS scanner for Single Page Applications")); console.log(chalk.blue("https://github.com/fcavallarin/domdig")); console.log(""); @@ -192,6 +193,8 @@ function usage(){ " -r print all XHR/fetch and websocket requests triggered while scanning", " -D dry-run, do not use any payload, just crawl the page", " -B restart the browser every new payload", + " -L FILE_NAME run the Sequence Builder and save the sequnce to file", + " -O do not crawl non same-origin frames", " -h this help" ].join("\n")); } @@ -233,7 +236,14 @@ function parseCookiesString(str, domain){ } -function parseArgs(args, url){ +function parseArgs(args, url, database){ + const save = opt => { + if(!database){ + return; + } + database.addScanArgument(opt); + } + const settings = []; const options = {}; for(let arg in args){ switch(arg){ @@ -243,19 +253,24 @@ function parseArgs(args, url){ } catch(e){ options.setCookies = parseCookiesString(args[arg], url.hostname); } + settings.push([`-${arg}`, JSON.stringify(options.setCookies)]); break; case "A": var arr = args[arg].split(":"); options.httpAuth = [arr[0], arr.slice(1).join(":")]; + settings.push([`-${arg}`, args[arg]]); break; case "x": options.maxExecTime = parseInt(args[arg]) * 1000; + settings.push([`-${arg}`, args[arg]]); break; case "U": options.userAgent = args[arg]; + settings.push([`-${arg}`, args[arg]]); break; case "R": options.referer = args[arg]; + settings.push([`-${arg}`, args[arg]]); break; case "p": var tmp = args[arg].split(":"); @@ -264,9 +279,16 @@ function parseArgs(args, url){ } else { options.proxy = args[arg]; } + settings.push([`-${arg}`, args[arg]]); break; case "l": options.headlessChrome = !args[arg]; + if(!options.headlessChrome){ + options.openChromeDevtools = true; + } + if(args[arg]){ + settings.push([`-${arg}`, null]); + } break; case "E": try { @@ -279,6 +301,7 @@ function parseArgs(args, url){ options.extraHeaders[t[0]] = t.slice(1).join("="); } } + settings.push([`-${arg}`, JSON.stringify(options.extraHeaders)]); break; case "g": try { @@ -292,7 +315,6 @@ function parseArgs(args, url){ options.localStorage.push({type: t[0] == "S" ? "S" : "L", key: t[1], val: ls[s]}); } } - } catch(e){ let ls = typeof args[arg] == 'string' ? [args[arg]] : args[arg]; options.localStorage = []; @@ -309,6 +331,7 @@ function parseArgs(args, url){ options.localStorage.push({type:type, key:key, val:val}); } } + settings.push([`-${arg}`, args[arg]]); break; case "s": try{ @@ -321,32 +344,64 @@ function parseArgs(args, url){ process.exit(1); } } + settings.push([`-${arg}`, JSON.stringify(options.initSequence)]); break; case "X": options.excludedUrls = typeof args[arg] == 'string' ? [args[arg]] : args[arg]; + settings.push([`-${arg}`, args[arg]]); break; case "d": options.databaseFileName = args[arg]; + settings.push([`-${arg}`, args[arg]]); break; case "r": options.printRequests = args[arg]; + if(args[arg]){ + settings.push([`-${arg}`, null]); + } break; case "D": options.dryRun = args[arg]; + if(args[arg]){ + settings.push([`-${arg}`, null]); + } break; case "B": options.singleBrowser = !args[arg]; + if(args[arg]){ + settings.push([`-${arg}`, null]); + } break; case "S": options.scanStored = !args[arg]; + if(args[arg]){ + settings.push([`-${arg}`, null]); + } break; case "T": options.checkTemplateInj = !args[arg]; + if(args[arg]){ + settings.push([`-${arg}`, null]); + } + break; + case "L": + options.sequenceBuilder = args[arg]; + if(args[arg]){ + settings.push([`-${arg}`, null]); + } + break; + case "O": + options.includeAllOrigins = !args[arg]; + if(args[arg]){ + settings.push([`-${arg}`, null]); + } break; - } } - return options; + return { + options: options, + settings: settings, + } } function genFilename(fname){ @@ -408,35 +463,8 @@ function prettifyJson(obj, layer){ return obj; } - -async function getElementSelector(element){ - return await element.evaluate( i => { - function gs(element){ - if(!element || !(element instanceof HTMLElement)) - return ""; - var name = element.nodeName.toLowerCase(); - var ret = []; - var selector = "" - var id = element.getAttribute("id"); - - if(id && id.match(/^[a-z][a-z0-9\-_:\.]*$/i)){ - selector = "#" + id; - } else { - let p = element; - let cnt = 1; - while(p = p.previousSibling){ - if(p instanceof HTMLElement && p.nodeName.toLowerCase() == name){ - cnt++; - } - } - selector = name + (cnt > 1 ? `:nth-of-type(${cnt})` : ""); - if(element != document.documentElement && name != "body" && element.parentNode){ - ret.push(gs(element.parentNode)); - } - } - ret.push(selector); - return ret.join(" > "); - } - return gs(i); +function sleep(n){ + return new Promise(resolve => { + setTimeout(resolve, n); }); -} \ No newline at end of file +};