├── Build ├── build.depend.psd1 ├── build.ps1 ├── build.psake.ps1 └── deploy.psdeploy.ps1 ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Documentation ├── Get-NetworkClass.md ├── Get-Subnet.md └── Test-PrivateIP.md ├── PSScriptAnalyzerSettings.psd1 ├── README.md ├── Subnet ├── Private │ ├── Convert-IPtoInt64.ps1 │ └── Convert-Int64toIP.ps1 ├── Public │ ├── Get-NetworkClass.ps1 │ ├── Get-Subnet.ps1 │ └── Test-PrivateIP.ps1 ├── Subnet.psd1 └── Subnet.psm1 ├── Tests ├── Common │ ├── Help.Tests.ps1 │ ├── Manifest.Tests.ps1 │ └── PSSA.Tests.ps1 ├── Get-NetworkClass.tests.ps1 ├── Get-Subnet.tests.ps1 ├── Subnet.Tests.ps1 └── Test-PrivateIP.tests.ps1 └── azure-pipelines.yml /Build/build.depend.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | # Defaults for all dependencies 3 | PSDependOptions = @{ 4 | Target = 'CurrentUser' 5 | Parameters = @{ 6 | # Use a local repository for offline support 7 | Repository = 'PSGallery' 8 | SkipPublisherCheck = $true 9 | } 10 | } 11 | 12 | # Dependency Management modules 13 | # PackageManagement = '1.2.2' 14 | # PowerShellGet = '2.0.1' 15 | 16 | # Common modules 17 | BuildHelpers = '2.0.1' 18 | Pester = '5.3.0' 19 | PlatyPS = '0.12.0' 20 | psake = '4.7.4' 21 | PSDeploy = '1.0.1' 22 | PSScriptAnalyzer = '1.17.1' 23 | # 'VMware.VimAutomation.Cloud' = '11.0.0.10379994' 24 | } 25 | -------------------------------------------------------------------------------- /Build/build.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | [Parameter()] 4 | [System.String[]] 5 | $TaskList = 'Default', 6 | 7 | [Parameter()] 8 | [System.Collections.Hashtable] 9 | $Parameters, 10 | 11 | [Parameter()] 12 | [System.Collections.Hashtable] 13 | $Properties, 14 | 15 | [Parameter()] 16 | [Switch] 17 | $ResolveDependency 18 | ) 19 | 20 | Write-Output "`nSTARTED TASKS: $($TaskList -join ',')`n" 21 | 22 | Write-Output "`nPowerShell Version Information:" 23 | $PSVersionTable 24 | 25 | # Load dependencies 26 | if ($PSBoundParameters.Keys -contains 'ResolveDependency') { 27 | # Bootstrap environment 28 | Get-PackageProvider -Name 'NuGet' -ForceBootstrap | Out-Null 29 | 30 | # Install PSDepend module if it is not already installed 31 | if (-not (Get-Module -Name 'PSDepend' -ListAvailable)) { 32 | Write-Output "`nPSDepend is not yet installed...installing PSDepend now..." 33 | Install-Module -Name 'PSDepend' -Scope 'CurrentUser' -Force 34 | } else { 35 | Write-Output "`nPSDepend already installed...skipping." 36 | } 37 | 38 | # Install build dependencies 39 | $psdependencyConfigPath = Join-Path -Path $PSScriptRoot -ChildPath 'build.depend.psd1' 40 | Write-Output "Checking / resolving module dependencies from [$psdependencyConfigPath]..." 41 | Import-Module -Name 'PSDepend' 42 | $invokePSDependParams = @{ 43 | Path = $psdependencyConfigPath 44 | # Tags = 'Bootstrap' 45 | Import = $true 46 | Confirm = $false 47 | Install = $true 48 | # Verbose = $true 49 | } 50 | Invoke-PSDepend @invokePSDependParams 51 | 52 | # Remove ResolveDependency PSBoundParameter ready for passthru to PSake 53 | $PSBoundParameters.Remove('ResolveDependency') 54 | } else { 55 | Write-Host "Skipping dependency check...`n" -ForegroundColor 'Yellow' 56 | } 57 | 58 | 59 | # Init BuildHelpers 60 | Set-BuildEnvironment -Force 61 | 62 | 63 | # Execute PSake tasts 64 | $invokePsakeParams = @{ 65 | buildFile = (Join-Path -Path $env:BHProjectPath -ChildPath 'Build\build.psake.ps1') 66 | nologo = $true 67 | } 68 | Invoke-Psake @invokePsakeParams @PSBoundParameters 69 | 70 | Write-Output "`nFINISHED TASKS: $($TaskList -join ',')" 71 | 72 | exit ( [int](-not $psake.build_success) ) 73 | -------------------------------------------------------------------------------- /Build/build.psake.ps1: -------------------------------------------------------------------------------- 1 | # PSake makes variables declared here available in other scriptblocks 2 | Properties { 3 | $ProjectRoot = $ENV:BHProjectPath 4 | 5 | if (-not $ProjectRoot) { 6 | $ProjectRoot = "$PSScriptRoot/.." 7 | } 8 | 9 | $Timestamp = Get-Date -UFormat '%Y%m%d-%H%M%S' 10 | $PSVersion = $PSVersionTable.PSVersion.Major 11 | $lines = '----------------------------------------------------------------------' 12 | 13 | # Pester 14 | $TestScripts = Get-ChildItem "$ProjectRoot\Tests\*.Tests.ps1" -Recurse 15 | $TestFile = "Test-Unit_$($TimeStamp).xml" 16 | 17 | # Script Analyzer 18 | [ValidateSet('Error', 'Warning', 'Any', 'None')] 19 | $ScriptAnalysisFailBuildOnSeverityLevel = 'Error' 20 | $ScriptAnalyzerSettingsPath = "$ProjectRoot\PSScriptAnalyzerSettings.psd1" 21 | 22 | # Build 23 | $ArtifactFolder = Join-Path -Path $ProjectRoot -ChildPath 'Artifacts' 24 | 25 | # Staging 26 | $StagingFolder = Join-Path -Path $ProjectRoot -ChildPath 'Staging' 27 | $StagingModulePath = Join-Path -Path $StagingFolder -ChildPath $env:BHProjectName 28 | $StagingModuleManifestPath = Join-Path -Path $StagingModulePath -ChildPath "$($env:BHProjectName).psd1" 29 | 30 | # Documentation 31 | $DocumentationPath = Join-Path -Path $ProjectRoot -ChildPath 'Documentation' 32 | } 33 | 34 | 35 | # Define top-level tasks 36 | Task 'Default' -Depends 'Test' 37 | 38 | 39 | # Show build variables 40 | Task 'Init' { 41 | $lines 42 | 43 | Set-Location $ProjectRoot 44 | 45 | "Build System Details:" 46 | Get-Item ENV:BH* 47 | "`n" 48 | } 49 | 50 | 51 | # Clean the Artifact and Staging folders 52 | Task 'Clean' -Depends 'Init' { 53 | $lines 54 | 55 | $foldersToClean = @( 56 | $ArtifactFolder 57 | $StagingFolder 58 | ) 59 | 60 | # Remove folders 61 | foreach ($folderPath in $foldersToClean) { 62 | Remove-Item -Path $folderPath -Recurse -Force -ErrorAction 'SilentlyContinue' 63 | New-Item -Path $folderPath -ItemType 'Directory' -Force | Out-String | Write-Verbose 64 | } 65 | } 66 | 67 | 68 | # Create a single .psm1 module file containing all functions 69 | # Copy new module and other supporting files (Documentation / Examples) to Staging folder 70 | Task 'CombineFunctionsAndStage' -Depends 'Clean' { 71 | $lines 72 | 73 | # Create folders 74 | New-Item -Path $StagingFolder -ItemType 'Directory' -Force | Out-String | Write-Verbose 75 | New-Item -Path $StagingModulePath -ItemType 'Directory' -Force | Out-String | Write-Verbose 76 | 77 | # Get public and private function files 78 | $publicFunctions = @( Get-ChildItem -Path "$env:BHModulePath\Public\*.ps1" -Recurse -ErrorAction 'SilentlyContinue' ) 79 | $privateFunctions = @( Get-ChildItem -Path "$env:BHModulePath\Private\*.ps1" -Recurse -ErrorAction 'SilentlyContinue' ) 80 | 81 | # Combine functions into a single .psm1 module 82 | $combinedModulePath = Join-Path -Path $StagingModulePath -ChildPath "$($env:BHProjectName).psm1" 83 | @($publicFunctions + $privateFunctions) | Get-Content | Add-Content -Path $combinedModulePath 84 | 85 | # Copy other required folders and files if they exist 86 | $PathsToCopy = @( 87 | Join-Path -Path $ProjectRoot -ChildPath 'Documentation' 88 | Join-Path -Path $ProjectRoot -ChildPath 'Examples' 89 | Join-Path -Path $ProjectRoot -ChildPath 'CHANGELOG.md' 90 | Join-Path -Path $ProjectRoot -ChildPath 'README.md' 91 | ) 92 | 93 | foreach ($Path in $PathsToCopy) { 94 | if (Test-Path $Path) { 95 | Copy-Item -Path $Path -Destination $StagingFolder -Recurse 96 | } 97 | } 98 | 99 | # Copy existing manifest 100 | Copy-Item -Path $env:BHPSModuleManifest -Destination $StagingModulePath -Recurse 101 | } 102 | 103 | 104 | # Import new module 105 | Task 'ImportStagingModule' -Depends 'Init' { 106 | $lines 107 | Write-Output "Reloading staged module from path: [$StagingModulePath]`n" 108 | 109 | # Reload module 110 | if (Get-Module -Name $env:BHProjectName) { 111 | Remove-Module -Name $env:BHProjectName 112 | } 113 | # Global scope used for UpdateDocumentation (PlatyPS) 114 | Import-Module -Name $StagingModulePath -ErrorAction 'Stop' -Force -Global 115 | } 116 | 117 | 118 | # Run PSScriptAnalyzer against code to ensure quality and best practices are used 119 | Task 'Analyze' -Depends 'ImportStagingModule' { 120 | $lines 121 | Write-Output "Running PSScriptAnalyzer on path: [$StagingModulePath]`n" 122 | 123 | $Results = Invoke-ScriptAnalyzer -Path $StagingModulePath -Recurse -Settings $ScriptAnalyzerSettingsPath -Verbose:$VerbosePreference 124 | $Results | Select-Object 'RuleName', 'Severity', 'ScriptName', 'Line', 'Message' | Format-List 125 | 126 | switch ($ScriptAnalysisFailBuildOnSeverityLevel) { 127 | 'None' { 128 | return 129 | } 130 | 'Error' { 131 | Assert -conditionToCheck ( 132 | ($Results | Where-Object 'Severity' -eq 'Error').Count -eq 0 133 | ) -failureMessage 'One or more ScriptAnalyzer errors were found. Build cannot continue!' 134 | } 135 | 'Warning' { 136 | Assert -conditionToCheck ( 137 | ($Results | Where-Object { 138 | $_.Severity -eq 'Warning' -or $_.Severity -eq 'Error' 139 | }).Count -eq 0) -failureMessage 'One or more ScriptAnalyzer warnings were found. Build cannot continue!' 140 | } 141 | default { 142 | Assert -conditionToCheck ($analysisResult.Count -eq 0) -failureMessage 'One or more ScriptAnalyzer issues were found. Build cannot continue!' 143 | } 144 | } 145 | } 146 | 147 | 148 | # Run Pester tests 149 | # Unit tests: verify inputs / outputs / expected execution path 150 | # Misc tests: verify manifest data, check comment-based help exists 151 | Task 'Test' -Depends 'ImportStagingModule' { 152 | $lines 153 | 154 | # Gather test results. Store them in a variable and file 155 | $CodeFiles = (Get-ChildItem $ENV:BHModulePath -Recurse -Include '*.ps1').FullName 156 | $TestFilePath = Join-Path -Path $ArtifactFolder -ChildPath $TestFile 157 | $TestResults = Invoke-Pester -Script $TestScripts -PassThru -CodeCoverage $CodeFiles -OutputFormat 'NUnitXml' -OutputFile $TestFilePath -PesterOption @{IncludeVSCodeMarker = $true } -ExcludeTag 'Integration' 158 | 159 | # Fail build if any tests fail 160 | if ($TestResults.FailedCount -gt 0) { 161 | Write-Error "Failed '$($TestResults.FailedCount)' tests, build failed" 162 | } 163 | 164 | $CodeCoverage = [Math]::floor($TestResults.CodeCoverage.CoveragePercent) 165 | 166 | #Update readme.md with Code Coverage result 167 | Set-ShieldsIoBadge -Path (Join-Path $ProjectRoot 'README.md') -Subject 'coverage' -Status $CodeCoverage -AsPercentage 168 | } 169 | 170 | 171 | # Create new Documentation markdown files from comment-based help 172 | Task 'UpdateDocumentation' -Depends 'ImportStagingModule' { 173 | $lines 174 | Write-Output "Updating Markdown help in Staging folder: [$DocumentationPath]`n" 175 | 176 | If (Test-Path $DocumentationPath) { 177 | Remove-Item -Path $DocumentationPath -Recurse -Force -ErrorAction 'SilentlyContinue' 178 | Start-Sleep -Seconds 5 179 | } 180 | 181 | # Cleanup 182 | New-Item -Path $DocumentationPath -ItemType 'Directory' | Out-Null 183 | 184 | # Create new Documentation markdown files 185 | $platyPSParams = @{ 186 | Module = $env:BHProjectName 187 | OutputFolder = $DocumentationPath 188 | NoMetadata = $true 189 | } 190 | New-MarkdownHelp @platyPSParams -ErrorAction 'SilentlyContinue' -Verbose | Out-Null 191 | } 192 | 193 | 194 | # Create a versioned zip file of all staged files 195 | # NOTE: Admin Rights are needed if you run this locally 196 | Task 'CreateBuildArtifact' -Depends 'Init' { 197 | $lines 198 | 199 | # Create /Release folder 200 | New-Item -Path $ArtifactFolder -ItemType 'Directory' -Force | Out-String | Write-Verbose 201 | 202 | # Get current manifest version 203 | try { 204 | $manifest = Test-ModuleManifest -Path $StagingModuleManifestPath -ErrorAction 'Stop' 205 | [Version]$manifestVersion = $manifest.Version 206 | 207 | } 208 | catch { 209 | throw "Could not get manifest version from [$StagingModuleManifestPath]" 210 | } 211 | 212 | # Create zip file 213 | try { 214 | $releaseFilename = "$($env:BHProjectName)-v$($manifestVersion.ToString()).zip" 215 | $releasePath = Join-Path -Path $ArtifactFolder -ChildPath $releaseFilename 216 | Write-Host "Creating release artifact [$releasePath] using manifest version [$manifestVersion]" -ForegroundColor 'Yellow' 217 | Compress-Archive -Path "$StagingFolder/*" -DestinationPath $releasePath -Force -Verbose -ErrorAction 'Stop' 218 | } 219 | catch { 220 | throw "Could not create release artifact [$releasePath] using manifest version [$manifestVersion]" 221 | } 222 | 223 | Write-Output "`nFINISHED: Release artifact creation." 224 | } 225 | 226 | Task 'Deploy' -Depends 'Init' { 227 | $lines 228 | 229 | # Load the module, read the exported functions, update the psd1 FunctionsToExport 230 | Set-ModuleFunctions -Name $env:BHPSModuleManifest 231 | 232 | # Bump the module version 233 | try { 234 | $Version = Get-NextPSGalleryVersion -Name $env:BHProjectName -ErrorAction 'Stop' 235 | Update-Metadata -Path $env:BHPSModuleManifest -PropertyName 'ModuleVersion' -Value $Version -ErrorAction 'Stop' 236 | } 237 | catch { 238 | throw "Failed to update version for '$env:BHProjectName': $_.`n" 239 | } 240 | 241 | if (Get-Item "$ProjectRoot/CHANGELOG.md") { 242 | 243 | $ChangeLog = Get-Content "$ProjectRoot/CHANGELOG.md" 244 | 245 | if ($ChangeLog -contains '## !Deploy') { 246 | 247 | $Params = @{ 248 | Path = "$ProjectRoot/Build/deploy.psdeploy.ps1" 249 | Force = $true 250 | Recurse = $false 251 | } 252 | 253 | Invoke-PSDeploy @Verbose @Params 254 | 255 | # Update ChangeLog with deployment version and date 256 | $ChangeLog = $ChangeLog -replace '## !Deploy', "## [$Version] - $(Get-Date -Format 'yyyy-MM-dd')" 257 | Set-Content -Path "$ProjectRoot/CHANGELOG.md" -Value $ChangeLog 258 | } 259 | else { 260 | Write-Host 'CHANGELOG.md did not contain ## !Deploy. Skipping deployment.' 261 | } 262 | } 263 | else { 264 | Write-Host "$ProjectRoot/CHANGELOG.md not found. Skipping deployment." 265 | } 266 | } 267 | 268 | Task 'Commit' -Depends 'Init' { 269 | $lines 270 | 271 | Set-Location $ProjectRoot 272 | $Module = $env:BHProjectName 273 | 274 | git --version 275 | git config --global user.email "build@azuredevops.com" 276 | git config --global user.name "AzureDevOps" 277 | git checkout $env:BUILD_SOURCEBRANCHNAME 278 | git pull 279 | git add Documentation/*.md 280 | git add README.md 281 | git add CHANGELOG.md 282 | git commit -m "[skip ci] AzureDevOps Build $($env:BUILD_BUILDID)" 283 | git push 284 | } 285 | -------------------------------------------------------------------------------- /Build/deploy.psdeploy.ps1: -------------------------------------------------------------------------------- 1 | # Config file for PSDeploy 2 | # Set-BuildEnvironment from BuildHelpers module has populated ENV:BHModulePath and related variables 3 | # Publish to gallery with a few restrictions 4 | if ($StagingModulePath) { 5 | $ModuleSourcePath = $StagingModulePath 6 | } 7 | else { 8 | $ModuleSourcePath = $env:BHPSModulePath 9 | } 10 | 11 | if ( 12 | $ModuleSourcePath -and 13 | $env:BHBuildSystem -ne 'Unknown' -and 14 | $env:BHBranchName -eq "master" -and 15 | $ENV:NugetApiKey 16 | ) { 17 | Deploy Module { 18 | By PSGalleryModule { 19 | FromSource $ModuleSourcePath 20 | To PSGallery 21 | WithOptions @{ 22 | ApiKey = $ENV:NugetApiKey 23 | } 24 | } 25 | } 26 | } else { 27 | "Skipping deployment: To deploy, ensure that...`n" + 28 | "`t* You are in a known build system (Current: $ENV:BHBuildSystem)`n" + 29 | "`t* You are committing to the master branch (Current: $ENV:BHBranchName) `n" + 30 | "`t* You have access to the Nuget API key" | 31 | Write-Host 32 | } 33 | 34 | # Publish to AppVeyor if we're in AppVeyor 35 | if ($env:BHPSModulePath -and $env:BHBuildSystem -eq 'AppVeyor') { 36 | Deploy DeveloperBuild { 37 | By AppVeyorModule { 38 | FromSource $ModuleSourcePath 39 | To AppVeyor 40 | WithOptions @{ 41 | Version = $env:APPVEYOR_BUILD_VERSION 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [1.0.14] - 2019-09-10 4 | 5 | * Test deployment, no changes. 6 | 7 | ## [1.0.12] - 2019-04-15 8 | 9 | * [Fix] The `Test-PrivateIP` cmdlet now returns a correct result when a Private IP is provided via pipeline input. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Guidelines 4 | 5 | Contributions to this module are welcomed. Please consider the following guidance when making a contribution: 6 | 7 | - Create an Issue for any features/bugs/questions and (if you submit a code change) reference this via your Pull Request. 8 | - In general, this repository follows the guidance in the PowerShell Best Practices and Style Guide here: https://github.com/PoshCode/PowerShellPracticeAndStyle. In particular: 9 | - Use full cmdlet names 10 | - Be consistent with the existing code layout style (this is Stroustrup and its the default behaviour of the code auto-formatter for PowerShell in VSCode which I encourage you to use). 11 | - Function names should follow the Verb-Noun structure (even Private functions). 12 | - Where possible please create a Pester test to cover any code changes you make and/or modify existing tests. 13 | - Please ensure you update the comment-based help text for any new parameters you add as well as add new examples where it may be useful. 14 | 15 | ## Deployment 16 | 17 | This module uses Azure Pipelines for CI/CD. When a version of the code is ready for deployment, the following change needs to be committed in order for a new version to be deployed to the PowerShell Gallery: 18 | 19 | - Update [CHANGELOG.md](CHANGELOG.md) to have a new section, titled `## !Deploy` followed by a list of changes for the new version. 20 | 21 | As part of the CI/CD pipeline the CHANGELOG.md file will then be automatically updated with the date and version of the module deployed, replacing the `!Deploy` text. 22 | 23 | Note deployments only occur from the Master branch. 24 | -------------------------------------------------------------------------------- /Documentation/Get-NetworkClass.md: -------------------------------------------------------------------------------- 1 | # Get-NetworkClass 2 | 3 | ## SYNOPSIS 4 | Use to determine the network class of a given IP address. 5 | 6 | ## SYNTAX 7 | 8 | ``` 9 | Get-NetworkClass [-IP] [] 10 | ``` 11 | 12 | ## DESCRIPTION 13 | Returns A, B, C, D or E depending on the numeric value of the first octet of a given IP address. 14 | 15 | ## EXAMPLES 16 | 17 | ### EXAMPLE 1 18 | ``` 19 | Get-NetworkClass -IP 172.16.1.2 20 | ``` 21 | 22 | Result 23 | ------ 24 | B 25 | 26 | ### EXAMPLE 2 27 | ``` 28 | '10.1.1.1' | Get-NetworkClass 29 | ``` 30 | 31 | Result 32 | ------ 33 | A 34 | 35 | ## PARAMETERS 36 | 37 | ### -IP 38 | The IP address to test. 39 | 40 | ```yaml 41 | Type: String 42 | Parameter Sets: (All) 43 | Aliases: 44 | 45 | Required: True 46 | Position: 1 47 | Default value: None 48 | Accept pipeline input: True (ByValue) 49 | Accept wildcard characters: False 50 | ``` 51 | 52 | ### CommonParameters 53 | This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. 54 | For more information, see about_CommonParameters (http://go.microsoft.com/fwlink/?LinkID=113216). 55 | 56 | ## INPUTS 57 | 58 | ## OUTPUTS 59 | 60 | ## NOTES 61 | 62 | ## RELATED LINKS 63 | -------------------------------------------------------------------------------- /Documentation/Get-Subnet.md: -------------------------------------------------------------------------------- 1 | # Get-Subnet 2 | 3 | ## SYNOPSIS 4 | Returns subnet details for the local IP address, or a given network address and mask. 5 | 6 | ## SYNTAX 7 | 8 | ``` 9 | Get-Subnet [[-IP] ] [[-MaskBits] ] [-Force] [] 10 | ``` 11 | 12 | ## DESCRIPTION 13 | Use to get subnet details for a given network address and mask, including network address, broadcast address, network class, address range, host addresses and host address count. 14 | 15 | ## EXAMPLES 16 | 17 | ### EXAMPLE 1 18 | ``` 19 | Get-Subnet 10.1.2.3/24 20 | ``` 21 | 22 | Description 23 | ----------- 24 | Returns the subnet details for the specified network and mask, specified as a single string to the -IP parameter. 25 | 26 | ### EXAMPLE 2 27 | ``` 28 | Get-Subnet 192.168.0.1 -MaskBits 23 29 | ``` 30 | 31 | Description 32 | ----------- 33 | Returns the subnet details for the specified network and mask. 34 | 35 | ### EXAMPLE 3 36 | ``` 37 | Get-Subnet 38 | ``` 39 | 40 | Description 41 | ----------- 42 | Returns the subnet details for the current local IP. 43 | 44 | ### EXAMPLE 4 45 | ``` 46 | '10.1.2.3/24','10.1.2.4/24' | Get-Subnet 47 | ``` 48 | 49 | Description 50 | ----------- 51 | Returns the subnet details for two specified networks. 52 | 53 | ## PARAMETERS 54 | 55 | ### -IP 56 | The network IP address or IP address with subnet mask via slash notation. 57 | 58 | ```yaml 59 | Type: String 60 | Parameter Sets: (All) 61 | Aliases: 62 | 63 | Required: False 64 | Position: 1 65 | Default value: None 66 | Accept pipeline input: True (ByValue) 67 | Accept wildcard characters: False 68 | ``` 69 | 70 | ### -MaskBits 71 | The numerical representation of the subnet mask. 72 | 73 | ```yaml 74 | Type: Int32 75 | Parameter Sets: (All) 76 | Aliases: CIDR 77 | 78 | Required: False 79 | Position: 2 80 | Default value: 0 81 | Accept pipeline input: False 82 | Accept wildcard characters: False 83 | ``` 84 | 85 | ### -Force 86 | Use to force the return of all host IP addresses regardless of the subnet size (skipped by default for subnets larger than /16). 87 | 88 | ```yaml 89 | Type: SwitchParameter 90 | Parameter Sets: (All) 91 | Aliases: 92 | 93 | Required: False 94 | Position: Named 95 | Default value: False 96 | Accept pipeline input: False 97 | Accept wildcard characters: False 98 | ``` 99 | 100 | ### CommonParameters 101 | This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. 102 | For more information, see about_CommonParameters (http://go.microsoft.com/fwlink/?LinkID=113216). 103 | 104 | ## INPUTS 105 | 106 | ## OUTPUTS 107 | 108 | ## NOTES 109 | 110 | ## RELATED LINKS 111 | -------------------------------------------------------------------------------- /Documentation/Test-PrivateIP.md: -------------------------------------------------------------------------------- 1 | # Test-PrivateIP 2 | 3 | ## SYNOPSIS 4 | Use to determine if a given IP address is within the IPv4 private address space ranges. 5 | 6 | ## SYNTAX 7 | 8 | ``` 9 | Test-PrivateIP [-IP] [] 10 | ``` 11 | 12 | ## DESCRIPTION 13 | Returns $true or $false for a given IP address string depending on whether or not is is within the private IP address ranges. 14 | 15 | ## EXAMPLES 16 | 17 | ### EXAMPLE 1 18 | ``` 19 | Test-PrivateIP -IP 172.16.1.2 20 | ``` 21 | 22 | Result 23 | ------ 24 | True 25 | 26 | ## PARAMETERS 27 | 28 | ### -IP 29 | The IP address to test. 30 | 31 | ```yaml 32 | Type: String 33 | Parameter Sets: (All) 34 | Aliases: 35 | 36 | Required: True 37 | Position: 1 38 | Default value: None 39 | Accept pipeline input: True (ByValue) 40 | Accept wildcard characters: False 41 | ``` 42 | 43 | ### CommonParameters 44 | This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. 45 | For more information, see about_CommonParameters (http://go.microsoft.com/fwlink/?LinkID=113216). 46 | 47 | ## INPUTS 48 | 49 | ## OUTPUTS 50 | 51 | ## NOTES 52 | 53 | ## RELATED LINKS 54 | -------------------------------------------------------------------------------- /PSScriptAnalyzerSettings.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | ExcludeRules = @( 3 | 'PSAvoidTrailingWhitespace', 4 | 'PSAvoidGlobalVars' 5 | ) 6 | 7 | Severity = @( 8 | "Warning", 9 | "Error" 10 | ) 11 | 12 | Rules = @{} 13 | } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PowerShell-Subnet 2 | 3 | [![Build Status](https://dev.azure.com/markwragg/GitHub/_apis/build/status/markwragg.PowerShell-Subnet?branchName=master)](https://dev.azure.com/markwragg/GitHub/_build/latest?definitionId=10&branchName=master) ![coverage](https://img.shields.io/badge/coverage-91%25-brightgreen.svg) 4 | 5 | A PowerShell module for cmdlets related to network subnet calculations. 6 | 7 | ## Installation 8 | 9 | The module is published in the PSGallery, so if you have PowerShell 5 can be installed by running: 10 | 11 | ```powershell 12 | Install-Module Subnet -Scope CurrentUser 13 | ``` 14 | 15 | ## Usage 16 | 17 | Get the subnet details for a specified network address and mask using slash notation: 18 | 19 | ```powershell 20 | Get-Subnet 192.168.4.56/24 21 | ``` 22 | 23 | Result: 24 | 25 | ```text 26 | IPAddress : 192.168.4.56 27 | MaskBits : 24 28 | NetworkAddress : 192.168.4.0 29 | BroadcastAddress : 192.168.4.255 30 | SubnetMask : 255.255.255.0 31 | NetworkClass : C 32 | Range : 192.168.4.0 ~ 192.168.4.255 33 | HostAddresses : {192.168.4.1, 192.168.4.2, 192.168.4.3, 192.168.4.4...} 34 | HostAddressCount : 254 35 | ``` 36 | 37 | Get the subnet details for a specified network address and mask, as specified via the `-MaskBits` parameter: 38 | 39 | ```powershell 40 | Get-Subnet -IP 192.168.4.56 -MaskBits 20 41 | ``` 42 | 43 | Result: 44 | 45 | ```text 46 | IPAddress : 192.168.4.56 47 | MaskBits : 20 48 | NetworkAddress : 192.168.0.0 49 | BroadcastAddress : 192.168.15.255 50 | SubnetMask : 255.255.240.0 51 | NetworkClass : C 52 | Range : 192.168.0.0 ~ 192.168.15.255 53 | HostAddresses : {192.168.0.1, 192.168.0.2, 192.168.0.3, 192.168.0.4...} 54 | HostAddressCount : 4094 55 | ``` 56 | 57 | Get the subnet details for the current local network IP: 58 | 59 | ```powershell 60 | Get-Subnet 61 | ``` 62 | 63 | ## Other Features 64 | 65 | - If the subnet size specified is larger than a /16, the cmdlet will not return the host addresses by default, and instead warn that this would take some time. 66 | If you want to force the return of host addresses for these subnets, use `-Force`. 67 | - If no subnet mask size is specified, the cmdlet will use the default size for the class of address, and show a warning that it has done so. 68 | -------------------------------------------------------------------------------- /Subnet/Private/Convert-IPtoInt64.ps1: -------------------------------------------------------------------------------- 1 | function Convert-IPtoInt64 ($ip) { 2 | $octets = $ip.split(".") 3 | [int64]([int64]$octets[0] * 16777216 + [int64]$octets[1] * 65536 + [int64]$octets[2] * 256 + [int64]$octets[3]) 4 | } -------------------------------------------------------------------------------- /Subnet/Private/Convert-Int64toIP.ps1: -------------------------------------------------------------------------------- 1 | function Convert-Int64toIP ([int64]$int) { 2 | (([math]::truncate($int / 16777216)).tostring() + "." + ([math]::truncate(($int % 16777216) / 65536)).tostring() + "." + ([math]::truncate(($int % 65536) / 256)).tostring() + "." + ([math]::truncate($int % 256)).tostring() ) 3 | } -------------------------------------------------------------------------------- /Subnet/Public/Get-NetworkClass.ps1: -------------------------------------------------------------------------------- 1 | function Get-NetworkClass { 2 | <# 3 | .SYNOPSIS 4 | Use to determine the network class of a given IP address. 5 | 6 | .DESCRIPTION 7 | Returns A, B, C, D or E depending on the numeric value of the first octet of a given IP address. 8 | 9 | .PARAMETER IP 10 | The IP address to test. 11 | 12 | .EXAMPLE 13 | Get-NetworkClass -IP 172.16.1.2 14 | 15 | Result 16 | ------ 17 | B 18 | 19 | .EXAMPLE 20 | '10.1.1.1' | Get-NetworkClass 21 | 22 | Result 23 | ------ 24 | A 25 | #> 26 | param( 27 | [parameter(Mandatory,ValueFromPipeline)] 28 | [string] 29 | $IP 30 | ) 31 | process { 32 | 33 | switch ($IP.Split('.')[0]) { 34 | { $_ -in 0..127 } { 'A' } 35 | { $_ -in 128..191 } { 'B' } 36 | { $_ -in 192..223 } { 'C' } 37 | { $_ -in 224..239 } { 'D' } 38 | { $_ -in 240..255 } { 'E' } 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /Subnet/Public/Get-Subnet.ps1: -------------------------------------------------------------------------------- 1 | function Get-Subnet { 2 | <# 3 | .SYNOPSIS 4 | Returns subnet details for the local IP address, or a given network address and mask. 5 | 6 | .DESCRIPTION 7 | Use to get subnet details for a given network address and mask, including network address, broadcast address, network class, address range, host addresses and host address count. 8 | 9 | .PARAMETER IP 10 | The network IP address or IP address with subnet mask via slash notation. 11 | 12 | .PARAMETER MaskBits 13 | The numerical representation of the subnet mask. 14 | 15 | .PARAMETER Force 16 | Use to force the return of all host IP addresses regardless of the subnet size (skipped by default for subnets larger than /16). 17 | 18 | .EXAMPLE 19 | Get-Subnet 10.1.2.3/24 20 | 21 | Description 22 | ----------- 23 | Returns the subnet details for the specified network and mask, specified as a single string to the -IP parameter. 24 | 25 | .EXAMPLE 26 | Get-Subnet 192.168.0.1 -MaskBits 23 27 | 28 | Description 29 | ----------- 30 | Returns the subnet details for the specified network and mask. 31 | 32 | .EXAMPLE 33 | Get-Subnet 34 | 35 | Description 36 | ----------- 37 | Returns the subnet details for the current local IP. 38 | 39 | .EXAMPLE 40 | '10.1.2.3/24','10.1.2.4/24' | Get-Subnet 41 | 42 | Description 43 | ----------- 44 | Returns the subnet details for two specified networks. 45 | #> 46 | param ( 47 | [parameter(ValueFromPipeline)] 48 | [string] 49 | $IP, 50 | 51 | [ValidateRange(0, 32)] 52 | [Alias('CIDR')] 53 | [int] 54 | $MaskBits, 55 | 56 | [switch] 57 | $Force 58 | ) 59 | process { 60 | 61 | if ($PSBoundParameters.ContainsKey('MaskBits')) { 62 | $Mask = $MaskBits 63 | } 64 | 65 | if (-not $IP) { 66 | $LocalIP = (Get-NetIPAddress | Where-Object { $_.AddressFamily -eq 'IPv4' -and $_.PrefixOrigin -ne 'WellKnown' }) 67 | 68 | $IP = $LocalIP.IPAddress 69 | If ($Mask -notin 0..32) { $Mask = $LocalIP.PrefixLength } 70 | } 71 | 72 | if ($IP -match '/\d') { 73 | $IPandMask = $IP -Split '/' 74 | $IP = $IPandMask[0] 75 | $Mask = $IPandMask[1] 76 | } 77 | 78 | $Class = Get-NetworkClass -IP $IP 79 | 80 | if ($Mask -notin 0..32) { 81 | 82 | $Mask = switch ($Class) { 83 | 'A' { 8 } 84 | 'B' { 16 } 85 | 'C' { 24 } 86 | default { 87 | throw "Subnet mask size was not specified and could not be inferred because the address is Class $Class." 88 | } 89 | } 90 | 91 | Write-Warning "Subnet mask size was not specified. Using default subnet size for a Class $Class network of /$Mask." 92 | } 93 | 94 | $IPAddr = [ipaddress]::Parse($IP) 95 | $MaskAddr = [ipaddress]::Parse((Convert-Int64toIP -int ([convert]::ToInt64(("1" * $Mask + "0" * (32 - $Mask)), 2)))) 96 | $NetworkAddr = [ipaddress]($MaskAddr.address -band $IPAddr.address) 97 | $BroadcastAddr = [ipaddress](([ipaddress]::parse("255.255.255.255").address -bxor $MaskAddr.address -bor $NetworkAddr.address)) 98 | $Range = "$NetworkAddr ~ $BroadcastAddr" 99 | 100 | $HostStartAddr = (Convert-IPtoInt64 -ip $NetworkAddr.ipaddresstostring) + 1 101 | $HostEndAddr = (Convert-IPtoInt64 -ip $broadcastaddr.ipaddresstostring) - 1 102 | 103 | if ($Mask -ge 16 -or $Force) { 104 | 105 | Write-Progress "Calcualting host addresses for $NetworkAddr/$Mask.." 106 | if ($Mask -ge 31) { 107 | $HostAddresses = ,$NetworkAddr 108 | if ($Mask -eq 31) { 109 | $HostAddresses += $BroadcastAddr 110 | } 111 | 112 | $HostAddressCount = $HostAddresses.Length 113 | $NetworkAddr = $null 114 | $BroadcastAddr = $null 115 | } else { 116 | $HostAddresses = for ($i = $HostStartAddr; $i -le $HostEndAddr; $i++) { 117 | Convert-Int64toIP -int $i 118 | } 119 | $HostAddressCount = ($HostEndAddr - $HostStartAddr) + 1 120 | } 121 | } 122 | else { 123 | Write-Warning "Host address enumeration was not performed because it would take some time for a /$Mask subnet. `nUse -Force if you want it to occur." 124 | } 125 | 126 | [pscustomobject]@{ 127 | IPAddress = $IPAddr 128 | MaskBits = $Mask 129 | NetworkAddress = $NetworkAddr 130 | BroadcastAddress = $broadcastaddr 131 | SubnetMask = $MaskAddr 132 | NetworkClass = $Class 133 | Range = $Range 134 | HostAddresses = $HostAddresses 135 | HostAddressCount = $HostAddressCount 136 | } 137 | } 138 | } -------------------------------------------------------------------------------- /Subnet/Public/Test-PrivateIP.ps1: -------------------------------------------------------------------------------- 1 | function Test-PrivateIP { 2 | <# 3 | .SYNOPSIS 4 | Use to determine if a given IP address is within the IPv4 private address space ranges. 5 | 6 | .DESCRIPTION 7 | Returns $true or $false for a given IP address string depending on whether or not is is within the private IP address ranges. 8 | 9 | .PARAMETER IP 10 | The IP address to test. 11 | 12 | .EXAMPLE 13 | Test-PrivateIP -IP 172.16.1.2 14 | 15 | Result 16 | ------ 17 | True 18 | #> 19 | param( 20 | [parameter(Mandatory,ValueFromPipeline)] 21 | [string] 22 | $IP 23 | ) 24 | process { 25 | 26 | if ($IP -Match '(^127\.)|(^192\.168\.)|(^10\.)|(^172\.1[6-9]\.)|(^172\.2[0-9]\.)|(^172\.3[0-1]\.)') { 27 | $true 28 | } 29 | else { 30 | $false 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /Subnet/Subnet.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'Subnet' 3 | # 4 | # Generated by: Mark Wragg 5 | # 6 | # Generated on: 9/7/2019 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | RootModule = 'Subnet.psm1' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '1.0.14' 16 | 17 | # Supported PSEditions 18 | # CompatiblePSEditions = @() 19 | 20 | # ID used to uniquely identify this module 21 | GUID = 'b62855ce-0076-4326-ba0d-ec45a512a623' 22 | 23 | # Author of this module 24 | Author = 'Mark Wragg' 25 | 26 | # Company or vendor of this module 27 | CompanyName = 'wragg.io' 28 | 29 | # Copyright statement for this module 30 | Copyright = '(c) 2019 Mark Wragg. All rights reserved.' 31 | 32 | # Description of the functionality provided by this module 33 | Description = 'A PowerShell module for cmdlets related to network subnet calculations' 34 | 35 | # Minimum version of the Windows PowerShell engine required by this module 36 | PowerShellVersion = '3.0' 37 | 38 | # Name of the Windows PowerShell host required by this module 39 | # PowerShellHostName = '' 40 | 41 | # Minimum version of the Windows PowerShell host required by this module 42 | # PowerShellHostVersion = '' 43 | 44 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 45 | # DotNetFrameworkVersion = '' 46 | 47 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 48 | # CLRVersion = '' 49 | 50 | # Processor architecture (None, X86, Amd64) required by this module 51 | # ProcessorArchitecture = '' 52 | 53 | # Modules that must be imported into the global environment prior to importing this module 54 | # RequiredModules = @() 55 | 56 | # Assemblies that must be loaded prior to importing this module 57 | # RequiredAssemblies = @() 58 | 59 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 60 | # ScriptsToProcess = @() 61 | 62 | # Type files (.ps1xml) to be loaded when importing this module 63 | # TypesToProcess = @() 64 | 65 | # Format files (.ps1xml) to be loaded when importing this module 66 | # FormatsToProcess = @() 67 | 68 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 69 | # NestedModules = @() 70 | 71 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 72 | FunctionsToExport = 'Get-NetworkClass', 'Get-Subnet', 'Test-PrivateIP' 73 | 74 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 75 | CmdletsToExport = @() 76 | 77 | # Variables to export from this module 78 | VariablesToExport = '*' 79 | 80 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 81 | AliasesToExport = @() 82 | 83 | # DSC resources to export from this module 84 | # DscResourcesToExport = @() 85 | 86 | # List of all modules packaged with this module 87 | # ModuleList = @() 88 | 89 | # List of all files packaged with this module 90 | # FileList = @() 91 | 92 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 93 | PrivateData = @{ 94 | 95 | PSData = @{ 96 | 97 | # Tags applied to this module. These help with module discovery in online galleries. 98 | Tags = 'Subnet','Subnet-calculator','Networking' 99 | 100 | # A URL to the license for this module. 101 | # LicenseUri = '' 102 | 103 | # A URL to the main website for this project. 104 | ProjectUri = 'https://github.com/markwragg/PowerShell-Subnet' 105 | 106 | # A URL to an icon representing this module. 107 | # IconUri = '' 108 | 109 | # ReleaseNotes of this module 110 | ReleaseNotes = 'https://github.com/markwragg/PowerShell-Subnet/blob/master/README.md' 111 | 112 | # Prerelease string of this module 113 | # Prerelease = '' 114 | 115 | # Flag to indicate whether the module requires explicit user acceptance for install/update 116 | # RequireLicenseAcceptance = $false 117 | 118 | # External dependent modules of this module 119 | # ExternalModuleDependencies = @() 120 | 121 | } # End of PSData hashtable 122 | 123 | } # End of PrivateData hashtable 124 | 125 | # HelpInfo URI of this module 126 | # HelpInfoURI = '' 127 | 128 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 129 | # DefaultCommandPrefix = '' 130 | 131 | } 132 | 133 | -------------------------------------------------------------------------------- /Subnet/Subnet.psm1: -------------------------------------------------------------------------------- 1 | $Public = @( Get-ChildItem -Path "$PSScriptRoot\Public\*.ps1" -Recurse ) 2 | $Private = @( Get-ChildItem -Path "$PSScriptRoot\Private\*.ps1" -Recurse ) 3 | 4 | @($Public + $Private) | ForEach-Object { 5 | Try { 6 | . $_.FullName 7 | } 8 | Catch { 9 | Write-Error -Message "Failed to import function $($_.FullName): $_" 10 | } 11 | } 12 | 13 | Export-ModuleMember -Function $Public.BaseName -------------------------------------------------------------------------------- /Tests/Common/Help.Tests.ps1: -------------------------------------------------------------------------------- 1 | # Taken with love from @juneb_get_help (https://raw.githubusercontent.com/juneb/PesterTDD/master/Module.Help.Tests.ps1) 2 | 3 | BeforeDiscovery { 4 | function global:FilterOutCommonParams { 5 | param ($Params) 6 | $commonParams = @( 7 | 'Debug', 'ErrorAction', 'ErrorVariable', 'InformationAction', 'InformationVariable', 8 | 'OutBuffer', 'OutVariable', 'PipelineVariable', 'Verbose', 'WarningAction', 9 | 'WarningVariable', 'Confirm', 'Whatif' 10 | ) 11 | $params | Where-Object { $_.Name -notin $commonParams } | Sort-Object -Property Name -Unique 12 | } 13 | 14 | # Get module commands 15 | # Remove all versions of the module from the session. Pester can't handle multiple versions. 16 | Get-Module $env:BHProjectName | Remove-Module -Force -ErrorAction Ignore 17 | Import-Module -Name $PSScriptRoot\..\..\$env:BHProjectName -Verbose:$false -ErrorAction Stop 18 | $params = @{ 19 | Module = (Get-Module $env:BHProjectName) 20 | CommandType = [System.Management.Automation.CommandTypes[]]'Cmdlet, Function' # Not alias 21 | } 22 | if ($PSVersionTable.PSVersion.Major -lt 6) { 23 | $params.CommandType[0] += 'Workflow' 24 | } 25 | $commands = Get-Command @params 26 | 27 | ## When testing help, remember that help is cached at the beginning of each session. 28 | ## To test, restart session. 29 | } 30 | 31 | AfterAll { 32 | Remove-Item Function:/FilterOutCommonParams 33 | Get-Module $env:BHProjectName | Remove-Module -Force -ErrorAction Ignore 34 | } 35 | 36 | Describe "Test help for <_.Name>" -ForEach $commands { 37 | 38 | BeforeDiscovery { 39 | # Get command help, parameters, and links 40 | $command = $_ 41 | $commandHelp = Get-Help $command.Name -ErrorAction SilentlyContinue 42 | $commandParameters = global:FilterOutCommonParams -Params $command.ParameterSets.Parameters 43 | $commandParameterNames = $commandParameters.Name 44 | $helpLinks = $commandHelp.relatedLinks.navigationLink.uri 45 | } 46 | 47 | BeforeAll { 48 | # These vars are needed in both discovery and test phases so we need to duplicate them here 49 | $command = $_ 50 | $commandName = $_.Name 51 | $commandHelp = Get-Help $command.Name -ErrorAction SilentlyContinue 52 | $commandParameters = global:FilterOutCommonParams -Params $command.ParameterSets.Parameters 53 | $commandParameterNames = $commandParameters.Name 54 | $helpParameters = global:FilterOutCommonParams -Params $commandHelp.Parameters.Parameter 55 | $helpParameterNames = $helpParameters.Name 56 | } 57 | 58 | # If help is not found, synopsis in auto-generated help is the syntax diagram 59 | It 'Help is not auto-generated' { 60 | $commandHelp.Synopsis | Should -Not -BeLike '*`[``]*' 61 | } 62 | 63 | # Should be a description for every function 64 | It "Has description" { 65 | $commandHelp.Description | Should -Not -BeNullOrEmpty 66 | } 67 | 68 | # Should be at least one example 69 | It "Has example code" { 70 | ($commandHelp.Examples.Example | Select-Object -First 1).Code | Should -Not -BeNullOrEmpty 71 | } 72 | 73 | # Should be at least one example description 74 | It "Has example help" { 75 | ($commandHelp.Examples.Example.Remarks | Select-Object -First 1).Text | Should -Not -BeNullOrEmpty 76 | } 77 | 78 | It "Help link <_> is valid" -ForEach $helpLinks { 79 | (Invoke-WebRequest -Uri $_ -UseBasicParsing).StatusCode | Should -Be '200' 80 | } 81 | 82 | Context "Parameter <_.Name>" -Foreach $commandParameters { 83 | 84 | BeforeAll { 85 | $parameter = $_ 86 | $parameterName = $parameter.Name 87 | $parameterHelp = $commandHelp.parameters.parameter | Where-Object Name -eq $parameterName 88 | $parameterHelpType = if ($parameterHelp.ParameterValue) { $parameterHelp.ParameterValue.Trim() } 89 | } 90 | 91 | # Should be a description for every parameter 92 | It "Has description" { 93 | $parameterHelp.Description.Text | Should -Not -BeNullOrEmpty 94 | } 95 | 96 | # Required value in Help should match IsMandatory property of parameter 97 | It "Has correct [mandatory] value" { 98 | $codeMandatory = $_.IsMandatory.toString() 99 | $parameterHelp.Required | Should -Be $codeMandatory 100 | } 101 | 102 | # Parameter type in help should match code 103 | It "Has correct parameter type" { 104 | $parameterHelpType | Should -Be $parameter.ParameterType.Name 105 | } 106 | } 107 | 108 | Context "Test <_> help parameter help for " -Foreach $helpParameterNames { 109 | 110 | # Shouldn't find extra parameters in help. 111 | It "finds help parameter in code: <_>" { 112 | $_ -in $parameterNames | Should -Be $true 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /Tests/Common/Manifest.Tests.ps1: -------------------------------------------------------------------------------- 1 | BeforeAll { 2 | $moduleName = $env:BHProjectName 3 | $manifest = Import-PowerShellDataFile -Path $env:BHPSModuleManifest 4 | $outputManifestPath = Join-Path -Path $PSScriptRoot\..\..\$env:BHProjectName -Child "$($moduleName).psd1" 5 | $manifestData = Test-ModuleManifest -Path $outputManifestPath -Verbose:$false -ErrorAction Stop -WarningAction SilentlyContinue 6 | 7 | $changelogPath = Join-Path -Path $env:BHProjectPath -Child 'CHANGELOG.md' 8 | $changelogVersion = Get-Content $changelogPath | ForEach-Object { 9 | if ($_ -match "^##\s\[(?(\d+\.){1,3}\d+)\]") { 10 | $changelogVersion = $matches.Version 11 | break 12 | } 13 | } 14 | 15 | $script:manifest = $null 16 | } 17 | 18 | Describe 'Module manifest' { 19 | 20 | Context 'Validation' { 21 | 22 | It 'Has a valid manifest' { 23 | $manifestData | Should -Not -BeNullOrEmpty 24 | } 25 | 26 | It 'Has a valid name in the manifest' { 27 | $manifestData.Name | Should -Be $moduleName 28 | } 29 | 30 | It 'Has a valid root module' { 31 | $manifestData.RootModule | Should -Be "$($moduleName).psm1" 32 | } 33 | 34 | It 'Has a valid version in the manifest' { 35 | $manifestData.Version -as [Version] | Should -Not -BeNullOrEmpty 36 | } 37 | 38 | It 'Has a valid description' { 39 | $manifestData.Description | Should -Not -BeNullOrEmpty 40 | } 41 | 42 | It 'Has a valid author' { 43 | $manifestData.Author | Should -Not -BeNullOrEmpty 44 | } 45 | 46 | It 'Has a valid guid' { 47 | {[guid]::Parse($manifestData.Guid)} | Should -Not -Throw 48 | } 49 | 50 | It 'Has a valid copyright' { 51 | $manifestData.CopyRight | Should -Not -BeNullOrEmpty 52 | } 53 | 54 | It 'Has a valid version in the changelog' { 55 | $changelogVersion | Should -Not -BeNullOrEmpty 56 | $changelogVersion -as [Version] | Should -Not -BeNullOrEmpty 57 | } 58 | 59 | It 'Changelog and manifest versions are the same' { 60 | $changelogVersion -as [Version] | Should -Be ( $manifestData.Version -as [Version] ) 61 | } 62 | } 63 | } 64 | 65 | Describe 'Git tagging' -Skip { 66 | 67 | BeforeAll { 68 | $gitTagVersion = $null 69 | 70 | if ($git = Get-Command git -CommandType Application -ErrorAction SilentlyContinue) { 71 | $thisCommit = & $git log --decorate --oneline HEAD~1..HEAD 72 | if ($thisCommit -match 'tag:\s*(\d+(?:\.\d+)*)') { $gitTagVersion = $matches[1] } 73 | } 74 | } 75 | 76 | It 'Is tagged with a valid version' { 77 | $gitTagVersion | Should -Not -BeNullOrEmpty 78 | $gitTagVersion -as [Version] | Should -Not -BeNullOrEmpty 79 | } 80 | 81 | It 'Matches manifest version' { 82 | $manifestData.Version -as [Version] | Should -Be ( $gitTagVersion -as [Version]) 83 | } 84 | } -------------------------------------------------------------------------------- /Tests/Common/PSSA.Tests.ps1: -------------------------------------------------------------------------------- 1 | # This runs all PSScriptAnalyzer rules as Pester tests to enable visibility when publishing test results 2 | 3 | Describe 'Testing against PSSA rules' { 4 | 5 | Context 'PSSA Standard Rules' { 6 | 7 | BeforeAll { 8 | $ScriptAnalyzerSettingsPath = Join-Path -Path $env:BHProjectPath -ChildPath 'PSScriptAnalyzerSettings.psd1' 9 | $analysis = Invoke-ScriptAnalyzer -Path $env:BHModulePath -Recurse -Settings $ScriptAnalyzerSettingsPath 10 | } 11 | 12 | $scriptAnalyzerRules = Get-ScriptAnalyzerRule 13 | 14 | It "Should pass <_>" -TestCases $scriptAnalyzerRules { 15 | $rule = $_ 16 | If ($analysis.RuleName -contains $rule) { 17 | $analysis | Where-Object RuleName -EQ $rule -OutVariable 'failures' | Out-Default 18 | $failures.Count | Should -Be 0 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/Get-NetworkClass.tests.ps1: -------------------------------------------------------------------------------- 1 | Describe "Get-NetworkClass PS$PSVersion" { 2 | 3 | BeforeAll { 4 | Import-Module $PSScriptRoot\..\Subnet 5 | 6 | Mock Write-Warning {} -ModuleName Subnet 7 | Mock Write-Progress {} -ModuleName Subnet 8 | } 9 | 10 | Context 'Class A IPs' { 11 | 12 | $ClassAIPs = '0.0.0.0', '1.1.1.1', '127.0.0.0' 13 | 14 | It "Should return A for <_>" -TestCases $ClassAIPs { 15 | Get-NetworkClass -IP $_ | Should -Be 'A' 16 | } 17 | } 18 | 19 | Context 'Class B IPs' { 20 | 21 | $ClassBIPs = '128.0.0.0', '175.255.0.0', '191.255.0.0' 22 | 23 | It "Should return B for <_>" -TestCases $ClassBIPs { 24 | Get-NetworkClass -IP $_ | Should -Be 'B' 25 | } 26 | } 27 | 28 | Context 'Class C IPs' { 29 | 30 | $ClassCIPs = '192.0.0.0', '200.255.255.0', '223.255.255.0' 31 | 32 | It "Should return C for <_>" -TestCases $ClassCIPs { 33 | Get-NetworkClass -IP $_ | Should -Be 'C' 34 | } 35 | } 36 | 37 | Context 'Class D IPs' { 38 | 39 | $ClassDIPs = '224.0.0.0', '235.0.0.0', '239.255.255.255' 40 | 41 | It "Should return D for <_>" -TestCases $ClassDIPs { 42 | Get-NetworkClass -IP $_ | Should -Be 'D' 43 | } 44 | } 45 | 46 | Context 'Class E IPs' { 47 | 48 | $ClassEIPs = '240.0.0.0', '245.0.0.0', '255.255.255.255' 49 | 50 | It "Should return E for <_>" -TestCases $ClassEIPs { 51 | Get-NetworkClass -IP $_ | Should -Be 'E' 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /Tests/Get-Subnet.tests.ps1: -------------------------------------------------------------------------------- 1 | Describe "Get-Subnet PS$PSVersion" { 2 | 3 | BeforeAll { 4 | Import-Module $PSScriptRoot\..\Subnet 5 | 6 | Mock Write-Warning {} -ModuleName Subnet 7 | Mock Write-Progress {} -ModuleName Subnet 8 | } 9 | 10 | It 'Should calculate a Subnet IP with mask' { 11 | 12 | $Result = Get-Subnet -IP 1.2.3.4/24 13 | 14 | $Result | Should -BeOfType [pscustomobject] 15 | $Result.IPAddress | Should -Be '1.2.3.4' 16 | $Result.MaskBits | Should -Be 24 17 | $Result.NetworkAddress | Should -Be '1.2.3.0' 18 | $Result.BroadcastAddress | Should -Be '1.2.3.255' 19 | $Result.NetworkClass | Should -Be 'A' 20 | $Result.Range | Should -Be '1.2.3.0 ~ 1.2.3.255' 21 | $Result.HostAddresses | Should -HaveCount 254 22 | } 23 | 24 | It 'Should calculate a Subnet IP with mask declared separately' { 25 | 26 | $Result = Get-Subnet -IP 1.2.3.4 -Mask 24 27 | 28 | $Result | Should -BeOfType [pscustomobject] 29 | $Result.IPAddress | Should -Be '1.2.3.4' 30 | $Result.MaskBits | Should -Be 24 31 | $Result.NetworkAddress | Should -Be '1.2.3.0' 32 | $Result.BroadcastAddress | Should -Be '1.2.3.255' 33 | $Result.NetworkClass | Should -Be 'A' 34 | $Result.Range | Should -Be '1.2.3.0 ~ 1.2.3.255' 35 | $Result.HostAddresses | Should -HaveCount 254 36 | } 37 | 38 | It 'Should calculate a Subnet IP for a /31' { 39 | 40 | $Result = Get-Subnet -IP 1.2.3.4/31 41 | 42 | $Result | Should -BeOfType [pscustomobject] 43 | $Result.IPAddress | Should -Be '1.2.3.4' 44 | $Result.MaskBits | Should -Be 31 45 | $Result.NetworkAddress | Should -Be $null 46 | $Result.BroadcastAddress | Should -Be $null 47 | $Result.NetworkClass | Should -Be 'A' 48 | $Result.Range | Should -Be '1.2.3.4 ~ 1.2.3.5' 49 | $Result.HostAddresses | Should -HaveCount 2 50 | } 51 | 52 | It 'Should calculate a Subnet IP for a /32' { 53 | 54 | $Result = Get-Subnet -IP 1.2.3.4/32 55 | 56 | $Result | Should -BeOfType [pscustomobject] 57 | $Result.IPAddress | Should -Be '1.2.3.4' 58 | $Result.MaskBits | Should -Be 32 59 | $Result.NetworkAddress | Should -Be $null 60 | $Result.BroadcastAddress | Should -Be $null 61 | $Result.NetworkClass | Should -Be 'A' 62 | $Result.Range | Should -Be '1.2.3.4 ~ 1.2.3.4' 63 | $Result.HostAddresses | Should -HaveCount 1 64 | } 65 | 66 | #skipped for ci/cd 67 | It 'Should calculate the Subnet of the local NIC IP' -Skip { 68 | 69 | $Result = Get-Subnet 70 | 71 | $Result | Should -BeOfType [pscustomobject] 72 | $Result.IPAddress | Should -Not -Be $null 73 | $Result.MaskBits | Should -Not -Be $null 74 | $Result.NetworkAddress | Should -Not -Be $null 75 | $Result.BroadcastAddress | Should -Not -Be $null 76 | $Result.NetworkClass | Should -Not -Be $null 77 | $Result.Range | Should -Not -Be $null 78 | } 79 | 80 | Context 'CIDR to Subnet conversions' { 81 | $TestCases = @( 82 | @{'CIDR' = 0; 'Subnet' = '0.0.0.0' } 83 | @{'CIDR' = 1; 'Subnet' = '128.0.0.0' } 84 | @{'CIDR' = 2; 'Subnet' = '192.0.0.0' } 85 | @{'CIDR' = 3; 'Subnet' = '224.0.0.0' } 86 | @{'CIDR' = 4; 'Subnet' = '240.0.0.0' } 87 | @{'CIDR' = 5; 'Subnet' = '248.0.0.0' } 88 | @{'CIDR' = 6; 'Subnet' = '252.0.0.0' } 89 | @{'CIDR' = 7; 'Subnet' = '254.0.0.0' } 90 | @{'CIDR' = 8; 'Subnet' = '255.0.0.0' } 91 | @{'CIDR' = 9; 'Subnet' = '255.128.0.0' } 92 | @{'CIDR' = 10; 'Subnet' = '255.192.0.0' } 93 | @{'CIDR' = 11; 'Subnet' = '255.224.0.0' } 94 | @{'CIDR' = 12; 'Subnet' = '255.240.0.0' } 95 | @{'CIDR' = 13; 'Subnet' = '255.248.0.0' } 96 | @{'CIDR' = 14; 'Subnet' = '255.252.0.0' } 97 | @{'CIDR' = 15; 'Subnet' = '255.254.0.0' } 98 | @{'CIDR' = 16; 'Subnet' = '255.255.0.0' } 99 | @{'CIDR' = 17; 'Subnet' = '255.255.128.0' } 100 | @{'CIDR' = 18; 'Subnet' = '255.255.192.0' } 101 | @{'CIDR' = 19; 'Subnet' = '255.255.224.0' } 102 | @{'CIDR' = 20; 'Subnet' = '255.255.240.0' } 103 | @{'CIDR' = 21; 'Subnet' = '255.255.248.0' } 104 | @{'CIDR' = 22; 'Subnet' = '255.255.252.0' } 105 | @{'CIDR' = 23; 'Subnet' = '255.255.254.0' } 106 | @{'CIDR' = 24; 'Subnet' = '255.255.255.0' } 107 | @{'CIDR' = 25; 'Subnet' = '255.255.255.128' } 108 | @{'CIDR' = 26; 'Subnet' = '255.255.255.192' } 109 | @{'CIDR' = 27; 'Subnet' = '255.255.255.224' } 110 | @{'CIDR' = 28; 'Subnet' = '255.255.255.240' } 111 | @{'CIDR' = 29; 'Subnet' = '255.255.255.248' } 112 | @{'CIDR' = 30; 'Subnet' = '255.255.255.252' } 113 | @{'CIDR' = 31; 'Subnet' = '255.255.255.254' } 114 | @{'CIDR' = 32; 'Subnet' = '255.255.255.255' } 115 | ) 116 | 117 | It "Should convert / to " -TestCases $TestCases { 118 | (Get-Subnet -IP 10.1.2.3 -MaskBits $CIDR).SubnetMask | Should -BeExactly $Subnet 119 | } 120 | } 121 | 122 | Context 'Network class identification' { 123 | $TestCases = @( 124 | @{'IP' = '0.1.2.3'; 'Class' = 'A'; 'SubnetMask' = '255.0.0.0' } 125 | @{'IP' = '128.1.2.3'; 'Class' = 'B'; 'SubnetMask' = '255.255.0.0' } 126 | @{'IP' = '192.1.2.3'; 'Class' = 'C'; 'SubnetMask' = '255.255.255.0' } 127 | ) 128 | 129 | It "Should identify as with Mask " -TestCases $TestCases { 130 | param($IP, $Class, $SubnetMask) 131 | 132 | $Result = (Get-Subnet -IP $IP) 133 | 134 | $Result.NetworkClass | Should -Be $Class 135 | $Result.SubnetMask | Should -Be $SubnetMask 136 | } 137 | 138 | $TestCases = @( 139 | @{'IP' = '224.1.2.3'; 'Class' = 'D' } 140 | @{'IP' = '240.1.2.3'; 'Class' = 'E' } 141 | ) 142 | 143 | It "Should identify as " -TestCases $TestCases { 144 | param($IP, $Class) 145 | 146 | $Result = (Get-Subnet -IP $IP -MaskBits 24) 147 | 148 | $Result.NetworkClass | Should -Be $Class 149 | } 150 | } 151 | 152 | Context 'Invalid IP' { 153 | 154 | It "Should throw for an invalid IP" { 155 | { Get-Subnet -IP 300.1.2.3 } | Should -Throw 156 | } 157 | } 158 | } -------------------------------------------------------------------------------- /Tests/Subnet.Tests.ps1: -------------------------------------------------------------------------------- 1 | Describe "Subnet Module Tests PS$PSVersion" { 2 | 3 | BeforeAll { 4 | if (-not $PSScriptRoot) { $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent } 5 | 6 | $PSVersion = $PSVersionTable.PSVersion.Major 7 | $Root = "$PSScriptRoot\.." 8 | $Module = 'Subnet' 9 | 10 | Get-Module $Module | Remove-Module -Force 11 | } 12 | 13 | It "Should import $Module without errors" { 14 | { Import-Module (Join-Path $Root $Module) -Force -ErrorAction Stop } | Should -Not -Throw 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Tests/Test-PrivateIP.tests.ps1: -------------------------------------------------------------------------------- 1 | Describe "Test-PrivateIP PS$PSVersion" { 2 | 3 | BeforeAll { 4 | Import-Module $PSScriptRoot\..\Subnet 5 | } 6 | 7 | $PrivateIPs = '10.0.0.0', '10.255.255.255', '172.16.0.0', '172.31.255.255', '192.168.0.0', '192.168.255.255' 8 | 9 | Context 'Private IPs' { 10 | 11 | It "Should return $true for <_>" -TestCases $PrivateIPs { 12 | InModuleScope Subnet -Parameters @{ IP = $_ } { 13 | Test-PrivateIP -IP $IP | Should -Be $true 14 | } 15 | } 16 | } 17 | 18 | Context 'Private IPs with pipeline input' { 19 | 20 | It "Should return $true for <_>" -TestCases $PrivateIPs { 21 | InModuleScope Subnet -Parameters @{ IP = $_ } { 22 | $IP | Test-PrivateIP | Should -Be $true 23 | } 24 | } 25 | } 26 | 27 | 28 | $PublicIPs = '9.255.255.255', '11.0.0.0', '172.15.255.255', '172.32.0.0', '192.167.255.255', '192.169.0.0' 29 | 30 | Context 'Public IPs' { 31 | 32 | 33 | It "Should return $false for <_>" -TestCases $PublicIPs { 34 | InModuleScope Subnet -Parameters @{ IP = $_ } { 35 | Test-PrivateIP -IP $IP | Should -Be $false 36 | } 37 | } 38 | } 39 | 40 | Context 'Public IPs with pipeline input' { 41 | 42 | It "Should return $false for <_>" -TestCases $PublicIPs { 43 | InModuleScope Subnet -Parameters @{ IP = $_ } { 44 | $IP | Test-PrivateIP | Should -Be $false 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | variables: 2 | - group: powershell-gallery 3 | 4 | # Build Pipeline 5 | pool: 6 | # What environment will the build agent run on? (Windows / Linux / macOS) 7 | vmImage: "windows-latest" 8 | 9 | trigger: 10 | batch: true 11 | 12 | # What branches will trigger a build? 13 | branches: 14 | include: 15 | # Any Pull Request merging into the master branch 16 | - master 17 | 18 | steps: 19 | - checkout: self 20 | persistCredentials: true 21 | clean: true 22 | 23 | - powershell: | 24 | .\Build\build.ps1 -ResolveDependency -TaskList 'Init' 25 | displayName: "Install Dependencies" 26 | 27 | - powershell: | 28 | .\Build\build.ps1 -TaskList 'CombineFunctionsAndStage' 29 | displayName: "Combine PowerShell functions into single module file" 30 | 31 | - powershell: | 32 | .\Build\build.ps1 -TaskList 'Analyze' 33 | displayName: "Analyze" 34 | 35 | - powershell: | 36 | .\Build\build.ps1 -TaskList 'Test' 37 | displayName: "Test" 38 | 39 | - powershell: | 40 | .\Build\build.ps1 -TaskList 'UpdateDocumentation' 41 | displayName: "Update Documentation" 42 | 43 | - powershell: | 44 | .\Build\build.ps1 -TaskList 'Deploy' 45 | displayName: "Deploy" 46 | 47 | - powershell: | 48 | .\Build\build.ps1 -TaskList 'Commit' 49 | displayName: "Commit Changes" 50 | 51 | - powershell: | 52 | .\Build\build.ps1 -TaskList 'CreateBuildArtifact' 53 | displayName: "Create Build Artifact" 54 | 55 | - task: PublishTestResults@2 56 | displayName: "Publish Pester Tests" 57 | inputs: 58 | testRunner: "NUnit" 59 | searchFolder: "Artifacts" 60 | testRunTitle: "PesterTests" 61 | condition: always() 62 | 63 | - task: PublishBuildArtifacts@1 64 | displayName: "Publish Artifact: PowerShell Module Zipped for offline use" 65 | inputs: 66 | PathtoPublish: Artifacts 67 | ArtifactName: Artifacts 68 | condition: always() 69 | 70 | - task: PublishBuildArtifacts@1 71 | displayName: "Publish Artifact: PowerShell Module" 72 | inputs: 73 | PathtoPublish: Staging 74 | ArtifactName: PSModule 75 | condition: always() 76 | --------------------------------------------------------------------------------