Property has a value but cannot select it - powershell

I have a function that checks the registry for an uninstall key called Get-InstalledApps
Function Get-InstalledApps {
param (
[Parameter(ValueFromPipeline=$true)]
[string[]]$ComputerName = $env:COMPUTERNAME,
[string]$NameRegex = ''
)
foreach ($comp in $ComputerName) {
$keys = '','\Wow6432Node'
foreach ($key in $keys) {
try {
$reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $comp)
$apps = $reg.OpenSubKey("SOFTWARE$key\Microsoft\Windows\CurrentVersion\Uninstall").GetSubKeyNames()
} catch {
continue
}
foreach ($app in $apps) {
$program = $reg.OpenSubKey("SOFTWARE$key\Microsoft\Windows\CurrentVersion\Uninstall\$app")
$name = $program.GetValue('DisplayName')
if ($name -and $name -match $NameRegex) {
[pscustomobject]#{
ComputerName = $comp
DisplayName = $name
DisplayVersion = $program.GetValue('DisplayVersion')
Publisher = $program.GetValue('Publisher')
InstallDate = $program.GetValue('InstallDate')
UninstallString = $program.GetValue('UninstallString')
Bits = $(if ($key -eq '\Wow6432Node') {'64'} else {'32'})
Path = $program.name
}
}
}
}
}
}
and then I grab the DisplayName/Version for what I need. My current problem is that it only seems to work on certain machines. Example:
Get-InstalledApps | Where-Object {$_.Displayname -like "*Citrix Receiver*"}
Name Value
---- -----
InstallDate
ComputerName Computer
DisplayName Citrix Receiver 4.7
Bits 64
UninstallString C:\ProgramData\Citrix\Citrix Receiver 4.7\TrolleyExpress.exe /uninstall /cleanup
Path HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\CitrixOnlinePluginPackWeb
Publisher Citrix Systems, Inc.
DisplayVersion 14.7.0.13011
So this is great, I get what I want. Now I normally just pipe in | Select-Object Displayname -ExpandProperty Displayname and it would return "Citrix Receiver 4.7" just like I want. My problem is that on certain machines I'm getting this:
Get-InstalledApps | Where-Object {$_.Displayname -like "*Citrix Receiver*"} | Select-Object DisplayName
DisplayName
-----------
And that's it. Why is there no value listed? If I try to expandproperty I get an error because it says nothing is there, but clearly there is something there or the Where-Object would not have found it in my search. Again, in a lot cases this code works just fine and I get the value I want but on a lot of machines I'm getting what you see above.
Edited in from comments:
I run this on a user's machine and I get the results I posted. If I run it on my machine I'll get the value "Citrix Receiver 4.7" every time. Also, on my machine I don't get the Name and Value columns. Only about 1/4 of the machines I ran this code on actually gave me the value I expected. Windows 7 vs Windows 10 thing?

It looks to me like your function returns a [hashtable], but you're using it like it's an object with properties.
That happens to work fine with Where-Object because the .Member syntax works for accessing [hashtable] values, but it's not going to work with Select-Object because it's operating on actual properties.
So what can you do?
If you want to keep it as a [hashtable], and insist on doing it in a pipeline, you can use ForEach-Object:
Get-InstalledApps |
Where-Object {$_.Displayname -like "*Citrix Receiver*"} |
ForEach-Object -Process { $_.DisplayName }
or
Get-InstalledApps |
Where-Object {$_.Displayname -like "*Citrix Receiver*"} |
ForEach-Object -MemberName Item -ArgumentList DisplayName
Another thing you can do is change your function to return an object.
This is really easy to do with a [hashtable]; so say your function is about to return $hash, instead return:
New-Object -TypeName PSObject -Property $hash
Now you can use the normal suite of cmdlets and have them work as expected.
Edit: after seeing your code, it looks like you are converting your hashtable to an object already, but your output says otherwise. It wouldn't display as Name and Value columns if that were the case, so I still think something is wrong and the output is a [hashtable].
Edit 2: with info from comments about the platform differences, this seems to be happening because the object conversion is being done with the [pscustomobject] type accelerator which was added in PowerShell v3. Since the problematic machine is running Windows 7, it may be running v2 (which is what Win 7 shipped with).
Recommendations:
Get rid of Windows 7.
If you can't do that, upgrade PowerShell (Windows Management Framework) on that machine.
Either way, use New-Object as posted above.

Related

Change a Windows product key remotely with PowerShell

