# Copyright (c) 2026 MergeGuide, Inc. All rights reserved.
# SPDX-License-Identifier: Proprietary
"""CI/CD Pipeline Security Scanner.

Scans CI/CD configuration files for security issues:
- GitHub Actions workflows
- GitLab CI configurations
- Jenkins pipelines
- CircleCI configurations
- Azure Pipelines
- Bitbucket Pipelines
"""

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 CICDPlatform(Enum):
    """Supported CI/CD platforms."""

    GITHUB_ACTIONS = "github_actions"
    GITLAB_CI = "gitlab_ci"
    JENKINS = "jenkins"
    CIRCLECI = "circleci"
    AZURE_PIPELINES = "azure_pipelines"
    BITBUCKET = "bitbucket"
    TRAVIS = "travis"
    BUILDKITE = "buildkite"
    UNKNOWN = "unknown"


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

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


class IssueCategory(Enum):
    """Security issue categories."""

    SECRETS = "secrets"
    PERMISSIONS = "permissions"
    INJECTION = "injection"
    CONFIGURATION = "configuration"
    SUPPLY_CHAIN = "supply_chain"
    CONTAINER = "container"
    NETWORK = "network"


@dataclass
class CICDSecurityRule:
    """A CI/CD security rule definition."""

    id: str
    name: str
    description: str
    pattern: str
    severity: IssueSeverity
    category: IssueCategory
    platforms: list[CICDPlatform]
    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


# =============================================================================
# CI/CD Security Rules
# =============================================================================

