# Copyright (c) 2026 MergeGuide, Inc. All rights reserved.
# SPDX-License-Identifier: Proprietary
"""Infrastructure as Code (IaC) Security Scanner.

Scans IaC configuration files for security misconfigurations:
- Terraform (.tf, .tf.json)
- CloudFormation (template.yaml, template.json)
- Kubernetes (*.yaml, *.yml with apiVersion)
- Helm (Chart.yaml, values.yaml, templates/)
"""

from __future__ import annotations

import logging
import re
from dataclasses import dataclass, field
from datetime import UTC, datetime
from enum import Enum
from pathlib import Path
from typing import Any

logger = logging.getLogger(__name__)


class IaCPlatform(Enum):
    """Supported IaC platforms."""

    TERRAFORM = "terraform"
    CLOUDFORMATION = "cloudformation"
    KUBERNETES = "kubernetes"
    HELM = "helm"
    ANSIBLE = "ansible"
    PULUMI = "pulumi"
    UNKNOWN = "unknown"


class IaCIssueSeverity(Enum):
    """Security issue severity levels."""

    CRITICAL = "critical"
    HIGH = "high"
    MEDIUM = "medium"
    LOW = "low"
    INFO = "info"


class IaCIssueCategory(Enum):
    """Security issue categories for IaC."""

    SECRETS = "secrets"
    ENCRYPTION = "encryption"
    ACCESS_CONTROL = "access_control"
    NETWORK = "network"
    LOGGING = "logging"
    COMPLIANCE = "compliance"
    CONFIGURATION = "configuration"
    CONTAINER = "container"


@dataclass
class IaCSecurityRule:
    """An IaC security rule definition."""

    id: str
    name: str
    description: str
    pattern: str
    severity: IaCIssueSeverity
    category: IaCIssueCategory
    platforms: list[IaCPlatform]
    remediation: str
    flags: str = ""  # "i" for case-insensitive, "m" for multiline

    def __post_init__(self) -> None:
        """Compile the regex pattern."""
        regex_flags = 0
        if "i" in self.flags:
            regex_flags |= re.IGNORECASE
        if "m" in self.flags:
            regex_flags |= re.MULTILINE
        if "s" in self.flags:
            regex_flags |= re.DOTALL
        self._compiled = re.compile(self.pattern, regex_flags)

    def check(self, content: str) -> list[dict]:
        """Check content against the rule.

        Returns list of matches with line numbers.
        """
        matches = []
        lines = content.split("\n")

        for line_no, line in enumerate(lines, start=1):
            for match in self._compiled.finditer(line):
                matches.append(
                    {
                        "line_no": line_no,
                        "column_start": match.start() + 1,
                        "column_end": match.end() + 1,
                        "matched_text": match.group(),
                        "line_content": line.strip(),
                    }
                )

        return matches


# =============================================================================
# IaC Security Rules
# =============================================================================

