5

I created the script below to find all Unix computers created since a specific date. It doesn't return an error, but it doesn't work either. There is only one date used in the script but both formats have same outcome.

Neither date format fails, but they don't return anything -- the query just shows as running but never returns any data:

$SomeDate1 = 'Wednesday, November 7, 2018 2:41:59 PM'
$SomeDate2 = '2018-11-07 14:41:59.000'

Get-ADComputer -Filter '*' -Properties *  | Where {$($PSitem.CanonicalName) -like '*Unix*'  -and  $($PSitem.whenCreated) -gt $SomeDate1 } |` 
        Select Name,OperatingSystem,OperatingSystemVersion,ipv4Address,Created,whenCreated,Deleted,whenChanged,Modified,Description,@{Label="ServicePrincipalNames";Expression={$_.ServicePrincipalNames -join ";" }},DisplayName,Location,DistinguishedName,DNSHostName 
BSMP
  • 4,596
  • 8
  • 33
  • 44
Leo Torres
  • 673
  • 1
  • 6
  • 18
  • 1
    I'm writing an answer for you but in the meantime I recommend you review [this answer](https://stackoverflow.com/questions/51218670/how-to-effectively-use-the-filter-parameter-on-active-directory-cmdlets) of mine to understand how to utilize the `-Filter` parameter. There is an example on filtering by date on `ADObjects` – codewario Sep 08 '21 at 15:54
  • OK looking. thanks – Leo Torres Sep 08 '21 at 15:55
  • Note `CanonicalName` is a computed attribute, so you also need to provide it explicitly with the `-Properties` parameter. You can do `-Properties *, CanonicalName` to achieve this, but normally you do not want to return `-Properties *` as this puts undue load on the AD controller you hit. – codewario Sep 08 '21 at 16:02
  • Yes, I found that out the hard way. I had introduce the "-Properties *" for it to work. So with the "-Properties *, CanonicalName" your saying less load on server. I did this with out a date and it errored at 21K servers. I know there are 36K to be returned. Maybe I am not doing this very efficiently. – Leo Torres Sep 08 '21 at 16:10
  • I'm saying you should only specify properties you want to operate on once returned, not all properties available for the ADObject in question. Avoid using `-Properties *` at all, aside from the occasional investigative case when you want to see what is available to operate on or if you truly need to record *all* of the static attributes of an `ADObject` – codewario Sep 08 '21 at 16:20
  • In any case I'm seeing some strangeness trying to use `-Filter` with `whenCreated` so I'm doing some testing before I post my answer – codewario Sep 08 '21 at 16:21
  • @LeoTorres To compare the dates, you should transform them into `DateTime` objects. Otherwise, you will compare strings. – stackprotector Sep 08 '21 at 16:46
  • @stackprotector That would be the solution if you filter on the date in the `Where-Object` clause but you really want to do the filtering with `-Filter`. In any event, @LeoTorres I've figured out the inconsistency and will post my answer shortly – codewario Sep 08 '21 at 16:57
  • I don't see why you need to use the `$( )` around $PSitem.CanonicalName and $PSitem.whenCreated. It seemed to work for me although slowly. I had trouble with canonicalname and whencreated within the filter. Actually just using -property canonicalname,whencreated was pretty quick, at least `| select -first 5`. – js2010 Sep 08 '21 at 16:58

2 Answers2

7

Use the -Filter parameter to do this, though we'll need to do some legwork to get the date in a format appropriate for whenCreated, as it is defined differently in the AD schema than some other "Date" type attributes. The code below will work, explanation is below it:

Note: I briefly mention it below but you do not want to do -Properties * if you can help it. Returning all of the static attributes for an object can cause undue load on the DC you are hitting. Only specify the properties you want to return. See my linked answer at the bottom of this answer for a more detailed explanation.


# Create a yyyMMddHHmmss.Z formatted date string in UTC
$SomeDate1= ( Get-Date 'Wednesday, November 7, 2018 2:41:59 PM' ).ToUniversalTime().ToString('yyyMMddHHmmss.Z')

# These are the properties we both want to return from AD and include in the final output
$propertiesToReturn =
  "Name",
  "OperatingSystem",
  "OperatingSystemVersion",
  "ipv4Address",
  "Created",
  "whenCreated",
  "Deleted",
  "whenChanged",
  "Modified",
  "Description",
  "DisplayName",
  "Location",
  "DistinguishedName",
  "DNSHostName"

# These are properties we need from AD but are not required directly in the final output
$additionalProperties =
  "CanonicalName",
  "ServicePrincipalNames"

# Define the computed property here for clarity
$spnComputedProperty = @{
  Label = "ServicePrincipalNames";
  Expression = {
    $_.ServicePrincipalNames -join ";"
  } 
}

# Obtain the target computer list and apply your select-object expression to it
Get-ADComputer -Filter "whenCreated -gt '${SomeDate1}'" -Properties ( $propertiesToReturn + $additionalProperties ) | Where-Object {
  $_.CanonicalName -match 'Unix'
} | Select-Object ( $propertiesToReturn + $spnComputedProperty )

Now, there are a lot of changes here, so I'll explain what I've done and why:


New variables and changes to existing ones

  • I've omitted $SomeDate2 since it was not referenced otherwise in your code sample.
  • $SomeDate1 is not defined as an Interval type in the AD schema, unlike other date-type properties such as LastLogonDate. It is instead defined as a Generalized-Time string in the format yyyMMddHHmmss.Z and is in UTC, so we need the timestamp formatted in this way rather than rely on the default ToString() behavior of a [DateTime]. If we don't do this, the date comparison will not work. This is confusing because the RSAT AD Cmdlets will convert this (and other Generalized-Time strings) to a localized DateTime string for easier PowerShell processing when the data is ultimately returned.
    • Note that yyyMMddHHmmss.Z cannot be directly converted back into a DateTime object for use elsewhere.
  • For clarity, I've defined the common properties to return from Get-ADComputer and
    Select-Object as an array, and defined another array with the elements we only want to return from AD for further processing. These are $propertiesToReturn and $additionalProperties, respectively. This prevents us from having to redefine these properties in multiple places, and allows us to avoid the costly
    -Properties * invocation.
    • ServicePrincipalNames is included under $additionalProperties because you want to transform the attribute value to a string, so we don't want to include the original value of it in Select-Object.
    • CanonicalName is a computed property, and cannot be filtered on in either -Filter or
      -LDAPFilter. We must return and process this property locally, even though you don't want it in the final output.
    • Some property names defined under $propertiesToReturn are returned regardless but it does not hurt to include them in the -Properties array.
  • Also for clarity, I've defined your computed property for Select-Object as the $spnComputedProperty variable. This can be on a single line but I've made it multiline here for readability.

Invoking Get-ADComputer with a proper -Filter

Now that we have our property arrays and date string formatted correctly, we can finally call
Get-ADComputer.

  • We can use the "whenCreated -gt '${SomeDate1}'" filter string (do not use a ScriptBlock with -Filter) to return all ADComputers created after $SomeDate1.
  • Normally I don't recommend concatenating strings or arrays using + but this is a convenience exception where it's unlikely to cause memory issues. For Get-ADComputer -Properties we provide $propertiesToReturn and $additionalProperties as a single array.
    • Trying to use the syntax -Properties $propertiesToReturn, $additionalProperties will result in a type mismatch error.
  • I have reduced your Where-Object clause to only further filter on CanonicalName. As mentioned above, CanonicalName is a computed attribute and cannot be filtered on with
    -Filter or -LDAPFilter and must be done here.
    • I have also change -like to -match and removed the * from the (technically regex) expression but you could use your original -like clause with the globs if you prefer.
  • Finally, we pipe the result to Select-Object as you did before, and do the same concatenation trick we did with Get-ADComputer -Properties. However, this time we add $spnComputedProperty instead.

This should give you all ADComputers created after the date specified in $SomeDate1 with Unix in the CanonicalName with the properties you wished, including the customized ServicePrincipalNames field.


Note about avoiding converting the target DateTime to the Generalized-Time format

Technically speaking, you can use either of the following filter to avoid needing to convert your DateTime to the use the Generalized-Time format:

Get-ADComputer -Filter 'whenCreated -lt $SomeDate1'

# Double-quoted variant is useful if you have other variables which
# should be directly rendered as part of the -Filter string
Get-ADComputer -Filter "whenCreated -lt `$SomeDate1"

