当前位置:网站首页 > 更多 > 玩电脑 > 正文

[玩转系统] 来自 PowerShell 的 Active Directory 更改报告

作者:精品下载站 日期:2024-12-14 08:01:24 浏览:13 分类:玩电脑

来自 PowerShell 的 Active Directory 更改报告


[玩转系统] 来自 PowerShell 的 Active Directory 更改报告

几天前,我发布了一些 PowerShell 代码,您可以使用这些代码在 Active Directory 中发生变化时收到警报。该代码使用 PowerShell 和 CIM 事件来通知您,例如,当创建新用户帐户时。当您需要警报时,这会很有帮助。但也许您只需要报告。自给定日期和时间(例如过去 24 小时)以来,Active Directory 中发生了哪些变化?如果能有一份漂亮的报告不是很好吗?让我来帮忙。以下是我如何使用 PowerShell 和 ActiveDirectory 模块解决该问题。

何时改变

Active Directory 中的对象具有 WhenChanged 属性。许多 ActiveDirectory 命令都有过滤器参数。这个参数非常灵活,你一定要阅读它的命令帮助。使用过滤器的原因是对象是在源头过滤的,速度要快得多。这不是推荐的过滤方式:

$since = (Get-Date).AddMinutes(-90)
Get-ADuser -filter * -properties WhenChanged | Where { $_.WhenChanged -ge $since }

它会起作用,但请考虑一下。您告诉 PowerShell,“从域中获取所有用户对象,将它们发送到我的计算机,然后过滤 WhenChanged 属性大于过去 90 分钟的位置。”在小域中,您不会注意到。但如果你有很大的域,那就有明显的区别。

相反,创建一个像这样的过滤器:

Get-ADuser -filter {WhenChanged -ge $since} -Properties WhenChanged |
 Select-Object DistinguishedName,Name,WhenChanged

棘手的部分是您需要将这个特定的过滤器创建为脚本块。我有一段时间没有编写任何 AD 脚本了,花了一些时间才解决这个问题。我重新阅读了 cmdlet 帮助,这让我朝着正确的方向前进。请务必阅读帮助。

[玩转系统] 来自 PowerShell 的 Active Directory 更改报告

这是我的结果。当然,我不知道发生了什么变化,但我并不担心。需要记住的一件事是,您可能已经更改了对象,其中唯一的更改是由于复制和 USN 更新造成的。我还没有找到过滤掉这些类型的更改的好方法。

获取 ADObject

对于我的报告,我只关心用户、组、计算机和 OU。虽然我可以使用特定的 Get-* cmdlet,但使用 Get-ADObject 并创建良好的过滤器可能同样容易。

$filter = {(objectclass -eq 'user' -or objectclass -eq 'group' -or objectclass -eq 'organizationalunit' ) -AND (WhenChanged -gt $since )}

这是我使用此过滤器的一种方法。

Get-ADObject -Filter $filter -Properties WhenCreated,WhenChanged | 
 Sort-Object -Property ObjectClass,WhenChanged |
 Format-Table -GroupBy objectClass -Property DistinguishedName,WhenCreated,WhenChanged

[玩转系统] 来自 PowerShell 的 Active Directory 更改报告

需要指出的一件事是,如果 WhenCreated 大于或等于我的截止日期,则很可能是一个新对象。

使用 Get-ADObject 的另一个原因是它还会显示已删除的对象。但是,您需要启用 Active Directory 回收站。但如果你有的话,这里是修改后的代码:

Get-ADObject -Filter $filter -Properties WhenCreated,WhenChanged -IncludeDeletedObjects | 
 Select-Object *,@{Name="IsNew";Expression = { if ($_.WhenCreated -ge $since) { $True} else {$False}}},
 @{Name="IsDeleted";Expression = {$_.distinguishedname -match "Deleted Objects" }} |
 Sort-Object -Property ObjectClass,WhenChanged |
 Format-List -GroupBy objectClass -Property DistinguishedName,WhenCreated,WhenChanged,IsNew,IsDeleted

我正在定义一些附加属性来指示新的或已删除的对象。

[玩转系统] 来自 PowerShell 的 Active Directory 更改报告

