Private/DotNetInspect.ps1

# .NET assembly inspection. Cheap path reads CLR header + MetaData root from
# PE bytes (no AppDomain load). Deep path uses ReflectionOnlyLoadFrom for
# ComVisible / type enumeration.

function Get-DotNetCheapInfo {
    # Reads the CLR header and the MetaData root from PE bytes — does not
    # load anything into the AppDomain. Returns null when there is no CLR
    # data directory.
    param($Pe, [string]$FilePath)
    $clr = $Pe.DataDirectories[$script:DD_CLR]
    if (-not $clr -or $clr.Size -eq 0 -or $clr.Rva -eq 0) { return $null }

    $clrOff = ConvertTo-FileOffset -Rva $clr.Rva -Sections $Pe.Sections
    if ($null -eq $clrOff) { return $null }

    $br = $Pe.Reader
    $br.BaseStream.Position = $clrOff

    # IMAGE_COR20_HEADER
    $cb           = $br.ReadUInt32()
    $rtMaj        = $br.ReadUInt16()
    $rtMin        = $br.ReadUInt16()
    $mdRva        = $br.ReadUInt32()
    $mdSize       = $br.ReadUInt32()
    $corFlags     = $br.ReadUInt32()
    $entryToken   = $br.ReadUInt32()

    # MetaData root — runtime version string lives just after the BSJB header
    $rtVersion = $null
    if ($mdRva -ne 0) {
        $mdOff = ConvertTo-FileOffset -Rva $mdRva -Sections $Pe.Sections
        if ($null -ne $mdOff) {
            $br.BaseStream.Position = $mdOff
            $sig = $br.ReadUInt32()
            if ($sig -eq 0x424A5342) {   # 'BSJB'
                $null   = $br.ReadUInt16()    # iMajorVer
                $null   = $br.ReadUInt16()    # iMinorVer
                $null   = $br.ReadUInt32()    # iExtraData (reserved)
                $verLen = $br.ReadUInt32()    # padded to 4-byte boundary
                if ($verLen -gt 0 -and $verLen -lt 1024) {
                    $verBytes = $br.ReadBytes([int]$verLen)
                    $end = [Array]::IndexOf($verBytes, [byte]0)
                    if ($end -ge 0) { $verBytes = $verBytes[0..([Math]::Max($end - 1, 0))] }
                    if ($verBytes.Length -gt 0) {
                        $rtVersion = [Text.Encoding]::UTF8.GetString($verBytes)
                    }
                }
            }
        }
    }

    # Decode CorFlags into a string array
    $flagsDecoded = ConvertTo-Flags -Value $corFlags -Map $script:CorFlagsMap

    # PE kind: combines Machine + PE32/PE32+ + CorFlags
    $isILOnly    = ($corFlags -band 0x00001) -ne 0
    $is32Req     = ($corFlags -band 0x00002) -ne 0
    $is32Pref    = ($corFlags -band 0x20000) -ne 0
    $machine     = $Pe.Machine
    $peKind = if (-not $isILOnly) {
        'ManagedMixed'
    } elseif ($machine -eq 0x8664) {
        'x64'
    } elseif ($machine -eq 0xAA64) {
        'ARM64'
    } elseif ($is32Req -and $is32Pref) {
        'AnyCPUPrefer32'
    } elseif ($is32Req) {
        'x86'
    } else {
        'AnyCPU'
    }

    # AssemblyName.GetAssemblyName reads metadata only, doesn't load.
    $aname = $null
    try { $aname = [System.Reflection.AssemblyName]::GetAssemblyName($FilePath) } catch { }

    $asmName = $null; $asmVer = $null; $asmCulture = $null; $pkt = $null
    if ($aname) {
        $asmName    = $aname.Name
        $asmVer     = if ($aname.Version) { $aname.Version.ToString() } else { $null }
        $asmCulture = if ([string]::IsNullOrEmpty($aname.CultureName)) { 'neutral' } else { $aname.CultureName }
        $pktBytes   = $aname.GetPublicKeyToken()
        if ($pktBytes -and $pktBytes.Length -gt 0) {
            $pkt = -join ($pktBytes | ForEach-Object { '{0:x2}' -f $_ })
        } else {
            $pkt = ''   # not strong-name signed
        }
    }

    [pscustomobject]@{
        IsManaged         = $true
        RuntimeVersion    = $rtVersion
        CorHeaderVersion  = "$rtMaj.$rtMin"
        CorFlags          = ('0x{0:X}' -f $corFlags)
        CorFlagsDecoded   = $flagsDecoded
        PEKind            = $peKind
        AssemblyName      = $asmName
        Version           = $asmVer
        Culture           = $asmCulture
        PublicKeyToken    = $pkt
        EntryPointToken   = ('0x{0:X8}' -f $entryToken)
        HasMetaData       = ($null -ne $rtVersion)
        IsComVisible      = $null   # filled by deep path
        Types             = $null   # filled by deep path
    }
}