IAC_SECURITY_RULES: list[IaCSecurityRule] = [
    # -------------------------------------------------------------------------
    # Terraform - Secrets & Encryption
    # -------------------------------------------------------------------------
    IaCSecurityRule(
        id="iac-tf-hardcoded-secret",
        name="Hardcoded Secret in Terraform",
        description="Secret or password appears to be hardcoded in Terraform",
        pattern=r'(?i)(password|secret|api_key|access_key)\s*=\s*"[^"$]{8,}"',
        severity=IaCIssueSeverity.CRITICAL,
        category=IaCIssueCategory.SECRETS,
        platforms=[IaCPlatform.TERRAFORM],
        remediation="Use variables with sensitive=true or secrets manager",
        flags="i",
    ),
    IaCSecurityRule(
        id="iac-tf-unencrypted-s3",
        name="Unencrypted S3 Bucket",
        description="S3 bucket without server-side encryption",
        pattern=r'resource\s+"aws_s3_bucket"\s+"[^"]+"\s*\{(?:(?!server_side_encryption).)*\}',
        severity=IaCIssueSeverity.HIGH,
        category=IaCIssueCategory.ENCRYPTION,
        platforms=[IaCPlatform.TERRAFORM],
        remediation="Add aws_s3_bucket_server_side_encryption_configuration resource",
        flags="s",
    ),
    IaCSecurityRule(
        id="iac-tf-unencrypted-ebs",
        name="Unencrypted EBS Volume",
        description="EBS volume without encryption enabled",
        pattern=r'resource\s+"aws_ebs_volume".*encrypted\s*=\s*false',
        severity=IaCIssueSeverity.HIGH,
        category=IaCIssueCategory.ENCRYPTION,
        platforms=[IaCPlatform.TERRAFORM],
        remediation="Set encrypted = true on aws_ebs_volume",
        flags="is",
    ),
    IaCSecurityRule(
        id="iac-tf-unencrypted-rds",
        name="Unencrypted RDS Instance",
        description="RDS instance without storage encryption",
        pattern=r'resource\s+"aws_db_instance".*storage_encrypted\s*=\s*false',
        severity=IaCIssueSeverity.HIGH,
        category=IaCIssueCategory.ENCRYPTION,
        platforms=[IaCPlatform.TERRAFORM],
        remediation="Set storage_encrypted = true on aws_db_instance",
        flags="is",
    ),
    # -------------------------------------------------------------------------
    # Terraform - Access Control
    # -------------------------------------------------------------------------
    IaCSecurityRule(
        id="iac-tf-public-s3",
        name="Public S3 Bucket ACL",
        description="S3 bucket with public ACL",
        pattern=r'acl\s*=\s*"(public-read|public-read-write)"',
        severity=IaCIssueSeverity.CRITICAL,
        category=IaCIssueCategory.ACCESS_CONTROL,
        platforms=[IaCPlatform.TERRAFORM],
        remediation="Use acl = 'private' and configure bucket policies instead",
    ),
    IaCSecurityRule(
        id="iac-tf-open-security-group",
        name="Open Security Group (0.0.0.0/0)",
        description="Security group rule allows traffic from anywhere",
        pattern=r'cidr_blocks\s*=\s*\[\s*"0\.0\.0\.0/0"\s*\]',
        severity=IaCIssueSeverity.HIGH,
        category=IaCIssueCategory.NETWORK,
        platforms=[IaCPlatform.TERRAFORM],
        remediation="Restrict CIDR blocks to specific IP ranges",
    ),
    IaCSecurityRule(
        id="iac-tf-ssh-open",
        name="SSH Open to Internet",
        description="SSH port (22) open to 0.0.0.0/0",
        pattern=r'(from_port|to_port)\s*=\s*22.*cidr_blocks\s*=\s*\[\s*"0\.0\.0\.0/0"',
        severity=IaCIssueSeverity.CRITICAL,
        category=IaCIssueCategory.NETWORK,
        platforms=[IaCPlatform.TERRAFORM],
        remediation="Restrict SSH access to specific IP ranges or use bastion hosts",
        flags="is",
    ),
    IaCSecurityRule(
        id="iac-tf-rdp-open",
        name="RDP Open to Internet",
        description="RDP port (3389) open to 0.0.0.0/0",
        pattern=r'(from_port|to_port)\s*=\s*3389.*cidr_blocks\s*=\s*\[\s*"0\.0\.0\.0/0"',
        severity=IaCIssueSeverity.CRITICAL,
        category=IaCIssueCategory.NETWORK,
        platforms=[IaCPlatform.TERRAFORM],
        remediation="Restrict RDP access to specific IP ranges or use VPN",
        flags="is",
    ),
    IaCSecurityRule(
        id="iac-tf-wildcard-iam",
        name="Wildcard IAM Action",
        description="IAM policy with wildcard (*) action",
        pattern=r'"Action"\s*:\s*"\*"',
        severity=IaCIssueSeverity.HIGH,
        category=IaCIssueCategory.ACCESS_CONTROL,
        platforms=[IaCPlatform.TERRAFORM, IaCPlatform.CLOUDFORMATION],
        remediation="Specify explicit actions instead of wildcard",
    ),
    IaCSecurityRule(
        id="iac-tf-wildcard-resource",
        name="Wildcard IAM Resource",
        description="IAM policy with wildcard (*) resource",
        pattern=r'"Resource"\s*:\s*"\*"',
        severity=IaCIssueSeverity.MEDIUM,
        category=IaCIssueCategory.ACCESS_CONTROL,
        platforms=[IaCPlatform.TERRAFORM, IaCPlatform.CLOUDFORMATION],
        remediation="Specify explicit resource ARNs instead of wildcard",
    ),
    # -------------------------------------------------------------------------
    # Terraform - Logging & Monitoring
    # -------------------------------------------------------------------------
    IaCSecurityRule(
        id="iac-tf-no-cloudtrail",
        name="CloudTrail Not Enabled",
        description="S3 bucket without CloudTrail logging",
        pattern=r'resource\s+"aws_s3_bucket"\s+"[^"]+"\s*\{(?:(?!logging).)*\}',
        severity=IaCIssueSeverity.MEDIUM,
        category=IaCIssueCategory.LOGGING,
        platforms=[IaCPlatform.TERRAFORM],
        remediation="Enable S3 bucket logging with aws_s3_bucket_logging",
        flags="s",
    ),
    IaCSecurityRule(
        id="iac-tf-no-vpc-flow-logs",
        name="VPC Without Flow Logs",
        description="VPC created without flow logs enabled",
        pattern=r'resource\s+"aws_vpc"\s+"[^"]+"\s*\{(?:(?!flow_log).)*\}',
        severity=IaCIssueSeverity.MEDIUM,
        category=IaCIssueCategory.LOGGING,
        platforms=[IaCPlatform.TERRAFORM],
        remediation="Enable VPC flow logs with aws_flow_log resource",
        flags="s",
    ),
    # -------------------------------------------------------------------------
    # CloudFormation - Secrets & Encryption
    # -------------------------------------------------------------------------
    IaCSecurityRule(
        id="iac-cfn-hardcoded-secret",
        name="Hardcoded Secret in CloudFormation",
        description="Secret appears hardcoded in CloudFormation template",
        pattern=r'(?i)(Password|SecretKey|ApiKey):\s*["\']?[^!${\s][^"\'\s]{8,}',
        severity=IaCIssueSeverity.CRITICAL,
        category=IaCIssueCategory.SECRETS,
        platforms=[IaCPlatform.CLOUDFORMATION],
        remediation="Use !Ref to parameters with NoEcho: true or Secrets Manager",
        flags="i",
    ),
    IaCSecurityRule(
        id="iac-cfn-noecho-missing",
        name="Sensitive Parameter Without NoEcho",
        description="Password parameter without NoEcho protection",
        pattern=r"(?i)(Password|Secret|Key):\s*\n\s*Type:\s*String(?:(?!NoEcho).)*$",
        severity=IaCIssueSeverity.HIGH,
        category=IaCIssueCategory.SECRETS,
        platforms=[IaCPlatform.CLOUDFORMATION],
        remediation="Add NoEcho: true to sensitive parameters",
        flags="ims",
    ),
    # -------------------------------------------------------------------------
    # Kubernetes - Container Security
    # -------------------------------------------------------------------------
    IaCSecurityRule(
        id="iac-k8s-privileged",
        name="Privileged Container",
        description="Container running in privileged mode",
        pattern=r"privileged:\s*true",
        severity=IaCIssueSeverity.CRITICAL,
        category=IaCIssueCategory.CONTAINER,
        platforms=[IaCPlatform.KUBERNETES, IaCPlatform.HELM],
        remediation="Set privileged: false and use specific capabilities instead",
    ),
    IaCSecurityRule(
        id="iac-k8s-root-user",
        name="Container Running as Root",
        description="Container configured to run as root user",
        pattern=r"runAsUser:\s*0",
        severity=IaCIssueSeverity.HIGH,
        category=IaCIssueCategory.CONTAINER,
        platforms=[IaCPlatform.KUBERNETES, IaCPlatform.HELM],
        remediation="Set runAsUser to a non-root UID (e.g., 1000)",
    ),
    IaCSecurityRule(
        id="iac-k8s-run-as-non-root-false",
        name="runAsNonRoot Not Enforced",
        description="Container allows running as root",
        pattern=r"runAsNonRoot:\s*false",
        severity=IaCIssueSeverity.HIGH,
        category=IaCIssueCategory.CONTAINER,
        platforms=[IaCPlatform.KUBERNETES, IaCPlatform.HELM],
        remediation="Set runAsNonRoot: true to enforce non-root execution",
    ),
    IaCSecurityRule(
        id="iac-k8s-allow-privilege-escalation",
        name="Privilege Escalation Allowed",
        description="Container allows privilege escalation",
        pattern=r"allowPrivilegeEscalation:\s*true",
        severity=IaCIssueSeverity.HIGH,
        category=IaCIssueCategory.CONTAINER,
        platforms=[IaCPlatform.KUBERNETES, IaCPlatform.HELM],
        remediation="Set allowPrivilegeEscalation: false",
    ),
    IaCSecurityRule(
        id="iac-k8s-host-network",
        name="Host Network Enabled",
        description="Pod using host network namespace",
        pattern=r"hostNetwork:\s*true",
        severity=IaCIssueSeverity.HIGH,
        category=IaCIssueCategory.NETWORK,
        platforms=[IaCPlatform.KUBERNETES, IaCPlatform.HELM],
        remediation="Set hostNetwork: false unless absolutely required",
    ),
    IaCSecurityRule(
        id="iac-k8s-host-pid",
        name="Host PID Namespace",
        description="Pod using host PID namespace",
        pattern=r"hostPID:\s*true",
        severity=IaCIssueSeverity.HIGH,
        category=IaCIssueCategory.CONTAINER,
        platforms=[IaCPlatform.KUBERNETES, IaCPlatform.HELM],
        remediation="Set hostPID: false to isolate process namespace",
    ),
    IaCSecurityRule(
        id="iac-k8s-host-path",
        name="HostPath Volume Mount",
        description="Volume mount from host filesystem",
        pattern=r"hostPath:\s*\n\s*path:",
        severity=IaCIssueSeverity.MEDIUM,
        category=IaCIssueCategory.CONTAINER,
        platforms=[IaCPlatform.KUBERNETES, IaCPlatform.HELM],
        remediation="Use PersistentVolumes instead of hostPath",
        flags="m",
    ),
    IaCSecurityRule(
        id="iac-k8s-latest-tag",
        name="Using Latest Image Tag",
        description="Container image using :latest tag",
        pattern=r"image:\s*[^\s]+:latest",
        severity=IaCIssueSeverity.MEDIUM,
        category=IaCIssueCategory.CONTAINER,
        platforms=[IaCPlatform.KUBERNETES, IaCPlatform.HELM],
        remediation="Use specific image tags or SHA digests for reproducibility",
    ),
    IaCSecurityRule(
        id="iac-k8s-no-image-tag",
        name="Missing Image Tag",
        description="Container image without explicit tag",
        pattern=r'image:\s*"?[a-zA-Z0-9._/-]+(?!:[^\s"]+)"?\s*$',
        severity=IaCIssueSeverity.MEDIUM,
        category=IaCIssueCategory.CONTAINER,
        platforms=[IaCPlatform.KUBERNETES, IaCPlatform.HELM],
        remediation="Always specify image tags explicitly",
        flags="m",
    ),
    # -------------------------------------------------------------------------
    # Kubernetes - Network & Access
    # -------------------------------------------------------------------------
    IaCSecurityRule(
        id="iac-k8s-service-account-token",
        name="Auto-mounted Service Account Token",
        description="Service account token auto-mounted",
        pattern=r"automountServiceAccountToken:\s*true",
        severity=IaCIssueSeverity.MEDIUM,
        category=IaCIssueCategory.ACCESS_CONTROL,
        platforms=[IaCPlatform.KUBERNETES, IaCPlatform.HELM],
        remediation="Set automountServiceAccountToken: false if not needed",
    ),
    IaCSecurityRule(
        id="iac-k8s-default-namespace",
        name="Deployment in Default Namespace",
        description="Resource deployed to default namespace",
        pattern=r'namespace:\s*["\'"]?default["\'"]?',
        severity=IaCIssueSeverity.LOW,
        category=IaCIssueCategory.CONFIGURATION,
        platforms=[IaCPlatform.KUBERNETES, IaCPlatform.HELM],
        remediation="Deploy to a dedicated namespace for better isolation",
    ),
    # -------------------------------------------------------------------------
    # Kubernetes - Secrets
    # -------------------------------------------------------------------------
    IaCSecurityRule(
        id="iac-k8s-secret-env",
        name="Secret in Environment Variable",
        description="Secret passed directly as environment variable value",
        pattern=r"env:\s*\n\s*-\s*name:\s*\w*(SECRET|PASSWORD|KEY|TOKEN)\w*\s*\n\s*value:",
        severity=IaCIssueSeverity.HIGH,
        category=IaCIssueCategory.SECRETS,
        platforms=[IaCPlatform.KUBERNETES, IaCPlatform.HELM],
        remediation="Use secretKeyRef to reference Kubernetes secrets",
        flags="im",
    ),
    # -------------------------------------------------------------------------
    # Helm - Configuration
    # -------------------------------------------------------------------------
    IaCSecurityRule(
        id="iac-helm-deprecated-api",
        name="Deprecated Kubernetes API Version",
        description="Using deprecated Kubernetes API version",
        pattern=r"apiVersion:\s*(extensions/v1beta1|apps/v1beta1|apps/v1beta2)",
        severity=IaCIssueSeverity.MEDIUM,
        category=IaCIssueCategory.CONFIGURATION,
        platforms=[IaCPlatform.HELM, IaCPlatform.KUBERNETES],
        remediation="Update to current stable API versions (e.g., apps/v1)",
    ),
    # -------------------------------------------------------------------------
    # General IaC - Compliance
    # -------------------------------------------------------------------------
    IaCSecurityRule(
        id="iac-missing-tags",
        name="Missing Resource Tags",
        description="Resource without required tags (Owner, Environment)",
        pattern=r'resource\s+"aws_[^"]+"\s+"[^"]+"\s*\{(?:(?!tags\s*=).)*\}',
        severity=IaCIssueSeverity.LOW,
        category=IaCIssueCategory.COMPLIANCE,
        platforms=[IaCPlatform.TERRAFORM],
        remediation="Add tags block with Owner, Environment, and Project tags",
        flags="s",
    ),
    # -------------------------------------------------------------------------
    # Container Registry Security (Build & Deploy)
    # -------------------------------------------------------------------------
    IaCSecurityRule(
        id="iac-tf-ecr-no-scan",
        name="ECR Repository Without Image Scanning",
        description="ECR repository without scan_on_push enabled",
        pattern=r'resource\s+"aws_ecr_repository"\s+"[^"]+"\s*\{',
        severity=IaCIssueSeverity.HIGH,
        category=IaCIssueCategory.CONTAINER,
        platforms=[IaCPlatform.TERRAFORM],
        remediation="Add image_scanning_configuration { scan_on_push = true }",
    ),
    IaCSecurityRule(
        id="iac-tf-ecr-mutable-tags",
        name="ECR Mutable Image Tags",
        description="ECR repository allows mutable image tags, risking tag overwrite attacks",
        pattern=r'image_tag_mutability\s*=\s*"MUTABLE"',
        severity=IaCIssueSeverity.MEDIUM,
        category=IaCIssueCategory.CONTAINER,
        platforms=[IaCPlatform.TERRAFORM],
        remediation='Set image_tag_mutability = "IMMUTABLE" to prevent tag overwrites',
    ),
    IaCSecurityRule(
        id="iac-tf-ecr-no-lifecycle",
        name="ECR Repository Without Lifecycle Policy",
        description="ECR repository without lifecycle policy allows unbounded image accumulation",
        pattern=r'resource\s+"aws_ecr_repository"\s+"[^"]+"\s*\{(?:(?!lifecycle_policy).)*\}',
        severity=IaCIssueSeverity.LOW,
        category=IaCIssueCategory.CONFIGURATION,
        platforms=[IaCPlatform.TERRAFORM],
        remediation="Add aws_ecr_lifecycle_policy to manage image retention",
        flags="s",
    ),
    IaCSecurityRule(
        id="iac-tf-ecr-public",
        name="Public ECR Repository",
        description="Public ECR repository exposes container images to the internet",
        pattern=r'resource\s+"aws_ecrpublic_repository"\s+"[^"]+"\s*\{',
        severity=IaCIssueSeverity.MEDIUM,
        category=IaCIssueCategory.ACCESS_CONTROL,
        platforms=[IaCPlatform.TERRAFORM],
        remediation="Use private aws_ecr_repository unless public access is intentional",
    ),
    IaCSecurityRule(
        id="iac-k8s-image-pull-policy",
        name="ImagePullPolicy Always Without Digest",
        description="imagePullPolicy: Always without digest pinning may pull unexpected images",
        pattern=r"imagePullPolicy:\s*Always",
        severity=IaCIssueSeverity.LOW,
        category=IaCIssueCategory.CONTAINER,
        platforms=[IaCPlatform.KUBERNETES, IaCPlatform.HELM],
        remediation="Use imagePullPolicy: IfNotPresent with digest-pinned images",
    ),
    # -------------------------------------------------------------------------
    # Build Environment Security (Build & Deploy)
    # -------------------------------------------------------------------------
    IaCSecurityRule(
        id="iac-tf-codebuild-no-vpc",
        name="CodeBuild Project Without VPC",
        description="CodeBuild project without VPC config runs builds in public network",
        pattern=r'resource\s+"aws_codebuild_project"\s+"[^"]+"\s*\{(?:(?!vpc_config).)*\}',
        severity=IaCIssueSeverity.MEDIUM,
        category=IaCIssueCategory.NETWORK,
        platforms=[IaCPlatform.TERRAFORM],
        remediation="Add vpc_config block to isolate build environment",
        flags="s",
    ),
    IaCSecurityRule(
        id="iac-tf-codebuild-privileged",
        name="CodeBuild Privileged Mode",
        description="CodeBuild project with privileged_mode enables container escape risk",
        pattern=r"privileged_mode\s*=\s*true",
        severity=IaCIssueSeverity.HIGH,
        category=IaCIssueCategory.CONTAINER,
        platforms=[IaCPlatform.TERRAFORM],
        remediation="Set privileged_mode = false unless Docker-in-Docker is required",
    ),
    IaCSecurityRule(
        id="iac-tf-codebuild-plaintext-env",
        name="CodeBuild Plaintext Sensitive Environment Variable",
        description="CodeBuild environment variable uses PLAINTEXT type for sensitive value",
        pattern=r'type\s*=\s*"PLAINTEXT".*(?:PASSWORD|SECRET|KEY|TOKEN|CREDENTIAL)',
        severity=IaCIssueSeverity.CRITICAL,
        category=IaCIssueCategory.SECRETS,
        platforms=[IaCPlatform.TERRAFORM],
        remediation='Use type = "PARAMETER_STORE" or "SECRETS_MANAGER" for sensitive values',
        flags="i",
    ),
    IaCSecurityRule(
        id="iac-tf-runner-open-sg",
        name="Build Runner Open Security Group",
        description="Build runner security group allows unrestricted ingress from 0.0.0.0/0",
        pattern=r'(?:build|runner|ci|jenkins).*cidr_blocks\s*=\s*\[\s*"0\.0\.0\.0/0"\s*\]',
        severity=IaCIssueSeverity.HIGH,
        category=IaCIssueCategory.NETWORK,
        platforms=[IaCPlatform.TERRAFORM],
        remediation="Restrict build runner ingress to specific CIDR ranges",
        flags="i",
    ),
    # -------------------------------------------------------------------------
    # Deployment Config Security (Build & Deploy)
    # -------------------------------------------------------------------------
    IaCSecurityRule(
        id="iac-tf-ecs-plaintext-secret",
        name="ECS Task Definition Plaintext Secret",
        description="ECS task definition contains sensitive env var name without valueFrom",
        pattern=r'"name"\s*:\s*"[^"]*(?:PASSWORD|SECRET|KEY|TOKEN|CREDENTIAL)[^"]*"',
        severity=IaCIssueSeverity.CRITICAL,
        category=IaCIssueCategory.SECRETS,
        platforms=[IaCPlatform.TERRAFORM],
        remediation="Use secrets block with valueFrom referencing Secrets Manager or SSM",
        flags="i",
    ),
    IaCSecurityRule(
        id="iac-tf-ecs-root-user",
        name="ECS Task Definition Running as Root",
        description="ECS task definition without user field runs container as root",
        pattern=r'resource\s+"aws_ecs_task_definition"\s+"[^"]+"\s*\{(?:(?!"user").)*\}',
        severity=IaCIssueSeverity.MEDIUM,
        category=IaCIssueCategory.CONTAINER,
        platforms=[IaCPlatform.TERRAFORM],
        remediation='Add "user": "nonroot:nonroot" to container definition',
        flags="s",
    ),
    IaCSecurityRule(
        id="iac-tf-lambda-plaintext-env",
        name="Lambda Plaintext Sensitive Environment Variable",
        description="Lambda function environment variable with sensitive name in plaintext",
        pattern=r'(?:SECRET|PASSWORD|KEY|TOKEN|CREDENTIAL)[A-Z_]*\s*=\s*"[^"$][^"]{3,}"',
        severity=IaCIssueSeverity.CRITICAL,
        category=IaCIssueCategory.SECRETS,
        platforms=[IaCPlatform.TERRAFORM],
        remediation="Reference secrets from SSM Parameter Store or Secrets Manager",
        flags="i",
    ),
    IaCSecurityRule(
        id="iac-tf-lambda-no-vpc",
        name="Lambda Function Without VPC",
        description="Lambda function without vpc_config for sensitive workloads",
        pattern=r'resource\s+"aws_lambda_function"\s+"[^"]+"\s*\{(?:(?!vpc_config).)*\}',
        severity=IaCIssueSeverity.LOW,
        category=IaCIssueCategory.NETWORK,
        platforms=[IaCPlatform.TERRAFORM],
        remediation="Add vpc_config for Lambda functions accessing private resources",
        flags="s",
    ),
    IaCSecurityRule(
        id="iac-cfn-ecs-task-secret",
        name="CloudFormation ECS Task Plaintext Secret",
        description="CloudFormation ECS TaskDefinition with sensitive name in Environment block",
        pattern=r'Name:\s*"?[^"\n]*(?:SECRET|PASSWORD|KEY|TOKEN|CREDENTIAL)[^"\n]*"?',
        severity=IaCIssueSeverity.CRITICAL,
        category=IaCIssueCategory.SECRETS,
        platforms=[IaCPlatform.CLOUDFORMATION],
        remediation="Use Secrets block with ValueFrom referencing Secrets Manager ARN",
        flags="i",
    ),
    # -------------------------------------------------------------------------
    # Build Artifact Storage Security (Build & Deploy)
    # -------------------------------------------------------------------------
    IaCSecurityRule(
        id="iac-tf-artifact-bucket-public",
        name="Public Build Artifact Bucket",
        description="S3 bucket for build artifacts with public ACL exposes build outputs",
        pattern=r'(?:artifact|build|deploy|dist|release).*acl\s*=\s*"(?:public-read|public-read-write)"',
        severity=IaCIssueSeverity.CRITICAL,
        category=IaCIssueCategory.ACCESS_CONTROL,
        platforms=[IaCPlatform.TERRAFORM],
        remediation='Set acl = "private" for build artifact buckets',
        flags="is",
    ),
    IaCSecurityRule(
        id="iac-tf-artifact-bucket-no-encryption",
        name="Build Artifact Bucket Without Encryption",
        description="S3 bucket for build artifacts without server-side encryption",
        pattern=r'resource\s+"aws_s3_bucket"\s+"[^"]*(?:artifact|build|deploy|dist|release)[^"]*"\s*\{(?:(?!server_side_encryption|sse_algorithm).)*\}',
        severity=IaCIssueSeverity.HIGH,
        category=IaCIssueCategory.ENCRYPTION,
        platforms=[IaCPlatform.TERRAFORM],
        remediation="Add aws_s3_bucket_server_side_encryption_configuration with AES256 or aws:kms",
        flags="is",
    ),
    IaCSecurityRule(
        id="iac-tf-artifact-bucket-no-versioning",
        name="Build Artifact Bucket Without Versioning",
        description="S3 bucket for build artifacts without versioning for audit trail",
        pattern=r'resource\s+"aws_s3_bucket"\s+"[^"]*(?:artifact|build|deploy|dist|release)[^"]*"\s*\{(?:(?!versioning).)*\}',
        severity=IaCIssueSeverity.MEDIUM,
        category=IaCIssueCategory.CONFIGURATION,
        platforms=[IaCPlatform.TERRAFORM],
        remediation="Add aws_s3_bucket_versioning with status = Enabled",
        flags="is",
    ),
    # -------------------------------------------------------------------------
    # Terraform - Data Protection & Backup (WS3 DEV-1108)
    # -------------------------------------------------------------------------
    IaCSecurityRule(
        id="iac-tf-rds-no-backup",
        name="RDS Instance Without Backup Retention",
        description="RDS instance without backup_retention_period or with backups disabled (= 0)",
        pattern=r'resource\s+"aws_db_instance"\s+"[^"]+"\s*\{(?:(?!backup_retention_period).)*\}',
        severity=IaCIssueSeverity.HIGH,
        category=IaCIssueCategory.CONFIGURATION,
        platforms=[IaCPlatform.TERRAFORM],
        remediation="Add backup_retention_period (recommended: 7+ days) for disaster recovery",
        flags="s",
    ),
    IaCSecurityRule(
        id="iac-tf-rds-backup-disabled",
        name="RDS Backup Explicitly Disabled",
        description="RDS instance with backup_retention_period = 0 disables automated backups",
        pattern=r"backup_retention_period\s*=\s*0\b",
        severity=IaCIssueSeverity.HIGH,
        category=IaCIssueCategory.CONFIGURATION,
        platforms=[IaCPlatform.TERRAFORM],
        remediation="Set backup_retention_period to at least 7 days",
    ),
    IaCSecurityRule(
        id="iac-tf-cloudtrail-no-s3-bucket",
        name="CloudTrail Without S3 Bucket",
        description="CloudTrail resource without s3_bucket_name — logs are not persisted",
        pattern=r'resource\s+"aws_cloudtrail"\s+"[^"]+"\s*\{(?:(?!s3_bucket_name).)*\}',
        severity=IaCIssueSeverity.HIGH,
        category=IaCIssueCategory.LOGGING,
        platforms=[IaCPlatform.TERRAFORM],
        remediation="Set s3_bucket_name to specify the log storage destination",
        flags="s",
    ),
]


