# core/validators/security.validator.ts

> 66 lines of code and 18 definitions.

Tree: Bane's Lab Build Scripts
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/build-scripts#file-build-scripts-core-validators-security-validator-ts
Source text: https://banes-lab.com/source/build-scripts/core/validators/security.validator.ts.txt

Listed in [core/validators](https://banes-lab.com/api/source/build-scripts/core/validators.md), after [core/validators/schema.validator.ts](https://banes-lab.com/source/build-scripts/core/validators/schema.validator.ts.md) and before [core/validators/site.validator.ts](https://banes-lab.com/source/build-scripts/core/validators/site.validator.ts.md).

## Definitions

- `checkSecurityTxt` (lexical_declaration, line 33, exported)
- `securityFindings` (lexical_declaration, line 67, exported)
- `checkSignature` (lexical_declaration, line 63, exported)
- `isCurrentExpiry` (lexical_declaration, line 28)
- `SLASH` (lexical_declaration, line 12)
- `FIELD_JOIN` (lexical_declaration, line 13)
- `YEAR_MILLISECONDS` (lexical_declaration, line 14)
- `MAIL_SCHEME` (lexical_declaration, line 15)
- `fieldsOf` (lexical_declaration, line 17)
- `at` (lexical_declaration, line 29)
- `fields` (lexical_declaration, line 34, exported)
- `contact` (lexical_declaration, line 35, exported)
- `expires` (lexical_declaration, line 36, exported)
- `canonical` (lexical_declaration, line 37, exported)
- `encryption` (lexical_declaration, line 38, exported)
- `address` (lexical_declaration, line 39, exported)
- `keyAddress` (lexical_declaration, line 40, exported)
- `findings` (lexical_declaration, line 41, exported)

## Contained in

- [core/validators](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-validators.md)

## Uses

- [configuration/strings/site.strings.ts](https://banes-lab.com/source/build-scripts/configuration/strings/site.strings.ts.md)
- [core/adapters/security.adapter.ts](https://banes-lab.com/source/build-scripts/core/adapters/security.adapter.ts.md)
- [core/validators/build.validator.ts](https://banes-lab.com/source/build-scripts/core/validators/build.validator.ts.md)

## Used by

- [core/coordinators/validation.coordinator.ts](https://banes-lab.com/source/build-scripts/core/coordinators/validation.coordinator.ts.md)

## Linked from

- [configuration/strings](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-configuration-strings.md)
- [core/adapters](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-adapters.md)
- [core/coordinators](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-coordinators.md)
- [core/validators](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-validators.md)

## Source

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