├── Import.ps1 ├── img ├── vcredist256.png ├── installer1024.png ├── VisualStudioLogo2019-256.png ├── Microsoft-VisualStudio2026.png └── Product-Icon.svg ├── VcRedist ├── img │ └── vcredist.png ├── Public │ ├── Get-VcIntuneApplication.ps1 │ ├── Remove-VcIntuneApplication.ps1 │ ├── Export-VcManifest.ps1 │ ├── Get-InstalledVcRedist.ps1 │ ├── Test-VcRedistUri.ps1 │ ├── Get-VcList.ps1 │ ├── Install-VcRedist.ps1 │ ├── Update-VcMdtBundle.ps1 │ ├── Uninstall-VcRedist.ps1 │ ├── New-VcMdtBundle.ps1 │ ├── Import-VcIntuneApplication.ps1 │ └── Save-VcRedist.ps1 ├── Private │ ├── Edit-MdtDrive.ps1 │ ├── Get-Bitness.ps1 │ ├── Test-PSCore.ps1 │ ├── New-TemporaryFolder.ps1 │ ├── New-MdtDrive.ps1 │ ├── New-MdtApplicationFolder.ps1 │ ├── Import-MdtModule.ps1 │ ├── Test-VcListObject.ps1 │ ├── Invoke-Process.ps1 │ ├── Get-VcRedistAppsFromIntune.ps1 │ ├── Get-InstalledSoftware.ps1 │ └── Get-RequiredVcRedistUpdatesFromIntune.ps1 ├── VcRedist.json ├── en-US │ └── about_VcRedist.help.txt ├── VcRedist.psm1 ├── Intune.json └── VcRedist.psd1 ├── ci ├── README.md ├── Push-Tag.ps1 └── Update-Manifest.ps1 ├── .github ├── FUNDING.yml ├── dependabot.yml ├── ISSUE_TEMPLATE.md ├── workflows │ ├── analyzer.yml │ ├── publish-module.yml │ ├── validate-module.yml │ └── update-module.yml ├── PULL_REQUEST_TEMPLATE.md └── CODE_OF_CONDUCT.md ├── scripts ├── README.md ├── Update-Help.ps1 ├── Import-PrivateFunctions.ps1 ├── Import-IntoMdt.ps1 └── Compare-VersionNumber.ps1 ├── codecov.yml ├── .gitattributes ├── .rules ├── PSScriptAnalyzerSettings.psd1 ├── PascalCase.psm1 └── LowercaseKeyword.psm1 ├── tests ├── Private │ ├── Get-Bitness.Tests.ps1 │ ├── Get-VcRedistAppsFromIntune.Tests.ps1.txt │ ├── Get-RequiredVcRedistUpdatesFromIntune.Tests.ps1.txt │ ├── Test-PSCore.Tests.ps1 │ ├── Get-InstalledSoftware.Tests.ps1 │ ├── New-TemporaryFolder.Tests.ps1 │ ├── Edit-MdtDrive.Tests.ps1 │ ├── Invoke-Process.Tests.ps1 │ ├── New-MdtApplicationFolder.Tests.ps1 │ ├── New-MdtDrive.Tests.ps1 │ ├── Test-VcListObject.Tests.ps1 │ └── Import-MdtModule.Tests.ps1 ├── Public │ ├── Update-VcMdtBundle.Tests.ps1 │ ├── New-VcMdtBundle.Tests.ps1 │ ├── Get-VcIntuneApplication.Tests.ps1 │ ├── Export-VcManifest.Tests.ps1 │ ├── Test-VcRedistUri.Tests.ps1 │ ├── Get-InstalledVcRedist.Tests.ps1 │ ├── Import-VcMdtApplication.Tests.ps1 │ ├── Import-VcIntuneApplication.Tests.ps1 │ ├── Import-VcConfigMgrApplication.Tests.ps1 │ ├── Remove-VcIntuneApplication.Tests.ps1 │ ├── Uninstall-VcRedist.Tests.ps1 │ ├── Save-VcRedist.Tests.ps1 │ ├── Update-VcMdtApplication.Tests.ps1 │ ├── Get-VcList.Tests.ps1 │ └── Install-VcRedist.Tests.ps1 ├── Install-Pester.ps1 ├── Install-Mdt.ps1 ├── Module.Tests.ps1 └── Manifest.Tests.ps1 ├── .gitignore ├── LICENSE ├── .vscode └── settings.json ├── README.md └── docs └── versions.md /Import.ps1: -------------------------------------------------------------------------------- 1 | Import-Module ./VcRedist/VcRedist.psd1 -Verbose -Force 2 | -------------------------------------------------------------------------------- /img/vcredist256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EUCPilots/vcredist/HEAD/img/vcredist256.png -------------------------------------------------------------------------------- /img/installer1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EUCPilots/vcredist/HEAD/img/installer1024.png -------------------------------------------------------------------------------- /VcRedist/img/vcredist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EUCPilots/vcredist/HEAD/VcRedist/img/vcredist.png -------------------------------------------------------------------------------- /ci/README.md: -------------------------------------------------------------------------------- 1 | # Continuous Integration scripts 2 | 3 | Scripts used during workflows to validate this project. 4 | -------------------------------------------------------------------------------- /img/VisualStudioLogo2019-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EUCPilots/vcredist/HEAD/img/VisualStudioLogo2019-256.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: aaronparker 4 | ko_fi: stealthpuppy 5 | -------------------------------------------------------------------------------- /img/Microsoft-VisualStudio2026.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EUCPilots/vcredist/HEAD/img/Microsoft-VisualStudio2026.png -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | # Scripts 2 | 3 | Various scripts for testing purposes or examples for usage of the VcRedist module. 4 | -------------------------------------------------------------------------------- /scripts/Update-Help.ps1: -------------------------------------------------------------------------------- 1 | # platyPS Help 2 | # platyPS help markdown can be found here: [/docs/help](/docs/help). 3 | # To generate the external help use `New-ExternalHelp`: 4 | 5 | New-ExternalHelp -Path "docs/help/en-US" -OutputPath "VcRedist/en-US" -Force 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Set update schedule for GitHub Actions 2 | version: 2 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | # Check for updates to GitHub Actions every weekday 8 | interval: "daily" 9 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: yes 3 | 4 | coverage: 5 | precision: 2 6 | round: down 7 | range: "70...100" 8 | 9 | parsers: 10 | gcov: 11 | branch_detection: 12 | conditional: yes 13 | loop: yes 14 | method: no 15 | macro: no 16 | 17 | comment: 18 | layout: "reach,diff,flags,files,footer" 19 | behavior: default 20 | require_changes: no 21 | -------------------------------------------------------------------------------- /ci/Push-Tag.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Set a tag and push 4 | #> 5 | [CmdletBinding()] 6 | param() 7 | 8 | $Path = Resolve-Path -Path (((Get-Item (Split-Path -Parent -Path $MyInvocation.MyCommand.Definition)).Parent).FullName) 9 | $Module = Test-ModuleManifest -Path "$Path/VcRedist/VcRedist.psd1" 10 | if ($null -ne $Module) { 11 | git tag "v$($Module.Version.ToString())" 12 | git push origin --tags 13 | } 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.rtf diff=astextplain 18 | -------------------------------------------------------------------------------- /scripts/Import-PrivateFunctions.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Imports the private functions for testing. 4 | #> 5 | $projectRoot = ((Get-Item (Split-Path -Parent -Path $MyInvocation.MyCommand.Definition)).Parent).FullName 6 | $Private = @( Get-ChildItem -Path $projectRoot\VcRedist\Private\*.ps1 -ErrorAction "SilentlyContinue" ) 7 | foreach ($import in $Private) { 8 | Try { 9 | . $import.fullname 10 | } 11 | Catch { 12 | Write-Error -Message "Failed to import function $($import.fullname): $_" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /VcRedist/Public/Get-VcIntuneApplication.ps1: -------------------------------------------------------------------------------- 1 | function Get-VcIntuneApplication { 2 | <# 3 | .EXTERNALHELP VcRedist-help.xml 4 | #> 5 | [CmdletBinding(SupportsShouldProcess = $false, HelpURI = "https://vcredist.com/get-vcintuneapplication/")] 6 | param () 7 | 8 | begin { 9 | } 10 | 11 | process { 12 | # Get the existing VcRedist Win32 applications from Intune 13 | $WarningPreference = "SilentlyContinue" 14 | $VcList = Get-VcList -Export "All" 15 | $ExistingIntuneApps = Get-VcRedistAppsFromIntune -VcList $VcList 16 | return $ExistingIntuneApps 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /VcRedist/Private/Edit-MdtDrive.ps1: -------------------------------------------------------------------------------- 1 | function Edit-MdtDrive { 2 | <# 3 | .SYNOPSIS 4 | Tests for a validate drive letter and adds the : character if required 5 | #> 6 | [CmdletBinding(SupportsShouldProcess = $false)] 7 | param ( 8 | [System.String] $Drive 9 | ) 10 | 11 | switch -Regex ($Drive) { 12 | "^[a-z|A-Z|0-9]+$" { 13 | Write-Output -InputObject $("$Drive$(":")").ToUpper() 14 | } 15 | "^[a-z|A-Z|0-9]+:$" { 16 | Write-Output -InputObject $Drive.ToUpper() 17 | } 18 | default { 19 | throw [System.FormatException]::New("The MDT drive letter string represented by $Drive is not valid.") 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /VcRedist/VcRedist.json: -------------------------------------------------------------------------------- 1 | { 2 | "ReplaceText": { 3 | "ProductCode": "#ProductCode", 4 | "Installer": "#Installer" 5 | }, 6 | "Filters": { 7 | "Redist": "(Microsoft Visual C.*)(\bRedistributable|\bRuntime).*", 8 | "All": "Additional|Minimum" 9 | }, 10 | "UninstallKeys": { 11 | "32": "HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall", 12 | "64": "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall" 13 | }, 14 | "Urls": { 15 | "Privacy": "https://go.microsoft.com/fwlink/?LinkId=521839", 16 | "Docs": "https://visualstudio.microsoft.com/vs/support/", 17 | "IntuneWinAppUtil": "https://raw.githubusercontent.com/microsoft/Microsoft-Win32-Content-Prep-Tool/master/IntuneWinAppUtil.exe" 18 | } 19 | } -------------------------------------------------------------------------------- /.rules/PSScriptAnalyzerSettings.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | CustomRulePath = @( 3 | ".rules/LowercaseKeyword.psm1" 4 | ) 5 | IncludeDefaultRules = $true 6 | Severity = @("Error", "Warning") 7 | IncludeRules = @( 8 | "Measure-LowercaseKeyword" 9 | ) 10 | Rules = @{ 11 | PSUseCompatibleCmdlets = @{ 12 | Compatibility = @( 13 | 'desktop-5.1.14393.206-windows' 14 | 'core-6.1.0-windows' 15 | 'core-6.1.0-linux' 16 | 'core-6.1.0-linux-arm' 17 | 'core-6.1.0-macos' 18 | ) 19 | } 20 | PSUseCompatibleSyntax = @{ 21 | TargetedVersions = @( 22 | '7.0' 23 | '6.0' 24 | '5.1' 25 | ) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Private/Get-Bitness.Tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Public Pester function tests. 4 | #> 5 | [CmdletBinding()] 6 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Justification = "This OK for the tests files.")] 7 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Outputs to log host.")] 8 | param () 9 | 10 | BeforeDiscovery { 11 | if ($env:PROCESSOR_ARCHITECTURE -eq "AMD64") { 12 | $Skip = $false 13 | } 14 | else { 15 | $Skip = $true 16 | } 17 | } 18 | 19 | InModuleScope VcRedist { 20 | BeforeAll { 21 | } 22 | 23 | Describe -Name "Get-Bitness" -Skip:$Skip { 24 | Context "Get-Bitness returns the architecture" { 25 | It "Returns x64 when run on a 64-bit machine" { 26 | Get-Bitness | Should -BeExactly "x64" 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Private/Get-VcRedistAppsFromIntune.Tests.ps1.txt: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Pester tests for Get-VcRedistAppsFromIntune 4 | #> 5 | [CmdletBinding()] 6 | param () 7 | 8 | InModuleScope VcRedist { 9 | Describe "Get-VcRedistAppsFromIntune" { 10 | Context "Basic Functionality" { 11 | It "Should not throw when called with no parameters" { 12 | { Get-VcRedistAppsFromIntune } | Should -Not -Throw 13 | } 14 | It "Should return an array or $null" { 15 | $result = Get-VcRedistAppsFromIntune 16 | ($result -is [System.Array] -or $null -eq $result) | Should -BeTrue 17 | } 18 | } 19 | Context "Parameter validation" { 20 | It "Should throw when VcList is missing" { 21 | { Get-VcRedistAppsFromIntune } | Should -Throw 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /VcRedist/Private/Get-Bitness.ps1: -------------------------------------------------------------------------------- 1 | function Get-Bitness { 2 | <# 3 | .SYNOPSIS 4 | Tests the current operating system for 32-bit or 64-bit Windows. Uses '[System.IntPtr]::Size' for maximum compatibility 5 | 6 | .NOTES 7 | Author: Aaron Parker 8 | Twitter: @stealthpuppy 9 | 10 | .PARAMETER Architecture 11 | Specify a specific processor architecture to test for. 12 | #> 13 | [CmdletBinding(SupportsShouldProcess = $false)] 14 | param () 15 | 16 | # Alternative methods for checking bitness 17 | # [System.Environment]::Is64BitOperatingSystem 18 | # (Get-CimInstance -ClassName win32_operatingsystem).OSArchitecture 19 | 20 | [System.String] $output = "x64" 21 | switch ([System.IntPtr]::Size) { 22 | 8 { $output = "x64" } 23 | 4 { $output = "x86" } 24 | } 25 | Write-Output -InputObject $output 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | 10 | # Packages # 11 | ############ 12 | # it's better to unpack these files and commit the raw source 13 | # git has its own built in compression methods 14 | *.7z 15 | *.dmg 16 | *.gz 17 | *.iso 18 | *.jar 19 | *.rar 20 | *.tar 21 | *.zip 22 | 23 | # Logs and databases # 24 | ###################### 25 | *.log 26 | *.sql 27 | *.sqlite 28 | 29 | # OS generated files # 30 | ###################### 31 | .DS_Store 32 | .DS_Store? 33 | ._* 34 | .Spotlight-V100 35 | .Trashes 36 | ehthumbs.db 37 | Thumbs.db 38 | Redists.xml 39 | 40 | # Files generated by tests # 41 | ###################### 42 | Redists.json 43 | VcDownloads 44 | codecov.exe 45 | .cache 46 | CodeCoverage*.xml 47 | TestResults*.xml 48 | enricomi-publish-action-venv 49 | .enricomi-publish-action-pip 50 | codecov.exe.SHA256SUM 51 | codecov.exe.SHA256SUM.sig 52 | -------------------------------------------------------------------------------- /VcRedist/Private/Test-PSCore.ps1: -------------------------------------------------------------------------------- 1 | function Test-PSCore { 2 | <# 3 | .SYNOPSIS 4 | Returns True is running on PowerShell Core. 5 | 6 | .NOTES 7 | Author: Aaron Parker 8 | Twitter: @stealthpuppy 9 | 10 | .PARAMETER Version 11 | The version of PowerShell Core. Optionally specified where value needs to be something other than 6.0.0. 12 | #> 13 | [CmdletBinding()] 14 | [OutputType([System.Boolean])] 15 | param ( 16 | [Parameter(Position = 0)] 17 | [System.String] $Version = "6.0.0" 18 | ) 19 | 20 | # Check whether current PowerShell environment matches or is higher than $Version 21 | if (($PSVersionTable.PSVersion -ge [System.Version]::Parse($Version)) -and ($PSVersionTable.PSEdition -eq "Core")) { 22 | Write-Output -InputObject $true 23 | } 24 | else { 25 | Write-Output -InputObject $false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /VcRedist/Private/New-TemporaryFolder.ps1: -------------------------------------------------------------------------------- 1 | function New-TemporaryFolder { 2 | <# 3 | .SYNOPSIS 4 | Creates a new temporary folder 5 | 6 | .NOTES 7 | Author: Aaron Parker 8 | Twitter: @stealthpuppy 9 | #> 10 | [CmdletBinding(SupportsShouldProcess = $true)] 11 | [OutputType([System.String])] 12 | param () 13 | 14 | # Check whether current PowerShell environment matches or is higher than $Version 15 | try { 16 | $Folder = "vcredist_$([System.Convert]::ToString((Get-Random -Maximum 65535),16).PadLeft(4,'0')).tmp" 17 | $T = Join-Path -Path $Env:Temp -ChildPath $Folder 18 | if ($PSCmdlet.ShouldProcess($T, "New directory.")) { 19 | $Path = New-Item -Path $T -ItemType "Directory" -ErrorAction "SilentlyContinue" 20 | Write-Output -InputObject $Path.FullName 21 | } 22 | } 23 | catch { 24 | throw $_ 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Private/Get-RequiredVcRedistUpdatesFromIntune.Tests.ps1.txt: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Pester tests for Get-RequiredVcRedistUpdatesFromIntune 4 | #> 5 | [CmdletBinding()] 6 | param () 7 | 8 | InModuleScope VcRedist { 9 | Describe "Get-RequiredVcRedistUpdatesFromIntune" { 10 | Context "Basic Functionality" { 11 | It "Should not throw when called with no parameters" { 12 | { Get-RequiredVcRedistUpdatesFromIntune } | Should -Not -Throw 13 | } 14 | It "Should return an array or $null" { 15 | $result = Get-RequiredVcRedistUpdatesFromIntune 16 | ($result -is [System.Array] -or $null -eq $result) | Should -BeTrue 17 | } 18 | } 19 | Context "Parameter validation" { 20 | It "Should throw when VcList is missing" { 21 | { Get-RequiredVcRedistUpdatesFromIntune } | Should -Throw 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Expected Behavior 2 | 3 | Please describe the behavior you are expecting. 4 | 5 | # Current Behavior 6 | 7 | What is the current behavior? 8 | 9 | # Failure Information (for bugs) 10 | 11 | Please help provide information about the failure if this is a bug by issuing the command using the `-Verbose` command. 12 | 13 | ``` 14 | Paste the verbose output from the command here 15 | ``` 16 | 17 | **_If it is not a bug, please remove the rest of this template._** 18 | 19 | ## Steps to Reproduce 20 | 21 | Please provide detailed steps for reproducing the issue. 22 | 23 | 1. Step 1 24 | 1. Step 2 25 | 1. Step 3 (and so on) 26 | 27 | ## Context 28 | 29 | Please provide any relevant information about your setup. This is important in case the issue is not reproducible except for under certain conditions. 30 | 31 | * **VcRedist Version**: Use `Get-Module -ListAvailable VcRedist` 32 | * **PowerShell Version**: Use `Get-PSVersion` 33 | * **Operating System**: 34 | 35 | ## Failure Logs 36 | 37 | Please include any relevant log snippets or files here. 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Aaron Parker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /VcRedist/en-US/about_VcRedist.help.txt: -------------------------------------------------------------------------------- 1 | TOPIC 2 | about_vcredist 3 | 4 | SHORT DESCRIPTION 5 | Lifecycle management of the Microsoft Visual C++ Redistributables. 6 | 7 | LONG DESCRIPTION 8 | VcRedist is a PowerShell module for lifecycle management of the Microsoft 9 | Visual C++ Redistributables. VcRedist downloads the supported (and 10 | unsupported) Redistributables, for local install, gold image deployment or 11 | importing as applications into the Microsoft Deployment Toolkit, Microsoft 12 | Endpoint Configuration Manager or Microsoft Intune. Supports passive and 13 | silent installs and uninstalls of the Visual C++ Redistributables. 14 | 15 | NOTE 16 | Review the list of supported Microsoft Visual C++ Redistributables: https://support.microsoft.com/en-au/help/2977003/the-latest-supported-visual-c-downloads. 17 | Review the VcRedist documentation: https://vcredist.com/. 18 | 19 | TROUBLESHOOTING NOTE 20 | Review the known issues: https://vcredist.com/known-issues. 21 | 22 | SEE ALSO 23 | Evergreen: https://stealthpuppy.com/Evergreen/index. 24 | 25 | KEYWORDS 26 | - VcRedist, Visual C++ Redistributables, Microsoft 27 | 28 | -------------------------------------------------------------------------------- /tests/Private/Test-PSCore.Tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Public Pester function tests. 4 | #> 5 | [CmdletBinding()] 6 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Justification = "This OK for the tests files.")] 7 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Outputs to log host.")] 8 | param () 9 | 10 | BeforeDiscovery { 11 | } 12 | 13 | InModuleScope VcRedist { 14 | Describe -Name "Test-PSCore" { 15 | Context "Test for Windows PowerShell or PowerShell Core" { 16 | if (($PSVersionTable.PSVersion -ge [System.Version]::Parse("6.0.0")) -and ($PSVersionTable.PSEdition -eq "Core")) { 17 | It "Returns true when running on PowerShell Core" { 18 | Test-PSCore | Should -BeTrue 19 | } 20 | } 21 | 22 | if ($PSVersionTable.PSEdition -eq "Desktop") { 23 | It "Returns False if running Windows PowerShell" { 24 | Test-PSCore | Should -BeFalse 25 | } 26 | 27 | It "Returns False if running Windows PowerShell and when passed a version string" { 28 | Test-PSCore -Version "7.0.0" | Should -BeFalse 29 | } 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /scripts/Import-IntoMdt.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Downloads the VcRedists, imports them into an MDT deployment share and creates a bundle. 4 | #> 5 | [CmdletBinding()] 6 | param ( 7 | [Parameter(Mandatory = $false)] 8 | [System.String] $Path = "C:\Temp\VcRedists", 9 | 10 | [Parameter(Mandatory = $false)] 11 | [System.String] $DeploymentShare = "\\marty.local\Deployment\Automata" 12 | ) 13 | 14 | # Download the VcRedists 15 | if (!(Test-Path -Path $Path)) { New-Item -Path $Path -ItemType Directory } 16 | Save-VcRedist -VcList (Get-VcList) -Path $Path 17 | 18 | # Add to the deployment share 19 | Import-VcMdtApplication -VcList (Get-VcList) -Path $Path -MdtPath $DeploymentShare -Silent 20 | New-VcMdtBundle -MdtPath $DeploymentShare 21 | 22 | $params = @{ 23 | VcList = (Get-VcList -Release "14" -Architecture "x64") 24 | Path = "E:\Temp\Deploy" 25 | MdtPath = "E:\Temp\VcRedist" 26 | AppFolder = "VcRedists" 27 | Silent = $true 28 | DontHide = $true 29 | Force = $true 30 | MdtDrive = "DS099" 31 | Publisher = "Microsoft" 32 | Language = "en-US" 33 | Verbose = $true 34 | } 35 | Import-VcMdtApplication @params 36 | -------------------------------------------------------------------------------- /VcRedist/Public/Remove-VcIntuneApplication.ps1: -------------------------------------------------------------------------------- 1 | function Remove-VcIntuneApplication { 2 | <# 3 | .EXTERNALHELP VcRedist-help.xml 4 | #> 5 | [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High", HelpURI = "https://vcredist.com/remove-vcintuneapplication/")] 6 | param ( 7 | [Parameter( 8 | Mandatory = $true, 9 | Position = 0, 10 | ValueFromPipeline, 11 | HelpMessage = "Pass a VcList object from Save-VcRedist.")] 12 | [ValidateNotNullOrEmpty()] 13 | [System.Management.Automation.PSObject] $VcList 14 | ) 15 | 16 | begin { 17 | # Get the existing VcRedist Win32 applications from Intune 18 | $ExistingIntuneApps = Get-VcRedistAppsFromIntune -VcList $VcList 19 | } 20 | 21 | process { 22 | foreach ($Application in $ExistingIntuneApps) { 23 | if ($PSCmdlet.ShouldProcess($Application.displayName, "Remove")) { 24 | Write-Verbose -Message "Removing application: $($Application.displayName) with ID: $($Application.Id)." 25 | Remove-IntuneWin32App -Id $Application.Id 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Public/Update-VcMdtBundle.Tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Public Pester function tests. 4 | #> 5 | [CmdletBinding()] 6 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Justification = "This OK for the tests files.")] 7 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Outputs to log host.")] 8 | param () 9 | 10 | BeforeDiscovery { 11 | if ($env:PROCESSOR_ARCHITECTURE -eq "AMD64") { 12 | $Skip = $false 13 | } 14 | else { 15 | $Skip = $true 16 | } 17 | } 18 | 19 | Describe -Name "Update-VcMdtBundle" -Skip:$Skip { 20 | BeforeAll { 21 | # Install the MDT Workbench 22 | & "$env:GITHUB_WORKSPACE\tests\Install-Mdt.ps1" 23 | } 24 | 25 | Context "Update-VcMdtBundle updates the bundle in the MDT deployment share" { 26 | It "Updates the bundle in the deployment share OK" { 27 | $params = @{ 28 | MdtPath = "$env:RUNNER_TEMP\Deployment" 29 | AppFolder = "VcRedists" 30 | MdtDrive = "DS020" 31 | BundleName = "Visual C++ Redistributables" 32 | Publisher = "Microsoft" 33 | } 34 | { Update-VcMdtBundle @params } | Should -Not -Throw 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Private/Get-InstalledSoftware.Tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Public Pester function tests. 4 | #> 5 | [CmdletBinding()] 6 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Justification = "This OK for the tests files.")] 7 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Outputs to log host.")] 8 | param () 9 | 10 | BeforeDiscovery { 11 | } 12 | 13 | InModuleScope VcRedist { 14 | Describe -Name "Get-InstalledSoftware" { 15 | Context "Get-InstalledSoftware returns the expected output" { 16 | It "Returns null for software that is not installed" { 17 | Get-InstalledSoftware -Name "SoftwareThatIsNotInstalled" | Should -BeNullOrEmpty 18 | } 19 | 20 | It "Returns an object of the expected type" { 21 | Get-InstalledSoftware | Should -BeOfType [System.Management.Automation.PSObject] 22 | } 23 | 24 | It "Returns details for installed software" { 25 | (Get-InstalledSoftware).Count | Should -BeGreaterThan 0 26 | } 27 | 28 | It "Returns details for GitHub CLI" { 29 | (Get-InstalledSoftware -Name "Github CLI").Publisher | Should -BeExactly "GitHub, Inc." 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Public/New-VcMdtBundle.Tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Public Pester function tests. 4 | #> 5 | [CmdletBinding()] 6 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Justification = "This OK for the tests files.")] 7 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Outputs to log host.")] 8 | param () 9 | 10 | BeforeDiscovery { 11 | if ($env:PROCESSOR_ARCHITECTURE -eq "AMD64") { 12 | $Skip = $false 13 | } 14 | else { 15 | $Skip = $true 16 | } 17 | } 18 | 19 | Describe -Name "New-VcMdtBundle" -Skip:$Skip { 20 | BeforeAll { 21 | # Install the MDT Workbench 22 | & "$env:GITHUB_WORKSPACE\tests\Install-Mdt.ps1" 23 | } 24 | 25 | Context "New-VcMdtBundle creates a bundle in the MDT deployment share"{ 26 | It "Creates the bundle in the deployment share OK" { 27 | $params = @{ 28 | MdtPath = "$env:RUNNER_TEMP\Deployment" 29 | AppFolder = "VcRedists" 30 | Force = $true 31 | MdtDrive = "DS020" 32 | BundleName = "Visual C++ Redistributables" 33 | Publisher = "Microsoft" 34 | Language = "en-US" 35 | } 36 | { New-VcMdtBundle @params } | Should -Not -Throw 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Private/New-TemporaryFolder.Tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Public Pester function tests. 4 | #> 5 | [CmdletBinding()] 6 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Justification = "This OK for the tests files.")] 7 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Outputs to log host.")] 8 | param () 9 | 10 | BeforeDiscovery { 11 | } 12 | 13 | InModuleScope VcRedist { 14 | Describe 'New-TemporaryFolder' { 15 | BeforeAll { 16 | if ($env:Temp) { 17 | $Path = Join-Path -Path $env:Temp -ChildPath "Downloads" 18 | } 19 | elseif ($env:TMPDIR) { 20 | $Path = Join-Path -Path $env:TMPDIR -ChildPath "Downloads" 21 | } 22 | elseif ($env:RUNNER_TEMP) { 23 | $Path = Join-Path -Path $env:RUNNER_TEMP -ChildPath "Downloads" 24 | } 25 | New-Item -Path $Path -ItemType "Directory" -Force -ErrorAction "SilentlyContinue" > $Null 26 | } 27 | 28 | Context "Test New-TemporaryFolder" { 29 | It "Does not throw" { 30 | { $Path = New-TemporaryFolder } | Should -Not -Throw 31 | } 32 | 33 | It "Creates a temporary directory" { 34 | Test-Path -Path $Path | Should -BeTrue 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Public/Get-VcIntuneApplication.Tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Pester tests for Get-VcIntuneApplication 4 | #> 5 | [CmdletBinding()] 6 | param () 7 | 8 | Describe "Get-VcIntuneApplication" { 9 | BeforeAll { 10 | foreach ($Module in @("MSAL.PS", "IntuneWin32App")) { 11 | Install-Module -Name $Module -Force 12 | } 13 | 14 | try { 15 | # Authenticate to the Graph API 16 | # Expects secrets to be passed into environment variables 17 | Write-Information -MessageData "Authenticate to the Graph API" 18 | $params = @{ 19 | TenantId = "$env:TENANT_ID" 20 | ClientId = "$env:CLIENT_ID" 21 | ClientSecret = "$env:CLIENT_SECRET" 22 | } 23 | $script:AuthToken = Connect-MSIntuneGraph @params 24 | } 25 | catch { 26 | throw $_ 27 | } 28 | } 29 | 30 | It "Should not throw when called" { 31 | { Get-VcIntuneApplication } | Should -Not -Throw 32 | } 33 | 34 | It "Should return the list of Intune apps" { 35 | $result = Get-VcIntuneApplication 36 | $result | Should -BeOfType [System.Management.Automation.PSCustomObject] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Private/Edit-MdtDrive.Tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Public Pester function tests. 4 | #> 5 | [CmdletBinding()] 6 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Justification = "This OK for the tests files.")] 7 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Outputs to log host.")] 8 | param () 9 | 10 | InModuleScope VcRedist { 11 | BeforeDiscovery { 12 | if ($env:PROCESSOR_ARCHITECTURE -eq "AMD64") { 13 | $Skip = $false 14 | } 15 | else { 16 | $Skip = $true 17 | } 18 | } 19 | 20 | BeforeAll { 21 | } 22 | 23 | Describe -Name "Edit-MdtDrive" -Skip:$Skip { 24 | Context "Validate Edit-MdtDrive" { 25 | It "Should not throw when sent a valid string" { 26 | { Edit-MdtDrive -Drive "DS009" } | Should -Not -Throw 27 | } 28 | 29 | It "Should throw when sent an invalid string" { 30 | { Edit-MdtDrive -Drive "%^&&*&&*(%^^" } | Should -Throw 31 | } 32 | 33 | It "Returns the expected value from 'ds009'" { 34 | Edit-MdtDrive -Drive "ds009" | Should -BeExactly "DS009:" 35 | } 36 | 37 | It "Returns the expected value from 'DS008:'" { 38 | Edit-MdtDrive -Drive "DS008:" | Should -BeExactly "DS008:" 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/analyzer.yml: -------------------------------------------------------------------------------- 1 | name: Run PSScriptAnalyzer 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'VcRedist/**' 7 | branches-ignore: 8 | - main 9 | pull_request: 10 | branches: 11 | - main 12 | paths: 13 | - 'VcRedist/**' 14 | workflow_dispatch: 15 | 16 | jobs: 17 | psscriptanalyzer: 18 | permissions: 19 | contents: read # for actions/checkout to fetch code 20 | security-events: write # for github/codeql-action/upload-sarif to upload SARIF results 21 | actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status 22 | name: Run PSScriptAnalyzer 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v6 26 | 27 | - name: Run PSScriptAnalyzer (development push) 28 | uses: microsoft/psscriptanalyzer-action@6b2948b1944407914a58661c49941824d149734f 29 | with: 30 | path: "./VcRedist" 31 | recurse: true 32 | settings: "./.rules/PSScriptAnalyzerSettings.psd1" 33 | output: results.sarif 34 | 35 | # Upload the SARIF file generated in the previous step 36 | - name: Upload SARIF results file 37 | uses: github/codeql-action/upload-sarif@v4 38 | with: 39 | sarif_file: results.sarif 40 | -------------------------------------------------------------------------------- /tests/Install-Pester.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | Install the latest version of Pester 3 | #> 4 | 5 | # Trust the PSGallery for modules 6 | [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 7 | Install-PackageProvider -Name "NuGet" -MinimumVersion "2.8.5.208" -ErrorAction "SilentlyContinue" 8 | Install-PackageProvider -Name "PowerShellGet" -MinimumVersion "2.2.5" -ErrorAction "SilentlyContinue" 9 | if (Get-PSRepository | Where-Object { $_.Name -eq $Repository -and $_.InstallationPolicy -ne "Trusted" }) { 10 | Set-PSRepository -Name $Repository -InstallationPolicy "Trusted" 11 | } 12 | 13 | foreach ($module in "Pester") { 14 | $installedModule = Get-Module -Name $module -ListAvailable -ErrorAction "SilentlyContinue" | ` 15 | Sort-Object -Property @{ Expression = { [System.Version]$_.Version }; Descending = $true } -ErrorAction "SilentlyContinue" | ` 16 | Select-Object -First 1 17 | $publishedModule = Find-Module -Name $module -ErrorAction "SilentlyContinue" 18 | if (($null -eq $installedModule) -or ([System.Version]$publishedModule.Version -gt [System.Version]$installedModule.Version)) { 19 | $params = @{ 20 | Name = $module 21 | SkipPublisherCheck = $true 22 | ErrorAction = "Stop" 23 | } 24 | Install-Module @params 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /VcRedist/Public/Export-VcManifest.ps1: -------------------------------------------------------------------------------- 1 | function Export-VcManifest { 2 | <# 3 | .EXTERNALHELP VcRedist-help.xml 4 | #> 5 | [Alias("Export-VcXml")] 6 | [CmdletBinding(SupportsShouldProcess = $false, HelpURI = "https://vcredist.com/export-vcmanifest/")] 7 | [OutputType([System.IO.FileSystemInfo])] 8 | param ( 9 | [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline)] 10 | [ValidateNotNullOrEmpty()] 11 | [ValidateScript( { if (Test-Path -Path $_ -PathType "Container") { $true } else { throw [System.IO.DirectoryNotFoundException]::New("Cannot find path: $_") } })] 12 | [System.String] $Path 13 | ) 14 | 15 | process { 16 | # Get the list of VcRedists from Get-VcList 17 | [System.String] $Manifest = (Join-Path -Path $MyInvocation.MyCommand.Module.ModuleBase -ChildPath "VisualCRedistributables.json") 18 | 19 | # Output the manifest to supplied path 20 | try { 21 | Write-Verbose -Message "Copy from: '$Manifest'." 22 | Write-Verbose -Message " Copy to: '$Path'." 23 | $params = @{ 24 | Path = $Manifest 25 | Destination = $Path 26 | PassThru = $true 27 | ErrorAction = "Stop" 28 | } 29 | Copy-Item @params 30 | } 31 | catch { 32 | throw $_ 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /VcRedist/VcRedist.psm1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | VcRedist script to initiate the module 4 | #> 5 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Justification = "Variable VcManifest is used internally by the module.")] 6 | [CmdletBinding()] 7 | param () 8 | 9 | #region Get public and private function definition files 10 | $PublicRoot = Join-Path -Path $PSScriptRoot -ChildPath "Public" 11 | $PrivateRoot = Join-Path -Path $PSScriptRoot -ChildPath "Private" 12 | $Public = @( Get-ChildItem -Path (Join-Path $PublicRoot "*.ps1") -ErrorAction "SilentlyContinue" ) 13 | $Private = @( Get-ChildItem -Path (Join-Path $PrivateRoot "*.ps1") -ErrorAction "SilentlyContinue" ) 14 | 15 | # Dot source the files 16 | foreach ($import in @($Public + $Private)) { 17 | try { 18 | . $import.FullName 19 | } 20 | catch { 21 | Write-Warning -Message "Failed to import function $($import.FullName)." 22 | throw $_ 23 | } 24 | } 25 | 26 | # Export the public functions, aliases and variables 27 | [System.String] $VcManifest = Join-Path -Path $PSScriptRoot -ChildPath "VisualCRedistributables.json" 28 | Export-ModuleMember -Function $Public.Basename -Alias * -Variable "VcManifest" 29 | 30 | # Add the Microsoft.PowerShell.Commands.Utility type required by [Microsoft.PowerShell.Commands.PSUserAgent]::Chrome 31 | Add-Type -AssemblyName "Microsoft.PowerShell.Commands.Utility" -ErrorAction "SilentlyContinue" 32 | -------------------------------------------------------------------------------- /tests/Public/Export-VcManifest.Tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Public Pester function tests. 4 | #> 5 | [CmdletBinding()] 6 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Justification = "This OK for the tests files.")] 7 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Outputs to log host.")] 8 | param () 9 | 10 | BeforeDiscovery { 11 | } 12 | 13 | Describe -Name "Export-VcManifest" { 14 | Context "Validate Export-VcManifest" { 15 | BeforeAll { 16 | $Json = Export-VcManifest -Path $env:RUNNER_TEMP 17 | $VcList = Get-VcList -Path $Json 18 | 19 | $VcCount = @{ 20 | "Default" = 2 21 | "Supported" = 9 22 | "Unsupported" = 18 23 | "All" = 27 24 | } 25 | } 26 | 27 | It "Given valid parameter -Path, it exports an JSON file" { 28 | Test-Path -Path $Json | Should -BeTrue 29 | } 30 | 31 | It "Given valid parameter -Path, it exports an JSON file" { 32 | $VcList.Count | Should -BeGreaterOrEqual $VcCount.Default 33 | } 34 | 35 | It "Given an invalid path, it should throw an error" { 36 | { Export-VcManifest -Path $([System.IO.Path]::Combine($env:RUNNER_TEMP, "Temp")) } | Should -Throw 37 | } 38 | 39 | It "Given an valid path, it should not throw an error" { 40 | { Export-VcManifest -Path $env:RUNNER_TEMP } | Should -Not -Throw 41 | } 42 | 43 | It "Given an valid path, it should not throw an error" { 44 | { Export-VcManifest -Path $env:RUNNER_TEMP } | Should -Not -Throw 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/Private/Invoke-Process.Tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Public Pester function tests. 4 | #> 5 | [CmdletBinding()] 6 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Justification = "This OK for the tests files.")] 7 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Outputs to log host.")] 8 | param () 9 | 10 | BeforeDiscovery { 11 | if ($env:PROCESSOR_ARCHITECTURE -eq "AMD64") { 12 | $Skip = $false 13 | } 14 | else { 15 | $Skip = $true 16 | } 17 | } 18 | 19 | InModuleScope VcRedist { 20 | BeforeAll { 21 | } 22 | 23 | Describe -Name "Invoke-Process" -Skip:$Skip { 24 | Context "Invoke-Process works as expected" { 25 | It "Should run the command without throwing an exception" { 26 | $params = @{ 27 | FilePath = "$env:SystemRoot\System32\cmd.exe" 28 | ArgumentList = "/c dir $Env:RUNNER_TEMP" 29 | } 30 | { Invoke-Process @params } | Should -Not -Throw 31 | } 32 | 33 | It "Returns a string from cmd.exe" { 34 | $params = @{ 35 | FilePath = "$env:SystemRoot\System32\cmd.exe" 36 | ArgumentList = "/c dir $Env:RUNNER_TEMP" 37 | } 38 | Invoke-Process @params | Should -BeOfType [System.String] 39 | } 40 | 41 | It "Should throw when passed an executable that does not exist" { 42 | $params = @{ 43 | FilePath = "$env:SystemRoot\System32\cmd1.exe" 44 | ArgumentList = "/c dir $Env:RUNNER_TEMP" 45 | } 46 | { Invoke-Process @params } | Should -Throw 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/Public/Test-VcRedistUri.Tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Public Pester function tests. 4 | #> 5 | [CmdletBinding()] 6 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Justification = "This OK for the tests files.")] 7 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Outputs to log host.")] 8 | param () 9 | 10 | BeforeDiscovery { 11 | $SupportedReleases = @("2015", "2017", "2019", "14") 12 | } 13 | 14 | Describe -Name "Test-VcRedistUri" -ForEach $SupportedReleases { 15 | BeforeAll { 16 | $Release = $_ 17 | } 18 | 19 | Context "Test-VcRedistUri returns true for release x64" { 20 | BeforeAll { 21 | $params = @{ 22 | VcList = (Get-VcList -Release $Release -Architecture "x64") 23 | ShowProgress = $true 24 | } 25 | $Test = Test-VcRedistUri @params 26 | } 27 | 28 | It "Returns a true result" { 29 | $Test.Result | Should -BeTrue 30 | } 31 | 32 | It "Returns an architecture of x64" { 33 | $Test.Architecture | Should -BeExactly "x64" 34 | } 35 | } 36 | 37 | Context "Test-VcRedistUri returns true for release x86" { 38 | BeforeAll { 39 | $params = @{ 40 | VcList = (Get-VcList -Release $Release -Architecture "x86") 41 | ShowProgress = $true 42 | } 43 | $Test = Test-VcRedistUri @params 44 | } 45 | 46 | It "Returns a true result" { 47 | $Test.Result | Should -BeTrue 48 | } 49 | 50 | It "Returns an architecture of x86" { 51 | $Test.Architecture | Should -BeExactly "x86" 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/Private/New-MdtApplicationFolder.Tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Public Pester function tests. 4 | #> 5 | [CmdletBinding()] 6 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Justification = "This OK for the tests files.")] 7 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Outputs to log host.")] 8 | param () 9 | 10 | InModuleScope VcRedist { 11 | BeforeDiscovery { 12 | if ($env:PROCESSOR_ARCHITECTURE -eq "AMD64") { 13 | $Skip = $false 14 | } 15 | else { 16 | $Skip = $true 17 | } 18 | } 19 | 20 | BeforeAll { 21 | # Install the MDT Workbench 22 | & "$env:GITHUB_WORKSPACE\tests\Install-Mdt.ps1" 23 | Import-Module -Name "$Env:ProgramFiles\Microsoft Deployment Toolkit\Bin\MicrosoftDeploymentToolkit.psd1" 24 | } 25 | 26 | Describe 'New-MdtApplicationFolder' -Skip:$Skip { 27 | BeforeAll { 28 | New-MdtDrive -Drive "DS020" -Path "$Env:RUNNER_TEMP\Deployment" 29 | Restore-MDTPersistentDrive -Force > $null 30 | } 31 | 32 | Context "Validates New-MdtApplicationFolder" { 33 | It "Does not throw when creating an application folder" { 34 | { New-MdtApplicationFolder -Drive "DS020:" -Name "Test1" -Verbose } | Should -Not -Throw 35 | } 36 | 37 | It "Returns true if the application folder is created" { 38 | New-MdtApplicationFolder -Drive "DS020:" -Name "Test2" -Verbose | Should -BeTrue 39 | } 40 | 41 | It "It throws when referencing a drive that does not exist" { 42 | { New-MdtApplicationFolder -Drive "DS021:" -Name "Test1" -Verbose } | Should -Throw 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Private/New-MdtDrive.Tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Public Pester function tests. 4 | #> 5 | [CmdletBinding()] 6 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Justification = "This OK for the tests files.")] 7 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Outputs to log host.")] 8 | param () 9 | 10 | InModuleScope -ModuleName "VcRedist" { 11 | BeforeDiscovery { 12 | if ($env:PROCESSOR_ARCHITECTURE -eq "AMD64") { 13 | $Skip = $false 14 | } 15 | else { 16 | $Skip = $true 17 | } 18 | } 19 | 20 | BeforeAll { 21 | } 22 | 23 | Describe -Name "New-MdtDrive" -Skip:$Skip { 24 | BeforeAll { 25 | # Install the MDT Workbench 26 | & "$env:GITHUB_WORKSPACE\tests\Install-Mdt.ps1" 27 | Import-Module -Name "$Env:ProgramFiles\Microsoft Deployment Toolkit\Bin\MicrosoftDeploymentToolkit.psd1" 28 | } 29 | 30 | Context "Creates a new MDT drive" { 31 | It "Does not throw when connecting to an MDT share" { 32 | $Path = "$Env:RUNNER_TEMP\Deployment" 33 | { $Drive = New-MdtDrive -Drive "DS020" -Path $Path } | Should -Not -Throw 34 | } 35 | 36 | It "Returns the expected MDT drive name" { 37 | Remove-PSDrive -Name "DS020" -ErrorAction "SilentlyContinue" 38 | $Path = "$Env:RUNNER_TEMP\Deployment" 39 | New-MdtDrive -Drive "DS020" -Path $Path | Should -Be "DS020" 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /VcRedist/Public/Get-InstalledVcRedist.ps1: -------------------------------------------------------------------------------- 1 | function Get-InstalledVcRedist { 2 | <# 3 | .EXTERNALHELP VcRedist-help.xml 4 | #> 5 | [CmdletBinding(SupportsShouldProcess = $false, HelpURI = "https://vcredist.com/get-installedvcredist/")] 6 | [OutputType([System.Management.Automation.PSObject])] 7 | param ( 8 | [Parameter(Mandatory = $false)] 9 | [System.Management.Automation.SwitchParameter] $ExportAll 10 | ) 11 | 12 | if ($PSBoundParameters.ContainsKey("ExportAll")) { 13 | # If -ExportAll used, export everything instead of filtering for the primary Redistributable 14 | # Get all installed Visual C++ Redistributables installed components 15 | Write-Verbose -Message "-ExportAll specified. Exporting all install Visual C++ Redistributables and runtimes." 16 | $Filter = "(Microsoft Visual C.*).*" 17 | } 18 | else { 19 | $Filter = "(Microsoft Visual C.*)(\bRedistributable).*" 20 | } 21 | 22 | # Get all installed Visual C++ Redistributables installed components 23 | Write-Verbose -Message "Matching installed VcRedists with: '$Filter'." 24 | $VcRedists = Get-InstalledSoftware | Where-Object { $_.Name -match $Filter } 25 | 26 | # Add Architecture property to each entry 27 | Write-Verbose -Message "Add Architecture property to output object." 28 | $VcRedists | ForEach-Object { 29 | if ($_.Name -like "*x64*") { $_.Architecture = "x64" } 30 | if ($_.Name -like "*Arm64*") { $_.Architecture = "ARM64" } 31 | } 32 | 33 | # Write the installed VcRedists to the pipeline 34 | Write-Output -InputObject $VcRedists 35 | } 36 | -------------------------------------------------------------------------------- /VcRedist/Private/New-MdtDrive.ps1: -------------------------------------------------------------------------------- 1 | function New-MdtDrive { 2 | <# 3 | .SYNOPSIS 4 | Creates a new persistent PS drive mapped to an MDT share. 5 | 6 | .NOTES 7 | Author: Aaron Parker 8 | Twitter: @stealthpuppy 9 | 10 | .PARAMETER Path 11 | A path to a Microsoft Deployment Toolkit share. 12 | 13 | .PARAMETER Drive 14 | A PS drive letter to map to the MDT share. 15 | #> 16 | [CmdletBinding(SupportsShouldProcess = $true)] 17 | [OutputType([System.String])] 18 | param ( 19 | [Parameter(Mandatory = $false, Position = 0)] 20 | [ValidateNotNullOrEmpty()] 21 | [System.String] $Drive = "DS099", 22 | 23 | [Parameter(Mandatory = $true, Position = 1)] 24 | [ValidateNotNullOrEmpty()] 25 | [System.String] $Path 26 | ) 27 | 28 | # Set a description to be applied to the new MDT drive 29 | $Description = "MDT drive created by $($MyInvocation.MyCommand)" 30 | 31 | if ($PSCmdlet.ShouldProcess("$($Drive): to $($Path)", "Mapping")) { 32 | $params = @{ 33 | Name = $Drive 34 | PSProvider = "MDTProvider" 35 | Root = $Path 36 | Description = $Description 37 | ErrorAction = "Stop" 38 | } 39 | New-PSDrive @params | Add-MDTPersistentDrive | Out-Null 40 | 41 | # Return the MDT drive name 42 | $psDrive = Get-MdtPersistentDrive | Where-Object { $_.Path -eq $Path -and $_.Name -eq $Drive } 43 | Write-Verbose -Message "Found: $($psDrive.Name)" 44 | Write-Output -InputObject $psDrive.Name 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Please describe your pull request in detail. 4 | 5 | ## Related Issue 6 | 7 | This project only accepts pull requests related to open issues. 8 | 9 | * If suggesting a new feature or change, please discuss it in an issue first. 10 | * If fixing a bug, there should be an issue describing it with steps to reproduce 11 | 12 | _Please link to the issue here_ 13 | 14 | ## Motivation and Context 15 | 16 | Why is this change required? What problem does it solve? 17 | 18 | ## How Has This Been Tested? 19 | 20 | * Please describe in detail how you tested your changes. 21 | * Include details of your testing environment, and the tests you ran to see how your change affects other areas of the code, etc. 22 | 23 | ## Screenshots (if appropriate): 24 | 25 | ## Types of changes 26 | 27 | What types of changes does your code introduce? Put an `x` in all the boxes that apply: 28 | - [ ] Bug fix (non-breaking change which fixes an issue) 29 | - [ ] New feature (non-breaking change which adds functionality) 30 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 31 | 32 | ## Checklist: 33 | 34 | Go over all the following points, and put an `x` in all the boxes that apply. If you're unsure about any of these, don't hesitate to ask. We're here to help! 35 | - [ ] My code follows the code style of this project. 36 | - [ ] My change requires a change to the documentation. 37 | - [ ] I have updated the documentation accordingly. 38 | - [ ] I have updated the CHANGELOG file accordingly for the version that this merge modifies. 39 | - [ ] I have added tests to cover my changes. 40 | - [ ] All new and existing tests passed. 41 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "yaml.schemas": { 3 | "https://squidfunk.github.io/mkdocs-material/schema.json": "mkdocs.yml" 4 | }, 5 | "cSpell.words": [ 6 | "aaronparker", 7 | "arithmatex", 8 | "betterem", 9 | "Bitness", 10 | "codecov", 11 | "codeql", 12 | "contoso", 13 | "Donath", 14 | "Dont", 15 | "ehthumbs", 16 | "fontawesome", 17 | "gcov", 18 | "ghaction", 19 | "globbing", 20 | "GPGKEY", 21 | "GPGPASSPHRASE", 22 | "gpgsign", 23 | "GUIDs", 24 | "HKEY", 25 | "inlinehilite", 26 | "installedvcredist", 27 | "magiclink", 28 | "MAML", 29 | "mkdocs", 30 | "newversion", 31 | "noreboot", 32 | "notmatch", 33 | "NUGETAPIKEY", 34 | "potatoqualitee", 35 | "psmodulecache", 36 | "Pygments", 37 | "pymdown", 38 | "pymdownx", 39 | "robocopy", 40 | "Roboto", 41 | "Runtimes", 42 | "sarif", 43 | "signingkey", 44 | "smartsymbols", 45 | "Snapins", 46 | "softprops", 47 | "squidfunk", 48 | "stefanzweifel", 49 | "superfences", 50 | "TMPDIR", 51 | "vcconfigmgrapplication", 52 | "vcintuneapplication", 53 | "vclist", 54 | "vcmanifest", 55 | "vcmdtapplication", 56 | "vcmdtbundle", 57 | "VcRedist", 58 | "visualstudiologo" 59 | ], 60 | "pester.autoRunOnSave": false, 61 | "powershell.scriptAnalysis.settingsPath": ".rules/PSScriptAnalyzerSettings.psd1", 62 | "powershell.scriptAnalysis.enable": true, 63 | "files.eol": "\n" 64 | } -------------------------------------------------------------------------------- /.github/workflows/publish-module.yml: -------------------------------------------------------------------------------- 1 | name: Publish module to PowerShell Gallery 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | workflow_dispatch: 8 | 9 | jobs: 10 | publish-module: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v6 14 | 15 | - name: Install and cache PowerShell modules 16 | id: psmodulecache 17 | uses: potatoqualitee/psmodulecache@v6.2.1 18 | with: 19 | modules-to-cache: PowerShellGet 20 | shell: pwsh 21 | 22 | # Import GPG key so that we can sign the commit 23 | - name: Import GPG key 24 | id: import_gpg 25 | uses: crazy-max/ghaction-import-gpg@v6 26 | with: 27 | gpg_private_key: ${{ secrets.GPGKEY }} 28 | passphrase: ${{ secrets.GPGPASSPHRASE }} 29 | git_user_signingkey: true 30 | git_commit_gpgsign: true 31 | git_config_global: true 32 | git_tag_gpgsign: true 33 | git_push_gpgsign: false 34 | git_committer_name: ${{ secrets.COMMIT_NAME }} 35 | git_committer_email: ${{ secrets.COMMIT_EMAIL }} 36 | 37 | # Create release 38 | - name: Create release 39 | uses: softprops/action-gh-release@v2 40 | if: startsWith(github.ref, 'refs/tags/') 41 | with: 42 | prerelease: false 43 | 44 | # Push the updated module to the PowerShell Gallery 45 | - name: Push module to PowerShell Gallery 46 | shell: pwsh 47 | run: | 48 | $params = @{ 49 | Path = "${{ github.workspace }}/VcRedist" 50 | NuGetApiKey = "${{ secrets.NUGETAPIKEY }}" 51 | ErrorAction = "Stop" 52 | } 53 | Publish-Module @params 54 | -------------------------------------------------------------------------------- /tests/Install-Mdt.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | Downloads and installs the Microsoft Deployment Toolkit for testing MDT functions 3 | #> 4 | 5 | # Check if the script is running in x64 environment 6 | if ($Env:PROCESSOR_ARCHITECTURE -eq "AMD64") { 7 | 8 | # Download the MDT Workbench 9 | $OutFile = $([System.IO.Path]::Combine($env:RUNNER_TEMP, "MicrosoftDeploymentToolkit_x64.msi")) 10 | if (-not(Test-Path -Path $OutFile)) { 11 | Write-Host "Downloading and installing the Microsoft Deployment Toolkit" 12 | $Url = "https://download.microsoft.com/download/3/3/9/339BE62D-B4B8-4956-B58D-73C4685FC492/MicrosoftDeploymentToolkit_x64.msi" 13 | $ProgressPreference = [System.Management.Automation.ActionPreference]::SilentlyContinue 14 | $params = @{ 15 | Uri = $Url 16 | OutFile = $OutFile 17 | UseBasicParsing = $true 18 | } 19 | Invoke-WebRequest @params 20 | } 21 | 22 | # Install the Microsoft Deployment Toolkit 23 | $MdtModule = "$Env:ProgramFiles\Microsoft Deployment Toolkit\Bin\MicrosoftDeploymentToolkit.psd1" 24 | if (-not(Test-Path -Path $MdtModule)) { 25 | $params = @{ 26 | FilePath = "$env:SystemRoot\System32\msiexec.exe" 27 | ArgumentList = "/package $OutFile /quiet" 28 | NoNewWindow = $true 29 | Wait = $true 30 | PassThru = $false 31 | } 32 | Start-Process @params 33 | } 34 | 35 | # Create a deployment share for testing 36 | $Path = "$Env:RUNNER_TEMP\Deployment" 37 | if (-not(Test-Path -Path "$Path\Control\CustomSettings.ini")) { 38 | Import-Module -Name "$Env:ProgramFiles\Microsoft Deployment Toolkit\Bin\MicrosoftDeploymentToolkit.psd1" 39 | New-Item -Path $Path -ItemType "Directory" -ErrorAction "SilentlyContinue" | Out-Null 40 | $params = @{ 41 | Name = "DS020" 42 | PSProvider = "MDTProvider" 43 | Root = $Path 44 | Description = "MDT Deployment Share" 45 | } 46 | New-PSDrive @params | Add-MDTPersistentDrive 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /VcRedist/Intune.json: -------------------------------------------------------------------------------- 1 | { 2 | "PackageInformation": { 3 | "SetupType": "EXE", 4 | "SetupFile": "vcredist.exe", 5 | "Version": "14.31.31103.0", 6 | "SourceFolder": "Source", 7 | "OutputFolder": "Package", 8 | "IconFile": "" 9 | }, 10 | "Information": { 11 | "DisplayName": "Microsoft Visual C++ Redistributable", 12 | "Description": "Visual C++ Redistributable Packages install runtime components of Visual C++ Libraries.", 13 | "Publisher": "Microsoft", 14 | "InformationURL": "https://visualstudio.microsoft.com/vs/support/", 15 | "PrivacyURL": "https://go.microsoft.com/fwlink/?LinkId=521839", 16 | "Notes": "Imported via VcRedist https://vcredist.com/" 17 | }, 18 | "Program": { 19 | "InstallCommand": "vcredist.exe /install /quiet /norestart", 20 | "UninstallCommand": "msiexec /x{guid} /qn", 21 | "InstallExperience": "system", 22 | "DeviceRestartBehavior": "basedOnReturnCode" 23 | }, 24 | "RequirementRule": { 25 | "MinimumRequiredOperatingSystem": "W10_1809", 26 | "Architecture": "x64", 27 | "SizeInMBValue": "100" 28 | }, 29 | "DetectionRule": [ 30 | { 31 | "Type": "Registry", 32 | "DetectionMethod": "Existence", 33 | "KeyPath": "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{guid}", 34 | "ValueName": "", 35 | "DetectionType": "exists", 36 | "Check32BitOn64System": "false" 37 | }, 38 | { 39 | "Type": "File", 40 | "DetectionMethod": "Version", 41 | "Path": "%SystemRoot%\\System32", 42 | "FileOrFolder": "vcruntime140.dll", 43 | "Operator": "greaterThanOrEqual", 44 | "VersionValue": "#version", 45 | "Check32BitOn64System": "#architecture" 46 | } 47 | ] 48 | } -------------------------------------------------------------------------------- /scripts/Compare-VersionNumber.ps1: -------------------------------------------------------------------------------- 1 | function Compare-VersionNumber { 2 | <# 3 | .SYNOPSIS 4 | Compares two version numbers to determine whether one is greater than the other. 5 | 6 | .DESCRIPTION 7 | Compares two version numbers to determine whether one is greater than the other. 8 | 9 | .NOTES 10 | Author: Aaron Parker 11 | Twitter: @stealthpuppy 12 | 13 | .PARAMETER LowVersion 14 | The lower version number to compare. 15 | 16 | .PARAMETER HighVersion 17 | The higher version number to compare. 18 | #> 19 | [CmdletBinding(SupportsShouldProcess = $false)] 20 | [OutputType([System.Boolean])] 21 | param ( 22 | [Parameter(Mandatory = $true, Position = 0)] 23 | [ValidateNotNull()] 24 | [System.String] $LowVersion, 25 | 26 | [Parameter(Mandatory = $true, Position = 1)] 27 | [ValidateNotNull()] 28 | [System.String] $HighVersion, 29 | 30 | [Parameter(Mandatory = $false)] 31 | [System.Management.Automation.SwitchParameter] $MatchMinor 32 | ) 33 | begin { 34 | # Convert parameters to version numbers 35 | $low = New-Object -TypeName "System.Version" -ArgumentList $LowVersion 36 | $high = New-Object -TypeName "System.Version" -ArgumentList $HighVersion 37 | } 38 | 39 | process { 40 | # Compare versions 41 | if ($MatchMinor) { 42 | if ($high.Major -eq $low.Major) { 43 | $result = $high.Minor -gt $low.Minor 44 | } 45 | else { 46 | # If major version numbers don't match return false 47 | $result = $false 48 | } 49 | } 50 | else { 51 | $result = $high -gt $low 52 | } 53 | } 54 | 55 | end { 56 | # Return result 57 | Write-Output -InputObject $result 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/Private/Test-VcListObject.Tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Public Pester function tests. 4 | #> 5 | [CmdletBinding()] 6 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Justification = "This OK for the tests files.")] 7 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Outputs to log host.")] 8 | param () 9 | 10 | BeforeDiscovery { 11 | } 12 | 13 | InModuleScope VcRedist { 14 | BeforeAll { 15 | $ValidObject = [PSCustomObject]@{ 16 | Name = "Visual C++ Redistributable for Visual Studio 14" 17 | ProductCode = "{6ba9fb5e-8366-4cc4-bf65-25fe9819b2fc}" 18 | Version = "14.34.31931.0" 19 | URL = "https://www.visualstudio.com/downloads/" 20 | URI = "https://aka.ms/vs/17/release/VC_redist.x86.exe" 21 | Release = "14" 22 | Architecture = "x86" 23 | Install = "/install /passive /norestart" 24 | SilentInstall = "/install /quiet /norestart" 25 | SilentUninstall = "%ProgramData%\Package Cache\{6ba9fb5e-8366-4cc4-bf65-25fe9819b2fc}\VC_redist.x86.exe /uninstall /quiet /norestart" 26 | UninstallKey = "32" 27 | Path = "C:\Temp\VcRedist.exe" 28 | PackageId = "b1e3c2a7-8f2d-4c3a-9e2a-7c4b1e2d3f4a" 29 | DetectionFile = "%SystemRoot%\\System32\\vcruntime140.dll" 30 | } 31 | 32 | $InvalidObject = [PSCustomObject]@{ 33 | Property1 = "Visual C++ Redistributable for Visual Studio 14" 34 | Property2 = "{6ba9fb5e-8366-4cc4-bf65-25fe9819b2fc}" 35 | } 36 | } 37 | 38 | Describe -Name "Test-VcListObject" { 39 | Context "Test-VcListObject validates a valid VcList object" { 40 | It "Should not throw with a valid object" { 41 | { Test-VcListObject -VcList $ValidObject } | Should -Not -Throw 42 | } 43 | 44 | It "Should return true a valid object" { 45 | Test-VcListObject -VcList $ValidObject | Should -BeTrue 46 | } 47 | } 48 | 49 | Context "Test-VcListObject validates an invalid VcList object" { 50 | It "Should throw with a valid object" { 51 | { Test-VcListObject -VcList $InvalidObject } | Should -Throw 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/Private/Import-MdtModule.Tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Public Pester function tests. 4 | #> 5 | [CmdletBinding()] 6 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Justification = "This OK for the tests files.")] 7 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Outputs to log host.")] 8 | param () 9 | 10 | InModuleScope VcRedist { 11 | BeforeDiscovery { 12 | if ($env:PROCESSOR_ARCHITECTURE -eq "AMD64") { 13 | $Skip = $false 14 | } 15 | else { 16 | $Skip = $true 17 | } 18 | } 19 | 20 | BeforeAll { 21 | } 22 | 23 | Describe -Name "Import-MdtModule without MDT installed" -Skip:$Skip { 24 | Context "Import-MdtModule without MDT installed" { 25 | It "Should throw when MDT is not installed" { 26 | { Import-MdtModule } | Should -Throw 27 | } 28 | } 29 | } 30 | 31 | Describe -Name "Import-MdtModule with MDT installed OK" -Skip:$Skip { 32 | BeforeAll { 33 | # Install the MDT Workbench 34 | & "$env:GITHUB_WORKSPACE\tests\Install-Mdt.ps1" 35 | } 36 | 37 | Context "Import-MdtModule with MDT installed OK" { 38 | It "Should return true if the module is installed" { 39 | Import-MdtModule -Force | Should -BeTrue 40 | } 41 | } 42 | } 43 | 44 | Describe -Name "Import-MdtModule fails with MDT installed but module missing" -Skip:$Skip { 45 | BeforeAll { 46 | $RegPath = "HKLM:SOFTWARE\Microsoft\Deployment 4" 47 | $MdtReg = Get-ItemProperty -Path $RegPath -ErrorAction "SilentlyContinue" 48 | $MdtInstallDir = Resolve-Path -Path $MdtReg.Install_Dir 49 | $MdtModule = [System.IO.Path]::Combine($MdtInstallDir, "bin", "MicrosoftDeploymentToolkit.psd1") 50 | Rename-Item -Path $MdtModule -NewName "MicrosoftDeploymentToolkit.psd1.rename" 51 | } 52 | 53 | Context "Import-MdtModule with MDT module file missing" { 54 | It "Should throw when MDT module file is missing" { 55 | { Import-MdtModule } | Should -Throw 56 | } 57 | } 58 | 59 | AfterAll { 60 | $MdtModule = [System.IO.Path]::Combine($MdtInstallDir, "bin", "MicrosoftDeploymentToolkit.psd1.rename") 61 | Rename-Item -Path $MdtModule -NewName "MicrosoftDeploymentToolkit.psd1" 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/Public/Get-InstalledVcRedist.Tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Public Pester function tests. 4 | #> 5 | [CmdletBinding()] 6 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Justification = "This OK for the tests files.")] 7 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Outputs to log host.")] 8 | param () 9 | 10 | BeforeDiscovery { 11 | $VcList = Get-InstalledVcRedist 12 | $AllVcList = Get-InstalledVcRedist -ExportAll 13 | } 14 | 15 | Describe -Name "Get-InstalledVcRedist" { 16 | Context "Validate Get-InstalledVcRedist" { 17 | It "Should not throw" { 18 | { Get-InstalledVcRedist } | Should -Not -Throw 19 | } 20 | 21 | It "Should not throw with -ExportAll" { 22 | { Get-InstalledVcRedist -ExportAll } | Should -Not -Throw 23 | } 24 | } 25 | } 26 | 27 | Describe -Name "Get-InstalledVcRedist with default VcRedists" -ForEach $VcList { 28 | Context "Validate Get-InstalledVcRedist array properties" { 29 | It "VcRedist <_.Name> has expected Name property" { 30 | [System.Boolean]$_.Name | Should -BeTrue 31 | } 32 | 33 | It "VcRedist <_.Name> has expected Version property" { 34 | [System.Boolean]$_.Version | Should -BeTrue 35 | } 36 | 37 | It "VcRedist <_.Name> has expected ProductCode property" { 38 | [System.Boolean]$_.ProductCode | Should -BeTrue 39 | } 40 | 41 | It "VcRedist <_.Name> has expected UninstallString property" { 42 | [System.Boolean]$_.UninstallString | Should -BeTrue 43 | } 44 | } 45 | } 46 | 47 | Describe -Name "Get-InstalledVcRedist with all VcRedists" -ForEach $AllVcList { 48 | Context "Validate Get-InstalledVcRedist array properties" { 49 | It "VcRedist <_.Name> has expected Name property" { 50 | [System.Boolean]$_.Name | Should -BeTrue 51 | } 52 | 53 | It "VcRedist <_.Name> has expected Version property" { 54 | [System.Boolean]$_.Version | Should -BeTrue 55 | } 56 | 57 | It "VcRedist <_.Name> has expected ProductCode property" { 58 | [System.Boolean]$_.ProductCode | Should -BeTrue 59 | } 60 | 61 | It "VcRedist <_.Name> has expected UninstallString property" { 62 | [System.Boolean]$_.UninstallString | Should -BeTrue 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /VcRedist/Private/New-MdtApplicationFolder.ps1: -------------------------------------------------------------------------------- 1 | function New-MdtApplicationFolder { 2 | <# 3 | .SYNOPSIS 4 | Creates a new Application folder in an MDT deployment share. 5 | 6 | .NOTES 7 | Author: Aaron Parker 8 | Twitter: @stealthpuppy 9 | 10 | .PARAMETER Drive 11 | A PS drive letter mapped to the MDT share. 12 | 13 | .PARAMETER Name 14 | A folder name to create below the MDT Applications folder. 15 | #> 16 | [CmdletBinding(SupportsShouldProcess = $true)] 17 | [OutputType([System.Boolean])] 18 | param ( 19 | [Parameter(Mandatory = $true, Position = 0)] 20 | [ValidateNotNullOrEmpty()] 21 | [System.String] $Drive, 22 | 23 | [Parameter(Mandatory = $true, Position = 1)] 24 | [ValidateNotNullOrEmpty()] 25 | [Alias("AppFolder")] 26 | [System.String] $Name, 27 | 28 | [Parameter(Mandatory = $false, Position = 2)] 29 | [ValidateNotNullOrEmpty()] 30 | [System.String] $Description = "Microsoft Visual C++ Redistributables imported with VcRedist https://vcredist.com/" 31 | ) 32 | 33 | # Create a sub-folder below Applications to import the Redistributables into 34 | $MdtPath = "$($Drive)\Applications\$($Name)" 35 | 36 | if (Test-Path -Path $MdtPath) { 37 | Write-Verbose -Message "MDT folder exists: $MdtPath" 38 | Write-Output -InputObject $true 39 | } 40 | else { 41 | if ($PSCmdlet.ShouldProcess($MdtPath, "Create folder")) { 42 | try { 43 | # Create -AppFolder below Applications; Splat New-Item parameters 44 | $params = @{ 45 | Path = "$($Drive)\Applications" 46 | Enable = "True" 47 | Name = $Name 48 | Comments = $Description 49 | ItemType = "Folder" 50 | ErrorAction = "Continue" 51 | } 52 | New-Item @params | Out-Null 53 | } 54 | catch [System.Exception] { 55 | throw $_ 56 | } 57 | Write-Output -InputObject $true 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /VcRedist/Private/Import-MdtModule.ps1: -------------------------------------------------------------------------------- 1 | function Import-MdtModule { 2 | <# 3 | .SYNOPSIS 4 | Tests for and imports the MDT PowerShell module. Returns True or False depending on whether the module can be loaded. 5 | 6 | .NOTES 7 | Author: Aaron Parker 8 | Twitter: @stealthpuppy 9 | 10 | .PARAMETER Force 11 | Re-imports the MDT module and its members, even if the module or its members have an access mode of read-only. 12 | #> 13 | [CmdletBinding(SupportsShouldProcess = $false)] 14 | [OutputType([System.Boolean])] 15 | param ( 16 | [Parameter(Mandatory = $false)] 17 | [System.Management.Automation.SwitchParameter] $Force 18 | ) 19 | 20 | # Get path to the MDT PowerShell module via the Registry and fail if we can't read the properties 21 | $RegPath = "HKLM:SOFTWARE\Microsoft\Deployment 4" 22 | if (Test-Path -Path $RegPath -ErrorAction "SilentlyContinue") { 23 | Write-Verbose -Message "Get MDT details from registry at: $RegPath" 24 | $MdtReg = Get-ItemProperty -Path $RegPath -ErrorAction "SilentlyContinue" 25 | } 26 | else { 27 | $Msg = "Unable to read MDT Registry path properties at '$RegPath'. Ensure the Microsoft Deployment Toolkit is installed and try again." 28 | throw [System.IO.DirectoryNotFoundException]::New($Msg) 29 | } 30 | 31 | # Attempt to load the module 32 | $MdtInstallDir = Resolve-Path -Path $MdtReg.Install_Dir 33 | $MdtModule = [System.IO.Path]::Combine($MdtInstallDir, "bin", "MicrosoftDeploymentToolkit.psd1") 34 | if (Test-Path -Path $mdtModule -ErrorAction "SilentlyContinue") { 35 | try { 36 | Write-Verbose -Message "Loading MDT module from: $MdtInstallDir." 37 | $params = @{ 38 | Name = $MdtModule 39 | ErrorAction = "Stop" 40 | Force = if ($Force) { $true } else { $false } 41 | } 42 | Import-Module @params 43 | Write-Output -InputObject $true 44 | } 45 | catch { 46 | throw $_ 47 | } 48 | } 49 | else { 50 | $Msg = "Unable to find the MDT PowerShell module at $MdtModule. Ensure the Microsoft Deployment Toolkit is installed and try again." 51 | throw [System.IO.FileNotFoundException]::New($Msg) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/Public/Import-VcMdtApplication.Tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Public Pester function tests. 4 | #> 5 | [CmdletBinding()] 6 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Justification = "This OK for the tests files.")] 7 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Outputs to log host.")] 8 | param () 9 | 10 | BeforeDiscovery { 11 | $SupportedReleases = @("2015", "2017", "2019", "14") 12 | if ($env:PROCESSOR_ARCHITECTURE -eq "AMD64") { 13 | $Skip = $false 14 | } 15 | else { 16 | $Skip = $true 17 | } 18 | } 19 | 20 | Describe -Name "Import-VcMdtApplication with " -ForEach $SupportedReleases -Skip:$Skip { 21 | BeforeAll { 22 | if ($env:PROCESSOR_ARCHITECTURE -eq "AMD64") { 23 | $Skip = $false 24 | 25 | # Install the MDT Workbench 26 | & "$env:GITHUB_WORKSPACE\tests\Install-Mdt.ps1" 27 | 28 | $Release = $_ 29 | $Path = $([System.IO.Path]::Combine($env:RUNNER_TEMP, "Downloads")) 30 | New-Item -Path $Path -ItemType "Directory" -ErrorAction "SilentlyContinue" | Out-Null 31 | 32 | $VcListX64 = Get-VcList -Release $Release -Architecture "x64" | Save-VcRedist -Path $Path 33 | $VcListX86 = Get-VcList -Release $Release -Architecture "x86" | Save-VcRedist -Path $Path 34 | } 35 | else { 36 | $Skip = $true 37 | } 38 | } 39 | 40 | Context "Import-VcMdtApplication imports Redistributables into the MDT share" { 41 | It "Imports the x64 Redistributables into MDT OK" { 42 | $params = @{ 43 | VcList = $VcListX64 44 | MdtPath = "$env:RUNNER_TEMP\Deployment" 45 | AppFolder = "VcRedists" 46 | Silent = $true 47 | DontHide = $true 48 | Force = $true 49 | MdtDrive = "DS020" 50 | Publisher = "Microsoft" 51 | Language = "en-US" 52 | } 53 | { Import-VcMdtApplication @params } | Should -Not -Throw 54 | } 55 | 56 | It "Imports the x86 Redistributables into MDT OK" { 57 | $params = @{ 58 | VcList = $VcListX86 59 | MdtPath = "$env:RUNNER_TEMP\Deployment" 60 | AppFolder = "VcRedists" 61 | Silent = $true 62 | DontHide = $true 63 | Force = $true 64 | MdtDrive = "DS020" 65 | Publisher = "Microsoft" 66 | Language = "en-US" 67 | } 68 | { Import-VcMdtApplication @params } | Should -Not -Throw 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /VcRedist/Private/Test-VcListObject.ps1: -------------------------------------------------------------------------------- 1 | function Test-VcListObject { 2 | <# 3 | .SYNOPSIS 4 | Returns True if running on PowerShell Core. 5 | 6 | .NOTES 7 | Author: Aaron Parker 8 | Twitter: @stealthpuppy 9 | 10 | .PARAMETER InputObject 11 | The InputObject to validate RequiredProperties against 12 | 13 | .PARAMETER Version 14 | An array of the require properties to validate against the InputObject 15 | #> 16 | [CmdletBinding()] 17 | [OutputType([System.Boolean])] 18 | param ( 19 | [Parameter( 20 | Mandatory = $true, 21 | Position = 0, 22 | ValueFromPipeline = $true, 23 | HelpMessage = "Pass a VcList object from Get-VcList.")] 24 | [ValidateNotNullOrEmpty()] 25 | [System.Management.Automation.PSObject] $VcList, 26 | 27 | [Parameter(Position = 1)] 28 | [System.String[]] $RequiredProperties = @("Architecture", "Install", "Name", "ProductCode", 29 | "Release", "SilentInstall", "SilentUninstall", "UninstallKey", "URI", "URL", "Version", 30 | "Path", "PackageId", "DetectionFile") 31 | ) 32 | 33 | process { 34 | foreach ($Item in $VcList) { 35 | $Members = Get-Member -InputObject $Item -MemberType "NoteProperty" 36 | $params = @{ 37 | ReferenceObject = $RequiredProperties 38 | DifferenceObject = $Members.Name 39 | PassThru = $true 40 | ErrorAction = "Stop" 41 | } 42 | $MissingProperties = Compare-Object @params 43 | 44 | if (-not($missingProperties)) { 45 | $Result = $true 46 | } 47 | else { 48 | $MissingProperties | ForEach-Object { 49 | throw [System.Management.Automation.ValidationMetadataException] "Property: '$_' missing." 50 | } 51 | } 52 | 53 | $Item.PSObject.Properties | ForEach-Object { 54 | if (([System.String]::IsNullOrEmpty($_.Value))) { 55 | throw [System.Management.Automation.ValidationMetadataException] "Property '$($_.Name)' is null or empty." 56 | } 57 | } 58 | } 59 | 60 | # Return true if all is good with the object 61 | return $Result 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/Public/Import-VcIntuneApplication.Tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Public Pester function tests. 4 | #> 5 | [CmdletBinding()] 6 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Justification = "This OK for the tests files.")] 7 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Outputs to log host.")] 8 | param () 9 | 10 | BeforeDiscovery { 11 | $SupportedReleases = @("14") 12 | if ($env:PROCESSOR_ARCHITECTURE -eq "AMD64") { 13 | $Skip = $false 14 | } 15 | else { 16 | $Skip = $true 17 | } 18 | } 19 | 20 | BeforeAll { 21 | } 22 | 23 | Describe -Name "Import-VcIntuneApplication without IntuneWin32App" -ForEach $SupportedReleases -Skip:$Skip { 24 | Context "Validate Import-VcIntuneApplication fail scenarios" { 25 | It "Should fail without IntuneWin32App" { 26 | { Import-VcIntuneApplication -VcList (Get-VcList -Release $_) } | Should -Throw 27 | } 28 | } 29 | } 30 | 31 | Describe -Name "Import-VcIntuneApplication imports VcRedists" -ForEach $SupportedReleases -Skip:$Skip { 32 | BeforeAll { 33 | foreach ($Module in @("MSAL.PS", "IntuneWin32App")) { 34 | Install-Module -Name $Module -Force 35 | } 36 | } 37 | 38 | Context "Validate Import-VcIntuneApplication fail scenarios" { 39 | It "Should fail without an authentication token" { 40 | { Import-VcIntuneApplication -VcList (Get-VcList -Release $_) } | Should -Throw 41 | } 42 | } 43 | 44 | # Context "Import-VcIntuneApplication imports VcRedists into a target tenant" { 45 | # BeforeAll { 46 | # try { 47 | # # Authenticate to the Graph API 48 | # # Expects secrets to be passed into environment variables 49 | # Write-Information -MessageData "Authenticate to the Graph API" 50 | # $params = @{ 51 | # TenantId = "$env:TENANT_ID" 52 | # ClientId = "$env:CLIENT_ID" 53 | # ClientSecret = "$env:CLIENT_SECRET" 54 | # } 55 | # $script:AuthToken = Connect-MSIntuneGraph @params 56 | # } 57 | # catch { 58 | # throw $_ 59 | # } 60 | # } 61 | 62 | # It "Imports VcRedist into the target tenant OK" { 63 | # # Path with VcRedist downloads 64 | # $Path = "$env:RUNNER_TEMP\Deployment" 65 | # $SavedVcRedist = Save-VcRedist -Path $Path -VcList (Get-VcList -Release $_ -Architecture "x64") 66 | # { Import-VcIntuneApplication -VcList $SavedVcRedist | Out-Null } | Should -Not -Throw 67 | # } 68 | # } 69 | } 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VcRedist 2 | 3 | [![License][license-badge]][license] 4 | [![PowerShell Gallery Version][psgallery-version-badge]][psgallery] 5 | [![PowerShell Gallery][psgallery-badge]][psgallery] 6 | 7 | ## About 8 | 9 | VcRedist is a PowerShell module for lifecycle management of the [Microsoft Visual C++ Redistributables](https://learn.microsoft.com/en-au/cpp/windows/latest-supported-vc-redist). VcRedist downloads the supported (and unsupported) Redistributables, for local install, gold image deployment or importing as applications into the Microsoft Deployment Toolkit, Microsoft Configuration Manager or Microsoft Intune. Supports passive and silent installs and uninstalls of the Visual C++ Redistributables. 10 | 11 | [![validate-module](https://github.com/aaronparker/vcredist/actions/workflows/validate-module.yml/badge.svg)](https://github.com/aaronparker/vcredist/actions/workflows/validate-module.yml) [![codecov](https://codecov.io/gh/aaronparker/vcredist/branch/main/graph/badge.svg?token=0AUlPVPhiQ)](https://codecov.io/gh/aaronparker/vcredist) 12 | 13 | ### Visual C++ Redistributables 14 | 15 | The Microsoft Visual C++ Redistributables are a core component of any Windows desktop deployment. Because multiple versions are often deployed they need to be imported into your deployment solution or installed locally, which can be time consuming. The aim of this module is to provide a definitive list of available Redistributables and functions for managing deployment of those Redistributables across various mechanisms. 16 | 17 | ### Documentation 18 | 19 | Full documentation for the module is located at [https://vcredist.com/](https://vcredist.com/) 20 | 21 | ### PowerShell Gallery 22 | 23 | The VcRedist module is published to the PowerShell Gallery and can be found here: [VcRedist](https://www.powershellgallery.com/packages/VcRedist/). Install the module from the gallery with: 24 | 25 | ```powershell 26 | Install-Module -Name "VcRedist" -Force 27 | ``` 28 | 29 | [psgallery-badge]: https://img.shields.io/powershellgallery/dt/vcredist.svg?logo=PowerShell&style=flat-square 30 | [psgallery]: https://www.powershellgallery.com/packages/vcredist 31 | [psgallery-version-badge]: https://img.shields.io/powershellgallery/v/vcredist.svg?logo=PowerShell&style=flat-square 32 | [license-badge]: https://img.shields.io/github/license/aaronparker/Install-VisualCRedistributables.svg?style=flat-square 33 | [license]: https://github.com/aaronparker/vcredist/blob/main/LICENSE 34 | -------------------------------------------------------------------------------- /tests/Public/Import-VcConfigMgrApplication.Tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Public Pester function tests. 4 | #> 5 | [CmdletBinding()] 6 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Justification = "This OK for the tests files.")] 7 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Outputs to log host.")] 8 | param () 9 | 10 | BeforeDiscovery { 11 | $SupportedReleases = @("14") 12 | if ($env:PROCESSOR_ARCHITECTURE -eq "AMD64") { 13 | $Skip = $false 14 | } 15 | else { 16 | $Skip = $true 17 | } 18 | } 19 | 20 | Describe -Name "Validate Import-VcConfigMgrApplication" -ForEach $SupportedReleases -Skip:$Skip { 21 | BeforeAll { 22 | $Release = $_ 23 | $Path = $([System.IO.Path]::Combine($env:RUNNER_TEMP, "Downloads")) 24 | New-Item -Path $Path -ItemType "Directory" -ErrorAction "SilentlyContinue" | Out-Null 25 | $VcList = Save-VcRedist -Path $Path -VcList (Get-VcList -Release $Release) 26 | } 27 | 28 | Context "ConfigMgr is not installed" { 29 | It "Should throw when the ConfigMgr module is not installed" { 30 | $params = @{ 31 | VcList = $VcList 32 | CMPath = $env:RUNNER_TEMP 33 | SMSSiteCode = "LAB" 34 | AppFolder = "VcRedists" 35 | Silent = $true 36 | NoCopy = $true 37 | Publisher = "Microsoft" 38 | Keyword = "Visual C++ Redistributable" 39 | } 40 | { Import-VcConfigMgrApplication @params } | Should -Throw 41 | } 42 | } 43 | 44 | Context "ConfigMgr is not installed but env:SMS_ADMIN_UI_PATH set to a valid path" { 45 | BeforeAll { 46 | [Environment]::SetEnvironmentVariable("SMS_ADMIN_UI_PATH", "$env:RUNNER_TEMP") 47 | } 48 | 49 | It "Should throw when env:SMS_ADMIN_UI_PATH is valid but module does not exist" { 50 | $params = @{ 51 | VcList = $VcList 52 | CMPath = $env:RUNNER_TEMP 53 | SMSSiteCode = "LAB" 54 | AppFolder = "VcRedists" 55 | Silent = $true 56 | NoCopy = $true 57 | Publisher = "Microsoft" 58 | Keyword = "Visual C++ Redistributable" 59 | } 60 | { Import-VcConfigMgrApplication @params } | Should -Throw 61 | } 62 | } 63 | 64 | Context "ConfigMgr is not installed but env:SMS_ADMIN_UI_PATH set to an invalid path" { 65 | BeforeAll { 66 | [Environment]::SetEnvironmentVariable("SMS_ADMIN_UI_PATH", "$env:RUNNER_TEMP\Test") 67 | } 68 | 69 | It "Should throw when env:SMS_ADMIN_UI_PATH is invalid" { 70 | $params = @{ 71 | VcList = $VcList 72 | CMPath = $env:RUNNER_TEMP 73 | SMSSiteCode = "LAB" 74 | AppFolder = "VcRedists" 75 | Silent = $true 76 | NoCopy = $true 77 | Publisher = "Microsoft" 78 | Keyword = "Visual C++ Redistributable" 79 | } 80 | { Import-VcConfigMgrApplication @params } | Should -Throw 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /VcRedist/Private/Invoke-Process.ps1: -------------------------------------------------------------------------------- 1 | function Invoke-Process { 2 | <#PSScriptInfo 3 | .VERSION 1.4 4 | .GUID b787dc5d-8d11-45e9-aeef-5cf3a1f690de 5 | .AUTHOR Adam Bertram 6 | .COMPANYNAME Adam the Automator, LLC 7 | .TAGS Processes 8 | #> 9 | <# 10 | .DESCRIPTION 11 | Invoke-Process is a simple wrapper function that aims to "PowerShellyify" launching typical external processes. There 12 | are lots of ways to invoke processes in PowerShell with Start-Process, Invoke-Expression, & and others but none account 13 | well for the various streams and exit codes that an external process returns. Also, it's hard to write good tests 14 | when launching external processes. 15 | 16 | This function ensures any errors are sent to the error stream, standard output is sent via the Output stream and any 17 | time the process returns an exit code other than 0, treat it as an error. 18 | #> 19 | [CmdletBinding(SupportsShouldProcess = $true)] 20 | param ( 21 | [Parameter(Mandatory)] 22 | [ValidateNotNullOrEmpty()] 23 | [System.String] $FilePath, 24 | 25 | [Parameter()] 26 | [ValidateNotNullOrEmpty()] 27 | [System.String] $ArgumentList 28 | ) 29 | 30 | $ErrorActionPreference = "Stop" 31 | try { 32 | $stdOutTempFile = "$env:TEMP\$((New-Guid).Guid)" 33 | $stdErrTempFile = "$env:TEMP\$((New-Guid).Guid)" 34 | 35 | $startProcessParams = @{ 36 | FilePath = $FilePath 37 | ArgumentList = $ArgumentList 38 | RedirectStandardError = $stdErrTempFile 39 | RedirectStandardOutput = $stdOutTempFile 40 | Wait = $true 41 | PassThru = $true 42 | NoNewWindow = $true 43 | } 44 | if ($PSCmdlet.ShouldProcess("Process [$($FilePath)]", "Run with args: [$($ArgumentList)]")) { 45 | $cmd = Start-Process @startProcessParams 46 | 47 | $cmdOutput = Get-Content -Path $stdOutTempFile -Raw 48 | $cmdError = Get-Content -Path $stdErrTempFile -Raw 49 | if ($cmd.ExitCode -ne 0) { 50 | if ($cmdError) { 51 | throw $cmdError.Trim() 52 | } 53 | if ($cmdOutput) { 54 | throw $cmdOutput.Trim() 55 | } 56 | } 57 | else { 58 | if ([System.String]::IsNullOrEmpty($cmdOutput) -eq $false) { 59 | Write-Output -InputObject $cmdOutput 60 | } 61 | } 62 | } 63 | } 64 | catch { 65 | $PSCmdlet.ThrowTerminatingError($_) 66 | } 67 | finally { 68 | Remove-Item -Path $stdOutTempFile, $stdErrTempFile -Force -ErrorAction "Ignore" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/Public/Remove-VcIntuneApplication.Tests.ps1: -------------------------------------------------------------------------------- 1 | <#! 2 | .SYNOPSIS 3 | Pester tests for Remove-VcIntuneApplication 4 | #> 5 | [CmdletBinding()] 6 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Justification = "This OK for the tests files.")] 7 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Outputs to log host.")] 8 | param () 9 | 10 | BeforeDiscovery { 11 | $SupportedReleases = @("2017") 12 | if ($env:PROCESSOR_ARCHITECTURE -eq "AMD64") { 13 | $Skip = $false 14 | } 15 | else { 16 | $Skip = $true 17 | } 18 | } 19 | 20 | Describe "Remove-VcIntuneApplication" -Skip:$Skip { 21 | BeforeAll { 22 | } 23 | 24 | Context "Parameter validation" { 25 | It "Should throw when VcList is null" { 26 | { Remove-VcIntuneApplication -VcList $null } | Should -Throw 27 | } 28 | } 29 | 30 | # Context "Import-VcIntuneApplication imports VcRedists into a target tenant" { 31 | # BeforeAll { 32 | # foreach ($Module in @("MSAL.PS", "IntuneWin32App")) { 33 | # Install-Module -Name $Module -Force 34 | # } 35 | 36 | # try { 37 | # # Authenticate to the Graph API 38 | # # Expects secrets to be passed into environment variables 39 | # Write-Information -MessageData "Authenticate to the Graph API" 40 | # $params = @{ 41 | # TenantId = "$env:TENANT_ID" 42 | # ClientId = "$env:CLIENT_ID" 43 | # ClientSecret = "$env:CLIENT_SECRET" 44 | # } 45 | # $script:AuthToken = Connect-MSIntuneGraph @params 46 | # } 47 | # catch { 48 | # throw $_ 49 | # } 50 | 51 | # New-Item -Path "$env:RUNNER_TEMP\Deployment" -ItemType Directory -Force | Out-Null 52 | # $Path = "$env:RUNNER_TEMP\Deployment" 53 | # $SavedVcRedist = Save-VcRedist -Path $Path -VcList (Get-VcList -Release "2017" ) 54 | # Import-VcIntuneApplication -VcList $SavedVcRedist | Out-Null 55 | # Start-Sleep -Seconds 5 # Wait for Intune to process the import 56 | # } 57 | 58 | # It "Removes VcRedist from Intune OK" { 59 | # { Remove-VcIntuneApplication -VcList $SavedVcRedist -Confirm:$false } | Should -Not -Throw 60 | # } 61 | # } 62 | 63 | # Context "ShouldProcess support" { 64 | # It "Should honor ShouldProcess and not call Remove-IntuneWin32App if ShouldProcess returns false" { 65 | # Mock -CommandName Remove-IntuneWin32App -MockWith { throw "Should not be called" } 66 | # Mock -CommandName $ExecutionContext.InvokeCommand.GetCommand('ShouldProcess', 'Cmdlet') -MockWith { $false } 67 | # { Remove-VcIntuneApplication -VcList $TestVcList } | Should -Not -Throw 68 | # } 69 | # } 70 | } 71 | -------------------------------------------------------------------------------- /VcRedist/Private/Get-VcRedistAppsFromIntune.ps1: -------------------------------------------------------------------------------- 1 | function Get-VcRedistAppsFromIntune { 2 | <# 3 | .SYNOPSIS 4 | Retrieves existing Microsoft Visual C++ Win32 applications from Intune that match a given VcList object. 5 | 6 | .DESCRIPTION 7 | The Get-VcRedistAppsFromIntune function queries Intune for Win32 applications whose display names start with 8 | "Microsoft Visual C" and whose notes contain metadata indicating they were created by VcRedist. 9 | It then filters these applications to return only those whose GUIDs match the PackageId values in the provided VcList object. 10 | 11 | .PARAMETER VcList 12 | A PSObject representing a list of Visual C++ Redistributable packages, typically generated by Save-VcRedist. 13 | The function uses the PackageId property from this object to match against Intune applications. 14 | 15 | .EXAMPLE 16 | $VcList = Save-VcRedist 17 | Get-VcRedistAppsFromIntune -VcList $VcList 18 | 19 | This example retrieves all Visual C++ Redistributable applications in Intune that match the packages in $VcList. 20 | 21 | .NOTES 22 | Requires the IntuneWin32App module to be available in the session. 23 | #> 24 | [CmdletBinding(SupportsShouldProcess = $false)] 25 | param ( 26 | [Parameter( 27 | Mandatory = $true, 28 | Position = 0, 29 | ValueFromPipeline, 30 | HelpMessage = "Pass a VcList object from Save-VcRedist.")] 31 | [ValidateNotNullOrEmpty()] 32 | [System.Management.Automation.PSObject] $VcList 33 | ) 34 | 35 | begin { 36 | $DisplayNamePattern = "^Microsoft Visual C*" 37 | $NotesPattern = '^{"CreatedBy":"VcRedist","Guid":.*}$' 38 | 39 | # Get the existing Win32 applications from Intune 40 | Write-Verbose -Message "Retrieving existing Win32 applications from Intune." 41 | $ExistingIntuneApps = Get-IntuneWin32App | ` 42 | Where-Object { $_.displayName -match $DisplayNamePattern -and $_.notes -match $NotesPattern } | ` 43 | Select-Object -Property * -ExcludeProperty "largeIcon" 44 | if ($ExistingIntuneApps.Count -gt 0) { 45 | Write-Verbose -Message "Found $($ExistingIntuneApps.Count) existing Visual C++ applications in Intune." 46 | } 47 | } 48 | 49 | process { 50 | Write-Verbose -Message "Filtering existing applications to match VcList PackageId." 51 | foreach ($Application in $ExistingIntuneApps) { 52 | if (($Application.notes | ConvertFrom-Json -ErrorAction "Stop").Guid -in $VcList.PackageId) { 53 | 54 | # Add the packageId to the application object for easier reference 55 | $Application | Add-Member -MemberType "NoteProperty" -Name "packageId" -Value $($Application.notes | ConvertFrom-Json -ErrorAction "Stop").Guid -Force 56 | Write-Output -InputObject $Application 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /.rules/PascalCase.psm1: -------------------------------------------------------------------------------- 1 | using namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic 2 | 3 | function Measure-PascalCase { 4 | <# 5 | .SYNOPSIS 6 | The variables names should be in PascalCase. 7 | 8 | .DESCRIPTION 9 | Variable names should use a consistent capitalization style, i.e. : PascalCase. 10 | In PascalCase, only the first letter is capitalized. Or, if the variable name is made of multiple concatenated words, 11 | only the first letter of each concatenated word is capitalized. 12 | To fix a violation of this rule, please consider using PascalCase for variable names. 13 | 14 | .EXAMPLE 15 | Measure-PascalCase -ScriptBlockAst $ScriptBlockAst 16 | 17 | .INPUTS 18 | [System.Management.Automation.Language.ScriptBlockAst] 19 | 20 | .OUTPUTS 21 | [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]] 22 | 23 | .NOTES 24 | https://msdn.microsoft.com/en-us/library/dd878270(v=vs.85).aspx 25 | https://msdn.microsoft.com/en-us/library/ms229043(v=vs.110).aspx 26 | #> 27 | [CmdletBinding()] 28 | [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] 29 | param ( 30 | [Parameter(Mandatory = $true)] 31 | [ValidateNotNullOrEmpty()] 32 | [System.Management.Automation.Language.ScriptBlockAst] 33 | $ScriptBlockAst 34 | ) 35 | 36 | process { 37 | $Results = @() 38 | try { 39 | #region Define predicates to find ASTs. 40 | [ScriptBlock]$Predicate = { 41 | param ([System.Management.Automation.Language.Ast]$Ast) 42 | [bool]$ReturnValue = $false 43 | if ($Ast -is [System.Management.Automation.Language.AssignmentStatementAst]) { 44 | [System.Management.Automation.Language.AssignmentStatementAst]$VariableAst = $Ast 45 | if ($VariableAst.Left.VariablePath.UserPath -cnotmatch '^([A-Z][a-z]+)+$') { 46 | $ReturnValue = $true 47 | } 48 | } 49 | return $ReturnValue 50 | } 51 | #endregion 52 | 53 | #region Finds ASTs that match the predicates. 54 | [System.Management.Automation.Language.Ast[]]$Violations = $ScriptBlockAst.FindAll($Predicate, $true) 55 | if ($Violations.Count -ne 0) { 56 | foreach ($Violation in $Violations) { 57 | $Result = New-Object ` 58 | -TypeName "Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord" ` 59 | -ArgumentList "$((Get-Help $MyInvocation.MyCommand.Name).Description.Text)", $Violation.Extent, $PSCmdlet.MyInvocation.InvocationName, Information, $null 60 | $Results += $Result 61 | } 62 | } 63 | return $Results 64 | #endregion 65 | } 66 | catch { 67 | $PSCmdlet.ThrowTerminatingError($_) 68 | } 69 | } 70 | } 71 | 72 | Export-ModuleMember -Function Measure-PascalCase 73 | -------------------------------------------------------------------------------- /tests/Module.Tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Main Pester function tests. 4 | #> 5 | [CmdletBinding()] 6 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Justification = "This OK for the tests files.")] 7 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Outputs to log host.")] 8 | param () 9 | 10 | BeforeDiscovery { 11 | } 12 | 13 | Describe -Name "General module validation" { 14 | Context "Validation" { 15 | BeforeAll { 16 | $scripts = Get-ChildItem -Path "$env:GITHUB_WORKSPACE\VcRedist" -Recurse -Include "*.ps1", "*.psm1", "*.psd1" 17 | 18 | # TestCases are splatted to the script so we need hashtables 19 | $testCase = $scripts | ForEach-Object { @{file = $_ } } 20 | } 21 | 22 | It "Script should exist" -TestCases $testCase { 23 | param($file) 24 | $file.FullName | Should -Exist 25 | } 26 | 27 | It "Script should be valid PowerShell" -TestCases $testCase { 28 | param($file) 29 | $contents = Get-Content -Path $file.FullName -ErrorAction "Stop" 30 | $errors = $null 31 | $null = [System.Management.Automation.PSParser]::Tokenize($contents, [ref]$errors) 32 | $errors.Count | Should -Be 0 33 | } 34 | } 35 | } 36 | 37 | Describe -Name "function validation" { 38 | Context "Validation" { 39 | BeforeEach { 40 | $scripts = Get-ChildItem -Path "$env:GITHUB_WORKSPACE\VcRedist" -Recurse -Include "*.ps1" 41 | $testCase = $scripts | ForEach-Object { @{file = $_ } } 42 | } 43 | 44 | It "Script should only contain one function" -TestCases $testCase { 45 | param($file) 46 | $contents = Get-Content -Path $file.FullName -ErrorAction "Stop" 47 | $describes = [Management.Automation.Language.Parser]::ParseInput($contents, [ref]$null, [ref]$null) 48 | $test = $describes.FindAll( { $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true) 49 | $test.Count | Should -Be 1 50 | } 51 | 52 | It " should match function name" -TestCases $testCase { 53 | param($file) 54 | $contents = Get-Content -Path $file.FullName -ErrorAction "Stop" 55 | $describes = [Management.Automation.Language.Parser]::ParseInput($contents, [ref]$null, [ref]$null) 56 | $test = $describes.FindAll( { $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true) 57 | $test[0].name | Should -Match $file.Name 58 | } 59 | } 60 | } 61 | 62 | # Test module and manifest 63 | Describe -Name "Module Metadata validation" { 64 | Context "File info" { 65 | BeforeAll { 66 | } 67 | 68 | It "Script fileinfo should be OK" { 69 | { Test-ModuleManifest -Path "$env:GITHUB_WORKSPACE\VcRedist\VcRedist.psd1" -ErrorAction "Stop" } | Should -Not -Throw 70 | } 71 | 72 | It "Import module should be OK" { 73 | { Import-Module "$env:GITHUB_WORKSPACE\VcRedist" -Force -ErrorAction "Stop" } | Should -Not -Throw 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /VcRedist/Private/Get-InstalledSoftware.ps1: -------------------------------------------------------------------------------- 1 | function Get-InstalledSoftware { 2 | <# 3 | .SYNOPSIS 4 | Retrieves a list of all software installed 5 | 6 | .EXAMPLE 7 | Get-InstalledSoftware 8 | 9 | This example retrieves all software installed on the local computer 10 | 11 | .PARAMETER Name 12 | The software title you"d like to limit the query to. 13 | 14 | .NOTES 15 | Author: Adam Bertram 16 | URL: https://4sysops.com/archives/find-the-product-guid-of-installed-software-with-powershell/ 17 | #> 18 | [CmdletBinding(SupportsShouldProcess = $false)] 19 | [OutputType([System.Management.Automation.PSObject])] 20 | param ( 21 | [Parameter()] 22 | [ValidateNotNullOrEmpty()] 23 | [System.String] $Name 24 | ) 25 | 26 | process { 27 | $UninstallKeys = "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall", "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall" 28 | $null = New-PSDrive -Name "HKU" -PSProvider "Registry" -Root "Registry::HKEY_USERS" 29 | 30 | $UninstallKeys += Get-ChildItem -Path "HKU:" -ErrorAction "SilentlyContinue" | ` 31 | Where-Object { $_.Name -match "S-\d-\d+-(\d+-){1,14}\d+$" } | ` 32 | ForEach-Object { "HKU:\$($_.PSChildName)\Software\Microsoft\Windows\CurrentVersion\Uninstall" } 33 | 34 | foreach ($UninstallKey in $UninstallKeys) { 35 | if ($PSBoundParameters.ContainsKey("Name")) { 36 | $WhereBlock = { ($_.PSChildName -match "^{[A-Z0-9]{8}-([A-Z0-9]{4}-){3}[A-Z0-9]{12}}$") -and ($_.GetValue("DisplayName") -like "$Name*") } 37 | } 38 | else { 39 | $WhereBlock = { ($_.PSChildName -match "^{[A-Z0-9]{8}-([A-Z0-9]{4}-){3}[A-Z0-9]{12}}$") -and ($_.GetValue("DisplayName")) } 40 | } 41 | 42 | $SelectProperties = @( 43 | @{n = "Publisher"; e = { $_.GetValue("Publisher") } }, 44 | @{n = "Name"; e = { $_.GetValue("DisplayName") } }, 45 | @{n = "Version"; e = { $_.GetValue("DisplayVersion") } }, 46 | @{n = "ProductCode"; e = { $_.PSChildName } }, 47 | @{n = "BundleCachePath"; e = { $_.GetValue("BundleCachePath") } }, 48 | @{n = "Architecture"; e = { if ($_.GetValue("DisplayName") -like "*x64*") { "x64" } else { "x86" } } }, 49 | @{n = "Release"; e = { if ($_.GetValue("DisplayName") -match [RegEx]"(\d{4})\s+") { $matches[0].Trim(" ") } elseif ($_.GetValue("DisplayName") -match [RegEx]"v(\d+)") { $matches[1].Trim(" ") } } }, 50 | @{n = "UninstallString"; e = { $_.GetValue("UninstallString") } }, 51 | @{n = "QuietUninstallString"; e = { $_.GetValue("QuietUninstallString") } }, 52 | @{n = "UninstallKey"; e = { $UninstallKey } } 53 | ) 54 | 55 | $params = @{ 56 | Path = $UninstallKey 57 | ErrorAction = "SilentlyContinue" 58 | } 59 | Get-ChildItem @params | Where-Object $WhereBlock | Select-Object -Property $SelectProperties 60 | } 61 | } 62 | 63 | end { 64 | Remove-PSDrive -Name "HKU" -ErrorAction "SilentlyContinue" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at aaron@stealthpuppy.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /docs/versions.md: -------------------------------------------------------------------------------- 1 | # Included Redistributables 2 | 3 | VcRedist `4.1.526` includes the following Redistributables (supported and unsupported): 4 | 5 | | Version | Architecture | Name | 6 | | -------------- | ------------ | -------------------------------------------------------------------------- | 7 | | 14.50.35719.0 | x64 | Visual C++ v14 Redistributable (x64) | 8 | | 14.50.35719.0 | x86 | Visual C++ v14 Redistributable (x86) | 9 | | 14.50.35719.0 | ARM64 | Visual C++ v14 Redistributable (Arm64) | 10 | | 14.29.30156.0 | x64 | Visual C++ Redistributable for Visual Studio 2019 | 11 | | 14.29.30156.0 | x86 | Visual C++ Redistributable for Visual Studio 2019 | 12 | | 14.16.27052.0 | x64 | Visual C++ Redistributable for Visual Studio 2017 | 13 | | 14.16.27052.0 | x86 | Visual C++ Redistributable for Visual Studio 2017 | 14 | | 14.0.24215.1 | x64 | Visual C++ 2015 Redistributable Update 3 | 15 | | 14.0.24215.1 | x86 | Visual C++ 2015 Redistributable Update 3 | 16 | | 12.0.40664.0 | x64 | Visual C++ 2013 Update 5 Redistributable Package | 17 | | 12.0.40664.0 | x86 | Visual C++ 2013 Update 5 Redistributable Package | 18 | | 12.0.30501.0 | x64 | Visual C++ Redistributable Packages for Visual Studio 2013 | 19 | | 12.0.30501.0 | x86 | Visual C++ Redistributable Packages for Visual Studio 2013 | 20 | | 11.0.61030.0 | x64 | Visual C++ Redistributable for Visual Studio 2012 Update 4 | 21 | | 11.0.61030.0 | x86 | Visual C++ Redistributable for Visual Studio 2012 Update 4 | 22 | | 10.0.40219.325 | x64 | Visual C++ 2010 Service Pack 1 Redistributable Package MFC Security Update | 23 | | 10.0.40219.325 | x86 | Visual C++ 2010 Service Pack 1 Redistributable Package MFC Security Update | 24 | | 9.0.30729.6161 | x64 | Visual C++ 2008 Service Pack 1 Redistributable Package MFC Security Update | 25 | | 9.0.30729.6161 | x86 | Visual C++ 2008 Service Pack 1 Redistributable Package MFC Security Update | 26 | | 9.0.30729.4148 | x64 | Visual C++ 2008 Service Pack 1 Redistributable Package ATL Security Update | 27 | | 9.0.30729.4148 | x86 | Visual C++ 2008 Service Pack 1 Redistributable Package ATL Security Update | 28 | | 9.0.30411 | x64 | Visual C++ 2008 Redistributable Package ATL Security Update | 29 | | 9.0.30411 | x86 | Visual C++ 2008 Redistributable Package ATL Security Update | 30 | | 8.0.61000 | x64 | Visual C++ 2005 Service Pack 1 Redistributable Package MFC Security Update | 31 | | 8.0.61000 | x86 | Visual C++ 2005 Service Pack 1 Redistributable Package MFC Security Update | 32 | | 8.0.59192 | x64 | Visual C++ 2005 Service Pack 1 Redistributable Package ATL Security Update | 33 | | 8.0.59192 | x86 | Visual C++ 2005 Service Pack 1 Redistributable Package ATL Security Update | 34 | -------------------------------------------------------------------------------- /VcRedist/Private/Get-RequiredVcRedistUpdatesFromIntune.ps1: -------------------------------------------------------------------------------- 1 | function Get-RequiredVcRedistUpdatesFromIntune { 2 | <# 3 | .SYNOPSIS 4 | Determines which Visual C++ Redistributable applications in Intune require an update based on a provided VcList. 5 | 6 | .DESCRIPTION 7 | The Get-RequiredVcRedistUpdatesFromIntune function compares the versions of Microsoft Visual C++ Redistributable Win32 applications currently present in Intune with those specified in a provided VcList object. 8 | It outputs objects indicating whether an update is required for each matched application. 9 | 10 | .PARAMETER VcList 11 | A PSObject containing a list of Visual C++ Redistributable packages, typically generated by Save-VcRedist. 12 | Each object in the list should include at least 'PackageId' and 'Version' properties. 13 | 14 | .INPUTS 15 | System.Management.Automation.PSObject 16 | Accepts a VcList object from the pipeline. 17 | 18 | .OUTPUTS 19 | PSCustomObject 20 | Outputs an object for each matched application with the following properties: 21 | - AppId: The Intune application ID. 22 | - IntuneVersion: The version currently in Intune. 23 | - UpdateVersion: The version available in the VcList. 24 | - UpdateRequired: Boolean indicating if an update is required. 25 | 26 | .EXAMPLE 27 | $vcList = Save-VcRedist 28 | Get-RequiredVcRedistUpdatesFromIntune -VcList $vcList 29 | 30 | Compares the Visual C++ Redistributable applications in Intune with those in $vcList and outputs which require updates. 31 | 32 | .NOTES 33 | Requires the Get-IntuneWin32App cmdlet to retrieve Win32 applications from Intune. 34 | Only applications with notes containing a 'Guid' property matching a VcList PackageId are considered. 35 | #> 36 | [CmdletBinding(SupportsShouldProcess = $false)] 37 | param ( 38 | [Parameter( 39 | Mandatory = $true, 40 | Position = 0, 41 | ValueFromPipeline, 42 | HelpMessage = "Pass a VcList object from Save-VcRedist.")] 43 | [ValidateNotNullOrEmpty()] 44 | [System.Management.Automation.PSObject] $VcList 45 | ) 46 | 47 | begin { 48 | # Get the existing VcRedist Win32 applications from Intune 49 | $ExistingIntuneApps = Get-VcRedistAppsFromIntune -VcList $VcList 50 | } 51 | 52 | process { 53 | foreach ($Application in $ExistingIntuneApps) { 54 | $VcRedist = $VcList | Where-Object { $_.PackageId -eq $Application.packageId } 55 | if ($null -eq $VcRedist) { 56 | Write-Verbose -Message "No matching VcRedist found for application with ID: $($Application.Id). Skipping." 57 | continue 58 | } 59 | else { 60 | $Update = $false 61 | if ([System.Version]$VcRedist.Version -gt [System.Version]$Application.displayVersion) { 62 | $Update = $true 63 | Write-Verbose -Message "Update required for $($Application.displayName): $($VcRedist.Version) > $($Application.displayVersion)." 64 | } 65 | $Object = [PSCustomObject]@{ 66 | "AppId" = $Application.Id 67 | "IntuneVersion" = $Application.displayVersion 68 | "UpdateVersion" = $VcRedist.Version 69 | "UpdateRequired" = $Update 70 | } 71 | Write-Output -InputObject $Object 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/Public/Uninstall-VcRedist.Tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Public Pester function tests. 4 | #> 5 | [CmdletBinding()] 6 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Justification = "This OK for the tests files.")] 7 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Outputs to log host.")] 8 | param () 9 | 10 | BeforeDiscovery { 11 | $SupportedReleases = @("2015", "2017", "2019", "14") 12 | 13 | if ($env:PROCESSOR_ARCHITECTURE -eq "AMD64") { 14 | $SkipAmd = $false 15 | } 16 | else { 17 | $SkipAmd = $true 18 | } 19 | if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") { 20 | $SkipArm = $false 21 | } 22 | else { 23 | $SkipArm = $true 24 | } 25 | } 26 | 27 | Describe -Name "AMD64 specific tests" -Skip:$SkipAmd { 28 | Describe -Name "Uninstall-VcRedist" -ForEach $SupportedReleases { 29 | BeforeAll { 30 | $Release = $_ 31 | 32 | # Create download path 33 | if ($env:Temp) { 34 | $Path = Join-Path -Path $env:Temp -ChildPath "Downloads" 35 | } 36 | elseif ($env:TMPDIR) { 37 | $Path = Join-Path -Path $env:TMPDIR -ChildPath "Downloads" 38 | } 39 | elseif ($env:RUNNER_TEMP) { 40 | $Path = Join-Path -Path $env:RUNNER_TEMP -ChildPath "Downloads" 41 | } 42 | New-Item -Path $Path -ItemType "Directory" -Force -ErrorAction "SilentlyContinue" > $Null 43 | 44 | $VcList = Get-VcList -Release $Release | Save-VcRedist -Path $Path 45 | Install-VcRedist -VcList $VcList -Silent | Out-Null 46 | } 47 | 48 | Context "Uninstall VcRedist " { 49 | It "Uninstalls the VcRedist x64" { 50 | { Uninstall-VcRedist -Release $Release -Architecture "x64" -Confirm:$false } | Should -Not -Throw 51 | } 52 | 53 | It "Uninstalls the VcRedist x86" { 54 | { Uninstall-VcRedist -Release $Release -Architecture "x86" -Confirm:$false } | Should -Not -Throw 55 | } 56 | } 57 | } 58 | } 59 | 60 | Describe -Name "ARM64 specific tests" -Skip:$SkipArm { 61 | Describe -Name "Uninstall-VcRedist" -ForEach $SupportedReleases { 62 | BeforeAll { 63 | $Release = $_ 64 | 65 | # Create download path 66 | if ($env:Temp) { 67 | $Path = Join-Path -Path $env:Temp -ChildPath "Downloads" 68 | } 69 | elseif ($env:TMPDIR) { 70 | $Path = Join-Path -Path $env:TMPDIR -ChildPath "Downloads" 71 | } 72 | elseif ($env:RUNNER_TEMP) { 73 | $Path = Join-Path -Path $env:RUNNER_TEMP -ChildPath "Downloads" 74 | } 75 | New-Item -Path $Path -ItemType "Directory" -Force -ErrorAction "SilentlyContinue" > $Null 76 | 77 | $VcList = Get-VcList -Release $Release | Save-VcRedist -Path $Path 78 | Install-VcRedist -VcList $VcList -Silent | Out-Null 79 | } 80 | 81 | Context "Uninstall VcRedist " { 82 | It "Uninstalls the VcRedist arm64" { 83 | { Uninstall-VcRedist -Release $Release -Architecture "arm64" -Confirm:$false } | Should -Not -Throw 84 | } 85 | } 86 | } 87 | } 88 | 89 | Describe -Name "Uninstall VcRedist via the pipeline" { 90 | Context "Test uninstall via the pipeline" { 91 | It "Uninstalls the 14 Redistributables via the pipeline" { 92 | { Get-VcList -Release "14" | Uninstall-VcRedist -Confirm:$false } | Should -Not -Throw 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /VcRedist/Public/Test-VcRedistUri.ps1: -------------------------------------------------------------------------------- 1 | function Test-VcRedistUri { 2 | <# 3 | .EXTERNALHELP Vcredist-help.xml 4 | #> 5 | [Alias("Test-VcRedistDownload")] 6 | [OutputType([System.Management.Automation.PSObject])] 7 | [CmdletBinding(SupportsShouldProcess = $true, HelpURI = "https://stealthpuppy.com/vcredist/test/", DefaultParameterSetName = "Path")] 8 | param ( 9 | [Parameter( 10 | Mandatory = $true, 11 | Position = 0, 12 | ValueFromPipeline, 13 | HelpMessage = "Pass a VcList object from Get-VcList.")] 14 | [ValidateNotNullOrEmpty()] 15 | [System.Management.Automation.PSObject] $VcList, 16 | 17 | [Parameter(Mandatory = $false, Position = 1)] 18 | [System.String] $Proxy, 19 | 20 | [Parameter(Mandatory = $false, Position = 2)] 21 | [System.Management.Automation.PSCredential] 22 | $ProxyCredential = [System.Management.Automation.PSCredential]::Empty, 23 | 24 | [Parameter(Mandatory = $false)] 25 | [System.Management.Automation.SwitchParameter] $ShowProgress 26 | ) 27 | 28 | begin { 29 | # Disable the Invoke-WebRequest progress bar for faster downloads 30 | if ($PSBoundParameters.ContainsKey("Verbose") -or ($PSBoundParameters.ContainsKey("ShowProgress"))) { 31 | $ProgressPreference = [System.Management.Automation.ActionPreference]::Continue 32 | } 33 | else { 34 | $ProgressPreference = [System.Management.Automation.ActionPreference]::SilentlyContinue 35 | } 36 | 37 | # Enable TLS 1.2 38 | [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 39 | } 40 | 41 | process { 42 | # Loop through each object and download to the target path 43 | foreach ($Object in $VcList) { 44 | 45 | #region Validate the URI property and find the output filename 46 | if ([System.Boolean]($Object.URI) -eq $false) { 47 | $Msg = "Object does not have valid URI property." 48 | throw [System.Management.Automation.PropertyNotFoundException]::New($Msg) 49 | } 50 | #endregion 51 | 52 | try { 53 | $params = @{ 54 | Uri = $Object.URI 55 | Method = "HEAD" 56 | UseBasicParsing = $true 57 | ErrorAction = "SilentlyContinue" 58 | } 59 | if ($PSBoundParameters.ContainsKey("Proxy")) { 60 | $params.Proxy = $Proxy 61 | } 62 | if ($PSBoundParameters.ContainsKey("ProxyCredential")) { 63 | $params.ProxyCredential = $ProxyCredential 64 | } 65 | $Result = $true 66 | Invoke-WebRequest @params | Out-Null 67 | } 68 | catch [System.Exception] { 69 | $Result = $false 70 | } 71 | $PSObject = [PSCustomObject] @{ 72 | Result = $Result 73 | Release = $Object.Release 74 | Architecture = $Object.Architecture 75 | Version = $Object.Version 76 | URI = $Object.URI 77 | } 78 | Write-Output -InputObject $PSObject 79 | } 80 | } 81 | 82 | end { 83 | if ($PSCmdlet.ShouldProcess("Remove variables")) { 84 | if (Test-Path -Path Variable:params) { Remove-Variable -Name "params" -ErrorAction "SilentlyContinue" } 85 | Remove-Variable -Name "OutPath", "OutFile" -ErrorAction "SilentlyContinue" 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /.rules/LowercaseKeyword.psm1: -------------------------------------------------------------------------------- 1 | using namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic 2 | 3 | function Measure-LowercaseKeyword { 4 | <# 5 | .SYNOPSIS 6 | PowerShell keywords and constants should be in lowercase. 7 | 8 | .DESCRIPTION 9 | PowerShell keywords (function, if, foreach, etc.) and constants ($true, $false, $null) 10 | should use lowercase for consistency and best practices. 11 | 12 | .EXAMPLE 13 | Measure-LowercaseKeyword -ScriptBlockAst $ScriptBlockAst 14 | 15 | .INPUTS 16 | [System.Management.Automation.Language.ScriptBlockAst] 17 | 18 | .OUTPUTS 19 | [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]] 20 | #> 21 | [CmdletBinding()] 22 | [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] 23 | param ( 24 | [Parameter(Mandatory = $true)] 25 | [ValidateNotNullOrEmpty()] 26 | [System.Management.Automation.Language.ScriptBlockAst] $ScriptBlockAst 27 | ) 28 | 29 | process { 30 | $Results = @() 31 | 32 | # Define keywords and constants we want to check 33 | $Keywords = @('function', 'foreach', 'if', 'else', 'elseif', 'return', 'switch', 'param', 34 | 'begin', 'process', 'end', 'in', 'do', 'while', 'until', 'for', 'trap', 35 | 'throw', 'catch', 'try', 'finally', 'data', 'dynamicparam', 'break', 36 | 'continue', 'exit', 'class', 'enum', 'using') 37 | $Constants = @('$true', '$false', '$null') 38 | 39 | try { 40 | # Get all tokens from the script 41 | $Tokens = @() 42 | $ParseErrors = @() 43 | [void][System.Management.Automation.Language.Parser]::ParseInput( 44 | $ScriptBlockAst.ToString(), 45 | [ref]$Tokens, 46 | [ref]$ParseErrors 47 | ) 48 | 49 | if ($ParseErrors.Count -gt 0) { 50 | return $Results 51 | } 52 | 53 | # Track processed token positions to avoid duplicates 54 | $ProcessedTokens = @{} 55 | 56 | foreach ($Token in $Tokens) { 57 | $TokenText = $Token.Text 58 | $LowerTokenText = $TokenText.ToLower() 59 | 60 | # Create a unique key for this token position 61 | $TokenKey = "$($Token.Extent.StartLineNumber):$($Token.Extent.StartColumnNumber):$TokenText" 62 | 63 | # Skip if we've already processed this exact token at this position 64 | if ($ProcessedTokens.ContainsKey($TokenKey)) { 65 | continue 66 | } 67 | 68 | # Check keywords (check text content directly to be more reliable) 69 | if ($Keywords -contains $LowerTokenText -and $TokenText -cne $LowerTokenText) { 70 | $Results += [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]@{ 71 | Message = "Keyword '$TokenText' should be lowercase ('$LowerTokenText')." 72 | Extent = $Token.Extent 73 | RuleName = $PSCmdlet.MyInvocation.InvocationName 74 | Severity = 'Warning' 75 | } 76 | $ProcessedTokens[$TokenKey] = $true 77 | } 78 | # Check constants 79 | elseif ($Constants -contains $LowerTokenText -and $TokenText -cne $LowerTokenText) { 80 | $Results += [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]@{ 81 | Message = "Constant '$TokenText' should be lowercase ('$LowerTokenText')." 82 | Extent = $Token.Extent 83 | RuleName = $PSCmdlet.MyInvocation.InvocationName 84 | Severity = 'Warning' 85 | } 86 | $ProcessedTokens[$TokenKey] = $true 87 | } 88 | } 89 | 90 | return $Results 91 | } 92 | catch { 93 | $PSCmdlet.ThrowTerminatingError($_) 94 | } 95 | } 96 | } 97 | 98 | Export-ModuleMember -Function Measure-LowercaseKeyword 99 | -------------------------------------------------------------------------------- /tests/Public/Save-VcRedist.Tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Public Pester function tests. 4 | #> 5 | [CmdletBinding()] 6 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Justification = "This OK for the tests files.")] 7 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Outputs to log host.")] 8 | param () 9 | 10 | BeforeDiscovery { 11 | $SupportedReleases = @("2015", "2017", "2019", "14") 12 | } 13 | 14 | Describe -Name "Save-VcRedist" -ForEach $SupportedReleases { 15 | BeforeAll { 16 | if ($env:Temp) { 17 | $Path = Join-Path -Path $env:Temp -ChildPath "Downloads" 18 | } 19 | elseif ($env:TMPDIR) { 20 | $Path = Join-Path -Path $env:TMPDIR -ChildPath "Downloads" 21 | } 22 | elseif ($env:RUNNER_TEMP) { 23 | $Path = Join-Path -Path $env:RUNNER_TEMP -ChildPath "Downloads" 24 | } 25 | New-Item -Path $Path -ItemType "Directory" -Force -ErrorAction "SilentlyContinue" > $Null 26 | 27 | #region Functions used in tests 28 | function Test-VcDownload { 29 | <# 30 | .SYNOPSIS 31 | Tests downloads from Get-VcList are successful. 32 | #> 33 | [CmdletBinding()] 34 | param ( 35 | [Parameter()] 36 | [PSCustomObject] $VcList, 37 | 38 | [Parameter()] 39 | [string] $Path 40 | ) 41 | $Output = $false 42 | foreach ($VcRedist in $VcList) { 43 | $folder = [System.IO.Path]::Combine((Resolve-Path -Path $Path), $VcRedist.Release, $VcRedist.Version, $VcRedist.Architecture) 44 | $Target = [System.IO.Path]::Combine($Folder, $(Split-Path -Path $VcRedist.URI -Leaf)) 45 | if (Test-Path -Path $Target -PathType Leaf) { 46 | Write-Verbose "$($Target) - exists." 47 | $Output = $true 48 | } 49 | else { 50 | Write-Warning "$($Target) - not found." 51 | $Output = $false 52 | } 53 | } 54 | Write-Output $Output 55 | } 56 | #endregion 57 | } 58 | 59 | Context "Download Redistributables" { 60 | It "Downloads the release <_> x64 and returns the expected object" { 61 | Save-VcRedist -VcList (Get-VcList -Release $_ -Architecture "x64") -Path $Path | Should -BeOfType "PSCustomObject" 62 | } 63 | 64 | It "Downloads the release <_> x86 and returns the expected object" { 65 | Save-VcRedist -VcList (Get-VcList -Release $_ -Architecture "x86") -Path $Path | Should -BeOfType "PSCustomObject" 66 | } 67 | } 68 | 69 | Context "Test downloaded Redistributables" { 70 | It "Downloaded Visual C++ Redistributables <_> x64 OK" { 71 | Test-VcDownload -VcList (Get-VcList -Release $_ -Architecture "x64") -Path $Path | Should -BeTrue 72 | } 73 | 74 | It "Downloaded Visual C++ Redistributables <_> x86 OK" { 75 | Test-VcDownload -VcList (Get-VcList -Release $_ -Architecture "x86") -Path $Path | Should -BeTrue 76 | } 77 | } 78 | } 79 | 80 | Describe -Name "Save-VcRedist pipeline" -ForEach $SupportedReleases { 81 | BeforeAll { 82 | if ($env:Temp) { 83 | $Path = Join-Path -Path $env:Temp -ChildPath "Downloads" 84 | } 85 | elseif ($env:TMPDIR) { 86 | $Path = Join-Path -Path $env:TMPDIR -ChildPath "Downloads" 87 | } 88 | elseif ($env:RUNNER_TEMP) { 89 | $Path = Join-Path -Path $env:RUNNER_TEMP -ChildPath "Downloads" 90 | } 91 | New-Item -Path $Path -ItemType "Directory" -Force -ErrorAction "SilentlyContinue" > $Null 92 | Push-Location -Path $Path 93 | } 94 | 95 | Context "Test pipeline support" { 96 | It "Should not throw when passed <_> x64 via pipeline with no parameters" { 97 | { Get-VcList -Release $_ -Architecture "x64" | Save-VcRedist } | Should -Not -Throw 98 | } 99 | 100 | It "Should not throw when passed <_> x86 via pipeline with no parameters" { 101 | { Get-VcList -Release $_ -Architecture "x86" | Save-VcRedist } | Should -Not -Throw 102 | } 103 | } 104 | 105 | AfterAll { 106 | Pop-Location 107 | } 108 | } 109 | 110 | Describe -Name "Save-VcRedist fail scenarios" { 111 | Context "Test fail scenarios" { 112 | It "Given an invalid path, it should throw an error" { 113 | { Save-VcRedist -Path ([System.IO.Path]::Combine($Path, "Temp")) } | Should -Throw 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /ci/Update-Manifest.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Update manifest for newer VcRedist versions. 4 | #> 5 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] 6 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] 7 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUserDeclaredVarsMoreThanAssignments", "")] 8 | [CmdletBinding()] 9 | param ( 10 | [System.String[]] $Release, 11 | [System.String[]] $Architecture = @("x64", "x86"), 12 | [System.String] $Path, 13 | [System.String] $VcManifest 14 | ) 15 | 16 | begin { 17 | $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop 18 | $InformationPreference = [System.Management.Automation.ActionPreference]::Continue 19 | $ProgressPreference = [System.Management.Automation.ActionPreference]::SilentlyContinue 20 | [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 21 | } 22 | 23 | process { 24 | 25 | # Get an array of VcRedists from the current manifest and the installed VcRedists 26 | Write-Information -MessageData "$($PSStyle.Foreground.Cyan)`tGetting manifest from: $VcManifest." 27 | $CurrentManifest = Get-Content -Path $VcManifest | ConvertFrom-Json 28 | $InstalledVcRedists = Get-InstalledVcRedist 29 | 30 | $Output = @() 31 | $FoundNewVersion = $false 32 | foreach ($Arch in $Architecture) { 33 | foreach ($Rls in $Release) { 34 | 35 | Write-Information -MessageData "$($PSStyle.Foreground.Cyan)`tInstalling VcRedist $Rls." 36 | Get-VcList -Release $Rls -Architecture $Arch | Save-VcRedist -Path $Path | Install-VcRedist -Silent 37 | $InstalledVcRedists = Get-InstalledVcRedist | Where-Object { $_.Name -notmatch "Debug Runtime" } 38 | 39 | # Filter the VcRedists for the target version and compare against what has been installed 40 | foreach ($ManifestVcRedist in ($CurrentManifest.Supported | Where-Object { $_.Release -eq $Rls -and $_.Architecture -eq $Arch })) { 41 | $InstalledItem = $InstalledVcRedists | Where-Object { ($_.Release -eq $ManifestVcRedist.Release) -and ($_.Architecture -eq $ManifestVcRedist.Architecture) } 42 | 43 | # If the manifest version of the VcRedist is lower than the installed version, the manifest is out of date 44 | if ([System.Version]$InstalledItem.Version -gt [System.Version]$ManifestVcRedist.Version) { 45 | Write-Information -MessageData "$($PSStyle.Foreground.Cyan)`tVcRedist manifest is out of date." 46 | Write-Information -MessageData "$($PSStyle.Foreground.Cyan)`tInstalled version:`t$($InstalledItem.Version)" 47 | Write-Information -MessageData "$($PSStyle.Foreground.Cyan)`tManifest version:`t$($ManifestVcRedist.Version)" 48 | 49 | # Find the index of the VcRedist in the manifest and update it's properties 50 | $Index = $CurrentManifest.Supported::IndexOf($CurrentManifest.Supported.ProductCode, $ManifestVcRedist.ProductCode) 51 | $CurrentManifest.Supported[$Index].ProductCode = $InstalledItem.ProductCode 52 | $CurrentManifest.Supported[$Index].Version = $InstalledItem.Version 53 | 54 | # Create output variable 55 | # $NewVersion = $InstalledItem.Version 56 | $FoundNewVersion = $true 57 | $Output += $Rls 58 | } 59 | } 60 | } 61 | } 62 | 63 | # If a version was found and were aren't in the main branch 64 | Write-Information -MessageData "$($PSStyle.Foreground.Cyan)`tFound new version $FoundNewVersion." 65 | if ($FoundNewVersion -eq $true) { 66 | 67 | # Convert to JSON and export to the module manifest 68 | try { 69 | Write-Information -MessageData "$($PSStyle.Foreground.Cyan)`tUpdating module manifest for VcRedist $($Output -join ", ")." 70 | $CurrentManifest | ConvertTo-Json | Set-Content -Path $VcManifest -Force 71 | } 72 | catch { 73 | throw "Failed to convert to JSON and write back to the manifest." 74 | } 75 | } 76 | else { 77 | Write-Information -MessageData "$($PSStyle.Foreground.Cyan)`tInstalled VcRedist matches manifest." 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /.github/workflows/validate-module.yml: -------------------------------------------------------------------------------- 1 | name: Validate module 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'VcRedist/**' 7 | - 'tests/**' 8 | branches-ignore: 9 | - main 10 | pull_request: 11 | branches: 12 | - main 13 | paths: 14 | - 'VcRedist/**' 15 | workflow_dispatch: 16 | 17 | jobs: 18 | run-pester: 19 | name: Test with Pester 20 | runs-on: ${{ matrix.os }} 21 | strategy: 22 | matrix: 23 | os: [windows-2025, windows-11-arm] 24 | 25 | steps: 26 | - uses: actions/checkout@v6 27 | 28 | # Run Pester tests 29 | - name: Run Pester tests 30 | shell: powershell 31 | working-directory: "${{ github.workspace }}" 32 | env: 33 | TENANT_ID: ${{ secrets.TENANT_ID }} 34 | CLIENT_ID: ${{ secrets.CLIENT_ID }} 35 | CLIENT_SECRET: ${{ secrets.CLIENT_SECRET }} 36 | run: | 37 | .\tests\Install-Pester.ps1 38 | Import-Module -Name "Pester" -Force -ErrorAction "Stop" 39 | Import-Module -Name "$env:GITHUB_WORKSPACE\VcRedist" -Force 40 | 41 | $Config = New-PesterConfiguration 42 | $Config.Run.Path = "$env:GITHUB_WORKSPACE\tests" 43 | $Config.Run.PassThru = $True 44 | $Config.CodeCoverage.Enabled = $True 45 | $Config.CodeCoverage.Path = "$env:GITHUB_WORKSPACE\VcRedist" 46 | $Config.CodeCoverage.OutputFormat = "JaCoCo" 47 | $Config.CodeCoverage.OutputPath = "$env:GITHUB_WORKSPACE\CodeCoverage-${{ matrix.os }}.xml" 48 | $Config.TestResult.Enabled = $True 49 | $Config.TestResult.OutputFormat = "NUnitXml" 50 | $Config.TestResult.OutputPath = "$env:GITHUB_WORKSPACE\tests\TestResults-${{ matrix.os }}.xml" 51 | $Config.Output.Verbosity = "Detailed" 52 | Invoke-Pester -Configuration $Config 53 | 54 | - name: Upload artifacts 55 | id: upload-artifacts 56 | if: always() 57 | uses: actions/upload-artifact@v6 58 | with: 59 | name: 'pester-test-results-${{ matrix.os }}' 60 | path: | 61 | ${{ github.workspace }}\tests\TestResults-${{ matrix.os }}.xml 62 | 63 | # Publish Pester test results 64 | - name: Publish Pester test results 65 | uses: EnricoMi/publish-unit-test-result-action/windows@v2 66 | if: always() 67 | with: 68 | files: "${{ github.workspace }}//tests//TestResults-${{ matrix.os }}.xml" 69 | 70 | # - name: Upload to Codecov 71 | # id: codecov 72 | # if: always() 73 | # uses: codecov/codecov-action@v5 74 | # with: 75 | # token: ${{ secrets.CODECOV_TOKEN }} 76 | # files: ./CodeCoverage-${{ matrix.os }}.xml 77 | # verbose: true 78 | 79 | update-module-version: 80 | name: Update module version 81 | needs: [run-pester] 82 | runs-on: 'windows-latest' 83 | 84 | steps: 85 | - uses: actions/checkout@v6 86 | 87 | - name: Install and cache PowerShell modules 88 | id: psmodulecache 89 | uses: potatoqualitee/psmodulecache@v6.2.1 90 | with: 91 | modules-to-cache: BuildHelpers 92 | shell: pwsh 93 | 94 | - name: Update module version 95 | shell: pwsh 96 | working-directory: "${{ github.workspace }}" 97 | run: | 98 | Import-Module -Name BuildHelpers 99 | Step-ModuleVersion -Path .\VcRedist\VcRedist.psd1 -By Build 100 | 101 | # Import GPG key so that we can sign the commit 102 | - name: Import GPG key 103 | id: import_gpg 104 | uses: crazy-max/ghaction-import-gpg@v6 105 | with: 106 | gpg_private_key: ${{ secrets.GPGKEY }} 107 | passphrase: ${{ secrets.GPGPASSPHRASE }} 108 | git_user_signingkey: true 109 | git_commit_gpgsign: true 110 | git_config_global: true 111 | git_tag_gpgsign: true 112 | git_push_gpgsign: false 113 | git_committer_name: ${{ secrets.COMMIT_NAME }} 114 | git_committer_email: ${{ secrets.COMMIT_EMAIL }} 115 | 116 | - name: Commit changes 117 | id: commit 118 | uses: stefanzweifel/git-auto-commit-action@v7 119 | with: 120 | commit_message: "Update module version" 121 | commit_user_name: ${{ secrets.COMMIT_NAME }} 122 | commit_user_email: ${{ secrets.COMMIT_EMAIL }} 123 | -------------------------------------------------------------------------------- /tests/Public/Update-VcMdtApplication.Tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Public Pester function tests. 4 | #> 5 | [CmdletBinding()] 6 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Justification = "This OK for the tests files.")] 7 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Outputs to log host.")] 8 | param () 9 | 10 | BeforeDiscovery { 11 | $SupportedReleases = @("2015", "2017", "2019", "14") 12 | if ($env:PROCESSOR_ARCHITECTURE -eq "AMD64") { 13 | $Skip = $false 14 | } 15 | else { 16 | $Skip = $true 17 | } 18 | } 19 | 20 | Describe -Name "Update-VcMdtApplication with " -ForEach $SupportedReleases -Skip:$Skip { 21 | BeforeAll { 22 | # Install the MDT Workbench 23 | & "$env:GITHUB_WORKSPACE\tests\Install-Mdt.ps1" 24 | 25 | $Release = $_ 26 | $Path = $([System.IO.Path]::Combine($env:RUNNER_TEMP, "Downloads")) 27 | New-Item -Path $Path -ItemType "Directory" -ErrorAction "SilentlyContinue" | Out-Null 28 | 29 | $VcListX64 = Get-VcList -Release $Release -Architecture "x64" | Save-VcRedist -Path $Path 30 | $VcListX86 = Get-VcList -Release $Release -Architecture "x86" | Save-VcRedist -Path $Path 31 | } 32 | 33 | Context "Update-VcMdtApplication updates OK with existing Redistributables in the MDT share" { 34 | It "Does not throw when updating the existing x64 Redistributables" { 35 | $params = @{ 36 | VcList = $VcListX64 37 | MdtPath = "$env:RUNNER_TEMP\Deployment" 38 | AppFolder = "VcRedists" 39 | Silent = $true 40 | MdtDrive = "DS020" 41 | Publisher = "Microsoft" 42 | } 43 | { Update-VcMdtApplication @params } | Should -Not -Throw 44 | } 45 | 46 | It "Does not throw when updating the existing x86 Redistributables" { 47 | $params = @{ 48 | VcList = $VcListX86 49 | MdtPath = "$env:RUNNER_TEMP\Deployment" 50 | AppFolder = "VcRedists" 51 | Silent = $true 52 | MdtDrive = "DS020" 53 | Publisher = "Microsoft" 54 | } 55 | { Update-VcMdtApplication @params } | Should -Not -Throw 56 | } 57 | } 58 | } 59 | 60 | Describe -Name "Update-VcMdtApplication updates an existing application" -Skip:$Skip { 61 | BeforeAll { 62 | if ($env:PROCESSOR_ARCHITECTURE -eq "AMD64") { 63 | $Skip = $false 64 | 65 | # Setup existing 14 VcRedist applications with details that need to be updated 66 | $Path = $([System.IO.Path]::Combine($env:RUNNER_TEMP, "Downloads")) 67 | $SaveVcRedist = Save-VcRedist -Path $Path -VcList (Get-VcList -Release "2019") 68 | $Version = (Get-VcList -Release "14" -Architecture "x64").Version 69 | $VcPath = "$env:RUNNER_TEMP\Deployment\Applications\Microsoft VcRedist\14\$Version" 70 | foreach ($Item in $SaveVcRedist) { 71 | foreach ($Arch in @("x64", "x86")) { 72 | Copy-Item -Path $Item.Path -Destination "$VcPath\$Arch" -Force 73 | } 74 | } 75 | Import-Module -Name "$Env:ProgramFiles\Microsoft Deployment Toolkit\bin\MicrosoftDeploymentToolkit.psd1" 76 | $params = @{ 77 | Name = "DS020" 78 | PSProvider = "MDTProvider" 79 | Root = "$env:RUNNER_TEMP\Deployment" 80 | } 81 | New-PSDrive @params | Add-MDTPersistentDrive 82 | Restore-MDTPersistentDrive | Out-Null 83 | $MdtDrive = "DS020" 84 | $MdtTargetFolder = "$($MdtDrive):\Applications\VcRedists" 85 | $gciParams = @{ 86 | Path = $MdtTargetFolder 87 | Recurse = $true 88 | ErrorAction = "Continue" 89 | } 90 | foreach ($Architecture in @("x86", "x64")) { 91 | $ExistingVcRedist = Get-ChildItem @gciParams | Where-Object { $_.ShortName -match "14 $Architecture" } 92 | $params = @{ 93 | Path = (Join-Path -Path $MdtTargetFolder -ChildPath $ExistingVcRedist.Name) 94 | Name = "UninstallKey" 95 | Value = $((New-Guid).Guid) 96 | } 97 | Set-ItemProperty @params 98 | } 99 | } 100 | else { 101 | $Skip = $true 102 | } 103 | } 104 | 105 | Context "Update-VcMdtApplication updates Redistributables in the MDT share" { 106 | It "Updates the 14 x64 Redistributables in MDT OK" { 107 | $params = @{ 108 | VcList = $(Get-VcList -Release "14" -Architecture "x64" | Save-VcRedist -Path $Path) 109 | MdtPath = "$env:RUNNER_TEMP\Deployment" 110 | AppFolder = "VcRedists" 111 | Silent = $true 112 | MdtDrive = "DS020" 113 | Publisher = "Microsoft" 114 | } 115 | { Update-VcMdtApplication @params } | Should -Not -Throw 116 | } 117 | 118 | It "Updates the 14 x86 Redistributables in MDT OK" { 119 | $params = @{ 120 | VcList = $(Get-VcList -Release "14" -Architecture "x86" | Save-VcRedist -Path $Path) 121 | Path = $Path 122 | MdtPath = "$env:RUNNER_TEMP\Deployment" 123 | AppFolder = "VcRedists" 124 | Silent = $true 125 | MdtDrive = "DS020" 126 | Publisher = "Microsoft" 127 | } 128 | { Update-VcMdtApplication @params } | Should -Not -Throw 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /tests/Public/Get-VcList.Tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Public Pester function tests. 4 | #> 5 | [CmdletBinding()] 6 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Justification = "This OK for the tests files.")] 7 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Outputs to log host.")] 8 | param () 9 | 10 | BeforeDiscovery { 11 | $SupportedReleases = @("2015", "2017", "2019", "14") 12 | $SupportedVcRedists = Get-VcList -Release $SupportedReleases 13 | } 14 | 15 | Describe -Name "Validate Get-VcList for " -ForEach $SupportedVcRedists { 16 | BeforeAll { 17 | $VcRedist = $_ 18 | $Name = $_.Name 19 | $Architecture = $_.Architecture 20 | } 21 | 22 | Context "Validate Get-VcList array properties" { 23 | It "VcRedist ', ' has a Name property" { 24 | [System.Boolean]($VcRedist.Name) | Should -BeTrue 25 | } 26 | 27 | It "VcRedist ', ' has a ProductCode property" { 28 | [System.Boolean]($VcRedist.ProductCode) | Should -BeTrue 29 | } 30 | 31 | It "VcRedist ', ' has a Version property" { 32 | [System.Boolean]($VcRedist.Version) | Should -BeTrue 33 | } 34 | 35 | It "VcRedist ', ' has a URL property" { 36 | [System.Boolean]($VcRedist.URL) | Should -BeTrue 37 | } 38 | 39 | It "VcRedist ', ' has a URI property" { 40 | [System.Boolean]($VcRedist.URI) | Should -BeTrue 41 | } 42 | 43 | It "VcRedist ', ' has a Release property" { 44 | [System.Boolean]($VcRedist.Release) | Should -BeTrue 45 | } 46 | 47 | It "VcRedist ', ' has an Architecture property" { 48 | [System.Boolean]($VcRedist.Architecture) | Should -BeTrue 49 | } 50 | 51 | It "VcRedist ', ' has an Install property" { 52 | [System.Boolean]($VcRedist.Install) | Should -BeTrue 53 | } 54 | 55 | It "VcRedist ', ' has a SilentInstall property" { 56 | [System.Boolean]($VcRedist.SilentInstall) | Should -BeTrue 57 | } 58 | 59 | It "VcRedist ', ' has a SilentUninstall property" { 60 | [System.Boolean]($VcRedist.SilentUninstall) | Should -BeTrue 61 | } 62 | 63 | It "VcRedist ', ' has an UninstallKey property" { 64 | [System.Boolean]($VcRedist.UninstallKey) | Should -BeTrue 65 | } 66 | } 67 | } 68 | 69 | # Describe -Name "Get-VcRedist parameters" { 70 | # Context "Test Get-VcRedist parameters" { 71 | # It "Returns the expected output for VcRedist 14" { 72 | # (Get-VcList -Release "14")[0].Name | Should -BeLike "Microsoft Visual C\+\+ v14 Redistributable \((Arm64|x64|x86)\)" 73 | # } 74 | 75 | # It "Returns 1 item for x64" { 76 | # (Get-VcList -Architecture "x64").Release | Should -BeExactly "14" 77 | # } 78 | # } 79 | # } 80 | 81 | Describe -Name "Validate manifest counts from Get-VcList" { 82 | BeforeAll { 83 | $VcCount = @{ 84 | "Default" = 2 85 | "Supported" = 9 86 | "Unsupported" = 18 87 | "All" = 27 88 | } 89 | } 90 | 91 | Context "Return built-in manifest with Get-VcList" { 92 | It "Given no parameters, it returns supported Visual C++ Redistributables" { 93 | Get-VcList | Should -HaveCount $VcCount.Default 94 | } 95 | It "Given valid parameter -Export All, it returns all Visual C++ Redistributables" { 96 | Get-VcList -Export "All" | Should -HaveCount $VcCount.All 97 | } 98 | It "Given valid parameter -Export Supported, it returns all Visual C++ Redistributables" { 99 | Get-VcList -Export "Supported" | Should -HaveCount $VcCount.Supported 100 | } 101 | It "Given valid parameter -Export Unsupported, it returns unsupported Visual C++ Redistributables" { 102 | Get-VcList -Export "Unsupported" | Should -HaveCount $VcCount.Unsupported 103 | } 104 | } 105 | } 106 | 107 | Describe -Name "Validate manifest scenarios with Get-VcList" { 108 | Context 'Validate Get-VcList' { 109 | BeforeAll { 110 | $Json = Export-VcManifest -Path $env:RUNNER_TEMP 111 | $VcList = Get-VcList -Path $Json 112 | $VcCount = @{ 113 | "Default" = 2 114 | "Supported" = 9 115 | "Unsupported" = 18 116 | "All" = 27 117 | } 118 | } 119 | 120 | It "Given valid parameter -Path, it returns Visual C++ Redistributables from an external manifest" { 121 | $VcList.Count | Should -BeGreaterOrEqual $VcCount.Default 122 | } 123 | It "Given an JSON file that does not exist, it should throw an error" { 124 | { Get-VcList -Path $([System.IO.Path]::Combine($env:RUNNER_TEMP, "RedistsFail.json")) } | Should -Throw 125 | } 126 | It "Given an invalid JSON file, should throw an error on read" { 127 | { Get-VcList -Path $([System.IO.Path]::Combine($env:GITHUB_WORKSPACE, "README.MD")) } | Should -Throw 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /tests/Manifest.Tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Manifest tests. 4 | #> 5 | [CmdletBinding()] 6 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Justification = "This OK for the tests files.")] 7 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Outputs to log host.")] 8 | param () 9 | 10 | BeforeDiscovery { 11 | $ValidateReleasesAmd64 = @("2017", "2019", "14") 12 | $ValidateReleasesArm64 = @("14") 13 | 14 | if ($env:PROCESSOR_ARCHITECTURE -eq "AMD64") { 15 | $SkipAmd = $false 16 | } 17 | else { 18 | $SkipAmd = $true 19 | } 20 | if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") { 21 | $SkipArm = $false 22 | } 23 | else { 24 | $SkipArm = $true 25 | } 26 | } 27 | 28 | Describe -Name "AMD64 specific tests" -Skip:$SkipAmd { 29 | Describe -Name "VcRedist manifest tests" -ForEach $ValidateReleasesAmd64 { 30 | BeforeAll { 31 | Get-InstalledVcRedist | Uninstall-VcRedist -Confirm:$false 32 | } 33 | 34 | Context "Validate manifest" { 35 | BeforeAll { 36 | $VcManifest = "$env:GITHUB_WORKSPACE\VcRedist\VisualCRedistributables.json" 37 | Write-Host -ForegroundColor "Cyan" "`tGetting manifest from: $VcManifest." 38 | $CurrentManifest = Get-Content -Path $VcManifest | ConvertFrom-Json 39 | $VcRedist = $_ 40 | 41 | $Path = $([System.IO.Path]::Combine($env:RUNNER_TEMP, "Downloads")) 42 | New-Item -Path $Path -ItemType "Directory" -ErrorAction "SilentlyContinue" > $null 43 | Save-VcRedist -VcList (Get-VcList -Release $VcRedist) -Path $Path 44 | 45 | $Architectures = @("x86", "x64") 46 | } 47 | 48 | Context "Compare manifest version against installed version for " -ForEach $Architectures { 49 | BeforeEach { 50 | $VcList = Get-VcList -Release $VcRedist | Save-VcRedist -Path $([System.IO.Path]::Combine($env:RUNNER_TEMP, "Downloads")) 51 | Install-VcRedist -VcList $VcList -Silent 52 | $InstalledVcRedists = Get-InstalledVcRedist 53 | 54 | $ManifestVcRedist = $CurrentManifest.Supported | Where-Object { $_.Release -eq $VcRedist } 55 | $InstalledItem = $InstalledVcRedists | Where-Object { ($VcRedist -eq $ManifestVcRedist.Release) -and ($_ -eq $ManifestVcRedist.Architecture) } 56 | } 57 | 58 | # If the manifest version of the VcRedist is lower than the installed version, the manifest is out of date 59 | It "$($ManifestVcRedist.Release) $($ManifestVcRedist.Architecture) version should be current" { 60 | [System.Version]$InstalledItem.Version -gt [System.Version]$ManifestVcRedist.Version | Should -Be $false 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | Describe -Name "ARM64 specific tests" -Skip:$SkipArm { 68 | Describe -Name "VcRedist manifest tests" -ForEach $ValidateReleasesArm64 { 69 | BeforeAll { 70 | Get-InstalledVcRedist | Uninstall-VcRedist -Confirm:$false 71 | } 72 | 73 | Context "Validate manifest" { 74 | BeforeAll { 75 | $VcManifest = "$env:GITHUB_WORKSPACE\VcRedist\VisualCRedistributables.json" 76 | Write-Host -ForegroundColor "Cyan" "`tGetting manifest from: $VcManifest." 77 | $CurrentManifest = Get-Content -Path $VcManifest | ConvertFrom-Json 78 | $VcRedist = $_ 79 | 80 | $Path = $([System.IO.Path]::Combine($env:RUNNER_TEMP, "Downloads")) 81 | New-Item -Path $Path -ItemType "Directory" -ErrorAction "SilentlyContinue" > $null 82 | Save-VcRedist -VcList (Get-VcList -Release $VcRedist) -Path $Path 83 | 84 | $Architectures = @("arm64") 85 | } 86 | 87 | Context "Compare manifest version against installed version for " -ForEach $Architectures { 88 | BeforeEach { 89 | $VcList = Get-VcList -Release $VcRedist | Save-VcRedist -Path $([System.IO.Path]::Combine($env:RUNNER_TEMP, "Downloads")) 90 | Install-VcRedist -VcList $VcList -Silent 91 | $InstalledVcRedists = Get-InstalledVcRedist 92 | 93 | $ManifestVcRedist = $CurrentManifest.Supported | Where-Object { $_.Release -eq $VcRedist } 94 | $InstalledItem = $InstalledVcRedists | Where-Object { ($VcRedist -eq $ManifestVcRedist.Release) -and ($_ -eq $ManifestVcRedist.Architecture) } 95 | } 96 | 97 | # If the manifest version of the VcRedist is lower than the installed version, the manifest is out of date 98 | It "$($ManifestVcRedist.Release) $($ManifestVcRedist.Architecture) version should be current" { 99 | [System.Version]$InstalledItem.Version -gt [System.Version]$ManifestVcRedist.Version | Should -Be $false 100 | } 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /VcRedist/Public/Get-VcList.ps1: -------------------------------------------------------------------------------- 1 | function Get-VcList { 2 | <# 3 | .EXTERNALHELP VcRedist-help.xml 4 | #> 5 | [Alias("Get-VcRedist")] 6 | [OutputType([System.Management.Automation.PSObject])] 7 | [CmdletBinding(DefaultParameterSetName = "Manifest", HelpURI = "https://vcredist.com/get-vclist/")] 8 | param ( 9 | [Parameter(Mandatory = $false, Position = 0, ParameterSetName = "Manifest")] 10 | [ValidateSet("2012", "2013", "2015", "2017", "2019", "14")] 11 | [System.String[]] $Release = @("2012", "2013", "14"), 12 | 13 | [Parameter(Mandatory = $false, Position = 1, ParameterSetName = "Manifest")] 14 | [ValidateSet("x86", "x64", "ARM64")] 15 | [System.String[]] $Architecture = @("x86", "x64"), 16 | 17 | [Parameter(Mandatory = $false, Position = 2, ValueFromPipeline, ParameterSetName = "Manifest")] 18 | [ValidateScript( { if (Test-Path -Path $_ -PathType "Leaf") { $true } else { throw "Cannot find file $_" } })] 19 | [ValidateNotNullOrEmpty()] 20 | [Alias("Xml")] 21 | [System.String] $Path = $(Join-Path -Path $MyInvocation.MyCommand.Module.ModuleBase -ChildPath "VisualCRedistributables.json"), 22 | 23 | [Parameter(Mandatory = $false, Position = 0, ParameterSetName = "Export")] 24 | [ValidateSet("Supported", "All", "Unsupported")] 25 | [System.String] $Export = "Supported" 26 | ) 27 | 28 | process { 29 | try { 30 | # Convert the JSON content to an object 31 | Write-Verbose -Message "Reading VcRedist manifest '$Path'." 32 | $params = @{ 33 | Path = $Path 34 | Raw = $true 35 | ErrorAction = "Stop" 36 | } 37 | $Content = Get-Content @params 38 | Write-Verbose -Message "Converting JSON." 39 | $JsonManifest = $Content | ConvertFrom-Json -ErrorAction "Continue" 40 | } 41 | catch [System.Exception] { 42 | Write-Warning -Message "Unable to convert manifest JSON to required object. Please validate the input manifest." 43 | throw $_ 44 | } 45 | 46 | if ($null -ne $JsonManifest) { 47 | if ($PSBoundParameters.ContainsKey("Export")) { 48 | switch ($Export) { 49 | "All" { 50 | Write-Verbose -Message "Exporting all VcRedists." 51 | Write-Warning -Message "This list includes unsupported Visual C++ Redistributables." 52 | [System.Management.Automation.PSObject] $Output = $JsonManifest.Supported + $JsonManifest.Unsupported 53 | break 54 | } 55 | "Supported" { 56 | Write-Verbose -Message "Exporting supported VcRedists." 57 | [System.Management.Automation.PSObject] $Output = $JsonManifest.Supported 58 | break 59 | } 60 | "Unsupported" { 61 | Write-Verbose -Message "Exporting unsupported VcRedists." 62 | Write-Warning -Message "This list includes unsupported Visual C++ Redistributables." 63 | [System.Management.Automation.PSObject] $Output = $JsonManifest.Unsupported 64 | break 65 | } 66 | } 67 | } 68 | else { 69 | # Filter the list for architecture and release 70 | # if ($Release -match $JsonManifest.Unsupported.Release) { 71 | # Write-Warning -Message "This list includes unsupported Visual C++ Redistributables." 72 | # } 73 | [System.Management.Automation.PSObject]$Output = $JsonManifest.Supported | Where-Object { $Release -contains $_.Release } | ` 74 | Where-Object { $Architecture -contains $_.Architecture } 75 | } 76 | 77 | try { 78 | # Get the count of items in $Output; Because it's a PSCustomObject we can't use the .count property so need to measure the object 79 | # Grab a NoteProperty and count how many of those there are to get the object count 80 | $Property = $Output | Get-Member -ErrorAction "SilentlyContinue" | Where-Object { $_.MemberType -eq "NoteProperty" } | Select-Object -ExpandProperty "Name" | Select-Object -First 1 81 | $Count = $Output.$Property.Count - 1 82 | } 83 | catch { 84 | $Count = 0 85 | } 86 | 87 | # Replace strings in the manifest 88 | Write-Verbose -Message "Object count is: $($Output.$Property.Count)." 89 | for ($i = 0; $i -le $Count; $i++) { 90 | try { 91 | $Output[$i].SilentUninstall = $Output[$i].SilentUninstall ` 92 | -replace "#Installer", $(Split-Path -Path $Output[$i].URI -Leaf) ` 93 | -replace "#ProductCode", $Output[$i].ProductCode 94 | } 95 | catch { 96 | Write-Verbose -Message "Failed to replace strings in: $($JsonManifest[$i].Name)." 97 | } 98 | } 99 | Write-Output -InputObject $Output 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /VcRedist/Public/Install-VcRedist.ps1: -------------------------------------------------------------------------------- 1 | function Install-VcRedist { 2 | <# 3 | .EXTERNALHELP VcRedist-help.xml 4 | #> 5 | [CmdletBinding(SupportsShouldProcess = $true, HelpURI = "https://vcredist.com/install-vcredist/")] 6 | [OutputType([System.Management.Automation.PSObject])] 7 | param ( 8 | [Parameter( 9 | Mandatory = $true, 10 | Position = 0, 11 | ValueFromPipeline = $true, 12 | HelpMessage = "Pass a VcList object from Save-VcRedist.")] 13 | [ValidateNotNullOrEmpty()] 14 | [System.Management.Automation.PSObject] $VcList, 15 | 16 | [Parameter(Mandatory = $false)] 17 | [System.ObsoleteAttribute("This parameter is not longer supported. The Path property must be on the object passed to -VcList.")] 18 | [System.String] $Path, 19 | 20 | [Parameter(Mandatory = $false)] 21 | [System.Management.Automation.SwitchParameter] $Silent, 22 | 23 | [Parameter(Mandatory = $false)] 24 | [System.Management.Automation.SwitchParameter] $Force 25 | ) 26 | 27 | begin { 28 | # Get script elevation status 29 | [System.Boolean] $Elevated = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator") 30 | if ($Elevated -eq $false) { 31 | $Msg = "Installing the Visual C++ Redistributables requires elevation. The current Windows PowerShell session is not running as Administrator. Start Windows PowerShell by using the Run as Administrator option, and then try running the script again" 32 | throw [System.Management.Automation.ScriptRequiresException]::New($Msg) 33 | } 34 | 35 | # Get currently installed VcRedist versions 36 | $currentInstalled = Get-InstalledVcRedist 37 | } 38 | 39 | process { 40 | 41 | # Make sure that $VcList has the required properties 42 | if ((Test-VcListObject -VcList $VcList) -ne $true) { 43 | $Msg = "Required properties not found. Please ensure the output from Save-VcRedist is sent to this function. " 44 | throw [System.Management.Automation.PropertyNotFoundException]::New($Msg) 45 | } 46 | 47 | # Sort $VcList by version number from oldest to newest 48 | foreach ($VcRedist in ($VcList | Sort-Object -Property @{ Expression = { [System.Version]$_.Version }; Descending = $false })) { 49 | 50 | # If already installed or the -Force parameter is not specified, skip 51 | if (($currentInstalled | Where-Object { $VcRedist.ProductCode -contains $_.ProductCode }) -and !($PSBoundParameters.ContainsKey("Force"))) { 52 | Write-Information -MessageData "VcRedist already installed: '$($VcRedist.Name) $($VcRedist.Version) $($VcRedist.Architecture)'" -InformationAction "Continue" 53 | } 54 | else { 55 | 56 | # Avoid installing 64-bit Redistributable on x86 Windows 57 | if (((Get-Bitness) -eq "x86") -and ($VcRedist.Architecture -eq "x64")) { 58 | Write-Warning -Message "Incompatible architecture: '$($VcRedist.Name) $($VcRedist.Version) $($VcRedist.Architecture)'" 59 | } 60 | else { 61 | 62 | if (Test-Path -Path $VcRedist.Path) { 63 | Write-Verbose -Message "Install VcRedist: '$($VcRedist.Name) $($VcRedist.Version) $($VcRedist.Architecture)'" 64 | if ($PSCmdlet.ShouldProcess("$($VcRedist.Path) $($VcRedist.Install)", "Install")) { 65 | 66 | try { 67 | # Create parameters with -ArgumentList set based on Install/SilentInstall properties in the manifest 68 | $params = @{ 69 | FilePath = $VcRedist.Path 70 | ArgumentList = if ($Silent) { $VcRedist.SilentInstall } else { $VcRedist.Install } 71 | PassThru = $true 72 | Wait = $true 73 | NoNewWindow = $true 74 | Verbose = $VerbosePreference 75 | ErrorAction = "Continue" 76 | } 77 | $Result = Start-Process @params 78 | } 79 | catch { 80 | throw $_ 81 | } 82 | $Installed = Get-InstalledVcRedist | Where-Object { $_.ProductCode -eq $VcRedist.ProductCode } 83 | if ($Installed) { 84 | Write-Verbose -Message "Installed successfully: '$($VcRedist.Name) $($VcRedist.Version) $($VcRedist.Architecture)'; ExitCode: $($Result.ExitCode)" 85 | } 86 | } 87 | } 88 | else { 89 | Write-Warning -Message "Cannot find: '$($VcRedist.Path)'. Download with Save-VcRedist." 90 | } 91 | } 92 | } 93 | } 94 | } 95 | 96 | end { 97 | # Get the installed Visual C++ Redistributables applications to return on the pipeline 98 | Write-Output -InputObject (Get-InstalledVcRedist) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /VcRedist/VcRedist.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'VcRedist' 3 | # 4 | # Generated by: Aaron Parker 5 | # 6 | # Generated on: 11/27/2025 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | RootModule = 'VcRedist.psm1' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '4.1.526' 16 | 17 | # Supported PSEditions 18 | # CompatiblePSEditions = @() 19 | 20 | # ID used to uniquely identify this module 21 | GUID = '9139778c-9a1a-4faf-aa88-5ac6fd3b3e48' 22 | 23 | # Author of this module 24 | Author = 'Aaron Parker' 25 | 26 | # Company or vendor of this module 27 | CompanyName = 'stealthpuppy' 28 | 29 | # Copyright statement for this module 30 | Copyright = '(c) 2025 stealthpuppy. All rights reserved.' 31 | 32 | # Description of the functionality provided by this module 33 | Description = 'A module for lifecycle management of the Microsoft Visual C++ Redistributables. VcRedist downloads, installs and uninstalls the supported (and unsupported) Redistributables. Use for local install, gold image creation and update, or importing as applications into the Microsoft Deployment Toolkit, Microsoft Configuration Manager or Microsoft Intune. Supports passive and silent installs, and uninstalls of the Visual C++ Redistributables.' 34 | 35 | # Minimum version of the PowerShell engine required by this module 36 | PowerShellVersion = '3.0' 37 | 38 | # Name of the PowerShell host required by this module 39 | # PowerShellHostName = '' 40 | 41 | # Minimum version of the PowerShell host required by this module 42 | # PowerShellHostVersion = '' 43 | 44 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 45 | # DotNetFrameworkVersion = '' 46 | 47 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 48 | # ClrVersion = '' 49 | 50 | # Processor architecture (None, X86, Amd64) required by this module 51 | # ProcessorArchitecture = '' 52 | 53 | # Modules that must be imported into the global environment prior to importing this module 54 | # RequiredModules = @() 55 | 56 | # Assemblies that must be loaded prior to importing this module 57 | # RequiredAssemblies = @() 58 | 59 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 60 | # ScriptsToProcess = @() 61 | 62 | # Type files (.ps1xml) to be loaded when importing this module 63 | # TypesToProcess = @() 64 | 65 | # Format files (.ps1xml) to be loaded when importing this module 66 | # FormatsToProcess = @() 67 | 68 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 69 | # NestedModules = @() 70 | 71 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 72 | FunctionsToExport = @('Export-VcManifest', 'Get-InstalledVcRedist', 73 | 'Get-VcIntuneApplication', 'Get-VcList', 74 | 'Import-VcConfigMgrApplication', 'Import-VcIntuneApplication', 75 | 'Import-VcMdtApplication', 'Install-VcRedist', 'New-VcMdtBundle', 76 | 'Remove-VcIntuneApplication', 'Save-VcRedist', 'Test-VcRedistUri', 77 | 'Uninstall-VcRedist', 'Update-VcMdtApplication', 'Update-VcMdtBundle') 78 | 79 | # 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. 80 | CmdletsToExport = @() 81 | 82 | # Variables to export from this module 83 | VariablesToExport = 'VcManifest' 84 | 85 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 86 | AliasesToExport = 'Get-VcRedist', 'Export-VcXml', 'Import-VcCmApp', 'Import-VcMdtApp', 87 | 'Test-VcRedistDownload' 88 | 89 | # DSC resources to export from this module 90 | # DscResourcesToExport = @() 91 | 92 | # List of all modules packaged with this module 93 | # ModuleList = @() 94 | 95 | # List of all files packaged with this module 96 | # FileList = @() 97 | 98 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 99 | PrivateData = @{ 100 | 101 | #RepositorySourceLocation of this module 102 | RepositorySourceLocation = 'https://github.com/aaronparker/vcredist/' 103 | 104 | PSData = @{ 105 | 106 | # Tags applied to this module. These help with module discovery in online galleries. 107 | Tags = 'Redistributables','C++','VisualC','VisualStudio','MDT','ConfigMgr','SCCM','Intune','Windows' 108 | 109 | # A URL to the license for this module. 110 | LicenseUri = 'https://github.com/aaronparker/vcredist/blob/main/LICENSE' 111 | 112 | # A URL to the main website for this project. 113 | ProjectUri = 'https://vcredist.com/' 114 | 115 | # A URL to an icon representing this module. 116 | IconUri = 'https://vcredist.com/img/logo.png' 117 | 118 | # ReleaseNotes of this module 119 | ReleaseNotes = 'https://vcredist.com/changelog/' 120 | 121 | # Prerelease string of this module 122 | # Prerelease = '' 123 | 124 | # Flag to indicate whether the module requires explicit user acceptance for install/update/save 125 | # RequireLicenseAcceptance = $false 126 | 127 | # External dependent modules of this module 128 | # ExternalModuleDependencies = @() 129 | 130 | } # End of PSData hashtable 131 | 132 | } # End of PrivateData hashtable 133 | 134 | # HelpInfo URI of this module 135 | # HelpInfoURI = '' 136 | 137 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 138 | # DefaultCommandPrefix = '' 139 | 140 | } 141 | 142 | -------------------------------------------------------------------------------- /VcRedist/Public/Update-VcMdtBundle.ps1: -------------------------------------------------------------------------------- 1 | function Update-VcMdtBundle { 2 | <# 3 | .EXTERNALHELP VcRedist-help.xml 4 | #> 5 | [CmdletBinding(SupportsShouldProcess = $true, HelpURI = "https://vcredist.com/update-vcmdtbundle/")] 6 | [OutputType([System.Management.Automation.PSObject])] 7 | param ( 8 | [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline)] 9 | [ValidateScript( { if (Test-Path -Path $_ -PathType 'Container') { $true } else { throw "Cannot find path $_" } })] 10 | [System.String] $MdtPath, 11 | 12 | [Parameter(Mandatory = $false)] 13 | [ValidatePattern("^[a-zA-Z0-9]+$")] 14 | [ValidateNotNullOrEmpty()] 15 | [System.String] $AppFolder = "VcRedists", 16 | 17 | [Parameter(Mandatory = $false, Position = 1)] 18 | [ValidatePattern("^[a-zA-Z0-9]+$")] 19 | [System.String] $MdtDrive = "DS099", 20 | 21 | [Parameter(Mandatory = $false, Position = 2)] 22 | [ValidatePattern("^[a-zA-Z0-9]+$")] 23 | [System.String] $Publisher = "Microsoft", 24 | 25 | [Parameter(Mandatory = $false, Position = 3)] 26 | [ValidatePattern("^[a-zA-Z0-9\+ ]+$")] 27 | [System.String] $BundleName = "Visual C++ Redistributables" 28 | ) 29 | 30 | begin { 31 | # Variables 32 | $Applications = "Applications" 33 | Write-Warning -Message "Attempting to update bundle: [$Publisher $BundleName]." 34 | 35 | # If running on PowerShell Core, error and exit. 36 | if (Test-PSCore) { 37 | Write-Warning -Message "PowerShell Core doesn't support PSSnapins. We can't load the MicrosoftDeploymentToolkit module." 38 | throw [System.Management.Automation.InvalidPowerShellStateException] 39 | } 40 | } 41 | 42 | process { 43 | # Import the MDT module and create a PS drive to MdtPath 44 | if (Import-MdtModule) { 45 | if ($PSCmdlet.ShouldProcess($Path, "Mapping")) { 46 | try { 47 | $params = @{ 48 | Drive = $MdtDrive 49 | Path = $MdtPath 50 | ErrorAction = "SilentlyContinue" 51 | } 52 | New-MdtDrive @params > $null 53 | Restore-MDTPersistentDrive -Force > $null 54 | } 55 | catch [System.Exception] { 56 | Write-Warning -Message "Failed to map drive to [$MdtPath]." 57 | throw $_.Exception.Message 58 | } 59 | } 60 | } 61 | else { 62 | Write-Warning -Message "Failed to import the MDT PowerShell module. Please install the MDT Workbench and try again." 63 | throw [System.Management.Automation.InvalidPowerShellStateException] 64 | } 65 | 66 | # Get properties from the existing bundle/s 67 | try { 68 | $gciParams = @{ 69 | Path = "$($MdtDrive):\$Applications" 70 | Recurse = $true 71 | ErrorAction = "SilentlyContinue" 72 | } 73 | $Bundles = Get-ChildItem @gciParams | Where-Object { $_.Name -eq "$Publisher $BundleName" } 74 | } 75 | catch [System.Exception] { 76 | Write-Warning -Message "Failed to retrieve the existing Visual C++ Redistributables bundle." 77 | throw $_.Exception.Message 78 | } 79 | 80 | foreach ($Bundle in $Bundles) { 81 | Write-Verbose -Message "Found bundle: '$($Bundle.Name)'." 82 | 83 | # Grab the Visual C++ Redistributable application guids; Sort added VcRedists by version so they are ordered correctly 84 | $target = "$($MdtDrive):\$Applications\$AppFolder" 85 | Write-Verbose -Message "Gathering VcRedist applications in: $target" 86 | $existingVcRedists = Get-ChildItem -Path $target | Where-Object { ($_.Name -like "*Visual C++*") -and ($_.guid -ne $bundle.guid) -and ($_.CommandLine -ne "") } 87 | $existingVcRedists = $existingVcRedists | Sort-Object -Property @{ Expression = { [System.Version]$_.Version }; Descending = $false } 88 | $dependencies = @(); foreach ($app in $existingVcRedists) { $dependencies += $app.guid } 89 | 90 | if ($PSCmdlet.ShouldProcess($bundle.PSPath, "Update dependencies")) { 91 | try { 92 | $sipParams = @{ 93 | Path = ($bundle.PSPath.Replace($bundle.PSProvider, "")).Trim(":") 94 | Name = "Dependency" 95 | Value = $dependencies 96 | ErrorAction = "SilentlyContinue" 97 | Force = $true 98 | } 99 | Set-ItemProperty @sipParams > $null 100 | } 101 | catch [System.Exception] { 102 | Write-Warning -Message "Error updating VcRedist bundle dependencies." 103 | throw $_.Exception.Message 104 | } 105 | } 106 | if ($PSCmdlet.ShouldProcess($bundle.PSPath, "Update version")) { 107 | try { 108 | $sipParams = @{ 109 | Path = $($bundle.PSPath.Replace($bundle.PSProvider, "")).Trim(":") 110 | Name = "Version" 111 | Value = $(Get-Date -Format (([System.Globalization.CultureInfo]::CurrentUICulture.DateTimeFormat).ShortDatePattern)) 112 | ErrorAction = "SilentlyContinue" 113 | Force = $true 114 | } 115 | Set-ItemProperty @sipParams > $null 116 | } 117 | catch [System.Exception] { 118 | Write-Warning -Message "Error updating VcRedist bundle version." 119 | throw $_.Exception.Message 120 | } 121 | } 122 | 123 | # Write the bundle to the pipeline 124 | Write-Output -InputObject ($bundle | Select-Object -Property * ) 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /VcRedist/Public/Uninstall-VcRedist.ps1: -------------------------------------------------------------------------------- 1 | function Uninstall-VcRedist { 2 | <# 3 | .EXTERNALHELP VcRedist-help.xml 4 | #> 5 | [CmdletBinding(DefaultParameterSetName = "Manual", SupportsShouldProcess = $true, ConfirmImpact = "High", HelpURI = "https://vcredist.com/uninstall-vcredist/")] 6 | [OutputType([System.Management.Automation.PSObject])] 7 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")] 8 | param ( 9 | [Parameter(Mandatory = $false, Position = 0, ParameterSetName = "Manual")] 10 | [ValidateSet("2005", "2008", "2010", "2012", "2013", "2015", "2017", "2019", "14")] 11 | [System.String[]] $Release = @("2005", "2008", "2010", "2012", "2013", "2015", "2017", "2019", "14"), 12 | 13 | [Parameter(Mandatory = $false, Position = 1, ParameterSetName = "Manual")] 14 | [ValidateSet("x86", "x64", "ARM64")] 15 | [System.String[]] $Architecture = @("x86", "x64"), 16 | 17 | [Parameter( 18 | Mandatory = $true, 19 | Position = 0, 20 | ValueFromPipeline, 21 | ParameterSetName = "Pipeline", 22 | HelpMessage = "Pass a VcList object from Get-VcList.")] 23 | [ValidateNotNullOrEmpty()] 24 | [System.Management.Automation.PSObject] $VcList 25 | ) 26 | 27 | begin { 28 | # Get script elevation status 29 | [System.Boolean] $Elevated = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator") 30 | if ($Elevated -eq $false) { 31 | $Msg = "Uninstalling the Visual C++ Redistributables requires elevation. The current Windows PowerShell session is not running as Administrator. Start Windows PowerShell by using the Run as Administrator option, and then try running the script again" 32 | throw [System.Management.Automation.ScriptRequiresException]::New($Msg) 33 | } 34 | } 35 | 36 | process { 37 | switch ($PSCmdlet.ParameterSetName) { 38 | "Manual" { 39 | # Get the installed VcRedists and filter 40 | Write-Verbose -Message "Getting locally installed Visual C++ Redistributables" 41 | $VcRedistsToRemove = Get-InstalledVcRedist | Where-Object { $Release -contains $_.Release } | Where-Object { $Architecture -contains $_.Architecture } 42 | } 43 | "Pipeline" { 44 | Write-Verbose -Message "Removing installed Visual C++ Redistributables passed via the pipeline" 45 | $VcRedistsToRemove = $VcList 46 | } 47 | } 48 | 49 | # Walk through each VcRedist and uninstall 50 | foreach ($VcRedist in $VcRedistsToRemove) { 51 | 52 | # We could be passed an object from Get-VcList or Get InstalledVcRedist 53 | if ([System.String]::IsNullOrEmpty($VcRedist.UninstallString) -eq $false) { 54 | $UninstallString = $VcRedist.UninstallString 55 | } 56 | elseif ([System.String]::IsNullOrEmpty($VcRedist.SilentUninstall) -eq $false) { 57 | $UninstallString = $VcRedist.SilentUninstall 58 | } 59 | 60 | if ([System.String]::IsNullOrEmpty($UninstallString)) { 61 | $Msg = "Cannot find uninstall string. Please check object passed to this function." 62 | throw [System.Management.Automation.PropertyNotFoundException]::New($Msg) 63 | } 64 | else { 65 | # Build the uninstall command 66 | switch -Regex ($UninstallString.ToLower()) { 67 | "^msiexec.*$" { 68 | Write-Verbose -Message "VcRedist uninstall uses Msiexec." 69 | $params = @{ 70 | FilePath = "$Env:SystemRoot\System32\msiexec.exe" 71 | ArgumentList = "/uninstall $($VcRedist.ProductCode) /quiet /norestart" 72 | PassThru = $true 73 | Wait = $true 74 | NoNewWindow = $true 75 | Verbose = $VerbosePreference 76 | } 77 | } 78 | default { 79 | $FilePath = [Regex]::Match($UninstallString, '\"(.*)\"').Captures.Groups[1].Value 80 | Write-Verbose -Message "VcRedist uninstall uses '$FilePath'." 81 | $params = @{ 82 | FilePath = $FilePath 83 | ArgumentList = "/uninstall /quiet /norestart" 84 | PassThru = $true 85 | Wait = $true 86 | NoNewWindow = $true 87 | Verbose = $VerbosePreference 88 | } 89 | } 90 | } 91 | 92 | if ($PSCmdlet.ShouldProcess($VcRedist.Name, "Uninstall")) { 93 | try { 94 | $Result = Start-Process @params 95 | $State = "Uninstalled" 96 | } 97 | catch [System.Exception] { 98 | Write-Warning -Message "Failure in uninstalling $($VcRedist.Name) $($VcRedist.Version) $($VcRedist.Architecture)" 99 | $State = "Failed" 100 | } 101 | finally { 102 | $Object = [PSCustomObject] @{ 103 | Name = $VcRedist.Name 104 | Version = $VcRedist.Version 105 | Release = $VcRedist.Release 106 | Architecture = $VcRedist.Architecture 107 | State = $State 108 | ExitCode = $Result.ExitCode 109 | } 110 | Write-Output -InputObject $Object 111 | } 112 | } 113 | } 114 | } 115 | } 116 | 117 | end { 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /tests/Public/Install-VcRedist.Tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Public Pester function tests. 4 | #> 5 | [CmdletBinding()] 6 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Justification = "This OK for the tests files.")] 7 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Outputs to log host.")] 8 | param () 9 | 10 | BeforeDiscovery { 11 | $SupportedReleasesAmd64 = @("2015", "2017", "2019", "14") 12 | $SupportedReleasesArm64 = @("14") 13 | $UnsupportedReleases = @("2008", "2010", "2012", "2013") 14 | # $UnsupportedReleases = Get-VcList -Export "Unsupported" 15 | 16 | if ($env:PROCESSOR_ARCHITECTURE -eq "AMD64") { 17 | $SkipAmd = $false 18 | } 19 | else { 20 | $SkipAmd = $true 21 | } 22 | if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") { 23 | $SkipArm = $false 24 | } 25 | else { 26 | $SkipArm = $true 27 | } 28 | } 29 | 30 | Describe -Name "AMD64 specific tests" -Skip:$SkipAmd { 31 | Describe -Name "Install-VcRedist with unsupported Redistributables" -ForEach $UnsupportedReleases { 32 | BeforeAll { 33 | $Release = $_ 34 | 35 | # Create download path 36 | if ($env:Temp) { 37 | $Path = Join-Path -Path $env:Temp -ChildPath "Downloads" 38 | } 39 | elseif ($env:TMPDIR) { 40 | $Path = Join-Path -Path $env:TMPDIR -ChildPath "Downloads" 41 | } 42 | elseif ($env:RUNNER_TEMP) { 43 | $Path = Join-Path -Path $env:RUNNER_TEMP -ChildPath "Downloads" 44 | } 45 | New-Item -Path $Path -ItemType "Directory" -Force -ErrorAction "SilentlyContinue" > $null 46 | } 47 | 48 | Context "Install x64 Redistributable" { 49 | BeforeAll { 50 | # $VcRedist = $Release | Save-VcRedist -Path $Path 51 | $VcRedist = Get-VcList -Export Unsupported | ` 52 | Where-Object { $_.Release -eq $Release -and $_.Architecture -eq "x64" } | ` 53 | Save-VcRedist -Path $Path 54 | } 55 | 56 | It "Installs OK via parameters" { 57 | { Install-VcRedist -VcList $VcRedist -Silent } | Should -Not -Throw 58 | } 59 | } 60 | 61 | Context "Install x86 Redistributable" { 62 | BeforeAll { 63 | $VcRedist = Get-VcList -Export Unsupported | ` 64 | Where-Object { $_.Release -eq $Release -and $_.Architecture -eq "x86" } | ` 65 | Save-VcRedist -Path $Path 66 | } 67 | 68 | It "Installs OK via parameters" { 69 | { Install-VcRedist -VcList $VcRedist -Silent } | Should -Not -Throw 70 | } 71 | } 72 | } 73 | 74 | Describe -Name "Install-VcRedist with supported Redistributables" -ForEach $SupportedReleasesAmd64 { 75 | BeforeAll { 76 | $Release = $_ 77 | 78 | # Create download path 79 | if ($env:Temp) { 80 | $Path = Join-Path -Path $env:Temp -ChildPath "Downloads" 81 | } 82 | elseif ($env:TMPDIR) { 83 | $Path = Join-Path -Path $env:TMPDIR -ChildPath "Downloads" 84 | } 85 | elseif ($env:RUNNER_TEMP) { 86 | $Path = Join-Path -Path $env:RUNNER_TEMP -ChildPath "Downloads" 87 | } 88 | New-Item -Path $Path -ItemType "Directory" -Force -ErrorAction "SilentlyContinue" > $Null 89 | } 90 | 91 | Context "Install x64 Redistributable" { 92 | BeforeAll { 93 | $VcRedist = Get-VcList -Release $Release -Architecture "x64" | Save-VcRedist -Path $Path 94 | } 95 | 96 | It "Installs the VcRedist: via parameters" { 97 | { Install-VcRedist -VcList $VcRedist -Silent } | Should -Not -Throw 98 | } 99 | 100 | It "Returns the list of installed VcRedists after install" { 101 | Install-VcRedist -VcList $VcRedist -Silent | Should -BeOfType "System.Management.Automation.PSObject" 102 | } 103 | 104 | It "Installs the VcRedist: via the pipeline" { 105 | { Get-VcList -Release $Release -Architecture "x64" | ` 106 | Save-VcRedist -Path $Path | ` 107 | Install-VcRedist -Silent } | Should -Not -Throw 108 | } 109 | } 110 | 111 | Context "Install x86 Redistributable" { 112 | BeforeAll { 113 | $VcRedist = Get-VcList -Release $Release -Architecture "x86" | Save-VcRedist -Path $Path 114 | } 115 | 116 | It "Installs the VcRedist: via parameters" { 117 | { Install-VcRedist -VcList $VcRedist -Silent } | Should -Not -Throw 118 | } 119 | 120 | It "Returns the list of installed VcRedists after install" { 121 | Install-VcRedist -VcList $VcRedist -Silent | Should -BeOfType "System.Management.Automation.PSObject" 122 | } 123 | 124 | It "Installs the VcRedist: via the pipeline" { 125 | { Get-VcList -Release $Release -Architecture "x86" | ` 126 | Save-VcRedist -Path $Path | ` 127 | Install-VcRedist -Silent } | Should -Not -Throw 128 | } 129 | } 130 | } 131 | } 132 | 133 | Describe -Name "ARM64 specific tests" -Skip:$SkipArm { 134 | Describe -Name "Install-VcRedist with supported Redistributables" -ForEach $SupportedReleasesArm64 { 135 | BeforeAll { 136 | $Release = $_ 137 | 138 | # Create download path 139 | if ($env:Temp) { 140 | $Path = Join-Path -Path $env:Temp -ChildPath "Downloads" 141 | } 142 | elseif ($env:TMPDIR) { 143 | $Path = Join-Path -Path $env:TMPDIR -ChildPath "Downloads" 144 | } 145 | elseif ($env:RUNNER_TEMP) { 146 | $Path = Join-Path -Path $env:RUNNER_TEMP -ChildPath "Downloads" 147 | } 148 | New-Item -Path $Path -ItemType "Directory" -Force -ErrorAction "SilentlyContinue" > $Null 149 | } 150 | 151 | Context "Install arm64 Redistributable" { 152 | BeforeAll { 153 | $VcRedist = Get-VcList -Release $Release -Architecture "arm64" | Save-VcRedist -Path $Path 154 | $VcRedist | Uninstall-VcRedist -Confirm:$false -ErrorAction "SilentlyContinue" 155 | } 156 | 157 | It "Installs the VcRedist: via parameters" { 158 | { Install-VcRedist -VcList $VcRedist -Silent } | Should -Not -Throw 159 | } 160 | 161 | It "Returns the list of installed VcRedists after install" { 162 | Install-VcRedist -VcList $VcRedist -Silent | Should -BeOfType "System.Management.Automation.PSObject" 163 | } 164 | 165 | It "Installs the VcRedist: via the pipeline" { 166 | { Get-VcList -Release $Release -Architecture "arm64" | ` 167 | Save-VcRedist -Path $Path | ` 168 | Install-VcRedist -Silent } | Should -Not -Throw 169 | } 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /VcRedist/Public/New-VcMdtBundle.ps1: -------------------------------------------------------------------------------- 1 | function New-VcMdtBundle { 2 | <# 3 | .EXTERNALHELP VcRedist-help.xml 4 | #> 5 | [CmdletBinding(SupportsShouldProcess = $true, HelpURI = "https://vcredist.com/import-vcmdtapplication/")] 6 | [OutputType([System.Management.Automation.PSObject])] 7 | param ( 8 | [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline)] 9 | [ValidateScript( { if (Test-Path -Path $_ -PathType "Container") { $true } else { throw "Cannot find path $_" } })] 10 | [System.String] $MdtPath, 11 | 12 | [Parameter(Mandatory = $false, Position = 1)] 13 | [ValidatePattern("^[a-zA-Z0-9]+$")] 14 | [ValidateNotNullOrEmpty()] 15 | [System.String] $AppFolder = "VcRedists", 16 | 17 | [Parameter(Mandatory = $false)] 18 | [System.Management.Automation.SwitchParameter] $Force, 19 | 20 | [Parameter(Mandatory = $false, Position = 2)] 21 | [ValidatePattern("^[a-zA-Z0-9]+$")] 22 | [System.String] $MdtDrive = "DS099", 23 | 24 | [Parameter(Mandatory = $false, Position = 3)] 25 | [ValidatePattern("^[a-zA-Z0-9]+$")] 26 | [System.String] $Publisher = "Microsoft", 27 | 28 | [Parameter(Mandatory = $false, Position = 4)] 29 | [ValidatePattern("^[a-zA-Z0-9\+ ]+$")] 30 | [System.String] $BundleName = "Visual C++ Redistributables", 31 | 32 | [Parameter(Mandatory = $false, Position = 5)] 33 | [ValidatePattern("^[a-zA-Z0-9-]+$")] 34 | [System.String] $Language = "en-US" 35 | ) 36 | 37 | begin { 38 | # If running on PowerShell Core, error and exit. 39 | if (Test-PSCore) { 40 | $Msg = "We can't load the MicrosoftDeploymentToolkit module on PowerShell Core. Please use PowerShell 5.1." 41 | throw [System.TypeLoadException]::New($Msg) 42 | } 43 | 44 | # Import the MDT module and create a PS drive to MdtPath 45 | if (Import-MdtModule) { 46 | if ($PSCmdlet.ShouldProcess($Path, "Mapping")) { 47 | try { 48 | $params = @{ 49 | Drive = $MdtDrive 50 | Path = $MdtPath 51 | ErrorAction = "Continue" 52 | } 53 | New-MdtDrive @params > $null 54 | Restore-MDTPersistentDrive -Force > $null 55 | } 56 | catch [System.Exception] { 57 | Write-Warning -Message "Failed to map drive to: $MdtPath, with: $($_.Exception.Message)" 58 | throw $_ 59 | } 60 | } 61 | } 62 | else { 63 | $Msg = "Failed to import the MDT PowerShell module. Please install the MDT Workbench and try again." 64 | throw [System.Management.Automation.InvalidPowerShellStateException]::New($Msg) 65 | } 66 | } 67 | 68 | process { 69 | Write-Verbose -Message "Getting existing Visual C++ Redistributables the deployment share" 70 | $TargetMdtFolder = "$($MdtDrive):\Applications\$AppFolder" 71 | $existingVcRedists = Get-ChildItem -Path $TargetMdtFolder -ErrorAction "SilentlyContinue" | Where-Object { $_.Name -like "*Visual C++*" } 72 | if ($null -eq $existingVcRedists) { 73 | Write-Warning -Message "Failed to find existing VcRedist applications in the MDT share. Please import the VcRedists with Import-VcMdtApplication." 74 | } 75 | 76 | if (($null -ne $existingVcRedists) -and (Test-Path -Path $TargetMdtFolder)) { 77 | 78 | # Remove the existing bundle if -Force was specified 79 | if ($PSBoundParameters.ContainsKey("Force")) { 80 | if (Test-Path -Path $("$TargetMdtFolder\$Publisher $BundleName")) { 81 | if ($PSCmdlet.ShouldProcess("$($Publisher) $($BundleName)", "Remove bundle")) { 82 | Remove-Item -Path $("$TargetMdtFolder\$Publisher $BundleName") -Force 83 | } 84 | } 85 | } 86 | 87 | # Create the application bundle 88 | if (Test-Path -Path "$TargetMdtFolder\$Publisher $BundleName") { 89 | Write-Verbose "'$($Publisher) $($BundleName)' exists. Use -Force to overwrite the existing bundle." 90 | } 91 | else { 92 | if ($PSCmdlet.ShouldProcess("$($Publisher) $($BundleName)", "Create bundle")) { 93 | 94 | # Grab the Visual C++ Redistributable application guids; Sort added VcRedists by version so they are ordered correctly 95 | Write-Verbose -Message "Gathering VcRedist applications in: $TargetMdtFolder" 96 | $existingVcRedists = $existingVcRedists | Sort-Object -Property @{ Expression = { [System.Version]$_.Version }; Descending = $false } 97 | $dependencies = @(); foreach ($app in $existingVcRedists) { $dependencies += $app.guid } 98 | 99 | # Import the bundle 100 | try { 101 | # Splat the Import-MDTApplication parameters 102 | $importMDTAppParams = @{ 103 | Path = $TargetMdtFolder 104 | Name = "$($Publisher) $($BundleName)" 105 | Enable = $true 106 | Reboot = $false 107 | Hide = $false 108 | Comments = "Application bundle for installing Visual C++ Redistributables. Generated by $($MyInvocation.MyCommand)" 109 | ShortName = $BundleName 110 | Version = $(Get-Date -Format (([System.Globalization.CultureInfo]::CurrentUICulture.DateTimeFormat).ShortDatePattern)) 111 | Publisher = $Publisher 112 | Language = $Language 113 | Dependency = $dependencies 114 | Bundle = $true 115 | } 116 | Import-MDTApplication @importMDTAppParams > $null 117 | } 118 | catch [System.Exception] { 119 | Write-Warning -Message "Error importing the VcRedist bundle. If -Force was specified, the original bundle will have been removed." 120 | throw $_ 121 | } 122 | } 123 | } 124 | } 125 | else { 126 | Write-Error -Message "Failed to find path $TargetMdtFolder." 127 | } 128 | 129 | if (Test-Path -Path $TargetMdtFolder) { 130 | # Return list of apps to the pipeline 131 | Write-Output -InputObject (Get-ChildItem -Path "$TargetMdtFolder\$($Publisher) $($BundleName)" | Select-Object -Property *) 132 | } 133 | else { 134 | Write-Error -Message "Failed to find path $TargetMdtFolder." 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /.github/workflows/update-module.yml: -------------------------------------------------------------------------------- 1 | name: Update module version 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | env: 7 | BUILD_NUMBER: "483" 8 | 9 | jobs: 10 | update-module: 11 | runs-on: windows-latest 12 | outputs: 13 | output1: ${{ steps.commit.outputs.changes_detected }} 14 | 15 | steps: 16 | - uses: actions/checkout@v6 17 | with: 18 | ref: main 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | 21 | - name: Install and cache PowerShell modules 22 | id: psmodulecache 23 | uses: potatoqualitee/psmodulecache@v6.2.1 24 | with: 25 | modules-to-cache: MarkdownPS 26 | shell: pwsh 27 | 28 | # Update the version number in the module manifest 29 | - name: Update module version number 30 | id: update-version 31 | shell: pwsh 32 | run: | 33 | $modulePath = "${{ github.workspace }}\VcRedist" 34 | $manifestPath = "${{ github.workspace }}\VcRedist\VcRedist.psd1" 35 | 36 | # Importing the manifest to determine the version 37 | $manifest = Test-ModuleManifest -Path $manifestPath 38 | [System.Version]$version = $manifest.Version 39 | [System.String]$newVersion = New-Object -TypeName "System.Version" -ArgumentList ($version.Major, $version.Minor, ($version.Build + 1)) 40 | Write-Host "New version is: $newVersion" 41 | 42 | # Update the manifest with the new version value and fix the weird string replace bug 43 | $functionList = ((Get-ChildItem -Path (Join-Path -Path $modulePath -ChildPath "Public")).BaseName) 44 | Update-ModuleManifest -Path $manifestPath -ModuleVersion $newVersion -FunctionsToExport $functionList 45 | (Get-Content -Path $manifestPath) -replace 'PSGet_$module', $module | Set-Content -Path $manifestPath 46 | (Get-Content -Path $manifestPath) -replace 'NewManifest', $module | Set-Content -Path $manifestPath 47 | (Get-Content -Path $manifestPath) -replace 'FunctionsToExport = ','FunctionsToExport = @(' | Set-Content -Path $manifestPath -Force 48 | (Get-Content -Path $manifestPath) -replace "$($functionList[-1])'", "$($functionList[-1])')" | Set-Content -Path $manifestPath -Force 49 | echo "::set-output name=newversion::$($newVersion)" 50 | 51 | # Update the change log with the new version number 52 | - name: Update CHANGELOG.md 53 | id: update-changelog 54 | shell: pwsh 55 | run: | 56 | $changeLog = "${{ github.workspace }}\docs\changelog.md" 57 | $replaceString = "^## VERSION$" 58 | $content = Get-Content -Path $changeLog 59 | if ($content -match $replaceString) { 60 | $content -replace $replaceString, "## ${{steps.update-version.outputs.newversion}}" | Set-Content -Path $changeLog 61 | } 62 | else { 63 | Write-Host "No match in $changeLog for '## VERSION'. Manual update of CHANGELOG required." -ForegroundColor Cyan 64 | } 65 | 66 | # Update the docs with the new version number and supported VcRedists 67 | - name: Update VERSIONS.md 68 | id: update-versions 69 | shell: pwsh 70 | run: | 71 | Import-Module "${{ github.workspace }}\VcRedist" -Force 72 | $VcRedists = Get-Vclist -Export All | ` 73 | Sort-Object -Property @{ Expression = { [System.Version]$_.Version }; Descending = $true } | ` 74 | Select-Object Version, Architecture, Name 75 | $OutFile = "${{ github.workspace }}\docs\versions.md" 76 | $markdown = New-MDHeader -Text "Included Redistributables" -Level 1 77 | $markdown += "`n" 78 | $line = "VcRedist " + '`' + "${{steps.update-version.outputs.newversion}}" + '`' + " includes the following Redistributables (supported and unsupported):" 79 | $markdown += $line 80 | $markdown += "`n`n" 81 | $markdown += $VcRedists | New-MDTable 82 | ($markdown.TrimEnd("`n")) | Out-File -FilePath $OutFile -Force -Encoding "Utf8" 83 | 84 | # Import GPG key so that we can sign the commit 85 | - name: Import GPG key 86 | id: import_gpg 87 | uses: crazy-max/ghaction-import-gpg@v6 88 | with: 89 | gpg_private_key: ${{ secrets.GPGKEY }} 90 | passphrase: ${{ secrets.GPGPASSPHRASE }} 91 | git_user_signingkey: true 92 | git_commit_gpgsign: true 93 | git_config_global: true 94 | git_tag_gpgsign: true 95 | git_push_gpgsign: false 96 | git_committer_name: ${{ secrets.COMMIT_NAME }} 97 | git_committer_email: ${{ secrets.COMMIT_EMAIL }} 98 | 99 | - name: Commit changes 100 | id: commit 101 | uses: stefanzweifel/git-auto-commit-action@v7 102 | with: 103 | commit_message: "Update module ${{steps.update-version.outputs.newversion}}" 104 | commit_user_name: ${{ secrets.COMMIT_NAME }} 105 | commit_user_email: ${{ secrets.COMMIT_EMAIL }} 106 | 107 | - name: "Run if changes have been detected" 108 | if: steps.commit.outputs.changes_detected == 'true' 109 | run: echo "Changes committed." 110 | 111 | - name: "Run if no changes have been detected" 112 | if: steps.commit.outputs.changes_detected == 'false' 113 | run: echo "No changes detected." 114 | 115 | # tag-repo: 116 | # needs: update-module 117 | # if: needs.update-module.outputs.output1 == 'true' 118 | # runs-on: ubuntu-latest 119 | # steps: 120 | # - uses: actions/checkout@v6 121 | # with: 122 | # token: ${{ secrets.PAT }} 123 | # repository: aaronparker/vcredist 124 | 125 | # - name: Git pull 126 | # id: pull 127 | # shell: pwsh 128 | # run: | 129 | # git pull origin main 130 | 131 | # - name: Get module version number 132 | # id: get-version 133 | # shell: pwsh 134 | # run: | 135 | # $manifestPath = "${{ github.workspace }}/VcRedist/VcRedist.psd1" 136 | # $manifest = Test-ModuleManifest -Path $manifestPath 137 | # Write-Host "Found version: $($manifest.Version)" 138 | # echo "::set-output name=version::$($manifest.Version)" 139 | 140 | # # Import GPG key so that we can sign the commit 141 | # - name: Import GPG key 142 | # id: import_gpg 143 | # uses: crazy-max/ghaction-import-gpg@v6 144 | # with: 145 | # gpg_private_key: ${{ secrets.GPGKEY }} 146 | # passphrase: ${{ secrets.GPGPASSPHRASE }} 147 | # git_user_signingkey: true 148 | # git_commit_gpgsign: true 149 | # git_config_global: true 150 | # git_tag_gpgsign: true 151 | # git_push_gpgsign: false 152 | # git_committer_name: ${{ secrets.COMMIT_NAME }} 153 | # git_committer_email: ${{ secrets.COMMIT_EMAIL }} 154 | 155 | # # Push tag 156 | # - name: Push tag 157 | # shell: bash 158 | # run: | 159 | # git tag -a "v${{steps.get-version.outputs.version}}" -m "v${{steps.get-version.outputs.version}}" 160 | # git push origin "v${{steps.get-version.outputs.version}}" 161 | -------------------------------------------------------------------------------- /img/Product-Icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /VcRedist/Public/Import-VcIntuneApplication.ps1: -------------------------------------------------------------------------------- 1 | function Import-VcIntuneApplication { 2 | <# 3 | .EXTERNALHELP VcRedist-help.xml 4 | #> 5 | [CmdletBinding(SupportsShouldProcess = $false, HelpURI = "https://vcredist.com/import-vcintuneapplication/")] 6 | [OutputType([System.String])] 7 | param ( 8 | [Parameter( 9 | Mandatory = $true, 10 | Position = 0, 11 | ValueFromPipeline, 12 | HelpMessage = "Pass a VcList object from Save-VcRedist.")] 13 | [ValidateNotNullOrEmpty()] 14 | [System.Management.Automation.PSObject] $VcList 15 | ) 16 | 17 | begin { 18 | # IntuneWin32App currently supports Windows PowerShell only 19 | if (Test-PSCore) { 20 | $Msg = "We can't load the IntuneWin32App module on PowerShell Core. Please use PowerShell 5.1." 21 | throw [System.TypeLoadException]::New($Msg) 22 | } 23 | 24 | # Test for required variables 25 | $Modules = "IntuneWin32App" 26 | foreach ($Module in $Modules) { 27 | if (Get-Module -Name $Module -ListAvailable -ErrorAction "SilentlyContinue") { 28 | Write-Verbose -Message "Support module installed: $Module." 29 | } 30 | else { 31 | $Msg = "Required module missing: $Module." 32 | throw [System.TypeLoadException]::New($Msg) 33 | } 34 | } 35 | 36 | # Test for authentication token 37 | if ($null -eq $Global:AccessToken) { 38 | $Msg = "Microsoft Graph API access token missing. Authenticate to the Graph API with Connect-MSIntuneGraph." 39 | throw [System.UnauthorizedAccessException]::New($Msg) 40 | } 41 | 42 | # Get the Intune app manifest 43 | $IntuneManifest = Get-Content -Path $(Join-Path -Path $MyInvocation.MyCommand.Module.ModuleBase -ChildPath "Intune.json") | ConvertFrom-Json -ErrorAction "Stop" 44 | Write-Verbose -Message "Loaded Intune app manifest." 45 | 46 | # Create the icon object for the app 47 | $IconPath = [System.IO.Path]::Combine($MyInvocation.MyCommand.Module.ModuleBase, "img", "vcredist.png") 48 | if (Test-Path -Path $IconPath) { 49 | Write-Verbose -Message "Using icon image from path: $IconPath." 50 | $Icon = New-IntuneWin32AppIcon -FilePath $IconPath 51 | } 52 | else { 53 | Write-Error -Message "Unable to find icon image in path: $IconPath." 54 | } 55 | } 56 | 57 | process { 58 | # Make sure that $VcList has the required properties 59 | if ((Test-VcListObject -VcList $VcList) -ne $true) { 60 | $Msg = "Required properties not found. Please ensure the output from Save-VcRedist is sent to this function. " 61 | throw [System.Management.Automation.PropertyNotFoundException]::New($Msg) 62 | } 63 | 64 | foreach ($VcRedist in $VcList) { 65 | 66 | # Check if the package already exists in Intune 67 | $AppDetails = Get-RequiredVcRedistUpdatesFromIntune -VcList $VcRedist 68 | if ($null -eq $AppDetails -or $AppDetails.UpdateRequired -eq $true) { 69 | 70 | # Package MSI as .intunewin file 71 | $OutputFolder = New-TemporaryFolder 72 | $params = @{ 73 | SourceFolder = $(Split-Path -Path $VcRedist.Path -Parent) 74 | SetupFile = $(Split-Path -Path $VcRedist.Path -Leaf) 75 | OutputFolder = $OutputFolder 76 | } 77 | $Package = New-IntuneWin32AppPackage @params 78 | Write-Verbose -Message "Created IntuneWin package: $($Package.Path)." 79 | 80 | # Requirement rule 81 | switch ($VcRedist.Architecture) { 82 | "x86" { $Architecture = "All" } 83 | "x64" { $Architecture = "x64" } 84 | default { 85 | $Architecture = "All" 86 | continue 87 | } 88 | } 89 | $params = @{ 90 | Architecture = $Architecture 91 | MinimumSupportedWindowsRelease = $IntuneManifest.RequirementRule.MinimumRequiredOperatingSystem 92 | MinimumFreeDiskSpaceInMB = $IntuneManifest.RequirementRule.SizeInMBValue 93 | } 94 | $RequirementRule = New-IntuneWin32AppRequirementRule @params 95 | 96 | # Create the detection rules for the Win32 app 97 | $DetectionRules = New-IntuneWin32AppDetectionRule -VcList $VcRedist -IntuneManifest $IntuneManifest 98 | 99 | # Construct a table of default parameters for Win32 app 100 | $DisplayName = "$($IntuneManifest.Information.Publisher) $($VcRedist.Name) $($VcRedist.Version) $($VcRedist.Architecture)" 101 | Write-Verbose -Message "Creating Win32 app for $DisplayName." 102 | 103 | # Create a Notes property with identifying information 104 | $Notes = [PSCustomObject] @{ 105 | "CreatedBy" = "VcRedist" 106 | "Guid" = $VcRedist.PackageId 107 | "Date" = $(Get-Date -Format "yyyy-MM-dd") 108 | } | ConvertTo-Json -Compress 109 | 110 | $Win32AppArgs = @{ 111 | "FilePath" = $Package.Path 112 | "DisplayName" = $DisplayName 113 | "Description" = "$($IntuneManifest.Information.Description). $DisplayName" 114 | "AppVersion" = $VcRedist.Version 115 | "Notes" = $Notes 116 | "Publisher" = $IntuneManifest.Information.Publisher 117 | "InformationURL" = $IntuneManifest.Information.InformationURL 118 | "PrivacyURL" = $IntuneManifest.Information.PrivacyURL 119 | "CompanyPortalFeaturedApp" = $false 120 | "InstallExperience" = $IntuneManifest.Program.InstallExperience 121 | "RestartBehavior" = $IntuneManifest.Program.DeviceRestartBehavior 122 | "DetectionRule" = $DetectionRules 123 | "RequirementRule" = $RequirementRule 124 | "InstallCommandLine" = "$(Split-Path -Path $VcRedist.URI -Leaf) $($VcRedist.SilentInstall)" 125 | "UninstallCommandLine" = $VcRedist.SilentUninstall 126 | } 127 | if ($null -ne $Icon) { 128 | $Win32AppArgs.Add("Icon", $Icon) 129 | } 130 | $Application = Add-IntuneWin32App @Win32AppArgs 131 | if ($null -ne $Application) { 132 | # Exclude the largeIcon property from the output 133 | $Application | Select-Object -Property * -ExcludeProperty "largeIcon" | Write-Output 134 | } 135 | 136 | # Clean up the temporary intunewin package 137 | Write-Verbose -Message "Removing temporary output folder: $OutputFolder." 138 | Remove-Item -Path $OutputFolder -Recurse -Force -ErrorAction "SilentlyContinue" 139 | } 140 | else { 141 | Write-Verbose -Message "No update required for $($VcRedist.Name) $($VcRedist.Version) $($VcRedist.Architecture)." 142 | } 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /VcRedist/Public/Save-VcRedist.ps1: -------------------------------------------------------------------------------- 1 | function Save-VcRedist { 2 | <# 3 | .EXTERNALHELP VcRedist-help.xml 4 | #> 5 | [Alias("Get-VcRedist")] 6 | [CmdletBinding(SupportsShouldProcess = $true, HelpURI = "https://vcredist.com/save-vcredist/")] 7 | [OutputType([System.Management.Automation.PSObject])] 8 | param ( 9 | [Parameter( 10 | Mandatory = $true, 11 | Position = 0, 12 | ValueFromPipeline = $true, 13 | HelpMessage = "Pass a VcList object from Get-VcList.")] 14 | [ValidateNotNullOrEmpty()] 15 | [System.Management.Automation.PSObject] $VcList, 16 | 17 | [Parameter(Mandatory = $false, Position = 1)] 18 | [ValidateScript( { if (Test-Path -Path $_ -PathType "Container") { $true } else { throw "Cannot find path $_" } })] 19 | [System.String] $Path = (Resolve-Path -Path $PWD), 20 | 21 | [Parameter(Mandatory = $false)] 22 | [System.ObsoleteAttribute("This parameter should no longer be used. Invoke-WebRequest is used for all download operations.")] 23 | [System.Management.Automation.SwitchParameter] $ForceWebRequest, 24 | 25 | [Parameter(Mandatory = $false)] 26 | [System.ObsoleteAttribute("This parameter should no longer be used. Invoke-WebRequest is used for all download operations.")] 27 | [ValidateSet("Foreground", "High", "Normal", "Low")] 28 | [System.String] $Priority = "Foreground", 29 | 30 | [Parameter(Mandatory = $false, Position = 3)] 31 | [System.String] $Proxy, 32 | 33 | [Parameter(Mandatory = $false, Position = 4)] 34 | [System.Management.Automation.PSCredential] 35 | $ProxyCredential = [System.Management.Automation.PSCredential]::Empty, 36 | 37 | [Parameter(Mandatory = $false, Position = 5)] 38 | [ValidateNotNullOrEmpty()] 39 | [System.String] $UserAgent = [Microsoft.PowerShell.Commands.PSUserAgent]::Chrome, 40 | 41 | [Parameter(Mandatory = $false)] 42 | [System.Management.Automation.SwitchParameter] $ShowProgress 43 | ) 44 | 45 | begin { 46 | 47 | # Disable the Invoke-WebRequest progress bar for faster downloads 48 | if ($PSBoundParameters.ContainsKey("Verbose") -or ($PSBoundParameters.ContainsKey("ShowProgress"))) { 49 | $ProgressPreference = "Continue" 50 | } 51 | else { 52 | $ProgressPreference = "SilentlyContinue" 53 | } 54 | 55 | # Enable TLS 1.2 56 | [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 57 | } 58 | 59 | process { 60 | foreach ($VcRedist in $VcList) { 61 | # Loop through each Redistributable and download to the target path 62 | 63 | # Build the path to save the VcRedist into; Create the folder to store the downloaded file. Skip if it exists 64 | $TargetDirectory = [System.IO.Path]::Combine((Resolve-Path -Path $Path), $VcRedist.Release, $VcRedist.Version, $VcRedist.Architecture) 65 | Write-Verbose -Message "Test directory exists: $TargetDirectory." 66 | if (Test-Path -Path $TargetDirectory) { 67 | Write-Verbose -Message "Directory exists: $TargetDirectory. Skipping." 68 | } 69 | else { 70 | if ($PSCmdlet.ShouldProcess($TargetDirectory, "Create")) { 71 | $params = @{ 72 | Path = $TargetDirectory 73 | Type = "Directory" 74 | Force = $true 75 | ErrorAction = "Continue" 76 | } 77 | New-Item @params > $null 78 | } 79 | } 80 | 81 | # Test whether the VcRedist is already on disk 82 | $TargetVcRedist = Join-Path -Path $TargetDirectory -ChildPath $(Split-Path -Path $VcRedist.URI -Leaf) 83 | Write-Verbose -Message "Testing for downloaded VcRedist: $($TargetVcRedist)" 84 | 85 | if (Test-Path -Path $TargetVcRedist -PathType "Leaf") { 86 | $ProductVersion = $(Get-Item -Path $TargetVcRedist).VersionInfo.ProductVersion 87 | 88 | # If the target Redistributable is already downloaded, compare the version 89 | if (([System.Version]$VcRedist.Version -gt [System.Version]$ProductVersion) -or ($null -eq [System.Version]$ProductVersion)) { 90 | 91 | # Download the newer version 92 | Write-Verbose -Message "Manifest version: '$($VcRedist.Version)' > file version: '$ProductVersion'." 93 | $DownloadRequired = $true 94 | } 95 | else { 96 | Write-Verbose -Message "Manifest version: '$($VcRedist.Version)' matches file version: '$ProductVersion'." 97 | $DownloadRequired = $false 98 | } 99 | } 100 | else { 101 | $DownloadRequired = $true 102 | } 103 | 104 | # The VcRedist needs to be downloaded 105 | if ($DownloadRequired -eq $true) { 106 | if ($PSCmdlet.ShouldProcess($VcRedist.URI, "Invoke-WebRequest")) { 107 | 108 | try { 109 | # Download the file 110 | Write-Verbose -Message "Download VcRedist: '$($VcRedist.Name) $($VcRedist.Version) $($VcRedist.Architecture)'" 111 | $iwrParams = @{ 112 | Uri = $VcRedist.URI 113 | OutFile = $TargetVcRedist 114 | UseBasicParsing = $true 115 | UserAgent = $UserAgent 116 | ErrorAction = "SilentlyContinue" 117 | } 118 | if ($PSBoundParameters.ContainsKey("Proxy")) { 119 | $iwrParams.Proxy = $Proxy 120 | } 121 | if ($PSBoundParameters.ContainsKey("ProxyCredential")) { 122 | $iwrParams.ProxyCredential = $ProxyCredential 123 | } 124 | Invoke-WebRequest @iwrParams 125 | $Downloaded = $true 126 | } 127 | catch [System.Exception] { 128 | $Downloaded = $false 129 | throw $_ 130 | } 131 | 132 | # Return the $VcList array on the pipeline so that we can act on what was downloaded 133 | # Add the Path property pointing to the downloaded file 134 | if ($Downloaded -eq $true) { 135 | Write-Verbose -Message "Add Path property: $TargetVcRedist" 136 | $VcRedist | Add-Member -MemberType "NoteProperty" -Name "Path" -Value $TargetVcRedist 137 | Write-Output -InputObject $VcRedist 138 | } 139 | } 140 | } 141 | else { 142 | # Return the $VcList array on the pipeline so that we can act on what was downloaded 143 | # Add the Path property pointing to the downloaded file 144 | Write-Verbose -Message "VcRedist exists: $TargetVcRedist." 145 | Write-Verbose -Message "Add Path property: $TargetVcRedist" 146 | $VcRedist | Add-Member -MemberType "NoteProperty" -Name "Path" -Value $TargetVcRedist 147 | Write-Output -InputObject $VcRedist 148 | } 149 | } 150 | } 151 | 152 | end { } 153 | } 154 | --------------------------------------------------------------------------------