LogoLogo
  • PlaceOS Documentation
  • Overview
    • Key Concepts
      • Drivers
      • Interfaces
      • Modules
      • Settings
      • Systems
      • Triggers
      • Zones
    • Languages
      • Crystal
      • TypeScript
    • Protocols
      • MQTT
      • SAML
      • OAuth2
  • How To
    • Configure PlaceOS for Microsoft 365
      • Step 1: Room Calendar Access
        • Create Azure App Registration (Application Permissions)
        • Exchange Calendar Group
        • Limit Application Permissions
        • Configure PlaceOS Calendar Driver
      • Step 2: User Authentication & Calendar Access
        • Create a PlaceOS Authentication Source
        • Create Azure App Registration (Delegated Permissions)
        • Configure PlaceOS Authentication Source
        • Add User Login Redirects
      • Concierge Access
      • Troubleshooting
        • Blocked or Blacklisted IP Error
    • Configure PlaceOS for Google Workspace
      • Google Configuration
        • Create Google Cloud Project & Enable API
        • Configure Google Cloud Service Account
        • Add Google Workplace Permissions
        • Create Google Marketplace App (optional)
        • Google Workspace Service User (RBAC)
        • Configure Access to Google Resource Calendars
      • User Authentication
        • Create a PlaceOS Authentication Source for Google
        • Create Google Cloud OAuth2 Client App
        • Configure PlaceOS Auth Source for Google
        • Add User Login Redirects
    • Deployment
      • Deploy AWS Fargate on Modular CloudFormation Stacks
      • Deploy AWS Fargate on Nested CloudFormation Stacks
      • Writing Import Scripts
    • Analytics
      • MQTT Integration
    • Backoffice
      • Add a Domain to PlaceOS
      • Backoffice File Upload
      • Configure Staff API
      • Calendar Driver
      • Enable Sensor UI
      • Bookings Driver
      • Configure a webhook
    • Authentication
      • Azure B2C
        • Azure B2C Custom Policy Framework
        • Configure PlaceOS for Azure B2C
        • 365 Room Resources on Azure B2C
      • Configure SAML SSO
        • Configure SAML2 with AD FS
        • Configure SAML2 with Auth0
        • Configure SAML2 with Azure AD
        • Configure SAML2 with Google Workspace
      • Configure OAuth2 SSO
      • X-API Keys
      • Bearer tokens
    • Location Services
      • Location Services
      • Area Management
      • Discovering User Devices
      • Locating Users on a Network
      • People Finding with Cisco Meraki on PlaceOS
      • People Finding with Juniper Mist on PlaceOS
    • Notifications
      • Catering Orders
    • User Interfaces
      • Booking Panel App
      • Workplace App
      • Native Booking Panel App
      • Deploy a Frontend Interface
      • Microsoft Outlook Plugin
      • Configure Endpoint Auto Login
      • SVG Map Creation
      • Configuring a default UI
  • Tutorials
    • Setup a dev environment
    • Backend
      • Troubleshooting Backend Failures
      • Import Bookable Rooms
      • Writing A Driver
        • Testing drivers
        • ChatGPT / LLM Capabilities
          • Native GPT Plugins
      • Testing Internal Builds
    • Backoffice
      • Adding Drivers & Modules
      • Add Zone Structure
    • Common Configurations
      • Asset Manager
      • Catering
      • Locker Booking
      • Webex Instant Connect
      • Desk booking
      • Sensor Data Collection
        • Configure Kontakt IO
        • Configuring Meraki
        • Configuring DNA Spaces
      • Elevated Privileges
  • Reference
    • API
      • Real-time Websocket
      • Rest API
      • Staff API
    • Drivers
      • PlaceOS
        • Bookings
        • Staff API
        • Visitor Mailer
        • Lockers
      • Microsoft
        • Graph API
    • PlaceOS Skills
    • Privacy Policy
    • Recommended Products
    • Supported Integrations
    • System Architecture
    • System Functionality & Requirements
    • Infrastructure Requirements
    • Security Compliance
      • FAQ
      • GDPR
      • Security
    • Microsoft Azure Permissions
  • Glossary
  • 🎯PlaceOS Roadmap
  • 🆘PlaceOS Support
  • 👩‍💻PlaceOS Github
  • 📝PlaceOS Changelog
Powered by GitBook
On this page
  • Remote Event Query
  • Querying a MS Network Policy Server (RADIUS)
  • Workstation Monitoring
  • Untrusted or Self Signed Certificates
  • Protocol violation errors
Export as PDF
  1. How To
  2. Location Services

Discovering User Devices

Map Usernames to MAC Addresses via IP Addresses

PreviousArea ManagementNextLocating Users on a Network

Last updated 3 years ago

