functions/new-adgroupreport.ps1

Function New-ADGroupReport {
    [cmdletbinding()]
    [outputtype("System.IO.File")]
    Param (
        [parameter(Position = 0, HelpMessage = "Enter an AD Group name. Wildcards are allowed.")]
        [validatenotnullorEmpty()]
        [string]$Name = "*",
        [Parameter(HelpMessage = "Enter the distinguished name of the top-level container or organizational unit.")]
        [ValidateScript( {
                $testDN = $_
                Try {
                    [void](Get-ADObject -Identity $_ -ErrorAction Stop)
                    $True
                }
                Catch {
                    Write-Warning "Failed to verify $TestDN as a valid searchbase."
                    Throw $_.Exception.message
                    $False
                }
            })]
        [string]$SearchBase,
        [Parameter(HelpMessage = "Filter on the group category")]
        [ValidateSet("All", "Distribution", "Security")]
        [string]$Category = "All",
        [Parameter(HelpMessage = "Filter on group scope")]
        [ValidateSet("Any", "DomainLocal", "Global", "Universal")]
        [string]$Scope = "Any",
        [Parameter(HelpMessage = "Exclude BuiltIn and Users")]
        [switch]$ExcludeBuiltIn,
        [Parameter(Mandatory, HelpMessage = "Specify the output HTML file.")]
        [ValidateScript( {
                #validate the parent folder
                Test-Path (Split-Path $_)
            })]
        [string]$FilePath,
        [Parameter(HelpMessage = "Enter the name of the report to be displayed in the web browser")]
        [ValidateNotNullOrEmpty()]
        [string]$ReportTitle = "AD Group Report",
        [Parameter(HelpMessage = "Specify the path the CSS file. If you don't specify one, the default module file will be used.")]
        [ValidateScript( { Test-Path $_ })]
        [string]$CSSUri = "$PSScriptRoot\..\reports\groupreport.css",
        [Parameter(HelpMessage = "Embed the CSS file into the HTML document head. You can only embed from a file, not a URL.")]
        [switch]$EmbedCSS,
        [Parameter(HelpMessage = "Specify a domain controller to query.")]
        [alias("dc", "domaincontroller")]
        [string]$Server,
        [Parameter(HelpMessage = "Specify an alternate credential.")]
        [alias("RunAs")]
        [PSCredential]$Credential
    )

    Write-Verbose "[$((Get-Date).TimeofDay)] Starting $($myinvocation.mycommand)"

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

    #region report metadata
    $cmd = Get-Command $myinvocation.InvocationName
    $thisScript = "{0}\{1}" -f $cmd.source, $cmd.name
    $reportVersion = ( $cmd).version.tostring()
    #who is running the report?
    if ($Credential.Username) {
        $who = $Credential.UserName
    }
    else {
        $who = "$($env:USERDOMAIN)\$($env:USERNAME)"
    }

    #where are they running the report from?
    Try {
        #disable verbose output from Resolve-DNSName
        $where = (Resolve-DnsName -Name $env:COMPUTERNAME -Type A -ErrorAction Stop -Verbose:$False).Name | Select-Object -Last 1
    }
    Catch {
        $where = $env:COMPUTERNAME
    }

    $post = @"
<table class='footer'>
    <tr align = "right"><td>Report run: <i>$(Get-Date)</i></td></tr>
    <tr align = "right"><td>Author: <i>$($Who.toUpper())</i></td></tr>
    <tr align = "right"><td>Source: <i>$thisScript</i></td></tr>
    <tr align = "right"><td>Version: <i>$ReportVersion</i></td></tr>
    <tr align = "right"><td>Computername: <i>$($where.toUpper())</i></td></tr>
</table>
"@


    #endregion

    #region HTML setup
    $head = @"
<title>$ReportTitle</Title>
<style>
td[tip]:hover {
    color: #2112f1cc;
    position: relative;
}
td[tip]:hover:after {
    content: attr(tip);
    left: 0;
    top: 100%;
    margin-left: 80px;
    margin-top: 10px;
    width: 400px;
    padding: 3px 8px;
    position: absolute;
    color: #111111;
    font-family: 'Courier New', Courier, monospace;
    font-size: 10pt;
    background-color: #dcdc0d;
    white-space: pre-wrap;
}
th.dn {
    width:40%;
}
</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>
"@


    #parameters to splat to ConvertTo-Html
    $cHtml = @{
        Head        = $head
        Body        = ""
        PostContent = $post
    }

    If ($EmbedCSS) {
        if (Test-Path -Path $CSSUri) {
            Write-Verbose "[$((Get-Date).TimeofDay)] Embedding CSS content from $CSSUri"
            $cssContent = Get-Content -Path $CSSUri | Where-Object { $_ -notmatch "^@" }
            $head += @"
<style>
$cssContent
</style>
"@

            #update the hashtable
            $cHtml.Head = $head
        }
        else {
            Write-Error "Failed to find a CSS file at $CSSUri. You can only embed from a file."
            #bail out
            Write-Verbose "[$((Get-Date).TimeofDay)] Ending $($myinvocation.mycommand)"
            return
        }
    } #if embedCSS
    else {
        $cHtml.add("CSSUri", $CSSUri)
    }

    #endregion

    #region get group data
    $splat = @{
        ErrorAction    = "Stop"
        Name           = $Name
        Scope          = $Scope
        Category       = $Category
        ExcludeBuiltIn = $ExcludeBuiltIn
    }

    if ($SearchBase) {
        $splat.Add("SearchBase", $SearchBase)
    }
    Try {
        $data = Get-ADGroupReport @splat | Sort-Object -Property Branch, Name
    }
    Catch {
        Throw $_
    }

    #endregion

    #region format data
    if ($data) {
        Write-Verbose "[$((Get-Date).TimeofDay)] Processing $($data.name.count) groups."
        #get domain root
        Try {
            Write-Verbose "[$((Get-Date).TimeofDay)] Getting domain root."
            $root = Get-ADDomain -ErrorAction stop
        }
        Catch {
            Write-Warning "Failed to get domain information. $($_.Exception.message)"
            Write-Verbose "[$((Get-Date).TimeofDay)] Ending $($myinvocation.mycommand)"
            #bail out
            return
        }

        Write-Verbose "[$((Get-Date).TimeofDay)] Creating HTML"
        $Fragments = [System.Collections.Generic.List[string]]::new()
        $fragments.Add("<H1>$($root.dnsroot)</H1>")
        $fragments.Add("<H1>Group Membership Report</H1>")

        #show report parameters
        $splat.Remove("ErrorAction")
        if ($PSBoundParameters.ContainsKey("Server")) {
            $splat.Add("Server",$server)
        }

        $fragments.Add("<a href='javascript:toggleAll();' title='Click to toggle all group sections'>+/-</a>")
        $Fragments.Add("<br><H3>Report Parameters</H3>")
        $fragments.Add( $([pscustomobject]$splat | ConvertTo-Html -as List -Fragment ))

        ForEach ($group in $data) {
            Write-Verbose "[$((Get-Date).TimeofDay)] Processing group $($group.distinguishedname)"
            $fragGroup = [System.Collections.Generic.List[string]]::new()
            $div = $group.DistinguishedName -replace "\W", ""
            $heading = $group.DistinguishedName
            [xml]$html = $group | Select-Object -Property Name, Category, Scope, Description, Created, Modified |
            ConvertTo-Html -As table -Fragment

            for ($i = 1; $i -le $html.table.tr.count - 1; $i++) {
                $pop1 = $html.CreateAttribute("tip")
                $pop1.Value = "Managed by: $($group.ManagedBy)"
                [void]($html.table.tr[$i].ChildNodes[0].Attributes.append($pop1))
                $pop2 = $html.CreateAttribute("tip")
                $pop2.Value = "Age: $($group.age)"
                [void]($html.table.tr[$i].ChildNodes[5].Attributes.append($pop2))
            }

            #add members
            $fragGroup.Add($html.InnerXml)

            if ($group.members.count -gt 0) {
                $fragGroup.Add("<H3>Members</H3>")

                [xml]$html = $group.members |
                Select-Object -Property DistinguishedName, Name, Description, Enabled |
                ConvertTo-Html -Fragment -As Table

                #insert class to set first column width
                $th = $html.CreateAttribute("class")
                $th.Value = "dn"
                [void]($html.table.tr[0].childnodes[0].attributes.append($th))

                #process member HTML daa
                for ($i = 1; $i -le $html.table.tr.count - 1; $i++) {
                    $dn = $html.table.tr[$i].ChildNodes[0]."#text"

                    $pop = $html.CreateAttribute("tip")
                    $pop.value = (_getPopData -Identity $dn | Format-List | Out-String).Trim()
                    [void]($html.table.tr[$i].ChildNodes[0].Attributes.append($pop))

                    if ($html.table.tr[$i].ChildNodes[3].'#text' -eq 'False') {
                        #flag the account as disabled
                        Write-Verbose "[$((Get-Date).TimeofDay)] Flagging $dn as disabled"
                        $class = $html.CreateAttribute("class")
                        $class.value = "alert"
                       [void]$html.table.tr[$i].Attributes.Append($class)
                    }
                }
                $fragGroup.Add($html.InnerXml)
            }
            else {
                $fragGroup.Add("<H3 class='alert'>No Members</H3>")
            }
            $H = _inserttoggle -Text $heading -div $div -Heading "H2" -Data $fragGroup -NoConvert
            $Fragments.add($H)
        }
    }
    else {
        Write-Warning "No group data found to build a report with."
    }
    #endregion

    #region create report
    if ($Fragments.count -gt 0) {
        $cHtml.Body = $Fragments

        #convert filepath to a valid filesystem name
        $parent = (Split-Path -Path $filePath)
        $file = Join-Path -Path $parent -ChildPath (Split-Path -Path $filePath -Leaf)

        Write-Verbose "[$((Get-Date).TimeofDay)] Saving file to $file"
        ConvertTo-Html @cHtml | Out-File -FilePath $file
    }

    #endregion

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

} #close function