Quantcast
Channel: PowerShell – Indented!
Viewing all 23 articles
Browse latest View live

DnsShell – alpha release

$
0
0

DnsShell is my PowerShell module intended to help administer MS DNS. This is an alpha relaese, it may contain bugs, it should be tested in a safe environment first.

The module is currently available on MSDN.

The following CmdLets are implemented at this stage:

  • Get-Dns
  • Clear-DnsCache
  • Get-DnsRecord
  • Get-DnsServer
  • Get-DnsZone
  • New-DnsRecord
  • New-DnsZone
  • Remove-DnsObject
  • Set-DnsRecord
  • Get-ADDnsRecord

I am in the process of writing detailed CmdLet help, it will be made available as soon as possible.


DnsShell: Get-AD*

$
0
0

A new version of DnsShell has been released, this release improves Get-ADDnsRecord and adds Get-ADDnsPartition and Get-ADDnsZone.

The updated release is available on MSDN as version 0.2.0.

code.msdn.microsoft.com/dnsshell

Basic help is available for each of the new CmdLets.

DnsShell: Zone and Server CmdLets

$
0
0

After fixing a couple of authentication bugs with Set-DnsRecord and New-DnsRecord I decided to make an attempt to finish off some of the zone and server CmdLets.

The module now contains these additional CmdLets.

  • Refresh-DnsZone – Implements the ForceRefresh method for Secondary Zones
  • Reload-ADDnsZone – Implements the UpdateFromDS method for AD Integrated Zones
  • Reload-DnsZone
  • Reset-DnsZoneType – Implements the ChangeZoneType method
  • Resume-DnsZone
  • Set-DnsZoneTransfer – Implements the ResetSecondaries method
  • Start-DnsScavenging
  • Start-DnsService
  • Stop-DnsService
  • Suspend-DnsZone – Implements the Pause method
  • Update-DnsZoneFile

As usual, the module can be downloaded from code.msdn.microsoft.com/dnsshell.

NetShell

$
0
0

It’s been a while since I’ve posted, and since it’s almost Christmas I thought I’d better get on with it.

Without further ado I want to post NetShell (I’m not very good at coming up with imaginative names). NetShell is a collection of 17 functions and a few supporting functions in a script module.

Download NetShell

Installation is a manual process, but not too hard. Open up Documents\WindowsPowerShell\Modules, extract the ZIP file. Make sure it includes the NetShell folder or it won’t work. The module is not currently signed, something else on the to-do list. Once it’s there, Import-Module NetShell and off you go.

It includes the following:

ConvertTo-BinaryIP Converts an IP address into a binary string
ConvertTo-Byte A supporting function, a simple conversion of a string to a byte array (ASCII encoding)
ConvertTo-DecimalIP Converts an IP address to 32-bit decimal number
ConvertTo-DottedDecimalIP Converts a binary or 32-bit decimal back to an IP
ConvertTo-Mask Converts from a mask length. For example, gets you from 22 to 255.255.252.0
ConvertTo-MaskLength Converts from the IP form of a mask to the length
ConvertTo-String Another supporting function, converts a byte array to a string (ASCII encoding)
Get-BroadcastAddress Returns the broadcast address for the specified IP address and subnet mask
Get-NetworkAddress Returns the network address for the specified IP address and subnet mask
Get-NetworkRange Returns every IP within the specified range
Get-NetworkSummary Everything about an IP address and mask I considered useful
New-DhcpDiscoverPacket A supporting function for Send-DhcpDiscover. Creates the packet to send (a large byte array)
New-Socket Creates an instance of System.Net.Sockets.Socket, an arbitrary network socket to do with as you please.
New-SysLogDateTime A supporting function to create a DateTime string in the format SysLog likes.
Read-DhcpOption A supporting function to read off an Option from a DHCP packet. Needs to be fed the Extended.BinaryReader class at the top of the module.
Read-DhcpPacket Creates and uses an instance of Extended.BinaryReader to process a DHCP packet and translate the fields.
Receive-Bytes Receives a stream of bytes from the network using a socket
Remove-Socket Cleans up after New-Socket
Send-Bytes Sends an arbitrary byte array over the network using a socket
Send-DhcpDiscover Creates and sends a DHCPDISCOVER packet, then processes and returns the response
Start-Syslog Starts a SysLog server. No termination for this one at the moment. Needs a bit more work.
Test-Smtp Does the SMTP test you normally wind up doing with telnet, returning all the results along with the SMTP banner.
Test-SysLogDateTime A supporting function to check the format of a DateTime that may or may not be present in a SysLog message.
Test-SysLogPRI A supporting function to test of the PRI value in a SysLog message.
Test-TcpPort Returns a boolean indicating whether or not the port connection succeeded.

Get-LocalGroupMember

$
0
0

A quick PowerShell 2 function for a Monday evening. A function to return properties from the WinNT provider, most commonly used to enumerate local group membership.

This should also work for domain groups (using the domain as the SystemName), although if the domain is Active Directory the LDAP provider returns far more information.

The function can be copied into the prompt as is, or made into a script by dropping the opening “Function …” and closing }.

Once it’s there, you can see usage with:

Get-Help Get-LocalGroupMember
Get-Help Get-LocalGroupMember -Full
Get-Help Get-LocalGroupMember -Examples

And the function itself:

Function Get-LocalGroupMember {
  <#
    .Synopsis
      This function returns members of a local group, by default the 
      Administrators group.
    .Description
      Get-LocalGroupMember returns the members of a local group, 
      including all properties exposed by the WinNT provider. Complex 
      properties are returned in their raw form, additional work is needed
      to make sense of many.
    .Parameter SystemName
      The name of the system to execute against. By default, the function 
      uses the local system.
    .Parameter Name
      The group name to extract membership from. By default, the function 
      uses Administrators.
    .Parameter Properties
      A list of properties to return. Refer to the default value for a list of 
      permissible properties.
    .Example
      Get-LocalGroupMember | Select-Object SystemName, Name, Class, Description

      Return members of the administrator group on the local computer.
    .Example
      Get-Content ServerList.txt | ForEach-Object { Get-LocalGroupMember $_ }

      Return members of the administrator group all computers names in 
      a text file.
    .Example
      Get-QADComputer -OperatingSystem "Windows 7*" | Get-LocalGroupMember

      Return members of the administrator group for all computers running 
      Windows 7 in Active Directory
  #>

  Param(
    [Parameter(ValueFromPipelineByPropertyName = $True)]
    [Alias("DnsHostName")]
    [String]$SystemName = $Env:ComputerName,

    [Alias("Group")]
    [String]$Name = "Administrators",

    [String[]]$Properties = @(
      "AccountDisabled", "AccountExpirationDate", "AdsPath", 
      "BadLoginAddress", "BadLoginCount", "Class", "Department",
      "Description", "Division", "EmailAddress", "EmployeeID", 
      "FaxNumber", "FirstName", "FullName", "GraceLoginsAllowed", 
      "GraceLoginsRemaining", "GUID", "HomeDirectory", "HomePage", 
      "IsAccountLocked", "Languages", "LastFailedLogin", "LastLogin", 
      "LastLogoff", "LastName", "LoginHours", "LoginScript", "LoginWorkstations", 
      "Manager", "MaxLogins", "MaxStorage", "Name", "NamePrefix", 
      "NameSuffix", "OfficeLocations", "OtherName", "Parent", 
      "PasswordExpirationDate", "PasswordLastChanged", "PasswordMinimumLength",
      "PasswordRequired", "Picture", "PostalAddresses", "PostalCodes", "Profile",
      "RequireUniquePassword", "Schema", "SeeAlso", "TelephoneHome", 
      "TelephoneMobile", "TelephoneNumber", "TelephonePager", "Title")
  )

  Process {
    $Select = $Properties | ForEach-Object {    
      Invoke-Expression "@{n='$_';e={ 
        `$_.GetType().InvokeMember('$_', 'GetProperty', `$Null, `$_, `$Null) }}"
    }  
  
    ([ADSI]"WinNT://$SystemName/$Name").Members() | 
      Select-Object ([Array](@{n='SystemName';e={ $SystemName }}) + $Select)
  }
}

More IPv4 subnet maths with PowerShell

$
0
0

I’ve been extended and upgrading my library of network functions for some time now, I thought I’d post the functions that work directly with my original collection.

These functions are also included in my NetShell module, I need to post an update to that very soon as well.

ConvertTo-HexIP

Sometimes being able to convert to a hexadecimal format is useful, this little function does exactly that. The body is very simple; it uses the built in formatters.

Function ConvertTo-HexIP {
  <#
    .Synopsis
      Converts a dotted decimal IP address into a hexadecimal string.
    .Description
      ConvertTo-HexIP takes a dotted decimal IP and returns a single hexadecimal string value.
    .Parameter IPAddress
      An IP Address to convert.
  #>

  [CmdLetBinding()]
  Param(
    [Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)]
    [Net.IPAddress]$IPAddress
  )

  Process {
    "$($IPAddress.GetAddressBytes() | ForEach-Object { '{0:x2}' -f $_ })" -Replace '\s'
  }
}

ConvertFrom-HexIP

And back again, using System.Convert.ToUInt32 with a base-16 value to get to UInt32, then a call to ConvertTo-DottedDecimalIP (from the original set).

Function ConvertFrom-HexIP {
  <#
    .Synopsis
      Converts a hexadecimal IP address into a dotted decimal string.
    .Description
      ConvertFrom-HexIP takes a hexadecimal string and returns a dotted decimal IP address.
    .Parameter IPAddress
      An IP Address to convert.
  #>

  [CmdLetBinding()]
  Param(
    [Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)]
    [ValidatePattern('^[0-9a-f]{8}$')]
    [String]$IPAddress
  )

  Process {
    ConvertTo-DottedDecimalIP ([Convert]::ToUInt32($IPAddress, 16))
  }
}

Get-NetworkSummary

Get-NetworkSummary gets a bit of an improvement.

