core/quality/eslint/no-hardcoded-secrets.eslint.rule.ts

core/quality/eslint/no-hardcoded-secrets.eslint.rule.ts is a file in GovLab Quality. 168 lines of code and 48 definitions.

import { isAlpha, isDigit, isUpperAlpha } from "@govlab/constants";
import type { Rule } from "eslint";
import { SECRET_LABELS } from "#configuration/strings/rule.strings";
import { govlabMeta } from "#core/factories/eslint.factory";

interface AstNode {
    type: string;
    name?: string;
    value?: unknown;
    object?: AstNode;
    property?: AstNode;
    init?: AstNode | null;
    right?: AstNode;
    parent?: AstNode;
}

const DB_SCHEMES = ["mongodb://", "postgres://", "postgresql://", "mysql://", "redis://", "amqp://"];
const SSH_PREFIXES = ["ssh-rsa ", "ssh-ed25519 ", "ssh-dss "];
const SLACK_PREFIXES = ["xoxb-", "xoxp-"];
const MIN_SECRET_LEN = 10;
const SK_MIN = 20;
const GITHUB_MIN = 36;
const GLPAT_MIN = 20;
const SLACK_MIN = 10;
const AWS_MIN = 16;
const BEARER_MIN = 20;
const SCHEME_LEN = 3;

const isAstNode = (value: unknown): value is AstNode => typeof value === "object" && value !== null && "type" in value;

const asNode = (value: unknown): AstNode | null => (isAstNode(value) ? value : null);

const isAlnum = (ch: string): boolean => isAlpha(ch) || isDigit(ch);

interface TailMatcher {
    minRest: number;
    pred: (ch: string) => boolean;
}

const alnumOrDash = (ch: string): boolean => isAlnum(ch) || ch === "-";

const upperOrDigit = (ch: string): boolean => isDigit(ch) || isUpperAlpha(ch);

const tailMatches = (value: string, prefix: string, matcher: TailMatcher): boolean => {
    if (!value.startsWith(prefix)) {
        return false;
    }
    const rest = value.slice(prefix.length);
    if (rest.length < matcher.minRest) {
        return false;
    }
    for (const ch of rest) {
        if (!matcher.pred(ch)) {
            return false;
        }
    }
    return true;
};

const credBoundary = (value: string, authStart: number): number => {
    for (let i = authStart; i < value.length; i += 1) {
        const ch = value[i];
        if (ch === "/" || ch === "?" || ch === "#") {
            return i;
        }
    }
    return value.length;
};

const authorityHasCreds = (value: string, authStart: number): boolean => {
    const authority = value.slice(authStart, credBoundary(value, authStart));
    const at = authority.indexOf("@");
    if (at === -1) {
        return false;
    }
    const colon = authority.indexOf(":");
    return colon !== -1 && colon < at;
};

const hasEmbeddedCreds = (value: string): boolean => {
    let from = value.indexOf("://");
    while (from >= 1) {
        if (authorityHasCreds(value, from + SCHEME_LEN)) {
            return true;
        }
        from = value.indexOf("://", from + SCHEME_LEN);
    }
    return false;
};

const PREFIX_TOKENS: { label: string; matcher: TailMatcher; prefixes: string[] }[] = [
    { label: SECRET_LABELS.openai, matcher: { minRest: SK_MIN, pred: isAlnum }, prefixes: ["sk-"] },
    { label: SECRET_LABELS.github, matcher: { minRest: GITHUB_MIN, pred: isAlnum }, prefixes: ["ghp_", "gho_"] },
    { label: SECRET_LABELS.gitlab, matcher: { minRest: GLPAT_MIN, pred: alnumOrDash }, prefixes: ["glpat-"] },
    { label: SECRET_LABELS.slack, matcher: { minRest: SLACK_MIN, pred: alnumOrDash }, prefixes: SLACK_PREFIXES },
    { label: SECRET_LABELS.awsKey, matcher: { minRest: AWS_MIN, pred: upperOrDigit }, prefixes: ["AKIA"] },
];

const isPrefixedToken = (value: string): string | null => {
    const hit = PREFIX_TOKENS.find((token) =>
        token.prefixes.some((prefix) => tailMatches(value, prefix, token.matcher)),
    );
    return hit ? hit.label : null;
};

const SECRET_CHECKS: { label: string; test: (value: string) => boolean }[] = [
    { label: SECRET_LABELS.privateKey, test: (value) => value.includes("-----BEGIN") && value.includes("PRIVATE KEY") },
    { label: SECRET_LABELS.database, test: (value) => DB_SCHEMES.some((scheme) => value.startsWith(scheme)) },
    { label: SECRET_LABELS.sshKey, test: (value) => SSH_PREFIXES.some((prefix) => value.startsWith(prefix)) },
    { label: SECRET_LABELS.bearer, test: (value) => value.startsWith("Bearer ") && value.length > BEARER_MIN },
    { label: SECRET_LABELS.credentialedUrl, test: hasEmbeddedCreds },
];

const matchSecretPattern = (value: string): string | null => {
    if (value.length < MIN_SECRET_LEN) {
        return null;
    }
    const prefixed = isPrefixedToken(value);
    if (prefixed !== null) {
        return prefixed;
    }
    const hit = SECRET_CHECKS.find((check) => check.test(value));
    return hit ? hit.label : null;
};

const secretLabel = (value: unknown): string | null => {
    const node = asNode(value);
    if (node?.type !== "Literal" || typeof node.value !== "string") {
        return null;
    }
    return matchSecretPattern(node.value);
};

const isTestFile = (filename: string): boolean => filename.split("\\").join("/").includes("/tests/");

const reportSecret = function reportSecret(context: Rule.RuleContext, node: Rule.Node, valueField: unknown): void {
    const label = secretLabel(valueField);
    if (label !== null) {
        context.report({ data: { label }, messageId: "secretValueDetected", node });
    }
};

const onAssignment = function onAssignment(context: Rule.RuleContext, node: Rule.Node): void {
    if (node.type === "AssignmentExpression") {
        reportSecret(context, node, node.right);
    }
};

const onProperty = function onProperty(context: Rule.RuleContext, node: Rule.Node): void {
    if (node.type === "Property" && node.parent.type === "ObjectExpression") {
        reportSecret(context, node, node.value);
    }
};

const onVariableDeclarator = function onVariableDeclarator(context: Rule.RuleContext, node: Rule.Node): void {
    if (node.type === "VariableDeclarator") {
        reportSecret(context, node, node.init);
    }
};

export default {
    create(context: Rule.RuleContext): Rule.RuleListener {
        if (isTestFile(context.filename)) {
            return {};
        }
        const assignment = (node: Rule.Node): void => {
            onAssignment(context, node);
        };
        const property = (node: Rule.Node): void => {
            onProperty(context, node);
        };
        const variable = (node: Rule.Node): void => {
            onVariableDeclarator(context, node);
        };
        const handlers: [string, (node: Rule.Node) => void][] = [
            ["AssignmentExpression", assignment],
            ["Property", property],
            ["VariableDeclarator", variable],
        ];
        return Object.fromEntries(handlers);
    },
    meta: govlabMeta({
        canonical: ["hardcoded-secret", "credentials"],
        description: "Disallow hardcoded secrets, API keys, tokens, private keys, and credential URLs",
        messages: {
            secretValueDetected:
                "Hardcoded {{label}} detected. Move it to .env and reference it via the config layer / process.env — never commit a live credential.",
        },
        ruleId: "env_only_secrets",
    }),
} satisfies Rule.RuleModule;