diff --git a/_examples/README.md b/_examples/README.md index 0cba3c00..c48d6c6d 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -16,6 +16,7 @@ * [Xinted Service challenge with custom xinetd config](./xinetd-service) * [Service challenge with auto-generated xinetd config](./service) * [A bare challenge using docker](./docker-type) +* [Docker compose challenge](./compose-type) To test any of the above challenges, cd to \_example directory and use the below command: diff --git a/_examples/compose-type/beast.toml b/_examples/compose-type/beast.toml new file mode 100644 index 00000000..25104c84 --- /dev/null +++ b/_examples/compose-type/beast.toml @@ -0,0 +1,23 @@ +[author] +name = "contact" +email = "contact@sdslabs.co.in" +ssh_key = "ssh-rsa AAAAB3NzaC1y" + +[challenge.metadata] +name = "compose-type" +flag = "winterhack{w45_th15_y0ur_f1r5t_4dm1n_b07_ch4ll}" +type = "web" +points = 200 + +[[challenge.metadata.hints]] +text = "simple_hint_1" +points = 10 + +[[challenge.metadata.hints]] +text = "simple_hint_2" +points = 20 + +[challenge.env] +docker_compose = "docker-compose.yml" +web_root = "web" +ports = [10005] diff --git a/_examples/compose-type/bot/.dockerignore b/_examples/compose-type/bot/.dockerignore new file mode 100644 index 00000000..c81b8d31 --- /dev/null +++ b/_examples/compose-type/bot/.dockerignore @@ -0,0 +1,3 @@ +node_modules +npm-debug.log +.env diff --git a/_examples/compose-type/bot/.gitignore b/_examples/compose-type/bot/.gitignore new file mode 100644 index 00000000..40b878db --- /dev/null +++ b/_examples/compose-type/bot/.gitignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/_examples/compose-type/bot/Dockerfile b/_examples/compose-type/bot/Dockerfile new file mode 100644 index 00000000..756b2a55 --- /dev/null +++ b/_examples/compose-type/bot/Dockerfile @@ -0,0 +1,28 @@ +FROM node:16-alpine3.16 + +ENV LANG="C.UTF-8" \ + PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true + +RUN apk update && apk add --no-cache \ + zlib-dev \ + udev \ + nss \ + ca-certificates \ + chromium && \ + adduser -h /bot -D -u 10086 bot + +WORKDIR /bot + +COPY package*.json ./ + +RUN npm ci --only=production && \ + npm cache clean --force + +COPY . . + +RUN chown -R bot:bot /bot + + +EXPOSE 9000 + +CMD ["node", "index.js"] diff --git a/_examples/compose-type/bot/bot.js b/_examples/compose-type/bot/bot.js new file mode 100644 index 00000000..4371d760 --- /dev/null +++ b/_examples/compose-type/bot/bot.js @@ -0,0 +1,71 @@ +const puppeteer = require('puppeteer'); +const BASE_URL = process.env.BASE_URL || 'http://0.0.0.0'; +const FLAG= process.env.FLAG || 'test{flag}'; +const sleep = ms => new Promise(res => setTimeout(res, ms)); + +const CONFIG = { + APPNAME: process.env['APPNAME'] || "Admin", + APPURL: process.env['BASE_URL'] || "http://127.0.0.1:8000", + APPURLREGEX: process.env['APPURLREGEX'] || "^.*$", + APPFLAG: process.env['FLAG'] || "winterhack{w45_th15_y0ur_f1r5t_4dm1n_b07_ch4ll}", + APPLIMITTIME: Number(process.env['APPLIMITTIME'] || "60"), + APPLIMIT: Number(process.env['APPLIMIT'] || "5"), +} + +console.table(CONFIG) + +const initBrowser = puppeteer.launch({ + executablePath: "/usr/bin/chromium-browser", + headless: 'new', + args: [ + '--disable-dev-shm-usage', + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-gpu', + '--no-gpu', + '--disable-default-apps', + '--disable-translate', + '--disable-device-discovery-notifications', + '--disable-software-rasterizer', + '--disable-xss-auditor' + ], + ipDataDir: '/home/bot/data/', + ignoreHTTPSErrors: true +}); + +console.log("Bot started..."); + +module.exports = { + name: CONFIG.APPNAME, + urlRegex: CONFIG.APPURLREGEX, + rateLimit: { + windowS: CONFIG.APPLIMITTIME, + max: CONFIG.APPLIMIT + }, + bot: async (urlToVisit) => { + const browser = await initBrowser; + const context = await browser.createIncognitoBrowserContext() + try { + const page = await context.newPage(); + url = BASE_URL ; + await page.setCookie({ name: 'flag', value: FLAG, domain: new URL(url).hostname, httpOnly: false, secure: false }); + + console.log(`bot visiting ${urlToVisit}`) + await page.goto(urlToVisit, { + timeout: 3000, + waitUntil: 'domcontentloaded' + }); + + console.log("bot visited") + await sleep(20000) + + console.log("browser close...") + await context.close() + return true; + } catch (e) { + console.error(e); + await context.close(); + return false; + } + } +} \ No newline at end of file diff --git a/_examples/compose-type/bot/index.js b/_examples/compose-type/bot/index.js new file mode 100644 index 00000000..7814acfc --- /dev/null +++ b/_examples/compose-type/bot/index.js @@ -0,0 +1,48 @@ +const express = require("express") +const app = express(); +const path = require("path") +const route = express.Router() +const bot = require("./bot") +const rateLimit = require("express-rate-limit") + +app.use(express.urlencoded({ extended: false })) +app.set('view engine', 'ejs'); +app.set('views', path.join(__dirname, 'views')); +app.set('trust proxy', () => true) + +const limit = rateLimit({ + ...bot, + handler: ((req, res, _next) => { + const timeRemaining = Math.ceil((req.rateLimit.resetTime - Date.now()) / 1000) + res.status(429).json({ + error: `Too many requests, please try again later after ${timeRemaining} seconds.`, + }); + }) +}) + + +route.post("/", limit, async (req, res) => { + var { url } = req.body; + if (!url) { + return res.status(400).send({ error: "Url is missing." }); + } + if (!RegExp(bot.urlRegex).test(url)) { + return res.status(422).send({ error: "URL din't match this regex format " + bot.urlRegex }) + } + if (await bot.bot(url)) { + return res.send({ success: "Admin successfully visited the URL." }); + } else { + return res.status(500).send({ error: "Admin failed to visit the URL." }); + } +}); + +route.get("/", (_, res) => { + const { name } = bot + res.render("index", { name }); +}); + +app.use("/", route) + +app.listen(8000, () => { + console.log("Server running at http://localhost:8000"); +}); \ No newline at end of file diff --git a/_examples/compose-type/bot/package-lock.json b/_examples/compose-type/bot/package-lock.json new file mode 100644 index 00000000..bb6f8c04 --- /dev/null +++ b/_examples/compose-type/bot/package-lock.json @@ -0,0 +1,1793 @@ +{ + "name": "bot", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "bot", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "body-parser": "^1.20.2", + "bot": "file:", + "ejs": "^3.1.9", + "express": "^4.21.2", + "express-rate-limit": "^6.7.0", + "puppeteer": "^19.10.1" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-0.5.0.tgz", + "integrity": "sha512-Uw6oB7VvmPRLE4iKsjuOh8zgDabhNX67dzo8U/BB0f9527qx+4eeUs+korU98OhG5C4ubg7ufBgVi63XYwS6TQ==", + "dependencies": { + "debug": "4.3.4", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.1", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "yargs": "17.7.1" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=14.1.0" + }, + "peerDependencies": { + "typescript": ">= 4.7.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@puppeteer/browsers/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@puppeteer/browsers/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/@types/node": { + "version": "22.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "optional": true, + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "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/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/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bot": { + "version": "1.0.0", + "resolved": "file:", + "license": "MIT", + "dependencies": { + "body-parser": "^1.20.2", + "bot": "file:", + "ejs": "^3.1.9", + "express": "^4.18.2", + "express-rate-limit": "^6.7.0", + "puppeteer": "^19.10.1" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "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/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "engines": { + "node": "*" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "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/chromium-bidi": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.7.tgz", + "integrity": "sha512-6+mJuFXwTMU6I3vYLs6IL8A1DyQTPjCfIL971X0aMPVGRbGnNfl6i6Cl0NMbxi2bRYLGESt9T2ZIMRM5PAEcIQ==", + "dependencies": { + "mitt": "3.0.0" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/cosmiconfig": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.1.3.tgz", + "integrity": "sha512-/UkO2JKI18b5jVMJUp0lvKFMpa/Gye+ZgZjKD+DGEN9y7NRcf/nK1A0sp67ONmKtnDCNMS44E6jrk0Yc3bDuUw==", + "dependencies": { + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "dependencies": { + "node-fetch": "2.6.7" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1107588", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1107588.tgz", + "integrity": "sha512-yIR+pG9x65Xko7bErCUSQaDLrO/P1p3JUzEk7JCU4DowPcGHkTGUGQapcfcLc4qj0UaALwZ+cr0riFgiqpixcg==" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.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/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.11.2.tgz", + "integrity": "sha512-a7uwwfNTh1U60ssiIkuLFWHt4hAC5yxlLGU2VP0X4YNlyEDZAqF4tK3GD3NSitVBrCQmQ0++0uOyFOgC2y4DDw==", + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "express": "^4 || ^5" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/extract-zip/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz", + "integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "dunder-proto": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "function-bind": "^1.1.2", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mitt": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.0.tgz", + "integrity": "sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==" + }, + "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/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/object-inspect": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "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" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/puppeteer": { + "version": "19.11.1", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-19.11.1.tgz", + "integrity": "sha512-39olGaX2djYUdhaQQHDZ0T0GwEp+5f9UB9HmEP0qHfdQHIq0xGQZuAZ5TLnJIc/88SrPLpEflPC+xUqOTv3c5g==", + "deprecated": "< 22.8.2 is no longer supported", + "hasInstallScript": true, + "dependencies": { + "@puppeteer/browsers": "0.5.0", + "cosmiconfig": "8.1.3", + "https-proxy-agent": "5.0.1", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "puppeteer-core": "19.11.1" + } + }, + "node_modules/puppeteer-core": { + "version": "19.11.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-19.11.1.tgz", + "integrity": "sha512-qcuC2Uf0Fwdj9wNtaTZ2OvYRraXpAK+puwwVW8ofOhOgLPZyz1c68tsorfIZyCUOpyBisjr+xByu7BMbEYMepA==", + "dependencies": { + "@puppeteer/browsers": "0.5.0", + "chromium-bidi": "0.4.7", + "cross-fetch": "3.1.5", + "debug": "4.3.4", + "devtools-protocol": "0.0.1107588", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.1", + "proxy-from-env": "1.1.0", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "ws": "8.13.0" + }, + "engines": { + "node": ">=14.14.0" + }, + "peerDependencies": { + "typescript": ">= 4.7.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/puppeteer-core/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/puppeteer-core/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "engines": { + "node": ">=4" + } + }, + "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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "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/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "optional": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "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/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "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/ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", + "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } +} diff --git a/_examples/compose-type/bot/package.json b/_examples/compose-type/bot/package.json new file mode 100644 index 00000000..4c042cf5 --- /dev/null +++ b/_examples/compose-type/bot/package.json @@ -0,0 +1,15 @@ +{ + "name": "bot", + "version": "1.0.0", + "main": "index.js", + "author": "dimas maulana", + "license": "MIT", + "dependencies": { + "body-parser": "^1.20.2", + "bot": "file:", + "ejs": "^3.1.9", + "express": "^4.21.2", + "express-rate-limit": "^6.7.0", + "puppeteer": "^19.10.1" + } +} diff --git a/_examples/compose-type/bot/views/index.ejs b/_examples/compose-type/bot/views/index.ejs new file mode 100644 index 00000000..51cc2168 --- /dev/null +++ b/_examples/compose-type/bot/views/index.ejs @@ -0,0 +1,29 @@ + + + + + + Bot Home + + + +
+

Welcome to the Bot Service

+
+ +
+

Hello, <%= name %>!

+ +

Request to visit a URL

+
+ + + +
+
+ + + + diff --git a/_examples/compose-type/docker-compose.yml b/_examples/compose-type/docker-compose.yml new file mode 100644 index 00000000..ef12ce6b --- /dev/null +++ b/_examples/compose-type/docker-compose.yml @@ -0,0 +1,33 @@ +version: "3" +services: + web: + build: ./web + ports: + - "80:80" + - "5432" + - "8000" + environment: + POSTGRES_HOST: "0.0.0.0" + POSTGRES_DB: notes_db + POSTGRES_USER: postgres + POSTGRES_PASSWORD: pa55word + SECRET_KEY: hard_to_guess + BASE_URL: http://0.0.0.0 + BOT_URL: http://0.0.0.0:8000 + db: + image: postgres:latest + environment: + POSTGRES_HOST: "0.0.0.0" + POSTGRES_DB: notes_db + POSTGRES_USER: postgres + POSTGRES_PASSWORD: pa55word + network_mode: service:web + bot: + build: ./bot + environment: + FLAG: winterhack{w45_th15_y0ur_f1r5t_4dm1n_b07_ch4ll} + BASE_URL: http://0.0.0.0 + network_mode: service:web + + + diff --git a/_examples/compose-type/web/Dockerfile b/_examples/compose-type/web/Dockerfile new file mode 100644 index 00000000..d54e0746 --- /dev/null +++ b/_examples/compose-type/web/Dockerfile @@ -0,0 +1,31 @@ +FROM python:3.8-slim + +WORKDIR /app + +RUN apt-get update && \ + apt-get install -y postgresql-client curl && \ + rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . + +RUN pip install -r requirements.txt + +COPY . . + +RUN mkdir -p /app/app/static/js && \ + chmod -R 755 /app/app/static + +RUN chmod +x entrypoint.sh + +# Add a non-root user and switch to this user + +# Change ownership of the app directory to the non-root user +RUN useradd -m hacker \ + && chown -R root:root /app \ + && chmod -R 755 /app \ + && chmod -R o-w /app + +USER hacker + +EXPOSE 80 +ENTRYPOINT ["./entrypoint.sh"] \ No newline at end of file diff --git a/_examples/compose-type/web/app/__init__.py b/_examples/compose-type/web/app/__init__.py new file mode 100644 index 00000000..3b609992 --- /dev/null +++ b/_examples/compose-type/web/app/__init__.py @@ -0,0 +1,47 @@ +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from flask_login import LoginManager +from flask_wtf.csrf import CSRFProtect +import os +from apscheduler.schedulers.background import BackgroundScheduler + +db = SQLAlchemy() +login_manager = LoginManager() +csrf = CSRFProtect() +scheduler = BackgroundScheduler() + +def clear_database(): + from app.models import User, Note + with scheduler.app.app_context(): + db.session.query(Note).delete() + db.session.query(User).delete() + db.session.commit() + +def create_app(): + app = Flask(__name__) + + app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'default_secret_key') + app.config['SQLALCHEMY_DATABASE_URI'] = f"postgresql://{os.environ.get('POSTGRES_USER')}:{os.environ.get('POSTGRES_PASSWORD')}@{os.environ.get('POSTGRES_HOST')}:{os.environ.get('POSTGRES_PORT', 5432)}/{os.environ.get('POSTGRES_DB')}" + + db.init_app(app) + login_manager.init_app(app) + csrf.init_app(app) + + from app.models import User, Note + + @login_manager.user_loader + def load_user(user_id): + return User.query.get(int(user_id)) + + from app.views import main as main_blueprint + app.register_blueprint(main_blueprint) + + with app.app_context(): + db.create_all() + + scheduler.add_job(func=clear_database, trigger="interval", minutes=60) + scheduler.start() + + scheduler.app = app + + return app diff --git a/_examples/compose-type/web/app/forms.py b/_examples/compose-type/web/app/forms.py new file mode 100644 index 00000000..579f104a --- /dev/null +++ b/_examples/compose-type/web/app/forms.py @@ -0,0 +1,32 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, PasswordField, TextAreaField, SubmitField +from wtforms.validators import DataRequired, Length, URL + + +class LoginForm(FlaskForm): + username = StringField('Username',validators=[DataRequired()]) + password = PasswordField('Password', validators=[DataRequired()]) + submit = SubmitField('Login') + + +class RegisterForm(FlaskForm): + username = StringField('Username') + password = PasswordField('Password', validators=[DataRequired()]) + submit = SubmitField('Register') + + +class NoteForm(FlaskForm): + content = TextAreaField('Content', validators=[DataRequired()]) + submit = SubmitField('Save') + + +class ContactForm(FlaskForm): + name = StringField('Name', validators=[DataRequired()]) + email = StringField('Email', validators=[DataRequired()]) + message = TextAreaField('Message', validators=[DataRequired()]) + submit = SubmitField('Send') + + +class ReportForm(FlaskForm): + note_url = StringField('Note URL', validators=[DataRequired()]) + submit = SubmitField('Report Note') diff --git a/_examples/compose-type/web/app/main.py b/_examples/compose-type/web/app/main.py new file mode 100644 index 00000000..fe05d461 --- /dev/null +++ b/_examples/compose-type/web/app/main.py @@ -0,0 +1,6 @@ +from app import create_app + +app = create_app() + +if __name__ == "__main__": + app.run() diff --git a/_examples/compose-type/web/app/models.py b/_examples/compose-type/web/app/models.py new file mode 100644 index 00000000..f57d7dab --- /dev/null +++ b/_examples/compose-type/web/app/models.py @@ -0,0 +1,20 @@ +from app import db +from flask_login import UserMixin +import hashlib +import time + +class User(db.Model, UserMixin): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(150), unique=True, nullable=False) + password = db.Column(db.String(150), nullable=False) + +class Note(db.Model): + id = db.Column(db.String(32), primary_key=True, default=lambda: generate_md5_id()) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + content = db.Column(db.Text, nullable=False) + user = db.relationship('User', backref=db.backref('notes', lazy=True)) + +def generate_md5_id(): + # Use current time (in seconds) to generate a unique MD5 hash. + current_time = str(time.time()).encode('utf-8') + return hashlib.md5(current_time).hexdigest() # Generates a 32-character MD5 hash diff --git a/_examples/compose-type/web/app/static/css/forms.css b/_examples/compose-type/web/app/static/css/forms.css new file mode 100644 index 00000000..d3c3ce89 --- /dev/null +++ b/_examples/compose-type/web/app/static/css/forms.css @@ -0,0 +1,73 @@ +form { + background: #fff; + padding: 25px; + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + margin: 20px auto; + max-width: 500px; + animation: fadeIn 0.7s ease-in-out; + position: relative; + transition: box-shadow 0.3s ease; +} + +form:focus-within { + box-shadow: 0 0 20px rgba(51, 51, 51, 0.2); +} + +form .form-group { + margin-bottom: 20px; + text-align: left; +} + +form .form-group label { + display: block; + margin-bottom: 8px; + font-weight: 600; + color: #444; +} + +form .form-group input, +form .form-group textarea { + width: 100%; + padding: 12px; + border: 1px solid #ccc; + border-radius: 6px; + box-sizing: border-box; + transition: border-color 0.3s ease, box-shadow 0.3s ease; +} + +form .form-group input:focus, +form .form-group textarea:focus { + border-color: #1e88e5; + box-shadow: 0 0 10px rgba(30, 136, 229, 0.2); + outline: none; +} + +form .form-group button { + width: 100%; + padding: 12px; + background: #007bff; + color: #fff; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 16px; + font-weight: 600; + transition: background 0.3s ease, transform 0.2s ease; +} + +form .form-group button:hover { + background: #0056b3; + transform: scale(1.03); +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(15px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/_examples/compose-type/web/app/static/css/general.css b/_examples/compose-type/web/app/static/css/general.css new file mode 100644 index 00000000..1ff2fbeb --- /dev/null +++ b/_examples/compose-type/web/app/static/css/general.css @@ -0,0 +1,107 @@ +body { + font-family: "Roboto", sans-serif; + margin: 0; + padding: 0; + background: linear-gradient(135deg, #f4f4f4, #e3f2fd); + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + text-align: center; +} + +.create-container { + background: #ffffff; + padding: 40px; + border-radius: 15px; + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1); + width: 100%; + max-width: 600px; + animation: fadeIn 1s ease-in-out; +} + +h1 { + font-size: 2.5rem; + margin-bottom: 15px; + color: #1e88e5; + text-transform: uppercase; + letter-spacing: 1px; +} + +p { + font-size: 1rem; + color: #666; + margin-bottom: 25px; +} + +.form-group { + margin-bottom: 20px; +} + +.form-control { + width: 100%; + padding: 12px; + border: 1px solid #ddd; + border-radius: 8px; + transition: box-shadow 0.3s ease; + font-size: 1rem; +} + +.form-control:focus { + box-shadow: 0 0 10px rgba(30, 136, 229, 0.3); + border-color: #1e88e5; + outline: none; +} + +button.btn-primary { + background-color: #1e88e5; + color: #fff; + padding: 12px 20px; + border: none; + border-radius: 8px; + font-size: 1.2rem; + cursor: pointer; + transition: background-color 0.3s ease, transform 0.2s ease; +} + +button.btn-primary:hover { + background-color: #1565c0; + transform: translateY(-2px); +} + +button.btn-primary:active { + transform: translateY(1px); +} + +.note-id-section { + margin-top: 30px; + padding: 20px; + background: #e3f2fd; + border-radius: 10px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); +} + +.note-link a { + background-color: #1e88e5; + color: #fff; + padding: 10px 15px; + border-radius: 8px; + text-decoration: none; + transition: background-color 0.3s ease, transform 0.2s ease; +} + +.note-link a:hover { + background-color: #1565c0; + transform: translateY(-2px); +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/_examples/compose-type/web/app/static/css/home.css b/_examples/compose-type/web/app/static/css/home.css new file mode 100644 index 00000000..8b0ef58b --- /dev/null +++ b/_examples/compose-type/web/app/static/css/home.css @@ -0,0 +1,68 @@ +form { + background: #fff; + padding: 20px; + border-radius: 10px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + margin: 20px auto; + max-width: 500px; + animation: fadeIn 0.5s ease-in-out; +} + +form:focus-within { + box-shadow: 0 0 15px rgba(51, 51, 51, 0.2); +} + +form .form-group { + margin-bottom: 15px; + text-align: left; +} + +form .form-group label { + display: block; + margin-bottom: 5px; + font-weight: 600; + color: #333; +} + +form .form-group input { + width: 100%; + padding: 10px; + border: 1px solid #ccc; + border-radius: 5px; + box-sizing: border-box; + transition: border-color 0.3s ease; +} + +form .form-group input:focus { + border-color: #333; + box-shadow: 0 0 8px rgba(51, 51, 51, 0.2); + outline: none; +} + +form .form-group button { + width: 100%; + padding: 10px; + background: #007bff; + color: #fff; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 16px; + font-weight: 600; + transition: background 0.3s ease; +} + +form .form-group button:hover { + background: #0056b3; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/_examples/compose-type/web/app/templates/401.html b/_examples/compose-type/web/app/templates/401.html new file mode 100644 index 00000000..2f73314a --- /dev/null +++ b/_examples/compose-type/web/app/templates/401.html @@ -0,0 +1,57 @@ + +{% extends "base.html" %} + +{% block content %} +
+

Unauthorized Access

+

+ You do not have permission to view this page. Please log in to continue. +

+
+ Go Back to Login +
+
+ + +{% endblock %} diff --git a/_examples/compose-type/web/app/templates/500.html b/_examples/compose-type/web/app/templates/500.html new file mode 100644 index 00000000..c5a06c3f --- /dev/null +++ b/_examples/compose-type/web/app/templates/500.html @@ -0,0 +1,56 @@ +{% extends "base.html" %} + +{% block content %} +
+

500 - Internal Server Error

+

+ Something went wrong on our end. Please try again later or contact support if the issue persists. +

+
+ Go Back to Home +
+
+ + +{% endblock %} diff --git a/_examples/compose-type/web/app/templates/base.html b/_examples/compose-type/web/app/templates/base.html new file mode 100644 index 00000000..bc2dcec8 --- /dev/null +++ b/_examples/compose-type/web/app/templates/base.html @@ -0,0 +1,26 @@ + + + + + + + Cyberpunk notes + + + + + + + + +
+
{% block content %}{% endblock %}
+
+ + + diff --git a/_examples/compose-type/web/app/templates/create.html b/_examples/compose-type/web/app/templates/create.html new file mode 100644 index 00000000..c0a77261 --- /dev/null +++ b/_examples/compose-type/web/app/templates/create.html @@ -0,0 +1,156 @@ +{% extends "base.html" %} + +{% block content %} +
+

Create Note

+

Create and share the notes

+
+ {{ form.hidden_tag() }} +
+ {{ form.content.label(class="cyberpunk-label") }} + {{ form.content(class="form-control non-resizable cyberpunk-textarea", rows=10, cols=50, id="note-content") }} +
+
+ +
+
+ +
+ + + + +{% endblock %} diff --git a/_examples/compose-type/web/app/templates/home.html b/_examples/compose-type/web/app/templates/home.html new file mode 100644 index 00000000..6ae7c271 --- /dev/null +++ b/_examples/compose-type/web/app/templates/home.html @@ -0,0 +1,81 @@ +{% extends "base.html" %} +{% block content %} +
+

Welcome to Cyberpunk Notes ⚠️

+

+ Choose +

+
+ + + +
+
+ +{% endblock %} diff --git a/_examples/compose-type/web/app/templates/index.html b/_examples/compose-type/web/app/templates/index.html new file mode 100644 index 00000000..e2cdaf9c --- /dev/null +++ b/_examples/compose-type/web/app/templates/index.html @@ -0,0 +1 @@ +{% extends "base.html" %} {% block content %} {% endblock %} \ No newline at end of file diff --git a/_examples/compose-type/web/app/templates/login.html b/_examples/compose-type/web/app/templates/login.html new file mode 100644 index 00000000..aebd3922 --- /dev/null +++ b/_examples/compose-type/web/app/templates/login.html @@ -0,0 +1,134 @@ +{% extends "base.html" %} +{% block content %} +
+

Login

+ + +
+ {{ form.hidden_tag() }} + +
+ + {{ form.username(class="form-control cyberpunk-input") }} +
+ +
+ + {{ form.password(class="form-control cyberpunk-input") }} +
+ +
+ +
+ + {% if error_login %} +
+

{{ error_login }}

+
+ {% endif %} +
+ + +

+ Need an account? Register here +

+
+ + +{% endblock %} diff --git a/_examples/compose-type/web/app/templates/register.html b/_examples/compose-type/web/app/templates/register.html new file mode 100644 index 00000000..9f573d92 --- /dev/null +++ b/_examples/compose-type/web/app/templates/register.html @@ -0,0 +1,138 @@ +{% extends "base.html" %} +{% block content %} +
+

Register

+ + +
+ {{ form.hidden_tag() }} + + +
+ + {{ form.username(class="form-control cyberpunk-input") }} +
+ + +
+ + {{ form.password(class="form-control cyberpunk-input") }} +
+ + +
+ +
+ + + {% if error_register %} +
+

{{ error_register }}

+
+ {% endif %} +
+ + +

+ Already have an account? Login here +

+
+ + +{% endblock %} diff --git a/_examples/compose-type/web/app/templates/report.html b/_examples/compose-type/web/app/templates/report.html new file mode 100644 index 00000000..929da776 --- /dev/null +++ b/_examples/compose-type/web/app/templates/report.html @@ -0,0 +1,111 @@ +{% extends "base.html" %} +{% block content %} +
+
+

Report a Note

+

+ Found a note that violates our terms of service? Report it here. +

+
+
+ {{ form.hidden_tag() }} +
+ + {{ form.note_url(class="form-control cyberpunk-input", id="note-url-input") }} +
+
+ +
+
+

+ {{ messages }} +

+
+ + Back to Home + +
+
+ + +{% endblock %} diff --git a/_examples/compose-type/web/app/templates/view.html b/_examples/compose-type/web/app/templates/view.html new file mode 100644 index 00000000..2d16cfa3 --- /dev/null +++ b/_examples/compose-type/web/app/templates/view.html @@ -0,0 +1,171 @@ +{% extends "base.html" %} +{% block content %} +
+

View Note

+

+ You can view stored notes securely here by entering the Note ID. +

+ +
+
+ + +
+
+ +
+
+ + + + + +
+ + Back to Home + +
+ +
+ + + + +{% endblock %} diff --git a/_examples/compose-type/web/app/views.py b/_examples/compose-type/web/app/views.py new file mode 100644 index 00000000..9eee3fec --- /dev/null +++ b/_examples/compose-type/web/app/views.py @@ -0,0 +1,206 @@ +import os +from flask import Blueprint, render_template, redirect, url_for, request, jsonify,make_response,current_app +from flask_login import login_user, login_required, logout_user, current_user +from urllib.parse import urlparse, urljoin +from app import db +from app.models import User, Note +from app.forms import LoginForm, RegisterForm, NoteForm, ContactForm, ReportForm +import hashlib +import time +import bleach +import datetime +import requests +import subprocess +import threading +from markupsafe import escape +from html import escape as html_escape + + +main = Blueprint('main', __name__) + +BASE_URL = os.getenv('BASE_URL', 'http://0.0.0.0') +BOT_URL = os.getenv('BOT_URL', 'http://0.0.0.0:8000') + +reporting_users = set() +reporting_lock = threading.Lock() + +def custom_escape(text): + if text is None: + return '' + return text.replace('$', '$').replace('{', '{').replace('}', '}').replace('[', '[').replace(']', ']') + +@main.errorhandler(500) +def internal_server_error(error): + current_app.logger.error(f"Server Error: {error}, Path: {request.path}") + + return render_template('500.html'), 500 + + +@main.errorhandler(401) +def unauthorized_error(error): + return render_template('401.html'), 401 + +def is_secure_link(target): + test_url = urlparse(urljoin(request.host_url, target)) + if target=="/": + return False + if test_url.scheme in ('http', 'https'): + return True + return False + +@main.route('/') +def index(): + if current_user.is_authenticated: + return redirect(url_for('main.home')) + else: + return redirect(url_for('main.login')) + +@main.route('/home') +def home(): + return render_template('home.html') + +@main.route('/api/notes/fetch/', methods=['GET']) +def fetch(note_id): + if len(note_id) != 32 or not all(c in '0123456789abcdef' for c in note_id): + return jsonify({'error': 'Invalid note ID format'}), 400 + + note = Note.query.get(note_id) + if note: + return jsonify({'content': note.content, 'note_id': note.id}) + return jsonify({'error': 'Note not found'}), 404 + +@main.route('/api/notes/store', methods=['POST']) +@login_required +def store(): + data = request.get_json() + content = data.get('content') + + if not content: + return jsonify(success=False, error="Note content cannot be empty"), 400 + + sanitized_content = bleach.clean(content) + + new_note = Note(user_id=current_user.id, content=sanitized_content) + db.session.add(new_note) + db.session.commit() + + return jsonify({'success': 'Note stored', 'note_id': new_note.id}) + +@main.route('/register', methods=['GET', 'POST']) +def register(): + form = RegisterForm() + error_register = '' + if form.validate_on_submit(): + user = User.query.filter_by(username=form.username.data).first() + if user: + error_register='Username already exists. Please choose a different one.' + else: + user = User(username=form.username.data, password=form.password.data) + db.session.add(user) + db.session.commit() + login_user(user) + return redirect(url_for('main.home')) + elif request.method == 'POST': + error_register='Registration Unsuccessful. Please check the errors and try again.' + return render_template('register.html', form=form) + +@main.route('/login', methods=['GET', 'POST']) +def login(): + error_login = '' + form = LoginForm() + if form.validate_on_submit(): + user = User.query.filter_by(username=form.username.data).first() + if user and user.password == form.password.data: + login_user(user) + return redirect(url_for('main.home')) + else: + error_login='Login Unsuccessful. Please check username and password' + return render_template('login.html', form=form,error_login=error_login) + +@main.route('/create', methods=['GET', 'POST']) +@login_required +def create_note(): + form = NoteForm() + if form.validate_on_submit(): + note = Note(user_id=current_user.id, content=form.content.data) + db.session.merge(note) + db.session.commit() + return redirect(url_for('main.view_note', note=note.id)) + return render_template('create.html', form=form) + +@main.route('/view', methods=['GET']) +def view_note(): + note_id = request.args.get('note') or '' + name_param = request.args.get('name') + safe_name = custom_escape(name_param) + random_hash = hashlib.md5(str(time.time()).encode()).hexdigest() + name = request.args.get('name') if safe_name else (current_user.username if current_user.is_authenticated else "Guest") + again_name_safe=custom_escape(name) + return render_template('view.html', note_id=note_id, username=again_name_safe,random_hash=random_hash) + +@main.route('/iframe_content', methods=['GET']) +def iframe_content(): + return render_template('iframe_content.html') + + +@main.route('/bay', methods=['GET', 'POST']) +def bay(): + return_url = request.args.get('return', '/') + form = ContactForm() + if request.method == 'POST': + if form.validate_on_submit(): + if is_secure_link(return_url): + return redirect(return_url) + return redirect(url_for('main.home')) + else: + if is_secure_link(return_url): + return redirect(return_url) + return render_template('bay.html', form=form, return_url=return_url) + +@main.route('/logout') +@login_required +def logout(): + logout_user() + return redirect(url_for('main.home')) + +@main.route('/report', methods=['GET', 'POST']) +@login_required +def report(): + form = ReportForm() + log='' + if form.validate_on_submit(): + note_url = form.note_url.data + parsed_url = urlparse(note_url) + base_url_parsed = urlparse(BASE_URL) + if not parsed_url.scheme.startswith('http'): + log='URL must begin with http(s)://' + + elif parsed_url.netloc == base_url_parsed.netloc and parsed_url.path == '/view' and 'note=' in parsed_url.query: + note_id = parsed_url.query.split('=')[-1] + + try: + if len(note_id) == 32 and all(c in '0123456789abcdef' for c in note_id): + with reporting_lock: + if current_user.id in reporting_users: + log='You already have a report in progress. Please respect our moderation capabilities.' + else: + reporting_users.add(current_user.id) + threading.Thread(target=bot, args=( + note_url, current_user.id)).start() + log='Note reported successfully' + except ValueError: + pass + else: + log=f'Please provide a valid note URL eg. http://{base_url_parsed.netloc}/view?note=7c1ce3ded0e8b97db1bdd2fabba701b4' + + return render_template('report.html', form=form,messages=log) +def bot(note_url, user_id): + try: + response = requests.post(f"{BOT_URL}", data={"url": note_url}) + if response.status_code == 200: + pass + else: + pass + finally: + with reporting_lock: + reporting_users.remove(user_id) \ No newline at end of file diff --git a/_examples/compose-type/web/entrypoint.sh b/_examples/compose-type/web/entrypoint.sh new file mode 100644 index 00000000..0a91eca8 --- /dev/null +++ b/_examples/compose-type/web/entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +until pg_isready -h 0.0.0.0 -p 5432 -U postgres; do + echo "Waiting for database..." + sleep 1 +done + +gunicorn --chdir /app --bind 0.0.0.0:80 app.main:app --access-logfile - --error-logfile - diff --git a/_examples/compose-type/web/requirements.txt b/_examples/compose-type/web/requirements.txt new file mode 100644 index 00000000..56598146 --- /dev/null +++ b/_examples/compose-type/web/requirements.txt @@ -0,0 +1,11 @@ +Flask[async] +Flask-WTF +Flask-SQLAlchemy +Flask-Login +Flask-CORS +gunicorn +psycopg2-binary +bleach +markupsafe +requests +APScheduler diff --git a/core/config/challenge.go b/core/config/challenge.go index 1fcfe113..5c5e392f 100644 --- a/core/config/challenge.go +++ b/core/config/challenge.go @@ -268,6 +268,7 @@ type ChallengeEnv struct { Entrypoint string `toml:"entrypoint"` DockerCtx string `toml:"docker_context"` XinetdConf string `toml:"xinetd_conf"` + DockerCompose string `toml:"docker_compose"` EnvironmentVars []EnvironmentVar `toml:"var"` Traffic string `toml:"traffic"` } diff --git a/core/database/challenges.go b/core/database/challenges.go index 3d867673..d413084d 100644 --- a/core/database/challenges.go +++ b/core/database/challenges.go @@ -99,7 +99,7 @@ func CreateChallengeEntry(challenge *Challenge) error { tx := Db.Begin() if tx.Error != nil { - return fmt.Errorf("Error while starting transaction", tx.Error) + return fmt.Errorf("error while starting transaction: %s", tx.Error) } if err := tx.FirstOrCreate(challenge, *challenge).Error; err != nil { @@ -277,7 +277,7 @@ func BatchUpdateChallenge(whereMap map[string]interface{}, chall Challenge) erro tx := Db.Where(whereMap).First(&challenge) if errors.Is(tx.Error, gorm.ErrRecordNotFound) { - return fmt.Errorf("No challenge entry to update : WhereClause : %s", whereMap) + return fmt.Errorf("no challenge entry to update : WhereClause : %s", whereMap) } if tx.Error != nil { @@ -328,7 +328,7 @@ func DeleteChallengeEntry(challenge *Challenge) error { tx := Db.Begin() if tx.Error != nil { - return fmt.Errorf("Error while starting transaction : %s", tx.Error) + return fmt.Errorf("error while starting transaction : %s", tx.Error) } if err := tx.Unscoped().Delete(challenge).Error; err != nil { @@ -445,7 +445,7 @@ func updateScript(user *User) error { scriptPath := filepath.Join(core.BEAST_GLOBAL_DIR, core.BEAST_SCRIPTS_DIR, fmt.Sprintf("%x", SHA256.Sum(nil))) challs, err := GetRelatedChallenges(user) if err != nil { - return fmt.Errorf("Error while getting related challenges : %v", err) + return fmt.Errorf("error while getting related challenges : %s", err) } mapOfChall := make(map[string]string) @@ -462,12 +462,12 @@ func updateScript(user *User) error { var script bytes.Buffer scriptTemplate, err := template.New("script").Parse(tools.SSH_LOGIN_SCRIPT_TEMPLATE) if err != nil { - return fmt.Errorf("Error while parsing script template :: %s", err) + return fmt.Errorf("error while parsing script template :: %s", err) } err = scriptTemplate.Execute(&script, data) if err != nil { - return fmt.Errorf("Error while executing script template :: %s", err) + return fmt.Errorf("error while executing script template :: %s", err) } return ioutil.WriteFile(scriptPath, script.Bytes(), 0755) @@ -509,54 +509,52 @@ func QueryDynamicFlagEntries(whereMap map[string]interface{}) ([]DynamicFlag, er } func SubtractScoreFromSolvers(challengeID uint) error { - DBMux.Lock() - defer DBMux.Unlock() - - tx := Db.Begin() - if tx.Error != nil { - return fmt.Errorf("error while starting transaction: %v", tx.Error) - } - - var challenge Challenge - if err := tx.Where("id = ?", challengeID).First(&challenge).Error; err != nil { - tx.Rollback() - return fmt.Errorf("error fetching challenge: %v", err) - } - - pointsToSubtract := challenge.Points - - var solvers []UserChallenges - if err := tx.Where("challenge_id = ? AND solved = ?", challengeID, true).Find(&solvers).Error; err != nil { - tx.Rollback() - return fmt.Errorf("error fetching solvers: %v", err) - } - - for _, solver := range solvers { - if err := tx.Model(&User{}).Where("id = ?", solver.UserID). - UpdateColumn("score", gorm.Expr("CASE WHEN score - ? < 0 THEN 0 ELSE score - ? END", pointsToSubtract, pointsToSubtract)).Error; err != nil { - tx.Rollback() - return fmt.Errorf("error updating score for user %d: %v", solver.UserID, err) - } - } - - return tx.Commit().Error -} + DBMux.Lock() + defer DBMux.Unlock() -func DeleteAllUserChallenges(challengeID uint) error { - DBMux.Lock() - defer DBMux.Unlock() + tx := Db.Begin() + if tx.Error != nil { + return fmt.Errorf("error while starting transaction: %v", tx.Error) + } - tx := Db.Begin() - if tx.Error != nil { - return fmt.Errorf("error while starting transaction: %v", tx.Error) - } + var challenge Challenge + if err := tx.Where("id = ?", challengeID).First(&challenge).Error; err != nil { + tx.Rollback() + return fmt.Errorf("error fetching challenge: %v", err) + } + + pointsToSubtract := challenge.Points + + var solvers []UserChallenges + if err := tx.Where("challenge_id = ? AND solved = ?", challengeID, true).Find(&solvers).Error; err != nil { + tx.Rollback() + return fmt.Errorf("error fetching solvers: %v", err) + } - if err := tx.Where("challenge_id = ?", challengeID).Delete(&UserChallenges{}).Error; err != nil { - tx.Rollback() - return fmt.Errorf("error deleting user challenge entries: %v", err) - } + for _, solver := range solvers { + if err := tx.Model(&User{}).Where("id = ?", solver.UserID). + UpdateColumn("score", gorm.Expr("CASE WHEN score - ? < 0 THEN 0 ELSE score - ? END", pointsToSubtract, pointsToSubtract)).Error; err != nil { + tx.Rollback() + return fmt.Errorf("error updating score for user %d: %v", solver.UserID, err) + } + } - return tx.Commit().Error + return tx.Commit().Error } +func DeleteAllUserChallenges(challengeID uint) error { + DBMux.Lock() + defer DBMux.Unlock() + tx := Db.Begin() + if tx.Error != nil { + return fmt.Errorf("error while starting transaction: %v", tx.Error) + } + + if err := tx.Where("challenge_id = ?", challengeID).Delete(&UserChallenges{}).Error; err != nil { + tx.Rollback() + return fmt.Errorf("error deleting user challenge entries: %v", err) + } + + return tx.Commit().Error +} diff --git a/core/manager/challenge.go b/core/manager/challenge.go index 5a2a4fa7..943137d0 100644 --- a/core/manager/challenge.go +++ b/core/manager/challenge.go @@ -632,27 +632,53 @@ func undeployChallenge(challengeName string, purge bool) error { return fmt.Errorf("ChallengeName %s not valid", challengeName) } - // If a existing container ID is not found make sure that you atleast - // set the deploy status to undeployed. This earlier caused problem since if a challenge - // was in staging state(and deployed is cancled) then we can neither deploy new - // version nor we can undeploy the existing version(since it does not exist) - // So this.... - if challenge.ContainerId == coreUtils.GetTempContainerId(challengeName) { - log.Warnf("No instance of challenge(%s) deployed", challengeName) - } else { - log.Debug("Removing challenge instance for ", challengeName) + // ContainerId is empty => docker compose + if challenge.ContainerId == "" { + log.Debugf("Detected Docker Compose for challenge %s", challengeName) + + stagedDir := filepath.Join(core.BEAST_GLOBAL_DIR, core.BEAST_STAGING_DIR, challengeName) if challenge.ServerDeployed != core.LOCALHOST && challenge.ServerDeployed != "" { server := config.Cfg.AvailableServers[challenge.ServerDeployed] - err = remoteManager.StopAndRemoveContainerRemote(challenge.ContainerId, server) + + if !purge { + err = remoteManager.ComposeDownRemote(challengeName, stagedDir, server) + } else { + err = remoteManager.ComposePurgeRemote(challengeName, stagedDir, server) + } } else { - err = cr.StopAndRemoveContainer(challenge.ContainerId) + if !purge { + err = cr.ComposeDown(challengeName, stagedDir) + } else { + err = cr.ComposePurge(challengeName, stagedDir) + } } if err != nil { - // This should not return from here, this should assume that - // the container instance does not exist and hence should update the database - // with the container ID. - p := fmt.Errorf("error while removing challenge instance : %s", err) - log.Error(p.Error()) + log.Errorf("Error while removing challenge instance : %s", err) + return fmt.Errorf("error while removing challenge instance : %s", err) + } + } else { + // If a existing container ID is not found make sure that you atleast + // set the deploy status to undeployed. This earlier caused problem since if a challenge + // was in staging state(and deployed is cancled) then we can neither deploy new + // version nor we can undeploy the existing version(since it does not exist) + // So this.... + if challenge.ContainerId == coreUtils.GetTempContainerId(challengeName) { + log.Warnf("No instance of challenge(%s) deployed", challengeName) + } else { + log.Debug("Removing challenge instance for ", challengeName) + if challenge.ServerDeployed != core.LOCALHOST && challenge.ServerDeployed != "" { + server := config.Cfg.AvailableServers[challenge.ServerDeployed] + err = remoteManager.StopAndRemoveContainerRemote(challenge.ContainerId, server) + } else { + err = cr.StopAndRemoveContainer(challenge.ContainerId) + } + if err != nil { + // This should not return from here, this should assume that + // the container instance does not exist and hence should update the database + // with the container ID. + p := fmt.Errorf("error while removing challenge instance : %s", err) + log.Error(p.Error()) + } } } diff --git a/core/manager/pipeline.go b/core/manager/pipeline.go index 94ffc661..e8323538 100644 --- a/core/manager/pipeline.go +++ b/core/manager/pipeline.go @@ -41,37 +41,48 @@ func stageChallenge(challengeDir string, config *cfg.BeastChallengeConfig) error challengeConfig := filepath.Join(contextDir, core.CHALLENGE_CONFIG_FILE_NAME) log.Debugf("Reading challenge config from : %s", challengeConfig) - var dockerfileCtx, serviceConfig string + var dockerfileCtx, serviceConfig, dockerCompose string dockerfileProvided := false - if config.Challenge.Env.DockerCtx != "" { - dockerfileProvided = true - dockerfileCtx = filepath.Join(challengeDir, config.Challenge.Env.DockerCtx) - err := utils.ValidateFileExists(dockerfileCtx) + additionalCtx := make(map[string]string) + if config.Challenge.Env.DockerCompose != "" { + dockerCompose = filepath.Join(challengeDir, config.Challenge.Env.DockerCompose) + err := utils.ValidateFileExists(dockerCompose) if err != nil { return err } + // additionalCtx["docker-compose.yml"] = dockerCompose + log.Debug("Got docker-compose file from the challenge config") } else { - config.Challenge.Env.DockerCtx = core.DEFAULT_DOCKER_FILE - dockerfileCtx, err = GenerateChallengeDockerfileCtx(config) - if err != nil { - return err - } - log.Debug("Got dockerfile context from the challenge config") - } - if config.Challenge.Metadata.Type == core.SERVICE_CHALLENGE_TYPE_NAME { - if config.Challenge.Env.XinetdConf != "" { - serviceConfig = filepath.Join(challengeDir, config.Challenge.Env.XinetdConf) - err := utils.ValidateFileExists(serviceConfig) + if config.Challenge.Env.DockerCtx != "" { + dockerfileProvided = true + dockerfileCtx = filepath.Join(challengeDir, config.Challenge.Env.DockerCtx) + err := utils.ValidateFileExists(dockerfileCtx) if err != nil { return err } + } else { + config.Challenge.Env.DockerCtx = core.DEFAULT_DOCKER_FILE + dockerfileCtx, err = GenerateChallengeDockerfileCtx(config) + if err != nil { + return err + } + log.Debug("Got dockerfile context from the challenge config") + } + + if config.Challenge.Metadata.Type == core.SERVICE_CHALLENGE_TYPE_NAME { + if config.Challenge.Env.XinetdConf != "" { + serviceConfig = filepath.Join(challengeDir, config.Challenge.Env.XinetdConf) + err := utils.ValidateFileExists(serviceConfig) + if err != nil { + return err + } + additionalCtx[core.DEFAULT_XINETD_CONF_FILE] = serviceConfig + } } } - additionalCtx := make(map[string]string) additionalCtx["Dockerfile"] = dockerfileCtx - additionalCtx[core.DEFAULT_XINETD_CONF_FILE] = serviceConfig // Here we try to add all the additional context that are required like xinetd.conf // instead of mounting these files inside the container, since we want reproducibility @@ -138,27 +149,61 @@ func commitChallenge(challenge *database.Challenge, config cfg.BeastChallengeCon log.Errorf("Error while cleaning up the challenge") return err } - var imageId string - var buildErr error - var logBytes []byte + var ( + imageId string + buildErr error + logBytes []byte + ) + imageId = "" + challengeTag := coreUtils.EncodeID(challengeName) log.Printf("== Server for challenge %s : %s", challengeName, challenge.ServerDeployed) - if challenge.ServerDeployed != core.LOCALHOST && challenge.ServerDeployed != "" { - server := cfg.Cfg.AvailableServers[challenge.ServerDeployed] - stagedRemoteChallengePath := filepath.Join(core.BEAST_REMOTE_GLOBAL_DIR, core.BEAST_STAGING_DIR, challengeName) - remoteStagedPath := filepath.Join(stagedRemoteChallengePath, fmt.Sprintf("%s.tar.gz", challengeName)) - err := remoteManager.ValidateFileRemoteExists(server, stagedRemoteChallengePath) - if err != nil { - return fmt.Errorf("error while checking if the challenge is staged on the remote server") + if config.Challenge.Env.DockerCompose != "" { + + // Should add some validation for the compose file + + if challenge.ServerDeployed != core.LOCALHOST && challenge.ServerDeployed != "" { + + server := cfg.Cfg.AvailableServers[challenge.ServerDeployed] + logBytes, buildErr = remoteManager.BuildImagesFromComposeRemote( + challengeName, + challengeTag, + stagedPath, + server, + noCache, + ) + } else { + var buff *bytes.Buffer + + buff, buildErr = cr.BuildImagesFromCompose(challengeName, challengeTag, stagedPath, config.Challenge.Env.DockerCompose, noCache) + if buff != nil { + logBytes = buff.Bytes() + } else { + logBytes = []byte("BuildImagesFromCompose returned nil buffer") + } + // For Docker Compose challenges, ensure ImageId is empty in the database + if err := database.UpdateChallenge(challenge, map[string]interface{}{"ImageId": ""}); err != nil { + return fmt.Errorf("error while setting empty ImageId for Docker Compose challenge: %s", err) + } } - logBytes, imageId, buildErr = remoteManager.BuildImageFromTarContextRemote(challengeName, challengeTag, remoteStagedPath, server) } else { - var buff *bytes.Buffer - buff, imageId, buildErr = cr.BuildImageFromTarContext(challengeName, challengeTag, stagedPath, config.Challenge.Env.DockerCtx, noCache) - if buff != nil { - logBytes = buff.Bytes() + if challenge.ServerDeployed != core.LOCALHOST && challenge.ServerDeployed != "" { + server := cfg.Cfg.AvailableServers[challenge.ServerDeployed] + stagedRemoteChallengePath := filepath.Join(core.BEAST_REMOTE_GLOBAL_DIR, core.BEAST_STAGING_DIR, challengeName) + remoteStagedPath := filepath.Join(stagedRemoteChallengePath, fmt.Sprintf("%s.tar.gz", challengeName)) + err := remoteManager.ValidateFileRemoteExists(server, stagedRemoteChallengePath) + if err != nil { + return fmt.Errorf("error while checking if the challenge is staged on the remote server") + } + logBytes, imageId, buildErr = remoteManager.BuildImageFromTarContextRemote(challengeName, challengeTag, remoteStagedPath, server) } else { - logBytes = []byte("BuildImageFromTarContext returned nil buffer") + var buff *bytes.Buffer + buff, imageId, buildErr = cr.BuildImageFromTarContext(challengeName, challengeTag, stagedPath, config.Challenge.Env.DockerCtx, noCache) + if buff != nil { + logBytes = buff.Bytes() + } else { + logBytes = []byte("BuildImageFromTarContext returned nil buffer") + } } } // Create logs directory for the challenge in staging directory. @@ -183,13 +228,16 @@ func commitChallenge(challenge *database.Challenge, config cfg.BeastChallengeCon log.Error("Error while building image from the tar context of challenge") return buildErr } - if imageId == "" { - log.Error("Error while creating image logs written to the logfile") - return fmt.Errorf("error while getting imageId for the commited challenge") - } + // Only update imageId for non-Docker Compose challenges + if config.Challenge.Env.DockerCompose == "" { + if imageId == "" { + log.Error("Error while creating image logs written to the logfile") + return fmt.Errorf("error while getting imageId for the commited challenge") + } - if err = database.UpdateChallenge(challenge, map[string]interface{}{"ImageId": imageId}); err != nil { - return fmt.Errorf("error while writing imageId to database : %s", err) + if err = database.UpdateChallenge(challenge, map[string]interface{}{"ImageId": imageId}); err != nil { + return fmt.Errorf("error while writing imageId to database : %s", err) + } } log.Infof("Image build for `%s` done", challengeName) @@ -218,87 +266,120 @@ func commitChallenge(challenge *database.Challenge, config cfg.BeastChallengeCon func deployChallenge(challenge *database.Challenge, config cfg.BeastChallengeConfig) error { log.Debug("Starting to deploy the challenge") - staticMount := make(map[string]string) - var staticMountDir string - if challenge.ServerDeployed == core.LOCALHOST || challenge.ServerDeployed == "" { - staticMountDir = filepath.Join(core.BEAST_GLOBAL_DIR, core.BEAST_STAGING_DIR, config.Challenge.Metadata.Name, core.BEAST_STATIC_FOLDER) - } else { - staticMountDir = filepath.Join("$HOME/.beast", core.BEAST_STAGING_DIR, config.Challenge.Metadata.Name, core.BEAST_STATIC_FOLDER) - } - relativeStaticContentDir := config.Challenge.Env.StaticContentDir - if relativeStaticContentDir == "" { - relativeStaticContentDir = core.PUBLIC - } - staticMount[staticMountDir] = filepath.Join("/challenge", relativeStaticContentDir) - log.Debugf("Static mount config for deploy : %s", staticMount) + if config.Challenge.Env.DockerCompose != "" { + challengeName := config.Challenge.Metadata.Name + stagingDir := filepath.Join(core.BEAST_GLOBAL_DIR, core.BEAST_STAGING_DIR, challengeName) - var containerEnv []string - var containerNetwork string - if config.Challenge.Metadata.Sidecar != "" { - // We need to configure the sidecar for the challenge container. - // Push the environment variables to the container and link to the sidecar. - env := getSidecarEnv(&config) - containerEnv = append(containerEnv, env...) + if challenge.ServerDeployed != core.LOCALHOST && challenge.ServerDeployed != "" { + server := cfg.Cfg.AvailableServers[challenge.ServerDeployed] + // remote deployment + err := remoteManager.DeployContainerFromComposeRemote(challengeName, stagingDir, server) + if err != nil { + return fmt.Errorf("error while deploying challenge with docker-compose on remote: %v", err) + } - containerNetwork = getSidecarNetwork(config.Challenge.Metadata.Sidecar) - } + // For Docker Compose challenges on remote servers, ensure ContainerId is empty in the database + if err := database.UpdateChallenge(challenge, map[string]interface{}{"ContainerId": ""}); err != nil { + return fmt.Errorf("error while setting empty ContainerId for Docker Compose challenge: %s", err) + } - for _, env := range config.Challenge.Env.EnvironmentVars { - containerEnv = append(containerEnv, fmt.Sprintf("%s=%s", env.Key, filepath.Join(core.BEAST_DOCKER_CHALLENGE_DIR, env.Value))) - } + } else { + // local deplyoment + err := cr.DeployContainerFromCompose(challengeName, stagingDir) + if err != nil { + return fmt.Errorf("error while deploying challenge with docker-compose: %v", err) + } - log.Debugf("Container config for challenge %s are: CPU(%d), Memory(%d), PidsLimit(%d)", - config.Challenge.Metadata.Name, - config.Resources.CPUShares, - config.Resources.Memory, - config.Resources.PidsLimit, - ) + // For Docker Compose challenges, ensure ContainerId is empty in the database + if err := database.UpdateChallenge(challenge, map[string]interface{}{"ContainerId": ""}); err != nil { + return fmt.Errorf("error while setting empty ContainerId for Docker Compose challenge: %s", err) + } + } - // Since till this point we have already valiadated the challenge config this is highly - // unlikely to fail. - portMapping, err := config.Challenge.Env.GetPortMappings() - if err != nil { - return fmt.Errorf("error while parsing port mapping for the challenge %s: %s", config.Challenge.Metadata.Name, err) - } + log.Infof("Challenge %s deployed with docker-compose successfully", challengeName) + return nil - containerConfig := cr.CreateContainerConfig{ - PortMapping: portMapping, - MountsMap: staticMount, - ImageId: challenge.ImageId, - ContainerName: coreUtils.EncodeID(config.Challenge.Metadata.Name), - ContainerEnv: containerEnv, - ContainerNetwork: containerNetwork, - Traffic: config.Challenge.Env.TrafficType(), - CPUShares: config.Resources.CPUShares, - Memory: config.Resources.Memory, - PidsLimit: config.Resources.PidsLimit, - } - log.Debugf("create container config for challenge(%s): %v", config.Challenge.Metadata.Name, containerConfig) - var containerId string - if challenge.ServerDeployed == core.LOCALHOST || challenge.ServerDeployed == "" { - containerId, err = cr.CreateContainerFromImage(&containerConfig) } else { - server := cfg.Cfg.AvailableServers[challenge.ServerDeployed] - containerId, err = remoteManager.CreateContainerFromImageRemote(containerConfig, server) - } + staticMount := make(map[string]string) + var staticMountDir string + if challenge.ServerDeployed == core.LOCALHOST || challenge.ServerDeployed == "" { + staticMountDir = filepath.Join(core.BEAST_GLOBAL_DIR, core.BEAST_STAGING_DIR, config.Challenge.Metadata.Name, core.BEAST_STATIC_FOLDER) + } else { + staticMountDir = filepath.Join("$HOME/.beast", core.BEAST_STAGING_DIR, config.Challenge.Metadata.Name, core.BEAST_STATIC_FOLDER) + } + relativeStaticContentDir := config.Challenge.Env.StaticContentDir + if relativeStaticContentDir == "" { + relativeStaticContentDir = core.PUBLIC + } + staticMount[staticMountDir] = filepath.Join("/challenge", relativeStaticContentDir) + log.Debugf("Static mount config for deploy : %s", staticMount) + + var containerEnv []string + var containerNetwork string + if config.Challenge.Metadata.Sidecar != "" { + // We need to configure the sidecar for the challenge container. + // Push the environment variables to the container and link to the sidecar. + env := getSidecarEnv(&config) + containerEnv = append(containerEnv, env...) + + containerNetwork = getSidecarNetwork(config.Challenge.Metadata.Sidecar) + } - if err != nil { - if containerId != "" { - if e := database.UpdateChallenge(challenge, map[string]interface{}{"ContainerId": containerId}); e != nil { - return fmt.Errorf("error while starting container : %s and saving database : %s", err, e) - } + for _, env := range config.Challenge.Env.EnvironmentVars { + containerEnv = append(containerEnv, fmt.Sprintf("%s=%s", env.Key, filepath.Join(core.BEAST_DOCKER_CHALLENGE_DIR, env.Value))) + } + + log.Debugf("Container config for challenge %s are: CPU(%d), Memory(%d), PidsLimit(%d)", + config.Challenge.Metadata.Name, + config.Resources.CPUShares, + config.Resources.Memory, + config.Resources.PidsLimit, + ) - return fmt.Errorf("error while starting the container : %s", err) + // Since till this point we have already valiadated the challenge config this is highly + // unlikely to fail. + portMapping, err := config.Challenge.Env.GetPortMappings() + if err != nil { + return fmt.Errorf("error while parsing port mapping for the challenge %s: %s", config.Challenge.Metadata.Name, err) } - return fmt.Errorf("error while trying to create a container for the challenge: %s", err) - } + containerConfig := cr.CreateContainerConfig{ + PortMapping: portMapping, + MountsMap: staticMount, + ImageId: challenge.ImageId, + ContainerName: coreUtils.EncodeID(config.Challenge.Metadata.Name), + ContainerEnv: containerEnv, + ContainerNetwork: containerNetwork, + Traffic: config.Challenge.Env.TrafficType(), + CPUShares: config.Resources.CPUShares, + Memory: config.Resources.Memory, + PidsLimit: config.Resources.PidsLimit, + } + log.Debugf("create container config for challenge(%s): %v", config.Challenge.Metadata.Name, containerConfig) + var containerId string + if challenge.ServerDeployed == core.LOCALHOST || challenge.ServerDeployed == "" { + containerId, err = cr.CreateContainerFromImage(&containerConfig) + } else { + server := cfg.Cfg.AvailableServers[challenge.ServerDeployed] + containerId, err = remoteManager.CreateContainerFromImageRemote(containerConfig, server) + } - challenge.ContainerId = containerId - if err = database.UpdateChallenge(challenge, map[string]interface{}{"ContainerId": containerId}); err != nil { - return fmt.Errorf("error while saving containerId to database : %s", err) - } + if err != nil { + if containerId != "" { + if e := database.UpdateChallenge(challenge, map[string]interface{}{"ContainerId": containerId}); e != nil { + return fmt.Errorf("error while starting container : %s and saving database : %s", err, e) + } + + return fmt.Errorf("error while starting the container : %s", err) + } + return fmt.Errorf("error while trying to create a container for the challenge: %s", err) + } + + if err = database.UpdateChallenge(challenge, map[string]interface{}{"ContainerId": containerId}); err != nil { + return fmt.Errorf("error while saving containerId to database : %s", err) + } + } return nil } diff --git a/pkg/cr/containers.go b/pkg/cr/containers.go index 777e8f4c..141d6c66 100644 --- a/pkg/cr/containers.go +++ b/pkg/cr/containers.go @@ -1,9 +1,13 @@ package cr import ( + "bytes" "fmt" "io/ioutil" + "os/exec" + "path/filepath" "strconv" + "strings" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" @@ -154,7 +158,7 @@ func CreateContainerFromImage(containerConfig *CreateContainerConfig) (string, e for _, portMapping := range containerConfig.PortMapping { natPort, err := nat.NewPort(containerConfig.TrafficType(), strconv.Itoa(int(portMapping.ContainerPort))) if err != nil { - return "", fmt.Errorf("Error while creating new port from port %d", portMapping.ContainerPort) + return "", fmt.Errorf("error while creating new port from port %d", portMapping.ContainerPort) } portSet[natPort] = struct{}{} @@ -197,7 +201,7 @@ func CreateContainerFromImage(containerConfig *CreateContainerConfig) (string, e createResp, err := cli.ContainerCreate(ctx, config, hostConfig, nil, nil, containerName) if err != nil { - log.Error("Error while creating the container with name %s", containerName) + log.Errorf("Error while creating the container with name %s", containerName) return "", err } @@ -207,7 +211,7 @@ func CreateContainerFromImage(containerConfig *CreateContainerConfig) (string, e } if err := cli.ContainerStart(ctx, containerId, types.ContainerStartOptions{}); err != nil { - log.Error("Error while starting the container : %s", err) + log.Errorf("Error while starting the container : %s", err) return "", err } @@ -279,3 +283,64 @@ func CommitContainer(containerId string) (string, error) { return commitResp.ID, nil } + +func DeployContainerFromCompose(challengeName, stagedPath string) error { + extractDir := filepath.Join(stagedPath, challengeName) + log.Debugf("Deploying challenge %s using docker-compose at %s", challengeName,extractDir ) + upCmd := exec.Command("bash", "-c", fmt.Sprintf("cd %s && docker compose up -d", extractDir)) + var upOutput bytes.Buffer + upCmd.Stdout = &upOutput + upCmd.Stderr = &upOutput + + if err := upCmd.Run(); err != nil { + log.Errorf("docker-compose up failed for challenge %s. Output:\n%s", challengeName, upOutput.String()) + return fmt.Errorf("error while running docker compose up: %v", err) + } + + var psOutput bytes.Buffer + checkCmd := exec.Command("bash", "-c", fmt.Sprintf("cd %s && docker compose ps", extractDir)) + checkCmd.Stdout = &psOutput + checkCmd.Stderr = &psOutput + if err := checkCmd.Run(); err != nil { + return fmt.Errorf("error checking container status after compose up for challenge %s. Output:\n%s", challengeName, psOutput.String()) + } + if !strings.Contains(psOutput.String(), "Up") { + return fmt.Errorf("container not running after compose up for challenge %s. Output:\n%s", challengeName, psOutput.String()) + } + + return nil +} + +func ComposeDown(challengeName, stagedDir string) error { + log.Debugf("Stopping challenge %s using docker-compose", challengeName) + extractDir := filepath.Join(stagedDir, challengeName) + downCmd := exec.Command("bash", "-c",fmt.Sprintf("cd %s && docker compose down", extractDir)) + + var downOutput bytes.Buffer + downCmd.Stdout = &downOutput + downCmd.Stderr = &downOutput + + if err := downCmd.Run(); err != nil { + log.Errorf("docker-compose down failed for challenge %s. Output:\n%s", challengeName, downOutput.String()) + return fmt.Errorf("error while running docker compose down: %v", err) + } + + return nil +} + +func ComposePurge(challengeName, stagedDir string) error { + log.Debugf("Purging challenge %s using docker-compose", challengeName) + extractDir := filepath.Join(stagedDir, challengeName) + purgeCmd := exec.Command("bash","-c",fmt.Sprintf("cd %s && docker compose down --remove-orphans --volumes --rmi all", extractDir)) + + var purgeOutput bytes.Buffer + purgeCmd.Stdout = &purgeOutput + purgeCmd.Stderr = &purgeOutput + + if err := purgeCmd.Run(); err != nil { + log.Errorf("docker-compose purge failed for challenge %s. Output:\n%s", challengeName, purgeOutput.String()) + return fmt.Errorf("error while running docker compose purge: %v", err) + } + + return nil +} diff --git a/pkg/cr/images.go b/pkg/cr/images.go index 4cf001e3..e2727419 100644 --- a/pkg/cr/images.go +++ b/pkg/cr/images.go @@ -4,10 +4,14 @@ import ( "bytes" "fmt" "os" + "os/exec" + "path/filepath" + "strings" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/client" + "github.com/sdslabs/beastv4/core" log "github.com/sirupsen/logrus" "golang.org/x/net/context" @@ -66,9 +70,10 @@ func SearchImageByFilter(filterMap map[string]string) ([]types.ImageSummary, err } func BuildImageFromTarContext(challengeName, challengeTag, tarContextPath, dockerCtxFile string, noCache bool) (*bytes.Buffer, string, error) { + ctx := context.Background() builderContext, err := os.Open(tarContextPath) if err != nil { - return nil, "", fmt.Errorf("Error while opening staged file :: %s", tarContextPath) + return nil, "", fmt.Errorf("error while opening staged file :: %s", tarContextPath) } defer builderContext.Close() @@ -81,13 +86,13 @@ func BuildImageFromTarContext(challengeName, challengeTag, tarContextPath, docke dockerClient, err := client.NewEnvClient() if err != nil { - return nil, "", fmt.Errorf("Error while creating a docker client for beast: %s", err) + return nil, "", fmt.Errorf("error while creating a docker client for beast: %s", err) } log.Debug("Image build in process") - imageBuildResp, err := dockerClient.ImageBuild(context.Background(), builderContext, buildOptions) + imageBuildResp, err := dockerClient.ImageBuild(ctx, builderContext, buildOptions) if err != nil { - return nil, "", fmt.Errorf("An error while build image for challenge %s :: %s", challengeName, err) + return nil, "", fmt.Errorf("an error while build image for challenge %s :: %s", challengeName, err) } defer imageBuildResp.Body.Close() @@ -102,3 +107,33 @@ func BuildImageFromTarContext(challengeName, challengeTag, tarContextPath, docke return buf, "", err } + +// TODO: find a better way to build images from docker-compose instead of cmd running +func BuildImagesFromCompose(challengeName, challengeTag, stagedPath, ComposeFile string, noCache bool) (*bytes.Buffer, error) { + extractPath := filepath.Join(core.BEAST_GLOBAL_DIR, core.BEAST_STAGING_DIR, challengeName, challengeName) + + extractCmd := fmt.Sprintf("mkdir -p %s && tar -xf %s -C %s", extractPath, stagedPath, extractPath) + err := exec.Command("bash", "-c", extractCmd).Run() + if err != nil { + return nil, fmt.Errorf("error while extracting tar file %s to %s: %v", stagedPath, extractPath, err) + } + chngDir := fmt.Sprintf("cd %s", extractPath) + cmdArgs := []string{"compose", "build"} + if noCache { + cmdArgs = append(cmdArgs, "--no-cache") + } + composeCmd := fmt.Sprintf("%s && docker %s", chngDir, strings.Join(cmdArgs, " ")) + log.Debugf("Building image for challenge %s with tag %s", challengeName, challengeTag) + log.Debugf("Running the command: docker %v", cmdArgs) + + cmd := exec.Command("bash", "-c", composeCmd) + cmd.Dir = extractPath + var outBuffer bytes.Buffer + cmd.Stdout = &outBuffer + cmd.Stderr = &outBuffer + + if err := cmd.Run(); err != nil { + return &outBuffer, fmt.Errorf("error while building image for challenge %s with tag %s: %v", challengeName, challengeTag, err) + } + return &outBuffer, nil +} diff --git a/pkg/remoteManager/container.go b/pkg/remoteManager/container.go index e73f32b0..1b2f0294 100644 --- a/pkg/remoteManager/container.go +++ b/pkg/remoteManager/container.go @@ -73,7 +73,7 @@ func StopAndRemoveContainerRemote(containerId string, server config.AvailableSer log.Debugf("no container with container id %s present", containerId) return nil } - return fmt.Errorf("DATABASE ERROR while fetching user details.") + return fmt.Errorf("DATABASE ERROR while fetching user details") } if len(chall) > 0 { server = config.Cfg.AvailableServers[chall[0].ServerDeployed] @@ -194,3 +194,42 @@ func CommitContainerRemote(containerID string, server config.AvailableServer) (s imageID := strings.TrimSpace(output) return imageID, nil } + +func DeployContainerFromComposeRemote(challengeName, stagedDir string, server config.AvailableServer) error { + extractDir := fmt.Sprintf("%s/%s", stagedDir, challengeName) + upCommand := fmt.Sprintf("cd %s && docker compose up -d",extractDir) + log.Debugf("Deploying challenge %s using docker compose remotely: %s", challengeName, upCommand) + upOutput, err := RunCommandOnServer(server, upCommand) + if err != nil { + log.Errorf("docker compose up failed for challenge %s. Output:\n%s", challengeName, upOutput) + return fmt.Errorf("error while running docker compose up on remote: %v", err) + } + + return nil +} + +func ComposeDownRemote(challengeName, stagedDir string, server config.AvailableServer) error { + extractDir := fmt.Sprintf("%s/%s", stagedDir, challengeName) + downCommand := fmt.Sprintf("cd %s && docker compose down",extractDir) + log.Debugf("Stopping challenge %s using docker compose remotely: %s", challengeName, downCommand) + downOutput, err := RunCommandOnServer(server, downCommand) + if err != nil { + log.Errorf("docker compose down failed for challenge %s. Output:\n%s", challengeName, downOutput) + return fmt.Errorf("error while running docker compose down on remote: %v", err) + } + + return nil +} + +func ComposePurgeRemote(challengeName, stagedDir string, server config.AvailableServer) error { + extractDir := fmt.Sprintf("%s/%s", stagedDir, challengeName) + purgeCommand := fmt.Sprintf("cd %s && docker compose down --volumes --remove-orphans --rmi all", extractDir) + log.Debugf("Purge challenge %s using docker compose remotely: %s", challengeName, purgeCommand) + purgeOutput, err := RunCommandOnServer(server, purgeCommand) + if err != nil { + log.Errorf("docker compose purge failed for challenge %s. Output:\n%s", challengeName, purgeOutput) + return fmt.Errorf("error while running docker compose purge on remote: %v", err) + } + + return nil +} diff --git a/pkg/remoteManager/file.go b/pkg/remoteManager/file.go index decef398..89b00dfd 100644 --- a/pkg/remoteManager/file.go +++ b/pkg/remoteManager/file.go @@ -103,3 +103,24 @@ func BuildImageFromTarContextRemote(challengeName string, imageTag string, stage } return []byte(output), strings.TrimSpace(imageID), nil } + +func BuildImagesFromComposeRemote(challengeName, imageTag, stagedDir string, server config.AvailableServer, noCache bool) ([]byte, error) { + remoteExtractPath := filepath.Join(core.BEAST_REMOTE_GLOBAL_DIR, core.BEAST_STAGING_DIR, challengeName, challengeName) + _, err := RunCommandOnServer(server, fmt.Sprintf("mkdir -p %s && tar -xf %s -C %s", remoteExtractPath, stagedDir, remoteExtractPath)) + if err != nil { + return []byte{}, fmt.Errorf("failed to extract tar: %s", err) + } + cmdBase := "docker compose build" + if noCache { + cmdBase += " --no-cache" + } + dockerComposeBuildCmd := fmt.Sprintf("cd %s && %s", remoteExtractPath, cmdBase) + + // Execute the command on the remote server + output, err := RunCommandOnServer(server, dockerComposeBuildCmd) + if err != nil { + return []byte(output), fmt.Errorf("failed to build docker compose images remotely: %s\nOutput: %s", err, output) + } + + return []byte(output), nil +}