Skip to content

Instantly share code, notes, and snippets.

@JohnLBevan
Created March 3, 2020 09:18
Show Gist options
  • Select an option

  • Save JohnLBevan/8c9b8ad33291a629f6b1bf3128b4b9c4 to your computer and use it in GitHub Desktop.

Select an option

Save JohnLBevan/8c9b8ad33291a629f6b1bf3128b4b9c4 to your computer and use it in GitHub Desktop.
A C# implementation of Get-ADUserLastLogon that works for a single server, running for all DCs in a given domain's forest / doesn't rely on the DC having web services installed. For a similar PowerShell script (which does rely on web services) see: https://sid-500.com/2019/08/12/powershell-get-last-domain-logon-with-get-aduserlastlogon/
<Query Kind="Program">
<Reference>&lt;RuntimeDirectory&gt;\System.DirectoryServices.AccountManagement.dll</Reference>
<Reference>&lt;RuntimeDirectory&gt;\System.DirectoryServices.dll</Reference>
<Namespace>System.DirectoryServices</Namespace>
<Namespace>System.DirectoryServices.AccountManagement</Namespace>
<Namespace>System.DirectoryServices.ActiveDirectory</Namespace>
<Namespace>System.Threading.Tasks</Namespace>
</Query>
void Main()
{
// // CHANGE AS NEEDED \\
var domain = "the domain you're trying to query";
var samAccountName = "the user you're hunting for";
// \\ CHANGE AS NEEDED //
var result = GetUserLastLogonTime(domain, samAccountName);
if (result == DateTime.MinValue) // using MinValue as a NotFound placeholder
{
Console.WriteLine($"{samAccountName} has never logged on, so far as we can see");
}
else
{
Console.WriteLine($"{samAccountName} last logged in on {result.ToString("yyyy-MM-dd HH:mm:ss")}");
}
}
DateTime GetUserLastLogonTime(string domain, string samAccountName)
{
var result = DateTime.MinValue;
var domainObj = Domain.GetDomain(new DirectoryContext(DirectoryContextType.Domain, domain));
var dcs = domainObj.Forest.Domains.Cast<Domain>().SelectMany(x => x.DomainControllers.Cast<DomainController>()).Distinct();
var tasks = new List<Task<DateTime?>>();
foreach (DomainController dc in dcs) //linqpad requires type; not var
{
tasks.Add( GetUsersLastLogonFromDomainControllerAsync(dc, samAccountName) );
}
foreach (var task in tasks) {
task.Wait();
if (task.Result.HasValue && task.Result.Value > result) result = task.Result.Value;
}
return result; //GetMostRecentDate(result.Values.Select<Tuple<DateTime?, string>, DateTime?>(x => x.Item1)); //original code from my thinking I'd return a tuple per dc holding logondate or error info... but that's pointless complexity
}
DateTime GetMostRecentDate (IEnumerable<DateTime?> dates)
{
return dates.Select<DateTime?, DateTime>(x => x ?? DateTime.MinValue).OrderByDescending(x => x).FirstOrDefault();
}
DirectoryEntry GetDCDirectoryEntry (DomainController dc)
{
return new DirectoryEntry(string.Format("LDAP://{0}", dc.Name)); //caller to dispose
}
DirectorySearcher GetDirectorySearcher(DirectoryEntry de, string filter, string[] properties)
{
var ds = new DirectorySearcher(de);
ds.PageSize = 1000;
ds.Filter = filter;
ds.PropertiesToLoad.AddRange(properties);
return ds; //caller to dispose
}
string EscapeLdapQueryValue(string term)
{
return Regex.Replace(term, @"[\\*\()\u0000\/]", match => {
switch (match.Value[0]) {
case '\\': return @"\5c";
case '*': return @"\2a";
case '(': return @"\28";
case ')': return @"\29";
case '\u0000': return @"\00";
case '/': return @"\2f";
default: throw new InvalidOperationException($"Issue escaping regex term: '{term}'. Match value '{match.Value}'.");
}
});
}
Task<DateTime?> GetUsersLastLogonFromDomainControllerAsync (DomainController dc, string samAccountName)
{
return Task.Run(() => GetUsersLastLogonFromDomainControllerWrapped(dc, samAccountName));
}
DateTime? GetUsersLastLogonFromDomainControllerWrapped(DomainController dc, string samAccountName)
{
try
{
return new Nullable<DateTime>(GetUsersLastLogonFromDomainController(dc, samAccountName));
}
catch (Exception e)
{
Debug.WriteLine($"Error Processing Domain Controller {dc}");
Debug.WriteLine($"- {e.ToString()}");
return null;
}
}
DateTime GetUsersLastLogonFromDomainController (DomainController dc, string samAccountName)
{
using (var de = GetDCDirectoryEntry(dc))
{
var filter = $"(&(objectClass=user)(!objectClass=computer)(sAMAccountName={EscapeLdapQueryValue(samAccountName)}))";
var adProperties = new[] { "lastLogon" }; //"SAMAccountName","distinguishedName" //we may want to add in these other fields if we amend to run for multiple users / need to check the DN to confirm we're hitting the write domain / etc - overkill for now
using (var ds = GetDirectorySearcher(de, filter, adProperties))
{
var results = ds.FindAll();
Debug.WriteLine($"- DC {dc.Name} - found {results.Count} matches for {samAccountName}");
if (results == null || results.Count == 0 || !results[0].Properties.Contains("lastLogon")) return DateTime.MinValue; //or throw new NoMatchingPrincipalException(samAccountName)... if it's a valid user presumably they'd have an entry on all DCs even if no logonDate
if (results != null && results.Count > 1) throw new MultipleMatchesException(samAccountName);
var result = ConvertToDateTime((long)results[0].Properties["lastLogon"][0]);
Debug.WriteLine($"- DC {dc.Name} - last logon for {samAccountName} is {result.ToString("yyyy-MM-dd HH:mm:ss")}");
return result;
}
}
}
DateTime ConvertToDateTime(long lastLogon)
{
return DateTime.FromFileTime(lastLogon);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment