How to grant permission to private key from powershell - powershell

I'm trying to find a way to grant permissions for private key from powershell script. Certificate is stored in CNG. All ideas are welcome.

The answer above is technically correct however it did not help me when I was looking for the same thing because it fails to mention that you need to use assemblies loaded from the CLRSecurity project on codeplex https://clrsecurity.codeplex.com/.
Here is an extract of how I achieved the same thing including loading the CLR Security assembly that you need to use Security.Cryptography.dll. There are a couple of function declarations that are needed first. I have these included in modules however you can use them as you wish.
Function Load-Assembly()
{
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory)][string][ValidateScript({Test-Path $_})] $DirectoryPath,
[Parameter(Mandatory)][string][ValidateNotNullOrEmpty()] $Name
)
$assemblyFileNameFullPath = Join-Path -Path $DirectoryPath -ChildPath $Name
If (Test-Path -Path $assemblyFileNameFullPath -PathType Leaf)
{
Write-Verbose "Loading .NET assembly from path ""$assemblyFileNameFullPath"""
#Load the assembly using the bytes as this gets around security restrictions that stop certain assemblies from loading from external sources
$assemblyBytes = [System.IO.File]::ReadAllBytes($assemblyFileNameFullPath)
$assemblyLoaded = [System.Reflection.Assembly]::Load($assemblyBytes);
if ($assemblyLoaded -ne $null)
{
return $assemblyLoaded
}
else
{
Throw "Cannot load .NET assembly ""$Name"" from directory ""$DirectoryPath"""
}
}
else
{
Write-Error "Cannot find required .NET assembly at path ""$assemblyFileNameFullPath"""
}
}
Function Get-PrivateKeyContainerPath()
{
[CmdletBinding(PositionalBinding=$false)]
Param(
[Parameter(Mandatory=$True)][string][ValidateNotNullOrEmpty()] $Name,
[Parameter(Mandatory=$True)][boolean] $IsCNG
)
If ($IsCNG)
{
$searchDirectories = #("Microsoft\Crypto\Keys","Microsoft\Crypto\SystemKeys")
}
else
{
$searchDirectories = #("Microsoft\Crypto\RSA\MachineKeys","Microsoft\Crypto\RSA\S-1-5-18","Microsoft\Crypto\RSA\S-1-5-19","Crypto\DSS\S-1-5-20")
}
foreach ($searchDirectory in $searchDirectories)
{
$machineKeyDirectory = Join-Path -Path $([Environment]::GetFolderPath("CommonApplicationData")) -ChildPath $searchDirectory
$privateKeyFile = Get-ChildItem -Path $machineKeyDirectory -Filter $Name -Recurse
if ($privateKeyFile -ne $null)
{
return $privateKeyFile.FullName
}
}
Throw "Cannot find private key file path for key container ""$Name"""
}
#Extracted code of how to obtain the private key file path (taken from a function)
#Requires an x509Certificate2 object in variable $Certificate and string variable $CertificateStore that contains the name of the certificate store
#Need to use the Security.Cryptography assembly
$assembly = Load-Assembly -DirectoryPath $PSScriptRoot -Name Security.Cryptography.dll
#Uses the extension methods in Security.Cryptography assembly from (https://clrsecurity.codeplex.com/)
If ([Security.Cryptography.X509Certificates.X509CertificateExtensionMethods]::HasCngKey($Certificate))
{
Write-Verbose "Private key CSP is CNG"
$privateKey = [Security.Cryptography.X509Certificates.X509Certificate2ExtensionMethods]::GetCngPrivateKey($Certificate)
$keyContainerName = $privateKey.UniqueName
$privateKeyPath = Get-PrivateKeyContainerPath -Name $keyContainerName -IsCNG $true
}
elseif ($Certificate.PrivateKey -ne $null)
{
Write-Verbose "Private key CSP is legacy"
$privateKey = $Certificate.PrivateKey
$keyContainerName = $Certificate.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName
$privateKeyPath = Get-PrivateKeyContainerPath -Name $keyContainerName -IsCNG $false
}
else
{
Throw "Certificate ""$($Certificate.GetNameInfo("SimpleName",$false))"" in store ""$CertificateStore"" does not have a private key, or that key is inaccessible, therefore permission cannot be granted"
}
Sorry if this seems like a repeat from above, as I said it does use the same technique but hopefully others may find this more useful since it explains how to use the methods in the CLR Security project including how to load the assembly.

Cmdlet code for getting private key filename.
[Cmdlet("Get", "PrivateKeyName")]
public class GetKeyNameCmdlet : Cmdlet
{
[Parameter(Position = 0, Mandatory = false)]
public X509Certificate2 Cert;
protected override void ProcessRecord()
{
WriteObject(GetUniqueKeyName(Cert));
}
private static string GetUniqueKeyName(X509Certificate2 cert)
{
if (cert == null)
throw new ArgumentNullException("cert");
var cngPrivateKey = cert.GetCngPrivateKey();
if (cngPrivateKey != null)
return cngPrivateKey.UniqueName;
var rsaPrivateKey = cert.PrivateKey as RSACryptoServiceProvider;
if (rsaPrivateKey != null)
return rsaPrivateKey.CspKeyContainerInfo.UniqueKeyContainerName;
throw new Exception("cert");
}
}
using cmdlet. CngCrypt.dll - dll with cmdlet code.
Import-Module .\CngCrypt.dll
$local:certificateRootPath = join-path $env:ALLUSERSPROFILE '\Microsoft\Crypto\RSA\MachineKeys\'
$WorkingCert = Get-ChildItem CERT:\LocalMachine\My |where {$_.Subject -match 'Test'}| sort
Get-PrivateKeyName ($WorkingCert)

If you have certificate already installed on machine/server and just looking for how to give permission to specific user using powershell.
Here is the answer
How to Grant permission to user on Certificate private key using powershell?

Related

Powershell Global Variable usage as parameter to argument

