15

SCENARIO

To simplify this down to it's easiest example:

I have a Windows 2008 R2 standard DC with the DHCP server role. It hands out IPs via various IPv4 scopes, no problem there.

WHAT I'D LIKE

I would like a way to create a notification/eventlog entry/similar whenever a device gets a DHCP address lease and that device IS NOT a domain joined computer in Active Directory. It doesn't matter to me whether it is custom Powershell, etc.

Bottom line = I'd like a way to know when non-domain devices are on the network without using 802.1X at the moment. I know this won't account for static IP devices. I do have monitoring software that will scan the network and find devices, but it isn't quite this granular in detail.

RESEARCH DONE/OPTIONS CONSIDERED

I don't see any such possibilities with the built in logging.

Yes, I'm aware of 802.1X and have the ability to implement it long-term at this location but we are some time away from a project like that, and while that would solve network authentication issues, this is still helpful to me outside of 802.1X goals.

I've looked around for some script bits, etc. that might prove useful but the things I'm finding lead me to believe that my google-fu is failing me at the moment.

I believe the below logic is sound (assuming there isn't some existing solution):

  1. Device receives DHCP address
  2. Event log entry is recorded (event ID 10 in the DHCP audit log should work (since a new lease is what I'd be most interested in, not renewals): http://technet.microsoft.com/en-us/library/dd759178.aspx)
  3. At this point a script of some kind would probably have to take over for the remaining "STEPS" below.
  4. Somehow query this DHCP log for these event ID 10's (I would love push, but I'm guessing pull is the only recourse here)
  5. Parse the query for the name of the device being assigned the new lease
  6. Query AD for the device's name
  7. IF not found in AD, send a notification email

If anyone has any ideas on how to properly do this, I'd really appreciate it. I'm not looking for a "gimme the codez" but would love to know if there are alternatives to the above list or if I'm not thinking clear and another method exists for gathering this information. If you have code snippets/PS commands you'd like to share to help accomplish this, all the better.

4
  • Are you looking to block them, or just get notified if they get an IP?
    – HostBits
    Nov 4, 2013 at 20:58
  • @Cheekaleak - just get notified.
    – TheCleaner
    Nov 4, 2013 at 21:09
  • What about network printers that use DHCP?
    – jftuga
    Nov 6, 2013 at 16:48
  • @jftuga - we use static IPs for network printers.
    – TheCleaner
    Nov 6, 2013 at 16:50

4 Answers 4

6

With much thanks to ErikE and the others here, I've gone down a path...I won't say it's the right path, but the Powershell script I've come up with does the trick.

The code is below if anyone wants it. Just run it manually pointing at each DHCP server or schedule it (again pointing to each DHCP server in the script).

What the script does:

  1. Gets lease information from the DHCP server (ipv4 leases)
  2. Outputs the leases to a csv file
  3. Reads back in that CSV file for querying AD
  4. Queries AD for the computer
  5. If not found outputs to a new txt file
  6. Creates a unique list final txt file from the one created in #5 above (since there can be dupes if the client registers more than once or with more than one adapter)
  7. emails the contents of the final output file to an admin

What you'll need:

The script uses the AD module (import-module activedirectory) so it is best run on an AD DC running DHCP. If this isn't the case for you, you can install the AD powershell module: http://blogs.msdn.com/b/rkramesh/archive/2012/01/17/how-to-add-active-directory-module-in-powershell-in-windows-7.aspx

You'll also need Quest's AD Powershell cmdlets found here: http://www.quest.com/powershell/activeroles-server.aspx . Install THESE BEFORE running the script or it will fail.

The script itself (sanitized, you'll need to setup some of the variables to suit your needs like the input file names, domain to connect to, dhcp server to connect to, email settings near the end, etc.):

# Get-nonADclientsOnDHCP.ps1

# Author : TheCleaner http://serverfault.com/users/7861/thecleaner with a big thanks for a lot of the lease grab code to Assaf Miron on code.google.com

# Description : This Script grabs the current leases on a Windows DHCP server, outputs it to a csv
# then takes that csv file as input and determines if the lease is from a non-AD joined computer.  It then emails
# an administrator notification.  Set it up on a schedule of your choosing in Task Scheduler.
# This helps non-802.1X shops keep track of rogue DHCP clients that aren't part of the domain.

#

# Input : leaselog.csv

# Output: Lease log = leaselog.csv
# Output: Rogue Clients with dupes = RogueClients.txt
# Output: Rogue Clients - unique = RogueClientsFinal.txt

$DHCP_SERVER = "PUT YOUR SERVER NAME OR IP HERE" # The DHCP Server Name

$LOG_FOLDER = "C:\DHCP" # A Folder to save all the Logs

# Create Log File Paths

$LeaseLog = $LOG_FOLDER+"\LeaseLog.csv"

#region Create Scope Object

# Create a New Object

$Scope = New-Object psobject

# Add new members to the Object

$Scope | Add-Member noteproperty "Address" ""

$Scope | Add-Member noteproperty "Mask" ""

$Scope | Add-Member noteproperty "State" ""

$Scope | Add-Member noteproperty "Name" ""

$Scope | Add-Member noteproperty "LeaseDuration" ""

# Create Each Member in the Object as an Array

$Scope.Address = @()

$Scope.Mask = @()

$Scope.State = @()

$Scope.Name = @()

$Scope.LeaseDuration = @()

#endregion


#region Create Lease Object

# Create a New Object

$LeaseClients = New-Object psObject

# Add new members to the Object

$LeaseClients | Add-Member noteproperty "IP" ""

$LeaseClients | Add-Member noteproperty "Name" ""

$LeaseClients | Add-Member noteproperty "Mask" ""

$LeaseClients | Add-Member noteproperty "MAC" ""

$LeaseClients | Add-Member noteproperty "Expires" ""

$LeaseClients | Add-Member noteproperty "Type" ""

# Create Each Member in the Object as an Array

$LeaseClients.IP = @()

$LeaseClients.Name = @()

$LeaseClients.MAC = @()

$LeaseClients.Mask = @()

$LeaseClients.Expires = @()

$LeaseClients.Type = @()

#endregion


#region Create Reserved Object

# Create a New Object

$LeaseReserved = New-Object psObject

# Add new members to the Object

$LeaseReserved | Add-Member noteproperty "IP" ""

$LeaseReserved | Add-Member noteproperty "MAC" ""

# Create Each Member in the Object as an Array

$LeaseReserved.IP = @()

$LeaseReserved.MAC = @()

#endregion


#region Define Commands

#Commad to Connect to DHCP Server

$NetCommand = "netsh dhcp server \\$DHCP_SERVER"

#Command to get all Scope details on the Server

$ShowScopes = "$NetCommand show scope"

#endregion


function Get-LeaseType( $LeaseType )

{

# Input : The Lease type in one Char

# Output : The Lease type description

# Description : This function translates a Lease type Char to it's relevant Description


Switch($LeaseType){

"N" { return "None" }

"D" { return "DHCP" }

"B" { return "BOOTP" }

"U" { return "UNSPECIFIED" }

"R" { return "RESERVATION IP" }

}

}


function Check-Empty( $Object ){

# Input : An Object with values.

# Output : A Trimmed String of the Object or '-' if it's Null.

# Description : Check the object if its null or not and return it's value.

If($Object -eq $null)

{

return "-"

}

else

{

return $Object.ToString().Trim()

}

}


function out-CSV ( $LogFile, $Append = $false) {

# Input : An Object with values, Boolean value if to append the file or not, a File path to a Log File

# Output : Export of the object values to a CSV File

# Description : This Function Exports all the Values and Headers of an object to a CSV File.

#  The Object is recieved with the Input Const (Used with Pipelineing) or the $inputObject

Foreach ($item in $input){

# Get all the Object Properties

$Properties = $item.PsObject.get_properties()

# Create Empty Strings - Start Fresh

$Headers = ""

$Values = ""

# Go over each Property and get it's Name and value

$Properties | %{ 

$Headers += $_.Name + ","

$Values += $_.Value

}

# Output the Object Values and Headers to the Log file

If($Append -and (Test-Path $LogFile)) {

$Values | Out-File -Append -FilePath $LogFile -Encoding Unicode

}

else {

# Used to mark it as an Powershell Custum object - you can Import it later and use it

# "#TYPE System.Management.Automation.PSCustomObject" | Out-File -FilePath $LogFile

$Headers | Out-File -FilePath $LogFile -Encoding Unicode

$Values | Out-File -Append -FilePath $LogFile -Encoding Unicode

}

}

}


#region Get all Scopes in the Server 

# Run the Command in the Show Scopes var

$AllScopes = Invoke-Expression $ShowScopes

# Go over all the Results, start from index 5 and finish in last index -3

for($i=5;$i -lt $AllScopes.Length-3;$i++)

{

# Split the line and get the strings

$line = $AllScopes[$i].Split("-")

$Scope.Address += Check-Empty $line[0]

$Scope.Mask += Check-Empty $line[1]

$Scope.State += Check-Empty $line[2]

# Line 3 and 4 represent the Name and Comment of the Scope

# If the name is empty, try taking the comment

If (Check-Empty $line[3] -eq "-") {

$Scope.Name += Check-Empty $line[4]

}

else { $Scope.Name += Check-Empty $line[3] }

}

# Get all the Active Scopes IP Address

$ScopesIP = $Scope | Where { $_.State -eq "Active" } | Select Address

# Go over all the Adresses to collect Scope Client Lease Details

Foreach($ScopeAddress in $ScopesIP.Address){

# Define some Commands to run later - these commands need to be here because we use the ScopeAddress var that changes every loop

#Command to get all Lease Details from a specific Scope - when 1 is amitted the output includes the computer name

$ShowLeases = "$NetCommand scope "+$ScopeAddress+" show clients 1"

#Command to get all Reserved IP Details from a specific Scope

$ShowReserved = "$NetCommand scope "+$ScopeAddress+" show reservedip"

#Command to get all the Scopes Options (Including the Scope Lease Duration)

$ShowScopeDuration = "$NetCommand scope "+$ScopeAddress+" show option"

# Run the Commands and save the output in the accourding var

$AllLeases = Invoke-Expression $ShowLeases 

$AllReserved = Invoke-Expression $ShowReserved 

$AllOptions = Invoke-Expression $ShowScopeDuration

# Get the Lease Duration from Each Scope

for($i=0; $i -lt $AllOptions.count;$i++) 

{ 

# Find a Scope Option ID number 51 - this Option ID Represents  the Scope Lease Duration

if($AllOptions[$i] -match "OptionId : 51")

{ 

# Get the Lease Duration from the Specified line

$tmpLease = $AllOptions[$i+4].Split("=")[1].Trim()

# The Lease Duration is recieved in Ticks / 10000000

$tmpLease = [int]$tmpLease * 10000000; # Need to Convert to Int and Multiply by 10000000 to get Ticks

# Create a TimeSpan Object

$TimeSpan = New-Object -TypeName TimeSpan -ArgumentList $tmpLease

# Calculate the $tmpLease Ticks to Days and put it in the Scope Lease Duration

$Scope.LeaseDuration += $TimeSpan.TotalDays

# After you found one Exit the For

break;

} 

}

# Get all Client Leases from Each Scope

for($i=8;$i -lt $AllLeases.Length-4;$i++)

{

# Split the line and get the strings

$line = [regex]::split($AllLeases[$i],"\s{2,}")

# Check if you recieve all the lines that you need

$LeaseClients.IP += Check-Empty $line[0]

$LeaseClients.Mask += Check-Empty $line[1].ToString().replace("-","").Trim()

$LeaseClients.MAC += $line[2].ToString().substring($line[2].ToString().indexOf("-")+1,$line[2].toString().Length-1).Trim()

$LeaseClients.Expires += $(Check-Empty $line[3]).replace("-","").Trim()

$LeaseClients.Type += Get-LeaseType $(Check-Empty $line[4]).replace("-","").Trim()

$LeaseClients.Name += Check-Empty $line[5]

}

# Get all Client Lease Reservations from Each Scope

for($i=7;$i -lt $AllReserved.Length-5;$i++)

{

# Split the line and get the strings

$line = [regex]::split($AllReserved[$i],"\s{2,}")

$LeaseReserved.IP += Check-Empty $line[0]

$LeaseReserved.MAC += Check-Empty $line[2]

}

}

#endregion 


#region Create a Temp Scope Object

# Create a New Object

$tmpScope = New-Object psobject

# Add new members to the Object

$tmpScope | Add-Member noteproperty "Address" ""

$tmpScope | Add-Member noteproperty "Mask" ""

$tmpScope | Add-Member noteproperty "State" ""

$tmpScope | Add-Member noteproperty "Name" ""

$tmpScope | Add-Member noteproperty "LeaseDuration" ""

#endregion

#region Create a Temp Lease Object

# Create a New Object

$tmpLeaseClients = New-Object psObject

# Add new members to the Object

$tmpLeaseClients | Add-Member noteproperty "IP" ""

$tmpLeaseClients | Add-Member noteproperty "Name" ""

$tmpLeaseClients | Add-Member noteproperty "Mask" ""

$tmpLeaseClients | Add-Member noteproperty "MAC" ""

$tmpLeaseClients | Add-Member noteproperty "Expires" ""

$tmpLeaseClients | Add-Member noteproperty "Type" ""

#endregion

#region Create a Temp Reserved Object

# Create a New Object

$tmpLeaseReserved = New-Object psObject

# Add new members to the Object

$tmpLeaseReserved | Add-Member noteproperty "IP" ""

$tmpLeaseReserved | Add-Member noteproperty "MAC" ""

#endregion

# Go over all the Client Lease addresses and export each detail to a temporary var and out to the log file

For($l=0; $l -lt $LeaseClients.IP.Length;$l++)

{

# Get all Scope details to a temp var

$tmpLeaseClients.IP = $LeaseClients.IP[$l] + ","

$tmpLeaseClients.Name = $LeaseClients.Name[$l] + ","

$tmpLeaseClients.Mask =  $LeaseClients.Mask[$l] + ","

$tmpLeaseClients.MAC = $LeaseClients.MAC[$l] + ","

$tmpLeaseClients.Expires = $LeaseClients.Expires[$l] + ","

$tmpLeaseClients.Type = $LeaseClients.Type[$l]

# Export with the Out-CSV Function to the Log File

$tmpLeaseClients | out-csv $LeaseLog -append $true

}



#Continue on figuring out if the DHCP lease clients are in AD or not

#Import the Active Directory module
import-module activedirectory

#import Quest AD module
Add-PSSnapin Quest.ActiveRoles.ADManagement

#connect to AD
Connect-QADService PUTTHEFQDNOFYOURDOMAINHERE_LIKE_DOMAIN.LOCAL | Out-Null

# get input CSV
$leaselogpath = "c:\DHCP\LeaseLog.csv"
Import-csv -path $leaselogpath | 
#query AD for computer name based on csv log
foreach-object `
{ 
   $NameResult = Get-QADComputer -DnsName $_.Name
   If ($NameResult -eq $null) {$RogueSystem = $_.Name}
   $RogueSystem | Out-File C:\DHCP\RogueClients.txt -Append
   $RogueSystem = $null

}
Get-Content C:\DHCP\RogueClients.txt | Select-Object -Unique | Out-File C:\DHCP\RogueClientsFinal.txt
Remove-Item C:\DHCP\RogueClients.txt

#send email to netadmin
$smtpserver = "SMTP SERVER IP"
$from="[email protected]"
$to="[email protected]"
$subject="Non-AD joined DHCP clients"
$body= (Get-Content C:\DHCP\RogueClientsFinal.txt) -join '<BR>&nbsp;<BR>'
$mailer = new-object Net.Mail.SMTPclient($smtpserver)
$msg = new-object Net.Mail.MailMessage($from,$to,$subject,$body)
$msg.IsBodyHTML = $true
$mailer.send($msg)

Hope that helps someone else!

3

OK, I´m not sure I´m following etiquette here but am posting a second answer instead of editing my previous one, as it did contain some info which may be of use to somebody even if proven irrelevant to this case. If that makes me an idiot in this forum feel free to inform me of my erroneous ways.

The problem is divided into several parts, here are suggestions for the ones I find most interesting. Without examples from the log this is the best I can do, so it's just suggestions not solutions.

To parse the log use get-content with the -wait parameter. For my use case it's enough to find an error in an error log.

This is what worked for my own use case, forgive the formatting:

get-content E:\temp13\log.txt -tail(1) -wait | where {$_ -match "ERROR"} |
    foreach {
        send-mailmessage `
        -port 25 `
        -smtpserver my.mail.server `
        -from [email protected] `
        -to [email protected] `
        -subject "test logmonitor" `
        -body "ERROR found: $_" `
        }

Instead of the $_ -match "ERROR" you would need to separate the log ID field and the computer name somehow. I'm not sure how to go about that in the best way right now, but since where-object -match gives regex support I guess that could be an option. You could also begin by storing the $_ variable in another new variable, to be able to pick it up at your convenience later in the pipeline, inside nested foreach loops etc.

Assuming you can get at the computername, I guess the get-adcomputer cmdlet would be your simplest way of querying your AD (import-module activedirectory), and I guess on error send mail?

Using the import-csv would of course be far more elegant in your case, but I'm not aware of any way of tailing it (if anybody happens to read this and knows a trick up that alley then please, please share).

3
  • Thanks ErikE, I'll run with this and let you know. I'll need to figure out a way to grab the info, query AD, then after the "alert" ignore future checks of that same line of input. For instance, if it queries the file every five minutes I don't want it to reparse the same info and send a dupe alert every 5 minutes.
    – TheCleaner
    Nov 5, 2013 at 14:01
  • Two things I find slight neat: If you just let the script run, the wait parameter will have it constantly waiting for a new line to appear. You don't have to rerun the script. Hhis will give a push effect rather than pull. Also, the tail(1) will have it just parsing the last 1 line at start. Thus, if task manager finds it has to restart the script, and you find a way to inject a placeholder line displacing the last line to trigger an alert, you will have disarmed the annoyance of realerts.
    – ErikE
    Nov 5, 2013 at 17:12
  • 1
    Erik, I've found a way to export the leases from DHCP (crappy that 2012 has a PS cmdlet for it but 2008 doesn't) to a csv file. This way I don't mess with the actual audit logs and don't have to be concerned about breaking anything with input. I'll start messing with getting the rest of the code done and update soon.
    – TheCleaner
    Nov 5, 2013 at 22:40
1

Under the assumption that you are certain of the Event ID, and that no other events log to this ID in the DHCP log but the ones you are interested in, push is indeed an option.

1) Open the Server Manager, go to the DHCP log in Event Viewer.

