appwarden
Version:
CLI tool to manage Appwarden monitors and deployments
387 lines (373 loc) • 11.2 kB
JavaScript
// 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();