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, andPATH(system and logged-on users)- Running
java.exe,javaw.exe,javaws.exe, andjp2launcher.exeprocesses, wherever they are on disk - A folder search of
C:\Program Files,C:\Program Files (x86),C:\ProgramData, other top-level folders onC:\, other fixed drives, and each user'sAppData\Local\Programs,AppData\Local\JetBrains, and.jdksfolders
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
- Sign in to the Tanium Console.
- Open the main menu, then go to Administration > Content > Sensors.
- 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:
- Set Delimiter to
|(the pipe character, Shift + Backslash on a US keyboard). - 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
Step 7: Save
- Scroll to the bottom of the page and click Save.
- 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. - If the console reports that the name already exists, cancel and edit the existing sensor.
Step 8: Test on a few machines first
- Go to Interact.
- 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. - 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.
- You see 10 columns. If everything is in one column with
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:
- 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.
- 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 - 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 example1.8.0_). - 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.
- 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'sJAVA_HOMEpoints 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.
- 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.
- 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.exemeans you deleteC:\Tools\jdk-17.0.5, and/opt/jdk-17.0.5/bin/javameans/opt/jdk-17.0.5. If the Java Path ends in\bin\server\jvm.dll, remove that part instead. - Check that the folder holds only Java. A Java folder contains
bin,lib, and usually areleasefile. If the folder name is generic (for exampleC:\App\runtime) or it sits inside another product's folder, treat it as bundled instead. - Delete it with a Tanium package. On Windows use
cmd.exe /c rmdir /s /q "C:\Tools\jdk-17.0.5", and on Linux userm -rf /opt/jdk-17.0.5, with the folder from step 2. - 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.
- 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.
- Install the new Java with a Tanium package.
- Repoint every dependency listed in Used By:
- Windows: the service (procrun
Jvmsetting, wrapper.conf, WinSW.xml, or the service path), the scheduled task action,JAVA_HOMEand PATH, and the.jarfile association. - Linux: the systemd unit (
ExecStartorEnvironment=JAVA_HOME), cron entries, environment files, and the default Java (alternatives --set javaon RHEL,update-alternatives --set javaon Debian and Ubuntu).
- Windows: the service (procrun
- Restart the service or run the task, and confirm the application works on the new Java.
- 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')) orEXTRA_SCAN_ROOTS(Linux, for example"/data/apps") near the top of the script. - Scans stop early on large servers. Raise
$ScanSecondsorSCAN_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 showUnknown (user profile not logged on), and a userJAVA_HOMEor 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.