294 lines
12 KiB
TypeScript
294 lines
12 KiB
TypeScript
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}` });
|
|
}
|
|
}
|
|
}
|