# VulnRadar - Script de detection du stack technique (Windows / PowerShell) # Usage : powershell -ExecutionPolicy Bypass -File vulnradar-detect.ps1 # Sans options : affiche le JSON (a coller dans VulnRadar > Environnement > Importer) # Avec -Send -Url : envoie directement a VulnRadar # Avec -Yes : installe aussi une tache planifiee quotidienne (heure nocturne aleatoire) # # -- Passage du jeton d'ingestion (ordre de priorite decroissant) --------------- # # 1. -TokenFile Fichier de jeton explicite # 2. $env:VULNRADAR_TOKEN Variable d'environnement # 3. $env:LOCALAPPDATA\VulnRadar\token.txt Fichier par defaut # 4. -Token Argument en ligne de commande -- DEPRECIEE : # le jeton apparait dans la definition de la tache # planifiee, dans Get-ScheduledTask et en clair dans # le XML C:\Windows\System32\Tasks\VulnRadarDetect. # Utiliser un fichier de jeton a la place. # # La tache planifiee installee par -Yes ne contient JAMAIS le jeton en clair : # elle appelle le script sans -Token, qui lit le fichier de jeton a chaque execution. Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' param( [switch]$Send, [string]$Token = "", [string]$TokenFile = "", [string]$Url = "", [switch]$Yes ) # -- Resolution du jeton d'ingestion (priorite 1->4) --------------------------- $_resolvedToken = "" $_defaultTokenFile = Join-Path $env:LOCALAPPDATA "VulnRadar\token.txt" function Resolve-IngestToken { # Priorite 1 : -TokenFile explicite if ($TokenFile) { if (-not (Test-Path $TokenFile)) { Write-Error "Fichier de jeton introuvable : $TokenFile"; exit 1 } $script:_resolvedToken = (Get-Content $TokenFile -Raw).Trim() return } # Priorite 2 : variable d'environnement VULNRADAR_TOKEN if ($env:VULNRADAR_TOKEN) { $script:_resolvedToken = $env:VULNRADAR_TOKEN.Trim() return } # Priorite 3 : fichier par defaut $env:LOCALAPPDATA\VulnRadar\token.txt if (Test-Path $_defaultTokenFile) { $script:_resolvedToken = (Get-Content $_defaultTokenFile -Raw).Trim() return } # Priorite 4 : -Token (deprecie) if ($Token) { Write-Warning "-Token passe le jeton en clair (visible dans la definition de la tache planifiee et dans ps)." Write-Warning " Prefere : mkdir `"$(Split-Path $_defaultTokenFile)`" -Force; Set-Content `"$_defaultTokenFile`" ''" $script:_resolvedToken = $Token.Trim() return } } # Note : la verification des permissions NTFS (ACL) pour s'assurer que le fichier # n'est pas lisible par 'Everyone'/'Users' est possible via Get-Acl, mais son # interpretation correcte necessite d'evaluer les ACL heritees et les groupes imbriques, # ce qui est fragile en PowerShell portable. Ce controle n'est pas implemente ; # l'utilisateur doit s'assurer que le fichier de jeton est protege par les permissions # NTFS appropriees (acces restreint au compte courant). $results = [System.Collections.Generic.List[hashtable]]::new() function Add-Tech { param([string]$Name, [string]$Version, [string]$Category, [string]$SourceType = "host", [string]$SourceRef = "") if ($Version -and $Version -match '\d+\.\d+') { $v = [regex]::Match($Version, '\d+\.\d+(\.\d+)?').Value if ($SourceType -and $SourceType -ne "host") { $results.Add(@{ name = $Name; version = $v; category = $Category; source_type = $SourceType; source_ref = $SourceRef }) } else { $results.Add(@{ name = $Name; version = $v; category = $Category }) } } } function Get-CmdVersion { param([string]$Cmd, [string]$CmdArgs = "--version") try { $out = & $Cmd $CmdArgs 2>&1 | Select-Object -First 1 return [string]$out } catch { return $null } } # -- Web servers ------------------------------------------------------------ $iis = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\InetStp" -ErrorAction SilentlyContinue if ($iis) { Add-Tech "IIS" $iis.VersionString "webserver" } # -- Languages / runtimes ------------------------------------------------------ Add-Tech "PHP" (Get-CmdVersion "php") "language" Add-Tech "Python" (Get-CmdVersion "python" "--version") "language" Add-Tech "Node.js" (Get-CmdVersion "node") "language" Add-Tech "Ruby" (Get-CmdVersion "ruby" "--version") "language" Add-Tech "Go" (Get-CmdVersion "go" "version") "language" Add-Tech ".NET" (Get-CmdVersion "dotnet") "language" Add-Tech "Rust" (Get-CmdVersion "rustc" "--version") "language" try { $jv = java -version 2>&1 | Select-Object -First 1 Add-Tech "Java" ([string]$jv) "language" } catch {} # -- Databases ------------------------------------------------------------ Add-Tech "MySQL" (Get-CmdVersion "mysql") "database" Add-Tech "PostgreSQL" (Get-CmdVersion "psql") "database" Add-Tech "Redis" (Get-CmdVersion "redis-server") "database" Add-Tech "MongoDB" (Get-CmdVersion "mongod" "--version") "database" $sqlSvc = Get-Service -Name "MSSQLSERVER" -ErrorAction SilentlyContinue if ($sqlSvc) { $sqlReg = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL" -ErrorAction SilentlyContinue if ($sqlReg) { $inst = ($sqlReg.PSObject.Properties | Where-Object { $_.Name -notlike "PS*" } | Select-Object -First 1).Value if ($inst) { $sqlVer = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$inst\MSSQLServer\CurrentVersion" -ErrorAction SilentlyContinue if ($sqlVer) { Add-Tech "SQL Server" $sqlVer.CurrentVersion "database" } } } } # -- Security / TLS ------------------------------------------------------------ Add-Tech "OpenSSL" (Get-CmdVersion "openssl" "version") "library" # -- Containers ------------------------------------------------------------ Add-Tech "Docker" (Get-CmdVersion "docker") "container" # -- OS (Windows) ------------------------------------------------------------ $os = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue if ($os) { $build = ($os.Version -split '\.')[2] if ($os.Caption -match "Server") { Add-Tech "Windows Server" $os.Version "os" } elseif ([int]$build -ge 22000) { Add-Tech "Windows 11" $os.Version "os" } else { Add-Tech "Windows 10" $os.Version "os" } } # -- Python packages (pip) ----------------------------------------------------- $pyCmd = $null if (Get-Command python3 -ErrorAction SilentlyContinue) { $pyCmd = "python3" } elseif (Get-Command python -ErrorAction SilentlyContinue) { $pyCmd = "python" } if ($pyCmd) { try { $pipJson = & $pyCmd -m pip list --format=json 2>$null if ($pipJson) { foreach ($p in ($pipJson | ConvertFrom-Json)) { if ($p.name -and $p.version -and $p.version -match '\d') { $results.Add(@{ name = $p.name; version = $p.version; category = "python-package" }) } } } } catch {} } # -- Node.js packages (npm global) --------------------------------------------- if (Get-Command npm -ErrorAction SilentlyContinue) { try { $npmJson = npm list -g --depth=0 --json 2>$null | ConvertFrom-Json if ($npmJson -and $npmJson.dependencies) { foreach ($name in $npmJson.dependencies.PSObject.Properties.Name) { $ver = $npmJson.dependencies.$name.version if ($ver -and $ver -match '\d') { $results.Add(@{ name = $name; version = $ver; category = "npm-package" }) } } } } catch {} } # -- Winget packages ----------------------------------------------------------- if (Get-Command winget -ErrorAction SilentlyContinue) { try { $wingetOut = winget list --accept-source-agreements 2>$null foreach ($line in ($wingetOut | Select-Object -Skip 3)) { $parts = $line -split '\s{2,}' if ($parts.Count -ge 3) { $pkgName = $parts[0].Trim() $pkgVer = $parts[2].Trim() if ($pkgName -and $pkgVer -and $pkgVer -match '\d+\.\d+') { $v = [regex]::Match($pkgVer, '\d+\.\d+(\.\d+)?').Value $results.Add(@{ name = $pkgName; version = $v; category = "system-package" }) } } } } catch {} } # -- Tache planifiee (-Yes) ---------------------------------------------------- if ($Yes -and $Send -and $Url) { # /!\ R25 (revue) : la tache planifiee ne peut etre autorisee que par une source de # jeton qu'elle pourra RELIRE a chaque declenchement. -Token (jamais recopie dans les # arguments de la tache) et une variable d'environnement de portee Process # n'existent plus quand la tache se declenche : enregistrer la tache sur cette base # produit une ingestion qui echoue chaque nuit, en silence. Sources durables : # un fichier de jeton lisible, ou une variable persistee (portee User/Machine). $_taskTokenFile = "" if ($TokenFile -and (Test-Path $TokenFile)) { $_taskTokenFile = $TokenFile } elseif (Test-Path $_defaultTokenFile) { $_taskTokenFile = $_defaultTokenFile } $_persistedEnv = [Environment]::GetEnvironmentVariable('VULNRADAR_TOKEN', 'User') if (-not $_persistedEnv) { $_persistedEnv = [Environment]::GetEnvironmentVariable('VULNRADAR_TOKEN', 'Machine') } if (-not $_taskTokenFile -and -not $_persistedEnv) { Write-Error "Aucune source de jeton durable -- tache planifiee NON creee." if ($TokenFile) { Write-Host " -TokenFile '$TokenFile' est introuvable." } elseif ($env:VULNRADAR_TOKEN) { Write-Host " `$env:VULNRADAR_TOKEN n'est definie que pour ce processus : la tache" Write-Host " planifiee ne la verra pas. Persistez-la ou creez le fichier de jeton." } elseif ($Token) { Write-Host " -Token n'est jamais recopie dans la tache planifiee (jeton en clair)." } Write-Host " Creez le fichier de jeton, puis relancez avec -Yes :" Write-Host " New-Item -ItemType Directory -Path (Split-Path '$_defaultTokenFile') -Force | Out-Null" Write-Host " Set-Content '$_defaultTokenFile' ''" exit 1 } $scriptPath = Join-Path $env:USERPROFILE "vulnradar-detect.ps1" $taskName = "VulnRadarDetect" $existing = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue if (-not $existing) { # La tache planifiee appelle le script SANS -Token : le jeton est relu depuis sa # source durable a chaque declenchement. Le jeton n'apparait PAS dans la # definition de la tache (ni dans Get-ScheduledTask, ni dans le XML de Tasks\). # En revanche le CHEMIN du fichier doit y figurer des qu'il n'est pas celui par # defaut : un chemin n'est pas un secret, et sans lui la tache chercherait # token.txt dans LOCALAPPDATA, ne le trouverait pas, et echouerait en silence. $tokenOpt = "" if ($_taskTokenFile -and $_taskTokenFile -ne $_defaultTokenFile) { $tokenOpt = " -TokenFile `"$_taskTokenFile`"" } $argStr = "-ExecutionPolicy Bypass -WindowStyle Hidden -File `"$scriptPath`" -Send -Url `"$Url`"$tokenOpt" $action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $argStr # Heure nocturne tiree au hasard (1h-5h59) pour eviter que toutes les machines # poussent a la meme minute et saturent le serveur (Maximum est exclusif). $randHour = Get-Random -Minimum 1 -Maximum 6 $randMin = Get-Random -Minimum 0 -Maximum 60 $atTime = '{0:D2}:{1:D2}' -f $randHour, $randMin $trigger = New-ScheduledTaskTrigger -Daily -At $atTime $settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Hours 1) Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger ` -Settings $settings -Force | Out-Null Write-Host "Tache planifiee creee (quotidien a $atTime - heure repartie aleatoirement)." if ($_taskTokenFile) { Write-Host " Le jeton sera lu depuis $_taskTokenFile a chaque execution." } else { Write-Host " Le jeton sera lu depuis la variable persistee VULNRADAR_TOKEN." } } else { Write-Host "Tache planifiee deja en place." } } # -- Sortie JSON ------------------------------------------------------------ $json = @($results) | ConvertTo-Json -Depth 5 if ($Send -and $Url) { # Resoudre le jeton selon la priorite (fichier > env > defaut > arg depreciee) Resolve-IngestToken if (-not $_resolvedToken) { Write-Error "Aucun jeton d'ingestion disponible. Utilisez -TokenFile, `$env:VULNRADAR_TOKEN, ou creez $($_defaultTokenFile)." exit 1 } # @($results) force un tableau JSON meme a 0 ou 1 element. # GetBytes(UTF8) evite l'encodage systeme Windows (CP1252/UTF-16) qui casse le JSON. $bodyStr = [ordered]@{ technologies = @($results) } | ConvertTo-Json -Depth 5 -Compress $bodyBytes = [System.Text.Encoding]::UTF8.GetBytes($bodyStr) try { # Le jeton est passe via le dictionnaire Headers, pas en argument de ligne de # commande visible dans ps. Invoke-RestMethod ne l'expose pas dans la liste des # processus (contrairement a curl sans --config). Invoke-RestMethod -Uri $Url -Method Post -Body $bodyBytes ` -ContentType "application/json" ` -Headers @{ "X-Ingest-Token" = $_resolvedToken } | Out-Null Write-Host "Envoye ($($results.Count) technologies)." } catch { Write-Error "Erreur lors de l'envoi : $_" exit 1 } } else { Write-Output "{`"technologies`":$json}" }