core/validators/security.validator.ts
core/validators/security.validator.ts is a file in Bane's Lab Build Scripts. 66 lines of code and 18 definitions.
import { SECURITY_KEY_FILE, SECURITY_TXT_FIELDS, SECURITY_TXT_FILE } from "#configuration/constants/site.constants";
import {
UNSIGNED_SECURITY_TXT,
WANTED_SECURITY_CONTACT,
WANTED_SECURITY_EXPIRY,
malformedSecurityField,
} from "#configuration/strings/site.strings";
import type { Finding } from "#types/validation.types";
import { checkFile } from "#core/validators/build.validator";
import { isSignedBy } from "#core/adapters/security.adapter";
const SLASH = "/";
const FIELD_JOIN = ": ";
const YEAR_MILLISECONDS = 31_536_000_000;
const MAIL_SCHEME = "mailto:";
const fieldsOf = function fieldsOf(text: string): ReadonlyMap<string, string> {
const fields = new Map<string, string>();
for (const line of text.split("\n")) {
const at = line.indexOf(FIELD_JOIN);
if (at > 0) {
fields.set(line.slice(0, at), line.slice(at + FIELD_JOIN.length));
}
}
return fields;
};
const isCurrentExpiry = function isCurrentExpiry(value: string, now: Date): boolean {
const at = Date.parse(value);
return !Number.isNaN(at) && at > now.getTime() && at - now.getTime() <= YEAR_MILLISECONDS;
};
export const checkSecurityTxt = function checkSecurityTxt(file: string, text: string, site: string, now: Date): Finding[] {
const fields = fieldsOf(text);
const contact = fields.get(SECURITY_TXT_FIELDS.contact) ?? "";
const expires = fields.get(SECURITY_TXT_FIELDS.expires) ?? "";
const canonical = fields.get(SECURITY_TXT_FIELDS.canonical) ?? "";
const encryption = fields.get(SECURITY_TXT_FIELDS.encryption) ?? "";
const address = `${site}${SLASH}${SECURITY_TXT_FILE}`;
const keyAddress = `${site}${SLASH}${SECURITY_KEY_FILE}`;
const findings: Finding[] = [];
if (!contact.startsWith(MAIL_SCHEME) || contact.length === MAIL_SCHEME.length) {
findings.push({
file,
message: malformedSecurityField(SECURITY_TXT_FIELDS.contact, contact, WANTED_SECURITY_CONTACT),
});
}
if (!isCurrentExpiry(expires, now)) {
findings.push({
file,
message: malformedSecurityField(SECURITY_TXT_FIELDS.expires, expires, WANTED_SECURITY_EXPIRY),
});
}
if (canonical !== address) {
findings.push({ file, message: malformedSecurityField(SECURITY_TXT_FIELDS.canonical, canonical, address) });
}
if (encryption !== keyAddress) {
findings.push({ file, message: malformedSecurityField(SECURITY_TXT_FIELDS.encryption, encryption, keyAddress) });
}
return findings;
};
export const checkSignature = function checkSignature(file: string, text: string, publicKey: string): Finding[] {
return isSignedBy(publicKey, text) ? [] : [{ file, message: UNSIGNED_SECURITY_TXT }];
};
export const securityFindings = function securityFindings(site: string, now: Date): Finding[] {
return checkFile(SECURITY_TXT_FILE, (text) => [
...checkSecurityTxt(SECURITY_TXT_FILE, text, site, now),
...checkFile(SECURITY_KEY_FILE, (key) => checkSignature(SECURITY_TXT_FILE, text, key)),
]);
};