The reason I avoided mentioning this is because this behavior is not well understood or documented. There is some magic which the cmdlet does to get the variable value even though it should not be rendered as it is a literal string. As it's poorly understood, and is unclear what ruleset defines how the DateTime is transformed (e.g. do the rules change per attribute and type, do DateTimes always get converted to a Generalized-Time string? We don't know) I don't recommend this approach.

The behavior of whenCreated is well documented in the AD Schema docs, and had I consulted these from the get go it would have been clear how the -Filter needed to be crafted, rather than the trial-and-error I underwent to understand the comparison issue. Documentation links are below.


Additional Resources

If you get strange behavior on AD attributes not filtering correctly, or you just want to understand more about different attributes, I suggest looking the attribute up in OpenSpecs or the AD Schema documentation.

Also, see this answer of mine which goes into great detail about the -Filter parameter on the RSAT AD cmdlets.

codewario
  • 19,553
  • 20
  • 90
  • 159
-2

These ad filters tend to be trial and error. It's not completely obvious how they work. This worked for me, with a DateTime variable, a ScriptBlock filter, and Name instead of CanonicalName ("error: a filter was passed that uses constructed attributes").

The docs for get-adcomputer show using a scriptblock in the Backus-Naur form under -Filter.

The other alternative is to generate a GeneralizedTime (except the minutes, seconds, and period are required?) instead of a Datetime, which includes the time zone.

