Skip to content

Java - Runtime Inventory - Dependencies (Sensor)

This page shows how to create the Java - Runtime Inventory - Dependencies sensor by hand in the Tanium Console, and how to use it with your vulnerability scanner to find and fix vulnerable Java for the long term.

What it returns: every Java runtime on each endpoint, patched or not, one row per runtime per thing that uses it. Each row gives the java.exe path, the Java line (8, 11, 17, 21, 25, ...), the version, the vendor, what installed it, the uninstall command, and the service, process, scheduled task, variable, or file association that depends on it.

Why there is no version list in this sensor

Java gets security fixes every quarter, so any list of vulnerable versions inside a sensor goes out of date within three months and then gives wrong answers without any warning. This sensor only reports facts that do not expire: what is installed, where, and what uses it. Your vulnerability scanner decides which versions are vulnerable, because it already receives new CVE data automatically. For a one-time cleanup with a built-in version list, use the Tanium sensor Java - Vulnerable Installs - Dependencies.

Build it by hand

The Tanium Console limits each platform script to 28,000 characters. The Windows script below is about 25,800 characters. Paste it exactly as shown; do not add comments or indentation.


How the sensor works

1. Find every Java runtime

A Java runtime is any folder that has bin\java.exe or bin\server\jvm.dll. The Windows script looks in:

  • The JavaSoft registry keys (where Oracle installers register Java)
  • Installed programs in Apps and Features that point at a Java folder
  • JAVA_HOME, JRE_HOME, JDK_HOME, and PATH (system and logged-on users)
  • Running java.exe, javaw.exe, javaws.exe, and jp2launcher.exe processes, wherever they are on disk
  • A folder search of C:\Program Files, C:\Program Files (x86), C:\ProgramData, other top-level folders on C:\, other fixed drives, and each user's AppData\Local\Programs, AppData\Local\JetBrains, and .jdks folders

The folder search skips cache and data folders and stops after 35 seconds. If it stops early, the sensor adds a Scan incomplete row so you know that result is partial.

The Linux script looks in /usr/lib/jvm, /usr/java, the default java on the PATH, running Java processes, JAVA_HOME in /etc/environment, /etc/profile.d, and /etc/default, and searches /opt, /usr/local, /srv, /app, /apps, /u01, and /data without crossing into network mounts. That search stops after 30 seconds in total and adds the same Scan incomplete row.

2. Read the version and who installed it

The version comes from the release file in the Java folder, or from the file version of java.exe if there is no release file. The sensor reports it two ways: Version String exactly as Java writes it (for example 1.8.0_401), and Version in a sortable form (for example 8.0.401), so Java 8 and newer lines can be compared the same way.

3. Find what uses each runtime

Used By Type How it is found
Service / Service (path in arguments) The service executable is inside the Java folder, or the Java path appears in the service command line
Service (procrun Jvm setting) Apache Commons Daemon services (Tomcat and many vendor services) store their JVM path in the registry under Procrun 2.0\<service>\Parameters\Java
Service (Java Service Wrapper .conf) / Service (WinSW .xml) The wrapper configuration file points at this Java
Service (java from PATH) The service runs a bare java command, which Windows resolves through the system PATH
Running Process A process running from this Java folder. Shows the process name, PID, owning service, and the jar name or main class
Scheduled Task / Scheduled Task (path in arguments) The task runs this java.exe, passes its path as an argument, or runs a .bat, .cmd, or .ps1 file that calls it
Environment Variable / PATH JAVA_HOME, JRE_HOME, JDK_HOME, or a PATH entry points at this Java (including the Oracle javapath folder)
Registry Default The JavaSoft registry lists this runtime as the default Java
File Association .jar files open with this Java
Service (systemd) / Service (init script) / Cron Job / Environment File / Default Java Linux equivalents: a unit file, init script, cron entry, or environment file references this Java, or /usr/bin/java points to it
... (same application folder, likely uses it) A service or process in the same application folder as a bundled Java. This is an inference, not a direct reference
None found Nothing references it. This runtime is usually the safest to remove

Command lines are never returned. For running Java processes the sensor shows only the jar file name or main class, so -D arguments (which can hold passwords) stay on the endpoint.


Before you start

  • You need a role that can create sensors in the content set you plan to use.
  • If a sensor named Java - Runtime Inventory - Dependencies already exists, edit it instead of creating a second one. Sensor names must be unique.
  • Copy each script with the copy button on its code block so nothing is changed.

Create the sensor

Step 1: Open a new sensor

  1. Sign in to the Tanium Console.
  2. Open the main menu, then go to Administration > Content > Sensors.
  3. Click Create Sensor.

Step 2: Details section

Field Value
Name Java - Runtime Inventory - Dependencies
Description Returns every Java runtime on the endpoint, patched or not, one row per runtime per thing that uses it: the java.exe path, the Java line (8, 11, 17, 21, 25, ...), the version, the vendor, what installed it, the uninstall command, and the service, process, scheduled task, variable, or file association that depends on it.
Content Set Your own custom content set (for example the one you use for lab or custom sensors)

Step 3: Settings section

Field Value
Category Miscellaneous (or any category you use for custom sensors)
Result Type Text
Max Sensor Age 1 and set the unit dropdown to Hours. Installed Java changes rarely, and the folder search takes a few seconds per endpoint
Max String Age Leave Enable unchecked
Max Strings Leave Enable unchecked
Ignore case in result values Checked
Hide this sensor from sensor lists and parse results Unchecked
Split into multiple columns Checked

Step 4: Column settings

After you check Split into multiple columns:

  1. Set Delimiter to | (the pipe character, Shift + Backslash on a US keyboard).
  2. Add these 10 columns in this order. The order must match, because the scripts return the values in this order.
Index Column Name Value Type Ignore case Hidden
0 Java Path Text Checked Unchecked
1 Java Line Numeric Checked Unchecked
2 Version Version Checked Unchecked
3 Version String Text Checked Unchecked
4 Type Text Checked Unchecked
5 Vendor Text Checked Unchecked
6 Installed By Text Checked Unchecked
7 Uninstall Command Text Checked Unchecked
8 Used By Type Text Checked Unchecked
9 Used By Text Checked Unchecked

Java Line as Numeric and Version as Version let the results grid sort them correctly (so 8.0.60 sorts before 8.0.401). Rows where the version could not be read show unknown in both columns.

Step 5: Parameters section

Leave it empty. This sensor has no parameters.

Step 6: Scripts section

For each platform in the table: click the platform tab on the left, check Enable sensor for [platform] platform, set Query Type in the top right, click inside the script editor, press Ctrl + A, press Delete, then paste the script for that platform.

