From 14b7ab2c1174c58e46f26affeda073394680ab75 Mon Sep 17 00:00:00 2001 From: Adam Haynes Date: Wed, 22 Aug 2018 09:31:30 +0200 Subject: [PATCH] Adding test and release Utilities (#84) * added test module * added tools * Moved files * updated paths * updated paths * added test data * fixed paths * rem old tests * added ps 6 return * fixed root path * rem local file * rem old files * fixed path * added PS core check --- .gitignore | 4 +- Tests/Integration/.tests.header.ps1 | 12 +- .../DSCResources/.tests.header.ps1 | 2 +- .../PowerStig.Integration.tests.ps1 | 8 +- Tests/Unit/.tests.header.ps1 | 13 + Tests/Unit/DSCResources/.tests.header.ps1 | 2 +- Tests/Unit/DSCResources/Composite.tests.ps1 | 2 +- Tests/Unit/Module/.tests.header.ps1 | 10 +- Tests/Unit/Tools/Release.Tests.ps1 | 746 +++++++++++ Tests/Unit/Tools/TestHelper.tests.ps1 | 13 + Tests/Unit/Tools/WikiPages.tests.ps1 | 58 + Tests/helper.psm1 | 195 --- Tools/AppVeyor/AppVeyor.psm1 | 274 ++++ Tools/Release/Release.psm1 | 1129 +++++++++++++++++ .../TestHelper}/CompositeResourceFilter.psd1 | 0 Tools/TestHelper/Data/readme.md | 51 + .../Data/sampleXccdf.xml.readme.txt | 46 + Tools/TestHelper/Data/sampleXccdf.xml.txt | 23 + Tools/TestHelper/Data/samplegroup.xml.txt | 25 + Tools/TestHelper/TestHelper.psm1 | 449 +++++++ Tools/WikiPages/WikiPages.psm1 | 626 +++++++++ appveyor.yml | 7 +- 22 files changed, 3466 insertions(+), 229 deletions(-) create mode 100644 Tests/Unit/.tests.header.ps1 create mode 100644 Tests/Unit/Tools/Release.Tests.ps1 create mode 100644 Tests/Unit/Tools/TestHelper.tests.ps1 create mode 100644 Tests/Unit/Tools/WikiPages.tests.ps1 delete mode 100644 Tests/helper.psm1 create mode 100644 Tools/AppVeyor/AppVeyor.psm1 create mode 100644 Tools/Release/Release.psm1 rename {Tests => Tools/TestHelper}/CompositeResourceFilter.psd1 (100%) create mode 100644 Tools/TestHelper/Data/readme.md create mode 100644 Tools/TestHelper/Data/sampleXccdf.xml.readme.txt create mode 100644 Tools/TestHelper/Data/sampleXccdf.xml.txt create mode 100644 Tools/TestHelper/Data/samplegroup.xml.txt create mode 100644 Tools/TestHelper/TestHelper.psm1 create mode 100644 Tools/WikiPages/WikiPages.psm1 diff --git a/.gitignore b/.gitignore index 23681f22a..4f3039eee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ # Cloned modules DSCResource.Tests -PowerStig.Tests # Editors .vscode/ .vs/ + +# local preference scripts/utilities +.local/ diff --git a/Tests/Integration/.tests.header.ps1 b/Tests/Integration/.tests.header.ps1 index 4a72f4659..0caa29442 100644 --- a/Tests/Integration/.tests.header.ps1 +++ b/Tests/Integration/.tests.header.ps1 @@ -3,14 +3,6 @@ $script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) $script:moduleName = 'PowerStig.Convert' $script:modulePath = "$($script:moduleRoot)\$($script:moduleName).psm1" -if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'PowerStig.Tests'))) -or ` - (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'PowerStig.Tests\TestHelper.psm1'))) ) -{ - & git @('clone', 'https://github.com/Microsoft/PowerStig.Tests', (Join-Path -Path $script:moduleRoot -ChildPath 'PowerStig.Tests')) -} - -Import-Module -Name (Join-Path -Path $script:moduleRoot -ChildPath ( - Join-Path -Path 'PowerStig.Tests' -ChildPath 'TestHelper.psm1') - ) -Force - +$helperModulePath = Join-Path -Path $script:moduleRoot -ChildPath 'Tools\TestHelper\TestHelper.psm1' +Import-Module $helperModulePath -Force Import-Module $modulePath -Force diff --git a/Tests/Integration/DSCResources/.tests.header.ps1 b/Tests/Integration/DSCResources/.tests.header.ps1 index 5475a5d34..bc24e5f64 100644 --- a/Tests/Integration/DSCResources/.tests.header.ps1 +++ b/Tests/Integration/DSCResources/.tests.header.ps1 @@ -9,7 +9,7 @@ if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCR & git @('clone','https://github.com/PowerShell/DscResource.Tests.git',(Join-Path -Path $script:moduleRoot -ChildPath '\DSCResource.Tests\')) } -Import-Module (Join-Path -Path $script:moduleRoot -ChildPath 'Tests\helper.psm1' ) -Force +Import-Module (Join-Path -Path $script:moduleRoot -ChildPath 'Tools\TestHelper\TestHelper.psm1' ) -Force Import-Module (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1') -Force $TestEnvironment = Initialize-TestEnvironment ` -DSCModuleName $script:DSCModuleName ` diff --git a/Tests/Integration/PowerStig.Integration.tests.ps1 b/Tests/Integration/PowerStig.Integration.tests.ps1 index b1d8d8153..597c1e064 100644 --- a/Tests/Integration/PowerStig.Integration.tests.ps1 +++ b/Tests/Integration/PowerStig.Integration.tests.ps1 @@ -4,13 +4,7 @@ $script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) $script:moduleName = 'PowerStig' $script:modulePath = "$($script:moduleRoot)\$($script:moduleName).psd1" -if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'PowerStig.Tests'))) -or ` - (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'PowerStig.Tests\TestHelper.psm1'))) ) -{ - & git @('clone', 'https://github.com/Microsoft/PowerStig.Tests', (Join-Path -Path $script:moduleRoot -ChildPath 'PowerStig.Tests')) -} - -Import-Module -Name (Join-Path -Path $script:moduleRoot -ChildPath (Join-Path -Path 'PowerStig.Tests' -ChildPath 'TestHelper.psm1')) -Force +Import-Module -Name (Join-Path -Path $script:moduleRoot -ChildPath 'Tools\TestHelper\TestHelper.psm1') -Force Import-Module $modulePath -Force #endregion diff --git a/Tests/Unit/.tests.header.ps1 b/Tests/Unit/.tests.header.ps1 new file mode 100644 index 000000000..39fdedd43 --- /dev/null +++ b/Tests/Unit/.tests.header.ps1 @@ -0,0 +1,13 @@ +# Unit Test Header +$script:moduleSection = Split-Path -Path (Split-Path -Path (Get-PSCallStack)[1].ScriptName -Parent ) -Leaf +$script:projectRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +$script:toolsRoot = Join-Path -Path $script:projectRoot -ChildPath 'Tools' +$script:moduleName = (Get-PSCallStack)[1].Command -replace '\.tests\.ps1', '' + +$script:modulePath = "$($script:projectRoot)\$($script:moduleSection)\$script:moduleName\$($script:moduleName).psm1" + +$helperModulePath = Join-Path -Path $script:toolsRoot -ChildPath ( + Join-Path -Path 'TestHelper' -ChildPath 'TestHelper.psm1') + +Import-Module -Name $helperModulePath -Force -Global +Import-Module -Name $script:modulePath -Force diff --git a/Tests/Unit/DSCResources/.tests.header.ps1 b/Tests/Unit/DSCResources/.tests.header.ps1 index d630130b1..13ed16ecb 100644 --- a/Tests/Unit/DSCResources/.tests.header.ps1 +++ b/Tests/Unit/DSCResources/.tests.header.ps1 @@ -1,3 +1,3 @@ # Unit Test Header $script:moduleRoot = Split-Path -Parent (Split-Path -Parent ( Split-Path -Parent $PSScriptRoot ) ) -Import-Module (Join-Path -Path $moduleRoot -ChildPath 'Tests\helper.psm1') -Force +Import-Module (Join-Path -Path $moduleRoot -ChildPath 'Tools\TestHelper\TestHelper.psm1') -Force diff --git a/Tests/Unit/DSCResources/Composite.tests.ps1 b/Tests/Unit/DSCResources/Composite.tests.ps1 index f3981368e..61cb1ec96 100644 --- a/Tests/Unit/DSCResources/Composite.tests.ps1 +++ b/Tests/Unit/DSCResources/Composite.tests.ps1 @@ -2,7 +2,7 @@ $script:DSCModuleName = 'PowerStig' # Header $script:moduleRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) -Import-Module (Join-Path -Path $moduleRoot -ChildPath 'Tests\helper.psm1') -Force +Import-Module (Join-Path -Path $moduleRoot -ChildPath 'Tools\TestHelper\TestHelper.psm1') -Force Describe 'Common Tests - Configuration Module Requirements' { diff --git a/Tests/Unit/Module/.tests.header.ps1 b/Tests/Unit/Module/.tests.header.ps1 index 09ba21104..b4b735183 100644 --- a/Tests/Unit/Module/.tests.header.ps1 +++ b/Tests/Unit/Module/.tests.header.ps1 @@ -3,15 +3,7 @@ $script:moduleRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $script:moduleName = (Get-PSCallStack)[1].Command -replace '\.tests\.ps1', '' $script:modulePath = "$($script:moduleRoot)$(($PSScriptRoot -split 'Unit')[1])\$script:moduleName\$($script:moduleName).psm1" -if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'PowerStig.Tests'))) -or ` - (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'PowerStig.Tests\TestHelper.psm1'))) ) -{ - & git @('clone', 'https://github.com/Microsoft/PowerStig.Tests', (Join-Path -Path $script:moduleRoot -ChildPath 'PowerStig.Tests')) -} - -Import-Module -Name (Join-Path -Path $script:moduleRoot -ChildPath ( - Join-Path -Path 'PowerStig.Tests' -ChildPath 'TestHelper.psm1') - ) -Force -Global +Import-Module -Name (Join-Path -Path $script:moduleRoot -ChildPath 'Tools\TestHelper\TestHelper.psm1') -Force -Global <# Several classes check for duplicate rules against a global variable stigSettings. diff --git a/Tests/Unit/Tools/Release.Tests.ps1 b/Tests/Unit/Tools/Release.Tests.ps1 new file mode 100644 index 000000000..7a2470581 --- /dev/null +++ b/Tests/Unit/Tools/Release.Tests.ps1 @@ -0,0 +1,746 @@ +if ($PSVersionTable.PSEdition -ne 'Core') +{ + return +} +$unitTestRoot = Split-Path -Path $PSScriptRoot -Parent +. "$unitTestRoot\.tests.header.ps1" + +try +{ + InModuleScope $script:ModuleName { + + Describe 'Test-ModuleVersion' -Tag 'tools' { + + $manifestPath = "$TestDrive\testManifest.psd1" + New-ModuleManifest -Path $manifestPath -ModuleVersion '1.0.0.0' + $moduleVersion = '1.1.0.0' + + Mock -CommandName Get-GitBranch -MockWith { 'dev' } + Mock -CommandName Set-GitBranch -MockWith { } -ParameterFilter {$Branch -eq 'master'} -Verifiable + + It "Should change to the master branch if not already in it" { + $null = Test-ModuleVersion -ModuleVersion $moduleVersion -ManifestPath $manifestPath + Assert-VerifiableMock + } + It "Should return $true when the supplied module version is higher than the master branch manifest" { + Test-ModuleVersion -ModuleVersion $moduleVersion -ManifestPath $manifestPath | Should Be $true + } + It "Should return $false when the supplied module version is NOT higher than the master branch manifest" { + $moduleVersion = '1.0.0.0' + Test-ModuleVersion -ModuleVersion $moduleVersion -ManifestPath $manifestPath | Should Be $false + } + It "Should import the manifest from `$PWD if a ManifestPath is not provided" { + $mockGetChildItem = @{FullName = $manifestPath} + Mock -CommandName Get-ChildItem -MockWith { return $mockGetChildItem } -Verifiable + $null = Test-ModuleVersion -ModuleVersion $moduleVersion + Assert-VerifiableMock + } + } + + Describe 'Get-PowerStigRepository' -Tag 'tools' { + $mockGetCommand = @{ + 'CommandType' = 'Application' + 'Name' = 'git.exe' + 'Version' = '2.15.1.2' + 'Source' = 'C:\Program Files\Git\cmd\git.exe' + } + + It "Should Throw if Git is not installed" { + Mock -CommandName Get-Command -MockWith { return $null } + { Get-PowerStigRepository } | Should Throw + } + + $repositoryList = @( + @{ + Name = 'https://github.com/Microsoft/PowerStig.git' + Result = @{ + 'name' = 'PowerStig' + 'html_url' = 'https://github.com/Microsoft/PowerStig' + 'api_url' = 'https://api.github.com/repos/Microsoft/PowerStig' + } + }, + @{ + Name = 'https://github.com/Microsoft/PowerStigDsc.git' + Result = @{ + 'name' = 'PowerStigDsc' + 'html_url' = 'https://github.com/Microsoft/PowerStigDsc' + 'api_url' = 'https://api.github.com/repos/Microsoft/PowerStigDsc' + } + } + ) + + foreach ($repository in $repositoryList) + { + Mock -CommandName Get-Command -MockWith { return $mockGetCommand } + Mock -CommandName Invoke-Command -MockWith { return $repository.Name } + Mock -CommandName Invoke-Git -MockWith { $repository.Name} + $PowerStigRepository = Get-PowerStigRepository + + It "Should return the correct name" { + $PowerStigRepository.name | Should Be $repository.Result.name + } + It "Should return the correct html URL" { + $PowerStigRepository.html_url | Should Be $repository.Result.html_url + } + It "Should return the correct Api URL" { + $PowerStigRepository.api_url | Should Be $repository.Result.api_url + } + } + + It "Should throw an error if a non PowerStig project is suplied" { + $repository = 'https://github.com/Microsoft/NotPowerStig.git' + Mock -CommandName Invoke-Git -MockWith { return $repository } + { Test-PowerStigRepository } | Should throw + } + } + + Describe 'Set-GitBranch' -Tag 'tools' { + $branch = 'dev' + + Context 'Already in dev Branch' { + Mock -CommandName Get-GitBranch -MockWith { return $branch } + Mock -CommandName Invoke-Git -MockWith { return } -Verifiable + It "Should not invoke 'git checkout $branch" { + Set-GitBranch -Branch $branch + Assert-MockCalled -CommandName Invoke-Git -ParameterFilter { $Command -eq "checkout $Branch" } -Times 0 + } + } + + Context 'Not already in dev Branch' { + Mock -CommandName Get-GitBranch -MockWith { return 'master' } + Mock -CommandName Invoke-Git -MockWith { return } ` + -ParameterFilter { $Command -eq "checkout $Branch" } -Verifiable + It "Should invoke 'git checkout $branch" { + Set-GitBranch -Branch $branch + Assert-MockCalled -CommandName Invoke-Git -Times 1 + } + } + + Context 'SkipPull' { + Mock -CommandName Get-GitBranch -MockWith { return $branch } + Mock -CommandName Invoke-Command -MockWith { return } -Verifiable + It "Should not invoke a pull when SkipPull is used" { + Set-GitBranch -Branch $branch -SkipPull + Assert-MockCalled -CommandName Invoke-Command -ParameterFilter { $ScriptBlock.ToString() -match "git pull" } -Times 0 + } + } + } + + Describe 'New-GitReleaseBranch' -Tag 'tools' { + $branchName = '1.2.3.4-release' + + Mock -CommandName Get-GitBranch -MockWith { return 'master' } -Verifiable + Mock -CommandName Set-GitBranch -MockWith { } -ParameterFilter { $Branch -eq 'dev'} -Verifiable + + Context 'Branch exists' { + Mock -CommandName Invoke-Git -MockWith {"[$branchName] Last Commit message"} ` + -ParameterFilter { $Command -eq "show-branch $branchName"} -Verifiable + Mock -CommandName Invoke-Git -MockWith {$null} ` + -ParameterFilter { $Command -eq "checkout $branchName"} -Verifiable + + It "Should switch to the $branchName branch" { + New-GitReleaseBranch -BranchName $branchName + Assert-VerifiableMock + } + } + + Context 'Branch does not exist' { + Mock -CommandName Invoke-Git -MockWith {"fatal: bad sha1 reference $branchName"} ` + -ParameterFilter { $Command -eq "show-branch $branchName" } -Verifiable + Mock -CommandName Invoke-Git -MockWith {$null} ` + -ParameterFilter { $Command -eq "checkout -b $branchName dev" } -Verifiable + + It "Should create the $branchName branch" { + New-GitReleaseBranch -BranchName $branchName + Assert-VerifiableMock + } + } + } + + Describe 'Remove-GitReleaseBranch' -Tag 'tools' { + $branchName = '1.2.3.4-release' + Mock -CommandName Invoke-Git -MockWith {} ` + -ParameterFilter {$Command -eq "merge $branchName"} -Verifiable + Mock -CommandName Invoke-Git -MockWith {} ` + -ParameterFilter {$Command -eq "push"} -Verifiable + Mock -CommandName Invoke-Git -MockWith {} ` + -ParameterFilter {$Command -eq "branch -d $branchName"} -Verifiable + Mock -CommandName Invoke-Git -MockWith {} ` + -ParameterFilter {$Command -eq "push origin -d $branchName"} -Verifiable + Mock -CommandName Invoke-Git -MockWith {} ` + -ParameterFilter {$Command -eq "remote prune origin"} -Verifiable + It "Should remove $branchName from the local and remote repo" { + Remove-GitReleaseBranch -BranchName $branchName + Assert-VerifiableMock + } + } + + Describe 'Push-GitBranch' -Tag 'tools' { + $branchName = '1.2.3.4-release' + + Mock -CommandName Invoke-Git -MockWith { return } -ParameterFilter { $Command -eq "push -u origin $BranchName"} -Verifiable + + It "Should NOT Commit or push $branchName if there are no changes" { + Mock -CommandName Invoke-Git -MockWith { 'Your branch is up to date' } ` + -ParameterFilter { $Command -eq "commit -a -m $CommitMessage"} -Verifiable + Push-GitBranch -Name $branchName -CommitMessage 'Commit Message' + Assert-MockCalled -CommandName Invoke-Git -Times 1 + } + + It "Should Commit or push $branchName if there are no changes" { + Mock -CommandName Invoke-Git -MockWith { 'Last commit message' } ` + -ParameterFilter { $Command -eq "commit -a -m $CommitMessage"} -Verifiable + Push-GitBranch -Name $branchName -CommitMessage 'Commit Message' + Assert-MockCalled -CommandName Invoke-Git -Times 2 + } + } + + Describe 'Get-UnreleasedNotes' -Tag 'tools' { + $sampleReadme = New-Object System.Text.StringBuilder + $null = $sampleReadme.AppendLine('') + $null = $sampleReadme.AppendLine('### Unreleased') + $null = $sampleReadme.AppendLine('') + $null = $sampleReadme.AppendLine('Update 1') + $null = $sampleReadme.AppendLine('Update 2') + $null = $sampleReadme.AppendLine('') + $null = $sampleReadme.AppendLine('### 1.0.0.0') + $null = $sampleReadme.AppendLine('') + + Mock -CommandName Get-ChildItem -MockWith { return @{'FullName' = 'empty\path'} } + Mock -CommandName Get-Content -MockWith { return $sampleReadme.ToString().Split("`n") } + + It "Should return the unreleased notes trimmed of extra lines" { + Get-UnreleasedNotes | Should Be ("Update 1`r`r`nUpdate 2" | Out-String).Trim() + } + } + + Describe 'Update-Readme' -Tag 'tools' { + $moduleVersion = '1.2.3.4' + $sampleReadme = New-Object System.Text.StringBuilder + $null = $sampleReadme.AppendLine('') + $contributors = $null = $sampleReadme.AppendLine('### Contributors').Length + $null = $sampleReadme.AppendLine('') + $unreleased = $sampleReadme.AppendLine('### Unreleased').Length + $null = $sampleReadme.AppendLine('') + $null = $sampleReadme.AppendLine('Update 1') + $null = $sampleReadme.AppendLine('Update 2') + $null = $sampleReadme.AppendLine('') + $null = $sampleReadme.AppendLine('### 1.0.0.0') + $null = $sampleReadme.AppendLine('') + + $sampleReadmePath = "$TestDrive\readme.md" + Mock -CommandName Get-ChildItem -MockWith { @{ FullName = $sampleReadmePath } } + Mock -CommandName Get-Content -MockWith {$sampleReadme.ToString()} + + Context 'ReleaseNotes' { + + It "Should correctly add the module version to the readme" { + Update-Readme -ModuleVersion $moduleVersion + $null = $sampleReadme.Insert($unreleased, "`n### $moduleVersion`n") + $readmeContent = Get-Content -Path $sampleReadmePath + $readmeContent | Should Be $sampleReadme.ToString() + } + } + + Context 'Contributors' { + $contributorList = @( + @{ + login = 'tester' + Name = 'Test' + } + ) + Mock -CommandName Get-ProjectContributorList -MockWith { $contributorList } + It "Should correctly add the contributors to the readme" { + Update-Readme -Repository @{} + $null = $sampleReadme.Insert($contributors, + "`n* [@$($contributorList.login)](https://github.com/$($contributorList.login)) ($($contributorList.Name))`n") + $readmeContent = Get-Content -Path $sampleReadmePath + $readmeContent | Should Be $sampleReadme.ToString() + } + } + } + + Describe 'Update-Manifest' -Tag 'tools' { + $moduleVersion = '1.2.3.4' + $releaseNotes = 'Added super cool feature' + $manifestPath = "$TestDrive\testManifest.psd1" + New-ModuleManifest -Path $manifestPath -ModuleVersion '1.0.0.0' -ReleaseNotes 'test' + Update-Manifest -ModuleVersion $moduleVersion -ReleaseNotes $releaseNotes -ManifestPath $manifestPath + $manifest = Import-PowerShellDataFile -Path $manifestPath + + It "Should update the manifest version number" { + $manifest.ModuleVersion | Should Be $moduleVersion + } + It "Should update the manifest release notes" { + $manifest.PrivateData.PSData.ReleaseNotes | Should Be $releaseNotes + } + } + + Describe 'Update-AppVeyorConfiguration' -Tag 'tools' { + $moduleVersion = '1.2.3.4' + $versionString = 'version: 0.2.0.{build}' + $newVersionString = 'version: 1.2.3.{build}' + $appveyor = New-Object System.Text.StringBuilder + $null = $appveyor.AppendLine('#---------------------------------#') + $null = $appveyor.AppendLine('') + $null = $appveyor.AppendLine($versionString) + $null = $appveyor.AppendLine('install:') + $null = $appveyor.AppendLine(' - ps: |') + $null = $appveyor.AppendLine(' Import-Module "$env:APPVEYOR_BUILD_FOLDER\AppVeyor.psm1"') + $null = $appveyor.AppendLine(' Invoke-AppveyorInstallTask') + $null = $appveyor.AppendLine('') + $null = $appveyor.AppendLine('#---------------------------------#') + $appveyorPath = "$TestDrive\appveyor.yml" + + Mock -CommandName Get-ChildItem -MockWith { @{ FullName = $appveyorPath } } + Mock -CommandName Get-Content -MockWith {$appveyor.ToString()} + $appveyorValue = $appveyor.ToString() -replace [regex]::Escape($versionString), $newVersionString + Mock -CommandName Set-Content -MockWith {} ` + -ParameterFilter { + $Path -eq $appveyorPath -and + $Value -eq $appveyorValue.TrimEnd()} -Verifiable + + It "Should update the version number" { + Update-AppVeyorConfiguration -ModuleVersion $moduleVersion + Assert-VerifiableMock + } + } + + Describe 'Get-ProjectContributorList' -Tag 'tools' { + $repository = @{ + name = 'PowerStig' + api_url = 'https://api.github.com' + } + $users = @( + @{ + user = @{ + login = 'tester1' + } + }, + @{ + user = @{ + login = 'tester2' + } + } | ConvertTo-Json + ) + Mock -CommandName Invoke-RestMethod -MockWith { return $users | ConvertFrom-Json } ` + -ParameterFilter {$URI -eq "$($repository.api_url)/pulls"} + + Mock -CommandName Invoke-RestMethod -MockWith { return 'testDetails' } ` + -ParameterFilter {$URI -match "$($repository.api_url)/users"} + + $list = Get-ProjectContributorList -Repository $repository + It 'Should return list if user details' { + $list[0] | Should Be 'testDetails' + } + } + + Describe 'Get-GitHubApiKey' -Tag 'tools' { + + It 'Should load the secure string from disk' { + Mock -CommandName Split-Path -MockWith { return } -Verifiable + Mock -CommandName Get-Content -MockWith { 'APIKeyMaterial' } ` + -ParameterFilter { $Path.EndsWith('PowerStigGitHubApi.txt')} -Verifiable + Mock -CommandName ConvertTo-SecureString -MockWith {} -Verifiable + Get-GitHubApiKey + Assert-VerifiableMock + } + + It 'Should load the file that is passed in' { + Mock -CommandName Test-Path -MockWith {return $true} -Verifiable + Mock -CommandName Get-Content -MockWith { 'APIKeyMaterial' } ` + -ParameterFilter { $Path.EndsWith('sampleFile.txt')} -Verifiable + Mock -CommandName ConvertTo-SecureString -MockWith {} -Verifiable + Get-GitHubApiKey -SecureFilePath "$Testdrive\sampleFile.txt" + Assert-VerifiableMock + } + } + + Describe 'Get-GitHubRefStatus' -Tag 'tools' { + + $stateList = @('pending', 'failure', 'success') + $repository = @{} + + Foreach ($state in $stateList) + { + It "Should return '$state' from rest API" { + Mock -CommandName Invoke-RestMethod -MockWith { return @{ state = $state } } + $status = Get-GitHubRefStatus -Repository $repository -Name test + $status | Should Be $state + } + } + + It "Should throw after waiting 10 minutes for a task to complete" { + Mock -CommandName Invoke-RestMethod -MockWith { return @{ state = 'pending' } } + Mock -CommandName Start-Sleep -MockWith { continue } -Verifiable + { Get-GitHubRefStatus -Repository $repository -Name test -WaitForSuccess } | Should Throw + } + } + + Describe 'New-GitHubPullRequest' -Tag 'tools' { + $moduleVersion = '1.2.3.4' + $repository = @{ + name = 'PowerStig' + api_url = 'https://api.github.com' + } + Mock -CommandName Invoke-RestMethod -MockWith { return @{ Status = '201 Created' } } ` + -ParameterFilter { $Uri -eq "$($Repository.api_url)/pulls"} -Verifiable + It 'Should create a PR on GitHub' { + $response = New-GitHubPullRequest -Repository $repository -ModuleVersion $moduleVersion -BranchHead 'dev' + $response.Status | Should Be '201 Created' + Assert-VerifiableMock + } + } + + Describe 'Get-GitHubPullRequest' -Tag 'tools' { + $pullRequestNumber = '34' + $repository = @{ + name = 'PowerStig' + api_url = 'https://api.github.com' + } + + $openPullRequestList = @( + @{ + id = '27' + head = @{ + ref = 'new-feature1' + } + base = @{ + ref = 'dev' + } + }, + @{ + id = $pullRequestNumber + head = @{ + ref = 'new-feature2' + } + base = @{ + ref = 'master' + } + } #| ConvertTo-Json + ) + + It 'Should return a specifc PR with the correct head and base on GitHub' { + Mock -CommandName Invoke-RestMethod -MockWith { return $openPullRequestList } ` + -ParameterFilter { $Uri -eq "$($Repository.api_url)/pulls"} -Verifiable + $response = Get-GitHubPullRequest -Repository $repository -BranchHead 'new-feature1' -BranchBase 'dev' + $response.head.ref | Should Be 'new-feature1' + Assert-VerifiableMock + } + + It 'Should return a specifc PR number on GitHub' { + Mock -CommandName Invoke-RestMethod -MockWith { return $openPullRequestList[1] } ` + -ParameterFilter { $Uri -eq "$($Repository.api_url)/pulls/$pullRequestNumber"} -Verifiable + $response = Get-GitHubPullRequest -Repository $repository -Number $pullRequestNumber + $response.id | Should Be $pullRequestNumber + Assert-VerifiableMock + } + } + + Describe 'Approve-GitHubPullRequest' -Tag 'tools' { + + $pullRequest = @{ + name = 'PowerStig' + url = 'https://api.github.com/pullrequest' + } + Mock -CommandName Invoke-RestMethod -MockWith { return @{ merged = $true } } ` + -ParameterFilter { $Uri -eq "$($pullRequest.url)/merge"} -Verifiable + It 'Should create a PR on GitHub' { + $response = Approve-GitHubPullRequest -PullRequest $pullRequest -CommitTitle 'Commit Title' -CommitMessage 'Commit Message' + $response.merged | Should Be $true + Assert-VerifiableMock + } + } + + Describe 'New-GitHubRelease' -Tag 'tools' { + $repository = @{ + name = 'PowerStig' + url = 'https://api.github.com/pullrequest' + } + Mock -CommandName Invoke-RestMethod -MockWith { return @{ status = '201 Created' } } ` + -ParameterFilter { $Uri -eq "$($repository.api_url)/releases"} -Verifiable + It 'Should create a release on GitHub' { + $response = New-GitHubRelease -Repository $repository -TagName '1.2.3.4-PSGallery' -Title 'Release Title' -Description 'Release Notes' + $response.status | Should Be '201 Created' + Assert-VerifiableMock + } + } + + Describe 'Start-PowerStigRelease' -Tag 'tools' { + + $testGitRepositoryPath = 'c:\dev\project' + $testModuleVersion = '1.2.3.4' + $testReleaseBranchName = "$testModuleVersion-Release" + $testReleaseNotes = 'added feature X' + $repository = @{ + name = 'PowerStig' + url = 'https://api.github.com' + } + $pullRequest = @{ + head = @{ + sha = 'b8e9aca7e6114734b1710b727786294d0e6a277b' + } + } + Mock -CommandName Push-Location -MockWith { } ` + -ParameterFilter { $GitRepositoryPath -eq $testGitRepositoryPath } -Verifiable + Mock -CommandName Get-GitBranch -MockWith { 'dev' } -Verifiable + Mock -CommandName Get-PowerStigRepository -MockWith { return $repository } -Verifiable + Mock -CommandName Test-ModuleVersion -MockWith { $true } ` + -ParameterFilter { $ModuleVersion -eq $testModuleVersion } -Verifiable + Mock -CommandName New-GitReleaseBranch -MockWith { } ` + -ParameterFilter { $BranchName -eq $testReleaseBranchName } -Verifiable + Mock -CommandName Get-UnreleasedNotes -MockWith { return $testReleaseNotes } -Verifiable + Mock -CommandName Update-Readme -MockWith { } ` + -ParameterFilter { $ModuleVersion -eq $testModuleVersion } -Verifiable + Mock -CommandName Update-Manifest -MockWith { } ` + -ParameterFilter { $ModuleVersion -eq $testModuleVersion -and $ReleaseNotes -eq $testReleaseNotes } -Verifiable + Mock -CommandName Update-AppVeyorConfiguration -MockWith { } ` + -ParameterFilter { $ModuleVersion -eq $testModuleVersion } -Verifiable + Mock -CommandName Push-GitBranch -MockWith { } ` + -ParameterFilter { $Name -eq $testReleaseBranchName } -Verifiable + Mock -CommandName Get-GitHubApiKey -MockWith { } -Verifiable + Mock -CommandName Get-GitHubRefStatus -MockWith { 'Success' } ` + -ParameterFilter { $Name -eq $testReleaseBranchName -and $WaitForSuccess -eq $true } -Verifiable + Mock -CommandName New-GitHubPullRequest -MockWith { return $pullRequest } ` + -ParameterFilter { $BranchHead -eq $testReleaseBranchName } -Verifiable + Mock -CommandName Get-GitHubRefStatus -MockWith { 'Success' } ` + -ParameterFilter { $Name -eq $pullRequest.head.sha -and $WaitForSuccess -eq $true } -Verifiable + + Context 'New Release' { + + Start-PowerStigRelease -GitRepositoryPath $testGitRepositoryPath -ModuleVersion $testModuleVersion + + It 'Should push to the repo location' { + Assert-MockCalled -CommandName Push-Location + } + It 'Should get the repo details' { + Assert-MockCalled -CommandName Get-PowerStigRepository + } + + Context 'Module Version' { + It 'Should test the module version is greater than currently release' { + Start-PowerStigRelease -GitRepositoryPath $testGitRepositoryPath -ModuleVersion $testModuleVersion + Assert-MockCalled -CommandName Test-ModuleVersion + } + It 'Should throw if the module version is not greater than currently release' { + Mock -CommandName Test-ModuleVersion -MockWith { $false } ` + -ParameterFilter { $ModuleVersion -eq $testModuleVersion } + {Start-PowerStigRelease -GitRepositoryPath $testGitRepositoryPath -ModuleVersion $testModuleVersion} | + Should Throw + } + } + It 'Should create a new release branch' { + Assert-MockCalled -CommandName New-GitReleaseBranch + } + Context 'Release Notes' { + It 'Should return the release notes' { + Start-PowerStigRelease -GitRepositoryPath $testGitRepositoryPath -ModuleVersion $testModuleVersion + Assert-MockCalled -CommandName Get-UnreleasedNotes + } + It 'Should throw if no release notes are found' { + Mock -CommandName Get-UnreleasedNotes -MockWith { return '' } -Verifiable + {Start-PowerStigRelease -GitRepositoryPath $testGitRepositoryPath -ModuleVersion $testModuleVersion} | + Should Throw + } + } + It 'Should update the readme' { + Assert-MockCalled -CommandName Update-Readme + } + It 'Should update the module manifest' { + Assert-MockCalled -CommandName Update-Manifest + } + It 'Should update the AppVeyor yaml' { + Assert-MockCalled -CommandName Update-AppVeyorConfiguration + } + It 'Should push the release branch to GitHub' { + Assert-MockCalled -CommandName Push-GitBranch + } + It 'Should get the GitHub api key' { + Assert-MockCalled -CommandName Get-GitHubApiKey + } + Context 'Release branch build status' { + It 'Should check the status of the release build' { + Start-PowerStigRelease -GitRepositoryPath $testGitRepositoryPath -ModuleVersion $testModuleVersion + Assert-MockCalled -CommandName Get-GitHubRefStatus + } + It 'Should throw if the release build status is not success' { + Mock -CommandName Get-GitHubRefStatus -MockWith { 'Failed' } ` + -ParameterFilter { $Name -eq $testReleaseBranchName -and $WaitForSuccess -eq $true } + {Start-PowerStigRelease -GitRepositoryPath $testGitRepositoryPath -ModuleVersion $testModuleVersion} | + Should Throw + } + } + Context 'Pull request' { + It 'Should create a new Pull request' { + Start-PowerStigRelease -GitRepositoryPath $testGitRepositoryPath -ModuleVersion $testModuleVersion + Assert-MockCalled -CommandName New-GitHubPullRequest + } + It 'Should check the status of the pull request build' { + Assert-MockCalled -CommandName Get-GitHubRefStatus -ParameterFilter { $Name -eq $pullRequest.head.sha -and $WaitForSuccess -eq $true } + } + It 'Should throw if the pull request build status is not success' { + Mock -CommandName Get-GitHubRefStatus -MockWith { 'Failed' } ` + -ParameterFilter { $Name -eq $pullRequest.head.sha -and $WaitForSuccess -eq $true } + {Start-PowerStigRelease -GitRepositoryPath $testGitRepositoryPath -ModuleVersion $testModuleVersion} | + Should Throw + } + } + } + + Context 'Continue Release' { + + Start-PowerStigRelease -GitRepositoryPath $testGitRepositoryPath -ModuleVersion $testModuleVersion -Continue + + It 'Should push to the repo location' { + Assert-MockCalled -CommandName Push-Location + } + It 'Should get the repo details' { + Assert-MockCalled -CommandName Get-PowerStigRepository + } + It 'Should not test the module version' { + Assert-MockCalled -CommandName Test-ModuleVersion -Times 0 + } + It 'Should not create a new release branch' { + Assert-MockCalled -CommandName New-GitReleaseBranch -Times 0 + } + It 'Should not return the release notes' { + Assert-MockCalled -CommandName Get-UnreleasedNotes -Times 0 + } + It 'Should not update the readme' { + Assert-MockCalled -CommandName Update-Readme -Times 0 + } + It 'Should not update the module manifest' { + Assert-MockCalled -CommandName Update-Manifest -Times 0 + } + It 'Should not update the AppVeyor yaml' { + Assert-MockCalled -CommandName Update-AppVeyorConfiguration -Times 0 + } + It 'Should not push the release branch to GitHub' { + Assert-MockCalled -CommandName Push-GitBranch -Times 0 + } + } + } + + Describe 'Complete-PowerStigRelease' -Tag 'tools' { + $testGitRepositoryPath = 'c:\dev\project' + $testModuleVersion = '1.2.3.4' + $testReleaseBranchName = "$testModuleVersion-Release" + $testReleaseNotes = 'added feature X' + $powerShellDataFileObject = @{ + PrivateData = @{ + PSData = @{ + ReleaseNotes = $testReleaseNotes + } + } + } + $repository = @{ + name = 'PowerStig' + url = 'https://api.github.com' + } + $pullRequest = @{ + head = @{ + sha = 'b8e9aca7e6114734b1710b727786294d0e6a277b' + } + } + $testManifestPath = 'c:\test\path\manifest.psd1' + + Mock -CommandName Push-Location -MockWith { } ` + -ParameterFilter { $GitRepositoryPath -eq $testGitRepositoryPath } -Verifiable + Mock -CommandName Get-GitBranch -MockWith { 'dev' } -Verifiable + Mock -CommandName Get-PowerStigRepository -MockWith { return $repository } -Verifiable + Mock -CommandName Get-GitHubApiKey -MockWith { } -Verifiable + Mock -CommandName Get-GitHubPullRequest -MockWith { return $pullRequest } ` + -ParameterFilter { $BranchHead -eq $testReleaseBranchName } -Verifiable + Mock -CommandName Approve-GitHubPullRequest -MockWith { } ` + -ParameterFilter { $PullRequest -eq $PullRequest -and $MergeMethod -eq 'merge' } -Verifiable + Mock -CommandName Get-ChildItem -MockWith { @{FullName = $testManifestPath } } + Mock -CommandName Import-PowerShellDataFile -MockWith { return $powerShellDataFileObject } ` + -ParameterFilter { $Path -eq $testManifestPath } -Verifiable + Mock -CommandName New-GitHubRelease -ParameterFilter { $Description -eq $testReleaseNotes } -Verifiable + Mock -CommandName Remove-GitReleaseBranch -ParameterFilter { $BranchName -eq $testReleaseBranchName} -Verifiable + + Complete-PowerStigRelease -GitRepositoryPath $testGitRepositoryPath -ModuleVersion $testModuleVersion + + It 'Should push to the repo location' { + Assert-MockCalled -CommandName Push-Location + } + It 'Should get the repo details' { + Assert-MockCalled -CommandName Get-PowerStigRepository + } + It 'Should get the GitHub API key' { + Assert-MockCalled -CommandName Get-GitHubApiKey + } + It 'Should get the GitHub release pull request' { + Assert-MockCalled -CommandName Get-GitHubPullRequest + } + It 'Should approve the GitHub release pull request' { + Assert-MockCalled -CommandName Approve-GitHubPullRequest + } + It 'Should Create a new GitHub release' { + Assert-MockCalled -CommandName New-GitHubRelease + } + It 'Should remove the release branch from all repos' { + Assert-MockCalled -CommandName Remove-GitReleaseBranch + } + } + + Describe 'Complete-PowerStigDevMerge' -Tag 'tools' { + $testGitRepositoryPath = 'c:\dev\project' + $testPullRequestNumber = 488 + $repository = @{ + name = 'PowerStig' + url = 'https://api.github.com' + } + $pullRequest = @{ + head = @{ + sha = 'b8e9aca7e6114734b1710b727786294d0e6a277b' + } + } + + Mock -CommandName Push-Location -MockWith { } ` + -ParameterFilter { $GitRepositoryPath -eq $testGitRepositoryPath } -Verifiable + Mock -CommandName Get-PowerStigRepository -MockWith { return $repository } -Verifiable + Mock -CommandName Get-GitHubApiKey -MockWith { } -Verifiable + Mock -CommandName Get-GitHubPullRequest -MockWith { return $pullRequest } ` + -ParameterFilter { $Number -eq $testPullRequestNumber } -Verifiable + Mock -CommandName Approve-GitHubPullRequest -MockWith { } ` + -ParameterFilter { $PullRequest -eq $PullRequest -and $MergeMethod -eq 'squash' } -Verifiable + Mock Set-GitBranch -MockWith {} -ParameterFilter { $Branch -eq 'dev' } -Verifiable + Mock -CommandName Update-Readme -MockWith { } ` + -ParameterFilter { $Repository -eq $repository } -Verifiable + Mock -CommandName Push-GitBranch -MockWith { } ` + -ParameterFilter { $Name -eq 'dev' } -Verifiable + + Complete-PowerStigDevMerge -GitRepositoryPath $testGitRepositoryPath -PullRequestNumber $testPullRequestNumber + + It 'Should push to the repo location' { + Assert-MockCalled -CommandName Push-Location + } + It 'Should get the repo details' { + Assert-MockCalled -CommandName Get-PowerStigRepository + } + It 'Should get the GitHub API key' { + Assert-MockCalled -CommandName Get-GitHubApiKey + } + It 'Should get the GitHub release pull request' { + Assert-MockCalled -CommandName Get-GitHubPullRequest + } + It 'Should approve the GitHub release pull request' { + Assert-MockCalled -CommandName Approve-GitHubPullRequest + } + It 'Should switch to the dev branch' { + Assert-MockCalled -CommandName Set-GitBranch + } + It 'Should update the dev branch readme contributors' { + Assert-MockCalled -CommandName Update-Readme + } + It 'Should push the dev branch to GitHub' { + Assert-MockCalled -CommandName Push-GitBranch + } + } + } +} +finally +{ + +} diff --git a/Tests/Unit/Tools/TestHelper.tests.ps1 b/Tests/Unit/Tools/TestHelper.tests.ps1 new file mode 100644 index 000000000..734c72e8b --- /dev/null +++ b/Tests/Unit/Tools/TestHelper.tests.ps1 @@ -0,0 +1,13 @@ +$unitTestRoot = Split-Path -Path $PSScriptRoot -Parent +. "$unitTestRoot\.tests.header.ps1" + +try +{ + Describe 'TestHelper' -Tag 'tools' { + + } +} +finally +{ + +} diff --git a/Tests/Unit/Tools/WikiPages.tests.ps1 b/Tests/Unit/Tools/WikiPages.tests.ps1 new file mode 100644 index 000000000..5a42df985 --- /dev/null +++ b/Tests/Unit/Tools/WikiPages.tests.ps1 @@ -0,0 +1,58 @@ +$unitTestRoot = Split-Path -Path $PSScriptRoot -Parent +. "$unitTestRoot\.tests.header.ps1" +try +{ + InModuleScope $script:ModuleName { + + Describe 'Format-HelpString' -Tag 'tools' { + + Context 'Multi-Line' { + It "Should Format the help string" { + $testString = @{ + sample = @( + 'This is a multi line string. ' + 'There are multiple lines.') -Join "`n" + result = @( + 'This is a multi line string.' + 'There are multiple lines.') -Join "`n" + } + Format-HelpString -String $testString.sample | + Should Be $testString.result + } + It "Should return full sentences on each line." { + $testString = @{ + sample = @( + 'This is a multi line' + 'string. That has a period.') -Join "`n" + result = @( + 'This is a multi line string.' + 'That has a period.') -Join "`n" + } + + Format-HelpString -String $testString.sample | + Should Be $testString.result + } + } + + Context 'Single-Line' { + + It "Should return multiple sentences on a singe line." { + $testString = @{ + sample = @( + 'This is a multi line string. ' + 'There are multiple lines.') -Join "`n" + result = @( + 'This is a multi line string.' + 'There are multiple lines.') -Join " " + } + Format-HelpString -String $testString.sample -SingleLine | + Should Be $testString.result + } + } + } + } +} +finally +{ + +} diff --git a/Tests/helper.psm1 b/Tests/helper.psm1 deleted file mode 100644 index 9f8bc52d3..000000000 --- a/Tests/helper.psm1 +++ /dev/null @@ -1,195 +0,0 @@ -Import-Module $PSScriptRoot\..\PowerStig.psm1 - -function Get-RequiredStigDataVersion -{ - [CmdletBinding()] - param() - - $Manifest = Import-PowerShellDataFile -Path "$relDirectory\$moduleName.psd1" - - return $Manifest.RequiredModules.Where({$PSItem.ModuleName -eq 'PowerStig'}).ModuleVersion -} - -function Get-StigDataRootPath -{ - param ( ) - - return Resolve-Path -Path "$PsScriptRoot\..\StigData" -} - -<# - .SYNOPSIS - Get all of the version files to test - - .PARAMETER CompositeResourceName - The name of the composite resource used to filter the results -#> -function Get-StigFileList -{ - [CmdletBinding()] - param - ( - [Parameter(Mandatory = $true)] - [string] - $CompositeResourceName - ) - - # - $stigFilePath = Get-StigDataRootPath - $stigVersionFiles = Get-ChildItem -Path $stigFilePath -Exclude "*.org*" - - $stigVersionFiles -} - -<# - .SYNOPSIS - Returns a list of stigs for a given resource. This is used in integration testign by looping - through every valide STIG found in the StigData directory. - - .PARAMETER CompositeResourceName - The resource to filter the results - - .PARAMETER Filter - Parameter description - -#> -function Get-StigVersionTable -{ - [OutputType([psobject])] - [CmdletBinding()] - param - ( - [Parameter(Mandatory = $true)] - [string] - $CompositeResourceName, - - [Parameter()] - [string] - $Filter - ) - - $include = Import-PowerShellDataFile -Path $PSScriptRoot\CompositeResourceFilter.psd1 - - $path = "$(Get-StigDataRootPath)\Processed" - - $versions = Get-ChildItem -Path $path -Exclude "*.org.*", "*.xsd" -Include $include.$CompositeResourceName -File -Recurse - - $versionTable = @() - foreach ($version in $versions) - { - if ($version.Basename -match $Filter) - { - $stigDetails = $version.BaseName -Split "-" - - $versionTable += @{ - 'Technology' = $stigDetails[0] - 'TechnologyVersion' = $stigDetails[1] - 'TechnologyRole' = $stigDetails[2] - 'StigVersion' = $stigDetails[3] - 'Path' = $version.fullname - } - } - } - - return $versionTable -} - -<# - .SYNOPSIS - Using an AST, it returns the name of a configuration in the composite resource schema file. - - .PARAMETER FilePath - The full path to the resource schema module file -#> -function Get-ConfigurationName -{ - [CmdletBinding()] - [OutputType([string[]])] - param - ( - [Parameter(Mandatory = $true)] - [String] - $FilePath - ) - - $AST = [System.Management.Automation.Language.Parser]::ParseFile( - $FilePath, [ref] $null, [ref] $Null - ) - - # Get the Export-ModuleMember details from the module file - $ModuleMember = $AST.Find( { - $args[0] -is [System.Management.Automation.Language.ConfigurationDefinitionAst]}, $true) - - return $ModuleMember.InstanceName.Value -} - -<# - .SYNOPSIS - Returns the list of StigVersion nunmbers that are defined in the ValidateSet parameter attribute - - .PARAMETER FilePath - THe full path to the resource to read from -#> -function Get-StigVersionParameterValidateSet -{ - [OutputType([string])] - [CmdletBinding()] - param - ( - [Parameter(Mandatory = $true)] - [string] - $FilePath - ) - - $compositeResource = Get-Content -Path $FilePath -Raw - - $AbstractSyntaxTree = [System.Management.Automation.Language.Parser]::ParseInput( - $compositeResource, [ref]$null, [ref]$null) - - $params = $AbstractSyntaxTree.FindAll( - {$args[0] -is [System.Management.Automation.Language.ParameterAst]}, $true) - - # Filter the specifc ParameterAst - $paramToUpdate = $params | - Where-Object {$PSItem.Name.VariablePath.UserPath -eq 'StigVersion'} - - # Get the specifc parameter attribute to update - $validate = $paramToUpdate.Attributes.Where( - {$PSItem.TypeName.Name -eq 'ValidateSet'}) - - return $validate.PositionalArguments.Value -} - -<# - .SYNOPSIS - Get a unique list of valid STIG versions from the StigData - - .PARAMETER TechnologyRoleFilter - The technology role to filter the results -#> - -function Get-ValidStigVersionNumbers -{ - [OutputType([string])] - [CmdletBinding()] - param - ( - [Parameter(Mandatory = $true)] - [string] - $TechnologyRoleFilter - ) - - $versionNumbers = (Get-Stiglist | - Where-Object {$PSItem.TechnologyRole -match $TechnologyRoleFilter} | - Select-Object StigVersion -ExpandProperty StigVersion -Unique ) - - return $versionNumbers -} - -Export-ModuleMember -Function @( - 'Get-PowerStigVersionFromManifest', - 'Get-StigVersionTable', - 'Get-ConfigurationName', - 'Get-StigVersionParameterValidateSet', - 'Get-ValidStigVersionNumbers' -) diff --git a/Tools/AppVeyor/AppVeyor.psm1 b/Tools/AppVeyor/AppVeyor.psm1 new file mode 100644 index 000000000..954791483 --- /dev/null +++ b/Tools/AppVeyor/AppVeyor.psm1 @@ -0,0 +1,274 @@ +# Load the test helper module. +#$testHelperPath = Join-Path -Path $PSScriptRoot -ChildPath 'TestHelper.psm1' +#Import-Module -Name $testHelperPath -Force + +<# + .SYNOPSIS + Performs the after tests tasks for the AppVeyor build process. + + This includes: + 1. Optional: Produce and upload Wiki documentation to AppVeyor. + 2. Set version number in Module Manifest to build version + 3. Zip up the module content and produce a checksum file and upload to AppVeyor. + 4. Pack the module into a Nuget Package. + 5. Upload the Nuget Package to AppVeyor. + + Executes Start-CustomAppveyorAfterTestTask if defined in .AppVeyor\CustomAppVeyorTasks.psm1 + in resource module repository. + + .PARAMETER Type + This controls the additional processes that can be run after testing. + To produce wiki documentation specify 'Wiki', otherwise leave empty to use + default value 'Default'. + + .PARAMETER MainModulePath + This is the relative path of the folder that contains the module manifest. + If not specified it will default to the root folder of the repository. + + .PARAMETER ResourceModuleName + Name of the Resource Module being produced. + If not specified will default to GitHub repository name. + + .PARAMETER Author + The Author string to insert into the NUSPEC file for the package. + If not specified will default to 'Microsoft'. + + .PARAMETER Owners + The Owners string to insert into the NUSPEC file for the package. + If not specified will default to 'Microsoft'. +#> +function Invoke-PowerStigAppveyorAfterTestTask +{ + [CmdletBinding(DefaultParameterSetName = 'Default')] + param + ( + [Parameter()] + [ValidateNotNullOrEmpty()] + [String] + $MainModulePath = $env:APPVEYOR_BUILD_FOLDER, + + [Parameter()] + [String] + $ModuleName = (($env:APPVEYOR_REPO_NAME -split '/')[1]), + + [Parameter()] + [String] + $Author, + + [Parameter()] + [String] + $Owners, + + [Parameter()] + [String] + $Tags + ) + + # Convert the Main Module path into an absolute path if it is relative + if (-not ([System.IO.Path]::IsPathRooted($MainModulePath))) + { + $MainModulePath = Join-Path -Path $env:APPVEYOR_BUILD_FOLDER ` + -ChildPath $MainModulePath + } + + $manifest = Import-PowerShellDataFile -Path "$MainModulePath\$moduleName`.psd1" + + $fileListPath = "$env:APPVEYOR_BUILD_FOLDER\.NuspecFileList.json" + if (Test-Path -Path $fileListPath ) + { + $fileList = Get-Content -Path $fileListPath | ConvertFrom-Json + } + + # Create the Nuspec file for the Nuget Package in the Main Module Folder + $nuspecPath = Join-Path -Path $MainModulePath -ChildPath "$ModuleName.nuspec" + + $nuspecParams = @{ + PackageName = $ModuleName + DestinationPath = $MainModulePath + Version = $env:APPVEYOR_BUILD_VERSION + Author = $manifest.Author + Owners = $manifest.CompanyName + LicenseUrl = $manifest.PrivateData.PSData.LicenseUri + ProjectUrl = $manifest.PrivateData.PSData.ProjectUri + PackageDescription = $ModuleName + Tags = $manifest.PrivateData.PSData.Tags -join ' ' + ReleaseNotes = $manifest.PrivateData.PSData.ReleaseNotes + FileList = $fileList + RequiredModules = $manifest.RequiredModules + } + New-Nuspec @nuspecParams + + # Create the Nuget Package + $nugetExePath = (Get-Command nuget).Source + + Start-Process -FilePath $nugetExePath -Wait -ArgumentList @( + 'Pack', $nuspecPath + '-OutputDirectory', $env:APPVEYOR_BUILD_FOLDER + '-BasePath', $MainModulePath + ) + + # Push the Nuget Package up to AppVeyor + $nugetPackageName = Join-Path -Path $env:APPVEYOR_BUILD_FOLDER ` + -ChildPath "$ModuleName.$($env:APPVEYOR_BUILD_VERSION).nupkg" + Get-ChildItem $nugetPackageName | ForEach-Object -Process { + Push-AppveyorArtifact -Path $_.FullName -FileName $_.Name + } + + Write-Info -Message 'After Test Task Complete.' +} + + +<# + .SYNOPSIS + Creates a nuspec file for a nuget package at the specified path. + + .EXAMPLE + New-Nuspec ` + -PackageName 'TestPackage' ` + -Version '1.0.0.0' ` + -Author 'Microsoft Corporation' ` + -Owners 'Microsoft Corporation' ` + -DestinationPath C:\temp ` + -LicenseUrl 'http://license' ` + -PackageDescription 'Description of the package' ` + -Tags 'tag1 tag2' +#> +function New-Nuspec +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [String] + $PackageName, + + [Parameter(Mandatory = $true)] + [String] + $Version, + + [Parameter(Mandatory = $true)] + [String] + $Author, + + [Parameter(Mandatory = $true)] + [String] + $Owners, + + [Parameter(Mandatory = $true)] + [String] + $DestinationPath, + + [Parameter(Mandatory = $true)] + [String] + $LicenseUrl, + + [Parameter(Mandatory = $true)] + [String] + $ProjectUrl, + + [Parameter()] + [String] + $IconUrl, + + [Parameter(Mandatory = $true)] + [String] + $PackageDescription, + + [Parameter(Mandatory = $true)] + [String] + $ReleaseNotes, + + [Parameter()] + [String] + $Tags, + + [Parameter()] + [AllowNull()] + [pscustomobject] + $FileList, + + [Parameter()] + [AllowNull()] + [pscustomobject] + $RequiredModules + ) + # https://docs.microsoft.com/en-us/nuget/reference/nuspec + + $currentYear = (Get-Date).Year + + $nuspecFileContent = New-Object System.Text.StringBuilder + $null = $nuspecFileContent.AppendLine('') + $null = $nuspecFileContent.AppendLine('') + $null = $nuspecFileContent.AppendLine(' ') + $null = $nuspecFileContent.AppendLine(" $PackageName") + $null = $nuspecFileContent.AppendLine(" $Version") + $null = $nuspecFileContent.AppendLine(" $Author") + $null = $nuspecFileContent.AppendLine(" $Owners") + + if (-not [String]::IsNullOrEmpty($LicenseUrl)) + { + $null = $nuspecFileContent.AppendLine(" $LicenseUrl") + } + + if (-not [String]::IsNullOrEmpty($ProjectUrl)) + { + $null = $nuspecFileContent.AppendLine(" $ProjectUrl") + } + + if (-not [String]::IsNullOrEmpty($IconUrl)) + { + $null = $nuspecFileContent.AppendLine(" $IconUrl") + } + + $null = $nuspecFileContent.AppendLine(" true") + $null = $nuspecFileContent.AppendLine(" $PackageDescription") + $null = $nuspecFileContent.AppendLine(" $ReleaseNotes") + $null = $nuspecFileContent.AppendLine(" Copyright $currentYear") + $null = $nuspecFileContent.AppendLine(" $Tags") + + if ($RequiredModules) + { + $null = $nuspecFileContent.AppendLine(" ") + + ForEach($dependency in $RequiredModules) + { + $moduleName = $dependency.ModuleName + $moduleVersion = "[$($dependency.ModuleVersion)]" + $null = $nuspecFileContent.AppendLine(" ") + } + $null = $nuspecFileContent.AppendLine(" ") + } + + $null = $nuspecFileContent.AppendLine(" ") + + if (-not [String]::IsNullOrEmpty($fileList)) + { + $null = $nuspecFileContent.AppendLine(" ") + + foreach ($file in $fileList) + { + $null = $nuspecFileContent.AppendLine(" ") + } + $null = $nuspecFileContent.AppendLine(" ") + } + + $null = $nuspecFileContent.AppendLine("") + + if (-not $DestinationPath) + { + $DestinationPath = $Path + } + + if (-not (Test-Path -Path $DestinationPath)) + { + $null = New-Item -Path $DestinationPath -ItemType 'Directory' + } + + $nuspecFilePath = Join-Path -Path $DestinationPath -ChildPath "$PackageName.nuspec" + + $null = New-Item -Path $nuspecFilePath -ItemType 'File' -Force + + $null = Set-Content -Path $nuspecFilePath -Value $nuspecFileContent.ToString() +} + +Export-ModuleMember -Function * diff --git a/Tools/Release/Release.psm1 b/Tools/Release/Release.psm1 new file mode 100644 index 000000000..8f6472128 --- /dev/null +++ b/Tools/Release/Release.psm1 @@ -0,0 +1,1129 @@ +#requires -module posh-git +#requires -version 6.0 + +$script:ReleaseName = "{0}-Release" + +<# + .SYNOPSIS + Validates that the supplied module version is greater than the current + version listed in the module manifest. +#> +function Test-ModuleVersion +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [version] + $ModuleVersion, + + [Parameter()] + [string] + $ManifestPath + ) + + if (Get-GitBranch -ne 'master') + { + Set-GitBranch -Branch master + } + + if (-not $ManifestPath) + { + $ManifestPath = (Get-ChildItem -Path $PWD -Filter "*.psd1").FullName + } + + $masterManifest = Import-PowerShellDataFile -Path $ManifestPath + + if ($ModuleVersion -le $masterManifest.ModuleVersion) + { + return $false + } + + return $true +} +#region Git + +function Invoke-Git +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string] + $Command + ) + + Invoke-Expression "git $Command" 2>&1 +} + +<# + .SYNOPSIS + Returns the remote url details of the local git repository. The remote + url is used to build the return object properties. +#> +function Get-PowerStigRepository +{ + [OutputType([string])] + [CmdletBinding()] + param() + + Write-Host "Testing git is installed" + + $git = Get-Command "git" -ErrorAction SilentlyContinue + if ($git) + { + Write-Verbose "Git is found on the System at $($git.Source)" + } + else + { + throw "Git was not found on the System. Please install Git and try again" + } + + Write-Host "Getting remote repository details" + + $gitRemote = Invoke-Git -Command "remote get-url origin" + + $baseUrl = $gitRemote -replace '\.git$','' + if ([string]::IsNullOrEmpty($gitRemote) -or + $gitRemote -notmatch "^https://github.com/Microsoft/PowerStig") + { + throw "$gitRemote is not a PowerStig Project. Please select a PowerStig project to release." + } + else + { + Write-Verbose -Message "Releasing $baseUrl" + } + + return [ordered]@{ + 'name' = ($baseUrl -split "/")[-1] + 'html_url' = $baseUrl + 'api_url' = $baseUrl -replace 'github\.com', 'api.github.com/repos' + } +} + +<# + .SYNOPSIS + A set companion function to the posh-git Get-GitBranch function. + + .PARAMETER Branch + The name of the branch to switch to. + + .Parameter SkipPull + By default the branch remote is pulled as soon as it is swithced into. + This switch skips the pull step. This normaly just a perf boost. +#> +function Set-GitBranch +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string] + $Branch, + + [Parameter()] + [switch] + $SkipPull + ) + + $currentBranch = Get-GitBranch + + # If the case is input incorrectly, drop it to lower for dev and master + if ($Branch -match "dev|master") + { + $Branch = $Branch.ToLower() + } + + if ($currentBranch -ne $Branch) + { + Write-Host "Switching to $Branch branch" + $null = Invoke-Git -Command "checkout $Branch" + } + else + { + Write-Verbose -Message "Already in $Branch branch" + } + + if ($SkipPull) + { + Write-Verbose -Message "Skipping Pull" + } + else + { + Write-Verbose -Message "Pulling $Branch branch from origin" + $null = Invoke-Git -Command "pull" + } +} + +function New-GitReleaseBranch +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string] + $BranchName + ) + + if(Get-GitBranch -ne 'dev') + { + Set-GitBranch -Branch dev + } + + Write-Host "Creating Git branch ($BranchName)" + + $branch = Invoke-Git -Command "show-branch $BranchName" + + If ($branch -match "^\[$BranchName\]") + { + $null = Invoke-Git -Command "checkout $BranchName" + } + else + { + $null = Invoke-Git -Command "checkout -b $BranchName dev" + } +} + +function Remove-GitReleaseBranch +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string] + $BranchName + ) + + Write-Host "Removing Git branch ($BranchName)" + + # Merge the release back into dev + if(Get-GitBranch -ne 'dev') + { + Set-GitBranch -Branch dev + } + + Invoke-Git -Command "merge $branchName" + # Push dev to GitHub + Invoke-Git -Command "push" + # Delete the release branch (Locally and remotely) + Invoke-Git -Command "branch -d $branchName" + # push the delete to GitHub + Invoke-Git -Command "push origin -d $branchName" + # Remove the origin branch reference from the local repo + Invoke-Git -Command "remote prune origin" +} + +function Push-GitBranch +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(Mandatory = $true)] + [string] + $CommitMessage + ) + + Write-Host "Pushing ($Name) to GitHub." + + $commit = Invoke-Git -Command "commit -a -m '$CommitMessage'" + + if ($commit -match 'Your branch is up to date') + { + Write-Host "Nothing to commit or push" + return + } + $null = Invoke-Git -Command "push -u origin $Name" +} +#endregion + +#region Update Version and release notes + +<# + .SYNOPSIS + Get Unreleased content from the readme +#> +function Get-UnreleasedNotes +{ + [OutputType([string])] + [CmdletBinding()] + param + ( + [Parameter()] + [string] + $Branch + ) + Write-Host "Getting unreleased notes from Readme." + $readmePath = (Get-ChildItem -Path $PWD -Filter "readme.md").FullName + $readmeContent = Get-Content -Path $readmePath + + $h3 = '^###' + $unreleasedHeader = "$h3\s+Unreleased" + $latestedreleaseHeader = "$h3\s+\d\.\d\.\d\.\d" + + $unreleasedLine = $readmeContent | + Select-String -Pattern $unreleasedHeader + $latestedreleaseLine = ($readmeContent | + Select-String -Pattern $latestedreleaseHeader)[0] + + $releaseNotes = $readmeContent[ + ($unreleasedLine.LineNumber)..($latestedreleaseLine.LineNumber - 2)] | + Out-String + + return $releaseNotes.Trim() +} + +<# + .SYNOPSIS + Adds a new version section to the readme released notes or updates the + list of contributors to the Contributors section. + + .PARAMETER ModuleVersion + The module version to add to the readme below the unreleased section + + .PARAMETER Repository + A hashtable that contains the repository api url to query for the list of contributors +#> +function Update-Readme +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true, ParameterSetName = 'ReleaseNotes')] + [version] + $ModuleVersion, + + [Parameter(Mandatory = $true, ParameterSetName = 'Contributors')] + [hashtable] + $Repository + ) + + Write-Host "Updating $($PSCmdlet.ParameterSetName)." + $readmePath = (Get-ChildItem -Path $PWD -Filter "readme.md").FullName + $readmeContent = Get-Content -Path $readmePath -Raw + + switch ($PSCmdlet.ParameterSetName) + { + 'ReleaseNotes' + { + $unreleasedHeaderRegEx = '###\sUnreleased' + $unreleasedHeaderReplace = New-Object System.Text.StringBuilder + $null = $unreleasedHeaderReplace.AppendLine('### Unreleased') + $null = $unreleasedHeaderReplace.AppendLine('') + $null = $unreleasedHeaderReplace.AppendLine("### $ModuleVersion") + + $readmeContent = $readmeContent -replace $unreleasedHeaderRegEx, + $unreleasedHeaderReplace.ToString().Trim() + continue + } + 'Contributors' + { + $contributorList = Get-ProjectContributorList -Repository $Repository + $contributorsMd = New-Object System.Text.StringBuilder + $null = $contributorsMd.AppendLine('') + $null = $contributorsMd.AppendLine('') + + foreach ($contributor in $contributorList) + { + $line = "* [@$($contributor.login)](https://github.com/$($contributor.login))" + if ($contributor.Name) + { + $line = $line + " ($($contributor.Name))" + } + + $null = $contributorsMd.AppendLine($line) + } + $null = $contributorsMd.AppendLine('') + + $readmeContributorsRegEx = '(?<=### Contributors)[^#]+(?=#)' + $readmeContent = $readmeContent -replace $readmeContributorsRegEx,$contributorsMd.ToString() + continue + } + } + + Set-Content -Path $readmePath -Value $readmeContent.Trim() +} + +<# + .SYNOPSIS + Updates a module manifest version and release notes. + + .PARAMETER ModuleVersion + The version number to update the manifest with + + .PARAMETER ReleaseNotes + The release notes to update the manifest with +#> +function Update-Manifest +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [version] + $ModuleVersion, + + [Parameter(Mandatory = $true)] + [string] + $ReleaseNotes, + + [Parameter()] + [string] + $ManifestPath + ) + + Write-Host "Updating version number and release notes in module manifest." + + if (-not $ManifestPath) + { + $ManifestPath = (Get-ChildItem -Path $PWD -Filter "*.psd1").FullName + } + + $manifestContent = Get-Content -Path $ManifestPath -Raw + $moduleVersionRegex = '(?<=ModuleVersion\s*=\s*'')(?.*)(?=''(?!(\s*)}))' + $manifestContent = $manifestContent -replace $moduleVersionRegex, $ModuleVersion + + $releaseNotesRegEx = "(?<=ReleaseNotes\s*=\s*')[^']+(?=')" + $manifestContent = $manifestContent -replace $releaseNotesRegEx, $ReleaseNotes + + Set-Content -Path $ManifestPath -Value $manifestContent.TrimEnd() +} + +<# + .SYNOPSIS + Updates the AppVeyor build configuration yaml file with the module + version number + + .PARAMETER ModuleVersion + The version number to update the manifest with +#> +function Update-AppVeyorConfiguration +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [version] + $ModuleVersion + ) + + Write-Host "Updating appveyor configuration." + + $appveyorPath = (Get-ChildItem -Path $PWD -Filter "appveyor.yml").FullName + $appveyorContent = Get-Content -Path $appveyorPath + $regex = 'version\:\s\d\.\d\.\d\.\{build\}' + $appveyorContent = $appveyorContent -replace $regex, + "version: $($moduleVersion.major).$($moduleVersion.Minor).$($moduleVersion.Build).{build}" + + Set-Content -Path $appveyorPath -Value $appveyorContent.TrimEnd() +} + +#endregion +#region Update Contributor list + +<# + .SYNOPSIS + Queries the GitHub repo and returns a unique list of users that have + submitted closed pull requests into the dev branch + + .PARAMETER Repository + A hashtable that contains the repository api url to query for the list of contributors +#> +function Get-ProjectContributorList +{ + [OutputType([string[]])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [hashtable] + $Repository + ) + + # https://developer.github.com/v3/pulls/#list-pull-requests + $gitHubReleaseParam = [ordered]@{ + Authentication = 'OAuth' + Token = $script:GitHubApiKeySecure + Uri = "$($Repository.api_url)/pulls" + Method = 'Get' + Body = [ordered]@{ + state = 'closed' + base = 'dev' + } + } + $pulls = Invoke-RestMethod @gitHubReleaseParam + + [System.Collections.ArrayList]$users = @(($pulls | Select-Object user ).user.login | Select-Object -Unique) + + # There were several contributors before this project was moved to GitHub, so + # make sure they are given credit along side the contributions from GitHub. + $preGitHubContributors = @{ + PowerStig = @('jcwalker','regedit32','bgouldman','mcollera') + PowerStigDsc = @('jcwalker','regedit32','bgouldman','mcollera') + } + + foreach($user in $preGitHubContributors.($Repository.name)) + { + if($users -notcontains $user) + { + $null = $users.Add($user) + } + } + + $contributors = [System.Collections.ArrayList]@() + foreach ($user in $users) + { + # https://developer.github.com/v3/users/#get-a-single-user + $gitHubReleaseParam = [ordered]@{ + Authentication = 'OAuth' + Token = $script:GitHubApiKeySecure + Uri = "https://api.github.com/users/$user" + Method = 'Get' + } + # The GitHub release triggers the AppVeyor deployment to the Gallery. + $userDetails = Invoke-RestMethod @gitHubReleaseParam + + $null = $contributors.Add($userDetails) + } + + return $contributors | Sort-Object login +} + +#endregion +#region GitHub + +function Get-GitHubApiKey +{ + [CmdletBinding()] + param + ( + [Parameter(ParameterSetName = 'SecureFile')] + [AllowNull()] + [string] + $SecureFilePath + ) + + if ([string]::IsNullOrEmpty($SecureFilePath)) + { + $SecureFilePath = "$(Split-Path $profile)\PowerStigGitHubApi.txt" + } + elseif (-not (Test-Path -Path $SecureFilePath)) + { + throw "$SecureFilePath was not found. Please move the secure file here or provide a valid path." + } + + try + { + [System.Security.SecureString] $GitHubKeySecure = + Get-Content -Path $SecureFilePath | ConvertTo-SecureString + } + catch + { + throw "Unable to convert $SecureFilePath to a secure string." + } + + $script:GitHubApiKeySecure = $GitHubKeySecure +} + +<# + .SYNOPSIS + Get's the status of a branch based on the policies that are applied + + .PARAMETER Repository + A hashtable that contains the repository api url to query + + .PARAMETER Name + A SHA, branch name, or tag name to get the status for + + .PARAMETER WaitForSuccess + A switch that will start a loop that waits for a success status message + or a 10 minute wait timeout +#> +function Get-GitHubRefStatus +{ + [OutputType([string])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [hashtable] + $Repository, + + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter()] + [switch] + $WaitForSuccess + ) + + # https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref + $restParameters = [ordered]@{ + 'Authentication' = 'OAuth' + 'Token' = $script:GitHubApiKeySecure + 'Uri' = "$($Repository.api_url)/commits/$Name/status" + 'Method' = 'Get' + } + + [int] $i = 0 + [int] $waitCounter = 20 + [int] $waitSeconds = 30 + do + { + $response = (Invoke-RestMethod @restParameters).state + + if ($response -eq 'pending') + { + if (-not $WaitForSuccess) + { + return $response + } + Write-Host "$Name is in a pending state, starting sleep (30 seconds)." + Start-Sleep -Seconds $waitSeconds + $i ++ + } + elseif ($response -eq 'failure') + { + return $response + } + else + { + return $response + } + } + until ($response.state -eq 'success' -or $i -ge $waitCounter ) + + throw "Timeout $([timespan]::fromseconds($waitCounter*$waitSeconds))" +} + +<# + .SYNOPSIS + Create a Pull request on GitHub with the provided branch head that is + merging into the branch base. + +#> +function New-GitHubPullRequest +{ + [OutputType([PSObject])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [hashtable] + $Repository, + + [Parameter(Mandatory = $true)] + [version] + $ModuleVersion, + + [Parameter(Mandatory = $true)] + [string] + $BranchHead, + + [Parameter()] + [string] + $BranchBase = 'master' + ) + + # https://developer.github.com/v3/pulls/#create-a-pull-request + $restMethodParamList = [ordered]@{ + Authentication = 'OAuth' + Token = $script:GitHubApiKeySecure + Uri = "$($Repository.api_url)/pulls" + Method = 'Post' + Body = [ordered]@{ + title = "Release of version $ModuleVersion." + body = "Releasing version $ModuleVersion." + head = $BranchName + base = $BranchBase + } | ConvertTo-Json + } + + Invoke-RestMethod @restMethodParamList +} + +function Get-GitHubPullRequest +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true, ParameterSetName = 'one')] + [Parameter(Mandatory = $true, ParameterSetName = 'all')] + [hashtable] + $Repository, + + [Parameter(Mandatory = $true, ParameterSetName = 'one')] + [int] + $Number, + + [Parameter(Mandatory = $true, ParameterSetName = 'all')] + [string] + $BranchHead, + + [Parameter(Mandatory = $true, ParameterSetName = 'all')] + [string] + $BranchBase + ) + + # https://developer.github.com/v3/pulls/#list-pull-requests + $pullRequestParams = @{ + Authentication = 'OAuth' + Token = $script:GitHubApiKeySecure + Method = 'Get' + Uri = "$($Repository.api_url)/pulls" + } + + If ($PSCmdlet.ParameterSetName -eq 'one') + { + # https://developer.github.com/v3/pulls/#get-a-single-pull-request + $pullRequestParams.Uri = $pullRequestParams.Uri + "/$Number" + return Invoke-RestMethod @pullRequestParams + } + + $prList = Invoke-RestMethod @pullRequestParams + + return $prList | Where-Object { + $PSItem.head.ref -eq $BranchHead -and + $PSItem.base.ref -eq $BranchBase + } +} + +function Approve-GitHubPullRequest +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [psobject] + $PullRequest, + + [Parameter(Mandatory = $true)] + [string] + $CommitTitle, + + [Parameter(Mandatory = $true)] + [string] + $CommitMessage, + + [Parameter()] + [ValidateSet('merge','squash','rebase')] + [string] + $MergeMethod = 'merge' + ) + + # https://developer.github.com/v3/pulls/#merge-a-pull-request-merge-button + $restMethodParam = [ordered]@{ + Authentication = 'OAuth' + Token = $script:GitHubApiKeySecure + Uri = "$($PullRequest.url)/merge" + Method = 'Put' + Body = [ordered]@{ + commit_title = $CommitTitle + commit_message = $CommitMessage + sha = $PullRequest.head.sha + merge_method = $MergeMethod.ToLower() + } | ConvertTo-Json + } + + Invoke-RestMethod @restMethodParam +} + +function New-GitHubRelease +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [hashtable] + $Repository, + + [Parameter(Mandatory = $true)] + [string] + $TagName, + + [Parameter(Mandatory = $true)] + [string] + $Title, + + [Parameter(Mandatory = $true)] + [string] + $Description, + + [Parameter()] + [bool] + $Draft, + + [Parameter()] + [bool] + $Prerelease + ) + + # https://developer.github.com/v3/repos/releases/#create-a-release + $restMethodParam = [ordered]@{ + Authentication = 'OAuth' + Token = $script:GitHubApiKeySecure + Uri = "$($Repository.api_url)/releases" + Method = 'Post' + Body = [ordered]@{ + tag_name = $TagName + target_commitish = 'master' + name = $Title + body = $Description + draft = $Draft + prerelease = $Prerelease + } | ConvertTo-Json + } + + Invoke-RestMethod @restMethodParam +} + +#endregion + +<# + .SYNOPSIS + Starts the PowerStig release process for a given module + + .DESCRIPTION + Applies a standard process and comment structure to the release process + for a given module. At a high level, this function will: + + 1. Validate that you are trying to deploy and PowerStig module + 2. Validate that you are trying to release a higher version number than + is currently released. + 3. Create a release branch from dev and update the module version number + in all of the appropriate places. + 4. Push the release branch to GitHub and create a pull request into the + master branch. + + .PARAMETER GitRepositoryPath + The path to the git repository on your local machine. If this path is + not a valid git repository, this function will throw an error. + Additionally, if the repository remote origin is not a PowerStig project, + this function will throw an error + + .PARAMETER ModuleVersion + The version number that is injected to the different areas of the + release process. Specifically the module manifest, AppVeyor build config, + and readme release notes. + + .PARAMETER GitHubApiSecureFilePath + The path to the secured GitHub API key. If you have a file named + PowerStigGitHubApi.txt in $profile, it will automatically be loaded. + Running the following command will prompt you for your GitHub API key + and create the file for you so that you can skip this parameter. + + Read-Host "Enter Password" -AsSecureString | + ConvertFrom-SecureString | + Out-File "$(Split-Path $profile)\PowerStigGitHubApi.txt" +#> +function Start-PowerStigRelease +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string] + $GitRepositoryPath, + + [Parameter(Mandatory = $true)] + [string] + $ModuleVersion, + + [Parameter()] + [string] + $GitHubApiSecureFilePath, + + [Parameter()] + [switch] + $Continue + ) + + # Convert GitRepositoryPath into an absolute path if it is relative + if (-not ([System.IO.Path]::IsPathRooted($GitRepositoryPath))) + { + $GitRepositoryPath = Resolve-Path -Path $GitRepositoryPath + } + + Push-Location -Path $GitRepositoryPath + $beginGitBranch = Get-GitBranch + $branchName = $script:ReleaseName -f $ModuleVersion + + try + { + $repository = Get-PowerStigRepository + + if (-not $Continue) + { + if (Test-ModuleVersion -ModuleVersion $ModuleVersion) + { + Write-Verbose -Message "$ModuleVersion is greater than $($masterManifest.ModuleVersion)" + } + else + { + throw "The module version ($ModuleVersion) is not greater than currently released." + } + + New-GitReleaseBranch -BranchName $branchName + + $releaseNotes = Get-UnreleasedNotes + + if ([string]::IsNullOrEmpty($releaseNotes)) + { + throw 'There are no release notes for this release.' + } + + Update-Readme -ModuleVersion $ModuleVersion + + Update-Manifest -ModuleVersion $ModuleVersion -ReleaseNotes $releaseNotes + + Update-AppVeyorConfiguration -ModuleVersion $ModuleVersion + + # Push the release branch to GitHub + Push-GitBranch -Name $branchName -CommitMessage "Bumped version number to $ModuleVersion for release." + } + + Get-GitHubApiKey -SecureFilePath $GitHubApiSecureFilePath + + # Get the Dev Banch status. Wait until it is success or failure + $gitHubRefStatusParam = [ordered]@{ + 'Repository' = $repository + 'Name' = $branchName + 'WaitForSuccess' = $true + } + $gitHubReleaseBranchStatus = Get-GitHubRefStatus @gitHubRefStatusParam + + if ($gitHubReleaseBranchStatus -eq 'success') + { + $pullRequestParameters = @{ + Repository = $Repository + ModuleVersion = $ModuleVersion + BranchHead = $branchName + } + $pullRequest = New-GitHubPullRequest @pullRequestParameters + + $gitHubRefStatusParam = [ordered]@{ + Repository = $repository + Name = $pullRequest.head.sha + WaitForSuccess = $true + } + $gitHubPullRequestStatus = Get-GitHubRefStatus @gitHubRefStatusParam + + if ($gitHubPullRequestStatus -eq 'success') + { + Write-Output "Pull request for $branchName success." + } + else + { + throw 'Pull Request build failed, Aborting release' + } + } + else + { + throw "$branchName is currently failing and cannot be merged into Master." + } + } + finally + { + Write-Verbose -Message 'Reverting to initial location' + Pop-Location + Write-Verbose -Message 'Reverting to initial branch' + Set-GitBranch -Branch $beginGitBranch -SkipPull + } +} + +<# + .SYNOPSIS + Completes the PowerStig release process for a given module that was + created using the Start-PowerStigRelease function. + + .DESCRIPTION + Applies a standard process and comment structure to the release process + for a given module. At a high level, this function will: + + 1. Validate that you are trying to deploy and PowerStig module + 2. Approve the pull request on GitHub + 3. Create a GitHub Release (Triggers an AppVeyor deployment) + 4. Cleans up the release branch from the local and remote repository + + .PARAMETER GitRepositoryPath + The path to the git repository on your local machine. If this path is + not a valid git repository, this function will throw an error. + Additionally, if the repository remote origin is not a PowerStig project, + this function will throw an error + + .PARAMETER ModuleVersion + The version number that is injected to the different areas of the + release process. Specifically the module manifest, AppVeyor build config, + and readme release notes. + + .PARAMETER GitHubApiSecureFilePath + The path to the secured GitHub API key. If you have a file named + PowerStigGitHubApi.txt in $profile, it will automatically be loaded. + Running the following command will prompt you for your GitHub API key + and create the file for you so that you can skip this parameter. + + Read-Host "Enter Password" -AsSecureString | + ConvertFrom-SecureString | + Out-File "$(Split-Path $profile)\PowerStigGitHubApi.txt" +#> +function Complete-PowerStigRelease +{ + param + ( + [Parameter(Mandatory = $true)] + [string] + $GitRepositoryPath, + + [Parameter(Mandatory = $true)] + [string] + $ModuleVersion, + + [Parameter()] + [string] + $GitHubApiSecureFilePath + ) + + # Convert GitRepositoryPath into an absolute path if it is relative + if (-not ([System.IO.Path]::IsPathRooted($GitRepositoryPath))) + { + $GitRepositoryPath = Resolve-Path -Path $GitRepositoryPath + } + + Push-Location -Path $GitRepositoryPath + $beginGitBranch = Get-GitBranch + $branchName = $script:ReleaseName -f $ModuleVersion + + try + { + $repository = Get-PowerStigRepository + + Get-GitHubApiKey -SecureFilePath $GitHubApiSecureFilePath + + $pullRequestParam = @{ + Repository = $repository + BranchBase = 'master' + BranchHead = $branchName + } + $pullRequest = Get-GitHubPullRequest @pullRequestParam + + $approvePullRequestParam = [ordered]@{ + PullRequest = $pullRequest + CommitTitle = 'Release' + CommitMessage = 'This PR is automatically completed.' + MergeMethod = 'merge' + } + $null = Approve-GitHubPullRequest @approvePullRequestParam + + # Get the manifest release notes to add to release + $manifestPath = (Get-ChildItem -Path $PWD -Filter "*.psd1").FullName + $releaseNotes = (Import-PowerShellDataFile -Path $manifestPath).PrivateData.PSData.ReleaseNotes + + $gitHubReleaseParams = @{ + Repository = $repository + TagName = $branchName -replace 'Release', 'PSGallery' + Title = "Release of version $(($branchName -Split '-')[0])" + Description = $releaseNotes + } + # The GitHub release triggers the AppVeyor deployment to the Gallery. + $null = New-GitHubRelease @gitHubReleaseParams + + Remove-GitReleaseBranch -BranchName $branchName + } + finally + { + Write-Verbose -Message 'Reverting to initial location' + Pop-Location -Verbose + Write-Verbose -Message 'Reverting to initial branch' + Set-GitBranch -Branch $beginGitBranch -SkipPull + } +} + +<# + .SYNOPSIS + Completes the PowerStig release process for a given module that was + created using the Start-PowerStigRelease function. + + .DESCRIPTION + + + .PARAMETER GitRepositoryPath + The path to the git repository on your local machine. If this path is + not a valid git repository, this function will throw an error. + Additionally, if the repository remote origin is not a PowerStig project, + this function will throw an error + + .PARAMETER PullRequestNumber + The pull request number to complete. + + .PARAMETER GitHubApiSecureFilePath + The path to the secured GitHub API key. If you have a file named + PowerStigGitHubApi.txt in $profile, it will automatically be loaded. + Running the following command will prompt you for your GitHub API key + and create the file for you so that you can skip this parameter. + + Read-Host "Enter Password" -AsSecureString | + ConvertFrom-SecureString | + Out-File "$(Split-Path $profile)\PowerStigGitHubApi.txt" +#> +function Complete-PowerStigDevMerge +{ + param + ( + [Parameter(Mandatory = $true)] + [string] + $GitRepositoryPath, + + [Parameter(Mandatory = $true)] + [int] + $PullRequestNumber, + + [Parameter()] + [string] + $GitHubApiSecureFilePath + ) + + # Convert GitRepositoryPath into an absolute path if it is relative + if (-not ([System.IO.Path]::IsPathRooted($GitRepositoryPath))) + { + $GitRepositoryPath = Resolve-Path -Path $GitRepositoryPath + } + + Push-Location -Path $GitRepositoryPath + + try + { + $repository = Get-PowerStigRepository + + Get-GitHubApiKey -SecureFilePath $GitHubApiSecureFilePath + + $pullRequestParam = @{ + Repository = $repository + Number = $PullRequestNumber + } + $pullRequest = Get-GitHubPullRequest @pullRequestParam + + $approvePullRequestParam = [ordered]@{ + PullRequest = $pullRequest + CommitTitle = 'Merged into dev' + CommitMessage = 'Accepted PR' + MergeMethod = 'squash' + } + $pullRequest = Approve-GitHubPullRequest @approvePullRequestParam + + Set-GitBranch -Branch dev + + Update-Readme -Repository $repository + + Push-GitBranch -Name 'dev' -CommitMessage "Updated contributor list" + } + catch + { + Pop-Location + } +} + +Export-ModuleMember -Function '*-PowerStigRelease', '*-PowerStigDevMerge' diff --git a/Tests/CompositeResourceFilter.psd1 b/Tools/TestHelper/CompositeResourceFilter.psd1 similarity index 100% rename from Tests/CompositeResourceFilter.psd1 rename to Tools/TestHelper/CompositeResourceFilter.psd1 diff --git a/Tools/TestHelper/Data/readme.md b/Tools/TestHelper/Data/readme.md new file mode 100644 index 000000000..ac66cbe53 --- /dev/null +++ b/Tools/TestHelper/Data/readme.md @@ -0,0 +1,51 @@ +# Sample XCCDF + +The Sample xccdf base content is designed to be reusable across all unit and integration tests using +PowerShell composite formatting. + +## Index +The samplexccdf contains 4 indexes that you can inject data into for testing. If any of these fields +are not applicable to your tests, then simply pass in an empty string. Since these are indexed, the +order is important. + +* 0 - The STIG Title +* 1 - release-info +* 2 - version +* 3 - groups + If you do not want to provide the contents of the group element, you can use the sample group + + +## How to use the content in your tests that use the benchmark element. + + # Import the base benchmark xml string data. + $BaseXccdfContent = Get-Content -Path "$PSScriptRoot\..\..\..\sampleXccdf.xml.txt" + + # Create a test drive File + $TestFile = "TestDrive:\TextData.xml" + + # Inject the data you need for your tests using the index above + $BaseXccdfContent -f $title.key,'','','' | Out-File $TestFile + + +# How to use the content in your tests that use the group elements. + + # Import the base benchmark xml string data. + $BaseXccdfContent = Get-Content -Path "$PSScriptRoot\..\..\..\sampleXccdf.xml.txt" + + # Create a test drive File + $TestFile = "TestDrive:\TextData.xml" + + # Create a sample group that structured like your test target + $group = ' + + Rule Title + + + The check content you want to test goes here. + + + + + ' + # Inject the data you need for your tests using the index above + $BaseXccdfContent -f $title.key,'','',$group | Out-File $TestFile diff --git a/Tools/TestHelper/Data/sampleXccdf.xml.readme.txt b/Tools/TestHelper/Data/sampleXccdf.xml.readme.txt new file mode 100644 index 000000000..47020bfe2 --- /dev/null +++ b/Tools/TestHelper/Data/sampleXccdf.xml.readme.txt @@ -0,0 +1,46 @@ +The Sample xccdf base content is designed to be reusable across all unit and integration tests using +PowerShell composite formatting. + +Index + +0 - The STIG Title +1 - release-info +2 - version +3 - groups +4 - id - an element of the Benchmark node + + +# How to use the content in your tests that use the benchmark element. + + # Import the base benchmark xml string data. + $BaseXccdfContent = Get-Content -Path "$PSScriptRoot\..\..\..\sampleXccdf.xml.txt" + + # Create a test drive File + $TestFile = "TestDrive:\TextData.xml" + + # Inject the data you need for your tests using the index above + $BaseXccdfContent -f $title.key,'','','' | Out-File $TestFile + + +# How to use the content in your tests that use the group elements. + + # Import the base benchmark xml string data. + $BaseXccdfContent = Get-Content -Path "$PSScriptRoot\..\..\..\sampleXccdf.xml.txt" + + # Create a test drive File + $TestFile = "TestDrive:\TextData.xml" + + # Create a sample group that structured like your test target + $group = ' + + Rule Title + + + The check content you want to test goes here. + + + + + ' + # Inject the data you need for your tests using the index above + $BaseXccdfContent -f $title.key,'','',$group | Out-File $TestFile diff --git a/Tools/TestHelper/Data/sampleXccdf.xml.txt b/Tools/TestHelper/Data/sampleXccdf.xml.txt new file mode 100644 index 000000000..44aee439c --- /dev/null +++ b/Tools/TestHelper/Data/sampleXccdf.xml.txt @@ -0,0 +1,23 @@ + + + + accepted + {0} + The {0} (STIG) is published as a tool to improve the security of Department of Defense (DoD) information systems. Comments or proposed revisions to this document should be sent via e-mail to the following address: disa.stig_spt@mail.mil. + Developed_by + + DISA + STIG.DOD.MIL + + {1} + {2} +{3} + diff --git a/Tools/TestHelper/Data/samplegroup.xml.txt b/Tools/TestHelper/Data/samplegroup.xml.txt new file mode 100644 index 000000000..c7d0499a8 --- /dev/null +++ b/Tools/TestHelper/Data/samplegroup.xml.txt @@ -0,0 +1,25 @@ + + {1} + <GroupDescription></GroupDescription> + + XXXX-YY-000002 + {2} + <VulnDiscussion>{3}</VulnDiscussion><FalsePositives></FalsePositives><FalseNegatives></FalseNegatives><Documentable>false</Documentable><Mitigations></Mitigations><SeverityOverrideGuidance></SeverityOverrideGuidance><PotentialImpacts></PotentialImpacts><ThirdPartyTools></ThirdPartyTools><MitigationControl></MitigationControl><Responsibility></Responsibility><IAControls></IAControls> + + + DPMS Target Technology + DISA + DPMS Target + Technology + 2350 + + CCE--12345-6 + CCI-123456 + {4} + + + + {5} + + + diff --git a/Tools/TestHelper/TestHelper.psm1 b/Tools/TestHelper/TestHelper.psm1 new file mode 100644 index 000000000..af0cd6d6a --- /dev/null +++ b/Tools/TestHelper/TestHelper.psm1 @@ -0,0 +1,449 @@ + +<# + .SYNOPSIS + Used to process the Check-Content test strings the same way that the SplitCheckContent + static method does in the the STIG base class. + .PARAMETER CheckContent + Strings to process. +#> +function Split-TestStrings +{ + [OutputType([string[]])] + param + ( + [Parameter(mandatory = $true)] + [string] + $CheckContent + ) + + $CheckContent -split '\n' | + Select-String -Pattern "\w" | + ForEach-Object { $PSitem.ToString().Trim() } +} + +<# + .SYNOPSIS + Used to validate an xml file against a specified schema + + .PARAMETER XmlFile + Path and file name of the XML file to be validated + + .PARAMETER Xml + An already loaded System.Xml.XmlDocument + + .PARAMETER SchemaFile + Path of XML schema used to validate the XML document + + .PARAMETER ValidationEventHandler + Script block that is run when an error occurs while validating XML + + .EXAMPLE + Test-XML -XmlFile C:\source\test.xml -SchemaFile C:\Source\test.xsd + + .EXAMPLE + $xmlobject = Get-StigData -OsVersion 2012R2 -OsRole MemberServer + Test-XML -Xml $xmlobject -SchemaFile C:\Source\test.xsd +#> +function Test-Xml +{ + [OutputType([Boolean])] + [CmdletBinding()] + param + ( + [Parameter(ValueFromPipeline = $true, Mandatory = $true, ParameterSetName = 'File')] + [string] + $XmlFile, + + [Parameter(ValueFromPipeline = $true, Mandatory = $true, ParameterSetName = 'Object')] + [xml] + $Xml, + + [Parameter(Mandatory = $true)] + [string] + $SchemaFile, + + [scriptblock] + $ValidationEventHandler = { Throw $PSItem.Exception } + ) + + If (-not (Test-Path -Path $SchemaFile)) + { + Throw "Schema file not found" + } + + $schemaReader = New-Object System.Xml.XmlTextReader $SchemaFile + $schema = [System.Xml.Schema.XmlSchema]::Read($schemaReader, $ValidationEventHandler) + + If ($PsCmdlet.ParameterSetName -eq "File") + { + $xml = New-Object System.Xml.XmlDocument + $xml.Load($XmlFile) + } + + $xml.Schemas.Add($schema) | Out-Null + $xml.Validate($ValidationEventHandler) +} + +<# + .SYNOPSIS + Creates a sample xml docuement that injects class specifc data + + .PARAMETER TestFile + The test rule to merge into the data +#> +function Get-TestStigRule +{ + [CmdletBinding(DefaultParameterSetName = 'UseExisting')] + param + ( + [Parameter(Parametersetname = 'UseExisting')] + [string] + $XccdfTitle = '(Technology) Security Technical Implementation Guide', + + [Parameter(Parametersetname = 'UseExisting')] + [string] + $XccdfRelease = 'Release: 1 Benchmark Date: 1 Jan 1970', + + [Parameter(Parametersetname = 'UseExisting')] + [string] + $XccdfVersion = '2', + + [Parameter(Parametersetname = 'UseExisting')] + [string] + $XccdfId = 'Technology_Target', + + [Parameter(Parametersetname = 'UseExisting')] + [string] + $CheckContent = 'This is a string of text that tells an admin what item to check to verify compliance.', + + [Parameter(Parametersetname = 'UseExisting')] + [string] + $GroupId = 'V-1000', + + [Parameter(Parametersetname = 'UseExisting')] + [string] + $GroupTitle = 'Sample Title', + + [Parameter(Parametersetname = 'UseExisting')] + [string] + $GroupRuleTitle = 'A more descriptive title.', + + [Parameter(Parametersetname = 'UseExisting')] + [string] + $GroupRuleDescription = 'A description of what this vulnerability addresses and how it mitigates the threat.', + + [Parameter(Parametersetname = 'UseExisting')] + [string] + $FixText = 'This is a string of text that tells an admin how to fix an item if it is not currently configured properly and ignored by the parser', + + [Parameter(Parametersetname = 'UseExisting')] + [Parameter(Parametersetname = 'FileProvided')] + [switch] + $ReturnGroupOnly, + + [Parameter(Mandatory = $true, Parametersetname = 'FileProvided')] + [string] + $FilePathToGroupElementText + ) + + # If a file path is provided, override the default sample group element text. + if ( $FilePathToGroupElementText ) + { + try + { + $groupElement = Get-Content -Path $FilePathToGroupElementText -Raw + } + catch + { + throw "$FilePathToGroupElementText was not found" + } + } + else + { + # Get the samplegroup element text and merge in the parameter strings + $groupElement = Get-Content -Path "$PSScriptRoot\data\sampleGroup.xml.txt" -Encoding UTF8 -Raw + $groupElement = $groupElement -f $GroupId, $GroupTitle, $RuleTitle, $RuleDescription, $FixText, $CheckContent + } + + # Get and merge the group element data into the xccdf xml document and create an xml object to return + $xmlDocument = Get-Content -Path "$PSScriptRoot\data\sampleXccdf.xml.txt" -Encoding UTF8 -Raw + [xml] $xmlDocument = $xmlDocument -f $XccdfTitle, $XccdfRelease, $XccdfVersion, $groupElement, $XccdfId + + # Some tests only need the group to test functionality. + if ($ReturnGroupOnly) + { + return $xmlDocument.Benchmark.group + } + + return $xmlDocument +} + +<# + .SYNOPSIS + Used to retrieve an array of Stig class base methods and optionally add child class methods + for purpose of testing methods + + .PARAMETER ChildClassMethodNames + An array of child class method to add to the class base methods + + .EXAMPLE + $RegistryRuleClassMethods = Get-StigBaseMethods -ChildClassMethodsNames @('SetKey','SetName') +#> +Function Get-StigBaseMethods +{ + [OutputType([System.Collections.ArrayList])] + param + ( + [Parameter()] + [system.array] + $ChildClassMethodNames, + + [Parameter()] + [switch] + $Static + ) + + if ( $Static ) + { + $stigClassMethodNames = @('Equals', 'new', 'ReferenceEquals', 'SplitCheckContent', + 'GetRuleTypeMatchList', 'GetFixText') + } + else + { + $objectClassMethodNames = @('Equals', 'GetHashCode', 'GetType', 'ToString') + $stigClassMethodNames = @('Clone', 'IsDuplicateRule', 'SetDuplicateTitle', , 'SetStatus', + 'SetIsNullOrEmpty', 'SetOrganizationValueRequired', 'GetOrganizationValueTestString', + 'ConvertToHashTable', 'SetStigRuleResource', 'IsHardCoded', 'GetHardCodedString', + 'IsHardCodedOrganizationValueTestString', 'GetHardCodedOrganizationValueTestString', + 'IsExistingRule') + + $stigClassMethodNames += $ObjectClassMethodNames + } + + if ( $ChildClassMethodNames ) + { + $stigClassMethodNames += $ChildClassMethodNames + } + + return ( $stigClassMethodNames | Select-Object -Unique ) +} + +function Format-RuleText +{ + [CmdletBinding()] + Param + ( + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [string[]] + $RuleText + ) + + $return = ($RuleText -split '\n' | + Select-String -Pattern "\w" | + ForEach-Object { $PSitem.ToString().Trim() }) + + return $return +} + + +function Get-RequiredStigDataVersion +{ + [CmdletBinding()] + param() + + $Manifest = Import-PowerShellDataFile -Path "$relDirectory\$moduleName.psd1" + + return $Manifest.RequiredModules.Where({$PSItem.ModuleName -eq 'PowerStig'}).ModuleVersion +} + +function Get-StigDataRootPath +{ + param ( ) + + $projectRoot = Split-Path -Path (Split-Path -Path $PsScriptRoot) + return Join-Path -Path $projectRoot -Child 'StigData' +} + +<# + .SYNOPSIS + Get all of the version files to test + + .PARAMETER CompositeResourceName + The name of the composite resource used to filter the results +#> +function Get-StigFileList +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string] + $CompositeResourceName + ) + + # + $stigFilePath = Get-StigDataRootPath + $stigVersionFiles = Get-ChildItem -Path $stigFilePath -Exclude "*.org*" + + $stigVersionFiles +} + +<# + .SYNOPSIS + Returns a list of stigs for a given resource. This is used in integration testign by looping + through every valide STIG found in the StigData directory. + + .PARAMETER CompositeResourceName + The resource to filter the results + + .PARAMETER Filter + Parameter description + +#> +function Get-StigVersionTable +{ + [OutputType([psobject])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string] + $CompositeResourceName, + + [Parameter()] + [string] + $Filter + ) + + $include = Import-PowerShellDataFile -Path $PSScriptRoot\CompositeResourceFilter.psd1 + + $path = "$(Get-StigDataRootPath)\Processed" + + $versions = Get-ChildItem -Path $path -Exclude "*.org.*", "*.xsd" -Include $include.$CompositeResourceName -File -Recurse + + $versionTable = @() + foreach ($version in $versions) + { + if ($version.Basename -match $Filter) + { + $stigDetails = $version.BaseName -Split "-" + + $versionTable += @{ + 'Technology' = $stigDetails[0] + 'TechnologyVersion' = $stigDetails[1] + 'TechnologyRole' = $stigDetails[2] + 'StigVersion' = $stigDetails[3] + 'Path' = $version.fullname + } + } + } + + return $versionTable +} + +<# + .SYNOPSIS + Using an AST, it returns the name of a configuration in the composite resource schema file. + + .PARAMETER FilePath + The full path to the resource schema module file +#> +function Get-ConfigurationName +{ + [CmdletBinding()] + [OutputType([string[]])] + param + ( + [Parameter(Mandatory = $true)] + [String] + $FilePath + ) + + $AST = [System.Management.Automation.Language.Parser]::ParseFile( + $FilePath, [ref] $null, [ref] $Null + ) + + # Get the Export-ModuleMember details from the module file + $ModuleMember = $AST.Find( { + $args[0] -is [System.Management.Automation.Language.ConfigurationDefinitionAst]}, $true) + + return $ModuleMember.InstanceName.Value +} + +<# + .SYNOPSIS + Returns the list of StigVersion nunmbers that are defined in the ValidateSet parameter attribute + + .PARAMETER FilePath + THe full path to the resource to read from +#> +function Get-StigVersionParameterValidateSet +{ + [OutputType([string])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string] + $FilePath + ) + + $compositeResource = Get-Content -Path $FilePath -Raw + + $AbstractSyntaxTree = [System.Management.Automation.Language.Parser]::ParseInput( + $compositeResource, [ref]$null, [ref]$null) + + $params = $AbstractSyntaxTree.FindAll( + {$args[0] -is [System.Management.Automation.Language.ParameterAst]}, $true) + + # Filter the specifc ParameterAst + $paramToUpdate = $params | + Where-Object {$PSItem.Name.VariablePath.UserPath -eq 'StigVersion'} + + # Get the specifc parameter attribute to update + $validate = $paramToUpdate.Attributes.Where( + {$PSItem.TypeName.Name -eq 'ValidateSet'}) + + return $validate.PositionalArguments.Value +} + +<# + .SYNOPSIS + Get a unique list of valid STIG versions from the StigData + + .PARAMETER TechnologyRoleFilter + The technology role to filter the results +#> + +function Get-ValidStigVersionNumbers +{ + [OutputType([string])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string] + $TechnologyRoleFilter + ) + + $versionNumbers = (Get-Stiglist | + Where-Object {$PSItem.TechnologyRole -match $TechnologyRoleFilter} | + Select-Object StigVersion -ExpandProperty StigVersion -Unique ) + + return $versionNumbers +} + +Export-ModuleMember -Function @( + 'Split-TestStrings', + 'Get-StigDataRootPath', + 'Test-Xml', + 'Get-TestStigRule', + 'Get-StigBaseMethods', + 'Format-RuleText', + 'Get-PowerStigVersionFromManifest', + 'Get-StigVersionTable', + 'Get-ConfigurationName', + 'Get-StigVersionParameterValidateSet', + 'Get-ValidStigVersionNumbers' +) diff --git a/Tools/WikiPages/WikiPages.psm1 b/Tools/WikiPages/WikiPages.psm1 new file mode 100644 index 000000000..fc99b1ffe --- /dev/null +++ b/Tools/WikiPages/WikiPages.psm1 @@ -0,0 +1,626 @@ +#region Strings + +$header = @' +# {0} Class + +{1} +'@ + +$advancedHeader = @' +# {0} Syntax +'@ + +$constructorHeader = @' + +## Constructors + +| Name | Description | +|-|-| +'@ + +$advancedconstructorHeader = @' + +## Constructors + +'@ + +$advancedConstructor = @' +{0} + +```PowerShell +{1} +``` + +### Parameters + +| Name | Type | Description | +|-|-|-| +{2} + +'@ + +$propertiesHeader = @' + +## Properties + +| Name | Description | +|-|-| +'@ + +$advancedPropertyHeader = @' +## {0} + +```PowerShell +{1} +``` + +'@ + +$methodsHeader = @' + +## Methods + +| Name | Description | +|-|-| +'@ + +$advancedMethod = @' +{0} + +```PowerShell +{1} +``` + +### Parameters + +{2} + +'@ + +$examplesHeader = @' + +## Example +'@ + +$powershellCodeSnip = @' + +'@ + +#endregion + +function Get-WikiContent +{ + [OutputType([hashtable])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string] + $Path + ) + + $input = Get-Content -Path $Path -Raw + + [System.Management.Automation.Language.Token[]] $tokens = $null + + $ast = [System.Management.Automation.Language.Parser]::ParseInput( + $input, [ref]$tokens, [ref]$null) + + $return = [System.Collections.Specialized.OrderedDictionary]@{} + <# + 1. Get the Type Definition and Build a hashtable for the + a. Class + b. Constructors + c. Properties + d. Methods + + 2. Modify the Class definition in memory to replace key words with the function key word + 3. Parse the updated script file + 4. Search for FunctionDefinitionAst to get access to GetHelpContent + 5. Update each item in the hashtable from step one with it's help content. + #> + + $TypeDefinitionAst = $ast.FindAll( + {$args[0] -is [System.Management.Automation.Language.TypeDefinitionAst ]}, $true) + + $return.Add( + "Class.$($TypeDefinitionAst.Extent.StartLineNumber)", + @{ + Name = $TypeDefinitionAst.Name + } + ) + + #Properties + foreach ($entry in $TypeDefinitionAst.Members.Where( {$PSItem.PropertyType -ne $null})) + { + $details = @{ + Name = $entry.Name + LineNumber = $entry.Extent.StartLineNumber + PropertyType = $entry.PropertyType.TypeName.FullName + PropertyAttributes = $entry.PropertyAttributes + Link = "[Property.$($entry.Name)]: Stig.OrganizationalSettings.Class.Syntax#property$($entry.Name.ToLower())" + } + $return.Add("Property.$($entry.Extent.StartLineNumber)", $details) + } + + # Constructors + foreach ($entry in $TypeDefinitionAst.Members.Where( {$PSItem.Name -eq $TypeDefinitionAst.Name})) + { + $details = @{ + Name = $entry.Name + LineNumber = $entry.Extent.StartLineNumber + MethodAttributes = $entry.MethodAttributes + Parameters = $entry.Parameters + } + + if ($details.Parameters.Count -gt 0) + { + $parameterType = $entry.Parameters.Attributes.TypeName.Name -join "." + $linkpath = "Constructor.$parameterType" + } + else + { + $linkpath = "Constructor" + } + + $details.Add('Link', "[$linkpath]: Stig.$($TypeDefinitionAst.Name).Class.Syntax#$($linkpath.ToLower() -replace "\.",'')") + $return.Add("Constructor.$($entry.Extent.StartLineNumber)", $details) + } + + #Methods + foreach ($entry in $TypeDefinitionAst.Members.Where( {$PSItem.Name -ne $TypeDefinitionAst.Name -and + $PSItem.MethodAttributes -ne $null})) + { + $details = @{ + Name = $entry.Name + LineNumber = $entry.Extent.StartLineNumber + PropertyType = $entry.PropertyType.TypeName.FullName + MethodAttributes = $entry.MethodAttributes + ReturnType = $entry.ReturnType.TypeName.FullName + Parameters = $entry.Parameters + } + + if ($details.Parameters.Count -gt 0) + { + $parameterType = $entry.Parameters.Attributes.TypeName.Name + $linkpath = "Method.$($entry.Name).$parameterType" + } + else + { + $linkpath = "Method.$($entry.Name)" + } + $details.Add('Link', "[$linkpath]: Stig.OrganizationalSettings.Class.Syntax#$($linkpath.ToLower() -replace "\.",'')") + $return.Add("Method.$($entry.Extent.StartLineNumber)", $details) + } + + # Update the content with the function keyword to extract help content + $classAndConstructorFilter = "^(Class)?\s+$($TypeDefinitionAst.Name)(\s+:\s+\w+)?" + $methodFilter = '^\s*(static)?\s*\[\w*(\[(\s*)?\])?\]\s*' + + $inputModified = (($input -split "`n") -replace $classAndConstructorFilter, "function $($TypeDefinitionAst.Name)") + <# + I couldn't come up with a regex that updated the methods without also + stepping on the class properties, so I just apply the regex against directly + to the method line number. + #> + foreach ($method in $return.Keys.Where( {$PSItem -match "^Method\."})) + { + $lineNumber = $return.$method.LineNumber - 1 + $inputModified[$lineNumber] = $inputModified[$lineNumber] -replace $methodFilter, "function " + } + + $astModified = [System.Management.Automation.Language.Parser]::ParseInput( + $inputModified, [ref]$tokens, [ref]$null) + + $FunctionDefinitionAst = $astModified.FindAll( + {$args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst]}, $true) + + Foreach ($Function in $FunctionDefinitionAst) + { + $keyName = ($return.Keys.Where( {$PSitem -match ".\.$($Function.Extent.StartLineNumber)"}))[0] + + [void] $return[$keyName].Add('Help', $Function.GetHelpContent()) + } + + return $return +} + +<# + .SYNOPSIS + The help content is returned as typed, so this function will merge everything + back into individual sentences and whole lines. +#> +function Format-HelpString +{ + [OutputType([string])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [string] + $String, + + [Parameter()] + [switch] + $SingleLine + ) + + $returnString = $String -split "`n" -join " " + + if ($SingleLine) + { + $returnString = $returnString -replace "\.\s+", ". " + } + else + { + $returnString = $returnString -replace "\.\s+", ".`n" + } + + return $returnString.TrimEnd() +} + +function New-WikiPage +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [string] + $Path, + + [Parameter(Mandatory = $true)] + [string] + $OutputPath + ) + + begin + { + } + + process + { + $wikiContent = Get-WikiContent -Path $Path + + $helpFileStrings = [System.Collections.ArrayList]@() + $advancedHelpFileStrings = [System.Collections.ArrayList]@() + $linkTable = [System.Collections.ArrayList]@('') + + #region Header + + $classKey = ($wikiContent.Keys.Where( {$PSitem -match 'Class\.'}))[0] + #$classDefinition = $TypeDefinitionAstForHelp.Where({$PSItem.Name -eq $TypeDefinitionAst.Name}) + #$helpString = Get-HelpString -FunctionDefinitionAst $classDefinition -Property DESCRIPTION + + $class = $wikiContent[$classKey] + $description = Format-HelpString -String $class.Help.Description + $null = $helpFileStrings.Add(($header -f $class.Name, $description)) + $null = $advancedHelpFileStrings.Add($advancedHeader -f $class.Name) + #endregion + + #region Constructors + $constructorKeyList = $wikiContent.Keys.Where( {$PSitem -match 'Constructor\.'}) + $null = $helpFileStrings.Add($constructorHeader) + $null = $advancedHelpFileStrings.Add($advancedconstructorHeader) + + foreach ($constructorKey in $constructorKeyList.GetEnumerator()) + { + $constructor = $wikiContent[$constructorKey] + + $link = ($constructor.Link -split "\:")[0] + $synopsis = Format-HelpString -String $constructor.Help.Synopsis -SingleLine + $parameterString = $constructor.Parameters.Attributes.TypeName.Name -join "," + $null = $helpFileStrings.Add("| [$($constructor.name)($parameterString)]$link | $synopsis |") + $null = $linkTable.Add($constructor.Link) + # Advanced syntax + $description = Format-HelpString -String $constructor.Help.Description -SingleLine + $constructorSyntax = "$($constructor.MethodAttributes) $($constructor.name)($($constructor.Parameters -Join ', '))" + + $parameterList = $constructor.Parameters + $parameterhelp = $constructor.Help.Parameters + + $parameterHelpString = @() + foreach ($parameter in $parameterList.GetEnumerator()) + { + $parameterName = $parameter.Name.VariablePath.UserPath + $parameterType = $parameter.StaticType.FullName + $parameterDescription = Format-HelpString -String $parameterhelp.$($parameterName.ToUpper()) -SingleLine + + $parameterHelpString += "| $parameterName | $parameterType | $parameterDescription |" + } + $parameterHelpString = $parameterHelpString | Out-String + $null = $advancedHelpFileStrings.Add( + ($advancedConstructor -f $description, $constructorSyntax, $parameterHelpString) + ) + } + + #endregion + + #region Properties + $propertyKeyList = $wikiContent.Keys.Where( {$PSitem -match 'Property\.'}) + [void]$helpFileStrings.Add($propertiesHeader) + + Foreach ($propertyKey in $propertyKeyList.GetEnumerator()) + { + $property = $wikiContent[$propertyKey] + + $link = ($property.Link -split "\:")[0] + $helpString = $class.Help.Parameters[$property.Name.ToUpper()] + $synopsis = Format-HelpString -String $helpString -SingleLine + $propertyEntry = "| {0} | {1} |" -f "[$($property.Name)]$link", $synopsis + $null = $helpFileStrings.Add($propertyEntry) + $null = $linkTable.Add($property.Link) + + + $advancedPropertTitle = $link -replace '\[|\]', '' + $advancedPropertySyntax = "$($property.PropertyAttributes) $($property.PropertyType) {get; set;}" + $null = $advancedHelpFileStrings.Add( + ($advancedPropertyHeader -f $advancedPropertTitle,$advancedPropertySyntax) + ) + } + #endregion + + #region Methods + $methodKeyList = $wikiContent.Keys.Where( {$PSitem -match 'Method\.'}) + [void]$helpFileStrings.Add($methodsHeader) + + Foreach ($methodKey in $methodKeyList.GetEnumerator()) + { + $method = $wikiContent[$methodKey] + $link = ($method.Link -split "\:")[0] + $synopsis = Format-HelpString -String $method.Help.Synopsis -SingleLine + + $parameterString = $method.Parameters.Attributes.TypeName.Name -join "," + $methodEntry = "| [$($method.name)($parameterString)]$link | $synopsis |" + [void] $helpFileStrings.Add($methodEntry) + [void] $linkTable.Add($method.Link) + + + '| Name | Type | Description |' + '|-|-|-|' + } + #endregion + <# + #region Examples + + [void]$helpFileStrings.Add($examplesHeader) + + foreach($example in $examples) + { + "```PowerShell`n$example`n``` " + } + #endregion + + #> + + + [void]$helpFileStrings.Add($linkTable) + + $file = Get-Item -Path $path + + $helpFileStrings | Out-File -FilePath $OutputPath\"$($file.BaseName).Class.md" + $advancedHelpFileStrings | Out-File -FilePath $OutputPath\"$($file.BaseName).Class.Syntax.md" + } + + end + { + } +} + +function Get-DscCompositeWikiContent +{ + [OutputType([hashtable])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string] + $Path + ) + + $input = Get-Content -Path $Path -Raw + + [System.Management.Automation.Language.Token[]] $tokens = $null + + $ast = [System.Management.Automation.Language.Parser]::ParseInput( + $input, [ref]$tokens, [ref]$null) + + +} + +function ConvertTo-Function +{ + [OutputType([System.Management.Automation.Language.FunctionDefinitionAst])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string] + $Path + ) + $configurationContent = Get-Content -Path $Path -Raw + + # Update the content with the function keyword to extract help content + $ConfigurationKeyword = "^Configuration(?=\s+\w+)" + $inputModified = (($configurationContent -split "`n") -replace $ConfigurationKeyword, "function") + + + [System.Management.Automation.Language.Token[]] $tokens = $null + $astModified = [System.Management.Automation.Language.Parser]::ParseInput( + $inputModified, [ref]$tokens, [ref]$null) + + $FunctionDefinitionAst = $astModified.FindAll( + {$args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst]}, $true) + + return $FunctionDefinitionAst +} + +function Get-ConfigurationParameters +{ + [OutputType([hashtable[]])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.Management.Automation.Language.FunctionDefinitionAst] + $FunctionDefinitionAst + ) + + $help = $FunctionDefinitionAst.GetHelpContent() + $parameterList = [System.Collections.ArrayList]@() + foreach ($parameter in $FunctionDefinitionAst.body.ParamBlock.Parameters) + { + $parameterName = $parameter.Name.VariablePath.UserPath + + $parametermandatory = $false + $parameterAttribute = $parameter.Attributes.Where( {$PSItem.TypeName.Name -eq 'Parameter'}) + If ($parameterAttribute.NamedArguments.Where( {$PSItem.ArgumentName -eq 'Mandatory'}).Argument.VariablePath.UserPath -eq $true) + { + $parametermandatory = $true + } + + $parameterObject = @{ + Name = $parameterName + DataType = $parameter.StaticType.Name + Attribute = $parametermandatory + } + + $parameterSetAttribute = $parameter.Attributes.Where( {$PSItem.TypeName.Name -eq 'ValidateSet'}) + if ($null -ne $parameterSetAttribute) + { + $null = $parameterObject.Add( + 'AllowedValues', $parameterSetAttribute.PositionalArguments.Value -join ',' + ) + } + + $null = $parameterObject.Add( + 'Description', (Format-HelpString -String $help.Parameters.($parameterName.ToUpper()) -SingleLine ) + ) + $null = $parameterList.Add($parameterObject) + } + return $parameterList +} + +function Get-ConfigurationExamples +{ + [OutputType([hashtable[]])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string] + $ConfigurationName, + + [Parameter()] + [string] + $Path + ) + + $basePath = 'https://github.com/Microsoft/PowerStigDsc/tree/master/Examples' + $examplelist = [System.Collections.ArrayList]@() + + $filter = "Sample_$ConfigurationName*.ps1" + $rootPath = ($Path -split 'DscResources')[0] + $exampleFileList = Get-ChildItem -Path $rootPath -Recurse -Filter $filter + + foreach ($exampleFile in $exampleFileList) + { + $configurationContent = ConvertTo-Function -Path $exampleFile.FullName + + $exampleObject = @{ + Text = (Format-HelpString -String $configurationContent.GetHelpContent().Synopsis -SingleLine) + Link = "$basePath/$($exampleFile.Name)" + } + $null = $examplelist.Add($exampleObject) + } + return $exampleList +} + +<# + .SYNOPSIS + Creates a DSC Composite wiki page +#> +function New-DscCompositeWikiPage +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [string] + $Path, + + [Parameter(Mandatory = $true)] + [string] + $OutputPath + ) + + process + { + $wikiStrings = [System.Collections.ArrayList]@() + + $configurationContent = Get-Content -Path $Path -Raw + + [System.Management.Automation.Language.Token[]] $tokens = $null + + $ast = [System.Management.Automation.Language.Parser]::ParseInput( + $configurationContent, [ref]$tokens, [ref]$null) + + $configuration = $ast.FindAll( + {$args[0] -is [System.Management.Automation.Language.ConfigurationDefinitionAst]}, $true) + + # ConfigurationName + + $null = $wikiStrings.Add("# $($configuration.InstanceName.Value)") + $null = $wikiStrings.Add("") + + # Configuration Synopsis as the header text TO DO + $configurationDetails = ConvertTo-Function -Path $Path + $help = $configurationDetails.GetHelpContent() + $null = $wikiStrings.Add("$(Format-HelpString -String $help.Synopsis)") + + $null = $wikiStrings.Add("") + $null = $wikiStrings.Add("## Requirements") + $null = $wikiStrings.Add("") + + $requirements = # Get the list of DSC resources and using statements defined in the composite + if ($requirements) + { + # | Resource Name | Resource Version | + } + else + { + $null = $wikiStrings.Add("None") + } + + $null = $wikiStrings.Add("") + $null = $wikiStrings.Add("## Parameters") + $null = $wikiStrings.Add("") + $null = $wikiStrings.Add("| Parameter | Attribute | DataType | Description | Allowed Values |") + $null = $wikiStrings.Add("| --------- | --------- | -------- | ----------- | -------------- |") + + $parameterList = Get-ConfigurationParameters -FunctionDefinitionAst $configurationDetails + foreach ($parameter in $parameterList) + { + $null = $wikiStrings.Add("| $($parameter.Name) | $($parameter.Attribute) | $($parameter.DataType) | $($parameter.Description) | $($parameter.AllowedValues) |") + } + + $null = $wikiStrings.Add("") + $null = $wikiStrings.Add("## Examples") + $null = $wikiStrings.Add("") + + $exampleList = Get-ConfigurationExamples -Configurationname $configuration.InstanceName.Value -Path $Path + foreach ($example in $exampleList) + { + $null = $wikiStrings.Add("* [$($example.Text)]($($example.Link))") + } + + $wikiStrings | Out-File -FilePath "$OutputPath\$($configuration.InstanceName.Value).md" + } +} + +function New-WikiPageAdvanced +{ + +} + +Export-ModuleMember -Function 'New-WikiPage', 'New-DscCompositeWikiPage' diff --git a/appveyor.yml b/appveyor.yml index be59ca004..931b6e73b 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -25,8 +25,7 @@ install: ForEach-Object { Install-Module $PSItem.moduleName -RequiredVersion $PSItem.ModuleVersion -Repository PSGallery -Scope CurrentUser -Force } - git clone https://github.com/PowerShell/DscResource.Tests - ps: Import-Module "$env:APPVEYOR_BUILD_FOLDER\DscResource.Tests\AppVeyor.psm1" - - git clone https://github.com/Microsoft/PowerStig.Tests - - ps: Import-Module "$env:APPVEYOR_BUILD_FOLDER\PowerStig.Tests\PowerStig.Tests.psd1" + - ps: Import-Module "$env:APPVEYOR_BUILD_FOLDER\Tools\AppVeyor\AppVeyor.psm1" - ps: Invoke-AppveyorInstallTask #---------------------------------# @@ -41,7 +40,7 @@ build: false test_script: - ps: | - Invoke-AppveyorTestScriptTask -CodeCoverage -CodeCovIo -ExcludeTag @() + Invoke-AppveyorTestScriptTask -CodeCoverage -CodeCovIo -ExcludeTag @('tools') #---------------------------------# # deployment configuration # @@ -49,7 +48,7 @@ test_script: # scripts to run before deployment before_deploy: - - ps: PowerStig.Tests\Invoke-AppveyorAfterTestTask + - ps: Invoke-PowerStigAppveyorAfterTestTask for: -