$startdate = [datetime]'3/12/2018'

Get-ADcomputer -Filter {name -like '*computer*' -and 
  whencreated -gt $startDate} -property whencreated,canonicalname

With a string filter, $startDate can't have extra quotes around it. The whole expression can't be double-quoted. I've seen this work in other cases.

Get-ADcomputer -Filter "name -like '*computer*' -and 
  whencreated -gt '$startDate'" -property whencreated,canonicalname
# error or no result

The date can't be the string version of the DateTime.

Get-ADcomputer -Filter "name -like '*computer*' -and 
  whencreated -gt '3/12/2018 12:00:00 AM'" -property whencreated,canonicalname
# no result

A string filter that works:

$startdate = [datetime]'3/12/2018'

# get string version of scriptblock
{ name -like "*computer*" -and whencreated -gt $startdate }.tostring()  
 name -like "*computer*" -and whencreated -gt $startdate

Get-ADcomputer -Filter 'name -like "*computer*" -and 
  whencreated -gt $startDate' -property whencreated,canonicalname
js2010
  • 23,033
  • 6
  • 64
  • 66
  • Yes it will work but it's a really bad habit. The AD cmdlets do not actually support script blocks and inevitably leads to confusion when you try something like `Import-Csv C:\userInfoWithEmails.csv | Foreach-Object { Get-ADUser -Filter { EmailAddress -eq $_.Email } }` – codewario Sep 08 '21 at 18:25
  • @BendertheGreatest `Foreach-Object { $email = $_.email; Get-ADUser -Filter { EmailAddress -eq $_.Email } }` The problem is no one understands how this black box of a filter works. It's trial and error. – js2010 Sep 08 '21 at 18:40
  • However, AD cmdlets and schema aside, it's a poor choice to use script blocks where strings are expected and teaches poor coding habits. This is a syntax issue, not AD confusion. – codewario Sep 08 '21 at 18:49
  • @BendertheGreatest Why doesn't a datetime variable work with the string form then? It seems unlikely that people are going to use a string filter and have to make a GeneralizedTime. – js2010 Sep 08 '21 at 18:58
  • AD filters are based on PowerShell Expression Syntax. [I'm not sure why you claim no one knows how they work.](https://learn.microsoft.com/en-us/previous-versions/windows/server/hh531527(v=ws.10)) You have to understand PES, how the attributes are defined in the schema, and also understand how the cmdlets return information. I'll admit the magic bits which transform one date format into another for instance might require some trial and error, but for filtering within the cmdlet itself, if you pull up your AD schema documentation you can understand the filter syntax requirement. – codewario Sep 08 '21 at 19:26
  • 2
    @js2010 Your answer still contains incorrect information. You *can* use double quoted strings, but you have to escape the `$` so it doesn't get rendered into the provided filter string by PowerShell. Your second code block has some extra bits that aren't meant to be there. You also only get errors on constructed attributes if you try to filter on it within the filter string itself, but you can still pass the property down and filter on it via `Where-Object` or other means without issue. OP can still curate the returned objects based on `CanonicalName`, just not within the cmdlet itself. – codewario Sep 08 '21 at 21:33
  • 5
    @js2010 When a mod of the site makes a change to your answer: It's almost always in the best interest of the site, as they've been elected by the community. The edits you made to revert back are destructive, as your current answer has unneeded commentary, and should be cleaned up. I encourage you to revert back to one of the better revisions, and respect that commentary like that should be left out of posts. – Blue Sep 11 '21 at 05:05
  • If you want to discuss why we rolled your post back again, you can ask a question on [Meta]. – Martijn Pieters Sep 11 '21 at 14:02