Source code for klea_utils.mcp.sensitive

#!/usr/bin/env python3
"""
Sensitive-file matcher for the client-side permission gate (ADR-0007).

The path-boundary check (``path_detect``) only asks about paths *outside* the
permitted roots.  This module is the second round: a curated, best-effort
matcher for files that commonly hold credentials (env files, private keys,
cloud config) so the gate can ask before a tool reads one that is *inside* the
project root.

It is advisory: a renamed secret, a secret inline in source, or a directory
scan (``grep -r``) can slip past a filename matcher.  Operators can extend the
list with the comma-separated ``KLEA_SENSITIVE_PATTERNS`` env var (extra
``fnmatch`` globs on the basename).

See ``devdocs/system/mcp-permissions.md``.

File: klea_utils/mcp/sensitive.py

Copyright 2026 Ankur Sinha
Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
"""

import logging
import os
from fnmatch import fnmatch
from pathlib import Path

logger = logging.getLogger(__name__)

#: Env var with extra comma-separated basename globs to treat as sensitive.
SENSITIVE_ENV_VAR = "KLEA_SENSITIVE_PATTERNS"

#: Exact basenames (lowercased) that commonly hold credentials.
_SENSITIVE_NAMES: frozenset[str] = frozenset(
    {
        ".env",
        ".netrc",
        "_netrc",
        ".git-credentials",
        ".npmrc",
        ".pypirc",
        ".htpasswd",
        ".pgpass",
        ".my.cnf",
        ".envrc",
        ".dockercfg",
        "id_rsa",
        "id_dsa",
        "id_ecdsa",
        "id_ed25519",
        "credentials",
        "credentials.json",
        "kubeconfig",
        "secrets.yaml",
        "secrets.yml",
        "secrets.json",
        "secrets.toml",
    }
)

#: File suffixes (lowercased) that commonly hold keys/certs.
_SENSITIVE_SUFFIXES: frozenset[str] = frozenset(
    {".pem", ".key", ".p12", ".pfx", ".jks", ".keystore", ".ppk", ".tfvars"}
)

#: Directory names (lowercased) anywhere in the path that hold credentials.
_SENSITIVE_DIRS: frozenset[str] = frozenset({".ssh", ".aws", ".gnupg", ".kube"})

#: Basename globs for families of sensitive files.
_SENSITIVE_GLOBS: tuple[str, ...] = (
    ".env.*",
    "*.env",
    "secrets.*",
    "service-account*.json",
    "*credentials.json",
)


def _extra_patterns() -> tuple[str, ...]:
    """Return the extra globs from :data:`SENSITIVE_ENV_VAR` (lowercased)."""
    raw = os.environ.get(SENSITIVE_ENV_VAR, "")
    return tuple(part.strip().lower() for part in raw.split(",") if part.strip())


[docs] def is_sensitive(path: str | os.PathLike) -> bool: """Return whether *path* is a well-known credential-bearing file. Matches on the basename (exact names and globs), the suffix, and any directory in the path (:data:`_SENSITIVE_DIRS`), plus the operator's extra patterns from :data:`SENSITIVE_ENV_VAR`. :param path: Path to test (need not exist). :returns: ``True`` when the path looks sensitive. """ the_path = Path(path) name = the_path.name.lower() sensitive = ( name in _SENSITIVE_NAMES or the_path.suffix.lower() in _SENSITIVE_SUFFIXES or any(part.lower() in _SENSITIVE_DIRS for part in the_path.parts) or any( fnmatch(name, pattern) for pattern in (*_SENSITIVE_GLOBS, *_extra_patterns()) ) ) logger.debug(f"Sensitive check\n{the_path = }\n{sensitive = }") return sensitive