Skip to content

Checks API Reference

PolicyCheck

Base class for all validation checks.

from typing import ClassVar
from iam_validator.core.check_registry import PolicyCheck, CheckConfig
from iam_validator.core.aws_service import AWSServiceFetcher
from iam_validator.core.models import Statement, IAMPolicy, ValidationIssue


class PolicyCheck:
    """Base class for validation checks."""

    check_id: ClassVar[str]           # Unique identifier
    description: ClassVar[str]        # What the check does
    default_severity: ClassVar[str]   # Default severity level ("warning" when omitted)

    # Check ids this check's findings supersede, when matches() is also True
    supersedes: ClassVar[frozenset[str]] = frozenset()

    # PolicyType values this check is meaningful for; None (default) means all
    applies_to_policy_types: ClassVar[frozenset[str] | None] = None

    async def execute(
        self,
        statement: Statement,
        statement_idx: int,
        fetcher: AWSServiceFetcher,
        config: CheckConfig,
    ) -> list[ValidationIssue]:
        """Execute check on a single statement."""
        ...

    async def execute_policy(
        self,
        policy: IAMPolicy,
        policy_file: str,
        fetcher: AWSServiceFetcher,
        config: CheckConfig,
        **kwargs,
    ) -> list[ValidationIssue]:
        """Execute check on entire policy (optional)."""
        ...

    def get_severity(self, config: CheckConfig) -> str:
        """Get effective severity (config override or default)."""
        ...

    def matches(self, statement: Statement) -> bool:
        """True if this check dominates the statement. Only read when supersedes is set."""
        ...

    def applies_to(self, policy_type: str | None) -> bool:
        """True when the check is meaningful for policy_type; None means all types."""
        ...

applies_to_policy_types is how a check opts out of a policy type it cannot reason about. In an SCP or RCP an Allow declines to restrict rather than granting access, so the grant-shaped checks (full_wildcard, service_wildcard, wildcard_action, wildcard_resource, sensitive_action, action_condition_enforcement) exclude SERVICE_CONTROL_POLICY and RESOURCE_CONTROL_POLICY; principal_validation excludes only RCP, where AWS syntax requires Principal: "*". Registration raises ValueError on a value that is not a PolicyType.

CheckConfig

Configuration passed to checks.

class CheckConfig:
    check_id: str                   # Check identifier
    enabled: bool                   # Whether check is enabled
    severity: str | None            # Severity override
    config: dict                    # Check-specific config
    description: str                # Description override
    root_config: dict               # Full config, for cross-check access
    ignore_patterns: list[dict]     # Patterns whose findings are dropped
    hide_severities: frozenset[str] | None   # Severities removed from the run

root_config is the whole resolved configuration, so a check can ask another check what it is configured to do — see enforced_actions below.

CheckRegistry

Registry for managing checks.

from iam_validator.core.check_registry import CheckRegistry

# Register a check
CheckRegistry.register_check(MyCheck)

# Get all registered checks
checks = CheckRegistry.get_all_checks()

# Execute checks
issues = await registry.execute_checks_parallel(
    statement, idx, fetcher, config
)

enforced_actions

ActionConditionEnforcementCheck.enforced_actions(root_config, policy_file=None) is the supported way for another check to find out which actions action_condition_enforcement will actually report on, so it can suppress its own duplicate finding.

from iam_validator.checks.action_condition_enforcement import ActionConditionEnforcementCheck
from iam_validator.checks.utils.aws_matching import action_matches

covered = ActionConditionEnforcementCheck.enforced_actions(config.root_config)
if any(action_matches(action, pattern) for pattern in covered):
    return []  # action_condition_enforcement reports this one

It returns the action patterns as configured — match them with action_matches, not ==, because a pattern may be a glob (iam:Pass* covers iam:PassRole) and IAM action names are case-insensitive. It returns an empty set when the check is disabled, and it honours merge_strategy, user-supplied action_condition_requirements and ignore_patterns, so a requirement that is configured out of play is not returned.

Pass policy_file when you have it: without it a requirement scoped by an ignore_patterns filepath cannot be evaluated and counts as not enforced, which errs toward a possible duplicate rather than dropping a real finding. Statement-level checks receive no policy_file; policy-level checks get it as the second argument to execute_policy.

See Custom Checks Best Practices for the full call-site guidance.

Creating a Check

from typing import ClassVar

from iam_validator.core.check_registry import PolicyCheck, CheckConfig
from iam_validator.core.aws_service import AWSServiceFetcher
from iam_validator.core.models import Statement, ValidationIssue


class MyCheck(PolicyCheck):
    check_id: ClassVar[str] = "my_check"
    description: ClassVar[str] = "My custom check"
    default_severity: ClassVar[str] = "high"

    async def execute(
        self,
        statement: Statement,
        statement_idx: int,
        fetcher: AWSServiceFetcher,
        config: CheckConfig,
    ) -> list[ValidationIssue]:
        issues = []

        # Your check logic here

        if problem_found:
            issues.append(
                ValidationIssue(
                    severity=self.get_severity(config),
                    statement_index=statement_idx,
                    statement_sid=statement.sid,
                    issue_type="my_issue",
                    message="Problem description",
                    suggestion="How to fix",
                    line_number=statement.line_number,
                )
            )

        return issues