Public/Get-LGBinaryLicenseAudit.ps1
|
function Get-LGBinaryLicenseAudit { <# .SYNOPSIS Performs a comprehensive license compliance audit on compiled binaries (.exe, .dll). Inspects PE metadata, parses .NET .deps.json files, checks for SBOM files, and verifies local attribution files. .EXAMPLE Get-LGBinaryLicenseAudit -Path "C:\Projects\MyApp\bin\Release" #> [CmdletBinding()] param( [Parameter(Mandatory=$true)] [string[]]$Path ) $L = Get-LGEffectiveStrings $results = [System.Collections.Generic.List[PSCustomObject]]::new() $seenResults = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $nugetLicenseCache = @{} $licenseFilePatterns = @("LICENSE*", "LICENSES*", "COPYING*", "NOTICE*", "3rdpartylicenses*", "THIRD-PARTY-NOTICES*") $buildArtifactPatterns = @("*.js", "*.mjs", "*.cjs", "*.css", "*.wasm") function Add-LGUniqueResult { param( [Parameter(Mandatory = $true)] [PSCustomObject]$Item ) $key = "{0}|{1}|{2}|{3}|{4}" -f $Item.Name, $Item.Version, $Item.Publisher, $Item.License, $Item.Detail if ($seenResults.Add($key)) { $results.Add($Item) } } function Resolve-LGNuGetLicenseFromApi { param( [Parameter(Mandatory = $true)][string]$PackageId, [Parameter(Mandatory = $true)][string]$Version ) $cacheKey = "$($PackageId.ToLowerInvariant())/$Version" if ($nugetLicenseCache.ContainsKey($cacheKey)) { return $nugetLicenseCache[$cacheKey] } $license = "Unknown" try { $pkg = $PackageId.ToLowerInvariant() $ver = $Version.ToLowerInvariant() $uri = "https://api.nuget.org/v3/registration5-semver1/$pkg/$ver.json" $resp = Invoke-RestMethod -Uri $uri -Method Get -TimeoutSec 5 $entry = $resp.catalogEntry if ($entry -and $entry.licenseExpression) { $license = [string]$entry.licenseExpression } elseif ($entry -and $entry.licenseUrl) { $url = [string]$entry.licenseUrl if ($url -match "licenses.nuget.org/([^/?#]+)") { $license = $Matches[1] } else { $license = $url } } } catch { # Keep Unknown on network/API failures. } $nugetLicenseCache[$cacheKey] = $license return $license } function Get-LGSbomComponents { param([string]$Directory) $components = [System.Collections.Generic.List[PSCustomObject]]::new() $sbomFiles = Get-ChildItem -Path $Directory -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -like "*sbom*.json" -or $_.Name -like "*bom*.json" -or $_.Name -like "*spdx*.json" } foreach ($sbom in $sbomFiles) { Write-Host " Found SBOM file: $($sbom.Name), parsing..." -ForegroundColor DarkGray try { $sbomData = Get-Content $sbom.FullName -Raw | ConvertFrom-Json if ($sbomData.bomFormat -eq "CycloneDX" -and $sbomData.components) { foreach ($comp in $sbomData.components) { $compLicense = "Unknown" if ($comp.licenses -and $comp.licenses.Count -gt 0) { $lic = $comp.licenses[0] if ($lic.license) { $compLicense = if ($lic.license.id) { $lic.license.id } else { $lic.license.name } } elseif ($lic.expression) { $compLicense = $lic.expression } } $components.Add([PSCustomObject]@{ Source = $sbom.Name Name = $comp.name Version = $comp.version Publisher = if ($comp.publisher) { $comp.publisher } else { "Unknown" } License = $compLicense }) } } } catch { # Skip invalid SBOMs } } $components } foreach ($targetPath in $Path) { if (-not (Test-Path $targetPath)) { Write-Warning "Binary path not found: $targetPath" continue } # Resolve directory if a file path was passed $dir = $targetPath $specificFiles = @() if (Test-Path $targetPath -PathType Leaf) { $dir = Split-Path $targetPath $specificFiles = @((Get-Item $targetPath)) } # 1. Verify if the directory has a LICENSE or 3rd Party Notice file (Attribution Check) $rootLicenseFiles = Get-ChildItem -Path $dir -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -like "LICENSE*" -or $_.Name -like "3rdpartylicenses*" -or $_.Name -like "LICENSES*" -or $_.Name -like "NOTICE*" } $hasRootLicense = ($rootLicenseFiles.Count -gt 0) $sbomComponents = @(Get-LGSbomComponents -Directory $dir) $sbomPackageKeys = @{} foreach ($component in $sbomComponents) { if ($component.Name -and $component.Version) { $key = "{0}|{1}" -f $component.Name.ToLowerInvariant(), $component.Version $sbomPackageKeys[$key] = $true } } # 2. Get list of binaries to inspect (.exe and .dll) $binaries = if ($specificFiles.Count -gt 0) { $specificFiles } else { Get-ChildItem -Path $dir -Include "*.exe", "*.dll" -Recurse -File -ErrorAction SilentlyContinue | Where-Object { $_.FullName -notmatch '[\\/]node_modules[\\/]' } } if ($binaries.Count -gt 0) { Write-Host " Auditing $($binaries.Count) binaries in $dir..." -ForegroundColor Cyan } $buildArtifacts = Get-ChildItem -Path $dir -Include $buildArtifactPatterns -Recurse -File -ErrorAction SilentlyContinue | Where-Object { $_.FullName -notmatch '[\\/]node_modules[\\/]' } if ($buildArtifacts.Count -gt 0) { Write-Host " Auditing $($buildArtifacts.Count) build artifact(s) in $dir..." -ForegroundColor Cyan } foreach ($artifact in $buildArtifacts) { $artifactText = '' try { $artifactText = Get-Content $artifact.FullName -Raw -ErrorAction Stop } catch {} $licenseType = "Unknown" if ($artifactText -match "(?i)Affero General Public License|\bAGPL\b") { $licenseType = "AGPL" } elseif ($artifactText -match "(?i)General Public License|\bGPL\b|GPL-3\.0|GPL-2\.0") { $licenseType = "GPL" } elseif ($artifactText -match "(?i)MIT License|\bMIT\b") { $licenseType = "MIT" } elseif ($artifactText -match "(?i)Apache License|Apache-2\.0") { $licenseType = "Apache-2.0" } elseif ($artifactText -match "(?i)BSD License|\bBSD\b") { $licenseType = "BSD" } elseif ($artifactText -match "(?i)\bISC\b") { $licenseType = "ISC" } $detailText = "Build Artifact License Scan" if (-not $hasRootLicense) { $detailText += " [MISSING LICENSE/ATTRIBUTION FILE]" } if ($licenseType -eq "Unknown") { $detailText += " [UNKNOWN LICENSE]" } Add-LGUniqueResult -Item ([PSCustomObject]@{ Module = "BinaryAudit" Name = "Build Artifact: $($artifact.Name)" Version = "" Publisher = "BuildOutput" License = $licenseType Detail = $detailText Status = Get-LGLicenseAuditStatus -License $licenseType -HasMissingAttribution (-not $hasRootLicense) ComputerName = $env:COMPUTERNAME }) } foreach ($bin in $binaries) { $binName = $bin.Name $binPath = $bin.FullName # A. Inspect PE Metadata $vi = $null try { $vi = $bin.VersionInfo } catch {} $company = if ($vi -and $vi.CompanyName) { $vi.CompanyName } else { "Unknown Company" } $copyright = if ($vi -and $vi.LegalCopyright) { $vi.LegalCopyright } else { "" } $version = if ($vi -and $vi.ProductVersion) { $vi.ProductVersion } else { $bin.VersionInfo.FileVersion } if (-not $version) { $version = "1.0.0" } # Basic heuristics: Check copyright/trademarks for "GPL" or "AGPL" terms $licenseType = "Commercial/Proprietary" if ($copyright -match "GPL" -or $copyright -match "General Public License" -or $vi.LegalTrademarks -match "GPL") { $licenseType = "GPL" } elseif ($copyright -match "Apache" -or $copyright -match "ASL") { $licenseType = "Apache" } elseif ($copyright -match "MIT" -or $copyright -match "X11") { $licenseType = "MIT" } # Compliance Warning: Blank metadata in own binaries might indicate lack of governance $complianceDetail = "Binary Metadata Check" if (-not $copyright -and -not $vi.CompanyName) { $complianceDetail += " [MISSING METADATA]" } if (-not $hasRootLicense) { $complianceDetail += " [MISSING LICENSE/ATTRIBUTION FILE]" } Add-LGUniqueResult -Item ([PSCustomObject]@{ Module = "BinaryAudit" Name = "Binary: $($binName)" Version = $version Publisher = $company License = $licenseType Detail = "Copyright: $copyright | $complianceDetail" Status = Get-LGLicenseAuditStatus -License $licenseType -HasMissingAttribution (-not $hasRootLicense) ComputerName = $env:COMPUTERNAME }) # B. Check for .NET Core .deps.json $depsJsonPath = [System.IO.Path]::ChangeExtension($binPath, ".deps.json") if (Test-Path $depsJsonPath) { Write-Host " Found .deps.json for $($binName), parsing NuGet dependencies..." -ForegroundColor DarkGray try { $depsJson = Get-Content $depsJsonPath -Raw | ConvertFrom-Json # Parse dependencies from targets section if ($depsJson.targets) { $targetFramework = ($depsJson.targets.psobject.Properties | Select-Object -First 1).Name if ($targetFramework) { $targetDeps = $depsJson.targets.$targetFramework foreach ($depProp in $targetDeps.psobject.Properties) { # Format: "PackageName/Version" $depKey = $depProp.Name if ($depKey -match "^([^/]+)/(.+)$") { $packageId = $Matches[1] $pkgVersion = $Matches[2] $sbomKey = "{0}|{1}" -f $packageId.ToLowerInvariant(), $pkgVersion if ($sbomPackageKeys.ContainsKey($sbomKey)) { continue } # Only look at package dependencies, skip projects or runtimes $depVal = $depProp.Value if ($depVal.type -eq "package" -or $null -eq $depVal.type) { # Resolve package license from NuGet cache $nugetCache = Join-Path $env:USERPROFILE ".nuget\packages" $pkgFolder = Join-Path $nugetCache $packageId.ToLowerInvariant() $versionFolder = Join-Path $pkgFolder $pkgVersion $nuspecPath = Join-Path $versionFolder "$($packageId.ToLowerInvariant()).nuspec" $license = "Unknown" $hasLicenseFile = $false if (Test-Path $nuspecPath) { try { [xml]$nuspecXml = Get-Content $nuspecPath $ns = New-Object System.Xml.XmlNamespaceManager($nuspecXml.NameTable) $ns.AddNamespace("ns", $nuspecXml.DocumentElement.NamespaceURI) $licenseNode = $nuspecXml.SelectSingleNode("//ns:license", $ns) if ($licenseNode) { $license = $licenseNode.InnerText } else { $licenseUrlNode = $nuspecXml.SelectSingleNode("//ns:licenseUrl", $ns) if ($licenseUrlNode) { $url = $licenseUrlNode.InnerText if ($url -match "licenses.nuget.org/MIT" -or $url -match "opensource.org/licenses/MIT") { $license = "MIT" } elseif ($url -match "licenses.nuget.org/Apache-2.0") { $license = "Apache-2.0" } else { $license = $url } } } foreach ($pattern in $licenseFilePatterns) { $files = Get-ChildItem -Path $versionFolder -Filter $pattern -File -ErrorAction SilentlyContinue if ($files -and $files.Count -gt 0) { $hasLicenseFile = $true break } } } catch {} } if ($license -eq "Unknown") { $license = Resolve-LGNuGetLicenseFromApi -PackageId $packageId -Version $pkgVersion } $detailText = "NuGet Package: $packageId (Runtime Dependency of $binName)" if (-not $hasLicenseFile -and $license -ne "Unknown") { $detailText += " [MISSING PHYSICAL LICENSE FILE]" } Add-LGUniqueResult -Item ([PSCustomObject]@{ Module = "BinaryAudit" Name = "Binary Dependency ($binName): $packageId" Version = $pkgVersion Publisher = "NuGet" License = $license Detail = $detailText Status = Get-LGLicenseAuditStatus -License $license -HasMissingAttribution (-not $hasLicenseFile) ComputerName = $env:COMPUTERNAME }) } } } } } } catch { # Skip invalid deps.json files } } } foreach ($component in $sbomComponents) { Add-LGUniqueResult -Item ([PSCustomObject]@{ Module = "BinaryAudit" Name = "SBOM Dependency ($($component.Source)): $($component.Name)" Version = $component.Version Publisher = $component.Publisher License = $component.License Detail = "SBOM Package License: $($component.License)" Status = Get-LGLicenseAuditStatus -License $component.License ComputerName = $env:COMPUTERNAME }) } } $results } |