Vulnerability scanning lab: find, rank and fix with Greenbone (OpenVAS)
0 / 12 steps
The goal
Run a real vulnerability scanner, Greenbone Community Edition (the free home of the OpenVAS scanner), against a deliberately old web server that you build yourself. You will read what the scanner found, look the worst findings up in three public sources, decide what to fix first and why, apply the fix, and then scan again to prove the fix worked. That find, rank, fix, verify loop is what vulnerability management means in a real job.
Scan only what you own. The target in this lab is a container you create on a private Docker network that has no route to the internet, inside your own machine. Pointing a vulnerability scanner at someone else’s computer, website or network without their written permission is illegal in most countries, even “just to look”, and scans are noisy enough that people notice. Step 12 removes everything.
Why this lab. A scanner will happily hand you a report with forty “critical” findings. Nobody can fix forty things on Monday morning, so the real skill is deciding which three to fix first, and then proving they are fixed. Security+ asks about exactly this: CVSS scores, why a high score alone isn’t enough, false positives, compensating controls and rescanning to validate a fix.
A scanner is a smoke detector with opinions. It will scream about the toast. Your job is to find the actual fire. — Nyx
Before you start. Use an Ubuntu machine you control: the VM from Lab 00 or Ubuntu 24.04 in WSL2. You need Docker Engine with the Compose plugin (the Docker security lab and Docker’s own install guide cover it), plus sudo apt install curl jq python3. Greenbone is big: allow about 50 GB of disk and 4 GB of free memory. Open a root shell with sudo -i (prompt #) and keep that same terminal open for the whole lab, because later steps reuse things you set earlier.
Words you’ll meet
| Term | What it means |
|---|---|
| Vulnerability | A weakness in software or settings that an attacker could use. A scanner looks for known ones. |
| CVE | Common Vulnerabilities and Exposures: the public ID a known flaw gets, like CVE-2021-41773. One ID, one flaw, so everyone talks about the same thing. |
| CVSS | Common Vulnerability Scoring System: a 0 to 10 score for how bad a flaw is in general (9.0 and up is “critical”). The base score knows nothing about your network. |
| EPSS | Exploit Prediction Scoring System, run by FIRST: the estimated chance (0 to 1) that a CVE gets exploited somewhere in the next 30 days. |
| KEV | CISA’s Known Exploited Vulnerabilities catalog: CVEs that attackers are confirmed to be using. US federal agencies must fix these by a deadline. |
| NVT | Network Vulnerability Test: one of Greenbone’s individual checks. The feed holds well over a hundred thousand of them. |
| QoD | Quality of Detection: how sure Greenbone is that a finding is real, 0 to 100%. A check that only reads a version number scores lower than one that actually proves the flaw. |
| False positive | The scanner says “vulnerable” but you aren’t. A false negative is the scarier opposite: a real flaw the scanner missed. |
Steps
01Download Greenbone and pull its images
Greenbone publishes a Docker Compose file: one text file that lists every container the scanner needs (about twenty, from the database to the web interface) and how they connect. Make a folder for it and download the current version straight from Greenbone’s documentation site.
# mkdir -p ~/greenbone && cd ~/greenbone # curl -fsSL https://greenbone.github.io/docs/latest/_static/compose.yaml -o compose.yaml # docker compose -p greenbone pull -q # docker image ls --format "{{.Repository}}:{{.Tag}} {{.Size}}" | sort
The pull is the slow part: in our run it took 8 minutes and the images came to roughly 12.5 GB. The listing (trimmed) shows the two biggest are the feeds themselves:
... registry.community.greenbone.net/community/gvmd:stable 1.09GB ... registry.community.greenbone.net/community/ospd-openvas:stable 1.18GB ... registry.community.greenbone.net/community/scap-data:latest 3.39GB registry.community.greenbone.net/community/vulnerability-tests:latest 3.03GB
What is a feed? A scanner is only as good as its list of known problems. Greenbone’s free Community Feed ships the NVTs (the individual tests), SCAP data (the official CVE and product lists from the US National Vulnerability Database) and CERT advisories. They arrive as data containers so the rest of the stack can load them.
02Start Greenbone and set the admin password
Start everything in the background, then replace the default admin password with one of your own. Put your own password in the GVM_PW= line; later steps reuse it.
# docker compose -p greenbone up -d # GVM_PW='pick-your-own-long-password' # docker compose -p greenbone exec -u gvmd gvmd gvmd --user=admin --new-password="$GVM_PW"
The password change printed Modifying user password. In our run, up -d took under 3 minutes.
Now make a short helper, gmp, so you can talk to the scanner from the terminal. GMP (Greenbone Management Protocol) is the XML language the web interface itself uses behind the scenes; gvm-cli sends it for you.
# gmp() { docker compose -p greenbone run --rm -T --no-deps gvm-tools gvm-cli --gmp-username admin --gmp-password "$GVM_PW" socket --pretty --xml "$1"; } # gmp "<get_version/>"
<get_version_response status="200" status_text="OK"> <version>22.7</version> </get_version_response>
Prefer clicking? The same scanner has a web interface at https://127.0.0.1 (log in as admin with your password; accept the self-signed certificate warning). This lab sticks to the terminal so every step is copy-and-paste and you can see exactly what happens. Each gmp call starts a small helper container, so expect 5 to 30 seconds per call.
03Wait for the feeds to load
The containers are running, but the manager still has to read every CVE and test into its database. Check on it:
# gmp "<get_feeds/>" | grep -E "<name>|<currently_syncing>" # gmp "<get_configs/>" | grep -A12 "<name>Full and fast</name>" | grep nvt_count
While a feed is still loading you will see <currently_syncing> under it, and the second command prints nothing. When it’s ready the second command prints a count. Ours, once loaded:
<name>Greenbone Community Feed</name>
<name>Greenbone SCAP Data Feed</name>
<name>Greenbone CERT Data Feed</name>
<name>Greenbone Data Objects Feed</name>
<nvt_count>191859<growing>1</growing></nvt_count>
How long? On our machine the full load needed more than an hour and a half after up -d: over an hour importing years of CVE records, then 10 to 15 minutes for the tests themselves. Don’t restart the machine or WSL while it runs; ours was interrupted, which caused both problems in the step 6 note. Go and do something else, then rerun the two commands every ten minutes. Scans started early simply wait in a queue. Full and fast is Greenbone’s standard scan configuration: every safe test, skipping ones that could crash the target.
04Build an old web server to scan
Your target is Apache HTTP Server 2.4.49, a real release from September 2021 that had serious, widely attacked bugs. It runs in a container on vulnlab, a Docker network created with --internal, which means nothing on it can reach the internet. Then plug the scanner into the same network.
# docker network create --internal --subnet 172.30.99.0/24 vulnlab # docker run -d --name oldweb --network vulnlab --ip 172.30.99.10 httpd:2.4.49 # docker network connect vulnlab greenbone-ospd-openvas-1 # docker exec oldweb httpd -v # docker run --rm --network vulnlab curlimages/curl:8.11.1 -sI http://172.30.99.10/ # docker run --rm --network vulnlab curlimages/curl:8.11.1 -s -m 5 https://example.com -o /dev/null; echo "internet from vulnlab? exit $?"
... Server version: Apache/2.4.49 (Unix) Server built: Sep 28 2021 07:46:54 HTTP/1.1 200 OK Date: Thu, 05 Nov 2026 02:02:07 GMT Server: Apache/2.4.49 (Unix) ... internet from vulnlab? exit 6
Reading it. The Server: header tells anyone who asks exactly which version is running, which is how an uncredentialed scan (one that has no login on the machine) recognizes it. The last line failed with exit 6, curl’s “couldn’t resolve host”: the network really is cut off from the internet, so this deliberately weak server can’t be reached from outside your machine. greenbone-ospd-openvas-1 is the container that actually sends the tests.
05Point the scanner at it and start the scan
A scan in Greenbone has three parts: a target (what to scan), a task (how to scan it) and the scanner that does the work. Create the target with the TCP ports to try, create the task with the Full and fast configuration, then start it. Each command saves the ID it gets back (target, task, and the report the scan will fill in) so later commands can use it.
# TARGET=$(gmp "<create_target><name>oldweb</name><hosts>172.30.99.10</hosts><alive_tests>Consider Alive</alive_tests><port_list id=\"33d0cd82-57c6-11e1-8ed1-406186ea4fc5\"/></create_target>" | grep -o 'id="[^"]*"' | head -1 | cut -d'"' -f2) # TASK=$(gmp "<create_task><name>oldweb scan</name><config id=\"daba56c8-73ec-11df-a475-002264764cea\"/><target id=\"$TARGET\"/><scanner id=\"08b69003-5fc2-4037-a479-93b440211c73\"/></create_task>" | grep -o 'id="[^"]*"' | head -1 | cut -d'"' -f2) # REPORT=$(gmp "<start_task task_id=\"$TASK\"/>" | grep -o '<report_id>[^<]*' | cut -d'>' -f2) # echo "target $TARGET task $TASK report $REPORT"
target 645fac7a-2b87-469a-8549-6c683848447b task 4cc7c4d4-8311-41bf-9f44-773633bcf947 report f29c6fc0-cb8c-474a-bbd1-27978e620736
The long IDs are built into every Greenbone install: 33d0cd82-… is the port list “All IANA assigned TCP”, daba56c8-… is the “Full and fast” scan configuration and 08b69003-… is the built-in OpenVAS scanner. Consider Alive skips the “is anybody there?” ping, because you already know your target is up.
06Watch it run, then list what it found
Check the progress every few minutes:
# gmp "<get_tasks task_id=\"$TASK\"/>" | grep -E "<status>|<progress>" | head -2
Ours went from Requested to Running to Done in about 6 minutes. Greenbone’s raw results are long XML, so save this tiny Python helper that turns them into one line per finding, worst first:
# cat > ~/greenbone/findings.py <<'EOF'
import sys, xml.etree.ElementTree as ET
# Greenbone XML in, one line per finding out (worst first).
# Add a word, e.g. "python3 findings.py Traversal", for full details of matching findings.
word = sys.argv[1].lower() if len(sys.argv) > 1 else None
rows = []
for r in ET.fromstring(sys.stdin.read()).iter("result"):
nvt = r.find("nvt")
if nvt is None:
continue
tags = dict(t.split("=", 1) for t in (nvt.findtext("tags") or "").split("|") if "=" in t)
rows.append({"sev": float(r.findtext("severity") or 0), "qod": r.findtext("qod/value"),
"port": r.findtext("port"), "name": r.findtext("name"),
"cves": [x.get("id") for x in nvt.iter("ref") if x.get("type") == "cve"],
"vector": tags.get("cvss_base_vector", ""), "how": tags.get("vuldetect", ""),
"fix": nvt.findtext("solution") or "", "found": (r.findtext("description") or "").strip()})
rows.sort(key=lambda x: -x["sev"])
if word is None:
print(f"{len(rows)} findings")
for f in rows:
if word is None:
more = f" (+{len(f['cves']) - 3} more)" if len(f["cves"]) > 3 else ""
print(f"{f['sev']:4.1f} QoD {f['qod']}% {f['port']:8} {f['name'][:70]}")
if f["cves"]:
print(f" CVEs: {', '.join(f['cves'][:3])}{more}")
elif word in f["name"].lower():
print(f["name"])
print(f" severity {f['sev']} {f['vector']}")
print(f" QoD {f['qod']}% CVEs: {', '.join(f['cves']) or 'none listed'}")
print(" what it saw: " + " / ".join(" ".join(l.split()) for l in f["found"].splitlines() if l.strip()))
print(f" how it checked: {' '.join(f['how'].split())}")
print(f" fix: {f['fix']}\n")
EOF
# gmp "<get_reports report_id=\"$REPORT\" details=\"1\" filter=\"severity>0 min_qod=70 rows=100\"/>" | python3 ~/greenbone/findings.py
3 findings 5.8 QoD 99% 80/tcp HTTP Debugging Methods (TRACE/TRACK) Enabled 2.6 QoD 80% general/tcp TCP Timestamps Information Disclosure 2.1 QoD 80% general/icmp ICMP Timestamp Reply Information Disclosure
Only three findings, and none about Apache? That’s Greenbone’s default view hiding things on purpose: min_qod=70 shows only findings it is at least 70% sure of. The TRACE finding is proven (QoD 99%: the scanner sent a TRACE request and the server echoed it straight back). The two “timestamp” items are minor information leaks. Hold that thought for step 7.
If your list has no 80/tcp line at all, the scanner never found the web server. On our first try that happened because the feed load had been interrupted, which left the “Full and fast” configuration without its port scanner. Rebuild Greenbone’s built-in configurations, then start the task again with the REPORT=$(gmp … start_task …) line from step 5. If findings show no CVEs: lines, the CVE database is missing; rebuild it (that one takes well over an hour; ours was still in its final stage after 110 minutes):
# docker compose -p greenbone exec -u gvmd gvmd gvmd --rebuild-gvmd-data=all # docker compose -p greenbone exec -T -u gvmd gvmd gvmd --rebuild-scap
07Lower the bar, then read one finding properly
Ask for every finding however unsure the scanner is (min_qod=0), then add a word to the helper to zoom in on one of them.
# gmp "<get_reports report_id=\"$REPORT\" details=\"1\" filter=\"severity>0 min_qod=0 rows=100\"/>" | python3 ~/greenbone/findings.py # gmp "<get_reports report_id=\"$REPORT\" details=\"1\" filter=\"severity>0 min_qod=0 rows=100\"/>" | python3 ~/greenbone/findings.py Traversal
Now there are 24 findings. Ours, trimmed:
24 findings 9.8 QoD 30% 80/tcp Apache HTTP Server 2.4.0 - 2.4.55 HTTP Request Smuggling Vulnerability 9.8 QoD 30% 80/tcp Apache HTTP Server 2.4.49 - 2.4.50 Directory Traversal / RCE Vulnerabi 9.8 QoD 30% 80/tcp Apache HTTP Server 2.4.49 Multiple Vulnerabilities - Linux 9.8 QoD 30% 80/tcp Apache HTTP Server < 2.4.54 Multiple Vulnerabilities - Linux 9.8 QoD 30% 80/tcp Apache HTTP Server < 2.4.60 Multiple Vulnerabilities - Linux 9.8 QoD 30% 80/tcp Apache HTTP Server <= 2.4.51 Buffer Overflow Vulnerability - Linux 9.8 QoD 30% 80/tcp Apache HTTP Server <= 2.4.52 Multiple Vulnerabilities - Linux 9.0 QoD 30% 80/tcp Apache HTTP Server < 2.4.55 Multiple Vulnerabilities - Linux 8.2 QoD 30% 80/tcp Apache HTTP Server 2.4.7 - 2.4.51 Multiple Vulnerabilities - Linux ... 5.8 QoD 99% 80/tcp HTTP Debugging Methods (TRACE/TRACK) Enabled ... 2.6 QoD 80% general/tcp TCP Timestamps Information Disclosure 2.1 QoD 80% general/icmp ICMP Timestamp Reply Information Disclosure
And the detail view of the one we’ll follow through the rest of the lab:
Apache HTTP Server 2.4.49 - 2.4.50 Directory Traversal / RCE Vulnerability - Linux severity 9.8 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H QoD 30% CVEs: none listed what it saw: Installed version: 2.4.49 / Fixed version: 2.4.51 / Installation / path / port: 80/tcp how it checked: Checks if a vulnerable version is present on the target host. fix: Update to version 2.4.51 or later.
Why “CVEs: none listed”? Greenbone links each test to its CVE IDs through its CVE database, and ours was still rebuilding after the interrupted feed load (see the note in step 6), so our run printed none. The finding still gives you what you need: the affected versions and the fixed version, 2.4.51. Apache’s own security page for 2.4 lists every CVE by the version that fixed it; for this finding that’s CVE-2021-42013, the incomplete fix for CVE-2021-41773.
Why were 21 Apache findings hidden? Look at QoD 30% and how it checked: Greenbone read the version from the Server: header you saw in step 4 and compared it with the versions known to be affected. It rates that only 30% sure because Linux distributions such as Ubuntu and Red Hat often fix (“backport”) a security bug without changing the version number, so on a distro-packaged server a version-only match is often a false positive. Hiding them by default means fewer false alarms, at the risk of false negatives, which is exactly what happened in step 6.
So are these real? Confirm before you act. In step 4, httpd -v inside the container also said 2.4.49, and this is Apache’s own build, not a patched distro package. So these are true positives. Real still doesn’t always mean exploitable: Apache’s default setting, Require all denied on the whole filesystem, blocks the classic version of the path traversal attack on this exact setup. That’s luck, not a fix: one changed line in the config and the door opens.
Credentialed vs uncredentialed. Your scan was uncredentialed: it looked at the server from the network, like an outsider, and mostly had to trust what the server said about itself. A credentialed scan is given a login (for example SSH on Linux or an admin account on Windows), so it can read the real installed package list and settings. It finds far more, with far fewer false positives, and it is what most organizations run on their own servers. Agent vs agentless is the same question from another angle: an agent is a small program installed on each machine that checks itself and reports in, which suits laptops that are rarely on the office network; agentless scanning, like this lab, reaches out over the network and installs nothing.
08Look it up: CVSS, EPSS and the KEV catalog
Take six CVEs behind these findings (from Apache’s security page, matched by the fixed versions the scan reported) and ask three free public sources about them: the US National Vulnerability Database (NVD) for the CVSS base score, FIRST’s EPSS service for the chance of exploitation, and CISA’s KEV catalog for proof that attackers are already using it. The sleep 7 keeps you under NVD’s limit for visitors without an API key.
# CVES="CVE-2021-41773,CVE-2021-42013,CVE-2021-44790,CVE-2022-31813,CVE-2023-25690,CVE-2024-38475" # for c in ${CVES//,/ }; do curl -s "https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=$c" | jq -r --arg c "$c" '.vulnerabilities[0].cve.metrics.cvssMetricV31[0].cvssData | "\($c) CVSS \(.baseScore) \(.baseSeverity) \(.vectorString)"'; sleep 7; done # curl -s "https://api.first.org/data/v1/epss?cve=$CVES" | jq -r '.data[] | "\(.cve) EPSS \(.epss) percentile \(.percentile)"' # curl -s https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json -o kev.json # jq -r '"KEV catalog \(.catalogVersion): \(.count) entries"' kev.json # jq -r --arg c "$CVES" '.vulnerabilities[] | select(.cveID as $id | $c | split(",") | index($id)) | "\(.cveID) in KEV since \(.dateAdded) ransomware: \(.knownRansomwareCampaignUse)"' kev.json
CVE-2021-41773 CVSS 9.8 CRITICAL CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H CVE-2021-42013 CVSS 9.8 CRITICAL CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H CVE-2021-44790 CVSS 9.8 CRITICAL CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H CVE-2022-31813 CVSS 9.8 CRITICAL CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H CVE-2023-25690 CVSS 9.8 CRITICAL CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H CVE-2024-38475 CVSS 9.1 CRITICAL CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N CVE-2024-38475 EPSS 0.999570000 percentile 0.999750000 CVE-2023-25690 EPSS 0.845080000 percentile 0.997020000 CVE-2022-31813 EPSS 0.035430000 percentile 0.893300000 CVE-2021-44790 EPSS 0.968400000 percentile 0.998870000 CVE-2021-42013 EPSS 0.999640000 percentile 0.999760000 CVE-2021-41773 EPSS 0.999920000 percentile 0.999860000 KEV catalog 2026.11.04: 1768 entries CVE-2024-38475 in KEV since 2025-05-01 ransomware: Unknown CVE-2021-42013 in KEV since 2021-11-03 ransomware: Known CVE-2021-41773 in KEV since 2021-11-03 ransomware: Known
Reading it. CVSS calls every one of them critical (9.1 or 9.8), so CVSS alone can’t tell you where to start. The vector AV:N/AC:L/PR:N/UI:N means: reachable over the network, easy, no login needed, no user has to click anything; C:H/I:H/A:H means high damage to confidentiality, integrity and availability. EPSS pulls them apart: 0.99992 for CVE-2021-41773 but 0.035 (3.5%) for CVE-2022-31813. A percentile of 0.9999 means the score is higher than 99.99% of all CVEs. KEV settles it: three are confirmed exploited in real attacks, and two of those are known to be used by ransomware gangs.
Prefer a browser? The same facts are on the NVD page for CVE-2021-41773, CISA’s KEV catalog search and FIRST’s EPSS site. EPSS scores are recalculated every day, so yours will be slightly different.
09Decide what to fix first
Scores describe the flaw. Your decision also needs context: is the risky feature even switched on? Ask the old image which of the relevant Apache modules it loads by default:
# docker run --rm httpd:2.4.49 grep -E "^#?LoadModule (lua|proxy|proxy_http|rewrite|cgid)_module" /usr/local/apache2/conf/httpd.conf
#LoadModule lua_module modules/mod_lua.so #LoadModule proxy_module modules/mod_proxy.so #LoadModule proxy_http_module modules/mod_proxy_http.so #LoadModule rewrite_module modules/mod_rewrite.so
The # at the start of each line means the module is switched off. Put it all together:
| Order | Finding | CVSS | EPSS | KEV | Why it sits here |
|---|---|---|---|---|---|
| 1 | CVE-2021-41773 and CVE-2021-42013: path traversal (reading files outside the website), code execution if CGI is on. Scan: “2.4.49 Multiple” and “2.4.49 - 2.4.50 Directory Traversal” | 9.8 | 0.9999 | Yes, ransomware | Exploited for years, in Apache’s core (no extra module needed), and our exact version |
| 2 | CVE-2024-38475: mod_rewrite mapping URLs to files it shouldn’t. Scan: “< 2.4.60 Multiple” | 9.1 | 0.9996 | Yes | Exploited, but mod_rewrite is switched off here, so less urgent on this box |
| 3 | CVE-2021-44790: buffer overflow in mod_lua. Scan: “<= 2.4.51 Buffer Overflow” | 9.8 | 0.968 | No | Very likely to be exploited somewhere, but mod_lua is off by default |
| 4 | CVE-2023-25690: request smuggling through mod_proxy and mod_rewrite. Scan: “2.4.0 - 2.4.55 HTTP Request Smuggling” | 9.8 | 0.845 | No | Needs two modules that are both off here |
| 5 | CVE-2022-31813: mod_proxy can drop a header some apps use for IP-based access rules. Scan: “< 2.4.54 Multiple” | 9.8 | 0.035 | No | Same “critical” CVSS as number 1, yet the lowest chance of exploitation, the module is off, and Apache itself rates it “low” |
| Later | TRACE enabled; TCP and ICMP timestamps | 5.8, 2.6, 2.1 | – | – | Settings, not version bugs; low risk; fix or accept in the next window |
How the order was decided. KEV first: someone is already exploiting those, so assume your turn will come. Then high EPSS, then the rest by CVSS. Context moves things within that: asset value (a shop taking card payments outranks a test box), exposure (facing the internet or only reachable inside), and compensating controls that are already in place, such as a module being switched off or a firewall rule in front. Compensating controls buy time and belong in your risk register; they don’t close the finding.
The twist. Here one upgrade fixes all 21 Apache findings at once, so the real answer is simply “upgrade today”. Ranking earns its keep when you have hundreds of machines, each needing a different fix, and only one maintenance window this week.
CVSS told me everything was critical. EPSS told me which critical. KEV told me which critical was already on fire. — Nyx
10Fix it: upgrade the server
The real fix for an outdated server is to update it. Here that means throwing the old container away and starting the current Apache 2.4 release in its place, with the same name and address so the scan target still points at it.
# docker rm -f oldweb # docker run -d --name oldweb --network vulnlab --ip 172.30.99.10 httpd:2.4 # docker exec oldweb httpd -v
oldweb c44825e403497be8016d0e71296f9bafa6cbf1a8337f2364807ecaeadc67085c Server version: Apache/2.4.69 (Unix) Server built: Oct 28 2026 01:15:47
On a normal server the same step is your package manager or Windows Update, tested on one machine first and done in an agreed maintenance window. If you can’t patch yet, a compensating control lowers the risk in the meantime: block the port at the firewall, put the site behind a web application firewall, or switch the service off if nobody needs it.
11Rescan to prove the fix
A fix you haven’t verified is a guess. Run the same task again. Greenbone keeps the first report and starts a new one, so save the new report’s ID, wait for Done as in step 6, then list everything in the new report, however unsure.
# REPORT2=$(gmp "<start_task task_id=\"$TASK\"/>" | grep -o '<report_id>[^<]*' | cut -d'>' -f2) # gmp "<get_tasks task_id=\"$TASK\"/>" | grep -E "<status>|<progress>" | head -2 # gmp "<get_reports report_id=\"$REPORT2\" details=\"1\" filter=\"severity>0 min_qod=0 rows=100\"/>" | python3 ~/greenbone/findings.py
The rescan took about 7 minutes. Even with the bar at zero, every Apache finding is gone:
3 findings 5.8 QoD 99% 80/tcp HTTP Debugging Methods (TRACE/TRACK) Enabled 2.6 QoD 80% general/tcp TCP Timestamps Information Disclosure 2.1 QoD 80% general/icmp ICMP Timestamp Reply Information Disclosure
What’s left, and why. The upgrade fixed the version problems. The three survivors are about settings and behavior, which a new version doesn’t change: Apache still answers the old TRACE debugging method (turn it off with the line TraceEnable off in its config), and the system answers timestamp requests that leak a little information about it. That’s normal. Fix what is cheap, and formally accept the rest with a reason and a review date. The two reports side by side are your evidence for the change ticket.
12Clean up
Remove the target and its network, then stop Greenbone. The last command also deletes Greenbone’s stored data (feeds, database and reports); leave it out if you want to scan again later without the long feed load.
# docker rm -f oldweb # docker network disconnect vulnlab greenbone-ospd-openvas-1 # docker network rm vulnlab # docker compose -p greenbone down # docker compose -p greenbone down -v
Not tested as written. On our test machine the Linux system was restarted from outside just before this step, which stopped every container, so we cleaned up our copy directly instead. These five lines are the standard Docker clean-up for what this lab created; if one reports that something is already gone, that is fine. The downloaded images (the list from step 1) still take about 12 GB after this; Docker’s own documentation covers removing images you no longer need.
What you learned
- find, rank, fix, verify — vulnerability management is a loop, and the rescan is what closes it
- CVSS — how bad a flaw is in general; a 9.8 tells you severity, not urgency
- EPSS and KEV — how likely exploitation is, and proof that it is already happening; KEV jumps the queue
- context — asset value, exposure and compensating controls change the order, but they buy time rather than replace the fix
- QoD and false positives — version-based findings need confirming; credentialed scans and agents see more than a banner
Tried it? Tell us
Nova built this lab from Steven’s ideas, and we ran steps 1 to 11 before publishing (the clean-up step is marked as untested). If a step broke, behaved differently for you, or you know a better way, tell us. Ideas for the next lab are welcome too — no promises on timing.
NextOpenSCAP: check a system against a security baseline→
Go deeper
- Linux for Defenders 6: packages and patching — keeping a real server’s software current
- Risk management explained — accept, mitigate, transfer, avoid, and putting a number on it
- Docker security lab — why an old image is a vulnerability you shipped yourself
- CompTIA Security+ roadmap — every free lesson and lab by exam domain
Original walkthrough. Steps 1 to 11 were run on Ubuntu 24.04 (WSL2, kernel 6.6) with Docker Engine 29.9.3 and Compose 2.40.3, using Greenbone Community Edition containers from Greenbone’s own compose file (gvmd 26.43, OpenVAS scanner 23.51, ospd-openvas 22.10, feed of 4 Nov 2026) on 4–5 Nov 2026; outputs are from that run, trimmed where marked with .... Step 12’s clean-up lines were not run as written (see the note there), and our CVE database never finished rebuilding, so our findings show no CVE lines. CVSS, EPSS and KEV results were looked up live on 4 Nov 2026; EPSS changes daily. If something doesn’t match what your machine does, tell me and I will fix it.