您也可以使用一个名为 IsDeleted 的属性,尽管它仅存在于已删除的对象上。我的代码为所有对象定义了它。

一旦获得了对象和属性,就可以无休止地格式化或使用结果。

创建 HTML 报告

我喜欢创建报告,我最喜欢的工具是 ConvertTo-Html。在我的报告脚本中,我倾向于创建具有嵌入式样式的独立文件。如果您计划通过电子邮件发送文件,这是一个好方法。我的脚本实际上是一个控制脚本,包含 AD、ConvertTo-HTML 和 Out-File cmdlet。我没有编写一个函数来将对象发送到管道。我正在运行一组精心设计的 PowerShell 命令来产生所需的结果。这是一个控制脚本。

这些类型的脚本也可以有参数。我经常将脚本参数传递给底层 cmdlet,通常使用 PSBoundParameters 和 splatting。尽管最近我一直在使用私有版本 $PSDefaultParameterValues,这正是我在此脚本中使用的版本。我现在不妨向您展示一下。

#requires -module ActiveDirectory

#Reporting on deleted items requires the Active Directory Recycle Bin feature
[cmdletbinding()]
Param(
    [Parameter(Position = 0,HelpMessage = "Enter a last modified datetime for AD objects. The default is the last 8 hours.")]
    [ValidateNotNullOrEmpty()]
    [datetime]$Since = ((Get-Date).AddHours(-8)),
    [Parameter(HelpMessage = "What is the report title?")]
    [string]$ReportTitle = "Active Directory Change Report",
    [Parameter(HelpMessage = "Add a second grouping based on the object's container or OU.")]
    [switch]$ByContainer,
    [Parameter(HelpMessage = "Specify the path for the output file.")]
    [ValidateNotNullOrEmpty()]
    [string]$Path = ".\ADChangeReport.html",
    [Parameter(HelpMessage = "Specifies the Active Directory Domain Services domain controller to query. The default is your Logon server.")]
    [string]$Server = $env:LOGONSERVER.SubString(2),
    [Parameter(HelpMessage = "Specify an alternate credential for authentication.")]
    [pscredential]$Credential,
    [ValidateSet("Negotiate","Basic")]
    [string]$AuthType
)

#region helper functions

#a private helper function to convert the objects to html fragments
Function _convertObjects {
    Param([object[]]$Objects)
    #convert each table to an XML fragment so I can insert a class attribute
    [xml]$frag = $objects | Sort-Object -property WhenChanged |
    Select-Object -Property DistinguishedName,Name,WhenCreated,WhenChanged,IsDeleted |
    ConvertTo-Html -Fragment

    for ($i = 1; $i -lt $frag.table.tr.count;$i++) {
        if (($frag.table.tr[$i].td[2] -as [datetime]) -ge $since) {
            #highlight new objects in green
            $class = $frag.CreateAttribute("class")
            $class.value="new"
            [void]$frag.table.tr[$i].Attributes.append($class)
        } #if new

        #insert the alert attribute if the object has been deleted.
        if ($frag.table.tr[$i].td[-1] -eq 'True') {
            #highlight deleted objects in red
            $class = $frag.CreateAttribute("class")
            $class.value="alert"
            [void]$frag.table.tr[$i].Attributes.append($class)
        } #if deleted
    } #for

    #write the innerXML (ie HTML code) as the function output
    $frag.InnerXml
}

# private helper function to insert javascript code into my html
function _insertToggle {
    [cmdletbinding()]
    #The text to display, the name of the div, the data to collapse, and the heading style
    #the div Id needs to be simple text
    Param([string]$Text, [string]$div, [object[]]$Data, [string]$Heading = "H2", [switch]$NoConvert)

    $out = [System.Collections.Generic.list[string]]::New()
    if (-Not $div) {
        $div = $Text.Replace(" ", "_")
    }
    $out.add("<a href='javascript:toggleDiv(""$div"");' title='click to collapse or expand this section'><$Heading>$Text</$Heading></a><div id=""$div"">")
    if ($NoConvert) {
        $out.Add($Data)
    }
    else {
        $out.Add($($Data | ConvertTo-Html -Fragment))
    }
    $out.Add("</div>")
    $out
}