This is for mapping usernames to MAC addresses via IP address. It requires multiple points of integration and captures data from Windows Domains.

  • We use for IP address filtering

Remote Event Query

This grabs user device details that are interacting with the domain controller. It takes five minutes worth of details, so should run every five minutes.

# Required for reliable Resolve-DnsName.
import-module dnsclient

# Helper for filtering IP addresses belonging to a subnet
function checkSubnet ([string]$cidr, [string]$ip) {
    $network, [uint32]$subnetlen = $cidr.Split('/')
    $a = [uint32[]]$network.split('.')
    [uint32] $unetwork = ($a[0] -shl 24) + ($a[1] -shl 16) + ($a[2] -shl 8) + $a[3]
    $mask = (-bnot [uint32]0) -shl (32 - $subnetlen)
    $a = [uint32[]]$ip.split('.')
    [uint32] $uip = ($a[0] -shl 24) + ($a[1] -shl 16) + ($a[2] -shl 8) + $a[3]
    $unetwork -eq ($mask -band $uip)
}

# Use a password file: https://blogs.technet.microsoft.com/robcost/2008/05/01/powershell-tip-storing-and-using-password-credentials/
$User = "YourDomain\service_account"
$PWord = ConvertTo-SecureString -String "service_account_pass" -AsPlainText -Force
$Credential = New-Object -TypeName "System.Management.Automation.PSCredential" -ArgumentList $User, $PWord

$results = New-Object System.Collections.Generic.List[System.Object]
$ips = @()
$events = $null

try {
    Write-Host "Requesting events from remote server...";

    $events = Get-WinEvent -ComputerName "domain.controller.com" -Credential $Credential -LogName "Security" -FilterXPath @"
    *[System[Provider[@Name='Microsoft-Windows-Security-Auditing'] and
      EventID=4768 and TimeCreated[timediff(@SystemTime) <= 320000]]] and
    *[EventData[Data[@Name='Status'] and (Data='0x0')]] and
    *[EventData[Data[@Name='TargetDomainName'] and (Data='YourDomain')]]
"@
} catch {
    Write-Host "Server found no results...";
    Write-Host $_.Exception.Message;
    exit 0
}

Write-Host "Events received from remote server";

# This makes the events look like they were requested locally
# (remote event requests come back as generic objects)
ForEach ($event in $events) {
    $eventXML = [xml]$event.ToXml()

    # Iterate through each one of the XML message properties
    For ($i=0; $i -lt $eventXML.Event.EventData.Data.Count; $i++) {
        # Append these as object properties
        Add-Member -InputObject $event -MemberType NoteProperty -Force `
            -Name  $eventXML.Event.EventData.Data[$i].name `
            -Value $eventXML.Event.EventData.Data[$i].'#text'
    }
}

Write-Host "IP addresses discovered:";