CICD_SECURITY_RULES: list[CICDSecurityRule] = [
    # ---------------------------------------------------------------------
    # Secrets & Credentials
    # ---------------------------------------------------------------------
    CICDSecurityRule(
        id="cicd-hardcoded-github-token",
        name="Hardcoded GitHub Token",
        description="GitHub personal access token hardcoded in workflow",
        pattern=r"['\"]?(ghp|gho|ghu|ghs|ghr)_[a-zA-Z0-9]{36,}['\"]?",
        severity=IssueSeverity.CRITICAL,
        category=IssueCategory.SECRETS,
        platforms=[CICDPlatform.GITHUB_ACTIONS, CICDPlatform.GITLAB_CI],
        remediation="Use ${{ secrets.GITHUB_TOKEN }} or store in repository secrets",
    ),
    CICDSecurityRule(
        id="cicd-hardcoded-api-key",
        name="Hardcoded API Key",
        description="API key or secret appears to be hardcoded",
        pattern=r"(?i)(api[_-]?key|api[_-]?secret|apikey)\s*[=:]\s*['\"][a-zA-Z0-9_\-]{16,}['\"]",
        severity=IssueSeverity.CRITICAL,
        category=IssueCategory.SECRETS,
        platforms=[p for p in CICDPlatform if p != CICDPlatform.UNKNOWN],
        remediation="Store API keys in CI/CD secrets, not in configuration files",
        flags="i",
    ),
    CICDSecurityRule(
        id="cicd-hardcoded-password",
        name="Hardcoded Password",
        description="Password appears to be hardcoded in pipeline",
        pattern=r"(?i)(password|passwd|pwd)\s*[=:]\s*['\"][^'\"]{4,}['\"]",
        severity=IssueSeverity.CRITICAL,
        category=IssueCategory.SECRETS,
        platforms=[p for p in CICDPlatform if p != CICDPlatform.UNKNOWN],
        remediation="Use CI/CD secrets management for passwords",
        flags="i",
    ),
    CICDSecurityRule(
        id="cicd-hardcoded-aws-credentials",
        name="Hardcoded AWS Credentials",
        description="AWS access key or secret key hardcoded",
        pattern=r"(?i)(aws_access_key_id|aws_secret_access_key)\s*[=:]\s*['\"][A-Z0-9]{16,}['\"]",
        severity=IssueSeverity.CRITICAL,
        category=IssueCategory.SECRETS,
        platforms=[p for p in CICDPlatform if p != CICDPlatform.UNKNOWN],
        remediation="Use OIDC or IAM roles instead of static credentials",
        flags="i",
    ),
    CICDSecurityRule(
        id="cicd-hardcoded-docker-credentials",
        name="Hardcoded Docker Credentials",
        description="Docker Hub or registry credentials hardcoded",
        pattern=r"(?i)(docker_username|docker_password|registry_password)\s*[=:]\s*['\"][a-zA-Z0-9_\-]{4,}['\"]",
        severity=IssueSeverity.CRITICAL,
        category=IssueCategory.SECRETS,
        platforms=[p for p in CICDPlatform if p != CICDPlatform.UNKNOWN],
        remediation="Use CI/CD secrets for Docker registry credentials",
        flags="i",
    ),
    CICDSecurityRule(
        id="cicd-private-key",
        name="Private Key in Pipeline",
        description="Private key embedded in CI/CD configuration",
        pattern=r"-----BEGIN\s+(RSA\s+|EC\s+|OPENSSH\s+)?PRIVATE\s+KEY-----",
        severity=IssueSeverity.CRITICAL,
        category=IssueCategory.SECRETS,
        platforms=[p for p in CICDPlatform if p != CICDPlatform.UNKNOWN],
        remediation="Never embed private keys in config files; use secrets management",
    ),
    # ---------------------------------------------------------------------
    # Permissions & Access Control
    # ---------------------------------------------------------------------
    CICDSecurityRule(
        id="cicd-github-write-all",
        name="Overly Permissive GitHub Permissions",
        description="Using write-all permissions grants excessive access",
        pattern=r"permissions:\s*write-all",
        severity=IssueSeverity.HIGH,
        category=IssueCategory.PERMISSIONS,
        platforms=[CICDPlatform.GITHUB_ACTIONS],
        remediation="Specify only required permissions (contents: write, packages: write, etc.)",
    ),
    CICDSecurityRule(
        id="cicd-github-contents-write",
        name="Contents Write Permission",
        description="Contents write permission allows modifying repository files",
        pattern=r"contents:\s*write",
        severity=IssueSeverity.MEDIUM,
        category=IssueCategory.PERMISSIONS,
        platforms=[CICDPlatform.GITHUB_ACTIONS],
        remediation="Only grant contents:write when necessary; prefer read-only access",
    ),
    CICDSecurityRule(
        id="cicd-github-id-token-write",
        name="ID Token Write Permission",
        description="id-token:write enables OIDC; ensure intentional",
        pattern=r"id-token:\s*write",
        severity=IssueSeverity.INFO,
        category=IssueCategory.PERMISSIONS,
        platforms=[CICDPlatform.GITHUB_ACTIONS],
        remediation="Verify OIDC is intended; this enables AWS/GCP/Azure authentication",
    ),
    # ---------------------------------------------------------------------
    # Command Injection Risks
    # ---------------------------------------------------------------------
    CICDSecurityRule(
        id="cicd-injection-pr-title",
        name="PR Title in Shell Command",
        description="User-controlled PR title used in shell command (injection risk)",
        pattern=r"run:\s*[|>]?.*\$\{\{\s*github\.event\.pull_request\.title\s*\}\}",
        severity=IssueSeverity.HIGH,
        category=IssueCategory.INJECTION,
        platforms=[CICDPlatform.GITHUB_ACTIONS],
        remediation="Validate/sanitize PR titles before using in commands",
    ),
    CICDSecurityRule(
        id="cicd-injection-pr-body",
        name="PR Body in Shell Command",
        description="User-controlled PR body used in shell command",
        pattern=r"run:\s*[|>]?.*\$\{\{\s*github\.event\.pull_request\.body\s*\}\}",
        severity=IssueSeverity.HIGH,
        category=IssueCategory.INJECTION,
        platforms=[CICDPlatform.GITHUB_ACTIONS],
        remediation="Never use PR body directly in commands; validate and sanitize first",
    ),
    CICDSecurityRule(
        id="cicd-injection-issue-title",
        name="Issue Title in Shell Command",
        description="User-controlled issue title used in shell command",
        pattern=r"run:\s*[|>]?.*\$\{\{\s*github\.event\.issue\.title\s*\}\}",
        severity=IssueSeverity.HIGH,
        category=IssueCategory.INJECTION,
        platforms=[CICDPlatform.GITHUB_ACTIONS],
        remediation="Sanitize issue titles before using in shell commands",
    ),
    CICDSecurityRule(
        id="cicd-injection-comment-body",
        name="Comment Body in Shell Command",
        description="User-controlled comment body used in shell command",
        pattern=r"run:\s*[|>]?.*\$\{\{\s*github\.event\.comment\.body\s*\}\}",
        severity=IssueSeverity.CRITICAL,
        category=IssueCategory.INJECTION,
        platforms=[CICDPlatform.GITHUB_ACTIONS],
        remediation="Never use comment body in shell; this is a common attack vector",
    ),
    CICDSecurityRule(
        id="cicd-injection-head-ref",
        name="Head Ref in Shell Command",
        description="Branch name (head_ref) used in shell command",
        pattern=r"run:\s*[|>]?.*\$\{\{\s*github\.head_ref\s*\}\}",
        severity=IssueSeverity.MEDIUM,
        category=IssueCategory.INJECTION,
        platforms=[CICDPlatform.GITHUB_ACTIONS],
        remediation="Branch names can contain special characters; sanitize before use",
    ),
    # ---------------------------------------------------------------------
    # Supply Chain Security
    # ---------------------------------------------------------------------
    CICDSecurityRule(
        id="cicd-unpinned-action",
        name="Unpinned Action Version",
        description="GitHub Action using unpinned version (e.g., @v1 instead of SHA)",
        pattern=r"uses:\s*[a-zA-Z0-9_\-]+/[a-zA-Z0-9_\-]+@(v\d+|main|master|latest)",
        severity=IssueSeverity.MEDIUM,
        category=IssueCategory.SUPPLY_CHAIN,
        platforms=[CICDPlatform.GITHUB_ACTIONS],
        remediation="Pin actions to full SHA: uses: actions/checkout@<sha>",
    ),
    CICDSecurityRule(
        id="cicd-third-party-action",
        name="Third-Party Action Usage",
        description="Using action from non-verified third party",
        pattern=r"uses:\s*(?!actions/|github/|azure/|aws-actions/|google-github-actions/)[a-zA-Z0-9_\-]+/[a-zA-Z0-9_\-]+@",
        severity=IssueSeverity.INFO,
        category=IssueCategory.SUPPLY_CHAIN,
        platforms=[CICDPlatform.GITHUB_ACTIONS],
        remediation="Verify third-party actions; prefer official or well-maintained actions",
    ),
    CICDSecurityRule(
        id="cicd-curl-pipe-bash",
        name="Curl Pipe to Bash",
        description="Downloading and executing scripts directly (supply chain risk)",
        pattern=r"curl\s+.*\|\s*(ba)?sh|wget\s+.*\|\s*(ba)?sh",
        severity=IssueSeverity.HIGH,
        category=IssueCategory.SUPPLY_CHAIN,
        platforms=[p for p in CICDPlatform if p != CICDPlatform.UNKNOWN],
        remediation="Download scripts first, verify checksums, then execute",
    ),
    # ---------------------------------------------------------------------
    # Container Security
    # ---------------------------------------------------------------------
    CICDSecurityRule(
        id="cicd-privileged-container",
        name="Privileged Container",
        description="Container running with privileged flag",
        pattern=r"(?i)(--privileged|privileged:\s*true)",
        severity=IssueSeverity.HIGH,
        category=IssueCategory.CONTAINER,
        platforms=[p for p in CICDPlatform if p != CICDPlatform.UNKNOWN],
        remediation="Avoid privileged containers; use specific capabilities instead",
        flags="i",
    ),
    CICDSecurityRule(
        id="cicd-docker-socket-mount",
        name="Docker Socket Mount",
        description="Mounting Docker socket exposes host Docker daemon",
        pattern=r"/var/run/docker\.sock",
        severity=IssueSeverity.HIGH,
        category=IssueCategory.CONTAINER,
        platforms=[p for p in CICDPlatform if p != CICDPlatform.UNKNOWN],
        remediation="Avoid mounting Docker socket; use rootless Docker or dedicated builders",
    ),
    CICDSecurityRule(
        id="cicd-latest-image-tag",
        name="Using Latest Image Tag",
        description="Using :latest tag for container image (non-reproducible)",
        pattern=r"image:\s*[a-zA-Z0-9_\-/]+:latest",
        severity=IssueSeverity.LOW,
        category=IssueCategory.CONTAINER,
        platforms=[p for p in CICDPlatform if p != CICDPlatform.UNKNOWN],
        remediation="Pin container images to specific version or SHA digest",
    ),
    # ---------------------------------------------------------------------
    # Configuration Best Practices
    # ---------------------------------------------------------------------
    CICDSecurityRule(
        id="cicd-ubuntu-latest",
        name="Using ubuntu-latest Runner",
        description="ubuntu-latest may change; prefer pinned version",
        pattern=r"runs-on:\s*\[?\s*ubuntu-latest\s*\]?",
        severity=IssueSeverity.LOW,
        category=IssueCategory.CONFIGURATION,
        platforms=[CICDPlatform.GITHUB_ACTIONS],
        remediation="Use ubuntu-22.04 or ubuntu-20.04 for reproducibility",
    ),
    CICDSecurityRule(
        id="cicd-workflow-dispatch-any",
        name="Unrestricted Workflow Dispatch",
        description="workflow_dispatch without input validation",
        pattern=r"workflow_dispatch:\s*$",
        severity=IssueSeverity.INFO,
        category=IssueCategory.CONFIGURATION,
        platforms=[CICDPlatform.GITHUB_ACTIONS],
        remediation="Consider adding input validation for manual workflow triggers",
    ),
    CICDSecurityRule(
        id="cicd-pull-request-target",
        name="Pull Request Target Trigger",
        description="pull_request_target runs in context of base branch (security risk)",
        pattern=r"on:\s*\[?.*pull_request_target",
        severity=IssueSeverity.HIGH,
        category=IssueCategory.CONFIGURATION,
        platforms=[CICDPlatform.GITHUB_ACTIONS],
        remediation="Be extremely careful with pull_request_target; don't check out PR code",
    ),
    CICDSecurityRule(
        id="cicd-debug-enabled",
        name="Debug Mode Enabled",
        description="Debug or verbose logging enabled (may leak secrets)",
        pattern=r"(?i)(ACTIONS_STEP_DEBUG|ACTIONS_RUNNER_DEBUG):\s*['\"]?true['\"]?",
        severity=IssueSeverity.MEDIUM,
        category=IssueCategory.CONFIGURATION,
        platforms=[CICDPlatform.GITHUB_ACTIONS],
        remediation="Disable debug mode in production workflows",
        flags="i",
    ),
    # ---------------------------------------------------------------------
    # GitLab CI Specific
    # ---------------------------------------------------------------------
    CICDSecurityRule(
        id="cicd-gitlab-allow-failure",
        name="Security Job Allow Failure",
        description="Security scanning job configured to allow failure",
        pattern=r"(?i)(sast|dast|dependency.?scan|secret.?detect|container.?scan)[:\s]*\n.*allow_failure:\s*true",
        severity=IssueSeverity.MEDIUM,
        category=IssueCategory.CONFIGURATION,
        platforms=[CICDPlatform.GITLAB_CI],
        remediation="Security jobs should not allow failure; fix issues instead",
        flags="ims",
    ),
    # ---------------------------------------------------------------------
    # Jenkins Specific
    # ---------------------------------------------------------------------
    CICDSecurityRule(
        id="cicd-jenkins-script-approval",
        name="Jenkins Script Approval Bypass",
        description="Attempting to bypass script approval sandbox",
        pattern=r"@NonCPS|@Grab|evaluate\s*\(",
        severity=IssueSeverity.HIGH,
        category=IssueCategory.CONFIGURATION,
        platforms=[CICDPlatform.JENKINS],
        remediation="Avoid sandbox escapes; use approved methods only",
    ),
]


