Nessus is one of the most widely deployed vulnerability scanners in the world. Driven from Python, an entire inspection, launch, wait, read the findings, becomes a handful of method calls instead of a sequence of clicks in a web interface.
Analogy: The building inspector
A building inspector arrives with a long checklist of known defects, cracked beams, faulty wiring, blocked fire exits, and works through it room by room, noting every item that fails and how serious each one is. Nessus is that inspector for a computer: it checks a target against a continuously updated database of known vulnerabilities and reports each finding with a severity score. What makes it powerful from Python is that the whole inspection can be launched, monitored and read back through an API, so it slots into the assembly line.
Tool in focus: the Nessus REST API
Nessus, from Tenable, exposes a REST API that Python's requests library drives directly, with no Nessus-specific package needed. Authentication uses an API access key and secret key pair generated in the Nessus web interface, which replaces a reusable login password on every request.
Python concept: An API key pair, and why not a password
An API key pair is like a hotel keycard rather than the master key. It can be issued, scoped and revoked without changing your actual account password, and if a script leaks it, you cancel that one card rather than your whole identity. This is why automation authenticates with keys, never with a stored login password.
A small client
nessus_client.py
# nessus_client.py
import requests, time
requests.packages.urllib3.disable_warnings() # Nessus uses a self-signed cert
class NessusClient:
def __init__(self, base_url, access_key, secret_key):
self.base_url = base_url.rstrip("/")
self.headers = {
"X-ApiKeys": f"accessKey={access_key}; secretKey={secret_key}",
"Content-Type": "application/json",
}
def launch_scan(self, scan_id):
r = requests.post(f"{self.base_url}/scans/{scan_id}/launch",
headers=self.headers, verify=False)
r.raise_for_status()
return r.json()["scan_uuid"]
def scan_status(self, scan_id):
r = requests.get(f"{self.base_url}/scans/{scan_id}",
headers=self.headers, verify=False)
r.raise_for_status()
return r.json()["info"]["status"]
def wait_for_completion(self, scan_id, poll_seconds=15):
while True:
status = self.scan_status(scan_id)
print(f"Scan {scan_id} status: {status}")
if status == "completed":
return
time.sleep(poll_seconds)
Caution: verify=False is a deliberate, narrow exception
verify=False skips certificate verification, reasonable only because a typical Nessus install uses a self-signed certificate on an internal network. An integration reaching Nessus over an untrusted network should install and verify a proper certificate instead of silently disabling the check this way.
wait_for_completion is a small polling loop: ask the status, print it, and either return once the scan is "completed" or sleep and ask again. This is the same shape as a loop you could write around any slow operation with no notification of its own, checked, waited, checked again.
Severity: reading the triage
Nessus reports severity on a five-point scale.
Analogy: The hospital emergency ward
Triage sees the critically injured first and the minor cases wait. Nessus's severity scale works the same way.
| Level | Severity | Typical response |
|---|---|---|
| 4 | Critical | Same-day attention, especially on an internet-facing host |
| 3 | High | Patch within the current cycle; do not let it linger |
| 2 | Medium | Schedule for the next maintenance window |
| 1 | Low | Fix when convenient; track so it is not forgotten |
| 0 | Info | No action; context for the analyst |
Turning a list of findings into a triage summary
nessus_results.py
# nessus_results.py
def get_vulnerabilities(client, scan_id):
r = requests.get(f"{client.base_url}/scans/{scan_id}",
headers=client.headers, verify=False)
r.raise_for_status()
return r.json()["vulnerabilities"]
def summarise_by_severity(vulnerabilities):
names = {4: "Critical", 3: "High", 2: "Medium", 1: "Low", 0: "Info"}
counts = {name: 0 for name in names.values()}
for v in vulnerabilities:
counts[names.get(v["severity"], "Info")] += 1
return counts
nessus_client = NessusClient("https://127.0.0.1:8834", "access-key", "secret-key")
nessus_client.wait_for_completion(scan_id=42)
vulns = get_vulnerabilities(nessus_client, scan_id=42)
print(summarise_by_severity(vulns))
The get_vulnerabilities() call needs a live Nessus server, which this course cannot ship, so it has not been run against a real one here. summarise_by_severity() itself needs nothing but a list of dictionaries with a "severity" key, so it is fully testable on its own with made-up findings shaped like the real thing:
Sample output, summarise_by_severity() run against 68 synthetic findings (2 critical, 5 high, 13 medium, 7 low, 41 info)
{'Critical': 2, 'High': 5, 'Medium': 13, 'Low': 7, 'Info': 41}
That dictionary, two critical findings demanding same-day attention against forty-one informational items that can wait, is the whole triage summary. A dashboard reads it directly, which is the whole point of the assembly line: no one has to read sixty-eight rows to know where to look first.
Try it yourself: See the pure half run on your own machine
You do not need a Nessus server to try summarise_by_severity() yourself. Build a list of dictionaries with a severity key set to any mix of 0 to 4, call the function, and check the counts add up to the length of your list. This is the testable half of almost every integration in this unit: the logic that shapes data, separate from the network call that fetches it.
Check your understanding
Question 1: Why is an API key pair preferred over a reusable username and password for a script? Use the hotel-keycard idea in your answer.
A hotel keycard can be issued for one stay, scoped to one room, and cancelled without changing the hotel's master lock. An API key pair works the same way: it can be issued for one script's purpose, scoped to what that script needs, and revoked on its own if it leaks, without having to change the account's actual login password everywhere else it is used.
Question 2: What does wait_for_completion() actually do, and why does it need a sleep between checks?
It repeatedly asks the Nessus server for the scan's status and returns once that status is "completed". It sleeps between checks so it does not hammer the server with requests many times a second while the scan, which can take minutes, is still running.
Question 3: Why is summarise_by_severity() testable without a running Nessus server, while get_vulnerabilities() is not?
summarise_by_severity() only operates on a list of dictionaries already in memory; it never talks to a network at all. get_vulnerabilities() makes an HTTP request to a live Nessus server and needs one running and reachable to produce any result.
Key takeaway
Nessus is driven entirely through its REST API, authenticated with a revocable key pair rather than a password, and polled until a scan completes. Its severity scale reads like hospital triage, and summarising findings by severity turns a long list into the one number that actually decides what gets fixed first.