$events | ForEach-Object {
    try {
        $ip = $_.IpAddress
        $username = $_.TargetUserName
        $userlower = $username.ToLower()
        $domain = $_.TargetDomainName

        # Ensure the event includes the IP address
        if ([string]::IsNullOrWhiteSpace($ip) -Or ($ip -eq "-") -Or [string]::IsNullOrWhiteSpace($username) -Or [string]::IsNullOrWhiteSpace($domain)) {
            return
        }

        # Ensure IP address is of the correct type
        [IPAddress]$address = $ip
        if (($PSVersionTable.PSVersion.Major -ge 5) -Or (($PSVersionTable.PSVersion.Major -eq 4) -And ($PSVersionTable.PSVersion.Minor -ge 5))) {
            if ($address.IsIPv4MappedToIPv6) {
                $ip = $address.MapToIPv4().IPAddressToString
            } else {
                # Ignore IPv6
                if ($address.AddressFamily.ToString() -eq "InterNetworkV6") {
                    Write-Host "Ignoring IPv6 address: ", $ip
                    return
                }
            }
        } else {
            # Check IPv6 Mapping manually
            if (($address.AddressFamily.ToString() -eq "InterNetworkV6") -And $ip.StartsWith("::ffff:")) {
                $new_ip = $ip.Split("::ffff:")[-1]

                try {
                    [IPAddress]$address = $new_ip
                    if ($address.AddressFamily.ToString() -eq "InterNetwork") {
                        $ip = $new_ip
                    }
                } catch {
                    # we are not interested in this IP address
                    Write-Host "Ignoring IPv6 address: ", $ip
                    return
                }
            }
        }

        # Check the IP address hasn't been seen already
        if ($ips.Contains($ip)) { return }

        # Filter IP ranges, service accounts and computer names$
        if ( `
            ( `
                (checkSubnet "127.0.0.0/16" $ip) -Or `
                (checkSubnet "192.168.0.0/16" $ip) -Or `
                (checkSubnet "192.155.0.0/16" $ip) `
            ) -and `
            (!$userlower.StartsWith("sccm.")) -and `
            (!$userlower.StartsWith("svc.")) -and `
            ($username[-1] -ne "$") `
        ) {
            $ips += $ip
            Write-Host $ip;

            # Try to grab the computers hostname
            try {
                $hostname = (Resolve-DnsName $ip -ErrorAction SilentlyContinue)[0].NameHost
                $results.Add(@($ip,$username,$domain,$hostname))
            } catch {
                $results.Add(@($ip,$username,$domain))
            }
        }
    } catch {
        Write-Host "Error parsing event";
        Write-Host $_.Exception.Message;
    }
}

$resultArr = $results.ToArray()

# Only post to the server if there are results
if ($resultArr.length -gt 0) {
    Write-Host "Posting to control server";

    # Send to the server
    $postParams = ConvertTo-Json @($resultArr)
    $res = Invoke-WebRequest -UseBasicParsing -Uri https://placeos.server.com/api/engine/v2/webhook/trig-98UC/notify?secret=046856ff816d49f26261d3c9c9789da&exec=true&mod=LocationServices&method=ip_mappings -Method POST -Body $postParams -ContentType "application/json" -TimeoutSec 40
    Write-Host "Response code was:" $res.StatusCode;

    if ($res.StatusCode -ne 200) {
        Write-Host "Webhook post failed...";
        exit 1
    }
} else {
    Write-Host "No results found...";
}

Querying a MS Network Policy Server (RADIUS)

This allows us to grab MAC addresses of BYOD devices. Useful if tracking mobile phones on the Wi-Fi is desirable.

# Required for reliable Resolve-DnsName.
import-module dnsclient
import-module dhcpserver

# Use a password file: https://blogs.technet.microsoft.com/robcost/2008/05/01/powershell-tip-storing-and-using-password-credentials/
$User = "YourDomain\service_account"
$PWord = ConvertTo-SecureString -String "service_account_pass" -AsPlainText -Force
$Credential = New-Object -TypeName "System.Management.Automation.PSCredential" -ArgumentList $User, $PWord

$results = New-Object System.Collections.Generic.List[System.Object]
$macs = @()
$events = $null

try {
    Write-Host "Requesting events from Network Policy Server...";

    $events = Get-WinEvent -ComputerName "radius.server.com" -Credential $Credential -LogName "Security" -FilterXPath @"
    *[System[Provider[@Name='Microsoft-Windows-Security-Auditing'] and
      EventID=6278 and TimeCreated[timediff(@SystemTime) <= 90000]]] and
    *[EventData[Data[@Name='SubjectDomainName'] and (Data='YourDomain')]]
"@
} catch {
    Write-Host "Server found no results...";
    Write-Host $_.Exception.Message;
    exit 0
}

Write-Host "Events received from remote server";

# This makes the events look like they were requested locally
# (remote event requests come back as generic objects)
ForEach ($event in $events) {
    $eventXML = [xml]$event.ToXml()

    # Iterate through each one of the XML message properties
    For ($i=0; $i -lt $eventXML.Event.EventData.Data.Count; $i++) {
        # Append these as object properties
        Add-Member -InputObject $event -MemberType NoteProperty -Force `
            -Name  $eventXML.Event.EventData.Data[$i].name `
            -Value $eventXML.Event.EventData.Data[$i].'#text'
    }
}

Write-Host "MAC addresses discovered:";

$events | ForEach-Object {
    try {
        $mac_address = $_.CallingStationID
        # Username in domain\username format
        $username = $_.FullyQualifiedSubjectUserName
        $ip = $null

        # Grab the IP address assigned to the MAC address
        try {
            $ip = Get-DhcpServerv4Scope -ComputerName "dhcpserver.contoso.com" -ScopeId 192.168.4.0 | Get-DhcpServerv4Lease -ComputerName "dhcpserver.contoso.com" | where {$_.Clientid -like "$mac_address"}
            $ip = $ip.IPAddress.IPAddressToString
        } catch {
            # Ignore errors as it just means we won't able find the hostname
        }

        # Ensure the event includes the username and device mac address
        if ([string]::IsNullOrWhiteSpace($mac_address) -Or ($mac_address -eq "-") -Or [string]::IsNullOrWhiteSpace($username) -Or ($username -eq "-")) {
            return
        }

        # Check the IP address hasn't been seen already
        if ($macs.Contains($mac_address)) { return }

        # Filter IP ranges and computer name$
        $macs += $mac_address
        Write-Host $mac_address

        # Try to grab the computers hostname
        try {
            $hostname = (Resolve-DnsName $ip -ErrorAction SilentlyContinue)[0].NameHost
            $results.Add(@($mac_address,$username,$hostname))
        } catch {
            $results.Add(@($mac_address,$username))
        }
    } catch {
        Write-Host "Error parsing event";
        Write-Host $_.Exception.Message;
    }
}

$resultArr = $results.ToArray()

# Only post to the server if there are results
if ($resultArr.length -gt 0) {
    Write-Host "Posting to control server";

    # Send to the server
    $postParams = ConvertTo-Json @($resultArr)
    $res = Invoke-WebRequest -UseBasicParsing -Uri https://placeos.server.com/api/engine/v2/webhook/trig-98UC/notify?secret=046856ff816d49f26261d3c9c9789da&exec=true&mod=LocationServices&method=mac_mappings -Method POST -Body $postParams -ContentType "application/json" -TimeoutSec 40
    Write-Host "Response code was:" $res.StatusCode;

    if ($res.StatusCode -ne 202) {
        Write-Host "Webhook post failed...";
        exit 1
    }
} else {
    Write-Host "No results found...";
}

Workstation Monitoring

  <Triggers>
    <EventTrigger>
      <ValueQueries>
          <Value name="username">Event/EventData/Data[@Name='TargetUserName']</Value>
          <Value name="domain">Event/EventData/Data[@Name='TargetDomainName']</Value>
      </ValueQueries>
      <Enabled>true</Enabled>
      <Subscription>&lt;QueryList&gt;&lt;Query Id="0" Path="Security"&gt;&lt;Select Path="Security"&gt;*[System[Provider[@Name='Microsoft-Windows-Security-Auditing'] and EventID=4624]] and *[EventData[Data[@Name='LogonType'] and (Data='2' or Data='7')]]&lt;/Select&gt;&lt;/Query&gt;&lt;/QueryList&gt;</Subscription>
    </EventTrigger>
  </Triggers>

This catches log off events and marks the workstation as free.

param (
    [Parameter(Mandatory=$false)][string]$username,
    [Parameter(Mandatory=$false)][string]$domain
)

# Get the IP address of the local PC
$ipV4 = Test-Connection -ComputerName $env:COMPUTERNAME -Count 1  | Select -ExpandProperty IPV4Address
$ip = $ipV4.IPAddressToString

# Ensure the event includes the IP address
if ([string]::IsNullOrWhiteSpace($ip) -Or ($ip -eq "-") -Or [string]::IsNullOrWhiteSpace($username) -Or [string]::IsNullOrWhiteSpace($domain)) {
    Write-Host "IP address was blank";
    exit 0
}

# Post to details to server
$postParams = ConvertTo-Json @(,@($ip,$username,$domain))
Invoke-WebRequest -UseBasicParsing -Uri https://placeos.server.com/api/engine/v2/webhook/trig-98UC/notify?secret=046856ff816d49f26261d3c9c9789da&exec=true&mod=LocationServices&method=ip_mappings -Method POST -Body $postParams -ContentType "application/json"

Untrusted or Self Signed Certificates

Add this to ignore certificate errors

add-type @"
    using System.Net;
    using System.Security.Cryptography.X509Certificates;
    public class TrustAllCertsPolicy : ICertificatePolicy {
        public bool CheckValidationResult(
            ServicePoint srvPoint, X509Certificate certificate,
            WebRequest request, int certificateProblem) {
            return true;
        }
    }
"@
[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy

Protocol violation errors

function Set-UseUnsafeHeaderParsing
{
    param(
        [Parameter(Mandatory,ParameterSetName='Enable')]
        [switch]$Enable,

        [Parameter(Mandatory,ParameterSetName='Disable')]
        [switch]$Disable
    )

    $ShouldEnable = $PSCmdlet.ParameterSetName -eq 'Enable'

    $netAssembly = [Reflection.Assembly]::GetAssembly([System.Net.Configuration.SettingsSection])

    if($netAssembly)
    {
        $bindingFlags = [Reflection.BindingFlags] 'Static,GetProperty,NonPublic'
        $settingsType = $netAssembly.GetType('System.Net.Configuration.SettingsSectionInternal')

        $instance = $settingsType.InvokeMember('Section', $bindingFlags, $null, $null, @())

        if($instance)
        {
            $bindingFlags = 'NonPublic','Instance'
            $useUnsafeHeaderParsingField = $settingsType.GetField('useUnsafeHeaderParsing', $bindingFlags)

            if($useUnsafeHeaderParsingField)
            {
              $useUnsafeHeaderParsingField.SetValue($instance, $ShouldEnable)
            }
        }
    }
}

# Call this before Invoke-WebRequest
Set-UseUnsafeHeaderParsing -Enable

This is for when users log onto a shared resource and we want to know who is sitting at which workstation. We should attach an event to particular events using the filter below. More details on how to set this up

Add this to ignore errors, see this thread on

CIDR notation
Event result code details
Event details
are here
Event Type Details
Powershell wget protocol violation