$global:af_fp = "C:\Path\to\folder\"
Function function-name {
do things …
$global:af_fp = $global:af_fp + $variableFromDo_things + "_AF.csv"
}
function-name | ConvertTo-CSV -NoTypeInformation | Add-Content -Path $($af_fp)
Above is the generalized (and abbreviated) script contents for a powershell script.
Every time I run the script in this way, I get the following error:
Add-Content : Could not find a part of the path 'C:\Users\timeuser\Documents\'.
At C:\Users\timeuser\Documents\get_software.ps1:231 char:51
+ ... ware | ConvertTo-CSV -NoTypeInformation | Add-Content -Path $($af_fp)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (C:\Users\timeuser\Documents\:String) [Add-Content], DirectoryNotFoundException
+ FullyQualifiedErrorId : GetContentWriterDirectoryNotFoundError,Microsoft.PowerShell.Commands.AddContentCommand
When I run
Get-Variable -Scope global
after running the script and seeing the error, the variable af_fp contains exactly the information I am seeking for the file name, however, the error shows the variable contents ending in ':String'.
To confuse me even more, if I comment out the lines containing '$global:...' and re-run the same script, IT ACTUALL RUNS AND SAVES THE FILE USING THE LINE
function-name | ConvertTo-CSV -NoTypeInformation | Add-Content -Path $($af_fp)
AS INTENDED. Of course, I had to run the script and watch it error first, then re-run the script with the global variable declaration and update commented out for it to actually work. I want to run the script ONCE and still get the same results.
FYI, I am a complete noob to powershell, but very familiar with the concept of variable scope.....but why is this global not working when initially created and updated, but then work the second time around, when, as far as I can tell, the CONTENT AND SCOPE of the global remains the same...…. any assistance to finding a solution to this small issue would be greatly appreciated; I have tried sooooo may different methods from inquiries through here and on Google...…..
EDIT: not sure why this will matter, because the script ran before as intended when I explicitly typed the parameter for -Path as 'C:\path\to\file'. The ONLY CHANGES MADE to the original, working script (below) were my inclusion of the global variable declaration, the update to the contents of the global variable (near the end of the function), and the attempt to use the global variable as the parameter to -Path, that is why I omitted the script:
'''
$global:af_fp = "C:\Users\timeuser\Documents\"
Function Get-Software {
[OutputType('System.Software.Inventory')]
[Cmdletbinding()]
Param(
[Parameter(ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
[String[]]$Computername = $env:COMPUTERNAME
)
Begin {
}
Process {
ForEach ($Computer in $Computername) {
If (Test-Connection -ComputerName $Computer -Count 1 -Quiet) {
$Paths = #("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", "SOFTWARE\\Wow6432node\\Microsoft\\Windows\\CurrentVersion\\Uninstall")
ForEach ($Path in $Paths) {
Write-Verbose "Checking Path: $Path"
# Create an instance of the Registry Object and open the HKLM base key
Try {
$reg = [microsoft.win32.registrykey]::OpenRemoteBaseKey('LocalMachine', $Computer, 'Registry64')
}
Catch {
Write-Error $_
Continue
}
# Drill down into the Uninstall key using the OpenSubKey Method
Try {
$regkey = $reg.OpenSubKey($Path)
# Retrieve an array of string that contain all the subkey names
$subkeys = $regkey.GetSubKeyNames()
# Open each Subkey and use GetValue Method to return the required values for each
ForEach ($key in $subkeys) {
Write-Verbose "Key: $Key"
$thisKey = $Path + "\\" + $key
Try {
$thisSubKey = $reg.OpenSubKey($thisKey)
# Prevent Objects with empty DisplayName
$DisplayName = $thisSubKey.getValue("DisplayName")
If ($DisplayName -AND $DisplayName -notmatch '^Update for|rollup|^Security Update|^Service Pack|^HotFix') {
$Date = $thisSubKey.GetValue('InstallDate')
If ($Date) {
Try {
$Date = [datetime]::ParseExact($Date, 'yyyyMMdd', $Null)
}
Catch {
Write-Warning "$($Computer): $_ <$($Date)>"
$Date = $Null
}
}
# Create New Object with empty Properties
$Publisher = Try {
$thisSubKey.GetValue('Publisher').Trim()
}
Catch {
$thisSubKey.GetValue('Publisher')
}
$Version = Try {
#Some weirdness with trailing [char]0 on some strings
$thisSubKey.GetValue('DisplayVersion').TrimEnd(([char[]](32, 0)))
}
Catch {
$thisSubKey.GetValue('DisplayVersion')
}
$UninstallString = Try {
$thisSubKey.GetValue('UninstallString').Trim()
}
Catch {
$thisSubKey.GetValue('UninstallString')
}
$InstallLocation = Try {
$thisSubKey.GetValue('InstallLocation').Trim()
}
Catch {
$thisSubKey.GetValue('InstallLocation')
}
$InstallSource = Try {
$thisSubKey.GetValue('InstallSource').Trim()
}
Catch {
$thisSubKey.GetValue('InstallSource')
}
$HelpLink = Try {
$thisSubKey.GetValue('HelpLink').Trim()
}
Catch {
$thisSubKey.GetValue('HelpLink')
}
$Object = [pscustomobject]#{
#Potential Candidate for AssetID in the TIME system
AssetID = $Computer
#String that contains word or word combinations for the product field of CPE WFN; may also contain the valid values necessary for update, edition, language, sw_edition, target_hw/sw fields as well.
cpeprodinfo = $DisplayName
cpeversion = $Version
InstallDate = $Date
cpevendor = $Publisher
UninstallString = $UninstallString
InstallLocation = $InstallLocation
InstallSource = $InstallSource
HelpLink = $thisSubKey.GetValue('HelpLink')
EstimatedSizeMB = [decimal]([math]::Round(($thisSubKey.GetValue('EstimatedSize') * 1024) / 1MB, 2))
}
$Object.pstypenames.insert(0, 'System.Software.Inventory')
Write-Output $Object
}
}
Catch {
Write-Warning "$Key : $_"
}
}
}
Catch { }
$reg.Close()
}
}
Else {
Write-Error "$($Computer): unable to reach remote system!"
}
$global:af_fp = $global:af_fp + $Computer + "_AF.csv"
}
}
}
Get-Software | ConvertTo-CSV -NoTypeInformation | Add-Content -Path $($af_fp)
'''
IGNORE FORMATTING PLEASE- HAD TROUBLE MAKING INDENTS CORRECTLY FROM COPY-PASTE AND RESTRICTIONS ON SITE FOR CODE BLOCKS.....
NOTE: the ONLY changes I made, that I am asking about, are the global declaration, the global variable update in the function, and the attempt to use the global variable for the -Path parameter....script otherwise runs and will even run WITH THE LAST LINE AS IS if I ran it and errored the first time.....not sure how the addition script will help in any way, shape, or form!
With a little effort, Nasir's solution worked! HOWEVER, I ran across a sample file that had a way of adding to a parameter that inspired me to make a change to my ORIGINAL, that also worked: remove global variable from script entirely and add this code the very end:
$file_suffix = '_AF.csv'
Get-Software | ConvertTo-CSV -NoTypeInformation | Add-Content -Path $env:COMPUTERNAME$file_suffix
In this way, I was able to accomplish exactly what I was setting out to do! Thanks Nasir for your response as well! I was able to also make that work as intended!
Global variables are generally frowned upon, since they often lead to poor scripts, with hard to debug issues.
It seems like your function returns some stuff, which you need to write to a file, the name of which is also generated by the same function. You can try something like this:
function function-name {
param($PathPrefix)
#do things
[pscustomobject]#{"DoThings_data" = $somevariablefromDoThings; "Filename" = "$($PathPrefix)$($variableFromDo_Things)_AF.csv"}
}
function-name -PathPrefix "C:\Path\to\folder\" | Foreach-Object { $_.DoThings_data | Export-Csv -Path $_.Filename -NoTypeInformation }
Or just have your function write the CSV data out and then return the data if you need to further process it outside the function.
Edit: this is just me extrapolating from partial code you have provided. To Lee_Dailey's point, yes, please provide more details.