#endregion

#some report metadata
$reportVersion = "2.1.1"
$thisScript = Convert-Path $myinvocation.InvocationName

Write-Verbose "[$(Get-Date)] Starting $($myinvocation.MyCommand)"
Write-Verbose "[$(Get-Date)] Detected these bound parameters"
$PSBoundParameters | Out-String | Write-Verbose

#set some default parameter values
$params = "Credential","AuthType"
$script:PSDefaultParameterValues = @{"Get-AD*:Server" = $Server}
ForEach ($param in $params) {
    if ($PSBoundParameters.ContainsKey($param)) {
        Write-Verbose "[$(Get-Date)] Adding 'Get-AD*:$param' to script PSDefaultParameterValues"
        $script:PSDefaultParameterValues["Get-AD*:$param"] = $PSBoundParameters.Item($param)
    }
}

Write-Verbose "[$(Get-Date)] Getting current Active Directory domain"
$domain = Get-ADDomain

#create a list object to hold all of the HTML fragments
Write-Verbose "[$(Get-Date)] Initializing fragment list"
$fragments = [System.Collections.Generic.list[string]]::New()
$fragments.Add("<H2>$($domain.dnsroot)</H2>")
$fragments.Add("<a href='javascript:toggleAll();' title='Click to toggle all sections'>+/-</a>")

Write-Verbose "[$(Get-Date)] Querying $($domain.dnsroot)"
$filter = {(objectclass -eq 'user' -or objectclass -eq 'group' -or objectclass -eq 'organizationalunit' ) -AND (WhenChanged -gt $since )}

Write-Verbose "[$(Get-Date)] Filtering for changed objects since $since"
$items = Get-ADObject -filter $filter -IncludeDeletedObjects -Properties WhenCreated,WhenChanged,IsDeleted -OutVariable all | Group-Object -property objectclass

Write-Verbose "[$(Get-Date)] Found $($all.count) total items"

if ($items.count -gt 0) {
    foreach ($item in $items) {
        $category = "{0}{1}" -f $item.name[0].ToString().toUpper(),$item.name.Substring(1)
        Write-Verbose "[$(Get-Date)] Processing $category [$($item.count)]"

        if ($ByContainer) {
            Write-Verbose "[$(Get-Date)] Organizing by container"
            $subgroup = $item.group | Group-Object -Property { $_.distinguishedname.split(',', 2)[1] } | Sort-Object -Property Name
            $fraghtml = [System.Collections.Generic.list[string]]::new()
            foreach ($subitem in $subgroup) {
                Write-Verbose "[$(Get-Date)] $($subItem.name)"
                $fragGroup = _convertObjects $subitem.group
                $divid = $subitem.name -replace "=|,",""
                $fraghtml.Add($(_inserttoggle -Text "$($subItem.name) [$($subitem.count)]" -div $divid -Heading "H4" -Data $fragGroup -NoConvert))
            } #foreach subitem
        } #if by container
        else {
            $fragHtml = _convertObjects $item.group
        }
         $code = _insertToggle -Text "$category [$($item.count)]" -div $category -Heading "H3" -Data $fragHtml -NoConvert
        $fragments.Add($code)
    } #foreach item

#my embedded CSS
    $head = @"
<Title>$ReportTitle</Title>
<style>
h2 {
    width:95%;
    background-color:#7BA7C7;
    font-family:Tahoma;
    font-size:12pt;
}
h4 {
    width:95%;
    background-color:#b5f144;
}
body {
    background-color:#FFFFFF;
    font-family:Tahoma;
    font-size:12pt;
}
td, th {
    border:1px solid black;
    border-collapse:collapse;
}
th {
    color:white;
    background-color:black;
}
table, tr, td, th {
    padding-left: 10px;
    margin: 0px
}
tr:nth-child(odd) {background-color: lightgray}
table {
    width:95%;
    margin-left:5px;
    margin-bottom:20px;
}
.alert { color:red; }
.new { color:green; }
.footer { font-size:10pt; }
.footer tr:nth-child(odd) {background-color: white}
.footer td,tr {
    border-collapse:collapse;
    border:none;
}
.footer table {width:15%;}
td.size {
    text-align: right;
    padding-right: 25px;
}
</style>
<script type='text/javascript' src='https://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js'>
</script>
<script type='text/javascript'>
function toggleDiv(divId) {
`$("#"+divId).toggle();
}
function toggleAll() {
var divs = document.getElementsByTagName('div');
for (var i = 0; i < divs.length; i++) {
var div = divs[i];
`$("#"+div.id).toggle();
}
}
</script>
<H1>$ReportTitle</H1>
"@