@dataclass
class IaCSecurityFinding:
    """An IaC security finding."""

    rule_id: str
    rule_name: str
    description: str
    file_path: str
    line_no: int
    column_start: int
    column_end: int
    matched_text: str
    line_content: str
    severity: IaCIssueSeverity
    category: IaCIssueCategory
    platform: IaCPlatform
    remediation: str

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary for serialization."""
        return {
            "rule_id": self.rule_id,
            "rule_name": self.rule_name,
            "description": self.description,
            "file_path": self.file_path,
            "line_no": self.line_no,
            "column_start": self.column_start,
            "column_end": self.column_end,
            "matched_text": self.matched_text[:100],  # Truncate for safety
            "line_content": self.line_content[:200],
            "severity": self.severity.value,
            "category": self.category.value,
            "platform": self.platform.value,
            "remediation": self.remediation,
        }


@dataclass
class IaCScanResult:
    """Result of an IaC security scan."""

    scan_id: str
    timestamp: str
    repository: str
    ref: str = ""

    # Counts by severity
    total_issues: int = 0
    critical_count: int = 0
    high_count: int = 0
    medium_count: int = 0
    low_count: int = 0
    info_count: int = 0

    # Counts by category
    secrets_count: int = 0
    encryption_count: int = 0
    access_control_count: int = 0
    network_count: int = 0
    logging_count: int = 0
    compliance_count: int = 0
    configuration_count: int = 0
    container_count: int = 0

    findings: list[IaCSecurityFinding] = field(default_factory=list)
    files_scanned: list[str] = field(default_factory=list)
    platforms_detected: set[IaCPlatform] = field(default_factory=set)
    errors: list[str] = field(default_factory=list)

    @property
    def passed(self) -> bool:
        """Check if scan passed (no critical/high issues)."""
        return self.critical_count == 0 and self.high_count == 0

    @property
    def security_score(self) -> int:
        """Calculate a security score (0-100)."""
        if self.total_issues == 0:
            return 100

        # Weight by severity
        penalty = (
            self.critical_count * 25
            + self.high_count * 15
            + self.medium_count * 5
            + self.low_count * 2
            + self.info_count * 1
        )

        return max(0, 100 - penalty)

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary for serialization."""
        return {
            "scan_id": self.scan_id,
            "timestamp": self.timestamp,
            "repository": self.repository,
            "ref": self.ref,
            "passed": self.passed,
            "security_score": self.security_score,
            "total_issues": self.total_issues,
            "severity_counts": {
                "critical": self.critical_count,
                "high": self.high_count,
                "medium": self.medium_count,
                "low": self.low_count,
                "info": self.info_count,
            },
            "category_counts": {
                "secrets": self.secrets_count,
                "encryption": self.encryption_count,
                "access_control": self.access_control_count,
                "network": self.network_count,
                "logging": self.logging_count,
                "compliance": self.compliance_count,
                "configuration": self.configuration_count,
                "container": self.container_count,
            },
            "platforms_detected": [p.value for p in self.platforms_detected],
            "findings": [f.to_dict() for f in self.findings],
            "files_scanned": self.files_scanned,
            "errors": self.errors,
        }


