diff --git a/.github/workflows/UnitTests.yml b/.github/workflows/UnitTests.yml index b46950dd..55e3c6b5 100644 --- a/.github/workflows/UnitTests.yml +++ b/.github/workflows/UnitTests.yml @@ -5,7 +5,7 @@ on: branches: - main - dev - - CI_6.7 + - Add-CCS paths: - "Modules/**" workflow_dispatch: @@ -23,16 +23,48 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Create Credential File + env: + DOMAINADMINUSERNAME: ${{ secrets.DOMAINADMINUSERNAME }} + DOMAINADMINPASSWORD: ${{ secrets.DOMAINADMINPASSWORD }} + run: | + $Path = "D:\PowerShell\Credentials\$($env:USERNAME)DomainAdminPesterTests.xml" + + if (Test-Path $Path) { + Remove-Item $Path -Force + } + + $securePassword = ConvertTo-SecureString $env:DOMAINADMINPASSWORD -AsPlainText -Force + $credential = New-Object System.Management.Automation.PSCredential ($env:DOMAINADMINUSERNAME, $securePassword) + $credential | Export-Clixml -Path $Path + shell: pwsh + - name: Run Pester run: | - Import-Module Pester - $configuration = New-PesterConfiguration - $configuration.Run.Exit = $true - $configuration.TestResult.OutputPath = 'C:\Temp\PesterTests.xml' - $configuration.TestResult.Enabled = $true - $configuration.TestResult.OutputFormat = 'JUnitXml' - - Invoke-Pester -Configuration $configuration + $LogFolder = 'C:\Temp' + $LogFileName = 'PesterTests.log' + + if (-Not (Test-Path $LogFolder)) { + New-Item -Path $LogFolder -ItemType Directory | Out-Null + } + + if (Test-Path "$LogFolder\$LogFileName") { + Remove-Item "$LogFolder\$LogFileName" -Force + } + + Start-Transcript -Path "$LogFolder\$LogFileName" -Append + + Import-Module Pester + $configuration = New-PesterConfiguration + $configuration.Run.Exit = $true + $configuration.TestResult.OutputPath = 'C:\Temp\PesterTests.xml' + $configuration.TestResult.Enabled = $true + $configuration.TestResult.OutputFormat = 'JUnitXml' + $configuration.Output.Verbosity = 'Detailed' + $configuration.Filter.ExcludeTag = @('Integration', 'Performance') + + Invoke-Pester -Configuration $configuration + Stop-Transcript shell: pwsh - name: Upload Test Report @@ -42,6 +74,13 @@ jobs: name: Pester Test Report path: 'C:\Temp\PesterTests.xml' + - name: Upload Test Log + if: always() + uses: actions/upload-artifact@v4 + with: + name: Pester Test Log + path: 'C:\Temp\PesterTests.log' + - name: Test Report if: always() uses: dorny/test-reporter@v2 diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADComputerToSecurityGroup.Tests.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADComputerToSecurityGroup.Tests.ps1 index 2c35e241..cf347255 100644 --- a/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADComputerToSecurityGroup.Tests.ps1 +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADComputerToSecurityGroup.Tests.ps1 @@ -1,15 +1,232 @@ -# TODO: #431 Write tests for Add-CCSADComputerToSecurityGroup -$Url = 'https://mracapa02.capainstaller.com/CCSWebservice/CCS.asmx' -$CCSCredential = Get-Credential -$DomainCredential = Get-Credential -Add-CCSADComputerToSecurityGroup -ComputerName 'MRADTEST02' -SecurityGroupName 'TestGruppe' -Domain 'Firmax.local' -Url $Url -CCSCredential $CCSCredential -DomainCredential $DomainCredential -Add-CCSADComputerToSecurityGroup -ComputerName 'MRADTEST02' -SecurityGroupName 'TestGruppe1' -Domain 'Firmax.local' -Url $Url -CCSCredential $CCSCredential -DomainCredential $DomainCredential - -$Splat = @{ - ComputerName = 'MRADTEST02' - Domain = 'Firmax.local' - Url = $Url - CCSCredential = $CCSCredential - DomainCredential = $DomainCredential -} -$Result = Remove-CCSADComputer @Splat \ No newline at end of file +BeforeAll { + $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent + + $Items = Get-ChildItem -Path "$RootPath\Capa.PowerShell.Module.CCS\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + + # Setup test environment + $script:TestUrl = "https://$(hostname).capainstaller.com/CCSWebservice/CCS.asmx" + $script:TestDomain = 'Firmax.local' + $CredentialPath = "D:\PowerShell\Credentials\$($env:USERNAME)DomainAdminPesterTests.xml" + + # Setup credentials from environment variables (GitHub secrets) + if ($env:DOMAINADMINUSERNAME -and $env:DOMAINADMINPASSWORD) { + $securePassword = ConvertTo-SecureString $env:DOMAINADMINPASSWORD -AsPlainText -Force + $script:TestDomainCredential = New-Object System.Management.Automation.PSCredential($env:DOMAINADMINUSERNAME, $securePassword) + $script:TestCCSCredential = $script:TestDomainCredential + } elseif (Test-Path -Path $CredentialPath) { + $script:TestDomainCredential = Import-Clixml -Path $CredentialPath + $script:TestCCSCredential = $script:TestDomainCredential + } else { + $script:TestDomainCredential = Get-Credential -Message 'Enter domain admin credentials for integration tests' + $script:TestCCSCredential = $script:TestDomainCredential + } + + $DebugPreference = 'Continue' + $ErrorActionPreference = 'Stop' +} + +Describe 'Add-CCSADComputerToSecurityGroup' -Tag 'Unit' { + + Context 'Parameter Validation' { + + It 'Should have mandatory ComputerName parameter' { + (Get-Command Add-CCSADComputerToSecurityGroup).Parameters['ComputerName'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory SecurityGroupName parameter' { + (Get-Command Add-CCSADComputerToSecurityGroup).Parameters['SecurityGroupName'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory Domain parameter' { + (Get-Command Add-CCSADComputerToSecurityGroup).Parameters['Domain'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory Url parameter' { + (Get-Command Add-CCSADComputerToSecurityGroup).Parameters['Url'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory CCSCredential parameter' { + (Get-Command Add-CCSADComputerToSecurityGroup).Parameters['CCSCredential'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have optional DomainCredential parameter' { + (Get-Command Add-CCSADComputerToSecurityGroup).Parameters['DomainCredential'].Attributes.Mandatory | Should -Be $false + } + + It 'Should have optional DomainOUPath parameter' { + (Get-Command Add-CCSADComputerToSecurityGroup).Parameters['DomainOUPath'].Attributes.Mandatory | Should -Be $false + } + + It 'Should accept array of computer names' { + (Get-Command Add-CCSADComputerToSecurityGroup).Parameters['ComputerName'].ParameterType.Name | Should -Be 'String[]' + } + + It 'Should accept pipeline input for ComputerName' { + (Get-Command Add-CCSADComputerToSecurityGroup).Parameters['ComputerName'].Attributes.ValueFromPipeline | Should -Be $true + } + } + + Context 'Parameter Aliases' { + + It 'Should have alias "Name" for ComputerName' { + (Get-Command Add-CCSADComputerToSecurityGroup).Parameters['ComputerName'].Aliases | Should -Contain 'Name' + } + + It 'Should have alias "Computer" for ComputerName' { + (Get-Command Add-CCSADComputerToSecurityGroup).Parameters['ComputerName'].Aliases | Should -Contain 'Computer' + } + + It 'Should have alias "GroupName" for SecurityGroupName' { + (Get-Command Add-CCSADComputerToSecurityGroup).Parameters['SecurityGroupName'].Aliases | Should -Contain 'GroupName' + } + + It 'Should have alias "OU" for DomainOUPath' { + (Get-Command Add-CCSADComputerToSecurityGroup).Parameters['DomainOUPath'].Aliases | Should -Contain 'OU' + } + } + + Context 'DomainOUPath Validation' { + + It 'Should accept empty DomainOUPath' { + { Add-CCSADComputerToSecurityGroup -ComputerName 'PC01' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath '' -WhatIf } | Should -Not -Throw + } + + It 'Should accept standard DN format' { + { Add-CCSADComputerToSecurityGroup -ComputerName 'PC01' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'OU=Computers,DC=Firmax,DC=local' -WhatIf } | Should -Not -Throw + } + + It 'Should accept DC-only format' { + { Add-CCSADComputerToSecurityGroup -ComputerName 'PC01' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'DC=Firmax,DC=local' -WhatIf } | Should -Not -Throw + } + + It 'Should accept LDAP format' { + { Add-CCSADComputerToSecurityGroup -ComputerName 'PC01' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'LDAP://DC01.Firmax.local/OU=Computers,DC=Firmax,DC=local' -WhatIf } | Should -Not -Throw + } + + It 'Should reject invalid format' { + { Add-CCSADComputerToSecurityGroup -ComputerName 'PC01' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'InvalidPath' -WhatIf } | Should -Throw + } + } + + Context 'URL Validation' { + + It 'Should accept HTTPS URL' { + { Add-CCSADComputerToSecurityGroup -ComputerName 'PC01' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Not -Throw + } + + It 'Should reject HTTP URL (only HTTPS allowed)' { + $httpUrl = $script:TestUrl -replace '^https:', 'http:' + { Add-CCSADComputerToSecurityGroup -ComputerName 'PC01' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $httpUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Throw + } + + It 'Should reject URL without protocol' { + { Add-CCSADComputerToSecurityGroup -ComputerName 'PC01' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url 'test.com/CCS.asmx' -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Throw + } + } + + Context 'Domain Validation' { + + It 'Should accept valid domain format' { + { Add-CCSADComputerToSecurityGroup -ComputerName 'PC01' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Not -Throw + } + + It 'Should accept subdomain format' { + { Add-CCSADComputerToSecurityGroup -ComputerName 'PC01' -SecurityGroupName 'TestGroup' -Domain 'sub.firmax.local' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Not -Throw + } + + It 'Should reject invalid domain format' { + { Add-CCSADComputerToSecurityGroup -ComputerName 'PC01' -SecurityGroupName 'TestGroup' -Domain 'invalid_domain' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Throw + } + } + + Context 'ShouldProcess Support' { + + It 'Should support WhatIf' { + (Get-Command Add-CCSADComputerToSecurityGroup).Parameters.ContainsKey('WhatIf') | Should -Be $true + } + + It 'Should support Confirm' { + (Get-Command Add-CCSADComputerToSecurityGroup).Parameters.ContainsKey('Confirm') | Should -Be $true + } + } + + Context 'CmdletBinding Attributes' { + + It 'Should be an advanced function' { + (Get-Command Add-CCSADComputerToSecurityGroup).CmdletBinding | Should -Be $true + } + + It 'Should have SupportsShouldProcess' { + (Get-Command Add-CCSADComputerToSecurityGroup).Parameters['WhatIf'] | Should -Not -BeNullOrEmpty + } + + It 'Should have OutputType defined' { + (Get-Command Add-CCSADComputerToSecurityGroup).OutputType.Name | Should -Contain 'System.String' + } + } +} + +Describe 'Add-CCSADComputerToSecurityGroup - Integration Tests' -Tag 'Integration' { + + Context 'Live CCS Web Service Operations' { + + BeforeAll { + # Create unique test group name + $script:TestGroupName = "PesterTest_$(Get-Random -Minimum 1000 -Maximum 9999)" + $script:TestComputerName = 'PESTER-TEST-PC' + } + + It 'Should connect to CCS Web Service successfully' { + { Add-CCSADComputerToSecurityGroup -ComputerName $script:TestComputerName -SecurityGroupName $script:TestGroupName -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Not -Throw + } + + It 'Should add computer to security group with domain credentials' { + $result = Add-CCSADComputerToSecurityGroup -ComputerName $script:TestComputerName -SecurityGroupName $script:TestGroupName -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Not -BeNullOrEmpty + } + + It 'Should process multiple computers' { + $computers = @("$script:TestComputerName`1", "$script:TestComputerName`2") + { $computers | Add-CCSADComputerToSecurityGroup -SecurityGroupName $script:TestGroupName -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should convert LDAP path format' { + $ldapPath = 'LDAP://DC01.Firmax.local/DC=Firmax,DC=local' + { Add-CCSADComputerToSecurityGroup -ComputerName $script:TestComputerName -SecurityGroupName $script:TestGroupName -DomainOUPath $ldapPath -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should add PESTER-TEST-PC to TestPester group' { + $result = Add-CCSADComputerToSecurityGroup -ComputerName 'PESTER-TEST-PC' -SecurityGroupName 'TestPester' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction Stop + $result | Should -Be 'ok' + } + } + + Context 'Error Handling' { + + It 'Should handle invalid credentials gracefully' { + $badCred = New-Object System.Management.Automation.PSCredential('baduser', (ConvertTo-SecureString 'badpass' -AsPlainText -Force)) + { Add-CCSADComputerToSecurityGroup -ComputerName 'TestPC' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $badCred -ErrorAction Stop } | Should -Throw + } + + It 'Should handle non-existent URL gracefully' { + { Add-CCSADComputerToSecurityGroup -ComputerName 'TestPC' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url 'https://nonexistent.invalid/CCS.asmx' -CCSCredential $script:TestCCSCredential -ErrorAction Stop } | Should -Throw + } + } +} + +Describe 'Add-CCSADComputerToSecurityGroup - Performance Tests' -Tag 'Performance' { + + Context 'Pipeline Performance' { + + It 'Should process pipeline input efficiently' { + $computers = 1..10 | ForEach-Object { "PC$_" } + $measure = Measure-Command { + $computers | Add-CCSADComputerToSecurityGroup -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf -WarningAction SilentlyContinue + } + $measure.TotalSeconds | Should -BeLessThan 5 + } + } +} diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADComputerToSecurityGroup.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADComputerToSecurityGroup.ps1 index efa3ddf3..c7c5fee6 100644 --- a/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADComputerToSecurityGroup.ps1 +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADComputerToSecurityGroup.ps1 @@ -1,91 +1,318 @@ -<# +function Add-CCSADComputerToSecurityGroup { + <# .SYNOPSIS Adds a computer to a security group in Active Directory using the CCS Web Service. .DESCRIPTION - Adds a computer to a security group in Active Directory using the CCS Web Service. This function requires the CCS Web Service URL and credentials to access it. + Adds a computer to a security group in Active Directory using the CCS Web Service. + This advanced function includes comprehensive error handling, input validation, and supports pipeline input. .PARAMETER ComputerName The name of the computer to be added to the security group. + Supports pipeline input and accepts multiple computer names. .PARAMETER SecurityGroupName The name of the security group to which the computer will be added. .PARAMETER DomainOUPath The Organizational Unit (OU) path in which the computer resides. + If not specified, searches the entire domain. + Supports both standard DN format (OU=Computers,DC=example,DC=com) and LDAP format (LDAP://DC01.example.local/OU=Computers,DC=example,DC=com). .PARAMETER Domain The domain in which the computer resides. + Must be a valid domain name format. .PARAMETER Url - The URL of the CCS Web Service. Example "https://example.com/CCSWebservice/CCS.asmx". + The URL of the CCS Web Service. + Must be a valid URI format. Example: "https://example.com/CCSWebservice/CCS.asmx" .PARAMETER CCSCredential The credentials used to authenticate with the CCS Web Service. .PARAMETER DomainCredential - The credentials of an account with permissions to add the computer to the security group, if not defined it will run in the CCSWebservice context. + The credentials of an account with permissions to add the computer to the security group. + If not defined, it will run in the CCS Web Service context. .PARAMETER PasswordIsEncrypted - Indicates if the password in the DomainCredential is encrypted. Default is false. + Indicates if the password in the DomainCredential is encrypted. Default is $false. .EXAMPLE - PS C:\> Add-CCSADComputerToSecurityGroup -ComputerName "TestPC" -SecurityGroupName "TestGroup" -DomainOUPath "OU=Computers,DC=example,DC=com" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + PS C:\> Add-CCSADComputerToSecurityGroup -ComputerName "TestPC" -SecurityGroupName "TestGroup" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Adds TestPC to TestGroup using default CCS context. .EXAMPLE PS C:\> Add-CCSADComputerToSecurityGroup -ComputerName "TestPC" -SecurityGroupName "TestGroup" -DomainOUPath "OU=Computers,DC=example,DC=com" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential -DomainCredential $DomainCredential -#> -function Add-CCSADComputerToSecurityGroup { + + Adds TestPC to TestGroup using specific domain credentials and OU path. + + .EXAMPLE + PS C:\> "PC01", "PC02", "PC03" | Add-CCSADComputerToSecurityGroup -SecurityGroupName "TestGroup" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Adds multiple computers to TestGroup using pipeline input. + + .EXAMPLE + PS C:\> Add-CCSADComputerToSecurityGroup -ComputerName "TestPC" -SecurityGroupName "TestGroup" -DomainOUPath "LDAP://DC01.Firmax.local/DC=FirmaX,DC=local" -Domain "Firmax.local" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Adds TestPC to TestGroup using LDAP format for the OU path. The LDAP path will be automatically converted to standard DN format. + + .OUTPUTS + System.String + Returns the result message from the CCS Web Service operation. + + .NOTES + This is an advanced function with support for ShouldProcess, pipeline input, and comprehensive error handling. + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] + [OutputType([string])] param ( - [Parameter(Mandatory = $true)] - [string]$ComputerName, - [Parameter(Mandatory = $true)] + [Parameter( + Mandatory = $true, + Position = 0, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the computer name to add to the security group' + )] + [ValidateNotNullOrEmpty()] + [Alias('Name', 'Computer', 'CN')] + [string[]]$ComputerName, + + [Parameter( + Mandatory = $true, + Position = 1, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the security group name' + )] + [ValidateNotNullOrEmpty()] + [Alias('GroupName', 'Group')] [string]$SecurityGroupName, - [Parameter(Mandatory = $false)] - [string]$DomainOUPath, - [Parameter(Mandatory = $true)] + + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] + [ValidateScript({ + if ([string]::IsNullOrEmpty($_)) { + return $true + } + # Support standard DN format: OU=...,DC=...,DC=... + if ($_ -match '^(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + # Support LDAP format: LDAP://server/DN + if ($_ -match '^LDAP://[^/]+/(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + throw "DomainOUPath must be in format 'OU=...,DC=...,DC=...' or 'LDAP://server/OU=...,DC=...,DC=...' or empty" + })] + [Alias('OU', 'Path')] + [string]$DomainOUPath = '', + + [Parameter( + Mandatory = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the domain name' + )] + [ValidateNotNullOrEmpty()] + [ValidatePattern('^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$')] [string]$Domain, - [Parameter(Mandatory = $true)] + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service URL' + )] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + if ($_ -match '^https://') { + $true + } else { + throw "URL must start with https:// (secure connection required)" + } + })] + [Alias('WebServiceUrl', 'Uri')] [string]$Url, - [Parameter(Mandatory = $true)] + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service credentials' + )] + [ValidateNotNull()] + [Alias('Credential', 'WebServiceCredential')] [pscredential]$CCSCredential, + [Parameter(Mandatory = $false)] + [ValidateNotNull()] + [Alias('ADCredential')] [pscredential]$DomainCredential, + [Parameter(Mandatory = $false)] - [bool]$PasswordIsEncrypted = $false + [Alias('Encrypted')] + [switch]$PasswordIsEncrypted ) - $FunctionName = 'Add-CCSADComputerToSecurityGroup' - if ($Global:Cs) { - Job_WriteLog -Text "$FunctionName ComputerName: $ComputerName, SecurityGroupName: $SecurityGroupName, DomainOUPath: $DomainOUPath, Domain: $Domain, Url: $Url, PasswordIsEncrypted: $PasswordIsEncrypted" - } + begin { + $FunctionName = $PSCmdlet.MyInvocation.MyCommand.Name - $CCS = Initialize-CCS -Url $Url -WebServiceCredential $CCSCredential - $ADPassword = $null + Write-Verbose "[$FunctionName] Starting function execution" + Write-Verbose "[$FunctionName] SecurityGroupName: $SecurityGroupName" + Write-Verbose "[$FunctionName] Domain: $Domain" + Write-Verbose "[$FunctionName] DomainOUPath: $DomainOUPath" + Write-Verbose "[$FunctionName] URL: $Url" - if ($DomainCredential -and $PasswordIsEncrypted) { - $ADPassword = $DomainCredential.GetNetworkCredential().Password - } elseif ($DomainCredential -and -not $PasswordIsEncrypted) { - $ADPassword = Get-CCSEncryptedPassword -String $DomainCredential.GetNetworkCredential().Password - } + # Initialize CCS connection once in begin block + try { + Write-Verbose "[$FunctionName] Initializing CCS Web Service connection" + $CCS = Initialize-CCS -Url $Url -WebServiceCredential $CCSCredential -ErrorAction Stop + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to initialize CCS Web Service: $_" ` + -ErrorCategory ConnectionError ` + -TargetObject $Url ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Verify the URL is correct and the CCS Web Service is accessible. Check credentials." + } - $Result = $CCS.ActiveDirectory_AddComputerToSecurityGroup( - $ComputerName, - $SecurityGroupName, - $DomainOUPath, - $Domain, - $DomainCredential.UserName, - $ADPassword - ) + # Prepare domain credentials if provided + $ADUsername = $null + $ADPassword = $null - if ($Global:Cs) { - Job_WriteLog -Text "$FunctionName Result: $Result" + if ($DomainCredential) { + $ADUsername = $DomainCredential.UserName + Write-Verbose "[$FunctionName] Using domain credential: $ADUsername" + + try { + if ($PasswordIsEncrypted) { + $ADPassword = $DomainCredential.GetNetworkCredential().Password + Write-Verbose "[$FunctionName] Using pre-encrypted password" + } else { + $ADPassword = Get-CCSEncryptedPassword -String $DomainCredential.GetNetworkCredential().Password -ErrorAction Stop + Write-Verbose "[$FunctionName] Password encrypted successfully" + } + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to process domain credential password: $_" ` + -ErrorCategory SecurityError ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Ensure the domain credential is valid and the password can be encrypted." + } + } else { + Write-Verbose "[$FunctionName] No domain credential provided, using CCS Web Service context" + } + + $ProcessedCount = 0 + $SuccessCount = 0 + $FailureCount = 0 } - $Throw = Invoke-CCSIsError -Result $Result - if ($Throw) { - throw "$FunctionName $Result" + process { + foreach ($Computer in $ComputerName) { + $ProcessedCount++ + + try { + Write-Verbose "[$FunctionName] Processing computer: $Computer ($ProcessedCount)" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Processing: ComputerName=$Computer, SecurityGroupName=$SecurityGroupName, DomainOUPath=$DomainOUPath, Domain=$Domain") + } + + # ShouldProcess support + $WhatIfMessage = "Add computer '$Computer' to security group '$SecurityGroupName' in domain '$Domain'" + if ($PSCmdlet.ShouldProcess($Computer, $WhatIfMessage)) { + Write-Verbose "[$FunctionName] Calling ActiveDirectory_AddComputerToSecurityGroup for $Computer" + + $Result = $CCS.ActiveDirectory_AddComputerToSecurityGroup( + $Computer, + $SecurityGroupName, + $DomainOUPath, + $Domain, + $ADUsername, + $ADPassword + ) + Write-Debug "Result from CCS Web Service: $Result" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Result for $Computer : $Result") + } + + Write-Verbose "[$FunctionName] Result for $Computer : $Result" + + # Check for errors in result + $IsError = Invoke-CCSIsError -Result $Result + + Write-Debug "IsError evaluation for result: $IsError" + if ($IsError) { + $FailureCount++ + + # Determine appropriate error category based on result message + $ErrorCat = [System.Management.Automation.ErrorCategory]::OperationStopped + $RecommendedActionText = "Verify the computer and security group names are correct." + + if ($Result -like '*does not exist*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::ObjectNotFound + $RecommendedActionText = "Verify that the computer '$Computer' and security group '$SecurityGroupName' exist in Active Directory." + } elseif ($Result -like '*unwilling to process*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::PermissionDenied + $RecommendedActionText = "Check that the domain credentials have sufficient permissions to modify the security group." + } elseif ($Result -like '*already a member*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::ResourceExists + $RecommendedActionText = "The computer is already a member of the security group." + } + + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + Write-Debug "ErrorAction from PSBoundParameters: $($PSBoundParameters['ErrorAction'])" + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + Write-Debug "ErrorAction from ErrorActionPreference: $ErrorActionPreference" + } + Write-Debug "ShouldThrow is set to: $ShouldThrow" + + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to add computer '$Computer' to security group '$SecurityGroupName': $Result" ` + -ErrorCategory $ErrorCat ` + -TargetObject $Computer ` + -FunctionName $FunctionName ` + -RecommendedAction $RecommendedActionText ` + -Throw:$ShouldThrow + } else { + $SuccessCount++ + Write-Verbose "[$FunctionName] Successfully added $Computer to $SecurityGroupName" + Write-Output $Result + } + } else { + Write-Verbose "[$FunctionName] WhatIf: Would add $Computer to $SecurityGroupName" + } + } catch { + $FailureCount++ + + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + } + + Invoke-CCSErrorHandling ` + -ErrorMessage "Exception occurred while processing computer '$Computer': $_" ` + -ErrorCategory InvalidOperation ` + -TargetObject $Computer ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Check the error details and verify all parameters are correct." ` + -Throw:$ShouldThrow + } + } } - return $Result + end { + Write-Verbose "[$FunctionName] Function execution completed" + Write-Verbose "[$FunctionName] Total processed: $ProcessedCount | Success: $SuccessCount | Failed: $FailureCount" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Summary - Processed: $ProcessedCount, Success: $SuccessCount, Failed: $FailureCount") + } + } } \ No newline at end of file diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADDomainLocalSecurityGroup.Tests.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADDomainLocalSecurityGroup.Tests.ps1 index 35fff558..aee530e9 100644 --- a/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADDomainLocalSecurityGroup.Tests.ps1 +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADDomainLocalSecurityGroup.Tests.ps1 @@ -1,10 +1,203 @@ -# TODO: #432 Writes tests for Add-CCSADDomainLocalSecurityGroup -$Splat = @{ - GroupName = 'TestCCSCreate' - Description = 'Test Description' - Domain = 'Firmax.local' - Url = 'https://mracapa02.capainstaller.com/CCSWebservice/CCS.asmx' - CCSCredential = $CCSCredential - DomainCredential = $DomainCredential -} -Add-CCSADDomainLocalSecurityGroup @Splat \ No newline at end of file + +BeforeAll { + $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent + + $Items = Get-ChildItem -Path "$RootPath\Capa.PowerShell.Module.CCS\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + + $script:TestUrl = "https://$(hostname).capainstaller.com/CCSWebservice/CCS.asmx" + $script:TestDomain = 'Firmax.local' + $CredentialPath = "D:\PowerShell\Credentials\$($env:USERNAME)DomainAdminPesterTests.xml" + + if ($env:DOMAINADMINUSERNAME -and $env:DOMAINADMINPASSWORD) { + $securePassword = ConvertTo-SecureString $env:DOMAINADMINPASSWORD -AsPlainText -Force + $script:TestDomainCredential = New-Object System.Management.Automation.PSCredential($env:DOMAINADMINUSERNAME, $securePassword) + $script:TestCCSCredential = $script:TestDomainCredential + } elseif (Test-Path -Path $CredentialPath) { + $script:TestDomainCredential = Import-Clixml -Path $CredentialPath + $script:TestCCSCredential = $script:TestDomainCredential + } else { + $script:TestDomainCredential = Get-Credential -Message 'Enter domain admin credentials for integration tests' + $script:TestCCSCredential = $script:TestDomainCredential + } + + $DebugPreference = 'Continue' + $ErrorActionPreference = 'Stop' +} + +Describe 'Add-CCSADDomainLocalSecurityGroup' -Tag 'Unit' { + + Context 'Parameter Validation' { + It 'Should have mandatory GroupName parameter' { + (Get-Command Add-CCSADDomainLocalSecurityGroup).Parameters['GroupName'].Attributes.Mandatory | Should -Be $true + } + It 'Should have mandatory Domain parameter' { + (Get-Command Add-CCSADDomainLocalSecurityGroup).Parameters['Domain'].Attributes.Mandatory | Should -Be $true + } + It 'Should have mandatory Url parameter' { + (Get-Command Add-CCSADDomainLocalSecurityGroup).Parameters['Url'].Attributes.Mandatory | Should -Be $true + } + It 'Should have mandatory CCSCredential parameter' { + (Get-Command Add-CCSADDomainLocalSecurityGroup).Parameters['CCSCredential'].Attributes.Mandatory | Should -Be $true + } + It 'Should have optional DomainCredential parameter' { + (Get-Command Add-CCSADDomainLocalSecurityGroup).Parameters['DomainCredential'].Attributes.Mandatory | Should -Be $false + } + It 'Should have optional DomainOUPath parameter' { + (Get-Command Add-CCSADDomainLocalSecurityGroup).Parameters['DomainOUPath'].Attributes.Mandatory | Should -Be $false + } + It 'Should accept array of group names' { + (Get-Command Add-CCSADDomainLocalSecurityGroup).Parameters['GroupName'].ParameterType.Name | Should -Be 'String[]' + } + It 'Should accept pipeline input for GroupName' { + (Get-Command Add-CCSADDomainLocalSecurityGroup).Parameters['GroupName'].Attributes.ValueFromPipeline | Should -Be $true + } + } + + Context 'Parameter Aliases' { + It 'Should have alias "Name" for GroupName' { + (Get-Command Add-CCSADDomainLocalSecurityGroup).Parameters['GroupName'].Aliases | Should -Contain 'Name' + } + It 'Should have alias "Group" for GroupName' { + (Get-Command Add-CCSADDomainLocalSecurityGroup).Parameters['GroupName'].Aliases | Should -Contain 'Group' + } + It 'Should have alias "OU" for DomainOUPath' { + (Get-Command Add-CCSADDomainLocalSecurityGroup).Parameters['DomainOUPath'].Aliases | Should -Contain 'OU' + } + It 'Should have alias "Path" for DomainOUPath' { + (Get-Command Add-CCSADDomainLocalSecurityGroup).Parameters['DomainOUPath'].Aliases | Should -Contain 'Path' + } + It 'Should have alias "Desc" for Description' { + (Get-Command Add-CCSADDomainLocalSecurityGroup).Parameters['Description'].Aliases | Should -Contain 'Desc' + } + } + + Context 'DomainOUPath Validation' { + It 'Should accept empty DomainOUPath' { + { Add-CCSADDomainLocalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath '' -WhatIf } | Should -Not -Throw + } + It 'Should accept standard DN format' { + { Add-CCSADDomainLocalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'OU=Groups,DC=Firmax,DC=local' -WhatIf } | Should -Not -Throw + } + It 'Should accept DC-only format' { + { Add-CCSADDomainLocalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'DC=Firmax,DC=local' -WhatIf } | Should -Not -Throw + } + It 'Should accept LDAP format' { + { Add-CCSADDomainLocalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'LDAP://DC01.Firmax.local/OU=Groups,DC=Firmax,DC=local' -WhatIf } | Should -Not -Throw + } + It 'Should reject invalid format' { + { Add-CCSADDomainLocalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'InvalidPath' -WhatIf } | Should -Throw + } + } + + Context 'URL Validation' { + It 'Should accept HTTPS URL' { + { Add-CCSADDomainLocalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Not -Throw + } + It 'Should reject HTTP URL (only HTTPS allowed)' { + $httpUrl = $script:TestUrl -replace '^https:', 'http:' + { Add-CCSADDomainLocalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain $script:TestDomain -Url $httpUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Throw + } + It 'Should reject URL without protocol' { + { Add-CCSADDomainLocalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain $script:TestDomain -Url 'test.com/CCS.asmx' -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Throw + } + } + + Context 'Domain Validation' { + It 'Should accept valid domain format' { + { Add-CCSADDomainLocalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Not -Throw + } + It 'Should accept subdomain format' { + { Add-CCSADDomainLocalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain 'sub.firmax.local' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Not -Throw + } + It 'Should reject invalid domain format' { + { Add-CCSADDomainLocalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain 'invalid_domain' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Throw + } + } + + Context 'ShouldProcess Support' { + It 'Should support WhatIf' { + (Get-Command Add-CCSADDomainLocalSecurityGroup).Parameters.ContainsKey('WhatIf') | Should -Be $true + } + It 'Should support Confirm' { + (Get-Command Add-CCSADDomainLocalSecurityGroup).Parameters.ContainsKey('Confirm') | Should -Be $true + } + } + + Context 'CmdletBinding Attributes' { + It 'Should be an advanced function' { + (Get-Command Add-CCSADDomainLocalSecurityGroup).CmdletBinding | Should -Be $true + } + It 'Should have SupportsShouldProcess' { + (Get-Command Add-CCSADDomainLocalSecurityGroup).Parameters['WhatIf'] | Should -Not -BeNullOrEmpty + } + It 'Should have OutputType defined' { + (Get-Command Add-CCSADDomainLocalSecurityGroup).OutputType.Name | Should -Contain 'System.String' + } + } +} + +Describe 'Add-CCSADDomainLocalSecurityGroup - Integration Tests' -Tag 'Integration' { + Context 'Live CCS Web Service Operations' { + BeforeAll { + $script:TestGroupName = "PesterTestDLG_$(Get-Random -Minimum 1000 -Maximum 9999)" + $script:TestDescription = 'Pester integration test group' + } + + It 'Should connect to CCS Web Service successfully' { + { Add-CCSADDomainLocalSecurityGroup -GroupName $script:TestGroupName -Description $script:TestDescription -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Not -Throw + } + + It 'Should create domain local security group with domain credentials' { + $result = Add-CCSADDomainLocalSecurityGroup -GroupName $script:TestGroupName -Description $script:TestDescription -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Not -BeNullOrEmpty + } + + It 'Should process multiple group names' { + $groups = @("$script:TestGroupName`1", "$script:TestGroupName`2") + { $groups | Add-CCSADDomainLocalSecurityGroup -Description $script:TestDescription -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should convert LDAP path format' { + $ldapPath = 'LDAP://DC01.Firmax.local/DC=Firmax,DC=local' + { Add-CCSADDomainLocalSecurityGroup -GroupName $script:TestGroupName -Description $script:TestDescription -DomainOUPath $ldapPath -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should create PesterTestDLG group in TestPester-DomainLocal OU' { + $result = Add-CCSADDomainLocalSecurityGroup -GroupName 'TestPester-DomainLocal' -Description 'Pester test group' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction Stop + + $TestOk = $false + if ($result -eq 'ok') { + $TestOk = $true + } elseif ($result -match 'group exist') { + $TestOk = $true + } + + $TestOk | Should -Be $true + } + } + + Context 'Error Handling' { + It 'Should handle invalid credentials gracefully' { + $badCred = New-Object System.Management.Automation.PSCredential('baduser', (ConvertTo-SecureString 'badpass' -AsPlainText -Force)) + { Add-CCSADDomainLocalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $badCred -ErrorAction Stop } | Should -Throw + } + It 'Should handle non-existent URL gracefully' { + { Add-CCSADDomainLocalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain $script:TestDomain -Url 'https://nonexistent.invalid/CCS.asmx' -CCSCredential $script:TestCCSCredential -ErrorAction Stop } | Should -Throw + } + } +} + +Describe 'Add-CCSADDomainLocalSecurityGroup - Performance Tests' -Tag 'Performance' { + Context 'Pipeline Performance' { + It 'Should process pipeline input efficiently' { + $groups = 1..10 | ForEach-Object { "DLG$_" } + $measure = Measure-Command { + $groups | Add-CCSADDomainLocalSecurityGroup -Description 'desc' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf -WarningAction SilentlyContinue + } + $measure.TotalSeconds | Should -BeLessThan 5 + } + } +} diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADDomainLocalSecurityGroup.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADDomainLocalSecurityGroup.ps1 index 2be884e2..768c6c99 100644 --- a/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADDomainLocalSecurityGroup.ps1 +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADDomainLocalSecurityGroup.ps1 @@ -1,9 +1,11 @@ -<# +function Add-CCSADDomainLocalSecurityGroup { + <# .SYNOPSIS - Creates a domain local security group in Active Directory. + Creates a domain local security group in Active Directory using the CCS Web Service. .DESCRIPTION Creates a domain local security group in Active Directory using the CCS Web Service. + This advanced function includes comprehensive error handling, input validation, and supports pipeline input. .PARAMETER GroupName The name of the security group to be created. @@ -13,76 +15,279 @@ .PARAMETER DomainOUPath The Organizational Unit (OU) path in which the security group will be created. + Supports both standard DN format (OU=Groups,DC=example,DC=com) and LDAP format (LDAP://DC01.example.local/OU=Groups,DC=example,DC=com). .PARAMETER Domain The domain in which the security group will be created. + Must be a valid domain name format. .PARAMETER Url - The URL of the CCS Web Service. Example "https://example.com/CCSWebservice/CCS.asmx". + The URL of the CCS Web Service. + Must be a valid URI format. Example: "https://example.com/CCSWebservice/CCS.asmx" .PARAMETER CCSCredential The credentials used to authenticate with the CCS Web Service. .PARAMETER DomainCredential - The credentials of an account with permissions to create the security group, if not defined it will run in the CCSWebservice context. + The credentials of an account with permissions to create the security group. + If not defined, it will run in the CCS Web Service context. .PARAMETER PasswordIsEncrypted - Indicates if the password in the DomainCredential is encrypted. Default is false. + Indicates if the password in the DomainCredential is encrypted. Default is $false. .EXAMPLE - Add-CCSADDomainLocalSecurityGroup -GroupName 'TestGroup' -Description 'Test Description' -DomainOUPath 'OU=Groups,DC=example,DC=com' -Domain 'example.com' -Url 'https://example.com/CCSWebservice/CCS.asmx' -CCSCredential $CCSCredential -DomainCredential $DomainCredential -PasswordIsEncrypted $false -#> -function Add-CCSADDomainLocalSecurityGroup { + PS C:\> Add-CCSADDomainLocalSecurityGroup -GroupName "TestGroup" -Description "Test Description" -DomainOUPath "OU=Groups,DC=example,DC=com" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + .OUTPUTS + System.String + Returns the result message from the CCS Web Service operation. + + .NOTES + This is an advanced function with support for ShouldProcess, pipeline input, and comprehensive error handling. + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] + [OutputType([string])] param ( - [Parameter(Mandatory = $true)] - [string]$GroupName, - [Parameter(Mandatory = $false)] - [string]$Description, - [Parameter(Mandatory = $false)] - [string]$DomainOUPath, - [Parameter(Mandatory = $true)] + [Parameter( + Mandatory = $true, + Position = 0, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the group name to create' + )] + [ValidateNotNullOrEmpty()] + [Alias('Name', 'Group')] + [string[]]$GroupName, + + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] + [Alias('Desc')] + [string]$Description = '', + + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] + [ValidateScript({ + if ([string]::IsNullOrEmpty($_)) { + return $true + } + # Support standard DN format: OU=...,DC=...,DC=... + if ($_ -match '^(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + # Support LDAP format: LDAP://server/DN + if ($_ -match '^LDAP://[^/]+/(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + throw "DomainOUPath must be in format 'OU=...,DC=...,DC=...' or 'LDAP://server/OU=...,DC=...,DC=...' or empty" + })] + [Alias('OU', 'Path')] + [string]$DomainOUPath = '', + + [Parameter( + Mandatory = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the domain name' + )] + [ValidateNotNullOrEmpty()] + [ValidatePattern('^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$')] [string]$Domain, - [Parameter(Mandatory = $true)] + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service URL' + )] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + if ($_ -match '^https://') { + $true + } else { + throw "URL must start with https:// (secure connection required)" + } + })] + [Alias('WebServiceUrl', 'Uri')] [string]$Url, - [Parameter(Mandatory = $true)] + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service credentials' + )] + [ValidateNotNull()] + [Alias('Credential', 'WebServiceCredential')] [pscredential]$CCSCredential, + [Parameter(Mandatory = $false)] + [ValidateNotNull()] + [Alias('ADCredential')] [pscredential]$DomainCredential, + [Parameter(Mandatory = $false)] - [bool]$PasswordIsEncrypted = $false + [Alias('Encrypted')] + [switch]$PasswordIsEncrypted ) - $FunctionName = 'Add-CCSADDomainLocalSecurityGroup' - if ($Global:Cs) { - Job_WriteLog -Text "$FunctionName GroupName: $GroupName, Description: $Description, DomainOUPath: $DomainOUPath, Domain: $Domain, Url: $Url, PasswordIsEncrypted: $PasswordIsEncrypted" - } + begin { + $FunctionName = $PSCmdlet.MyInvocation.MyCommand.Name - $CCS = Initialize-CCS -Url $Url -WebServiceCredential $CCSCredential - $ADPassword = $null + Write-Verbose "[$FunctionName] Starting function execution" + Write-Verbose "[$FunctionName] Description: $Description" + Write-Verbose "[$FunctionName] Domain: $Domain" + Write-Verbose "[$FunctionName] DomainOUPath: $DomainOUPath" + Write-Verbose "[$FunctionName] URL: $Url" - if ($DomainCredential -and $PasswordIsEncrypted) { - $ADPassword = $DomainCredential.GetNetworkCredential().Password - } elseif ($DomainCredential -and -not $PasswordIsEncrypted) { - $ADPassword = Get-CCSEncryptedPassword -String $DomainCredential.GetNetworkCredential().Password - } + # Initialize CCS connection once in begin block + try { + Write-Verbose "[$FunctionName] Initializing CCS Web Service connection" + $CCS = Initialize-CCS -Url $Url -WebServiceCredential $CCSCredential -ErrorAction Stop + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to initialize CCS Web Service: $_" ` + -ErrorCategory ConnectionError ` + -TargetObject $Url ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Verify the URL is correct and the CCS Web Service is accessible. Check credentials." + } - $Result = $CCS.ActiveDirectory_AddDomainLocalSecurityGroup( - $GroupName, - $Description, - $DomainOUPath, - $Domain, - $DomainCredential.UserName, - $ADPassword - ) + # Prepare domain credentials if provided + $ADUsername = $null + $ADPassword = $null + + if ($DomainCredential) { + $ADUsername = $DomainCredential.UserName + Write-Verbose "[$FunctionName] Using domain credential: $ADUsername" - if ($Global:Cs) { - Job_WriteLog -Text "$FunctionName Result: $Result" + try { + if ($PasswordIsEncrypted) { + $ADPassword = $DomainCredential.GetNetworkCredential().Password + Write-Verbose "[$FunctionName] Using pre-encrypted password" + } else { + $ADPassword = Get-CCSEncryptedPassword -String $DomainCredential.GetNetworkCredential().Password -ErrorAction Stop + Write-Verbose "[$FunctionName] Password encrypted successfully" + } + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to process domain credential password: $_" ` + -ErrorCategory SecurityError ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Ensure the domain credential is valid and the password can be encrypted." + } + } else { + Write-Verbose "[$FunctionName] No domain credential provided, using CCS Web Service context" + } + + $ProcessedCount = 0 + $SuccessCount = 0 + $FailureCount = 0 } - $Throw = Invoke-CCSIsError -Result $Result - if ($Throw) { - throw "$FunctionName $Result" + process { + foreach ($Group in $GroupName) { + $ProcessedCount++ + + try { + Write-Verbose "[$FunctionName] Processing group: $Group ($ProcessedCount)" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Processing: GroupName=$Group, Description=$Description, DomainOUPath=$DomainOUPath, Domain=$Domain") + } + + # ShouldProcess support + $WhatIfMessage = "Create domain local security group '$Group' in domain '$Domain'" + if ($PSCmdlet.ShouldProcess($Group, $WhatIfMessage)) { + Write-Verbose "[$FunctionName] Calling ActiveDirectory_AddDomainLocalSecurityGroup for $Group" + + $Result = $CCS.ActiveDirectory_AddDomainLocalSecurityGroup( + $Group, + $Description, + $DomainOUPath, + $Domain, + $ADUsername, + $ADPassword + ) + Write-Debug "Result from CCS Web Service: $Result" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Result for $Group : $Result") + } + + Write-Verbose "[$FunctionName] Result for $Group : $Result" + + # Check for errors in result + $IsError = Invoke-CCSIsError -Result $Result + + Write-Debug "IsError evaluation for result: $IsError" + if ($IsError) { + $FailureCount++ + + # Determine appropriate error category based on result message + $ErrorCat = [System.Management.Automation.ErrorCategory]::OperationStopped + $RecommendedActionText = "Verify the group name and parameters are correct." + + if ($Result -like '*already exists*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::ResourceExists + $RecommendedActionText = "The group already exists." + } elseif ($Result -like '*does not exist*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::ObjectNotFound + $RecommendedActionText = "Verify that the OU and domain exist." + } elseif ($Result -like '*unwilling to process*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::PermissionDenied + $RecommendedActionText = "Check that the domain credentials have sufficient permissions to create the group." + } + + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + Write-Debug "ErrorAction from PSBoundParameters: $($PSBoundParameters['ErrorAction'])" + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + Write-Debug "ErrorAction from ErrorActionPreference: $ErrorActionPreference" + } + Write-Debug "ShouldThrow is set to: $ShouldThrow" + + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to create domain local security group '$Group': $Result" ` + -ErrorCategory $ErrorCat ` + -TargetObject $Group ` + -FunctionName $FunctionName ` + -RecommendedAction $RecommendedActionText ` + -Throw:$ShouldThrow + } else { + $SuccessCount++ + Write-Verbose "[$FunctionName] Successfully created $Group" + Write-Output $Result + } + } else { + Write-Verbose "[$FunctionName] WhatIf: Would create $Group" + } + } catch { + $FailureCount++ + + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + } + + Invoke-CCSErrorHandling ` + -ErrorMessage "Exception occurred while processing group '$Group': $_" ` + -ErrorCategory InvalidOperation ` + -TargetObject $Group ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Check the error details and verify all parameters are correct." ` + -Throw:$ShouldThrow + } + } } - return $Result + end { + Write-Verbose "[$FunctionName] Function execution completed" + Write-Verbose "[$FunctionName] Total processed: $ProcessedCount | Success: $SuccessCount | Failed: $FailureCount" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Summary - Processed: $ProcessedCount, Success: $SuccessCount, Failed: $FailureCount") + } + } } \ No newline at end of file diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADGlobalSecurityGroup.Tests.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADGlobalSecurityGroup.Tests.ps1 index ac0a3d77..c6ece25e 100644 --- a/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADGlobalSecurityGroup.Tests.ps1 +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADGlobalSecurityGroup.Tests.ps1 @@ -1,14 +1,201 @@ -# TODO: #434 Create Tests for Add-CCSADGlobalSecurityGroup -$CCSCredential = Get-Credential -Message 'Enter CCS Web Service credentials' -$DomainCredential = Get-Credential -Message 'Enter Domain credentials' +BeforeAll { + $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent -$Splat = @{ - GroupName = 'Test_Add-CCSADGlobalSecurityGroup' - Description = 'Test Description' - Domain = 'Firmax.local' - Url = 'https://mracapa02.capainstaller.com/CCSWebservice/CCS.asmx' - CCSCredential = $CCSCredential - DomainCredential = $DomainCredential + $Items = Get-ChildItem -Path "$RootPath\Capa.PowerShell.Module.CCS\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + + $script:TestUrl = "https://$(hostname).capainstaller.com/CCSWebservice/CCS.asmx" + $script:TestDomain = 'Firmax.local' + $CredentialPath = "D:\PowerShell\Credentials\$($env:USERNAME)DomainAdminPesterTests.xml" + + if ($env:DOMAINADMINUSERNAME -and $env:DOMAINADMINPASSWORD) { + $securePassword = ConvertTo-SecureString $env:DOMAINADMINPASSWORD -AsPlainText -Force + $script:TestDomainCredential = New-Object System.Management.Automation.PSCredential($env:DOMAINADMINUSERNAME, $securePassword) + $script:TestCCSCredential = $script:TestDomainCredential + } elseif (Test-Path -Path $CredentialPath) { + $script:TestDomainCredential = Import-Clixml -Path $CredentialPath + $script:TestCCSCredential = $script:TestDomainCredential + } else { + $script:TestDomainCredential = Get-Credential -Message 'Enter domain admin credentials for integration tests' + $script:TestCCSCredential = $script:TestDomainCredential + } + + $DebugPreference = 'Continue' + $ErrorActionPreference = 'Stop' +} + +Describe 'Add-CCSADGlobalSecurityGroup' -Tag 'Unit' { + + Context 'Parameter Validation' { + It 'Should have mandatory GroupName parameter' { + (Get-Command Add-CCSADGlobalSecurityGroup).Parameters['GroupName'].Attributes.Mandatory | Should -Be $true + } + It 'Should have mandatory Domain parameter' { + (Get-Command Add-CCSADGlobalSecurityGroup).Parameters['Domain'].Attributes.Mandatory | Should -Be $true + } + It 'Should have mandatory Url parameter' { + (Get-Command Add-CCSADGlobalSecurityGroup).Parameters['Url'].Attributes.Mandatory | Should -Be $true + } + It 'Should have mandatory CCSCredential parameter' { + (Get-Command Add-CCSADGlobalSecurityGroup).Parameters['CCSCredential'].Attributes.Mandatory | Should -Be $true + } + It 'Should have optional DomainCredential parameter' { + (Get-Command Add-CCSADGlobalSecurityGroup).Parameters['DomainCredential'].Attributes.Mandatory | Should -Be $false + } + It 'Should have optional DomainOUPath parameter' { + (Get-Command Add-CCSADGlobalSecurityGroup).Parameters['DomainOUPath'].Attributes.Mandatory | Should -Be $false + } + It 'Should accept array of group names' { + (Get-Command Add-CCSADGlobalSecurityGroup).Parameters['GroupName'].ParameterType.Name | Should -Be 'String[]' + } + It 'Should accept pipeline input for GroupName' { + (Get-Command Add-CCSADGlobalSecurityGroup).Parameters['GroupName'].Attributes.ValueFromPipeline | Should -Be $true + } + } + + Context 'Parameter Aliases' { + It 'Should have alias "Name" for GroupName' { + (Get-Command Add-CCSADGlobalSecurityGroup).Parameters['GroupName'].Aliases | Should -Contain 'Name' + } + It 'Should have alias "Group" for GroupName' { + (Get-Command Add-CCSADGlobalSecurityGroup).Parameters['GroupName'].Aliases | Should -Contain 'Group' + } + It 'Should have alias "OU" for DomainOUPath' { + (Get-Command Add-CCSADGlobalSecurityGroup).Parameters['DomainOUPath'].Aliases | Should -Contain 'OU' + } + It 'Should have alias "Path" for DomainOUPath' { + (Get-Command Add-CCSADGlobalSecurityGroup).Parameters['DomainOUPath'].Aliases | Should -Contain 'Path' + } + It 'Should have alias "Desc" for Description' { + (Get-Command Add-CCSADGlobalSecurityGroup).Parameters['Description'].Aliases | Should -Contain 'Desc' + } + } + + Context 'DomainOUPath Validation' { + It 'Should accept empty DomainOUPath' { + { Add-CCSADGlobalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath '' -WhatIf } | Should -Not -Throw + } + It 'Should accept standard DN format' { + { Add-CCSADGlobalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'OU=Groups,DC=Firmax,DC=local' -WhatIf } | Should -Not -Throw + } + It 'Should accept DC-only format' { + { Add-CCSADGlobalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'DC=Firmax,DC=local' -WhatIf } | Should -Not -Throw + } + It 'Should accept LDAP format' { + { Add-CCSADGlobalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'LDAP://DC01.Firmax.local/OU=Groups,DC=Firmax,DC=local' -WhatIf } | Should -Not -Throw + } + It 'Should reject invalid format' { + { Add-CCSADGlobalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'InvalidPath' -WhatIf } | Should -Throw + } + } + + Context 'URL Validation' { + It 'Should accept HTTPS URL' { + { Add-CCSADGlobalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Not -Throw + } + It 'Should reject HTTP URL (only HTTPS allowed)' { + $httpUrl = $script:TestUrl -replace '^https:', 'http:' + { Add-CCSADGlobalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain $script:TestDomain -Url $httpUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Throw + } + It 'Should reject URL without protocol' { + { Add-CCSADGlobalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain $script:TestDomain -Url 'test.com/CCS.asmx' -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Throw + } + } + + Context 'Domain Validation' { + It 'Should accept valid domain format' { + { Add-CCSADGlobalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Not -Throw + } + It 'Should accept subdomain format' { + { Add-CCSADGlobalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain 'sub.firmax.local' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Not -Throw + } + It 'Should reject invalid domain format' { + { Add-CCSADGlobalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain 'invalid_domain' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Throw + } + } + + Context 'ShouldProcess Support' { + It 'Should support WhatIf' { + (Get-Command Add-CCSADGlobalSecurityGroup).Parameters.ContainsKey('WhatIf') | Should -Be $true + } + It 'Should support Confirm' { + (Get-Command Add-CCSADGlobalSecurityGroup).Parameters.ContainsKey('Confirm') | Should -Be $true + } + } + + Context 'CmdletBinding Attributes' { + It 'Should be an advanced function' { + (Get-Command Add-CCSADGlobalSecurityGroup).CmdletBinding | Should -Be $true + } + It 'Should have SupportsShouldProcess' { + (Get-Command Add-CCSADGlobalSecurityGroup).Parameters['WhatIf'] | Should -Not -BeNullOrEmpty + } + It 'Should have OutputType defined' { + (Get-Command Add-CCSADGlobalSecurityGroup).OutputType.Name | Should -Contain 'System.String' + } + } +} + +Describe 'Add-CCSADGlobalSecurityGroup - Integration Tests' -Tag 'Integration' { + Context 'Live CCS Web Service Operations' { + BeforeAll { + $script:TestGroupName = "PesterTestGLG_$(Get-Random -Minimum 1000 -Maximum 9999)" + $script:TestDescription = 'Pester integration test global group' + } + + It 'Should connect to CCS Web Service successfully' { + { Add-CCSADGlobalSecurityGroup -GroupName $script:TestGroupName -Description $script:TestDescription -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Not -Throw + } + + It 'Should create global security group with domain credentials' { + $result = Add-CCSADGlobalSecurityGroup -GroupName $script:TestGroupName -Description $script:TestDescription -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Not -BeNullOrEmpty + } + + It 'Should process multiple group names' { + $groups = @("$script:TestGroupName`1", "$script:TestGroupName`2") + { $groups | Add-CCSADGlobalSecurityGroup -Description $script:TestDescription -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should convert LDAP path format' { + $ldapPath = 'LDAP://DC01.Firmax.local/DC=Firmax,DC=local' + { Add-CCSADGlobalSecurityGroup -GroupName $script:TestGroupName -Description $script:TestDescription -DomainOUPath $ldapPath -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should create PesterTestGLG group in TestPester-Global OU' { + $result = Add-CCSADGlobalSecurityGroup -GroupName 'TestPester-Global' -Description 'Pester test global group' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction Stop + + $TestsOK = $false + if ($result -eq 'ok' -or $result -match 'group exist') { + $TestsOK = $true + } + + $TestsOK | Should -Be $true + } + } + + Context 'Error Handling' { + It 'Should handle invalid credentials gracefully' { + $badCred = New-Object System.Management.Automation.PSCredential('baduser', (ConvertTo-SecureString 'badpass' -AsPlainText -Force)) + { Add-CCSADGlobalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $badCred -ErrorAction Stop } | Should -Throw + } + It 'Should handle non-existent URL gracefully' { + { Add-CCSADGlobalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain $script:TestDomain -Url 'https://nonexistent.invalid/CCS.asmx' -CCSCredential $script:TestCCSCredential -ErrorAction Stop } | Should -Throw + } + } +} + +Describe 'Add-CCSADGlobalSecurityGroup - Performance Tests' -Tag 'Performance' { + Context 'Pipeline Performance' { + It 'Should process pipeline input efficiently' { + $groups = 1..10 | ForEach-Object { "GLG$_" } + $measure = Measure-Command { + $groups | Add-CCSADGlobalSecurityGroup -Description 'desc' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf -WarningAction SilentlyContinue + } + $measure.TotalSeconds | Should -BeLessThan 5 + } + } } -Add-CCSADGlobalSecurityGroup @Splat \ No newline at end of file diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADGlobalSecurityGroup.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADGlobalSecurityGroup.ps1 index 9376a555..5233890c 100644 --- a/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADGlobalSecurityGroup.ps1 +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADGlobalSecurityGroup.ps1 @@ -1,88 +1,290 @@ -<# +function Add-CCSADGlobalSecurityGroup { + <# .SYNOPSIS - Creates a global security group in Active Directory. + Creates a global security group in Active Directory using the CCS Web Service. .DESCRIPTION Creates a global security group in Active Directory using the CCS Web Service. + This advanced function includes comprehensive error handling, input validation, and supports pipeline input. .PARAMETER GroupName - The name of the security group to be created. + The name of the security group to be created. Supports pipeline input and accepts multiple group names. .PARAMETER Description A description for the security group. .PARAMETER DomainOUPath The Organizational Unit (OU) path in which the security group will be created. + Supports both standard DN format (OU=Groups,DC=example,DC=com) and LDAP format (LDAP://DC01.example.local/OU=Groups,DC=example,DC=com). .PARAMETER Domain - The domain in which the security group will be created. + The domain in which the security group will be created. Must be a valid domain name format. .PARAMETER Url - The URL of the CCS Web Service. Example "https://example.com/CCSWebservice/CCS.asmx". + The URL of the CCS Web Service. Must be a valid URI format. Example: "https://example.com/CCSWebservice/CCS.asmx" .PARAMETER CCSCredential The credentials used to authenticate with the CCS Web Service. .PARAMETER DomainCredential - The credentials of an account with permissions to create the security group, if not defined it will run in the CCSWebservice context. + The credentials of an account with permissions to create the security group. If not defined, it will run in the CCS Web Service context. .PARAMETER PasswordIsEncrypted - Indicates if the password in the DomainCredential is encrypted. Default is false. + Indicates if the password in the DomainCredential is encrypted. Default is $false. .EXAMPLE - Add-CCSADGlobalSecurityGroup -GroupName 'TestGroup' -Description 'Test Description' -DomainOUPath 'OU=Groups,DC=example,DC=com' -Domain 'example.com' -Url 'https://example.com/CCSWebservice/CCS.asmx' -CCSCredential $CCSCredential -DomainCredential $DomainCredential -PasswordIsEncrypted $false -#> -function Add-CCSADGlobalSecurityGroup { + PS C:\> Add-CCSADGlobalSecurityGroup -GroupName "TestGroup" -Description "Test Description" -DomainOUPath "OU=Groups,DC=example,DC=com" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + .OUTPUTS + System.String + Returns the result message from the CCS Web Service operation. + + .NOTES + This is an advanced function with support for ShouldProcess, pipeline input, and comprehensive error handling. + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] + [OutputType([string])] param ( - [Parameter(Mandatory = $true)] - [string]$GroupName, - [Parameter(Mandatory = $false)] - [string]$Description, - [Parameter(Mandatory = $false)] - [string]$DomainOUPath, - [Parameter(Mandatory = $true)] + [Parameter( + Mandatory = $true, + Position = 0, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the group name to create' + )] + [ValidateNotNullOrEmpty()] + [Alias('Name', 'Group')] + [string[]]$GroupName, + + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] + [Alias('Desc')] + [string]$Description = '', + + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] + [ValidateScript({ + if ([string]::IsNullOrEmpty($_)) { + return $true + } + # Support standard DN format: OU=...,DC=...,DC=... + if ($_ -match '^(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + # Support LDAP format: LDAP://server/DN + if ($_ -match '^LDAP://[^/]+/(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + throw "DomainOUPath must be in format 'OU=...,DC=...,DC=...' or 'LDAP://server/OU=...,DC=...,DC=...' or empty" + })] + [Alias('OU', 'Path')] + [string]$DomainOUPath = '', + + [Parameter( + Mandatory = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the domain name' + )] + [ValidateNotNullOrEmpty()] + [ValidatePattern('^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$')] [string]$Domain, - [Parameter(Mandatory = $true)] + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service URL' + )] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + if ($_ -match '^https://') { + $true + } else { + throw "URL must start with https:// (secure connection required)" + } + })] + [Alias('WebServiceUrl', 'Uri')] [string]$Url, - [Parameter(Mandatory = $true)] + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service credentials' + )] + [ValidateNotNull()] + [Alias('Credential', 'WebServiceCredential')] [pscredential]$CCSCredential, + [Parameter(Mandatory = $false)] + [ValidateNotNull()] + [Alias('ADCredential')] [pscredential]$DomainCredential, + [Parameter(Mandatory = $false)] - [bool]$PasswordIsEncrypted = $false + [Alias('Encrypted')] + [switch]$PasswordIsEncrypted ) - $FunctionName = 'Add-CCSADGlobalSecurityGroup' - if ($Global:Cs) { - Job_WriteLog -Text "$FunctionName GroupName: $GroupName, Description: $Description, DomainOUPath: $DomainOUPath, Domain: $Domain, Url: $Url, PasswordIsEncrypted: $PasswordIsEncrypted" - } + begin { + $FunctionName = $PSCmdlet.MyInvocation.MyCommand.Name - $CCS = Initialize-CCS -Url $Url -WebServiceCredential $CCSCredential - $ADPassword = $null + Write-Verbose "[$FunctionName] Starting function execution" + Write-Verbose "[$FunctionName] Description: $Description" + Write-Verbose "[$FunctionName] Domain: $Domain" + Write-Verbose "[$FunctionName] DomainOUPath: $DomainOUPath" + Write-Verbose "[$FunctionName] URL: $Url" - if ($DomainCredential -and $PasswordIsEncrypted) { - $ADPassword = $DomainCredential.GetNetworkCredential().Password - } elseif ($DomainCredential -and -not $PasswordIsEncrypted) { - $ADPassword = Get-CCSEncryptedPassword -String $DomainCredential.GetNetworkCredential().Password - } + # Initialize CCS connection once in begin block + try { + Write-Verbose "[$FunctionName] Initializing CCS Web Service connection" + $CCS = Initialize-CCS -Url $Url -WebServiceCredential $CCSCredential -ErrorAction Stop + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to initialize CCS Web Service: $_" ` + -ErrorCategory ConnectionError ` + -TargetObject $Url ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Verify the URL is correct and the CCS Web Service is accessible. Check credentials." + } - $Result = $CCS.ActiveDirectory_AddGlobalSecurityGroup( - $GroupName, - $Description, - $DomainOUPath, - $Domain, - $DomainCredential.UserName, - $ADPassword - ) + # Prepare domain credentials if provided + $ADUsername = $null + $ADPassword = $null + + if ($DomainCredential) { + $ADUsername = $DomainCredential.UserName + Write-Verbose "[$FunctionName] Using domain credential: $ADUsername" - if ($Global:Cs) { - Job_WriteLog -Text "$FunctionName Result: $Result" + try { + if ($PasswordIsEncrypted) { + $ADPassword = $DomainCredential.GetNetworkCredential().Password + Write-Verbose "[$FunctionName] Using pre-encrypted password" + } else { + $ADPassword = Get-CCSEncryptedPassword -String $DomainCredential.GetNetworkCredential().Password -ErrorAction Stop + Write-Verbose "[$FunctionName] Password encrypted successfully" + } + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to process domain credential password: $_" ` + -ErrorCategory SecurityError ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Ensure the domain credential is valid and the password can be encrypted." + } + } else { + Write-Verbose "[$FunctionName] No domain credential provided, using CCS Web Service context" + } + + $ProcessedCount = 0 + $SuccessCount = 0 + $FailureCount = 0 } - $Throw = Invoke-CCSIsError -Result $Result - if ($Throw) { - throw "$FunctionName $Result" + process { + foreach ($Group in $GroupName) { + $ProcessedCount++ + + try { + Write-Verbose "[$FunctionName] Processing group: $Group ($ProcessedCount)" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Processing: GroupName=$Group, Description=$Description, DomainOUPath=$DomainOUPath, Domain=$Domain") + } + + # ShouldProcess support + $WhatIfMessage = "Create global security group '$Group' in domain '$Domain'" + if ($PSCmdlet.ShouldProcess($Group, $WhatIfMessage)) { + Write-Verbose "[$FunctionName] Calling ActiveDirectory_AddGlobalSecurityGroup for $Group" + + $Result = $CCS.ActiveDirectory_AddGlobalSecurityGroup( + $Group, + $Description, + $DomainOUPath, + $Domain, + $ADUsername, + $ADPassword + ) + Write-Debug "Result from CCS Web Service: $Result" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Result for $Group : $Result") + } + + Write-Verbose "[$FunctionName] Result for $Group : $Result" + + # Check for errors in result + $IsError = Invoke-CCSIsError -Result $Result + + Write-Debug "IsError evaluation for result: $IsError" + if ($IsError) { + $FailureCount++ + + # Determine appropriate error category based on result message + $ErrorCat = [System.Management.Automation.ErrorCategory]::OperationStopped + $RecommendedActionText = "Verify the group name and parameters are correct." + + if ($Result -like '*already exists*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::ResourceExists + $RecommendedActionText = "The group already exists." + } elseif ($Result -like '*does not exist*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::ObjectNotFound + $RecommendedActionText = "Verify that the OU and domain exist." + } elseif ($Result -like '*unwilling to process*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::PermissionDenied + $RecommendedActionText = "Check that the domain credentials have sufficient permissions to create the group." + } + + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + Write-Debug "ErrorAction from PSBoundParameters: $($PSBoundParameters['ErrorAction'])" + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + Write-Debug "ErrorAction from ErrorActionPreference: $ErrorActionPreference" + } + Write-Debug "ShouldThrow is set to: $ShouldThrow" + + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to create global security group '$Group': $Result" ` + -ErrorCategory $ErrorCat ` + -TargetObject $Group ` + -FunctionName $FunctionName ` + -RecommendedAction $RecommendedActionText ` + -Throw:$ShouldThrow + } else { + $SuccessCount++ + Write-Verbose "[$FunctionName] Successfully created $Group" + Write-Output $Result + } + } else { + Write-Verbose "[$FunctionName] WhatIf: Would create $Group" + } + } catch { + $FailureCount++ + + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + } + + Invoke-CCSErrorHandling ` + -ErrorMessage "Exception occurred while processing group '$Group': $_" ` + -ErrorCategory InvalidOperation ` + -TargetObject $Group ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Check the error details and verify all parameters are correct." ` + -Throw:$ShouldThrow + } + } } - return $Result + end { + Write-Verbose "[$FunctionName] Function execution completed" + Write-Verbose "[$FunctionName] Total processed: $ProcessedCount | Success: $SuccessCount | Failed: $FailureCount" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Summary - Processed: $ProcessedCount, Success: $SuccessCount, Failed: $FailureCount") + } + } } \ No newline at end of file diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADUniversalSecurityGroup.Tests.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADUniversalSecurityGroup.Tests.ps1 index 15221e92..1168a7f7 100644 --- a/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADUniversalSecurityGroup.Tests.ps1 +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADUniversalSecurityGroup.Tests.ps1 @@ -1,14 +1,201 @@ -# TODO: #435 Create Tests for Add-CCSADUniversalSecurityGroup -$CCSCredential = Get-Credential -Message 'Enter CCS Web Service credentials' -$DomainCredential = Get-Credential -Message 'Enter Domain credentials' +BeforeAll { + $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent -$Splat = @{ - GroupName = 'Test_Add-CCSADUniversalSecurityGroup' - Description = 'Test Description' - Domain = 'Firmax.local' - Url = 'https://mracapa02.capainstaller.com/CCSWebservice/CCS.asmx' - CCSCredential = $CCSCredential - DomainCredential = $DomainCredential + $Items = Get-ChildItem -Path "$RootPath\Capa.PowerShell.Module.CCS\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + + $script:TestUrl = "https://$(hostname).capainstaller.com/CCSWebservice/CCS.asmx" + $script:TestDomain = 'Firmax.local' + $CredentialPath = "D:\PowerShell\Credentials\$($env:USERNAME)DomainAdminPesterTests.xml" + + if ($env:DOMAINADMINUSERNAME -and $env:DOMAINADMINPASSWORD) { + $securePassword = ConvertTo-SecureString $env:DOMAINADMINPASSWORD -AsPlainText -Force + $script:TestDomainCredential = New-Object System.Management.Automation.PSCredential($env:DOMAINADMINUSERNAME, $securePassword) + $script:TestCCSCredential = $script:TestDomainCredential + } elseif (Test-Path -Path $CredentialPath) { + $script:TestDomainCredential = Import-Clixml -Path $CredentialPath + $script:TestCCSCredential = $script:TestDomainCredential + } else { + $script:TestDomainCredential = Get-Credential -Message 'Enter domain admin credentials for integration tests' + $script:TestCCSCredential = $script:TestDomainCredential + } + + $DebugPreference = 'Continue' + $ErrorActionPreference = 'Stop' +} + +Describe 'Add-CCSADUniversalSecurityGroup' -Tag 'Unit' { + + Context 'Parameter Validation' { + It 'Should have mandatory GroupName parameter' { + (Get-Command Add-CCSADUniversalSecurityGroup).Parameters['GroupName'].Attributes.Mandatory | Should -Be $true + } + It 'Should have mandatory Domain parameter' { + (Get-Command Add-CCSADUniversalSecurityGroup).Parameters['Domain'].Attributes.Mandatory | Should -Be $true + } + It 'Should have mandatory Url parameter' { + (Get-Command Add-CCSADUniversalSecurityGroup).Parameters['Url'].Attributes.Mandatory | Should -Be $true + } + It 'Should have mandatory CCSCredential parameter' { + (Get-Command Add-CCSADUniversalSecurityGroup).Parameters['CCSCredential'].Attributes.Mandatory | Should -Be $true + } + It 'Should have optional DomainCredential parameter' { + (Get-Command Add-CCSADUniversalSecurityGroup).Parameters['DomainCredential'].Attributes.Mandatory | Should -Be $false + } + It 'Should have optional DomainOUPath parameter' { + (Get-Command Add-CCSADUniversalSecurityGroup).Parameters['DomainOUPath'].Attributes.Mandatory | Should -Be $false + } + It 'Should accept array of group names' { + (Get-Command Add-CCSADUniversalSecurityGroup).Parameters['GroupName'].ParameterType.Name | Should -Be 'String[]' + } + It 'Should accept pipeline input for GroupName' { + (Get-Command Add-CCSADUniversalSecurityGroup).Parameters['GroupName'].Attributes.ValueFromPipeline | Should -Be $true + } + } + + Context 'Parameter Aliases' { + It 'Should have alias "Name" for GroupName' { + (Get-Command Add-CCSADUniversalSecurityGroup).Parameters['GroupName'].Aliases | Should -Contain 'Name' + } + It 'Should have alias "Group" for GroupName' { + (Get-Command Add-CCSADUniversalSecurityGroup).Parameters['GroupName'].Aliases | Should -Contain 'Group' + } + It 'Should have alias "OU" for DomainOUPath' { + (Get-Command Add-CCSADUniversalSecurityGroup).Parameters['DomainOUPath'].Aliases | Should -Contain 'OU' + } + It 'Should have alias "Path" for DomainOUPath' { + (Get-Command Add-CCSADUniversalSecurityGroup).Parameters['DomainOUPath'].Aliases | Should -Contain 'Path' + } + It 'Should have alias "Desc" for Description' { + (Get-Command Add-CCSADUniversalSecurityGroup).Parameters['Description'].Aliases | Should -Contain 'Desc' + } + } + + Context 'DomainOUPath Validation' { + It 'Should accept empty DomainOUPath' { + { Add-CCSADUniversalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath '' -WhatIf } | Should -Not -Throw + } + It 'Should accept standard DN format' { + { Add-CCSADUniversalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'OU=Groups,DC=Firmax,DC=local' -WhatIf } | Should -Not -Throw + } + It 'Should accept DC-only format' { + { Add-CCSADUniversalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'DC=Firmax,DC=local' -WhatIf } | Should -Not -Throw + } + It 'Should accept LDAP format' { + { Add-CCSADUniversalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'LDAP://DC01.Firmax.local/OU=Groups,DC=Firmax,DC=local' -WhatIf } | Should -Not -Throw + } + It 'Should reject invalid format' { + { Add-CCSADUniversalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'InvalidPath' -WhatIf } | Should -Throw + } + } + + Context 'URL Validation' { + It 'Should accept HTTPS URL' { + { Add-CCSADUniversalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Not -Throw + } + It 'Should reject HTTP URL (only HTTPS allowed)' { + $httpUrl = $script:TestUrl -replace '^https:', 'http:' + { Add-CCSADUniversalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain $script:TestDomain -Url $httpUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Throw + } + It 'Should reject URL without protocol' { + { Add-CCSADUniversalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain $script:TestDomain -Url 'test.com/CCS.asmx' -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Throw + } + } + + Context 'Domain Validation' { + It 'Should accept valid domain format' { + { Add-CCSADUniversalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Not -Throw + } + It 'Should accept subdomain format' { + { Add-CCSADUniversalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain 'sub.firmax.local' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Not -Throw + } + It 'Should reject invalid domain format' { + { Add-CCSADUniversalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain 'invalid_domain' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Throw + } + } + + Context 'ShouldProcess Support' { + It 'Should support WhatIf' { + (Get-Command Add-CCSADUniversalSecurityGroup).Parameters.ContainsKey('WhatIf') | Should -Be $true + } + It 'Should support Confirm' { + (Get-Command Add-CCSADUniversalSecurityGroup).Parameters.ContainsKey('Confirm') | Should -Be $true + } + } + + Context 'CmdletBinding Attributes' { + It 'Should be an advanced function' { + (Get-Command Add-CCSADUniversalSecurityGroup).CmdletBinding | Should -Be $true + } + It 'Should have SupportsShouldProcess' { + (Get-Command Add-CCSADUniversalSecurityGroup).Parameters['WhatIf'] | Should -Not -BeNullOrEmpty + } + It 'Should have OutputType defined' { + (Get-Command Add-CCSADUniversalSecurityGroup).OutputType.Name | Should -Contain 'System.String' + } + } +} + +Describe 'Add-CCSADUniversalSecurityGroup - Integration Tests' -Tag 'Integration' { + Context 'Live CCS Web Service Operations' { + BeforeAll { + $script:TestGroupName = "PesterTestUSG_$(Get-Random -Minimum 1000 -Maximum 9999)" + $script:TestDescription = 'Pester integration test universal group' + } + + It 'Should connect to CCS Web Service successfully' { + { Add-CCSADUniversalSecurityGroup -GroupName $script:TestGroupName -Description $script:TestDescription -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Not -Throw + } + + It 'Should create universal security group with domain credentials' { + $result = Add-CCSADUniversalSecurityGroup -GroupName $script:TestGroupName -Description $script:TestDescription -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Not -BeNullOrEmpty + } + + It 'Should process multiple group names' { + $groups = @("$script:TestGroupName`1", "$script:TestGroupName`2") + { $groups | Add-CCSADUniversalSecurityGroup -Description $script:TestDescription -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should convert LDAP path format' { + $ldapPath = 'LDAP://DC01.Firmax.local/DC=Firmax,DC=local' + { Add-CCSADUniversalSecurityGroup -GroupName $script:TestGroupName -Description $script:TestDescription -DomainOUPath $ldapPath -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should create PesterTestUSG group in TestPester-Universal OU' { + $result = Add-CCSADUniversalSecurityGroup -GroupName 'TestPester-Universal' -Description 'Pester test universal group' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction Stop + + $TestsOk = $false + if ($result -eq 'ok' -or $result -match 'group exist') { + $TestsOk = $true + } + + $TestsOk | Should -Be $true + } + } + + Context 'Error Handling' { + It 'Should handle invalid credentials gracefully' { + $badCred = New-Object System.Management.Automation.PSCredential('baduser', (ConvertTo-SecureString 'badpass' -AsPlainText -Force)) + { Add-CCSADUniversalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $badCred -ErrorAction Stop } | Should -Throw + } + It 'Should handle non-existent URL gracefully' { + { Add-CCSADUniversalSecurityGroup -GroupName 'TestGroup' -Description 'desc' -Domain $script:TestDomain -Url 'https://nonexistent.invalid/CCS.asmx' -CCSCredential $script:TestCCSCredential -ErrorAction Stop } | Should -Throw + } + } +} + +Describe 'Add-CCSADUniversalSecurityGroup - Performance Tests' -Tag 'Performance' { + Context 'Pipeline Performance' { + It 'Should process pipeline input efficiently' { + $groups = 1..10 | ForEach-Object { "USG$_" } + $measure = Measure-Command { + $groups | Add-CCSADUniversalSecurityGroup -Description 'desc' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf -WarningAction SilentlyContinue + } + $measure.TotalSeconds | Should -BeLessThan 5 + } + } } -Add-CCSADUniversalSecurityGroup @Splat \ No newline at end of file diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADUniversalSecurityGroup.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADUniversalSecurityGroup.ps1 index 3772cdb5..b2b2e9b9 100644 --- a/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADUniversalSecurityGroup.ps1 +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADUniversalSecurityGroup.ps1 @@ -1,88 +1,290 @@ -<# +function Add-CCSADUniversalSecurityGroup { + <# .SYNOPSIS - Adds a Universal Security Group to Active Directory. + Creates a universal security group in Active Directory using the CCS Web Service. .DESCRIPTION - Adds a Universal Security Group to Active Directory. + Creates a universal security group in Active Directory using the CCS Web Service. + This advanced function includes comprehensive error handling, input validation, and supports pipeline input. .PARAMETER GroupName - The name of the group to be created. + The name of the security group to be created. Supports pipeline input and accepts multiple group names. .PARAMETER Description - The description of the group. + A description for the security group. .PARAMETER DomainOUPath - The Organizational Unit (OU) path where the group will be created. + The Organizational Unit (OU) path in which the security group will be created. + Supports both standard DN format (OU=Groups,DC=example,DC=com) and LDAP format (LDAP://DC01.example.local/OU=Groups,DC=example,DC=com). .PARAMETER Domain - The domain where the group will be created. + The domain in which the security group will be created. Must be a valid domain name format. .PARAMETER Url - The URL of the CCS Web Service. + The URL of the CCS Web Service. Must be a valid URI format. Example: "https://example.com/CCSWebservice/CCS.asmx" .PARAMETER CCSCredential - The credentials for the CCS Web Service. + The credentials used to authenticate with the CCS Web Service. .PARAMETER DomainCredential - The credentials for the domain. + The credentials of an account with permissions to create the security group. If not defined, it will run in the CCS Web Service context. .PARAMETER PasswordIsEncrypted - Indicates whether the AD password is encrypted. + Indicates if the password in the DomainCredential is encrypted. Default is $false. .EXAMPLE - Add-CCSADUniversalSecurityGroup -GroupName 'TestGroup' -Description 'Test Description' -DomainOUPath 'OU=Groups,DC=example,DC=com' -Domain 'example.com' -Url 'https://example.com/CCSWebservice/CCS.asmx' -CCSCredential $CCSCredential -DomainCredential $DomainCredential -PasswordIsEncrypted $false -#> -function Add-CCSADUniversalSecurityGroup { + PS C:\> Add-CCSADUniversalSecurityGroup -GroupName "TestGroup" -Description "Test Description" -DomainOUPath "OU=Groups,DC=example,DC=com" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + .OUTPUTS + System.String + Returns the result message from the CCS Web Service operation. + + .NOTES + This is an advanced function with support for ShouldProcess, pipeline input, and comprehensive error handling. + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] + [OutputType([string])] param ( - [Parameter(Mandatory = $true)] - [string]$GroupName, - [Parameter(Mandatory = $false)] - [string]$Description, - [Parameter(Mandatory = $false)] - [string]$DomainOUPath, - [Parameter(Mandatory = $true)] + [Parameter( + Mandatory = $true, + Position = 0, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the group name to create' + )] + [ValidateNotNullOrEmpty()] + [Alias('Name', 'Group')] + [string[]]$GroupName, + + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] + [Alias('Desc')] + [string]$Description = '', + + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] + [ValidateScript({ + if ([string]::IsNullOrEmpty($_)) { + return $true + } + # Support standard DN format: OU=...,DC=...,DC=... + if ($_ -match '^(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + # Support LDAP format: LDAP://server/DN + if ($_ -match '^LDAP://[^/]+/(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + throw "DomainOUPath must be in format 'OU=...,DC=...,DC=...' or 'LDAP://server/OU=...,DC=...,DC=...' or empty" + })] + [Alias('OU', 'Path')] + [string]$DomainOUPath = '', + + [Parameter( + Mandatory = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the domain name' + )] + [ValidateNotNullOrEmpty()] + [ValidatePattern('^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$')] [string]$Domain, - [Parameter(Mandatory = $true)] + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service URL' + )] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + if ($_ -match '^https://') { + $true + } else { + throw "URL must start with https:// (secure connection required)" + } + })] + [Alias('WebServiceUrl', 'Uri')] [string]$Url, - [Parameter(Mandatory = $true)] + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service credentials' + )] + [ValidateNotNull()] + [Alias('Credential', 'WebServiceCredential')] [pscredential]$CCSCredential, + [Parameter(Mandatory = $false)] + [ValidateNotNull()] + [Alias('ADCredential')] [pscredential]$DomainCredential, + [Parameter(Mandatory = $false)] - [bool]$PasswordIsEncrypted = $false + [Alias('Encrypted')] + [switch]$PasswordIsEncrypted ) - $FunctionName = 'Add-CCSADUniversalSecurityGroup' - if ($Global:Cs) { - Job_WriteLog -Text "$FunctionName GroupName: $GroupName, Description: $Description, DomainOUPath: $DomainOUPath, Domain: $Domain, Url: $Url, PasswordIsEncrypted: $PasswordIsEncrypted" - } + begin { + $FunctionName = $PSCmdlet.MyInvocation.MyCommand.Name - $CCS = Initialize-CCS -Url $Url -WebServiceCredential $CCSCredential - $ADPassword = $null + Write-Verbose "[$FunctionName] Starting function execution" + Write-Verbose "[$FunctionName] Description: $Description" + Write-Verbose "[$FunctionName] Domain: $Domain" + Write-Verbose "[$FunctionName] DomainOUPath: $DomainOUPath" + Write-Verbose "[$FunctionName] URL: $Url" - if ($DomainCredential -and $PasswordIsEncrypted) { - $ADPassword = $DomainCredential.GetNetworkCredential().Password - } elseif ($DomainCredential -and -not $PasswordIsEncrypted) { - $ADPassword = Get-CCSEncryptedPassword -String $DomainCredential.GetNetworkCredential().Password - } + # Initialize CCS connection once in begin block + try { + Write-Verbose "[$FunctionName] Initializing CCS Web Service connection" + $CCS = Initialize-CCS -Url $Url -WebServiceCredential $CCSCredential -ErrorAction Stop + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to initialize CCS Web Service: $_" ` + -ErrorCategory ConnectionError ` + -TargetObject $Url ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Verify the URL is correct and the CCS Web Service is accessible. Check credentials." + } - $Result = $CCS.ActiveDirectory_AddUniversalSecurityGroup( - $GroupName, - $Description, - $DomainOUPath, - $Domain, - $DomainCredential.UserName, - $ADPassword - ) + # Prepare domain credentials if provided + $ADUsername = $null + $ADPassword = $null + + if ($DomainCredential) { + $ADUsername = $DomainCredential.UserName + Write-Verbose "[$FunctionName] Using domain credential: $ADUsername" - if ($Global:Cs) { - Job_WriteLog -Text "$FunctionName Result: $Result" + try { + if ($PasswordIsEncrypted) { + $ADPassword = $DomainCredential.GetNetworkCredential().Password + Write-Verbose "[$FunctionName] Using pre-encrypted password" + } else { + $ADPassword = Get-CCSEncryptedPassword -String $DomainCredential.GetNetworkCredential().Password -ErrorAction Stop + Write-Verbose "[$FunctionName] Password encrypted successfully" + } + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to process domain credential password: $_" ` + -ErrorCategory SecurityError ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Ensure the domain credential is valid and the password can be encrypted." + } + } else { + Write-Verbose "[$FunctionName] No domain credential provided, using CCS Web Service context" + } + + $ProcessedCount = 0 + $SuccessCount = 0 + $FailureCount = 0 } - $Throw = Invoke-CCSIsError -Result $Result - if ($Throw) { - throw "$FunctionName $Result" + process { + foreach ($Group in $GroupName) { + $ProcessedCount++ + + try { + Write-Verbose "[$FunctionName] Processing group: $Group ($ProcessedCount)" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Processing: GroupName=$Group, Description=$Description, DomainOUPath=$DomainOUPath, Domain=$Domain") + } + + # ShouldProcess support + $WhatIfMessage = "Create universal security group '$Group' in domain '$Domain'" + if ($PSCmdlet.ShouldProcess($Group, $WhatIfMessage)) { + Write-Verbose "[$FunctionName] Calling ActiveDirectory_AddUniversalSecurityGroup for $Group" + + $Result = $CCS.ActiveDirectory_AddUniversalSecurityGroup( + $Group, + $Description, + $DomainOUPath, + $Domain, + $ADUsername, + $ADPassword + ) + Write-Debug "Result from CCS Web Service: $Result" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Result for $Group : $Result") + } + + Write-Verbose "[$FunctionName] Result for $Group : $Result" + + # Check for errors in result + $IsError = Invoke-CCSIsError -Result $Result + + Write-Debug "IsError evaluation for result: $IsError" + if ($IsError) { + $FailureCount++ + + # Determine appropriate error category based on result message + $ErrorCat = [System.Management.Automation.ErrorCategory]::OperationStopped + $RecommendedActionText = "Verify the group name and parameters are correct." + + if ($Result -like '*already exists*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::ResourceExists + $RecommendedActionText = "The group already exists." + } elseif ($Result -like '*does not exist*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::ObjectNotFound + $RecommendedActionText = "Verify that the OU and domain exist." + } elseif ($Result -like '*unwilling to process*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::PermissionDenied + $RecommendedActionText = "Check that the domain credentials have sufficient permissions to create the group." + } + + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + Write-Debug "ErrorAction from PSBoundParameters: $($PSBoundParameters['ErrorAction'])" + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + Write-Debug "ErrorAction from ErrorActionPreference: $ErrorActionPreference" + } + Write-Debug "ShouldThrow is set to: $ShouldThrow" + + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to create universal security group '$Group': $Result" ` + -ErrorCategory $ErrorCat ` + -TargetObject $Group ` + -FunctionName $FunctionName ` + -RecommendedAction $RecommendedActionText ` + -Throw:$ShouldThrow + } else { + $SuccessCount++ + Write-Verbose "[$FunctionName] Successfully created $Group" + Write-Output $Result + } + } else { + Write-Verbose "[$FunctionName] WhatIf: Would create $Group" + } + } catch { + $FailureCount++ + + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + } + + Invoke-CCSErrorHandling ` + -ErrorMessage "Exception occurred while processing group '$Group': $_" ` + -ErrorCategory InvalidOperation ` + -TargetObject $Group ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Check the error details and verify all parameters are correct." ` + -Throw:$ShouldThrow + } + } } - return $Result + end { + Write-Verbose "[$FunctionName] Function execution completed" + Write-Verbose "[$FunctionName] Total processed: $ProcessedCount | Success: $SuccessCount | Failed: $FailureCount" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Summary - Processed: $ProcessedCount, Success: $SuccessCount, Failed: $FailureCount") + } + } } \ No newline at end of file diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADUserToSecurityGroup.Tests.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADUserToSecurityGroup.Tests.ps1 index 4eb6b7f1..320eed60 100644 --- a/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADUserToSecurityGroup.Tests.ps1 +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADUserToSecurityGroup.Tests.ps1 @@ -1,10 +1,237 @@ -# TODO: #436 Write tests for Add-CCSADUserToSecurityGroup -$Splat = @{ - UserName = "Test" - SecurityGroupName = "TestGruppe1" - Domain = 'Firmax.local' - Url = 'https://mracapa02.capainstaller.com/CCSWebservice/CCS.asmx' - CCSCredential = $CCSCredential - DomainCredential = $DomainCredential +BeforeAll { + $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent + + $Items = Get-ChildItem -Path "$RootPath\Capa.PowerShell.Module.CCS\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + + # Setup test environment + $script:TestUrl = "https://$(hostname).capainstaller.com/CCSWebservice/CCS.asmx" + $script:TestDomain = 'Firmax.local' + $CredentialPath = "D:\PowerShell\Credentials\$($env:USERNAME)DomainAdminPesterTests.xml" + + # Setup credentials from environment variables (GitHub secrets) + if ($env:DOMAINADMINUSERNAME -and $env:DOMAINADMINPASSWORD) { + $securePassword = ConvertTo-SecureString $env:DOMAINADMINPASSWORD -AsPlainText -Force + $script:TestDomainCredential = New-Object System.Management.Automation.PSCredential($env:DOMAINADMINUSERNAME, $securePassword) + $script:TestCCSCredential = $script:TestDomainCredential + } elseif (Test-Path -Path $CredentialPath) { + $script:TestDomainCredential = Import-Clixml -Path $CredentialPath + $script:TestCCSCredential = $script:TestDomainCredential + } else { + $script:TestDomainCredential = Get-Credential -Message 'Enter domain admin credentials for integration tests' + $script:TestCCSCredential = $script:TestDomainCredential + } + + $DebugPreference = 'Continue' + $ErrorActionPreference = 'Stop' } -Add-CCSADUserToSecurityGroup @Splat \ No newline at end of file + +Describe 'Add-CCSADUserToSecurityGroup' -Tag 'Unit' { + + Context 'Parameter Validation' { + + It 'Should have mandatory UserName parameter' { + (Get-Command Add-CCSADUserToSecurityGroup).Parameters['UserName'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory SecurityGroupName parameter' { + (Get-Command Add-CCSADUserToSecurityGroup).Parameters['SecurityGroupName'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory Domain parameter' { + (Get-Command Add-CCSADUserToSecurityGroup).Parameters['Domain'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory Url parameter' { + (Get-Command Add-CCSADUserToSecurityGroup).Parameters['Url'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory CCSCredential parameter' { + (Get-Command Add-CCSADUserToSecurityGroup).Parameters['CCSCredential'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have optional DomainCredential parameter' { + (Get-Command Add-CCSADUserToSecurityGroup).Parameters['DomainCredential'].Attributes.Mandatory | Should -Be $false + } + + It 'Should have optional DomainOUPath parameter' { + (Get-Command Add-CCSADUserToSecurityGroup).Parameters['DomainOUPath'].Attributes.Mandatory | Should -Be $false + } + + It 'Should accept array of user names' { + (Get-Command Add-CCSADUserToSecurityGroup).Parameters['UserName'].ParameterType.Name | Should -Be 'String[]' + } + + It 'Should accept pipeline input for UserName' { + (Get-Command Add-CCSADUserToSecurityGroup).Parameters['UserName'].Attributes.ValueFromPipeline | Should -Be $true + } + } + + Context 'Parameter Aliases' { + + It 'Should have alias "Name" for UserName' { + (Get-Command Add-CCSADUserToSecurityGroup).Parameters['UserName'].Aliases | Should -Contain 'Name' + } + + It 'Should have alias "User" for UserName' { + (Get-Command Add-CCSADUserToSecurityGroup).Parameters['UserName'].Aliases | Should -Contain 'User' + } + + It 'Should have alias "GroupName" for SecurityGroupName' { + (Get-Command Add-CCSADUserToSecurityGroup).Parameters['SecurityGroupName'].Aliases | Should -Contain 'GroupName' + } + + It 'Should have alias "OU" for DomainOUPath' { + (Get-Command Add-CCSADUserToSecurityGroup).Parameters['DomainOUPath'].Aliases | Should -Contain 'OU' + } + } + + Context 'DomainOUPath Validation' { + + It 'Should accept empty DomainOUPath' { + { Add-CCSADUserToSecurityGroup -UserName 'testuser' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath '' -WhatIf } | Should -Not -Throw + } + + It 'Should accept standard DN format' { + { Add-CCSADUserToSecurityGroup -UserName 'testuser' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'OU=Users,DC=Firmax,DC=local' -WhatIf } | Should -Not -Throw + } + + It 'Should accept DC-only format' { + { Add-CCSADUserToSecurityGroup -UserName 'testuser' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'DC=Firmax,DC=local' -WhatIf } | Should -Not -Throw + } + + It 'Should accept LDAP format' { + { Add-CCSADUserToSecurityGroup -UserName 'testuser' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'LDAP://DC01.Firmax.local/OU=Users,DC=Firmax,DC=local' -WhatIf } | Should -Not -Throw + } + + It 'Should reject invalid format' { + { Add-CCSADUserToSecurityGroup -UserName 'testuser' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'InvalidPath' -WhatIf } | Should -Throw + } + } + + Context 'URL Validation' { + + It 'Should accept HTTPS URL' { + { Add-CCSADUserToSecurityGroup -UserName 'testuser' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Not -Throw + } + + It 'Should reject HTTP URL (only HTTPS allowed)' { + $httpUrl = $script:TestUrl -replace '^https:', 'http:' + { Add-CCSADUserToSecurityGroup -UserName 'testuser' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $httpUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Throw + } + + It 'Should reject URL without protocol' { + { Add-CCSADUserToSecurityGroup -UserName 'testuser' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url 'test.com/CCS.asmx' -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Throw + } + } + + Context 'Domain Validation' { + + It 'Should accept valid domain format' { + { Add-CCSADUserToSecurityGroup -UserName 'testuser' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Not -Throw + } + + It 'Should accept subdomain format' { + { Add-CCSADUserToSecurityGroup -UserName 'testuser' -SecurityGroupName 'TestGroup' -Domain 'sub.firmax.local' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Not -Throw + } + + It 'Should reject invalid domain format' { + { Add-CCSADUserToSecurityGroup -UserName 'testuser' -SecurityGroupName 'TestGroup' -Domain 'invalid_domain' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Throw + } + } + + Context 'ShouldProcess Support' { + + It 'Should support WhatIf' { + (Get-Command Add-CCSADUserToSecurityGroup).Parameters.ContainsKey('WhatIf') | Should -Be $true + } + + It 'Should support Confirm' { + (Get-Command Add-CCSADUserToSecurityGroup).Parameters.ContainsKey('Confirm') | Should -Be $true + } + } + + Context 'CmdletBinding Attributes' { + + It 'Should be an advanced function' { + (Get-Command Add-CCSADUserToSecurityGroup).CmdletBinding | Should -Be $true + } + + It 'Should have SupportsShouldProcess' { + (Get-Command Add-CCSADUserToSecurityGroup).Parameters['WhatIf'] | Should -Not -BeNullOrEmpty + } + + It 'Should have OutputType defined' { + (Get-Command Add-CCSADUserToSecurityGroup).OutputType.Name | Should -Contain 'System.String' + } + } +} + +Describe 'Add-CCSADUserToSecurityGroup - Integration Tests' -Tag 'Integration' { + + Context 'Live CCS Web Service Operations' { + + BeforeAll { + $script:TestUserName = 'PesterTests' + $script:TestGroupName1 = 'TestPester' + $script:TestGroupName2 = 'TestPester-DomainLocal' + } + + It 'Should connect to CCS Web Service successfully' { + { Add-CCSADUserToSecurityGroup -UserName $script:TestUserName -SecurityGroupName $script:TestGroupName1 -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Not -Throw + } + + It 'Should add user to security group with domain credentials' { + $result = Add-CCSADUserToSecurityGroup -UserName $script:TestUserName -SecurityGroupName $script:TestGroupName1 -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Not -BeNullOrEmpty + } + + It 'Should add user to domain local group' { + $result = Add-CCSADUserToSecurityGroup -UserName $script:TestUserName -SecurityGroupName $script:TestGroupName2 -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Not -BeNullOrEmpty + } + + It 'Should process multiple users' { + $users = @("$script:TestUserName`1", "$script:TestUserName`2") + { $users | Add-CCSADUserToSecurityGroup -SecurityGroupName $script:TestGroupName1 -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should convert LDAP path format' { + $ldapPath = 'LDAP://DC01.Firmax.local/DC=Firmax,DC=local' + { Add-CCSADUserToSecurityGroup -UserName $script:TestUserName -SecurityGroupName $script:TestGroupName1 -DomainOUPath $ldapPath -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should add PesterTests to TestPester group and return ok' { + $result = Add-CCSADUserToSecurityGroup -UserName 'PesterTests' -SecurityGroupName 'TestPester' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction Stop + $result | Should -Be 'The user is already member og the group' + } + } + + Context 'Error Handling' { + + It 'Should handle invalid credentials gracefully' { + $badCred = New-Object System.Management.Automation.PSCredential('baduser', (ConvertTo-SecureString 'badpass' -AsPlainText -Force)) + { Add-CCSADUserToSecurityGroup -UserName 'testuser' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $badCred -ErrorAction Stop } | Should -Throw + } + + It 'Should handle non-existent URL gracefully' { + { Add-CCSADUserToSecurityGroup -UserName 'testuser' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url 'https://nonexistent.invalid/CCS.asmx' -CCSCredential $script:TestCCSCredential -ErrorAction Stop } | Should -Throw + } + } +} + +Describe 'Add-CCSADUserToSecurityGroup - Performance Tests' -Tag 'Performance' { + + Context 'Pipeline Performance' { + + It 'Should process pipeline input efficiently' { + $users = 1..10 | ForEach-Object { "User$_" } + $measure = Measure-Command { + $users | Add-CCSADUserToSecurityGroup -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf -WarningAction SilentlyContinue + } + $measure.TotalSeconds | Should -BeLessThan 5 + } + } +} \ No newline at end of file diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADUserToSecurityGroup.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADUserToSecurityGroup.ps1 index df0eda8b..c6913b86 100644 --- a/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADUserToSecurityGroup.ps1 +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Add-CCSADUserToSecurityGroup.ps1 @@ -1,88 +1,318 @@ -<# +function Add-CCSADUserToSecurityGroup { + <# .SYNOPSIS - Adds a user to a security group in Active Directory using the CCS API. + Adds a user to a security group in Active Directory using the CCS Web Service. .DESCRIPTION - Adds a user to a security group in Active Directory using the CCS API. + Adds a user to a security group in Active Directory using the CCS Web Service. + This advanced function includes comprehensive error handling, input validation, and supports pipeline input. .PARAMETER UserName The username of the user to be added to the security group. + Supports pipeline input and accepts multiple user names. .PARAMETER SecurityGroupName The name of the security group to which the user will be added. .PARAMETER DomainOUPath - The Organizational Unit (OU) path of the domain where the security group resides. + The Organizational Unit (OU) path in which the user resides. + If not specified, searches the entire domain. + Supports both standard DN format (OU=Users,DC=example,DC=com) and LDAP format (LDAP://DC01.example.local/OU=Users,DC=example,DC=com). .PARAMETER Domain - The domain name where the security group resides. + The domain in which the user resides. + Must be a valid domain name format. .PARAMETER Url - The URL of the CCS API. + The URL of the CCS Web Service. + Must be a valid URI format. Example: "https://example.com/CCSWebservice/CCS.asmx" .PARAMETER CCSCredential - The credentials for the CCS API. + The credentials used to authenticate with the CCS Web Service. .PARAMETER DomainCredential - The credentials for the domain where the security group resides. + The credentials of an account with permissions to add the user to the security group. + If not defined, it will run in the CCS Web Service context. .PARAMETER PasswordIsEncrypted - Indicates whether the password is encrypted. Default is $false. + Indicates if the password in the DomainCredential is encrypted. Default is $false. .EXAMPLE - Add-CCSADUserToSecurityGroup -UserName "jdoe" -SecurityGroupName "Domain Users" -DomainOUPath "OU=Users,DC=example,DC=com" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $ccsCredential -DomainCredential $domainCredential -#> -function Add-CCSADUserToSecurityGroup { + PS C:\> Add-CCSADUserToSecurityGroup -UserName "jdoe" -SecurityGroupName "Domain Users" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Adds user jdoe to Domain Users group using default CCS context. + + .EXAMPLE + PS C:\> Add-CCSADUserToSecurityGroup -UserName "jdoe" -SecurityGroupName "Domain Users" -DomainOUPath "OU=Users,DC=example,DC=com" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential -DomainCredential $DomainCredential + + Adds user jdoe to Domain Users group using specific domain credentials and OU path. + + .EXAMPLE + PS C:\> "jdoe", "asmith", "bwilson" | Add-CCSADUserToSecurityGroup -SecurityGroupName "Domain Users" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Adds multiple users to Domain Users group using pipeline input. + + .EXAMPLE + PS C:\> Add-CCSADUserToSecurityGroup -UserName "jdoe" -SecurityGroupName "Domain Users" -DomainOUPath "LDAP://DC01.Firmax.local/OU=Users,DC=FirmaX,DC=local" -Domain "Firmax.local" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Adds user jdoe to Domain Users group using LDAP format for the OU path. The LDAP path will be automatically converted to standard DN format. + + .OUTPUTS + System.String + Returns the result message from the CCS Web Service operation. + + .NOTES + This is an advanced function with support for ShouldProcess, pipeline input, and comprehensive error handling. + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] + [OutputType([string])] param ( - [Parameter(Mandatory = $true)] - [string]$UserName, - [Parameter(Mandatory = $true)] + [Parameter( + Mandatory = $true, + Position = 0, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the username to add to the security group' + )] + [ValidateNotNullOrEmpty()] + [Alias('Name', 'User', 'SamAccountName')] + [string[]]$UserName, + + [Parameter( + Mandatory = $true, + Position = 1, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the security group name' + )] + [ValidateNotNullOrEmpty()] + [Alias('GroupName', 'Group')] [string]$SecurityGroupName, - [Parameter(Mandatory = $false)] - [string]$DomainOUPath, - [Parameter(Mandatory = $true)] + + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] + [ValidateScript({ + if ([string]::IsNullOrEmpty($_)) { + return $true + } + # Support standard DN format: OU=...,DC=...,DC=... + if ($_ -match '^(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + # Support LDAP format: LDAP://server/DN + if ($_ -match '^LDAP://[^/]+/(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + throw "DomainOUPath must be in format 'OU=...,DC=...,DC=...' or 'LDAP://server/OU=...,DC=...,DC=...' or empty" + })] + [Alias('OU', 'Path')] + [string]$DomainOUPath = '', + + [Parameter( + Mandatory = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the domain name' + )] + [ValidateNotNullOrEmpty()] + [ValidatePattern('^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$')] [string]$Domain, - [Parameter(Mandatory = $true)] + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service URL' + )] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + if ($_ -match '^https://') { + $true + } else { + throw "URL must start with https:// (secure connection required)" + } + })] + [Alias('WebServiceUrl', 'Uri')] [string]$Url, - [Parameter(Mandatory = $true)] + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service credentials' + )] + [ValidateNotNull()] + [Alias('Credential', 'WebServiceCredential')] [pscredential]$CCSCredential, + [Parameter(Mandatory = $false)] + [ValidateNotNull()] + [Alias('ADCredential')] [pscredential]$DomainCredential, + [Parameter(Mandatory = $false)] - [bool]$PasswordIsEncrypted = $false + [Alias('Encrypted')] + [switch]$PasswordIsEncrypted ) - $FunctionName = 'Add-CCSADUserToSecurityGroup' - if ($Global:Cs) { - Job_WriteLog -Text "$FunctionName UserName: $UserName, SecurityGroupName: $SecurityGroupName, DomainOUPath: $DomainOUPath, Domain: $Domain, Url: $Url, PasswordIsEncrypted: $PasswordIsEncrypted" - } + begin { + $FunctionName = $PSCmdlet.MyInvocation.MyCommand.Name - $CCS = Initialize-CCS -Url $Url -WebServiceCredential $CCSCredential - $ADPassword = $null + Write-Verbose "[$FunctionName] Starting function execution" + Write-Verbose "[$FunctionName] SecurityGroupName: $SecurityGroupName" + Write-Verbose "[$FunctionName] Domain: $Domain" + Write-Verbose "[$FunctionName] DomainOUPath: $DomainOUPath" + Write-Verbose "[$FunctionName] URL: $Url" - if ($DomainCredential -and $PasswordIsEncrypted) { - $ADPassword = $DomainCredential.GetNetworkCredential().Password - } elseif ($DomainCredential -and -not $PasswordIsEncrypted) { - $ADPassword = Get-CCSEncryptedPassword -String $DomainCredential.GetNetworkCredential().Password - } + # Initialize CCS connection once in begin block + try { + Write-Verbose "[$FunctionName] Initializing CCS Web Service connection" + $CCS = Initialize-CCS -Url $Url -WebServiceCredential $CCSCredential -ErrorAction Stop + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to initialize CCS Web Service: $_" ` + -ErrorCategory ConnectionError ` + -TargetObject $Url ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Verify the URL is correct and the CCS Web Service is accessible. Check credentials." + } - $Result = $CCS.ActiveDirectory_AddUserToSecurityGroup( - $UserName, - $SecurityGroupName, - $DomainOUPath, - $Domain, - $DomainCredential.UserName, - $ADPassword - ) + # Prepare domain credentials if provided + $ADUsername = $null + $ADPassword = $null - if ($Global:Cs) { - Job_WriteLog -Text "$FunctionName Result: $Result" + if ($DomainCredential) { + $ADUsername = $DomainCredential.UserName + Write-Verbose "[$FunctionName] Using domain credential: $ADUsername" + + try { + if ($PasswordIsEncrypted) { + $ADPassword = $DomainCredential.GetNetworkCredential().Password + Write-Verbose "[$FunctionName] Using pre-encrypted password" + } else { + $ADPassword = Get-CCSEncryptedPassword -String $DomainCredential.GetNetworkCredential().Password -ErrorAction Stop + Write-Verbose "[$FunctionName] Password encrypted successfully" + } + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to process domain credential password: $_" ` + -ErrorCategory SecurityError ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Ensure the domain credential is valid and the password can be encrypted." + } + } else { + Write-Verbose "[$FunctionName] No domain credential provided, using CCS Web Service context" + } + + $ProcessedCount = 0 + $SuccessCount = 0 + $FailureCount = 0 } - $Throw = Invoke-CCSIsError -Result $Result - if ($Throw) { - throw "$FunctionName $Result" + process { + foreach ($User in $UserName) { + $ProcessedCount++ + + try { + Write-Verbose "[$FunctionName] Processing user: $User ($ProcessedCount)" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Processing: UserName=$User, SecurityGroupName=$SecurityGroupName, DomainOUPath=$DomainOUPath, Domain=$Domain") + } + + # ShouldProcess support + $WhatIfMessage = "Add user '$User' to security group '$SecurityGroupName' in domain '$Domain'" + if ($PSCmdlet.ShouldProcess($User, $WhatIfMessage)) { + Write-Verbose "[$FunctionName] Calling ActiveDirectory_AddUserToSecurityGroup for $User" + + $Result = $CCS.ActiveDirectory_AddUserToSecurityGroup( + $User, + $SecurityGroupName, + $DomainOUPath, + $Domain, + $ADUsername, + $ADPassword + ) + Write-Debug "Result from CCS Web Service: $Result" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Result for $User : $Result") + } + + Write-Verbose "[$FunctionName] Result for $User : $Result" + + # Check for errors in result + $IsError = Invoke-CCSIsError -Result $Result + + Write-Debug "IsError evaluation for result: $IsError" + if ($IsError) { + $FailureCount++ + + # Determine appropriate error category based on result message + $ErrorCat = [System.Management.Automation.ErrorCategory]::OperationStopped + $RecommendedActionText = "Verify the user and security group names are correct." + + if ($Result -like '*does not exist*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::ObjectNotFound + $RecommendedActionText = "Verify that the user '$User' and security group '$SecurityGroupName' exist in Active Directory." + } elseif ($Result -like '*unwilling to process*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::PermissionDenied + $RecommendedActionText = "Check that the domain credentials have sufficient permissions to modify the security group." + } elseif ($Result -like '*already a member*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::ResourceExists + $RecommendedActionText = "The user is already a member of the security group." + } + + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + Write-Debug "ErrorAction from PSBoundParameters: $($PSBoundParameters['ErrorAction'])" + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + Write-Debug "ErrorAction from ErrorActionPreference: $ErrorActionPreference" + } + Write-Debug "ShouldThrow is set to: $ShouldThrow" + + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to add user '$User' to security group '$SecurityGroupName': $Result" ` + -ErrorCategory $ErrorCat ` + -TargetObject $User ` + -FunctionName $FunctionName ` + -RecommendedAction $RecommendedActionText ` + -Throw:$ShouldThrow + } else { + $SuccessCount++ + Write-Verbose "[$FunctionName] Successfully added $User to $SecurityGroupName" + Write-Output $Result + } + } else { + Write-Verbose "[$FunctionName] WhatIf: Would add $User to $SecurityGroupName" + } + } catch { + $FailureCount++ + + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + } + + Invoke-CCSErrorHandling ` + -ErrorMessage "Exception occurred while processing user '$User': $_" ` + -ErrorCategory InvalidOperation ` + -TargetObject $User ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Check the error details and verify all parameters are correct." ` + -Throw:$ShouldThrow + } + } } - return $Result + end { + Write-Verbose "[$FunctionName] Function execution completed" + Write-Verbose "[$FunctionName] Total processed: $ProcessedCount | Success: $SuccessCount | Failed: $FailureCount" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Summary - Processed: $ProcessedCount, Success: $SuccessCount, Failed: $FailureCount") + } + } } \ No newline at end of file diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Confirm-CCSADUser.Tests.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Confirm-CCSADUser.Tests.ps1 new file mode 100644 index 00000000..5d7a3d5c --- /dev/null +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Confirm-CCSADUser.Tests.ps1 @@ -0,0 +1,114 @@ +BeforeAll { + $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent + + $Items = Get-ChildItem -Path "$RootPath\Capa.PowerShell.Module.CCS\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + + # Setup test environment + $script:TestUrl = "https://$(hostname).capainstaller.com/CCSWebservice/CCS.asmx" + $script:TestDomain = 'Firmax.local' + $CredentialPath = "D:\PowerShell\Credentials\$($env:USERNAME)DomainAdminPesterTests.xml" + + # Setup credentials from environment variables (GitHub secrets) + if ($env:DOMAINADMINUSERNAME -and $env:DOMAINADMINPASSWORD) { + $securePassword = ConvertTo-SecureString $env:DOMAINADMINPASSWORD -AsPlainText -Force + $script:TestDomainCredential = New-Object System.Management.Automation.PSCredential($env:DOMAINADMINUSERNAME, $securePassword) + $script:TestCCSCredential = $script:TestDomainCredential + } elseif (Test-Path -Path $CredentialPath) { + $script:TestDomainCredential = Import-Clixml -Path $CredentialPath + $script:TestCCSCredential = $script:TestDomainCredential + } else { + $script:TestDomainCredential = Get-Credential -Message 'Enter domain admin credentials for integration tests' + $script:TestCCSCredential = $script:TestDomainCredential + } + + # Create invalid credentials for testing + $invalidPassword = ConvertTo-SecureString 'InvalidPassword123!' -AsPlainText -Force + $script:InvalidDomainCredential = New-Object System.Management.Automation.PSCredential('InvalidUser', $invalidPassword) + + $DebugPreference = 'Continue' + $ErrorActionPreference = 'Stop' +} + +Describe 'Confirm-CCSADUser' -Tag 'Unit' { + + Context 'Parameter Validation' { + + It 'Should have mandatory DomainCredential parameter' { + (Get-Command Confirm-CCSADUser).Parameters['DomainCredential'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory Domain parameter' { + (Get-Command Confirm-CCSADUser).Parameters['Domain'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory Url parameter' { + (Get-Command Confirm-CCSADUser).Parameters['Url'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory CCSCredential parameter' { + (Get-Command Confirm-CCSADUser).Parameters['CCSCredential'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have optional DomainOUPath parameter' { + (Get-Command Confirm-CCSADUser).Parameters['DomainOUPath'].Attributes.Mandatory | Should -Be $false + } + + It 'Should have optional PasswordIsEncrypted parameter' { + (Get-Command Confirm-CCSADUser).Parameters['PasswordIsEncrypted'].Attributes.Mandatory | Should -Be $false + } + + It 'Should have optional UsePrincipalContext parameter' { + (Get-Command Confirm-CCSADUser).Parameters['UsePrincipalContext'].Attributes.Mandatory | Should -Be $false + } + + It 'Should accept pipeline input for DomainCredential' { + (Get-Command Confirm-CCSADUser).Parameters['DomainCredential'].Attributes.ValueFromPipeline | Should -Be $true + } + + It 'Should have PSCredential type for DomainCredential' { + (Get-Command Confirm-CCSADUser).Parameters['DomainCredential'].ParameterType.Name | Should -Be 'PSCredential' + } + + It 'Should have PSCredential type for CCSCredential' { + (Get-Command Confirm-CCSADUser).Parameters['CCSCredential'].ParameterType.Name | Should -Be 'PSCredential' + } + } + + Context 'Function Attributes' { + + It 'Should have alias "Validate-CCSADUser" for function' { + $Command = Get-Command Confirm-CCSADUser + $Command.Name | Should -Be 'Confirm-CCSADUser' + } + } + + Context 'CmdletBinding Attributes' { + + It 'Should be an advanced function' { + (Get-Command Confirm-CCSADUser).CmdletBinding | Should -Be $true + } + + It 'Should NOT have SupportsShouldProcess (read-only operation)' { + (Get-Command Confirm-CCSADUser).Parameters['WhatIf'] | Should -BeNullOrEmpty + } + + It 'Should have OutputType attribute' { + $Command = Get-Command Confirm-CCSADUser + $Command.OutputType | Should -Not -BeNullOrEmpty + } + } + + Context 'Switch Parameters' { + + It 'Should have PasswordIsEncrypted as switch parameter' { + (Get-Command Confirm-CCSADUser).Parameters['PasswordIsEncrypted'].ParameterType.Name | Should -Be 'SwitchParameter' + } + + It 'Should have UsePrincipalContext as switch parameter' { + (Get-Command Confirm-CCSADUser).Parameters['UsePrincipalContext'].ParameterType.Name | Should -Be 'SwitchParameter' + } + } +} diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Confirm-CCSADUser.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Confirm-CCSADUser.ps1 new file mode 100644 index 00000000..e9838bdb --- /dev/null +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Confirm-CCSADUser.ps1 @@ -0,0 +1,186 @@ +<# + .SYNOPSIS + Validates a user's credentials against Active Directory. + + .DESCRIPTION + This function validates a user's credentials against Active Directory using the CCS Web Service. + It can use either PrincipalContext-based validation or direct LDAP validation. + + .PARAMETER DomainOUPath + The distinguished name or LDAP path of the OU where the user is located. + If not specified, the entire domain will be searched. + + .PARAMETER Domain + The domain name where the user is located. + + .PARAMETER Url + The URL of the CCS Web Service, e.g., https://ccs.example.com/CCS. + + .PARAMETER CCSCredential + The credentials used to connect to the CCS Web Service. + + .PARAMETER DomainCredential + The credentials to validate against Active Directory. + This is the user whose credentials you want to verify. + + .PARAMETER PasswordIsEncrypted + Indicates whether the password in the credentials is already encrypted. + + .PARAMETER UsePrincipalContext + When specified, uses PrincipalContext-based validation. + When not specified, uses direct LDAP validation. + Default is to use PrincipalContext. + + .EXAMPLE + PS C:\> $CCSCred = Get-Credential + PS C:\> $UserCred = Get-Credential + PS C:\> Confirm-CCSADUser -Domain 'contoso.com' -Url 'https://ccs.contoso.com/CCS' -CCSCredential $CCSCred -DomainCredential $UserCred + + Validates the user credentials in $UserCred against the contoso.com domain using PrincipalContext validation. + + .EXAMPLE + PS C:\> Confirm-CCSADUser -DomainOUPath 'OU=Users,DC=contoso,DC=com' -Domain 'contoso.com' -Url 'https://ccs.contoso.com/CCS' -CCSCredential $CCSCred -DomainCredential $UserCred + + Validates the user credentials against the specified OU in the contoso.com domain. + + .EXAMPLE + PS C:\> Confirm-CCSADUser -Domain 'contoso.com' -Url 'https://ccs.contoso.com/CCS' -CCSCredential $CCSCred -DomainCredential $UserCred -UsePrincipalContext:$false + + Validates the user credentials using direct LDAP validation instead of PrincipalContext. + + .NOTES + For more information, see https://capasystems.atlassian.net/wiki/spaces/CI65DOC/pages/19306246741/ActiveDirectory+ValidateUser + + .LINK + https://capasystems.atlassian.net/wiki/spaces/CI65DOC/pages/19306246741/ActiveDirectory+ValidateUser +#> +function Confirm-CCSADUser { + [CmdletBinding()] + [OutputType([System.Boolean])] + [Alias('Validate-CCSADUser')] + param + ( + [Parameter(Mandatory = $false)] + [ValidateScript({ + if ([string]::IsNullOrWhiteSpace($_)) { + $true + } elseif ($_ -match '^(CN=|OU=|DC=)' -or $_ -match '^LDAP://[^/]+/(CN=|OU=|DC=)') { + $true + } else { + throw "DomainOUPath must be a valid Distinguished Name (DN) or LDAP path starting with 'LDAP://server/DN'." + } + })] + [string]$DomainOUPath, + [Parameter(Mandatory = $true)] + [ValidatePattern('^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')] + [string]$Domain, + [Parameter(Mandatory = $true)] + [ValidateScript({ + if ($_ -match '^https://') { + $true + } else { + throw "URL must start with 'https://'" + } + })] + [string]$Url, + [Parameter(Mandatory = $true)] + [pscredential]$CCSCredential, + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [pscredential]$DomainCredential, + [Parameter(Mandatory = $false)] + [switch]$PasswordIsEncrypted, + [Parameter(Mandatory = $false)] + [switch]$UsePrincipalContext = $true + ) + + begin { + $FunctionName = $MyInvocation.MyCommand.Name + Write-Verbose "[$FunctionName] Starting function" + + # Initialize CCS connection + try { + Write-Verbose "[$FunctionName] Initializing CCS Web Service connection" + $CCS = Initialize-CCS -Url $Url -WebServiceCredential $CCSCredential -ErrorAction Stop + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to initialize CCS Web Service: $_" ` + -ErrorCategory ConnectionError ` + -TargetObject $Url ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Verify the URL is correct and the CCS Web Service is accessible. Check credentials." + return + } + + # Convert LDAP path to DN if needed + if ($PSBoundParameters.ContainsKey('DomainOUPath')) { + if ($DomainOUPath -match '^LDAP://[^/]+/(.+)$') { + $DomainOUPath = $Matches[1] + Write-Verbose "Converted LDAP path to DN: $DomainOUPath" + } + } else { + $DomainOUPath = '' + Write-Verbose "No DomainOUPath specified, will search entire domain" + } + + # Extract domain username + $DomainUser = $DomainCredential.UserName + Write-Verbose "Validating credentials for user: $DomainUser" + + # Get encrypted password + $DomainUserPwd = Get-CCSEncryptedPassword -SecureString $DomainCredential.Password + + $TotalProcessed = 0 + $TotalValid = 0 + $TotalInvalid = 0 + } + + process { + try { + Write-Verbose "Calling ActiveDirectory_ValidateUser for user '$DomainUser' with UsePrincipalContext=$($UsePrincipalContext.IsPresent)" + + # Call CCS Web Service + $Result = $CCS.ActiveDirectory_ValidateUser( + $DomainOUPath, + $Domain, + $DomainUser, + $DomainUserPwd, + $UsePrincipalContext.IsPresent + ) + + $TotalProcessed++ + + if ($Result) { + Write-Verbose "Credentials validated successfully for user '$DomainUser'" + $TotalValid++ + } else { + Write-Verbose "Credentials validation failed for user '$DomainUser'" + $TotalInvalid++ + } + + # Return the boolean result + return $Result + + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to validate user '$DomainUser': $_" ` + -ErrorCategory AuthenticationError ` + -TargetObject $DomainUser ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Verify the domain credentials are correct and the domain is accessible." ` + -Throw:$false + + $TotalProcessed++ + $TotalInvalid++ + return $false + } + } + + end { + Write-Verbose "Completed credential validation" + Write-Verbose "Total processed: $TotalProcessed" + Write-Verbose "Valid credentials: $TotalValid" + Write-Verbose "Invalid credentials: $TotalInvalid" + } +} diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Create-CCSADSecurityGroupsForShares.Tests.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Create-CCSADSecurityGroupsForShares.Tests.ps1 new file mode 100644 index 00000000..b6444b00 --- /dev/null +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Create-CCSADSecurityGroupsForShares.Tests.ps1 @@ -0,0 +1,322 @@ +BeforeAll { + $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent + + $Items = Get-ChildItem -Path "$RootPath\Capa.PowerShell.Module.CCS\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + + # Setup test environment + $script:TestUrl = "https://$(hostname).capainstaller.com/CCSWebservice/CCS.asmx" + $script:TestDomain = 'Firmax.local' + $CredentialPath = "D:\PowerShell\Credentials\$($env:USERNAME)DomainAdminPesterTests.xml" + + # Setup credentials from environment variables (GitHub secrets) + if ($env:DOMAINADMINUSERNAME -and $env:DOMAINADMINPASSWORD) { + $securePassword = ConvertTo-SecureString $env:DOMAINADMINPASSWORD -AsPlainText -Force + $script:TestDomainCredential = New-Object System.Management.Automation.PSCredential($env:DOMAINADMINUSERNAME, $securePassword) + $script:TestCCSCredential = $script:TestDomainCredential + } elseif (Test-Path -Path $CredentialPath) { + $script:TestDomainCredential = Import-Clixml -Path $CredentialPath + $script:TestCCSCredential = $script:TestDomainCredential + } else { + $script:TestDomainCredential = Get-Credential -Message 'Enter domain admin credentials for integration tests' + $script:TestCCSCredential = $script:TestDomainCredential + } + + $DebugPreference = 'Continue' + $ErrorActionPreference = 'Stop' +} + +Describe 'Create-CCSADSecurityGroupsForShares' -Tag 'Unit' { + + Context 'Parameter Validation' { + + It 'Should have optional DomainOUPath parameter' { + (Get-Command Create-CCSADSecurityGroupsForShares).Parameters['DomainOUPath'].Attributes.Mandatory | Should -Be $false + } + + It 'Should have mandatory Domain parameter' { + (Get-Command Create-CCSADSecurityGroupsForShares).Parameters['Domain'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory Url parameter' { + (Get-Command Create-CCSADSecurityGroupsForShares).Parameters['Url'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory CCSCredential parameter' { + (Get-Command Create-CCSADSecurityGroupsForShares).Parameters['CCSCredential'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have optional DomainCredential parameter' { + (Get-Command Create-CCSADSecurityGroupsForShares).Parameters['DomainCredential'].Attributes.Mandatory | Should -Be $false + } + + It 'Should have mandatory GroupFormat parameter' { + (Get-Command Create-CCSADSecurityGroupsForShares).Parameters['GroupFormat'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory GroupDescriptionFormat parameter' { + (Get-Command Create-CCSADSecurityGroupsForShares).Parameters['GroupDescriptionFormat'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have optional CreateReadGroup parameter' { + (Get-Command Create-CCSADSecurityGroupsForShares).Parameters['CreateReadGroup'].Attributes.Mandatory | Should -Be $false + } + + It 'Should have optional CreateReadWriteGroup parameter' { + (Get-Command Create-CCSADSecurityGroupsForShares).Parameters['CreateReadWriteGroup'].Attributes.Mandatory | Should -Be $false + } + + It 'Should have optional ExcludeStandardShares parameter' { + (Get-Command Create-CCSADSecurityGroupsForShares).Parameters['ExcludeStandardShares'].Attributes.Mandatory | Should -Be $false + } + + It 'Should have optional PasswordIsEncrypted parameter' { + (Get-Command Create-CCSADSecurityGroupsForShares).Parameters['PasswordIsEncrypted'].Attributes.Mandatory | Should -Be $false + } + } + + Context 'Parameter Aliases' { + + It 'Should have alias "OU" for DomainOUPath' { + (Get-Command Create-CCSADSecurityGroupsForShares).Parameters['DomainOUPath'].Aliases | Should -Contain 'OU' + } + + It 'Should have alias "Path" for DomainOUPath' { + (Get-Command Create-CCSADSecurityGroupsForShares).Parameters['DomainOUPath'].Aliases | Should -Contain 'Path' + } + + It 'Should have alias "Format" for GroupFormat' { + (Get-Command Create-CCSADSecurityGroupsForShares).Parameters['GroupFormat'].Aliases | Should -Contain 'Format' + } + + It 'Should have alias "NameFormat" for GroupFormat' { + (Get-Command Create-CCSADSecurityGroupsForShares).Parameters['GroupFormat'].Aliases | Should -Contain 'NameFormat' + } + + It 'Should have alias "Read" for CreateReadGroup' { + (Get-Command Create-CCSADSecurityGroupsForShares).Parameters['CreateReadGroup'].Aliases | Should -Contain 'Read' + } + + It 'Should have alias "ReadWrite" for CreateReadWriteGroup' { + (Get-Command Create-CCSADSecurityGroupsForShares).Parameters['CreateReadWriteGroup'].Aliases | Should -Contain 'ReadWrite' + } + } + + Context 'ShouldProcess Support' { + + It 'Should support WhatIf' { + (Get-Command Create-CCSADSecurityGroupsForShares).Parameters.ContainsKey('WhatIf') | Should -Be $true + } + + It 'Should support Confirm' { + (Get-Command Create-CCSADSecurityGroupsForShares).Parameters.ContainsKey('Confirm') | Should -Be $true + } + } + + Context 'CmdletBinding Attributes' { + + It 'Should be an advanced function' { + (Get-Command Create-CCSADSecurityGroupsForShares).CmdletBinding | Should -Be $true + } + + It 'Should have SupportsShouldProcess' { + (Get-Command Create-CCSADSecurityGroupsForShares).Parameters['WhatIf'] | Should -Not -BeNullOrEmpty + } + + It 'Should have OutputType defined' { + (Get-Command Create-CCSADSecurityGroupsForShares).OutputType.Name | Should -Contain 'System.String' + } + } +} + +Describe 'Create-CCSADSecurityGroupsForShares - Integration Tests' -Tag 'Integration' { + + Context 'DomainOUPath Validation' { + + It 'Should accept empty DomainOUPath' { + { Create-CCSADSecurityGroupsForShares -GroupFormat 'Share_$sharename$' -GroupDescriptionFormat 'Access to $sharename$' -CreateReadGroup -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath '' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept standard DN format' { + { Create-CCSADSecurityGroupsForShares -GroupFormat 'Share_$sharename$' -GroupDescriptionFormat 'Access to $sharename$' -CreateReadGroup -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'OU=Groups,DC=Firmax,DC=local' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept DC-only format' { + { Create-CCSADSecurityGroupsForShares -GroupFormat 'Share_$sharename$' -GroupDescriptionFormat 'Access to $sharename$' -CreateReadGroup -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'DC=Firmax,DC=local' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept LDAP format' { + { Create-CCSADSecurityGroupsForShares -GroupFormat 'Share_$sharename$' -GroupDescriptionFormat 'Access to $sharename$' -CreateReadGroup -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'LDAP://DC01.Firmax.local/OU=Groups,DC=Firmax,DC=local' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should reject invalid format' { + { Create-CCSADSecurityGroupsForShares -GroupFormat 'Share_$sharename$' -GroupDescriptionFormat 'Access to $sharename$' -CreateReadGroup -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'InvalidPath' } | Should -Throw + } + } + + Context 'URL Validation' { + + It 'Should accept HTTPS URL' { + { Create-CCSADSecurityGroupsForShares -GroupFormat 'Share_$sharename$' -GroupDescriptionFormat 'Access to $sharename$' -CreateReadGroup -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should reject HTTP URL (only HTTPS allowed)' { + $httpUrl = $script:TestUrl -replace '^https:', 'http:' + { Create-CCSADSecurityGroupsForShares -GroupFormat 'Share_$sharename$' -GroupDescriptionFormat 'Access to $sharename$' -CreateReadGroup -Domain $script:TestDomain -Url $httpUrl -CCSCredential $script:TestCCSCredential } | Should -Throw + } + + It 'Should reject URL without protocol' { + { Create-CCSADSecurityGroupsForShares -GroupFormat 'Share_$sharename$' -GroupDescriptionFormat 'Access to $sharename$' -CreateReadGroup -Domain $script:TestDomain -Url 'test.com/CCS.asmx' -CCSCredential $script:TestCCSCredential } | Should -Throw + } + } + + Context 'Domain Validation' { + + It 'Should accept valid domain format' { + { Create-CCSADSecurityGroupsForShares -GroupFormat 'Share_$sharename$' -GroupDescriptionFormat 'Access to $sharename$' -CreateReadGroup -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept subdomain format' { + { Create-CCSADSecurityGroupsForShares -GroupFormat 'Share_$sharename$' -GroupDescriptionFormat 'Access to $sharename$' -CreateReadGroup -Domain 'sub.firmax.local' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should reject invalid domain format' { + { Create-CCSADSecurityGroupsForShares -GroupFormat 'Share_$sharename$' -GroupDescriptionFormat 'Access to $sharename$' -CreateReadGroup -Domain 'invalid_domain' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential } | Should -Throw + } + } + + Context 'Group Type Validation' { + + It 'Should throw when neither CreateReadGroup nor CreateReadWriteGroup is specified' { + { Create-CCSADSecurityGroupsForShares -GroupFormat 'Share_$sharename$' -GroupDescriptionFormat 'Access to $sharename$' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction Stop } | Should -Throw + } + + It 'Should accept when only CreateReadGroup is specified' { + { Create-CCSADSecurityGroupsForShares -GroupFormat 'Share_$sharename$' -GroupDescriptionFormat 'Access to $sharename$' -CreateReadGroup -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept when only CreateReadWriteGroup is specified' { + { Create-CCSADSecurityGroupsForShares -GroupFormat 'Share_$sharename$' -GroupDescriptionFormat 'Access to $sharename$' -CreateReadWriteGroup -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept when both CreateReadGroup and CreateReadWriteGroup are specified' { + { Create-CCSADSecurityGroupsForShares -GroupFormat 'Share_$sharename$' -GroupDescriptionFormat 'Access to $sharename$' -CreateReadGroup -CreateReadWriteGroup -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + } + + Context 'Live CCS Web Service Operations' { + + It 'Should create security groups for shares with domain credentials' { + $result = Create-CCSADSecurityGroupsForShares ` + -GroupFormat 'PesterShare_$sharename$' ` + -GroupDescriptionFormat 'Test share access to $sharename$' ` + -CreateReadGroup ` + -CreateReadWriteGroup ` + -ExcludeStandardShares ` + -Domain $script:TestDomain ` + -Url $script:TestUrl ` + -CCSCredential $script:TestCCSCredential ` + -DomainCredential $script:TestDomainCredential ` + -ErrorAction SilentlyContinue ` + -WarningAction SilentlyContinue + $result | Should -Not -BeNullOrEmpty + } + + It 'Should create only read groups when CreateReadGroup is specified' { + { Create-CCSADSecurityGroupsForShares ` + -GroupFormat 'PesterRO_$sharename$' ` + -GroupDescriptionFormat 'Read-only access to $sharename$' ` + -CreateReadGroup ` + -ExcludeStandardShares ` + -Domain $script:TestDomain ` + -Url $script:TestUrl ` + -CCSCredential $script:TestCCSCredential ` + -DomainCredential $script:TestDomainCredential ` + -ErrorAction SilentlyContinue ` + -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should work without DomainCredential (CCS context)' { + { Create-CCSADSecurityGroupsForShares ` + -GroupFormat 'PesterShare_$sharename$' ` + -GroupDescriptionFormat 'Share access' ` + -CreateReadGroup ` + -Domain $script:TestDomain ` + -Url $script:TestUrl ` + -CCSCredential $script:TestCCSCredential ` + -ErrorAction SilentlyContinue ` + -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should handle LDAP format DomainOUPath' { + $ldapPath = 'LDAP://DC01.Firmax.local/OU=Groups,DC=Firmax,DC=local' + { Create-CCSADSecurityGroupsForShares ` + -DomainOUPath $ldapPath ` + -GroupFormat 'PesterShare_$sharename$' ` + -GroupDescriptionFormat 'Share access' ` + -CreateReadGroup ` + -CreateReadWriteGroup ` + -ExcludeStandardShares ` + -Domain $script:TestDomain ` + -Url $script:TestUrl ` + -CCSCredential $script:TestCCSCredential ` + -DomainCredential $script:TestDomainCredential ` + -ErrorAction SilentlyContinue ` + -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should handle placeholders in group description' { + { Create-CCSADSecurityGroupsForShares ` + -GroupFormat 'FS_$sharename$' ` + -GroupDescriptionFormat 'Access to $sharename$ at $shareunc$ (Path: $localpath$)' ` + -CreateReadGroup ` + -ExcludeStandardShares ` + -Domain $script:TestDomain ` + -Url $script:TestUrl ` + -CCSCredential $script:TestCCSCredential ` + -DomainCredential $script:TestDomainCredential ` + -ErrorAction SilentlyContinue ` + -WarningAction SilentlyContinue } | Should -Not -Throw + } + } + + Context 'Error Handling' { + + It 'Should handle invalid credentials gracefully' { + $badCred = New-Object System.Management.Automation.PSCredential('baduser', (ConvertTo-SecureString 'badpass' -AsPlainText -Force)) + { Create-CCSADSecurityGroupsForShares ` + -GroupFormat 'Share_$sharename$' ` + -GroupDescriptionFormat 'Access' ` + -CreateReadGroup ` + -Domain $script:TestDomain ` + -Url $script:TestUrl ` + -CCSCredential $badCred ` + -ErrorAction Stop } | Should -Throw + } + } +} + +Describe 'Create-CCSADSecurityGroupsForShares - Performance Tests' -Tag 'Performance' { + + Context 'Execution Performance' { + + It 'Should complete in reasonable time' { + $measure = Measure-Command { + Create-CCSADSecurityGroupsForShares ` + -GroupFormat 'PesterPerf_$sharename$' ` + -GroupDescriptionFormat 'Performance test' ` + -CreateReadGroup ` + -ExcludeStandardShares ` + -Domain $script:TestDomain ` + -Url $script:TestUrl ` + -CCSCredential $script:TestCCSCredential ` + -DomainCredential $script:TestDomainCredential ` + -ErrorAction SilentlyContinue ` + -WarningAction SilentlyContinue + } + $measure.TotalSeconds | Should -BeLessThan 30 + } + } +} diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Create-CCSADSecurityGroupsForShares.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Create-CCSADSecurityGroupsForShares.ps1 new file mode 100644 index 00000000..f72fae7c --- /dev/null +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Create-CCSADSecurityGroupsForShares.ps1 @@ -0,0 +1,344 @@ +function Create-CCSADSecurityGroupsForShares { + <# + .SYNOPSIS + Creates Active Directory security groups for network shares using the CCS Web Service. + + .DESCRIPTION + Creates Active Directory security groups for network shares using the CCS Web Service. + This advanced function includes comprehensive error handling, input validation, and supports creating both read and read/write groups. + The function can automatically create security groups based on share names with customizable group name and description formats. + + .PARAMETER DomainOUPath + The Organizational Unit (OU) path where the security groups will be created. + If not specified, groups will be created in the default location. + Supports both standard DN format (OU=Groups,DC=example,DC=com) and LDAP format (LDAP://DC01.example.local/OU=Groups,DC=example,DC=com). + + .PARAMETER Domain + The domain in which the security groups will be created. + Must be a valid domain name format. + + .PARAMETER Url + The URL of the CCS Web Service. + Must be a valid HTTPS URI format. Example: "https://example.com/CCSWebservice/CCS.asmx" + + .PARAMETER CCSCredential + The credentials used to authenticate with the CCS Web Service. + + .PARAMETER DomainCredential + The credentials of an account with permissions to create security groups in Active Directory. + If not defined, it will run in the CCS Web Service context. + + .PARAMETER GroupFormat + The format string for the group name. Use placeholders: $sharename$, $sharecaption$ + Example: "Share_$sharename$" will create groups like "Share_Data_R" and "Share_Data_RW" + + .PARAMETER GroupDescriptionFormat + The format string for the group description. Use placeholders: $sharename$, $sharecaption$, $shareunc$, $localpath$ + Example: "Access to $sharename$ share at $shareunc$" + + .PARAMETER CreateReadGroup + If specified, creates a read-only group (suffix: _R) for each share. + + .PARAMETER CreateReadWriteGroup + If specified, creates a read/write group (suffix: _RW) for each share. + + .PARAMETER ExcludeStandardShares + If specified, excludes standard Windows shares (C$, ADMIN$, IPC$, etc.) from group creation. + + .PARAMETER PasswordIsEncrypted + Indicates if the password in the DomainCredential is encrypted. Default is $false. + + .EXAMPLE + PS C:\> Create-CCSADSecurityGroupsForShares -GroupFormat "Share_`$sharename`$" -GroupDescriptionFormat "Access to `$sharename`$" -CreateReadGroup -CreateReadWriteGroup -ExcludeStandardShares -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Creates both read and read/write groups for all shares (excluding standard shares) using default CCS context. + + .EXAMPLE + PS C:\> Create-CCSADSecurityGroupsForShares -DomainOUPath "OU=ShareGroups,DC=example,DC=com" -GroupFormat "FS_`$sharename`$" -GroupDescriptionFormat "File share: `$sharename`$ (`$localpath`$)" -CreateReadGroup -CreateReadWriteGroup -ExcludeStandardShares -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential -DomainCredential $DomainCredential + + Creates groups in a specific OU with custom format including local path in description. + + .EXAMPLE + PS C:\> Create-CCSADSecurityGroupsForShares -GroupFormat "RO_`$sharename`$" -GroupDescriptionFormat "Read-only access to `$shareunc`$" -CreateReadGroup -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Creates only read groups for all shares. + + .OUTPUTS + System.String + Returns the result message from the CCS Web Service operation. + + .NOTES + This is an advanced function with support for ShouldProcess, comprehensive error handling, and detailed logging. + Group name suffixes: _R (read-only), _RW (read/write) + Available placeholders: $sharename$, $sharecaption$, $shareunc$, $localpath$ + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] + [OutputType([string])] + param ( + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] + [ValidateScript({ + if ([string]::IsNullOrEmpty($_)) { + return $true + } + # Support standard DN format: OU=...,DC=...,DC=... + if ($_ -match '^(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + # Support LDAP format: LDAP://server/DN + if ($_ -match '^LDAP://[^/]+/(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + throw "DomainOUPath must be in format 'OU=...,DC=...,DC=...' or 'LDAP://server/OU=...,DC=...,DC=...' or empty" + })] + [Alias('OU', 'Path')] + [string]$DomainOUPath = '', + + [Parameter( + Mandatory = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the domain name' + )] + [ValidateNotNullOrEmpty()] + [ValidatePattern('^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$')] + [string]$Domain, + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service URL' + )] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + if ($_ -match '^https://') { + $true + } else { + throw "URL must start with https:// (secure connection required)" + } + })] + [Alias('WebServiceUrl', 'Uri')] + [string]$Url, + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service credentials' + )] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + if ($_ -is [System.Management.Automation.PSCredential]) { + $true + } else { + throw "CCSCredential must be a PSCredential object" + } + })] + [Alias('Credential', 'WebServiceCredential')] + [pscredential]$CCSCredential, + + [Parameter(Mandatory = $false)] + [ValidateNotNull()] + [Alias('ADCredential')] + [pscredential]$DomainCredential, + + [Parameter( + Mandatory = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the group name format (use $sharename$ or $sharecaption$ as placeholders)' + )] + [ValidateNotNullOrEmpty()] + [Alias('Format', 'NameFormat')] + [string]$GroupFormat, + + [Parameter( + Mandatory = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the group description format (use $sharename$, $sharecaption$, $shareunc$, or $localpath$ as placeholders)' + )] + [ValidateNotNullOrEmpty()] + [Alias('DescriptionFormat', 'Description')] + [string]$GroupDescriptionFormat, + + [Parameter(Mandatory = $false)] + [Alias('Read', 'ReadOnly')] + [switch]$CreateReadGroup, + + [Parameter(Mandatory = $false)] + [Alias('ReadWrite', 'RW')] + [switch]$CreateReadWriteGroup, + + [Parameter(Mandatory = $false)] + [Alias('ExcludeStandard', 'SkipStandardShares')] + [switch]$ExcludeStandardShares, + + [Parameter(Mandatory = $false)] + [Alias('Encrypted')] + [switch]$PasswordIsEncrypted + ) + + begin { + $FunctionName = $PSCmdlet.MyInvocation.MyCommand.Name + + Write-Verbose "[$FunctionName] Starting function execution" + Write-Verbose "[$FunctionName] Domain: $Domain" + Write-Verbose "[$FunctionName] DomainOUPath: $DomainOUPath" + Write-Verbose "[$FunctionName] URL: $Url" + Write-Verbose "[$FunctionName] GroupFormat: $GroupFormat" + Write-Verbose "[$FunctionName] GroupDescriptionFormat: $GroupDescriptionFormat" + Write-Verbose "[$FunctionName] CreateReadGroup: $CreateReadGroup" + Write-Verbose "[$FunctionName] CreateReadWriteGroup: $CreateReadWriteGroup" + Write-Verbose "[$FunctionName] ExcludeStandardShares: $ExcludeStandardShares" + + # Validate that at least one group type is selected + if (-not $CreateReadGroup -and -not $CreateReadWriteGroup) { + $msg = "At least one of CreateReadGroup or CreateReadWriteGroup must be specified" + Write-Error $msg + throw $msg + } + + # Initialize CCS connection once in begin block + try { + Write-Verbose "[$FunctionName] Initializing CCS Web Service connection" + $CCS = Initialize-CCS -Url $Url -WebServiceCredential $CCSCredential -ErrorAction Stop + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to initialize CCS Web Service: $_" ` + -ErrorCategory ConnectionError ` + -TargetObject $Url ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Verify the URL is correct and the CCS Web Service is accessible. Check credentials." + } + + # Prepare domain credentials if provided + $ADUsername = $null + $ADPassword = $null + + if ($DomainCredential) { + $ADUsername = $DomainCredential.UserName + Write-Verbose "[$FunctionName] Using domain credential: $ADUsername" + + try { + if ($PasswordIsEncrypted) { + $ADPassword = $DomainCredential.GetNetworkCredential().Password + Write-Verbose "[$FunctionName] Using pre-encrypted password" + } else { + $SecurePassword = ConvertTo-SecureString $DomainCredential.GetNetworkCredential().Password -AsPlainText -Force + $ADPassword = Get-CCSEncryptedPassword -SecureString $SecurePassword -ErrorAction Stop + Write-Verbose "[$FunctionName] Password encrypted successfully" + } + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to process domain credential password: $_" ` + -ErrorCategory SecurityError ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Ensure the domain credential is valid and the password can be encrypted." + } + } else { + Write-Verbose "[$FunctionName] No domain credential provided, using CCS Web Service context" + } + } + + process { + try { + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Processing: DomainOUPath=$DomainOUPath, Domain=$Domain, GroupFormat=$GroupFormat, CreateReadGroup=$CreateReadGroup, CreateReadWriteGroup=$CreateReadWriteGroup, ExcludeStandardShares=$ExcludeStandardShares") + } + + # ShouldProcess support + $WhatIfMessage = "Create security groups for shares in domain '$Domain' using format '$GroupFormat'" + if ($PSCmdlet.ShouldProcess($Domain, $WhatIfMessage)) { + Write-Verbose "[$FunctionName] Calling ActiveDirectory_CreateSecurityGroupsForShares" + + $Result = $CCS.ActiveDirectory_CreateSecurityGroupsForShares( + $DomainOUPath, + $Domain, + $ADUsername, + $ADPassword, + $GroupFormat, + $GroupDescriptionFormat, + $CreateReadGroup.IsPresent, + $CreateReadWriteGroup.IsPresent, + $ExcludeStandardShares.IsPresent + ) + Write-Debug "Result from CCS Web Service: $Result" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Result: $Result") + } + + Write-Verbose "[$FunctionName] Result: $Result" + + # Check for errors in result + $IsError = Invoke-CCSIsError -Result $Result + + Write-Debug "IsError evaluation for result: $IsError" + if ($IsError) { + # Determine appropriate error category based on result message + $ErrorCat = [System.Management.Automation.ErrorCategory]::OperationStopped + $RecommendedActionText = "Verify the domain and OU path are correct." + + if ($Result -like '*does not exist*' -or $Result -like '*not found*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::ObjectNotFound + $RecommendedActionText = "Verify that the domain and OU path exist in Active Directory." + } elseif ($Result -like '*unwilling to process*' -or $Result -like '*permission*' -or $Result -like '*access*denied*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::PermissionDenied + $RecommendedActionText = "Check that the domain credentials have sufficient permissions to create security groups." + } elseif ($Result -like '*already exists*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::ResourceExists + $RecommendedActionText = "One or more groups already exist. Consider using different group format." + } elseif ($Result -like '*invalid*format*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::InvalidArgument + $RecommendedActionText = "Check the GroupFormat and GroupDescriptionFormat parameters for valid placeholders." + } + + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + Write-Debug "ErrorAction from PSBoundParameters: $($PSBoundParameters['ErrorAction'])" + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + Write-Debug "ErrorAction from ErrorActionPreference: $ErrorActionPreference" + } + Write-Debug "ShouldThrow is set to: $ShouldThrow" + + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to create security groups for shares: $Result" ` + -ErrorCategory $ErrorCat ` + -TargetObject $Domain ` + -FunctionName $FunctionName ` + -RecommendedAction $RecommendedActionText ` + -Throw:$ShouldThrow + } else { + Write-Verbose "[$FunctionName] Successfully created security groups for shares" + Write-Output $Result + } + } else { + Write-Verbose "[$FunctionName] WhatIf: Would create security groups for shares in domain '$Domain'" + } + } catch { + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + } + + Invoke-CCSErrorHandling ` + -ErrorMessage "Exception occurred while creating security groups for shares: $_" ` + -ErrorCategory InvalidOperation ` + -TargetObject $Domain ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Check the error details and verify all parameters are correct." ` + -Throw:$ShouldThrow + } + } + + end { + Write-Verbose "[$FunctionName] Function execution completed" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Summary - Completed") + } + } +} diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Dependencies/README.md b/Modules/Capa.PowerShell.Module.CCS/Dev/Dependencies/README.md index 67c81d86..b0522f90 100644 --- a/Modules/Capa.PowerShell.Module.CCS/Dev/Dependencies/README.md +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Dependencies/README.md @@ -2,7 +2,7 @@ ## How to create the CCSProxy.dll -1. In browser ogen CCSWebservice path like `https://mracapa02.capainstaller.com/CCSWebservice/CCS.asmx?WSDL` +1. In browser open CCSWebservice path like `https://mracapa02.capainstaller.com/CCSWebservice/CCS.asmx?WSDL` 2. Save site as `CCS.wsdl` 3. Run `Developer Powershell for VS 2022` as administrator 4. cd to the directory where `CCS.wsdl` is located diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADComputerNames.Tests.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADComputerNames.Tests.ps1 index a29e386e..98a4c053 100644 --- a/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADComputerNames.Tests.ps1 +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADComputerNames.Tests.ps1 @@ -1,7 +1,176 @@ -$Splat = @{ - Domain = 'Firmax.local' - CCSCredential = $CCSCredential - Url = 'https://mracapa02.capainstaller.com/CCSWebservice/CCS.asmx' - DomainCredential = $DomainCredential + +BeforeAll { + $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent + + $Items = Get-ChildItem -Path "$RootPath\Capa.PowerShell.Module.CCS\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + + $script:TestUrl = "https://$(hostname).capainstaller.com/CCSWebservice/CCS.asmx" + $script:TestDomain = 'Firmax.local' + $CredentialPath = "D:\PowerShell\Credentials\$($env:USERNAME)DomainAdminPesterTests.xml" + + if ($env:DOMAINADMINUSERNAME -and $env:DOMAINADMINPASSWORD) { + $securePassword = ConvertTo-SecureString $env:DOMAINADMINPASSWORD -AsPlainText -Force + $script:TestDomainCredential = New-Object System.Management.Automation.PSCredential($env:DOMAINADMINUSERNAME, $securePassword) + $script:TestCCSCredential = $script:TestDomainCredential + } elseif (Test-Path -Path $CredentialPath) { + $script:TestDomainCredential = Import-Clixml -Path $CredentialPath + $script:TestCCSCredential = $script:TestDomainCredential + } else { + $script:TestDomainCredential = Get-Credential -Message 'Enter domain admin credentials for integration tests' + $script:TestCCSCredential = $script:TestDomainCredential + } + + $DebugPreference = 'Continue' + $ErrorActionPreference = 'Stop' +} + +Describe 'Get-CCSADComputerNames' -Tag 'Unit' { + + Context 'Parameter Validation' { + It 'Should have mandatory Domain parameter' { + (Get-Command Get-CCSADComputerNames).Parameters['Domain'].Attributes.Mandatory | Should -Be $true + } + It 'Should have mandatory Url parameter' { + (Get-Command Get-CCSADComputerNames).Parameters['Url'].Attributes.Mandatory | Should -Be $true + } + It 'Should have mandatory CCSCredential parameter' { + (Get-Command Get-CCSADComputerNames).Parameters['CCSCredential'].Attributes.Mandatory | Should -Be $true + } + It 'Should have optional DomainCredential parameter' { + (Get-Command Get-CCSADComputerNames).Parameters['DomainCredential'].Attributes.Mandatory | Should -Be $false + } + It 'Should have optional DomainOUPath parameter' { + (Get-Command Get-CCSADComputerNames).Parameters['DomainOUPath'].Attributes.Mandatory | Should -Be $false + } + } + + Context 'Parameter Aliases' { + It 'Should have alias "OU" for DomainOUPath' { + (Get-Command Get-CCSADComputerNames).Parameters['DomainOUPath'].Aliases | Should -Contain 'OU' + } + It 'Should have alias "Path" for DomainOUPath' { + (Get-Command Get-CCSADComputerNames).Parameters['DomainOUPath'].Aliases | Should -Contain 'Path' + } + It 'Should have alias "WebServiceUrl" for Url' { + (Get-Command Get-CCSADComputerNames).Parameters['Url'].Aliases | Should -Contain 'WebServiceUrl' + } + It 'Should have alias "Credential" for CCSCredential' { + (Get-Command Get-CCSADComputerNames).Parameters['CCSCredential'].Aliases | Should -Contain 'Credential' + } + } + + Context 'DomainOUPath Validation' { + It 'Should accept empty DomainOUPath' { + { Get-CCSADComputerNames -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath '' -WhatIf } | Should -Not -Throw + } + It 'Should accept standard DN format' { + { Get-CCSADComputerNames -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'OU=Computers,DC=Firmax,DC=local' -WhatIf } | Should -Not -Throw + } + It 'Should accept DC-only format' { + { Get-CCSADComputerNames -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'DC=Firmax,DC=local' -WhatIf } | Should -Not -Throw + } + It 'Should accept LDAP format' { + { Get-CCSADComputerNames -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'LDAP://DC01.Firmax.local/OU=Computers,DC=Firmax,DC=local' -WhatIf } | Should -Not -Throw + } + It 'Should reject invalid format' { + { Get-CCSADComputerNames -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'InvalidPath' -WhatIf } | Should -Throw + } + } + + Context 'URL Validation' { + It 'Should accept HTTPS URL' { + { Get-CCSADComputerNames -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Not -Throw + } + It 'Should reject HTTP URL (only HTTPS allowed)' { + $httpUrl = $script:TestUrl -replace '^https:', 'http:' + { Get-CCSADComputerNames -Domain $script:TestDomain -Url $httpUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Throw + } + It 'Should reject URL without protocol' { + { Get-CCSADComputerNames -Domain $script:TestDomain -Url 'test.com/CCS.asmx' -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Throw + } + } + + Context 'Domain Validation' { + It 'Should accept valid domain format' { + { Get-CCSADComputerNames -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Not -Throw + } + It 'Should accept subdomain format' { + { Get-CCSADComputerNames -Domain 'sub.firmax.local' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Not -Throw + } + It 'Should reject invalid domain format' { + { Get-CCSADComputerNames -Domain 'invalid_domain' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Throw + } + } + + Context 'ShouldProcess Support' { + It 'Should support WhatIf' { + (Get-Command Get-CCSADComputerNames).Parameters.ContainsKey('WhatIf') | Should -Be $true + } + It 'Should support Confirm' { + (Get-Command Get-CCSADComputerNames).Parameters.ContainsKey('Confirm') | Should -Be $true + } + } + + Context 'CmdletBinding Attributes' { + It 'Should be an advanced function' { + (Get-Command Get-CCSADComputerNames).CmdletBinding | Should -Be $true + } + It 'Should have SupportsShouldProcess' { + (Get-Command Get-CCSADComputerNames).Parameters['WhatIf'] | Should -Not -BeNullOrEmpty + } + It 'Should have OutputType defined' { + (Get-Command Get-CCSADComputerNames).OutputType.Name | Should -Contain 'System.String[]' + } + } +} + +Describe 'Get-CCSADComputerNames - Integration Tests' -Tag 'Integration' { + Context 'Live CCS Web Service Operations' { + BeforeAll { + $script:TestOU = 'OU=Computers,OU=AzureAD synk,DC=FirmaX,DC=Local' + } + + It 'Should connect to CCS Web Service successfully' { + { Get-CCSADComputerNames -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Not -Throw + } + + It 'Should get computer names with domain credentials' { + $result = Get-CCSADComputerNames -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Not -BeNullOrEmpty + } + + It 'Should get computer names for specific OU' { + $result = Get-CCSADComputerNames -Domain $script:TestDomain -DomainOUPath $script:TestOU -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Not -BeNullOrEmpty + } + + It 'Should convert LDAP path format' { + $ldapPath = 'LDAP://DC01.Firmax.local/OU=Computers,DC=Firmax,DC=local' + { Get-CCSADComputerNames -Domain $script:TestDomain -DomainOUPath $ldapPath -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + } + + Context 'Error Handling' { + It 'Should handle invalid credentials gracefully' { + $badCred = New-Object System.Management.Automation.PSCredential('baduser', (ConvertTo-SecureString 'badpass' -AsPlainText -Force)) + { Get-CCSADComputerNames -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $badCred -ErrorAction Stop } | Should -Throw + } + It 'Should handle non-existent URL gracefully' { + { Get-CCSADComputerNames -Domain $script:TestDomain -Url 'https://nonexistent.invalid/CCS.asmx' -CCSCredential $script:TestCCSCredential -ErrorAction Stop } | Should -Throw + } + } +} + +Describe 'Get-CCSADComputerNames - Performance Tests' -Tag 'Performance' { + Context 'Pipeline Performance' { + It 'Should process efficiently' { + $measure = Measure-Command { + Get-CCSADComputerNames -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf -WarningAction SilentlyContinue + } + $measure.TotalSeconds | Should -BeLessThan 5 + } + } } -$Result = Get-CCSADComputerNames @Splat \ No newline at end of file diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADComputerNames.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADComputerNames.ps1 index 4d057cb5..32721b44 100644 --- a/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADComputerNames.ps1 +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADComputerNames.ps1 @@ -1,76 +1,223 @@ -<# +function Get-CCSADComputerNames { + <# .SYNOPSIS - Gets the computer names from Active Directory using the CCS API. + Gets computer names from Active Directory using the CCS Web Service. .DESCRIPTION - Gets the computer names from Active Directory using the CCS API. + Gets computer names from Active Directory using the CCS Web Service. + This advanced function includes comprehensive error handling, input validation, and supports pipeline input. .PARAMETER DomainOUPath - The Organizational Unit (OU) path of the domain where the computers reside. + The Organizational Unit (OU) path of the domain where the computers reside. Supports both standard DN format (OU=Computers,DC=example,DC=com) and LDAP format (LDAP://DC01.example.local/OU=Computers,DC=example,DC=com). .PARAMETER Domain - The domain name where the computers reside. + The domain name where the computers reside. Must be a valid domain name format. .PARAMETER Url - The URL of the CCS API. + The URL of the CCS Web Service. Must be a valid URI format. Example: "https://example.com/CCSWebservice/CCS.asmx" .PARAMETER CCSCredential - The credentials for the CCS API. + The credentials for the CCS Web Service. .PARAMETER DomainCredential - The credentials for the domain where the computers reside. + The credentials for the domain where the computers reside. If not defined, it will run in the CCS Web Service context. .PARAMETER PasswordIsEncrypted Indicates whether the password is encrypted. Default is $false. .EXAMPLE - Get-CCSADComputerNames -DomainOUPath "OU=Test,DC=FirmaX,DC=local" -Domain "FirmaX.local" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $ccsCredential -DomainCredential $domainCredential -#> -function Get-CCSADComputerNames { + PS C:\> Get-CCSADComputerNames -DomainOUPath "OU=Test,DC=FirmaX,DC=local" -Domain "FirmaX.local" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $ccsCredential -DomainCredential $domainCredential + + .OUTPUTS + System.String[] + Returns an array of computer names from the CCS Web Service operation. + + .NOTES + This is an advanced function with support for ShouldProcess, pipeline input, and comprehensive error handling. + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')] + [OutputType([string[]])] param ( - [Parameter(Mandatory = $false)] - [string]$DomainOUPath, - [Parameter(Mandatory = $true)] + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] + [ValidateScript({ + if ([string]::IsNullOrEmpty($_)) { + return $true + } + # Support standard DN format: OU=...,DC=...,DC=... + if ($_ -match '^(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + # Support LDAP format: LDAP://server/DN + if ($_ -match '^LDAP://[^/]+/(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + throw "DomainOUPath must be in format 'OU=...,DC=...,DC=...' or 'LDAP://server/OU=...,DC=...,DC=...' or empty" + })] + [Alias('OU', 'Path')] + [string]$DomainOUPath = '', + + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, HelpMessage = 'Enter the domain name')] + [ValidateNotNullOrEmpty()] + [ValidatePattern('^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$')] [string]$Domain, - [Parameter(Mandatory = $true)] + + [Parameter(Mandatory = $true, HelpMessage = 'Enter the CCS Web Service URL')] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + if ($_ -match '^https://') { + $true + } else { + throw "URL must start with https:// (secure connection required)" + } + })] + [Alias('WebServiceUrl', 'Uri')] [string]$Url, - [Parameter(Mandatory = $true)] + + [Parameter(Mandatory = $true, HelpMessage = 'Enter the CCS Web Service credentials')] + [ValidateNotNull()] + [Alias('Credential', 'WebServiceCredential')] [pscredential]$CCSCredential, + [Parameter(Mandatory = $false)] + [ValidateNotNull()] + [Alias('ADCredential')] [pscredential]$DomainCredential, + [Parameter(Mandatory = $false)] - [bool]$PasswordIsEncrypted = $false + [Alias('Encrypted')] + [switch]$PasswordIsEncrypted ) - $FunctionName = 'Add-CCSADUserToSecurityGroup' - if ($Global:Cs) { - Job_WriteLog -Text "$FunctionName DomainOUPath: $DomainOUPath, Domain: $Domain, Url: $Url, PasswordIsEncrypted: $PasswordIsEncrypted" - } + begin { + $FunctionName = $PSCmdlet.MyInvocation.MyCommand.Name - $CCS = Initialize-CCS -Url $Url -WebServiceCredential $CCSCredential - $ADPassword = $null + Write-Verbose "[$FunctionName] Starting function execution" + Write-Verbose "[$FunctionName] Domain: $Domain" + Write-Verbose "[$FunctionName] DomainOUPath: $DomainOUPath" + Write-Verbose "[$FunctionName] URL: $Url" - if ($DomainCredential -and $PasswordIsEncrypted) { - $ADPassword = $DomainCredential.GetNetworkCredential().Password - } elseif ($DomainCredential -and -not $PasswordIsEncrypted) { - $ADPassword = Get-CCSEncryptedPassword -String $DomainCredential.GetNetworkCredential().Password - } + # Initialize CCS connection once in begin block + try { + Write-Verbose "[$FunctionName] Initializing CCS Web Service connection" + $CCS = Initialize-CCS -Url $Url -WebServiceCredential $CCSCredential -ErrorAction Stop + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to initialize CCS Web Service: $_" ` + -ErrorCategory ConnectionError ` + -TargetObject $Url ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Verify the URL is correct and the CCS Web Service is accessible. Check credentials." + } - $Result = $CCS.ActiveDirectory_GetComputerNames( - $DomainOUPath, - $Domain, - $DomainCredential.UserName, - $ADPassword - ) + # Prepare domain credentials if provided + $ADUsername = $null + $ADPassword = $null - if ($Global:Cs) { - Job_WriteLog -Text "$FunctionName Result: $Result" - } + if ($DomainCredential) { + $ADUsername = $DomainCredential.UserName + Write-Verbose "[$FunctionName] Using domain credential: $ADUsername" + + try { + if ($PasswordIsEncrypted) { + $ADPassword = $DomainCredential.GetNetworkCredential().Password + Write-Verbose "[$FunctionName] Using pre-encrypted password" + } else { + $ADPassword = Get-CCSEncryptedPassword -String $DomainCredential.GetNetworkCredential().Password -ErrorAction Stop + Write-Verbose "[$FunctionName] Password encrypted successfully" + } + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to process domain credential password: $_" ` + -ErrorCategory SecurityError ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Ensure the domain credential is valid and the password can be encrypted." + } + } else { + Write-Verbose "[$FunctionName] No domain credential provided, using CCS Web Service context" + } - $Throw = Invoke-CCSIsError -Result $Result - if ($Throw) { - throw "$FunctionName $Result" } - return $Result + process { + # ShouldProcess support + $WhatIfMessage = "Get computer names from domain '$Domain'" + if ($PSCmdlet.ShouldProcess($Domain, $WhatIfMessage)) { + try { + Write-Verbose "[$FunctionName] Calling ActiveDirectory_GetComputerNames" + + $Result = $CCS.ActiveDirectory_GetComputerNames( + $DomainOUPath, + $Domain, + $ADUsername, + $ADPassword + ) + Write-Debug "Result from CCS Web Service: $Result" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Result: $Result") + } + + # Check for errors in result + $IsError = Invoke-CCSIsError -Result $Result + + Write-Debug "IsError evaluation for result: $IsError" + if ($IsError) { + # Determine appropriate error category based on result message + $ErrorCat = [System.Management.Automation.ErrorCategory]::OperationStopped + $RecommendedActionText = "Verify the OU, domain, and credentials are correct." + + if ($Result -like '*does not exist*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::ObjectNotFound + $RecommendedActionText = "Verify that the OU and domain exist." + } elseif ($Result -like '*unwilling to process*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::PermissionDenied + $RecommendedActionText = "Check that the domain credentials have sufficient permissions." + } + + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + Write-Debug "ErrorAction from PSBoundParameters: $($PSBoundParameters['ErrorAction'])" + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + Write-Debug "ErrorAction from ErrorActionPreference: $ErrorActionPreference" + } + Write-Debug "ShouldThrow is set to: $ShouldThrow" + + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to get computer names: $Result" ` + -ErrorCategory $ErrorCat ` + -TargetObject $Domain ` + -FunctionName $FunctionName ` + -RecommendedAction $RecommendedActionText ` + -Throw:$ShouldThrow + } else { + Write-Verbose "[$FunctionName] Successfully retrieved computer names" + return $Result + } + } catch { + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + } + + Invoke-CCSErrorHandling ` + -ErrorMessage "Exception occurred while retrieving computer names: $_" ` + -ErrorCategory InvalidOperation ` + -TargetObject $Domain ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Check the error details and verify all parameters are correct." ` + -Throw:$ShouldThrow + } + } else { + Write-Verbose "[$FunctionName] WhatIf: Would get computer names from $Domain" + } + } } \ No newline at end of file diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADComputerOU.Tests.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADComputerOU.Tests.ps1 new file mode 100644 index 00000000..ef469ec9 --- /dev/null +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADComputerOU.Tests.ps1 @@ -0,0 +1,212 @@ +BeforeAll { + $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent + + $Items = Get-ChildItem -Path "$RootPath\Capa.PowerShell.Module.CCS\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + + # Setup test environment + $script:TestUrl = "https://$(hostname).capainstaller.com/CCSWebservice/CCS.asmx" + $script:TestDomain = 'Firmax.local' + $CredentialPath = "D:\PowerShell\Credentials\$($env:USERNAME)DomainAdminPesterTests.xml" + + # Setup credentials from environment variables (GitHub secrets) + if ($env:DOMAINADMINUSERNAME -and $env:DOMAINADMINPASSWORD) { + $securePassword = ConvertTo-SecureString $env:DOMAINADMINPASSWORD -AsPlainText -Force + $script:TestDomainCredential = New-Object System.Management.Automation.PSCredential($env:DOMAINADMINUSERNAME, $securePassword) + $script:TestCCSCredential = $script:TestDomainCredential + } elseif (Test-Path -Path $CredentialPath) { + $script:TestDomainCredential = Import-Clixml -Path $CredentialPath + $script:TestCCSCredential = $script:TestDomainCredential + } else { + $script:TestDomainCredential = Get-Credential -Message 'Enter domain admin credentials for integration tests' + $script:TestCCSCredential = $script:TestDomainCredential + } + + $DebugPreference = 'Continue' + $ErrorActionPreference = 'Stop' +} + +Describe 'Get-CCSADComputerOU' -Tag 'Unit' { + + Context 'Parameter Validation' { + + It 'Should have mandatory ComputerName parameter' { + (Get-Command Get-CCSADComputerOU).Parameters['ComputerName'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory Domain parameter' { + (Get-Command Get-CCSADComputerOU).Parameters['Domain'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory Url parameter' { + (Get-Command Get-CCSADComputerOU).Parameters['Url'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory CCSCredential parameter' { + (Get-Command Get-CCSADComputerOU).Parameters['CCSCredential'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have optional DomainCredential parameter' { + (Get-Command Get-CCSADComputerOU).Parameters['DomainCredential'].Attributes.Mandatory | Should -Be $false + } + + It 'Should have optional DomainOUPath parameter' { + (Get-Command Get-CCSADComputerOU).Parameters['DomainOUPath'].Attributes.Mandatory | Should -Be $false + } + + It 'Should accept array of computer names' { + (Get-Command Get-CCSADComputerOU).Parameters['ComputerName'].ParameterType.Name | Should -Be 'String[]' + } + + It 'Should accept pipeline input for ComputerName' { + (Get-Command Get-CCSADComputerOU).Parameters['ComputerName'].Attributes.ValueFromPipeline | Should -Be $true + } + } + + Context 'Parameter Aliases' { + + It 'Should have alias "Name" for ComputerName' { + (Get-Command Get-CCSADComputerOU).Parameters['ComputerName'].Aliases | Should -Contain 'Name' + } + + It 'Should have alias "Computer" for ComputerName' { + (Get-Command Get-CCSADComputerOU).Parameters['ComputerName'].Aliases | Should -Contain 'Computer' + } + + It 'Should have alias "OU" for DomainOUPath' { + (Get-Command Get-CCSADComputerOU).Parameters['DomainOUPath'].Aliases | Should -Contain 'OU' + } + + It 'Should have alias "SearchBase" for DomainOUPath' { + (Get-Command Get-CCSADComputerOU).Parameters['DomainOUPath'].Aliases | Should -Contain 'SearchBase' + } + } + + Context 'CmdletBinding Attributes' { + + It 'Should be an advanced function' { + (Get-Command Get-CCSADComputerOU).CmdletBinding | Should -Be $true + } + + It 'Should have OutputType defined' { + (Get-Command Get-CCSADComputerOU).OutputType.Name | Should -Contain 'System.String' + } + } +} + +Describe 'Get-CCSADComputerOU - Integration Tests' -Tag 'Integration' { + + Context 'DomainOUPath Validation' { + + It 'Should accept empty DomainOUPath' { + { Get-CCSADComputerOU -ComputerName $env:COMPUTERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath '' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept standard DN format' { + { Get-CCSADComputerOU -ComputerName $env:COMPUTERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'OU=Computers,DC=Firmax,DC=local' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept DC-only format' { + { Get-CCSADComputerOU -ComputerName $env:COMPUTERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'DC=Firmax,DC=local' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept LDAP format' { + { Get-CCSADComputerOU -ComputerName $env:COMPUTERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'LDAP://DC01.Firmax.local/OU=Computers,DC=Firmax,DC=local' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should reject invalid format' { + { Get-CCSADComputerOU -ComputerName $env:COMPUTERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'InvalidPath' } | Should -Throw + } + } + + Context 'URL Validation' { + + It 'Should accept HTTPS URL' { + { Get-CCSADComputerOU -ComputerName $env:COMPUTERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should reject HTTP URL (only HTTPS allowed)' { + $httpUrl = $script:TestUrl -replace '^https:', 'http:' + { Get-CCSADComputerOU -ComputerName $env:COMPUTERNAME -Domain $script:TestDomain -Url $httpUrl -CCSCredential $script:TestCCSCredential } | Should -Throw + } + + It 'Should reject URL without protocol' { + { Get-CCSADComputerOU -ComputerName $env:COMPUTERNAME -Domain $script:TestDomain -Url 'test.com/CCS.asmx' -CCSCredential $script:TestCCSCredential } | Should -Throw + } + } + + Context 'Domain Validation' { + + It 'Should accept valid domain format' { + { Get-CCSADComputerOU -ComputerName $env:COMPUTERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept subdomain format' { + { Get-CCSADComputerOU -ComputerName $env:COMPUTERNAME -Domain 'sub.firmax.local' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should reject invalid domain format' { + { Get-CCSADComputerOU -ComputerName $env:COMPUTERNAME -Domain 'invalid_domain' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential } | Should -Throw + } + } + + Context 'Live CCS Web Service Operations' { + + It 'Should connect to CCS Web Service successfully' { + { Get-CCSADComputerOU -ComputerName $env:COMPUTERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should retrieve OU path for local computer' { + $result = Get-CCSADComputerOU -ComputerName $env:COMPUTERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Not -BeNullOrEmpty + # Should match either LDAP format or standard DN format + $result | Should -Match '(^LDAP://.*|(^(OU=.*,)?(DC=.+)(,DC=.+)*$))' + } + + It 'Should retrieve OU path with domain credentials' { + $result = Get-CCSADComputerOU -ComputerName $env:COMPUTERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Not -BeNullOrEmpty + } + + It 'Should process multiple computers via pipeline' { + $computers = @($env:COMPUTERNAME) + $results = $computers | Get-CCSADComputerOU -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $results | Should -Not -BeNullOrEmpty + $results.Count | Should -Be $computers.Count + } + + It 'Should handle LDAP format DomainOUPath' { + $ldapPath = 'LDAP://DC01.Firmax.local/OU=Computers,DC=Firmax,DC=local' + { Get-CCSADComputerOU -ComputerName $env:COMPUTERNAME -DomainOUPath $ldapPath -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + } + + Context 'Error Handling' { + + It 'Should handle non-existent computer gracefully' { + $result = Get-CCSADComputerOU -ComputerName 'NonExistentPC999' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -BeNullOrEmpty + } + + It 'Should handle invalid credentials gracefully' { + $badCred = New-Object System.Management.Automation.PSCredential('baduser', (ConvertTo-SecureString 'badpass' -AsPlainText -Force)) + { Get-CCSADComputerOU -ComputerName $env:COMPUTERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $badCred -ErrorAction Stop } | Should -Throw + } + } +} + +Describe 'Get-CCSADComputerOU - Performance Tests' -Tag 'Performance' { + + Context 'Pipeline Performance' { + + It 'Should process pipeline input efficiently' { + $computers = @($env:COMPUTERNAME, $env:COMPUTERNAME, $env:COMPUTERNAME) + $measure = Measure-Command { + $results = $computers | Get-CCSADComputerOU -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + } + $measure.TotalSeconds | Should -BeLessThan 10 + } + } +} diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADComputerOU.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADComputerOU.ps1 new file mode 100644 index 00000000..54339fb6 --- /dev/null +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADComputerOU.ps1 @@ -0,0 +1,279 @@ +function Get-CCSADComputerOU { + <# + .SYNOPSIS + Gets the Organizational Unit (OU) path of a computer in Active Directory using the CCS Web Service. + + .DESCRIPTION + Retrieves the OU path where a computer resides in Active Directory using the CCS Web Service. + This advanced function includes comprehensive error handling, input validation, and supports pipeline input. + + .PARAMETER ComputerName + The name of the computer to retrieve the OU path for. + Supports pipeline input and accepts multiple computer names. + + .PARAMETER DomainOUPath + The Organizational Unit (OU) path to search within. + If not specified, searches the entire domain. + Supports both standard DN format (OU=Computers,DC=example,DC=com) and LDAP format (LDAP://DC01.example.local/OU=Computers,DC=example,DC=com). + + .PARAMETER Domain + The domain in which the computer resides. + Must be a valid domain name format. + + .PARAMETER Url + The URL of the CCS Web Service. + Must be a valid URI format. Example: "https://example.com/CCSWebservice/CCS.asmx" + + .PARAMETER CCSCredential + The credentials used to authenticate with the CCS Web Service. + + .PARAMETER DomainCredential + The credentials of an account with permissions to query Active Directory. + If not defined, it will run in the CCS Web Service context. + + .PARAMETER PasswordIsEncrypted + Indicates if the password in the DomainCredential is encrypted. Default is $false. + + .EXAMPLE + PS C:\> Get-CCSADComputerOU -ComputerName "TestPC" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Gets the OU path for TestPC using default CCS context. + + .EXAMPLE + PS C:\> Get-CCSADComputerOU -ComputerName "TestPC" -DomainOUPath "OU=Computers,DC=example,DC=com" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential -DomainCredential $DomainCredential + + Gets the OU path for TestPC using specific domain credentials and limiting search to specific OU. + + .EXAMPLE + PS C:\> "PC01", "PC02", "PC03" | Get-CCSADComputerOU -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Gets the OU paths for multiple computers using pipeline input. + + .EXAMPLE + PS C:\> Get-CCSADComputerOU -ComputerName "TestPC" -DomainOUPath "LDAP://DC01.Firmax.local/DC=FirmaX,DC=local" -Domain "Firmax.local" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Gets the OU path for TestPC using LDAP format for the search path. The LDAP path will be automatically converted to standard DN format. + + .OUTPUTS + System.String + Returns the OU path where the computer resides in Distinguished Name format. + + .NOTES + This is an advanced function with support for pipeline input and comprehensive error handling. + #> + [CmdletBinding()] + [OutputType([string])] + param ( + [Parameter( + Mandatory = $true, + Position = 0, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the computer name to retrieve the OU path for' + )] + [ValidateNotNullOrEmpty()] + [Alias('Name', 'Computer', 'CN')] + [string[]]$ComputerName, + + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] + [ValidateScript({ + if ([string]::IsNullOrEmpty($_)) { + return $true + } + # Support standard DN format: OU=...,DC=...,DC=... + if ($_ -match '^(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + # Support LDAP format: LDAP://server/DN + if ($_ -match '^LDAP://[^/]+/(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + throw "DomainOUPath must be in format 'OU=...,DC=...,DC=...' or 'LDAP://server/OU=...,DC=...,DC=...' or empty" + })] + [Alias('OU', 'Path', 'SearchBase')] + [string]$DomainOUPath = '', + + [Parameter( + Mandatory = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the domain name' + )] + [ValidateNotNullOrEmpty()] + [ValidatePattern('^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$')] + [string]$Domain, + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service URL' + )] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + if ($_ -match '^https://') { + $true + } else { + throw "URL must start with https:// (secure connection required)" + } + })] + [Alias('WebServiceUrl', 'Uri')] + [string]$Url, + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service credentials' + )] + [ValidateNotNull()] + [Alias('Credential', 'WebServiceCredential')] + [pscredential]$CCSCredential, + + [Parameter(Mandatory = $false)] + [ValidateNotNull()] + [Alias('ADCredential')] + [pscredential]$DomainCredential, + + [Parameter(Mandatory = $false)] + [Alias('Encrypted')] + [switch]$PasswordIsEncrypted + ) + + begin { + $FunctionName = $PSCmdlet.MyInvocation.MyCommand.Name + + Write-Verbose "[$FunctionName] Starting function execution" + Write-Verbose "[$FunctionName] Domain: $Domain" + Write-Verbose "[$FunctionName] DomainOUPath: $DomainOUPath" + Write-Verbose "[$FunctionName] URL: $Url" + + # Initialize CCS connection once in begin block + try { + Write-Verbose "[$FunctionName] Initializing CCS Web Service connection" + $CCS = Initialize-CCS -Url $Url -WebServiceCredential $CCSCredential -ErrorAction Stop + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to initialize CCS Web Service: $_" ` + -ErrorCategory ConnectionError ` + -TargetObject $Url ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Verify the URL is correct and the CCS Web Service is accessible. Check credentials." + } + + # Prepare domain credentials if provided + $ADUsername = $null + $ADPassword = $null + + if ($DomainCredential) { + $ADUsername = $DomainCredential.UserName + Write-Verbose "[$FunctionName] Using domain credential: $ADUsername" + + try { + if ($PasswordIsEncrypted) { + $ADPassword = $DomainCredential.GetNetworkCredential().Password + Write-Verbose "[$FunctionName] Using pre-encrypted password" + } else { + $ADPassword = Get-CCSEncryptedPassword -SecureString $DomainCredential.Password -ErrorAction Stop + Write-Verbose "[$FunctionName] Password encrypted successfully" + } + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to process domain credential password: $_" ` + -ErrorCategory SecurityError ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Ensure the domain credential is valid and the password can be encrypted." + } + } else { + Write-Verbose "[$FunctionName] No domain credential provided, using CCS Web Service context" + } + + $ProcessedCount = 0 + $SuccessCount = 0 + $FailureCount = 0 + } + + process { + foreach ($Computer in $ComputerName) { + $ProcessedCount++ + + try { + Write-Verbose "[$FunctionName] Processing computer: $Computer ($ProcessedCount)" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Processing: ComputerName=$Computer, DomainOUPath=$DomainOUPath, Domain=$Domain") + } + + Write-Verbose "[$FunctionName] Calling ActiveDirectory_GetComputerOU for $Computer" + + $Result = $CCS.ActiveDirectory_GetComputerOU( + $Computer, + $DomainOUPath, + $Domain, + $ADUsername, + $ADPassword + ) + + Write-Debug "[$FunctionName] Result from CCS Web Service: $Result" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Result for $Computer : $Result") + } + + Write-Verbose "[$FunctionName] OU path for $Computer : $Result" + + # Check for errors in result + if (Invoke-CCSIsError -Result $Result) { + $FailureCount++ + + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + Write-Debug "[$FunctionName] ErrorAction from PSBoundParameters: $($PSBoundParameters['ErrorAction'])" + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + Write-Debug "[$FunctionName] ErrorAction from preference: $ErrorActionPreference" + } + Write-Debug "[$FunctionName] ShouldThrow is set to: $ShouldThrow" + + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to get OU path for computer '$Computer': $Result" ` + -ErrorCategory ObjectNotFound ` + -TargetObject $Computer ` + -FunctionName $FunctionName ` + -RecommendedAction "Verify the computer name is correct and exists in Active Directory. Check domain credentials if provided." ` + -Throw:$ShouldThrow + } else { + $SuccessCount++ + Write-Output $Result + } + } catch { + $FailureCount++ + + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + } + + Invoke-CCSErrorHandling ` + -ErrorMessage "Exception occurred while processing computer '$Computer': $_" ` + -ErrorCategory InvalidOperation ` + -TargetObject $Computer ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Check the error details and verify all parameters are correct." ` + -Throw:$ShouldThrow + } + } + } + + end { + Write-Verbose "[$FunctionName] Function execution completed" + Write-Verbose "[$FunctionName] Total processed: $ProcessedCount | Success: $SuccessCount | Failed: $FailureCount" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Summary - Processed: $ProcessedCount, Success: $SuccessCount, Failed: $FailureCount") + } + } +} diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADCustomAttributeForUser.Tests.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADCustomAttributeForUser.Tests.ps1 new file mode 100644 index 00000000..cdb9e8da --- /dev/null +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADCustomAttributeForUser.Tests.ps1 @@ -0,0 +1,218 @@ +BeforeAll { + $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent + + $Items = Get-ChildItem -Path "$RootPath\Capa.PowerShell.Module.CCS\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + + # Setup test environment + $script:TestUrl = "https://$(hostname).capainstaller.com/CCSWebservice/CCS.asmx" + $script:TestDomain = 'Firmax.local' + $CredentialPath = "D:\PowerShell\Credentials\$($env:USERNAME)DomainAdminPesterTests.xml" + + # Setup credentials from environment variables (GitHub secrets) + if ($env:DOMAINADMINUSERNAME -and $env:DOMAINADMINPASSWORD) { + $securePassword = ConvertTo-SecureString $env:DOMAINADMINPASSWORD -AsPlainText -Force + $script:TestDomainCredential = New-Object System.Management.Automation.PSCredential($env:DOMAINADMINUSERNAME, $securePassword) + $script:TestCCSCredential = $script:TestDomainCredential + } elseif (Test-Path -Path $CredentialPath) { + $script:TestDomainCredential = Import-Clixml -Path $CredentialPath + $script:TestCCSCredential = $script:TestDomainCredential + } else { + $script:TestDomainCredential = Get-Credential -Message 'Enter domain admin credentials for integration tests' + $script:TestCCSCredential = $script:TestDomainCredential + } + + $DebugPreference = 'Continue' + $ErrorActionPreference = 'Stop' +} + +Describe 'Get-CCSADCustomAttributeForUser' -Tag 'Unit' { + + Context 'Parameter Validation' { + + It 'Should have mandatory UserName parameter' { + (Get-Command Get-CCSADCustomAttributeForUser).Parameters['UserName'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory Attribute parameter' { + (Get-Command Get-CCSADCustomAttributeForUser).Parameters['Attribute'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory Domain parameter' { + (Get-Command Get-CCSADCustomAttributeForUser).Parameters['Domain'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory Url parameter' { + (Get-Command Get-CCSADCustomAttributeForUser).Parameters['Url'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory CCSCredential parameter' { + (Get-Command Get-CCSADCustomAttributeForUser).Parameters['CCSCredential'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have optional DomainCredential parameter' { + (Get-Command Get-CCSADCustomAttributeForUser).Parameters['DomainCredential'].Attributes.Mandatory | Should -Be $false + } + + It 'Should have optional DomainOUPath parameter' { + (Get-Command Get-CCSADCustomAttributeForUser).Parameters['DomainOUPath'].Attributes.Mandatory | Should -Be $false + } + + It 'Should accept array of usernames' { + (Get-Command Get-CCSADCustomAttributeForUser).Parameters['UserName'].ParameterType.Name | Should -Be 'String[]' + } + + It 'Should accept pipeline input for UserName' { + (Get-Command Get-CCSADCustomAttributeForUser).Parameters['UserName'].Attributes.ValueFromPipeline | Should -Be $true + } + } + + Context 'Parameter Aliases' { + + It 'Should have alias "User" for UserName' { + (Get-Command Get-CCSADCustomAttributeForUser).Parameters['UserName'].Aliases | Should -Contain 'User' + } + + It 'Should have alias "SamAccountName" for UserName' { + (Get-Command Get-CCSADCustomAttributeForUser).Parameters['UserName'].Aliases | Should -Contain 'SamAccountName' + } + + It 'Should have alias "AttributeName" for Attribute' { + (Get-Command Get-CCSADCustomAttributeForUser).Parameters['Attribute'].Aliases | Should -Contain 'AttributeName' + } + + It 'Should have alias "OU" for DomainOUPath' { + (Get-Command Get-CCSADCustomAttributeForUser).Parameters['DomainOUPath'].Aliases | Should -Contain 'OU' + } + + It 'Should have alias "SearchBase" for DomainOUPath' { + (Get-Command Get-CCSADCustomAttributeForUser).Parameters['DomainOUPath'].Aliases | Should -Contain 'SearchBase' + } + } + + Context 'CmdletBinding Attributes' { + + It 'Should be an advanced function' { + (Get-Command Get-CCSADCustomAttributeForUser).CmdletBinding | Should -Be $true + } + + It 'Should have OutputType defined' { + (Get-Command Get-CCSADCustomAttributeForUser).OutputType.Name | Should -Contain 'System.String' + } + } +} + +Describe 'Get-CCSADCustomAttributeForUser - Integration Tests' -Tag 'Integration' { + + Context 'DomainOUPath Validation' { + + It 'Should accept empty DomainOUPath' { + { Get-CCSADCustomAttributeForUser -UserName $env:USERNAME -Attribute 'department' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath '' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept standard DN format' { + { Get-CCSADCustomAttributeForUser -UserName $env:USERNAME -Attribute 'department' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'OU=Users,DC=Firmax,DC=local' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept DC-only format' { + { Get-CCSADCustomAttributeForUser -UserName $env:USERNAME -Attribute 'department' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'DC=Firmax,DC=local' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept LDAP format' { + { Get-CCSADCustomAttributeForUser -UserName $env:USERNAME -Attribute 'department' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'LDAP://DC01.Firmax.local/OU=Users,DC=Firmax,DC=local' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should reject invalid format' { + { Get-CCSADCustomAttributeForUser -UserName $env:USERNAME -Attribute 'department' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'InvalidPath' } | Should -Throw + } + } + + Context 'URL Validation' { + + It 'Should accept HTTPS URL' { + { Get-CCSADCustomAttributeForUser -UserName $env:USERNAME -Attribute 'department' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should reject HTTP URL (only HTTPS allowed)' { + $httpUrl = $script:TestUrl -replace '^https:', 'http:' + { Get-CCSADCustomAttributeForUser -UserName $env:USERNAME -Attribute 'department' -Domain $script:TestDomain -Url $httpUrl -CCSCredential $script:TestCCSCredential } | Should -Throw + } + + It 'Should reject URL without protocol' { + { Get-CCSADCustomAttributeForUser -UserName $env:USERNAME -Attribute 'department' -Domain $script:TestDomain -Url 'test.com/CCS.asmx' -CCSCredential $script:TestCCSCredential } | Should -Throw + } + } + + Context 'Domain Validation' { + + It 'Should accept valid domain format' { + { Get-CCSADCustomAttributeForUser -UserName $env:USERNAME -Attribute 'department' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept subdomain format' { + { Get-CCSADCustomAttributeForUser -UserName $env:USERNAME -Attribute 'department' -Domain 'sub.firmax.local' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should reject invalid domain format' { + { Get-CCSADCustomAttributeForUser -UserName $env:USERNAME -Attribute 'department' -Domain 'invalid_domain' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential } | Should -Throw + } + } + + Context 'Live CCS Web Service Operations' { + + It 'Should connect to CCS Web Service successfully' { + { Get-CCSADCustomAttributeForUser -UserName $env:USERNAME -Attribute 'department' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should retrieve attribute for current user' { + $result = Get-CCSADCustomAttributeForUser -UserName $env:USERNAME -Attribute 'sAMAccountName' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Not -BeNullOrEmpty + } + + It 'Should retrieve attribute with domain credentials' { + $result = Get-CCSADCustomAttributeForUser -UserName $env:USERNAME -Attribute 'sAMAccountName' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Not -BeNullOrEmpty + } + + It 'Should process multiple users via pipeline' { + $users = @($env:USERNAME) + $results = $users | Get-CCSADCustomAttributeForUser -Attribute 'name' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $results | Should -Not -BeNullOrEmpty + $results.Count | Should -Be $users.Count + } + + It 'Should handle LDAP format DomainOUPath' { + $ldapPath = 'LDAP://DC01.Firmax.local/OU=Users,DC=Firmax,DC=local' + { Get-CCSADCustomAttributeForUser -UserName $env:USERNAME -Attribute 'department' -DomainOUPath $ldapPath -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + } + + Context 'Error Handling' { + + It 'Should handle non-existent user gracefully' { + $result = Get-CCSADCustomAttributeForUser -UserName 'NonExistentUser999' -Attribute 'department' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -BeNullOrEmpty + } + + It 'Should handle invalid credentials gracefully' { + $badCred = New-Object System.Management.Automation.PSCredential('baduser', (ConvertTo-SecureString 'badpass' -AsPlainText -Force)) + { Get-CCSADCustomAttributeForUser -UserName $env:USERNAME -Attribute 'department' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $badCred -ErrorAction Stop } | Should -Throw + } + } +} + +Describe 'Get-CCSADCustomAttributeForUser - Performance Tests' -Tag 'Performance' { + + Context 'Pipeline Performance' { + + It 'Should process pipeline input efficiently' { + $users = @($env:USERNAME, $env:USERNAME, $env:USERNAME) + $measure = Measure-Command { + $results = $users | Get-CCSADCustomAttributeForUser -Attribute 'department' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + } + $measure.TotalSeconds | Should -BeLessThan 10 + } + } +} diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADCustomAttributeForUser.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADCustomAttributeForUser.ps1 new file mode 100644 index 00000000..a535b339 --- /dev/null +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADCustomAttributeForUser.ps1 @@ -0,0 +1,294 @@ +function Get-CCSADCustomAttributeForUser { + <# + .SYNOPSIS + Gets a custom attribute value for a user from Active Directory using the CCS Web Service. + + .DESCRIPTION + Retrieves a custom attribute value for a user from Active Directory using the CCS Web Service. + This advanced function includes comprehensive error handling, input validation, and supports pipeline input. + + .PARAMETER UserName + The username of the user to retrieve the attribute for. + Supports pipeline input and accepts multiple usernames. + + .PARAMETER Attribute + The name of the custom attribute to retrieve. + + .PARAMETER DomainOUPath + The Organizational Unit (OU) path in which the user resides. + If not specified, searches the entire domain. + Supports both standard DN format (OU=Users,DC=example,DC=com) and LDAP format (LDAP://DC01.example.local/OU=Users,DC=example,DC=com). + + .PARAMETER Domain + The domain in which the user resides. + Must be a valid domain name format. + + .PARAMETER Url + The URL of the CCS Web Service. + Must be a valid URI format. Example: "https://example.com/CCSWebservice/CCS.asmx" + + .PARAMETER CCSCredential + The credentials used to authenticate with the CCS Web Service. + + .PARAMETER DomainCredential + The credentials of an account with permissions to query Active Directory. + If not defined, it will run in the CCS Web Service context. + + .PARAMETER PasswordIsEncrypted + Indicates if the password in the DomainCredential is encrypted. Default is $false. + + .EXAMPLE + PS C:\> Get-CCSADCustomAttributeForUser -UserName "jdoe" -Attribute "department" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Gets the department attribute for user jdoe using default CCS context. + + .EXAMPLE + PS C:\> Get-CCSADCustomAttributeForUser -UserName "jdoe" -Attribute "extensionAttribute1" -DomainOUPath "OU=Users,DC=example,DC=com" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential -DomainCredential $DomainCredential + + Gets a custom extension attribute for user jdoe using specific domain credentials and OU path. + + .EXAMPLE + PS C:\> "jdoe", "asmith", "bjones" | Get-CCSADCustomAttributeForUser -Attribute "employeeID" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Gets the employeeID attribute for multiple users using pipeline input. + + .EXAMPLE + PS C:\> Get-CCSADCustomAttributeForUser -UserName "jdoe" -Attribute "title" -DomainOUPath "LDAP://DC01.Firmax.local/OU=Users,DC=FirmaX,DC=local" -Domain "Firmax.local" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Gets the title attribute for user jdoe using LDAP format for the OU path. The LDAP path will be automatically converted to standard DN format. + + .OUTPUTS + System.String + Returns the value of the specified custom attribute. + + .NOTES + This is an advanced function with support for pipeline input and comprehensive error handling. + #> + [CmdletBinding()] + [OutputType([string])] + param ( + [Parameter( + Mandatory = $true, + Position = 0, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the username to retrieve the attribute for' + )] + [ValidateNotNullOrEmpty()] + [Alias('User', 'SamAccountName', 'Identity')] + [string[]]$UserName, + + [Parameter( + Mandatory = $true, + Position = 1, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the attribute name to retrieve' + )] + [ValidateNotNullOrEmpty()] + [Alias('AttributeName', 'Property')] + [string]$Attribute, + + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] + [ValidateScript({ + if ([string]::IsNullOrEmpty($_)) { + return $true + } + # Support standard DN format: OU=...,DC=...,DC=... + if ($_ -match '^(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + # Support LDAP format: LDAP://server/DN + if ($_ -match '^LDAP://[^/]+/(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + throw "DomainOUPath must be in format 'OU=...,DC=...,DC=...' or 'LDAP://server/OU=...,DC=...,DC=...' or empty" + })] + [Alias('OU', 'Path', 'SearchBase')] + [string]$DomainOUPath = '', + + [Parameter( + Mandatory = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the domain name' + )] + [ValidateNotNullOrEmpty()] + [ValidatePattern('^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$')] + [string]$Domain, + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service URL' + )] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + if ($_ -match '^https://') { + $true + } else { + throw "URL must start with https:// (secure connection required)" + } + })] + [Alias('WebServiceUrl', 'Uri')] + [string]$Url, + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service credentials' + )] + [ValidateNotNull()] + [Alias('Credential', 'WebServiceCredential')] + [pscredential]$CCSCredential, + + [Parameter(Mandatory = $false)] + [ValidateNotNull()] + [Alias('ADCredential')] + [pscredential]$DomainCredential, + + [Parameter(Mandatory = $false)] + [Alias('Encrypted')] + [switch]$PasswordIsEncrypted + ) + + begin { + $FunctionName = $PSCmdlet.MyInvocation.MyCommand.Name + + Write-Verbose "[$FunctionName] Starting function execution" + Write-Verbose "[$FunctionName] Attribute: $Attribute" + Write-Verbose "[$FunctionName] Domain: $Domain" + Write-Verbose "[$FunctionName] DomainOUPath: $DomainOUPath" + Write-Verbose "[$FunctionName] URL: $Url" + + # Initialize CCS connection once in begin block + try { + Write-Verbose "[$FunctionName] Initializing CCS Web Service connection" + $CCS = Initialize-CCS -Url $Url -WebServiceCredential $CCSCredential -ErrorAction Stop + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to initialize CCS Web Service: $_" ` + -ErrorCategory ConnectionError ` + -TargetObject $Url ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Verify the URL is correct and the CCS Web Service is accessible. Check credentials." + } + + # Prepare domain credentials if provided + $ADUsername = $null + $ADPassword = $null + + if ($DomainCredential) { + $ADUsername = $DomainCredential.UserName + Write-Verbose "[$FunctionName] Using domain credential: $ADUsername" + + try { + if ($PasswordIsEncrypted) { + $ADPassword = $DomainCredential.GetNetworkCredential().Password + Write-Verbose "[$FunctionName] Using pre-encrypted password" + } else { + $ADPassword = Get-CCSEncryptedPassword -SecureString $DomainCredential.Password -ErrorAction Stop + Write-Verbose "[$FunctionName] Password encrypted successfully" + } + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to process domain credential password: $_" ` + -ErrorCategory SecurityError ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Ensure the domain credential is valid and the password can be encrypted." + } + } else { + Write-Verbose "[$FunctionName] No domain credential provided, using CCS Web Service context" + } + + $ProcessedCount = 0 + $SuccessCount = 0 + $FailureCount = 0 + } + + process { + foreach ($User in $UserName) { + $ProcessedCount++ + + try { + Write-Verbose "[$FunctionName] Processing user: $User ($ProcessedCount)" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Processing: UserName=$User, Attribute=$Attribute, DomainOUPath=$DomainOUPath, Domain=$Domain") + } + + Write-Verbose "[$FunctionName] Calling ActiveDirectory_GetCustomAttributeForUser for $User" + + $Result = $CCS.ActiveDirectory_GetCustomAttributeForUser( + $User, + $Attribute, + $DomainOUPath, + $Domain, + $ADUsername, + $ADPassword + ) + + Write-Debug "[$FunctionName] Result from CCS Web Service: $Result" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Result for $User : $Result") + } + + Write-Verbose "[$FunctionName] Attribute value for $User : $Result" + + # Check for errors in result + if (Invoke-CCSIsError -Result $Result) { + $FailureCount++ + + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + Write-Debug "[$FunctionName] ErrorAction from PSBoundParameters: $($PSBoundParameters['ErrorAction'])" + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + Write-Debug "[$FunctionName] ErrorAction from preference: $ErrorActionPreference" + } + Write-Debug "[$FunctionName] ShouldThrow is set to: $ShouldThrow" + + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to get attribute '$Attribute' for user '$User': $Result" ` + -ErrorCategory ObjectNotFound ` + -TargetObject $User ` + -FunctionName $FunctionName ` + -RecommendedAction "Verify the username is correct and exists in Active Directory. Check that the attribute exists. Check domain credentials if provided." ` + -Throw:$ShouldThrow + } else { + $SuccessCount++ + Write-Output $Result + } + } catch { + $FailureCount++ + + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + } + + Invoke-CCSErrorHandling ` + -ErrorMessage "Exception occurred while processing user '$User': $_" ` + -ErrorCategory InvalidOperation ` + -TargetObject $User ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Check the error details and verify all parameters are correct." ` + -Throw:$ShouldThrow + } + } + } + + end { + Write-Verbose "[$FunctionName] Function execution completed" + Write-Verbose "[$FunctionName] Total processed: $ProcessedCount | Success: $SuccessCount | Failed: $FailureCount" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Summary - Processed: $ProcessedCount, Success: $SuccessCount, Failed: $FailureCount") + } + } +} diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADDescriptionForGroup.Tests.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADDescriptionForGroup.Tests.ps1 new file mode 100644 index 00000000..12b45752 --- /dev/null +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADDescriptionForGroup.Tests.ps1 @@ -0,0 +1,210 @@ +BeforeAll { + $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent + + $Items = Get-ChildItem -Path "$RootPath\Capa.PowerShell.Module.CCS\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + + # Setup test environment + $script:TestUrl = "https://$(hostname).capainstaller.com/CCSWebservice/CCS.asmx" + $script:TestDomain = 'Firmax.local' + $CredentialPath = "D:\PowerShell\Credentials\$($env:USERNAME)DomainAdminPesterTests.xml" + + # Setup credentials from environment variables (GitHub secrets) + if ($env:DOMAINADMINUSERNAME -and $env:DOMAINADMINPASSWORD) { + $securePassword = ConvertTo-SecureString $env:DOMAINADMINPASSWORD -AsPlainText -Force + $script:TestDomainCredential = New-Object System.Management.Automation.PSCredential($env:DOMAINADMINUSERNAME, $securePassword) + $script:TestCCSCredential = $script:TestDomainCredential + } elseif (Test-Path -Path $CredentialPath) { + $script:TestDomainCredential = Import-Clixml -Path $CredentialPath + $script:TestCCSCredential = $script:TestDomainCredential + } else { + $script:TestDomainCredential = Get-Credential -Message 'Enter domain admin credentials for integration tests' + $script:TestCCSCredential = $script:TestDomainCredential + } + + $DebugPreference = 'Continue' + $ErrorActionPreference = 'Stop' +} + +Describe 'Get-CCSADDescriptionForGroup' -Tag 'Unit' { + + Context 'Parameter Validation' { + + It 'Should have mandatory GroupName parameter' { + (Get-Command Get-CCSADDescriptionForGroup).Parameters['GroupName'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory Domain parameter' { + (Get-Command Get-CCSADDescriptionForGroup).Parameters['Domain'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory Url parameter' { + (Get-Command Get-CCSADDescriptionForGroup).Parameters['Url'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory CCSCredential parameter' { + (Get-Command Get-CCSADDescriptionForGroup).Parameters['CCSCredential'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have optional DomainCredential parameter' { + (Get-Command Get-CCSADDescriptionForGroup).Parameters['DomainCredential'].Attributes.Mandatory | Should -Be $false + } + + It 'Should have optional DomainOUPath parameter' { + (Get-Command Get-CCSADDescriptionForGroup).Parameters['DomainOUPath'].Attributes.Mandatory | Should -Be $false + } + + It 'Should accept array of group names' { + (Get-Command Get-CCSADDescriptionForGroup).Parameters['GroupName'].ParameterType.Name | Should -Be 'String[]' + } + + It 'Should accept pipeline input for GroupName' { + (Get-Command Get-CCSADDescriptionForGroup).Parameters['GroupName'].Attributes.ValueFromPipeline | Should -Be $true + } + } + + Context 'Parameter Aliases' { + + It 'Should have alias "Group" for GroupName' { + (Get-Command Get-CCSADDescriptionForGroup).Parameters['GroupName'].Aliases | Should -Contain 'Group' + } + + It 'Should have alias "Name" for GroupName' { + (Get-Command Get-CCSADDescriptionForGroup).Parameters['GroupName'].Aliases | Should -Contain 'Name' + } + + It 'Should have alias "OU" for DomainOUPath' { + (Get-Command Get-CCSADDescriptionForGroup).Parameters['DomainOUPath'].Aliases | Should -Contain 'OU' + } + + It 'Should have alias "SearchBase" for DomainOUPath' { + (Get-Command Get-CCSADDescriptionForGroup).Parameters['DomainOUPath'].Aliases | Should -Contain 'SearchBase' + } + } + + Context 'CmdletBinding Attributes' { + + It 'Should be an advanced function' { + (Get-Command Get-CCSADDescriptionForGroup).CmdletBinding | Should -Be $true + } + + It 'Should have OutputType defined' { + (Get-Command Get-CCSADDescriptionForGroup).OutputType.Name | Should -Contain 'System.String' + } + } +} + +Describe 'Get-CCSADDescriptionForGroup - Integration Tests' -Tag 'Integration' { + + Context 'DomainOUPath Validation' { + + It 'Should accept empty DomainOUPath' { + { Get-CCSADDescriptionForGroup -GroupName 'Domain Admins' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath '' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept standard DN format' { + { Get-CCSADDescriptionForGroup -GroupName 'Domain Admins' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'OU=Groups,DC=Firmax,DC=local' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept DC-only format' { + { Get-CCSADDescriptionForGroup -GroupName 'Domain Admins' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'DC=Firmax,DC=local' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept LDAP format' { + { Get-CCSADDescriptionForGroup -GroupName 'Domain Admins' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'LDAP://DC01.Firmax.local/OU=Groups,DC=Firmax,DC=local' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should reject invalid format' { + { Get-CCSADDescriptionForGroup -GroupName 'Domain Admins' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'InvalidPath' } | Should -Throw + } + } + + Context 'URL Validation' { + + It 'Should accept HTTPS URL' { + { Get-CCSADDescriptionForGroup -GroupName 'Domain Admins' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should reject HTTP URL (only HTTPS allowed)' { + $httpUrl = $script:TestUrl -replace '^https:', 'http:' + { Get-CCSADDescriptionForGroup -GroupName 'Domain Admins' -Domain $script:TestDomain -Url $httpUrl -CCSCredential $script:TestCCSCredential } | Should -Throw + } + + It 'Should reject URL without protocol' { + { Get-CCSADDescriptionForGroup -GroupName 'Domain Admins' -Domain $script:TestDomain -Url 'test.com/CCS.asmx' -CCSCredential $script:TestCCSCredential } | Should -Throw + } + } + + Context 'Domain Validation' { + + It 'Should accept valid domain format' { + { Get-CCSADDescriptionForGroup -GroupName 'Domain Admins' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept subdomain format' { + { Get-CCSADDescriptionForGroup -GroupName 'Domain Admins' -Domain 'sub.firmax.local' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should reject invalid domain format' { + { Get-CCSADDescriptionForGroup -GroupName 'Domain Admins' -Domain 'invalid_domain' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential } | Should -Throw + } + } + + Context 'Live CCS Web Service Operations' { + + It 'Should connect to CCS Web Service successfully' { + { Get-CCSADDescriptionForGroup -GroupName 'Domain Admins' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should retrieve description for Domain Admins group' { + $result = Get-CCSADDescriptionForGroup -GroupName 'Domain Admins' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + # Domain Admins may or may not have a description, so we just verify no error + { $result } | Should -Not -Throw + } + + It 'Should retrieve description with domain credentials' { + $result = Get-CCSADDescriptionForGroup -GroupName 'Domain Admins' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + { $result } | Should -Not -Throw + } + + It 'Should process multiple groups via pipeline' { + $groups = @('Domain Admins') + $results = $groups | Get-CCSADDescriptionForGroup -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + { $results } | Should -Not -Throw + } + + It 'Should handle LDAP format DomainOUPath' { + $ldapPath = 'LDAP://DC01.Firmax.local/DC=Firmax,DC=local' + { Get-CCSADDescriptionForGroup -GroupName 'Domain Admins' -DomainOUPath $ldapPath -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + } + + Context 'Error Handling' { + + It 'Should handle non-existent group gracefully' { + $result = Get-CCSADDescriptionForGroup -GroupName 'NonExistentGroup999' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -BeNullOrEmpty + } + + It 'Should handle invalid credentials gracefully' { + $badCred = New-Object System.Management.Automation.PSCredential('baduser', (ConvertTo-SecureString 'badpass' -AsPlainText -Force)) + { Get-CCSADDescriptionForGroup -GroupName 'Domain Admins' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $badCred -ErrorAction Stop } | Should -Throw + } + } +} + +Describe 'Get-CCSADDescriptionForGroup - Performance Tests' -Tag 'Performance' { + + Context 'Pipeline Performance' { + + It 'Should process pipeline input efficiently' { + $groups = @('Domain Admins', 'Domain Users', 'Domain Computers') + $measure = Measure-Command { + $results = $groups | Get-CCSADDescriptionForGroup -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + } + $measure.TotalSeconds | Should -BeLessThan 15 + } + } +} diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADDescriptionForGroup.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADDescriptionForGroup.ps1 new file mode 100644 index 00000000..698e2505 --- /dev/null +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADDescriptionForGroup.ps1 @@ -0,0 +1,279 @@ +function Get-CCSADDescriptionForGroup { + <# + .SYNOPSIS + Gets the description of a security group from Active Directory using the CCS Web Service. + + .DESCRIPTION + Retrieves the description of a security group from Active Directory using the CCS Web Service. + This advanced function includes comprehensive error handling, input validation, and supports pipeline input. + + .PARAMETER GroupName + The name of the security group to retrieve the description for. + Supports pipeline input and accepts multiple group names. + + .PARAMETER DomainOUPath + The Organizational Unit (OU) path in which the group resides. + If not specified, searches the entire domain. + Supports both standard DN format (OU=Groups,DC=example,DC=com) and LDAP format (LDAP://DC01.example.local/OU=Groups,DC=example,DC=com). + + .PARAMETER Domain + The domain in which the group resides. + Must be a valid domain name format. + + .PARAMETER Url + The URL of the CCS Web Service. + Must be a valid URI format. Example: "https://example.com/CCSWebservice/CCS.asmx" + + .PARAMETER CCSCredential + The credentials used to authenticate with the CCS Web Service. + + .PARAMETER DomainCredential + The credentials of an account with permissions to query Active Directory. + If not defined, it will run in the CCS Web Service context. + + .PARAMETER PasswordIsEncrypted + Indicates if the password in the DomainCredential is encrypted. Default is $false. + + .EXAMPLE + PS C:\> Get-CCSADDescriptionForGroup -GroupName "TestGroup" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Gets the description for TestGroup using default CCS context. + + .EXAMPLE + PS C:\> Get-CCSADDescriptionForGroup -GroupName "TestGroup" -DomainOUPath "OU=Groups,DC=example,DC=com" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential -DomainCredential $DomainCredential + + Gets the description for TestGroup using specific domain credentials and OU path. + + .EXAMPLE + PS C:\> "Group01", "Group02", "Group03" | Get-CCSADDescriptionForGroup -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Gets the descriptions for multiple groups using pipeline input. + + .EXAMPLE + PS C:\> Get-CCSADDescriptionForGroup -GroupName "TestGroup" -DomainOUPath "LDAP://DC01.Firmax.local/OU=Groups,DC=FirmaX,DC=local" -Domain "Firmax.local" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Gets the description for TestGroup using LDAP format for the OU path. The LDAP path will be automatically converted to standard DN format. + + .OUTPUTS + System.String + Returns the description of the security group. + + .NOTES + This is an advanced function with support for pipeline input and comprehensive error handling. + #> + [CmdletBinding()] + [OutputType([string])] + param ( + [Parameter( + Mandatory = $true, + Position = 0, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the security group name to retrieve the description for' + )] + [ValidateNotNullOrEmpty()] + [Alias('Group', 'Name', 'SamAccountName')] + [string[]]$GroupName, + + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] + [ValidateScript({ + if ([string]::IsNullOrEmpty($_)) { + return $true + } + # Support standard DN format: OU=...,DC=...,DC=... + if ($_ -match '^(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + # Support LDAP format: LDAP://server/DN + if ($_ -match '^LDAP://[^/]+/(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + throw "DomainOUPath must be in format 'OU=...,DC=...,DC=...' or 'LDAP://server/OU=...,DC=...,DC=...' or empty" + })] + [Alias('OU', 'Path', 'SearchBase')] + [string]$DomainOUPath = '', + + [Parameter( + Mandatory = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the domain name' + )] + [ValidateNotNullOrEmpty()] + [ValidatePattern('^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$')] + [string]$Domain, + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service URL' + )] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + if ($_ -match '^https://') { + $true + } else { + throw "URL must start with https:// (secure connection required)" + } + })] + [Alias('WebServiceUrl', 'Uri')] + [string]$Url, + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service credentials' + )] + [ValidateNotNull()] + [Alias('Credential', 'WebServiceCredential')] + [pscredential]$CCSCredential, + + [Parameter(Mandatory = $false)] + [ValidateNotNull()] + [Alias('ADCredential')] + [pscredential]$DomainCredential, + + [Parameter(Mandatory = $false)] + [Alias('Encrypted')] + [switch]$PasswordIsEncrypted + ) + + begin { + $FunctionName = $PSCmdlet.MyInvocation.MyCommand.Name + + Write-Verbose "[$FunctionName] Starting function execution" + Write-Verbose "[$FunctionName] Domain: $Domain" + Write-Verbose "[$FunctionName] DomainOUPath: $DomainOUPath" + Write-Verbose "[$FunctionName] URL: $Url" + + # Initialize CCS connection once in begin block + try { + Write-Verbose "[$FunctionName] Initializing CCS Web Service connection" + $CCS = Initialize-CCS -Url $Url -WebServiceCredential $CCSCredential -ErrorAction Stop + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to initialize CCS Web Service: $_" ` + -ErrorCategory ConnectionError ` + -TargetObject $Url ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Verify the URL is correct and the CCS Web Service is accessible. Check credentials." + } + + # Prepare domain credentials if provided + $ADUsername = $null + $ADPassword = $null + + if ($DomainCredential) { + $ADUsername = $DomainCredential.UserName + Write-Verbose "[$FunctionName] Using domain credential: $ADUsername" + + try { + if ($PasswordIsEncrypted) { + $ADPassword = $DomainCredential.GetNetworkCredential().Password + Write-Verbose "[$FunctionName] Using pre-encrypted password" + } else { + $ADPassword = Get-CCSEncryptedPassword -SecureString $DomainCredential.Password -ErrorAction Stop + Write-Verbose "[$FunctionName] Password encrypted successfully" + } + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to process domain credential password: $_" ` + -ErrorCategory SecurityError ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Ensure the domain credential is valid and the password can be encrypted." + } + } else { + Write-Verbose "[$FunctionName] No domain credential provided, using CCS Web Service context" + } + + $ProcessedCount = 0 + $SuccessCount = 0 + $FailureCount = 0 + } + + process { + foreach ($Group in $GroupName) { + $ProcessedCount++ + + try { + Write-Verbose "[$FunctionName] Processing group: $Group ($ProcessedCount)" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Processing: GroupName=$Group, DomainOUPath=$DomainOUPath, Domain=$Domain") + } + + Write-Verbose "[$FunctionName] Calling ActiveDirectory_GetDescriptionForGroup for $Group" + + $Result = $CCS.ActiveDirectory_GetDescriptionForGroup( + $Group, + $DomainOUPath, + $Domain, + $ADUsername, + $ADPassword + ) + + Write-Debug "[$FunctionName] Result from CCS Web Service: $Result" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Result for $Group : $Result") + } + + Write-Verbose "[$FunctionName] Description for $Group : $Result" + + # Check for errors in result + if (Invoke-CCSIsError -Result $Result) { + $FailureCount++ + + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + Write-Debug "[$FunctionName] ErrorAction from PSBoundParameters: $($PSBoundParameters['ErrorAction'])" + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + Write-Debug "[$FunctionName] ErrorAction from preference: $ErrorActionPreference" + } + Write-Debug "[$FunctionName] ShouldThrow is set to: $ShouldThrow" + + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to get description for group '$Group': $Result" ` + -ErrorCategory ObjectNotFound ` + -TargetObject $Group ` + -FunctionName $FunctionName ` + -RecommendedAction "Verify the group name is correct and exists in Active Directory. Check domain credentials if provided." ` + -Throw:$ShouldThrow + } else { + $SuccessCount++ + Write-Output $Result + } + } catch { + $FailureCount++ + + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + } + + Invoke-CCSErrorHandling ` + -ErrorMessage "Exception occurred while processing group '$Group': $_" ` + -ErrorCategory InvalidOperation ` + -TargetObject $Group ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Check the error details and verify all parameters are correct." ` + -Throw:$ShouldThrow + } + } + } + + end { + Write-Verbose "[$FunctionName] Function execution completed" + Write-Verbose "[$FunctionName] Total processed: $ProcessedCount | Success: $SuccessCount | Failed: $FailureCount" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Summary - Processed: $ProcessedCount, Success: $SuccessCount, Failed: $FailureCount") + } + } +} diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADDisplayNameForUser.Tests.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADDisplayNameForUser.Tests.ps1 new file mode 100644 index 00000000..c7fe70c7 --- /dev/null +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADDisplayNameForUser.Tests.ps1 @@ -0,0 +1,210 @@ +BeforeAll { + $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent + + $Items = Get-ChildItem -Path "$RootPath\Capa.PowerShell.Module.CCS\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + + # Setup test environment + $script:TestUrl = "https://$(hostname).capainstaller.com/CCSWebservice/CCS.asmx" + $script:TestDomain = 'Firmax.local' + $CredentialPath = "D:\PowerShell\Credentials\$($env:USERNAME)DomainAdminPesterTests.xml" + + # Setup credentials from environment variables (GitHub secrets) + if ($env:DOMAINADMINUSERNAME -and $env:DOMAINADMINPASSWORD) { + $securePassword = ConvertTo-SecureString $env:DOMAINADMINPASSWORD -AsPlainText -Force + $script:TestDomainCredential = New-Object System.Management.Automation.PSCredential($env:DOMAINADMINUSERNAME, $securePassword) + $script:TestCCSCredential = $script:TestDomainCredential + } elseif (Test-Path -Path $CredentialPath) { + $script:TestDomainCredential = Import-Clixml -Path $CredentialPath + $script:TestCCSCredential = $script:TestDomainCredential + } else { + $script:TestDomainCredential = Get-Credential -Message 'Enter domain admin credentials for integration tests' + $script:TestCCSCredential = $script:TestDomainCredential + } + + $DebugPreference = 'Continue' + $ErrorActionPreference = 'Stop' +} + +Describe 'Get-CCSADDisplayNameForUser' -Tag 'Unit' { + + Context 'Parameter Validation' { + + It 'Should have mandatory UserName parameter' { + (Get-Command Get-CCSADDisplayNameForUser).Parameters['UserName'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory Domain parameter' { + (Get-Command Get-CCSADDisplayNameForUser).Parameters['Domain'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory Url parameter' { + (Get-Command Get-CCSADDisplayNameForUser).Parameters['Url'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory CCSCredential parameter' { + (Get-Command Get-CCSADDisplayNameForUser).Parameters['CCSCredential'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have optional DomainCredential parameter' { + (Get-Command Get-CCSADDisplayNameForUser).Parameters['DomainCredential'].Attributes.Mandatory | Should -Be $false + } + + It 'Should have optional DomainOUPath parameter' { + (Get-Command Get-CCSADDisplayNameForUser).Parameters['DomainOUPath'].Attributes.Mandatory | Should -Be $false + } + + It 'Should accept array of usernames' { + (Get-Command Get-CCSADDisplayNameForUser).Parameters['UserName'].ParameterType.Name | Should -Be 'String[]' + } + + It 'Should accept pipeline input for UserName' { + (Get-Command Get-CCSADDisplayNameForUser).Parameters['UserName'].Attributes.ValueFromPipeline | Should -Be $true + } + } + + Context 'Parameter Aliases' { + + It 'Should have alias "User" for UserName' { + (Get-Command Get-CCSADDisplayNameForUser).Parameters['UserName'].Aliases | Should -Contain 'User' + } + + It 'Should have alias "SamAccountName" for UserName' { + (Get-Command Get-CCSADDisplayNameForUser).Parameters['UserName'].Aliases | Should -Contain 'SamAccountName' + } + + It 'Should have alias "OU" for DomainOUPath' { + (Get-Command Get-CCSADDisplayNameForUser).Parameters['DomainOUPath'].Aliases | Should -Contain 'OU' + } + + It 'Should have alias "SearchBase" for DomainOUPath' { + (Get-Command Get-CCSADDisplayNameForUser).Parameters['DomainOUPath'].Aliases | Should -Contain 'SearchBase' + } + } + + Context 'CmdletBinding Attributes' { + + It 'Should be an advanced function' { + (Get-Command Get-CCSADDisplayNameForUser).CmdletBinding | Should -Be $true + } + + It 'Should have OutputType defined' { + (Get-Command Get-CCSADDisplayNameForUser).OutputType.Name | Should -Contain 'System.String' + } + } +} + +Describe 'Get-CCSADDisplayNameForUser - Integration Tests' -Tag 'Integration' { + + Context 'DomainOUPath Validation' { + + It 'Should accept empty DomainOUPath' { + { Get-CCSADDisplayNameForUser -UserName $env:USERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath '' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept standard DN format' { + { Get-CCSADDisplayNameForUser -UserName $env:USERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'OU=Users,DC=Firmax,DC=local' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept DC-only format' { + { Get-CCSADDisplayNameForUser -UserName $env:USERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'DC=Firmax,DC=local' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept LDAP format' { + { Get-CCSADDisplayNameForUser -UserName $env:USERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'LDAP://DC01.Firmax.local/OU=Users,DC=Firmax,DC=local' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should reject invalid format' { + { Get-CCSADDisplayNameForUser -UserName $env:USERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'InvalidPath' } | Should -Throw + } + } + + Context 'URL Validation' { + + It 'Should accept HTTPS URL' { + { Get-CCSADDisplayNameForUser -UserName $env:USERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should reject HTTP URL (only HTTPS allowed)' { + $httpUrl = $script:TestUrl -replace '^https:', 'http:' + { Get-CCSADDisplayNameForUser -UserName $env:USERNAME -Domain $script:TestDomain -Url $httpUrl -CCSCredential $script:TestCCSCredential } | Should -Throw + } + + It 'Should reject URL without protocol' { + { Get-CCSADDisplayNameForUser -UserName $env:USERNAME -Domain $script:TestDomain -Url 'test.com/CCS.asmx' -CCSCredential $script:TestCCSCredential } | Should -Throw + } + } + + Context 'Domain Validation' { + + It 'Should accept valid domain format' { + { Get-CCSADDisplayNameForUser -UserName $env:USERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept subdomain format' { + { Get-CCSADDisplayNameForUser -UserName $env:USERNAME -Domain 'sub.firmax.local' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should reject invalid domain format' { + { Get-CCSADDisplayNameForUser -UserName $env:USERNAME -Domain 'invalid_domain' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential } | Should -Throw + } + } + + Context 'Live CCS Web Service Operations' { + + It 'Should connect to CCS Web Service successfully' { + { Get-CCSADDisplayNameForUser -UserName $env:USERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should retrieve display name for current user' { + $result = Get-CCSADDisplayNameForUser -UserName $env:USERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + # Display name may be empty, so just verify no error + { $result } | Should -Not -Throw + } + + It 'Should retrieve display name with domain credentials' { + $result = Get-CCSADDisplayNameForUser -UserName $env:USERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + { $result } | Should -Not -Throw + } + + It 'Should process multiple users via pipeline' { + $users = @($env:USERNAME) + $results = $users | Get-CCSADDisplayNameForUser -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + { $results } | Should -Not -Throw + } + + It 'Should handle LDAP format DomainOUPath' { + $ldapPath = 'LDAP://DC01.Firmax.local/OU=Users,DC=Firmax,DC=local' + { Get-CCSADDisplayNameForUser -UserName $env:USERNAME -DomainOUPath $ldapPath -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + } + + Context 'Error Handling' { + + It 'Should handle non-existent user gracefully' { + $result = Get-CCSADDisplayNameForUser -UserName 'NonExistentUser999' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -BeNullOrEmpty + } + + It 'Should handle invalid credentials gracefully' { + $badCred = New-Object System.Management.Automation.PSCredential('baduser', (ConvertTo-SecureString 'badpass' -AsPlainText -Force)) + { Get-CCSADDisplayNameForUser -UserName $env:USERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $badCred -ErrorAction Stop } | Should -Throw + } + } +} + +Describe 'Get-CCSADDisplayNameForUser - Performance Tests' -Tag 'Performance' { + + Context 'Pipeline Performance' { + + It 'Should process pipeline input efficiently' { + $users = @($env:USERNAME, $env:USERNAME, $env:USERNAME) + $measure = Measure-Command { + $results = $users | Get-CCSADDisplayNameForUser -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + } + $measure.TotalSeconds | Should -BeLessThan 10 + } + } +} diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADDisplayNameForUser.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADDisplayNameForUser.ps1 new file mode 100644 index 00000000..45da7e51 --- /dev/null +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADDisplayNameForUser.ps1 @@ -0,0 +1,279 @@ +function Get-CCSADDisplayNameForUser { + <# + .SYNOPSIS + Gets the display name of a user from Active Directory using the CCS Web Service. + + .DESCRIPTION + Retrieves the display name of a user from Active Directory using the CCS Web Service. + This advanced function includes comprehensive error handling, input validation, and supports pipeline input. + + .PARAMETER UserName + The username of the user to retrieve the display name for. + Supports pipeline input and accepts multiple usernames. + + .PARAMETER DomainOUPath + The Organizational Unit (OU) path in which the user resides. + If not specified, searches the entire domain. + Supports both standard DN format (OU=Users,DC=example,DC=com) and LDAP format (LDAP://DC01.example.local/OU=Users,DC=example,DC=com). + + .PARAMETER Domain + The domain in which the user resides. + Must be a valid domain name format. + + .PARAMETER Url + The URL of the CCS Web Service. + Must be a valid URI format. Example: "https://example.com/CCSWebservice/CCS.asmx" + + .PARAMETER CCSCredential + The credentials used to authenticate with the CCS Web Service. + + .PARAMETER DomainCredential + The credentials of an account with permissions to query Active Directory. + If not defined, it will run in the CCS Web Service context. + + .PARAMETER PasswordIsEncrypted + Indicates if the password in the DomainCredential is encrypted. Default is $false. + + .EXAMPLE + PS C:\> Get-CCSADDisplayNameForUser -UserName "jdoe" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Gets the display name for user jdoe using default CCS context. + + .EXAMPLE + PS C:\> Get-CCSADDisplayNameForUser -UserName "jdoe" -DomainOUPath "OU=Users,DC=example,DC=com" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential -DomainCredential $DomainCredential + + Gets the display name for user jdoe using specific domain credentials and OU path. + + .EXAMPLE + PS C:\> "jdoe", "asmith", "bjones" | Get-CCSADDisplayNameForUser -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Gets the display names for multiple users using pipeline input. + + .EXAMPLE + PS C:\> Get-CCSADDisplayNameForUser -UserName "jdoe" -DomainOUPath "LDAP://DC01.Firmax.local/OU=Users,DC=FirmaX,DC=local" -Domain "Firmax.local" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Gets the display name for user jdoe using LDAP format for the OU path. The LDAP path will be automatically converted to standard DN format. + + .OUTPUTS + System.String + Returns the display name of the user. + + .NOTES + This is an advanced function with support for pipeline input and comprehensive error handling. + #> + [CmdletBinding()] + [OutputType([string])] + param ( + [Parameter( + Mandatory = $true, + Position = 0, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the username to retrieve the display name for' + )] + [ValidateNotNullOrEmpty()] + [Alias('User', 'SamAccountName', 'Identity')] + [string[]]$UserName, + + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] + [ValidateScript({ + if ([string]::IsNullOrEmpty($_)) { + return $true + } + # Support standard DN format: OU=...,DC=...,DC=... + if ($_ -match '^(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + # Support LDAP format: LDAP://server/DN + if ($_ -match '^LDAP://[^/]+/(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + throw "DomainOUPath must be in format 'OU=...,DC=...,DC=...' or 'LDAP://server/OU=...,DC=...,DC=...' or empty" + })] + [Alias('OU', 'Path', 'SearchBase')] + [string]$DomainOUPath = '', + + [Parameter( + Mandatory = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the domain name' + )] + [ValidateNotNullOrEmpty()] + [ValidatePattern('^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$')] + [string]$Domain, + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service URL' + )] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + if ($_ -match '^https://') { + $true + } else { + throw "URL must start with https:// (secure connection required)" + } + })] + [Alias('WebServiceUrl', 'Uri')] + [string]$Url, + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service credentials' + )] + [ValidateNotNull()] + [Alias('Credential', 'WebServiceCredential')] + [pscredential]$CCSCredential, + + [Parameter(Mandatory = $false)] + [ValidateNotNull()] + [Alias('ADCredential')] + [pscredential]$DomainCredential, + + [Parameter(Mandatory = $false)] + [Alias('Encrypted')] + [switch]$PasswordIsEncrypted + ) + + begin { + $FunctionName = $PSCmdlet.MyInvocation.MyCommand.Name + + Write-Verbose "[$FunctionName] Starting function execution" + Write-Verbose "[$FunctionName] Domain: $Domain" + Write-Verbose "[$FunctionName] DomainOUPath: $DomainOUPath" + Write-Verbose "[$FunctionName] URL: $Url" + + # Initialize CCS connection once in begin block + try { + Write-Verbose "[$FunctionName] Initializing CCS Web Service connection" + $CCS = Initialize-CCS -Url $Url -WebServiceCredential $CCSCredential -ErrorAction Stop + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to initialize CCS Web Service: $_" ` + -ErrorCategory ConnectionError ` + -TargetObject $Url ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Verify the URL is correct and the CCS Web Service is accessible. Check credentials." + } + + # Prepare domain credentials if provided + $ADUsername = $null + $ADPassword = $null + + if ($DomainCredential) { + $ADUsername = $DomainCredential.UserName + Write-Verbose "[$FunctionName] Using domain credential: $ADUsername" + + try { + if ($PasswordIsEncrypted) { + $ADPassword = $DomainCredential.GetNetworkCredential().Password + Write-Verbose "[$FunctionName] Using pre-encrypted password" + } else { + $ADPassword = Get-CCSEncryptedPassword -SecureString $DomainCredential.Password -ErrorAction Stop + Write-Verbose "[$FunctionName] Password encrypted successfully" + } + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to process domain credential password: $_" ` + -ErrorCategory SecurityError ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Ensure the domain credential is valid and the password can be encrypted." + } + } else { + Write-Verbose "[$FunctionName] No domain credential provided, using CCS Web Service context" + } + + $ProcessedCount = 0 + $SuccessCount = 0 + $FailureCount = 0 + } + + process { + foreach ($User in $UserName) { + $ProcessedCount++ + + try { + Write-Verbose "[$FunctionName] Processing user: $User ($ProcessedCount)" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Processing: UserName=$User, DomainOUPath=$DomainOUPath, Domain=$Domain") + } + + Write-Verbose "[$FunctionName] Calling ActiveDirectory_GetDisplayNameForUser for $User" + + $Result = $CCS.ActiveDirectory_GetDisplayNameForUser( + $User, + $DomainOUPath, + $Domain, + $ADUsername, + $ADPassword + ) + + Write-Debug "[$FunctionName] Result from CCS Web Service: $Result" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Result for $User : $Result") + } + + Write-Verbose "[$FunctionName] Display name for $User : $Result" + + # Check for errors in result + if (Invoke-CCSIsError -Result $Result) { + $FailureCount++ + + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + Write-Debug "[$FunctionName] ErrorAction from PSBoundParameters: $($PSBoundParameters['ErrorAction'])" + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + Write-Debug "[$FunctionName] ErrorAction from preference: $ErrorActionPreference" + } + Write-Debug "[$FunctionName] ShouldThrow is set to: $ShouldThrow" + + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to get display name for user '$User': $Result" ` + -ErrorCategory ObjectNotFound ` + -TargetObject $User ` + -FunctionName $FunctionName ` + -RecommendedAction "Verify the username is correct and exists in Active Directory. Check domain credentials if provided." ` + -Throw:$ShouldThrow + } else { + $SuccessCount++ + Write-Output $Result + } + } catch { + $FailureCount++ + + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + } + + Invoke-CCSErrorHandling ` + -ErrorMessage "Exception occurred while processing user '$User': $_" ` + -ErrorCategory InvalidOperation ` + -TargetObject $User ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Check the error details and verify all parameters are correct." ` + -Throw:$ShouldThrow + } + } + } + + end { + Write-Verbose "[$FunctionName] Function execution completed" + Write-Verbose "[$FunctionName] Total processed: $ProcessedCount | Success: $SuccessCount | Failed: $FailureCount" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Summary - Processed: $ProcessedCount, Success: $SuccessCount, Failed: $FailureCount") + } + } +} diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADEmailForUser.Tests.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADEmailForUser.Tests.ps1 new file mode 100644 index 00000000..c264982e --- /dev/null +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADEmailForUser.Tests.ps1 @@ -0,0 +1,210 @@ +BeforeAll { + $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent + + $Items = Get-ChildItem -Path "$RootPath\Capa.PowerShell.Module.CCS\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + + # Setup test environment + $script:TestUrl = "https://$(hostname).capainstaller.com/CCSWebservice/CCS.asmx" + $script:TestDomain = 'Firmax.local' + $CredentialPath = "D:\PowerShell\Credentials\$($env:USERNAME)DomainAdminPesterTests.xml" + + # Setup credentials from environment variables (GitHub secrets) + if ($env:DOMAINADMINUSERNAME -and $env:DOMAINADMINPASSWORD) { + $securePassword = ConvertTo-SecureString $env:DOMAINADMINPASSWORD -AsPlainText -Force + $script:TestDomainCredential = New-Object System.Management.Automation.PSCredential($env:DOMAINADMINUSERNAME, $securePassword) + $script:TestCCSCredential = $script:TestDomainCredential + } elseif (Test-Path -Path $CredentialPath) { + $script:TestDomainCredential = Import-Clixml -Path $CredentialPath + $script:TestCCSCredential = $script:TestDomainCredential + } else { + $script:TestDomainCredential = Get-Credential -Message 'Enter domain admin credentials for integration tests' + $script:TestCCSCredential = $script:TestDomainCredential + } + + $DebugPreference = 'Continue' + $ErrorActionPreference = 'Stop' +} + +Describe 'Get-CCSADEmailForUser' -Tag 'Unit' { + + Context 'Parameter Validation' { + + It 'Should have mandatory UserName parameter' { + (Get-Command Get-CCSADEmailForUser).Parameters['UserName'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory Domain parameter' { + (Get-Command Get-CCSADEmailForUser).Parameters['Domain'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory Url parameter' { + (Get-Command Get-CCSADEmailForUser).Parameters['Url'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory CCSCredential parameter' { + (Get-Command Get-CCSADEmailForUser).Parameters['CCSCredential'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have optional DomainCredential parameter' { + (Get-Command Get-CCSADEmailForUser).Parameters['DomainCredential'].Attributes.Mandatory | Should -Be $false + } + + It 'Should have optional DomainOUPath parameter' { + (Get-Command Get-CCSADEmailForUser).Parameters['DomainOUPath'].Attributes.Mandatory | Should -Be $false + } + + It 'Should accept array of usernames' { + (Get-Command Get-CCSADEmailForUser).Parameters['UserName'].ParameterType.Name | Should -Be 'String[]' + } + + It 'Should accept pipeline input for UserName' { + (Get-Command Get-CCSADEmailForUser).Parameters['UserName'].Attributes.ValueFromPipeline | Should -Be $true + } + } + + Context 'Parameter Aliases' { + + It 'Should have alias "User" for UserName' { + (Get-Command Get-CCSADEmailForUser).Parameters['UserName'].Aliases | Should -Contain 'User' + } + + It 'Should have alias "SamAccountName" for UserName' { + (Get-Command Get-CCSADEmailForUser).Parameters['UserName'].Aliases | Should -Contain 'SamAccountName' + } + + It 'Should have alias "OU" for DomainOUPath' { + (Get-Command Get-CCSADEmailForUser).Parameters['DomainOUPath'].Aliases | Should -Contain 'OU' + } + + It 'Should have alias "SearchBase" for DomainOUPath' { + (Get-Command Get-CCSADEmailForUser).Parameters['DomainOUPath'].Aliases | Should -Contain 'SearchBase' + } + } + + Context 'CmdletBinding Attributes' { + + It 'Should be an advanced function' { + (Get-Command Get-CCSADEmailForUser).CmdletBinding | Should -Be $true + } + + It 'Should have OutputType defined' { + (Get-Command Get-CCSADEmailForUser).OutputType.Name | Should -Contain 'System.String' + } + } +} + +Describe 'Get-CCSADEmailForUser - Integration Tests' -Tag 'Integration' { + + Context 'DomainOUPath Validation' { + + It 'Should accept empty DomainOUPath' { + { Get-CCSADEmailForUser -UserName $env:USERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath '' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept standard DN format' { + { Get-CCSADEmailForUser -UserName $env:USERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'OU=Users,DC=Firmax,DC=local' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept DC-only format' { + { Get-CCSADEmailForUser -UserName $env:USERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'DC=Firmax,DC=local' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept LDAP format' { + { Get-CCSADEmailForUser -UserName $env:USERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'LDAP://DC01.Firmax.local/OU=Users,DC=Firmax,DC=local' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should reject invalid format' { + { Get-CCSADEmailForUser -UserName $env:USERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'InvalidPath' } | Should -Throw + } + } + + Context 'URL Validation' { + + It 'Should accept HTTPS URL' { + { Get-CCSADEmailForUser -UserName $env:USERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should reject HTTP URL (only HTTPS allowed)' { + $httpUrl = $script:TestUrl -replace '^https:', 'http:' + { Get-CCSADEmailForUser -UserName $env:USERNAME -Domain $script:TestDomain -Url $httpUrl -CCSCredential $script:TestCCSCredential } | Should -Throw + } + + It 'Should reject URL without protocol' { + { Get-CCSADEmailForUser -UserName $env:USERNAME -Domain $script:TestDomain -Url 'test.com/CCS.asmx' -CCSCredential $script:TestCCSCredential } | Should -Throw + } + } + + Context 'Domain Validation' { + + It 'Should accept valid domain format' { + { Get-CCSADEmailForUser -UserName $env:USERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept subdomain format' { + { Get-CCSADEmailForUser -UserName $env:USERNAME -Domain 'sub.firmax.local' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should reject invalid domain format' { + { Get-CCSADEmailForUser -UserName $env:USERNAME -Domain 'invalid_domain' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential } | Should -Throw + } + } + + Context 'Live CCS Web Service Operations' { + + It 'Should connect to CCS Web Service successfully' { + { Get-CCSADEmailForUser -UserName $env:USERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should retrieve email for current user' { + $result = Get-CCSADEmailForUser -UserName $env:USERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + # Email may be empty, so just verify no error + { $result } | Should -Not -Throw + } + + It 'Should retrieve email with domain credentials' { + $result = Get-CCSADEmailForUser -UserName $env:USERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + { $result } | Should -Not -Throw + } + + It 'Should process multiple users via pipeline' { + $users = @($env:USERNAME) + $results = $users | Get-CCSADEmailForUser -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + { $results } | Should -Not -Throw + } + + It 'Should handle LDAP format DomainOUPath' { + $ldapPath = 'LDAP://DC01.Firmax.local/OU=Users,DC=Firmax,DC=local' + { Get-CCSADEmailForUser -UserName $env:USERNAME -DomainOUPath $ldapPath -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + } + + Context 'Error Handling' { + + It 'Should handle non-existent user gracefully' { + $result = Get-CCSADEmailForUser -UserName 'NonExistentUser999' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -BeNullOrEmpty + } + + It 'Should handle invalid credentials gracefully' { + $badCred = New-Object System.Management.Automation.PSCredential('baduser', (ConvertTo-SecureString 'badpass' -AsPlainText -Force)) + { Get-CCSADEmailForUser -UserName $env:USERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $badCred -ErrorAction Stop } | Should -Throw + } + } +} + +Describe 'Get-CCSADEmailForUser - Performance Tests' -Tag 'Performance' { + + Context 'Pipeline Performance' { + + It 'Should process pipeline input efficiently' { + $users = @($env:USERNAME, $env:USERNAME, $env:USERNAME) + $measure = Measure-Command { + $results = $users | Get-CCSADEmailForUser -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + } + $measure.TotalSeconds | Should -BeLessThan 10 + } + } +} diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADEmailForUser.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADEmailForUser.ps1 new file mode 100644 index 00000000..d64c09ee --- /dev/null +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADEmailForUser.ps1 @@ -0,0 +1,279 @@ +function Get-CCSADEmailForUser { + <# + .SYNOPSIS + Gets the email address of a user from Active Directory using the CCS Web Service. + + .DESCRIPTION + Retrieves the email address of a user from Active Directory using the CCS Web Service. + This advanced function includes comprehensive error handling, input validation, and supports pipeline input. + + .PARAMETER UserName + The username of the user to retrieve the email address for. + Supports pipeline input and accepts multiple usernames. + + .PARAMETER DomainOUPath + The Organizational Unit (OU) path in which the user resides. + If not specified, searches the entire domain. + Supports both standard DN format (OU=Users,DC=example,DC=com) and LDAP format (LDAP://DC01.example.local/OU=Users,DC=example,DC=com). + + .PARAMETER Domain + The domain in which the user resides. + Must be a valid domain name format. + + .PARAMETER Url + The URL of the CCS Web Service. + Must be a valid URI format. Example: "https://example.com/CCSWebservice/CCS.asmx" + + .PARAMETER CCSCredential + The credentials used to authenticate with the CCS Web Service. + + .PARAMETER DomainCredential + The credentials of an account with permissions to query Active Directory. + If not defined, it will run in the CCS Web Service context. + + .PARAMETER PasswordIsEncrypted + Indicates if the password in the DomainCredential is encrypted. Default is $false. + + .EXAMPLE + PS C:\> Get-CCSADEmailForUser -UserName "jdoe" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Gets the email address for user jdoe using default CCS context. + + .EXAMPLE + PS C:\> Get-CCSADEmailForUser -UserName "jdoe" -DomainOUPath "OU=Users,DC=example,DC=com" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential -DomainCredential $DomainCredential + + Gets the email address for user jdoe using specific domain credentials and OU path. + + .EXAMPLE + PS C:\> "jdoe", "asmith", "bjones" | Get-CCSADEmailForUser -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Gets the email addresses for multiple users using pipeline input. + + .EXAMPLE + PS C:\> Get-CCSADEmailForUser -UserName "jdoe" -DomainOUPath "LDAP://DC01.Firmax.local/OU=Users,DC=FirmaX,DC=local" -Domain "Firmax.local" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Gets the email address for user jdoe using LDAP format for the OU path. The LDAP path will be automatically converted to standard DN format. + + .OUTPUTS + System.String + Returns the email address of the user. + + .NOTES + This is an advanced function with support for pipeline input and comprehensive error handling. + #> + [CmdletBinding()] + [OutputType([string])] + param ( + [Parameter( + Mandatory = $true, + Position = 0, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the username to retrieve the email address for' + )] + [ValidateNotNullOrEmpty()] + [Alias('User', 'SamAccountName', 'Identity')] + [string[]]$UserName, + + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] + [ValidateScript({ + if ([string]::IsNullOrEmpty($_)) { + return $true + } + # Support standard DN format: OU=...,DC=...,DC=... + if ($_ -match '^(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + # Support LDAP format: LDAP://server/DN + if ($_ -match '^LDAP://[^/]+/(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + throw "DomainOUPath must be in format 'OU=...,DC=...,DC=...' or 'LDAP://server/OU=...,DC=...,DC=...' or empty" + })] + [Alias('OU', 'Path', 'SearchBase')] + [string]$DomainOUPath = '', + + [Parameter( + Mandatory = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the domain name' + )] + [ValidateNotNullOrEmpty()] + [ValidatePattern('^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$')] + [string]$Domain, + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service URL' + )] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + if ($_ -match '^https://') { + $true + } else { + throw "URL must start with https:// (secure connection required)" + } + })] + [Alias('WebServiceUrl', 'Uri')] + [string]$Url, + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service credentials' + )] + [ValidateNotNull()] + [Alias('Credential', 'WebServiceCredential')] + [pscredential]$CCSCredential, + + [Parameter(Mandatory = $false)] + [ValidateNotNull()] + [Alias('ADCredential')] + [pscredential]$DomainCredential, + + [Parameter(Mandatory = $false)] + [Alias('Encrypted')] + [switch]$PasswordIsEncrypted + ) + + begin { + $FunctionName = $PSCmdlet.MyInvocation.MyCommand.Name + + Write-Verbose "[$FunctionName] Starting function execution" + Write-Verbose "[$FunctionName] Domain: $Domain" + Write-Verbose "[$FunctionName] DomainOUPath: $DomainOUPath" + Write-Verbose "[$FunctionName] URL: $Url" + + # Initialize CCS connection once in begin block + try { + Write-Verbose "[$FunctionName] Initializing CCS Web Service connection" + $CCS = Initialize-CCS -Url $Url -WebServiceCredential $CCSCredential -ErrorAction Stop + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to initialize CCS Web Service: $_" ` + -ErrorCategory ConnectionError ` + -TargetObject $Url ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Verify the URL is correct and the CCS Web Service is accessible. Check credentials." + } + + # Prepare domain credentials if provided + $ADUsername = $null + $ADPassword = $null + + if ($DomainCredential) { + $ADUsername = $DomainCredential.UserName + Write-Verbose "[$FunctionName] Using domain credential: $ADUsername" + + try { + if ($PasswordIsEncrypted) { + $ADPassword = $DomainCredential.GetNetworkCredential().Password + Write-Verbose "[$FunctionName] Using pre-encrypted password" + } else { + $ADPassword = Get-CCSEncryptedPassword -SecureString $DomainCredential.Password -ErrorAction Stop + Write-Verbose "[$FunctionName] Password encrypted successfully" + } + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to process domain credential password: $_" ` + -ErrorCategory SecurityError ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Ensure the domain credential is valid and the password can be encrypted." + } + } else { + Write-Verbose "[$FunctionName] No domain credential provided, using CCS Web Service context" + } + + $ProcessedCount = 0 + $SuccessCount = 0 + $FailureCount = 0 + } + + process { + foreach ($User in $UserName) { + $ProcessedCount++ + + try { + Write-Verbose "[$FunctionName] Processing user: $User ($ProcessedCount)" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Processing: UserName=$User, DomainOUPath=$DomainOUPath, Domain=$Domain") + } + + Write-Verbose "[$FunctionName] Calling ActiveDirectory_GetEmailForUser for $User" + + $Result = $CCS.ActiveDirectory_GetEmailForUser( + $User, + $DomainOUPath, + $Domain, + $ADUsername, + $ADPassword + ) + + Write-Debug "[$FunctionName] Result from CCS Web Service: $Result" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Result for $User : $Result") + } + + Write-Verbose "[$FunctionName] Email address for $User : $Result" + + # Check for errors in result + if (Invoke-CCSIsError -Result $Result) { + $FailureCount++ + + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + Write-Debug "[$FunctionName] ErrorAction from PSBoundParameters: $($PSBoundParameters['ErrorAction'])" + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + Write-Debug "[$FunctionName] ErrorAction from preference: $ErrorActionPreference" + } + Write-Debug "[$FunctionName] ShouldThrow is set to: $ShouldThrow" + + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to get email address for user '$User': $Result" ` + -ErrorCategory ObjectNotFound ` + -TargetObject $User ` + -FunctionName $FunctionName ` + -RecommendedAction "Verify the username is correct and exists in Active Directory. Check that the user has an email address configured. Check domain credentials if provided." ` + -Throw:$ShouldThrow + } else { + $SuccessCount++ + Write-Output $Result + } + } catch { + $FailureCount++ + + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + } + + Invoke-CCSErrorHandling ` + -ErrorMessage "Exception occurred while processing user '$User': $_" ` + -ErrorCategory InvalidOperation ` + -TargetObject $User ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Check the error details and verify all parameters are correct." ` + -Throw:$ShouldThrow + } + } + } + + end { + Write-Verbose "[$FunctionName] Function execution completed" + Write-Verbose "[$FunctionName] Total processed: $ProcessedCount | Success: $SuccessCount | Failed: $FailureCount" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Summary - Processed: $ProcessedCount, Success: $SuccessCount, Failed: $FailureCount") + } + } +} diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADLastLoginForComputers.Tests.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADLastLoginForComputers.Tests.ps1 new file mode 100644 index 00000000..27df2c68 --- /dev/null +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADLastLoginForComputers.Tests.ps1 @@ -0,0 +1,211 @@ +BeforeAll { + $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent + + $Items = Get-ChildItem -Path "$RootPath\Capa.PowerShell.Module.CCS\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + + # Setup test environment + $script:TestUrl = "https://$(hostname).capainstaller.com/CCSWebservice/CCS.asmx" + $script:TestDomain = 'Firmax.local' + $CredentialPath = "D:\PowerShell\Credentials\$($env:USERNAME)DomainAdminPesterTests.xml" + + # Setup credentials from environment variables (GitHub secrets) + if ($env:DOMAINADMINUSERNAME -and $env:DOMAINADMINPASSWORD) { + $securePassword = ConvertTo-SecureString $env:DOMAINADMINPASSWORD -AsPlainText -Force + $script:TestDomainCredential = New-Object System.Management.Automation.PSCredential($env:DOMAINADMINUSERNAME, $securePassword) + $script:TestCCSCredential = $script:TestDomainCredential + } elseif (Test-Path -Path $CredentialPath) { + $script:TestDomainCredential = Import-Clixml -Path $CredentialPath + $script:TestCCSCredential = $script:TestDomainCredential + } else { + $script:TestDomainCredential = Get-Credential -Message 'Enter domain admin credentials for integration tests' + $script:TestCCSCredential = $script:TestDomainCredential + } + + $DebugPreference = 'Continue' + $ErrorActionPreference = 'Stop' +} + +Describe 'Get-CCSADLastLoginForComputers' -Tag 'Unit' { + + Context 'Parameter Validation' { + + It 'Should have mandatory Domain parameter' { + (Get-Command Get-CCSADLastLoginForComputers).Parameters['Domain'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory Url parameter' { + (Get-Command Get-CCSADLastLoginForComputers).Parameters['Url'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory CCSCredential parameter' { + (Get-Command Get-CCSADLastLoginForComputers).Parameters['CCSCredential'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have optional DomainCredential parameter' { + (Get-Command Get-CCSADLastLoginForComputers).Parameters['DomainCredential'].Attributes.Mandatory | Should -Be $false + } + + It 'Should have optional DomainOUPath parameter' { + (Get-Command Get-CCSADLastLoginForComputers).Parameters['DomainOUPath'].Attributes.Mandatory | Should -Be $false + } + } + + Context 'Parameter Aliases' { + + It 'Should have alias "OU" for DomainOUPath' { + (Get-Command Get-CCSADLastLoginForComputers).Parameters['DomainOUPath'].Aliases | Should -Contain 'OU' + } + + It 'Should have alias "SearchBase" for DomainOUPath' { + (Get-Command Get-CCSADLastLoginForComputers).Parameters['DomainOUPath'].Aliases | Should -Contain 'SearchBase' + } + + It 'Should have alias "Credential" for CCSCredential' { + (Get-Command Get-CCSADLastLoginForComputers).Parameters['CCSCredential'].Aliases | Should -Contain 'Credential' + } + + It 'Should have alias "ADCredential" for DomainCredential' { + (Get-Command Get-CCSADLastLoginForComputers).Parameters['DomainCredential'].Aliases | Should -Contain 'ADCredential' + } + } + + Context 'CmdletBinding Attributes' { + + It 'Should be an advanced function' { + (Get-Command Get-CCSADLastLoginForComputers).CmdletBinding | Should -Be $true + } + + It 'Should have OutputType defined' { + (Get-Command Get-CCSADLastLoginForComputers).OutputType | Should -Not -BeNullOrEmpty + } + } +} + +Describe 'Get-CCSADLastLoginForComputers - Integration Tests' -Tag 'Integration' { + + Context 'DomainOUPath Validation' { + + It 'Should accept empty DomainOUPath' { + $result = Get-CCSADLastLoginForComputers -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath '' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Not -BeNullOrEmpty + } + + It 'Should accept standard DN format' { + { Get-CCSADLastLoginForComputers -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'OU=Computers,DC=Firmax,DC=local' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept DC-only format' { + $result = Get-CCSADLastLoginForComputers -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'DC=Firmax,DC=local' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Not -BeNullOrEmpty + } + + It 'Should accept LDAP format' { + { Get-CCSADLastLoginForComputers -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'LDAP://DC01.Firmax.local/OU=Computers,DC=Firmax,DC=local' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should reject invalid format' { + { Get-CCSADLastLoginForComputers -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'InvalidPath' -ErrorAction Stop } | Should -Throw + } + } + + Context 'URL Validation' { + + It 'Should accept HTTPS URL' { + $result = Get-CCSADLastLoginForComputers -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Not -BeNullOrEmpty + } + + It 'Should reject HTTP URL (only HTTPS allowed)' { + $httpUrl = $script:TestUrl -replace '^https:', 'http:' + { Get-CCSADLastLoginForComputers -Domain $script:TestDomain -Url $httpUrl -CCSCredential $script:TestCCSCredential -ErrorAction Stop } | Should -Throw + } + + It 'Should reject URL without protocol' { + { Get-CCSADLastLoginForComputers -Domain $script:TestDomain -Url 'test.com/CCS.asmx' -CCSCredential $script:TestCCSCredential -ErrorAction Stop } | Should -Throw + } + } + + Context 'Domain Validation' { + + It 'Should accept valid domain format' { + $result = Get-CCSADLastLoginForComputers -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Not -BeNullOrEmpty + } + + It 'Should accept subdomain format' { + { Get-CCSADLastLoginForComputers -Domain 'sub.firmax.local' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should reject invalid domain format' { + { Get-CCSADLastLoginForComputers -Domain 'invalid_domain' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction Stop } | Should -Throw + } + } + + Context 'Live CCS Web Service Operations' { + + It 'Should connect to CCS Web Service successfully' { + $result = Get-CCSADLastLoginForComputers -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Not -BeNullOrEmpty + } + + It 'Should retrieve computer last login information' { + $result = Get-CCSADLastLoginForComputers -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType [PSCustomObject] + } + + It 'Should retrieve computers with domain credentials' { + $result = Get-CCSADLastLoginForComputers -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Not -BeNullOrEmpty + } + + It 'Should return objects with correct properties' { + $result = Get-CCSADLastLoginForComputers -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $firstComputer = $result | Select-Object -First 1 + $firstComputer.PSObject.Properties.Name | Should -Contain 'Name' + $firstComputer.PSObject.Properties.Name | Should -Contain 'LastLogon' + $firstComputer.PSObject.Properties.Name | Should -Contain 'LastLogonDC' + $firstComputer.PSObject.Properties.Name | Should -Contain 'OperatingSystem' + $firstComputer.PSObject.Properties.Name | Should -Contain 'Path' + } + + It 'Should handle LDAP format for DomainOUPath' { + $ldapPath = 'LDAP://DC01.Firmax.local/DC=Firmax,DC=local' + { Get-CCSADLastLoginForComputers -DomainOUPath $ldapPath -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should retrieve computers from specific OU' { + $ouPath = 'DC=Firmax,DC=local' + $result = Get-CCSADLastLoginForComputers -DomainOUPath $ouPath -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Not -BeNullOrEmpty + } + } + + Context 'Error Handling' { + + It 'Should handle invalid credentials gracefully' { + $badCred = New-Object System.Management.Automation.PSCredential('baduser', (ConvertTo-SecureString 'badpass' -AsPlainText -Force)) + { Get-CCSADLastLoginForComputers -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $badCred -ErrorAction Stop } | Should -Throw + } + + It 'Should handle non-existent URL gracefully' { + { Get-CCSADLastLoginForComputers -Domain $script:TestDomain -Url 'https://nonexistent.invalid/CCS.asmx' -CCSCredential $script:TestCCSCredential -ErrorAction Stop } | Should -Throw + } + } +} + +Describe 'Get-CCSADLastLoginForComputers - Performance Tests' -Tag 'Performance' { + + Context 'Query Performance' { + + It 'Should retrieve computer information in reasonable time' { + $measure = Measure-Command { + Get-CCSADLastLoginForComputers -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WarningAction SilentlyContinue -ErrorAction SilentlyContinue + } + $measure.TotalSeconds | Should -BeLessThan 30 + } + } +} diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADLastLoginForComputers.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADLastLoginForComputers.ps1 new file mode 100644 index 00000000..28ac9232 --- /dev/null +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADLastLoginForComputers.ps1 @@ -0,0 +1,266 @@ +function Get-CCSADLastLoginForComputers { + <# + .SYNOPSIS + Retrieves last login information for all computers in Active Directory using the CCS Web Service. + + .DESCRIPTION + Retrieves comprehensive last login information for all computers in Active Directory using the CCS Web Service. + Returns computer name, last logon time, last logon DC, operating system, and AD path for each computer. + This advanced function includes comprehensive error handling and input validation. + + .PARAMETER DomainOUPath + The Organizational Unit (OU) path to search for computers. + If not specified, searches the entire domain. + Supports both standard DN format (OU=Computers,DC=example,DC=com) and LDAP format (LDAP://DC01.example.local/OU=Computers,DC=example,DC=com). + + .PARAMETER Domain + The domain to query for computer information. + Must be a valid domain name format. + + .PARAMETER Url + The URL of the CCS Web Service. + Must be a valid URI format. Example: "https://example.com/CCSWebservice/CCS.asmx" + + .PARAMETER CCSCredential + The credentials used to authenticate with the CCS Web Service. + + .PARAMETER DomainCredential + The credentials of an account with permissions to query Active Directory. + If not defined, it will run in the CCS Web Service context. + + .PARAMETER PasswordIsEncrypted + Indicates if the password in the DomainCredential is encrypted. Default is $false. + + .EXAMPLE + PS C:\> Get-CCSADLastLoginForComputers -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Retrieves last login information for all computers in the domain using default CCS context. + + .EXAMPLE + PS C:\> Get-CCSADLastLoginForComputers -DomainOUPath "OU=Computers,DC=example,DC=com" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential -DomainCredential $DomainCredential + + Retrieves last login information for all computers in a specific OU using domain credentials. + + .EXAMPLE + PS C:\> Get-CCSADLastLoginForComputers -DomainOUPath "LDAP://DC01.Firmax.local/OU=Servers,DC=FirmaX,DC=local" -Domain "Firmax.local" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Retrieves last login information using LDAP format for the OU path. The LDAP path will be automatically converted to standard DN format. + + .EXAMPLE + PS C:\> $computers = Get-CCSADLastLoginForComputers -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + PS C:\> $computers | Where-Object { $_.LastLogon -lt (Get-Date).AddDays(-30) } + + Retrieves all computers and filters for those that haven't logged in for 30 days. + + .OUTPUTS + System.Collections.Generic.List[ADComputerInfo] + Returns a list of ADComputerInfo objects containing: + - Name: Computer name + - LastLogon: Last logon timestamp + - LastLogonDC: Domain controller where last logon occurred + - OperatingSystem: Operating system version + - Path: Active Directory path + + .NOTES + This is an advanced function with comprehensive error handling. + The function returns a strongly-typed list of computer objects from Active Directory. + #> + [CmdletBinding()] + [OutputType([System.Collections.Generic.List[PSCustomObject]])] + param ( + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] + [ValidateScript({ + if ([string]::IsNullOrEmpty($_)) { + return $true + } + # Support standard DN format: OU=...,DC=...,DC=... + if ($_ -match '^(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + # Support LDAP format: LDAP://server/DN + if ($_ -match '^LDAP://[^/]+/(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + throw "DomainOUPath must be in format 'OU=...,DC=...,DC=...' or 'LDAP://server/OU=...,DC=...,DC=...' or empty" + })] + [Alias('OU', 'Path', 'SearchBase')] + [string]$DomainOUPath = '', + + [Parameter( + Mandatory = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the domain name' + )] + [ValidateNotNullOrEmpty()] + [ValidatePattern('^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$')] + [string]$Domain, + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service URL' + )] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + if ($_ -match '^https://') { + $true + } else { + throw "URL must start with https:// (secure connection required)" + } + })] + [Alias('WebServiceUrl', 'Uri')] + [string]$Url, + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service credentials' + )] + [ValidateNotNull()] + [Alias('Credential', 'WebServiceCredential')] + [pscredential]$CCSCredential, + + [Parameter(Mandatory = $false)] + [ValidateNotNull()] + [Alias('ADCredential')] + [pscredential]$DomainCredential, + + [Parameter(Mandatory = $false)] + [Alias('Encrypted')] + [switch]$PasswordIsEncrypted + ) + + begin { + $FunctionName = $PSCmdlet.MyInvocation.MyCommand.Name + + Write-Verbose "[$FunctionName] Starting function execution" + Write-Verbose "[$FunctionName] Domain: $Domain" + Write-Verbose "[$FunctionName] DomainOUPath: $DomainOUPath" + Write-Verbose "[$FunctionName] URL: $Url" + + # Initialize CCS connection once in begin block + try { + Write-Verbose "[$FunctionName] Initializing CCS Web Service connection" + $CCS = Initialize-CCS -Url $Url -WebServiceCredential $CCSCredential -ErrorAction Stop + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to initialize CCS Web Service: $_" ` + -ErrorCategory ConnectionError ` + -TargetObject $Url ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Verify the URL is correct and the CCS Web Service is accessible. Check credentials." + } + + # Prepare domain credentials if provided + $ADUsername = $null + $ADPassword = $null + + if ($DomainCredential) { + $ADUsername = $DomainCredential.UserName + Write-Verbose "[$FunctionName] Using domain credential: $ADUsername" + + try { + if ($PasswordIsEncrypted) { + $ADPassword = $DomainCredential.GetNetworkCredential().Password + Write-Verbose "[$FunctionName] Using pre-encrypted password" + } else { + $ADPassword = Get-CCSEncryptedPassword -SecureString $DomainCredential.Password -ErrorAction Stop + Write-Verbose "[$FunctionName] Password encrypted successfully" + } + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to process domain credential password: $_" ` + -ErrorCategory SecurityError ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Ensure the domain credential is valid and the password can be encrypted." + } + } else { + Write-Verbose "[$FunctionName] No domain credential provided, using CCS Web Service context" + } + } + + process { + try { + Write-Verbose "[$FunctionName] Querying Active Directory for computer last login information" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Processing: DomainOUPath=$DomainOUPath, Domain=$Domain") + } + + Write-Verbose "[$FunctionName] Calling ActiveDirectory_GetLastloginForComputers" + + $Result = $CCS.ActiveDirectory_GetLastloginForComputers( + $DomainOUPath, + $Domain, + $ADUsername, + $ADPassword + ) + + Write-Debug "Result from CCS Web Service: $($Result.Count) computers retrieved" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Result: Retrieved $($Result.Count) computers") + } + + Write-Verbose "[$FunctionName] Retrieved $($Result.Count) computers" + + # Check if result is null or empty + if ($null -eq $Result) { + Invoke-CCSErrorHandling ` + -ErrorMessage "No data returned from CCS Web Service" ` + -ErrorCategory ObjectNotFound ` + -TargetObject $Domain ` + -FunctionName $FunctionName ` + -RecommendedAction "Verify the domain and OU path are correct and contain computers." + return + } + + # Convert the result to PSCustomObject for better PowerShell compatibility + foreach ($Computer in $Result) { + $ComputerObject = [PSCustomObject]@{ + Name = $Computer.Name + LastLogon = $Computer.LastLogon + LastLogonDC = $Computer.LastLogonDC + OperatingSystem = $Computer.OperatingSystem + Path = $Computer.Path + } + + # Add custom type name for formatting + $ComputerObject.PSObject.TypeNames.Insert(0, 'CCS.ADComputerInfo') + + Write-Output $ComputerObject + } + + Write-Verbose "[$FunctionName] Successfully retrieved last login information for $($Result.Count) computers" + + } catch { + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + Write-Debug "ErrorAction from PSBoundParameters: $($PSBoundParameters['ErrorAction'])" + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + Write-Debug "ErrorAction from ErrorActionPreference: $ErrorActionPreference" + } + Write-Debug "ShouldThrow is set to: $ShouldThrow" + + Invoke-CCSErrorHandling ` + -ErrorMessage "Exception occurred while retrieving computer information: $_" ` + -ErrorCategory InvalidOperation ` + -TargetObject $Domain ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Check the error details and verify all parameters are correct." ` + -Throw:$ShouldThrow + } + } + + end { + Write-Verbose "[$FunctionName] Function execution completed" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Completed") + } + } +} diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADOUStructure.Tests.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADOUStructure.Tests.ps1 new file mode 100644 index 00000000..efd8b379 --- /dev/null +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADOUStructure.Tests.ps1 @@ -0,0 +1,220 @@ +BeforeAll { + $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent + + $Items = Get-ChildItem -Path "$RootPath\Capa.PowerShell.Module.CCS\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + + # Setup test environment + $script:TestUrl = "https://$(hostname).capainstaller.com/CCSWebservice/CCS.asmx" + $script:TestDomain = 'Firmax.local' + $CredentialPath = "D:\PowerShell\Credentials\$($env:USERNAME)DomainAdminPesterTests.xml" + + # Setup credentials from environment variables (GitHub secrets) + if ($env:DOMAINADMINUSERNAME -and $env:DOMAINADMINPASSWORD) { + $securePassword = ConvertTo-SecureString $env:DOMAINADMINPASSWORD -AsPlainText -Force + $script:TestDomainCredential = New-Object System.Management.Automation.PSCredential($env:DOMAINADMINUSERNAME, $securePassword) + $script:TestCCSCredential = $script:TestDomainCredential + } elseif (Test-Path -Path $CredentialPath) { + $script:TestDomainCredential = Import-Clixml -Path $CredentialPath + $script:TestCCSCredential = $script:TestDomainCredential + } else { + $script:TestDomainCredential = Get-Credential -Message 'Enter domain admin credentials for integration tests' + $script:TestCCSCredential = $script:TestDomainCredential + } + + $DebugPreference = 'Continue' + $ErrorActionPreference = 'Stop' +} + +Describe 'Get-CCSADOUStructure' -Tag 'Unit' { + + Context 'Parameter Validation' { + + It 'Should have mandatory Domain parameter' { + (Get-Command Get-CCSADOUStructure).Parameters['Domain'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory Url parameter' { + (Get-Command Get-CCSADOUStructure).Parameters['Url'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory CCSCredential parameter' { + (Get-Command Get-CCSADOUStructure).Parameters['CCSCredential'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have optional DomainCredential parameter' { + (Get-Command Get-CCSADOUStructure).Parameters['DomainCredential'].Attributes.Mandatory | Should -Be $false + } + + It 'Should have optional DomainTopPath parameter' { + (Get-Command Get-CCSADOUStructure).Parameters['DomainTopPath'].Attributes.Mandatory | Should -Be $false + } + + It 'Should have optional ExcludedOUNames parameter' { + (Get-Command Get-CCSADOUStructure).Parameters['ExcludedOUNames'].Attributes.Mandatory | Should -Be $false + } + + It 'Should accept array of excluded OU names' { + (Get-Command Get-CCSADOUStructure).Parameters['ExcludedOUNames'].ParameterType.Name | Should -Be 'String[]' + } + } + + Context 'Parameter Aliases' { + + It 'Should have alias "TopPath" for DomainTopPath' { + (Get-Command Get-CCSADOUStructure).Parameters['DomainTopPath'].Aliases | Should -Contain 'TopPath' + } + + It 'Should have alias "SearchBase" for DomainTopPath' { + (Get-Command Get-CCSADOUStructure).Parameters['DomainTopPath'].Aliases | Should -Contain 'SearchBase' + } + + It 'Should have alias "Credential" for CCSCredential' { + (Get-Command Get-CCSADOUStructure).Parameters['CCSCredential'].Aliases | Should -Contain 'Credential' + } + + It 'Should have alias "ADCredential" for DomainCredential' { + (Get-Command Get-CCSADOUStructure).Parameters['DomainCredential'].Aliases | Should -Contain 'ADCredential' + } + + It 'Should have alias "Exclude" for ExcludedOUNames' { + (Get-Command Get-CCSADOUStructure).Parameters['ExcludedOUNames'].Aliases | Should -Contain 'Exclude' + } + } + + Context 'CmdletBinding Attributes' { + + It 'Should be an advanced function' { + (Get-Command Get-CCSADOUStructure).CmdletBinding | Should -Be $true + } + + It 'Should have OutputType defined' { + (Get-Command Get-CCSADOUStructure).OutputType | Should -Not -BeNullOrEmpty + } + } +} + +Describe 'Get-CCSADOUStructure - Integration Tests' -Tag 'Integration' { + + Context 'DomainTopPath Validation' { + + It 'Should accept empty DomainTopPath' { + { Get-CCSADOUStructure -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainTopPath '' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept standard DN format with OU' { + { Get-CCSADOUStructure -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainTopPath 'OU=IT,DC=Firmax,DC=local' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept DC-only format' { + { Get-CCSADOUStructure -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainTopPath 'DC=Firmax,DC=local' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should reject invalid format' { + { Get-CCSADOUStructure -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainTopPath 'InvalidPath' -ErrorAction Stop } | Should -Throw + } + } + + Context 'URL Validation' { + + It 'Should accept HTTPS URL' { + { Get-CCSADOUStructure -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should reject HTTP URL (only HTTPS allowed)' { + $httpUrl = $script:TestUrl -replace '^https:', 'http:' + { Get-CCSADOUStructure -Domain $script:TestDomain -Url $httpUrl -CCSCredential $script:TestCCSCredential -ErrorAction Stop } | Should -Throw + } + + It 'Should reject URL without protocol' { + { Get-CCSADOUStructure -Domain $script:TestDomain -Url 'test.com/CCS.asmx' -CCSCredential $script:TestCCSCredential -ErrorAction Stop } | Should -Throw + } + } + + Context 'Domain Validation' { + + It 'Should accept valid domain format' { + { Get-CCSADOUStructure -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept subdomain format' { + { Get-CCSADOUStructure -Domain 'sub.firmax.local' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should reject invalid domain format' { + { Get-CCSADOUStructure -Domain 'invalid_domain' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction Stop } | Should -Throw + } + } + + Context 'Live CCS Web Service Operations' { + + It 'Should connect to CCS Web Service successfully' { + { Get-CCSADOUStructure -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should retrieve OU structure' { + { Get-CCSADOUStructure -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should retrieve OUs with domain credentials' { + { Get-CCSADOUStructure -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should return objects with correct properties if data available' { + $result = Get-CCSADOUStructure -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + if ($result) { + $firstOU = $result | Select-Object -First 1 + $firstOU.PSObject.Properties.Name | Should -Contain 'Name' + $firstOU.PSObject.Properties.Name | Should -Contain 'Path' + $firstOU.PSObject.Properties.Name | Should -Contain 'Children' + } else { + Set-ItResult -Skipped -Because "No data returned from web service in test environment" + } + } + + It 'Should support hierarchical structure with children if data available' { + $result = Get-CCSADOUStructure -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + if ($result) { + $result.Children | Should -Not -BeNull + } else { + Set-ItResult -Skipped -Because "No data returned from web service in test environment" + } + } + + It 'Should retrieve OUs from specific top path' { + $topPath = 'DC=Firmax,DC=local' + { Get-CCSADOUStructure -DomainTopPath $topPath -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should exclude OUs by name' { + { Get-CCSADOUStructure -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ExcludedOUNames @('Test', 'Archive') -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + } + + Context 'Error Handling' { + + It 'Should handle invalid credentials gracefully' { + $badCred = New-Object System.Management.Automation.PSCredential('baduser', (ConvertTo-SecureString 'badpass' -AsPlainText -Force)) + { Get-CCSADOUStructure -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $badCred -ErrorAction Stop } | Should -Throw + } + + It 'Should handle non-existent URL gracefully' { + { Get-CCSADOUStructure -Domain $script:TestDomain -Url 'https://nonexistent.invalid/CCS.asmx' -CCSCredential $script:TestCCSCredential -ErrorAction Stop } | Should -Throw + } + } +} + +Describe 'Get-CCSADOUStructure - Performance Tests' -Tag 'Performance' { + + Context 'Query Performance' { + + It 'Should retrieve OU structure in reasonable time' { + $measure = Measure-Command { + Get-CCSADOUStructure -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WarningAction SilentlyContinue -ErrorAction SilentlyContinue + } + $measure.TotalSeconds | Should -BeLessThan 30 + } + } +} diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADOUStructure.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADOUStructure.ps1 new file mode 100644 index 00000000..884ea61e --- /dev/null +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADOUStructure.ps1 @@ -0,0 +1,304 @@ +function Get-CCSADOUStructure { + <# + .SYNOPSIS + Retrieves the Organizational Unit (OU) structure from Active Directory using the CCS Web Service. + + .DESCRIPTION + Retrieves the hierarchical Organizational Unit (OU) structure from Active Directory using the CCS Web Service. + Returns a nested structure of OUs with their names, paths, and child OUs. + This advanced function includes comprehensive error handling and input validation. + + .PARAMETER DomainTopPath + The top-level domain path to start the OU structure retrieval from. + Must be in DN format (DC=example,DC=com). + If not specified, retrieves from the root of the domain. + + .PARAMETER Domain + The domain to query for OU structure. + Must be a valid domain name format. + + .PARAMETER Url + The URL of the CCS Web Service. + Must be a valid URI format. Example: "https://example.com/CCSWebservice/CCS.asmx" + + .PARAMETER CCSCredential + The credentials used to authenticate with the CCS Web Service. + + .PARAMETER DomainCredential + The credentials of an account with permissions to query Active Directory. + If not defined, it will run in the CCS Web Service context. + + .PARAMETER ExcludedOUNames + An array of OU names to exclude from the structure. + OUs with these names will not be included in the results. + + .PARAMETER PasswordIsEncrypted + Indicates if the password in the DomainCredential is encrypted. Default is $false. + + .EXAMPLE + PS C:\> Get-CCSADOUStructure -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Retrieves the complete OU structure for the domain using default CCS context. + + .EXAMPLE + PS C:\> Get-CCSADOUStructure -DomainTopPath "DC=example,DC=com" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential -DomainCredential $DomainCredential + + Retrieves the OU structure starting from the domain root using specific domain credentials. + + .EXAMPLE + PS C:\> Get-CCSADOUStructure -DomainTopPath "OU=IT,DC=example,DC=com" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Retrieves the OU structure starting from a specific OU. + + .EXAMPLE + PS C:\> Get-CCSADOUStructure -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential -ExcludedOUNames @("Test", "Archive") + + Retrieves the OU structure excluding OUs named "Test" or "Archive". + + .OUTPUTS + System.Collections.Generic.List[PSCustomObject] + Returns a hierarchical list of OU structures containing: + - Name: OU name + - Path: Full DN path + - Children: Nested list of child OUs + + .NOTES + This is an advanced function with comprehensive error handling. + The function returns a hierarchical structure that can be traversed to view the complete OU tree. + #> + [CmdletBinding()] + [OutputType([System.Collections.Generic.List[PSCustomObject]])] + param ( + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] + [ValidateScript({ + if ([string]::IsNullOrEmpty($_)) { + return $true + } + # Support standard DN format: OU=...,DC=...,DC=... or DC=...,DC=... + if ($_ -match '^(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + throw "DomainTopPath must be in format 'OU=...,DC=...,DC=...' or 'DC=...,DC=...' or empty" + })] + [Alias('TopPath', 'BasePath', 'SearchBase')] + [string]$DomainTopPath = '', + + [Parameter( + Mandatory = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the domain name' + )] + [ValidateNotNullOrEmpty()] + [ValidatePattern('^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$')] + [string]$Domain, + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service URL' + )] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + if ($_ -match '^https://') { + $true + } else { + throw "URL must start with https:// (secure connection required)" + } + })] + [Alias('WebServiceUrl', 'Uri')] + [string]$Url, + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service credentials' + )] + [ValidateNotNull()] + [Alias('Credential', 'WebServiceCredential')] + [pscredential]$CCSCredential, + + [Parameter(Mandatory = $false)] + [ValidateNotNull()] + [Alias('ADCredential')] + [pscredential]$DomainCredential, + + [Parameter(Mandatory = $false)] + [ValidateNotNull()] + [Alias('Exclude', 'ExcludeOUs')] + [string[]]$ExcludedOUNames, + + [Parameter(Mandatory = $false)] + [Alias('Encrypted')] + [switch]$PasswordIsEncrypted + ) + + begin { + $FunctionName = $PSCmdlet.MyInvocation.MyCommand.Name + + Write-Verbose "[$FunctionName] Starting function execution" + Write-Verbose "[$FunctionName] Domain: $Domain" + Write-Verbose "[$FunctionName] DomainTopPath: $DomainTopPath" + Write-Verbose "[$FunctionName] URL: $Url" + + # Initialize CCS connection once in begin block + try { + Write-Verbose "[$FunctionName] Initializing CCS Web Service connection" + $CCS = Initialize-CCS -Url $Url -WebServiceCredential $CCSCredential -ErrorAction Stop + } catch { + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + Write-Debug "ErrorAction from PSBoundParameters: $($PSBoundParameters['ErrorAction'])" + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + Write-Debug "ErrorAction from ErrorActionPreference: $ErrorActionPreference" + } + Write-Debug "ShouldThrow is set to: $ShouldThrow" + + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to initialize CCS Web Service: $_" ` + -ErrorCategory ConnectionError ` + -TargetObject $Url ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Verify the URL is correct and the CCS Web Service is accessible. Check credentials." ` + -Throw:$ShouldThrow + return + } + + # Prepare domain credentials if provided + $ADUsername = $null + $ADPassword = $null + + if ($DomainCredential) { + $ADUsername = $DomainCredential.UserName + Write-Verbose "[$FunctionName] Using domain credential: $ADUsername" + + try { + if ($PasswordIsEncrypted) { + $ADPassword = $DomainCredential.GetNetworkCredential().Password + Write-Verbose "[$FunctionName] Using pre-encrypted password" + } else { + $ADPassword = Get-CCSEncryptedPassword -SecureString $DomainCredential.Password -ErrorAction Stop + Write-Verbose "[$FunctionName] Password encrypted successfully" + } + } catch { + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + } + + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to process domain credential password: $_" ` + -ErrorCategory SecurityError ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Ensure the domain credential is valid and the password can be encrypted." ` + -Throw:$ShouldThrow + return + } + } else { + Write-Verbose "[$FunctionName] No domain credential provided, using CCS Web Service context" + } + } + + process { + try { + Write-Verbose "[$FunctionName] Querying Active Directory for OU structure" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Processing: DomainTopPath=$DomainTopPath, Domain=$Domain") + } + + Write-Verbose "[$FunctionName] Calling ActiveDirectory_Get_OU_Structure" + + $Result = $CCS.ActiveDirectory_Get_OU_Structure( + $DomainTopPath, + $ADUsername, + $ADPassword, + $ExcludedOUNames + ) + + Write-Debug "Result from CCS Web Service: $($Result.Count) OUs retrieved" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Result: Retrieved $($Result.Count) OUs") + } + + Write-Verbose "[$FunctionName] Retrieved $($Result.Count) OUs" + + # Check if result is null or empty + if ($null -eq $Result) { + Invoke-CCSErrorHandling ` + -ErrorMessage "No data returned from CCS Web Service" ` + -ErrorCategory ObjectNotFound ` + -TargetObject $Domain ` + -FunctionName $FunctionName ` + -RecommendedAction "Verify the domain and top path are correct." + return + } + + # Recursive function to convert the OU structure to PSCustomObject + function ConvertTo-OUStructure { + param($OU) + + $OUObject = [PSCustomObject]@{ + Name = $OU.Name + Path = $OU.Path + Children = @() + } + + # Add custom type name for formatting + $OUObject.PSObject.TypeNames.Insert(0, 'CCS.ActiveDirectoryOUStructure') + + # Recursively process children + if ($OU.Children -and $OU.Children.Count -gt 0) { + foreach ($Child in $OU.Children) { + $OUObject.Children += ConvertTo-OUStructure -OU $Child + } + } + + return $OUObject + } + + # Convert the result to PSCustomObject for better PowerShell compatibility + foreach ($OU in $Result) { + $OUStructure = ConvertTo-OUStructure -OU $OU + Write-Output $OUStructure + } + + Write-Verbose "[$FunctionName] Successfully retrieved OU structure with $($Result.Count) top-level OUs" + + } catch { + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + Write-Debug "ErrorAction from PSBoundParameters: $($PSBoundParameters['ErrorAction'])" + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + Write-Debug "ErrorAction from ErrorActionPreference: $ErrorActionPreference" + } + Write-Debug "ShouldThrow is set to: $ShouldThrow" + + Invoke-CCSErrorHandling ` + -ErrorMessage "Exception occurred while retrieving OU structure: $_" ` + -ErrorCategory InvalidOperation ` + -TargetObject $Domain ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Check the error details and verify all parameters are correct." ` + -Throw:$ShouldThrow + } + } + + end { + Write-Verbose "[$FunctionName] Function execution completed" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Completed") + } + } +} diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADSid.Tests.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADSid.Tests.ps1 new file mode 100644 index 00000000..b95cfe5b --- /dev/null +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADSid.Tests.ps1 @@ -0,0 +1,226 @@ +BeforeAll { + $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent + + $Items = Get-ChildItem -Path "$RootPath\Capa.PowerShell.Module.CCS\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + + # Setup test environment + $script:TestUrl = "https://$(hostname).capainstaller.com/CCSWebservice/CCS.asmx" + $script:TestDomain = 'Firmax.local' + $CredentialPath = "D:\PowerShell\Credentials\$($env:USERNAME)DomainAdminPesterTests.xml" + + # Setup credentials from environment variables (GitHub secrets) + if ($env:DOMAINADMINUSERNAME -and $env:DOMAINADMINPASSWORD) { + $securePassword = ConvertTo-SecureString $env:DOMAINADMINPASSWORD -AsPlainText -Force + $script:TestDomainCredential = New-Object System.Management.Automation.PSCredential($env:DOMAINADMINUSERNAME, $securePassword) + $script:TestCCSCredential = $script:TestDomainCredential + } elseif (Test-Path -Path $CredentialPath) { + $script:TestDomainCredential = Import-Clixml -Path $CredentialPath + $script:TestCCSCredential = $script:TestDomainCredential + } else { + $script:TestDomainCredential = Get-Credential -Message 'Enter domain admin credentials for integration tests' + $script:TestCCSCredential = $script:TestDomainCredential + } + + $DebugPreference = 'Continue' + $ErrorActionPreference = 'Stop' +} + +Describe 'Get-CCSADSid' -Tag 'Unit' { + + Context 'Parameter Validation' { + + It 'Should have mandatory ComputerName parameter' { + (Get-Command Get-CCSADSid).Parameters['ComputerName'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory Domain parameter' { + (Get-Command Get-CCSADSid).Parameters['Domain'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory Url parameter' { + (Get-Command Get-CCSADSid).Parameters['Url'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory CCSCredential parameter' { + (Get-Command Get-CCSADSid).Parameters['CCSCredential'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have optional DomainCredential parameter' { + (Get-Command Get-CCSADSid).Parameters['DomainCredential'].Attributes.Mandatory | Should -Be $false + } + + It 'Should have optional DomainOUPath parameter' { + (Get-Command Get-CCSADSid).Parameters['DomainOUPath'].Attributes.Mandatory | Should -Be $false + } + + It 'Should accept array of computer names' { + (Get-Command Get-CCSADSid).Parameters['ComputerName'].ParameterType.Name | Should -Be 'String[]' + } + + It 'Should accept pipeline input for ComputerName' { + (Get-Command Get-CCSADSid).Parameters['ComputerName'].Attributes.ValueFromPipeline | Should -Be $true + } + } + + Context 'Parameter Aliases' { + + It 'Should have alias "Name" for ComputerName' { + (Get-Command Get-CCSADSid).Parameters['ComputerName'].Aliases | Should -Contain 'Name' + } + + It 'Should have alias "Computer" for ComputerName' { + (Get-Command Get-CCSADSid).Parameters['ComputerName'].Aliases | Should -Contain 'Computer' + } + + It 'Should have alias "OU" for DomainOUPath' { + (Get-Command Get-CCSADSid).Parameters['DomainOUPath'].Aliases | Should -Contain 'OU' + } + + It 'Should have alias "SearchBase" for DomainOUPath' { + (Get-Command Get-CCSADSid).Parameters['DomainOUPath'].Aliases | Should -Contain 'SearchBase' + } + } + + Context 'CmdletBinding Attributes' { + + It 'Should be an advanced function' { + (Get-Command Get-CCSADSid).CmdletBinding | Should -Be $true + } + + It 'Should have OutputType defined' { + (Get-Command Get-CCSADSid).OutputType.Name | Should -Contain 'System.String' + } + } +} + +Describe 'Get-CCSADSid - Integration Tests' -Tag 'Integration' { + + Context 'DomainOUPath Validation' { + + It 'Should accept empty DomainOUPath' { + $result = Get-CCSADSid -ComputerName $env:COMPUTERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath '' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Not -BeNullOrEmpty + } + + It 'Should accept standard DN format' { + { Get-CCSADSid -ComputerName $env:COMPUTERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'OU=Computers,DC=Firmax,DC=local' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept DC-only format' { + $result = Get-CCSADSid -ComputerName $env:COMPUTERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'DC=Firmax,DC=local' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Not -BeNullOrEmpty + } + + It 'Should accept LDAP format' { + { Get-CCSADSid -ComputerName $env:COMPUTERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'LDAP://DC01.Firmax.local/OU=Computers,DC=Firmax,DC=local' -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should reject invalid format' { + { Get-CCSADSid -ComputerName $env:COMPUTERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'InvalidPath' -ErrorAction Stop } | Should -Throw + } + } + + Context 'URL Validation' { + + It 'Should accept HTTPS URL' { + $result = Get-CCSADSid -ComputerName $env:COMPUTERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Not -BeNullOrEmpty + } + + It 'Should reject HTTP URL (only HTTPS allowed)' { + $httpUrl = $script:TestUrl -replace '^https:', 'http:' + { Get-CCSADSid -ComputerName $env:COMPUTERNAME -Domain $script:TestDomain -Url $httpUrl -CCSCredential $script:TestCCSCredential -ErrorAction Stop } | Should -Throw + } + + It 'Should reject URL without protocol' { + { Get-CCSADSid -ComputerName $env:COMPUTERNAME -Domain $script:TestDomain -Url 'test.com/CCS.asmx' -CCSCredential $script:TestCCSCredential -ErrorAction Stop } | Should -Throw + } + } + + Context 'Domain Validation' { + + It 'Should accept valid domain format' { + $result = Get-CCSADSid -ComputerName $env:COMPUTERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Not -BeNullOrEmpty + } + + It 'Should accept subdomain format' { + { Get-CCSADSid -ComputerName $env:COMPUTERNAME -Domain 'sub.firmax.local' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should reject invalid domain format' { + { Get-CCSADSid -ComputerName $env:COMPUTERNAME -Domain 'invalid_domain' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction Stop } | Should -Throw + } + } + + Context 'Live CCS Web Service Operations' { + + It 'Should connect to CCS Web Service successfully' { + $result = Get-CCSADSid -ComputerName $env:COMPUTERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Not -BeNullOrEmpty + } + + It 'Should retrieve SID for current computer' { + $result = Get-CCSADSid -ComputerName $env:COMPUTERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Not -BeNullOrEmpty + $result | Should -Match '^S-1-5-21-\d+-\d+-\d+-\d+$' + } + + It 'Should retrieve SID with domain credentials' { + $result = Get-CCSADSid -ComputerName $env:COMPUTERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Not -BeNullOrEmpty + $result | Should -Match '^S-1-5-21-\d+-\d+-\d+-\d+$' + } + + It 'Should process multiple computers' { + $computers = @($env:COMPUTERNAME, 'PESTER-TEST-PC') + $results = $computers | Get-CCSADSid -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $results | Should -Not -BeNullOrEmpty + } + + It 'Should handle LDAP format for DomainOUPath' { + $ldapPath = 'LDAP://DC01.Firmax.local/DC=Firmax,DC=local' + { Get-CCSADSid -ComputerName $env:COMPUTERNAME -DomainOUPath $ldapPath -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should retrieve SID from specific OU' { + $ouPath = 'DC=Firmax,DC=local' + $result = Get-CCSADSid -ComputerName $env:COMPUTERNAME -DomainOUPath $ouPath -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Not -BeNullOrEmpty + } + } + + Context 'Error Handling' { + + It 'Should handle non-existent computer gracefully' { + $result = Get-CCSADSid -ComputerName 'NonExistentPC12345' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + # Should either return error message or nothing + } + + It 'Should handle invalid credentials gracefully' { + $badCred = New-Object System.Management.Automation.PSCredential('baduser', (ConvertTo-SecureString 'badpass' -AsPlainText -Force)) + { Get-CCSADSid -ComputerName $env:COMPUTERNAME -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $badCred -ErrorAction Stop } | Should -Throw + } + + It 'Should handle non-existent URL gracefully' { + { Get-CCSADSid -ComputerName $env:COMPUTERNAME -Domain $script:TestDomain -Url 'https://nonexistent.invalid/CCS.asmx' -CCSCredential $script:TestCCSCredential -ErrorAction Stop } | Should -Throw + } + } +} + +Describe 'Get-CCSADSid - Performance Tests' -Tag 'Performance' { + + Context 'Pipeline Performance' { + + It 'Should process pipeline input efficiently' { + $computers = 1..10 | ForEach-Object { $env:COMPUTERNAME } + $measure = Measure-Command { + $computers | Get-CCSADSid -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WarningAction SilentlyContinue -ErrorAction SilentlyContinue + } + $measure.TotalSeconds | Should -BeLessThan 30 + } + } +} diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADSid.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADSid.ps1 new file mode 100644 index 00000000..2031457a --- /dev/null +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSADSid.ps1 @@ -0,0 +1,295 @@ +function Get-CCSADSid { + <# + .SYNOPSIS + Retrieves the Security Identifier (SID) for a computer in Active Directory using the CCS Web Service. + + .DESCRIPTION + Retrieves the Security Identifier (SID) for a computer in Active Directory using the CCS Web Service. + This advanced function includes comprehensive error handling, input validation, and supports pipeline input. + + .PARAMETER ComputerName + The name of the computer to retrieve the SID for. + Supports pipeline input and accepts multiple computer names. + + .PARAMETER DomainOUPath + The Organizational Unit (OU) path in which the computer resides. + If not specified, searches the entire domain. + Supports both standard DN format (OU=Computers,DC=example,DC=com) and LDAP format (LDAP://DC01.example.local/OU=Computers,DC=example,DC=com). + + .PARAMETER Domain + The domain in which the computer resides. + Must be a valid domain name format. + + .PARAMETER Url + The URL of the CCS Web Service. + Must be a valid URI format. Example: "https://example.com/CCSWebservice/CCS.asmx" + + .PARAMETER CCSCredential + The credentials used to authenticate with the CCS Web Service. + + .PARAMETER DomainCredential + The credentials of an account with permissions to query Active Directory. + If not defined, it will run in the CCS Web Service context. + + .PARAMETER PasswordIsEncrypted + Indicates if the password in the DomainCredential is encrypted. Default is $false. + + .EXAMPLE + PS C:\> Get-CCSADSid -ComputerName "TestPC" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Retrieves the SID for TestPC using default CCS context. + + .EXAMPLE + PS C:\> Get-CCSADSid -ComputerName "TestPC" -DomainOUPath "OU=Computers,DC=example,DC=com" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential -DomainCredential $DomainCredential + + Retrieves the SID for TestPC using specific domain credentials and OU path. + + .EXAMPLE + PS C:\> "PC01", "PC02", "PC03" | Get-CCSADSid -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Retrieves SIDs for multiple computers using pipeline input. + + .EXAMPLE + PS C:\> Get-CCSADSid -ComputerName "TestPC" -DomainOUPath "LDAP://DC01.Firmax.local/DC=FirmaX,DC=local" -Domain "Firmax.local" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Retrieves the SID using LDAP format for the OU path. The LDAP path will be automatically converted to standard DN format. + + .OUTPUTS + System.String + Returns the Security Identifier (SID) for the specified computer. + + .NOTES + This is an advanced function with support for pipeline input and comprehensive error handling. + #> + [CmdletBinding()] + [OutputType([string])] + param ( + [Parameter( + Mandatory = $true, + Position = 0, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the computer name to retrieve the SID for' + )] + [ValidateNotNullOrEmpty()] + [Alias('Name', 'Computer', 'CN')] + [string[]]$ComputerName, + + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] + [ValidateScript({ + if ([string]::IsNullOrEmpty($_)) { + return $true + } + # Support standard DN format: OU=...,DC=...,DC=... + if ($_ -match '^(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + # Support LDAP format: LDAP://server/DN + if ($_ -match '^LDAP://[^/]+/(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + throw "DomainOUPath must be in format 'OU=...,DC=...,DC=...' or 'LDAP://server/OU=...,DC=...,DC=...' or empty" + })] + [Alias('OU', 'Path', 'SearchBase')] + [string]$DomainOUPath = '', + + [Parameter( + Mandatory = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the domain name' + )] + [ValidateNotNullOrEmpty()] + [ValidatePattern('^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$')] + [string]$Domain, + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service URL' + )] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + if ($_ -match '^https://') { + $true + } else { + throw "URL must start with https:// (secure connection required)" + } + })] + [Alias('WebServiceUrl', 'Uri')] + [string]$Url, + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service credentials' + )] + [ValidateNotNull()] + [Alias('Credential', 'WebServiceCredential')] + [pscredential]$CCSCredential, + + [Parameter(Mandatory = $false)] + [ValidateNotNull()] + [Alias('ADCredential')] + [pscredential]$DomainCredential, + + [Parameter(Mandatory = $false)] + [Alias('Encrypted')] + [switch]$PasswordIsEncrypted + ) + + begin { + $FunctionName = $PSCmdlet.MyInvocation.MyCommand.Name + + Write-Verbose "[$FunctionName] Starting function execution" + Write-Verbose "[$FunctionName] Domain: $Domain" + Write-Verbose "[$FunctionName] DomainOUPath: $DomainOUPath" + Write-Verbose "[$FunctionName] URL: $Url" + + # Initialize CCS connection once in begin block + try { + Write-Verbose "[$FunctionName] Initializing CCS Web Service connection" + $CCS = Initialize-CCS -Url $Url -WebServiceCredential $CCSCredential -ErrorAction Stop + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to initialize CCS Web Service: $_" ` + -ErrorCategory ConnectionError ` + -TargetObject $Url ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Verify the URL is correct and the CCS Web Service is accessible. Check credentials." + } + + # Prepare domain credentials if provided + $ADUsername = $null + $ADPassword = $null + + if ($DomainCredential) { + $ADUsername = $DomainCredential.UserName + Write-Verbose "[$FunctionName] Using domain credential: $ADUsername" + + try { + if ($PasswordIsEncrypted) { + $ADPassword = $DomainCredential.GetNetworkCredential().Password + Write-Verbose "[$FunctionName] Using pre-encrypted password" + } else { + $ADPassword = Get-CCSEncryptedPassword -SecureString $DomainCredential.Password -ErrorAction Stop + Write-Verbose "[$FunctionName] Password encrypted successfully" + } + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to process domain credential password: $_" ` + -ErrorCategory SecurityError ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Ensure the domain credential is valid and the password can be encrypted." + } + } else { + Write-Verbose "[$FunctionName] No domain credential provided, using CCS Web Service context" + } + + $ProcessedCount = 0 + $SuccessCount = 0 + $FailureCount = 0 + } + + process { + foreach ($Computer in $ComputerName) { + $ProcessedCount++ + + try { + Write-Verbose "[$FunctionName] Processing computer: $Computer ($ProcessedCount)" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Processing: ComputerName=$Computer, DomainOUPath=$DomainOUPath, Domain=$Domain") + } + + Write-Verbose "[$FunctionName] Calling ActiveDirectory_GetSID for $Computer" + + $Result = $CCS.ActiveDirectory_GetSID( + $Computer, + $DomainOUPath, + $Domain, + $ADUsername, + $ADPassword + ) + + Write-Debug "Result from CCS Web Service: $Result" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Result for $Computer : $Result") + } + + Write-Verbose "[$FunctionName] Result for $Computer : $Result" + + # Check for errors in result + $IsError = Invoke-CCSIsError -Result $Result + + Write-Debug "IsError evaluation for result: $IsError" + if ($IsError) { + $FailureCount++ + + # Determine appropriate error category based on result message + $ErrorCat = [System.Management.Automation.ErrorCategory]::OperationStopped + $RecommendedActionText = "Verify the computer name is correct." + + if ($Result -like '*does not exist*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::ObjectNotFound + $RecommendedActionText = "Verify that the computer '$Computer' exists in Active Directory." + } elseif ($Result -like '*unwilling to process*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::PermissionDenied + $RecommendedActionText = "Check that the domain credentials have sufficient permissions to query Active Directory." + } + + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + Write-Debug "ErrorAction from PSBoundParameters: $($PSBoundParameters['ErrorAction'])" + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + Write-Debug "ErrorAction from ErrorActionPreference: $ErrorActionPreference" + } + Write-Debug "ShouldThrow is set to: $ShouldThrow" + + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to retrieve SID for computer '$Computer': $Result" ` + -ErrorCategory $ErrorCat ` + -TargetObject $Computer ` + -FunctionName $FunctionName ` + -RecommendedAction $RecommendedActionText ` + -Throw:$ShouldThrow + } else { + $SuccessCount++ + Write-Verbose "[$FunctionName] Successfully retrieved SID for $Computer" + Write-Output $Result + } + } catch { + $FailureCount++ + + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + } + + Invoke-CCSErrorHandling ` + -ErrorMessage "Exception occurred while processing computer '$Computer': $_" ` + -ErrorCategory InvalidOperation ` + -TargetObject $Computer ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Check the error details and verify all parameters are correct." ` + -Throw:$ShouldThrow + } + } + } + + end { + Write-Verbose "[$FunctionName] Function execution completed" + Write-Verbose "[$FunctionName] Total processed: $ProcessedCount | Success: $SuccessCount | Failed: $FailureCount" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Summary - Processed: $ProcessedCount, Success: $SuccessCount, Failed: $FailureCount") + } + } +} diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSEncryptedPassword.Tests.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSEncryptedPassword.Tests.ps1 index 3f4631df..fb805295 100644 --- a/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSEncryptedPassword.Tests.ps1 +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSEncryptedPassword.Tests.ps1 @@ -1,12 +1,54 @@ BeforeAll { - . $PSCommandPath.Replace('.Tests.ps1', '.ps1') + $FunctionPath = $PSCommandPath -replace '\.Tests\.ps1$', '.ps1' + . $FunctionPath + $ValidPlain = 'Admin1234' + $ValidSecure = ConvertTo-SecureString $ValidPlain -AsPlainText -Force + $ExpectedEncrypted = 'mOQXCLuC/rSkIrAQU3Ttbg==' +} - $Plain = "Admin1234" - $Encrypted = "mOQXCLuC/rSkIrAQU3Ttbg==" +Describe 'Get-CCSEncryptedPassword - Advanced Function' -Tag 'Integration' { + Context 'Parameter Validation' { + It 'Throws if SecureString is missing' { + { Get-CCSEncryptedPassword } | Should -Throw + } + It 'Throws if SecureString is empty or whitespace' { + # PowerShell throws if you try to create a SecureString from an empty string, so test whitespace instead + $white = ConvertTo-SecureString ' ' -AsPlainText -Force + { Get-CCSEncryptedPassword -SecureString $white } | Should -Throw + } + } + Context 'Pipeline Support' { + It 'Accepts input from pipeline' { + $result = $ValidSecure | Get-CCSEncryptedPassword + $result | Should -Be $ExpectedEncrypted + } + } + Context 'Encryption Logic' { + It 'Returns expected encrypted string for known input' { + $result = Get-CCSEncryptedPassword -SecureString $ValidSecure + $result | Should -Be $ExpectedEncrypted + } + } + Context 'Error Handling' { + It 'Throws if InstallationScreen.exe is missing' { + $oldPath = $env:TEMP + $script:origExe = (Join-Path (Split-Path $FunctionPath -Parent) 'Dependencies' 'InstallationScreen.exe') + if (Test-Path $script:origExe) { + Rename-Item $script:origExe ($script:origExe + '.bak') + } + try { + { Get-CCSEncryptedPassword -SecureString $ValidSecure } | Should -Throw + } finally { + if (Test-Path ($script:origExe + '.bak')) { + Rename-Item ($script:origExe + '.bak') $script:origExe + } + } + } + } + Context 'Output Type' { + It 'Returns a string' { + $result = Get-CCSEncryptedPassword -SecureString $ValidSecure + $result | Should -BeOfType 'System.String' + } + } } -Describe 'Testing Get-CCSEncryptedPassword' { - It "Should return $Encrypted" { - $Result = Get-CCSEncryptedPassword -String $Plain - $Result | Should -Be $Encrypted - } -} \ No newline at end of file diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSEncryptedPassword.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSEncryptedPassword.ps1 index 85ea6005..a3cb0f81 100644 --- a/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSEncryptedPassword.ps1 +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Get-CCSEncryptedPassword.ps1 @@ -1,41 +1,97 @@ -<# +function Get-CCSEncryptedPassword { + <# .SYNOPSIS - This function encrypts a string using the InstallationScreen.exe utility. + Encrypts a SecureString using the InstallationScreen.exe utility for CCS Webservice use. .DESCRIPTION - This function takes a string as input and uses the InstallationScreen.exe utility to encrypt it. - The encrypted string is returned as output and used multiple times, when working with the CCS Webservice. + Takes a SecureString and uses the InstallationScreen.exe utility to encrypt it. Returns the encrypted string, suitable for use with CCS Webservice operations. Includes robust error handling, parameter validation, and advanced function features. - .PARAMETER String - The string to be encrypted. + .PARAMETER SecureString + The SecureString to encrypt. Must not be empty. .EXAMPLE - PS C:\> Get-CCSEncryptedPassword -String "Admin1234" -#> -function Get-CCSEncryptedPassword { + $secure = ConvertTo-SecureString "Admin1234" -AsPlainText -Force + Get-CCSEncryptedPassword -SecureString $secure + + .OUTPUTS + System.String. The encrypted string. + + .NOTES + Advanced function with CmdletBinding, error handling, and pipeline support. + #> + [CmdletBinding(SupportsShouldProcess = $false, ConfirmImpact = 'None')] + [OutputType([string])] param ( - [Parameter(Mandatory = $true)] - [string]$String + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, HelpMessage = 'Enter the SecureString to encrypt')] + [ValidateNotNull()] + [Alias('Password','Secret')] + [System.Security.SecureString]$SecureString ) - $OutputPath = Join-Path $env:TEMP 'InstallationScreen.log' - try { + begin { + $FunctionName = $PSCmdlet.MyInvocation.MyCommand.Name $ExePath = Join-Path $PSScriptRoot 'Dependencies' 'InstallationScreen.exe' - $Arguments = "power $String" + $OutputPath = Join-Path $env:TEMP 'InstallationScreen.log' + Write-Verbose "[$FunctionName] Using InstallationScreen.exe at: $ExePath" + } - if (Test-Path $OutputPath) { - Remove-Item $OutputPath -Force + process { + # Convert SecureString to plain text + try { + $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString) + $Plain = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($BSTR) + } catch { + $msg = "[$FunctionName] Failed to convert SecureString to plain text: $_" + Write-Error $msg + throw $msg + } finally { + if ($BSTR) { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR) } + } + Write-Verbose "[$FunctionName] Encrypting string (length: $($Plain.Length))" + if ([string]::IsNullOrWhiteSpace($Plain)) { + $msg = "Input SecureString is empty or whitespace." + Write-Error $msg + throw $msg + } + if (-not (Test-Path $ExePath)) { + $msg = "InstallationScreen.exe not found at $ExePath" + Write-Error $msg + throw $msg } - - Start-Process -FilePath $ExePath -ArgumentList $Arguments -Wait -RedirectStandardOutput $OutputPath -NoNewWindow - $Output = Get-Content $OutputPath - - return $Output.Trim() -replace '\r?\n', '' - } catch { - <#Do this if a terminating exception happens#> - } finally { if (Test-Path $OutputPath) { - Remove-Item $OutputPath -Force + Remove-Item $OutputPath -Force -ErrorAction SilentlyContinue + } + $Arguments = "power $Plain" + try { + Write-Debug "[$FunctionName] Running: $ExePath $Arguments" + $proc = Start-Process -FilePath $ExePath -ArgumentList $Arguments -Wait -RedirectStandardOutput $OutputPath -NoNewWindow -PassThru + if ($proc.ExitCode -ne 0) { + $msg = "InstallationScreen.exe exited with code $($proc.ExitCode)" + Write-Error $msg + throw $msg + } + if (-not (Test-Path $OutputPath)) { + $msg = "Output file not created: $OutputPath" + Write-Error $msg + throw $msg + } + $Output = Get-Content $OutputPath -Raw + $Encrypted = $Output.Trim() -replace '\r?\n', '' + if ([string]::IsNullOrWhiteSpace($Encrypted)) { + $msg = "No encrypted output returned from InstallationScreen.exe." + Write-Error $msg + throw $msg + } + Write-Verbose "[$FunctionName] Encrypted string: $Encrypted" + Write-Output $Encrypted + } catch { + $msg = "[$FunctionName] Failed to encrypt string: $_" + Write-Error $msg + throw $msg + } finally { + if (Test-Path $OutputPath) { + Remove-Item $OutputPath -Force -ErrorAction SilentlyContinue + } } } } \ No newline at end of file diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Initialize-CCS.Tests.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Initialize-CCS.Tests.ps1 index 28d7425f..2240c48a 100644 --- a/Modules/Capa.PowerShell.Module.CCS/Dev/Initialize-CCS.Tests.ps1 +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Initialize-CCS.Tests.ps1 @@ -1,23 +1,135 @@ BeforeAll { - . $PSCommandPath.Replace('.Tests.ps1', '.ps1') + $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent - $Url1 = "https://mracapa02.capainstaller.com/CCSWebservice/CCS.asmx" - $Url2 = "http://mracapa02.capainstaller.com/CCSWebservice/CCS.asmx" + $Items = Get-ChildItem -Path "$RootPath\Capa.PowerShell.Module.CCS\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + + # Setup test environment + $script:TestUrl = "https://$(hostname).capainstaller.com/CCSWebservice/CCS.asmx" + $script:TestUrlHttp = "http://$(hostname).capainstaller.com/CCSWebservice/CCS.asmx" + $CredentialPath = "D:\PowerShell\Credentials\$($env:USERNAME)DomainAdminPesterTests.xml" - $User = "svc_capawebservice" - $Password = "Admin1234" + # Setup credentials from environment variables (GitHub secrets) + if ($env:DOMAINADMINUSERNAME -and $env:DOMAINADMINPASSWORD) { + $securePassword = ConvertTo-SecureString $env:DOMAINADMINPASSWORD -AsPlainText -Force + $script:TestCredential = New-Object System.Management.Automation.PSCredential($env:DOMAINADMINUSERNAME, $securePassword) + } elseif (Test-Path -Path $CredentialPath) { + $script:TestCredential = Import-Clixml -Path $CredentialPath + } else { + $script:TestCredential = Get-Credential -Message 'Enter domain admin credentials for integration tests' + } - $SecurePassword = ConvertTo-SecureString -String $Password -AsPlainText -Force - $Credential = New-Object System.Management.Automation.PSCredential($User, $SecurePassword) + $DebugPreference = 'Continue' + $ErrorActionPreference = 'Stop' } -Describe "Tests that should work" { - It "Should return a obejct" { - $Client = Initialize-CCS -Url $Url1 -WebServiceCredential $Credential - $Client | Should -BeOfType CapaProxy.CCSSoapClient + +Describe 'Initialize-CCS' -Tag 'Unit' { + + Context 'Parameter Validation' { + It 'Should have mandatory Url parameter' { + (Get-Command Initialize-CCS).Parameters['Url'].Attributes.Mandatory | Should -Be $true + } + It 'Should have mandatory WebServiceCredential parameter' { + (Get-Command Initialize-CCS).Parameters['WebServiceCredential'].Attributes.Mandatory | Should -Be $true + } + } + + Context 'Parameter Aliases' { + It 'Should have alias "Uri" for Url' { + (Get-Command Initialize-CCS).Parameters['Url'].Aliases | Should -Contain 'Uri' + } + It 'Should have alias "WebServiceUrl" for Url' { + (Get-Command Initialize-CCS).Parameters['Url'].Aliases | Should -Contain 'WebServiceUrl' + } + It 'Should have alias "Credential" for WebServiceCredential' { + (Get-Command Initialize-CCS).Parameters['WebServiceCredential'].Aliases | Should -Contain 'Credential' + } + It 'Should have alias "Cred" for WebServiceCredential' { + (Get-Command Initialize-CCS).Parameters['WebServiceCredential'].Aliases | Should -Contain 'Cred' + } + } + + Context 'URL Validation' { + It 'Should accept HTTPS URL' { + { Initialize-CCS -Url $script:TestUrl -WebServiceCredential $script:TestCredential -ErrorAction Stop } | Should -Not -Throw + } + It 'Should reject HTTP URL (only HTTPS allowed)' { + { Initialize-CCS -Url $script:TestUrlHttp -WebServiceCredential $script:TestCredential } | Should -Throw + } + It 'Should reject URL without protocol' { + { Initialize-CCS -Url 'test.com/CCS.asmx' -WebServiceCredential $script:TestCredential } | Should -Throw + } + It 'Should throw if Url is empty' { + { Initialize-CCS -Url '' -WebServiceCredential $script:TestCredential } | Should -Throw + } + } + + Context 'Input Validation' { + It 'Should have Url parameter marked as mandatory' { + (Get-Command Initialize-CCS).Parameters['Url'].Attributes.Mandatory | Should -Be $true + } + It 'Should have WebServiceCredential parameter marked as mandatory' { + (Get-Command Initialize-CCS).Parameters['WebServiceCredential'].Attributes.Mandatory | Should -Be $true + } + } + + Context 'ShouldProcess & CmdletBinding' { + It 'Should be an advanced function' { + (Get-Command Initialize-CCS).CmdletBinding | Should -Be $true + } + It 'Should not support WhatIf' { + (Get-Command Initialize-CCS).Parameters.ContainsKey('WhatIf') | Should -Be $false + } + It 'Should not support Confirm' { + (Get-Command Initialize-CCS).Parameters.ContainsKey('Confirm') | Should -Be $false + } + } +} + +Describe 'Initialize-CCS - Integration Tests' -Tag 'Integration' { + + Context 'Live CCS Web Service Operations' { + It 'Should connect to CCS Web Service successfully' { + $client = Initialize-CCS -Url $script:TestUrl -WebServiceCredential $script:TestCredential -ErrorAction Stop + $client | Should -Not -BeNullOrEmpty + } + It 'Should return a CapaProxy.CCSSoapClient object for valid input' { + $client = Initialize-CCS -Url $script:TestUrl -WebServiceCredential $script:TestCredential + $client | Should -BeOfType CapaProxy.CCSSoapClient + } + It 'Should set client credentials correctly' { + $client = Initialize-CCS -Url $script:TestUrl -WebServiceCredential $script:TestCredential + $client.ClientCredentials.UserName.UserName | Should -Be $script:TestCredential.UserName + } + } + + Context 'Error Handling' { + It 'Should throw if DLL is missing' { + $dllPath = Join-Path (Split-Path $PSCommandPath -Parent) 'Dependencies' 'CCSProxy.dll' + if (Test-Path $dllPath) { + Rename-Item $dllPath ($dllPath + '.bak') + } + try { + { Initialize-CCS -Url $script:TestUrl -WebServiceCredential $script:TestCredential -ErrorAction Stop } | Should -Throw + } finally { + if (Test-Path ($dllPath + '.bak')) { + Rename-Item ($dllPath + '.bak') $dllPath + } + } + } } } -Describe "Tests that should fail" { - It "Should throw an exception" { - { Initialize-CCS -Url $Url2 -WebServiceCredential $Credential } | Should -Throw + +Describe 'Initialize-CCS - Performance Tests' -Tag 'Performance' { + + Context 'Initialization Performance' { + It 'Should initialize client in under 3 seconds' { + $measure = Measure-Command { + $client = Initialize-CCS -Url $script:TestUrl -WebServiceCredential $script:TestCredential + } + $measure.TotalSeconds | Should -BeLessThan 3 + } } } \ No newline at end of file diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Initialize-CCS.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Initialize-CCS.ps1 index 9aa49691..efd2d871 100644 --- a/Modules/Capa.PowerShell.Module.CCS/Dev/Initialize-CCS.ps1 +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Initialize-CCS.ps1 @@ -1,42 +1,122 @@ -<# +function Initialize-CCS { + <# .SYNOPSIS - This function initializes the CCS Webservice client. + Initializes the CCS Web Service client for secure communication. .DESCRIPTION - This function initializes the CCS Webservice client by loading the necessary DLL and setting up the binding and endpoint. - It also sets the client credentials for authentication. + Initializes the CCS Web Service client by loading the necessary DLL, setting up the binding and endpoint, and configuring client credentials for authentication. + This advanced function includes comprehensive error handling, input validation, and verbose/debug output. .PARAMETER Url - The URL of the CCS Webservice. + The URL of the CCS Web Service. Must be a valid HTTPS URI format. + Example: "https://example.com/CCSWebservice/CCS.asmx" .PARAMETER WebServiceCredential - The credentials used to authenticate with the CCS Webservice. + The credentials used to authenticate with the CCS Web Service. .EXAMPLE PS C:\> Initialize-CCS -Url "https://example.com/CCSWebservice/CCS.asmx" -WebServiceCredential $Credential -#> -function Initialize-CCS { + + Initializes the CCS client with the specified URL and credentials. + + .EXAMPLE + PS C:\> $client = Initialize-CCS -Url $url -Credential $cred -Verbose + + Initializes the CCS client with verbose output, using the Credential alias. + + .OUTPUTS + CapaProxy.CCSSoapClient + Returns the initialized CCS SOAP client object. + + .NOTES + This is an advanced function with comprehensive error handling, parameter validation, and verbose output. + #> + [CmdletBinding()] param ( - [Parameter(Mandatory = $true)] - [string]$Url, - [Parameter(Mandatory = $true)] - [pscredential]$WebServiceCredential - ) - if ($Url -notlike 'https://*') { - throw "Invalid URL format. Please provide a valid HTTPS URL." + [Parameter( + Mandatory = $true, + Position = 0, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the CCS Web Service URL' + )] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + if ($_ -match '^https://') { + $true + } else { + throw "URL must start with https:// (secure connection required)" + } + })] + [Alias('Uri', 'WebServiceUrl')] + [string]$Url, + + [Parameter( + Mandatory = $true, + Position = 1, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the CCS Web Service credentials' + )] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + if ($_ -is [System.Management.Automation.PSCredential]) { + $true + } else { + throw "WebServiceCredential must be a PSCredential object" + } + })] + [Alias('Credential', 'Cred')] + [pscredential]$WebServiceCredential +) begin { + $FunctionName = $PSCmdlet.MyInvocation.MyCommand.Name + Write-Verbose "[$FunctionName] Starting function execution" + Write-Verbose "[$FunctionName] URL: $Url" + Write-Verbose "[$FunctionName] Username: $($WebServiceCredential.UserName)" } - $DllPath = Join-Path $PSScriptRoot 'Dependencies' 'CCSProxy.dll' - Add-Type -Path $DllPath + process { + try { + # Validate and load DLL + $DllPath = Join-Path $PSScriptRoot 'Dependencies' 'CCSProxy.dll' + Write-Verbose "[$FunctionName] DLL Path: $DllPath" - $binding = New-Object System.ServiceModel.BasicHttpBinding - $binding.Security.Mode = [System.ServiceModel.BasicHttpSecurityMode]::Transport - $binding.Security.Transport.ClientCredentialType = [System.ServiceModel.HttpClientCredentialType]::Basic + if (-not (Test-Path $DllPath)) { + $msg = "CCSProxy.dll not found at $DllPath" + Write-Error $msg + throw $msg + } - $endpoint = New-Object System.ServiceModel.EndpointAddress($Url) - $client = New-Object CapaProxy.CCSSoapClient($binding, $endpoint) - $client.ClientCredentials.UserName.UserName = $WebServiceCredential.UserName - $client.ClientCredentials.UserName.Password = $WebServiceCredential.GetNetworkCredential().Password + Write-Debug "[$FunctionName] Loading CCSProxy.dll" + Add-Type -Path $DllPath -ErrorAction Stop - return $client + # Configure binding + Write-Debug "[$FunctionName] Configuring BasicHttpBinding" + $binding = New-Object System.ServiceModel.BasicHttpBinding + $binding.Security.Mode = [System.ServiceModel.BasicHttpSecurityMode]::Transport + $binding.Security.Transport.ClientCredentialType = [System.ServiceModel.HttpClientCredentialType]::Basic + + # Create endpoint + Write-Debug "[$FunctionName] Creating endpoint for $Url" + $endpoint = New-Object System.ServiceModel.EndpointAddress($Url) + + # Create client + Write-Verbose "[$FunctionName] Initializing CCS SOAP client" + $client = New-Object CapaProxy.CCSSoapClient($binding, $endpoint) + + # Set credentials + $client.ClientCredentials.UserName.UserName = $WebServiceCredential.UserName + $client.ClientCredentials.UserName.Password = $WebServiceCredential.GetNetworkCredential().Password + + Write-Verbose "[$FunctionName] CCS client initialized successfully" + Write-Output $client + + } catch { + $msg = "[$FunctionName] Failed to initialize CCS Web Service client: $_" + Write-Error $msg + throw $msg + } + } + + end { + Write-Verbose "[$FunctionName] Function execution completed" + } } \ No newline at end of file diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Invoke-CCSErrorHandling.Tests.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Invoke-CCSErrorHandling.Tests.ps1 new file mode 100644 index 00000000..e5a77c6c --- /dev/null +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Invoke-CCSErrorHandling.Tests.ps1 @@ -0,0 +1,263 @@ +BeforeAll { + $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent + + $Items = Get-ChildItem -Path "$RootPath\Capa.PowerShell.Module.CCS\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } +} + +Describe 'Invoke-CCSErrorHandling' -Tag 'Unit' { + + Context 'Parameter Validation' { + + It 'Should have mandatory ErrorMessage parameter' { + (Get-Command Invoke-CCSErrorHandling).Parameters['ErrorMessage'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have optional ErrorCategory parameter' { + (Get-Command Invoke-CCSErrorHandling).Parameters['ErrorCategory'].Attributes.Mandatory | Should -Be $false + } + + It 'Should have optional TargetObject parameter' { + (Get-Command Invoke-CCSErrorHandling).Parameters['TargetObject'].Attributes.Mandatory | Should -Be $false + } + + It 'Should have optional FunctionName parameter' { + (Get-Command Invoke-CCSErrorHandling).Parameters['FunctionName'].Attributes.Mandatory | Should -Be $false + } + + It 'Should have optional RecommendedAction parameter' { + (Get-Command Invoke-CCSErrorHandling).Parameters['RecommendedAction'].Attributes.Mandatory | Should -Be $false + } + + It 'Should validate ErrorCategory values' { + $param = (Get-Command Invoke-CCSErrorHandling).Parameters['ErrorCategory'] + $validateSet = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] } + $validateSet.ValidValues | Should -Contain 'ConnectionError' + $validateSet.ValidValues | Should -Contain 'AuthenticationError' + $validateSet.ValidValues | Should -Contain 'ObjectNotFound' + } + } + + Context 'Error Throwing Behavior' { + + It 'Should throw a terminating error by default' { + { Invoke-CCSErrorHandling -ErrorMessage "Test error" } | Should -Throw + } + + It 'Should throw with custom error category' { + { Invoke-CCSErrorHandling -ErrorMessage "Connection failed" -ErrorCategory ConnectionError } | Should -Throw -ErrorId "*ConnectionError*" + } + + It 'Should write non-terminating error when Throw is false' { + $ErrorActionPreference = 'SilentlyContinue' + { Invoke-CCSErrorHandling -ErrorMessage "Test warning" -Throw:$false -ErrorAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should include error message in exception' { + try { + Invoke-CCSErrorHandling -ErrorMessage "Custom error message" + } catch { + $_.Exception.Message | Should -BeLike "*Custom error message*" + } + } + } + + Context 'Function Name Detection' { + + It 'Should auto-detect calling function name' { + function Test-CallingFunction { + Invoke-CCSErrorHandling -ErrorMessage "Auto-detect test" + } + + try { + Test-CallingFunction + } catch { + $_.Exception.Message | Should -BeLike "*Test-CallingFunction*" + } + } + + It 'Should use provided function name when specified' { + try { + Invoke-CCSErrorHandling -ErrorMessage "Custom function" -FunctionName "MyCustomFunction" + } catch { + $_.Exception.Message | Should -BeLike "*MyCustomFunction*" + } + } + } + + Context 'Error Record Properties' { + + It 'Should set TargetObject correctly' { + $testObject = "TestTarget" + try { + Invoke-CCSErrorHandling -ErrorMessage "Target test" -TargetObject $testObject + } catch { + $_.TargetObject | Should -Be $testObject + } + } + + It 'Should set ErrorCategory correctly' { + try { + Invoke-CCSErrorHandling -ErrorMessage "Category test" -ErrorCategory AuthenticationError + } catch { + $_.CategoryInfo.Category | Should -Be ([System.Management.Automation.ErrorCategory]::AuthenticationError) + } + } + + It 'Should include recommended action when provided' { + try { + Invoke-CCSErrorHandling -ErrorMessage "Action test" -RecommendedAction "Try again with valid credentials" + } catch { + $_.ErrorDetails.RecommendedAction | Should -BeLike "*Try again with valid credentials*" + } + } + + It 'Should create proper ErrorId format' { + try { + Invoke-CCSErrorHandling -ErrorMessage "ID test" -FunctionName "TestFunction" -ErrorCategory ObjectNotFound + } catch { + $_.FullyQualifiedErrorId | Should -BeLike "CCS.TestFunction.ObjectNotFound" + } + } + } + + Context 'Global Cs Logging' { + + BeforeEach { + # Mock global Cs object with Add-Member for method + $Global:Cs = [PSCustomObject]@{ + LogMessages = @() + } + + $Global:Cs | Add-Member -MemberType ScriptMethod -Name Job_WriteLog -Value { + param($message, $isError) + $this.LogMessages += @{ Message = $message; IsError = $isError } + } -Force + } + + AfterEach { + Remove-Variable -Name Cs -Scope Global -ErrorAction SilentlyContinue + } + + It 'Should log to global Cs object when available' { + try { + Invoke-CCSErrorHandling -ErrorMessage "Cs logging test" -FunctionName "TestFunc" + } catch { + # Error was thrown, check if it was logged + } + $Global:Cs.LogMessages.Count | Should -BeGreaterThan 0 + $Global:Cs.LogMessages[0].Message | Should -BeLike "*Cs logging test*" + $Global:Cs.LogMessages[0].IsError | Should -Be $true + } + + It 'Should not log when LogToCs is false' { + try { + Invoke-CCSErrorHandling -ErrorMessage "No logging test" -LogToCs:$false + } catch { + # Error was thrown + } + $Global:Cs.LogMessages.Count | Should -Be 0 + } + + It 'Should not fail when Cs object is not available' { + Remove-Variable -Name Cs -Scope Global -ErrorAction SilentlyContinue + { + try { + Invoke-CCSErrorHandling -ErrorMessage "No Cs test" + } catch { + # Expected to throw, but not because of missing Cs + } + } | Should -Not -Throw + } + } + + Context 'Exception Wrapping' { + + It 'Should accept and wrap an existing exception' { + $originalException = New-Object System.UnauthorizedAccessException("Access denied") + + try { + Invoke-CCSErrorHandling -ErrorMessage "Wrapped error" -Exception $originalException + } catch { + $_.Exception | Should -BeOfType [System.UnauthorizedAccessException] + $_.Exception.Message | Should -Be "Access denied" + } + } + + It 'Should create new exception when none provided' { + try { + Invoke-CCSErrorHandling -ErrorMessage "New exception" + } catch { + $_.Exception | Should -Not -BeNullOrEmpty + $_.Exception.GetType().Name | Should -Be 'InvalidOperationException' + } + } + } + + Context 'Different Error Categories' { + + It 'Should handle ConnectionError category' { + { Invoke-CCSErrorHandling -ErrorMessage "Connection failed" -ErrorCategory ConnectionError } | Should -Throw + } + + It 'Should handle AuthenticationError category' { + { Invoke-CCSErrorHandling -ErrorMessage "Auth failed" -ErrorCategory AuthenticationError } | Should -Throw + } + + It 'Should handle ObjectNotFound category' { + { Invoke-CCSErrorHandling -ErrorMessage "Not found" -ErrorCategory ObjectNotFound } | Should -Throw + } + + It 'Should handle PermissionDenied category' { + { Invoke-CCSErrorHandling -ErrorMessage "Access denied" -ErrorCategory PermissionDenied } | Should -Throw + } + + It 'Should handle InvalidArgument category' { + { Invoke-CCSErrorHandling -ErrorMessage "Invalid input" -ErrorCategory InvalidArgument } | Should -Throw + } + } +} + +Describe 'Invoke-CCSErrorHandling - Integration with Other Functions' -Tag 'Integration' { + + Context 'Real-world Usage Scenarios' { + + It 'Should provide useful error information for debugging' { + function Test-RealWorldScenario { + param($ComputerName) + + if ($ComputerName -eq 'INVALID') { + Invoke-CCSErrorHandling ` + -ErrorMessage "Computer '$ComputerName' not found in Active Directory" ` + -ErrorCategory ObjectNotFound ` + -TargetObject $ComputerName ` + -RecommendedAction "Verify that the computer exists in AD and the name is spelled correctly" + } + } + + try { + Test-RealWorldScenario -ComputerName 'INVALID' + } catch { + $_.Exception.Message | Should -BeLike "*INVALID*" + $_.CategoryInfo.Category | Should -Be ([System.Management.Automation.ErrorCategory]::ObjectNotFound) + $_.TargetObject | Should -Be 'INVALID' + $_.ErrorDetails.RecommendedAction | Should -BeLike "*Verify that the computer exists*" + } + } + + It 'Should work well with try-catch blocks' { + $ErrorCaught = $false + + try { + Invoke-CCSErrorHandling -ErrorMessage "Test try-catch" -ErrorCategory OperationStopped + } catch { + $ErrorCaught = $true + $_.Exception.Message | Should -BeLike "*Test try-catch*" + } + + $ErrorCaught | Should -Be $true + } + } +} diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Invoke-CCSErrorHandling.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Invoke-CCSErrorHandling.ps1 new file mode 100644 index 00000000..d2a1bb67 --- /dev/null +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Invoke-CCSErrorHandling.ps1 @@ -0,0 +1,151 @@ +function Invoke-CCSErrorHandling { + <# + .SYNOPSIS + Throws a structured error with proper error records for CCS operations. + + .DESCRIPTION + This function creates and throws a properly formatted PowerShell error with appropriate + error categories, target objects, and detailed error messages. It also logs to the global + Cs object if available and supports different error severity levels. + + .PARAMETER ErrorMessage + The main error message to display. + + .PARAMETER ErrorCategory + The PowerShell error category. Default is 'OperationStopped'. + Valid values: NotSpecified, OpenError, CloseError, DeviceError, DeadlockDetected, InvalidArgument, + InvalidData, InvalidOperation, InvalidResult, InvalidType, MetadataError, NotImplemented, + NotInstalled, ObjectNotFound, OperationStopped, OperationTimeout, SyntaxError, ParserError, + PermissionDenied, ResourceBusy, ResourceExists, ResourceUnavailable, ReadError, WriteError, + FromStdErr, SecurityError, ProtocolError, ConnectionError, AuthenticationError, LimitsExceeded, + QuotaExceeded, NotEnabled. + + .PARAMETER TargetObject + The object that was being processed when the error occurred. + + .PARAMETER FunctionName + The name of the function where the error occurred. If not specified, uses the calling function's name. + + .PARAMETER Exception + An existing exception object to wrap in the error record. + + .PARAMETER RecommendedAction + Recommended action for the user to resolve the error. + + .PARAMETER LogToCs + Whether to log the error to the global Cs object. Default is $true. + + .PARAMETER Throw + Whether to throw the error (terminating) or write it as a non-terminating error. Default is $true (throw). + + .EXAMPLE + PS C:\> Invoke-CCSErrorHandling -ErrorMessage "Failed to connect to CCS Web Service" -ErrorCategory ConnectionError -TargetObject $Url + + Throws a connection error with the specified message. + + .EXAMPLE + PS C:\> Invoke-CCSErrorHandling -ErrorMessage "Computer not found in AD" -ErrorCategory ObjectNotFound -TargetObject $ComputerName -RecommendedAction "Verify the computer name exists in Active Directory" + + Throws an object not found error with a recommended action. + + .EXAMPLE + PS C:\> Invoke-CCSErrorHandling -ErrorMessage "Invalid credentials" -ErrorCategory AuthenticationError -Throw:$false + + Writes a non-terminating authentication error. + + .OUTPUTS + None. This function either throws a terminating error or writes a non-terminating error. + + .NOTES + This function provides consistent error handling across all CCS module functions. + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, Position = 0)] + [ValidateNotNullOrEmpty()] + [string]$ErrorMessage, + + [Parameter(Mandatory = $false)] + [ValidateSet( + 'NotSpecified', 'OpenError', 'CloseError', 'DeviceError', 'DeadlockDetected', + 'InvalidArgument', 'InvalidData', 'InvalidOperation', 'InvalidResult', 'InvalidType', + 'MetadataError', 'NotImplemented', 'NotInstalled', 'ObjectNotFound', 'OperationStopped', + 'OperationTimeout', 'SyntaxError', 'ParserError', 'PermissionDenied', 'ResourceBusy', + 'ResourceExists', 'ResourceUnavailable', 'ReadError', 'WriteError', 'FromStdErr', + 'SecurityError', 'ProtocolError', 'ConnectionError', 'AuthenticationError', + 'LimitsExceeded', 'QuotaExceeded', 'NotEnabled' + )] + [System.Management.Automation.ErrorCategory]$ErrorCategory = [System.Management.Automation.ErrorCategory]::OperationStopped, + + [Parameter(Mandatory = $false)] + [object]$TargetObject, + + [Parameter(Mandatory = $false)] + [string]$FunctionName, + + [Parameter(Mandatory = $false)] + [System.Exception]$Exception, + + [Parameter(Mandatory = $false)] + [string]$RecommendedAction, + + [Parameter(Mandatory = $false)] + [bool]$LogToCs = $true, + + [Parameter(Mandatory = $false)] + [bool]$Throw = $true + ) + + # Get the calling function name if not provided + if (-not $FunctionName) { + $CallingFunction = (Get-PSCallStack)[1] + if ($CallingFunction.Command) { + $FunctionName = $CallingFunction.Command + } else { + $FunctionName = 'Unknown' + } + } + + # Format the full error message + $FullErrorMessage = "[$FunctionName] $ErrorMessage" + + # Log to global Cs object if available and requested + if ($LogToCs -and $Global:Cs) { + try { + $Global:Cs.Job_WriteLog("ERROR: $FullErrorMessage", $true) + } catch { + # Silently continue if logging fails + Write-Verbose "Failed to log to Cs object: $_" + } + } + + # Create or use the exception + if (-not $Exception) { + $Exception = New-Object System.InvalidOperationException($FullErrorMessage) + } + + # Create the error record + $ErrorRecord = New-Object System.Management.Automation.ErrorRecord( + $Exception, + "CCS.$FunctionName.$ErrorCategory", + $ErrorCategory, + $TargetObject + ) + + # Add recommended action if provided + if ($RecommendedAction) { + $ErrorRecord.ErrorDetails = New-Object System.Management.Automation.ErrorDetails($ErrorMessage) + $ErrorRecord.ErrorDetails.RecommendedAction = $RecommendedAction + } + + # Add category message for better context + $ErrorRecord.CategoryInfo.Activity = $FunctionName + $ErrorRecord.CategoryInfo.Reason = $Exception.GetType().Name + + # Either throw or write the error + if ($Throw) { + throw $ErrorRecord + } else { + Write-Error -ErrorRecord $ErrorRecord + } +} diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Invoke-CCSIsError.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Invoke-CCSIsError.ps1 index 56fcf635..dc56ab2e 100644 --- a/Modules/Capa.PowerShell.Module.CCS/Dev/Invoke-CCSIsError.ps1 +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Invoke-CCSIsError.ps1 @@ -11,6 +11,12 @@ function Invoke-CCSIsError { 'The server is unwilling to process the request*' { return $true } + "Computer does not exist*" { + return $true + } + "The server is not operational*" { + return $true + } default { return $false } diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Move-CCSADComputer.Tests.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Move-CCSADComputer.Tests.ps1 new file mode 100644 index 00000000..14668c3e --- /dev/null +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Move-CCSADComputer.Tests.ps1 @@ -0,0 +1,262 @@ +BeforeAll { + $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent + + $Items = Get-ChildItem -Path "$RootPath\Capa.PowerShell.Module.CCS\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + + # Setup test environment + $script:TestUrl = "https://$(hostname).capainstaller.com/CCSWebservice/CCS.asmx" + $script:TestDomain = 'Firmax.local' + $CredentialPath = "D:\PowerShell\Credentials\$($env:USERNAME)DomainAdminPesterTests.xml" + + # Setup credentials from environment variables (GitHub secrets) + if ($env:DOMAINADMINUSERNAME -and $env:DOMAINADMINPASSWORD) { + $securePassword = ConvertTo-SecureString $env:DOMAINADMINPASSWORD -AsPlainText -Force + $script:TestDomainCredential = New-Object System.Management.Automation.PSCredential($env:DOMAINADMINUSERNAME, $securePassword) + $script:TestCCSCredential = $script:TestDomainCredential + } elseif (Test-Path -Path $CredentialPath) { + $script:TestDomainCredential = Import-Clixml -Path $CredentialPath + $script:TestCCSCredential = $script:TestDomainCredential + } else { + $script:TestDomainCredential = Get-Credential -Message 'Enter domain admin credentials for integration tests' + $script:TestCCSCredential = $script:TestDomainCredential + } + + $DebugPreference = 'Continue' + $ErrorActionPreference = 'Stop' +} + +Describe 'Move-CCSADComputer' -Tag 'Unit' { + + Context 'Parameter Validation' { + + It 'Should have mandatory ComputerName parameter' { + (Get-Command Move-CCSADComputer).Parameters['ComputerName'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory DestinationOU parameter' { + (Get-Command Move-CCSADComputer).Parameters['DestinationOU'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory Domain parameter' { + (Get-Command Move-CCSADComputer).Parameters['Domain'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory Url parameter' { + (Get-Command Move-CCSADComputer).Parameters['Url'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory CCSCredential parameter' { + (Get-Command Move-CCSADComputer).Parameters['CCSCredential'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have optional DomainCredential parameter' { + (Get-Command Move-CCSADComputer).Parameters['DomainCredential'].Attributes.Mandatory | Should -Be $false + } + + It 'Should have optional DomainOUPath parameter' { + (Get-Command Move-CCSADComputer).Parameters['DomainOUPath'].Attributes.Mandatory | Should -Be $false + } + + It 'Should accept array of computer names' { + (Get-Command Move-CCSADComputer).Parameters['ComputerName'].ParameterType.Name | Should -Be 'String[]' + } + + It 'Should accept pipeline input for ComputerName' { + (Get-Command Move-CCSADComputer).Parameters['ComputerName'].Attributes.ValueFromPipeline | Should -Be $true + } + } + + Context 'Parameter Aliases' { + + It 'Should have alias "Name" for ComputerName' { + (Get-Command Move-CCSADComputer).Parameters['ComputerName'].Aliases | Should -Contain 'Name' + } + + It 'Should have alias "Computer" for ComputerName' { + (Get-Command Move-CCSADComputer).Parameters['ComputerName'].Aliases | Should -Contain 'Computer' + } + + It 'Should have alias "TargetOU" for DestinationOU' { + (Get-Command Move-CCSADComputer).Parameters['DestinationOU'].Aliases | Should -Contain 'TargetOU' + } + + It 'Should have alias "SourceOU" for DomainOUPath' { + (Get-Command Move-CCSADComputer).Parameters['DomainOUPath'].Aliases | Should -Contain 'SourceOU' + } + + It 'Should have alias "Credential" for CCSCredential' { + (Get-Command Move-CCSADComputer).Parameters['CCSCredential'].Aliases | Should -Contain 'Credential' + } + } + + Context 'DestinationOU Validation' { + + It 'Should accept standard DN format with OU' { + { Move-CCSADComputer -ComputerName 'PC01' -DestinationOU 'OU=Workstations,DC=Firmax,DC=local' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WhatIf } | Should -Not -Throw + } + + It 'Should accept DC-only format' { + { Move-CCSADComputer -ComputerName 'PC01' -DestinationOU 'DC=Firmax,DC=local' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WhatIf } | Should -Not -Throw + } + + It 'Should reject invalid format' { + { Move-CCSADComputer -ComputerName 'PC01' -DestinationOU 'InvalidPath' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Throw + } + + It 'Should reject LDAP format for DestinationOU' { + { Move-CCSADComputer -ComputerName 'PC01' -DestinationOU 'LDAP://DC01.Firmax.local/OU=Workstations,DC=Firmax,DC=local' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Throw + } + } + + Context 'DomainOUPath Validation' { + + It 'Should accept empty DomainOUPath' { + { Move-CCSADComputer -ComputerName 'PC01' -DestinationOU 'OU=Workstations,DC=Firmax,DC=local' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath '' -ErrorAction SilentlyContinue -WhatIf } | Should -Not -Throw + } + + It 'Should accept standard DN format' { + { Move-CCSADComputer -ComputerName 'PC01' -DestinationOU 'OU=Workstations,DC=Firmax,DC=local' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'OU=Computers,DC=Firmax,DC=local' -ErrorAction SilentlyContinue -WhatIf } | Should -Not -Throw + } + + It 'Should accept DC-only format' { + { Move-CCSADComputer -ComputerName 'PC01' -DestinationOU 'OU=Workstations,DC=Firmax,DC=local' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'DC=Firmax,DC=local' -ErrorAction SilentlyContinue -WhatIf } | Should -Not -Throw + } + + It 'Should accept LDAP format' { + { Move-CCSADComputer -ComputerName 'PC01' -DestinationOU 'OU=Workstations,DC=Firmax,DC=local' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'LDAP://DC01.Firmax.local/OU=Computers,DC=Firmax,DC=local' -ErrorAction SilentlyContinue -WhatIf } | Should -Not -Throw + } + + It 'Should reject invalid format' { + { Move-CCSADComputer -ComputerName 'PC01' -DestinationOU 'OU=Workstations,DC=Firmax,DC=local' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'InvalidPath' -WhatIf } | Should -Throw + } + } + + Context 'URL Validation' { + + It 'Should accept HTTPS URL' { + { Move-CCSADComputer -ComputerName 'PC01' -DestinationOU 'OU=Workstations,DC=Firmax,DC=local' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WhatIf } | Should -Not -Throw + } + + It 'Should reject HTTP URL (only HTTPS allowed)' { + $httpUrl = $script:TestUrl -replace '^https:', 'http:' + { Move-CCSADComputer -ComputerName 'PC01' -DestinationOU 'OU=Workstations,DC=Firmax,DC=local' -Domain $script:TestDomain -Url $httpUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Throw + } + + It 'Should reject URL without protocol' { + { Move-CCSADComputer -ComputerName 'PC01' -DestinationOU 'OU=Workstations,DC=Firmax,DC=local' -Domain $script:TestDomain -Url 'test.com/CCS.asmx' -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Throw + } + } + + Context 'Domain Validation' { + + It 'Should accept valid domain format' { + { Move-CCSADComputer -ComputerName 'PC01' -DestinationOU 'OU=Workstations,DC=Firmax,DC=local' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WhatIf } | Should -Not -Throw + } + + It 'Should accept subdomain format' { + { Move-CCSADComputer -ComputerName 'PC01' -DestinationOU 'OU=Workstations,DC=Firmax,DC=local' -Domain 'sub.firmax.local' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WhatIf } | Should -Not -Throw + } + + It 'Should reject invalid domain format' { + { Move-CCSADComputer -ComputerName 'PC01' -DestinationOU 'OU=Workstations,DC=Firmax,DC=local' -Domain 'invalid_domain' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Throw + } + } + + Context 'ShouldProcess Support' { + + It 'Should support WhatIf' { + (Get-Command Move-CCSADComputer).Parameters.ContainsKey('WhatIf') | Should -Be $true + } + + It 'Should support Confirm' { + (Get-Command Move-CCSADComputer).Parameters.ContainsKey('Confirm') | Should -Be $true + } + + It 'Should have ConfirmImpact set to High' { + $command = Get-Command Move-CCSADComputer + $command.ScriptBlock.Attributes | Where-Object { $_ -is [System.Management.Automation.CmdletBindingAttribute] } | + Select-Object -First 1 -ExpandProperty ConfirmImpact | Should -Be 'High' + } + } + + Context 'CmdletBinding Attributes' { + + It 'Should be an advanced function' { + (Get-Command Move-CCSADComputer).CmdletBinding | Should -Be $true + } + + It 'Should have SupportsShouldProcess' { + (Get-Command Move-CCSADComputer).Parameters['WhatIf'] | Should -Not -BeNullOrEmpty + } + + It 'Should have OutputType defined' { + (Get-Command Move-CCSADComputer).OutputType.Name | Should -Contain 'System.String' + } + } +} + +Describe 'Move-CCSADComputer - Integration Tests' -Tag 'Integration' { + + Context 'Live CCS Web Service Operations' { + + BeforeAll { + $script:TestComputerName = 'PESTER-TEST-PC' + $script:TestDestinationOU = 'OU=Test,DC=Firmax,DC=local' + } + + It 'Should connect to CCS Web Service successfully' { + { Move-CCSADComputer -ComputerName $script:TestComputerName -DestinationOU $script:TestDestinationOU -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should move computer with domain credentials' { + { Move-CCSADComputer -ComputerName $script:TestComputerName -DestinationOU $script:TestDestinationOU -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -Confirm:$false -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should process multiple computers' { + $computers = @("$script:TestComputerName`1", "$script:TestComputerName`2") + { $computers | Move-CCSADComputer -DestinationOU $script:TestDestinationOU -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -Confirm:$false -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should convert LDAP path format for source OU' { + $ldapPath = 'LDAP://DC01.Firmax.local/DC=Firmax,DC=local' + { Move-CCSADComputer -ComputerName $script:TestComputerName -DestinationOU $script:TestDestinationOU -DomainOUPath $ldapPath -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -Confirm:$false -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should move computer from specific source OU' { + { Move-CCSADComputer -ComputerName $script:TestComputerName -DestinationOU $script:TestDestinationOU -DomainOUPath 'OU=Computers,DC=Firmax,DC=local' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -Confirm:$false -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + } + + Context 'Error Handling' { + + It 'Should handle invalid credentials gracefully' { + $badCred = New-Object System.Management.Automation.PSCredential('baduser', (ConvertTo-SecureString 'badpass' -AsPlainText -Force)) + { Move-CCSADComputer -ComputerName 'TestPC' -DestinationOU 'OU=Test,DC=Firmax,DC=local' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $badCred -ErrorAction Stop } | Should -Throw + } + + It 'Should handle non-existent URL gracefully' { + { Move-CCSADComputer -ComputerName 'TestPC' -DestinationOU 'OU=Test,DC=Firmax,DC=local' -Domain $script:TestDomain -Url 'https://nonexistent.invalid/CCS.asmx' -CCSCredential $script:TestCCSCredential -ErrorAction Stop } | Should -Throw + } + + It 'Should handle non-existent computer gracefully' { + { Move-CCSADComputer -ComputerName 'NonExistentPC-12345' -DestinationOU 'OU=Test,DC=Firmax,DC=local' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -Confirm:$false -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + } +} + +Describe 'Move-CCSADComputer - Performance Tests' -Tag 'Performance' { + + Context 'Pipeline Performance' { + + It 'Should process pipeline input efficiently' { + $computers = 1..10 | ForEach-Object { "PC$_" } + $measure = Measure-Command { + $computers | Move-CCSADComputer -DestinationOU 'OU=Test,DC=Firmax,DC=local' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf -Confirm:$false -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + } + $measure.TotalSeconds | Should -BeLessThan 5 + } + } +} diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Move-CCSADComputer.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Move-CCSADComputer.ps1 new file mode 100644 index 00000000..2dcb7bdf --- /dev/null +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Move-CCSADComputer.ps1 @@ -0,0 +1,349 @@ +function Move-CCSADComputer { + <# + .SYNOPSIS + Moves a computer to a different Organizational Unit (OU) in Active Directory using the CCS Web Service. + + .DESCRIPTION + Moves a computer to a different Organizational Unit (OU) in Active Directory using the CCS Web Service. + This advanced function includes comprehensive error handling, input validation, and supports pipeline input. + + .PARAMETER ComputerName + The name of the computer to be moved. + Supports pipeline input and accepts multiple computer names. + + .PARAMETER DestinationOU + The Distinguished Name (DN) of the destination Organizational Unit where the computer will be moved. + Must be in format: OU=Computers,DC=example,DC=com + + .PARAMETER DomainOUPath + The Organizational Unit (OU) path where the computer currently resides. + If not specified, searches the entire domain. + Supports both standard DN format (OU=Computers,DC=example,DC=com) and LDAP format (LDAP://DC01.example.local/OU=Computers,DC=example,DC=com). + + .PARAMETER Domain + The domain in which the computer resides. + Must be a valid domain name format. + + .PARAMETER Url + The URL of the CCS Web Service. + Must be a valid HTTPS URI format. Example: "https://example.com/CCSWebservice/CCS.asmx" + + .PARAMETER CCSCredential + The credentials used to authenticate with the CCS Web Service. + + .PARAMETER DomainCredential + The credentials of an account with permissions to move the computer. + If not defined, it will run in the CCS Web Service context. + + .PARAMETER PasswordIsEncrypted + Indicates if the password in the DomainCredential is encrypted. Default is $false. + + .EXAMPLE + PS C:\> Move-CCSADComputer -ComputerName "TestPC" -DestinationOU "OU=NewLocation,DC=example,DC=com" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Moves TestPC to the NewLocation OU using default CCS context. + + .EXAMPLE + PS C:\> Move-CCSADComputer -ComputerName "TestPC" -DestinationOU "OU=NewLocation,DC=example,DC=com" -DomainOUPath "OU=OldLocation,DC=example,DC=com" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential -DomainCredential $DomainCredential + + Moves TestPC from OldLocation OU to NewLocation OU using specific domain credentials. + + .EXAMPLE + PS C:\> "PC01", "PC02", "PC03" | Move-CCSADComputer -DestinationOU "OU=NewLocation,DC=example,DC=com" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Moves multiple computers to NewLocation OU using pipeline input. + + .EXAMPLE + PS C:\> Move-CCSADComputer -ComputerName "TestPC" -DestinationOU "OU=Workstations,DC=Firmax,DC=local" -DomainOUPath "LDAP://DC01.Firmax.local/DC=Firmax,DC=local" -Domain "Firmax.local" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Moves TestPC to Workstations OU using LDAP format for the current OU path. + + .OUTPUTS + System.String + Returns the result message from the CCS Web Service operation. + + .NOTES + This is an advanced function with support for ShouldProcess, pipeline input, and comprehensive error handling. + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + [OutputType([string])] + param ( + [Parameter( + Mandatory = $true, + Position = 0, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the computer name to move' + )] + [ValidateNotNullOrEmpty()] + [Alias('Name', 'Computer', 'CN')] + [string[]]$ComputerName, + + [Parameter( + Mandatory = $true, + Position = 1, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the destination OU Distinguished Name' + )] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + # Must be in DN format: OU=...,DC=...,DC=... + if ($_ -match '^(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + throw "DestinationOU must be in Distinguished Name format: 'OU=...,DC=...,DC=...'" + })] + [Alias('TargetOU', 'OU', 'Target')] + [string]$DestinationOU, + + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] + [ValidateScript({ + if ([string]::IsNullOrEmpty($_)) { + return $true + } + # Support standard DN format: OU=...,DC=...,DC=... + if ($_ -match '^(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + # Support LDAP format: LDAP://server/DN + if ($_ -match '^LDAP://[^/]+/(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + throw "DomainOUPath must be in format 'OU=...,DC=...,DC=...' or 'LDAP://server/OU=...,DC=...,DC=...' or empty" + })] + [Alias('SourceOU', 'Path', 'CurrentOU')] + [string]$DomainOUPath = '', + + [Parameter( + Mandatory = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the domain name' + )] + [ValidateNotNullOrEmpty()] + [ValidatePattern('^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$')] + [string]$Domain, + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service URL' + )] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + if ($_ -match '^https://') { + $true + } else { + throw "URL must start with https:// (secure connection required)" + } + })] + [Alias('WebServiceUrl', 'Uri')] + [string]$Url, + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service credentials' + )] + [ValidateNotNull()] + [Alias('Credential', 'WebServiceCredential')] + [pscredential]$CCSCredential, + + [Parameter(Mandatory = $false)] + [ValidateNotNull()] + [Alias('ADCredential')] + [pscredential]$DomainCredential, + + [Parameter(Mandatory = $false)] + [Alias('Encrypted')] + [switch]$PasswordIsEncrypted + ) + + begin { + $FunctionName = $PSCmdlet.MyInvocation.MyCommand.Name + + Write-Verbose "[$FunctionName] Starting function execution" + Write-Verbose "[$FunctionName] DestinationOU: $DestinationOU" + Write-Verbose "[$FunctionName] Domain: $Domain" + Write-Verbose "[$FunctionName] DomainOUPath: $DomainOUPath" + Write-Verbose "[$FunctionName] URL: $Url" + + # Initialize CCS connection once in begin block + try { + Write-Verbose "[$FunctionName] Initializing CCS Web Service connection" + $CCS = Initialize-CCS -Url $Url -WebServiceCredential $CCSCredential -ErrorAction Stop + } catch { + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + Write-Debug "ErrorAction from PSBoundParameters: $($PSBoundParameters['ErrorAction'])" + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + Write-Debug "ErrorAction from ErrorActionPreference: $ErrorActionPreference" + } + Write-Debug "ShouldThrow is set to: $ShouldThrow" + + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to initialize CCS Web Service: $_" ` + -ErrorCategory ConnectionError ` + -TargetObject $Url ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Verify the URL is correct and the CCS Web Service is accessible. Check credentials." ` + -Throw:$ShouldThrow + return + } + + # Prepare domain credentials if provided + $ADUsername = $null + $ADPassword = $null + + if ($DomainCredential) { + $ADUsername = $DomainCredential.UserName + Write-Verbose "[$FunctionName] Using domain credential: $ADUsername" + + try { + if ($PasswordIsEncrypted) { + $ADPassword = $DomainCredential.GetNetworkCredential().Password + Write-Verbose "[$FunctionName] Using pre-encrypted password" + } else { + $ADPassword = Get-CCSEncryptedPassword -SecureString $DomainCredential.Password -ErrorAction Stop + Write-Verbose "[$FunctionName] Password encrypted successfully" + } + } catch { + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + } + + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to process domain credential password: $_" ` + -ErrorCategory SecurityError ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Ensure the domain credential is valid and the password can be encrypted." ` + -Throw:$ShouldThrow + return + } + } else { + Write-Verbose "[$FunctionName] No domain credential provided, using CCS Web Service context" + } + + $ProcessedCount = 0 + $SuccessCount = 0 + $FailureCount = 0 + } + + process { + foreach ($Computer in $ComputerName) { + $ProcessedCount++ + + try { + Write-Verbose "[$FunctionName] Processing computer: $Computer ($ProcessedCount)" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Processing: ComputerName=$Computer, DestinationOU=$DestinationOU, DomainOUPath=$DomainOUPath, Domain=$Domain") + } + + # ShouldProcess support + $WhatIfMessage = "Move computer '$Computer' to OU '$DestinationOU' in domain '$Domain'" + if ($PSCmdlet.ShouldProcess($Computer, $WhatIfMessage)) { + Write-Verbose "[$FunctionName] Calling ActiveDirectory_MoveComputerToOU for $Computer" + + $Result = $CCS.ActiveDirectory_MoveComputerToOU( + $Computer, + $DestinationOU, + $DomainOUPath, + $Domain, + $ADUsername, + $ADPassword + ) + Write-Debug "Result from CCS Web Service: $Result" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Result for $Computer : $Result") + } + + Write-Verbose "[$FunctionName] Result for $Computer : $Result" + + # Check for errors in result + $IsError = Invoke-CCSIsError -Result $Result + + Write-Debug "IsError evaluation for result: $IsError" + if ($IsError) { + $FailureCount++ + + # Determine appropriate error category based on result message + $ErrorCat = [System.Management.Automation.ErrorCategory]::OperationStopped + $RecommendedActionText = "Verify the computer name and destination OU are correct." + + if ($Result -like '*does not exist*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::ObjectNotFound + $RecommendedActionText = "Verify that the computer '$Computer' and destination OU '$DestinationOU' exist in Active Directory." + } elseif ($Result -like '*unwilling to process*' -or $Result -like '*Access is denied*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::PermissionDenied + $RecommendedActionText = "Check that the domain credentials have sufficient permissions to move the computer." + } elseif ($Result -like '*already exists*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::ResourceExists + $RecommendedActionText = "The computer already exists in the destination OU." + } + + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + Write-Debug "ErrorAction from PSBoundParameters: $($PSBoundParameters['ErrorAction'])" + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + Write-Debug "ErrorAction from ErrorActionPreference: $ErrorActionPreference" + } + Write-Debug "ShouldThrow is set to: $ShouldThrow" + + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to move computer '$Computer' to OU '$DestinationOU': $Result" ` + -ErrorCategory $ErrorCat ` + -TargetObject $Computer ` + -FunctionName $FunctionName ` + -RecommendedAction $RecommendedActionText ` + -Throw:$ShouldThrow + } else { + $SuccessCount++ + Write-Verbose "[$FunctionName] Successfully moved $Computer to $DestinationOU" + Write-Output $Result + } + } else { + Write-Verbose "[$FunctionName] WhatIf: Would move $Computer to $DestinationOU" + } + } catch { + $FailureCount++ + + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + } + + Invoke-CCSErrorHandling ` + -ErrorMessage "Exception occurred while processing computer '$Computer': $_" ` + -ErrorCategory InvalidOperation ` + -TargetObject $Computer ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Check the error details and verify all parameters are correct." ` + -Throw:$ShouldThrow + } + } + } + + end { + Write-Verbose "[$FunctionName] Function execution completed" + Write-Verbose "[$FunctionName] Total processed: $ProcessedCount | Success: $SuccessCount | Failed: $FailureCount" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Summary - Processed: $ProcessedCount, Success: $SuccessCount, Failed: $FailureCount") + } + } +} diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Remove-CCSADComputer.Tests.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Remove-CCSADComputer.Tests.ps1 index 57272279..2d12bce8 100644 --- a/Modules/Capa.PowerShell.Module.CCS/Dev/Remove-CCSADComputer.Tests.ps1 +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Remove-CCSADComputer.Tests.ps1 @@ -1,51 +1,224 @@ BeforeAll { - . $PSCommandPath.Replace('.Tests.ps1', '.ps1') + $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent - $Url = 'https://mracapa02.capainstaller.com/CCSWebservice/CCS.asmx' + $Items = Get-ChildItem -Path "$RootPath\Capa.PowerShell.Module.CCS\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } - $CCSUser = 'svc_capawebservice' - $ADUser = 'Administrator' - $Password = 'Admin1234' - $Domain = 'FirmaX.local' + # Setup test environment + $script:TestUrl = "https://$(hostname).capainstaller.com/CCSWebservice/CCS.asmx" + $script:TestDomain = 'Firmax.local' + $CredentialPath = "D:\PowerShell\Credentials\$($env:USERNAME)DomainAdminPesterTests.xml" - $SecurePassword = ConvertTo-SecureString -String $Password -AsPlainText -Force - $CCSCredential = New-Object System.Management.Automation.PSCredential($CCSUser, $SecurePassword) - $DomainCredential = New-Object System.Management.Automation.PSCredential($ADUser, $SecurePassword) + # Setup credentials from environment variables (GitHub secrets) + if ($env:DOMAINADMINUSERNAME -and $env:DOMAINADMINPASSWORD) { + $securePassword = ConvertTo-SecureString $env:DOMAINADMINPASSWORD -AsPlainText -Force + $script:TestDomainCredential = New-Object System.Management.Automation.PSCredential($env:DOMAINADMINUSERNAME, $securePassword) + $script:TestCCSCredential = $script:TestDomainCredential + } elseif (Test-Path -Path $CredentialPath) { + $script:TestDomainCredential = Import-Clixml -Path $CredentialPath + $script:TestCCSCredential = $script:TestDomainCredential + } else { + $script:TestDomainCredential = Get-Credential -Message 'Enter domain admin credentials for integration tests' + $script:TestCCSCredential = $script:TestDomainCredential + } - $ComputerName1 = 'Test-Computer' - $ComputerName2 = 'Test-Computer2' - $OUPath = "CN=Computers,DC=FirmaX,DC=local" + $DebugPreference = 'Continue' + $ErrorActionPreference = 'Stop' +} + +Describe 'Remove-CCSADComputer' -Tag 'Unit' { + + Context 'Parameter Validation' { + + It 'Should have mandatory ComputerName parameter' { + (Get-Command Remove-CCSADComputer).Parameters['ComputerName'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have optional DomainOUPath parameter' { + (Get-Command Remove-CCSADComputer).Parameters['DomainOUPath'].Attributes.Mandatory | Should -Be $false + } + + It 'Should have mandatory Domain parameter' { + (Get-Command Remove-CCSADComputer).Parameters['Domain'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory Url parameter' { + (Get-Command Remove-CCSADComputer).Parameters['Url'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory CCSCredential parameter' { + (Get-Command Remove-CCSADComputer).Parameters['CCSCredential'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have optional DomainCredential parameter' { + (Get-Command Remove-CCSADComputer).Parameters['DomainCredential'].Attributes.Mandatory | Should -Be $false + } + + It 'Should have optional PasswordIsEncrypted parameter' { + (Get-Command Remove-CCSADComputer).Parameters['PasswordIsEncrypted'].Attributes.Mandatory | Should -Be $false + } + + It 'Should accept array of computer names' { + (Get-Command Remove-CCSADComputer).Parameters['ComputerName'].ParameterType.Name | Should -Be 'String[]' + } + + It 'Should accept pipeline input for ComputerName' { + (Get-Command Remove-CCSADComputer).Parameters['ComputerName'].Attributes.ValueFromPipeline | Should -Be $true + } + } + + Context 'Parameter Aliases' { + + It 'Should have alias "Name" for ComputerName' { + (Get-Command Remove-CCSADComputer).Parameters['ComputerName'].Aliases | Should -Contain 'Name' + } + + It 'Should have alias "Computer" for ComputerName' { + (Get-Command Remove-CCSADComputer).Parameters['ComputerName'].Aliases | Should -Contain 'Computer' + } + + It 'Should have alias "OU" for DomainOUPath' { + (Get-Command Remove-CCSADComputer).Parameters['DomainOUPath'].Aliases | Should -Contain 'OU' + } + } + + Context 'DomainOUPath Validation' { + + It 'Should accept empty DomainOUPath' { + { Remove-CCSADComputer -ComputerName 'PC01' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath '' -WhatIf } | Should -Not -Throw + } + + It 'Should accept standard DN format' { + { Remove-CCSADComputer -ComputerName 'PC01' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'OU=Computers,DC=Firmax,DC=local' -WhatIf } | Should -Not -Throw + } + + It 'Should accept DC-only format' { + { Remove-CCSADComputer -ComputerName 'PC01' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'DC=Firmax,DC=local' -WhatIf } | Should -Not -Throw + } + + It 'Should accept LDAP format' { + { Remove-CCSADComputer -ComputerName 'PC01' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'LDAP://DC01.Firmax.local/OU=Computers,DC=Firmax,DC=local' -WhatIf } | Should -Not -Throw + } + + It 'Should reject invalid format' { + { Remove-CCSADComputer -ComputerName 'PC01' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'InvalidPath' -WhatIf } | Should -Throw + } + } + + Context 'URL Validation' { + + It 'Should accept HTTPS URL' { + { Remove-CCSADComputer -ComputerName 'PC01' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Not -Throw + } + + It 'Should reject HTTP URL (only HTTPS allowed)' { + $httpUrl = $script:TestUrl -replace '^https:', 'http:' + { Remove-CCSADComputer -ComputerName 'PC01' -Domain $script:TestDomain -Url $httpUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Throw + } - New-ADComputer -Name $ComputerName2 -Credential $DomainCredential + It 'Should reject URL without protocol' { + { Remove-CCSADComputer -ComputerName 'PC01' -Domain $script:TestDomain -Url 'test.com/CCS.asmx' -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Throw + } + } + + Context 'Domain Validation' { + + It 'Should accept valid domain format' { + { Remove-CCSADComputer -ComputerName 'PC01' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Not -Throw + } + + It 'Should accept subdomain format' { + { Remove-CCSADComputer -ComputerName 'PC01' -Domain 'sub.firmax.local' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Not -Throw + } + + It 'Should reject invalid domain format' { + { Remove-CCSADComputer -ComputerName 'PC01' -Domain 'invalid_domain' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Throw + } + } + + Context 'ShouldProcess Support' { + + It 'Should support WhatIf' { + (Get-Command Remove-CCSADComputer).Parameters.ContainsKey('WhatIf') | Should -Be $true + } + + It 'Should support Confirm' { + (Get-Command Remove-CCSADComputer).Parameters.ContainsKey('Confirm') | Should -Be $true + } + } + + Context 'CmdletBinding Attributes' { + + It 'Should be an advanced function' { + (Get-Command Remove-CCSADComputer).CmdletBinding | Should -Be $true + } + + It 'Should have SupportsShouldProcess' { + (Get-Command Remove-CCSADComputer).Parameters['WhatIf'] | Should -Not -BeNullOrEmpty + } + + It 'Should have OutputType defined' { + (Get-Command Remove-CCSADComputer).OutputType.Name | Should -Contain 'System.String' + } + } } -Describe 'Tests that should work' { - It "Should say 'Computer does not exist.'" { - $Splat = @{ - ComputerName = $ComputerName1 - Domain = $Domain - Url = $Url - CCSCredential = $CCSCredential - DomainCredential = $DomainCredential - } - $Result = Remove-CCSADComputer @Splat - $Result | Should -BeOfType [string] - $Result | Should -Be 'Computer does not exist.' - } - It "Should say 'Computer Deleted.'" { - $Splat = @{ - ComputerName = $ComputerName2 - Domain = $Domain - Url = $Url - CCSCredential = $CCSCredential - DomainCredential = $DomainCredential - } - $Result = Remove-CCSADComputer @Splat - $Result | Should -BeOfType [string] - $Result | Should -Be 'Computer Deleted.' - } + +Describe 'Remove-CCSADComputer - Integration Tests' -Tag 'Integration' { + + Context 'Live CCS Web Service Operations' { + + BeforeAll { + $script:TestComputerName = 'PESTER-REMOVE-TEST-PC' + } + + It 'Should connect to CCS Web Service successfully' { + { Remove-CCSADComputer -ComputerName $script:TestComputerName -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Not -Throw + } + + It 'Should handle non-existent computer gracefully' { + { Remove-CCSADComputer -ComputerName $script:TestComputerName -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should remove computer with domain credentials' { + { Remove-CCSADComputer -ComputerName $script:TestComputerName -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should process multiple computers' { + $computers = @("$script:TestComputerName`1", "$script:TestComputerName`2") + { $computers | Remove-CCSADComputer -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should convert LDAP path format' { + $ldapPath = 'LDAP://DC01.Firmax.local/DC=Firmax,DC=local' + { Remove-CCSADComputer -ComputerName $script:TestComputerName -DomainOUPath $ldapPath -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should work without DomainCredential (CCS context)' { + { Remove-CCSADComputer -ComputerName $script:TestComputerName -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + } + + Context 'Error Handling' { + + It 'Should handle invalid credentials gracefully' { + $badCred = New-Object System.Management.Automation.PSCredential('baduser', (ConvertTo-SecureString 'badpass' -AsPlainText -Force)) + { Remove-CCSADComputer -ComputerName 'TestPC' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $badCred -ErrorAction Stop } | Should -Throw + } + } } -AfterAll { - try { - Remove-ADComputer -Identity $ComputerName2 -Credential $DomainCredential -Confirm:$false - } catch {} + +Describe 'Remove-CCSADComputer - Performance Tests' -Tag 'Performance' { + + Context 'Pipeline Performance' { + + It 'Should process pipeline input efficiently' { + $computers = 1..10 | ForEach-Object { "PC$_" } + $measure = Measure-Command { + $computers | Remove-CCSADComputer -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf -WarningAction SilentlyContinue + } + $measure.TotalSeconds | Should -BeLessThan 5 + } + } } \ No newline at end of file diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Remove-CCSADComputer.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Remove-CCSADComputer.ps1 index 70a336e9..efd52aa7 100644 --- a/Modules/Capa.PowerShell.Module.CCS/Dev/Remove-CCSADComputer.ps1 +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Remove-CCSADComputer.ps1 @@ -1,81 +1,311 @@ -<# +function Remove-CCSADComputer { + <# .SYNOPSIS - Remove a computer from Active Directory using the CCS Web Service. + Removes a computer from Active Directory using the CCS Web Service. .DESCRIPTION - Removes a computer from Active Directory using the CCS Web Service. This function requires the CCS Web Service URL and credentials to access it. + Removes a computer from Active Directory using the CCS Web Service. + This advanced function includes comprehensive error handling, input validation, and supports pipeline input. .PARAMETER ComputerName The name of the computer to be removed from Active Directory. + Supports pipeline input and accepts multiple computer names. .PARAMETER DomainOUPath The Organizational Unit (OU) path in which the computer resides. + If not specified, searches the entire domain. + Supports both standard DN format (OU=Computers,DC=example,DC=com) and LDAP format (LDAP://DC01.example.local/OU=Computers,DC=example,DC=com). .PARAMETER Domain The domain in which the computer resides. + Must be a valid domain name format. .PARAMETER Url - The URL of the CCS Web Service. Example "https://example.com/CCSWebservice/CCS.asmx". + The URL of the CCS Web Service. + Must be a valid HTTPS URI format. Example: "https://example.com/CCSWebservice/CCS.asmx" .PARAMETER CCSCredential The credentials used to authenticate with the CCS Web Service. .PARAMETER DomainCredential - The credentials of an account with permissions to remove the computer from Active Directory, if not defined it will run in the CCSWebservice context. + The credentials of an account with permissions to remove the computer from Active Directory. + If not defined, it will run in the CCS Web Service context. .PARAMETER PasswordIsEncrypted - Indicates if the password in the DomainCredential is encrypted. Default is false. + Indicates if the password in the DomainCredential is encrypted. Default is $false. .EXAMPLE - PS C:\> Remove-CCSADComputer -ComputerName "TestPC" -DomainOUPath "OU=Computers,DC=example,DC=com" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + PS C:\> Remove-CCSADComputer -ComputerName "TestPC" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Removes TestPC using default CCS context. .EXAMPLE PS C:\> Remove-CCSADComputer -ComputerName "TestPC" -DomainOUPath "OU=Computers,DC=example,DC=com" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential -DomainCredential $DomainCredential + Removes TestPC using specific domain credentials and OU path. + .EXAMPLE - PS C:\> Remove-CCSADComputer -ComputerName "TestPC" -DomainOUPath "LDAP://DC01.FirmaX.local/OU=Computers,DC=FirmaX,DC=local" -Domain "FirmaX.local" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential -DomainCredential $DomainCredential -PasswordIsEncrypted $true -#> -function Remove-CCSADComputer { + PS C:\> "PC01", "PC02", "PC03" | Remove-CCSADComputer -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Removes multiple computers using pipeline input. + + .EXAMPLE + PS C:\> Remove-CCSADComputer -ComputerName "TestPC" -DomainOUPath "LDAP://DC01.Firmax.local/OU=Computers,DC=Firmax,DC=local" -Domain "Firmax.local" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential -DomainCredential $DomainCredential + + Removes TestPC using LDAP format for the OU path. The LDAP path will be automatically converted to standard DN format. + + .OUTPUTS + System.String + Returns the result message from the CCS Web Service operation. + + .NOTES + This is an advanced function with support for ShouldProcess, pipeline input, and comprehensive error handling. + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + [OutputType([string])] param ( - [Parameter(Mandatory = $true)] - [string]$ComputerName, - [Parameter(Mandatory = $false)] - [string]$DomainOUPath, - [Parameter(Mandatory = $true)] + [Parameter( + Mandatory = $true, + Position = 0, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the computer name to remove from Active Directory' + )] + [ValidateNotNullOrEmpty()] + [Alias('Name', 'Computer', 'CN')] + [string[]]$ComputerName, + + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] + [ValidateScript({ + if ([string]::IsNullOrEmpty($_)) { + return $true + } + # Support standard DN format: OU=...,DC=...,DC=... + if ($_ -match '^(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + # Support LDAP format: LDAP://server/DN + if ($_ -match '^LDAP://[^/]+/(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + throw "DomainOUPath must be in format 'OU=...,DC=...,DC=...' or 'LDAP://server/OU=...,DC=...,DC=...' or empty" + })] + [Alias('OU', 'Path')] + [string]$DomainOUPath = '', + + [Parameter( + Mandatory = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the domain name' + )] + [ValidateNotNullOrEmpty()] + [ValidatePattern('^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$')] [string]$Domain, - [Parameter(Mandatory = $true)] + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service URL' + )] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + if ($_ -match '^https://') { + $true + } else { + throw "URL must start with https:// (secure connection required)" + } + })] + [Alias('WebServiceUrl', 'Uri')] [string]$Url, - [Parameter(Mandatory = $true)] + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service credentials' + )] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + if ($_ -is [System.Management.Automation.PSCredential]) { + $true + } else { + throw "CCSCredential must be a PSCredential object" + } + })] + [Alias('Credential', 'WebServiceCredential')] [pscredential]$CCSCredential, + [Parameter(Mandatory = $false)] + [ValidateNotNull()] + [Alias('ADCredential')] [pscredential]$DomainCredential, + [Parameter(Mandatory = $false)] - [bool]$PasswordIsEncrypted = $false + [Alias('Encrypted')] + [switch]$PasswordIsEncrypted ) - if ($Global:Cs) { - Job_WriteLog -Text "Remove-CCSADComputer: ComputerName: $ComputerName, DomainOUPath: $DomainOUPath, Domain: $Domain, Url: $Url, PasswordIsEncrypted: $PasswordIsEncrypted" - } - $CCS = Initialize-CCS -Url $Url -WebServiceCredential $CCSCredential - $ADPassword = $null + begin { + $FunctionName = $PSCmdlet.MyInvocation.MyCommand.Name + + Write-Verbose "[$FunctionName] Starting function execution" + Write-Verbose "[$FunctionName] Domain: $Domain" + Write-Verbose "[$FunctionName] DomainOUPath: $DomainOUPath" + Write-Verbose "[$FunctionName] URL: $Url" + + # Initialize CCS connection once in begin block + try { + Write-Verbose "[$FunctionName] Initializing CCS Web Service connection" + $CCS = Initialize-CCS -Url $Url -WebServiceCredential $CCSCredential -ErrorAction Stop + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to initialize CCS Web Service: $_" ` + -ErrorCategory ConnectionError ` + -TargetObject $Url ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Verify the URL is correct and the CCS Web Service is accessible. Check credentials." + } - if ($DomainCredential -and $PasswordIsEncrypted) { - $ADPassword = $DomainCredential.GetNetworkCredential().Password - } elseif ($DomainCredential -and -not $PasswordIsEncrypted) { - $ADPassword = Get-CCSEncryptedPassword -String $DomainCredential.GetNetworkCredential().Password + # Prepare domain credentials if provided + $ADUsername = $null + $ADPassword = $null + + if ($DomainCredential) { + $ADUsername = $DomainCredential.UserName + Write-Verbose "[$FunctionName] Using domain credential: $ADUsername" + + try { + if ($PasswordIsEncrypted) { + $ADPassword = $DomainCredential.GetNetworkCredential().Password + Write-Verbose "[$FunctionName] Using pre-encrypted password" + } else { + $SecurePassword = ConvertTo-SecureString $DomainCredential.GetNetworkCredential().Password -AsPlainText -Force + $ADPassword = Get-CCSEncryptedPassword -SecureString $SecurePassword -ErrorAction Stop + Write-Verbose "[$FunctionName] Password encrypted successfully" + } + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to process domain credential password: $_" ` + -ErrorCategory SecurityError ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Ensure the domain credential is valid and the password can be encrypted." + } + } else { + Write-Verbose "[$FunctionName] No domain credential provided, using CCS Web Service context" + } + + $ProcessedCount = 0 + $SuccessCount = 0 + $FailureCount = 0 } - $Result = $CCS.ActiveDirectory_RemoveComputer( - $ComputerName, - $DomainOUPath, - $Domain, - $DomainCredential.UserName, - $ADPassword - ) + process { + foreach ($Computer in $ComputerName) { + $ProcessedCount++ + + try { + Write-Verbose "[$FunctionName] Processing computer: $Computer ($ProcessedCount)" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Processing: ComputerName=$Computer, DomainOUPath=$DomainOUPath, Domain=$Domain") + } + + # ShouldProcess support + $WhatIfMessage = "Remove computer '$Computer' from Active Directory in domain '$Domain'" + if ($PSCmdlet.ShouldProcess($Computer, $WhatIfMessage)) { + Write-Verbose "[$FunctionName] Calling ActiveDirectory_RemoveComputer for $Computer" + + $Result = $CCS.ActiveDirectory_RemoveComputer( + $Computer, + $DomainOUPath, + $Domain, + $ADUsername, + $ADPassword + ) + Write-Debug "Result from CCS Web Service: $Result" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Result for $Computer : $Result") + } + + Write-Verbose "[$FunctionName] Result for $Computer : $Result" - if ($Global:Cs) { - Job_WriteLog -Text "Remove-CCSADComputer: Result: $Result" + # Check for errors in result + $IsError = Invoke-CCSIsError -Result $Result + + Write-Debug "IsError evaluation for result: $IsError" + if ($IsError) { + $FailureCount++ + + # Determine appropriate error category based on result message + $ErrorCat = [System.Management.Automation.ErrorCategory]::OperationStopped + $RecommendedActionText = "Verify the computer name is correct." + + if ($Result -like '*does not exist*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::ObjectNotFound + $RecommendedActionText = "Verify that the computer '$Computer' exists in Active Directory." + } elseif ($Result -like '*unwilling to process*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::PermissionDenied + $RecommendedActionText = "Check that the domain credentials have sufficient permissions to remove the computer." + } elseif ($Result -like '*access*denied*' -or $Result -like '*permission*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::PermissionDenied + $RecommendedActionText = "Verify that the credentials have sufficient permissions to remove the computer." + } + + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + Write-Debug "ErrorAction from PSBoundParameters: $($PSBoundParameters['ErrorAction'])" + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + Write-Debug "ErrorAction from ErrorActionPreference: $ErrorActionPreference" + } + Write-Debug "ShouldThrow is set to: $ShouldThrow" + + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to remove computer '$Computer' from Active Directory: $Result" ` + -ErrorCategory $ErrorCat ` + -TargetObject $Computer ` + -FunctionName $FunctionName ` + -RecommendedAction $RecommendedActionText ` + -Throw:$ShouldThrow + } else { + $SuccessCount++ + Write-Verbose "[$FunctionName] Successfully removed $Computer from Active Directory" + Write-Output $Result + } + } else { + Write-Verbose "[$FunctionName] WhatIf: Would remove $Computer from Active Directory" + } + } catch { + $FailureCount++ + + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + } + + Invoke-CCSErrorHandling ` + -ErrorMessage "Exception occurred while processing computer '$Computer': $_" ` + -ErrorCategory InvalidOperation ` + -TargetObject $Computer ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Check the error details and verify all parameters are correct." ` + -Throw:$ShouldThrow + } + } } - return $Result + end { + Write-Verbose "[$FunctionName] Function execution completed" + Write-Verbose "[$FunctionName] Total processed: $ProcessedCount | Success: $SuccessCount | Failed: $FailureCount" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Summary - Processed: $ProcessedCount, Success: $SuccessCount, Failed: $FailureCount") + } + } } \ No newline at end of file diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Remove-CCSADComputerFromSecurityGroup.Tests.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Remove-CCSADComputerFromSecurityGroup.Tests.ps1 new file mode 100644 index 00000000..d482d756 --- /dev/null +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Remove-CCSADComputerFromSecurityGroup.Tests.ps1 @@ -0,0 +1,226 @@ +BeforeAll { + $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent + + $Items = Get-ChildItem -Path "$RootPath\Capa.PowerShell.Module.CCS\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + + # Setup test environment + $script:TestUrl = "https://$(hostname).capainstaller.com/CCSWebservice/CCS.asmx" + $script:TestDomain = 'Firmax.local' + $CredentialPath = "D:\PowerShell\Credentials\$($env:USERNAME)DomainAdminPesterTests.xml" + + # Setup credentials from environment variables (GitHub secrets) + if ($env:DOMAINADMINUSERNAME -and $env:DOMAINADMINPASSWORD) { + $securePassword = ConvertTo-SecureString $env:DOMAINADMINPASSWORD -AsPlainText -Force + $script:TestDomainCredential = New-Object System.Management.Automation.PSCredential($env:DOMAINADMINUSERNAME, $securePassword) + $script:TestCCSCredential = $script:TestDomainCredential + } elseif (Test-Path -Path $CredentialPath) { + $script:TestDomainCredential = Import-Clixml -Path $CredentialPath + $script:TestCCSCredential = $script:TestDomainCredential + } else { + $script:TestDomainCredential = Get-Credential -Message 'Enter domain admin credentials for integration tests' + $script:TestCCSCredential = $script:TestDomainCredential + } + + $DebugPreference = 'Continue' + $ErrorActionPreference = 'Stop' +} + +Describe 'Remove-CCSADComputerFromSecurityGroup' -Tag 'Unit' { + + Context 'Parameter Validation' { + + It 'Should have mandatory ComputerName parameter' { + (Get-Command Remove-CCSADComputerFromSecurityGroup).Parameters['ComputerName'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory SecurityGroupName parameter' { + (Get-Command Remove-CCSADComputerFromSecurityGroup).Parameters['SecurityGroupName'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory Domain parameter' { + (Get-Command Remove-CCSADComputerFromSecurityGroup).Parameters['Domain'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory Url parameter' { + (Get-Command Remove-CCSADComputerFromSecurityGroup).Parameters['Url'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory CCSCredential parameter' { + (Get-Command Remove-CCSADComputerFromSecurityGroup).Parameters['CCSCredential'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have optional DomainCredential parameter' { + (Get-Command Remove-CCSADComputerFromSecurityGroup).Parameters['DomainCredential'].Attributes.Mandatory | Should -Be $false + } + + It 'Should have optional DomainOUPath parameter' { + (Get-Command Remove-CCSADComputerFromSecurityGroup).Parameters['DomainOUPath'].Attributes.Mandatory | Should -Be $false + } + + It 'Should accept array of computer names' { + (Get-Command Remove-CCSADComputerFromSecurityGroup).Parameters['ComputerName'].ParameterType.Name | Should -Be 'String[]' + } + + It 'Should accept pipeline input for ComputerName' { + (Get-Command Remove-CCSADComputerFromSecurityGroup).Parameters['ComputerName'].Attributes.ValueFromPipeline | Should -Be $true + } + } + + Context 'Parameter Aliases' { + + It 'Should have alias "Name" for ComputerName' { + (Get-Command Remove-CCSADComputerFromSecurityGroup).Parameters['ComputerName'].Aliases | Should -Contain 'Name' + } + + It 'Should have alias "Computer" for ComputerName' { + (Get-Command Remove-CCSADComputerFromSecurityGroup).Parameters['ComputerName'].Aliases | Should -Contain 'Computer' + } + + It 'Should have alias "GroupName" for SecurityGroupName' { + (Get-Command Remove-CCSADComputerFromSecurityGroup).Parameters['SecurityGroupName'].Aliases | Should -Contain 'GroupName' + } + + It 'Should have alias "OU" for DomainOUPath' { + (Get-Command Remove-CCSADComputerFromSecurityGroup).Parameters['DomainOUPath'].Aliases | Should -Contain 'OU' + } + } + + Context 'DomainOUPath Validation' { + + It 'Should accept empty DomainOUPath' { + { Remove-CCSADComputerFromSecurityGroup -ComputerName 'PC01' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath '' -WhatIf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept standard DN format' { + { Remove-CCSADComputerFromSecurityGroup -ComputerName 'PC01' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'OU=Computers,DC=Firmax,DC=local' -WhatIf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept DC-only format' { + { Remove-CCSADComputerFromSecurityGroup -ComputerName 'PC01' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'DC=Firmax,DC=local' -WhatIf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept LDAP format' { + { Remove-CCSADComputerFromSecurityGroup -ComputerName 'PC01' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'LDAP://DC01.Firmax.local/OU=Computers,DC=Firmax,DC=local' -WhatIf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should reject invalid format' { + { Remove-CCSADComputerFromSecurityGroup -ComputerName 'PC01' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'InvalidPath' -WhatIf } | Should -Throw + } + } + + Context 'URL Validation' { + + It 'Should accept HTTPS URL' { + { Remove-CCSADComputerFromSecurityGroup -ComputerName 'PC01' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should reject HTTP URL (only HTTPS allowed)' { + $httpUrl = $script:TestUrl -replace '^https:', 'http:' + { Remove-CCSADComputerFromSecurityGroup -ComputerName 'PC01' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $httpUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Throw + } + + It 'Should reject URL without protocol' { + { Remove-CCSADComputerFromSecurityGroup -ComputerName 'PC01' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url 'test.com/CCS.asmx' -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Throw + } + } + + Context 'Domain Validation' { + + It 'Should accept valid domain format' { + { Remove-CCSADComputerFromSecurityGroup -ComputerName 'PC01' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept subdomain format' { + { Remove-CCSADComputerFromSecurityGroup -ComputerName 'PC01' -SecurityGroupName 'TestGroup' -Domain 'sub.firmax.local' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should reject invalid domain format' { + { Remove-CCSADComputerFromSecurityGroup -ComputerName 'PC01' -SecurityGroupName 'TestGroup' -Domain 'invalid_domain' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Throw + } + } + + Context 'ShouldProcess Support' { + + It 'Should support WhatIf' { + (Get-Command Remove-CCSADComputerFromSecurityGroup).Parameters.ContainsKey('WhatIf') | Should -Be $true + } + + It 'Should support Confirm' { + (Get-Command Remove-CCSADComputerFromSecurityGroup).Parameters.ContainsKey('Confirm') | Should -Be $true + } + } + + Context 'CmdletBinding Attributes' { + + It 'Should be an advanced function' { + (Get-Command Remove-CCSADComputerFromSecurityGroup).CmdletBinding | Should -Be $true + } + + It 'Should have SupportsShouldProcess' { + (Get-Command Remove-CCSADComputerFromSecurityGroup).Parameters['WhatIf'] | Should -Not -BeNullOrEmpty + } + + It 'Should have OutputType defined' { + (Get-Command Remove-CCSADComputerFromSecurityGroup).OutputType.Name | Should -Contain 'System.String' + } + } +} + +Describe 'Remove-CCSADComputerFromSecurityGroup - Integration Tests' -Tag 'Integration' { + + Context 'Live CCS Web Service Operations' { + + BeforeAll { + # Use existing test group name + $script:TestGroupName = "TestPester" + $script:TestComputerName = 'PESTER-TEST-PC' + } + + It 'Should connect to CCS Web Service successfully' { + { Remove-CCSADComputerFromSecurityGroup -ComputerName $script:TestComputerName -SecurityGroupName $script:TestGroupName -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should remove computer from security group with domain credentials' { + { Remove-CCSADComputerFromSecurityGroup -ComputerName $script:TestComputerName -SecurityGroupName $script:TestGroupName -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -Confirm:$false -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should process multiple computers' { + $computers = @("$script:TestComputerName`1", "$script:TestComputerName`2") + { $computers | Remove-CCSADComputerFromSecurityGroup -SecurityGroupName $script:TestGroupName -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -Confirm:$false -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should convert LDAP path format' { + $ldapPath = 'LDAP://DC01.Firmax.local/DC=Firmax,DC=local' + { Remove-CCSADComputerFromSecurityGroup -ComputerName $script:TestComputerName -SecurityGroupName $script:TestGroupName -DomainOUPath $ldapPath -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -Confirm:$false -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + } + + Context 'Error Handling' { + + It 'Should handle invalid credentials gracefully' { + $badCred = New-Object System.Management.Automation.PSCredential('baduser', (ConvertTo-SecureString 'badpass' -AsPlainText -Force)) + { Remove-CCSADComputerFromSecurityGroup -ComputerName 'TestPC' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $badCred -ErrorAction Stop } | Should -Throw + } + + It 'Should handle non-existent URL gracefully' { + { Remove-CCSADComputerFromSecurityGroup -ComputerName 'TestPC' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url 'https://nonexistent.invalid/CCS.asmx' -CCSCredential $script:TestCCSCredential -ErrorAction Stop } | Should -Throw + } + } +} + +Describe 'Remove-CCSADComputerFromSecurityGroup - Performance Tests' -Tag 'Performance' { + + Context 'Pipeline Performance' { + + It 'Should process pipeline input efficiently' { + $computers = 1..10 | ForEach-Object { "PC$_" } + $measure = Measure-Command { + $computers | Remove-CCSADComputerFromSecurityGroup -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf -Confirm:$false -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + } + $measure.TotalSeconds | Should -BeLessThan 5 + } + } +} diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Remove-CCSADComputerFromSecurityGroup.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Remove-CCSADComputerFromSecurityGroup.ps1 new file mode 100644 index 00000000..826ea3df --- /dev/null +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Remove-CCSADComputerFromSecurityGroup.ps1 @@ -0,0 +1,341 @@ +function Remove-CCSADComputerFromSecurityGroup { + <# + .SYNOPSIS + Removes a computer from a security group in Active Directory using the CCS Web Service. + + .DESCRIPTION + Removes a computer from a security group in Active Directory using the CCS Web Service. + This advanced function includes comprehensive error handling, input validation, and supports pipeline input. + + .PARAMETER ComputerName + The name of the computer to be removed from the security group. + Supports pipeline input and accepts multiple computer names. + + .PARAMETER SecurityGroupName + The name of the security group from which the computer will be removed. + + .PARAMETER DomainOUPath + The Organizational Unit (OU) path in which the computer resides. + If not specified, searches the entire domain. + Supports both standard DN format (OU=Computers,DC=example,DC=com) and LDAP format (LDAP://DC01.example.local/OU=Computers,DC=example,DC=com). + + .PARAMETER Domain + The domain in which the computer resides. + Must be a valid domain name format. + + .PARAMETER Url + The URL of the CCS Web Service. + Must be a valid URI format. Example: "https://example.com/CCSWebservice/CCS.asmx" + + .PARAMETER CCSCredential + The credentials used to authenticate with the CCS Web Service. + + .PARAMETER DomainCredential + The credentials of an account with permissions to remove the computer from the security group. + If not defined, it will run in the CCS Web Service context. + + .PARAMETER PasswordIsEncrypted + Indicates if the password in the DomainCredential is encrypted. Default is $false. + + .EXAMPLE + PS C:\> Remove-CCSADComputerFromSecurityGroup -ComputerName "TestPC" -SecurityGroupName "TestGroup" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Removes TestPC from TestGroup using default CCS context. + + .EXAMPLE + PS C:\> Remove-CCSADComputerFromSecurityGroup -ComputerName "TestPC" -SecurityGroupName "TestGroup" -DomainOUPath "OU=Computers,DC=example,DC=com" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential -DomainCredential $DomainCredential + + Removes TestPC from TestGroup using specific domain credentials and OU path. + + .EXAMPLE + PS C:\> "PC01", "PC02", "PC03" | Remove-CCSADComputerFromSecurityGroup -SecurityGroupName "TestGroup" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Removes multiple computers from TestGroup using pipeline input. + + .EXAMPLE + PS C:\> Remove-CCSADComputerFromSecurityGroup -ComputerName "TestPC" -SecurityGroupName "TestGroup" -DomainOUPath "LDAP://DC01.Firmax.local/DC=FirmaX,DC=local" -Domain "Firmax.local" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Removes TestPC from TestGroup using LDAP format for the OU path. The LDAP path will be automatically converted to standard DN format. + + .OUTPUTS + System.String + Returns the result message from the CCS Web Service operation. + + .NOTES + This is an advanced function with support for ShouldProcess, pipeline input, and comprehensive error handling. + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] + [OutputType([string])] + param ( + [Parameter( + Mandatory = $true, + Position = 0, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the computer name to remove from the security group' + )] + [ValidateNotNullOrEmpty()] + [Alias('Name', 'Computer', 'CN')] + [string[]]$ComputerName, + + [Parameter( + Mandatory = $true, + Position = 1, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the security group name' + )] + [ValidateNotNullOrEmpty()] + [Alias('GroupName', 'Group')] + [string]$SecurityGroupName, + + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] + [ValidateScript({ + if ([string]::IsNullOrEmpty($_)) { + return $true + } + # Support standard DN format: OU=...,DC=...,DC=... + if ($_ -match '^(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + # Support LDAP format: LDAP://server/DN + if ($_ -match '^LDAP://[^/]+/(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + throw "DomainOUPath must be in format 'OU=...,DC=...,DC=...' or 'LDAP://server/OU=...,DC=...,DC=...' or empty" + })] + [Alias('OU', 'Path')] + [string]$DomainOUPath = '', + + [Parameter( + Mandatory = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the domain name' + )] + [ValidateNotNullOrEmpty()] + [ValidatePattern('^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$')] + [string]$Domain, + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service URL' + )] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + if ($_ -match '^https://') { + $true + } else { + throw "URL must start with https:// (secure connection required)" + } + })] + [Alias('WebServiceUrl', 'Uri')] + [string]$Url, + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service credentials' + )] + [ValidateNotNull()] + [Alias('Credential', 'WebServiceCredential')] + [pscredential]$CCSCredential, + + [Parameter(Mandatory = $false)] + [ValidateNotNull()] + [Alias('ADCredential')] + [pscredential]$DomainCredential, + + [Parameter(Mandatory = $false)] + [Alias('Encrypted')] + [switch]$PasswordIsEncrypted + ) + + begin { + $FunctionName = $PSCmdlet.MyInvocation.MyCommand.Name + + Write-Verbose "[$FunctionName] Starting function execution" + Write-Verbose "[$FunctionName] SecurityGroupName: $SecurityGroupName" + Write-Verbose "[$FunctionName] Domain: $Domain" + Write-Verbose "[$FunctionName] DomainOUPath: $DomainOUPath" + Write-Verbose "[$FunctionName] URL: $Url" + + # Initialize CCS connection once in begin block + try { + Write-Verbose "[$FunctionName] Initializing CCS Web Service connection" + $CCS = Initialize-CCS -Url $Url -WebServiceCredential $CCSCredential -ErrorAction Stop + } catch { + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + Write-Debug "ErrorAction from PSBoundParameters: $($PSBoundParameters['ErrorAction'])" + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + Write-Debug "ErrorAction from ErrorActionPreference: $ErrorActionPreference" + } + Write-Debug "ShouldThrow is set to: $ShouldThrow" + + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to initialize CCS Web Service: $_" ` + -ErrorCategory ConnectionError ` + -TargetObject $Url ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Verify the URL is correct and the CCS Web Service is accessible. Check credentials." ` + -Throw:$ShouldThrow + return + } + + # Prepare domain credentials if provided + $ADUsername = $null + $ADPassword = $null + + if ($DomainCredential) { + $ADUsername = $DomainCredential.UserName + Write-Verbose "[$FunctionName] Using domain credential: $ADUsername" + + try { + if ($PasswordIsEncrypted) { + $ADPassword = $DomainCredential.GetNetworkCredential().Password + Write-Verbose "[$FunctionName] Using pre-encrypted password" + } else { + $ADPassword = Get-CCSEncryptedPassword -SecureString $DomainCredential.Password -ErrorAction Stop + Write-Verbose "[$FunctionName] Password encrypted successfully" + } + } catch { + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + } + + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to process domain credential password: $_" ` + -ErrorCategory SecurityError ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Ensure the domain credential is valid and the password can be encrypted." ` + -Throw:$ShouldThrow + return + } + } else { + Write-Verbose "[$FunctionName] No domain credential provided, using CCS Web Service context" + } + + $ProcessedCount = 0 + $SuccessCount = 0 + $FailureCount = 0 + } + + process { + foreach ($Computer in $ComputerName) { + $ProcessedCount++ + + try { + Write-Verbose "[$FunctionName] Processing computer: $Computer ($ProcessedCount)" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Processing: ComputerName=$Computer, SecurityGroupName=$SecurityGroupName, DomainOUPath=$DomainOUPath, Domain=$Domain") + } + + # ShouldProcess support + $WhatIfMessage = "Remove computer '$Computer' from security group '$SecurityGroupName' in domain '$Domain'" + if ($PSCmdlet.ShouldProcess($Computer, $WhatIfMessage)) { + Write-Verbose "[$FunctionName] Calling ActiveDirectory_RemoveComputerFromSecurityGroup for $Computer" + + $Result = $CCS.ActiveDirectory_RemoveComputerFromSecurityGroup( + $Computer, + $SecurityGroupName, + $DomainOUPath, + $Domain, + $ADUsername, + $ADPassword + ) + Write-Debug "Result from CCS Web Service: $Result" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Result for $Computer : $Result") + } + + Write-Verbose "[$FunctionName] Result for $Computer : $Result" + + # Check for errors in result + $IsError = Invoke-CCSIsError -Result $Result + + Write-Debug "IsError evaluation for result: $IsError" + if ($IsError) { + $FailureCount++ + + # Determine appropriate error category based on result message + $ErrorCat = [System.Management.Automation.ErrorCategory]::OperationStopped + $RecommendedActionText = "Verify the computer and security group names are correct." + + if ($Result -like '*does not exist*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::ObjectNotFound + $RecommendedActionText = "Verify that the computer '$Computer' and security group '$SecurityGroupName' exist in Active Directory." + } elseif ($Result -like '*unwilling to process*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::PermissionDenied + $RecommendedActionText = "Check that the domain credentials have sufficient permissions to modify the security group." + } elseif ($Result -like '*not a member*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::InvalidOperation + $RecommendedActionText = "The computer is not a member of the security group." + } + + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + Write-Debug "ErrorAction from PSBoundParameters: $($PSBoundParameters['ErrorAction'])" + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + Write-Debug "ErrorAction from ErrorActionPreference: $ErrorActionPreference" + } + Write-Debug "ShouldThrow is set to: $ShouldThrow" + + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to remove computer '$Computer' from security group '$SecurityGroupName': $Result" ` + -ErrorCategory $ErrorCat ` + -TargetObject $Computer ` + -FunctionName $FunctionName ` + -RecommendedAction $RecommendedActionText ` + -Throw:$ShouldThrow + } else { + $SuccessCount++ + Write-Verbose "[$FunctionName] Successfully removed $Computer from $SecurityGroupName" + Write-Output $Result + } + } else { + Write-Verbose "[$FunctionName] WhatIf: Would remove $Computer from $SecurityGroupName" + } + } catch { + $FailureCount++ + + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + } + + Invoke-CCSErrorHandling ` + -ErrorMessage "Exception occurred while processing computer '$Computer': $_" ` + -ErrorCategory InvalidOperation ` + -TargetObject $Computer ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Check the error details and verify all parameters are correct." ` + -Throw:$ShouldThrow + } + } + } + + end { + Write-Verbose "[$FunctionName] Function execution completed" + Write-Verbose "[$FunctionName] Total processed: $ProcessedCount | Success: $SuccessCount | Failed: $FailureCount" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Summary - Processed: $ProcessedCount, Success: $SuccessCount, Failed: $FailureCount") + } + } +} diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Remove-CCSADUserFromSecurityGroup.Tests.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Remove-CCSADUserFromSecurityGroup.Tests.ps1 new file mode 100644 index 00000000..cb529cd7 --- /dev/null +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Remove-CCSADUserFromSecurityGroup.Tests.ps1 @@ -0,0 +1,243 @@ +BeforeAll { + $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent + + $Items = Get-ChildItem -Path "$RootPath\Capa.PowerShell.Module.CCS\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + + # Setup test environment + $script:TestUrl = "https://$(hostname).capainstaller.com/CCSWebservice/CCS.asmx" + $script:TestDomain = 'Firmax.local' + $CredentialPath = "D:\PowerShell\Credentials\$($env:USERNAME)DomainAdminPesterTests.xml" + + # Setup credentials from environment variables (GitHub secrets) + if ($env:DOMAINADMINUSERNAME -and $env:DOMAINADMINPASSWORD) { + $securePassword = ConvertTo-SecureString $env:DOMAINADMINPASSWORD -AsPlainText -Force + $script:TestDomainCredential = New-Object System.Management.Automation.PSCredential($env:DOMAINADMINUSERNAME, $securePassword) + $script:TestCCSCredential = $script:TestDomainCredential + } elseif (Test-Path -Path $CredentialPath) { + $script:TestDomainCredential = Import-Clixml -Path $CredentialPath + $script:TestCCSCredential = $script:TestDomainCredential + } else { + $script:TestDomainCredential = Get-Credential -Message 'Enter domain admin credentials for integration tests' + $script:TestCCSCredential = $script:TestDomainCredential + } + + $DebugPreference = 'Continue' + $ErrorActionPreference = 'Stop' +} + +Describe 'Remove-CCSADUserFromSecurityGroup' -Tag 'Unit' { + + Context 'Parameter Validation' { + + It 'Should have mandatory UserName parameter' { + (Get-Command Remove-CCSADUserFromSecurityGroup).Parameters['UserName'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory SecurityGroupName parameter' { + (Get-Command Remove-CCSADUserFromSecurityGroup).Parameters['SecurityGroupName'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory Domain parameter' { + (Get-Command Remove-CCSADUserFromSecurityGroup).Parameters['Domain'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory Url parameter' { + (Get-Command Remove-CCSADUserFromSecurityGroup).Parameters['Url'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory CCSCredential parameter' { + (Get-Command Remove-CCSADUserFromSecurityGroup).Parameters['CCSCredential'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have optional DomainCredential parameter' { + (Get-Command Remove-CCSADUserFromSecurityGroup).Parameters['DomainCredential'].Attributes.Mandatory | Should -Be $false + } + + It 'Should have optional DomainOUPath parameter' { + (Get-Command Remove-CCSADUserFromSecurityGroup).Parameters['DomainOUPath'].Attributes.Mandatory | Should -Be $false + } + + It 'Should accept array of user names' { + (Get-Command Remove-CCSADUserFromSecurityGroup).Parameters['UserName'].ParameterType.Name | Should -Be 'String[]' + } + + It 'Should accept pipeline input for UserName' { + (Get-Command Remove-CCSADUserFromSecurityGroup).Parameters['UserName'].Attributes.ValueFromPipeline | Should -Be $true + } + } + + Context 'Parameter Aliases' { + + It 'Should have alias "Name" for UserName' { + (Get-Command Remove-CCSADUserFromSecurityGroup).Parameters['UserName'].Aliases | Should -Contain 'Name' + } + + It 'Should have alias "User" for UserName' { + (Get-Command Remove-CCSADUserFromSecurityGroup).Parameters['UserName'].Aliases | Should -Contain 'User' + } + + It 'Should have alias "GroupName" for SecurityGroupName' { + (Get-Command Remove-CCSADUserFromSecurityGroup).Parameters['SecurityGroupName'].Aliases | Should -Contain 'GroupName' + } + + It 'Should have alias "Group" for SecurityGroupName' { + (Get-Command Remove-CCSADUserFromSecurityGroup).Parameters['SecurityGroupName'].Aliases | Should -Contain 'Group' + } + + It 'Should have alias "OU" for DomainOUPath' { + (Get-Command Remove-CCSADUserFromSecurityGroup).Parameters['DomainOUPath'].Aliases | Should -Contain 'OU' + } + } + + Context 'DomainOUPath Validation' { + + It 'Should accept empty DomainOUPath' { + { Remove-CCSADUserFromSecurityGroup -UserName 'TestUser' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath '' -WhatIf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept standard DN format' { + { Remove-CCSADUserFromSecurityGroup -UserName 'TestUser' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'OU=Users,DC=Firmax,DC=local' -WhatIf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept DC-only format' { + { Remove-CCSADUserFromSecurityGroup -UserName 'TestUser' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'DC=Firmax,DC=local' -WhatIf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept LDAP format' { + { Remove-CCSADUserFromSecurityGroup -UserName 'TestUser' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'LDAP://DC01.Firmax.local/OU=Users,DC=Firmax,DC=local' -WhatIf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should reject invalid format' { + { Remove-CCSADUserFromSecurityGroup -UserName 'TestUser' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'InvalidPath' -WhatIf } | Should -Throw + } + } + + Context 'URL Validation' { + + It 'Should accept HTTPS URL' { + { Remove-CCSADUserFromSecurityGroup -UserName 'TestUser' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should reject HTTP URL (only HTTPS allowed)' { + $httpUrl = $script:TestUrl -replace '^https:', 'http:' + { Remove-CCSADUserFromSecurityGroup -UserName 'TestUser' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $httpUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Throw + } + + It 'Should reject URL without protocol' { + { Remove-CCSADUserFromSecurityGroup -UserName 'TestUser' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url 'test.com/CCS.asmx' -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Throw + } + } + + Context 'Domain Validation' { + + It 'Should accept valid domain format' { + { Remove-CCSADUserFromSecurityGroup -UserName 'TestUser' -SecurityGroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should accept subdomain format' { + { Remove-CCSADUserFromSecurityGroup -UserName 'TestUser' -SecurityGroupName 'TestGroup' -Domain 'sub.firmax.local' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should reject invalid domain format' { + { Remove-CCSADUserFromSecurityGroup -UserName 'TestUser' -SecurityGroupName 'TestGroup' -Domain 'invalid_domain' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -WhatIf } | Should -Throw + } + } + + Context 'ShouldProcess Support' { + + It 'Should support WhatIf' { + (Get-Command Remove-CCSADUserFromSecurityGroup).Parameters.ContainsKey('WhatIf') | Should -Be $true + } + + It 'Should support Confirm' { + (Get-Command Remove-CCSADUserFromSecurityGroup).Parameters.ContainsKey('Confirm') | Should -Be $true + } + } + + Context 'CmdletBinding Attributes' { + + It 'Should be an advanced function' { + (Get-Command Remove-CCSADUserFromSecurityGroup).CmdletBinding | Should -Be $true + } + + It 'Should have SupportsShouldProcess' { + (Get-Command Remove-CCSADUserFromSecurityGroup).Parameters['WhatIf'] | Should -Not -BeNullOrEmpty + } + + It 'Should have OutputType defined' { + (Get-Command Remove-CCSADUserFromSecurityGroup).OutputType.Name | Should -Contain 'System.String' + } + } +} + +Describe "Remove-CCSADUserFromSecurityGroup Integration Tests" -Tag 'Integration' { + + BeforeAll { + $TestUserName = "TestUser_$([guid]::NewGuid().ToString().Substring(0, 8))" + $TestSecurityGroupName = "TestGroup_Remove" + $TestDomain = "Firmax.local" + $TestUrl = "https://psccsdev.firmax.local:443/CCSWebservice/CCS.asmx" + $TestCCSCredentialUsername = "admin" + $TestCCSCredentialPassword = ConvertTo-SecureString "Iamhim" -AsPlainText -Force + $TestCCSCredential = New-Object System.Management.Automation.PSCredential($TestCCSCredentialUsername, $TestCCSCredentialPassword) + $TestDomainOUPath = "DC=FirmaX,DC=local" + } + + It "Should remove a user from a security group successfully" -Skip { + $Result = Remove-CCSADUserFromSecurityGroup -UserName $TestUserName -SecurityGroupName $TestSecurityGroupName -DomainOUPath $TestDomainOUPath -Domain $TestDomain -Url $TestUrl -CCSCredential $TestCCSCredential -Confirm:$false -ErrorAction SilentlyContinue + $Result | Should -Not -BeNullOrEmpty + } + + It "Should handle multiple users via pipeline" -Skip { + $Users = @("User1", "User2", "User3") + $Result = $Users | Remove-CCSADUserFromSecurityGroup -SecurityGroupName $TestSecurityGroupName -DomainOUPath $TestDomainOUPath -Domain $TestDomain -Url $TestUrl -CCSCredential $TestCCSCredential -Confirm:$false -ErrorAction SilentlyContinue + $Result.Count | Should -Be 3 + } + + It "Should support WhatIf parameter" { + $Result = Remove-CCSADUserFromSecurityGroup -UserName $TestUserName -SecurityGroupName $TestSecurityGroupName -DomainOUPath $TestDomainOUPath -Domain $TestDomain -Url $TestUrl -CCSCredential $TestCCSCredential -WhatIf -Confirm:$false -ErrorAction SilentlyContinue + $Result | Should -BeNullOrEmpty + } + + It "Should handle LDAP format for DomainOUPath" -Skip { + $LdapPath = "LDAP://DCFIRMAX01.Firmax.local/DC=FirmaX,DC=local" + $Result = Remove-CCSADUserFromSecurityGroup -UserName $TestUserName -SecurityGroupName $TestSecurityGroupName -DomainOUPath $LdapPath -Domain $TestDomain -Url $TestUrl -CCSCredential $TestCCSCredential -Confirm:$false -ErrorAction SilentlyContinue + $Result | Should -Not -BeNullOrEmpty + } + + It "Should handle empty DomainOUPath" -Skip { + $Result = Remove-CCSADUserFromSecurityGroup -UserName $TestUserName -SecurityGroupName $TestSecurityGroupName -DomainOUPath "" -Domain $TestDomain -Url $TestUrl -CCSCredential $TestCCSCredential -Confirm:$false -ErrorAction SilentlyContinue + $Result | Should -Not -BeNullOrEmpty + } + + It "Should handle non-existent user gracefully" { + $Result = Remove-CCSADUserFromSecurityGroup -UserName "NonExistentUser_xyz123" -SecurityGroupName $TestSecurityGroupName -DomainOUPath $TestDomainOUPath -Domain $TestDomain -Url $TestUrl -CCSCredential $TestCCSCredential -Confirm:$false -ErrorAction SilentlyContinue + # Should not throw, but may return error message + $true | Should -Be $true + } +} + +Describe "Remove-CCSADUserFromSecurityGroup Performance Tests" -Tag 'Performance' { + + BeforeAll { + $TestUserName = "TestUser_$([guid]::NewGuid().ToString().Substring(0, 8))" + $TestSecurityGroupName = "TestGroup_Remove" + $TestDomain = "Firmax.local" + $TestUrl = "https://psccsdev.firmax.local:443/CCSWebservice/CCS.asmx" + $TestCCSCredentialUsername = "admin" + $TestCCSCredentialPassword = ConvertTo-SecureString "Iamhim" -AsPlainText -Force + $TestCCSCredential = New-Object System.Management.Automation.PSCredential($TestCCSCredentialUsername, $TestCCSCredentialPassword) + $TestDomainOUPath = "DC=FirmaX,DC=local" + } + + It "Should process 10 users in reasonable time" -Skip { + $Users = 1..10 | ForEach-Object { "TestUser_$_" } + $Stopwatch = [System.Diagnostics.Stopwatch]::StartNew() + $Users | Remove-CCSADUserFromSecurityGroup -SecurityGroupName $TestSecurityGroupName -DomainOUPath $TestDomainOUPath -Domain $TestDomain -Url $TestUrl -CCSCredential $TestCCSCredential -Confirm:$false -ErrorAction SilentlyContinue + $Stopwatch.Stop() + $Stopwatch.Elapsed.TotalSeconds | Should -BeLessThan 30 + } +} diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Remove-CCSADUserFromSecurityGroup.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Remove-CCSADUserFromSecurityGroup.ps1 new file mode 100644 index 00000000..b963bae8 --- /dev/null +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Remove-CCSADUserFromSecurityGroup.ps1 @@ -0,0 +1,341 @@ +function Remove-CCSADUserFromSecurityGroup { + <# + .SYNOPSIS + Removes a user from a security group in Active Directory using the CCS Web Service. + + .DESCRIPTION + Removes a user from a security group in Active Directory using the CCS Web Service. + This advanced function includes comprehensive error handling, input validation, and supports pipeline input. + + .PARAMETER UserName + The name of the user to be removed from the security group. + Supports pipeline input and accepts multiple user names. + + .PARAMETER SecurityGroupName + The name of the security group from which the user will be removed. + + .PARAMETER DomainOUPath + The Organizational Unit (OU) path in which the user resides. + If not specified, searches the entire domain. + Supports both standard DN format (OU=Users,DC=example,DC=com) and LDAP format (LDAP://DC01.example.local/OU=Users,DC=example,DC=com). + + .PARAMETER Domain + The domain in which the user resides. + Must be a valid domain name format. + + .PARAMETER Url + The URL of the CCS Web Service. + Must be a valid URI format. Example: "https://example.com/CCSWebservice/CCS.asmx" + + .PARAMETER CCSCredential + The credentials used to authenticate with the CCS Web Service. + + .PARAMETER DomainCredential + The credentials of an account with permissions to remove the user from the security group. + If not defined, it will run in the CCS Web Service context. + + .PARAMETER PasswordIsEncrypted + Indicates if the password in the DomainCredential is encrypted. Default is $false. + + .EXAMPLE + PS C:\> Remove-CCSADUserFromSecurityGroup -UserName "TestUser" -SecurityGroupName "TestGroup" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Removes TestUser from TestGroup using default CCS context. + + .EXAMPLE + PS C:\> Remove-CCSADUserFromSecurityGroup -UserName "TestUser" -SecurityGroupName "TestGroup" -DomainOUPath "OU=Users,DC=example,DC=com" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential -DomainCredential $DomainCredential + + Removes TestUser from TestGroup using specific domain credentials and OU path. + + .EXAMPLE + PS C:\> "User01", "User02", "User03" | Remove-CCSADUserFromSecurityGroup -SecurityGroupName "TestGroup" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Removes multiple users from TestGroup using pipeline input. + + .EXAMPLE + PS C:\> Remove-CCSADUserFromSecurityGroup -UserName "TestUser" -SecurityGroupName "TestGroup" -DomainOUPath "LDAP://DC01.Firmax.local/DC=FirmaX,DC=local" -Domain "Firmax.local" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Removes TestUser from TestGroup using LDAP format for the OU path. The LDAP path will be automatically converted to standard DN format. + + .OUTPUTS + System.String + Returns the result message from the CCS Web Service operation. + + .NOTES + This is an advanced function with support for ShouldProcess, pipeline input, and comprehensive error handling. + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] + [OutputType([string])] + param ( + [Parameter( + Mandatory = $true, + Position = 0, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the user name to remove from the security group' + )] + [ValidateNotNullOrEmpty()] + [Alias('Name', 'User', 'SamAccountName')] + [string[]]$UserName, + + [Parameter( + Mandatory = $true, + Position = 1, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the security group name' + )] + [ValidateNotNullOrEmpty()] + [Alias('GroupName', 'Group')] + [string]$SecurityGroupName, + + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] + [ValidateScript({ + if ([string]::IsNullOrEmpty($_)) { + return $true + } + # Support standard DN format: OU=...,DC=...,DC=... + if ($_ -match '^(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + # Support LDAP format: LDAP://server/DN + if ($_ -match '^LDAP://[^/]+/(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + throw "DomainOUPath must be in format 'OU=...,DC=...,DC=...' or 'LDAP://server/OU=...,DC=...,DC=...' or empty" + })] + [Alias('OU', 'Path')] + [string]$DomainOUPath = '', + + [Parameter( + Mandatory = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the domain name' + )] + [ValidateNotNullOrEmpty()] + [ValidatePattern('^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$')] + [string]$Domain, + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service URL' + )] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + if ($_ -match '^https://') { + $true + } else { + throw "URL must start with https:// (secure connection required)" + } + })] + [Alias('WebServiceUrl', 'Uri')] + [string]$Url, + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service credentials' + )] + [ValidateNotNull()] + [Alias('Credential', 'WebServiceCredential')] + [pscredential]$CCSCredential, + + [Parameter(Mandatory = $false)] + [ValidateNotNull()] + [Alias('ADCredential')] + [pscredential]$DomainCredential, + + [Parameter(Mandatory = $false)] + [Alias('Encrypted')] + [switch]$PasswordIsEncrypted + ) + + begin { + $FunctionName = $PSCmdlet.MyInvocation.MyCommand.Name + + Write-Verbose "[$FunctionName] Starting function execution" + Write-Verbose "[$FunctionName] SecurityGroupName: $SecurityGroupName" + Write-Verbose "[$FunctionName] Domain: $Domain" + Write-Verbose "[$FunctionName] DomainOUPath: $DomainOUPath" + Write-Verbose "[$FunctionName] URL: $Url" + + # Initialize CCS connection once in begin block + try { + Write-Verbose "[$FunctionName] Initializing CCS Web Service connection" + $CCS = Initialize-CCS -Url $Url -WebServiceCredential $CCSCredential -ErrorAction Stop + } catch { + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + Write-Debug "ErrorAction from PSBoundParameters: $($PSBoundParameters['ErrorAction'])" + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + Write-Debug "ErrorAction from ErrorActionPreference: $ErrorActionPreference" + } + Write-Debug "ShouldThrow is set to: $ShouldThrow" + + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to initialize CCS Web Service: $_" ` + -ErrorCategory ConnectionError ` + -TargetObject $Url ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Verify the URL is correct and the CCS Web Service is accessible. Check credentials." ` + -Throw:$ShouldThrow + return + } + + # Prepare domain credentials if provided + $ADUsername = $null + $ADPassword = $null + + if ($DomainCredential) { + $ADUsername = $DomainCredential.UserName + Write-Verbose "[$FunctionName] Using domain credential: $ADUsername" + + try { + if ($PasswordIsEncrypted) { + $ADPassword = $DomainCredential.GetNetworkCredential().Password + Write-Verbose "[$FunctionName] Using pre-encrypted password" + } else { + $ADPassword = Get-CCSEncryptedPassword -SecureString $DomainCredential.Password -ErrorAction Stop + Write-Verbose "[$FunctionName] Password encrypted successfully" + } + } catch { + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + } + + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to process domain credential password: $_" ` + -ErrorCategory SecurityError ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Ensure the domain credential is valid and the password can be encrypted." ` + -Throw:$ShouldThrow + return + } + } else { + Write-Verbose "[$FunctionName] No domain credential provided, using CCS Web Service context" + } + + $ProcessedCount = 0 + $SuccessCount = 0 + $FailureCount = 0 + } + + process { + foreach ($User in $UserName) { + $ProcessedCount++ + + try { + Write-Verbose "[$FunctionName] Processing user: $User ($ProcessedCount)" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Processing: UserName=$User, SecurityGroupName=$SecurityGroupName, DomainOUPath=$DomainOUPath, Domain=$Domain") + } + + # ShouldProcess support + $WhatIfMessage = "Remove user '$User' from security group '$SecurityGroupName' in domain '$Domain'" + if ($PSCmdlet.ShouldProcess($User, $WhatIfMessage)) { + Write-Verbose "[$FunctionName] Calling ActiveDirectory_RemoveUserFromSecurityGroup for $User" + + $Result = $CCS.ActiveDirectory_RemoveUserFromSecurityGroup( + $User, + $SecurityGroupName, + $DomainOUPath, + $Domain, + $ADUsername, + $ADPassword + ) + Write-Debug "Result from CCS Web Service: $Result" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Result for $User : $Result") + } + + Write-Verbose "[$FunctionName] Result for $User : $Result" + + # Check for errors in result + $IsError = Invoke-CCSIsError -Result $Result + + Write-Debug "IsError evaluation for result: $IsError" + if ($IsError) { + $FailureCount++ + + # Determine appropriate error category based on result message + $ErrorCat = [System.Management.Automation.ErrorCategory]::OperationStopped + $RecommendedActionText = "Verify the user and security group names are correct." + + if ($Result -like '*does not exist*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::ObjectNotFound + $RecommendedActionText = "Verify that the user '$User' and security group '$SecurityGroupName' exist in Active Directory." + } elseif ($Result -like '*unwilling to process*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::PermissionDenied + $RecommendedActionText = "Check that the domain credentials have sufficient permissions to modify the security group." + } elseif ($Result -like '*not a member*') { + $ErrorCat = [System.Management.Automation.ErrorCategory]::InvalidOperation + $RecommendedActionText = "The user is not a member of the security group." + } + + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + Write-Debug "ErrorAction from PSBoundParameters: $($PSBoundParameters['ErrorAction'])" + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + Write-Debug "ErrorAction from ErrorActionPreference: $ErrorActionPreference" + } + Write-Debug "ShouldThrow is set to: $ShouldThrow" + + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to remove user '$User' from security group '$SecurityGroupName': $Result" ` + -ErrorCategory $ErrorCat ` + -TargetObject $User ` + -FunctionName $FunctionName ` + -RecommendedAction $RecommendedActionText ` + -Throw:$ShouldThrow + } else { + $SuccessCount++ + Write-Verbose "[$FunctionName] Successfully removed $User from $SecurityGroupName" + Write-Output $Result + } + } else { + Write-Verbose "[$FunctionName] WhatIf: Would remove $User from $SecurityGroupName" + } + } catch { + $FailureCount++ + + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + } + + Invoke-CCSErrorHandling ` + -ErrorMessage "Exception occurred while processing user '$User': $_" ` + -ErrorCategory InvalidOperation ` + -TargetObject $User ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Check the error details and verify all parameters are correct." ` + -Throw:$ShouldThrow + } + } + } + + end { + Write-Verbose "[$FunctionName] Function execution completed" + Write-Verbose "[$FunctionName] Total processed: $ProcessedCount | Success: $SuccessCount | Failed: $FailureCount" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Summary - Processed: $ProcessedCount, Success: $SuccessCount, Failed: $FailureCount") + } + } +} diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Test-CCSADIsUserMemberOfGroup.Tests.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Test-CCSADIsUserMemberOfGroup.Tests.ps1 new file mode 100644 index 00000000..df0c1b26 --- /dev/null +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Test-CCSADIsUserMemberOfGroup.Tests.ps1 @@ -0,0 +1,209 @@ +BeforeAll { + $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent + + $Items = Get-ChildItem -Path "$RootPath\Capa.PowerShell.Module.CCS\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + + # Setup test environment + $script:TestUrl = "https://$(hostname).capainstaller.com/CCSWebservice/CCS.asmx" + $script:TestDomain = 'Firmax.local' + $CredentialPath = "D:\PowerShell\Credentials\$($env:USERNAME)DomainAdminPesterTests.xml" + + # Setup credentials from environment variables (GitHub secrets) + if ($env:DOMAINADMINUSERNAME -and $env:DOMAINADMINPASSWORD) { + $securePassword = ConvertTo-SecureString $env:DOMAINADMINPASSWORD -AsPlainText -Force + $script:TestDomainCredential = New-Object System.Management.Automation.PSCredential($env:DOMAINADMINUSERNAME, $securePassword) + $script:TestCCSCredential = $script:TestDomainCredential + } elseif (Test-Path -Path $CredentialPath) { + $script:TestDomainCredential = Import-Clixml -Path $CredentialPath + $script:TestCCSCredential = $script:TestDomainCredential + } else { + $script:TestDomainCredential = Get-Credential -Message 'Enter domain admin credentials for integration tests' + $script:TestCCSCredential = $script:TestDomainCredential + } + + $DebugPreference = 'Continue' + $ErrorActionPreference = 'Stop' +} + +Describe 'Test-CCSADIsUserMemberOfGroup' -Tag 'Unit' { + + Context 'Parameter Validation' { + + It 'Should have mandatory UserName parameter' { + (Get-Command Test-CCSADIsUserMemberOfGroup).Parameters['UserName'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory GroupName parameter' { + (Get-Command Test-CCSADIsUserMemberOfGroup).Parameters['GroupName'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory Domain parameter' { + (Get-Command Test-CCSADIsUserMemberOfGroup).Parameters['Domain'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory Url parameter' { + (Get-Command Test-CCSADIsUserMemberOfGroup).Parameters['Url'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have mandatory CCSCredential parameter' { + (Get-Command Test-CCSADIsUserMemberOfGroup).Parameters['CCSCredential'].Attributes.Mandatory | Should -Be $true + } + + It 'Should have optional DomainCredential parameter' { + (Get-Command Test-CCSADIsUserMemberOfGroup).Parameters['DomainCredential'].Attributes.Mandatory | Should -Be $false + } + + It 'Should have optional DomainOUPath parameter' { + (Get-Command Test-CCSADIsUserMemberOfGroup).Parameters['DomainOUPath'].Attributes.Mandatory | Should -Be $false + } + + It 'Should accept pipeline input for UserName' { + (Get-Command Test-CCSADIsUserMemberOfGroup).Parameters['UserName'].Attributes.ValueFromPipeline | Should -Be $true + } + } + + Context 'Parameter Aliases' { + + It 'Should have alias "Name" for UserName' { + (Get-Command Test-CCSADIsUserMemberOfGroup).Parameters['UserName'].Aliases | Should -Contain 'Name' + } + + It 'Should have alias "User" for UserName' { + (Get-Command Test-CCSADIsUserMemberOfGroup).Parameters['UserName'].Aliases | Should -Contain 'User' + } + + It 'Should have alias "Group" for GroupName' { + (Get-Command Test-CCSADIsUserMemberOfGroup).Parameters['GroupName'].Aliases | Should -Contain 'Group' + } + + It 'Should have alias "OU" for DomainOUPath' { + (Get-Command Test-CCSADIsUserMemberOfGroup).Parameters['DomainOUPath'].Aliases | Should -Contain 'OU' + } + } + + Context 'DomainOUPath Validation' { + + It 'Should reject invalid format' { + { Test-CCSADIsUserMemberOfGroup -UserName 'testuser' -GroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainOUPath 'InvalidPath' -ErrorAction Stop } | Should -Throw + } + } + + Context 'URL Validation' { + + It 'Should reject HTTP URL (only HTTPS allowed)' { + $httpUrl = $script:TestUrl -replace '^https:', 'http:' + { Test-CCSADIsUserMemberOfGroup -UserName 'testuser' -GroupName 'TestGroup' -Domain $script:TestDomain -Url $httpUrl -CCSCredential $script:TestCCSCredential -ErrorAction Stop } | Should -Throw + } + + It 'Should reject URL without protocol' { + { Test-CCSADIsUserMemberOfGroup -UserName 'testuser' -GroupName 'TestGroup' -Domain $script:TestDomain -Url 'test.com/CCS.asmx' -CCSCredential $script:TestCCSCredential -ErrorAction Stop } | Should -Throw + } + } + + Context 'Domain Validation' { + + It 'Should reject invalid domain format' { + { Test-CCSADIsUserMemberOfGroup -UserName 'testuser' -GroupName 'TestGroup' -Domain 'invalid_domain' -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction Stop } | Should -Throw + } + } + + Context 'CmdletBinding Attributes' { + + It 'Should be an advanced function' { + (Get-Command Test-CCSADIsUserMemberOfGroup).CmdletBinding | Should -Be $true + } + + It 'Should NOT have SupportsShouldProcess (read-only operation)' { + (Get-Command Test-CCSADIsUserMemberOfGroup).Parameters['WhatIf'] | Should -BeNullOrEmpty + } + + It 'Should have OutputType attribute' { + $Command = Get-Command Test-CCSADIsUserMemberOfGroup + $Command.OutputType | Should -Not -BeNullOrEmpty + } + } +} + +Describe 'Test-CCSADIsUserMemberOfGroup - Integration Tests' -Tag 'Integration' { + + Context 'Live CCS Web Service Operations' { + + BeforeAll { + $script:TestUserName = $env:USERNAME + $script:TestGroupName = 'Domain Users' + } + + It 'Should connect to CCS Web Service successfully' { + { Test-CCSADIsUserMemberOfGroup -UserName $script:TestUserName -GroupName $script:TestGroupName -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } | Should -Not -Throw + } + + It 'Should return true for user that is member of group' { + $result = Test-CCSADIsUserMemberOfGroup -UserName $script:TestUserName -GroupName $script:TestGroupName -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Be $true + } + + It 'Should return false for user that is not member of group' { + $result = Test-CCSADIsUserMemberOfGroup -UserName $script:TestUserName -GroupName 'NonExistentGroup_XYZ123' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Be $false + } + + It 'Should test user membership with domain credentials' { + $result = Test-CCSADIsUserMemberOfGroup -UserName $script:TestUserName -GroupName $script:TestGroupName -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Be $true + } + + It 'Should process multiple users via pipeline' { + $users = @($script:TestUserName, $script:TestUserName) + $results = $users | Test-CCSADIsUserMemberOfGroup -GroupName $script:TestGroupName -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $results.Count | Should -Be 2 + $results | ForEach-Object { $_ | Should -Be $true } + } + + It 'Should convert LDAP path format' { + $ldapPath = 'LDAP://DC01.Firmax.local/DC=Firmax,DC=local' + $result = Test-CCSADIsUserMemberOfGroup -UserName $script:TestUserName -GroupName $script:TestGroupName -DomainOUPath $ldapPath -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -DomainCredential $script:TestDomainCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Be $true + } + + It 'Should handle empty DomainOUPath' { + $result = Test-CCSADIsUserMemberOfGroup -UserName $script:TestUserName -GroupName $script:TestGroupName -DomainOUPath '' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Be $true + } + } + + Context 'Error Handling' { + + It 'Should handle invalid credentials gracefully' { + $badCred = New-Object System.Management.Automation.PSCredential('baduser', (ConvertTo-SecureString 'badpass' -AsPlainText -Force)) + $result = Test-CCSADIsUserMemberOfGroup -UserName 'TestUser' -GroupName 'TestGroup' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $badCred -ErrorAction SilentlyContinue + $result | Should -Be $false + } + + It 'Should handle non-existent URL gracefully' { + $result = Test-CCSADIsUserMemberOfGroup -UserName 'TestUser' -GroupName 'TestGroup' -Domain $script:TestDomain -Url 'https://nonexistent.invalid/CCS.asmx' -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue + $result | Should -Be $false + } + + It 'Should handle non-existent user gracefully' { + $result = Test-CCSADIsUserMemberOfGroup -UserName 'NonExistentUser_XYZ123' -GroupName $script:TestGroupName -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result | Should -Be $false + } + } +} + +Describe 'Test-CCSADIsUserMemberOfGroup - Performance Tests' -Tag 'Performance' { + + Context 'Pipeline Performance' { + + It 'Should process pipeline input efficiently' { + $users = 1..10 | ForEach-Object { $env:USERNAME } + $measure = Measure-Command { + $users | Test-CCSADIsUserMemberOfGroup -GroupName 'Domain Users' -Domain $script:TestDomain -Url $script:TestUrl -CCSCredential $script:TestCCSCredential -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + } + $measure.TotalSeconds | Should -BeLessThan 10 + } + } +} diff --git a/Modules/Capa.PowerShell.Module.CCS/Dev/Test-CCSADIsUserMemberOfGroup.ps1 b/Modules/Capa.PowerShell.Module.CCS/Dev/Test-CCSADIsUserMemberOfGroup.ps1 new file mode 100644 index 00000000..e8b1e644 --- /dev/null +++ b/Modules/Capa.PowerShell.Module.CCS/Dev/Test-CCSADIsUserMemberOfGroup.ps1 @@ -0,0 +1,272 @@ +function Test-CCSADIsUserMemberOfGroup { + <# + .SYNOPSIS + Tests if a user is a member of a security group in Active Directory using the CCS Web Service. + + .DESCRIPTION + Tests if a user is a member of a security group in Active Directory using the CCS Web Service. + This advanced function includes comprehensive error handling and input validation. + + .PARAMETER UserName + The username to check for group membership. + This should be the user's login name (samAccountName). + + .PARAMETER GroupName + The name of the security group to check membership for. + + .PARAMETER DomainOUPath + The Organizational Unit (OU) path in which the user resides. + If not specified, searches the entire domain. + Supports both standard DN format (OU=Users,DC=example,DC=com) and LDAP format (LDAP://DC01.example.local/OU=Users,DC=example,DC=com). + + .PARAMETER Domain + The domain in which the user resides. + Must be a valid domain name format. + + .PARAMETER Url + The URL of the CCS Web Service. + Must be a valid URI format. Example: "https://example.com/CCSWebservice/CCS.asmx" + + .PARAMETER CCSCredential + The credentials used to authenticate with the CCS Web Service. + + .PARAMETER DomainCredential + The credentials of the user account to check for group membership. + If not provided, uses the CCS Web Service context. + + .PARAMETER PasswordIsEncrypted + Indicates if the password in the DomainCredential is encrypted. Default is $false. + + .EXAMPLE + PS C:\> Test-CCSADIsUserMemberOfGroup -UserName "jdoe" -GroupName "IT-Admins" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Tests if user 'jdoe' is a member of 'IT-Admins' group using default CCS context. + + .EXAMPLE + PS C:\> Test-CCSADIsUserMemberOfGroup -UserName "jdoe" -GroupName "IT-Admins" -DomainOUPath "OU=Users,DC=example,DC=com" -Domain "example.com" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential -DomainCredential $DomainCredential + + Tests if user 'jdoe' is a member of 'IT-Admins' group using specific domain credentials and OU path. + + .EXAMPLE + PS C:\> Test-CCSADIsUserMemberOfGroup -UserName "jdoe" -GroupName "IT-Admins" -DomainOUPath "LDAP://DC01.Firmax.local/DC=Firmax,DC=local" -Domain "Firmax.local" -Url "https://example.com/CCSWebservice/CCS.asmx" -CCSCredential $Credential + + Tests group membership using LDAP format for the OU path. The LDAP path will be automatically converted to standard DN format. + + .OUTPUTS + System.Boolean + Returns $true if the user is a member of the group, $false otherwise. + + .NOTES + This is an advanced function with comprehensive error handling. + For more information, see https://capasystems.atlassian.net/wiki/spaces/CI65DOC/pages/19306246741/ActiveDirectory+isUserMemberOf + + .LINK + https://capasystems.atlassian.net/wiki/spaces/CI65DOC/pages/19306246741/ActiveDirectory+isUserMemberOf + #> + [CmdletBinding()] + [OutputType([System.Boolean])] + param ( + [Parameter( + Mandatory = $true, + Position = 0, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the username to check for group membership' + )] + [ValidateNotNullOrEmpty()] + [Alias('Name', 'User', 'SamAccountName')] + [string]$UserName, + + [Parameter( + Mandatory = $true, + Position = 1, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the security group name' + )] + [ValidateNotNullOrEmpty()] + [Alias('Group', 'SecurityGroupName')] + [string]$GroupName, + + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] + [ValidateScript({ + if ([string]::IsNullOrEmpty($_)) { + return $true + } + # Support standard DN format: OU=...,DC=...,DC=... + if ($_ -match '^(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + # Support LDAP format: LDAP://server/DN + if ($_ -match '^LDAP://[^/]+/(OU=.*,)?(DC=.+)(,DC=.+)*$') { + return $true + } + throw "DomainOUPath must be in format 'OU=...,DC=...,DC=...' or 'LDAP://server/OU=...,DC=...,DC=...' or empty" + })] + [Alias('OU', 'Path')] + [string]$DomainOUPath = '', + + [Parameter( + Mandatory = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'Enter the domain name' + )] + [ValidateNotNullOrEmpty()] + [ValidatePattern('^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$')] + [string]$Domain, + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service URL' + )] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + if ($_ -match '^https://') { + $true + } else { + throw "URL must start with https:// (secure connection required)" + } + })] + [Alias('WebServiceUrl', 'Uri')] + [string]$Url, + + [Parameter( + Mandatory = $true, + HelpMessage = 'Enter the CCS Web Service credentials' + )] + [ValidateNotNull()] + [Alias('Credential', 'WebServiceCredential')] + [pscredential]$CCSCredential, + + [Parameter(Mandatory = $false)] + [ValidateNotNull()] + [Alias('ADCredential')] + [pscredential]$DomainCredential, + + [Parameter(Mandatory = $false)] + [Alias('Encrypted')] + [switch]$PasswordIsEncrypted + ) + + begin { + $FunctionName = $PSCmdlet.MyInvocation.MyCommand.Name + + Write-Verbose "[$FunctionName] Starting function execution" + Write-Verbose "[$FunctionName] UserName: $UserName" + Write-Verbose "[$FunctionName] GroupName: $GroupName" + Write-Verbose "[$FunctionName] Domain: $Domain" + Write-Verbose "[$FunctionName] DomainOUPath: $DomainOUPath" + Write-Verbose "[$FunctionName] URL: $Url" + + # Initialize CCS connection once in begin block + try { + Write-Verbose "[$FunctionName] Initializing CCS Web Service connection" + $CCS = Initialize-CCS -Url $Url -WebServiceCredential $CCSCredential -ErrorAction Stop + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to initialize CCS Web Service: $_" ` + -ErrorCategory ConnectionError ` + -TargetObject $Url ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Verify the URL is correct and the CCS Web Service is accessible. Check credentials." + return + } + + # Prepare domain credentials if provided + $ADUsername = $null + $ADPassword = $null + + if ($DomainCredential) { + $ADUsername = $DomainCredential.UserName + Write-Verbose "[$FunctionName] Using domain credential: $ADUsername" + + try { + if ($PasswordIsEncrypted) { + $ADPassword = $DomainCredential.GetNetworkCredential().Password + Write-Verbose "[$FunctionName] Using pre-encrypted password" + } else { + $ADPassword = Get-CCSEncryptedPassword -SecureString $DomainCredential.Password -ErrorAction Stop + Write-Verbose "[$FunctionName] Password encrypted successfully" + } + } catch { + Invoke-CCSErrorHandling ` + -ErrorMessage "Failed to process domain credential password: $_" ` + -ErrorCategory SecurityError ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Ensure the domain credential is valid and the password can be encrypted." + return + } + } else { + Write-Verbose "[$FunctionName] No domain credential provided, using CCS Web Service context" + $ADUsername = $UserName + $ADPassword = '' + } + + # Convert LDAP path to DN if needed + if (-not [string]::IsNullOrEmpty($DomainOUPath) -and $DomainOUPath -match '^LDAP://[^/]+/(.+)$') { + $DomainOUPath = $Matches[1] + Write-Verbose "[$FunctionName] Converted LDAP path to DN: $DomainOUPath" + } + } + + process { + try { + Write-Verbose "[$FunctionName] Checking if user '$UserName' is member of group '$GroupName'" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Processing: UserName=$UserName, GroupName=$GroupName, DomainOUPath=$DomainOUPath, Domain=$Domain") + } + + Write-Verbose "[$FunctionName] Calling ActiveDirectory_isUserMemberOf for $UserName" + + $Result = $CCS.ActiveDirectory_isUserMemberOf( + $DomainOUPath, + $Domain, + $ADUsername, + $ADPassword, + $GroupName + ) + + Write-Debug "Result from CCS Web Service: $Result" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Result for $UserName : $Result") + } + + Write-Verbose "[$FunctionName] Result for $UserName : $Result" + + # Return the boolean result + Write-Output $Result + + } catch { + # Determine if we should throw based on the ErrorAction preference + $ShouldThrow = $false + if ($PSBoundParameters.ContainsKey('ErrorAction')) { + $ShouldThrow = $PSBoundParameters['ErrorAction'] -eq 'Stop' + } else { + $ShouldThrow = $ErrorActionPreference -eq 'Stop' + } + + Invoke-CCSErrorHandling ` + -ErrorMessage "Exception occurred while checking group membership for user '$UserName': $_" ` + -ErrorCategory InvalidOperation ` + -TargetObject $UserName ` + -FunctionName $FunctionName ` + -Exception $_.Exception ` + -RecommendedAction "Check the error details and verify all parameters are correct." ` + -Throw:$ShouldThrow + + # Return false on error + return $false + } + } + + end { + Write-Verbose "[$FunctionName] Function execution completed" + + if ($Global:Cs) { + $Global:Cs.Job_WriteLog("$FunctionName Completed") + } + } +} diff --git a/Modules/Capa.PowerShell.Module.PowerPack.CMS/Dev/Add-PpCMSComputerToCalendarGroup.Tests.ps1 b/Modules/Capa.PowerShell.Module.PowerPack.CMS/Dev/Add-PpCMSComputerToCalendarGroup.Tests.ps1 index 82305d66..8e26c3ec 100644 --- a/Modules/Capa.PowerShell.Module.PowerPack.CMS/Dev/Add-PpCMSComputerToCalendarGroup.Tests.ps1 +++ b/Modules/Capa.PowerShell.Module.PowerPack.CMS/Dev/Add-PpCMSComputerToCalendarGroup.Tests.ps1 @@ -2,17 +2,22 @@ BeforeAll { . $PSCommandPath.Replace('.Tests.ps1', '.ps1') $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Authentication\Dev\Initialize-CapaSDK.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\New-CapaPowerPack.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\Remove-CapaPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Unit\Dev\Add-CapaUnitToPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\Exist-CapaPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\Import-CapaPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Utilities\Dev\Restart-CapaAgent.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Unit\Dev\Get-CapaUnitPackageStatus.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Group\Dev\Remove-CapaGroup.ps1" + $Folders = @( + 'Capa.PowerShell.Module.SDK.Authentication', + 'Capa.PowerShell.Module.SDK.Package', + 'Capa.PowerShell.Module.SDK.Unit', + 'Capa.PowerShell.Module.SDK.Utilities', + 'Capa.PowerShell.Module.SDK.Group' + ) + foreach ($Folder in $Folders) { + $Items = Get-ChildItem -Path "$RootPath\$Folder\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + } - $oCMS = Initialize-CapaSDK -Server $env:COMPUTERNAME -Database 'CapaInstaller' -InstanceManagementPoint 1 + $oCMSDev = Initialize-CapaSDK -Server $env:COMPUTERNAME -Database 'CapaInstaller' -InstanceManagementPoint 1 + $oCMSProd = Initialize-CapaSDK -Server $env:COMPUTERNAME -Database 'CapaInstaller' -InstanceManagementPoint 2 $ScriptContent = Get-Content "$PSScriptRoot\HelpFilesForTests\Default.ps1" -Raw $FunctionCode = Get-Content $($PSCommandPath.Replace('.Tests.ps1', '.ps1')) -Raw @@ -27,7 +32,7 @@ BeforeAll { $ScriptContent = $ScriptContent -replace '#FUNCTION', $FunctionCode -replace '#TESTCODE', $TestCode $SplattingPowerPack = @{ - CapaSDK = $oCMS + CapaSDK = $oCMSDev PackageName = 'Test1' PackageVersion = 'v1.0' DisplayName = 'Test1' @@ -37,15 +42,16 @@ BeforeAll { AllowInstallOnServer = $true } New-CapaPowerPack @SplattingPowerPack - Add-CapaUnitToPackage -CapaSDK $oCMS -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' -UnitName $env:COMPUTERNAME -UnitType 'Computer' + Initialize-CapaPackagePromote -CapaSDK $oCMSDev -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' + Add-CapaUnitToPackage -CapaSDK $oCMSProd -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' -UnitName $env:COMPUTERNAME -UnitType 'Computer' Start-Sleep -Seconds 15 - Restart-CapaAgent -CapaSDK $oCMS -UnitName $env:COMPUTERNAME -UnitType 'Computer' + Restart-CapaAgent -CapaSDK $oCMSProd -UnitName $env:COMPUTERNAME -UnitType 'Computer' Start-Sleep -Seconds 30 $Run = $true while ($Run) { - $Status = Get-CapaUnitPackageStatus -CapaSDK $oCMS -Unitname $env:COMPUTERNAME -UnitType 'Computer' -PackageName 'Test1' -PackageVersion 'v1.0' + $Status = Get-CapaUnitPackageStatus -CapaSDK $oCMSProd -Unitname $env:COMPUTERNAME -UnitType 'Computer' -PackageName 'Test1' -PackageVersion 'v1.0' if ($Status -eq 'Installed' -or $Status -eq 'Failed') { $Run = $false } else { @@ -55,11 +61,11 @@ BeforeAll { } Describe 'Add-PpCMSComputerToCalendarGroup' { It 'The test package should exist' { - $Exist = Exist-CapaPackage -CapaSDK $oCMS -Name 'Test1' -Version 'v1.0' -Type 'Computer' + $Exist = Exist-CapaPackage -CapaSDK $oCMSProd -Name 'Test1' -Version 'v1.0' -Type 'Computer' $Exist | Should -Be $true } It 'Should add the package to the unit' { - $Status = Get-CapaUnitPackageStatus -CapaSDK $oCMS -Unitname $env:COMPUTERNAME -UnitType 'Computer' -PackageName 'Test1' -PackageVersion 'v1.0' + $Status = Get-CapaUnitPackageStatus -CapaSDK $oCMSProd -Unitname $env:COMPUTERNAME -UnitType 'Computer' -PackageName 'Test1' -PackageVersion 'v1.0' $Status | Should -Be 'Installed' } It 'The log should contain the right text' { @@ -72,11 +78,12 @@ Describe 'Add-PpCMSComputerToCalendarGroup' { } } AfterAll { - Remove-CapaPackage -CapaSDK $oCMS -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' -Force $true - Remove-CapaGroup -CapaSDK $oCMS -GroupName 'CalenderGroup' -GroupType 'Calendar' -UnitType 'Computer' + Remove-CapaPackage -CapaSDK $oCMSProd -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' -Force $true + Remove-CapaPackage -CapaSDK $oCMSDev -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' -Force $true + Remove-CapaGroup -CapaSDK $oCMSProd -GroupName 'CalenderGroup' -GroupType 'Calendar' -UnitType 'Computer' Get-Module | Where-Object { $_.Name -like '*-Capa*' -or $_.Name -like '*-Pp*' } | Remove-Module # Start sleep to make sure the Package is deleted - Start-Sleep -Seconds 5 + Start-Sleep -Seconds 15 } \ No newline at end of file diff --git a/Modules/Capa.PowerShell.Module.PowerPack.CMS/Dev/Add-PpCMSComputerToDepartmentGroup.Tests.ps1 b/Modules/Capa.PowerShell.Module.PowerPack.CMS/Dev/Add-PpCMSComputerToDepartmentGroup.Tests.ps1 index eb15a670..9d7949e0 100644 --- a/Modules/Capa.PowerShell.Module.PowerPack.CMS/Dev/Add-PpCMSComputerToDepartmentGroup.Tests.ps1 +++ b/Modules/Capa.PowerShell.Module.PowerPack.CMS/Dev/Add-PpCMSComputerToDepartmentGroup.Tests.ps1 @@ -2,17 +2,22 @@ BeforeAll { . $PSCommandPath.Replace('.Tests.ps1', '.ps1') $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Authentication\Dev\Initialize-CapaSDK.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\New-CapaPowerPack.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\Remove-CapaPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Unit\Dev\Add-CapaUnitToPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\Exist-CapaPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\Import-CapaPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Utilities\Dev\Restart-CapaAgent.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Unit\Dev\Get-CapaUnitPackageStatus.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Group\Dev\Remove-CapaGroup.ps1" + $Folders = @( + 'Capa.PowerShell.Module.SDK.Authentication', + 'Capa.PowerShell.Module.SDK.Package', + 'Capa.PowerShell.Module.SDK.Unit', + 'Capa.PowerShell.Module.SDK.Utilities', + 'Capa.PowerShell.Module.SDK.Group' + ) + foreach ($Folder in $Folders) { + $Items = Get-ChildItem -Path "$RootPath\$Folder\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + } - $oCMS = Initialize-CapaSDK -Server $env:COMPUTERNAME -Database 'CapaInstaller' -InstanceManagementPoint 1 + $oCMSDev = Initialize-CapaSDK -Server $env:COMPUTERNAME -Database 'CapaInstaller' -InstanceManagementPoint 1 + $oCMSProd = Initialize-CapaSDK -Server $env:COMPUTERNAME -Database 'CapaInstaller' -InstanceManagementPoint 2 $ScriptContent = Get-Content "$PSScriptRoot\HelpFilesForTests\Default.ps1" -Raw $FunctionCode = Get-Content $($PSCommandPath.Replace('.Tests.ps1', '.ps1')) -Raw @@ -27,7 +32,7 @@ BeforeAll { $ScriptContent = $ScriptContent -replace '#FUNCTION', $FunctionCode -replace '#TESTCODE', $TestCode $SplattingPowerPack = @{ - CapaSDK = $oCMS + CapaSDK = $oCMSDev PackageName = 'Test1' PackageVersion = 'v1.0' DisplayName = 'Test1' @@ -37,15 +42,15 @@ BeforeAll { AllowInstallOnServer = $true } New-CapaPowerPack @SplattingPowerPack - Add-CapaUnitToPackage -CapaSDK $oCMS -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' -UnitName $env:COMPUTERNAME -UnitType 'Computer' + Initialize-CapaPackagePromote -CapaSDK $oCMSDev -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' + Add-CapaUnitToPackage -CapaSDK $oCMSProd -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' -UnitName $env:COMPUTERNAME -UnitType 'Computer' Start-Sleep -Seconds 15 - Restart-CapaAgent -CapaSDK $oCMS -UnitName $env:COMPUTERNAME -UnitType 'Computer' - + Restart-CapaAgent -CapaSDK $oCMSProd -UnitName $env:COMPUTERNAME -UnitType 'Computer' Start-Sleep -Seconds 30 $Run = $true while ($Run) { - $Status = Get-CapaUnitPackageStatus -CapaSDK $oCMS -UnitName $env:COMPUTERNAME -UnitType 'Computer' -PackageName 'Test1' -PackageVersion 'v1.0' + $Status = Get-CapaUnitPackageStatus -CapaSDK $oCMSProd -UnitName $env:COMPUTERNAME -UnitType 'Computer' -PackageName 'Test1' -PackageVersion 'v1.0' if ($Status -eq 'Installed' -or $Status -eq 'Failed') { $Run = $false } else { @@ -55,11 +60,11 @@ BeforeAll { } Describe 'Add-PpCMSComputerToDepartmentGroup' { It 'The test package should exist' { - $Exist = Exist-CapaPackage -CapaSDK $oCMS -Name 'Test1' -Version 'v1.0' -Type 'Computer' + $Exist = Exist-CapaPackage -CapaSDK $oCMSProd -Name 'Test1' -Version 'v1.0' -Type 'Computer' $Exist | Should -Be $true } It 'Should add the package to the unit' { - $Status = Get-CapaUnitPackageStatus -CapaSDK $oCMS -UnitName $env:COMPUTERNAME -UnitType 'Computer' -PackageName 'Test1' -PackageVersion 'v1.0' + $Status = Get-CapaUnitPackageStatus -CapaSDK $oCMSProd -UnitName $env:COMPUTERNAME -UnitType 'Computer' -PackageName 'Test1' -PackageVersion 'v1.0' $Status | Should -Be 'Installed' } It 'The log should contain the right text' { @@ -72,11 +77,12 @@ Describe 'Add-PpCMSComputerToDepartmentGroup' { } } AfterAll { - Remove-CapaPackage -CapaSDK $oCMS -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' -Force $true - Remove-CapaGroup -CapaSDK $oCMS -GroupName 'DepartmentGroup' -GroupType 'Department' -UnitType 'Computer' + Remove-CapaPackage -CapaSDK $oCMSProd -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' -Force $true + Remove-CapaPackage -CapaSDK $oCMSDev -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' -Force $true + Remove-CapaGroup -CapaSDK $oCMSProd -GroupName 'DepartmentGroup' -GroupType 'Department' -UnitType 'Computer' Get-Module | Where-Object { $_.Name -like '*-Capa*' -or $_.Name -like '*-Pp*' } | Remove-Module # Start sleep to make sure the Package is deleted - Start-Sleep -Seconds 5 + Start-Sleep -Seconds 15 } \ No newline at end of file diff --git a/Modules/Capa.PowerShell.Module.PowerPack.CMS/Dev/Add-PpCMSComputerToPowerSchemeGroup.Tests.ps1 b/Modules/Capa.PowerShell.Module.PowerPack.CMS/Dev/Add-PpCMSComputerToPowerSchemeGroup.Tests.ps1 index 991aff42..bb1ef304 100644 --- a/Modules/Capa.PowerShell.Module.PowerPack.CMS/Dev/Add-PpCMSComputerToPowerSchemeGroup.Tests.ps1 +++ b/Modules/Capa.PowerShell.Module.PowerPack.CMS/Dev/Add-PpCMSComputerToPowerSchemeGroup.Tests.ps1 @@ -3,16 +3,22 @@ BeforeAll { . $PSCommandPath.Replace('.Tests.ps1', '.ps1') $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Authentication\Dev\Initialize-CapaSDK.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\New-CapaPowerPack.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\Remove-CapaPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Unit\Dev\Add-CapaUnitToPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\Exist-CapaPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\Import-CapaPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Utilities\Dev\Restart-CapaAgent.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Unit\Dev\Get-CapaUnitPackageStatus.ps1" + $Folders = @( + 'Capa.PowerShell.Module.SDK.Authentication', + 'Capa.PowerShell.Module.SDK.Package', + 'Capa.PowerShell.Module.SDK.Unit', + 'Capa.PowerShell.Module.SDK.Utilities', + 'Capa.PowerShell.Module.SDK.Group' + ) + foreach ($Folder in $Folders) { + $Items = Get-ChildItem -Path "$RootPath\$Folder\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + } - $oCMS = Initialize-CapaSDK -Server $env:COMPUTERNAME -Database 'CapaInstaller' -InstanceManagementPoint 1 + $oCMSDev = Initialize-CapaSDK -Server $env:COMPUTERNAME -Database 'CapaInstaller' -InstanceManagementPoint 1 + $oCMSProd = Initialize-CapaSDK -Server $env:COMPUTERNAME -Database 'CapaInstaller' -InstanceManagementPoint 2 $ScriptContent = Get-Content "$PSScriptRoot\HelpFilesForTests\Default.ps1" -Raw $FunctionCode = Get-Content $($PSCommandPath.Replace('.Tests.ps1', '.ps1')) -Raw @@ -35,7 +41,7 @@ BeforeAll { $ScriptContent = $ScriptContent -replace '#FUNCTION', $FunctionCode -replace '#TESTCODE', $TestCode $SplattingPowerPack = @{ - CapaSDK = $oCMS + CapaSDK = $oCMSDev PackageName = 'Test1' PackageVersion = 'v1.0' DisplayName = 'Test1' @@ -45,15 +51,15 @@ BeforeAll { AllowInstallOnServer = $true } New-CapaPowerPack @SplattingPowerPack - Add-CapaUnitToPackage -CapaSDK $oCMS -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' -UnitName $env:COMPUTERNAME -UnitType 'Computer' + Initialize-CapaPackagePromote -CapaSDK $oCMSDev -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' + Add-CapaUnitToPackage -CapaSDK $oCMSProd -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' -UnitName $env:COMPUTERNAME -UnitType 'Computer' Start-Sleep -Seconds 15 - Restart-CapaAgent -CapaSDK $oCMS -UnitName $env:COMPUTERNAME -UnitType 'Computer' - + Restart-CapaAgent -CapaSDK $oCMSProd -UnitName $env:COMPUTERNAME -UnitType 'Computer' Start-Sleep -Seconds 30 $Run = $true while ($Run) { - $Status = Get-CapaUnitPackageStatus -CapaSDK $oCMS -UnitName $env:COMPUTERNAME -UnitType 'Computer' -PackageName 'Test1' -PackageVersion 'v1.0' + $Status = Get-CapaUnitPackageStatus -CapaSDK $oCMSProd -UnitName $env:COMPUTERNAME -UnitType 'Computer' -PackageName 'Test1' -PackageVersion 'v1.0' if ($Status -eq 'Installed' -or $Status -eq 'Failed') { $Run = $false } else { @@ -63,11 +69,11 @@ BeforeAll { } Describe 'Add-PpCMSComputerToPowerSchemeGroup' { It 'The test package should exist' { - $Exist = Exist-CapaPackage -CapaSDK $oCMS -Name 'Test1' -Version 'v1.0' -Type 'Computer' + $Exist = Exist-CapaPackage -CapaSDK $oCMSProd -Name 'Test1' -Version 'v1.0' -Type 'Computer' $Exist | Should -Be $true } It 'Should add the package to the unit' { - $Status = Get-CapaUnitPackageStatus -CapaSDK $oCMS -UnitName $env:COMPUTERNAME -UnitType 'Computer' -PackageName 'Test1' -PackageVersion 'v1.0' + $Status = Get-CapaUnitPackageStatus -CapaSDK $oCMSProd -UnitName $env:COMPUTERNAME -UnitType 'Computer' -PackageName 'Test1' -PackageVersion 'v1.0' $Status | Should -Be 'Installed' } It 'The log should contain the correct message' { @@ -80,10 +86,11 @@ Describe 'Add-PpCMSComputerToPowerSchemeGroup' { } } AfterAll { - Remove-CapaPackage -CapaSDK $oCMS -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' -Force $true + Remove-CapaPackage -CapaSDK $oCMSProd -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' -Force $true + Remove-CapaPackage -CapaSDK $oCMSDev -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' -Force $true Get-Module | Where-Object { $_.Name -like '*-Capa*' -or $_.Name -like '*-Pp*' } | Remove-Module # Start sleep to make sure the Package is deleted - Start-Sleep -Seconds 5 + Start-Sleep -Seconds 15 } \ No newline at end of file diff --git a/Modules/Capa.PowerShell.Module.PowerPack.CMS/Dev/Add-PpCMSComputerToStaticGroup.Tests.ps1 b/Modules/Capa.PowerShell.Module.PowerPack.CMS/Dev/Add-PpCMSComputerToStaticGroup.Tests.ps1 index 921bded9..2c58838c 100644 --- a/Modules/Capa.PowerShell.Module.PowerPack.CMS/Dev/Add-PpCMSComputerToStaticGroup.Tests.ps1 +++ b/Modules/Capa.PowerShell.Module.PowerPack.CMS/Dev/Add-PpCMSComputerToStaticGroup.Tests.ps1 @@ -2,17 +2,22 @@ BeforeAll { . $PSCommandPath.Replace('.Tests.ps1', '.ps1') $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Authentication\Dev\Initialize-CapaSDK.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\New-CapaPowerPack.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\Remove-CapaPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Unit\Dev\Add-CapaUnitToPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\Exist-CapaPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\Import-CapaPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Utilities\Dev\Restart-CapaAgent.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Unit\Dev\Get-CapaUnitPackageStatus.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Group\Dev\Remove-CapaGroup.ps1" + $Folders = @( + 'Capa.PowerShell.Module.SDK.Authentication', + 'Capa.PowerShell.Module.SDK.Package', + 'Capa.PowerShell.Module.SDK.Unit', + 'Capa.PowerShell.Module.SDK.Utilities', + 'Capa.PowerShell.Module.SDK.Group' + ) + foreach ($Folder in $Folders) { + $Items = Get-ChildItem -Path "$RootPath\$Folder\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + } - $oCMS = Initialize-CapaSDK -Server $env:COMPUTERNAME -Database 'CapaInstaller' -InstanceManagementPoint 1 + $oCMSDev = Initialize-CapaSDK -Server $env:COMPUTERNAME -Database 'CapaInstaller' -InstanceManagementPoint 1 + $oCMSProd = Initialize-CapaSDK -Server $env:COMPUTERNAME -Database 'CapaInstaller' -InstanceManagementPoint 2 $ScriptContent = Get-Content "$PSScriptRoot\HelpFilesForTests\Default.ps1" -Raw $FunctionCode = Get-Content $($PSCommandPath.Replace('.Tests.ps1', '.ps1')) -Raw @@ -27,7 +32,7 @@ BeforeAll { $ScriptContent = $ScriptContent -replace '#FUNCTION', $FunctionCode -replace '#TESTCODE', $TestCode $SplattingPowerPack = @{ - CapaSDK = $oCMS + CapaSDK = $oCMSDev PackageName = 'Test1' PackageVersion = 'v1.0' DisplayName = 'Test1' @@ -37,15 +42,16 @@ BeforeAll { AllowInstallOnServer = $true } New-CapaPowerPack @SplattingPowerPack - Add-CapaUnitToPackage -CapaSDK $oCMS -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' -UnitName $env:COMPUTERNAME -UnitType 'Computer' + Initialize-CapaPackagePromote -CapaSDK $oCMSDev -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' + Add-CapaUnitToPackage -CapaSDK $oCMSProd -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' -UnitName $env:COMPUTERNAME -UnitType 'Computer' Start-Sleep -Seconds 15 - Restart-CapaAgent -CapaSDK $oCMS -UnitName $env:COMPUTERNAME -UnitType 'Computer' + Restart-CapaAgent -CapaSDK $oCMSProd -UnitName $env:COMPUTERNAME -UnitType 'Computer' Start-Sleep -Seconds 30 $Run = $true while ($Run) { - $Status = Get-CapaUnitPackageStatus -CapaSDK $oCMS -UnitName $env:COMPUTERNAME -UnitType 'Computer' -PackageName 'Test1' -PackageVersion 'v1.0' + $Status = Get-CapaUnitPackageStatus -CapaSDK $oCMSProd -UnitName $env:COMPUTERNAME -UnitType 'Computer' -PackageName 'Test1' -PackageVersion 'v1.0' if ($Status -eq 'Installed' -or $Status -eq 'Failed') { $Run = $false } else { @@ -55,11 +61,11 @@ BeforeAll { } Describe 'Add-PpCMSComputerToDepartmentGroup' { It 'The test package should exist' { - $Exist = Exist-CapaPackage -CapaSDK $oCMS -Name 'Test1' -Version 'v1.0' -Type 'Computer' + $Exist = Exist-CapaPackage -CapaSDK $oCMSProd -Name 'Test1' -Version 'v1.0' -Type 'Computer' $Exist | Should -Be $true } It 'Should add the package to the unit' { - $Status = Get-CapaUnitPackageStatus -CapaSDK $oCMS -UnitName $env:COMPUTERNAME -UnitType 'Computer' -PackageName 'Test1' -PackageVersion 'v1.0' + $Status = Get-CapaUnitPackageStatus -CapaSDK $oCMSProd -UnitName $env:COMPUTERNAME -UnitType 'Computer' -PackageName 'Test1' -PackageVersion 'v1.0' $Status | Should -Be 'Installed' } It 'The log should contain the right text' { @@ -71,11 +77,12 @@ Describe 'Add-PpCMSComputerToDepartmentGroup' { } } AfterAll { - Remove-CapaPackage -CapaSDK $oCMS -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' -Force $true - Remove-CapaGroup -CapaSDK $oCMS -GroupName 'Test' -GroupType 'Static' -UnitType 'Computer' + Remove-CapaPackage -CapaSDK $oCMSProd -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' -Force $true + Remove-CapaPackage -CapaSDK $oCMSDev -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' -Force $true + Remove-CapaGroup -CapaSDK $oCMSProd -GroupName 'Test' -GroupType 'Static' -UnitType 'Computer' Get-Module | Where-Object { $_.Name -like '*-Capa*' -or $_.Name -like '*-Pp*' } | Remove-Module # Start sleep to make sure the Package is deleted - Start-Sleep -Seconds 5 + Start-Sleep -Seconds 15 } \ No newline at end of file diff --git a/Modules/Capa.PowerShell.Module.PowerPack.CMS/Dev/Add-PpCMSCustomInventory.Tests.ps1 b/Modules/Capa.PowerShell.Module.PowerPack.CMS/Dev/Add-PpCMSCustomInventory.Tests.ps1 index e448b7f8..529e130c 100644 --- a/Modules/Capa.PowerShell.Module.PowerPack.CMS/Dev/Add-PpCMSCustomInventory.Tests.ps1 +++ b/Modules/Capa.PowerShell.Module.PowerPack.CMS/Dev/Add-PpCMSCustomInventory.Tests.ps1 @@ -2,18 +2,23 @@ BeforeAll { . $PSCommandPath.Replace('.Tests.ps1', '.ps1') $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Authentication\Dev\Initialize-CapaSDK.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\New-CapaPowerPack.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\Remove-CapaPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Unit\Dev\Add-CapaUnitToPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\Exist-CapaPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\Import-CapaPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Utilities\Dev\Restart-CapaAgent.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Unit\Dev\Get-CapaUnitPackageStatus.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Inventory\Dev\Get-CapaCustomInventoryForUnit.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Inventory\Dev\Convert-CapaDataType.ps1" + $Folders = @( + 'Capa.PowerShell.Module.SDK.Authentication', + 'Capa.PowerShell.Module.SDK.Package', + 'Capa.PowerShell.Module.SDK.Unit', + 'Capa.PowerShell.Module.SDK.Utilities', + 'Capa.PowerShell.Module.SDK.Group', + 'Capa.PowerShell.Module.SDK.Inventory' + ) + foreach ($Folder in $Folders) { + $Items = Get-ChildItem -Path "$RootPath\$Folder\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + } - $oCMS = Initialize-CapaSDK -Server $env:COMPUTERNAME -Database 'CapaInstaller' -InstanceManagementPoint 1 + $oCMSDev = Initialize-CapaSDK -Server $env:COMPUTERNAME -Database 'CapaInstaller' -InstanceManagementPoint 1 + $oCMSProd = Initialize-CapaSDK -Server $env:COMPUTERNAME -Database 'CapaInstaller' -InstanceManagementPoint 2 $ScriptContent = Get-Content "$PSScriptRoot\HelpFilesForTests\Default.ps1" -Raw $FunctionCode = Get-Content $($PSCommandPath.Replace('.Tests.ps1', '.ps1')) -Raw @@ -28,7 +33,7 @@ BeforeAll { $ScriptContent = $ScriptContent -replace '#FUNCTION', $FunctionCode -replace '#TESTCODE', $TestCode $SplattingPowerPack = @{ - CapaSDK = $oCMS + CapaSDK = $oCMSDev PackageName = 'Test1' PackageVersion = 'v1.0' DisplayName = 'Test1' @@ -38,15 +43,16 @@ BeforeAll { AllowInstallOnServer = $true } New-CapaPowerPack @SplattingPowerPack - Add-CapaUnitToPackage -CapaSDK $oCMS -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' -UnitName $env:COMPUTERNAME -UnitType 'Computer' + Initialize-CapaPackagePromote -CapaSDK $oCMSDev -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' + Add-CapaUnitToPackage -CapaSDK $oCMSProd -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' -UnitName $env:COMPUTERNAME -UnitType 'Computer' Start-Sleep -Seconds 15 - Restart-CapaAgent -CapaSDK $oCMS -UnitName $env:COMPUTERNAME -UnitType 'Computer' + Restart-CapaAgent -CapaSDK $oCMSProd -UnitName $env:COMPUTERNAME -UnitType 'Computer' Start-Sleep -Seconds 30 $Run = $true while ($Run) { - $Status = Get-CapaUnitPackageStatus -CapaSDK $oCMS -UnitName $env:COMPUTERNAME -UnitType 'Computer' -PackageName 'Test1' -PackageVersion 'v1.0' + $Status = Get-CapaUnitPackageStatus -CapaSDK $oCMSProd -UnitName $env:COMPUTERNAME -UnitType 'Computer' -PackageName 'Test1' -PackageVersion 'v1.0' if ($Status -eq 'Installed' -or $Status -eq 'Failed') { $Run = $false } else { @@ -56,11 +62,11 @@ BeforeAll { } Describe 'Add-PpCMSCustomInventory' { It 'The test package should exist' { - $Exist = Exist-CapaPackage -CapaSDK $oCMS -Name 'Test1' -Version 'v1.0' -Type 'Computer' + $Exist = Exist-CapaPackage -CapaSDK $oCMSProd -Name 'Test1' -Version 'v1.0' -Type 'Computer' $Exist | Should -Be $true } It 'Should add the package to the unit' { - $Status = Get-CapaUnitPackageStatus -CapaSDK $oCMS -UnitName $env:COMPUTERNAME -UnitType 'Computer' -PackageName 'Test1' -PackageVersion 'v1.0' + $Status = Get-CapaUnitPackageStatus -CapaSDK $oCMSProd -UnitName $env:COMPUTERNAME -UnitType 'Computer' -PackageName 'Test1' -PackageVersion 'v1.0' $Status | Should -Be 'Installed' } It 'The log should contain the right text' { @@ -71,16 +77,17 @@ Describe 'Add-PpCMSCustomInventory' { $LogContent | Should -Match 'Add-PpCMSCustomInventory: Custom inventory added successfully' } It 'The custom inventory should contain the right text' { - $CustomInventory = Get-CapaCustomInventoryForUnit -CapaSDK $oCMS -UnitName $env:COMPUTERNAME -UnitType 'Computer' | Where-Object { $_.Category -eq 'MyCategory' -and $_.Entry -eq 'MyEntry' } + $CustomInventory = Get-CapaCustomInventoryForUnit -CapaSDK $oCMSProd -UnitName $env:COMPUTERNAME -UnitType 'Computer' | Where-Object { $_.Category -eq 'MyCategory' -and $_.Entry -eq 'MyEntry' } $CustomInventory | Should -Not -BeNullOrEmpty $CustomInventory.Value | Should -Be 'MyValue' } } AfterAll { - Remove-CapaPackage -CapaSDK $oCMS -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' -Force $true + Remove-CapaPackage -CapaSDK $oCMSProd -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' -Force $true + Remove-CapaPackage -CapaSDK $oCMSDev -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' -Force $true Get-Module | Where-Object { $_.Name -like '*-Capa*' -or $_.Name -like '*-Pp*' } | Remove-Module # Start sleep to make sure the Package is deleted - Start-Sleep -Seconds 5 + Start-Sleep -Seconds 15 } \ No newline at end of file diff --git a/Modules/Capa.PowerShell.Module.PowerPack.CMS/Dev/Add-PpCMSHardwareInventory.Tests.ps1 b/Modules/Capa.PowerShell.Module.PowerPack.CMS/Dev/Add-PpCMSHardwareInventory.Tests.ps1 index 09ed45e8..1cfdcfae 100644 --- a/Modules/Capa.PowerShell.Module.PowerPack.CMS/Dev/Add-PpCMSHardwareInventory.Tests.ps1 +++ b/Modules/Capa.PowerShell.Module.PowerPack.CMS/Dev/Add-PpCMSHardwareInventory.Tests.ps1 @@ -2,18 +2,23 @@ BeforeAll { . $PSCommandPath.Replace('.Tests.ps1', '.ps1') $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Authentication\Dev\Initialize-CapaSDK.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\New-CapaPowerPack.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\Remove-CapaPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Unit\Dev\Add-CapaUnitToPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\Exist-CapaPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\Import-CapaPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Utilities\Dev\Restart-CapaAgent.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Unit\Dev\Get-CapaUnitPackageStatus.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Inventory\Dev\Get-CapaHardwareInventoryForUnit.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Inventory\Dev\Convert-CapaDataType.ps1" + $Folders = @( + 'Capa.PowerShell.Module.SDK.Authentication', + 'Capa.PowerShell.Module.SDK.Package', + 'Capa.PowerShell.Module.SDK.Unit', + 'Capa.PowerShell.Module.SDK.Utilities', + 'Capa.PowerShell.Module.SDK.Group', + 'Capa.PowerShell.Module.SDK.Inventory' + ) + foreach ($Folder in $Folders) { + $Items = Get-ChildItem -Path "$RootPath\$Folder\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + } - $oCMS = Initialize-CapaSDK -Server $env:COMPUTERNAME -Database 'CapaInstaller' -InstanceManagementPoint 1 + $oCMSDev = Initialize-CapaSDK -Server $env:COMPUTERNAME -Database 'CapaInstaller' -InstanceManagementPoint 1 + $oCMSProd = Initialize-CapaSDK -Server $env:COMPUTERNAME -Database 'CapaInstaller' -InstanceManagementPoint 2 $ScriptContent = Get-Content "$PSScriptRoot\HelpFilesForTests\Default.ps1" -Raw $FunctionCode = Get-Content $($PSCommandPath.Replace('.Tests.ps1', '.ps1')) -Raw @@ -28,7 +33,7 @@ BeforeAll { $ScriptContent = $ScriptContent -replace '#FUNCTION', $FunctionCode -replace '#TESTCODE', $TestCode $SplattingPowerPack = @{ - CapaSDK = $oCMS + CapaSDK = $oCMSDev PackageName = 'Test1' PackageVersion = 'v1.0' DisplayName = 'Test1' @@ -38,15 +43,16 @@ BeforeAll { AllowInstallOnServer = $true } New-CapaPowerPack @SplattingPowerPack - Add-CapaUnitToPackage -CapaSDK $oCMS -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' -UnitName $env:COMPUTERNAME -UnitType 'Computer' + Initialize-CapaPackagePromote -CapaSDK $oCMSDev -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' + Add-CapaUnitToPackage -CapaSDK $oCMSProd -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' -UnitName $env:COMPUTERNAME -UnitType 'Computer' Start-Sleep -Seconds 15 - Restart-CapaAgent -CapaSDK $oCMS -UnitName $env:COMPUTERNAME -UnitType 'Computer' + Restart-CapaAgent -CapaSDK $oCMSProd -UnitName $env:COMPUTERNAME -UnitType 'Computer' Start-Sleep -Seconds 30 $Run = $true while ($Run) { - $Status = Get-CapaUnitPackageStatus -CapaSDK $oCMS -UnitName $env:COMPUTERNAME -UnitType 'Computer' -PackageName 'Test1' -PackageVersion 'v1.0' + $Status = Get-CapaUnitPackageStatus -CapaSDK $oCMSProd -UnitName $env:COMPUTERNAME -UnitType 'Computer' -PackageName 'Test1' -PackageVersion 'v1.0' if ($Status -eq 'Installed' -or $Status -eq 'Failed') { $Run = $false } else { @@ -56,11 +62,11 @@ BeforeAll { } Describe 'Add-PpCMSComputerToDepartmentGroup' { It 'The test package should exist' { - $Exist = Exist-CapaPackage -CapaSDK $oCMS -Name 'Test1' -Version 'v1.0' -Type 'Computer' + $Exist = Exist-CapaPackage -CapaSDK $oCMSProd -Name 'Test1' -Version 'v1.0' -Type 'Computer' $Exist | Should -Be $true } It 'Should add the package to the unit' { - $Status = Get-CapaUnitPackageStatus -CapaSDK $oCMS -UnitName $env:COMPUTERNAME -UnitType 'Computer' -PackageName 'Test1' -PackageVersion 'v1.0' + $Status = Get-CapaUnitPackageStatus -CapaSDK $oCMSProd -UnitName $env:COMPUTERNAME -UnitType 'Computer' -PackageName 'Test1' -PackageVersion 'v1.0' $Status | Should -Be 'Installed' } It 'The log should contain the right text' { @@ -71,16 +77,17 @@ Describe 'Add-PpCMSComputerToDepartmentGroup' { $LogContent | Should -Match 'Add-PpCMSHardwareInventory: Hardware inventory added successfully' } It 'Check the hardware inventory' { - $HardwareInventory = Get-CapaHardwareInventoryForUnit -CapaSDK $oCMS -UnitName $env:COMPUTERNAME -UnitType 'Computer' | Where-Object { $_.Category -eq 'MyCategory' -and $_.Entry -eq 'MyEntry' } + $HardwareInventory = Get-CapaHardwareInventoryForUnit -CapaSDK $oCMSProd -UnitName $env:COMPUTERNAME -UnitType 'Computer' | Where-Object { $_.Category -eq 'MyCategory' -and $_.Entry -eq 'MyEntry' } $HardwareInventory | Should -Not -BeNullOrEmpty $HardwareInventory.Value | Should -Be 'MyValue' } } AfterAll { - Remove-CapaPackage -CapaSDK $oCMS -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' -Force $true + Remove-CapaPackage -CapaSDK $oCMSProd -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' -Force $true + Remove-CapaPackage -CapaSDK $oCMSDev -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' -Force $true Get-Module | Where-Object { $_.Name -like '*-Capa*' -or $_.Name -like '*-Pp*' } | Remove-Module # Start sleep to make sure the Package is deleted - Start-Sleep -Seconds 5 + Start-Sleep -Seconds 15 } \ No newline at end of file diff --git a/Modules/Capa.PowerShell.Module.PowerPack/Dev/Initialize-PpInputObject.Tests.ps1 b/Modules/Capa.PowerShell.Module.PowerPack/Dev/Initialize-PpInputObject.Tests.ps1 index 983c9e3c..6b59e178 100644 --- a/Modules/Capa.PowerShell.Module.PowerPack/Dev/Initialize-PpInputObject.Tests.ps1 +++ b/Modules/Capa.PowerShell.Module.PowerPack/Dev/Initialize-PpInputObject.Tests.ps1 @@ -1,7 +1 @@ # TODO: #422 Create Tests for Initialize-PpInputObject - -$Global:InputObject = $null -Initialize-PpInputObject -CMS_AddPackageToUnit -package 'findes ikke' -version 'v1.0' -CMS_AddPackageToUnit -package 'Test' -version 'v1.0' -$Global:InputObject.ShowMessageBox('MessageBox title', 'This is a test messageBox from a PowerPack (æøå). Please click one of the three buttons below', 'YESNOCANCEL', 'TWO', 'INFORMATION', 180, $false) diff --git a/Modules/Capa.PowerShell.Module.PowerPack/Dev/Initialize-PpVariables.Tests.ps1 b/Modules/Capa.PowerShell.Module.PowerPack/Dev/Initialize-PpVariables.Tests.ps1 index 4c2d3417..67dd5f87 100644 --- a/Modules/Capa.PowerShell.Module.PowerPack/Dev/Initialize-PpVariables.Tests.ps1 +++ b/Modules/Capa.PowerShell.Module.PowerPack/Dev/Initialize-PpVariables.Tests.ps1 @@ -514,8 +514,8 @@ Describe '$global:gsDisplayVersion' { } } Describe '$global:gsWindowsType' { - It 'Should be "LanmanNT"' { - $global:gsWindowsType | Should -Be 'LanmanNT' + It 'Should be "ServerNT"' { + $global:gsWindowsType | Should -Be 'ServerNT' } It 'Should be a string' { $global:gsWindowsType | Should -BeOfType [string] diff --git a/Modules/Capa.PowerShell.Module.SDK.Package/Dev/Copy-CapaPackageRelation.Tests.ps1 b/Modules/Capa.PowerShell.Module.SDK.Package/Dev/Copy-CapaPackageRelation.Tests.ps1 index 46ea38ea..2703ac65 100644 --- a/Modules/Capa.PowerShell.Module.SDK.Package/Dev/Copy-CapaPackageRelation.Tests.ps1 +++ b/Modules/Capa.PowerShell.Module.SDK.Package/Dev/Copy-CapaPackageRelation.Tests.ps1 @@ -1,10 +1,26 @@ BeforeAll { . $PSCommandPath.Replace('.Tests.ps1', '.ps1') + $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent - $oCMS = Initialize-CapaSDK -Server $env:COMPUTERNAME -Database 'CapaInstaller' -InstanceManagementPoint '1' + $Folders = @( + 'Capa.PowerShell.Module.SDK.Authentication', + 'Capa.PowerShell.Module.SDK.Package', + 'Capa.PowerShell.Module.SDK.Unit', + 'Capa.PowerShell.Module.SDK.Group', + 'Capa.PowerShell.Module.SDK.Container' + ) + foreach ($Folder in $Folders) { + $Items = Get-ChildItem -Path "$RootPath\$Folder\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + } + + $oCMSDev = Initialize-CapaSDK -Server $env:COMPUTERNAME -Database 'CapaInstaller' -InstanceManagementPoint '1' + $oCMSProd = Initialize-CapaSDK -Server $env:COMPUTERNAME -Database 'CapaInstaller' -InstanceManagementPoint '2' $PckFrom = @{ - CapaSDK = $oCMS + CapaSDK = $oCMSDev PackageName = 'TestFrom' PackageVersion = 'v1.0' DisplayName = 'TestFrom v1.0' @@ -12,7 +28,7 @@ BeforeAll { Database = 'CapaInstaller' } $PckTo = @{ - CapaSDK = $oCMS + CapaSDK = $oCMSDev PackageName = 'TestTo' PackageVersion = 'v1.0' DisplayName = 'TestTo v1.0' @@ -22,25 +38,28 @@ BeforeAll { New-CapaPowerPack @PckFrom New-CapaPowerPack @PckTo - Create-CapaGroup -CapaSDK $oCMS -GroupName 'TestGroup' -UnitType Computer -GroupType 'Static' + Initialize-CapaPackagePromote -CapaSDK $oCMSDev -PackageName 'TestFrom' -PackageVersion 'v1.0' -PackageType 'Computer' + Initialize-CapaPackagePromote -CapaSDK $oCMSDev -PackageName 'TestTo' -PackageVersion 'v1.0' -PackageType 'Computer' + + Create-CapaGroup -CapaSDK $oCMSProd -GroupName 'TestGroup' -UnitType Computer -GroupType 'Static' Start-Sleep -Seconds 1 - $Unit = (Get-CapaUnits -CapaSDK $oCms -Type Computer | Where-Object { $_.Name -eq $env:COMPUTERNAME })[0] + $Unit = (Get-CapaUnits -CapaSDK $oCMSProd -Type Computer | Where-Object { $_.Name -eq $env:COMPUTERNAME })[0] - Add-CapaUnitToGroup -CapaSDK $oCMS -UnitName $Unit.UUID -UnitType Computer -GroupName 'TestGroup' -GroupType 'Static' - Add-CapaPackageToGroup -CapaSDK $oCMS -PackageName 'TestFrom' -PackageVersion 'v1.0' -PackageType Computer -GroupName 'TestGroup' -GroupType 'Static' + Add-CapaUnitToGroup -CapaSDK $oCMSProd -UnitName $Unit.UUID -UnitType Computer -GroupName 'TestGroup' -GroupType 'Static' + Add-CapaPackageToGroup -CapaSDK $oCMSProd -PackageName 'TestFrom' -PackageVersion 'v1.0' -PackageType Computer -GroupName 'TestGroup' -GroupType 'Static' Start-Sleep -Seconds 1 } Describe 'Copy groups' { It 'Does the group exist in the destination package' { - $Group = Get-CapaGroups -CapaSDK $oCMS -GroupType 'Static' | Where-Object { $_.Name -eq 'TestGroup' } + $Group = Get-CapaGroups -CapaSDK $oCMSProd -GroupType 'Static' | Where-Object { $_.Name -eq 'TestGroup' } $Group | Should -Not -BeNullOrEmpty } It 'Does the command work' { $Splat = @{ - CapaSDK = $oCMS + CapaSDK = $oCMSProd FromPackageName = 'TestFrom' FromPackageVersion = 'v1.0' FromPackageType = 'Computer' @@ -54,18 +73,18 @@ Describe 'Copy groups' { $bStatus | Should -Be $true } It 'Does the group have the correct package' { - $Group = Get-CapaPackageGroups -CapaSDK $oCMS -PackageName 'TestTo' -PackageVersion 'v1.0' -PackageType Computer | Where-Object { $_.Name -eq 'TestGroup' } + $Group = Get-CapaPackageGroups -CapaSDK $oCMSProd -PackageName 'TestTo' -PackageVersion 'v1.0' -PackageType Computer | Where-Object { $_.Name -eq 'TestGroup' } $Group | Should -Not -BeNullOrEmpty } } Describe 'Copy units' { It 'Does the unit exist in the destination package' { - $Unit = Get-CapaUnits -CapaSDK $oCms -Type Computer | Where-Object { $_.Name -eq $env:COMPUTERNAME } + $Unit = Get-CapaUnits -CapaSDK $oCmsProd -Type Computer | Where-Object { $_.Name -eq $env:COMPUTERNAME } $Unit | Should -Not -BeNullOrEmpty } It 'Does the command work' { $Splat = @{ - CapaSDK = $oCMS + CapaSDK = $oCMSProd FromPackageName = 'TestFrom' FromPackageVersion = 'v1.0' FromPackageType = 'Computer' @@ -80,14 +99,14 @@ Describe 'Copy units' { $bStatus | Should -Be $true } It 'Does the unit have the correct package' { - $Unit = Get-CapaPackageUnits -CapaSDK $oCMS -PackageName 'TestTo' -PackageVersion 'v1.0' -PackageType Computer + $Unit = Get-CapaPackageUnits -CapaSDK $oCMSProd -PackageName 'TestTo' -PackageVersion 'v1.0' -PackageType Computer $Unit | Should -Not -BeNullOrEmpty } } Describe 'Copy groups and units' { It 'Does the command work' { $Splat = @{ - CapaSDK = $oCMS + CapaSDK = $oCMSProd FromPackageName = 'TestFrom' FromPackageVersion = 'v1.0' FromPackageType = 'Computer' @@ -103,18 +122,18 @@ Describe 'Copy groups and units' { $bStatus | Should -Be $true } It 'Does the group have the correct package' { - $Group = Get-CapaPackageGroups -CapaSDK $oCMS -PackageName 'TestTo' -PackageVersion 'v1.0' -PackageType Computer | Where-Object { $_.Name -eq 'TestGroup' } + $Group = Get-CapaPackageGroups -CapaSDK $oCMSProd -PackageName 'TestTo' -PackageVersion 'v1.0' -PackageType Computer | Where-Object { $_.Name -eq 'TestGroup' } $Group | Should -Not -BeNullOrEmpty } It 'Does the unit have the correct package' { - $Unit = Get-CapaPackageUnits -CapaSDK $oCMS -PackageName 'TestTo' -PackageVersion 'v1.0' -PackageType Computer | Where-Object { $_.Name -eq $env:COMPUTERNAME } + $Unit = Get-CapaPackageUnits -CapaSDK $oCMSProd -PackageName 'TestTo' -PackageVersion 'v1.0' -PackageType Computer | Where-Object { $_.Name -eq $env:COMPUTERNAME } $Unit | Should -Not -BeNullOrEmpty } } Describe 'Try the rest of the parameters' { It 'Does the command work' { $Splat = @{ - CapaSDK = $oCMS + CapaSDK = $oCMSProd FromPackageName = 'TestFrom' FromPackageVersion = 'v1.0' FromPackageType = 'Computer' @@ -130,16 +149,18 @@ Describe 'Try the rest of the parameters' { $bStatus | Should -Be $true } It 'Should have no groups linked' { - $Group = Get-CapaPackageGroups -CapaSDK $oCMS -PackageName 'TestFrom' -PackageVersion 'v1.0' -PackageType Computer + $Group = Get-CapaPackageGroups -CapaSDK $oCMSProd -PackageName 'TestFrom' -PackageVersion 'v1.0' -PackageType Computer $Group | Should -BeNullOrEmpty } It 'Should have no units linked' { - $Unit = Get-CapaPackageUnits -CapaSDK $oCMS -PackageName 'TestFrom' -PackageVersion 'v1.0' -PackageType Computer + $Unit = Get-CapaPackageUnits -CapaSDK $oCMSProd -PackageName 'TestFrom' -PackageVersion 'v1.0' -PackageType Computer $Unit | Should -BeNullOrEmpty } } AfterAll { - Remove-CapaPackage -CapaSDK $oCms -PackageName 'TestFrom' -PackageVersion 'v1.0' -PackageType Computer - Remove-CapaPackage -CapaSDK $oCms -PackageName 'TestTo' -PackageVersion 'v1.0' -PackageType Computer - Remove-CapaGroup -CapaSDK $oCms -GroupName 'TestGroup' -GroupType 'Static' -UnitType Computer + Remove-CapaPackage -CapaSDK $oCMSProd -PackageName 'TestFrom' -PackageVersion 'v1.0' -PackageType Computer + Remove-CapaPackage -CapaSDK $oCMSProd -PackageName 'TestTo' -PackageVersion 'v1.0' -PackageType Computer + Remove-CapaPackage -CapaSDK $oCMSDev -PackageName 'TestFrom' -PackageVersion 'v1.0' -PackageType Computer + Remove-CapaPackage -CapaSDK $oCMSDev -PackageName 'TestTo' -PackageVersion 'v1.0' -PackageType Computer + Remove-CapaGroup -CapaSDK $oCMSProd -GroupName 'TestGroup' -GroupType 'Static' -UnitType Computer } \ No newline at end of file diff --git a/Modules/Capa.PowerShell.Module.SDK.Package/Dev/Import-CapaPackage.Tests.ps1 b/Modules/Capa.PowerShell.Module.SDK.Package/Dev/Import-CapaPackage.Tests.ps1 index afe662b5..1f2d687d 100644 --- a/Modules/Capa.PowerShell.Module.SDK.Package/Dev/Import-CapaPackage.Tests.ps1 +++ b/Modules/Capa.PowerShell.Module.SDK.Package/Dev/Import-CapaPackage.Tests.ps1 @@ -3,16 +3,21 @@ BeforeAll { . $PSCommandPath.Replace('.Tests.ps1', '.ps1') $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\Remove-CapaPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\Exist-CapaPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\New-CapaPowerPack.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\Export-CapaPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Authentication\Dev\Initialize-CapaSDK.ps1" + $Folders = @( + 'Capa.PowerShell.Module.SDK.Authentication', + 'Capa.PowerShell.Module.SDK.Package' + ) + foreach ($Folder in $Folders) { + $Items = Get-ChildItem -Path "$RootPath\$Folder\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + } - $CapaSDK = Initialize-CapaSDK -Server $env:COMPUTERNAME -Database 'CapaInstaller' -InstanceManagementPoint 1 + $oCMSDev = Initialize-CapaSDK -Server $env:COMPUTERNAME -Database 'CapaInstaller' -InstanceManagementPoint 1 $PackageSplatting = @{ - CapaSDK = $CapaSDK + CapaSDK = $oCMSDev PackageName = 'Test1' PackageVersion = 'v1.0' DisplayName = 'Test1 v1.0' @@ -20,7 +25,7 @@ BeforeAll { Database = 'CapaInstaller' } $PackageExportSplatting = @{ - CapaSDK = $CapaSDK + CapaSDK = $oCMSDev PackageType = 'Computer' PackageName = 'Test1' PackageVersion = 'v1.0' @@ -28,7 +33,7 @@ BeforeAll { } $PackageRemoveSplatting = @{ - CapaSDK = $CapaSDK + CapaSDK = $oCMSDev PackageName = 'Test1' PackageVersion = 'v1.0' PackageType = 'Computer' @@ -48,7 +53,7 @@ BeforeAll { Describe 'Test Import-CapaPackage' { BeforeAll { $PackageImportSplatting = @{ - CapaSDK = $CapaSDK + CapaSDK = $oCMSDev FilePath = 'C:\Temp\Test1_v1.0.zip' OverrideCIPCdata = $true ImportFolderStructure = $true diff --git a/Modules/Capa.PowerShell.Module.SDK.Package/Dev/New-CapaPackage.Tests.ps1 b/Modules/Capa.PowerShell.Module.SDK.Package/Dev/New-CapaPackage.Tests.ps1 index f6a926b5..9ca880c1 100644 --- a/Modules/Capa.PowerShell.Module.SDK.Package/Dev/New-CapaPackage.Tests.ps1 +++ b/Modules/Capa.PowerShell.Module.SDK.Package/Dev/New-CapaPackage.Tests.ps1 @@ -3,9 +3,16 @@ BeforeAll { . $PSCommandPath.Replace('.Tests.ps1', '.ps1') $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\Remove-CapaPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\Exist-CapaPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Authentication\Dev\Initialize-CapaSDK.ps1" + $Folders = @( + 'Capa.PowerShell.Module.SDK.Authentication', + 'Capa.PowerShell.Module.SDK.Package' + ) + foreach ($Folder in $Folders) { + $Items = Get-ChildItem -Path "$RootPath\$Folder\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + } $CapaSDK = Initialize-CapaSDK -Server $env:COMPUTERNAME -Database 'CapaInstaller' -InstanceManagementPoint 1 diff --git a/Modules/Capa.PowerShell.Module.SDK.Package/Dev/New-CapaPackageWithGit.Tests.ps1 b/Modules/Capa.PowerShell.Module.SDK.Package/Dev/New-CapaPackageWithGit.Tests.ps1 index f49e3531..2e2d6c78 100644 --- a/Modules/Capa.PowerShell.Module.SDK.Package/Dev/New-CapaPackageWithGit.Tests.ps1 +++ b/Modules/Capa.PowerShell.Module.SDK.Package/Dev/New-CapaPackageWithGit.Tests.ps1 @@ -65,13 +65,11 @@ Describe 'VB package not advanced' { It 'Does the installfile contains the correct content' { $InstallScript | Should -FileContentMatch $PackageSpllatting.PackageName $InstallScript | Should -FileContentMatch $PackageSpllatting.PackageVersion - $InstallScript | Should -FileContentMatch $env:USERNAME $InstallScript | Should -FileContentMatch (Get-Date -Format 'dd-MM-yyyy') } It 'Does the uninstallfile contains the correct content' { $UninstallScript | Should -FileContentMatch $PackageSpllatting.PackageName $UninstallScript | Should -FileContentMatch $PackageSpllatting.PackageVersion - $UninstallScript | Should -FileContentMatch $env:USERNAME $UninstallScript | Should -FileContentMatch (Get-Date -Format 'dd-MM-yyyy') } It 'Does the update package script contains the correct content' { @@ -179,13 +177,11 @@ Describe 'VB package advanced' { It 'Does the installfile contains the correct content' { $InstallScript | Should -FileContentMatch $PackageSpllatting.SoftwareName $InstallScript | Should -FileContentMatch $PackageSpllatting.SoftwareVersion - $InstallScript | Should -FileContentMatch $env:USERNAME $InstallScript | Should -FileContentMatch (Get-Date -Format 'dd-MM-yyyy') } It 'Does the uninstallfile contains the correct content' { $UninstallScript | Should -FileContentMatch $PackageSpllatting.SoftwareName $UninstallScript | Should -FileContentMatch $PackageSpllatting.SoftwareVersion - $UninstallScript | Should -FileContentMatch $env:USERNAME $UninstallScript | Should -FileContentMatch (Get-Date -Format 'dd-MM-yyyy') } It 'Does the update package script contains the correct content' { @@ -209,7 +205,4 @@ Describe 'VB package advanced' { AfterAll { Remove-Item -Path $PackagePath -Recurse -Force } -} -AfterAll { - Get-Module | Remove-Module } \ No newline at end of file diff --git a/Modules/Capa.PowerShell.Module.SDK.Package/Dev/New-CapaPowerPack.Tests.ps1 b/Modules/Capa.PowerShell.Module.SDK.Package/Dev/New-CapaPowerPack.Tests.ps1 index 5f078ea4..da3c0f3b 100644 --- a/Modules/Capa.PowerShell.Module.SDK.Package/Dev/New-CapaPowerPack.Tests.ps1 +++ b/Modules/Capa.PowerShell.Module.SDK.Package/Dev/New-CapaPowerPack.Tests.ps1 @@ -1,204 +1,230 @@ BeforeAll { - # Import file - . $PSCommandPath.Replace('.Tests.ps1', '.ps1') - $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent - - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\Import-CapaPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\Remove-CapaPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\Exist-CapaPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Authentication\Dev\Initialize-CapaSDK.ps1" - - $CapaSDK = Initialize-CapaSDK -Server $env:COMPUTERNAME -Database 'CapaInstaller' -InstanceManagementPoint 1 - - $PackageRoot = Get-ItemPropertyValue -Path 'registry::HKEY_LOCAL_MACHINE\SOFTWARE\CapaSystems\CapaInstaller' -Name 'Packageroot' - $ComputerJobPath = Join-Path $PackageRoot 'ComputerJobs' + # Import file + . $PSCommandPath.Replace('.Tests.ps1', '.ps1') + $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent + + $Folders = @( + 'Capa.PowerShell.Module.SDK.Authentication', + 'Capa.PowerShell.Module.SDK.Package' + ) + foreach ($Folder in $Folders) { + $Items = Get-ChildItem -Path "$RootPath\$Folder\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + } + + $oCMSDev = Initialize-CapaSDK -Server $env:COMPUTERNAME -Database 'CapaInstaller' -InstanceManagementPoint 1 + $oCMSProd = Initialize-CapaSDK -Server $env:COMPUTERNAME -Database 'CapaInstaller' -InstanceManagementPoint 2 + + $PackageRoot = Get-ItemPropertyValue -Path 'registry::HKEY_LOCAL_MACHINE\SOFTWARE\CapaSystems\CapaInstaller' -Name 'Packageroot' + # PackageRoot format: \\ServerName\Prod\... or \\ServerName\COMPUTERNAME\... + # Only replace 'Prod' with COMPUTERNAME if it's not already the computer name + if ($PackageRoot -match '\\\\[^\\]+\\Prod\\') { + $PackageRoot = $PackageRoot.Replace('\Prod\', "\$env:COMPUTERNAME\") + } + $ComputerJobPath = Join-Path $PackageRoot 'ComputerJobs' } Describe 'New plain PowerPack' { - BeforeAll { - $TempFolder = "C:\Users\$env:UserName\AppData\Local\CapaInstaller\CMS\TempScripts" - $TempTempFolder = Join-Path $TempFolder 'Temp' - - $PowerPackSplatting = @{ - CapaSDK = $CapaSDK - PackageName = 'Test1' - PackageVersion = 'v1.0' - SqlServerInstance = $env:COMPUTERNAME - Database = 'CapaInstaller' - } - New-CapaPowerPack @PowerPackSplatting - } - It 'Package should exist' { - $Package = Exist-CapaPackage -CapaSDK $CapaSDK -Name $PowerPackSplatting.PackageName -Version $PowerPackSplatting.PackageVersion -Type 'Computer' - $Package | Should -Not -BeNullOrEmpty - } - It 'Temptemp folder should not exist' { - $TempTempFolder | Should -Not -Exist - } - It 'Test package structure' { - $PackagePath = Join-Path $ComputerJobPath $PowerPackSplatting.PackageName $PowerPackSplatting.PackageVersion - $DummyFile = Join-Path $PackagePath '\Kit\Dummy.txt' - $KitFile = Join-Path $PackagePath '\Zip\CapaInstaller.kit' - - $DummyFile | Should -Exist - $KitFile | Should -Exist - } - It 'Check data in DB' { - $Query = "SELECT * FROM JOB WHERE Name = '$($PowerPackSplatting.PackageName)' AND Version = '$($PowerPackSplatting.PackageVersion)'" - $Package = Invoke-Sqlcmd -ServerInstance $PowerPackSplatting.SqlServerInstance -Database $PowerPackSplatting.Database -Query $Query -TrustServerCertificate - - $Package | Should -Not -BeNullOrEmpty - $Package.POWERPACK | Should -Be 'True' - $Package.INSTALLSCRIPTCONTENT | Should -Not -BeNullOrEmpty - $Package.UNINSTALLSCRIPTCONTENT | Should -Not -BeNullOrEmpty - } - AfterAll { - $PackageSplatting = @{ - CapaSDK = $CapaSDK - PackageName = $PowerPackSplatting.PackageName - PackageVersion = $PowerPackSplatting.PackageVersion - PackageType = 'Computer' - } - Remove-CapaPackage @PackageSplatting - } + BeforeAll { + $TempFolder = "C:\Users\$env:UserName\AppData\Local\CapaInstaller\CMS\TempScripts" + $TempTempFolder = Join-Path $TempFolder 'Temp' + + $PowerPackSplatting = @{ + CapaSDK = $oCMSDev + PackageName = 'Test1' + PackageVersion = 'v1.0' + SqlServerInstance = $env:COMPUTERNAME + Database = 'CapaInstaller' + } + New-CapaPowerPack @PowerPackSplatting + Start-Sleep -Seconds 5 + Initialize-CapaPackagePromote -CapaSDK $oCMSDev -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' + } + It 'Package should exist' { + $Package = Exist-CapaPackage -CapaSDK $oCMSProd -Name $PowerPackSplatting.PackageName -Version $PowerPackSplatting.PackageVersion -Type 'Computer' + $Package | Should -Not -BeNullOrEmpty + } + It 'Temptemp folder should not exist' { + $TempTempFolder | Should -Not -Exist + } + It 'Test package structure' { + $PackagePath = Join-Path $ComputerJobPath $PowerPackSplatting.PackageName $PowerPackSplatting.PackageVersion + $DummyFile = Join-Path $PackagePath '\Kit\Dummy.txt' + $KitFile = Join-Path $PackagePath '\Zip\CapaInstaller.kit' + + $DummyFile | Should -Exist + $KitFile | Should -Exist + } + It 'Check data in DB' { + $Query = "SELECT * FROM JOB WHERE Name = '$($PowerPackSplatting.PackageName)' AND Version = '$($PowerPackSplatting.PackageVersion)' AND CMPID = 1" + $Package = Invoke-Sqlcmd -ServerInstance $PowerPackSplatting.SqlServerInstance -Database $PowerPackSplatting.Database -Query $Query -TrustServerCertificate + + $Package | Should -Not -BeNullOrEmpty + $Package.POWERPACK | Should -Be $true + $Package.INSTALLSCRIPTCONTENT | Should -Not -BeNullOrEmpty + $Package.UNINSTALLSCRIPTCONTENT | Should -Not -BeNullOrEmpty + } + AfterAll { + $PackageSplatting = @{ + CapaSDK = $oCMSProd + PackageName = $PowerPackSplatting.PackageName + PackageVersion = $PowerPackSplatting.PackageVersion + PackageType = 'Computer' + Force = $true + } + Remove-CapaPackage @PackageSplatting + $PackageSplatting.CapaSDK = $oCMSDev + Remove-CapaPackage @PackageSplatting + Initialize-CapaPackagePromote -CapaSDK $oCMSDev -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' + } } Describe 'New PowerPack with it all' { - BeforeAll { - $TempFolder = "C:\Users\$env:UserName\AppData\Local\CapaInstaller\CMS\TempScripts" - $TempTempFolder = Join-Path $TempFolder 'Temp' - $KitFileName = 'Test2.txt' - - $PowerPackSplatting = @{ - CapaSDK = $CapaSDK - PackageName = 'Test2' - PackageVersion = 'v1.0' - DisplayName = 'PowerPack Test2' - InstallScriptContent = 'Write-Host "Install"' - UninstallScriptContent = 'Write-Host "Uninstall"' - KitFolderPath = 'C:\Temp\Kit' - ChangelogComment = 'Test' - SqlServerInstance = $env:COMPUTERNAME - Database = 'CapaInstaller' - PointId = 1 - } - - New-Item -Path $PowerPackSplatting.KitFolderPath -Name $KitFileName -ItemType File -Force | Out-Null - - New-CapaPowerPack @PowerPackSplatting - } - It 'Package should exist' { - $Package = Exist-CapaPackage -CapaSDK $CapaSDK -Name $PowerPackSplatting.PackageName -Version $PowerPackSplatting.PackageVersion -Type 'Computer' - $Package | Should -Not -BeNullOrEmpty - } - It 'Temptemp folder should not exist' { - $TempTempFolder | Should -Not -Exist - } - It 'Test package structure' { - $PackagePath = Join-Path $ComputerJobPath $PowerPackSplatting.PackageName $PowerPackSplatting.PackageVersion - $DummyFile = Join-Path $PackagePath 'Kit' $KitFileName - $KitFile = Join-Path $PackagePath '\Zip\CapaInstaller.kit' - - $DummyFile | Should -Exist - $KitFile | Should -Exist - } - It 'Check data in DB' { - $Query = "SELECT * FROM JOB WHERE Name = '$($PowerPackSplatting.PackageName)' AND Version = '$($PowerPackSplatting.PackageVersion)'" - $Package = Invoke-Sqlcmd -ServerInstance $PowerPackSplatting.SqlServerInstance -Database $PowerPackSplatting.Database -Query $Query -TrustServerCertificate - - $Package | Should -Not -BeNullOrEmpty - $Package.POWERPACK | Should -Be 'True' - $Package.INSTALLSCRIPTCONTENT | Should -Not -BeNullOrEmpty - $Package.UNINSTALLSCRIPTCONTENT | Should -Not -BeNullOrEmpty - $Package.DISPLAYNAME | Should -Be $PowerPackSplatting.DisplayName - } - AfterAll { - $PackageSplatting = @{ - CapaSDK = $CapaSDK - PackageName = $PowerPackSplatting.PackageName - PackageVersion = $PowerPackSplatting.PackageVersion - PackageType = 'Computer' - } - Remove-CapaPackage @PackageSplatting - } + BeforeAll { + $TempFolder = "C:\Users\$env:UserName\AppData\Local\CapaInstaller\CMS\TempScripts" + $TempTempFolder = Join-Path $TempFolder 'Temp' + $KitFileName = 'Test2.txt' + + $PowerPackSplatting = @{ + CapaSDK = $oCMSDev + PackageName = 'Test2' + PackageVersion = 'v1.0' + DisplayName = 'PowerPack Test2' + InstallScriptContent = 'Write-Host "Install"' + UninstallScriptContent = 'Write-Host "Uninstall"' + KitFolderPath = 'C:\Temp\Kit' + ChangelogComment = 'Test' + SqlServerInstance = $env:COMPUTERNAME + Database = 'CapaInstaller' + PointId = 1 + } + + New-Item -Path $PowerPackSplatting.KitFolderPath -Name $KitFileName -ItemType File -Force | Out-Null + + New-CapaPowerPack @PowerPackSplatting + Initialize-CapaPackagePromote -CapaSDK $oCMSDev -PackageName 'Test2' -PackageVersion 'v1.0' -PackageType 'Computer' + } + It 'Package should exist' { + $Package = Exist-CapaPackage -CapaSDK $oCMSProd -Name $PowerPackSplatting.PackageName -Version $PowerPackSplatting.PackageVersion -Type 'Computer' + $Package | Should -Not -BeNullOrEmpty + } + It 'Temptemp folder should not exist' { + $TempTempFolder | Should -Not -Exist + } + It 'Test package structure' { + $PackagePath = Join-Path $ComputerJobPath $PowerPackSplatting.PackageName $PowerPackSplatting.PackageVersion + $DummyFile = Join-Path $PackagePath 'Kit' $KitFileName + $KitFile = Join-Path $PackagePath '\Zip\CapaInstaller.kit' + + $DummyFile | Should -Exist + $KitFile | Should -Exist + } + It 'Check data in DB' { + $Query = "SELECT * FROM JOB WHERE Name = '$($PowerPackSplatting.PackageName)' AND Version = '$($PowerPackSplatting.PackageVersion)' AND CMPID = 2" + $Package = Invoke-Sqlcmd -ServerInstance $PowerPackSplatting.SqlServerInstance -Database $PowerPackSplatting.Database -Query $Query -TrustServerCertificate + + $Package | Should -Not -BeNullOrEmpty + $Package.POWERPACK | Should -Be 'True' + $Package.INSTALLSCRIPTCONTENT | Should -Not -BeNullOrEmpty + $Package.UNINSTALLSCRIPTCONTENT | Should -Not -BeNullOrEmpty + $Package.DISPLAYNAME | Should -Be $PowerPackSplatting.DisplayName + } + AfterAll { + $PackageSplatting = @{ + CapaSDK = $oCMSProd + PackageName = $PowerPackSplatting.PackageName + PackageVersion = $PowerPackSplatting.PackageVersion + PackageType = 'Computer' + Force = $true + } + Remove-CapaPackage @PackageSplatting + $PackageSplatting.CapaSDK = $oCMSDev + Remove-CapaPackage @PackageSplatting + } } Describe 'New PowerPack with large kit folder' { - BeforeAll { - $TempFolder = "C:\Users\$env:UserName\AppData\Local\CapaInstaller\CMS\TempScripts" - $TempTempFolder = Join-Path $TempFolder 'Temp' - $KitFolderPath = 'C:\Temp\Kit' - $KitFileName1 = 'Test3.1.txt' - $KitFileName2 = 'Test3.2.txt' - $KitFileName3 = 'Test3.3.txt' - - $PowerPackSplatting = @{ - CapaSDK = $CapaSDK - PackageName = 'Test3' - PackageVersion = 'v1.0' - DisplayName = 'PowerPack Test3' - InstallScriptContent = 'Write-Host "Install"' - UninstallScriptContent = 'Write-Host "Uninstall"' - KitFolderPath = $KitFolderPath - ChangelogComment = 'Test' - SqlServerInstance = $env:COMPUTERNAME - Database = 'CapaInstaller' - PointId = 1 - } - - # Create large files - $Size = 1GB - $Content = New-Object byte[] $Size - (New-Object System.Random).NextBytes($Content) - - [System.IO.File]::WriteAllBytes("$KitFolderPath\$KitFileName1", $Content) - [System.IO.File]::WriteAllBytes("$KitFolderPath\$KitFileName2", $Content) - [System.IO.File]::WriteAllBytes("$KitFolderPath\$KitFileName3", $Content) - - New-CapaPowerPack @PowerPackSplatting - } - It 'Package should exist' { - $Package = Exist-CapaPackage -CapaSDK $CapaSDK -Name $PowerPackSplatting.PackageName -Version $PowerPackSplatting.PackageVersion -Type 'Computer' - $Package | Should -Not -BeNullOrEmpty - } - It 'Temptemp folder should not exist' { - $TempTempFolder | Should -Not -Exist - } - It 'Test package structure' { - $PackagePath = Join-Path $ComputerJobPath $PowerPackSplatting.PackageName $PowerPackSplatting.PackageVersion - $DummyFile1 = Join-Path $PackagePath 'Kit' $KitFileName1 - $DummyFile2 = Join-Path $PackagePath 'Kit' $KitFileName2 - $DummyFile3 = Join-Path $PackagePath 'Kit' $KitFileName3 - $KitFile = Join-Path $PackagePath '\Zip\CapaInstaller.kit' - $DummyFile1Size = (Get-Item $DummyFile1).Length - $DummyFile2Size = (Get-Item $DummyFile2).Length - $DummyFile3Size = (Get-Item $DummyFile3).Length - - $DummyFile1 | Should -Exist - $DummyFile2 | Should -Exist - $DummyFile3 | Should -Exist - $DummyFile1Size | Should -Be 1GB - $DummyFile2Size | Should -Be 1GB - $DummyFile3Size | Should -Be 1GB - $KitFile | Should -Exist - } - It 'Check data in DB' { - $Query = "SELECT * FROM JOB WHERE Name = '$($PowerPackSplatting.PackageName)' AND Version = '$($PowerPackSplatting.PackageVersion)'" - $Package = Invoke-Sqlcmd -ServerInstance $PowerPackSplatting.SqlServerInstance -Database $PowerPackSplatting.Database -Query $Query -TrustServerCertificate - - $Package | Should -Not -BeNullOrEmpty - $Package.POWERPACK | Should -Be 'True' - $Package.INSTALLSCRIPTCONTENT | Should -Not -BeNullOrEmpty - $Package.UNINSTALLSCRIPTCONTENT | Should -Not -BeNullOrEmpty - $Package.DISPLAYNAME | Should -Be $PowerPackSplatting.DisplayName - } - AfterAll { - $PackageSplatting = @{ - CapaSDK = $CapaSDK - PackageName = $PowerPackSplatting.PackageName - PackageVersion = $PowerPackSplatting.PackageVersion - PackageType = 'Computer' - } - Remove-CapaPackage @PackageSplatting - } + BeforeAll { + $TempFolder = "C:\Users\$env:UserName\AppData\Local\CapaInstaller\CMS\TempScripts" + $TempTempFolder = Join-Path $TempFolder 'Temp' + $KitFolderPath = 'C:\Temp\Kit' + $KitFileName1 = 'Test3.1.txt' + $KitFileName2 = 'Test3.2.txt' + $KitFileName3 = 'Test3.3.txt' + + $PowerPackSplatting = @{ + CapaSDK = $oCMSDev + PackageName = 'Test3' + PackageVersion = 'v1.0' + DisplayName = 'PowerPack Test3' + InstallScriptContent = 'Write-Host "Install"' + UninstallScriptContent = 'Write-Host "Uninstall"' + KitFolderPath = $KitFolderPath + ChangelogComment = 'Test' + SqlServerInstance = $env:COMPUTERNAME + Database = 'CapaInstaller' + PointId = 1 + } + + # Create large files + $Size = 1GB + $Content = New-Object byte[] $Size + (New-Object System.Random).NextBytes($Content) + + [System.IO.File]::WriteAllBytes("$KitFolderPath\$KitFileName1", $Content) + [System.IO.File]::WriteAllBytes("$KitFolderPath\$KitFileName2", $Content) + [System.IO.File]::WriteAllBytes("$KitFolderPath\$KitFileName3", $Content) + + New-CapaPowerPack @PowerPackSplatting + Initialize-CapaPackagePromote -CapaSDK $oCMSDev -PackageName 'Test3' -PackageVersion 'v1.0' -PackageType 'Computer' + } + It 'Package should exist' { + $Package = Exist-CapaPackage -CapaSDK $oCMSDev -Name $PowerPackSplatting.PackageName -Version $PowerPackSplatting.PackageVersion -Type 'Computer' + $Package | Should -Not -BeNullOrEmpty + } + It 'Temptemp folder should not exist' { + $TempTempFolder | Should -Not -Exist + } + It 'Test package structure' { + $PackagePath = Join-Path $ComputerJobPath $PowerPackSplatting.PackageName $PowerPackSplatting.PackageVersion + $DummyFile1 = Join-Path $PackagePath 'Kit' $KitFileName1 + $DummyFile2 = Join-Path $PackagePath 'Kit' $KitFileName2 + $DummyFile3 = Join-Path $PackagePath 'Kit' $KitFileName3 + $KitFile = Join-Path $PackagePath '\Zip\CapaInstaller.kit' + $DummyFile1Size = (Get-Item $DummyFile1).Length + $DummyFile2Size = (Get-Item $DummyFile2).Length + $DummyFile3Size = (Get-Item $DummyFile3).Length + + $DummyFile1 | Should -Exist + $DummyFile2 | Should -Exist + $DummyFile3 | Should -Exist + $DummyFile1Size | Should -Be 1GB + $DummyFile2Size | Should -Be 1GB + $DummyFile3Size | Should -Be 1GB + $KitFile | Should -Exist + } + It 'Check data in DB' { + $Query = "SELECT * FROM JOB WHERE Name = '$($PowerPackSplatting.PackageName)' AND Version = '$($PowerPackSplatting.PackageVersion)' AND CMPID = 2" + $Package = Invoke-Sqlcmd -ServerInstance $PowerPackSplatting.SqlServerInstance -Database $PowerPackSplatting.Database -Query $Query -TrustServerCertificate + + $Package | Should -Not -BeNullOrEmpty + $Package.POWERPACK | Should -Be 'True' + $Package.INSTALLSCRIPTCONTENT | Should -Not -BeNullOrEmpty + $Package.UNINSTALLSCRIPTCONTENT | Should -Not -BeNullOrEmpty + $Package.DISPLAYNAME | Should -Be $PowerPackSplatting.DisplayName + } + AfterAll { + $PackageSplatting = @{ + CapaSDK = $oCMSProd + PackageName = $PowerPackSplatting.PackageName + PackageVersion = $PowerPackSplatting.PackageVersion + PackageType = 'Computer' + Force = $true + } + Remove-CapaPackage @PackageSplatting + $PackageSplatting.CapaSDK = $oCMSDev + Remove-CapaPackage @PackageSplatting + } } AfterAll { - Remove-Item -Path 'C:\Temp\Kit' -Force -Confirm:$false -Recurse + Remove-Item -Path 'C:\Temp\Kit' -Force -Confirm:$false -Recurse } \ No newline at end of file diff --git a/Modules/Capa.PowerShell.Module.SDK.Package/Dev/New-CapaPowerPack.ps1 b/Modules/Capa.PowerShell.Module.SDK.Package/Dev/New-CapaPowerPack.ps1 index d95eec72..231c85c4 100644 --- a/Modules/Capa.PowerShell.Module.SDK.Package/Dev/New-CapaPowerPack.ps1 +++ b/Modules/Capa.PowerShell.Module.SDK.Package/Dev/New-CapaPowerPack.ps1 @@ -169,5 +169,10 @@ function New-CapaPowerPack { } Catch { $PSCmdlet.ThrowTerminatingError($PSitem) return -1 - } + } Finally { + # Cleanup Temp Folder + If (Test-Path $TempTempFolder) { + Remove-Item -Path $TempTempFolder -Recurse -Force | Out-Null + } + } } diff --git a/Modules/Capa.PowerShell.Module.SDK.Package/Dev/Set-CapaPackageFolder.Tests.ps1 b/Modules/Capa.PowerShell.Module.SDK.Package/Dev/Set-CapaPackageFolder.Tests.ps1 index 712f3fcd..bc773638 100644 --- a/Modules/Capa.PowerShell.Module.SDK.Package/Dev/Set-CapaPackageFolder.Tests.ps1 +++ b/Modules/Capa.PowerShell.Module.SDK.Package/Dev/Set-CapaPackageFolder.Tests.ps1 @@ -3,10 +3,16 @@ BeforeAll { . $PSCommandPath.Replace('.Tests.ps1', '.ps1') $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\Get-CapaPackageFolder.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\New-CapaPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Authentication\Dev\Initialize-CapaSDK.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\Remove-CapaPackage.ps1" + $Folders = @( + 'Capa.PowerShell.Module.SDK.Authentication', + 'Capa.PowerShell.Module.SDK.Package' + ) + foreach ($Folder in $Folders) { + $Items = Get-ChildItem -Path "$RootPath\$Folder\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + } $CapaSDK = Initialize-CapaSDK -Server $env:COMPUTERNAME -Database 'CapaInstaller' -InstanceManagementPoint 1 @@ -46,5 +52,4 @@ Describe 'Test with a package allready in the folder' { } AfterAll { Remove-CapaPackage -CapaSDK $CapaSDK -PackageName 'Test1' -PackageVersion 'v1.0' -PackageType 'Computer' -Force True - Get-Module | Remove-Module } \ No newline at end of file diff --git a/Modules/Capa.PowerShell.Module.SDK.Package/Dev/Update-CapaPackageScriptAndKit.Tests.ps1 b/Modules/Capa.PowerShell.Module.SDK.Package/Dev/Update-CapaPackageScriptAndKit.Tests.ps1 index 820b5be2..eca62aa3 100644 --- a/Modules/Capa.PowerShell.Module.SDK.Package/Dev/Update-CapaPackageScriptAndKit.Tests.ps1 +++ b/Modules/Capa.PowerShell.Module.SDK.Package/Dev/Update-CapaPackageScriptAndKit.Tests.ps1 @@ -3,11 +3,16 @@ BeforeAll { . $PSCommandPath.Replace('.Tests.ps1', '.ps1') $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\Remove-CapaPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\New-CapaPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\New-CapaPowerPack.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Package\Dev\Exist-CapaPackage.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Authentication\Dev\Initialize-CapaSDK.ps1" + $Folders = @( + 'Capa.PowerShell.Module.SDK.Authentication', + 'Capa.PowerShell.Module.SDK.Package' + ) + foreach ($Folder in $Folders) { + $Items = Get-ChildItem -Path "$RootPath\$Folder\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + } #endregion ##region Parameters @@ -27,7 +32,7 @@ BeforeAll { } $PackageRoot = Get-ItemPropertyValue -Path 'registry::HKEY_LOCAL_MACHINE\SOFTWARE\CapaSystems\CapaInstaller' -Name 'Packageroot' - $ComputerJobsPath = Join-Path $PackageRoot 'ComputerJobs' + $ComputerJobsPath = Join-Path $PackageRoot.Replace('Prod', $env:COMPUTERNAME) 'ComputerJobs' $VBPackageFolder = Join-Path $ComputerJobsPath $VBPackageSplat.PackageName $VBPackageSplat.PackageVersion $VBScriptsFolder = Join-Path $VBPackageFolder 'Scripts' $VBInstallScriptFile = Join-Path $VBScriptsFolder "$($VBPackageSplat.PackageName).cis" diff --git a/Modules/Capa.PowerShell.Module.SDK.Unit/Dev/Get-CapaUnitPackageStatus.Tests.ps1 b/Modules/Capa.PowerShell.Module.SDK.Unit/Dev/Get-CapaUnitPackageStatus.Tests.ps1 index 9e9b68b2..2af8677b 100644 --- a/Modules/Capa.PowerShell.Module.SDK.Unit/Dev/Get-CapaUnitPackageStatus.Tests.ps1 +++ b/Modules/Capa.PowerShell.Module.SDK.Unit/Dev/Get-CapaUnitPackageStatus.Tests.ps1 @@ -2,11 +2,19 @@ BeforeAll { . $PSCommandPath.Replace('.Tests.ps1', '.ps1') $RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Unit\Dev\Get-CapaUnits.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Unit\Dev\Get-CapaUnitPackages.ps1" - Import-Module "$RootPath\Capa.PowerShell.Module.SDK.Authentication\Dev\Initialize-CapaSDK.ps1" + $Folders = @( + 'Capa.PowerShell.Module.SDK.Authentication', + 'Capa.PowerShell.Module.SDK.Unit' + ) + foreach ($Folder in $Folders) { + $Items = Get-ChildItem -Path "$RootPath\$Folder\Dev\" -Filter '*.ps1' | Where-Object { $_.Name -notlike '*Tests.ps1' } + foreach ($Item in $Items) { + Import-Module $Item.FullName -Force -ErrorAction Stop + } + } - $oCMS = Initialize-CapaSDK -Server 'localhost' -Database 'CapaInstaller' -DefaultManagementPoint '1' + + $oCMS = Initialize-CapaSDK -Server 'localhost' -Database 'CapaInstaller' -DefaultManagementPoint '2' } Describe 'Get-CapaUnitPackageStatus' { BeforeAll { diff --git a/Modules/Capa.PowerShell.Module.Tools/Dev/Start-ScriptLogging.Tests.ps1 b/Modules/Capa.PowerShell.Module.Tools/Dev/Start-ScriptLogging.Tests.ps1 index 2083dfb7..07e3d1a5 100644 --- a/Modules/Capa.PowerShell.Module.Tools/Dev/Start-ScriptLogging.Tests.ps1 +++ b/Modules/Capa.PowerShell.Module.Tools/Dev/Start-ScriptLogging.Tests.ps1 @@ -2,8 +2,8 @@ BeforeAll { # Import functions . $PSCommandPath.Replace('.Tests.ps1', '.ps1') - $StopLoggingPath = Join-Path $PSScriptRoot 'Stop-ScriptLogging.ps1' - Import-Module $StopLoggingPath -Force + Import-Module (Join-Path $PSScriptRoot 'Stop-ScriptLogging.ps1') -Force + Import-Module (Join-Path $PSScriptRoot 'Write-LogLine.ps1') -Force # Create test folder $TempFolderPath = Join-Path $PSScriptRoot 'Temp' @@ -141,6 +141,7 @@ Describe 'Does $DeleteDaysOldLogs work' { Describe 'Does $DeleteAllLogs work' { It 'Only one file should exist' { Stop-ScriptLogging + Start-Sleep -Seconds 2 Start-ScriptLogging -Path $TempFolderPath -LogName $TestFolderName -DeleteAllLogs $true Start-Sleep -Seconds 2 diff --git a/Modules/Capa.PowerShell.Module.Tools/Dev/Start-ScriptLogging.ps1 b/Modules/Capa.PowerShell.Module.Tools/Dev/Start-ScriptLogging.ps1 index 44204ff3..a47a5b44 100644 --- a/Modules/Capa.PowerShell.Module.Tools/Dev/Start-ScriptLogging.ps1 +++ b/Modules/Capa.PowerShell.Module.Tools/Dev/Start-ScriptLogging.ps1 @@ -136,7 +136,7 @@ function Start-ScriptLogging { $LogFiles = Get-ChildItem -Path $LogFolderPath foreach ($LogFile in $LogFiles) { - If ($LogFile.CreationTime.Date -le ((Get-Date).AddDays(-$DeleteDaysOldLogs).ToString('yyyy-MM-dd'))) { + If ($LogFile.CreationTime.Date -le ((Get-Date).AddDays(-$DeleteDaysOldLogs).ToString('yyyy-MM-dd')) -or $DeleteAllLogs -eq $true) { Remove-Item $LogFile -Force -ErrorAction SilentlyContinue Write-LogLine -ScriptPart $FunctionName -Text "Deleting $LogFile" }