Powershell: Why are my RegKey value checks failing using Get-ItemProperty when the value I am checking for is 0? - windows

I have been tasked with scripting regkey checks to make sure a sampling of machines are compliant with our security posture.
The script works great when checking for any non-zero value (such as the example input below).
however, there is a consistent theme where my code if erroneously reporting that the checks have failed if the desired value is 0. See example code below:
$failReview = "" #String to track compliance issues
$OutFile = "C:\local\file.text"
Function Test-STIG_SingleKey {
#check for single acceptable key value
#up to 3 additional acceptable key values may be checked for single key
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[string[]] $CV, #Check Vulnerability number
[string[]] $Path, #RegKey Path
[string[]] $Key, #Key to check
[int] $value, #Expected Regkey value for compliance
$FailReview,
[Parameter(Mandatory=$false)]
[int] $value1, #optional acceptable value
[int] $value2, #optional acceptable value
[int] $value3 #optional acceptable value
)
Begin {}
Process {
$KeyObject = Get-ItemProperty $Path #Obtain RegKey object values using RegKey Path
$Exist = ([bool]($KeyObject.PSObject.Properties.name -imatch $Key)) #Check for match of key value property name and cast to boolean
If(!($Exist)){ #If does NOT exist
Write-Host " !!! $CV is a finding !!!" -ForegroundColor "Red"; $Global:failReview += "$CV, "} #This IS a finding, append CV to list for review
ElseIf (($KeyObject)."$Key" = $value){ #checks relevant object property value and compares to Primary expected value
Write-Host " $CV is not a finding" -ForegroundColor "Green"
}
ElseIf (((($KeyObject)."$Key" = $value1) -or (($KeyObject)."$Key" = $value2) -or (($KeyObject)."$Key" = $value3))) { #alternate acceptable value check
Write-Host " $CV is not a finding, alternate acceptable values used" -ForegroundColor "Green" #not a finding, but indicates alt value present
}
else {Write-Host " !!! $CV is a finding !!!" -ForegroundColor "Red"; $Global:FailReview += "$CV, "} #This IS a finding, append CV to list for review
}
End {Return $CV, $Path, $Key, $value, $KeyObject, $failReview | Out-File $OutFile -Append} #Pass all variable values to out-file for review if needed
}
#EXAMPLE INPUT:
Test-STIG_SingleKey -CV "V70955" `
-Path "HKLM:\Software\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_HTTP_USERNAME_PASSWORD_DISABLE" `
-Key "excel.exe" `
-value 1
I can obtain the value manually by looking at $KeyObject.Key, but can't seem to get the output to evaluate to true if I'm looking for a 0.
I think this is likely because it's evaluating the 0 to $false at some point, but it's an int.
I've already tried casting to [int] and converting it to hex.
Any advice would be appreciated, because I've almost spent more time trying to figure this out then it would take to just manually look at the keys for the sampling that we require.
Thanks!

This is because in your if statements, you are setting ($KeyObject)."$Key" equal to something instead of checking if it is set to a particular value.
So
... ElseIf (($KeyObject)."$Key" = $value){ ...
should be
... ElseIf (($KeyObject)."$Key" -eq $value){ ...

Related

Active Directory - list full subgroup dependency of a given group and omit subgroups without any user

I have to list full subgroup dependency of specific group - filter -> only subgroups which contain at least 1 user.
I have tried this approach:
dsquery group -samid <specific_group> | dsget group -members -expand | dsquery * -filter "(&(objectclass=group))"
This is an adapted version of my answer here to omit groups without any user object.
Unfortunately, using Get-ADGroupMember together with switch -Recursive will not return members that are groups.
As the docs state:
If the Recursive parameter is specified, the cmdlet gets all members
in the hierarchy of the group that do not contain child objects.
To get an array of nested group objects within a certain parent group, you will need a recursive function like below:
function Get-NestedADGroup {
Param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
[ValidateNotNullOrEmpty()]
[Alias ('Identity')]
[string]$Group,
# the other parameters are optional
[string]$Server = $null,
[string]$SearchBase = $null,
[ValidateSet('Base', 'OneLevel', 'Subtree')]
[string]$SearchScope = 'Subtree'
)
$params = #{
Identity = $Group
SearchScope = $SearchScope
Properties = 'Members'
ErrorAction = 'SilentlyContinue'
}
if (![string]::IsNullOrWhiteSpace($Server)) { $params['Server'] = $Server }
if (![string]::IsNullOrWhiteSpace($SearchBase)) { $params['SearchBase'] = $SearchBase }
$adGroup = Get-ADGroup #params
if ($adGroup) {
if (-not $script:groupsHash.ContainsKey($Group)) {
# output this group object only if it has at least one user object
if (#($adGroup.Members | Where-Object {$_.objectClass -eq 'user'}).Count -gt 0) {
$adGroup
}
# avoid circular group references
$script:groupsHash[$Group] = $true
# and recurse to get the nested groups
foreach ($group in ($adGroup.Members | Where-Object {$_.objectClass -eq 'group'})) {
Get-NestedADGroup -Group $group.DistinguishedName -Server $Server -SearchBase $SearchBase
}
}
}
else {
Write-Warning "Group '$($Group)' could not be found.."
}
}
# create a Hashtable to avoid circular nested groups
$groupsHash = #{}
# call the function
$result = Get-NestedADGroup -Group 'SpecificGroup'
# output just the names if you like
$result.Name
# save to CSV
$result | Export-Csv -Path 'X:\Somewhere\SubgroupsWithAtLeastOneUser.csv' -NoTypeInformation
Here is an easy alternative leveraging Active Directory filtering capabilities. See inline comments to understand the logic.
Do note, this answer requires the ActiveDirectory Module available.
# Get the DN of the parent group (this is the initial group)
$parent = (Get-ADGroup parentGroup).distinguishedName
$param = #{
LDAPFilter = "(memberof:1.2.840.113556.1.4.1941:=$parent)"
Properties = "member"
}
# Find, recursively, all child groups Members Of this parent group
# and filter
Get-ADGroup #param | Where-Object {
# For each member of this child group,
# check if this member is an object of the Class `user`,
# if it is, we already can break the pipeline and return
# this child group, since we already know it has
# at least 1 user member
$_.member | Get-ADObject | Where-Object ObjectClass -EQ user |
Select-Object -First 1
}

How to get the highest value from list of Device Names

Generating a list of windows workstation computer names by reading the active directory and I need to find the highest number so that I can then assign a new device with the next available number - I am not having any success in doing this - how to do it? And as you can see from the list of names, I also have missing numbers in the sequence that ideally, I would like to fill in with new devices also...
The code I am using to get the list from AD is below.
((Get-ADComputer -Filter {operatingsystem -notlike "*server*" -and Name -like $NamingConvention -and enabled -eq "true"} -Credential $credential -server $ADServerIP).Name)
List of device names
PC01
PC28
PC29
PC30
PC31
PC32
PC33
PC34
PC35
PC36
PC37
PC38
PC40
PC41
PC42
PC43
PC44
PC45
PC46
PC47
PC27
PC48
PC26
PC24
PC179
PC18
PC180
PC181
PC182
PC183
PC184
PC185
PC186
PC187
PC188
PC189
PC19
PC190
PC191
PC192
PC21
PC22
PC23
PC25
PC178
PC49
PC51
PC77
PC78
PC79
PC80
PC81
PC83
PC84
PC85
PC87
PC88
PC89
PC90
PC91
PC92
PC93
PC94
PC95
PC96
PC97
PC76
PC50
PC75
PC72
PC52
PC53
PC54
PC55
PC56
PC57
PC59
PC60
PC61
PC62
PC63
PC64
PC65
PC66
PC67
PC68
PC69
PC70
PC71
PC73
PC98
PC177
PC175
PC115
PC116
PC117
PC118
PC119
PC12
PC120
PC121
PC122
PC123
PC124
PC125
PC126
PC127
PC128
PC129
PC13
PC130
PC131
PC114
PC132
PC113
PC111
PC02
PC03
PC04
PC06
PC08
PC09
PC10
PC100
PC101
PC102
PC103
PC104
PC105
PC106
PC107
PC108
PC109
PC11
PC110
PC112
PC176
PC133
PC135
PC158
PC159
PC16
PC160
PC161
PC162
PC163
PC164
PC165
PC166
PC167
PC168
PC169
PC17
PC170
PC171
PC172
PC173
PC174
PC157
PC134
PC156
PC154
PC136
PC137
PC138
PC139
PC14
PC140
PC141
PC142
PC143
PC144
PC145
PC146
PC147
PC148
PC149
PC150
PC151
PC152
PC153
PC155
PC99
Sort the pc names on their numeric values and select the last one:
$lastPC = (Get-ADComputer -Filter {operatingsystem -notlike "*server*" -and Name -like $NamingConvention -and enabled -eq "true"} -Credential $credential -server $ADServerIP).Name |
Sort-Object { [int]($_ -replace '\D+')} | Select-Object -Last 1
Here's a solution that will give you the highest number ($dataMax), the missing numbers ($dataMissing), and the next number to use ($dataNext). The next number to use will be either the 1st missing number, or if there are no missing numbers then it will be the highest number + 1
# load the computers list
$data = ((Get-ADComputer -Filter {operatingsystem -notlike "*server*" -and Name -like $NamingConvention -and enabled -eq "true"} -Credential $credential -server $ADServerIP).Name)
# create an array by splitting the data text using the "space" character as a delimiter
$data = $data.Split(" ")
# remove all the alpha characters ("PC"), leaving only the number values so it can be sorted easier
$dataCleaned = $data -replace "[^0-9]" , '' | sort { [int]$_ }
# after sorting the data, [-1] represents the last element in the array which will be the highest number
[int]$dataMax = $dataCleaned[-1]
# create a number range that represents all the numbers from 1 to the highest number
$range = 1..$dataMax | foreach-object { '{0:d2}' -f $_ }
# compare the created range against the numbers actually in the computer array to find the missing numbers
$dataMissing = #(compare $range $dataCleaned -PassThru)
# if there's a missing value, [0] represents the first element in the array of missing numbers
if ($dataMissing)
{
$dataNext = $dataMissing[0]
}
# if there's no missing values, the next value is the max value + 1
else
{
$dataMissing = "none"
$dataNext = $dataMax + 1
}
Write-Host "The highest number is:"('{0:d2}' -f $dataMax)
Write-Host "The missing numbers are: $dataMissing"
Write-Host "The next number to use is:" ('{0:d2}' -f $dataNext)
Assuming your list is exactly as it appears to be, then this appears to be one way to do it:
$List = 'PC01 PC28 PC29 PC30 PC31 PC32 PC33 PC34 PC35 PC36 PC37 PC38 PC40 PC41 PC42 PC43 PC44 PC45 PC46 PC47 PC27 PC48 PC26 PC24 PC179 PC18 PC180 PC181 PC182 PC183 PC184 PC185 PC186 PC187 PC188 PC189 PC19 PC190 PC191 PC192 PC21 PC22 PC23 PC25 PC178 PC49 PC51 PC77 PC78 PC79 PC80 PC81 PC83 PC84 PC85 PC87 PC88 PC89 PC90 PC91 PC92 PC93 PC94 PC95 PC96 PC97 PC76 PC50 PC75 PC72 PC52 PC53 PC54 PC55 PC56 PC57 PC59 PC60 PC61 PC62 PC63 PC64 PC65 PC66 PC67 PC68 PC69 PC70 PC71 PC73 PC98 PC177 PC175 PC115 PC116 PC117 PC118 PC119 PC12 PC120 PC121 PC122 PC123 PC124 PC125 PC126 PC127 PC128 PC129 PC13 PC130 PC131 PC114 PC132 PC113 PC111 PC02 PC03 PC04 PC06 PC08 PC09 PC10 PC100 PC101 PC102 PC103 PC104 PC105 PC106 PC107 PC108 PC109 PC11 PC110 PC112 PC176 PC133 PC135 PC158 PC159 PC16 PC160 PC161 PC162 PC163 PC164 PC165 PC166 PC167 PC168 PC169 PC17 PC170 PC171 PC172 PC173 PC174 PC157 PC134 PC156 PC154 PC136 PC137 PC138 PC139 PC14 PC140 PC141 PC142 PC143 PC144 PC145 PC146 PC147 PC148 PC149 PC150 PC151 PC152 PC153 PC155 PC99'
$NextNumber = ($List -split "\s" | ForEach-Object { if ($_ -match 'PC(?<Number>\d+)') { $Matches.Number } } | Measure-Object -Maximum).Maximum + 1
$NextNumber
"PC$NextNumber"

How save png as jpg without saving the file in dir

I'm using FromFile to get the image out of files, and it has the following error for the png's on the FromFile line:
Exception calling "FromFile" with "1" argument(s): "The given path's
format is not supported."
So, I'm trying to convert the bmp's to jpg, (see convert line above FromFile below) but all the examples I see (that seem usable) are saving the file. I don't want to save the file in the dir. All I need is the image format, so FromFile can use it like this example. I saw ConvertTo-Jpeg, but I don't think this is a standard powershell module, or don't see how to install it.
I saw this link, but I don't think that would leave the image in the format needed by FromFile.
This is my code:
$imageFile2 = Get-ChildItem -Recurse -Path $ImageFullBasePath -Include #("*.bmp","*.jpg","*.png") | Where-Object {$_.Name -match "$($pictureName)"} #$imageFile | Select-String -Pattern '$($pictureName)' -AllMatches
Write-Host $imageFile2
if($imageFile2.Exists)
{
if($imageFile2 -Match "png")
{
$imageFile2 | .\ConvertTo-Jpeg #I don't think this will work with FromFile below
}
$image = [System.Drawing.Image]::FromFile($imageFile2) step
}
else {
Write-Host "$($imageFile2) does not exist"
}
And then I put it in excel:
$xlsx = $result | Export-Excel -Path $outFilePath -WorksheetName $errCode -Autosize -AutoFilter -FreezeTopRow -BoldTopRow -PassThru # -ClearSheet can't ClearSheet every time or it clears previous data ###left off
$ws = $xlsx.Workbook.Worksheets[$errCode]
$ws.Dimension.Columns #number of columns
$tempRowCount = $ws.Dimension.Rows #number of rows
#only change width of 3rd column
$ws.Column(3).Width
$ws.Column(3).Width = 100
#Change all row heights
for ($row = 2 ;( $row -le $tempRowCount ); $row++)
{
#Write-Host $($ws.Dimension.Rows)
#Write-Host $($row)
$ws.Row($row).Height
$ws.Row($row).Height = 150
#place the image in spreadsheet
#https://github.com/dfinke/ImportExcel/issues/1041 https://github.com/dfinke/ImportExcel/issues/993
$drawingName = "$($row.PictureID)_Col3_$($row)" #Name_ColumnIndex_RowIndex
Write-Host $image
$picture = $ws.Drawings.AddPicture("$drawingName",$image)
$picture.SetPosition($row - 1, 0, 3 - 1, 0)
if($ws.Row($row).Height -lt $image.Height * (375/500)) {
$ws.Row($row).Height = $image.Height * (375/500)
}
if($ws.Column(3).Width -lt $image.Width * (17/120)){
$ws.Column(3).Width = $image.Width * (17/120)
}
}
Update:
I just wanted to reiterate that FromFile can't be used for a png image. So where Hey Scripting Guy saves the image like this doesn't work:
$image = [drawing.image]::FromFile($imageFile2)
I figured out that the $imageFile2 path has 2 filenames in it. It must be that two met the Get-ChildItem/Where-Object/match criteria. The images look identical, but have similar names, so will be easy to process. After I split the names, it does FromFile ok.

Faster iteration

I have this code, which is part of a function that returns a list of SQL rows based on a time range.
The query itself (1st line of code) is quite fast. But the foreach loop that extract the relevant data takes a while to complete.
I have around 350.000 lines to iterate, and despite it's has to take a while, I was wondering if there is any change I could make in order to make it faster.
$SqlDocmasterTableResuls = $this.SqlConnection.GetSqlData("SELECT DOCNUM, DOCLOC FROM MHGROUP.DOCMASTER WHERE ENTRYWHEN between '" + $this.FromDate + "' and '" + $this.ToDate + "'")
[System.Collections.ArrayList]$ListOfDocuments = [System.Collections.ArrayList]::New()
if ($SqlDocmasterTableResuls.Rows.Count)
{
foreach ($Row in $SqlDocmasterTableResuls.Rows)
{
$DocProperties = #{
"DOCNUM" = $Row.DOCNUM
"SOURCE" = $Row.DOCLOC
"DESTINATION" = $Row.DOCLOC -replace ([regex]::Escape($this.iManSourceFileServerName + ":" + $this.iManSourceFileServerPath.ROOTPATH)),
([regex]::Escape($this.iManDestinationFileServerName + ":" + $this.iManDestinationFileServerPath.ROOTPATH))
}
$DocObj = New-Object -TypeName PSObject -Property $DocProperties
$ListOfDocuments.Add($DocObj)
}
return $ListOfDocuments
Avoid appending to an array in a loop. The best way to capture loop data in a variable is to simply collect the loop output in a variable:
$ListOfDocuments = foreach ($Row in $SqlDocmasterTableResuls.Rows) {
New-Object -Type PSObject -Property #{
"DOCNUM" = $Row.DOCNUM
"SOURCE" = $Row.DOCLOC
"DESTINATION" = $Row.DOCLOC -replace ...
}
}
You don't need the surrounding if conditional, because if the table doesn't have any rows the loop should skip right over it, leaving you with an empty result.
Since you want to return the list anyway, you don't even need to collect the loop output in a variable. Just leave the output as it is and it will get returned anyway.
Also avoid repeating operations in a loop when their result doesn't change. Calculate the escaped source and destination paths once before the loop:
$srcPath = [regex]::Escape($this.iManSourceFileServerName + ':' + $this.iManSourceFileServerPath.ROOTPATH)
$dstPath = [regex]::Escape($this.iManDestinationFileServerName + ':' + $this.iManDestinationFileServerPath.ROOTPATH)
and use the variables $srcPath and $dstPath inside the loop.
Something like this should do:
$SqlDocmasterTableResuls = $this.SqlConnection.GetSqlData("SELECT ...")
$srcPath = [regex]::Escape($this.iManSourceFileServerName + ':' + $this.iManSourceFileServerPath.ROOTPATH)
$dstPath = [regex]::Escape($this.iManDestinationFileServerName + ':' + $this.iManDestinationFileServerPath.ROOTPATH)
foreach ($Row in $SqlDocmasterTableResuls.Rows) {
New-Object -Type PSObject -Property #{
'DOCNUM' = $Row.DOCNUM
'SOURCE' = $Row.DOCLOC
'DESTINATION' = $Row.DOCLOC -replace $srcPath, $dstPath
}
}
return
[edit - per Ansgar Wiechers, the PSCO accelerator is only available with ps3+.]
one other thing that may help is to replace New-Object with [PSCustomObject]. that is usually somewhat faster to use. something like this ...
$DocObj = [PSCustomObject]$DocProperties
another way to use that type accelerator is to do what Ansgar Wiechers did in his code sample, but use the accelerator instead of the cmdlet. like this ...
[PSCustomObject]#{
'DOCNUM' = $Row.DOCNUM
'SOURCE' = $Row.DOCLOC
'DESTINATION' = $Row.DOCLOC -replace $srcPath, $dstPath
}
hope that helps,
lee

Change the background color of a Word file via PowerShell

How can I change the background color of a Word file via PowerShell?
$wd = New-Object -COM 'Word.Application'
$wd.Visible = $true # set to $false for production
Get-ChildItem 'C:\1\*.doc' | % {
$doc = $wd.Documents.Open($_.FullName)
# Here's the problem
$doc.Background.Fill.ForeColor.RGB = RGB(192, 192, 192)
# Switch doc view to Online Layout view
$doc.ActiveWindow.View.Type = 6
$doc.Save($true)
$doc.Close()
}
$wd.Quit()
[Runtime.InteropServices.Marshal]::ReleaseComObject($wd)
[GC]::Collect()
[GC]::WaitForPendingFinalizers()
I'm getting 2 errors :
* RGB : The term 'RGB' is not recognized as the name of a cmdlet...
* Cannot find an overload for "Save" and the argument count: "1".
So lets address the couple of issues I know about...
RGB : The term 'RGB' is not recognized as the name of a cmdlet...
Of course you know why that is not working since PowerShell does not have a RGB cmdlet. However if you look at the input for $doc.Background.Fill.ForeColor.RGB it is looking for an integer. Referring to this related article you can see how we can make the transition.
$doc.Background.Fill.ForeColor.RGB = [long](192 + (192* 256) + (192 * 65536))
There is a caveat that needs to be addressed. While the above code does not cause an error you are going to notice that the colour is not displayed on the document. That is because backround colour is not displayed by default. You will need to enable it.
$doc.Background.Fill.Visible = $true
Ok, that one is done now how about....
Cannot find an overload for "Save" and the argument count: "1".
We use Get-Member to see exactly why
PS C:\users\Cameron\Downloads> $doc | gm
TypeName: Microsoft.Office.Interop.Word.DocumentClass
...
RunLetterWizard Method void RunLetterWizard([ref] System.Object LetterContent, [ref] System.Object WizardMode), void _Document.Ru...
Save Method void Save(), void _Document.Save()
SaveAs Method void SaveAs([ref] System.Object FileName, [ref] System.Object FileFormat, [ref] System.Object LockComments...
...
It does not take any parameters so we simply need to remove $true. The other entries are there just to show some other methods do take parameters.
$doc.Save()
.... so .... for the last one
True : The term 'True' is not recognized as the name of a cmdlet...
I did not see that error and will assume it was a typo that didnt get carried over into the code of your question.
Still miss RGB?
A crude PowerShell function that replicates the functionality with a little data validation
Function Get-RGB
{
Param(
[Parameter(Mandatory=$false)]
[ValidateRange(0,255)]
[Int]
$Red = 0,
[Parameter(Mandatory=$false)]
[ValidateRange(0,255)]
[Int]
$Green = 0,
[Parameter(Mandatory=$false)]
[ValidateRange(0,255)]
[Int]
$Blue = 0
)
Process
{
[long]($Red + ($Green * 256) + ($Blue * 65536))
}
}
Example
PS C:\users\Cameron\Downloads> Get-RGB 129 0 54
3539073
To clarify, I'm posting the final script here.
This script will loop through ALL .doc files in the selected path.
I encountered a problem only when the doc files were read-only, So I changed save() to SaveAs([ref]$name) at another folder, and that fixed the problem.
$wd = New-Object -COM 'Word.Application'
$wd.Visible = $true # set to $false for production
Get-ChildItem 'C:\songs\*.doc' | % {
$doc = $wd.Documents.Open($_.FullName)
$doc.Background.Fill.ForeColor.RGB = [long](249 + (232* 256) + (163 * 65536))
$doc.Background.Fill.Visible = $true
# Switch doc view to Online Layout view, otherwise the changes won't appear in normal view
$doc.ActiveWindow.View.Type = 6
# Replace folder path to solve "read-only" problem
$Name=($doc.Fullname).replace("songs","songs_edited")
$doc.SaveAs([ref]$Name)
$doc.Close()
}
$wd.Quit()
[Runtime.InteropServices.Marshal]::ReleaseComObject($wd)
[GC]::Collect()
[GC]::WaitForPendingFinalizers()

Resources