function Get-DotNetDeepInfo {
    # Calls ReflectionOnlyLoadFrom and reads ComVisible at the assembly
    # level plus a per-type listing with [ComVisible]/[Guid]/[ProgId].
    # Returns @{ IsComVisible; Types } or $null on failure (mixed-mode
    # assemblies, missing deps, etc.).
    param([string]$FilePath, [bool]$IncludeTypes)

    # A scriptblock cast to a delegate handles missing-dep failures.
    $resolver = [System.ResolveEventHandler]{
        param($sender, $args)
        try { [System.Reflection.Assembly]::ReflectionOnlyLoad($args.Name) }
        catch { $null }
    }

    $domain = [System.AppDomain]::CurrentDomain
    $domain.add_ReflectionOnlyAssemblyResolve($resolver)
    try {
        $asm = [System.Reflection.Assembly]::ReflectionOnlyLoadFrom($FilePath)

        # Assembly-level ComVisible (default is true for an assembly that
        # carries no [ComVisible] attribute, but we report null when the
        # attribute is absent so the caller can distinguish).
        $asmComVisible = $null
        foreach ($cad in [System.Reflection.CustomAttributeData]::GetCustomAttributes($asm)) {
            if ($cad.AttributeType.FullName -eq 'System.Runtime.InteropServices.ComVisibleAttribute') {
                if ($cad.ConstructorArguments.Count -gt 0) {
                    $asmComVisible = [bool]$cad.ConstructorArguments[0].Value
                }
                break
            }
        }

        $typesOut = $null
        if ($IncludeTypes) {
            $allTypes = $null
            try {
                $allTypes = $asm.GetTypes()
            } catch [System.Reflection.ReflectionTypeLoadException] {
                $allTypes = $_.Exception.Types | Where-Object { $null -ne $_ }
            }
            $list = New-Object System.Collections.Generic.List[object]
            foreach ($t in $allTypes) {
                if (-not $t) { continue }
                $tcv = $null; $tguid = $null; $tprog = $null
                try {
                    foreach ($cad in [System.Reflection.CustomAttributeData]::GetCustomAttributes($t)) {
                        switch ($cad.AttributeType.FullName) {
                            'System.Runtime.InteropServices.ComVisibleAttribute' {
                                if ($cad.ConstructorArguments.Count -gt 0) {
                                    $tcv = [bool]$cad.ConstructorArguments[0].Value
                                }
                            }
                            'System.Runtime.InteropServices.GuidAttribute' {
                                if ($cad.ConstructorArguments.Count -gt 0) {
                                    $tguid = [string]$cad.ConstructorArguments[0].Value
                                }
                            }
                            'System.Runtime.InteropServices.ProgIdAttribute' {
                                if ($cad.ConstructorArguments.Count -gt 0) {
                                    $tprog = [string]$cad.ConstructorArguments[0].Value
                                }
                            }
                        }
                    }
                } catch { }

                [void]$list.Add([pscustomobject]@{
                    FullName     = $t.FullName
                    Namespace    = $t.Namespace
                    BaseType     = if ($t.BaseType) { $t.BaseType.FullName } else { $null }
                    IsClass      = [bool]$t.IsClass
                    IsInterface  = [bool]$t.IsInterface
                    IsEnum       = [bool]$t.IsEnum
                    IsValueType  = [bool]$t.IsValueType
                    IsAbstract   = [bool]$t.IsAbstract
                    IsSealed     = [bool]$t.IsSealed
                    IsPublic     = [bool]$t.IsPublic
                    IsComVisible = $tcv
                    Guid         = $tguid
                    ProgId       = $tprog
                })
            }
            $typesOut = $list.ToArray()
        }

        return @{
            IsComVisible = $asmComVisible
            Types        = $typesOut
        }
    } catch {
        return $null
    } finally {
        $domain.remove_ReflectionOnlyAssemblyResolve($resolver)
    }
}