I have a Powershell script that queries AD user certificates. I'd like to check and inform the user if his VPN certificate is going to expire soon. The script runs fine so far, but I have some instances where the user already has 2 VPN certificates and I don't want to notify if this is the case (in Script on step "### Execute next steps if count is less then 2"). I've tried to add ".count" to some of the variables, but since it's within the foreach, it's always giving me a "1" as a match. I have no clue how to achieve this, please help. Here's the script:
param (
[string]$queryDN = '...DC=com',
[string]$VPNcertname = 'VPN OID',
[int]$days = 14
)
# Make decision what to check
$userpath = $queryDN
$cert2check = $VPNcertname
# Begin script
$users = (Get-ADGroupMember -Identity $userpath).distinguishedName
foreach ($dude in $users) {
$user = Get-ADUser $dude -Property Certificates
$Certificatelist = $user.Certificates | foreach {
New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $_
}
ForEach($cert in $Certificatelist){
$querycertificate = if($cert.EnhancedKeyUsageList.Where({$_.FriendlyName -eq $cert2check})){
### Execute next steps if count is less then 2
$expirationDate = $Cert.NotAfter
if ($expirationDate -lt [datetime]::Today.AddDays($days)) {
write-host The $cert2check certificate for user: $user.UserPrincipalName`, expires: $expirationDate. This is less than: $days days and should be changed soon.
}
}
}
}
Thanks
Thanks #Vesper. This was exactly what I was looking for:
$CertList2=$Certificatelist|where {$_.EnhancedKeyUsageList.FriendlyName -eq $cert2check}; if ($CertList2.count -gt 1) {...}
Related
this is my first time on Stack Overflow so please have mercy :)
Im trying to create a Powershell GUI to do a search request on our Active directory that shows me all expired and soon expiring User Accounts. I tried it with the following function.
I get an syntax error in my request (Get-ADUser)...
Error Message in Powershell ISE
$ShowExpiring.Add_Click({
$ADUserSearch.Visible = $False
$CheckDisabled.Visible = $False
$ShowExpiring.Visible = $False
$Back.Visible = $True
$Results.Visible = $True
$Results.Clear()
$Results.ScrollBars = "Vertical"
Import-Module ActiveDirectory
$CurrentDate = Get-Date
$ExpiringPasswords = Get-ADUser -Filter '((PasswordExpired -eq $True) -or (PasswordLastSet -le ((get-date).AddDays(-((get-addefaultdomainpolicy).MaxPasswordAge.Days)))))' -Properties Name,PasswordLastSet
if($ExpiringPasswords) {
$ExpiringPasswords = $ExpiringPasswords | sort PasswordLastSet
foreach ($User in $ExpiringPasswords) {
if ($User.PasswordLastSet -lt (Get-Date).AddDays(-((Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge.Days))) {
$Results.SelectionColor = "Red"
else {
$Results.SelectionColor = "Orange"
}
$Results.AppendText("Username: $($User.Name) Expiration Date: $($User.PasswordLastSet)`n")
}
else {
$Results.AppendText("No passwords expiring or already expired.")
}
})
I also tried it with this code which gives me no error message but also no result from disabled users:
$ShowExpiring.Add_Click({
$ADUserSearch.Visible = $False
$CheckDisabled.Visible = $False
$ShowExpiring.Visible = $False
$Back.Visible = $True
$Results.Visible = $True
$Results.Clear()
$Results.ScrollBars = "Vertical"
Import-Module ActiveDirectory
$CurrentDate = Get-Date
$ExpiringPasswords = (Search-ADAccount -AccountExpired -UsersOnly | select Name, #{n='ExpirationDate';e={[datetime]::FromFileTime($_.AccountExpirationDate)}}) + (Search-ADAccount -AccountExpiring -TimeSpan (New-TimeSpan -Days 10) -UsersOnly | select Name, #{n='ExpirationDate';e={[datetime]::FromFileTime($_.AccountExpirationDate)}})
if($ExpiringPasswords) {
$ExpiringPasswords = $ExpiringPasswords | sort ExpirationDate
foreach ($User in $ExpiringPasswords) {
if ($User.ExpirationDate -lt $CurrentDate) {
$Results.SelectionColor = "Red"
else {
$Results.SelectionColor = "Orange"
}
$Results.AppendText("Username: $($User.Name) Expiration Date: $($User.ExpirationDate)`n")
}
else {
$Results.AppendText("No passwords expiring or already expired.")
}
})
Thank you for helping me.
The reason for your syntax error is likely the fact that you are trying to use
Get-ADDefaultDomainPolicy
...which does not exist. What you're looking for is
Get-ADDefaultDomainPasswordPolicy
Here is some code that you should substitute in the appropriate place in your first example. I broke things down a little bit to make the code/filter easier to understand, but you can recombine it if you are so inclined to do so.
$MaxPasswordAgeDays = $(Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge.Days
$OldestAcceptablePasswordLastSetDate = $(Get-Date).AddDays(-$MaxPasswordAgeDays)
$ExpiringPasswords = Get-ADUser -Filter {PasswordExpired -eq $True -or PasswordLastSet -le $OldestAcceptablePasswordLastSetDate} -Properties Name,PasswordLastSet
I would suggest using { } instead of single quotes ' ' so that your Powershell editor can help you with intellisense and syntax highlighting rather than the single quotes in your example. Aside from that, if you ever encounter syntax errors, I would recommend trying to break it down as I did above to help you understand which part of your code (in this case, your filter) is failing. I discovered rather quickly that you were trying to use a non-existent cmdlet by doing so.
Abstract
So I work for a company that has roughly 10k computer assets on my domain. My issue is the time it takes to query if a user exists on a computer to see if they've ever logged into said computer. We need this functionality for audits in case they've done something they shouldn't have.
I have two methods in mind I've researched to complete this task, and a third alternative solution I have not thought of;
-Method A: Querying every computer for the "C:\Users<USER>" to see if LocalPath exists
-Method B: Checking every computer registry for the "HKU:<SID>" to see if the SID exists
-Method C: You are all smarter than me and have a better way? XD
Method A Function
$AllCompFound = #()
$AllADComputer = Get-ADComputer -Properties Name -SearchBase "WhatsItToYa" -filter 'Name -like "*"' | Select-Object Name
ForEach($Computer in $AllADComputers) {
$CName = $Computer.Name
if (Get-CimInstance -ComputerName "$CName" -ClassName Win32_Profile | ? {"C:\Users\'$EDIPI'" -contains $_.LocalPath}) {
$AllCompFound += $CName
} else {
#DOOTHERSTUFF
}
}
NOTE: I have another function that prompts me to enter a username to check for. Where I work they are numbers so case sensitivity is not an issue. My issue with this function is I believe it is the 'if' statement returns true every time because it ran rather than because it matched the username.
Method B Function
$AllCompFound = #()
$AllADComputer = Get-ADComputer -Properties Name -SearchBase "WhatsItToYa" -filter 'Name -like "*"' | Select-Object Name
$hive = [Microsoft:Win32.RegistryHive]::Users
ForEach($Computer in $AllADComputers) {
try {
$base = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($hive, $Computer.Name)
$key = &base.OpenSubKey($strSID)
if ($!key) {
#DOSTUFF
} else {
$AllCompFound += $Computer.Name
#DOOTHERSTUFF
}
} catch {
#IDONTTHROWBECAUSEIWANTITTOCONTINUE
} finally {
if($key) {
$key.Close()
}
if ($base) {
$base.Close()
}
}
}
NOTE: I have another function that converts the username into a SID prior to this function. It works.
Where my eyes start to glaze over is using Invoke-Command and actually return a value back, and whether or not to run all of these queries as their own PS-Session or not. My Method A returns false positives and my Method B seems to hang up on some computers.
Neither of these methods are really fast enough to get through 10k results, I've been using smaller pools of computers in order to get test these results when requested. I'm by no means an expert, but I think I have a good understanding, so any help is appreciated!
First, use WMI Win32_UserProfile, not C:\Users or registry.
Second, use reports from pc to some database, not from server to pc. This is much better usually.
About GPO: If you get access, you can Add\Remove scheduled task for such reports through GPP (not GPO) from time to time.
Third: Use PoshRSJob to make parallel queries.
Get-WmiObject -Class 'Win32_USerProfile' |
Select #(
'SID',
#{
Name = 'LastUseTime';
Expression = {$_.ConvertToDateTime($_.LastUseTime)}}
#{
Name = 'NTAccount';
Expression = { [System.Security.Principal.SecurityIdentifier]::new($_.SID).Translate([System.Security.Principal.NTAccount])}}
)
Be careful with translating to NTAccount: if SID does not translates, it will cause error, so, maybe, it's better not to collect NTAccount from user space.
If you have no other variants, parallel jobs using PoshRSJob
Example for paralleling ( maybe there are some typos )
$ToDo = [System.Collections.Concurrent.ConcurrentQueue[string]]::new() # This is Queue (list) of computers that SHOULD be processed
<# Some loop through your computers #>
<#...#> $ToDo.Enqueue($computerName)
<#LoopEnd#>
$result = [System.Collections.Concurrent.ConcurrentBag[Object]]::new() # This is Bag (list) of processing results
# This function has ComputerName on input, and outputs some single value (object) as a result of processing this computer
Function Get-MySpecialComputerStats
{
Param(
[String]$ComputerName
)
<#Some magic#>
# Here we make KSCustomObject form Hashtable. This is result object
return [PSCustomObject]#{
ComputerName = $ComputerName;
Result = 'OK'
SomeAdditionalInfo1 = 'whateverYouWant'
SomeAdditionalInfo2 = 42 # Because 42
}
}
# This is script that runs on background. It can not output anything.
# It takes 2 args: 1st is Input queue, 2nd is output queue
$JobScript = [scriptblock]{
$inQueue = [System.Collections.Concurrent.ConcurrentQueue[string]]$args[0]
$outBag = [System.Collections.Concurrent.ConcurrentBag[Object]]$args[1]
$compName = $null
# Logging inside, if you need it
$log = [System.Text.StringBuilder]::new()
# we work until inQueue is empty ( then TryDequeue will return false )
while($inQueue.TryDequeue([ref] $compName) -eq $true)
{
$r= $null
try
{
$r = Get-MySpecialComputerStats -ComputerName $compName -EA Stop
[void]$log.AppendLine("[_]: $($compName) : OK!")
[void]$outBag.Add($r) # We append result to outBag
}
catch
{
[void]$log.AppendLine("[E]: $($compName) : $($_.Exception.Message)")
}
}
# we return log.
return $log.ToString()
}
# Some progress counters
$i_max = $ToDo.Count
$i_cur = $i_max
# We start 20 jobs. Dont forget to say about our functions be available inside job
$jobs = #(1..20) <# Run 20 threads #> | % { Start-RSJob -ScriptBlock $JobScript -ArgumentList #($ToDo, $result) -FunctionsToImport 'Get-MySpecialComputerStats' }
# And once per 3 seconds we check, how much entries left in Queue ($todo)
while ($i_cur -gt 0)
{
Write-Progress -Activity 'Working' -Status "$($i_cur) left of $($i_max) computers" -PercentComplete (100 - ($i_cur / $i_max * 100))
Start-Sleep -Seconds 3
$i_cur = $ToDo.Count
}
# When there is zero, we shall wait for jobs to complete last items and return logs, and we collect logs
$logs = $jobs | % { Wait-RSJob -Job $_ } | % { Receive-RSJob -Job $_ }
# Logs is LOGS, not result
# Result is in the result variable.
$result | Export-Clixml -Path 'P:/ath/to/file.clixml' # Exporting result to CliXML file, or whatever you want
Please be careful: there is no output inside $JobScript done, so it must be perfectly done, and function Get-MySpecialComputerStats must be tested on unusual ways to return value that can be interpreted.
So I'm trying to output a complete KB list for all computers on a server (which works on one computer) but it doesn't recognize Get-ADcomputer as a cmdlet. When checking various sources, it appears that the AD module isn't included. As I'm doing this on a work computer/server I'm hesitant to download anything or anything of that nature.
Is there any way I can achieve the following without using the AD module or someway I might be missing how to import the module (if it exists, which I don't think it does on this system)?
# 1. Define credentials
$cred = Get-Credential
# 2. Define a scriptblock
$sb = {
$Session = New-Object -ComObject Microsoft.Update.Session
$Searcher = $Session.CreateUpdateSearcher()
$HistoryCount = $Searcher.GetTotalHistoryCount()
$Searcher.QueryHistory(0,$HistoryCount) | ForEach-Object -Process {
$Title = $null
if ($_.Title -match "\(KB\d{6,7}\)") {
# Split returns an array of strings
$Title = ($_.Title -split '.*\((?<KB>KB\d{6,7})\)')[1]
} else {
$Title = $_.Title
}
$Result = $null
switch ($_.ResultCode) {
0 { $Result = 'NotStarted'}
1 { $Result = 'InProgress' }
2 { $Result = 'Succeeded' }
3 { $Result = 'SucceededWithErrors' }
4 { $Result = 'Failed' }
5 { $Result = 'Aborted' }
default { $Result = $_ }
}
New-Object -TypeName PSObject -Property #{
InstalledOn = Get-Date -Date $_.Date;
Title = $Title;
Name = $_.Title;
Status = $Result
}
} | Sort-Object -Descending:$false -Property InstalledOn | Where {
$_.Title -notmatch "^Definition\sUpdate"
}
}
#Get all servers in your AD (if less than 10000)
Get-ADComputer -ResultPageSize 10000 -SearchScope Subtree -Filter {
(OperatingSystem -like "Windows*Server*")
} | ForEach-Object {
# Get the computername from the AD object
$computer = $_.Name
# Create a hash table for splatting
$HT = #{
ComputerName = $computer ;
ScriptBlock = $sb ;
Credential = $cred;
ErrorAction = "Stop";
}
# Execute the code on remote computers
try {
Invoke-Command #HT
} catch {
Write-Warning -Message "Failed to execute on $computer because $($_.Exception.Message)"
}
} | Format-Table PSComputerName,Title,Status,InstalledOn,Name -AutoSize
You've got 3 options:
First is to just install the RSAT feature for AD which will include the AD module. This is probably the best option unless there is something specific preventing it. If you're running your script from a client operating systems you need to install the RSAT first, though.
Option 2 (which should only be used if adding the Windows feature is somehow an issue) is to download and use the Quest AD tools, which give very similar functionality, but it looks like Dell is doing their best to hide these now so that may be difficult to locate...
Option 3 is to use the .NET ADSI classes to access AD directly, which will work without any additional downloads on any system capable of running PowerShell. If you'd like to go this route you should check out the documentation for the interface Here and for the System.DirectoryServices namespace Here.
Edit
Just noticed the last part of your question, what do you mean by "a complete KB list"? Not just Windows updates or things updated manually or whatever? What else would be in a list of Windows updates that was not a Windows update?
You have not mentioned the OSes you are using but in general if you have a server 2008 R2 or above, all you have to do it activate the RSAT feature AD PowerShell Module and you will have the cmdlet you are looking for.
On a client machine, you 'have to' install RSAT, and then activate the features. You can take a look at the technet article for more info: https://technet.microsoft.com/en-us/library/ee449483(v=ws.10).aspx
If you don't want to use that option, then you will have to use .NET ADSI classes. There are tons of examples on how to do this, it basically boils down to a couple of lines really. Technet has examples on this as well: https://technet.microsoft.com/en-us/library/ff730967.aspx
I have an issue which is to do with AD replication. We use a 3rd party app the create accounts in AD and then a powershell script (called by the app) to create the exchange accounts.
In the 3rd party app we can not tell which GC the ad account has been created on and therefore have to wait 20 minutes for replication to happen.
What I am trying to do is find which GC the account has been created on or is replicated to and connect to that server using....
set-adserversettings -preferredserver $ADserver
I currently have the below script and what I can't work out is to get it to stop when it finds the account and assign that GC to the $ADserver variable. The write-host line is only there for testing.
$ForestInfo = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
$GCs = $ForestInfo.FindAllGlobalCatalogs()
Import-module activedirectory
ForEach ($GC in $GCs)
{
Write-Host $GC.Name
Get-aduser $ADUser
}
TIA
Andy
You can check whether Get-ADUser returns more than 0 objects to determine whether the GC satisfied your query. After that, use Set-ADServerSettings -PreferredGlobalCatalog to configure the preference
You will need to specify that you want to search the Global Catalog and not just the local directory. The Global Catalog is accessible from port 3268 on the DC, so it becomes something like:
$ForestInfo = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
$GCs = $ForestInfo.FindAllGlobalCatalogs()
Import-module ActiveDirectory
$ADUserName = "someusername"
$ADDomainDN = "DC=child,DC=domain,DC=tld"
$FinalGlobalCatalog = $null
foreach ($GC in $GCs)
{
$GCendpoint = "{0}:3268" -f $GC.Name
$SearchResult = Get-ADUser -LDAPFilter "(&(samaccountname=$ADUserName))" -Server $GCEndpoint -SearchBase $ADDomainDN -ErrorAction SilentlyContinue
if(#($SearchResult).Count -gt 0){
$FinalGlobalCatalog = $GC
break
}
}
if($FinalGlobalCatalog){
Write-Host "Found one: $($FinalGlobalCatalog.Name)"
Set-ADServerSettings -PreferredGlobalCatalog $FinalGlobalCatalog.Name
} else {
Write-Host "Unable to locate GC replica containing user $ADUserName"
}
We have a WSUS server, and four computer groups (Alpha, Beta, Production, Workstations). Our patching process has us approve all "Not Approved" patches for the Alpha group, right after they're released by Microsoft. One week later, we approve all of the updates from the previous week, for the Beta group. One week later, we do the same for Production.
I'm writing a script (which I can't test until next week), and wonder if there's a better way to get the list of updates that are approved for Alpha. Here is the code:
$updateScope = New-Object Microsoft.UpdateServices.Administration.UpdateScope
$updateScope.ApprovedStates = [Microsoft.UpdateServices.Administration.ApprovedStates]::LatestRevisionApproved
$updateScope.FromArrivalDAte = (Get-Date).AddMonths(-1)
$wsusGroup = $wsus.GetComputerTargetGroups() | Where {$_.Name -eq "$PatchingGroup"}
$updateScope
$updateScope.getType()
$updateScope.count
$updateScope.ApprovedComputerTargetGroups.add($wsusGroup)
$wsus.GetUpdates($updateScope)
$Updates = $wsus.GetUpdates($updateScope)
I assume I can take the $Updates variable and do the following for the Beta and Production groups:
Foreach ($update in $updates) {
$update.Approve(“Install”,$PatchingGroup)
}
Is this going to work, and is there a better way?
Here is the code I ended-up using. It works, but I can't help feeling that there is a better way.
<#
.Synopsis
Approve WSUS updates for installation.
.DESCRIPTION
This script takes the name of a WSUS approval group, and approves updates based on their age.
.NOTES
Author: Mike Hashemi
V1 date: 24 Feb 2014
.LINK
.PARAMETER PrimaryWSUSServer
Default value: server.domain.local. This parameter specifies the DNS name of the primary WSUS server.
.PARAMETER PatchingGroup
Manadatory parameter. Valid values are 'Alpha','Beta','Production','Excluded','Workstations','COC-OMI-WORKSTATIONS'. The value of this parameter determines what patching groups will have updates approved for installation. Multiple groups can be entered at once, unless one of the is Alpha
.EXAMPLE
.\manageWSUSUpdates-Parameterized.ps1 -PatchingGroup Alpha
In this example, the script will approve all updates with an approval status not equal to 'IsDeclined', for installation to servers in the Alpha group.
.EXAMPLE
.\manageWSUSUpdates-Parameterized.ps1 -PatchingGroup Beta
In this example, the script will get the list of updates approved for the Alpha group, in the last three months (from the date the script is run), and will approve them for installation to servers in the Beta group.
#>
[CmdletBinding()]
param(
[string]$PrimaryWSUSServer = “server.domain.local”,
[Parameter(Mandatory=$True)]
[ValidateSet('Alpha','Beta','Production','Excluded','Workstations','COC-OMI-WORKSTATIONS')]
[string[]]$PatchingGroup
)
#Initialize variables
$BeginScriptTime = Get-Date
# Load the Required .NET assembly
[void][reflection.assembly]::LoadWithPartialName(“Microsoft.UpdateServices.Administration”)
$wsus = [Microsoft.UpdateServices.Administration.AdminProxy]::getUpdateServer($PrimaryWSUSServer,$False)
Function Approve-AlphaPatches {
#Get the list of all updates that are not declined.
$unapprovedUpdates = $wsus.getupdates() | where {$_.isdeclined -ne $true}
#If an update has a license agreement, accept it
$license = $unapprovedUpdates | where {$_.RequiresLicenseAgreementAcceptance}
$license | ForEach {$_.AcceptLicenseAgreement()}
#Get members of Alpha patching group.
$installGroup = $wsus.GetComputerTargetGroups() | where {$_.Name -eq $PatchingGroup}
#Approve updates for the Beta group.
Foreach ($update in $unapprovedUpdates) {
$update.Approve(“Install”,$installGroup)
}
}
Function Approve-NonAlphaPatches {
Foreach ($group in $PatchingGroup) {
#Get the updates that have arrived in the last three months.
$updateScope = New-Object Microsoft.UpdateServices.Administration.UpdateScope
$updateScope.ApprovedStates = [Microsoft.UpdateServices.Administration.ApprovedStates]::LatestRevisionApproved
$updateScope.FromArrivalDAte = (Get-Date).AddMonths(-3)
#Get the updates approved for the Alpha group.
$alphaGroup = $wsus.GetComputerTargetGroups() | Where {$_.Name -eq 'Alpha'}
$updateScope.ApprovedComputerTargetGroups.add($alphaGroup)
$Updates = $wsus.GetUpdates($updateScope)
#Get members of Alpha patching group.
$installGroup = $wsus.GetComputerTargetGroups() | where {$_.Name -eq $group}
#Approve updates for the user-specified patching group.
Foreach ($update in $updates) {
$update.Approve(“Install”,$installGroup)
}
}
}
#Begin Script
If (($PatchingGroup.Count -gt 1) -and ($PatchingGroup -ccontains 'Alpha')) {
Write-Error ("This script cannot approve Alpha patches with other patching groups. If you want to approve more groups at the same time, please approve the rest in a second execution of the script.")
Return
}
Else {
If ($PatchingGroup -eq 'Alpha') {
Approve-AlphaPatches
}
Else {
Approve-NonAlphaPatches
}
}
I couldn't see any reference to a specific version of PowerShell in your original post, but is it possible that the Windows 8.1 / Windows Server 2012 R2 WSUS module would achieve your goal?
There is a function called Approve-WsusUpdate, and it has a -TargetGroupName parameter.
http://technet.microsoft.com/en-us/library/hh826164.aspx
If you are not using Windows 8.1 and PowerShell version 4.0, then forgive my ignorance.
I added an exclude list to prevent to re-enable disabled updates:
#Load KBs to exclude
$pattern = '[^0-9]'
if(Test-Path ($PSScriptRoot + '\exclude.csv')){
$exclude = #(Import-Csv ($PSScriptRoot + '\exclude.csv') -Delimiter ';' -Encoding UTF8 | SELECT KBArticle)
}
#Approve updates for the Beta group.
Foreach ($update in $unapprovedUpdates) {
if (($exclude -eq $null) -or ($exclude | where {($_.KBArticle -replace $pattern, '') -ne $update.KnowledgebaseArticles} )){
$update.Approve(“Install”,$installGroup)
}
}
the exclude.csv is like follow:
KBArticle
KB4011052