Function Get-NetworkSummary {
  <#
    .Synopsis
      Generates a summary of a network range
    .Description
      Get-NetworkSummary uses most of the IP conversion CmdLets to provide a summary of a network 
      range from any IP address in the range and a subnet mask.
    .Parameter IPAddress
      Any IP address within the network range.
    .Parameter Network
      A network description in the format 1.2.3.4/24
    .Parameter SubnetMask
      The subnet mask for the network.
  #>

  [CmdLetBinding(DefaultParameterSetName = "IPAndMask")]
  Param(
    [Parameter(Mandatory = $True, Position = 0, ParameterSetName = "IPAndMask", ValueFromPipeline = $True)]
    [Net.IPAddress]$IPAddress,
    
    [Parameter(Mandatory = $True, Position = 1, ParameterSetName = "IPAndMask")]
    [Alias("Mask")]
    [Net.IPAddress]$SubnetMask,
    
    [Parameter(Mandatory = $True, ParameterSetName = "CIDRNotation", ValueFromPipeline = $True)]
    [ValidatePattern('^(\d{1,3}\.){3}\d{1,3}[\\/]\d{1,2}$')]
    [String]$Network
  )

  Process {
    If ($PsCmdLet.ParameterSetName -eq 'CIDRNotation') {
      $Temp = $Network.Split("\/")
      $IPAddress = $Temp[0]
      $SubnetMask = ConvertTo-Mask $Temp[1]
    }

    $DecimalIP = ConvertTo-DecimalIP $IPAddress
    $DecimalMask = ConvertTo-DecimalIP $SubnetMask
    $DecimalNetwork =  $DecimalIP -BAnd $DecimalMask
    $DecimalBroadcast = $DecimalIP -BOr ((-BNot $DecimalMask) -BAnd [UInt32]::MaxValue)
  
    $NetworkSummary = New-Object PSObject -Property @{
      "NetworkAddress"   = (ConvertTo-DottedDecimalIP $DecimalNetwork);
      "NetworkDecimal"   = $DecimalNetwork
      "BroadcastAddress" = (ConvertTo-DottedDecimalIP $DecimalBroadcast);
      "BroadcastDecimal" = $DecimalBroadcast
      "Mask"             = $SubnetMask;
      "MaskLength"       = (ConvertTo-MaskLength $SubnetMask);
      "MaskHexadecimal"  = (ConvertTo-HexIP $SubnetMask);
      "HostRange"        = "";
      "NumberOfHosts"    = ($DecimalBroadcast - $DecimalNetwork - 1);
      "Class"            = "";
      "IsPrivate"        = $False}

    If ($NetworkSummary.MaskLength -lt 31) {
      $NetworkSummary.HostRange = [String]::Format("{0} - {1}",
        (ConvertTo-DottedDecimalIP ($DecimalNetwork + 1)),
        (ConvertTo-DottedDecimalIP ($DecimalBroadcast - 1)))
    }
  
    Switch -RegEx ($(ConvertTo-BinaryIP $IPAddress)) {
      "^1111"              { $NetworkSummary.Class = "E" }
      "^1110"              { $NetworkSummary.Class = "D" }
      "^11000000.10101000" { $NetworkSummary.Class = "C"; $NetworkSummary.IsPrivate = $True }
      "^110"               { $NetworkSummary.Class = "C" }
      "^10101100.0001"     { $NetworkSummary.Class = "B"; $NetworkSummary.IsPrivate = $True }
      "^10"                { $NetworkSummary.Class = "B" }
      "^00001010"          { $NetworkSummary.Class = "A"; $NetworkSummary.IsPrivate = $True }
      "^0"                 { $NetworkSummary.Class = "A" }
    }   
  
    Return $NetworkSummary
  }
}

Get-NetworkRange

And so does Get-NetworkRange.

Function Get-NetworkRange {
  <#
    .Synopsis
      Generates IP addresses within the specified network.
    .Description
      Get-NetworkRange finds the network and broadcast address as decimal values then starts a 
      counter between the two, returning Net.IPAddress for each.
    .Parameter IPAddress
      Any IP address within the network range.
    .Parameter Network
      A network description in the format 1.2.3.4/24
    .Parameter SubnetMask
      The subnet mask for the network.
  #>

  [CmdLetBinding(DefaultParameterSetName = "IPAndMask")]
  Param(
    [Parameter(Mandatory = $True, Position = 0, ParameterSetName = "IPAndMask", ValueFromPipeline = $True)]
    [Net.IPAddress]$IPAddress, 
    
    [Parameter(Mandatory = $True, Position = 1, ParameterSetName = "IPAndMask")]
    [Alias("Mask")]
    [Net.IPAddress]$SubnetMask,
    
    [Parameter(Mandatory = $True, ParameterSetName = "CIDRNotation", ValueFromPipeline = $True)]
    [ValidatePattern('^(\d{1,3}\.){3}\d{1,3}[\\/]\d{1,2}$')]
    [String]$Network
  )

  Process {
    If ($PsCmdLet.ParameterSetName -eq 'CIDRNotation') {
      $Temp = $Network.Split("\/")
      $IPAddress = $Temp[0]
      $SubnetMask = ConvertTo-Mask $Temp[1]
    }

    $DecimalIP = ConvertTo-DecimalIP $IPAddress
    $DecimalMask = ConvertTo-DecimalIP $SubnetMask
  
    $DecimalNetwork = $DecimalIP -BAnd $DecimalMask
    $DecimalBroadcast = $DecimalIP -BOr ((-BNot $DecimalMask) -BAnd [UInt32]::MaxValue)

    For ($i = $($DecimalNetwork + 1); $i -lt $DecimalBroadcast; $i++) {
      ConvertTo-DottedDecimalIP $i
    }
  }
}

Get-Subnets

I ran into a need to calculate a list of subnets of a specific length within a super-net, this function does just that.

Function Get-Subnets {
  <#
    .Synopsis
      Generates a list of subnets for a given network range
    .Description
      Generates a list of subnets for a given network range using either the address class or a 
      user-specified value.
    .Parameter NetworkAddress
      Any address in the supernet range.
    .Parameter SubnetMask
      The desired mask, determines the size of the resulting subnet. Must be a valid subnet mask.
    .Parameter SupernetLength
      By default Get-Subnets uses the address class to determine the size of the supernet. Where the 
      supernet describes the range of addresses being split.
  #>

  [CmdLetBinding(DefaultParameterSetName = "PS1")]
  Param(
    [Parameter(Mandatory = $True, Position = 0, ParameterSetName = "IPAndMask", ValueFromPipeline = $True)]
    [Net.IPAddress]$IPAddress, 
    
    [Parameter(Mandatory = $True, Position = 1, ParameterSetName = "IPAndMask")]
    [Alias("Mask")]
    [Net.IPAddress]$SubnetMask,
    
    [Parameter(Mandatory = $True, ParameterSetName = "CIDRNotation", ValueFromPipeline = $True)]
    [ValidatePattern('^(\d{1,3}\.){3}\d{1,3}[\\/]\d{1,2}$')]
    [String]$Network,

    [ValidateRange(1, 32)]
    [UInt32]$SupernetLength
  )

  Process {
    If ($PsCmdLet.ParameterSetName -eq 'CIDRNotation') {
      $Temp = $Network.Split("\/")
      $IPAddress = $Temp[0]
      $SubnetMask = ConvertTo-Mask $Temp[1]
    } Else {
      $SubnetLength = ConvertTo-MaskLength $SubnetMask
    }

    If (!$SupernetLength) {
      $SupernetLength = Switch -RegEx ($(ConvertTo-BinaryIP $IPAddress)) {
        "^110"  { 24 }
        "^10"   { 16 }
        "^0"    { 8 }
        default { 24 }
      }
    }
    If ($SupernetLength -gt $SubnetLength) {
      Write-Error "Subnet is larger than supernet. Aborting"
    }
    $SupernetMask = ConvertTo-Mask $SupernetLength

    $NumberOfNets = [Math]::Pow(2, ($SubnetLength - $SupernetLength))
    $NumberOfAddresses = [Math]::Pow(2, (32 - $SubnetLength))

    $SupernetAddress = Get-NetworkAddress $IPAddress $SupernetMask
    $DecimalAddress = ConvertTo-DecimalIP $SupernetAddress

    For ($i = 0; $i -lt $NumberOfNets; $i++) {
      $NetworkAddress = ConvertTo-DottedDecimalIP $DecimalAddress 

      "" | Select-Object @{n='NetworkAddress';e={ $NetworkAddress }},
        @{n='BroadcastAddress';e={ Get-BroadcastAddress $NetworkAddress $SubnetMask }},
        @{n='SubnetMask';e={ $SubnetMask }},
        @{n='HostAddresses';e={ 
          $NumberOfHosts = $NumberOfAddresses - 2
          If ($NumberOfHosts -lt 0) { 0 } Else { $NumberOfHosts } }}

      $DecimalAddress += $NumberOfAddresses
    }
  }
}

Sorting by IP address

$
0
0

Sorting an array of IP addresses is one of those things that’s not quite as clean and simple as would be nice.

Fortunately, the process can be simplified greatly by converting an IP address to decimal for the sort operation. To do this, I like to use ConvertTo-DecimalIP from my NetShell module (or my collection of subnet maths functions).

"10.1.10.10", "192.168.1.42", "172.16.1.1", "127.0.0.1" | Sort-Object { ConvertTo-DecimalIP $_ }

Short and simple.

PuTTY and PowerShell

$
0
0

A friend of mine found this enormously useful, one of the things PowerShell can do easily that is (apparently) helpful for people who want nothing to do with it otherwise (where it may be either PowerShell or Windows).

PuTTY stores session settings in the Registry, this little snippet using the PowerShell Registry Provider to update PuTTY session values. Anything goes as long as you can figure out the registry value you want to change.

Everything is in short-hand to make it, well, short. So, for reference this is what the shorthand really means:

  • ls is Get-ChildItem
  • sp is Set-ItemProperty
  • % is ForEach-Object (a loop operator)

List all sessions

ls HKCU:\Software\SimonTatham\PuTTY\Sessions

List some sessions

ls HKCU:\Software\SimonTatham\PuTTY\Sessions\*.domain.example
ls HKCU:\Software\SimonTatham\PuTTY\Sessions\*-rtr-*
ls HKCU:\Software\SimonTatham\PuTTY\Sessions\*-sw-*

More complex filtering can be performed using Where-Object (?), but that breaks the simplicity I’m striving for here.

The items returned by ls (Get-ChildItem) can be piped directly into sp (Set-ItemProperty) to change things.

Set the console size

ls HKCU:\Software\SimonTatham\PuTTY\Sessions | sp -name TermWidth -value 140
ls HKCU:\Software\SimonTatham\PuTTY\Sessions | sp -name TermHeight -value 50

Set a proxy

ls HKCU:\Software\SimonTatham\PuTTY\Sessions | sp -name ProxyHost -value "proxy.domain.example"
ls HKCU:\Software\SimonTatham\PuTTY\Sessions | sp -name ProxyPort -value 3128

Add a username

This assumes none of the sessions have a username yet, if it were to run against a session with a username it would double up. That is, me@host becomes me@me@host.

ls HKCU:\Software\SimonTatham\PuTTY\Sessions | %{
  sp $_.PSPath -name HostName -value "me@$($_.GetValue('hostname'))" 
}

Replace a username

Swapping out root@ with me@, a regular expression matches ^root at the beginning of the HostName. If the session does not use root nothing will change.

