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)),
    ]);
};