diff --git a/PSModuleDevelopment/PSModuleDevelopment.psd1 b/PSModuleDevelopment/PSModuleDevelopment.psd1 index 74bf660..71ce75e 100644 --- a/PSModuleDevelopment/PSModuleDevelopment.psd1 +++ b/PSModuleDevelopment/PSModuleDevelopment.psd1 @@ -5,7 +5,7 @@ # Version number of this module. - ModuleVersion = '2.2.11.146' + ModuleVersion = '2.2.11.163' # ID used to uniquely identify this module GUID = '37dd5fce-e7b5-4d57-ac37-832055ce49d6' diff --git a/PSModuleDevelopment/changelog.md b/PSModuleDevelopment/changelog.md index 15fc78e..8178cec 100644 --- a/PSModuleDevelopment/changelog.md +++ b/PSModuleDevelopment/changelog.md @@ -1,14 +1,24 @@ # Changelog -## Unreleased +## 2.2.11.163 (2024-02-25) + New: Template ApplockerPipeline - A project that can be used to generate AppLocker policies across environments ++ Upd: Template AzureFunction - Added automatic Endpoint generation based on functions defined in the module + Upd: Template PSFProject - Improved PSScriptAnalyzer test results ++ Upd: Template PSFProject - Extended list of blacklisted commands ++ Upd: Template PSFProject - Build logic now also supports compiling the C# solution + Upd: Template PSFModule - Improved PSScriptAnalyzer test results ++ Upd: Template PSFModule - Extended list of blacklisted commands + Upd: Template PSFHelp - Improved PSScriptAnalyzer test results ++ Upd: Template PSFHelp - Extended list of blacklisted commands ++ Upd: Template MiniModule - Extended list of blacklisted commands + Fix: Template PSFProject - Help test fails on PS7.4 ++ Fix: Template PSFProject - Help test added ProgressAction to common parameters + Fix: Template PSFModule - Help test fails on PS7.4 ++ Fix: Template PSFModule - Help test added ProgressAction to common parameters + Fix: Template PSFHelp - Help test fails on PS7.4 ++ Fix: Template PSFHelp - Help test added ProgressAction to common parameters ++ Fix: Template MiniModule - Help test - added ProgressAction to common parameters ## 2.2.11.146 (2023-05-31) diff --git a/PSModuleDevelopment/functions/templating/Invoke-PSMDTemplate.ps1 b/PSModuleDevelopment/functions/templating/Invoke-PSMDTemplate.ps1 index 3a8b34f..639a7cd 100644 --- a/PSModuleDevelopment/functions/templating/Invoke-PSMDTemplate.ps1 +++ b/PSModuleDevelopment/functions/templating/Invoke-PSMDTemplate.ps1 @@ -82,6 +82,11 @@ PS C:\> Invoke-PSMDTemplate -TemplateName "module" -Name "MyModule" Creates a project based on the module template with the name "MyModule" + + .EXAMPLE + PS C:\> Invoke-PSMDTemplate MiniModule -Parameters @{ Author = 'Fred' } + + Creates a new project based on the template MiniModule and predefines the value for the "Author" placeholder. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSPossibleIncorrectUsageOfAssignmentOperator", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] diff --git a/PSModuleDevelopment/internal/functions/template/Get-PsmdTemplateStore.ps1 b/PSModuleDevelopment/internal/functions/templating/Get-PsmdTemplateStore.ps1 similarity index 100% rename from PSModuleDevelopment/internal/functions/template/Get-PsmdTemplateStore.ps1 rename to PSModuleDevelopment/internal/functions/templating/Get-PsmdTemplateStore.ps1 diff --git a/templates/AzureFunction/build/build.config.psd1 b/templates/AzureFunction/build/build.config.psd1 new file mode 100644 index 0000000..8822c4a --- /dev/null +++ b/templates/AzureFunction/build/build.config.psd1 @@ -0,0 +1,26 @@ +@{ + TimerTrigger = @{ + # Default Schedule for timed executions + Schedule = '0 5 * * * *' + + # Different Schedules for specific timed endpoints + ScheduleOverrides = @{ + # 'Update-Whatever' = '0 5 12 * * *' + } + } + + HttpTrigger = @{ + <# + AuthLevels: + https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook-trigger?tabs=python-v2%2Cisolated-process%2Cnodejs-v4%2Cfunctionsv2&pivots=programming-language-csharp#http-auth + + anonymous: No Token needed (combine with Identity Provider for Entra ID auth without also needing a token) + function: (default) Require a function-endpoint-specific token with the request + admin: Require a Function-App-global admin token (master key) for the request + #> + AuthLevel = 'function' + AuthLevelOverrides = @{ + # 'Set-Foo' = 'anonymous' + } + } +} \ No newline at end of file diff --git a/templates/AzureFunction/build/build.ps1 b/templates/AzureFunction/build/build.ps1 index 9a79a68..35a4530 100644 --- a/templates/AzureFunction/build/build.ps1 +++ b/templates/AzureFunction/build/build.ps1 @@ -1,8 +1,15 @@ param ( - [string] - $Repository = 'PSGallery' + [string] + $Repository = 'PSGallery', + + [string] + $AppRg, + + [string] + $AppName ) $workingDirectory = Split-Path $PSScriptRoot +$config = Import-PowerShellDataFile -Path "$PSScriptRoot\build.config.psd1" # Prepare output path and copy function folder Remove-Item -Path "$workingDirectory/publish" -Recurse -Force -ErrorAction Ignore @@ -12,12 +19,46 @@ Copy-Item -Path "$workingDirectory/function/*" -Destination $buildFolder.FullNam # Process Dependencies $requiredModules = (Import-PowerShellDataFile -Path "$workingDirectory/þnameþ/þnameþ.psd1").RequiredModules foreach ($module in $requiredModules) { - Save-Module -Name $module -Path "$($buildFolder.FullName)/modules" -Force -Repository $Repository + Save-Module -Name $module -Path "$($buildFolder.FullName)/modules" -Force -Repository $Repository } # Process Function Module Copy-Item -Path "$workingDirectory/þnameþ" -Destination "$($buildFolder.FullName)/modules" -Force -Recurse +$commands = Get-ChildItem -Path "$($buildFolder.FullName)/modules/þnameþ/Functions" -Recurse -Filter *.ps1 | ForEach-Object BaseName +Update-ModuleManifest -Path "$($buildFolder.FullName)/modules/þnameþ/þnameþ.psd1" -FunctionsToExport $commands + +# Generate Http Trigger +$httpCode = Get-Content -Path "$PSScriptRoot\functionHttp\run.ps1" | Join-String "`n" +$httpConfig = Get-Content -Path "$PSScriptRoot\functionHttp\function.json" | Join-String "`n" +foreach ($command in Get-ChildItem -Path "$workingDirectory\þnameþ\functions\httpTrigger" -Recurse -File -Filter *.ps1) { + $authLevel = $config.HttpTrigger.AuthLevel + if ($config.HttpTrigger.AuthLevelOverride.$($command.BaseName)) { + $authLevel = $config.HttpTrigger.AuthLevelOverride.$($command.BaseName) + } + $endpointFolder = New-Item -Path $buildFolder.FullName -Name $command.BaseName -ItemType Directory + $httpCode -replace '%COMMAND%',$command.BaseName | Set-Content -Path "$($endpointFolder.FullName)\run.ps1" + $httpConfig -replace '%AUTHLEVEL%', $authLevel | Set-Content -Path "$($endpointFolder.FullName)\function.json" +} + +# Generate Timer Trigger +$timerCode = Get-Content -Path "$PSScriptRoot\functionTimer\run.ps1" | Join-String "`n" +$timerConfig = Get-Content -Path "$PSScriptRoot\functionTimer\function.json" | Join-String "`n" +foreach ($command in Get-ChildItem -Path "$workingDirectory\þnameþ\functions\timerTrigger" -Recurse -File -Filter *.ps1) { + $schedule = $config.TimerTrigger.Schedule + if ($config.TimerTrigger.ScheduleOverride.$($command.BaseName)) { + $schedule = $config.TimerTrigger.ScheduleOverride.$($command.BaseName) + } + $endpointFolder = New-Item -Path $buildFolder.FullName -Name $command.BaseName -ItemType Directory + $timerCode -replace '%COMMAND%',$command.BaseName | Set-Content -Path "$($endpointFolder.FullName)\run.ps1" + $timerConfig -replace '%SCHEDULE%', $schedule | Set-Content -Path "$($endpointFolder.FullName)\function.json" +} # Package & Cleanup +Remove-Item -Path "$workingDirectory/Function.zip" -Recurse -Force -ErrorAction Ignore Compress-Archive -Path "$($buildFolder.FullName)/*" -DestinationPath "$workingDirectory/Function.zip" -Remove-Item -Path $buildFolder.FullName -Recurse -Force -ErrorAction Ignore \ No newline at end of file +Remove-Item -Path $buildFolder.FullName -Recurse -Force -ErrorAction Ignore + +if ($AppRg -and $AppName) { + Write-Host "Publishing Function App to $AppRg/$AppName" + Publish-AzWebApp -ResourceGroupName $AppRG -Name $AppName -ArchivePath "$workingDirectory/Function.zip" -Confirm:$false -Force +} \ No newline at end of file diff --git a/templates/AzureFunction/build/functionHttp/function.json b/templates/AzureFunction/build/functionHttp/function.json new file mode 100644 index 0000000..436ab49 --- /dev/null +++ b/templates/AzureFunction/build/functionHttp/function.json @@ -0,0 +1,20 @@ +{ + "bindings": [ + { + "authLevel": "%AUTHLEVEL%", + "type": "httpTrigger", + "direction": "in", + "name": "Request", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "Response" + } + ], + "disabled": false +} \ No newline at end of file diff --git a/templates/AzureFunction/build/functionHttp/run.ps1 b/templates/AzureFunction/build/functionHttp/run.ps1 new file mode 100644 index 0000000..d1eb16b --- /dev/null +++ b/templates/AzureFunction/build/functionHttp/run.ps1 @@ -0,0 +1,24 @@ +param ( + $Request, + + $TriggerMetadata +) + + +Write-Host "Trigger: %COMMAND% has been invoked" + +$parameters = Get-RestParameter -Request $Request -Command %COMMAND% + +try { + $results = %COMMAND% @parameters -ErrorAction Stop +} +catch { + $_ | Out-String | ForEach-Object { + foreach ($line in ($_ -split "`n")) { + Write-Warning $line + } + } + Write-FunctionResult -Status InternalServerError -Body "$_" + return +} +Write-FunctionResult -Status OK -Body $results \ No newline at end of file diff --git a/templates/AzureFunction/build/functionTimer/function.json b/templates/AzureFunction/build/functionTimer/function.json new file mode 100644 index 0000000..14536aa --- /dev/null +++ b/templates/AzureFunction/build/functionTimer/function.json @@ -0,0 +1,11 @@ +{ + "bindings": [ + { + "name": "Timer", + "type": "timerTrigger", + "direction": "in", + "schedule": "%SCHEDULE%" + } + ], + "disabled": false +} \ No newline at end of file diff --git a/templates/AzureFunction/build/functionTimer/run.ps1 b/templates/AzureFunction/build/functionTimer/run.ps1 new file mode 100644 index 0000000..2ba4950 --- /dev/null +++ b/templates/AzureFunction/build/functionTimer/run.ps1 @@ -0,0 +1,11 @@ +# Input bindings are passed in via param block. +param ($Timer) + +$ErrorActionPreference = 'Stop' +Write-Host "%COMMAND% start time: $((Get-Date).ToUniversalTime())" +try { %COMMAND% } +catch { + Write-Warning "%COMMAND% failed: $_ $((Get-Date).ToUniversalTime())" + throw "%COMMAND% failed: $_ $((Get-Date).ToUniversalTime())" +} +Write-Host "%COMMAND% end time: $((Get-Date).ToUniversalTime())" \ No newline at end of file diff --git "a/templates/AzureFunction/\303\276name\303\276/functions/httpTrigger/readme.md" "b/templates/AzureFunction/\303\276name\303\276/functions/httpTrigger/readme.md" new file mode 100644 index 0000000..059abe3 --- /dev/null +++ "b/templates/AzureFunction/\303\276name\303\276/functions/httpTrigger/readme.md" @@ -0,0 +1,3 @@ +# Functions: http Trigger + +Each function placed under this folder will be exposed as an http-trigger function endpoint on build. diff --git "a/templates/AzureFunction/\303\276name\303\276/functions/nonPublished/readme.md" "b/templates/AzureFunction/\303\276name\303\276/functions/nonPublished/readme.md" new file mode 100644 index 0000000..bb0cc6a --- /dev/null +++ "b/templates/AzureFunction/\303\276name\303\276/functions/nonPublished/readme.md" @@ -0,0 +1,3 @@ +# Functions: Non-Published + +Functions placed here will not be published as a function endpoint \ No newline at end of file diff --git "a/templates/AzureFunction/\303\276name\303\276/functions/readme.md" "b/templates/AzureFunction/\303\276name\303\276/functions/readme.md" index 81c52a2..24219d9 100644 --- "a/templates/AzureFunction/\303\276name\303\276/functions/readme.md" +++ "b/templates/AzureFunction/\303\276name\303\276/functions/readme.md" @@ -1,3 +1,12 @@ # Functions Place all your function code here +One file per function, file should have the same name as the function. + +There are three subdirectories: + ++ httpTrigger: Each function placed in this folder will be exposed via httpTrigger as a function endpoint. ++ timerTrigger: Each function placed in this folder will be registered as a timer-triggered function endpoint. ++ nonPublished: Any function placed here will be exposed by the module, but not as a function endpoint. + +The function endpoints are generated while running build.ps1. diff --git "a/templates/AzureFunction/\303\276name\303\276/functions/timerTrigger/readme.md" "b/templates/AzureFunction/\303\276name\303\276/functions/timerTrigger/readme.md" new file mode 100644 index 0000000..b7d7035 --- /dev/null +++ "b/templates/AzureFunction/\303\276name\303\276/functions/timerTrigger/readme.md" @@ -0,0 +1,3 @@ +# Functions: Timer Trigger + +Each function placed under this folder will be exposed as an timer-trigger function endpoint on build. diff --git "a/templates/AzureFunction/\303\276name\303\276/internal/functions/readme.md" "b/templates/AzureFunction/\303\276name\303\276/internal/functions/readme.md" new file mode 100644 index 0000000..99ea360 --- /dev/null +++ "b/templates/AzureFunction/\303\276name\303\276/internal/functions/readme.md" @@ -0,0 +1,3 @@ +# Functions (Internal) + +Place all your internal helper function code here diff --git "a/templates/AzureFunction/\303\276name\303\276/internal/scripts/readme.md" "b/templates/AzureFunction/\303\276name\303\276/internal/scripts/readme.md" new file mode 100644 index 0000000..3815300 --- /dev/null +++ "b/templates/AzureFunction/\303\276name\303\276/internal/scripts/readme.md" @@ -0,0 +1,3 @@ +# Scripts + +The place where all the code goes that should be run on import only. diff --git "a/templates/AzureFunction/\303\276name\303\276/\303\276name\303\276.psd1" "b/templates/AzureFunction/\303\276name\303\276/\303\276name\303\276.psd1" index 9c8f039..fbe2b02 100644 --- "a/templates/AzureFunction/\303\276name\303\276/\303\276name\303\276.psd1" +++ "b/templates/AzureFunction/\303\276name\303\276/\303\276name\303\276.psd1" @@ -46,9 +46,7 @@ # NestedModules = @() # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. - FunctionsToExport = @( - - ) + FunctionsToExport = '*' # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. # CmdletsToExport = '*' diff --git "a/templates/AzureFunction/\303\276name\303\276/\303\276name\303\276.psm1" "b/templates/AzureFunction/\303\276name\303\276/\303\276name\303\276.psm1" index 2509b6f..34b1377 100644 --- "a/templates/AzureFunction/\303\276name\303\276/\303\276name\303\276.psm1" +++ "b/templates/AzureFunction/\303\276name\303\276/\303\276name\303\276.psm1" @@ -1,3 +1,10 @@ -foreach ($file in Get-ChildItem $PSScriptRoot\functions -Recurse -Filter '*.ps1') { +$script:moduleRoot = $PSScriptRoot +foreach ($file in Get-ChildItem $PSScriptRoot\internal\functions -Recurse -Filter '*.ps1') { + . $file.FullName +} +foreach ($file in Get-ChildItem $PSScriptRoot\functions -Recurse -Filter '*.ps1') { + . $file.FullName +} +foreach ($file in Get-ChildItem $PSScriptRoot\internal\scripts -Recurse -Filter '*.ps1') { . $file.FullName } \ No newline at end of file diff --git a/templates/MiniModule/tests/general/FileIntegrity.Exceptions.ps1 b/templates/MiniModule/tests/general/FileIntegrity.Exceptions.ps1 index 0d92e79..4729d29 100644 --- a/templates/MiniModule/tests/general/FileIntegrity.Exceptions.ps1 +++ b/templates/MiniModule/tests/general/FileIntegrity.Exceptions.ps1 @@ -11,6 +11,20 @@ $global:BannedCommands = @( # Use Get-WinEvent instead 'Get-EventLog' + + # User Preference should not be used in automation + 'Clear-Host' # Console Screen belongs to the user + 'Set-Location' # Console path belongs to the user. Use $PSScriptRoot instead. + + # Dynamic Variables are undesirable. Use hashtable instead. + 'Get-Variable' + 'Set-Variable' + 'Clear-Variable' + 'Remove-Variable' + 'New-Variable' + + # Dynamic Code execution should not require this + 'Invoke-Expression' # Consider splatting instead. Yes, you can splat parameters for external applications! ) <# diff --git a/templates/MiniModule/tests/general/Help.Tests.ps1 b/templates/MiniModule/tests/general/Help.Tests.ps1 index f1dc4d3..5b9aced 100644 --- a/templates/MiniModule/tests/general/Help.Tests.ps1 +++ b/templates/MiniModule/tests/general/Help.Tests.ps1 @@ -94,7 +94,7 @@ foreach ($command in $commands) { Context "Test parameter help for $commandName" { - $common = 'Debug', 'ErrorAction', 'ErrorVariable', 'InformationAction', 'InformationVariable', 'OutBuffer', 'OutVariable', 'PipelineVariable', 'Verbose', 'WarningAction', 'WarningVariable' + $common = 'Debug', 'ErrorAction', 'ErrorVariable', 'InformationAction', 'InformationVariable', 'OutBuffer', 'OutVariable', 'PipelineVariable', 'Verbose', 'WarningAction', 'WarningVariable', 'ProgressAction' $parameters = $command.ParameterSets.Parameters | Sort-Object -Property Name -Unique | Where-Object Name -notin $common $parameterNames = $parameters.Name diff --git a/templates/PSFProject/PSMDTemplate.ps1 b/templates/PSFProject/PSMDTemplate.ps1 index 37887a9..547fc75 100644 --- a/templates/PSFProject/PSMDTemplate.ps1 +++ b/templates/PSFProject/PSMDTemplate.ps1 @@ -1,6 +1,6 @@ @{ TemplateName = 'PSFProject' - Version = "1.3.3" + Version = "1.3.4" AutoIncrementVersion = $true Tags = 'module','psframework' Author = 'Friedrich Weinmann' diff --git a/templates/PSFProject/build/vsts-build.ps1 b/templates/PSFProject/build/vsts-build.ps1 index cb69b66..fd5b684 100644 --- a/templates/PSFProject/build/vsts-build.ps1 +++ b/templates/PSFProject/build/vsts-build.ps1 @@ -18,7 +18,10 @@ param ( $SkipPublish, [switch] - $AutoVersion + $AutoVersion, + + [switch] + $Build ) #region Handle Working Directory Defaults @@ -33,6 +36,14 @@ if (-not $WorkingDirectory) if (-not $WorkingDirectory) { $WorkingDirectory = Split-Path $PSScriptRoot } #endregion Handle Working Directory Defaults +# Build Library +if ($Build) { + dotnet build "$WorkingDirectory\library\þnameþ.sln" + if ($LASTEXITCODE -ne 0) { + throw "Failed to build þnameþ.dll!" + } +} + # Prepare publish folder Write-PSFMessage -Level Important -Message "Creating and populating publishing directory" $publishDir = New-Item -Path $WorkingDirectory -Name publish -ItemType Directory -Force diff --git a/templates/PSFTests/general/FileIntegrity.Exceptions.ps1 b/templates/PSFTests/general/FileIntegrity.Exceptions.ps1 index e385e41..c9082f8 100644 --- a/templates/PSFTests/general/FileIntegrity.Exceptions.ps1 +++ b/templates/PSFTests/general/FileIntegrity.Exceptions.ps1 @@ -17,6 +17,20 @@ $global:BannedCommands = @( # Use Get-WinEvent instead 'Get-EventLog' + + # User Preference should not be used in automation + 'Clear-Host' # Console Screen belongs to the user + 'Set-Location' # Console path belongs to the user. Use $PSScriptRoot instead. + + # Dynamic Variables are undesirable. Use hashtable instead. + 'Get-Variable' + 'Set-Variable' + 'Clear-Variable' + 'Remove-Variable' + 'New-Variable' + + # Dynamic Code execution should not require this + 'Invoke-Expression' # Consider splatting instead. Yes, you can splat parameters for external applications! ) <# diff --git a/templates/PSFTests/general/Help.Tests.ps1 b/templates/PSFTests/general/Help.Tests.ps1 index be1c012..070aeda 100644 --- a/templates/PSFTests/general/Help.Tests.ps1 +++ b/templates/PSFTests/general/Help.Tests.ps1 @@ -89,7 +89,7 @@ foreach ($command in $commands) { Context "Test parameter help for $commandName" { - $common = 'Debug', 'ErrorAction', 'ErrorVariable', 'InformationAction', 'InformationVariable', 'OutBuffer', 'OutVariable', 'PipelineVariable', 'Verbose', 'WarningAction', 'WarningVariable' + $common = 'Debug', 'ErrorAction', 'ErrorVariable', 'InformationAction', 'InformationVariable', 'OutBuffer', 'OutVariable', 'PipelineVariable', 'Verbose', 'WarningAction', 'WarningVariable', 'ProgressAction' $parameters = $command.ParameterSets.Parameters | Sort-Object -Property Name -Unique | Where-Object Name -notin $common $parameterNames = $parameters.Name