Public/Get-LGProjectDependencies.ps1

function Get-LGProjectDependencies {
    <#
    .SYNOPSIS
        Scans project directories for dependencies (e.g. Node.js node_modules and .NET NuGet csproj) and extracts their licenses.
        Can scan recursively if multiple projects exist under a specified path.
        Also verifies if the physical license files exist within the packages and project roots.
    .EXAMPLE
        Get-LGProjectDependencies -ProjectPath "C:\Projects\MyApp"
    .EXAMPLE
        Get-LGProjectDependencies -ProjectPath "C:\Projects"
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string[]]$ProjectPath
    )

    $L = Get-LGEffectiveStrings
    $deps = [System.Collections.Generic.List[PSCustomObject]]::new()
    $foundPackages = @{}

    # Common filenames for license texts
    $licenseFilePatterns = @("LICENSE*", "LICENSES*", "COPYING*", "NOTICE*", "LICENSE-MIT*", "LICENSE-APACHE*")

    foreach ($path in $ProjectPath) {
        if (-not (Test-Path $path)) {
            Write-Warning "Project path not found: $path"
            continue
        }

        # --- A. Node.js (NPM) Discovery ---
        # Find all project roots (folders containing package.json, but not inside node_modules)
        $projectFiles = Get-ChildItem -Path $path -Filter "package.json" -Recurse -ErrorAction SilentlyContinue | 
                        Where-Object { $_.FullName -notmatch '[\\/]node_modules[\\/]' }

        # If no package.json is found recursively, check if the path itself is a project root with node_modules
        $projectDirs = @()
        if ($projectFiles) {
            $projectDirs = $projectFiles | ForEach-Object { $_.DirectoryName }
        } elseif (Test-Path (Join-Path $path "node_modules")) {
            $projectDirs = @($path)
        }

        # Deduplicate directories
        $projectDirs = $projectDirs | Select-Object -Unique

        # Scan each discovered Node.js project
        foreach ($dir in $projectDirs) {
            # Check if project root has a license file
            $rootLicenseFiles = Get-ChildItem -Path $dir -File -ErrorAction SilentlyContinue | 
                                Where-Object { $_.Name -like "LICENSE*" -or $_.Name -like "3rdpartylicenses*" -or $_.Name -like "LICENSES*" }
            
            if (-not $rootLicenseFiles) {
                Write-Host " [WARN] NPM Project '$($dir)' does not have a root LICENSE or 3rdpartylicenses file!" -ForegroundColor Yellow
            } else {
                Write-Host " [OK] Found NPM project license/attribution files: $(($rootLicenseFiles | Select-Object -ExpandProperty Name) -join ', ')" -ForegroundColor Green
            }

            $nodeModulesPath = Join-Path $dir "node_modules"
            if (-not (Test-Path $nodeModulesPath)) {
                continue
            }

            Write-Host " Scanning NPM dependencies in $dir..." -ForegroundColor Cyan
            $packages = Get-ChildItem -Path $nodeModulesPath -Filter "package.json" -Recurse -Depth 4 -ErrorAction SilentlyContinue

            foreach ($pkg in $packages) {
                try {
                    $json = Get-Content $pkg.FullName -Raw | ConvertFrom-Json -ErrorAction Stop
                    if ($json -and $json.name) {
                        $projectName = Split-Path $dir -Leaf
                        $key = "npm_$($projectName)_$($json.name)_$($json.version)"
                        if ($foundPackages.ContainsKey($key)) { continue }
                        $foundPackages[$key] = $true

                        $license = "Unknown"
                        if ($null -ne $json.license) {
                            if ($json.license -is [string]) { $license = $json.license }
                            elseif ($null -ne $json.license.type) { $license = $json.license.type }
                        } elseif ($null -ne $json.licenses -and $json.licenses.Count -gt 0) {
                            if ($null -ne $json.licenses[0].type) {
                                $license = $json.licenses[0].type
                            } elseif ($json.licenses[0] -is [string]) {
                                $license = $json.licenses[0]
                            }
                        }

                        # Check if physical license file exists
                        $packageDir = $pkg.DirectoryName
                        $hasLicenseFile = $false
                        foreach ($pattern in $licenseFilePatterns) {
                            $files = Get-ChildItem -Path $packageDir -Filter $pattern -File -ErrorAction SilentlyContinue
                            if ($files -and $files.Count -gt 0) {
                                $hasLicenseFile = $true
                                break
                            }
                        }

                        $detailText = "Package License: $license (Project: $projectName)"
                        if (-not $hasLicenseFile -and $license -ne "Unknown") {
                            $detailText += " [MISSING PHYSICAL LICENSE FILE]"
                        }
                        $auditStatus = Get-LGLicenseAuditStatus -License $license -HasMissingAttribution (-not $hasLicenseFile)

                        $deps.Add([PSCustomObject]@{
                            Module       = "NPM"
                            Name         = "$($projectName): $($json.name)"
                            Version      = $json.version
                            Publisher    = "NPM"
                            License      = $license
                            Detail       = $detailText
                            Status       = $auditStatus
                            ComputerName = $env:COMPUTERNAME
                            HasLicense   = $hasLicenseFile
                        })
                    }
                } catch {}
            }
        }

        # --- B. .NET (NuGet) Discovery ---
        # Find all .csproj files recursively (ignoring bin/obj directories)
        $csprojFiles = Get-ChildItem -Path $path -Filter "*.csproj" -Recurse -ErrorAction SilentlyContinue | 
                       Where-Object { $_.FullName -notmatch '[\\/]obj[\\/]' -and $_.FullName -notmatch '[\\/]bin[\\/]' }

        foreach ($csproj in $csprojFiles) {
            $projectName = Split-Path $csproj.DirectoryName -Leaf
            Write-Host " Scanning NuGet dependencies in $($csproj.Name)..." -ForegroundColor Cyan

            # Check if project root has a license file
            $rootLicenseFiles = Get-ChildItem -Path $csproj.DirectoryName -File -ErrorAction SilentlyContinue | 
                                Where-Object { $_.Name -like "LICENSE*" -or $_.Name -like "3rdpartylicenses*" -or $_.Name -like "LICENSES*" }
            
            if (-not $rootLicenseFiles) {
                Write-Host " [WARN] .NET Project '$projectName' does not have a root LICENSE or 3rdpartylicenses file!" -ForegroundColor Yellow
            }

            try {
                [xml]$xml = Get-Content $csproj.FullName -ErrorAction Stop
                
                # Find all PackageReference nodes
                $packageRefs = $xml.SelectNodes("//PackageReference")
                
                foreach ($ref in $packageRefs) {
                    $packageId = $ref.Include
                    $version = $ref.Version
                    
                    if (-not $version -and $ref.HasAttribute("Version")) {
                        $version = $ref.GetAttribute("Version")
                    }
                    if (-not $version) {
                        $verNode = $ref.SelectSingleNode("Version")
                        if ($verNode) { $version = $verNode.InnerText }
                    }

                    if (-not $packageId -or -not $version) { continue }

                    # Track unique NuGet package per project
                    $key = "nuget_${projectName}_${packageId}_${version}"
                    if ($foundPackages.ContainsKey($key)) { continue }
                    $foundPackages[$key] = $true

                    # Resolve license from local NuGet cache
                    $nugetCache = Join-Path $env:USERPROFILE ".nuget\packages"
                    $pkgFolder = Join-Path $nugetCache $packageId.ToLowerInvariant()
                    $versionFolder = Join-Path $pkgFolder $version
                    $nuspecPath = Join-Path $versionFolder "$($packageId.ToLowerInvariant()).nuspec"
                    
                    $license = "Unknown"
                    $hasLicenseFile = $false
                    
                    if (Test-Path $nuspecPath) {
                        try {
                            [xml]$nuspecXml = Get-Content $nuspecPath
                            
                            # NuSpec XML elements are usually namespaced
                            $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) {
                                    # Use the URL if metadata tag isn't set, e.g. mapping to SPDX format if recognizable
                                    $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 }
                                }
                            }
                            
                            # Check physical license files in package folder
                            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 {}
                    }
                    
                    $detailText = "Package License: $license (Project: $projectName)"
                    if (-not $hasLicenseFile -and $license -ne "Unknown") {
                        $detailText += " [MISSING PHYSICAL LICENSE FILE]"
                    }
                    $auditStatus = Get-LGLicenseAuditStatus -License $license -HasMissingAttribution (-not $hasLicenseFile)

                    $deps.Add([PSCustomObject]@{
                        Module       = "NuGet"
                        Name         = "$($projectName): $($packageId)"
                        Version      = $version
                        Publisher    = "NuGet"
                        License      = $license
                        Detail       = $detailText
                        Status       = $auditStatus
                        ComputerName = $env:COMPUTERNAME
                        HasLicense   = $hasLicenseFile
                    })
                }
            } catch {}
        }
    }

    if ($deps.Count -gt 0) {
        Write-Host (" {0} project dependencies found in total." -f $deps.Count) -ForegroundColor DarkGray
    }

    $deps
}