Public/Get-DllInfo.ps1

function Get-DllInfo {
    <#
    .SYNOPSIS
        Read-only inspector for Windows PE files (DLL, OCX, EXE, SYS).
 
    .DESCRIPTION
        Parses the PE/COFF headers, version resource, and (optionally) the
        Authenticode signature of a Windows binary, and reports whether it is a
        self-registering COM server and/or a managed (.NET) assembly. Emits one
        PSCustomObject per input path; output serializes cleanly with
        ConvertTo-Json (suitable for Ansible).
 
        The inspector NEVER calls LoadLibrary. The file is opened read-only with
        FileShare.ReadWrite and parsed as raw bytes. Consequences:
 
          * 32-bit DLLs can be inspected from 64-bit PowerShell and vice versa.
          * Old / corrupt / unsigned binaries can be examined without side effects.
          * DllMain is never executed.
 
    .PARAMETER Path
        One or more paths to PE files. Accepts pipeline input (e.g. from
        Get-ChildItem) and the FullName / PSPath / FilePath property aliases.
 
    .PARAMETER IncludeImports
        Include the full imports table — modules and per-module function lists
        (with name+hint or ordinal). Reads the IDT and ILT/IAT from the PE.
 
    .PARAMETER IncludeExports
        Include the full exports table — name, ordinal, RVA, and forwarder info
        (when an export points back into the export directory and resolves to
        "OtherModule.OtherFunction" or "OtherModule.#ordinal").
 
    .PARAMETER IncludeResources
        Include a flat list of resource entries (Type, Name, Language, Size,
        CodePage). Walks the full 3-level resource directory tree.
 
    .PARAMETER IncludeTypeLib
        When the binary carries a TYPELIB resource, load it via oleaut32!
        LoadTypeLibEx (REGKIND_NONE — no registration) and emit the COM type
        library: LibId, version, name, plus every CoClass / interface /
        dispinterface / enum / alias with their GUIDs, parents, methods, params
        and members.
 
    .PARAMETER IncludeDotNetTypes
        For managed assemblies, additionally load the file via
        Assembly.ReflectionOnlyLoadFrom and enumerate every type with its
        attributes (IsComVisible, Guid, ProgId, base type, kind). The
        cheap fields (RuntimeVersion, PEKind, CorFlags, AssemblyName,
        Version, Culture, PublicKeyToken) are reported always when the
        binary has a CLR header.
 
    .PARAMETER IncludeSignature
        Include Authenticode signature info (signer, issuer, validity, status).
        Slower since it goes through CryptoAPI.
 
    .PARAMETER IncludeHash
        Include the SHA-256 hash of the file.
 
    .PARAMETER Detailed
        Convenience switch — turns on every Include* switch.
 
    .EXAMPLE
        Get-DllInfo -Path C:\Windows\System32\scrrun.dll | ConvertTo-Json -Depth 12
 
    .EXAMPLE
        Get-ChildItem C:\App -Filter *.dll -Recurse |
            Get-DllInfo -IncludeHash -IncludeSignature
 
    .EXAMPLE
        # Find every COM-registrable DLL under a directory
        Get-ChildItem C:\Legacy -Include *.dll,*.ocx -Recurse |
            Get-DllInfo |
            Where-Object { $_.Com.IsComServer }
 
    .EXAMPLE
        # Audit which DLLs in a directory import a given API
        Get-ChildItem C:\App -Filter *.dll |
            Get-DllInfo -IncludeImports |
            Where-Object {
                $_.Imports.Functions.Name -contains 'CreateRemoteThread'
            } |
            Select-Object Path
 
    .EXAMPLE
        # Full inventory (all sections + signature + hash) for one binary
        Get-DllInfo -Path .\foo.dll -Detailed | ConvertTo-Json -Depth 12
 
    .NOTES
        PowerShell 5.1+ on Windows.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('FullName', 'PSPath', 'FilePath')]
        [string[]]$Path,

        [switch]$IncludeImports,
        [switch]$IncludeExports,
        [switch]$IncludeResources,
        [switch]$IncludeTypeLib,
        [switch]$IncludeDotNetTypes,
        [switch]$IncludeSignature,
        [switch]$IncludeHash,
        [switch]$Detailed
    )

    process {
        if ($Detailed) {
            $IncludeImports     = $true
            $IncludeExports     = $true
            $IncludeResources   = $true
            $IncludeTypeLib     = $true
            $IncludeDotNetTypes = $true
            $IncludeSignature   = $true
            $IncludeHash        = $true
        }

        foreach ($p in $Path) {
            try {
                $resolved = (Resolve-Path -LiteralPath $p -ErrorAction Stop).ProviderPath
            } catch {
                Write-Error "Failed to resolve '$p': $($_.Exception.Message)"
                continue
            }
            $fi = Get-Item -LiteralPath $resolved

            $result = [ordered]@{
                Path       = $resolved
                FileSize   = $fi.Length
                LastWrite  = $fi.LastWriteTimeUtc
                IsValidPE  = $false
                ParseError = $null
                PE         = $null
                Version    = $null
                Com        = $null
                DotNet     = $null
                Imports    = $null
                Exports    = $null
                Resources  = $null
                Signature  = $null
                Sha256     = $null
            }

            $pe = $null
            try {
                $pe = Read-PEImage -FilePath $resolved
                $result.IsValidPE = $true

                $arch = $script:MachineMap[[int]$pe.Machine]
                if (-not $arch) { $arch = ('Unknown(0x{0:X4})' -f $pe.Machine) }
                $sub  = $script:SubsystemMap[[int]$pe.Subsystem]
                if (-not $sub)  { $sub  = ('Unknown({0})'    -f $pe.Subsystem) }

                # Some compilers emit a deterministic "reproducible build" hash in
                # TimeDateStamp instead of a real time_t. We surface the decoded
                # value as-is and let the caller decide.
                $tsUtc = $null
                if ($pe.TimeDateStamp -gt 0) {
                    try {
                        $tsUtc = ([System.DateTimeOffset]::FromUnixTimeSeconds([int64]$pe.TimeDateStamp)).UtcDateTime
                    } catch { $tsUtc = $null }
                }

                $result.PE = [pscustomobject]@{
                    Architecture       = $arch
                    MachineRaw         = ('0x{0:X4}' -f $pe.Machine)
                    Is64Bit            = $pe.Is64Bit
                    Subsystem          = $sub
                    IsDll              = (($pe.Characteristics    -band 0x2000) -ne 0)
                    IsExecutable       = (($pe.Characteristics    -band 0x0002) -ne 0)
                    Characteristics    = (ConvertTo-Flags -Value $pe.Characteristics    -Map $script:CharacteristicsMap)
                    DllCharacteristics = (ConvertTo-Flags -Value $pe.DllCharacteristics -Map $script:DllCharacteristicsMap)
                    TimestampUtc       = $tsUtc
                    NumberOfSections   = $pe.NumberOfSections
                    Sections           = $pe.Sections | Select-Object Name, VirtualSize, VirtualAddress, SizeOfRawData, PointerToRawData
                }

                $result.Version = Get-PEVersionInfoSafe -FilePath $resolved

                # Exports — always read (cheap), used both for COM detection and
                # for the Exports field when -IncludeExports is set.
                $expData = @{ Names = @(); Exports = @() }
                try { $expData = Get-PEExportsFull -Pe $pe } catch { }
                $exportNames = $expData.Names
                $selfReg = @($script:ComSelfRegSymbols | Where-Object { $exportNames -contains $_ })

                # Resources — when requested, do a full walk and derive HasTypeLib
                # from it; otherwise the cheap level-1 detector is enough.
                $resources = $null
                $hasTlb    = $false
                if ($IncludeResources) {
                    try { $resources = @(Get-PEResources -Pe $pe) } catch { $resources = @() }
                    $hasTlb = [bool]($resources | Where-Object { $_.Type -ieq 'TYPELIB' -or $_.TypeRaw -ieq 'TYPELIB' })
                } else {
                    try { $hasTlb = Test-PEHasTypeLibResource -Pe $pe } catch { }
                }

                $tlibInfo = $null
                if ($IncludeTypeLib -and $hasTlb) {
                    $tlibInfo = Get-TypeLibInfoSafe -FilePath $resolved
                }
                $result.Com = [pscustomobject]@{
                    IsComServer    = (($selfReg -contains 'DllGetClassObject') -and ($selfReg -contains 'DllRegisterServer'))
                    HasTypeLib     = $hasTlb
                    SelfRegExports = @($selfReg)
                    TypeLib        = $tlibInfo
                }

                # .NET — cheap path: CLR header + MetaData root + AssemblyName.
                $dotNet = Get-DotNetCheapInfo -Pe $pe -FilePath $resolved
                if ($null -eq $dotNet) {
                    $dotNet = [pscustomobject]@{ IsManaged = $false }
                } elseif ($IncludeDotNetTypes) {
                    $deep = Get-DotNetDeepInfo -FilePath $resolved -IncludeTypes $true
                    if ($deep) {
                        $dotNet.IsComVisible = $deep.IsComVisible
                        $dotNet.Types        = $deep.Types
                    }
                }
                $result.DotNet = $dotNet

                if ($IncludeImports) {
                    try   { $result.Imports = @(Get-PEImports -Pe $pe) }
                    catch { $result.Imports = @() }
                }
                if ($IncludeExports) {
                    $result.Exports = @($expData.Exports)
                }
                if ($IncludeResources) {
                    $result.Resources = @($resources)
                }
            }
            catch {
                $result.ParseError = $_.Exception.Message
            }
            finally {
                if ($pe) {
                    try { $pe.Reader.Dispose() } catch {}
                    try { $pe.Stream.Dispose() } catch {}
                }
            }

            if ($IncludeSignature) {
                $result.Signature = Get-PESignatureInfoSafe -FilePath $resolved
            }
            if ($IncludeHash) {
                try   { $result.Sha256 = (Get-FileHash -LiteralPath $resolved -Algorithm SHA256).Hash }
                catch { $result.Sha256 = $null }
            }

            [pscustomobject]$result
        }
    }
}