I'm trying to install/activate a MAK key on remote servers. All of them have RemotePS enabled and firewall exception rules in place.
$Results = Invoke-Command -ComputerName Server1 {
$Props = #{ComputerName = $env:ComputerName}
slmgr.vbs /ipk "12345-12345-12345-12345-12345"
$LicStatus = slmgr.vbs /dlv
$Props.Add('LicenseStatus',$LicStatus)
New-Object -TypeName PSObject -Property $Props
}
$Results | Select-Object ComputerName,LicenseStatus
The above does install the MAK key but I don't get any confirmation of this process which is why I've tried adding in the license check option (/dlv) but get nothing returned in the LicenseStatus field. I'm assuming this is because it returns a multi-value maybe!?
Ultimately I'm just trying to get confirmation that the key was installed. There are articles out there about performing this using RemotePS but they all say a notification message is returned for each computer which isn't the case in my experience: https://4sysops.com/archives/change-a-product-key-remotely-with-powershell/
Any ideas how I can check this?
I would call the slmgr.vbs script using Cscript.exe in order to get the results as string array. Otherwise the system will default to using Wscript.exe which is designed to output everything in a messagebox.
Unfortunately, all output of slmgr is localized, so using a regex or something on the LicenseStatus is a no go (on a Dutch NL machine it reads 'Licentiestatus')
What you can do is using switch /dli, because that returns a string array where the last (not empty) value has the status.
Try
$Results = Invoke-Command -ComputerName Server1 {
# install MAK key
$null = cscript.exe "$env:SystemRoot\System32\slmgr.vbs" /ipk "12345-12345-12345-12345-12345"
# test LicenseStatus
$LicStatus = (((cscript.exe "$env:SystemRoot\System32\slmgr.vbs" /dli) |
Where-Object { $_ -match '\S' })[-1] -split ':', 2)[1].Trim()
# return an object
[PsCustomObject]#{
ComputerName = $env:COMPUTERNAME
LicenseStatus = $LicStatus
}
}
$Results

Calculate days since last program installed using PowerShell

