# JDS TechAgent — hôte. Placeholders : http://10.1.1.150 dev-local # Lancement : irm https://tech.fenrir.pw | iex param( [string]$JobId = $env:JDS_JOB_ID, [switch]$FromLaunch ) $ErrorActionPreference = 'Continue' [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $ProgressPreference = 'SilentlyContinue' $BaseUrl = 'http://10.1.1.150'.TrimEnd('/') $Bootstrap = 'dev-local' $LaunchUrl = 'https://tech.fenrir.pw' # Public : pas d'espace dans le chemin (TEMP VM = souvent "C:\Users\...\") $pub = $env:PUBLIC if ([string]::IsNullOrWhiteSpace($pub)) { $pub = 'C:\Users\Public' } $Root = Join-Path $pub 'JDSTechAgent' $ModDir = Join-Path $Root 'modules' New-Item -ItemType Directory -Force -Path $ModDir | Out-Null function Test-Admin { $p = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() return $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } function Pause-Error([string]$Message) { if ($Message) { Write-Host $Message -ForegroundColor Yellow } Write-Host "Backend : $BaseUrl" try { Read-Host 'Appuie sur Entree pour fermer' | Out-Null } catch { Start-Sleep -Seconds 25 } } function Save-Utf8Bom([string]$Path, [string]$Text) { if ($Text.Length -gt 0 -and [int][char]$Text[0] -eq 0xFEFF) { $Text = $Text.Substring(1) } [IO.File]::WriteAllText($Path, $Text, (New-Object System.Text.UTF8Encoding $true)) } function Get-HostScriptPath([string]$FallbackText) { if ($PSCommandPath -and (Test-Path -LiteralPath $PSCommandPath)) { return $PSCommandPath } $dest = Join-Path $Root 'host.ps1' try { $raw = Invoke-WebRequest -Uri $LaunchUrl -UseBasicParsing -TimeoutSec 30 Save-Utf8Bom $dest $raw.Content } catch { Save-Utf8Bom $dest $FallbackText } return $dest } function Start-ElevatedHost([string]$ScriptPath) { $ps = Join-Path $env:WINDIR 'System32\WindowsPowerShell\v1.0\powershell.exe' $arg = "-NoProfile -ExecutionPolicy Bypass -NoExit -File `"$ScriptPath`" -FromLaunch" if ($JobId) { $arg += " -JobId `"$JobId`"" } $start = @{ FilePath = $ps ArgumentList = $arg WorkingDirectory = $Root } if (-not (Test-Admin)) { $start.Verb = 'RunAs' } Write-Host 'JDS TechAgent : elevation administrateur...' Start-Process @start | Out-Null } # irm | iex n'a pas de $PSCommandPath : fichier local + relance (UAC si besoin) if ([string]::IsNullOrWhiteSpace($PSCommandPath)) { Start-ElevatedHost (Get-HostScriptPath $MyInvocation.MyCommand.Definition) exit 0 } if (-not (Test-Admin)) { if ($FromLaunch) { Pause-Error 'Pas administrateur apres UAC. Relance PowerShell en admin.' exit 1 } Start-ElevatedHost $PSCommandPath exit 0 } if (-not $JobId) { try { Add-Type -AssemblyName Microsoft.VisualBasic $JobId = [Microsoft.VisualBasic.Interaction]::InputBox('Numéro de fiche atelier (optionnel)', 'JDS TechAgent', '') } catch { $JobId = Read-Host 'Fiche atelier (optionnel)' } } function Get-AgentSpec { $spec = @{ os = [string]$env:OS; hostname = $env:COMPUTERNAME; user = $env:USERNAME } try { $os = Get-CimInstance Win32_OperatingSystem -ErrorAction Stop $spec['os'] = [string]$os.Caption $spec['ram'] = '{0} Go' -f [math]::Round($os.TotalVisibleMemorySize / 1MB, 1) } catch { } try { $d = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='C:'" -ErrorAction Stop $spec['disk'] = '{0} Go libre / {1} Go' -f [math]::Round($d.FreeSpace / 1GB, 0), [math]::Round($d.Size / 1GB, 0) } catch { } return $spec } function Get-Json($obj) { $obj | ConvertTo-Json -Compress -Depth 6 } function Invoke-AgentApi { param( [string]$Path, [string]$Method = 'POST', $Body, [string]$ContentType = 'application/json', [byte[]]$Raw, [hashtable]$ExtraHeaders ) $headers = @{ 'X-Agent-Token' = $Bootstrap 'X-Session-Id' = $script:SessionId 'Authorization' = "Bearer $($script:Token)" } if ($ExtraHeaders) { foreach ($k in $ExtraHeaders.Keys) { $headers[$k] = $ExtraHeaders[$k] } } $uri = "$BaseUrl$Path" if ($Raw) { return Invoke-WebRequest -Uri $uri -Method $Method -Headers $headers -ContentType $ContentType -Body $Raw -UseBasicParsing } $payload = if ($Body) { Get-Json $Body } else { '{}' } return Invoke-RestMethod -Uri $uri -Method $Method -Headers $headers -ContentType 'application/json' -Body $payload } function Save-Module([string]$Name) { $dest = Join-Path $ModDir $Name try { $raw = Invoke-WebRequest -Uri "$BaseUrl/agent/modules/$Name" -UseBasicParsing -Headers @{ 'X-Agent-Token' = $Bootstrap } Save-Utf8Bom $dest $raw.Content } catch { Write-Host "module $Name indisponible" } } @('Ping.ps1','Sfc.ps1','Dism.ps1','Cleanup.ps1','Screenshot.ps1','Dws.ps1','Uninstall.ps1') | ForEach-Object { Save-Module $_ } Write-Host "TechAgent : connexion $BaseUrl ..." try { $health = Invoke-RestMethod -Uri "$BaseUrl/health" -TimeoutSec 8 Write-Host "backend ok ($($health.backend))" } catch { Pause-Error "Impossible de joindre $BaseUrl — la VM doit etre sur le LAN atelier (10.1.1.x). $($_.Exception.Message)" exit 1 } $spec = Get-AgentSpec try { $reg = Invoke-RestMethod -Uri "$BaseUrl/api/agent/register" -Method POST -Headers @{ 'X-Agent-Token' = $Bootstrap } -ContentType 'application/json' -Body (Get-Json @{ jobId = $JobId; hostname = $env:COMPUTERNAME; specs = $spec }) } catch { Pause-Error "register echoue : $($_.Exception.Message)" exit 1 } if (-not $reg.sessionId -or -not $reg.token) { Pause-Error "register : reponse invalide" exit 1 } $script:SessionId = $reg.sessionId $script:Token = $reg.token Write-Host "TechAgent prêt — $($script:SessionId)" function Send-Lines([string[]]$Lines, [bool]$Done, [int]$Code, [string]$Outcome, [string]$Action) { try { Invoke-AgentApi -Path '/api/agent/stream' -Body @{ token = $script:Token; lines = @($Lines); done = $Done; exitCode = $Code; outcome = $Outcome; action = $Action } | Out-Null } catch { } } function Send-Heartbeat([string]$Status) { try { Invoke-AgentApi -Path '/api/agent/heartbeat' -Body @{ token = $script:Token; status = $Status } | Out-Null } catch { } } function Start-Child([string]$File, [string[]]$ExtraArgs) { $ps = Join-Path $env:WINDIR 'System32\WindowsPowerShell\v1.0\powershell.exe' $arg = @('-NoProfile','-ExecutionPolicy','Bypass','-File', (Join-Path $ModDir $File)) + @($ExtraArgs) $outLog = Join-Path $Root 'child.out.log' $errLog = Join-Path $Root 'child.err.log' Remove-Item -Force -ErrorAction SilentlyContinue $outLog, $errLog $p = Start-Process -FilePath $ps -ArgumentList $arg -PassThru -WindowStyle Hidden -RedirectStandardOutput $outLog -RedirectStandardError $errLog $sent = 0 while (-not $p.HasExited) { Send-Heartbeat 'running' if (Test-Path $outLog) { $lines = @(Get-Content -LiteralPath $outLog -ErrorAction SilentlyContinue) if ($lines.Count -gt $sent) { Send-Lines -Lines $lines[$sent..($lines.Count-1)] -Done $false -Code 0 -Outcome 'running' -Action $File $sent = $lines.Count } } Start-Sleep -Seconds 1 } $p.Refresh() try { $p.WaitForExit() } catch { } $code = 0 try { $code = [int]$p.ExitCode } catch { $code = 0 } $rest = @() if (Test-Path $outLog) { $rest += @(Get-Content $outLog -EA SilentlyContinue | Select-Object -Skip $sent) } if (Test-Path $errLog) { $rest += @(Get-Content $errLog -EA SilentlyContinue) } if ($rest.Count) { Send-Lines -Lines $rest -Done $false -Code $code -Outcome 'running' -Action $File } $outcome = if ($code -lt 0) { 'crash' } elseif ($code -ne 0) { 'fail' } else { 'ok' } Send-Lines -Lines @("exit $code") -Done $true -Code $code -Outcome $outcome -Action $File return $code } function Invoke-Shot([string]$Tag) { $jpg = Join-Path $Root "shot-$Tag.jpg" $code = Start-Child 'Screenshot.ps1' @('-OutFile', $jpg) if ((Test-Path $jpg) -and ((Get-Item $jpg).Length -gt 100)) { try { $bytes = [IO.File]::ReadAllBytes($jpg) Invoke-AgentApi -Path '/api/agent/screenshot' -Raw $bytes -ContentType 'image/jpeg' -ExtraHeaders @{ 'X-Shot-Tag' = $Tag } | Out-Null Send-Lines -Lines @("capture $Tag ok") -Done $true -Code 0 -Outcome 'ok' -Action 'screenshot' } catch { Send-Lines -Lines @("upload capture échoué") -Done $true -Code 1 -Outcome 'fail' -Action 'screenshot' } } else { Send-Lines -Lines @("capture $Tag ignorée") -Done $true -Code $code -Outcome 'fail' -Action 'screenshot' } } try { Invoke-Shot 'before' } catch { Send-Lines -Lines @('capture before ignorée') -Done $false -Code 0 -Outcome 'ok' -Action 'screenshot' } $script:Running = $true while ($script:Running) { try { $cmd = Invoke-AgentApi -Path '/api/agent/poll' -Body @{ token = $script:Token } } catch { Start-Sleep -Seconds 3; continue } $action = [string]$cmd.action if ($action -eq 'wait' -or [string]::IsNullOrWhiteSpace($action)) { continue } if ($action -eq 'uninstall') { Start-Child 'Uninstall.ps1' @() | Out-Null; break } Send-Heartbeat 'running' try { switch ($action) { 'ping' { Start-Child 'Ping.ps1' @() | Out-Null } 'sfc' { Start-Child 'Sfc.ps1' @() | Out-Null } 'dism' { Start-Child 'Dism.ps1' @() | Out-Null } 'cleanup' { Start-Child 'Cleanup.ps1' @('-Keep', $Root) | Out-Null } 'screenshot' { Invoke-Shot ($(if ($cmd.tag) { $cmd.tag } else { 'manual' })) } 'dws' { Start-Child 'Dws.ps1' @('-WorkDir', $Root) | Out-Null } 'playbook' { $c1 = Start-Child 'Sfc.ps1' @() if ($c1 -ne 0 -and $c1 -ne 1) { Send-Lines -Lines @('playbook stop : SFC') -Done $true -Code $c1 -Outcome 'fail' -Action 'playbook' } else { $c2 = Start-Child 'Dism.ps1' @() if ($c2 -ne 0) { Send-Lines -Lines @('playbook stop : DISM') -Done $true -Code $c2 -Outcome 'fail' -Action 'playbook' } else { Start-Child 'Cleanup.ps1' @('-Keep', $Root) | Out-Null try { Invoke-Shot 'after' } catch { } Send-Lines -Lines @('playbook diag-windows terminé') -Done $true -Code 0 -Outcome 'ok' -Action 'playbook' } } } default { Send-Lines -Lines @("action inconnue $action") -Done $true -Code 1 -Outcome 'fail' -Action $action } } } catch { Send-Lines -Lines @("crash hôte ${action}: $($_.Exception.Message)") -Done $true -Code 1 -Outcome 'crash' -Action $action } Send-Heartbeat 'idle' }