Platform Enable Query Type Script
Windows Checked PowerShell Windows script below
Linux Checked UnixShell Linux script below
Mac Checked UnixShell Stub below, returns N/A
Solaris Checked UnixShell Stub below, returns N/A
AIX Checked UnixShell Stub below, returns N/A
# Java - Runtime Inventory - Dependencies (Windows)
# Returns EVERY Java runtime, one row per runtime per dependency:
# Java Path|Java Line|Version|Version String|Type|Vendor|Installed By|Uninstall Command|Used By Type|Used By
# No version list is built in; decide what is vulnerable with your scanner or Interact filters.
# Command lines are never returned; only the jar name or main class.
$ErrorActionPreference = 'SilentlyContinue'
$ScanSeconds    = 35
$ExtraScanRoots = @() # e.g. @('D:\Apps')
$sw = [Diagnostics.Stopwatch]::StartNew()
$script:ScanTruncated = $false
function Format-Field([object]$s) {
 if ($null -eq $s) { return '' }
 return ((([string]$s) -replace '[\r\n\t]+', ' ') -replace '\|', '/').Trim()
}
function Norm([string]$p) {
 if ([string]::IsNullOrWhiteSpace($p)) { return $null }
 $p = [Environment]::ExpandEnvironmentVariables($p.Trim().Trim('"').Trim())
 if ($p -notmatch '^[A-Za-z]:\\|^\\\\') { return $null }
 try { $p = [IO.Path]::GetFullPath($p) } catch { return $null }
 return $p.TrimEnd('\')
}
function Test-JavaHome([string]$h) {
 return ([IO.File]::Exists("$h\bin\java.exe") -or [IO.File]::Exists("$h\bin\server\jvm.dll") -or [IO.File]::Exists("$h\bin\client\jvm.dll"))
}
function Get-HomeFromFile([string]$file) {
 $f = Norm $file
 if (-not $f) { return $null }
 $d = [IO.Path]::GetDirectoryName($f)
 if ([IO.Path]::GetFileName($d) -imatch '^(server|client)$') { $d = [IO.Path]::GetDirectoryName($d) }
 if ([IO.Path]::GetFileName($d) -ieq 'bin') { return [IO.Path]::GetDirectoryName($d) }
 return $null
}
function Resolve-Link([string]$f) {
 try {
  $i = Get-Item -LiteralPath $f -Force -ErrorAction Stop
  if ($i.LinkType -and $i.Target) { return [string](@($i.Target)[0]) }
 } catch {}
 return $f
}
function Open-Key($hive, $view, [string]$path) {
 try { return [Microsoft.Win32.RegistryKey]::OpenBaseKey($hive, $view).OpenSubKey($path) } catch { return $null }
}
$Views = @([Microsoft.Win32.RegistryView]::Registry64, [Microsoft.Win32.RegistryView]::Registry32)
$Pf  = if ($env:ProgramW6432) { $env:ProgramW6432 } else { $env:ProgramFiles }
$Pfx = ${env:ProgramFiles(x86)}
$Pd  = $env:ProgramData
$SysDrive = $env:SystemDrive
$Sys32 = if ([IO.Directory]::Exists("$env:windir\Sysnative")) { "$env:windir\Sysnative" } else { "$env:windir\System32" }
$BadRoots = @($Pf, $Pfx, $Pd, $SysDrive, "$SysDrive\", $env:windir) | Where-Object { $_ } | ForEach-Object { $_.TrimEnd('\').ToLowerInvariant() }
$Homes = @{}
function Add-Home([string]$path) {
 $h = Norm $path
 if (-not $h) { return }
 if (-not (Test-JavaHome $h)) { return }
 if ([IO.Path]::GetFileName($h) -ieq 'jre') {
  $parent = [IO.Path]::GetDirectoryName($h)
  if ([IO.File]::Exists("$parent\bin\javac.exe") -or [IO.File]::Exists("$parent\release")) { $h = $parent }
 }
 $k = $h.ToLowerInvariant()
 if (-not $Homes.ContainsKey($k)) { $Homes[$k] = $h }
}
$Services = @(Get-CimInstance -ClassName Win32_Service -ErrorAction SilentlyContinue)
$Procs    = @(Get-CimInstance -ClassName Win32_Process -ErrorAction SilentlyContinue)
$Apps = New-Object System.Collections.Generic.List[object]
function Read-Uninstall($baseKey) {
 if (-not $baseKey) { return }
 foreach ($n in $baseKey.GetSubKeyNames()) {
  $k = $baseKey.OpenSubKey($n)
  if (-not $k) { continue }
  $dn = [string]$k.GetValue('DisplayName')
  if (-not $dn) { continue }
  $Apps.Add([pscustomobject]@{
   Name      = $dn
   Version   = [string]$k.GetValue('DisplayVersion')
   Location  = (Norm ([string]$k.GetValue('InstallLocation')))
   Uninstall = [string]$k.GetValue('UninstallString')
   Quiet     = [string]$k.GetValue('QuietUninstallString')
   Key       = $n
  })
 }
}
foreach ($v in $Views) { Read-Uninstall (Open-Key 'LocalMachine' $v 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall') }
$Hku = $null
try { $Hku = [Microsoft.Win32.RegistryKey]::OpenBaseKey('Users', 'Default') } catch {}
$UserSids = @()
if ($Hku) { $UserSids = @($Hku.GetSubKeyNames() | Where-Object { $_ -match '^S-1-5-21-[\d-]+$' }) }
foreach ($sid in $UserSids) { Read-Uninstall ($Hku.OpenSubKey("$sid\Software\Microsoft\Windows\CurrentVersion\Uninstall")) }
function Get-UserName([string]$sid) {
 $k = Open-Key 'LocalMachine' 'Registry64' "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$sid"
 if ($k) { $p = [string]$k.GetValue('ProfileImagePath'); if ($p) { return [IO.Path]::GetFileName($p) } }
 return $sid
}
$Unloaded = @()
$plk = Open-Key 'LocalMachine' 'Registry64' 'SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList'
if ($plk) {
 foreach ($sid in $plk.GetSubKeyNames()) {
  if ($sid -notmatch '^S-1-5-21-' -or $UserSids -contains $sid) { continue }
  $pp = Norm ([string]$plk.OpenSubKey($sid).GetValue('ProfileImagePath'))
  if ($pp) { $Unloaded += $pp.ToLowerInvariant() }
 }
}
$JavaSoftDefaults = New-Object System.Collections.Generic.List[object]
foreach ($v in $Views) {
 $bits = if ($v -eq 'Registry32') { '32-bit' } else { '64-bit' }
 foreach ($prod in 'Java Runtime Environment', 'Java Development Kit', 'JRE', 'JDK') {
  $k = Open-Key 'LocalMachine' $v "SOFTWARE\JavaSoft\$prod"
  if (-not $k) { continue }
  foreach ($sub in $k.GetSubKeyNames()) {
   $sk = $k.OpenSubKey($sub)
   if ($sk) { Add-Home ([string]$sk.GetValue('JavaHome')) }
  }
  $cur = [string]$k.GetValue('CurrentVersion')
  if ($cur) {
   $ck = $k.OpenSubKey($cur)
   if ($ck) {
    $jh = Norm ([string]$ck.GetValue('JavaHome'))
    if ($jh) { $JavaSoftDefaults.Add([pscustomobject]@{ Home = $jh; Label = "Registered default $prod $cur ($bits JavaSoft registry)" }) }
   }
  }
 }
}
foreach ($a in $Apps) { if ($a.Location) { Add-Home $a.Location } }
$EnvSources = New-Object System.Collections.Generic.List[object]
$mk = Open-Key 'LocalMachine' 'Registry64' 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment'
if ($mk) {
 foreach ($n in $mk.GetValueNames()) { $EnvSources.Add([pscustomobject]@{ Scope = 'system'; Name = $n; Value = [string]$mk.GetValue($n) }) }
}
foreach ($sid in $UserSids) {
 $uk = $Hku.OpenSubKey("$sid\Environment")
 if ($uk) {
  $un = Get-UserName $sid
  foreach ($n in $uk.GetValueNames()) { $EnvSources.Add([pscustomobject]@{ Scope = "user $un"; Name = $n; Value = [string]$uk.GetValue($n) }) }
 }
}
$SystemJavaHome = $null
foreach ($e in $EnvSources) {
 if ($e.Name -imatch '^(JAVA_HOME|JRE_HOME|JDK_HOME)$') {
  Add-Home $e.Value
  if ($e.Scope -eq 'system' -and $e.Name -ieq 'JAVA_HOME') { $SystemJavaHome = Norm $e.Value }
 }
 elseif ($e.Name -ieq 'Path') {
  foreach ($entry in ($e.Value -split ';')) {
   $d = Norm $entry
   if ($d -and [IO.File]::Exists("$d\java.exe")) { Add-Home (Get-HomeFromFile (Resolve-Link "$d\java.exe")) }
  }
 }
}
$PathJavaHome = $null
foreach ($entry in ([Environment]::GetEnvironmentVariable('Path', 'Machine') -split ';')) {
 $d = Norm $entry
 if ($d -and [IO.File]::Exists("$d\java.exe")) { $PathJavaHome = Get-HomeFromFile (Resolve-Link "$d\java.exe"); break }
}
foreach ($p in $Procs) {
 if ($p.ExecutablePath -and $p.Name -imatch '^(java|javaw|javaws|jp2launcher)\.exe$') { Add-Home (Get-HomeFromFile $p.ExecutablePath) }
}
$SkipDirs = @('node_modules', 'WindowsApps', '.git', 'WinSxS', 'Package Cache', 'Installer', 'Windows Defender',
 'Windows Defender Advanced Threat Protection', 'Microsoft Office', 'Windows Kits', 'Reference Assemblies',
 '$Recycle.Bin', 'System Volume Information', 'Temporary Internet Files', 'INetCache',
 'queue', 'diff', 'cache', '.cache', 'logs', 'log', 'temp', 'tmp', '__pycache__', 'site-packages')
function Search-JavaHomes([string]$root, [int]$maxDepth, [string[]]$extraSkip) {
 if (-not $root -or -not [IO.Directory]::Exists($root)) { return }
 $stack = New-Object System.Collections.Stack
 $stack.Push(@($root, 0))
 while ($stack.Count -gt 0) {
  if ($sw.Elapsed.TotalSeconds -gt $ScanSeconds) { $script:ScanTruncated = $true; return }
  $item = $stack.Pop(); $dir = [string]$item[0]; $depth = [int]$item[1]
  if ([IO.Path]::GetFileName($dir) -ieq 'bin') {
   if ([IO.File]::Exists("$dir\java.exe") -or [IO.File]::Exists("$dir\server\jvm.dll") -or [IO.File]::Exists("$dir\client\jvm.dll")) {
    Add-Home ([IO.Path]::GetDirectoryName($dir))
    continue
   }
  }
  if ($depth -ge $maxDepth) { continue }
  $subs = $null
  try { $subs = [IO.Directory]::GetDirectories($dir) } catch { continue }
  if ($subs.Count -gt 300) { continue }
  foreach ($s in $subs) {
   $n = [IO.Path]::GetFileName($s)
   if ($SkipDirs -contains $n) { continue }
   if ($extraSkip -and ($extraSkip -contains $n)) { continue }
   try { if (([IO.File]::GetAttributes($s) -band [IO.FileAttributes]::ReparsePoint) -ne 0) { continue } } catch { continue }
   $stack.Push(@($s, ($depth + 1)))
  }
 }
}
foreach ($r in $ExtraScanRoots) { Search-JavaHomes $r 7 $null }
Search-JavaHomes $Pf 7 $null
if ($Pfx -and $Pfx -ne $Pf) { Search-JavaHomes $Pfx 7 $null }
Search-JavaHomes $Pd 5 @('Microsoft')
$TopSkip = @('Windows', 'Program Files', 'Program Files (x86)', 'ProgramData', 'Users', 'Recovery', 'PerfLogs',
 'Config.Msi', 'Documents and Settings', 'MSOCache', '$WinREAgent', '$SysReset', '$Windows.~BT', '$Windows.~WS',
 '$Recycle.Bin', 'System Volume Information')
foreach ($d in @([IO.Directory]::GetDirectories("$SysDrive\"))) {
 if ($TopSkip -contains [IO.Path]::GetFileName($d)) { continue }
 try { if (([IO.File]::GetAttributes($d) -band [IO.FileAttributes]::ReparsePoint) -ne 0) { continue } } catch { continue }
 Search-JavaHomes $d 6 $null
}
foreach ($drv in [IO.DriveInfo]::GetDrives()) {
 if ($drv.DriveType -ne 'Fixed' -or -not $drv.IsReady) { continue }
 if ($drv.Name.TrimEnd('\') -ieq $SysDrive) { continue }
 Search-JavaHomes $drv.RootDirectory.FullName 6 $null
}
foreach ($u in @([IO.Directory]::GetDirectories("$SysDrive\Users"))) {
 try { if (([IO.File]::GetAttributes($u) -band [IO.FileAttributes]::ReparsePoint) -ne 0) { continue } } catch { continue }
 Search-JavaHomes "$u\AppData\Local\Programs" 5 $null
 Search-JavaHomes "$u\AppData\Local\JetBrains" 6 $null
 Search-JavaHomes "$u\.jdks" 2 $null
}
function Convert-FileVer([string]$fv) {
 if ($fv -notmatch '^(\d+)\.(\d+)\.(\d+)') { return $null }
 $maj = [int]$matches[1]; $min = [int]$matches[2]; $p = [int]$matches[3]
 if ($maj -le 8) { return [version]("$maj.0." + [int][math]::Floor($p / 10)) }
 return [version]("$maj.$min.$p")
}
function Format-JavaVer([version]$v) {
 if (-not $v) { return 'unknown' }
 if ($v.Major -le 8) { return "$($v.Major)u$($v.Build)" }
 return "$($v.Major).$($v.Minor).$($v.Build)"
}
function Get-JavaInfo([string]$h) {
 $jv = $null; $impl = $null
 if ([IO.File]::Exists("$h\release")) {
  foreach ($line in [IO.File]::ReadAllLines("$h\release")) {
   if ($line -match '^JAVA_VERSION="?([^"]+)"?') { $jv = $matches[1] }
   elseif ($line -match '^IMPLEMENTOR="?([^"]+)"?') { $impl = $matches[1] }
  }
 }
 $file = if ([IO.File]::Exists("$h\bin\java.exe")) { "$h\bin\java.exe" } elseif ([IO.File]::Exists("$h\bin\server\jvm.dll")) { "$h\bin\server\jvm.dll" } else { "$h\bin\client\jvm.dll" }
 $fvi = $null
 try { $fvi = [Diagnostics.FileVersionInfo]::GetVersionInfo($file) } catch {}
 $v = $null
 if ($jv) {
  if ($jv -match '^1\.(\d+)\.\d+(?:_(\d+))?') { $v = [version]("$($matches[1]).0.$([int]$matches[2])") }
  elseif ($jv -match '^(\d+)(?:\.(\d+))?(?:\.(\d+))?') { $v = [version]("$($matches[1]).$([int]$matches[2]).$([int]$matches[3])") }
 }
 if (-not $v -and $fvi) {
  $v = Convert-FileVer $fvi.ProductVersion
  if (-not $v) { $v = Convert-FileVer $fvi.FileVersion }
 }
 if (-not $impl -and $fvi) { $impl = $fvi.CompanyName }
 $display = if ($jv) { $jv } else { Format-JavaVer $v }
 $type = if ([IO.File]::Exists("$h\bin\javac.exe")) { 'JDK' } else { 'JRE' }
 return [pscustomobject]@{ File = $file; Version = $v; Display = $display; Vendor = $impl; Type = $type }
}
$JavaNamePattern = '(?i)\bjava\b|\bjdk\b|\bjre\b|development kit|openjdk|temurin|adoptium|corretto|zulu|liberica|semeru|graalvm|sapmachine|dragonwell'
function Get-UninstallCmd($a) {
 $u = if ($a.Quiet) { $a.Quiet } else { $a.Uninstall }
 if ($u -match '(?i)msiexec(?:\.exe)?"?\s+/[IX]\s*(\{[0-9A-F-]{36}\})') { return "MsiExec.exe /X$($matches[1]) /qn /norestart" }
 if ($u -match '(?i)msiexec' -and $a.Key -match '^\{[0-9A-Fa-f-]{36}\}$') { return "MsiExec.exe /X$($a.Key) /qn /norestart" }
 return $u
}
function Get-Owner([string]$h, $info) {
 $hl = $h.ToLowerInvariant()
 $best = $null
 foreach ($a in $Apps) {
  if (-not $a.Location) { continue }
  $l = $a.Location.ToLowerInvariant()
  if ($BadRoots -contains $l) { continue }
  if ($hl -eq $l -or $hl.StartsWith($l + '\')) {
   if (-not $best -or $a.Location.Length -gt $best.Location.Length) { $best = $a }
  }
 }
 if ($best) {
  if ($best.Name -match $JavaNamePattern -and ($best.Location.ToLowerInvariant() -eq $hl -or $hl.StartsWith($best.Location.ToLowerInvariant() + '\jre'))) {
   return [pscustomobject]@{ Kind = 'Standalone'; By = "$($best.Name) $($best.Version)".Trim(); Uninstall = (Get-UninstallCmd $best); Root = $null }
  }
  return [pscustomobject]@{ Kind = 'Bundled'; By = "Bundled with $($best.Name) $($best.Version)".Trim(); Uninstall = "Update or remove $($best.Name)"; Root = $best.Location }
 }
 $leaf = [IO.Path]::GetFileName($h)
 if ($info.Version -and ($leaf -match '(?i)^(jre|jdk)' -or $h -match '(?i)\\java\\')) {
  $is32 = ($Pfx -and $hl.StartsWith($Pfx.ToLowerInvariant() + '\'))
  foreach ($a in $Apps) {
   if ($a.Name -notmatch $JavaNamePattern) { continue }
   $av = Convert-FileVer $a.Version
   if (-not $av -or $av -ne $info.Version) { continue }
   $aIsJdk = ($a.Name -match '(?i)development kit|\bjdk\b')
   if ($aIsJdk -ne ($info.Type -eq 'JDK')) { continue }
   if ($is32 -and $a.Name -match '64-bit') { continue }
   if (-not $is32 -and $a.Name -match '32-bit') { continue }
   return [pscustomobject]@{ Kind = 'Standalone'; By = "$($a.Name) $($a.Version)".Trim(); Uninstall = (Get-UninstallCmd $a); Root = $null }
  }
 }
 foreach ($u in $Unloaded) {
  if ($hl.StartsWith($u + '\')) { return [pscustomobject]@{ Kind = 'Unknown'; By = 'Unknown (user profile not logged on)'; Uninstall = 'Unknown (user profile not logged on)'; Root = $null } }
 }
 $root = $null
 $parent = [IO.Path]::GetDirectoryName($h)
 if ($leaf -match '(?i)^(jre|jdk|jbr|java|runtime|jvm|jre64|jre32|openjdk)$' -and $parent -and ($BadRoots -notcontains $parent.ToLowerInvariant()) -and ([IO.Path]::GetFileName($parent) -notmatch '(?i)^java$|adoptium|zulu|corretto|openjdk|jdks$')) { $root = $parent }
 $by = if ($root) { "No installer entry (inside $root)" } else { 'No installer entry found' }
 return [pscustomobject]@{ Kind = 'Unknown'; By = $by; Uninstall = 'None registered'; Root = $root }
}
$Runtimes = @{}
foreach ($k in $Homes.Keys) {
 $h = $Homes[$k]
 $info = Get-JavaInfo $h
 $owner = Get-Owner $h $info
 $javaPath = if ([IO.File]::Exists("$h\bin\java.exe")) { "$h\bin\java.exe" } else { $info.File }
 $Runtimes[$k] = [pscustomobject]@{ Home = $h; Path = $javaPath; Info = $info; Owner = $owner }
}
$Deps = New-Object System.Collections.Generic.List[object]
$DepSeen = @{}
function Add-Dep([string]$key, [string]$type, [string]$detail) {
 if (-not $key) { return }
 $id = "$key|$type|$detail".ToLowerInvariant()
 if ($DepSeen.ContainsKey($id)) { return }
 $DepSeen[$id] = $true
 $Deps.Add([pscustomobject]@{ Key = $key; Type = $type; Detail = $detail })
}
function Get-RuntimeKey([string]$path) {
 $p = Norm $path
 if (-not $p) { return $null }
 $pl = $p.ToLowerInvariant()
 $best = $null
 foreach ($k in $Runtimes.Keys) {
  if ($pl -eq $k -or $pl.StartsWith($k + '\')) { if (-not $best -or $k.Length -gt $best.Length) { $best = $k } }
 }
 return $best
}
function Get-RuntimeKeyByRoot([string]$path) {
 $p = Norm $path
 if (-not $p) { return $null }
 $pl = $p.ToLowerInvariant()
 foreach ($k in $Runtimes.Keys) {
  $r = $Runtimes[$k].Owner.Root
  if ($r -and $pl.StartsWith($r.ToLowerInvariant() + '\')) { return $k }
 }
 return $null
}
function Get-RuntimeKeysInText([string]$text) {
 $out = @()
 if (-not $text) { return $out }
 $t = [Environment]::ExpandEnvironmentVariables($text).ToLowerInvariant()
 foreach ($k in $Runtimes.Keys) { if ($t.Contains($k)) { $out += $k } }
 return $out
}
function Get-ExeFromCommand([string]$cmd) {
 if (-not $cmd) { return $null }
 $c = [Environment]::ExpandEnvironmentVariables($cmd.Trim())
 if ($c.StartsWith('"')) { $e = $c.Substring(1); $i = $e.IndexOf('"'); if ($i -gt 0) { return $e.Substring(0, $i) }; return $e }
 if ($c -match '^(.+?\.(exe|bat|cmd|com))(\s|$)') { return $matches[1] }
 return ($c -split '\s+')[0]
}
function Test-BareJava([string]$exe) { return ($exe -and $exe -notmatch '[\\/]' -and $exe -imatch '^javaw?(\.exe)?$') }
$PathKey = if ($PathJavaHome) { Get-RuntimeKey $PathJavaHome } else { $null }
$JavaHomeKey = if ($SystemJavaHome) { Get-RuntimeKey $SystemJavaHome } else { $null }
function Get-JavaAppHint([string]$cmd) {
 if (-not $cmd) { return '' }
 $tokens = @([regex]::Matches($cmd, '"[^"]*"|\S+') | ForEach-Object { $_.Value.Trim('"') })
 for ($i = 1; $i -lt $tokens.Count; $i++) {
  $t = $tokens[$i]
  if ($t -ieq '-jar') { if ($i + 1 -lt $tokens.Count) { return 'jar ' + [IO.Path]::GetFileName($tokens[$i + 1]) }; return '' }
  if ($t -imatch '^(-m|--module)$') { if ($i + 1 -lt $tokens.Count) { return 'module ' + $tokens[$i + 1] }; return '' }
  if ($t -imatch '^(-cp|-classpath|--class-path|-p|--module-path|--add-opens|--add-exports|--add-modules)$') { $i++; continue }
  if ($t.StartsWith('-') -or $t.StartsWith('@')) { continue }
  if ($t -eq 'org.apache.catalina.startup.Bootstrap') { return 'Apache Tomcat (org.apache.catalina.startup.Bootstrap)' }
  return 'class ' + $t
 }
 return ''
}
function Get-RunAs([string]$id) {
 switch ($id) { 'S-1-5-18' { 'SYSTEM' } 'S-1-5-19' { 'LOCAL SERVICE' } 'S-1-5-20' { 'NETWORK SERVICE' } default { $id } }
}
if ($Runtimes.Count -gt 0) {
 foreach ($s in $Services) {
  $label = "$($s.DisplayName) [$($s.Name)] $($s.State), $($s.StartMode)"
  $rawExe = Get-ExeFromCommand $s.PathName
  $key = $null; $how = 'Service'
  if (Test-BareJava $rawExe) { $key = $PathKey; $how = 'Service (java from PATH)' }
  elseif ($rawExe) { $key = Get-RuntimeKey $rawExe }
  if (-not $key) { $hits = @(Get-RuntimeKeysInText $s.PathName); if ($hits.Count) { $key = $hits[0]; $how = 'Service (path in arguments)' } }
  if (-not $key) {
   foreach ($v in $Views) {
    $pk = Open-Key 'LocalMachine' $v "SOFTWARE\Apache Software Foundation\Procrun 2.0\$($s.Name)\Parameters\Java"
    if (-not $pk) { continue }
    $jvm = [string]$pk.GetValue('Jvm')
    if ($jvm -ieq 'auto' -or -not $jvm) {
     foreach ($d in $JavaSoftDefaults) { $kk = Get-RuntimeKey $d.Home; if ($kk) { $key = $kk; $how = 'Service (procrun, Jvm=auto uses registered default)'; break } }
    }
    else { $key = Get-RuntimeKey (Get-HomeFromFile $jvm); if ($key) { $how = 'Service (procrun Jvm setting)' } }
    if ($key) { break }
   }
  }
  if (-not $key -and $rawExe -and [IO.Path]::GetFileName($rawExe) -imatch 'wrapper' -and $s.PathName -match '"([^"]+\.conf)"|(\S+\.conf)') {
   $conf = if ($matches[1]) { $matches[1] } else { $matches[2] }
   if (-not [IO.Path]::IsPathRooted($conf)) { $conf = Join-Path ([IO.Path]::GetDirectoryName($rawExe)) $conf }
   if ([IO.File]::Exists($conf)) {
    $line = [IO.File]::ReadAllLines($conf) | Where-Object { $_ -match '^\s*wrapper\.java\.command\s*=' } | Select-Object -First 1
    if ($line) {
     $jc = ($line -split '=', 2)[1].Trim()
     if (Test-BareJava $jc) { $key = $PathKey } else { $key = Get-RuntimeKey $jc }
     if ($key) { $how = 'Service (Java Service Wrapper .conf)' }
    }
   }
  }
  if (-not $key -and $rawExe) {
   $xmlPath = [IO.Path]::ChangeExtension($rawExe, '.xml')
   if ([IO.File]::Exists($xmlPath)) {
    $xt = [IO.File]::ReadAllText($xmlPath)
    if ($xt -match '<executable>\s*([^<]+?)\s*</executable>') {
     $jc = $matches[1] -replace '(?i)%BASE%', [IO.Path]::GetDirectoryName($rawExe)
     if (Test-BareJava $jc) { $key = $PathKey } else { $key = Get-RuntimeKey $jc }
     if ($key) { $how = 'Service (WinSW .xml)' }
    }
   }
  }
  if ($key) { Add-Dep $key $how $label; continue }
  if ($rawExe -and $rawExe -match '[\\/]') {
   $rk = Get-RuntimeKeyByRoot $rawExe
   if ($rk) { Add-Dep $rk 'Service (same application folder, likely uses it)' $label }
  }
 }
 $SvcByPid = @{}
 foreach ($s in $Services) { if ($s.ProcessId) { $SvcByPid[[int]$s.ProcessId] = $s.Name } }
 foreach ($p in $Procs) {
  if (-not $p.ExecutablePath) { continue }
  $svc = ''
  if ($SvcByPid.ContainsKey([int]$p.ProcessId)) { $svc = " (service $($SvcByPid[[int]$p.ProcessId]))" }
  elseif ($SvcByPid.ContainsKey([int]$p.ParentProcessId)) { $svc = " (started by service $($SvcByPid[[int]$p.ParentProcessId]))" }
  $key = Get-RuntimeKey $p.ExecutablePath
  if ($key) {
   $hint = Get-JavaAppHint $p.CommandLine
   $detail = "$($p.Name) PID $($p.ProcessId)$svc"
   if ($hint) { $detail += " - $hint" }
   Add-Dep $key 'Running Process' $detail
   continue
  }
  $rk = Get-RuntimeKeyByRoot $p.ExecutablePath
  if ($rk) { Add-Dep $rk 'Running Process (same application folder, likely uses it)' "$($p.Name) PID $($p.ProcessId)$svc" }
 }
 $TaskRoot = "$Sys32\Tasks"
 $taskFiles = New-Object System.Collections.Generic.List[string]
 $tstack = New-Object System.Collections.Stack
 if ([IO.Directory]::Exists($TaskRoot)) { $tstack.Push($TaskRoot) }
 while ($tstack.Count -gt 0) {
  $d = [string]$tstack.Pop()
  try { foreach ($f in [IO.Directory]::GetFiles($d)) { $taskFiles.Add($f) } } catch {}
  try { foreach ($sd in [IO.Directory]::GetDirectories($d)) { $tstack.Push($sd) } } catch {}
 }
 foreach ($tf in $taskFiles) {
  $x = New-Object Xml.XmlDocument
  try { $x.Load($tf) } catch { continue }
  $ns = New-Object Xml.XmlNamespaceManager($x.NameTable)
  $ns.AddNamespace('t', 'http://schemas.microsoft.com/windows/2004/02/mit/task')
  $execs = $x.SelectNodes('//t:Actions/t:Exec', $ns)
  if (-not $execs -or $execs.Count -eq 0) { continue }
  $en = $x.SelectSingleNode('//t:Settings/t:Enabled', $ns)
  $state = if ($en -and $en.InnerText -eq 'false') { 'Disabled' } else { 'Enabled' }
  $uid = $x.SelectSingleNode('//t:Principals/t:Principal/t:UserId', $ns)
  $runAs = if ($uid) { Get-RunAs $uid.InnerText } else { 'unknown' }
  $taskName = $tf.Substring($TaskRoot.Length)
  $label = "$taskName - runs as $runAs, $state"
  foreach ($e in $execs) {
   $cn = $e.SelectSingleNode('t:Command', $ns); $an = $e.SelectSingleNode('t:Arguments', $ns)
   $cmd = if ($cn) { $cn.InnerText.Trim().Trim('"') } else { '' }
   $arg = if ($an) { $an.InnerText } else { '' }
   $key = $null; $how = 'Scheduled Task'
   if (Test-BareJava $cmd) { $key = $PathKey; $how = 'Scheduled Task (java from PATH)' }
   elseif ($cmd) { $key = Get-RuntimeKey $cmd }
   if (-not $key) { $hits = @(Get-RuntimeKeysInText $arg); if ($hits.Count) { $key = $hits[0]; $how = 'Scheduled Task (path in arguments)' } }
   if (-not $key) {
    $script = $null
    if ($cmd -match '(?i)\.(bat|cmd|ps1)$') { $script = $cmd }
    elseif ($arg -match '(?i)"?([A-Za-z]:\\[^"]+?\.(bat|cmd|ps1))"?') { $script = $matches[1] }
    $script = if ($script) { [Environment]::ExpandEnvironmentVariables($script) } else { $null }
    if ($script -and [IO.File]::Exists($script) -and (Get-Item -LiteralPath $script).Length -lt 262144) {
     $txt = [IO.File]::ReadAllText($script)
     $hits = @(Get-RuntimeKeysInText $txt)
     if ($hits.Count) { $key = $hits[0] }
     elseif ($txt -match '(?i)%JAVA_HOME%|\$env:JAVA_HOME') { $key = $JavaHomeKey }
     elseif ($txt -match '(?im)(^|[\s"&(])javaw?(\.exe)?\s') { $key = $PathKey }
     if ($key) { $how = "Scheduled Task (via script $([IO.Path]::GetFileName($script)))" }
    }
   }
   if ($key) { Add-Dep $key $how $label }
  }
 }
 foreach ($e in $EnvSources) {
  if ($e.Name -imatch '^(JAVA_HOME|JRE_HOME|JDK_HOME)$') {
   $key = Get-RuntimeKey $e.Value
   if ($key) { Add-Dep $key 'Environment Variable' "$($e.Name.ToUpper()) ($($e.Scope))" }
  }
  elseif ($e.Name -ieq 'Path') {
   $i = 0
   foreach ($entry in ($e.Value -split ';')) {
    $i++
    $d = Norm $entry
    if (-not $d) { continue }
    $key = Get-RuntimeKey $d
    if ($key) { Add-Dep $key 'PATH' "PATH ($($e.Scope)) entry $i"; continue }
    if ([IO.File]::Exists("$d\java.exe")) {
     $t = Resolve-Link "$d\java.exe"
     if ($t -ne "$d\java.exe") { $key = Get-RuntimeKey $t; if ($key) { Add-Dep $key 'PATH' "PATH ($($e.Scope)) entry $i via $d" } }
    }
   }
  }
 }
 foreach ($d in $JavaSoftDefaults) { $key = Get-RuntimeKey $d.Home; if ($key) { Add-Dep $key 'Registry Default' $d.Label } }
 foreach ($v in $Views) {
  $jk = Open-Key 'LocalMachine' $v 'SOFTWARE\Classes\jarfile\shell\open\command'
  if (-not $jk) { continue }
  $jexe = Get-ExeFromCommand ([string]$jk.GetValue(''))
  if ($jexe) { $key = Get-RuntimeKey $jexe; if ($key) { Add-Dep $key 'File Association' '.jar files open with this Java' } }
 }
}
$rows = New-Object System.Collections.Generic.List[string]
foreach ($k in $Runtimes.Keys) {
 $r = $Runtimes[$k]
 $v = $r.Info.Version
 $line = if ($v) { [string]$v.Major } else { 'unknown' }
 $norm = if ($v) { "$($v.Major).$($v.Minor).$($v.Build)" } else { 'unknown' }
 $base = @($r.Path, $line, $norm, $r.Info.Display, $r.Info.Type, $r.Info.Vendor, $r.Owner.By, $r.Owner.Uninstall) | ForEach-Object { Format-Field $_ }
 $mine = @($Deps | Where-Object { $_.Key -eq $k })
 if ($mine.Count -eq 0) {
  $rows.Add((($base + @('None found', 'No service, process, scheduled task, variable, or file association references it')) -join '|'))
 }
 else {
  foreach ($d in $mine) { $rows.Add((($base + @((Format-Field $d.Type), (Format-Field $d.Detail))) -join '|')) }
 }
}
$rows | Sort-Object | Write-Output
if ($script:ScanTruncated) {
 Write-Output ((@('Scan incomplete', '', '', '', '', '', '', '', 'Scan incomplete', "Folder search stopped after $ScanSeconds seconds. Some folders were not checked.")) -join '|')
}
#!/bin/bash
# =====================================================================
# Java - Runtime Inventory - Dependencies (Linux)
#
# Finds every Java runtime (JDK or JRE, package or tarball, standalone
# or bundled) and returns ALL of them, with what uses each one.
# No version list is built in: decide which versions are vulnerable with
# your vulnerability scanner or Interact filters on Java Line and Version.
#
# One row per Java runtime per dependency:
#   Java Path | Java Line | Version | Version String | Type | Vendor |
#   Installed By | Uninstall Command | Used By Type | Used By
#
# Command lines are never returned in full: only the jar name or main
# class is shown.
# =====================================================================
SCAN_SECONDS=30
EXTRA_SCAN_ROOTS=""     # optional, space separated, e.g. "/data/apps"

PATH=/usr/sbin:/usr/bin:/sbin:/bin:$PATH
export LC_ALL=C
WORK=$(mktemp -d 2>/dev/null || { mkdir -p "/tmp/javavuln.$$"; echo "/tmp/javavuln.$$"; })
trap 'rm -rf "$WORK"' EXIT
HOMES="$WORK/homes"; RUNTIMES="$WORK/vuln"; DEPS="$WORK/deps"; REFS="$WORK/refs"
: > "$HOMES"; : > "$RUNTIMES"; : > "$DEPS"; : > "$REFS"

clean() { printf '%s' "$1" | tr '|\t\r\n' '/   '; }

add_home() {
    # $1 = a java binary or a Java home directory
    local p h parent
    [ -n "$1" ] || return 0
    p=$(readlink -f "$1" 2>/dev/null) || return 0
    if [ -f "$p" ]; then h=$(dirname "$(dirname "$p")"); else h="$p"; fi
    [ -x "$h/bin/java" ] || return 0
    case "$h" in
        */jre)
            parent=$(dirname "$h")
            if [ -x "$parent/bin/javac" ] || [ -f "$parent/release" ]; then h="$parent"; fi ;;
    esac
    echo "$h" >> "$HOMES"
}

# ---------- discovery ----------
for f in /usr/lib/jvm/*/bin/java /usr/lib/jvm/*/jre/bin/java /usr/java/*/bin/java /usr/java/*/jre/bin/java \
         /usr/lib64/jvm/*/bin/java /usr/local/java/*/bin/java /usr/local/jdk*/bin/java; do
    [ -x "$f" ] && add_home "$f"
done
command -v java >/dev/null 2>&1 && add_home "$(command -v java)"
[ -e /etc/alternatives/java ] && add_home /etc/alternatives/java
DEFAULT_HOME=""
if command -v java >/dev/null 2>&1; then
    d=$(readlink -f "$(command -v java)" 2>/dev/null)
    [ -n "$d" ] && DEFAULT_HOME=$(dirname "$(dirname "$d")")
    case "$DEFAULT_HOME" in */jre) [ -x "$(dirname "$DEFAULT_HOME")/bin/javac" ] && DEFAULT_HOME=$(dirname "$DEFAULT_HOME");; esac
fi

# Running Java processes
for pd in /proc/[0-9]*; do
    exe=$(readlink "$pd/exe" 2>/dev/null) || continue
    case "$exe" in */bin/java) add_home "$exe" ;; esac
done

# JAVA_HOME in system environment files
for f in /etc/environment /etc/profile /etc/profile.d/*.sh /etc/default/*; do
    [ -f "$f" ] || continue
    grep -hE '^[[:space:]]*(export[[:space:]]+)?(JAVA_HOME|JRE_HOME)=' "$f" 2>/dev/null |
        sed -E 's/^[^=]*=//; s/["'\'']//g; s/[[:space:]].*$//' | while read -r v; do add_home "$v"; done
done

# Folder search (stays on each folder's own file system, skips network mounts)
# SCAN_SECONDS is the total for all folders, not per folder.
ROOTS="/opt /usr/local /srv /app /apps /u01 /data $EXTRA_SCAN_ROOTS"
START=$(date +%s)
for r in $ROOTS; do
    [ -d "$r" ] || continue
    left=$((SCAN_SECONDS - ($(date +%s) - START)))
    [ "$left" -gt 0 ] || { touch "$WORK/truncated"; break; }
    timeout "$left" find "$r" -xdev -maxdepth 7 -type f -name java -path '*/bin/java' 2>/dev/null
    [ $? -eq 124 ] && touch "$WORK/truncated"
done | while read -r f; do add_home "$f"; done

sort -u "$HOMES" -o "$HOMES"

# ---------- version and vendor ----------
pkg_owner() {  # prints "package|remove command" or nothing
    local f="$1" out
    if command -v rpm >/dev/null 2>&1; then
        out=$(rpm -qf --qf '%{NAME}\n' "$f" 2>/dev/null | head -1)
        case "$out" in ""|*"not owned"*|*"No such"*) ;; *)
            if command -v dnf >/dev/null 2>&1; then echo "$out|dnf remove $out"; else echo "$out|yum remove $out"; fi; return ;; esac
    fi
    if command -v dpkg >/dev/null 2>&1; then
        out=$(dpkg -S "$f" 2>/dev/null | head -1 | cut -d: -f1)
        [ -n "$out" ] && { echo "$out|apt-get remove $out"; return; }
    fi
}

while read -r h; do
    [ -n "$h" ] || continue
    jv=""; impl=""
    if [ -f "$h/release" ]; then
        jv=$(sed -n 's/^JAVA_VERSION="\{0,1\}\([^"]*\)"\{0,1\}.*/\1/p' "$h/release" | head -1)
        impl=$(sed -n 's/^IMPLEMENTOR="\{0,1\}\([^"]*\)"\{0,1\}.*/\1/p' "$h/release" | head -1)
    fi
    if [ -z "$jv" ]; then
        jv=$(timeout 10 "$h/bin/java" -version 2>&1 | sed -n 's/.*version "\([^"]*\)".*/\1/p' | head -1)
    fi
    if [ -z "$impl" ]; then
        case "$(timeout 10 "$h/bin/java" -version 2>&1 | sed -n '2p')" in
            *"Java(TM)"*) impl="Oracle Corporation" ;;
            *OpenJDK*) impl="OpenJDK" ;;
            *IBM*) impl="IBM" ;;
            *) impl="unknown" ;;
        esac
    fi
    maj=""; min=0; pat=0
    case "$jv" in
        1.*) maj=$(echo "$jv" | cut -d. -f2 | tr -cd '0-9')
             pat=$(echo "$jv" | sed -n 's/^[^_]*_\([0-9]*\).*/\1/p') ;;
        [0-9]*) maj=$(echo "$jv" | cut -d. -f1 | tr -cd '0-9')
             min=$(echo "$jv" | cut -s -d. -f2 | sed 's/[^0-9].*//')
             pat=$(echo "$jv" | cut -s -d. -f3 | sed 's/[^0-9].*//') ;;
    esac
    [ -z "$min" ] && min=0; [ -z "$pat" ] && pat=0
    if [ -n "$maj" ]; then line="$maj"; norm="$maj.$min.$pat"; else line=unknown; norm=unknown; fi
    if [ -x "$h/bin/javac" ]; then type=JDK; else type=JRE; fi
    [ -z "$jv" ] && jv=unknown
    real=$(readlink -f "$h/bin/java")
    owner=$(pkg_owner "$real")
    if [ -n "$owner" ]; then
        by="Package ${owner%%|*}"; un="${owner#*|}"
    else
        case "$h" in
            /opt/*) app=$(echo "$h" | cut -d/ -f1-3); by="No package owner (inside $app)" ;;
            *) by="No package owner found" ;;
        esac
        un="None registered"
    fi
    printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$h" "$h/bin/java" "$line" "$norm" "$jv" "$type" "$impl" "$by" "$un" >> "$RUNTIMES"
done < "$HOMES"

# ---------- dependencies ----------
if [ -s "$RUNTIMES" ]; then
    cut -f1 "$RUNTIMES" > "$WORK/runtimehomes"

    home_of() {  # prints the Java home that path $1 lives in
        local r h
        r=$(readlink -f "$1" 2>/dev/null) || return 1
        while read -r h; do
            case "$r" in "$h"|"$h"/*) echo "$h"; return 0 ;; esac
        done < "$WORK/runtimehomes"
        return 1
    }
    add_dep() { printf '%s\t%s\t%s\n' "$1" "$2" "$3" >> "$DEPS"; }

    # Collect path references (resolved through symlinks) from config files
    scan_refs() {  # $1 = file, $2 = type label, $3 = detail
        local f="$1" t h
        grep -v '^[[:space:]]*#' "$f" 2>/dev/null | grep -oE '/[A-Za-z0-9._/+-]+' | sort -u | while read -r t; do
            case "$t" in *java*|*jvm*|*jre*|*jdk*|*Java*|*JDK*|*JRE*)
                h=$(home_of "$t") && add_dep "$h" "$2" "$3" ;;
            esac
        done
        if [ -n "$DEFAULT_HOME" ] && grep -v '^[[:space:]]*#' "$f" 2>/dev/null | grep -qE '(^|[[:space:]"=;&(])java[[:space:]]'; then
            h=$(home_of "$DEFAULT_HOME") && add_dep "$h" "$2 (java from PATH)" "$3"
        fi
    }

    # systemd services
    for f in /etc/systemd/system/*.service /usr/lib/systemd/system/*.service /lib/systemd/system/*.service \
             /run/systemd/system/*.service /etc/systemd/system/*.service.d/*.conf; do
        [ -f "$f" ] || continue
        grep -qiE 'java|jvm|jre|jdk' "$f" 2>/dev/null || continue
        unit=$(basename "$f"); case "$f" in *.service.d/*) unit=$(basename "$(dirname "$f")" .d) ;; esac
        state=$(systemctl is-enabled "$unit" 2>/dev/null); active=$(systemctl is-active "$unit" 2>/dev/null)
        scan_refs "$f" "Service (systemd)" "$unit ${active:-unknown}, ${state:-unknown}"
    done
    # SysV init scripts
    for f in /etc/init.d/*; do [ -f "$f" ] && scan_refs "$f" "Service (init script)" "$(basename "$f")"; done
    # cron
    for f in /etc/crontab /etc/cron.d/* /var/spool/cron/* /var/spool/cron/crontabs/*; do
        [ -f "$f" ] && scan_refs "$f" "Cron Job" "$f"
    done
    # environment files
    for f in /etc/environment /etc/profile /etc/profile.d/*.sh /etc/default/*; do
        [ -f "$f" ] && scan_refs "$f" "Environment File" "$f"
    done

    # running processes
    for pd in /proc/[0-9]*; do
        exe=$(readlink "$pd/exe" 2>/dev/null) || continue
        h=$(home_of "$exe") || continue
        pid=${pd##*/}
        name=$(cat "$pd/comm" 2>/dev/null)
        user=$(stat -c %U "$pd" 2>/dev/null)
        unit=$(grep -oE '[^/]+\.service' "$pd/cgroup" 2>/dev/null | head -1)
        hint=$(tr '\0' '\n' < "$pd/cmdline" 2>/dev/null | awk '
            NR==1 {next}
            skip {skip=0; next}
            $0=="-jar" {getline j; n=split(j,a,"/"); print "jar " a[n]; exit}
            $0=="-m" || $0=="--module" {getline m; print "module " m; exit}
            $0=="-cp" || $0=="-classpath" || $0=="--class-path" || $0=="-p" || $0=="--module-path" {skip=1; next}
            /^-/ || /^@/ {next}
            {print ($0=="org.apache.catalina.startup.Bootstrap" ? "Apache Tomcat (org.apache.catalina.startup.Bootstrap)" : "class " $0); exit}')
        detail="$name PID $pid, user ${user:-unknown}"
        [ -n "$unit" ] && detail="$detail (service $unit)"
        [ -n "$hint" ] && detail="$detail - $hint"
        add_dep "$h" "Running Process" "$detail"
    done

    # default java on PATH (/usr/bin/java, alternatives)
    if [ -n "$DEFAULT_HOME" ]; then
        h=$(home_of "$DEFAULT_HOME") && add_dep "$h" "Default Java" "/usr/bin/java points here (alternatives)"
    fi
fi

# ---------- output ----------
sort -u "$DEPS" -o "$DEPS"
while IFS="$(printf '\t')" read -r h path line norm jv type impl by un; do
    base="$(clean "$path")|$(clean "$line")|$(clean "$norm")|$(clean "$jv")|$(clean "$type")|$(clean "$impl")|$(clean "$by")|$(clean "$un")"
    found=0
    while IFS="$(printf '\t')" read -r dh dt dd; do
        [ "$dh" = "$h" ] || continue
        found=1
        echo "$base|$(clean "$dt")|$(clean "$dd")"
    done < "$DEPS"
    [ $found -eq 0 ] && echo "$base|None found|No service, process, cron job, or environment file references it"
done < "$RUNTIMES" | sort -u
[ -f "$WORK/truncated" ] && echo "Scan incomplete||||||||Scan incomplete|Folder search stopped after $SCAN_SECONDS seconds. Some folders were not checked."
exit 0
#!/bin/sh
echo "N/A"
#!/bin/sh
echo "N/A"
#!/bin/sh
echo "N/A"

Step 7: Save

  1. Scroll to the bottom of the page and click Save.
  2. If you see SensorQueryTooLong, the Windows script is over 28,000 characters. Clear the Windows editor and paste the script again using the copy button.
  3. If the console reports that the name already exists, cancel and edit the existing sensor.

Step 8: Test on a few machines first

  1. Go to Interact.
  2. In the question bar, enter Get Java - Runtime Inventory - Dependencies from all machines with Computer Name contains "<hostname>", replacing <hostname> with a machine you know has Java.
  3. Check the results:
    • You see 10 columns. If everything is in one column with | inside the value, the column split was not saved. Edit the sensor and recheck Step 4.
    • Every Java on the machine is listed, including patched ones.
    • No row starts with Scan incomplete, and the result does not stay on [current result unavailable]. If either happens, see Troubleshooting.

Finding vulnerable Java with this sensor

This sensor tells you where Java is and what depends on it. Your vulnerability scanner tells you which versions are vulnerable. Use them together:

  1. Get the fixed version from the scanner. Open a Java finding in your vulnerability report and note the fixed (or first non-vulnerable) version for each Java line, for example Java 17 fixed in 17.0.21. A Java line that no longer receives updates (check the vendor's support roadmap) has no fixed version, so every copy of it is vulnerable.
  2. List Java on the affected machines. Ask the sensor on all machines, or narrow it to the machines in the report: Get Java - Runtime Inventory - Dependencies from all machines
  3. Narrow to one Java line if needed. In the results, narrow by the Java Line column (for example 8) or the Version String column (for example 1.8.0_).
  4. Compare. In the results grid, sort by Java Line and then Version. Every row whose Version is below the fixed version for its Java line is vulnerable. For large result sets, export the results to CSV from the results grid and compare there.
  5. Remediate using the Installed By, Uninstall Command, and Used By columns. See Remediation by finding.

Reading the results

Column What it tells you
Java Path Full path to java.exe (or jvm.dll if there is no java.exe). Use it with File Exists to confirm removal
Java Line The major version: 8, 11, 17, 21, 25, and so on. Java 1.8 is reported as 8
Version Sortable version, for example 8.0.401 for Java 8 update 401, or 17.0.5
Version String Exactly as Java writes it, for example 1.8.0_401 or 17.0.5. This is the value vulnerability reports usually show
Type JDK if javac.exe is present, otherwise JRE
Vendor For example Oracle Corporation, Eclipse Adoptium, Amazon.com Inc., Azul Systems
Installed By The installer entry (standalone Java), Bundled with <application>, Package <name> on Linux, No installer entry (copied or unzipped), or Unknown (user profile not logged on) (see Check before you remove anything)
Uninstall Command For MSI installs: MsiExec.exe /X{GUID} /qn /norestart. For bundled Java: Update or remove <application>. On Linux: the package manager command
Used By Type / Used By What depends on this runtime (see the table in How the sensor works)

Example row (values are illustrative): C:\Program Files\Java\jre1.8.0_401\bin\java.exe|8|8.0.401|1.8.0_401|JRE|Oracle Corporation|Java 8 Update 401 (64-bit) 8.0.4010.11|MsiExec.exe /X{00000000-0000-0000-0000-000000000000} /qn /norestart|None found|No service, process, scheduled task, variable, or file association references it


Remediation by finding

Match each vulnerable row to a finding below using its Installed By and Used By Type values. The findings are not steps in a sequence. One machine can have rows in several of them.

Check before you remove anything

Skip a row, or confirm it first, when any of these apply:

  • Installed By is Unknown (user profile not logged on). The Java is inside the profile of a user who was logged off when the sensor ran, so the sensor could not read that user's installer entries or environment variables. The row doesn't show whether the Java was installed or copied, or whether that user's JAVA_HOME points at it. Don't act on it. Ask the sensor again on that machine while the user is logged on, and use the new answer.
  • Used By Type ends with likely uses it. The link is based on folder location only. Confirm with the application owner.
  • The machine also returned a Scan incomplete row. The list for that machine is partial.
  • The answer is older than the change you are about to make. Results are reused for up to an hour (Max Sensor Age), so ask again right before removing anything.

Standalone Java, nothing uses it

Installed By is a Java installer, Uninstall Command starts with MsiExec.exe /X, and Used By Type is None found. Uninstall with a Tanium package that runs the Uninstall Command.

Standalone Java that something uses

Installed By is a Java installer, and Used By Type lists a service, process, task, variable, or file association. Install a supported Java version first, point the service, task, or JAVA_HOME at it, then uninstall the old one.

Java bundled with an application

Installed By starts with Bundled with, or reads No installer entry (inside <folder>) on Windows or No package owner (inside /opt/<app>) on Linux. The Java sits inside an application's folder, and removing it will break the application. Update the application or ask its vendor for a version with a supported Java.

Java with no installer entry (copied or unzipped)

Installed By is No installer entry found (Windows) or No package owner found (Linux), and Uninstall Command is None registered. Nothing registered this Java, so there is no uninstaller and it has to be deleted.

  1. Confirm nothing uses it. Used By Type must be None found in a fresh answer. If anything is listed, move it to a supported Java first, as in Standalone Java that something uses.
  2. Work out the folder to delete. It's the Java Path minus \bin\java.exe (Windows) or /bin/java (Linux). For example, C:\Tools\jdk-17.0.5\bin\java.exe means you delete C:\Tools\jdk-17.0.5, and /opt/jdk-17.0.5/bin/java means /opt/jdk-17.0.5. If the Java Path ends in \bin\server\jvm.dll, remove that part instead.
  3. Check that the folder holds only Java. A Java folder contains bin, lib, and usually a release file. If the folder name is generic (for example C:\App\runtime) or it sits inside another product's folder, treat it as bundled instead.
  4. Delete it with a Tanium package. On Windows use cmd.exe /c rmdir /s /q "C:\Tools\jdk-17.0.5", and on Linux use rm -rf /opt/jdk-17.0.5, with the folder from step 2.
  5. Confirm with Get File Exists[<Java Path>] from all machines, replacing <Java Path> with the value from the Java Path column.

Java lines that no longer get updates

Your scanner reports no fixed version for the Java line, or the Java Line is not a long-term support (LTS) release. The LTS lines are 8, 11, 17, 21, and 25. Every other line gets updates only until the next release, six months later. For LTS lines, the end of updates depends on the vendor, so check the Vendor column against that vendor's support roadmap. These runtimes can't be patched in place.

  1. Pick the replacement. Choose the newest LTS release the application supports. Check with the application owner or vendor first. If the row is bundled, follow Java bundled with an application instead.
  2. Install the new Java with a Tanium package.
  3. Repoint every dependency listed in Used By:
    • Windows: the service (procrun Jvm setting, wrapper .conf, WinSW .xml, or the service path), the scheduled task action, JAVA_HOME and PATH, and the .jar file association.
    • Linux: the systemd unit (ExecStart or Environment=JAVA_HOME), cron entries, environment files, and the default Java (alternatives --set java on RHEL, update-alternatives --set java on Debian and Ubuntu).
  4. Restart the service or run the task, and confirm the application works on the new Java.
  5. Remove the old Java using the finding above that matches its Installed By.

After any removal, ask this sensor again and check the Version column against the fixed version.


Maintenance

None for new Java releases or security updates: the sensor has no version list. Revisit it only if:

  • Java is installed in a folder the search does not cover. Add it to $ExtraScanRoots (Windows, for example @('D:\Apps')) or EXTRA_SCAN_ROOTS (Linux, for example "/data/apps") near the top of the script.
  • Scans stop early on large servers. Raise $ScanSeconds or SCAN_SECONDS.

Troubleshooting

Result Meaning What to do
Scan incomplete row The folder search hit its time limit on that machine Raise $ScanSeconds (Windows) or SCAN_SECONDS (Linux), or add the application folder to $ExtraScanRoots / EXTRA_SCAN_ROOTS
Stays on [current result unavailable] The script is running longer than the Tanium Client allows and is being stopped Lower $ScanSeconds to 20 and test again
All values in one column Split into multiple columns or the \| delimiter was not saved Edit the sensor and recheck Step 4
unknown in Java Line and Version No release file and no version on java.exe Check the version on that machine manually
Java you expected is missing It is outside the searched folders and not running Add its folder to $ExtraScanRoots / EXTRA_SCAN_ROOTS
N/A on Mac / Solaris / AIX Expected Those platforms are not covered

Limits

  • Per-user Java folders (AppData\Local\Programs, AppData\Local\JetBrains, .jdks) are searched for every profile, logged on or not. Only the registry checks are limited to logged-on users: per-user installer entries and user environment variables are read from loaded profiles only. For a user who is logged off, a per-user Java still appears, but Installed By and Uninstall Command show Unknown (user profile not logged on), and a user JAVA_HOME or PATH entry that points at it is not listed.
  • (java from PATH) dependencies use the system PATH, so a task that runs as a user with a different PATH may resolve to another Java.
  • Rows marked likely uses it are based on folder location only. Confirm before removing.
  • The sensor returns more rows than the vulnerable-only version, because patched runtimes are included.
  • Java-based products such as Apache Tomcat or Log4j have their own vulnerabilities. Updating Java does not fix those.