Skip to content

Env credentials

env_credentials

EnvCredentialValueRead dataclass

A credential environment read whose value is consumed by the module.

Source code in packages/axm-audit/src/axm_audit/core/rules/practices/env_credentials.py
Python
@dataclass(frozen=True, slots=True)
class EnvCredentialValueRead:
    """A credential environment read whose value is consumed by the module."""

    lineno: int
    env_var: str
    module_path: str

EnvCredentialsRule

Bases: ProjectRule

Detect direct reads of credential values from the environment.

Source code in packages/axm-audit/src/axm_audit/core/rules/practices/env_credentials.py
Python
@register_rule("practices")
class EnvCredentialsRule(ProjectRule):
    """Detect direct reads of credential values from the environment."""

    @property
    def rule_id(self) -> str:
        """Unique identifier for this rule."""
        return "PRACTICE_ENV_CREDENTIAL_READ"

    def check(self, project_path: Path) -> CheckResult:
        """Report credential environment reads used as values in source code."""
        early = self.check_src(project_path)
        if early is not None:
            return early

        violations: list[dict[str, str | int]] = []
        for src_dir in iter_src_dirs(project_path):
            self._collect_violations(src_dir, violations)

        count = len(violations)
        passed = count == 0
        text_lines = [
            f"• {violation['file']}:{violation['line']}: "
            f"direct credential environment read ({violation['env_var']})"
            for violation in violations
        ]
        return CheckResult(
            rule_id=self.rule_id,
            passed=passed,
            message=f"{count} credential environment read(s) found",
            severity=Severity.WARNING if not passed else Severity.INFO,
            score=max(0, 100 - count * 15),
            details={"violations": violations},
            text="\n".join(text_lines) if text_lines else None,
            fix_hint=(
                "Resolve credentials through the axm-vault credential catalogue "
                "and its axm.credentials entry-point group"
            )
            if not passed
            else None,
        )

    @staticmethod
    def _collect_violations(
        src_dir: Path,
        violations: list[dict[str, str | int]],
    ) -> None:
        """Collect direct credential reads from one source root."""
        modules: list[tuple[Path, str, ast.AST]] = []
        for path in get_python_files(src_dir):
            relative_path = path.relative_to(src_dir)
            if _is_test_module(relative_path):
                continue
            module_path = ".".join(relative_path.with_suffix("").parts)
            if is_credential_layer_module(module_path):
                continue
            parsed_tree = parse_file_safe(path)
            if parsed_tree is not None:
                modules.append((relative_path, module_path, parsed_tree))

        trees = [module_tree for _, _, module_tree in modules]
        class_attribute_names = _class_attribute_env_names(trees)
        class_attribute_names_by_class = _class_scoped_attribute_env_names(trees)
        for relative_path, module_path, module_tree in modules:
            violations.extend(
                {
                    "file": relative_path.as_posix(),
                    "line": read.lineno,
                    "env_var": read.env_var,
                }
                for read in find_env_credential_value_reads(
                    module_tree,
                    module_path=module_path,
                    class_attribute_names=class_attribute_names,
                    class_attribute_names_by_class=class_attribute_names_by_class,
                )
            )
rule_id property

Unique identifier for this rule.

check(project_path)

Report credential environment reads used as values in source code.

Source code in packages/axm-audit/src/axm_audit/core/rules/practices/env_credentials.py
Python
def check(self, project_path: Path) -> CheckResult:
    """Report credential environment reads used as values in source code."""
    early = self.check_src(project_path)
    if early is not None:
        return early

    violations: list[dict[str, str | int]] = []
    for src_dir in iter_src_dirs(project_path):
        self._collect_violations(src_dir, violations)

    count = len(violations)
    passed = count == 0
    text_lines = [
        f"• {violation['file']}:{violation['line']}: "
        f"direct credential environment read ({violation['env_var']})"
        for violation in violations
    ]
    return CheckResult(
        rule_id=self.rule_id,
        passed=passed,
        message=f"{count} credential environment read(s) found",
        severity=Severity.WARNING if not passed else Severity.INFO,
        score=max(0, 100 - count * 15),
        details={"violations": violations},
        text="\n".join(text_lines) if text_lines else None,
        fix_hint=(
            "Resolve credentials through the axm-vault credential catalogue "
            "and its axm.credentials entry-point group"
        )
        if not passed
        else None,
    )

find_env_credential_value_reads(tree, *, module_path, class_attribute_names=None, class_attribute_names_by_class=None)

Find credential environment reads used as values rather than guards.

Source code in packages/axm-audit/src/axm_audit/core/rules/practices/env_credentials.py
Python
def find_env_credential_value_reads(
    tree: ast.AST,
    *,
    module_path: str,
    class_attribute_names: dict[str, set[str]] | None = None,
    class_attribute_names_by_class: dict[tuple[str, str], set[str]] | None = None,
) -> list[EnvCredentialValueRead]:
    """Find credential environment reads used as values rather than guards."""
    module_constants = _module_constant_env_names(tree)
    parents = _parent_map(tree)
    reads: list[EnvCredentialValueRead] = []
    for node in ast.walk(tree):
        if not isinstance(node, (ast.Call, ast.Subscript)):
            continue
        if _is_boolean_only_read(node, parents):
            continue
        env_vars = _environment_variables(
            node,
            module_constants,
            class_attribute_names or {},
            class_attribute_names_by_class,
            parents,
        )
        reads.extend(
            EnvCredentialValueRead(
                lineno=node.lineno,
                env_var=env_var,
                module_path=module_path,
            )
            for env_var in sorted(env_vars)
            if is_credential_env_var(env_var)
        )
    return sorted(reads, key=lambda read: (read.lineno, read.env_var))

is_credential_env_var(name)

Return whether an environment variable name denotes a credential.

Source code in packages/axm-audit/src/axm_audit/core/rules/practices/env_credentials.py
Python
def is_credential_env_var(name: str) -> bool:
    """Return whether an environment variable name denotes a credential."""
    normalized = name.upper()
    return any(fnmatchcase(normalized, pattern) for pattern in CREDENTIAL_NAME_PATTERNS)

is_credential_layer_module(module_path)

Return whether a dotted path belongs to the credential layer.

Source code in packages/axm-audit/src/axm_audit/core/rules/practices/env_credentials.py
Python
def is_credential_layer_module(module_path: str) -> bool:
    """Return whether a dotted path belongs to the credential layer."""
    return any(
        fnmatchcase(module_path, pattern)
        for pattern in CREDENTIAL_LAYER_MODULE_PATTERNS
    )