How to get multiple Module configs auomatically to different folder using PowerShell?

I have Multiple folders, for Example, www contains Folders A, B, C and So on and this same structure exist in a different environment like INT, REG, DR, PROD. each contains different web.config.
So Here comes the challenge we need to provide an automated way to check-in each web.config.
Named like web_A_int.config, web_A_REG.config , Web_A_Prod.config, Web_A_DR.config and Web_B_int.config , Web_B_Reg_config and so on .
The environment is TFS (2015) Source Control and solution is .net based solution.
Please let me know if anything you might need to understand the question.
How to maintain this structure post build compilation. or How we can use proj or Powershell script to achieve it?
Desired Output:
INT--> www--> A, B,C Folders and each web.config placed inside respective folder.
REG--> www--> A, B, C --> with each web.config and each module level placed correctly.
One way to solve this is to use web transforms and create a web.{environment}.config file and then apply that transformation on deployment.
The Microsoft docs should get you started on how to use web transforms. I can provide you with some Powershell code on how to do this if that's how you decide to go.
I'm not familiar web server deployment in TFS, but here are some regular powershell functions you could use to build loops:
$environmentTypes = #('INT', 'REG', 'DR', 'PROD')
Function Get-WebConfig([String]$Path)
{
try {
$configContent = Get-Content \\path\to\config\file
}
catch {
Write-Debug "ERROR: Could not get content of $($Path)"
}
return $configContent
}
Function Create-FolderStructure() {
[cmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidateSet('INT','REG','DR','PROD')]
[String]$EnvironmentType,
[Parameter(Mandatory)]
[String]$Server,
[Parameter(Mandatory)]
[String[]]$FolderNames,
[Parameter(Mandatory=$false)]
[Switch]$CheckServerUp
)
Begin {}
Process {
try {
if($CheckServerUp)
{
try {
Test-Connection -ComputerName $Server
}
catch {
Write-Debug "ERROR: Unable to test connection to $Server"
}
}
foreach($item in $FolderNames)
{
try {
New-Item -Path "\\$Server\c$\www\$item" -ItemType Directory
}
catch {
Write-Debug "ERROR: Unable to create folder $item"
}
try {
switch($EnvironmentType)
{
'INT'
{
$neededConfig = Get-WebConfig -Path \\path\to\your\intconfig
break
}
'REG'
{
$neededConfig = Get-WebConfig -Path \\path\to\your\regconfig
break
}
'DR'
{
$neededConfig = Get-WebConfig -Path \\path\to\your\drconfig
break
}
'PROD'
{
$neededConfig = Get-WebConfig -Path \\path\to\your\prodconfig
break
}
try {
New-Item -Path "\\$Server\c$\www\$item\Web_$EnvironmentType.config"
Set-Content -Path "\\$Server\c$\www\$item\Web_$EnvironmentType.config" -Value $neededConfig
}
catch {
Write-Debug "ERROR: Unable to create file"
}
}
}
catch {
Write-Debug "ERROR: Unable to get web config content"
}
}
}
catch {
Write-Debug "ERROR: Could not get content of $($Path)"
}
}
End{}
}

Fast Registry Searcher in Powershell

