rules/eslint/no-fail-open-guard.eslint.rule.ts

rules/eslint/no-fail-open-guard.eslint.rule.ts is a file in GovLab Extension Host. 142 lines of code and 34 definitions.

import type { Rule } from "eslint";
import { defineCheck } from "@govlab/context/check";

defineCheck({ detects: [], enforces: ["architecture:fail-secure"] });

import { folderFor } from "../../shared/manifests/taxonomy.manifest.ts";

interface AstNode {
    type: string;
    name?: string;
    callee?: AstNode;
    params?: AstNode[];
}

const GUARD_CONCERN = "guard";
const GUARD_FOLDER = folderFor(GUARD_CONCERN);

const GUARD_PATH_MARKERS = new Set(
    ["middleware", GUARD_FOLDER].filter((name): name is string => name !== undefined).map((name) => `/${name}/`),
);

const CONTINUATION_PARAMS = new Set(["next", "proceed", "allow", "continueRequest"]);

const MESSAGE =
    "A request guard must not continue the request from its failure path. This error handler invokes the same continuation the success path uses. Deny explicitly on the failure path. [no_fail_open_guard] [canon: quality:concept:input-validation]";

const isNode = function isNode(value: unknown): value is AstNode {
    return value !== null && typeof value === "object" && "type" in value;
};

const asNode = function asNode(value: unknown): AstNode | null {
    return isNode(value) ? value : null;
};

const normalize = function normalize(filename: string): string {
    return filename.split("\\").join("/");
};

const inGuardPath = function inGuardPath(filename: string): boolean {
    for (const marker of GUARD_PATH_MARKERS) {
        if (filename.includes(marker)) {
            return true;
        }
    }
    return false;
};

const continuationOf = function continuationOf(node: Rule.Node): string {
    const fn = asNode(node);
    const params = fn?.params ?? [];
    for (const param of params) {
        const name = param.name ?? "";
        if (CONTINUATION_PARAMS.has(name)) {
            return name;
        }
    }
    return "";
};

interface Tracker {
    onCall: (node: Rule.Node) => void;
    onCatchEnter: () => void;
    onCatchExit: () => void;
    onEnter: (node: Rule.Node) => void;
    onExit: (node: Rule.Node) => void;
}

interface ContinuationStack {
    active: () => readonly string[];
    pop: (node: Rule.Node) => void;
    push: (node: Rule.Node) => void;
}

const createContinuationStack = function createContinuationStack(): ContinuationStack {
    let active: readonly string[] = [];
    return {
        active: (): readonly string[] => active,
        pop(node: Rule.Node): void {
            if (continuationOf(node).length > 0) {
                active = active.slice(0, -1);
            }
        },
        push(node: Rule.Node): void {
            const continuation = continuationOf(node);
            if (continuation.length > 0) {
                active = [...active, continuation];
            }
        },
    };
};

interface CatchDepth {
    enter: () => void;
    exit: () => void;
    inside: () => boolean;
}

const createCatchDepth = function createCatchDepth(): CatchDepth {
    let depth = 0;
    return {
        enter: (): void => {
            depth += 1;
        },
        exit: (): void => {
            depth -= 1;
        },
        inside: (): boolean => depth > 0,
    };
};

const createTracker = function createTracker(context: Rule.RuleContext): Tracker {
    const stack = createContinuationStack();
    const catches = createCatchDepth();
    return {
        onCall(node: Rule.Node): void {
            const callee = asNode(node)?.callee;
            if (catches.inside() && callee?.type === "Identifier" && stack.active().includes(callee.name ?? "")) {
                context.report({ messageId: "failOpen", node });
            }
        },
        onCatchEnter: catches.enter,
        onCatchExit: catches.exit,
        onEnter: stack.push,
        onExit: stack.pop,
    };
};

const listenersFor = function listenersFor(tracker: Tracker): Rule.RuleListener {
    const entries: [string, (node: Rule.Node) => void][] = [
        ["FunctionDeclaration", tracker.onEnter],
        ["FunctionDeclaration:exit", tracker.onExit],
        ["FunctionExpression", tracker.onEnter],
        ["FunctionExpression:exit", tracker.onExit],
        ["ArrowFunctionExpression", tracker.onEnter],
        ["ArrowFunctionExpression:exit", tracker.onExit],
        ["CallExpression", tracker.onCall],
    ];
    const catchEntries: [string, () => void][] = [
        ["CatchClause", tracker.onCatchEnter],
        ["CatchClause:exit", tracker.onCatchExit],
    ];
    return Object.fromEntries([...entries, ...catchEntries]);
};

const noFailOpenGuard: Rule.RuleModule = {
    create(context: Rule.RuleContext): Rule.RuleListener {
        if (!inGuardPath(normalize(context.filename))) {
            return {};
        }
        return listenersFor(createTracker(context));
    },
    meta: {
        docs: {
            description:
                "A guard that cannot determine the answer refuses rather than allows. A failure path that returns the permissive value, or invokes the success continuation, is a fail-open guard.",
        },
        messages: { failOpen: MESSAGE },
        schema: [],
        type: "problem",
    },
};

export default { plugins: { "govlab-local": { rules: { "no-fail-open-guard": noFailOpenGuard } } }, tool: "eslint" };