From afcb6dbbfa330c1a553070db644f516bdee4120c Mon Sep 17 00:00:00 2001 From: GraceSolutions Date: Mon, 14 Apr 2025 22:48:53 -0400 Subject: [PATCH] Add port forwarding support, improve XML documentation, and add unit tests --- .github/workflows/build-and-release.yml | 126 ++ .gitignore | 3 + CHANGELOG.md | 69 + CONTRIBUTING.md | 179 +++ LICENSE | 21 + PSOPNSenseAPI.sln | 35 + PSOPNSenseAPI/PSOPNSenseAPI.psd1 | 208 +++ PSOPNSenseAPI/PSOPNSenseAPI.psm1 | 3 + README.md | 197 +++ build/build-release.ps1 | 201 +++ build/build.ps1 | 91 ++ build/test.ps1 | 23 + docs/API-Reference.md | 1405 +++++++++++++++++ docs/Alias-Management.md | 120 ++ docs/Cron-Job-Management.md | 326 ++++ docs/DHCP-Management.md | 393 +++++ docs/DNS-Management.md | 411 +++++ docs/Development-Guidelines.md | 238 +++ docs/Firewall-Rules.md | 268 ++++ docs/Firmware-Management.md | 295 ++++ docs/Gateway-Management.md | 289 ++++ docs/IPNetwork2-Integration.md | 166 ++ docs/Installation-Setup.md | 217 +++ docs/Interface-Management.md | 298 ++++ docs/NAT-Rules-Management.md | 378 +++++ docs/Network-Utilities.md | 155 ++ docs/Plugin-Management.md | 296 ++++ docs/Port-Forwarding.md | 158 ++ docs/Route-Management.md | 291 ++++ docs/System-Backup-Restore.md | 296 ++++ docs/Tailscale-Integration.md | 263 +++ docs/Troubleshooting.md | 334 ++++ docs/User-Management.md | 388 +++++ docs/VLAN-Subnet-Management.md | 173 ++ docs/Visual-Studio-Solution.md | 132 ++ docs/en-US/about_PSOPNSenseAPI.help.txt | 135 ++ docs/examples/Alias-Management.ps1 | 81 + docs/examples/Basic-Usage.ps1 | 22 + docs/examples/Configuration-Management.ps1 | 29 + docs/examples/Cron-Management.ps1 | 36 + docs/examples/DHCP-Management.ps1 | 41 + docs/examples/DNS-Configuration.ps1 | 27 + docs/examples/DNS-Management.ps1 | 52 + docs/examples/Firmware-Management.ps1 | 37 + docs/examples/Gateway-Route-Management.ps1 | 59 + docs/examples/Interface-Management.ps1 | 27 + docs/examples/Network-Supernetting.ps1 | 57 + docs/examples/Network-Utilities.ps1 | 44 + docs/examples/Plugin-Management.ps1 | 29 + docs/examples/Port-Forwarding-README.md | 212 +++ docs/examples/Port-Forwarding.ps1 | 42 + docs/examples/Subnet-VLAN-Creation.ps1 | 31 + docs/examples/Tailscale-Management.ps1 | 45 + docs/examples/User-Management.ps1 | 28 + output/PSOPNSenseAPI/Newtonsoft.Json.dll | Bin 0 -> 711952 bytes output/PSOPNSenseAPI/PSOPNSenseAPI.dll | Bin 0 -> 534016 bytes output/PSOPNSenseAPI/PSOPNSenseAPI.psd1 | 85 + output/PSOPNSenseAPI/System.Buffers.dll | Bin 0 -> 20856 bytes .../System.Management.Automation.dll | Bin 0 -> 354304 bytes output/PSOPNSenseAPI/System.Memory.dll | Bin 0 -> 142240 bytes output/PSOPNSenseAPI/System.Net.IPNetwork.dll | Bin 0 -> 28160 bytes .../PSOPNSenseAPI/System.Numerics.Vectors.dll | Bin 0 -> 115856 bytes ...System.Runtime.CompilerServices.Unsafe.dll | Bin 0 -> 16768 bytes src/PSOPNSenseAPI.Tests/ApiClientTests.cs | 122 ++ .../Cmdlets/GetOPNSenseAliasCmdletTests.cs | 354 +++++ .../Cmdlets/NewOPNSenseAliasCmdletTests.cs | 285 ++++ .../Cmdlets/RemoveOPNSenseAliasCmdletTests.cs | 213 +++ .../Cmdlets/SetOPNSenseAliasCmdletTests.cs | 456 ++++++ .../PSOPNSenseAPI.Tests.csproj | 20 + .../Services/AliasServiceTests.cs | 211 +++ .../ApplyOPNSenseFirewallChangesCmdlet.cs | 100 ++ .../Cmdlets/BackupOPNSenseConfigCmdlet.cs | 53 + .../Cmdlets/ConnectOPNSenseCmdlet.cs | 99 ++ .../Cmdlets/ConnectOPNSenseTailscaleCmdlet.cs | 253 +++ .../ConvertToOPNSenseNetworkNotationCmdlet.cs | 147 ++ .../Cmdlets/DisableOPNSenseCronJobCmdlet.cs | 63 + .../DisableOPNSenseFirewallRuleCmdlet.cs | 48 + .../Cmdlets/DisableOPNSensePluginCmdlet.cs | 49 + .../Cmdlets/DisableOPNSenseTailscaleCmdlet.cs | 116 ++ .../Cmdlets/DisconnectOPNSenseCmdlet.cs | 49 + .../DisconnectOPNSenseTailscaleCmdlet.cs | 87 + .../Cmdlets/EnableOPNSenseCronJobCmdlet.cs | 63 + .../EnableOPNSenseFirewallRuleCmdlet.cs | 48 + .../Cmdlets/EnableOPNSensePluginCmdlet.cs | 49 + .../Cmdlets/EnableOPNSenseTailscaleCmdlet.cs | 235 +++ .../Cmdlets/ExportOPNSenseConfigCmdlet.cs | 76 + .../Cmdlets/GetOPNSenseAliasCmdlet.cs | 106 ++ .../Cmdlets/GetOPNSenseConfigBackupCmdlet.cs | 41 + .../Cmdlets/GetOPNSenseConnectionCmdlet.cs | 49 + .../Cmdlets/GetOPNSenseCronJobCmdlet.cs | 61 + .../Cmdlets/GetOPNSenseDHCPLeaseCmdlet.cs | 46 + .../Cmdlets/GetOPNSenseDHCPOptionCmdlet.cs | 68 + .../Cmdlets/GetOPNSenseDHCPServerCmdlet.cs | 76 + .../GetOPNSenseDHCPStaticMappingCmdlet.cs | 68 + .../Cmdlets/GetOPNSenseDNSForwardingCmdlet.cs | 41 + .../GetOPNSenseDNSForwardingHostCmdlet.cs | 61 + .../Cmdlets/GetOPNSenseDNSOverrideCmdlet.cs | 61 + .../Cmdlets/GetOPNSenseDNSServerCmdlet.cs | 52 + .../Cmdlets/GetOPNSenseFirewallRuleCmdlet.cs | 86 + .../Cmdlets/GetOPNSenseFirmwareCmdlet.cs | 112 ++ .../Cmdlets/GetOPNSenseGatewayCmdlet.cs | 107 ++ .../Cmdlets/GetOPNSenseInterfaceCmdlet.cs | 61 + .../GetOPNSenseInterfaceStatisticsCmdlet.cs | 59 + .../Cmdlets/GetOPNSensePluginCmdlet.cs | 103 ++ .../GetOPNSensePortForwardingRuleCmdlet.cs | 58 + .../Cmdlets/GetOPNSenseRouteCmdlet.cs | 61 + .../Cmdlets/GetOPNSenseSystemDNSCmdlet.cs | 41 + .../GetOPNSenseTailscaleStatusCmdlet.cs | 96 ++ .../Cmdlets/GetOPNSenseUserCmdlet.cs | 61 + .../Cmdlets/GetOPNSenseVLANCmdlet.cs | 61 + .../Cmdlets/ImportOPNSenseConfigCmdlet.cs | 75 + .../Cmdlets/InstallOPNSensePluginCmdlet.cs | 121 ++ .../InvokeOPNSenseNetworkCalculationCmdlet.cs | 347 ++++ .../Cmdlets/NewOPNSenseAliasCmdlet.cs | 148 ++ .../Cmdlets/NewOPNSenseCronJobCmdlet.cs | 125 ++ .../Cmdlets/NewOPNSenseDHCPOptionCmdlet.cs | 100 ++ .../NewOPNSenseDHCPStaticMappingCmdlet.cs | 106 ++ .../NewOPNSenseDNSForwardingHostCmdlet.cs | 92 ++ .../Cmdlets/NewOPNSenseDNSOverrideCmdlet.cs | 100 ++ .../Cmdlets/NewOPNSenseFirewallRuleCmdlet.cs | 117 ++ .../Cmdlets/NewOPNSenseGatewayCmdlet.cs | 149 ++ .../NewOPNSensePortForwardingRuleCmdlet.cs | 150 ++ .../Cmdlets/NewOPNSenseRouteCmdlet.cs | 92 ++ .../Cmdlets/NewOPNSenseSubnetVLANsCmdlet.cs | 358 +++++ .../Cmdlets/NewOPNSenseUserCmdlet.cs | 98 ++ .../Cmdlets/NewOPNSenseVLANCmdlet.cs | 77 + .../Cmdlets/OPNSenseBaseCmdlet.cs | 66 + src/PSOPNSenseAPI/Cmdlets/OPNSenseCmdlet.cs | 34 + .../Cmdlets/RemoveOPNSenseAliasCmdlet.cs | 94 ++ .../Cmdlets/RemoveOPNSenseCronJobCmdlet.cs | 89 ++ .../Cmdlets/RemoveOPNSenseDHCPLeaseCmdlet.cs | 89 ++ .../Cmdlets/RemoveOPNSenseDHCPOptionCmdlet.cs | 96 ++ .../RemoveOPNSenseDHCPStaticMappingCmdlet.cs | 96 ++ .../RemoveOPNSenseDNSForwardingHostCmdlet.cs | 89 ++ .../RemoveOPNSenseDNSOverrideCmdlet.cs | 89 ++ .../RemoveOPNSenseFirewallRuleCmdlet.cs | 74 + .../Cmdlets/RemoveOPNSenseGatewayCmdlet.cs | 89 ++ .../RemoveOPNSensePortForwardingRuleCmdlet.cs | 72 + .../Cmdlets/RemoveOPNSenseRouteCmdlet.cs | 89 ++ .../Cmdlets/RemoveOPNSenseUserCmdlet.cs | 74 + .../Cmdlets/RemoveOPNSenseVLANCmdlet.cs | 74 + .../Cmdlets/RestartOPNSenseFirewallCmdlet.cs | 160 ++ .../Cmdlets/RestartOPNSenseInterfaceCmdlet.cs | 59 + .../Cmdlets/RestoreOPNSenseConfigCmdlet.cs | 60 + .../Cmdlets/SetOPNSenseAliasCmdlet.cs | 177 +++ .../Cmdlets/SetOPNSenseCronJobCmdlet.cs | 158 ++ .../Cmdlets/SetOPNSenseDHCPOptionCmdlet.cs | 109 ++ .../Cmdlets/SetOPNSenseDHCPServerCmdlet.cs | 159 ++ .../SetOPNSenseDHCPStaticMappingCmdlet.cs | 144 ++ .../Cmdlets/SetOPNSenseDNSForwardingCmdlet.cs | 127 ++ .../SetOPNSenseDNSForwardingHostCmdlet.cs | 130 ++ .../Cmdlets/SetOPNSenseDNSServerCmdlet.cs | 130 ++ .../Cmdlets/SetOPNSenseFirewallRuleCmdlet.cs | 152 ++ .../Cmdlets/SetOPNSenseGatewayCmdlet.cs | 253 +++ .../Cmdlets/SetOPNSenseInterfaceCmdlet.cs | 131 ++ .../SetOPNSensePortForwardingRuleCmdlet.cs | 202 +++ .../Cmdlets/SetOPNSenseRouteCmdlet.cs | 130 ++ .../Cmdlets/SetOPNSenseSystemDNSCmdlet.cs | 185 +++ .../Cmdlets/SetOPNSenseUserCmdlet.cs | 131 ++ .../Cmdlets/SetOPNSenseVLANCmdlet.cs | 93 ++ .../Cmdlets/UninstallOPNSensePluginCmdlet.cs | 127 ++ .../Cmdlets/UpdateOPNSenseFirmwareCmdlet.cs | 114 ++ .../Cmdlets/UpgradeOPNSenseFirmwareCmdlet.cs | 129 ++ src/PSOPNSenseAPI/Logging/ILogger.cs | 38 + src/PSOPNSenseAPI/Logging/NullLogger.cs | 53 + src/PSOPNSenseAPI/Logging/PowerShellLogger.cs | 70 + src/PSOPNSenseAPI/Models/InterfaceConfig.cs | 297 ++++ .../Models/InterfaceStatistics.cs | 65 + .../Models/OPNSenseApiException.cs | 48 + src/PSOPNSenseAPI/Models/OPNSenseSession.cs | 25 + src/PSOPNSenseAPI/Models/PluginModels.cs | 203 +++ .../Models/PortForwardingRule.cs | 107 ++ src/PSOPNSenseAPI/PSOPNSenseAPI.csproj | 41 + src/PSOPNSenseAPI/PSOPNSenseAPI.psd1 | 239 +++ src/PSOPNSenseAPI/Services/AliasService.cs | 346 ++++ src/PSOPNSenseAPI/Services/ApiClient.cs | 99 ++ src/PSOPNSenseAPI/Services/ConfigService.cs | 263 +++ src/PSOPNSenseAPI/Services/CronService.cs | 390 +++++ src/PSOPNSenseAPI/Services/DHCPService.cs | 1068 +++++++++++++ src/PSOPNSenseAPI/Services/DNSService.cs | 735 +++++++++ src/PSOPNSenseAPI/Services/FirewallService.cs | 458 ++++++ src/PSOPNSenseAPI/Services/FirmwareService.cs | 333 ++++ src/PSOPNSenseAPI/Services/GatewayService.cs | 454 ++++++ .../Services/IOPNSenseApiClient.cs | 43 + .../Services/InterfaceService.cs | 531 +++++++ .../Services/OPNSenseApiClient.cs | 203 +++ .../Services/OPNSenseSessionState.cs | 29 + src/PSOPNSenseAPI/Services/PluginService.cs | 263 +++ .../Services/PortForwardingService.cs | 153 ++ .../Services/PortForwardingService.cs.bak | 202 +++ src/PSOPNSenseAPI/Services/RouteService.cs | 292 ++++ src/PSOPNSenseAPI/Services/ServiceBase.cs | 41 + .../Services/SystemDNSService.cs | 161 ++ src/PSOPNSenseAPI/Services/SystemService.cs | 182 +++ .../Services/TailscaleService.cs | 362 +++++ src/PSOPNSenseAPI/Services/UserService.cs | 382 +++++ src/PSOPNSenseAPI/Utilities/NetworkUtility.cs | 605 +++++++ src/global.json | 6 + temp_interface_service.cs | 289 ++++ .../InterfaceServiceTests.cs | 128 ++ tests/PSOPNSenseAPI.Tests/MSTestSettings.cs | 1 + .../PSOPNSenseAPI.Tests.csproj | 24 + tests/PSOPNSenseAPI.Tests/Test1.cs | 10 + 203 files changed, 30165 insertions(+) create mode 100644 .github/workflows/build-and-release.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 PSOPNSenseAPI.sln create mode 100644 PSOPNSenseAPI/PSOPNSenseAPI.psd1 create mode 100644 PSOPNSenseAPI/PSOPNSenseAPI.psm1 create mode 100644 README.md create mode 100644 build/build-release.ps1 create mode 100644 build/build.ps1 create mode 100644 build/test.ps1 create mode 100644 docs/API-Reference.md create mode 100644 docs/Alias-Management.md create mode 100644 docs/Cron-Job-Management.md create mode 100644 docs/DHCP-Management.md create mode 100644 docs/DNS-Management.md create mode 100644 docs/Development-Guidelines.md create mode 100644 docs/Firewall-Rules.md create mode 100644 docs/Firmware-Management.md create mode 100644 docs/Gateway-Management.md create mode 100644 docs/IPNetwork2-Integration.md create mode 100644 docs/Installation-Setup.md create mode 100644 docs/Interface-Management.md create mode 100644 docs/NAT-Rules-Management.md create mode 100644 docs/Network-Utilities.md create mode 100644 docs/Plugin-Management.md create mode 100644 docs/Port-Forwarding.md create mode 100644 docs/Route-Management.md create mode 100644 docs/System-Backup-Restore.md create mode 100644 docs/Tailscale-Integration.md create mode 100644 docs/Troubleshooting.md create mode 100644 docs/User-Management.md create mode 100644 docs/VLAN-Subnet-Management.md create mode 100644 docs/Visual-Studio-Solution.md create mode 100644 docs/en-US/about_PSOPNSenseAPI.help.txt create mode 100644 docs/examples/Alias-Management.ps1 create mode 100644 docs/examples/Basic-Usage.ps1 create mode 100644 docs/examples/Configuration-Management.ps1 create mode 100644 docs/examples/Cron-Management.ps1 create mode 100644 docs/examples/DHCP-Management.ps1 create mode 100644 docs/examples/DNS-Configuration.ps1 create mode 100644 docs/examples/DNS-Management.ps1 create mode 100644 docs/examples/Firmware-Management.ps1 create mode 100644 docs/examples/Gateway-Route-Management.ps1 create mode 100644 docs/examples/Interface-Management.ps1 create mode 100644 docs/examples/Network-Supernetting.ps1 create mode 100644 docs/examples/Network-Utilities.ps1 create mode 100644 docs/examples/Plugin-Management.ps1 create mode 100644 docs/examples/Port-Forwarding-README.md create mode 100644 docs/examples/Port-Forwarding.ps1 create mode 100644 docs/examples/Subnet-VLAN-Creation.ps1 create mode 100644 docs/examples/Tailscale-Management.ps1 create mode 100644 docs/examples/User-Management.ps1 create mode 100644 output/PSOPNSenseAPI/Newtonsoft.Json.dll create mode 100644 output/PSOPNSenseAPI/PSOPNSenseAPI.dll create mode 100644 output/PSOPNSenseAPI/PSOPNSenseAPI.psd1 create mode 100644 output/PSOPNSenseAPI/System.Buffers.dll create mode 100644 output/PSOPNSenseAPI/System.Management.Automation.dll create mode 100644 output/PSOPNSenseAPI/System.Memory.dll create mode 100644 output/PSOPNSenseAPI/System.Net.IPNetwork.dll create mode 100644 output/PSOPNSenseAPI/System.Numerics.Vectors.dll create mode 100644 output/PSOPNSenseAPI/System.Runtime.CompilerServices.Unsafe.dll create mode 100644 src/PSOPNSenseAPI.Tests/ApiClientTests.cs create mode 100644 src/PSOPNSenseAPI.Tests/Cmdlets/GetOPNSenseAliasCmdletTests.cs create mode 100644 src/PSOPNSenseAPI.Tests/Cmdlets/NewOPNSenseAliasCmdletTests.cs create mode 100644 src/PSOPNSenseAPI.Tests/Cmdlets/RemoveOPNSenseAliasCmdletTests.cs create mode 100644 src/PSOPNSenseAPI.Tests/Cmdlets/SetOPNSenseAliasCmdletTests.cs create mode 100644 src/PSOPNSenseAPI.Tests/PSOPNSenseAPI.Tests.csproj create mode 100644 src/PSOPNSenseAPI.Tests/Services/AliasServiceTests.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/ApplyOPNSenseFirewallChangesCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/BackupOPNSenseConfigCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/ConnectOPNSenseCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/ConnectOPNSenseTailscaleCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/ConvertToOPNSenseNetworkNotationCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/DisableOPNSenseCronJobCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/DisableOPNSenseFirewallRuleCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/DisableOPNSensePluginCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/DisableOPNSenseTailscaleCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/DisconnectOPNSenseCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/DisconnectOPNSenseTailscaleCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/EnableOPNSenseCronJobCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/EnableOPNSenseFirewallRuleCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/EnableOPNSensePluginCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/EnableOPNSenseTailscaleCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/ExportOPNSenseConfigCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/GetOPNSenseAliasCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/GetOPNSenseConfigBackupCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/GetOPNSenseConnectionCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/GetOPNSenseCronJobCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/GetOPNSenseDHCPLeaseCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/GetOPNSenseDHCPOptionCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/GetOPNSenseDHCPServerCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/GetOPNSenseDHCPStaticMappingCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/GetOPNSenseDNSForwardingCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/GetOPNSenseDNSForwardingHostCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/GetOPNSenseDNSOverrideCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/GetOPNSenseDNSServerCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/GetOPNSenseFirewallRuleCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/GetOPNSenseFirmwareCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/GetOPNSenseGatewayCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/GetOPNSenseInterfaceCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/GetOPNSenseInterfaceStatisticsCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/GetOPNSensePluginCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/GetOPNSensePortForwardingRuleCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/GetOPNSenseRouteCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/GetOPNSenseSystemDNSCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/GetOPNSenseTailscaleStatusCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/GetOPNSenseUserCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/GetOPNSenseVLANCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/ImportOPNSenseConfigCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/InstallOPNSensePluginCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/InvokeOPNSenseNetworkCalculationCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/NewOPNSenseAliasCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/NewOPNSenseCronJobCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/NewOPNSenseDHCPOptionCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/NewOPNSenseDHCPStaticMappingCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/NewOPNSenseDNSForwardingHostCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/NewOPNSenseDNSOverrideCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/NewOPNSenseFirewallRuleCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/NewOPNSenseGatewayCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/NewOPNSensePortForwardingRuleCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/NewOPNSenseRouteCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/NewOPNSenseSubnetVLANsCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/NewOPNSenseUserCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/NewOPNSenseVLANCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/OPNSenseBaseCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/OPNSenseCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseAliasCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseCronJobCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseDHCPLeaseCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseDHCPOptionCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseDHCPStaticMappingCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseDNSForwardingHostCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseDNSOverrideCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseFirewallRuleCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseGatewayCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/RemoveOPNSensePortForwardingRuleCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseRouteCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseUserCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseVLANCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/RestartOPNSenseFirewallCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/RestartOPNSenseInterfaceCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/RestoreOPNSenseConfigCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/SetOPNSenseAliasCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/SetOPNSenseCronJobCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/SetOPNSenseDHCPOptionCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/SetOPNSenseDHCPServerCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/SetOPNSenseDHCPStaticMappingCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/SetOPNSenseDNSForwardingCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/SetOPNSenseDNSForwardingHostCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/SetOPNSenseDNSServerCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/SetOPNSenseFirewallRuleCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/SetOPNSenseGatewayCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/SetOPNSenseInterfaceCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/SetOPNSensePortForwardingRuleCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/SetOPNSenseRouteCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/SetOPNSenseSystemDNSCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/SetOPNSenseUserCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/SetOPNSenseVLANCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/UninstallOPNSensePluginCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/UpdateOPNSenseFirmwareCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Cmdlets/UpgradeOPNSenseFirmwareCmdlet.cs create mode 100644 src/PSOPNSenseAPI/Logging/ILogger.cs create mode 100644 src/PSOPNSenseAPI/Logging/NullLogger.cs create mode 100644 src/PSOPNSenseAPI/Logging/PowerShellLogger.cs create mode 100644 src/PSOPNSenseAPI/Models/InterfaceConfig.cs create mode 100644 src/PSOPNSenseAPI/Models/InterfaceStatistics.cs create mode 100644 src/PSOPNSenseAPI/Models/OPNSenseApiException.cs create mode 100644 src/PSOPNSenseAPI/Models/OPNSenseSession.cs create mode 100644 src/PSOPNSenseAPI/Models/PluginModels.cs create mode 100644 src/PSOPNSenseAPI/Models/PortForwardingRule.cs create mode 100644 src/PSOPNSenseAPI/PSOPNSenseAPI.csproj create mode 100644 src/PSOPNSenseAPI/PSOPNSenseAPI.psd1 create mode 100644 src/PSOPNSenseAPI/Services/AliasService.cs create mode 100644 src/PSOPNSenseAPI/Services/ApiClient.cs create mode 100644 src/PSOPNSenseAPI/Services/ConfigService.cs create mode 100644 src/PSOPNSenseAPI/Services/CronService.cs create mode 100644 src/PSOPNSenseAPI/Services/DHCPService.cs create mode 100644 src/PSOPNSenseAPI/Services/DNSService.cs create mode 100644 src/PSOPNSenseAPI/Services/FirewallService.cs create mode 100644 src/PSOPNSenseAPI/Services/FirmwareService.cs create mode 100644 src/PSOPNSenseAPI/Services/GatewayService.cs create mode 100644 src/PSOPNSenseAPI/Services/IOPNSenseApiClient.cs create mode 100644 src/PSOPNSenseAPI/Services/InterfaceService.cs create mode 100644 src/PSOPNSenseAPI/Services/OPNSenseApiClient.cs create mode 100644 src/PSOPNSenseAPI/Services/OPNSenseSessionState.cs create mode 100644 src/PSOPNSenseAPI/Services/PluginService.cs create mode 100644 src/PSOPNSenseAPI/Services/PortForwardingService.cs create mode 100644 src/PSOPNSenseAPI/Services/PortForwardingService.cs.bak create mode 100644 src/PSOPNSenseAPI/Services/RouteService.cs create mode 100644 src/PSOPNSenseAPI/Services/ServiceBase.cs create mode 100644 src/PSOPNSenseAPI/Services/SystemDNSService.cs create mode 100644 src/PSOPNSenseAPI/Services/SystemService.cs create mode 100644 src/PSOPNSenseAPI/Services/TailscaleService.cs create mode 100644 src/PSOPNSenseAPI/Services/UserService.cs create mode 100644 src/PSOPNSenseAPI/Utilities/NetworkUtility.cs create mode 100644 src/global.json create mode 100644 temp_interface_service.cs create mode 100644 tests/PSOPNSenseAPI.Tests/InterfaceServiceTests.cs create mode 100644 tests/PSOPNSenseAPI.Tests/MSTestSettings.cs create mode 100644 tests/PSOPNSenseAPI.Tests/PSOPNSenseAPI.Tests.csproj create mode 100644 tests/PSOPNSenseAPI.Tests/Test1.cs diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml new file mode 100644 index 0000000..4f02240 --- /dev/null +++ b/.github/workflows/build-and-release.yml @@ -0,0 +1,126 @@ +name: Build and Release + +on: + push: + branches: [ main ] + paths-ignore: + - '**.md' + - 'docs/**' + - '.github/**' + - '!.github/workflows/build-and-release.yml' + pull_request: + branches: [ main ] + workflow_dispatch: + inputs: + release: + description: 'Create a release' + required: false + default: false + type: boolean + +jobs: + build: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '6.0.x' + + - name: Setup PowerShell + uses: actions/setup-powershell@v1 + with: + powershell-version: '7.2' + + - name: Restore dependencies + run: dotnet restore src/PSOPNSenseAPI.sln + + - name: Run tests + run: pwsh -Command "./build/test.ps1 -Configuration Release" + + - name: Build and package + run: | + pwsh -Command "./build/build-release.ps1 -Clean -Package" + + - name: Upload build artifacts + uses: actions/upload-artifact@v3 + with: + name: PSOPNSenseAPI + path: output/PSOPNSenseAPI/ + + - name: Upload package artifacts + uses: actions/upload-artifact@v3 + with: + name: PSOPNSenseAPI-Package + path: packages/*.zip + + release: + needs: build + if: github.event_name == 'push' && github.ref == 'refs/heads/main' || github.event.inputs.release == 'true' + runs-on: windows-latest + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Setup PowerShell + uses: actions/setup-powershell@v1 + with: + powershell-version: '7.2' + + - name: Download build artifacts + uses: actions/download-artifact@v3 + with: + name: PSOPNSenseAPI + path: output/PSOPNSenseAPI/ + + - name: Download package artifacts + uses: actions/download-artifact@v3 + with: + name: PSOPNSenseAPI-Package + path: packages/ + + - name: Configure Git + run: | + git config --global user.name "GitHub Actions" + git config --global user.email "actions@github.com" + + - name: Create Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + $version = Get-Date -Format "yyyy.MM.dd.HHmm" + + # Update version in project file + $projectFile = "src/PSOPNSenseAPI/PSOPNSenseAPI.csproj" + $projectXml = [xml](Get-Content $projectFile) + $versionNode = $projectXml.Project.PropertyGroup.Version + $versionNode.InnerText = $version + $projectXml.Save($projectFile) + + # Update version in module manifest + $manifestFile = "PSOPNSenseAPI/PSOPNSenseAPI.psd1" + $manifestContent = Get-Content -Path $manifestFile -Raw + $manifestContent = $manifestContent -replace "ModuleVersion\s*=\s*['`"].*['`"]", "ModuleVersion = '$version'" + Set-Content -Path $manifestFile -Value $manifestContent + + # Commit changes + git add $projectFile + git add $manifestFile + git commit -m "Release version $version [skip ci]" + git push + + # Create release + $packagePath = Get-ChildItem -Path "packages/*.zip" | Select-Object -First 1 -ExpandProperty FullName + + gh release create "v$version" ` + --title "Release v$version" ` + --notes "Release version $version - $(Get-Date -Format 'yyyy-MM-dd')" ` + $packagePath + shell: pwsh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4b82ccd --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.vs/ +bin/ +obj/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1ed1d69 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,69 @@ +# Changelog + +All notable changes to the PSOPNSenseAPI module will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.6.0] - 2025-04-14 + +### Added +- Port forwarding management (Get, New, Set, Remove) +- Added unit tests for interface management + +### Changed +- Updated IPNetwork2 library from version 2.6.618 to 3.1.764 +- Updated code to use the new namespace (System.Net) for IPNetwork2 +- Added support for .NET 8.0 and .NET 9.0 +- Improved XML documentation for all classes and methods +- Made OPNSenseApiClient more testable by introducing an interface + +## [0.5.0] - 2025-04-16 + +### Added +- Tailscale VPN management (Get, Enable, Disable, Connect, Disconnect) +- Auto-installation of Tailscale plugin if not present +- Support for configuring Tailscale settings +- Subnet route advertising for Tailscale networks + +## [0.4.0] - 2025-04-15 + +### Added +- DHCP server management (Get, Set) +- DHCP static mapping management (Get, New, Set, Remove) +- Cron job management (Get, New, Set, Remove, Enable, Disable) +- Improved credential handling in Restart-OPNSenseFirewall +- Switched from IPNetwork to IPNetwork2 for better subnet handling +- Implemented DHCP configuration in New-OPNSenseSubnetVLANs + +## [0.3.0] - 2025-04-14 + +### Added +- Plugin management (Get, Enable, Disable, Install, Uninstall) +- User management (Get, New, Set, Remove) +- Firmware management (Get, Update, Upgrade) +- Firewall reboot functionality with wait for reconnection +- Subnet division and VLAN creation utility +- Completed all firewall rule, alias, and NAT rule management cmdlets + +## [0.2.0] - 2025-04-14 + +### Added +- Interface management (Get, Set, Restart, Statistics) +- VLAN management (Get, New, Set, Remove) +- DNS configuration (Servers, Domains, Overrides) +- Configuration backup and restore functionality + +## [0.1.0] - 2025-04-14 + +### Added +- Initial release +- Basic connection management (Connect-OPNSense, Disconnect-OPNSense) +- Firewall rule management (Get, New, Set, Remove, Enable, Disable) +- Alias management (Get, New, Set, Remove) +- NAT rule management (Get, New, Set, Remove) +- Comprehensive logging system +- Error handling and retry logic +- Support for both PowerShell 5.1 and PowerShell 7 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2b318f0 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,179 @@ +# Contributing to PSOPNSenseAPI + +Thank you for your interest in contributing to the PSOPNSenseAPI module! This document provides guidelines and instructions for contributing to the project. + +## Code of Conduct + +This project adheres to a Code of Conduct that all contributors are expected to follow. Please read and understand the [Code of Conduct](CODE_OF_CONDUCT.md) before contributing. + +## How Can I Contribute? + +There are many ways to contribute to the PSOPNSenseAPI module: + +- Reporting bugs +- Suggesting enhancements +- Writing documentation +- Submitting code changes +- Reviewing pull requests +- Helping other users + +### Reporting Bugs + +Before reporting a bug, please check the [issue tracker](https://github.com/freedbygrace/PSOPNSenseAPI/issues) to see if the bug has already been reported. If it has, you can add additional information to the existing issue. + +When reporting a bug, please include: + +- A clear and descriptive title +- A detailed description of the issue +- Steps to reproduce the bug +- Expected behavior +- Actual behavior +- Screenshots or error messages (if applicable) +- Your environment (PowerShell version, OPNSense version, module version) + +### Suggesting Enhancements + +Enhancement suggestions are welcome! Please include: + +- A clear and descriptive title +- A detailed description of the enhancement +- The motivation for the enhancement +- Any potential implementation details +- Examples of how the enhancement would be used + +### Writing Documentation + +Documentation improvements are always welcome. You can contribute by: + +- Fixing typos or errors in existing documentation +- Adding missing documentation +- Improving examples or explanations +- Adding tutorials or guides + +### Submitting Code Changes + +1. Fork the repository +2. Create a new branch for your changes +3. Make your changes +4. Write or update tests for your changes +5. Ensure all tests pass +6. Submit a pull request + +#### Coding Standards + +- Follow the existing code style and conventions +- Use meaningful variable and function names +- Write clear and concise comments +- Include XML documentation for all public members +- Follow PowerShell best practices + +#### Testing + +- Write tests for all new functionality +- Ensure all existing tests pass +- Test your changes on both PowerShell 5.1 and PowerShell 7 +- Test your changes on different OPNSense versions if possible + +### Pull Request Process + +1. Update the README.md and documentation with details of your changes +2. Update the CHANGELOG.md with details of your changes +3. Ensure all tests pass +4. Submit the pull request with a clear description of the changes + +## Development Environment Setup + +### Prerequisites + +- Visual Studio 2019 or later +- PowerShell 5.1 or PowerShell 7+ +- .NET Framework 4.7.2 SDK (for Windows PowerShell 5.1 compatibility) +- .NET Core 3.1 SDK or later (for PowerShell 7+ compatibility) +- Git + +### Setting Up the Development Environment + +1. Clone the repository: + ``` + git clone https://github.com/freedbygrace/PSOPNSenseAPI.git + cd PSOPNSenseAPI + ``` + +2. Open the solution in Visual Studio: + ``` + start PSOPNSenseAPI.sln + ``` + +3. Restore NuGet packages: + ``` + dotnet restore + ``` + +4. Build the solution: + ``` + dotnet build + ``` + +### Building and Testing + +To build the module: + +```powershell +.\build\build.ps1 +``` + +To run tests: + +```powershell +.\build\test.ps1 +``` + +To build a release version: + +```powershell +.\build\build-release.ps1 -Clean -Test -Package +``` + +## Project Structure + +The project follows this structure: + +- **src/PSOPNSenseAPI**: Main source code + - **Cmdlets**: PowerShell cmdlets + - **Models**: Data models + - **Services**: Service classes for API communication + - **Utilities**: Utility classes + - **Logging**: Logging infrastructure +- **tests**: Unit and integration tests +- **docs**: Documentation +- **build**: Build scripts +- **output**: Build output (not in source control) + +## Versioning + +The module uses a versioning scheme of `yyyy.MM.dd.HHmm` for releases, which is automatically generated during the build process. + +## Release Process + +1. Update the CHANGELOG.md file +2. Build a release package: + ``` + .\build\build-release.ps1 -Clean -Test -Package + ``` +3. Create a new release on GitHub with the generated package + +## Getting Help + +If you need help with contributing: + +- Open an issue on GitHub +- Reach out to the maintainers +- Check the documentation and existing issues + +## License + +By contributing to this project, you agree that your contributions will be licensed under the project's [MIT License](LICENSE). + +## Acknowledgments + +Thank you to all contributors who have helped improve this module! diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2a38299 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 PSOPNSenseAPI Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/PSOPNSenseAPI.sln b/PSOPNSenseAPI.sln new file mode 100644 index 0000000..6de8e98 --- /dev/null +++ b/PSOPNSenseAPI.sln @@ -0,0 +1,35 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PSOPNSenseAPI", "src\PSOPNSenseAPI\PSOPNSenseAPI.csproj", "{B5FF021D-65A9-4E5A-A661-152CB7549ED2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{17CE58CD-B605-49EA-8AD4-6D034E0330E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PSOPNSenseAPI.Tests", "tests\PSOPNSenseAPI.Tests\PSOPNSenseAPI.Tests.csproj", "{D1BF04F2-AD6E-419B-9160-C0CB328594E4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B5FF021D-65A9-4E5A-A661-152CB7549ED2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B5FF021D-65A9-4E5A-A661-152CB7549ED2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B5FF021D-65A9-4E5A-A661-152CB7549ED2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B5FF021D-65A9-4E5A-A661-152CB7549ED2}.Release|Any CPU.Build.0 = Release|Any CPU + {D1BF04F2-AD6E-419B-9160-C0CB328594E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1BF04F2-AD6E-419B-9160-C0CB328594E4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1BF04F2-AD6E-419B-9160-C0CB328594E4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1BF04F2-AD6E-419B-9160-C0CB328594E4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F1E2D3C4-B5A6-9876-5432-1098F7E6D5C4} + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {D1BF04F2-AD6E-419B-9160-C0CB328594E4} = {17CE58CD-B605-49EA-8AD4-6D034E0330E8} + EndGlobalSection +EndGlobal diff --git a/PSOPNSenseAPI/PSOPNSenseAPI.psd1 b/PSOPNSenseAPI/PSOPNSenseAPI.psd1 new file mode 100644 index 0000000..4e30d97 --- /dev/null +++ b/PSOPNSenseAPI/PSOPNSenseAPI.psd1 @@ -0,0 +1,208 @@ +@{ + # Script module or binary module file associated with this manifest. + RootModule = 'PSOPNSenseAPI.psm1' + + # Version number of this module. + ModuleVersion = '2025.04.14.1839' + + # Supported PSEditions + CompatiblePSEditions = @('Desktop', 'Core') + + # ID used to uniquely identify this module + GUID = '8f7a3d7a-0e5a-4b7c-9b5a-9c5b3a5a8e7a' + + # Author of this module + Author = 'PSOPNSenseAPI Contributors' + + # Company or vendor of this module + CompanyName = 'PSOPNSenseAPI' + + # Copyright statement for this module + Copyright = '(c) 2025 PSOPNSenseAPI Contributors. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'PowerShell module for interacting with the OPNSense API to configure firewalls' + + # Minimum version of the PowerShell engine required by this module + PowerShellVersion = '5.1' + + # Name of the PowerShell host required by this module + # PowerShellHostName = '' + + # Minimum version of the PowerShell host required by this module + # PowerShellHostVersion = '' + + # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + DotNetFrameworkVersion = '4.7.2' + + # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # ClrVersion = '' + + # Processor architecture (None, X86, Amd64) required by this module + # ProcessorArchitecture = '' + + # Modules that must be imported into the global environment prior to importing this module + # RequiredModules = @() + + # Assemblies that must be loaded prior to importing this module + RequiredAssemblies = @('bin\PSOPNSenseAPI.dll') + + # Script files (.ps1) that are run in the caller's environment prior to importing this module. + # ScriptsToProcess = @() + + # Type files (.ps1xml) to be loaded when importing this module + # TypesToProcess = @() + + # Format files (.ps1xml) to be loaded when importing this module + # FormatsToProcess = @() + + # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess + NestedModules = @('bin\PSOPNSenseAPI.dll') + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = @() + + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = @( + # Connection Management + 'Connect-OPNSense', + 'Disconnect-OPNSense', + 'Get-OPNSenseConnection', + + # Firewall Rule Management + 'Get-OPNSenseFirewallRule', + 'New-OPNSenseFirewallRule', + 'Set-OPNSenseFirewallRule', + 'Remove-OPNSenseFirewallRule', + 'Enable-OPNSenseFirewallRule', + 'Disable-OPNSenseFirewallRule', + 'Apply-OPNSenseFirewallChanges', + + # Alias Management + 'Get-OPNSenseAlias', + 'New-OPNSenseAlias', + 'Set-OPNSenseAlias', + 'Remove-OPNSenseAlias', + + # NAT Rule Management + 'Get-OPNSenseNATRule', + 'New-OPNSenseNATRule', + 'Set-OPNSenseNATRule', + 'Remove-OPNSenseNATRule', + + # Interface Management + 'Get-OPNSenseInterface', + 'Set-OPNSenseInterface', + 'Restart-OPNSenseInterface', + 'Get-OPNSenseInterfaceStatistics', + 'New-OPNSenseSubnetVLANs', + + # VLAN Management + 'Get-OPNSenseVLAN', + 'New-OPNSenseVLAN', + 'Set-OPNSenseVLAN', + 'Remove-OPNSenseVLAN', + + # DNS Configuration + 'Get-OPNSenseDNSServer', + 'Set-OPNSenseDNSServer', + 'Get-OPNSenseDNSDomain', + 'Set-OPNSenseDNSDomain', + 'Get-OPNSenseDNSOverride', + 'New-OPNSenseDNSOverride', + 'Remove-OPNSenseDNSOverride', + + # Configuration Management + 'Backup-OPNSenseConfig', + 'Restore-OPNSenseConfig', + 'Export-OPNSenseConfig', + 'Import-OPNSenseConfig', + 'Get-OPNSenseConfigBackup', + + # Plugin Management + 'Get-OPNSensePlugin', + 'Install-OPNSensePlugin', + 'Uninstall-OPNSensePlugin', + 'Enable-OPNSensePlugin', + 'Disable-OPNSensePlugin', + + # User Management + 'Get-OPNSenseUser', + 'New-OPNSenseUser', + 'Set-OPNSenseUser', + 'Remove-OPNSenseUser', + + # Firmware Management + 'Get-OPNSenseFirmware', + 'Update-OPNSenseFirmware', + 'Upgrade-OPNSenseFirmware', + + # System Management + 'Restart-OPNSenseFirewall' + ) + + # Variables to export from this module + VariablesToExport = '*' + + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = @() + + # DSC resources to export from this module + # DscResourcesToExport = @() + + # List of all modules packaged with this module + # ModuleList = @() + + # List of all files packaged with this module + # FileList = @() + + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('OPNSense', 'Firewall', 'API', 'Network', 'Security') + + # A URL to the license for this module. + LicenseUri = 'https://github.com/freedbygrace/PSOPNSenseAPI/blob/main/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/freedbygrace/PSOPNSenseAPI' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + ReleaseNotes = 'Initial release of the PSOPNSenseAPI module' + + # Prerelease string of this module + # Prerelease = '' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + + } # End of PrivateData hashtable + + # HelpInfoURI = '' + + # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. + # DefaultCommandPrefix = '' +} + + + + + + + + + + + + diff --git a/PSOPNSenseAPI/PSOPNSenseAPI.psm1 b/PSOPNSenseAPI/PSOPNSenseAPI.psm1 new file mode 100644 index 0000000..feac251 --- /dev/null +++ b/PSOPNSenseAPI/PSOPNSenseAPI.psm1 @@ -0,0 +1,3 @@ +# This file is intentionally left empty. +# The module functionality is provided by the binary module (DLL). +# This file exists to allow for future PowerShell script functions if needed. diff --git a/README.md b/README.md new file mode 100644 index 0000000..751466a --- /dev/null +++ b/README.md @@ -0,0 +1,197 @@ +# PSOPNSenseAPI + +PowerShell module for interacting with the OPNSense API to configure firewalls. + +## Overview + +PSOPNSenseAPI is a PowerShell module that provides cmdlets for managing OPNSense firewalls through their API. The module is built as a binary module in C# and is compatible with both PowerShell 5.1 and PowerShell 7. + +## Features + +- Connect to OPNSense firewalls using API credentials +- Manage firewall rules (create, read, update, delete) +- Configure NAT rules and port forwarding +- Manage aliases (host, network, port, URL, etc.) +- Manage network interfaces and VLANs +- Configure DNS settings, overrides, and forwarding +- Configure system DNS servers +- Backup, restore, export, and import configurations +- Manage plugins (install, uninstall, enable, disable) +- Manage users and permissions +- Update and upgrade firmware +- Reboot firewall with wait for reconnection +- Create VLANs from subnet divisions +- Configure DHCP servers, static mappings, leases, and options +- Manage cron jobs +- Configure and manage Tailscale VPN (with auto-installation) +- Manage gateways and static routes +- Network calculation utilities (subnet, supernet, CIDR conversion) +- Apply and revert configuration changes +- Extensive logging and error handling +- Compatible with both PowerShell 5.1 and PowerShell 7 + +## Requirements + +- PowerShell 5.1 or PowerShell 7+ +- .NET Framework 4.7.2+ (for PowerShell 5.1) +- .NET Core 3.1+ (for PowerShell 7) + +## Installation + +```powershell +# Install from PowerShell Gallery (when published) +Install-Module -Name PSOPNSenseAPI + +# Or install manually +# 1. Download the module +# 2. Extract to a directory in your PSModulePath +# 3. Import the module +Import-Module PSOPNSenseAPI +``` + +## Quick Start + +```powershell +# Connect to an OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get all firewall rules +Get-OPNSenseFirewallRule + +# Create a new firewall rule +New-OPNSenseFirewallRule -Description "Allow HTTP" -Protocol TCP -SourceNet "192.168.1.0/24" -DestinationPort 80 -Action Pass + +# Apply changes +Apply-OPNSenseFirewallChanges + +# Manage aliases +Get-OPNSenseAlias +New-OPNSenseAlias -Name "WebServers" -Type host -Content "192.168.1.10,192.168.1.11" -Description "Web Servers" -Apply +New-OPNSenseAlias -Name "WebPorts" -Type port -Content "80,443" -Protocol TCP -Description "Web Ports" -Apply +Set-OPNSenseAlias -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Content "192.168.1.10,192.168.1.11,192.168.1.12" -Apply + +# Manage interfaces +Get-OPNSenseInterface +Set-OPNSenseInterface -Name "lan" -Description "Local Network" -IpAddress "192.168.1.1" -SubnetMask "24" + +# Create VLANs from subnet divisions +New-OPNSenseSubnetVLANs -ParentInterface "em0" -Network "192.168.0.0/24" -SubnetMaskBits 27 -StartingVlanId 10 -VlanIdIncrement 10 -EnableDHCP + +# Configure DNS +Set-OPNSenseDNSServer -Forwarding -Forwarders "8.8.8.8","8.8.4.4" -Apply +New-OPNSenseDNSOverride -Hostname "server" -Domain "local" -IpAddress "192.168.1.10" + +# Manage plugins +Get-OPNSensePlugin -Installed +Install-OPNSensePlugin -Name "os-acme-client" -Wait + +# Manage users +New-OPNSenseUser -Username "john" -Password "P@ssw0rd" -FullName "John Doe" -Groups "admins" + +# Configure DHCP +Get-OPNSenseDHCPServer +Set-OPNSenseDHCPServer -Interface "lan" -RangeFrom "192.168.1.100" -RangeTo "192.168.1.200" -Apply +New-OPNSenseDHCPStaticMapping -Interface "lan" -MacAddress "00:11:22:33:44:55" -IpAddress "192.168.1.50" -Hostname "printer" +Get-OPNSenseDHCPLease +Remove-OPNSenseDHCPLease -MacAddress "00:11:22:33:44:55" -Apply +Get-OPNSenseDHCPOption -Interface "lan" +New-OPNSenseDHCPOption -Interface "lan" -Number 66 -Value "192.168.1.10" -Description "TFTP Server" -Apply + +# Manage cron jobs +Get-OPNSenseCronJob +New-OPNSenseCronJob -Description "Daily backup" -Command "/usr/local/bin/backup.sh" -Minutes "0" -Hours "2" -Apply + +# Configure and manage Tailscale +Get-OPNSenseTailscaleStatus -IncludeInterfaces +Enable-OPNSenseTailscale -AcceptDns -AcceptRoutes -Force +# Advertise subnet routes to Tailscale network +Enable-OPNSenseTailscale -AdvertiseRoutes -SubnetRoutes "192.168.1.0/24","10.0.0.0/8" -Force +Connect-OPNSenseTailscale -AuthKey "tskey-auth-abcdef123456" -InstallIfMissing -Force + +# Manage DNS forwarding +Get-OPNSenseDNSForwarding +Set-OPNSenseDNSForwarding -Enabled -DnsServers "8.8.8.8","8.8.4.4" -Apply +New-OPNSenseDNSForwardingHost -Domain "example.com" -Server "192.168.1.10" -Apply + +# Configure system DNS +Get-OPNSenseSystemDNS +Set-OPNSenseSystemDNS -Hostname "firewall" -Domain "example.com" -DnsServers "1.1.1.1","1.0.0.1" -Apply + +# Manage gateways and routes +Get-OPNSenseGateway -IncludeStatus +New-OPNSenseGateway -Name "WAN2_GW" -Interface "opt1" -IpAddress "203.0.113.1" -Description "Secondary WAN" -Apply +Get-OPNSenseRoute +New-OPNSenseRoute -Network "192.168.100.0/24" -Gateway "WAN2_GW" -Description "Remote Office" -Apply + +# Network utilities +Invoke-OPNSenseNetworkCalculation -Network "192.168.1.0/24" -Operation Info +Invoke-OPNSenseNetworkCalculation -Network "10.0.0.0/16" -Operation Subnet -PrefixLength 24 +ConvertTo-OPNSenseNetworkNotation -CIDR 24 # Returns "255.255.255.0" + +# Advanced supernetting +Invoke-OPNSenseNetworkCalculation -Network "192.168.1.0/24" -Operation Supernet -AdditionalNetworks "192.168.2.0/24" +Invoke-OPNSenseNetworkCalculation -Network "10.0.0.0/24" -Operation SupernetSummarize -AdditionalNetworks "10.0.1.0/24","10.0.2.0/24" + +# Firmware management +Update-OPNSenseFirmware -Wait + +# Reboot firewall +Restart-OPNSenseFirewall -Wait -Timeout 300 + +# Backup and restore configuration +Backup-OPNSenseConfig -Filename "pre-upgrade-backup" +Export-OPNSenseConfig -Path "C:\Backups\opnsense-config.xml" +``` + +## Documentation + +For detailed documentation, see the [docs](./docs) directory or use PowerShell's built-in help: + +```powershell +Get-Help Connect-OPNSense -Full +``` + +## Development + +### Build and Test + +To build and test the module locally: + +```powershell +# Run tests +.\build\test.ps1 + +# Build the module +.\build\build.ps1 + +# Build a release version with automatic versioning +.\build\build-release.ps1 -Clean -Test -Package +``` + +### Versioning + +The module uses a versioning scheme of `yyyy.MM.dd.HHmm` for releases, which is automatically generated during the build process. + +### CI/CD + +The project uses GitHub Actions for continuous integration and deployment: + +- Builds and tests are run on every push to the main branch +- Release packages are automatically created with the versioning scheme +- Release artifacts are uploaded to GitHub Releases + +## Contributing + +Contributions are welcome! Please follow these steps: + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/amazing-feature`) +3. Make your changes +4. Run tests to ensure they pass +5. Commit your changes (`git commit -m 'Add some amazing feature'`) +6. Push to the branch (`git push origin feature/amazing-feature`) +7. Open a Pull Request + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. diff --git a/build/build-release.ps1 b/build/build-release.ps1 new file mode 100644 index 0000000..4740d8f --- /dev/null +++ b/build/build-release.ps1 @@ -0,0 +1,201 @@ +[CmdletBinding()] +param ( + [Parameter()] + [switch] + $Clean, + + [Parameter()] + [switch] + $Test, + + [Parameter()] + [switch] + $Package, + + [Parameter()] + [switch] + $Push, + + [Parameter()] + [string] + $GitHubToken +) + +$ErrorActionPreference = 'Stop' + +# Define paths +$rootDir = Split-Path -Parent $PSScriptRoot +$srcDir = Join-Path -Path $rootDir -ChildPath "src" +$testDir = Join-Path -Path $srcDir -ChildPath "PSOPNSenseAPI.Tests" +$projectDir = Join-Path -Path $srcDir -ChildPath "PSOPNSenseAPI" +$outputDir = Join-Path -Path $rootDir -ChildPath "output\PSOPNSenseAPI" +$moduleDir = Join-Path -Path $rootDir -ChildPath "PSOPNSenseAPI" + +# Generate version number based on current date and time +$version = Get-Date -Format "yyyy.MM.dd.HHmm" +Write-Output "Building version: $version" + +# Clean output directory if requested +if ($Clean -and (Test-Path -Path $outputDir)) { + Write-Verbose "Cleaning output directory: $outputDir" + Remove-Item -Path $outputDir -Recurse -Force +} + +# Create output directory if it doesn't exist +if (-not (Test-Path -Path $outputDir)) { + Write-Verbose "Creating output directory: $outputDir" + New-Item -Path $outputDir -ItemType Directory -Force | Out-Null +} + +# Update version in project file +$projectFile = Join-Path -Path $projectDir -ChildPath "PSOPNSenseAPI.csproj" + +# Update the version using XML manipulation +Write-Output "Updating version in project file to $version" +$projectContent = Get-Content -Path $projectFile -Raw +$updatedContent = $projectContent -replace '[^<]+', "$version" +Set-Content -Path $projectFile -Value $updatedContent + +# Verify the update +$newContent = Get-Content -Path $projectFile -Raw +if ($newContent -match "$version") { + Write-Output "Successfully updated version in project file" +} else { + Write-Warning "Failed to update version in project file" +} + +# Project file has been updated + +# Update version in module manifest +$manifestFile = Join-Path -Path $moduleDir -ChildPath "PSOPNSenseAPI.psd1" +if (Test-Path $manifestFile) { + Write-Output "Updating module manifest version to $version" + $manifestContent = Get-Content -Path $manifestFile -Raw + + # Use a more flexible regex pattern to match the ModuleVersion line + if ($manifestContent -match "ModuleVersion\s*=\s*['`"].*['`"]") { + $newManifestContent = $manifestContent -replace "ModuleVersion\s*=\s*['`"].*['`"]", "ModuleVersion = '$version'" + Set-Content -Path $manifestFile -Value $newManifestContent + Write-Output "Successfully updated module manifest version" + } else { + # Try a different approach if the regex doesn't match + $lines = Get-Content -Path $manifestFile + $versionLineIndex = $lines | Select-String -Pattern "ModuleVersion" | Select-Object -First 1 -ExpandProperty LineNumber + + if ($versionLineIndex) { + $lines[$versionLineIndex - 1] = " ModuleVersion = '$version'" + Set-Content -Path $manifestFile -Value $lines + Write-Output "Successfully updated module manifest version using line replacement" + } else { + Write-Warning "Failed to update module version in manifest. ModuleVersion line not found." + } + } +} else { + Write-Warning "Module manifest file not found at $manifestFile" +} + +# Run tests if requested +if ($Test) { + Write-Output "Running tests..." + & "$PSScriptRoot\test.ps1" -Configuration "Release" + + if ($LASTEXITCODE -ne 0) { + throw "Tests failed with exit code $LASTEXITCODE" + } +} + +# Build the solution +Write-Output "Building solution in Release configuration..." + +# Create bin directory if it doesn't exist +$binDir = Join-Path -Path $outputDir -ChildPath "bin" +if (-not (Test-Path -Path $binDir)) { + Write-Output "Creating bin directory: $binDir" + New-Item -Path $binDir -ItemType Directory -Force | Out-Null +} + +# Build the project +dotnet build "$projectDir" -c Release -o "$binDir" + +if ($LASTEXITCODE -ne 0) { + throw "Build failed with exit code $LASTEXITCODE" +} + +Write-Output "DLLs built successfully to: $binDir" + +# Copy module files +Write-Output "Copying module files to output directory" +Copy-Item -Path "$moduleDir\*" -Destination $outputDir -Recurse -Force + +# Verify DLLs were built +$dllPath = Join-Path -Path $binDir -ChildPath "PSOPNSenseAPI.dll" +if (Test-Path $dllPath) { + Write-Output "Verified DLL was built: $dllPath" +} else { + Write-Warning "DLL was not found at expected location: $dllPath" +} + +# Create a zip package if requested +if ($Package) { + $packageDir = Join-Path -Path $rootDir -ChildPath "packages" + if (-not (Test-Path -Path $packageDir)) { + New-Item -Path $packageDir -ItemType Directory -Force | Out-Null + } + + $zipFile = Join-Path -Path $packageDir -ChildPath "PSOPNSenseAPI-$version.zip" + + if (Test-Path -Path $zipFile) { + Remove-Item -Path $zipFile -Force + } + + Write-Output "Creating package: $zipFile" + Compress-Archive -Path "$outputDir\*" -DestinationPath $zipFile +} + +# Push to GitHub and create a release if requested +if ($Push) { + if ([string]::IsNullOrEmpty($GitHubToken)) { + throw "GitHubToken parameter is required for pushing to GitHub" + } + + Write-Output "Committing changes to Git..." + git add "$projectFile" + git add "$manifestFile" + git commit -m "Release version $version" + + Write-Output "Pushing changes to GitHub..." + git push + + Write-Output "Creating GitHub release..." + $releaseNotes = "Release version $version - $(Get-Date -Format 'yyyy-MM-dd')" + + # Create the release using GitHub CLI or REST API + $headers = @{ + "Authorization" = "token $GitHubToken" + "Accept" = "application/vnd.github.v3+json" + } + + $releaseData = @{ + tag_name = "v$version" + name = "Release v$version" + body = $releaseNotes + draft = $false + prerelease = $false + } | ConvertTo-Json + + $response = Invoke-RestMethod -Uri "https://api.github.com/repos/freedbygrace/PSOPNSenseAPI/releases" -Method Post -Headers $headers -Body $releaseData + + if ($Package) { + Write-Output "Uploading package to GitHub release..." + $uploadUrl = $response.upload_url -replace "{.*}", "" + $fileName = Split-Path -Path $zipFile -Leaf + + Invoke-RestMethod -Uri "$uploadUrl?name=$fileName" -Method Post -Headers $headers -InFile $zipFile -ContentType "application/zip" + } +} + +Write-Output "Build completed successfully!" +Write-Output "Module output is available at: $outputDir" +if ($Package) { + Write-Output "Package is available at: $zipFile" +} diff --git a/build/build.ps1 b/build/build.ps1 new file mode 100644 index 0000000..94eca11 --- /dev/null +++ b/build/build.ps1 @@ -0,0 +1,91 @@ +[CmdletBinding()] +param ( + [Parameter()] + [string] + $Configuration = "Release", + + [Parameter()] + [switch] + $Clean, + + [Parameter()] + [switch] + $UpdateVersion +) + +$ErrorActionPreference = 'Stop' + +# Define paths +$rootDir = Split-Path -Parent $PSScriptRoot +$srcDir = Join-Path -Path $rootDir -ChildPath "src" +$outputDir = Join-Path -Path $rootDir -ChildPath "output\PSOPNSenseAPI" +$moduleDir = Join-Path -Path $rootDir -ChildPath "PSOPNSenseAPI" + +# Clean output directory if requested +if ($Clean -and (Test-Path -Path $outputDir)) { + Write-Verbose "Cleaning output directory: $outputDir" + Remove-Item -Path $outputDir -Recurse -Force +} + +# Create output directory if it doesn't exist +if (-not (Test-Path -Path $outputDir)) { + Write-Verbose "Creating output directory: $outputDir" + New-Item -Path $outputDir -ItemType Directory -Force | Out-Null +} + +# Update version if requested +if ($UpdateVersion) { + $version = Get-Date -Format "yyyy.MM.dd.HHmm" + Write-Output "Updating version to: $version" + + # Update version in project file + $projectFile = Join-Path -Path "$srcDir\PSOPNSenseAPI" -ChildPath "PSOPNSenseAPI.csproj" + if (Test-Path $projectFile) { + $projectXml = [xml](Get-Content $projectFile) + $versionNodes = $projectXml.SelectNodes("//Version") + if ($versionNodes.Count -gt 0) { + foreach ($node in $versionNodes) { + $node.InnerText = $version + } + $projectXml.Save($projectFile) + Write-Output "Updated version in project file to $version" + } else { + Write-Warning "Could not find Version node in project file" + } + } else { + Write-Warning "Project file not found at $projectFile" + } + + # Update version in module manifest + $manifestFile = Join-Path -Path $moduleDir -ChildPath "PSOPNSenseAPI.psd1" + if (Test-Path $manifestFile) { + $manifestContent = Get-Content -Path $manifestFile -Raw + $newManifestContent = $manifestContent -replace "ModuleVersion\s*=\s*['`"].*['`"]", "ModuleVersion = '$version'" + Set-Content -Path $manifestFile -Value $newManifestContent + Write-Output "Updated version in module manifest to $version" + } else { + Write-Warning "Module manifest file not found at $manifestFile" + } +} + +# Build the solution +Write-Output "Building solution in $Configuration configuration..." +dotnet build "$srcDir\PSOPNSenseAPI" -c $Configuration -o "$outputDir\bin" + +if ($LASTEXITCODE -ne 0) { + throw "Build failed with exit code $LASTEXITCODE" +} + +# Copy module files +Write-Verbose "Copying module files to output directory" +Copy-Item -Path "$moduleDir\*" -Destination $outputDir -Recurse -Force + +# Create module directory in PSModulePath if it doesn't exist +$modulePath = $env:PSModulePath -split ';' | Select-Object -First 1 +$installPath = Join-Path -Path $modulePath -ChildPath "PSOPNSenseAPI" + +Write-Verbose "Module can be installed to: $installPath" +Write-Verbose "To install, run: Copy-Item -Path '$outputDir\*' -Destination '$installPath' -Recurse -Force" + +Write-Output "Build completed successfully!" +Write-Output "Module output is available at: $outputDir" diff --git a/build/test.ps1 b/build/test.ps1 new file mode 100644 index 0000000..725384f --- /dev/null +++ b/build/test.ps1 @@ -0,0 +1,23 @@ +[CmdletBinding()] +param ( + [Parameter()] + [string] + $Configuration = "Debug" +) + +$ErrorActionPreference = 'Stop' + +# Define paths +$rootDir = Split-Path -Parent $PSScriptRoot +$srcDir = Join-Path -Path $rootDir -ChildPath "src" +$testDir = Join-Path -Path $srcDir -ChildPath "PSOPNSenseAPI.Tests" + +# Run the tests +Write-Verbose "Running tests in $Configuration configuration" +dotnet test "$testDir" -c $Configuration + +if ($LASTEXITCODE -ne 0) { + throw "Tests failed with exit code $LASTEXITCODE" +} + +Write-Output "Tests completed successfully!" diff --git a/docs/API-Reference.md b/docs/API-Reference.md new file mode 100644 index 0000000..ace23f6 --- /dev/null +++ b/docs/API-Reference.md @@ -0,0 +1,1405 @@ +# API Reference + +This document provides a comprehensive reference for all cmdlets in the PSOPNSenseAPI module. + +## Table of Contents + +- [Connection Management](#connection-management) +- [Firewall Management](#firewall-management) +- [NAT Management](#nat-management) +- [Interface Management](#interface-management) +- [VLAN Management](#vlan-management) +- [Gateway Management](#gateway-management) +- [Route Management](#route-management) +- [DNS Management](#dns-management) +- [DHCP Management](#dhcp-management) +- [Alias Management](#alias-management) +- [User Management](#user-management) +- [Plugin Management](#plugin-management) +- [Firmware Management](#firmware-management) +- [System Management](#system-management) +- [Network Utilities](#network-utilities) +- [Tailscale Integration](#tailscale-integration) +- [Cron Job Management](#cron-job-management) + +## Connection Management + +### Connect-OPNSense + +Establishes a connection to an OPNSense firewall. + +#### Syntax + +```powershell +Connect-OPNSense + -Server + -ApiKey + -ApiSecret + [-SkipCertificateCheck] +``` + +#### Parameters + +- **Server** - The URL of the OPNSense firewall. +- **ApiKey** - The API key for authentication. +- **ApiSecret** - The API secret for authentication. +- **SkipCertificateCheck** - Skips certificate validation for HTTPS connections. + +#### Examples + +```powershell +# Connect to an OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" + +# Connect to an OPNSense firewall with certificate validation disabled +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck +``` + +### Disconnect-OPNSense + +Terminates the connection to an OPNSense firewall. + +#### Syntax + +```powershell +Disconnect-OPNSense +``` + +#### Examples + +```powershell +# Disconnect from an OPNSense firewall +Disconnect-OPNSense +``` + +## Firewall Management + +### Get-OPNSenseFirewallRule + +Retrieves firewall rules from an OPNSense firewall. + +#### Syntax + +```powershell +Get-OPNSenseFirewallRule + [-Uuid ] + [-Interface ] + [-Direction ] + [-Protocol ] + [-Action ] +``` + +#### Parameters + +- **Uuid** - The UUID of a specific firewall rule to retrieve. +- **Interface** - Filter rules by interface. +- **Direction** - Filter rules by direction (in, out). +- **Protocol** - Filter rules by protocol. +- **Action** - Filter rules by action (pass, block, reject). + +#### Examples + +```powershell +# Get all firewall rules +Get-OPNSenseFirewallRule + +# Get a specific firewall rule by UUID +Get-OPNSenseFirewallRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" + +# Get all rules for a specific interface +Get-OPNSenseFirewallRule -Interface "lan" +``` + +### New-OPNSenseFirewallRule + +Creates a new firewall rule on an OPNSense firewall. + +#### Syntax + +```powershell +New-OPNSenseFirewallRule + -Interface + [-Direction ] + [-Protocol ] + [-Source ] + [-SourcePort ] + [-Destination ] + [-DestinationPort ] + [-Action ] + [-Description ] + [-Enabled ] + [-Log ] + [-Force] +``` + +#### Parameters + +- **Interface** - The interface for the rule. +- **Direction** - The direction for the rule (in, out). Default is "in". +- **Protocol** - The protocol for the rule (tcp, udp, icmp, etc.). +- **Source** - The source address for the rule. Default is "any". +- **SourcePort** - The source port for the rule. Default is "any". +- **Destination** - The destination address for the rule. Default is "any". +- **DestinationPort** - The destination port for the rule. +- **Action** - The action to take (pass, block, reject). Default is "pass". +- **Description** - A description for the rule. +- **Enabled** - Whether the rule is enabled. Default is true. +- **Log** - Whether to log matches for this rule. Default is false. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Create a rule to allow HTTP traffic +New-OPNSenseFirewallRule -Interface "lan" -Protocol "tcp" -Destination "any" -DestinationPort "80" -Description "Allow HTTP" + +# Create a rule to block outgoing SMTP traffic +New-OPNSenseFirewallRule -Interface "lan" -Direction "out" -Protocol "tcp" -DestinationPort "25" -Action "block" -Description "Block outgoing SMTP" -Log +``` + +### Set-OPNSenseFirewallRule + +Updates an existing firewall rule on an OPNSense firewall. + +#### Syntax + +```powershell +Set-OPNSenseFirewallRule + -Uuid + [-Interface ] + [-Direction ] + [-Protocol ] + [-Source ] + [-SourcePort ] + [-Destination ] + [-DestinationPort ] + [-Action ] + [-Description ] + [-Enabled ] + [-Log ] + [-Force] + [-PassThru] +``` + +#### Parameters + +- **Uuid** - The UUID of the firewall rule to update. +- **Interface** - The interface for the rule. +- **Direction** - The direction for the rule (in, out). +- **Protocol** - The protocol for the rule. +- **Source** - The source address for the rule. +- **SourcePort** - The source port for the rule. +- **Destination** - The destination address for the rule. +- **DestinationPort** - The destination port for the rule. +- **Action** - The action to take (pass, block, reject). +- **Description** - A description for the rule. +- **Enabled** - Whether the rule is enabled. +- **Log** - Whether to log matches for this rule. +- **Force** - Suppresses the confirmation prompt. +- **PassThru** - Returns the updated rule. + +#### Examples + +```powershell +# Update a firewall rule's description +Set-OPNSenseFirewallRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -Description "Updated HTTP rule" + +# Update a firewall rule's destination port +Set-OPNSenseFirewallRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -DestinationPort "8080" +``` + +### Remove-OPNSenseFirewallRule + +Removes a firewall rule from an OPNSense firewall. + +#### Syntax + +```powershell +Remove-OPNSenseFirewallRule + -Uuid + [-Force] +``` + +#### Parameters + +- **Uuid** - The UUID of the firewall rule to remove. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Remove a firewall rule +Remove-OPNSenseFirewallRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" + +# Remove a firewall rule without confirmation +Remove-OPNSenseFirewallRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -Force +``` + +### Enable-OPNSenseFirewallRule + +Enables a firewall rule on an OPNSense firewall. + +#### Syntax + +```powershell +Enable-OPNSenseFirewallRule + -Uuid + [-Force] + [-PassThru] +``` + +#### Parameters + +- **Uuid** - The UUID of the firewall rule to enable. +- **Force** - Suppresses the confirmation prompt. +- **PassThru** - Returns the updated rule. + +#### Examples + +```powershell +# Enable a firewall rule +Enable-OPNSenseFirewallRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" +``` + +### Disable-OPNSenseFirewallRule + +Disables a firewall rule on an OPNSense firewall. + +#### Syntax + +```powershell +Disable-OPNSenseFirewallRule + -Uuid + [-Force] + [-PassThru] +``` + +#### Parameters + +- **Uuid** - The UUID of the firewall rule to disable. +- **Force** - Suppresses the confirmation prompt. +- **PassThru** - Returns the updated rule. + +#### Examples + +```powershell +# Disable a firewall rule +Disable-OPNSenseFirewallRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" +``` + +### Apply-OPNSenseFirewallChanges + +Applies pending firewall changes on an OPNSense firewall. + +#### Syntax + +```powershell +Apply-OPNSenseFirewallChanges + [-Force] +``` + +#### Parameters + +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Apply firewall changes +Apply-OPNSenseFirewallChanges + +# Apply firewall changes without confirmation +Apply-OPNSenseFirewallChanges -Force +``` + +## NAT Management + +### Get-OPNSenseNatRule + +Retrieves NAT rules from an OPNSense firewall. + +#### Syntax + +```powershell +Get-OPNSenseNatRule + [-Uuid ] + [-Type ] + [-Interface ] + [-Description ] +``` + +#### Parameters + +- **Uuid** - The UUID of a specific NAT rule to retrieve. +- **Type** - Filter rules by type (port_forward, outbound, 1to1). +- **Interface** - Filter rules by interface. +- **Description** - Filter rules by description. + +#### Examples + +```powershell +# Get all NAT rules +Get-OPNSenseNatRule + +# Get a specific NAT rule by UUID +Get-OPNSenseNatRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" + +# Get all port forwarding rules +Get-OPNSenseNatRule -Type "port_forward" +``` + +### New-OPNSensePortForwardRule + +Creates a new port forwarding rule on an OPNSense firewall. + +#### Syntax + +```powershell +New-OPNSensePortForwardRule + -Interface + -Protocol + [-Source ] + [-SourcePort ] + [-Destination ] + -DestinationPort + -TargetIP + -TargetPort + [-Description ] + [-NatReflection ] + [-FilterRuleAssociation ] + [-Enabled ] + [-Log ] + [-Force] +``` + +#### Parameters + +- **Interface** - The interface for the rule. +- **Protocol** - The protocol for the rule (tcp, udp, tcp/udp). +- **Source** - The source address for the rule. Default is "any". +- **SourcePort** - The source port for the rule. Default is "any". +- **Destination** - The destination address for the rule. Default is "wanip". +- **DestinationPort** - The destination port for the rule. +- **TargetIP** - The target IP address for the rule. +- **TargetPort** - The target port for the rule. +- **Description** - A description for the rule. +- **NatReflection** - The NAT reflection mode (enable, disable, purenat). Default is "enable". +- **FilterRuleAssociation** - Whether to create an associated filter rule. Default is true. +- **Enabled** - Whether the rule is enabled. Default is true. +- **Log** - Whether to log matches for this rule. Default is false. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Create a basic port forwarding rule +New-OPNSensePortForwardRule -Interface "wan" -Protocol "tcp" -DestinationPort "80" -TargetIP "192.168.1.100" -TargetPort "80" -Description "Web Server" + +# Create a port forwarding rule with custom source and destination +New-OPNSensePortForwardRule -Interface "wan" -Protocol "tcp" -Source "203.0.113.0/24" -DestinationPort "443" -TargetIP "192.168.1.100" -TargetPort "443" -Description "Secure Web Server" +``` + +### Apply-OPNSenseNatChanges + +Applies pending NAT changes on an OPNSense firewall. + +#### Syntax + +```powershell +Apply-OPNSenseNatChanges + [-Force] +``` + +#### Parameters + +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Apply NAT changes +Apply-OPNSenseNatChanges + +# Apply NAT changes without confirmation +Apply-OPNSenseNatChanges -Force +``` + +## Interface Management + +### Get-OPNSenseInterface + +Retrieves interface configurations from an OPNSense firewall. + +#### Syntax + +```powershell +Get-OPNSenseInterface + [-Name ] + [-Type ] + [-Enabled ] +``` + +#### Parameters + +- **Name** - The name of a specific interface to retrieve. +- **Type** - Filter interfaces by type (e.g., physical, vlan, bridge). +- **Enabled** - Filter interfaces by enabled status. + +#### Examples + +```powershell +# Get all interfaces +Get-OPNSenseInterface + +# Get a specific interface by name +Get-OPNSenseInterface -Name "lan" + +# Get all VLAN interfaces +Get-OPNSenseInterface -Type "vlan" +``` + +### Set-OPNSenseInterface + +Updates an existing interface on an OPNSense firewall. + +#### Syntax + +```powershell +Set-OPNSenseInterface + -Name + [-Description ] + [-IPv4Address ] + [-IPv4Subnet ] + [-IPv6Address ] + [-IPv6Subnet ] + [-Enabled ] + [-BlockPrivate ] + [-BlockBogons ] + [-Force] + [-PassThru] +``` + +#### Parameters + +- **Name** - The name of the interface to update. +- **Description** - A description for the interface. +- **IPv4Address** - The IPv4 address for the interface. +- **IPv4Subnet** - The IPv4 subnet mask for the interface. +- **IPv6Address** - The IPv6 address for the interface. +- **IPv6Subnet** - The IPv6 subnet mask for the interface. +- **Enabled** - Whether the interface is enabled. +- **BlockPrivate** - Whether to block private networks. +- **BlockBogons** - Whether to block bogon networks. +- **Force** - Suppresses the confirmation prompt. +- **PassThru** - Returns the updated interface. + +#### Examples + +```powershell +# Update an interface's description +Set-OPNSenseInterface -Name "lan" -Description "Updated LAN Interface" + +# Update an interface's IPv4 address +Set-OPNSenseInterface -Name "opt1" -IPv4Address "192.168.2.254" -IPv4Subnet "24" +``` + +### Apply-OPNSenseInterfaceChanges + +Applies pending interface changes on an OPNSense firewall. + +#### Syntax + +```powershell +Apply-OPNSenseInterfaceChanges + [-Force] +``` + +#### Parameters + +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Apply interface changes +Apply-OPNSenseInterfaceChanges + +# Apply interface changes without confirmation +Apply-OPNSenseInterfaceChanges -Force +``` + +## VLAN Management + +### Get-OPNSenseVLAN + +Retrieves VLAN configurations from an OPNSense firewall. + +#### Syntax + +```powershell +Get-OPNSenseVLAN + [-Uuid ] +``` + +#### Parameters + +- **Uuid** - The UUID of a specific VLAN to retrieve. + +#### Examples + +```powershell +# Get all VLANs +Get-OPNSenseVLAN + +# Get a specific VLAN by UUID +Get-OPNSenseVLAN -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" +``` + +### New-OPNSenseVLAN + +Creates a new VLAN on an OPNSense firewall. + +#### Syntax + +```powershell +New-OPNSenseVLAN + -Interface + -Tag + [-Priority ] + [-Description ] + [-Force] +``` + +#### Parameters + +- **Interface** - The parent interface for the VLAN. +- **Tag** - The VLAN tag (1-4094). +- **Priority** - The VLAN priority (0-7). +- **Description** - A description for the VLAN. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Create a new VLAN +New-OPNSenseVLAN -Interface "em0" -Tag 10 -Description "Management VLAN" + +# Create a new VLAN with priority +New-OPNSenseVLAN -Interface "em1" -Tag 20 -Priority 5 -Description "Voice VLAN" +``` + +### New-OPNSenseSubnetVLANs + +Creates VLANs for subnets by dividing a network into smaller subnets. + +#### Syntax + +```powershell +New-OPNSenseSubnetVLANs + -ParentInterface + -Network + -SubnetMaskBits + -StartingVlanId + [-VlanIdIncrement ] + [-DescriptionPrefix ] + [-EnableDHCP ] + [-DHCPRangeStart ] + [-DHCPRangeEnd ] + [-DHCPDomain ] + [-DHCPDnsServers ] + [-Force] +``` + +#### Parameters + +- **ParentInterface** - The parent interface for the VLANs. +- **Network** - The network in CIDR notation to divide into subnets. +- **SubnetMaskBits** - The subnet mask bits for the subnets. +- **StartingVlanId** - The starting VLAN ID. +- **VlanIdIncrement** - The increment for VLAN IDs. Default is 1. +- **DescriptionPrefix** - The prefix for VLAN descriptions. Default is "VLAN-". +- **EnableDHCP** - Whether to enable DHCP on the created VLANs. Default is false. +- **DHCPRangeStart** - The starting IP address for the DHCP range. +- **DHCPRangeEnd** - The ending IP address for the DHCP range. +- **DHCPDomain** - The domain name for DHCP clients. +- **DHCPDnsServers** - The DNS servers for DHCP clients. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Create VLANs for subnets +New-OPNSenseSubnetVLANs -ParentInterface "em0" -Network "192.168.0.0/24" -SubnetMaskBits 27 -StartingVlanId 10 + +# Create VLANs for subnets with DHCP enabled +New-OPNSenseSubnetVLANs -ParentInterface "em0" -Network "10.0.0.0/16" -SubnetMaskBits 24 -StartingVlanId 100 -EnableDHCP -DHCPDomain "example.com" -DHCPDnsServers "8.8.8.8","8.8.4.4" +``` + +## Gateway Management + +### Get-OPNSenseGateway + +Retrieves gateway configurations from an OPNSense firewall. + +#### Syntax + +```powershell +Get-OPNSenseGateway + [-Name ] + [-Interface ] + [-Protocol ] +``` + +#### Parameters + +- **Name** - The name of a specific gateway to retrieve. +- **Interface** - Filter gateways by interface. +- **Protocol** - Filter gateways by protocol (IPv4, IPv6). + +#### Examples + +```powershell +# Get all gateways +Get-OPNSenseGateway + +# Get a specific gateway by name +Get-OPNSenseGateway -Name "WAN_GATEWAY" + +# Get all gateways for a specific interface +Get-OPNSenseGateway -Interface "wan" +``` + +### New-OPNSenseGateway + +Creates a new gateway on an OPNSense firewall. + +#### Syntax + +```powershell +New-OPNSenseGateway + -Name + -Interface + -IPAddress + [-Description ] + [-Enabled ] + [-Monitor ] + [-MonitorDisable ] + [-Weight ] + [-Protocol ] + [-Force] +``` + +#### Parameters + +- **Name** - The name of the gateway. +- **Interface** - The interface for the gateway. +- **IPAddress** - The IP address of the gateway. +- **Description** - A description for the gateway. +- **Enabled** - Whether the gateway is enabled. Default is true. +- **Monitor** - The IP address to monitor for gateway status. +- **MonitorDisable** - Whether to disable monitoring for this gateway. Default is false. +- **Weight** - The weight of the gateway for load balancing. Default is 1. +- **Protocol** - The protocol for the gateway (IPv4, IPv6). Default is IPv4. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Create a basic gateway +New-OPNSenseGateway -Name "WAN2_GATEWAY" -Interface "opt1" -IPAddress "203.0.113.1" -Description "Secondary WAN Gateway" + +# Create a gateway with monitoring +New-OPNSenseGateway -Name "WAN3_GATEWAY" -Interface "opt2" -IPAddress "198.51.100.1" -Description "Tertiary WAN Gateway" -Monitor "198.51.100.254" +``` + +### Apply-OPNSenseGatewayChanges + +Applies pending gateway changes on an OPNSense firewall. + +#### Syntax + +```powershell +Apply-OPNSenseGatewayChanges + [-Force] +``` + +#### Parameters + +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Apply gateway changes +Apply-OPNSenseGatewayChanges + +# Apply gateway changes without confirmation +Apply-OPNSenseGatewayChanges -Force +``` + +## Route Management + +### Get-OPNSenseRoute + +Retrieves static routes from an OPNSense firewall. + +#### Syntax + +```powershell +Get-OPNSenseRoute + [-Uuid ] + [-Network ] + [-Gateway ] + [-Description ] +``` + +#### Parameters + +- **Uuid** - The UUID of a specific route to retrieve. +- **Network** - Filter routes by network. +- **Gateway** - Filter routes by gateway. +- **Description** - Filter routes by description. + +#### Examples + +```powershell +# Get all static routes +Get-OPNSenseRoute + +# Get a specific route by UUID +Get-OPNSenseRoute -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" + +# Get routes for a specific network +Get-OPNSenseRoute -Network "192.168.100.0/24" +``` + +### New-OPNSenseRoute + +Creates a new static route on an OPNSense firewall. + +#### Syntax + +```powershell +New-OPNSenseRoute + -Network + -Gateway + [-Description ] + [-Disabled ] + [-Force] +``` + +#### Parameters + +- **Network** - The network for the route in CIDR notation. +- **Gateway** - The gateway for the route. +- **Description** - A description for the route. +- **Disabled** - Whether the route is disabled. Default is false. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Create a basic static route +New-OPNSenseRoute -Network "192.168.100.0/24" -Gateway "WAN_GATEWAY" -Description "Remote Office Network" + +# Create a disabled static route +New-OPNSenseRoute -Network "10.0.0.0/8" -Gateway "WAN2_GATEWAY" -Description "Corporate Network" -Disabled +``` + +### Apply-OPNSenseRouteChanges + +Applies pending route changes on an OPNSense firewall. + +#### Syntax + +```powershell +Apply-OPNSenseRouteChanges + [-Force] +``` + +#### Parameters + +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Apply route changes +Apply-OPNSenseRouteChanges + +# Apply route changes without confirmation +Apply-OPNSenseRouteChanges -Force +``` + +## DNS Management + +### Get-OPNSenseDNSServer + +Retrieves DNS server configurations from an OPNSense firewall. + +#### Syntax + +```powershell +Get-OPNSenseDNSServer +``` + +#### Examples + +```powershell +# Get DNS server configuration +Get-OPNSenseDNSServer +``` + +### Set-OPNSenseDNSServer + +Updates the DNS server configuration on an OPNSense firewall. + +#### Syntax + +```powershell +Set-OPNSenseDNSServer + [-Enabled ] + [-ListenIPs ] + [-Port ] + [-DNSSECEnabled ] + [-ForwardingEnabled ] + [-ForwardingServers ] + [-CacheEnabled ] + [-CacheSize ] + [-PrefetchEnabled ] + [-PrefetchDomains ] + [-Force] + [-PassThru] +``` + +#### Parameters + +- **Enabled** - Whether the DNS server is enabled. +- **ListenIPs** - The IP addresses to listen on. +- **Port** - The port to listen on. Default is 53. +- **DNSSECEnabled** - Whether DNSSEC is enabled. +- **ForwardingEnabled** - Whether DNS forwarding is enabled. +- **ForwardingServers** - The DNS servers to forward queries to. +- **CacheEnabled** - Whether DNS caching is enabled. +- **CacheSize** - The size of the DNS cache in MB. +- **PrefetchEnabled** - Whether DNS prefetching is enabled. +- **PrefetchDomains** - Whether to prefetch domains. +- **Force** - Suppresses the confirmation prompt. +- **PassThru** - Returns the updated DNS server configuration. + +#### Examples + +```powershell +# Enable the DNS server +Set-OPNSenseDNSServer -Enabled + +# Configure DNS forwarding +Set-OPNSenseDNSServer -ForwardingEnabled -ForwardingServers "8.8.8.8","8.8.4.4" +``` + +### Apply-OPNSenseDNSChanges + +Applies pending DNS changes on an OPNSense firewall. + +#### Syntax + +```powershell +Apply-OPNSenseDNSChanges + [-Force] +``` + +#### Parameters + +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Apply DNS changes +Apply-OPNSenseDNSChanges + +# Apply DNS changes without confirmation +Apply-OPNSenseDNSChanges -Force +``` + +## DHCP Management + +### Get-OPNSenseDHCPServer + +Retrieves DHCP server configurations from an OPNSense firewall. + +#### Syntax + +```powershell +Get-OPNSenseDHCPServer + [-Interface ] +``` + +#### Parameters + +- **Interface** - The interface to get the DHCP server configuration for. + +#### Examples + +```powershell +# Get all DHCP servers +Get-OPNSenseDHCPServer + +# Get the DHCP server for a specific interface +Get-OPNSenseDHCPServer -Interface "lan" +``` + +### New-OPNSenseDHCPServer + +Creates a new DHCP server on an OPNSense firewall. + +#### Syntax + +```powershell +New-OPNSenseDHCPServer + -Interface + -RangeFrom + -RangeTo + [-Domain ] + [-DnsServers ] + [-GatewayIP ] + [-LeaseTime ] + [-DenyUnknownClients ] + [-IgnoreClientIdentifiers ] + [-Enabled ] + [-Force] +``` + +#### Parameters + +- **Interface** - The interface to create the DHCP server on. +- **RangeFrom** - The starting IP address of the DHCP range. +- **RangeTo** - The ending IP address of the DHCP range. +- **Domain** - The domain name to provide to DHCP clients. +- **DnsServers** - The DNS servers to provide to DHCP clients. +- **GatewayIP** - The gateway IP address to provide to DHCP clients. +- **LeaseTime** - The lease time in seconds. Default is 86400 (1 day). +- **DenyUnknownClients** - Whether to deny unknown clients. Default is false. +- **IgnoreClientIdentifiers** - Whether to ignore client identifiers. Default is false. +- **Enabled** - Whether the DHCP server is enabled. Default is true. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Create a basic DHCP server +New-OPNSenseDHCPServer -Interface "lan" -RangeFrom "192.168.1.100" -RangeTo "192.168.1.200" -Domain "example.com" -DnsServers "192.168.1.1","8.8.8.8" -GatewayIP "192.168.1.1" + +# Create a DHCP server with advanced options +New-OPNSenseDHCPServer -Interface "opt1" -RangeFrom "10.0.0.100" -RangeTo "10.0.0.200" -Domain "internal.example.com" -DnsServers "10.0.0.1","8.8.4.4" -GatewayIP "10.0.0.1" -LeaseTime 43200 -DenyUnknownClients +``` + +### Get-OPNSenseDHCPLease + +Retrieves DHCP leases from an OPNSense firewall. + +#### Syntax + +```powershell +Get-OPNSenseDHCPLease + [-Interface ] + [-MACAddress ] + [-IPAddress ] + [-Hostname ] + [-Status ] +``` + +#### Parameters + +- **Interface** - The interface to get DHCP leases for. +- **MACAddress** - Filter leases by MAC address. +- **IPAddress** - Filter leases by IP address. +- **Hostname** - Filter leases by hostname. +- **Status** - Filter leases by status (active, expired, etc.). + +#### Examples + +```powershell +# Get all DHCP leases +Get-OPNSenseDHCPLease + +# Get DHCP leases for a specific interface +Get-OPNSenseDHCPLease -Interface "lan" + +# Get DHCP leases for a specific MAC address +Get-OPNSenseDHCPLease -MACAddress "00:11:22:33:44:55" +``` + +### Apply-OPNSenseDHCPChanges + +Applies pending DHCP changes on an OPNSense firewall. + +#### Syntax + +```powershell +Apply-OPNSenseDHCPChanges + [-Force] +``` + +#### Parameters + +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Apply DHCP changes +Apply-OPNSenseDHCPChanges + +# Apply DHCP changes without confirmation +Apply-OPNSenseDHCPChanges -Force +``` + +## Alias Management + +### Get-OPNSenseAlias + +Retrieves aliases from an OPNSense firewall. + +#### Syntax + +```powershell +Get-OPNSenseAlias + [-Name ] + [-Type ] +``` + +#### Parameters + +- **Name** - The name of a specific alias to retrieve. +- **Type** - Filter aliases by type (host, network, port, url, urltable, geoip). + +#### Examples + +```powershell +# Get all aliases +Get-OPNSenseAlias + +# Get a specific alias by name +Get-OPNSenseAlias -Name "WebServers" + +# Get all network aliases +Get-OPNSenseAlias -Type "network" +``` + +### New-OPNSenseAlias + +Creates a new alias on an OPNSense firewall. + +#### Syntax + +```powershell +New-OPNSenseAlias + -Name + -Type + -Content + [-Description ] + [-Enabled ] + [-UpdateFrequency ] + [-Force] +``` + +#### Parameters + +- **Name** - The name of the alias. +- **Type** - The type of the alias (host, network, port, url, urltable, geoip). +- **Content** - The content of the alias (comma-separated list of values). +- **Description** - A description for the alias. +- **Enabled** - Whether the alias is enabled. Default is true. +- **UpdateFrequency** - The update frequency for URL and URL table aliases (in days). +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Create a host alias +New-OPNSenseAlias -Name "WebServers" -Type "host" -Content "192.168.1.10,192.168.1.11,192.168.1.12" -Description "Web Servers" + +# Create a network alias +New-OPNSenseAlias -Name "InternalNetworks" -Type "network" -Content "192.168.1.0/24,192.168.2.0/24" -Description "Internal Networks" + +# Create a URL table alias +New-OPNSenseAlias -Name "BlockList" -Type "urltable" -Content "https://www.example.com/blocklist.txt" -Description "Malicious IPs" -UpdateFrequency 1 +``` + +### Apply-OPNSenseAliasChanges + +Applies pending alias changes on an OPNSense firewall. + +#### Syntax + +```powershell +Apply-OPNSenseAliasChanges + [-Force] +``` + +#### Parameters + +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Apply alias changes +Apply-OPNSenseAliasChanges + +# Apply alias changes without confirmation +Apply-OPNSenseAliasChanges -Force +``` + +## Network Utilities + +### Invoke-OPNSenseNetworkCalculation + +Performs various network calculations on IP networks. + +#### Syntax + +```powershell +Invoke-OPNSenseNetworkCalculation + -Network + -Operation + [-PrefixLength ] + [-SubnetCount ] + [-IpAddress ] + [-AdditionalNetworks ] +``` + +#### Parameters + +- **Network** - The network in CIDR notation to perform calculations on. +- **Operation** - The operation to perform (Info, Subnet, SubnetByCount, Contains, Overlaps, Supernet, SupernetSummarize). +- **PrefixLength** - The prefix length to use for subnetting. +- **SubnetCount** - The number of subnets to create when using SubnetByCount. +- **IpAddress** - The IP address to check when using Contains. +- **AdditionalNetworks** - Additional networks to use with Overlaps, Supernet, or SupernetSummarize. + +#### Examples + +```powershell +# Show network information +Invoke-OPNSenseNetworkCalculation -Network "192.168.1.0/24" -Operation Info + +# Subnet a network into /27 networks +Invoke-OPNSenseNetworkCalculation -Network "10.0.0.0/24" -Operation Subnet -PrefixLength 27 + +# Check if an IP is within a network +Invoke-OPNSenseNetworkCalculation -Network "192.168.1.0/24" -Operation Contains -IpAddress "192.168.1.100" +``` + +### ConvertTo-OPNSenseNetworkNotation + +Converts between different network notation formats. + +#### Syntax + +```powershell +ConvertTo-OPNSenseNetworkNotation + [-CIDR ] + [-SubnetMask ] +``` + +#### Parameters + +- **CIDR** - The CIDR prefix length to convert to a subnet mask. +- **SubnetMask** - The subnet mask to convert to a CIDR prefix length. + +#### Examples + +```powershell +# Convert CIDR to subnet mask +ConvertTo-OPNSenseNetworkNotation -CIDR 24 # Returns "255.255.255.0" + +# Convert subnet mask to CIDR +ConvertTo-OPNSenseNetworkNotation -SubnetMask "255.255.255.0" # Returns 24 +``` + +## Tailscale Integration + +### Get-OPNSenseTailscaleStatus + +Retrieves the status of Tailscale on an OPNSense firewall. + +#### Syntax + +```powershell +Get-OPNSenseTailscaleStatus +``` + +#### Examples + +```powershell +# Get Tailscale status +Get-OPNSenseTailscaleStatus +``` + +### Install-OPNSenseTailscale + +Installs the Tailscale plugin on an OPNSense firewall. + +#### Syntax + +```powershell +Install-OPNSenseTailscale + [-Force] +``` + +#### Parameters + +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Install Tailscale +Install-OPNSenseTailscale + +# Install Tailscale without confirmation +Install-OPNSenseTailscale -Force +``` + +### Enable-OPNSenseTailscale + +Enables Tailscale on an OPNSense firewall. + +#### Syntax + +```powershell +Enable-OPNSenseTailscale + -AuthKey + [-Hostname ] + [-AcceptDNS ] + [-AcceptRoutes ] + [-AdvertiseRoutes ] + [-ExitNode ] + [-Force] + [-PassThru] +``` + +#### Parameters + +- **AuthKey** - The Tailscale authentication key. +- **Hostname** - The hostname to use for the Tailscale node. +- **AcceptDNS** - Whether to accept DNS settings from Tailscale. +- **AcceptRoutes** - Whether to accept routes from Tailscale. +- **AdvertiseRoutes** - The routes to advertise to Tailscale. +- **ExitNode** - Whether to use this node as an exit node. +- **Force** - Suppresses the confirmation prompt. +- **PassThru** - Returns the Tailscale status. + +#### Examples + +```powershell +# Enable Tailscale with basic settings +Enable-OPNSenseTailscale -AuthKey "tskey-auth-abcdef123456" + +# Enable Tailscale with route advertisement +Enable-OPNSenseTailscale -AuthKey "tskey-auth-abcdef123456" -AdvertiseRoutes "192.168.1.0/24,10.0.0.0/8" +``` + +## Cron Job Management + +### Get-OPNSenseCronJob + +Retrieves cron jobs from an OPNSense firewall. + +#### Syntax + +```powershell +Get-OPNSenseCronJob + [-Uuid ] + [-Description ] + [-Command ] +``` + +#### Parameters + +- **Uuid** - The UUID of a specific cron job to retrieve. +- **Description** - Filter cron jobs by description. +- **Command** - Filter cron jobs by command. + +#### Examples + +```powershell +# Get all cron jobs +Get-OPNSenseCronJob + +# Get a specific cron job by UUID +Get-OPNSenseCronJob -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" + +# Get cron jobs with a specific description +Get-OPNSenseCronJob -Description "Backup" +``` + +### New-OPNSenseCronJob + +Creates a new cron job on an OPNSense firewall. + +#### Syntax + +```powershell +New-OPNSenseCronJob + -Description + -Command + -Parameters + -Minute + -Hour + -Day + -Month + -Weekday + [-Enabled ] + [-Force] +``` + +#### Parameters + +- **Description** - A description for the cron job. +- **Command** - The command to execute. +- **Parameters** - The parameters for the command. +- **Minute** - The minute(s) when the job should run (0-59, *, */5, etc.). +- **Hour** - The hour(s) when the job should run (0-23, *, */2, etc.). +- **Day** - The day(s) of the month when the job should run (1-31, *, */2, etc.). +- **Month** - The month(s) when the job should run (1-12, *, */2, etc.). +- **Weekday** - The weekday(s) when the job should run (0-7, *, etc., where 0 and 7 are Sunday). +- **Enabled** - Whether the cron job is enabled. Default is true. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Create a daily backup cron job +New-OPNSenseCronJob -Description "Daily Backup" -Command "configctl" -Parameters "system backup" -Minute "0" -Hour "2" -Day "*" -Month "*" -Weekday "*" + +# Create a weekly update cron job +New-OPNSenseCronJob -Description "Weekly Update" -Command "configctl" -Parameters "firmware update" -Minute "0" -Hour "3" -Day "*" -Month "*" -Weekday "0" +``` + +### Apply-OPNSenseCronJobChanges + +Applies pending cron job changes on an OPNSense firewall. + +#### Syntax + +```powershell +Apply-OPNSenseCronJobChanges + [-Force] +``` + +#### Parameters + +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Apply cron job changes +Apply-OPNSenseCronJobChanges + +# Apply cron job changes without confirmation +Apply-OPNSenseCronJobChanges -Force +``` diff --git a/docs/Alias-Management.md b/docs/Alias-Management.md new file mode 100644 index 0000000..49319a9 --- /dev/null +++ b/docs/Alias-Management.md @@ -0,0 +1,120 @@ +# Alias Management + +This document describes how to use the alias management cmdlets in the PSOPNSenseAPI module. + +## Overview + +Aliases in OPNSense are named lists of networks, hosts, ports, or URLs that can be used in firewall rules. Using aliases makes it easier to manage firewall rules, as you can update the alias content without having to modify the rules that use it. + +The PSOPNSenseAPI module provides cmdlets for managing aliases: + +- `Get-OPNSenseAlias`: Retrieves aliases from an OPNSense firewall +- `New-OPNSenseAlias`: Creates a new alias on an OPNSense firewall +- `Set-OPNSenseAlias`: Updates an alias on an OPNSense firewall +- `Remove-OPNSenseAlias`: Removes an alias from an OPNSense firewall + +## Alias Types + +OPNSense supports several types of aliases: + +- `host`: A list of IP addresses +- `network`: A list of networks in CIDR notation +- `port`: A list of port numbers +- `url`: A URL that points to a list of IPs or networks +- `urltable`: A URL that points to a table of IPs or networks +- `geoip`: A list of country codes for GeoIP filtering +- `networkgroup`: A group of networks +- `mac`: A list of MAC addresses +- `interface`: A list of network interfaces +- `dynipv6host`: A list of dynamic IPv6 hosts +- `internal`: Special internal aliases +- `external`: Special external aliases + +## Examples + +### Getting Aliases + +```powershell +# Get all aliases +Get-OPNSenseAlias + +# Get a specific alias by UUID +Get-OPNSenseAlias -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" + +# Get a specific alias by name +Get-OPNSenseAlias -Name "WebServers" + +# Get all host aliases +Get-OPNSenseAlias -Type host + +# Get all port aliases +Get-OPNSenseAlias -Type port +``` + +### Creating Aliases + +```powershell +# Create a host alias +New-OPNSenseAlias -Name "WebServers" -Type host -Content "192.168.1.10,192.168.1.11" -Description "Web Servers" -Apply + +# Create a network alias +New-OPNSenseAlias -Name "InternalNetworks" -Type network -Content "192.168.1.0/24,192.168.2.0/24" -Description "Internal Networks" -Apply + +# Create a port alias +New-OPNSenseAlias -Name "WebPorts" -Type port -Content "80,443" -Protocol TCP -Description "Web Ports" -Apply + +# Create a URL alias +New-OPNSenseAlias -Name "BlockList" -Type url -Content "https://example.com/blocklist.txt" -UpdateFrequency 1 -Description "Block List" -Apply +``` + +### Updating Aliases + +```powershell +# Update an alias's content +Set-OPNSenseAlias -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Content "192.168.1.10,192.168.1.11,192.168.1.12" -Apply + +# Update an alias's description +Set-OPNSenseAlias -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Description "Updated description" -Apply + +# Disable an alias +Set-OPNSenseAlias -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Disabled -Apply + +# Enable an alias +Set-OPNSenseAlias -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Enabled -Apply + +# Enable counters for an alias +Set-OPNSenseAlias -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -EnableCounters -Apply +``` + +### Removing Aliases + +```powershell +# Remove an alias by UUID +Remove-OPNSenseAlias -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Apply + +# Remove an alias with confirmation +Remove-OPNSenseAlias -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Confirm -Apply + +# Remove an alias without confirmation +Remove-OPNSenseAlias -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Force -Apply + +# Remove aliases by pipeline +Get-OPNSenseAlias -Type host | Remove-OPNSenseAlias -Force -Apply +``` + +## Using Aliases in Firewall Rules + +Once you have created aliases, you can use them in firewall rules: + +```powershell +# Create a firewall rule using aliases +New-OPNSenseFirewallRule -Description "Allow Web Traffic" -Protocol TCP -SourceNet "InternalNetworks" -DestinationPort "WebPorts" -Action pass -Apply +``` + +## Best Practices + +1. **Use descriptive names**: Choose alias names that clearly indicate their purpose. +2. **Use aliases for groups of objects**: Aliases are most useful when they group related objects. +3. **Keep aliases organized**: Use a consistent naming convention for aliases. +4. **Document aliases**: Use the description field to document the purpose of the alias. +5. **Apply changes**: Always use the `-Apply` parameter to apply changes immediately, or apply them later using `Apply-OPNSenseFirewallChanges`. diff --git a/docs/Cron-Job-Management.md b/docs/Cron-Job-Management.md new file mode 100644 index 0000000..9aa3a38 --- /dev/null +++ b/docs/Cron-Job-Management.md @@ -0,0 +1,326 @@ +# Cron Job Management + +This component provides cmdlets for managing cron jobs on OPNSense firewalls. + +## Overview + +The Cron Job Management component allows you to configure and manage scheduled tasks (cron jobs) on OPNSense firewalls. It provides cmdlets for creating, viewing, modifying, and deleting cron jobs. + +## Cmdlets + +### Get-OPNSenseCronJob + +Retrieves cron jobs from an OPNSense firewall. + +#### Parameters + +- **Uuid** - The UUID of a specific cron job to retrieve. If not specified, all cron jobs are returned. +- **Description** - Filter cron jobs by description. +- **Command** - Filter cron jobs by command. + +#### Examples + +```powershell +# Get all cron jobs +Get-OPNSenseCronJob + +# Get a specific cron job by UUID +Get-OPNSenseCronJob -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" + +# Get cron jobs with a specific description +Get-OPNSenseCronJob -Description "Backup" + +# Get cron jobs with a specific command +Get-OPNSenseCronJob -Command "configctl" +``` + +### New-OPNSenseCronJob + +Creates a new cron job on an OPNSense firewall. + +#### Parameters + +- **Description** - A description for the cron job. +- **Command** - The command to execute. +- **Parameters** - The parameters for the command. +- **Minute** - The minute(s) when the job should run (0-59, *, */5, etc.). +- **Hour** - The hour(s) when the job should run (0-23, *, */2, etc.). +- **Day** - The day(s) of the month when the job should run (1-31, *, */2, etc.). +- **Month** - The month(s) when the job should run (1-12, *, */2, etc.). +- **Weekday** - The weekday(s) when the job should run (0-7, *, etc., where 0 and 7 are Sunday). +- **Enabled** - Whether the cron job is enabled. Default is true. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Create a daily backup cron job +New-OPNSenseCronJob -Description "Daily Backup" -Command "configctl" -Parameters "system backup" -Minute "0" -Hour "2" -Day "*" -Month "*" -Weekday "*" + +# Create a weekly update cron job +New-OPNSenseCronJob -Description "Weekly Update" -Command "configctl" -Parameters "firmware update" -Minute "0" -Hour "3" -Day "*" -Month "*" -Weekday "0" + +# Create a monthly log rotation cron job +New-OPNSenseCronJob -Description "Monthly Log Rotation" -Command "configctl" -Parameters "system rotate" -Minute "0" -Hour "4" -Day "1" -Month "*" -Weekday "*" + +# Create a disabled cron job +New-OPNSenseCronJob -Description "Temporary Job" -Command "configctl" -Parameters "system reboot" -Minute "0" -Hour "5" -Day "*" -Month "*" -Weekday "*" -Enabled:$false +``` + +### Set-OPNSenseCronJob + +Updates an existing cron job on an OPNSense firewall. + +#### Parameters + +- **Uuid** - The UUID of the cron job to update. +- **Description** - A description for the cron job. +- **Command** - The command to execute. +- **Parameters** - The parameters for the command. +- **Minute** - The minute(s) when the job should run (0-59, *, */5, etc.). +- **Hour** - The hour(s) when the job should run (0-23, *, */2, etc.). +- **Day** - The day(s) of the month when the job should run (1-31, *, */2, etc.). +- **Month** - The month(s) when the job should run (1-12, *, */2, etc.). +- **Weekday** - The weekday(s) when the job should run (0-7, *, etc., where 0 and 7 are Sunday). +- **Enabled** - Whether the cron job is enabled. +- **Force** - Suppresses the confirmation prompt. +- **PassThru** - Returns the updated cron job. + +#### Examples + +```powershell +# Update a cron job's description +Set-OPNSenseCronJob -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -Description "Updated Backup Job" + +# Update a cron job's schedule +Set-OPNSenseCronJob -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -Minute "30" -Hour "3" + +# Update a cron job's command and parameters +Set-OPNSenseCronJob -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -Command "configctl" -Parameters "system backup full" + +# Disable a cron job +Set-OPNSenseCronJob -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -Enabled:$false + +# Update multiple properties of a cron job +Set-OPNSenseCronJob -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -Description "Full Backup" -Command "configctl" -Parameters "system backup full" -Minute "0" -Hour "1" -PassThru +``` + +### Remove-OPNSenseCronJob + +Removes a cron job from an OPNSense firewall. + +#### Parameters + +- **Uuid** - The UUID of the cron job to remove. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Remove a cron job +Remove-OPNSenseCronJob -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" + +# Remove a cron job without confirmation +Remove-OPNSenseCronJob -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -Force +``` + +### Enable-OPNSenseCronJob + +Enables a cron job on an OPNSense firewall. + +#### Parameters + +- **Uuid** - The UUID of the cron job to enable. +- **Force** - Suppresses the confirmation prompt. +- **PassThru** - Returns the updated cron job. + +#### Examples + +```powershell +# Enable a cron job +Enable-OPNSenseCronJob -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" + +# Enable a cron job and return the updated job +Enable-OPNSenseCronJob -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -PassThru +``` + +### Disable-OPNSenseCronJob + +Disables a cron job on an OPNSense firewall. + +#### Parameters + +- **Uuid** - The UUID of the cron job to disable. +- **Force** - Suppresses the confirmation prompt. +- **PassThru** - Returns the updated cron job. + +#### Examples + +```powershell +# Disable a cron job +Disable-OPNSenseCronJob -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" + +# Disable a cron job and return the updated job +Disable-OPNSenseCronJob -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -PassThru +``` + +### Apply-OPNSenseCronJobChanges + +Applies pending cron job changes on an OPNSense firewall. + +#### Parameters + +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Apply cron job changes +Apply-OPNSenseCronJobChanges + +# Apply cron job changes without confirmation +Apply-OPNSenseCronJobChanges -Force +``` + +## Common Scenarios + +### Basic Backup Schedule + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Create a daily backup cron job +New-OPNSenseCronJob -Description "Daily Configuration Backup" -Command "configctl" -Parameters "system backup" -Minute "0" -Hour "2" -Day "*" -Month "*" -Weekday "*" -Force + +# Apply the changes +Apply-OPNSenseCronJobChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Comprehensive Maintenance Schedule + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Define maintenance jobs +$cronJobs = @( + @{ Description = "Daily Backup"; Command = "configctl"; Parameters = "system backup"; Minute = "0"; Hour = "2"; Day = "*"; Month = "*"; Weekday = "*" }, + @{ Description = "Weekly Full Backup"; Command = "configctl"; Parameters = "system backup full"; Minute = "0"; Hour = "3"; Day = "*"; Month = "*"; Weekday = "0" }, + @{ Description = "Daily Log Rotation"; Command = "configctl"; Parameters = "system rotate"; Minute = "30"; Hour = "2"; Day = "*"; Month = "*"; Weekday = "*" }, + @{ Description = "Weekly Update Check"; Command = "configctl"; Parameters = "firmware check"; Minute = "0"; Hour = "4"; Day = "*"; Month = "*"; Weekday = "1" }, + @{ Description = "Monthly Cleanup"; Command = "configctl"; Parameters = "system cleanup"; Minute = "0"; Hour = "5"; Day = "1"; Month = "*"; Weekday = "*" } +) + +# Create cron jobs +foreach ($job in $cronJobs) { + New-OPNSenseCronJob -Description $job.Description -Command $job.Command -Parameters $job.Parameters -Minute $job.Minute -Hour $job.Hour -Day $job.Day -Month $job.Month -Weekday $job.Weekday -Force +} + +# Apply the changes +Apply-OPNSenseCronJobChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Managing Existing Cron Jobs + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get all cron jobs +$cronJobs = Get-OPNSenseCronJob + +# Update backup jobs to run at a different time +$backupJobs = $cronJobs | Where-Object { $_.Description -like "*Backup*" } +foreach ($job in $backupJobs) { + Set-OPNSenseCronJob -Uuid $job.Uuid -Hour "4" -Force + Write-Output "Updated job: $($job.Description) to run at 4:00 AM" +} + +# Disable temporary jobs +$tempJobs = $cronJobs | Where-Object { $_.Description -like "*Temporary*" } +foreach ($job in $tempJobs) { + Disable-OPNSenseCronJob -Uuid $job.Uuid -Force + Write-Output "Disabled job: $($job.Description)" +} + +# Remove obsolete jobs +$obsoleteJobs = $cronJobs | Where-Object { $_.Description -like "*Obsolete*" } +foreach ($job in $obsoleteJobs) { + Remove-OPNSenseCronJob -Uuid $job.Uuid -Force + Write-Output "Removed job: $($job.Description)" +} + +# Apply the changes +Apply-OPNSenseCronJobChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Scheduling Network Tests + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Define network test jobs +$testJobs = @( + @{ Description = "Hourly Gateway Ping"; Command = "configctl"; Parameters = "gateway pingall"; Minute = "0"; Hour = "*"; Day = "*"; Month = "*"; Weekday = "*" }, + @{ Description = "Daily DNS Test"; Command = "configctl"; Parameters = "system dns test"; Minute = "15"; Hour = "*/6"; Day = "*"; Month = "*"; Weekday = "*" }, + @{ Description = "Weekly Network Diagnostics"; Command = "configctl"; Parameters = "system diagnostics"; Minute = "30"; Hour = "5"; Day = "*"; Month = "*"; Weekday = "0" } +) + +# Create cron jobs +foreach ($job in $testJobs) { + New-OPNSenseCronJob -Description $job.Description -Command $job.Command -Parameters $job.Parameters -Minute $job.Minute -Hour $job.Hour -Day $job.Day -Month $job.Month -Weekday $job.Weekday -Force +} + +# Apply the changes +Apply-OPNSenseCronJobChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Scheduling Firewall Reboots + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Create a monthly reboot cron job +New-OPNSenseCronJob -Description "Monthly Maintenance Reboot" -Command "configctl" -Parameters "system reboot" -Minute "0" -Hour "3" -Day "15" -Month "*" -Weekday "*" -Force + +# Apply the changes +Apply-OPNSenseCronJobChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +## Notes + +- Cron job changes are not applied until you call `Apply-OPNSenseCronJobChanges`. +- The cron job schedule uses the standard cron format: + - **Minute**: 0-59, *, */5 (every 5 minutes), etc. + - **Hour**: 0-23, *, */2 (every 2 hours), etc. + - **Day**: 1-31, *, */2 (every 2 days), etc. + - **Month**: 1-12, *, */2 (every 2 months), etc. + - **Weekday**: 0-7, * (where 0 and 7 are Sunday), etc. +- The `*` character means "every" (e.g., every minute, every hour, etc.). +- The `*/n` format means "every n" (e.g., every 5 minutes, every 2 hours, etc.). +- Multiple values can be specified with commas (e.g., "1,3,5" for 1st, 3rd, and 5th). +- Ranges can be specified with hyphens (e.g., "1-5" for 1st through 5th). +- The `Command` parameter typically uses `configctl` for OPNSense system commands. +- Consider using the `-PassThru` parameter when updating cron jobs to verify the changes. +- Cron job descriptions should be descriptive and follow a consistent naming convention. +- Be careful when scheduling system reboots, as they will interrupt all services. +- Cron jobs run with system privileges, so they can execute any command available to the system. +- For complex schedules, consider using multiple cron jobs instead of a single job with a complex schedule. diff --git a/docs/DHCP-Management.md b/docs/DHCP-Management.md new file mode 100644 index 0000000..e3c2aa0 --- /dev/null +++ b/docs/DHCP-Management.md @@ -0,0 +1,393 @@ +# DHCP Management + +This component provides cmdlets for managing DHCP servers and leases on OPNSense firewalls. + +## Overview + +The DHCP Management component allows you to configure and manage DHCP servers on OPNSense firewalls. It provides cmdlets for creating, viewing, modifying, and deleting DHCP servers, as well as managing DHCP leases and static mappings. + +## Cmdlets + +### Get-OPNSenseDHCPServer + +Retrieves DHCP server configurations from an OPNSense firewall. + +#### Parameters + +- **Interface** - The interface to get the DHCP server configuration for. If not specified, all DHCP servers are returned. + +#### Examples + +```powershell +# Get all DHCP servers +Get-OPNSenseDHCPServer + +# Get the DHCP server for a specific interface +Get-OPNSenseDHCPServer -Interface "lan" +``` + +### New-OPNSenseDHCPServer + +Creates a new DHCP server on an OPNSense firewall. + +#### Parameters + +- **Interface** - The interface to create the DHCP server on. +- **Enabled** - Whether the DHCP server is enabled. Default is true. +- **RangeFrom** - The starting IP address of the DHCP range. +- **RangeTo** - The ending IP address of the DHCP range. +- **Domain** - The domain name to provide to DHCP clients. +- **DnsServers** - The DNS servers to provide to DHCP clients. +- **GatewayIP** - The gateway IP address to provide to DHCP clients. +- **LeaseTime** - The lease time in seconds. Default is 86400 (1 day). +- **DenyUnknownClients** - Whether to deny unknown clients. Default is false. +- **IgnoreClientIdentifiers** - Whether to ignore client identifiers. Default is false. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Create a basic DHCP server +New-OPNSenseDHCPServer -Interface "lan" -RangeFrom "192.168.1.100" -RangeTo "192.168.1.200" -Domain "example.com" -DnsServers "192.168.1.1","8.8.8.8" -GatewayIP "192.168.1.1" + +# Create a DHCP server with advanced options +New-OPNSenseDHCPServer -Interface "opt1" -RangeFrom "10.0.0.100" -RangeTo "10.0.0.200" -Domain "internal.example.com" -DnsServers "10.0.0.1","8.8.4.4" -GatewayIP "10.0.0.1" -LeaseTime 43200 -DenyUnknownClients -Force +``` + +### Set-OPNSenseDHCPServer + +Updates an existing DHCP server on an OPNSense firewall. + +#### Parameters + +- **Interface** - The interface of the DHCP server to update. +- **Enabled** - Whether the DHCP server is enabled. +- **RangeFrom** - The starting IP address of the DHCP range. +- **RangeTo** - The ending IP address of the DHCP range. +- **Domain** - The domain name to provide to DHCP clients. +- **DnsServers** - The DNS servers to provide to DHCP clients. +- **GatewayIP** - The gateway IP address to provide to DHCP clients. +- **LeaseTime** - The lease time in seconds. +- **DenyUnknownClients** - Whether to deny unknown clients. +- **IgnoreClientIdentifiers** - Whether to ignore client identifiers. +- **Force** - Suppresses the confirmation prompt. +- **PassThru** - Returns the updated DHCP server. + +#### Examples + +```powershell +# Update a DHCP server's range +Set-OPNSenseDHCPServer -Interface "lan" -RangeFrom "192.168.1.50" -RangeTo "192.168.1.150" + +# Update a DHCP server's DNS servers +Set-OPNSenseDHCPServer -Interface "opt1" -DnsServers "10.0.0.1","1.1.1.1" + +# Update multiple properties of a DHCP server +Set-OPNSenseDHCPServer -Interface "lan" -Domain "updated.example.com" -LeaseTime 43200 -DenyUnknownClients -PassThru +``` + +### Remove-OPNSenseDHCPServer + +Removes a DHCP server from an OPNSense firewall. + +#### Parameters + +- **Interface** - The interface of the DHCP server to remove. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Remove a DHCP server +Remove-OPNSenseDHCPServer -Interface "opt1" + +# Remove a DHCP server without confirmation +Remove-OPNSenseDHCPServer -Interface "opt1" -Force +``` + +### Get-OPNSenseDHCPLease + +Retrieves DHCP leases from an OPNSense firewall. + +#### Parameters + +- **Interface** - The interface to get DHCP leases for. If not specified, leases for all interfaces are returned. +- **MACAddress** - Filter leases by MAC address. +- **IPAddress** - Filter leases by IP address. +- **Hostname** - Filter leases by hostname. +- **Status** - Filter leases by status (active, expired, etc.). + +#### Examples + +```powershell +# Get all DHCP leases +Get-OPNSenseDHCPLease + +# Get DHCP leases for a specific interface +Get-OPNSenseDHCPLease -Interface "lan" + +# Get DHCP leases for a specific MAC address +Get-OPNSenseDHCPLease -MACAddress "00:11:22:33:44:55" + +# Get active DHCP leases +Get-OPNSenseDHCPLease -Status "active" +``` + +### New-OPNSenseDHCPStaticMapping + +Creates a new DHCP static mapping on an OPNSense firewall. + +#### Parameters + +- **Interface** - The interface to create the static mapping on. +- **MACAddress** - The MAC address of the client. +- **IPAddress** - The IP address to assign to the client. +- **Hostname** - The hostname of the client. +- **Description** - A description for the static mapping. +- **Enabled** - Whether the static mapping is enabled. Default is true. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Create a basic static mapping +New-OPNSenseDHCPStaticMapping -Interface "lan" -MACAddress "00:11:22:33:44:55" -IPAddress "192.168.1.50" -Hostname "printer" -Description "Office Printer" + +# Create a static mapping without confirmation +New-OPNSenseDHCPStaticMapping -Interface "opt1" -MACAddress "AA:BB:CC:DD:EE:FF" -IPAddress "10.0.0.10" -Hostname "server" -Description "File Server" -Force +``` + +### Set-OPNSenseDHCPStaticMapping + +Updates an existing DHCP static mapping on an OPNSense firewall. + +#### Parameters + +- **Interface** - The interface of the static mapping to update. +- **MACAddress** - The MAC address of the static mapping to update. +- **IPAddress** - The IP address to assign to the client. +- **Hostname** - The hostname of the client. +- **Description** - A description for the static mapping. +- **Enabled** - Whether the static mapping is enabled. +- **Force** - Suppresses the confirmation prompt. +- **PassThru** - Returns the updated static mapping. + +#### Examples + +```powershell +# Update a static mapping's IP address +Set-OPNSenseDHCPStaticMapping -Interface "lan" -MACAddress "00:11:22:33:44:55" -IPAddress "192.168.1.60" + +# Update a static mapping's description +Set-OPNSenseDHCPStaticMapping -Interface "opt1" -MACAddress "AA:BB:CC:DD:EE:FF" -Description "Updated File Server" + +# Update multiple properties of a static mapping +Set-OPNSenseDHCPStaticMapping -Interface "lan" -MACAddress "00:11:22:33:44:55" -IPAddress "192.168.1.70" -Hostname "new-printer" -Description "New Office Printer" -PassThru +``` + +### Remove-OPNSenseDHCPStaticMapping + +Removes a DHCP static mapping from an OPNSense firewall. + +#### Parameters + +- **Interface** - The interface of the static mapping to remove. +- **MACAddress** - The MAC address of the static mapping to remove. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Remove a static mapping +Remove-OPNSenseDHCPStaticMapping -Interface "lan" -MACAddress "00:11:22:33:44:55" + +# Remove a static mapping without confirmation +Remove-OPNSenseDHCPStaticMapping -Interface "opt1" -MACAddress "AA:BB:CC:DD:EE:FF" -Force +``` + +### Apply-OPNSenseDHCPChanges + +Applies pending DHCP changes on an OPNSense firewall. + +#### Parameters + +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Apply DHCP changes +Apply-OPNSenseDHCPChanges + +# Apply DHCP changes without confirmation +Apply-OPNSenseDHCPChanges -Force +``` + +## Common Scenarios + +### Basic DHCP Configuration + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Create a DHCP server for the LAN interface +New-OPNSenseDHCPServer -Interface "lan" -RangeFrom "192.168.1.100" -RangeTo "192.168.1.200" -Domain "example.com" -DnsServers "192.168.1.1","8.8.8.8" -GatewayIP "192.168.1.1" -Force + +# Apply the changes +Apply-OPNSenseDHCPChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Managing Static Mappings + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Create static mappings for important devices +$staticMappings = @( + @{ MACAddress = "00:11:22:33:44:55"; IPAddress = "192.168.1.10"; Hostname = "printer"; Description = "Office Printer" }, + @{ MACAddress = "AA:BB:CC:DD:EE:FF"; IPAddress = "192.168.1.20"; Hostname = "fileserver"; Description = "File Server" }, + @{ MACAddress = "11:22:33:44:55:66"; IPAddress = "192.168.1.30"; Hostname = "nas"; Description = "Network Storage" } +) + +foreach ($mapping in $staticMappings) { + New-OPNSenseDHCPStaticMapping -Interface "lan" -MACAddress $mapping.MACAddress -IPAddress $mapping.IPAddress -Hostname $mapping.Hostname -Description $mapping.Description -Force +} + +# Apply the changes +Apply-OPNSenseDHCPChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Converting DHCP Leases to Static Mappings + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get active DHCP leases +$leases = Get-OPNSenseDHCPLease -Interface "lan" -Status "active" + +# Convert leases to static mappings +foreach ($lease in $leases) { + New-OPNSenseDHCPStaticMapping -Interface "lan" -MACAddress $lease.MACAddress -IPAddress $lease.IPAddress -Hostname $lease.Hostname -Description "Converted from lease on $(Get-Date -Format 'yyyy-MM-dd')" -Force +} + +# Apply the changes +Apply-OPNSenseDHCPChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Managing Multiple DHCP Servers + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Define DHCP server configurations for different interfaces +$dhcpConfigs = @( + @{ + Interface = "lan"; + RangeFrom = "192.168.1.100"; + RangeTo = "192.168.1.200"; + Domain = "internal.example.com"; + DnsServers = @("192.168.1.1", "8.8.8.8"); + GatewayIP = "192.168.1.1"; + LeaseTime = 86400; + }, + @{ + Interface = "opt1"; + RangeFrom = "10.0.0.100"; + RangeTo = "10.0.0.200"; + Domain = "guest.example.com"; + DnsServers = @("10.0.0.1", "8.8.4.4"); + GatewayIP = "10.0.0.1"; + LeaseTime = 3600; + DenyUnknownClients = $true; + }, + @{ + Interface = "opt2"; + RangeFrom = "172.16.0.100"; + RangeTo = "172.16.0.200"; + Domain = "iot.example.com"; + DnsServers = @("172.16.0.1"); + GatewayIP = "172.16.0.1"; + LeaseTime = 86400; + } +) + +# Create or update DHCP servers +foreach ($config in $dhcpConfigs) { + $existing = Get-OPNSenseDHCPServer -Interface $config.Interface + + if ($existing) { + # Update existing DHCP server + $params = @{ + Interface = $config.Interface + RangeFrom = $config.RangeFrom + RangeTo = $config.RangeTo + Domain = $config.Domain + DnsServers = $config.DnsServers + GatewayIP = $config.GatewayIP + LeaseTime = $config.LeaseTime + Force = $true + } + + if ($config.DenyUnknownClients) { + $params.Add("DenyUnknownClients", $true) + } + + Set-OPNSenseDHCPServer @params + Write-Output "Updated DHCP server for interface $($config.Interface)" + } + else { + # Create new DHCP server + $params = @{ + Interface = $config.Interface + RangeFrom = $config.RangeFrom + RangeTo = $config.RangeTo + Domain = $config.Domain + DnsServers = $config.DnsServers + GatewayIP = $config.GatewayIP + LeaseTime = $config.LeaseTime + Force = $true + } + + if ($config.DenyUnknownClients) { + $params.Add("DenyUnknownClients", $true) + } + + New-OPNSenseDHCPServer @params + Write-Output "Created DHCP server for interface $($config.Interface)" + } +} + +# Apply the changes +Apply-OPNSenseDHCPChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +## Notes + +- DHCP servers are configured per interface on OPNSense firewalls. +- Changes to DHCP servers and static mappings are not applied until you call `Apply-OPNSenseDHCPChanges`. +- When creating a DHCP server, ensure that the range is within the subnet of the interface. +- Static mappings take precedence over dynamic leases. +- The `DenyUnknownClients` option only allows clients with static mappings to receive DHCP leases. +- The `IgnoreClientIdentifiers` option can help with clients that change their identifier between requests. +- DHCP lease times are specified in seconds. Common values are: + - 3600 (1 hour) + - 43200 (12 hours) + - 86400 (1 day) + - 604800 (1 week) +- Consider using the `-PassThru` parameter when updating DHCP servers or static mappings to verify the changes. diff --git a/docs/DNS-Management.md b/docs/DNS-Management.md new file mode 100644 index 0000000..7d0d58a --- /dev/null +++ b/docs/DNS-Management.md @@ -0,0 +1,411 @@ +# DNS Management + +This component provides cmdlets for managing DNS settings on OPNSense firewalls. + +## Overview + +The DNS Management component allows you to configure and manage DNS settings on OPNSense firewalls. It provides cmdlets for configuring DNS servers, DNS forwarders, DNS overrides, and DNS forwarding. + +## Cmdlets + +### Get-OPNSenseDNSServer + +Retrieves DNS server configurations from an OPNSense firewall. + +#### Examples + +```powershell +# Get DNS server configuration +Get-OPNSenseDNSServer +``` + +### Set-OPNSenseDNSServer + +Updates the DNS server configuration on an OPNSense firewall. + +#### Parameters + +- **Enabled** - Whether the DNS server is enabled. +- **ListenIPs** - The IP addresses to listen on. +- **Port** - The port to listen on. Default is 53. +- **DNSSECEnabled** - Whether DNSSEC is enabled. +- **ForwardingEnabled** - Whether DNS forwarding is enabled. +- **ForwardingServers** - The DNS servers to forward queries to. +- **CacheEnabled** - Whether DNS caching is enabled. +- **CacheSize** - The size of the DNS cache in MB. +- **PrefetchEnabled** - Whether DNS prefetching is enabled. +- **PrefetchDomains** - Whether to prefetch domains. +- **Force** - Suppresses the confirmation prompt. +- **PassThru** - Returns the updated DNS server configuration. + +#### Examples + +```powershell +# Enable the DNS server +Set-OPNSenseDNSServer -Enabled + +# Configure DNS forwarding +Set-OPNSenseDNSServer -ForwardingEnabled -ForwardingServers "8.8.8.8","8.8.4.4" + +# Configure DNS caching +Set-OPNSenseDNSServer -CacheEnabled -CacheSize 10 -PrefetchEnabled -PrefetchDomains + +# Configure DNS server with multiple options +Set-OPNSenseDNSServer -Enabled -ListenIPs "192.168.1.1" -Port 53 -DNSSECEnabled -ForwardingEnabled -ForwardingServers "1.1.1.1","1.0.0.1" -CacheEnabled -CacheSize 20 -PrefetchEnabled -PassThru +``` + +### Get-OPNSenseDNSOverride + +Retrieves DNS overrides from an OPNSense firewall. + +#### Parameters + +- **Host** - Filter overrides by host. +- **Domain** - Filter overrides by domain. +- **IP** - Filter overrides by IP address. +- **Description** - Filter overrides by description. + +#### Examples + +```powershell +# Get all DNS overrides +Get-OPNSenseDNSOverride + +# Get DNS overrides for a specific host +Get-OPNSenseDNSOverride -Host "server" + +# Get DNS overrides for a specific domain +Get-OPNSenseDNSOverride -Domain "example.com" +``` + +### New-OPNSenseDNSOverride + +Creates a new DNS override on an OPNSense firewall. + +#### Parameters + +- **Host** - The hostname to override. +- **Domain** - The domain to override. +- **IP** - The IP address to resolve to. +- **Description** - A description for the override. +- **Enabled** - Whether the override is enabled. Default is true. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Create a DNS override for a specific host +New-OPNSenseDNSOverride -Host "server" -Domain "example.com" -IP "192.168.1.10" -Description "Internal Server" + +# Create a DNS override for a wildcard domain +New-OPNSenseDNSOverride -Host "*" -Domain "example.com" -IP "192.168.1.20" -Description "All example.com hosts" + +# Create a DNS override without confirmation +New-OPNSenseDNSOverride -Host "printer" -Domain "example.com" -IP "192.168.1.30" -Description "Office Printer" -Force +``` + +### Set-OPNSenseDNSOverride + +Updates an existing DNS override on an OPNSense firewall. + +#### Parameters + +- **Uuid** - The UUID of the DNS override to update. +- **Host** - The hostname to override. +- **Domain** - The domain to override. +- **IP** - The IP address to resolve to. +- **Description** - A description for the override. +- **Enabled** - Whether the override is enabled. +- **Force** - Suppresses the confirmation prompt. +- **PassThru** - Returns the updated DNS override. + +#### Examples + +```powershell +# Update a DNS override's IP address +Set-OPNSenseDNSOverride -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -IP "192.168.1.15" + +# Update a DNS override's description +Set-OPNSenseDNSOverride -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -Description "Updated Server" + +# Update multiple properties of a DNS override +Set-OPNSenseDNSOverride -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -Host "newserver" -Domain "example.com" -IP "192.168.1.20" -Description "New Server" -PassThru +``` + +### Remove-OPNSenseDNSOverride + +Removes a DNS override from an OPNSense firewall. + +#### Parameters + +- **Uuid** - The UUID of the DNS override to remove. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Remove a DNS override +Remove-OPNSenseDNSOverride -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" + +# Remove a DNS override without confirmation +Remove-OPNSenseDNSOverride -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -Force +``` + +### Get-OPNSenseDNSForwarder + +Retrieves DNS forwarder configurations from an OPNSense firewall. + +#### Examples + +```powershell +# Get DNS forwarder configuration +Get-OPNSenseDNSForwarder +``` + +### Set-OPNSenseDNSForwarder + +Updates the DNS forwarder configuration on an OPNSense firewall. + +#### Parameters + +- **Enabled** - Whether the DNS forwarder is enabled. +- **ListenIPs** - The IP addresses to listen on. +- **Port** - The port to listen on. Default is 53. +- **Interfaces** - The interfaces to listen on. +- **DNSSECEnabled** - Whether DNSSEC is enabled. +- **RegdhcpStatic** - Whether to register DHCP static mappings. +- **RegdhcpDynamic** - Whether to register DHCP leases. +- **StrictBind** - Whether to use strict binding. +- **Force** - Suppresses the confirmation prompt. +- **PassThru** - Returns the updated DNS forwarder configuration. + +#### Examples + +```powershell +# Enable the DNS forwarder +Set-OPNSenseDNSForwarder -Enabled + +# Configure DNS forwarder interfaces +Set-OPNSenseDNSForwarder -Interfaces "lan","opt1" + +# Configure DNS forwarder with DHCP registration +Set-OPNSenseDNSForwarder -RegdhcpStatic -RegdhcpDynamic + +# Configure DNS forwarder with multiple options +Set-OPNSenseDNSForwarder -Enabled -ListenIPs "192.168.1.1" -Port 53 -Interfaces "lan","opt1" -DNSSECEnabled -RegdhcpStatic -RegdhcpDynamic -StrictBind -PassThru +``` + +### Get-OPNSenseDNSForwarding + +Retrieves DNS forwarding configurations from an OPNSense firewall. + +#### Parameters + +- **Domain** - Filter forwarding by domain. +- **Server** - Filter forwarding by server. +- **Description** - Filter forwarding by description. + +#### Examples + +```powershell +# Get all DNS forwarding configurations +Get-OPNSenseDNSForwarding + +# Get DNS forwarding for a specific domain +Get-OPNSenseDNSForwarding -Domain "example.com" +``` + +### New-OPNSenseDNSForwarding + +Creates a new DNS forwarding configuration on an OPNSense firewall. + +#### Parameters + +- **Domain** - The domain to forward. +- **Server** - The server to forward to. +- **Description** - A description for the forwarding. +- **Enabled** - Whether the forwarding is enabled. Default is true. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Create a DNS forwarding for a specific domain +New-OPNSenseDNSForwarding -Domain "example.com" -Server "192.168.1.10" -Description "Internal DNS" + +# Create a DNS forwarding for a domain to multiple servers +New-OPNSenseDNSForwarding -Domain "example.org" -Server "192.168.1.10,192.168.1.11" -Description "Redundant DNS" + +# Create a DNS forwarding without confirmation +New-OPNSenseDNSForwarding -Domain "example.net" -Server "192.168.1.12" -Description "External DNS" -Force +``` + +### Set-OPNSenseDNSForwarding + +Updates an existing DNS forwarding configuration on an OPNSense firewall. + +#### Parameters + +- **Uuid** - The UUID of the DNS forwarding to update. +- **Domain** - The domain to forward. +- **Server** - The server to forward to. +- **Description** - A description for the forwarding. +- **Enabled** - Whether the forwarding is enabled. +- **Force** - Suppresses the confirmation prompt. +- **PassThru** - Returns the updated DNS forwarding. + +#### Examples + +```powershell +# Update a DNS forwarding's server +Set-OPNSenseDNSForwarding -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -Server "192.168.1.15" + +# Update a DNS forwarding's description +Set-OPNSenseDNSForwarding -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -Description "Updated DNS" + +# Update multiple properties of a DNS forwarding +Set-OPNSenseDNSForwarding -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -Domain "new.example.com" -Server "192.168.1.20" -Description "New DNS" -PassThru +``` + +### Remove-OPNSenseDNSForwarding + +Removes a DNS forwarding configuration from an OPNSense firewall. + +#### Parameters + +- **Uuid** - The UUID of the DNS forwarding to remove. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Remove a DNS forwarding +Remove-OPNSenseDNSForwarding -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" + +# Remove a DNS forwarding without confirmation +Remove-OPNSenseDNSForwarding -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -Force +``` + +### Apply-OPNSenseDNSChanges + +Applies pending DNS changes on an OPNSense firewall. + +#### Parameters + +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Apply DNS changes +Apply-OPNSenseDNSChanges + +# Apply DNS changes without confirmation +Apply-OPNSenseDNSChanges -Force +``` + +## Common Scenarios + +### Basic DNS Configuration + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Configure the DNS server +Set-OPNSenseDNSServer -Enabled -ListenIPs "192.168.1.1" -ForwardingEnabled -ForwardingServers "1.1.1.1","1.0.0.1" -CacheEnabled -CacheSize 10 -Force + +# Apply the changes +Apply-OPNSenseDNSChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### DNS Overrides for Internal Services + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Create DNS overrides for internal services +$dnsOverrides = @( + @{ Host = "www"; Domain = "example.com"; IP = "192.168.1.10"; Description = "Internal Web Server" }, + @{ Host = "mail"; Domain = "example.com"; IP = "192.168.1.11"; Description = "Internal Mail Server" }, + @{ Host = "files"; Domain = "example.com"; IP = "192.168.1.12"; Description = "Internal File Server" }, + @{ Host = "*"; Domain = "internal.example.com"; IP = "192.168.1.13"; Description = "Internal Services" } +) + +foreach ($override in $dnsOverrides) { + New-OPNSenseDNSOverride -Host $override.Host -Domain $override.Domain -IP $override.IP -Description $override.Description -Force +} + +# Apply the changes +Apply-OPNSenseDNSChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### DNS Forwarding for Specific Domains + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Configure DNS forwarding for specific domains +$dnsForwarding = @( + @{ Domain = "example.com"; Server = "192.168.1.10"; Description = "Internal Domain" }, + @{ Domain = "partner.com"; Server = "10.0.0.10"; Description = "Partner Domain" }, + @{ Domain = "vendor.com"; Server = "172.16.0.10"; Description = "Vendor Domain" } +) + +foreach ($forwarding in $dnsForwarding) { + New-OPNSenseDNSForwarding -Domain $forwarding.Domain -Server $forwarding.Server -Description $forwarding.Description -Force +} + +# Apply the changes +Apply-OPNSenseDNSChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Managing DNS Overrides + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get all DNS overrides +$overrides = Get-OPNSenseDNSOverride + +# Update all overrides for a specific domain +$overrides | Where-Object { $_.Domain -eq "example.com" } | ForEach-Object { + Set-OPNSenseDNSOverride -Uuid $_.Uuid -IP "192.168.2.$($_.IP.Split('.')[-1])" -Description "$($_.Description) (Migrated)" -Force + Write-Output "Updated override: $($_.Host).$($_.Domain)" +} + +# Remove all overrides with "Temporary" in the description +$overrides | Where-Object { $_.Description -like "*Temporary*" } | ForEach-Object { + Remove-OPNSenseDNSOverride -Uuid $_.Uuid -Force + Write-Output "Removed override: $($_.Host).$($_.Domain)" +} + +# Apply the changes +Apply-OPNSenseDNSChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +## Notes + +- DNS changes are not applied until you call `Apply-OPNSenseDNSChanges`. +- DNS overrides take precedence over DNS forwarding. +- When using DNS forwarding, ensure that the forwarded domains are accessible from the OPNSense firewall. +- DNS caching can improve performance by reducing the number of external DNS queries. +- DNSSEC provides additional security by validating DNS responses. +- Consider using the `-PassThru` parameter when updating DNS configurations to verify the changes. +- Wildcard DNS overrides (using "*" as the host) can be used to override all subdomains of a domain. +- DNS forwarding can be used to direct queries for specific domains to internal or external DNS servers. diff --git a/docs/Development-Guidelines.md b/docs/Development-Guidelines.md new file mode 100644 index 0000000..b68ae82 --- /dev/null +++ b/docs/Development-Guidelines.md @@ -0,0 +1,238 @@ +# Development and Contribution Guidelines + +This document provides guidelines for developing and contributing to the PSOPNSenseAPI module. + +## Development Environment Setup + +### Prerequisites + +- Visual Studio 2019 or later +- PowerShell 5.1 or PowerShell 7+ +- .NET Framework 4.7.2 SDK (for Windows PowerShell 5.1 compatibility) +- .NET Core 3.1 SDK or later (for PowerShell 7+ compatibility) +- Git + +### Setting Up the Development Environment + +1. Clone the repository: + ``` + git clone https://github.com/freedbygrace/PSOPNSenseAPI.git + cd PSOPNSenseAPI + ``` + +2. Open the solution in Visual Studio: + ``` + start PSOPNSenseAPI.sln + ``` + +3. Restore NuGet packages: + ``` + dotnet restore + ``` + +4. Build the solution: + ``` + dotnet build + ``` + +## Coding Standards + +### C# Code Style + +- Follow Microsoft's C# coding conventions +- Use meaningful names for classes, methods, and variables +- Add XML documentation comments to all public members +- Use proper exception handling +- Write unit tests for all functionality +- Keep methods small and focused on a single responsibility +- Use dependency injection where appropriate +- Avoid static methods except for utility functions + +### PowerShell Cmdlet Design + +- Follow the PowerShell Verb-Noun naming convention +- Use approved PowerShell verbs (Get, Set, New, Remove, etc.) +- Implement proper parameter validation +- Support pipeline input where appropriate +- Implement ShouldProcess for cmdlets that make changes +- Provide meaningful error messages +- Write detailed help content +- Support common parameters (-Verbose, -Debug, etc.) +- Return appropriate objects, not strings + +### Documentation + +- Document all public APIs with XML comments +- Include examples in cmdlet help +- Update README.md and other documentation when adding features +- Document breaking changes in CHANGELOG.md + +## Project Structure + +The project follows this structure: + +- **src/PSOPNSenseAPI**: Main source code + - **Cmdlets**: PowerShell cmdlets + - **Models**: Data models + - **Services**: Service classes for API communication + - **Utilities**: Utility classes + - **Logging**: Logging infrastructure +- **tests**: Unit and integration tests +- **docs**: Documentation +- **build**: Build scripts +- **output**: Build output (not in source control) + +## Development Workflow + +### Feature Development + +1. Create a new branch from `main`: + ``` + git checkout -b feature/my-feature + ``` + +2. Implement the feature: + - Add model classes if needed + - Implement service classes + - Create PowerShell cmdlets + - Add unit tests + - Update documentation + +3. Build and test locally: + ``` + .\build\build.ps1 + .\build\test.ps1 + ``` + +4. Commit your changes with meaningful commit messages: + ``` + git commit -m "Add feature: description of the feature" + ``` + +5. Push your branch and create a pull request: + ``` + git push origin feature/my-feature + ``` + +### Bug Fixes + +1. Create a new branch from `main`: + ``` + git checkout -b fix/bug-description + ``` + +2. Fix the bug: + - Add a test that reproduces the bug + - Fix the bug + - Verify that the test passes + +3. Commit your changes: + ``` + git commit -m "Fix: description of the bug fix" + ``` + +4. Push your branch and create a pull request: + ``` + git push origin fix/bug-description + ``` + +## Testing + +### Unit Testing + +- Write unit tests for all functionality +- Use xUnit for testing +- Mock external dependencies +- Aim for high code coverage +- Run tests before submitting pull requests + +### Integration Testing + +- Write integration tests for API interactions +- Use a test OPNSense instance if possible +- Skip integration tests if no test instance is available + +### Running Tests + +```powershell +# Run all tests +.\build\test.ps1 + +# Run specific tests +dotnet test --filter "FullyQualifiedName~PSOPNSenseAPI.Tests.NetworkUtilityTests" +``` + +## Building and Packaging + +### Building the Module + +```powershell +# Build the module +.\build\build.ps1 + +# Build a release version with automatic versioning +.\build\build-release.ps1 -Clean -Test -Package +``` + +### Versioning + +The module uses a versioning scheme of `yyyy.MM.dd.HHmm` for releases, which is automatically generated during the build process. + +### Creating a Release + +1. Update the CHANGELOG.md file +2. Build a release package: + ``` + .\build\build-release.ps1 -Clean -Test -Package + ``` +3. Create a new release on GitHub with the generated package + +## Continuous Integration + +The project uses GitHub Actions for continuous integration: + +- Builds and tests are run on every push to the main branch +- Release packages are automatically created with the versioning scheme +- Release artifacts are uploaded to GitHub Releases + +## Pull Request Guidelines + +When submitting a pull request: + +1. Ensure all tests pass +2. Update documentation if needed +3. Add entries to CHANGELOG.md for new features or bug fixes +4. Follow the coding standards +5. Keep pull requests focused on a single change +6. Provide a clear description of the changes +7. Link to any related issues + +## Code Review Process + +All pull requests will be reviewed for: + +- Code quality and style +- Test coverage +- Documentation +- Performance considerations +- Security implications + +## Release Process + +1. Merge approved pull requests into the main branch +2. Update the version in the module manifest +3. Update CHANGELOG.md +4. Create a new release on GitHub +5. Publish the module to PowerShell Gallery (if applicable) + +## Support + +If you need help with development: + +- Open an issue on GitHub +- Reach out to the maintainers +- Check the documentation and existing issues + +## License + +All contributions to this project are subject to the terms of the MIT License. diff --git a/docs/Firewall-Rules.md b/docs/Firewall-Rules.md new file mode 100644 index 0000000..53e6fff --- /dev/null +++ b/docs/Firewall-Rules.md @@ -0,0 +1,268 @@ +# Firewall Rules Management + +This component provides cmdlets for managing firewall rules on OPNSense firewalls. + +## Overview + +The Firewall Rules Management component allows you to create, view, modify, and delete firewall rules on OPNSense firewalls. It provides a comprehensive set of cmdlets to manage all aspects of firewall rules, including enabling, disabling, and applying changes. + +## Cmdlets + +### Get-OPNSenseFirewallRule + +Retrieves firewall rules from an OPNSense firewall. + +#### Parameters + +- **Uuid** - The UUID of a specific firewall rule to retrieve. If not specified, all rules are returned. +- **Interface** - Filter rules by interface. +- **Direction** - Filter rules by direction (in, out). +- **Protocol** - Filter rules by protocol. +- **Action** - Filter rules by action (pass, block, reject). + +#### Examples + +```powershell +# Get all firewall rules +Get-OPNSenseFirewallRule + +# Get a specific firewall rule by UUID +Get-OPNSenseFirewallRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" + +# Get all rules for a specific interface +Get-OPNSenseFirewallRule -Interface "lan" + +# Get all block rules +Get-OPNSenseFirewallRule -Action "block" +``` + +### New-OPNSenseFirewallRule + +Creates a new firewall rule on an OPNSense firewall. + +#### Parameters + +- **Enabled** - Whether the rule is enabled. Default is true. +- **Action** - The action to take (pass, block, reject). Default is "pass". +- **Interface** - The interface for the rule. +- **Direction** - The direction for the rule (in, out). Default is "in". +- **Protocol** - The protocol for the rule (tcp, udp, icmp, etc.). +- **Source** - The source address for the rule. Default is "any". +- **SourcePort** - The source port for the rule. Default is "any". +- **Destination** - The destination address for the rule. Default is "any". +- **DestinationPort** - The destination port for the rule. +- **Description** - A description for the rule. +- **Log** - Whether to log matches for this rule. Default is false. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Create a rule to allow HTTP traffic +New-OPNSenseFirewallRule -Interface "lan" -Protocol "tcp" -Destination "any" -DestinationPort "80" -Description "Allow HTTP" + +# Create a rule to block outgoing SMTP traffic +New-OPNSenseFirewallRule -Interface "lan" -Direction "out" -Protocol "tcp" -DestinationPort "25" -Action "block" -Description "Block outgoing SMTP" -Log + +# Create a rule to allow traffic from a specific subnet to a specific server +New-OPNSenseFirewallRule -Interface "lan" -Protocol "tcp" -Source "192.168.1.0/24" -Destination "192.168.2.10" -DestinationPort "443" -Description "Allow subnet to server" +``` + +### Set-OPNSenseFirewallRule + +Updates an existing firewall rule on an OPNSense firewall. + +#### Parameters + +- **Uuid** - The UUID of the firewall rule to update. +- **Enabled** - Whether the rule is enabled. +- **Action** - The action to take (pass, block, reject). +- **Interface** - The interface for the rule. +- **Direction** - The direction for the rule (in, out). +- **Protocol** - The protocol for the rule. +- **Source** - The source address for the rule. +- **SourcePort** - The source port for the rule. +- **Destination** - The destination address for the rule. +- **DestinationPort** - The destination port for the rule. +- **Description** - A description for the rule. +- **Log** - Whether to log matches for this rule. +- **Force** - Suppresses the confirmation prompt. +- **PassThru** - Returns the updated rule. + +#### Examples + +```powershell +# Update a firewall rule's description +Set-OPNSenseFirewallRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -Description "Updated HTTP rule" + +# Update a firewall rule's destination port +Set-OPNSenseFirewallRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -DestinationPort "8080" + +# Update multiple properties of a firewall rule +Set-OPNSenseFirewallRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -Action "block" -Log -Description "Block traffic" -PassThru +``` + +### Remove-OPNSenseFirewallRule + +Removes a firewall rule from an OPNSense firewall. + +#### Parameters + +- **Uuid** - The UUID of the firewall rule to remove. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Remove a firewall rule +Remove-OPNSenseFirewallRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" + +# Remove a firewall rule without confirmation +Remove-OPNSenseFirewallRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -Force +``` + +### Enable-OPNSenseFirewallRule + +Enables a firewall rule on an OPNSense firewall. + +#### Parameters + +- **Uuid** - The UUID of the firewall rule to enable. +- **Force** - Suppresses the confirmation prompt. +- **PassThru** - Returns the updated rule. + +#### Examples + +```powershell +# Enable a firewall rule +Enable-OPNSenseFirewallRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" + +# Enable a firewall rule and return the updated rule +Enable-OPNSenseFirewallRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -PassThru +``` + +### Disable-OPNSenseFirewallRule + +Disables a firewall rule on an OPNSense firewall. + +#### Parameters + +- **Uuid** - The UUID of the firewall rule to disable. +- **Force** - Suppresses the confirmation prompt. +- **PassThru** - Returns the updated rule. + +#### Examples + +```powershell +# Disable a firewall rule +Disable-OPNSenseFirewallRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" + +# Disable a firewall rule and return the updated rule +Disable-OPNSenseFirewallRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -PassThru +``` + +### Apply-OPNSenseFirewallChanges + +Applies pending firewall changes on an OPNSense firewall. + +#### Parameters + +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Apply firewall changes +Apply-OPNSenseFirewallChanges + +# Apply firewall changes without confirmation +Apply-OPNSenseFirewallChanges -Force +``` + +## Common Scenarios + +### Basic Firewall Configuration + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Create rules for basic web access +New-OPNSenseFirewallRule -Interface "lan" -Protocol "tcp" -DestinationPort "80" -Description "Allow HTTP" -Force +New-OPNSenseFirewallRule -Interface "lan" -Protocol "tcp" -DestinationPort "443" -Description "Allow HTTPS" -Force + +# Create a rule to allow DNS +New-OPNSenseFirewallRule -Interface "lan" -Protocol "udp" -DestinationPort "53" -Description "Allow DNS" -Force + +# Apply the changes +Apply-OPNSenseFirewallChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Securing a Network + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Block outgoing SMTP to prevent spam +New-OPNSenseFirewallRule -Interface "lan" -Direction "out" -Protocol "tcp" -DestinationPort "25" -Action "block" -Description "Block outgoing SMTP" -Log -Force + +# Allow only specific hosts to access the management interface +New-OPNSenseFirewallRule -Interface "lan" -Protocol "tcp" -Source "192.168.1.10,192.168.1.11" -Destination "192.168.1.1" -DestinationPort "443" -Description "Allow management access" -Force + +# Block all other access to the management interface +New-OPNSenseFirewallRule -Interface "lan" -Protocol "tcp" -Destination "192.168.1.1" -DestinationPort "443" -Action "block" -Description "Block management access" -Log -Force + +# Apply the changes +Apply-OPNSenseFirewallChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Managing Existing Rules + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get all firewall rules +$rules = Get-OPNSenseFirewallRule + +# Disable all rules with "Temporary" in the description +$rules | Where-Object { $_.Description -like "*Temporary*" } | ForEach-Object { + Disable-OPNSenseFirewallRule -Uuid $_.Uuid -Force + Write-Output "Disabled rule: $($_.Description)" +} + +# Update all rules with "HTTP" in the description to use port 8080 instead of 80 +$rules | Where-Object { $_.Description -like "*HTTP*" -and $_.DestinationPort -eq "80" } | ForEach-Object { + Set-OPNSenseFirewallRule -Uuid $_.Uuid -DestinationPort "8080" -Description "$($_.Description) (Updated Port)" -Force + Write-Output "Updated rule: $($_.Description)" +} + +# Remove all rules with "Obsolete" in the description +$rules | Where-Object { $_.Description -like "*Obsolete*" } | ForEach-Object { + Remove-OPNSenseFirewallRule -Uuid $_.Uuid -Force + Write-Output "Removed rule: $($_.Description)" +} + +# Apply the changes +Apply-OPNSenseFirewallChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +## Notes + +- Firewall rules are processed in order, with the first matching rule being applied. +- Changes to firewall rules are not applied until you call `Apply-OPNSenseFirewallChanges`. +- When creating or updating rules, consider the rule order and potential security implications. +- Use the `-Log` parameter for rules that you want to monitor for security purposes. +- Use aliases for frequently used IP addresses or networks to make rules more maintainable. +- Consider using the `-PassThru` parameter when updating rules to verify the changes. +- Always apply the principle of least privilege when creating firewall rules. diff --git a/docs/Firmware-Management.md b/docs/Firmware-Management.md new file mode 100644 index 0000000..c150c02 --- /dev/null +++ b/docs/Firmware-Management.md @@ -0,0 +1,295 @@ +# Firmware Management + +This component provides cmdlets for managing firmware on OPNSense firewalls. + +## Overview + +The Firmware Management component allows you to check for updates, install updates, and manage firmware settings on OPNSense firewalls. It provides cmdlets for checking firmware status, upgrading firmware, and managing firmware settings. + +## Cmdlets + +### Get-OPNSenseFirmwareStatus + +Retrieves the current firmware status of an OPNSense firewall. + +#### Examples + +```powershell +# Get firmware status +Get-OPNSenseFirmwareStatus +``` + +### Get-OPNSenseFirmwareUpdate + +Checks for available firmware updates for an OPNSense firewall. + +#### Parameters + +- **Force** - Forces a check for updates even if the update cache is current. + +#### Examples + +```powershell +# Check for updates +Get-OPNSenseFirmwareUpdate + +# Force a check for updates +Get-OPNSenseFirmwareUpdate -Force +``` + +### Install-OPNSenseFirmwareUpdate + +Installs available firmware updates on an OPNSense firewall. + +#### Parameters + +- **RebootAfterUpdate** - Whether to reboot the firewall after updating. Default is false. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Install updates +Install-OPNSenseFirmwareUpdate + +# Install updates and reboot +Install-OPNSenseFirmwareUpdate -RebootAfterUpdate + +# Install updates without confirmation +Install-OPNSenseFirmwareUpdate -Force +``` + +### Invoke-OPNSenseFirmwareUpgrade + +Performs a firmware upgrade on an OPNSense firewall. + +#### Parameters + +- **MajorVersion** - The major version to upgrade to. +- **RebootAfterUpgrade** - Whether to reboot the firewall after upgrading. Default is false. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Upgrade to the latest version +Invoke-OPNSenseFirmwareUpgrade + +# Upgrade to a specific major version +Invoke-OPNSenseFirmwareUpgrade -MajorVersion "23.1" + +# Upgrade and reboot +Invoke-OPNSenseFirmwareUpgrade -RebootAfterUpgrade + +# Upgrade without confirmation +Invoke-OPNSenseFirmwareUpgrade -Force +``` + +### Get-OPNSenseFirmwareSettings + +Retrieves the firmware settings of an OPNSense firewall. + +#### Examples + +```powershell +# Get firmware settings +Get-OPNSenseFirmwareSettings +``` + +### Set-OPNSenseFirmwareSettings + +Updates the firmware settings of an OPNSense firewall. + +#### Parameters + +- **Mirror** - The firmware mirror to use. +- **Flavour** - The firmware flavour to use (e.g., OpenSSL, LibreSSL). +- **Subscription** - The firmware subscription to use. +- **AutomaticUpdates** - Whether to enable automatic updates. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Set firmware mirror +Set-OPNSenseFirmwareSettings -Mirror "https://opnsense.c0urier.net" + +# Enable automatic updates +Set-OPNSenseFirmwareSettings -AutomaticUpdates $true + +# Set multiple firmware settings +Set-OPNSenseFirmwareSettings -Mirror "https://opnsense.c0urier.net" -Flavour "OpenSSL" -AutomaticUpdates $true + +# Set firmware settings without confirmation +Set-OPNSenseFirmwareSettings -Mirror "https://opnsense.c0urier.net" -Force +``` + +### Get-OPNSenseFirmwareAudit + +Retrieves the firmware audit log of an OPNSense firewall. + +#### Parameters + +- **Limit** - The maximum number of entries to retrieve. Default is 50. + +#### Examples + +```powershell +# Get firmware audit log +Get-OPNSenseFirmwareAudit + +# Get the last 100 entries from the firmware audit log +Get-OPNSenseFirmwareAudit -Limit 100 +``` + +### Invoke-OPNSenseReboot + +Reboots an OPNSense firewall. + +#### Parameters + +- **Force** - Suppresses the confirmation prompt. +- **Wait** - Whether to wait for the firewall to come back online. Default is false. +- **Timeout** - The timeout in seconds to wait for the firewall to come back online. Default is 300. + +#### Examples + +```powershell +# Reboot the firewall +Invoke-OPNSenseReboot + +# Reboot the firewall without confirmation +Invoke-OPNSenseReboot -Force + +# Reboot the firewall and wait for it to come back online +Invoke-OPNSenseReboot -Wait + +# Reboot the firewall and wait with a custom timeout +Invoke-OPNSenseReboot -Wait -Timeout 600 +``` + +## Common Scenarios + +### Checking and Installing Updates + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Check firmware status +$status = Get-OPNSenseFirmwareStatus +Write-Output "Current firmware version: $($status.Version)" +Write-Output "Current firmware status: $($status.Status)" + +# Check for updates +$updates = Get-OPNSenseFirmwareUpdate -Force +if ($updates.Count -gt 0) { + Write-Output "Updates available:" + $updates | Format-Table -Property Name, Version, Size, Description + + # Install updates + Install-OPNSenseFirmwareUpdate -Force + + # Reboot if necessary + if ($updates | Where-Object { $_.RebootRequired -eq $true }) { + Write-Output "Rebooting to apply updates..." + Invoke-OPNSenseReboot -Force -Wait + } +} else { + Write-Output "No updates available." +} + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Upgrading to a New Major Version + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Check current version +$status = Get-OPNSenseFirmwareStatus +Write-Output "Current firmware version: $($status.Version)" + +# Perform a backup before upgrading +New-OPNSenseBackup -IncludeRRD -IncludeInstalled -Force +$backups = Get-OPNSenseBackup +$latestBackup = $backups | Sort-Object -Property Date -Descending | Select-Object -First 1 +$backupPath = "C:\Backups\opnsense-pre-upgrade-$(Get-Date -Format 'yyyyMMdd').xml" +Save-OPNSenseBackup -BackupId $latestBackup.Id -Path $backupPath -Force +Write-Output "Backup saved to: $backupPath" + +# Upgrade to the latest version +Write-Output "Upgrading firmware..." +Invoke-OPNSenseFirmwareUpgrade -RebootAfterUpgrade -Force + +# Wait for the firewall to come back online +Write-Output "Waiting for the firewall to come back online..." +Start-Sleep -Seconds 300 + +# Reconnect to the firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Check new version +$newStatus = Get-OPNSenseFirmwareStatus +Write-Output "New firmware version: $($newStatus.Version)" + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Configuring Firmware Settings + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get current firmware settings +$settings = Get-OPNSenseFirmwareSettings +Write-Output "Current firmware settings:" +$settings | Format-List + +# Update firmware settings +Set-OPNSenseFirmwareSettings -Mirror "https://opnsense.c0urier.net" -Flavour "OpenSSL" -AutomaticUpdates $true -Force + +# Get updated firmware settings +$updatedSettings = Get-OPNSenseFirmwareSettings +Write-Output "Updated firmware settings:" +$updatedSettings | Format-List + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Monitoring Firmware Audit Log + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get firmware audit log +$auditLog = Get-OPNSenseFirmwareAudit -Limit 20 +Write-Output "Recent firmware changes:" +$auditLog | Format-Table -Property Date, User, Action, Package + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +## Notes + +- Firmware updates and upgrades can take some time to complete. +- Always create a backup before performing firmware updates or upgrades. +- Some updates may require a reboot to take effect. +- Major version upgrades should be performed with caution and only after thorough testing. +- Automatic updates can be configured to run at specific times. +- Firmware mirrors can affect the download speed and availability of updates. +- The firmware audit log provides a history of firmware changes. +- Consider using the `-Force` parameter when performing firmware operations in scripts to avoid confirmation prompts. +- The `-Wait` parameter for `Invoke-OPNSenseReboot` can be useful for scripts that need to wait for the firewall to come back online. +- Firmware settings are typically stored in the OPNSense configuration file. +- Some firmware operations may require administrative privileges. +- Firmware updates and upgrades can fail if there is insufficient disk space. +- Always check the OPNSense release notes before upgrading to a new major version. diff --git a/docs/Gateway-Management.md b/docs/Gateway-Management.md new file mode 100644 index 0000000..448fcf3 --- /dev/null +++ b/docs/Gateway-Management.md @@ -0,0 +1,289 @@ +# Gateway Management + +This component provides cmdlets for managing gateways on OPNSense firewalls. + +## Overview + +The Gateway Management component allows you to configure and manage gateways on OPNSense firewalls. It provides cmdlets for creating, viewing, modifying, and deleting gateways, as well as monitoring gateway status. + +## Cmdlets + +### Get-OPNSenseGateway + +Retrieves gateway configurations from an OPNSense firewall. + +#### Parameters + +- **Name** - The name of a specific gateway to retrieve. If not specified, all gateways are returned. +- **Interface** - Filter gateways by interface. +- **Protocol** - Filter gateways by protocol (IPv4, IPv6). + +#### Examples + +```powershell +# Get all gateways +Get-OPNSenseGateway + +# Get a specific gateway by name +Get-OPNSenseGateway -Name "WAN_GATEWAY" + +# Get all gateways for a specific interface +Get-OPNSenseGateway -Interface "wan" + +# Get all IPv4 gateways +Get-OPNSenseGateway -Protocol "IPv4" +``` + +### New-OPNSenseGateway + +Creates a new gateway on an OPNSense firewall. + +#### Parameters + +- **Name** - The name of the gateway. +- **Interface** - The interface for the gateway. +- **IPAddress** - The IP address of the gateway. +- **Description** - A description for the gateway. +- **Enabled** - Whether the gateway is enabled. Default is true. +- **Monitor** - The IP address to monitor for gateway status. +- **MonitorDisable** - Whether to disable monitoring for this gateway. Default is false. +- **Weight** - The weight of the gateway for load balancing. Default is 1. +- **Protocol** - The protocol for the gateway (IPv4, IPv6). Default is IPv4. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Create a basic gateway +New-OPNSenseGateway -Name "WAN2_GATEWAY" -Interface "opt1" -IPAddress "203.0.113.1" -Description "Secondary WAN Gateway" + +# Create a gateway with monitoring +New-OPNSenseGateway -Name "WAN3_GATEWAY" -Interface "opt2" -IPAddress "198.51.100.1" -Description "Tertiary WAN Gateway" -Monitor "198.51.100.254" + +# Create a gateway with load balancing weight +New-OPNSenseGateway -Name "LB_GATEWAY" -Interface "opt3" -IPAddress "192.0.2.1" -Description "Load Balanced Gateway" -Weight 2 + +# Create an IPv6 gateway +New-OPNSenseGateway -Name "WAN_IPv6_GATEWAY" -Interface "wan" -IPAddress "2001:db8::1" -Description "IPv6 WAN Gateway" -Protocol "IPv6" +``` + +### Set-OPNSenseGateway + +Updates an existing gateway on an OPNSense firewall. + +#### Parameters + +- **Name** - The name of the gateway to update. +- **Interface** - The interface for the gateway. +- **IPAddress** - The IP address of the gateway. +- **Description** - A description for the gateway. +- **Enabled** - Whether the gateway is enabled. +- **Monitor** - The IP address to monitor for gateway status. +- **MonitorDisable** - Whether to disable monitoring for this gateway. +- **Weight** - The weight of the gateway for load balancing. +- **Force** - Suppresses the confirmation prompt. +- **PassThru** - Returns the updated gateway. + +#### Examples + +```powershell +# Update a gateway's IP address +Set-OPNSenseGateway -Name "WAN_GATEWAY" -IPAddress "203.0.113.2" + +# Update a gateway's description +Set-OPNSenseGateway -Name "WAN2_GATEWAY" -Description "Updated Secondary WAN Gateway" + +# Update a gateway's monitoring settings +Set-OPNSenseGateway -Name "WAN3_GATEWAY" -Monitor "198.51.100.253" + +# Disable a gateway +Set-OPNSenseGateway -Name "LB_GATEWAY" -Enabled:$false + +# Update multiple properties of a gateway +Set-OPNSenseGateway -Name "WAN_GATEWAY" -IPAddress "203.0.113.3" -Description "Updated WAN Gateway" -Weight 3 -PassThru +``` + +### Remove-OPNSenseGateway + +Removes a gateway from an OPNSense firewall. + +#### Parameters + +- **Name** - The name of the gateway to remove. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Remove a gateway +Remove-OPNSenseGateway -Name "WAN2_GATEWAY" + +# Remove a gateway without confirmation +Remove-OPNSenseGateway -Name "WAN3_GATEWAY" -Force +``` + +### Get-OPNSenseGatewayStatus + +Retrieves the status of gateways on an OPNSense firewall. + +#### Parameters + +- **Name** - The name of a specific gateway to get the status for. If not specified, the status of all gateways is returned. + +#### Examples + +```powershell +# Get the status of all gateways +Get-OPNSenseGatewayStatus + +# Get the status of a specific gateway +Get-OPNSenseGatewayStatus -Name "WAN_GATEWAY" +``` + +### Apply-OPNSenseGatewayChanges + +Applies pending gateway changes on an OPNSense firewall. + +#### Parameters + +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Apply gateway changes +Apply-OPNSenseGatewayChanges + +# Apply gateway changes without confirmation +Apply-OPNSenseGatewayChanges -Force +``` + +## Common Scenarios + +### Basic Gateway Configuration + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Create a primary WAN gateway +New-OPNSenseGateway -Name "WAN_GATEWAY" -Interface "wan" -IPAddress "203.0.113.1" -Description "Primary WAN Gateway" -Monitor "8.8.8.8" -Force + +# Apply the changes +Apply-OPNSenseGatewayChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Multi-WAN Configuration + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Create gateways for multiple WAN connections +$wanGateways = @( + @{ Name = "WAN1_GATEWAY"; Interface = "wan"; IPAddress = "203.0.113.1"; Description = "Primary WAN Gateway"; Monitor = "8.8.8.8"; Weight = 1 }, + @{ Name = "WAN2_GATEWAY"; Interface = "opt1"; IPAddress = "198.51.100.1"; Description = "Secondary WAN Gateway"; Monitor = "8.8.4.4"; Weight = 2 }, + @{ Name = "WAN3_GATEWAY"; Interface = "opt2"; IPAddress = "192.0.2.1"; Description = "Tertiary WAN Gateway"; Monitor = "1.1.1.1"; Weight = 3 } +) + +foreach ($gateway in $wanGateways) { + New-OPNSenseGateway -Name $gateway.Name -Interface $gateway.Interface -IPAddress $gateway.IPAddress -Description $gateway.Description -Monitor $gateway.Monitor -Weight $gateway.Weight -Force +} + +# Apply the changes +Apply-OPNSenseGatewayChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Monitoring Gateway Status + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get the status of all gateways +$gatewayStatus = Get-OPNSenseGatewayStatus + +# Display gateway status +$gatewayStatus | Format-Table -Property Name, Status, RTT, Loss + +# Check for down gateways +$downGateways = $gatewayStatus | Where-Object { $_.Status -ne "online" } +if ($downGateways) { + Write-Output "The following gateways are not online:" + $downGateways | Format-Table -Property Name, Status, RTT, Loss +} + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Updating Gateway Monitoring + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get all gateways +$gateways = Get-OPNSenseGateway + +# Update monitoring settings for all gateways +foreach ($gateway in $gateways) { + # Use different monitoring IPs based on the gateway name + $monitorIP = switch ($gateway.Name) { + "WAN1_GATEWAY" { "8.8.8.8" } + "WAN2_GATEWAY" { "8.8.4.4" } + "WAN3_GATEWAY" { "1.1.1.1" } + default { "9.9.9.9" } + } + + Set-OPNSenseGateway -Name $gateway.Name -Monitor $monitorIP -Force + Write-Output "Updated monitoring for gateway $($gateway.Name) to $monitorIP" +} + +# Apply the changes +Apply-OPNSenseGatewayChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Failover Gateway Configuration + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Create primary and backup gateways +New-OPNSenseGateway -Name "PRIMARY_GW" -Interface "wan" -IPAddress "203.0.113.1" -Description "Primary Gateway" -Monitor "8.8.8.8" -Weight 1 -Force +New-OPNSenseGateway -Name "BACKUP_GW" -Interface "opt1" -IPAddress "198.51.100.1" -Description "Backup Gateway" -Monitor "8.8.4.4" -Weight 2 -Force + +# Apply the changes +Apply-OPNSenseGatewayChanges -Force + +# Create a gateway group for failover +New-OPNSenseGatewayGroup -Name "FAILOVER_GROUP" -Description "Failover Gateway Group" -Gateways "PRIMARY_GW,BACKUP_GW" -Trigger "1" -Force + +# Apply the changes +Apply-OPNSenseGatewayGroupChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +## Notes + +- Gateway changes are not applied until you call `Apply-OPNSenseGatewayChanges`. +- When creating a gateway, ensure that the IP address is reachable from the specified interface. +- The `Monitor` parameter specifies an IP address to ping to determine the gateway status. +- The `Weight` parameter is used for load balancing when multiple gateways are used in a gateway group. +- Gateway names should be descriptive and follow a consistent naming convention. +- Consider using the `-PassThru` parameter when updating gateways to verify the changes. +- Gateway monitoring can impact performance if too many gateways are monitored with a high frequency. +- The default gateway is typically named "WAN_GATEWAY" for IPv4 and "WAN_GATEWAY_V6" for IPv6. +- Gateway groups can be used for failover or load balancing between multiple gateways. diff --git a/docs/IPNetwork2-Integration.md b/docs/IPNetwork2-Integration.md new file mode 100644 index 0000000..d70b357 --- /dev/null +++ b/docs/IPNetwork2-Integration.md @@ -0,0 +1,166 @@ +# IPNetwork2 Integration + +This document provides information about the integration of the IPNetwork2 library in the PSOPNSenseAPI module. + +## Overview + +The PSOPNSenseAPI module uses the IPNetwork2 library for IP network calculations and manipulations. This library provides functionality for working with IP networks, subnets, CIDR notation, and other network-related operations. + +## IPNetwork2 Library + +The IPNetwork2 library is a .NET library that provides classes and methods for working with IP networks. It supports both IPv4 and IPv6 networks and provides functionality for: + +- Parsing CIDR notation +- Subnetting +- Supernetting +- Network calculations +- IP address manipulation +- Network containment and overlap checks + +## Integration in PSOPNSenseAPI + +The IPNetwork2 library is integrated into the PSOPNSenseAPI module in the following ways: + +1. **NetworkUtility Class**: The `NetworkUtility` class in the `PSOPNSenseAPI.Utilities` namespace provides wrapper methods for the IPNetwork2 functionality. + +2. **Network Calculation Cmdlets**: The `Invoke-OPNSenseNetworkCalculation` cmdlet exposes the IPNetwork2 functionality to PowerShell users. + +3. **VLAN and Subnet Management**: The `New-OPNSenseSubnetVLANs` cmdlet uses IPNetwork2 for subnet calculations when creating VLANs from subnet divisions. + +## Using IPNetwork2 in the Code + +When using IPNetwork2 in the code, it's important to use the correct namespace and class name: + +```csharp +using System.Net; + +// Parse a CIDR notation string +IPNetwork2 network = IPNetwork2.Parse("192.168.1.0/24"); + +// Get network properties +IPAddress networkAddress = network.Network; +IPAddress broadcastAddress = network.Broadcast; +int cidrPrefix = network.Cidr; +BigInteger totalHosts = network.Total; + +// Subnet a network +IEnumerable subnets = IPNetwork2.Subnet(network, 27); + +// Check if an IP address is in a network +bool contains = network.Contains(IPAddress.Parse("192.168.1.100")); + +// Check if networks overlap +bool overlaps = network1.Overlap(network2); + +// Create a supernet +IPNetwork2[] supernet = IPNetwork2.Supernet(new[] { network1, network2 }); +``` + +## Common Issues and Solutions + +### Namespace and Class Name + +The most common issue when working with IPNetwork2 is using the incorrect namespace or class name. The correct usage is: + +```csharp +using System.Net; + +// Use the class name directly +IPNetwork2 network = IPNetwork2.Parse("192.168.1.0/24"); + +// Or use the fully qualified name for clarity +System.Net.IPNetwork2 network = System.Net.IPNetwork2.Parse("192.168.1.0/24"); +``` + +### Version Compatibility + +Ensure that you're using a compatible version of IPNetwork2. The PSOPNSenseAPI module uses version 3.1.764, which is the latest version as of April 2025. + +```xml + +``` + +This version supports .NET 8.0, .NET 9.0, .NET Standard 2.0, and .NET Standard 2.1. + +### Method Signatures + +Be aware of the method signatures when using IPNetwork2. For example, the `Subnet` method has multiple overloads: + +```csharp +// Subnet by prefix length +IEnumerable Subnet(IPNetwork2 network, int prefixLength); + +// Subnet by subnet count +IEnumerable Subnet(IPNetwork2 network, BigInteger subnetCount); + +// Subnet by target network +IEnumerable Subnet(IPNetwork2 network, IPNetwork2 targetNetwork); +``` + +### Memory Usage + +When working with large networks (e.g., /8 networks) and small subnets (e.g., /30), be aware that operations like subnetting can generate a large number of results and consume significant memory. + +## Exposing IPNetwork2 Functionality to PowerShell + +The PSOPNSenseAPI module exposes IPNetwork2 functionality to PowerShell users through the `Invoke-OPNSenseNetworkCalculation` cmdlet: + +```powershell +# Show network information +Invoke-OPNSenseNetworkCalculation -Network "192.168.1.0/24" -Operation Info + +# Subnet a network +Invoke-OPNSenseNetworkCalculation -Network "10.0.0.0/24" -Operation Subnet -PrefixLength 27 + +# Check if networks overlap +Invoke-OPNSenseNetworkCalculation -Network "10.0.0.0/16" -Operation Overlaps -AdditionalNetworks "10.0.1.0/24" +``` + +## Advanced Usage + +### Custom Network Utility Methods + +The `NetworkUtility` class provides additional methods that extend the functionality of IPNetwork2: + +```csharp +// Get the first usable host address in a network +IPAddress firstHost = NetworkUtility.GetFirstUsableHost(network); + +// Get the last usable host address in a network +IPAddress lastHost = NetworkUtility.GetLastUsableHost(network); + +// Get the number of usable host addresses in a network +BigInteger usableHosts = NetworkUtility.GetUsableHostCount(network); + +// Convert between CIDR and subnet mask +int cidr = NetworkUtility.GetCIDRFromSubnetMask("255.255.255.0"); +string subnetMask = NetworkUtility.GetSubnetMaskFromCIDR(24); +``` + +### Network Summarization + +The `SupernetSummarize` method provides advanced network summarization: + +```csharp +// Summarize multiple networks into the smallest possible set +List networks = new List +{ + IPNetwork2.Parse("192.168.1.0/24"), + IPNetwork2.Parse("192.168.2.0/24"), + IPNetwork2.Parse("192.168.3.0/24"), + IPNetwork2.Parse("192.168.4.0/24") +}; + +List summarized = NetworkUtility.SupernetSummarize(networks); +// Result: 192.168.0.0/22 +``` + +## Notes + +- The IPNetwork2 library is a third-party library and is subject to its own licensing terms. +- The PSOPNSenseAPI module includes the IPNetwork2 library as a NuGet package dependency. +- The IPNetwork2 library is used for local network calculations and does not require communication with the OPNSense firewall. +- Version 3.1.764 of IPNetwork2 has moved the `IPNetwork2` class to the `System.Net` namespace, which is different from earlier versions that used the `System.Net.IPNetwork` namespace. +- When upgrading the IPNetwork2 library, ensure that all code is updated to use the correct namespace and class names. +- Version 3.1.764 supports .NET 8.0, .NET 9.0, .NET Standard 2.0, and .NET Standard 2.1. +- For .NET Standard 2.0 and 2.1, IPNetwork2 has a dependency on System.Memory (>= 4.5.5). diff --git a/docs/Installation-Setup.md b/docs/Installation-Setup.md new file mode 100644 index 0000000..eee8557 --- /dev/null +++ b/docs/Installation-Setup.md @@ -0,0 +1,217 @@ +# Installation and Setup + +This document provides instructions for installing and setting up the PSOPNSenseAPI PowerShell module. + +## Overview + +The PSOPNSenseAPI module is a PowerShell module that provides cmdlets for managing OPNSense firewalls through the OPNSense API. This document covers the installation of the module, setting up API access on the OPNSense firewall, and establishing a connection to the firewall. + +## Prerequisites + +Before installing the PSOPNSenseAPI module, ensure that you have the following: + +- PowerShell 5.1 or later (Windows PowerShell or PowerShell Core) +- .NET Framework 4.7.2 or later (for Windows PowerShell) +- .NET Core 3.1 or later (for PowerShell Core) +- OPNSense 21.1 or later with API access configured + +## Installation Methods + +### Installing from PowerShell Gallery + +The recommended way to install the PSOPNSenseAPI module is from the PowerShell Gallery: + +```powershell +# Install the module for the current user +Install-Module -Name PSOPNSenseAPI -Scope CurrentUser + +# Install the module for all users (requires administrator privileges) +Install-Module -Name PSOPNSenseAPI -Scope AllUsers +``` + +### Installing from GitHub + +You can also install the module directly from the GitHub repository: + +```powershell +# Clone the repository +git clone https://github.com/freedbygrace/PSOPNSenseAPI.git + +# Navigate to the repository directory +cd PSOPNSenseAPI + +# Build the module +.\build\build.ps1 + +# Import the module +Import-Module .\output\PSOPNSenseAPI +``` + +### Manual Installation + +To install the module manually: + +1. Download the latest release from the [GitHub releases page](https://github.com/freedbygrace/PSOPNSenseAPI/releases) +2. Extract the archive to a module directory: + - For the current user: `$env:USERPROFILE\Documents\WindowsPowerShell\Modules\PSOPNSenseAPI` + - For all users: `$env:ProgramFiles\WindowsPowerShell\Modules\PSOPNSenseAPI` +3. Import the module: + +```powershell +Import-Module PSOPNSenseAPI +``` + +## Setting Up API Access on OPNSense + +Before you can use the PSOPNSenseAPI module, you need to set up API access on your OPNSense firewall: + +1. Log in to the OPNSense web interface +2. Navigate to **System > Access > Users** +3. Select the user you want to use for API access or create a new user +4. Ensure the user has the necessary privileges for the operations you want to perform +5. Click on the **API keys** tab +6. Click **+** to add a new API key +7. Note the **Key** and **Secret** values, as you will need them to connect to the firewall + +## Connecting to an OPNSense Firewall + +Once you have installed the module and set up API access, you can connect to an OPNSense firewall: + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" + +# Connect to the OPNSense firewall with certificate validation disabled (for self-signed certificates) +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck +``` + +## Verifying the Installation + +To verify that the module is installed and working correctly: + +```powershell +# Check the module version +Get-Module -Name PSOPNSenseAPI + +# List available cmdlets +Get-Command -Module PSOPNSenseAPI + +# Get help for a specific cmdlet +Get-Help -Name Connect-OPNSense -Full +``` + +## Updating the Module + +To update the module to the latest version: + +```powershell +# Update the module from PowerShell Gallery +Update-Module -Name PSOPNSenseAPI +``` + +## Uninstalling the Module + +To uninstall the module: + +```powershell +# Uninstall the module +Uninstall-Module -Name PSOPNSenseAPI +``` + +## Troubleshooting + +### Connection Issues + +If you encounter connection issues: + +1. Verify that the OPNSense firewall is reachable from your computer +2. Check that the API key and secret are correct +3. Ensure that the user associated with the API key has the necessary privileges +4. If using HTTPS with a self-signed certificate, use the `-SkipCertificateCheck` parameter +5. Check if there are any firewall rules blocking access to the OPNSense web interface + +### Module Loading Issues + +If you encounter issues loading the module: + +1. Verify that you have the required PowerShell and .NET versions +2. Check that the module is installed in a valid module path: + +```powershell +# List module paths +$env:PSModulePath -split ';' + +# Check if the module is in a valid path +Get-Module -Name PSOPNSenseAPI -ListAvailable +``` + +3. Try importing the module with verbose output: + +```powershell +Import-Module -Name PSOPNSenseAPI -Verbose +``` + +### API Access Issues + +If you encounter issues with API access: + +1. Verify that the API is enabled on the OPNSense firewall +2. Check that the user has the necessary privileges +3. Ensure that the API key and secret are valid +4. Check if there are any firewall rules blocking access to the API + +## Common Configuration + +### Creating a Profile for Quick Connection + +You can create a profile to store your connection information for quick access: + +```powershell +# Create a directory for profiles +$profilesDir = "$env:USERPROFILE\Documents\PSOPNSenseAPI\Profiles" +New-Item -Path $profilesDir -ItemType Directory -Force + +# Create a profile +$profile = @{ + Server = "https://firewall.example.com" + ApiKey = "your_api_key" + ApiSecret = "your_api_secret" + SkipCertificateCheck = $true +} + +# Save the profile +$profilePath = "$profilesDir\firewall.json" +$profile | ConvertTo-Json | Set-Content -Path $profilePath + +# Load the profile and connect +$profile = Get-Content -Path $profilePath | ConvertFrom-Json +Connect-OPNSense -Server $profile.Server -ApiKey $profile.ApiKey -ApiSecret $profile.ApiSecret -SkipCertificateCheck:$profile.SkipCertificateCheck +``` + +### Setting Up a Module Import Profile + +You can create a PowerShell profile that automatically imports the module: + +```powershell +# Add the following line to your PowerShell profile +if (-not (Get-Module -Name PSOPNSenseAPI -ErrorAction SilentlyContinue)) { + Import-Module -Name PSOPNSenseAPI +} + +# To edit your PowerShell profile +notepad $PROFILE +``` + +## Notes + +- Store API keys and secrets securely, as they provide access to your OPNSense firewall. +- Consider using a dedicated user with limited privileges for API access. +- Always disconnect from the firewall when you're done: + +```powershell +Disconnect-OPNSense +``` + +- For security reasons, avoid storing API keys and secrets in scripts or profiles that are shared or stored in unsecured locations. +- When using the module in scripts, consider using secure string variables or environment variables for API keys and secrets. +- The module uses HTTPS for all communication with the OPNSense firewall, ensuring that all data is encrypted in transit. diff --git a/docs/Interface-Management.md b/docs/Interface-Management.md new file mode 100644 index 0000000..8e00bfd --- /dev/null +++ b/docs/Interface-Management.md @@ -0,0 +1,298 @@ +# Interface Management + +This component provides cmdlets for managing network interfaces on OPNSense firewalls. + +## Overview + +The Interface Management component allows you to configure and manage network interfaces on OPNSense firewalls. It provides cmdlets for creating, viewing, modifying, and deleting interfaces, as well as configuring interface settings. + +## Cmdlets + +### Get-OPNSenseInterface + +Retrieves interface configurations from an OPNSense firewall. + +#### Parameters + +- **Name** - The name of a specific interface to retrieve. If not specified, all interfaces are returned. +- **Type** - Filter interfaces by type (e.g., physical, vlan, bridge). +- **Enabled** - Filter interfaces by enabled status. + +#### Examples + +```powershell +# Get all interfaces +Get-OPNSenseInterface + +# Get a specific interface by name +Get-OPNSenseInterface -Name "lan" + +# Get all VLAN interfaces +Get-OPNSenseInterface -Type "vlan" + +# Get all enabled interfaces +Get-OPNSenseInterface -Enabled $true +``` + +### New-OPNSenseInterface + +Creates a new interface on an OPNSense firewall. + +#### Parameters + +- **Name** - The name of the interface. +- **Description** - A description for the interface. +- **Type** - The type of the interface (e.g., physical, vlan, bridge). +- **Device** - The physical device for the interface. +- **IPv4Address** - The IPv4 address for the interface. +- **IPv4Subnet** - The IPv4 subnet mask for the interface. +- **IPv6Address** - The IPv6 address for the interface. +- **IPv6Subnet** - The IPv6 subnet mask for the interface. +- **Enabled** - Whether the interface is enabled. Default is true. +- **BlockPrivate** - Whether to block private networks. Default is false. +- **BlockBogons** - Whether to block bogon networks. Default is false. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Create a basic interface +New-OPNSenseInterface -Name "opt1" -Description "Optional Interface 1" -Type "physical" -Device "em1" -IPv4Address "192.168.2.1" -IPv4Subnet "24" + +# Create a VLAN interface +New-OPNSenseInterface -Name "vlan10" -Description "VLAN 10" -Type "vlan" -Device "em0.10" -IPv4Address "10.0.10.1" -IPv4Subnet "24" + +# Create an interface with IPv6 +New-OPNSenseInterface -Name "opt2" -Description "Optional Interface 2" -Type "physical" -Device "em2" -IPv4Address "192.168.3.1" -IPv4Subnet "24" -IPv6Address "2001:db8:1::1" -IPv6Subnet "64" + +# Create an interface with security options +New-OPNSenseInterface -Name "dmz" -Description "DMZ Interface" -Type "physical" -Device "em3" -IPv4Address "192.168.100.1" -IPv4Subnet "24" -BlockPrivate -BlockBogons +``` + +### Set-OPNSenseInterface + +Updates an existing interface on an OPNSense firewall. + +#### Parameters + +- **Name** - The name of the interface to update. +- **Description** - A description for the interface. +- **IPv4Address** - The IPv4 address for the interface. +- **IPv4Subnet** - The IPv4 subnet mask for the interface. +- **IPv6Address** - The IPv6 address for the interface. +- **IPv6Subnet** - The IPv6 subnet mask for the interface. +- **Enabled** - Whether the interface is enabled. +- **BlockPrivate** - Whether to block private networks. +- **BlockBogons** - Whether to block bogon networks. +- **Force** - Suppresses the confirmation prompt. +- **PassThru** - Returns the updated interface. + +#### Examples + +```powershell +# Update an interface's description +Set-OPNSenseInterface -Name "lan" -Description "Updated LAN Interface" + +# Update an interface's IPv4 address +Set-OPNSenseInterface -Name "opt1" -IPv4Address "192.168.2.254" -IPv4Subnet "24" + +# Update an interface's IPv6 address +Set-OPNSenseInterface -Name "opt2" -IPv6Address "2001:db8:2::1" -IPv6Subnet "64" + +# Enable security options for an interface +Set-OPNSenseInterface -Name "dmz" -BlockPrivate -BlockBogons + +# Disable an interface +Set-OPNSenseInterface -Name "opt3" -Enabled:$false + +# Update multiple properties of an interface +Set-OPNSenseInterface -Name "lan" -Description "Main LAN Interface" -IPv4Address "192.168.1.254" -IPv4Subnet "24" -BlockBogons -PassThru +``` + +### Remove-OPNSenseInterface + +Removes an interface from an OPNSense firewall. + +#### Parameters + +- **Name** - The name of the interface to remove. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Remove an interface +Remove-OPNSenseInterface -Name "opt3" + +# Remove an interface without confirmation +Remove-OPNSenseInterface -Name "vlan20" -Force +``` + +### Get-OPNSenseInterfaceStatistics + +Retrieves interface statistics from an OPNSense firewall. + +#### Parameters + +- **Name** - The name of a specific interface to get statistics for. If not specified, statistics for all interfaces are returned. + +#### Examples + +```powershell +# Get statistics for all interfaces +Get-OPNSenseInterfaceStatistics + +# Get statistics for a specific interface +Get-OPNSenseInterfaceStatistics -Name "lan" +``` + +### Apply-OPNSenseInterfaceChanges + +Applies pending interface changes on an OPNSense firewall. + +#### Parameters + +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Apply interface changes +Apply-OPNSenseInterfaceChanges + +# Apply interface changes without confirmation +Apply-OPNSenseInterfaceChanges -Force +``` + +## Common Scenarios + +### Basic Interface Configuration + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Configure the LAN interface +Set-OPNSenseInterface -Name "lan" -Description "Main LAN Interface" -IPv4Address "192.168.1.1" -IPv4Subnet "24" -Force + +# Configure the WAN interface +Set-OPNSenseInterface -Name "wan" -Description "WAN Interface" -BlockPrivate -BlockBogons -Force + +# Apply the changes +Apply-OPNSenseInterfaceChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Creating VLAN Interfaces + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Define VLAN interfaces +$vlans = @( + @{ Name = "vlan10"; Description = "Management VLAN"; Device = "em0.10"; IPv4Address = "10.0.10.1"; IPv4Subnet = "24" }, + @{ Name = "vlan20"; Description = "User VLAN"; Device = "em0.20"; IPv4Address = "10.0.20.1"; IPv4Subnet = "24" }, + @{ Name = "vlan30"; Description = "Guest VLAN"; Device = "em0.30"; IPv4Address = "10.0.30.1"; IPv4Subnet = "24" }, + @{ Name = "vlan40"; Description = "IoT VLAN"; Device = "em0.40"; IPv4Address = "10.0.40.1"; IPv4Subnet = "24" } +) + +# Create VLAN interfaces +foreach ($vlan in $vlans) { + New-OPNSenseInterface -Name $vlan.Name -Description $vlan.Description -Type "vlan" -Device $vlan.Device -IPv4Address $vlan.IPv4Address -IPv4Subnet $vlan.IPv4Subnet -Force +} + +# Apply the changes +Apply-OPNSenseInterfaceChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Configuring Dual-Stack IPv4/IPv6 Interfaces + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Define dual-stack interfaces +$interfaces = @( + @{ Name = "lan"; Description = "LAN Interface"; IPv4Address = "192.168.1.1"; IPv4Subnet = "24"; IPv6Address = "2001:db8:1::1"; IPv6Subnet = "64" }, + @{ Name = "opt1"; Description = "Optional Interface 1"; IPv4Address = "192.168.2.1"; IPv4Subnet = "24"; IPv6Address = "2001:db8:2::1"; IPv6Subnet = "64" }, + @{ Name = "dmz"; Description = "DMZ Interface"; IPv4Address = "192.168.100.1"; IPv4Subnet = "24"; IPv6Address = "2001:db8:100::1"; IPv6Subnet = "64" } +) + +# Configure dual-stack interfaces +foreach ($interface in $interfaces) { + Set-OPNSenseInterface -Name $interface.Name -Description $interface.Description -IPv4Address $interface.IPv4Address -IPv4Subnet $interface.IPv4Subnet -IPv6Address $interface.IPv6Address -IPv6Subnet $interface.IPv6Subnet -Force +} + +# Apply the changes +Apply-OPNSenseInterfaceChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Monitoring Interface Statistics + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get interface statistics +$statistics = Get-OPNSenseInterfaceStatistics + +# Display interface statistics +$statistics | Format-Table -Property Name, Status, InBytes, OutBytes, InPackets, OutPackets, Errors + +# Check for interfaces with errors +$interfacesWithErrors = $statistics | Where-Object { $_.Errors -gt 0 } +if ($interfacesWithErrors) { + Write-Output "The following interfaces have errors:" + $interfacesWithErrors | Format-Table -Property Name, Status, Errors +} + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Securing Interfaces + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get all interfaces +$interfaces = Get-OPNSenseInterface + +# Enable security options for external interfaces +foreach ($interface in $interfaces) { + if ($interface.Name -eq "wan" -or $interface.Name -like "opt*") { + Set-OPNSenseInterface -Name $interface.Name -BlockPrivate -BlockBogons -Force + Write-Output "Enabled security options for interface $($interface.Name)" + } +} + +# Apply the changes +Apply-OPNSenseInterfaceChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +## Notes + +- Interface changes are not applied until you call `Apply-OPNSenseInterfaceChanges`. +- When creating or updating interfaces, ensure that the IP addresses do not conflict with other interfaces. +- The `BlockPrivate` option blocks traffic from private networks (RFC 1918) on the interface. +- The `BlockBogons` option blocks traffic from bogon networks (unallocated or reserved IP space) on the interface. +- Interface names should follow OPNSense conventions (e.g., "lan", "wan", "opt1", "vlan10"). +- VLAN interfaces require the parent interface to be properly configured. +- Consider using the `-PassThru` parameter when updating interfaces to verify the changes. +- Interface statistics can be useful for monitoring network traffic and identifying issues. +- When removing an interface, ensure that no other configurations (e.g., firewall rules, NAT rules) reference the interface. +- IPv6 addresses should be specified in the standard format (e.g., "2001:db8::1"). +- IPv4 subnet masks can be specified in CIDR notation (e.g., "24" for "255.255.255.0") or dotted decimal notation (e.g., "255.255.255.0"). diff --git a/docs/NAT-Rules-Management.md b/docs/NAT-Rules-Management.md new file mode 100644 index 0000000..12c805f --- /dev/null +++ b/docs/NAT-Rules-Management.md @@ -0,0 +1,378 @@ +# NAT Rules Management + +This component provides cmdlets for managing Network Address Translation (NAT) rules on OPNSense firewalls. + +## Overview + +The NAT Rules Management component allows you to configure and manage NAT rules on OPNSense firewalls. It provides cmdlets for creating, viewing, modifying, and deleting different types of NAT rules, including port forwarding, outbound NAT, and 1:1 NAT. + +## Cmdlets + +### Get-OPNSenseNatRule + +Retrieves NAT rules from an OPNSense firewall. + +#### Parameters + +- **Uuid** - The UUID of a specific NAT rule to retrieve. If not specified, all NAT rules are returned. +- **Type** - Filter rules by type (port_forward, outbound, 1to1). +- **Interface** - Filter rules by interface. +- **Description** - Filter rules by description. + +#### Examples + +```powershell +# Get all NAT rules +Get-OPNSenseNatRule + +# Get a specific NAT rule by UUID +Get-OPNSenseNatRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" + +# Get all port forwarding rules +Get-OPNSenseNatRule -Type "port_forward" + +# Get all NAT rules for a specific interface +Get-OPNSenseNatRule -Interface "wan" +``` + +### New-OPNSensePortForwardRule + +Creates a new port forwarding rule on an OPNSense firewall. + +#### Parameters + +- **Interface** - The interface for the rule. +- **Protocol** - The protocol for the rule (tcp, udp, tcp/udp). +- **Source** - The source address for the rule. Default is "any". +- **SourcePort** - The source port for the rule. Default is "any". +- **Destination** - The destination address for the rule. Default is "wanip". +- **DestinationPort** - The destination port for the rule. +- **TargetIP** - The target IP address for the rule. +- **TargetPort** - The target port for the rule. +- **Description** - A description for the rule. +- **NatReflection** - The NAT reflection mode (enable, disable, purenat). Default is "enable". +- **FilterRuleAssociation** - Whether to create an associated filter rule. Default is true. +- **Enabled** - Whether the rule is enabled. Default is true. +- **Log** - Whether to log matches for this rule. Default is false. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Create a basic port forwarding rule +New-OPNSensePortForwardRule -Interface "wan" -Protocol "tcp" -DestinationPort "80" -TargetIP "192.168.1.100" -TargetPort "80" -Description "Web Server" + +# Create a port forwarding rule with custom source and destination +New-OPNSensePortForwardRule -Interface "wan" -Protocol "tcp" -Source "203.0.113.0/24" -DestinationPort "443" -TargetIP "192.168.1.100" -TargetPort "443" -Description "Secure Web Server" + +# Create a port forwarding rule with NAT reflection disabled +New-OPNSensePortForwardRule -Interface "wan" -Protocol "tcp" -DestinationPort "3389" -TargetIP "192.168.1.200" -TargetPort "3389" -Description "RDP Server" -NatReflection "disable" + +# Create a port forwarding rule without an associated filter rule +New-OPNSensePortForwardRule -Interface "wan" -Protocol "tcp" -DestinationPort "22" -TargetIP "192.168.1.200" -TargetPort "22" -Description "SSH Server" -FilterRuleAssociation:$false +``` + +### New-OPNSenseOutboundNatRule + +Creates a new outbound NAT rule on an OPNSense firewall. + +#### Parameters + +- **Interface** - The interface for the rule. +- **Source** - The source address for the rule. +- **Destination** - The destination address for the rule. Default is "any". +- **TranslationAddress** - The translation address for the rule. +- **Description** - A description for the rule. +- **Enabled** - Whether the rule is enabled. Default is true. +- **Log** - Whether to log matches for this rule. Default is false. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Create a basic outbound NAT rule +New-OPNSenseOutboundNatRule -Interface "wan" -Source "192.168.1.0/24" -TranslationAddress "203.0.113.1" -Description "Outbound NAT for LAN" + +# Create an outbound NAT rule with a specific destination +New-OPNSenseOutboundNatRule -Interface "wan" -Source "192.168.1.0/24" -Destination "198.51.100.0/24" -TranslationAddress "203.0.113.1" -Description "Outbound NAT for specific destination" + +# Create an outbound NAT rule with logging enabled +New-OPNSenseOutboundNatRule -Interface "wan" -Source "192.168.1.0/24" -TranslationAddress "203.0.113.1" -Description "Logged Outbound NAT" -Log +``` + +### New-OPNSense1to1NatRule + +Creates a new 1:1 NAT rule on an OPNSense firewall. + +#### Parameters + +- **Interface** - The interface for the rule. +- **ExternalNetwork** - The external network for the rule. +- **InternalIP** - The internal IP address for the rule. +- **Description** - A description for the rule. +- **NatReflection** - The NAT reflection mode (enable, disable, purenat). Default is "enable". +- **Enabled** - Whether the rule is enabled. Default is true. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Create a basic 1:1 NAT rule +New-OPNSense1to1NatRule -Interface "wan" -ExternalNetwork "203.0.113.10" -InternalIP "192.168.1.10" -Description "1:1 NAT for Server" + +# Create a 1:1 NAT rule with NAT reflection disabled +New-OPNSense1to1NatRule -Interface "wan" -ExternalNetwork "203.0.113.11" -InternalIP "192.168.1.11" -Description "1:1 NAT without reflection" -NatReflection "disable" + +# Create a 1:1 NAT rule for a network +New-OPNSense1to1NatRule -Interface "wan" -ExternalNetwork "203.0.113.0/29" -InternalIP "192.168.1.0/29" -Description "1:1 NAT for Network" +``` + +### Set-OPNSenseNatRule + +Updates an existing NAT rule on an OPNSense firewall. + +#### Parameters + +- **Uuid** - The UUID of the NAT rule to update. +- **Interface** - The interface for the rule. +- **Protocol** - The protocol for the rule (tcp, udp, tcp/udp). +- **Source** - The source address for the rule. +- **SourcePort** - The source port for the rule. +- **Destination** - The destination address for the rule. +- **DestinationPort** - The destination port for the rule. +- **TargetIP** - The target IP address for the rule. +- **TargetPort** - The target port for the rule. +- **TranslationAddress** - The translation address for the rule. +- **ExternalNetwork** - The external network for the rule. +- **InternalIP** - The internal IP address for the rule. +- **Description** - A description for the rule. +- **NatReflection** - The NAT reflection mode (enable, disable, purenat). +- **FilterRuleAssociation** - Whether to create an associated filter rule. +- **Enabled** - Whether the rule is enabled. +- **Log** - Whether to log matches for this rule. +- **Force** - Suppresses the confirmation prompt. +- **PassThru** - Returns the updated rule. + +#### Examples + +```powershell +# Update a port forwarding rule's target IP +Set-OPNSenseNatRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -TargetIP "192.168.1.101" + +# Update a port forwarding rule's description +Set-OPNSenseNatRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -Description "Updated Web Server" + +# Update an outbound NAT rule's translation address +Set-OPNSenseNatRule -Uuid "b2c3d4e5-f6g7-h8i9-j0k1-l2m3n4o5p6q7" -TranslationAddress "203.0.113.2" + +# Update a 1:1 NAT rule's internal IP +Set-OPNSenseNatRule -Uuid "c3d4e5f6-g7h8-i9j0-k1l2-m3n4o5p6q7r8" -InternalIP "192.168.1.12" + +# Disable a NAT rule +Set-OPNSenseNatRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -Enabled:$false + +# Update multiple properties of a NAT rule +Set-OPNSenseNatRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -TargetIP "192.168.1.101" -TargetPort "8080" -Description "Updated Web Server" -PassThru +``` + +### Remove-OPNSenseNatRule + +Removes a NAT rule from an OPNSense firewall. + +#### Parameters + +- **Uuid** - The UUID of the NAT rule to remove. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Remove a NAT rule +Remove-OPNSenseNatRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" + +# Remove a NAT rule without confirmation +Remove-OPNSenseNatRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -Force +``` + +### Enable-OPNSenseNatRule + +Enables a NAT rule on an OPNSense firewall. + +#### Parameters + +- **Uuid** - The UUID of the NAT rule to enable. +- **Force** - Suppresses the confirmation prompt. +- **PassThru** - Returns the updated rule. + +#### Examples + +```powershell +# Enable a NAT rule +Enable-OPNSenseNatRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" + +# Enable a NAT rule and return the updated rule +Enable-OPNSenseNatRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -PassThru +``` + +### Disable-OPNSenseNatRule + +Disables a NAT rule on an OPNSense firewall. + +#### Parameters + +- **Uuid** - The UUID of the NAT rule to disable. +- **Force** - Suppresses the confirmation prompt. +- **PassThru** - Returns the updated rule. + +#### Examples + +```powershell +# Disable a NAT rule +Disable-OPNSenseNatRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" + +# Disable a NAT rule and return the updated rule +Disable-OPNSenseNatRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -PassThru +``` + +### Apply-OPNSenseNatChanges + +Applies pending NAT changes on an OPNSense firewall. + +#### Parameters + +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Apply NAT changes +Apply-OPNSenseNatChanges + +# Apply NAT changes without confirmation +Apply-OPNSenseNatChanges -Force +``` + +## Common Scenarios + +### Setting Up Port Forwarding for a Web Server + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Create port forwarding rules for HTTP and HTTPS +New-OPNSensePortForwardRule -Interface "wan" -Protocol "tcp" -DestinationPort "80" -TargetIP "192.168.1.100" -TargetPort "80" -Description "Web Server HTTP" -Force +New-OPNSensePortForwardRule -Interface "wan" -Protocol "tcp" -DestinationPort "443" -TargetIP "192.168.1.100" -TargetPort "443" -Description "Web Server HTTPS" -Force + +# Apply the changes +Apply-OPNSenseNatChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Configuring Outbound NAT for Multiple Networks + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Define networks and their translation addresses +$networks = @( + @{ Source = "192.168.1.0/24"; Translation = "203.0.113.1"; Description = "LAN Outbound NAT" }, + @{ Source = "192.168.2.0/24"; Translation = "203.0.113.2"; Description = "Guest Outbound NAT" }, + @{ Source = "192.168.3.0/24"; Translation = "203.0.113.3"; Description = "DMZ Outbound NAT" } +) + +# Create outbound NAT rules +foreach ($network in $networks) { + New-OPNSenseOutboundNatRule -Interface "wan" -Source $network.Source -TranslationAddress $network.Translation -Description $network.Description -Force +} + +# Apply the changes +Apply-OPNSenseNatChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Setting Up 1:1 NAT for a Server Farm + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Define server mappings +$servers = @( + @{ External = "203.0.113.10"; Internal = "192.168.1.10"; Description = "Web Server 1" }, + @{ External = "203.0.113.11"; Internal = "192.168.1.11"; Description = "Web Server 2" }, + @{ External = "203.0.113.12"; Internal = "192.168.1.12"; Description = "Database Server" }, + @{ External = "203.0.113.13"; Internal = "192.168.1.13"; Description = "Mail Server" } +) + +# Create 1:1 NAT rules +foreach ($server in $servers) { + New-OPNSense1to1NatRule -Interface "wan" -ExternalNetwork $server.External -InternalIP $server.Internal -Description $server.Description -Force +} + +# Apply the changes +Apply-OPNSenseNatChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Managing Existing NAT Rules + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get all NAT rules +$natRules = Get-OPNSenseNatRule + +# Update port forwarding rules to use a new target IP +$portForwardRules = $natRules | Where-Object { $_.Type -eq "port_forward" -and $_.TargetIP -eq "192.168.1.100" } +foreach ($rule in $portForwardRules) { + Set-OPNSenseNatRule -Uuid $rule.Uuid -TargetIP "192.168.1.101" -Description "$($rule.Description) (Updated)" -Force + Write-Output "Updated rule: $($rule.Description)" +} + +# Disable temporary NAT rules +$tempRules = $natRules | Where-Object { $_.Description -like "*Temporary*" } +foreach ($rule in $tempRules) { + Disable-OPNSenseNatRule -Uuid $rule.Uuid -Force + Write-Output "Disabled rule: $($rule.Description)" +} + +# Remove obsolete NAT rules +$obsoleteRules = $natRules | Where-Object { $_.Description -like "*Obsolete*" } +foreach ($rule in $obsoleteRules) { + Remove-OPNSenseNatRule -Uuid $rule.Uuid -Force + Write-Output "Removed rule: $($rule.Description)" +} + +# Apply the changes +Apply-OPNSenseNatChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +## Notes + +- NAT rules are processed in order, with the first matching rule being applied. +- Changes to NAT rules are not applied until you call `Apply-OPNSenseNatChanges`. +- Port forwarding rules create a mapping from an external port to an internal IP address and port. +- Outbound NAT rules control how internal addresses are translated when accessing external networks. +- 1:1 NAT rules create a one-to-one mapping between an external IP address and an internal IP address. +- NAT reflection allows internal clients to access forwarded services using the external IP address. +- The `FilterRuleAssociation` parameter for port forwarding rules determines whether a corresponding firewall rule is created. +- When creating or updating NAT rules, consider the rule order and potential security implications. +- Use the `-Log` parameter for rules that you want to monitor for security purposes. +- Consider using the `-PassThru` parameter when updating NAT rules to verify the changes. +- NAT rules can reference aliases for source, destination, and port fields. +- The `NatReflection` parameter can be set to "enable", "disable", or "purenat". +- The `Source` parameter can be set to "any" to match any source address. +- The `Destination` parameter for port forwarding rules can be set to "wanip" to use the WAN IP address. +- The `TranslationAddress` parameter for outbound NAT rules can be set to "wanip" to use the WAN IP address. diff --git a/docs/Network-Utilities.md b/docs/Network-Utilities.md new file mode 100644 index 0000000..a0afa4e --- /dev/null +++ b/docs/Network-Utilities.md @@ -0,0 +1,155 @@ +# Network Utilities + +This component provides cmdlets for performing network calculations and conversions. + +## Overview + +The Network Utilities component includes cmdlets for working with IP networks, subnets, CIDR notation, and other network-related calculations. These utilities help with planning and managing network configurations on OPNSense firewalls. + +## Cmdlets + +### Invoke-OPNSenseNetworkCalculation + +Performs various network calculations on IP networks. + +#### Parameters + +- **Network** - The network in CIDR notation to perform calculations on. +- **Operation** - The operation to perform. Valid values are: + - **Info** - Shows detailed information about the network. + - **Subnet** - Divides the network into subnets of a specified prefix length. + - **SubnetByCount** - Divides the network into a specified number of subnets. + - **Contains** - Checks if an IP address or network is contained within the specified network. + - **Overlaps** - Checks if the network overlaps with other networks. + - **Supernet** - Combines the network with other networks into a supernet. + - **SupernetSummarize** - Summarizes multiple networks into the smallest possible set of networks. +- **PrefixLength** - The prefix length to use for subnetting. +- **SubnetCount** - The number of subnets to create when using SubnetByCount. +- **IpAddress** - The IP address to check when using Contains. +- **AdditionalNetworks** - Additional networks to use with Overlaps, Supernet, or SupernetSummarize. + +#### Examples + +```powershell +# Show network information +Invoke-OPNSenseNetworkCalculation -Network "192.168.1.0/24" -Operation Info + +# Subnet a network into /27 networks +Invoke-OPNSenseNetworkCalculation -Network "10.0.0.0/24" -Operation Subnet -PrefixLength 27 + +# Create 4 equal-sized subnets +Invoke-OPNSenseNetworkCalculation -Network "172.16.0.0/24" -Operation SubnetByCount -SubnetCount 4 + +# Check if an IP is within a network +Invoke-OPNSenseNetworkCalculation -Network "192.168.1.0/24" -Operation Contains -IpAddress "192.168.1.100" + +# Check if networks overlap +Invoke-OPNSenseNetworkCalculation -Network "10.0.0.0/16" -Operation Overlaps -AdditionalNetworks "10.0.1.0/24","10.0.2.0/24" + +# Combine networks into a supernet +Invoke-OPNSenseNetworkCalculation -Network "192.168.0.0/24" -Operation Supernet -AdditionalNetworks "192.168.1.0/24" + +# Summarize multiple networks +Invoke-OPNSenseNetworkCalculation -Network "10.0.0.0/24" -Operation SupernetSummarize -AdditionalNetworks "10.0.1.0/24","10.0.2.0/24" +``` + +### ConvertTo-OPNSenseNetworkNotation + +Converts between different network notation formats. + +#### Parameters + +- **CIDR** - The CIDR prefix length to convert to a subnet mask. +- **SubnetMask** - The subnet mask to convert to a CIDR prefix length. + +#### Examples + +```powershell +# Convert CIDR to subnet mask +ConvertTo-OPNSenseNetworkNotation -CIDR 24 # Returns "255.255.255.0" + +# Convert subnet mask to CIDR +ConvertTo-OPNSenseNetworkNotation -SubnetMask "255.255.255.0" # Returns 24 +``` + +## Common Scenarios + +### Network Planning + +```powershell +# Plan a network subdivision +$network = "10.0.0.0/16" +$subnets = Invoke-OPNSenseNetworkCalculation -Network $network -Operation Subnet -PrefixLength 24 +$subnets | Format-Table -Property CIDR, Network, Broadcast, FirstUsableHost, LastUsableHost, UsableHostCount + +# Create equal-sized subnets +$departmentCount = 8 +$subnets = Invoke-OPNSenseNetworkCalculation -Network "172.16.0.0/20" -Operation SubnetByCount -SubnetCount $departmentCount +$subnets | Format-Table -Property CIDR, Network, Broadcast, UsableHostCount +``` + +### Network Verification + +```powershell +# Verify if an IP address is within a network +$serverIP = "192.168.1.50" +$network = "192.168.1.0/24" +$isInNetwork = Invoke-OPNSenseNetworkCalculation -Network $network -Operation Contains -IpAddress $serverIP + +# Check if networks overlap +$network1 = "10.0.0.0/16" +$network2 = "10.0.5.0/24" +$overlaps = Invoke-OPNSenseNetworkCalculation -Network $network1 -Operation Overlaps -AdditionalNetworks $network2 +``` + +### Network Optimization + +```powershell +# Optimize a list of networks by summarizing them +$networks = @( + "192.168.1.0/24", + "192.168.2.0/24", + "192.168.3.0/24", + "192.168.4.0/24" +) +$optimized = Invoke-OPNSenseNetworkCalculation -Network $networks[0] -Operation SupernetSummarize -AdditionalNetworks $networks[1..($networks.Length-1)] +$optimized | Format-Table -Property CIDR, Network, Broadcast +``` + +### VLAN Planning + +```powershell +# Plan VLANs with appropriate subnet sizes +$corporateNetwork = "10.0.0.0/16" +$vlans = @{ + "Management" = 10 + "Servers" = 20 + "Users" = 30 + "Guest" = 40 + "IoT" = 50 +} + +foreach ($vlan in $vlans.GetEnumerator()) { + $vlanName = $vlan.Key + $vlanId = $vlan.Value + $prefix = switch ($vlanName) { + "Management" { 24 } # 254 hosts + "Servers" { 23 } # 510 hosts + "Users" { 22 } # 1022 hosts + "Guest" { 24 } # 254 hosts + "IoT" { 23 } # 510 hosts + } + + # Calculate network details + $subnet = Invoke-OPNSenseNetworkCalculation -Network $corporateNetwork -Operation Subnet -PrefixLength $prefix | Select-Object -First 1 + + Write-Output "VLAN $vlanId ($vlanName): $($subnet.CIDR) - $($subnet.UsableHostCount) usable hosts" +} +``` + +## Notes + +- The Network Utilities component uses the IPNetwork2 library for network calculations. +- All operations are performed locally and do not require communication with the OPNSense firewall. +- These utilities can be used for planning network configurations before implementing them on the firewall. +- When working with large networks, be aware that some operations (like subnetting a /8 network into /30 networks) may generate a large number of results. diff --git a/docs/Plugin-Management.md b/docs/Plugin-Management.md new file mode 100644 index 0000000..0cc1400 --- /dev/null +++ b/docs/Plugin-Management.md @@ -0,0 +1,296 @@ +# Plugin Management + +This component provides cmdlets for managing plugins on OPNSense firewalls. + +## Overview + +The Plugin Management component allows you to install, configure, and manage plugins on OPNSense firewalls. It provides cmdlets for listing available plugins, installing and uninstalling plugins, and managing plugin configurations. + +## Cmdlets + +### Get-OPNSensePlugin + +Retrieves a list of available and installed plugins on an OPNSense firewall. + +#### Parameters + +- **Name** - The name of a specific plugin to retrieve. If not specified, all plugins are returned. +- **Category** - Filter plugins by category. +- **Installed** - Filter plugins by installation status. + +#### Examples + +```powershell +# Get all plugins +Get-OPNSensePlugin + +# Get a specific plugin by name +Get-OPNSensePlugin -Name "os-tailscale" + +# Get all installed plugins +Get-OPNSensePlugin -Installed $true + +# Get all plugins in a specific category +Get-OPNSensePlugin -Category "security" +``` + +### Install-OPNSensePlugin + +Installs a plugin on an OPNSense firewall. + +#### Parameters + +- **Name** - The name of the plugin to install. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Install a plugin +Install-OPNSensePlugin -Name "os-tailscale" + +# Install a plugin without confirmation +Install-OPNSensePlugin -Name "os-wireguard" -Force +``` + +### Uninstall-OPNSensePlugin + +Uninstalls a plugin from an OPNSense firewall. + +#### Parameters + +- **Name** - The name of the plugin to uninstall. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Uninstall a plugin +Uninstall-OPNSensePlugin -Name "os-tailscale" + +# Uninstall a plugin without confirmation +Uninstall-OPNSensePlugin -Name "os-wireguard" -Force +``` + +### Enable-OPNSensePlugin + +Enables a plugin on an OPNSense firewall. + +#### Parameters + +- **Name** - The name of the plugin to enable. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Enable a plugin +Enable-OPNSensePlugin -Name "os-tailscale" + +# Enable a plugin without confirmation +Enable-OPNSensePlugin -Name "os-wireguard" -Force +``` + +### Disable-OPNSensePlugin + +Disables a plugin on an OPNSense firewall. + +#### Parameters + +- **Name** - The name of the plugin to disable. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Disable a plugin +Disable-OPNSensePlugin -Name "os-tailscale" + +# Disable a plugin without confirmation +Disable-OPNSensePlugin -Name "os-wireguard" -Force +``` + +### Get-OPNSensePluginConfiguration + +Retrieves the configuration of a plugin on an OPNSense firewall. + +#### Parameters + +- **Name** - The name of the plugin to get the configuration for. + +#### Examples + +```powershell +# Get the configuration of a plugin +Get-OPNSensePluginConfiguration -Name "os-tailscale" +``` + +### Set-OPNSensePluginConfiguration + +Updates the configuration of a plugin on an OPNSense firewall. + +#### Parameters + +- **Name** - The name of the plugin to update the configuration for. +- **Configuration** - The configuration to set. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Update the configuration of a plugin +$config = @{ + enabled = "1" + settings = @{ + hostname = "opnsense-tailscale" + acceptDns = "1" + } +} +Set-OPNSensePluginConfiguration -Name "os-tailscale" -Configuration $config + +# Update the configuration of a plugin without confirmation +Set-OPNSensePluginConfiguration -Name "os-tailscale" -Configuration $config -Force +``` + +### Apply-OPNSensePluginChanges + +Applies pending plugin changes on an OPNSense firewall. + +#### Parameters + +- **Name** - The name of the plugin to apply changes for. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Apply changes for a plugin +Apply-OPNSensePluginChanges -Name "os-tailscale" + +# Apply changes for a plugin without confirmation +Apply-OPNSensePluginChanges -Name "os-tailscale" -Force +``` + +## Common Scenarios + +### Installing and Configuring a Plugin + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Install the Tailscale plugin +Install-OPNSensePlugin -Name "os-tailscale" -Force + +# Configure the plugin +$config = @{ + enabled = "1" + settings = @{ + hostname = "opnsense-tailscale" + acceptDns = "1" + acceptRoutes = "1" + advertiseRoutes = "192.168.1.0/24,10.0.0.0/8" + } +} +Set-OPNSensePluginConfiguration -Name "os-tailscale" -Configuration $config -Force + +# Apply the changes +Apply-OPNSensePluginChanges -Name "os-tailscale" -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Managing Multiple Plugins + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Define plugins to install +$plugins = @( + "os-tailscale", + "os-wireguard", + "os-acme-client", + "os-theme-vicuna" +) + +# Install plugins +foreach ($plugin in $plugins) { + Install-OPNSensePlugin -Name $plugin -Force + Write-Output "Installed plugin: $plugin" +} + +# Get all installed plugins +$installedPlugins = Get-OPNSensePlugin -Installed $true +Write-Output "Installed plugins:" +$installedPlugins | Format-Table -Property Name, Version, Description + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Updating Plugin Configurations + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get the current configuration of a plugin +$currentConfig = Get-OPNSensePluginConfiguration -Name "os-wireguard" + +# Update the configuration +$currentConfig.server.enabled = "1" +$currentConfig.server.port = "51820" +$currentConfig.server.dns = "192.168.1.1" + +# Set the updated configuration +Set-OPNSensePluginConfiguration -Name "os-wireguard" -Configuration $currentConfig -Force + +# Apply the changes +Apply-OPNSensePluginChanges -Name "os-wireguard" -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Cleaning Up Unused Plugins + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get all installed plugins +$installedPlugins = Get-OPNSensePlugin -Installed $true + +# Define plugins to keep +$pluginsToKeep = @( + "os-tailscale", + "os-wireguard", + "os-acme-client" +) + +# Uninstall unused plugins +foreach ($plugin in $installedPlugins) { + if ($pluginsToKeep -notcontains $plugin.Name) { + Uninstall-OPNSensePlugin -Name $plugin.Name -Force + Write-Output "Uninstalled plugin: $($plugin.Name)" + } +} + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +## Notes + +- Plugin installation and uninstallation may require a firewall reboot to take effect. +- Some plugins may have dependencies that need to be installed first. +- Plugin configurations vary widely depending on the plugin. +- Always check the OPNSense documentation for specific plugin requirements and configuration options. +- Plugin management requires administrative privileges. +- Plugin installation and uninstallation can take some time, especially for larger plugins. +- Some plugins may not be available for all OPNSense versions. +- Plugin configurations are typically stored in XML format. +- Changes to plugin configurations may not take effect until the plugin service is restarted or the firewall is rebooted. +- Consider using the `-Force` parameter when installing or uninstalling plugins in scripts to avoid confirmation prompts. diff --git a/docs/Port-Forwarding.md b/docs/Port-Forwarding.md new file mode 100644 index 0000000..5a5fb00 --- /dev/null +++ b/docs/Port-Forwarding.md @@ -0,0 +1,158 @@ +# Port Forwarding Management + +This component provides cmdlets for managing port forwarding rules on OPNSense firewalls. + +## Overview + +Port forwarding (also known as destination NAT or DNAT) allows external traffic to be redirected to internal servers. This component provides cmdlets to create, view, modify, and delete port forwarding rules on OPNSense firewalls. + +## Cmdlets + +### Get-OPNSensePortForwardingRule + +Retrieves port forwarding rules from an OPNSense firewall. + +#### Parameters + +- **Uuid** - The UUID of a specific port forwarding rule to retrieve. If not specified, all rules are returned. + +#### Examples + +```powershell +# Get all port forwarding rules +Get-OPNSensePortForwardingRule + +# Get a specific port forwarding rule by UUID +Get-OPNSensePortForwardingRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" +``` + +### New-OPNSensePortForwardingRule + +Creates a new port forwarding rule on an OPNSense firewall. + +#### Parameters + +- **Enabled** - Whether the rule is enabled. Default is true. +- **Description** - A description for the rule. +- **Interface** - The interface for the rule (e.g., "WAN"). +- **Protocol** - The TCP/IP protocol (tcp, udp, tcp/udp). Default is "tcp". +- **Source** - The source address for the rule. Default is "any". +- **SourcePort** - The source port range for the rule. Default is "any". +- **Destination** - The destination address for the rule. Default is "wanip". +- **DestinationPort** - The destination port range for the rule. +- **TargetIP** - The target IP address for the rule. +- **TargetPort** - The target port for the rule. +- **NatReflection** - The NAT reflection mode (enable, disable, purenat). Default is "enable". +- **FilterRuleAssociation** - Whether to create an associated filter rule. Default is true. +- **Log** - Whether to enable logging for this rule. Default is false. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Create a new port forwarding rule for HTTP +New-OPNSensePortForwardingRule -Interface "WAN" -Protocol "tcp" -DestinationPort "80" -TargetIP "192.168.1.100" -TargetPort "80" -Description "Web Server HTTP" + +# Create a new port forwarding rule for HTTPS with logging enabled +New-OPNSensePortForwardingRule -Interface "WAN" -Protocol "tcp" -DestinationPort "443" -TargetIP "192.168.1.100" -TargetPort "443" -Description "Web Server HTTPS" -Log -Force +``` + +### Set-OPNSensePortForwardingRule + +Updates an existing port forwarding rule on an OPNSense firewall. + +#### Parameters + +- **Uuid** - The UUID of the port forwarding rule to update. +- **Enabled** - Whether the rule is enabled. +- **Description** - A description for the rule. +- **Interface** - The interface for the rule. +- **Protocol** - The TCP/IP protocol (tcp, udp, tcp/udp). +- **Source** - The source address for the rule. +- **SourcePort** - The source port range for the rule. +- **Destination** - The destination address for the rule. +- **DestinationPort** - The destination port range for the rule. +- **TargetIP** - The target IP address for the rule. +- **TargetPort** - The target port for the rule. +- **NatReflection** - The NAT reflection mode (enable, disable, purenat). +- **FilterRuleAssociation** - Whether to create an associated filter rule. +- **Log** - Whether to enable logging for this rule. +- **Force** - Suppresses the confirmation prompt. +- **PassThru** - Returns the updated rule. + +#### Examples + +```powershell +# Update a port forwarding rule's target IP +Set-OPNSensePortForwardingRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -TargetIP "192.168.1.200" + +# Update a port forwarding rule's description and enable logging +Set-OPNSensePortForwardingRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -Description "Updated Web Server" -Log -PassThru +``` + +### Remove-OPNSensePortForwardingRule + +Removes a port forwarding rule from an OPNSense firewall. + +#### Parameters + +- **Uuid** - The UUID of the port forwarding rule to remove. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Remove a port forwarding rule +Remove-OPNSensePortForwardingRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" + +# Remove a port forwarding rule without confirmation +Remove-OPNSensePortForwardingRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -Force +``` + +## Common Scenarios + +### Web Server Port Forwarding + +```powershell +# Forward HTTP and HTTPS to an internal web server +New-OPNSensePortForwardingRule -Interface "WAN" -Protocol "tcp" -DestinationPort "80" -TargetIP "192.168.1.100" -TargetPort "80" -Description "Web Server HTTP" +New-OPNSensePortForwardingRule -Interface "WAN" -Protocol "tcp" -DestinationPort "443" -TargetIP "192.168.1.100" -TargetPort "443" -Description "Web Server HTTPS" +``` + +### Remote Desktop Access + +```powershell +# Forward RDP to an internal server on a non-standard port +New-OPNSensePortForwardingRule -Interface "WAN" -Protocol "tcp" -DestinationPort "33389" -TargetIP "192.168.1.200" -TargetPort "3389" -Description "Remote Desktop" +``` + +### Game Server + +```powershell +# Forward multiple ports for a game server +New-OPNSensePortForwardingRule -Interface "WAN" -Protocol "tcp/udp" -DestinationPort "27015-27020" -TargetIP "192.168.1.150" -TargetPort "27015-27020" -Description "Game Server" +``` + +### Managing Rules + +```powershell +# Get all port forwarding rules +$rules = Get-OPNSensePortForwardingRule + +# Update all rules targeting a specific IP +$rules | Where-Object { $_.TargetIP -eq "192.168.1.100" } | ForEach-Object { + Set-OPNSensePortForwardingRule -Uuid $_.Uuid -TargetIP "192.168.1.101" -Force +} + +# Remove all rules with a specific description +$rules | Where-Object { $_.Description -like "*Temporary*" } | ForEach-Object { + Remove-OPNSensePortForwardingRule -Uuid $_.Uuid -Force +} +``` + +## Notes + +- Port forwarding rules are applied immediately after creation, update, or deletion. +- When creating or updating rules, consider security implications and only forward necessary ports. +- Use the `FilterRuleAssociation` parameter to automatically create associated firewall rules. +- NAT reflection allows internal clients to access forwarded services using the external IP address. diff --git a/docs/Route-Management.md b/docs/Route-Management.md new file mode 100644 index 0000000..4d92688 --- /dev/null +++ b/docs/Route-Management.md @@ -0,0 +1,291 @@ +# Route Management + +This component provides cmdlets for managing static routes on OPNSense firewalls. + +## Overview + +The Route Management component allows you to configure and manage static routes on OPNSense firewalls. It provides cmdlets for creating, viewing, modifying, and deleting static routes. + +## Cmdlets + +### Get-OPNSenseRoute + +Retrieves static routes from an OPNSense firewall. + +#### Parameters + +- **Uuid** - The UUID of a specific route to retrieve. If not specified, all routes are returned. +- **Network** - Filter routes by network. +- **Gateway** - Filter routes by gateway. +- **Description** - Filter routes by description. + +#### Examples + +```powershell +# Get all static routes +Get-OPNSenseRoute + +# Get a specific route by UUID +Get-OPNSenseRoute -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" + +# Get routes for a specific network +Get-OPNSenseRoute -Network "192.168.100.0/24" + +# Get routes using a specific gateway +Get-OPNSenseRoute -Gateway "WAN_GATEWAY" +``` + +### New-OPNSenseRoute + +Creates a new static route on an OPNSense firewall. + +#### Parameters + +- **Network** - The network for the route in CIDR notation. +- **Gateway** - The gateway for the route. +- **Description** - A description for the route. +- **Disabled** - Whether the route is disabled. Default is false. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Create a basic static route +New-OPNSenseRoute -Network "192.168.100.0/24" -Gateway "WAN_GATEWAY" -Description "Remote Office Network" + +# Create a disabled static route +New-OPNSenseRoute -Network "10.0.0.0/8" -Gateway "WAN2_GATEWAY" -Description "Corporate Network" -Disabled + +# Create a static route without confirmation +New-OPNSenseRoute -Network "172.16.0.0/16" -Gateway "WAN3_GATEWAY" -Description "Partner Network" -Force +``` + +### Set-OPNSenseRoute + +Updates an existing static route on an OPNSense firewall. + +#### Parameters + +- **Uuid** - The UUID of the route to update. +- **Network** - The network for the route in CIDR notation. +- **Gateway** - The gateway for the route. +- **Description** - A description for the route. +- **Disabled** - Whether the route is disabled. +- **Force** - Suppresses the confirmation prompt. +- **PassThru** - Returns the updated route. + +#### Examples + +```powershell +# Update a route's network +Set-OPNSenseRoute -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -Network "192.168.200.0/24" + +# Update a route's gateway +Set-OPNSenseRoute -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -Gateway "WAN2_GATEWAY" + +# Update a route's description +Set-OPNSenseRoute -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -Description "Updated Remote Office Network" + +# Enable a disabled route +Set-OPNSenseRoute -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -Disabled:$false + +# Update multiple properties of a route +Set-OPNSenseRoute -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -Network "192.168.200.0/24" -Gateway "WAN3_GATEWAY" -Description "Updated Remote Office Network" -PassThru +``` + +### Remove-OPNSenseRoute + +Removes a static route from an OPNSense firewall. + +#### Parameters + +- **Uuid** - The UUID of the route to remove. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Remove a route +Remove-OPNSenseRoute -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" + +# Remove a route without confirmation +Remove-OPNSenseRoute -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -Force +``` + +### Apply-OPNSenseRouteChanges + +Applies pending route changes on an OPNSense firewall. + +#### Parameters + +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Apply route changes +Apply-OPNSenseRouteChanges + +# Apply route changes without confirmation +Apply-OPNSenseRouteChanges -Force +``` + +## Common Scenarios + +### Basic Route Configuration + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Create a static route to a remote network +New-OPNSenseRoute -Network "192.168.100.0/24" -Gateway "WAN_GATEWAY" -Description "Remote Office Network" -Force + +# Apply the changes +Apply-OPNSenseRouteChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Managing Multiple Routes + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Define routes for different networks +$routes = @( + @{ Network = "192.168.100.0/24"; Gateway = "WAN_GATEWAY"; Description = "Remote Office 1" }, + @{ Network = "192.168.200.0/24"; Gateway = "WAN_GATEWAY"; Description = "Remote Office 2" }, + @{ Network = "10.0.0.0/8"; Gateway = "WAN2_GATEWAY"; Description = "Corporate Network" }, + @{ Network = "172.16.0.0/16"; Gateway = "WAN3_GATEWAY"; Description = "Partner Network" } +) + +# Create routes +foreach ($route in $routes) { + New-OPNSenseRoute -Network $route.Network -Gateway $route.Gateway -Description $route.Description -Force +} + +# Apply the changes +Apply-OPNSenseRouteChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Updating Routes for a Gateway Change + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get all routes using a specific gateway +$routes = Get-OPNSenseRoute -Gateway "WAN_GATEWAY" + +# Update routes to use a different gateway +foreach ($route in $routes) { + Set-OPNSenseRoute -Uuid $route.Uuid -Gateway "WAN2_GATEWAY" -Description "$($route.Description) (Updated)" -Force + Write-Output "Updated route for network $($route.Network) to use gateway WAN2_GATEWAY" +} + +# Apply the changes +Apply-OPNSenseRouteChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Temporarily Disabling Routes + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get all routes +$routes = Get-OPNSenseRoute + +# Disable routes for a specific network range +foreach ($route in $routes) { + if ($route.Network -like "10.*") { + Set-OPNSenseRoute -Uuid $route.Uuid -Disabled:$true -Force + Write-Output "Disabled route for network $($route.Network)" + } +} + +# Apply the changes +Apply-OPNSenseRouteChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Route Cleanup + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get all routes +$routes = Get-OPNSenseRoute + +# Remove routes with "Temporary" in the description +foreach ($route in $routes) { + if ($route.Description -like "*Temporary*") { + Remove-OPNSenseRoute -Uuid $route.Uuid -Force + Write-Output "Removed route for network $($route.Network) with description '$($route.Description)'" + } +} + +# Apply the changes +Apply-OPNSenseRouteChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Creating Routes from Network Calculations + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Calculate subnets +$network = "10.0.0.0/16" +$prefixLength = 24 +$subnets = Invoke-OPNSenseNetworkCalculation -Network $network -Operation Subnet -PrefixLength $prefixLength + +# Create routes for each subnet +foreach ($subnet in $subnets) { + # Determine the gateway based on the subnet + $gateway = if ($subnet.CIDR -like "10.0.1*") { + "WAN_GATEWAY" + } elseif ($subnet.CIDR -like "10.0.2*") { + "WAN2_GATEWAY" + } else { + "WAN3_GATEWAY" + } + + New-OPNSenseRoute -Network $subnet.CIDR -Gateway $gateway -Description "Subnet $($subnet.CIDR)" -Force + Write-Output "Created route for network $($subnet.CIDR) via gateway $gateway" +} + +# Apply the changes +Apply-OPNSenseRouteChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +## Notes + +- Route changes are not applied until you call `Apply-OPNSenseRouteChanges`. +- When creating a route, ensure that the gateway is properly configured and reachable. +- Static routes take precedence over dynamic routes. +- The network should be specified in CIDR notation (e.g., "192.168.1.0/24"). +- Routes can be temporarily disabled without removing them. +- Consider using the `-PassThru` parameter when updating routes to verify the changes. +- Route descriptions should be descriptive and follow a consistent naming convention. +- When managing multiple routes, consider using a CSV file or other structured data source to maintain the route information. +- Static routes are useful for directing traffic to specific networks through different gateways. +- For complex routing scenarios, consider using policy-based routing instead of static routes. diff --git a/docs/System-Backup-Restore.md b/docs/System-Backup-Restore.md new file mode 100644 index 0000000..0a95ef6 --- /dev/null +++ b/docs/System-Backup-Restore.md @@ -0,0 +1,296 @@ +# System Backup and Restore + +This component provides cmdlets for backing up and restoring OPNSense firewall configurations. + +## Overview + +The System Backup and Restore component allows you to create, download, upload, and restore backups of OPNSense firewall configurations. It provides cmdlets for managing full and partial backups, as well as scheduling automatic backups. + +## Cmdlets + +### Get-OPNSenseBackup + +Retrieves a list of available backups on an OPNSense firewall. + +#### Examples + +```powershell +# Get all available backups +Get-OPNSenseBackup +``` + +### New-OPNSenseBackup + +Creates a new backup of an OPNSense firewall configuration. + +#### Parameters + +- **IncludeRRD** - Whether to include RRD data in the backup. Default is false. +- **IncludeInstalled** - Whether to include installed packages in the backup. Default is false. +- **Encrypted** - Whether to encrypt the backup. Default is false. +- **Password** - The password to use for encryption. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Create a basic configuration backup +New-OPNSenseBackup + +# Create a full backup including RRD data and installed packages +New-OPNSenseBackup -IncludeRRD -IncludeInstalled + +# Create an encrypted backup +New-OPNSenseBackup -Encrypted -Password "SecurePassword123" + +# Create a full encrypted backup without confirmation +New-OPNSenseBackup -IncludeRRD -IncludeInstalled -Encrypted -Password "SecurePassword123" -Force +``` + +### Save-OPNSenseBackup + +Downloads a backup from an OPNSense firewall to a local file. + +#### Parameters + +- **BackupId** - The ID of the backup to download. +- **Path** - The local path to save the backup to. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Download the latest backup +$backups = Get-OPNSenseBackup +$latestBackup = $backups | Sort-Object -Property Date -Descending | Select-Object -First 1 +Save-OPNSenseBackup -BackupId $latestBackup.Id -Path "C:\Backups\opnsense-backup.xml" + +# Download a specific backup +Save-OPNSenseBackup -BackupId "config-1234567890.xml" -Path "C:\Backups\opnsense-specific-backup.xml" + +# Download a backup and overwrite existing file without confirmation +Save-OPNSenseBackup -BackupId "config-1234567890.xml" -Path "C:\Backups\opnsense-backup.xml" -Force +``` + +### Restore-OPNSenseBackup + +Restores an OPNSense firewall configuration from a backup. + +#### Parameters + +- **BackupId** - The ID of the backup to restore. +- **Password** - The password for encrypted backups. +- **RebootAfterRestore** - Whether to reboot the firewall after restoring. Default is false. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Restore a backup +Restore-OPNSenseBackup -BackupId "config-1234567890.xml" + +# Restore an encrypted backup +Restore-OPNSenseBackup -BackupId "config-1234567890.xml" -Password "SecurePassword123" + +# Restore a backup and reboot the firewall +Restore-OPNSenseBackup -BackupId "config-1234567890.xml" -RebootAfterRestore + +# Restore a backup without confirmation +Restore-OPNSenseBackup -BackupId "config-1234567890.xml" -Force +``` + +### Import-OPNSenseBackup + +Uploads and restores a backup file to an OPNSense firewall. + +#### Parameters + +- **Path** - The local path of the backup file to upload. +- **Password** - The password for encrypted backups. +- **RebootAfterRestore** - Whether to reboot the firewall after restoring. Default is false. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Upload and restore a backup +Import-OPNSenseBackup -Path "C:\Backups\opnsense-backup.xml" + +# Upload and restore an encrypted backup +Import-OPNSenseBackup -Path "C:\Backups\opnsense-encrypted-backup.xml" -Password "SecurePassword123" + +# Upload and restore a backup and reboot the firewall +Import-OPNSenseBackup -Path "C:\Backups\opnsense-backup.xml" -RebootAfterRestore + +# Upload and restore a backup without confirmation +Import-OPNSenseBackup -Path "C:\Backups\opnsense-backup.xml" -Force +``` + +### Remove-OPNSenseBackup + +Removes a backup from an OPNSense firewall. + +#### Parameters + +- **BackupId** - The ID of the backup to remove. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Remove a backup +Remove-OPNSenseBackup -BackupId "config-1234567890.xml" + +# Remove a backup without confirmation +Remove-OPNSenseBackup -BackupId "config-1234567890.xml" -Force +``` + +## Common Scenarios + +### Creating and Downloading a Backup + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Create a full backup +New-OPNSenseBackup -IncludeRRD -IncludeInstalled -Force + +# Get the latest backup +$backups = Get-OPNSenseBackup +$latestBackup = $backups | Sort-Object -Property Date -Descending | Select-Object -First 1 + +# Download the backup +$backupPath = "C:\Backups\opnsense-$(Get-Date -Format 'yyyyMMdd-HHmmss').xml" +Save-OPNSenseBackup -BackupId $latestBackup.Id -Path $backupPath -Force + +Write-Output "Backup saved to: $backupPath" + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Scheduled Backup with Rotation + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Create a new backup +New-OPNSenseBackup -IncludeRRD -IncludeInstalled -Force + +# Get all backups +$backups = Get-OPNSenseBackup + +# Keep only the 5 most recent backups +if ($backups.Count -gt 5) { + $backupsToRemove = $backups | Sort-Object -Property Date -Descending | Select-Object -Skip 5 + foreach ($backup in $backupsToRemove) { + Remove-OPNSenseBackup -BackupId $backup.Id -Force + Write-Output "Removed old backup: $($backup.Id)" + } +} + +# Download the latest backup +$latestBackup = $backups | Sort-Object -Property Date -Descending | Select-Object -First 1 +$backupPath = "C:\Backups\opnsense-$(Get-Date -Format 'yyyyMMdd').xml" +Save-OPNSenseBackup -BackupId $latestBackup.Id -Path $backupPath -Force + +Write-Output "Backup saved to: $backupPath" + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Restoring from a Local Backup + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Upload and restore a backup +$backupPath = "C:\Backups\opnsense-backup.xml" +Import-OPNSenseBackup -Path $backupPath -RebootAfterRestore -Force + +Write-Output "Backup restored from: $backupPath" +Write-Output "The firewall will reboot to apply the changes." + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Creating Encrypted Backups + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Generate a secure password +$password = -join ((65..90) + (97..122) + (48..57) | Get-Random -Count 16 | ForEach-Object { [char]$_ }) + +# Create an encrypted backup +New-OPNSenseBackup -IncludeRRD -IncludeInstalled -Encrypted -Password $password -Force + +# Get the latest backup +$backups = Get-OPNSenseBackup +$latestBackup = $backups | Sort-Object -Property Date -Descending | Select-Object -First 1 + +# Download the backup +$backupPath = "C:\Backups\opnsense-encrypted-$(Get-Date -Format 'yyyyMMdd-HHmmss').xml" +Save-OPNSenseBackup -BackupId $latestBackup.Id -Path $backupPath -Force + +# Save the password to a secure file +$passwordPath = "C:\Backups\opnsense-backup-password.txt" +$password | Out-File -FilePath $passwordPath -Force + +Write-Output "Encrypted backup saved to: $backupPath" +Write-Output "Password saved to: $passwordPath" +Write-Output "Keep the password file secure, as it is required to restore the backup." + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Managing Backup History + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get all backups +$backups = Get-OPNSenseBackup + +# Display backup information +Write-Output "Available backups:" +$backups | Format-Table -Property Id, Date, Size, Description + +# Calculate total backup size +$totalSize = ($backups | Measure-Object -Property Size -Sum).Sum +Write-Output "Total backup size: $($totalSize / 1MB) MB" + +# Remove backups older than 30 days +$cutoffDate = (Get-Date).AddDays(-30) +$oldBackups = $backups | Where-Object { [DateTime]::Parse($_.Date) -lt $cutoffDate } +foreach ($backup in $oldBackups) { + Remove-OPNSenseBackup -BackupId $backup.Id -Force + Write-Output "Removed old backup: $($backup.Id) from $($backup.Date)" +} + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +## Notes + +- Backups can be large, especially when including RRD data and installed packages. +- Encrypted backups provide additional security but require the password for restoration. +- The `RebootAfterRestore` parameter will cause the firewall to reboot immediately after restoring a backup. +- When restoring a backup, all current configuration settings will be replaced with those from the backup. +- Consider implementing a backup rotation strategy to manage disk space. +- Store backup passwords securely, as they are required to restore encrypted backups. +- Regular backups are essential for disaster recovery. +- Backup files contain sensitive information, so they should be stored securely. +- The backup ID is typically in the format "config-timestamp.xml". +- When scheduling automatic backups, consider using the `New-OPNSenseCronJob` cmdlet to create a scheduled task. +- Backups can be used to migrate configurations between OPNSense firewalls. +- Before major configuration changes, create a backup to allow for easy rollback if needed. diff --git a/docs/Tailscale-Integration.md b/docs/Tailscale-Integration.md new file mode 100644 index 0000000..911a705 --- /dev/null +++ b/docs/Tailscale-Integration.md @@ -0,0 +1,263 @@ +# Tailscale Integration + +This component provides cmdlets for managing Tailscale VPN on OPNSense firewalls. + +## Overview + +The Tailscale Integration component allows you to install, configure, and manage Tailscale VPN on OPNSense firewalls. It provides cmdlets for enabling Tailscale, configuring subnet routes, and managing the Tailscale connection. + +## Cmdlets + +### Get-OPNSenseTailscaleStatus + +Retrieves the status of Tailscale on an OPNSense firewall. + +#### Examples + +```powershell +# Get Tailscale status +Get-OPNSenseTailscaleStatus +``` + +### Install-OPNSenseTailscale + +Installs the Tailscale plugin on an OPNSense firewall. + +#### Parameters + +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Install Tailscale +Install-OPNSenseTailscale + +# Install Tailscale without confirmation +Install-OPNSenseTailscale -Force +``` + +### Enable-OPNSenseTailscale + +Enables Tailscale on an OPNSense firewall. + +#### Parameters + +- **AuthKey** - The Tailscale authentication key. +- **Hostname** - The hostname to use for the Tailscale node. +- **AcceptDNS** - Whether to accept DNS settings from Tailscale. +- **AcceptRoutes** - Whether to accept routes from Tailscale. +- **AdvertiseRoutes** - The routes to advertise to Tailscale. +- **ExitNode** - Whether to use this node as an exit node. +- **Force** - Suppresses the confirmation prompt. +- **PassThru** - Returns the Tailscale status. + +#### Examples + +```powershell +# Enable Tailscale with basic settings +Enable-OPNSenseTailscale -AuthKey "tskey-auth-abcdef123456" + +# Enable Tailscale with a custom hostname +Enable-OPNSenseTailscale -AuthKey "tskey-auth-abcdef123456" -Hostname "opnsense-firewall" + +# Enable Tailscale with DNS and route acceptance +Enable-OPNSenseTailscale -AuthKey "tskey-auth-abcdef123456" -AcceptDNS -AcceptRoutes + +# Enable Tailscale with route advertisement +Enable-OPNSenseTailscale -AuthKey "tskey-auth-abcdef123456" -AdvertiseRoutes "192.168.1.0/24,10.0.0.0/8" + +# Enable Tailscale as an exit node +Enable-OPNSenseTailscale -AuthKey "tskey-auth-abcdef123456" -ExitNode +``` + +### Disable-OPNSenseTailscale + +Disables Tailscale on an OPNSense firewall. + +#### Parameters + +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Disable Tailscale +Disable-OPNSenseTailscale + +# Disable Tailscale without confirmation +Disable-OPNSenseTailscale -Force +``` + +### Set-OPNSenseTailscaleRoutes + +Updates the routes advertised by Tailscale on an OPNSense firewall. + +#### Parameters + +- **AdvertiseRoutes** - The routes to advertise to Tailscale. +- **Force** - Suppresses the confirmation prompt. +- **PassThru** - Returns the Tailscale status. + +#### Examples + +```powershell +# Set Tailscale routes +Set-OPNSenseTailscaleRoutes -AdvertiseRoutes "192.168.1.0/24,10.0.0.0/8" + +# Set Tailscale routes without confirmation +Set-OPNSenseTailscaleRoutes -AdvertiseRoutes "192.168.1.0/24,10.0.0.0/8" -Force +``` + +### Get-OPNSenseTailscalePeers + +Retrieves the Tailscale peers connected to an OPNSense firewall. + +#### Examples + +```powershell +# Get Tailscale peers +Get-OPNSenseTailscalePeers +``` + +### Apply-OPNSenseTailscaleChanges + +Applies pending Tailscale changes on an OPNSense firewall. + +#### Parameters + +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Apply Tailscale changes +Apply-OPNSenseTailscaleChanges + +# Apply Tailscale changes without confirmation +Apply-OPNSenseTailscaleChanges -Force +``` + +## Common Scenarios + +### Installing and Configuring Tailscale + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Install Tailscale +Install-OPNSenseTailscale -Force + +# Enable Tailscale with basic settings +Enable-OPNSenseTailscale -AuthKey "tskey-auth-abcdef123456" -Hostname "opnsense-firewall" -Force + +# Apply the changes +Apply-OPNSenseTailscaleChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Configuring Tailscale as a Subnet Router + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get the current network interfaces +$interfaces = Get-OPNSenseInterface + +# Build a list of subnets to advertise +$subnets = @() +foreach ($interface in $interfaces) { + if ($interface.Name -ne "wan" -and $interface.IPv4Address) { + $subnet = "$($interface.IPv4Network)/$($interface.IPv4Subnet)" + $subnets += $subnet + } +} + +# Join the subnets with commas +$advertisedRoutes = $subnets -join "," + +# Enable Tailscale with route advertisement +Enable-OPNSenseTailscale -AuthKey "tskey-auth-abcdef123456" -AdvertiseRoutes $advertisedRoutes -Force + +# Apply the changes +Apply-OPNSenseTailscaleChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Configuring Tailscale as an Exit Node + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Enable Tailscale as an exit node +Enable-OPNSenseTailscale -AuthKey "tskey-auth-abcdef123456" -ExitNode -Force + +# Apply the changes +Apply-OPNSenseTailscaleChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Updating Tailscale Routes + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get the current Tailscale status +$status = Get-OPNSenseTailscaleStatus + +# Add a new subnet to the advertised routes +$currentRoutes = $status.AdvertisedRoutes -split "," +$newSubnet = "172.16.0.0/16" +if ($currentRoutes -notcontains $newSubnet) { + $currentRoutes += $newSubnet +} + +# Update the advertised routes +$advertisedRoutes = $currentRoutes -join "," +Set-OPNSenseTailscaleRoutes -AdvertiseRoutes $advertisedRoutes -Force + +# Apply the changes +Apply-OPNSenseTailscaleChanges -Force + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Monitoring Tailscale Peers + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get Tailscale peers +$peers = Get-OPNSenseTailscalePeers + +# Display peer information +$peers | Format-Table -Property Name, IP, Status, LastSeen + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +## Notes + +- Tailscale requires the Tailscale plugin to be installed on the OPNSense firewall. +- The `Install-OPNSenseTailscale` cmdlet will install the plugin if it's not already installed. +- Tailscale authentication keys can be generated from the Tailscale admin console. +- Authentication keys are single-use and expire after a short period. +- When advertising routes, ensure that the routes are valid and do not overlap with Tailscale's internal routes. +- The `AcceptDNS` option allows Tailscale to configure DNS settings on the OPNSense firewall. +- The `AcceptRoutes` option allows Tailscale to add routes to the OPNSense firewall. +- The `ExitNode` option allows other Tailscale nodes to use the OPNSense firewall as an internet gateway. +- Changes to Tailscale settings are not applied until you call `Apply-OPNSenseTailscaleChanges`. +- Consider using the `-PassThru` parameter when updating Tailscale settings to verify the changes. diff --git a/docs/Troubleshooting.md b/docs/Troubleshooting.md new file mode 100644 index 0000000..dbc7cd2 --- /dev/null +++ b/docs/Troubleshooting.md @@ -0,0 +1,334 @@ +# Troubleshooting Guide + +This document provides troubleshooting information for common issues with the PSOPNSenseAPI module. + +## Connection Issues + +### Unable to Connect to OPNSense Firewall + +**Symptoms:** +- `Connect-OPNSense` fails with connection errors +- Error messages about unreachable hosts or connection timeouts + +**Possible Causes and Solutions:** + +1. **Incorrect URL:** + - Ensure the URL is correct and includes the protocol (https://) + - Verify there are no typos in the domain name or IP address + + ```powershell + # Correct format + Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" + ``` + +2. **Network Connectivity:** + - Verify that you can reach the OPNSense firewall from your computer + - Try pinging the firewall or accessing the web interface in a browser + + ```powershell + # Test connectivity + Test-NetConnection -ComputerName "firewall.example.com" -Port 443 + ``` + +3. **Certificate Issues:** + - If the firewall uses a self-signed certificate, use the `-SkipCertificateCheck` parameter + + ```powershell + Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + ``` + +4. **Firewall Rules:** + - Check if there are any firewall rules blocking access to the OPNSense web interface + - Ensure that port 443 (HTTPS) is open for your IP address + +5. **Proxy Settings:** + - If you're behind a proxy, configure PowerShell to use the proxy + + ```powershell + # Configure proxy + [System.Net.WebRequest]::DefaultWebProxy = New-Object System.Net.WebProxy("http://proxy.example.com:8080") + [System.Net.WebRequest]::DefaultWebProxy.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials + ``` + +### Authentication Failures + +**Symptoms:** +- `Connect-OPNSense` fails with authentication errors +- Error messages about invalid credentials or unauthorized access + +**Possible Causes and Solutions:** + +1. **Incorrect API Key or Secret:** + - Verify that the API key and secret are correct + - Regenerate the API key and secret if necessary + +2. **API Access Not Enabled:** + - Ensure that API access is enabled for the user + - Check the user's privileges in the OPNSense web interface + +3. **User Permissions:** + - Verify that the user has the necessary privileges for the operations you're trying to perform + - Consider using a user with administrative privileges for testing + +4. **API Key Expiration:** + - Some OPNSense configurations may have API key expiration + - Generate a new API key if the current one has expired + +## Module Loading Issues + +### Module Not Found + +**Symptoms:** +- `Import-Module PSOPNSenseAPI` fails with module not found errors +- Commands from the module are not recognized + +**Possible Causes and Solutions:** + +1. **Module Not Installed:** + - Verify that the module is installed + + ```powershell + # Check if the module is installed + Get-Module -Name PSOPNSenseAPI -ListAvailable + ``` + +2. **Module Not in PSModulePath:** + - Check if the module is installed in a valid module path + + ```powershell + # List module paths + $env:PSModulePath -split ';' + ``` + +3. **Module Version Mismatch:** + - Check if you have multiple versions of the module installed + + ```powershell + # List all versions of the module + Get-Module -Name PSOPNSenseAPI -ListAvailable | Select-Object Name, Version + ``` + +4. **PowerShell Version Compatibility:** + - Verify that you're using a compatible PowerShell version + - The module requires PowerShell 5.1 or later + + ```powershell + # Check PowerShell version + $PSVersionTable + ``` + +### Module Loading Errors + +**Symptoms:** +- `Import-Module PSOPNSenseAPI` fails with loading errors +- Error messages about missing dependencies or assembly loading failures + +**Possible Causes and Solutions:** + +1. **Missing Dependencies:** + - Ensure that all dependencies are installed + - The module requires .NET Framework 4.7.2 or later for Windows PowerShell + +2. **Assembly Loading Issues:** + - Try loading the module with verbose output to identify the specific issue + + ```powershell + # Load module with verbose output + Import-Module -Name PSOPNSenseAPI -Verbose + ``` + +3. **File Corruption:** + - Reinstall the module to ensure all files are intact + + ```powershell + # Uninstall and reinstall the module + Uninstall-Module -Name PSOPNSenseAPI -Force + Install-Module -Name PSOPNSenseAPI -Force + ``` + +## Command Execution Issues + +### Command Fails with Error + +**Symptoms:** +- Commands fail with error messages +- Unexpected results or behavior + +**Possible Causes and Solutions:** + +1. **Not Connected to Firewall:** + - Ensure that you're connected to the firewall before running commands + + ```powershell + # Check if connected + if (-not $OPNSenseSessionState.Instance.ApiClient) { + Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" + } + ``` + +2. **Insufficient Permissions:** + - Verify that the user has the necessary privileges for the operation + - Check the error message for permission-related issues + +3. **Invalid Parameters:** + - Check that you're using the correct parameters and values + - Use `Get-Help` to see the required and optional parameters + + ```powershell + # Get help for a command + Get-Help -Name New-OPNSenseFirewallRule -Full + ``` + +4. **API Limitations:** + - Some operations may not be supported by the OPNSense API + - Check the OPNSense API documentation for supported operations + +5. **Firewall Configuration:** + - The current state of the firewall may prevent certain operations + - Check the OPNSense logs for more information + +### Changes Not Applied + +**Symptoms:** +- Commands appear to succeed, but changes are not reflected on the firewall +- Configuration changes don't persist after a firewall reboot + +**Possible Causes and Solutions:** + +1. **Apply Changes Not Called:** + - Many commands require an explicit call to apply the changes + + ```powershell + # Apply changes after making configuration changes + Apply-OPNSenseFirewallChanges + ``` + +2. **Caching Issues:** + - The OPNSense web interface may be showing cached data + - Refresh the web interface or wait a few moments + +3. **Conflicting Configurations:** + - Other configurations may be overriding your changes + - Check for conflicting rules or settings + +4. **Firewall Reboot Required:** + - Some changes may require a firewall reboot to take effect + + ```powershell + # Reboot the firewall + Invoke-OPNSenseReboot + ``` + +## Performance Issues + +### Slow Command Execution + +**Symptoms:** +- Commands take a long time to execute +- Timeouts or performance degradation + +**Possible Causes and Solutions:** + +1. **Network Latency:** + - High latency between your computer and the firewall can slow down commands + - Consider running commands from a location with better connectivity + +2. **Firewall Load:** + - The firewall may be under heavy load + - Try running commands during periods of lower activity + +3. **Large Data Sets:** + - Commands that retrieve large amounts of data may be slow + - Use filtering parameters to limit the data returned + + ```powershell + # Filter results to reduce data + Get-OPNSenseFirewallRule -Interface "lan" + ``` + +4. **Inefficient Scripts:** + - Scripts that make many individual API calls can be slow + - Batch operations where possible and minimize the number of API calls + +## Logging and Debugging + +### Enabling Verbose Logging + +To troubleshoot issues, you can enable verbose logging: + +```powershell +# Enable verbose output +$VerbosePreference = "Continue" + +# Run commands with verbose output +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -Verbose +``` + +### Capturing HTTP Traffic + +For advanced troubleshooting, you can capture the HTTP traffic between the module and the OPNSense firewall: + +```powershell +# Enable Fiddler capture +[System.Net.WebRequest]::DefaultWebProxy = New-Object System.Net.WebProxy("http://localhost:8888") +[System.Net.WebRequest]::DefaultWebProxy.BypassProxyOnLocal = $false +``` + +Note: This requires [Fiddler](https://www.telerik.com/fiddler) or a similar HTTP debugging proxy to be installed and running. + +### Checking OPNSense Logs + +Check the OPNSense logs for additional information: + +1. Log in to the OPNSense web interface +2. Navigate to **System > Log Files > General** +3. Look for entries related to the API or the operations you're performing + +## Common Error Messages + +### "Not connected to an OPNSense firewall" + +**Cause:** You're trying to run a command without first connecting to the firewall. + +**Solution:** Connect to the firewall before running commands: + +```powershell +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" +``` + +### "The remote server returned an error: (401) Unauthorized" + +**Cause:** The API key or secret is incorrect, or the user doesn't have the necessary privileges. + +**Solution:** Verify the API key and secret, and check the user's privileges. + +### "The remote server returned an error: (404) Not Found" + +**Cause:** The requested resource or API endpoint doesn't exist. + +**Solution:** Check that you're using the correct command and parameters, and that the resource exists on the firewall. + +### "The remote server returned an error: (500) Internal Server Error" + +**Cause:** An error occurred on the OPNSense firewall while processing the request. + +**Solution:** Check the OPNSense logs for more information, and verify that the firewall is functioning correctly. + +### "Unable to find type [System.Net.IPNetwork.IPNetwork]" + +**Cause:** The IPNetwork2 assembly is not loaded or is missing. + +**Solution:** Reinstall the module to ensure all dependencies are properly installed. + +## Getting Help + +If you're still experiencing issues after trying the troubleshooting steps above: + +1. Check the [GitHub repository](https://github.com/freedbygrace/PSOPNSenseAPI) for known issues and solutions +2. Open an issue on GitHub with detailed information about the problem +3. Include the following information in your issue: + - PSOPNSenseAPI module version + - PowerShell version + - OPNSense version + - Error messages (with sensitive information redacted) + - Steps to reproduce the issue diff --git a/docs/User-Management.md b/docs/User-Management.md new file mode 100644 index 0000000..5afce39 --- /dev/null +++ b/docs/User-Management.md @@ -0,0 +1,388 @@ +# User Management + +This component provides cmdlets for managing users on OPNSense firewalls. + +## Overview + +The User Management component allows you to create, view, modify, and delete users on OPNSense firewalls. It provides cmdlets for managing user accounts, privileges, and API keys. + +## Cmdlets + +### Get-OPNSenseUser + +Retrieves users from an OPNSense firewall. + +#### Parameters + +- **Username** - The username of a specific user to retrieve. If not specified, all users are returned. +- **Uid** - The UID of a specific user to retrieve. + +#### Examples + +```powershell +# Get all users +Get-OPNSenseUser + +# Get a specific user by username +Get-OPNSenseUser -Username "admin" + +# Get a specific user by UID +Get-OPNSenseUser -Uid 1000 +``` + +### New-OPNSenseUser + +Creates a new user on an OPNSense firewall. + +#### Parameters + +- **Username** - The username for the new user. +- **Password** - The password for the new user. +- **FullName** - The full name of the user. +- **Email** - The email address of the user. +- **Authorized** - Whether the user is authorized to log in. Default is true. +- **Privileges** - The privileges to assign to the user. +- **Group** - The group to assign the user to. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Create a basic user +New-OPNSenseUser -Username "john" -Password "SecurePassword123" -FullName "John Doe" -Email "john@example.com" + +# Create a user with specific privileges +New-OPNSenseUser -Username "jane" -Password "SecurePassword456" -FullName "Jane Smith" -Email "jane@example.com" -Privileges "page-system-access", "page-diagnostics-logs" + +# Create a user in a specific group +New-OPNSenseUser -Username "bob" -Password "SecurePassword789" -FullName "Bob Johnson" -Email "bob@example.com" -Group "admins" + +# Create a user without confirmation +New-OPNSenseUser -Username "alice" -Password "SecurePassword101" -FullName "Alice Brown" -Email "alice@example.com" -Force +``` + +### Set-OPNSenseUser + +Updates an existing user on an OPNSense firewall. + +#### Parameters + +- **Username** - The username of the user to update. +- **Password** - The new password for the user. +- **FullName** - The new full name of the user. +- **Email** - The new email address of the user. +- **Authorized** - Whether the user is authorized to log in. +- **Privileges** - The new privileges to assign to the user. +- **Group** - The new group to assign the user to. +- **Force** - Suppresses the confirmation prompt. +- **PassThru** - Returns the updated user. + +#### Examples + +```powershell +# Update a user's email address +Set-OPNSenseUser -Username "john" -Email "john.doe@example.com" + +# Update a user's password +Set-OPNSenseUser -Username "jane" -Password "NewSecurePassword456" + +# Update a user's privileges +Set-OPNSenseUser -Username "bob" -Privileges "page-system-access", "page-diagnostics-logs", "page-firewall-rules" + +# Update a user's group +Set-OPNSenseUser -Username "alice" -Group "operators" + +# Disable a user +Set-OPNSenseUser -Username "temp" -Authorized:$false + +# Update multiple properties of a user +Set-OPNSenseUser -Username "john" -FullName "John A. Doe" -Email "john.a.doe@example.com" -Privileges "page-system-access", "page-diagnostics-logs" -PassThru +``` + +### Remove-OPNSenseUser + +Removes a user from an OPNSense firewall. + +#### Parameters + +- **Username** - The username of the user to remove. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Remove a user +Remove-OPNSenseUser -Username "temp" + +# Remove a user without confirmation +Remove-OPNSenseUser -Username "guest" -Force +``` + +### Get-OPNSenseUserGroup + +Retrieves user groups from an OPNSense firewall. + +#### Parameters + +- **Name** - The name of a specific group to retrieve. If not specified, all groups are returned. + +#### Examples + +```powershell +# Get all user groups +Get-OPNSenseUserGroup + +# Get a specific user group by name +Get-OPNSenseUserGroup -Name "admins" +``` + +### New-OPNSenseUserGroup + +Creates a new user group on an OPNSense firewall. + +#### Parameters + +- **Name** - The name of the new group. +- **Description** - A description for the group. +- **Privileges** - The privileges to assign to the group. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Create a basic user group +New-OPNSenseUserGroup -Name "operators" -Description "System Operators" + +# Create a user group with specific privileges +New-OPNSenseUserGroup -Name "firewall-admins" -Description "Firewall Administrators" -Privileges "page-firewall-rules", "page-firewall-nat" + +# Create a user group without confirmation +New-OPNSenseUserGroup -Name "readonly" -Description "Read-Only Users" -Privileges "page-system-access" -Force +``` + +### Set-OPNSenseUserGroup + +Updates an existing user group on an OPNSense firewall. + +#### Parameters + +- **Name** - The name of the group to update. +- **Description** - The new description for the group. +- **Privileges** - The new privileges to assign to the group. +- **Force** - Suppresses the confirmation prompt. +- **PassThru** - Returns the updated group. + +#### Examples + +```powershell +# Update a user group's description +Set-OPNSenseUserGroup -Name "operators" -Description "System Operators with Limited Access" + +# Update a user group's privileges +Set-OPNSenseUserGroup -Name "firewall-admins" -Privileges "page-firewall-rules", "page-firewall-nat", "page-firewall-aliases" + +# Update multiple properties of a user group +Set-OPNSenseUserGroup -Name "readonly" -Description "Read-Only Access Users" -Privileges "page-system-access", "page-diagnostics-logs" -PassThru +``` + +### Remove-OPNSenseUserGroup + +Removes a user group from an OPNSense firewall. + +#### Parameters + +- **Name** - The name of the group to remove. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Remove a user group +Remove-OPNSenseUserGroup -Name "temp-group" + +# Remove a user group without confirmation +Remove-OPNSenseUserGroup -Name "guest-group" -Force +``` + +### Get-OPNSenseApiKey + +Retrieves API keys for a user on an OPNSense firewall. + +#### Parameters + +- **Username** - The username to get API keys for. + +#### Examples + +```powershell +# Get API keys for a user +Get-OPNSenseApiKey -Username "admin" +``` + +### New-OPNSenseApiKey + +Creates a new API key for a user on an OPNSense firewall. + +#### Parameters + +- **Username** - The username to create an API key for. +- **Description** - A description for the API key. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Create an API key for a user +New-OPNSenseApiKey -Username "admin" -Description "Automation API Key" + +# Create an API key without confirmation +New-OPNSenseApiKey -Username "john" -Description "Monitoring API Key" -Force +``` + +### Remove-OPNSenseApiKey + +Removes an API key from an OPNSense firewall. + +#### Parameters + +- **KeyId** - The ID of the API key to remove. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Remove an API key +Remove-OPNSenseApiKey -KeyId "abcdef123456" + +# Remove an API key without confirmation +Remove-OPNSenseApiKey -KeyId "abcdef123456" -Force +``` + +## Common Scenarios + +### Creating a New Administrator User + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Create a new administrator user +New-OPNSenseUser -Username "admin2" -Password "SecurePassword123" -FullName "Secondary Admin" -Email "admin2@example.com" -Group "admins" -Force + +# Create an API key for the new user +New-OPNSenseApiKey -Username "admin2" -Description "Admin API Key" -Force + +# Get the API key details +$apiKeys = Get-OPNSenseApiKey -Username "admin2" +Write-Output "API Key created for admin2:" +$apiKeys | Format-Table -Property KeyId, Secret, Description, Created + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Creating Users with Different Privilege Levels + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Create user groups with different privilege levels +New-OPNSenseUserGroup -Name "firewall-admins" -Description "Firewall Administrators" -Privileges "page-firewall-rules", "page-firewall-nat", "page-firewall-aliases" -Force +New-OPNSenseUserGroup -Name "network-admins" -Description "Network Administrators" -Privileges "page-interfaces", "page-routing", "page-diagnostics-interface" -Force +New-OPNSenseUserGroup -Name "monitoring" -Description "Monitoring Users" -Privileges "page-diagnostics-logs", "page-diagnostics-system", "page-status-dashboard" -Force + +# Create users in different groups +$users = @( + @{ Username = "firewall"; FullName = "Firewall Admin"; Email = "firewall@example.com"; Group = "firewall-admins" }, + @{ Username = "network"; FullName = "Network Admin"; Email = "network@example.com"; Group = "network-admins" }, + @{ Username = "monitor"; FullName = "Monitoring User"; Email = "monitor@example.com"; Group = "monitoring" } +) + +foreach ($user in $users) { + # Generate a secure password + $password = -join ((65..90) + (97..122) + (48..57) | Get-Random -Count 16 | ForEach-Object { [char]$_ }) + + # Create the user + New-OPNSenseUser -Username $user.Username -Password $password -FullName $user.FullName -Email $user.Email -Group $user.Group -Force + + Write-Output "Created user: $($user.Username) with password: $password" +} + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Managing API Keys + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get all users +$users = Get-OPNSenseUser + +# Create API keys for users without them +foreach ($user in $users) { + $apiKeys = Get-OPNSenseApiKey -Username $user.Username + + if (-not $apiKeys) { + New-OPNSenseApiKey -Username $user.Username -Description "Automated API Key" -Force + Write-Output "Created API key for user: $($user.Username)" + } else { + Write-Output "User $($user.Username) already has $($apiKeys.Count) API key(s)" + } +} + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Cleaning Up Inactive Users + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get all users +$users = Get-OPNSenseUser + +# Define a cutoff date (e.g., 90 days ago) +$cutoffDate = (Get-Date).AddDays(-90) + +# Disable inactive users +foreach ($user in $users) { + # Skip the admin user + if ($user.Username -eq "admin") { + continue + } + + # Check if the user has been inactive + if ($user.LastLogin -and [DateTime]::Parse($user.LastLogin) -lt $cutoffDate) { + Set-OPNSenseUser -Username $user.Username -Authorized:$false -Force + Write-Output "Disabled inactive user: $($user.Username) (Last login: $($user.LastLogin))" + } +} + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +## Notes + +- User management requires administrative privileges. +- The `admin` user cannot be removed. +- API keys provide access to the OPNSense API and should be kept secure. +- User privileges determine what actions a user can perform in the OPNSense web interface and API. +- User groups provide a way to assign the same privileges to multiple users. +- Passwords should be strong and meet the OPNSense password policy requirements. +- When a user is removed, all associated API keys are also removed. +- API keys can be used to authenticate with the OPNSense API without a username and password. +- Consider using the `-Force` parameter when managing users in scripts to avoid confirmation prompts. +- User accounts can be disabled by setting the `Authorized` parameter to `$false`. +- The `PassThru` parameter can be used to return the updated user or group for further processing. +- User privileges are typically specified as page identifiers (e.g., "page-system-access"). +- The available privileges can be found in the OPNSense web interface under System > Access > Privileges. +- User groups can be used to organize users and assign privileges more efficiently. +- API keys can have descriptions to help identify their purpose. +- When creating API keys, both the key ID and secret are returned, but the secret is only shown once. diff --git a/docs/VLAN-Subnet-Management.md b/docs/VLAN-Subnet-Management.md new file mode 100644 index 0000000..db188f7 --- /dev/null +++ b/docs/VLAN-Subnet-Management.md @@ -0,0 +1,173 @@ +# VLAN and Subnet Management + +This component provides cmdlets for managing VLANs and subnets on OPNSense firewalls. + +## Overview + +The VLAN and Subnet Management component allows you to create, view, modify, and delete VLANs on OPNSense firewalls. It also includes advanced functionality for automatically creating VLANs from subnet divisions, making it easy to implement complex network segmentation. + +## Cmdlets + +### Get-OPNSenseVLAN + +Retrieves VLAN configurations from an OPNSense firewall. + +#### Parameters + +- **Uuid** - The UUID of a specific VLAN to retrieve. If not specified, all VLANs are returned. + +#### Examples + +```powershell +# Get all VLANs +Get-OPNSenseVLAN + +# Get a specific VLAN by UUID +Get-OPNSenseVLAN -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" +``` + +### New-OPNSenseVLAN + +Creates a new VLAN on an OPNSense firewall. + +#### Parameters + +- **Interface** - The parent interface for the VLAN. +- **Tag** - The VLAN tag (1-4094). +- **Priority** - The VLAN priority (0-7). +- **Description** - A description for the VLAN. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Create a new VLAN +New-OPNSenseVLAN -Interface "em0" -Tag 10 -Description "Management VLAN" + +# Create a new VLAN with priority and without confirmation +New-OPNSenseVLAN -Interface "em1" -Tag 20 -Priority 5 -Description "Voice VLAN" -Force +``` + +### Remove-OPNSenseVLAN + +Removes a VLAN from an OPNSense firewall. + +#### Parameters + +- **Uuid** - The UUID of the VLAN to remove. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Remove a VLAN +Remove-OPNSenseVLAN -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" + +# Remove a VLAN without confirmation +Remove-OPNSenseVLAN -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -Force +``` + +### New-OPNSenseSubnetVLANs + +Creates VLANs for subnets by dividing a network into smaller subnets. + +#### Parameters + +- **ParentInterface** - The parent interface for the VLANs. +- **Network** - The network in CIDR notation to divide into subnets. +- **SubnetMaskBits** - The subnet mask bits for the subnets. +- **StartingVlanId** - The starting VLAN ID. +- **VlanIdIncrement** - The increment for VLAN IDs. +- **DescriptionPrefix** - The prefix for VLAN descriptions. +- **EnableDHCP** - Whether to enable DHCP on the created VLANs. +- **DHCPRangeStart** - The starting IP address for the DHCP range. +- **DHCPRangeEnd** - The ending IP address for the DHCP range. +- **DHCPDomain** - The domain name for DHCP clients. +- **DHCPDnsServers** - The DNS servers for DHCP clients. +- **Force** - Suppresses the confirmation prompt. + +#### Examples + +```powershell +# Create VLANs for subnets with sequential VLAN IDs +New-OPNSenseSubnetVLANs -ParentInterface "em0" -Network "192.168.0.0/24" -SubnetMaskBits 27 -StartingVlanId 10 -VlanIdIncrement 1 + +# Create VLANs for subnets with VLAN IDs in multiples of 10 +New-OPNSenseSubnetVLANs -ParentInterface "em1" -Network "10.0.0.0/16" -SubnetMaskBits 24 -StartingVlanId 100 -VlanIdIncrement 10 + +# Create VLANs for subnets with DHCP enabled +New-OPNSenseSubnetVLANs -ParentInterface "em2" -Network "172.16.0.0/20" -SubnetMaskBits 24 -StartingVlanId 200 -VlanIdIncrement 1 -EnableDHCP -DescriptionPrefix "VLAN-DHCP" + +# Create VLANs with custom DHCP settings +New-OPNSenseSubnetVLANs -ParentInterface "em3" -Network "192.168.100.0/24" -SubnetMaskBits 26 -StartingVlanId 300 -EnableDHCP -DHCPDomain "example.com" -DHCPDnsServers "8.8.8.8","8.8.4.4" +``` + +## Common Scenarios + +### Basic VLAN Management + +```powershell +# Get all VLANs +$vlans = Get-OPNSenseVLAN +$vlans | Format-Table -Property Uuid, Tag, Interface, Description + +# Create a new VLAN +New-OPNSenseVLAN -Interface "em0" -Tag 100 -Description "Server VLAN" + +# Remove a VLAN +$vlanToRemove = $vlans | Where-Object { $_.Description -eq "Temporary VLAN" } +if ($vlanToRemove) { + Remove-OPNSenseVLAN -Uuid $vlanToRemove.Uuid -Force +} +``` + +### Network Segmentation with VLANs + +```powershell +# Create VLANs for different departments +$departments = @{ + "Management" = 10 + "Finance" = 20 + "HR" = 30 + "Engineering" = 40 + "Sales" = 50 + "Guest" = 60 +} + +foreach ($dept in $departments.GetEnumerator()) { + New-OPNSenseVLAN -Interface "em0" -Tag $dept.Value -Description "$($dept.Key) VLAN" -Force +} +``` + +### Automated Subnet Division with VLANs + +```powershell +# Divide a /24 network into /27 subnets (8 subnets) with VLANs +New-OPNSenseSubnetVLANs -ParentInterface "em0" -Network "192.168.1.0/24" -SubnetMaskBits 27 -StartingVlanId 100 -VlanIdIncrement 1 -DescriptionPrefix "Dept-" + +# Divide a /16 network into /20 subnets (16 subnets) with VLANs in multiples of 10 +New-OPNSenseSubnetVLANs -ParentInterface "em1" -Network "10.0.0.0/16" -SubnetMaskBits 20 -StartingVlanId 100 -VlanIdIncrement 10 -DescriptionPrefix "Branch-" + +# Create VLANs with DHCP for a campus network +New-OPNSenseSubnetVLANs -ParentInterface "em2" -Network "172.16.0.0/16" -SubnetMaskBits 22 -StartingVlanId 200 -VlanIdIncrement 1 -EnableDHCP -DescriptionPrefix "Campus-" -DHCPDomain "campus.example.com" +``` + +### Creating a Multi-Tenant Network + +```powershell +# Create a multi-tenant network with VLANs and DHCP +$tenantNetwork = "10.100.0.0/16" +$tenantCount = 16 # Number of tenants +$subnetMaskBits = 20 # /20 networks for each tenant (4,094 hosts per tenant) + +New-OPNSenseSubnetVLANs -ParentInterface "em0" -Network $tenantNetwork -SubnetMaskBits $subnetMaskBits -StartingVlanId 1000 -VlanIdIncrement 1 -EnableDHCP -DescriptionPrefix "Tenant-" -DHCPDomain "tenants.example.com" -DHCPDnsServers "10.0.0.1","10.0.0.2" +``` + +## Notes + +- When creating VLANs, ensure that the parent interface supports VLAN tagging. +- The `New-OPNSenseSubnetVLANs` cmdlet automatically creates interfaces for each VLAN. +- If DHCP is enabled, the cmdlet configures DHCP servers for each VLAN with appropriate ranges. +- The VLAN ID must be between 1 and 4094, and must be unique for each parent interface. +- When dividing networks, ensure that the subnet mask bits are larger than the original network's mask bits (e.g., dividing a /24 into /27 subnets). +- The cmdlet automatically calculates appropriate gateway addresses for each subnet (first usable IP address). diff --git a/docs/Visual-Studio-Solution.md b/docs/Visual-Studio-Solution.md new file mode 100644 index 0000000..8970591 --- /dev/null +++ b/docs/Visual-Studio-Solution.md @@ -0,0 +1,132 @@ +# Visual Studio Solution + +This document provides information about the Visual Studio solution for the PSOPNSenseAPI project. + +## Overview + +The PSOPNSenseAPI solution is a Visual Studio solution that contains the source code for the PSOPNSenseAPI PowerShell module. The solution is designed to be opened in Visual Studio for development, debugging, and troubleshooting. + +## Solution Structure + +The solution contains the following projects: + +- **PSOPNSenseAPI**: The main project containing the PowerShell module code. + +## Opening the Solution + +To open the solution in Visual Studio: + +1. Launch Visual Studio +2. Select "Open a project or solution" +3. Navigate to the root directory of the PSOPNSenseAPI repository +4. Select the `PSOPNSenseAPI.sln` file +5. Click "Open" + +## Building the Solution + +To build the solution in Visual Studio: + +1. Open the solution in Visual Studio +2. Select the desired build configuration (Debug or Release) from the dropdown in the toolbar +3. Select "Build > Build Solution" from the menu or press F6 + +Alternatively, you can build the solution from the command line using the build scripts: + +```powershell +# Build the module +.\build\build.ps1 + +# Build a release version with automatic versioning +.\build\build-release.ps1 -Clean -Test -Package +``` + +## Debugging + +To debug the module in Visual Studio: + +1. Open the solution in Visual Studio +2. Set breakpoints in the code +3. Select "Debug > Start Debugging" from the menu or press F5 + +For debugging PowerShell cmdlets, you can also use the PowerShell Integrated Scripting Environment (ISE) or Visual Studio Code with the PowerShell extension. + +## Troubleshooting IPNetwork2 Issues + +If you encounter issues with the IPNetwork2 library, here are some common troubleshooting steps: + +1. **Check the reference**: Ensure that the IPNetwork2 package is properly referenced in the project file. + ```xml + + ``` + +2. **Verify the namespace**: The correct namespace for IPNetwork2 is `System.Net.IPNetwork`. + ```csharp + using System.Net.IPNetwork; + ``` + +3. **Check the class name**: The main class in IPNetwork2 is `IPNetwork`, not `IPNetwork2`. + ```csharp + // Correct usage + System.Net.IPNetwork.IPNetwork network = System.Net.IPNetwork.IPNetwork.Parse("192.168.1.0/24"); + + // Incorrect usage + IPNetwork2 network = IPNetwork2.Parse("192.168.1.0/24"); + ``` + +4. **Update package references**: If you're still having issues, try updating the package reference to the latest version. + ```powershell + # In Package Manager Console + Update-Package IPNetwork2 + ``` + +5. **Restore NuGet packages**: Ensure all NuGet packages are properly restored. + ```powershell + # In Package Manager Console + Restore-Package + ``` + +6. **Clean and rebuild**: Sometimes a clean rebuild can resolve reference issues. + ```powershell + # In Package Manager Console + Clean-Project + Build-Project + ``` + +## Adding New Features + +When adding new features to the solution: + +1. Create appropriate model classes in the `Models` directory +2. Implement service classes in the `Services` directory +3. Create PowerShell cmdlets in the `Cmdlets` directory +4. Update the module manifest (`PSOPNSenseAPI.psd1`) to export the new cmdlets +5. Add tests for the new features +6. Update documentation + +## Testing + +The solution includes unit tests that can be run from Visual Studio using the Test Explorer or from the command line: + +```powershell +# Run tests +.\build\test.ps1 +``` + +## Continuous Integration + +The project uses GitHub Actions for continuous integration. The CI workflow builds and tests the solution on every push to the main branch. + +## Dependencies + +The solution has the following dependencies: + +- PowerShellStandard.Library (5.1.1) +- Newtonsoft.Json (13.0.3) +- System.Net.Http (4.3.4) +- IPNetwork2 (2.6.618) + +## Notes + +- The solution is configured to target both .NET Framework 4.7.2 and .NET Standard 2.0 to support both Windows PowerShell 5.1 and PowerShell 7+. +- The module uses a versioning scheme of `yyyy.MM.dd.HHmm` for releases, which is automatically generated during the build process. +- The solution includes XML documentation comments that are used to generate help content for the PowerShell cmdlets. diff --git a/docs/en-US/about_PSOPNSenseAPI.help.txt b/docs/en-US/about_PSOPNSenseAPI.help.txt new file mode 100644 index 0000000..da771b2 --- /dev/null +++ b/docs/en-US/about_PSOPNSenseAPI.help.txt @@ -0,0 +1,135 @@ +TOPIC + about_PSOPNSenseAPI + +SHORT DESCRIPTION + PowerShell module for interacting with the OPNSense API to configure firewalls. + +LONG DESCRIPTION + The PSOPNSenseAPI module provides cmdlets for managing OPNSense firewalls through their API. + It allows you to perform various firewall management tasks such as: + + - Managing firewall rules (create, read, update, delete) + - Configuring NAT rules and port forwarding + - Managing aliases and address groups + - Managing network interfaces and VLANs + - Configuring DNS settings and overrides + - Backing up, restoring, exporting, and importing configurations + - Managing plugins (install, uninstall, enable, disable) + - Managing users and permissions + - Updating and upgrading firmware + - Rebooting firewall with wait for reconnection + - Creating VLANs from subnet divisions + - Configuring DHCP servers and static mappings + - Managing cron jobs + - Configuring and managing Tailscale VPN (with auto-installation) + - Applying and reverting configuration changes + + The module is built as a binary module in C# and is compatible with both PowerShell 5.1 and PowerShell 7. + +EXAMPLES + Connect to an OPNSense firewall: + + Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" + + Get all firewall rules: + + Get-OPNSenseFirewallRule + + Create a new firewall rule: + + New-OPNSenseFirewallRule -Description "Allow HTTP" -Protocol TCP -DestinationPort 80 -Action pass + + Apply changes: + + Apply-OPNSenseFirewallChanges + + Manage interfaces: + + Get-OPNSenseInterface + Set-OPNSenseInterface -Name "lan" -Description "Local Network" -IpAddress "192.168.1.1" -SubnetMask "24" + + Create VLANs from subnet divisions: + + New-OPNSenseSubnetVLANs -ParentInterface "em0" -Network "192.168.0.0/24" -SubnetMaskBits 27 -StartingVlanId 10 -VlanIdIncrement 10 + + Configure DNS: + + Set-OPNSenseDNSServer -Forwarding -Forwarders "8.8.8.8","8.8.4.4" -Apply + New-OPNSenseDNSOverride -Hostname "server" -Domain "local" -IpAddress "192.168.1.10" + + Manage plugins: + + Get-OPNSensePlugin -Installed + Install-OPNSensePlugin -Name "os-acme-client" -Wait + + Manage users: + + New-OPNSenseUser -Username "john" -Password "P@ssw0rd" -FullName "John Doe" -Groups "admins" + + Configure DHCP: + + Get-OPNSenseDHCPServer + Set-OPNSenseDHCPServer -Interface "lan" -RangeFrom "192.168.1.100" -RangeTo "192.168.1.200" -Apply + New-OPNSenseDHCPStaticMapping -Interface "lan" -MacAddress "00:11:22:33:44:55" -IpAddress "192.168.1.50" + + Manage cron jobs: + + Get-OPNSenseCronJob + New-OPNSenseCronJob -Description "Daily backup" -Command "/usr/local/bin/backup.sh" -Minutes "0" -Hours "2" -Apply + + Configure and manage Tailscale: + + Get-OPNSenseTailscaleStatus -IncludeInterfaces + Enable-OPNSenseTailscale -AcceptDns -AcceptRoutes -Force + # Advertise subnet routes to Tailscale network + Enable-OPNSenseTailscale -AdvertiseRoutes -SubnetRoutes "192.168.1.0/24","10.0.0.0/8" -Force + Connect-OPNSenseTailscale -AuthKey "tskey-auth-abcdef123456" -InstallIfMissing -Force + + Firmware management: + + Update-OPNSenseFirmware -Wait + + Reboot firewall: + + Restart-OPNSenseFirewall -Wait -Timeout 300 + + Backup and restore configuration: + + Backup-OPNSenseConfig -Filename "pre-upgrade-backup" + Export-OPNSenseConfig -Path "C:\Backups\opnsense-config.xml" + +KEYWORDS + OPNSense, Firewall, API, Network, Security + +SEE ALSO + Connect-OPNSense + Disconnect-OPNSense + Get-OPNSenseFirewallRule + New-OPNSenseFirewallRule + Apply-OPNSenseFirewallChanges + Get-OPNSenseInterface + Set-OPNSenseInterface + New-OPNSenseSubnetVLANs + Get-OPNSenseVLAN + New-OPNSenseVLAN + Get-OPNSenseDNSServer + Set-OPNSenseDNSServer + Get-OPNSensePlugin + Install-OPNSensePlugin + Get-OPNSenseUser + New-OPNSenseUser + Get-OPNSenseDHCPServer + Set-OPNSenseDHCPServer + Get-OPNSenseDHCPStaticMapping + New-OPNSenseDHCPStaticMapping + Get-OPNSenseCronJob + New-OPNSenseCronJob + Get-OPNSenseTailscaleStatus + Enable-OPNSenseTailscale + Connect-OPNSenseTailscale + Disconnect-OPNSenseTailscale + Get-OPNSenseFirmware + Update-OPNSenseFirmware + Restart-OPNSenseFirewall + Backup-OPNSenseConfig + Export-OPNSenseConfig diff --git a/docs/examples/Alias-Management.ps1 b/docs/examples/Alias-Management.ps1 new file mode 100644 index 0000000..4c79254 --- /dev/null +++ b/docs/examples/Alias-Management.ps1 @@ -0,0 +1,81 @@ +# Import the module +Import-Module PSOPNSenseAPI + +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get all aliases +$aliases = Get-OPNSenseAlias +Write-Output "All Aliases:" +$aliases | Format-Table -Property Uuid, Name, Type, Content, Description, Enabled + +# Get aliases by type +$hostAliases = Get-OPNSenseAlias -Type host +Write-Output "Host Aliases:" +$hostAliases | Format-Table -Property Uuid, Name, Content, Description + +$networkAliases = Get-OPNSenseAlias -Type network +Write-Output "Network Aliases:" +$networkAliases | Format-Table -Property Uuid, Name, Content, Description + +$portAliases = Get-OPNSenseAlias -Type port +Write-Output "Port Aliases:" +$portAliases | Format-Table -Property Uuid, Name, Content, Description + +# Get a specific alias by name +$webServersAlias = Get-OPNSenseAlias -Name "WebServers" +Write-Output "WebServers Alias:" +$webServersAlias | Format-Table -Property Uuid, Name, Type, Content, Description + +# Get a specific alias by UUID +$aliasUuid = $aliases[0].Uuid +$specificAlias = Get-OPNSenseAlias -Uuid $aliasUuid +Write-Output "Specific Alias by UUID:" +$specificAlias | Format-List + +# Create a new host alias +$webServersUuid = New-OPNSenseAlias -Name "WebServers" -Type host -Content "192.168.1.10,192.168.1.11" -Description "Web Servers" -Apply +Write-Output "Created WebServers alias with UUID: $webServersUuid" + +# Create a new network alias +$internalNetworksUuid = New-OPNSenseAlias -Name "InternalNetworks" -Type network -Content "192.168.1.0/24,192.168.2.0/24" -Description "Internal Networks" -Apply +Write-Output "Created InternalNetworks alias with UUID: $internalNetworksUuid" + +# Create a new port alias +$webPortsUuid = New-OPNSenseAlias -Name "WebPorts" -Type port -Content "80,443" -Protocol TCP -Description "Web Ports" -Apply +Write-Output "Created WebPorts alias with UUID: $webPortsUuid" + +# Create a new URL alias +$blockListUuid = New-OPNSenseAlias -Name "BlockList" -Type url -Content "https://example.com/blocklist.txt" -UpdateFrequency 1 -Description "Block List" -Apply +Write-Output "Created BlockList alias with UUID: $blockListUuid" + +# Update an alias's content +Set-OPNSenseAlias -Uuid $webServersUuid -Content "192.168.1.10,192.168.1.11,192.168.1.12" -Apply +Write-Output "Updated WebServers alias content" + +# Update an alias's description +Set-OPNSenseAlias -Uuid $internalNetworksUuid -Description "All Internal Networks" -Apply +Write-Output "Updated InternalNetworks alias description" + +# Disable an alias +Set-OPNSenseAlias -Uuid $blockListUuid -Disabled -Apply +Write-Output "Disabled BlockList alias" + +# Enable an alias +Set-OPNSenseAlias -Uuid $blockListUuid -Enabled -Apply +Write-Output "Enabled BlockList alias" + +# Enable counters for an alias +Set-OPNSenseAlias -Uuid $webServersUuid -EnableCounters -Apply +Write-Output "Enabled counters for WebServers alias" + +# Remove an alias +Remove-OPNSenseAlias -Uuid $blockListUuid -Force -Apply +Write-Output "Removed BlockList alias" + +# Remove aliases by pipeline +Get-OPNSenseAlias -Name "WebServers" | Remove-OPNSenseAlias -Force -Apply +Write-Output "Removed WebServers alias" + +# Disconnect from the firewall +Disconnect-OPNSense diff --git a/docs/examples/Basic-Usage.ps1 b/docs/examples/Basic-Usage.ps1 new file mode 100644 index 0000000..4838516 --- /dev/null +++ b/docs/examples/Basic-Usage.ps1 @@ -0,0 +1,22 @@ +# Import the module +Import-Module PSOPNSenseAPI + +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get the current connection information +Get-OPNSenseConnection + +# Get all firewall rules +$rules = Get-OPNSenseFirewallRule +$rules | Format-Table -Property Uuid, Description, Protocol, SourceNet, DestinationPort, Action + +# Create a new firewall rule to allow HTTP traffic +$ruleId = New-OPNSenseFirewallRule -Description "Allow HTTP" -Protocol TCP -DestinationPort 80 -Action pass +Write-Output "Created rule with ID: $ruleId" + +# Apply the changes +Apply-OPNSenseFirewallChanges -CancelRollback -Timeout 30 + +# Disconnect from the firewall +Disconnect-OPNSense diff --git a/docs/examples/Configuration-Management.ps1 b/docs/examples/Configuration-Management.ps1 new file mode 100644 index 0000000..aacdfa3 --- /dev/null +++ b/docs/examples/Configuration-Management.ps1 @@ -0,0 +1,29 @@ +# Import the module +Import-Module PSOPNSenseAPI + +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get all configuration backups +$backups = Get-OPNSenseConfigBackup +$backups | Format-Table -Property Filename, Description, FileSize, Date, Version + +# Create a new configuration backup +$backupFilename = Backup-OPNSenseConfig -Filename "pre-upgrade-backup" +Write-Output "Created backup: $backupFilename" + +# Export the configuration to a file +$exportPath = "C:\Backups\opnsense-config.xml" +Export-OPNSenseConfig -Path $exportPath -Force +Write-Output "Exported configuration to: $exportPath" + +# Import a configuration from a file +# Note: This will restart the firewall +# Import-OPNSenseConfig -Path $exportPath -Confirm + +# Restore a configuration backup +# Note: This will restart the firewall +# Restore-OPNSenseConfig -Filename $backupFilename -Confirm + +# Disconnect from the firewall +Disconnect-OPNSense diff --git a/docs/examples/Cron-Management.ps1 b/docs/examples/Cron-Management.ps1 new file mode 100644 index 0000000..82cb2b8 --- /dev/null +++ b/docs/examples/Cron-Management.ps1 @@ -0,0 +1,36 @@ +# Import the module +Import-Module PSOPNSenseAPI + +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get all cron jobs +$cronJobs = Get-OPNSenseCronJob +$cronJobs | Format-Table -Property Uuid, Description, Command, Minutes, Hours, Days, Months, Weekdays, Enabled + +# Get a specific cron job +$job = Get-OPNSenseCronJob -Uuid $cronJobs[0].Uuid +$job + +# Create a new cron job that runs daily at 2:00 AM +$dailyBackup = New-OPNSenseCronJob -Description "Daily backup" -Command "/usr/local/bin/backup.sh" -Minutes "0" -Hours "2" -Days "*" -Months "*" -Weekdays "*" -Apply +$dailyBackup + +# Create a new cron job that runs weekly on Sunday at 3:00 AM +$weeklyCleanup = New-OPNSenseCronJob -Description "Weekly cleanup" -Command "/usr/local/bin/cleanup.sh" -Minutes "0" -Hours "3" -Days "*" -Months "*" -Weekdays "0" -Apply +$weeklyCleanup + +# Update a cron job +Set-OPNSenseCronJob -Uuid $dailyBackup -Hours "3" -Description "Updated daily backup" -Apply + +# Disable a cron job +Disable-OPNSenseCronJob -Uuid $dailyBackup -Apply + +# Enable a cron job +Enable-OPNSenseCronJob -Uuid $dailyBackup -Apply + +# Remove a cron job +Remove-OPNSenseCronJob -Uuid $weeklyCleanup -Force -Apply + +# Disconnect from the firewall +Disconnect-OPNSense diff --git a/docs/examples/DHCP-Management.ps1 b/docs/examples/DHCP-Management.ps1 new file mode 100644 index 0000000..4fb5240 --- /dev/null +++ b/docs/examples/DHCP-Management.ps1 @@ -0,0 +1,41 @@ +# Import the module +Import-Module PSOPNSenseAPI + +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get all DHCP servers +$dhcpServers = Get-OPNSenseDHCPServer +$dhcpServers | Format-Table -Property Interface, Enabled, RangeFrom, RangeTo, Gateway + +# Get a specific DHCP server +$lanDhcp = Get-OPNSenseDHCPServer -Interface "lan" +$lanDhcp + +# Update a DHCP server +Set-OPNSenseDHCPServer -Interface "lan" -RangeFrom "192.168.1.100" -RangeTo "192.168.1.200" -Domain "example.local" -DnsServers "8.8.8.8","8.8.4.4" -Apply + +# Get all DHCP leases +$dhcpLeases = Get-OPNSenseDHCPLease +$dhcpLeases | Format-Table -Property MacAddress, IpAddress, Hostname, State + +# Get active DHCP leases +$activeLeases = $dhcpLeases | Where-Object { $_.State -eq "active" } +$activeLeases | Format-Table -Property MacAddress, IpAddress, Hostname + +# Get all static mappings for an interface +$staticMappings = Get-OPNSenseDHCPStaticMapping -Interface "lan" +$staticMappings | Format-Table -Property Uuid, MacAddress, IpAddress, Hostname, Description + +# Create a new static mapping +$newMapping = New-OPNSenseDHCPStaticMapping -Interface "lan" -MacAddress "00:11:22:33:44:55" -IpAddress "192.168.1.50" -Hostname "printer" -Description "Office Printer" -Apply +$newMapping + +# Update a static mapping +Set-OPNSenseDHCPStaticMapping -Interface "lan" -Uuid $newMapping -IpAddress "192.168.1.51" -Description "Updated Printer" -Apply + +# Remove a static mapping +Remove-OPNSenseDHCPStaticMapping -Interface "lan" -Uuid $newMapping -Force -Apply + +# Disconnect from the firewall +Disconnect-OPNSense diff --git a/docs/examples/DNS-Configuration.ps1 b/docs/examples/DNS-Configuration.ps1 new file mode 100644 index 0000000..65f3b86 --- /dev/null +++ b/docs/examples/DNS-Configuration.ps1 @@ -0,0 +1,27 @@ +# Import the module +Import-Module PSOPNSenseAPI + +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get DNS server configuration +$dnsConfig = Get-OPNSenseDNSServer +Write-Output "DNS Server Configuration:" +$dnsConfig + +# Update DNS server configuration +Set-OPNSenseDNSServer -Forwarding -Forwarders "8.8.8.8","8.8.4.4" -RegisterDhcp -RegisterDhcpDomain "local" -Apply + +# Get all DNS overrides +$overrides = Get-OPNSenseDNSOverride +$overrides | Format-Table -Property Uuid, Hostname, Domain, IpAddress, Description + +# Create a new DNS override +$overrideId = New-OPNSenseDNSOverride -Hostname "server" -Domain "local" -IpAddress "192.168.1.10" -Description "Internal server" -Apply +Write-Output "Created DNS override with ID: $overrideId" + +# Remove a DNS override +Remove-OPNSenseDNSOverride -Uuid $overrideId -Apply -Confirm:$false + +# Disconnect from the firewall +Disconnect-OPNSense diff --git a/docs/examples/DNS-Management.ps1 b/docs/examples/DNS-Management.ps1 new file mode 100644 index 0000000..80694ae --- /dev/null +++ b/docs/examples/DNS-Management.ps1 @@ -0,0 +1,52 @@ +# Import the module +Import-Module PSOPNSenseAPI + +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get DNS server configuration +$dnsServer = Get-OPNSenseDNSServer +Write-Output "DNS Server Configuration:" +$dnsServer | Format-List + +# Update DNS server configuration +Set-OPNSenseDNSServer -Forwarding -Forwarders "8.8.8.8","8.8.4.4" -Apply +Write-Output "DNS Server updated to use Google DNS" + +# Get DNS forwarding configuration +$dnsForwarding = Get-OPNSenseDNSForwarding +Write-Output "DNS Forwarding Configuration:" +$dnsForwarding | Format-List + +# Enable DNS forwarding +Set-OPNSenseDNSForwarding -Enabled -DnsServers "1.1.1.1","1.0.0.1" -Apply +Write-Output "DNS Forwarding enabled with Cloudflare DNS" + +# Get DNS forwarding hosts +$forwardingHosts = Get-OPNSenseDNSForwardingHost +Write-Output "DNS Forwarding Hosts:" +$forwardingHosts | Format-Table + +# Create a new DNS forwarding host +$newHost = New-OPNSenseDNSForwardingHost -Domain "example.com" -Server "192.168.1.10" -Description "Internal DNS Server" -Apply +Write-Output "Created new forwarding host with UUID: $newHost" + +# Update a DNS forwarding host +Set-OPNSenseDNSForwardingHost -Uuid $newHost -Server "192.168.1.20" -Apply +Write-Output "Updated forwarding host server" + +# Remove a DNS forwarding host +Remove-OPNSenseDNSForwardingHost -Uuid $newHost -Force -Apply +Write-Output "Removed forwarding host" + +# Get system DNS configuration +$systemDNS = Get-OPNSenseSystemDNS +Write-Output "System DNS Configuration:" +$systemDNS | Format-List + +# Update system DNS configuration +Set-OPNSenseSystemDNS -Hostname "firewall" -Domain "example.com" -DnsServers "8.8.8.8","8.8.4.4" -Apply +Write-Output "Updated system DNS configuration" + +# Disconnect from the firewall +Disconnect-OPNSense diff --git a/docs/examples/Firmware-Management.ps1 b/docs/examples/Firmware-Management.ps1 new file mode 100644 index 0000000..2ab71a3 --- /dev/null +++ b/docs/examples/Firmware-Management.ps1 @@ -0,0 +1,37 @@ +# Import the module +Import-Module PSOPNSenseAPI + +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get firmware status +$firmwareStatus = Get-OPNSenseFirmware +Write-Output "Firmware Status:" +$firmwareStatus + +# Check for updates +Get-OPNSenseFirmware -Check + +# Get firmware changelog +$changelog = Get-OPNSenseFirmware -Changelog +Write-Output "Firmware Changelog:" +$changelog + +# Get firmware audit +$audit = Get-OPNSenseFirmware -Audit +$audit | Format-Table -Property Name, Version, Installed + +# Update firmware (install updates) +Update-OPNSenseFirmware -Wait +Write-Output "Firmware updated successfully" + +# Upgrade firmware (major version upgrade) +# Note: This will restart the firewall +# Upgrade-OPNSenseFirmware -Wait -Timeout 1200 + +# Restart the firewall +Restart-OPNSenseFirewall -Wait -Timeout 300 +Write-Output "Firewall restarted successfully" + +# Disconnect from the firewall +Disconnect-OPNSense diff --git a/docs/examples/Gateway-Route-Management.ps1 b/docs/examples/Gateway-Route-Management.ps1 new file mode 100644 index 0000000..6d0924f --- /dev/null +++ b/docs/examples/Gateway-Route-Management.ps1 @@ -0,0 +1,59 @@ +# Import the module +Import-Module PSOPNSenseAPI + +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get all gateways +$gateways = Get-OPNSenseGateway +Write-Output "Gateways:" +$gateways | Format-Table + +# Get gateways with status information +$gatewaysWithStatus = Get-OPNSenseGateway -IncludeStatus +Write-Output "Gateways with Status:" +$gatewaysWithStatus | Format-Table + +# Create a new gateway +$newGateway = New-OPNSenseGateway -Name "WAN2_GW" -Interface "opt1" -IpAddress "203.0.113.1" -Description "Secondary WAN" -Apply +Write-Output "Created new gateway with UUID: $newGateway" + +# Update a gateway +Set-OPNSenseGateway -Uuid $newGateway -MonitorIp "8.8.8.8" -Weight 2 -Apply +Write-Output "Updated gateway monitor IP and weight" + +# Make a gateway the default gateway +Set-OPNSenseGateway -Uuid $newGateway -Default -Apply +Write-Output "Set gateway as default" + +# Get all routes +$routes = Get-OPNSenseRoute +Write-Output "Routes:" +$routes | Format-Table + +# Create a new route +$newRoute = New-OPNSenseRoute -Network "192.168.100.0/24" -Gateway "WAN2_GW" -Description "Remote Office" -Apply +Write-Output "Created new route with UUID: $newRoute" + +# Update a route +Set-OPNSenseRoute -Uuid $newRoute -Gateway "WAN_GW" -Apply +Write-Output "Updated route gateway" + +# Disable a route +Set-OPNSenseRoute -Uuid $newRoute -Disabled -Apply +Write-Output "Disabled route" + +# Enable a route +Set-OPNSenseRoute -Uuid $newRoute -Enabled -Apply +Write-Output "Enabled route" + +# Remove a route +Remove-OPNSenseRoute -Uuid $newRoute -Force -Apply +Write-Output "Removed route" + +# Remove a gateway +Remove-OPNSenseGateway -Uuid $newGateway -Force -Apply +Write-Output "Removed gateway" + +# Disconnect from the firewall +Disconnect-OPNSense diff --git a/docs/examples/Interface-Management.ps1 b/docs/examples/Interface-Management.ps1 new file mode 100644 index 0000000..4fc4732 --- /dev/null +++ b/docs/examples/Interface-Management.ps1 @@ -0,0 +1,27 @@ +# Import the module +Import-Module PSOPNSenseAPI + +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get all interfaces +$interfaces = Get-OPNSenseInterface +$interfaces | Format-Table -Property Name, Description, IpAddress, SubnetMask, Status + +# Get interface statistics +$stats = Get-OPNSenseInterfaceStatistics +$stats | Format-Table -Property Name, InPackets, OutPackets, InBytes, OutBytes + +# Update an interface +Set-OPNSenseInterface -Name "lan" -Description "Local Area Network" -IpAddress "192.168.1.1" -SubnetMask "24" -Apply + +# Create a new VLAN +$vlanId = New-OPNSenseVLAN -Interface "em0" -Tag 10 -Description "Management VLAN" +Write-Output "Created VLAN with ID: $vlanId" + +# Get all VLANs +$vlans = Get-OPNSenseVLAN +$vlans | Format-Table -Property Uuid, Tag, Interface, Description + +# Disconnect from the firewall +Disconnect-OPNSense diff --git a/docs/examples/Network-Supernetting.ps1 b/docs/examples/Network-Supernetting.ps1 new file mode 100644 index 0000000..414c2a8 --- /dev/null +++ b/docs/examples/Network-Supernetting.ps1 @@ -0,0 +1,57 @@ +# Import the module +Import-Module PSOPNSenseAPI + +# Basic supernetting of two networks +$supernet = Invoke-OPNSenseNetworkCalculation -Network "192.168.1.0/24" -Operation Supernet -AdditionalNetworks "192.168.2.0/24" +Write-Output "Supernet of 192.168.1.0/24 and 192.168.2.0/24:" +$supernet + +# Supernetting of multiple networks +$supernet = Invoke-OPNSenseNetworkCalculation -Network "10.0.0.0/24" -Operation Supernet -AdditionalNetworks "10.0.1.0/24","10.0.2.0/24","10.0.3.0/24" +Write-Output "Supernet of multiple /24 networks:" +$supernet + +# Advanced supernetting with summarization +$summarizedNetworks = Invoke-OPNSenseNetworkCalculation -Network "172.16.0.0/24" -Operation SupernetSummarize -AdditionalNetworks "172.16.1.0/24","172.16.2.0/24","172.16.4.0/24","172.16.5.0/24" +Write-Output "Summarized networks:" +$summarizedNetworks | Format-Table + +# Example of non-contiguous networks being summarized efficiently +$summarizedNetworks = Invoke-OPNSenseNetworkCalculation -Network "10.1.0.0/24" -Operation SupernetSummarize -AdditionalNetworks "10.1.1.0/24","10.1.2.0/24","10.2.0.0/24","10.2.1.0/24" +Write-Output "Summarized non-contiguous networks:" +$summarizedNetworks | Format-Table + +# Example with mixed IPv4 and IPv6 networks +$summarizedNetworks = Invoke-OPNSenseNetworkCalculation -Network "192.168.0.0/24" -Operation SupernetSummarize -AdditionalNetworks "192.168.1.0/24","2001:db8::/64","2001:db8:1::/64" +Write-Output "Summarized mixed IPv4 and IPv6 networks:" +$summarizedNetworks | Format-Table + +# Practical example: Summarizing a large number of networks +$networks = @( + "10.0.0.0/24" + "10.0.1.0/24" + "10.0.2.0/24" + "10.0.3.0/24" + "10.0.4.0/24" + "10.0.5.0/24" + "10.0.6.0/24" + "10.0.7.0/24" + "10.1.0.0/24" + "10.1.1.0/24" + "10.2.0.0/24" + "10.2.1.0/24" + "172.16.0.0/24" + "172.16.1.0/24" + "172.16.2.0/24" + "172.16.3.0/24" +) + +$summarizedNetworks = Invoke-OPNSenseNetworkCalculation -Network $networks[0] -Operation SupernetSummarize -AdditionalNetworks $networks[1..($networks.Length-1)] +Write-Output "Summarized large set of networks:" +$summarizedNetworks | Format-Table + +# Using the summarized networks for route configuration +Write-Output "Example of using summarized networks for route configuration:" +foreach ($network in $summarizedNetworks) { + Write-Output "New-OPNSenseRoute -Network '$network' -Gateway 'WAN_GW' -Description 'Summarized Route' -Apply" +} diff --git a/docs/examples/Network-Utilities.ps1 b/docs/examples/Network-Utilities.ps1 new file mode 100644 index 0000000..7444f3e --- /dev/null +++ b/docs/examples/Network-Utilities.ps1 @@ -0,0 +1,44 @@ +# Import the module +Import-Module PSOPNSenseAPI + +# Network information +$networkInfo = Invoke-OPNSenseNetworkCalculation -Network "192.168.1.0/24" -Operation Info +Write-Output "Network Information:" +$networkInfo | Format-List + +# Subnet a network into smaller networks +$subnets = Invoke-OPNSenseNetworkCalculation -Network "10.0.0.0/16" -Operation Subnet -PrefixLength 24 +Write-Output "First 5 subnets of 10.0.0.0/16 divided into /24 networks:" +$subnets | Select-Object -First 5 | Format-Table + +# Subnet a network into a specific number of subnets +$subnets = Invoke-OPNSenseNetworkCalculation -Network "172.16.0.0/16" -Operation SubnetByCount -SubnetCount 4 +Write-Output "172.16.0.0/16 divided into 4 equal subnets:" +$subnets | Format-Table + +# Check if an IP address is within a network +$contains = Invoke-OPNSenseNetworkCalculation -Network "192.168.1.0/24" -Operation Contains -IPAddress "192.168.1.100" +Write-Output "Is 192.168.1.100 within 192.168.1.0/24? $contains" + +# Check if networks overlap +$overlaps = Invoke-OPNSenseNetworkCalculation -Network "10.0.0.0/24" -Operation Overlaps -AdditionalNetworks "10.0.0.128/25" +Write-Output "Network overlap check:" +$overlaps | Format-Table + +# Create a supernet from multiple networks +$supernet = Invoke-OPNSenseNetworkCalculation -Network "192.168.1.0/24" -Operation Supernet -AdditionalNetworks "192.168.2.0/24","192.168.3.0/24" +Write-Output "Supernet of 192.168.1.0/24, 192.168.2.0/24, and 192.168.3.0/24:" +$supernet + +# Convert between CIDR and subnet mask +$subnetMask = ConvertTo-OPNSenseNetworkNotation -CIDR 24 +Write-Output "Subnet mask for /24: $subnetMask" + +$cidr = ConvertTo-OPNSenseNetworkNotation -SubnetMask "255.255.255.0" +Write-Output "CIDR for 255.255.255.0: $cidr" + +$ipWithSubnetMask = ConvertTo-OPNSenseNetworkNotation -IPWithCIDR "192.168.1.0/24" +Write-Output "IP with subnet mask for 192.168.1.0/24: $ipWithSubnetMask" + +$ipWithCIDR = ConvertTo-OPNSenseNetworkNotation -IPWithSubnetMask "192.168.1.0 255.255.255.0" +Write-Output "IP with CIDR for 192.168.1.0 255.255.255.0: $ipWithCIDR" diff --git a/docs/examples/Plugin-Management.ps1 b/docs/examples/Plugin-Management.ps1 new file mode 100644 index 0000000..ddcd4c3 --- /dev/null +++ b/docs/examples/Plugin-Management.ps1 @@ -0,0 +1,29 @@ +# Import the module +Import-Module PSOPNSenseAPI + +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get all installed plugins +$installedPlugins = Get-OPNSensePlugin -Installed +$installedPlugins | Format-Table -Property Name, Version, Comment, Enabled + +# Get all available plugins +$availablePlugins = Get-OPNSensePlugin -Available +$availablePlugins | Format-Table -Property Name, Version, Comment + +# Install a plugin +Install-OPNSensePlugin -Name "os-acme-client" -Wait +Write-Output "Plugin installed successfully" + +# Enable a plugin +Enable-OPNSensePlugin -Name "os-acme-client" + +# Disable a plugin +Disable-OPNSensePlugin -Name "os-acme-client" + +# Uninstall a plugin +Uninstall-OPNSensePlugin -Name "os-acme-client" -Wait -Confirm:$false + +# Disconnect from the firewall +Disconnect-OPNSense diff --git a/docs/examples/Port-Forwarding-README.md b/docs/examples/Port-Forwarding-README.md new file mode 100644 index 0000000..2eb4013 --- /dev/null +++ b/docs/examples/Port-Forwarding-README.md @@ -0,0 +1,212 @@ +# Port Forwarding Examples + +This document provides examples of using the PSOPNSenseAPI module to manage port forwarding rules on OPNSense firewalls. + +## Basic Port Forwarding + +The following example demonstrates how to create, view, update, and delete port forwarding rules: + +```powershell +# Import the module +Import-Module PSOPNSenseAPI + +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get all port forwarding rules +$rules = Get-OPNSensePortForwardingRule +Write-Output "Current port forwarding rules:" +$rules | Format-Table -Property Uuid, Description, Interface, Protocol, DestinationPort, TargetIP, TargetPort + +# Create a new port forwarding rule for HTTP +$httpRule = New-OPNSensePortForwardingRule -Interface "WAN" -Protocol "tcp" -DestinationPort "80" -TargetIP "192.168.1.100" -TargetPort "80" -Description "Web Server HTTP" +Write-Output "Created HTTP port forwarding rule:" +$httpRule | Format-Table -Property Uuid, Description, Interface, Protocol, DestinationPort, TargetIP, TargetPort + +# Create a new port forwarding rule for HTTPS +$httpsRule = New-OPNSensePortForwardingRule -Interface "WAN" -Protocol "tcp" -DestinationPort "443" -TargetIP "192.168.1.100" -TargetPort "443" -Description "Web Server HTTPS" +Write-Output "Created HTTPS port forwarding rule:" +$httpsRule | Format-Table -Property Uuid, Description, Interface, Protocol, DestinationPort, TargetIP, TargetPort + +# Update the HTTP rule to point to a different server +$updatedRule = Set-OPNSensePortForwardingRule -Uuid $httpRule.Uuid -TargetIP "192.168.1.200" -Description "Updated Web Server HTTP" -PassThru +Write-Output "Updated HTTP port forwarding rule:" +$updatedRule | Format-Table -Property Uuid, Description, Interface, Protocol, DestinationPort, TargetIP, TargetPort + +# Get a specific rule by UUID +$rule = Get-OPNSensePortForwardingRule -Uuid $httpsRule.Uuid +Write-Output "Retrieved HTTPS port forwarding rule:" +$rule | Format-Table -Property Uuid, Description, Interface, Protocol, DestinationPort, TargetIP, TargetPort + +# Remove the HTTP rule +Remove-OPNSensePortForwardingRule -Uuid $httpRule.Uuid -Force +Write-Output "Removed HTTP port forwarding rule" + +# Get all port forwarding rules again to verify changes +$rules = Get-OPNSensePortForwardingRule +Write-Output "Updated port forwarding rules:" +$rules | Format-Table -Property Uuid, Description, Interface, Protocol, DestinationPort, TargetIP, TargetPort + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +## Advanced Port Forwarding Scenarios + +### Web Server with Multiple Services + +This example shows how to set up port forwarding for a web server with multiple services: + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Define the web server details +$webServerIP = "192.168.1.100" +$services = @( + @{ Name = "HTTP"; Protocol = "tcp"; ExternalPort = "80"; InternalPort = "80" }, + @{ Name = "HTTPS"; Protocol = "tcp"; ExternalPort = "443"; InternalPort = "443" }, + @{ Name = "Alternative HTTP"; Protocol = "tcp"; ExternalPort = "8080"; InternalPort = "8080" }, + @{ Name = "WebSocket"; Protocol = "tcp"; ExternalPort = "9000"; InternalPort = "9000" } +) + +# Create port forwarding rules for each service +foreach ($service in $services) { + New-OPNSensePortForwardingRule -Interface "WAN" ` + -Protocol $service.Protocol ` + -DestinationPort $service.ExternalPort ` + -TargetIP $webServerIP ` + -TargetPort $service.InternalPort ` + -Description "Web Server - $($service.Name)" ` + -Force +} + +# Verify the rules were created +Get-OPNSensePortForwardingRule | Where-Object { $_.TargetIP -eq $webServerIP } | + Format-Table -Property Description, Protocol, DestinationPort, TargetPort + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Game Server Port Forwarding + +This example demonstrates how to set up port forwarding for a game server with multiple ports: + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Define the game server details +$gameServerIP = "192.168.1.150" +$gameServerPorts = @( + @{ Name = "Game Server - Main"; Protocol = "tcp/udp"; Ports = "27015" }, + @{ Name = "Game Server - Query"; Protocol = "udp"; Ports = "27016" }, + @{ Name = "Game Server - RCON"; Protocol = "tcp"; Ports = "27017" }, + @{ Name = "Game Server - Voice"; Protocol = "udp"; Ports = "9987-9989" } +) + +# Create port forwarding rules for the game server +foreach ($port in $gameServerPorts) { + New-OPNSensePortForwardingRule -Interface "WAN" ` + -Protocol $port.Protocol ` + -DestinationPort $port.Ports ` + -TargetIP $gameServerIP ` + -TargetPort $port.Ports ` + -Description $port.Name ` + -Force +} + +# Verify the rules were created +Get-OPNSensePortForwardingRule | Where-Object { $_.TargetIP -eq $gameServerIP } | + Format-Table -Property Description, Protocol, DestinationPort, TargetPort + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Remote Access Services + +This example shows how to set up port forwarding for remote access services: + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Define remote access services +$remoteServices = @( + @{ Name = "RDP - Admin Server"; TargetIP = "192.168.1.200"; ExternalPort = "33389"; InternalPort = "3389"; Protocol = "tcp" }, + @{ Name = "SSH - Dev Server"; TargetIP = "192.168.1.201"; ExternalPort = "2222"; InternalPort = "22"; Protocol = "tcp" }, + @{ Name = "VNC - Support"; TargetIP = "192.168.1.202"; ExternalPort = "5900"; InternalPort = "5900"; Protocol = "tcp" } +) + +# Create port forwarding rules for remote access +foreach ($service in $remoteServices) { + New-OPNSensePortForwardingRule -Interface "WAN" ` + -Protocol $service.Protocol ` + -DestinationPort $service.ExternalPort ` + -TargetIP $service.TargetIP ` + -TargetPort $service.InternalPort ` + -Description $service.Name ` + -Log ` # Enable logging for security + -Force +} + +# Verify the rules were created +Get-OPNSensePortForwardingRule | Where-Object { $_.Description -like "* - *" } | + Format-Table -Property Description, Protocol, DestinationPort, TargetIP, TargetPort + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +### Bulk Management of Port Forwarding Rules + +This example demonstrates how to perform bulk operations on port forwarding rules: + +```powershell +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get all port forwarding rules +$rules = Get-OPNSensePortForwardingRule + +# Disable all rules with "Temporary" in the description +$rules | Where-Object { $_.Description -like "*Temporary*" } | ForEach-Object { + Set-OPNSensePortForwardingRule -Uuid $_.Uuid -Enabled:$false -Force + Write-Output "Disabled rule: $($_.Description)" +} + +# Update all rules pointing to a decommissioned server +$oldServerIP = "192.168.1.100" +$newServerIP = "192.168.1.150" + +$rules | Where-Object { $_.TargetIP -eq $oldServerIP } | ForEach-Object { + Set-OPNSensePortForwardingRule -Uuid $_.Uuid -TargetIP $newServerIP -Description "$($_.Description) (Migrated)" -Force + Write-Output "Updated rule: $($_.Description) to point to $newServerIP" +} + +# Delete all rules with "Obsolete" in the description +$rules | Where-Object { $_.Description -like "*Obsolete*" } | ForEach-Object { + Remove-OPNSensePortForwardingRule -Uuid $_.Uuid -Force + Write-Output "Removed rule: $($_.Description)" +} + +# Verify changes +$updatedRules = Get-OPNSensePortForwardingRule +Write-Output "Rules after bulk operations:" +$updatedRules | Format-Table -Property Uuid, Description, Enabled, TargetIP, DestinationPort, TargetPort + +# Disconnect from the firewall +Disconnect-OPNSense +``` + +## Notes + +- Port forwarding rules are applied immediately after creation, update, or deletion. +- When creating or updating rules, consider security implications and only forward necessary ports. +- Use the `FilterRuleAssociation` parameter to automatically create associated firewall rules. +- NAT reflection allows internal clients to access forwarded services using the external IP address. +- For security-sensitive rules, enable logging with the `-Log` parameter. +- Always use strong authentication and encryption for remote access services. +- Consider using non-standard external ports for common services to reduce automated scanning attempts. diff --git a/docs/examples/Port-Forwarding.ps1 b/docs/examples/Port-Forwarding.ps1 new file mode 100644 index 0000000..2047833 --- /dev/null +++ b/docs/examples/Port-Forwarding.ps1 @@ -0,0 +1,42 @@ +# Import the module +Import-Module PSOPNSenseAPI + +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get all port forwarding rules +$rules = Get-OPNSensePortForwardingRule +Write-Output "Current port forwarding rules:" +$rules | Format-Table -Property Uuid, Description, Interface, Protocol, DestinationPort, TargetIP, TargetPort + +# Create a new port forwarding rule for HTTP +$httpRule = New-OPNSensePortForwardingRule -Interface "WAN" -Protocol "tcp" -DestinationPort "80" -TargetIP "192.168.1.100" -TargetPort "80" -Description "Web Server HTTP" +Write-Output "Created HTTP port forwarding rule:" +$httpRule | Format-Table -Property Uuid, Description, Interface, Protocol, DestinationPort, TargetIP, TargetPort + +# Create a new port forwarding rule for HTTPS +$httpsRule = New-OPNSensePortForwardingRule -Interface "WAN" -Protocol "tcp" -DestinationPort "443" -TargetIP "192.168.1.100" -TargetPort "443" -Description "Web Server HTTPS" +Write-Output "Created HTTPS port forwarding rule:" +$httpsRule | Format-Table -Property Uuid, Description, Interface, Protocol, DestinationPort, TargetIP, TargetPort + +# Update the HTTP rule to point to a different server +$updatedRule = Set-OPNSensePortForwardingRule -Uuid $httpRule.Uuid -TargetIP "192.168.1.200" -Description "Updated Web Server HTTP" -PassThru +Write-Output "Updated HTTP port forwarding rule:" +$updatedRule | Format-Table -Property Uuid, Description, Interface, Protocol, DestinationPort, TargetIP, TargetPort + +# Get a specific rule by UUID +$rule = Get-OPNSensePortForwardingRule -Uuid $httpsRule.Uuid +Write-Output "Retrieved HTTPS port forwarding rule:" +$rule | Format-Table -Property Uuid, Description, Interface, Protocol, DestinationPort, TargetIP, TargetPort + +# Remove the HTTP rule +Remove-OPNSensePortForwardingRule -Uuid $httpRule.Uuid -Force +Write-Output "Removed HTTP port forwarding rule" + +# Get all port forwarding rules again to verify changes +$rules = Get-OPNSensePortForwardingRule +Write-Output "Updated port forwarding rules:" +$rules | Format-Table -Property Uuid, Description, Interface, Protocol, DestinationPort, TargetIP, TargetPort + +# Disconnect from the firewall +Disconnect-OPNSense diff --git a/docs/examples/Subnet-VLAN-Creation.ps1 b/docs/examples/Subnet-VLAN-Creation.ps1 new file mode 100644 index 0000000..bad9495 --- /dev/null +++ b/docs/examples/Subnet-VLAN-Creation.ps1 @@ -0,0 +1,31 @@ +# Import the module +Import-Module PSOPNSenseAPI + +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get available interfaces +$interfaces = Get-OPNSenseInterface +$interfaces | Format-Table -Property Name, Description, Status + +# Create VLANs for subnets with sequential VLAN IDs +$result1 = New-OPNSenseSubnetVLANs -ParentInterface "em0" -Network "192.168.0.0/24" -SubnetMaskBits 27 -StartingVlanId 10 -VlanIdIncrement 1 +Write-Output "Created VLANs with sequential IDs:" +$result1.VLANs | Format-Table -Property VlanId, Subnet, Gateway, Status + +# Create VLANs for subnets with VLAN IDs in multiples of 10 +$result2 = New-OPNSenseSubnetVLANs -ParentInterface "em1" -Network "10.0.0.0/16" -SubnetMaskBits 24 -StartingVlanId 100 -VlanIdIncrement 10 +Write-Output "Created VLANs with IDs in multiples of 10:" +$result2.VLANs | Format-Table -Property VlanId, Subnet, Gateway, Status + +# Create VLANs for subnets with DHCP enabled +$result3 = New-OPNSenseSubnetVLANs -ParentInterface "em2" -Network "172.16.0.0/20" -SubnetMaskBits 24 -StartingVlanId 200 -VlanIdIncrement 1 -EnableDHCP -DescriptionPrefix "VLAN-DHCP" +Write-Output "Created VLANs with DHCP enabled:" +$result3.VLANs | Format-Table -Property VlanId, Subnet, Gateway, Status + +# Get all VLANs +$vlans = Get-OPNSenseVLAN +$vlans | Format-Table -Property Uuid, Tag, Interface, Description + +# Disconnect from the firewall +Disconnect-OPNSense diff --git a/docs/examples/Tailscale-Management.ps1 b/docs/examples/Tailscale-Management.ps1 new file mode 100644 index 0000000..31c12cc --- /dev/null +++ b/docs/examples/Tailscale-Management.ps1 @@ -0,0 +1,45 @@ +# Import the module +Import-Module PSOPNSenseAPI + +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get Tailscale status +$status = Get-OPNSenseTailscaleStatus +Write-Output "Tailscale Status:" +$status + +# Get detailed Tailscale status with interfaces and settings +$detailedStatus = Get-OPNSenseTailscaleStatus -IncludeInterfaces -IncludeSettings +Write-Output "Tailscale Interfaces:" +$detailedStatus.Interfaces + +# Enable Tailscale with default settings (will install plugin if not present) +Enable-OPNSenseTailscale -Force + +# Enable Tailscale with custom settings +Enable-OPNSenseTailscale -AcceptDns -AcceptRoutes -Hostname "opnsense-firewall" -Force + +# Enable Tailscale with subnet route advertising +Enable-OPNSenseTailscale -AdvertiseRoutes -SubnetRoutes "192.168.1.0/24","10.0.0.0/8" -Force + +# Connect to Tailscale network with an auth key +# You can generate an auth key at https://login.tailscale.com/admin/settings/keys +Connect-OPNSenseTailscale -AuthKey "tskey-auth-abcdef123456" -InstallIfMissing -EnableIfDisabled -StartIfStopped -Force + +# Connect to Tailscale and advertise subnet routes +Connect-OPNSenseTailscale -AuthKey "tskey-auth-abcdef123456" -SubnetRoutes "192.168.1.0/24","10.0.0.0/8" -AdvertiseRoutes -Force + +# Get Tailscale status after connecting +$connectedStatus = Get-OPNSenseTailscaleStatus -IncludeInterfaces +Write-Output "Tailscale Connected Status:" +$connectedStatus + +# Disconnect from Tailscale network +Disconnect-OPNSenseTailscale -Force + +# Disable Tailscale and stop the service +Disable-OPNSenseTailscale -Stop -Force + +# Disconnect from the firewall +Disconnect-OPNSense diff --git a/docs/examples/User-Management.ps1 b/docs/examples/User-Management.ps1 new file mode 100644 index 0000000..13107d2 --- /dev/null +++ b/docs/examples/User-Management.ps1 @@ -0,0 +1,28 @@ +# Import the module +Import-Module PSOPNSenseAPI + +# Connect to the OPNSense firewall +Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + +# Get all users +$users = Get-OPNSenseUser +$users | Format-Table -Property Uuid, Username, FullName, Email, Disabled + +# Create a new user +$userId = New-OPNSenseUser -Username "john" -Password "P@ssw0rd" -FullName "John Doe" -Email "john@example.com" -Groups "admins" +Write-Output "Created user with ID: $userId" + +# Update a user +Set-OPNSenseUser -Uuid $userId -Email "john.doe@example.com" -FullName "John A. Doe" + +# Disable a user +Set-OPNSenseUser -Uuid $userId -Disabled + +# Enable a user +Set-OPNSenseUser -Uuid $userId -Enabled + +# Remove a user +Remove-OPNSenseUser -Uuid $userId -Confirm:$false + +# Disconnect from the firewall +Disconnect-OPNSense diff --git a/output/PSOPNSenseAPI/Newtonsoft.Json.dll b/output/PSOPNSenseAPI/Newtonsoft.Json.dll new file mode 100644 index 0000000000000000000000000000000000000000..341d08fc8b7ea5cda1f12b3d2756b539e1f94ed0 GIT binary patch literal 711952 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
0O;K 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

D|0>6~5=A-sN(m`K7i$%JNQC>RPJqcXmv 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%0DXabbnX(CCbe`!iZ2g0u|Gex- z82K;Aj3a*j3o?r^@)ydDNB#VTau;FbFOs`{_VX9XgM^X4SZ@8*&tEKy2_yeS+2nUW z|3%rFF!GnktUvtxC2|2_?m3I=Z1>ai^ za9)x_2{#8E6fen9gr9-)IfT=j6XCzcQHDGt1=> zgL(e2T>fV81o5*4>)|QOWn6h3e}cFrQR5X7ei6*zg;g|uO;#g(7Zm%RiLcArgx7-n z3;BieOu|l8_?{&H4Vh{%&!<+(Ar<`oUMa^B#`1Vmwn_B!-;~!9M*b=}+x7ET$wh>b zzgiY5KYz8{NErEVNs;8|za?G5$X_E5M)23jk_i4**=>ttKP z$X_puYxw!=C$vuP6Kgyy@Lpy-3~>A-`ANOZ*DPUwh@W2>ur`i}h8Q%c?)g}n5%=~X;*kG1FC~FwZ@&{#olCKEu`?)#? zWn&+IfBv`fDj$aX4rOPOZ&+UU2Zv;T!sVdmYdq9oPVbN$8x{W{k`IFaFY0kf&W+$7 zmJ5hq0RHV)|FB#c!9OD3CjL$+&pGvv$SSAl{9=A&hodqTacK1&dc6HvwkG@uU|0Ms zuOK{hpyvM~I}v^euq%F%HxO=P;vbWJ2zN8_kICB%=KTFCha#W$uYQ#e8azQXhm);G z@_&_c3BPRcZ}P_o`4ag%@wbCNf6XtEZbL6WmiS#(HF$!k4fW~Q{NH60gSr3xL-smd z%kzH1A94_393TFa{hIpuf65_*kuOxov;BObdK%32!%{aJJV97s-<D8D)N zZ8bJR-cb(`eXDK7d41eizDO{Q~~j^J|(C%gt0y)s86EgKO;W2uLSi& z1i!rcHG*GWRcz+>XGK-n;0dA^__O9|6;-1Mepoe+;D^b#{K?Fde&3j4d(s+`l_G7?9cjYkT1R^>Z@S}!+t8v zPmj#3uVxW$1@p}#GaIOE!r9P&o?md9+C=zP-F{~_R142V`_6>%*eW<(6%xK1`p>on zXQ;gfvws??uZjN#_;a(&VBzkW7WA%kJQ4Dyp7YpfoQ;5SuS#J?5FqyOxt zYI6ktY_*g40_yi>=WMkAoS$>l>BOH1 z@jHui)P)iJbJb%Yj=JYR9;yizOUZz3@PY?&7ex(&$ zrfNm-FIQ&}|4=#IpIoj&=lkQgRj$Dk#BOLGJ)dif*F*bwsGy@-MfeP8-e9Wj{G zyH@=f5&v2he*xCtbSUqc1=p&I2D5w@Rn1^7&n~Ji$^Q!NeQrS))hI%~t7;J;-&I{i z@{OVYeXUhj)geOuI(0*Y{B^1i$uETd__bEosXHU&yQ%vly&K$q3zg{hkkiS8#jF7)Utta`@AiuA*xiw%bFjVdEwTxj?SF08+v40l%{#8wHMFSbMv_y837+s*o{he{#-nmzD) z7d?BbzJxQ2p#Ky%soM<3@(GGwDueiFPj6MVmEWG;>S$|?*`D6Yy$EsL6m4H`)s*n8 zT8P^lTtxQvR$UO2y?s=7;-kHN)Ij2+y?xcIgwfu9Y87FePxV&~+Ms>-ey6`mC5-%= zRh!HF{F~Lagpof$t!?Y)4^UePBR@?Yy3)^2Q-2uD{<=j~y$bp0uUpkH!a9CWajV+X zPV?Dcx2h7t=&xHJJg}he*PWm4};f>d2hq-I~3fh+-o&|z4!p|D+Pm9Yr@wY z6k>J3J!+uA+`jKsLk#Bm^1W(MSHJ#y)dL1||1eZdGMM8JRnrZg0N?t;_qzo{)hr)B zD2Ay;26KBEuJ#Z{e-Br4uhZ$VzlW>k1{WpltYnE#3Wlpyh{?bAsSU(O|K6vz5+D6@ zziM}b-~Rj64F=Tlzlj z2h>x9_5ET~=8sZK5z~Fsqt%;)@&3or>MfGT-*XtPHW9|(a~Pv`5yszh7^n7AeEs_p z2j-`%V}$knX@};ISLJWi`Q`G+P)P=Jd1k1|J^cR2P_qf6zb2}!J^lQNs+chHC#lq4 ze*PqNnZcOeCoevvy8CcYOi}leJhrc?Y8YWG@2To2VroBARSEI2Kb)$Z-X^^SEZ?cB z0%8l>&%>%SVJyFC>PCano}l=bx`+6f{&Y2rFs46U-O~s4?@Y2_Jw9EHL`?N-x*A7( zOnPO;Z`j4t#2xIz>Dytu+Kg;y@kE#U3l>Vd2 zB|fJAsH#f*PSF4FEqF}bMi|r2Qg<87>CaSCh>z*dQqu@y`mVCqQ{%keYU{3!D^#t)T{W)qbVN8FHs(G_N{Wr`FkNI7wh7cd~yHLGH81uVOZ84bry-*z@KKgr+YBxyR!}F0vD(x=6eT&pM z!f4+j^`yaU-y$_1G1<3By-0ktZ;@I?e6(+|3J=Eg(7qQ{6@%HnB`THpXkWG(L>T>- zt&;Bb+n24H8O-)%s|yg5Jvr(U;-ft|svYsso*XrwFxr!&mKe?~F9`}pwwOYJw9>*q4nMhljf|1#BJgg-yPCye=7rfxEr^RrA1 zKuq~rrtTm<=4Y82MtscA%j!eIn4eeFHiOyUud1WOM}IF@9Y^}@Tdul2;J0tN8bTQD zTdw|PFx$6W%|uN0Emw1ikM=ExAsO|feJfP?2N9!vuc=A~vwg3tX2eJP3e_~iSU(HZ z(NTW;3Y9w=G1^zCni|aZ6{;49$-Y9>n)qm6p=wKfwC@eIgfQB-QoUp_+xMp0M0~Vw zm8viX^Mm%SQVYiV?OUZ*8O-*qQg0(BdseFtiI4WIR@;e>_PnJ|ABXzUo;B)hgV~<7 zsw45yo^@&gVYFwRx?sHDo^`4lVf5EJb+^H6-#T?aVzO_Y8bf@vZ=ISvA= z{NM6EQfUTrd)=&t`1tU~LXDvK3f9+~oz3b&!k5DOd|mY|Y7F6Ps_OT*eiV36tx2r`Ne*bS*YYi?+=nwOqwoA9GjflzL zJJc59qrZ2kJqC0BcBq2}v;TIeqXth9(;%5^mhMm`K7LT_ROKh?^x3{ol}i}y`&9Kq zOyl9FY9R4({`aZ6oA_wYr%JC%bEbhxplAI{Q~t9=>S?e^DQqzn?)uShAsf7)c^qzu zz?T}^VlM2J>G1nuO|EfVil&ZmOuT^Dvp{1@WIm7d)&^Q1d{6WU;!eLHZP5lPhaaD6 z8U5$QM>z~F8y+oJ7Gt_6i*JiuxbsWr>%PaeY_uNcM~DAcV_VF6N~iCLZR<4+h?xU4 z<#cRuDg0KB=A&GI;aXPzdGVk50YmWrUidgT>#qG9i_dyv@i{)q1xQa>{SWUxUiuv7 z$)7kKmMa_YM!2KL`(rsr@>9c=5{hWV{=Y?;9Jv+bu5wjhZNbaYtW$KkL zhBJ1=hwyEl_s9B~+9EU<^IF8ybT6YEE*6UoIHdxBB+c=2){#B`8CzxF3$9w)Z< zLyXiGg>NG6yjD~23-woH*C77Xj6;s#c#-3ameGGazLg#KBIUV0dGxjA7@zIKdJgx7 zHPzuZ`PmVdZ9+Nb$CV)ejFlLEVtciW{`1oPr{mwr@)e7}UY{X^@94TN^{BQh5~H;& zhY0QW%P~I|<#^2JbdE=3+51<+%P&MbIbNFp_D@`Xj88_Z!QCs`PNdvkqp>62ggd^p z9Mf+LFkXF%KjJyW<=|UXU7zOdI6mh3eX@4Jd~O$`-_iO4qTmfpgQe|>+d1ov#E!TY zzD3b?AHUuPO8Fe0(~Z?$ng8#yi_7=_8s+hqDUKg@>3oFCnaAZ5V_j$TACC)&JZzVa zH>ii@qQfJx4fT9G%(rY9zfOnwo(=W0%4|)8;$cJM#2#3e*dkt>JI|j!#?#}Tj;HIj zj;HIjjwb}hgSSSd@rFl?rOSxMln54|6j!? z(|>%v&ZGW=`z}p>IeiR`4lg?&Kshhmi|0?z<44a|{vPI3JiQ#^)eG+TqT9)=4O-R` z8D<`6iOBLUi_bY<%iE#}oGI7!^=EkZqWiZ@gM(rke7m9bVf|xj3#{K|ab&%;L^Q^7 z;PS9UUz5IvnU8i`Vld5TzccN?5i1Ra`|-?vg-zHIgGZx$ge!_Og)TVfjxSH(d@Ge~rAM@vhbN?D0 z&ia_nT8;kpWA2xaXU|1uAJ7)J80w`LE1c~>zC-Z>rTIepJpZsBwukvxU%m9U-WzGR z#%#Ct8>frmk@2)##o;>s@xyssIbQx*ueR@Gd~L^x>^znDMKCVv_Mp?#`ZV_EQ~Sry zXMeH$-{JpP>-+Dv_kV+a-1qhB+wscfcz*x8@&29Ve>}VW>7T58qT@&7zZ(B!@_s(I zXKXhoE+2oq-v{CN5p6$@ z=Nul5V}-|(V|h#TgK<{-oA-$l&Hkz-(pF)3^gNsSPn!3YmSFr>X+2sucK;01;qr~t zV+*}6s_lymhx_rG=x|$f8;$&{%)XvC4(tByc=ev=W8DA6qW*ao>(O?wJnl1`xSjdq zZH0XJ+o{Id{#f%IosK^~+x^8yI-b`LMz?>q&u_oB+n>H)uP$G|U0B|>I2-)wpMUA} z{c>!NE;qlNKfbne|2S=LK)7aJ6fE^0%dx%D81;JP`5zqV|Nr>o@B71F|D)&cT(7xa zGS>d~w>y5%SFI0*BR_iG0Y|62U$`N9Z#pLP6tX^C&s^k`!XU0?P7Kl_X2 z8AsO_9B+7j#`VV*9M0v+^A?`3PdEF#4%NSaXanP!u8)6}&+$;7U8)D?E8e{2RNsHG z9OoPLc<e*6CyyV` z_0!=2G0{*)9Ju{kMw?oarAqq>);L^ox+xp zn)06|BEzBlX#To{?B#kEiJ=~v{KNUe-dY#$n|k3azxa^md*NswwEJ`&ZV8s-ddYe@ zJU7oT2m822pDhN`J~-o!aOZ_i=YxGH&*9M+{o;sW#$TMiM>##Fc%IP~wdp-LhqGUz zDb|0B>NWetE6-T`=yam>^86mhcQ}tq=U+6{^=3;x=DRoatJ<&3zXQJQ@MG57$((<% z#l%r4-^{!};(c@HgyH%px2zPY!v=M}4*!XX>YyzP4ZIFYIDII9+b1 z{`0F|y72pp)c*f2U!VKX>CUEehP;o#`dXOw>iL2B38tQS=V?*iqI0&#r#hXzRDN1M z8te9Ti8fDd)34ym!~*c2_#rr^B_~j0aNxfahmyq2+b^yk(xI0rP%FzvpN9zbc1%9MS{* zGugp&#QC2M$G5=kZQfh=X>GYe>08lcQIVoQT@mIIX;GZ z;n94?9%cR9?}vM#UZl1t`aoki&kg;I_5=5Ge!lk8@xpbyo=0NG)BLy1cnR-OO}d<3 zG)B4qj`UB>KCd5*w9{J`_{07F=<~AnAD53K&O00H&p_%Y(ZBE>%=nq*7)Mjv_*0iJ z%WL~jgmt-jd@mmRo%JC0@)H>j-_KJzk+iJ6b8gk?aXDi7>;CjGjOVT!6fgEidt%8w z1;2Hx&q0GtHT{} ze;q70oagYm-Q%Bizn0@aSU;@Q|7OZ^9={U34?z0Y`N!7}>rHd+8uDT4lO^!{oGq5E zLVKdau|IN3`N{oIZq;E;ZJN(I;-P~Y2gDEGXv+PE{DTplD;h z2Z|F9uhwb8eALKseRfC=*5O7D&h3%h$?6Nw-?Nu#y#bMBDAyMqrvJEJAkrA?L2S(B z&Hag<7wdiG)1E*tnE#JpSxw>nnrWXfFCP<$vAsH_>!Mh4(eav5yF__Q++*hZxGvTGdQUhP zrt9lAdJn_=eMZg^vw>>*M!E-uFv9 zkL+sv=gr439KN^sSnCVGRcpHZVIFJb;^@1jcuFTE+J30zxcqF2=TN*r>32?%>3QEj z@x1C()9qr`NtmwdCX~M4h@}VZ4w4;l0^`S%-J#O>yibOD9D(-0xm)9hvibMh0lz*I zQxBf6^7dDv?L_<7PeHOTj_ivs)f*iT&!vH0Q!XdZpWa8-^GCfd()I9-epqfCo&)XP zAI|%X(KJ^7h3!D=)PH}!f5LS;e2q)}sr7{1pd^CprHzUXV*c8jd7R#YtY5Puub$d=Q3i3m+m4nuZ_mG_#EyQ)_Ra%7WX#ua);!+_*~9Ny>Q%bw5gq0 zw9m-)SYnT{i{~5gdwgU!$777t7VWdNZuma^fTm^R;du;OOfvHthx$1!tN*y%d3^Tf z2ft(@pT7s=c>}gnPmbSv;JzQf_h3F#=$~o-4)+6LJp%h4I(_cXd4DB(|B&YeI8H)+ zHT#Q!()i5BeI|>>W&Tdw%NOn++2XB@I$cMcQ4!NWet1CiHT|kZ`#aI`m>-1q6ts`HUj(RyhpWkL2No~<%56ZI~ zQtcFVp%0`(a)Odi%cE ze!Y6j{YWI$I`!Yl}SR+%zgu`U(I+|)^7f86ZLp} zj315R`{S=UOjGZB?Hh_UF3aC<VHc<}GZuUMFe&XdhEpx6`<- z^RLILE9PtJi1wFg8W3X*4vISsjuU5ohI%+&G-W?>x__6((u?yg*sp-R==#^&j61Mz zZ`w^zTy3ydkI)`l@ORr>zC2EHIF7$Ijc*S5Hz0m9<3-TKhg(m?4Q3x)`#}GB<2c7h zy|AB6`{nEJMY;269$MGzqgbWq2f6<8ckD=QaeHr!$GEgUoAP45HxG!GTi8JRhyTFN zJN3V9(TVmckZ*}&gE1WWmblfdyKNJ$>j8&z`Uy+4yd{v9J&(Zch1UxhPsh=JJU_L> zHghk9P4iGk;Q0nvr&0al^q4+r?t8Gshi0D6^K;h6`$?FJKOLU8U_J5XEnH98ZzuCN z+Yu@6`Ttb?7|U-b^H-!Fy?mD1WzK=|K8p7q8~07Pzdbo+`RMgqWWQ&LNF2Q$jJ*zw zj>qMB{O|y+GlR5lJh@(cALNw_>x~RQRlS~{QID5zT+hOH$7X)+$?^I(x?Cc$Cm$K^ zl^@%U_03E7?_szvh1yL`Gd@@XX)HT9oN3v5!}SE;&y|(Ke$5N#evaee{yC0gx}RY` z_``Mo&f$2z&!+JU`XyT5G9M|=``KPRSEl=eNV$I(_VUl^bN-of`0;6U{Ae62A7$mD z(~HK(Pd}Fa|5xGB`TDE)Zfc^c}2a2nTm-Vi+wG9Tw{j`+r`H(*|7+HsJ^&A8Iu<2s|9p-S z?TIbtw^yf+?LSa@pVi;hgZ)c@?9o2Zf8M!i>=!({czlhvCsL1B583b7ZUX}MEja&} znojR7Q=dG4Vf%#lM`mB&TbH69hx{5Sy_bdU*<<=qhx(gHf9rJ`mvcpWuMzEc%m>@& zah-DXk&k&@2^*Hg6SqH|8CZ^vn zYv;%Nu^idXwr2eV`@XcV$m2oey*1u_He8RH^7}Ky(e=UW=ed85mhT;>$C0w*0?V^M z*v}W4a^&*k@!6*Q!}^}|p@17n{Zlr}RBUv^|)P*T3QT8&|3)Qr>&N%l<%$ z^Ge-+@c0!OkNxDu=k~yQSUy&GEPrFZW7*5;dj8h=)%8Ce{Hps|^pj`TsiqTIPF{TO zC!+Jk^9l_2`sI^h_6zn4+;4m36)XRIo*eD;^8ep0kL@b1^n5wnAIqK-*SE6e7t8)r zEjKRjlE-wv;LW?Z-$r}9c62iPW7&i2IkaEz6YB3pb^p%#xc_0y?VabNvBJ4t{a>Sd z-$nm*`vfWX=Quuk{XY9Ix<4NZ?-#ZIab2hXt^fFa9j<5eI;F#QUop@9X6EGqWaDFKNS6cGC%NljQoC=>s2K6zPpY7j+Ng> z@_hwtR23U8>`H zzcZlwLI1rk+|TNJj{Wz(@HrZ!-o3|NX}_DPu3!584ZbIt`&qwU?T_gGo8_b55904S zc>Q_w?@w_4kn;O${$7}=7tZw;?e+4(;n<$|Jp{*xb5-=+G*kW#E&4m=f9hw}A8jAk z`&i|G^B4GDl*;4y7=I7KUp{Q_srofmd2;z)T@qPuI6S&OaehvfM%O!T-_aEBt>E^- z)Dnee9S`?8kblt6aE^MBmT|;cziH}~b7Vb$^@EWMl*;khb& z+qM|}I{F90c^qKAcg}{(iT$^80LJ6~jN3V{ce$PZ-Tpf|pV64pi*9F;^@4v-=G5EC z@$0cy@3i5L$N2`IAM?sLR{!?T#w$*T zDcc>1{pUA$KE&k|X)ngp`%uXD^2PO^Dc9G?amtgA)_dD`%+nOgo6Z4ZxFznU?^?Fg zeFXd+6P~+v#8k5{!S?|L#dK=-vFb0!M?T-96QFzjy>b7a*85oXo7*w>2kbYd%#Xy} zUzP1=Sq|HcSI>Fg&Fvk>XRg<=$8Gi-j?cV*$<))&`tZ9-ON^y{_xP0SjVH(LgzZOa zi*L>ON3T6a^1c3%!@2)rzeQs9o2O6ny?X3@hr;Eh+oOMe%Hh%dRD;!6U%CCVpSk_~ zQ_AHKT@N|jUp{_&dEC_Y_~+Bn`HCgSc5{BX9!;yP^J|M5bl#oim`3;8IRDXc^k1a^ zwH}U#<%LpZ|Ih0euU&G!@%P!_-h9*EIi@Gi{XCBc|2n8;oEX2e=fz{Y{zJ9D{^OTZ ztq0uiaDC%-Bx3IzX!P#}@_Qnd=l3^^xqtDuS6wc#<_FPo9G`#xiQ6^q@7c7!r^{OZ zS#&N9k%wV_n!Xd^`|MZ`VvZMEj>m7N9A7TywEUF!>&jBzpW=OokInZ~@Vyk>8^ZHf zZvRN(z8bQhzfa)rf}$zM<8o(z>icVT|FM6Zre*J+<@*bn^7zR57^7Z3@5=WXayyHC zKQ+HUV7vaO(i8j3>&K(VwMf5MB6_^#arobzZyx{BX#6`;Lzllr--E*babu00((j$4 zzkgows^&+^>G$q@-kjS(biO(y>-DQQP9o-d;`x!s!|3nP*k9Zp@H;_E%-*Q&;Xm*D zJq(BYFKM35@tE@a*?$*pLHDF^dhDM+=yy@jAKvee@_jwA_#8i)#**jx`IU8aIXL3a zwc2Mqf3`%dcz6#?Y(CykVu|xXmoDEQNdLohZx){Su*JcAOz*q(ntFJq!InUZ;Wp@+ z3;mm>oIgJA zx0dG{Jg3$MkPqF^`gS8yry94(%(7gsY&Uo`tyq|~p-nd>w z@11U*s`c<6+&e>hj=ZA7yzzzWM@^~+Jl|%@{1FqQ(Leev@byU;Pl}>S<@e~9LGa# zo<6TW#L5@y=lHD8DfJuX+Y85h2V(k*>ks=~=UcxQ(f6xqyLexW=b_P*>p#{z?>!37 zclh3%II~`N#7NQ`8E%OKbUp&9E!NV#O{|C0iABH3jw~0%wrFAABf&jpGyVLs;VR-D z6-(5C`&4xP`JNxX|NRphkNNj|nMT{i<0S9@Mh->=}k=ia#@j?ed~b9(>1RNJio_`Mqct_=56uudWWa(~QtJ@w;U z4&2Vim~-lIueJHz97}My@cJn7J0DAYbB9hJet%=2rf@Hz!O{J=hl}&I9DJumlz#_= zX{_>Rdy#4#`j7Jszh`OkgP|u6kCyZFus`_yC1cFD7Z3g6l|Pp|&lfmUzM_3z`fM-fgDHpqK=T~DKg|++&YbIIH0Jut-$C%X_&(?BH2I!M_|C6Q zB<6f``E&j3MeBE_>}SSI2aMPHPWESsQ2Y}+KV2_9KA*(&_~+tcoonQCPSMoce_?x~Y2&7L^K&wJuE*Ye-I3`rpXtf;{Zsy_r&IG?Eo+PCztz+d*O>i5@BYcN_UZ6|c-Qq?O* zt`rI${qH1r?ty2db&Yre9!*DEokg>6_DNv(DRzOOuPmuza~1sGukQyTnM-j zaG}A6?LzT8Jof-^Bs>Yu8=|Xt1LD5{{5Ql6Vx{N_Pak-0hG(?3QnYYZiqqg}>1+`9 ziVdJ^1B7pYbT^1AAnY{zZ82WF19+pD1kaD~w0Aa&sX(u{4vDSKAyMoc7JJ|+c771s z;VE{Gh+Xg$J4fO95uTsm`5B&H;Q19~eg*m)$ovMh1ay=DwX9;tvG%}I?3A+}73Hka zR(WfdC=ax%bxc%)r#d_};7Ng}7Cg1#sRK`4cX{Rz;hlv=fiUWJT2k55S~`>w1(#*crJ$L5_sCc zb16KR!E-r0ZQ;2Bo-5(G3Z8cGw1=kyJXgch5uQ%)Tmw&Mc&>$~3p`!nxelIg@LUhi z4e;Cu&rR_3f~Pk;ecL|5yIYw=L2{)LD(h;+XT8cL7Ytx z=Oasrk09(L>pq~n;YqhPTVup#2-|Eu0CYDz>DCsAy9L6wK-?`5wgtkrLfBRa+X`V@ zA#5vzeGKVrgYaz-z74{+LHITZ-wt8hA#6K@ZHKV!5Vixtc0kw;2-^W+J0NT)gzbc| zoe;JY!gfN~E(qHNVY?t~7liGCu-y>08^U%&*lq~h4Pl=_*k=&-8H9ZXVV^Xg({)92N2ikkRL7eNRL9d;+wllcOuxB< z=`?q~ci_YegntRUxHHAP0P1NJ)buQ2IXZtmA?2Sd!xvD1Yrv@w_|MUJh{!Z@mPiw4 z`~<^-l_qAuu1+~?7|D+kI^D)rQSb?P`_&m}$FBwS3>iY_zZ_7V@BTp14;ez+JrJkM`{4ng_rurBJt18CYnWAJqg;`V;m3lnz^^Wiw~htJ z!|Gv(I2ODIZtd?0G!eoz-30M8U0vPMJx_2{cOf1Fdeg-i-lPu3e+>9=H*l~Ad=>O0 z@b|#2Li2#e!&^5UzZdL=YP$V7P3Kbnvnc<|g!c1?)-s{}yaUEJ%vT}htB~?lNck$H zd=*l@+Q;nxzx`k#{RL?E@4z2+`?#0y5+WYxZQx%`%k31RJka^D`zwK_89Up@O)nOr z3gA9A+;mE`A<$}@g=k`LC3}mF{|DLa18d-``4RS%xMfhUhd{|=`8S~a93@;rI5Vzz zwXV;L?TS$T&)SRax&=bK090)dA_wTRQ-xRtbRE#wfm#iOcnfH8eIecf`hKzyn}EIm zbQ{p-b%gi~XgQ!?0v&ma5C?%)1$qSN2k;f&uRwFZ)`Z&eA!thRn0Bg87FNPPaRE|10Wn!fMsFnF_5VDL6)1<}<+ z-y!-D(Ylt7x6Ro>xQOV1_<@k0&z%GDE6x-^QGdLh8NVF*kzxqPc!Y_-D9wkJ@0Ad$vy!J;>NJCE#92VC>d9)135c;)< z{NKweStUeTpqF(7=zSo!725M8pgNstfeIv-ZuD3JsY^5~fOu{ojr^QOejY@45aD5j zhY?;({a0rE2Iv>w3(T|5g!cSV0PWol7|R{QKPUdaz$gp*ol(|H&~64e8P@Ip((O2d z^pCQ1KUOU`&BAst&Gh>{#gzCDq5d@qX2zfSg04TA@hc&p@piRPK|T0o-(VKS&m#Ng z5}r$V0pSINtJy8mh3FBiW@kXZ*ca$XsONVDi>z#@j}Ha2EzMsD6#3bf=D!+TM&ZjS zd>Mr=D9s1f<54CZON_Fx{~2XrzgyR;2>oJ@xJDt}4*SIw#LpQg#658bO6$wrke>y> zANq_Ci{sXkp0(DX`j%J=;ksSF3*o4DBjpG6?gzeZx8KJdFnUFNU28DZgPQS;Lb{(g zE#3<0^v;Xl8WFD-@O3-CCBBiR`J>`9<9~wm9*(bO9l00mh~Gu>35H(?;X0pR#urm~ zH4}ae;^}%93T4KpnsRCxDtuIk#1PgO=-GpZpd4FD3m0$-A^WTqLOWp^FlFQFt#3A7m)> z_@Rejw|RZSFuSkm$48lVwJKXeZ4p z^a$;Se(=Hy{X>IczSE_`rM4aiZ>+F5{tx*2b9jYDAw51Xt&nN@`&S|Sap(tz1I>cn z3r!aq+$f~S-O3NsBZ7)R;+711?~2NikI5w z0fpZ%v%hSv`?HNyKQ>bNZ6&;w@Gh!XyQm!%6D}samGD-=8wqbDyo>bhB7Mb#iwPej ze2_4Xw?8>YDf}pfmk=%?9B`-|JJ^mZ5UxNtiEt9(6v8Ql57>L5JRVPMZ}*4!hxP~i zMb95x(LdDhB?0Ax^NCl1>hVR7@7m9c6BDREPO#EpTzf6C8eteLKyESge|2GgzA7vX@-z`=f^fzt!Z3;Gcwe_=S4!V$x7b(wWTW2>#1?;jK$ zo#$a(yaVL0pYKNeUc^sx(i`Y`(~h`WkX|U1=6nm|!joYb8O^*N``hY}zRr&&hLL}U zk-fvnPooT8l+YzK3+7W*+y*9{8{G^_r%UJq@Z%kBmr(g%g%|^LKeYEJTpZ_fK@Qg~ z)6BZ&eK(8r%_X{^RR39US}$H^#!m&mH<#v zGE5YP;BUfmD<}+HWOkB(HTS^uXL%s z<}4xRRVuR9fq#|)tpf9i<&_$Rc0>R4W~GD9573W%R_Q3w5~J_eN=Pdt1ZUhBgscKT`roo0xUh(=sKX>+91{Zef}~UvFrG0JeiB z0bHl1256mQKMR$bqnCSxK}`rdkrh6k$fXthqnNX>8Ay7y*tR{5B3vi93Mt@3?sP%b{}Z31DpeP z0kp$`l@HjzKQF}HmAiy=IZg(gb`#iJc~n5JdlpsB2wbs3h&O@02>yDzatf_GQ)u0p zLhH^HT6d-hv?qnupDDEd>=N1v>)&ml=V#ddO$=p_T^VFoMj#1J2Oa{xZjZyPi~z33 zGXl6y&j@@E>#<{%aeh@&Ig83?hv+*J+IMn#2x&_*UerpS8|YqHh)aNeRYQo&lMjn7 zFkW4ioZx8xc1d1f{Im#u@=e#nk;zEM5Df~%6A4cx`UugidR+PL$+*=+_RwXw#>1GjyeJg_xft`B2^h@#+p>jKg_%m5L z_FH=0Bb^}7_$tzI@)5@YO{%gu)a(%Y|6O27Upii0c zC&5}wbnkR1R|wbkqyU{dP2-jnuce8131FR{iOD!`zk&4K26P_izXxbB^cy39UI6xv zty;~{M40MIB z%xyPTA8PL4zF!k+ zcWVQBzi0;Vao=dI=|37)ZesRnG}Zma(=wIXb#n*zzf!4v8w(Vya)QY2T>O^t)}UA2i{8s~1u~QRL|UYgF|WW;{r*UJ__= zwk5KvSD^jI3PJsr`BlJuj|fo+{KugFuLJq?w_zO$c+n3+YytkLazgB^o@C-JN+=0T zhV{TV)l*1MiqTWEMhfXk3F^1g^=hneazW4OHA(_6fu3`LKf~xL3G9V_@Uj{WD1HMI z|1Q92#|oze=F|5AzJ=sph4db*(S+oi82RS_vwSVkw-E543fj(8l1nvm@6<>oxzwOO zjqm~3wI1|uh45=g|7sXdi)yqs@s8F=3cd~FjMnpkt7%DK0;K<2jkct>tkr7h`g zTdMbb7&p#>@Cpe!UPqGeXyiMkbR_wXrSdZ%ovsjmnvw5D^4*O5Jt^HtzFSbgRo4D| z;8oZMg7C`5?p`F{%gAS?^dk8c&cz`21mF^*H;v@djNG!6G?GgT>aS9@Kho>KNgD`X zX8bpZgt)ra+#t?JR-1NsORW{uPpqK%`U+YZk)_LLe0pGY#D)|2zm!gYwexr-S}BwG#p_ z0Kcb52#h^TQyF+3{Bjk@UC;#ez61NJT7Q%Ck#08luo(21mOm^COul#6M`1n%`~9JV zhL84khIpNg9f*4X{*m_Fde{b#PfoovClm+x^SU*(^vEzNa_~$7(yKnEJe~_D4=j7*{{4-6QZ7l=o+~7n9s# zl3Pr2{R2f1Pq&{;NcXGS{R6*&f5z18AGikkYaOoZ`M02_?qVTIfa>-FXBLAvkK7@K zfF1Sf92O6PeU0j@2*km-eQupyl;19)s?bk%t#iP`$Oxe^6(hnV0A{NBX_j zS9NgT^rt!tXq}1k%cQ!uoAg}RH`DLQYShh)pAO|%w{8jPKWgZgRaQHfG=cV7S0+3H z^{QvxKpf_KwUY$(;c962!*3R1ZQTlS*q^L+sz7`AxGwe&dx?GzRQC^mf*ke-Nu(zw zZg-J>f7c)m*F{ZA^=SFC>!p%hYMegJG5~PH@_K!U>D&gmoJ4#l%$x46x7ry4{?7oa z?VeGub)2^QAiP0%4(6*pA)fBHih%zS*suG?hF`*b8t`DKzdApip`4uhZR7MfSE>G# zfSzxkS^u#3|Cl=a__oV>{eQEhNz-+2dBfy*%Z*Bzw7$k z7ryxCe)qaQ_w)DuK7ZRDdpw?ZbuSEm;a8q_2KYMGQ)jC?9;T3=^Kxf6?4O5q`SI?s zU06U_N+U zS!?^*+pQc2<7;s}&;9ZM)@Mf`t9~qRJrCo!eaai&|NCEDw{)+TzgGQOt9f1*OyKzM z#n%O&s$##77ZfijzEND>`aF)4C&V{tdu`VCDl7l~N$xpDS^3M_&gGyx?z`*w?eT5O zzb(lBD*G#FeyubP?!x)F5#O$P*%o{d*B8*rzg^pZySD%K;8D0fA4lA6{}JUI37!q# z-!l?)$D5k`T5#GZt`B=^@^{JKC4X12FXDM0=-H+A_b9$c_3csIJ?A{OXReR;*Jt$H z7iwbOt~w%bO!eS7^!Gh)sd!P(xaw)zJRE(*ep^>}bcF*vKL4oa4X^wp-dpx8Z~Y3^ z=|`jCz+5|T?6-1w{tzL=4yRaVqj{N82`kMNKxc~WkPcl6FD_lSG0(aW;2N1{abhmpiw+Y90 zJ`ccqeq;C13)Rngfq$IFdgldpeT_V<@g5G=asFR4`LNc3^IN=tJ!j-|w>Pog|KsRL z$nF2rLvH_H-nt(5@lPD>p0|B8dRXgemh#1w?^xCIhW900*Ia?;@m%j8)p57`LsgH* z-{Zwo#k0j_;$`Be#Wmt}9~!gccegk{nzHurTc`IMGreeXU&zf%U&!qjeIa)p)EClo8kJiea>vuP%C}be-2VKPIZMNQ4)g6f z?s@MbJYWA7?q{~nxjFR17JQjHXPwo9JDd>zJNxs{{O|0`{p&)1Xyx;hK0E&38D1CK zfc5$G$HEac z=dW0j$T> zbKmf`{oSqG3Vg2D{kXm*cjI_RhQGvh`-b-q{C4^mk^jjbJ3rxb>-(O?PEW)AJ@Y@F zb@~;Qqt4^}j_)b7zRFr(+d^#_9M9)&54rQ}h_=giL6gYAFie2**M6d$(ZLGc)Oeei5_zqbD&#h16ffb+oUG0R&Q>A244h41_D zVf8!E>h_?r$?&rYnCq;O2Lho+Hd^b?YuJ z9haWo>YjH;pG^KUAA#l%X+?O7=!Rl=sS8cft z=es|ENBs`>Iq=AT0^b2FK0euUc^T)6SEYF@LN*SP%E@>k1W*UI<#Paj_pH)X&yF9m!&tU|LanE@ zzaW3R`5R7f&oPT9xbGwmpD<$mIL22~KemR)vEO}r&aPIszwEMheBQ2BcV4?M{0_!> z`iXt^y~Z)qM_T*e^t>dr+fVxR9BQQ1J@*;Wy6f|%*>kRO)w`$F9Z&bPy5nfG)jc0i zS$mP`d0z_i9-6dY$JzbL=e}c^I?p}VF3j6(@AnR$;J&Lke8M61_fV^Qzja9E4z;@X zT!&iS`>j6Z+hEtZL`8|f1Gah;CEhC|FQpU$B*ZOkI!FV^?3^x zTYWPYq{8mGS}N?Gt1VVNi*0+Z^(_dy?b4_I^x6Bbwfa8k@CjZ1O3CwXThOQNFrs;1 zrsr+TRR1#7KW^*x=p*`C-FG1SCIxUDzY6aWcztoy$@?b#1^ew)M?9=?Y;Du8(lv(DIWrx!)F4ezkG$`!`CjlipvEM&$ z(sN(Nedj6jCVdFUokORTRsS~CJJ0vw1TNoCb>CaP;NPzJh*;D5+GV_>tEO^$!ft!G z^T|h}N#CtoJnu`V-W<9F_l@_0_u~HMr>DB_bbfy7p0M^WTOS$UxYlV?{ok+czF*sY zm)22J1}S^&N3fQ{M)$S+|d;` z;=Oa5o9AQOTtAO(bK?uQt*LOoU!eLHt9+`>U3V^PbNkr}`F(BfxU*WvpVd14WYx}U zm0PQF>(t+MZEn9Qs2>IOqo8sHi)VZ*lz)ZtZ?yWn1)HqC84EV6A7zcV+~)SvWqPl> zsm-mAf|Z-uvQ7QiCcn?h*B5P6Jq6L}x8^#%X!3UR-#rf>UbI@@%Z7sx^Us)TyzGpPdatoq?aj0MqVJ!v(D)~_eUsdGgYLcSU(a~M zt^>|I)2I6>U+_RXzb{^9^<97FvUaZjwZ0YYWBr_m743YVaqF2Y)ZfMb)Vxo<=gj4; zj6aMx=W)^Gaa-3{`o`7IK8>$Whs3m{Z7~R-#fat>%Gi8 zi%;>cw|OX>wNN^5dgEEkgY3usXLZ?gH+-3G`k}K{Tm4U*wO0LDtA4CiKla&t_!G%C zciok2n{CI(aa*_bMeEwN|FygK;p1NMEgsL;wY&FK>)PG%WnH^FzO1wMj+s904c*J{ zz{lTxujIMwk8v*pzXj#>zRCNp;Lo8@nozga%+Yp=s|k|W%AjIGI;_WpWC(!Ea}OuFx@ zz9D@JG{1Yk9prb-$=W=ju~lp*D*pFLNZnR?#%@zl+sFrL5mrF^Z7e>#Qtx8P?} zGmUFf^NjzN+SShMzd?}CHNTZwX#RhtV)j1!T6|B#@5xBM$0qr{``WIW`dL#yYwG`f zA$LCBqj8TLu^-rexW;#L=$((_{mQBC`?a%|tk(AnO~s$L?^3_MWNYY!S9l*?)_Hl{ z*2&D4{kBf_d;7IcnySC4_#wp)Dek^cnsv_2q4RF_ym#h|1>5m^6kBGv@1j0(&ekB$ z8|C0uJAQ0>pLFmRsYS z6>-;far5sCA1lr>?tT9P<;!b7T^w=8hne>MK-YOQeW&BTWH$J{oadc*UfA>*AoWMi zOGVsu#4@$BLiI0G{bRu?hw=Ld=Z)ETes*4;`D=WA5x2g>w!YpuZ=vtAcrFxPI@5H= zQnwD`OT)fC`26mG+l^I=pVtnfF(T7{daKyJbyjDe-JlBv=N+z`io01_k9%gz2I*I zzl-DY72dw!1onf_qt2e?6 zqijX}*W0?d-1pFHynepiw-tP3>BG8C%G$i)f4FNMX0_ce_w9Hc&v%#ZRKA^xH-bn0 z(baPs`v1mKU+57V&*k=hgYCYFbw?)Qvh_Pl0_H&c9G=fAM&uJgN$ zGtQrDJklF$<98*|^OL4ObpBWy*Y)w|FExG2`PcYv!Sm2F!AH^V`RC6Jalgj*{~{hr?ngHNv;L*_T=E*df6M%n7mwro{gH(sUPs`(srt67z7f^) zcH6l1Yee-g4}I45%kBTH|4Y!JNy|fL+qm9t`%TgF&Rpi$E1oZ2B(4xI7t_nOX}sGs z-nZMnj{WmXr)EO#`BNrzip^`reov{zt7%@Hj$Pn*^aZ=*yWb!Bi{JU{-{-id%kc=$ zant)Q4>hn37O4E07dU+m_%@z{t-By>=er}kRknTe7wl0#SE)aHw7>0%xcj<2+VA%0 zc(E#UC-#fSE^yaL4b=OQ58-|pd>zN>zh2;eAE)KQRrY&Yvo5^e+QD%ml*0aX)e);g zZ)bU&UKP6ceZ22p6`B^s_L{LO^flBQN4eY4|2Y?qtG#ipyK(h%zx@63_sh>{JZmTY zAHHk*(uId3Z|%W%tQQ744%+k1?Y?pFJJ2uL^PTO!sTJHuUAWy>hhF2`uHXCIrhMBJ z-=_FB#b-`(^~{{)%Fmp{^7w*&lKZ{UO^P>s>_`7anMv%&=Pp_`$@OE^B({(JK;<`C zc|503`ACQB|MU*m|5@t)EcH9C__2yFP<*r6U7&oMRsUwyzghK{m9MOLS@E*s+Z5lX z_%_A2DZX9t?TT+#e7oW!ijOEhqWFm7HN|U+*A%ZQzF6Z+X?)8xzGWKM&7q@l9tus` zsCqW4o{g$!qw0CycO{-9;XR4+6_l@_d*D1bE@fGS%pZb$kf3oV&>JDC4 z@cw=&&I3<;Y_0S1_LmHZEZ+b+At|0=E@H($2F|Now3e5-#1%wKlBzr%FKf6S3S__zCi^$R>V z1iy>x)6JKy@}H0QW1BC#S$bE8`wr!L`<^f~X^+Oa$HsYkbiBiTU(l4_H2?3H?Kgc# zSF_`9lW`olyxGx$_hn~Xe#m@$@7Lkp>pyS5xAdLvK&N}39q4rLw9k zy`P@d>E1`j#bf0!41DQpyuMk`>E7Ed?sU&B7j(M!cd1Tpha<08*2(SL;w|gseb$02 zQk^`HpLNAddv18y6)P-$XLy_LGp6e~QlI%gudmZRugOYhO}}>fYU$OcuUx!VdadcV zPG2Xz&UAQjp_6|#?&aZ)YUs341f6{Eo5lJzs@{#A|NTLyUHuz7`PZS| zK4PQV*{F6lcK+=s{GP}sHmbdiYHy?3+t}%zA8k~-3)&d25w#t@5o^zP0bxtN13xHz~eJ@y(jA z&6=;xwk~qLn>+t|CcfMF>d)GW{?))a^`xxJ5)nD#($N6oY?l`}_ zQ|GBpcYYdCyr%dr#dj&bNAYpRn~FCT->>)~#SbffSn#NoFeecYcz+`v*8kp>^_sV)c9g!cAzfAMAOzUWwt(SlO)O6)rrsrA9Y@P-_ zwM^@#)tlj+mTvWCd1oMv+s||C=M(Mc1@`l4-r0yI%t@JZo;l0BbKz&rzta5G=3j08 zb>^=#KWF}p=4>=)ll^>~{XE^rKFsj353_u1E9zs-=S`x&%+iZ3U18~EmiAeiwlr($ zm6l#@=~_##o5Xt7O=8VCa|)K;Xz50zlW=XYEZyndHt9m-**xjHX}=fs7x8o4KZJCS z|4yWRe)c5oXFXXzdw8XvHLv#Hjr3~&Pmr$lKZx`?{|M4`{wmU({|Tfw`kzMnkkv3^ zHB_vIn$_^6)v(KIc+P6rV>P^NHH=#gb^mjyq3M4S>F=zz{r*?*^Pl{`L3+sFMEaJ0 zAJW7AKc=U8J%Q7Z&b4%&r3)=xV(C&#FR*mErI#Yb813gz+Ry2LjWocKt~TdtbJm)( z&YYY%H`>n|?dMJQ^KJI?X3JSN=T39Bne*cS=j|c$N6fF7|D-v)%z4h7J(hFa{JQx~ z^Y@$cCv)DipAXy5UJH90Xkq=WE!-|sS~#xhEgZ`Xi_WrW)S~egZnZfr+-j$_97MXf zdBJ0lE(@N3^y1(Gq$`38kzN)&6KP+t7il_p9@1>^0;E?4KZw=$0nhRcy^rj$ZY*Uakc3Y71xjD%B{IU7l&40-J5%@uGPUznri$<=su*Ug&shC2&)kdB95M_LaB zKi%myLmxo;yU=w=_lI6Y`lrxI{hi*S&^M9375XjG!=W>>ojCQ~f;7c3^i*#H9KW}*^?z3Ry_;Ga$bYjr zn_8Rr`F1N+vh;3CuMcxpbCwpuoZB12oZF3I&h1TMuGLLpuEyKKT#cK5BGApX>B4YoGkNsov!l?Q8!aoOJscNVDx1!(V0fTyN+up`}YLU5*reh&+rI`y!9C^~kT-dgQlAuZ;LUk3LwQ z)sd;6@AR&YoDAn0bJj-w70z{$tKeL3&br7~;p8IULRyI2gY?G8FA?2f(T$NhoSPzV zz`5C+O%ZQ3&I^%gNH<50Mp}%VuzIRjvS>MyfOBW$BS^PJK8^Ipk*knyk9-yBLy?=2 zjzsQ6{zol;CGt}^wa61lpNzbSbXVjRM4z$fbCEaT?1>zP^J_R$y}g$H+0u6`?dYIC z*V1{GCM~_d(&d(3YUwADVvSk4v4iXFrVg&RO&wfmw{`G%wYh^wp<)M*73Gfa;^#X% zLSMj<*-DLAsfv}_WzKWvylg*@+s}227CYHixs$Ej*~wP6b=nc5lWlG9z(X#vydAI(F!@0^|7Om0lkEy& zvRxreegb~p{K90;#*LGC?$|h)^KdtOtfmJsQp}-eEt`(q9$Jg zX9pb2q502EPQ%|b`P=YcH0R~Xcfc8+{6jduHm5oHLge3TrCvAZkmdig{rr~Ye8+zF zru+nD15>If+cJf%bXaIh4J?e!h6htEg?ol>Kln zHRrM^;WgO1r$pCG^*(7%ddfv`vQsXDvl0$w($arJiZ_>2mcbdYpL0|0T7!G6DR(31 zdUI}^axa{XQ|^QFEpu*~azC6+Q+^8Pzu-*uzGvw$QmoY}55m9OoS$0ysHHnBea6xk zEgiG8fi&PP=(=l7z&ow$ZlsI5?nRpDx({ip>wcu?b&Vih*7X?Di@PdFS9Cp&^s=rR z(!Q=IkfytyM4Ih-3h9+yyO6H#dKT%`UC$w1+w}s{>$>(JUDx#z(p=ZeYjCB}#Xfse zxvvGLa`m=O<$8-u<-Rs$Dp%U{sod9QOywGzHI-{DI+gocd@5JhoT*%0$4=#y$)9Gh zwnH;m+gmeO+u<497v3@7o5?x@Gg)WrOx78h$vUUZWS!Gzvd$}yWDTp2WDQpz$r{!k zc`mM~hmYhH^^Bul!FHT=)Yq=U`S+;nuI}{WN8NA@&ca9C@kOkqSuDHG(%dZ8P?*IU zZk)vt9Wwtd^AE$vIp_n-e>ld}n{6{N+h$<4&A@D%f!Q_#vuy@u+YHR+IrN&7xZMXV zz1`BD`E=%4y2{eqEgiOW`N@oa($Z&6;m>21R!`;6Gf!jMW9f00E<`%XyC!khnjVi& z=Z6RIat43(bK%(WU_0({$Z6g)50FQBFB!YNKN~-+_g!dWK{oap_pZ6-pYrSWTU-AB89P}2Au-rGjWyV{*yz#r<|1%DG zKQP|qHAm(n-;wy;iv`9l1HIt==qK3*E-`<2;1c735j>o-c>jCIe0cb5^?VNGeB>1$ zd=HtA`Vp`A3SR2j{0%8iGEP3?{o)n8#IyQFP5<$gr=)j_6Xv(R`g`evA|A0=|L{=* z>Y4p&3pm@mYXHA|Z}m+RyDfg&t9a;Q@%i$5&A;%~C8mc5mRfw_*d>aulz**s4&;2T zm%jnz_-|EwSpIJ53Gf(fPm=4W2{Qjd#r=~!Zxr=S1G!y#z}emd151pL4=gpJKZ>uE zf35T&$okit9v&DL?^k>je8kJXxknK;H|J7HU(t8y@Xgo0D_p@F{_M4jZ1VEN= zF+M&p4P?9P?q%ScZ80iOD56OI4koi_h4=PSFPCnw@_Uf&s z4~!HQ9|E}!hRq)yxL@&6(B?z_PLTEQR=f^!`%Zwi-OL{z*ef119vC@f+%n(=wEn1Ail-It2Osfvy_yAWUMxO5 zaINvcNDj31DgRdaMfpP@$3?QA!yxDBe$xj=Dj>^`g53Tj^O28u|NH6=>D|TyBX!W` z3uOKY#rJ}&_n>L~u2+l8=M()P`$4k)7LfU;nLaQQ18u%Qn=i%ZE1p!m7i2vo>scaR zV#M(aWIg>L^O4NA5@dVVnm#a+H;v;M$ozvK$3rszdW+vRaI0yoGm!a8;t*)_YH`df zX!B|s^9nNm4#juNp8#2oWIKDsgNnBVoj=WZV5A%L`@Z>HkNo-adqJBA`AftM$aT`M zIBDY-^NN$~-+I&7Z^Rv-^~>}x2fUDLrv;qty*dyDS*{!8ajeJu*9H>umq;%KkHk1v z8ixl~Nw1eKf$S&A`iA9KrAI;5zeD~`4)?F@p5z3i$n5BmM1x$Vd)*>ZX=EhAnPHSpTs$Je6RFDBhEK%>OaVQ0po#@7U?L+ zadv~Ok7T|calY}uNU!1~<4eR#6z^A@WSnIGvx@VtRnWgydJwdEkYA9$Uiwz)A&~h< zmLC@HSA0})lJT1S9n!m{CqSDo`Fq8Kiu>DLoMb&M;xr?UpP=oRpv{xwam9P2=SwF+ z?tdi9^@2S9k*q&uJTS6E`V#33$o%~v+aZ}htN2RkYo+rb+aXzQ5M()$6Fm1~UHy$ofgv-&B0B z^g(Ga;_|nE%s0)5^F7G+NY>YF8tc{K!vk@U?e|E}Hy#*CfGkI{T*~|{1D7bCR-9x! zCrwh{3i3K~$b8I?Y0Qst%fMFgek0CHAj^${952an>=1VwF)twVO@Qnl$$WdogGQX^ zJ6t|L$ofd;YZ0dzF)tQ(WWC*{F+a*jw!@E`-ZIc5e=f-5*nHENCy?cPLH3JedrQQ$ z;u(_7e{pvHdz-yFQTl{h)0}ko~4+zgwiI8F9P;nZHN=T#)C#`HCmsL*`3KFEQeN zQyP!`(awPpJnXmj@W9%R7uU)k1Z_JgzFxdl@gc=Y_O~p5SbD2*%fS8eD{0$3rrIO8ye*OT>PV`AFtl zDPAiMDo!%KUc6N-TKvFB$uy1+rm-FsFB`EArSCUl9fBO!sBz0cP5ut!@W4*vmVw>! z>!7VW=?T+Y2KJgB9yloa@Kp-8vmfNR_>~I#9hN^$x?AzM{2uvprRPicf~;qW^d(}( zi2F*#SIWOu95f%-InwLJTg4&8N%pU7#Cb+~tMvWSJH*{$6WopUzE}F7@xVv`e+7is z!!1UfH;h;x(kbaB(wB(+psfSxmEyHVjKkuNwhk1(RUEQ7w!3s0WdDYx?>Az*gWOL? zLAFP-zM65%zz*r%V#9b~WUur=v1O_&H_eFS1jzG0$@;rNt{0N^_lWbwUW;S@kX|BQ zVmvT{2X<~gjM&bKuN1Ep2Q7}{H^}EJ>*Wv0CvE#0v3;fQ7e_&x2kDv-#|`NT#YxuF zRD7?rf0`>#GTtIaLC!py=&d-XIj4u(hM%*7tuQKAk zT>c>pdEh|-ywge5!*+8Q+luBetZ?o{#+kCqU*SnQyP? zn?bz=|2#|c1TvrBd>l{2nBv`vlbmmo?e&1Hcdq$s#*(1T6Uh2{6;By)zbCyEob9a{ z%Ye+kQvNFWdHL(*Z;)S-Up61>TJf!lSB(qDNY=YU@d?o8;XRuDZ<@w_D!&C^3vxW& zAoG*dJ>pz&w)f?+lxd7dei~$Z{UG-*lI2#4ImPp)aXtl^k7T}r{Pofsz}emnW5dSR z2DVD?P<*FpJa-4VA2!X$I23O=QsV$^9MU}?x8D-6-{R=6;#u?Yd|dII#kUOPEsp+5 zlkE3;u_(V}8rOv&=VM4d$^64&MLs#<{dr{6G_DI3uYtR{{iN#_-!ec>cs-Bqm5-NX zHa`D*$aY$w$!_^1b&og~^!pP3lK>~Y?4!M)tyjfUMr>c{wBr5mA?sVIcuxM{d&u~D z#f$QX-b2QR6|cx2HDVs*?-1*XPrQdLzgO{nMqK~mD`Ado)i_r8ZLpRzdSLHW{(lM~)+kM>KG%)e4xWyJYb90b`flKIw)8x${sTrb1&E7GGN z%aJU%L#!)4p*YF-UU8oh_n-Lcm-YHVn-`GzTEv(U*NcjijQ5C1#d{Se8DAo16z^A@ zWPGKVSA0-$lJWK8kbIJU*@*il=~2Z=#&?Jl@=5x8#eE>JhyD0E+{Ozs-XexU#=8|K z8SfE$&Bt|>biXvod|4xo1M*4wL#DBve6-Q&aZtMYe9Hx=J2z0dTDv6k7c z{V>ROdZd?#X^`W|n8x*!{Jiua$Z@TgUy@I4W!9nurhdqG|w z`{S-Z-D0n}1Z4mErB_N1O0Sn5k{%T&M6buy>leeI-?#sNqSD=B0_1fJ$@%UT`^7ME}w1zt}7Gi#c&nEQ&*71!Vgq+aDEog4`|>()*y_>o z2gM19Tzh4Z4^rImA*)4yr{Dk~o`TgRcSO8gmNP1MPgRE~t+MDaN zALKklrMso$AnQrU@0FjD-!DBVZUFgQd`P+svizv@gt!mnb6Ni}uH9}i2{M1LbiX(# z4vC}UgxCaGuKUBTzFv^~VZZdCSOA%CNV+UNDm@|kk9G9~K&w|e4s!eVN~fgzr3b|! zaa5cD*&qLLj$zR11v&3?rTN?D-2X^>AE5Xk$nryqkIJ7A{l{w_#a=N3vYmeEJjnhJ zN|zKLk{%T&ME?n@SL_x0#X*q$9g-duC*=E2)I5SVUg@}WuXGyZdxn1Lob(`Q^~*9Au~#~!c)#?ZSdc#?Jt|I!{`uNYVlQaxUpfo2-9h<7@<+u9(Z4|R z1@inEm5zzsVnT6}^P3cV#eOjha-IgI3#RehS9(;O5d9~+eg{BXFVabHwwD^~HI4bQ zIQDOl?PpD+9qA#lY(CE0(sgk{^iFZ*{bB%QyHV+GF(IZD@0ZSkwm(T1#UZgGj*4}0 zLiA2m`(n4)E2hPMF((d+MR7>n3bOwd=}~b)^q=P1i-O#~-O_VGmP^R*mESL}QhZQ) zNE`+ETwtf-6Y}>-`xm?(pwd;${z(;56N*h6rTXO?(p%fJI;W1oB^3XCfzOeiv3~^ zobXO~Y*2aw$bJ>gU-{UO{H^jU<_|qKDt|)U2l6`9Tg>_<9`l3N57S5eyc^`aCQM)Y z^Iqu;INM8)^@Gfxv-s^lACxYd{>{(p##xmH$med}8Eofcm8kKSN({7hWBQksw6UX_ zGcK*>LDpY1U8t5o)-n7^l5l~2-dNR!m~m-1GQq>f3G)ZJnNobaYRo|Ntt)8;RK zJR{vN=FGq8@x1h)STuk9@e;`Ab|l-Wm_KK9C&+PoXR*EPXw>-q(S-4}(WM~cY17Bm zR!Qeff38+E4%fEIubBRAttP!wteZc3$3E$3g6(~JN80%P9jidLlQUi4Q86C#gqLK# z&p(kc{^W_A@&BHv80Y=MJDc&Vevtrq{vkQOv@}Vb1-ZU*@{8hDknL7M=Bt6s*D&5N zM$+F0`h7jW@%1u(!x%|F46=O8e7uK~pA@s^-#xwxWWKU=I7Pi>pa`KJ602fE^quSS$3PpO^itDX z23CoAaJE+*D=FSEjrlpx%~Kd;dohsnl@v4PqhHc_X_EO$;#Tvw3{<5Xrtuwyv~Q{E z1-V@^VqShp+zN6WJB@hme?IeLdx>*Fn`e;iXT-d?LGdccat+b<5tn}{X!9(c7fWJQ ztbx`K_|-SZ3Y0&?#!mV(T`O3ax*_sI>?71Jx9+$o*B(Dg4P=EV)5 z-}l4cl%%U-9h~s)eA0K38%In`f^3(x`H;>?=RwW`$#@Z*@P7YfNxEwB6=Njhb&DUr zvmx!fSnY`kaKiiI&OFF^i>9~lEJ;^EtH=B|clwsQd@(TrPIzZMl?1K7Vix2$@*vNT zB;#fIRrBu|i+xn%1=)TQWWJ^HGtzmn2u^spr%KXQu>o=&ku2}K#4#o&LH2`WJPl5G zk3Us1AMb6%y7~WjDz?JqOJ3@j5%XdZobVPuU6QVf4bk^8wJ#>cjF=ZoVpVL2-pAed zp+1oPjY`L)lVS$syphb8mnNx8(j;}&^bKPqbwl(n<9M!r+6U5)i3#%`cseOv1o_=+ z3FP*zNLNA5uXnln4Kf~;j!7rQA~@k4e7YoE0ahoJf;?VlK<4wl=<-FygqQ>wPfKT{ z^B~(T$}dS*q^r^m(RZ!OUjbQPUG&zfpJE$a+&>cFcn|uCKWAIk70#LGFJ|kmaK5oGyVZSCMXj79Vi&21xCF)#-$o0_oSK zegENf0%SWG>AdLWT)(1XLQI0JhqV67&q$XmjMVg3}~*RE&Xs-?L*0>7?oCD=CokmzJLsi(*Bri?Q{tKS|NMf%7o?mjTeW!`EFr zBYHQwe)vF+BPU%HOY$qyRq48PL)zP*dO+3_m5xa#q?6KV>5O#r8=M#1XMI!iF4o29 zM(3x+oLCg2-*WMUm;>35qL}+n7cbq+c^Dq3fSmuTwC@(@C&i3d5*woLR^<~jVpa5Q za`B8<5*uRdzf@k#ixtqGnUoLCe~idUqo z(sgO?PFHUXWIZX6-#b@89)GLyy{&G0M8!DB?U#^Fiy8BW2XfMR>4Fj05%SB@RmB58 za`i<)+n-J2_nzdZ#Jv2nbXB@0T?g6khWyxF8kZ5*1t8aJMt%;&bGl#VL0)GSO+Wd| zy0PQA2FQ8wZF7u?88QE^f47Tg#JuRcN4{7RDHxecR7nz68krq)ek2lk*3V-Ul9u{slm8->CdF$nBgH3n0f=l&*+XaJKiAR~p8zztS{* zdCa%R^(Q9ALDruD+3%$Mv~)%~3)(u9pO;^hE=iX`=BvoB%CAc|q`en4PSEB@IwqX} zInJbX2IToMYg{vyQ#`MD0o;xLm!zv8^EafMrtv=cC0DNxWV->7?}ua3ap|P-u7Q+v zM)9n44rKrG@(c1y(q-wYbWOS;-IVrzrT&1{AJF+WD zY8tHkH{LZ+0J(pXtfvIBUuBCg93$yh<<~&wuS+*X zZ(RKY*}s^W0Bt{!P8qihq~&Mi=cMz}MbPF)tcwlBeZST`7;#-=L_g)%q<#OR{)kC2 zWyF24bVJPl#^o=GRk0y@b=R+`7y~)p1Zd-wPD|%Nj-x16#45;gb?Jt**U&tIj7Ozo z(h2FLbXqziodekqlI`Z@7v-0vt71d+{nqVAG2^QP36SNJ(rM`o$bRLd^U_7>l5|yU zh`9N)`h1{`OFAZ<6w@I4pOMZ6qyU2I5AXzvU-E_9HEwkA>qw$3UKUQqmbC-q(pmkk<<( z>6#JsfGp?z(ZvHG>yH_+pU6*2rxeeC97j&^ymUeFlJTyAisDu2x^&YtzF#`v>Is1S zt|$t!pE3Dy`3d9lkPWIwXzFC5Fs&&w~!FUl{;FB@^5mhb(^wHp;vS~bb2YH=SkzX|*#~0~_wD)IiKhWmS zi1jR8maaN9mGS6`LUUZ<6Q3q~DcGgIs?Z={(4O7mOIMbX9DC z?7uJI^2Nji$n#YiydUjk%*Xjox(c#fLyWe#d5St*M(|RG-UmwVNV#e17k|4`v#D?O&R+ldT+Huc_bs}cO9BBOkS#L>x zRk|VV;lIt=^)dhJEb`^WqWPG2ko_ndaa;rKxTbhj@rLNbA7p0!n3x1vZ$`|TkLy+N zBy69ubVG3;{+qbX1IT(3AjgrEPD^K`^I}P?f-K(8k1Bfrd2yTPn*1j5r@jC&iTf49NP*(sBHELvF`3XziKC@kcBx z-V{CjH$Ud9fxKU@OE;wh_^+$<<6=rogEo&-8OQqu(AqQo_&`y*1acfz(VOP8=lUf7 zo=jAlq>h0$U-A?3)6yjK)gqRj{Vo|J!b{|1`S`mX(%ww{|X=pChgigA$bB&5?~PArOL(E2M~ldelg@n6c>?}V5Z zb7J`eu6)zDWuWjur)y$UOyR#ma$RM`g6KunZ!sZyG5KN^6};;D`H*rdQ>ks+k19ADxDBhAlpkz=ft8|23f8mT^GHh zHC{0xro}91`=xY2x+q-{>!SA|)h8yzw3ri%VnwWr-W-(|6JlD-i3O0~XBMT)(iQ2t z*ff9Pm^WAR2XZ`7>9}-4IxUvXUoln_1IMu3tK((Rw)2NoA4naQPKaqSt9VYjC|1O} z=pF0otBc-os{eS`-?$h!L0U|Je9xPbPD^K{bJ9hzBG$zwXy3t~=;{f8)KTe#m5`EMADi#d?(rA~I+sr();!v0c|j-JAHUwS?Ta(kvtPkEsrT?AQAS-LLmo$CAo zXvYaLdz#ZV(Oby+&wU|kT>nBGWc~`sdh23pk@EwmJ0?KJ(_&FOc8cASu|i{6p3^BY4YK~MbWXY{om%ShXT_QrIN$kku`D*l^haF1 zIk5n;A7!y7W|uj?Af_+iIyz~07G(aW7`@Qd7YFGlq;q0Xj9%pOrNIer)ozmWPyl(n zEt-G-Ztr5of4@5p+I9t>M!vM@EqC=q#W=`zQ({_vRyrqLFpckbr4t`@^`=0!Q@q60 zTL!6XVqJdfQndpvLcIAgrvo2%jDi#12lvFKbD*tX>E?UL^4?```~i^tk4hJ$%VJGz ziqR`vxrCS&b7J5Vst=s-F4_~9E`sb2$@whHugI@SH^pq9t0xDtTtT`hU6!s$*G2D> zE`J>4xYE)&u?~*<3VWK0d;d!P@SXr@^I-Z9d*aeL(E2TwsNj6A@irDN$ZDnUE0g4KOp;)7K>u#UAPv|^(E2IeG~&Ad8dp9iM!zU6Rzz>Do4vT{C{`l(qd7ph>0(`e8uZrzPcE_ zUggA`SQIN_{ayddDlewL;&e_dighviRTocyJkF=ZoP2WB_l+0p#ur}n{=?;qiU~0- z=ER~{0XhHVsBiX5iJaO4M}41vDQCRrr6S1o6JKK->u=EIOBbA<6YJ|)?(LVNH;{{e zRROJj(}TbAzD|F~uX5jTe(_tbzKWQ-$!+%n$o0|`qc^*JB>NEuZN11($WNJ%{Zcw7 z7C_cl0sX$oFV{e>|G+KIPl*MP^;ST(Qx{XWx^-LwSuVB7F$?ZSzM8c6U#_2VkogK? zq7K06FhzF)Ke_cJVZ5^Cev{ z;(A3)46D3Y7o$ILenPDL(A85Hy*r(bin;fY^HY>2sVib#^tNgq#e|p^v!Kl@$aziu z$ZgLWX#QP}O_19;aJSPbF$Y>d#G3r37{ABmFM#$uS2}R7^P?ck$E6EmQ%wC>?SpK; ze4o=Kw{ufE`xEDr^a~*O-?H)5ftuJfzBwMhpYc}*QpPvO%f^Lc6_EX@fgFEb+S~5@ z0BFxCq*Fh2{i=XgPP+Jj(`C@sopfD{KIq~JF$FSTPP!;o#JZSx$dyZrIk6}U05QK9cjAeaz`1_&3%oU6+pj-1%v-C`K#J zPl4=DTDl-zl&*_`s%s|-vYoV86w8X&r2~&EKWO>IqF5KBqb?o?Ij*#HLAoeiHjVoc zkn6TCzbPH4x%CwVtvxXda(qSUvUCk(dv*EI9V#al#kv@MLUFMu*2U;A6c>wPU5q}d zxL6eHVsxkCVo|J%(Wev_i(*}jKCQS|6zgJim*Qektc%fS6c>wP8MJwljy~)1#X;L% z(m9a(TT!ftb#G@YzIu+D-+jv$1J6610J)uWVnwVe?!DmR0g&}3 zq_d`Rzaw1{z1R6;=b!8YTV!c%H>bLBEITa z2DvVy<4(sxYgf7`rho0`EhiSmidYxD|Iu*>WdCwvMf85-`~+z8CYC|ATam6wH^p>a z^@(MW{jNyY#c0FjON&{M^%jhHt{}fIdJ`^xR7{9zu_)F>uj%r~L7OM(oLH7$k*D`E{~eRb*R?_IkokbX|OEL}6=e(Mj6V;ms&yNVd!@5!~ir|K$7xNIxy+#Dd~Q>55nvy*FIB0Lc0aVo`oY ztjVuSd;jOkMa2}z@@eU!Se9RxjvjR75@K4ci{2sCD<;GgX!T1Mq>IvZu_?dsXP3VS za$aiE-e26jMnU>nXqG*c20QyL>5-@tkx; ztjYKO>heX!6lmj>&WS~_tawGbE_#1cJz@&9dZcq=L4HxXBG$#G;@)A^C&odmPdXMgp<)n*ZMXZZWkomlz+7;s<{e*Nu zx*}Z{oASMo%8PN(%1fukoLErYYjx!UAoEA1zam|eu1m+;UAZ*K?N}7E5$6}gvRDD_ zcqv_%Zc4{HT)70ud@1R)bk;Pk_oNHbb&&0QovvI|Op7_OD3&L?c4|``n_{5L>A08@ z3u0NU8F7Cs#^2}41>WyiK2rK9)%O9%6lm{hKPW90{>Ax$nDd+Oq1_iAPaoeQie<4T zHpRdRE?-4KEQ>X?A=;CFN&*f^;O|fvX;$lrqEq8ua zEQmF+DF#03@}%PjlrJ;UkJV#>-u8A|OM6%Q2ioVhpWL2ozo7l{_Lc4H+HY*%)P8sS6Yalg|4VyEBpNw4@~Ozm z$mb(pj(jb$F|sA{gUHV#waAl^XCu2KuS9+mIlJS$jujp0j@2Ds?D%TOH#+{a<2xNc z==e#;V;xU*jCcI8r8cKJO91&Yn}hu`Tfp&JD=!$s`L5Impc!3Zk>GZ zw&JHcRk(pVppTw<4K8wd`; zEe%P4v>|~AQrBBos??xHTk2G(A*Dbm&_a#kbyrGnh4%LMoOAYUq@nkIKY#srJmxj8 zdC&XI%$YOt9@)@pN_u&EUHbLuccp)uu4SZV49dvOD9WhKI6LFfj6E4&WyH4b(|SSc zRjoI--qE^8=J?DRnI)M!Gn3o2YSXJtew)+U+}GyiHe1_#+vcY>Np0JZ+EKbd`;)L zF4bMucX_|d_gzkQ8Q%4@u9aQG-OlRvVz)irzU|hbyTALm?hCrF?tZv?N{^X6=J#0A zhnXNmVMjyUEcSCzOVLe-)~;Oh5f$n z*RlVQ{xka*_pj@JR{v-FZ|(nk|L}l`1Ih-}4mfkb`2(&Uuxh~S0c!_5I^g60nPtr? z%Bsq`Fl%MjwyfCfF4<$V3$qtyFV0?;eRlSB+4p6Ka!${=F=tiI+c{nAbM003OYPU# z@3lW+f8M^;{)YV>`yqQf$3RD}W0B)($K8&H9WOh!JKk{o-o|1i>I5{=^f%7 z>t^%b)L`$bcbvLnaM5ZAk5q^M+hGIBNLw61RVCeCo zvBPY`S`Et^Rx+%7SlzH2hrKuKqha3-OB~*FxPN%f@XLnZIDFOc$A>>N{K)W6hkrl( zKf{xQ-GYu_Zg6t2ELa=d6nrCiB=}A6WYCtIp4&ZlVD9wXs@w~5ug<+K_mSLZb9dzK z%{`L)Rc>miO{ibU9`c5!hUSNsgf0nP9l9a3HuPj@Q|PtOx1qm6o$~I;dpz%%yn}gP z=KYkXg%iRV;Zfm<;W^=R!Z(KR2(Jxq4et(r5DjQ@Ci*9oI0)J#}4VatS1CRiqpn|R*DH52zuY(J@N(z%mfm=ss&FPvR? ze&M}^ttR^>FP^+^@*9(nPaZzy(3J4h-qR*cJ9pYs(>|G&I(@?Qsnd5%e|vhX8JEwv zZpH&M#?72F^NE?S&HQ3!%UPXfjh|IMYt^i$XT37(wOK7@7tUTZd-?2#W*?mW)9j=< zt>$!{JFF{gBH<=l(sUNQI1x%bX}X6}n~-<^AC?hkYSJvX+pTZ=emo_Nug|Fx){XvOT4A-af6(H%)I(NXjk zS~|E+$gq zN#bQOgHq2Dn?$jAT`b^M*Fy1zI9==&XJ8@LiTz@Q_&}T`J``t*&&7q}OH%$yTq1rF zSBl@nRU%GaEmGvQB28W|TF4tjC%IB|mN$u^{OQGEaupZ3?hvQRJ4La)Pn62FqC!3> zu9pvs8{|4J3p^rj$HpIjoMT47VTwuo3>Hj zu5FcfYCGlK+Ag_9+bi$W_Q?mdx8#G`+wx)U9r=j1Up}V2FQ3p3$fvY}@@eg`d{#Rm zH)u!Y3)(UHlJ3#8u9PnAjD-n_bKUJga5DFgt!rZ@emh*Y8 zYs25YU5F=E3h@H|qxhTf3)dLw{`!CruM+NZCUuK%zgCF1@H3Vh^&Gd> zD9?NM8Ty&$3Gp8BO~0cVe=7OQhlDuHb6ejK;v@WD_h9SdU;i#PEPmEILY%-i>*KOG zDSjvX%bg5r>xFpk4k6<4&H6LHN7MQ>>$L#=B=nEpB}5DSYZ%!w@n1hIL{K;|4qwjmaA!bRk?Kl*zj?cZ$Yr>ZBjKFoSD z>({Iw^ZCD?H~QHnZKOUPp4G?bw;irG@;~oU+AR6rJIn~*j-Kk5=5tHWHTq}TTp?DX z_g!Bj{!IMa2oH&s;(q)!{I!F}@gI6zi0AQ}=65IgY(^f861B`mTigC3g za#auA@Z0}sg#YYf)PD!YK{YR_e0mcfLwi*7(p~)3hb+RkqOZdD%`p5`7aRVRi2UCX z|HGbApO4b^3*q9qu>%b~)$am?U$YtC>4(>h?{Bt;gM$sds}qg*S2{{D41F`5%`1%W z?e1>8&$u3jf9+ewbC2F&_*=+F)kEwt!{10)mHYRH48NO2iZJoEi2wNejCq9Bd@=!F z%`?;S)x0?me_kZtu}h8o)jXy2N~nMJ+@+D{-~CC53iaFx!@n;Qwnf6V_{)etPPs0` zw`3dTNjxINm4t6O$4EahGVh#WHOgte-+z0r!~E5ln@DHSVI#fV$on^(zi%gA&D;0m zt9kr!eDk>(e;Lo4^=9UmO#6Bsy@z<;P58GiGs+no$tRn?hNJ4&?1!I6%5Bcu-$vd? z&FgBOJ&o~9**QaZ)4xdXCdQ*(_@|v~q-)me@B>EuzCw9b`o)Z=%5PU{_$Mwk{5jVc z?Z~tjx<}fP`JL2!qux{5AN%mtyrjObnvV|RtNBKK=b1MM@d^IL5q})%sBkU5>R(OU ziP=8P_OhFP@-6!2JY>#4=KN&Nzos2z&Re$Q-Ym z&TCD_Z*!hB=Q%Y_{K9*07;5-`_LKTJW%k?FjKk`?mGD=Fl>cnR|8Mh{Ip3M{S=0Hd zslBYezv`D)F^?#J#gj((^fV*;+|pM_yl7i z{xuOlgs;-QB;qgm)p%}Y#NUCRjQ+f5jrVL#Sk1RB@YQ?Y{EOkM{?-P)*7z!Zc_iEd zU+FDpo>2YcL-Of?-qg;Vs*~^dR4H-@Pi8()`K-l0RC+%0Q~r~F!yh}_@W11GsPsM= zVC2)({xHYg8*dWALwaU=H1$<|s^?XG4I=*krJmGts-BeJw4O@;V2mX_RbR?JQS~v3 zu&M{;t9qHLeBzaF)~8uNP3yB^J*jex>s82O-UV;1w`s%x=dbt^2$?u5x_u`v&jH&lVwefsYeKUU94@NzG z(cbvJ$J-hHVC)i=UP^DH|Cn~soJiQTk4$@}sh!d_#dvNLcE0)!=U!*zlN4zeaWAqL zLi$%{OYsE$130A@dz0Z`c#q-#FYo;<&;G7S@iP8b-wClD|1#_!m7e*YW<98Wt=h3! zANl7PS>7drqa_>7-GwEH4&o`Bu>6!5z=o?DU%-^gpb6o7fIHl&(i`yE0%jw2=WZ7o; zIjpM^d7llx)5h`5^}gw6Fb;P`u0_TX(?0(`vJNoqxU5L~d?Hd`dl^rA^PFi{eoi^n z_;bOHhQFuQ@Xhc29N$mH=V0$B|7emiZmo*g0cl0X{Jmx=--2{a`J10(KN5ZkUwx-p zv?~?vgs;MP@Z3RibC6XO^$2zWKerW!12SMdly*9`B02n&;F!s{WehorKjs zfSQ*^@P2Cv_h&t(*3D))Te3f+!Us<`(!X+{;ZKi*Rk_rDqq$%3OG_ypC7t!HjOUt$ z`yyB4lo`K=dV8vYzBxacMH}%_~p74vwP2;SiFL*UMXzXBSfwz&W6stf@+)k=e+@YTW ztszw@?g2H!o)JUf&w`qGj?)||Hh`LV zUIgJUfSPzwgy1iMns}Kr9a<}>adtBTz8TcS7Eu6y1=Pe=F&h3VsEKX-7rs(#2Q{&S z^B*Z*12wUeb0Epd&qVkxQ3&4+YGRL=0)GS4#GCxLtCAC;Y4ClV7tsqrO?<++ktRMB zbC7%nYMc-i!M^}CPKt`*UxAwVT9m-Q0X6ZhD1(0oYT|oNoFso#bOHQFQ4T*2YMd^e z0Y3q1{He@E;J-x`_={Kq{wiv~-#DGp#P4DmnkPX`^pPvTzVa-vpF9V2%kw~wya4pd zi@>0~1pQo46CrsiJP*|PLmrpI^Fd9FkXOP-f|@Ace2ZBR)Wm4cxuh5aYGN$sT~dq# zH8EbUgiio9F_E(`DJFrMD3rIrCxe=pB5#9F1vN2E-VUD*YGQ`G6Fw8v#4LF?d^V_Y zZng$K7t}`2d@D&QOl{E6m_5`mU1#D#WGM6%Q>Bs zVg;y)GdZD?;w(@Tx5zE*TxeM?g(HD)+-512s+;55S)Q zHSwf81b+&o_wmQy>3tx*PacCm3)1`Khwu#`y-$7&e*vWT$xq=gf%HE4Iea5X?~`A` zH-q#(`8E6%klrW1g}(~Y`{eiV?I68R{s?~!r1#06;ID)9KKV0zH%RZ3zrf!B>3#Ay z_+F6SCr`rP0=d7(A0d_epz2TfyYesid!WW?r<9WOPRY2Q&grBS8K5TSVrJ8pIHT0W zK~5+&aftItDGr00IKmmF6h}c#9OJA~iVr|de8_pF6d!?__*iQN{{+-Tr#)1W3=$BqCe#uk9nV@HECV#k8>V#kA}u@liO12wTA zwh&wxI|V$0Kj1>oik$&2ik$^M7CQ%gDz*rGCbk&d5L*I1A6rHWFMzb1*ahI`*m7`N z>>0?ngR~0%oP;J`kF5fC$1VZih^+zNjI9Ir#x4WDja>o$8haMlG4332K-_s?R@?=o zlMOQW#a#rqgUo$#m%yDMb6?!0a5u=<7SlmkZa8MIvaW{e2#odDBdQcNL#N7s832NfTxZB}3fs7AvcY?RZ-3{)E zTZ8-!khhDw7koeNe(*rtTJT`pL*Svfb>QK+N5S^-kAofJp9DL`ujkoLAnh>z8F&|v zb{PL0ycM_XGKk@mt^(Amd*AR(KuAxEH?- zTo%6rTpqs@ToJzuJTra|I%k2JI6MAL@SOO4;Fa-jgV)Bt3tkt$AG|*P0C+?EA#i2< z5uUmc)Wl8k$H1H8KSXj1NX^8548IN3#H#pD!Mo$t&BWCpBV_!S;63r`F5F z`28R~BK~{$T96(Q|0Db%kg+TNC-9~CpOL%_YGPyjFYrwuEinE!_!dwTuf(5(Zv{2+ zYW$zzw)nrm?eS7$?23;8UyF|ecg9;Z#x9VtD?S0f8)PhsPlUe*YGQwUGWdRcDtI8i z1@eO+-!#4zcsM=-JQAOY{3yuC6WDs@I(-s*3uv3RwbBX$w8h9Vg*|q@D?EBr^N+t1u}kG zJn#&Vk-_4FXM)(hmH@mh$o)~vAh4rl2$D`9EzmLy-VLM$T7vK%AT7`mg7*SxftE15 z4@e8NjDYt8v12U-@Btuw*)kfQ4bqn_W8rp?zHAu}cY^d~%S5;vq%T_v;a-rwY?%W0 zgY;z!w^hVIkiKl00Ur#~mo2m4LqSapx6FYDK~3aZiolSi7)c(8bz~`l=Yv>BmNNKA z5bMaY06q%DIpBxp9eMZ zg7p{piy)(}^*8v-AfvAJBzzOdsB8Tbz6E5|wf+U)3Nq?ir8@ry8Fj5O@Ess6+!_bp z3DUx?7WgiZ7H&;|?*VDy)_}ic+-m$iTzYA*OJ!>oYevnbx zngKrmGV@w9;fFv*X=_{f5l|CHt?l8*KuvsL?FfEo?Tq9jkQQ$33jYMu#1Gc)ph)Nm z%7osame3cpCiF)q0c2!O$bu(=jLZo+V5bBJlFpzex+J*ZT|w^ZCwSo9L9EIIAG{~1 ziCzf-cyAC3GhqGGP|H0Mx{&ggNlhAmd>|5jZ2E7@V0<0?tY(17{~J0Our> zqd6C(za*RiKMlk>O;`k92x_7xp$e=`SOV51)PPqd)PdI}ECa7kSOKn1I1BwXAiXK! z9QeH;JtE;e@acpLz~>S!LcRgSj!n1(+?sGHl2<|6Tf*hw`w3Tq2NSLa4<%d+#@Vh% zCmz&9qHQJE%61c&Zo37{wB1H*8<3gQc01VKb|=`ub~o7Bwg&8CyBF+gyC3XfTMPEK zJp}f#tpoeo9tHc^9tZo|o&@c-^`PJO3^>~M95})DJh@B+H8IKdBD@gP#A&vd!4lgh zaG`Arc!6yzc%f|@c#&-fniqpuZnmA^8rv@L9@`%9UfY}CeYSnzQ?|Fkr)}?o&)D{Z zn{5Ypb_+<4wjF|R1L@JWBk&y{J=%5*z7xcPvV92O4Pq7BK8C*mGF#a`h3^HKt!$se z-vXJfY+u6P0qLQ(ufZd>Z;>1YnS*TKgCE&`1V6U@1b%A!nb^-jW*plu@Gn4S9NTZ; zaob5GKY`30wm-pt+x`N_CrW-HT$~sK&P$8~&rGy{|4K{%FHKAYFH1}YuSiSsEI!kJH!73X*o$&NoRl`B`pF!O{xMvOIiYco>T*VkyHnMnY0Z2 zDrp7yb<$a+^9`tp?~={|zfU?3{2}QA@OaWi;7>`HfG3hJMe}EnR*`f${1*^gBI!!- z_oS=AlS$Wte@K=K+eBl%u1 zGx>hxZ9sOClh=ank{<#)B(DQICO=ASCy_p3tV^U3?cVDbSllza%xOFlwu7-WP`J_a8F(t49WgcpE}@W~&;M}zDe zB!3ED1hTG7{v13j`AZ~cgRCf%zlNU+YT~@)Z{g>I?4c!p58jphBa*v8#_;5y;A=p} zrR1N%hmwB*A4~oXd@}hY_*C+r;4{g8q4_LGt4@}2jOWQQoWi~U(w>v!;4g!;=VS|f z6G(ebP5@s?PDHX5q&+7m!?%I7=j2rQYanw?N(*=$$l5uj6?_@U+BqcyT#=Fqo|)1X z`B@-q=alx~IVl~%b5lBlYg4*{52kbnA4=&7KAh4U{5qvCPkjSo6{qxve+SZfQ?lSc zfXs=hIpEw>2Y6ws3%oeh174l#1FuaDfOn@30@tJt0iQ`720oV>1h=Gyz*kbk;P%uJ zq`3oR97`<#-%TA29!ebx9!VV!ew;cH{4})?{5o|C_-*Pm@Tb%n;LoYEKrL+!7@JlE zCZ!dFDQP8OMp_w|nYIAzoK_BYO*;b|khTcSPOAdFX-hzVS`9cntq#mhTLu=StpG=- zodp)AodZruI}bcP?Ej-e8wE)~~9Sy!>9Sgo{9S?4|P6S`G7J{!^r+~Yy)4(^ZGr+ypS)3~T zDY;a?mos$odehH|hUkb*iUk=8nUkO^%uLf=D*Mdpu*MlkPE5Wq%o4}Uow}9#C zw}GwGZwK3?-wAd|4}+c3?*_Z1j{v)+7l1v|$AZ1m$Af*+*MR-f)vsj+q>l!((+fd+ z`V`QaJ`MDy&j9`Dv%rDr_kx4d?+1sbuLXyvKLqBcuLJYa9|iN%9|uRKKM9UXUk{E+ ze+C?v{v0?V{dsUw`itP?^q0Y@>64&0q-0pG|>0QY7lf^TIegYRUfg70Ov z0N>AS1s=@I01szof=4skf*)kI2S3W}C~a~~`;bVKQ^7X!i}r)Sui6h0ZRI!cw(>i8 zJNW~=ojeY2FHgYR%YVZ=$Y0?d;>;5`@s9i ze(=6>0KBivhWC?pct7cc_m^&Xf9Zt}kbd|8IS`&D2g9@EP2x8{i>%BRnK;hUdv!;dycuJS^{k zhvi-He7PE)FYkemkoUny$Oqsf<%95%@?m&^d<0$~AA^sQPrygXr{JUI)9}&qS@;;a z0X{~)03Rz~f{&FO;p60H_&E6re7t-WK3;BzPmr&{C&<^~6XkCBMEM4MlH3cQB;SG; z%6H&}@;&%u`96HIJP4m655uR(qwuNn1Nc<=5qz5b1U^lE2A?Hcb{qtzcO1ewVr%$p z*#4cX^ zH+;U#h0m9H@XO?J_+|10{Brqk_~r6f_!aVZ_!aUG_#JX`rx53MS9LlEyr$E6oTB}w zlMfV~Yrxpf7l84dmx0#KD?nT4i@>DLjC(Szvkz>E=0@2L&5g1Hnj2*&G&jmFXl|6< z(A*?@bRGou>O6!qxjyjCvLAel901=Uv*E8uJNy;tgm0BzQr;^4q`XxQ?YspXPRd(l zE-7!7d7Za{`K0`+9LZC!%27PEO^$(YljGn!B=`oJZ_BsPd|SSQ=G*c;a(`RCkLKI*AewK> z!{q+9Jc{N!@*_0gk)NRXj{FSGcjOmnz9YXv^BwsOn(xT((0o_^z}vkmkMnl>Kh^$2O zh+K^35m}Aq5m}4oQMnw=qw-8NkIM7WJSs0l^QgQS&7<;PXdac9p?OSRh2}AN4VuT~ z4QL*dH==n=-i+okc`KU7DOP7|bjn-yrCE5kr<=S=HEm}rQrWo%3t=?<(MXMiLbx9wdK0CcAeNp-a z>6fSPOn)o=)AZldJ7#2MOw5>`u{z_qjGr@-T6Ee zY`3J{RqgI<_jtRH+hw#bYrm@fmiFJY@7f{Up}fOY9Ukehw?kaVo*l<@+|Y4L$J|a& zb$YSW>zyv?e0%5hoin=xyIj^~ZI>6jWOaS9>+Y_J-O{^t?AEK>tZt8XOYFY1`}y53 z>i$gk-Q9oe{(JYXJ^J(*)1$D*qdng2@l%hJJ*+)5diLm9*0ZYTik=_#tmswO>%m^n z^?Iwuu}f?NivNq)$bkoqeo*oqb32y{_-AeOLE=s_%<^KkFOU zFRkD8{ch{`aKH8a`uBJD?>?aa0M~%Q1GW!%bHKu^E3?*SJ)2dWeQEaX+3T}k%zi)n z%k00hGjayxOwBnbC&r#+&#;fSPqr_#CpdaI6I~r#16*#`5ZA-5XI&>;N$wu*x7|;A zc6g3@61;7_eY{tB@9;k0ea~z2weSUexxO*JDZV1#TfPr{Klr-)C;OlF@9}@>|H=QS zKPgZacsZ~?@Im0$K<297c4Aoi@J_?ahp!mE zdU#%FY-oAt?$9rxv-3X4`z^0F{6hFp_~QIk`O`-fk2q_@zea2x@#ToWMwE?QI`W2* z`$ldlIDJ&jsI8-Rj~X!g($UwBzGbv+Ov^Ft#5&rk^$a-07R9 ze=_}>=_jTq%$PLeycs)Z?3;05#)mV$obls~zh+owrp@d(^Yoc3W?nG!@|i1V-ahl* znMY^l&Tczr^PHV?-kS5}oa1v&&S^Qf{oEdN2hGi!J9h5PbHAKhSu}F9jhhV1!TGV5 zfeT`v02ju-2A0SE0G=M3w#CNXhAv=5>~wHZ>~gR&_ByaCb~U&-_BC)x>U~Q4bnj(v}L>6m^EY=QLtQoRcD`c@o$YO1f#hQS>?ttry z>xb))!)IZ;XJNBvVXJ3hqi128XJM0PVT)&BgJ)rTXJK<^ zVQXh$V`pJoXJJ!kVM}LWLuX+-XJIpEVJqigBj;cn=U@}(U<>D91Lt7-=3w*YVC&{! zEepjt za=JJdcb=Rf&c|IKXNe1O7vV0JbHpXMf8j30T_%gLWs9(3i^P?(SX_m>8g~uuTHJNG z>v1>8g<>V{M%+!ZT-=Pi1$Qg%Hry(?h#PQ=u!8Hj{Z_}_w>oaV)p75wPOQP*gS!`Z zAMSqK1Kd7bi+hlpgb&G^x#M;-H{5RKe%sC5Zd=9OwpHA0dw_dw4{)pPLGHAz<3`&$ z?z26{Z8r7yG@jxn+Xn8jZQvH$2JWzJ;0D_U?yqgYrrv*A8h#Nw zSN+Y6m#}MJ;;!08Y+Cjha656Y<92bsZ#Qla?hW}0H`8`tdG5fb+<~>Y13QpC0NlH{ z_gIndmwU0u_F-$ig%$O-I3(Y}MtTS9=MZ+yVJw!T*doXH+a8~Zk8mI3KEZt|t@1P6 z=Q2Znf%_8o74B=?H@I(c-{HQ;{eb&XPM62!9r7o+U7nD;)Ih=w;dp*^rv7=O>|*g8 zzi?8~7mJPjRz>~0DjIv2IJ<`tdzbi(UkIpwtD~{^MA>$osZ_R$Y5XEi{d+AM`*xJQ z8)f^W>_C(qin7BZJ4NWFIxJRY>dA?t(Xt$kmgSS^6N9DkZjx4SzU7%w_G*;9u9>lK zMAMhx$=Wo=!#hb_IY{=C{3Eqq9y{GFb>HE=C4o2DGC_5TuA4J(lQT9oc zeHLY3MA=tS_Dz(17iB+0+3_el5oQ05ur}h4s7%C|Z?46d@2ka_Uo}1|i;v1$M4xCA zW$mJ@LzH!jvMy29Ey}zxtLGT?=Z{f!s#=FX#;nuDQN8LYtJRs(tBtWnTI|wj?D8l( zGpcuHRPXGl?Cgjvmb;SXn|~55!6(rY1g&OlILgk8vJ0Z@@+iA9%C5GW?{;lec72qs zjIx`e?3O576J@_7m?bzFWq(CkjLnR-L|I~lwc*s)j2#wbp(qPS*@!4Bh_ca9cD2oH z5m(#H7V%~@c3+ge9cAxE+5RXy5M{}UW~ox6tVNV%L|JB(wT-g&QPwfa&Wo}OqU@q5 z+n;EDg(Jykz8PsI%Zjp`DDy8U!v@{C_5QtF|Ev&7#C&y>A^_rOlYN2Q7twh zDoc#Y5~H$YU8Z_NYBV-A8rvc&YY~;Tipp9=Wtq`bUW~FYGtBq+G0J|5vWHrmv5!Vr ztT+~BA4l2eIy)#Fv2WpqTgS)klM8Sw;)Yw#i+fj|hkqmfjd2I$lkhF@E%>|ecj3Q_ zuiP>C=lGxFe~51lwAzgYB5? zW7{X)xB}aw+HArLakYfc$6bZHmGFJIC*UvPUbC&&4&gq>{Y3aL;t~`0$xK}L#A7lC ze-Lg&;(DzRSCsg?wlwj8yb#$niJP>wiQBZziMzA|iTku468CGCq$66pqTI<*%{X#=WCT` zb;RabA8YllRNrWCt1Ad!g_{%mSgX~@>ta96NK2g6dX4sJMqTWcZP#d5wk?T!s%>5D zM{U=|INGh|yR5c$?zk>yddKteS6g>=u8SSiWnIkZE?JiE<8pAf#XR06YF%^_>b}$RUiTxGkv;awWw?uQ*Y;>ld>dRkIv(q$o=NyAxY5?H zdoHoA>9q{m3fyfmn|hv)-i5eJD0f|~D!cOg_MU@V7xO&+mfl&GO+D*kPxM|HbE5b8 zJa?USPM_Oi-s_%2zSL*zGM>${IAYytpJuEi%{AJlp6;{>{oH9&akKlKZ@r{ndfJu! zd`&DJ%W5Xtc>}(XL{Pk{^wi2!=32whA&AtF<^~$3D1tra;K#s zE5x6XbvSu^+~MTa@rRQ~_BfopF6VIalep(_FXA3+bvStw{#M)$-1q42!haL@ICYxg z%CYrwy_a}Nztz@RtqW{lxaP#Jx849xPmA@QpEw6s;vJCYh|R{mZCmJDM!Q&}ZTFqD z9r3?y%LtsC*gkMuOhzC*ttxO0ahLJ^j#w7@)@TcT>tcQ$SeV#p&}i#IUoYFzK`UdH z4jPiMAJ=_w>sA8>+gdqsg9cl1En9^Kr?lEXC=I`LtCeZA<-scxw+z0KH22A$2H%}% z9YUMK^%|0f{%Wg#sIAo1>#_r`ynk}=`u zl%W&;Oj$4?F7=uT$*Ipw$Vh#E!uphx6Sk!UCT>d!<4(sd#oaVL&G0y%~28?y*VhQ$E4{fcp*Cy>NX>4sKN8wv@s`M`|tpErbu^KEr*F zJBiFPd3{R%$$`}IleeYJ!Yv?tHSRX}J@~KSe>S;yYUe4vQ~TrGQ?{in$6buOANM4( z7jWBgZxa3*cLMh(E`I8^lvG?BTvuE_oC`M?Hx*ZeTZmhNI}>*i?ke0(xVvy`rw&Qj zG<9_9_i+Wb52o%({GRdV-&5Dcw4Amr#fKY?E5McDDsXkUJ8=);-o(9+JBjN$eO=7y zGl;`Ihub`3OVV#M*2UD!Y}slvPRzk5 zTbzqu#*+HrLi6~USnl_f(2y0nE_Wd8b~=J~hu59&bh#a2r#%$%_=~`~i>0N6JG>ii+~eYZg}&uLxBX*VH)dMM@edDst;=L|JWN zS#^1FMfsV<^D4^5mMxlBR$Vi6o*Az+J-TLnO{rKz)--j0GD@)31czZoPBdSSp z8m4T#_=2LM;IiWK+Q=6xFRM8_nyAB}3Qv*q%H|i>Rn#`9U{xj~x@nmVX`^NmuH@^f zCfg*l6=z41;H6z=Uisx^6{R)7%F_B0smzRcvt8+5PJc~9-n0-Asj7A}-O$4F3eqfJ zT3lOZX?Ls4lBii5Zfn#cf{d)Sqsu z8!)7*<*39PWZj_t^(Zwa>K)D)Lsip?7Q5M`n-x0T&3+(5sy>X)sGsoi+Oo=;@~XyuNtHSPgs2Kg1m3u@{+K=TPUTUbwXQnw-H6?{4i zRhQAS8fR_9=)FWGkzZZ4C{)Z~Q&Co`_mAe%jV9~#s%UDaKvy<`DlJ{!G%a1&^kW-B z9&!3qTG?ud*W08XRB5?&Mkc}9+UoLowA?0xm7&lyVULkOlOF1gCaV);B#wN*W*>;^ zJN%J%4OTDE`+y-(C3KomlggIVl~P4AJBxxX~Q~gb~u!+p5 zQofq95qOEnoIycr%4&-ymCY|>;4dkgR8~_}LA05A9aAzNN`Knu(L6zZjRi*H(MFGH zTiT>^^OT{~yrB|Fw1EN8cz7e87iu(ctGZagr`3Cy5noVKxVWsOe13UZX|P0n3NtQ} zwJ|SJvfAom!)}Nc%jm{Uq9#|b$W;|kuxL?Ttuh8_{kjp~L@$zjxt=Eg0% z*+`2TZqy!mdGm{?{$+f@#u^K&>MAg649m4_GG=CjCya(uP+3~Gyn&Qlnu^G%p~xsr z)BvZtRO8iG2WnJl7!k3C8;Hh~R#cTRgp4UIu3mr-k^mnT8@MJ^Og}1KK!dBTtFB)$ zI2>v{wMw2C3+yE;MMKHRi0jqEU>w ze95b-<0D6-JPzS=y8=OXC=hnpgCVywm(_(QnCA#ObG;5<*ck}>9L_+lH!m+3@&!VE zzc=7^=Q@0TN5JWIhC+^rX*gCV*bb1%6h)s8?L{y?f%(n;zS@Xrva*2Hzo7bphOFu2 zfw7LJIoRn>{LQjgpSQAB+4W}fIy;qF9Z|y0g*EQ@y3DYBBU)Iqux4#vGaa|mXs$>6 ztz$WLRlTZoM&^wMfaWYWl}nSk4a?1}eX|UnQJT?K|3tO_ zX^f95H9y7^O$D4{@n}RgTmd$o0Eqv`2_TyH|6-4!9IUIY3RNvytme~bhexgk4mEt! zMH&roL`6DaL+!#d$`>~bt0r1+m#OU^+GCyaj4@CJn>0_gzCNuvIje@|Bu&Yq&WvR%UZztV2^AB@R>KG>OY3ZbgEXl~rofOU=Qm9{e6N+6z%O7oOTFQ==ZV zKz$61=13?~i;>W%#=-H`r5G!vjfLSQb*$!$2l#;V%NNw4Gr73t41Mz?5;Fl=c?nhn zSt{$UkbOJo#JaLN<+EE>at6M746i^RlP0jKMIgL?GF_)u1@Qd&yu8d+RfT44kiEv{V=9sP>Su~<>6Pik6q@nURCBUe>P z23Id!QC>@)QCX;ty`@EZAE#guD?!&*=4Hchs2CH!G`bR_-C_@_T&r1HR_S@wH)P~d z^T^!3Ll)U)pr0;Z!7COUI;xoTRJ@VJH46<^Fs8glNeaw*ii8`cKyr!)m*cI~n@;r`wuPcwONT1yq_T+|rUVEO;AMk~|7!COzN671NlIlNh!4j{J zG!)0IZ1ySY%k7Gkdt9B`m}L5`%B`D^^&TxZ3WY0bYReX7(@J$yznpH7J-KR%K35C% z5e(M6TD?}V#^Ck)yDPA_qZzrmn0CsoZI4s!I(5-T6MZFBo(NTwZrD*AWQh zsXbdSJGb_r*NF`k^yUVgVV5Tya0c>x&VZXOTY;&S>k9?_4sXEj@pyx|PBwn+j-cNi z@Vf%}L6?XB0nP5t&2@VNF26J6%kxs*!CW`Sm){k3ipkWKntrjC%xcwb*N6qBV*frI zBjvU0x6uD)K_O7)lbN>8PGvqhBNm|UV*oajCEGiZq}V%wH8chy5o{H}jLG|dncxh4 zW+L}c55|WWTpaOBB7S*|F{+fBa#q4Mrlk>)l`Us6Rm=X7k%ZcFHJ>*2>+;GL*Dj2R zSn5}^30*HJZNSSTO10`#p>Y}$s?-hwYhfdks)*0&25a{DW^i#tyriy*Z6nGW3C}OD zW<6UUo~QSONMvz?Dac5@1WhuG#H%fviZYcGL+B!giqys(-= z78cJhD`bjLl7bP{RdtK0MtHp$Nx)crLq?aaP+I}fSI3wY1L0hgu<1LCn8s5#E&$zQN>G(3rniY z7uWKhRIb`jWc^;g2wSDPe)M&ij~NTPXq!>;$b_eN7jqZ0!IBq^$P_e%#o&B;Kz)mJ z6B(HyScWUhqg3tE8KX`#6QyOYn)Nv%8sl*Zm)~y>YWWt5MzI z$#>=F`8;{~UVo68(;s%Z{oY{MmFw|)JZ_ho2uS*$$KOe8PL@|vGinreMFv>iXMp8t zeXzr4#4@Qq?jc&-;M7xfUPPRTsP3&dyNIZ~W^#3%KG+doQC7LY80HA*Bbj=L!>@R&**GDb{`WS+*%sFGMAz;>MWN<*DzC@#S2r|{vT}J10MVqHf zwM!KlX^)E5H#(Q_+C4#6e!vwB`+@;q*lrIwJl_0(&le83opz@$9Q3%HVW-y>^yIqT z_8`Zqfq>tg@3-^4^6YNYC}9MRerg9&RX{ZWH)xkg8yQrp{Nr@|YW?d(A62m`W3_%znMWgB?tidI+-913Vw9-SLA{6ANT+C=nU{#at?LjrZ#*d* zi^xb1I;C2@mTqIA=LiHjy$*VOxpto`>&2y-_3MG`v=C zqzxM}a%{=Pp{hEgnG$X^gAvw88=k3cTyezwZSq25A!|;I_2vLI21LF5 zd{os&5VEjbY-|i0VRImjM5}VEe$JY!yhg1oO#_EV^>;F^zOYtd1);BX8)~Q~;Spm? zQ8vdf&p&izhd1&;*=JJ2Zdt{Of0#O}>aa`yK6{cz9TU$i(Re(Iim%1BxjaUpJ; z_?X>%E>FOl>tcD5o5$elak=vC;h-8vJuW-Lsw3a&vpX1!18QU0o=0o;`&>@5NXE); zT=BSQp@=&5Sn1+9z5bily^3JuYH;wbj69@`S`$it+pPJg?7YHFvEBKekl(HD+xT2= zU)aGy&K|ahLjfk1fZG!ea`VO)^4sZ=YVnuv#u)Gga$Nyt{z&W7eOey{Sgg;bOEE0V zbW;TtMNGH zBF>z7&2Bk<{`?wFs?ApH)>ZVevGKD3{w9n!oELKVT&`TZ*Prk9u-JEn>3g|$w0YH%=X=ahUt}%0o|OXw-4z3UCqEaKvje zh^lCsVD#pVYJl$gM;fQ)^NR(XMNXNV?~hc9?o%l|v9M`v=&6J%s%q+610uZ=8Yhdi z{`y?B!W@A{FCUdV1H$eN_?=FFZoWSd2nEAIzM{t!_JqA5AH6s1cY8x_+P@cb)$Xz9 zaq%oKFW>G8=J|Zie20sKqs@ua$VJ8Vw*lS61?OYNn^)Zs1pQu@H_y&w?qq!Mhx57C z=Cix<@?5@Lwn+T>p|Hp6aQWF*;sDCeu@WPHD2ODK>&o@(xi!}^i5MCX9*X97c!D9j z+Z*=fxxLtPL66_d7FM3RJLm8R^W6F2fS(DKOLWW?OdfemB=&IF;|!5VeYatdYr5A& zy#5IyTTqpydP(XHGUCTpmDW|14W+h?n_UHJ(_JmM_1)n-HV(@xYKDqDw|bj!Zjd?% zPzTIu?w~!-<)C7`4mNr0OluydI=9=wn3CtkQCcE=;c&C9^Tmz4lynV?IEYfndfnmo;N61fsm9tL}k!rb932&%60iY zA+qy(7}xXcc8@bZ-xcss99J-y=M6gYg1LF_fG3}73<^Gf!0z+d{j6}93Bq}9yF0}9 z^V=B&h{U|k^ZMAf3c0*|MyFHd;P*M~USG)R&kKsGd8g};D@KHbi}}Nzk9DKU5O$#G z#zYKx{Zx_H9?J8$!<1BwB_5|U*Ar%7b#n6ParuId{BSrd+~GVn)ZE^DUYJ3cmYVO* za|fM1S0FbWu;+%UaEFhE=gJM_yV=ZS&(iMmF)KN-sPgm0m@`L?)O)0U8B2}3QzjJ% z^Gb!W=|Sh#Pjm{#G>Ks6xA}{wwO z>oT7TeRPOUucDwv3C-z#JSMc-MlLEe2!3I$I>9a!xz+R8T`toZdk+RtdS)I)rKOX_ z6uriZ8c5Y90oqD5a%9n>MbS6}a6LOQrg+i3(qb{LY*{UqX4U-KY_(gTZ5-_B^&g?k zNJSAEEG>;N{a7Kw)j~Z&YwKzBZpZj)y>~`5R3;H>eo|xbjtXldObyB*V^b_5DT?f( zn8E6*DpR?rM!!5H_!8=vUmuE4)W**~biC+B^ zxD`-tP_u&GOlAtP~m`6E|wo5oc}wNyz9Y9fzBnqA}!#5j2=GehQ`fO>(kqhG(3 zR+FuNwMe(1ciEP~KlUkETZ^+EbGeYJ=>^q3);!a<~HnQgpNmDD|NWbw$ z62;KJK@+KQBe7t<+Gyr}N@SU!=K8OR@uqzxBC%|u43IC_q1e2VA>DkD-2_ZI4RRO_qH*kO$n$Jn#}A5IGXtN!1&M~p}Q zk9#YThszg~*ETA!`D2axFRZ>`^9Kw@i)gTtH2Z_TC)H5i^fF+y4V$BFgLS-_NQ2`N zBcp~pj*UmfMlq2O+er4`_PO=1+w28{#u@1^R$n#E9S54lXMi*mjX{ee!{{EPkz4aS zZRX=fPW6v8w1}Hq|2OQN&F-!k3R5bZIcqYa%qh_*wQlo@nrdZeaAPCPmh<@eQ)(l@ zX;=kDu*A5zz@-J^1~IQFt5i+bq0*iI)Ga`+c;bV&{xjI{FoX8S2cxXjYk=pU%S2R&! z)b2+CvsWz+BB_|QM3AS9`6?1+SfvDNbb}M3SEbSE2<5VBhz3YC@)(w#8rh5%xKJ@= zM~aAz+lZay(+KavQhu^{GhX>%=r8fTEK0L$d!EFV`2IHT4Ui; zFn?TGSt+`?Rm-D%O69x;p~iDFwNjSPBj$oE8XjM1)T3ed=mrT#s?0pxy($&OrKLsn zU&xeJmo4I|R5KCdxm{*{PE)yn-@=y9Z*V8hj3_Gko36@ET?#W*A{jLljjwDNDynFp zt)4cAnIK!!4F6l_imyTBLrOOO2`aZ!VWO*JAg>I0`ZP zcJy*h^cM=cT=k&(A1&zpzlmr8YerIQs?|g^rLxBSM;OHEI##>ERLS zl@abC_lUf7O;4w08HQzPhGrP%Y1oHlXqshMhGkfmeOi`%+^2n5=D*qRJKs5W`CU9B zQd!-#u%*gyvtxI&W9Mea?8gdG+jNYpVHS}rD(s8!Xl1u)BFdwkxK7sGg^P;S%!*eL zGDed0Jy&OQ+^9&7vJ>HM(U(T&*8=5G}psBRsaLsZA@z1ZOnNTvkTpjI4+0bn8X|xr$Dhp$_|efa~L^c{hS_L z9?_>7yNein8I!-j^_w&0^~F+tQ1->6(Vi9pmrLO`#gUpO?aMlx_5seypFowO+U=Pu z(9T{vLn1)HUAq*dx#|IfLfe&GY7Qp*fH%<@*tFhm^jPTFq0Z6%Si6La0=o31*ft28qHJsjc74N~$ z&K0;O1BDz;Y6Bs6{C+3Jc3OWD_NrYHU)I*EZXLL#(5Y99sSCeR5vOi?O{u!+PGh9aJOidHBC?+$TXcCjOk3+pWGhqwe{Ka?Dbcd zWkK%L{ye(9)t|;vPBo{xm*2s{JZfkN{k32nitW~)N2Iqut+b|s_w%H=JNsSIkS%cv)dG%BC_2ra3hM#EJ<}B ztBC(VqLw^~Lj8@=#t;p5I|&M4-tD~g(e3A$H_>_$%r&f&UES$c!yf1~s+$Icq~eAL zNNg5CV<^(#$%9Vqt(-&I5;X_=emLu`$#(K^a@wJ;X)TAdR#Q*4vajQIL$W&_k#NaZ ze&?c_seP}oo5M5#<5N9z<`zm~*-)07(}&Xof3~PI=ZWuk+YQ^zx-6yS(5(Ud1=!Vv zS3UwmnV`s=UieIz$VOtNyPYY8z~NjW;|}MjM5N(tMuoe*Ff74~ADsn)5R*g+YRiKe zaSb#-qc)hU)=9g~we}!}y@CghJulShngoJXw-Oqor@Mp6ZL!02?4D{2O?G<{e93SU z(QO+BU%TNpK3JQJtnzGG!4yd%*VTqJI^R@iO{DATorp}J)+f}PS47H1_`96F5_?Aa-j7vl?^23i66>PFV8?Omkfqd z#ry|S)jt#b2Xf>K=slypx;T1Lx^Z{*#^w=ubx%o0r=ApFj_zDa=_yRogE@l)7{{IA zzI(Wu?G6@kjjDdmW-zxrc-*cC+dtKQzPi;Je_|Smc=crB%%3SEHc6pduB%dfuBWZk zPEVTe--?3wWP=wRP9M5sg-VRt%>>d-bh@!7$L12p_mC8&Uvz757C)4{iHnLfhYY3r z4(`?qxdQKr@j6^*^)SP|@U4@hBN)f+Dk$8FIqKegymq21Or5IJmK#X76Z3A5?tq>> zHnoM zG+hFe(Wap<+2(c^W3(p|;ml6wfxe6b3H4U#7StetM{jq2cR=oXG@xb;bQOBKrLK8 zbJCV7;@)_1>ZBr&r%D*-lrmNcvjhAZP*jYakbALA{Q*%3IR+bL#g{9aqX>}MV>{z+ z@rfmDZ!J!i$ncZ*-`m+)DQx>>3q-jf_$2+-Z5o6v!v3ZR?k_>IfdMc<{8YJmW(OTk z>BmuzqZusBtNT_la$TEFVT|>-3ofe!=sB=#j#MyJmvs@p|V|_ z#ix(qApxAU$OxUS%LJh8h36TqB9YBoG4@4*qeYKQhP0qxUrD)cf=XCKJDYF3Sk$lt zn%P9NL;YfdwpWW|&3LXK(JUoc>du^H+*IxRC%>RVe#teby22M#EW<6R>~QO2TAFox zD7Hu_MqVZD?BbOcd)Z7}+C<1GmlHpj;lZ3L4a8=#jEqt!B6jHgA?2c17`BQ%H3oC` zeT(0kE&<&!H0Oh24KY70x4kjeV0w6r%2W$7i#VBpHsgG^mpLE-E+#V&dZ>rTIA%T` zGdtc2Wg^_hG+~MwN3o~W6o;9n4UmCYyBskDEM6Q@ITDp1UODg1JXOqM*HbEn7%D#d(i@K14^ zYk`Lmpa?_G11C`STCaWauqj3V6zzH*=C)WwzAzT1xy5I6VQjmy4rf_QPx6PXWBUfAop%^f@>f(19V&5jNsN(;m_atO8&WAr<8NFT2S zzcNIG7xN0+e{&DU7Evkwe!8HK90FT2y6tI0(-Fm_7jsD=sa1e8pqt86v|v?%G7$Rc z*6|Y*GD+Sl;ik1ZEMPYoE#yK_$Lh*fx0g1V((!-9EPQ1&$ovIe7I79)D+7>vAIsN7Q0YuTUh zRZW*#xSDk;>?)H7!>Q)-xJ6wM?_tg+-SQN1)}_lhoW)eacoY(zVITX;noFsdffBYbAtd>ukEoK{bk8jb-A^mk30c7)POSD`w^ zu*rq1l${$Kgx^He1O)Z30hpMSm?t?lZ#T(zZA#E=U>_$)s)hKHpvqT)Qbya%DJKSq zC-_q|hHM0o+AjK(Uzl2BhNI zw=UJu0-4b{<+p28789w?aV$-a9PqZ4W#d_>f1-rfg;GUCev6>o)dO?8OCaA)DGpU? ze3(kY-S6bkWMHU=^fNS#(vtmf$->0EZS$lw#)f8GH^?rQhW5P)hxQpRtH&CrBb&wi zT+6~qX|A2`^(->i6cU%Yo-CGBlI`U-N%X*Qd!HX(|Wfic{jVJw@cz%6(j`K z2t>~mK5vg9wc53Ub;IRAceztIM*do6(`hNV0LUCy?8|QrP0MB|^0@Dwh3zRT2Xgy}txCB{`&$K34{na{O~ZRYi;+FbJPJ~KR0pw&>fnu9LbFbjF=RqJ4KL%H2Gg-F<%O~uf6}}Qxp}OvNYIq-1MGQx6 zb?T}>R$%$FQ}jB2^2F$-l}GngA$RKQmXMlISL)X{Z4#X{_FX#Gab zaZNYH{jkK(qId}V-nLTZZkYj!CaESL&#zU#g{n>@fJyp+MU%V=&l0?~V8g4Sz{ zWDfi75_4y99Szb~5XL256|*46hI={|-ke7WB1!~7!0)llXoI(lAtK|GOt4T9gj`j8 zRVK2d?TgYlMS`$2+Pem_ZfA&w)P9oyv?@QCsGOD$+Pp8ulH=exPjegrn`;B5i4CLC zWiuavN#i|F!ffTxUwjbIqQtf`np1XGxMNk~%XK@F0Q>LL|65 z>y@3T76y04c`D^oi|f?Zvw3@7K@@*-Ak-q=t9lEyxK@Hg$W;s)Pt+4?@L1gycU>hW z;`74{Ja}Yq2$@M+j1@c46FNJIA~`Zb4|$ z~EM3%Z%2**Vn58bR0he6Dob__6E}MxC{KR*q;@MA?n-Fwrsr$U`0nXsv;Hl zrtC}!Csz1mLX8NAd}41Yqz>Yck^MNL3VgvWg&j&&;n%C$q;1dm}xNtV9SFoY`>f! zsGy)yAo!5p_5~)w^nfV~x3jW_Y-loSTtvAWiztLew@885=U!P?cGGttQ_q6Z8i0MO z07*`_WOHdaCgDpDYwKid+Tdc<5&~(w8;GzxYwaA>r`JB@VUu+gAJf3YQxZWuOlCX- zMn;;eGZEz+;K-E6k8=oODo>ybWdQVhj@rET!GjFv)5XgQVnx%jr-}3wG~atSlqtsk_@0Y)p+s|3R|$8NQZW*}jxNYG36Nyoc2n=BW6 z!_*1@fq>s^0}oHkEda5|d@0?F3=bmgGu;V{i-rxaci@K*WKB4fDQ zCOJ^);DLv{5Q#HZ%g^mkzA{}ZzH_eyC^m_5+&$VwL%wgkP}-vG!cX3R2d+z2`*n(w zgc&}=@-chJSh~Jzx2vEZlMe9fhE0ifm?|5Xu{b!VL>@9Rehbek#VuXS)R@J(`8)~a z!tO9$+a|7fW6lU|%3OJ|<;{(wNZH-Zk?VN$58qNQAUD8+>FrT~NGrltn2U0FINZ$_ z3UX5UrbvbsN8n}coAZJR7p>C$As6OL!G3m{f#9li6`f6HW@5FI)ts6_cj?rVV!-$l zFr5S15b2e+x(NmGq{?w^dhcX^pAvFW3PNoS$>gj&%FiXpIfVB8={U54&vkzqCfA(- za(%I+W{EAqEgMKqg7Afnwa_kb3rSApvii167$q{0^K4@0*cUoe4(CCpUFCe&7E`F! zpm6+_@Mte5WK;kLFTr`ys{~CwH}3R^&+VM--_NDe3@ov?568;~IceLZuMa4uK3bFs z@Wwg;C3!+8ud4`v93JFiAf$0QECX+ipWHL&nGABBI|FduzIa4MCxak)_G|))Kzk>7_Cge8Sc&TYIX*uot{B4A#}TNj?+z~ibRJBkjL@CU@pvreX`T?bt@?> z(m63Cg>YQiUKDAD-QYrF3G6uqQ_}~g6Hq%Vw-`2g8_b>619sGG=MGWc)STRnNoYP9 za|$(aKqAhPb_l4edILZ(d|xp-*}ca82BNmp-fvD*ifDRY4^II$Dc z$3+5H$as~p`&@ua)CYe=uHMRv85ke7U@#+amRnGIDa&-8TgqdB;yr(o6IebA`x)-e zP8=l`g}z&NYG<@JU(UJtTc=5rVFe#;L_Y0O16VIY$wU8B?V z$rSI<>X#E?#J1Vi4?Ca`=U5?DoX7(=R|YGDS`?6=V@6({LF4q%J{P1!aU{>VIPicU zwqfbHF=(eyZkfpx4!KvcpbLfo0<|755is)ruhOQ`eBzZs-6PbF@pqvuf?FsJGW2zK zRu}U102;+rauQ+_gvm%cIyaZAuCP z9TcDlsu0(!co(LT2}o%W^f)+CEP>Eg8AXn*8ggg{@yg7?x9u~uira*9_T#Fr35>SC z(!(|l{YaoDZ0q8&%n4!p{T#;Q1Lgze9S$SQ%Ixegmn z1>D)y3vTMog!&oir1yRSpT)WB5S~G&BZ}Zogy+P9;G{fs7`vD*HSbEmjd+BUcIb;@ZP9D-a?njGw>0N5g9v^+j8>f)|#&ZkL z{dHpKUw#gf%;+jYO-T1Mg1%+zLHbz2ul?Cs-Q+rJ4{)Mbee(lnj_C>8%fnUa@!58I z?eqfsQ+jdI82#C^6ZOg>j3@QdUWbyIsCqC8nkC5r&y_cqAIu)(xdprq(Kiw5{b;p5 zQ${Gf&bU6S`MR)^r)4baY-gY8O(E+utxA9LeV+eXRE)-gX%;L-IzEQljA6n+Smf1H zy#}Cf4Xk50TrPmvP6pvUCCkO$#5Qs4wkdAIa=wCh6*)^N^MZVrAZ*$;h!?1rFh_&c z!7_u9fQs=4m-|od!Ix{b+T80~M5BVU$z@y2pRC1Wb1&+z#Zf^Q)$k!4-qo{9Yq-e^ z^UonT*B*N0x6l~Ty4lGXuJ3~6L%&?XuEP&%;b40;!M$3L!~}xmab*ex1d1*)AeKJA z<=4RoW~Db;$7FaG$R@TAuB}9AyP@rsrUCO(hj3uPd`OZ_LzWQQU<(YMX32Y2BWiW4 zpzaHy)`s6iGygtD*xeN2nx@E<-!oyBar;GN0+)KdImAml3?@9}rB1?T5=C=f=!Q7; z>|{-L=3y%2clB}efaf7>_zZ4Q;NaX2{zIi#1=m|Sd5QrFvrvbjy8LeywRSNoxN`TY?fNL$!gU= zyU-APyD6;2E(vy%IDmnq9q0r5W&x{PXKmar{gw5n*iPye@X6>V?94lZX0x8Z?vL%E zLLg7m6zkiUR*CSb1ly1;#Y~{Em8B%MGLYo$45aQX?3B1uA={#GL&IFvN(YEX_FLIl zHu2-~jYy+>{V6QN#pzGw@PsZ+9e ztEc2jTzQ}&_60^jx=U4)*oFZC>P#$Xl+z3xRoNb}h-sZwHRxn6n0u?9fkYpA94T}P zqqTvOFe@(}ea2UC3dzrHp_lzwfGIS9W>-%59~}`&)e>Urd+&-@H$2CXn&yD*MHP)V zGk|wv&td$N^?2M&9b)eRVrpsWvu1T|)eA8PahbwnSx*aQZFxF)hMjnOR7YH8Sq~?> zhR4~IZVwayKj(J{*D)UJDl)m*#33{VdXR@uxHD^3OdJ@~`-(Qz1cK?U2b7X&sq@ov z9_rB+YBRKfk55{J{mDDsf!t6XAn1#^mUI$2^ulW?sD(rJjQ)7!M)+=O$$W)$CET;v zddE6dDQV2fo5hP(7T-!>G3MpNyI{slu&uaBTdj} zy+2+n@xUG%^EmYKm8N^wZlo96iDwGtLe=xtk)%Yy-TaJ2xSZ``VbnGjC(>!v8HLux zY1STuIYV}|UY1!8EmUW-#kW7Kcj?0(u`LLVa@nRqX!%VL#&0h8XBQ{3)=9Huf!En1 z)W+v)h!Lig4*#FW$|(mfDXUVs`BFylT(#9@M{_06-qR=nqm`Nd8+)~7#ZwP1oIbA`THRjXZ%j_$@5K{pQgAvh&xb zM~J6cGeYJLJm&bUbU#%Z%xo4urTUC0!kvr3Dx%|viG5-$0fSg4Ku7_p)M12nt*at* zaJbo6O=dGg8X6zWK?bkV8IP|3^D=b=vno^=j;qQnx^d;hRN8#&2cR|ci*d3d2%1eMaV@m)nv#H++ zX8WK91+#!|3l|D@FSCja3xOtODtDBZ8_nT9i*J~^of{Z$a|V~CsF_m>QjHbXaVC%; z-oj3KQh%hPCpg8|7AVVJDHmISYvYV=R0mjNv<%C(@eyBwPiIS>9+TZdQ@3lPU=zy+ z3T08+Tn`zS;hZ5YsBZOt0N+sVL3L0&(BjHe09N&@z?KP@2K>lcQP4A;U|V#;dfQxY z7ckysZ?0XT1*kFI#|%z7hcuwk83>XVQBN#r$Rglk3J1RHhevSlGKmbg+gY6+Cz7lv zRVE)ijrQERO39%>W$=c-84=ka9nKfrJe1U}dQ6{uztSyG1K-@8vvFTlI1iwO@ezMT zjz2s`aO=t8y~ROu?_>|=k&kh_YKUZH#bRUzIkq$ATd$UY0Zzw<6b1N$U5>x)X@&jh ztIMO}0i{#Ug?=BI~Js6%UW6dg+WNcA~WP9rRAMhgpRmsM|9d2T+BYC%6AwD67T zF>ZE1d{SZ1NBV(AZT!17KBfT4Kn!CeKJs3d3onL0JMH{$vKer^OQf9RY^%6v1eYySU?ddJx$S z4favs2s^H{M2bk|EN-kKwm90k0eS-dwtXMf-h`rJ0qdxlSm07eFBDp)AxYb;$XV4! zsK72r&G@CKY+B8>B8rP~c^1(8l+ z4w#tlkEKacmMmhRZ^=`|YZ~Jq8}8ktNrGUKTM$}tlt!+u5GNBOHwSxXBzQMLX*lkl z;sbbe4daak1+WbjkUEBGhCm?**WP`H1PJz50c{i&1Vi_IpknYFpk|{8R!#xkdxTOB zHK=(+A543O8_o0LJzh0p=$M_oGKC7yW+3bEIY?~520KZ(dkg!U?|ENa95Ph&*&!b- z0EUf+7l-g9O-tv-*Nw@sG0QG~*kvE9#=od!SQTMlr(RS*em3Ps%I5aM=0+@;nzg0o z3!Ly{IJ~zzE&!12wD*!SZW8+fQZbweLqV75U&PTuNX!K^i$fIIw+jhdwihqgU#p5C?arN+9HbA`4GvN?3z4K()f9(lW_}PSO;kq09va{O@W*0V(=<~@;bX2n;WKd|_ zEikrG3&OHOq;BCh9HrYJJS*XyV3#ObK!ix^Q^sLa%ZaAmLnv2P@+Qu9{`yK|BD+!@ zZ*Ckc?$J(A0ps)|v_vIh|5QGKbt*e5^`?e%8T;Tj;@)syB2H^aHtL!T?QsqeTA?<> z?uLoXICz1zg3AWPgUQZ}7mSL>+FO{|dzJ$@Q^kEM@+NK|Bt$Ds-2E;#ae^Yj z=finhk&wAl0hBrob%&P{3pht5J_8x4fZJXA^1vWXW6V!)BHO;RcvMt`X)I#u^foR{ zE^uePOkqc&>Y~6TFTfcHBwygWHd{J+oS^_`w=P7Lu!iiW3W4hJk_J3d%*Ia41epA$ z1ZA^>A*zPWviz_Z%Y2TplA}NtDZ%Vde~PN;PXj9TCmi)Qf{g_NV^2HK=HsDB3Z>U) zvP}x=&49Hkf6Jn4$pYcFr&Z0~vu2MLjS;&B+D8?Q_frIg!180r08)X=%a{4UE6qjuxFW@e7lu)4~ z4#6KEVYeY=UKHWI#zzBbpa-b&1Afvax1rj?Folm6;iD3K)hOp6t9;y-5FJ_RJ574` zD3^;mX~4=*^mcPk2B!?<*(?sy6Q9p1HeKZbK%G>_s54Pej`^F-u?KQA`Wq9MPJ40| zHfQ)!6w?t-&a=ha5z#~pGDCD*GzsDM;i;VJBbAUHl}uB0#Fe1##1;u~>65CYcMzX# z*~@GImr*;W+`=_uP|Ci|MtxHzN(;62qS;9}5z1mA$X9Fw+B{7lraS06jh4C`KeAu; z9@G$m*Rj1Hs3QZuS0h5upOhZGpuM6f0we*6E&vldfw1a+$vUk$+@8Z>fa>=!&NU6@ zg!w8AowM;im;^5nhp-Rvgy$KQ3@$4O?+J|wCbiW-hi0v@U`5kU zq4+p#Ar-fB*kCD4YHmPUjOZe)nhy_8?4kU;4rEuB<}Z_2n077;R8p`(Br;pjC1nez zeQPly2nIC8BPJMaf%X;$+m=+ZP`U|js>Lx~m6VYgE0WjIKbm58=LkGcy9VSl~rpO6}1P~m5VP`Y;!44{I@Ux1KTkP5i5#r3rDPx-GB17C! zyd7`b$pYeH4{-zvC=Wdwc}I;8k9e|YCARSk>cATwJa{2l#oIGcui0VPls*j7T$z65 zE#ezK&CX(vKaSLprVLRd4m9|xgE$7K|sZx$mb z$uYfGZ3)B#vS^EV#bCs9i~EGN48E3RcCrx_XMVY0X)x}$A z3_TpSjhTzOhYiC$%rjWa04F(cARd@nbgm!P!X9FdjcIw<*p`Py@;t))9}j#t z-USzvOFAEwhZ}}}ULGz#6jr=K3PZL%plMXVU_)dm6jAt6jP+9|6~q1UlvTH&6M0ax6Sf=& zit0uVZ0Ri=o_Msj3Of)#z{8|3Iy4_HQ|)me*FE-~8DSmH>F$HWl)=Z+`jV1ISXocW zF}&)M9XRMl5M_^Vwc^^6QC^nyyp|4!07^%Z$Yq>qL{Y#YXrzpyjWTK%DL{`t#yJ}H zGf&yLO!xG|X_A+{T{uO;&ON&-9Z_b0^A{30a|-P&44{J}d04^=a&@wtniB1h9g|5S z0!KA?W8!vn0Pf{*ARZl9=x;}w>e<+Q5KRY3q4S6}Xd4l?12-`3EZPy4mK*C)i z&5s1?PDFY$5Kkc%Y5Ke^%gwy};ut1F@@{D_*s3O@;Zs`n`8&!Uyy%<7WsHw6S(^qU zhpA;#yl`E?L0C+~?<-TKVezVhW87Qzkz#vRNq&2IAoZc9r(KAaRD4Zx`lza^GPgG$ zVb4gMYCg|FB2$h{QkJA2)CYk>-x!I}ZaxA#Z5H+y2h99%vMF59z5KP?W&&P`^I%9Q zIF*9UA>h?htO;1Zz8YZmS~8o6Xu$IuehE^X=OtKm%9o%RL)RD{siG?p%M+L&Vl^px zU0*^;@xy~Q1ei_HKIMp4W)mO}J%K2HL_~Yk_Xk7`bC|>HtZxdVpB?-40ly zSO=Uqv!%#UvuxLNlTN9fseRt7=-)(cv{YZE*PCFwYx1L&gCiN*I=%j;|ZJDWL1Kpnk?|~CwixPMLWkuBsPWhPZ42@XSR+e zS(l`i0(j9S_k9`RjNYN_SQjb`@hlX)J!m~+D<1!44vM`yJc$w0r9)UqSZUVSHk$1-kwiFBrn6t3r-B zxE106u9G?cdYscy#?76B)p|NVjO0PkD@Bt6 zz%BxTDHb8w)+@z0b?%6b)8xoQK5VDR0l-F#L*xL#q88gT7tD9Nx@j<8Jnlki^~&Xs zs~ARNmxRE0##f2Dv0o(Tqozk3&gmJhFdfsIPlE?Q_X4a1(gBl zC2=zY*s4qf8OHv#VKOGbsMbz6G`%PncD)9xmtkyD>P`hNO>{XkOhGx~N)-U44B=S~ z%zzSy3)BmtL(YFrhMx-(X|~Kg&QVXBhvzyulilpgQElTKKMFxA;pQZTIM0~;Kxz*X zdcFL#XpyB49-Wdx<(YBoKozq#j3L_aV5f~DOVx(M>oy!nzkSvVQfI%k7|?06IQ|U% zw;3hYnn3JB7mB)ipf`$-$fpWiiR;#p-wsH5I|HVeo>JLpHS8*tWrNjL2`o0401}ky zr)kx2H9=Ts2gv$5!1Ts>*%Eg{U7=4#2cEvUT{WFOyRqL=3fu2b!Hmj~!CM!5vx5VQ z95<(X$2eDCLQ%Q7c!XaF9{duZnXMHu4iOM6VojfdMQXuDamizKpGO@y zy53zJ;CKuY4NI5KMZH!gt0qcvD*jKT)U9j=M$C8>LD2 z_cvT|SG6c18<_yKlYI=zpxD%^-JLzeNv5nMwuL;LB(&nnk#({+f#bl~d(*gNLuorl zA^w%kur_zC#(Tz4SbTokupKV#61Kj`#DdxQLT9U@bZqK=z^oEzpEXQ@aU(Yk;jp)YgJTv#F_ktaMVed`=nkI-CZ9 zG`L4@zkZ5X98&N;E;9Hr3UMfWgILVO?*d@9vm&0Ha0?hF)y%}8Wn5yj?0<+49M~(> z7GNu*mHY?rfd|}CwlF_OjJHA^!lkYuTsGIQ)v?H$zAO}gB4>bhR$lMMy;V2>z_tqx zJxTGxl0ei!_G!Tq6Thikj$_V#w{!KjCIpvxAvlGSKFScaQlDJX6>w7u6XR=`&+GqmCRx z?;_hBNRK=U+J(wlfPp+kj8(^XR0=S<*aL;g{?!RwtIG;h&y+S|lOe)8+uMLs@5acZV;#FdI1lShj`kM_p#!Q`06-UI-9;phu@VQ(4-VZ5R@ zl`A%!IzY&Y?&*`=G%gl%!RGK53smLsz>g7ip|YG5g?$_aWS|G9oL{iXaKen4wp$g# z*syDD9X~m6tZjkP8=js;Cu|Duvij7t`gJ`;;;yb@cAjeOien3^75BpFm{RehP&GZ74`VB66j?#8rxOJc#uQ-+ z29;Q{l4}S)Acl$9qH{EE*#r*p7@i*>6B;Msbfp{8MH~fv6VW@0K07NKF^U{$uH}`Y zNW6@)9Rc~ZUW`NaNUqlGyufPjr0BRh(aL5 zCl}G1DDK(x#f23tCzZxY`iA$2ITW9Hf*bCW3Z@IVK(s_A=Wgfs0zgoq8yCzN@WmCSfJ6;Bx1 zSZ4lP)Rlx(7?=0}rKIWz^J}<*QU#~J5;Vb#*Y==A1+ljRDoB!6Au)@b@M8M`h)a0d zCAgUrOy^IaqNfSA%41D1`#dNTj9!^+d3%BDC^j1VUFYOs8QLBm1`NC^-8;Gom^RZK zEd?k@_m%L^Fqqjoa~6tI*;m7IjM>+py#M{3o#Xqnq%Ra28!jN=_BVvRCo8072u>20Qhx7(=_tA?r+6|>o^+Gs(T zZF@F(u;OPA6mUtUeN4$b+Y7KyFavnYD(^`hg=qq*M&!utw->n*<*$o((|%@Iz94W@&3r> z(qZuqNdky|dMZVv$#E6IF7h}}=sjpBdmhHtUM?Pc#1u($jULON^5929F*27g%OhTN(d^A!aN}>vx-H?xMD4MsX;%-jFR#iODy5hP$ ztFU}H6fSIl$@;7myq#^&narp7ny^3_dlYx{@`#z=MbC-h5_qT=Ic8YMgdG8i>cGFR zPNq3i8E1Od)^eUU4DmJyOHgZqxkY2;?ffLey$RNbc)F|TLJiu%aP_)ZZU@pG-vP>H zDgf*C*MW*0dKpLnyA`6Kk4BL^YdUDNS+(;Cd$mQgZkjwyL=Gz6m07Z%{6guFLJ9=jq#>WNKwRe5?BKO5tA@)E zVXgcYKK6DTAnV~i?oNC5h5r(L+`OZiO%aogafKXDgi@#DV@r&ob!hod_y*29-}@df zyruZ}+i|5DhC&vXnn)3R<&N138+lc5I_HOQ6-JL0;8Hbvv2lJmI_J}DLURkZr znim(+ZT&{}jn-nlBaxFfn8p#=ks)_* zAcLIMB75olu4$lH&DR$&`6swjh3`jA4{-LUM7h)3E*P7-1Z-s*TOCT9^2<-tZW;6F z=PGy|99ob&7ok25;|l~2W4w}_?ye|w5`b5)@g8oG%`2fih|4LtxXl~52pZ_5*^~{W zO?vWM8*061A&$`tkot@!K@9MGN8#scl()!vLm2zG0I?Nl@fayKCxobdUY>49RDCe+<()$5>uz*y)W!RIGnAk)^ z<2WawJ;~JNbXdbOkU)612D4?|OMop0*a^p~p#@X#Zs7}Tc5;@NKniAjp3OEp&j4kt zQpjIi+*xUqIGM#m929+5$9P%_Aqhfp9IKY#J@LSIsROo7?CGyYI18aTY%_Xe0XpIM zalBb{ZMKgWeVbkVV0=*I;(Kw0*BR#1o;_Qc{VY}JZt4!@!>9>@N9yo}InW~dt*`un zyNID3-)K;2arVd@=d=Qvj%;)K310h6c5I+3>avf{uJ|4kbc&OM6U`@%)9nKbZaw!1 zzw06}5r4G$aB;ZUCv{gLaFb&Mse*Zs+3;eJsgPtZawEpi!6U}fIFi5*=L(QDei$$_ z&=3H{|JL4>z6^n|x7wn*!93rvX^!H;-f$>tDfj zCGxm$IV8TO912kU-#DgMKc1tr-6;ZK$l^f`F4W2*wFPqHS__&Ed%U3gN_#9>X;><9 zxU0Pxxhw&2DOnh1`z169h zIEw4qm?RSyjX<4IsVNCWbRgO!04KZxFb6KmjqL%$lx+tRbsGfclIxkIC&W@2HlHHu zAl01&VtNHQ42SW&Qp}9oi&MIc3p(jQRJAxq%EroDpoPAp78Tuq1RP=EXr))U5LLri zYYo9Qd-GmQY6#G?&cO)=?6pD9Vo$i{lWeJ%E6W3!-LkVbhW$tTYVS`7p( zs{zSdSmON(UN@S1G!15zA0@f|7~*Jps-%tuEz!dzO4Cq3(z?J!@^cT#y~iWz)HJR) z2R-xzu%z~-S&Y-}qthiYU!^s^@0B6}hNN`ByX#=Lnc=oxKulfAQF@wzz}WZdy$ns& zpKU??o!TFV-htf{XFm(MtlD|4r;O)|`IAHNX3{l>*(`cE`ry$Lo6{E+JT0WXsHU)B zdk(I8d`ike@%aXi?mRv`@E}hSW6Vsgp!6e1GiK>&$m` zV62Yj%R3u}b?z(X=@Wec-D5ULTu$R9BnJ_(H){}_!^<4Du%>4hwZNR@F-?1p&r=Fl z){MZOb75ozCJqjZHhEmL!NH8e_Mlnza06QqbNm7g=LdQKnmMQTXpiI)uC`Fc zU}<6*R;=yZtdGGsDxp1(pL`@zfqOV%O-4&jw+hfZM{qUr_;rVu= zIZ+}BBb^l0kkjwX_U|7z_@oK`V$yUU((XS}_U7Y!v&jrC9?|TkFvtO2F@p|~A`E?= z%S^^+&CL1C1&k)VYs!tTO?+SBxWof5D{4Ggdi|OKD*BZ7-R0}RdwQlB7vj#OO$UEx zdR$UpaiO}Q1$EKSWX1>!6C$5Y~4Jsu_L@?g$>d*L8ELGv(r>mlq74-thtXo%oppabn4 zk)mz`i9eh^=7G@m0(%HF%dWZLKFAoSiU6RL>6yS`#0<6^JmmQ-rlF5#tNb5oQy1HN6DHh3kOrOyy@}cs`#jr)t&pT^J4F z4Q{Hh&@UotHDWaNB4*2j zpmiRS_T!OYZyu$~I}7#@w4_(Mi#Z5esI-MiI%&R;(G1Pcf2JX3^=s;axX>T__!N5| zXCLQl6gNlCSps)P&RGI9sCoh0&N|Xr&H4IU4|iQpO15>*4zIzPXGR9@%wKOe1`y&I z8U|84Ln$D}Gn8739M4c+3qek)p+u4@E5APqd5zEG>~79u;ePV|yF2mhFZ@{E%aIMF35YBryi}!w0q=GPs$ZOY?VOGMTKUsv%Ei@3CsL zLF1KU$T-k5^xBp^!)tR)XGg_2twqdiA~}9Tev!(0FBM|t^l`}*N~AKg2is(=EfA`L zrJ4sdDph-93bvaLkrj7r@l!aBW@10=0hOFQN7+DIKHs`UY3dC5SYOJ!V=@>^8AdM| zf}W30Gs7M&9JP?4q=g`4YKoPmP=qbH5)G7P8y7($b2``&zyt`o@S%g=Uz{Ak8PeV6EX6F)jlPlxvT!(N2W_e>Fyd_4-G_J&$-ojGEf`;iv`DI3 zg^Mv$z-G-V2{vJ|bXUz{I5S6%5l%Movu*l@4M8CYiVEP`_ykw3wsEfu8VNN+Y|krC z6x%ZxcLY8%bprLs{^9_i5nKkN&V<*y@L6AaWGw)+-3T|}#}$;@!^{H}lzHuSoYDLU zMVYCH&J39Hpv5Ya$C=KjPpz~CNM0@ATZ39j^SS+HW(&yQmd}0gBsry}2Luda{ekuU z0QjLilPU&;wtVi#ps?xS7TlS$VSBsAW(DYW9371?Y|;3{AkUsXk+;-RcmaPBmEmu) z@Ujw)L@tn#gU{|(kQ;FS=hgbCBY1NKoR&EZJ$X);KFw_%4)*6QMLkjPe2R~3csDT` z9!eLsQk#Q`rxVkx$_EddkM4{g0flhv3VVyuIRX+nTeE?8cY*Nsk^wC{=0zK?8Dzlb zJcoD2Dc@P@H>WhTaE82G1~X2uM~vKFJTg-i&JlfcGK5tA@czLbJl-rP>|Y=>h!D+E z1o!zKHTJ{;-U@ld16|J1_5#{9Zqz-tlH`Gpt(Zb*=qKhvc~6dZc}!z9ke={PNW|j} z;1ULMP(~lpq;cj}{Q%Ay12&phoeHn;9=1Y&$LUsb9Z(8sCEWfMp>woC>@^B05?xLJ zk*}O{tDQ3ekTZn?rB!Yp3t}u*QI|ZRI#cY$hZSu+{Q7Z(-H60h8S|`MIcAnmm`#Fc zUk)@P2%@atrNPq`3!G!`%^v&P<%#$^bP^XG_+7buxTq+zUJ>^QrolsQ#qijXSDcKi zcub5f;c`TCcYY6lIc9wT4kGxKCytbOl4GX0-7^K;-nqoDxix=0UE-V^PqjUPhSI*WbNQj zuTm4x%7-@=w|G$D%P{prD8me8CE2$qeZ@#;ao<>+29tihw|H#P2r;Dywg6W#{3gdJAUf4ym^~w0y7UK0l$ZmI3{1^Ol{I< z1Pf4nMz8?s)Vcr!*DFAY)fF(dnLqbeBy2B=NYX$Hn9+MqegRXy?fBeYk+QR!XUDGU zigr+kwL}P_1!7Kh5zx}|sFj7B4eb5i$qHyQ#!J|ELDvpd{gqrD zLQNyaM(FQydwTrd4}_B>n{>S3lNzFsYcuTpa$RwKhbKP1RBGMH?kU^11sv~2>2BTJ~SI;O;$%bquicR(l!-~l^Vaw&+UunqF={Hv1% z9y~};js@hVfK2!>OG;mz+@tK0;?|i8V)dqU?WGizTfP-aup8j>e0O-IS5*_7BrHXU zDk%bYLG%hw&X|8$0#jo*X(2%%KfnSi@AH@g$o5F*9!=S#iKS@bl(k}iiUB(n=Zq)Y zV}cG;N)-_!AqBZiv*3C16N#!aM={2aOxVph@)-J#OtUo%lNwv!zGi`Zd`$#DH~-=Z z{)eBPzxa!W|9kO4Gi{zU&Bbf@cL(wBHJ6+3HO&k5_p@tAJ!obPK2hHsBfM-bA$-!T z5WCde1LRUO#y{L5tOTdrpFWbbo(@_^pk=EYj=EBPlt+f-R zHBbND20Fcq#K^#ou$gJ|7=JG{dnk5-N;ovm+NCBp#Cc`sIhUN-JuAmq-!te89lM{> zIg9L2Iov2)4hJ*-66>`NzPx9hWx3|1Iba9)uX$ZPntD5&_~x|{`VsAN5t`TEYTB?1 zFWyFdw=wXquiAV(vyx{Crtbarq%%R= zj@r#jDfGe@K)(wwyxDx&T)2oqUA)vZ-})GRRuA9wX+WNL{un(blFu=wm>$BoeYhh7 z1DW;?woUW)r>N(^@SwYo%TioKw7s+Kn=59GxTei<3pG7%NFR>8XZQ?Z>cu6L<8~8` z;Fh_BoR6{GQ~XD~<$_(f&h;V&axVmu)Uy@u5YYS zU5@KSGQ++I{aB}n%%UMJoY!@4ou!BMt`*|e?Utmg`Ng{amUW%Gx1X)6`5thMqg^@q zJjX0t@1HZy7U%AfVF}kb@FZrxY_r+wuZ8oanLk@iXP?WG1D$&=KY7-bFI%npx)x9G zVHOtH38YT*^RLh|<7wS))!%dMPVQ)u@vpUD-d(qcb}>Hg zp>y~2?z3XkHE(g-X15lCn(_0hbrZCE50rr-Q8gP^`}geWJ3qK9xX8=Dk6IG9*qCrJhn0qfkde zd4j)PCe-aDP8rc7GS4~Mkl@J4W9_@eQZ*a5k;>L6j+YY>6G0In*B>M|FLrZlBlV2i zLxNwMIfnEVQ{QeaPv17=TzqF;mH^l)9?XG#Nf4^sR&j`ci`R`6scKY#lShW$pMYltY2x7u* zO&yOrfPbsbycVUBD1FjDn28Ce_!0qX(_R|dUlK@yG920Tg{m97j7nA2k1yD&>ZRH| zM=8cAD7A&V>m4zaOI0h}Roy?mg4$S9o)PXSA{E71p(M2;&3mh77yrCzHb>}DNgvqT znawNP=1iPc-=CxYw9g;(&oSE%(Qs(~&et-~?NNVQ&ucwNcMgqYv=oe||9S)DFi@q9 z2AKqM+JnZNU;?@XzOSo6WBRvBwfvv87msvlVM|YQ3273!pf*P+*5ZcL$4Zp1>V5ie z+ZFe9v@}PwC!yOB;{74ILQ$!U=OSPvjwk3ituf?YWv4F8`d9lbn8nG@1}Ea-y8bnX z?a_aPzH^UkVPB9qQpN47n~TQxz)cqHGN)BKZAy$-!FU*l(5<2IvhDhl(g~^*qkWNn^*_ge?lb4+fM>uxrfHP$b0Wz(sEQe%uGXSoaCaJtGMz5DFiEfMyAts+dq3~T z(zEU{3RohhsH-yDL{GW;G}RLC%iLX>$KCUX4b5|+b%80U`A64*8)3j|*IX;;<#%r+j&7Ct>AEtcoWzz=`j@mo=ElAbtxDH4T(^5+ZM&zI{^3x&+~1{IP$@WWGtY1G9T@1FKRh6>+t~qxf7)9Yf#1`tYqAxt+fvvpT zcpuZN$#7<>MESN%L012%5jBzG^|`rKNfaqFk*u?GWYxci?p^p6yUp{GN;|=M1z9I7 z;xD|7&D6Z`ht1nf^W{|#gsgCXY+H%quw)`mJ-2bTNeAnocF64=njA^9V%*NG0TcCl zwm&4r;s^a?g+weYnPKtBOtX3CQgai`i(`n*zs_2Zwm+*8P%}<~!x|{2^XQ9gq-L}k zrGzYD0TIhWy>G`zq94JoXg9}OP{x)Pm}#ml$baLu&CFYvXDSO)FtmDNq8cI zwiah^MS>4)#Bq=rTUv_dncDko3q8+gjHO_8+9=pE=TIk6^*An!hwD9izn`@QiXvL| zz$31}vF#9TP~`VYH|{ip=cn>M!$aKt&KC{-&80`sx-xtCqt&XjDB9n)~iqOgBRB(rDE6q zWtQafw%~$NmdeLD+U1m}vTp*VRJtY|AfIF#Q6`1UK4vAjlrFF-2hPxAOp@RvjfR^H z`$ma{P#yL(FJ~EZ$X1_o@IWf5!m|z36h^s_x;U;{mda=O?Srs-l}HbBZF&AM((km_ zEvHh-YF>&n&7nEbc*_}KKS}svM)DZZ-g~#PQk1cD;zu@^)3b3U=EC~$U6m>GFc0OZF$Mw~UA}H$N@gL}-ffRJVzQ6D!?C z1E9{gsqF7ynGe&}A;LEf7pu{hGI+}}$?Zo*#db=iPy~es())%&SrEnKY-+n?hTEisgQi}4b z$~}YW(JEWx#*O1C5n&PmO0zTt<(9XD_-vdu-aLOXUX3+I)cm;Lh*PdZkel#a5JAG~ z{TAmg6U9%_L%CHp1CBl%jc{DYaW%%A+SgZph4$F`0o^!X3MYZ)2O8@TXV z9Y>CDwIS3bE_}NWx(ydpIvO-D^6#I*iATS*aW}8zFj8JCV_dW5BHDWKE%;vl1<3LP z%oE9H^V9zM4q+f1oPO^4OR9kxzdAxKrJxnASj0uK2Wenzi+fd~lS;Vl`jxlguqvkb zk74Bb0~!y1)Ia+6n0lNmUj1{FFY0^o4;n(6Z;@SPHbdNn>-2AJ+wTofiDRq|$vvqT z7`vy?g`}189hJF;6tXVTkE-OiluqWB=}+UOG+#1ER;AvEQW6AMEAtXRNn4GX!|2Ig zmvuDX6^zt~44CPg#$vf|gQ+%NJJ)(J&$m^R=7MdwAmeb&L;?RDoLBt8fV`&(^gjD2Ux<~5Fot){)a(Z$~LVQlld zZfMX=Keh7=wF=8(A2hdaVIJvUK{rjxeiTbTz`44Pm5vbOJQ5pAvAxa{Sk}BcLM@~^ z!hz`Eq{vooaev88J?PwK?#x`Ez_bel$`c$EL35Q=Ks<95*&BR z`-Am)Mg4`VnVL0Q&pO8LjHNYy={9;rjBH-in3~@vP1fD2*hy(cUe^kIfE6MC+CAgU zb3AQTjg;u7?K*Nu?gh$1=*P=c$VgAWYMM_@AMsG#CDz@-7>}BN2JegJ6^{HN+T)>K z^PMYbl^!#@=mYa)jzgKY`J#4h9%_qP2I(JnsLackv8%e!81FxW(Yg7>H>tbbURCqm zUq{W&m*1q;?p%C=`I_24n~iTY)9u-4)s5q^(n0-vt2kWu^PZ6{!ykH`KmQqN)%{#& zB-j1TaJq{lqyop9jh*MYh96>|(ok2_|6K8}uD_W)&%SSgcEvr~qW{lze!6XZGfLRr zLmL=@41UIV9S>wv~nrr)HU_S_7%GX8b9(tPziTPSg8a7>4P zpDi{o{5hxz|IY0%_&dUT_V=Fsg)jXp^o0K&{`)7*4?OlRV(%iZG{t^o`FW}L&*9wE zT>M=e28G>VSR17j92Pa1$M?}1r7vnJrknkWvKmeJ^+A~QGg%kHsw&N)EWhq}CEHde zpdivGczv80?ViSV%c0vgdq-_2A0tsRE{|@`Q1{cn?jIN%5C5uf(m>zoAw|8c{FQE+ z%_e&j!de(!<#NcOQl#gddKm|Y7c@>+nm^jGiInR{tr&(fm`o5CfiO@14W$rP&*nl; zq~;X%j@rhM_1~!Gl3lc;R>3RUEO(6hb~fT9Ew<$~|3145tS4XgqhDRyG2}$PpCD3BVpbwgNgey7jk8uc}2s=2QQ z)NJ;bz@X;ozrTgah$xqg%iT3+wGXng3LIrfU)pAwGVQvV54m^&B_Ze00ecVzp-5|H zHEX#j8bqjo;*-umXpsmih@`VTiIfRUJ{3@cUpqoC z${R<`&->@3M37)*GxUnfAAWcg>$p*bqO0Zt%zl0ITh>)0{pm9u^ENcw7fw!Zvu&seM`!(a;0;}ww=89AGaFk$_6CfgM3!$f9=h?i*$A|VHvd4(w? zh{T!Z)yy<3XUnW0p=1nv*o0{!9>XDiL`o(L!kg=kXC#JYnYJ1e<)5u)VI>V`91C&Q zQ~|q!)aJ`%8*QHh;+^IsE+T6MGhTrJenpeB{0-i}2FR!XMFS-AmhyL&hcXJ6Y={I` z=TDRt<@DSpwm}%XQj<;j2@uv1BLs^Ih{ zNd|Z~>nsBRd3pM8KgHCMCWqYLZ90yZ+We)Qi3tYLc7P#YSQ^Kc>J?%x%f&49t-ysR z#KDqUGFKd;wWmwdqebqYp6Gw(m!#OGs0Y-Z zs2|mmeV#yRwl$Ldr**@4zFj}tM(x8L4FO2_YAA%-tj*-M@jUw9O)xEO3LD0QRR>GL zFPUY_tkOG`DRU%N02QjgAh|m){@-&-M)O3 zw#mO!uDAPgksq+PVbYMZ1Ldq{wT&K=|4a1=&HfHx^1ciygb*HGwx2WMyoE<-pNpF0 zT`HfP@h2Et^HUP~wu&z&e(q_hVw!7tni37Q->|N~=qsHq6g5J}E~Qj7RC6NbFLNe2 z=kWae&TafY!uF7aLrmk5ZAd=Nw@PeCvwy4H%*3Sfah)v1hFv9(OS4!~9gOsovNo!$ zL$zg>s#9wGQH>qcefW65Ww;}d(Q)oRHu_4+7}U|Y`EQlnygF3-9qM1ybe6WhtZ^US zrtUZUon3@vdY}f@k@jEXUL<2B%O-v{Z+&XGB3HUv10q%U2z7^L47l|4Z#nt2+|XT^ zP4EPk6;cvlrw#$3HZD?6LF!U*nyKy*HHaV_3LzRiQC6>N5-rQA%}B;=Mehe~Fp`Bm zLv4~7$uX2?$P@qT20$g&^hwrzMjCeRbmtg$FEDxd!O*s5xPi{q`dHv*+ZBZqBmsbv zn%#IXv~RU(CUZ2OalAA&gdcY-OSuo)BMa@Bf(EJ52sM;bkJi5ayP=AeIjU$^P2$8Z z1R0>GDs571AS_vB*YbK3V@M{2j2v9>YYHRu(-GmLxDqbygfT^URbG!!?!Y_ZPcB1Y zqE^eoutChCCqQ@}$Q0Y2_)?@{%N!ef#Zjw|+#`>D)Lq4{O|nWwAbS94Cw*x3X8%KZ zuuz?p^$kn)q`s_753W3SvGHjl;chL{G}C>^LxL>U%Uws7LKZ*A=2IRqIT+J3VvO~Tk-C9Qq)MwxFQ51uB*C6c#p+o=@-bi`kgVYJ`6NX@5C>8HbW;TX&JlsFeUZxjR4>}Kj#?tu$0{yuOA0t*;0F)D2H z>*}xZUWA_OH_*b<|M4!WyNQdkZ&A6X6W08VQ*y#aFv&e@2SvD~_JB$L%yYqpC^H}X7{tzK&WF=qBM zcmWaOS2(i_7W{kg1phr(>`7^U56bo*-~v{zC!<4M%NJn8e+*^xkD;J{$ug^4<_L&H ztAZp9*)2-0Q}#xd5Fsy(=;c`_4pRj&I+dnQJ6Jjvz zF_~g_A%sh%z(gg2kL*jXZR1~w6V2DZbGBwf(5;QzC68W5txls?q*yXv4Rdx$hQwLP zq1<#5zBA?Le))ug!K)G}-Cr$|+2 z$bIVa$5cyyx&H3fJeu|n^^NS36RE%C)E@mx!;7Vh=QOy7%33(JBs8VVIQtu|=X#_9lnhIAa}zCbcj|s9k%jZB z(Gn%Nr=>Uy&sR-H^LmtsOXeE3lg)zZZ=k)l{u%9ZR<#1^0XYDBp*4z?;Vk753kRgm z$pt`bCV2lCh=o+>csYe@GcQemIB8w_PXnwVPVjIWEoyXaut>DNRjY{JO?MdtlbCq+L zTSi-7Zl_ImzOs~*v+or)drBL(&Xz zJGX1ztw&XE*>3-}2{{LIKlIGzLCnV2FFY^STGGRkQh(7B;DZi}<^**uJ5CDq_sv4T z-nP~L&1Rv($RYN_mCl%)Sx^7DD|7d=Jnl`3_1UAW*2wxFMIbvJrMos{ewks`jFG*3 zUTiTVUC9(T(05s4ezRF(+56_{|6T7yN;PNI>2gJ(wXtli<#9P`-60^qq1xOKp8uu{ z?fs$FgFH+(p=dNf1QU3c;&U-fHtfnk+4hZ9TAMabT@b&(Qilq$(t*aw=e za%Lr_Q6Qp!M2H@x2`EG)k`a` zeLW4V_UXT(K{gDLkzr_}%R#8BFsj7kKDnl|cMsh#$&jSsED~Y+$S6-ql8b&`E=Zt9 zEX8?bPV^0*P~M2BoeD)va1`2`MDIAbqFp6A@U*Bdm~IB;09t_pwgx z4=$GU5}nMF=7ytm9vW##-Wk%=SZDN#I8J?1qHHtiLn!V1_~&Pq*@&qs{lWY!%`HTL1A}pqP5v{NMu{H&=rO7ovCH)@qgd8m~X}AqS=9 zAS-Q!vJ_RdGy6@XG&)dC&68RZo$}nAM30LVNX0(o_DS4g@@j%tD%)Yg5t79)g9yn6 z)$LKaKe7LQ(SKybTFNtn({0#gy=Jkt2Q`J7)U`$C`NI7X#>Ryrj7C5$T{a?c^l?H= zsgbBn20_ON=($gxSC8{)F;|nW1>80|os~@>3t2XzPx@Ov-UcRb!ghO&qJi@Ut-b_Eox9Cwj)E}*$s*{MTwe1?db1y z7%STgCCbL-D4nrH{j!zf4$`v?C)2`UUZk8n=;rh8*YKNJ(djkiv)Yk)u#j0#2N7!2 zgYfEo&}a6V=bPBgQ}%_arJHPQuMkViEw>(#iASRC)osc%Q=#@Os|WfdYnC;!3BLZz zbsfnIUb4Z{o`q2Zba;@_p>HT;f82azarF@gsY2UzeEq&(FZqP=fs!8C8Y#|JYoRht z;-XVN%2FDo6A8LJdL|k~A|)cir(<{-GqA z7lER9jv(&)oK<2f{p`3>Z80U(g67L-k;l$l`wh-`+v`>01#ztQ3@kB~Jq}$Bv%UY# z>M6w>lEAJLxD2`2RHuv4n$Gqf56TkMZdg>mgll)TT2UiheE?XWB#>idx;eB-$uBs8 zWSc|1&FDwjroMR|Tw>k!9J-PayeoJK7p_Z2xJlcGU_kWVSKZEwE*BPC#i>l+Hs!pK zFysfHW3|q%q03Z4$NLKpt{WlLwr-!wC21j^;Hd%tiFrVs>n4P!Q(qk$PhRczLU>jdTieo*rHF+Lwg^eZZt&a;Y{FH3s zz_vNpxja7$&Q90+eyyf?Gq1wBxxMg4Nd^bj>kHbux4^#mjkzOdLlSNKzTi{DI$I^1QBCUohPop<;Am~;jl+Gy9BcUyQa)~a}5O^7)c?4vm3bfK26U;lDvi-U%hiFlR; zlLnuwATF2Ytmjno*T33Mw#$tCeKIz&Wt}b%#8_H(Fa*i9?au!H76!90;LdP%?AU4% zm**@#Ohyny`c5@S6P}ZZ^1I!2Ue*s*_U4FfN2G5)sqaxyu%osoL6cfhd270xDnYQ^ zM(3-MVnfNu^&Drd*R*fke`cGLTJIywBp()~kfx95NHElenEd|17IMTF4|;n1=sa@} zZz60YK9aRn_U0BwL^c+dt=R7yfKqei*NODhX!AxpZ+@pCN;Nm)q3iR1S)AX-dK0tD z|1MnI$90Z=v6;eSc!8O5^^NY{=;XmyBxr8p-NrE<$GVL-MugeMqoQ?%UwNUpMK8wY+f?dU8&{n75oOG7zEumK`f5&t?}Ctp z5r>;UkWo;A8)uIO@LIO;Qs3#zIm$J?q014hnU6;G)?urbWfY8}1woHqm8FGtAU;LL zc=?^Y=69($@I*}pHBy6H7%v&9m*~w=)$zLOyos_DR;h3{fU%Ye_L}k)qXdMx3JB)D z{Vg!QlGD6Kc=?^g=2fmzR=t~7cr{MGnZVlgX?m^#T>zU`>ltlcVsAAkvOY0xPv(A` zYFT`9vFO`ZUF_Vz>crJ03gG2o&(Noyquo+cUG;ya_OH4vZdQuGS=+4lMjsyc?vZhg zcRWo911!xTl8WnFp_F1cLLE-YR3I6KD_v6r?7}a*Bg{uTVfF|LSC_7pqW=DOYagmB z3$QXIvA0yb@(lO3u44Uv@yvv!dvM4f{-{FN7uYkQpv`?J4A}Z zTi$#FN&ABuM%I_QU(o{AiKx|509No?3Xw%37Y5 zk~zc^f;#$|VHoo46&c`iNfjiLNrMTUBP?LK-tzQ-%468Z%FXq5f|i@SYb6UL<;?{m z3yB)RG`dV_F>v#%#l+TP4=;0hW z6Q`A}$m6GtAe@f>eglb2=F11Fwyq{y&)D^B0cZ6kfeZhOoIekTof{$lG446OTMb!u zRjv*%_w;q}-%(m7a{NYh3*X%KNB{BJE6OjT+4=aAtE?Q{d|ZQAb!`huqk2b!tMI?{ zt-?^nd4|8)ig4eQXBebPbR!_TN}N7@b?l`dtVxQ0S-lPkyfBNcnu+G{R~%|4~`9$SoB{y|PMl(XTK`ri=DF_X2Yt|B27|qOw64_o`x(*-+Y1#i<}tT~S^4_qZL>U( zf>HO9Y#CX3)I!=NaezxvFB2^iVGAP7uKAc2WOy>8le`EvM$Pmh4Sy$Im!b$nAtvw0 z-1R)+ajYD*v9Ay^ExH1(=)DKg$H6f4m zX^tZ61f`76-iQBf7LNmxY$JHJl>KF$CYW7cM}lJO=6M?sj|QA1>lv(Drcgk=o_In3 zc79?zOH`@97onfb0<-#Br<9ZJ$9c9z6k=_(wxPqvigsq_>A&f!#X(oJd)e8{qO%r5 z1gOGz3Et^d)xJ|e@P+TzqirhwrydM2vNz&U!E0_!b2=e2C)TX!ircPC3^H% zx3OUKE6d`kD4r%)s7X2=N-_&FmWoL)f%miy^VmH#W=7+4o(; z{FX?R;cC%D5bJ_1{VgK1-u$OdCGyfd{ZC?j8Si51YD-LMUkUkJ9!GsAlwFjgeT(qyo>te_y3UP_qw9ri0*Yh8F$;)r>S1( z>*|U1SWcy@PycQmd2X}G)2jRCav>SrqA*hy^5Co<8h2^wB~Vg?*voy4{tU|JsFpTk z!)4VxKK`6(6frr^PPE@_DAZQ9vL6_!RQ+CBiGJ8n)f0-{1VD#x43ktzYu< zSNYACy=!@B$Wj^e)!6WtTCwC?DM4V}64pSAgfq@g&Yjdp_N zUP0?*n^Ye7Nr~px`O51&bPB!lfpHbV7&sQ5|^aAK z*NtXg`kdqCnH=}*`TM+OU%l|M=j`(q)|Y#g82{`e_PIW1+c95`-{&m7h1nvbb4mlZ zS}FKpw_A^!e8iJ8R*I+?|2fy}2Jo9Fq$qjL-5KtryJ?``iC0-Qu8C zg+0_&Bk9ek?X975dGybBCOz=Vg0oMt*qqvCun+ zE!S=<^};`D{t*sw|9SJz8_|jfm`ScKXO-AD!T9w7w{DxP0Uvn$ zv(v^VzL{5xrn7>x7Id$Fo!jNPFL*rvn3UqhpEmrjc_IG7*X_mMbr^q}7k<*bZ+{WK z_!ETQC-fp7h zOp>{@MV0li?9OFWcHPKzd8fY=9yzHb9)z);<6?q(9h3$({i3kXWVZ1P(>6fx%Mp{9 zXenYlaFMkX5^UporMW2musZ{xZcuFx=lPC9=hLt0F+qAjBk@&Cjt6-mhYty+p-<6WF}Mn;JawGpEZ>G<7FfgzygtVMd-|Od(Op zEIY_ywh?Ec9%m36+>22c9TB6gwjYsjQ;dQ0L^f>OQe!<+Rjp6|%eHf%zSG-Wu%waPvR$j5oa874WUkg% zXFUz_o$h*;Qr?fxrpO+b2af5T+`{5>7t#TRx&n{W%P-03t9GOp3h{_Z=fmV0tnn65 zB3~EWKn;>Q(tUszWs7WH{uDXsO$`M9ahH(gtyGVU{jd7`!}?xBY;}GkhgLT7vnP$( zyoI*nB)u7M)+35*^9&)R`pQkf)hhKDP^^KC&`;!R&KabBxc-7TJpH@=9apmWOb`1T z+QkCMywt}_HIdom8T_;jFHNDneD$mTo|aS6=Je}(R&x2?7JK+&)W}hk_e1l1k=^|3 z)4M96A{Q$Kfpx>!M#ExMc@^3DybA=U1lZh_%_t4k_j{~ z=Q(J;E7q$$E(F%*+Xr<@c8jYcjGgPjb|}1-cov6iZHI-h47ki>Uxe?9Oyzcqn)2## z9HpqudE?@*p|JI#-pAEE@h+d9o`Zyhdx`YFY$?sKi8oPip& z=Pf3qU%YIuZdOo2FYhI1Pyh3Ja!`-qA7b7mhZpKy4@3XdP?{>b)^UgqH&@FI*`8pj zCAk(VF)?|Wur^Qs`gGcL9!g#}%hf#>J%9BYVoIk=Xn)@5u{>WgoqMbgXs4eqxQ)+s}C@%K^p&v5SW z26AgRjPaEgQu6%$jSH_am36T_^P9W)XHE0cpLrjfx8Fbs-EY;GH)#)JzPB(sYM7*v z*%R@?%WoKjp95>Y{YGz|WQKt&LyDzCem0rX{=wz?t&|_yu8~r1O3tC- z`20o@q^@i&e{*5v1g|Gjy^zu|MlG|?9N^Ey-c3a)A6E$yVq#u#Mg111aciBL>y!Xo zM~s<5ql60X=+?+%Uf42$@x4Y($%$@3+y_kGjn?Z7^`f?Zky=!9!!^;{^JK%s!$RNb zeYI|8Iav6VbtvlEzZ!RoUG=_mFY~%ibZaedg`&E8aQWRjToeD83Cf;++a$IQ?A z=Bi3?cSx&W^CLq;>e26575W~@NpdlCeU+-}HJ9QP|F{BF{Wrp;jcoB%`NzU*l&Eo) zsofQCzun!q+}}v#n3w(Z-dyb_UU;u>Ga`?ZUv8UzBXqSVg*ut(+lXtw^;6@ng*X+# zjbc1!YeK5NYx(DCb#pB@`HdbiKi8{OD&Nc&?q$2=jlzx9W-Nv|!Ct=<*4X_8Gag>| z@!i*VoxyDklj9y}=EB(5P>a5W(kT3*MX^

(T}H@mdam_OzOC!_o9?4{p$!2lpS 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 DhO?6O literal 0 HcmV?d00001 diff --git a/output/PSOPNSenseAPI/PSOPNSenseAPI.dll b/output/PSOPNSenseAPI/PSOPNSenseAPI.dll new file mode 100644 index 0000000000000000000000000000000000000000..9bde645a06307bdba4031b6ea6170921ca1ab00a GIT binary patch literal 534016 zcmce92Y6h?74`b9v@5M-V@Z}|Y>Z8@8N8AKn_`=0y6GKk(~RlO4_0*V1Jg@rCJBVz zdkZDh&;qH{kOHANgqqMo5<*BI=0E4mySuY1nK=3W|0l8L%(-{&yVK{*oA-9>Sr=ro znM|eu|Gxe@lX(y!|C^@2-~4Aj#6w0uJS1~p^K;8RIB2WqmfPvTgZjoCUWyMW?S0so zefFL^H$HOAoc+d>j+#5>pt)l<-gd_^hsFEuH(~JL!iuTs?Ka6|wi=YpY&30-T{`n- zX8AEKgI3LCmTAgl@PGa{b!~t#0MkSa6^^{$l;`+||ILRSJpapPW}lYH4AuXuVH))+ znLkc#%4|yvyJxZx>i?VFlxYR`#gwMZPW7^HijS$dF0CfSrw95Ij@<9qBN3lAGR+tB zn)+>ONHTjusjsvT2>HgmGo#BQ4E~!2GZRYt9U4QCzGmRFY^~T&Z^Iy&;4&;%{5Ku{ zqD*6E+T&5?!Q&e9I;?=uy1N*(&V+kRW#U*j{?2KISujnC9uVxN&T^jXosXLt*r8~giA#oRpp4Sl}h zH*YUrvbPdQ0r{_*$P84Sa5v&xQUzQ!zJ>e?y;7dvLRrYsy&)1Nz)DDC~pO zTVkJ!wLU8v_*{_BE&Y9_Vs0M)hCWZ-u=DU5pQSLM&y#9=rrr|!+*9kbqJhsX@)EQJAm-nqtS>MgO)y|q3o8u;8Qp9lB%nTol2{2Tf_;@)vh zH9kvWK%bKupQ*RRK2NCiS<%4f!SZ=Xf1jzCo5#QL7@F{N_r?cY^<<6DQW((Zt7?3v z-V*z~3NKh)Z73S}JVZV()!%0-=H~Hl=<^?M>$$GRXDJNm^G|Dhrrr|!jHPYpv!a2| zOUY-fWa`^96?60WH}v_gtIvJ5#%C!E=<|m)K2vXreO|5BXGH^_hsx)+{ytMNH;;cq zpI3kC-`zDnOJP8t2S?#pN4+KXd19^4iUvNn$>;X|K2tF_kAFj-S37f!^J;vS!hk;a z*7!`lCH8qztt?t6bAJ9m>Qp{x5PfLQR}m!fzQL_b7y~_shFF`zoE}Z z?XvlfH9kvWK%cLw@tJx{?DLwnJ}Vmd+$o>C`uj}9+&umbeO~UTZ*EuPvlIsO`KL8L zQ*TLqc59_RGsN8>d~Coimzzv_`6%nAq#(wfoS1lR1M3isi?T1p>k{dHY$xQmi}^eh zH*flHnp+Q2w?6(HREXkf2y-)9@=?42(C`WQG4r8Tj7E=Ht-JImacF925-e^E?eVGP#EH=1B}R7#B5K5&;7=iU z-a*|nTZ+r&@(adbUnR53Qn~#3+w<;pv+fx$?NDsZ<-0D=LnYs}%4@m4O=%ix|Ivrd zs5r0>sfa;8AG%fxu0IpkADYX{uK|6CGT(hNEy^&l$ij(hqDfQa?yDQ4PT|D zbYV7gcv{zPE2Ky5^fctUE*)R`iju8C9nZ3F$c!A6$t=yZy)C8n8X((-vdDoh7IHC~ zPN6r~hk7iG_p$CtQEq(hgbf=q4P$$|$DObNz@pp}g-ot*2WX9*Pf5HZpi;Hy@7P>d zjFw!8cgCMqi!k$pyE__+$K~1@B=2Z!Yg8j^p0HxR$@QU?#=AhPDc+TTA*6v+XK-=t zrY3pVJyWUM8r_jd(Nj?@j`#*64c#+38rm9suGT;P>#x7|#=2H(4O8XEXGWr&HbQ}n zP(NXjbWiH=`HF&kjTe<>JBnvOuXXZmsGIi4%|v8y-WO+UzFFCrd_H9PzF82B^$h)q zcSBs2#=?f}ab^gLKw|)!V`ikvm#reJMld1j8Xsk24vRfeH(GqP zr)M=bsn@q?k zH7siES~Q5`pD&9cndMilmIcQ540aJQnrLLrR#P_;joc;S)*jkph+}?0TmbwZpNV_Kz4(>wd+p4*HR9nN$(WCd8vsX_p z*FB?9oYcn7(O6uOqQ+FzR@x5ZL*(Ybjvrgv8mZORxa`E2bK{i+?RVLUcgYL*IPr4M zF^!qS;lpV9&^57<^HNu-OiP-V+&&d^orT=!F_ErT7;&zqj;`G3;rnXX-s`2y$9CsN z53Rd_qgw4Jm||e*R^2u@Hx5e1*3<~ znVlQHU|IZKdckbYzt|r}dj4k)MKumZH4a6gLs5-G8mAA09hF1ued+QQ+_hlLB1rZc z^A-N0RYdMMbnilL45DA}#lM9~X#R|Us`${`E&jFQH$PDuUvvxOU0imcITyx17WoTK zs$TF@Slw&PoA}EM`exyK58}I2_Oh|^XowmTED##ho}vtvHJRm5p6wa8KWryEVzh71 zM9EGHJChw0mObqv%p&a>#T;_4HBFRRA2ycvHV&eVZBiTCD$FKmXRz6sZ0&7%vCDc} zjJZQ00N`CCj>xX zj`M^72n@=e5I~c{Xd+t}CA~lZni#S?ApioJo)7?m#_tIM5MzuG0D-3EH3WjIp}ly4 zKp;Tndw~E}o&Lr(~RSjh+h5Y7kz5GxxY00P~?+Yku+DVjh4nmtAc zfWQ`&PbvTc>o-pbfWYR8Cj>yOYJ>oY)r=4TfqQPBMBwPCv;)T7gwm+A3nBSxH>lLtD8U|5UgPWfk1$}BX3H;1Rbm9GLNIZqC_$Gu+2o-?sKu0!_%tKt6ULdUMeJ$ zkrczr6b6mSR0Mq(`U-_14LxIXC+v-ut0m@afJM0%xWqgj;<587iRaN~uCGi(=ot;s zJpPRn#N!k2r!))M?mLmu`S{aNDkI+LP6BRmC*yBEJ_Uc>srb_xBreT|UQ^#`pySi= zhx33Rv(Okg&YS@fXL5kTvKPB1-v=3UXH{%%hH^nWs@OVAX`*DV z!fX;NY)B4Q*qI!vuQ(F!}0lESiAa|Lgc zvB2i~@Dr`LVsjs*iIRO4W|RFCb|!Namc7`ucw3BlTh-}gZB|o5vJL|Hs&eI+hK|8& zoXv7QN4BEm+|>5BylqdrR&SdzcTS}|=_mRUZJbLR$E7xoSC~!aDeO#+Rao|7H`v=^ z%wZ)S)HAvvZCyZHrzlO7oT@OJoTjidIay)Zi`@`!i!pa$jV<(G+WIzaeOGCs2?U_Iu@M4+<|ZZ( z2n3s&Kp+sHe|tX#0>N|>2n2%7O(0-`YRB0Ea&(+6saEYb7`s{im$UruP&r4*ooTt> z?#tcNuEUo*WA4gIJ7B-TFi#s-(ZR)wnOv)|?8UCz+hWY!TCqj{Fm=(^ zZM1c_(nQHU3bV<*3LBF96m}-xS6KFHZn(F}n7gB5lYJ8NAZ^}Bo0lq0lw6kDxWwD= zv|HNSV9b4=Hqd~#vNj|DVrwG=Kx|`#0Elgk5U^$k!FDDP0I|Ih0w8uULIA{$MhJk| z$p`@uI~ySY0;>VvPXs{hYJ>oY8Ab>M4$d@zKp>c90s)xV%?JSyyBi?@Vhue0TBBdApl}OBLqO~Z-fAd1B?&=ai9?bAPzD@ z0BwtF7z{4@FDjkG6(O&eLS89_yhaLXJ%zNJE+lEOx{y2=F^%2=LB`Y!#SoUJKjJL&@d#GZIV-F@u+IpC_9#xtsc}!t8d0b&b@`S?9a4VD-F(Z&xn z!m=0hj2mq+=6+PM#XL^)ww|J`QA!gf%PPz!qZKwJV-$8K%P1^+H8;lFWX%1zVw3)1 zQ9%EGLYse5xkSka3bVAs!)`s5J&uQx&rHPVv6=sw76m}+W zD=d4lTi)AZ%so?M3ri6C^(<|*DKAmdt}vT)DC|szDlB_35Aw0x7<128Y_Z%n_O@Q2 ztrn$;l2(P;WQf9sWU#`{q@b|u)!d5SCS&fGH8wZ#Hr3vqQ=X#ad4<{J1%(aCiwZlF zXBC#cnj7nFG8VKq%r==#z0H^C=L+&8N>)^uO~xr~NX9B`Ojc6ZnJllc?A6^kZ<{gq z>l%Mi4lK{#(Dp+46D7Y_m`(nmupxOrbY4fK_6D2=Wm`#4Jup#+{!p`I;3d>&2jrTSgb8l8`vW~X&Hvd4I zT`K!1=~kFcmR8u13|H8gj8NE_bSf-+byxJZ8FOz}Y_kloNMRYgL))*&pD6jA!ff)I z!iMBkg`LUE3d>&2^>~|%x%VnIIfh}KL!0l@=2J=&B~Pa|e&lU<+Vy%HjJZG22Byp- zY?cuKQ8GdRM4u4?AdWObz$PXLjxvD&h@*`V0C9{F0w9hxLIA{ZMhJj7-UtB@^NbJ( zE?+i*Kp;p=AOI657$Fd7o@fFAXwEl60K`c~2!J@*2muhM7$E@SR3ijHoMwaoh|`S_ z2>dz21OkEJOcMye#92lNfH>O-0TACZLIA`$MhJj7*9ZX+=NTaY;(Q|n0)H+rfdDiY z7$E@SLL&q~eA@^C5Z^IE0K|8V5CCzJ5dt7CHbUUqbgf1oqS6Ii?a_8rx|EPst1Gzg zSxSK*CzqH&AP`(?0)aqqnF$2o)#XMAfVjd40T5RjArRQQ$^-(?yxIr>5Z4$X0ODFB z1VCJ8gaC-^jSvW~c7q870>OTK1OmZ5CJ+b&_nJT;5Zq@1fk5yB69~ZL z`;8C)@qiHmARaVA0K`K^2!MFl2mug}7$Fc`{)Z+I2n3IsKmaBlGeQ8w<3-2#o*41OkEJrzQ}9iJuuE0OIFH2!Qy75dwj& zXG|af&1a1e0P&m=0wA6@LI5rAMh-5NR$JZ+rXvts??n>`1cG0hK)?jmo4uE4zajY* z)iTw+uxK zr^3!;H-%*{cB^<>jJZEoY^?=b+j(1`($);6iISPAja|JBPrFsU4aVGGD>mrQ_TI+d zXk*9J#!d>e$<7M#yj5Y@i`{D87Gv)3HMVx}wmzq=NlFtXt1HYVYbfkYCMqm@v76{^ zG3LIg*xC$!?dWa&leP|3nkYF;VK$koupx;Rb|!}?EPFLK$=hVieOa-|yzb;}enp$> zDovEEr!bqWudp*&M`78E-Rj;JV{TE!7R!ETZ|iH?8m}}_QcP{E>}`13t>JAj<^~P2 zX8^RZi?y2!m=0h z@F4H&7<2iGE&4OV+iIe%ZIvcUwo7en<864FCk<(XF;}SApg%LcjTYM2BDJxl!fdjY z!p>xKg=H`1VMW?v%nh!wHOt!?LR%A*CQ4ROm`!kOLeKAe6_&l&P4%`Ib3-e(*q(Ot zw%TZGf2E0%0~BVH0~IzT2Py1K_ET8)YHn?BlQGv(vB|vd?rjdE&B;m=B~uh;lc@?j zleH9n(r7P0ODOE z1VFrJgaC;5jSv9wMid0T7=UArKhH zuxLL zfk5zu2?PScKTRM2kN;(a0EjP*5WxL~&KFQUD&5C@@^HVDR;XfHf&1gZ8w=m>W~E#d&MCx3wH?U9L1y za)rWda;3tCJOHW_m(RBYl(nZ3Qu6>0M{rHPW$6=st&6m}-3DlB`k z+rZmm%#EwqqJML|t(9o&BBhCvixp;*?YN-d{<%FtMOd{=9w|Ka>eFau(^-7 zIi5C8Qkp0^Ikkar!3Y5mXg*#;00f$jCj>yOW`qC;v=Xl& z00OV}dO`ri>P86Q^xZC;66sV_dYX`Ybx>Zz)CGe4tZ4#)K(Lkx1OmZi69@!?DJBpI z1XE2Q5D3;bfq)6B1OGaZW8hzxYL&BF`}_Wff+!^SsGmj2y=lL@+xI(9yG?z+W6UAn z!I{s`SFARh-pR$fIt?!h5!h-?+F1A8yO)00v>w}0c&s&z-uoM0I{hN z0wCb1*AM_P-3S2?n;RhjVhbY#f~#$50)arVl?em_!PX`a2n5@hKp+roYXX5lu$>76 z;O6#52!PnZ2muf~8X*wa+Q|e0(A?Pw0T8;VLNykW$CM^Y9#3sN>TP)1?ci-N=1_})jXrM!by`S%nc8?M zweh02;c2&{x51d(oi@;vcC!{E00IKvM+HFaVT1sPJ&h2sMh3xNCJ+EI+Xw*=dmAAD zVvZ34Aoejr0K~pV2!Pnn2muiL8zB(*bASm10>ObM5P*q;j1T~Eun__v4lzOi#GytA zfH=$u0T6SI5C9PyApqiVBLqMkVT1sPk`V$y7Wzye0L>$f5CCzM5dt8NHbMZzF-8c0 zIMxUO5XTuI0OEKf1VGF)LI6bB2mug@5d!sR+IHYNijCp4Za z&}<56u(~`u0WqCv69|gxL=y-Eg83#82m~jYKp+sDYyyEmaEb{8kn2;85CCzS5dt7i zH$ni!8Ab@0@#^4xCgd2r&!QT}4jaFZ^!*>bvyl8X?f-vE`~P2j|M#@p$@hQ89J**Q zrXA&Npqt{1Rchk{g*aoS5NE6umc7{R>}@gT(07CJ=xA>X{TJu&lqO34oZ9%<+wip8 z#oJ)ap);p8IBp-~ZJ!Xy*tR`vEC+n zcp>>Z^$Xvr(_Y7*)Yc;JpBK9s-WFpH-9E_Uao!fXKK9L0TVE;cO#bCu;6q16tb?HEe9Hs^AkHyD z0K~aQ2!J@x2mui18zBJV0wV;1t1U2rKp?o#1OhPeZ6gFge8&g@5Z^UI0K`Q`2!OcQ z2!Y^g-!p*#G%qni0K}z62!OcE2muh68zB%Fzrq9pf#6CL2n2$wOdt>lt~P-HE_#g- z0wAt6LIA{dMhJko-Uxxf_zfly2n08pKmaCgGC}~v%|-}-xWxzo5Vsm35E#GB1OkEJ zb`uD|#2rQmfVk5L0T6c?ArRR5z6k`NdAAV)Anq|j0K~mU2n4q7Gl4)L_<;!o0>S+z z5C{YhQ-Etdl7>G_!$;HbF@}6GNI>&cE;DgZCUXMD06$wtLmJ2SwvH=d#hre+B6$$0 zvJTUHp{J#U5h3LpjKY|gQjJDoh_~-v^nXW@USh7KlXypwK1M?t68advt2h)RmEKic z3Sd$0gqF&?ij>s6tM~v-%;Vq1zN@I$6T=r4t8Xf{1WEO#V!rP*Jm=P%ia0m}tEJ({ zPFAn!$B*PPxv1Nn(SWbL_<5fDuP6F3w9GLy!5_|vCn(G&SlJ-nnP4GofO!_QbjhgR*_8% z*s_K3nYO&x&Gxn!^R}i&-SN4|b>uK^7slU%BSa{gogce;)SS7 zB@}fhR2o0wgvVJq7|u0UJVB1lP{7*n+!MyCrq5taW1uheVQ??ZXUgFH`6m%YN@LWpFa%xg=7k>u>7Yg>`W#rEPLAFX#mTJF^7>qEqC7Uof6nsEwwQ*wSiY}VYBRM zHU!#WEU>|HKGoa641zNksg3m%;>?9YoVid~_F{glfwmZPm{x*$8I6!WCMj|FLrneKwFGC%rk+l)4eUsHHD-{X`-Z8VK$kdurnztEPJuTQvljx z%wYlwY@Ok4T|rwbDNU5%^?I$KL=5C{Y>nm`~B{L%yhf#4+*2*8_P86jYr z=$Nj#5yKn4n}zc(M3wuQvwVGB$NJhOt*>3v`r6som#6t{3f4Je4)cG|e$MtbF!vXd z?Nb{&D9k21D(ps_>lT8(tz1SV> zZ87FxDY_hw;&Sa*-vKRB48T5-Whb2Rh$8!T)TPaPHY^^Yx zY@@I<*-~NIi}|Sy+G5Pxs@_}v8r6g$?>DS1j4^g^d7i%pmL_=SCtFd{m)btU+xE1> zE+TC+=CD2q{5;>=z#65H9G2Rcs}S$ZE5y6<3d>&1Z+p-dV-5?Jz}5xc78WaoWFMu$ zd+iGGUb{lP*RHVa#SXiPw8fai?48=;ea-@J3zKglIa+C=$leL1ke#*HbMZzD@F)__?-~~)>R>R)dT_{ zUNb@f#Op=~fOx|Q0T2s~5D1L_-UI@H;7t<;1_D0)gNI69@!?4^1Et2tJ|!{(Q`E|8xPJ zO-M(5KgLQikpv%qTNW!yEO;crvNks^5LbKpCrsFo{F&w|{ro$=tzqF)Nam+)?WDA= zo#@+|r}=R=wl&5aro^D_f7jcLZ}Fq@pNuroPDVcCoMjU3uy%wdj9ZL#fN z;!Fi9=#yoGs)BL6yZ7}9AX{zmi%Di#yClHk7UrZo?Lj9`|0wDfogaC-o zj1U0vcOwKqd~SpQh<_L%0OAWH1VH@L2!ZF*16aIdy7kG6r7ez^Ng*##ZG8ViOt};Y za`~kR1WZtE0bkKfL-KE`1!MiizRa=Ez&9__GQTJ-^Y8dF_q4+fAIqFE_iRnQeb3v# z%A=5+o7y-pwQ-KO;c0%`h&C8=Sat+;e~Gt&B}XASE46WUYU50A!_y8sdbGiq!+ImI zajCa~HAf*?klMH~wQ+&B;c0%fi#8Z@zo89O&mt>80V_ZVzBYjX2z(IF*MR_tK}HCG zh>Q>bku^dfFy3GS0cbWFApjz0gaC-V5dt8Zj1U0PY=i)af)N72kCFz?|xd}`yS)W!|ohNm5NI%$J3heav_;yKZ;|g!X(++puw82>558K=g-Ub!|ILDIOxGuGEjkn=xhdXcD zV9a45ps`{p>ox+`ZP3w%nm_}Dl&GKF+6g>)>{rAsNK zGbyAS)oE@dVw&azf&yN~1OkCzlnDfo!DWpQ05RGKfgtG^69_QKuZ}e>v%gRFXYT71WOWWk{e4F&N!wwGHBx4S1%Cv8= zpWWnbU_FU*9jT4qD$FJ?E9^{ut+4FH4m&us#hAmQGO%^Cw}rJM&O9g$&O9i@nFob9 z^PsTo#r)hFZ87Gs3`=eCzUvlm3rjG(=dUz)&tG9Sc~fCS@&|=@&tGBLt2ylC&?aLJ zi>1K7TfI#zn{fU_X`1a!N>MNR2*AW7BLqOKZiE1cHH;7dv8E9MAl5QM zAh_CO69_E+v%cvF1TIc9fj}VG zzytz;U_%oK;QAXGApl}yBLqNfVuV0oYf}>lKyx!A1VBtTLIA|(MhJk|!U%!D_?9LR z2n1W1KmaDTHbMZzHbw}5*wzRE5Zf6c0AhP11cIyWU;=?au%ihCU}7gD1VHR;gaC+L zj1T~^s}TYqW*8v=Vx|!SAZ8gM0Ae>I1VHR=gaC*=j1T~^rx5}m_A){M#B3u3KM;IXhqGW^sh(03(f^s?11OkEJC=&<-f}>3!0NckH zApqi7BLqMkXM_NVoY(~J-Z@_4!l1fY3_5dt92G(rHxSw;weINJyT5Z^LF0K_>)2!J@( z2mui186g1Td?N%vTwsI%hy_LnfVj{IfgqFLHh};%zhi^|i0>L90OBGe1VCJDgg{{Y zdnOQo<|Rf5fVk8M0T7oNApqiXBLo8DSC~KmnpYYj0OBem1VCJEgaC+Zj1T~Etq}qs zt}{Xa#Pvo9fVja30T4GDApqhgBLqO)Y=i)aTZ|9@ajOvmSpRVij??w}4-c1_#i+C| z?`PT8y)HY ztoW*r*C(*qthjKR(s$<9Jaz4?cAb>&rwqbT9~i>XM=y z?l9j3K-_7B0EoMc5Ww#)WHS#8%4BZGdb#g)JWwy-x&6Ib-E$3d3d}T{A%=eww2( z-W-OLca;8Sq(@Rls+WpNA5_gQ%DuUGUpsQ%*|uDrw~@mQNHzDRsI*-Ze~gh3EebTi*kS9EpoN+7nGKdn8#;npQL6mc3Oo*9{9Z2_y-vcaMh>9PS>gNE z0tBGPHh`y3nLF49-hz8*1Ic?R!rG?rOZ+lI@;>QBxwn~#A%&{l!gg#b~bYOeniq6qMG5O`o7zLJOB^jhafQ zKr(uym#L>a_HKs;oG0EmZ;5CHLr z5dt87XoLWWM~x5|iH}d<)y~XaY)9{;?dXsF+R=76D3JWgx1)F2jyjMDeg?c44+H9G zY;Sa(08{Tl9YqceG1rl6&uQH0T`nr!fD-cK;hHU(!$CNkjZL!ho?=KcV%~iwVJ0-Bv93QN(DLw>Yd}tau zjFGu%BxdCBG;#zZr8I(fpmQ-^fX*c!pq$4p%6-a$9*u1A!|TO(3^O>SeTZ9*0CU>t zA;@WKd^D0&vzu1ZpT4>N%Sh9EWaMzAYUTIr6P0GdrnR9DXk&R^A4dp^@d~``(DtEj zMS`WG(hqq3rQ+jiu8)!V!*9Me+aBBV$4Krw(LqbqwP!8|^0AzR;sS`h;_#t$isz{b z58T_PLD_oS{?scSPbTecAJNIN%p9Is7vpivQ(JqRTL}PV`vg>*;!|p}7L~pdds+>P z41fGh8HW55${nwTXWPYiWnQbjz1@um7~EH6lt2B|15ldU#)%8>(&zem5sxR}kIAM> z#$q`KN0tWlSC}CM7Vu&e4upcwR{z9jOUBIgm)!|jb9`ovENW|Tno3(cI-<6Y%4BM5 zfx#F%_rO?;K2cc<1oP@+HgF4IB!Aop0T53ZApqh@BLq0sV)hqGvM^Pq0l??$u;x}QnsI(4z zj2sh{4yL;&VgA=ISC#HWbYwW`EJ9JYHQ`Ezh^enM1gg$=J& znhqE^e=VY+^9wg_!>`A6G`BUuO%@%dgVIZMp}DQ#%Wy-keALv2ceF%pEv2_04ktjZ zO|0F)*$j@$=-+VQx^>ErtPu&IRXt^d0N0|8nMaWBZmuEzg09Q8D5kNxwdj{{HTjtI zqTFBE!Pi6y@Ygwt@mfG!i%v#}wP-YqWs%9FBD(WAjxY^%WCh=3e^xdFm_H4|TrkV8LI!um7O0)95P>g%slr zQeQXpz8)`M3sqn7)%3tu%o~eWP^%pHUgG#O`sL*(mKOoEy`LH(z138p(Xx_>fLi&?n#4vk7@!)x6yyn&Tj1_}Pf1a%8Hu4*d_x0YPGGU5?6 zzcR`P3!{8oT^HqJuFqnvhVoj{#aaX9wea)npnR|l%E#4JP(H3Mg7R^74U~_oOQ63Y zkGPpbooLz0p3!T|=%^DJOPA3bpra#eUovZTOQojvCM}iHl~PmPLMd`XLhC&nVTDb@ z>H6R+T3nt@rn>kkv={v12g}Sk(8YqU-{R-rOjoz?ZfS4P!n=O1zZ5pQ7c*0HDsR?0 zeK87L3^Z$vv={}hB%14162Y9^8nv}n#)@$4+!L*Z|Lp$V=4_4;yP@Cx0BuwEti6My z(!(q=-IQ*sTh&7St?97zcC*lxIXoP!x?7@rX%@)QHIo5uGX1!f$!xSyrn_$J{+V?b z0gVOO%r$5N_p`q&Lebal9jU*3=KIUnteLw0($L>RKCb*SOS1`<(^Vrv7a$ z`X&dpt{mLF_*G>pTI`}Sl`nQpnaVG(_*PvuVrxYMV=H!(+L|h@48Oq;>=S(lrNw{s z?F_@>&#j&XK>Wf8f$ALn0P8)AV&EM8ch+;w)}iKTEbBO2Hn6b%!yG+=bHLNcQ2#j^ z8;G@YbR+HkKb)hVu`COyt@}CpLFPG^=J_A}@~k-;>r&=9Papro96hF*=l^+*e%5>y zNPXoT{SbX^N`3ujKVLORV}(m!o9XLD9B^^yvKVj7mXGzsCJ3>f@Kf-LRbTNdPy@`- z11kJ?bM$kT7Xh@r=Zz2m@q!ToAYL>=0K_ki5CHL#5dt87WrP5TUmGC+;x|SJfcUKu z0w7*CLIA`oMhJlToe=_Qy*!4^qK8>81z#`y))!y;3Ta`5)Qy)M*0eE(J$4Lr(@-DP z`FZ(%+gunOU*#yr3F3{-`|WtOe!~_1!;Tkz$h>aHYcP$~t!}sksIG4O+N3pdtHSM` z9esFS&lc$}5?SpcVZh}kQd4zl9k~fLImMt}*-*j@mYcHGt^y5PKkZUkKSd6|#2S=Q z?Y5Ejn<6*<8&tFl6}g^oP|+f+-o@GrX>Hczmt~Qg2xDV=^W(w=TI5!*u+^nn>r@*^~H++=Z@pNjQ`;hvdwDW~9`0&Vdr zx}7c_hw{pG(|K!qt6w}~G|MiH#bZm|gkIdK&I2w<*N3KAZTlvxC%- zYqZ70kf?1)bukgHCGc~48UC}i#5QTKOgJ=FpXI!2Gklvfh5>m45pKuUqj5puFBNLIA`@$U+MD&YJ9Zqa13@Z@Dd8j2x&bEgjbfp0EKh+_{f(vhwfpY< z?kj(9tylm)@_97gx8QGBxAl!cF|ZvELZQ^QW8Gh1_pX0C?nS#C$>Q6C#RpTx828j_ z$9u6D(T+!R_l|4RB9)h+@@y(!P366T)QIN*u)#D#yV784da&*Dopxbu|FwF4f_WGP z^R@N7BMc^4)$_8v_CCD!HN5t|K&b25X15zU9+=VHdQn-6&uAYQAo(j7}W?R-mhg($(wf z>R~|4*Ib0m*Cp0&^wxUIpAw=S4U>1mXo9h^kFiES z_vdidqzCcRQ*Z1b|(Ph9U}y){p%^V!?9^QY#O*7HdotWbspxBgaZe;zGILncO%=?u|RBx$01}p z#2eW`|5}O4$5R>aixlH|KkOSNx1}@_L!$u z=Cu=E8}FAC;}d{5ew~Pr*2R@)k#5eXn|O7l7@q{h@;sUDqCBy^5R_*Rl~18E&IuLcQ-M^Trvb1$pZH(K ztM@El0ztlh!hH3n`5HWMzOY?B@H{txuAWX;@ybgvJ_CsPIujxDg)qq1DpWp;%C}Sb zY#`<9TL8=#TG{{5PQH)6@>AwvRhX}x=Ts+4!S_YE)p+f5cx{|BE5_#n(b4k|7Bm#% zWQ+zvj&(V%s&X+FJBuIcQuRYHI;85Dz-G> zZ|zn99rTYz2!QyL5dt7SFhaok@6XsqCZZXz|KjOV-Mst}98TI6*Ul%g`JB(@gJXNe z_=2?gEI`P1K8#N|E9<${seB=oaTcu@e;Y{c{5t?_=U?GcOSaxPJ$>JiS;a|UW-uDP zZ^C?4?`%J`>4Qx-(@M`6h|?>h^53$yNdx> zrYBmNPCfxG0}0k*g6}Z_9?uu!OPGK=L6-vH9wa^)34F0m-pZ$#%oLX~#XU@MIa6>K z=n4Q<#Ko0MjPwbnFu|2ffG5nw_$nsg6TGVlut~KKa)KRj>)CTiOe0jAy}8|HaX_c$vm{_JG1`yl~Ed!biMt-vNa$c;TJ{3P1M3 z{cD7_!oezLaMU)qvWf}&ZWr1O{|&HK`Li`M0kpeMjSyhFYsgH*cJ43O?$$>6v2S+t z>zkLtsbpBC`PJ0mlg0Q3AP(&}B4j)IArh-^PJR?#q8+VE{hO$dt-WG= zGmyIHEdbavSb$q-ow$e)pMo_E)2zodw=xYLI~C*Gn1&DOZYRh!$2a35F%}zW!jpTE z1nu}v#+v;yyUps>xSyn8_5STDSHK;aZG z{K0_2?Y-~;2p7NPPjj}P*L~<4b#w7QAxAct$M`Ok&=fvRz->dwf1f4SVx^*<(h@I1 zilC>YS3QPTZHY5@BGg}aq8CO33eWYzh8kh<(D16Ii=orsEU#I*X+ROLxEK+yS0~~r zdfh)Xxx10&sXzTU?jJ_BH!bSw9c1?p7^T>@D$_;Hqx+2AOJHuxOhdD4LSL%CmlzIi zEdCA9KmTIgQ2;hRh=2m485GJTEN9_XDXjJ^;WWx*gh;%rEiY$MG1>pJg64g!$Svp?vR# z?~8I9@!Aja+IS{bj2{BRmnGf92;+@#-S`opmdQC7!!5-R5ouL-wkPZCugDmNmEfK^ zeKdZTmpw*X(_yRHj8cWC0)?#xQuq}Vx^xJxU)pzrv|9|6_RBzFn}HMtFS}Ud%MBn( zAL^qN9!G|ze&TO*2QBKFQfa7|A{uza#Ezg<+UCY+S4*P^_rSlfZGz4m29Ey*Xq$hv zwkd$Q<8MX?__^a6wCm?s&l~rvXFXpYu6o{t_51`Jci7u6#!mwA9`Q#AvB$p(g3?;Z zv|r40rVPJ6A!Z6+XvxKwp%~+*pxZh*0fBmd8j<>AJ!%qJ_hX2vwT2Z#4==JME`rUO zp8&_#*3^n=>=tNTKcL1TfyRvkYMc~k+&rMh`GLl*18Q6qXxu)a#@&I&ozPhP{E%i| zdq`>KzrQ32IPR-nss(9Bm{9mXMIlZ-#<#<0QP=vFc9>S*OP!<9*iPd808cxNhn4BH@+__3QA;s?4oK75^8lFE{EUWIr`#{o*W&(e*$}XK z_yy)+^E3}5eIA16HP0{;IGtqkaP1!67WCl-`hdqS#rQ=a=H!w#MOCtODexa zWjyyN#=ioR!@maLJp2l@|ND8Edw4H04_m=}?L6GV|y!LN+?I(Ec-vV)b zei&J-7`y70p-c`Rto(_k6TD~|Lhswpt)E!qrdkedxe;MPA ztXfOOXz!>y}H7^0Q3G2Tvv3>0br#J?U z?$>`WU?0FCyn*}gPRz_J%*>O_%JnaZzG`A1ZK9Z3E64FL9E z{5t7>*?-Y({%QFV2=etS=4+QUU(5B&m)6+JFD_rZ($$4@^(nggdm!fPO@z$XUNz&= z3@YQvVZq^9Krwy`Ncnmjfce_~zaE#6uYXy-1VDUggaGTWF@t9~nO`%XGnvoGJ;Z5Z zGhDWM_JdSurVajm??6~e5FL$e1j>T5AK3QCp}9vuGOyfP=p7z;t)RXu;tjMlr^=s9F;CL7DrfVJ9Eo z%jPtPb2ATU^=I3l9JwcPVLRJ1WoKM8Dm5&}!g~q=-GoK%#|GVoRdWwPOOunsW-FRz zS4ZPyw3-lE_K8<}G{+7Cz zht*M_dX|k!%Q51e7o^ACxD2Kv?wlyWF_i=pqtYpeK=7pH+`mBiDA-ZHh^y@qc8Xg> zr9b-=G!>MUavXe0Kv~2s9yae#WGtCO9d-^14!3B9sfDk<{<^KPcVtvrd4-yi<8KF~ zZ?CuFEnU2zTfIk2&xG@tC%(#m>xtEwxVcc!nRZ0^Uh?`Ep7p{u|Y-% zfQXC`0FgC907Qck0w5ZV5MVpKALIRRQOf9$$Spta&ynC6;FkQ9lOfDI+>PQ$)`*## zG$IcZ@NX~B8>L}8!lu!KCv4X=x@~QoXv$39nu)p`7ORE9&7(J(ux0b;Z71y3JbK3o zhfH3LveLsSJ=Oix`Ao2k$fu3$J$`aQ%uB}TMfJ&ip*vstq(XkcwD{^Z$jpjnuoLmg z&xmZVkbI41^8F%LtdIwj z^@iT3HoTJl@0cEY0!aTWz#hD_&hR|u?5&E89mdr;b6skKy6#o{DdNl7wFb~_Pe(HE zM7@E9`_TiXevPTu86frNi%-2$CH3n_y>Pe=^_7lW=|8*|c?Fsc{|$%te8$#+b53WW zSYPHiTtwa2`Kw!CDH|`3PFr1Ur8k53N`xJq!oc#UB*!hxV&uP)e6z)-f1h%GyFWLu z{k)?*UaWkqIEO^p~Y1vMWuU@zCkmQ=X4d#M81)&xpHkQ z9giQyI20Gc9aMU^37Z_#Rz`^9du@>8R=g@-*oDj%;vyvmCQc{G|-NCVe1m`CGf;+C40t z+5hsk7TIjG%y70_Ui&Uw`!%*(+|LyX4J}=*#pCl$m+In!v(5a)3yf3#cC2!|`BvDj zZ`XG8`G=4-*EJQBar|s+sJqR{L5kIWp{aXDYg7KbgC@?maz=;bVbRj_{j*Xj$BQni z<^{)uGOsf)Xf}nh*?4CJg@)Fy!NsnoW}l^wh8%VXDQNDVF}N8oT2Fi@7w?8RfBS*O zY=y`j@)BO8>;H;$<=Pu>T)p;8T>Fjx;o7d`FPrWNq(jg$-+0|=)BJU@>CujY<9B0n zHsK0@$QvO50uMC&qig{X%|-}-C>S9CqQwXS5UoZCfEa9q0Ei(*2%t>(T=Hia8}Vef zt#41iMh(~GCm{O?-tK6~^{YYP?M8qpiL*s`GpJ{=WmP-I^t!S+-xSs6a~3zoJ(>HDLY0dd#6XKHW6tqKai8{xnm z8qZ%@;j4T688MQR|D1TT$6pXn_4r@J>v;SX@p>LFBA(`P2K$v(O?4l_*$^5@kq#`% z&0*zX+q!9ujmN7yA#3&;KI9~k&e!t1Kv9E6%3Ems4JL9CY$GvJI>*EX(nnDvd z#3y@PAVzVMZzW#G;~~WB0VflDPC_cZ8--FqmE z*bTNScYhkes9tc-(g=ogKS*F0$A}V@Hia2Xdf1!@?WBeWs$4q@|HcP?&jPo#H?k60 zgw@Je*2+x)1v%OX0T5%15CE~95dt8VH$ni!3PuQkSkVXpKcDa!=pR@=2c-3bw+08Q zAH0i*E{yv*)T& zYsD*o@*Zb|0Em^05CGwf5CE~V5dyd>`w$=Xzr{K_*w;}w=Wos)Fj19*T}T%8iY)jy zJRV2BkV9COSbY@S^Q;ieJ*p5)IjoQeRUz2&tf`Qm zz_cplP#VFQRd6rR2!<@*r7&QrOXYghrEaaYlaFP1zzVUo_doyX`&6YqSc=v97;j}M zfWj>rApoMs2muhiMhJkIV1xjORg4hu_0x{}d7Jff*kbAjFS4TZT^EtwISl5T6s8CbEmN>Rra|zx~ZW!CQ4h}~_ zxE`w!4TX3bF)~2BAu(Jf-k2Cp5pPP2%M(v0p5pNq#A|!J6)~D8b+!TKGf8dS*S=qk zCZ0>yf-{S9N3c?{UMo0k+T+O^5Ze=`c|6E0mC!PB@w!kChSii%jq-s;b#K)A-iIIh zcNDlj!7ZTvOFtXTT1p}7Cxxt;6tYfI$lAc}H)llFgDq89;?)L$Qt%Bz05vn!2muf% z8?PY%0u|s10T9TbCj>yOXM_NV^^Fh!G0g}85E~dF0AfQU1VC(LgaC+*jSxU7ai8Eh z^nrKThDxZJx@WwXvfd_Zy!gmh@(!%>U(^4+}yw*-Wmf>`$3vc95x8Yd#1nQXoY^=k&?|HUzHl1qA z-o%=R0NTr@MhJk|%m@Jx(~S@SvAGcfAhs|<0K}F?2!Pni2mugV8zBH<8zTfjY-@x7 zi0zCJ0I|Ih0w8uULICB{v1%@JJIdrew*8~j_WSS~TO7#_MfJGlQ1tPYo|p?FzS`r% ziSgT1R4fr+@9~kuH+p7cnM!?0I{_9Br;Q?9z|Nv-Ow>HXQua`KGv)PS|Fu$BuSHOka~1 zKvUYu2muf~8zBH<7b65f>}rHSWgU$tEp2_rsO{BE-a52|&uw6x_Ajis(Q?javH#8E zbBO=$@p;7m@c07Y#f+-|ghsNiud8EOS1Ylu@OZfxW0|V8*~+Y|sB|F8J0E`u{c43o z?!v&d2CU;~1WVb1!?V9)Tm;e!0a&|g9epXx;MTN(2QW}u>67Ku@PKPGo6Bt-ZSV4< z^k!J;34oYsgaC+HMhJk|%?JSyyBi?@Vh zsy?261K*Fxtf20Y!2$Qc_{>9HFRJ4peA4G~T(PMepJurdU>5er$1GYZlUWU)Q9Tic z({Hiv1Bd!H!k<#ps<@hGg9}}R3q|f`SkOyITIX$vuWdqk#JRXD#fa0SENIAKhB4&$ zYp3+m-sZIqYwSYlS|qeX3{znnqf4-W)d89HeUqKX)^-nfxv(wkkIU<|ru#sR!&X|E z^12f+Jk!JzFgpH%2OWG2f|IMmk_}WJSBKf?>8`!DHi4*@_vQIgye^6bIxNRz8U$S( zT1(>{jLD}rptf6L+#ldwCVVvTHd-DGa67>efIA45D!qjK;+-2ra`R|Ig#z(vOgXfz zAvbSBuy!6IVVQRrB@eQAf=4Sc|+^rH7$m%|Y%4kcyO#C! zpw!cx|IiDrhoE;@UpGz{jZGU-JQexDk7hW^`qoAyei+KhW^!gx?i3aS?w1M<4^WG| zB`kD|Y#-@xgT!K3ej=)G#5}q_Ph~NrZK-Mz_?F+*)>Uo!Rsxld7qrGyZQ%Bw3I}fg zsZj0Tx0?QD)&8%9zg1CP74@uWp56(y(vO%o=4Zp&p4bR2cA9kf@Pqjb4M+I!&I}xl zYKhlJ@l2ijC0>$hZENYtgTb@Y!t3=HoR*u%rl6_(1eSCYOS-ztJgR+^E3nA%I~%{H zJImDVGW!z7JFdQj8zx!zsAke+JP_gMzy7inm9El=`zve+g|@~j&+kN}%ZFv7wq>hZ z0hM=Y@ME5tOE4s#gc*hRJNDmTfE_V6&&EHy590ZbHNcNS4~b$PJcu{JpF*U`dGzRs zdC!i|<<2{(d*=B(wJ>(G?inxbkn7tNivBD@HUs>hZKOt*odlzsF>#I2;i1u5zo^{b zul^>wXIA_=Ix200l1sRh!S_)Kxn`-}>&>y1ivX4~`xqerVqYT!KPh20Jigt%VBbf3rtmKl-(JsgKJp%~gT^vy!^Z0J!Cp^BF_(vZ9 zfEb%L)PDqcF?&!y@fr_6qc^Xih@%gqe)Rd7mp@c1XVlAo0eP|ooLiJTjXh=>dkhX( z;CtCXTz6yNg?m{0qO^1IvrrFKzA5nydstt7>G!aLb?-q|d;%!XgN+aXaflHDAPzM` zfcramrk?qL<$Zd;^41b-OO%5}txNmE8rP7wG2VcL!R#*Y|l#Q8*D5nomPT8a0#@eco>lqg1 z^m|_6m-NR++0uSUp8m#{e$$4psOYrD2zMTGG5#m16V+Sg-v4lUGl2ZewfqQxh>Z{c zakvozzEACjKJ^jn_)L^N&kApiny6jU>Nv(Wq!;DRVhwGFnWXv!IUP{gfs4M&un=z< zX*&ofA+uP{=i@6ua|dC8h^6!K)gnfeE+(OUg4TR|JxG7M-`-TZ43d;O3FVI4kYA%h zD({WsFhx`8I_TIv!1quC@a}ASw+X7n;mA)h#@-XQ!0=&J>^Y%rlrmCu=O$P@0_&$w(VDcI=Zl(ufKDdpFNmkyu{_doTmEQ zi}g1etEiUJZ&dcn18WdM)vQQ75gNVSO{zstss2=asGrxvqFY@3oX4wAXZ_$Q9ir^UKM46GkKTvz?*_Z;&5f6rmD^|KtxbHc#& zbEK8N0IK6CBLqMkZG?d8hwt2d!uq)&t)K1t)sJo&ckuPIfce2bQ^8@WUE~kd`CV!3 zXBJ}IHluz%RQ)spmm1e#H_7v97)uFN)l6JlRitXF>YcC@7gtpm(h+Q$7938*6#0vF zzN)qZKv!A{SB~6U)v6k1PNZGwz3?Kcs&CU`-C~hdrNtt;l8z+g>lTakU8%A+&A!9~ z?07|bpuTjB)r0`5=2#;HKpba;0Epv_5a9gBy2C9{Tit>h6=USQl4!7txXJ zS$A*JksYk=unWn$+fsG+QMK-{b6L~PJ`FFTy1V%QXWh-SiV#4}l#LJokr*KW;shfE ztnNN#-F+{uyPXHFyIp+UUBV0=!@9eXMRu&!-EpkDH#9EprwV%$IPG_D+319Rhu16W z>yA`S-F*=j>*DI}QaZ8|>+TP91lz*C-|gb-?hMu4=heEy_w8!x?qA_WRCkyC|E#+c zts(?aGxLoQ0CAEL0+liHFRZJ})4JNVUtQ^V$P8asS1?2KSy#8Ph)%M)I+=C#Jlb+g z=_b_`UbD5j!uzk4e)ckPZ9gMbQ&*YhC0K4;Nk_0PUvO{H5o|I0y292n``NElSBt83 z^(S*89V4UgBC4yaXt9kJU!cWyTI3knL4Yw*XH4>O?HQB*b&NdOYC-^2bBYlHAWk(x zKy`Hx#>Bs}uC7k&YUaRoHOtr4HO$cItgG9Yp);(m&SYKTPp)MdTQB|_#ijM)xuCgT zTzXACamvT%ix{D9ckS4b`FH^dTR46jk-F+6Ra2c!VM#Bp>s?D<@e}}?zw{N`|Gql0 z?a%6@f04_%k@kEUgW)J*b+DCv zM8C-F_CPuIR928ws;Q%)Ve#Q-W5N}Fv~XKYus6~f>`N5fJFFl)Tk#cy{Rvi(|7ENp z)e6E5T~I;jCUxHm_4kN_Gw*-a?>)>jtmp(#mS-9v0OBko1k^WpFZCJA_W!W<9$-=w z-S_aa1G~c_N*XX{uw=o20Wo9F7%?YM3@D?!Mz#krE0|Fc11O3C6)}JciaB7;8BDMG zn$v66@jK_^VeJm4 zvGIsUV-brzYV6@^>|Ic!T8GaT``+*)%Zz%%;&CC;8>W+SSu%zv6*#ypPihQq@}xq2 z#ght^gWJEnmKx_d$9az;6Te z)fIf?_E{jE>iJb+ePzYcG)cuPE1HJGTId*4aQ`|Ts;-iLL#XLGM0EzAZ<;2C6_xOvQEL$(Jbf%U0|mmP9v@QxI;Ke;bP1 zZJ?#;?eo2{i<|kZNw||2zf|KqZ&HcC#^-xLA^*O$#6~TIDgZq>f!9u2X2P|N6?3sm#kH zwz-=E-eTK4LX*7h+vXj%4d0*8sp&CkkePl#6J6jg#x`yZ8j^vkal~FmB(Yv+Fd`O-S~cnPE8BMZimoQKbPn*`my=+;f!ZZ zNGM~%0sl`E-pg7XNfY9cgT^9Ofz^aZsR>yFe8(r4aI~+1j2x9FOrI_P;Oka4oZw9+ z5!moj4@d;SWgd_SfXh7~5k4Dna&1k`Z?y4!v@zC%DUk1olxGS2x8Um63u}VgAxCy0 zvk_iUIt+~<-#_ToW04-6d*a6savuR7c)O24{aSisfsYzCu1c zsE2ijc=f-j&mhoo@xP+zL7WBFHcdt{z5;|I~kp<~bP4f4zFn&vsEH9pIZ{Pj%>$daBsbfvs|r}})yuI;;aYCeYsTBLVZ z+&yd-OCJtPm+R(IHWr*49Bw5v7LTP`ervml5%kqhNUHB&7`Oo}(7CuyY5~f|Qx5y! z9Eq=sFWiaW#%|$3ixr#6ILx2ORCe?-g@rZxI)#q*It3aiex1Tfe4RoAU#D;{_#y+k z;pDWJc66!j+NI(?A-Z}^5thFIA>Xmp4`CnN+au1uqu_P?_w(}+wafE1N{OD0_bkw# z!>zQ5-pwTfzrVr*65%=qSGF;}bZvfsevkF}BVbkBFoK@+fvp1G?Xf9GG|ZBwalE9$$vZGCtoysmVV>U)U# zuQ$^oZUXDbd?`jW71i&>OkO+Xu9*_ut zDISmrfNMP<5dhbDKq3I9dO#uouJ?dM0NmgKiRd}%F8Ji1wDZGeXFP0Hv6dYj8==pzzt^=ei$2AI$!1;Sp0zHUy<4k8)~+P0JWM@tycU^GHaF1?u(R9QW|9*_utn>-*905^L;A^>jjfJ6XH_kctI-0A^| z0JzNq5&>|#2P6Vuh6f}9;0_N+M6QeYBAUP0KOeXLc}%PRDfjV?)&BVe4V%4>$Nm{^ zL5k+I@NC8w`)qY^VLxlviy`bK5^(~NlS1NLg`eR(szcS!{9cBcm<0^lwWNCd#$ z9*_utdpsZ!0JA(G5dgD2AQAE5Eielz(}$lnA8w|zW}efL`(?)wF4xVcNwW9L=)>W5 zSzYrcSYm}&je$AUhk5JF|Dt+qit;X+|84bYDT+&^^jFqrq$qEx>F=$N*J?b&YK&!D zV@`nE32bZLw!^A8M?Jk;mLR{i_2wXm+xjddrB5cvmHo41%#-msGIk^*+q!`fw6)yn ztZRLzv#qv92df!~e?O;;#=LudD@X**a-Rnz0^oiRNCdzf4@d;STn|VD0M;ILo+c3h z4|+f%0Ool>A^@;1saYff;9(C)1i&L6kcjxpaQN`w^q1$%Urwm*FDGh4Jx{Y@ZXJES zi~ceShBa1C0=4_4HbO5z%`Gd9HJ@4gI>bq!`w*SHf5bd79ww0p%NaE&aXKSW$ZSl}AW zR9~FQsu8Mn51z%{dk{t$6ZVS$4&Q+@GeR*ewM z@D9yzEr-TlN39n!D(GwP{{QG}9J4S>skyhh zuf6HLKq7F9w>%&b0B?IhA^_g;fJ6Yi>j8-Xc+Ue80kFUW5&^K#0}=u7z6T^?s^n1swPGhP5&7VM0nrJP&PBJRh4TzRkUO z0b-o#?T6Gx+*f@iZi#4aL3JYs(o7bI7F?^o$XoTAEzjVY>(*!k8Yc4ms2-pl2fjY`UY zl`;DR@4pg(Lx1Q2i2(S>0}=u7u?Hjq;6EOa2!KyKAQ1qcdO#uoKJ$P?0Q}bj5&`hJ z2P6XE3lB&{#{JJRHne9y`#}3y>2x+kX59aZ?Q@2<&xg`J>~1rpeXP4(%xnv@Vf^j0^ko1NW{)l zf9B-fQ(c=sw*GS#J4&YiT#4o=olUr0_a8Q8_8BwIo5CYzbVWWv+D9Z^I%9~o*pR^E?D$+ zJf(9Wv(`0`ExyaMNCrSZT5pr|E)TP4Kf-V+{V1CqouuLr^rI}HKScVGu%I6?6MhNA zsu8Mni)Jc;Ib@d%knP=4r0`*gmRe=AXNr_VP^&DVKSXW>$suX2o@K2F;hl$>N}yJY zX8_dd*$P`ptuUlZt+LsDk>L>3Dof}Oky;5$TC0~?D?+ty$xJ0stEDplYIV`3Ray`u zh14pWJyWC{f?8z>{UK6uIV7#stE?3vbe~VyeJv1 z2AOCi#8Z126`XH>O2%c$7#@P;OksIaW2PVvLFQXO1R2j1;`y3+-@dbbe0XZOFz8Pp;JYCX@);6pf>fA`t+24@d++M-NB@K!XP) z0-%!zBm!U&4@d++XAejOz@i?I2!O>rAQ1pvJRlJOT|FQX0Qd;1R$U?hx_dw(0D5>p zA^-{=kO+SRyes-(J^lAf^WXE;e=D9PK94Bc@GG|AeEKi$z3aoo{K53IKuQ4c-h8_ME?`LSZTsOpEY zP!HY^2N(JFviLZDE4kvhjd>j3Y!)9k9QCU>Zc`q|x1Pnv9pC!6&3GK&h!!7rdh6r1 z;BkCgT72B3*2itd)G`jHwZKY41tuJu#%@fk0kKmEgchfJ6XbW}~DM zk#&|CSU>7WfBixIHD1q@kAq&Kj`}0*i8b-)>!5U0tcN$qr$I51S{cvd!ugIwTd1RU z15oU)=us)!gJ?m~lTvh9qRS~dIz^W!x`Lufimpg>C2UF)#HmLig?C~Y%AJ@1>w04^ z9LqZ~#OsSanVnFrI|ey2-JvN1pgWAjSYwa6%;eS-;*mHeLfAlc_p;=1-)VLK^%QqOo`_Y1lKb zV+1|(MDSoP>M}$Xcdf!)6vJ)}-o`EeRn|bA_jrk*3t$?rT|goL`g%Yj02)0Y5di%> zAQ1qV-YbVh0IcQ#i2zvL0}|mq`KNJSXkb74S^HV(QueFNdEp``E?q{rT=xr&l>N;N zyIc6lOr5^J2zMlr%fG+q3>aJ9NzBit`xhCVC18CY{Y{O3rO`PldJhTgsw5;V7SF} z2=k26JXik9JR_Lr7|nBaTKeHco3JTOQVD-h!ZbD?e&^6wyphevwY;0O`SgcKmmr|0?4LF8H-EB!b3R*8>s(fQx@ULLvax^MFJE4Dx_Pto;_n zn6(Jo?@w#LE1?qaH$?r@-)r#GwAWJ5ofs1RD~#SkMYHWZfkwJYjr13dbTf^Fr+^!a zw;;xLo{kVdX?PVp=o2)|u=FP}tK* z_1>$fuJ^ZBTlpK7FzQOzsK4PWF?G$i(%=51zukry-S>9&w;#b%Up$$Mosqv)qUvwA z!FY64Jh0p_XlE2!+tj!%tu_M*=5I57H1@Z*a8#wg;dvN*`*(8WZ|zu7Occc5m;isn z53U-EQ-R6v>#k>n-~QbP_04|!ceU+Of0L;9w{a-OXE_#uX;0AdrJ>yv5}b^#kT2cG)nAhi>06aZP-3zw1055)YqUwzWa*@^=R#{qQ0186VIS8;s+9q#XAtAL(NoQoCTixVv;p-QzfdtI6JDypi$zB z--=o9LxTC@93PE+aqU9O=-w=)M` zt@$C;Y3yKnBo>X9#aP@I-)g9>83?Au;E%cDU|?8hZ7-4YmO(l8(b+rUOn7l)ZC&47 zaSI&OUbgN8v~XWsPv>f$21gfg;M*wRZ>VkA5<#cBe#7cpdf|zNrk5eQ_yE?~n!ZJ> zGroX<^ya%j+r52#OHUkvU&$8n>xk%!jdvrp|HidFdpDFwijRwywq}@Yfo-=AwGI!* zH!j>AAJ4!(zC)ZaEEf)MSF==Y!7^c%R>t78$igLXjkR1^iG-kQotuRM7WT-(c{dwq zU;W+YqBZATS+fM~y{?v;bN4cp{Ww>H4=~kq70uhAU})YB0W=?JnwJ@ZhHlv&pp0)S zf0r!S*~IrKqD}3UjQn-QeL*|PYrD2D?Z7bE7Ta!UQapqz+KJ+K*9pUN;RdgrnLx8O zXsEpdMGBD+bnW2gAVswYIPd0Co!5Tp)Koi5&AA7ds@i#`=_cBDL=v+PXnD$de z=FR{k?RRHEuf2!|9UF_g@I1siKZsJS*~v}myD&_4!j{tU9GXZcO6d&C1$=Tro1K|J zv#T|`NEad@fb@q_=>g8Whee#%KH<7lJB!V^M}qeIp}o6k-xW#FzB>YF-_5jt44K;4 zxt-zb*1|a(39&7N;Ng4{>n_;piwiJrnLL!tXg)%G`MtVTJO~@z+hXd>)D|o^=U!s^YRxb3^l%W6^z_is3)y6>n}x8a*a(-$v=Od& zX`>~@UdCdkJV#9_b}A|Zf$JRWB91DtnX0PUNOd65sW*qr-Pk}F>TPjgo>G@ul&Lm=HD=@w}vt_;?7lq5oi+ zzLT4y;aLom)3N1fh&fc_g0ZLoUd0HA10ut60j;g_nGXXZ9^h)|UHl3c8~PTZLenOw z!Sc@`YZI6JoA<IPZP{8l4*-eJS-7Rx{^*)O6Yk zFM_IBN^EsDl3=TI5x`bM%~nNJN*!bhVyRUyg=#F~(O9+0Pl%XR$U>`(W0;(SEvcorCknYaFrE>G1V2MWv47rWP&-?(1XAQk} zGINBOz%aQ8TaFMIokTlPjt~sXg#-MQkeN6_!~>)d2?0h3JgR8g1Dtm??X%i1o|3jP z3(mQAdH}U^j9gZIm2?{N$=#$1SjVUG1Bo(h=c%6-Z7OKV7xwFb$Ff8br8$V zxlTbR*Kn2>o!1}2nF_nYx4DM1f@q$KBxt??0W@O`M>IbVrL`^707kxDzeffPEe`A1XB*g5-GX+K zv&C;@m|Tx7TO0+8cA{)?hULOdUOO{^W;@d^3mHNr1ZeRdsrCTpT_M&Ut>LiLoa-61 za}8%j(S8$>p#2sE(2g}6(Y}_*oDMM3J~9h>?L`np-^=I~q?4P{Z)KR=j4h?FWYURJ zI>U0|1Mlq21nKzEUT}6H5&}qHB9$KCyjxPl`F4-ia9C{4Efuu$yoEZTPv3?lXrF-q z+Hu|z?GqtW$Md;}r7BmZO8in#&*e)8m1LpHI~XRnV@s92O(jvPWLPd>Jy)HZnV=HC zEetA!NC=>E*;HkK^KLnnkqDs zi{1@fzFCWx+aQW3wpSTib4+80ra=hgezKZlTkNKX9Hxhi#T=~ZL`fix9PLfkqu7Tw z8|ZDeLa-TYM4QcGnB0jiZMKTpj3{l!uv~b|+l-lDv-M(|36T)MW-FyO3vk}8Tw$~M zZ>Hxv7Mydvz0Gtz2rUPj%|;Szb{_)x2G)bbH%=9$_XCXj#oR22(QytAXf--I8e@zf z9s2}huqcj>a~UT0V$0ESRWk-rj*bk=1-$4&9f+AQI-U}bjzS~^7#&?|i~#4|s;brZ zljs_m<>p-9V2elKCul&};sGSV7V{9m7W>t=9K&+m>25C+#n1PDtCH<>uV#K`+ns@IdH&6iLwgI0ER!xnA@R+|!Q> z!x4j%7xi4<$ad|>cBOZ&L76(CB@LbY35LmI*wV@I_AeRviPFg#mJ7#uq09uD1`{eH zzYqxloP0nkG{AYcmg?~B8lCG|ZqBV8baEXCz6hO9A_+R5MxZ_}IxmAdymtj)XxF9% zSr9({3=U{s2eh04h$%1w=ucbJ(H5Kmtcz5$1&gAOKg%$A3S0X4YGw)Vs+Yr-Y>Qg_gBAQQMAR&43p=vr7hMlTM(r!7)Bj{ znP3aw0mK$UBm}TU=l}suJ3tj%u-uIQb3TMl2wS{@B-r9L1h56>L*f^cME2_dBU^l& z1w*e`lj?_1J>7Xj*v348mc3$tsVB-_!LVGo-FpHvfo5m(1nCt*Bn0Rc z8>0aAgaGH=CTwZ1J(>@()STOtshkUT;I|WN(2t@0O(a44+X!&391Gm!FSa*b;T_;j zLnQhxqS4v0n1W4btm+T-=R+GR)}m2%^+p*SjKWfBl=m1WZ(&QLtZhaiN~16=7iM~+ zFcXZjTWk~|5&{@y^VBE-&buvCb~+~BJ~tf`S#Zv6nY9ZBEZAiMl3d&j3n6Q69iFTS!o}7z^4GCzH)RH z41HxFZsSS`;E7fczmy z(Y_MkyxSRQwZ6hqb8Z)=vahIi4BXKEUnD{M7YHKl(?sT%03+>RXThKyUnxVE0J~`4 z4H45$a%%sIVe&b))V{uHCra%M%Y`Mqc4mV1JwTIc7a}2m_Mu2o?E%iaJ%Lu!&Qf!3 zFQ%$?UN>P}hxV_L1nu7%2r9`!l|L~|e#Dk4H#U_-sghy2 z@TtEBU?!-<>r_Gm3y}~&+0O#GoDkB~9qU$CWoO8px&JH#Bs7npbUeNh7lA!Zf z1kibk>HJNa^EZGt;QfwhG~f-~k3K-q20R{`8Ssjm&?b1Hi`ry(un9|~P5xk*{DLiQ zvZ>jGC~d;9T);U?F9nzhHsRYrQ>PaqA%IN|O>Gk3ycH`Uj4p!&bCNA;VTdZJX% zuw1|`0aedTP(LzOFGNBB^+%@a1DtmwMO+oXXR$eVR8~7q+|Z6Mmr#2SL8SdNk=Y(# zq`m+Cbb=I2wQo-Cc!P~>&XC`RJYn4LF3y}~& zdok4>;JiDgE$u8e=Z+29+2=6}f%ZBiL3%c=NCmZ>hoVqIzj96JT%+qx28=_@-{g!*n}n0CW|pl@C!7t z$u?#aqO=Laa^Y8R6J~-<_{Pe#&kK8#eC}_$6EH5`RfLL30TnsxE<_+9KZS;(2@8=v2-Jc=3E{uo3IP3&y1wCipR* zTrlonHX_OkMuxaE=H~;<1e()qJ|Gv2LL>yZU_3pwQGoOAjEs%u&QH$?EH>xP^fuCK zXG}$5Bm88JHo^}Y=^t1tllegZ1L+_5fn)3+$7exYJL3nB?VHa6ZG7##BTcc9H^tdV zwW$${qA6BjnBZr6Vv3#26hvtXhULO!Zwh9DDK?HxAw)s|Q=E$ebXFMPyc?%ly}v~3 z2rM_}#%nsSAL%cc$-)-+r6z5GUxrrr%QR7r9}9Ya>4RuAgnTLK1kGP~sP~su5O1}> zFoZ@r!yDz#HpBi{JCT|#4vKaqU&qB zP-ZvEWS_q{$RrJij(!Z2zSwf;*wthb<&%mkY5CR0`tgh&W5bWBKP1~~67 zRUKYuw7$l2bMCUBllRgvwTI6BNP^DQ5kM#IrHRh*sm>>;v$h5P)4us~5U>Y`Xso2| zM%9b?#+`^%>j5NXuA|gGKW?NN>Ix1hM8lsJY-Z zUxQTBOj2rImtnFNw$wb-G!vy}hULOoubG*kxo50dh=c%|r=S4U9N@gWma3{~W~n)M z9aB~FOVEsq8)zPgBxoLl0GhF0ChdGEl-9Pa4=|b+&dP$p%lD-Ay{Mh;IW=e}IlX)X zhRJ%^(#!WU?L_J249kVtUOO{^W^dCjUM@sJ0588G)gIuyn-*)2*2`EbUX7O3jw>~2 z-w;XAzA=JG`)49^6M&KSRfo|aDHycxP3`-7?KcJOB&YUG873QHOYQrZcB0hIuw1y` zYiA~C-!IlKL_z@Vx1`zwoOjdP(9TkG?$)54b7EY;LHl4NLHp(ipdHu2((Xmb)H(6> zh10tl7%|AVVG=%Ep;AX zI*C#z!*T)7rs+(NnV^#|&ravLLL>yxd3UNazZs1Hj0a&(DHsE8~_|^xf^*K|Q&ten*DMcGyz=L8hK4)iW#?+WA&y zCa6CxRxdI0m2_lr0`hD`{+XJD~8Hz#PPFXQ?e+IK<{wC{`{YU76>Q+@fj zxQ!2{$}@Z$&kZWcLY2EPOom`fmBUOWQL1EEF0}V5nF%V-j8zJe5J2UFsmcK7-8_|1 z&6inl&OH=#vW;=+4V}9p2|9O25Vi5@hp>(J02sA#F$<<`Je=y!_Ua!F>d8&@Lm4K! zVN3Ogn0lgA&#+vm_v)Dm>d%SQ3y}~&{iCV+0O#FfBCbjsv)G(_JZNVdV;uq7_e2u3 z?~Nd8<5`fYZTul(smepCa-3KBL{LcFlfP_q}dR5`*_5~WIpwj2&H>dQk$WI(k35jddLta~Ia zG0t1!jbI5DMN1sXFd2a@Epe1tf+#J)5R28`63hfkjE^lLL_z>d;C`D~BEWg~wrcgZ zh^{+XE?$X}wFOp+V2hDRf-R0l5ZU5Xkv$4vWQ)hLAZ$^@0j*}^N7EMXqAckN?;&Dd z!J=r3V;Ck!VM|-!xjlKVfhcXkuv}Qd- z)svj+&taIHi7nNiWa^1hJ;QS00pG^V1e$}ajpaI9h=c&@e@oQ|7(YLtI2w_^&oCK> zEu~}eNZgw!r8C42bi8zCg7k~yh8H3sfOOm!$}qVQTbdy;GZ3X27?umLzFwd*6U=aFYz83`0+=C(0<@P0IPco4PH%_Fx25Ac z%gwnC-VXX4FV=2hhs%%zJ4{57KF2F=aL$qR_A7wfbG(xfZSy(a=fctYq>>$t%L_jB zI~0Mxb!8_)te!UV%aGb>>VnBwBbw|=hRNmF(qv=IWJGB)hULP1KV&cyOtwjEG9eNI zm@J=~EWml!QB9K01)}G8S#ZuZc$?|H39K8#W>+BzHoFD^d1qZY`OPnJ9!x4m62%Um4SQO6^*D_45#+GLZtVc>WBFeJ_!*bzk??220noDg2 zk$I622?5R$ol|23IPVr!t=@m4IU>u=xy6Dlc%Il*Y;hfuV2kS!z!o@9VEM9r(`d-n zel!~~bQ3J(a`tm7bx!e(-X-WH4IBLihRIZH+32U4PNHmdhULO?UMDl5(XWjgU5JDL zjovNQ8Q{F@p4AzhCs=OI^$0q7ox7XpoQ5Rmya@qxVqH#jE=+YUa#YY+#QD0hILXT_ z1es)^%$pe|H)2bfXP8W)l*zDMSi;L>Cdj-pmMKI+0GYi~nE}qb#Z^Y?+tKqWEI8+u z$m+y$KYaTZBthq`2qK+pL!Gwgk+IG*sq=ELbIG8SEYx`$!(=+P)OnWaBubqO%Y~j^ zCo@6k#8{^g2?2C2o$3s5-YwI%P8OVV%Vu@rjsSGtjwI;313{$o%2ekwvCgxp^J=ei zxuBCQ)H#!3G6P%cJjZkrrA~(B!qQ$RGePGyu}&cp0_a>J)fwQtTd{4OEI8*@%IX{{ zI`2dhbl#01()qts=gLP%etRx;%B#Fs!IgtfvQXze43oRCrOt7tlPGmEEEiVvI++PH zd?Q%8&MHJg0G)kOodM3fRod3cf^*IVog9a7mjmrN3rWyFiM) zPocMQyk!kn4MIsnq4zONW@AgC=b2EV6w0t%z*U@{ubBxnx7&CtGd3X-0tjtPg$6k9 z`l${--bUjP%gwp|S)I5&1D*FH2|DK@h;&|^>U=b#^L*;O!|Plv=p+qwKEN=UgDrJl zU^uUgtWMmyfzAh!1f35dP@T9# z)4u7OROj-e#BXsh&np*F=W<@>T0tjisPkcl$vkYS^CHtplsXxf3-Er`$xP6>e5_N5 zgaA6%Np%J|@78ToC(F&bfk7v)Eg=XxA3+jyK864~acwC&heDnD?Mc*$-;E%>c|8#D z+EPU0+VWzmUYe>oYZ!!7n{|?uvxX-aCXZsvS;Hl!nkZ)t49f)!%Bq@~K(magmRW-k z2?1se8=wH4bp|-^Hl!-=!_l=ROU=2Bn5x&7yyn6UD`0%*osf@q!$rTC2q zz-a#TMi#_O!diR+wGZg(eWOI1#!%3Hn4ZGuX&Q01!( zlb5lj$}3DIQL1EEE;M?T%mkIU#VUnJ2%vJiRAqqkZhMtcZ4SkPb8d&MPTYBf&exCx zoo^tBbk0t7z7^}7M4dN#ojV4dWTDPC878k|OPyDmPNLMwuv~CnCo@6kEwN4^5(4NP zlIjdF9@KAJCkw{2`avhx%5h5)I^RMPbiRWCI&mE&?b-eqy6w9FBVXPy3t~ij4+pdw z(XOH~F83qauE7{AiX++rhRNI5azwk@j6swm8pCqoJ8ukT!iY989?^tI2r#1Uo*E;- zdAEmZ^}Zcl2eI6o8yalEaUAzsVT*-Gf-OEk09#-j$JwNP(;<+pzC9K(sQwV?&3l4? z<2dGt@}9zLsCpf*dM~8fI8IWk{)l1nKDJan*;EsyYKG+kI)IMj%mmfz#;S!#2%vf& z6rkgHfb(u&s`8qnah#>*++x>%flGFV^W0?F0Te|4{+WcD&l;*N8>n) z&AAalJI8U{riS*fkp%7EB7k;`B8P&#d7MybcmIbwKb0lI>|zv|6`c^fGu_2XgY~fC&O}KZLgD=p!42Xrw|DNbdE}O z1~~7EZR=#gId@FZNnbuxbpC`S===pi3o)16!(`ZYqgVCBt$7b6{1;Oi=l8 ztWt=C04h&PRR%ckPF5M!e3=F3+~}Z_b8b9+0ADU62|8=)M5iCC*M&O#1_fZ$#>29p zZ{r@tTPgo|KUTK{`6Q?OS`gJHe~YeKcbmy4O8E@Sg_pg2W`g_^;;~wYgaGnOsr&%v z-59F#Z5)l&EH&p&3EDYNIb5{oP%yN2K!7&JJVnN<8$@Ouz)1UZSrBLJdK}O^7PRph z`*vDlJz7E@t4FGxu~`()*m;IYdu(~eo?(_C$}={@asiVabp&SO1)NzzuDpau2yn(e z9R=taJHU9XUbT8#MDrAui|6V!-Tx;22(d*+B*7M)5Ja}Tw#aY1?EQ+>Rlws0QYXBVbTJ^U-btFT!39SR9z#K28IOZuqw^4Ss_V7x*0uHSG<@@* z={PaS`G9+!KNJrT^zOOb>em!6z`i>bujVRUh**8geVq7>l|u=rLFeL?a`?Q`4t2dY zm*#!$07;>7WuZJI6TCJ6*~ zK>A2&=PLoCov%VrrJb(^T)myIL5gX9Kr|DG+j$Vpho=57f*$BMlabzUT@B@}pWQoH z{-=>2L)|dAPmw-?c=nB`7=6`MSVqU_rKtR9%d4=jz5S0`YP;5zrZD6Up@wdF$NxQS z%WqK&x$ndeLnS;Q@)hO-6MY2(9A0`7yoH~Wio8Wjs4ra?N~o`Ovx2uU0WF*`jJ!ok zs878mZ^}PSEvMd+*H_{X!r8EB-qMl&AjS45QNHTx+A;E$R;k`w@cixC(2YO(3)bFi zPQZVqzhD)q2Gj7G<#CMp3r!dK3+@>s{V4I5sYrsqT#o=_=N5Ro7h=U}+}y~`%3?(H zyqmCR?ZQ0l!#6WbZo!s)cs8wu+dgO)Ca@1PEEllStmg#g15=pZ_$Y#SKpQ1h9vc_XwL|k>sLg177EEj}Hph8GMi_ZJ ze}GeG@xFp*ooYJ57jVz7W^aV+L}LpsyK69|saYcU3Y*h>WwqwBcy8$(pIeq}#xINT zMFYKV#)E`4M~ffz!9lQ#LjXTIy1wO6o-+1>4e&cAfZAO0d4sV4F@O+lz8$4;67q;G z8^9?Vi-S2ET}SZ#`9qV_Q7MpW-NwWN&hx+kAAI`6hNP_39M*3e+ z{X|>opF{myc>Q+={p6?q)fp!Ju%-UFrk^PFGb|Ty6{Px^3HrB;^$U>@K>w^%e}M6M zhqm;ySbX0h=;t*Dp8kXWHIM}T0}wzzuAim-e}z=Ne(raoTysb}7ayR?N2rps<@w@`^-sI1{fc5P#M*(Ian~h z<&f2RjObh&Nzl12f=K5vP^UWW-lOWAN1fkzoeu_`WTDQ143l-RrA{o}i#HRcPKM z3Bah|I)E6rh_>ptXz#}27i?*cTaSj8CO`Y_rVNvfuw}nR=a7~r%6`kRTsYXbG&6zb zOKWKvw}eOt&~G14TROn_a)mUczjm4!-fPZc@!g8h&eY!|`UfKk`Zq@q>EB)SZvimU zKSuQvZKeM)>gPA*ME_GkKl!PDONPm2*i!%Frk^PFGb|Ts{bZb(pnp2}(ubghNC=>R zeyTsf_@G5w`dKW#XA$({oe4F~qJJwSLH{-gpubB!AF8Trx*1ZXKlA#g=_?AaclAi= zh+jY9(Bc!+`y2>m1bZG4n;Vgj9={#KWNU2c@lWFL1!GYtzJLRWPS3DhSj|gjJ~;i0 zVCvJ<^W>Ui7FoCnrV?5mhy7Mli7_wX*wj2yu9OG!yo?k*tpzymUIB^|ce|xgkD3*0 zlF2oF^g3hFn%R%1&(*QYIrpj_%j;~sOQGgCvDNlSf~|H$09!3(l{*fV!|$R1248{M zhBQ5yP4E`%QO$UIl4k)l-Vlb#4%pIoPtjRU!uNBqPs=fkEXPdVa=jK=xIt>QAF5fc zXKFDiObUU;UQaC+;JkZ-R#1EOuW7ltW<_tW2cAjo#aiNP$H88B`$7$#9mY9vXC%R1 zyCR6}wee`$Yd3%yd+pS8K{mnLtB5-RmA&bx(F?)%{hSA>2zb6#pARygP04>qdD zJYDUKR|=nB?)P0 zmV+542VzUJJWsRWl7uMD!mwPxb1FKCV0O+`4bTELQF*CIh+@$+W8au zr${x`B;^Tx1jA%FwmhM~XsU_wgwC*BxX`O+CY;b$jZf%8Bm`*a|DpgrV+AVwoCrarI%Y|*dbY_C|m*XoIArb;e#|=D_9^kzDQN;OnkM7^H*qp;{Mc?kc{%jHL z$07;Zn-M@eu0KWlLdeuU{U>6nN;r9A@l~(#r=XH7RCyf3qzPN91eG)}QL1EEF6`h{ zG80t37ONB@A%MzXQk4PDJKTk&jB3}PEI8+I1JUc`eV;^h9*-pGJP`qO;=YgQ>@$Y` zeGCul2q9@1T=3Z1gbxz&x$v9U$xLYU>*8K5L_&Z@ zZ&&Aa1~~6(fpQMs1tPGzAC2QIH|KJi&UrU|dyMEj1xe6(DgyOw(fM7fbHFJYm+WCs4^y0G_CTCzvFUAcM>C!~$#SF`ZpS^5mf)`JYy;z8Z0A9QZ3efXafb*_1 zRr$7!&Q~lo=N4tEp0C)pV@2~hNP^~Z2()egq7OfrYW`E(7BftIFK{76E(RL8>gJJz@Dc8H;`s?HCn^H|?idxFn&l7%`iWSE?fEp>iq zI*C#z!*T&L7(D?q6LdDkT~&yL06G^>bp|-^mS|fi3(mPEgHDb~r-{yskOZBVAb?Jc zNz$Ijq&hE&boMBIM4?N8M;df#L`*1YD0BkDFY^R9QBI$3Vc^$9vT zCgD~Vu1Bvx5_DdP06H-y;kViCn`%z=e!D4R7?Z9-dh;qE;FyG;gUd6JpHg)n4#iv) zbVxPTB;}ZNHN#{Qwj7f_Gu1>nCNV4*=K95aW&+JBHWbUEpb!ZG#-zR|KwCG!dDlo) zRmLQinsfb_s$&w{_Ds=y4U(XF3Ic6g(R^I0c^);J%pS%6QsqkigwsE$Bn{j4T87DF zY}vM-n@XZ=TZZMreO@Iqp>0==PdGv(1ZdmUQ&L}#6O?3`9E$A$ML78}5K)dRIAd@VVc>}{_Dz=pQrO6~pnGDN?d0r+n zL1y1rrVt4MWUigc3~=79qcWeL8S92sMGW6)L7?N)VY+` zIWXuX3w7SiFu4(1>ipVt5~WUt<-%QFCo@6k(y>k<5(4NPlrN!WUUwsi?Da}yul3blMZBJ}PHs%{OFVL4QoqDQ27HO<9tO!>*z!v} z-_s?qE=?3);$aZo#bOqa@UCQ!a*hVUnL!4aK4-unc#8&3UwXAR56gA3047r~DN720 z+XRCIII}LjV$Fzif<2<|xU!ZRdxY_Q7Ls6(dlA4Mn4?Iw#-Um|9z2fNzZl<1L-)Cl zVKN(Ay3Y^RQHaua7)IkeGr@N<Wk4WGoeU$L%v7mUh=d@cGr<3(ljXLd^Fbs*=R*i0od-aj zw&$h)PA6%o^I?VwoKkfD&vX){PKJ?AW~$UFL_(0!8Q_1?$#OF~ITsr*{rC|iLFZ!# zpwrLA9!+)rPM!E&A<~-%W8jSEVn0Dfad~t`nTu_PRGW*Dlyk8s7$%Qm%efe?1f_L} zaxTWOT)^kgbq2~zn2X`9P2mol5D5VWvMo@6&cyemV#+TI0GR3u4p~@jZ%IsA()7PVIEh9fNj~)5~9E zm^_Cqd-Lz6ohZGWVYz@iBHG@}1ezmEyNuI9Bn0sCA*uEN<0ojb_Gk^4rQ-KzK|AN9 z7l`(kkOb|oAb@tvNk#j-w7uVBdz;GQ9~8MOXhh_0h*$%Yg(6>Nn7oWFMdIRGoSG;_ zGAtMHbfSu6CWzbvH0kA|5D5W94n>MKaDeeq7@%mvYI9N+jPJte0rYFGRbMDNUqcdf zzJUNbu~scQ8_s4MzX>pE@;x2?10d zl&TCcz6+x=s$1hyz-U zXzie`c)cIdh6iJ?D2`|!F-#U>%MlF^l!(I<<%q_xTtGKahi4{?Xg9H8){A@mNlRqwDoLzb9x{#xl|y0Ipd8$DSmurDaiwy+L5^39<5lB$SveGT zos@%n4tF~uj;o@2ykQ(~8prF(p|Hc+6}51$q8?W(2NsnZO79rQyTtp- z8cGX|>wV)|pj-;OZpy_y-n*&7HC4H8Wk34JxIQ+n50y({*Il`|=Wdt5%J(1qz?Jox zL5@$2<1^#k8OQ3z(O)?f=B*uaaId0&->V$b7X}!|TE?-a zawzPaa&WJrFWjdbGDd7{9Ge)&M#`bE!(9&6gL@Ty;r@tYGvnCYI0h?+!o0IV4(?TO z%!#zDYa9cOV;$vC*flB#_uA-BbCv5No~O1luC0x0OXX77^;0hHxl5{^ryfwQBgnO# zacys0+bWmBuD^0|&y6HkIu1Xm93#lFlW`0&jvbXlVTT95X&>&n!>iV3o^p*N*RIC3 zn{n-;Tnf9@m5Y1sysBIeDc2No4K=PkjcX6(QrO{Qh4tc|yN+C`HvDRX*ti?J93OOj`7BEu5u{sHc}4m zIsBrf%6as8<@$nL7Z}%t#&y1ODeUmpV%nN}4o~A(;d(*2mZF_5F|G;5b+K|O%r_R3 zi+gUFjICc(j>XAwxp7Q1j?0uoVYjJraL+AS)z&X5*Cyn;(zvcNu1U(Jup6vg+;f97 z^}(aN^u^7{G1)k#7{@irp)h|#N=@A3*JdOCc||$4B*#?axZXIfQx1jQ=E}i6w{?c& zRpnTh95))rO~x@zITUtVC zuPfK~jn((kG zIrbsPY~#4sIA$q_!fqSo;2ysTRz(w5M96gzx#k$xT;sZ5xfFKWDi`T=SJnVYj1lagT35itMyNIk1e- zPtDw8s&T!nTnf7(%EdjtB_gU1CcWgq zFO9~;Mq%Av5^O*yz%ac%Iia(qHfpBl$! z#_@@ADC~Au4({n%R(c-&k8;Sh(ig_@rEz?&916QVl!JSEt<-A#K}BfeFIk^&jO$zD z`dYaZc0-kmdwdotYV%K(u=-wOSu$w z`zROp+*MW2>rl*k;dVenskWo79Z86_BMB3Q-M-4jJ-&f8vNalt99R-WC< z4u#!*%E3Lp{jZ9)ugp%=)?i$njH{z^DeU%FF7CNe)Ry`pJc0E&o*auB$706OSveGT z2Pg;k+=*4~^Nn&%C094&>TXI-nUO7G^$Fjz;oN+9p916Q(%E3MNIXPPO zjUSW?kKJJ1$GBE9t`(F^VK-d4xaS_o)aS>jK7EX1731iw916Qbl!JS1)aQT7wJ3eD zuW>aR*Q&~;usc+_xaYcL{P`#4_?wzmGmh1bqrY+}>_#XD_guTG{`|9Y{YD+l-5sL!v;wJhzkzHx0}T!WNL zVRwXbanCJZRoidMwG+8EHm*&KYa``S*d3`{+;cmVEA6YlE61MX*vvRKH;%!|p|BgN z9NcqzSFO(<%7x`{tOps_*2cA^aw+VNQZDYfTdQ*Ysa#kB#Cni%ZEsxLDwo3UXyxJ_ z-vShA`%5{nl!Nsk;}~KbJ1U35Zj^FxkMGB*($C7ubr89BHLl%^YZv8G*cFwFdlmE7 zzm-GIK|_sWPvh7_ITUusCpYT>C4R z!mdfVxW^BWMQwtI0?09f9K(!bxN#h;916Q;<=`GaU|FS2aHpPJkFjr$Fs{Rl>rmxV z*d3=_+;dN`Z?_spamSuqINe~~$hbxt*AdF4usdG4xW}h;t8n3teQKvs##J<~qm@fx zcY<1ZDb9k~T>R*d0hxGSL zjAMdvT&x@l^J~?zPDGCCa|_(TXMJRC_j2Q!Xk3>mm%?tWa&fO>Z5Ma&$?+d*y3#nV zGLA{gp|CquIk@LO&1k|Md~%4U$;L6oIId9+h4~F?StlY#bxpW~PcB)HooZay8`pKp zrLa3)xwu!c9*aBoEagy`-;t&!?zJ&b!X0~Z$y)9lKUd382 z?%0##Pqx!M<9Nt89#js6-8kjoo-1eC33u#MO^+JKW5)4_awyENMpF~_+R%hM_vDf_ z+$W9eDdT!VxfFKiDHr!D)^Ksho*XN)ot`m{XN_aNawzQ1R}SvEK5VB}=N8N$$hAGW zUNEi~jq7>kQrKOfT->WzXT+U*a_mlySB&FT<9Jy)6m}OX2lw1i)+hBF+_@)*%t7BU zjyH|tb>&dlU8Ee`Yhw<&ymHC<**nJdu5rDsTnh7>kZdpRRji+_pd7j`WE}4s#{%V8 zD1o~~Ik>0mLal7PqH@Xl*+-J8w)C-th0=!-CJMU=%Ei5k^|O_fV=is{sd0Q}9G@tM z!u-G^HF1yc?Tc)@vU12=_zUCs(l|a>4u#!i%E7%h=EA*|3(F2zA2P0Qjq7XWQrKOt zT->WzAL^qVxLm~gka7HI9N#O4!u)z0HF1wW45;GAt0y(Ro6?5D*l>@K$#yXL4EN2|cD2Kvss&a78-IUQZKsjWNyP|QdWE?9f zhr;}-9yM{VjXCaG$|du`KE}0*arIU%h20Ix#l4F8;M&UZAvN_ijz;5HRXG%P)0BgI z?&FN6b(BNqgR2?G>c-JuITYqMsHlm1ZOon4RW5m6V1RM0Wn612m%{EQ<>H>pJ})p( zIpjKYUE>&N9P22D!tQ3};GSNGroOjcq-lNQ*uXdjDTl)R5)(CXuMJIuluPEg8ynXq z#}wo*D~H1FPUYa9>z>gxIMQ^0aU5tI`zwdS?k?rvUK^S=Q!aVtVVH3ZH?D(~ zOJR4na&b?ec}Q)%xpI6>-y2~ZhZ)DA%Aqj7O+>%pp8GcAds{@Bjx>&u#&LvlDC}k_ z2lv|0w54*%ynU2$6^-j?Ud6n98|B!A^%-p(Eyi)OawzQXR}SvE-7l{axnzCyG~+tmxK33rh231`;$Fr2>h{VZYl~+Y$Jxel zrgA9k9#9VM>DprIdpjtH%#p?!$9Us7S2+}R4=M-u+L$Bls9bU_a)EJOXk6zjm%?tI za&fQXT4X2XkZX}kjAMdvT&x@lyN8s6du?2c3{ft*7P;KGCK}ge%B8S-Sh=`YaV@g5 za@RCb9(ES*_kDk#$2y!-bx!rKPjz)qcXh9g zahZ*AiHTv%@uMaNwfMfoxEX3<=(orfHpZ1U#%vSAnEC!b#|^bAzeR?bSXvJ-*T%Zq z#+qYd8FT!&iABw<2Po%!xQX!unXa=juD3C+H8G4ie!|3{#;*zo=Y?HO3@p{d`X3wP zCL7}h6T_I}1ttbHel50%<9mdOrQcV#+E};QShtv1#vDIsVo|I3eYKm3fk%zFpJQX( zVPo8GVifMD zjC)NCV~!V^7}Vk;o7|pJCe}ra^^lGAu#NSgiDk_3GbR?b_!7n{=X?(n<0{5@%*J@! z#(31kFy{DK6N6fnXXc|#tlo_Eq>c5IjkUnUGUoU>6N_5hC$T+ynixG9;~5*{SsP=a ziDAs~^CkwhD(lC_m{?md)(bY)i#FEtCYCYBFPK==;=YOP*(=zdmu!reZH&bxhB3!4 zni$l~_UIk@t0MeOPsB(!^6g4Io8rrscn`HtuJHrBr`fvUhcrI+QHy6-{Mg6)HF$m4 zeC&hG@v)C>n$B(#ZZ?W(#Pq6T%Pq5&-C1h#%o+x>X z@|{OvYoPptqIexjez#t>4LjVOurlEtDGSQJ5q z=u(*>g^_B_46>e}` zbOBeZkV|3Y53ctS-W*ZhjPE4Fiesh;E_RFWB%6AEi-5<}cyaNKU~usTVRKA<#=$of zpZjlUZDA(i>9&PoYrxz?q4$Of^QwiR6)+T~!p#nbE`TY7FbX4o!1&Mncysx?N3F1- z_*Q6eOx=ZeiEsOmWrA4v7A;u#QfOiMg)dD14HmWr%Oe!UhftE` zUzUYduuudnbU|KL3-h8d@=vgMb2BXLHNnC;VL#>NQB;A2FDjd3hhr?)_$iMa%yrp@ zQB2V~Hbt>d5gmc`1V!;Nl*D@7V$ljLir`CubO9D_O6x-&N|D0IA6Q+=De`9frNAbS z9d?`eR-n((0#t#EFHirC9I-WEo~9^1iIOmHSQuIXLlNYNE|ob_82Kl1|QyBS!#m{Np+{~Qz0^%(-r|qx2EJhVr_|b!aWjI+@w|UWb3eDvkTZ82lisDNs z$?_k|LMvD(g0B?P1uX5tyeN$P!P2{&7jJf7DQqGy>^1R?LVvuxiYl=1;|l>xwfUK? z!SXtVzR5tAw=4^-V4(<(7rIm)FA5|7#PQLBQ%OvIH4OD@JAAty1c4J;T z*}Qc1#|vA7|lNd(FI_eg@^d2_Sd33jt&ynN2mf9KT2W79=RHDb3WV9$Fk%+3tNNb zQ;OopD9Q4kWuX-;6s5wGj)g8@*&<|782N*xuFT@i`M5s85?tr7*G$~PvoJ@K#qt@d zz`~DWm>hkr9DT(cWtF4v_=5pA{*|LFKV5I;I(mZZON!#>D9QD{<)Rf_6s5vK$3+)# z;m*H52PusF!G%A7Wv}17IUm>Q1N=?oh`nawdR6OpH;<`<#kB-g;QAWE98=$l>l<>_ zgR9{Zer~g$#nK;pG+1;5);APIeb56~A6P6}fkhGgsYkj13pf8gmcqy%SpH8vdb9ge zk4^nvV7H0C^~kZ;Ag*sw1ulLp#EiY~Kvebj0$T&-2a4i%D4C-VEex%Ip$N{cbOB7_ z+^R702aG?rdUG@9R`$Box%CjS{D>;B@S`OGOSN+=TZ82n3Vq9jEFW1GTERjQoLlJv zmc+SLVdM`Me{S{WX3nkrf=c4t%D+85MJ&If3M~A13Rt$zH8L-`#_yS(YSTA;K=cQN zzN|u`k1Y|cAfgDSZ@PdeF?}nH{6XZWZ*MM7-{rA3>B#at$1l1h=5LO*L&fwbs=&k# zu>|?4cD=yXpo#FdPg>t+AkSXxmOTcRY(7nX%quuueJj4qX9Okw1o7-Qbt%ot;@ zON}v;mlaV3mX#p_mTGxnYp}GYD6WK(EMHm{TERjQCi22- zoFkQ&0;<5WDg;R&Y@S*P(O)SK>NU zVdM`ke;w-0&0L4Bf_O_^haM%a)ldbl)gerd$W`?^l&!(ijzZsMBFk5ng;uao1lOT- z0ZZaKRAJ-~7JnV;&COhgwne<9u0yAZWerq;r9Fhn(J#u;&&<(U%2BoJP(Hac)d5xD>I4BU{I>f|T)&cQ zZC;07%f4GgbC6-NHRnqR)W_Gb4d zx69WC!TtD-2$=Yr+dKy~imNlKz_l)f8GFBjsOohnTLUJh(D$+k^R0!U6)+URIfyQR zNt}ZeM*e{D=OAxx<{Y#R;w^O!DvD)2RDorEh=8TqbtqedWdjO*bBin(oO(n@D_AIk z>rlFYC2<|9F!BeBzYg{0X0AiKAl_2fq0`0E6;)u_2m&m)4rN~Ow~d&cYS*Fk0Z}&! zeF2O_c&sNPT0uk+T!+#HM2YKAg^@pq{B@`|H+vnrAtEky9eRwIx}yqAJt2bpR67^3 zHE1@bDE2@}8a&Mt4XvP|2+l=x0ZrmMRAJ-~8h;(?&CQ&P*lXfk#N)=W^gFX6As+5pStEV1~xnmZ$>DRuBQp?p3bc z*cvQbQ|McDWck^$& zc5jre!7_kC-^?S+FP4Q?uuudnbg5)f82Kkyytx^cZ4hrMmSdHdfv5t@wh)HpPtIg` zn=IR~y*+6j;BSbsHCVQzC=NnNmR~Iktze-j6|l6|+&82PSg?A|-@H*6`Ge)#^21(l z&d1;BVXnI;72IoMubKFJ|E!nyq-Kg`Fsi_^0|Z!bEl|?j{1?i2dtM71ODwDp)2+4# z5yIYTqa(0(q$qBWl32f4ELwp@Q7YiAJcC6SU=0hg6h{8Qdbo_`&H4Bdv)zs>xE5fy znfOu9#X0RbaqWaEaP0zNazw6%8HCx@!aU|-*cvcHDD(|N!u)PwXax*Ksen)785p_% zW<&_1F!Be?6J;20&c_S1v5qCk(awmMiJwfe950rkr~=Dy2*ZLK)in(mE!l?AmgQ&9 z!q#Bfm7+KdC0YKkEVP1!qEx^){|pOVz_Mq^qA>CY%dce?Z_dZRRbyeVnfQ0l!tr*3 zSVo`XkK%aylURFMtX2IjYdQjJcM5%ukyw9PELwp@Q7XLau;>D; zu_2bi$RAj%m2X*lb3Sfs?n%2G1>^YZ0b&0!Vfg4twgJzK#5LJ-(Ggq+Q4|kANv^czq7_^er2<}2FkEy2 z7ar7jE`^alxEjh_-kgsQsmjG}Gw~G9#kt}%aZN@QxEdhL@kOqN-3fz-5P~_>!>~1A z4xuO>jFK=J3qvblC`yIz91L9mgQox7lq9UNgtUT?qEx_F zfK0mR0z%xU@q`K^e-KV96MAz#KH4BOb$qekOx)-hd3>EA#=}ts#v>ujIILi7!1oWc z4fy>bu%ZW}BfyTLC?0{5V6_&ER)A5I3courx&RC}0X&$($RA+Gl)=0?AI~t`?Q$0! zU+gv$AM3d|4rhsL8mhq62w}z{9zxbM;PNcnfFFi}x!uFCHDHPq`UWjwvKEF`z)+M5 zcwgUSi7tS_F8~jtF!Bcs{!pTQw(8CK_)cxCV+qFL(TJCc=X)0B=uELpM-^CRK$sjY zr*YVV<8TTGA1>6g4Y=kN)?FTpj=(yWqIe8SV&yCrt-zuv6|gYCV9^CwxX|=i3L}4D z-BZT$=6rmw*>1-b3fP3|#<&s~8WXF!Be?gJl?R&c_dFV;xJ7Blen!ANDNF(b-}-5mjJ0 z8N%eKrE;`9b96X!v=gy#-WJv)9*d5^I)$Qm5=vs#Su9$CMNul?)j*RYx&R9&NROp3 z@(0#qWh`&b$B&!sc3eS@*li|$!gDc4=ZNc6RDtVs2$LgnHS9h5%XtuHysfD5){5h;k>d?d8nO)-o5K3TW6=><=Tqpj z&BR*HV$ljLic$fe$ur}PF2KTo@K_2Xe_)l$Sl*nE|1#U{aukfWa}h8TM{XTSP4o!3 z&KK7Or~=nT5a1e}YYe!igDcl4#2NmdGfBIIwAeGEP5Ha}3p+#xyQP>(Nms1okMM;#F7KK)zP?QSzPL4sL3s5c!Q4~i0KxtP-@#cKI zhBnr*1jiP8&BSXaS+J>i?`Aftz;Y!-z_L48F0m{H&%)MVxr##H*(S>hmW5WZP?QR* zITpHr<xc zD2j7YlBJbpp%p9?r9wN$LKm>i4p|gN{$S})X7T2H+_4%9d(Fh1k}MaCN@W&r&c`cPV_~nE zxOI{RLkM}f0aakR2_j$_PL{crWo^&G)?m4rLf=0p%Sx7oRsDjA5%Dr{>{&Q}V+4cc7F2=dHVClb-ehad-z#(eK8rKj<;1$yV)ge} zbOhEsisG#(iM6uDq7_&ar9x+iMHgUQ7h)-l{DC#FjOESwc#zp{HxC5!H@nTm+j=fu zFI*m- zycIa!F5q~>(t>Qm9Tsbx$D$*!9-t`Rhmu&USS(tBMNukrcUW`*)}0}i!pI+3@#cJde>E2Nnu#CqEIfARh-D$F!164FId)b9ORf=1neYuI9y|Dnl5Kd# zVjbuokI@lW&ruYgK}oE(7K>J3QIrb(9Tr`H^=ycxF!Bf18)YnS&c|<>?RMiWICj`= zCjPhQ;<1C%C35sUs=)Okgvk-P8lEJ~^A_eH55v}gSwvBM0VQEpw=lEqlaN@z`RaTd<`XG*03@#cK|i#FD=1UX`_nfTWv3$A03qc>0mmVZMSmNm$-9H05WW?5?d zcw=j@{D-3WCQ7obX<29m3q`50i({b+SpF5VD2)8Ul0|s)zN$Cp;~cDA%J)qJ7WSHn z^QzYS4$KQKqrmbOs=)FNgkf2eEY;p4W^1s#OHq6qC0W{A7FxkV5v;|a3s@3sF%(Ar zVDW1)yt$dR80>YawHVinkC?5&@*zd>eUxNb%d*f47K&hv z3|+voT9_Awkv~}cdLM6YW{nJcU22UCE-#Unk5C1cPap!8YVUQjHCR5SD1MBREFCNh ztze-D-s_|bSh|EP3L}58`1d-!xtaGm+3Qm8b>iw4ET5qYEMGtbEY;S@u{Bt}q$qxl zk}Mr93$0+G2-eBb1uX4C7KM>NSo}IUZ*FFt9D7ZyljHn+qga-p3M^kkfCayIu*ekW z(Do?f+c`g<%i;Eh9d4uiG(bmSeM3?F3MH{RSu9$CMNujYbJGA_fc0jGr7-db*68vy z;LZ7XPjj5RIW3r<*=;5slbi?e6975-7FFQ-9>U~^Tn*%Il{xRHDLar zDE@|$FzZ+tS^+~*DvWk8bO8+BTJtapBY(ggRfh5Ad^}AX>sW#uvDZv|v}fUaPq&KY zPgH^BF9@(;y=f=q2uqu58nAvR+wh4c`qQtPp$~{6eD@V^3PB;!x|WDm5K)v0c#3J# zLl+Qz8WJgt{6X|rdDVgj5~5QTuK<+o!71E2R#g1113*V%%WtL*0V6Q z0*0bg81G={0vIg5_v21s9bG_#rv z%~(+=6u|$`n+=6vb6g zlBTPrp%pX~!TUCJ0nHZSXj2&ZgT}va_@7v4+%Y3n{hAObE4gr=Oa*bV; zpAAsPS^R;WT;qm>!=asRz`_HiY7?I-x&y8qMX@bP!fj~bXayWasW8Q*iY|b|<9?qi zg^@qtHY=yfoAYsRX2KmuQ;#b@$6}|Mc=KeQaElFjS_4&JYY$=aM7D;BM8Sh>p>*>o zYz>sPD2i*MB+5n>g;t61g(Ih zC>8LrK{Msi1#oyM>ERSc{($RRhV$lpydk&Vu?2ZzrhPwX|3C(d`c6^cBqhbpkF4*?cj z2lY^%dXi{2J`BdwM2)e0zO-+WLl+R?ah^|(!pI*) zmz0y^&G{JT_-f}P_M3?>OXdf6WWm%GRbbi(!sLfcT%Vq8z;i3noa1TO8Z_M~iW{ON zO;1ZhD`+T6_;kOep$lm6w8+yajQl}!UYW+5^D*w)Hhn%~ubKD)&%*rNFP8480!vQ_ zlb?;1pI#)|nM8OpBBI%zh&~|Nn4;JNC5bk+M6`m4qEtB65zz%icpl-26h{6ay0T2< z&H4B$v&l_eFR;tD2kh)Buy_%Ln~+~g8a~> zGCv9<|3rSgxtaX1*QN6FpjdjN3M_pfOnx>|el{f$t|xKl$L42=&kub-v;{?RbCe|7 z#1hd8B8pPsc$Xi#fC#tpeSQ>1{vi6goF8w_$KO=X5Btr;-}?M;%snKgEl~xgtso53 zW@741rrCUIhugXubG`jlH+{ggHAS&6N-}L~nP>$QMX7L-W1a%FpH`!g{W31MY-~=z32?9}sOvQ5=MlM7=E$tstT(70z%(bO90WKX@XAkw1tQ zmx;VNAHQTaxvBiH-%R{+k_nHoFlP@&6_|E_FicyBX-hJ_KqmYg71PI_i9TT3k)pUg zN-}M3nP>$QMX7L>W1{JDUAHV^l6#NoAdE!)tT6DCjQ(ram?Xq8klxM6_|E` zFk_BP4U0*G-#emt*VC{yXogS}cScE?K9+`7&`^{L=QV z0*fMe?oAh9C7ydLjQoM+pL=_AbI-lm?NZOZpAgpwRDo+Ggvk-Ps@~ILYryPIQQQqB zVYaj|v;u}AxTi%Iz$ETzDUAF91$bN1q(%RT}l_QBz`|AjQqjke?NG0GuNf;HE~_a@%E%x#-Iu;V!MR6QT(rs<&XayZbsc@O2qYLOT*LymJkw55;FVlH*K0bk4@7RLh zMeGzy!95%Ph;{U|*e0L~Y!e~CHX_%!9djl?jzp^)%56!x2Y>zH@Db(7{sM`CK)F9f zaX*x#>}M%y1tmqPaD}6!3n(#uJ*C3PAC#w-FOa-BAD?Efe;nsAXOw@Zvs)~uPF^1^ z6z3#Vf%8BJaN;-9VA0`K*qVm(_{0L6Dx~fF6AStP=^%>Y0Vs*IjYXmrNED^Q9EU^~ zAfb^*QW*IIY5VdM3vb4%Ci6(5JT8NKcnMui5h%$u&~niVE{am& z8aD{(0Gu2&;6!G96W5 znE_#PG(=->7mhtFhp7GzGaZ3-EJg7cl*GdB7Z$C+q6pq$rVFsfhPPi8M*hHBtvvU6 zb93)7v)jZw%;b7eTr*JxuHzw^=88C{4@P?V7a~?d$9C+|5nLxw6puqmuI(%rt>B_4 z6|Q$0JB zhA?A~`Ko%4oUH+K3PtfGl!O7al0++DD1v+BbOB7_9=XEEA29wNxi>d+kDR?G?vXP` zSW1PlcPgsDayo>`(J+m@p&WZy@`3l_H49bXIt!v{F8qPoY{M>=YYWds zM{u1@Q9KhRxpuHzw1SJGRJg@)(FI&XLN0}oKe+mqxx5)mZK`syTdcM5T)b|_(lg}i z98`hpJP0%Pn6IkW?Q9L0^C^nwq9hEGsU*<~7>eM!oi2b$T(>KX`~l;y+r7D&>vr~< zxNhgWzF7GNmJ3h?mWv?3f_Le6)z}-(v4>?Xcs`?lVgXI{cj*}egcnm3FGNW~j4%<> z3POtDU3$8JaQ|=$R2cb#(7#LX&CR|`&wdl{(v$I3Fz;ZQ&$=7ZglOs4Lu}TPIQvbxtm+J4IGX@B+p(xHpNkYsHBBT|B z6v6xFbOGVCa7-$U{6RRaoG))~_Wg7An|S}6WAb$|UW+O)UJtQMMl9mMIlzv|LD&d; zie?Nj{)3`;9ZE6|wT!fak)l+%!;MM0fN{5QOe&20!MI(S(VO$}U^C>J8k6i7=X}$^ zoujdy4SBo)Rbadc!i-7gvFey)YtYcSeW3+;XA{di&sT`9EBmcyh^yX&9 zBzsMaNuJBFoDM9vpb9LvL4XCnO?KCq9LX_>B|gXxA09OEE;=27HIJfrD@tNvP7oHY zz@iA=MW+j}jtd8(!pI+3{#|r$Zth)lcAIz?om~GG*X^hR*BuZ|bL~#9-Rjxhl z!F4A^@t-Kkh4<^kMJu={N`<@J;G+w;Mume1z4wqV^3k^4=n$Vy*D@ajy=0gykk$Ux5f1!s=)OyMAKYD$TixIz3cth zqa(N;p(s9tl3aLySX{J%i=tGx*Nr{8fNRfi>?w@=!F5ASxmg^_<^?0IuDV~@Qq zHTJMX6)X!-1(v5EOpf-{*c;8U*OFroEBcyz-=2=ZdYYp6BuZl8&=(f1z@iA=x2FrR z&I!k!!pI+3{(XCIZti`1cAN3<+ncqQ?}=+6s=)OuMAKZ~l54CTdq4ZJM@MiyM^StR zCAs#nT(p9VqEvXmjXk=6YwvLEDUAHV^=oo3A`?U%qe{lKt zYrVO-_iNd0;{953;Ykd*7NZJWFGDoVbtSpR*?jf&ci`y=u2(3EFQFtCrU7x$3NDIL z;bE6Ax`1onFkcEIe{gMGz60;g`M96C$KJ$wirwPIu)hb--;y7S>s3^N>t7Jy!aX$R ztLk%Zwg$}W6vfw25@w8rp%pL`!E^AYfAh|vk*ITFp*EMe%)<!|%$RAuw%K7rN9bDhAMD<0Rb+|70g%F^$BbZ zm@g@cpQ9uUCP*cTR=`jM>l5e#n8f-7g^@pC{Q3lMZf1P~dtGXM0^Vmpj+USbEMG&I z9F5o5+m~an3&&p7_oCndtZyiaU!f!xrfOl)3M`87y(qP+crOYz;}5Lhy{NLe+4rK* zDigjJ#j%H1E5P+Fs=)O?w@=!L>{IfulF$lMv>4ViRMJ-Qt@N{&^$0z7*Gwr~=o|5Qb|%*KjucuY-jO2L5^?>C zDscS{(KHuU=w}-awD}t2=L$N4>ko?JZz#!yX+T`Gf{UV5c-rNQF38tGVZIbb{@@y0 zo-4dLAMagtu3)#Bc%S54feUhs!#`04uD>9_g}GuPGS#pF2j9UKrlTKxYz>$Qe_b4J zNVJU?@t3XB`Y(0Mih{D2)68vvzs#d2>GQtik2RUa)?Sy=LNd{Mh3)5>{k_ zC54{Bl7TQR`;(y-+DUAGqHN8CEyg45qQ+2$tTYUP_k2iARhZ1sBhbnNj zfG}JKi3{Hdt%(Pb>s3DEKFV^X{qHF{f@?X7Vm(T79bma=1s6rB@QUN23%I6*TnZz9 zaMhH5PkD1buC4leirwNb+b4fdeJ8HvQ3b9QAPm>R;+jmZ9m#bpxrW+nqzAD{_F96D z;A%xtY>ARwm`9W^TERsTtVy5?xDsm;6h{8w@@o>jxw$n7>^89`f$N&S7uSlY0@unA z;KI753a(sZ0}vaIBVD7-+H;|G^ z;#vb$;A#(H#$N?juJKSN?JthMW3V^*%W54zYtS8ZYf%)}L`gcFi$q5&=qO5sH{JN7 z3xdo*u=45zg^@q#;_|HF&G~pe&I;~)HPyct#!m4$1V3l+eD$-~I-m+{ogmEkBU{zA z32Y6NwJC}nQ4-}4i$W_d)h}Y{j4H6K z3jr29AD*i5cNoXt(HMWZM*XwMp;+x(>x==yn4-82N)qDCtz^*(LW|f;Ttw zJdV9C^*ru(v2;fjSb9Q$1<&J-)VMr?%vuhOCZ?Hze%!_}Cu^+0y*dGFnm=~)v<{I(w<(h`QIWL}I=fyKH zZtTS{BY|`PMR6OHB*l*b<&svAQs835&5Lva>51XIs4(&e>6ztW>CO50ELiq9GK;ol%kqKVXy{T0umCzry228eKqiN;uLKM*bjrxjfRmIUm1bhEn;Sd$4Yw z{bu4fCcB^8OqNwDC5UC@l{(BLq}lkPEp(qC9$Sk zELwp@5v+-!3$PMvViZRH!18Niyt%nGG3++6CWhzWT5*j+6}U!2fD7l~3a(t^u^fUU znY7dF5PZ!KLArx(Pm1CmC`pIkFiIM&prgPaPjW+$E}%O-9D)iXf6%>N9)jMSkKf=3 zYvLTtPVpVEvCe*2UTF%7s9a2B+K?p%Pf0CCiuGsYz>z26vc5U$#Se^p%p9? z!QVBY3s}w!=Q@RvKUi)pr^TC_`MU<}HSu>1IM?OHG67X!nFs+E%yneRH6BNz9Y}Px z9ca&@OM6zO4~X`sDDH=nMEF&r?9d7#ic;ZoH_+$;qI1H5rZDmc5gz&3n=sy-kMTw? zZ?iNt*RkJB{Gw^#?!(lHX%ecybRa~KpK5a*TZ85xisAt%NrRt7qM;Qu6v0@dOXXNo z82Km0nm0Ey*4S%etnvOPe$8Oso{TE6G(dm_bKMEb&+#Z@JhH344u+1vI)tKlFiK(_ zZ?R|v7DccQhAvD}IP4Tg{=o92Lw4q|pjGic(>T8-jEJ-TC1VR2cb#Zu#ZQQ zEqT|uiMfuQX5tl+_oSB-+u^7J+mR3^XD2IXCoyMu+)#Zj3>`sr6h-j}l%&FysHkWK z6-BTXhAyB=tc6h+`Gd-@h4JR**21vc#9A2U3?IwESe%9`a5X}hoKh=v z2-)Y1b9zg$O-B{jWubOBxOaQrEZ{6Xj6iT38|??kiH#5>WP z=T{Wlsi*?m=@3oltlINAI)ds9isES~NrkIJ<&0KPQ3TKD=mM(5^Eri)KdAikId5+6 z`5e1l>iOJC;+lmjaGeEVxX#d6oW-%Y8OLI^wOMoo*Vz=sGf|QY7pmf-6c-@3wIG791K^3^pg8&zGgv&wmko#G>%$@A7KVw;UBuw4lOHk`M}R&{L_ zTLa}PisBV0iE@@jp%o|;!P+dk0A+&^MPcL*6u&mho10mi#a|(RV;H*1(vHJ z49mG>srJ2kwg$^J6veqH$%0=injmNe3q|m~db)rGZ+rPiaS9`Uu=wxQdvi12t7or? z@6~gDXd{+uQ3aOkA;5z9fh@Vk^GJjX%`88VrH704egz4AK=coa;&muVgr7_zq7_6G zrNU2cpwR_Hxa9Ig3L}3IEmvMa;?4PZd9y^MsriBZX5yB94GHtJnwV}t6_{><2=apq z#os* zwLR=L6K6aN&$n&Gato@!avKC#@Vn{)<>!2qF)l%>uLGnbu;x(|Z$(Kg{6bNZXayEU zunv$ez)E}{U18)8EWZxWo10q)$Zj*?IzY~KtBdP)RDtUb2ykJptKiBtUdSPMK9hE} zP1?TLLH4IT-9dLJMe&~~Nr#_IqN5db6s5u+E@^ZD-8Es-6h{7_n@~=gH{+2hv*L0V zJjY?DcwU;!Sv#@KM-|xahA=t1SUJ0hIm3Cg`dUCbg6bZM;$0|7b%CX#6;u?#T0pvh zD)GH^g^@p~{8~V7Zf-3gyG^VGWX{$Q*S)9$*ZmMCXBAwz#!Hy9Ry;plXLELvKhWq7 zx(6tV_n{;met#)vw1SSJRQSu~j4q(NKFpcI$RBj4AiP-<= ztcl@qwx-w~L>1T`hA=t1OgX!hIm4MI+ko$dW*cs>R8M&-I)dsEisC~kNrfMRqM{X4 z6s1BsZ9lh37f{_8QYno5LA9_<<;_?gW47CI1#8&ZEv_d$7w5wE;(8QS;CdVaTsZHN ztLnGL*%~lUP!u0SNtlZ*46T5n2);E=7r-RGHLfu72aNyLxHmWRt#S66_|`aC@M{#; zvI|fJmZu;L%WSfY;LrG*ZDQ8*iD7H7JWWx25+zwKu`INLg`!lbb%~)1SZ)auqcHLZ z%lhTScym6+s!Yn}uZQD3<3@1(p{f z49k_w%Wh=3&E{o8&%)MVSwvBM0VP>*1u7O=!9r0gKT3*;1EU!=$UqVTi%PkA7V4(=|LYKZJ#Tx#%9R5FI;|;rQ*t1*D?!9TLjSfQj$QGHX(=k!B!Iqim zl^IdgY53GbCmlTTFt&&u%tX;Cu#FfVMFVT1RxowicEsQTkgcHqqgN{Gw9P>c*V#fMxx&Ep+D4UKK{`T61x-sosAV8{4X0lJ#;^usSh5;@~9yXj9(!-X#(v_ zhMYJe8;uxt75*N}K0{fqqkkW(f3K;skIGL~UO$9w(keF`!e|dp9)K&HBX)UUd^S1* zk+V^DD9d|>(cb?cmV<{3z;mxRb5Bfe8C`zh6O*&ig4#ZXm7;gY9gUX#)bfyQpF+#% zzGKcj=&)$Ocy8P3S^B=%pXEd9>z&%3`)8wPWLc*_d*3z7^31Vp6XV~jL^lt6d~Dlj z$D>>A(>5BuFUza8ZiPSK^1<>0Mz@V#$G=-f3v*lT*D`v2G|RsZm@}hov~nZcJdo>C z$VU72W6KV(tP<@p_VKaV=tykID$$9Pw}tPLt)7^?W;FNUnlWoes|;iL#t@eC_GLMH zD9e3hIYYIxCf8t(-B&wc^qNr$mS-*9E$C~_=t$M(M05w*WTTVBG7ftq8(o*9rJw%2 z$8P-hu2b3M79{u`&Xq8cHb1A&qMq8V`G3k^^ZKIEC zS*|?%6uej#kC;3?8|_wPn_q{~(ycZB-4*TEjHVn#qCs1*{OK67)Q(~nwpnY{5o<>2 zEbZs)&GPXXtUYHmd)#Oe%Z7eKT4$s8_F~y~B+IpCusnWmmM@FD z(r(qZw1|32TOw__XftVD)x+}9meP)r)-u{g+O^VFh_;pXlC)OQj?(bwztP`{(NJkP zcS2h!+D+OhX)8ygrQzBUb*-blrL`OK(!`;ln;>l-W&vvZMKeKr)Qkrw^uy#c5&NxG zG_&t6(vFdq-JudB?!PI(1PfDv9L~WC3p|nkprnYJHoV07Uq}C@|EN%Wy)V7S?kha!7 z)cQvMk#>ao>mPk0?fn6)8yI~l?bCtO21Q>>yIs9+8+|A3haFkBUG$^0cccxDevvj% z`(^v+cWG_a`;Jj2MWS^J^cxZ_C+#KmH!5l`ZNVtk?Gbg9wxagSxTvSJx3wn@hz3a8 zL+l4e+e%Am%MOaRmo`^hJvrJ*+Plii!O;+Dzih!C8lvIS_SBY5iT0P)PTO*LG)3CC zsw+mvN}G59{ia99OPjhkwHeVV(hgH@PmV5;Hc+`eCAwVN{bD#fx<%UlVz@YZNZKfE z=Oxjj(oT_fY4n7&6V&@<(Nof%7{UIoj21~7yFImQqqn53sQ#{t-jy~*{oNFOA#F?L zc3$+8w07$K_UKn>M`@q^Gx|f?nc7QtM1M(JLwonmD4k};epFWGN43&+l-~nUYiVue z_h7WDv`6;hmOUD+C2ec%mnWn3rLCv!d@9;d+WFefXQNG}jnQ_#7;PUg%F39hsD~f%cQUE1s_pzKx?b9K+Alxj zZ_lxAu(tD;=oV>X6!Ta7jV9Jbiunir`UkZh^7}KoPg?is+|E?$acSdKmrcDOZJhRJ zF11+N`-<5j^`^9Yv_Dr!eIV@u_0THyv9#~Cohzn3llG(q7hH>X>Ss;dbt~KkGWBR+YBPF4Q(ib&=Lmz4uCODeXS(rA<;>OY5%QH%;}I zc8_9imKr4OBF!PaQ-h^FBihYVJ4#!iE$froMcM({>Mc^kq?I(z`lj}fwzalpU}}=I z59PN*>IiACYFlLO`(YUUf2x>VW_F^o>lmDcVU`i)K9B<&d0O-wx??Pz6l|J1|Mep5CNOT8xT zb?wh%QcI-GS2ky)zLB=FdOtSxy|nKXb7tx%XuLT_tTL@x73`!}v*iF?E-;0UBqEQuj(bRNCUy1Jb%^ zFTIp{SlT1V$~Wm(Qcp-bO*wic^{nwz@2{m^lD23Vdw(tOAnFuoLa7s-c#B|6SH}t8H05eT1~XwAJm> z)1-YYzctcDX(QCbTIu7Z%~M^+^eNJ=(iKFf^y$(jiJ^1)Y-#T*N9(39k~Vi=_P&1l zDrx&EO84|F(l%F)dZgz`o2PxYS^7a~=O{;8rJs{FLgRYt^o!D-Q;zzjUy}BM`r9V` zs`%C-Bcxp$a2T5Cb?9lx$iKeGVWO>O;_7vpvr!Qb-t ztFG<7yNu`u?Gb5pR(n!f-y`Q7baBls>F1=iK8)JD^efU1ICRcIAK@fz~hYiYZw z?&9wQ{QOxJkYfJmBfjvB*j-_ochBwm#q`h(k>;9A8UfNyQ z>WgdMNso}Wt{C1)kCYZE$~);%(ssd?U0m~SdW^Jj@_RQuR@z32`F?tWw1w*LgY+b6 z6Qq5VZjkm5?cI;kQ=~nj9zISVE^VTse3G6ftwDUB;GJvk!5a1cMfyZ(U610ne4Rc^ z+762HZTbRftEq?Y(w9hkRQu(-^kven#Qwaz=7;ngX?G~*kLhcr9i+YcQ~CyJ`zXp! z>6@f25$!MO+og3;%U{#;rEMg>-_rL=J6Zkxk$za(8_+(AqRc{RX~oQD-j>!wTfIW& zJ874yhc=mPp0q0{%9@$h(!P>k=givD-jZLJOgCvaX)krl^p!SN`=xhgn6ytsJ1}#Q zwD&|iJad|~+1l5;XD*bsnzV75+obI#zDb#f^E0t6Yr^lNXmVzuw9}*=oOw|#?~~ub znU|z}D8DJ0H>Gt~e^WE>OY1MbBQu{%yIT9KG4q48
3Q<8R-_>RkD>$oR&7T<}P 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
_(|C> zl42>5n*S{O1yU>}Qu9;wlR_zyQpx^Rj%Z4O6iSg4ONrF{tXxVUg;FHNQX-|2eO@l5 zDFsp}MN;xbIU<$p%d)L01yU$QQYD%n?Bo)k!-6iKm^NU0S3yIfup_E9eWS+)KffP!S6ibPeO6F^MQY6JvBBhd5G*$|v zP>Q5jN~Bb>s+K23QYDp^fqr9cX$NQxz^YpfJVp%h86lt`&$4J}U!q)#Z0A}N*5Qppz6@}xitrAUgUL`o%FT+5RJ zDU>29mJ%tIf+fo3g;FHNQX-|2eWx7TlmaP~5-F8zNsW~PDU>29mJ%tIY$+{I3Zzhq zq*zL%RI;VDJSmlI8C9h~3Z+Pjr9?_4TUN`H0x6UtDV1zFjg9FtQ1MHlt`%*tf;Y4B*ju9rBX0TW2H!nr9?_4Td5q|lp-mX5-F8z zWsQ{rDU=c^m8_|;QXqv=BBheGX{;1Tp_E9eWUFYb6iKm^NU3D2YOEAVv6TJG0r{We zrBbk3*%nHX6ibPeO2O(HD@9T)*&1cPrW8n_lt`&$Yig_%NTC!-v1Ds$tQ1J0lt`&$ z?HVgZQYD%p26Rtlt0ilkUdq*SuCwLB@1LMf7BDUnhsSf^ZGC`D2%B~mKcy5-oW z6iA^INwJhjsbt^N@}xitrAUgUL`tP#bh*4xilkUdq*Su?%CSu;kU}YvVkwbQDHu~O zFO(uFmJ%tI?EB@|rW8n_6iKm^NU3D&Yk5*2g;FHNQX-{NutB-JP>Q5jN~BaW{$^5QpvW|@}xitrAUgUL`o&wO3RZ1DU=c^l`PO$DUd=bl42>5Qpq~BJSmVuDUxC- zky6Qipyf${6iSg4ONo?9wzZZg1yU$QQYDh1=p<%Lot#Zn@rl5JCtZAyU@N|6*x ziIhtALoH7Vq)>{aSh8(3Rtlt0ilkUdq*Su)v^*)0LMf7BDUnjiI<-70kU}YvVkwbQ z$$q5eNr4nfkrYdbluE(&5QptW?j%`YT6iSg4ONo?9!4Bo}LMf7BDUnji zeo~HYN`VwgkrYdblu9;U%aZ~rlp-mX5-F8}pO(uDrAUgUL`o&wu^ii!0x6UtDV7o` zm24+1Pl}{iN~Bb>oi$bpq)>{aShAmKtQ1J06iKm^NU3DIXn9g3#Zn@rlI^OoQY6Jv zBBhe;rm<2Wg;KD)XQ#M-6Dg7s$@VC#ffPy2JegxN~Pw3>LH zd6fD|v6M>9qt#D}rBrGjqkd8>rBd_f>LLohIGGyE)B5vD27)8S8X0vM3vC_pubGZWK7Zh$5%4_cPY%DwH~T z512K3va#JLbee@Cr!fyQ!s%j^IDLgur-{GkeSeXS9Yjs1(GT&yP8Xrb=?j!N?UOLp z=>gO{B^x_(fcJHZP~?=N#3^`~_c%4{cMWPf)gEDl(}5^*nuQXl?>)+UI9-C8r)6XP z#~9&sAc~w`M2XYpk2At4djhz5dN!i^1S6a#qsZxLlsJ9=Nk%yR8rd1yh<~D{)9!=p z1E>2?kW4DaF8heD^X zQS7wOvy5fwW1SvAsnhZ=F!tss0Ar;dC%cod!|!f^5W^Z?ff1y(n~=hhnE4{=|DY%|NNs(sLO5t8DD?C~%sC zLZ@v~#yZ`I5~uI*>|g41JZfH;jZIPD^b?-=3!NT7vD0^Xx-W6M1f@=Op5$x(Ivaa5 z3Y^|Rq0=~?%Zr_ELW$E-JX>cMWg|{RO{eU6yTEB2Ps&A3*Q3~J8J>VkolZsen{4b~ zP}6BUo^cDEZa|UKw|KHGaXJX4PEVoc#o5^J@?=}+bUuom{)tkjz2BwZC0W0RP~fx@ zPppMb(@^a621=Z^;5oI_=_=H`G#mRh3Y;ds&se7?Q0%n!KN#zD9!i})Le0yvvAcc1 zSf__k=(O&KjCHyMB~D+X)M@fZjJ-S?`wR-4Hu;#bPSa8BwCE>{bvgp2PA{S671`L0 z|H)XVD^cWB`INCv`=i9^F=SU}Bi8sAW1V_Y;PftvoW_5~Sf~3@>a^zPjJ+xwdpc@5 zy^TVrabGaj={giUed|lcI!!@#bvAYu3Y^-%Vyx2{D0X@qB~IJ_n-Os~Vg_nDE%P-a zoTj44X$U1KzqYczQh^jniDcKPpA<=nWYg78iljuc->RP!Nr`0Fs-F}|iDbW1KPi$D z$*xmBDUuS&u2(-Pk`l@K)lZ6~M6w&yPl}{OvK!S;iljuco77K=q(rit)lZ6~M6z4d zPl}{OvRlL*1~BH8WgCq+^s*&XU9MN%T!o$4n=QX<(*^^+ng zk?bz@lOidR>~8gwA}NvV9`%zVDUs}6^^+ngk?cP8lOidR?0)r=A}NvV0rit2DUs|! z^^+ngk?i;CCq+^s*+c3lMN%SJqJB~&C6W!OpA<=nWDl#K6iJC>kEov%Nr_~Ss-F}| ziDZwdpA<=nWRI(#6iJC>PpF?1Nr_}ns-F}|iDZN7Cq+^s*;DE#MN%T!EcKHjDUs}H z^^+ngk?a}ulOidR>{<1bA}NvVIrWnwDUs}X^^+ngk?aNalOidR>_zpHA}Nt*AovCFA#qr zz91SkvlWRk#1DzRh{K7~h|7puiARZ7iN6z-I_p8KOZ8@N5ll;Xrh<6inxP#l6aH&kZ829PQ)1Ehs0jQ;l$ZQOw1&nBIXbu6RoXg zD-r7x+Y$Q^M-pccR}eFZ$BEa8e-O2A@tF{#iE+dp#G%9~#3jVd#KXiZ#Jhwy!fY90 zZ6Y9kOq@zgC;mcwK{US2Ium1v9};^JhZDWTRm2S9QQ~>xZQ^rcu|>>QBQ_;|LQEu% zBhDqRA?_xgCWeUjh|h>c7d2az*o4@D*n^lvoIspTTu$6ZJVv}myiZgYGuxbKEY7+S zI}xuhVfLNxa4Ud#nD~;|VoCM~aTsv{aV;^2SbHh6pA$ zKZyH@HI`#6F^kw}dDe3U)_J7a@kC1Ox}w=1h>lTgD=~vuaV0)0;wj>gmASn`oI_kh z^bxlc&l7Wr&x!6P ziKmI?njDM7gTxYRnY9zsh{uV=+SwN3Sz^R@`Fx1Gh}N~)SH$_mTwR$+QsO~kh^TGEHWND# zM-Ueg_Y!Xr%WllN5R-{ZiKmHw5KC>s=Su8NM8t22Cy957l{e-1BBl`u@fOjtnb}xk z0&xm)9Wje|pIBk6*_On9#OcHh#IwYQ#4?*RXA)D03y7J-tHf8tT3hfPBTgo!6HgHH zh{d*K9w2rj&LUj#l&>tR^oo*31T)ehnPov zN>qNt_7fwCHHr0!t%&W3-H3_AVZ>D84B|rKYT`!XZsHN*IpTHV9pWS6YhuLqY(KFo zF`5`lY)kA;>`xp{oJgEW{F;b~n}~aeM~UZ&H;BIy9~1Utvu_j25vvjF5t|d+5kDhB z;t=9E;#A^%;tJw=;!YwVo+kcCyhZ$j_>ySZf$t^JB-SA|A;u9uCH5u`B#tH`;vC`< z;aRqTbaVL=wPZRGG-}x!uHDY&S zDsc%hNPJGT@5nh2F^!0cM~J@=tvj)Qh)&`V;ymJZ;$`APV(FdvS`yvFImE5Ri^TiH zcYenE5l0f!iARXJL}eF#?-8BEA;fva?ZnH(zlf2$GRF~J#Hqw}#8bq3#NxYgtP;Bt z#}ii&4-tPRYP+-FiS3C)i3^B3iC2iviBWs-c@T#YzanN5ekHo)-krUVtL>F-?aUJm#@gA{w$bKMpC5|VqARZ$AOw_vA55)Gw zp~MBmoy04|=ftS}*bl@+;&fs<@i>wFo5SCp$oH1mi8zM1gt(vh6XEU8F-VLf4j|4V zZYG{5J|dQx#JL%dHKm3Wc( zgjnuCJ{w|RBKvnYe@kTln&w+Xqnm35Vh7@I;zHss;#J~HV&#MQolHz3&LnOmo+W(q ztU|7mYvek)L2e5Q`?^<9B5AJ6_K_-fBO!+iXX>({{3ZZD+2~ zerCV7UF>1o)gEDO9<@E}G27D~x4rB++uNSEee5OMmn*ml_Bv~svX*bNmVaR_-(fBP zYLjf(CUbRnfW5~W|J}Oz3)~0Ur*^RY%MP(GZ3>6gq2_sqneQE874JxEct_b1-qH3Q z?-(2D{oGdbj1&JCCc%^X*{o0z1U}l}+(3 zv_rj%>?rRycC>dfSDTmERPR#j@h-E|z02(k?+QE9yV82StL&HF)pnj2+poPoyU4r7 zF7>9{mELddD(_mm+WVcw-gVrRzMdPpH*lqTBR5WO;-=os+_1Zan{l^tqwO|sqTRub zuRFQ9G?N=McX89@Zf%Zp$`yqSDOYBW=!2aYtVspGl zE%hF=x4p;hUGGVI&l|MAdr#SC-Yomvd)mJ6p0R&>&vKYQXP*CpRs0vN;m@{3{FiJ| z|7Bas|AUS6U$GVaKiVk&Ra?n_%~tVWw>AAYY%Tvy`=0+N8|}}rz)$U`{*dkH|Jio( z=h`0rTeg?~w(aZx#rF5#u_OGy+Ry!Ao9h3~di}rKS^oQWuKy4FrT>9l=znOJ`ybiW z{>Rqme`448|Fr4;zwBE7Gy9$Yxn1vnVg3G>c7y+w-R=L|?)5$I0l(rs=vTdm{F;~e z4ew#U#e2m6miMSX!h6#Hw)eEZi1)0&nD?B&xc7p;g!iJqq&M4N%6r*g+WUjQjQ5Ja ztoN$FocE@`yf@ch!F$&q>3!m_=zZaj^8Ct5UZb+Iw@9VwEmLXpR;jGwtyfvq+pw~l zw^e0z?}wE&ydPE8^nP4f%iE#S?)|j#U2o^g+TN~}b-X<*>w5cEzUOsSMtl2L*7GJ; z#(3S8?|TPV*7pvrY~USU+0Z+(vXS@m%EsPtl})@8Dw}$zRyOm_tBm!2Q`y|>t8C%j zP}$PEwX&6WS0(Tss&sfyRes>TUfJ4vyE4xEq_U0oW#xxnqq?oPSamyZscNUUeDz1( zidAOI{Z~1YInoaKLH;^sYW}+Z()nwxsK2&L{__J9^4AqM&tJElmcMRU{Csyu{_|QV z<*z3d?N9BM|GZ?eoY!{Be;!}7@483+^DV`=8z<&JAF{`qZvF0>n!nz0LjJnRl(k*^ z&21aF*Ulg0ug`6rzpg$tf4yl!{(4$7e?4gbP5z_)FYKB3-|nRRb>D;LU(PqZ-#3jf z*SmH?zTEP4wJG_}VfPkpJtq{~Rc^=SJLmPW|Ie}c`&~3Se_ie1{Pjo0dJi0s{~Q)Sm-~I~rFV48`#)XI|LORJ?e_)u|HAjn zZf-m7Ew+28c#VtCY4nNt_W!xqPd68zQ+Zqt9Mt8;m-}Vhqyy&r`M|yM@f(Es>r2IQ zKQCTaFFx-FDVk8(dQ_`2}@+2hu`{9G2cU(3(ET)$^` zIoplcoE}n0HJg{GW9Bfv6@5|$;+#j#) z+voa!Q@?MjFKoLP-TXSY{BplPR~+BtCgsQ9xLxw~OS|)*=M~58^2Kp_#L{_v%cB1` zZ~x8nZGZW_Dv$f}`&Mp$xn5gNyL-O%{7=VyQ@@4nm-2Y{{^k$P_rB%lQ+_|cd3zt3 zZ+pw_{Cr}*-~Q+Ad}hA)`=ru^RH+5dvB+$$7uA#rxe`j9>8m%AcPo-gn~h@BJ^AJFhqox_Q#4 z^VQ4aY^P%RFHFnd_rTrq*M%P!TmbQ3xxdQocwzH=T={*yy_gru_qp%*y#9Q#J}aIy z!Y${&JASoddCwK+Z9nVCm%l>szW=Y5TW)uGT$Hcn=W*tNOU$=jW%~z9#Ebq*3$rFZWluALkXvP5FDjT;27ev^y!Kc#rx zpg0elT)b{FWmDI`++XGK@nLbiT~Zw9<@yZlwYBU2P5r*9UcO(s{^jeRisR(G;{3^q z<7Z**qjv9f>ocV-fBnOjJGh@WK4E9~IK^o9gBJmHWMXEw`uKe=AKq z#=YOTy{5X?1%EG>zfbSkD&Jop6~8}LD6anwD6adiF0Rw6#rJ=~_bcDGd|kO1S6)wz zD1P3xoR^C8?zotLzbxvf6zAzz?vbz06;t!qC5z*@T>g*t=yBUqexC7xdA~1<>!(eF z{O6g)&%MR*P;UPx6Z3ktn1|OZ-fyiF^Y#U|Z`v;T@p)P^e=XOee4Sgo-?&L1^4rmwpZNipZ0v+y>2pPj(aVSgYrI7d0(mg9G@z_ zALViIP4o8`#qW=D9zU-5zJ6ZpmwCna@X+G*-ktOHF86==zUB9_oX5-KxLlwAv|l-{ zJg&<3DSuCv=jUq_^JsZ~-cih_<@tGUar_Pz^UUW({qK9{$J>lu^Vj<1VYgrZu-ixO z_0HmT?`ipcy7InMc|Eh>@7qzk=i`>xDt|q^*q^`NHUIg^X8szq<*zd*=dZ6#{>;7K zra}I5IX+$L->$tpPkOZ2j^~Qscjf;1!%6wL^5^pN9?>=b^2+6w{T4PZJ}@8mWwCzc zdM&uV3(o8RF1B;MJ@WBuotVFlE?&1OUd#1Zq4+&e{#>qCx&Gz#XW3qU?hF3BV0&3# z@O78s_tEY_{&|+)mnVw#o_Kt|z6Tbc`##0zSuSVPl)PT9-@M}UT&p8*FZ-3ROHRt$ z&p6v2xzurOldOlxlS9yGt{r~ItOY`NG%PBvnZ@Pc8VE=Ev-!=dHEbKnPX$x#u zdHk0B%ik;I_Ac1JY%k}X|Mb2pJ`TLZg%kK}$kLM4I^MUexzv;Z{*zSDXBE@UDKIMM@aOeDeFF84XE!TU& z^Z4J5Z=Ci2zb{a}f4N=d_kF?k^0-~Fy?p!;?mV{Oau@uZ{;Tua|NQ3?`S<(tALOqK9{=U{ zYhl~D;QExuUpc>&^If?g7Pg!P+soe{<@a#G{aUVPd0zCNF8{d`^X(irDgU01+a>?r zr^WYmUNNuzkNfZQ&vWH*Qodg~zx{XT#FFefBP17j_?|TyD8OM=a#Je8Jxz z3$9-|zTAHw?wlXbmz{~C-W_flN+2{GHG58#^&+}F5&OH9d-xuO#c>c_@Wy!uR$1`Z2El=iX=Q%Wwr{ONeEAlLwXQRj* z|2&W8S(EJBDm;_s*{Wo|5j>aX@zh)&UV~@TJf57Jj@RP(G>@m}uEpQw88we5=&r-- zc>Q=?GT$JcSMzK%*|+t0X3euPWWG~8x8~UfWZyRQX5fv;zHQ8NY#vYG-GMjdSvHTS z@Mhw%JkRFwB;MV43!Z87Y)i6lTk%|*X93yg`Md}556Hfa;~6)Pr}ZAf+wz>7XWNl| zp4)o_Z%_7Z2cCKJ>?dU3e(F7ecO?6EAkV*fJkd9Z58@d(kEi-(;X`;1&g03xXYirk zbNDc_Z%249;3LUC&-=~BN0WU!#(Nq6ob21N-YfVxvTw(Gui_KPKF)zk z#Cs24KxRJi-p3b`nNPeA@I_?i6YnE@G1=!i#82>LWZ$mvKE+p(nNPgW@YQ7I6YmS$ zM`k|pzQWVV%qQO0_*ydaiN}BZwRO*9KJhBJpUiyX)$omEpTFeYz_*Z@PrO!q8=3jU z8-Z^pGoN^i;5*68C*ETCE;93pw*lm+bQl=T`XdWacQZ1OJ2U+Xvp(_(QU9A9>s0 zkIBCM)7ut*O7?l~vlD+#_U%h=d;AsIx39e&u=zV+-ye@FWacq{M_eZ}kNG>}7Bcgg zzYG2rncunoZur||pXWyRz>AW7Tg=}JFHZJt34b5_9WwKnKLIaI_H9|e3ol3ZZFzqp zUV-fM%;_Y&BH8D;(*y8IWZ#;8H*O;{kNF4VRmsd_{uI1AnR(1V46jLM9`ldD?PTB9 z_K(8rkeR#uWAJD)bC-WC9z*u+`~LBGeX?&G_*3zQWach^8s3D=+~uE)HzPB5`8{}Z zGIN)I3f_|J^E~WnxP$E5*8UlI9ND)Y`n`Btvd=TJXX8D|zU}Rwi}xY>JTrS9op=3k1t$;@y5<@jJS^P7JqoG&A3&$GJM;$z9a9p_(%k0&#y`Th7rGIN@LBR+}j+sXdTI3oMj zXC;Rq*KL5?9I0OO>T@oy@#cSr)gD znU^Zd<8P6fmntLix5>;)l~H(6GV@YpWxP0#YcnU^YS z;uXluOOavfS0VehYGn*wjqKa%mG$u&WZ%}TY>3w) zbADCX7_Uv{{Hn4kUYE=qRT+y%lbNF`Ti`Kd=BUb6czrT+RHXxNNcQ>L>s#ZE$z0J? zw!xc{ecP@wQ~&Ix9QkACb8ls_cw+ zAp17HvJ3tx*|$9_yWzda%wv^3@IGYbvC3X}0-1TNvJdVe`?gBzzFrw}UGO;6up1O{sL_L&?lzm4or&WX{tnQ}B^w&eJM~;iJjSW0fQD zv1HE8Do5cH$iAIeIR;N7Gj~;v#Sz)JUsR6Ar;wSiDpT?4WUlWj)9{&OuJ0-*_@$|?9fGUsEJ)9?jk&c`Ze;0wu|k5zi{MP%PDuAGf8A^Ubs{Hk&RzK+cKRpmn5PxkG`%0>7lGV@a9Vtfmk^Q+3G_%<@QI#3^DKc|ZjWj6jh*|&dGUdA7gIoGPZff5+R<{Gg24qlSX zJ&o!xUWUvZRh@^IBQr-;-@_}AnWL)j;}yxwQPmIdN@V7!>PNUq_HC8wCwNsd^HTLw zyat(hsrngSi_CqJ>KFLCWX_$cU*UDgzO7sR8h?-M+vuuS@oYUZ^HQ~f*C#VCRcm-d zGV@Zkfj1^IFI8Ldrex-&>IgiR%)C@x1aC?9EvPPrJIKsS)g|yaGV@Y(NxUuDx9zG+ z<4!Wyebr_0_GIR$>hgF8vTr}Bj>O~1+{3Ak!aI?D`&o5mybGDTI@LD38`-zrtE=KY z$joWg)$v|r=CtaXcptKF6RPbvBr}gy*Txgc%wyGc@gy?ySamc$fXw+=bqww%Gmll* z$A^%a$Eq9RL&?lz)s6AtWahEzruaxQ=VR5e_!u(RnAI)tab(|4sBVR)k~s&fc92i1 zZcRS9x()dk)osbAR6EJ1R<|dgR^5SodUZUb&LA^WRd*zxSKXPG^U2&(s_ufXAp3S@ zbvJw!nQOr69=MOp997*5PbV`+RrkTyl9{8b6YzCp=BR2H?k98ZRGo-#By&%xItkxQ z_U)GH0r*xj=TX&eJcG;}RXrHrN%rlo>J)r8nR%&t7`~6pyi`2`KS*X?svd$U2!cs7~qwd(QsWioR}bt-;^%p6jkhF>N7_ImYX{05opwQ3Lk6WO;p)l+av z_HC$o8vZkxbDruM_$@N?Mzt5eL*|^PdN%$Wnd`Ocx%gc&^G5YN{C6_fYt;+zKgiru zs$PgcBK!7n^&#{2AHj|0;7i{*ufbQoRy?P39b_b~X0N%p0{n zTqAQ{RGUt2sa;E6tacski<6m8YW;XgGUq(C8}ZU)?kUx7#>qX?G8MO?AuDUnRsQgZ_V1>xQ)!*Qo9$gN@i}U-H+EGb1hbT5Vw;#pQ$~B$B?;S zR2#q>khxz}djxMp=9;Va7~X`;HCOEkycwBmuG%2pob20{wOM#8GV@058QekUE^h5P zyfvBoMYR|3He}|I+HAZnnK`8PGVUZZhtyud+mpF>RC^Wwgv@up_B!5&%y+-`CJxEI zb=BtJ{m8yetPSD)$-Ygh&Bc?+z8z3|8y`sKe5UpeK8VabP#eaFkeLT+^YEc$=7HLK z_;51wK<#~eB$@M>+6VY(GUqe3kMOZ%&Sz?$;1kH4&(uD})5y#Qwa@U$WbQT9zQ8?X z=7ic;_*61;LhWmOI+<&+npfpojLi8=t%A=Xb1hb@;a`$DSE)7dC1ma$)mrgoWaf?9 z2z&*Zd84)nzKYDeQCkf6k(oDYOW^5b=8f8t_*ydaMr~<)9hrHfwk+-^GjG(E$2XEW zx2cW9w~(1DYNPNBGS^(SmGK>9uDNP$cqW;7qqZu(o9x>?wbk*xWUjwzYvTLK+<&gM z;nZ#Mq_Or!B*wK4c{GUqn6_3@Kr&TVQN;-|>IJzd)v zKSSnxrnV`5p3GcS8;fU?x#p^Ef&W0}nya=I{v(-t+O-b+8kxDMwl#i(%sEVL8$5^1 zIZSO^JVfRkrq+q)k~xQ|ZI9n3GZ)o%!0(Wmi)!QX-^kq4uI-55Bm4ZHrgp~flR1Z} z?SelbGZ)o%!yl2Ei)wq|Psq$gwY~7CWX@-5`{2*XoX^xI;IGKcMYS&cHJS67`b6xJ zIiIOd!WA;}QvCp2Bl}jbcjE?`*{OaoZYBHnt@;!^g3P_~`eAqxGWW*oN8rWC+#9bS zg_j^RkJXRCOOlz#>c`@x$;@N*)JbEJANUW?2*QvGcFT{7oL^>gt$WX_T5=i%>>IY+8r zfY&4YHl}_d{yv#`uYM8UfXuvCzZh>sX5On`iZ>xM@6|8In~|CK>Q~~;$(%3Muf|)F znfK~_xP#33QhhoeM`o_8UyHXTGuPFx!<}U2x_Up}p3M1D{YJb4ne(Ok&3HVSd*=09 z@s4EQcB;?7JCl9erG5u_*ZNHI9`(Cv-;>Ori`DNX?_Iy2yiff>+V>?hXVxFWA(=U| zK7jWlGiTNx!TXb$GwYAx$zQSbq;+ zK<3)8{yx5t%(Y?t1AGyg`&#vn@Wo{AYt=u&my(%p>!0E)$jrC(&+t`b->$BIfnzfF zwd!BtYskJ$uYZkyOZM&Bx>xh;cVzBy)hqaVvTyzM8oq(db!EMQZz41I)?4u{WUedg zBk*lxt}E+{;M>WZv(*>FcapiTtS^D@A~W~am&EsynH%d%<7dd+o2oC1pCfaRy}mqt zfy{YYeI$OF%(+>86n>S=^<8~s{5qNIyLuablg!*$Ulq?GGdI>($3tZ1!}^-|Z8Gy= zy&bUSYJCK>98pq=CWagj7@pw-%^GstZ4#~_jjcIs4GILAgWIUP7Ias3yA4KLHtZ@oH zgv_~C<1~CEnLpQSoPmE%=3aNB7oSMxI;(LuK8ehAR^wb8k-691I1m4V%$(4;0H01~ zPH0?+&m=P^G%mttk(m=37vpotTz@q##pjc`zumYT|BCF}MU5-*Z^(S#8&~5i$b8=$ zefTOe-|@zDd<~iJb>mw6J2Kzj#&!56GT+}uKfZ;`ceZgOzMahXv~e@Oi_EoE<5qkh znLkr&%)k$jIWKD5ffF+4MU9#GVKV1Mjl1!qWX_8k_u|LNoC`JX$4`;D&(nAiKTYO7 zPvar{ESdA7#sGev%zd85Bltx!_jwwR;g`sKuNzO`Ib^=qjX^v_=A5T73(qC<9dA5? z-zM`NZ#;+JA@dz?ynu(ve8(HJ@jNo$@y5&eJu=_%#w+-JGT-aQtN0@_=RA$q@h4=y z4ru4q{TuTN&KXju|(NM^2RSsHIlX0B*i7H>*s zu4q{vk0modw2Z`Ck(nP_M&S-J*KsW? zczZJQMoT;1fy}(ovNj%1=6t4QUA!}ydqFLu@h)V}YFftN-N@VvYFQueLFQgi%Z7L_ zGG{m~8{>V*+~0256i*;?f4gNY?jmzvsAUU0k<5LemaXt4GWUgAI`9Ex?hCbSjl0R5 z+q7(hr;s_fY1tMZM&{h6r4t|V|B?0{@KF^1|NqV$%Izf<5PQRdiV6}$0TDEWVw4^b z6fuV62qz&I?-DTZ!G?;xV!_@KyVwghRP4Qc)Q|eH_uloR{-5ue-Mzir4gS85KRn*A zyyiW#v$Ip)Q+E$!Y?Is%o(CD*BuB&BK*l!7vGBPdW1Hl7cst10CRqcY4>Gn%PJ%B0 z8QUZ$!;b_R+awQw9}P0LNlt|?1X(poPJqzXikwCXa?M2N|~{ z7s77`v4_dy;CF(ohbI@o?*_4r$rIuCg4o97$?*F@Y-93N_(LGJF?l-t5fIy$JQMyX zh;2-s4SyWOHYU#jpGuyG=4lX1m|OzBl)Mnl%OK;Qwwrj_i1<+5WD9-3tu0^?zzvyHw3YJ z?u+n^L2RA-GJJCoTj#zC-x6d!%6%Qa4T!CC-+=c3v32fS@a;kDoBK9=ClLGQz6qjPxvGdJLdik zpA2HhT-6yn24ctD1bixp9dp-!PXn=IZfAHci0yJ+cs+>ia?9Xp5ZmRh1#bkgUG6&Y z49FbK?E;?#V#nO|;Il#On7cka3u4FI4dI7@*fDowcpk)#xtqeiIeadN?Q*w- z&j+zx?pE*xAhyfh27V-n?Q(m-j|Q<_?)LD7Ahyfx2|o_RcDXyj7lGI=w-@|m5ZmSM z0zVbRcDWVs(?M*PyDR)m5ZmSM20t5QUgq|Pp95mY+=1}(Kg5L!)7jq}W?*Ul{a}R*u2eOv#PKB=kIfvj* zgFgsr=MlFSz7pgNm0JgY4CG9vTMu6aYUc?z4Sy2Evbl}$r$Od=ZU+7=$Xw5z1%DpI z*15CcFN4@RHw%9i#MZfo!CwcN_qloa8z6SiZG*oBV)xv+@V7zip4$$87i4_s&WFDT zVgua;@DD)jo_i$x6Ob{Xdo=tr5F6+&gnt2I1Ks1`UxC;_cM<#>5F6;82>%XbeCVDG z{{h4vx~IZ_0F{4b?4f%m{5KGL=$;M#1H>M>=fM94v4^Si;0nYZrk21HAZuc& z3*l>k*v8bw@XjE%F?A{21+k5(%i(1pMRCKU25CHwUqwsXO3Xg4oa0UGQx{>}Tp8 zcn=W!nYs_YJ;+!wwF2G~#FnNWgzp4mOH&WSdx6X`Q!C-SfY{a4WAF+PyP8@B-xXxs zn0gYv8;D&^Jq;fSVpmho!Uuua)ztIw!64(t)Qj-FKx}O4Wq2isjZM7@uL2o2re22+ z2eG%QH{c^d>}~2Tcs0nlG4(clKM>oSdKW$##P+7%gO3HVy{Ql2<3Vh1>LYj!i0w^% z0-pq8dsCmmCxh7A)EDrnAoe!(6?__qy-j@suLT)5roMyMf!N;E5Ab>r+nf3co(8eK zsbAoYAoe!(8+;asy-ocApABMfQ-8v@A4BO(o!MAoe!327E5a zxG~ik-VS1WQ!acyi0w_4!54to-qc#~BSGwKY907O5PO^I0zVGK-lo=rF9O+9NUaY) z5ybYUHiVxHVtZ2?!%qdVy{S#%r-Rtu)aLLrLF{dcIf`=*h`mj11wRkO-ln#JF9ET) zsUGkPLF{d6d-%m5W5rZY_~jt>HnkJ{N)UUS>IJ_V#NMWMfiDHIx2X#FG7uY^+7*5y zh>cC{2EQ4^#-{qiZw1+FNDYMF0b*lQgWz|8*x1xy_&p#tHnkW0K5&#eX8oSdD0S@m zJ8>fUc=%{_0(^`*2|h-h0w1eRgO62bz{jbx;N#R{_;__Le7rgzUZXC9*QhJt6V+Al ziRv2oBy}x(lDZDQzq$dwzq$!NS=|Dktd_&4sN3OF)Sd7H)ZOp{)V=Tn)&1}T)dTRU z>LK`4^$7eR^(g!x^*DT*dICO8Jq15lJp(^jJqNEcB1Fu*Ah1aY1;SK6Tc!T;Fo>rg2)9Q2h4D}^^hWZ-bsJ?|as_)@5)sOI* z>SuUH{R+>h-{FU<|G^Jcf5B&QB%&v%H`s}j+@0V}YEAfTm4we$DR{Fghc~OW;aRmV zJgd6GTU0lAi`oEwnA!+_nA!xMQ=7qaY72N?b%*EG*6>!fExc822X9k5z}wW0@HuK{ z_#D+6K3DaD&sBZlhpT?@!`1Hab~OOruJ(Y>Q+vYasUh(BYH#>_H5C3YH4OeQH3Ggs z?E_z+_JtpzM!}CzW8g=saquJ61o%;EBK#<|Km2Gl1%9+T5Ppn02!4z@7`{*)0$-@6 z!;e)B@MF~s_;G3`{5W+e{CL#_KVCJ%7pWHbB9()mpjzQ4s5$Tx)#30H)jaq~>R<4a z)DiHL)lu-1)iLl>)UohW)ba3B)d}!Z)k*Nv)G6@O)M@b3)fw>9)miW})MEG<>RkAl z>U{W_>H_##>LU1A>Js?b>N5D*>I(Q`brpQEx(0rZx)y$px(x&eN!x(R-sx&?lo zS`I&7-3~ur-3ec!?uIW>_rfnw_rotx55O-}55X@~kH9ZdkHRlfkHarkPrxr$Pr)xy z&%iHH&%rNMFTgKVFTpQUufQ);ufZ=@|At?#-h^MF{sX^4y#v2e{TF_vdLMq3`VfAV z`WSw-`V@Y(`W$|Z`VxMP`Wn7eeG6ZzzK36{euQ7EeuiJC{(|$1HXH85*?PTUPp}jG z2DK*q29<>0q}GMsq`JayR^8w?s}10{sEy#as7>Ivs?FfHsx9ElRd@JuwKe=UwJrQM zwH^F+wFCTiwIlovwKMz<)f;}N>I1)1^@ZQ1`oZr~yTk8R1K@Y7J>d7KJ>mDLA@FeE19M0{DySBKV8y68KB%GWbjC3i!+FD)`Il8u%;fTKFsK zI{2&V2KcM$CirXW7WivwIsA2XJN$KZCwy0(+N38~zR6CUTwfdBPp=E#O?QRwrn|v+ z*Biih*Bin6>rLSO^=9w^dJFgf-5owqZw())w}tPaw}bDYcYqJlJHiL)o#A`x-tawj zANXM17d}|`gAdWW!-wbr@V)dN@V)e&@V)gA_}+SNc%>c+uhhffL-h#wP`wYlO79D= z(xc$R^ceUsJq|uxPk;~C6X7HD{_qic3VftK5I#~L1m8y=4Btl|0C+WrT{q?!<{q_0q$@&8LWPK5Q zioOIsMPCL#KwkkrKwkwvP+tQ-P+tq5s;`4j)i=No(l@~m(zn2;>E-Zg`gZui`cC-4 z`fhlwz87Ar?}s0vAAldCAA;BEN8ok(QTTNIIDEQ(0$#75g4gS3;0^jYc!Pccp4KnH z)A|+o4E-8>hWi@zs`h9pte+WNRe+)lVe+r+aKZnoKU&5R8 z*YGC&Equ2A9zI+D2yfOu!<+T5@T~qFp4I<@x9Gp%EgWs6kJfrq`e@w=p3`f>b2l8e%%i*nhZFs9*7v843!rOE=_#C|fe2(4-K38u7pQ|^6AFj86AFjK@+x6D)cD*fp zp56{VPwxPquXlvc*E_@irF+BwrTf4a=)Uj;x*z-qy*vB}Jpg{B-UEK5-V=V59s)l~ z?+rg%4}~AChry50BjCsAec%iAzVL;56#Q5{27as_2R}|vfFGwP!jISc!;jZf;EVKu z@J0F{_zC)8_zC(D_=$Qt{6yUVKS|GkpQLBPPu7RRPu5NFQ*<-@6x{+pRp;QR>Q?w^ zdJgd^eF}WBJ`KKD zp8-Edp9MchFNUA1&xN0>&xfC);pa8{ik|o8TAeTi_S#Bk;@h zqwp*AM`&~^*H!rdIJ10JrVx6-XH$Bo&sN`4}`DM2f?4v2g9GxhrplI z)8S9*2KZBY2K*^K6aKV56#lesfT&*{VA&+B>c=k>qf zFX$uSFX*G-FY069FY06AFX`jqFXp!++Fk!++H4!hh0T;Xmna@SpVt@SpWY z@L%*M@L%+1@L%;7@LzRz_-}e^_-}e!`0si<`0siL_#b*l_#b*_`2Tco`2Tbt_@BBj z{7>Bv{+Hez{+AvA|6A_?|6A_~ujcbP{EUxt401MdC2||G5_uW<0QmvoNT#zMvJJ8; zQiaqY^++CBh@6dFiQI$d8D-Ccg=Z?27DzOha#1CcREJ#sj5DslyK7xE199`Z9%mU5i#$Zp8KNG;NeoQPb4+=e`Xyp4R1bS~q! zx{wNFf8=1~G~`C)N#rHuOJuEb(vI{;Mj>^`9OQK5D&#KYHRKC~&r&$+BRe8{Arp}d zas+ZVaxJm~c?tO&>9)4x^hAarvyfAe%aJ>gr;-06KOw1g_&q`-i(G)*h}?-hg}jWs zhx~;6g>1Smzu|-oKyE2$kyoOYEqYWTakt>lqkQK;d z$m__v$S24*NRRcY1ISP$k1RsYK$ajkB6lJ$ARi)MA?t6zGa-GELC9ES3UU^54RSMb zH}VYfD)J8UBa+&XJVQnylaPav95Np{2U&(JN8UufMt(t@jVME8b7VMj05Tn!g&c{T zfLx8-gFJ$Kj{Jpm-k9I;LbgSEA@h*akn@nsklT?J$m7VH$Op)m2p>LiT%;?q9kL73 zhMb6;gyS;6U629D1Y`!1MP5KYM7~0PMp9c)7m-28 zSY!%PhqNI_AXg!GBM&2gB3-s5E0CVZU}OaH1hQ3kY#1^KsX`_rhae{-mm=39FC(8I z-y**uYi-4Emm!tNB;;V^MC4-R8suhVCGrgN3sSZ3BIhEHA#Wh>BU|i(eL)5xbCHvg#mLLZ zC&;(R=6$Fq75qhRMm|KoLbmM7eUJl@dgLCY|E}2Fexw%}yc_iyX-AGhRDbFKawp;r zTw-$oa_SNT;JH)6Sjv<-5m|j`wdcHo24U;O}>bQg_z3iUi*f`JcA@cl7mB zEH4c|Y-+rIUW3)IW&apV%%YCg`r*CEah9WBx6bMl_K(5!(RZ3=6{Yumg=dt1`>BoC z!P%Idn~mAA*_fT0joG2un4Ou8`IWeB7@uv!Zp=39!feCt%Qoz~Y|C!Tw(PQO%kIjy z?5b?bZpyaoqHN3VNiTLyda+y5i(Qgl?2hzeY}bq3kY4P93}E+T0J|Oo*zFj=F2?|N zHwLh)F@W8S0qkN7VE1AGyA}i3tr);A#Q=6E1~4uhz;46QOeG9AhEv(kJuu8vx)%gXCM;EXfzkpTv1+2a=VAXvAtL-PS z%6nE_PegdoMC$Nfs0;}gIuxh@T)$+xxk}qa;d@*C$#jJ)eW)*xftKW-R^04Qi zzLizzTUmX+l~w2EtTr!am3cX<%gb3+Ue0Rra#oR7uzI|LRpS+`7C*u&@guAbKfRUE-zvCM^ z->a1Rf$!)1sLIt(d^_i7wYK_&@8Xgp&79v=SM>+q%lV(`rvBtxIe)1Q)Zct3 z$I%-rr8iM5rKyD8Om)(mt2Oi%YE8YR>a4r7S-h2U_0}q-w^3zCIkJ}CR;{gjsCD!X zYF*t^bZOOP-g<=EMUPZ{^ggOWSF65yU$v|Lx9W%N#y2;1NBZkG)d0S^F%a1U8HDVK3`T|^ zdm(!xmB>*2A63OSIffy_^*d?=-{csn|Eu;vs*!z>{g6@mJvAB`gN#MS>G#!mgkL&T zHTpv}QGcW+A^RhfktxUl$bksIf~XGCAFFBl6Lm0Bt3OqT@coWDWV-%b)${$11|+S& zR5Or9zTq(w$>^`up?t$*7Q#xYnvFE`9gnR3UbP^H=^s=M$s?^u8!|`#sOBPvBkg?K zV;(YJ|E&JSw>=iE`Y&}d z-}yKNIhDgJry+csK%aq}skJ@}Ia??6V&oj%NuP_Hr`OQuBTMv}`U2fqUx-|!llo%q z>PwJIbxL1`T#j6!%k-7VRk~bXt=H1m=(Y7yy^g*XS%zGv*VWe}Hy}6aF8U_qX5<## zRo{v%M{d*W>D!SzkUMoZeHU^!a*tkL-;3Ob+^;v#E072DhWbI|A>?7bk$wbOsW;Y- zB99@D>rM13FEto9k!v7W!GerG8F#*Uuv_ATR2z^h?Og$SZnl{VMVr z^19we{~LJ&c~ftz-$MR_ysdlacaV3H|LX1Zd&v992YP$`A@ULOvED&{f_$ob>d%nR zkuUU)`b*?1C_l^$~nlm%bkA4y|(k3aj)xaVBB4ujB$5!8jO1b zXP$9yZsO#OelzD}Xu|F2Y-j#X zO#Hy;fpLlVcZ&B{xmKU>{;uc!UElk=A%8Dr4}o7D=gT3C+4x=uzdr6<48F(LFc@|5 zg$l-3d@q98fy&VFcTsHyk2ZKJxS_fN++5uSZlj(Cd#e9}yQrUxMyf!Y8Zq;>); z)!yJpH3=N84h3t}k>CMpF<7gXfob&sI7__@9;QA8=c+%!1*+?svbM85c%s@9JY9_k z&rvhL3)Ot^a&-ncS6u`yRkwmSs>i|G)LY;^>Ra$ZwMJ*go@x{DX|)UZq8bLit_}d- zR?XlCY9aWUIv@N--3b1q9s&PQuY*c|33k?s5!_mO18_aP6S%S78{AS)0(RO<-)i1 z^^4$J`eX1t{RjAoUbl?W*FC@=^d4X$G0)%w>OS}s4}sm5Il}Wr?@IsfkYv{s7*bNUqIVNMdvFzC>T};lxOTH3n@Xjk!VF?gN94-KwjJ;&`dhIZ|Cy2ap226tsn;C4FN;8KI{ z7~F=n7PnKi!OILjZg9kgqHi*IrNK`PuFZOh+o{H2yTRKG{%UZGjm7r_gEtv`#o+G- zyR+Wmc3Nm~nZf4`4q{!y?Nn#*I)mM1)uPi02A3O5urA?t>S^#0gYOvZw1w!a4Q354 zH(0i%==&S&-Cg*2gG&s)XRz~Dq91GUJcBC?uCcY~_cC~z!P^afZ?NAs;(H+2xaJ<) z3Lj&z)!+pNUokkOhxndl@MDAJ+ll57gSQ(@ZZDdH3|?*UH-qUNM1PaPUkujt6ivIq zI}QF|aK9Zze~H2G3?9CdXr4B>#m>UdF!*nSeS3-Kaf2K87QUCkMFw9sxbrTeKhEHL zU^iFtFw=RLKEma>@|~x^ySh@x7IbbmTx!~)&Pz;~@q|CK^L>U(3NP&ZA$(cq?J5NO z89dzJ@nAPs>i^2lcNqQ-_49yxxS7_BA~Dyy23! zW0Swb4@hp_PhyjrQ=hCf{5@jMCR+`cx^zHtso_%A^OH{-E+u|k@*BgY+)q!gx0}S% zAG|O**6=*IEO|26)s@lo}lV7S!Yk?#A3OC28Xt~o%`BQ{~GyQAS^J<{$N!^MVV-JIcKN!s0W4HtW|(7o4i zu_~v!@4&mcQg4^IT?Pu5`h2xJ%y6mIH@jKGF97dzmm4m2b(Q;?;bK=`bbmKo+Vb0O zuRSClvAUnQ;|-S<_nq4Y@9K&L`@>ygxY)5ZQY%atv99Z+-ZNZU(8j6qK@yMHv~5y- z;9Xs@b-hv(4HxUzKXtg_V*M&p7a1O$M`;=ZbZ@Ej8M3 zu_P-}hru0JtnRARIfjeXeKB>9;bMp1O1*8k*wv3xYKVjv>-tTq2VA>ie|}3<8!mQl zjk13kF4kk6vZe4IW#57uxSF-D9%bDP9&E71;GN(BW#Ze-m7c4%>_@|;|H_nYzPCK7 z^aA;^O2fsHFDOgH^JRIkn=2N6QQ1X?i@iFt>@mYX2QMzum9(a^?ZK|DXl^YV1HY%N z-RKvA-CVKZE6c7oT&()@W$zd+w)c%PJyfohe&>U-?F^UR=c}@P;oV%ZpTCr~87}ry zl`k=2q;D!KUt#oOT{kTM*66!cQCrG)2D`dq!*?kk2k+*#f`iIWF!)44czdQ?HR(7tzd%ITGhF)FJIbFkTzcCF%fE+rbEUs{vV8LqGV-hd zC+oq6SA+F>61cgl1E(ft8*B&fOB@H@oH*0)i$T72XZRi9s>H*FKMOvacmup6@ez1{ z`VOpBe}dz+J5t7n>w{AhTY)zxdVzN&27(VKhJ*Jd#)DH6(+tlTeVgG&g9oTn!CJKh z9Ivki`zLM%rzTbyd=k7l@hW&n;yv)b#8=?MiQf#av5#oFfZM3e!B?H0;PcLI;D)La ztW=}HLFxc7tM-y&wE$eGP6QXIbHK~h<=}IyKc2ZI@14KC4>z_WE7c$c0HuGH<|3Vj^-q&^eu zk+>Kfn^*=`C++|n6Ay!RiD$v%5^sQ06CZ*1CB6fnP5cSooN)J*@$LHH%ZaVP_jE7t zEjNaIU)5@D<>e`bkjfSHW-8 zdxn1n?xlYNFV$=8C$o|+;H1Rn;Os7W5A8|f#6Ph2B>rn{7fAI{-I7XnsdQ{`U-GAeG|Au-wR%^9|v#MFM(qd z?|@SipM#BwpTYKo9xYG0Huy$j6Yz_~cHn!uFZhEV0-l%H7rZ#JKlrMi4)#>d!ku~G zd(QFTSI${rrMd(ht*!%W)t%ry>JjiU^&I%3dJ|lzJ_b)z-y6+e;N>bcMrOwwfG?`8 z!OprjxR%}nRC)xsv7P|-&<7jMq2TLk4*0e@#%N9h2kHyJ;rbeIiCzv~tseky)lY#d z^=shs`hDW572O2ZA3ZW`OVM z9Qc(!0<1`!1Xd-^1>5u$U`F2r&e!)E{y4bGeGmM^?KDpK_TY=|zTnEvv%zI$r-1Xz zZ!~xxSX2Iv;a`G{Fq?yil@9^umyZIcm1n_4$JsQhSfe)(14wDMKpva*kjrgM#Gb_C~_4*-WJr-EIQM}X_3E(bGZkAlm} zJ^&9Z?=(?j+a8=>zOTV4;IgtA;I#5Z2G0TC?tCY>pz~|sqRu~q-*w(-l8lW9frF9< zgAXQ;2A@n`1-_hI1-_m99Q-7?*8Vci>;wLhtO3*R95CxH0o&dC!G-R-;Hj>jOgXwe zz(3r5z%^2{!2YSzz+S0a!OGMt;5MmW!2MF2Op(#fVDRG9A>i4mh2W~xHQ-yRC&3p} zUxN9vbqRz42AqdX5jSbi?} zQR*J>o78{6KRf>oE-Kr4s*F8`gPG1Vz%x6a0B)YV9_*5Q4$LOM1M8FB4wCVIf3QAz zAXwop0DHKXfrH(ZhJOsc?Yh%sbhk5@NsR?BE^7v#FFPGvS+*Qpm3kHYCiN>=pWO6d z8O03&*C?w4yST@KXO}NE`lrAJoxcF{o!6?B6!rltk~QG=$vI$`)4Ib@D8O!G-Q2;COeT(Od)m;mX?M zckY+qiK%s_%Xqafm`P0nZ!J3~Z-Se;e;Q5q8B%Ynz^mOEU=R01Fzem`-t0baG~a_$-Srz~ zRx$wW;Z6nraE|~tPF)V3n0gdkn)(FHq*61b9D9Lll#K&_OXa~^%gzOtmE8e;Q1%-5 zOWAMW%CgNeQdjo^H!PnHs`BH&JIb#GpDceG{3!J`xK3G@LwWYHe&B+#$>5~aJaE6% z#b9mfA;Uia`M2k|T(&-)&re9#XLAihrq|Y1^l|pA%BAaC)BBIA98;NZZ>}HGVAVCP z*_N7gZce5?U8Eo0TsOTbEk5a7kw;}qOH=!>F*Q{)>zW(W`KVu<$E219Za%i9HIr=) zuINjuhSb;A4$I_Qn(ErCn(FfTzI|$AwEbeWRymO8sE~_l+FH`N=5%XK+w9qOxy-z@ zd2-`bdY6j6ad+7@R=ZoQwr{_<82ZI(ceh#+YDn+;)@)AF78^KLS+RTE-FA)D?iQ=< zJ0LEGezDr!jkb5apU&7oRw{-83QF)HJ=WepZne6)LXsa?!AKQ#x7?@hGh!)VLTe z^(ID3y@}CMW}=lGiffU0wHdD=Giqxqo3qXBv$JjaiR~@viav*M1UMisff18rY(y>CMDE!pOL!8G(WzLBWnwH3QcRFS~(+KPU$f#bCmyTt~M z*H-Kv8#rED(LXkDytZOMY~Xlp#lYCWq8-uJT+g2|ZB0#5aiP`2o7-lmbDnElF5Qr+ z$AaQBu`WN0Kl`+{w(OIxYe?tzuWM>cSJ4jA^9O~l6K!w+k#Ys$2?+t5ZO!7At0Wm=8PPaJlP;G;76R{2vm zubn>_;JMk{tO}>9X4^f745+Pb%GTF4<*5g@?G?BGzP?%Lpq=ASL#S!3Yt7V;u4`$* z2!whZe5gLv62^!Pv(MGHn?H+Yo zu4%|_wZf!T$Av>ja>r$Jtp%esp{*&@OUlKx`@Xiw`#Srj+fDh6tIOmL>FW&3m@=%( zwd0twTc3ed>0E1OhADe_fH5|A`}*13w<&glt=DB78s+htmDDwUqiz%BORsIw_a7QsqdR_1wDl zBK3sy^lY|Jsd)xFdh4KI`3!Yb#uP84ztw|_OuVWnu=m$mO>6+Mzb>o7uMO-Myr&KA;_*_>DIbT zQ}{xghH&YIeYuqsZi&iNrm(JRX1abBX27PRP`v)#y=O12S=PIxrdXv{Q$jasOt;on zkDJra$@|1FqOipbE~wbeuSlz@5rm1 zWl>zV1oY~T&Ao7VN=LfAuxeO#ZgW$%t|7KSiOb%is7wQGaj~K{@q3oXG{-z$Q=xQi z2(ou9^QK&;A!JGLGCfn!d`i10du=%7Hf5#dzMJhl2yS!HB*X6>gUc&+_e)|mg-w`9 zVF%ZR3p-E-%_g{!@oLO56bY-yr%*10eQfn8sx(AlA7wR_hA?#B_F0STO^~+YS|aLZ zjJDXAl6r!dOgpkU#!b@OSQ8#iWKo?6Zc|(*Q1h%c>DJbw!3pYp>Y56c7I)E@JczBd zba!_eZDZcuZ`Jk=cJf@PX%+qbOj}L)5++ig@GUB)KwDf)sBOiIloURu0A;14kiBTfwH1ml zh_$r1?A0YJ8x@>6rw}Ji95EoK0MJd$Ha0e;!v%oSrY|m&gDZ+M8Le$KLH?o{es~MR zPn%z1y*DQ=$zU!Bo;@5Ir3};y*+D>Kyi5}-HC(=@mp3uqvJ@o^tu2=}#S$`$HVGqh z+3*<3OIPuD$to+`8Zw1)L1*h`X$cIJQF=_~qw^jvcwSVPmFN-#dYiv-MS^Gb?3k>! z@iQ}PYMmLrgJ1cI*3$?aJ6W-j*s>Cf7aBBLFE6K~el{~|>gJ?dvYF;WK5^OTYy*R) zA*N=IuB@{&Cfg)B(r#SG}vpnqan_4%??{oQWXh zlo~&=F2|p0>lZl0r>c6`1apNU6Lan3>T>zCafo;IxXGE;nL(hCbEK^3O=ns;nmOo* zTy}PF&4h^)O^ota;uy^&xz0Y8VJtb;IkTD0m-KgNUrB!g-s0(hJ)g6pgy!3VCYvbz3 zm(;GttB``2k&suX3PU3&==noyc;2(xI%3mexqYU>2+DhmkiyG_phK6e4SCxL$m>ww z6ET`jq{k{0d|?&ECejvWsn*`vHb`%gDBK2OJnW;ycyNOl?F81@D)Tk1Ie!gW zo~w8iBbt!WcVbqjB}mYALX%D>GW1~?$Sh!oI%_97;c?{8l0hMB%Db7*cwV5>gOBu?qw1U5r;y@TPhd=8Q#^q7Xb}vOHzS zQ%A>mNy7{6$XTUno>6toIL(GH3QDS@wy2OqF-8TKF*}~KgTbCWYIyMH#I2A>Ad~7s z)I|-_Q~SjknsmvPMPqxLwRyY5WSdeU(o(mr{RRJU9W7`>qg07q>R`ds)~14+5-C`! z!a}QD7#Z1_#1{s7;*1XQLR#-Gq%v6jmResQb`wWnH4B4gCzzFbt*m&gNSh9p9^>qA zuzKiS9eGH5OWR;O@FZa!_yjX#$Dez7Y49>5+!l*e_I^dGLKfPq_KePiEBXv+aANcw zs4L<__l;NY8n5mbuihr*Q(HSC zlWxM*);vAi*4z-%%&`05Ax%cMKSJ)D%!Oqlf0%Hxx(xIqfw3Hq!YX3F*gauiE$U@Xcwyq zP?J8aEzO~r=%pkiPF<62i`7>)HD%|L?fxD{G(sure-6ZksRZgc8j6m!t*UH8Oqwej zuoJDBe0uodnbt9}LB`cJ##}g>9sXDkZ*ieRf18>Py+?80tt~d<$~kNx%2ADIMvcxk zXV_=(nnu)LM$u927_r*X>csJocpql^4Q=(U@t!06ZXgn&j9*O$5%G~>UcAr5OkC22 zrDxQ&HMN>qKzxYNb%(FUcM`ouT)Yz3ylit!X4KSYTVm8=_~j5)H21{#$NR)Z-%QK~H9Q(*6vr`HgN=F&qw|YurRK({ zhjR92*0@||POR_b^z=MW7LCoMFRC`@RHEvtIWbYx$nncat@2kA;(eu_cHq-g*Bb9L zv92+Co5HLs>NlaSnLWXni@b%S=oPlR^V)IjrC#G+T@bH(tDZ4Blh4OSGAbLBs9=pL znyi}q%&5|)HP&lfU2Dt*94woWpt>|rv7q_DRW3v9S%&4E4^heb-vobBA zvrZj^8daBP-Hme<(FnYgQBjY==0ViYj?7jUVtC8Uw4B3hie7AX2I4#>W?6HHhBrO; zoH(DVmNqtL(lOTzpH0sajSAb;nyt^q_*A!a>^Br^G>J28k#=b}7NYtIa>84-T%um% zXp&4wVtizF84W)y%ag?D!^78(!i>#jID8e2b3%GfCeMk-sL%ea%fxzkM!iFSY!Lf9 zcAY(+t6Bqw54i99SJMbLKG4u|+D$VJ>XnM!gwN6OS@D6ZBy*&f-523t?X>5YM z=}a^41TaHxkUC;di*|T3hJXJY)~=ex#F5U|Gw+ViJTn-Tt{{5dnCxmiO>LZ*jb7y$ z-s+a9m+aIIw$=`O zs+&#T`fJqDIH||2Gh-iw6Av`&x+braiwRTBdD|GZeF)le^loNwI5gYH@&2f%?0_5F z8sk^VYD<>)YmAMH@%PTdMkBSW=TW^kUc+Wb{i;|5!y?D{Sd-SVzh^W%4q%&9Ow2Or z=+KkA?9i{ed3Y|DjY}-Gq9d<;n1sh9LMAd^bBN}O%w<;N*`b#l#I;A@W8(B%U({<< zHXoP5=5eg9rCVdXc-#*C3Zt!|nYe51EIgVBzxkLYgQ)j}?A(}y4C7#RH0>QegJkqSs1!b>vsslBr48=i+WTIh|>o8IzRByoeFKiP__cs=cXs z)T27zq1TvfYlnWE_h6qE|FZ20Lbz zIC3wNXWm%qFn}!wV-{kfZE5Skhx*ll4;3~SlR^^0rh0S7zVVmImTNR!wqC~jg(_xz z0J~lt?Ps0Mcpku1$ zv^5%AD?4!cnOQc1?Dm+ysn+03%w=Y?JZ<#einQ4Z^SOlg7Nm{d=c4!al+3kWr)$Fa zT*Az-yG)MhuIwAcg@Z)$KC!vlU%)n(2M)R3>`9q0!64EED!3*xrOoqBSYZt5MOn1G z?ZsTI%lPm7a6(O9mK&R!EQ@nBE$o9ivRNZtGfQVqa8%A$*ljDeoqa`+9$yi}?JI(G z`U-hX#3bBTjL76>^J=OYEXZAFl(@~%(hr=YK?NbPPMn7Iz3Pix(onq;VWr3q(-NyC z1-v}e)Z~XE1x0R~55BL>1mEQyLhy2|w#B^Sf=I?o!>Jm7_hb)xa z1k8js8y+^SrPK(2d&0z5y7ghgMwAklj4w;JKuq8`JmP}HkZk))&w@BxD^{! z`f!<&H#ZuG-*SyZGA<{V3z1zM6OSS0NQQ6rj3zKt#wAyb@HU3bRe?i%y!Eai%COt` z*X6hy6ReuF**%h~HVhjgul~0e;`ertZ2Z1MuAdQh_}fV)Sm2Og{&te_2^``hBMqZ9 zL^P#aNyay#l#t;qB@?PhAv+~Oq;`MF1oCLLITa)|EX}NFCJQwVc~Ioehduqo%5GGU zVB-)Uvm0g9mWfU#1hTbc)PckEFn^PvT zIK5{@W%bwek!H`z#A2DWG|QUgEm`9e-oUbt72454QP|!x!R;DKXh+Lj6V=KidRtp2 zgwN&VrEd}neRrVoj_McOp_jJ&J*B|2wA&6mXmO^u_RLYB4|M^7&i0LVi6Kz>1B`U8 z#KVU6793)Nm-=|m;~hiyCM*~d+jomaSdCW@za2x^)fSZUHe29ly5P_`%)mpQ!4!6& zk-WFV=Rjp^8~IYA_tIUW4MtKW`cS@>_yk2%q7Ual)XOV8`H)PIQd7lBf^y%&@Tt^K z#)vfQ;A;Xg{&rEj#6!$kiK$sB`TA3CAvt~mH2yESE4l!5#$uTd^4&Xah@R z->wMs);@cC@Fm*dC5k{VZG|iRLXaDU)bVlwMsa(FO^jj>wVo zARYew%<5yHwYQU_=7Gv@Wx;v#z{du*2hRg#kQr!Xr7ZBUmDIjQRHC$R5(P>z4f1iC zK;apNB3*EwB5lZ66zl!&vq)5lq~;YV3bn6DZE9eV zrckr<#rklMDVycwl6Lkmf60?j*w|z_9AI1 zM4X=9COg4Hb8JM&#L%i`#9PBGmF+vL@L#VjQtmmVqPCW5PCRl!_}rFBSCN93GwqQs z6V^M$WrF#1P3(pSzPJ)-ST}585|^{5Svap= z@Cw)@IZqk59Hu21qD`BLbwO=&KpwqR7s{1_m!HUqnbz)h(;1x&n|fK z*0Y^(7gU`5sNt1?)^_O~^98*v8Iy885F?S5;)-+vN+&KDG^Uq191h0s;A8+aX| z;P$Vs%C@v~zGh~~%cQX~m#b@!3(gnBnnNBOsB2{oFNr0AX3sz5>nDW`CA(fbe0ED~ z$bVlxMMnBLF%#BG8!+a;)N0E_my;z_N4D2Z)i={)6T~uE<7cx-K9)suV?g159L+HY z>ZRhyOXlP4*UiHI5;-euHkvu|JvDO!jy9U;<*x*hGl)iEh-m!TXW&z$kPuSO0^h)4 ze7sYQCdLuIgRfY&V7l@nIA7Ex_cP`z@@-3#9!@`+i{-DGDwuh3VFGO{C~s<+H{Cr~ z(BXORj6XE-+@Y?|)0hG%_!h>5zOHmc=;>wYDip+3O zk!z~boNn>1_0E)<3w>@ox!zvu={vepAnvmZ6 zIfmOMh{Bmub9qE*>ZpC0xFo2zCK1xgK|fg}+P$BB)7hDrEpdAK2GfU1@`v(41Zlsr z+U!40=}}jcu!vF;MqV0E`77>V-e9x^m&q|>&rL9nx0(R*S1z*;?Q09J(VTcS0p#eb zxxx^0XLI1ydWKHG+CX6iO))qhYl7QpiU}B^HD>G|KEK+=ddzI>iFA}0qbjxvrgK`t z#8Et_F>#fsB#nhbv?fHzS)Cu1&9)SxlLN6f7Q^J?xZ|`YNW4eP;aii^;JmH5w&3z= zH|HRF2j3(#7kCGhY>Mr-3jI$N8vnxK51X3c!yd-B^f;~w6H$)j-Fx1%lIj|ct~e_B zV5Og;*}{jV>oc?Knv5319fB%gAMLx`#I2=94U3`4N3@mWZGQiQPGCaw}wK*s3o*LUTxy=sLeZPU%nt@ zpW8cVZNm7cuAQ8(l}Nk}&ZggYvLtH;ec24AkhlW@avOw5T_OLyn-E2t%WL6M5(WjfpbsHW}m}=r+E=@owWP zIt*!IvuC`mr#bE|1&yI<&DB|_mo|p|-hpp(S<&Hcb9J#s65{pQ#U8SK6YCLJ=ECQU z%$*n>$qJ5Sty~*&$~|oV$zF8n>2DLbW8YBvI=a|PWt=xz#7?a_YZ zL95rfOY_0oZ=c3l=q{|&-$FL7aWyn~GsQDbxwkj$tZp%PFSz7DL{nXhc{Vw_Z@djL zDGq&q%O*RlsA)>4?UizZ-}oEiwKo~8$Pog^P7mz9nd48a%rculM;Mi9KFld8gWg7I zUeZ6bwss|*ZDuu~pfMBNf>Ykk4?=r`|AN9B zkrWiRrz*Im`zW|<)hf8#SVkEnnbGfEFuf3tR1l` zd$`rt$Q=u>c$l<@oRVF1)I`Ro#`&;>O`#00Oh`i{1}OulW^SgHmqq=BAvuEKeQMo2 zSB)tdTb(WSp4NXU#CY*ofm{4-d2;(vk_oo`nj?J<6X5LJiRs*I^8!nnZ$1>Fd@?U@lP?2Lv*BSMq2LS_BFxy-2JzbJ zuuNTJGc`;iG2?vsd}M=-Ft{vlA`CetA>JwRkX9;0R4ZXi-(<1RRPuUD$?#Q5gX3#? z|5^PBzFn-Ny7~Tg{Sm7zZ}!=gm%JBaZda-+`u<|?Nv+;HG$t!OE6OW1#%Y!dO(d2@ zPaNg3b@IXgbnxYS@6u}jtUqp(KOwhQ1e{54lcTA{_Fmz!8DL-MF>TMjAYrc>nYM59m<(tA*=i-r0*8&N=+z!C%3|Hb!s$`F zl_#Oho}P)EK9HAx%#|M9b4b71+SZwwyfZpepUaxjfSll~WMQy9kJ{vbH)<-p8)TyJ z2U8(+T{L8~ouLPnIanL(Ez{?`{UL~aNeeJ^J^xnmjIdw68Q7wMR`V>F0t>^~W8(?SgaO^e+?Spn6`u7)bYKZ{Zzr zi}9f|lpny6fKu~9PPy4o8TtmjhmFgB>B)o-91_|N-~*M!SE7EMK^$WYry@D62Olqkm;j8^us*GRYkn3oGrt7k?7F;#?rg7P(OntlO zn>MdxnZ{yXwK6eTRx>l()+8^(%KD`VA;7zbi^vvnx^2a*K6x z2YJQIL~cm1?_;;uyu9(>_p*ryTyie#Yrouk*|iemf91$G37NnYvH{L z6|CsVi()2T>n$GwHY!dp^YbMpEA8Tr4P@4NY;k(8Tw0A(_M*4QOc;4_%v@)PJdC_F zW;BL~#(!bVXafh9izN!jj#cfnA~&0Z3C#1IeaB8elu5SOPD_lf^Eeee!-#2K+H*4t zEYTEl%|4wutWF-<2Mk;$wb&8wU35P{XaPf16L>RaPTHRXn~fr~87RG7jJ76L%XBCv zY$yhsWL73^fjXgRO1;MUVXW!#Q;#=-G-rLQw@H z%IM>jdU4W0POSJTk9voTBeaz3#T>e6)H9USsAq9@1_KT+-jWP0^`^KAMKOl+Y6)W!scC{zg%bp~Q^|I&5V!c8cOgr?3WU@wQ3n!HF zIPYkVn>n!m8!pjYF@r6cI+p|q-zTyQZ<7+rt&U*V)%9gG7ZdEAa}Y!4BqRIFgTjO0&8Yeax^rbfd63bd#_;l;WUIF04Iy zcQS_v{hAZB1orFrt=ni~f?m>&KVzSx$NWtbg&48e?8Lr%}v`y7Jr8#v9F-%h~&rFq`<9X^AfK$LF_ z1iHNUW4~s29DG8-L|1Ut*7@I7Fv0BS6igV8N@C<=3RWkCjVL+16VF0&2F*1 zh5QDMW7Z5z%--DH`1yP!AC#AqWMetNZ$6GFOD6f&g45)L{|yFnY4Ak`6KUZi48}9$ zlq<2>e1^gJc$6D@jI#4sriGgB=aF5(HkXh`o~zog>jjl$o98QQ;U2A>S>TcY*`=;- z2tMjyqAk@)IF=w~m(BS5-01D69jxBtqL<^X%!j;WW13~-!MoaW3B~h;Kl8w5bg{eM z_Ws`0{{ET0-aM4&BXJSd+#}uGV16{m&_Fr$ZhJY^&2T4gqP#5rj5&Y zYivZ`CnfA7_zv?7-X|rD53^4j4Xg8(m>HwJS$-%##wrn7r|*(T?24yNsLVP{WLP7Z zd@8^)b2l>8{~m?CV_4yZ2x1_A4X>vsbz~2cf{0rj8guJ+kP9vs5~m#G1<8e`&K3dU&%1u@;80BQITuK-^eflqFP?Xl=Hjh8qZ3b zkUp1NHQG%(6UI&^g70UTu%$Y=hEHXf@N(=yxU>hK%bs80Z@#Kwy)6}^BEHg~Wx3Z6 z>FpHSSnbN&h`QOCChtD-WAk1#!Xyp;Cpe5caG2G;z$M}A2RTeAkJ{t=!TZf7w!-H* zOvsRvuE75#WYaA)vu0Z-zl$t}+)Sy$n>X!j7@Gm9-LtVt^}p|7q7EE|#hu>%E=%w| z3v)i2RU@o@+TS&I__xh`XTH(xoCW+{z<;Nw4KsrfO-|ZrCfscP z#)RD7hOBq8glaWmTjRoQX~P{#xao2JJ*M$e~teCh1DYKfm{E7(x?UOuCL*T4f%a<8h|Y>bYqejeQKiy63skbdw}(JqemY zs4SIT8bk$6p*L(I36V#a>m_^>arjlQ#b~9W98L(^SQ<^zyHj{lshUhxm~LWVRLFk` zCsjl;qqpN^Doy(2T4|0_!CK9=-Mw^4JUOyL(kIvXX*rD8GsZ2|)>gjmCWS48$$3}% zY2{9pBe>o+X}LmD6AdqV`@B+6HYN7%#L%Tw=j>F?edZ9am=lRLoTJ^zNqMGO&K~$U zZKYwWoN8y7GXcMj^HHv}r6pxCnrF6oWOGzd(wOGIH1a-(RCYP#WWCXs3}c3%96K8};9aGb5IQIdR+ zn0&*ti*qQkiwSZPdpJ(nP?IAP#!2kxIBtJZ*@yp5*#KvE(q{Nt@{BoR5(DR~Q*2lq zx5dPf=V}QdIpD0-#639ujw{C%(@lu9GYMA{zLK_#vmd_gja1V?9OZOwCEgat=`1C|{mR6QN$lv> zlJIrpvD9~GtJ2sjO*)5@YH5c&$t=;dnDVo^A$PL1USe(Hzm(NXa|d~PDapBnkXkJ5 zPtqoFNl3|+dTOJsZ(>GfQbqJ4=w$(Z%U-0+R7#c)oKiq6qs}ec4XlUE-iE$ zmsI#hbGL$7kqot!xs7UO%3 z(`H6yyB0C6(oF1Vcy-AT8zut;X-;_>lw2n^%64b*1tuY7h~W<;rDBQAY2c1_U}PR{ zYFzfpl`@}`;f~z1xLV3QsG0wM26tze(Mqhj;9g#JjA?#0RpGd8^OtJ_!#+PQO~q*_ z&X>`(RTMBPLp3%(gP1?UTn>bZI=E#!H(P{T!k~x%+o3uLJh!9S0H_y zUC{NT|17>^IE6v_H!luTl8OzBeS@nK3`7PM`*o+07eCKx=CTL;|9QIZJl$^m?;ZKe zJaCudRE3{8wp7-!BkoY zbdD7>6QUND&dkJ8+0mP`PPpeM_vgm7#{b(Guej>^!)4C}X#i!WwyU-i#9`A-7{rz-AVVq2_Tj4!Q{{wVhO z{nGxw#N?HGW!!T$m{wJs#~t=htI5Hlo@4#+U{OlU)Ssi4#UMJyOB%kqWOuIwZz6PDmB+Jsgt?t|AE)llORCqz&fY7%JCr3vf)T+?sRFk-#_(4l9$v=?BKW2UGuXFA_ zhl^u8ol=~`I_s>x*4k^Yz4qE4=Uf^jhd?KtDdwUKrcqHTgug4x#a<~x8JPmZQ_8h3 zf!<~wZN1zkmRhmS946neVxPYO^~QMtc38~&d3G>Nu7u0f{;6&D+`V5+PHq4P9PDwK zIrcIJXOw|MGKPBOWS!#y8fX766gGQ zwP3Kpp~hS>u-^CxD25Q-2cn#HU**l?)-Y z^!(a+;E6LjOcS#033@=w=|xpuT((}s`77!Vbvo7zCDBW9@U>n))T1E4>PJRwC=PFnUDU-0MW2WuaOZZgI zQ=v{rmCqhk{tDidh9zxtJHR^rG05iSaBMfEuzrZ%gG(~lr}VxedgCCpk?C2M&BK$P zDOyFJftR&W?-&AENdGnH(*kmEnt_`3f$ea?2J3DWeF7=)d8jkCO)Jz6EsMPdX%UBg z1N{T;ZM^>w^g_QUAA0#yXeW4Oz+cX0tWokfWBg^YRd`WWCXXP4^g!F|1vcxYUx0j+ z$9>m&2}@q1o`ikYdKvu{m%xz%BgX*tiesO`UtG`Z#d^Nf0)KIB0Q(cPF-&c6a}16{ z0G?X?7}|(y53%O>d76M=B$L*)&dYu#z0KdxFqX?CR4h}5H;U4lQK<%x(BTBI`T+BS!@$cnN-mhxV<7{FsE`D6=0&g@+6v=^O!4$c zsAI78zDddHS~ryIgUFU^l~^Y-)?rQ){!4Sv!663C#mm|lbb>=4Ru$I;E3IFVy1b`{ zLlOpcR)B{ET1urYD_-64N6~Y!*{sj=HEyu> zm^I>rIoI;5^&+|e$0zIkd?nymEw`)5cTbxoFM__gTF1AHoVP6IWGoip9t5WaSTSx+ zTka?@ zOqew5E0de9`Tn%Q=AL3U9q!c9&6CN>VJY!`5Nein{n1Fq1~CXX?%(^ z6CAx@L~>k{Jb22gTAU7g6MXs_m=mif&+y>H04FIj1Ht%&Q$pOe$aTg^uN~u1&O`B= z6pb+U>XmO(A2aq@Kk?IrM2JJDU^42b%+Zf+f3w30qX6cN#e2tEuf3g|M+>vh7%unr z-tKKs!zmD%q3^(w>5PhI$j9sh(5}xSvSbTj%*%h z%_;HYAde8X9h(cZRL!c;Z+|s#o&+04>s|085{yfU`?%XJrT#~4}?aRGcIS}*z0j27RM|+ zZ~P0$qR&I0#$8BUmM~SvMFdzHXN%hrqOSO(7OUN$X;Jvm#0y=3I>A8aK{?DoDRvJ>l%c;=uNf4Jpy z*MMK~&~1TPhioIj{yaBFJ>ua)om}?T?Vq1ST&U$`dMOvTnh-|WF<3&BF$L>k z9hXuKpFA$xo6F%T{n*>*X#~aGg6l8K1*1O2O4w<|nJms(`P81jt*w)$r)72F7iEjW z-1$XZq{gHhvY(?8Z=Lej0vEs8X1QB7n_hAXp?5~}+X~KaBUqmU<%aJA!{FXPb_c}9 zma__6&15b7rKs6n4R&24ZG1^5c+?n&F*I3+V1=uQT_JYtGtl4~7#8;?tYexU28ow> zc~U+V3wp4V;7|I9GwdYDlnz(uFcK$us}zRlx2>On+r?m2Kbr>RysGhLI!Js$Pkdtv zcPeCAlLnr=b_whkmf*1qMo|CPWZ9c=7@Gzalp2Z0y2Egd^^_*)-e-6rnQN;O+ zFm*0atIK%_iV9OMoAOz!8k7KvLt=(&@$#^RXxDQ(MaqBHV0|2&&_uOw4jn#}-258s zlZ*4u&4%%x_b{7+bK>ChXVCzDp6Dz8i>Z?7y8h!U(%r)RB{%i6Pw5dOtc%K9<3JBC|7UX`=kC=THgIZkFC6y2+M;t4+C33OkAPCUmGGTy?` z4Nr1mRw^^VKgMXGkf2B8Bc-IRPu&*d}xJsiCZ%N=@6pqO3(B=vy9?!Z83nBXa6o$_$FrwjP z22+r$uy@R>CQq>(wg1M$tWh5ym)m|gjpytOH^Z%ydb5pH#8UE*k@a@4nx~AX2;+Mn z7e#hwoyEBpr!CpMav&N5gNoD(a{z~_We zI;TeH&iOAET0cRZvGggm(taJ{w*SuhX|@Li>#7qoy=IS`V1e{>m<;wJPNzQr{jp^8-9Dtg6)x4!8+_}ODP39*?aP}xye2L;Zs4TnDf9ipy>+wR%vTpb z=R7p|y`<`hNpoSOt^}9TI)H&3%u!Uv2`WL$Q=9=YOZ-mtWeHsT7 z_HGNlA{G4LS~vJTA6oGr%R+zUC#Qe|t{urQy_}++60|V%a`@;)b`O-?`M@UnfXUT- zV^&7&KLCBMzy@Eg-sa~P5;?%HYab7w_ zwi(BICFN6j6q=*FOqTFnLtMj==Qi}Ykm!R66_TT7&-Bc-Mo9nN7=nXbN8Pu z`I7o+u1z->KTd^9=1k{P&a-{U1?j`*a$mY?n8{X74 zIfE`2v-`5PL9TZC7!D&`fY#)iaTLSU)_P0pq00?IeYsz^K&0ZcKp^$!QBwVu*T?3J z!_7b5!*3#5gtZB@2y9jbGtlT3$*EODOR&_vT0-}I=@+^!WTeMj>AD>HA;*60hxqy# z_J?kiF20VS=Lfl@W0s-o=I4%*~S5u5*Y_R-j4=* zw&aEI8k;Q{snO4~a|hi!Ouq`r`XbsBbr_;A*cjvEM^W-*yL~r5V!4<6e)M6s+-KYR zQfkC_{ATt%K<|B-yH?hFxU!GSMEExWy-!+3dNhvBIpzZJDZ$`eKuX}4aD(EQp3}-x zuhyxcL_fWpC8)O@-wKx$JPB)E2=We6V0_`6L-G%9<>IHcf}?nTH4E|E(?Okr_2p4r z>I3obe7FX|`h>{*Feo!vPG?k7ZqN>(2fT!Z{mLB8_S!$S-V91+ZBEpvF$Tls1M^}HCYXHZl0pImhf%C*jAM`7985uTKX_p(1*e;Cf+?|sk0 ze}zor)Q;pT>y%rj^q`RT-7_+=TcWXc}jg6r12je^H= z&_ltu-U?EjqHPp;atg-#Z#1nC*Sadhin^M7r}dd%9vY;57Odl zn>^FB#<6t?Ww~;0D13xMI-dC-^?z z@Av|2D=dfML22ZTW$7)$3)xGPLEZK=*L%Sd!P?-WHvT3GPLO47u(M@{pdhC`W$)=8 z9Dod|u=ylEk*7NVF1q6s*ni5wf1U&y&RDz;(w>6Wk$kNXcQChJ@$-k6QbX5@1#rzi(K^c3pP8k)PxM+byTF2f5(46U>%_ zx}U>7Eq}ED(_T!H`INvn(2^ljx!1BMSpJgMPonwqgvxp^J8=8?gapnvqPcUZhMu40 z$t=I+^y)%3 z7B!biD<)&uPvlhbB8`Y-!3bgO9FlOVg5#Sqy8h;9~IP&`C zut#QVa48q(JZcZ}FJ{ZhU3)Ww$kqTjWkuuf+Mcqu>X^r?!S<9AK>E4d_0az3*PnUv z@fU6vd|eiK;AUzzFYuG|+1T8c${kC=OWw%tpV>V3vXm(X|1R2Us9TC zj~97j)Qd6l=tuC9T^rcWI{hKU0E~hBZBYE~hJ1#W!LQQs!36noruAYgEYM!!(kDL) z(BJ|je$j)@r=88_t;>dRKaCvm!{)d-ignOE5wim1E03UwZ-M+cyM#{*_?;mBrM#fT z;Mu?cU;2czUlU$%K#O8;r#iwG_ukyiHe~hPny zm>v6VMH=~jSphv3`-uM#cy#vS-xRc7%iabIa5IhzW#>ZX7GL&4BrfN7wvm z+R1M_>&?%>mVR6a*U0ulpOT+-zy%S!=OtS9B>XT5zB`XSls}W4d&z15?2j#Ky~!5Q zXIxK#p2$&V8FF^UziW!$9J&tw;n&P!e`rRK?krvM)q(oOA7$q-`Ia~PuW{@k$3m2> z8D}@LW)N|GHQ}e2uj&@c&zc-_f+Op})q{Q4sK-UKMfmAc)*@?b6(bw|j%dEMfqoa^ zTYk;`_J3Nv#V7OsD0{TdSLgkE5|86X3-~_M57BTtc^E#*XhFKz)WVU{;RYkMpvysJi%wEUvOG3-?*oX)CFiK)hYrUrP8Izvp zkVtQ3ZM2zmfy~4>wro0{5`8O5}>F?(wowI1awCt6KT+&b{oMV`4Gge+QbC%3nZObK-JzDw)M5(uO z)e-v`+7tH?QV>Z+pUaA)xg{V+@w<(ub8$x<%=Ytn@Ou|Qa(2F)OOlzJTd(v(skcFH z&|hMQ`{DHsJmlz~|L$J~jii^d_Z9FbU2}W0sUfak;IS=iJ1v_&;MZ;HruP?+lGi=H z8-LPUee(xL9ohc5wf479etnQsOxnlm6tBz&}i(r)vJ&>3{uSO#Z?utc=Bq;nz@OhXJr{ zye&OkSr99QD*q%Rjyo>@`R7G&cM@)quA%}dPL>{dIbIw)ZXG&u+$t`L@lDao zarbRN9)*w6FjQ1oP;lHj4&Qo=6(6?>i{K&LgZR>;)}hk4d#?0k1#l_>JaIt)?#tvo z0&ssq-Xj3_Zba9=I&5rF#|d5-|x*UEbY;J!}YBLMgH z@*V-WZ;`2-v(78nCF~gtzgLPtr9TBF%&UF9IR3>; zWifDtKaKqe+CK@`!WV!_5Q7%Q0RB_JkRO#F1+jQf-U028YJl$$HVM!hMMq5mFWcWh zQ7IJEAYI^ZaixXN&`?AIRX~>3NpiG;;-bQnhoQd3$BGK=-eZzY5meXx9IF^R20&5K zA)-23bfg%FfgH`;RSIS-EiFA%6gx^#1-LMo3T<8#vqu|>UbdZo5sUYr&m(l)Dyoge zVr4(F4!?lU;oGw>$DK*Q6Mem)Pt((S!qp1gO~Ta@uBn*$Iu0S52w5kQBZRCYWTQk* z5V8?!5_kR^>dvJn55+Z0D~FUM33%vDDRFXc{Z-cD;Z~yGih- z-eL)m^8i~52(Cd}ojeS1tn}nzE_t1lX_d-+OUkqovW?(2g4+phC%A>+7J^f<;4C3i zgzS*WIYM?2GQrk@RzUkE2sb1N?-Fu|kew1aPsmO}_7L1da9R>95Hd~335i@JDCc)k&t&KvVo9y2{|i~RfL=+USt6SW zxlG78iDZAABjmb7wiD$#A?GEsgOKxtye*NnguG42U5QK(au< z*Auc%B3B7nN62c4Tqk5TAsZ!fgOH7ctd+=3Le>(pK_a&Z*+9q|iQFb+4IwKfa)*$W zgshUtM}({*WSd0p60(huO#=q$MB(j4jM+i9~k=#cn2stN_Jw!Q2$lDT`BIIpC&PwDkA!i9W zBasdvX9#&yB5x7$CL!-gBz2szJA`y3k~+?qbEK#MM_0(J3QCVc-fNG|L!QG!68)un zNOU+CC_EDP<_eCK791%)`huk9a`(9082&9dQo)b$h9578$M_exINve>-CD6&1&qti zzl+5^&^qp(MBoJ)*8%2KW9)%ceV!`$fatvboG%qTZwj8-iLOHwd}L#w#VdH9~DMs(?^igjy$f zo#0(5bd`|1gnTTKYlM7E$Q_B?B;*bupGf2uA)gTPkwk72@)04oC31(5+lU-5l*nB~ zju#TLSRgYNA&UuVNn{}*Ekc${WC%bCS(&KYb273*ATKnB5R1Ufsl;^HxgV=a6Q2ll9-jLASBDJ zB)CcvaH%Rnwo4?1mGO2$wn=0Q@oyt!vqZKNvYC(_5=miYyn~P}64^YEt=kMe04mk9G2I=umd^{0% zw>1L5N8;HdD1MAOCrto=<_9!Ag2@2RkD#Lxo+b+L5IgiFvcwq!a)&9Al;UH94hb?P zJpioBHUy=~fGECK)*srE6(UI8_dRMdsQ+L;<36?O+Om81fIdDZ!-uup(B zB^3ZnRnJp1Y{XyFvJG^zxJ$P`EGj8|*>(@dic5=6mX;PnB}$9Ij9@QBf)k&F2_a7D zOUsInRuJ3Q^08?*}0}jXCEk?Lo0XZ(lVt6b{B0weGEh>34?kzFGTMCr)a5#d;q9g)T(pzdQ zIu;*yz!{+_(O;#f^7Blk_ShVbS&Nn|D!>r&kBW}Q$4AKHYl2K_HIo`3CF4#+8JDVa z%Wnw6&BVP$q4{F4ii=dmMOj4vxQdHSMK8t2XR*Hp>ZsLHoljI7dZ+AsH2`8YVQIwq z`+}E?j>N}r!!%!(Jqhawzb?_8^5kJy-#K|0A|`Z8{EJEe5>^GE7eVX&C3YrUBB()& z&BfC!G9B+>pod~@%Dnm|#M8Kflx7L|m#UdIbcpvLMUS9>zeIue?q9{?DPDU@y;Mod zQpYju{uSm&xR6-@A-@3P`(KygipeaObY?*VvnM*UxD-n_q5R6QHJn|qhNjQMpSK*3IL{R z`|YB`0)(DKk1ej4;Rb@!VHIE>SW!}(f2*2LEpoPCI>7JAX zV*qqCioYJh!Aerx@MPRTfThkWsrQsrf=KGU*NYCv$2Vct#N_aoCsA}OVAD)A-v@GG zCIm=M=B};N3sDZ&J(wE|`;cg;8!RDis2#WB-kMMsnp8vuZ#7uOHEN+X*+Kwt71vA! zJqj4!s-yydsa7#~dtz|&viddkA5Zol1i094eNjn#d5!K!6iq*VbR>6sR@YMCaYYLZj{FjW(YdK~v0MtDx# z69a%Z91!I>WJO2X2LP_9V~X->51ekCEcDR6#sf4#o zwOy9=06;K(5q3)5zX5XihiS9>H^+{`1dDKgQwDkA7p29Apu@rZ3*^Jx`B3pu9Q|Rd z6*f$9-yAmCzId^yfPuLC+KXVBFJN~OmKl-0p!DS;4rVS)-(ZVXkyR{SPe92F9kxEXgQxH=9@RLuN7eiL)+n;;4oCg3ks`le|aXM&n% zLN*ToMC451%x9u7?yTw-hOQVot)%|u$o_@^^I0W6#c@}}Ww#CZ#obzd?AC(Dramsk zUsuLo1c;B@=A)`bt=(FoH3CdkOVUc>E{F6Oek{QiF$gOA6|VPsm;Y8MOqlgYb$Lp0mMVLkAK-1RPWl zqG;u&qM`!Ecp>L_S*$>w;UFYzQBeJSo!`4fIM9FsgjVI2&oVtk)g!3@V9WHFRBjK9cv1F!Qlth3^LK!G&C)X|}iK$dHsH4!{(NY5InCjiW|hIqEd-Fv`uNg(GnPnar) z2HYj$xg>c4z&w|TXItF82|RBJ1>6e4;(>J_Med0;z{Z zcQfG7E(IJ{1{?&K(IzPebCg;L3XP2PYLs$bV??NJ#0qm26DhU89Sz>m{Oc_y?Nh$!CsthyXiQ%}HQlh<7K!lef^;1%P z09gGL6@~=vt}5)@4HS0nVqxd5D2xCVcJ7{r?W_qWJ`{HxMx{AXE!NDOh+yatUq|u< zfcZMO*gFAXbS3UQ6gVRmK8%Qk4~gYN$r1o&`H)ynKtzhW?Tk}PnK&{;1P>YL_zj^>+gUDXB37K`ic83qD2+tBz2k8f)fOi<%+8YE%=; z*|>WC^td>6)5pL#)<(y;*0JEGUmcZ{`++7qnBPuRNuv{dTi;^V(%yN;81u3;-GP`HcQ*uXRof-F zE9GvG<{MIT0GQ7PX?_<%X54FF94=w`o7roK2%HVb*=rD-5g^W91DSa~?sdk!9)asn z8hSm@qXibb^Knd7?=eC~SYogM5jO5^2~s1#ZbbyxEz)UA>I4Amv_&i-WIc&zEP>lq zI%TYQM!0&WFc2l^m;w|MED<1<8Q=0?hLW^Qp`-$UsS2sN79q07GaUkF1on=gv<|F5 zrbF@pfcbR%1c%Bc2<&lBjI+2JVR)GroI80mEAnQfF#up=%-{mS#B$ubuPgxIJ{AaQ zQs&*4i~(TA_lfaJ+!Mz13PAudc6T9J7Tfx;=*#rki@#(=Pg)THW?Xm#b`|)9>fKdc?iV5*TZ9xGZ zU@rSntmK6P@z@@%rDG7zuTQ`cX(;ZDh>QVK=3gI^gwBXPS_i8VNRk#5X&_LX()MV* zc-|wP_n6On;Lx-bF(F0Z%7hj%VUITc7FWV?e6?_3#4-pSd$b9*F=6cmBK^0L*|$LR zBJK#{vId}qv7B#(v7B$UsBi7j7T;YfS&#X}o&S}s;~-1i_pB(cJOsx`+-s$!m9VBG z1jL=c_3>!)-L+Fy9B}M8s7s05RG>2%a8eQwntaY4?NH?NKJs}`0UO_$m-PXF8sC}M z4Cd|8PO`2u11oLf`5jsGjw}iQ7rmp4-htEBSw-*3qW5G`0J!KqUG$zk+QaPtwVah= zDNR#;Gi#5gRMi%sYGuk23n+vTn&pB$npWhHyc&kP3*?&cu01-$vhPFcA!XADqGWH_ zqr(;GP!7C8d{xERam!2Z>cIg3{>HvkKIFibmm0SVW{mUtaNcL-71p=*1tw) zSt5|s3W>bhCMCMH_ULVv2sCXV5#K&(73|u89T<&J%4o27H#(<^R6&snC{k%?07VF; zi-0C~lqL;QlLnu*4fg0=MbjZ^Xw*^KcG#oyil#==)cE++*rWFp4IZYU&ASqy!H(sY z2p2~{JkolW*rN-oY_Tj0Hs-Q2BD*poyT!U}u|0ZUm93Ly>wLP^*`te+#@P}yd;~z& z(b=*`mn4nbEcrD1_%z$2%ZjE^(lq*L8tu^)N#pEFS-U2Uvulq&P&7M|X2(afV~?&% z8n;}^DmQ7|a(nclqA8O!Wj>lRdvr~?O?$x8+I^($_UO7IEd`G-Jrs;U&&e)?{#13ih1G{R_U_m$xgcs}qM4O6vp$+xd-RE-fq{v2pZC$s+hdlZxg%+C@X__UV~-Uonqf&Z?4udB$BGqA zTGFI_G--RRM42=NCar>2=n-nAfD}whC@ne#uWG0wb5NuPirf}O=C}xzvd5}e72vZDs?Tl4y0fkLTSOCqQ_^huXg2M! zT1B%eX;ytStM*u(qFIqND?XYPd#qm3Y)P6eAI+9M)}X4n5vb;dk95NxYh-q?{b>ny zaLB{3tY_I{O^QYaDmV^e(v;a_&5EW(@+tArl-Oe}il#!+RQPBr?6Fo!;HcV?BzdMvAWS(bU*uDMiyPX_|dB&GuMY(NsyA zDj!XiJvOAO&=9CXgO9Yq9^-7#YlZy-AZ>u!PKesx0#fK7gwnOWWslut4WNh&k-QEm zM`v%=9&;2;ht#seC%VHP8&Nbew0XYJ=0RwiP&93l4<=k%pEi5!mZEVajpL(n?6Da| zGc0L_eKf=N*sP*SNt%?8CS{Lt9*E}Z0dv)Zxe~%$JwOWPB9vCI#~$N65R1$NEIi}m zK4XvFsVpikhIQEE&>_ENzTz)pa6uNfrs2ICczKw2_<#&ItMO%ATbdtB5Dn~U)7BTv z&G0WqSvYCW1&>>0f*y{FGI>?e3sUyqnsZ1|P|LrUB@Hn?eu#kWeCEf-gbky=6BSs1 zr?_b5{4KuGOdFQ4laKn9z9h7QGGl_wcK+`p@H)YdtWuf(MP@rT4=&Wdu)l@9r(`6cAxjr%-dtjie_2TEc$An8+&X` z6oz9K;xQNY5v|jd-&@g}BvOWtjFU)jMVv{B;<)K6xW!h4g`XX1#T{QQckD6F4?UpS z1amY&4-?Cxuysv6j8KX#>}`+jD7M>@?Y58YwmtSp(X2_DH6P8IJ;wPVie8m8t3H}l zdu*3OE6~7T>{HJ-CVMa@KVgxNVKGv$YVAc$6FLlLeeCFhP*E!y#oODj)dl)u*X|f*%n#0#mB$J9&b}LO_Ii!#bui8@pe@; zNI-m5^W`C#4x8Vdn^*<)Y`DUUjuW^&hgXF0*aBV=UMF~+VBCnYCpHMieQbMz-t?T< zC3u(cxIb)9Y!m#1;3o)AS_s1xVO%4}EAq$f4!k0~L+}p4j|iqWGbbJs{Fut(8n`_P zE8cJ|A-II#GJ@${%E=Q1pCEiW!Q}*35KJ#CPF503FDp(~5lrtWPSy}yL-e?@XHQlW zTuU&$X*pR(a2?_639cu&f#3#$8wqYCxQXBPd@NI(U2%aPO4#9T_zDw|3g69dIC-@%0_Xu7fc!A*i1m7ok zk>Evwmk3@Wc$wg3f>#J$A@~8o4+vf*c$MIX1V1Erjo>wc*9l%Hc!S^#f;S1?BzTM9 zErPcR-X?g5;2nY=5&VeY#{@qnc$eT^f}arl1mP(QVYnh(NN^#+#RL}mR})-Ka1FsV1lJN=OK=^*bp+QFTu*QV!3_jA z65L2|6TwXcHxt}Ua0|gL1h*30N^l#&Z3MRy+)i)@!5sv565L5}g5U(fJp}gDkQmk3@ac$wf8 zf>#KBK=1>CR|#Gv_#wd$30@<3jo@{H*9qPrc!S_gf;S1?B6y47ZGyK6-XVC0;70^M zBKR@Ej|tu-c$eTO1V2G|+Cmtv2p1AuNN_R1#RQiSTtaXe!DR%WAov8q^6T!^{Hxt}K za0|h$1h*30MsOR!?F6?I+(B>$!JPzm5}Y77L2wVjJp`u+P7$0YI8E>n!9xTO6Ff}t zO@eO{>=5h_JVNjY!4m{e5PXZ^TLjM#JVWp-!LtP4CiphNa|F*3e23sW1m7k2F2VBz z&l7x);Clow5WGO}eS+^3yh!jO!Ak@$5xh+BGQleZuMqrz;0FY+61+6TD0CF2PR-enNYABz2G~>N_a# zNEN6T5`y!biN}UQyNXwT2ZP$*E{9m&e!j zh0%kaHCd-fLPZ!e%vyUA&ZnR&Peim*qaq1yr6wkUBgLVu)XXF^flmRbR$87+>VO~7hrbt3tsa=tTny^E+JlLmGcce);GJ>`O6+(^MqYFbXNyVuEe0YG` zi$6LbYJBprJ(=d`#@7UVhl?hMw@hwBeNcT_mqKj|ovMZsm!*9)O#Mwnzi?uhoy+O40QU=lc~+00zrW|Iq?x}UtINE!u6 zqmQJ~o}6J4SWDRzy6l=HuvI@ft4MYP$&Qa?$DX{+B=DM2xe!!tlE6m&O?`=%?$7q)G^?@(HT4CpQ#{aBt7&-X6I3CX=k0?_isPI_$|U zMIx5ryf&YpHhXfLN#G={Xz8piv&kJM*)ZOY_HhBPd&DH0u+Gml%0L9r&HyhbPd=`o z;It{$_=!xwKq@zz1je4+WsX~M1D(3R|_OYl}a0r|#Q6%+(q~6pbQ*Td|DUz_UWa z$rJx_v?77E=&2e-vMh92_UW>0Pu0?RgKyR{#Z4ekhh1;lQ+31wo;wplkqMK9Ght8F zGf83G5kBDfd;s2|ooY}dVfn;VBa;-z^}DjBAUJ$C)uc#-CuAMzn`&l~lJJv(Q!R=l zluIj<;2W@9Y4^TQm;3fqnK2pKs&{PY2%Z*HXiv>BNgZsqupoZHQ`!~Y;+&dg5_pq1 zRF~V_2^;h$ri@OQwx{NpMT2_dGwaa$)Ez|<+E4E?Nn?0kI5n?G*0?&(nrReg&7Qia zNJ6{v0+YbopLNW|tuwj6d!SSI6-k32fy(K@%5AWx78OZoKV4#y=I{h`YFUwV2tge_ zK^^wgiXsUW^ngiP!n4b%RYg)G1l9Ni)!0)H6-lU|H702d&n~Cd70HqiwB!@CWKV4< zlF+cS$t3WusOV>IFae#~QY6B0U0)}1p%ZN@l2AcAOwt~nfKEM9BqF%*OQo_Cxe#0) zGf78yMmM#qNE+ELS;w`eo+y&AC^&5~NoRN#HO<+|L}#vSWxANlC&F{4=@LbG8E602@>Dr3(g-**DwjbO)3PXOfK+_ z>2xiV+=MqmnS@^$m7W4`l1{_hJ1}F1H$&4*fs-bc(4_6@dS(ID6gAFz>1Mh?k%Yz4 z=|(1j0ex90vTW)KbL{CRMe063KbRu}M)f`x%6U``+P(iayawj}!Pv2G~!gt*bpP&tU zdX7op3|4%D>kOgI(b(>ye5_oyGMUb?Zy5KOhu1GorNr#W5!=Bz? zlBI}YXj730ujO}XW$5>2?CC8g!Ix%*U3}9vczD9cLYHD6mtuQ*SCWKtc~blrf7bDfSL!w=i~n@+!fMS={=@%P zT>I^(|Ih#UU;X$0p?Klvm;V#{?_T)b;?w_g{V%?K_mgwQ_>)y^(?=1#re%{PKXNl3I8Vk!l?;Pw;{&W~u~>OoIfP5#1{9WF0eUL=P*j znTdiUFBBeugU*stYMDX4&cDZJe}Cx6$x=Kfjqiv9cC-c?34de?3ehh!-GtUk!2iR9 zfQ`AEbV?k4v&k7z=fy`Pby`v}092?RZRqu=aucc?L&(cfI0w~eK8|Jz{PL1w@Ga_p zOWn@l?TlhDryk5J+`Lfm`)mt}=x2yu%F#}MMy6^Oq?#ZcsRekhnwP7(!f)!ZC!n5`|+3am5P95aQ|-jv>G~TM{QAq-&Nq0U@qY z;TXcQyAmfL#O){?Ls+(4;sk`aGKFIZaqSAn5aJ5aV3vANNDw}#Ryc+bH-p5T>cNa6 zzK;(U)Pwu#!3aKZ)PoWAU`gQ^LiV>5jv>S)6pkUp%_YH$ zT$RExgt!KUV+e7#Pu1(<>LY$*;3?Xh< z;TS?(O5qqnTn{RlP!DYZJ;spOgp+s=d!F||7W8)10~}bq z4fSACJ=j(_hES_Dg<}YDs|v>u;HA$doA@Lr7e%a10@?MBx}hTpcp4P!H+|!Ur`9#}MM`6^f&+$FrAQ;nw( zTE)vlysY763oqMvLHEh*!es=Taijzv-9~%O+m7@Uo4U9lSik3)XPd!b>4uit$o{momJR;|1$FTIs8Ui^awh zc);~9^vIN zUZ{po0GdGSOcdg!7%wGwDZ|SNyp-dm0xy+#slrPwUh42tkCz6#G~%TRFU@#q!AmP% z+VIkjmkzvi;w6EX9=xRRlE%vrUWV~<6E6;4M({F$ms@z5!OJXOZsTPRFL&^A7ccX8 zxrdhpyxhmjB3_p8vW%A%ygb0qP*>pZ4a4W9yTsow;LF$Io7FJW!k@~8ANs{#5RQLQ z1b6r|!KD^|o*jNB7{5Df+`^RkixTS)FhD5&#W7%73>>=f6H55QyGP;Ib&=;$_?aSz zk8?2h<`2oi2b+4}ckbxzSopk@`ij%d`0z3H!6m0OKCGU=XR7EYlju`S6ZB>+2u<N8hl=1gzwld-C5qLzr4_pLtY$g~Oj!%~ijqd(HQ@PDsq)FgB~oQ) z0j6U3b}vYFZu2kp36Nj;g^7}8w_JKm6ZnY3yq9i`e4V9qETVO!f} zo9yz4?eYkJD@Lx{K>J~9^H0kZanaIZLgEjZ;!E34xY!fP*gc^byC;ys6+a^fzn=~_ z^+fL0EAl2y-b55FOwppO?Ix5r;I|f`9|)Im)7sX<+GX4km~v_JOn^BYO?b<&#*Ah) zt7X6*0{+OH|gd!-`rN@ zcQyH4qPWKt_l_x#St%_p)^b-31=nE)r4`%&1=lp;rr}FlM!#{lHO-FUdylwJLyvVI zXr&&IQV&_FhuUPj3iK=u7HY6qgC!a)(_n?N!4oY`xtOP2%sU||y>dl~plCj?N>#|K z(bmDQv)3u>cn$D5-8xO!s9nSZ_mr%fm8fbhsv2$J@h1YkTHSZN7NhTYEga8pLp;~~ z+O)(DLty2>Hkr(0j-zkv6w;4)mWBeIFfGc1ejX`WaOk)g~;Fuuij5 zyfNKoS4Aq=cJZKp2>*nSl%gwgJT~#Vi)C~d%RnEJ1L&q=Fy@J2xp-(ZOVX|4rmJ9Ti{i5T|S}sH*5aQ zq(Lid(5fhUG=+-EnY2cxsn{?VgARr|;?S3wFkB}yaj4RI0bTk9`sO7p(}TxA{)7N* z)Q~&RK}v}E8#r%2ETCv>d<08WpD&R z@>UQ}Q406P5lk2%Bc(s3@C$!%ce0eLau4g}-2o*2P|6oCRTjcqNdGGC{AJwvE8F>N zWlVQRgO3#G%qcKar$LB;q79vO1-e@b^p-TZNKJs5C|FyTT2`P7O((E!qXL~>1-j)L zEYsk;28T7cs=*ZndNmsCP#{`f2=>+#m}ya<=P1xwP#`SZ$YM7Y(lO+=DXd$i!TTB< z(V#Y-SFf<%CbS&>@`c9S(jfS;G*_VlGwlj=G$r8O+A1!t2J1o`PQgtnVB0L zB*jqDt)OeY!Zy);3tf8@c8Kn8(pAZJ@6!D}MfHI0AF6vL3$i1jxmT{BYNUo#MQIAz ziKJ?$XPw3~RdE2 zxvd|?teDkS@Pl6tbfrJ(@B47LJDDEp?eA;r8R)+;7z4TjqWd^zRlM`bhi$(<&~+_& zqkrJ5nqMRb27&OWU)R0!-aBW2N$Cmou&H-2o$C7L6QDhTr7G2vN}`U(tYfX0K5hNv zQnGI_dA{|cWtGINqR*14WY=KQvVh&&7y8rR4D?>@8LF)AezWpS?U{3xz9N+u`um0k zdaqm`>K_=itV=QLlh*zl$$?8f$yBQHT7TktDp~nu|3GDL-%xU(t9z)o?`q|Z-l3k# zp`K)=lwAqJD~I|kyZigT?7ez@0E+YuByV)3Qo!?g%qm5}@6^3>y6&AbXX@UytP?To zCBoGj$VRBoXUTN`U@s{1&F2HDw|j<$(u2Qy>eN*bc>T&d-Tl{2eL0X!Ca!#Q6?93S z@>RvMeipOd_o3dolpOfFw>vrb&iPbt*Pyt2tKp;M5FwI-=Lf&(>;75d^5wIZ)ey7Z z+gEW_xX!ZR$iwgERrta{vTG>FbHOT$Cm&&90TFxk3LYyaSovKZ7jKT)mMbs?zc zdok50l4`WtiNIB?1;Pj?%G3JA=?ys(=eIE8v%=#cdF6<>HBUN>9wu>>V zDL+0<{Wtnj{auNioWS(WG3!EpoIXnqLbpujWCX2|f7RHyIr%^XosU@!`6=|_aJqkB zi1{evKvU(<;o`L%9NxFA-;G)4!gLtu?=zeF5Bjh8TKN5#RhNHJr8cx_MD)2}3T4j# zxvDTUW4?tsc$kHu>GCbi4QCdH_R6<#TmRLosiavLnkph_`RaT=olbpop{J|wsu8iF zp(2DgHG^Ms^rv{!f@7kusbIMIy^yBLS@?r*KJMyPMu!%Q7;bzF*lwhNDh_QH;itah z=M#xbLtR6?-F^NHKkGs-oNPn~uPoJ@@rkM5feA~d}qKtVOGLMMh z=~IRKzE2y?2Rt%gKB84}>jSQtFCX^o5PiTw^X(yE;!i1IHf#$Xx)1;6;@TMCqWQWQ z8#TyaA3n;~3@|tYKANuvY{no1aM64j_ zjN)v7Y!2X&d2#>;fa0G*h#Kfk7-1TGG0Fyp4t@^G5x@yA#H^o%^-zNWyMdvF;sKtR zCl6%^Srf$r{4q}+>utiXZuWThM@Oe^r?h=l9+#f)9G9$4!$nEh)@gP|khNmzMhmFQ@YRh*5C z8Ym9YNmCXFwylo?STiCbWpQAW`Z$0Q_rrnHULOYt1rhxxO9!^YrLM1GWds&slrbP0 zMC2Y>Y;q0}7!UTPOIKfaGWA)1Ds=@Ww@MWZ2@zo$bh!pmgt9m}++fad-RD}-diWV! z=t*{e1#ZqQtl5BzMdS=wdT?=74REh$|Mn~Kc^Vd>652bXhng(m(7iZ3r}}kp-Ls4O zRRA96uP1u7Q$(jdxpZhwcBO`TlnubEBHa5USb4h9^^F-K1WLC7M2QGr2|2lY{b3=ZAVQ6tC+97Ce~9Szzy zVkzq)F7kc}E30M;E2OWDBL*c^ASaim-R&vH5Y?{e%O8(1ouey<0g$#<_z+Rs7!0>`qf_FQX^xuV?dyd zaMA4Y7yH0PQ>kPk$OyV%#JnQFNN{1Sst$C!h-4(d;`6>xg1z13L%fBYzUP7&`C`&tO=H{H<2x*07Yn`K;J>UNXvqs(A;QOD&tdB!n&tEc{ zr)o9upwDAg>;71BeDKTj<+tEY`{&9LBZ4b<*#5b442j?hE*Ft7e%60|$Tt)cLN^K6 zFJc8ccWGsOFjGYL_ZMcH6Hb)}gXCSFt?MrjRnNP;m@ma9Ks_Gx{pJhB4(!9 zJcMrr*+6fJm{(=9$!^o=fhn5W-U85Hbaxv*o3qMmlFdK~MZrMykZk)v@ zIvdO81MAa$$!_g#xc#!P_Jt9@JIDii+P>Ns8b;;2+DlG@!$x%W&j;Z|jj`B{_qs5s z5V7N&tE_4Z5F0Um`HJ!wf+$rQ`bWN{*{yx0qsLC4Y#u*+rJ+MageYHWF!@IV{pL{$ z=nH=mvy%SMdf{3EPPV{NAKWaKsW>co&^aU#oSEjWhM#p`zATPcT}X8e4xT=9x%RDw zhE|!aclQq@es<;Z<=USmo|mZ82O{cpu3DXWexlBzsIHjRk-M$MD z`Z8v9<(43Vh7_TqN|=?sA zW_9OP4>^kBJU^^5QQ`SY%=#iPK|CLL8WG8u)tQ%wJx743RylAf)Gh}ug}VNMOQ9}# z;8G~QM0&Ew`DLfv(*dG9`FRN1yKg?D8j%}3j~Y=Mg$Ds%%a!f(9fIb;ilBB0AOHQe z_^u%{JZJXHIf#hqm#}AlITw{Ohxh+yB!x2O1DCQ#Tz*D%*&{ALBPrAy58Psu`yaRz z%Et~|3XNnDJ(Au;Q1Of22MNnzqOyyB60?4lJBNWK5Xqiix(VYRxsGvg?5TGoV%Ft7 zwdW~5Py53vQ5%sKUyfP-cyA%mX?N84et`VRNa0RoG%=p}ro;YEW1;XdFd%`?8;6OA z^3#JAL3YYp1TL#42f}I*)zk+of@;wlvl4rHE1mE+kK%__CaOuEizKR3c(677>yt}* zhdZo3QT;r>`j}Gi*ld`jC^vX6lE`x5Q+mJUih_CN z>(TT7D0d3qK{bY>?6Xs0HHm7M=Oc<5=!=-uzGnmRQKXPA7*&x2 z@YO4$&DDYrpDGG#@F?GU4syt1&p{4_uIC_!2C0si^~*iI7Y|2-HA8l{3)34(d+p85OU##moCE8kLnHKNx3-rCPhRC^T;IdfMt-S!Uv>Jk~d@>BG;$R zM&;uN$DXwBj9Gu2yKfoV@3Hw1ssBL=pc;g?u~7qDPKaLGITIBm4p;`&BRm}l)PwS; zZt>lPu%34&s_Q);QTwP>zDpKoqK5JOYV}=3QLVyPb zNj{4T-#rNIF$t`T^60#x%u@;=JB3ei3_ImNFF5y|Lw;no9@$Uw8d08#eg1vp()BBS z;+?**zEKy|*YoNd-$BT8sR?ZcKEs;iJex%s^g#=um7ZrPyuy0oVqKG!t*Vbok8 za$wAhdsIptnT>`o#C(>#*8g=fuVgWDGxI3nee$u+n*)#ou))~+`Q}dU8U{MlHXMtSu%X_G2oMX&%z>Y_#EVr^`3*A zec3IaIbYO_EZ_Qk2RUT7e23XRCV!DWaKJL2M?Llxee$2jMtMNK(dRo$Bm3o>m_|-) zBfa22B^-Pm$OFQ6v;9#sY6cb6<_E~1ER-+*s5#oR(Elm;lm1aj?jXLKF6V$UEQYO) zL}pOWQcBK6h5gk49OQJa(X>y1@aYxYt9|HLGye>k>#F`C^BI9@`!0bb2=I}LPc(<9K0AZ+`)_4 zhu4`#i#lW;x!j(gD|}Z``|!H&DvAskp0W77E;=$qcGrUyK?XZm5oDqI?0L*Na;BA6?|5nv`S*^e7D0vz-=jBr zN7VXN0-H75`AJfvJM9>h3XQ1RE0n3Co2NU zNAw$LQ6G6YXd!#G3jY#A-c^Y@krvS^&qfN>r8q{v2Wwfs!#@pM0KXo4?0hQKf8*op zsiEFPPsL!*}FmksGFd&sq&W6nFj=c!QVU*+q)%d@@P90XUxMG1?YsJn#lH+cZVU488=%Ef zKK-Jn#M%<8-eAFB<$d|m^>i9OxjlHPr~i5?A-?e#b7t^Ido9Zu6FPs~)z@`3c@2J` z^quqYbGX;w)vf-%ci=Ff>`%qiIYUfXqL-`$hd9z_N)$)gb$<}E4#C@B$E3Y!`JaAU0DrW)!haeDXY%kt zVex~?za90Ef9S)qPBMR0RhG+f#IkC*7rq|C$xjSMMOEWcRd|ZgfVu@VB0kHub_;ix zAB9QakA+l4KbDmNJE9b2dv)B*vfkvH_3|2!gnA11@(d@5* z{3eBEwXspdx;~gd(uUeylSqkK-AaBvB0GHehl5|THeunzBo1o^lZN#fw5(Uy1ZC0$ zKt$=WWd-AmSn0B?J>ra@3yU-2ahO5WGK*?}Fh+_JKkENGA+D#0q`WGw@2j%Dih%r{ zUSYSaA2N{|LFBj#*(v(VZ^^>h3pSQj%Y7lxzd!CzBvXU$YDgO) zQP?nm&tye{%yCk^CvzkUV~)=xb7u|m4So++H9J=urz|0# z?(q`SUl5_vUvs$O(ZtkRjV8`-xSaB}>=^ld?SUBdf{iGm`?-K6jrZCjdL~9dFk&FQ z2C*C$oqq$}m`?!uJ?Q&tO^P*trz#cn|L=9R{ts2H>J=-?f+C&%Q+l>uj`g$2oTA{_qtk}s>%KVW z8~R>1+yAbb&6kz^L#q?!l;7(*L55W%exJ`=>!O{-?{&o>!;%$KxuaUt`d-%z0zK8N ze;}zAFTVdvGB_cGQ!V~E8PAUa9h}wBJ|NQPNS|{*QViP7X!b~Vd5MNz==L*JwUqC)+BLsX~?WF(S<-2=U8-jT+q$-b^DDLBAGg1b_^ zUBDA{NDuT6^;_0UaKPYtI?*+h{Bj`qr>s*qDg?g|YgsQsV+MNL%Hs_O)wWeV`cLT!CmCzhYSkTrJyBJ+NEJT49Yr`2na0 zQYKNZz@c&oCVINlfW<1a&Dc1-Xo@ep;KTvn4|MfiOX5H?f5Hf)NkV6Gco-w1Jmb_WZf z-SM~**Z0f*fg4=|36g-$BJRmOu;m7lSJ85o6^~iR#Eq<-fkMyeh2}vc!In$!*U8Ju zvMdrCi!~P_xT(N8Z0Nz{pI%S)b%W;UV&uNv{m_e0sX_3V?&RgZByhl9Anv&qCt}u1 z&@JFswP}NdskpH;l1b4fs2!eiLNg4&&kc%ZPJZ3XzKi~o>IZux-|KuN@A5UUG@RXH zZMuy^A!vd`|Bb#>e^=r%_X$)Oe)4zda(7R%`zx+8cR{GB>>1Rdk#ZoJ?jP(O>K_mu z(Ld086+H*lV}9sE-96}*q!u=D*Y!j%m;#^l@(+_^1VDwqzMkq!4s>1VP4y1-qEBOW zy1xd^5x8;V20KPi5`I>_M{*E7fVG2;i0TO6Wq-d4#jsTpy@Mj!pe1i4d$0BgH|kC6 z7DGJ*m&>Ar@-=8hG#7g+oIJcHR2&!rM#yw>CvB@+EA>aqFN4ss8S-umP}f!AJ2l9anGwq(TX**EfSuTMmpp9pZo*SQLal%K;49E^)nk=rRXWY(I%nAsyqKM0O2ygYTf2G}r;UGt`T` zG=2T@HQaF5w=~wU1+Kw|+0e_;N>`KUrJ{Ftb$2JzLzgKMV1E+#WIU)2?7JQBN`NBJ z>dDK=;a<=agos$99+>0|UmohetO{~Pp$GN$U6$b(8-8$5bc1BN2hL`aQfPCCt;zqd zy{q|cqYC2VdQ)%Jl7@m1QY&g95G7EzL}?IhMM>W)6s=>&F>GvWy>4+Y za6w2t0yo6}zyWdNPziD7uRyIhaO1$C>F+mh*IwJHYjUZOy4LQ#_jbPLy_wm~t`clE z0c_HW#==gcp@EW}UPvI7dXy+TpdCw;9nb40s48*-RHj2}kdP>xaYPXzPq!27VEy0I zI2Jmxg?}E72?a-S;o}uTmIXwsx9-gMTKHG#8VoSDX(|W$iy*K-m?t<-bR=3#A;!WW z#=;23!e)d)5@pWzaQqwEG9BlT{zQg@Bwb@-jhpBCie&<^7u zV!fvk7#B6Y0e0VY1`xg>JUp8 zCTzwA`k4ZbuQB70p=q0IoTEdl6fdrvfTNmQb%L{DfZBPM9Zl`(V+i1fIj3Q4H+5FXY&24`^IZocWRS$N^_M=Joyh=U>r_b#+ zrpkbc#bv9V$#TyL@ABRA2;ewbdVS7Rt1omqfG70J&-tN@cZ zRW|P>O}W0$+pal8bCPl!-H4o(&#pPis|DxK;!5(`##JkS4SgrSW~~>TW%INRC>C?^ zl%_k}HS?Q|82>i_pjn(!TL>G$6c0$uBY{5=X%lRE{q)f{U;jWF*^4EE8K@UPUOvne#Kg<gAw9Q-31H$lS(mA+r*VBI;nAQ_p*{(&CnFfuPlL)db10{HZ_ zv0Ov7>ud0I-Whm+b`VVL3}~!^cTKVB#g=qclNv0JETx_u981d?X!|a#9m$v;W39AO zpp>*v8D0s%g}Nffst)TRKh6SrD6$EAoKfR^(bvWJ(*Z3r!#I!n))+^Fww!?-262Zy z66u|Mz>|?|YyBvN?ConKNh3oH=u5<|J-npUH@U5MsjT z^Jj#9f;WD`X!!S_9IB0-w;H33dM8|dA`(uxq!&n4ykeyyUnwf$iA8d`Le0yO@RTKT zo>b0@N>1YyDRLzOBP0E`TG1)d2qh2<^z*>P{<_|7B3GUPK_P^hxEg9iB)oa>82}%I zOzASI(Hj%6{`6A|5a{QAMKG?gv6vPA*Yeh)m;}%B2!xVxVE~Mog$vq8fKaI$l*D}?mj%Q4ME-`WEqdx}916)0ww`h0k?Nrg2q z>0f8GyN(!Mh9uigVFLs&lHtMjAn3Uxk+CUa0F|g=c)9RlY-}%cQVFe#8GO)IpWg<6 z2g%^u(Ql7>4Nn`I#kSf4TDn`4_3k!|dYT`z+GEzu8b(K&vCS8Z?zYTkW+opdU*F@X zhTDbaw)=|Py$!3S1&i;9o7jy6ZJ8dvN)9M%$!CD-9ucR2M)IAYzMQU29pwyVYz63{*+no4hg%NYYjt_IJZ%&vHH~D-h@jC$ERNzXsra`Z7Fq2<1+n&|1_xTn- z<7@slf5*@Kil4NxWAkB5`nqww)sD;lfA_f9c7JCF*4K8hwb$A~K(=8pS$t1y6PR^a z-ufN_6|5e1ITJfsD9OXo&DobvJB9_tLc_pj;B#gq-y66Y)|7z~!d0w7^GubML z*!uN)Hoc7WxRa#5_et&uAy7b5g)e@CZ*tHF=mNs58N=GgKsl|YYj5k3e!%4JZPd=X z8U|zUUXF2(k$7PGmIj)x4Y}knEOV07N6IzQl=W&9{y;L|czLlE0Z{UCWpn5rIg_Ls z#hV!ep;`gKfX(p$J>9huKpI<*Yun3jwT6*fGb{)gX*CQyISfrfD?Cgi+k$Cj!D5>+ zoBJ?h(SpAuwW!Om^(1T*mUP#6jpl(JAe5UiybY9Vuo;4($Dxc#-h>BZl9aZyKD&9a z_DnCBJ6sV0Y#K7VhM@=rjJA*gM>BLQ4uc*#uxf^`fB=jGg9SyR9?&O5Av4mZw_UEj zwE>%lclp7O62&3z!T-aD>3D*1wsJW`=)ro91%d`gvz1T`r5IeR%7vMqZzGs z5H`IHIP?%WjQZ=Q>^~{*q3hcbQHh$qBhtD@5Qu=@HD`dq{sKziNBIfCVepER0Zf6( zaT|%yc6gryM{rA1E2Z*$73P=>W7!UGw~REDV5T`Exy5G0MFA}f@a^ENZV@s?4rYc2 z_7@_vU%j552_WwXKLk+-_YSJjX9sA4T;T(I3xZK&zeBK5(>!1w1k0K}%79oL4`@1W zk3S$A$1M7dP&HFCDmHpfve|4jlYyZp*~BtI+0dMg^vHHV=}JCive70Qo}u9g2Bw(O zumcTy(sUIKqZo&wb}@l@E-}Y|ggGRJjl^CK8?iXJb^!}Z-bLUZzauc^9TQWeblc0s zMFtynpkW^dE>|)yG71p{F3{>E6Z5N>O+ar?mxr@3r3WpiguwJ+2*al|-JXU)G+a*i zzMIv=VxvT$fItMH7alyv<=}YjP%S&L6?T}Z!79KVxM(HRF^~bU!$Fx?$(Dk>A?yK0 zKpl(@<^oHRvoWV0$Yz0&vtc620CPqRs7rz=O9bzBMrLR=>j)`CzoQeR88RW{FhT+I zG})s(iTN7!C^zK12A&@|Y{CVdWB-QOY)|wW^(>!)&XYaMcQ7!#17$Z92K+AQ7&{U; z6M=_7!=Qc*M)_+q z)&qNz3xa1F`ozuzpA$4={b5y#G#Gazah)_q1zN0@c?Yl(8k=H!kM#hsDvS{`xeaJM zGDdR&!@BIHu?;lmOtgT&bz7UUC(usw9AxEj{vhdtD-aH{s#zbwZu@A?F}62gPA$+e z8q@S=flkrbXmkM`g1V)6;h@oIJz~L`ca@t4n~fg84diDUtD>biqdhdHk>ZR_XgIZX zSDUfLFo)u{SV{#q23-Xc$!P2#!g{(OwL7jmh_Ifn=pn|4X=oJ3gwYmtYp21&0dq(F zF@{D%4-dctH8MxjGCQEjnmUkaOmsjA?P*Ss*&Z+uh&F>ik%$MrOrSqt-IAyjpsox% z;}>NHg46ytC`<$=e4yuo91;mYCV_lunQJ+wrX-q8Q!;>uL?;+HjwC@@6PF3}Z8{5; zfa82!`;U6T@(DDU?lGL!XB&)2(<68(M-SbAWn~7CjoJXzhiG_%Wr~C}4548-4dZCo zgND6nD59a1hJyfF!OFbBvO^O9^1v6d%w;s(L&GC9yabRybpX8qS~G(XL5yH`0(g!Y z1+b9~>xaPW2E!gS%%ov|v<``hJh(sN_1%WZM}wh^J>HP^bQdC=n1U?nUNBsY=FsIw zXd{Dw7?{D?fF7Zfte??o^aHUIU?JxaK&+=B4TS_AyCuRN=m3-lP{z?>4hkTyLK(*o z2ikj1qjAU*${J1_GeYRL;ly;76VS0w#StEBn1#deHa2*lfeM( zQ6@k)GytF{Dg@|9!;UnJM5BNbk7@v>p{)S>qTc|D!TkiN5Y+=zp`|q3Ps7X1a5NEm z83Xakk^B+OMHAVGz?1=iJ!h5!EMwIIB-vcpQ96)*Xazb6aq}IrB?N>YSfM}UP6+UC zCe*Vac7p@Lu4M_HSxK}3xQOWla0PM&n2S8Mkc2i2SRq_!LqaPKKm&M_u(}KY@*wWF zLkrMNWI%K!nB|)N8X7}8gk-RzXefZtkC=eNnfAdPK~RrnYxKco90TylMDsR>cQ_h_ zV&MZ%lq647B2$Mem7=l~g+ew!h>GDqUsUXeIwdM{OJtHRC@mGGqroCsi6lE4MGuz9 z)lnj~2oj19^T12WuU80l&iaSUv5Gk8$!e!7>qy{`%t}0PVG?kG` z2~E?rh8{E$qe}}!B`S<{N|mS-B}%a*y{uT$r7$}?LL@GPlUR&YBFjapY!%&0oLr?A z$;A@XY!`aeX>>~zD^Yh7sR|+?pN1k8Ma7_3bg8&NB+r+i)DpQ`T0~3lCX(mMB$ylb z6{A!XX(>GwGLc#eRd`sf^+B0RsaisJ6{%2yaC$r_T&0o}<;cnsC2CPF7#>Whl;)JE zHNAr%WvoOlQR1PuRJBsLg_t29EKy4Hq}bM2{g#>}Nr_r1lIgTjCBSG(MER{aq9i#b z`T4kIODmmb=~DH#$T6T>Pl-|`Rmi_?6DQA8D2u=l3b{!36<3->Tmr@@OOYsxq^fUA zh?1zpN@+2c@U?iIW6Pu>>~mijjP5j5A`_L;rB?O20i`Ix^~LIzhPsMKMRA!@nqSbW zDl#DrdjWJLZ$+i;-_kxs1bJE7n_6fsGQo%BWi1t2C!&2zEzOb2zz{99D!dq^LnSB~ zCNoh~DlIB0qH)c-ql;-};O#|%D5)r4u289^VwE6WDH4ODBW+cB0XSDKCV+$Lrd@L@ z!4z#NR}&tzSb`!S6-ni|!vu*uUtNH*HGOI^t<6CvdZb!v(U)jiu~eP_%N}Lp#jgn` z36jAQnNBm!>QxDHWion&ge&t)iXi5|8k5kIovqXLiAecHYyc%;DJmg6DYel{N6{=wsEWqX6k*qh zf(VfUCcwBrtX5!1zaZeIsFgZXv~mp{N$Yk3tP+kYX%gjNsaT?FuGe<1LNTQ}k5LJ< z{-m3=v{cB4YeK9Fb{8Bou$dv)s89w(J*ip>C6N;DzR`n9M6zb-x)~F+Ecw6d2*Fh6 zNR-KW+KzRusKN!j?Lc>Uf^~|v(5zyxR%jG+S%O3oPwPNCZh!F!XmqG>Ny zfj$wmM5OcNRwP&|&;YMF5T<2Q6lX`5LOc*l)hN0YKq`1|tJscmr1CgSE54R*s&r$3BIH#a)#;XR$@1*ZrU8;-z>PzfqR5+p~U z3Mz#YkpvY(S&9@u%Z4u{N>CpBNq{#AXlgk5DdAm&QUEG&?!vJMiJ&Exri4-%N}%QC z18)IB?tiXTK}#$ROBTb~O9Fhzz6g4h!>5SW=L-o2qGrjEuQ)oPX_s%?q*dQ9dDj2q zjV8q85d@dPLj(&7W@Z>Q#(-q;NW$2driQbv%uNXkgc0-J3=YjJu8#khakj3{43JUFC$F z5U|_Q{gMQi%i)kFW+i5DpCN2b2&|(mm&bq#pmKOjC~&!k9443RXl33OTH#S~K>{wb zX+|!Wt*wJYCuH}`EggXk9wr+KBpx7w0<iQQ6FsCSwO>ES)-O zd50$-%}#Dx)v>|&Q^SrQU1bBSgY*;5GG1GDE0k|{?LBr$aIBO3)u7{37l}HdpWaC> zIQ(++*u5q(b)?oge00V_`^L7%JJ=k&WN~M4xL>Fv>(b6Mp25dJ4O7H~j5byX06yCA>6msa`zYQPzv6J&AP2Nj&)T;zh#o7*3d-<(dvyPMBX_kC`c3_4hR&6`1wny;1DY!oit4=loo>|IBG$>fenaYu+4kG zr#E~oVP<$R0P`Lg^oF@Ih7X~SNUg8o>4b^mf&=3>smEi{-h*2~fyu>90rX~YkujI6 z^-KmIe1(AXK7O3=bfuHlnMzS{l0x3RBf+k(7@{J851S2Aq6A_|2@x{xX&$ zWJe&YM5$P*P$}}%JUEjUE8v)kH&oWcT`l*wB*dVr102iDli1uT zh5Gp9OVtG>IRdew$R}5!mP^z=aG85@J*{Y!n9MT>-t48n?NR@T2!5xmN=BAVOp zzflov5g&R<88ef>#ofMKC<1zO#s|?>w=R?he(}rE#+^7yO@WV0+emjJQ3-SgG1)M=>e0y@*&Om&1biCQzipcy-yA4t=TT z!S7V?k5fb{l|n4VmsZ{oSc^P0{R0-TArJ1&ydqH<&6NW`VbNv*cxnYtsum!3*Tz4V zTV$BtU=>}-Uy?k~D}L|YI_2PdrVA=cj8~`3W!VqO$lZ0~+19m%&hyQduMF~FtV>n@ z==$K=f#|_oy@Kv8BVQMvH;FS8%rYf6?|Brt-c8PD{}NfB@W+$Jl-QrTSlJDFYj7sL z`1h1ydM_5~7Z?xQa&q0CnBKASXctT49oRm)-^^eWI)JUtg~P`U@0L+%PaGGg+&ewJ z+qeOpR*cVaR^a)M4$TVB%Epl~OBfKCrOMI-n=Cj9W+`$8W`)bklG6kLBU3WKE$G0N z^`(<#L5#{P{S#48oSTF7`QPx^Mz(0_i_5$gymd+UyBG~E8+M&2nJiHRmpXcvmPqKE zA+G)G4l;Df{?;P?7nTWnAn-k1`IpLT_5%@eBnwzSS0rE)=v~51W3ha_82(N z%iw$;2PgYHxGUhNEyzNUEW|ejIpkbr@YdZnXa=;F83il^=mhsL`c{zt4M{wjhl1R& z=FcDHKncDfM<&oVlD?tn?lt&J)HJx4s6e6wZWw|uZZ)_Te&+^XlIq$O)4G*`B_M?f zE%1{FnutLk1xQiAXw=|XxECedmN0BRiWpi4-RPr0f{Jb{ZFRH4GXIfIp^)h%n#bM~ zPFjp|dT9WFvs2~3jXkFO-7mY`V5EM zd^F5-Ak4U?y=FCuVLotcY)8EAMBptF<^zBE zFt7ODUIM-2??>=W-v{);H-9;_Q3HmlxQ{%z_4@z?mvKKqpb_@JJkUB1%D(Uy0F+?R z6Xy_e01tusT#yh1FaT;W#gE2wpgs?1V)*3J62P>;!3EofgMN4{MZhBi8}Yy%U#kFhz zk|2a8lo4BRKvg$F!eEjF3sG5YeNQ&QBr1bQf?1nHB~Ui4C?=F1gFv;+da@i*!aucT z^3F-F22@S+d2e)T&q^QR)E)d1OP9>99YLVK#uI9d8U&QEsS^q_^=N9f(Wj4p1oufXlt@&`vNnQG>4dT`#{uf};{YRuYw9k&%=$%(bIC)0VPs_T~gG!RCUmvsfGkgL1?S#!PFb z<+aafUtjA+$-MS?4?i+z+XoazkE7CYwHY&!ieD5*#R|PCPwYGf7HwOcCl1m<_+pt{ zjK3*`6qbsYAT5HgTXTihlqF_oSs3~HQIycv(?7sp=ohHb$_;B(ZrM$qdoP7`wsN^m zP>}1UqNPVm$Qhb$opHBRTy1P^bX6qIE$dDm|B3<5$&M_4iN)^Q_AF)#A13Ez} z3j7#c7<+Iw2HsSVHx(>^dP<}lH$gBns5GjF4yVYf=zrLOM5$K&gD%vRA-0zVL40PC zaK8R>K{F~z0!@D&b${H>@ivuSJ%8XgdcT=cSsxg-hFx2b_)gkqX%RAdnZ8y=z=27IdhRzdne@`7(bnk2v|HeG_(;4RuGB&3D)^F8Dmo4)O zeEa1$zC3l?b>3{YIr@;cVovmev4h_}cbXXx_Vl-)4Z%;JZdAYQOj!>1lN{|jKAAIP znfsN&X-|9axyw0s*}-|&FE6gIx_rMbXDf64>y)r91*i6_FN*{?>1ZIU)hvgzae<= zfx+nu*)wmOhOU={zOP()pJC^5aDGG59HULUpBLDs%v`j3Xjg}O<5&3JJ)1`iVIJCO z|HmsgamM1D%hz__JKJDUx<2c0)%6{$B@(ak$1-FdF7K;Xn;rG{>+JdD{Z9SEMW60H z__X!D5w9AJ=3bwC|G0efx!tBK;>vs;O`Y1sX!9tlk~5U5WaVhK0ezA2s(nyk8n7ha++P~aa4v}^=IDu@d9r2;~z5WEot z(x{Na=)ZlVswBVO5=guykgz4dnp^Swozm1nugxZpz6GQCs?5+%I61tSD$Y zR@4-lRcHM&X6w!1+?Dd&fYK$kViWJPVbk8+xy~^aOpnkK4Z6MeUw#%inywJwMoec1ZWYlf&0dUG3o3uq^MX$a&x`e%$=*@weUkopp)t zlDdCWX4kfi=ckAE^*Qf$r?{UyJw$r=uPE=wl{Xg%sJ4w*CF%D=<275aa6;KNjIMMUAX4MwY7P-_spso_Uyv5 z_y=(v8Z7ur8b;)o-~TSBG<&_zm|rvJW$hZm=l}k!XdnLvuOGsKlK0#i8#Q6SUc!NM ziz9v1GhWNzl=6CcS@at)qozx;-DUKszqfo@BD23z7I z%9O3AJ?U8yOgN08R(s$#H={Pq4J=8fp_4w`2JtN~5`|L+INX~sv6w2#q&fDpCurwSV{`$}ij3m)`99>ozyo>{6)|DoKaKW*6~yo1KqXx6}QNd^J_E z5G&7PR?MO*rc)JDnvEo2P!*%8uDb3>!rJfey2G1yu0pKxDOO60M9MNT{5zfkbrBWT zEP$l^oqTzYt&%C(aPh`pq2tt286@9uf(j>+@I_bimAruG_@{tM@4opjsus__nOg6Drr9m=)Hx9|N1Q1$oV-VpE%55{W}otO(dbV*yFS!E+HqIxvLBnISH!zq z0~XKfCmAz&#Kf4CjEja-ho7;FcWCMqF){Vj+K+{Ix^M&?=HBmYzxdoH$05^0Za>OB z64kkMs9Tf8h$WNNqpM#YYe&X-?ETJk+oEMG!@182-W3RD)OvLFDC`v{cGi>1`^=ts zcl68MKQzU7UjNYX)Q*5gx%=vSYuZ0Mb*;&G%^d!$*@?!T4PJ03TyWm!YkT|o0q zF4!0sqJL0-@4;28@2$Ud#cF(y=w89TgWB7TTKBU3o9kYoJn8K9y(bjN6%AX|`@&f4 zC4>jROJ!G!#5{xD8xmjL`eBrV!fHfx!{B>i9+DsT^-ImE+UF<^oKa?_g^)^#=Q zZ=F~?>(p=k#Nl`Pa^{We!XCmt&0bgHY`Ie;+VuSDL3`%T8{vnH`M+J4_&lBUM_ty; zi|9gK%#Pkovli>ccQc(+?tBV$IIwQ+;;zv{oB|G>sjpu+bg1jQZZn)$ypMG&f3@Jv zuEH(xvu-~rDYbk0IA~6pZT#m88{7&??yq_GVd4{m^2gGSYd%oVmgYg0X4_j|e zD(l|gBUAgUFt?Aaw4lZZRN&Dj8=sutcX-Ow z-(IOy=H17i$#%XdyUIQ?@pjztC+z}>4Q)MK=UQxDZJND}OdUUW@2Z0J>rO7rcw1N( z_*-^~N;0Nf(&QI`2@~eqPm#MD#=ic}_H0qd7k@mhZ$GblSAB!ysPTTzU6XOI-cQm*n^BKe9lAToXPtTfAvFQ*>nhD} zQ-Q;RB2P!&hNJeF!x6kH23GyMM|8}VPf zP`0Dm!mb1NPsj8EkxZiErKhFwqSKN>V`8X4Z~ur$KktZ;sBob><%anf77i_^+;sT$ zuy>kTR8-8<{%D}K(l~^wWd06mnMD{(z|E9rV zD^6Q~>}lw|A*H70s~;zh|B%02Z`0$>jcw}FU)rx5Wbd|ic(-VS#aAp2Dj)Uf?LXu9 zn#1N-9uHhA_L%ziSx)yj_s7%bTR&8vIdsQKTs3BC@xI&_hek_li}Q(-k7YY;396yz z_O%22Hf)gJf3m@6di`ymGvn%CAH1;hfODs17cLx)u<>@6b_z={?;I18cRj%9H13vQe_kkF8c==g(b29i&kwlShrKE}B6(6_(J03&&ZDE1XJ+0@TsV35 zy2cykmKm$2ZB2F4A8_cb^}e3dW;=fl%U(0pP=D4@wO-MXAj3&@o8LUxw zf<9x5GN_)K(`YgUX_s0^yENxS{<+gAtS?wy;HWt5LeXg#A$=0T=^7 z7*lqh_+!c2vzy8jd8H>_UJqXsKkcb3x8DN0btR{}oc8&&+?ITFt$OD(BT-_L>ipOT z`oo6q&l=s^(p zdh3!9dKLJexWbpxe=^qpoFs%M@KKO;fLP}j7!ZI1Uw{Vn$LPN~EdQfOwan3I`w+A5 zLU~U=Sj>9$Zj^L-p*r34{9=7nIrjat;RW$t*$4LB6YU+mrAwY?-{)3o?U_R7aC^HM zj=$(xOd2ktdat9ROnx6QeoT6#;jf851#>q4_4_!)x%2bPl9fBYi%1OXaoS`ayG#D4S9!Y`GowZ}o{Ik{zgKf~-O?9B zJ@kdoFDJ35Y#z@Icv$MN_R*XZogVl^ZEbjYHFw7%Vh>MmTIq=o+jk#{>et_T9>Gd> zP1HrI`w*#a|Lqn0o1y7nas@yCN6%wFJgm>kDLCDE`sBX9{!(MR|Nk}UHD8gnK`+qT z*Y95y^!`FNT)k^?oP!@4e`dlZreZ2p@xvFl?+U83F8;yoTlkkDHYGkzbM=l)N%PVE zgOq>b?u~!I-g5WGKR5ZO?%tgH<9it0K6rC>*NQ=m&hvk)b5U$~vgG(H;m(t++si(^ z%PeX`ZPzBXT9xj6EM4N&v%uANRkV=SKeL+=jTH-BW_DqiT@z5U0GH9ns<-<#yOEhoF?N@m!j z-+!5tH~IU|19L`QJJ8)=O1s-3Pup}KB%VLarD)&0AL~248#Q9)z{#aciq71c_tx=4 z@Wtsb4=&c9S+!`#0=w1Bh|xc7Dxd2b%-iICFZb!|m%B!ad%K$q6@+9Ct{=3V{hi4K z>#d>&vtul-5KkDP21o_MqBtw+s| zW_i@vZED~68{%QF=6vU4f5>G`?@g<(Ms=E%GU(U3GY2y6NUvw^Ienel{qgwy#P>$K z`XxV_@6;yPXZy)sEJ34DZA!r9M!oh9=QoTDaN=&g9C+ny_7B(9GzlJ%$W z-f!yu^R5&1Zu*c`;ILVq#thPqVTEoo@6L*{*fwk|De3X9W>P1?#GCoGkQ=KgUo`UH zykE8YW{L&>a)X6#y;iX(S6h?j`(L0x1=kB50giMU0;Q%xTP~YSLg+<#Q1E{`p#|V% zDhB{bI<(am%ob~E-|QcYYXe)gCGmIB%^3@PbtP-{DK2i$;MDQzbY-Hdw)MFL^kr03 zprx^X47WaRQzw-zK6*U#{qA2(c`mCLI`<7Qaz7MGKD@(xdoCg)ciN7`+R#sV>%6Dt zgmyb#{9xjKMr!vrL>pQ4sVBav^G97-H0|kne%f#g=4|+1UK|d%dg6@z)0$q3e-0(y z?PGaVTaOk^5e@qJQ&_c}QXZZ^ee6-gZH9ZZX1oX$Rx*s>;;2Wv^R$2ObZQbw-?+E7 zP9#+l-6^}~bOU6PjJ`-He3dNPH)r4eZ`|e_ZJ3w47?usXtaCb zI~7&&u9X1NM_BQgs(3(E+@&gZFnI}PqgFkLPOyDxnibFL{d>j4!**MijgSB7{LAfc zSDkngN>$AMAFvL-Z8*r3k@{+5|+W zt=Q%mxbwse{>x$6f|Z#Yt(Pv|*^edI-1&R{u_nI`dE=};y*)9q_Dnb7g{}`Xr`47Y z-ha2$@Os3(JoGwv&8XQ;)vLN6e80SOugKCl-8mq{sqX?+UTXRIZHrxE_YJI{Crqw! ziYnUYG;AEXd#>R1^yuSVOI{@Gi%Lo1EG;{>=W0{;Z4=tv^1q+dQTD3#bm7GOePd3| zym9OdV?TRFY+mD36!qP+*do!;;nHbSr|oz1ijQqqw8>r1^Lyj`r7P|{UpqiD_V@SS VSA1A~Xq)uh0^wu(F;p2s{|B2kf;9jD literal 0 HcmV?d00001 diff --git a/output/PSOPNSenseAPI/System.Management.Automation.dll b/output/PSOPNSenseAPI/System.Management.Automation.dll new file mode 100644 index 0000000000000000000000000000000000000000..3dfa7d84720c8b704d1f2c07b16cb267dd5c9c72 GIT binary patch literal 354304 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

X&fnF+Y`s|_cAmggqp~?TFg6{uPFy`#8)_bW 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 ReeN3#mjAnAf2&5|{{y$RYIpzu literal 0 HcmV?d00001 diff --git a/output/PSOPNSenseAPI/System.Memory.dll b/output/PSOPNSenseAPI/System.Memory.dll new file mode 100644 index 0000000000000000000000000000000000000000..46171997966f26bda11e58586c51b2817bde5cca GIT binary patch literal 142240 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 literal 0 HcmV?d00001 diff --git a/output/PSOPNSenseAPI/System.Net.IPNetwork.dll b/output/PSOPNSenseAPI/System.Net.IPNetwork.dll new file mode 100644 index 0000000000000000000000000000000000000000..ce68dd578481a27125bf1ba8c1487f00f4199ae3 GIT binary patch literal 28160 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 P{I8?_N4x3&m2&?VWhv1N literal 0 HcmV?d00001 diff --git a/output/PSOPNSenseAPI/System.Numerics.Vectors.dll b/output/PSOPNSenseAPI/System.Numerics.Vectors.dll new file mode 100644 index 0000000000000000000000000000000000000000..08659724d4f8534ee97a543e93dd6d3c702fcd15 GIT binary patch literal 115856 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

A56h=o3u)Ar^3va&BemI`C*}HC8>xqw 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 P)6yq*$X=&d&4~X8FD<25 literal 0 HcmV?d00001 diff --git a/output/PSOPNSenseAPI/System.Runtime.CompilerServices.Unsafe.dll b/output/PSOPNSenseAPI/System.Runtime.CompilerServices.Unsafe.dll new file mode 100644 index 0000000000000000000000000000000000000000..de9e12447827d55c3995c3ea6b26f8ef50d1eb65 GIT binary patch literal 16768 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 _loggerMock; + + [TestInitialize] + public void Initialize() + { + _loggerMock = new Mock(); + } + + [TestMethod] + public void Constructor_ValidParameters_CreatesClient() + { + // Arrange + string baseUrl = "https://firewall.example.com"; + string apiKey = "key"; + string apiSecret = "secret"; + bool skipCertificateCheck = true; + + // Act + var client = new OPNSenseApiClient(baseUrl, apiKey, apiSecret, skipCertificateCheck, _loggerMock.Object); + + // Assert + Assert.IsNotNull(client); + Assert.AreEqual(baseUrl, client.BaseUrl); + Assert.IsTrue(client.IsConnected); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void Constructor_NullBaseUrl_ThrowsArgumentNullException() + { + // Arrange + string baseUrl = null; + string apiKey = "key"; + string apiSecret = "secret"; + bool skipCertificateCheck = true; + + // Act + var client = new OPNSenseApiClient(baseUrl, apiKey, apiSecret, skipCertificateCheck, _loggerMock.Object); + + // Assert - Exception expected + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void Constructor_NullApiKey_ThrowsArgumentNullException() + { + // Arrange + string baseUrl = "https://firewall.example.com"; + string apiKey = null; + string apiSecret = "secret"; + bool skipCertificateCheck = true; + + // Act + var client = new OPNSenseApiClient(baseUrl, apiKey, apiSecret, skipCertificateCheck, _loggerMock.Object); + + // Assert - Exception expected + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void Constructor_NullApiSecret_ThrowsArgumentNullException() + { + // Arrange + string baseUrl = "https://firewall.example.com"; + string apiKey = "key"; + string apiSecret = null; + bool skipCertificateCheck = true; + + // Act + var client = new OPNSenseApiClient(baseUrl, apiKey, apiSecret, skipCertificateCheck, _loggerMock.Object); + + // Assert - Exception expected + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void Constructor_NullLogger_ThrowsArgumentNullException() + { + // Arrange + string baseUrl = "https://firewall.example.com"; + string apiKey = "key"; + string apiSecret = "secret"; + bool skipCertificateCheck = true; + + // Act + var client = new OPNSenseApiClient(baseUrl, apiKey, apiSecret, skipCertificateCheck, null); + + // Assert - Exception expected + } + + [TestMethod] + public void Dispose_SetsIsConnectedToFalse() + { + // Arrange + string baseUrl = "https://firewall.example.com"; + string apiKey = "key"; + string apiSecret = "secret"; + bool skipCertificateCheck = true; + var client = new OPNSenseApiClient(baseUrl, apiKey, apiSecret, skipCertificateCheck, _loggerMock.Object); + + // Act + client.Dispose(); + + // Assert + Assert.IsFalse(client.IsConnected); + } + } +} diff --git a/src/PSOPNSenseAPI.Tests/Cmdlets/GetOPNSenseAliasCmdletTests.cs b/src/PSOPNSenseAPI.Tests/Cmdlets/GetOPNSenseAliasCmdletTests.cs new file mode 100644 index 0000000..d2ed432 --- /dev/null +++ b/src/PSOPNSenseAPI.Tests/Cmdlets/GetOPNSenseAliasCmdletTests.cs @@ -0,0 +1,354 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using PSOPNSenseAPI.Cmdlets; +using PSOPNSenseAPI.Logging; +using PSOPNSenseAPI.Services; +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Threading.Tasks; + +namespace PSOPNSenseAPI.Tests.Cmdlets +{ + [TestClass] + public class GetOPNSenseAliasCmdletTests + { + private Mock _apiClientMock; + private Mock _loggerMock; + private GetOPNSenseAliasCmdlet _cmdlet; + + [TestInitialize] + public void Initialize() + { + _apiClientMock = new Mock("https://example.com", "key", "secret", false, Mock.Of()); + _loggerMock = new Mock(); + + _cmdlet = new GetOPNSenseAliasCmdlet(); + + // Use reflection to set private fields + typeof(OPNSenseBaseCmdlet).GetProperty("ApiClient").SetValue(_cmdlet, _apiClientMock.Object); + typeof(OPNSenseBaseCmdlet).GetProperty("Logger").SetValue(_cmdlet, _loggerMock.Object); + } + + [TestMethod] + public void ProcessRecord_GetAllAliases_WritesAliasesToPipeline() + { + // Arrange + var aliases = new List + { + new Alias + { + Uuid = "uuid1", + Name = "WebServers", + Type = "host", + Content = "192.168.1.10,192.168.1.11", + Description = "Web Servers", + Enabled = "1" + }, + new Alias + { + Uuid = "uuid2", + Name = "WebPorts", + Type = "port", + Content = "80,443", + Description = "Web Ports", + Enabled = "1" + } + }; + + var response = new AliasListResponse + { + Total = aliases.Count, + RowCount = aliases.Count, + Current = 1, + Rows = aliases + }; + + var aliasServiceMock = new Mock(_apiClientMock.Object, _loggerMock.Object); + aliasServiceMock.Setup(x => x.GetAliasesAsync()).ReturnsAsync(response); + + // Mock the AliasService creation + var taskCompletionSource = new TaskCompletionSource(); + taskCompletionSource.SetResult(response); + var task = taskCompletionSource.Task; + + _apiClientMock.Setup(x => x.GetAsync("firewall/alias/searchItem")) + .Returns(task); + + // Create a PowerShell pipeline to capture output + var results = new List(); + _cmdlet.CommandRuntime = new MockCommandRuntime(output => results.Add(output)); + + // Act + _cmdlet.ProcessRecord(); + + // Assert + Assert.AreEqual(2, results.Count); + Assert.IsInstanceOfType(results[0], typeof(Alias)); + Assert.IsInstanceOfType(results[1], typeof(Alias)); + + var alias1 = results[0] as Alias; + var alias2 = results[1] as Alias; + + Assert.AreEqual("WebServers", alias1.Name); + Assert.AreEqual("WebPorts", alias2.Name); + } + + [TestMethod] + public void ProcessRecord_GetAliasByUuid_WritesAliasDetailToPipeline() + { + // Arrange + string uuid = "uuid1"; + _cmdlet.Uuid = uuid; + + // Use reflection to set the parameter set name + typeof(PSCmdlet).GetProperty("ParameterSetName").SetValue(_cmdlet, "ByUuid"); + + var aliasDetail = new AliasDetail + { + Name = "WebServers", + Type = "host", + Content = "192.168.1.10,192.168.1.11", + Description = "Web Servers", + Protocol = "", + UpdateFrequency = "", + Counters = "0", + Enabled = "1" + }; + + var response = new AliasResponse + { + Alias = aliasDetail + }; + + var aliasServiceMock = new Mock(_apiClientMock.Object, _loggerMock.Object); + aliasServiceMock.Setup(x => x.GetAliasAsync(uuid)).ReturnsAsync(response); + + // Mock the AliasService creation + var taskCompletionSource = new TaskCompletionSource(); + taskCompletionSource.SetResult(response); + var task = taskCompletionSource.Task; + + _apiClientMock.Setup(x => x.GetAsync($"firewall/alias/getItem/{uuid}")) + .Returns(task); + + // Create a PowerShell pipeline to capture output + var results = new List(); + _cmdlet.CommandRuntime = new MockCommandRuntime(output => results.Add(output)); + + // Act + _cmdlet.ProcessRecord(); + + // Assert + Assert.AreEqual(1, results.Count); + Assert.IsInstanceOfType(results[0], typeof(AliasDetail)); + + var alias = results[0] as AliasDetail; + + Assert.AreEqual("WebServers", alias.Name); + Assert.AreEqual("host", alias.Type); + Assert.AreEqual("192.168.1.10,192.168.1.11", alias.Content); + } + + [TestMethod] + public void ProcessRecord_GetAliasByName_WritesFilteredAliasesToPipeline() + { + // Arrange + string name = "WebServers"; + _cmdlet.Name = name; + + // Use reflection to set the parameter set name + typeof(PSCmdlet).GetProperty("ParameterSetName").SetValue(_cmdlet, "ByName"); + + var aliases = new List + { + new Alias + { + Uuid = "uuid1", + Name = "WebServers", + Type = "host", + Content = "192.168.1.10,192.168.1.11", + Description = "Web Servers", + Enabled = "1" + }, + new Alias + { + Uuid = "uuid2", + Name = "WebPorts", + Type = "port", + Content = "80,443", + Description = "Web Ports", + Enabled = "1" + } + }; + + var response = new AliasListResponse + { + Total = aliases.Count, + RowCount = aliases.Count, + Current = 1, + Rows = aliases + }; + + var aliasServiceMock = new Mock(_apiClientMock.Object, _loggerMock.Object); + aliasServiceMock.Setup(x => x.GetAliasesAsync()).ReturnsAsync(response); + + // Mock the AliasService creation + var taskCompletionSource = new TaskCompletionSource(); + taskCompletionSource.SetResult(response); + var task = taskCompletionSource.Task; + + _apiClientMock.Setup(x => x.GetAsync("firewall/alias/searchItem")) + .Returns(task); + + // Create a PowerShell pipeline to capture output + var results = new List(); + _cmdlet.CommandRuntime = new MockCommandRuntime(output => results.Add(output)); + + // Act + _cmdlet.ProcessRecord(); + + // Assert + Assert.AreEqual(1, results.Count); + Assert.IsInstanceOfType(results[0], typeof(Alias)); + + var alias = results[0] as Alias; + + Assert.AreEqual("WebServers", alias.Name); + Assert.AreEqual("host", alias.Type); + } + + [TestMethod] + public void ProcessRecord_GetAliasByType_WritesFilteredAliasesToPipeline() + { + // Arrange + string type = "host"; + _cmdlet.Type = type; + + var aliases = new List + { + new Alias + { + Uuid = "uuid1", + Name = "WebServers", + Type = "host", + Content = "192.168.1.10,192.168.1.11", + Description = "Web Servers", + Enabled = "1" + }, + new Alias + { + Uuid = "uuid2", + Name = "WebPorts", + Type = "port", + Content = "80,443", + Description = "Web Ports", + Enabled = "1" + } + }; + + var response = new AliasListResponse + { + Total = aliases.Count, + RowCount = aliases.Count, + Current = 1, + Rows = aliases + }; + + var aliasServiceMock = new Mock(_apiClientMock.Object, _loggerMock.Object); + aliasServiceMock.Setup(x => x.GetAliasesAsync()).ReturnsAsync(response); + + // Mock the AliasService creation + var taskCompletionSource = new TaskCompletionSource(); + taskCompletionSource.SetResult(response); + var task = taskCompletionSource.Task; + + _apiClientMock.Setup(x => x.GetAsync("firewall/alias/searchItem")) + .Returns(task); + + // Create a PowerShell pipeline to capture output + var results = new List(); + _cmdlet.CommandRuntime = new MockCommandRuntime(output => results.Add(output)); + + // Act + _cmdlet.ProcessRecord(); + + // Assert + Assert.AreEqual(1, results.Count); + Assert.IsInstanceOfType(results[0], typeof(Alias)); + + var alias = results[0] as Alias; + + Assert.AreEqual("WebServers", alias.Name); + Assert.AreEqual("host", alias.Type); + } + } + + // Helper class for testing cmdlets + public class MockCommandRuntime : ICommandRuntime + { + private readonly Action _outputHandler; + private readonly Action _warningHandler; + private readonly Action _verboseHandler; + private readonly bool _shouldProcessResult; + + public MockCommandRuntime(Action outputHandler, Action warningHandler = null, Action verboseHandler = null, bool shouldProcessResult = true) + { + _outputHandler = outputHandler; + _warningHandler = warningHandler ?? (_ => { }); + _verboseHandler = verboseHandler ?? (_ => { }); + _shouldProcessResult = shouldProcessResult; + } + + public PSHost Host => throw new NotImplementedException(); + + public PSTransactionContext CurrentPSTransaction => throw new NotImplementedException(); + + public virtual bool ShouldProcess(string target) => _shouldProcessResult; + + public virtual bool ShouldProcess(string target, string action) => _shouldProcessResult; + + public virtual bool ShouldProcess(string verboseDescription, string verboseWarning, string caption) => _shouldProcessResult; + + public virtual bool ShouldProcess(string verboseDescription, string verboseWarning, string caption, out ShouldProcessReason shouldProcessReason) + { + shouldProcessReason = ShouldProcessReason.None; + return _shouldProcessResult; + } + + public bool ShouldContinue(string query, string caption) => true; + + public bool ShouldContinue(string query, string caption, ref bool yesToAll, ref bool noToAll) => true; + + public void WriteCommandDetail(string text) { } + + public void WriteDebug(string text) { } + + public void WriteError(ErrorRecord errorRecord) { throw errorRecord.Exception; } + + public void WriteObject(object sendToPipeline) => _outputHandler(sendToPipeline); + + public void WriteObject(object sendToPipeline, bool enumerateCollection) + { + if (enumerateCollection && sendToPipeline is IEnumerable enumerable) + { + foreach (var item in enumerable) + { + _outputHandler(item); + } + } + else + { + _outputHandler(sendToPipeline); + } + } + + public void WriteProgress(ProgressRecord progressRecord) { } + + public void WriteProgress(long sourceId, ProgressRecord progressRecord) { } + + public void WriteVerbose(string text) => _verboseHandler(text); + + public void WriteWarning(string text) => _warningHandler(text); + } +} diff --git a/src/PSOPNSenseAPI.Tests/Cmdlets/NewOPNSenseAliasCmdletTests.cs b/src/PSOPNSenseAPI.Tests/Cmdlets/NewOPNSenseAliasCmdletTests.cs new file mode 100644 index 0000000..66512c9 --- /dev/null +++ b/src/PSOPNSenseAPI.Tests/Cmdlets/NewOPNSenseAliasCmdletTests.cs @@ -0,0 +1,285 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using PSOPNSenseAPI.Cmdlets; +using PSOPNSenseAPI.Logging; +using PSOPNSenseAPI.Services; +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Threading.Tasks; + +namespace PSOPNSenseAPI.Tests.Cmdlets +{ + [TestClass] + public class NewOPNSenseAliasCmdletTests + { + private Mock _apiClientMock; + private Mock _loggerMock; + private NewOPNSenseAliasCmdlet _cmdlet; + + [TestInitialize] + public void Initialize() + { + _apiClientMock = new Mock("https://example.com", "key", "secret", false, Mock.Of()); + _loggerMock = new Mock(); + + _cmdlet = new NewOPNSenseAliasCmdlet(); + + // Use reflection to set private fields + typeof(OPNSenseBaseCmdlet).GetProperty("ApiClient").SetValue(_cmdlet, _apiClientMock.Object); + typeof(OPNSenseBaseCmdlet).GetProperty("Logger").SetValue(_cmdlet, _loggerMock.Object); + } + + [TestMethod] + public void ProcessRecord_CreateHostAlias_ReturnsUuid() + { + // Arrange + _cmdlet.Name = "WebServers"; + _cmdlet.Type = "host"; + _cmdlet.Content = "192.168.1.10,192.168.1.11"; + _cmdlet.Description = "Web Servers"; + + var expectedResponse = new AliasCreateResponse + { + Uuid = "new-uuid" + }; + + // Mock the API client response + var taskCompletionSource = new TaskCompletionSource(); + taskCompletionSource.SetResult(expectedResponse); + var task = taskCompletionSource.Task; + + _apiClientMock.Setup(x => x.PostAsync("firewall/alias/addItem", It.IsAny())) + .Returns(task); + + // Create a PowerShell pipeline to capture output + var results = new List(); + _cmdlet.CommandRuntime = new MockCommandRuntime(output => results.Add(output)); + + // Act + _cmdlet.ProcessRecord(); + + // Assert + Assert.AreEqual(1, results.Count); + Assert.AreEqual("new-uuid", results[0]); + } + + [TestMethod] + public void ProcessRecord_CreatePortAlias_WithProtocol_ReturnsUuid() + { + // Arrange + _cmdlet.Name = "WebPorts"; + _cmdlet.Type = "port"; + _cmdlet.Content = "80,443"; + _cmdlet.Description = "Web Ports"; + _cmdlet.Protocol = "TCP"; + + var expectedResponse = new AliasCreateResponse + { + Uuid = "new-uuid" + }; + + // Mock the API client response + var taskCompletionSource = new TaskCompletionSource(); + taskCompletionSource.SetResult(expectedResponse); + var task = taskCompletionSource.Task; + + _apiClientMock.Setup(x => x.PostAsync("firewall/alias/addItem", It.IsAny())) + .Returns(task); + + // Create a PowerShell pipeline to capture output + var results = new List(); + _cmdlet.CommandRuntime = new MockCommandRuntime(output => results.Add(output)); + + // Act + _cmdlet.ProcessRecord(); + + // Assert + Assert.AreEqual(1, results.Count); + Assert.AreEqual("new-uuid", results[0]); + + // Verify the correct protocol was set + _apiClientMock.Verify(x => x.PostAsync("firewall/alias/addItem", + It.Is(o => + o.GetType().GetProperty("alias").GetValue(o, null).GetType().GetProperty("Protocol").GetValue( + o.GetType().GetProperty("alias").GetValue(o, null), null).ToString() == "TCP")), + Times.Once); + } + + [TestMethod] + public void ProcessRecord_CreateUrlAlias_WithUpdateFrequency_ReturnsUuid() + { + // Arrange + _cmdlet.Name = "BlockList"; + _cmdlet.Type = "url"; + _cmdlet.Content = "https://example.com/blocklist.txt"; + _cmdlet.Description = "Block List"; + _cmdlet.UpdateFrequency = 1; + + var expectedResponse = new AliasCreateResponse + { + Uuid = "new-uuid" + }; + + // Mock the API client response + var taskCompletionSource = new TaskCompletionSource(); + taskCompletionSource.SetResult(expectedResponse); + var task = taskCompletionSource.Task; + + _apiClientMock.Setup(x => x.PostAsync("firewall/alias/addItem", It.IsAny())) + .Returns(task); + + // Create a PowerShell pipeline to capture output + var results = new List(); + _cmdlet.CommandRuntime = new MockCommandRuntime(output => results.Add(output)); + + // Act + _cmdlet.ProcessRecord(); + + // Assert + Assert.AreEqual(1, results.Count); + Assert.AreEqual("new-uuid", results[0]); + + // Verify the correct update frequency was set + _apiClientMock.Verify(x => x.PostAsync("firewall/alias/addItem", + It.Is(o => + o.GetType().GetProperty("alias").GetValue(o, null).GetType().GetProperty("UpdateFrequency").GetValue( + o.GetType().GetProperty("alias").GetValue(o, null), null).ToString() == "1")), + Times.Once); + } + + [TestMethod] + public void ProcessRecord_CreateDisabledAlias_ReturnsUuid() + { + // Arrange + _cmdlet.Name = "WebServers"; + _cmdlet.Type = "host"; + _cmdlet.Content = "192.168.1.10,192.168.1.11"; + _cmdlet.Description = "Web Servers"; + _cmdlet.Disabled = true; + + var expectedResponse = new AliasCreateResponse + { + Uuid = "new-uuid" + }; + + // Mock the API client response + var taskCompletionSource = new TaskCompletionSource(); + taskCompletionSource.SetResult(expectedResponse); + var task = taskCompletionSource.Task; + + _apiClientMock.Setup(x => x.PostAsync("firewall/alias/addItem", It.IsAny())) + .Returns(task); + + // Create a PowerShell pipeline to capture output + var results = new List(); + _cmdlet.CommandRuntime = new MockCommandRuntime(output => results.Add(output)); + + // Act + _cmdlet.ProcessRecord(); + + // Assert + Assert.AreEqual(1, results.Count); + Assert.AreEqual("new-uuid", results[0]); + + // Verify the alias was created as disabled + _apiClientMock.Verify(x => x.PostAsync("firewall/alias/addItem", + It.Is(o => + o.GetType().GetProperty("alias").GetValue(o, null).GetType().GetProperty("Enabled").GetValue( + o.GetType().GetProperty("alias").GetValue(o, null), null).ToString() == "0")), + Times.Once); + } + + [TestMethod] + public void ProcessRecord_CreateAliasWithCounters_ReturnsUuid() + { + // Arrange + _cmdlet.Name = "WebServers"; + _cmdlet.Type = "host"; + _cmdlet.Content = "192.168.1.10,192.168.1.11"; + _cmdlet.Description = "Web Servers"; + _cmdlet.EnableCounters = true; + + var expectedResponse = new AliasCreateResponse + { + Uuid = "new-uuid" + }; + + // Mock the API client response + var taskCompletionSource = new TaskCompletionSource(); + taskCompletionSource.SetResult(expectedResponse); + var task = taskCompletionSource.Task; + + _apiClientMock.Setup(x => x.PostAsync("firewall/alias/addItem", It.IsAny())) + .Returns(task); + + // Create a PowerShell pipeline to capture output + var results = new List(); + _cmdlet.CommandRuntime = new MockCommandRuntime(output => results.Add(output)); + + // Act + _cmdlet.ProcessRecord(); + + // Assert + Assert.AreEqual(1, results.Count); + Assert.AreEqual("new-uuid", results[0]); + + // Verify the alias was created with counters enabled + _apiClientMock.Verify(x => x.PostAsync("firewall/alias/addItem", + It.Is(o => + o.GetType().GetProperty("alias").GetValue(o, null).GetType().GetProperty("Counters").GetValue( + o.GetType().GetProperty("alias").GetValue(o, null), null).ToString() == "1")), + Times.Once); + } + + [TestMethod] + public void ProcessRecord_CreateAliasAndApply_ReconfiguresAliases() + { + // Arrange + _cmdlet.Name = "WebServers"; + _cmdlet.Type = "host"; + _cmdlet.Content = "192.168.1.10,192.168.1.11"; + _cmdlet.Description = "Web Servers"; + _cmdlet.Apply = true; + + var createResponse = new AliasCreateResponse + { + Uuid = "new-uuid" + }; + + var reconfigureResponse = new AliasReconfigureResponse + { + Status = "ok" + }; + + // Mock the API client responses + var createTaskCompletionSource = new TaskCompletionSource(); + createTaskCompletionSource.SetResult(createResponse); + var createTask = createTaskCompletionSource.Task; + + var reconfigureTaskCompletionSource = new TaskCompletionSource(); + reconfigureTaskCompletionSource.SetResult(reconfigureResponse); + var reconfigureTask = reconfigureTaskCompletionSource.Task; + + _apiClientMock.Setup(x => x.PostAsync("firewall/alias/addItem", It.IsAny())) + .Returns(createTask); + + _apiClientMock.Setup(x => x.PostAsync("firewall/alias/reconfigure")) + .Returns(reconfigureTask); + + // Create a PowerShell pipeline to capture output + var results = new List(); + _cmdlet.CommandRuntime = new MockCommandRuntime(output => results.Add(output)); + + // Act + _cmdlet.ProcessRecord(); + + // Assert + Assert.AreEqual(1, results.Count); + Assert.AreEqual("new-uuid", results[0]); + + // Verify the reconfigure method was called + _apiClientMock.Verify(x => x.PostAsync("firewall/alias/reconfigure"), Times.Once); + } + } +} diff --git a/src/PSOPNSenseAPI.Tests/Cmdlets/RemoveOPNSenseAliasCmdletTests.cs b/src/PSOPNSenseAPI.Tests/Cmdlets/RemoveOPNSenseAliasCmdletTests.cs new file mode 100644 index 0000000..03b96d8 --- /dev/null +++ b/src/PSOPNSenseAPI.Tests/Cmdlets/RemoveOPNSenseAliasCmdletTests.cs @@ -0,0 +1,213 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using PSOPNSenseAPI.Cmdlets; +using PSOPNSenseAPI.Logging; +using PSOPNSenseAPI.Services; +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Threading.Tasks; + +namespace PSOPNSenseAPI.Tests.Cmdlets +{ + [TestClass] + public class RemoveOPNSenseAliasCmdletTests + { + private Mock _apiClientMock; + private Mock _loggerMock; + private RemoveOPNSenseAliasCmdlet _cmdlet; + + [TestInitialize] + public void Initialize() + { + _apiClientMock = new Mock("https://example.com", "key", "secret", false, Mock.Of()); + _loggerMock = new Mock(); + + _cmdlet = new RemoveOPNSenseAliasCmdlet(); + + // Use reflection to set private fields + typeof(OPNSenseBaseCmdlet).GetProperty("ApiClient").SetValue(_cmdlet, _apiClientMock.Object); + typeof(OPNSenseBaseCmdlet).GetProperty("Logger").SetValue(_cmdlet, _loggerMock.Object); + } + + [TestMethod] + public void ProcessRecord_RemoveAliasWithForce_Success() + { + // Arrange + string uuid = "test-uuid"; + _cmdlet.Uuid = uuid; + _cmdlet.Force = true; + + var getResponse = new AliasResponse + { + Alias = new AliasDetail + { + Name = "WebServers", + Type = "host", + Content = "192.168.1.10,192.168.1.11", + Description = "Web Servers", + Protocol = "", + UpdateFrequency = "", + Counters = "0", + Enabled = "1" + } + }; + + var deleteResponse = new AliasDeleteResponse + { + Result = "deleted" + }; + + // Mock the API client responses + var getTaskCompletionSource = new TaskCompletionSource(); + getTaskCompletionSource.SetResult(getResponse); + var getTask = getTaskCompletionSource.Task; + + var deleteTaskCompletionSource = new TaskCompletionSource(); + deleteTaskCompletionSource.SetResult(deleteResponse); + var deleteTask = deleteTaskCompletionSource.Task; + + _apiClientMock.Setup(x => x.GetAsync($"firewall/alias/getItem/{uuid}")) + .Returns(getTask); + + _apiClientMock.Setup(x => x.PostAsync($"firewall/alias/delItem/{uuid}")) + .Returns(deleteTask); + + // Create a PowerShell pipeline to capture output + var results = new List(); + _cmdlet.CommandRuntime = new MockCommandRuntime(output => results.Add(output)); + + // Act + _cmdlet.ProcessRecord(); + + // Assert + // Verify the delete method was called + _apiClientMock.Verify(x => x.PostAsync($"firewall/alias/delItem/{uuid}"), Times.Once); + } + + [TestMethod] + public void ProcessRecord_RemoveAliasWithShouldProcess_Success() + { + // Arrange + string uuid = "test-uuid"; + _cmdlet.Uuid = uuid; + + var getResponse = new AliasResponse + { + Alias = new AliasDetail + { + Name = "WebServers", + Type = "host", + Content = "192.168.1.10,192.168.1.11", + Description = "Web Servers", + Protocol = "", + UpdateFrequency = "", + Counters = "0", + Enabled = "1" + } + }; + + var deleteResponse = new AliasDeleteResponse + { + Result = "deleted" + }; + + // Mock the API client responses + var getTaskCompletionSource = new TaskCompletionSource(); + getTaskCompletionSource.SetResult(getResponse); + var getTask = getTaskCompletionSource.Task; + + var deleteTaskCompletionSource = new TaskCompletionSource(); + deleteTaskCompletionSource.SetResult(deleteResponse); + var deleteTask = deleteTaskCompletionSource.Task; + + _apiClientMock.Setup(x => x.GetAsync($"firewall/alias/getItem/{uuid}")) + .Returns(getTask); + + _apiClientMock.Setup(x => x.PostAsync($"firewall/alias/delItem/{uuid}")) + .Returns(deleteTask); + + // Create a PowerShell pipeline to capture output with ShouldProcess always returning true + var results = new List(); + _cmdlet.CommandRuntime = new MockCommandRuntime(output => results.Add(output)); + + // Act + _cmdlet.ProcessRecord(); + + // Assert + // Verify the delete method was called + _apiClientMock.Verify(x => x.PostAsync($"firewall/alias/delItem/{uuid}"), Times.Once); + } + + [TestMethod] + public void ProcessRecord_RemoveAliasAndApply_ReconfiguresAliases() + { + // Arrange + string uuid = "test-uuid"; + _cmdlet.Uuid = uuid; + _cmdlet.Force = true; + _cmdlet.Apply = true; + + var getResponse = new AliasResponse + { + Alias = new AliasDetail + { + Name = "WebServers", + Type = "host", + Content = "192.168.1.10,192.168.1.11", + Description = "Web Servers", + Protocol = "", + UpdateFrequency = "", + Counters = "0", + Enabled = "1" + } + }; + + var deleteResponse = new AliasDeleteResponse + { + Result = "deleted" + }; + + var reconfigureResponse = new AliasReconfigureResponse + { + Status = "ok" + }; + + // Mock the API client responses + var getTaskCompletionSource = new TaskCompletionSource(); + getTaskCompletionSource.SetResult(getResponse); + var getTask = getTaskCompletionSource.Task; + + var deleteTaskCompletionSource = new TaskCompletionSource(); + deleteTaskCompletionSource.SetResult(deleteResponse); + var deleteTask = deleteTaskCompletionSource.Task; + + var reconfigureTaskCompletionSource = new TaskCompletionSource(); + reconfigureTaskCompletionSource.SetResult(reconfigureResponse); + var reconfigureTask = reconfigureTaskCompletionSource.Task; + + _apiClientMock.Setup(x => x.GetAsync($"firewall/alias/getItem/{uuid}")) + .Returns(getTask); + + _apiClientMock.Setup(x => x.PostAsync($"firewall/alias/delItem/{uuid}")) + .Returns(deleteTask); + + _apiClientMock.Setup(x => x.PostAsync("firewall/alias/reconfigure")) + .Returns(reconfigureTask); + + // Create a PowerShell pipeline to capture output + var results = new List(); + _cmdlet.CommandRuntime = new MockCommandRuntime(output => results.Add(output)); + + // Act + _cmdlet.ProcessRecord(); + + // Assert + // Verify the delete and reconfigure methods were called + _apiClientMock.Verify(x => x.PostAsync($"firewall/alias/delItem/{uuid}"), Times.Once); + _apiClientMock.Verify(x => x.PostAsync("firewall/alias/reconfigure"), Times.Once); + } + } + + +} diff --git a/src/PSOPNSenseAPI.Tests/Cmdlets/SetOPNSenseAliasCmdletTests.cs b/src/PSOPNSenseAPI.Tests/Cmdlets/SetOPNSenseAliasCmdletTests.cs new file mode 100644 index 0000000..2c06ad9 --- /dev/null +++ b/src/PSOPNSenseAPI.Tests/Cmdlets/SetOPNSenseAliasCmdletTests.cs @@ -0,0 +1,456 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using PSOPNSenseAPI.Cmdlets; +using PSOPNSenseAPI.Logging; +using PSOPNSenseAPI.Services; +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Threading.Tasks; + +namespace PSOPNSenseAPI.Tests.Cmdlets +{ + [TestClass] + public class SetOPNSenseAliasCmdletTests + { + private Mock _apiClientMock; + private Mock _loggerMock; + private SetOPNSenseAliasCmdlet _cmdlet; + + [TestInitialize] + public void Initialize() + { + _apiClientMock = new Mock("https://example.com", "key", "secret", false, Mock.Of()); + _loggerMock = new Mock(); + + _cmdlet = new SetOPNSenseAliasCmdlet(); + + // Use reflection to set private fields + typeof(OPNSenseBaseCmdlet).GetProperty("ApiClient").SetValue(_cmdlet, _apiClientMock.Object); + typeof(OPNSenseBaseCmdlet).GetProperty("Logger").SetValue(_cmdlet, _loggerMock.Object); + } + + [TestMethod] + public void ProcessRecord_UpdateAliasContent_Success() + { + // Arrange + string uuid = "test-uuid"; + _cmdlet.Uuid = uuid; + _cmdlet.Content = "192.168.1.10,192.168.1.11,192.168.1.12"; + + var getResponse = new AliasResponse + { + Alias = new AliasDetail + { + Name = "WebServers", + Type = "host", + Content = "192.168.1.10,192.168.1.11", + Description = "Web Servers", + Protocol = "", + UpdateFrequency = "", + Counters = "0", + Enabled = "1" + } + }; + + var updateResponse = new AliasUpdateResponse + { + Result = "saved" + }; + + // Mock the API client responses + var getTaskCompletionSource = new TaskCompletionSource(); + getTaskCompletionSource.SetResult(getResponse); + var getTask = getTaskCompletionSource.Task; + + var updateTaskCompletionSource = new TaskCompletionSource(); + updateTaskCompletionSource.SetResult(updateResponse); + var updateTask = updateTaskCompletionSource.Task; + + _apiClientMock.Setup(x => x.GetAsync($"firewall/alias/getItem/{uuid}")) + .Returns(getTask); + + _apiClientMock.Setup(x => x.PostAsync($"firewall/alias/setItem/{uuid}", It.IsAny())) + .Returns(updateTask); + + // Create a PowerShell pipeline to capture output + var results = new List(); + _cmdlet.CommandRuntime = new MockCommandRuntime(output => results.Add(output)); + + // Act + _cmdlet.ProcessRecord(); + + // Assert + // Verify the update method was called with the correct content + _apiClientMock.Verify(x => x.PostAsync($"firewall/alias/setItem/{uuid}", + It.Is(o => + o.GetType().GetProperty("alias").GetValue(o, null).GetType().GetProperty("Content").GetValue( + o.GetType().GetProperty("alias").GetValue(o, null), null).ToString() == "192.168.1.10,192.168.1.11,192.168.1.12")), + Times.Once); + } + + [TestMethod] + public void ProcessRecord_UpdateAliasDescription_Success() + { + // Arrange + string uuid = "test-uuid"; + _cmdlet.Uuid = uuid; + _cmdlet.Description = "Updated Web Servers"; + + var getResponse = new AliasResponse + { + Alias = new AliasDetail + { + Name = "WebServers", + Type = "host", + Content = "192.168.1.10,192.168.1.11", + Description = "Web Servers", + Protocol = "", + UpdateFrequency = "", + Counters = "0", + Enabled = "1" + } + }; + + var updateResponse = new AliasUpdateResponse + { + Result = "saved" + }; + + // Mock the API client responses + var getTaskCompletionSource = new TaskCompletionSource(); + getTaskCompletionSource.SetResult(getResponse); + var getTask = getTaskCompletionSource.Task; + + var updateTaskCompletionSource = new TaskCompletionSource(); + updateTaskCompletionSource.SetResult(updateResponse); + var updateTask = updateTaskCompletionSource.Task; + + _apiClientMock.Setup(x => x.GetAsync($"firewall/alias/getItem/{uuid}")) + .Returns(getTask); + + _apiClientMock.Setup(x => x.PostAsync($"firewall/alias/setItem/{uuid}", It.IsAny())) + .Returns(updateTask); + + // Create a PowerShell pipeline to capture output + var results = new List(); + _cmdlet.CommandRuntime = new MockCommandRuntime(output => results.Add(output)); + + // Act + _cmdlet.ProcessRecord(); + + // Assert + // Verify the update method was called with the correct description + _apiClientMock.Verify(x => x.PostAsync($"firewall/alias/setItem/{uuid}", + It.Is(o => + o.GetType().GetProperty("alias").GetValue(o, null).GetType().GetProperty("Description").GetValue( + o.GetType().GetProperty("alias").GetValue(o, null), null).ToString() == "Updated Web Servers")), + Times.Once); + } + + [TestMethod] + public void ProcessRecord_DisableAlias_Success() + { + // Arrange + string uuid = "test-uuid"; + _cmdlet.Uuid = uuid; + _cmdlet.Disabled = true; + + var getResponse = new AliasResponse + { + Alias = new AliasDetail + { + Name = "WebServers", + Type = "host", + Content = "192.168.1.10,192.168.1.11", + Description = "Web Servers", + Protocol = "", + UpdateFrequency = "", + Counters = "0", + Enabled = "1" + } + }; + + var updateResponse = new AliasUpdateResponse + { + Result = "saved" + }; + + // Mock the API client responses + var getTaskCompletionSource = new TaskCompletionSource(); + getTaskCompletionSource.SetResult(getResponse); + var getTask = getTaskCompletionSource.Task; + + var updateTaskCompletionSource = new TaskCompletionSource(); + updateTaskCompletionSource.SetResult(updateResponse); + var updateTask = updateTaskCompletionSource.Task; + + _apiClientMock.Setup(x => x.GetAsync($"firewall/alias/getItem/{uuid}")) + .Returns(getTask); + + _apiClientMock.Setup(x => x.PostAsync($"firewall/alias/setItem/{uuid}", It.IsAny())) + .Returns(updateTask); + + // Create a PowerShell pipeline to capture output + var results = new List(); + _cmdlet.CommandRuntime = new MockCommandRuntime(output => results.Add(output)); + + // Act + _cmdlet.ProcessRecord(); + + // Assert + // Verify the update method was called with the alias disabled + _apiClientMock.Verify(x => x.PostAsync($"firewall/alias/setItem/{uuid}", + It.Is(o => + o.GetType().GetProperty("alias").GetValue(o, null).GetType().GetProperty("Enabled").GetValue( + o.GetType().GetProperty("alias").GetValue(o, null), null).ToString() == "0")), + Times.Once); + } + + [TestMethod] + public void ProcessRecord_EnableAlias_Success() + { + // Arrange + string uuid = "test-uuid"; + _cmdlet.Uuid = uuid; + _cmdlet.Enabled = true; + + var getResponse = new AliasResponse + { + Alias = new AliasDetail + { + Name = "WebServers", + Type = "host", + Content = "192.168.1.10,192.168.1.11", + Description = "Web Servers", + Protocol = "", + UpdateFrequency = "", + Counters = "0", + Enabled = "0" + } + }; + + var updateResponse = new AliasUpdateResponse + { + Result = "saved" + }; + + // Mock the API client responses + var getTaskCompletionSource = new TaskCompletionSource(); + getTaskCompletionSource.SetResult(getResponse); + var getTask = getTaskCompletionSource.Task; + + var updateTaskCompletionSource = new TaskCompletionSource(); + updateTaskCompletionSource.SetResult(updateResponse); + var updateTask = updateTaskCompletionSource.Task; + + _apiClientMock.Setup(x => x.GetAsync($"firewall/alias/getItem/{uuid}")) + .Returns(getTask); + + _apiClientMock.Setup(x => x.PostAsync($"firewall/alias/setItem/{uuid}", It.IsAny())) + .Returns(updateTask); + + // Create a PowerShell pipeline to capture output + var results = new List(); + _cmdlet.CommandRuntime = new MockCommandRuntime(output => results.Add(output)); + + // Act + _cmdlet.ProcessRecord(); + + // Assert + // Verify the update method was called with the alias enabled + _apiClientMock.Verify(x => x.PostAsync($"firewall/alias/setItem/{uuid}", + It.Is(o => + o.GetType().GetProperty("alias").GetValue(o, null).GetType().GetProperty("Enabled").GetValue( + o.GetType().GetProperty("alias").GetValue(o, null), null).ToString() == "1")), + Times.Once); + } + + [TestMethod] + public void ProcessRecord_EnableCounters_Success() + { + // Arrange + string uuid = "test-uuid"; + _cmdlet.Uuid = uuid; + _cmdlet.EnableCounters = true; + + var getResponse = new AliasResponse + { + Alias = new AliasDetail + { + Name = "WebServers", + Type = "host", + Content = "192.168.1.10,192.168.1.11", + Description = "Web Servers", + Protocol = "", + UpdateFrequency = "", + Counters = "0", + Enabled = "1" + } + }; + + var updateResponse = new AliasUpdateResponse + { + Result = "saved" + }; + + // Mock the API client responses + var getTaskCompletionSource = new TaskCompletionSource(); + getTaskCompletionSource.SetResult(getResponse); + var getTask = getTaskCompletionSource.Task; + + var updateTaskCompletionSource = new TaskCompletionSource(); + updateTaskCompletionSource.SetResult(updateResponse); + var updateTask = updateTaskCompletionSource.Task; + + _apiClientMock.Setup(x => x.GetAsync($"firewall/alias/getItem/{uuid}")) + .Returns(getTask); + + _apiClientMock.Setup(x => x.PostAsync($"firewall/alias/setItem/{uuid}", It.IsAny())) + .Returns(updateTask); + + // Create a PowerShell pipeline to capture output + var results = new List(); + _cmdlet.CommandRuntime = new MockCommandRuntime(output => results.Add(output)); + + // Act + _cmdlet.ProcessRecord(); + + // Assert + // Verify the update method was called with counters enabled + _apiClientMock.Verify(x => x.PostAsync($"firewall/alias/setItem/{uuid}", + It.Is(o => + o.GetType().GetProperty("alias").GetValue(o, null).GetType().GetProperty("Counters").GetValue( + o.GetType().GetProperty("alias").GetValue(o, null), null).ToString() == "1")), + Times.Once); + } + + [TestMethod] + public void ProcessRecord_UpdateAndApply_ReconfiguresAliases() + { + // Arrange + string uuid = "test-uuid"; + _cmdlet.Uuid = uuid; + _cmdlet.Content = "192.168.1.10,192.168.1.11,192.168.1.12"; + _cmdlet.Apply = true; + + var getResponse = new AliasResponse + { + Alias = new AliasDetail + { + Name = "WebServers", + Type = "host", + Content = "192.168.1.10,192.168.1.11", + Description = "Web Servers", + Protocol = "", + UpdateFrequency = "", + Counters = "0", + Enabled = "1" + } + }; + + var updateResponse = new AliasUpdateResponse + { + Result = "saved" + }; + + var reconfigureResponse = new AliasReconfigureResponse + { + Status = "ok" + }; + + // Mock the API client responses + var getTaskCompletionSource = new TaskCompletionSource(); + getTaskCompletionSource.SetResult(getResponse); + var getTask = getTaskCompletionSource.Task; + + var updateTaskCompletionSource = new TaskCompletionSource(); + updateTaskCompletionSource.SetResult(updateResponse); + var updateTask = updateTaskCompletionSource.Task; + + var reconfigureTaskCompletionSource = new TaskCompletionSource(); + reconfigureTaskCompletionSource.SetResult(reconfigureResponse); + var reconfigureTask = reconfigureTaskCompletionSource.Task; + + _apiClientMock.Setup(x => x.GetAsync($"firewall/alias/getItem/{uuid}")) + .Returns(getTask); + + _apiClientMock.Setup(x => x.PostAsync($"firewall/alias/setItem/{uuid}", It.IsAny())) + .Returns(updateTask); + + _apiClientMock.Setup(x => x.PostAsync("firewall/alias/reconfigure")) + .Returns(reconfigureTask); + + // Create a PowerShell pipeline to capture output + var results = new List(); + _cmdlet.CommandRuntime = new MockCommandRuntime(output => results.Add(output)); + + // Act + _cmdlet.ProcessRecord(); + + // Assert + // Verify the reconfigure method was called + _apiClientMock.Verify(x => x.PostAsync("firewall/alias/reconfigure"), Times.Once); + } + + [TestMethod] + public void ProcessRecord_BothEnabledAndDisabled_UsesEnabled() + { + // Arrange + string uuid = "test-uuid"; + _cmdlet.Uuid = uuid; + _cmdlet.Enabled = true; + _cmdlet.Disabled = true; + + var getResponse = new AliasResponse + { + Alias = new AliasDetail + { + Name = "WebServers", + Type = "host", + Content = "192.168.1.10,192.168.1.11", + Description = "Web Servers", + Protocol = "", + UpdateFrequency = "", + Counters = "0", + Enabled = "0" + } + }; + + var updateResponse = new AliasUpdateResponse + { + Result = "saved" + }; + + // Mock the API client responses + var getTaskCompletionSource = new TaskCompletionSource(); + getTaskCompletionSource.SetResult(getResponse); + var getTask = getTaskCompletionSource.Task; + + var updateTaskCompletionSource = new TaskCompletionSource(); + updateTaskCompletionSource.SetResult(updateResponse); + var updateTask = updateTaskCompletionSource.Task; + + _apiClientMock.Setup(x => x.GetAsync($"firewall/alias/getItem/{uuid}")) + .Returns(getTask); + + _apiClientMock.Setup(x => x.PostAsync($"firewall/alias/setItem/{uuid}", It.IsAny())) + .Returns(updateTask); + + // Create a PowerShell pipeline to capture output + var results = new List(); + _cmdlet.CommandRuntime = new MockCommandRuntime(output => results.Add(output), warning => { }); + + // Act + _cmdlet.ProcessRecord(); + + // Assert + // Verify the update method was called with the alias enabled + _apiClientMock.Verify(x => x.PostAsync($"firewall/alias/setItem/{uuid}", + It.Is(o => + o.GetType().GetProperty("alias").GetValue(o, null).GetType().GetProperty("Enabled").GetValue( + o.GetType().GetProperty("alias").GetValue(o, null), null).ToString() == "1")), + Times.Once); + } + } +} diff --git a/src/PSOPNSenseAPI.Tests/PSOPNSenseAPI.Tests.csproj b/src/PSOPNSenseAPI.Tests/PSOPNSenseAPI.Tests.csproj new file mode 100644 index 0000000..ca44c41 --- /dev/null +++ b/src/PSOPNSenseAPI.Tests/PSOPNSenseAPI.Tests.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp3.1;net472 + false + + + + + + + + + + + + + + + diff --git a/src/PSOPNSenseAPI.Tests/Services/AliasServiceTests.cs b/src/PSOPNSenseAPI.Tests/Services/AliasServiceTests.cs new file mode 100644 index 0000000..5d389a5 --- /dev/null +++ b/src/PSOPNSenseAPI.Tests/Services/AliasServiceTests.cs @@ -0,0 +1,211 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Newtonsoft.Json; +using PSOPNSenseAPI.Logging; +using PSOPNSenseAPI.Services; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace PSOPNSenseAPI.Tests.Services +{ + [TestClass] + public class AliasServiceTests + { + private Mock _apiClientMock; + private Mock _loggerMock; + private AliasService _aliasService; + + [TestInitialize] + public void Initialize() + { + _apiClientMock = new Mock("https://example.com", "key", "secret", false, Mock.Of()); + _loggerMock = new Mock(); + _aliasService = new AliasService(_apiClientMock.Object, _loggerMock.Object); + } + + [TestMethod] + public async Task GetAliasesAsync_ReturnsAliases() + { + // Arrange + var expectedResponse = new AliasListResponse + { + Total = 2, + RowCount = 2, + Current = 1, + Rows = new List + { + new Alias + { + Uuid = "uuid1", + Name = "WebServers", + Type = "host", + Content = "192.168.1.10,192.168.1.11", + Description = "Web Servers", + Enabled = "1" + }, + new Alias + { + Uuid = "uuid2", + Name = "WebPorts", + Type = "port", + Content = "80,443", + Description = "Web Ports", + Enabled = "1" + } + } + }; + + _apiClientMock.Setup(x => x.GetAsync("firewall/alias/searchItem")) + .ReturnsAsync(expectedResponse); + + // Act + var result = await _aliasService.GetAliasesAsync(); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(2, result.Total); + Assert.AreEqual(2, result.Rows.Count); + Assert.AreEqual("WebServers", result.Rows[0].Name); + Assert.AreEqual("WebPorts", result.Rows[1].Name); + _loggerMock.Verify(x => x.Information("Getting aliases"), Times.Once); + } + + [TestMethod] + public async Task GetAliasAsync_ReturnsAlias() + { + // Arrange + string uuid = "uuid1"; + var expectedResponse = new AliasResponse + { + Alias = new AliasDetail + { + Name = "WebServers", + Type = "host", + Content = "192.168.1.10,192.168.1.11", + Description = "Web Servers", + Protocol = "", + UpdateFrequency = "", + Counters = "0", + Enabled = "1" + } + }; + + _apiClientMock.Setup(x => x.GetAsync($"firewall/alias/getItem/{uuid}")) + .ReturnsAsync(expectedResponse); + + // Act + var result = await _aliasService.GetAliasAsync(uuid); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual("WebServers", result.Alias.Name); + Assert.AreEqual("host", result.Alias.Type); + _loggerMock.Verify(x => x.Information($"Getting alias with UUID {uuid}"), Times.Once); + } + + [TestMethod] + public async Task CreateAliasAsync_ReturnsUuid() + { + // Arrange + var alias = new AliasConfig + { + Name = "WebServers", + Type = "host", + Content = "192.168.1.10,192.168.1.11", + Description = "Web Servers", + Enabled = "1" + }; + + var expectedResponse = new AliasCreateResponse + { + Uuid = "new-uuid" + }; + + _apiClientMock.Setup(x => x.PostAsync("firewall/alias/addItem", It.IsAny())) + .ReturnsAsync(expectedResponse); + + // Act + var result = await _aliasService.CreateAliasAsync(alias); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual("new-uuid", result.Uuid); + _loggerMock.Verify(x => x.Information("Creating alias"), Times.Once); + } + + [TestMethod] + public async Task UpdateAliasAsync_ReturnsSuccess() + { + // Arrange + string uuid = "uuid1"; + var alias = new AliasConfig + { + Name = "WebServers", + Type = "host", + Content = "192.168.1.10,192.168.1.11,192.168.1.12", + Description = "Updated Web Servers", + Enabled = "1" + }; + + var expectedResponse = new AliasUpdateResponse + { + Result = "saved" + }; + + _apiClientMock.Setup(x => x.PostAsync($"firewall/alias/setItem/{uuid}", It.IsAny())) + .ReturnsAsync(expectedResponse); + + // Act + var result = await _aliasService.UpdateAliasAsync(uuid, alias); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual("saved", result.Result); + _loggerMock.Verify(x => x.Information($"Updating alias with UUID {uuid}"), Times.Once); + } + + [TestMethod] + public async Task DeleteAliasAsync_ReturnsSuccess() + { + // Arrange + string uuid = "uuid1"; + var expectedResponse = new AliasDeleteResponse + { + Result = "deleted" + }; + + _apiClientMock.Setup(x => x.PostAsync($"firewall/alias/delItem/{uuid}")) + .ReturnsAsync(expectedResponse); + + // Act + var result = await _aliasService.DeleteAliasAsync(uuid); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual("deleted", result.Result); + _loggerMock.Verify(x => x.Information($"Deleting alias with UUID {uuid}"), Times.Once); + } + + [TestMethod] + public async Task ReconfigureAliasesAsync_ReturnsSuccess() + { + // Arrange + var expectedResponse = new AliasReconfigureResponse + { + Status = "ok" + }; + + _apiClientMock.Setup(x => x.PostAsync("firewall/alias/reconfigure")) + .ReturnsAsync(expectedResponse); + + // Act + var result = await _aliasService.ReconfigureAliasesAsync(); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual("ok", result.Status); + _loggerMock.Verify(x => x.Information("Reconfiguring aliases"), Times.Once); + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/ApplyOPNSenseFirewallChangesCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/ApplyOPNSenseFirewallChangesCmdlet.cs new file mode 100644 index 0000000..d71d211 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/ApplyOPNSenseFirewallChangesCmdlet.cs @@ -0,0 +1,100 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Applies firewall changes on an OPNSense firewall. + /// The Apply-OPNSenseFirewallChanges cmdlet applies pending firewall changes on an OPNSense firewall. + /// This cmdlet creates a savepoint before applying changes, which allows for automatic rollback if the changes cause connectivity issues. + /// + /// Example 1: Apply firewall changes + /// Apply-OPNSenseFirewallChanges + /// This example applies pending firewall changes with automatic rollback if connectivity is lost. + /// + /// + /// Example 2: Apply firewall changes without automatic rollback + /// Apply-OPNSenseFirewallChanges -NoRollback + /// This example applies pending firewall changes without creating a savepoint for automatic rollback. + /// + /// + [Cmdlet(VerbsData.Update, "OPNSenseFirewallChanges")] + [OutputType(typeof(void))] + public class ApplyOPNSenseFirewallChangesCmdlet : OPNSenseBaseCmdlet + { + /// + /// Applies changes without creating a savepoint for automatic rollback. + /// + [Parameter(Mandatory = false)] + public SwitchParameter NoRollback { get; set; } + + /// + /// Cancels the automatic rollback after applying changes. + /// + [Parameter(Mandatory = false)] + public SwitchParameter CancelRollback { get; set; } + + /// + /// The timeout in seconds to wait before cancelling the rollback. + /// + [Parameter(Mandatory = false)] + [ValidateRange(1, 300)] + public int Timeout { get; set; } = 60; + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var firewallService = new FirewallService(ApiClient, Logger); + + if (NoRollback.IsPresent) + { + WriteVerbose("Applying firewall changes without rollback protection"); + var applyTask = Task.Run(async () => await firewallService.ApplyChangesAsync()); + var applyResult = applyTask.GetAwaiter().GetResult(); + WriteVerbose($"Firewall changes applied: {applyResult.Status}"); + } + else + { + WriteVerbose("Creating savepoint for rollback protection"); + var savepointTask = Task.Run(async () => await firewallService.CreateSavepointAsync()); + var savepointResult = savepointTask.GetAwaiter().GetResult(); + var revision = savepointResult.Revision; + + WriteVerbose($"Created savepoint with revision {revision}"); + WriteVerbose("Applying firewall changes with rollback protection"); + + var applyTask = Task.Run(async () => await firewallService.ApplyChangesAsync(revision)); + var applyResult = applyTask.GetAwaiter().GetResult(); + + WriteVerbose($"Firewall changes applied: {applyResult.Status}"); + + if (CancelRollback.IsPresent) + { + WriteVerbose($"Waiting {Timeout} seconds before cancelling rollback"); + System.Threading.Thread.Sleep(Timeout * 1000); + + WriteVerbose("Cancelling automatic rollback"); + var cancelTask = Task.Run(async () => await firewallService.CancelRollbackAsync(revision)); + var cancelResult = cancelTask.GetAwaiter().GetResult(); + + WriteVerbose($"Rollback cancelled: {cancelResult.Status}"); + } + else + { + WriteVerbose($"Automatic rollback will occur in 60 seconds if connectivity is lost"); + } + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/BackupOPNSenseConfigCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/BackupOPNSenseConfigCmdlet.cs new file mode 100644 index 0000000..5c4d527 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/BackupOPNSenseConfigCmdlet.cs @@ -0,0 +1,53 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Creates a backup of the OPNSense firewall configuration. + /// The Backup-OPNSenseConfig cmdlet creates a backup of the OPNSense firewall configuration. + /// + /// Example 1: Create a backup + /// Backup-OPNSenseConfig + /// This example creates a backup of the OPNSense firewall configuration with an automatically generated filename. + /// + /// + /// Example 2: Create a backup with a specific filename + /// Backup-OPNSenseConfig -Filename "pre-upgrade-backup" + /// This example creates a backup of the OPNSense firewall configuration with a specific filename. + /// + /// + [Cmdlet(VerbsData.Backup, "OPNSenseConfig")] + [OutputType(typeof(string))] + public class BackupOPNSenseConfigCmdlet : OPNSenseBaseCmdlet + { + /// + /// The filename for the backup. + /// + [Parameter(Mandatory = false, Position = 0)] + public string Filename { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var configService = new ConfigService(ApiClient, Logger); + + var task = Task.Run(async () => await configService.CreateConfigBackupAsync(Filename)); + var result = task.GetAwaiter().GetResult(); + + WriteVerbose($"Created configuration backup: {result.Filename}"); + WriteObject(result.Filename); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/ConnectOPNSenseCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/ConnectOPNSenseCmdlet.cs new file mode 100644 index 0000000..e276cd0 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/ConnectOPNSenseCmdlet.cs @@ -0,0 +1,99 @@ +using System; +using System.Management.Automation; +using PSOPNSenseAPI.Logging; +using PSOPNSenseAPI.Models; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Connects to an OPNSense firewall. + /// The Connect-OPNSense cmdlet establishes a connection to an OPNSense firewall using the API. + /// This cmdlet must be called before using any other cmdlets in the module. + /// + /// Example 1: Connect to an OPNSense firewall + /// Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" + /// This example connects to an OPNSense firewall at the specified URL using the provided API credentials. + /// + /// + /// Example 2: Connect to an OPNSense firewall with certificate validation disabled + /// Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck + /// This example connects to an OPNSense firewall at the specified URL using the provided API credentials, skipping certificate validation. + /// + /// + [Cmdlet(VerbsCommunications.Connect, "OPNSense")] + [OutputType(typeof(void))] + public class ConnectOPNSenseCmdlet : PSCmdlet + { + /// + /// The URL of the OPNSense firewall. + /// + [Parameter(Mandatory = true, Position = 0)] + [ValidateNotNullOrEmpty] + public string Server { get; set; } + + /// + /// The API key for authentication. + /// + [Parameter(Mandatory = true, Position = 1)] + [ValidateNotNullOrEmpty] + public string ApiKey { get; set; } + + /// + /// The API secret for authentication. + /// + [Parameter(Mandatory = true, Position = 2)] + [ValidateNotNullOrEmpty] + public string ApiSecret { get; set; } + + /// + /// Skips certificate validation when connecting to the OPNSense firewall. + /// + [Parameter(Mandatory = false)] + public SwitchParameter SkipCertificateCheck { get; set; } + + /// + /// Forces a new connection even if one already exists. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Force { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + // Check if already connected + if (OPNSenseSession.IsConnected && !Force.IsPresent) + { + WriteWarning($"Already connected to {OPNSenseSession.BaseUrl}. Use -Force to reconnect."); + return; + } + + // Dispose existing connection if there is one + OPNSenseSession.Current?.Dispose(); + + // Create a new logger + var logger = new PowerShellLogger(this); + + // Create a new API client + var client = new OPNSenseApiClient(Server, ApiKey, ApiSecret, SkipCertificateCheck.IsPresent, logger); + + // Set the current session + OPNSenseSession.Current = client; + + WriteVerbose($"Connected to OPNSense firewall at {Server}"); + } + catch (Exception ex) + { + WriteError(new ErrorRecord( + ex, + "ConnectionFailed", + ErrorCategory.ConnectionError, + Server)); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/ConnectOPNSenseTailscaleCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/ConnectOPNSenseTailscaleCmdlet.cs new file mode 100644 index 0000000..0e5fd4b --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/ConnectOPNSenseTailscaleCmdlet.cs @@ -0,0 +1,253 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Connects an OPNSense firewall to a Tailscale network. + /// The Connect-OPNSenseTailscale cmdlet connects an OPNSense firewall to a Tailscale network using an authentication key. + /// + /// Example 1: Connect to Tailscale network + /// Connect-OPNSenseTailscale -AuthKey "tskey-auth-abcdef123456" + /// This example connects the OPNSense firewall to a Tailscale network using the specified authentication key. + /// + /// + /// Example 2: Connect to Tailscale network with auto-installation + /// Connect-OPNSenseTailscale -AuthKey "tskey-auth-abcdef123456" -InstallIfMissing + /// This example installs the Tailscale plugin if it's not already installed, then connects to the Tailscale network. + /// + /// + [Cmdlet(VerbsCommunications.Connect, "OPNSenseTailscale", SupportsShouldProcess = true)] + [OutputType(typeof(PSObject))] + public class ConnectOPNSenseTailscaleCmdlet : OPNSenseBaseCmdlet + { + /// + /// The Tailscale authentication key to use for connecting. + /// + [Parameter(Mandatory = true, Position = 0)] + [ValidateNotNullOrEmpty] + public string AuthKey { get; set; } + + /// + /// Whether to install the Tailscale plugin if it's not already installed. + /// + [Parameter(Mandatory = false)] + public SwitchParameter InstallIfMissing { get; set; } + + /// + /// Whether to enable Tailscale if it's not already enabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter EnableIfDisabled { get; set; } + + /// + /// Whether to start the Tailscale service if it's not already running. + /// + [Parameter(Mandatory = false)] + public SwitchParameter StartIfStopped { get; set; } + + /// + /// Whether to advertise routes to the Tailscale network. + /// + [Parameter(Mandatory = false)] + public SwitchParameter AdvertiseRoutes { get; set; } + + /// + /// The subnet routes to advertise to the Tailscale network (e.g., "192.168.1.0/24", "10.0.0.0/8"). + /// Multiple routes can be specified. + /// + [Parameter(Mandatory = false)] + public string[] SubnetRoutes { get; set; } + + /// + /// Whether to advertise this node as an exit node. + /// + [Parameter(Mandatory = false)] + public SwitchParameter AdvertiseExitNode { get; set; } + + /// + /// Suppresses the confirmation prompt. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Force { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var tailscaleService = new TailscaleService(ApiClient, Logger); + + // Check if the plugin is installed + var isInstalledTask = Task.Run(async () => await tailscaleService.IsPluginInstalledAsync()); + var isInstalled = isInstalledTask.GetAwaiter().GetResult(); + + if (!isInstalled) + { + if (!InstallIfMissing.IsPresent) + { + WriteError(new ErrorRecord( + new Exception("Tailscale plugin is not installed. Use -InstallIfMissing to install it."), + "TailscalePluginNotInstalled", + ErrorCategory.InvalidOperation, + null)); + return; + } + + if (!Force.IsPresent && !ShouldProcess("OPNSense firewall", "Install Tailscale plugin")) + { + return; + } + + WriteVerbose("Tailscale plugin is not installed. Installing..."); + var installTask = Task.Run(async () => await tailscaleService.InstallPluginAsync()); + var installResult = installTask.GetAwaiter().GetResult(); + + if (!installResult) + { + WriteError(new ErrorRecord( + new Exception("Failed to install Tailscale plugin."), + "TailscalePluginInstallFailed", + ErrorCategory.InvalidOperation, + null)); + return; + } + + WriteVerbose("Tailscale plugin installed successfully."); + } + + // Get current status + var statusTask = Task.Run(async () => await tailscaleService.GetStatusAsync()); + var status = statusTask.GetAwaiter().GetResult(); + + // Enable Tailscale if needed + if (!status.Enabled && EnableIfDisabled.IsPresent) + { + WriteVerbose("Tailscale is disabled. Enabling..."); + + // Get current settings + var settingsTask = Task.Run(async () => await tailscaleService.GetSettingsAsync()); + var currentSettings = settingsTask.GetAwaiter().GetResult().General; + + // Process subnet routes if provided + string routesToAdvertise = currentSettings.RoutesToAdvertise; + bool advertiseRoutes = AdvertiseRoutes.IsPresent || currentSettings.AdvertiseRoutes == "1"; + bool advertiseExitNode = AdvertiseExitNode.IsPresent || currentSettings.AdvertiseExitNode == "1"; + + if (SubnetRoutes != null && SubnetRoutes.Length > 0) + { + routesToAdvertise = string.Join(",", SubnetRoutes); + WriteVerbose($"Advertising subnet routes: {routesToAdvertise}"); + + // If routes are specified but AdvertiseRoutes is not set, enable it automatically + if (!advertiseRoutes) + { + WriteVerbose("Automatically enabling route advertisement because subnet routes were specified."); + advertiseRoutes = true; + } + } + + // Create new settings with enabled flag + var settings = new TailscaleSettings + { + Enabled = "1", + AcceptDns = currentSettings.AcceptDns, + AcceptRoutes = currentSettings.AcceptRoutes, + AdvertiseExitNode = advertiseExitNode ? "1" : "0", + AdvertiseRoutes = advertiseRoutes ? "1" : "0", + RoutesToAdvertise = routesToAdvertise, + Hostname = currentSettings.Hostname, + LoginServer = currentSettings.LoginServer, + Ssh = currentSettings.Ssh, + Ephemeral = currentSettings.Ephemeral, + ResetOnStart = currentSettings.ResetOnStart + }; + + // Update settings + var updateTask = Task.Run(async () => await tailscaleService.UpdateSettingsAsync(settings)); + var updateResult = updateTask.GetAwaiter().GetResult(); + + WriteVerbose($"Tailscale settings updated: {updateResult.Result}"); + + // Get updated status + statusTask = Task.Run(async () => await tailscaleService.GetStatusAsync()); + status = statusTask.GetAwaiter().GetResult(); + } + + // Start the service if needed + if (!status.Running && StartIfStopped.IsPresent) + { + WriteVerbose("Tailscale service is not running. Starting..."); + var startTask = Task.Run(async () => await tailscaleService.StartServiceAsync()); + var startResult = startTask.GetAwaiter().GetResult(); + WriteVerbose($"Tailscale service started: {startResult.Status}"); + + // Get updated status + statusTask = Task.Run(async () => await tailscaleService.GetStatusAsync()); + status = statusTask.GetAwaiter().GetResult(); + } + + // Check if Tailscale is enabled and running + if (!status.Enabled) + { + WriteError(new ErrorRecord( + new Exception("Tailscale is not enabled. Use -EnableIfDisabled to enable it."), + "TailscaleNotEnabled", + ErrorCategory.InvalidOperation, + null)); + return; + } + + if (!status.Running) + { + WriteError(new ErrorRecord( + new Exception("Tailscale service is not running. Use -StartIfStopped to start it."), + "TailscaleNotRunning", + ErrorCategory.InvalidOperation, + null)); + return; + } + + if (!Force.IsPresent && !ShouldProcess("OPNSense firewall", "Connect to Tailscale network")) + { + return; + } + + // Connect to Tailscale + WriteVerbose("Connecting to Tailscale network..."); + var connectTask = Task.Run(async () => await tailscaleService.ConnectAsync(AuthKey)); + var connectResult = connectTask.GetAwaiter().GetResult(); + + WriteVerbose($"Tailscale connection status: {connectResult.Status}"); + WriteVerbose($"Tailscale connection message: {connectResult.Message}"); + + // Get updated status + statusTask = Task.Run(async () => await tailscaleService.GetStatusAsync()); + status = statusTask.GetAwaiter().GetResult(); + + // Get interfaces + var interfacesTask = Task.Run(async () => await tailscaleService.GetInterfacesAsync()); + var interfaces = interfacesTask.GetAwaiter().GetResult(); + + // Create result object + var result = new PSObject(); + result.Properties.Add(new PSNoteProperty("Status", status.Status)); + result.Properties.Add(new PSNoteProperty("Running", status.Running)); + result.Properties.Add(new PSNoteProperty("Enabled", status.Enabled)); + result.Properties.Add(new PSNoteProperty("ConnectionStatus", connectResult.Status)); + result.Properties.Add(new PSNoteProperty("ConnectionMessage", connectResult.Message)); + result.Properties.Add(new PSNoteProperty("Interfaces", interfaces.Interfaces)); + + WriteObject(result); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/ConvertToOPNSenseNetworkNotationCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/ConvertToOPNSenseNetworkNotationCmdlet.cs new file mode 100644 index 0000000..35401be --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/ConvertToOPNSenseNetworkNotationCmdlet.cs @@ -0,0 +1,147 @@ +using System; +using System.Management.Automation; +using PSOPNSenseAPI.Utilities; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Converts between different network notation formats. + /// The ConvertTo-OPNSenseNetworkNotation cmdlet converts between CIDR notation and subnet mask notation. + /// + /// Example 1: Convert CIDR to subnet mask + /// ConvertTo-OPNSenseNetworkNotation -CIDR 24 + /// This example converts the CIDR prefix length 24 to the subnet mask 255.255.255.0. + /// + /// + /// Example 2: Convert subnet mask to CIDR + /// ConvertTo-OPNSenseNetworkNotation -SubnetMask "255.255.255.0" + /// This example converts the subnet mask 255.255.255.0 to the CIDR prefix length 24. + /// + /// + /// Example 3: Convert IP with CIDR to IP with subnet mask + /// ConvertTo-OPNSenseNetworkNotation -IPWithCIDR "192.168.1.0/24" + /// This example converts the IP with CIDR 192.168.1.0/24 to the IP with subnet mask 192.168.1.0 255.255.255.0. + /// + /// + /// Example 4: Convert IP with subnet mask to IP with CIDR + /// ConvertTo-OPNSenseNetworkNotation -IPWithSubnetMask "192.168.1.0 255.255.255.0" + /// This example converts the IP with subnet mask 192.168.1.0 255.255.255.0 to the IP with CIDR 192.168.1.0/24. + /// + /// + [Cmdlet(VerbsData.ConvertTo, "OPNSenseNetworkNotation")] + [OutputType(typeof(string), typeof(int))] + public class ConvertToOPNSenseNetworkNotationCmdlet : PSCmdlet + { + /// + /// The CIDR prefix length to convert to a subnet mask. + /// + [Parameter(Mandatory = false, ParameterSetName = "FromCIDR")] + [ValidateRange(0, 32)] + public int? CIDR { get; set; } + + /// + /// The subnet mask to convert to a CIDR prefix length. + /// + [Parameter(Mandatory = false, ParameterSetName = "FromSubnetMask")] + public string SubnetMask { get; set; } + + /// + /// The IP address with CIDR notation to convert to IP address with subnet mask. + /// + [Parameter(Mandatory = false, ParameterSetName = "FromIPWithCIDR")] + public string IPWithCIDR { get; set; } + + /// + /// The IP address with subnet mask to convert to IP address with CIDR notation. + /// + [Parameter(Mandatory = false, ParameterSetName = "FromIPWithSubnetMask")] + public string IPWithSubnetMask { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + switch (ParameterSetName) + { + case "FromCIDR": + if (CIDR.HasValue) + { + string subnetMask = NetworkUtility.GetSubnetMaskFromCIDR(CIDR.Value); + WriteObject(subnetMask); + } + break; + + case "FromSubnetMask": + if (!string.IsNullOrEmpty(SubnetMask)) + { + int cidr = NetworkUtility.GetCIDRFromSubnetMask(SubnetMask); + WriteObject(cidr); + } + break; + + case "FromIPWithCIDR": + if (!string.IsNullOrEmpty(IPWithCIDR)) + { + string[] parts = IPWithCIDR.Split('/'); + if (parts.Length != 2) + { + WriteError(new ErrorRecord( + new ArgumentException($"Invalid IP with CIDR: {IPWithCIDR}. Expected format: 192.168.1.0/24"), + "InvalidIPWithCIDR", + ErrorCategory.InvalidArgument, + null)); + return; + } + + string ip = parts[0]; + if (!int.TryParse(parts[1], out int cidr)) + { + WriteError(new ErrorRecord( + new ArgumentException($"Invalid CIDR: {parts[1]}"), + "InvalidCIDR", + ErrorCategory.InvalidArgument, + null)); + return; + } + + string subnetMask = NetworkUtility.GetSubnetMaskFromCIDR(cidr); + WriteObject($"{ip} {subnetMask}"); + } + break; + + case "FromIPWithSubnetMask": + if (!string.IsNullOrEmpty(IPWithSubnetMask)) + { + string[] parts = IPWithSubnetMask.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length != 2) + { + WriteError(new ErrorRecord( + new ArgumentException($"Invalid IP with subnet mask: {IPWithSubnetMask}. Expected format: 192.168.1.0 255.255.255.0"), + "InvalidIPWithSubnetMask", + ErrorCategory.InvalidArgument, + null)); + return; + } + + string ip = parts[0]; + string subnetMask = parts[1]; + int cidr = NetworkUtility.GetCIDRFromSubnetMask(subnetMask); + WriteObject($"{ip}/{cidr}"); + } + break; + } + } + catch (Exception ex) + { + WriteError(new ErrorRecord( + ex, + "ConversionError", + ErrorCategory.InvalidOperation, + null)); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/DisableOPNSenseCronJobCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/DisableOPNSenseCronJobCmdlet.cs new file mode 100644 index 0000000..4472a62 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/DisableOPNSenseCronJobCmdlet.cs @@ -0,0 +1,63 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Disables a cron job on an OPNSense firewall. + /// The Disable-OPNSenseCronJob cmdlet disables a cron job on an OPNSense firewall. + /// + /// Example 1: Disable a cron job + /// Disable-OPNSenseCronJob -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" + /// This example disables a cron job by its UUID. + /// + /// + [Cmdlet(VerbsLifecycle.Disable, "OPNSenseCronJob")] + [OutputType(typeof(void))] + public class DisableOPNSenseCronJobCmdlet : OPNSenseBaseCmdlet + { + /// + /// The UUID of the cron job to disable. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// Whether to apply the changes immediately. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Apply { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var cronService = new CronService(ApiClient, Logger); + + var task = Task.Run(async () => await cronService.ToggleJobAsync(Uuid, false)); + var result = task.GetAwaiter().GetResult(); + + WriteVerbose($"Cron job {Uuid} disabled: {result.Result}"); + + // Apply the changes if requested + if (Apply.IsPresent) + { + var applyTask = Task.Run(async () => await cronService.ApplyChangesAsync()); + var applyResult = applyTask.GetAwaiter().GetResult(); + + WriteVerbose($"Cron changes applied: {applyResult.Status}"); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/DisableOPNSenseFirewallRuleCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/DisableOPNSenseFirewallRuleCmdlet.cs new file mode 100644 index 0000000..2f84dd3 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/DisableOPNSenseFirewallRuleCmdlet.cs @@ -0,0 +1,48 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Disables a firewall rule on an OPNSense firewall. + /// The Disable-OPNSenseFirewallRule cmdlet disables a firewall rule on an OPNSense firewall. + /// + /// Example 1: Disable a firewall rule + /// Disable-OPNSenseFirewallRule -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" + /// This example disables a firewall rule by its UUID. + /// + /// + [Cmdlet(VerbsLifecycle.Disable, "OPNSenseFirewallRule")] + [OutputType(typeof(void))] + public class DisableOPNSenseFirewallRuleCmdlet : OPNSenseBaseCmdlet + { + /// + /// The UUID of the firewall rule to disable. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var firewallService = new FirewallService(ApiClient, Logger); + + var task = Task.Run(async () => await firewallService.ToggleRuleAsync(Uuid, false)); + var result = task.GetAwaiter().GetResult(); + + WriteVerbose($"Firewall rule {Uuid} disabled: {result.Result}"); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/DisableOPNSensePluginCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/DisableOPNSensePluginCmdlet.cs new file mode 100644 index 0000000..057a82f --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/DisableOPNSensePluginCmdlet.cs @@ -0,0 +1,49 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Disables a plugin on an OPNSense firewall. + /// The Disable-OPNSensePlugin cmdlet disables a plugin on an OPNSense firewall. + /// + /// Example 1: Disable a plugin + /// Disable-OPNSensePlugin -Name "os-acme-client" + /// This example disables the ACME client plugin on the OPNSense firewall. + /// + /// + [Cmdlet(VerbsLifecycle.Disable, "OPNSensePlugin")] + [OutputType(typeof(void))] + public class DisableOPNSensePluginCmdlet : OPNSenseBaseCmdlet + { + /// + /// The name of the plugin to disable. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string Name { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var pluginService = new PluginService(ApiClient, Logger); + + var task = Task.Run(async () => await pluginService.DisablePluginAsync(Name)); + var result = task.GetAwaiter().GetResult(); + + WriteVerbose($"Plugin {Name} disabled: {result.Status}"); + WriteObject($"Plugin {Name} disabled: {result.Status}"); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/DisableOPNSenseTailscaleCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/DisableOPNSenseTailscaleCmdlet.cs new file mode 100644 index 0000000..222ecb4 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/DisableOPNSenseTailscaleCmdlet.cs @@ -0,0 +1,116 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Disables Tailscale on an OPNSense firewall. + /// The Disable-OPNSenseTailscale cmdlet disables Tailscale on an OPNSense firewall. + /// + /// Example 1: Disable Tailscale + /// Disable-OPNSenseTailscale + /// This example disables Tailscale on the connected OPNSense firewall. + /// + /// + /// Example 2: Disable Tailscale and stop the service + /// Disable-OPNSenseTailscale -Stop + /// This example disables Tailscale and stops the Tailscale service. + /// + /// + [Cmdlet(VerbsLifecycle.Disable, "OPNSenseTailscale", SupportsShouldProcess = true)] + [OutputType(typeof(PSObject))] + public class DisableOPNSenseTailscaleCmdlet : OPNSenseBaseCmdlet + { + /// + /// Whether to stop the Tailscale service after disabling it. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Stop { get; set; } + + /// + /// Suppresses the confirmation prompt. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Force { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var tailscaleService = new TailscaleService(ApiClient, Logger); + + // Check if the plugin is installed + var isInstalledTask = Task.Run(async () => await tailscaleService.IsPluginInstalledAsync()); + var isInstalled = isInstalledTask.GetAwaiter().GetResult(); + + if (!isInstalled) + { + WriteWarning("Tailscale plugin is not installed on the OPNSense firewall."); + return; + } + + // Get current settings + var settingsTask = Task.Run(async () => await tailscaleService.GetSettingsAsync()); + var currentSettings = settingsTask.GetAwaiter().GetResult().General; + + // Create new settings with disabled flag + var settings = new TailscaleSettings + { + Enabled = "0", + AcceptDns = currentSettings.AcceptDns, + AcceptRoutes = currentSettings.AcceptRoutes, + AdvertiseExitNode = currentSettings.AdvertiseExitNode, + AdvertiseRoutes = currentSettings.AdvertiseRoutes, + RoutesToAdvertise = currentSettings.RoutesToAdvertise, + Hostname = currentSettings.Hostname, + LoginServer = currentSettings.LoginServer, + Ssh = currentSettings.Ssh, + Ephemeral = currentSettings.Ephemeral, + ResetOnStart = currentSettings.ResetOnStart + }; + + if (!Force.IsPresent && !ShouldProcess("OPNSense firewall", "Disable Tailscale")) + { + return; + } + + // Update settings + var updateTask = Task.Run(async () => await tailscaleService.UpdateSettingsAsync(settings)); + var updateResult = updateTask.GetAwaiter().GetResult(); + + WriteVerbose($"Tailscale settings updated: {updateResult.Result}"); + + // Stop the service if requested + if (Stop.IsPresent) + { + WriteVerbose("Stopping Tailscale service..."); + var stopTask = Task.Run(async () => await tailscaleService.StopServiceAsync()); + var stopResult = stopTask.GetAwaiter().GetResult(); + WriteVerbose($"Tailscale service stopped: {stopResult.Status}"); + } + + // Get updated status + var statusTask = Task.Run(async () => await tailscaleService.GetStatusAsync()); + var status = statusTask.GetAwaiter().GetResult(); + + // Create result object + var result = new PSObject(); + result.Properties.Add(new PSNoteProperty("PluginInstalled", true)); + result.Properties.Add(new PSNoteProperty("Enabled", status.Enabled)); + result.Properties.Add(new PSNoteProperty("Running", status.Running)); + result.Properties.Add(new PSNoteProperty("Status", status.Status)); + + WriteObject(result); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/DisconnectOPNSenseCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/DisconnectOPNSenseCmdlet.cs new file mode 100644 index 0000000..6ee411f --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/DisconnectOPNSenseCmdlet.cs @@ -0,0 +1,49 @@ +using System; +using System.Management.Automation; +using PSOPNSenseAPI.Models; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Disconnects from an OPNSense firewall. + /// The Disconnect-OPNSense cmdlet closes the connection to an OPNSense firewall. + /// + /// Example 1: Disconnect from an OPNSense firewall + /// Disconnect-OPNSense + /// This example disconnects from the currently connected OPNSense firewall. + /// + /// + [Cmdlet(VerbsCommunications.Disconnect, "OPNSense")] + [OutputType(typeof(void))] + public class DisconnectOPNSenseCmdlet : PSCmdlet + { + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + if (!OPNSenseSession.IsConnected) + { + WriteWarning("Not connected to any OPNSense firewall."); + return; + } + + var baseUrl = OPNSenseSession.BaseUrl; + OPNSenseSession.Current?.Dispose(); + OPNSenseSession.Current = null; + + WriteVerbose($"Disconnected from OPNSense firewall at {baseUrl}"); + } + catch (Exception ex) + { + WriteError(new ErrorRecord( + ex, + "DisconnectionFailed", + ErrorCategory.ConnectionError, + null)); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/DisconnectOPNSenseTailscaleCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/DisconnectOPNSenseTailscaleCmdlet.cs new file mode 100644 index 0000000..dd22be5 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/DisconnectOPNSenseTailscaleCmdlet.cs @@ -0,0 +1,87 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Disconnects an OPNSense firewall from a Tailscale network. + /// The Disconnect-OPNSenseTailscale cmdlet disconnects an OPNSense firewall from a Tailscale network. + /// + /// Example 1: Disconnect from Tailscale network + /// Disconnect-OPNSenseTailscale + /// This example disconnects the OPNSense firewall from the Tailscale network. + /// + /// + [Cmdlet(VerbsCommunications.Disconnect, "OPNSenseTailscale", SupportsShouldProcess = true)] + [OutputType(typeof(PSObject))] + public class DisconnectOPNSenseTailscaleCmdlet : OPNSenseBaseCmdlet + { + /// + /// Suppresses the confirmation prompt. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Force { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var tailscaleService = new TailscaleService(ApiClient, Logger); + + // Check if the plugin is installed + var isInstalledTask = Task.Run(async () => await tailscaleService.IsPluginInstalledAsync()); + var isInstalled = isInstalledTask.GetAwaiter().GetResult(); + + if (!isInstalled) + { + WriteWarning("Tailscale plugin is not installed on the OPNSense firewall."); + return; + } + + // Get current status + var statusTask = Task.Run(async () => await tailscaleService.GetStatusAsync()); + var status = statusTask.GetAwaiter().GetResult(); + + if (!status.Running) + { + WriteWarning("Tailscale service is not running."); + return; + } + + if (!Force.IsPresent && !ShouldProcess("OPNSense firewall", "Disconnect from Tailscale network")) + { + return; + } + + // Disconnect from Tailscale + WriteVerbose("Disconnecting from Tailscale network..."); + var disconnectTask = Task.Run(async () => await tailscaleService.DisconnectAsync()); + var disconnectResult = disconnectTask.GetAwaiter().GetResult(); + + WriteVerbose($"Tailscale disconnection status: {disconnectResult.Status}"); + + // Get updated status + statusTask = Task.Run(async () => await tailscaleService.GetStatusAsync()); + status = statusTask.GetAwaiter().GetResult(); + + // Create result object + var result = new PSObject(); + result.Properties.Add(new PSNoteProperty("Status", status.Status)); + result.Properties.Add(new PSNoteProperty("Running", status.Running)); + result.Properties.Add(new PSNoteProperty("Enabled", status.Enabled)); + result.Properties.Add(new PSNoteProperty("DisconnectionStatus", disconnectResult.Status)); + + WriteObject(result); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/EnableOPNSenseCronJobCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/EnableOPNSenseCronJobCmdlet.cs new file mode 100644 index 0000000..b52c043 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/EnableOPNSenseCronJobCmdlet.cs @@ -0,0 +1,63 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Enables a cron job on an OPNSense firewall. + /// The Enable-OPNSenseCronJob cmdlet enables a cron job on an OPNSense firewall. + /// + /// Example 1: Enable a cron job + /// Enable-OPNSenseCronJob -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" + /// This example enables a cron job by its UUID. + /// + /// + [Cmdlet(VerbsLifecycle.Enable, "OPNSenseCronJob")] + [OutputType(typeof(void))] + public class EnableOPNSenseCronJobCmdlet : OPNSenseBaseCmdlet + { + /// + /// The UUID of the cron job to enable. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// Whether to apply the changes immediately. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Apply { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var cronService = new CronService(ApiClient, Logger); + + var task = Task.Run(async () => await cronService.ToggleJobAsync(Uuid, true)); + var result = task.GetAwaiter().GetResult(); + + WriteVerbose($"Cron job {Uuid} enabled: {result.Result}"); + + // Apply the changes if requested + if (Apply.IsPresent) + { + var applyTask = Task.Run(async () => await cronService.ApplyChangesAsync()); + var applyResult = applyTask.GetAwaiter().GetResult(); + + WriteVerbose($"Cron changes applied: {applyResult.Status}"); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/EnableOPNSenseFirewallRuleCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/EnableOPNSenseFirewallRuleCmdlet.cs new file mode 100644 index 0000000..7290d90 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/EnableOPNSenseFirewallRuleCmdlet.cs @@ -0,0 +1,48 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Enables a firewall rule on an OPNSense firewall. + /// The Enable-OPNSenseFirewallRule cmdlet enables a firewall rule on an OPNSense firewall. + /// + /// Example 1: Enable a firewall rule + /// Enable-OPNSenseFirewallRule -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" + /// This example enables a firewall rule by its UUID. + /// + /// + [Cmdlet(VerbsLifecycle.Enable, "OPNSenseFirewallRule")] + [OutputType(typeof(void))] + public class EnableOPNSenseFirewallRuleCmdlet : OPNSenseBaseCmdlet + { + /// + /// The UUID of the firewall rule to enable. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var firewallService = new FirewallService(ApiClient, Logger); + + var task = Task.Run(async () => await firewallService.ToggleRuleAsync(Uuid, true)); + var result = task.GetAwaiter().GetResult(); + + WriteVerbose($"Firewall rule {Uuid} enabled: {result.Result}"); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/EnableOPNSensePluginCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/EnableOPNSensePluginCmdlet.cs new file mode 100644 index 0000000..f1bf3b2 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/EnableOPNSensePluginCmdlet.cs @@ -0,0 +1,49 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Enables a plugin on an OPNSense firewall. + /// The Enable-OPNSensePlugin cmdlet enables a plugin on an OPNSense firewall. + /// + /// Example 1: Enable a plugin + /// Enable-OPNSensePlugin -Name "os-acme-client" + /// This example enables the ACME client plugin on the OPNSense firewall. + /// + /// + [Cmdlet(VerbsLifecycle.Enable, "OPNSensePlugin")] + [OutputType(typeof(void))] + public class EnableOPNSensePluginCmdlet : OPNSenseBaseCmdlet + { + /// + /// The name of the plugin to enable. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string Name { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var pluginService = new PluginService(ApiClient, Logger); + + var task = Task.Run(async () => await pluginService.EnablePluginAsync(Name)); + var result = task.GetAwaiter().GetResult(); + + WriteVerbose($"Plugin {Name} enabled: {result.Status}"); + WriteObject($"Plugin {Name} enabled: {result.Status}"); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/EnableOPNSenseTailscaleCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/EnableOPNSenseTailscaleCmdlet.cs new file mode 100644 index 0000000..6e28778 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/EnableOPNSenseTailscaleCmdlet.cs @@ -0,0 +1,235 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Enables and configures Tailscale on an OPNSense firewall. + /// The Enable-OPNSenseTailscale cmdlet enables and configures Tailscale on an OPNSense firewall. If the Tailscale plugin is not installed, it will be installed automatically. + /// + /// Example 1: Enable Tailscale with default settings + /// Enable-OPNSenseTailscale + /// This example enables Tailscale with default settings. + /// + /// + /// Example 2: Enable Tailscale with custom settings + /// Enable-OPNSenseTailscale -AcceptDns -AcceptRoutes -Hostname "opnsense-firewall" + /// This example enables Tailscale and configures it to accept DNS and routes from Tailscale, and sets the hostname. + /// + /// + [Cmdlet(VerbsLifecycle.Enable, "OPNSenseTailscale", SupportsShouldProcess = true)] + [OutputType(typeof(PSObject))] + public class EnableOPNSenseTailscaleCmdlet : OPNSenseBaseCmdlet + { + /// + /// Whether to accept DNS configuration from Tailscale. + /// + [Parameter(Mandatory = false)] + public SwitchParameter AcceptDns { get; set; } + + /// + /// Whether to accept routes from Tailscale. + /// + [Parameter(Mandatory = false)] + public SwitchParameter AcceptRoutes { get; set; } + + /// + /// Whether to advertise this node as an exit node. + /// + [Parameter(Mandatory = false)] + public SwitchParameter AdvertiseExitNode { get; set; } + + /// + /// Whether to advertise routes to the Tailscale network. + /// + [Parameter(Mandatory = false)] + public SwitchParameter AdvertiseRoutes { get; set; } + + /// + /// The routes to advertise to the Tailscale network (e.g., "192.168.1.0/24,10.0.0.0/8"). + /// Multiple routes should be comma-separated. + /// + [Parameter(Mandatory = false)] + public string[] SubnetRoutes { get; set; } + + /// + /// The hostname to use for this node in the Tailscale network. + /// + [Parameter(Mandatory = false)] + public string Hostname { get; set; } + + /// + /// The login server to use (leave empty for default). + /// + [Parameter(Mandatory = false)] + public string LoginServer { get; set; } + + /// + /// Whether to enable SSH access through Tailscale. + /// + [Parameter(Mandatory = false)] + public SwitchParameter EnableSsh { get; set; } + + /// + /// Whether to use an ephemeral node key (node will disappear from network when restarted). + /// + [Parameter(Mandatory = false)] + public SwitchParameter Ephemeral { get; set; } + + /// + /// Whether to reset the Tailscale state on service start. + /// + [Parameter(Mandatory = false)] + public SwitchParameter ResetOnStart { get; set; } + + /// + /// Whether to start the Tailscale service after enabling it. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Start { get; set; } = true; + + /// + /// Whether to restart the Tailscale service if it's already running. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Restart { get; set; } + + /// + /// Whether to force installation of the Tailscale plugin if it's not already installed. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Force { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var tailscaleService = new TailscaleService(ApiClient, Logger); + + // Check if the plugin is installed + var isInstalledTask = Task.Run(async () => await tailscaleService.IsPluginInstalledAsync()); + var isInstalled = isInstalledTask.GetAwaiter().GetResult(); + + if (!isInstalled) + { + if (!Force.IsPresent && !ShouldProcess("OPNSense firewall", "Install Tailscale plugin")) + { + WriteWarning("Tailscale plugin is not installed and -Force was not specified. Operation cancelled."); + return; + } + + WriteVerbose("Tailscale plugin is not installed. Installing..."); + var installTask = Task.Run(async () => await tailscaleService.InstallPluginAsync()); + var installResult = installTask.GetAwaiter().GetResult(); + + if (!installResult) + { + WriteError(new ErrorRecord( + new Exception("Failed to install Tailscale plugin."), + "TailscalePluginInstallFailed", + ErrorCategory.InvalidOperation, + null)); + return; + } + + WriteVerbose("Tailscale plugin installed successfully."); + } + + // Get current settings + var settingsTask = Task.Run(async () => await tailscaleService.GetSettingsAsync()); + var currentSettings = settingsTask.GetAwaiter().GetResult().General; + + // Process subnet routes if provided + string routesToAdvertise = currentSettings.RoutesToAdvertise; + if (SubnetRoutes != null && SubnetRoutes.Length > 0) + { + routesToAdvertise = string.Join(",", SubnetRoutes); + WriteVerbose($"Advertising subnet routes: {routesToAdvertise}"); + + // If routes are specified but AdvertiseRoutes is not set, enable it automatically + if (!AdvertiseRoutes.IsPresent) + { + WriteVerbose("Automatically enabling route advertisement because subnet routes were specified."); + AdvertiseRoutes = true; + } + } + + // Create new settings + var settings = new TailscaleSettings + { + Enabled = "1", + AcceptDns = AcceptDns.IsPresent ? "1" : "0", + AcceptRoutes = AcceptRoutes.IsPresent ? "1" : "0", + AdvertiseExitNode = AdvertiseExitNode.IsPresent ? "1" : "0", + AdvertiseRoutes = AdvertiseRoutes.IsPresent ? "1" : "0", + RoutesToAdvertise = routesToAdvertise, + Hostname = Hostname ?? currentSettings.Hostname, + LoginServer = LoginServer ?? currentSettings.LoginServer, + Ssh = EnableSsh.IsPresent ? "1" : "0", + Ephemeral = Ephemeral.IsPresent ? "1" : "0", + ResetOnStart = ResetOnStart.IsPresent ? "1" : "0" + }; + + if (!ShouldProcess("OPNSense firewall", "Enable and configure Tailscale")) + { + return; + } + + // Update settings + var updateTask = Task.Run(async () => await tailscaleService.UpdateSettingsAsync(settings)); + var updateResult = updateTask.GetAwaiter().GetResult(); + + WriteVerbose($"Tailscale settings updated: {updateResult.Result}"); + + // Get current status + var statusTask = Task.Run(async () => await tailscaleService.GetStatusAsync()); + var status = statusTask.GetAwaiter().GetResult(); + + // Start or restart the service if requested + if (Restart.IsPresent || (Start.IsPresent && !status.Running)) + { + if (status.Running) + { + if (Restart.IsPresent) + { + WriteVerbose("Restarting Tailscale service..."); + var restartTask = Task.Run(async () => await tailscaleService.RestartServiceAsync()); + var restartResult = restartTask.GetAwaiter().GetResult(); + WriteVerbose($"Tailscale service restarted: {restartResult.Status}"); + } + } + else if (Start.IsPresent) + { + WriteVerbose("Starting Tailscale service..."); + var startTask = Task.Run(async () => await tailscaleService.StartServiceAsync()); + var startResult = startTask.GetAwaiter().GetResult(); + WriteVerbose($"Tailscale service started: {startResult.Status}"); + } + } + + // Get updated status + statusTask = Task.Run(async () => await tailscaleService.GetStatusAsync()); + status = statusTask.GetAwaiter().GetResult(); + + // Create result object + var result = new PSObject(); + result.Properties.Add(new PSNoteProperty("PluginInstalled", true)); + result.Properties.Add(new PSNoteProperty("Enabled", status.Enabled)); + result.Properties.Add(new PSNoteProperty("Running", status.Running)); + result.Properties.Add(new PSNoteProperty("Status", status.Status)); + result.Properties.Add(new PSNoteProperty("Settings", settings)); + + WriteObject(result); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/ExportOPNSenseConfigCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/ExportOPNSenseConfigCmdlet.cs new file mode 100644 index 0000000..81f0083 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/ExportOPNSenseConfigCmdlet.cs @@ -0,0 +1,76 @@ +using System; +using System.IO; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Exports the OPNSense firewall configuration. + /// The Export-OPNSenseConfig cmdlet exports the OPNSense firewall configuration to a file. + /// + /// Example 1: Export the configuration to a file + /// Export-OPNSenseConfig -Path "C:\Backups\opnsense-config.xml" + /// This example exports the OPNSense firewall configuration to a file. + /// + /// + [Cmdlet(VerbsData.Export, "OPNSenseConfig")] + [OutputType(typeof(void))] + public class ExportOPNSenseConfigCmdlet : OPNSenseBaseCmdlet + { + /// + /// The path to save the configuration file. + /// + [Parameter(Mandatory = true, Position = 0)] + [ValidateNotNullOrEmpty] + public string Path { get; set; } + + /// + /// Overwrites the file if it exists. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Force { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + // Check if the file exists + if (File.Exists(Path) && !Force.IsPresent) + { + WriteError(new ErrorRecord( + new IOException($"The file '{Path}' already exists. Use -Force to overwrite."), + "FileExists", + ErrorCategory.ResourceExists, + Path)); + return; + } + + var configService = new ConfigService(ApiClient, Logger); + + var task = Task.Run(async () => await configService.ExportConfigAsync()); + var configContent = task.GetAwaiter().GetResult(); + + // Create the directory if it doesn't exist + var directory = System.IO.Path.GetDirectoryName(Path); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + // Write the configuration to the file + File.WriteAllBytes(Path, configContent); + + WriteVerbose($"Exported configuration to {Path}"); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseAliasCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseAliasCmdlet.cs new file mode 100644 index 0000000..50ef56a --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseAliasCmdlet.cs @@ -0,0 +1,106 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Gets aliases from an OPNSense firewall. + /// The Get-OPNSenseAlias cmdlet retrieves aliases from an OPNSense firewall. + /// + /// Example 1: Get all aliases + /// Get-OPNSenseAlias + /// This example retrieves all aliases from the connected OPNSense firewall. + /// + /// + /// Example 2: Get a specific alias by UUID + /// Get-OPNSenseAlias -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" + /// This example retrieves a specific alias by its UUID. + /// + /// + /// Example 3: Get aliases by type + /// Get-OPNSenseAlias | Where-Object { $_.Type -eq "host" } + /// This example retrieves all host aliases. + /// + /// + [Cmdlet(VerbsCommon.Get, "OPNSenseAlias")] + [OutputType(typeof(Alias), typeof(AliasDetail))] + public class GetOPNSenseAliasCmdlet : OPNSenseBaseCmdlet + { + /// + /// The UUID of the alias to retrieve. + /// + [Parameter(Mandatory = false, Position = 0, ParameterSetName = "ByUuid")] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// The name of the alias to retrieve. + /// + [Parameter(Mandatory = false, Position = 0, ParameterSetName = "ByName")] + [ValidateNotNullOrEmpty] + public string Name { get; set; } + + /// + /// The type of aliases to retrieve. + /// + [Parameter(Mandatory = false)] + [ValidateSet("host", "network", "port", "url", "urltable", "geoip", "networkgroup", "mac", "interface", "dynipv6host", "internal", "external")] + public string Type { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var aliasService = new AliasService(ApiClient, Logger); + + if (ParameterSetName == "ByUuid") + { + var task = Task.Run(async () => await aliasService.GetAliasAsync(Uuid)); + var result = task.GetAwaiter().GetResult(); + WriteObject(result.Alias); + } + else + { + var task = Task.Run(async () => await aliasService.GetAliasesAsync()); + var result = task.GetAwaiter().GetResult(); + + // Filter by name if specified + if (ParameterSetName == "ByName") + { + var filteredByName = result.Rows.FindAll(a => a.Name.Equals(Name, StringComparison.OrdinalIgnoreCase)); + + // Further filter by type if specified + if (!string.IsNullOrEmpty(Type)) + { + var filteredByType = filteredByName.FindAll(a => a.Type.Equals(Type, StringComparison.OrdinalIgnoreCase)); + WriteObject(filteredByType, true); + } + else + { + WriteObject(filteredByName, true); + } + } + // Filter by type if specified + else if (!string.IsNullOrEmpty(Type)) + { + var filteredByType = result.Rows.FindAll(a => a.Type.Equals(Type, StringComparison.OrdinalIgnoreCase)); + WriteObject(filteredByType, true); + } + else + { + WriteObject(result.Rows, true); + } + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseConfigBackupCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseConfigBackupCmdlet.cs new file mode 100644 index 0000000..4beb35f --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseConfigBackupCmdlet.cs @@ -0,0 +1,41 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Gets configuration backups from an OPNSense firewall. + /// The Get-OPNSenseConfigBackup cmdlet retrieves configuration backups from an OPNSense firewall. + /// + /// Example 1: Get all configuration backups + /// Get-OPNSenseConfigBackup + /// This example retrieves all configuration backups from the connected OPNSense firewall. + /// + /// + [Cmdlet(VerbsCommon.Get, "OPNSenseConfigBackup")] + [OutputType(typeof(ConfigBackup))] + public class GetOPNSenseConfigBackupCmdlet : OPNSenseBaseCmdlet + { + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var configService = new ConfigService(ApiClient, Logger); + + var task = Task.Run(async () => await configService.GetConfigBackupsAsync()); + var result = task.GetAwaiter().GetResult(); + + WriteObject(result.Backups, true); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseConnectionCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseConnectionCmdlet.cs new file mode 100644 index 0000000..e69259d --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseConnectionCmdlet.cs @@ -0,0 +1,49 @@ +using System; +using System.Management.Automation; +using PSOPNSenseAPI.Models; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Gets information about the current OPNSense connection. + /// The Get-OPNSenseConnection cmdlet retrieves information about the current connection to an OPNSense firewall. + /// + /// Example 1: Get the current connection information + /// Get-OPNSenseConnection + /// This example retrieves information about the current connection to an OPNSense firewall. + /// + /// + [Cmdlet(VerbsCommon.Get, "OPNSenseConnection")] + [OutputType(typeof(PSObject))] + public class GetOPNSenseConnectionCmdlet : PSCmdlet + { + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + if (!OPNSenseSession.IsConnected) + { + WriteWarning("Not connected to any OPNSense firewall."); + return; + } + + var connectionInfo = new PSObject(); + connectionInfo.Properties.Add(new PSNoteProperty("Server", OPNSenseSession.BaseUrl)); + connectionInfo.Properties.Add(new PSNoteProperty("Connected", OPNSenseSession.IsConnected)); + + WriteObject(connectionInfo); + } + catch (Exception ex) + { + WriteError(new ErrorRecord( + ex, + "GetConnectionFailed", + ErrorCategory.ConnectionError, + null)); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseCronJobCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseCronJobCmdlet.cs new file mode 100644 index 0000000..f36e7c6 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseCronJobCmdlet.cs @@ -0,0 +1,61 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Gets cron jobs from an OPNSense firewall. + /// The Get-OPNSenseCronJob cmdlet retrieves cron jobs from an OPNSense firewall. + /// + /// Example 1: Get all cron jobs + /// Get-OPNSenseCronJob + /// This example retrieves all cron jobs from the connected OPNSense firewall. + /// + /// + /// Example 2: Get a specific cron job by UUID + /// Get-OPNSenseCronJob -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" + /// This example retrieves a specific cron job by its UUID. + /// + /// + [Cmdlet(VerbsCommon.Get, "OPNSenseCronJob")] + [OutputType(typeof(CronJob), typeof(CronJobDetail))] + public class GetOPNSenseCronJobCmdlet : OPNSenseBaseCmdlet + { + /// + /// The UUID of the cron job to retrieve. + /// + [Parameter(Mandatory = false, Position = 0, ParameterSetName = "ByUuid")] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var cronService = new CronService(ApiClient, Logger); + + if (ParameterSetName == "ByUuid") + { + var task = Task.Run(async () => await cronService.GetJobAsync(Uuid)); + var result = task.GetAwaiter().GetResult(); + WriteObject(result.Job); + } + else + { + var task = Task.Run(async () => await cronService.GetJobsAsync()); + var result = task.GetAwaiter().GetResult(); + WriteObject(result.Rows, true); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseDHCPLeaseCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseDHCPLeaseCmdlet.cs new file mode 100644 index 0000000..f38693a --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseDHCPLeaseCmdlet.cs @@ -0,0 +1,46 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Gets DHCP leases from an OPNSense firewall. + /// The Get-OPNSenseDHCPLease cmdlet retrieves DHCP leases from an OPNSense firewall. + /// + /// Example 1: Get all DHCP leases + /// Get-OPNSenseDHCPLease + /// This example retrieves all DHCP leases from the connected OPNSense firewall. + /// + /// + /// Example 2: Get active DHCP leases + /// Get-OPNSenseDHCPLease | Where-Object { $_.State -eq "active" } + /// This example retrieves all active DHCP leases from the connected OPNSense firewall. + /// + /// + [Cmdlet(VerbsCommon.Get, "OPNSenseDHCPLease")] + [OutputType(typeof(DHCPLease))] + public class GetOPNSenseDHCPLeaseCmdlet : OPNSenseBaseCmdlet + { + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var dhcpService = new DHCPService(ApiClient, Logger); + + var task = Task.Run(async () => await dhcpService.GetLeasesAsync()); + var result = task.GetAwaiter().GetResult(); + + WriteObject(result.Rows, true); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseDHCPOptionCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseDHCPOptionCmdlet.cs new file mode 100644 index 0000000..0bf7ee5 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseDHCPOptionCmdlet.cs @@ -0,0 +1,68 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Gets DHCP options from an OPNSense firewall. + /// The Get-OPNSenseDHCPOption cmdlet retrieves DHCP options from an OPNSense firewall. + /// + /// Example 1: Get all DHCP options for an interface + /// Get-OPNSenseDHCPOption -Interface "lan" + /// This example retrieves all DHCP options for the LAN interface. + /// + /// + /// Example 2: Get a specific DHCP option by UUID + /// Get-OPNSenseDHCPOption -Interface "lan" -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" + /// This example retrieves a specific DHCP option by its UUID. + /// + /// + [Cmdlet(VerbsCommon.Get, "OPNSenseDHCPOption")] + [OutputType(typeof(DHCPOption), typeof(DHCPOptionDetail))] + public class GetOPNSenseDHCPOptionCmdlet : OPNSenseBaseCmdlet + { + /// + /// The interface name. + /// + [Parameter(Mandatory = true, Position = 0)] + [ValidateNotNullOrEmpty] + public string Interface { get; set; } + + /// + /// The UUID of the DHCP option to retrieve. + /// + [Parameter(Mandatory = false, Position = 1, ParameterSetName = "ByUuid")] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var dhcpService = new DHCPService(ApiClient, Logger); + + if (ParameterSetName == "ByUuid") + { + var task = Task.Run(async () => await dhcpService.GetOptionAsync(Interface, Uuid)); + var result = task.GetAwaiter().GetResult(); + WriteObject(result.Option); + } + else + { + var task = Task.Run(async () => await dhcpService.GetOptionsAsync(Interface)); + var result = task.GetAwaiter().GetResult(); + WriteObject(result.Rows, true); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseDHCPServerCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseDHCPServerCmdlet.cs new file mode 100644 index 0000000..7a74e27 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseDHCPServerCmdlet.cs @@ -0,0 +1,76 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Gets DHCP servers from an OPNSense firewall. + /// The Get-OPNSenseDHCPServer cmdlet retrieves DHCP servers from an OPNSense firewall. + /// + /// Example 1: Get all DHCP servers + /// Get-OPNSenseDHCPServer + /// This example retrieves all DHCP servers from the connected OPNSense firewall. + /// + /// + /// Example 2: Get a specific DHCP server by interface + /// Get-OPNSenseDHCPServer -Interface "lan" + /// This example retrieves the DHCP server for the LAN interface. + /// + /// + [Cmdlet(VerbsCommon.Get, "OPNSenseDHCPServer")] + [OutputType(typeof(PSObject), typeof(DHCPServerDetail))] + public class GetOPNSenseDHCPServerCmdlet : OPNSenseBaseCmdlet + { + /// + /// The interface to get the DHCP server for. + /// + [Parameter(Mandatory = false, Position = 0, ParameterSetName = "ByInterface")] + [ValidateNotNullOrEmpty] + public string Interface { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var dhcpService = new DHCPService(ApiClient, Logger); + + if (ParameterSetName == "ByInterface") + { + var task = Task.Run(async () => await dhcpService.GetServerAsync(Interface)); + var result = task.GetAwaiter().GetResult(); + WriteObject(result.Server); + } + else + { + var task = Task.Run(async () => await dhcpService.GetServersAsync()); + var result = task.GetAwaiter().GetResult(); + + foreach (var kvp in result.Servers.Interfaces) + { + var server = new PSObject(); + server.Properties.Add(new PSNoteProperty("Interface", kvp.Key)); + server.Properties.Add(new PSNoteProperty("Enabled", kvp.Value.Enabled == "1")); + server.Properties.Add(new PSNoteProperty("RangeFrom", kvp.Value.RangeFrom)); + server.Properties.Add(new PSNoteProperty("RangeTo", kvp.Value.RangeTo)); + server.Properties.Add(new PSNoteProperty("DefaultLeaseTime", kvp.Value.DefaultLeaseTime)); + server.Properties.Add(new PSNoteProperty("MaxLeaseTime", kvp.Value.MaxLeaseTime)); + server.Properties.Add(new PSNoteProperty("Domain", kvp.Value.Domain)); + server.Properties.Add(new PSNoteProperty("DnsServers", kvp.Value.DnsServers)); + server.Properties.Add(new PSNoteProperty("Gateway", kvp.Value.Gateway)); + + WriteObject(server); + } + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseDHCPStaticMappingCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseDHCPStaticMappingCmdlet.cs new file mode 100644 index 0000000..6604dea --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseDHCPStaticMappingCmdlet.cs @@ -0,0 +1,68 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Gets DHCP static mappings from an OPNSense firewall. + /// The Get-OPNSenseDHCPStaticMapping cmdlet retrieves DHCP static mappings from an OPNSense firewall. + /// + /// Example 1: Get all static mappings for an interface + /// Get-OPNSenseDHCPStaticMapping -Interface "lan" + /// This example retrieves all static mappings for the LAN interface. + /// + /// + /// Example 2: Get a specific static mapping by UUID + /// Get-OPNSenseDHCPStaticMapping -Interface "lan" -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" + /// This example retrieves a specific static mapping by its UUID. + /// + /// + [Cmdlet(VerbsCommon.Get, "OPNSenseDHCPStaticMapping")] + [OutputType(typeof(DHCPStaticMapping), typeof(DHCPStaticMappingDetail))] + public class GetOPNSenseDHCPStaticMappingCmdlet : OPNSenseBaseCmdlet + { + /// + /// The interface to get static mappings for. + /// + [Parameter(Mandatory = true, Position = 0)] + [ValidateNotNullOrEmpty] + public string Interface { get; set; } + + /// + /// The UUID of the static mapping to retrieve. + /// + [Parameter(Mandatory = false, Position = 1, ParameterSetName = "ByUuid")] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var dhcpService = new DHCPService(ApiClient, Logger); + + if (ParameterSetName == "ByUuid") + { + var task = Task.Run(async () => await dhcpService.GetStaticMappingAsync(Interface, Uuid)); + var result = task.GetAwaiter().GetResult(); + WriteObject(result.Mapping); + } + else + { + var task = Task.Run(async () => await dhcpService.GetStaticMappingsAsync(Interface)); + var result = task.GetAwaiter().GetResult(); + WriteObject(result.Rows, true); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseDNSForwardingCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseDNSForwardingCmdlet.cs new file mode 100644 index 0000000..630d147 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseDNSForwardingCmdlet.cs @@ -0,0 +1,41 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Gets the DNS forwarding configuration from an OPNSense firewall. + /// The Get-OPNSenseDNSForwarding cmdlet retrieves the DNS forwarding configuration from an OPNSense firewall. + /// + /// Example 1: Get DNS forwarding configuration + /// Get-OPNSenseDNSForwarding + /// This example retrieves the DNS forwarding configuration from the connected OPNSense firewall. + /// + /// + [Cmdlet(VerbsCommon.Get, "OPNSenseDNSForwarding")] + [OutputType(typeof(DNSForwardingConfig))] + public class GetOPNSenseDNSForwardingCmdlet : OPNSenseBaseCmdlet + { + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var dnsService = new DNSService(ApiClient, Logger); + + var task = Task.Run(async () => await dnsService.GetDNSForwardingAsync()); + var result = task.GetAwaiter().GetResult(); + + WriteObject(result.Forward); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseDNSForwardingHostCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseDNSForwardingHostCmdlet.cs new file mode 100644 index 0000000..9619c7c --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseDNSForwardingHostCmdlet.cs @@ -0,0 +1,61 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Gets DNS forwarding hosts from an OPNSense firewall. + /// The Get-OPNSenseDNSForwardingHost cmdlet retrieves DNS forwarding hosts from an OPNSense firewall. + /// + /// Example 1: Get all DNS forwarding hosts + /// Get-OPNSenseDNSForwardingHost + /// This example retrieves all DNS forwarding hosts from the connected OPNSense firewall. + /// + /// + /// Example 2: Get a specific DNS forwarding host by UUID + /// Get-OPNSenseDNSForwardingHost -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" + /// This example retrieves a specific DNS forwarding host by its UUID. + /// + /// + [Cmdlet(VerbsCommon.Get, "OPNSenseDNSForwardingHost")] + [OutputType(typeof(DNSForwardingHost), typeof(DNSForwardingHostDetail))] + public class GetOPNSenseDNSForwardingHostCmdlet : OPNSenseBaseCmdlet + { + /// + /// The UUID of the DNS forwarding host to retrieve. + /// + [Parameter(Mandatory = false, Position = 0, ParameterSetName = "ByUuid")] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var dnsService = new DNSService(ApiClient, Logger); + + if (ParameterSetName == "ByUuid") + { + var task = Task.Run(async () => await dnsService.GetDNSForwardingHostAsync(Uuid)); + var result = task.GetAwaiter().GetResult(); + WriteObject(result.Host); + } + else + { + var task = Task.Run(async () => await dnsService.GetDNSForwardingHostsAsync()); + var result = task.GetAwaiter().GetResult(); + WriteObject(result.Rows, true); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseDNSOverrideCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseDNSOverrideCmdlet.cs new file mode 100644 index 0000000..cb67318 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseDNSOverrideCmdlet.cs @@ -0,0 +1,61 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Gets DNS overrides from an OPNSense firewall. + /// The Get-OPNSenseDNSOverride cmdlet retrieves DNS overrides from an OPNSense firewall. + /// + /// Example 1: Get all DNS overrides + /// Get-OPNSenseDNSOverride + /// This example retrieves all DNS overrides from the connected OPNSense firewall. + /// + /// + /// Example 2: Get a specific DNS override by UUID + /// Get-OPNSenseDNSOverride -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" + /// This example retrieves a specific DNS override by its UUID. + /// + /// + [Cmdlet(VerbsCommon.Get, "OPNSenseDNSOverride")] + [OutputType(typeof(DNSOverride), typeof(DNSOverrideDetail))] + public class GetOPNSenseDNSOverrideCmdlet : OPNSenseBaseCmdlet + { + /// + /// The UUID of the DNS override to retrieve. + /// + [Parameter(Mandatory = false, Position = 0, ParameterSetName = "ByUuid")] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var dnsService = new DNSService(ApiClient, Logger); + + if (ParameterSetName == "ByUuid") + { + var task = Task.Run(async () => await dnsService.GetDNSOverrideAsync(Uuid)); + var result = task.GetAwaiter().GetResult(); + WriteObject(result.Host); + } + else + { + var task = Task.Run(async () => await dnsService.GetDNSOverridesAsync()); + var result = task.GetAwaiter().GetResult(); + WriteObject(result.Rows, true); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseDNSServerCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseDNSServerCmdlet.cs new file mode 100644 index 0000000..8fd7b71 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseDNSServerCmdlet.cs @@ -0,0 +1,52 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Gets DNS server configuration from an OPNSense firewall. + /// The Get-OPNSenseDNSServer cmdlet retrieves DNS server configuration from an OPNSense firewall. + /// + /// Example 1: Get DNS server configuration + /// Get-OPNSenseDNSServer + /// This example retrieves the DNS server configuration from the connected OPNSense firewall. + /// + /// + [Cmdlet(VerbsCommon.Get, "OPNSenseDNSServer")] + [OutputType(typeof(PSObject))] + public class GetOPNSenseDNSServerCmdlet : OPNSenseBaseCmdlet + { + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var dnsService = new DNSService(ApiClient, Logger); + + var task = Task.Run(async () => await dnsService.GetDNSConfigAsync()); + var result = task.GetAwaiter().GetResult(); + + var dnsConfig = new PSObject(); + dnsConfig.Properties.Add(new PSNoteProperty("Enabled", result.Unbound.Enabled == "1")); + dnsConfig.Properties.Add(new PSNoteProperty("Port", result.Unbound.Port)); + dnsConfig.Properties.Add(new PSNoteProperty("Interfaces", result.Unbound.Interfaces)); + dnsConfig.Properties.Add(new PSNoteProperty("Forwarding", result.Unbound.Forwarding == "1")); + dnsConfig.Properties.Add(new PSNoteProperty("Forwarders", result.Unbound.Forwarders)); + dnsConfig.Properties.Add(new PSNoteProperty("RegisterDhcp", result.Unbound.RegisterDhcp == "1")); + dnsConfig.Properties.Add(new PSNoteProperty("RegisterDhcpDomain", result.Unbound.RegisterDhcpDomain)); + dnsConfig.Properties.Add(new PSNoteProperty("RegisterDhcpStatic", result.Unbound.RegisterDhcpStatic == "1")); + dnsConfig.Properties.Add(new PSNoteProperty("ActiveInterfaces", result.Unbound.ActiveInterfaces)); + + WriteObject(dnsConfig); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseFirewallRuleCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseFirewallRuleCmdlet.cs new file mode 100644 index 0000000..4c1d89b --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseFirewallRuleCmdlet.cs @@ -0,0 +1,86 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Gets firewall rules from an OPNSense firewall. + /// The Get-OPNSenseFirewallRule cmdlet retrieves firewall rules from an OPNSense firewall. + /// + /// Example 1: Get all firewall rules + /// Get-OPNSenseFirewallRule + /// This example retrieves all firewall rules from the connected OPNSense firewall. + /// + /// + /// Example 2: Get a specific firewall rule by UUID + /// Get-OPNSenseFirewallRule -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" + /// This example retrieves a specific firewall rule by its UUID. + /// + /// + /// Example 3: Search for firewall rules by description + /// Get-OPNSenseFirewallRule -SearchPhrase "Allow HTTP" + /// This example searches for firewall rules with a description containing "Allow HTTP". + /// + /// + [Cmdlet(VerbsCommon.Get, "OPNSenseFirewallRule")] + [OutputType(typeof(FirewallRule), typeof(FirewallRuleDetails))] + public class GetOPNSenseFirewallRuleCmdlet : OPNSenseBaseCmdlet + { + /// + /// The UUID of the firewall rule to retrieve. + /// + [Parameter(Mandatory = false, Position = 0, ParameterSetName = "ByUuid")] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// The search phrase to filter rules by. + /// + [Parameter(Mandatory = false, ParameterSetName = "BySearch")] + public string SearchPhrase { get; set; } = ""; + + /// + /// The page number to retrieve. + /// + [Parameter(Mandatory = false, ParameterSetName = "BySearch")] + [ValidateRange(1, int.MaxValue)] + public int Page { get; set; } = 1; + + /// + /// The number of rules to retrieve per page. + /// + [Parameter(Mandatory = false, ParameterSetName = "BySearch")] + [ValidateRange(1, 1000)] + public int RowCount { get; set; } = 100; + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var firewallService = new FirewallService(ApiClient, Logger); + + if (ParameterSetName == "ByUuid") + { + var task = Task.Run(async () => await firewallService.GetRuleAsync(Uuid)); + var result = task.GetAwaiter().GetResult(); + WriteObject(result.Rule); + } + else + { + var task = Task.Run(async () => await firewallService.GetRulesAsync(SearchPhrase, Page, RowCount)); + var result = task.GetAwaiter().GetResult(); + WriteObject(result.Rows, true); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseFirmwareCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseFirmwareCmdlet.cs new file mode 100644 index 0000000..dcfb107 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseFirmwareCmdlet.cs @@ -0,0 +1,112 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Gets firmware information from an OPNSense firewall. + /// The Get-OPNSenseFirmware cmdlet retrieves firmware information from an OPNSense firewall. + /// + /// Example 1: Get firmware status + /// Get-OPNSenseFirmware + /// This example retrieves the firmware status from the connected OPNSense firewall. + /// + /// + /// Example 2: Get firmware changelog + /// Get-OPNSenseFirmware -Changelog + /// This example retrieves the firmware changelog from the connected OPNSense firewall. + /// + /// + /// Example 3: Get firmware audit + /// Get-OPNSenseFirmware -Audit + /// This example retrieves the firmware audit from the connected OPNSense firewall. + /// + /// + [Cmdlet(VerbsCommon.Get, "OPNSenseFirmware")] + [OutputType(typeof(PSObject))] + public class GetOPNSenseFirmwareCmdlet : OPNSenseBaseCmdlet + { + /// + /// Gets the firmware changelog. + /// + [Parameter(Mandatory = false, ParameterSetName = "Changelog")] + public SwitchParameter Changelog { get; set; } + + /// + /// Gets the firmware audit. + /// + [Parameter(Mandatory = false, ParameterSetName = "Audit")] + public SwitchParameter Audit { get; set; } + + /// + /// Gets the firmware health. + /// + [Parameter(Mandatory = false, ParameterSetName = "Health")] + public SwitchParameter Health { get; set; } + + /// + /// Checks for firmware updates. + /// + [Parameter(Mandatory = false, ParameterSetName = "Check")] + public SwitchParameter Check { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var firmwareService = new FirmwareService(ApiClient, Logger); + + switch (ParameterSetName) + { + case "Changelog": + var changelogTask = Task.Run(async () => await firmwareService.GetChangelogAsync()); + var changelogResult = changelogTask.GetAwaiter().GetResult(); + WriteObject(changelogResult.Changelog); + break; + + case "Audit": + var auditTask = Task.Run(async () => await firmwareService.GetAuditAsync()); + var auditResult = auditTask.GetAwaiter().GetResult(); + WriteObject(auditResult.Audit, true); + break; + + case "Health": + var healthTask = Task.Run(async () => await firmwareService.GetHealthAsync()); + var healthResult = healthTask.GetAwaiter().GetResult(); + WriteObject(healthResult.Health); + break; + + case "Check": + var checkTask = Task.Run(async () => await firmwareService.CheckForUpdatesAsync()); + var checkResult = checkTask.GetAwaiter().GetResult(); + WriteObject($"Check for updates: {checkResult.Status}"); + break; + + default: + var statusTask = Task.Run(async () => await firmwareService.GetStatusAsync()); + var statusResult = statusTask.GetAwaiter().GetResult(); + + var firmware = new PSObject(); + firmware.Properties.Add(new PSNoteProperty("Status", statusResult.Status)); + firmware.Properties.Add(new PSNoteProperty("Connection", statusResult.Connection)); + firmware.Properties.Add(new PSNoteProperty("DownloadSize", statusResult.DownloadSize)); + firmware.Properties.Add(new PSNoteProperty("LastCheck", statusResult.LastCheck)); + firmware.Properties.Add(new PSNoteProperty("UpgradeMessage", statusResult.UpgradeMessage)); + firmware.Properties.Add(new PSNoteProperty("Updates", statusResult.Updates)); + + WriteObject(firmware); + break; + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseGatewayCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseGatewayCmdlet.cs new file mode 100644 index 0000000..47c269f --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseGatewayCmdlet.cs @@ -0,0 +1,107 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Gets gateways from an OPNSense firewall. + /// The Get-OPNSenseGateway cmdlet retrieves gateways from an OPNSense firewall. + /// + /// Example 1: Get all gateways + /// Get-OPNSenseGateway + /// This example retrieves all gateways from the connected OPNSense firewall. + /// + /// + /// Example 2: Get a specific gateway by UUID + /// Get-OPNSenseGateway -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" + /// This example retrieves a specific gateway by its UUID. + /// + /// + [Cmdlet(VerbsCommon.Get, "OPNSenseGateway")] + [OutputType(typeof(Gateway), typeof(GatewayDetail))] + public class GetOPNSenseGatewayCmdlet : OPNSenseBaseCmdlet + { + /// + /// The UUID of the gateway to retrieve. + /// + [Parameter(Mandatory = false, Position = 0, ParameterSetName = "ByUuid")] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// Whether to include status information for the gateways. + /// + [Parameter(Mandatory = false)] + public SwitchParameter IncludeStatus { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var gatewayService = new GatewayService(ApiClient, Logger); + + if (ParameterSetName == "ByUuid") + { + var task = Task.Run(async () => await gatewayService.GetGatewayAsync(Uuid)); + var result = task.GetAwaiter().GetResult(); + WriteObject(result.Gateway); + } + else + { + var task = Task.Run(async () => await gatewayService.GetGatewaysAsync()); + var result = task.GetAwaiter().GetResult(); + + if (IncludeStatus.IsPresent) + { + var statusTask = Task.Run(async () => await gatewayService.GetGatewayStatusAsync()); + var statusResult = statusTask.GetAwaiter().GetResult(); + + foreach (var gateway in result.Rows) + { + var gatewayWithStatus = new PSObject(); + gatewayWithStatus.Properties.Add(new PSNoteProperty("Uuid", gateway.Uuid)); + gatewayWithStatus.Properties.Add(new PSNoteProperty("Name", gateway.Name)); + gatewayWithStatus.Properties.Add(new PSNoteProperty("Interface", gateway.Interface)); + gatewayWithStatus.Properties.Add(new PSNoteProperty("IpAddress", gateway.IpAddress)); + gatewayWithStatus.Properties.Add(new PSNoteProperty("MonitorIp", gateway.MonitorIp)); + gatewayWithStatus.Properties.Add(new PSNoteProperty("Description", gateway.Description)); + gatewayWithStatus.Properties.Add(new PSNoteProperty("IsDefault", gateway.IsDefault == "1")); + gatewayWithStatus.Properties.Add(new PSNoteProperty("Disabled", gateway.Disabled == "1")); + + if (statusResult.Items.ContainsKey(gateway.Name)) + { + var status = statusResult.Items[gateway.Name]; + gatewayWithStatus.Properties.Add(new PSNoteProperty("Status", status.Status)); + gatewayWithStatus.Properties.Add(new PSNoteProperty("RTT", status.RTT)); + gatewayWithStatus.Properties.Add(new PSNoteProperty("StdDev", status.StdDev)); + gatewayWithStatus.Properties.Add(new PSNoteProperty("Loss", status.Loss)); + } + else + { + gatewayWithStatus.Properties.Add(new PSNoteProperty("Status", "Unknown")); + gatewayWithStatus.Properties.Add(new PSNoteProperty("RTT", "N/A")); + gatewayWithStatus.Properties.Add(new PSNoteProperty("StdDev", "N/A")); + gatewayWithStatus.Properties.Add(new PSNoteProperty("Loss", "N/A")); + } + + WriteObject(gatewayWithStatus); + } + } + else + { + WriteObject(result.Rows, true); + } + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseInterfaceCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseInterfaceCmdlet.cs new file mode 100644 index 0000000..92a2099 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseInterfaceCmdlet.cs @@ -0,0 +1,61 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Gets interfaces from an OPNSense firewall. + /// The Get-OPNSenseInterface cmdlet retrieves interfaces from an OPNSense firewall. + /// + /// Example 1: Get all interfaces + /// Get-OPNSenseInterface + /// This example retrieves all interfaces from the connected OPNSense firewall. + /// + /// + /// Example 2: Get a specific interface by name + /// Get-OPNSenseInterface -Name "wan" + /// This example retrieves a specific interface by its name. + /// + /// + [Cmdlet(VerbsCommon.Get, "OPNSenseInterface")] + [OutputType(typeof(InterfaceInfo), typeof(InterfaceDetail))] + public class GetOPNSenseInterfaceCmdlet : OPNSenseBaseCmdlet + { + /// + /// The name of the interface to retrieve. + /// + [Parameter(Mandatory = false, Position = 0, ParameterSetName = "ByName")] + [ValidateNotNullOrEmpty] + public string Name { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var interfaceService = new InterfaceService(ApiClient, Logger); + + if (ParameterSetName == "ByName") + { + var task = Task.Run(async () => await interfaceService.GetInterfaceDetailAsync(Name)); + var result = task.GetAwaiter().GetResult(); + WriteObject(result.Interface); + } + else + { + var task = Task.Run(async () => await interfaceService.GetInterfacesAsync()); + var result = task.GetAwaiter().GetResult(); + WriteObject(result.Interfaces, true); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseInterfaceStatisticsCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseInterfaceStatisticsCmdlet.cs new file mode 100644 index 0000000..0145249 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseInterfaceStatisticsCmdlet.cs @@ -0,0 +1,59 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Gets interface statistics from an OPNSense firewall. + /// The Get-OPNSenseInterfaceStatistics cmdlet retrieves interface statistics from an OPNSense firewall. + /// + /// Example 1: Get all interface statistics + /// Get-OPNSenseInterfaceStatistics + /// This example retrieves statistics for all interfaces from the connected OPNSense firewall. + /// + /// + /// Example 2: Get statistics for a specific interface + /// Get-OPNSenseInterfaceStatistics | Where-Object { $_.Name -eq "wan" } + /// This example retrieves statistics for the WAN interface. + /// + /// + [Cmdlet(VerbsCommon.Get, "OPNSenseInterfaceStatistics")] + [OutputType(typeof(PSObject))] + public class GetOPNSenseInterfaceStatisticsCmdlet : OPNSenseBaseCmdlet + { + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var interfaceService = new InterfaceService(ApiClient, Logger); + + var task = Task.Run(async () => await interfaceService.GetInterfaceStatisticsAsync()); + var result = task.GetAwaiter().GetResult(); + + foreach (var kvp in result.Statistics) + { + var stats = new PSObject(); + stats.Properties.Add(new PSNoteProperty("Name", kvp.Key)); + stats.Properties.Add(new PSNoteProperty("InPackets", kvp.Value.InPackets)); + stats.Properties.Add(new PSNoteProperty("InBytes", kvp.Value.InBytes)); + stats.Properties.Add(new PSNoteProperty("InErrors", 0)); + stats.Properties.Add(new PSNoteProperty("OutPackets", kvp.Value.OutPackets)); + stats.Properties.Add(new PSNoteProperty("OutBytes", kvp.Value.OutBytes)); + stats.Properties.Add(new PSNoteProperty("OutErrors", 0)); + stats.Properties.Add(new PSNoteProperty("Collisions", 0)); + + WriteObject(stats); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/GetOPNSensePluginCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/GetOPNSensePluginCmdlet.cs new file mode 100644 index 0000000..a6e8b0e --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/GetOPNSensePluginCmdlet.cs @@ -0,0 +1,103 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Gets plugins from an OPNSense firewall. + /// The Get-OPNSensePlugin cmdlet retrieves plugins from an OPNSense firewall. + /// + /// Example 1: Get all installed plugins + /// Get-OPNSensePlugin -Installed + /// This example retrieves all installed plugins from the connected OPNSense firewall. + /// + /// + /// Example 2: Get all available plugins + /// Get-OPNSensePlugin -Available + /// This example retrieves all available plugins from the connected OPNSense firewall. + /// + /// + /// Example 3: Get all plugins + /// Get-OPNSensePlugin + /// This example retrieves all installed and available plugins from the connected OPNSense firewall. + /// + /// + [Cmdlet(VerbsCommon.Get, "OPNSensePlugin")] + [OutputType(typeof(PSObject))] + public class GetOPNSensePluginCmdlet : OPNSenseBaseCmdlet + { + /// + /// Gets only installed plugins. + /// + [Parameter(Mandatory = false, ParameterSetName = "Installed")] + public SwitchParameter Installed { get; set; } + + /// + /// Gets only available plugins. + /// + [Parameter(Mandatory = false, ParameterSetName = "Available")] + public SwitchParameter Available { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var pluginService = new PluginService(ApiClient, Logger); + + var task = Task.Run(async () => await pluginService.GetPluginsAsync()); + var result = task.GetAwaiter().GetResult(); + + if (ParameterSetName == "Installed" || ParameterSetName == "") + { + foreach (var kvp in result.Installed) + { + var plugin = new PSObject(); + plugin.Properties.Add(new PSNoteProperty("Name", kvp.Key)); + plugin.Properties.Add(new PSNoteProperty("Version", kvp.Value.Version)); + plugin.Properties.Add(new PSNoteProperty("Comment", kvp.Value.Comment)); + plugin.Properties.Add(new PSNoteProperty("Repository", kvp.Value.Repository)); + plugin.Properties.Add(new PSNoteProperty("Origin", kvp.Value.Origin)); + plugin.Properties.Add(new PSNoteProperty("License", kvp.Value.License)); + plugin.Properties.Add(new PSNoteProperty("FlatSize", kvp.Value.FlatSize)); + plugin.Properties.Add(new PSNoteProperty("Locked", kvp.Value.Locked == "1")); + plugin.Properties.Add(new PSNoteProperty("Enabled", kvp.Value.Enabled == "1")); + plugin.Properties.Add(new PSNoteProperty("Status", "Installed")); + + WriteObject(plugin); + } + } + + if (ParameterSetName == "Available" || ParameterSetName == "") + { + foreach (var kvp in result.Available) + { + // Skip if the plugin is already installed + if (result.Installed.ContainsKey(kvp.Key)) + continue; + + var plugin = new PSObject(); + plugin.Properties.Add(new PSNoteProperty("Name", kvp.Key)); + plugin.Properties.Add(new PSNoteProperty("Version", kvp.Value.Version)); + plugin.Properties.Add(new PSNoteProperty("Comment", kvp.Value.Comment)); + plugin.Properties.Add(new PSNoteProperty("Repository", kvp.Value.Repository)); + plugin.Properties.Add(new PSNoteProperty("Origin", kvp.Value.Origin)); + plugin.Properties.Add(new PSNoteProperty("License", kvp.Value.License)); + plugin.Properties.Add(new PSNoteProperty("FlatSize", kvp.Value.FlatSize)); + plugin.Properties.Add(new PSNoteProperty("Status", "Available")); + + WriteObject(plugin); + } + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/GetOPNSensePortForwardingRuleCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/GetOPNSensePortForwardingRuleCmdlet.cs new file mode 100644 index 0000000..5eedb00 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/GetOPNSensePortForwardingRuleCmdlet.cs @@ -0,0 +1,58 @@ +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Models; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Gets port forwarding rules from an OPNSense firewall. + /// The Get-OPNSensePortForwardingRule cmdlet gets port forwarding rules from an OPNSense firewall. + /// + /// Get all port forwarding rules + /// Get-OPNSensePortForwardingRule + /// + /// + /// Get a specific port forwarding rule by UUID + /// Get-OPNSensePortForwardingRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" + /// + /// + [Cmdlet(VerbsCommon.Get, "OPNSensePortForwardingRule")] + [OutputType(typeof(PortForwardingRule))] + public class GetOPNSensePortForwardingRuleCmdlet : OPNSenseCmdlet + { + /// + /// The UUID of the port forwarding rule to get. If not specified, all rules are returned. + /// + [Parameter(Position = 0, Mandatory = false)] + public string Uuid { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + var portForwardingService = new PortForwardingService(SessionState.ApiClient); + + if (!string.IsNullOrEmpty(Uuid)) + { + // Get a specific rule + var rule = Task.Run(async () => await portForwardingService.GetPortForwardingRuleAsync(Uuid)).GetAwaiter().GetResult(); + if (rule != null) + { + WriteObject(rule); + } + else + { + WriteWarning($"Port forwarding rule with UUID '{Uuid}' not found."); + } + } + else + { + // Get all rules + var rules = Task.Run(async () => await portForwardingService.GetPortForwardingRulesAsync()).GetAwaiter().GetResult(); + WriteObject(rules, true); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseRouteCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseRouteCmdlet.cs new file mode 100644 index 0000000..5241448 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseRouteCmdlet.cs @@ -0,0 +1,61 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Gets routes from an OPNSense firewall. + /// The Get-OPNSenseRoute cmdlet retrieves routes from an OPNSense firewall. + /// + /// Example 1: Get all routes + /// Get-OPNSenseRoute + /// This example retrieves all routes from the connected OPNSense firewall. + /// + /// + /// Example 2: Get a specific route by UUID + /// Get-OPNSenseRoute -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" + /// This example retrieves a specific route by its UUID. + /// + /// + [Cmdlet(VerbsCommon.Get, "OPNSenseRoute")] + [OutputType(typeof(Route), typeof(RouteDetail))] + public class GetOPNSenseRouteCmdlet : OPNSenseBaseCmdlet + { + /// + /// The UUID of the route to retrieve. + /// + [Parameter(Mandatory = false, Position = 0, ParameterSetName = "ByUuid")] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var routeService = new RouteService(ApiClient, Logger); + + if (ParameterSetName == "ByUuid") + { + var task = Task.Run(async () => await routeService.GetRouteAsync(Uuid)); + var result = task.GetAwaiter().GetResult(); + WriteObject(result.Route); + } + else + { + var task = Task.Run(async () => await routeService.GetRoutesAsync()); + var result = task.GetAwaiter().GetResult(); + WriteObject(result.Rows, true); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseSystemDNSCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseSystemDNSCmdlet.cs new file mode 100644 index 0000000..23f730d --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseSystemDNSCmdlet.cs @@ -0,0 +1,41 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Gets the system DNS configuration from an OPNSense firewall. + /// The Get-OPNSenseSystemDNS cmdlet retrieves the system DNS configuration from an OPNSense firewall. + /// + /// Example 1: Get system DNS configuration + /// Get-OPNSenseSystemDNS + /// This example retrieves the system DNS configuration from the connected OPNSense firewall. + /// + /// + [Cmdlet(VerbsCommon.Get, "OPNSenseSystemDNS")] + [OutputType(typeof(SystemDNSConfig))] + public class GetOPNSenseSystemDNSCmdlet : OPNSenseBaseCmdlet + { + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var systemDNSService = new SystemDNSService(ApiClient, Logger); + + var task = Task.Run(async () => await systemDNSService.GetSystemDNSAsync()); + var result = task.GetAwaiter().GetResult(); + + WriteObject(result.System); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseTailscaleStatusCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseTailscaleStatusCmdlet.cs new file mode 100644 index 0000000..a25f17b --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseTailscaleStatusCmdlet.cs @@ -0,0 +1,96 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Gets the status of Tailscale on an OPNSense firewall. + /// The Get-OPNSenseTailscaleStatus cmdlet retrieves the current status of Tailscale on an OPNSense firewall. + /// + /// Example 1: Get Tailscale status + /// Get-OPNSenseTailscaleStatus + /// This example retrieves the current status of Tailscale on the connected OPNSense firewall. + /// + /// + [Cmdlet(VerbsCommon.Get, "OPNSenseTailscaleStatus")] + [OutputType(typeof(PSObject))] + public class GetOPNSenseTailscaleStatusCmdlet : OPNSenseBaseCmdlet + { + /// + /// Whether to include detailed information about Tailscale interfaces. + /// + [Parameter(Mandatory = false)] + public SwitchParameter IncludeInterfaces { get; set; } + + /// + /// Whether to include Tailscale settings. + /// + [Parameter(Mandatory = false)] + public SwitchParameter IncludeSettings { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var tailscaleService = new TailscaleService(ApiClient, Logger); + + // Check if the plugin is installed + var isInstalledTask = Task.Run(async () => await tailscaleService.IsPluginInstalledAsync()); + var isInstalled = isInstalledTask.GetAwaiter().GetResult(); + + if (!isInstalled) + { + WriteWarning("Tailscale plugin is not installed on the OPNSense firewall."); + + var statusResult = new PSObject(); + statusResult.Properties.Add(new PSNoteProperty("PluginInstalled", false)); + statusResult.Properties.Add(new PSNoteProperty("Running", false)); + statusResult.Properties.Add(new PSNoteProperty("Enabled", false)); + statusResult.Properties.Add(new PSNoteProperty("Status", "Plugin not installed")); + + WriteObject(statusResult); + return; + } + + // Get the status + var statusTask = Task.Run(async () => await tailscaleService.GetStatusAsync()); + var status = statusTask.GetAwaiter().GetResult(); + + var result = new PSObject(); + result.Properties.Add(new PSNoteProperty("PluginInstalled", true)); + result.Properties.Add(new PSNoteProperty("Running", status.Running)); + result.Properties.Add(new PSNoteProperty("Enabled", status.Enabled)); + result.Properties.Add(new PSNoteProperty("Status", status.Status)); + + // Get interfaces if requested + if (IncludeInterfaces.IsPresent) + { + var interfacesTask = Task.Run(async () => await tailscaleService.GetInterfacesAsync()); + var interfaces = interfacesTask.GetAwaiter().GetResult(); + + result.Properties.Add(new PSNoteProperty("Interfaces", interfaces.Interfaces)); + } + + // Get settings if requested + if (IncludeSettings.IsPresent) + { + var settingsTask = Task.Run(async () => await tailscaleService.GetSettingsAsync()); + var settings = settingsTask.GetAwaiter().GetResult(); + + result.Properties.Add(new PSNoteProperty("Settings", settings.General)); + } + + WriteObject(result); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseUserCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseUserCmdlet.cs new file mode 100644 index 0000000..35cb2b9 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseUserCmdlet.cs @@ -0,0 +1,61 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Gets users from an OPNSense firewall. + /// The Get-OPNSenseUser cmdlet retrieves users from an OPNSense firewall. + /// + /// Example 1: Get all users + /// Get-OPNSenseUser + /// This example retrieves all users from the connected OPNSense firewall. + /// + /// + /// Example 2: Get a specific user by UUID + /// Get-OPNSenseUser -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" + /// This example retrieves a specific user by its UUID. + /// + /// + [Cmdlet(VerbsCommon.Get, "OPNSenseUser")] + [OutputType(typeof(User), typeof(UserDetail))] + public class GetOPNSenseUserCmdlet : OPNSenseBaseCmdlet + { + /// + /// The UUID of the user to retrieve. + /// + [Parameter(Mandatory = false, Position = 0, ParameterSetName = "ByUuid")] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var userService = new UserService(ApiClient, Logger); + + if (ParameterSetName == "ByUuid") + { + var task = Task.Run(async () => await userService.GetUserAsync(Uuid)); + var result = task.GetAwaiter().GetResult(); + WriteObject(result.User); + } + else + { + var task = Task.Run(async () => await userService.GetUsersAsync()); + var result = task.GetAwaiter().GetResult(); + WriteObject(result.Rows, true); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseVLANCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseVLANCmdlet.cs new file mode 100644 index 0000000..6006e2a --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseVLANCmdlet.cs @@ -0,0 +1,61 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Gets VLANs from an OPNSense firewall. + /// The Get-OPNSenseVLAN cmdlet retrieves VLANs from an OPNSense firewall. + /// + /// Example 1: Get all VLANs + /// Get-OPNSenseVLAN + /// This example retrieves all VLANs from the connected OPNSense firewall. + /// + /// + /// Example 2: Get a specific VLAN by UUID + /// Get-OPNSenseVLAN -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" + /// This example retrieves a specific VLAN by its UUID. + /// + /// + [Cmdlet(VerbsCommon.Get, "OPNSenseVLAN")] + [OutputType(typeof(VLAN), typeof(VLANDetail))] + public class GetOPNSenseVLANCmdlet : OPNSenseBaseCmdlet + { + /// + /// The UUID of the VLAN to retrieve. + /// + [Parameter(Mandatory = false, Position = 0, ParameterSetName = "ByUuid")] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var interfaceService = new InterfaceService(ApiClient, Logger); + + if (ParameterSetName == "ByUuid") + { + var task = Task.Run(async () => await interfaceService.GetVLANAsync(Uuid)); + var result = task.GetAwaiter().GetResult(); + WriteObject(result.Vlan); + } + else + { + var task = Task.Run(async () => await interfaceService.GetVLANsAsync()); + var result = task.GetAwaiter().GetResult(); + WriteObject(result.Rows, true); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/ImportOPNSenseConfigCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/ImportOPNSenseConfigCmdlet.cs new file mode 100644 index 0000000..2a9804e --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/ImportOPNSenseConfigCmdlet.cs @@ -0,0 +1,75 @@ +using System; +using System.IO; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Imports an OPNSense firewall configuration. + /// The Import-OPNSenseConfig cmdlet imports an OPNSense firewall configuration from a file. + /// + /// Example 1: Import a configuration from a file + /// Import-OPNSenseConfig -Path "C:\Backups\opnsense-config.xml" + /// This example imports an OPNSense firewall configuration from a file. + /// + /// + [Cmdlet(VerbsData.Import, "OPNSenseConfig", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + [OutputType(typeof(void))] + public class ImportOPNSenseConfigCmdlet : OPNSenseBaseCmdlet + { + /// + /// The path to the configuration file. + /// + [Parameter(Mandatory = true, Position = 0)] + [ValidateNotNullOrEmpty] + public string Path { get; set; } + + /// + /// Suppresses the confirmation prompt. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Force { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + // Check if the file exists + if (!File.Exists(Path)) + { + WriteError(new ErrorRecord( + new FileNotFoundException($"The file '{Path}' does not exist."), + "FileNotFound", + ErrorCategory.ObjectNotFound, + Path)); + return; + } + + if (!Force.IsPresent && !ShouldProcess(Path, "Import configuration")) + { + return; + } + + var configService = new ConfigService(ApiClient, Logger); + + // Read the configuration file + var configContent = File.ReadAllBytes(Path); + + var task = Task.Run(async () => await configService.ImportConfigAsync(configContent)); + var result = task.GetAwaiter().GetResult(); + + WriteVerbose($"Imported configuration from {Path}: {result.Status}"); + WriteWarning("The firewall is restarting. You may need to reconnect after it comes back online."); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/InstallOPNSensePluginCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/InstallOPNSensePluginCmdlet.cs new file mode 100644 index 0000000..8ad6fbd --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/InstallOPNSensePluginCmdlet.cs @@ -0,0 +1,121 @@ +using System; +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Installs a plugin on an OPNSense firewall. + /// The Install-OPNSensePlugin cmdlet installs a plugin on an OPNSense firewall. + /// + /// Example 1: Install a plugin + /// Install-OPNSensePlugin -Name "os-acme-client" + /// This example installs the ACME client plugin on the OPNSense firewall. + /// + /// + /// Example 2: Install a plugin and wait for completion + /// Install-OPNSensePlugin -Name "os-acme-client" -Wait + /// This example installs the ACME client plugin on the OPNSense firewall and waits for the installation to complete. + /// + /// + [Cmdlet(VerbsLifecycle.Install, "OPNSensePlugin", SupportsShouldProcess = true)] + [OutputType(typeof(void))] + public class InstallOPNSensePluginCmdlet : OPNSenseBaseCmdlet + { + /// + /// The name of the plugin to install. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string Name { get; set; } + + /// + /// Waits for the installation to complete. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Wait { get; set; } + + /// + /// The timeout in seconds to wait for the installation to complete. + /// + [Parameter(Mandatory = false)] + [ValidateRange(1, 3600)] + public int Timeout { get; set; } = 300; + + /// + /// The interval in seconds between status checks. + /// + [Parameter(Mandatory = false)] + [ValidateRange(1, 60)] + public int Interval { get; set; } = 5; + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + if (!ShouldProcess(Name, "Install plugin")) + { + return; + } + + var pluginService = new PluginService(ApiClient, Logger); + + var task = Task.Run(async () => await pluginService.InstallPluginAsync(Name)); + var result = task.GetAwaiter().GetResult(); + + WriteVerbose($"Plugin installation initiated: {result.Status}"); + WriteObject($"Plugin installation initiated: {result.Status}"); + + if (Wait.IsPresent) + { + WriteVerbose($"Waiting for plugin installation to complete (timeout: {Timeout} seconds, interval: {Interval} seconds)"); + WriteObject("Waiting for plugin installation to complete..."); + + DateTime startTime = DateTime.Now; + bool completed = false; + + while (DateTime.Now - startTime < TimeSpan.FromSeconds(Timeout)) + { + var statusTask = Task.Run(async () => await pluginService.GetPluginStatusAsync()); + var statusResult = statusTask.GetAwaiter().GetResult(); + + if (statusResult.Status == "done") + { + WriteVerbose("Plugin installation completed successfully"); + WriteObject("Plugin installation completed successfully"); + completed = true; + break; + } + else if (statusResult.Status == "error") + { + WriteError(new ErrorRecord( + new Exception($"Plugin installation failed: {statusResult.Log}"), + "PluginInstallationFailed", + ErrorCategory.InvalidOperation, + Name)); + completed = true; + break; + } + + WriteVerbose($"Plugin installation status: {statusResult.Status}"); + Thread.Sleep(Interval * 1000); + } + + if (!completed) + { + WriteWarning($"Timed out waiting for plugin installation to complete after {Timeout} seconds"); + } + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/InvokeOPNSenseNetworkCalculationCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/InvokeOPNSenseNetworkCalculationCmdlet.cs new file mode 100644 index 0000000..2f67113 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/InvokeOPNSenseNetworkCalculationCmdlet.cs @@ -0,0 +1,347 @@ +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Net; +using PSOPNSenseAPI.Utilities; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Performs network calculations using IPNetwork2. + /// The Invoke-OPNSenseNetworkCalculation cmdlet performs various network calculations using the IPNetwork2 library. + /// + /// Example 1: Get network information + /// Invoke-OPNSenseNetworkCalculation -Network "192.168.1.0/24" -Operation Info + /// This example returns detailed information about the 192.168.1.0/24 network. + /// + /// + /// Example 2: Divide a network into subnets + /// Invoke-OPNSenseNetworkCalculation -Network "10.0.0.0/16" -Operation Subnet -PrefixLength 24 + /// This example divides the 10.0.0.0/16 network into /24 subnets. + /// + /// + /// Example 3: Check if an IP address is within a network + /// Invoke-OPNSenseNetworkCalculation -Network "192.168.1.0/24" -Operation Contains -IPAddress "192.168.1.100" + /// This example checks if the IP address 192.168.1.100 is within the 192.168.1.0/24 network. + /// + /// + [Cmdlet(VerbsLifecycle.Invoke, "OPNSenseNetworkCalculation")] + [OutputType(typeof(PSObject), typeof(bool), typeof(IPNetwork2[]))] + public class InvokeOPNSenseNetworkCalculationCmdlet : PSCmdlet + { + /// + /// The network in CIDR notation to perform calculations on. + /// + [Parameter(Mandatory = true, Position = 0)] + [ValidateNotNullOrEmpty] + public string Network { get; set; } + + /// + /// The operation to perform. + /// + [Parameter(Mandatory = true, Position = 1)] + [ValidateSet("Info", "Subnet", "SubnetByCount", "Contains", "Overlaps", "Supernet", "SupernetSummarize")] + public string Operation { get; set; } + + /// + /// The prefix length for subnet operations. + /// + [Parameter(Mandatory = false)] + [ValidateRange(1, 128)] + public int? PrefixLength { get; set; } + + /// + /// The number of subnets for SubnetByCount operations. + /// + [Parameter(Mandatory = false)] + [ValidateRange(2, int.MaxValue)] + public int? SubnetCount { get; set; } + + /// + /// The IP address for Contains operations. + /// + [Parameter(Mandatory = false)] + public string IPAddress { get; set; } + + /// + /// The second network for Overlaps and Supernet operations. + /// + [Parameter(Mandatory = false)] + public string[] AdditionalNetworks { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + // Parse the network + IPNetwork2 network = NetworkUtility.ParseCIDR(Network); + + switch (Operation) + { + case "Info": + WriteNetworkInfo(network); + break; + + case "Subnet": + if (!PrefixLength.HasValue) + { + WriteError(new ErrorRecord( + new ArgumentException("PrefixLength is required for Subnet operations."), + "MissingPrefixLength", + ErrorCategory.InvalidArgument, + null)); + return; + } + + WriteSubnets(network, PrefixLength.Value); + break; + + case "SubnetByCount": + if (!SubnetCount.HasValue) + { + WriteError(new ErrorRecord( + new ArgumentException("SubnetCount is required for SubnetByCount operations."), + "MissingSubnetCount", + ErrorCategory.InvalidArgument, + null)); + return; + } + + WriteSubnetsByCount(network, SubnetCount.Value); + break; + + case "Contains": + if (string.IsNullOrEmpty(IPAddress)) + { + WriteError(new ErrorRecord( + new ArgumentException("IPAddress is required for Contains operations."), + "MissingIPAddress", + ErrorCategory.InvalidArgument, + null)); + return; + } + + WriteContains(network, IPAddress); + break; + + case "Overlaps": + if (AdditionalNetworks == null || AdditionalNetworks.Length == 0) + { + WriteError(new ErrorRecord( + new ArgumentException("AdditionalNetworks is required for Overlaps operations."), + "MissingAdditionalNetworks", + ErrorCategory.InvalidArgument, + null)); + return; + } + + WriteOverlaps(network, AdditionalNetworks); + break; + + case "Supernet": + if (AdditionalNetworks == null || AdditionalNetworks.Length == 0) + { + WriteError(new ErrorRecord( + new ArgumentException("AdditionalNetworks is required for Supernet operations."), + "MissingAdditionalNetworks", + ErrorCategory.InvalidArgument, + null)); + return; + } + + WriteSupernet(network, AdditionalNetworks); + break; + + case "SupernetSummarize": + if (AdditionalNetworks == null || AdditionalNetworks.Length == 0) + { + WriteError(new ErrorRecord( + new ArgumentException("AdditionalNetworks is required for SupernetSummarize operations."), + "MissingAdditionalNetworks", + ErrorCategory.InvalidArgument, + null)); + return; + } + + WriteSupernetSummarize(network, AdditionalNetworks); + break; + } + } + catch (Exception ex) + { + WriteError(new ErrorRecord( + ex, + "NetworkCalculationError", + ErrorCategory.InvalidOperation, + null)); + } + } + + private void WriteNetworkInfo(IPNetwork2 network) + { + var info = new PSObject(); + info.Properties.Add(new PSNoteProperty("CIDR", network.ToString())); + info.Properties.Add(new PSNoteProperty("Network", network.Network.ToString())); + info.Properties.Add(new PSNoteProperty("Netmask", network.Netmask.ToString())); + info.Properties.Add(new PSNoteProperty("Broadcast", network.Broadcast.ToString())); + info.Properties.Add(new PSNoteProperty("FirstUsableHost", NetworkUtility.GetFirstUsableHost(network).ToString())); + info.Properties.Add(new PSNoteProperty("LastUsableHost", NetworkUtility.GetLastUsableHost(network).ToString())); + info.Properties.Add(new PSNoteProperty("UsableHostCount", NetworkUtility.GetUsableHostCount(network).ToString())); + info.Properties.Add(new PSNoteProperty("TotalAddressCount", network.Total.ToString())); + info.Properties.Add(new PSNoteProperty("AddressFamily", network.AddressFamily.ToString())); + info.Properties.Add(new PSNoteProperty("PrefixLength", network.Cidr)); + info.Properties.Add(new PSNoteProperty("IsPrivate", NetworkUtility.IsPrivate(network.Network))); + + WriteObject(info); + } + + private void WriteSubnets(IPNetwork2 network, int prefixLength) + { + if (prefixLength <= network.Cidr) + { + WriteError(new ErrorRecord( + new ArgumentException($"Prefix length ({prefixLength}) must be greater than the network CIDR ({network.Cidr})."), + "InvalidPrefixLength", + ErrorCategory.InvalidArgument, + null)); + return; + } + + var subnets = NetworkUtility.Subnet(network, prefixLength); + WriteObject(subnets, true); + } + + private void WriteSubnetsByCount(IPNetwork2 network, int subnetCount) + { + try + { + var subnets = NetworkUtility.SubnetByCount(network, subnetCount); + WriteObject(subnets, true); + } + catch (ArgumentException ex) + { + WriteError(new ErrorRecord( + ex, + "InvalidSubnetCount", + ErrorCategory.InvalidArgument, + null)); + } + } + + private void WriteContains(IPNetwork2 network, string ipAddress) + { + try + { + // Check if the input is an IP address or a network + if (ipAddress.Contains("/")) + { + // It's a network + IPNetwork2 otherNetwork = NetworkUtility.ParseCIDR(ipAddress); + bool contains = NetworkUtility.Contains(network, otherNetwork); + WriteObject(contains); + } + else + { + // It's an IP address + System.Net.IPAddress ip = System.Net.IPAddress.Parse(ipAddress); + bool contains = NetworkUtility.Contains(network, ip); + WriteObject(contains); + } + } + catch (Exception ex) + { + WriteError(new ErrorRecord( + ex, + "InvalidIPAddress", + ErrorCategory.InvalidArgument, + null)); + } + } + + private void WriteOverlaps(IPNetwork2 network, string[] additionalNetworks) + { + try + { + var results = new List(); + + foreach (var additionalNetwork in additionalNetworks) + { + IPNetwork2 otherNetwork = NetworkUtility.ParseCIDR(additionalNetwork); + bool overlaps = NetworkUtility.Overlaps(network, otherNetwork); + + var result = new PSObject(); + result.Properties.Add(new PSNoteProperty("Network1", network.ToString())); + result.Properties.Add(new PSNoteProperty("Network2", otherNetwork.ToString())); + result.Properties.Add(new PSNoteProperty("Overlaps", overlaps)); + + results.Add(result); + } + + WriteObject(results, true); + } + catch (Exception ex) + { + WriteError(new ErrorRecord( + ex, + "InvalidAdditionalNetwork", + ErrorCategory.InvalidArgument, + null)); + } + } + + private void WriteSupernet(IPNetwork2 network, string[] additionalNetworks) + { + try + { + var networks = new List { network }; + + foreach (var additionalNetwork in additionalNetworks) + { + IPNetwork2 otherNetwork = NetworkUtility.ParseCIDR(additionalNetwork); + networks.Add(otherNetwork); + } + + IPNetwork2 supernet = NetworkUtility.Supernet(networks.ToArray()); + WriteObject(supernet); + } + catch (Exception ex) + { + WriteError(new ErrorRecord( + ex, + "SupernetError", + ErrorCategory.InvalidOperation, + null)); + } + } + + private void WriteSupernetSummarize(IPNetwork2 network, string[] additionalNetworks) + { + try + { + var networks = new List { network }; + + foreach (var additionalNetwork in additionalNetworks) + { + IPNetwork2 otherNetwork = NetworkUtility.ParseCIDR(additionalNetwork); + networks.Add(otherNetwork); + } + + List summarizedNetworks = NetworkUtility.SupernetSummarize(networks); + WriteObject(summarizedNetworks, true); + } + catch (Exception ex) + { + WriteError(new ErrorRecord( + ex, + "SupernetSummarizeError", + ErrorCategory.InvalidOperation, + null)); + } + } + } +} + + diff --git a/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseAliasCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseAliasCmdlet.cs new file mode 100644 index 0000000..d4eafde --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseAliasCmdlet.cs @@ -0,0 +1,148 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Creates a new alias on an OPNSense firewall. + /// The New-OPNSenseAlias cmdlet creates a new alias on an OPNSense firewall. + /// + /// Example 1: Create a new host alias + /// New-OPNSenseAlias -Name "WebServers" -Type host -Content "192.168.1.10,192.168.1.11" -Description "Web Servers" -Apply + /// This example creates a new host alias for web servers. + /// + /// + /// Example 2: Create a new network alias + /// New-OPNSenseAlias -Name "InternalNetworks" -Type network -Content "192.168.1.0/24,192.168.2.0/24" -Description "Internal Networks" -Apply + /// This example creates a new network alias for internal networks. + /// + /// + /// Example 3: Create a new port alias + /// New-OPNSenseAlias -Name "WebPorts" -Type port -Content "80,443" -Protocol TCP -Description "Web Ports" -Apply + /// This example creates a new port alias for web ports. + /// + /// + /// Example 4: Create a new URL alias + /// New-OPNSenseAlias -Name "BlockList" -Type url -Content "https://example.com/blocklist.txt" -UpdateFrequency 1 -Description "Block List" -Apply + /// This example creates a new URL alias for a block list that updates daily. + /// + /// + [Cmdlet(VerbsCommon.New, "OPNSenseAlias")] + [OutputType(typeof(string))] + public class NewOPNSenseAliasCmdlet : OPNSenseBaseCmdlet + { + /// + /// The name of the alias. + /// + [Parameter(Mandatory = true, Position = 0)] + [ValidateNotNullOrEmpty] + public string Name { get; set; } + + /// + /// The type of the alias. + /// + [Parameter(Mandatory = true, Position = 1)] + [ValidateSet("host", "network", "port", "url", "urltable", "geoip", "networkgroup", "mac", "interface", "dynipv6host", "internal", "external")] + public string Type { get; set; } + + /// + /// The content of the alias. For host/network/port types, use comma-separated values. For URL types, specify the URL. + /// + [Parameter(Mandatory = true, Position = 2)] + [ValidateNotNullOrEmpty] + public string Content { get; set; } + + /// + /// The description of the alias. + /// + [Parameter(Mandatory = false)] + public string Description { get; set; } + + /// + /// The protocol for port aliases (TCP, UDP, or TCP/UDP). + /// + [Parameter(Mandatory = false)] + [ValidateSet("TCP", "UDP", "TCP/UDP")] + public string Protocol { get; set; } + + /// + /// The update frequency for URL aliases (in days). + /// + [Parameter(Mandatory = false)] + [ValidateRange(1, 365)] + public int? UpdateFrequency { get; set; } + + /// + /// Whether to enable counters for the alias. + /// + [Parameter(Mandatory = false)] + public SwitchParameter EnableCounters { get; set; } + + /// + /// Whether the alias is disabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Disabled { get; set; } + + /// + /// Whether to apply the changes immediately. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Apply { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var aliasService = new AliasService(ApiClient, Logger); + + var alias = new AliasConfig + { + Name = Name, + Type = Type.ToLower(), + Content = Content, + Description = Description, + Enabled = Disabled.IsPresent ? "0" : "1", + Counters = EnableCounters.IsPresent ? "1" : "0" + }; + + // Set protocol for port aliases + if (Type.Equals("port", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(Protocol)) + { + alias.Protocol = Protocol.ToUpper(); + } + + // Set update frequency for URL aliases + if ((Type.Equals("url", StringComparison.OrdinalIgnoreCase) || Type.Equals("urltable", StringComparison.OrdinalIgnoreCase)) && UpdateFrequency.HasValue) + { + alias.UpdateFrequency = UpdateFrequency.Value.ToString(); + } + + var createTask = Task.Run(async () => await aliasService.CreateAliasAsync(alias)); + var createResult = createTask.GetAwaiter().GetResult(); + + WriteVerbose($"Created alias with UUID {createResult.Uuid}"); + + // Apply changes if requested + if (Apply.IsPresent) + { + var applyTask = Task.Run(async () => await aliasService.ReconfigureAliasesAsync()); + var applyResult = applyTask.GetAwaiter().GetResult(); + + WriteVerbose($"Alias changes applied: {applyResult.Status}"); + } + + WriteObject(createResult.Uuid); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseCronJobCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseCronJobCmdlet.cs new file mode 100644 index 0000000..59f0834 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseCronJobCmdlet.cs @@ -0,0 +1,125 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Creates a new cron job on an OPNSense firewall. + /// The New-OPNSenseCronJob cmdlet creates a new cron job on an OPNSense firewall. + /// + /// Example 1: Create a new cron job + /// New-OPNSenseCronJob -Description "Daily backup" -Command "/usr/local/bin/backup.sh" -Minutes "0" -Hours "2" -Days "*" -Months "*" -Weekdays "*" + /// This example creates a new cron job that runs a backup script at 2:00 AM every day. + /// + /// + /// Example 2: Create a new cron job that runs weekly + /// New-OPNSenseCronJob -Description "Weekly cleanup" -Command "/usr/local/bin/cleanup.sh" -Minutes "0" -Hours "3" -Days "*" -Months "*" -Weekdays "0" + /// This example creates a new cron job that runs a cleanup script at 3:00 AM every Sunday. + /// + /// + [Cmdlet(VerbsCommon.New, "OPNSenseCronJob")] + [OutputType(typeof(string))] + public class NewOPNSenseCronJobCmdlet : OPNSenseBaseCmdlet + { + /// + /// The description of the cron job. + /// + [Parameter(Mandatory = true, Position = 0)] + [ValidateNotNullOrEmpty] + public string Description { get; set; } + + /// + /// The command to run. + /// + [Parameter(Mandatory = true, Position = 1)] + [ValidateNotNullOrEmpty] + public string Command { get; set; } + + /// + /// The minutes part of the cron schedule (0-59, *, */5, etc.). + /// + [Parameter(Mandatory = false)] + public string Minutes { get; set; } = "*"; + + /// + /// The hours part of the cron schedule (0-23, *, */2, etc.). + /// + [Parameter(Mandatory = false)] + public string Hours { get; set; } = "*"; + + /// + /// The days part of the cron schedule (1-31, *, */2, etc.). + /// + [Parameter(Mandatory = false)] + public string Days { get; set; } = "*"; + + /// + /// The months part of the cron schedule (1-12, *, */2, etc.). + /// + [Parameter(Mandatory = false)] + public string Months { get; set; } = "*"; + + /// + /// The weekdays part of the cron schedule (0-6, *, etc.). + /// + [Parameter(Mandatory = false)] + public string Weekdays { get; set; } = "*"; + + /// + /// Whether the cron job is enabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Enabled { get; set; } = true; + + /// + /// Whether to apply the changes immediately. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Apply { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var cronService = new CronService(ApiClient, Logger); + + var job = new CronJobConfig + { + Description = Description, + Command = Command, + Minutes = Minutes, + Hours = Hours, + Days = Days, + Months = Months, + Weekdays = Weekdays, + Enabled = Enabled.IsPresent ? "1" : "0" + }; + + var createTask = Task.Run(async () => await cronService.CreateJobAsync(job)); + var createResult = createTask.GetAwaiter().GetResult(); + + WriteVerbose($"Created cron job with UUID {createResult.Uuid}"); + + // Apply the changes if requested + if (Apply.IsPresent) + { + var applyTask = Task.Run(async () => await cronService.ApplyChangesAsync()); + var applyResult = applyTask.GetAwaiter().GetResult(); + + WriteVerbose($"Cron changes applied: {applyResult.Status}"); + } + + WriteObject(createResult.Uuid); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseDHCPOptionCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseDHCPOptionCmdlet.cs new file mode 100644 index 0000000..80e4aaf --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseDHCPOptionCmdlet.cs @@ -0,0 +1,100 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Creates a new DHCP option on an OPNSense firewall. + /// The New-OPNSenseDHCPOption cmdlet creates a new DHCP option on an OPNSense firewall. + /// + /// Example 1: Create a new DHCP option + /// New-OPNSenseDHCPOption -Interface "lan" -Number 66 -Value "192.168.1.10" -Description "TFTP Server" -Apply + /// This example creates a new DHCP option for the TFTP server. + /// + /// + [Cmdlet(VerbsCommon.New, "OPNSenseDHCPOption")] + [OutputType(typeof(string))] + public class NewOPNSenseDHCPOptionCmdlet : OPNSenseBaseCmdlet + { + /// + /// The interface name. + /// + [Parameter(Mandatory = true, Position = 0)] + [ValidateNotNullOrEmpty] + public string Interface { get; set; } + + /// + /// The option number. + /// + [Parameter(Mandatory = true, Position = 1)] + [ValidateNotNullOrEmpty] + public string Number { get; set; } + + /// + /// The option value. + /// + [Parameter(Mandatory = true, Position = 2)] + [ValidateNotNullOrEmpty] + public string Value { get; set; } + + /// + /// The option type. + /// + [Parameter(Mandatory = false)] + [ValidateSet("string", "text", "boolean", "array")] + public string Type { get; set; } = "string"; + + /// + /// The option description. + /// + [Parameter(Mandatory = false)] + public string Description { get; set; } + + /// + /// Whether to apply the changes immediately. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Apply { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var dhcpService = new DHCPService(ApiClient, Logger); + + var option = new DHCPOptionConfig + { + Number = Number, + Value = Value, + Type = Type, + Description = Description + }; + + var createTask = Task.Run(async () => await dhcpService.CreateOptionAsync(Interface, option)); + var createResult = createTask.GetAwaiter().GetResult(); + + WriteVerbose($"Created DHCP option with UUID {createResult.Uuid}"); + + // Apply changes if requested + if (Apply.IsPresent) + { + var applyTask = Task.Run(async () => await dhcpService.ApplyChangesAsync()); + var applyResult = applyTask.GetAwaiter().GetResult(); + + WriteVerbose($"DHCP changes applied: {applyResult.Status}"); + } + + WriteObject(createResult.Uuid); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseDHCPStaticMappingCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseDHCPStaticMappingCmdlet.cs new file mode 100644 index 0000000..15a25c8 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseDHCPStaticMappingCmdlet.cs @@ -0,0 +1,106 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Creates a new DHCP static mapping on an OPNSense firewall. + /// The New-OPNSenseDHCPStaticMapping cmdlet creates a new DHCP static mapping on an OPNSense firewall. + /// + /// Example 1: Create a new static mapping + /// New-OPNSenseDHCPStaticMapping -Interface "lan" -MacAddress "00:11:22:33:44:55" -IpAddress "192.168.1.100" -Hostname "printer" -Description "Office Printer" + /// This example creates a new static mapping for a printer on the LAN interface. + /// + /// + [Cmdlet(VerbsCommon.New, "OPNSenseDHCPStaticMapping")] + [OutputType(typeof(string))] + public class NewOPNSenseDHCPStaticMappingCmdlet : OPNSenseBaseCmdlet + { + /// + /// The interface to create the static mapping for. + /// + [Parameter(Mandatory = true, Position = 0)] + [ValidateNotNullOrEmpty] + public string Interface { get; set; } + + /// + /// The MAC address of the static mapping. + /// + [Parameter(Mandatory = true, Position = 1)] + [ValidateNotNullOrEmpty] + public string MacAddress { get; set; } + + /// + /// The IP address of the static mapping. + /// + [Parameter(Mandatory = true, Position = 2)] + [ValidateNotNullOrEmpty] + public string IpAddress { get; set; } + + /// + /// The hostname of the static mapping. + /// + [Parameter(Mandatory = false)] + public string Hostname { get; set; } + + /// + /// The description of the static mapping. + /// + [Parameter(Mandatory = false)] + public string Description { get; set; } + + /// + /// Whether the static mapping is enabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Enabled { get; set; } = true; + + /// + /// Whether to apply the changes immediately. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Apply { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var dhcpService = new DHCPService(ApiClient, Logger); + + var mapping = new DHCPStaticMappingConfig + { + MacAddress = MacAddress, + IpAddress = IpAddress, + Hostname = Hostname, + Description = Description, + Enabled = Enabled.IsPresent ? "1" : "0" + }; + + var createTask = Task.Run(async () => await dhcpService.CreateStaticMappingAsync(Interface, mapping)); + var createResult = createTask.GetAwaiter().GetResult(); + + WriteVerbose($"Created static mapping with UUID {createResult.Uuid}"); + + // Apply the changes if requested + if (Apply.IsPresent) + { + var applyTask = Task.Run(async () => await dhcpService.ApplyChangesAsync()); + var applyResult = applyTask.GetAwaiter().GetResult(); + + WriteVerbose($"DHCP changes applied: {applyResult.Status}"); + } + + WriteObject(createResult.Uuid); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseDNSForwardingHostCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseDNSForwardingHostCmdlet.cs new file mode 100644 index 0000000..1f35a97 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseDNSForwardingHostCmdlet.cs @@ -0,0 +1,92 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Creates a new DNS forwarding host on an OPNSense firewall. + /// The New-OPNSenseDNSForwardingHost cmdlet creates a new DNS forwarding host on an OPNSense firewall. + /// + /// Example 1: Create a new DNS forwarding host + /// New-OPNSenseDNSForwardingHost -Domain "example.com" -Server "192.168.1.10" -Description "Internal DNS Server" -Apply + /// This example creates a new DNS forwarding host for example.com that forwards to 192.168.1.10. + /// + /// + [Cmdlet(VerbsCommon.New, "OPNSenseDNSForwardingHost")] + [OutputType(typeof(string))] + public class NewOPNSenseDNSForwardingHostCmdlet : OPNSenseBaseCmdlet + { + /// + /// The domain to forward. + /// + [Parameter(Mandatory = true, Position = 0)] + [ValidateNotNullOrEmpty] + public string Domain { get; set; } + + /// + /// The DNS server to forward to. + /// + [Parameter(Mandatory = true, Position = 1)] + [ValidateNotNullOrEmpty] + public string Server { get; set; } + + /// + /// The description of the forwarding host. + /// + [Parameter(Mandatory = false)] + public string Description { get; set; } + + /// + /// Whether the forwarding host is enabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Enabled { get; set; } = true; + + /// + /// Whether to apply the changes immediately. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Apply { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var dnsService = new DNSService(ApiClient, Logger); + + var host = new DNSForwardingHostConfig + { + Domain = Domain, + Server = Server, + Description = Description, + Enabled = Enabled.IsPresent ? "1" : "0" + }; + + var createTask = Task.Run(async () => await dnsService.CreateDNSForwardingHostAsync(host)); + var createResult = createTask.GetAwaiter().GetResult(); + + WriteVerbose($"Created DNS forwarding host with UUID {createResult.Uuid}"); + + // Apply changes if requested + if (Apply.IsPresent) + { + var applyTask = Task.Run(async () => await dnsService.ApplyDNSChangesAsync()); + var applyResult = applyTask.GetAwaiter().GetResult(); + + WriteVerbose($"DNS changes applied: {applyResult.Status}"); + } + + WriteObject(createResult.Uuid); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseDNSOverrideCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseDNSOverrideCmdlet.cs new file mode 100644 index 0000000..9b55da8 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseDNSOverrideCmdlet.cs @@ -0,0 +1,100 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Creates a new DNS override on an OPNSense firewall. + /// The New-OPNSenseDNSOverride cmdlet creates a new DNS override on an OPNSense firewall. + /// + /// Example 1: Create a new DNS override + /// New-OPNSenseDNSOverride -Hostname "server" -Domain "example.com" -IpAddress "192.168.1.10" -Description "Internal server" + /// This example creates a new DNS override for server.example.com pointing to 192.168.1.10. + /// + /// + [Cmdlet(VerbsCommon.New, "OPNSenseDNSOverride")] + [OutputType(typeof(string))] + public class NewOPNSenseDNSOverrideCmdlet : OPNSenseBaseCmdlet + { + /// + /// The hostname part of the DNS override. + /// + [Parameter(Mandatory = true, Position = 0)] + [ValidateNotNullOrEmpty] + public string Hostname { get; set; } + + /// + /// The domain part of the DNS override. + /// + [Parameter(Mandatory = true, Position = 1)] + [ValidateNotNullOrEmpty] + public string Domain { get; set; } + + /// + /// The IP address to resolve to. + /// + [Parameter(Mandatory = true, Position = 2)] + [ValidateNotNullOrEmpty] + public string IpAddress { get; set; } + + /// + /// The description of the DNS override. + /// + [Parameter(Mandatory = false)] + public string Description { get; set; } + + /// + /// Whether the DNS override is enabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Enabled { get; set; } = true; + + /// + /// Whether to apply the changes immediately. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Apply { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var dnsService = new DNSService(ApiClient, Logger); + + var dnsOverride = new DNSOverrideConfig + { + Hostname = Hostname, + Domain = Domain, + IpAddress = IpAddress, + Description = Description, + Enabled = Enabled.IsPresent ? "1" : "0" + }; + + var createTask = Task.Run(async () => await dnsService.CreateDNSOverrideAsync(dnsOverride)); + var createResult = createTask.GetAwaiter().GetResult(); + + WriteVerbose($"Created DNS override with UUID {createResult.Uuid}"); + + // Apply the changes if requested + if (Apply.IsPresent) + { + var applyTask = Task.Run(async () => await dnsService.ApplyDNSChangesAsync()); + var applyResult = applyTask.GetAwaiter().GetResult(); + + WriteVerbose($"DNS changes applied: {applyResult.Status}"); + } + + WriteObject(createResult.Uuid); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseFirewallRuleCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseFirewallRuleCmdlet.cs new file mode 100644 index 0000000..58dea0a --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseFirewallRuleCmdlet.cs @@ -0,0 +1,117 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Creates a new firewall rule on an OPNSense firewall. + /// The New-OPNSenseFirewallRule cmdlet creates a new firewall rule on an OPNSense firewall. + /// + /// Example 1: Create a simple firewall rule + /// New-OPNSenseFirewallRule -Description "Allow HTTP" -Protocol TCP -DestinationPort 80 -Action pass + /// This example creates a new firewall rule that allows HTTP traffic. + /// + /// + /// Example 2: Create a more specific firewall rule + /// New-OPNSenseFirewallRule -Description "Allow HTTP from LAN" -Protocol TCP -SourceNet "192.168.1.0/24" -DestinationPort 80 -Action pass + /// This example creates a new firewall rule that allows HTTP traffic from the LAN network. + /// + /// + [Cmdlet(VerbsCommon.New, "OPNSenseFirewallRule")] + [OutputType(typeof(string))] + public class NewOPNSenseFirewallRuleCmdlet : OPNSenseBaseCmdlet + { + /// + /// The description of the firewall rule. + /// + [Parameter(Mandatory = true, Position = 0)] + [ValidateNotNullOrEmpty] + public string Description { get; set; } + + /// + /// The protocol of the firewall rule. + /// + [Parameter(Mandatory = false)] + [ValidateSet("any", "TCP", "UDP", "ICMP", "ESP", "AH", "GRE", "IGMP", "PIM", "OSPF")] + public string Protocol { get; set; } = "any"; + + /// + /// The source network of the firewall rule. + /// + [Parameter(Mandatory = false)] + public string SourceNet { get; set; } = "any"; + + /// + /// The source port of the firewall rule. + /// + [Parameter(Mandatory = false)] + public string SourcePort { get; set; } = "any"; + + /// + /// The destination network of the firewall rule. + /// + [Parameter(Mandatory = false)] + public string DestinationNet { get; set; } = "any"; + + /// + /// The destination port of the firewall rule. + /// + [Parameter(Mandatory = false)] + public string DestinationPort { get; set; } = "any"; + + /// + /// The action of the firewall rule. + /// + [Parameter(Mandatory = false)] + [ValidateSet("pass", "block", "reject")] + public string Action { get; set; } = "pass"; + + /// + /// The sequence of the firewall rule. + /// + [Parameter(Mandatory = false)] + public string Sequence { get; set; } + + /// + /// Whether the firewall rule is enabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Enabled { get; set; } = true; + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var firewallService = new FirewallService(ApiClient, Logger); + + var rule = new FirewallRuleCreate + { + Description = Description, + Protocol = Protocol, + SourceNet = SourceNet, + SourcePort = SourcePort, + DestinationNet = DestinationNet, + DestinationPort = DestinationPort, + Action = Action, + Sequence = Sequence, + Enabled = Enabled.IsPresent ? "1" : "0" + }; + + var task = Task.Run(async () => await firewallService.CreateRuleAsync(rule)); + var result = task.GetAwaiter().GetResult(); + + WriteVerbose($"Created firewall rule with UUID {result.Uuid}"); + WriteObject(result.Uuid); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseGatewayCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseGatewayCmdlet.cs new file mode 100644 index 0000000..35a37cd --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseGatewayCmdlet.cs @@ -0,0 +1,149 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Creates a new gateway on an OPNSense firewall. + /// The New-OPNSenseGateway cmdlet creates a new gateway on an OPNSense firewall. + /// + /// Example 1: Create a new gateway + /// New-OPNSenseGateway -Name "WAN2" -Interface "opt1" -IpAddress "203.0.113.1" -Description "Secondary WAN" -Apply + /// This example creates a new gateway for a secondary WAN interface. + /// + /// + /// Example 2: Create a new default gateway + /// New-OPNSenseGateway -Name "WAN_MAIN" -Interface "wan" -IpAddress "203.0.113.1" -Description "Main WAN" -Default -Apply + /// This example creates a new default gateway for the WAN interface. + /// + /// + [Cmdlet(VerbsCommon.New, "OPNSenseGateway")] + [OutputType(typeof(string))] + public class NewOPNSenseGatewayCmdlet : OPNSenseBaseCmdlet + { + /// + /// The name of the gateway. + /// + [Parameter(Mandatory = true, Position = 0)] + [ValidateNotNullOrEmpty] + public string Name { get; set; } + + /// + /// The interface of the gateway. + /// + [Parameter(Mandatory = true, Position = 1)] + [ValidateNotNullOrEmpty] + public string Interface { get; set; } + + /// + /// The IP address of the gateway. + /// + [Parameter(Mandatory = true, Position = 2)] + [ValidateNotNullOrEmpty] + public string IpAddress { get; set; } + + /// + /// The monitor IP of the gateway. + /// + [Parameter(Mandatory = false)] + public string MonitorIp { get; set; } + + /// + /// The description of the gateway. + /// + [Parameter(Mandatory = false)] + public string Description { get; set; } + + /// + /// Whether the gateway is the default gateway. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Default { get; set; } + + /// + /// Whether the gateway is disabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Disabled { get; set; } + + /// + /// The weight of the gateway. + /// + [Parameter(Mandatory = false)] + [ValidateRange(1, 30)] + public int? Weight { get; set; } + + /// + /// The IP protocol of the gateway. + /// + [Parameter(Mandatory = false)] + [ValidateSet("inet", "inet6")] + public string IpProtocol { get; set; } = "inet"; + + /// + /// Whether to disable monitoring for the gateway. + /// + [Parameter(Mandatory = false)] + public SwitchParameter DisableMonitoring { get; set; } + + /// + /// Whether to force the gateway down. + /// + [Parameter(Mandatory = false)] + public SwitchParameter ForceDown { get; set; } + + /// + /// Whether to apply the changes immediately. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Apply { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var gatewayService = new GatewayService(ApiClient, Logger); + + var gateway = new GatewayConfig + { + Name = Name, + Interface = Interface, + IpAddress = IpAddress, + MonitorIp = MonitorIp ?? IpAddress, + Description = Description, + IsDefault = Default.IsPresent ? "1" : "0", + Disabled = Disabled.IsPresent ? "1" : "0", + Weight = Weight?.ToString() ?? "1", + IpProtocol = IpProtocol, + MonitorDisable = DisableMonitoring.IsPresent ? "1" : "0", + ForceDown = ForceDown.IsPresent ? "1" : "0" + }; + + var createTask = Task.Run(async () => await gatewayService.CreateGatewayAsync(gateway)); + var createResult = createTask.GetAwaiter().GetResult(); + + WriteVerbose($"Created gateway with UUID {createResult.Uuid}"); + + // Apply changes if requested + if (Apply.IsPresent) + { + var applyTask = Task.Run(async () => await gatewayService.ApplyGatewayChangesAsync()); + var applyResult = applyTask.GetAwaiter().GetResult(); + + WriteVerbose($"Gateway changes applied: {applyResult.Status}"); + } + + WriteObject(createResult.Uuid); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/NewOPNSensePortForwardingRuleCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/NewOPNSensePortForwardingRuleCmdlet.cs new file mode 100644 index 0000000..df4e61f --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/NewOPNSensePortForwardingRuleCmdlet.cs @@ -0,0 +1,150 @@ +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Models; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Creates a new port forwarding rule on an OPNSense firewall. + /// The New-OPNSensePortForwardingRule cmdlet creates a new port forwarding rule on an OPNSense firewall. + /// + /// Create a new port forwarding rule + /// New-OPNSensePortForwardingRule -Interface "WAN" -Protocol "tcp" -DestinationPort "80" -TargetIP "192.168.1.100" -TargetPort "80" -Description "Web Server" + /// + /// + [Cmdlet(VerbsCommon.New, "OPNSensePortForwardingRule", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Medium)] + [OutputType(typeof(PortForwardingRule))] + public class NewOPNSensePortForwardingRuleCmdlet : OPNSenseCmdlet + { + /// + /// Whether the rule is enabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Enabled { get; set; } = true; + + /// + /// The description of the rule. + /// + [Parameter(Mandatory = false)] + public string Description { get; set; } + + /// + /// The interface for the rule. + /// + [Parameter(Mandatory = true)] + public string Interface { get; set; } + + /// + /// The TCP/IP protocol (tcp, udp, tcp/udp). + /// + [Parameter(Mandatory = false)] + [ValidateSet("tcp", "udp", "tcp/udp")] + public string Protocol { get; set; } = "tcp"; + + /// + /// The source address for the rule. + /// + [Parameter(Mandatory = false)] + public string Source { get; set; } = "any"; + + /// + /// The source port range for the rule. + /// + [Parameter(Mandatory = false)] + public string SourcePort { get; set; } = "any"; + + /// + /// The destination address for the rule. + /// + [Parameter(Mandatory = false)] + public string Destination { get; set; } = "wanip"; + + /// + /// The destination port range for the rule. + /// + [Parameter(Mandatory = true)] + public string DestinationPort { get; set; } + + /// + /// The target IP address for the rule. + /// + [Parameter(Mandatory = true)] + public string TargetIP { get; set; } + + /// + /// The target port for the rule. + /// + [Parameter(Mandatory = true)] + public string TargetPort { get; set; } + + /// + /// The NAT reflection mode (enable, disable, purenat). + /// + [Parameter(Mandatory = false)] + [ValidateSet("enable", "disable", "purenat")] + public string NatReflection { get; set; } = "enable"; + + /// + /// Whether to create an associated filter rule. + /// + [Parameter(Mandatory = false)] + public SwitchParameter FilterRuleAssociation { get; set; } = true; + + /// + /// Whether to enable logging for this rule. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Log { get; set; } = false; + + /// + /// Suppresses the confirmation prompt. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Force { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + var rule = new PortForwardingRule + { + Enabled = Enabled, + Description = Description, + Interface = Interface, + Protocol = Protocol, + Source = Source, + SourcePort = SourcePort, + Destination = Destination, + DestinationPort = DestinationPort, + TargetIP = TargetIP, + TargetPort = TargetPort, + NatReflection = NatReflection, + FilterRuleAssociation = FilterRuleAssociation, + Log = Log + }; + + if (Force || ShouldProcess($"OPNSense firewall", $"Create port forwarding rule from {Destination}:{DestinationPort} to {TargetIP}:{TargetPort}")) + { + var portForwardingService = new PortForwardingService(SessionState.ApiClient); + var uuid = Task.Run(async () => await portForwardingService.CreatePortForwardingRuleAsync(rule)).GetAwaiter().GetResult(); + + if (!string.IsNullOrEmpty(uuid)) + { + rule.Uuid = uuid; + WriteObject(rule); + WriteVerbose($"Port forwarding rule created successfully with UUID: {uuid}"); + } + else + { + WriteError(new ErrorRecord( + new PSInvalidOperationException("Failed to create port forwarding rule."), + "PortForwardingRuleCreationFailed", + ErrorCategory.InvalidOperation, + null)); + } + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseRouteCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseRouteCmdlet.cs new file mode 100644 index 0000000..5be8585 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseRouteCmdlet.cs @@ -0,0 +1,92 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Creates a new route on an OPNSense firewall. + /// The New-OPNSenseRoute cmdlet creates a new route on an OPNSense firewall. + /// + /// Example 1: Create a new route + /// New-OPNSenseRoute -Network "192.168.100.0/24" -Gateway "WAN_GW" -Description "Remote Office" -Apply + /// This example creates a new route to a remote office network. + /// + /// + [Cmdlet(VerbsCommon.New, "OPNSenseRoute")] + [OutputType(typeof(string))] + public class NewOPNSenseRouteCmdlet : OPNSenseBaseCmdlet + { + /// + /// The network of the route. + /// + [Parameter(Mandatory = true, Position = 0)] + [ValidateNotNullOrEmpty] + public string Network { get; set; } + + /// + /// The gateway of the route. + /// + [Parameter(Mandatory = true, Position = 1)] + [ValidateNotNullOrEmpty] + public string Gateway { get; set; } + + /// + /// The description of the route. + /// + [Parameter(Mandatory = false)] + public string Description { get; set; } + + /// + /// Whether the route is disabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Disabled { get; set; } + + /// + /// Whether to apply the changes immediately. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Apply { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var routeService = new RouteService(ApiClient, Logger); + + var route = new RouteConfig + { + Network = Network, + Gateway = Gateway, + Description = Description, + Disabled = Disabled.IsPresent ? "1" : "0" + }; + + var createTask = Task.Run(async () => await routeService.CreateRouteAsync(route)); + var createResult = createTask.GetAwaiter().GetResult(); + + WriteVerbose($"Created route with UUID {createResult.Uuid}"); + + // Apply changes if requested + if (Apply.IsPresent) + { + var applyTask = Task.Run(async () => await routeService.ApplyRouteChangesAsync()); + var applyResult = applyTask.GetAwaiter().GetResult(); + + WriteVerbose($"Route changes applied: {applyResult.Status}"); + } + + WriteObject(createResult.Uuid); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseSubnetVLANsCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseSubnetVLANsCmdlet.cs new file mode 100644 index 0000000..c4f4c80 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseSubnetVLANsCmdlet.cs @@ -0,0 +1,358 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation; +using System.Net; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Creates VLANs for subnets on an OPNSense firewall. + /// The New-OPNSenseSubnetVLANs cmdlet divides a CIDR network into subnets and creates VLANs for each subnet on an OPNSense firewall. + /// + /// Example 1: Create VLANs for subnets with sequential VLAN IDs + /// New-OPNSenseSubnetVLANs -ParentInterface "em0" -Network "192.168.0.0/24" -SubnetMaskBits 27 -StartingVlanId 10 -VlanIdIncrement 1 + /// This example divides the 192.168.0.0/24 network into /27 subnets and creates VLANs with IDs 10, 11, 12, etc. + /// + /// + /// Example 2: Create VLANs for subnets with VLAN IDs in multiples of 10 + /// New-OPNSenseSubnetVLANs -ParentInterface "em0" -Network "10.0.0.0/16" -SubnetMaskBits 24 -StartingVlanId 10 -VlanIdIncrement 10 + /// This example divides the 10.0.0.0/16 network into /24 subnets and creates VLANs with IDs 10, 20, 30, etc. + /// + /// + /// Example 3: Create VLANs for subnets with DHCP enabled + /// New-OPNSenseSubnetVLANs -ParentInterface "em0" -Network "172.16.0.0/20" -SubnetMaskBits 24 -StartingVlanId 100 -VlanIdIncrement 1 -EnableDHCP + /// This example divides the 172.16.0.0/20 network into /24 subnets, creates VLANs with IDs 100, 101, 102, etc., and enables DHCP on each VLAN interface. + /// + /// + [Cmdlet(VerbsCommon.New, "OPNSenseSubnetVLANs", SupportsShouldProcess = true)] + [OutputType(typeof(PSObject))] + public class NewOPNSenseSubnetVLANsCmdlet : OPNSenseBaseCmdlet + { + /// + /// The parent interface for the VLANs. + /// + [Parameter(Mandatory = true, Position = 0)] + [ValidateNotNullOrEmpty] + public string ParentInterface { get; set; } + + /// + /// The network in CIDR notation to divide into subnets. + /// + [Parameter(Mandatory = true, Position = 1)] + [ValidateNotNullOrEmpty] + public string Network { get; set; } + + /// + /// The subnet mask bits for the subnets. + /// + [Parameter(Mandatory = true, Position = 2)] + [ValidateRange(1, 32)] + public int SubnetMaskBits { get; set; } + + /// + /// The starting VLAN ID. + /// + [Parameter(Mandatory = true, Position = 3)] + [ValidateRange(1, 4094)] + public int StartingVlanId { get; set; } + + /// + /// The increment for VLAN IDs. + /// + [Parameter(Mandatory = true, Position = 4)] + [ValidateRange(1, 4094)] + public int VlanIdIncrement { get; set; } + + /// + /// Whether to enable DHCP on the VLAN interfaces. + /// + [Parameter(Mandatory = false)] + public SwitchParameter EnableDHCP { get; set; } + + /// + /// The domain name for DHCP clients. + /// + [Parameter(Mandatory = false)] + public string Domain { get; set; } = "local"; + + /// + /// The DNS servers for DHCP clients. If not specified, the gateway IP will be used. + /// + [Parameter(Mandatory = false)] + public string[] DnsServers { get; set; } + + /// + /// The description prefix for the VLANs. + /// + [Parameter(Mandatory = false)] + public string DescriptionPrefix { get; set; } = "VLAN"; + + /// + /// Suppresses the confirmation prompt. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Force { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + // Parse the network CIDR + var ipNetwork = IPNetwork2.Parse(Network); + + // Validate subnet mask bits + if (SubnetMaskBits <= ipNetwork.Cidr) + { + WriteError(new ErrorRecord( + new ArgumentException($"Subnet mask bits ({SubnetMaskBits}) must be greater than the network CIDR ({ipNetwork.Cidr})."), + "InvalidSubnetMaskBits", + ErrorCategory.InvalidArgument, + SubnetMaskBits)); + return; + } + + // Calculate the number of subnets + int numSubnets = (int)Math.Pow(2, SubnetMaskBits - ipNetwork.Cidr); + + // Calculate the maximum VLAN ID + int maxVlanId = StartingVlanId + (numSubnets - 1) * VlanIdIncrement; + + // Validate the maximum VLAN ID + if (maxVlanId > 4094) + { + WriteError(new ErrorRecord( + new ArgumentException($"The maximum VLAN ID ({maxVlanId}) exceeds the maximum allowed value (4094)."), + "InvalidVlanIdRange", + ErrorCategory.InvalidArgument, + maxVlanId)); + return; + } + + // Get confirmation + if (!Force.IsPresent && !ShouldProcess($"Create {numSubnets} VLANs for subnets of {Network} with VLAN IDs {StartingVlanId}-{maxVlanId}", "New-OPNSenseSubnetVLANs")) + { + return; + } + + // Get the interface service + var interfaceService = new InterfaceService(ApiClient, Logger); + + // Verify the parent interface exists + var getInterfaceTask = Task.Run(async () => await interfaceService.GetInterfaceDetailAsync(ParentInterface)); + getInterfaceTask.GetAwaiter().GetResult(); + + // Get existing VLANs + var getVlansTask = Task.Run(async () => await interfaceService.GetVLANsAsync()); + var existingVlans = getVlansTask.GetAwaiter().GetResult().Rows; + + // Create a list to store the created VLANs + var createdVlans = new List(); + + // Get DHCP service if needed + DHCPService dhcpService = null; + if (EnableDHCP.IsPresent) + { + dhcpService = new DHCPService(ApiClient, Logger); + } + + // Divide the network into subnets + var subnets = ipNetwork.Subnet((byte)SubnetMaskBits); + int vlanId = StartingVlanId; + int subnetIndex = 0; + + foreach (var subnet in subnets) + { + // Calculate the gateway IP (first host address) + var gatewayIp = GetFirstHostAddress(subnet); + + // Calculate the DHCP range (second host address to last host address) + var dhcpStart = GetSecondHostAddress(subnet); + var dhcpEnd = GetLastHostAddress(subnet); + + // Create a description for the VLAN + string description = $"{DescriptionPrefix} {vlanId} - {subnet}"; + + // Check if the VLAN already exists + var existingVlan = existingVlans.FirstOrDefault(v => + v.Interface == ParentInterface && + int.Parse(v.Tag) == vlanId); + + if (existingVlan != null) + { + WriteVerbose($"VLAN {vlanId} already exists on interface {ParentInterface}"); + + // Add the existing VLAN to the list + var vlanInfo = new PSObject(); + vlanInfo.Properties.Add(new PSNoteProperty("VlanId", vlanId)); + vlanInfo.Properties.Add(new PSNoteProperty("Subnet", subnet.ToString())); + vlanInfo.Properties.Add(new PSNoteProperty("Gateway", gatewayIp.ToString())); + vlanInfo.Properties.Add(new PSNoteProperty("Uuid", existingVlan.Uuid)); + vlanInfo.Properties.Add(new PSNoteProperty("Status", "Existing")); + + createdVlans.Add(vlanInfo); + } + else + { + // Create the VLAN + var vlanConfig = new VLANConfig + { + Interface = ParentInterface, + Tag = vlanId.ToString(), + Priority = "0", + Description = description + }; + + var createVlanTask = Task.Run(async () => await interfaceService.CreateVLANAsync(vlanConfig)); + var createVlanResult = createVlanTask.GetAwaiter().GetResult(); + + WriteVerbose($"Created VLAN {vlanId} on interface {ParentInterface} with UUID {createVlanResult.Uuid}"); + + // Configure the VLAN interface + string vlanInterfaceName = $"{ParentInterface}.{vlanId}"; + + var interfaceConfig = new InterfaceConfig + { + Description = description, + IpAddress = gatewayIp.ToString(), + SubnetMask = SubnetMaskBits.ToString(), + Enabled = "1" + }; + + var updateInterfaceTask = Task.Run(async () => await interfaceService.UpdateInterfaceAsync(vlanInterfaceName, interfaceConfig)); + updateInterfaceTask.GetAwaiter().GetResult(); + + WriteVerbose($"Configured interface {vlanInterfaceName} with IP {gatewayIp}/{SubnetMaskBits}"); + + // If EnableDHCP is specified, configure DHCP for the interface + if (EnableDHCP.IsPresent && dhcpService != null) + { + // Configure DHCP server for the interface + var dhcpConfig = new DHCPServerConfig + { + Enabled = "1", + RangeFrom = dhcpStart.ToString(), + RangeTo = dhcpEnd.ToString(), + DefaultLeaseTime = "7200", + MaxLeaseTime = "86400", + Domain = Domain, + Gateway = gatewayIp.ToString(), + DnsServers = DnsServers != null ? new List(DnsServers) : new List { gatewayIp.ToString() } + }; + + var updateDhcpTask = Task.Run(async () => await dhcpService.UpdateServerAsync(vlanInterfaceName, dhcpConfig)); + updateDhcpTask.GetAwaiter().GetResult(); + + WriteVerbose($"Configured DHCP server for interface {vlanInterfaceName} with range {dhcpStart} - {dhcpEnd}"); + } + + // Add the created VLAN to the list + var vlanInfo = new PSObject(); + vlanInfo.Properties.Add(new PSNoteProperty("VlanId", vlanId)); + vlanInfo.Properties.Add(new PSNoteProperty("Subnet", subnet.ToString())); + vlanInfo.Properties.Add(new PSNoteProperty("Gateway", gatewayIp.ToString())); + vlanInfo.Properties.Add(new PSNoteProperty("Uuid", createVlanResult.Uuid)); + vlanInfo.Properties.Add(new PSNoteProperty("Status", "Created")); + if (EnableDHCP.IsPresent) + { + vlanInfo.Properties.Add(new PSNoteProperty("DHCPRange", $"{dhcpStart} - {dhcpEnd}")); + } + + createdVlans.Add(vlanInfo); + } + + // Increment the VLAN ID + vlanId += VlanIdIncrement; + subnetIndex++; + } + + // Apply DHCP changes if needed + if (EnableDHCP.IsPresent && dhcpService != null) + { + var applyDhcpTask = Task.Run(async () => await dhcpService.ApplyChangesAsync()); + var applyDhcpResult = applyDhcpTask.GetAwaiter().GetResult(); + WriteVerbose($"Applied DHCP changes: {applyDhcpResult.Status}"); + } + + // Create a result object + var result = new PSObject(); + result.Properties.Add(new PSNoteProperty("ParentInterface", ParentInterface)); + result.Properties.Add(new PSNoteProperty("Network", Network)); + result.Properties.Add(new PSNoteProperty("SubnetMaskBits", SubnetMaskBits)); + result.Properties.Add(new PSNoteProperty("VLANs", createdVlans)); + + WriteObject(result); + } + catch (Exception ex) + { + HandleException(ex); + } + } + + /// + /// Gets the first host address in a subnet + /// + /// The subnet + /// The first host address + private static IPAddress GetFirstHostAddress(IPNetwork2 subnet) + { + byte[] addressBytes = subnet.Network.GetAddressBytes(); + + // If the subnet is too small (e.g., /31 or /32), return the network address + if (subnet.Cidr >= 31) + return subnet.Network; + + // Increment the last byte of the network address by 1 + addressBytes[addressBytes.Length - 1]++; + + return new IPAddress(addressBytes); + } + + /// + /// Gets the second host address in a subnet + /// + /// The subnet + /// The second host address + private static IPAddress GetSecondHostAddress(IPNetwork2 subnet) + { + byte[] addressBytes = subnet.Network.GetAddressBytes(); + + // If the subnet is too small (e.g., /31 or /32), return the network address + if (subnet.Cidr >= 31) + return subnet.Network; + + // If the subnet is /30, return the broadcast address - 1 + if (subnet.Cidr == 30) + return subnet.Broadcast; + + // Increment the last byte of the network address by 2 + addressBytes[addressBytes.Length - 1] += 2; + + return new IPAddress(addressBytes); + } + + /// + /// Gets the last host address in a subnet + /// + /// The subnet + /// The last host address + private static IPAddress GetLastHostAddress(IPNetwork2 subnet) + { + byte[] broadcastBytes = subnet.Broadcast.GetAddressBytes(); + + // If the subnet is too small (e.g., /31 or /32), return the broadcast address + if (subnet.Cidr >= 31) + return subnet.Broadcast; + + // Decrement the last byte of the broadcast address by 1 + broadcastBytes[broadcastBytes.Length - 1]--; + + return new IPAddress(broadcastBytes); + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseUserCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseUserCmdlet.cs new file mode 100644 index 0000000..656bf9d --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseUserCmdlet.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Creates a new user on an OPNSense firewall. + /// The New-OPNSenseUser cmdlet creates a new user on an OPNSense firewall. + /// + /// Example 1: Create a new user + /// New-OPNSenseUser -Username "john" -Password "P@ssw0rd" -FullName "John Doe" -Email "john@example.com" -Groups "admins" + /// This example creates a new user with the specified properties. + /// + /// + [Cmdlet(VerbsCommon.New, "OPNSenseUser")] + [OutputType(typeof(string))] + public class NewOPNSenseUserCmdlet : OPNSenseBaseCmdlet + { + /// + /// The username of the user. + /// + [Parameter(Mandatory = true, Position = 0)] + [ValidateNotNullOrEmpty] + public string Username { get; set; } + + /// + /// The password of the user. + /// + [Parameter(Mandatory = true, Position = 1)] + [ValidateNotNullOrEmpty] + public string Password { get; set; } + + /// + /// The full name of the user. + /// + [Parameter(Mandatory = false)] + public string FullName { get; set; } + + /// + /// The email of the user. + /// + [Parameter(Mandatory = false)] + public string Email { get; set; } + + /// + /// The groups of the user. + /// + [Parameter(Mandatory = false)] + public string[] Groups { get; set; } + + /// + /// The authorizations of the user. + /// + [Parameter(Mandatory = false)] + public string[] Authorizations { get; set; } + + /// + /// Whether the user is disabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Disabled { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var userService = new UserService(ApiClient, Logger); + + var user = new UserConfig + { + Username = Username, + Password = Password, + FullName = FullName, + Email = Email, + Disabled = Disabled.IsPresent ? "1" : "0", + Groups = Groups != null ? new List(Groups) : new List(), + Authorizations = Authorizations != null ? new List(Authorizations) : new List() + }; + + var task = Task.Run(async () => await userService.CreateUserAsync(user)); + var result = task.GetAwaiter().GetResult(); + + WriteVerbose($"Created user with UUID {result.Uuid}"); + WriteObject(result.Uuid); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseVLANCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseVLANCmdlet.cs new file mode 100644 index 0000000..0e1f514 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseVLANCmdlet.cs @@ -0,0 +1,77 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Creates a new VLAN on an OPNSense firewall. + /// The New-OPNSenseVLAN cmdlet creates a new VLAN on an OPNSense firewall. + /// + /// Example 1: Create a new VLAN + /// New-OPNSenseVLAN -Interface "em0" -Tag 10 -Description "Management VLAN" + /// This example creates a new VLAN with tag 10 on interface em0. + /// + /// + [Cmdlet(VerbsCommon.New, "OPNSenseVLAN")] + [OutputType(typeof(string))] + public class NewOPNSenseVLANCmdlet : OPNSenseBaseCmdlet + { + /// + /// The parent interface for the VLAN. + /// + [Parameter(Mandatory = true, Position = 0)] + [ValidateNotNullOrEmpty] + public string Interface { get; set; } + + /// + /// The VLAN tag (1-4094). + /// + [Parameter(Mandatory = true, Position = 1)] + [ValidateRange(1, 4094)] + public int Tag { get; set; } + + /// + /// The priority of the VLAN (0-7). + /// + [Parameter(Mandatory = false)] + [ValidateRange(0, 7)] + public int Priority { get; set; } = 0; + + /// + /// The description of the VLAN. + /// + [Parameter(Mandatory = false)] + public string Description { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var interfaceService = new InterfaceService(ApiClient, Logger); + + var vlan = new VLANConfig + { + Interface = Interface, + Tag = Tag.ToString(), + Priority = Priority.ToString(), + Description = Description + }; + + var task = Task.Run(async () => await interfaceService.CreateVLANAsync(vlan)); + var result = task.GetAwaiter().GetResult(); + + WriteVerbose($"Created VLAN with UUID {result.Uuid}"); + WriteObject(result.Uuid); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/OPNSenseBaseCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/OPNSenseBaseCmdlet.cs new file mode 100644 index 0000000..ec1a698 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/OPNSenseBaseCmdlet.cs @@ -0,0 +1,66 @@ +using System; +using System.Management.Automation; +using PSOPNSenseAPI.Logging; +using PSOPNSenseAPI.Models; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Base class for all OPNSense cmdlets + /// + public abstract class OPNSenseBaseCmdlet : PSCmdlet + { + /// + /// Gets the API client + /// + protected OPNSenseApiClient ApiClient => OPNSenseSession.Current; + + /// + /// Gets the logger + /// + protected ILogger Logger { get; private set; } + + /// + /// Initializes the cmdlet + /// + protected override void BeginProcessing() + { + base.BeginProcessing(); + Logger = new PowerShellLogger(this); + + if (!OPNSenseSession.IsConnected) + { + ThrowTerminatingError(new ErrorRecord( + new InvalidOperationException("Not connected to an OPNSense firewall. Use Connect-OPNSense first."), + "NotConnected", + ErrorCategory.ConnectionError, + null)); + } + } + + /// + /// Handles exceptions + /// + /// The exception to handle + protected void HandleException(Exception ex) + { + if (ex is OPNSenseApiException apiEx) + { + WriteError(new ErrorRecord( + apiEx, + "OPNSenseApiError", + ErrorCategory.InvalidOperation, + null)); + } + else + { + WriteError(new ErrorRecord( + ex, + "OPNSenseError", + ErrorCategory.NotSpecified, + null)); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/OPNSenseCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/OPNSenseCmdlet.cs new file mode 100644 index 0000000..d96b59e --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/OPNSenseCmdlet.cs @@ -0,0 +1,34 @@ +using System; +using System.Management.Automation; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Base class for OPNSense cmdlets that use the new API client + /// + public abstract class OPNSenseCmdlet : PSCmdlet + { + /// + /// Gets the session state + /// + protected new OPNSenseSessionState SessionState => OPNSenseSessionState.Instance; + + /// + /// Initializes the cmdlet + /// + protected override void BeginProcessing() + { + base.BeginProcessing(); + + if (SessionState.ApiClient == null) + { + ThrowTerminatingError(new ErrorRecord( + new InvalidOperationException("Not connected to an OPNSense firewall. Use Connect-OPNSense first."), + "NotConnected", + ErrorCategory.ConnectionError, + null)); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseAliasCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseAliasCmdlet.cs new file mode 100644 index 0000000..dbd21fb --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseAliasCmdlet.cs @@ -0,0 +1,94 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Removes an alias from an OPNSense firewall. + /// The Remove-OPNSenseAlias cmdlet removes an alias from an OPNSense firewall. + /// + /// Example 1: Remove an alias + /// Remove-OPNSenseAlias -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Apply + /// This example removes an alias by its UUID. + /// + /// + /// Example 2: Remove an alias with confirmation + /// Remove-OPNSenseAlias -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Confirm -Apply + /// This example removes an alias by its UUID after confirmation. + /// + /// + /// Example 3: Remove aliases by pipeline + /// Get-OPNSenseAlias -Type host | Remove-OPNSenseAlias -Apply + /// This example removes all host aliases. + /// + /// + [Cmdlet(VerbsCommon.Remove, "OPNSenseAlias", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + [OutputType(typeof(void))] + public class RemoveOPNSenseAliasCmdlet : OPNSenseBaseCmdlet + { + /// + /// The UUID of the alias to remove. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// Suppresses the confirmation prompt. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Force { get; set; } + + /// + /// Whether to apply the changes immediately. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Apply { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var aliasService = new AliasService(ApiClient, Logger); + + // Get the alias details for the confirmation message + var getTask = Task.Run(async () => await aliasService.GetAliasAsync(Uuid)); + var alias = getTask.GetAwaiter().GetResult().Alias; + + string confirmMessage = $"Alias: {alias.Name} ({alias.Type})"; + if (!string.IsNullOrEmpty(alias.Description)) + { + confirmMessage += $" - {alias.Description}"; + } + + if (!Force.IsPresent && !ShouldProcess(confirmMessage, "Remove")) + { + return; + } + + var deleteTask = Task.Run(async () => await aliasService.DeleteAliasAsync(Uuid)); + var deleteResult = deleteTask.GetAwaiter().GetResult(); + + WriteVerbose($"Alias {Uuid} removed: {deleteResult.Result}"); + + // Apply changes if requested + if (Apply.IsPresent) + { + var applyTask = Task.Run(async () => await aliasService.ReconfigureAliasesAsync()); + var applyResult = applyTask.GetAwaiter().GetResult(); + + WriteVerbose($"Alias changes applied: {applyResult.Status}"); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseCronJobCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseCronJobCmdlet.cs new file mode 100644 index 0000000..cfdc5b3 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseCronJobCmdlet.cs @@ -0,0 +1,89 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Removes a cron job from an OPNSense firewall. + /// The Remove-OPNSenseCronJob cmdlet removes a cron job from an OPNSense firewall. + /// + /// Example 1: Remove a cron job + /// Remove-OPNSenseCronJob -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" + /// This example removes a cron job by its UUID. + /// + /// + /// Example 2: Remove a cron job with confirmation + /// Remove-OPNSenseCronJob -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Confirm + /// This example removes a cron job by its UUID after confirmation. + /// + /// + [Cmdlet(VerbsCommon.Remove, "OPNSenseCronJob", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + [OutputType(typeof(void))] + public class RemoveOPNSenseCronJobCmdlet : OPNSenseBaseCmdlet + { + /// + /// The UUID of the cron job to remove. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// Suppresses the confirmation prompt. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Force { get; set; } + + /// + /// Whether to apply the changes immediately. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Apply { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var cronService = new CronService(ApiClient, Logger); + + // Get the job details for the confirmation message + var getTask = Task.Run(async () => await cronService.GetJobAsync(Uuid)); + var job = getTask.GetAwaiter().GetResult().Job; + + string confirmMessage = $"Cron job: {job.Description}"; + if (!string.IsNullOrEmpty(job.Command)) + { + confirmMessage += $" ({job.Command})"; + } + + if (!Force.IsPresent && !ShouldProcess(confirmMessage, "Remove")) + { + return; + } + + var deleteTask = Task.Run(async () => await cronService.DeleteJobAsync(Uuid)); + var deleteResult = deleteTask.GetAwaiter().GetResult(); + + WriteVerbose($"Cron job {Uuid} removed: {deleteResult.Result}"); + + // Apply the changes if requested + if (Apply.IsPresent) + { + var applyTask = Task.Run(async () => await cronService.ApplyChangesAsync()); + var applyResult = applyTask.GetAwaiter().GetResult(); + + WriteVerbose($"Cron changes applied: {applyResult.Status}"); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseDHCPLeaseCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseDHCPLeaseCmdlet.cs new file mode 100644 index 0000000..7ea15bb --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseDHCPLeaseCmdlet.cs @@ -0,0 +1,89 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Removes a DHCP lease from an OPNSense firewall. + /// The Remove-OPNSenseDHCPLease cmdlet removes a DHCP lease from an OPNSense firewall. + /// + /// Example 1: Remove a DHCP lease + /// Remove-OPNSenseDHCPLease -MacAddress "00:11:22:33:44:55" -Apply + /// This example removes a DHCP lease by its MAC address. + /// + /// + /// Example 2: Remove a DHCP lease with confirmation + /// Remove-OPNSenseDHCPLease -MacAddress "00:11:22:33:44:55" -Confirm -Apply + /// This example removes a DHCP lease by its MAC address after confirmation. + /// + /// + [Cmdlet(VerbsCommon.Remove, "OPNSenseDHCPLease", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + [OutputType(typeof(void))] + public class RemoveOPNSenseDHCPLeaseCmdlet : OPNSenseBaseCmdlet + { + /// + /// The MAC address of the DHCP lease to remove. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string MacAddress { get; set; } + + /// + /// Suppresses the confirmation prompt. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Force { get; set; } + + /// + /// Whether to apply the changes immediately. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Apply { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var dhcpService = new DHCPService(ApiClient, Logger); + + // Get the lease details for the confirmation message + var getTask = Task.Run(async () => await dhcpService.GetLeaseByMacAsync(MacAddress)); + var lease = getTask.GetAwaiter().GetResult().Lease; + + string confirmMessage = $"DHCP lease: {lease.MacAddress} -> {lease.IpAddress}"; + if (!string.IsNullOrEmpty(lease.Hostname)) + { + confirmMessage += $" ({lease.Hostname})"; + } + + if (!Force.IsPresent && !ShouldProcess(confirmMessage, "Remove")) + { + return; + } + + var deleteTask = Task.Run(async () => await dhcpService.DeleteLeaseAsync(MacAddress)); + var deleteResult = deleteTask.GetAwaiter().GetResult(); + + WriteVerbose($"DHCP lease {MacAddress} removed: {deleteResult.Result}"); + + // Apply changes if requested + if (Apply.IsPresent) + { + var applyTask = Task.Run(async () => await dhcpService.ApplyChangesAsync()); + var applyResult = applyTask.GetAwaiter().GetResult(); + + WriteVerbose($"DHCP changes applied: {applyResult.Status}"); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseDHCPOptionCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseDHCPOptionCmdlet.cs new file mode 100644 index 0000000..441b9dd --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseDHCPOptionCmdlet.cs @@ -0,0 +1,96 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Removes a DHCP option from an OPNSense firewall. + /// The Remove-OPNSenseDHCPOption cmdlet removes a DHCP option from an OPNSense firewall. + /// + /// Example 1: Remove a DHCP option + /// Remove-OPNSenseDHCPOption -Interface "lan" -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Apply + /// This example removes a DHCP option by its UUID. + /// + /// + /// Example 2: Remove a DHCP option with confirmation + /// Remove-OPNSenseDHCPOption -Interface "lan" -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Confirm -Apply + /// This example removes a DHCP option by its UUID after confirmation. + /// + /// + [Cmdlet(VerbsCommon.Remove, "OPNSenseDHCPOption", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + [OutputType(typeof(void))] + public class RemoveOPNSenseDHCPOptionCmdlet : OPNSenseBaseCmdlet + { + /// + /// The interface name. + /// + [Parameter(Mandatory = true, Position = 0)] + [ValidateNotNullOrEmpty] + public string Interface { get; set; } + + /// + /// The UUID of the DHCP option to remove. + /// + [Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// Suppresses the confirmation prompt. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Force { get; set; } + + /// + /// Whether to apply the changes immediately. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Apply { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var dhcpService = new DHCPService(ApiClient, Logger); + + // Get the option details for the confirmation message + var getTask = Task.Run(async () => await dhcpService.GetOptionAsync(Interface, Uuid)); + var option = getTask.GetAwaiter().GetResult().Option; + + string confirmMessage = $"DHCP option: {option.Number} = {option.Value}"; + if (!string.IsNullOrEmpty(option.Description)) + { + confirmMessage += $" ({option.Description})"; + } + + if (!Force.IsPresent && !ShouldProcess(confirmMessage, "Remove")) + { + return; + } + + var deleteTask = Task.Run(async () => await dhcpService.DeleteOptionAsync(Interface, Uuid)); + var deleteResult = deleteTask.GetAwaiter().GetResult(); + + WriteVerbose($"DHCP option {Uuid} removed: {deleteResult.Result}"); + + // Apply changes if requested + if (Apply.IsPresent) + { + var applyTask = Task.Run(async () => await dhcpService.ApplyChangesAsync()); + var applyResult = applyTask.GetAwaiter().GetResult(); + + WriteVerbose($"DHCP changes applied: {applyResult.Status}"); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseDHCPStaticMappingCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseDHCPStaticMappingCmdlet.cs new file mode 100644 index 0000000..3676511 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseDHCPStaticMappingCmdlet.cs @@ -0,0 +1,96 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Removes a DHCP static mapping from an OPNSense firewall. + /// The Remove-OPNSenseDHCPStaticMapping cmdlet removes a DHCP static mapping from an OPNSense firewall. + /// + /// Example 1: Remove a static mapping + /// Remove-OPNSenseDHCPStaticMapping -Interface "lan" -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" + /// This example removes a static mapping by its UUID. + /// + /// + /// Example 2: Remove a static mapping with confirmation + /// Remove-OPNSenseDHCPStaticMapping -Interface "lan" -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Confirm + /// This example removes a static mapping by its UUID after confirmation. + /// + /// + [Cmdlet(VerbsCommon.Remove, "OPNSenseDHCPStaticMapping", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + [OutputType(typeof(void))] + public class RemoveOPNSenseDHCPStaticMappingCmdlet : OPNSenseBaseCmdlet + { + /// + /// The interface of the static mapping to remove. + /// + [Parameter(Mandatory = true, Position = 0)] + [ValidateNotNullOrEmpty] + public string Interface { get; set; } + + /// + /// The UUID of the static mapping to remove. + /// + [Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// Suppresses the confirmation prompt. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Force { get; set; } + + /// + /// Whether to apply the changes immediately. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Apply { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var dhcpService = new DHCPService(ApiClient, Logger); + + // Get the mapping details for the confirmation message + var getTask = Task.Run(async () => await dhcpService.GetStaticMappingAsync(Interface, Uuid)); + var mapping = getTask.GetAwaiter().GetResult().Mapping; + + string confirmMessage = $"Static mapping: {mapping.MacAddress} -> {mapping.IpAddress}"; + if (!string.IsNullOrEmpty(mapping.Hostname)) + { + confirmMessage += $" ({mapping.Hostname})"; + } + + if (!Force.IsPresent && !ShouldProcess(confirmMessage, "Remove")) + { + return; + } + + var deleteTask = Task.Run(async () => await dhcpService.DeleteStaticMappingAsync(Interface, Uuid)); + var deleteResult = deleteTask.GetAwaiter().GetResult(); + + WriteVerbose($"Static mapping {Uuid} removed: {deleteResult.Result}"); + + // Apply the changes if requested + if (Apply.IsPresent) + { + var applyTask = Task.Run(async () => await dhcpService.ApplyChangesAsync()); + var applyResult = applyTask.GetAwaiter().GetResult(); + + WriteVerbose($"DHCP changes applied: {applyResult.Status}"); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseDNSForwardingHostCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseDNSForwardingHostCmdlet.cs new file mode 100644 index 0000000..52c7f6e --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseDNSForwardingHostCmdlet.cs @@ -0,0 +1,89 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Removes a DNS forwarding host from an OPNSense firewall. + /// The Remove-OPNSenseDNSForwardingHost cmdlet removes a DNS forwarding host from an OPNSense firewall. + /// + /// Example 1: Remove a DNS forwarding host + /// Remove-OPNSenseDNSForwardingHost -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Apply + /// This example removes a DNS forwarding host by its UUID. + /// + /// + /// Example 2: Remove a DNS forwarding host with confirmation + /// Remove-OPNSenseDNSForwardingHost -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Confirm -Apply + /// This example removes a DNS forwarding host by its UUID after confirmation. + /// + /// + [Cmdlet(VerbsCommon.Remove, "OPNSenseDNSForwardingHost", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + [OutputType(typeof(void))] + public class RemoveOPNSenseDNSForwardingHostCmdlet : OPNSenseBaseCmdlet + { + /// + /// The UUID of the DNS forwarding host to remove. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// Suppresses the confirmation prompt. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Force { get; set; } + + /// + /// Whether to apply the changes immediately. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Apply { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var dnsService = new DNSService(ApiClient, Logger); + + // Get the host details for the confirmation message + var getTask = Task.Run(async () => await dnsService.GetDNSForwardingHostAsync(Uuid)); + var host = getTask.GetAwaiter().GetResult().Host; + + string confirmMessage = $"DNS forwarding host: {host.Domain} -> {host.Server}"; + if (!string.IsNullOrEmpty(host.Description)) + { + confirmMessage += $" ({host.Description})"; + } + + if (!Force.IsPresent && !ShouldProcess(confirmMessage, "Remove")) + { + return; + } + + var deleteTask = Task.Run(async () => await dnsService.DeleteDNSForwardingHostAsync(Uuid)); + var deleteResult = deleteTask.GetAwaiter().GetResult(); + + WriteVerbose($"DNS forwarding host {Uuid} removed: {deleteResult.Result}"); + + // Apply changes if requested + if (Apply.IsPresent) + { + var applyTask = Task.Run(async () => await dnsService.ApplyDNSChangesAsync()); + var applyResult = applyTask.GetAwaiter().GetResult(); + + WriteVerbose($"DNS changes applied: {applyResult.Status}"); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseDNSOverrideCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseDNSOverrideCmdlet.cs new file mode 100644 index 0000000..0777b50 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseDNSOverrideCmdlet.cs @@ -0,0 +1,89 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Removes a DNS override from an OPNSense firewall. + /// The Remove-OPNSenseDNSOverride cmdlet removes a DNS override from an OPNSense firewall. + /// + /// Example 1: Remove a DNS override + /// Remove-OPNSenseDNSOverride -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" + /// This example removes a DNS override by its UUID. + /// + /// + /// Example 2: Remove a DNS override with confirmation + /// Remove-OPNSenseDNSOverride -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Confirm + /// This example removes a DNS override by its UUID after confirmation. + /// + /// + [Cmdlet(VerbsCommon.Remove, "OPNSenseDNSOverride", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + [OutputType(typeof(void))] + public class RemoveOPNSenseDNSOverrideCmdlet : OPNSenseBaseCmdlet + { + /// + /// The UUID of the DNS override to remove. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// Suppresses the confirmation prompt. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Force { get; set; } + + /// + /// Whether to apply the changes immediately. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Apply { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var dnsService = new DNSService(ApiClient, Logger); + + // Get the DNS override details for the confirmation message + var getTask = Task.Run(async () => await dnsService.GetDNSOverrideAsync(Uuid)); + var dnsOverride = getTask.GetAwaiter().GetResult().Host; + + string confirmMessage = $"DNS override {dnsOverride.Hostname}.{dnsOverride.Domain} -> {dnsOverride.IpAddress}"; + if (!string.IsNullOrEmpty(dnsOverride.Description)) + { + confirmMessage += $" ({dnsOverride.Description})"; + } + + if (!Force.IsPresent && !ShouldProcess(confirmMessage, "Remove")) + { + return; + } + + var deleteTask = Task.Run(async () => await dnsService.DeleteDNSOverrideAsync(Uuid)); + var deleteResult = deleteTask.GetAwaiter().GetResult(); + + WriteVerbose($"DNS override {Uuid} removed: {deleteResult.Result}"); + + // Apply the changes if requested + if (Apply.IsPresent) + { + var applyTask = Task.Run(async () => await dnsService.ApplyDNSChangesAsync()); + var applyResult = applyTask.GetAwaiter().GetResult(); + + WriteVerbose($"DNS changes applied: {applyResult.Status}"); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseFirewallRuleCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseFirewallRuleCmdlet.cs new file mode 100644 index 0000000..1a75381 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseFirewallRuleCmdlet.cs @@ -0,0 +1,74 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Removes a firewall rule from an OPNSense firewall. + /// The Remove-OPNSenseFirewallRule cmdlet removes a firewall rule from an OPNSense firewall. + /// + /// Example 1: Remove a firewall rule + /// Remove-OPNSenseFirewallRule -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" + /// This example removes a firewall rule by its UUID. + /// + /// + /// Example 2: Remove a firewall rule with confirmation + /// Remove-OPNSenseFirewallRule -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Confirm + /// This example removes a firewall rule by its UUID after confirmation. + /// + /// + [Cmdlet(VerbsCommon.Remove, "OPNSenseFirewallRule", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + [OutputType(typeof(void))] + public class RemoveOPNSenseFirewallRuleCmdlet : OPNSenseBaseCmdlet + { + /// + /// The UUID of the firewall rule to remove. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// Suppresses the confirmation prompt. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Force { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var firewallService = new FirewallService(ApiClient, Logger); + + // Get the rule details for the confirmation message + var getTask = Task.Run(async () => await firewallService.GetRuleAsync(Uuid)); + var rule = getTask.GetAwaiter().GetResult().Rule; + + string confirmMessage = $"Firewall rule: {rule.Description}"; + if (!string.IsNullOrEmpty(rule.Protocol) && rule.Protocol != "any") + { + confirmMessage += $" ({rule.Protocol})"; + } + + if (!Force.IsPresent && !ShouldProcess(confirmMessage, "Remove")) + { + return; + } + + var deleteTask = Task.Run(async () => await firewallService.DeleteRuleAsync(Uuid)); + var deleteResult = deleteTask.GetAwaiter().GetResult(); + + WriteVerbose($"Firewall rule {Uuid} removed: {deleteResult.Result}"); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseGatewayCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseGatewayCmdlet.cs new file mode 100644 index 0000000..7e10376 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseGatewayCmdlet.cs @@ -0,0 +1,89 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Removes a gateway from an OPNSense firewall. + /// The Remove-OPNSenseGateway cmdlet removes a gateway from an OPNSense firewall. + /// + /// Example 1: Remove a gateway + /// Remove-OPNSenseGateway -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Apply + /// This example removes a gateway by its UUID. + /// + /// + /// Example 2: Remove a gateway with confirmation + /// Remove-OPNSenseGateway -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Confirm -Apply + /// This example removes a gateway by its UUID after confirmation. + /// + /// + [Cmdlet(VerbsCommon.Remove, "OPNSenseGateway", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + [OutputType(typeof(void))] + public class RemoveOPNSenseGatewayCmdlet : OPNSenseBaseCmdlet + { + /// + /// The UUID of the gateway to remove. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// Suppresses the confirmation prompt. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Force { get; set; } + + /// + /// Whether to apply the changes immediately. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Apply { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var gatewayService = new GatewayService(ApiClient, Logger); + + // Get the gateway details for the confirmation message + var getTask = Task.Run(async () => await gatewayService.GetGatewayAsync(Uuid)); + var gateway = getTask.GetAwaiter().GetResult().Gateway; + + string confirmMessage = $"Gateway: {gateway.Name} ({gateway.IpAddress})"; + if (!string.IsNullOrEmpty(gateway.Description)) + { + confirmMessage += $" - {gateway.Description}"; + } + + if (!Force.IsPresent && !ShouldProcess(confirmMessage, "Remove")) + { + return; + } + + var deleteTask = Task.Run(async () => await gatewayService.DeleteGatewayAsync(Uuid)); + var deleteResult = deleteTask.GetAwaiter().GetResult(); + + WriteVerbose($"Gateway {Uuid} removed: {deleteResult.Result}"); + + // Apply changes if requested + if (Apply.IsPresent) + { + var applyTask = Task.Run(async () => await gatewayService.ApplyGatewayChangesAsync()); + var applyResult = applyTask.GetAwaiter().GetResult(); + + WriteVerbose($"Gateway changes applied: {applyResult.Status}"); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSensePortForwardingRuleCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSensePortForwardingRuleCmdlet.cs new file mode 100644 index 0000000..a3ec126 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSensePortForwardingRuleCmdlet.cs @@ -0,0 +1,72 @@ +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Removes a port forwarding rule from an OPNSense firewall. + /// The Remove-OPNSensePortForwardingRule cmdlet removes a port forwarding rule from an OPNSense firewall. + /// + /// Remove a port forwarding rule + /// Remove-OPNSensePortForwardingRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" + /// + /// + [Cmdlet(VerbsCommon.Remove, "OPNSensePortForwardingRule", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + public class RemoveOPNSensePortForwardingRuleCmdlet : OPNSenseCmdlet + { + /// + /// The UUID of the port forwarding rule to remove. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)] + public string Uuid { get; set; } + + /// + /// Suppresses the confirmation prompt. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Force { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + var portForwardingService = new PortForwardingService(SessionState.ApiClient); + + // Get the rule to display information in the confirmation message + var rule = Task.Run(async () => await portForwardingService.GetPortForwardingRuleAsync(Uuid)).GetAwaiter().GetResult(); + + if (rule == null) + { + WriteWarning($"Port forwarding rule with UUID '{Uuid}' not found."); + return; + } + + string confirmationMessage = $"Remove port forwarding rule"; + if (!string.IsNullOrEmpty(rule.Description)) + { + confirmationMessage += $" '{rule.Description}'"; + } + confirmationMessage += $" with UUID '{Uuid}'"; + + if (Force || ShouldProcess($"OPNSense firewall", confirmationMessage)) + { + var success = Task.Run(async () => await portForwardingService.DeletePortForwardingRuleAsync(Uuid)).GetAwaiter().GetResult(); + + if (success) + { + WriteVerbose($"Port forwarding rule with UUID '{Uuid}' removed successfully."); + } + else + { + WriteError(new ErrorRecord( + new PSInvalidOperationException($"Failed to remove port forwarding rule with UUID '{Uuid}'."), + "PortForwardingRuleRemovalFailed", + ErrorCategory.InvalidOperation, + Uuid)); + } + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseRouteCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseRouteCmdlet.cs new file mode 100644 index 0000000..502c19f --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseRouteCmdlet.cs @@ -0,0 +1,89 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Removes a route from an OPNSense firewall. + /// The Remove-OPNSenseRoute cmdlet removes a route from an OPNSense firewall. + /// + /// Example 1: Remove a route + /// Remove-OPNSenseRoute -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Apply + /// This example removes a route by its UUID. + /// + /// + /// Example 2: Remove a route with confirmation + /// Remove-OPNSenseRoute -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Confirm -Apply + /// This example removes a route by its UUID after confirmation. + /// + /// + [Cmdlet(VerbsCommon.Remove, "OPNSenseRoute", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + [OutputType(typeof(void))] + public class RemoveOPNSenseRouteCmdlet : OPNSenseBaseCmdlet + { + /// + /// The UUID of the route to remove. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// Suppresses the confirmation prompt. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Force { get; set; } + + /// + /// Whether to apply the changes immediately. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Apply { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var routeService = new RouteService(ApiClient, Logger); + + // Get the route details for the confirmation message + var getTask = Task.Run(async () => await routeService.GetRouteAsync(Uuid)); + var route = getTask.GetAwaiter().GetResult().Route; + + string confirmMessage = $"Route: {route.Network} via {route.Gateway}"; + if (!string.IsNullOrEmpty(route.Description)) + { + confirmMessage += $" ({route.Description})"; + } + + if (!Force.IsPresent && !ShouldProcess(confirmMessage, "Remove")) + { + return; + } + + var deleteTask = Task.Run(async () => await routeService.DeleteRouteAsync(Uuid)); + var deleteResult = deleteTask.GetAwaiter().GetResult(); + + WriteVerbose($"Route {Uuid} removed: {deleteResult.Result}"); + + // Apply changes if requested + if (Apply.IsPresent) + { + var applyTask = Task.Run(async () => await routeService.ApplyRouteChangesAsync()); + var applyResult = applyTask.GetAwaiter().GetResult(); + + WriteVerbose($"Route changes applied: {applyResult.Status}"); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseUserCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseUserCmdlet.cs new file mode 100644 index 0000000..d92c844 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseUserCmdlet.cs @@ -0,0 +1,74 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Removes a user from an OPNSense firewall. + /// The Remove-OPNSenseUser cmdlet removes a user from an OPNSense firewall. + /// + /// Example 1: Remove a user + /// Remove-OPNSenseUser -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" + /// This example removes a user by its UUID. + /// + /// + /// Example 2: Remove a user with confirmation + /// Remove-OPNSenseUser -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Confirm + /// This example removes a user by its UUID after confirmation. + /// + /// + [Cmdlet(VerbsCommon.Remove, "OPNSenseUser", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + [OutputType(typeof(void))] + public class RemoveOPNSenseUserCmdlet : OPNSenseBaseCmdlet + { + /// + /// The UUID of the user to remove. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// Suppresses the confirmation prompt. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Force { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var userService = new UserService(ApiClient, Logger); + + // Get the user details for the confirmation message + var getTask = Task.Run(async () => await userService.GetUserAsync(Uuid)); + var user = getTask.GetAwaiter().GetResult().User; + + string confirmMessage = $"User: {user.Username}"; + if (!string.IsNullOrEmpty(user.FullName)) + { + confirmMessage += $" ({user.FullName})"; + } + + if (!Force.IsPresent && !ShouldProcess(confirmMessage, "Remove")) + { + return; + } + + var deleteTask = Task.Run(async () => await userService.DeleteUserAsync(Uuid)); + var deleteResult = deleteTask.GetAwaiter().GetResult(); + + WriteVerbose($"User {Uuid} removed: {deleteResult.Result}"); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseVLANCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseVLANCmdlet.cs new file mode 100644 index 0000000..752846d --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/RemoveOPNSenseVLANCmdlet.cs @@ -0,0 +1,74 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Removes a VLAN from an OPNSense firewall. + /// The Remove-OPNSenseVLAN cmdlet removes a VLAN from an OPNSense firewall. + /// + /// Example 1: Remove a VLAN + /// Remove-OPNSenseVLAN -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" + /// This example removes a VLAN by its UUID. + /// + /// + /// Example 2: Remove a VLAN with confirmation + /// Remove-OPNSenseVLAN -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Confirm + /// This example removes a VLAN by its UUID after confirmation. + /// + /// + [Cmdlet(VerbsCommon.Remove, "OPNSenseVLAN", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + [OutputType(typeof(void))] + public class RemoveOPNSenseVLANCmdlet : OPNSenseBaseCmdlet + { + /// + /// The UUID of the VLAN to remove. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// Suppresses the confirmation prompt. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Force { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var interfaceService = new InterfaceService(ApiClient, Logger); + + // Get the VLAN details for the confirmation message + var getTask = Task.Run(async () => await interfaceService.GetVLANAsync(Uuid)); + var vlan = getTask.GetAwaiter().GetResult().Vlan; + + string confirmMessage = $"VLAN {vlan.Tag} on interface {vlan.Interface}"; + if (!string.IsNullOrEmpty(vlan.Description)) + { + confirmMessage += $" ({vlan.Description})"; + } + + if (!Force.IsPresent && !ShouldProcess(confirmMessage, "Remove")) + { + return; + } + + var deleteTask = Task.Run(async () => await interfaceService.DeleteVLANAsync(Uuid)); + var deleteResult = deleteTask.GetAwaiter().GetResult(); + + WriteVerbose($"VLAN {Uuid} removed: {deleteResult.Result}"); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/RestartOPNSenseFirewallCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/RestartOPNSenseFirewallCmdlet.cs new file mode 100644 index 0000000..d0b410b --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/RestartOPNSenseFirewallCmdlet.cs @@ -0,0 +1,160 @@ +using System; +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using PSOPNSenseAPI.Logging; +using PSOPNSenseAPI.Models; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Restarts an OPNSense firewall. + /// The Restart-OPNSenseFirewall cmdlet restarts an OPNSense firewall and optionally waits for it to come back online. + /// + /// Example 1: Restart a firewall + /// Restart-OPNSenseFirewall + /// This example restarts the OPNSense firewall. + /// + /// + /// Example 2: Restart a firewall and wait for it to come back online + /// Restart-OPNSenseFirewall -Wait -Timeout 300 + /// This example restarts the OPNSense firewall and waits up to 5 minutes for it to come back online. + /// + /// + [Cmdlet(VerbsLifecycle.Restart, "OPNSenseFirewall", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + [OutputType(typeof(void))] + public class RestartOPNSenseFirewallCmdlet : OPNSenseBaseCmdlet + { + /// + /// Suppresses the confirmation prompt. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Force { get; set; } + + /// + /// Waits for the firewall to come back online after restarting. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Wait { get; set; } + + /// + /// The timeout in seconds to wait for the firewall to come back online. + /// + [Parameter(Mandatory = false)] + [ValidateRange(1, 3600)] + public int Timeout { get; set; } = 180; + + /// + /// The interval in seconds between connection attempts. + /// + [Parameter(Mandatory = false)] + [ValidateRange(1, 60)] + public int Interval { get; set; } = 5; + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + if (!Force.IsPresent && !ShouldProcess("OPNSense firewall", "Restart")) + { + return; + } + + var systemService = new SystemService(ApiClient, Logger); + + // Store connection information for reconnection + string baseUrl = OPNSenseSession.BaseUrl; + string apiKey = null; + string apiSecret = null; + bool skipCertificateCheck = false; + + // Extract connection information from the current session + if (ApiClient is OPNSenseApiClient client) + { + baseUrl = client.BaseUrl; + apiKey = client.ApiKey; + apiSecret = client.ApiSecret; + skipCertificateCheck = client.SkipCertificateCheck; + } + + // If we couldn't get the credentials, we can't reconnect + if (Wait.IsPresent && (string.IsNullOrEmpty(apiKey) || string.IsNullOrEmpty(apiSecret))) + { + WriteWarning("Could not retrieve API credentials from the current session. Will not attempt to reconnect after restart."); + Wait = false; + } + + // Restart the firewall + var task = Task.Run(async () => await systemService.RebootAsync()); + var result = task.GetAwaiter().GetResult(); + + WriteVerbose($"Firewall restart initiated: {result.Status}"); + WriteObject("Firewall restart initiated. The firewall is now restarting."); + + // Wait for the firewall to come back online if requested + if (Wait.IsPresent) + { + WriteVerbose($"Waiting for firewall to come back online (timeout: {Timeout} seconds, interval: {Interval} seconds)"); + WriteObject("Waiting for firewall to come back online..."); + + // Disconnect the current session + OPNSenseSession.Current?.Dispose(); + OPNSenseSession.Current = null; + + // Wait a bit for the firewall to start rebooting + Thread.Sleep(5000); + + // Try to reconnect + DateTime startTime = DateTime.Now; + bool reconnected = false; + + while (DateTime.Now - startTime < TimeSpan.FromSeconds(Timeout)) + { + try + { + WriteVerbose($"Attempting to reconnect to {baseUrl}..."); + + // Create a new logger + var logger = new PowerShellLogger(this); + + // Create a new API client + var newClient = new OPNSenseApiClient(baseUrl, apiKey, apiSecret, skipCertificateCheck, logger); + + // Try to get the system status + var statusService = new SystemService(newClient, logger); + var statusTask = Task.Run(async () => await statusService.GetStatusAsync()); + var statusResult = statusTask.GetAwaiter().GetResult(); + + // If we get here, the firewall is back online + WriteVerbose("Successfully reconnected to the firewall"); + WriteObject($"Firewall is back online. Uptime: {statusResult.Uptime}"); + + // Set the new session + OPNSenseSession.Current = newClient; + reconnected = true; + break; + } + catch (Exception ex) + { + WriteVerbose($"Reconnection attempt failed: {ex.Message}"); + Thread.Sleep(Interval * 1000); + } + } + + if (!reconnected) + { + WriteWarning($"Timed out waiting for firewall to come back online after {Timeout} seconds"); + } + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/RestartOPNSenseInterfaceCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/RestartOPNSenseInterfaceCmdlet.cs new file mode 100644 index 0000000..ab0a532 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/RestartOPNSenseInterfaceCmdlet.cs @@ -0,0 +1,59 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Restarts an interface on an OPNSense firewall. + /// The Restart-OPNSenseInterface cmdlet restarts an interface on an OPNSense firewall. + /// + /// Example 1: Restart an interface + /// Restart-OPNSenseInterface -Name "lan" + /// This example restarts the LAN interface. + /// + /// + [Cmdlet(VerbsLifecycle.Restart, "OPNSenseInterface", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + [OutputType(typeof(void))] + public class RestartOPNSenseInterfaceCmdlet : OPNSenseBaseCmdlet + { + /// + /// The name of the interface to restart. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string Name { get; set; } + + /// + /// Suppresses the confirmation prompt. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Force { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + if (!Force.IsPresent && !ShouldProcess(Name, "Restart interface")) + { + return; + } + + var interfaceService = new InterfaceService(ApiClient, Logger); + + var task = Task.Run(async () => await interfaceService.RestartInterfaceAsync(Name)); + var result = task.GetAwaiter().GetResult(); + + WriteVerbose($"Interface {Name} restarted: {result.Status}"); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/RestoreOPNSenseConfigCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/RestoreOPNSenseConfigCmdlet.cs new file mode 100644 index 0000000..497597c --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/RestoreOPNSenseConfigCmdlet.cs @@ -0,0 +1,60 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Restores an OPNSense firewall configuration from a backup. + /// The Restore-OPNSenseConfig cmdlet restores an OPNSense firewall configuration from a backup. + /// + /// Example 1: Restore a configuration backup + /// Restore-OPNSenseConfig -Filename "config-backup-20250414-1234.xml" + /// This example restores an OPNSense firewall configuration from a backup file. + /// + /// + [Cmdlet(VerbsData.Restore, "OPNSenseConfig", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + [OutputType(typeof(void))] + public class RestoreOPNSenseConfigCmdlet : OPNSenseBaseCmdlet + { + /// + /// The filename of the backup to restore. + /// + [Parameter(Mandatory = true, Position = 0)] + [ValidateNotNullOrEmpty] + public string Filename { get; set; } + + /// + /// Suppresses the confirmation prompt. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Force { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + if (!Force.IsPresent && !ShouldProcess(Filename, "Restore configuration backup")) + { + return; + } + + var configService = new ConfigService(ApiClient, Logger); + + var task = Task.Run(async () => await configService.RestoreConfigBackupAsync(Filename)); + var result = task.GetAwaiter().GetResult(); + + WriteVerbose($"Restored configuration backup: {result.Status}"); + WriteWarning("The firewall is restarting. You may need to reconnect after it comes back online."); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseAliasCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseAliasCmdlet.cs new file mode 100644 index 0000000..6d2dd5e --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseAliasCmdlet.cs @@ -0,0 +1,177 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Updates an alias on an OPNSense firewall. + /// The Set-OPNSenseAlias cmdlet updates an alias on an OPNSense firewall. + /// + /// Example 1: Update an alias's content + /// Set-OPNSenseAlias -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Content "192.168.1.10,192.168.1.11,192.168.1.12" -Apply + /// This example updates the content of an alias. + /// + /// + /// Example 2: Update an alias's description + /// Set-OPNSenseAlias -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Description "Updated description" -Apply + /// This example updates the description of an alias. + /// + /// + /// Example 3: Disable an alias + /// Set-OPNSenseAlias -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Disabled -Apply + /// This example disables an alias. + /// + /// + [Cmdlet(VerbsCommon.Set, "OPNSenseAlias")] + [OutputType(typeof(void))] + public class SetOPNSenseAliasCmdlet : OPNSenseBaseCmdlet + { + /// + /// The UUID of the alias to update. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// The content of the alias. For host/network/port types, use comma-separated values. For URL types, specify the URL. + /// + [Parameter(Mandatory = false)] + public string Content { get; set; } + + /// + /// The description of the alias. + /// + [Parameter(Mandatory = false)] + public string Description { get; set; } + + /// + /// The protocol for port aliases (TCP, UDP, or TCP/UDP). + /// + [Parameter(Mandatory = false)] + [ValidateSet("TCP", "UDP", "TCP/UDP")] + public string Protocol { get; set; } + + /// + /// The update frequency for URL aliases (in days). + /// + [Parameter(Mandatory = false)] + [ValidateRange(1, 365)] + public int? UpdateFrequency { get; set; } + + /// + /// Whether to enable counters for the alias. + /// + [Parameter(Mandatory = false)] + public SwitchParameter EnableCounters { get; set; } + + /// + /// Whether to disable counters for the alias. + /// + [Parameter(Mandatory = false)] + public SwitchParameter DisableCounters { get; set; } + + /// + /// Whether the alias is enabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Enabled { get; set; } + + /// + /// Whether the alias is disabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Disabled { get; set; } + + /// + /// Whether to apply the changes immediately. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Apply { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var aliasService = new AliasService(ApiClient, Logger); + + // Get current alias + var getTask = Task.Run(async () => await aliasService.GetAliasAsync(Uuid)); + var currentAlias = getTask.GetAwaiter().GetResult().Alias; + + // Create updated alias + var alias = new AliasConfig + { + Name = currentAlias.Name, + Type = currentAlias.Type, + Content = Content ?? currentAlias.Content, + Description = Description ?? currentAlias.Description, + Protocol = Protocol?.ToUpper() ?? currentAlias.Protocol, + UpdateFrequency = UpdateFrequency?.ToString() ?? currentAlias.UpdateFrequency, + }; + + // Handle enabled/disabled state + if (Enabled.IsPresent && Disabled.IsPresent) + { + WriteWarning("Both -Enabled and -Disabled parameters were specified. Using -Enabled."); + alias.Enabled = "1"; + } + else if (Enabled.IsPresent) + { + alias.Enabled = "1"; + } + else if (Disabled.IsPresent) + { + alias.Enabled = "0"; + } + else + { + alias.Enabled = currentAlias.Enabled; + } + + // Handle counters + if (EnableCounters.IsPresent && DisableCounters.IsPresent) + { + WriteWarning("Both -EnableCounters and -DisableCounters parameters were specified. Using -EnableCounters."); + alias.Counters = "1"; + } + else if (EnableCounters.IsPresent) + { + alias.Counters = "1"; + } + else if (DisableCounters.IsPresent) + { + alias.Counters = "0"; + } + else + { + alias.Counters = currentAlias.Counters; + } + + // Update alias + var updateTask = Task.Run(async () => await aliasService.UpdateAliasAsync(Uuid, alias)); + var updateResult = updateTask.GetAwaiter().GetResult(); + + WriteVerbose($"Alias {Uuid} updated: {updateResult.Result}"); + + // Apply changes if requested + if (Apply.IsPresent) + { + var applyTask = Task.Run(async () => await aliasService.ReconfigureAliasesAsync()); + var applyResult = applyTask.GetAwaiter().GetResult(); + + WriteVerbose($"Alias changes applied: {applyResult.Status}"); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseCronJobCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseCronJobCmdlet.cs new file mode 100644 index 0000000..c5c413e --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseCronJobCmdlet.cs @@ -0,0 +1,158 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Updates a cron job on an OPNSense firewall. + /// The Set-OPNSenseCronJob cmdlet updates a cron job on an OPNSense firewall. + /// + /// Example 1: Update a cron job's description + /// Set-OPNSenseCronJob -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Description "Updated backup job" + /// This example updates the description of a cron job. + /// + /// + /// Example 2: Update a cron job's schedule + /// Set-OPNSenseCronJob -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Hours "3" -Minutes "30" + /// This example updates the schedule of a cron job to run at 3:30 AM. + /// + /// + [Cmdlet(VerbsCommon.Set, "OPNSenseCronJob")] + [OutputType(typeof(void))] + public class SetOPNSenseCronJobCmdlet : OPNSenseBaseCmdlet + { + /// + /// The UUID of the cron job to update. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// The description of the cron job. + /// + [Parameter(Mandatory = false)] + public string Description { get; set; } + + /// + /// The command to run. + /// + [Parameter(Mandatory = false)] + public string Command { get; set; } + + /// + /// The minutes part of the cron schedule (0-59, *, */5, etc.). + /// + [Parameter(Mandatory = false)] + public string Minutes { get; set; } + + /// + /// The hours part of the cron schedule (0-23, *, */2, etc.). + /// + [Parameter(Mandatory = false)] + public string Hours { get; set; } + + /// + /// The days part of the cron schedule (1-31, *, */2, etc.). + /// + [Parameter(Mandatory = false)] + public string Days { get; set; } + + /// + /// The months part of the cron schedule (1-12, *, */2, etc.). + /// + [Parameter(Mandatory = false)] + public string Months { get; set; } + + /// + /// The weekdays part of the cron schedule (0-6, *, etc.). + /// + [Parameter(Mandatory = false)] + public string Weekdays { get; set; } + + /// + /// Whether the cron job is enabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Enabled { get; set; } + + /// + /// Whether the cron job is disabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Disabled { get; set; } + + /// + /// Whether to apply the changes immediately. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Apply { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var cronService = new CronService(ApiClient, Logger); + + // First, get the current job + var getTask = Task.Run(async () => await cronService.GetJobAsync(Uuid)); + var currentJob = getTask.GetAwaiter().GetResult().Job; + + // Create the updated job + var job = new CronJobConfig + { + Description = Description ?? currentJob.Description, + Command = Command ?? currentJob.Command, + Minutes = Minutes ?? currentJob.Minutes, + Hours = Hours ?? currentJob.Hours, + Days = Days ?? currentJob.Days, + Months = Months ?? currentJob.Months, + Weekdays = Weekdays ?? currentJob.Weekdays + }; + + // Handle enabled/disabled state + if (Enabled.IsPresent && Disabled.IsPresent) + { + WriteWarning("Both -Enabled and -Disabled parameters were specified. Using -Enabled."); + job.Enabled = "1"; + } + else if (Enabled.IsPresent) + { + job.Enabled = "1"; + } + else if (Disabled.IsPresent) + { + job.Enabled = "0"; + } + else + { + job.Enabled = currentJob.Enabled; + } + + // Update the job + var updateTask = Task.Run(async () => await cronService.UpdateJobAsync(Uuid, job)); + var updateResult = updateTask.GetAwaiter().GetResult(); + + WriteVerbose($"Cron job {Uuid} updated: {updateResult.Result}"); + + // Apply the changes if requested + if (Apply.IsPresent) + { + var applyTask = Task.Run(async () => await cronService.ApplyChangesAsync()); + var applyResult = applyTask.GetAwaiter().GetResult(); + + WriteVerbose($"Cron changes applied: {applyResult.Status}"); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseDHCPOptionCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseDHCPOptionCmdlet.cs new file mode 100644 index 0000000..f150502 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseDHCPOptionCmdlet.cs @@ -0,0 +1,109 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Updates a DHCP option on an OPNSense firewall. + /// The Set-OPNSenseDHCPOption cmdlet updates a DHCP option on an OPNSense firewall. + /// + /// Example 1: Update a DHCP option's value + /// Set-OPNSenseDHCPOption -Interface "lan" -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Value "192.168.1.20" -Apply + /// This example updates the value of a DHCP option. + /// + /// + [Cmdlet(VerbsCommon.Set, "OPNSenseDHCPOption")] + [OutputType(typeof(void))] + public class SetOPNSenseDHCPOptionCmdlet : OPNSenseBaseCmdlet + { + /// + /// The interface name. + /// + [Parameter(Mandatory = true, Position = 0)] + [ValidateNotNullOrEmpty] + public string Interface { get; set; } + + /// + /// The UUID of the DHCP option to update. + /// + [Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// The option number. + /// + [Parameter(Mandatory = false)] + public string Number { get; set; } + + /// + /// The option value. + /// + [Parameter(Mandatory = false)] + public string Value { get; set; } + + /// + /// The option type. + /// + [Parameter(Mandatory = false)] + [ValidateSet("string", "text", "boolean", "array")] + public string Type { get; set; } + + /// + /// The option description. + /// + [Parameter(Mandatory = false)] + public string Description { get; set; } + + /// + /// Whether to apply the changes immediately. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Apply { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var dhcpService = new DHCPService(ApiClient, Logger); + + // Get current option + var getTask = Task.Run(async () => await dhcpService.GetOptionAsync(Interface, Uuid)); + var currentOption = getTask.GetAwaiter().GetResult().Option; + + // Create updated option + var option = new DHCPOptionConfig + { + Number = Number ?? currentOption.Number, + Value = Value ?? currentOption.Value, + Type = Type ?? currentOption.Type, + Description = Description ?? currentOption.Description + }; + + // Update option + var updateTask = Task.Run(async () => await dhcpService.UpdateOptionAsync(Interface, Uuid, option)); + var updateResult = updateTask.GetAwaiter().GetResult(); + + WriteVerbose($"DHCP option {Uuid} updated: {updateResult.Result}"); + + // Apply changes if requested + if (Apply.IsPresent) + { + var applyTask = Task.Run(async () => await dhcpService.ApplyChangesAsync()); + var applyResult = applyTask.GetAwaiter().GetResult(); + + WriteVerbose($"DHCP changes applied: {applyResult.Status}"); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseDHCPServerCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseDHCPServerCmdlet.cs new file mode 100644 index 0000000..229bd2a --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseDHCPServerCmdlet.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Updates a DHCP server on an OPNSense firewall. + /// The Set-OPNSenseDHCPServer cmdlet updates a DHCP server on an OPNSense firewall. + /// + /// Example 1: Update a DHCP server's range + /// Set-OPNSenseDHCPServer -Interface "lan" -RangeFrom "192.168.1.100" -RangeTo "192.168.1.200" + /// This example updates the DHCP range for the LAN interface. + /// + /// + /// Example 2: Update a DHCP server's DNS servers + /// Set-OPNSenseDHCPServer -Interface "lan" -DnsServers "8.8.8.8","8.8.4.4" -Domain "example.com" + /// This example updates the DNS servers and domain for the LAN interface. + /// + /// + [Cmdlet(VerbsCommon.Set, "OPNSenseDHCPServer")] + [OutputType(typeof(void))] + public class SetOPNSenseDHCPServerCmdlet : OPNSenseBaseCmdlet + { + /// + /// The interface to update the DHCP server for. + /// + [Parameter(Mandatory = true, Position = 0)] + [ValidateNotNullOrEmpty] + public string Interface { get; set; } + + /// + /// The start of the DHCP range. + /// + [Parameter(Mandatory = false)] + public string RangeFrom { get; set; } + + /// + /// The end of the DHCP range. + /// + [Parameter(Mandatory = false)] + public string RangeTo { get; set; } + + /// + /// The default lease time in seconds. + /// + [Parameter(Mandatory = false)] + public int? DefaultLeaseTime { get; set; } + + /// + /// The maximum lease time in seconds. + /// + [Parameter(Mandatory = false)] + public int? MaxLeaseTime { get; set; } + + /// + /// The domain name for DHCP clients. + /// + [Parameter(Mandatory = false)] + public string Domain { get; set; } + + /// + /// The DNS servers for DHCP clients. + /// + [Parameter(Mandatory = false)] + public string[] DnsServers { get; set; } + + /// + /// The gateway for DHCP clients. + /// + [Parameter(Mandatory = false)] + public string Gateway { get; set; } + + /// + /// Whether the DHCP server is enabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Enabled { get; set; } + + /// + /// Whether the DHCP server is disabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Disabled { get; set; } + + /// + /// Whether to apply the changes immediately. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Apply { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var dhcpService = new DHCPService(ApiClient, Logger); + + // First, get the current server configuration + var getTask = Task.Run(async () => await dhcpService.GetServerAsync(Interface)); + var currentServer = getTask.GetAwaiter().GetResult().Server; + + // Create the updated configuration + var server = new DHCPServerConfig + { + RangeFrom = RangeFrom ?? currentServer.RangeFrom, + RangeTo = RangeTo ?? currentServer.RangeTo, + DefaultLeaseTime = DefaultLeaseTime?.ToString() ?? currentServer.DefaultLeaseTime, + MaxLeaseTime = MaxLeaseTime?.ToString() ?? currentServer.MaxLeaseTime, + Domain = Domain ?? currentServer.Domain, + DnsServers = DnsServers != null ? new List(DnsServers) : currentServer.DnsServers, + Gateway = Gateway ?? currentServer.Gateway + }; + + // Handle enabled/disabled state + if (Enabled.IsPresent && Disabled.IsPresent) + { + WriteWarning("Both -Enabled and -Disabled parameters were specified. Using -Enabled."); + server.Enabled = "1"; + } + else if (Enabled.IsPresent) + { + server.Enabled = "1"; + } + else if (Disabled.IsPresent) + { + server.Enabled = "0"; + } + else + { + server.Enabled = currentServer.Enabled; + } + + // Update the server + var updateTask = Task.Run(async () => await dhcpService.UpdateServerAsync(Interface, server)); + var updateResult = updateTask.GetAwaiter().GetResult(); + + WriteVerbose($"DHCP server for interface {Interface} updated: {updateResult.Result}"); + + // Apply the changes if requested + if (Apply.IsPresent) + { + var applyTask = Task.Run(async () => await dhcpService.ApplyChangesAsync()); + var applyResult = applyTask.GetAwaiter().GetResult(); + + WriteVerbose($"DHCP changes applied: {applyResult.Status}"); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseDHCPStaticMappingCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseDHCPStaticMappingCmdlet.cs new file mode 100644 index 0000000..9abf4d8 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseDHCPStaticMappingCmdlet.cs @@ -0,0 +1,144 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Updates a DHCP static mapping on an OPNSense firewall. + /// The Set-OPNSenseDHCPStaticMapping cmdlet updates a DHCP static mapping on an OPNSense firewall. + /// + /// Example 1: Update a static mapping's hostname + /// Set-OPNSenseDHCPStaticMapping -Interface "lan" -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Hostname "new-printer" + /// This example updates the hostname of a static mapping. + /// + /// + /// Example 2: Update a static mapping's IP address + /// Set-OPNSenseDHCPStaticMapping -Interface "lan" -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -IpAddress "192.168.1.101" + /// This example updates the IP address of a static mapping. + /// + /// + [Cmdlet(VerbsCommon.Set, "OPNSenseDHCPStaticMapping")] + [OutputType(typeof(void))] + public class SetOPNSenseDHCPStaticMappingCmdlet : OPNSenseBaseCmdlet + { + /// + /// The interface of the static mapping to update. + /// + [Parameter(Mandatory = true, Position = 0)] + [ValidateNotNullOrEmpty] + public string Interface { get; set; } + + /// + /// The UUID of the static mapping to update. + /// + [Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// The MAC address of the static mapping. + /// + [Parameter(Mandatory = false)] + public string MacAddress { get; set; } + + /// + /// The IP address of the static mapping. + /// + [Parameter(Mandatory = false)] + public string IpAddress { get; set; } + + /// + /// The hostname of the static mapping. + /// + [Parameter(Mandatory = false)] + public string Hostname { get; set; } + + /// + /// The description of the static mapping. + /// + [Parameter(Mandatory = false)] + public string Description { get; set; } + + /// + /// Whether the static mapping is enabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Enabled { get; set; } + + /// + /// Whether the static mapping is disabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Disabled { get; set; } + + /// + /// Whether to apply the changes immediately. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Apply { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var dhcpService = new DHCPService(ApiClient, Logger); + + // First, get the current mapping + var getTask = Task.Run(async () => await dhcpService.GetStaticMappingAsync(Interface, Uuid)); + var currentMapping = getTask.GetAwaiter().GetResult().Mapping; + + // Create the updated mapping + var mapping = new DHCPStaticMappingConfig + { + MacAddress = MacAddress ?? currentMapping.MacAddress, + IpAddress = IpAddress ?? currentMapping.IpAddress, + Hostname = Hostname ?? currentMapping.Hostname, + Description = Description ?? currentMapping.Description + }; + + // Handle enabled/disabled state + if (Enabled.IsPresent && Disabled.IsPresent) + { + WriteWarning("Both -Enabled and -Disabled parameters were specified. Using -Enabled."); + mapping.Enabled = "1"; + } + else if (Enabled.IsPresent) + { + mapping.Enabled = "1"; + } + else if (Disabled.IsPresent) + { + mapping.Enabled = "0"; + } + else + { + mapping.Enabled = currentMapping.Enabled; + } + + // Update the mapping + var updateTask = Task.Run(async () => await dhcpService.UpdateStaticMappingAsync(Interface, Uuid, mapping)); + var updateResult = updateTask.GetAwaiter().GetResult(); + + WriteVerbose($"Static mapping {Uuid} updated: {updateResult.Result}"); + + // Apply the changes if requested + if (Apply.IsPresent) + { + var applyTask = Task.Run(async () => await dhcpService.ApplyChangesAsync()); + var applyResult = applyTask.GetAwaiter().GetResult(); + + WriteVerbose($"DHCP changes applied: {applyResult.Status}"); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseDNSForwardingCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseDNSForwardingCmdlet.cs new file mode 100644 index 0000000..e170b55 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseDNSForwardingCmdlet.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Updates the DNS forwarding configuration on an OPNSense firewall. + /// The Set-OPNSenseDNSForwarding cmdlet updates the DNS forwarding configuration on an OPNSense firewall. + /// + /// Example 1: Enable DNS forwarding + /// Set-OPNSenseDNSForwarding -Enabled -DnsServers "8.8.8.8","8.8.4.4" -Apply + /// This example enables DNS forwarding and sets the DNS servers to Google's public DNS servers. + /// + /// + /// Example 2: Disable DNS forwarding + /// Set-OPNSenseDNSForwarding -Disabled -Apply + /// This example disables DNS forwarding. + /// + /// + [Cmdlet(VerbsCommon.Set, "OPNSenseDNSForwarding")] + [OutputType(typeof(void))] + public class SetOPNSenseDNSForwardingCmdlet : OPNSenseBaseCmdlet + { + /// + /// Whether DNS forwarding is enabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Enabled { get; set; } + + /// + /// Whether DNS forwarding is disabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Disabled { get; set; } + + /// + /// The type of DNS forwarding. + /// + [Parameter(Mandatory = false)] + [ValidateSet("forward", "redirect", "forward_domain")] + public string Type { get; set; } + + /// + /// The DNS servers to forward to. + /// + [Parameter(Mandatory = false)] + public string[] DnsServers { get; set; } + + /// + /// Whether to apply the changes immediately. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Apply { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var dnsService = new DNSService(ApiClient, Logger); + + // Get current configuration + var getTask = Task.Run(async () => await dnsService.GetDNSForwardingAsync()); + var currentConfig = getTask.GetAwaiter().GetResult().Forward; + + // Create updated configuration + var config = new DNSForwardingConfig + { + Type = Type ?? currentConfig.Type + }; + + // Handle enabled/disabled state + if (Enabled.IsPresent && Disabled.IsPresent) + { + WriteWarning("Both -Enabled and -Disabled parameters were specified. Using -Enabled."); + config.Enabled = "1"; + } + else if (Enabled.IsPresent) + { + config.Enabled = "1"; + } + else if (Disabled.IsPresent) + { + config.Enabled = "0"; + } + else + { + config.Enabled = currentConfig.Enabled; + } + + // Handle DNS servers + if (DnsServers != null && DnsServers.Length > 0) + { + config.DnsServers = new List(DnsServers); + } + else + { + config.DnsServers = currentConfig.DnsServers; + } + + // Update configuration + var updateTask = Task.Run(async () => await dnsService.UpdateDNSForwardingAsync(config)); + var updateResult = updateTask.GetAwaiter().GetResult(); + + WriteVerbose($"DNS forwarding configuration updated: {updateResult.Result}"); + + // Apply changes if requested + if (Apply.IsPresent) + { + var applyTask = Task.Run(async () => await dnsService.ApplyDNSChangesAsync()); + var applyResult = applyTask.GetAwaiter().GetResult(); + + WriteVerbose($"DNS changes applied: {applyResult.Status}"); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseDNSForwardingHostCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseDNSForwardingHostCmdlet.cs new file mode 100644 index 0000000..29068e5 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseDNSForwardingHostCmdlet.cs @@ -0,0 +1,130 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Updates a DNS forwarding host on an OPNSense firewall. + /// The Set-OPNSenseDNSForwardingHost cmdlet updates a DNS forwarding host on an OPNSense firewall. + /// + /// Example 1: Update a DNS forwarding host's server + /// Set-OPNSenseDNSForwardingHost -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Server "192.168.1.20" -Apply + /// This example updates the server of a DNS forwarding host. + /// + /// + /// Example 2: Disable a DNS forwarding host + /// Set-OPNSenseDNSForwardingHost -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Disabled -Apply + /// This example disables a DNS forwarding host. + /// + /// + [Cmdlet(VerbsCommon.Set, "OPNSenseDNSForwardingHost")] + [OutputType(typeof(void))] + public class SetOPNSenseDNSForwardingHostCmdlet : OPNSenseBaseCmdlet + { + /// + /// The UUID of the DNS forwarding host to update. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// The domain to forward. + /// + [Parameter(Mandatory = false)] + public string Domain { get; set; } + + /// + /// The DNS server to forward to. + /// + [Parameter(Mandatory = false)] + public string Server { get; set; } + + /// + /// The description of the forwarding host. + /// + [Parameter(Mandatory = false)] + public string Description { get; set; } + + /// + /// Whether the forwarding host is enabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Enabled { get; set; } + + /// + /// Whether the forwarding host is disabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Disabled { get; set; } + + /// + /// Whether to apply the changes immediately. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Apply { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var dnsService = new DNSService(ApiClient, Logger); + + // Get current host + var getTask = Task.Run(async () => await dnsService.GetDNSForwardingHostAsync(Uuid)); + var currentHost = getTask.GetAwaiter().GetResult().Host; + + // Create updated host + var host = new DNSForwardingHostConfig + { + Domain = Domain ?? currentHost.Domain, + Server = Server ?? currentHost.Server, + Description = Description ?? currentHost.Description + }; + + // Handle enabled/disabled state + if (Enabled.IsPresent && Disabled.IsPresent) + { + WriteWarning("Both -Enabled and -Disabled parameters were specified. Using -Enabled."); + host.Enabled = "1"; + } + else if (Enabled.IsPresent) + { + host.Enabled = "1"; + } + else if (Disabled.IsPresent) + { + host.Enabled = "0"; + } + else + { + host.Enabled = currentHost.Enabled; + } + + // Update host + var updateTask = Task.Run(async () => await dnsService.UpdateDNSForwardingHostAsync(Uuid, host)); + var updateResult = updateTask.GetAwaiter().GetResult(); + + WriteVerbose($"DNS forwarding host {Uuid} updated: {updateResult.Result}"); + + // Apply changes if requested + if (Apply.IsPresent) + { + var applyTask = Task.Run(async () => await dnsService.ApplyDNSChangesAsync()); + var applyResult = applyTask.GetAwaiter().GetResult(); + + WriteVerbose($"DNS changes applied: {applyResult.Status}"); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseDNSServerCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseDNSServerCmdlet.cs new file mode 100644 index 0000000..90c3a01 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseDNSServerCmdlet.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Updates DNS server configuration on an OPNSense firewall. + /// The Set-OPNSenseDNSServer cmdlet updates DNS server configuration on an OPNSense firewall. + /// + /// Example 1: Enable DNS forwarding + /// Set-OPNSenseDNSServer -Forwarding -Forwarders "8.8.8.8","8.8.4.4" + /// This example enables DNS forwarding and sets Google DNS servers as forwarders. + /// + /// + /// Example 2: Update DNS server port + /// Set-OPNSenseDNSServer -Port 5353 + /// This example changes the DNS server port to 5353. + /// + /// + [Cmdlet(VerbsCommon.Set, "OPNSenseDNSServer")] + [OutputType(typeof(void))] + public class SetOPNSenseDNSServerCmdlet : OPNSenseBaseCmdlet + { + /// + /// Whether the DNS server is enabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Enabled { get; set; } + + /// + /// The DNS server port. + /// + [Parameter(Mandatory = false)] + [ValidateRange(1, 65535)] + public int? Port { get; set; } + + /// + /// The interfaces to listen on. + /// + [Parameter(Mandatory = false)] + public string[] Interfaces { get; set; } + + /// + /// Whether DNS forwarding is enabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Forwarding { get; set; } + + /// + /// The DNS forwarders to use. + /// + [Parameter(Mandatory = false)] + public string[] Forwarders { get; set; } + + /// + /// Whether to register DHCP leases. + /// + [Parameter(Mandatory = false)] + public SwitchParameter RegisterDhcp { get; set; } + + /// + /// The domain to use for DHCP registrations. + /// + [Parameter(Mandatory = false)] + public string RegisterDhcpDomain { get; set; } + + /// + /// Whether to register static DHCP leases. + /// + [Parameter(Mandatory = false)] + public SwitchParameter RegisterDhcpStatic { get; set; } + + /// + /// Whether to apply the changes immediately. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Apply { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var dnsService = new DNSService(ApiClient, Logger); + + // First, get the current DNS configuration + var getTask = Task.Run(async () => await dnsService.GetDNSConfigAsync()); + var currentConfig = getTask.GetAwaiter().GetResult().Unbound; + + // Create the updated configuration + var dnsConfig = new DNSConfig + { + Enabled = MyInvocation.BoundParameters.ContainsKey(nameof(Enabled)) ? (Enabled.IsPresent ? "1" : "0") : currentConfig.Enabled, + Port = Port?.ToString() ?? currentConfig.Port, + Interfaces = Interfaces != null ? new List(Interfaces) : currentConfig.Interfaces, + Forwarding = MyInvocation.BoundParameters.ContainsKey(nameof(Forwarding)) ? (Forwarding.IsPresent ? "1" : "0") : currentConfig.Forwarding, + Forwarders = Forwarders != null ? new List(Forwarders) : currentConfig.Forwarders, + RegisterDhcp = MyInvocation.BoundParameters.ContainsKey(nameof(RegisterDhcp)) ? (RegisterDhcp.IsPresent ? "1" : "0") : currentConfig.RegisterDhcp, + RegisterDhcpDomain = RegisterDhcpDomain ?? currentConfig.RegisterDhcpDomain, + RegisterDhcpStatic = MyInvocation.BoundParameters.ContainsKey(nameof(RegisterDhcpStatic)) ? (RegisterDhcpStatic.IsPresent ? "1" : "0") : currentConfig.RegisterDhcpStatic, + ActiveInterfaces = currentConfig.ActiveInterfaces + }; + + // Update the DNS configuration + var updateTask = Task.Run(async () => await dnsService.UpdateDNSConfigAsync(dnsConfig)); + var updateResult = updateTask.GetAwaiter().GetResult(); + + WriteVerbose($"DNS server configuration updated: {updateResult.Result}"); + + // Apply the changes if requested + if (Apply.IsPresent) + { + var applyTask = Task.Run(async () => await dnsService.ApplyDNSChangesAsync()); + var applyResult = applyTask.GetAwaiter().GetResult(); + + WriteVerbose($"DNS changes applied: {applyResult.Status}"); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseFirewallRuleCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseFirewallRuleCmdlet.cs new file mode 100644 index 0000000..84ea326 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseFirewallRuleCmdlet.cs @@ -0,0 +1,152 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Updates a firewall rule on an OPNSense firewall. + /// The Set-OPNSenseFirewallRule cmdlet updates a firewall rule on an OPNSense firewall. + /// + /// Example 1: Update a firewall rule description + /// Set-OPNSenseFirewallRule -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Description "Updated rule description" + /// This example updates the description of a firewall rule. + /// + /// + /// Example 2: Update a firewall rule protocol and ports + /// Set-OPNSenseFirewallRule -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Protocol TCP -DestinationPort 443 + /// This example updates the protocol and destination port of a firewall rule. + /// + /// + [Cmdlet(VerbsCommon.Set, "OPNSenseFirewallRule")] + [OutputType(typeof(void))] + public class SetOPNSenseFirewallRuleCmdlet : OPNSenseBaseCmdlet + { + /// + /// The UUID of the firewall rule to update. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// The description of the firewall rule. + /// + [Parameter(Mandatory = false)] + public string Description { get; set; } + + /// + /// The protocol of the firewall rule. + /// + [Parameter(Mandatory = false)] + [ValidateSet("any", "TCP", "UDP", "ICMP", "ESP", "AH", "GRE", "IGMP", "PIM", "OSPF")] + public string Protocol { get; set; } + + /// + /// The source network of the firewall rule. + /// + [Parameter(Mandatory = false)] + public string SourceNet { get; set; } + + /// + /// The source port of the firewall rule. + /// + [Parameter(Mandatory = false)] + public string SourcePort { get; set; } + + /// + /// The destination network of the firewall rule. + /// + [Parameter(Mandatory = false)] + public string DestinationNet { get; set; } + + /// + /// The destination port of the firewall rule. + /// + [Parameter(Mandatory = false)] + public string DestinationPort { get; set; } + + /// + /// The action of the firewall rule. + /// + [Parameter(Mandatory = false)] + [ValidateSet("pass", "block", "reject")] + public string Action { get; set; } + + /// + /// The sequence of the firewall rule. + /// + [Parameter(Mandatory = false)] + public string Sequence { get; set; } + + /// + /// Whether the firewall rule is enabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Enabled { get; set; } + + /// + /// Whether the firewall rule is disabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Disabled { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var firewallService = new FirewallService(ApiClient, Logger); + + // First, get the current rule + var getTask = Task.Run(async () => await firewallService.GetRuleAsync(Uuid)); + var currentRule = getTask.GetAwaiter().GetResult().Rule; + + // Create the updated rule + var rule = new FirewallRuleCreate + { + Description = Description ?? currentRule.Description, + Protocol = Protocol ?? currentRule.Protocol, + SourceNet = SourceNet ?? currentRule.SourceNet, + SourcePort = SourcePort ?? currentRule.SourcePort, + DestinationNet = DestinationNet ?? currentRule.DestinationNet, + DestinationPort = DestinationPort ?? currentRule.DestinationPort, + Action = Action ?? currentRule.Action, + Sequence = Sequence ?? currentRule.Sequence + }; + + // Handle enabled/disabled state + if (Enabled.IsPresent && Disabled.IsPresent) + { + WriteWarning("Both -Enabled and -Disabled parameters were specified. Using -Enabled."); + rule.Enabled = "1"; + } + else if (Enabled.IsPresent) + { + rule.Enabled = "1"; + } + else if (Disabled.IsPresent) + { + rule.Enabled = "0"; + } + else + { + rule.Enabled = currentRule.Enabled; + } + + // Update the rule + var updateTask = Task.Run(async () => await firewallService.UpdateRuleAsync(Uuid, rule)); + var updateResult = updateTask.GetAwaiter().GetResult(); + + WriteVerbose($"Firewall rule {Uuid} updated: {updateResult.Result}"); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseGatewayCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseGatewayCmdlet.cs new file mode 100644 index 0000000..0cf6c28 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseGatewayCmdlet.cs @@ -0,0 +1,253 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Updates a gateway on an OPNSense firewall. + /// The Set-OPNSenseGateway cmdlet updates a gateway on an OPNSense firewall. + /// + /// Example 1: Update a gateway's monitor IP + /// Set-OPNSenseGateway -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -MonitorIp "8.8.8.8" -Apply + /// This example updates the monitor IP of a gateway. + /// + /// + /// Example 2: Make a gateway the default gateway + /// Set-OPNSenseGateway -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Default -Apply + /// This example makes a gateway the default gateway. + /// + /// + [Cmdlet(VerbsCommon.Set, "OPNSenseGateway")] + [OutputType(typeof(void))] + public class SetOPNSenseGatewayCmdlet : OPNSenseBaseCmdlet + { + /// + /// The UUID of the gateway to update. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// The name of the gateway. + /// + [Parameter(Mandatory = false)] + public string Name { get; set; } + + /// + /// The interface of the gateway. + /// + [Parameter(Mandatory = false)] + public string Interface { get; set; } + + /// + /// The IP address of the gateway. + /// + [Parameter(Mandatory = false)] + public string IpAddress { get; set; } + + /// + /// The monitor IP of the gateway. + /// + [Parameter(Mandatory = false)] + public string MonitorIp { get; set; } + + /// + /// The description of the gateway. + /// + [Parameter(Mandatory = false)] + public string Description { get; set; } + + /// + /// Whether the gateway is the default gateway. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Default { get; set; } + + /// + /// Whether the gateway is not the default gateway. + /// + [Parameter(Mandatory = false)] + public SwitchParameter NotDefault { get; set; } + + /// + /// Whether the gateway is enabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Enabled { get; set; } + + /// + /// Whether the gateway is disabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Disabled { get; set; } + + /// + /// The weight of the gateway. + /// + [Parameter(Mandatory = false)] + [ValidateRange(1, 30)] + public int? Weight { get; set; } + + /// + /// The IP protocol of the gateway. + /// + [Parameter(Mandatory = false)] + [ValidateSet("inet", "inet6")] + public string IpProtocol { get; set; } + + /// + /// Whether to enable monitoring for the gateway. + /// + [Parameter(Mandatory = false)] + public SwitchParameter EnableMonitoring { get; set; } + + /// + /// Whether to disable monitoring for the gateway. + /// + [Parameter(Mandatory = false)] + public SwitchParameter DisableMonitoring { get; set; } + + /// + /// Whether to force the gateway down. + /// + [Parameter(Mandatory = false)] + public SwitchParameter ForceDown { get; set; } + + /// + /// Whether to not force the gateway down. + /// + [Parameter(Mandatory = false)] + public SwitchParameter NoForceDown { get; set; } + + /// + /// Whether to apply the changes immediately. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Apply { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var gatewayService = new GatewayService(ApiClient, Logger); + + // Get current gateway + var getTask = Task.Run(async () => await gatewayService.GetGatewayAsync(Uuid)); + var currentGateway = getTask.GetAwaiter().GetResult().Gateway; + + // Create updated gateway + var gateway = new GatewayConfig + { + Name = Name ?? currentGateway.Name, + Interface = Interface ?? currentGateway.Interface, + IpAddress = IpAddress ?? currentGateway.IpAddress, + MonitorIp = MonitorIp ?? currentGateway.MonitorIp, + Description = Description ?? currentGateway.Description, + Weight = Weight?.ToString() ?? currentGateway.Weight, + IpProtocol = IpProtocol ?? currentGateway.IpProtocol + }; + + // Handle default/not default + if (Default.IsPresent && NotDefault.IsPresent) + { + WriteWarning("Both -Default and -NotDefault parameters were specified. Using -Default."); + gateway.IsDefault = "1"; + } + else if (Default.IsPresent) + { + gateway.IsDefault = "1"; + } + else if (NotDefault.IsPresent) + { + gateway.IsDefault = "0"; + } + else + { + gateway.IsDefault = currentGateway.IsDefault; + } + + // Handle enabled/disabled + if (Enabled.IsPresent && Disabled.IsPresent) + { + WriteWarning("Both -Enabled and -Disabled parameters were specified. Using -Enabled."); + gateway.Disabled = "0"; + } + else if (Enabled.IsPresent) + { + gateway.Disabled = "0"; + } + else if (Disabled.IsPresent) + { + gateway.Disabled = "1"; + } + else + { + gateway.Disabled = currentGateway.Disabled; + } + + // Handle monitoring + if (EnableMonitoring.IsPresent && DisableMonitoring.IsPresent) + { + WriteWarning("Both -EnableMonitoring and -DisableMonitoring parameters were specified. Using -EnableMonitoring."); + gateway.MonitorDisable = "0"; + } + else if (EnableMonitoring.IsPresent) + { + gateway.MonitorDisable = "0"; + } + else if (DisableMonitoring.IsPresent) + { + gateway.MonitorDisable = "1"; + } + else + { + gateway.MonitorDisable = currentGateway.MonitorDisable; + } + + // Handle force down + if (ForceDown.IsPresent && NoForceDown.IsPresent) + { + WriteWarning("Both -ForceDown and -NoForceDown parameters were specified. Using -ForceDown."); + gateway.ForceDown = "1"; + } + else if (ForceDown.IsPresent) + { + gateway.ForceDown = "1"; + } + else if (NoForceDown.IsPresent) + { + gateway.ForceDown = "0"; + } + else + { + gateway.ForceDown = currentGateway.ForceDown; + } + + // Update gateway + var updateTask = Task.Run(async () => await gatewayService.UpdateGatewayAsync(Uuid, gateway)); + var updateResult = updateTask.GetAwaiter().GetResult(); + + WriteVerbose($"Gateway {Uuid} updated: {updateResult.Result}"); + + // Apply changes if requested + if (Apply.IsPresent) + { + var applyTask = Task.Run(async () => await gatewayService.ApplyGatewayChangesAsync()); + var applyResult = applyTask.GetAwaiter().GetResult(); + + WriteVerbose($"Gateway changes applied: {applyResult.Status}"); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseInterfaceCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseInterfaceCmdlet.cs new file mode 100644 index 0000000..140fb99 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseInterfaceCmdlet.cs @@ -0,0 +1,131 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Updates an interface on an OPNSense firewall. + /// The Set-OPNSenseInterface cmdlet updates an interface on an OPNSense firewall. + /// + /// Example 1: Update an interface description + /// Set-OPNSenseInterface -Name "lan" -Description "Local Area Network" + /// This example updates the description of the LAN interface. + /// + /// + /// Example 2: Update an interface IP address + /// Set-OPNSenseInterface -Name "lan" -IpAddress "192.168.1.1" -SubnetMask "24" + /// This example updates the IP address and subnet mask of the LAN interface. + /// + /// + [Cmdlet(VerbsCommon.Set, "OPNSenseInterface")] + [OutputType(typeof(void))] + public class SetOPNSenseInterfaceCmdlet : OPNSenseBaseCmdlet + { + /// + /// The name of the interface to update. + /// + [Parameter(Mandatory = true, Position = 0)] + [ValidateNotNullOrEmpty] + public string Name { get; set; } + + /// + /// The description of the interface. + /// + [Parameter(Mandatory = false)] + public string Description { get; set; } + + /// + /// The IP address of the interface. + /// + [Parameter(Mandatory = false)] + public string IpAddress { get; set; } + + /// + /// The subnet mask of the interface. + /// + [Parameter(Mandatory = false)] + public string SubnetMask { get; set; } + + /// + /// Whether the interface is a DHCP client. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Dhcp { get; set; } + + /// + /// The gateway of the interface. + /// + [Parameter(Mandatory = false)] + public string Gateway { get; set; } + + /// + /// The MTU of the interface. + /// + [Parameter(Mandatory = false)] + public string Mtu { get; set; } + + /// + /// The speed/duplex of the interface. + /// + [Parameter(Mandatory = false)] + public string Media { get; set; } + + /// + /// Whether the interface is enabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Enabled { get; set; } = true; + + /// + /// Whether to apply the changes immediately. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Apply { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var interfaceService = new InterfaceService(ApiClient, Logger); + + // First, get the current interface configuration + var getTask = Task.Run(async () => await interfaceService.GetInterfaceDetailAsync(Name)); + var currentInterface = getTask.GetAwaiter().GetResult().Interface; + + // Create the updated configuration + var interfaceConfig = new InterfaceConfig + { + Description = Description ?? currentInterface.Description, + IpAddress = IpAddress ?? currentInterface.IpAddress, + SubnetMask = SubnetMask ?? currentInterface.SubnetMask, + Gateway = Gateway ?? currentInterface.Gateway, + Enabled = Enabled.IsPresent ? "1" : "0" + }; + + // Update the interface + var updateTask = Task.Run(async () => await interfaceService.UpdateInterfaceAsync(Name, interfaceConfig)); + var updateResult = updateTask.GetAwaiter().GetResult(); + + WriteVerbose($"Interface {Name} updated: {updateResult.Result}"); + + // Apply the changes if requested + if (Apply.IsPresent) + { + var restartTask = Task.Run(async () => await interfaceService.RestartInterfaceAsync(Name)); + var restartResult = restartTask.GetAwaiter().GetResult(); + + WriteVerbose($"Interface {Name} restarted: {restartResult.Status}"); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/SetOPNSensePortForwardingRuleCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/SetOPNSensePortForwardingRuleCmdlet.cs new file mode 100644 index 0000000..de694db --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/SetOPNSensePortForwardingRuleCmdlet.cs @@ -0,0 +1,202 @@ +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Models; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Updates an existing port forwarding rule on an OPNSense firewall. + /// The Set-OPNSensePortForwardingRule cmdlet updates an existing port forwarding rule on an OPNSense firewall. + /// + /// Update a port forwarding rule + /// Set-OPNSensePortForwardingRule -Uuid "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" -TargetIP "192.168.1.200" -Description "Updated Web Server" + /// + /// + [Cmdlet(VerbsCommon.Set, "OPNSensePortForwardingRule", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Medium)] + [OutputType(typeof(PortForwardingRule))] + public class SetOPNSensePortForwardingRuleCmdlet : OPNSenseCmdlet + { + /// + /// The UUID of the port forwarding rule to update. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)] + public string Uuid { get; set; } + + /// + /// Whether the rule is enabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Enabled { get; set; } + + /// + /// The description of the rule. + /// + [Parameter(Mandatory = false)] + public string Description { get; set; } + + /// + /// The interface for the rule. + /// + [Parameter(Mandatory = false)] + public string Interface { get; set; } + + /// + /// The TCP/IP protocol (tcp, udp, tcp/udp). + /// + [Parameter(Mandatory = false)] + [ValidateSet("tcp", "udp", "tcp/udp")] + public string Protocol { get; set; } + + /// + /// The source address for the rule. + /// + [Parameter(Mandatory = false)] + public string Source { get; set; } + + /// + /// The source port range for the rule. + /// + [Parameter(Mandatory = false)] + public string SourcePort { get; set; } + + /// + /// The destination address for the rule. + /// + [Parameter(Mandatory = false)] + public string Destination { get; set; } + + /// + /// The destination port range for the rule. + /// + [Parameter(Mandatory = false)] + public string DestinationPort { get; set; } + + /// + /// The target IP address for the rule. + /// + [Parameter(Mandatory = false)] + public string TargetIP { get; set; } + + /// + /// The target port for the rule. + /// + [Parameter(Mandatory = false)] + public string TargetPort { get; set; } + + /// + /// The NAT reflection mode (enable, disable, purenat). + /// + [Parameter(Mandatory = false)] + [ValidateSet("enable", "disable", "purenat")] + public string NatReflection { get; set; } + + /// + /// Whether to create an associated filter rule. + /// + [Parameter(Mandatory = false)] + public SwitchParameter FilterRuleAssociation { get; set; } + + /// + /// Whether to enable logging for this rule. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Log { get; set; } + + /// + /// Suppresses the confirmation prompt. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Force { get; set; } + + /// + /// Returns the updated rule. + /// + [Parameter(Mandatory = false)] + public SwitchParameter PassThru { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + var portForwardingService = new PortForwardingService(SessionState.ApiClient); + + // Get the existing rule + var existingRule = Task.Run(async () => await portForwardingService.GetPortForwardingRuleAsync(Uuid)).GetAwaiter().GetResult(); + + if (existingRule == null) + { + WriteError(new ErrorRecord( + new PSArgumentException($"Port forwarding rule with UUID '{Uuid}' not found."), + "PortForwardingRuleNotFound", + ErrorCategory.ObjectNotFound, + Uuid)); + return; + } + + // Update the rule with the provided parameters + if (MyInvocation.BoundParameters.ContainsKey(nameof(Enabled))) + existingRule.Enabled = Enabled; + + if (MyInvocation.BoundParameters.ContainsKey(nameof(Description))) + existingRule.Description = Description; + + if (MyInvocation.BoundParameters.ContainsKey(nameof(Interface))) + existingRule.Interface = Interface; + + if (MyInvocation.BoundParameters.ContainsKey(nameof(Protocol))) + existingRule.Protocol = Protocol; + + if (MyInvocation.BoundParameters.ContainsKey(nameof(Source))) + existingRule.Source = Source; + + if (MyInvocation.BoundParameters.ContainsKey(nameof(SourcePort))) + existingRule.SourcePort = SourcePort; + + if (MyInvocation.BoundParameters.ContainsKey(nameof(Destination))) + existingRule.Destination = Destination; + + if (MyInvocation.BoundParameters.ContainsKey(nameof(DestinationPort))) + existingRule.DestinationPort = DestinationPort; + + if (MyInvocation.BoundParameters.ContainsKey(nameof(TargetIP))) + existingRule.TargetIP = TargetIP; + + if (MyInvocation.BoundParameters.ContainsKey(nameof(TargetPort))) + existingRule.TargetPort = TargetPort; + + if (MyInvocation.BoundParameters.ContainsKey(nameof(NatReflection))) + existingRule.NatReflection = NatReflection; + + if (MyInvocation.BoundParameters.ContainsKey(nameof(FilterRuleAssociation))) + existingRule.FilterRuleAssociation = FilterRuleAssociation; + + if (MyInvocation.BoundParameters.ContainsKey(nameof(Log))) + existingRule.Log = Log; + + if (Force || ShouldProcess($"OPNSense firewall", $"Update port forwarding rule with UUID '{Uuid}'")) + { + var success = Task.Run(async () => await portForwardingService.UpdatePortForwardingRuleAsync(existingRule)).GetAwaiter().GetResult(); + + if (success) + { + WriteVerbose($"Port forwarding rule with UUID '{Uuid}' updated successfully."); + + if (PassThru) + { + WriteObject(existingRule); + } + } + else + { + WriteError(new ErrorRecord( + new PSInvalidOperationException($"Failed to update port forwarding rule with UUID '{Uuid}'."), + "PortForwardingRuleUpdateFailed", + ErrorCategory.InvalidOperation, + existingRule)); + } + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseRouteCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseRouteCmdlet.cs new file mode 100644 index 0000000..0030ff6 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseRouteCmdlet.cs @@ -0,0 +1,130 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Updates a route on an OPNSense firewall. + /// The Set-OPNSenseRoute cmdlet updates a route on an OPNSense firewall. + /// + /// Example 1: Update a route's gateway + /// Set-OPNSenseRoute -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Gateway "WAN2_GW" -Apply + /// This example updates the gateway of a route. + /// + /// + /// Example 2: Enable a disabled route + /// Set-OPNSenseRoute -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Enabled -Apply + /// This example enables a previously disabled route. + /// + /// + [Cmdlet(VerbsCommon.Set, "OPNSenseRoute")] + [OutputType(typeof(void))] + public class SetOPNSenseRouteCmdlet : OPNSenseBaseCmdlet + { + /// + /// The UUID of the route to update. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// The network of the route. + /// + [Parameter(Mandatory = false)] + public string Network { get; set; } + + /// + /// The gateway of the route. + /// + [Parameter(Mandatory = false)] + public string Gateway { get; set; } + + /// + /// The description of the route. + /// + [Parameter(Mandatory = false)] + public string Description { get; set; } + + /// + /// Whether the route is enabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Enabled { get; set; } + + /// + /// Whether the route is disabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Disabled { get; set; } + + /// + /// Whether to apply the changes immediately. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Apply { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var routeService = new RouteService(ApiClient, Logger); + + // Get current route + var getTask = Task.Run(async () => await routeService.GetRouteAsync(Uuid)); + var currentRoute = getTask.GetAwaiter().GetResult().Route; + + // Create updated route + var route = new RouteConfig + { + Network = Network ?? currentRoute.Network, + Gateway = Gateway ?? currentRoute.Gateway, + Description = Description ?? currentRoute.Description + }; + + // Handle enabled/disabled state + if (Enabled.IsPresent && Disabled.IsPresent) + { + WriteWarning("Both -Enabled and -Disabled parameters were specified. Using -Enabled."); + route.Disabled = "0"; + } + else if (Enabled.IsPresent) + { + route.Disabled = "0"; + } + else if (Disabled.IsPresent) + { + route.Disabled = "1"; + } + else + { + route.Disabled = currentRoute.Disabled; + } + + // Update route + var updateTask = Task.Run(async () => await routeService.UpdateRouteAsync(Uuid, route)); + var updateResult = updateTask.GetAwaiter().GetResult(); + + WriteVerbose($"Route {Uuid} updated: {updateResult.Result}"); + + // Apply changes if requested + if (Apply.IsPresent) + { + var applyTask = Task.Run(async () => await routeService.ApplyRouteChangesAsync()); + var applyResult = applyTask.GetAwaiter().GetResult(); + + WriteVerbose($"Route changes applied: {applyResult.Status}"); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseSystemDNSCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseSystemDNSCmdlet.cs new file mode 100644 index 0000000..0d399f8 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseSystemDNSCmdlet.cs @@ -0,0 +1,185 @@ +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Updates the system DNS configuration on an OPNSense firewall. + /// The Set-OPNSenseSystemDNS cmdlet updates the system DNS configuration on an OPNSense firewall. + /// + /// Example 1: Update system DNS servers + /// Set-OPNSenseSystemDNS -DnsServers "8.8.8.8","8.8.4.4" -Apply + /// This example updates the system DNS servers to Google's public DNS servers. + /// + /// + /// Example 2: Update system hostname and domain + /// Set-OPNSenseSystemDNS -Hostname "firewall" -Domain "example.com" -Apply + /// This example updates the system hostname and domain. + /// + /// + [Cmdlet(VerbsCommon.Set, "OPNSenseSystemDNS")] + [OutputType(typeof(void))] + public class SetOPNSenseSystemDNSCmdlet : OPNSenseBaseCmdlet + { + /// + /// The hostname of the system. + /// + [Parameter(Mandatory = false)] + public string Hostname { get; set; } + + /// + /// The domain of the system. + /// + [Parameter(Mandatory = false)] + public string Domain { get; set; } + + /// + /// The DNS servers of the system. + /// + [Parameter(Mandatory = false)] + public string[] DnsServers { get; set; } + + /// + /// Whether to allow DNS server override by DHCP/PPP. + /// + [Parameter(Mandatory = false)] + public SwitchParameter AllowDnsOverride { get; set; } + + /// + /// Whether to disallow DNS server override by DHCP/PPP. + /// + [Parameter(Mandatory = false)] + public SwitchParameter DisallowDnsOverride { get; set; } + + /// + /// Whether to disable DNS rebinding protection. + /// + [Parameter(Mandatory = false)] + public SwitchParameter DisableDnsRebindProtection { get; set; } + + /// + /// Whether to enable DNS rebinding protection. + /// + [Parameter(Mandatory = false)] + public SwitchParameter EnableDnsRebindProtection { get; set; } + + /// + /// The timezone of the system. + /// + [Parameter(Mandatory = false)] + public string Timezone { get; set; } + + /// + /// The time servers of the system. + /// + [Parameter(Mandatory = false)] + public string TimeServers { get; set; } + + /// + /// The language of the system. + /// + [Parameter(Mandatory = false)] + public string Language { get; set; } + + /// + /// Whether to apply the changes immediately. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Apply { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var systemDNSService = new SystemDNSService(ApiClient, Logger); + + // Get current configuration + var getTask = Task.Run(async () => await systemDNSService.GetSystemDNSAsync()); + var currentConfig = getTask.GetAwaiter().GetResult().System; + + // Create updated configuration + var config = new SystemDNSConfig + { + Hostname = Hostname ?? currentConfig.Hostname, + Domain = Domain ?? currentConfig.Domain, + Timezone = Timezone ?? currentConfig.Timezone, + TimeServers = TimeServers ?? currentConfig.TimeServers, + Language = Language ?? currentConfig.Language + }; + + // Handle DNS servers + if (DnsServers != null && DnsServers.Length > 0) + { + config.DnsServers = new List(DnsServers); + } + else + { + config.DnsServers = currentConfig.DnsServers; + } + + // Handle DNS override + if (AllowDnsOverride.IsPresent && DisallowDnsOverride.IsPresent) + { + WriteWarning("Both -AllowDnsOverride and -DisallowDnsOverride parameters were specified. Using -AllowDnsOverride."); + config.DnsAllowOverride = "1"; + } + else if (AllowDnsOverride.IsPresent) + { + config.DnsAllowOverride = "1"; + } + else if (DisallowDnsOverride.IsPresent) + { + config.DnsAllowOverride = "0"; + } + else + { + config.DnsAllowOverride = currentConfig.DnsAllowOverride; + } + + // Handle DNS rebind protection + if (DisableDnsRebindProtection.IsPresent && EnableDnsRebindProtection.IsPresent) + { + WriteWarning("Both -DisableDnsRebindProtection and -EnableDnsRebindProtection parameters were specified. Using -DisableDnsRebindProtection."); + config.DnssecStripped = "1"; + } + else if (DisableDnsRebindProtection.IsPresent) + { + config.DnssecStripped = "1"; + } + else if (EnableDnsRebindProtection.IsPresent) + { + config.DnssecStripped = "0"; + } + else + { + config.DnssecStripped = currentConfig.DnssecStripped; + } + + // Update configuration + var updateTask = Task.Run(async () => await systemDNSService.UpdateSystemDNSAsync(config)); + var updateResult = updateTask.GetAwaiter().GetResult(); + + WriteVerbose($"System DNS configuration updated: {updateResult.Result}"); + + // Apply changes if requested + if (Apply.IsPresent) + { + var applyTask = Task.Run(async () => await systemDNSService.ApplySystemDNSChangesAsync()); + var applyResult = applyTask.GetAwaiter().GetResult(); + + WriteVerbose($"System DNS changes applied: {applyResult.Status}"); + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseUserCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseUserCmdlet.cs new file mode 100644 index 0000000..ec85614 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseUserCmdlet.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Updates a user on an OPNSense firewall. + /// The Set-OPNSenseUser cmdlet updates a user on an OPNSense firewall. + /// + /// Example 1: Update a user's email + /// Set-OPNSenseUser -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Email "newemail@example.com" + /// This example updates the email of a user. + /// + /// + /// Example 2: Update a user's password + /// Set-OPNSenseUser -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Password "NewP@ssw0rd" + /// This example updates the password of a user. + /// + /// + [Cmdlet(VerbsCommon.Set, "OPNSenseUser")] + [OutputType(typeof(void))] + public class SetOPNSenseUserCmdlet : OPNSenseBaseCmdlet + { + /// + /// The UUID of the user to update. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// The password of the user. + /// + [Parameter(Mandatory = false)] + public string Password { get; set; } + + /// + /// The full name of the user. + /// + [Parameter(Mandatory = false)] + public string FullName { get; set; } + + /// + /// The email of the user. + /// + [Parameter(Mandatory = false)] + public string Email { get; set; } + + /// + /// The groups of the user. + /// + [Parameter(Mandatory = false)] + public string[] Groups { get; set; } + + /// + /// The authorizations of the user. + /// + [Parameter(Mandatory = false)] + public string[] Authorizations { get; set; } + + /// + /// Whether the user is enabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Enabled { get; set; } + + /// + /// Whether the user is disabled. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Disabled { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var userService = new UserService(ApiClient, Logger); + + // First, get the current user + var getTask = Task.Run(async () => await userService.GetUserAsync(Uuid)); + var currentUser = getTask.GetAwaiter().GetResult().User; + + // Create the updated user + var user = new UserConfig + { + Username = currentUser.Username, + Password = Password ?? "", + FullName = FullName ?? currentUser.FullName, + Email = Email ?? currentUser.Email, + Groups = Groups != null ? new List(Groups) : currentUser.Groups, + Authorizations = Authorizations != null ? new List(Authorizations) : currentUser.Authorizations + }; + + // Handle enabled/disabled state + if (Enabled.IsPresent && Disabled.IsPresent) + { + WriteWarning("Both -Enabled and -Disabled parameters were specified. Using -Enabled."); + user.Disabled = "0"; + } + else if (Enabled.IsPresent) + { + user.Disabled = "0"; + } + else if (Disabled.IsPresent) + { + user.Disabled = "1"; + } + else + { + user.Disabled = currentUser.Disabled; + } + + // Update the user + var updateTask = Task.Run(async () => await userService.UpdateUserAsync(Uuid, user)); + var updateResult = updateTask.GetAwaiter().GetResult(); + + WriteVerbose($"User {Uuid} updated: {updateResult.Result}"); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseVLANCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseVLANCmdlet.cs new file mode 100644 index 0000000..ae5ce36 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseVLANCmdlet.cs @@ -0,0 +1,93 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Updates a VLAN on an OPNSense firewall. + /// The Set-OPNSenseVLAN cmdlet updates a VLAN on an OPNSense firewall. + /// + /// Example 1: Update a VLAN description + /// Set-OPNSenseVLAN -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Description "Updated VLAN Description" + /// This example updates the description of a VLAN. + /// + /// + /// Example 2: Update a VLAN tag and priority + /// Set-OPNSenseVLAN -Uuid "9e4ec4f0-9dd1-4fa3-8c1d-8a8e9d772b0f" -Tag 20 -Priority 3 + /// This example updates the tag and priority of a VLAN. + /// + /// + [Cmdlet(VerbsCommon.Set, "OPNSenseVLAN")] + [OutputType(typeof(void))] + public class SetOPNSenseVLANCmdlet : OPNSenseBaseCmdlet + { + /// + /// The UUID of the VLAN to update. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string Uuid { get; set; } + + /// + /// The parent interface for the VLAN. + /// + [Parameter(Mandatory = false)] + public string Interface { get; set; } + + /// + /// The VLAN tag (1-4094). + /// + [Parameter(Mandatory = false)] + [ValidateRange(1, 4094)] + public int? Tag { get; set; } + + /// + /// The priority of the VLAN (0-7). + /// + [Parameter(Mandatory = false)] + [ValidateRange(0, 7)] + public int? Priority { get; set; } + + /// + /// The description of the VLAN. + /// + [Parameter(Mandatory = false)] + public string Description { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + var interfaceService = new InterfaceService(ApiClient, Logger); + + // First, get the current VLAN configuration + var getTask = Task.Run(async () => await interfaceService.GetVLANAsync(Uuid)); + var currentVlan = getTask.GetAwaiter().GetResult().Vlan; + + // Create the updated configuration + var vlan = new VLANConfig + { + Interface = Interface ?? currentVlan.Interface, + Tag = Tag?.ToString() ?? currentVlan.Tag, + Priority = Priority?.ToString() ?? currentVlan.Priority, + Description = Description ?? currentVlan.Description + }; + + // Update the VLAN + var updateTask = Task.Run(async () => await interfaceService.UpdateVLANAsync(Uuid, vlan)); + var updateResult = updateTask.GetAwaiter().GetResult(); + + WriteVerbose($"VLAN {Uuid} updated: {updateResult.Result}"); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/UninstallOPNSensePluginCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/UninstallOPNSensePluginCmdlet.cs new file mode 100644 index 0000000..751de84 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/UninstallOPNSensePluginCmdlet.cs @@ -0,0 +1,127 @@ +using System; +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Uninstalls a plugin from an OPNSense firewall. + /// The Uninstall-OPNSensePlugin cmdlet uninstalls a plugin from an OPNSense firewall. + /// + /// Example 1: Uninstall a plugin + /// Uninstall-OPNSensePlugin -Name "os-acme-client" + /// This example uninstalls the ACME client plugin from the OPNSense firewall. + /// + /// + /// Example 2: Uninstall a plugin and wait for completion + /// Uninstall-OPNSensePlugin -Name "os-acme-client" -Wait + /// This example uninstalls the ACME client plugin from the OPNSense firewall and waits for the uninstallation to complete. + /// + /// + [Cmdlet(VerbsLifecycle.Uninstall, "OPNSensePlugin", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + [OutputType(typeof(void))] + public class UninstallOPNSensePluginCmdlet : OPNSenseBaseCmdlet + { + /// + /// The name of the plugin to uninstall. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string Name { get; set; } + + /// + /// Suppresses the confirmation prompt. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Force { get; set; } + + /// + /// Waits for the uninstallation to complete. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Wait { get; set; } + + /// + /// The timeout in seconds to wait for the uninstallation to complete. + /// + [Parameter(Mandatory = false)] + [ValidateRange(1, 3600)] + public int Timeout { get; set; } = 300; + + /// + /// The interval in seconds between status checks. + /// + [Parameter(Mandatory = false)] + [ValidateRange(1, 60)] + public int Interval { get; set; } = 5; + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + if (!Force.IsPresent && !ShouldProcess(Name, "Uninstall plugin")) + { + return; + } + + var pluginService = new PluginService(ApiClient, Logger); + + var task = Task.Run(async () => await pluginService.UninstallPluginAsync(Name)); + var result = task.GetAwaiter().GetResult(); + + WriteVerbose($"Plugin uninstallation initiated: {result.Status}"); + WriteObject($"Plugin uninstallation initiated: {result.Status}"); + + if (Wait.IsPresent) + { + WriteVerbose($"Waiting for plugin uninstallation to complete (timeout: {Timeout} seconds, interval: {Interval} seconds)"); + WriteObject("Waiting for plugin uninstallation to complete..."); + + DateTime startTime = DateTime.Now; + bool completed = false; + + while (DateTime.Now - startTime < TimeSpan.FromSeconds(Timeout)) + { + var statusTask = Task.Run(async () => await pluginService.GetPluginStatusAsync()); + var statusResult = statusTask.GetAwaiter().GetResult(); + + if (statusResult.Status == "done") + { + WriteVerbose("Plugin uninstallation completed successfully"); + WriteObject("Plugin uninstallation completed successfully"); + completed = true; + break; + } + else if (statusResult.Status == "error") + { + WriteError(new ErrorRecord( + new Exception($"Plugin uninstallation failed: {statusResult.Log}"), + "PluginUninstallationFailed", + ErrorCategory.InvalidOperation, + Name)); + completed = true; + break; + } + + WriteVerbose($"Plugin uninstallation status: {statusResult.Status}"); + Thread.Sleep(Interval * 1000); + } + + if (!completed) + { + WriteWarning($"Timed out waiting for plugin uninstallation to complete after {Timeout} seconds"); + } + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/UpdateOPNSenseFirmwareCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/UpdateOPNSenseFirmwareCmdlet.cs new file mode 100644 index 0000000..58ded89 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/UpdateOPNSenseFirmwareCmdlet.cs @@ -0,0 +1,114 @@ +using System; +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Updates the firmware on an OPNSense firewall. + /// The Update-OPNSenseFirmware cmdlet updates the firmware on an OPNSense firewall. + /// + /// Example 1: Update firmware + /// Update-OPNSenseFirmware + /// This example updates the firmware on the OPNSense firewall. + /// + /// + /// Example 2: Update firmware and wait for completion + /// Update-OPNSenseFirmware -Wait + /// This example updates the firmware on the OPNSense firewall and waits for the update to complete. + /// + /// + [Cmdlet(VerbsData.Update, "OPNSenseFirmware", SupportsShouldProcess = true)] + [OutputType(typeof(void))] + public class UpdateOPNSenseFirmwareCmdlet : OPNSenseBaseCmdlet + { + /// + /// Waits for the update to complete. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Wait { get; set; } + + /// + /// The timeout in seconds to wait for the update to complete. + /// + [Parameter(Mandatory = false)] + [ValidateRange(1, 3600)] + public int Timeout { get; set; } = 600; + + /// + /// The interval in seconds between status checks. + /// + [Parameter(Mandatory = false)] + [ValidateRange(1, 60)] + public int Interval { get; set; } = 5; + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + if (!ShouldProcess("OPNSense firewall", "Update firmware")) + { + return; + } + + var firmwareService = new FirmwareService(ApiClient, Logger); + + var task = Task.Run(async () => await firmwareService.UpdateAsync()); + var result = task.GetAwaiter().GetResult(); + + WriteVerbose($"Firmware update initiated: {result.Status}"); + WriteObject($"Firmware update initiated: {result.Status}"); + + if (Wait.IsPresent) + { + WriteVerbose($"Waiting for firmware update to complete (timeout: {Timeout} seconds, interval: {Interval} seconds)"); + WriteObject("Waiting for firmware update to complete..."); + + DateTime startTime = DateTime.Now; + bool completed = false; + + while (DateTime.Now - startTime < TimeSpan.FromSeconds(Timeout)) + { + var statusTask = Task.Run(async () => await firmwareService.GetStatusAsync()); + var statusResult = statusTask.GetAwaiter().GetResult(); + + if (statusResult.Status == "done") + { + WriteVerbose("Firmware update completed successfully"); + WriteObject("Firmware update completed successfully"); + completed = true; + break; + } + else if (statusResult.Status == "error") + { + WriteError(new ErrorRecord( + new Exception($"Firmware update failed: {statusResult.Log}"), + "FirmwareUpdateFailed", + ErrorCategory.InvalidOperation, + null)); + completed = true; + break; + } + + WriteVerbose($"Firmware update status: {statusResult.Status}"); + Thread.Sleep(Interval * 1000); + } + + if (!completed) + { + WriteWarning($"Timed out waiting for firmware update to complete after {Timeout} seconds"); + } + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Cmdlets/UpgradeOPNSenseFirmwareCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/UpgradeOPNSenseFirmwareCmdlet.cs new file mode 100644 index 0000000..e9488a6 --- /dev/null +++ b/src/PSOPNSenseAPI/Cmdlets/UpgradeOPNSenseFirmwareCmdlet.cs @@ -0,0 +1,129 @@ +using System; +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Cmdlets +{ + /// + /// Upgrades the firmware on an OPNSense firewall. + /// The Upgrade-OPNSenseFirmware cmdlet upgrades the firmware on an OPNSense firewall to a new major version. + /// + /// Example 1: Upgrade firmware + /// Upgrade-OPNSenseFirmware + /// This example upgrades the firmware on the OPNSense firewall. + /// + /// + /// Example 2: Upgrade firmware and wait for completion + /// Upgrade-OPNSenseFirmware -Wait + /// This example upgrades the firmware on the OPNSense firewall and waits for the upgrade to complete. + /// + /// + [Cmdlet(VerbsData.Update, "OPNSenseFirmware", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + [OutputType(typeof(void))] + public class UpgradeOPNSenseFirmwareCmdlet : OPNSenseBaseCmdlet + { + /// + /// Suppresses the confirmation prompt. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Force { get; set; } + + /// + /// Waits for the upgrade to complete. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Wait { get; set; } + + /// + /// The timeout in seconds to wait for the upgrade to complete. + /// + [Parameter(Mandatory = false)] + [ValidateRange(1, 3600)] + public int Timeout { get; set; } = 1200; + + /// + /// The interval in seconds between status checks. + /// + [Parameter(Mandatory = false)] + [ValidateRange(1, 60)] + public int Interval { get; set; } = 10; + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + try + { + if (!Force.IsPresent && !ShouldProcess("OPNSense firewall", "Upgrade firmware")) + { + return; + } + + var firmwareService = new FirmwareService(ApiClient, Logger); + + var task = Task.Run(async () => await firmwareService.UpgradeAsync()); + var result = task.GetAwaiter().GetResult(); + + WriteVerbose($"Firmware upgrade initiated: {result.Status}"); + WriteObject($"Firmware upgrade initiated: {result.Status}"); + + if (Wait.IsPresent) + { + WriteVerbose($"Waiting for firmware upgrade to complete (timeout: {Timeout} seconds, interval: {Interval} seconds)"); + WriteObject("Waiting for firmware upgrade to complete..."); + + DateTime startTime = DateTime.Now; + bool completed = false; + + while (DateTime.Now - startTime < TimeSpan.FromSeconds(Timeout)) + { + try + { + var statusTask = Task.Run(async () => await firmwareService.GetStatusAsync()); + var statusResult = statusTask.GetAwaiter().GetResult(); + + if (statusResult.Status == "done") + { + WriteVerbose("Firmware upgrade completed successfully"); + WriteObject("Firmware upgrade completed successfully"); + completed = true; + break; + } + else if (statusResult.Status == "error") + { + WriteError(new ErrorRecord( + new Exception($"Firmware upgrade failed: {statusResult.Log}"), + "FirmwareUpgradeFailed", + ErrorCategory.InvalidOperation, + null)); + completed = true; + break; + } + + WriteVerbose($"Firmware upgrade status: {statusResult.Status}"); + } + catch (Exception ex) + { + // The firewall might be rebooting, so we'll ignore errors during the wait + WriteVerbose($"Error checking status (firewall might be rebooting): {ex.Message}"); + } + + Thread.Sleep(Interval * 1000); + } + + if (!completed) + { + WriteWarning($"Timed out waiting for firmware upgrade to complete after {Timeout} seconds"); + } + } + } + catch (Exception ex) + { + HandleException(ex); + } + } + } +} diff --git a/src/PSOPNSenseAPI/Logging/ILogger.cs b/src/PSOPNSenseAPI/Logging/ILogger.cs new file mode 100644 index 0000000..c1ca565 --- /dev/null +++ b/src/PSOPNSenseAPI/Logging/ILogger.cs @@ -0,0 +1,38 @@ +namespace PSOPNSenseAPI.Logging +{ + /// + /// Interface for logging + /// + public interface ILogger + { + /// + /// Logs a debug message + /// + /// The message to log + void Debug(string message); + + /// + /// Logs an informational message + /// + /// The message to log + void Information(string message); + + /// + /// Logs a warning message + /// + /// The message to log + void Warning(string message); + + /// + /// Logs an error message + /// + /// The message to log + void Error(string message); + + /// + /// Logs a verbose message + /// + /// The message to log + void Verbose(string message); + } +} diff --git a/src/PSOPNSenseAPI/Logging/NullLogger.cs b/src/PSOPNSenseAPI/Logging/NullLogger.cs new file mode 100644 index 0000000..7fa6569 --- /dev/null +++ b/src/PSOPNSenseAPI/Logging/NullLogger.cs @@ -0,0 +1,53 @@ +namespace PSOPNSenseAPI.Logging +{ + /// + /// A logger that does nothing + /// + public class NullLogger : ILogger + { + /// + /// Logs a debug message + /// + /// The message to log + public void Debug(string message) + { + // Do nothing + } + + /// + /// Logs an information message + /// + /// The message to log + public void Information(string message) + { + // Do nothing + } + + /// + /// Logs a warning message + /// + /// The message to log + public void Warning(string message) + { + // Do nothing + } + + /// + /// Logs an error message + /// + /// The message to log + public void Error(string message) + { + // Do nothing + } + + /// + /// Logs a verbose message + /// + /// The message to log + public void Verbose(string message) + { + // Do nothing + } + } +} diff --git a/src/PSOPNSenseAPI/Logging/PowerShellLogger.cs b/src/PSOPNSenseAPI/Logging/PowerShellLogger.cs new file mode 100644 index 0000000..6bbb56e --- /dev/null +++ b/src/PSOPNSenseAPI/Logging/PowerShellLogger.cs @@ -0,0 +1,70 @@ +using System.Management.Automation; + +namespace PSOPNSenseAPI.Logging +{ + /// + /// Logger that uses PowerShell cmdlets for logging + /// + public class PowerShellLogger : ILogger + { + private readonly PSCmdlet _cmdlet; + + /// + /// Initializes a new instance of the class + /// + /// The PowerShell cmdlet to use for logging + public PowerShellLogger(PSCmdlet cmdlet) + { + _cmdlet = cmdlet; + } + + /// + /// Logs a debug message + /// + /// The message to log + public void Debug(string message) + { + _cmdlet.WriteDebug(message); + } + + /// + /// Logs an informational message + /// + /// The message to log + public void Information(string message) + { + _cmdlet.WriteVerbose(message); + } + + /// + /// Logs a warning message + /// + /// The message to log + public void Warning(string message) + { + _cmdlet.WriteWarning(message); + } + + /// + /// Logs an error message + /// + /// The message to log + public void Error(string message) + { + _cmdlet.WriteError(new ErrorRecord( + new System.Exception(message), + "OPNSenseApiError", + ErrorCategory.NotSpecified, + null)); + } + + /// + /// Logs a verbose message + /// + /// The message to log + public void Verbose(string message) + { + _cmdlet.WriteVerbose(message); + } + } +} diff --git a/src/PSOPNSenseAPI/Models/InterfaceConfig.cs b/src/PSOPNSenseAPI/Models/InterfaceConfig.cs new file mode 100644 index 0000000..417e379 --- /dev/null +++ b/src/PSOPNSenseAPI/Models/InterfaceConfig.cs @@ -0,0 +1,297 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace PSOPNSenseAPI.Models +{ + /// + /// Response from the OPNSense API for interface list + /// + public class InterfaceListResponse + { + /// + /// List of interfaces + /// + [JsonProperty("rows")] + public List Interfaces { get; set; } + } + + /// + /// Interface item + /// + public class InterfaceItem + { + /// + /// Interface name + /// + [JsonProperty("if")] + public string Name { get; set; } + + /// + /// Interface status + /// + [JsonProperty("status")] + public string Status { get; set; } + + /// + /// Interface IPv4 address + /// + [JsonProperty("ipv4")] + public string IPv4 { get; set; } + + /// + /// Interface IPv6 address + /// + [JsonProperty("ipv6")] + public string IPv6 { get; set; } + + /// + /// Interface MAC address + /// + [JsonProperty("mac")] + public string MAC { get; set; } + + /// + /// Interface description + /// + [JsonProperty("description")] + public string Description { get; set; } + } + + /// + /// Response from the OPNSense API for interface detail + /// + public class InterfaceDetailResponse + { + /// + /// Interface detail + /// + [JsonProperty("interface")] + public InterfaceDetail Interface { get; set; } + } + + /// + /// Interface detail + /// + public class InterfaceDetail + { + /// + /// Interface name + /// + [JsonProperty("if")] + public string Name { get; set; } + + /// + /// Interface description + /// + [JsonProperty("descr")] + public string Description { get; set; } + + /// + /// Interface status + /// + [JsonProperty("enable")] + public bool Enabled { get; set; } + + /// + /// Interface IPv4 configuration type + /// + [JsonProperty("ipv4_type")] + public string IPv4Type { get; set; } + + /// + /// Interface IPv4 address + /// + [JsonProperty("ipv4_address")] + public string IPv4Address { get; set; } + + /// + /// Interface IPv4 subnet + /// + [JsonProperty("ipv4_subnet")] + public string IPv4Subnet { get; set; } + + /// + /// Interface IPv6 configuration type + /// + [JsonProperty("ipv6_type")] + public string IPv6Type { get; set; } + + /// + /// Interface IPv6 address + /// + [JsonProperty("ipv6_address")] + public string IPv6Address { get; set; } + + /// + /// Interface IPv6 subnet + /// + [JsonProperty("ipv6_subnet")] + public string IPv6Subnet { get; set; } + + /// + /// Interface block bogon networks + /// + [JsonProperty("blockbogons")] + public bool BlockBogons { get; set; } + + /// + /// Interface block private networks + /// + [JsonProperty("blockpriv")] + public bool BlockPrivate { get; set; } + + /// + /// Interface DHCP configuration + /// + [JsonProperty("dhcp")] + public string Dhcp { get; set; } + + /// + /// Interface MTU + /// + [JsonProperty("mtu")] + public string Mtu { get; set; } + + /// + /// Interface media configuration + /// + [JsonProperty("media")] + public string Media { get; set; } + + /// + /// Interface gateway + /// + [JsonProperty("gateway")] + public string Gateway { get; set; } + + /// + /// Interface IP address + /// + [JsonProperty("ipaddr")] + public string IpAddress { get; set; } + + /// + /// Interface subnet mask + /// + [JsonProperty("subnet")] + public string SubnetMask { get; set; } + + + } + + /// + /// Interface configuration + /// + public class InterfaceConfig + { + /// + /// Interface description + /// + [JsonProperty("descr")] + public string Description { get; set; } + + /// + /// Interface status + /// + [JsonProperty("enable")] + public string Enabled { get; set; } + + /// + /// Interface IPv4 configuration type + /// + [JsonProperty("ipv4_type")] + public string IPv4Type { get; set; } + + /// + /// Interface IPv4 address + /// + [JsonProperty("ipv4_address")] + public string IPv4Address { get; set; } + + /// + /// Interface IPv4 subnet + /// + [JsonProperty("ipv4_subnet")] + public string IPv4Subnet { get; set; } + + /// + /// Interface IPv6 configuration type + /// + [JsonProperty("ipv6_type")] + public string IPv6Type { get; set; } + + /// + /// Interface IPv6 address + /// + [JsonProperty("ipv6_address")] + public string IPv6Address { get; set; } + + /// + /// Interface IPv6 subnet + /// + [JsonProperty("ipv6_subnet")] + public string IPv6Subnet { get; set; } + + /// + /// Interface block bogon networks + /// + [JsonProperty("blockbogons")] + public string BlockBogons { get; set; } + + /// + /// Interface block private networks + /// + [JsonProperty("blockpriv")] + public string BlockPrivate { get; set; } + + /// + /// Interface DHCP configuration + /// + [JsonProperty("dhcp")] + public string Dhcp { get; set; } + + /// + /// Interface MTU + /// + [JsonProperty("mtu")] + public string Mtu { get; set; } + + /// + /// Interface media configuration + /// + [JsonProperty("media")] + public string Media { get; set; } + + /// + /// Interface gateway + /// + [JsonProperty("gateway")] + public string Gateway { get; set; } + + /// + /// Interface IP address + /// + [JsonProperty("ipaddr")] + public string IpAddress { get; set; } + + /// + /// Interface subnet mask + /// + [JsonProperty("subnet")] + public string SubnetMask { get; set; } + + + } + + /// + /// Response from the OPNSense API for interface update + /// + public class InterfaceUpdateResponse + { + /// + /// Result of the update + /// + [JsonProperty("result")] + public string Result { get; set; } + } +} diff --git a/src/PSOPNSenseAPI/Models/InterfaceStatistics.cs b/src/PSOPNSenseAPI/Models/InterfaceStatistics.cs new file mode 100644 index 0000000..b89fe62 --- /dev/null +++ b/src/PSOPNSenseAPI/Models/InterfaceStatistics.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace PSOPNSenseAPI.Models +{ + /// + /// Response from the OPNSense API for interface statistics + /// + public class InterfaceStatisticsResponse + { + /// + /// Dictionary of interface statistics + /// + [JsonProperty("statistics")] + public Dictionary Statistics { get; set; } + } + + /// + /// Interface statistics + /// + public class InterfaceStatistics + { + /// + /// Number of incoming packets + /// + [JsonProperty("inpkts")] + public long InPackets { get; set; } + + /// + /// Number of incoming bytes + /// + [JsonProperty("inbytes")] + public long InBytes { get; set; } + + /// + /// Number of incoming errors + /// + [JsonProperty("inerrs")] + public long InErrors { get; set; } + + /// + /// Number of outgoing packets + /// + [JsonProperty("outpkts")] + public long OutPackets { get; set; } + + /// + /// Number of outgoing bytes + /// + [JsonProperty("outbytes")] + public long OutBytes { get; set; } + + /// + /// Number of outgoing errors + /// + [JsonProperty("outerrs")] + public long OutErrors { get; set; } + + /// + /// Number of collisions + /// + [JsonProperty("collisions")] + public long Collisions { get; set; } + } +} diff --git a/src/PSOPNSenseAPI/Models/OPNSenseApiException.cs b/src/PSOPNSenseAPI/Models/OPNSenseApiException.cs new file mode 100644 index 0000000..5c52ed8 --- /dev/null +++ b/src/PSOPNSenseAPI/Models/OPNSenseApiException.cs @@ -0,0 +1,48 @@ +using System; +using System.Net; + +namespace PSOPNSenseAPI.Models +{ + /// + /// Exception thrown when an OPNSense API request fails + /// + public class OPNSenseApiException : Exception + { + /// + /// Gets the HTTP status code of the failed request + /// + public HttpStatusCode StatusCode { get; } + + /// + /// Gets the response content of the failed request + /// + public string ResponseContent { get; } + + /// + /// Initializes a new instance of the class + /// + /// The error message + /// The HTTP status code + /// The response content + public OPNSenseApiException(string message, HttpStatusCode statusCode, string responseContent) + : base(message) + { + StatusCode = statusCode; + ResponseContent = responseContent; + } + + /// + /// Initializes a new instance of the class + /// + /// The error message + /// The HTTP status code + /// The response content + /// The inner exception + public OPNSenseApiException(string message, HttpStatusCode statusCode, string responseContent, Exception innerException) + : base(message, innerException) + { + StatusCode = statusCode; + ResponseContent = responseContent; + } + } +} diff --git a/src/PSOPNSenseAPI/Models/OPNSenseSession.cs b/src/PSOPNSenseAPI/Models/OPNSenseSession.cs new file mode 100644 index 0000000..9b42418 --- /dev/null +++ b/src/PSOPNSenseAPI/Models/OPNSenseSession.cs @@ -0,0 +1,25 @@ +using PSOPNSenseAPI.Services; + +namespace PSOPNSenseAPI.Models +{ + /// + /// Represents a session with an OPNSense firewall + /// + public static class OPNSenseSession + { + /// + /// Gets or sets the current API client + /// + public static OPNSenseApiClient Current { get; set; } + + /// + /// Gets a value indicating whether there is an active connection + /// + public static bool IsConnected => Current != null && Current.IsConnected; + + /// + /// Gets the base URL of the current connection + /// + public static string BaseUrl => Current?.BaseUrl; + } +} diff --git a/src/PSOPNSenseAPI/Models/PluginModels.cs b/src/PSOPNSenseAPI/Models/PluginModels.cs new file mode 100644 index 0000000..c46c633 --- /dev/null +++ b/src/PSOPNSenseAPI/Models/PluginModels.cs @@ -0,0 +1,203 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace PSOPNSenseAPI.Models +{ + /// + /// Response from the OPNSense API for plugin list + /// + public class PluginListResponse + { + /// + /// List of plugins + /// + [JsonProperty("rows")] + public List Rows { get; set; } + + /// + /// Total number of plugins + /// + [JsonProperty("total")] + public int Total { get; set; } + + /// + /// Current page + /// + [JsonProperty("current")] + public int Current { get; set; } + + /// + /// Number of rows per page + /// + [JsonProperty("rowCount")] + public int RowCount { get; set; } + + /// + /// Status of the operation + /// + [JsonProperty("status")] + public string Status { get; set; } + + /// + /// Status message + /// + [JsonProperty("status_msg")] + public string StatusMessage { get; set; } + } + + /// + /// Plugin item + /// + public class PluginItem + { + /// + /// Plugin name + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// Plugin version + /// + [JsonProperty("version")] + public string Version { get; set; } + + /// + /// Plugin comment + /// + [JsonProperty("comment")] + public string Comment { get; set; } + + /// + /// Plugin repository + /// + [JsonProperty("repository")] + public string Repository { get; set; } + + /// + /// Plugin origin + /// + [JsonProperty("origin")] + public string Origin { get; set; } + + /// + /// Plugin category + /// + [JsonProperty("category")] + public string Category { get; set; } + + /// + /// Plugin license + /// + [JsonProperty("license")] + public string License { get; set; } + + /// + /// Plugin maintainer + /// + [JsonProperty("maintainer")] + public string Maintainer { get; set; } + + /// + /// Plugin website + /// + [JsonProperty("www")] + public string Website { get; set; } + + /// + /// Plugin installed + /// + [JsonProperty("installed")] + public bool Installed { get; set; } + + /// + /// Plugin locked + /// + [JsonProperty("locked")] + public bool Locked { get; set; } + + /// + /// Plugin size + /// + [JsonProperty("flatsize")] + public long Size { get; set; } + + /// + /// Plugin description + /// + [JsonProperty("desc")] + public string Description { get; set; } + } + + /// + /// Response from the OPNSense API for plugin installation + /// + public class PluginInstallResponse + { + /// + /// Status of the installation + /// + [JsonProperty("status")] + public string Status { get; set; } + + /// + /// Message from the installation + /// + [JsonProperty("msg")] + public string Message { get; set; } + + /// + /// Log from the installation + /// + [JsonProperty("log")] + public string Log { get; set; } + + /// + /// UUID of the installation + /// + [JsonProperty("uuid")] + public string Uuid { get; set; } + + /// + /// Status of the installation + /// + [JsonProperty("status_msg")] + public string StatusMessage { get; set; } + } + + /// + /// Response from the OPNSense API for plugin uninstallation + /// + public class PluginUninstallResponse + { + /// + /// Status of the uninstallation + /// + [JsonProperty("status")] + public string Status { get; set; } + + /// + /// Message from the uninstallation + /// + [JsonProperty("msg")] + public string Message { get; set; } + + /// + /// Log from the uninstallation + /// + [JsonProperty("log")] + public string Log { get; set; } + + /// + /// UUID of the uninstallation + /// + [JsonProperty("uuid")] + public string Uuid { get; set; } + + /// + /// Status message + /// + [JsonProperty("status_msg")] + public string StatusMessage { get; set; } + } +} diff --git a/src/PSOPNSenseAPI/Models/PortForwardingRule.cs b/src/PSOPNSenseAPI/Models/PortForwardingRule.cs new file mode 100644 index 0000000..e0e21df --- /dev/null +++ b/src/PSOPNSenseAPI/Models/PortForwardingRule.cs @@ -0,0 +1,107 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace PSOPNSenseAPI.Models +{ + /// + /// Represents a port forwarding rule in OPNSense + /// + public class PortForwardingRule + { + /// + /// Gets or sets the UUID of the rule + /// + [JsonProperty("uuid")] + public string Uuid { get; set; } + + /// + /// Gets or sets whether the rule is enabled + /// + [JsonProperty("enabled")] + public bool Enabled { get; set; } = true; + + /// + /// Gets or sets the description of the rule + /// + [JsonProperty("descr")] + public string Description { get; set; } + + /// + /// Gets or sets the interface for the rule + /// + [JsonProperty("interface")] + public string Interface { get; set; } + + /// + /// Gets or sets the TCP/IP protocol (tcp, udp, tcp/udp) + /// + [JsonProperty("protocol")] + public string Protocol { get; set; } = "tcp"; + + /// + /// Gets or sets the source address for the rule + /// + [JsonProperty("source")] + public string Source { get; set; } = "any"; + + /// + /// Gets or sets the source port range for the rule + /// + [JsonProperty("sourceport")] + public string SourcePort { get; set; } = "any"; + + /// + /// Gets or sets the destination address for the rule + /// + [JsonProperty("destination")] + public string Destination { get; set; } = "wanip"; + + /// + /// Gets or sets the destination port range for the rule + /// + [JsonProperty("destinationport")] + public string DestinationPort { get; set; } + + /// + /// Gets or sets the target IP address for the rule + /// + [JsonProperty("target")] + public string TargetIP { get; set; } + + /// + /// Gets or sets the target port for the rule + /// + [JsonProperty("targetport")] + public string TargetPort { get; set; } + + /// + /// Gets or sets the NAT reflection mode (enable, disable, purenat) + /// + [JsonProperty("natreflection")] + public string NatReflection { get; set; } = "enable"; + + /// + /// Gets or sets whether to filter rule association is enabled + /// + [JsonProperty("associated-rule-id")] + public bool FilterRuleAssociation { get; set; } = true; + + /// + /// Gets or sets whether logging is enabled for this rule + /// + [JsonProperty("log")] + public bool Log { get; set; } = false; + } + + /// + /// Represents a collection of port forwarding rules + /// + public class PortForwardingRules + { + /// + /// Gets or sets the list of port forwarding rules + /// + [JsonProperty("rules")] + public List Rules { get; set; } = new List(); + } +} diff --git a/src/PSOPNSenseAPI/PSOPNSenseAPI.csproj b/src/PSOPNSenseAPI/PSOPNSenseAPI.csproj new file mode 100644 index 0000000..4514995 --- /dev/null +++ b/src/PSOPNSenseAPI/PSOPNSenseAPI.csproj @@ -0,0 +1,41 @@ + + + + netstandard2.0;net472 + PSOPNSenseAPI + PSOPNSenseAPI + 2025.04.14.2246 + PSOPNSenseAPI Contributors + PSOPNSenseAPI + PowerShell module for interacting with the OPNSense API to configure firewalls + Copyright (c) 2025 PSOPNSenseAPI Contributors + MIT + https://github.com/freedbygrace/PSOPNSenseAPI + https://github.com/freedbygrace/PSOPNSenseAPI + git + PowerShell;OPNSense;Firewall;API + true + latest + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/PSOPNSenseAPI/PSOPNSenseAPI.psd1 b/src/PSOPNSenseAPI/PSOPNSenseAPI.psd1 new file mode 100644 index 0000000..f82a537 --- /dev/null +++ b/src/PSOPNSenseAPI/PSOPNSenseAPI.psd1 @@ -0,0 +1,239 @@ +@{ + # Script module or binary module file associated with this manifest. + RootModule = 'PSOPNSenseAPI.dll' + + # Version number of this module. + ModuleVersion = '0.5.0' + + # Supported PSEditions + CompatiblePSEditions = @('Desktop', 'Core') + + # ID used to uniquely identify this module + GUID = '9a3b4c55-5f9a-4b8c-87d9-9a7a5cdd5c9f' + + # Author of this module + Author = 'PSOPNSenseAPI Contributors' + + # Company or vendor of this module + CompanyName = 'PSOPNSenseAPI' + + # Copyright statement for this module + Copyright = '(c) 2025 PSOPNSenseAPI Contributors. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'PowerShell module for interacting with the OPNSense API to configure firewalls' + + # Minimum version of the PowerShell engine required by this module + PowerShellVersion = '5.1' + + # Name of the PowerShell host required by this module + # PowerShellHostName = '' + + # Minimum version of the PowerShell host required by this module + # PowerShellHostVersion = '' + + # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + DotNetFrameworkVersion = '4.7.2' + + # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + ClrVersion = '4.0' + + # Processor architecture (None, X86, Amd64) required by this module + # ProcessorArchitecture = '' + + # Modules that must be imported into the global environment prior to importing this module + # RequiredModules = @() + + # Assemblies that must be loaded prior to importing this module + RequiredAssemblies = @('System.Net.Http', 'Newtonsoft.Json', 'System.Net.IPNetwork') + + # Script files (.ps1) that are run in the caller's environment prior to importing this module. + # ScriptsToProcess = @() + + # Type files (.ps1xml) to be loaded when importing this module + # TypesToProcess = @() + + # Format files (.ps1xml) to be loaded when importing this module + # FormatsToProcess = @() + + # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess + # NestedModules = @() + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = @() + + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = @( + # Connection + 'Connect-OPNSense', + 'Disconnect-OPNSense', + + # Firewall Rules + 'Get-OPNSenseFirewallRule', + 'New-OPNSenseFirewallRule', + 'Set-OPNSenseFirewallRule', + 'Remove-OPNSenseFirewallRule', + 'Enable-OPNSenseFirewallRule', + 'Disable-OPNSenseFirewallRule', + 'Apply-OPNSenseFirewallChanges', + + # Aliases + 'Get-OPNSenseAlias', + 'New-OPNSenseAlias', + 'Set-OPNSenseAlias', + 'Remove-OPNSenseAlias', + + # Interfaces + 'Get-OPNSenseInterface', + 'Set-OPNSenseInterface', + 'Get-OPNSenseVLAN', + 'New-OPNSenseVLAN', + 'Remove-OPNSenseVLAN', + 'New-OPNSenseSubnetVLANs', + + # DNS + 'Get-OPNSenseDNSServer', + 'Set-OPNSenseDNSServer', + 'Get-OPNSenseDNSOverride', + 'New-OPNSenseDNSOverride', + 'Remove-OPNSenseDNSOverride', + 'Get-OPNSenseDNSForwarding', + 'Set-OPNSenseDNSForwarding', + 'Get-OPNSenseDNSForwardingHost', + 'New-OPNSenseDNSForwardingHost', + 'Set-OPNSenseDNSForwardingHost', + 'Remove-OPNSenseDNSForwardingHost', + 'Get-OPNSenseSystemDNS', + 'Set-OPNSenseSystemDNS', + + # Configuration + 'Backup-OPNSenseConfig', + 'Restore-OPNSenseConfig', + 'Export-OPNSenseConfig', + 'Import-OPNSenseConfig', + + # Plugins + 'Get-OPNSensePlugin', + 'Install-OPNSensePlugin', + 'Uninstall-OPNSensePlugin', + 'Enable-OPNSensePlugin', + 'Disable-OPNSensePlugin', + + # Users + 'Get-OPNSenseUser', + 'New-OPNSenseUser', + 'Set-OPNSenseUser', + 'Remove-OPNSenseUser', + + # Firmware + 'Get-OPNSenseFirmware', + 'Update-OPNSenseFirmware', + 'Upgrade-OPNSenseFirmware', + + # System + 'Restart-OPNSenseFirewall', + + # DHCP + 'Get-OPNSenseDHCPServer', + 'Set-OPNSenseDHCPServer', + 'Get-OPNSenseDHCPLease', + 'Remove-OPNSenseDHCPLease', + 'Get-OPNSenseDHCPStaticMapping', + 'New-OPNSenseDHCPStaticMapping', + 'Set-OPNSenseDHCPStaticMapping', + 'Remove-OPNSenseDHCPStaticMapping', + 'Get-OPNSenseDHCPOption', + 'New-OPNSenseDHCPOption', + 'Set-OPNSenseDHCPOption', + 'Remove-OPNSenseDHCPOption', + + # Cron + 'Get-OPNSenseCronJob', + 'New-OPNSenseCronJob', + 'Set-OPNSenseCronJob', + 'Remove-OPNSenseCronJob', + 'Enable-OPNSenseCronJob', + 'Disable-OPNSenseCronJob', + + # Tailscale + 'Get-OPNSenseTailscaleStatus', + 'Enable-OPNSenseTailscale', + 'Disable-OPNSenseTailscale', + 'Connect-OPNSenseTailscale', + 'Disconnect-OPNSenseTailscale', + + # Network Utilities + 'Invoke-OPNSenseNetworkCalculation', + 'ConvertTo-OPNSenseNetworkNotation', + + # Gateways and Routes + 'Get-OPNSenseGateway', + 'New-OPNSenseGateway', + 'Set-OPNSenseGateway', + 'Remove-OPNSenseGateway', + 'Get-OPNSenseRoute', + 'New-OPNSenseRoute', + 'Set-OPNSenseRoute', + 'Remove-OPNSenseRoute', + + # Port Forwarding + 'Get-OPNSensePortForwardingRule', + 'New-OPNSensePortForwardingRule', + 'Set-OPNSensePortForwardingRule', + 'Remove-OPNSensePortForwardingRule' + ) + + # Variables to export from this module + VariablesToExport = '*' + + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = @() + + # DSC resources to export from this module + # DscResourcesToExport = @() + + # List of all modules packaged with this module + # ModuleList = @() + + # List of all files packaged with this module + # FileList = @() + + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('PowerShell', 'OPNSense', 'Firewall', 'API') + + # A URL to the license for this module. + LicenseUri = 'https://github.com/freedbygrace/PSOPNSenseAPI/blob/main/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/freedbygrace/PSOPNSenseAPI' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + ReleaseNotes = 'https://github.com/freedbygrace/PSOPNSenseAPI/blob/main/CHANGELOG.md' + + # Prerelease string of this module + # Prerelease = '' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + + } # End of PrivateData hashtable + + # HelpInfoURI of this module + # HelpInfoURI = '' + + # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. + # DefaultCommandPrefix = '' +} diff --git a/src/PSOPNSenseAPI/Services/AliasService.cs b/src/PSOPNSenseAPI/Services/AliasService.cs new file mode 100644 index 0000000..a7d2722 --- /dev/null +++ b/src/PSOPNSenseAPI/Services/AliasService.cs @@ -0,0 +1,346 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; +using PSOPNSenseAPI.Logging; + +namespace PSOPNSenseAPI.Services +{ + /// + /// Service for interacting with the OPNSense alias API + /// + public class AliasService + { + private readonly OPNSenseApiClient _apiClient; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class + /// + /// The API client + /// The logger + public AliasService(OPNSenseApiClient apiClient, ILogger logger) + { + _apiClient = apiClient; + _logger = logger; + } + + /// + /// Gets all aliases + /// + /// A list of aliases + public async Task GetAliasesAsync() + { + _logger.Information("Getting aliases"); + + var endpoint = "firewall/alias/searchItem"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Gets an alias by UUID + /// + /// The UUID of the alias + /// The alias + public async Task GetAliasAsync(string uuid) + { + _logger.Information($"Getting alias with UUID {uuid}"); + + var endpoint = $"firewall/alias/getItem/{uuid}"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Creates a new alias + /// + /// The alias to create + /// The response containing the UUID of the new alias + public async Task CreateAliasAsync(AliasConfig alias) + { + _logger.Information("Creating alias"); + + var endpoint = "firewall/alias/addItem"; + var data = new { alias = alias }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Updates an alias + /// + /// The UUID of the alias to update + /// The updated alias + /// The response indicating success + public async Task UpdateAliasAsync(string uuid, AliasConfig alias) + { + _logger.Information($"Updating alias with UUID {uuid}"); + + var endpoint = $"firewall/alias/setItem/{uuid}"; + var data = new { alias = alias }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Deletes an alias + /// + /// The UUID of the alias to delete + /// The response indicating success + public async Task DeleteAliasAsync(string uuid) + { + _logger.Information($"Deleting alias with UUID {uuid}"); + + var endpoint = $"firewall/alias/delItem/{uuid}"; + return await _apiClient.PostAsync(endpoint); + } + + /// + /// Reconfigures aliases + /// + /// The response indicating success + public async Task ReconfigureAliasesAsync() + { + _logger.Information("Reconfiguring aliases"); + + var endpoint = "firewall/alias/reconfigure"; + return await _apiClient.PostAsync(endpoint); + } + } + + /// + /// Response for getting aliases + /// + public class AliasListResponse + { + /// + /// Gets or sets the total number of aliases + /// + [JsonProperty("total")] + public int Total { get; set; } + + /// + /// Gets or sets the number of rows per page + /// + [JsonProperty("rowCount")] + public int RowCount { get; set; } + + /// + /// Gets or sets the current page + /// + [JsonProperty("current")] + public int Current { get; set; } + + /// + /// Gets or sets the aliases + /// + [JsonProperty("rows")] + public List Rows { get; set; } + } + + /// + /// Alias + /// + public class Alias + { + /// + /// Gets or sets the UUID of the alias + /// + [JsonProperty("uuid")] + public string Uuid { get; set; } + + /// + /// Gets or sets the name of the alias + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// Gets or sets the type of the alias + /// + [JsonProperty("type")] + public string Type { get; set; } + + /// + /// Gets or sets the content of the alias + /// + [JsonProperty("content")] + public string Content { get; set; } + + /// + /// Gets or sets the description of the alias + /// + [JsonProperty("description")] + public string Description { get; set; } + + /// + /// Gets or sets whether the alias is enabled + /// + [JsonProperty("enabled")] + public string Enabled { get; set; } + } + + /// + /// Response for getting an alias + /// + public class AliasResponse + { + /// + /// Gets or sets the alias + /// + [JsonProperty("alias")] + public AliasDetail Alias { get; set; } + } + + /// + /// Alias details + /// + public class AliasDetail + { + /// + /// Gets or sets the name of the alias + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// Gets or sets the type of the alias + /// + [JsonProperty("type")] + public string Type { get; set; } + + /// + /// Gets or sets the content of the alias + /// + [JsonProperty("content")] + public string Content { get; set; } + + /// + /// Gets or sets the description of the alias + /// + [JsonProperty("description")] + public string Description { get; set; } + + /// + /// Gets or sets the protocol of the alias (for port aliases) + /// + [JsonProperty("proto")] + public string Protocol { get; set; } + + /// + /// Gets or sets the update frequency of the alias (for URL aliases) + /// + [JsonProperty("updatefreq")] + public string UpdateFrequency { get; set; } + + /// + /// Gets or sets the counters of the alias + /// + [JsonProperty("counters")] + public string Counters { get; set; } + + /// + /// Gets or sets whether the alias is enabled + /// + [JsonProperty("enabled")] + public string Enabled { get; set; } + } + + /// + /// Alias configuration + /// + public class AliasConfig + { + /// + /// Gets or sets the name of the alias + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// Gets or sets the type of the alias + /// + [JsonProperty("type")] + public string Type { get; set; } + + /// + /// Gets or sets the content of the alias + /// + [JsonProperty("content")] + public string Content { get; set; } + + /// + /// Gets or sets the description of the alias + /// + [JsonProperty("description")] + public string Description { get; set; } + + /// + /// Gets or sets the protocol of the alias (for port aliases) + /// + [JsonProperty("proto")] + public string Protocol { get; set; } + + /// + /// Gets or sets the update frequency of the alias (for URL aliases) + /// + [JsonProperty("updatefreq")] + public string UpdateFrequency { get; set; } + + /// + /// Gets or sets the counters of the alias + /// + [JsonProperty("counters")] + public string Counters { get; set; } = "0"; + + /// + /// Gets or sets whether the alias is enabled + /// + [JsonProperty("enabled")] + public string Enabled { get; set; } = "1"; + } + + /// + /// Response for creating an alias + /// + public class AliasCreateResponse + { + /// + /// Gets or sets the UUID of the new alias + /// + [JsonProperty("uuid")] + public string Uuid { get; set; } + } + + /// + /// Response for updating an alias + /// + public class AliasUpdateResponse + { + /// + /// Gets or sets the result of the update + /// + [JsonProperty("result")] + public string Result { get; set; } + } + + /// + /// Response for deleting an alias + /// + public class AliasDeleteResponse + { + /// + /// Gets or sets the result of the deletion + /// + [JsonProperty("result")] + public string Result { get; set; } + } + + /// + /// Response for reconfiguring aliases + /// + public class AliasReconfigureResponse + { + /// + /// Gets or sets the status of the reconfiguration + /// + [JsonProperty("status")] + public string Status { get; set; } + } +} diff --git a/src/PSOPNSenseAPI/Services/ApiClient.cs b/src/PSOPNSenseAPI/Services/ApiClient.cs new file mode 100644 index 0000000..614b264 --- /dev/null +++ b/src/PSOPNSenseAPI/Services/ApiClient.cs @@ -0,0 +1,99 @@ +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace PSOPNSenseAPI.Services +{ + /// + /// Client for making API requests to OPNSense + /// + public class ApiClient + { + private readonly HttpClient _httpClient; + private readonly string _baseUrl; + private readonly string _apiKey; + private readonly string _apiSecret; + + /// + /// Initializes a new instance of the class + /// + /// The base URL of the OPNSense API + /// The API key + /// The API secret + /// Whether to skip certificate validation + public ApiClient(string baseUrl, string apiKey, string apiSecret, bool skipCertificateCheck = false) + { + _baseUrl = baseUrl.TrimEnd('/'); + _apiKey = apiKey; + _apiSecret = apiSecret; + + var handler = new HttpClientHandler(); + if (skipCertificateCheck) + { + handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true; + } + + _httpClient = new HttpClient(handler); + _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + // Set basic authentication + var authString = $"{apiKey}:{apiSecret}"; + var base64Auth = Convert.ToBase64String(Encoding.ASCII.GetBytes(authString)); + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64Auth); + } + + /// + /// Sends a GET request to the specified endpoint + /// + /// The API endpoint + /// The HTTP response + public async Task GetAsync(string endpoint) + { + var url = $"{_baseUrl}{endpoint}"; + return await _httpClient.GetAsync(url); + } + + /// + /// Sends a POST request to the specified endpoint + /// + /// The API endpoint + /// The data to send + /// The HTTP response + public async Task PostAsync(string endpoint, object data) + { + var url = $"{_baseUrl}{endpoint}"; + var content = data != null + ? new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json") + : null; + + return await _httpClient.PostAsync(url, content); + } + + /// + /// Sends a PUT request to the specified endpoint + /// + /// The API endpoint + /// The data to send + /// The HTTP response + public async Task PutAsync(string endpoint, object data) + { + var url = $"{_baseUrl}{endpoint}"; + var content = new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json"); + return await _httpClient.PutAsync(url, content); + } + + /// + /// Sends a DELETE request to the specified endpoint + /// + /// The API endpoint + /// The HTTP response + public async Task DeleteAsync(string endpoint) + { + var url = $"{_baseUrl}{endpoint}"; + return await _httpClient.DeleteAsync(url); + } + } +} diff --git a/src/PSOPNSenseAPI/Services/ConfigService.cs b/src/PSOPNSenseAPI/Services/ConfigService.cs new file mode 100644 index 0000000..70aa330 --- /dev/null +++ b/src/PSOPNSenseAPI/Services/ConfigService.cs @@ -0,0 +1,263 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; +using PSOPNSenseAPI.Logging; +using PSOPNSenseAPI.Models; + +namespace PSOPNSenseAPI.Services +{ + /// + /// Service for interacting with the OPNSense configuration API + /// + public class ConfigService + { + private readonly OPNSenseApiClient _apiClient; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class + /// + /// The API client + /// The logger + public ConfigService(OPNSenseApiClient apiClient, ILogger logger) + { + _apiClient = apiClient; + _logger = logger; + } + + /// + /// Gets the list of configuration backups + /// + /// A list of configuration backups + public async Task GetConfigBackupsAsync() + { + _logger.Information("Getting configuration backups"); + + var endpoint = "core/backup/getBackups"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Creates a configuration backup + /// + /// The filename for the backup + /// The response indicating success + public async Task CreateConfigBackupAsync(string filename = null) + { + _logger.Information("Creating configuration backup"); + + var endpoint = "core/backup/backup"; + var data = string.IsNullOrEmpty(filename) ? null : new { filename = filename }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Downloads a configuration backup + /// + /// The filename of the backup to download + /// The backup content + public async Task DownloadConfigBackupAsync(string filename) + { + _logger.Information($"Downloading configuration backup {filename}"); + + var endpoint = $"core/backup/download/{filename}"; + var response = await _apiClient.GetAsync(endpoint); + + if (string.IsNullOrEmpty(response.Content)) + { + throw new OPNSenseApiException("Backup content is empty", System.Net.HttpStatusCode.NoContent, ""); + } + + return Convert.FromBase64String(response.Content); + } + + /// + /// Restores a configuration backup + /// + /// The filename of the backup to restore + /// The response indicating success + public async Task RestoreConfigBackupAsync(string filename) + { + _logger.Information($"Restoring configuration backup {filename}"); + + var endpoint = "core/backup/restore"; + var data = new { filename = filename }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Deletes a configuration backup + /// + /// The filename of the backup to delete + /// The response indicating success + public async Task DeleteConfigBackupAsync(string filename) + { + _logger.Information($"Deleting configuration backup {filename}"); + + var endpoint = "core/backup/deleteBackup"; + var data = new { filename = filename }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Exports the configuration + /// + /// The exported configuration + public async Task ExportConfigAsync() + { + _logger.Information("Exporting configuration"); + + var endpoint = "core/backup/download"; + var response = await _apiClient.GetAsync(endpoint); + + if (string.IsNullOrEmpty(response.Content)) + { + throw new OPNSenseApiException("Export content is empty", System.Net.HttpStatusCode.NoContent, ""); + } + + return Convert.FromBase64String(response.Content); + } + + /// + /// Imports a configuration + /// + /// The configuration content + /// The response indicating success + public async Task ImportConfigAsync(byte[] configContent) + { + _logger.Information("Importing configuration"); + + var endpoint = "core/backup/upload"; + var base64Content = Convert.ToBase64String(configContent); + var data = new { content = base64Content }; + return await _apiClient.PostAsync(endpoint, data); + } + } + + /// + /// Response for getting configuration backups + /// + public class ConfigBackupListResponse + { + /// + /// Gets or sets the backups + /// + [JsonProperty("backups")] + public List Backups { get; set; } + } + + /// + /// Configuration backup + /// + public class ConfigBackup + { + /// + /// Gets or sets the filename of the backup + /// + [JsonProperty("filename")] + public string Filename { get; set; } + + /// + /// Gets or sets the description of the backup + /// + [JsonProperty("description")] + public string Description { get; set; } + + /// + /// Gets or sets the size of the backup + /// + [JsonProperty("filesize")] + public string FileSize { get; set; } + + /// + /// Gets or sets the date of the backup + /// + [JsonProperty("date")] + public string Date { get; set; } + + /// + /// Gets or sets the version of the backup + /// + [JsonProperty("version")] + public string Version { get; set; } + } + + /// + /// Response for creating a configuration backup + /// + public class ConfigBackupCreateResponse + { + /// + /// Gets or sets the filename of the backup + /// + [JsonProperty("filename")] + public string Filename { get; set; } + + /// + /// Gets or sets the status of the backup + /// + [JsonProperty("status")] + public string Status { get; set; } + } + + /// + /// Response for downloading a configuration backup + /// + public class ConfigBackupDownloadResponse + { + /// + /// Gets or sets the content of the backup + /// + [JsonProperty("content")] + public string Content { get; set; } + } + + /// + /// Response for restoring a configuration backup + /// + public class ConfigBackupRestoreResponse + { + /// + /// Gets or sets the status of the restore + /// + [JsonProperty("status")] + public string Status { get; set; } + } + + /// + /// Response for deleting a configuration backup + /// + public class ConfigBackupDeleteResponse + { + /// + /// Gets or sets the status of the deletion + /// + [JsonProperty("status")] + public string Status { get; set; } + } + + /// + /// Response for exporting the configuration + /// + public class ConfigExportResponse + { + /// + /// Gets or sets the content of the export + /// + [JsonProperty("content")] + public string Content { get; set; } + } + + /// + /// Response for importing a configuration + /// + public class ConfigImportResponse + { + /// + /// Gets or sets the status of the import + /// + [JsonProperty("status")] + public string Status { get; set; } + } +} diff --git a/src/PSOPNSenseAPI/Services/CronService.cs b/src/PSOPNSenseAPI/Services/CronService.cs new file mode 100644 index 0000000..d1c57ce --- /dev/null +++ b/src/PSOPNSenseAPI/Services/CronService.cs @@ -0,0 +1,390 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; +using PSOPNSenseAPI.Logging; + +namespace PSOPNSenseAPI.Services +{ + /// + /// Service for interacting with the OPNSense cron API + /// + public class CronService + { + private readonly OPNSenseApiClient _apiClient; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class + /// + /// The API client + /// The logger + public CronService(OPNSenseApiClient apiClient, ILogger logger) + { + _apiClient = apiClient; + _logger = logger; + } + + /// + /// Gets all cron jobs + /// + /// A list of cron jobs + public async Task GetJobsAsync() + { + _logger.Information("Getting cron jobs"); + + var endpoint = "cron/settings/searchJobs"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Gets a cron job by UUID + /// + /// The UUID of the cron job + /// The cron job + public async Task GetJobAsync(string uuid) + { + _logger.Information($"Getting cron job with UUID {uuid}"); + + var endpoint = $"cron/settings/getJob/{uuid}"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Creates a new cron job + /// + /// The cron job to create + /// The response containing the UUID of the new cron job + public async Task CreateJobAsync(CronJobConfig job) + { + _logger.Information("Creating cron job"); + + var endpoint = "cron/settings/addJob"; + var data = new { job = job }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Updates a cron job + /// + /// The UUID of the cron job to update + /// The updated cron job + /// The response indicating success + public async Task UpdateJobAsync(string uuid, CronJobConfig job) + { + _logger.Information($"Updating cron job with UUID {uuid}"); + + var endpoint = $"cron/settings/setJob/{uuid}"; + var data = new { job = job }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Deletes a cron job + /// + /// The UUID of the cron job to delete + /// The response indicating success + public async Task DeleteJobAsync(string uuid) + { + _logger.Information($"Deleting cron job with UUID {uuid}"); + + var endpoint = $"cron/settings/delJob/{uuid}"; + return await _apiClient.PostAsync(endpoint); + } + + /// + /// Toggles a cron job + /// + /// The UUID of the cron job to toggle + /// Whether the cron job should be enabled + /// The response indicating success + public async Task ToggleJobAsync(string uuid, bool enabled) + { + _logger.Information($"Toggling cron job with UUID {uuid} to {(enabled ? "enabled" : "disabled")}"); + + var endpoint = $"cron/settings/{(enabled ? "enable" : "disable")}Job/{uuid}"; + return await _apiClient.PostAsync(endpoint); + } + + /// + /// Applies cron changes + /// + /// The response indicating success + public async Task ApplyChangesAsync() + { + _logger.Information("Applying cron changes"); + + var endpoint = "cron/service/reconfigure"; + return await _apiClient.PostAsync(endpoint); + } + } + + /// + /// Response for getting cron jobs + /// + public class CronJobListResponse + { + /// + /// Gets or sets the total number of jobs + /// + [JsonProperty("total")] + public int Total { get; set; } + + /// + /// Gets or sets the number of rows per page + /// + [JsonProperty("rowCount")] + public int RowCount { get; set; } + + /// + /// Gets or sets the current page + /// + [JsonProperty("current")] + public int Current { get; set; } + + /// + /// Gets or sets the jobs + /// + [JsonProperty("rows")] + public List Rows { get; set; } + } + + /// + /// Cron job + /// + public class CronJob + { + /// + /// Gets or sets the UUID of the job + /// + [JsonProperty("uuid")] + public string Uuid { get; set; } + + /// + /// Gets or sets the description of the job + /// + [JsonProperty("description")] + public string Description { get; set; } + + /// + /// Gets or sets the command of the job + /// + [JsonProperty("command")] + public string Command { get; set; } + + /// + /// Gets or sets the minutes of the job + /// + [JsonProperty("minutes")] + public string Minutes { get; set; } + + /// + /// Gets or sets the hours of the job + /// + [JsonProperty("hours")] + public string Hours { get; set; } + + /// + /// Gets or sets the days of the job + /// + [JsonProperty("days")] + public string Days { get; set; } + + /// + /// Gets or sets the months of the job + /// + [JsonProperty("months")] + public string Months { get; set; } + + /// + /// Gets or sets the weekdays of the job + /// + [JsonProperty("weekdays")] + public string Weekdays { get; set; } + + /// + /// Gets or sets whether the job is enabled + /// + [JsonProperty("enabled")] + public string Enabled { get; set; } + } + + /// + /// Response for getting a cron job + /// + public class CronJobResponse + { + /// + /// Gets or sets the job + /// + [JsonProperty("job")] + public CronJobDetail Job { get; set; } + } + + /// + /// Cron job details + /// + public class CronJobDetail + { + /// + /// Gets or sets the description of the job + /// + [JsonProperty("description")] + public string Description { get; set; } + + /// + /// Gets or sets the command of the job + /// + [JsonProperty("command")] + public string Command { get; set; } + + /// + /// Gets or sets the minutes of the job + /// + [JsonProperty("minutes")] + public string Minutes { get; set; } + + /// + /// Gets or sets the hours of the job + /// + [JsonProperty("hours")] + public string Hours { get; set; } + + /// + /// Gets or sets the days of the job + /// + [JsonProperty("days")] + public string Days { get; set; } + + /// + /// Gets or sets the months of the job + /// + [JsonProperty("months")] + public string Months { get; set; } + + /// + /// Gets or sets the weekdays of the job + /// + [JsonProperty("weekdays")] + public string Weekdays { get; set; } + + /// + /// Gets or sets whether the job is enabled + /// + [JsonProperty("enabled")] + public string Enabled { get; set; } + } + + /// + /// Cron job configuration + /// + public class CronJobConfig + { + /// + /// Gets or sets the description of the job + /// + [JsonProperty("description")] + public string Description { get; set; } + + /// + /// Gets or sets the command of the job + /// + [JsonProperty("command")] + public string Command { get; set; } + + /// + /// Gets or sets the minutes of the job + /// + [JsonProperty("minutes")] + public string Minutes { get; set; } = "*"; + + /// + /// Gets or sets the hours of the job + /// + [JsonProperty("hours")] + public string Hours { get; set; } = "*"; + + /// + /// Gets or sets the days of the job + /// + [JsonProperty("days")] + public string Days { get; set; } = "*"; + + /// + /// Gets or sets the months of the job + /// + [JsonProperty("months")] + public string Months { get; set; } = "*"; + + /// + /// Gets or sets the weekdays of the job + /// + [JsonProperty("weekdays")] + public string Weekdays { get; set; } = "*"; + + /// + /// Gets or sets whether the job is enabled + /// + [JsonProperty("enabled")] + public string Enabled { get; set; } = "1"; + } + + /// + /// Response for creating a cron job + /// + public class CronJobCreateResponse + { + /// + /// Gets or sets the UUID of the new job + /// + [JsonProperty("uuid")] + public string Uuid { get; set; } + } + + /// + /// Response for updating a cron job + /// + public class CronJobUpdateResponse + { + /// + /// Gets or sets the result of the update + /// + [JsonProperty("result")] + public string Result { get; set; } + } + + /// + /// Response for deleting a cron job + /// + public class CronJobDeleteResponse + { + /// + /// Gets or sets the result of the deletion + /// + [JsonProperty("result")] + public string Result { get; set; } + } + + /// + /// Response for toggling a cron job + /// + public class CronJobToggleResponse + { + /// + /// Gets or sets the result of the toggle + /// + [JsonProperty("result")] + public string Result { get; set; } + } + + /// + /// Response for applying cron changes + /// + public class CronApplyResponse + { + /// + /// Gets or sets the status of the apply + /// + [JsonProperty("status")] + public string Status { get; set; } + } +} diff --git a/src/PSOPNSenseAPI/Services/DHCPService.cs b/src/PSOPNSenseAPI/Services/DHCPService.cs new file mode 100644 index 0000000..5f260f5 --- /dev/null +++ b/src/PSOPNSenseAPI/Services/DHCPService.cs @@ -0,0 +1,1068 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; +using PSOPNSenseAPI.Logging; + +namespace PSOPNSenseAPI.Services +{ + /// + /// Service for interacting with the OPNSense DHCP API + /// + public class DHCPService + { + private readonly OPNSenseApiClient _apiClient; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class + /// + /// The API client + /// The logger + public DHCPService(OPNSenseApiClient apiClient, ILogger logger) + { + _apiClient = apiClient; + _logger = logger; + } + + /// + /// Gets all DHCP leases + /// + /// A list of DHCP leases + public async Task GetLeasesAsync() + { + _logger.Information("Getting DHCP leases"); + + var endpoint = "dhcp/leases/searchLease"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Gets a specific DHCP lease by MAC address + /// + /// The MAC address of the lease + /// The DHCP lease + public async Task GetLeaseByMacAsync(string macAddress) + { + _logger.Information($"Getting DHCP lease for MAC address {macAddress}"); + + var endpoint = $"dhcp/leases/getLeaseByMac/{macAddress}"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Deletes a DHCP lease + /// + /// The MAC address of the lease to delete + /// The response indicating success + public async Task DeleteLeaseAsync(string macAddress) + { + _logger.Information($"Deleting DHCP lease for MAC address {macAddress}"); + + var endpoint = $"dhcp/leases/delLease/{macAddress}"; + return await _apiClient.PostAsync(endpoint); + } + + /// + /// Adds a static DHCP lease + /// + /// The interface name + /// The lease to add + /// The response containing the UUID of the new lease + public async Task AddStaticLeaseAsync(string @interface, DHCPStaticMappingConfig lease) + { + _logger.Information($"Adding static DHCP lease for interface {@interface}"); + + return await CreateStaticMappingAsync(@interface, lease); + } + + /// + /// Gets all DHCP servers + /// + /// A list of DHCP servers + public async Task GetServersAsync() + { + _logger.Information("Getting DHCP servers"); + + var endpoint = "dhcp/service/get"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Gets a DHCP server by interface + /// + /// The interface name + /// The DHCP server + public async Task GetServerAsync(string @interface) + { + _logger.Information($"Getting DHCP server for interface {@interface}"); + + var endpoint = $"dhcp/service/getServer/{@interface}"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Updates a DHCP server + /// + /// The interface name + /// The updated server + /// The response indicating success + public async Task UpdateServerAsync(string @interface, DHCPServerConfig server) + { + _logger.Information($"Updating DHCP server for interface {@interface}"); + + var endpoint = $"dhcp/service/setServer/{@interface}"; + var data = new { server = server }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Enables a DHCP server + /// + /// The interface name + /// The response indicating success + public async Task EnableServerAsync(string @interface) + { + _logger.Information($"Enabling DHCP server for interface {@interface}"); + + var endpoint = $"dhcp/service/enableServer/{@interface}"; + return await _apiClient.PostAsync(endpoint); + } + + /// + /// Disables a DHCP server + /// + /// The interface name + /// The response indicating success + public async Task DisableServerAsync(string @interface) + { + _logger.Information($"Disabling DHCP server for interface {@interface}"); + + var endpoint = $"dhcp/service/disableServer/{@interface}"; + return await _apiClient.PostAsync(endpoint); + } + + /// + /// Gets all static mappings for a DHCP server + /// + /// The interface name + /// A list of static mappings + public async Task GetStaticMappingsAsync(string @interface) + { + _logger.Information($"Getting static mappings for interface {@interface}"); + + var endpoint = $"dhcp/service/searchStaticMapping/{@interface}"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Gets a static mapping by UUID + /// + /// The interface name + /// The UUID of the static mapping + /// The static mapping + public async Task GetStaticMappingAsync(string @interface, string uuid) + { + _logger.Information($"Getting static mapping with UUID {uuid} for interface {@interface}"); + + var endpoint = $"dhcp/service/getStaticMapping/{@interface}/{uuid}"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Creates a new static mapping + /// + /// The interface name + /// The static mapping to create + /// The response containing the UUID of the new static mapping + public async Task CreateStaticMappingAsync(string @interface, DHCPStaticMappingConfig mapping) + { + _logger.Information($"Creating static mapping for interface {@interface}"); + + var endpoint = $"dhcp/service/addStaticMapping/{@interface}"; + var data = new { mapping = mapping }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Updates a static mapping + /// + /// The interface name + /// The UUID of the static mapping to update + /// The updated static mapping + /// The response indicating success + public async Task UpdateStaticMappingAsync(string @interface, string uuid, DHCPStaticMappingConfig mapping) + { + _logger.Information($"Updating static mapping with UUID {uuid} for interface {@interface}"); + + var endpoint = $"dhcp/service/setStaticMapping/{@interface}/{uuid}"; + var data = new { mapping = mapping }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Deletes a static mapping + /// + /// The interface name + /// The UUID of the static mapping to delete + /// The response indicating success + public async Task DeleteStaticMappingAsync(string @interface, string uuid) + { + _logger.Information($"Deleting static mapping with UUID {uuid} for interface {@interface}"); + + var endpoint = $"dhcp/service/delStaticMapping/{@interface}/{uuid}"; + return await _apiClient.PostAsync(endpoint); + } + + /// + /// Gets DHCP options for an interface + /// + /// The interface name + /// The DHCP options + public async Task GetOptionsAsync(string @interface) + { + _logger.Information($"Getting DHCP options for interface {@interface}"); + + var endpoint = $"dhcp/service/searchOptions/{@interface}"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Gets a DHCP option by UUID + /// + /// The interface name + /// The UUID of the option + /// The DHCP option + public async Task GetOptionAsync(string @interface, string uuid) + { + _logger.Information($"Getting DHCP option with UUID {uuid} for interface {@interface}"); + + var endpoint = $"dhcp/service/getOption/{@interface}/{uuid}"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Creates a new DHCP option + /// + /// The interface name + /// The option to create + /// The response containing the UUID of the new option + public async Task CreateOptionAsync(string @interface, DHCPOptionConfig option) + { + _logger.Information($"Creating DHCP option for interface {@interface}"); + + var endpoint = $"dhcp/service/addOption/{@interface}"; + var data = new { option = option }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Updates a DHCP option + /// + /// The interface name + /// The UUID of the option to update + /// The updated option + /// The response indicating success + public async Task UpdateOptionAsync(string @interface, string uuid, DHCPOptionConfig option) + { + _logger.Information($"Updating DHCP option with UUID {uuid} for interface {@interface}"); + + var endpoint = $"dhcp/service/setOption/{@interface}/{uuid}"; + var data = new { option = option }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Deletes a DHCP option + /// + /// The interface name + /// The UUID of the option to delete + /// The response indicating success + public async Task DeleteOptionAsync(string @interface, string uuid) + { + _logger.Information($"Deleting DHCP option with UUID {uuid} for interface {@interface}"); + + var endpoint = $"dhcp/service/delOption/{@interface}/{uuid}"; + return await _apiClient.PostAsync(endpoint); + } + + /// + /// Applies DHCP changes + /// + /// The response indicating success + public async Task ApplyChangesAsync() + { + _logger.Information("Applying DHCP changes"); + + var endpoint = "dhcp/service/reconfigure"; + return await _apiClient.PostAsync(endpoint); + } + } + + /// + /// Response for getting DHCP leases + /// + public class DHCPLeaseListResponse + { + /// + /// Gets or sets the total number of leases + /// + [JsonProperty("total")] + public int Total { get; set; } + + /// + /// Gets or sets the number of rows per page + /// + [JsonProperty("rowCount")] + public int RowCount { get; set; } + + /// + /// Gets or sets the current page + /// + [JsonProperty("current")] + public int Current { get; set; } + + /// + /// Gets or sets the leases + /// + [JsonProperty("rows")] + public List Rows { get; set; } + } + + /// + /// DHCP lease + /// + public class DHCPLease + { + /// + /// Gets or sets the MAC address of the lease + /// + [JsonProperty("mac")] + public string MacAddress { get; set; } + + /// + /// Gets or sets the IP address of the lease + /// + [JsonProperty("ip")] + public string IpAddress { get; set; } + + /// + /// Gets or sets the hostname of the lease + /// + [JsonProperty("hostname")] + public string Hostname { get; set; } + + /// + /// Gets or sets the start time of the lease + /// + [JsonProperty("start")] + public string Start { get; set; } + + /// + /// Gets or sets the end time of the lease + /// + [JsonProperty("end")] + public string End { get; set; } + + /// + /// Gets or sets the state of the lease + /// + [JsonProperty("state")] + public string State { get; set; } + + /// + /// Gets or sets the type of the lease + /// + [JsonProperty("type")] + public string Type { get; set; } + } + + /// + /// Response for getting DHCP servers + /// + public class DHCPServerListResponse + { + /// + /// Gets or sets the DHCP servers + /// + [JsonProperty("dhcpd")] + public DHCPServers Servers { get; set; } + } + + /// + /// DHCP servers + /// + public class DHCPServers + { + /// + /// Gets or sets whether DHCP is enabled + /// + [JsonProperty("enable")] + public string Enabled { get; set; } + + /// + /// Gets or sets the interfaces + /// + [JsonProperty("interfaces")] + public Dictionary Interfaces { get; set; } + } + + /// + /// DHCP server information + /// + public class DHCPServerInfo + { + /// + /// Gets or sets whether the server is enabled + /// + [JsonProperty("enable")] + public string Enabled { get; set; } + + /// + /// Gets or sets the range start + /// + [JsonProperty("range_from")] + public string RangeFrom { get; set; } + + /// + /// Gets or sets the range end + /// + [JsonProperty("range_to")] + public string RangeTo { get; set; } + + /// + /// Gets or sets the default lease time + /// + [JsonProperty("defaultleasetime")] + public string DefaultLeaseTime { get; set; } + + /// + /// Gets or sets the maximum lease time + /// + [JsonProperty("maxleasetime")] + public string MaxLeaseTime { get; set; } + + /// + /// Gets or sets the domain name + /// + [JsonProperty("domain")] + public string Domain { get; set; } + + /// + /// Gets or sets the DNS servers + /// + [JsonProperty("dnsserver")] + public List DnsServers { get; set; } + + /// + /// Gets or sets the gateway + /// + [JsonProperty("gateway")] + public string Gateway { get; set; } + } + + /// + /// Response for getting a DHCP server + /// + public class DHCPServerResponse + { + /// + /// Gets or sets the server + /// + [JsonProperty("server")] + public DHCPServerDetail Server { get; set; } + } + + /// + /// DHCP server details + /// + public class DHCPServerDetail + { + /// + /// Gets or sets whether the server is enabled + /// + [JsonProperty("enable")] + public string Enabled { get; set; } + + /// + /// Gets or sets the range start + /// + [JsonProperty("range_from")] + public string RangeFrom { get; set; } + + /// + /// Gets or sets the range end + /// + [JsonProperty("range_to")] + public string RangeTo { get; set; } + + /// + /// Gets or sets the default lease time + /// + [JsonProperty("defaultleasetime")] + public string DefaultLeaseTime { get; set; } + + /// + /// Gets or sets the maximum lease time + /// + [JsonProperty("maxleasetime")] + public string MaxLeaseTime { get; set; } + + /// + /// Gets or sets the domain name + /// + [JsonProperty("domain")] + public string Domain { get; set; } + + /// + /// Gets or sets the DNS servers + /// + [JsonProperty("dnsserver")] + public List DnsServers { get; set; } + + /// + /// Gets or sets the gateway + /// + [JsonProperty("gateway")] + public string Gateway { get; set; } + } + + /// + /// DHCP server configuration + /// + public class DHCPServerConfig + { + /// + /// Gets or sets whether the server is enabled + /// + [JsonProperty("enable")] + public string Enabled { get; set; } = "1"; + + /// + /// Gets or sets the range start + /// + [JsonProperty("range_from")] + public string RangeFrom { get; set; } + + /// + /// Gets or sets the range end + /// + [JsonProperty("range_to")] + public string RangeTo { get; set; } + + /// + /// Gets or sets the default lease time + /// + [JsonProperty("defaultleasetime")] + public string DefaultLeaseTime { get; set; } = "7200"; + + /// + /// Gets or sets the maximum lease time + /// + [JsonProperty("maxleasetime")] + public string MaxLeaseTime { get; set; } = "86400"; + + /// + /// Gets or sets the domain name + /// + [JsonProperty("domain")] + public string Domain { get; set; } + + /// + /// Gets or sets the DNS servers + /// + [JsonProperty("dnsserver")] + public List DnsServers { get; set; } + + /// + /// Gets or sets the gateway + /// + [JsonProperty("gateway")] + public string Gateway { get; set; } + } + + /// + /// Response for updating a DHCP server + /// + public class DHCPServerUpdateResponse + { + /// + /// Gets or sets the result of the update + /// + [JsonProperty("result")] + public string Result { get; set; } + } + + /// + /// Response for enabling a DHCP server + /// + public class DHCPServerEnableResponse + { + /// + /// Gets or sets the result of the enable + /// + [JsonProperty("result")] + public string Result { get; set; } + } + + /// + /// Response for disabling a DHCP server + /// + public class DHCPServerDisableResponse + { + /// + /// Gets or sets the result of the disable + /// + [JsonProperty("result")] + public string Result { get; set; } + } + + /// + /// Response for getting static mappings + /// + public class DHCPStaticMappingListResponse + { + /// + /// Gets or sets the total number of mappings + /// + [JsonProperty("total")] + public int Total { get; set; } + + /// + /// Gets or sets the number of rows per page + /// + [JsonProperty("rowCount")] + public int RowCount { get; set; } + + /// + /// Gets or sets the current page + /// + [JsonProperty("current")] + public int Current { get; set; } + + /// + /// Gets or sets the mappings + /// + [JsonProperty("rows")] + public List Rows { get; set; } + } + + /// + /// DHCP static mapping + /// + public class DHCPStaticMapping + { + /// + /// Gets or sets the UUID of the mapping + /// + [JsonProperty("uuid")] + public string Uuid { get; set; } + + /// + /// Gets or sets the MAC address of the mapping + /// + [JsonProperty("mac")] + public string MacAddress { get; set; } + + /// + /// Gets or sets the IP address of the mapping + /// + [JsonProperty("ipaddr")] + public string IpAddress { get; set; } + + /// + /// Gets or sets the hostname of the mapping + /// + [JsonProperty("hostname")] + public string Hostname { get; set; } + + /// + /// Gets or sets the description of the mapping + /// + [JsonProperty("descr")] + public string Description { get; set; } + + /// + /// Gets or sets whether the mapping is enabled + /// + [JsonProperty("enable")] + public string Enabled { get; set; } + } + + /// + /// Response for getting a static mapping + /// + public class DHCPStaticMappingResponse + { + /// + /// Gets or sets the mapping + /// + [JsonProperty("mapping")] + public DHCPStaticMappingDetail Mapping { get; set; } + } + + /// + /// DHCP static mapping details + /// + public class DHCPStaticMappingDetail + { + /// + /// Gets or sets the MAC address of the mapping + /// + [JsonProperty("mac")] + public string MacAddress { get; set; } + + /// + /// Gets or sets the IP address of the mapping + /// + [JsonProperty("ipaddr")] + public string IpAddress { get; set; } + + /// + /// Gets or sets the hostname of the mapping + /// + [JsonProperty("hostname")] + public string Hostname { get; set; } + + /// + /// Gets or sets the description of the mapping + /// + [JsonProperty("descr")] + public string Description { get; set; } + + /// + /// Gets or sets whether the mapping is enabled + /// + [JsonProperty("enable")] + public string Enabled { get; set; } + } + + /// + /// DHCP static mapping configuration + /// + public class DHCPStaticMappingConfig + { + /// + /// Gets or sets the MAC address of the mapping + /// + [JsonProperty("mac")] + public string MacAddress { get; set; } + + /// + /// Gets or sets the IP address of the mapping + /// + [JsonProperty("ipaddr")] + public string IpAddress { get; set; } + + /// + /// Gets or sets the hostname of the mapping + /// + [JsonProperty("hostname")] + public string Hostname { get; set; } + + /// + /// Gets or sets the description of the mapping + /// + [JsonProperty("descr")] + public string Description { get; set; } + + /// + /// Gets or sets whether the mapping is enabled + /// + [JsonProperty("enable")] + public string Enabled { get; set; } = "1"; + } + + /// + /// Response for creating a static mapping + /// + public class DHCPStaticMappingCreateResponse + { + /// + /// Gets or sets the UUID of the new mapping + /// + [JsonProperty("uuid")] + public string Uuid { get; set; } + } + + /// + /// Response for updating a static mapping + /// + public class DHCPStaticMappingUpdateResponse + { + /// + /// Gets or sets the result of the update + /// + [JsonProperty("result")] + public string Result { get; set; } + } + + /// + /// Response for deleting a static mapping + /// + public class DHCPStaticMappingDeleteResponse + { + /// + /// Gets or sets the result of the deletion + /// + [JsonProperty("result")] + public string Result { get; set; } + } + + /// + /// Response for applying DHCP changes + /// + public class DHCPApplyResponse + { + /// + /// Gets or sets the status of the apply + /// + [JsonProperty("status")] + public string Status { get; set; } + } + + /// + /// Response for getting a DHCP lease by MAC address + /// + public class DHCPLeaseResponse + { + /// + /// Gets or sets the lease + /// + [JsonProperty("lease")] + public DHCPLeaseDetail Lease { get; set; } + } + + /// + /// DHCP lease details + /// + public class DHCPLeaseDetail + { + /// + /// Gets or sets the MAC address of the lease + /// + [JsonProperty("mac")] + public string MacAddress { get; set; } + + /// + /// Gets or sets the IP address of the lease + /// + [JsonProperty("address")] + public string IpAddress { get; set; } + + /// + /// Gets or sets the hostname of the lease + /// + [JsonProperty("hostname")] + public string Hostname { get; set; } + + /// + /// Gets or sets the start time of the lease + /// + [JsonProperty("start")] + public string Start { get; set; } + + /// + /// Gets or sets the end time of the lease + /// + [JsonProperty("end")] + public string End { get; set; } + + /// + /// Gets or sets the state of the lease + /// + [JsonProperty("state")] + public string State { get; set; } + + /// + /// Gets or sets the type of the lease + /// + [JsonProperty("type")] + public string Type { get; set; } + } + + /// + /// Response for deleting a DHCP lease + /// + public class DHCPLeaseDeleteResponse + { + /// + /// Gets or sets the result of the deletion + /// + [JsonProperty("result")] + public string Result { get; set; } + } + + /// + /// Response for getting DHCP options + /// + public class DHCPOptionsListResponse + { + /// + /// Gets or sets the total number of options + /// + [JsonProperty("total")] + public int Total { get; set; } + + /// + /// Gets or sets the number of rows per page + /// + [JsonProperty("rowCount")] + public int RowCount { get; set; } + + /// + /// Gets or sets the current page + /// + [JsonProperty("current")] + public int Current { get; set; } + + /// + /// Gets or sets the options + /// + [JsonProperty("rows")] + public List Rows { get; set; } + } + + /// + /// DHCP option + /// + public class DHCPOption + { + /// + /// Gets or sets the UUID of the option + /// + [JsonProperty("uuid")] + public string Uuid { get; set; } + + /// + /// Gets or sets the number of the option + /// + [JsonProperty("number")] + public string Number { get; set; } + + /// + /// Gets or sets the value of the option + /// + [JsonProperty("value")] + public string Value { get; set; } + + /// + /// Gets or sets the type of the option + /// + [JsonProperty("type")] + public string Type { get; set; } + + /// + /// Gets or sets the description of the option + /// + [JsonProperty("description")] + public string Description { get; set; } + } + + /// + /// Response for getting a DHCP option + /// + public class DHCPOptionResponse + { + /// + /// Gets or sets the option + /// + [JsonProperty("option")] + public DHCPOptionDetail Option { get; set; } + } + + /// + /// DHCP option details + /// + public class DHCPOptionDetail + { + /// + /// Gets or sets the number of the option + /// + [JsonProperty("number")] + public string Number { get; set; } + + /// + /// Gets or sets the value of the option + /// + [JsonProperty("value")] + public string Value { get; set; } + + /// + /// Gets or sets the type of the option + /// + [JsonProperty("type")] + public string Type { get; set; } + + /// + /// Gets or sets the description of the option + /// + [JsonProperty("description")] + public string Description { get; set; } + } + + /// + /// DHCP option configuration + /// + public class DHCPOptionConfig + { + /// + /// Gets or sets the number of the option + /// + [JsonProperty("number")] + public string Number { get; set; } + + /// + /// Gets or sets the value of the option + /// + [JsonProperty("value")] + public string Value { get; set; } + + /// + /// Gets or sets the type of the option + /// + [JsonProperty("type")] + public string Type { get; set; } = "string"; + + /// + /// Gets or sets the description of the option + /// + [JsonProperty("description")] + public string Description { get; set; } + } + + /// + /// Response for creating a DHCP option + /// + public class DHCPOptionCreateResponse + { + /// + /// Gets or sets the UUID of the new option + /// + [JsonProperty("uuid")] + public string Uuid { get; set; } + } + + /// + /// Response for updating a DHCP option + /// + public class DHCPOptionUpdateResponse + { + /// + /// Gets or sets the result of the update + /// + [JsonProperty("result")] + public string Result { get; set; } + } + + /// + /// Response for deleting a DHCP option + /// + public class DHCPOptionDeleteResponse + { + /// + /// Gets or sets the result of the deletion + /// + [JsonProperty("result")] + public string Result { get; set; } + } +} diff --git a/src/PSOPNSenseAPI/Services/DNSService.cs b/src/PSOPNSenseAPI/Services/DNSService.cs new file mode 100644 index 0000000..c1a55c9 --- /dev/null +++ b/src/PSOPNSenseAPI/Services/DNSService.cs @@ -0,0 +1,735 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; +using PSOPNSenseAPI.Logging; + +namespace PSOPNSenseAPI.Services +{ + /// + /// Service for interacting with the OPNSense DNS API + /// + public class DNSService + { + private readonly OPNSenseApiClient _apiClient; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class + /// + /// The API client + /// The logger + public DNSService(OPNSenseApiClient apiClient, ILogger logger) + { + _apiClient = apiClient; + _logger = logger; + } + + /// + /// Gets the DNS configuration + /// + /// The DNS configuration + public async Task GetDNSConfigAsync() + { + _logger.Information("Getting DNS configuration"); + + var endpoint = "unbound/settings/get"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Updates the DNS configuration + /// + /// The updated DNS configuration + /// The response indicating success + public async Task UpdateDNSConfigAsync(DNSConfig config) + { + _logger.Information("Updating DNS configuration"); + + var endpoint = "unbound/settings/set"; + var data = new { unbound = config }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Gets all DNS overrides + /// + /// A list of DNS overrides + public async Task GetDNSOverridesAsync() + { + _logger.Information("Getting DNS overrides"); + + var endpoint = "unbound/overrides/searchHostOverride"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Gets a DNS override by UUID + /// + /// The UUID of the override + /// The DNS override + public async Task GetDNSOverrideAsync(string uuid) + { + _logger.Information($"Getting DNS override with UUID {uuid}"); + + var endpoint = $"unbound/overrides/getHostOverride/{uuid}"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Creates a new DNS override + /// + /// The override to create + /// The response containing the UUID of the new override + public async Task CreateDNSOverrideAsync(DNSOverrideConfig @override) + { + _logger.Information("Creating DNS override"); + + var endpoint = "unbound/overrides/addHostOverride"; + var data = new { host = @override }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Updates a DNS override + /// + /// The UUID of the override to update + /// The updated override + /// The response indicating success + public async Task UpdateDNSOverrideAsync(string uuid, DNSOverrideConfig @override) + { + _logger.Information($"Updating DNS override with UUID {uuid}"); + + var endpoint = $"unbound/overrides/setHostOverride/{uuid}"; + var data = new { host = @override }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Deletes a DNS override + /// + /// The UUID of the override to delete + /// The response indicating success + public async Task DeleteDNSOverrideAsync(string uuid) + { + _logger.Information($"Deleting DNS override with UUID {uuid}"); + + var endpoint = $"unbound/overrides/delHostOverride/{uuid}"; + return await _apiClient.PostAsync(endpoint); + } + + /// + /// Applies DNS configuration changes + /// + /// The response indicating success + public async Task ApplyDNSChangesAsync() + { + _logger.Information("Applying DNS changes"); + + var endpoint = "unbound/service/reconfigure"; + return await _apiClient.PostAsync(endpoint); + } + + /// + /// Gets the DNS forwarding configuration + /// + /// The DNS forwarding configuration + public async Task GetDNSForwardingAsync() + { + _logger.Information("Getting DNS forwarding configuration"); + + var endpoint = "unbound/forward/get"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Updates the DNS forwarding configuration + /// + /// The updated forwarding configuration + /// The response indicating success + public async Task UpdateDNSForwardingAsync(DNSForwardingConfig config) + { + _logger.Information("Updating DNS forwarding configuration"); + + var endpoint = "unbound/forward/set"; + var data = new { forward = config }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Gets the DNS forwarding hosts + /// + /// The DNS forwarding hosts + public async Task GetDNSForwardingHostsAsync() + { + _logger.Information("Getting DNS forwarding hosts"); + + var endpoint = "unbound/forward/searchHost"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Gets a DNS forwarding host by UUID + /// + /// The UUID of the forwarding host + /// The DNS forwarding host + public async Task GetDNSForwardingHostAsync(string uuid) + { + _logger.Information($"Getting DNS forwarding host with UUID {uuid}"); + + var endpoint = $"unbound/forward/getHost/{uuid}"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Creates a new DNS forwarding host + /// + /// The forwarding host to create + /// The response containing the UUID of the new forwarding host + public async Task CreateDNSForwardingHostAsync(DNSForwardingHostConfig host) + { + _logger.Information("Creating DNS forwarding host"); + + var endpoint = "unbound/forward/addHost"; + var data = new { host = host }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Updates a DNS forwarding host + /// + /// The UUID of the forwarding host to update + /// The updated forwarding host + /// The response indicating success + public async Task UpdateDNSForwardingHostAsync(string uuid, DNSForwardingHostConfig host) + { + _logger.Information($"Updating DNS forwarding host with UUID {uuid}"); + + var endpoint = $"unbound/forward/setHost/{uuid}"; + var data = new { host = host }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Deletes a DNS forwarding host + /// + /// The UUID of the forwarding host to delete + /// The response indicating success + public async Task DeleteDNSForwardingHostAsync(string uuid) + { + _logger.Information($"Deleting DNS forwarding host with UUID {uuid}"); + + var endpoint = $"unbound/forward/delHost/{uuid}"; + return await _apiClient.PostAsync(endpoint); + } + } + + /// + /// Response for getting DNS configuration + /// + public class DNSConfigResponse + { + /// + /// Gets or sets the DNS configuration + /// + [JsonProperty("unbound")] + public DNSConfig Unbound { get; set; } + } + + /// + /// DNS configuration + /// + public class DNSConfig + { + /// + /// Gets or sets whether DNS is enabled + /// + [JsonProperty("enable")] + public string Enabled { get; set; } + + /// + /// Gets or sets the DNS port + /// + [JsonProperty("port")] + public string Port { get; set; } + + /// + /// Gets or sets the DNS interfaces + /// + [JsonProperty("interfaces")] + public List Interfaces { get; set; } + + /// + /// Gets or sets the DNS forwarders + /// + [JsonProperty("forwarding")] + public string Forwarding { get; set; } + + /// + /// Gets or sets the DNS forwarder addresses + /// + [JsonProperty("forwarders")] + public List Forwarders { get; set; } + + /// + /// Gets or sets the DNS domain + /// + [JsonProperty("regdhcp")] + public string RegisterDhcp { get; set; } + + /// + /// Gets or sets the DNS domain + /// + [JsonProperty("regdhcpdomain")] + public string RegisterDhcpDomain { get; set; } + + /// + /// Gets or sets the DNS domain + /// + [JsonProperty("regdhcpstatic")] + public string RegisterDhcpStatic { get; set; } + + /// + /// Gets or sets the DNS domain + /// + [JsonProperty("active_interface")] + public List ActiveInterfaces { get; set; } + } + + /// + /// Response for updating DNS configuration + /// + public class DNSUpdateResponse + { + /// + /// Gets or sets the result of the update + /// + [JsonProperty("result")] + public string Result { get; set; } + } + + /// + /// Response for getting DNS overrides + /// + public class DNSOverrideListResponse + { + /// + /// Gets or sets the total number of overrides + /// + [JsonProperty("total")] + public int Total { get; set; } + + /// + /// Gets or sets the number of rows per page + /// + [JsonProperty("rowCount")] + public int RowCount { get; set; } + + /// + /// Gets or sets the current page + /// + [JsonProperty("current")] + public int Current { get; set; } + + /// + /// Gets or sets the overrides + /// + [JsonProperty("rows")] + public List Rows { get; set; } + } + + /// + /// DNS override + /// + public class DNSOverride + { + /// + /// Gets or sets the UUID of the override + /// + [JsonProperty("uuid")] + public string Uuid { get; set; } + + /// + /// Gets or sets the hostname of the override + /// + [JsonProperty("hostname")] + public string Hostname { get; set; } + + /// + /// Gets or sets the domain of the override + /// + [JsonProperty("domain")] + public string Domain { get; set; } + + /// + /// Gets or sets the IP address of the override + /// + [JsonProperty("server")] + public string IpAddress { get; set; } + + /// + /// Gets or sets the description of the override + /// + [JsonProperty("description")] + public string Description { get; set; } + + /// + /// Gets or sets whether the override is enabled + /// + [JsonProperty("enabled")] + public string Enabled { get; set; } + } + + /// + /// Response for getting a DNS override + /// + public class DNSOverrideResponse + { + /// + /// Gets or sets the override + /// + [JsonProperty("host")] + public DNSOverrideDetail Host { get; set; } + } + + /// + /// DNS override details + /// + public class DNSOverrideDetail + { + /// + /// Gets or sets whether the override is enabled + /// + [JsonProperty("enabled")] + public string Enabled { get; set; } + + /// + /// Gets or sets the hostname of the override + /// + [JsonProperty("hostname")] + public string Hostname { get; set; } + + /// + /// Gets or sets the domain of the override + /// + [JsonProperty("domain")] + public string Domain { get; set; } + + /// + /// Gets or sets the IP address of the override + /// + [JsonProperty("server")] + public string IpAddress { get; set; } + + /// + /// Gets or sets the description of the override + /// + [JsonProperty("description")] + public string Description { get; set; } + } + + /// + /// DNS override configuration + /// + public class DNSOverrideConfig + { + /// + /// Gets or sets whether the override is enabled + /// + [JsonProperty("enabled")] + public string Enabled { get; set; } = "1"; + + /// + /// Gets or sets the hostname of the override + /// + [JsonProperty("hostname")] + public string Hostname { get; set; } + + /// + /// Gets or sets the domain of the override + /// + [JsonProperty("domain")] + public string Domain { get; set; } + + /// + /// Gets or sets the IP address of the override + /// + [JsonProperty("server")] + public string IpAddress { get; set; } + + /// + /// Gets or sets the description of the override + /// + [JsonProperty("description")] + public string Description { get; set; } + } + + /// + /// Response for creating a DNS override + /// + public class DNSOverrideCreateResponse + { + /// + /// Gets or sets the UUID of the new override + /// + [JsonProperty("uuid")] + public string Uuid { get; set; } + } + + /// + /// Response for updating a DNS override + /// + public class DNSOverrideUpdateResponse + { + /// + /// Gets or sets the result of the update + /// + [JsonProperty("result")] + public string Result { get; set; } + } + + /// + /// Response for deleting a DNS override + /// + public class DNSOverrideDeleteResponse + { + /// + /// Gets or sets the result of the deletion + /// + [JsonProperty("result")] + public string Result { get; set; } + } + + /// + /// Response for applying DNS changes + /// + public class DNSApplyResponse + { + /// + /// Gets or sets the status of the apply + /// + [JsonProperty("status")] + public string Status { get; set; } + } + + /// + /// Response for getting DNS forwarding configuration + /// + public class DNSForwardingResponse + { + /// + /// Gets or sets the DNS forwarding configuration + /// + [JsonProperty("forward")] + public DNSForwardingConfig Forward { get; set; } + } + + /// + /// DNS forwarding configuration + /// + public class DNSForwardingConfig + { + /// + /// Gets or sets whether DNS forwarding is enabled + /// + [JsonProperty("enabled")] + public string Enabled { get; set; } = "0"; + + /// + /// Gets or sets the type of DNS forwarding + /// + [JsonProperty("type")] + public string Type { get; set; } = "forward"; + + /// + /// Gets or sets the DNS servers to forward to + /// + [JsonProperty("dns")] + public List DnsServers { get; set; } = new List(); + } + + /// + /// Response for updating DNS forwarding configuration + /// + public class DNSForwardingUpdateResponse + { + /// + /// Gets or sets the result of the update + /// + [JsonProperty("result")] + public string Result { get; set; } + } + + /// + /// Response for getting DNS forwarding hosts + /// + public class DNSForwardingHostListResponse + { + /// + /// Gets or sets the total number of hosts + /// + [JsonProperty("total")] + public int Total { get; set; } + + /// + /// Gets or sets the number of rows per page + /// + [JsonProperty("rowCount")] + public int RowCount { get; set; } + + /// + /// Gets or sets the current page + /// + [JsonProperty("current")] + public int Current { get; set; } + + /// + /// Gets or sets the hosts + /// + [JsonProperty("rows")] + public List Rows { get; set; } + } + + /// + /// DNS forwarding host + /// + public class DNSForwardingHost + { + /// + /// Gets or sets the UUID of the host + /// + [JsonProperty("uuid")] + public string Uuid { get; set; } + + /// + /// Gets or sets the domain of the host + /// + [JsonProperty("domain")] + public string Domain { get; set; } + + /// + /// Gets or sets the DNS servers for the host + /// + [JsonProperty("server")] + public string Server { get; set; } + + /// + /// Gets or sets the description of the host + /// + [JsonProperty("description")] + public string Description { get; set; } + + /// + /// Gets or sets whether the host is enabled + /// + [JsonProperty("enabled")] + public string Enabled { get; set; } + } + + /// + /// Response for getting a DNS forwarding host + /// + public class DNSForwardingHostResponse + { + /// + /// Gets or sets the host + /// + [JsonProperty("host")] + public DNSForwardingHostDetail Host { get; set; } + } + + /// + /// DNS forwarding host details + /// + public class DNSForwardingHostDetail + { + /// + /// Gets or sets whether the host is enabled + /// + [JsonProperty("enabled")] + public string Enabled { get; set; } + + /// + /// Gets or sets the domain of the host + /// + [JsonProperty("domain")] + public string Domain { get; set; } + + /// + /// Gets or sets the DNS servers for the host + /// + [JsonProperty("server")] + public string Server { get; set; } + + /// + /// Gets or sets the description of the host + /// + [JsonProperty("description")] + public string Description { get; set; } + } + + /// + /// DNS forwarding host configuration + /// + public class DNSForwardingHostConfig + { + /// + /// Gets or sets whether the host is enabled + /// + [JsonProperty("enabled")] + public string Enabled { get; set; } = "1"; + + /// + /// Gets or sets the domain of the host + /// + [JsonProperty("domain")] + public string Domain { get; set; } + + /// + /// Gets or sets the DNS servers for the host + /// + [JsonProperty("server")] + public string Server { get; set; } + + /// + /// Gets or sets the description of the host + /// + [JsonProperty("description")] + public string Description { get; set; } + } + + /// + /// Response for creating a DNS forwarding host + /// + public class DNSForwardingHostCreateResponse + { + /// + /// Gets or sets the UUID of the new host + /// + [JsonProperty("uuid")] + public string Uuid { get; set; } + } + + /// + /// Response for updating a DNS forwarding host + /// + public class DNSForwardingHostUpdateResponse + { + /// + /// Gets or sets the result of the update + /// + [JsonProperty("result")] + public string Result { get; set; } + } + + /// + /// Response for deleting a DNS forwarding host + /// + public class DNSForwardingHostDeleteResponse + { + /// + /// Gets or sets the result of the deletion + /// + [JsonProperty("result")] + public string Result { get; set; } + } +} diff --git a/src/PSOPNSenseAPI/Services/FirewallService.cs b/src/PSOPNSenseAPI/Services/FirewallService.cs new file mode 100644 index 0000000..5a422cd --- /dev/null +++ b/src/PSOPNSenseAPI/Services/FirewallService.cs @@ -0,0 +1,458 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; +using PSOPNSenseAPI.Logging; + +namespace PSOPNSenseAPI.Services +{ + /// + /// Service for interacting with the OPNSense firewall API + /// + public class FirewallService + { + private readonly OPNSenseApiClient _apiClient; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class + /// + /// The API client + /// The logger + public FirewallService(OPNSenseApiClient apiClient, ILogger logger) + { + _apiClient = apiClient; + _logger = logger; + } + + /// + /// Gets all firewall rules + /// + /// A list of firewall rules + public async Task GetRulesAsync(string searchPhrase = "", int page = 1, int rowCount = 100) + { + _logger.Information("Getting firewall rules"); + + var endpoint = $"firewall/filter/searchRule?current={page}&rowCount={rowCount}&searchPhrase={searchPhrase}"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Gets a firewall rule by UUID + /// + /// The UUID of the rule + /// The firewall rule + public async Task GetRuleAsync(string uuid) + { + _logger.Information($"Getting firewall rule with UUID {uuid}"); + + var endpoint = $"firewall/filter/getRule/{uuid}"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Creates a new firewall rule + /// + /// The rule to create + /// The response containing the UUID of the new rule + public async Task CreateRuleAsync(FirewallRuleCreate rule) + { + _logger.Information("Creating firewall rule"); + + var endpoint = "firewall/filter/addRule"; + var data = new { rule = rule }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Updates a firewall rule + /// + /// The UUID of the rule to update + /// The updated rule + /// The response indicating success + public async Task UpdateRuleAsync(string uuid, FirewallRuleCreate rule) + { + _logger.Information($"Updating firewall rule with UUID {uuid}"); + + var endpoint = $"firewall/filter/setRule/{uuid}"; + var data = new { rule = rule }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Deletes a firewall rule + /// + /// The UUID of the rule to delete + /// The response indicating success + public async Task DeleteRuleAsync(string uuid) + { + _logger.Information($"Deleting firewall rule with UUID {uuid}"); + + var endpoint = $"firewall/filter/delRule/{uuid}"; + return await _apiClient.PostAsync(endpoint); + } + + /// + /// Toggles a firewall rule + /// + /// The UUID of the rule to toggle + /// Whether to enable or disable the rule + /// The response indicating success + public async Task ToggleRuleAsync(string uuid, bool enabled) + { + _logger.Information($"Toggling firewall rule with UUID {uuid} to {(enabled ? "enabled" : "disabled")}"); + + var endpoint = $"firewall/filter/toggleRule/{uuid}/{(enabled ? "1" : "0")}"; + return await _apiClient.PostAsync(endpoint); + } + + /// + /// Creates a savepoint for the firewall configuration + /// + /// The response containing the savepoint revision + public async Task CreateSavepointAsync() + { + _logger.Information("Creating firewall savepoint"); + + var endpoint = "firewall/filter/savepoint"; + return await _apiClient.PostAsync(endpoint); + } + + /// + /// Applies firewall changes + /// + /// The savepoint revision to use for rollback if needed + /// The response indicating success + public async Task ApplyChangesAsync(string revision = null) + { + _logger.Information("Applying firewall changes"); + + var endpoint = revision != null ? $"firewall/filter/apply/{revision}" : "firewall/filter/apply"; + return await _apiClient.PostAsync(endpoint); + } + + /// + /// Cancels a rollback + /// + /// The savepoint revision + /// The response indicating success + public async Task CancelRollbackAsync(string revision) + { + _logger.Information($"Cancelling rollback for revision {revision}"); + + var endpoint = $"firewall/filter/cancelRollback/{revision}"; + return await _apiClient.PostAsync(endpoint); + } + } + + /// + /// Response for firewall rule search + /// + public class FirewallRuleSearchResponse + { + /// + /// Gets or sets the total number of rules + /// + [JsonProperty("total")] + public int Total { get; set; } + + /// + /// Gets or sets the number of rows per page + /// + [JsonProperty("rowCount")] + public int RowCount { get; set; } + + /// + /// Gets or sets the current page + /// + [JsonProperty("current")] + public int Current { get; set; } + + /// + /// Gets or sets the rules + /// + [JsonProperty("rows")] + public List Rows { get; set; } + } + + /// + /// Firewall rule + /// + public class FirewallRule + { + /// + /// Gets or sets the UUID of the rule + /// + [JsonProperty("uuid")] + public string Uuid { get; set; } + + /// + /// Gets or sets whether the rule is enabled + /// + [JsonProperty("enabled")] + public string Enabled { get; set; } + + /// + /// Gets or sets the sequence of the rule + /// + [JsonProperty("sequence")] + public string Sequence { get; set; } + + /// + /// Gets or sets the description of the rule + /// + [JsonProperty("description")] + public string Description { get; set; } + + /// + /// Gets or sets the protocol of the rule + /// + [JsonProperty("protocol")] + public string Protocol { get; set; } + + /// + /// Gets or sets the source network of the rule + /// + [JsonProperty("source_net")] + public string SourceNet { get; set; } + + /// + /// Gets or sets the source port of the rule + /// + [JsonProperty("source_port")] + public string SourcePort { get; set; } + + /// + /// Gets or sets the destination network of the rule + /// + [JsonProperty("destination_net")] + public string DestinationNet { get; set; } + + /// + /// Gets or sets the destination port of the rule + /// + [JsonProperty("destination_port")] + public string DestinationPort { get; set; } + + /// + /// Gets or sets the action of the rule + /// + [JsonProperty("action")] + public string Action { get; set; } + } + + /// + /// Response for getting a firewall rule + /// + public class FirewallRuleResponse + { + /// + /// Gets or sets the rule + /// + [JsonProperty("rule")] + public FirewallRuleDetails Rule { get; set; } + } + + /// + /// Detailed firewall rule + /// + public class FirewallRuleDetails + { + /// + /// Gets or sets whether the rule is enabled + /// + [JsonProperty("enabled")] + public string Enabled { get; set; } + + /// + /// Gets or sets the sequence of the rule + /// + [JsonProperty("sequence")] + public string Sequence { get; set; } + + /// + /// Gets or sets the description of the rule + /// + [JsonProperty("description")] + public string Description { get; set; } + + /// + /// Gets or sets the protocol of the rule + /// + [JsonProperty("protocol")] + public string Protocol { get; set; } + + /// + /// Gets or sets the source network of the rule + /// + [JsonProperty("source_net")] + public string SourceNet { get; set; } + + /// + /// Gets or sets the source port of the rule + /// + [JsonProperty("source_port")] + public string SourcePort { get; set; } + + /// + /// Gets or sets the destination network of the rule + /// + [JsonProperty("destination_net")] + public string DestinationNet { get; set; } + + /// + /// Gets or sets the destination port of the rule + /// + [JsonProperty("destination_port")] + public string DestinationPort { get; set; } + + /// + /// Gets or sets the action of the rule + /// + [JsonProperty("action")] + public string Action { get; set; } + } + + /// + /// Request for creating a firewall rule + /// + public class FirewallRuleCreate + { + /// + /// Gets or sets whether the rule is enabled + /// + [JsonProperty("enabled")] + public string Enabled { get; set; } = "1"; + + /// + /// Gets or sets the sequence of the rule + /// + [JsonProperty("sequence")] + public string Sequence { get; set; } + + /// + /// Gets or sets the description of the rule + /// + [JsonProperty("description")] + public string Description { get; set; } + + /// + /// Gets or sets the protocol of the rule + /// + [JsonProperty("protocol")] + public string Protocol { get; set; } + + /// + /// Gets or sets the source network of the rule + /// + [JsonProperty("source_net")] + public string SourceNet { get; set; } + + /// + /// Gets or sets the source port of the rule + /// + [JsonProperty("source_port")] + public string SourcePort { get; set; } + + /// + /// Gets or sets the destination network of the rule + /// + [JsonProperty("destination_net")] + public string DestinationNet { get; set; } + + /// + /// Gets or sets the destination port of the rule + /// + [JsonProperty("destination_port")] + public string DestinationPort { get; set; } + + /// + /// Gets or sets the action of the rule + /// + [JsonProperty("action")] + public string Action { get; set; } = "pass"; + } + + /// + /// Response for creating a firewall rule + /// + public class FirewallRuleCreateResponse + { + /// + /// Gets or sets the UUID of the new rule + /// + [JsonProperty("uuid")] + public string Uuid { get; set; } + } + + /// + /// Response for updating a firewall rule + /// + public class FirewallRuleUpdateResponse + { + /// + /// Gets or sets the result of the update + /// + [JsonProperty("result")] + public string Result { get; set; } + } + + /// + /// Response for deleting a firewall rule + /// + public class FirewallRuleDeleteResponse + { + /// + /// Gets or sets the result of the deletion + /// + [JsonProperty("result")] + public string Result { get; set; } + } + + /// + /// Response for toggling a firewall rule + /// + public class FirewallRuleToggleResponse + { + /// + /// Gets or sets the result of the toggle + /// + [JsonProperty("result")] + public string Result { get; set; } + } + + /// + /// Response for creating a savepoint + /// + public class FirewallSavepointResponse + { + /// + /// Gets or sets the revision of the savepoint + /// + [JsonProperty("revision")] + public string Revision { get; set; } + } + + /// + /// Response for applying firewall changes + /// + public class FirewallApplyResponse + { + /// + /// Gets or sets the status of the apply + /// + [JsonProperty("status")] + public string Status { get; set; } + } + + /// + /// Response for cancelling a rollback + /// + public class FirewallCancelRollbackResponse + { + /// + /// Gets or sets the status of the cancel + /// + [JsonProperty("status")] + public string Status { get; set; } + } +} diff --git a/src/PSOPNSenseAPI/Services/FirmwareService.cs b/src/PSOPNSenseAPI/Services/FirmwareService.cs new file mode 100644 index 0000000..bd3e413 --- /dev/null +++ b/src/PSOPNSenseAPI/Services/FirmwareService.cs @@ -0,0 +1,333 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; +using PSOPNSenseAPI.Logging; + +namespace PSOPNSenseAPI.Services +{ + /// + /// Service for interacting with the OPNSense firmware API + /// + public class FirmwareService + { + private readonly OPNSenseApiClient _apiClient; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class + /// + /// The API client + /// The logger + public FirmwareService(OPNSenseApiClient apiClient, ILogger logger) + { + _apiClient = apiClient; + _logger = logger; + } + + /// + /// Gets the firmware status + /// + /// The firmware status + public async Task GetStatusAsync() + { + _logger.Information("Getting firmware status"); + + var endpoint = "core/firmware/status"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Checks for firmware updates + /// + /// The response indicating success + public async Task CheckForUpdatesAsync() + { + _logger.Information("Checking for firmware updates"); + + var endpoint = "core/firmware/check"; + return await _apiClient.PostAsync(endpoint); + } + + /// + /// Updates the firmware + /// + /// The response indicating success + public async Task UpdateAsync() + { + _logger.Information("Updating firmware"); + + var endpoint = "core/firmware/update"; + return await _apiClient.PostAsync(endpoint); + } + + /// + /// Upgrades the firmware + /// + /// The response indicating success + public async Task UpgradeAsync() + { + _logger.Information("Upgrading firmware"); + + var endpoint = "core/firmware/upgrade"; + return await _apiClient.PostAsync(endpoint); + } + + /// + /// Gets the firmware changelog + /// + /// The firmware changelog + public async Task GetChangelogAsync() + { + _logger.Information("Getting firmware changelog"); + + var endpoint = "core/firmware/changelog"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Gets the firmware audit + /// + /// The firmware audit + public async Task GetAuditAsync() + { + _logger.Information("Getting firmware audit"); + + var endpoint = "core/firmware/audit"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Gets the firmware health + /// + /// The firmware health + public async Task GetHealthAsync() + { + _logger.Information("Getting firmware health"); + + var endpoint = "core/firmware/health"; + return await _apiClient.GetAsync(endpoint); + } + } + + /// + /// Response for getting firmware status + /// + public class FirmwareStatusResponse + { + /// + /// Gets or sets the status of the firmware + /// + [JsonProperty("status")] + public string Status { get; set; } + + /// + /// Gets or sets the log of the firmware + /// + [JsonProperty("log")] + public string Log { get; set; } + + /// + /// Gets or sets the upgrade message + /// + [JsonProperty("upgrade_message")] + public string UpgradeMessage { get; set; } + + /// + /// Gets or sets the connection status + /// + [JsonProperty("connection")] + public string Connection { get; set; } + + /// + /// Gets or sets the download size + /// + [JsonProperty("download_size")] + public string DownloadSize { get; set; } + + /// + /// Gets or sets the last check + /// + [JsonProperty("last_check")] + public string LastCheck { get; set; } + + /// + /// Gets or sets the updates + /// + [JsonProperty("updates")] + public List Updates { get; set; } + } + + /// + /// Firmware update information + /// + public class FirmwareUpdate + { + /// + /// Gets or sets the name of the update + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// Gets or sets the version of the update + /// + [JsonProperty("version")] + public string Version { get; set; } + + /// + /// Gets or sets the comment of the update + /// + [JsonProperty("comment")] + public string Comment { get; set; } + + /// + /// Gets or sets the repository of the update + /// + [JsonProperty("repository")] + public string Repository { get; set; } + + /// + /// Gets or sets the origin of the update + /// + [JsonProperty("origin")] + public string Origin { get; set; } + + /// + /// Gets or sets the size of the update + /// + [JsonProperty("size")] + public string Size { get; set; } + } + + /// + /// Response for checking for firmware updates + /// + public class FirmwareCheckResponse + { + /// + /// Gets or sets the status of the check + /// + [JsonProperty("status")] + public string Status { get; set; } + } + + /// + /// Response for updating firmware + /// + public class FirmwareUpdateResponse + { + /// + /// Gets or sets the status of the update + /// + [JsonProperty("status")] + public string Status { get; set; } + } + + /// + /// Response for upgrading firmware + /// + public class FirmwareUpgradeResponse + { + /// + /// Gets or sets the status of the upgrade + /// + [JsonProperty("status")] + public string Status { get; set; } + } + + /// + /// Response for getting firmware changelog + /// + public class FirmwareChangelogResponse + { + /// + /// Gets or sets the changelog + /// + [JsonProperty("changelog")] + public string Changelog { get; set; } + } + + /// + /// Response for getting firmware audit + /// + public class FirmwareAuditResponse + { + /// + /// Gets or sets the audit + /// + [JsonProperty("audit")] + public List Audit { get; set; } + } + + /// + /// Firmware audit item + /// + public class FirmwareAuditItem + { + /// + /// Gets or sets the name of the audit item + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// Gets or sets the version of the audit item + /// + [JsonProperty("version")] + public string Version { get; set; } + + /// + /// Gets or sets the installed version of the audit item + /// + [JsonProperty("installed")] + public string Installed { get; set; } + + /// + /// Gets or sets the vulnerabilities of the audit item + /// + [JsonProperty("vulnerabilities")] + public List Vulnerabilities { get; set; } + } + + /// + /// Firmware vulnerability + /// + public class FirmwareVulnerability + { + /// + /// Gets or sets the name of the vulnerability + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// Gets or sets the description of the vulnerability + /// + [JsonProperty("description")] + public string Description { get; set; } + + /// + /// Gets or sets the CVE of the vulnerability + /// + [JsonProperty("cve")] + public string Cve { get; set; } + + /// + /// Gets or sets the WWW of the vulnerability + /// + [JsonProperty("www")] + public string Www { get; set; } + } + + /// + /// Response for getting firmware health + /// + public class FirmwareHealthResponse + { + /// + /// Gets or sets the health + /// + [JsonProperty("health")] + public string Health { get; set; } + } +} diff --git a/src/PSOPNSenseAPI/Services/GatewayService.cs b/src/PSOPNSenseAPI/Services/GatewayService.cs new file mode 100644 index 0000000..c997fae --- /dev/null +++ b/src/PSOPNSenseAPI/Services/GatewayService.cs @@ -0,0 +1,454 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; +using PSOPNSenseAPI.Logging; + +namespace PSOPNSenseAPI.Services +{ + /// + /// Service for interacting with the OPNSense gateway API + /// + public class GatewayService + { + private readonly OPNSenseApiClient _apiClient; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class + /// + /// The API client + /// The logger + public GatewayService(OPNSenseApiClient apiClient, ILogger logger) + { + _apiClient = apiClient; + _logger = logger; + } + + /// + /// Gets all gateways + /// + /// A list of gateways + public async Task GetGatewaysAsync() + { + _logger.Information("Getting gateways"); + + var endpoint = "routes/gateway/search"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Gets a gateway by UUID + /// + /// The UUID of the gateway + /// The gateway + public async Task GetGatewayAsync(string uuid) + { + _logger.Information($"Getting gateway with UUID {uuid}"); + + var endpoint = $"routes/gateway/get/{uuid}"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Creates a new gateway + /// + /// The gateway to create + /// The response containing the UUID of the new gateway + public async Task CreateGatewayAsync(GatewayConfig gateway) + { + _logger.Information("Creating gateway"); + + var endpoint = "routes/gateway/add"; + var data = new { gateway = gateway }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Updates a gateway + /// + /// The UUID of the gateway to update + /// The updated gateway + /// The response indicating success + public async Task UpdateGatewayAsync(string uuid, GatewayConfig gateway) + { + _logger.Information($"Updating gateway with UUID {uuid}"); + + var endpoint = $"routes/gateway/set/{uuid}"; + var data = new { gateway = gateway }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Deletes a gateway + /// + /// The UUID of the gateway to delete + /// The response indicating success + public async Task DeleteGatewayAsync(string uuid) + { + _logger.Information($"Deleting gateway with UUID {uuid}"); + + var endpoint = $"routes/gateway/del/{uuid}"; + return await _apiClient.PostAsync(endpoint); + } + + /// + /// Applies gateway changes + /// + /// The response indicating success + public async Task ApplyGatewayChangesAsync() + { + _logger.Information("Applying gateway changes"); + + var endpoint = "routes/gateway/reconfigure"; + return await _apiClient.PostAsync(endpoint); + } + + /// + /// Gets the gateway status + /// + /// The gateway status + public async Task GetGatewayStatusAsync() + { + _logger.Information("Getting gateway status"); + + var endpoint = "routes/gateway/status"; + return await _apiClient.GetAsync(endpoint); + } + } + + /// + /// Response for getting gateways + /// + public class GatewayListResponse + { + /// + /// Gets or sets the total number of gateways + /// + [JsonProperty("total")] + public int Total { get; set; } + + /// + /// Gets or sets the number of rows per page + /// + [JsonProperty("rowCount")] + public int RowCount { get; set; } + + /// + /// Gets or sets the current page + /// + [JsonProperty("current")] + public int Current { get; set; } + + /// + /// Gets or sets the gateways + /// + [JsonProperty("rows")] + public List Rows { get; set; } + } + + /// + /// Gateway + /// + public class Gateway + { + /// + /// Gets or sets the UUID of the gateway + /// + [JsonProperty("uuid")] + public string Uuid { get; set; } + + /// + /// Gets or sets the name of the gateway + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// Gets or sets the interface of the gateway + /// + [JsonProperty("interface")] + public string Interface { get; set; } + + /// + /// Gets or sets the IP address of the gateway + /// + [JsonProperty("gateway")] + public string IpAddress { get; set; } + + /// + /// Gets or sets the monitor IP of the gateway + /// + [JsonProperty("monitor")] + public string MonitorIp { get; set; } + + /// + /// Gets or sets the description of the gateway + /// + [JsonProperty("descr")] + public string Description { get; set; } + + /// + /// Gets or sets whether the gateway is the default gateway + /// + [JsonProperty("defaultgw")] + public string IsDefault { get; set; } + + /// + /// Gets or sets whether the gateway is disabled + /// + [JsonProperty("disabled")] + public string Disabled { get; set; } + } + + /// + /// Response for getting a gateway + /// + public class GatewayResponse + { + /// + /// Gets or sets the gateway + /// + [JsonProperty("gateway")] + public GatewayDetail Gateway { get; set; } + } + + /// + /// Gateway details + /// + public class GatewayDetail + { + /// + /// Gets or sets the name of the gateway + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// Gets or sets the interface of the gateway + /// + [JsonProperty("interface")] + public string Interface { get; set; } + + /// + /// Gets or sets the IP address of the gateway + /// + [JsonProperty("gateway")] + public string IpAddress { get; set; } + + /// + /// Gets or sets the monitor IP of the gateway + /// + [JsonProperty("monitor")] + public string MonitorIp { get; set; } + + /// + /// Gets or sets the description of the gateway + /// + [JsonProperty("descr")] + public string Description { get; set; } + + /// + /// Gets or sets whether the gateway is the default gateway + /// + [JsonProperty("defaultgw")] + public string IsDefault { get; set; } + + /// + /// Gets or sets whether the gateway is disabled + /// + [JsonProperty("disabled")] + public string Disabled { get; set; } + + /// + /// Gets or sets the weight of the gateway + /// + [JsonProperty("weight")] + public string Weight { get; set; } + + /// + /// Gets or sets the IP protocol of the gateway + /// + [JsonProperty("ipprotocol")] + public string IpProtocol { get; set; } + + /// + /// Gets or sets the monitor disable option + /// + [JsonProperty("monitor_disable")] + public string MonitorDisable { get; set; } + + /// + /// Gets or sets the force down option + /// + [JsonProperty("force_down")] + public string ForceDown { get; set; } + } + + /// + /// Gateway configuration + /// + public class GatewayConfig + { + /// + /// Gets or sets the name of the gateway + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// Gets or sets the interface of the gateway + /// + [JsonProperty("interface")] + public string Interface { get; set; } + + /// + /// Gets or sets the IP address of the gateway + /// + [JsonProperty("gateway")] + public string IpAddress { get; set; } + + /// + /// Gets or sets the monitor IP of the gateway + /// + [JsonProperty("monitor")] + public string MonitorIp { get; set; } + + /// + /// Gets or sets the description of the gateway + /// + [JsonProperty("descr")] + public string Description { get; set; } + + /// + /// Gets or sets whether the gateway is the default gateway + /// + [JsonProperty("defaultgw")] + public string IsDefault { get; set; } = "0"; + + /// + /// Gets or sets whether the gateway is disabled + /// + [JsonProperty("disabled")] + public string Disabled { get; set; } = "0"; + + /// + /// Gets or sets the weight of the gateway + /// + [JsonProperty("weight")] + public string Weight { get; set; } = "1"; + + /// + /// Gets or sets the IP protocol of the gateway + /// + [JsonProperty("ipprotocol")] + public string IpProtocol { get; set; } = "inet"; + + /// + /// Gets or sets the monitor disable option + /// + [JsonProperty("monitor_disable")] + public string MonitorDisable { get; set; } = "0"; + + /// + /// Gets or sets the force down option + /// + [JsonProperty("force_down")] + public string ForceDown { get; set; } = "0"; + } + + /// + /// Response for creating a gateway + /// + public class GatewayCreateResponse + { + /// + /// Gets or sets the UUID of the new gateway + /// + [JsonProperty("uuid")] + public string Uuid { get; set; } + } + + /// + /// Response for updating a gateway + /// + public class GatewayUpdateResponse + { + /// + /// Gets or sets the result of the update + /// + [JsonProperty("result")] + public string Result { get; set; } + } + + /// + /// Response for deleting a gateway + /// + public class GatewayDeleteResponse + { + /// + /// Gets or sets the result of the deletion + /// + [JsonProperty("result")] + public string Result { get; set; } + } + + /// + /// Response for applying gateway changes + /// + public class GatewayApplyResponse + { + /// + /// Gets or sets the status of the apply + /// + [JsonProperty("status")] + public string Status { get; set; } + } + + /// + /// Response for getting gateway status + /// + public class GatewayStatusResponse + { + /// + /// Gets or sets the gateway status items + /// + [JsonProperty("items")] + public Dictionary Items { get; set; } + } + + /// + /// Gateway status item + /// + public class GatewayStatusItem + { + /// + /// Gets or sets the name of the gateway + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// Gets or sets the status of the gateway + /// + [JsonProperty("status")] + public string Status { get; set; } + + /// + /// Gets or sets the RTT of the gateway + /// + [JsonProperty("rtt")] + public string RTT { get; set; } + + /// + /// Gets or sets the RTT standard deviation of the gateway + /// + [JsonProperty("stddev")] + public string StdDev { get; set; } + + /// + /// Gets or sets the loss percentage of the gateway + /// + [JsonProperty("loss")] + public string Loss { get; set; } + } +} diff --git a/src/PSOPNSenseAPI/Services/IOPNSenseApiClient.cs b/src/PSOPNSenseAPI/Services/IOPNSenseApiClient.cs new file mode 100644 index 0000000..742bae7 --- /dev/null +++ b/src/PSOPNSenseAPI/Services/IOPNSenseApiClient.cs @@ -0,0 +1,43 @@ +using System; +using System.Threading.Tasks; + +namespace PSOPNSenseAPI.Services +{ + /// + /// Interface for interacting with the OPNSense API + /// + public interface IOPNSenseApiClient : IDisposable + { + /// + /// Gets the base URL of the OPNSense API + /// + string BaseUrl { get; } + + /// + /// Gets the API key + /// + string ApiKey { get; } + + /// + /// Gets the API secret + /// + string ApiSecret { get; } + + /// + /// Sends a GET request to the OPNSense API + /// + /// The type to deserialize the response to + /// The API endpoint + /// The deserialized response + Task GetAsync(string endpoint); + + /// + /// Sends a POST request to the OPNSense API + /// + /// The type to deserialize the response to + /// The API endpoint + /// The data to send + /// The deserialized response + Task PostAsync(string endpoint, object data = null); + } +} diff --git a/src/PSOPNSenseAPI/Services/InterfaceService.cs b/src/PSOPNSenseAPI/Services/InterfaceService.cs new file mode 100644 index 0000000..0326df5 --- /dev/null +++ b/src/PSOPNSenseAPI/Services/InterfaceService.cs @@ -0,0 +1,531 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; +using PSOPNSenseAPI.Logging; + +namespace PSOPNSenseAPI.Services +{ + /// + /// Service for managing OPNSense interfaces + /// + public class InterfaceService + { + private readonly IOPNSenseApiClient _apiClient; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class + /// + /// The OPNSense API client + /// The logger + public InterfaceService(IOPNSenseApiClient apiClient, ILogger logger) + { + _apiClient = apiClient; + _logger = logger; + } + + /// + /// Gets all interfaces + /// + /// A list of interfaces + public async Task GetInterfacesAsync() + { + _logger.Information("Getting interfaces"); + + var endpoint = "interfaces/overview/searchInterfaces"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Gets details for a specific interface + /// + /// The name of the interface + /// The interface details + public async Task GetInterfaceDetailAsync(string interfaceName) + { + _logger.Information($"Getting interface {interfaceName}"); + + var endpoint = $"interfaces/overview/getInterface/{interfaceName}"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Gets statistics for all interfaces + /// + /// The interface statistics + public async Task GetInterfaceStatisticsAsync() + { + _logger.Information("Getting interface statistics"); + + var endpoint = "diagnostics/interface/getInterfaceStatistics"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Updates an interface + /// + /// The name of the interface to update + /// The interface configuration + /// The update response + public async Task UpdateInterfaceAsync(string interfaceName, InterfaceConfig interfaceConfig) + { + _logger.Information($"Updating interface {interfaceName}"); + + var endpoint = $"interfaces/overview/setInterface/{interfaceName}"; + var data = new { iface = interfaceConfig }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Restarts an interface + /// + /// The name of the interface to restart + /// The restart response + public async Task RestartInterfaceAsync(string interfaceName) + { + _logger.Information($"Restarting interface {interfaceName}"); + + var endpoint = $"interfaces/overview/reconfigure/{interfaceName}"; + return await _apiClient.PostAsync(endpoint); + } + + /// + /// Gets all VLANs + /// + /// A list of VLANs + public async Task GetVLANsAsync() + { + _logger.Information("Getting VLANs"); + + var endpoint = "interfaces/vlan/searchItem"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Gets a specific VLAN + /// + /// The UUID of the VLAN + /// The VLAN details + public async Task GetVLANAsync(string uuid) + { + _logger.Information($"Getting VLAN with UUID {uuid}"); + + var endpoint = $"interfaces/vlan/getItem/{uuid}"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Creates a new VLAN + /// + /// The VLAN configuration + /// The create response with the UUID of the created VLAN + public async Task CreateVLANAsync(VLANConfig vlan) + { + _logger.Information("Creating VLAN"); + + var endpoint = "interfaces/vlan/addItem"; + var data = new { vlan = vlan }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Updates an existing VLAN + /// + /// The UUID of the VLAN to update + /// The updated VLAN configuration + /// The update response + public async Task UpdateVLANAsync(string uuid, VLANConfig vlan) + { + _logger.Information($"Updating VLAN with UUID {uuid}"); + + var endpoint = $"interfaces/vlan/setItem/{uuid}"; + var data = new { vlan = vlan }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Deletes a VLAN + /// + /// The UUID of the VLAN to delete + /// The delete response + public async Task DeleteVLANAsync(string uuid) + { + _logger.Information($"Deleting VLAN with UUID {uuid}"); + + var endpoint = $"interfaces/vlan/delItem/{uuid}"; + return await _apiClient.PostAsync(endpoint); + } + } + + /// + /// Response from the OPNSense API for interface list + /// + public class InterfaceListResponse + { + /// + /// Gets or sets the list of interfaces + /// + [JsonProperty("rows")] + public List Interfaces { get; set; } + } + + /// + /// Information about an interface + /// + public class InterfaceInfo + { + /// + /// Gets or sets the name of the interface + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// Gets or sets the description of the interface + /// + [JsonProperty("description")] + public string Description { get; set; } + + /// + /// Gets or sets the IP address of the interface + /// + [JsonProperty("ipaddr")] + public string IpAddress { get; set; } + + /// + /// Gets or sets the subnet mask of the interface + /// + [JsonProperty("subnet")] + public string SubnetMask { get; set; } + + /// + /// Gets or sets the gateway of the interface + /// + [JsonProperty("gateway")] + public string Gateway { get; set; } + + /// + /// Gets or sets the status of the interface + /// + [JsonProperty("status")] + public string Status { get; set; } + } + + /// + /// Response from the OPNSense API for interface details + /// + public class InterfaceDetailResponse + { + /// + /// Gets or sets the interface details + /// + [JsonProperty("interface")] + public InterfaceDetail Interface { get; set; } + } + + /// + /// Detailed information about an interface + /// + public class InterfaceDetail + { + /// + /// Gets or sets the name of the interface + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// Gets or sets the description of the interface + /// + [JsonProperty("description")] + public string Description { get; set; } + + /// + /// Gets or sets the IP address of the interface + /// + [JsonProperty("ipaddr")] + public string IpAddress { get; set; } + + /// + /// Gets or sets the subnet mask of the interface + /// + [JsonProperty("subnet")] + public string SubnetMask { get; set; } + + /// + /// Gets or sets the gateway of the interface + /// + [JsonProperty("gateway")] + public string Gateway { get; set; } + + /// + /// Gets or sets whether the interface is enabled + /// + [JsonProperty("enable")] + public string Enabled { get; set; } + } + + /// + /// Configuration for an interface + /// + public class InterfaceConfig + { + /// + /// Gets or sets the description of the interface + /// + [JsonProperty("description")] + public string Description { get; set; } + + /// + /// Gets or sets the IP address of the interface + /// + [JsonProperty("ipaddr")] + public string IpAddress { get; set; } + + /// + /// Gets or sets the subnet mask of the interface + /// + [JsonProperty("subnet")] + public string SubnetMask { get; set; } + + /// + /// Gets or sets the gateway of the interface + /// + [JsonProperty("gateway")] + public string Gateway { get; set; } + + /// + /// Gets or sets whether the interface is enabled + /// + [JsonProperty("enable")] + public string Enabled { get; set; } + } + + /// + /// Response from the OPNSense API for interface update + /// + public class InterfaceUpdateResponse + { + /// + /// Gets or sets the result of the update + /// + [JsonProperty("result")] + public string Result { get; set; } + } + + /// + /// Response from the OPNSense API for interface restart + /// + public class InterfaceRestartResponse + { + /// + /// Gets or sets the status of the restart + /// + [JsonProperty("status")] + public string Status { get; set; } + } + + /// + /// Response from the OPNSense API for interface statistics + /// + public class InterfaceStatisticsResponse + { + /// + /// Gets or sets the statistics for each interface + /// + [JsonProperty("statistics")] + public Dictionary Statistics { get; set; } + } + + /// + /// Statistics for an interface + /// + public class InterfaceStatistics + { + /// + /// Gets or sets the name of the interface + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// Gets or sets the number of incoming packets + /// + [JsonProperty("inpkts")] + public string InPackets { get; set; } + + /// + /// Gets or sets the number of outgoing packets + /// + [JsonProperty("outpkts")] + public string OutPackets { get; set; } + + /// + /// Gets or sets the number of incoming bytes + /// + [JsonProperty("inbytes")] + public string InBytes { get; set; } + + /// + /// Gets or sets the number of outgoing bytes + /// + [JsonProperty("outbytes")] + public string OutBytes { get; set; } + } + + /// + /// Response from the OPNSense API for VLAN list + /// + public class VLANListResponse + { + /// + /// Gets or sets the list of VLANs + /// + [JsonProperty("rows")] + public List Rows { get; set; } + } + + /// + /// Information about a VLAN + /// + public class VLAN + { + /// + /// Gets or sets the UUID of the VLAN + /// + [JsonProperty("uuid")] + public string Uuid { get; set; } + + /// + /// Gets or sets the interface of the VLAN + /// + [JsonProperty("if")] + public string Interface { get; set; } + + /// + /// Gets or sets the tag of the VLAN + /// + [JsonProperty("tag")] + public string Tag { get; set; } + + /// + /// Gets or sets the priority of the VLAN + /// + [JsonProperty("pcp")] + public string Priority { get; set; } + + /// + /// Gets or sets the description of the VLAN + /// + [JsonProperty("descr")] + public string Description { get; set; } + } + + /// + /// Response from the OPNSense API for VLAN details + /// + public class VLANResponse + { + /// + /// Gets or sets the VLAN details + /// + [JsonProperty("vlan")] + public VLANDetail Vlan { get; set; } + } + + /// + /// Detailed information about a VLAN + /// + public class VLANDetail + { + /// + /// Gets or sets the interface of the VLAN + /// + [JsonProperty("if")] + public string Interface { get; set; } + + /// + /// Gets or sets the tag of the VLAN + /// + [JsonProperty("tag")] + public string Tag { get; set; } + + /// + /// Gets or sets the priority of the VLAN + /// + [JsonProperty("pcp")] + public string Priority { get; set; } + + /// + /// Gets or sets the description of the VLAN + /// + [JsonProperty("descr")] + public string Description { get; set; } + } + + /// + /// Configuration for a VLAN + /// + public class VLANConfig + { + /// + /// Gets or sets the interface of the VLAN + /// + [JsonProperty("if")] + public string Interface { get; set; } + + /// + /// Gets or sets the tag of the VLAN + /// + [JsonProperty("tag")] + public string Tag { get; set; } + + /// + /// Gets or sets the priority of the VLAN + /// + [JsonProperty("pcp")] + public string Priority { get; set; } + + /// + /// Gets or sets the description of the VLAN + /// + [JsonProperty("descr")] + public string Description { get; set; } + } + + /// + /// Response from the OPNSense API for VLAN creation + /// + public class VLANCreateResponse + { + /// + /// Gets or sets the UUID of the created VLAN + /// + [JsonProperty("uuid")] + public string Uuid { get; set; } + } + + /// + /// Response from the OPNSense API for VLAN update + /// + public class VLANUpdateResponse + { + /// + /// Gets or sets the result of the update + /// + [JsonProperty("result")] + public string Result { get; set; } + } + + /// + /// Response from the OPNSense API for VLAN deletion + /// + public class VLANDeleteResponse + { + /// + /// Gets or sets the result of the deletion + /// + [JsonProperty("result")] + public string Result { get; set; } + } +} diff --git a/src/PSOPNSenseAPI/Services/OPNSenseApiClient.cs b/src/PSOPNSenseAPI/Services/OPNSenseApiClient.cs new file mode 100644 index 0000000..b2d9221 --- /dev/null +++ b/src/PSOPNSenseAPI/Services/OPNSenseApiClient.cs @@ -0,0 +1,203 @@ +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; +using PSOPNSenseAPI.Logging; +using PSOPNSenseAPI.Models; + +namespace PSOPNSenseAPI.Services +{ + /// + /// Client for interacting with the OPNSense API + /// + public class OPNSenseApiClient : IOPNSenseApiClient + { + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + private bool _disposed = false; + + /// + /// Gets the base URL of the OPNSense API + /// + public string BaseUrl { get; } + + /// + /// Gets the API key + /// + public string ApiKey { get; } + + /// + /// Gets the API secret + /// + public string ApiSecret { get; } + + /// + /// Gets a value indicating whether to skip certificate validation + /// + public bool SkipCertificateCheck { get; } + + /// + /// Gets a value indicating whether the client is connected + /// + public bool IsConnected { get; private set; } + + /// + /// Initializes a new instance of the class + /// + /// The base URL of the OPNSense API + /// The API key + /// The API secret + /// Whether to skip certificate validation + /// The logger to use + public OPNSenseApiClient(string baseUrl, string apiKey, string apiSecret, bool skipCertificateCheck, ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + if (string.IsNullOrWhiteSpace(baseUrl)) + throw new ArgumentNullException(nameof(baseUrl)); + + if (string.IsNullOrWhiteSpace(apiKey)) + throw new ArgumentNullException(nameof(apiKey)); + + if (string.IsNullOrWhiteSpace(apiSecret)) + throw new ArgumentNullException(nameof(apiSecret)); + + BaseUrl = baseUrl.TrimEnd('/'); + ApiKey = apiKey; + ApiSecret = apiSecret; + SkipCertificateCheck = skipCertificateCheck; + + var handler = new HttpClientHandler(); + if (skipCertificateCheck) + { + handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true; + } + + _httpClient = new HttpClient(handler); + _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + // Set up basic authentication + var authValue = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{apiKey}:{apiSecret}")); + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authValue); + + _logger.Debug($"Initialized OPNSense API client for {baseUrl}"); + IsConnected = true; + } + + /// + /// Sends a GET request to the specified endpoint + /// + /// The type to deserialize the response to + /// The API endpoint + /// The deserialized response + public async Task GetAsync(string endpoint) + { + var url = $"{BaseUrl}/api/{endpoint.TrimStart('/')}"; + _logger.Debug($"GET {url}"); + + try + { + var response = await _httpClient.GetAsync(url); + return await ProcessResponseAsync(response); + } + catch (Exception ex) + { + _logger.Error($"Error in GET request to {url}: {ex.Message}"); + throw; + } + } + + /// + /// Sends a POST request to the specified endpoint + /// + /// The type to deserialize the response to + /// The API endpoint + /// The data to send + /// The deserialized response + public async Task PostAsync(string endpoint, object data = null) + { + var url = $"{BaseUrl}/api/{endpoint.TrimStart('/')}"; + _logger.Debug($"POST {url}"); + + try + { + HttpContent content = null; + if (data != null) + { + var json = JsonConvert.SerializeObject(data); + _logger.Debug($"Request body: {json}"); + content = new StringContent(json, Encoding.UTF8, "application/json"); + } + + var response = await _httpClient.PostAsync(url, content); + return await ProcessResponseAsync(response); + } + catch (Exception ex) + { + _logger.Error($"Error in POST request to {url}: {ex.Message}"); + throw; + } + } + + /// + /// Processes the HTTP response + /// + /// The type to deserialize the response to + /// The HTTP response + /// The deserialized response + private async Task ProcessResponseAsync(HttpResponseMessage response) + { + var content = await response.Content.ReadAsStringAsync(); + + _logger.Debug($"Response status: {(int)response.StatusCode} {response.StatusCode}"); + _logger.Debug($"Response body: {content}"); + + if (!response.IsSuccessStatusCode) + { + _logger.Error($"API request failed: {(int)response.StatusCode} {response.StatusCode}"); + _logger.Error($"Response content: {content}"); + throw new OPNSenseApiException($"API request failed with status code {(int)response.StatusCode}", response.StatusCode, content); + } + + try + { + return JsonConvert.DeserializeObject(content); + } + catch (JsonException ex) + { + _logger.Error($"Failed to deserialize response: {ex.Message}"); + _logger.Error($"Response content: {content}"); + throw new OPNSenseApiException($"Failed to deserialize response: {ex.Message}", response.StatusCode, content, ex); + } + } + + /// + /// Disposes the HTTP client + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes the HTTP client + /// + /// Whether to dispose managed resources + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) + { + _httpClient?.Dispose(); + IsConnected = false; + } + + _disposed = true; + } + } +} diff --git a/src/PSOPNSenseAPI/Services/OPNSenseSessionState.cs b/src/PSOPNSenseAPI/Services/OPNSenseSessionState.cs new file mode 100644 index 0000000..e558b8d --- /dev/null +++ b/src/PSOPNSenseAPI/Services/OPNSenseSessionState.cs @@ -0,0 +1,29 @@ +using System; + +namespace PSOPNSenseAPI.Services +{ + /// + /// Singleton class to maintain the OPNSense session state + /// + public class OPNSenseSessionState + { + private static readonly Lazy _instance = new Lazy(() => new OPNSenseSessionState()); + + /// + /// Gets the singleton instance + /// + public static OPNSenseSessionState Instance => _instance.Value; + + /// + /// Gets or sets the API client + /// + public OPNSenseApiClient ApiClient { get; set; } + + /// + /// Private constructor to prevent instantiation + /// + private OPNSenseSessionState() + { + } + } +} diff --git a/src/PSOPNSenseAPI/Services/PluginService.cs b/src/PSOPNSenseAPI/Services/PluginService.cs new file mode 100644 index 0000000..ad8c107 --- /dev/null +++ b/src/PSOPNSenseAPI/Services/PluginService.cs @@ -0,0 +1,263 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; +using PSOPNSenseAPI.Logging; + +namespace PSOPNSenseAPI.Services +{ + /// + /// Service for interacting with the OPNSense plugin API + /// + public class PluginService + { + private readonly OPNSenseApiClient _apiClient; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class + /// + /// The API client + /// The logger + public PluginService(OPNSenseApiClient apiClient, ILogger logger) + { + _apiClient = apiClient; + _logger = logger; + } + + /// + /// Gets all plugins + /// + /// A list of plugins + public async Task GetPluginsAsync() + { + _logger.Information("Getting plugins"); + + var endpoint = "core/firmware/getPlugins"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Gets all installed plugins + /// + /// A list of installed plugins + public async Task GetInstalledPluginsAsync() + { + _logger.Information("Getting installed plugins"); + + var endpoint = "core/firmware/getInstalledPlugins"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Installs a plugin + /// + /// The name of the plugin to install + /// The response indicating success + public async Task InstallPluginAsync(string name) + { + _logger.Information($"Installing plugin {name}"); + + var endpoint = "core/firmware/install"; + var data = new { pkg = name }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Uninstalls a plugin + /// + /// The name of the plugin to uninstall + /// The response indicating success + public async Task UninstallPluginAsync(string name) + { + _logger.Information($"Uninstalling plugin {name}"); + + var endpoint = "core/firmware/remove"; + var data = new { pkg = name }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Enables a plugin + /// + /// The name of the plugin to enable + /// The response indicating success + public async Task EnablePluginAsync(string name) + { + _logger.Information($"Enabling plugin {name}"); + + var endpoint = "core/firmware/enable"; + var data = new { pkg = name }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Disables a plugin + /// + /// The name of the plugin to disable + /// The response indicating success + public async Task DisablePluginAsync(string name) + { + _logger.Information($"Disabling plugin {name}"); + + var endpoint = "core/firmware/disable"; + var data = new { pkg = name }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Gets the status of a plugin operation + /// + /// The status of the plugin operation + public async Task GetPluginStatusAsync() + { + _logger.Information("Getting plugin operation status"); + + var endpoint = "core/firmware/status"; + return await _apiClient.GetAsync(endpoint); + } + } + + /// + /// Response for getting plugins + /// + public class PluginListResponse + { + /// + /// Gets or sets the installed plugins + /// + [JsonProperty("installed")] + public Dictionary Installed { get; set; } + + /// + /// Gets or sets the available plugins + /// + [JsonProperty("available")] + public Dictionary Available { get; set; } + } + + /// + /// Plugin information + /// + public class Plugin + { + /// + /// Gets or sets the name of the plugin + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// Gets or sets the version of the plugin + /// + [JsonProperty("version")] + public string Version { get; set; } + + /// + /// Gets or sets the comment of the plugin + /// + [JsonProperty("comment")] + public string Comment { get; set; } + + /// + /// Gets or sets the repository of the plugin + /// + [JsonProperty("repository")] + public string Repository { get; set; } + + /// + /// Gets or sets the origin of the plugin + /// + [JsonProperty("origin")] + public string Origin { get; set; } + + /// + /// Gets or sets the license of the plugin + /// + [JsonProperty("license")] + public string License { get; set; } + + /// + /// Gets or sets the flatsize of the plugin + /// + [JsonProperty("flatsize")] + public string FlatSize { get; set; } + + /// + /// Gets or sets the locked status of the plugin + /// + [JsonProperty("locked")] + public string Locked { get; set; } + + /// + /// Gets or sets the enabled status of the plugin + /// + [JsonProperty("enabled")] + public string Enabled { get; set; } + } + + /// + /// Response for installing a plugin + /// + public class PluginInstallResponse + { + /// + /// Gets or sets the status of the installation + /// + [JsonProperty("status")] + public string Status { get; set; } + } + + /// + /// Response for uninstalling a plugin + /// + public class PluginUninstallResponse + { + /// + /// Gets or sets the status of the uninstallation + /// + [JsonProperty("status")] + public string Status { get; set; } + } + + /// + /// Response for enabling a plugin + /// + public class PluginEnableResponse + { + /// + /// Gets or sets the status of the enable + /// + [JsonProperty("status")] + public string Status { get; set; } + } + + /// + /// Response for disabling a plugin + /// + public class PluginDisableResponse + { + /// + /// Gets or sets the status of the disable + /// + [JsonProperty("status")] + public string Status { get; set; } + } + + /// + /// Response for getting the status of a plugin operation + /// + public class PluginStatusResponse + { + /// + /// Gets or sets the status of the plugin operation + /// + [JsonProperty("status")] + public string Status { get; set; } + + /// + /// Gets or sets the log of the plugin operation + /// + [JsonProperty("log")] + public string Log { get; set; } + } +} diff --git a/src/PSOPNSenseAPI/Services/PortForwardingService.cs b/src/PSOPNSenseAPI/Services/PortForwardingService.cs new file mode 100644 index 0000000..64f8192 --- /dev/null +++ b/src/PSOPNSenseAPI/Services/PortForwardingService.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PSOPNSenseAPI.Models; +using PSOPNSenseAPI.Logging; + +namespace PSOPNSenseAPI.Services +{ + /// + /// Service for managing port forwarding rules in OPNSense + /// + public class PortForwardingService : ServiceBase + { + /// + /// Initializes a new instance of the class + /// + /// The API client to use for requests + public PortForwardingService(OPNSenseApiClient apiClient) : base(apiClient) + { + } + + /// + /// Initializes a new instance of the class + /// + /// The API client to use for requests + /// The logger to use + public PortForwardingService(OPNSenseApiClient apiClient, ILogger logger) : base(apiClient, logger) + { + } + + /// + /// Gets all port forwarding rules + /// + /// A list of port forwarding rules + public async Task> GetPortForwardingRulesAsync() + { + Logger.Information("Getting all port forwarding rules"); + + try + { + var endpoint = "firewall/nat/getRule"; + var response = await ApiClient.GetAsync(endpoint); + + var rules = new List(); + // Process response when API is implemented + return rules; + } + catch (Exception ex) + { + Logger.Error($"Error getting port forwarding rules: {ex.Message}"); + return new List(); + } + } + + /// + /// Gets a port forwarding rule by UUID + /// + /// The UUID of the rule to get + /// The port forwarding rule + public async Task GetPortForwardingRuleAsync(string uuid) + { + Logger.Information($"Getting port forwarding rule with UUID {uuid}"); + + try + { + var endpoint = $"firewall/nat/getRule/{uuid}"; + var response = await ApiClient.GetAsync(endpoint); + + // Process response when API is implemented + return new PortForwardingRule(); + } + catch (Exception ex) + { + Logger.Error($"Error getting port forwarding rule: {ex.Message}"); + return null; + } + } + + /// + /// Creates a new port forwarding rule + /// + /// The rule to create + /// The UUID of the created rule + public async Task CreatePortForwardingRuleAsync(PortForwardingRule rule) + { + Logger.Information("Creating new port forwarding rule"); + + try + { + var endpoint = "firewall/nat/addRule"; + var response = await ApiClient.PostAsync(endpoint, rule); + + // Process response when API is implemented + return "new-uuid"; + } + catch (Exception ex) + { + Logger.Error($"Error creating port forwarding rule: {ex.Message}"); + return string.Empty; + } + } + + /// + /// Updates an existing port forwarding rule + /// + /// The rule to update + /// True if successful + public async Task UpdatePortForwardingRuleAsync(PortForwardingRule rule) + { + Logger.Information($"Updating port forwarding rule with UUID {rule.Uuid}"); + + try + { + var endpoint = $"firewall/nat/setRule/{rule.Uuid}"; + var response = await ApiClient.PostAsync(endpoint, rule); + + // Process response when API is implemented + return true; + } + catch (Exception ex) + { + Logger.Error($"Error updating port forwarding rule: {ex.Message}"); + return false; + } + } + + /// + /// Deletes a port forwarding rule + /// + /// The UUID of the rule to delete + /// True if successful + public async Task DeletePortForwardingRuleAsync(string uuid) + { + Logger.Information($"Deleting port forwarding rule with UUID {uuid}"); + + try + { + var endpoint = $"firewall/nat/delRule/{uuid}"; + var response = await ApiClient.PostAsync(endpoint, null); + + // Process response when API is implemented + return true; + } + catch (Exception ex) + { + Logger.Error($"Error deleting port forwarding rule: {ex.Message}"); + return false; + } + } + } +} diff --git a/src/PSOPNSenseAPI/Services/PortForwardingService.cs.bak b/src/PSOPNSenseAPI/Services/PortForwardingService.cs.bak new file mode 100644 index 0000000..bb2b7d5 --- /dev/null +++ b/src/PSOPNSenseAPI/Services/PortForwardingService.cs.bak @@ -0,0 +1,202 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PSOPNSenseAPI.Models; + +namespace PSOPNSenseAPI.Services +{ + /// + /// Service for managing port forwarding rules in OPNSense + /// + public class PortForwardingService : ServiceBase + { + /// + /// Initializes a new instance of the class + /// + /// The API client to use for requests + public PortForwardingService(ApiClient apiClient) : base(apiClient) + { + } + + /// + /// Gets all port forwarding rules + /// + /// A list of port forwarding rules + public async Task> GetPortForwardingRulesAsync() + { + var response = await ApiClient.GetAsync("/firewall/nat/getRule"); + var responseContent = await response.Content.ReadAsStringAsync(); + var jsonResponse = JObject.Parse(responseContent); + + if (jsonResponse["status"]?.ToString() != "ok") + { + throw new Exception($"Failed to get port forwarding rules: {jsonResponse["status_msg"]}"); + } + + var rules = new List(); + var rulesData = jsonResponse["rows"]; + + foreach (var rule in rulesData) + { + var portForwardingRule = new PortForwardingRule + { + Uuid = rule["uuid"]?.ToString(), + Enabled = rule["enabled"]?.ToString() == "1", + Description = rule["descr"]?.ToString(), + Interface = rule["interface"]?.ToString(), + Protocol = rule["protocol"]?.ToString(), + Source = rule["source"]?.ToString(), + SourcePort = rule["source_port"]?.ToString(), + Destination = rule["destination"]?.ToString(), + DestinationPort = rule["destination_port"]?.ToString(), + TargetIP = rule["target"]?.ToString(), + TargetPort = rule["target_port"]?.ToString(), + NatReflection = rule["natreflection"]?.ToString(), + FilterRuleAssociation = rule["associated-rule-id"]?.ToString() != "none", + Log = rule["log"]?.ToString() == "1" + }; + + rules.Add(portForwardingRule); + } + + return rules; + } + + /// + /// Gets a port forwarding rule by UUID + /// + /// The UUID of the rule to get + /// The port forwarding rule + public async Task GetPortForwardingRuleAsync(string uuid) + { + var rules = await GetPortForwardingRulesAsync(); + return rules.Find(r => r.Uuid == uuid); + } + + /// + /// Creates a new port forwarding rule + /// + /// The rule to create + /// The UUID of the created rule + public async Task CreatePortForwardingRuleAsync(PortForwardingRule rule) + { + var ruleData = new + { + rule = new + { + enabled = rule.Enabled ? "1" : "0", + descr = rule.Description, + interface = rule.Interface, + protocol = rule.Protocol, + source = rule.Source, + source_port = rule.SourcePort, + destination = rule.Destination, + destination_port = rule.DestinationPort, + target = rule.TargetIP, + target_port = rule.TargetPort, + natreflection = rule.NatReflection, + associated_rule_id = rule.FilterRuleAssociation ? "pass" : "none", + log = rule.Log ? "1" : "0" + } + }; + + var response = await ApiClient.PostAsync("/firewall/nat/addRule", ruleData); + var responseContent = await response.Content.ReadAsStringAsync(); + var jsonResponse = JObject.Parse(responseContent); + + if (jsonResponse["status"]?.ToString() != "ok") + { + throw new Exception($"Failed to create port forwarding rule: {jsonResponse["status_msg"]}"); + } + + // Apply changes + await ApplyChangesAsync(); + + return jsonResponse["uuid"]?.ToString(); + } + + /// + /// Updates an existing port forwarding rule + /// + /// The rule to update + /// True if successful + public async Task UpdatePortForwardingRuleAsync(PortForwardingRule rule) + { + if (string.IsNullOrEmpty(rule.Uuid)) + { + throw new ArgumentException("Rule UUID cannot be null or empty", nameof(rule)); + } + + var ruleData = new + { + rule = new + { + enabled = rule.Enabled ? "1" : "0", + descr = rule.Description, + interface = rule.Interface, + protocol = rule.Protocol, + source = rule.Source, + source_port = rule.SourcePort, + destination = rule.Destination, + destination_port = rule.DestinationPort, + target = rule.TargetIP, + target_port = rule.TargetPort, + natreflection = rule.NatReflection, + associated_rule_id = rule.FilterRuleAssociation ? "pass" : "none", + log = rule.Log ? "1" : "0" + } + }; + + var response = await ApiClient.PostAsync($"/firewall/nat/setRule/{rule.Uuid}", ruleData); + var responseContent = await response.Content.ReadAsStringAsync(); + var jsonResponse = JObject.Parse(responseContent); + + if (jsonResponse["status"]?.ToString() != "ok") + { + throw new Exception($"Failed to update port forwarding rule: {jsonResponse["status_msg"]}"); + } + + // Apply changes + await ApplyChangesAsync(); + + return true; + } + + /// + /// Deletes a port forwarding rule + /// + /// The UUID of the rule to delete + /// True if successful + public async Task DeletePortForwardingRuleAsync(string uuid) + { + var response = await ApiClient.PostAsync($"/firewall/nat/delRule/{uuid}", null); + var responseContent = await response.Content.ReadAsStringAsync(); + var jsonResponse = JObject.Parse(responseContent); + + if (jsonResponse["status"]?.ToString() != "ok") + { + throw new Exception($"Failed to delete port forwarding rule: {jsonResponse["status_msg"]}"); + } + + // Apply changes + await ApplyChangesAsync(); + + return true; + } + + /// + /// Applies the changes to the firewall + /// + /// True if successful + private async Task ApplyChangesAsync() + { + var response = await ApiClient.PostAsync("/firewall/nat/apply", null); + var responseContent = await response.Content.ReadAsStringAsync(); + var jsonResponse = JObject.Parse(responseContent); + + return jsonResponse["status"]?.ToString() == "ok"; + } + } +} diff --git a/src/PSOPNSenseAPI/Services/RouteService.cs b/src/PSOPNSenseAPI/Services/RouteService.cs new file mode 100644 index 0000000..4d3a435 --- /dev/null +++ b/src/PSOPNSenseAPI/Services/RouteService.cs @@ -0,0 +1,292 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; +using PSOPNSenseAPI.Logging; + +namespace PSOPNSenseAPI.Services +{ + /// + /// Service for interacting with the OPNSense route API + /// + public class RouteService + { + private readonly OPNSenseApiClient _apiClient; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class + /// + /// The API client + /// The logger + public RouteService(OPNSenseApiClient apiClient, ILogger logger) + { + _apiClient = apiClient; + _logger = logger; + } + + /// + /// Gets all routes + /// + /// A list of routes + public async Task GetRoutesAsync() + { + _logger.Information("Getting routes"); + + var endpoint = "routes/routes/search"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Gets a route by UUID + /// + /// The UUID of the route + /// The route + public async Task GetRouteAsync(string uuid) + { + _logger.Information($"Getting route with UUID {uuid}"); + + var endpoint = $"routes/routes/get/{uuid}"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Creates a new route + /// + /// The route to create + /// The response containing the UUID of the new route + public async Task CreateRouteAsync(RouteConfig route) + { + _logger.Information("Creating route"); + + var endpoint = "routes/routes/add"; + var data = new { route = route }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Updates a route + /// + /// The UUID of the route to update + /// The updated route + /// The response indicating success + public async Task UpdateRouteAsync(string uuid, RouteConfig route) + { + _logger.Information($"Updating route with UUID {uuid}"); + + var endpoint = $"routes/routes/set/{uuid}"; + var data = new { route = route }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Deletes a route + /// + /// The UUID of the route to delete + /// The response indicating success + public async Task DeleteRouteAsync(string uuid) + { + _logger.Information($"Deleting route with UUID {uuid}"); + + var endpoint = $"routes/routes/del/{uuid}"; + return await _apiClient.PostAsync(endpoint); + } + + /// + /// Applies route changes + /// + /// The response indicating success + public async Task ApplyRouteChangesAsync() + { + _logger.Information("Applying route changes"); + + var endpoint = "routes/routes/reconfigure"; + return await _apiClient.PostAsync(endpoint); + } + } + + /// + /// Response for getting routes + /// + public class RouteListResponse + { + /// + /// Gets or sets the total number of routes + /// + [JsonProperty("total")] + public int Total { get; set; } + + /// + /// Gets or sets the number of rows per page + /// + [JsonProperty("rowCount")] + public int RowCount { get; set; } + + /// + /// Gets or sets the current page + /// + [JsonProperty("current")] + public int Current { get; set; } + + /// + /// Gets or sets the routes + /// + [JsonProperty("rows")] + public List Rows { get; set; } + } + + /// + /// Route + /// + public class Route + { + /// + /// Gets or sets the UUID of the route + /// + [JsonProperty("uuid")] + public string Uuid { get; set; } + + /// + /// Gets or sets the network of the route + /// + [JsonProperty("network")] + public string Network { get; set; } + + /// + /// Gets or sets the gateway of the route + /// + [JsonProperty("gateway")] + public string Gateway { get; set; } + + /// + /// Gets or sets the description of the route + /// + [JsonProperty("descr")] + public string Description { get; set; } + + /// + /// Gets or sets whether the route is disabled + /// + [JsonProperty("disabled")] + public string Disabled { get; set; } + } + + /// + /// Response for getting a route + /// + public class RouteResponse + { + /// + /// Gets or sets the route + /// + [JsonProperty("route")] + public RouteDetail Route { get; set; } + } + + /// + /// Route details + /// + public class RouteDetail + { + /// + /// Gets or sets the network of the route + /// + [JsonProperty("network")] + public string Network { get; set; } + + /// + /// Gets or sets the gateway of the route + /// + [JsonProperty("gateway")] + public string Gateway { get; set; } + + /// + /// Gets or sets the description of the route + /// + [JsonProperty("descr")] + public string Description { get; set; } + + /// + /// Gets or sets whether the route is disabled + /// + [JsonProperty("disabled")] + public string Disabled { get; set; } + } + + /// + /// Route configuration + /// + public class RouteConfig + { + /// + /// Gets or sets the network of the route + /// + [JsonProperty("network")] + public string Network { get; set; } + + /// + /// Gets or sets the gateway of the route + /// + [JsonProperty("gateway")] + public string Gateway { get; set; } + + /// + /// Gets or sets the description of the route + /// + [JsonProperty("descr")] + public string Description { get; set; } + + /// + /// Gets or sets whether the route is disabled + /// + [JsonProperty("disabled")] + public string Disabled { get; set; } = "0"; + } + + /// + /// Response for creating a route + /// + public class RouteCreateResponse + { + /// + /// Gets or sets the UUID of the new route + /// + [JsonProperty("uuid")] + public string Uuid { get; set; } + } + + /// + /// Response for updating a route + /// + public class RouteUpdateResponse + { + /// + /// Gets or sets the result of the update + /// + [JsonProperty("result")] + public string Result { get; set; } + } + + /// + /// Response for deleting a route + /// + public class RouteDeleteResponse + { + /// + /// Gets or sets the result of the deletion + /// + [JsonProperty("result")] + public string Result { get; set; } + } + + /// + /// Response for applying route changes + /// + public class RouteApplyResponse + { + /// + /// Gets or sets the status of the apply + /// + [JsonProperty("status")] + public string Status { get; set; } + } +} diff --git a/src/PSOPNSenseAPI/Services/ServiceBase.cs b/src/PSOPNSenseAPI/Services/ServiceBase.cs new file mode 100644 index 0000000..5b14fb0 --- /dev/null +++ b/src/PSOPNSenseAPI/Services/ServiceBase.cs @@ -0,0 +1,41 @@ +using PSOPNSenseAPI.Logging; + +namespace PSOPNSenseAPI.Services +{ + /// + /// Base class for all services + /// + public abstract class ServiceBase + { + /// + /// Gets the API client + /// + protected OPNSenseApiClient ApiClient { get; } + + /// + /// Gets the logger + /// + protected ILogger Logger { get; } + + /// + /// Initializes a new instance of the class + /// + /// The API client to use for requests + protected ServiceBase(OPNSenseApiClient apiClient) + { + ApiClient = apiClient; + Logger = new NullLogger(); + } + + /// + /// Initializes a new instance of the class + /// + /// The API client to use for requests + /// The logger to use + protected ServiceBase(OPNSenseApiClient apiClient, ILogger logger) + { + ApiClient = apiClient; + Logger = logger ?? new NullLogger(); + } + } +} diff --git a/src/PSOPNSenseAPI/Services/SystemDNSService.cs b/src/PSOPNSenseAPI/Services/SystemDNSService.cs new file mode 100644 index 0000000..dd0a5db --- /dev/null +++ b/src/PSOPNSenseAPI/Services/SystemDNSService.cs @@ -0,0 +1,161 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; +using PSOPNSenseAPI.Logging; + +namespace PSOPNSenseAPI.Services +{ + /// + /// Service for interacting with the OPNSense system DNS API + /// + public class SystemDNSService + { + private readonly OPNSenseApiClient _apiClient; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class + /// + /// The API client + /// The logger + public SystemDNSService(OPNSenseApiClient apiClient, ILogger logger) + { + _apiClient = apiClient; + _logger = logger; + } + + /// + /// Gets the system DNS configuration + /// + /// The system DNS configuration + public async Task GetSystemDNSAsync() + { + _logger.Information("Getting system DNS configuration"); + + var endpoint = "system/settings/get"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Updates the system DNS configuration + /// + /// The updated system DNS configuration + /// The response indicating success + public async Task UpdateSystemDNSAsync(SystemDNSConfig config) + { + _logger.Information("Updating system DNS configuration"); + + var endpoint = "system/settings/set"; + var data = new { system = config }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Applies system DNS changes + /// + /// The response indicating success + public async Task ApplySystemDNSChangesAsync() + { + _logger.Information("Applying system DNS changes"); + + var endpoint = "system/settings/reconfigure"; + return await _apiClient.PostAsync(endpoint); + } + } + + /// + /// Response for getting system DNS configuration + /// + public class SystemDNSResponse + { + /// + /// Gets or sets the system DNS configuration + /// + [JsonProperty("system")] + public SystemDNSConfig System { get; set; } + } + + /// + /// System DNS configuration + /// + public class SystemDNSConfig + { + /// + /// Gets or sets the hostname + /// + [JsonProperty("hostname")] + public string Hostname { get; set; } + + /// + /// Gets or sets the domain + /// + [JsonProperty("domain")] + public string Domain { get; set; } + + /// + /// Gets or sets the DNS servers + /// + [JsonProperty("dnsserver")] + public List DnsServers { get; set; } = new List(); + + /// + /// Gets or sets the DNS server options + /// + [JsonProperty("dnsallowoverride")] + public string DnsAllowOverride { get; set; } = "1"; + + /// + /// Gets or sets whether to disable DNS forwarder + /// + [JsonProperty("disablenatreflection")] + public string DisableNatReflection { get; set; } = "0"; + + /// + /// Gets or sets whether to disable DNS rebinding protection + /// + [JsonProperty("dnssecstripped")] + public string DnssecStripped { get; set; } = "0"; + + /// + /// Gets or sets the timezone + /// + [JsonProperty("timezone")] + public string Timezone { get; set; } + + /// + /// Gets or sets the time servers + /// + [JsonProperty("timeservers")] + public string TimeServers { get; set; } + + /// + /// Gets or sets the language + /// + [JsonProperty("language")] + public string Language { get; set; } + } + + /// + /// Response for updating system DNS configuration + /// + public class SystemDNSUpdateResponse + { + /// + /// Gets or sets the result of the update + /// + [JsonProperty("result")] + public string Result { get; set; } + } + + /// + /// Response for applying system DNS changes + /// + public class SystemDNSApplyResponse + { + /// + /// Gets or sets the status of the apply + /// + [JsonProperty("status")] + public string Status { get; set; } + } +} diff --git a/src/PSOPNSenseAPI/Services/SystemService.cs b/src/PSOPNSenseAPI/Services/SystemService.cs new file mode 100644 index 0000000..112bb66 --- /dev/null +++ b/src/PSOPNSenseAPI/Services/SystemService.cs @@ -0,0 +1,182 @@ +using System.Threading.Tasks; +using Newtonsoft.Json; +using PSOPNSenseAPI.Logging; + +namespace PSOPNSenseAPI.Services +{ + /// + /// Service for interacting with the OPNSense system API + /// + public class SystemService + { + private readonly OPNSenseApiClient _apiClient; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class + /// + /// The API client + /// The logger + public SystemService(OPNSenseApiClient apiClient, ILogger logger) + { + _apiClient = apiClient; + _logger = logger; + } + + /// + /// Reboots the firewall + /// + /// The response indicating success + public async Task RebootAsync() + { + _logger.Information("Rebooting firewall"); + + var endpoint = "core/system/reboot"; + return await _apiClient.PostAsync(endpoint); + } + + /// + /// Gets the system status + /// + /// The system status + public async Task GetStatusAsync() + { + _logger.Information("Getting system status"); + + var endpoint = "core/system/status"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Gets the system information + /// + /// The system information + public async Task GetInfoAsync() + { + _logger.Information("Getting system information"); + + var endpoint = "core/system/info"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Halts the firewall + /// + /// The response indicating success + public async Task HaltAsync() + { + _logger.Information("Halting firewall"); + + var endpoint = "core/system/halt"; + return await _apiClient.PostAsync(endpoint); + } + } + + /// + /// Response for rebooting the firewall + /// + public class SystemRebootResponse + { + /// + /// Gets or sets the status of the reboot + /// + [JsonProperty("status")] + public string Status { get; set; } + } + + /// + /// Response for getting the system status + /// + public class SystemStatusResponse + { + /// + /// Gets or sets the system status + /// + [JsonProperty("status")] + public string Status { get; set; } + + /// + /// Gets or sets the system uptime + /// + [JsonProperty("uptime")] + public string Uptime { get; set; } + + /// + /// Gets or sets the system date + /// + [JsonProperty("date")] + public string Date { get; set; } + + /// + /// Gets or sets the system time + /// + [JsonProperty("time")] + public string Time { get; set; } + + /// + /// Gets or sets the system load + /// + [JsonProperty("load")] + public string Load { get; set; } + } + + /// + /// Response for getting the system information + /// + public class SystemInfoResponse + { + /// + /// Gets or sets the system product name + /// + [JsonProperty("product_name")] + public string ProductName { get; set; } + + /// + /// Gets or sets the system product version + /// + [JsonProperty("product_version")] + public string ProductVersion { get; set; } + + /// + /// Gets or sets the system product architecture + /// + [JsonProperty("product_arch")] + public string ProductArchitecture { get; set; } + + /// + /// Gets or sets the system platform + /// + [JsonProperty("platform")] + public string Platform { get; set; } + + /// + /// Gets or sets the system CPU model + /// + [JsonProperty("cpu_model")] + public string CpuModel { get; set; } + + /// + /// Gets or sets the system CPU count + /// + [JsonProperty("cpu_count")] + public string CpuCount { get; set; } + + /// + /// Gets or sets the system memory + /// + [JsonProperty("memory")] + public string Memory { get; set; } + } + + /// + /// Response for halting the firewall + /// + public class SystemHaltResponse + { + /// + /// Gets or sets the status of the halt + /// + [JsonProperty("status")] + public string Status { get; set; } + } +} diff --git a/src/PSOPNSenseAPI/Services/TailscaleService.cs b/src/PSOPNSenseAPI/Services/TailscaleService.cs new file mode 100644 index 0000000..1d94ffc --- /dev/null +++ b/src/PSOPNSenseAPI/Services/TailscaleService.cs @@ -0,0 +1,362 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; +using PSOPNSenseAPI.Logging; + +namespace PSOPNSenseAPI.Services +{ + /// + /// Service for interacting with the OPNSense Tailscale API + /// + public class TailscaleService + { + private readonly OPNSenseApiClient _apiClient; + private readonly ILogger _logger; + private readonly PluginService _pluginService; + + /// + /// Initializes a new instance of the class + /// + /// The API client + /// The logger + public TailscaleService(OPNSenseApiClient apiClient, ILogger logger) + { + _apiClient = apiClient; + _logger = logger; + _pluginService = new PluginService(apiClient, logger); + } + + /// + /// Checks if the Tailscale plugin is installed + /// + /// True if the plugin is installed, false otherwise + public async Task IsPluginInstalledAsync() + { + _logger.Information("Checking if Tailscale plugin is installed"); + + try + { + var plugins = await _pluginService.GetInstalledPluginsAsync(); + return plugins != null; + } + catch (Exception ex) + { + _logger.Error($"Error checking if Tailscale plugin is installed: {ex.Message}"); + return false; + } + } + + /// + /// Installs the Tailscale plugin + /// + /// True if the installation was successful, false otherwise + public async Task InstallPluginAsync() + { + _logger.Information("Installing Tailscale plugin"); + + try + { + var result = await _pluginService.InstallPluginAsync("os-tailscale"); + return result != null && result.Status == "ok"; + } + catch (Exception ex) + { + _logger.Error($"Error installing Tailscale plugin: {ex.Message}"); + return false; + } + } + + /// + /// Gets the Tailscale settings + /// + /// The Tailscale settings + public async Task GetSettingsAsync() + { + _logger.Information("Getting Tailscale settings"); + + var endpoint = "tailscale/general/get"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Updates the Tailscale settings + /// + /// The updated settings + /// The response indicating success + public async Task UpdateSettingsAsync(TailscaleSettings settings) + { + _logger.Information("Updating Tailscale settings"); + + var endpoint = "tailscale/general/set"; + var data = new { general = settings }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Gets the Tailscale status + /// + /// The Tailscale status + public async Task GetStatusAsync() + { + _logger.Information("Getting Tailscale status"); + + var endpoint = "tailscale/service/status"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Starts the Tailscale service + /// + /// The response indicating success + public async Task StartServiceAsync() + { + _logger.Information("Starting Tailscale service"); + + var endpoint = "tailscale/service/start"; + return await _apiClient.PostAsync(endpoint); + } + + /// + /// Stops the Tailscale service + /// + /// The response indicating success + public async Task StopServiceAsync() + { + _logger.Information("Stopping Tailscale service"); + + var endpoint = "tailscale/service/stop"; + return await _apiClient.PostAsync(endpoint); + } + + /// + /// Restarts the Tailscale service + /// + /// The response indicating success + public async Task RestartServiceAsync() + { + _logger.Information("Restarting Tailscale service"); + + var endpoint = "tailscale/service/restart"; + return await _apiClient.PostAsync(endpoint); + } + + /// + /// Connects to the Tailscale network using an auth key + /// + /// The auth key to use + /// The response indicating success + public async Task ConnectAsync(string authKey) + { + _logger.Information("Connecting to Tailscale network"); + + var endpoint = "tailscale/service/connect"; + var data = new { authkey = authKey }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Disconnects from the Tailscale network + /// + /// The response indicating success + public async Task DisconnectAsync() + { + _logger.Information("Disconnecting from Tailscale network"); + + var endpoint = "tailscale/service/disconnect"; + return await _apiClient.PostAsync(endpoint); + } + + /// + /// Gets the Tailscale interfaces + /// + /// The Tailscale interfaces + public async Task GetInterfacesAsync() + { + _logger.Information("Getting Tailscale interfaces"); + + var endpoint = "tailscale/service/interfaces"; + return await _apiClient.GetAsync(endpoint); + } + } + + /// + /// Response for getting Tailscale settings + /// + public class TailscaleSettingsResponse + { + /// + /// Gets or sets the Tailscale settings + /// + [JsonProperty("general")] + public TailscaleSettings General { get; set; } + } + + /// + /// Tailscale settings + /// + public class TailscaleSettings + { + /// + /// Gets or sets whether Tailscale is enabled + /// + [JsonProperty("enabled")] + public string Enabled { get; set; } = "0"; + + /// + /// Gets or sets whether to accept DNS configuration from Tailscale + /// + [JsonProperty("accept_dns")] + public string AcceptDns { get; set; } = "0"; + + /// + /// Gets or sets whether to accept routes from Tailscale + /// + [JsonProperty("accept_routes")] + public string AcceptRoutes { get; set; } = "0"; + + /// + /// Gets or sets whether to advertise exit node + /// + [JsonProperty("advertise_exit_node")] + public string AdvertiseExitNode { get; set; } = "0"; + + /// + /// Gets or sets whether to advertise routes + /// + [JsonProperty("advertise_routes")] + public string AdvertiseRoutes { get; set; } = "0"; + + /// + /// Gets or sets the routes to advertise + /// + [JsonProperty("routes_to_advertise")] + public string RoutesToAdvertise { get; set; } = ""; + + /// + /// Gets or sets the hostname + /// + [JsonProperty("hostname")] + public string Hostname { get; set; } = ""; + + /// + /// Gets or sets the login server + /// + [JsonProperty("login_server")] + public string LoginServer { get; set; } = ""; + + /// + /// Gets or sets whether to use SSH + /// + [JsonProperty("ssh")] + public string Ssh { get; set; } = "0"; + + /// + /// Gets or sets the ephemeral node key + /// + [JsonProperty("ephemeral")] + public string Ephemeral { get; set; } = "0"; + + /// + /// Gets or sets whether to reset on start + /// + [JsonProperty("reset_on_start")] + public string ResetOnStart { get; set; } = "0"; + } + + /// + /// Response for updating Tailscale settings + /// + public class TailscaleUpdateResponse + { + /// + /// Gets or sets the result of the update + /// + [JsonProperty("result")] + public string Result { get; set; } + } + + /// + /// Response for Tailscale service operations + /// + public class TailscaleServiceResponse + { + /// + /// Gets or sets the status of the operation + /// + [JsonProperty("status")] + public string Status { get; set; } + } + + /// + /// Response for getting Tailscale status + /// + public class TailscaleStatusResponse + { + /// + /// Gets or sets whether the service is running + /// + [JsonProperty("running")] + public bool Running { get; set; } + + /// + /// Gets or sets whether the service is enabled + /// + [JsonProperty("enabled")] + public bool Enabled { get; set; } + + /// + /// Gets or sets the status message + /// + [JsonProperty("status")] + public string Status { get; set; } + } + + /// + /// Response for connecting to Tailscale + /// + public class TailscaleConnectResponse + { + /// + /// Gets or sets the status of the connection + /// + [JsonProperty("status")] + public string Status { get; set; } + + /// + /// Gets or sets the message + /// + [JsonProperty("message")] + public string Message { get; set; } + } + + /// + /// Response for getting Tailscale interfaces + /// + public class TailscaleInterfacesResponse + { + /// + /// Gets or sets the interfaces + /// + [JsonProperty("interfaces")] + public List Interfaces { get; set; } + } + + /// + /// Tailscale interface + /// + public class TailscaleInterface + { + /// + /// Gets or sets the name of the interface + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// Gets or sets the IP addresses of the interface + /// + [JsonProperty("ips")] + public List IpAddresses { get; set; } + } +} diff --git a/src/PSOPNSenseAPI/Services/UserService.cs b/src/PSOPNSenseAPI/Services/UserService.cs new file mode 100644 index 0000000..39bb45b --- /dev/null +++ b/src/PSOPNSenseAPI/Services/UserService.cs @@ -0,0 +1,382 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; +using PSOPNSenseAPI.Logging; + +namespace PSOPNSenseAPI.Services +{ + /// + /// Service for interacting with the OPNSense user API + /// + public class UserService + { + private readonly OPNSenseApiClient _apiClient; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class + /// + /// The API client + /// The logger + public UserService(OPNSenseApiClient apiClient, ILogger logger) + { + _apiClient = apiClient; + _logger = logger; + } + + /// + /// Gets all users + /// + /// A list of users + public async Task GetUsersAsync() + { + _logger.Information("Getting users"); + + var endpoint = "core/user/searchUser"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Gets a user by UUID + /// + /// The UUID of the user + /// The user + public async Task GetUserAsync(string uuid) + { + _logger.Information($"Getting user with UUID {uuid}"); + + var endpoint = $"core/user/getUser/{uuid}"; + return await _apiClient.GetAsync(endpoint); + } + + /// + /// Creates a new user + /// + /// The user to create + /// The response containing the UUID of the new user + public async Task CreateUserAsync(UserConfig user) + { + _logger.Information("Creating user"); + + var endpoint = "core/user/addUser"; + var data = new { user = user }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Updates a user + /// + /// The UUID of the user to update + /// The updated user + /// The response indicating success + public async Task UpdateUserAsync(string uuid, UserConfig user) + { + _logger.Information($"Updating user with UUID {uuid}"); + + var endpoint = $"core/user/setUser/{uuid}"; + var data = new { user = user }; + return await _apiClient.PostAsync(endpoint, data); + } + + /// + /// Deletes a user + /// + /// The UUID of the user to delete + /// The response indicating success + public async Task DeleteUserAsync(string uuid) + { + _logger.Information($"Deleting user with UUID {uuid}"); + + var endpoint = $"core/user/delUser/{uuid}"; + return await _apiClient.PostAsync(endpoint); + } + + /// + /// Gets all groups + /// + /// A list of groups + public async Task GetGroupsAsync() + { + _logger.Information("Getting groups"); + + var endpoint = "core/user/searchGroups"; + return await _apiClient.GetAsync(endpoint); + } + } + + /// + /// Response for getting users + /// + public class UserListResponse + { + /// + /// Gets or sets the total number of users + /// + [JsonProperty("total")] + public int Total { get; set; } + + /// + /// Gets or sets the number of rows per page + /// + [JsonProperty("rowCount")] + public int RowCount { get; set; } + + /// + /// Gets or sets the current page + /// + [JsonProperty("current")] + public int Current { get; set; } + + /// + /// Gets or sets the users + /// + [JsonProperty("rows")] + public List Rows { get; set; } + } + + /// + /// User information + /// + public class User + { + /// + /// Gets or sets the UUID of the user + /// + [JsonProperty("uuid")] + public string Uuid { get; set; } + + /// + /// Gets or sets the username of the user + /// + [JsonProperty("username")] + public string Username { get; set; } + + /// + /// Gets or sets the full name of the user + /// + [JsonProperty("fullname")] + public string FullName { get; set; } + + /// + /// Gets or sets the email of the user + /// + [JsonProperty("email")] + public string Email { get; set; } + + /// + /// Gets or sets whether the user is enabled + /// + [JsonProperty("disabled")] + public string Disabled { get; set; } + + /// + /// Gets or sets the groups of the user + /// + [JsonProperty("groups")] + public string Groups { get; set; } + } + + /// + /// Response for getting a user + /// + public class UserResponse + { + /// + /// Gets or sets the user + /// + [JsonProperty("user")] + public UserDetail User { get; set; } + } + + /// + /// User details + /// + public class UserDetail + { + /// + /// Gets or sets the username of the user + /// + [JsonProperty("username")] + public string Username { get; set; } + + /// + /// Gets or sets the full name of the user + /// + [JsonProperty("fullname")] + public string FullName { get; set; } + + /// + /// Gets or sets the email of the user + /// + [JsonProperty("email")] + public string Email { get; set; } + + /// + /// Gets or sets whether the user is enabled + /// + [JsonProperty("disabled")] + public string Disabled { get; set; } + + /// + /// Gets or sets the groups of the user + /// + [JsonProperty("groups")] + public List Groups { get; set; } + + /// + /// Gets or sets the authorizations of the user + /// + [JsonProperty("authorizations")] + public List Authorizations { get; set; } + } + + /// + /// User configuration + /// + public class UserConfig + { + /// + /// Gets or sets the username of the user + /// + [JsonProperty("username")] + public string Username { get; set; } + + /// + /// Gets or sets the password of the user + /// + [JsonProperty("password")] + public string Password { get; set; } + + /// + /// Gets or sets the full name of the user + /// + [JsonProperty("fullname")] + public string FullName { get; set; } + + /// + /// Gets or sets the email of the user + /// + [JsonProperty("email")] + public string Email { get; set; } + + /// + /// Gets or sets whether the user is enabled + /// + [JsonProperty("disabled")] + public string Disabled { get; set; } = "0"; + + /// + /// Gets or sets the groups of the user + /// + [JsonProperty("groups")] + public List Groups { get; set; } + + /// + /// Gets or sets the authorizations of the user + /// + [JsonProperty("authorizations")] + public List Authorizations { get; set; } + } + + /// + /// Response for creating a user + /// + public class UserCreateResponse + { + /// + /// Gets or sets the UUID of the new user + /// + [JsonProperty("uuid")] + public string Uuid { get; set; } + } + + /// + /// Response for updating a user + /// + public class UserUpdateResponse + { + /// + /// Gets or sets the result of the update + /// + [JsonProperty("result")] + public string Result { get; set; } + } + + /// + /// Response for deleting a user + /// + public class UserDeleteResponse + { + /// + /// Gets or sets the result of the deletion + /// + [JsonProperty("result")] + public string Result { get; set; } + } + + /// + /// Response for getting groups + /// + public class GroupListResponse + { + /// + /// Gets or sets the total number of groups + /// + [JsonProperty("total")] + public int Total { get; set; } + + /// + /// Gets or sets the number of rows per page + /// + [JsonProperty("rowCount")] + public int RowCount { get; set; } + + /// + /// Gets or sets the current page + /// + [JsonProperty("current")] + public int Current { get; set; } + + /// + /// Gets or sets the groups + /// + [JsonProperty("rows")] + public List Rows { get; set; } + } + + /// + /// Group information + /// + public class Group + { + /// + /// Gets or sets the UUID of the group + /// + [JsonProperty("uuid")] + public string Uuid { get; set; } + + /// + /// Gets or sets the name of the group + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// Gets or sets the description of the group + /// + [JsonProperty("description")] + public string Description { get; set; } + + /// + /// Gets or sets the scope of the group + /// + [JsonProperty("scope")] + public string Scope { get; set; } + + /// + /// Gets or sets whether the group is enabled + /// + [JsonProperty("enabled")] + public string Enabled { get; set; } + } +} diff --git a/src/PSOPNSenseAPI/Utilities/NetworkUtility.cs b/src/PSOPNSenseAPI/Utilities/NetworkUtility.cs new file mode 100644 index 0000000..8d2683a --- /dev/null +++ b/src/PSOPNSenseAPI/Utilities/NetworkUtility.cs @@ -0,0 +1,605 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Numerics; +using System.Linq; +using static System.Net.IPNetwork2; + +namespace PSOPNSenseAPI.Utilities +{ + /// + /// Utility class for network operations using IPNetwork + /// + public static class NetworkUtility + { + /// + /// Parses a CIDR notation string into an IPNetwork2 object + /// + /// The CIDR notation string (e.g., "192.168.1.0/24") + /// An IPNetwork2 object + public static IPNetwork2 ParseCIDR(string cidr) + { + return IPNetwork2.Parse(cidr); + } + + /// + /// Tries to parse a CIDR notation string into an IPNetwork2 object + /// + /// The CIDR notation string (e.g., "192.168.1.0/24") + /// The resulting IPNetwork2 object if parsing succeeds + /// True if parsing succeeds, false otherwise + public static bool TryParseCIDR(string cidr, out IPNetwork2 network) + { + return IPNetwork2.TryParse(cidr, out network); + } + + /// + /// Checks if an IP address is within a network + /// + /// The network to check + /// The IP address to check + /// True if the IP address is within the network, false otherwise + public static bool Contains(IPNetwork2 network, IPAddress ipAddress) + { + return network.Contains(ipAddress); + } + + /// + /// Checks if a network is within another network + /// + /// The container network + /// The subnet to check + /// True if the subnet is within the container network, false otherwise + public static bool Contains(IPNetwork2 container, IPNetwork2 subnet) + { + return container.Contains(subnet); + } + + /// + /// Checks if two networks overlap + /// + /// The first network + /// The second network + /// True if the networks overlap, false otherwise + public static bool Overlaps(IPNetwork2 network1, IPNetwork2 network2) + { + return network1.Overlap(network2); + } + + /// + /// Divides a network into subnets of a specified prefix length + /// + /// The network to divide + /// The prefix length of the subnets + /// An enumerable of subnets + public static IEnumerable Subnet(IPNetwork2 network, int prefixLength) + { + return network.Subnet((byte)prefixLength); + } + + /// + /// Divides a network into a specified number of subnets + /// + /// The network to divide + /// The number of subnets to create + /// An enumerable of subnets + public static IEnumerable SubnetByCount(IPNetwork2 network, int subnetCount) + { + // Calculate the prefix length needed for the specified number of subnets + int bitsNeeded = (int)Math.Ceiling(Math.Log(subnetCount, 2)); + int newPrefixLength = network.Cidr + bitsNeeded; + + // Ensure the prefix length is valid + if (newPrefixLength > (network.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork ? 32 : 128)) + { + throw new ArgumentException($"Cannot create {subnetCount} subnets from {network}. The resulting prefix length would be too large."); + } + + return Subnet(network, newPrefixLength); + } + + /// + /// Gets the first usable host address in a network + /// + /// The network + /// The first usable host address + public static IPAddress GetFirstUsableHost(IPNetwork2 network) + { + // For /31 and /32 networks, the network address is usable + if (network.Cidr >= 31 && network.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) + return network.Network; + + // For /127 and /128 networks, the network address is usable + if (network.Cidr >= 127 && network.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6) + return network.Network; + + // Otherwise, the first usable host is the network address + 1 + byte[] addressBytes = network.Network.GetAddressBytes(); + + // Increment the last byte + for (int i = addressBytes.Length - 1; i >= 0; i--) + { + if (addressBytes[i] < 255) + { + addressBytes[i]++; + break; + } + addressBytes[i] = 0; + } + + return new IPAddress(addressBytes); + } + + /// + /// Gets the last usable host address in a network + /// + /// The network + /// The last usable host address + public static IPAddress GetLastUsableHost(IPNetwork2 network) + { + // For /31 and /32 networks, the broadcast address is usable + if (network.Cidr >= 31 && network.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) + return network.Broadcast; + + // For /127 and /128 networks, the broadcast address is usable + if (network.Cidr >= 127 && network.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6) + return network.Broadcast; + + // Otherwise, the last usable host is the broadcast address - 1 + byte[] addressBytes = network.Broadcast.GetAddressBytes(); + + // Decrement the last byte + for (int i = addressBytes.Length - 1; i >= 0; i--) + { + if (addressBytes[i] > 0) + { + addressBytes[i]--; + break; + } + addressBytes[i] = 255; + } + + return new IPAddress(addressBytes); + } + + /// + /// Gets the total number of usable host addresses in a network + /// + /// The network + /// The number of usable host addresses + public static BigInteger GetUsableHostCount(IPNetwork2 network) + { + // For /31 networks, there are 2 usable hosts + if (network.Cidr == 31 && network.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) + return 2; + + // For /32 networks, there is 1 usable host + if (network.Cidr == 32 && network.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) + return 1; + + // For /127 networks, there are 2 usable hosts + if (network.Cidr == 127 && network.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6) + return 2; + + // For /128 networks, there is 1 usable host + if (network.Cidr == 128 && network.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6) + return 1; + + // Otherwise, the number of usable hosts is 2^(32-prefix) - 2 for IPv4 + // or 2^(128-prefix) for IPv6 (we don't subtract 2 for IPv6 as the network and broadcast addresses are usable) + if (network.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) + { + // For IPv4, subtract 2 for network and broadcast addresses + return network.Total - 2 > 0 ? network.Total - 2 : network.Total; + } + else + { + // For IPv6, all addresses are usable + return network.Total; + } + } + + /// + /// Supernets two or more networks into a single network + /// + /// The networks to supernet + /// The supernet network + public static IPNetwork2 Supernet(params IPNetwork2[] networks) + { + if (networks == null || networks.Length < 2) + throw new ArgumentException("At least two networks are required for supernetting."); + + IPNetwork2[] result = IPNetwork2.Supernet(networks); + if (result.Length > 0) + return result[0]; + throw new InvalidOperationException("Failed to supernet the networks."); + } + + /// + /// Attempts to supernet a list of networks into the smallest possible set of summary networks + /// + /// The networks to summarize + /// A list of summarized networks + public static List SupernetSummarize(IEnumerable networks) + { + if (networks == null) + throw new ArgumentNullException(nameof(networks)); + + var networkList = networks.ToList(); + if (networkList.Count == 0) + return new List(); + if (networkList.Count == 1) + return networkList; + + // Sort networks by address family, then by network address + networkList.Sort((a, b) => + { + int addressFamilyComparison = a.AddressFamily.CompareTo(b.AddressFamily); + if (addressFamilyComparison != 0) + return addressFamilyComparison; + + return CompareIPAddresses(a.Network, b.Network); + }); + + var result = new List(); + var currentGroup = new List { networkList[0] }; + var currentAddressFamily = networkList[0].AddressFamily; + + for (int i = 1; i < networkList.Count; i++) + { + var network = networkList[i]; + + // If address family changes, summarize the current group and start a new one + if (network.AddressFamily != currentAddressFamily) + { + result.AddRange(SupernetGroup(currentGroup)); + currentGroup.Clear(); + currentGroup.Add(network); + currentAddressFamily = network.AddressFamily; + continue; + } + + // Check if the current network can be part of the current group + var lastNetwork = currentGroup[currentGroup.Count - 1]; + if (CanBeCombined(lastNetwork, network)) + { + currentGroup.Add(network); + } + else + { + // If not, summarize the current group and start a new one with this network + result.AddRange(SupernetGroup(currentGroup)); + currentGroup.Clear(); + currentGroup.Add(network); + } + } + + // Don't forget to process the last group + if (currentGroup.Count > 0) + { + result.AddRange(SupernetGroup(currentGroup)); + } + + return result; + } + + /// + /// Supernets a group of networks that are known to be adjacent or overlapping + /// + /// The networks to supernet + /// A list of summarized networks + private static List SupernetGroup(List networks) + { + if (networks.Count == 1) + return new List { networks[0] }; + + // Try to find the most efficient summarization + var result = new List(); + var currentSet = new List(); + + foreach (var network in networks) + { + if (currentSet.Count == 0) + { + currentSet.Add(network); + continue; + } + + // Try to supernet with the current set + var tempSet = new List(currentSet) { network }; + var supernetResult = IPNetwork2.Supernet(tempSet.ToArray()); + if (supernetResult.Length == 0) + continue; + + var supernetted = supernetResult[0]; + + // Check if the supernet is efficient (doesn't include too many extra addresses) + bool isEfficient = IsEfficientSupernet(tempSet, supernetted); + + if (isEfficient) + { + currentSet.Add(network); + } + else + { + // If not efficient, summarize the current set and start a new one + if (currentSet.Count > 1) + { + IPNetwork2[] innerSupernetResult = IPNetwork2.Supernet(currentSet.ToArray()); + if (innerSupernetResult.Length > 0) + result.Add(innerSupernetResult[0]); + else + result.AddRange(currentSet); + } + else + { + result.Add(currentSet[0]); + } + currentSet.Clear(); + currentSet.Add(network); + } + } + + // Process the last set + if (currentSet.Count > 1) + { + IPNetwork2[] lastSupernetResult = IPNetwork2.Supernet(currentSet.ToArray()); + if (lastSupernetResult.Length > 0) + result.Add(lastSupernetResult[0]); + else + result.AddRange(currentSet); + } + else if (currentSet.Count == 1) + { + result.Add(currentSet[0]); + } + + return result; + } + + /// + /// Determines if two networks can potentially be combined in a supernet + /// + /// The first network + /// The second network + /// True if the networks can be combined, false otherwise + private static bool CanBeCombined(IPNetwork2 network1, IPNetwork2 network2) + { + // Different address families cannot be combined + if (network1.AddressFamily != network2.AddressFamily) + return false; + + // Check if the networks are adjacent or overlapping + if (network1.Overlap(network2)) + return true; + + // Check if they're adjacent (one network's broadcast + 1 = other network's network address) + byte[] broadcast1 = network1.Broadcast.GetAddressBytes(); + byte[] network2Addr = network2.Network.GetAddressBytes(); + + // Add 1 to broadcast1 + for (int i = broadcast1.Length - 1; i >= 0; i--) + { + if (broadcast1[i] < 255) + { + broadcast1[i]++; + break; + } + broadcast1[i] = 0; + } + + // Compare the arrays + for (int i = 0; i < broadcast1.Length; i++) + { + if (broadcast1[i] != network2Addr[i]) + return false; + } + + return true; + } + + /// + /// Determines if a supernet is efficient (doesn't include too many extra addresses) + /// + /// The original networks + /// The supernet + /// The efficiency threshold (0.0 to 1.0) that determines if a supernet is efficient + /// True if the supernet is efficient, false otherwise + private static bool IsEfficientSupernet(List networks, IPNetwork2 supernet, double threshold = 0.5) + { + // Calculate the total addresses in the original networks + BigInteger originalTotal = 0; + foreach (var network in networks) + { + originalTotal += network.Total; + } + + // Calculate the efficiency as a percentage + double originalTotalDouble = (double)((decimal)originalTotal); + double supernetTotalDouble = (double)((decimal)supernet.Total); + double efficiency = originalTotalDouble / supernetTotalDouble; + + // Consider it efficient if it's at least the threshold percentage efficient + return efficiency >= threshold; + } + + /// + /// Compares two IP addresses + /// + /// The first IP address + /// The second IP address + /// A negative value if a is less than b, 0 if they're equal, a positive value if a is greater than b + private static int CompareIPAddresses(IPAddress a, IPAddress b) + { + byte[] bytesA = a.GetAddressBytes(); + byte[] bytesB = b.GetAddressBytes(); + + // Different lengths (IPv4 vs IPv6) + if (bytesA.Length != bytesB.Length) + return bytesA.Length - bytesB.Length; + + // Compare byte by byte + for (int i = 0; i < bytesA.Length; i++) + { + if (bytesA[i] != bytesB[i]) + return bytesA[i] - bytesB[i]; + } + + return 0; // Equal + } + + /// + /// Gets the CIDR notation for a subnet mask + /// + /// The subnet mask (e.g., "255.255.255.0") + /// The CIDR prefix length + public static int GetCIDRFromSubnetMask(string subnetMask) + { + IPAddress mask = IPAddress.Parse(subnetMask); + byte[] bytes = mask.GetAddressBytes(); + int cidr = 0; + + foreach (byte b in bytes) + { + for (int i = 0; i < 8; i++) + { + if ((b & (1 << (7 - i))) != 0) + { + cidr++; + } + else + { + // Once we hit a 0 bit, all remaining bits should be 0 + // If not, this is an invalid subnet mask + for (int j = i + 1; j < 8; j++) + { + if ((b & (1 << (7 - j))) != 0) + { + throw new ArgumentException($"Invalid subnet mask: {subnetMask}"); + } + } + return cidr; + } + } + } + + return cidr; + } + + /// + /// Gets the subnet mask for a CIDR prefix length + /// + /// The CIDR prefix length + /// The subnet mask + public static string GetSubnetMaskFromCIDR(int cidr) + { + if (cidr < 0 || cidr > 32) + throw new ArgumentException($"Invalid CIDR prefix length: {cidr}. Must be between 0 and 32."); + + uint mask = 0xffffffff; + mask = mask << (32 - cidr); + + byte[] bytes = new byte[4]; + bytes[0] = (byte)((mask >> 24) & 0xff); + bytes[1] = (byte)((mask >> 16) & 0xff); + bytes[2] = (byte)((mask >> 8) & 0xff); + bytes[3] = (byte)(mask & 0xff); + + return new IPAddress(bytes).ToString(); + } + + /// + /// Converts an IP address to its integer representation + /// + /// The IP address + /// The integer representation of the IP address + public static uint IPAddressToUInt32(IPAddress ipAddress) + { + if (ipAddress.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork) + throw new ArgumentException("Only IPv4 addresses can be converted to UInt32."); + + byte[] bytes = ipAddress.GetAddressBytes(); + return (uint)(bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3]); + } + + /// + /// Converts an integer to its IP address representation + /// + /// The integer representation of the IP address + /// The IP address + public static IPAddress UInt32ToIPAddress(uint ipInt) + { + byte[] bytes = new byte[4]; + bytes[0] = (byte)((ipInt >> 24) & 0xff); + bytes[1] = (byte)((ipInt >> 16) & 0xff); + bytes[2] = (byte)((ipInt >> 8) & 0xff); + bytes[3] = (byte)(ipInt & 0xff); + + return new IPAddress(bytes); + } + + /// + /// Checks if an IP address is a private address + /// + /// The IP address to check + /// True if the IP address is private, false otherwise + public static bool IsPrivate(IPAddress ipAddress) + { + if (ipAddress.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork) + { + // For IPv6, check if it's a unique local address (fc00::/7) + byte[] bytes = ipAddress.GetAddressBytes(); + return (bytes[0] & 0xfe) == 0xfc; + } + + uint ip = IPAddressToUInt32(ipAddress); + + // 10.0.0.0/8 + if ((ip & 0xff000000) == 0x0a000000) + return true; + + // 172.16.0.0/12 + if ((ip & 0xfff00000) == 0xac100000) + return true; + + // 192.168.0.0/16 + if ((ip & 0xffff0000) == 0xc0a80000) + return true; + + // 169.254.0.0/16 (link-local) + if ((ip & 0xffff0000) == 0xa9fe0000) + return true; + + return false; + } + + /// + /// Checks if an IP address is a loopback address + /// + /// The IP address to check + /// True if the IP address is a loopback address, false otherwise + public static bool IsLoopback(IPAddress ipAddress) + { + return IPAddress.IsLoopback(ipAddress); + } + + /// + /// Checks if an IP address is a multicast address + /// + /// The IP address to check + /// True if the IP address is a multicast address, false otherwise + public static bool IsMulticast(IPAddress ipAddress) + { + if (ipAddress.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) + { + byte[] bytes = ipAddress.GetAddressBytes(); + return (bytes[0] & 0xf0) == 0xe0; // 224.0.0.0/4 + } + else + { + byte[] bytes = ipAddress.GetAddressBytes(); + return bytes[0] == 0xff; // ff00::/8 + } + } + } +} diff --git a/src/global.json b/src/global.json new file mode 100644 index 0000000..126149e --- /dev/null +++ b/src/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "6.0.100", + "rollForward": "latestMajor" + } +} diff --git a/temp_interface_service.cs b/temp_interface_service.cs new file mode 100644 index 0000000..b448b00 --- /dev/null +++ b/temp_interface_service.cs @@ -0,0 +1,289 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; +using PSOPNSenseAPI.Logging; +using PSOPNSenseAPI.Models; + +namespace PSOPNSenseAPI.Services +{ + public class InterfaceService + { + private readonly OPNSenseApiClient _apiClient; + private readonly ILogger _logger; + + public InterfaceService(OPNSenseApiClient apiClient, ILogger logger) + { + _apiClient = apiClient; + _logger = logger; + } + + public async Task GetInterfacesAsync() + { + _logger.Information("Getting interfaces"); + + var endpoint = "interfaces/overview/searchInterfaces"; + return await _apiClient.GetAsync(endpoint); + } + + public async Task GetInterfaceDetailAsync(string interfaceName) + { + _logger.Information($"Getting interface {interfaceName}"); + + var endpoint = $"interfaces/overview/getInterface/{interfaceName}"; + return await _apiClient.GetAsync(endpoint); + } + + public async Task GetInterfaceStatisticsAsync() + { + _logger.Information("Getting interface statistics"); + + var endpoint = "diagnostics/interface/getInterfaceStatistics"; + return await _apiClient.GetAsync(endpoint); + } + + public async Task UpdateInterfaceAsync(string interfaceName, InterfaceConfig interfaceConfig) + { + _logger.Information($"Updating interface {interfaceName}"); + + var endpoint = $"interfaces/overview/setInterface/{interfaceName}"; + var data = new { interface = interfaceConfig }; + return await _apiClient.PostAsync(endpoint, data); + } + + public async Task RestartInterfaceAsync(string interfaceName) + { + _logger.Information($"Restarting interface {interfaceName}"); + + var endpoint = $"interfaces/overview/reconfigure/{interfaceName}"; + return await _apiClient.PostAsync(endpoint); + } + + public async Task GetVLANsAsync() + { + _logger.Information("Getting VLANs"); + + var endpoint = "interfaces/vlan/searchItem"; + return await _apiClient.GetAsync(endpoint); + } + + public async Task GetVLANAsync(string uuid) + { + _logger.Information($"Getting VLAN with UUID {uuid}"); + + var endpoint = $"interfaces/vlan/getItem/{uuid}"; + return await _apiClient.GetAsync(endpoint); + } + + public async Task CreateVLANAsync(VLANConfig vlan) + { + _logger.Information("Creating VLAN"); + + var endpoint = "interfaces/vlan/addItem"; + var data = new { vlan = vlan }; + return await _apiClient.PostAsync(endpoint, data); + } + + public async Task UpdateVLANAsync(string uuid, VLANConfig vlan) + { + _logger.Information($"Updating VLAN with UUID {uuid}"); + + var endpoint = $"interfaces/vlan/setItem/{uuid}"; + var data = new { vlan = vlan }; + return await _apiClient.PostAsync(endpoint, data); + } + + public async Task DeleteVLANAsync(string uuid) + { + _logger.Information($"Deleting VLAN with UUID {uuid}"); + + var endpoint = $"interfaces/vlan/delItem/{uuid}"; + return await _apiClient.PostAsync(endpoint); + } + } + + public class InterfaceListResponse + { + [JsonProperty("rows")] + public List Interfaces { get; set; } + } + + public class InterfaceInfo + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("ipaddr")] + public string IpAddress { get; set; } + + [JsonProperty("subnet")] + public string SubnetMask { get; set; } + + [JsonProperty("gateway")] + public string Gateway { get; set; } + + [JsonProperty("status")] + public string Status { get; set; } + } + + public class InterfaceDetailResponse + { + [JsonProperty("interface")] + public InterfaceDetail Interface { get; set; } + } + + public class InterfaceDetail + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("ipaddr")] + public string IpAddress { get; set; } + + [JsonProperty("subnet")] + public string SubnetMask { get; set; } + + [JsonProperty("gateway")] + public string Gateway { get; set; } + + [JsonProperty("enable")] + public string Enabled { get; set; } + } + + public class InterfaceConfig + { + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("ipaddr")] + public string IpAddress { get; set; } + + [JsonProperty("subnet")] + public string SubnetMask { get; set; } + + [JsonProperty("gateway")] + public string Gateway { get; set; } + + [JsonProperty("enable")] + public string Enabled { get; set; } + } + + public class InterfaceUpdateResponse + { + [JsonProperty("result")] + public string Result { get; set; } + } + + public class InterfaceRestartResponse + { + [JsonProperty("status")] + public string Status { get; set; } + } + + public class InterfaceStatisticsResponse + { + [JsonProperty("statistics")] + public Dictionary Statistics { get; set; } + } + + public class InterfaceStatistics + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("inpkts")] + public string InPackets { get; set; } + + [JsonProperty("outpkts")] + public string OutPackets { get; set; } + + [JsonProperty("inbytes")] + public string InBytes { get; set; } + + [JsonProperty("outbytes")] + public string OutBytes { get; set; } + } + + public class VLANListResponse + { + [JsonProperty("rows")] + public List Rows { get; set; } + } + + public class VLAN + { + [JsonProperty("uuid")] + public string Uuid { get; set; } + + [JsonProperty("if")] + public string Interface { get; set; } + + [JsonProperty("tag")] + public string Tag { get; set; } + + [JsonProperty("pcp")] + public string Priority { get; set; } + + [JsonProperty("descr")] + public string Description { get; set; } + } + + public class VLANResponse + { + [JsonProperty("vlan")] + public VLANDetail Vlan { get; set; } + } + + public class VLANDetail + { + [JsonProperty("if")] + public string Interface { get; set; } + + [JsonProperty("tag")] + public string Tag { get; set; } + + [JsonProperty("pcp")] + public string Priority { get; set; } + + [JsonProperty("descr")] + public string Description { get; set; } + } + + public class VLANConfig + { + [JsonProperty("if")] + public string Interface { get; set; } + + [JsonProperty("tag")] + public string Tag { get; set; } + + [JsonProperty("pcp")] + public string Priority { get; set; } + + [JsonProperty("descr")] + public string Description { get; set; } + } + + public class VLANCreateResponse + { + [JsonProperty("uuid")] + public string Uuid { get; set; } + } + + public class VLANUpdateResponse + { + [JsonProperty("result")] + public string Result { get; set; } + } + + public class VLANDeleteResponse + { + [JsonProperty("result")] + public string Result { get; set; } + } +} diff --git a/tests/PSOPNSenseAPI.Tests/InterfaceServiceTests.cs b/tests/PSOPNSenseAPI.Tests/InterfaceServiceTests.cs new file mode 100644 index 0000000..972eb59 --- /dev/null +++ b/tests/PSOPNSenseAPI.Tests/InterfaceServiceTests.cs @@ -0,0 +1,128 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Newtonsoft.Json.Linq; +using PSOPNSenseAPI.Logging; +using PSOPNSenseAPI.Services; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace PSOPNSenseAPI.Tests +{ + [TestClass] + public class InterfaceServiceTests + { + private Mock _mockApiClient; + private Mock _mockLogger; + private InterfaceService _interfaceService; + + [TestInitialize] + public void Setup() + { + _mockApiClient = new Mock(); + _mockLogger = new Mock(); + _interfaceService = new InterfaceService(_mockApiClient.Object, _mockLogger.Object); + } + + [TestMethod] + public async Task GetInterfacesAsync_ReturnsInterfaces() + { + // Arrange + var response = new InterfaceListResponse + { + Interfaces = new List + { + new InterfaceInfo + { + Name = "wan", + Description = "WAN", + IpAddress = "192.168.1.1", + SubnetMask = "255.255.255.0", + Gateway = "192.168.1.254", + Status = "up" + } + } + }; + + _mockApiClient.Setup(x => x.GetAsync("interfaces/overview/searchInterfaces")) + .ReturnsAsync(response); + + // Act + var result = await _interfaceService.GetInterfacesAsync(); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(1, result.Interfaces.Count); + Assert.AreEqual("wan", result.Interfaces[0].Name); + Assert.AreEqual("WAN", result.Interfaces[0].Description); + Assert.AreEqual("192.168.1.1", result.Interfaces[0].IpAddress); + Assert.AreEqual("255.255.255.0", result.Interfaces[0].SubnetMask); + Assert.AreEqual("192.168.1.254", result.Interfaces[0].Gateway); + Assert.AreEqual("up", result.Interfaces[0].Status); + } + + [TestMethod] + public async Task GetInterfaceDetailAsync_ReturnsInterfaceDetail() + { + // Arrange + var interfaceName = "wan"; + var response = new InterfaceDetailResponse + { + Interface = new InterfaceDetail + { + Name = "wan", + Description = "WAN", + IpAddress = "192.168.1.1", + SubnetMask = "255.255.255.0", + Gateway = "192.168.1.254", + Enabled = "1" + } + }; + + _mockApiClient.Setup(x => x.GetAsync($"interfaces/overview/getInterface/{interfaceName}")) + .ReturnsAsync(response); + + // Act + var result = await _interfaceService.GetInterfaceDetailAsync(interfaceName); + + // Assert + Assert.IsNotNull(result); + Assert.IsNotNull(result.Interface); + Assert.AreEqual("wan", result.Interface.Name); + Assert.AreEqual("WAN", result.Interface.Description); + Assert.AreEqual("192.168.1.1", result.Interface.IpAddress); + Assert.AreEqual("255.255.255.0", result.Interface.SubnetMask); + Assert.AreEqual("192.168.1.254", result.Interface.Gateway); + Assert.AreEqual("1", result.Interface.Enabled); + } + + [TestMethod] + public async Task UpdateInterfaceAsync_ReturnsSuccess() + { + // Arrange + var interfaceName = "wan"; + var interfaceConfig = new InterfaceConfig + { + Description = "WAN", + IpAddress = "192.168.1.1", + SubnetMask = "255.255.255.0", + Gateway = "192.168.1.254", + Enabled = "1" + }; + + var response = new InterfaceUpdateResponse + { + Result = "saved" + }; + + _mockApiClient.Setup(x => x.PostAsync($"interfaces/overview/setInterface/{interfaceName}", It.IsAny())) + .ReturnsAsync(response); + + // Act + var result = await _interfaceService.UpdateInterfaceAsync(interfaceName, interfaceConfig); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual("saved", result.Result); + } + } +} diff --git a/tests/PSOPNSenseAPI.Tests/MSTestSettings.cs b/tests/PSOPNSenseAPI.Tests/MSTestSettings.cs new file mode 100644 index 0000000..aaf278c --- /dev/null +++ b/tests/PSOPNSenseAPI.Tests/MSTestSettings.cs @@ -0,0 +1 @@ +[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)] diff --git a/tests/PSOPNSenseAPI.Tests/PSOPNSenseAPI.Tests.csproj b/tests/PSOPNSenseAPI.Tests/PSOPNSenseAPI.Tests.csproj new file mode 100644 index 0000000..58477be --- /dev/null +++ b/tests/PSOPNSenseAPI.Tests/PSOPNSenseAPI.Tests.csproj @@ -0,0 +1,24 @@ + + + + net9.0 + latest + enable + enable + + + + + + + + + + + + + + + + + diff --git a/tests/PSOPNSenseAPI.Tests/Test1.cs b/tests/PSOPNSenseAPI.Tests/Test1.cs new file mode 100644 index 0000000..2979851 --- /dev/null +++ b/tests/PSOPNSenseAPI.Tests/Test1.cs @@ -0,0 +1,10 @@ +namespace PSOPNSenseAPI.Tests; + +[TestClass] +public sealed class Test1 +{ + [TestMethod] + public void TestMethod1() + { + } +}