ls HKCU:\Software\SimonTatham\PuTTY\Sessions | %{
  sp $_.PSPath -name HostName -value ($_.GetValue('hostname') -replace '^root@', 'me@') 
}

WhoIs in PowerShell

$
0
0

There are a number of WhoIs scripts knocking around, this is my version. Something of a long development cycle since I started this last year, got busy, then finished it today.

It has a couple of dependencies, it needs both NetShell (for sockets) and DnsShell (to attempt to locate a WhoIs server). I use whois-servers.net to find a server for name lookups, and I use ARIN as a starting point for IP lookups (with a recursive call to get detail).

Function Get-WhoIs {
  [CmdLetBinding()]
  Param(
    [Parameter(Mandatory = $True)]
    [String]$Name,
    [String]$WhoIsServer,
    [String]$Command
  )
 
  If (!(Get-Command Get-Dns)) { break }

  If (!$WhoIsServer) {
    If ([Net.IPAddress]::TryParse($Name, [Ref]$Null)  -Or $Name.EndsWith("arpa")) {
      $WhoIsServer = $WhoIsServerName = "whois.arin.net"
      $Command = "n "
    } Else {
      $WhoIsServer = $WhoIsServerName = "$($Name.Split('.')[-1]).whois-servers.net"
    }
  }
  If (!([Net.IPAddress]::TryParse($WhoIsServer, [Ref]$Null))) {
    $WhoIsServerRecord = Get-Dns -Name $WhoIsServer -RecordType A |
      Select-Object -Expand Answer |
      Where-Object { $_.RecordType -eq "A" } |
      Select-Object -First 1
    $WhoIsServer = $WhoIsServerRecord.IPAddress
  }
  
  If ($WhoIsServer) {
    Write-Verbose "Querying $WhoIsServerName ($WhoIsServer) for $Name"
  
    $Data = [Text.Encoding]::ASCII.GetBytes("$Command$Name`r`n")

    $Socket = New-Socket -Protocol Tcp
    Send-Bytes -Socket $Socket -IPAddress $WhoIsServer -Port 43 -Data $Data
    $ReceivedData = Receive-Bytes -Socket $Socket -BufferSize 4098

    If ($ReceivedData) {
      $WhoIsRecord = $ReceivedData | ForEach-Object {
        [Text.Encoding]::ASCII.GetString($_.Data).Trim()
      } | Out-String
      If ($WhoIsRecord -Match 'ReferralServer: whois://(.+):') {
        Get-WhoIs $Name -WhoIsServer $Matches[1]
      } Else {
        $WhoIsRecord
      }
    }
    Remove-Socket $Socket
  }
}

My test set is small, if you happen to try it and find something doesn’t work please let me know.

ADSystemInfo ComObject

$
0
0

This function adds members to an instance of ADSystemInfo in an attempt to simplify use.

All properties and methods for the IADsADSystemInfo interface have been accounted for. The return type is the original ComObject, the only change is the addition of NoteProperty and ScriptMethod members.