@dataclass
class CICDSecurityFinding:
    """A CI/CD 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: IssueSeverity
    category: IssueCategory
    platform: CICDPlatform
    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 CICDScanResult:
    """Result of a CI/CD 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
    permissions_count: int = 0
    injection_count: int = 0
    configuration_count: int = 0
    supply_chain_count: int = 0
    container_count: int = 0

    findings: list[CICDSecurityFinding] = field(default_factory=list)
    files_scanned: list[str] = field(default_factory=list)
    platforms_detected: set[CICDPlatform] = 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,
                "permissions": self.permissions_count,
                "injection": self.injection_count,
                "configuration": self.configuration_count,
                "supply_chain": self.supply_chain_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 CICDScanner:
    """Scans CI/CD configuration files for security issues."""

    # Map of file patterns to platforms
    FILE_PATTERNS: dict[str, CICDPlatform] = {
        ".github/workflows/": CICDPlatform.GITHUB_ACTIONS,
        ".gitlab-ci.yml": CICDPlatform.GITLAB_CI,
        ".gitlab-ci.yaml": CICDPlatform.GITLAB_CI,
        "Jenkinsfile": CICDPlatform.JENKINS,
        ".circleci/config.yml": CICDPlatform.CIRCLECI,
        ".circleci/config.yaml": CICDPlatform.CIRCLECI,
        "azure-pipelines.yml": CICDPlatform.AZURE_PIPELINES,
        "azure-pipelines.yaml": CICDPlatform.AZURE_PIPELINES,
        "bitbucket-pipelines.yml": CICDPlatform.BITBUCKET,
        "bitbucket-pipelines.yaml": CICDPlatform.BITBUCKET,
        ".travis.yml": CICDPlatform.TRAVIS,
        ".travis.yaml": CICDPlatform.TRAVIS,
        "buildkite.yml": CICDPlatform.BUILDKITE,
        "buildkite.yaml": CICDPlatform.BUILDKITE,
        ".buildkite/pipeline.yml": CICDPlatform.BUILDKITE,
    }

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

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

    def detect_platform(self, file_path: str) -> CICDPlatform:
        """Detect CI/CD platform from file path.

        Args:
            file_path: Path to the configuration file.

        Returns:
            Detected platform or UNKNOWN.
        """
        for pattern, platform in self.FILE_PATTERNS.items():
            if pattern in file_path:
                return platform

        # Check filename
        filename = Path(file_path).name
        for pattern, platform in self.FILE_PATTERNS.items():
            if filename == pattern or filename == Path(pattern).name:
                return platform

        return CICDPlatform.UNKNOWN

    def is_cicd_file(self, file_path: str) -> bool:
        """Check if a file is a CI/CD configuration file.

        Args:
            file_path: Path to check.

        Returns:
            True if it's a CI/CD configuration file.
        """
        return self.detect_platform(file_path) != CICDPlatform.UNKNOWN

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

        Args:
            content: File content.
            file_path: Path to the file.
            platform: CI/CD 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)

        findings = []

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

            matches = rule.check(content)

            for match in matches:
                finding = CICDSecurityFinding(
                    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,
    ) -> CICDScanResult:
        """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:
            CICDScanResult with all findings.
        """
        if scan_id is None:
            scan_id = self._generate_scan_id()

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

        for file_path, content in files.items():
            # Check if it's a CI/CD file
            platform = self.detect_platform(file_path)
            if platform == CICDPlatform.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 == IssueSeverity.CRITICAL:
                        result.critical_count += 1
                    elif finding.severity == IssueSeverity.HIGH:
                        result.high_count += 1
                    elif finding.severity == IssueSeverity.MEDIUM:
                        result.medium_count += 1
                    elif finding.severity == IssueSeverity.LOW:
                        result.low_count += 1
                    else:
                        result.info_count += 1

                    # Update category counts
                    if finding.category == IssueCategory.SECRETS:
                        result.secrets_count += 1
                    elif finding.category == IssueCategory.PERMISSIONS:
                        result.permissions_count += 1
                    elif finding.category == IssueCategory.INJECTION:
                        result.injection_count += 1
                    elif finding.category == IssueCategory.CONFIGURATION:
                        result.configuration_count += 1
                    elif finding.category == IssueCategory.SUPPLY_CHAIN:
                        result.supply_chain_count += 1
                    elif finding.category == IssueCategory.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 = {
            IssueSeverity.CRITICAL: 0,
            IssueSeverity.HIGH: 1,
            IssueSeverity.MEDIUM: 2,
            IssueSeverity.LOW: 3,
            IssueSeverity.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,
    ) -> CICDScanResult:
        """Scan a directory for CI/CD configuration files.

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

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

        # GitHub Actions
        gh_workflows = directory / ".github" / "workflows"
        if gh_workflows.exists():
            for yml_file in gh_workflows.glob("*.yml"):
                try:
                    files[str(yml_file)] = yml_file.read_text()
                except Exception as e:
                    logger.warning(f"Could not read {yml_file}: {e}")
            for yaml_file in gh_workflows.glob("*.yaml"):
                try:
                    files[str(yaml_file)] = yaml_file.read_text()
                except Exception as e:
                    logger.warning(f"Could not read {yaml_file}: {e}")

        # Other CI/CD files
        for pattern in self.FILE_PATTERNS:
            if "/" in pattern:
                continue  # Skip directory patterns
            config_path = directory / pattern
            if config_path.exists():
                try:
                    files[str(config_path)] = config_path.read_text()
                except Exception as e:
                    logger.warning(f"Could not read {config_path}: {e}")

        # CircleCI
        circleci_config = directory / ".circleci" / "config.yml"
        if circleci_config.exists():
            try:
                files[str(circleci_config)] = circleci_config.read_text()
            except Exception as e:
                logger.warning(f"Could not read {circleci_config}: {e}")

        # Buildkite
        buildkite_config = directory / ".buildkite" / "pipeline.yml"
        if buildkite_config.exists():
            try:
                files[str(buildkite_config)] = buildkite_config.read_text()
            except Exception as e:
                logger.warning(f"Could not read {buildkite_config}: {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"cicd_scan_{uuid.uuid4().hex[:12]}"


def scan_cicd_security(
    files: dict[str, str],
    repository: str = "",
    ref: str = "",
) -> CICDScanResult:
    """Convenience function to scan CI/CD files for security issues.

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

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