2) Find a representative entry which you wish to attach your action to. Select it and right click.

3) Choose "Attach Task To This Event".

4) The Task Creation Wizard opens, take it away from there...

There is actually an explicit email option, but if you need more logic than that you are of course free to use the start-a-program option to fire up powershell.exe and attach a script to it. There are plenty of excellent googleable howtos on how to let the Task Manager run powershell scripts if you need guidance.

The straight away alternative I see is to use pull by parsing the Event Log using powershell at scheduled intervals. "The Microsoft Scripting Guy", aka Ed Wilson has written some awesome blog posts on how to parse the Event Log using the cmdlets available in the different versions of powershell, so taking his blog as a starting point would be my suggestion.

As for actual cmdlets I don´t have the time right now to pull out my stash of handy snippets, but will look in again in a day or two and may contribute if nobody else has pitched in with some well chosen ones, or you haven´t solved it all by yourself :-)

3
  • 2
    Erik, thank you. The problem here is that the DHCPsrvlog-"day" in C:\windows\system32\DHCP (with DHCP auditing enabled in the DHCP server GUI), doesn't write to any Event Viewer log including the DHCP-Server event viewer log under Applications and Services Logs (so far based on my research/testing)
    – TheCleaner
    Nov 4, 2013 at 20:59
  • I had forgotten about those logs. But I believe this is a possible venue: Parse the text log using get-content with the -wait and -tail directives. This is similar to tail in *nix. To make sure an instance is always parsing the log, the Task Manager can schedule the script at system startup, then start every (shortest interval possible) but only allow one running instance. If your event pops up, fire logic.
    – ErikE
    Nov 4, 2013 at 21:49
  • Turns out I have a similar log parsing problem to solve on Windows, I´ll post my findings on that particular part when I´m certain it works, and likely some other building blocks I have lying around which should be useful to you. Could you paste a couple of representative but obfuscated rows from your dhcp log? I´m particularly interested in the device name format.
    – ErikE
    Nov 4, 2013 at 22:48
1

While this doesn't address your desired solution, an option that may achieve your goal is to utilize arpwatch(link) to notify you when a new (previously unseen) host is seen on the network.

A Windows alternative to arpwatch appears to be decaffeinatid but I've never used it so can't speak for it good or bad.

2
  • Thanks. The idea there is sound. I may go down that path if necessary.
    – TheCleaner
    Nov 4, 2013 at 22:29
  • The added benefit is that this would also catch new machines that use static IPs, which was not in your stated scope but probably should be.
    – mfinni
    Nov 5, 2013 at 13:38

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .