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

> 142 lines of code and 34 definitions.

Tree: Governance tree
Language: typescript
Canonical: https://banes-lab.com/anatomy/governance#file-governance-rules-eslint-no-fail-open-guard-eslint-rule-ts
Source text: https://banes-lab.com/source/governance/rules/eslint/no-fail-open-guard.eslint.rule.ts.txt

Listed in [rules/eslint](https://banes-lab.com/api/source/governance/rules/eslint.md), after [rules/eslint/no-exclusion-membership.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/no-exclusion-membership.eslint.rule.ts.md) and before [rules/eslint/no-hardcoded-exclusions.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/no-hardcoded-exclusions.eslint.rule.ts.md).

## Definitions

- `isNode` (lexical_declaration, line 27)
- `normalize` (lexical_declaration, line 35)
- `inGuardPath` (lexical_declaration, line 39)
- `continuationOf` (lexical_declaration, line 48)
- `createTracker` (lexical_declaration, line 111)
- `listenersFor` (lexical_declaration, line 128)
- `create` (method_definition, line 146)
- `asNode` (lexical_declaration, line 31)
- `pop` (method_definition, line 78)
- `AstNode` (interface_declaration, line 8)
- `GUARD_CONCERN` (lexical_declaration, line 15)
- `GUARD_FOLDER` (lexical_declaration, line 16)
- `GUARD_PATH_MARKERS` (lexical_declaration, line 18)
- `CONTINUATION_PARAMS` (lexical_declaration, line 22)
- `MESSAGE` (lexical_declaration, line 24)
- `fn` (lexical_declaration, line 49)
- `params` (lexical_declaration, line 50)
- `name` (lexical_declaration, line 52)
- `Tracker` (interface_declaration, line 60)
- `ContinuationStack` (interface_declaration, line 68)
- `createContinuationStack` (lexical_declaration, line 74)
- `active` (lexical_declaration, line 75)
- `push` (method_definition, line 83)
- `continuation` (lexical_declaration, line 84)
- `CatchDepth` (interface_declaration, line 92)
- `createCatchDepth` (lexical_declaration, line 98)
- `depth` (lexical_declaration, line 99)
- `stack` (lexical_declaration, line 112)
- `catches` (lexical_declaration, line 113)
- `onCall` (method_definition, line 115)
- `callee` (lexical_declaration, line 116)
- `entries` (lexical_declaration, line 129)
- `catchEntries` (lexical_declaration, line 138)
- `noFailOpenGuard` (lexical_declaration, line 145)

## Records this file checks

- [Fail Secure](https://banes-lab.com/records/architecture/fail-secure.md)

## Source

```typescript
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" };
```
