Collecting Tenable Nessus Agent Status With Powershell

Share on:

Information from the Tenable Nessus Agent, as far as I'm aware, is only available when using the Nessus Client command line application (NessusCli.exe). In this post, I'm going to show how we can capture this with PowerShell. I'm going to walk through how I do this step-by-step. If you're looking for just the script, you can skip all the way to the bottom.

The first step is to identify where to find our NessusCli.exe file. You could get dynamic about this if you wanted, but for the purposes of this post we'll assume it's always in the default location, like so.

1Try {
2    $nessusExe = Join-Path $env:ProgramFiles -ChildPath "Tenable\Nessus Agent\nessuscli.exe" -ErrorAction Stop
3}  Catch {
4    Throw "Cannot find NessusCli.exe"
5}

Next, we need to call this executable with the arguments agent status in order to get the information reported through the Standard Output and Standard Error streams. As an example, if you call this manually, you'll see something like this: Nessus Agent Status

To capture all of this information in PowerShell, I've set up a function that starts the process, waits for it to complete, and then reads in the streams we discussed above. This function is relatively generic on purpose so I can re-use in other scripts.

 1Function Start-ProcessGetStreams {
 2    [CmdLetBinding()]
 3    Param(
 4        [System.IO.FileInfo]$FilePath,
 5        [string[]]$ArgumentList
 6    )
 7
 8    $pInfo = New-Object System.Diagnostics.ProcessStartInfo
 9    $pInfo.FileName = $FilePath
10    $pInfo.Arguments = $ArgumentList
11    $pInfo.RedirectStandardError = $true
12    $pInfo.RedirectStandardOutput = $true
13    $pinfo.UseShellExecute = $false
14    $pInfo.CreateNoWindow = $true
15    $pInfo.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
16
17    $proc = New-Object System.Diagnostics.Process
18    $proc.StartInfo = $pInfo
19
20    Write-Verbose "Starting $FilePath"
21    $proc.Start() | Out-Null
22    
23    Write-Verbose "Waiting for $($FilePath.BaseName) to complete"
24    $proc.WaitForExit()
25
26    $stdOut = $proc.StandardOutput.ReadToEnd()
27    $stdErr = $proc.StandardError.ReadToEnd()
28    $exitCode = $proc.ExitCode
29
30    Write-Verbose "Standard Output: $stdOut"
31    Write-Verbose "Standard Error: $stdErr"
32    Write-Verbose "Exit Code: $exitCode"
33
34    [PSCustomObject]@{
35        "StdOut" = $stdOut
36        "Stderr" = $stdErr
37        "ExitCode" = $exitCode
38    }
39}

This function returns a hashtable that contains the StdOut (Standard Output) and StdErr (Standard Error) streams as well as the exit code of the process.

We can now call this function in our script like so:

1$agentStatus = Start-ProcessGetStreams -FilePath $nessusExe -ArgumentList "agent status"

If you look at the $agentStatus variable, you'll now see (on a working agent) output similar to what you saw when you ran it manually.

Nessus StdOut in PowerShell

We now need to break up that data into something useful. For this, we first split the block of text (in StdOut) into individual lines, and then break each line up where the item before the colon (:) is the name, and the item after is the value. The following function does this for us, and also replaces any non-alphanumeric characters in the name to underscores so that they fit nicely in a hashtable.

 1Function Get-NessusStatsFromStdOut {
 2    
 3    Param(
 4        [string]$stdOut
 5    )
 6    
 7    $stats = New-Object System.Collections.Hashtable
 8
 9    $StdOut -split "`r`n" | % {
10        if($_ -like "*:*") {
11            $result = $_ -split ":"
12            $stats.add(($result[0].Trim() -replace "[^A-Za-z0-9]","_").ToLower(),$result[1].Trim())
13        }
14    }
15
16    Return $stats
17}

When we call this function, we now have all of the items in a hashtable that we can reference. Here is what the table looks like: Nessus Status as HashTable in PowerShell

You'll notice that the Last Connection, Last Successful Connection, and Last Scanned values are integers and not dates. These integers count the number of seconds since 1/1/1970 at 00:00:00 UTC and are known as "Epoch Time". If you're interested in reading more about this or want to see how this works using a calculator, there is a great page here for more information. In order to convert these to valid dates using PowerShell, you can use this function.

