install-portless.ps1 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. <#
  2. .SYNOPSIS
  3. Install a specific version of portless globally with verification.
  4. .DESCRIPTION
  5. 1. Inspects the published npm tarball BEFORE installing.
  6. 2. Verifies tarball SHA-512 against the npm registry.
  7. 3. Scans the package contents for known IOC strings from recent npm
  8. supply-chain attacks (TanStack, mini-shai-hulud).
  9. 4. Confirms no install scripts (preinstall/postinstall) are present.
  10. 5. Installs globally via npm.
  11. 6. Verifies version matches what was requested.
  12. 7. Records the pinned version.
  13. .PARAMETER Version
  14. Specific version to install (e.g. "0.13.0"). Required.
  15. .PARAMETER TargetDir
  16. Where to record version metadata (a bin/PORTLESS_VERSION file). Optional.
  17. .EXAMPLE
  18. .\install-portless.ps1 -Version 0.13.0 -TargetDir X:\my-stack
  19. #>
  20. [CmdletBinding()]
  21. param(
  22. [Parameter(Mandatory)][string]$Version,
  23. [string]$TargetDir = $null
  24. )
  25. $ErrorActionPreference = 'Stop'
  26. Write-Host "Installing portless@$Version with pre-install audit" -ForegroundColor Cyan
  27. Write-Host ("=" * 60)
  28. Write-Host ""
  29. # Step 1 — Inspect tarball without installing
  30. $tmp = Join-Path $env:TEMP "portless-inspect-$(Get-Random)"
  31. New-Item -ItemType Directory -Force -Path $tmp | Out-Null
  32. try {
  33. Write-Host "[1/6] Downloading tarball..." -ForegroundColor Yellow
  34. $tarballUrl = "https://registry.npmjs.org/portless/-/portless-$Version.tgz"
  35. $tarball = Join-Path $tmp 'portless.tgz'
  36. Invoke-WebRequest -Uri $tarballUrl -OutFile $tarball
  37. Write-Host "[2/6] Verifying SHA-512 against npm registry..." -ForegroundColor Yellow
  38. $meta = npm view portless@$Version --json 2>$null | ConvertFrom-Json
  39. $expectedIntegrity = $meta.dist.integrity # like "sha512-..."
  40. if (-not $expectedIntegrity) {
  41. throw "Could not fetch published integrity hash for portless@$Version"
  42. }
  43. # npm's integrity is "sha512-<base64>". Compare with our computed value.
  44. $actualBytes = [System.Security.Cryptography.SHA512]::Create().ComputeHash([IO.File]::ReadAllBytes($tarball))
  45. $actualB64 = [Convert]::ToBase64String($actualBytes)
  46. $expectedB64 = $expectedIntegrity -replace '^sha512-', ''
  47. if ($actualB64 -ne $expectedB64) {
  48. throw "TARBALL SHA-512 MISMATCH - aborting. Expected: $expectedB64. Got: $actualB64."
  49. }
  50. Write-Host " Match: sha512-$actualB64" -ForegroundColor Green
  51. Write-Host "[3/6] Extracting + auditing contents..." -ForegroundColor Yellow
  52. Push-Location $tmp
  53. tar -xzf portless.tgz
  54. Pop-Location
  55. $pkgDir = Join-Path $tmp 'package'
  56. # Scan for install scripts in package.json
  57. $pkgJson = Get-Content (Join-Path $pkgDir 'package.json') | ConvertFrom-Json
  58. $scripts = $pkgJson.scripts.PSObject.Properties.Name
  59. $installScripts = @('preinstall', 'install', 'postinstall', 'prepare')
  60. $foundInstallScripts = $scripts | Where-Object { $_ -in $installScripts }
  61. if ($foundInstallScripts) {
  62. Write-Warning "Package has install scripts: $($foundInstallScripts -join ', ')"
  63. Write-Warning "Review these before installing!"
  64. } else {
  65. Write-Host " ✓ No install scripts (preinstall/install/postinstall/prepare)"
  66. }
  67. # Check runtime dependencies (should be empty for portless)
  68. if ($pkgJson.dependencies -and $pkgJson.dependencies.PSObject.Properties.Count -gt 0) {
  69. Write-Warning "Package has runtime deps: $($pkgJson.dependencies.PSObject.Properties.Name -join ', ')"
  70. } else {
  71. Write-Host " ✓ Zero runtime dependencies"
  72. }
  73. # Scan for known IOC strings from recent attacks
  74. Write-Host "[4/6] Scanning for known supply-chain IOC strings..." -ForegroundColor Yellow
  75. $iocPatterns = @(
  76. 'getsession.org', 'masscan.cloud', 'git-tanstack',
  77. 'router_init', 'router_runtime',
  78. 'EveryBoiWeBuildIsAWormyBoi',
  79. 'claude@users.noreply.github.com',
  80. 'filev2.getsession',
  81. '@tanstack/setup',
  82. 'gh-token-monitor',
  83. '/proc/self/environ', '.claude/settings.json'
  84. )
  85. $hits = @()
  86. Get-ChildItem -Recurse -File $pkgDir | ForEach-Object {
  87. $content = Get-Content $_.FullName -Raw -ErrorAction SilentlyContinue
  88. foreach ($pattern in $iocPatterns) {
  89. if ($content -and $content.Contains($pattern)) {
  90. $hits += "$($_.Name): $pattern"
  91. }
  92. }
  93. }
  94. if ($hits) {
  95. throw "IOC MATCH FOUND - aborting:`n$($hits -join "`n")"
  96. }
  97. Write-Host " ✓ Zero IOC matches"
  98. # Step 5 — Install via npm
  99. Write-Host "[5/6] npm install -g portless@$Version..." -ForegroundColor Yellow
  100. npm install -g portless@$Version 2>&1 | Tee-Object -Variable npmOutput | Out-Host
  101. # Step 6 — Verify installed version
  102. Write-Host "[6/6] Verifying installed version..." -ForegroundColor Yellow
  103. $installed = (portless --version).Trim()
  104. if ($installed -ne $Version) {
  105. throw "Version mismatch: requested $Version, installed $installed"
  106. }
  107. Write-Host " Installed: $installed" -ForegroundColor Green
  108. # Optional: record in target dir
  109. if ($TargetDir) {
  110. $binDir = Join-Path $TargetDir 'bin'
  111. New-Item -ItemType Directory -Force -Path $binDir | Out-Null
  112. $Version | Out-File -FilePath (Join-Path $binDir 'PORTLESS_VERSION') -NoNewline -Encoding ascii
  113. Write-Host " Recorded version in: $binDir\PORTLESS_VERSION"
  114. }
  115. Write-Host ""
  116. Write-Host "Done. portless@$Version installed and audited." -ForegroundColor Green
  117. } finally {
  118. Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
  119. }