package iis import ( "context" "encoding/json" "fmt" "log/slog" "runtime" "time" "github.com/shankar0123/certctl/internal/connector/target" ) // Config represents the IIS deployment target configuration. // This configuration is for Windows agents that manage IIS servers. type Config struct { Hostname string `json:"hostname"` // Target hostname or IP SiteName string `json:"site_name"` // IIS site name (e.g., "Default Web Site") CertStore string `json:"cert_store"` // Windows cert store (e.g., "My", "WebHosting") BindingInfo string `json:"binding_info"` // Binding info (e.g., "*.example.com") } // Connector implements the target.Connector interface for IIS (Internet Information Services). // This connector runs on Windows agents and manages certificate deployment via IIS. // // IIS certificate management requires: // - Windows Server with IIS installed // - PowerShell execution available // - Administrative privileges // // TODO: Implement actual PowerShell command execution for: // - Certificate import: Import-PfxCertificate // - IIS binding update: New-WebBinding, Set-WebBinding // - Validation: Get-WebBinding type Connector struct { config *Config logger *slog.Logger } // New creates a new IIS target connector with the given configuration and logger. func New(config *Config, logger *slog.Logger) *Connector { return &Connector{ config: config, logger: logger, } } // ValidateConfig checks that the IIS configuration is valid and accessible. // It verifies that we're on Windows and that the IIS site exists. // // TODO: Implement actual PowerShell checks. func (c *Connector) ValidateConfig(ctx context.Context, rawConfig json.RawMessage) error { var cfg Config if err := json.Unmarshal(rawConfig, &cfg); err != nil { return fmt.Errorf("invalid IIS config: %w", err) } if cfg.SiteName == "" || cfg.CertStore == "" { return fmt.Errorf("IIS site_name and cert_store are required") } // Verify we're on Windows if runtime.GOOS != "windows" { return fmt.Errorf("IIS connector only runs on Windows, got %s", runtime.GOOS) } c.logger.Info("validating IIS configuration", "site_name", cfg.SiteName, "cert_store", cfg.CertStore, "hostname", cfg.Hostname) // TODO: Implement PowerShell check // In production: // 1. Run PowerShell command: Get-IISSite -Name {SiteName} // 2. Verify site exists and is running // 3. Check cert store: Get-Item -Path "Cert:\LocalMachine\{CertStore}" c.logger.Warn("IIS validation not yet fully implemented", "site_name", cfg.SiteName) c.config = &cfg return nil } // DeployCertificate imports a certificate to the Windows certificate store and updates // the IIS binding to use the new certificate. // // The IIS deployment process (via PowerShell): // 1. Create a temporary PFX file from the certificate and existing private key // (Note: The private key is managed by the agent, not provided by the control plane) // 2. Import the PFX to the Windows certificate store (My store by default) // 3. Get the certificate thumbprint // 4. Update the IIS binding to use the new certificate by thumbprint // 5. Verify the binding is active // // TODO: Implement actual PowerShell commands: // - Import-PfxCertificate -FilePath {pfxPath} -CertStoreLocation "Cert:\LocalMachine\My" // - Get-ChildItem -Path "Cert:\LocalMachine\My" | Where {$_.Subject -eq "CN=..."} // - Set-WebBinding -Name {SiteName} -BindingInformation "{BindingInfo}" -Protocol https -SslFlags 1 -CertificateThumbprint {thumbprint} func (c *Connector) DeployCertificate(ctx context.Context, request target.DeploymentRequest) (*target.DeploymentResult, error) { c.logger.Info("deploying certificate to IIS", "site_name", c.config.SiteName, "cert_store", c.config.CertStore) startTime := time.Now() // TODO: Implement IIS certificate deployment // In production: // 1. Create temporary PFX from CertPEM and ChainPEM // (Private key should already exist on the agent) // 2. Import certificate: // PowerShell: Import-PfxCertificate -FilePath $pfxPath -CertStoreLocation "Cert:\LocalMachine\{CertStore}" -Password $password // 3. Get certificate thumbprint: // PowerShell: (Get-ChildItem -Path "Cert:\LocalMachine\{CertStore}" | Where {$_.Subject -like "*CN=*"}).Thumbprint // 4. Update IIS binding: // PowerShell: Set-WebBinding -Name "{SiteName}" -BindingInformation "{BindingInfo}:443:*.example.com" -Protocol https -CertificateThumbprint $thumbprint // 5. Remove temporary PFX file deploymentDuration := time.Since(startTime) c.logger.Warn("IIS deployment not yet implemented", "site_name", c.config.SiteName) return &target.DeploymentResult{ Success: true, TargetAddress: fmt.Sprintf("%s (IIS: %s)", c.config.Hostname, c.config.SiteName), DeploymentID: fmt.Sprintf("iis-%d", time.Now().Unix()), Message: "Certificate deployment to IIS initiated (stub)", DeployedAt: time.Now(), Metadata: map[string]string{ "hostname": c.config.Hostname, "site_name": c.config.SiteName, "cert_store": c.config.CertStore, "duration_ms": fmt.Sprintf("%d", deploymentDuration.Milliseconds()), }, }, nil } // ValidateDeployment verifies that the certificate is properly deployed in IIS. // It checks the IIS binding configuration to ensure it's active with the correct certificate. // // TODO: Implement actual PowerShell validation. // PowerShell command: // - Get-IISSiteBinding -Name {SiteName} | Where {$_.protocol -eq "https"} func (c *Connector) ValidateDeployment(ctx context.Context, request target.ValidationRequest) (*target.ValidationResult, error) { c.logger.Info("validating IIS deployment", "certificate_id", request.CertificateID, "serial", request.Serial, "site_name", c.config.SiteName) startTime := time.Now() // TODO: Implement IIS deployment validation // In production: // 1. Query IIS binding status: // PowerShell: Get-WebBinding -Name "{SiteName}" -Protocol "https" // 2. Verify binding exists and is active // 3. Extract certificate thumbprint from binding // 4. Query certificate store to verify thumbprint matches expected certificate // 5. Check certificate validity dates and key match validationDuration := time.Since(startTime) c.logger.Warn("IIS validation not yet implemented", "site_name", c.config.SiteName) return &target.ValidationResult{ Valid: true, Serial: request.Serial, TargetAddress: fmt.Sprintf("%s (IIS: %s)", c.config.Hostname, c.config.SiteName), Message: "Certificate deployment validation initiated (stub)", ValidatedAt: time.Now(), Metadata: map[string]string{ "hostname": c.config.Hostname, "site_name": c.config.SiteName, "duration_ms": fmt.Sprintf("%d", validationDuration.Milliseconds()), }, }, nil } // executePowerShellCommand will be implemented in V3 when IIS target connector ships. // Pattern: exec.CommandContext(ctx, "powershell", "-NoProfile", "-Command", psCommand)