Created IAC reverse generator
This commit is contained in:
425
src/iac_reverse/models.py
Normal file
425
src/iac_reverse/models.py
Normal file
@@ -0,0 +1,425 @@
|
||||
"""Core data models for the IaC Reverse Engineering tool.
|
||||
|
||||
Contains enums, dataclasses, and type definitions used across all components
|
||||
of the pipeline: Scanner, Dependency Resolver, Code Generator, State Builder,
|
||||
Validator, and Incremental Scan Engine.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Enums
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ProviderType(Enum):
|
||||
"""Supported on-premises infrastructure provider types."""
|
||||
|
||||
DOCKER_SWARM = "docker_swarm"
|
||||
KUBERNETES = "kubernetes"
|
||||
SYNOLOGY = "synology"
|
||||
HARVESTER = "harvester"
|
||||
BARE_METAL = "bare_metal"
|
||||
WINDOWS = "windows"
|
||||
|
||||
|
||||
class PlatformCategory(Enum):
|
||||
"""Categorizes providers by their infrastructure model."""
|
||||
|
||||
CONTAINER_ORCHESTRATION = "container" # Docker Swarm, Kubernetes
|
||||
STORAGE_APPLIANCE = "storage" # Synology Disk Station
|
||||
HCI = "hci" # SUSE Harvester (Hyper-Converged Infrastructure)
|
||||
BARE_METAL = "bare_metal" # Physical servers (Linux)
|
||||
WINDOWS = "windows" # Standalone Windows machines
|
||||
|
||||
|
||||
PROVIDER_PLATFORM_MAP: dict[ProviderType, PlatformCategory] = {
|
||||
ProviderType.DOCKER_SWARM: PlatformCategory.CONTAINER_ORCHESTRATION,
|
||||
ProviderType.KUBERNETES: PlatformCategory.CONTAINER_ORCHESTRATION,
|
||||
ProviderType.SYNOLOGY: PlatformCategory.STORAGE_APPLIANCE,
|
||||
ProviderType.HARVESTER: PlatformCategory.HCI,
|
||||
ProviderType.BARE_METAL: PlatformCategory.BARE_METAL,
|
||||
ProviderType.WINDOWS: PlatformCategory.WINDOWS,
|
||||
}
|
||||
|
||||
|
||||
class CpuArchitecture(Enum):
|
||||
"""CPU architecture of the host or resource."""
|
||||
|
||||
AMD64 = "amd64"
|
||||
ARM = "arm"
|
||||
AARCH64 = "aarch64"
|
||||
|
||||
|
||||
class ChangeType(Enum):
|
||||
"""Classification of resource changes between scan runs."""
|
||||
|
||||
ADDED = "added"
|
||||
REMOVED = "removed"
|
||||
MODIFIED = "modified"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Provider supported resource types
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
PROVIDER_SUPPORTED_RESOURCE_TYPES: dict[ProviderType, list[str]] = {
|
||||
ProviderType.DOCKER_SWARM: [
|
||||
"docker_service",
|
||||
"docker_network",
|
||||
"docker_volume",
|
||||
"docker_config",
|
||||
"docker_secret",
|
||||
],
|
||||
ProviderType.KUBERNETES: [
|
||||
"kubernetes_deployment",
|
||||
"kubernetes_service",
|
||||
"kubernetes_ingress",
|
||||
"kubernetes_config_map",
|
||||
"kubernetes_persistent_volume",
|
||||
"kubernetes_namespace",
|
||||
],
|
||||
ProviderType.SYNOLOGY: [
|
||||
"synology_shared_folder",
|
||||
"synology_volume",
|
||||
"synology_storage_pool",
|
||||
"synology_replication_task",
|
||||
"synology_user",
|
||||
],
|
||||
ProviderType.HARVESTER: [
|
||||
"harvester_virtualmachine",
|
||||
"harvester_volume",
|
||||
"harvester_image",
|
||||
"harvester_network",
|
||||
],
|
||||
ProviderType.BARE_METAL: [
|
||||
"bare_metal_hardware",
|
||||
"bare_metal_bmc_config",
|
||||
"bare_metal_network_interface",
|
||||
"bare_metal_raid_config",
|
||||
],
|
||||
ProviderType.WINDOWS: [
|
||||
"windows_service",
|
||||
"windows_scheduled_task",
|
||||
"windows_iis_site",
|
||||
"windows_iis_app_pool",
|
||||
"windows_network_adapter",
|
||||
"windows_firewall_rule",
|
||||
"windows_installed_software",
|
||||
"windows_feature",
|
||||
"windows_hyperv_vm",
|
||||
"windows_hyperv_switch",
|
||||
"windows_dns_record",
|
||||
"windows_local_user",
|
||||
"windows_local_group",
|
||||
],
|
||||
}
|
||||
|
||||
MAX_RESOURCE_TYPE_FILTERS = 200
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scanner dataclasses
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScanProfile:
|
||||
"""Configuration for a single infrastructure scan."""
|
||||
|
||||
provider: ProviderType
|
||||
credentials: dict[str, str]
|
||||
endpoints: Optional[list[str]] = None
|
||||
resource_type_filters: Optional[list[str]] = None
|
||||
authentik_token: Optional[str] = None
|
||||
|
||||
def validate(self) -> list[str]:
|
||||
"""Returns list of validation errors, empty if valid.
|
||||
|
||||
Validates:
|
||||
- credentials must not be empty
|
||||
- resource_type_filters must have at most MAX_RESOURCE_TYPE_FILTERS entries
|
||||
- resource_type_filters entries must be supported by the provider
|
||||
"""
|
||||
errors: list[str] = []
|
||||
|
||||
if not self.credentials:
|
||||
errors.append("credentials must not be empty")
|
||||
|
||||
if self.resource_type_filters is not None:
|
||||
if len(self.resource_type_filters) > MAX_RESOURCE_TYPE_FILTERS:
|
||||
errors.append(
|
||||
f"resource_type_filters must have at most "
|
||||
f"{MAX_RESOURCE_TYPE_FILTERS} entries, "
|
||||
f"got {len(self.resource_type_filters)}"
|
||||
)
|
||||
|
||||
supported = set(PROVIDER_SUPPORTED_RESOURCE_TYPES[self.provider])
|
||||
unsupported = [
|
||||
rt for rt in self.resource_type_filters if rt not in supported
|
||||
]
|
||||
if unsupported:
|
||||
errors.append(
|
||||
f"unsupported resource types for provider "
|
||||
f"'{self.provider.value}': {unsupported}"
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
@property
|
||||
def platform_category(self) -> PlatformCategory:
|
||||
"""Return the platform category for this profile's provider."""
|
||||
return PROVIDER_PLATFORM_MAP[self.provider]
|
||||
|
||||
|
||||
@dataclass
|
||||
class DiscoveredResource:
|
||||
"""A single resource discovered from an infrastructure provider."""
|
||||
|
||||
resource_type: str
|
||||
unique_id: str
|
||||
name: str
|
||||
provider: ProviderType
|
||||
platform_category: PlatformCategory
|
||||
architecture: CpuArchitecture
|
||||
endpoint: str
|
||||
attributes: dict
|
||||
raw_references: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScanResult:
|
||||
"""Complete result of a scan operation."""
|
||||
|
||||
resources: list[DiscoveredResource]
|
||||
warnings: list[str]
|
||||
errors: list[str]
|
||||
scan_timestamp: str
|
||||
profile_hash: str
|
||||
is_partial: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScanProgress:
|
||||
"""Progress update during a scan operation."""
|
||||
|
||||
current_resource_type: str
|
||||
resources_discovered: int
|
||||
resource_types_completed: int
|
||||
total_resource_types: int
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dependency Resolver dataclasses
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass
|
||||
class ResourceRelationship:
|
||||
"""A relationship between two discovered resources."""
|
||||
|
||||
source_id: str
|
||||
target_id: str
|
||||
relationship_type: str # "parent-child", "reference", "dependency"
|
||||
source_attribute: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class UnresolvedReference:
|
||||
"""A reference that could not be resolved to a known resource."""
|
||||
|
||||
source_resource_id: str
|
||||
source_attribute: str
|
||||
referenced_id: str
|
||||
suggested_resolution: str # "data_source" or "variable"
|
||||
|
||||
|
||||
@dataclass
|
||||
class CycleReport:
|
||||
"""Report of a detected circular dependency with resolution suggestions."""
|
||||
|
||||
cycle: list[str] # Resource IDs forming the cycle
|
||||
suggested_break: tuple[str, str] # (source_id, target_id) edge to break
|
||||
break_relationship_type: str # Type of the relationship to break
|
||||
resolution_strategy: str # Human-readable suggestion for resolution
|
||||
|
||||
|
||||
@dataclass
|
||||
class DependencyGraph:
|
||||
"""Complete dependency graph of discovered resources."""
|
||||
|
||||
resources: list[DiscoveredResource]
|
||||
relationships: list[ResourceRelationship]
|
||||
topological_order: list[str]
|
||||
cycles: list[list[str]]
|
||||
unresolved_references: list[UnresolvedReference]
|
||||
cycle_reports: list[CycleReport] = field(default_factory=list)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Code Generator dataclasses
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass
|
||||
class GeneratedFile:
|
||||
"""A single generated Terraform HCL file."""
|
||||
|
||||
filename: str
|
||||
content: str
|
||||
resource_count: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExtractedVariable:
|
||||
"""A Terraform variable extracted from common resource values."""
|
||||
|
||||
name: str
|
||||
type_expr: str
|
||||
default_value: str
|
||||
description: str
|
||||
used_by: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CodeGenerationResult:
|
||||
"""Complete result of code generation."""
|
||||
|
||||
resource_files: list[GeneratedFile]
|
||||
variables_file: GeneratedFile
|
||||
provider_file: GeneratedFile
|
||||
outputs_file: Optional[GeneratedFile] = None
|
||||
skipped_resources: list[tuple[str, str]] = field(default_factory=list)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# State Builder dataclasses
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass
|
||||
class StateEntry:
|
||||
"""A single resource entry in the Terraform state file."""
|
||||
|
||||
resource_type: str
|
||||
resource_name: str
|
||||
provider_id: str
|
||||
attributes: dict
|
||||
sensitive_attributes: list[str] = field(default_factory=list)
|
||||
schema_version: int = 0
|
||||
dependencies: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class StateFile:
|
||||
"""Terraform state file representation (format version 4)."""
|
||||
|
||||
version: int = 4
|
||||
terraform_version: str = ""
|
||||
serial: int = 1
|
||||
lineage: str = ""
|
||||
resources: list[StateEntry] = field(default_factory=list)
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Serialize to Terraform state JSON format."""
|
||||
import json
|
||||
import uuid
|
||||
|
||||
lineage = self.lineage or str(uuid.uuid4())
|
||||
|
||||
state_resources = []
|
||||
for entry in self.resources:
|
||||
state_resources.append(
|
||||
{
|
||||
"mode": "managed",
|
||||
"type": entry.resource_type,
|
||||
"name": entry.resource_name,
|
||||
"provider": f'provider["registry.terraform.io/hashicorp/{entry.resource_type.split("_")[0]}"]',
|
||||
"instances": [
|
||||
{
|
||||
"schema_version": entry.schema_version,
|
||||
"attributes": {
|
||||
"id": entry.provider_id,
|
||||
**entry.attributes,
|
||||
},
|
||||
"sensitive_attributes": entry.sensitive_attributes,
|
||||
"dependencies": entry.dependencies,
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
state = {
|
||||
"version": self.version,
|
||||
"terraform_version": self.terraform_version,
|
||||
"serial": self.serial,
|
||||
"lineage": lineage,
|
||||
"outputs": {},
|
||||
"resources": state_resources,
|
||||
}
|
||||
|
||||
return json.dumps(state, indent=2)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Validator dataclasses
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlannedChange:
|
||||
"""A single planned change reported by terraform plan."""
|
||||
|
||||
resource_address: str
|
||||
change_type: str # "add", "modify", "destroy"
|
||||
details: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValidationError:
|
||||
"""A validation error from terraform validate or plan."""
|
||||
|
||||
file: str
|
||||
message: str
|
||||
line: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValidationResult:
|
||||
"""Complete result of terraform validation."""
|
||||
|
||||
init_success: bool
|
||||
validate_success: bool
|
||||
plan_success: bool
|
||||
planned_changes: list[PlannedChange] = field(default_factory=list)
|
||||
errors: list[ValidationError] = field(default_factory=list)
|
||||
correction_attempts: int = 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Incremental Scan dataclasses
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass
|
||||
class ResourceChange:
|
||||
"""A single resource change between scan runs."""
|
||||
|
||||
resource_id: str
|
||||
resource_type: str
|
||||
resource_name: str
|
||||
change_type: ChangeType
|
||||
changed_attributes: Optional[dict] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChangeSummary:
|
||||
"""Summary of changes between two scan runs."""
|
||||
|
||||
added_count: int
|
||||
removed_count: int
|
||||
modified_count: int
|
||||
changes: list[ResourceChange] = field(default_factory=list)
|
||||
Reference in New Issue
Block a user