#a footer for the report. This could be styled with CSS
    $post = @"
<table class='footer'>
    <tr align = "right"><td>Report run: <i>$(Get-Date)</i></td></tr>
    <tr align = "right"><td>Report version: <i>$ReportVersion</i></td></tr>
    <tr align = "right"><td>Source: <i>$thisScript</i></td></tr>
</table>
"@

    $htmlParams = @{
        Head = $head
        precontent = "Active Directory changes since $since. Reported from $($Server.toUpper()). Replication only changes may be included."
        Body =($fragments | Out-String)
        PostContent = $post
    }
    Write-Verbose "[$(Get-Date)] Creating report $ReportTitle version $reportversion saved to $path"
    ConvertTo-HTML @htmlParams | Out-File -FilePath $Path
    Get-Item -Path $Path
}
else {
    Write-Warning "No modified objects found in the $($domain.dnsroot) domain since $since."
}

Write-Verbose "[$(Get-Date)] Ending $($myinvocation.MyCommand)"

该脚本创建一系列 HTML 片段,最终将它们组合在一起创建最终文件。我的脚本包含一些辅助函数。其中之一将 HTML 解析为 XML,以便我可以插入一个类属性来指示该对象是新的还是已删除。我有一个定义的样式,以红色显示已删除的对象,以绿色显示新对象。另一个函数帮助我插入 JavaScript,让我可以折叠文件的各个部分。

c:\scripts\ADChangeReport.ps1 -Since (Get-Date).Addhours(-1) -Path c:\scripts\ad6.html

尽管您可以指定不同的域控制器,但该脚本将默认使用当前用户的登录服务器。您还可以指定备用凭据。您可以而且应该从域成员桌面运行该脚本。

[玩转系统] 来自 PowerShell 的 Active Directory 更改报告

我可以单击 +/- 来切换折叠所有区域,或单击不同的类别。您可以看到绿色文本的新组。

[玩转系统] 来自 PowerShell 的 Active Directory 更改报告

在报告的底部,您可以看到红色的已删除用户。我还喜欢在报告的页脚中包含元数据信息。此信息帮助我了解该报告的来源。如果您设置了计划报告系统但忘记记录它,这会很有帮助。此报告中缺少的一项信息是计算机名称。我可以看到脚本文件的路径,但不知道是哪台计算机。我会把调整留给你。

报告脚本还支持按容器组织结果。我的代码将在通过分割专有名称获得的对象容器上进行分组。

if ($ByContainer) {
  Write-Verbose "[$(Get-Date)] Organizing by container"
  $subgroup = $item.group | Group-Object -Property { $_.distinguishedname.split(',', 2)[1] } | Sort-Object -Property Name
...

[玩转系统] 来自 PowerShell 的 Active Directory 更改报告

概括

这是一个公认的复杂的 PowerShell 脚本,因此如果您有任何不理解或有疑问,请随时在评论中提问。我还要指出,就像 AD 事件监视器一样,仅仅因为您可以使用这样的东西并不意味着您应该使用。只需进行最少更改的小型域就可以使用此脚本或类似的脚本。

不过,我对在具有高度动态的 Active Directory 基础设施的大型企业中使用它持怀疑态度。尽管如果你的改变窗口很小,它可能会有效。我假设,如果您正在运行大型 AD 环境,那么您的公司已投资于高质量且适当的管理和报告工具。您可以利用 PowerShell 来处理这些利基或特殊情况。

如果您可以尝试一下报告脚本,我很想听听您的经验。享受!

您需要 登录账户 后才能发表评论

取消回复欢迎 发表评论:

关灯