1Function Get-DateFromEpochSeconds {
2    Param(
3        [int]$seconds
4    )
5    
6    $utcTime = (Get-Date 01.01.1970)+([System.TimeSpan]::fromseconds($seconds))
7    Return Get-Date $utcTime.ToLocalTime() -Format "yyyy-MM-dd HH:mm:ss"
8} 

Bringing everything together, we end up with the following code. We've taken everything from above and also added in some checking so that if the output is not what we expect, we can throw some errors, and we also automatically convert the Epoch Times to local time.

 1#RequireAdmin
 2
 3Function Start-ProcessGetStreams {
 4
 5    [CmdLetBinding()]
 6    Param(
 7        [System.IO.FileInfo]$FilePath,
 8        [string[]]$ArgumentList
 9    )
10
11    $pInfo = New-Object System.Diagnostics.ProcessStartInfo
12    $pInfo.FileName = $FilePath
13    $pInfo.Arguments = $ArgumentList
14    $pInfo.RedirectStandardError = $true
15    $pInfo.RedirectStandardOutput = $true
16    $pinfo.UseShellExecute = $false
17    $pInfo.CreateNoWindow = $true
18    $pInfo.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
19
20    $proc = New-Object System.Diagnostics.Process
21    $proc.StartInfo = $pInfo
22
23    Write-Verbose "Starting $FilePath"
24    $proc.Start() | Out-Null
25    
26    Write-Verbose "Waiting for $($FilePath.BaseName) to complete"
27    $proc.WaitForExit()
28
29    $stdOut = $proc.StandardOutput.ReadToEnd()
30    $stdErr = $proc.StandardError.ReadToEnd()
31    $exitCode = $proc.ExitCode
32
33    Write-Verbose "Standard Output: $stdOut"
34    Write-Verbose "Standard Error: $stdErr"
35    Write-Verbose "Exit Code: $exitCode"
36
37    [PSCustomObject]@{
38        "StdOut" = $stdOut
39        "Stderr" = $stdErr
40        "ExitCode" = $exitCode
41    }
42
43}
44
45Function Get-NessusStatsFromStdOut {
46    
47    Param(
48        [string]$stdOut
49    )
50    
51    $stats = New-Object System.Collections.Hashtable
52
53    $StdOut -split "`r`n" | % {
54        if($_ -like "*:*") {
55            $result = $_ -split ":"
56            $stats.add(($result[0].Trim() -replace "[^A-Za-z0-9]","_").ToLower(),$result[1].Trim())
57        }
58    }
59
60    Return $stats
61}
62
63Function Get-DateFromEpochSeconds {
64    Param(
65        [int]$seconds
66    )
67    
68    $utcTime = (Get-Date 01.01.1970)+([System.TimeSpan]::fromseconds($seconds))
69    Return Get-Date $utcTime.ToLocalTime() -Format "yyyy-MM-dd HH:mm:ss"
70}    
71
72Try {
73    $nessusExe = Join-Path $env:ProgramFiles -ChildPath "Tenable\Nessus Agent\nessuscli.exe" -ErrorAction Stop
74}  Catch {
75    Throw "Cannot find NessusCli.exe"
76}
77    
78Write-Host "Getting Agent Status..."
79$agentStatus = Start-ProcessGetStreams -FilePath $nessusExe -ArgumentList "agent status"
80
81If($agentStatus.stdOut -eq "" -and $agentStatus.StdErr -eq "") {
82    Throw "No Data Returned from NessusCli"
83} elseif($agentStatus.StdOut -eq "" -and $agentStatus.StdErr -ne "") {
84    Throw "StdErr: $($agentStatus.StdErr)"
85} elseif(-not($agentStatus.stdOut -like "*Running: *")) {
86    Throw "StdOut: $($agentStatus.StdOut)"
87} else {
88    $stats = Get-NessusStatsFromStdOut -stdOut $agentStatus.StdOut
89    If($stats.last_connection_attempt -as [int]) { $stats.last_connection_attempt = Get-DateFromEpochSeconds $stats.last_connection_attempt }
90    If($stats.last_connect -as [int]) { $stats.last_connect = Get-DateFromEpochSeconds $stats.last_connect }
91    If($stats.last_scanned -as [int]) { $stats.last_connect = Get-DateFromEpochSeconds $stats.last_scanned }
92}
93
94$stats | Out-Host


No comments