UNPKG

appwarden

Version:

CLI tool to manage Appwarden monitors and deployments

387 lines (373 loc) 11.2 kB
#!/usr/bin/env node // src/runner/index.ts import { Command } from "commander"; // src/commands/init/init.ts import fs from "fs"; import YAML from "yaml"; // src/constants.ts var appwardenDir = "./.appwarden"; var domainsDir = `${appwardenDir}/domains`; var workerDir = `${appwardenDir}/cloudflare-worker`; function splitOnSerialNumber(str) { const index = str.search(/\d{4}/); if (index === -1) { return [str, ""]; } const before = str.substring(0, index); const after = str.substring(index); return [before, after]; } var unquote = (str) => ( // replace the opening and closing quotation in str if they exist str.replace(/^"|"$/g, "") ); var allowedDNSRecords = [ { type: "A" }, { type: "AAAA" }, { type: "CNAME" }, { type: "TXT", transformer: unquote }, { type: "MX" }, { type: "NS", transformer: (value) => /\.$/.test(value) ? value : `${value}.` }, { type: "SOA", transformer: (value) => { const [prefix] = splitOnSerialNumber(value); return prefix.trim(); }, validator: (value) => { const [, soaNumbers] = splitOnSerialNumber(value); if (soaNumbers) { return "starts-with"; } return void 0; } }, { type: "SRV" }, { type: "CAA" }, { type: "DS" }, { type: "DNSKEY" }, { type: "RRSIG" }, { type: "NSEC" }, { type: "NSEC3" }, { type: "NSEC3PARAM" }, { type: "SPF" }, { type: "DKIM" }, { type: "DMARC" } ]; var allowedHeaderNames = [ "content-type", "transfer-encoding", "connection", "cache-control", "vary", "server", // security headers "content-security-policy", "x-content-type-options", "x-frame-options", "strict-transport-security", "x-xss-protection", "referrer-policy", "feature-policy", "permissions-policy", "cross-origin-embedder-policy", "cross-origin-opener-policy", "cross-origin-resource-policy", "access-control-allow-origin", "access-control-allow-methods", "access-control-allow-headers", "access-control-expose-headers", "access-control-allow-credentials", "access-control-max-age", "public-key-pins", "expect-ct", "nel" ]; // src/utils/debug.ts var debug = (...msg) => { if (false) { console.log(...msg); } }; // src/utils/ensure-protocol.ts var protocolRegex = /^https?:\/\//i; var ensureProtocol = (maybeFQDN) => { const hasProtocol = protocolRegex.test(maybeFQDN); if (!hasProtocol) { return `https://${maybeFQDN}`; } return maybeFQDN; }; // src/utils/get-dns-records.ts import { getAllDnsRecords } from "@layered/dns-records"; // src/utils/parse-domain.ts import { ParseResultType, fromUrl, parseDomain } from "parse-domain"; var parseHostname = (hostname) => parseDomain(fromUrl(hostname)); var getDomainMetadata = (thing) => { const parseResult = parseHostname(thing); if (parseResult.type === ParseResultType.Listed && parseResult.domain) { return parseResult; } return false; }; var getRootDomain = (listed) => listed.domain + "." + listed.topLevelDomains.join("."); var transformLayeredDNSRecords = (recordsList) => sortDNSRecords( recordsList.map((record) => ({ name: record.name, type: record.type, content: record.data })) ); var sortDNSRecords = (records) => records.sort((a, b) => { if (a.type && b.type) { if (a.type < b.type) { return -1; } if (a.type > b.type) { return 1; } } if (a.name && b.name) { if (a.name < b.name) { return -1; } if (a.name > b.name) { return 1; } } if (a.content && b.content) { if (a.content < b.content) { return -1; } if (a.content > b.content) { return 1; } } return 0; }); // src/utils/get-dns-records.ts var getDNSRecords = (domain) => getAllDnsRecords(domain, { resolver: "cloudflare-dns", commonSubdomainsCheck: false, subdomains: [] }).then((records) => transformLayeredDNSRecords(records)); // src/utils/sleep.ts var sleep = (timeout = 1e3) => new Promise((resolve) => setTimeout(resolve, timeout)); // src/commands/init/init.ts var logFinishedScaffolding = (domains) => { console.log( `[@appwarden] Created an .appwarden folder in your current directory with ${domains.length} ${domains.length === 1 ? "domain" : "domains"}...` ); if (domains.length === 0) { console.log( "[@appwarden] To monitor a domain with Appwarden, run `npx appwarden init -d example.com`." ); } console.log(`[@appwarden] .appwarden/`); console.log(`[@appwarden] \u2514\u2500\u2500 domains/`); fs.readdirSync(".appwarden/domains").forEach((file, i, list) => { const isLast = i === list.length - 1; const prefix = isLast ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500"; console.log(`[@appwarden] ${prefix} ${file}`); }); console.log(`[@appwarden]`); }; var initCommand = { command: "init", description: "Set up Appwarden directory and domain configuration", options: [ "-d, --domain <domains...>", "Create a domain configuration file for this domain (e.g. appwarden.io)" ], action: async (args) => { console.log("[@appwarden] Running `init` command..."); const domains = args.domain ?? []; const domainsMap = {}; for (const domain of domains) { const domainMetadata = getDomainMetadata(domain); if (!domainMetadata) { console.error(`[@appwarden] Invalid domain: ${domain}`); process.exit(1); } const rootDomain = getRootDomain(domainMetadata); if (rootDomain) { domainsMap[rootDomain] = [ ...domainsMap[rootDomain] ?? [], domainMetadata ]; } } try { for (const dir of [appwardenDir, domainsDir]) { fs.mkdirSync(dir, { recursive: true }); } } catch (error) { } if (Object.keys(domainsMap).length === 0) { return logFinishedScaffolding(domains); } for (const [domain, subdomains] of Object.entries(domainsMap)) { let monitorFileContent = { hostname: domain, version: 1, dns: { options: { links: [] }, monitors: [] } }; let dnsRecords = []; try { const subdomainsList = subdomains.map((subdomain) => subdomain.hostname).flat(); const promises = []; for (const subdomain of subdomainsList) { promises.push(getDNSRecords(subdomain)); } dnsRecords = (await Promise.all(promises)).flat(); } catch (error) { if (error instanceof Error) { console.error( `[@appwarden] Error fetching DNS records for ${domain}:`, error.message ); } } debug(`dnsRecords`, dnsRecords); let isFirstRecord = true; const monitoredRecords = []; for (const dnsRecord of dnsRecords) { for (const allowedRecord of allowedDNSRecords) { if (dnsRecord.type !== allowedRecord.type) { continue; } const existingRecord = monitoredRecords.find( (record) => record.name === dnsRecord.name && record.type === dnsRecord.type ); const makeDNSEntry = (dnsRecord2, allowedRecord2, isFirstRecord2) => ({ content: allowedRecord2.transformer ? allowedRecord2.transformer(dnsRecord2.content) : dnsRecord2.content, ...allowedRecord2.validator && { validator: allowedRecord2.validator(dnsRecord2.content) }, ...isFirstRecord2 && { comment: "this is the first nested record" } }); if (existingRecord) { if (typeof existingRecord.content === "string") { existingRecord.content = [ makeDNSEntry(existingRecord, allowedRecord, isFirstRecord), makeDNSEntry(dnsRecord, allowedRecord) ]; isFirstRecord = false; } else { existingRecord.content.push( makeDNSEntry(dnsRecord, allowedRecord) ); } } else { monitoredRecords.push({ name: dnsRecord.name, type: dnsRecord.type, ...makeDNSEntry(dnsRecord, allowedRecord) }); } } } monitorFileContent.dns.monitors = monitoredRecords; for (const subdomain of subdomains) { const subdomainWebsiteMonitor = { url: subdomain.hostname, status: 200, headers: [] }; monitorFileContent = { ...monitorFileContent, websites: { ...monitorFileContent.websites, monitors: [ ...monitorFileContent.websites?.monitors ?? [], subdomainWebsiteMonitor ] } }; let websiteResponse = null; try { websiteResponse = await fetch( new URL(ensureProtocol(subdomain.hostname)), { headers: { "content-type": "text/html; charset=utf-8" } } ); } catch (error) { if (error instanceof Error) { console.log( `[@appwarden] Error fetching website headers from ${subdomain.hostname}:`, error.message ); } } let websiteHeaders = []; if (websiteResponse) { for (const headerName of allowedHeaderNames) { const headerValue = websiteResponse.headers.get(headerName); if (headerValue) { websiteHeaders.push({ name: headerName, value: headerValue }); } } } subdomainWebsiteMonitor.headers = websiteHeaders; } debug( `${domainsDir}/${domain.replaceAll(".", "-")}.yml `, `${YAML.stringify(monitorFileContent)} ` ); fs.writeFileSync( `${domainsDir}/${domain.replaceAll(".", "-")}.yml`, `${YAML.stringify(monitorFileContent)} ` ); } logFinishedScaffolding(domains); await sleep(); console.log( `[@appwarden] To finish setting up Appwarden, install the [Github integration](https://github.com/apps/appwarden).` ); await sleep(600); console.log( `[@appwarden] Create a pull request to your main branch and follow the instructions from the Appwarden bot.` ); await sleep(600); console.log( `[@appwarden] Learn more at https://appwarden.io/docs/getting-started.` ); await sleep(); console.log(`[@appwarden] Thanks for using Appwarden, goodbye!`); } }; // src/commands/index.ts var cli = { name: "appwarden", description: "CLI tool to manage Appwarden configuration files and deployments. Learn more at https://appwarden.io/docs." }; // src/runner/index.ts async function runner() { const program = new Command().name(cli.name).description(cli.description).version("1.2.0"); program.command(initCommand.command).description(initCommand.description).option(initCommand.options[0], initCommand.options[1]).action(initCommand.action); program.parse(process.argv); } // src/index.ts import("dotenv/config"); var run = async function() { await runner(); }; run();