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;