Function Get-ADSystemInfo {
  <#
    .Synopsis
      Rewrites properties and methods associated with the ComObject 
    .Description
      Get-ADSystemInfo adds members to the ADSystemInfo COM Object improving the 
      accessibility of each property and method (greatly simplifying syntax).
    .Example
      Get-ADSystemInfo
    .Example
      (Get-ADSystemInfo).GetAnyDCName()
    .Example
      (Get-ADSystemInfo).GetDCSiteName("A-DC&")
    .Example
      Get-ADSystemInfo | Get-Member
  #>

  # Dynamically enumerating these is really hard. Hard-coded.
  $Properties = "ComputerName", "DomainDnsName", "DomainShortName", 
    "ForestDnsName", "IsNativeMode", "PDCRoleOwner", "SchemaRoleOwner", 
    "SiteName", "UserName"
  # Methods excluding GetDCSiteName, requires a single hostname as an argument
  $MethodsWithoutArgs = "GetAnyDCName", "GetTrees", "RefreshSchemaCache"  
  
  # Create ComObject as the basic return value
  $Object = New-Object -ComObject ADSystemInfo
  # Add each of the known properties as a NoteProperty to the base object
  ForEach ($Property in $Properties) {
    $Object | Add-Member -Name $Property -Type NoteProperty -Value `
      ($Object.GetType().InvokeMember($Property, 'GetProperty', $Null, $Object, $Null))
  }
  # Create a ScriptMethod caller for each of the known methods and add each to the base object
  ForEach ($Method in $MethodsWithoutArgs) {
    $Object | Add-Member -Name $Method -Type ScriptMethod -Value $(
      Invoke-Expression "{ `$this.GetType().InvokeMember('$Method', 'InvokeMethod', `$Null, `$this, `$Null) }")
  }
  # Add GetDCSiteName taking a single (first) argument via $args[0]
  $Object | Add-Member -Name 'GetDCSiteName' -Type ScriptMethod -Value { 
    $this.GetType().InvokeMember('GetDCSiteName', 'InvokeMethod', $Null, $this, $args[0]) }
  
  # Return the updated object
  $Object
}

New-Enum

$
0
0

The code below allows creation of .NET enumerations in PowerShell without using Add-Type.

Before an Enum is created a Dynamic Assembly and Module is created to hold it. The function below does that and, by default, leaves the module in a global variable called Indented_ModuleBuilder. Attempting to retrieve the module builder dynamically returns an InternalModuleBuilder class which cannot be used later (at least not as I wish).

Function New-DynamicModuleBuilder {
  <#
    .Synopsis
      Creates an assembly and a dynamic module within the current AppDomain.
    .Description
      Prepares a System.Reflection.Emit.ModuleBuilder class to allow construction of dynamic types.
      The ModuleBuilder is created to allow the creation of multiple types under a single assembly.
    .Parameter AssemblyName
      A name for the in-memory assembly.
    .Parameter UseGlobalVariable
      By default, this function stores the requested ModuleBuilder in a global variable called 
      Indented_ModuleBuilder. This leaves the ModuleBuilder object accessible to New-Enum without 
      needing an explicit assignment operation.
    .Example
      New-DynamicModuleBuilder "Example.Assembly"
    .Inputs
      System.Reflection.AssemblyName
    .Outputs
      System.Reflection.Emit.ModuleBuilder
  #>
  
  Param(
    [Parameter(Mandatory = $true)]
    [Reflection.AssemblyName]$AssemblyName,
    
    [Boolean]$UseGlobalVariable = $True,
    
    [Switch]$PassThru
  )
  
  $AppDomain = [AppDomain]::CurrentDomain

  # Multiple assemblies of the same name can exist. This check aborts if the assembly name 
  # exists on the assumption that this is undesirable.
  $AssemblyRegEx = "^$($AssemblyName.Name -replace '\.', '\.'),"
  if ($AppDomain.GetAssemblies() |
    Where-Object { 
      $_.IsDynamic -and $_.Fullname -match $AssemblyRegEx }) {

    Write-Error "Dynamic assembly $($AssemblyName.Name) already exists."
    break
  }
  
  # Create a dynamic assembly in the current AppDomain
  $AssemblyBuilder = $AppDomain.DefineDynamicAssembly(
    $AssemblyName, 
    [Reflection.Emit.AssemblyBuilderAccess]::Run
  )

  $ModuleBuilder = $AssemblyBuilder.DefineDynamicModule($AssemblyName.Name)
  if ($UseGlobalVariable) {
    # Create a transient dynamic module within the new assembly
    New-Variable Indented_ModuleBuilder -Scope Global -Value $ModuleBuilder
    if ($PassThru) {
      $ModuleBuilder
    }
  } else {
    return $ModuleBuilder
  }
}

Once the dynamic module is created, any number of Enums can be created from hash tables.

Function New-Enum {
  <#
    .Synopsis
      Creates a new enum from a hashtable using an existing instance of ModuleBuilder.
    .Description
      New-Enum dynamically creates an enum with the specified name (and namespace).

      A hashtable is used to populate the enum. All values passed in via the hashtable must 
      be able to convert to the enum type.

      The enum is created, but not returned by this function. Once created the enum can be 
      used as described in the examples.
    .Parameter ModuleBuilder
      A dynamic module within a dynamic assembly, created by New-DynamicModuleBuilder. By 
      default, the function uses the global variable Indented_ModuleBuilder, populated if 
      New-DynamicModuleBuilder is executed with UseGlobalVariable set to true (the default 
      value).
    .Parameter Name
      A name for the enum, a namespace may be included.
    .Parameter Members
      A hashtable describing the members of the enum.
    .Parameter Type
      A .NET value type, by default Int32 is used. The type name is passed as a string 
      and converted to a Type by the function.
    .Parameter SetFlagsAttribute
      Optionally sets the System.FlagsAttribute on the enum, indicating the enum is 
      treated as a bit field. Note that the enum members must support this attribute.
    .Example
      C:\PS>New-DynamicModuleBuilder "Example"
      C:\PS>$EnumMembers = @{cat=1;dog=2;tortoise=4;rabbit=8}
      C:\PS>New-Enum -Name "Example.Pets" -SetFlagsAttribute -Members $EnumMembers
      C:\PS>[Example.Pets]10

      Returns values "dog" and "rabbit".
    .Example
      C:\PS>$Builder = New-DynamicModuleBuilder "Example" -UseGlobalVariable $False
      C:\PS>New-Enum -ModuleBuilder $Builder -Name "Example.Byte" `
      >> -Type "Byte" -Members @{one=1;two=2}
      >>
      C:\PS>[Example.Byte]2

      Uses a user-defined variable to store the created dynamic module. The example returns 
      the value "two".
    .Example
      C:\PS>New-DynamicModuleBuilder "Example"
      C:\PS>New-Enum -Name "Example.NumbersLow" -Members @{One=1; Two=2}
      C:\PS>New-Enum -Name "Example.NumbersHigh" -Members @{OneHundred=100; TwoHundred=200}
      C:\PS>[UInt32][Example.NumbersLow]::One + [UInt32][Example.NumbersHigh]::OneHundred

      Multiple Enums can be built within the same dynamic assembly, a module builder only needs
      to be created once.
  #>

  Param(
    [Reflection.Emit.ModuleBuilder]$ModuleBuilder = $Indented_ModuleBuilder,
    
    [Parameter(Mandatory = $true, Position = 1)]
    [ValidatePattern('^(\w+\.)*\w+$')]
    [String]$Name,

    [Type]$Type = "Int32",

    [Alias('Flags')]
    [Switch]$SetFlagsAttribute,

    [Parameter(Mandatory = $true)]
    [HashTable]$Members
  )
 
  # This function cannot overwrite or append to existing types. 
  # Abort if a type of the same name is found and return a more friendly error than ValidateScript.
  if ($Name -as [Type]) {
    Write-Error "Type $Name already exists"
    break
  }
 
  # Begin defining a public System.Enum 
  $EnumBuilder = $ModuleBuilder.DefineEnum(
    $Name,
    [Reflection.TypeAttributes]::Public,
    $Type)
  if ($?) {
    if ($SetFlagsAttribute) {
      $EnumBuilder.SetCustomAttribute(
        [FlagsAttribute].GetConstructor([Type]::EmptyTypes),
        @()
      )
    }
    $Members.Keys | ForEach-Object {
      $EnumBuilder.DefineLiteral($_, [Convert]::ChangeType($Members[$_], $Type)) | Out-Null
    }
    $Enum = $EnumBuilder.CreateType()
  }
}

The last example below shows how the functions can be used once they have been loaded.

New-DynamicModuleBuilder Indented

$FlagFields = @{
  Bit1 = 1;
  Bit2 = 2;
  Bit3 = 4;
  Bit4 = 8;
}
$SimpleValues = @{
  Value1 = 1;
  Value2 = 2;
}
New-Enum "Indented.Flags" -Flags -Members $FlagFields
New-Enum "Indented.Simple" -Members $SimpleValues

[Indented.Flags]7
[Indented.Simple]1

Membership trees

$
0
0

This post shows two functions to draw member trees. The functions are almost identical, a variation on a theme. The first shows group membership by following memberOf, the second shows members of a group by following member.

In both cases the functions are demonstrative; output is written using Write-Host and cannot be redirected to a file without changing that.

Following MemberOf

Function Get-MemberOfTree {
  # .SYNOPSIS
  #   Get memberOf for an object and present output as a tree.
  # .DESCRIPTION
  #   An recursive function which uses repeated ADSI searches to build a memberOf tree.
  # .PARAMETER Identity
  #   A DN or SamAccountName used to start the search.
  # .PARAMETER SearchRoot
  #   The root of the current domain by default. A fixed value can be supplied if required. Note that the search root is also used to locate the suer if a DN is not supplied.
  # .PARAMETER IndentLevel
  #   The starting indent level (repetition of the IndentCharacter value).
  # .PARAMETER IndentCharacter
  #   The character to use to indent values.
  # .PARAMETER GC
  #   Use a Global Catalog to search instead of LDAP (used for forest-wide searches).
  # .PARAMETER UseForestRoot
  #   Sets the SearchRoot value to the forest root domain taken from RootDSE.

  [CmdLetBinding(DefaultParameterSetName = 'ManualSearchRoot')]
  param(
    [Parameter(Mandatory = $true, Position = 1)]
    [String]$Identity,

    [Parameter(ParameterSetName = 'ManualSearchRoot', Position = 2)]
    [String]$SearchRoot = (([ADSI]"LDAP://RootDSE").defaultNamingContext[0]),

    [Parameter(Position = 3)]
    [UInt32]$IndentLevel = 0,

    [Parameter(Position = 4)]
    [String]$IndentCharacter = "`t",

    [Switch]$GC,

    [Parameter(Mandatory = $true, ParameterSetName = 'AutomaticForestSearchRoot')]
    [Switch]$UseForestRoot
  )

  process {
    $Protocol = "LDAP"
    # Switch the protocol if the GC switch parameter is used.
    if ($GC) {
      $Protocol = "GC"
    }
    if ($UseForestRoot) {
      $SearchRoot = ([ADSI]"LDAP://RootDSE").rootDomainNamingContext[0]
    }

    # If the value passed as identity is not an object DN, treat the value as a sAMAccountName 
    # and execute a search using the SearchRoot and GC parameters.
    if ($Identity -notmatch '^CN=.+(?:DC=\w+){1,}') {

      Write-Verbose "Executing directory search for $Identity using $SearchRoot"

      $Searcher = [ADSISearcher]"(sAMAccountName=$Identity)"
      $Searcher.SearchRoot = [ADSI]"$($Protocol)://$SearchRoot"
      $SearchResult = $Searcher.FindOne()

      if (!$SearchResult -or !$?) {
        Write-Error "Unable to resolve identity for $Identity"
        $Identity = $null
      } else {
        $Identity = $SearchResult.Properties['distinguishedname'][0]
      }
    }

    if ($Identity) {
      Write-Host "$($IndentCharacter * $IndentLevel)$($Identity -replace '^CN=|,(CN|OU|DC)=.+$')"

      $IndentLevel++

      $Searcher = [ADSISearcher]"(member=$Identity)"
      $Searcher.PageSize = 1000
      $Searcher.SearchRoot = [ADSI]"$($Protocol)://$SearchRoot"
      $Searcher.FindAll() | ForEach-Object {

        Write-Verbose "Name: $($_.Properties['name']) $($_.Properties['objectclass'])"

        if (([Array]$_.Properties['objectclass'])[-1] -eq 'group') {

          Get-MemberOfTree $_.Properties['distinguishedname'] $SearchRoot $IndentLevel $IndentCharacter

        } else {
          Write-Host "$($IndentCharacter * $IndentLevel)$($_.Properties['name'])"
        }
      }
    }
  }
}

Following member

Function Get-GroupMemberTree {
  # .SYNOPSIS
  #   Get members of a group and present output as a tree.
  # .DESCRIPTION
  #   An recursive function which uses repeated ADSI searches to build a member tree.
  # .PARAMETER Identity
  #   The DN, SamAccountName or Name of the group used to start the search. If anything but a DN is supplied a search will be executed to attempt to find the DN.
  # .PARAMETER SearchRoot
  #   The root of the current domain by default. A fixed value can be supplied if required. Note that the search root is also used to locate the suer if a DN is not supplied.
  # .PARAMETER IndentLevel
  #   The starting indent level (repetition of the IndentCharacter value).
  # .PARAMETER IndentCharacter
  #   The character to use to indent values.
  # .PARAMETER GC
  #   Use a Global Catalog to search instead of LDAP (used for forest-wide searches).
  # .PARAMETER UseForestRoot
  #   Sets the SearchRoot value to the forest root domain taken from RootDSE.

  [CmdLetBinding(DefaultParameterSetName = 'ManualSearchRoot')]
  param(
    [Parameter(Mandatory = $true, Position = 1)]
    [String]$Identity,

    [Parameter(ParameterSetName = 'ManualSearchRoot', Position = 2)]
    [String]$SearchRoot = (([ADSI]"LDAP://RootDSE").defaultNamingContext[0]),

    [Parameter(Position = 3)]
    [UInt32]$IndentLevel = 0,

    [Parameter(Position = 4)]
    [String]$IndentCharacter = "`t",

    [Switch]$GC,

    [Parameter(Mandatory = $true, ParameterSetName = 'AutomaticForestSearchRoot')]
    [Switch]$UseForestRoot
  )

  process {
    $Protocol = "LDAP"
    # Switch the protocol if the GC switch parameter is used.
    if ($GC) {
      $Protocol = "GC"
    }
    if ($UseForestRoot) {
      $SearchRoot = ([ADSI]"LDAP://RootDSE").rootDomainNamingContext[0]
    }

    # If the value passed as identity is not an object DN, treat the value as a sAMAccountName 
    # and execute a search using the SearchRoot and GC parameters.
    if ($Identity -notmatch '^CN=.+(?:DC=\w+){1,}') {

      Write-Verbose "Executing directory search for $Identity using $SearchRoot"

      $Searcher = [ADSISearcher]"(&(|(sAMAccountName=$Identity)(name=$Identity))(objectClass=group))"
      $Searcher.SearchRoot = [ADSI]"$($Protocol)://$SearchRoot"
      $SearchResult = $Searcher.FindOne()

      if (!$SearchResult -or !$?) {
        Write-Error "Unable to resolve identity for $Identity"
        $Identity = $null
      } else {
        $Identity = $SearchResult.Properties['distinguishedname'][0]
      }
    }

    if ($Identity) {
      Write-Host "$($IndentCharacter * $IndentLevel)$($Identity -replace '^CN=|,(CN|OU|DC)=.+$')"

      $IndentLevel++

      $Searcher = [ADSISearcher]"(memberOf=$Identity)"
      $Searcher.PageSize = 1000
      $Searcher.SearchRoot = [ADSI]"$($Protocol)://$SearchRoot"
      $Searcher.FindAll() | ForEach-Object {

        Write-Verbose "Name: $($_.Properties['name']) $($_.Properties['objectclass'])"

        if (([Array]$_.Properties['objectclass'])[-1] -eq 'group') {

          Get-GroupMemberTree $_.Properties['distinguishedname'] $SearchRoot $IndentLevel $IndentCharacter

        } else {
          Write-Host "$($IndentCharacter * $IndentLevel)$($_.Properties['name'])"
        }
      }
    }
  }
}

Spiceworks minus Administrator

$
0
0

I very much doubt that I am the first to do this, but I wanted to share it anyway.

How to set up an account to scan Windows devices from Spiceworks without administrator level rights.

Step 1: Create a group which can be used to grant access to WMI

WMI permissions may be set using the WMI Control option in Computer Management. Alternatively the script below can be used to create the group and assign permissions.

Access is granted to the root namespace and all sub-namespaces.


# Create an enumeration to make AccessMask values human-readable.
Add-Type @"
  using System;

  namespace WmiSecurity 
  {
    [FlagsAttribute]
    public enum AccessRight : int {
      Enable  = 1,
      Execute = 2,
      FullWriteRep = 4,
      PartialWriteRep = 8,
      WriteProvider = 16,
      RemoteAccess = 32,
      Subscribe = 64,
      Publish = 128,
      ReadControl = 131072,
      WriteDAC = 262144
    }
  }
"@

# Connect to the WinNT provider
$LocalMachine = [ADSI]"WinNT://localhost"

# Check for the group
$Group = $LocalMachine.PsBase.Children | Where-Object { $_.Class -eq 'group' -and $_.Name -eq 'WMI Users' }

# If the group does not exist
if (!$Group) {

  # Create the group and set a description
  $Group = $LocalMachine.Create("group", "WMI Users")
  $Group.Put("description", "Members of this group can access WMI remotely.")
  $Group.SetInfo()

  # Get the SID for the new group
  $Group = Get-WmiObject Win32_Account -Filter "Domain='$(hostname)' and Name='WMI Users'"

  # Create a trustee to use with a Access Control Entry
  $Trustee = ([WmiClass]"Win32_Trustee").CreateInstance()
  $Trustee.Name = $Group.Name
  $Trustee.Domain = $Group.Domain
  $Trustee.SIDString = $Group.SID

  # Create an Access Control Entry
  $ACE = ([WmiClass]"Win32_ACE").CreateInstance()

  # Set the Access Control Entry parameters and trustee
  $ACE.ACEFlags = [Security.AccessControl.InheritanceFlags]"ObjectInherit"
  $ACE.AccessMask = [WmiSecurity.AccessRight]"Enable, RemoteAccess, ReadControl"
  $ACE.ACEType = [Security.AccessControl.AccessControlType]"Allow"
  $ACE.Trustee = $Trustee

  # Extract the current security descriptor for the root WMI namespace.
  $SecurityDescriptorBaseObject = Invoke-WmiMethod -NameSpace root -Path "__systemsecurity=@" -Name GetSecurityDescriptor
  $SecurityDescriptor = $SecurityDescriptorBaseObject.Descriptor

  # Add the new Access Control Entry to the Discretionary Access Control List
  $SecurityDescriptor.DACL += $ACE.PsObject.ImmediateBaseObject

  # Apply the changes
  Invoke-WmiMethod -NameSpace root -Path "__systemsecurity=@" -Name SetSecurityDescriptor -ArgumentList $SecurityDescriptor.PsObject.ImmediateBaseObject
}

Step 2: Create a service account and add it to a few groups

The service account does not require administrative privileges, but it does need to be a member of a number of machine-local groups.

Restricted groups would be a useful way to setting appropriate group membership for the service account.

Once complete the service account should be able to poll Windows hosts for information and counters without the too-often recommended need for it to be an administrator.

Get-InstalledSoftware

$
0
0

Get-InstalledSoftware reads uninstall registry keys from a host, it will also read Uninstall keys from loaded user hives (HKEY_USERS).

Get-InstalledSofware does not load hives, a user must have been logged on (at some point) for the hive to be loaded. Rebooting a machine unloads all user hives.

function Get-InstalledSoftware {
  # .SYNOPSIS
  #   Get all installed from the Uninstall keys in the registry.
  # .DESCRIPTION
  #   Read a list of installed software from each Uninstall key.
  #
  #   This method provides an alternative to using the WMI class Win32_Product which causes an msi reconfiguration action.
  #
  #   This CmdLet assumes the user is authenticated.
  # .PARAMETER ComputerName
  #   The computer to execute against. By default, Get-InstalledSoftware reads registry keys on the local computer.
  # .PARAMETER StartRemoteRegistry
  #   The script should attempt to start the remote registry service if it is not already running. This parameter will only take effect if the service is not disabled.
  # .PARAMETER IncludeLoadedUserHives
  #   Some software packages, such as DropBox install into a users profile rather than into shared areas. Get-InstalledSoftware can increase the search to include each loaded user hive.
  #
  #   If a registry hive is not loaded it cannot be searched, this is a limitation of this search style.
  # .PARAMETER IncludeBlankNames
  #   By default Get-InstalledSoftware will suppress the display of entries with minimal information. If no DisplayName is set it will be hidden from view. This behaviour may be changed using this parameter.
  # .PARAMETER DebugConnection
  #   By default error messages are suppressed. A large number of errors may be returned by a single device because of the granular nature of registry permissions. This parameter allows the displays of all caught exceptions for debugging purposes.
  # .EXAMPLE
  #   Get-InstalledSoftware
  #
  #   Get the list of installed applications from the local computer.
  # .EXAMPLE
  #   Get-InstalledSoftware -IncludeLoadedUserHives
  #
  #   Get the list of installed applications from the local computer, including each loaded user hive.
  # .EXAMPLE
  #   Get-InstalledSoftware -ComputerName None -DebugConnection
  #
  #   Display all error messages thrown when attempting to audit the specified computer.
  # .EXAMPLE
  #   Get-InstalledSoftware -IncludeBlankNames
  #
  #   Display all results, including those with very limited information.
  
  [CmdLetBinding()]
  param(
    [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
    [String]$ComputerName = $Env:ComputerName,

    [Switch]$StartRemoteRegistry,

    [Switch]$IncludeLoadedUserHives,
    
    [Switch]$IncludeBlankNames,
    
    [Switch]$DebugConnection
  )

  process {

    # If the remote registry service is stopped before this script runs it will be stopped again afterwards.
    if ($StartRemoteRegistry) {
      $ShouldStop = $false
      $Service = Get-WmiObject Win32_Service -Filter "Name='RemoteRegistry'" -Computer $ComputerName
      If ($Service.State -eq "Stopped" -And $Service.StartMode -ne "Disabled") {
        $ShouldStop = $true
        $Service.StartService() | Out-Null
      }
    }
    
    # Create an array to hold open base keys. The Uninstall key should be relative and fixed from here.
    $BaseKeys = @()
    
    if ($IncludeLoadedUserHives) {
      try {
        $BaseKey = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey("Users", $ComputerName)
      } catch [Exception] {
        if ($DebugConnection) {
          $Message = $_.Exception.Message -replace "`n"
          Write-Error "ComputerName: $ComputerName :: $Message :: Users"
        }
      }
      
      if ($?) {
        $BaseKey.GetSubKeyNames() | ForEach-Object {
          $SubKeyName = $_
          try {
            $BaseKeys += $BaseKey.OpenSubKey($SubKeyName)
          } catch [Exception] {
            if ($DebugConnection) {
              $Message = $_.Exception.Message -replace "`n"
              Write-Error "ComputerName: $ComputerName :: $Message :: $SubKeyName"
            }
          }
        }
      }
    }
    
    # Connect to the base key
    try {
      $BaseKey = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey("LocalMachine", $ComputerName)
    } catch [Exception] {
      if ($DebugConnection) {
        $Message = $_.Exception.Message -replace "`n"
        Write-Error "ComputerName: $ComputerName :: $Message :: LocalMachine"
      }
    }
    if ($?) {
      $BaseKeys += $BaseKey
    }
    
    # Begin reading package information from the registry
    $Packages = @{}
    $BaseKeys | ForEach-Object {
      $BaseKey = $_

      # Uninstall keys relative to each base.
      "Software\Microsoft\Windows\CurrentVersion\Uninstall", "Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall" | ForEach-Object {
        $UninstallKeyString = $_
        try {
          $UninstallKey = $BaseKey.OpenSubKey($UninstallKeyString)
        } catch [Exception] {
          if ($DebugConnection) {
            $Message = $_.Exception.Message -replace "`n"
            Write-Error "ComputerName: $ComputerName :: $Message :: LocalMachine\$UninstallKeyString"
          }
        }
        if ($? -and $UninstallKey) {
          $UninstallKey.GetSubKeyNames() | ForEach-Object {
            $SubKeyName = $_
            $UninstallKey.OpenSubKey($_) | ForEach-Object {
              # Create a new record for this package
              if ($Packages.Contains($SubKeyName)) {
                [Array]$Packages[$SubKeyName].RegistryKeys += "$($BaseKey.ToString())\$UninstallKeyString"
              } else {
                $DateString = $_.GetValue("InstallDate"); $InstallDate = $null
                if ($DateString) {
                  $InstallDate = [DateTime]::ParseExact($DateString, "yyyyMMdd", [Globalization.CultureInfo]::CurrentCulture)
                }

                $Package = New-Object PsObject -Property ([Ordered]@{
                  ComputerName    = $ComputerName;
                  Name            = $_.GetValue("DisplayName");
                  DisplayVersion  = $_.GetValue("DisplayVersion");
                  InstallDate     = $InstallDate;
                  InstallLocation = $_.GetValue("InstallLocation");
                  HelpLink        = $_.GetValue("HelpLink");
                  Publisher       = $_.GetValue("Publisher");
                  UninstallString = $_.GetValue("UninstallString");
                  URLInfoAbout    = $_.GetValue("URLInfoAbout");
                  KeyName         = $SubKeyName;
                  RegistryKeys    = "$($BaseKey.ToString())\$UninstallKeyString";
                  InstalledAs     = "";
                })

                $Packages.Add($SubKeyName, $Package)
              }
            }
          }
        }
      }
    }

    # Attempt to resolve SID strings to something a bit more friendly. This method is a bit limited.
    $InstalledAs = @{}
    $Packages.Values |
      ForEach-Object { $_.RegistryKeys } |
      Select-Object -Unique |
      ForEach-Object {
        if ($_ -match '^HKEY_LOCAL_MACHINE') {
          if ($_ -match 'Wow6432Node') {
            $InstalledAs.Add($_, "LocalMachine\64Bit")
          } else {
            $InstalledAs.Add($_, "LocalMachine\32Bit")
          }
        } elseif ($_ -match '^HKEY_USERS\\(?[^\\]+)') {
          $NTAccount = (New-Object Security.Principal.SecurityIdentifier $matches.SID).Translate([Security.Principal.NTAccount]).Value
          if ($NTAccount) {
            $InstalledAs.Add($_, $NTAccount)
          } else {
            $InstalledAs.Add($_, $matches.SID)
          }
        }
      }
    $Packages.Keys | ForEach-Object {
      $Packages[$_].InstalledAs = ($Packages[$_].RegistryKeys | ForEach-Object { $InstalledAs[$_] })
    }
    
    # Stop the remote registry service if required
    if ($StartRemoteRegistry -and $ShouldStop) {
      $Service.StopService() | Out-Null
    }

    # Output filtering
    if ($IncludeBlankNames) {
      return $Packages.Values
    } else {
      return ($Packages.Values | Where-Object Name)
    }
  }
}

Compare-Array

$
0
0

Compare-Object and Group-Object are very powerful tools for comparing and manipulating arrays of objects.

Compare-Array is intended to be used against arrays containing primitive data types where Type comparison is imperative. In addition to the Type constraint, the ordering of an array is treated as a distinguishing factor.

Compare-Array returns a simple Boolean response indicating whether or not the arrays are equal. For the function to be efficient it stops at the first mismatch, therefore it cannot return the cause of the inequality.

The .NET framework (4) provides a simple way to compare two different arrays through use of the Collections.IStructuralEquatable interface.

An alternative method (the first in the function), which tends to be faster if the chance of equality is low, uses a foreach loop, comparing elements between arrays in sequence.

Examples are provided in the in-line help to demonstrate and describe the use of the function.

function Compare-Array {
  # .SYNOPSIS
  #   Compares two arrays to determine equality.
  # .DESCRIPTION
  #   This function presents two methods of comparing arrays.
  #
  #     1. A manual loop comparison method, exiting at the first opportunity. 
  #     2. A wrapper around the .NET 4 IStructuralEquatable interface.
  #
  #   Arrays must be exactly equal for the function to return true. That is, arrays must meet the following criteria:
  #
  #     * Must use simple values (primitive types).
  #     * Must be of equal length.
  #     * Must be ordered in the same way unless using the Sort parameter.
  #     * When comparing strings, case is important.
  #     * .NET Type must be equal (UInt32 is not the same as Int32).
  #
  # .PARAMETER Subject
  #   The object array to test.
  # .PARAMETER Object
  #   The object array to test against.
  # .PARAMETER Sort
  #   For an array to be considered equal it must also be ordered in the same way. Comparison of unordered arrays can be forced by setting this parameter.
  # .INPUTS
  #   System.Array
  #   System.Object[]
  # .OUTPUTS
  #   System.Boolean
  # .EXAMPLE
  #   C:\PS>Compare-Array -Subject 1, 2, 3 -Object 1, 2, 3
  #
  #   Returns true.
  # .EXAMPLE
  #   C:\PS>$a = [Byte[]](1, 2, 3)
  #   C:\PS>$b = [Byte[]](3, 2, 1)
  #   C:\PS>Compare-Array -Subject $a -Object $b
  #
  #   Returns false, elements are not ordered in the same way and types are equal.
  # .EXAMPLE
  #   C:\PS>$a = [Byte[]](1, 2, 3)
  #   C:\PS>$a = [UInt32[]](1, 2, 3)
  #   C:\PS>Compare-Array $a $b
  #
  #   Returns false, element Types are not equal.
  # .EXAMPLE
  #   C:\PS>$a = "one", "two"
  #   C:\PS>$b = "one", "two"
  #   C:\PS>Compare-Array $a $b
  #
  #   Returns true.
  # .EXAMPLE
  #   C:\PS>$a = "ONE", "TWO"
  #   C:\PS>$b = "one", "two"
  #   C:\PS>Compare-Array $a $b
  #
  #   Returns false.
  # .EXAMPLE
  #   C:\PS>$a = 1..10000
  #   C:\PS>$b = 1..10000
  #   C:\PS>Compare-Array $a $b -ManualLoop
  #
  #   Returns true.
  # .EXAMPLE
  #   C:\PS> Compare-Array @("1.2.3.4", "2.3.4.5") @("2.3.4.5", "1.2.3.4") -Sort
  #
  #   Returns true.
  
  param(
    [Parameter(Mandatory = $true)]
    [Object[]]$Subject,

    [Parameter(Mandatory = $true)]
    [Object[]]$Object,
    
    [Switch]$ManualLoop,
    
    [Switch]$Sort
  )

  if ($ManualLoop) {
    # If the arrays are not the same length they cannot be equal.
    if ($Subject.Length -ne $Object.Length) {
      return $false
    }
    
    # If Sort is set and the arrays are of equal length ensure both arrays are similarly ordered.
    if ($Sort) {
      $Subject = $Subject | Sort-Object
      $Object = $Object | Sort-Object
    }
    
    $Length = $Subject.Length
    $Equal = $true
    for ($i = 0; $i -lt $Length; $i++) {
      # Exit when the first match fails.
      if ($Subject[$i] -ne $Object[$i]) {
        return $false
      }
    }
    return $true
  } else {
    # If Sort is set and the arrays are of equal length ensure both arrays are similarly ordered.
    if ($Sort) {
      $Subject = $Subject | Sort-Object
      $Object = $Object | Sort-Object
    }

    ([Collections.IStructuralEquatable]$Subject).Equals(
      $Object,
      [Collections.StructuralComparisons]::StructuralEqualityComparer
    )
  }
}

Compare-Array is included as part of my Indented.Common module.


Indented module management

$
0
0

A while ago I decided to split a number of my modules up as they were becoming messy. Instead of standalone modules I now have modular modules.

The split introduced something of a version management problem for me, so I made up a set of module management functions and scripts to see me through from development to release and maintenance of released modules.

This article explores the module tree and looks at the installation and update functionality.

Modular modules

The module structure I use is hierarchical, the root module contains functions which are shared by other modules, functions involved in management, and functions which have no better home.

A few modules are still standalone, these are only bound to the hierarchical structure by the update mechanisms I incorporated into Indented.

Managing updates

Module management comes in three flavours.

  1. Manual updates
  2. Semi-automatic updates
  3. Automatic updates

All automatic components are part of Indented.Common and may be viewed in full by exploring the Indented.Common.psm1 file.

As automatic update components are part of Indented.Common they cannot be used to perform the very first installation of Indented.Common. Taking a bit of a hint from Lee Holmes I wrote a web-installation script which may be used to install Indented.Common.

Manual updates

Manual updating is, of course, fully supported. Files can be downloaded manually from their respective home pages, installation is entirely in the control of the user.

Dependencies must still be honoured, manifest files (psd1) technically enforce this through use of the RequiredModules option.

Semi-automatic updates

My web server hosts a module version control file which is consumed by Get-IndentedModule. The file is updated every time I update and release a version of one of my modules. This file is merged with the output from "Get-Module -ListAvailable" to give a picture of local vs server versions.

# Grab the list of modules from the web server (if possible). Merge everything into a single authoritative list.
$ModuleList = @()
$ModuleList += Get-WebContent http://www.indented.co.uk/ps-modules/modulelist.csv | ConvertFrom-Csv
# Required call to show information about loaded modules from paths other than $env:PsModulePath.
$ModuleList += Get-Module Indented.*
# Add information for modules available under $env:PsModulePath which are not loaded.
$ModuleList += Get-Module Indented.* -ListAvailable | Where-Object { -not (Get-Module $_.Name) }

Updates are still manually invoked, a user can elect to update a module using Install-IndentedModule. For example:

Get-IndentedModule Indented.Common | Install-IndentedModule

An attempt is made to reduce the number of calls to the web server, if running in a pipeline the call executes during begin.

begin {
  if ($PsCmdLet.ParameterSetName -eq 'Name') {
    # Call back to Get-IndentedModule to get the information we need.
    Get-IndentedModule $Name | Install-IndentedModule -ModulePath $ModulePath
  }
}

Automatic updates

Automatic updates, if enabled (it’s disabled by default), execute for installed modules only. If a module has not been installed one of the two manual processes above must be used.

Get-IndentedModule |
  Where-Object { $_.LocalVersion -ne "Not installed" -and $_.ServerVersion -ne "Not available" } |
  ForEach-Object {
    Write-Host "Start-IndentedAutoUpdate: Starting update for $($_.Name)" -ForegroundColor Cyan
    $_ | Install-IndentedModule
  }

To minimise the degree of intrusion the update process is executed when Indented.Common loads rather than through a scheduling mechanism. This means removal of the module remains nothing more than deletion of the module folder. The command is found at the very bottom of Indented.Common.psm1.

$AutoUpdateConfigurationFile = "$PsScriptRoot\AutoUpdateConfiguration.csv"
Start-IndentedAutoUpdate

To keep modules clean, an update (through Install-IndentedModule) will purge existing content.

# Delete the current instance of the module (files only).
if (Test-Path "$ModulePath\$($ModuleDescription.Name)") {
  Write-Verbose "Install-IndentedModule: Removing existing version of $($ModuleDescription.Name)"
  Remove-Item "$ModulePath\$($ModuleDescription.Name)" -Recurse
}

This also destroys the file which dictates how updates are performed if Indented.Common is updated. To overcome this the automatic update function, Start-IndentedAutoUpdate, reads and caches the file in memory, writing back much of the same content to a new file.

$AutoUpdateConfiguration = Get-IndentedAutoUpdate
# ...
$AutoUpdateConfiguration | Set-IndentedAutoUpdate -NextUpdate (((Get-Date) + $AutoUpdateConfiguration.Frequency).ToString())

Finally, to attempt to avoid errors finding a way in, the meaningful content of the auto-update configuration file is hashed. The hash is checked to verify the consistency, if the consistency check fails the file is dumped and regenerated. Perhaps overkill, but it was interesting to write.

if ($Updated) {
  # Update the change record
  $AutoUpdateConfiguration.LastModifiedBy = "$($env:UserDomain)\$($env:Username)"
  $AutoUpdateConfiguration.LastModified = (Get-Date).ToUniversalTime().ToString("u")
  $AutoUpdateConfiguration.Hash = NewIndentedAutoUpdateHash $AutoUpdateConfiguration
    
  # Export the modified configuration.
  $AutoUpdateConfiguration | 
    Select-Object EnableAutoUpdate, Frequency, NextUpdate, LastModifiedBy, LastModified, Hash |
    Export-Csv $AutoUpdateConfigurationFile -NoTypeInformation
}

Development to test and release

The script to execute this is a bit too environment specific to share. The general process is as follows:

  1. If test release copy the module $env:PsModulePath (first value).
  2. If live release:
    1. Update version numbers (minor unless explicitly stated to be a major update),
    2. Commit pending changes into TFS (tagged with the version number as a comment).
    3. Copy the module to $env:PsModulePath (first value).
    4. Package the module into a zip file and upload the zip file to my web server.
    5. Regenerate the module list consumed by Get-IndentedModule and upload the file to my web server.

A cron job on the web server takes it from there, setting appropriate permissions and putting the file in the right place.

Version number updates are handled via a pair of functions which deal with reading and writing module manifest files.

Get-ModuleManifest is a simple function which attempts to read the manifest as a hash table from a fixed location (the folder I use for development).

function Get-ModuleManifest {
  # .SYNOPSIS
  #   Get an existing module manifest file.
  # .DESCRIPTION
  #   Get the content of an existing manifest file.
  # .PARAMETER Name
  #   The name of the module within the TFS workspace.
  # .INPUTS
  #   System.String
  # .OUTPUTS
  #   System.Collections.HashTable
  # .EXAMPLE
  #   Get-ModuleManifest Indented.Common
  
  [CmdLetBinding()]
  param(
    [Parameter(Mandatory = $true)]
    [String]$Name
  )
  
  $ModuleManifestFile = "$WorkspacePath\$Name\$Name.psd1"
  
  if (-not (Test-Path $ModuleManifestFile)) {
    Write-Error "Get-ModuleManifest: Manifest file does not exist"
  } else {
    return Invoke-Expression (Get-Content $ModuleManifestFile | Out-String)
  }
}

Update-ModuleManifest allows arbitrary changes to properties, then uses splatting to pass the changed manifest information back to New-ModuleManifest.

function Update-ModuleManifest {
  # .SYNOPSIS
  #   Update an existing module manifest file.
  # .DESCRIPTION
  #   Update-ModuleManifest changes properties in an existing module manifest file.
  #
  #   Update-ModuleManifest validates the Property parameter, but does not validate the value which is being set.
  # .PARAMETER Name
  #   The name of the module within the TFS workspace.
  # .PARAMETER Property
  #   The property within the manifest to update.
  # .PARAMETER Value
  #   An arbitrary value to set. Validation of the value is left to New-ModuleManifest.
  # .INPUTS
  #   System.String
  #   System.Object
  # .EXAMPLE
  #   Update-ModuleManifest "SomeModule" -Property ModuleVersion -Value "1.9"
  # .EXAMPLE
  #   Update-ModuleManifest "SomeModule" -Property Guid -Value ([Guid]::NewGuid())

  [CmdLetBinding()]
  param(
    [Parameter(Mandatory = $true)]
    [String]$Name,
    
    [Parameter(Mandatory = $true)]
    [ValidateSet('AliasesToExport', 'Author', 'CLRVersion', 'CmdletsToExport', 'CompanyName', 'Copyright', 'DefaultCommandPrefix', 'Description', 'DotNetFrameworkVersion', 'FileList', 'FormatsToProcess', 'FunctionsToExport', 'GUID', 'HelpInfoURI', 'ModuleList', 'ModuleVersion', 'NestedModules', 'PowerShellHostName', 'PowerShellHostVersion', 'PowerShellVersion', 'PrivateData', 'ProcessorArchitecture', 'RequiredAssemblies', 'RequiredModules', 'RootModule', 'ScriptsToProcess', 'TypesToProcess', 'VariablesToExport')]
    [String]$Property,
    
    [Parameter(Mandatory = $true)]
    $Value
  )  
  
  $ModuleManifestFile = "$WorkspacePath\$Name\$Name.psd1"
  
  if (-not (Test-Path $ModuleManifestFile)) {
    Write-Error "Update-ModuleManifest: Manifest file does not exist"
  } else {
    $ModuleManifest = Get-ModuleManifest $Name
    
    if ($ModuleManifest.Contains($Property)) {
      $ModuleManifest[$Property] = $Value
    } else {
      $ModuleManifest.Add($Property, $Value) 
    }
    
    New-ModuleManifest -Path $ModuleManifestFile @ModuleManifest
  }
}

Perhaps the most frustrating thing about this process is the need to continually restart PowerShell to get the updated instance of the module. In the end I simplified that process by setting up a shortcut in exactly the way I wish, then simply creating a new process based on it:

function Restart-Console {
  Start-Process "C:\Stuff\PowerShell.lnk"
  exit
}

I like it, it’s extremely simple and leaves me with a console which has focus. No need to reach for the mouse and no need to alt-tab through everything else.

ConvertTo-Subnet

$
0
0

A new function has been added to Indented.NetworkTools: ConvertTo-Subnet.

ConvertTo-Subnet performs a bitwise comparison, determining the longest (contiguous) match between two IP addresses.

function ConvertTo-Subnet {
  # .SYNOPSIS
  #   Convert a start and end IP address to the closest matching subnet.
  # .DESCRIPTION
  #   ConvertTo-Subnet attempts to convert a starting and ending IP address from a range to the closest subnet.
  # .PARAMETER Start
  #   The first IP address from a range.
  # .PARAMETER End
  #   The last IP address from a range.
  # .INPUTS
  #   System.Net.IPAddress
  # .OUTPUTS
  #   Indented.NetworkTools.NetworkSummary
  # .EXAMPLE
  #   ConvertTo-Subnet 0.0.0.0 255.255.255.255
  # .EXAMPLE
  #   ConvertTo-Subnet 192.168.0.1 192.168.0.129
  # .EXAMPLE
  #   ConvertTo-Subnet 10.0.0.1 11.0.0.1
  # .EXAMPLE
  #   ConvertTo-Subnet 10.0.0.126 10.0.0.129
  # .EXAMPLE
  #   ConvertTo-Subnet 10.0.0.128 10.0.0.128
  # .EXAMPLE
  #   ConvertTo-Subnet 10.0.0.128 10.0.0.130
  # .NOTES
  #   2.14 - Added function to module.
 
  [CmdLetBinding()]
  param(
    [Parameter(Mandatory = $true)]
    [IPAddress]$Start,

    [Parameter(Mandatory = $true)]
    [IPAddress]$End
  )

  if ($Start -eq $End) {
    return (Get-NetworkSummary "$Start\32")
  }

  $DecimalStart = ConvertTo-DecimalIP $Start
  $DecimalEnd = ConvertTo-DecimalIP $End

  $i = 32
  do {
    $i--
  } until (($DecimalStart -band ([UInt32]1 -shl $i)) -ne ($DecimalEnd -band ([UInt32]1 -shl $i)))
  return (Get-NetworkSummary "$Start\$(32 - $i - 1)")
}

PowerShell file-based binary search

$
0
0

I thought I’d share my binary (half-interval) searcher implementation. The searcher may be used for finding text in a pre-sorted text file.

The advantage of a binary search is that it is extremely fast, especially in large data-sets (thousands or millions of record), when compared to simpler search methods (whether that’s Select-String or Where-Object). However, the search is specialised, if the data is not already sorted it cannot establish a meaningful half interval and therefore cannot find a match.

This implementation jumps about a file stream rather than an array in memory. The reader will always track-back to the beginning of a line before reading the line and attempting a match.

function Select-INString {
  # .SYNOPSIS
  #   Select a matching string from an alphabetically sorted file.
  # .DESCRIPTION
  #   Select-INString is a specialised binary (half interval) searcher designed to find matches in sorted ASCII encoded text files.
  # .PARAMETER FileName
  #   The name of the file to search.
  # .PARAMETER String
  #   The string to find. The string is treated as a regular expression and must match the beginning of the line.
  # .INPUTS
  #   System.String
  # .OUTPUTS
  #   System.String
  # .EXAMPLE
  #   Select-INString 
  # .NOTES
  #   Author: Chris Dent
  #
  #   Change log:
  #     11/08/2014 - Chris Dent - First release.

  param(
    [Parameter(Mandatory = $true)]
    [String]$String,
    
    [Parameter(Mandatory = $true)]
    [ValidateScript( { Test-Path $_ } )]
    [String]$FileName
  )
  
  $FileName = (Get-Item $FileName).FullName
  $FileStream = New-Object IO.FileStream($FileName, [IO.FileMode]::Open)
  $BinaryReader = New-Object IO.BinaryReader($FileStream)
  
  $Length = $BinaryReader.BaseStream.Length
  $Position = $Length / 2

  [Int64]$HalfInterval = $Length / 2
  $Position = $Length - $HalfInterval

  while ($Position -gt 1 -and $Position -lt $Length -and $Position -ne $LastPosition) {
    $LastPosition = $Position
    $HalfInterval = $HalfInterval / 2

    $BinaryReader.BaseStream.Seek($Position, [IO.SeekOrigin]::Begin) | Out-Null
    
    # Track back to the start of the line
    while ($true) {
      $Character = $BinaryReader.ReadByte()
      if ($BinaryReader.BaseStream.Position -eq 1) {
        $BinaryReader.BaseStream.Seek(-1, [IO.SeekOrigin]::Current) | Out-Null
        break
      } elseif ($Character -eq [Byte][Char]"`n") {
        break
      } else {
        $BinaryReader.BaseStream.Seek(-2, [IO.SeekOrigin]::Current) | Out-Null
      }
    }
    
    # Read the line
    $Characters = @()
    if ($BinaryReader.BaseStream.Position -lt $BinaryReader.BaseStream.Length) {
      do {
        $Characters += [Char][Int]$BinaryReader.ReadByte()
      } until ($Characters[-1] -eq [Char]"`n" -or $BinaryReader.BaseStream.Position -eq $BinaryReader.BaseStream.Length)
      $Line = (New-Object String (,[Char[]]$Characters)).Trim()
    } else {
      # End of file
      $FileStream.Close()
      return $null
    }

    if ($Line -match "^$String") {
      # Close the file stream and return the match immediately
      $FileStream.Close()
      return $Line
    } elseif ($Line -lt $String) {
      $Position = $Position + $HalfInterval
    } elseif ($Line -gt $String) {
      $Position = $Position - $HalfInterval
    }
  }
  
  # Close the file stream if no matches are found.
  $FileStream.Close()
}

Testing LDAPS

$
0
0

I’ve recently bumped into a need to verify certificates deployed for LDAPS on Active Directory. PowerShell remains my tool of choice for such occasions as this.

It is possible to very simply test an LDAPS connection using ADSI (System.DirectoryServices) like this:

$DirectoryEntry = New-Object DirectoryServices.DirectoryEntry("LDAP://someserver.domain.example")
$DirectoryEntry.AuthenticationType = [DirectoryServices.AuthenticationTypes]"Secure, SecureSocketsLayer, ServerBind"
$DirectoryEntry

A big red error suggests LDAPS is not avaiable of there is some other problem with the LDAP service. Anything else means the connection must have succeeded. We can infer that SSL negotiation also succeeded since we asked for it. Note that Secure is used to bind using the current users credentials, it is not necessary to provide explicit credentials for this. ServerBind removes any chance of the DCLocator becoming involved.

This method doesn’t really reveal very much unless you run a packet sniffer in parallel. I’m much happier when I can dig a bit deeper.

Happily the System.DirectoryServices.Protocols assembly contains just the tools we need to dig. It contains:

  • A means of defining an LDAP connection with a reasonable amount of detail
  • A way of showing negotiated SSL values such as protocols, hashing algorithms, and so on.
  • A callback delegate which will let us take a look at the certificate used for the connection.

Before we start, System.DirectoryServices.Protocols must be imported into the current session.

Add-Type -Assembly System.DirectoryServices.Protocols

Constructing the LDAP connection is simple. I elected to use the LdapDirectoryIdentifier, it saves a bit of messing around constructing strings describing the end-point.

$ComputerName = "someserver.domain.example"
$Port = 636
$DirectoryIdentifier = New-Object DirectoryServices.Protocols.LdapDirectoryIdentifier($ComputerName, $Port)

Next task is to build the LdapConnection object. Using Kerberos as the AuthType allows us to continue without authenticating again.

$Connection = New-Object DirectoryServices.Protocols.LdapConnection($DirectoryIdentifier)
$Connection.AuthType = [DirectoryServices.Protocols.AuthType]::Kerberos

Next a few session options should be set. By default the connection will be plain text and LDAPv2 will be used.

$Connection.SessionOptions.ProtocolVersion = 3
$Connection.SessionOptions.SecureSocketLayer = $true

If you look at the Connection object you should see an SslInformation property. Once we’ve bound to the directory this will be filled with algorithm information assuming SSL is successfully negotiated.

Getting any information about the certificate back needs a Callback Delegate defining for the VerifyServerCertificate property. Happily doing this needs nothing more complex than a ScriptBlock. I’m going to use a global scoped variable to let that return something to the main code block.

New-Variable LdapCertificate -Scope Global -Force
$Connection.SessionOptions.VerifyServerCertificate = {
  param(
    [DirectoryServices.Protocols.LdapConnection]$Connection,
    [Security.Cryptography.X509Certificates.X509Certificate2]$Certificate
  )
      
  $Global:LdapCertificate = $Certificate
    
  return $true
}

If you follow the link you’ll find the second argument passed into the delegate is X509Certificate (not X509Certificate2). Happily we can cast from one to the other, this exposes a few more properties we can immediately use.

Having assembled all of that, asking the connection to Bind will connect to the directory service and, hopefully, show us the certificate used.

$Connection.Bind()

As with the simple version at the top, a big red error suggests the directory service either doesn’t have a certificate or something else went wrong.

Having bound you can inspect these two to see what happened (note that LdapCertificate is the global variable):

$Connection.SessionOptions.SslInformation
$LdapCertificate | Format-List *

Finally, I’ve taken all that and wrapped it into a function which will return that as a handy object.

function Test-InLdapSSLConnection {
  # .SYNOPSIS
  #   Test and LDAPS connection.
  # .DESCRIPTION
  #   Test an LDAP connection returning information about the negotiated SSL connection including the server certificate.
  #
  #   The state message "The LDAP server is unavailable" indicates the server is either offline or unwilling to negotiate an SSL connection.
  # .PARAMETER ComputerName
  #   The name of a computer to test. By default serverless binding is used.
  # .PARAMETER Credential
  #   Credentials to use for the bind attempt. This command requires no special privileges.
  # .PARAMETER Port
  #   The port to connect to, by default the LDAPS port (636) is used.
  # .INPUTS
  #   System.Management.Automation.PSCredential
  #   System.String
  #   System.UInt16
  # .OUTPUTS
  #   Indented.LDAP.ConnectionInformation
  # .EXAMPLE
  #   Test-InLdapSSLConnection
  #
  #   Attempt to bind using SSL and serverless binding.
  # .EXAMPLE
  #   Test-InLdapSSLConnection -ComputerName servername
  #
  #   Attempt to negotiate SSL with "servername".
  # .NOTES
  #   Author: Chris Dent
  #
  #   Change log:
  #     31/03/2015 - Chris Dent - First release.

  param(
    [Parameter(ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true)]
    [Alias('DnsHostName')]
    [String]$ComputerName = "",
    
    [UInt16]$Port = 636,
    
    [PSCredential]$Credential
  )

  process {
    $DirectoryIdentifier = New-Object DirectoryServices.Protocols.LdapDirectoryIdentifier($ComputerName, $Port)
    if ($psboundparameters.ContainsKey("Credential")) {
      $Connection = New-Object DirectoryServices.Protocols.LdapConnection($DirectoryIdentifier, $Credential.GetNetworkCredential())
      $Connection.AuthType = [DirectoryServices.Protocols.AuthType]::Basic
    } else {
      $Connection = New-Object DirectoryServices.Protocols.LdapConnection($DirectoryIdentifier)
      $Connection.AuthType = [DirectoryServices.Protocols.AuthType]::Kerberos
    }
    $Connection.SessionOptions.ProtocolVersion = 3
    $Connection.SessionOptions.SecureSocketLayer = $true
    
    # Declare a script level variable which can be used to return information from the delegate.
    New-Variable LdapCertificate -Scope Script -Force
    
    # Create a callback delegate to retrieve the negotiated certificate.
    # Note:
    #   * The certificate is unlikely to return the subject.
    #   * The delegate is documented as using the X509Certificate type, automatically casting this to X509Certificate2 allows access to more information.
    $Connection.SessionOptions.VerifyServerCertificate = {
      param(
        [DirectoryServices.Protocols.LdapConnection]$Connection,
        [Security.Cryptography.X509Certificates.X509Certificate2]$Certificate
      )
      
      $Script:LdapCertificate = $Certificate
    
      return $true
    }
    
    $State = "Connected"  
    try {
      $Connection.Bind()
    } catch {
      $State = "Failed ($($_.Exception.InnerException.Message.Trim()))"
    }
    
    $ConnectionInformation = New-Object PSObject -Property ([Ordered]@{
      ComputerName         = $ComputerName
      Port                 = $Port
      State                = $State
      Protocol             = $Connection.SessionOptions.SslInformation.Protocol
      AlgorithmIdentifier  = $Connection.SessionOptions.SslInformation.AlgorithmIdentifier
      CipherStrength       = $Connection.SessionOptions.SslInformation.CipherStrength
      Hash                 = $Connection.SessionOptions.SslInformation.Hash
      HashStrength         = $Connection.SessionOptions.SslInformation.HashStrength
      KeyExchangeAlgorithm = [Security.Authentication.ExchangeAlgorithmType][Int]$Connection.SessionOptions.SslInformation.KeyExchangeAlgorithm
      ExchangeStrength     = $Connection.SessionOptions.SslInformation.ExchangeStrength
      X509Certificate      = $Script:LdapCertificate
    })
    
    $ConnectionInformation.PSObject.TypeNames.Add("Indented.LDAP.ConnectionInformation")
    
    return $ConnectionInformation
  }
}

Dynamic parameters

$
0
0

This short function simplifies the creation of dynamic parameters. The function exposes each of the possible validators as well as a few of the less well known options for ValidateSet and ValidatePattern.

function New-DynamicParameter {
  # .SYNOPSIS
  #   Create a new dynamic parameter object for use with a dynamicparam block.
  # .DESCRIPTION
  #   New-DynamicParameter allows simplified creation of runtime (dynamic) parameters.
  # .PARAMETER ParameterName
  #   The name of the parameter to create.
  # .PARAMETER ParameterType
  #   The .NET type of this parameter. 
  # .PARAMETER Mandatory
  #   Set the mandatory flag for this parameter.
  # .PARAMETER Position
  #   Define a position for the parameter.
  # .PARAMETER ValueFromPipeline
  #   The parameter can be filled from the input pipeline.
  # .PARAMETER ValueFromPipelineByPropertyName
  #   The parameter can be filled from a specific property in the input pipeline.
  # .PARAMETER ParameterSetName
  #   Assign the parameter to a specific parameter set.
  # .PARAMETER ValidateNotNullOrEmpty
  #   Disallow null or empty values for the parameter if the parameter is specified.
  # .PARAMETER ValidatePattern
  #   Test the parameter value using a regular expression.
  # .PARAMETER ValidatePatternOptions
  #   Regular expression options which dictate the behaviour of ValidatePattern.
  # .PARAMETER ValidateRange
  #   A minimum and maximum value to compare the argument to.
  # .PARAMETER ValidateScript
  #   Test the parameter value using a script.
  # .PARAMETER ValidateSet
  #   Test the parameter value against a set of values.
  # .PARAMETER ValidateSetIgnoreCase
  #   ValidateSet can be configured to be case sensitive by setting this parameter to $false. The default behaviour for ValidateSet ignores case.
  # .INPUTS
  #   System.Object
  #   System.Object[]
  #   System.String
  #   System.Type
  # .OUTPUTS
  #   System.Management.Automation.RuntimeDefinedParameter
  # .EXAMPLE
  #   New-DynamicParameter Name -DefaultValue "Test" -ParameterType "String" -Mandatory -ValidateSet "Test", "Live"
  # .EXAMPLE
  #   New-DynamicParameter Name -ValueFromPipelineByPropertyName
  # .EXAMPLE
  #   New-DynamicParameter Name -ValidateRange 1, 2
  # .NOTES
  #   Author: Chris Dent
  #
  #   Change log:
  #     24/10/2014 - Chris Dent - Added support for ValidatePattern options and ValidateSet case sensitivity.
  #     22/10/2014 - Chris Dent - First release.

  [CmdLetBinding()]
  param(
    [Parameter(Mandatory = $true)]
    [ValidateNotNullOrEmpty()]
    [Alias('Name')]
    [String]$ParameterName,
    
    [Object]$DefaultValue,
    
    [Type]$ParameterType = "Object",

    [Switch]$Mandatory,

    [Int32]$Position = -2147483648,
    
    [Switch]$ValueFromPipeline,
    
    [Switch]$ValueFromPipelineByPropertyName,
    
    [String]$ParameterSetName = "__AllParameterSets",
    
    [Switch]$ValidateNotNullOrEmpty,

    [ValidateNotNullOrEmpty()]
    [RegEx]$ValidatePattern,
    
    [Text.RegularExpressions.RegexOptions]$ValidatePatternOptions = [Text.RegularExpressions.RegexOptions]::IgnoreCase,

    [Object[]]$ValidateRange,
    
    [ValidateNotNullOrEmpty()]
    [ScriptBlock]$ValidateScript,
   
    [ValidateNotNullOrEmpty()]
    [Object[]]$ValidateSet,

    [Boolean]$ValidateSetIgnoreCase = $true
  )
  
  $AttributeCollection = @()

  $ParameterAttribute = New-Object Management.Automation.ParameterAttribute
  $ParameterAttribute.Mandatory = $Mandatory
  $ParameterAttribute.Position = $Position
  $ParameterAttribute.ValueFromPipeline = $ValueFromPipeline
  $ParameterAttribute.ValueFromPipelineByPropertyName = $ValueFromPipelineByPropertyName

  $AttributeCollection += $ParameterAttribute

  if ($psboundparameters.ContainsKey('ValidateNotNullOrEmpty')) {
    $AttributeCollection += New-Object Management.Automation.ValidateNotNullOrEmptyAttribute
  }
  if ($psboundparameters.ContainsKey('ValidatePattern')) {
    $ValidatePatternAttribute = New-Object Management.Automation.ValidatePatternAttribute($ValidatePattern.ToString())
    $ValidatePatternAttribute.Options = $ValidatePatternOptions

    $AttributeCollection += $ValidatePatternAttribute
  }
  if ($psboundparameters.ContainsKey('ValidateRange')) {
    $AttributeCollection += New-Object Management.Automation.ValidateRangeAttribute($ValidateRange)
  }
  if ($psboundparameters.ContainsKey('ValidateScript')) {
    $AttributeCollection += New-Object Management.Automation.ValidateScriptAttribute($ValidateScript)
  }
  if ($psboundparameters.ContainsKey('ValidateSet')) {
    $ValidateSetAttribute = New-Object Management.Automation.ValidateSetAttribute($ValidateSet)
    $ValidateSetAttribute.IgnoreCase = $ValidateSetIgnoreCase

    $AttributeCollection += $ValidateSetAttribute
  }

  $Parameter = New-Object Management.Automation.RuntimeDefinedParameter($ParameterName, $ParameterType, $AttributeCollection)
  if ($psboundparameters.ContainsKey('DefaultValue')) {
    $Parameter.Value = $DefaultValue
  }
  return $Parameter
}

Viewing all 23 articles
Browse latest View live