diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f853fcd..4e46b36 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -153,6 +153,27 @@ jobs: Import-Module Pester -PassThru Write-Host "[+] Pester installed successfully" + - name: Enable SSH Agent Service + shell: pwsh + run: | + Write-Host "[i] Enabling SSH agent service for tests..." + + # Check if OpenSSH is installed + $sshAgent = Get-Service -Name "ssh-agent" -ErrorAction SilentlyContinue + + if (-not $sshAgent) { + Write-Host "[i] Installing OpenSSH..." + Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0 + } + + # Set service to Manual and start it + Set-Service -Name "ssh-agent" -StartupType Manual + Start-Service -Name "ssh-agent" + + # Verify + $service = Get-Service -Name "ssh-agent" + Write-Host "[+] SSH agent status: $($service.Status)" + - name: Run Windows tests shell: pwsh run: | diff --git a/.github/workflows/test-scripts.yml b/.github/workflows/test-scripts.yml index 611e562..81efe71 100644 --- a/.github/workflows/test-scripts.yml +++ b/.github/workflows/test-scripts.yml @@ -24,6 +24,27 @@ jobs: Import-Module Pester Import-Module PSScriptAnalyzer + - name: Enable SSH Agent Service + shell: pwsh + run: | + Write-Host "[i] Enabling SSH agent service for tests..." + + # Check if OpenSSH is installed + $sshAgent = Get-Service -Name "ssh-agent" -ErrorAction SilentlyContinue + + if (-not $sshAgent) { + Write-Host "[i] Installing OpenSSH..." + Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0 + } + + # Set service to Manual and start it + Set-Service -Name "ssh-agent" -StartupType Manual + Start-Service -Name "ssh-agent" + + # Verify + $service = Get-Service -Name "ssh-agent" + Write-Host "[+] SSH agent status: $($service.Status)" + - name: Run PSScriptAnalyzer shell: pwsh run: | diff --git a/tests/TestHelpers.psm1 b/tests/TestHelpers.psm1 index 7067b23..4932e00 100644 --- a/tests/TestHelpers.psm1 +++ b/tests/TestHelpers.psm1 @@ -207,6 +207,54 @@ function Test-NoPrivateIPs { return $true } +function Test-NoPrivateIPsInCode { + <# + .SYNOPSIS + Checks script for hardcoded private IPs in executable code only. + .DESCRIPTION + Uses PowerShell AST to parse the script and only checks non-comment, + non-string-literal tokens for private IP addresses. This allows + example IPs in documentation/comments while catching actual hardcoded IPs. + .PARAMETER Path + Path to the script file. + .OUTPUTS + Returns $true if no private IPs found in code, $false otherwise. + #> + [CmdletBinding()] + [OutputType([bool])] + param( + [Parameter(Mandatory = $true)] + [string]$Path + ) + + $tokens = $null + $errors = $null + [System.Management.Automation.Language.Parser]::ParseFile( + $Path, [ref]$tokens, [ref]$errors + ) | Out-Null + + $patterns = @( + '10\.\d{1,3}\.\d{1,3}\.\d{1,3}', + '172\.(1[6-9]|2[0-9]|3[0-1])\.\d{1,3}\.\d{1,3}', + '192\.168\.\d{1,3}\.\d{1,3}' + ) + + $skipKinds = @('Comment', 'StringLiteral', 'StringExpandable', + 'HereStringLiteral', 'HereStringExpandable') + + foreach ($token in $tokens) { + if ($token.Kind -in $skipKinds) { continue } + foreach ($pattern in $patterns) { + if ($token.Text -match $pattern) { + Write-Warning "Private IP found in code at line $($token.Extent.StartLineNumber): $($Matches[0])" + return $false + } + } + } + + return $true +} + # ============================================================================ # OUTPUT FORMAT HELPERS # ============================================================================ @@ -429,6 +477,7 @@ Export-ModuleMember -Function @( 'Get-ScriptParameters' 'Test-NoHardcodedSecrets' 'Test-NoPrivateIPs' + 'Test-NoPrivateIPsInCode' 'Test-ConsistentLogging' 'New-MockService' 'New-MockProcess' diff --git a/tests/Windows/Integration.Advanced.Tests.ps1 b/tests/Windows/Integration.Advanced.Tests.ps1 index c312e28..9146702 100644 --- a/tests/Windows/Integration.Advanced.Tests.ps1 +++ b/tests/Windows/Integration.Advanced.Tests.ps1 @@ -98,9 +98,13 @@ Describe "Integration Tests - SSH Setup Workflow" { } } - It "Verifies SSH agent service is running" -Skip:(-not (Get-Service -Name 'ssh-agent' -ErrorAction SilentlyContinue)) { - # Assert if service exists + It "Verifies SSH agent service is running" { + # Check if ssh-agent service exists $service = Get-Service -Name 'ssh-agent' -ErrorAction SilentlyContinue + if (-not $service) { + Set-ItResult -Skipped -Because "SSH agent service is not installed" + return + } $service | Should -Not -BeNullOrEmpty } @@ -400,7 +404,14 @@ Describe "Integration Tests - Full Workflow Scenarios" { Mock-NetworkCommands -ReachableHosts @('github.com', 'registry.npmjs.org') } - It "Validates environment before starting setup" -Skip:(-not ((Get-Service -Name 'ssh-agent' -ErrorAction SilentlyContinue).Status -eq 'Running')) { + It "Validates environment before starting setup" { + # Check if ssh-agent is running, skip if not + $sshAgent = Get-Service -Name 'ssh-agent' -ErrorAction SilentlyContinue + if (-not $sshAgent -or $sshAgent.Status -ne 'Running') { + Set-ItResult -Skipped -Because "SSH agent service is not running" + return + } + # Arrange $requiredServices = @('ssh-agent') diff --git a/tests/Windows/Integration.Tests.ps1 b/tests/Windows/Integration.Tests.ps1 index 0dc5597..75e27a3 100644 --- a/tests/Windows/Integration.Tests.ps1 +++ b/tests/Windows/Integration.Tests.ps1 @@ -28,7 +28,11 @@ Describe "Cross-Script Integration" { $scriptExists | Should -Be $true } - It "SSH setup script can be parsed" -Skip:(-not $scriptExists) { + It "SSH setup script can be parsed" { + if (-not $scriptExists) { + Set-ItResult -Skipped -Because "SSH setup script does not exist" + return + } { [scriptblock]::Create((Get-Content $setupScript -Raw)) } | Should -Not -Throw } @@ -186,21 +190,41 @@ Describe "Security Integration Tests" { $foundSecrets | Should -BeNullOrEmpty } - It "No scripts contain private IPs (except examples)" -Skip { - # Skipped: Sysadmin toolkit scripts legitimately contain example IPs for documentation - $scriptsToCheck = $allScripts | Where-Object { - $_.FullName -notmatch 'examples|docs|README' - } + It "No scripts contain private IPs in executable code" { + # Uses AST-based detection: allows IPs in comments/strings (documentation) + # but rejects them in actual executable code + $privateIpPatterns = @( + '10\.\d{1,3}\.\d{1,3}\.\d{1,3}', + '172\.(1[6-9]|2[0-9]|3[0-1])\.\d{1,3}\.\d{1,3}', + '192\.168\.\d{1,3}\.\d{1,3}' + ) - $foundPrivateIPs = @() - foreach ($script in $scriptsToCheck) { - $result = Test-NoPrivateIPs -Path $script.FullName -AllowExampleIPs - if (-not $result) { - $foundPrivateIPs += $script.Name + $foundProblems = @() + + foreach ($script in $allScripts) { + $tokens = $null + $errors = $null + [System.Management.Automation.Language.Parser]::ParseFile( + $script.FullName, [ref]$tokens, [ref]$errors + ) | Out-Null + + foreach ($token in $tokens) { + # Skip comments and string literals (documentation/examples are OK) + $skipKinds = @('Comment', 'StringLiteral', 'StringExpandable', + 'HereStringLiteral', 'HereStringExpandable') + if ($token.Kind -in $skipKinds) { + continue + } + + foreach ($pattern in $privateIpPatterns) { + if ($token.Text -match $pattern) { + $foundProblems += "$($script.Name):$($token.Extent.StartLineNumber)" + } + } } } - $foundPrivateIPs | Should -BeNullOrEmpty + $foundProblems | Should -BeNullOrEmpty } } diff --git a/tests/Windows/Tier3Scripts.Tests.ps1 b/tests/Windows/Tier3Scripts.Tests.ps1 index 5b82ec3..4416eb3 100644 --- a/tests/Windows/Tier3Scripts.Tests.ps1 +++ b/tests/Windows/Tier3Scripts.Tests.ps1 @@ -682,21 +682,31 @@ Describe "Tier 3 Scripts - Standards Compliance" -Tag "Standards", "Tier3" { } } -Describe "Tier 3 Scripts - SupportsShouldProcess" -Tag "ShouldProcess", "Tier3" -Skip { - # Skipped: SupportsShouldProcess is a future enhancement, not currently implemented - $scriptsWithShouldProcess = @('Backup-BrowserProfiles', 'Manage-VPN', 'Manage-WSL', 'Manage-Docker') +Describe "Tier 3 Scripts - SupportsShouldProcess" -Tag "ShouldProcess", "Tier3" { + BeforeDiscovery { + # Initialize paths during discovery for -ForEach + $testRoot = Split-Path -Parent $PSScriptRoot + $repoRoot = Split-Path -Parent $testRoot + $windowsRoot = Join-Path $repoRoot "Windows" + + $script:ShouldProcessScripts = @( + @{ Name = 'Backup-BrowserProfiles'; Path = (Join-Path $windowsRoot "backup\Backup-BrowserProfiles.ps1") } + @{ Name = 'Manage-VPN'; Path = (Join-Path $windowsRoot "network\Manage-VPN.ps1") } + @{ Name = 'Manage-WSL'; Path = (Join-Path $windowsRoot "development\Manage-WSL.ps1") } + @{ Name = 'Manage-Docker'; Path = (Join-Path $windowsRoot "development\Manage-Docker.ps1") } + ) + } - foreach ($scriptName in $scriptsWithShouldProcess) { - Context "$scriptName ShouldProcess support" { - It "Should have SupportsShouldProcess = `$true" { - $content = Get-Content $script:Tier3Scripts[$scriptName] -Raw - $content | Should -Match 'SupportsShouldProcess\s*=\s*\$true' - } + # All Tier 3 scripts now implement SupportsShouldProcess + Context " ShouldProcess support" -ForEach $script:ShouldProcessScripts { + It "Should have SupportsShouldProcess = `$true" { + $content = Get-Content $Path -Raw + $content | Should -Match 'SupportsShouldProcess\s*=\s*\$true' + } - It "Should use PSCmdlet.ShouldProcess for destructive operations" { - $content = Get-Content $script:Tier3Scripts[$scriptName] -Raw - $content | Should -Match '\$PSCmdlet\.ShouldProcess' - } + It "Should use PSCmdlet.ShouldProcess for destructive operations" { + $content = Get-Content $Path -Raw + $content | Should -Match '\$PSCmdlet\.ShouldProcess' } } }