What is the way to find all recently installed/repaired/modified software?
What I have been trying is to get the list from the registry, then filter it to find only relevant programs, and then try to calculate which of them was installed before a certain threshold.
This is what I have been doing:
$Installed_Software=Get-ItemProperty HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Select-Object DisplayName, DisplayVersion, Publisher, InstallDate
$SoftwareInstall_Days = 40
$TheDate = (([datetime]::Now))
$Installed_recently= #()
$Installed_recently=($Installed_Software |
Where-Object {($_.DisplayName -match ('Something') -or $_.Publisher -match ('SomeOtherThing'))} |
where (($TheDate - $_.InstallDate) -le $SoftwareInstall_Days)
if ($Installed_recently) {
Write-Output "Relevant software was recently installed and/or repaired / modified.`nThese are the items:`n$Installed_recently"
}
else {
Write-Output "No relevant software was recently installed and/or repaired / modified."
}
Of course, the following condition is just an illustration of what I am failing to do:
where (($TheDate - $_.InstallDate) -le $SoftwareInstall_Days)
How can this be done?
Am I even in the right direction here?
In order to be able to compare dates, you need to parse the InstallDate string that you get from registry and then subtract it from the current date.
There are couple of formats that this string can be. On my machine, English Win10 x86, I see 2 date formats: yyyyMMdd and MM/dd/yyyy.
In the code below, I created an array that you can add additional formats to parse.
Here is the modified code - I used "Microsoft" string to test:
# Add additional property InstallDateObj that will hold the parsed DateTime object
$Installed_Software=Get-ItemProperty HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Select-Object DisplayName, DisplayVersion, Publisher, InstallDate, InstallDateObj
$SoftwareInstall_Days = 40
$TheDate = (([datetime]::Now))
# Try to parse dates.
$Installed_Software.ForEach({
# add more formats if you need
[string[]] $formats = #("yyyyMMdd","MM/dd/yyyy")
$installDate = $_.InstallDate
$installedDateObj = $null;
$formats.ForEach({ [DateTime] $dt = New-Object DateTime; if([datetime]::TryParseExact($installDate, $_, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::None, [ref]$dt)) { $installedDateObj = $dt} });
$_.InstallDateObj = $installedDateObj
})
$Installed_recently= #()
$Installed_recently=($Installed_Software |
Where-Object {($_.DisplayName -match ('Microsoft') -or $_.Publisher -match ('Microsoft') -and ($_.InstallDateObj -ne $null) -and ($TheDate - $_.InstallDateObj).Days -le $SoftwareInstall_Days)})
if($Installed_recently.Count -gt 0) {
Write-Output "Relevant software was recently installed and/or repaired / modified.`nThese are the items:`n"
Write-Output $Installed_recently
}
else {
Write-Output "No relevant software was recently installed and/or repaired / modified."
}
There are methods for datetime object to add or remove minutes,hours,days,years from datetime, see script below.
For your current exmaple of getting software from registry, InstallDate need to be converted to a datetime format and then compared to a needed date from report.
#Report date 40 days
$SoftwareInstall_Days = (([datetime]::Now)).AddDays(-40)
#Getting installed software and filtering based on report date
$Installed_recently=Get-ItemProperty HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Select-Object DisplayName, DisplayVersion, Publisher, #{Name = "InstalledDate" ; Expression = {[DateTime]::ParseExact($_.InstallDate, 'yyyyMMdd', [Globalization.CultureInfo]::InvariantCulture) } }| where {$_.installeddate -ge $SoftwareInstall_Days}
#Output
$Installed_recently
Installed software also can be gathered from different places:
#Using WMI
Get-WmiObject -Class Win32_Product
#Using PS Software provider
Get-Package

Can't figure out Powershell Where-Object. SSAS Cube Processed State via Powershell,

I've created a powershell script to list the Processed State of a number of SSAS Cubes.
I'm puzzled why the Where-Object only produces the correct results when I use -like. I can't seem to use -eq. Anyone got any ideas ?
I'm using the Where-Object to zero in on the production Database.
The code i'm using is this..
Import-Module SQLASCmdlets -DisableNameChecking
$SSASServer = New-Object Microsoft.AnalysisServices.Server
$instanceName = "SSASSVR01"
$SSASServer.connect($instanceName)
$ProdCubes = $SSASServer.Databases.Cubes
$ProdCubes | Select Parent , State , Name `
| Where-Object { $_.Parent -like "DWProd*" }
I'd expect that -eq would work, but it doesn't
I don't get any response if the last line is written as...
$ProdCubes | Select Parent , State , Name `
| Where-Object { $_.Parent -eq "DWProd" }
I suspect it's whitespace, but how do you filter out the whitespace in powershell.

Powershell - how to replace OS Version number with String

I am querying remote servers for their operating system. I know that I can return the Version, but I want to replace these values with the friendly name. The code I have so far is:
$Computer = (gc c:\servers.txt)
$BuildVersion = Get-WmiObject -Class Win32_OperatingSystem -Property Version, CSName -ComputerName $Computer -ErrorAction SilentlyContinue
$Build=$BuildVersion.version
If ({$BuildVersion.Version -match "5.2.3790"})
{$Build="2003"}
Elseif ({$BuildVersion.Version -match "6.1.7601"})
{$Build="2008"}
Elseif ({$BuildVersion.Version -like "6.3.9600"})
{$Build="2012"}
But this doesn't seem to work and only returns "2003" regardless. Please help, I'm fairly new to PS and coding.
thanks
The problem is your if statements. Putting the Boolean expression inside squiggly brackets makes it a script block, and that's going to get cast as a string before being cast as a Boolean. Strings cast to Booleans always evaluate to true unless they're empty.
PS C:\> {$BuildVersion.Version -match "5.2.3790"}
$BuildVersion.Version -match "5.2.3790"
PS C:\> ({$BuildVersion.Version -match "5.2.3790"}) -as [bool]
True
PS C:\> $BuildVersion.Version -match "5.2.3790"
False
PS C:\> ($BuildVersion.Version -match "5.2.3790") -as [bool]
False
So what you're running is essentially:
if ([bool]'$BuildVersion.Version -match "5.2.3790"') [...]
And that's always going to be true.
Try:
$Computer = (gc c:\servers.txt)
$BuildVersion = Get-WmiObject -Class Win32_OperatingSystem -Property Version, CSName -ComputerName $Computer -ErrorAction SilentlyContinue
$Build=$BuildVersion.version
If ($BuildVersion.Version -match "5.2.3790")
{
$Build = "2003"
}
Elseif ($BuildVersion.Version -match "6.1.7601")
{
$Build = "2008"
}
Elseif ($BuildVersion.Version -like "6.3.9600")
{
$Build = "2012"
}
Bottom line is that squiggly brackets are not parentheses and you can't use them like they are.
However, there's also a major logic error here. You're potentially fetching an array for $BuildVersion because you're reading from a file, but then you treat it like a single value. You never loop through $BuildVersion. However, I do not have enough information about what you're actually trying to do with your script (like what you do with $Build) to be able to fix that.
I originally said this, but I've since changed my mind
The reason this is only returning 2003 is that you're only running your If code on a single entry in the list.
Wrong
As TessellatingHeckler says, the reason your if wasn't working is that you had too many curly braces, so PowerShell wasn't actually evaluating your logic.
However, you still need to step through each of the computers to do what you're trying to do. We'll do that by adding in a ForEach loop. I also went ahead and replaced your If {} logic with a Switch statement, which I think is easier to understand for a scenario like this with multiple clauses. If's just get way too verbose.
Finally, I'm assuming you want to output the results too, so I added a custom object here, which is just a way of choosing which properties we want to display.
$Computer = (gc c:\servers.txt)
ForEach ($system in $computer){
$BuildVersion = Get-WmiObject -Class Win32_OperatingSystem -Property Version, CSName -ComputerName $system -ErrorAction SilentlyContinue
$Build=$BuildVersion.version
switch ($build){
"5.2.3790" {$Build="2003"}
"6.1.7601" {$Build="2008"}
"6.3.9600" {$Build="2012"}
}
#output results
[pscustomobject]#{Server=$system;OSVersion=$build;CSName=$buildVersion.CSname}
}#EndOfForEach
Output
>Server OSVersion CSName
------ --------- ------
dc2012 2012 DC2012
sccm1511 2012 SCCM1511
You can use this:
Get-WmiObject -Class Win32_OperatingSystem | Select-Object -ExpandProperty Caption
Additionally you can see everything this WMI object holds like this:
Get-WmiObject -Class Win32_OperatingSystem | fl *
Edit: if you want to remove some text from the string, you can use -replace:
(Get-WmiObject -Class Win32_OperatingSystem |
Select-Object -ExpandProperty Caption) -replace "Microsoft Windows Server ",""

Convert GUID string to octetBytes using PowerShell

I have a powershell script which outputs all Exchange 2003 mailboxes by size.
$computers = "vexch01","vexch02"
foreach ($computer in $computers) {
Get-Wmiobject -namespace root\MicrosoftExchangeV2 -class Exchange_Mailbox -computer $computer | sort-object -desc Size | select-object MailboxDisplayName,StoreName,#{Name="Size/Mb";Expression={[math]::round(($_.Size / 1024),2)}}, MailboxGUID | Export-Csv -notype -Path $computer.csv
}
Currently this outputs the MailboxGUID as a string type GUID (e.g. {21EC2020-3AEA-1069-A2DD-08002B30309D}). I want to look up users in AD by this, but AD stores them in octetBytes format.
I have found some powershell functions which will do the conversion but only when the curly braces are removed. The Guid.ToString method should supply this, but I can't get it to work in the above.
However, if I could figure out how to do that, the Guid.ToByteArray method might get me even closer.
Has anyone cracked this?
Update: the answers so far helped me write a function that converts the mailboxguid into the correct format for searching via LDAP. However, I now cannot get this working in the script. This is my updated script:
function ConvertGuidToLdapSearchString(
[parameter(mandatory=$true, position=0)]$Guid
)
{
$guid_object = [System.Guid]$Guid
($guid_object.ToByteArray() | foreach { '\' + $_.ToString('x2') }) -join ''
}
# Gets data through WMI from specified Exchange mailbox servers
$servers = "vexch01","vexch02"
foreach ($server in $servers) {
Get-Wmiobject -namespace root\MicrosoftExchangeV2 -class Exchange_Mailbox -computer $computer | sort-object -desc Size | select-object MailboxDisplayName,StoreName,#{Name="Size/Mb";Expression={[math]::round(($_.Size / 1024),2)}}, #{Name="LDAP Guid";Expression={ConvertGuidToLdapSearchString(MailboxGUID)}} | Export-Csv -notype -Path $server.csv
}
I'm not sure why using the function in the select-object with #{Name="LDAP Guid";Expression={ConvertGuidToLdapSearchString(MailboxGUID)}} doesn't work.
Is there another way of using this function in select-object that will give the string?
In conjunction with Andy Schneider's answer, you may find this function useful:
function Convert-GuidToLdapSearchString(
[parameter(mandatory=$true, position=0)][guid]$Guid
)
{
($Guid.ToByteArray() | foreach { '\' + $_.ToString('x2') }) -join ''
}
(I thought I had a more clever way to do this by adding a ScriptProperty to System.Guid, but I seem to have learned that you can't effectively add members to structs.)
I'm not sure I understand what you are trying to accomplish based on your comment, but I think you may have just left out a $_. Here is a somewhat contrived example that creates an object with a property that is a GUID, then uses select and Convert-GuidToLdapSearchString to convert the format. I hope it helps.
$o = New-Object PSObject -Property #{ GUID = $([Guid]::NewGuid()) }
$o
$o | select #{ Name='SearchString'; Expression={ Convert-GuidToLdapSearchString $_.GUID } }
This is not at all how I had imagined the function being used. I expected you would use it to create an LDAP search clause such as:
$searchString = Convert-GuidToLdapSearchString '{9e76c48b-e764-4f0c-8857-77659108a41e}'
$searcher = [adsisearcher]"(msExchMailboxGuid=$searchString)"
$searcher.FindAll()
Are you casting the string to a GUID ?
$guid = [System.Guid]"{21EC2020-3AEA-1069-A2DD-08002B30309D}"
$guid.ToString()
$guid.ToByteArray()