I'm trying to incorporate this search-registry script that I found on Github into one of my scripts.
https://github.com/KurtDeGreeff/PlayPowershell/blob/master/Search-Registry.ps1
To test it, I used one of the examples that it provided:
Search-Registry -StartKey HKLM -Pattern $ENV:USERNAME -MatchData
After executing, I was still prompted to enter a StartKey and a Pattern into the console. After providing that information once again, the command fails.
cmdlet Search-Registry.ps1 at command pipeline position 1
Supply values for the following parameters:
StartKey: HKLM
Pattern: $ENV:USERNAME -MatchData
You must specify at least one of: -MatchKey -MatchValue -MatchData
At C:\Users\Cole\Desktop\Powershell\Search-Registry.ps1:93 char:5
+ throw "You must specify at least one of: -MatchKey -MatchValue -M ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (You must specif...alue -MatchData:String) [], RuntimeException
+ FullyQualifiedErrorId : You must specify at least one of: -MatchKey -MatchValue -MatchData
Is there something that I'm doing incorrectly?
I have tested that same function on GitHub too and also came across errors with it, so I decided to do a complete rewrite myself.
The function below can use either Regular Expression matches or Wildcard comparisons using the -like operator.
To search for nameless default properties, either don't specify the Pattern or RegexPattern parameters at all, or use one of them with an empty string.
The function also has a Recurse switch, so it is up to you if you want to recursively search through all subkeys or not.
Note that although the function performs fast, searching the registry can take a long time to finish..
# If you want to run this on PowerShell < 3.0 use
# New-Object -TypeName PSObject -Property #{ ... } wherever it says [PSCustomObject]#{ ... }
# and change the -version value for 'requires' to 2
#requires -version 3
function Search-Registry {
<#
.SYNOPSIS
Searches the registry on one or more computers for a specified text pattern.
.DESCRIPTION
Searches the registry on one or more computers for a specified text pattern.
Supports searching for any combination of key names, value names, and/or value data.
The search pattern is either a regular expression or a wildcard pattern using the 'like' operator.
(both are case-insensitive)
.PARAMETER ComputerName
(Required) Searches the registry on the specified computer(s). This parameter supports piped input.
.PARAMETER Pattern
(Optional) Searches using a wildcard pattern and the -like operator.
Mutually exclusive with parameter 'RegexPattern'
.PARAMETER RegexPattern
(Optional) Searches using a regular expression pattern.
Mutually exclusive with parameter 'Pattern'
.PARAMETER Hive
(Optional) The registry hive rootname.
Can be any of 'HKEY_CLASSES_ROOT','HKEY_CURRENT_CONFIG','HKEY_CURRENT_USER','HKEY_DYN_DATA','HKEY_LOCAL_MACHINE',
'HKEY_PERFORMANCE_DATA','HKEY_USERS','HKCR','HKCC','HKCU','HKDD','HKLM','HKPD','HKU'
If not specified, the hive must be part of the 'KeyPath' parameter.
.PARAMETER KeyPath
(Optional) Starts the search at the specified registry key. The key name contains only the subkey.
This parameter can be prefixed with the hive name.
In that case, parameter 'Hive' is ignored as it is then taken from the given path.
Examples:
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Uninstall
HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall
Software\Microsoft\Windows\CurrentVersion\Uninstall
.PARAMETER MaximumResults
(Optional) Specifies the maximum number of results per computer searched.
A value <= 0 means will return the maximum number of possible matches (2147483647).
.PARAMETER SearchKeyName
(Optional) Searches for registry key names. You must specify at least one of -SearchKeyName, -SearchPropertyName, or -SearchPropertyValue.
.PARAMETER SearchPropertyName
(Optional) Searches for registry value names. You must specify at least one of -SearchKeyName, -SearchPropertyName, or -SearchPropertyValue.
.PARAMETER SearchPropertyValue
(Optional) Searches for registry value data. You must specify at least one of -SearchKeyName, -SearchPropertyName, or -SearchPropertyValue.
.PARAMETER Recurse
(Optional) If set, the function will recurse the search through all subkeys found.
.OUTPUTS
PSCustomObjects with the following properties:
ComputerName The computer name where the search was executed
Hive The hive name used in Win32 format ("CurrentUser", "LocalMachine" etc)
HiveName The hive name used ("HKEY_CURRENT_USER", "HKEY_LOCAL_MACHINE" etc.)
HiveShortName The abbreviated hive name used ("HKCU", "HKLM" etc.)
Path The full registry path ("HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Uninstall")
SubKey The subkey without the hive ("Software\Microsoft\Windows\CurrentVersion\Uninstall")
ItemType Informational: describes the type 'RegistryKey' or 'RegistryProperty'
DataType The .REG formatted datatype ("REG_SZ", "REG_EXPAND_SZ", "REG_DWORD" etc.). $null for ItemType 'RegistryKey'
ValueKind The Win32 datatype ("String", "ExpandString", "DWord" etc.). $null for ItemType 'RegistryKey'
PropertyName The name of the property. $null for ItemType 'RegistryKey'
PropertyValue The value of the registry property. $null for ItemType 'RegistryKey'
PropertyValueRaw The raw, unexpanded value of the registry property. $null for ItemType 'RegistryKey'
The difference between 'PropertyValue' and 'PropertyValueRaw' is that in 'PropertyValue' Environment names are expanded
('%SystemRoot%' in the data gets expanded to 'C:\Windows'), whereas in 'PropertyValueRaw' the data is returned as-is.
(Environment names return as '%SystemRoot%')
.EXAMPLE
Search-Registry -Hive HKLM -KeyPath SOFTWARE -Pattern $env:USERNAME -SearchPropertyValue -Recurse -Verbose
Searches HKEY_LOCAL_MACHINE on the local computer for registry values whose data contains the current user's name.
Searches like this can take a long time and you may see warning messages on registry keys you are not allowed to enter.
.EXAMPLE
Search-Registry -KeyPath 'HKEY_CURRENT_USER\Printers\Settings' -Pattern * -SearchPropertyName | Export-Csv -Path 'D:\printers.csv' -NoTypeInformation
or
Search-Registry -Hive HKEY_CURRENT_USER -KeyPath 'Printers\Settings' -Pattern * -SearchPropertyName | Export-Csv -Path 'D:\printers.csv' -NoTypeInformation
Searches HKEY_CURRENT_USER (HKCU) on the local computer for printer names and outputs it as a CSV file.
.EXAMPLE
Search-Registry -KeyPath 'HKLM:\SOFTWARE\Classes\Installer' -Pattern LastUsedSource -SearchPropertyName -Recurse
or
Search-Registry -Hive HKLM -KeyPath 'SOFTWARE\Classes\Installer' -Pattern LastUsedSource -SearchPropertyName -Recurse
Outputs the LastUsedSource registry entries on the current computer.
.EXAMPLE
Search-Registry -KeyPath 'HKCR\.odt' -RegexPattern '.*' -SearchKeyName -MaximumResults 10 -Verbose
or
Search-Registry -Hive HKCR -KeyPath '.odt' -RegexPattern '.*' -SearchKeyName -MaximumResults 10 -Verbose
Outputs at most ten matches if the specified key exists.
This command returns a result if the current computer has a program registered to open files with the .odt extension.
The pattern '.*' means match everything.
.EXAMPLE
Get-Content Computers.txt | Search-Registry -KeyPath "HKLM:\SOFTWARE\Microsoft\PowerShell\3\PowerShellEngine" -Pattern '*' -SearchPropertyName | Export-Csv -Path 'D:\powershell.csv' -NoTypeInformation
Searches for any property name in the registry on each computer listed in the file Computers.txt starting at the specified subkey.
Output is sent to the specified CSV file.
.EXAMPLE
Search-Registry -KeyPath 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Desktop\NameSpace' -SearchPropertyName -Recurse -Verbose
Searches for the default (nameless) properties in the specified registry key.
#>
[CmdletBinding(DefaultParameterSetName = 'ByWildCard')]
Param(
[Parameter(ValueFromPipeline = $true, Mandatory = $false, Position = 0)]
[string[]]$ComputerName = $env:COMPUTERNAME,
[Parameter(Mandatory = $false, ParameterSetName = 'ByRegex')]
[string]$RegexPattern,
[Parameter(Mandatory = $false, ParameterSetName = 'ByWildCard')]
[string]$Pattern,
[Parameter(Mandatory = $false)]
[ValidateSet('HKEY_CLASSES_ROOT','HKEY_CURRENT_CONFIG','HKEY_CURRENT_USER','HKEY_DYN_DATA','HKEY_LOCAL_MACHINE',
'HKEY_PERFORMANCE_DATA','HKEY_USERS','HKCR','HKCC','HKCU','HKDD','HKLM','HKPD','HKU')]
[string]$Hive,
[string]$KeyPath,
[int32] $MaximumResults = [int32]::MaxValue,
[switch]$SearchKeyName,
[switch]$SearchPropertyName,
[switch]$SearchPropertyValue,
[switch]$Recurse
)
Begin {
# detect if the function is called using the pipeline or not
# see: https://communary.net/2015/01/12/quick-tip-determine-if-input-comes-from-the-pipeline-or-not/
# and: https://www.petri.com/unraveling-mystery-myinvocation
[bool]$isPipeLine = $MyInvocation.ExpectingInput
# sanitize given parameters
if ([string]::IsNullOrWhiteSpace($ComputerName) -or $ComputerName -eq '.') { $ComputerName = $env:COMPUTERNAME }
# parse the give KeyPath
if ($KeyPath -match '^(HK(?:CR|CU|LM|U|PD|CC|DD)|HKEY_[A-Z_]+)[:\\]?') {
$Hive = $matches[1]
# remove HKLM, HKEY_CURRENT_USER etc. from the path
$KeyPath = $KeyPath.Split("\", 2)[1]
}
switch($Hive) {
{ #('HKCC', 'HKEY_CURRENT_CONFIG') -contains $_ } { $objHive = [Microsoft.Win32.RegistryHive]::CurrentConfig; break }
{ #('HKCR', 'HKEY_CLASSES_ROOT') -contains $_ } { $objHive = [Microsoft.Win32.RegistryHive]::ClassesRoot; break }
{ #('HKCU', 'HKEY_CURRENT_USER') -contains $_ } { $objHive = [Microsoft.Win32.RegistryHive]::CurrentUser; break }
{ #('HKDD', 'HKEY_DYN_DATA') -contains $_ } { $objHive = [Microsoft.Win32.RegistryHive]::DynData; break }
{ #('HKLM', 'HKEY_LOCAL_MACHINE') -contains $_ } { $objHive = [Microsoft.Win32.RegistryHive]::LocalMachine; break }
{ #('HKPD', 'HKEY_PERFORMANCE_DATA') -contains $_ } { $objHive = [Microsoft.Win32.RegistryHive]::PerformanceData; break }
{ #('HKU', 'HKEY_USERS') -contains $_ } { $objHive = [Microsoft.Win32.RegistryHive]::Users; break }
}
# critical: Hive could not be determined
if (!$objHive) {
Throw "Parameter 'Hive' not specified or could not be parsed from the 'KeyPath' parameter."
}
# critical: no search criteria given
if (-not ($SearchKeyName -or $SearchPropertyName -or $SearchPropertyValue)) {
Throw "You must specify at least one of these parameters: 'SearchKeyName', 'SearchPropertyName' or 'SearchPropertyValue'"
}
# no patterns given will only work for SearchPropertyName and SearchPropertyValue
if ([string]::IsNullOrEmpty($RegexPattern) -and [string]::IsNullOrEmpty($Pattern)) {
if ($SearchKeyName) {
Write-Warning "Both parameters 'RegexPattern' and 'Pattern' are emtpy strings. Searching for KeyNames will not yield results."
}
}
# create two variables for output purposes
switch ($objHive.ToString()) {
'CurrentConfig' { $hiveShort = 'HKCC'; $hiveName = 'HKEY_CURRENT_CONFIG' }
'ClassesRoot' { $hiveShort = 'HKCR'; $hiveName = 'HKEY_CLASSES_ROOT' }
'CurrentUser' { $hiveShort = 'HKCU'; $hiveName = 'HKEY_CURRENT_USER' }
'DynData' { $hiveShort = 'HKDD'; $hiveName = 'HKEY_DYN_DATA' }
'LocalMachine' { $hiveShort = 'HKLM'; $hiveName = 'HKEY_LOCAL_MACHINE' }
'PerformanceData' { $hiveShort = 'HKPD'; $hiveName = 'HKEY_PERFORMANCE_DATA' }
'Users' { $hiveShort = 'HKU' ; $hiveName = 'HKEY_USERS' }
}
if ($MaximumResults -le 0) { $MaximumResults = [int32]::MaxValue }
$script:resultCount = 0
[bool]$useRegEx = ($PSCmdlet.ParameterSetName -eq 'ByRegex')
# -------------------------------------------------------------------------------------
# Nested helper function to (recursively) search the registry
# -------------------------------------------------------------------------------------
function _RegSearch([Microsoft.Win32.RegistryKey]$objRootKey, [string]$regPath, [string]$computer) {
try {
if ([string]::IsNullOrWhiteSpace($regPath)) {
$objSubKey = $objRootKey
}
else {
$regPath = $regPath.TrimStart("\")
$objSubKey = $objRootKey.OpenSubKey($regPath, $false) # $false --> ReadOnly
}
}
catch {
Write-Warning ("Error opening $($objRootKey.Name)\$regPath" + "`r`n " + $_.Exception.Message)
return
}
$subKeys = $objSubKey.GetSubKeyNames()
# Search for Keyname
if ($SearchKeyName) {
foreach ($keyName in $subKeys) {
if ($script:resultCount -lt $MaximumResults) {
if ($useRegEx) { $isMatch = ($keyName -match $RegexPattern) }
else { $isMatch = ($keyName -like $Pattern) }
if ($isMatch) {
# for PowerShell < 3.0 use: New-Object -TypeName PSObject -Property #{ ... }
[PSCustomObject]#{
'ComputerName' = $computer
'Hive' = $objHive.ToString()
'HiveName' = $hiveName
'HiveShortName' = $hiveShort
'Path' = $objSubKey.Name
'SubKey' = "$regPath\$keyName".TrimStart("\")
'ItemType' = 'RegistryKey'
'DataType' = $null
'ValueKind' = $null
'PropertyName' = $null
'PropertyValue' = $null
'PropertyValueRaw' = $null
}
$script:resultCount++
}
}
}
}
# search for PropertyName and/or PropertyValue
if ($SearchPropertyName -or $SearchPropertyValue) {
foreach ($name in $objSubKey.GetValueNames()) {
if ($script:resultCount -lt $MaximumResults) {
$data = $objSubKey.GetValue($name)
$raw = $objSubKey.GetValue($name, '', [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
if ($SearchPropertyName) {
if ($useRegEx) { $isMatch = ($name -match $RegexPattern) }
else { $isMatch = ($name -like $Pattern) }
}
else {
if ($useRegEx) { $isMatch = ($data -match $RegexPattern -or $raw -match $RegexPattern) }
else { $isMatch = ($data -like $Pattern -or $raw -like $Pattern) }
}
if ($isMatch) {
$kind = $objSubKey.GetValueKind($name).ToString()
switch ($kind) {
'Binary' { $dataType = 'REG_BINARY'; break }
'DWord' { $dataType = 'REG_DWORD'; break }
'ExpandString' { $dataType = 'REG_EXPAND_SZ'; break }
'MultiString' { $dataType = 'REG_MULTI_SZ'; break }
'QWord' { $dataType = 'REG_QWORD'; break }
'String' { $dataType = 'REG_SZ'; break }
default { $dataType = 'REG_NONE'; break }
}
# for PowerShell < 3.0 use: New-Object -TypeName PSObject -Property #{ ... }
[PSCustomObject]#{
'ComputerName' = $computer
'Hive' = $objHive.ToString()
'HiveName' = $hiveName
'HiveShortName' = $hiveShort
'Path' = $objSubKey.Name
'SubKey' = $regPath.TrimStart("\")
'ItemType' = 'RegistryProperty'
'DataType' = $dataType
'ValueKind' = $kind
'PropertyName' = if ([string]::IsNullOrEmpty($name)) { '(Default)' } else { $name }
'PropertyValue' = $data
'PropertyValueRaw' = $raw
}
$script:resultCount++
}
}
}
}
# recurse through all subkeys
if ($Recurse) {
foreach ($keyName in $subKeys) {
if ($script:resultCount -lt $MaximumResults) {
$newPath = "$regPath\$keyName"
_RegSearch $objRootKey $newPath $computer
}
}
}
# close opened subkey
if (($objSubKey) -and $objSubKey.Name -ne $objRootKey.Name) { $objSubKey.Close() }
}
}
Process{
if ($isPipeLine) { $ComputerName = #($_) }
$ComputerName | ForEach-Object {
Write-Verbose "Searching the registry on computer '$ComputerName'.."
try {
$rootKey = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($objHive, $_)
_RegSearch $rootKey $KeyPath $_
}
catch {
Write-Error "$($_.Exception.Message)"
}
finally {
if ($rootKey) { $rootKey.Close() }
}
}
Write-Verbose "All Done searching the registry. Found $($script:resultCount) results."
}
}
It returns a collection of objects with the following properties:
ComputerName : MYMACHINE
Hive : LocalMachine
HiveName : HKEY_LOCAL_MACHINE
HiveShortName : HKLM
Path : HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Desktop\NameSpace\{F3F5824C-AD58-4728-AF59-A1EBE3392799}
SubKey : SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Desktop\NameSpace\{F3F5824C-AD58-4728-AF59-A1EBE3392799}
ItemType : RegistryProperty
DataType : REG_SZ
ValueKind : String
PropertyName : (Default)
PropertyValue : Sticky Notes Namespace Extension for Windows Desktop Search
PropertyValueRaw : Sticky Notes Namespace Extension for Windows Desktop Search
The difference between PropertyValue and PropertyValueRaw is that in PropertyValue Environment names are expanded
('%SystemRoot%' in the data gets expanded to 'C:\Windows'), whereas in PropertyValueRaw the data is returned as-is.
(Environment names return as '%SystemRoot%')
I suspect it may have to do with the way you downloaded the file, and how you're calling it. Please try the following steps and see if that works for you.
Click the link to view the file in RAW format, or just click here
Right-click on the page and Save As... a ps1 file in your scripts folder. Note: Make sure you save it with the .ps1 extension and not as txt.
Open Powershell, browse to the directory, and call it as so...
.\Search-Registry.ps1 -StartKey HKCU -Pattern "Visual Studio" -MatchData
You can modify however you like, of course, I chose to look for Visual Studio in HKCU since I knew it would be a quick search.

How to create nested Solution Folders with envdte

I've tried to create a visual studio solution with nested solution folders through Powershell (envdte). Everything works up to 1 level deep, but nesting a solution folder doesn't seem to work (interface is null). The issue is described in this SO question:
Creating a tree of Solution Folders using DTE and Package Manager Console
Unfortunately that question hasn't been answered yet. I've reachted out to the poster of that question but he had taken another route within his solution so the question is still open.
An excerpt of my code. The Find-Solution method finds the solutionfolder I'm looking for within the given context (= startfolder). When found, it returns this item:
function Find-SolutionFolder {
Param($SearchedFolderName, $StartFolder)
Write-Output "number " $StartFolder.ProjectItems.Count
For ($i = 1; $i -le $StartFolder.ProjectItems.Count; $i++) {
$Item = $StartFolder.ProjectItems.Item($i)
if ($Null -Eq $Item.Object) {
continue;
}
if ($Item.Object.Kind -eq [EnvDTE80.ProjectKinds]::vsProjectKindSolutionFolder) {
if ($Item.Name -Eq $SearchedFolderName) {
return $Item
}
Find-SolutionFolder $SearchedFolderName $Item
}
}
}
The Add-Projects method takes care of saving the structure to the solution. The structure is:
Solution
ModuleTypeFolder (ie. Foundation)
ModuleGroupFolder (optional)
Folder for project
projectfiles
This is a nested structure. Everything works without the ModuleGroupFolder but when the structure has the ModuleGroupFolder it causes the error due to the null result of the Get-Interface. I've confirmed that the correct solution folder is found. It's just the variable $moduleGroupNameFolderInterface is null.
The parameter modulePath is the path on disk
function Add-Projects {
Param(
[Parameter(Position = 0, Mandatory = $True)]
[string]$ModulePath
)
Write-Output "Adding project(s)..."
# For the sake of the example always use a folder named 'Foundation'
$moduleTypeFolder = Get-FoundationSolutionFolder
# When the literal 'Foundation' solution folder does not exist in the solution it will be created.
if (-Not $moduleTypeFolder) {
$dte.Solution.AddSolutionFolder($config.ModuleType)
$moduleTypeFolder = Get-ModuleTypeSolutionFolder
}
$moduleTypeFolderInterface = Get-Interface $moduleTypeFolder.Object ([EnvDTE80.SolutionFolder])
# Add ModuleGroup Folder if needed
if (-Not [string]::IsNullOrWhiteSpace($config.ModuleGroupName)) {
$moduleGroupNameFolder = Find-SolutionFolder $config.ModuleGroupName $moduleTypeFolder
if (-Not $moduleGroupNameFolder) {
$moduleTypeFolderInterface.AddSolutionFolder($config.ModuleGroupName)
$moduleGroupNameFolder = Find-SolutionFolder $config.ModuleGroupName $moduleTypeFolder
}
$moduleGroupNameFolderInterface = Get-Interface $moduleGroupNameFolder.Object ([EnvDTE80.SolutionFolder])
if ($Null -eq $moduleGroupNameFolderInterface) {
Write-Output "moduleGroupNameFolderInterface is null; this is wrong"
} else {
$moduleNameFolder = $moduleGroupNameFolderInterface.AddSolutionFolder($config.ModuleName)
$moduleNameFolderInterface = Get-Interface $moduleNameFolder.SubProject ([EnvDTE80.SolutionFolder])
# Search in the new module folder for csproj files and add those to the solution.
Get-ChildItem -File -Path $ModulePath -Filter "*$csprojExtension" -Recurse | ForEach-Object { $moduleNameFolderInterface.AddFromFile("$($_.FullName)")}
}
} else {
$moduleNameFolder = $moduleTypeFolderInterface.AddSolutionFolder($config.ModuleName)
$moduleNameFolderInterface = Get-Interface $moduleNameFolder.Object ([EnvDTE80.SolutionFolder])
# Search in the new module folder for csproj files and add those to the solution.
Get-ChildItem -File -Path $ModulePath -Filter "*$csprojExtension" -Recurse | ForEach-Object { $moduleNameFolderInterface.AddFromFile("$($_.FullName)")}
}
Write-Output "Saving solution..."
$dte.Solution.SaveAs($dte.Solution.FullName)
}
Note. the example is not optimized (ie. duplicate code)
Can anybody help me solve the issue.
Update - answer to question
I finally figured it out. Apparently when finding a nested solution folder where property Kind has guid {66A26722-8FB5-11D2-AA7E-00C04F688DDE} it's not the correct object yet. You have to use the object within the found item.
So basically you are looking for recursion. You can recurse like this.
For the solutions folder:
function RecurseSolutionFolderProjects(){
param($solutionFolder = $(throw "Please specify a solutionFolder"))
$projectList = #()
for($i = 1; $i -le $solutionFolder.ProjectItems.Count; $i++){
$subProject = $solutionFolder.ProjectItems.Item($i).subProject
if($subProject -eq $null){
continue;
}
if($subProject.Kind -eq [EnvDTE80.ProjectKinds]::vsProjectKindSolutionFolder)
{
$projectList += RecurseSolutionFolderProjects($subProject)
} else {
$projectList += $subProject
}
}
return $projectList
}
For Project Files:
function GetProjectFiles(){
param($project = $(throw "Please specify a project"))
write-debug ("getting project files for " + $project.Name + " "+ $project.ProjectName)
$projectItems = RecurseDescendants($project.ProjectItems)
return $projectItems | Where-Object {$_.Kind -ne [EnvDTE.Constants]::vsProjectItemKindPhysicalFolder}
}
For Project Items:
function GetProjectItems(){
param($project = $(throw "Please specify a project"))
if($project.ProjectItems.count -gt 0){
write-debug "getting project items for '$project.Name' '$project.ProjectName'"
}
#example: GetProjectItems((GetSolutionProjects).get_Item(1))
$result =RecurseDescendants($project.ProjectItems)
return $result
}
Refer the Solution Hierarchy answer where the above functions are neatly explained
You can get the latest version from this GitHub Link
Hope it helps.

TFS 2015 no longer adds build number to Global List upon build complete?

In TFS 2015 new build system, did the functionality to automatically add build number to Global List (Build - Project Name) upon build complete removed?
Do I need to write a custom PowerShell task to accomplish this?
Note: XAML builds still add build number to Global List as it did before.
Since many features are still missing in the vNext build system, I've made a PowerShell script that do the Job.
In a near futur, I plan to update this script to support IntegratedIn field filling and to convert the script as a custom build task.
[CmdletBinding(SupportsShouldProcess=$false)]
param()
function Update-GlobalListXml
{
[CmdletBinding(SupportsShouldProcess=$false)]
param(
[xml]$globalListsDoc,
[parameter(Mandatory=$true)][string][ValidateNotNullOrEmpty()]$glName,
[parameter(Mandatory=$true)][string][ValidateNotNullOrEmpty()]$buildNumber
)
Write-Verbose "Checking whether '$glName' exists"
$buildList = $globalListsDoc.GLOBALLISTS.GLOBALLIST | Where-Object { $_.name -eq $glName }
if ($buildList -eq $null)
{
Write-Host "GlobalList '$glName' does not exist and will be created"
$globalLists = $globalListsDoc.GLOBALLISTS
if($globalLists.OuterXml -eq $null)
{
$newDoc = [xml]"<gl:GLOBALLISTS xmlns:gl="""http://schemas.microsoft.com/VisualStudio/2005/workitemtracking/globallists"""></gl:GLOBALLISTS>"
$globalLists = $newDoc.GLOBALLISTS
}
$globalList = $globalLists.OwnerDocument.CreateElement("GLOBALLIST")
$globalList.SetAttribute("name", $glName)
$buildList = $globalLists.AppendChild($globalList)
}
if(($buildList.LISTITEM | where-object { $_.value -eq $buildNumber }) -ne $null)
{
throw "The LISTITEM value: '$buildNumber' already exists in the GLOBALLIST: '$glName'"
}
Write-Host "Adding '$buildNumber' as a new LISTITEM in '$glName'"
$build = $buildList.OwnerDocument.CreateElement("LISTITEM")
$build.SetAttribute("value", $buildNumber)
$buildList.AppendChild($build) | out-null
return $buildList.OwnerDocument
}
function Invoke-GlobalListAPI()
{
[CmdletBinding(SupportsShouldProcess=$false)]
param(
[parameter(Mandatory=$true)][Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemStore]$wiStore,
[parameter(Mandatory=$true,ParameterSetName="Import")][switch]$import,
[parameter(Mandatory=$true,ParameterSetName="Import")][xml]$globalLists,
[parameter(ParameterSetName="Export")][switch]$export
)
try {
if($import)
{
$wiStore.ImportGlobalLists($globalLists.OuterXml) # Account must be explicitly in the Project Administrator Group
}
if($export)
{
return [xml]$wiStore.ExportGlobalLists()
}
}
catch [Microsoft.TeamFoundation.TeamFoundationServerException] {
Write-Error "An error has occured while exporting or importing GlobalList"
throw $_
}
}
function Get-WorkItemStore()
{
[CmdletBinding(SupportsShouldProcess=$false)]
param(
[parameter(Mandatory=$true)][string][ValidateNotNullOrEmpty()]$tpcUri,
[parameter(Mandatory=$true)][string][ValidateNotNullOrEmpty()]$agentWorker
)
# Loads client API binaries from agent folder
$clientDll = Join-Path $agentWorker "Microsoft.TeamFoundation.Client.dll"
$wiTDll = Join-Path $agentWorker "Microsoft.TeamFoundation.WorkItemTracking.Client.dll"
[System.Reflection.Assembly]::LoadFrom($clientDll) | Write-Verbose
[System.Reflection.Assembly]::LoadFrom($wiTDll) | Write-Verbose
try {
Write-Host "Connecting to $tpcUri"
$tfsTpc = [Microsoft.TeamFoundation.Client.TfsTeamProjectCollectionFactory]::GetTeamProjectCollection($tpcUri)
return $tfsTpc.GetService([Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemStore])
}
catch [Microsoft.TeamFoundation.TeamFoundationServerException] {
Write-Error "An error has occured while retrieving WorkItemStore"
throw $_
}
}
function Get-WITDataStore64
{
[CmdletBinding(SupportsShouldProcess=$false)]
param()
if($env:VS140COMNTOOLS -eq $null)
{
throw New-Object System.InvalidOperationException "Visual Studio 2015 must be installed on the build agent" # TODO: Change it by checking agent capabilities
}
$idePath = Join-Path (Split-Path -Parent $env:VS140COMNTOOLS) "IDE"
return Get-ChildItem -Recurse -Path $idePath -Filter "Microsoft.WITDataStore64.dll" | Select-Object -First 1 -ExpandProperty FullName
}
function Update-GlobalList
{
[CmdletBinding(SupportsShouldProcess=$false)]
param()
# Get environment variables
$tpcUri = $env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI
Write-Verbose "Team Project Collection Url: '$tpcUri'"
$teamProjectName = $env:SYSTEM_TEAMPROJECT
Write-Verbose "Team Project: '$teamProjectName'"
$buildNumber = $env:BUILD_BUILDNUMBER
Write-Verbose "Build Number: '$buildNumber'"
$agentHome = $env:AGENT_HOMEDIRECTORY
Write-Verbose "Agent home direrctory: '$agentHome'"
$globalListName = "Builds - $teamProjectName"
Write-Verbose "GlobalList name: '$teamProjectName'"
# Copy 'Microsoft.WITDataStore64.dll' from Visual Studio directory to AgentBin directory if it does not exist
$agentWorker = Join-Path $agentHome "agent\Worker"
$targetPath = Join-Path $agentWorker "Microsoft.WITDataStore64.dll" # Only compatible with x64 process #TODO use constant instead
if(-not (Test-Path $targetPath))
{
$wITDataStore64FilePath = Get-WITDataStore64
Write-Host "Copying $wITDataStore64FilePath to $targetPath"
Copy-Item $wITDataStore64FilePath $targetPath | Write-Verbose
}
$wiStore = Get-WorkItemStore -tpcUri $tpcUri -agentWorker $agentWorker
# Retrive GLOBALLISTS
$xmlDoc = Invoke-GlobalListAPI -export -wiStore $wiStore
$gls2 = Update-GlobalListXml -globalListsDoc $xmlDoc -glName $globalListName -buildNumber $buildNumber
Invoke-GlobalListAPI -import -globalLists $gls2 -wiStore $wiStore
}
Update-GlobalList
Here is the link of the Github repo, feedbacks are welcome => https://github.com/GregoryOtt/UpdateWiBuildNum/blob/master/Update-GlobalList.ps1
[disclaimer - I work on the new build system]
That global list on the workitem is a mechanism that dated back to the original release of TFS. It's one that sort of worked in that day and age (days of nightly builds, pre-CI and CD agility). It's starts to fall apart and doesn't show as proper relationships in TFS. I worked on WIT at that time and we needed a queryable mechanism and that's what we had (blame me :)
So, when we started a new build system, we didn't want to rebuild things and repeat the same mistakes. We're trying to take an agile, incremental approach to a better build system.
In the next sprint (88), we are starting work on proper links between builds and workitems and the WIT team is also doing work to make them more first class. The first thing you'll see is a link on the WIT form and that should hopefully make QU1 as well (at least parts of it).
We realize this does leave a few gaps but we are working to close them (gated and label sources being two others) and hopefully in a better way for a better long term product.
As far as a workaround goes, it should be possible to automate via powershell and our clients but we don't have anything canned for others to use.