class IaCScanner:
    """Scans IaC configuration files for security issues."""

    # Map of file patterns to platforms
    FILE_PATTERNS: dict[str, IaCPlatform] = {
        ".tf": IaCPlatform.TERRAFORM,
        ".tf.json": IaCPlatform.TERRAFORM,
        "main.tf": IaCPlatform.TERRAFORM,
        "variables.tf": IaCPlatform.TERRAFORM,
        "outputs.tf": IaCPlatform.TERRAFORM,
        "provider.tf": IaCPlatform.TERRAFORM,
        "template.yaml": IaCPlatform.CLOUDFORMATION,
        "template.yml": IaCPlatform.CLOUDFORMATION,
        "template.json": IaCPlatform.CLOUDFORMATION,
        "cloudformation.yaml": IaCPlatform.CLOUDFORMATION,
        "cloudformation.yml": IaCPlatform.CLOUDFORMATION,
        "Chart.yaml": IaCPlatform.HELM,
        "Chart.yml": IaCPlatform.HELM,
        "values.yaml": IaCPlatform.HELM,
        "values.yml": IaCPlatform.HELM,
        "playbook.yaml": IaCPlatform.ANSIBLE,
        "playbook.yml": IaCPlatform.ANSIBLE,
        "Pulumi.yaml": IaCPlatform.PULUMI,
        "Pulumi.yml": IaCPlatform.PULUMI,
    }

    def __init__(self, rules: list[IaCSecurityRule] | None = None):
        """Initialize scanner.

        Args:
            rules: Custom rules to use. Defaults to built-in rules.
        """
        self._rules = rules or IAC_SECURITY_RULES

    def detect_platform(self, file_path: str, content: str = "") -> IaCPlatform:
        """Detect IaC platform from file path and content.

        Args:
            file_path: Path to the configuration file.
            content: File content for additional detection.

        Returns:
            Detected platform or UNKNOWN.
        """
        path = Path(file_path)
        filename = path.name
        suffix = path.suffix

        # Check exact filename match
        for pattern, platform in self.FILE_PATTERNS.items():
            if filename == pattern:
                return platform

        # Check suffix
        if suffix == ".tf":
            return IaCPlatform.TERRAFORM

        # Check content for Kubernetes
        if suffix in (".yaml", ".yml") and content:
            if "apiVersion:" in content and "kind:" in content:
                # Check if it's Helm template
                if "{{" in content and "}}" in content:
                    return IaCPlatform.HELM
                return IaCPlatform.KUBERNETES

            # Check for CloudFormation
            if "AWSTemplateFormatVersion" in content or "Resources:" in content:
                return IaCPlatform.CLOUDFORMATION

        # Check for Helm directory structure
        if "templates/" in file_path and suffix in (".yaml", ".yml"):
            return IaCPlatform.HELM

        return IaCPlatform.UNKNOWN

    def is_iac_file(self, file_path: str, content: str = "") -> bool:
        """Check if a file is an IaC configuration file.

        Args:
            file_path: Path to check.
            content: Optional file content.

        Returns:
            True if it's an IaC configuration file.
        """
        return self.detect_platform(file_path, content) != IaCPlatform.UNKNOWN

    def scan_file(
        self,
        content: str,
        file_path: str,
        platform: IaCPlatform | None = None,
    ) -> list[IaCSecurityFinding]:
        """Scan a single file for security issues.

        Args:
            content: File content.
            file_path: Path to the file.
            platform: IaC platform (auto-detected if not provided).

        Returns:
            List of security findings.
        """
        # Reject oversized content to prevent resource exhaustion
        if len(content) > 10 * 1024 * 1024:  # 10 MB
            logger.warning("File too large to scan (%d bytes): %s", len(content), file_path)
            return []

        if platform is None:
            platform = self.detect_platform(file_path, content)

        findings = []

        for rule in self._rules:
            # Check if rule applies to this platform
            if platform not in rule.platforms and IaCPlatform.UNKNOWN not in rule.platforms:
                continue

            matches = rule.check(content)

            for match in matches:
                finding = IaCSecurityFinding(
                    rule_id=rule.id,
                    rule_name=rule.name,
                    description=rule.description,
                    file_path=file_path,
                    line_no=match["line_no"],
                    column_start=match["column_start"],
                    column_end=match["column_end"],
                    matched_text=match["matched_text"],
                    line_content=match["line_content"],
                    severity=rule.severity,
                    category=rule.category,
                    platform=platform,
                    remediation=rule.remediation,
                )
                findings.append(finding)

        return findings

    def scan_files(
        self,
        files: dict[str, str],
        repository: str = "",
        ref: str = "",
        scan_id: str | None = None,
    ) -> IaCScanResult:
        """Scan multiple files for security issues.

        Args:
            files: Dictionary mapping file path to content.
            repository: Repository name.
            ref: Git ref being scanned.
            scan_id: Optional scan ID.

        Returns:
            IaCScanResult with all findings.
        """
        if scan_id is None:
            scan_id = self._generate_scan_id()

        result = IaCScanResult(
            scan_id=scan_id,
            timestamp=datetime.now(UTC).isoformat(),
            repository=repository,
            ref=ref,
        )

        for file_path, content in files.items():
            # Check if it's an IaC file
            platform = self.detect_platform(file_path, content)
            if platform == IaCPlatform.UNKNOWN:
                continue

            result.files_scanned.append(file_path)
            result.platforms_detected.add(platform)

            try:
                findings = self.scan_file(content, file_path, platform)

                for finding in findings:
                    result.findings.append(finding)
                    result.total_issues += 1

                    # Update severity counts
                    if finding.severity == IaCIssueSeverity.CRITICAL:
                        result.critical_count += 1
                    elif finding.severity == IaCIssueSeverity.HIGH:
                        result.high_count += 1
                    elif finding.severity == IaCIssueSeverity.MEDIUM:
                        result.medium_count += 1
                    elif finding.severity == IaCIssueSeverity.LOW:
                        result.low_count += 1
                    else:
                        result.info_count += 1

                    # Update category counts
                    if finding.category == IaCIssueCategory.SECRETS:
                        result.secrets_count += 1
                    elif finding.category == IaCIssueCategory.ENCRYPTION:
                        result.encryption_count += 1
                    elif finding.category == IaCIssueCategory.ACCESS_CONTROL:
                        result.access_control_count += 1
                    elif finding.category == IaCIssueCategory.NETWORK:
                        result.network_count += 1
                    elif finding.category == IaCIssueCategory.LOGGING:
                        result.logging_count += 1
                    elif finding.category == IaCIssueCategory.COMPLIANCE:
                        result.compliance_count += 1
                    elif finding.category == IaCIssueCategory.CONFIGURATION:
                        result.configuration_count += 1
                    elif finding.category == IaCIssueCategory.CONTAINER:
                        result.container_count += 1

            except Exception as e:
                logger.warning(f"Error scanning {file_path}: {e}")
                result.errors.append(f"Error scanning {file_path}: {str(e)}")

        # Sort findings by severity
        severity_order = {
            IaCIssueSeverity.CRITICAL: 0,
            IaCIssueSeverity.HIGH: 1,
            IaCIssueSeverity.MEDIUM: 2,
            IaCIssueSeverity.LOW: 3,
            IaCIssueSeverity.INFO: 4,
        }
        result.findings.sort(key=lambda f: severity_order.get(f.severity, 5))

        return result

    def scan_directory(
        self,
        directory: str | Path,
        repository: str = "",
        ref: str = "",
        scan_id: str | None = None,
    ) -> IaCScanResult:
        """Scan a directory for IaC configuration files.

        Args:
            directory: Path to directory.
            repository: Repository name.
            ref: Git ref.
            scan_id: Optional scan ID.

        Returns:
            IaCScanResult with findings.
        """
        directory = Path(directory)
        files: dict[str, str] = {}

        # Terraform files
        for tf_file in directory.rglob("*.tf"):
            try:
                files[str(tf_file)] = tf_file.read_text()
            except Exception as e:
                logger.warning(f"Could not read {tf_file}: {e}")

        # YAML files (CloudFormation, Kubernetes, Helm)
        for yaml_file in directory.rglob("*.yaml"):
            try:
                content = yaml_file.read_text()
                if self.is_iac_file(str(yaml_file), content):
                    files[str(yaml_file)] = content
            except Exception as e:
                logger.warning(f"Could not read {yaml_file}: {e}")

        for yml_file in directory.rglob("*.yml"):
            try:
                content = yml_file.read_text()
                if self.is_iac_file(str(yml_file), content):
                    files[str(yml_file)] = content
            except Exception as e:
                logger.warning(f"Could not read {yml_file}: {e}")

        # JSON files (CloudFormation, Terraform)
        for json_file in directory.rglob("*.json"):
            filename = json_file.name
            if filename.endswith(".tf.json") or "template" in filename.lower():
                try:
                    files[str(json_file)] = json_file.read_text()
                except Exception as e:
                    logger.warning(f"Could not read {json_file}: {e}")

        return self.scan_files(
            files,
            repository=repository or str(directory),
            ref=ref,
            scan_id=scan_id,
        )

    def _generate_scan_id(self) -> str:
        """Generate a unique scan ID."""
        import uuid

        return f"iac_scan_{uuid.uuid4().hex[:12]}"


def scan_iac_security(
    files: dict[str, str],
    repository: str = "",
    ref: str = "",
) -> IaCScanResult:
    """Convenience function to scan IaC files for security issues.

    Args:
        files: Dictionary mapping file path to content.
        repository: Repository name.
        ref: Git ref.

    Returns:
        IaCScanResult with findings.
    """
    scanner = IaCScanner()
    return scanner.scan_files(files, repository=repository, ref=ref)
