diff --git a/database/Partner.ts b/database/Partner.ts index d84a9f0..53bfcb6 100644 --- a/database/Partner.ts +++ b/database/Partner.ts @@ -14,7 +14,7 @@ export type PartnerTitle = | "Deputy Director of Engineering" | "Deputy Director of Operations" | "Services Manager" - | "Project Manager" + | "Community Manager" | "Engineering Core Partner" | "Operations Core Partner" | "Community Moderator" @@ -51,6 +51,9 @@ export default class Partner implements SharedMemberAttributes { @prop({ required: true }) public roleType: PartnerRoleType | undefined; + @prop() + public isKeyHolder: boolean | undefined; + @prop({ required: true }) public commissionType: PartnerCommissionType | undefined; diff --git a/discord/commands/TLS.ts b/discord/commands/TLS.ts new file mode 100644 index 0000000..76d175e --- /dev/null +++ b/discord/commands/TLS.ts @@ -0,0 +1,293 @@ +import DiscordInteractionCommand from "../../util/DiscordInteractionCommand"; +import { ChatInputCommandInteraction, EmbedBuilder } from "discord.js"; +import axios from "axios"; + +interface CertificateDetails { + bitLength: number; + connection: { + cipherSuite: string; + tlsVersion: string; + }; + emailAddresses: []; + extendedKeyUsage: number[]; + extendedKeyUsageAsText: string[]; + fingerprint: string; + issuer: { + commonName: string; + country: string[]; + locality: never; // TODO: needs clarification + province: string[]; + organization: string[]; + organizationalUnit: never; // TODO: needs clarification + }; + keyUsageAsText: string[]; + notAfter: Date; + notBefore: Date; + publicKeyAlgorithm: string; + san: string[]; + serialNumber: string; + signatureAlgorithm: string; + status: boolean; + subject: { + commonName: string; + country: string[]; + locality: never; // TODO: needs clarification + province: string[]; + organization: string[]; + organizationalUnit: never; // TODO: needs clarification + }; + validationType: "DV" | "OV" | "EV"; +} + +// Define an enum for security levels +enum SecurityLevel { + MostSecure, + Secure, + LessSecure, + NotSecure, +} + +interface CipherSuite { + cipher: string; + securityLevel: SecurityLevel; +} + +const CipherSuites: CipherSuite[] = [ + // Most Secure (TLS 1.3 AEAD Ciphers) + { cipher: "TLS_AES_256_GCM_SHA384", securityLevel: SecurityLevel.MostSecure }, + { cipher: "TLS_CHACHA20_POLY1305_SHA256", securityLevel: SecurityLevel.MostSecure }, + { cipher: "TLS_AES_128_GCM_SHA256", securityLevel: SecurityLevel.MostSecure }, + + // Secure (TLS 1.2 AEAD Ciphers with Forward Secrecy) + { cipher: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", securityLevel: SecurityLevel.Secure }, + { cipher: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", securityLevel: SecurityLevel.Secure }, + { cipher: "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", securityLevel: SecurityLevel.Secure }, + { cipher: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", securityLevel: SecurityLevel.Secure }, + { cipher: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", securityLevel: SecurityLevel.Secure }, + { cipher: "TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256", securityLevel: SecurityLevel.Secure }, + { cipher: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", securityLevel: SecurityLevel.Secure }, + { cipher: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", securityLevel: SecurityLevel.Secure }, + { cipher: "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", securityLevel: SecurityLevel.Secure }, + + // Less Secure (CBC with TLS 1.2 and SHA-256/SHA-384) + { cipher: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", securityLevel: SecurityLevel.LessSecure }, + { cipher: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", securityLevel: SecurityLevel.LessSecure }, + { cipher: "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256", securityLevel: SecurityLevel.LessSecure }, + { cipher: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", securityLevel: SecurityLevel.LessSecure }, + { cipher: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", securityLevel: SecurityLevel.LessSecure }, + { cipher: "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256", securityLevel: SecurityLevel.LessSecure }, + { cipher: "TLS_RSA_WITH_AES_256_CBC_SHA256", securityLevel: SecurityLevel.LessSecure }, + { cipher: "TLS_RSA_WITH_AES_128_CBC_SHA256", securityLevel: SecurityLevel.LessSecure }, + + // Not Secure (CBC with TLS 1.0/1.1 or SHA-1) + { cipher: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", securityLevel: SecurityLevel.NotSecure }, + { cipher: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", securityLevel: SecurityLevel.NotSecure }, + { cipher: "TLS_DHE_RSA_WITH_AES_256_CBC_SHA", securityLevel: SecurityLevel.NotSecure }, + { cipher: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", securityLevel: SecurityLevel.NotSecure }, + { cipher: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", securityLevel: SecurityLevel.NotSecure }, + { cipher: "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", securityLevel: SecurityLevel.NotSecure }, + { cipher: "TLS_RSA_WITH_AES_256_CBC_SHA", securityLevel: SecurityLevel.NotSecure }, + { cipher: "TLS_RSA_WITH_AES_128_CBC_SHA", securityLevel: SecurityLevel.NotSecure }, + { cipher: "TLS_RSA_WITH_3DES_EDE_CBC_SHA", securityLevel: SecurityLevel.NotSecure }, + { cipher: "TLS_RSA_WITH_RC4_128_SHA", securityLevel: SecurityLevel.NotSecure }, + { cipher: "TLS_RSA_WITH_RC4_128_MD5", securityLevel: SecurityLevel.NotSecure }, +]; + +export default class TLS extends DiscordInteractionCommand { + constructor() { + super("tls", "Receives TLS information about an HTTP server with a FQDN."); + this.builder.addStringOption((option) => { + return option + .setName("fqdn") + .setDescription( + "The Fully Qualified Domain Name (FQDN) for the server you want to perform the TLS/SSL lookup for." + ) + .setRequired(true) + .setMinLength(3); + }); + } + + public getCipherSecurityLevel(cipher: string): SecurityLevel | null { + const result = CipherSuites.find((entry) => entry.cipher === cipher); + return result ? result.securityLevel : null; + } + + public async execute(interaction: ChatInputCommandInteraction) { + await interaction.deferReply({ ephemeral: false }); + try { + const certAPIReq = await axios.get(`https://certapi.libraryofcode.org/`, { + params: { q: interaction.options.getString("fqdn", true) }, + }); + if (certAPIReq.status !== 200) { + return interaction.editReply({ + content: + "Could not fetch information for this FQDN's HTTP server. Please check the FQDN, its server, and try again.", + }); + } + const certData: CertificateDetails = certAPIReq.data; + + if (!certData.status) { + return interaction.editReply({ + content: "Issue when fetching this FQDN's TLS certificate. Please try again later.", + }); + } + const embed = new EmbedBuilder(); + embed.setAuthor({ + name: interaction.options.getString("fqdn", true), + iconURL: `https://${interaction.options.getString("fqdn", true)}/favicon.ico`, + }); + let desc = ""; + if (certData.validationType === "EV") { + desc += `**Certificate issued to:** __${certData.subject.organization[0]} [${certData.subject.country[0]}]__\n**Verified by:** __${certData.issuer.organization[0]}__\n\n`; + } else if (certData.issuer.organization) { + desc += `**Verified by:** __${certData.issuer.organization[0]}__\n\n`; + } + if (certData.subject) { + desc += "## Subject\n"; + if (certData.subject.organization && certData.issuer.commonName) { + desc += `__**${certData.subject.organization[0]} (${certData.subject.commonName})**__\n`; + } + if (certData.subject.commonName) { + desc += `**Common Name:** ${certData.subject.commonName}\n`; + } + if (certData.subject.organization) { + desc += `**Organization:** ${certData.subject.organization[0]}\n`; + } + if (certData.subject.organizationalUnit) { + desc += `**Organizational Unit:** ${certData.subject.organizationalUnit[0]}\n`; + } + if (certData.subject.locality) { + desc += `**Locality:** ${certData.subject.locality[0]}\n`; + } + if (certData.subject.province) { + desc += `**State/Province:** ${certData.subject.province[0]}\n`; + } + if (certData.subject.country) { + desc += `**Country:** ${certData.subject.country[0]}\n`; + } + } + if (certData.issuer) { + desc += "## Issuer\n"; + if (certData.issuer.organization && certData.issuer.commonName) { + desc += `__**${certData.issuer.organization[0]} (${certData.issuer.commonName})**__\n`; + } + if (certData.issuer.commonName) { + desc += `**Common Name:** ${certData.issuer.commonName}\n`; + } + if (certData.issuer.organization) { + desc += `**Organization:** ${certData.issuer.organization[0]}\n`; + } + if (certData.issuer.organizationalUnit) { + desc += `**Organizational Unit:** ${certData.issuer.organizationalUnit[0]}\n`; + } + if (certData.subject.locality) { + desc += `**Locality:** ${certData.subject.locality[0]}\n`; + } + if (certData.issuer.province) { + desc += `**State/Province:** ${certData.issuer.province[0]}\n`; + } + if (certData.issuer.country) { + desc += `**Country:** ${certData.issuer.country[0]}\n`; + } + } + embed.setDescription(desc); + let validationType: + | "Domain Validation (DV)" + | "Organization Validation (OV)" + | ":lock: Extended Validation (EV)" + | string; + switch (certData.validationType) { + case "DV": + validationType = "Domain Validation (DV)"; + break; + case "OV": + validationType = "Organization Validation (OV)"; + embed.setColor("#4287f5"); + break; + case "EV": + embed.setColor("#42f554"); + validationType = ":lock: Extended Validation (EV)"; + break; + default: + validationType = "N/A';"; + break; + } + let cipherSuiteText: string = ""; + switch (this.getCipherSecurityLevel(certData.connection.cipherSuite)) { + case SecurityLevel.MostSecure: + cipherSuiteText = `:green_circle: ${certData.connection.cipherSuite} (${certData.connection.tlsVersion})`; + break; + + case SecurityLevel.Secure: + cipherSuiteText = `:yellow_circle: ${certData.connection.cipherSuite} (${certData.connection.tlsVersion})`; + break; + + case SecurityLevel.LessSecure: + cipherSuiteText = `:orange_circle: ${certData.connection.cipherSuite} (${certData.connection.tlsVersion})`; + break; + + case SecurityLevel.NotSecure: + cipherSuiteText = `:red_circle: ${certData.connection.cipherSuite} (${certData.connection.tlsVersion})`; + break; + + default: + cipherSuiteText = `:grey_question: ${certData.connection.cipherSuite} (${certData.connection.tlsVersion})`; + break; + } + embed.addFields( + { + name: "Validation Type", + value: validationType, + inline: true, + }, + { + name: "Cipher Suite", + value: cipherSuiteText, + inline: false, + }, + { + name: "Public Key Algorithm", + value: `${certData.publicKeyAlgorithm} ${certData.bitLength}`, + inline: true, + }, + { + name: "Signature Algorithm", + value: certData.signatureAlgorithm, + inline: true, + }, + { name: "Not Before", value: new Date(certData.notBefore).toUTCString(), inline: true }, + { name: "Not After", value: new Date(certData.notAfter).toUTCString(), inline: true }, + { + name: "Serial Number", + value: certData.serialNumber, + inline: true, + } + ); + console.log(certData); // TODO: Remove after testing. + if (certData.keyUsageAsText?.length) + embed.addFields({ + name: "Key Usages", + value: certData.keyUsageAsText.join(", "), + inline: true, + }); + if (certData.extendedKeyUsageAsText?.length) + embed.addFields({ + name: "Extended Key Usages", + value: certData.extendedKeyUsageAsText.join(", "), + inline: true, + }); + // embed.addField('Common Name', x509.data.subject.commonName, true); + // embed.addField('Issuer', x509.data.issuer.commonName, true); + // embed.addBlankField(); + // embed.addField('Public Key Algorithm', x509.data.publicKeyAlgorithm, true); + // embed.addField('Not Before', new Date(x509.data.notBefore).toUTCString(), true); + // embed.addField('Not After', new Date(x509.data.notAfter).toUTCString(), true); + // if (x509.data.keyUsageAsText.length) embed.addField('Key Usages', x509.data.keyUsageAsText.join(', '), true); + // if (x509.data.extendedKeyUsageAsText.length) embed.addField('Extended Key Usages', x509.data.extendedKeyUsageAsText.join(', '), true); + + return await interaction.editReply({ embeds: [embed] }); + } catch (err) { + return interaction.editReply({ content: `Error processing retrieval from FQDN: ${err}` }); + } + } +} diff --git a/package-lock.json b/package-lock.json index 407e07e..597487a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,15 @@ { - "name": "CRRA", + "name": "crv2", "lockfileVersion": 3, "requires": true, "packages": { "": { + "name": "crv2", + "license": "AGPL-3.0-or-later", "dependencies": { "@typegoose/typegoose": "^12.2.0", + "auth0": "^4.12.0", + "axios": "^1.7.8", "discord.js": "^14.14.1", "mongoose": "^8.2.2", "stripe": "^14.21.0", @@ -325,9 +329,9 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.1.tgz", - "integrity": "sha512-HFZ4Mp26nbWk9d/BpvP0YNL6W4UoZF0VFcTw/aPPA8RpOxeFQgK+ClABGgAUXs9Y/RGX/l1vOmrqz1MQt9MNuw==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz", + "integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==", "dev": true, "dependencies": { "levn": "^0.4.1" @@ -933,6 +937,39 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/auth0": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/auth0/-/auth0-4.12.0.tgz", + "integrity": "sha512-5WDAHb8EvWSmRyA9D+FTBrHdEL1RM48PTPHVPxSmzbiAXrhR4pSgwSJyoGjia2+rvMR2NMXhtMfuRRqosEp7PA==", + "dependencies": { + "jose": "^4.13.2", + "undici-types": "^6.15.0", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/auth0/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==" + }, + "node_modules/axios": { + "version": "1.7.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.8.tgz", + "integrity": "sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1300,6 +1337,17 @@ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", @@ -1322,9 +1370,9 @@ "devOptional": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1377,6 +1425,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -1871,6 +1927,25 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", @@ -1886,6 +1961,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -2234,6 +2322,14 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -2618,6 +2714,25 @@ "node": ">=8.6" } }, + "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/mimic-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", @@ -3114,6 +3229,11 @@ "node": ">=6.0.0" } }, + "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/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",