Repository: intel/cve-bin-tool

Issue:
fix: gcc checker does not detect multiple versions in a file
### Description

As briefly discussed in #4058 ([this comment](https://github.com/intel/cve-bin-tool/issues/4058#issuecomment-2132269471) and further on), it seems like the `gcc` checker only reports the first version it found in a file (`strings` output order-wise). This is not a SBOM library issue as in #4058, it is indeed a lower-level checker part that reports only one version entry.

While preparing this report, I looked at the code and I'm no longer sure this is strictly a bug, as the `get_version()` code for the `Checker` base class does not seem to take this case into account at all. There _is_ some preparation for multiple hits in `VersionScanner.run_checkers()` (lines 262-266 in `version_scanner.py` as of right now), but by quick inspection there's no checker that returns multiple results (certainly not the `gcc` one). So please feel free to reclassify this as a feature request.

I have noticed that when running `cve-bin-tool` on a pintool - an analysis program that runs within the Intel Pin binary instrumentation and analysis framework. There's probably a way to create a smaller binary for tests (e.g., linking together several object files produced by different compiler versions), but it was simpler and faster for me to just use the Pin example.

### To reproduce

Steps to reproduce the behaviour:
1. Download and unpack Intel Pin 3.30: [Intel Pin release downloads](https://www.intel.com/content/www/us/en/developer/articles/tool/pin-a-binary-instrumentation-tool-downloads.html)
2. Unpack the downloaded file and `cd` to `source/tools/MyPinTool` directory within the unpacked one
3. Run `make`
4. The compilation will produce a `MyPinTool.so` file in `obj-intel64` subdirectory. A ready-made version is also attached ([MyPinTool.tar.gz](https://github.com/user-attachments/files/15809079/MyPinTool.tar.gz)).
5. Note that `strings` shows two GCC versions (the older one is from files linked into `MyPinTool` from Pin's precompiled files, and the newer one is from my host system, Fedora 40):
   ```console
   $ strings obj-intel64/MyPinTool.so |grep GNU
   GCC: (GNU) 11.2.0
   GCC: (GNU) 14.1.1 20240607 (Red Hat 14.1.1-5)
   __GNU_EH_FRAME_HDR
   ``` 
7. Scan the file using `cve-bin-tool` e.g., like the below, and observe that only one version is reported:
```console
$ cve-bin-tool -l debug -d CURL,EPSS,GAD,OSV,PURL2CPE,REDHAT,RSD --offline --disable-validation-check --disable-version-check source/tools/MyPinTool/obj-intel64/MyPinTool.so
<...>
DEBUG    cve_bin_tool.VersionScanner - Scanning file: /tmp/pin-3.30-98830-g1d7b601b3-gcc-linux/source/tools/MyPinTool/obj-intel64/MyPinTool.so                                                               version_scanner.py:215
           INFO     cve_bin_tool - filename = /tmp/pin-3.30-98830-g1d7b601b3-gcc-linux/source/tools/MyPinTool/obj-intel64/MyPinTool.so                                                                                  version_scanner.py:256
           DEBUG    cve_bin_tool.VersionScanner - /tmp/pin-3.30-98830-g1d7b601b3-gcc-linux/source/tools/MyPinTool/obj-intel64/MyPinTool.so contains gcc 11.2.0                                                          version_scanner.py:283
           DEBUG    cve_bin_tool - ProductInfo(vendor='gnu', product='gcc', version='11.2.0', location='/usr/bin/gcc'): /tmp/pin-3.30-98830-g1d7b601b3-gcc-linux/source/tools/MyPinTool/obj-intel64/MyPinTool.so                   cli.py:1042
           DEBUG    cve_bin_tool.CVEScanner - Known CVEs in ProductInfo(vendor='gnu', product='gcc', version='11.2.0', location='/usr/bin/gcc')                                                                             cve_scanner.py:281
           INFO     cve_bin_tool.CVEScanner - 1 CVE(s) in gnu.gcc version 11.2.0                                                                                                                                            cve_scanner.py:285
[19:01:24] DEBUG    cve_bin_tool.VersionScanner - Done scanning file: /tmp/pin-3.30-98830-g1d7b601b3-gcc-linux/source/tools/MyPinTool/obj-intel64/MyPinTool.so                                                          version_scanner.py:299
           INFO     cve_bin_tool - Overall CVE summary:                                                                                                                                                                            cli.py:1069
           INFO     cve_bin_tool - There are 1 products with known CVEs detected                                                                                                                                                   cli.py:1070
           INFO     cve_bin_tool - Known CVEs in ('gnu.gcc', '11.2.0'):                                                                                                                                                            cli.py:1081
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃                                                                                                     CVE BINARY TOOL version: 3.3.1dev0                                                                                                     ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

 • Report Generated: 2024-06-12  19:01:24                                                                                                                                                                                                     
 • Time of last update of CVE Data: 2024-06-12  18:34:39                                                                                                                                                                                      
╭─────────────╮
│ CVE SUMMARY │
╰─────────────╯
┏━━━━━━━━━━┳━━━━━━━┓
┃ Severity ┃ Count ┃
┡━━━━━━━━━━╇━━━━━━━┩
│ CRITICAL │ 0     │
│ HIGH     │ 0     │
│ MEDIUM   │ 1     │
│ LOW      │ 0     │
│ UNKNOWN  │ 0     │
└──────────┴───────┘
╭─────────────╮
│ CPE SUMMARY │
╰─────────────╯
┏━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓
┃ Vendor ┃ Product ┃ Version ┃ Latest Upstream Stable Version ┃ CRITICAL CVEs Count ┃ HIGH CVEs Count ┃ MEDIUM CVEs Count ┃ LOW CVEs Count ┃ UNKNOWN CVEs Count ┃ TOTAL CVEs Count ┃
┡━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩
│ gnu    │ gcc     │ 11.2.0  │ UNKNOWN (offline mode)         │ 0                   │ 0               │ 1                 │ 0              │ 0                  │ 1                │
└────────┴─────────┴─────────┴────────────────────────────────┴─────────────────────┴─────────────────┴───────────────────┴────────────────┴────────────────────┴──────────────────┘
╭─────────────────╮
│  NewFound CVEs  │
╰─────────────────╯
┏━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┓
┃ Vendor ┃ Product ┃ Version ┃ CVE Number    ┃ Source ┃ Severity ┃ Score (CVSS Version) ┃
┡━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━┩
│ gnu    │ gcc     │ 11.2.0  │ CVE-2023-4039 │ NVD    │ MEDIUM   │ 4.8 (v3)             │
└────────┴─────────┴─────────┴───────────────┴────────┴──────────┴──────────────────────┘
┏━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ Vendor ┃ Product ┃ Version ┃ Root               ┃ Filename     ┃
┡━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
│ gnu    │ gcc     │ 11.2.0  │ Root 0 (see below) │ MyPinTool.so │
└────────┴─────────┴─────────┴────────────────────┴──────────────┘


Root 0 : /tmp/pin-3.30-98830-g1d7b601b3-gcc-linux/source/tools/MyPinTool/obj-intel64
╭───────────────────────────────────────────────╮
│  Products with No Identified Vulnerabilities  │
╰───────────────────────────────────────────────╯
┏━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┓
┃ Vendor ┃ Product ┃ Version ┃
┡━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━┩
└────────┴─────────┴─────────┘
```

Expected behaviour: both versions, 11.2.0 and 14.1.1, are detected and reported
Actual behaviour: only the first one is

### Version/platform info

* Version of CVE-bin-tool( e.g. output of `cve-bin-tool --version`): current `main` as of commit e9f1ea8c8fdff0cb8e0f25142b39db55adc6556b
* Installed from: Github
* Operating system: Fedora 40
* Python version: 3.12.3
* Running in any particular CI environment we should know about?  (e.g. Github Actions): no

Hints from issue comments:
@alext-w: Any chance this will get looked at any time soon? If not I'll probably need to dive in myself :)@terriko: I'm on the road and only popping in to keep code reviews and merges moving, so I likely won't work on it in the next 2 weeks and I'm guessing our GSoC folk are busy with their projects.  Feel free to dive in!@stvml: OSS Hackathon Team 1 will take a shot at this one.

Solution patch:
diff --git a/cve_bin_tool/checkers/README.md b/cve_bin_tool/checkers/README.md
index 688b8dbb..5ca7e45e 100644
--- a/cve_bin_tool/checkers/README.md
+++ b/cve_bin_tool/checkers/README.md
@@ -311,7 +311,7 @@ That regex might look like this: `3\?Xiph.Org libVorbis ([0-9]+\.[0-9]+\.[0-9]+)
 
 > If you can't get a signature match using just regex you may end up needing to
 > overwrite the
-> [`get_version()`](https://github.com/intel/cve-bin-tool/blob/main/cve_bin_tool/checkers/__init__.py#L120-L132)
+> [`get_versions()`](https://github.com/intel/cve-bin-tool/blob/main/cve_bin_tool/checkers/__init__.py#L120-L132)
 > method for the checker, but that should be a last resort if you can't find a
 > regex that works for `VERSION_PATTERNS`.
 >
@@ -322,10 +322,10 @@ That regex might look like this: `3\?Xiph.Org libVorbis ([0-9]+\.[0-9]+\.[0-9]+)
 > 1.3.7. While this was a nice example for how one might find a signature, it
 > in the end is not all the work that is needed to create a checker for
 > libvorbis. We need to make sure that any checker we develop has a
-> `get_version()` function which works for versions of the software which have
+> `get_versions()` function which works for versions of the software which have
 > CVEs. If not overridden in a subclass the Checker base class implements a
-> `get_version()` method which will use regex to determine the version (as
-> described above). In the case of libvorbis a custom `get_version()` function
+> `get_versions()` method which will use regex to determine the version (as
+> described above). In the case of libvorbis a custom `get_versions()` function
 > is likely needed, this is because the signature we found is not in the 1.2.0
 > version, where the CVE is found.
 
@@ -503,25 +503,26 @@ You can set an arbitrary number of workers. A good rule of thumb is to specify n
 
 The CVE-bin-checker works by extracting strings from binaries and determining
 if a given library has been compiled into the binary. For this, Checker class
-contains two methods: 1) `guess_contains()` and 2) `get_version()`.
+contains two methods: 1) `guess_contains()` and 2) `get_versions()`.
 
 1. `guess_contains()` method takes list of extracted string lines as an input and
 return True if it finds any of the `CONTAINS_PATTERNS` on any line from the
 lines.
-2. `get_version()` method takes list of extracted string lines and the filename as
-inputs and returns information about whether the binary contains the library
-in question, is a copy of the library in question, and if either of those are
-true it also returns a version string. If the binary does not contain the
-library, this function returns an empty dictionary.
+2. `get_versions()` method takes a list of extracted string lines and the filename as
+inputs and returns a VersionMatchInfo. This object contains whether a match was
+found in the filename, whether a match was found in the contents of the file, and a list
+of version strings found in the file. If no matches are found, both booleans will be false 
+and the list of version strings will be empty. If a match is found but no valid version 
+strings are matched, the list of version strings will contain only "UNKNOWN".
 
-If `curl` product is being scanned, `get_version()` method of CurlChecker will
-return following dictionary.
+For example, if the `curl` product is being scanned, the `get_versions()` method of CurlChecker will
+return the following object:
 
 ```json
 {
-  "is_or_contains": "is",
-  "modulename": "curl",
-  "version": "6.41.0"
+  "matched_filename": true,
+  "matched_contains": true,
+  "versions": ["6.41.0"]
 }
 ```
 
diff --git a/cve_bin_tool/checkers/__init__.py b/cve_bin_tool/checkers/__init__.py
index 1fa5d0e8..6a42e33d 100644
--- a/cve_bin_tool/checkers/__init__.py
+++ b/cve_bin_tool/checkers/__init__.py
@@ -9,7 +9,7 @@ import re
 import sys
 
 from cve_bin_tool.error_handler import InvalidCheckerError
-from cve_bin_tool.util import regex_find
+from cve_bin_tool.util import VersionMatchInfo, regex_find
 
 if sys.version_info >= (3, 10):
     from importlib import metadata as importlib_metadata
@@ -475,21 +475,22 @@ class Checker(metaclass=CheckerMetaClass):
             return True
         return False
 
-    def get_version(self, lines, filename):
-        version_info = dict()
+    def get_versions(self, lines: str, filename: str) -> VersionMatchInfo:
+
+        version_details = VersionMatchInfo()
 
         if any(pattern.match(filename) for pattern in self.FILENAME_PATTERNS):
-            version_info["is_or_contains"] = "is"
+            version_details.matched_filename = True
 
-        if "is_or_contains" not in version_info and self.guess_contains(lines):
-            version_info["is_or_contains"] = "contains"
+        if self.guess_contains(lines):
+            version_details.matched_contains = True
 
-        if "is_or_contains" in version_info:
-            version_info["version"] = regex_find(
+        if version_details.matched_filename or version_details.matched_contains:
+            version_details.versions = regex_find(
                 lines, self.VERSION_PATTERNS, self.IGNORE_PATTERNS
             )
 
-        return version_info
+        return version_details
 
 
 BUILTIN_CHECKERS = {
diff --git a/cve_bin_tool/checkers/bind.py b/cve_bin_tool/checkers/bind.py
index f14b8006..b72140a9 100644
--- a/cve_bin_tool/checkers/bind.py
+++ b/cve_bin_tool/checkers/bind.py
@@ -28,6 +28,7 @@ class BindChecker(Checker):
         r"libisccc([-_]?(\d+\.)+\d.*)?\.so",
         r"libisccfg([-_]?(\d+\.)+\d.*)?\.so",
         r"libns([-_]?(\d+\.)+\d.*)?\.so",
+        r"libbind9([-_]?(\d+\.)+\d.*)?\.so",
     ]
     VERSION_PATTERNS = [
         r"version: BIND ([0-9]+\.[0-9]+\.[0-9]+)",  # for .rpm, .tgz, etc.
diff --git a/cve_bin_tool/checkers/curl.py b/cve_bin_tool/checkers/curl.py
index bb85ecd3..b995e2e6 100644
--- a/cve_bin_tool/checkers/curl.py
+++ b/cve_bin_tool/checkers/curl.py
@@ -13,6 +13,7 @@ RSS feed: http://www.cvedetails.com/vulnerability-feed.php?vendor_id=12682&produ
 
 Note: Some of the "first vulnerable in" data may not be entered correctly.
 """
+
 from cve_bin_tool.checkers import Checker
 
 
@@ -26,6 +27,8 @@ class CurlChecker(Checker):
         # r"error retrieving curl library information",
         # r"ignoring --proxy-capath, not supported by libcurl",
     ]
-    FILENAME_PATTERNS = [r"curl"]
+    FILENAME_PATTERNS = [
+        r"curl",
+    ]
     VERSION_PATTERNS = [r"\r?\ncurl[ -/]([678]+\.[0-9]+\.[0-9]+)"]
     VENDOR_PRODUCT = [("haxx", "curl")]
diff --git a/cve_bin_tool/checkers/ffmpeg.py b/cve_bin_tool/checkers/ffmpeg.py
index b3fc5255..c63f5768 100644
--- a/cve_bin_tool/checkers/ffmpeg.py
+++ b/cve_bin_tool/checkers/ffmpeg.py
@@ -11,6 +11,7 @@ https://www.cvedetails.com/vulnerability-list/vendor_id-3611/Ffmpeg.html
 
 Note: Some of the "first vulnerable in" data may not be entered correctly.
 """
+
 from cve_bin_tool.checkers import Checker
 
 
@@ -20,7 +21,12 @@ class FfmpegChecker(Checker):
         r"Codec '%s' is not recognized by FFmpeg.",
         r"Codec '%s' is known to FFmpeg, but no %s for it are available.",
     ]
-    FILENAME_PATTERNS = [r"ffmpeg"]
+    FILENAME_PATTERNS = [
+        # check if it's the library instead of the command line util
+        r"libffmpeg\.so(\.\d+)?",
+        # command line util
+        r"ffmpeg",
+    ]
     VERSION_PATTERNS = [
         r"%s version ([0-9]+\.[0-9]+\.[0-9]+)[a-zA-Z0-9 \(\)%~\-\r\n]*(?:avutil|FFmpeg)",
         r"FFmpeg version n?([0-9]+\.[0-9]+\.[0-9]+)",
diff --git a/cve_bin_tool/checkers/gstreamer.py b/cve_bin_tool/checkers/gstreamer.py
index eac30b3e..b575e460 100644
--- a/cve_bin_tool/checkers/gstreamer.py
+++ b/cve_bin_tool/checkers/gstreamer.py
@@ -8,6 +8,7 @@ References:
 https://www.cvedetails.com/vulnerability-list/vendor_id-9481/Gstreamer.html
 https://www.cvedetails.com/product/35669/Gstreamer-Project-Gstreamer.html?vendor_id=16047
 """
+
 from cve_bin_tool.checkers import Checker
 
 
@@ -15,7 +16,12 @@ class GstreamerChecker(Checker):
     CONTAINS_PATTERNS = [
         r"http://bugzilla.gnome.org/enter_bug.cgi?product=GStreamer",
     ]
-    FILENAME_PATTERNS = [r"gstreamer"]
+    FILENAME_PATTERNS = [
+        # check if it's the library instead of the command line util
+        r"libgstreamer\.so(\.\d+)?",
+        # command line util
+        r"gstreamer",
+    ]
     VERSION_PATTERNS = [
         r"((\d+\.)+\d+)[a-zA-Z \r\n]*GStreamer ",
         r"gstreamer[a-zA-Z \r\n]+((\d+\.)+\d+)",
diff --git a/cve_bin_tool/checkers/icu.py b/cve_bin_tool/checkers/icu.py
index 1950e28b..a58b1af6 100644
--- a/cve_bin_tool/checkers/icu.py
+++ b/cve_bin_tool/checkers/icu.py
@@ -9,6 +9,7 @@ References:
 https://www.cvedetails.com/vulnerability-list/vendor_id-17477/Icu-project.html
 https://www.cvedetails.com/product/101097/Unicode-International-Components-For-Unicode.html?vendor_id=21486
 """
+
 from cve_bin_tool.checkers import Checker
 
 
@@ -20,7 +21,13 @@ class IcuChecker(Checker):
         r"is about 300kB larger than the ucadata-implicithan\.icu version\.",
         r"the ucadata-unihan\.icu version of the collation root data",
     ]
-    FILENAME_PATTERNS = [r"genrb", r"uconv"]
+    FILENAME_PATTERNS = [
+        # check if it's the library instead of the command line utils
+        r"international_components_for_unicode.o",
+        # command line utils
+        r"genrb",
+        r"uconv",
+    ]
     VERSION_PATTERNS = [
         r"icu(?:-|/)([0-9]+\.[0-9]+(\.[0-9]+)?)",
         r"ICU ([0-9]+\.[0-9]+(\.[0-9]+)?)",
diff --git a/cve_bin_tool/checkers/libcurl.py b/cve_bin_tool/checkers/libcurl.py
index bcfd96e9..5fbe595a 100644
--- a/cve_bin_tool/checkers/libcurl.py
+++ b/cve_bin_tool/checkers/libcurl.py
@@ -15,6 +15,8 @@ from cve_bin_tool.checkers import Checker
 
 class LibcurlChecker(Checker):
     CONTAINS_PATTERNS: list[str] = []
-    FILENAME_PATTERNS: list[str] = []
+    FILENAME_PATTERNS: list[str] = [
+        r"libcurl\.so(\.\d+)?",
+    ]
     VERSION_PATTERNS = [r"libcurl[ -/]([678]+\.[0-9]+\.[0-9]+)"]
     VENDOR_PRODUCT = [("haxx", "libcurl")]
diff --git a/cve_bin_tool/checkers/sqlite.py b/cve_bin_tool/checkers/sqlite.py
index 0696cd32..e6a92afd 100644
--- a/cve_bin_tool/checkers/sqlite.py
+++ b/cve_bin_tool/checkers/sqlite.py
@@ -6,7 +6,7 @@
 CVE checker for sqlite
 
 References:
-SQLLite version/sha1 data from https://www.sqlite.org/changes.html
+SQLite version/sha1 data from https://www.sqlite.org/changes.html
 
 CVE list: https://www.cvedetails.com/vulnerability-list/vendor_id-9237/product_id-16355/Sqlite-Sqlite.html
 
@@ -81,14 +81,14 @@ class SqliteChecker(Checker):
         # If that fails, find a signature that might indicate presence of sqlite
         return super().guess_contains(lines)
 
-    def get_version(self, lines, filename):
+    def get_versions(self, lines, filename):
         """returns version information for sqlite as found in a given file.
 
         The most correct way to do this is to search for the sha1 sums per release.
         Fedora rpms have a simpler SQLite version string.
         """
 
-        version_info = super().get_version(lines, filename)
+        version_info = super().get_versions(lines, filename)
 
         for mapping in self.VERSION_MAP:
             # Truncate last four characters as "If the source code has been edited
@@ -97,6 +97,6 @@ class SqliteChecker(Checker):
             # https://www.sqlite.org/c3ref/c_source_id.html
             if mapping[1][:-4] in lines:
                 # overwrite version with the version found by sha mapping
-                version_info["version"] = mapping[0]
+                version_info.versions.append(mapping[0])
 
         return version_info
diff --git a/cve_bin_tool/util.py b/cve_bin_tool/util.py
index da5198b2..d2d6f827 100644
--- a/cve_bin_tool/util.py
+++ b/cve_bin_tool/util.py
@@ -194,6 +194,30 @@ class ScanInfo(NamedTuple):
     file_path: str
 
 
+class VersionMatchInfo:
+    """
+    Class to hold version match information
+    attributes:
+        matched_filename: bool
+        matched_contains: bool
+        versions: list[str]
+    """
+
+    matched_filename: bool
+    matched_contains: bool
+    versions: list[str]
+
+    def __init__(
+        self,
+        matched_filename: bool = False,
+        matched_contains: bool = False,
+        versions: list[str] = [],
+    ):
+        self.matched_filename = matched_filename
+        self.matched_contains = matched_contains
+        self.versions = versions or []
+
+
 class VersionInfo(NamedTuple):
     """
     Class to hold version information of a product
@@ -241,23 +265,32 @@ class CVEData(DefaultDict[str, Union[List[CVE], Set[str]]]):
 
 def regex_find(
     lines: str, version_patterns: list[Pattern[str]], ignore: list[Pattern[str]]
-) -> str:
-    """Search a set of lines to find a match for the given regex"""
-    new_guess = ""
+) -> list[str]:
+    """Search a set of lines to find all matches for the given regex"""
+    versions = list()
 
     for pattern in version_patterns:
-        match = pattern.search(lines)
-        if match:
-            new_guess = match.group(1).strip()
-            for i in ignore:
-                if str(i) in str(new_guess) or str(new_guess) in str(i):
-                    new_guess = ""
-            break
-    if new_guess != "":
-        new_guess = new_guess.replace("_", ".")
-        return new_guess.replace("-", ".")
-    else:
-        return "UNKNOWN"
+        version_matches = pattern.finditer(lines)
+        for match in version_matches:
+            # before collecting a potential version number, ensure the version string isn't on the ignore list
+            if not check_ignored(match.string, ignore):
+                version = match.group(1).strip()
+                version = version.replace("_", ".").replace("-", ".")
+                versions.append(version)
+
+    # if we searched and found no matches, just return a one-element list containing "UNKNOWN"
+    if not versions:
+        versions.append("UNKNOWN")
+
+    return versions
+
+
+def check_ignored(possible_version_string: str, ignore: list[Pattern[str]]) -> bool:
+    """Check if a possible version should be ignored based on the ignore patterns."""
+    for pattern in ignore:
+        if pattern.search(possible_version_string):
+            return True
+    return False
 
 
 def inpath(binary: str) -> bool:
diff --git a/cve_bin_tool/version_scanner.py b/cve_bin_tool/version_scanner.py
index 4a5930ec..27f8dba0 100644
--- a/cve_bin_tool/version_scanner.py
+++ b/cve_bin_tool/version_scanner.py
@@ -136,7 +136,7 @@ class VersionScanner:
         self.logger.info(f'Language Checkers: {", ".join(self.language_checkers)}')
 
     def number_of_language_checkers(self) -> int:
-        """return the number of langauge checkers avaialble"""
+        """return the number of language checkers available"""
         return len(self.language_checkers)
 
     def is_executable(self, filename: str) -> tuple[bool, str | None]:
@@ -254,31 +254,19 @@ class VersionScanner:
         # tko
         for dummy_checker_name, checker in self.checkers.items():
             checker = checker()
-            result = checker.get_version(lines, filename)
-            # do some magic so we can iterate over all results, even the ones that just return 1 hit
-            if "is_or_contains" in result:
-                results = [dict()]
-                results[0] = result
-            else:
-                results = result
-
-            for result in results:
-                if "is_or_contains" in result:
-                    version = "UNKNOWN"
-                    if "version" in result and result["version"] != "UNKNOWN":
-                        version = result["version"]
-                    elif result["version"] == "UNKNOWN":
+            version_results = checker.get_versions(lines, filename)
+
+            if version_results.matched_filename or version_results.matched_contains:
+                for version in version_results.versions:
+                    if version == "UNKNOWN":
                         file_path = "".join(self.file_stack)
                         self.logger.debug(
                             f"{dummy_checker_name} was detected with version UNKNOWN in file {file_path}"
                         )
                     else:
-                        self.logger.error(f"No version info for {dummy_checker_name}")
-
-                    if version != "UNKNOWN":
                         file_path = "".join(self.file_stack)
                         self.logger.debug(
-                            f'{file_path} {result["is_or_contains"]} {dummy_checker_name} {version}'
+                            f"{file_path} matched {dummy_checker_name} {version} ({version_results.matched_filename=}, {version_results.matched_contains=})"
                         )
                         for vendor, product in checker.VENDOR_PRODUCT:
                             yield ScanInfo(
@@ -292,7 +280,7 @@ class VersionScanner:
     def clean_file_path(filepath: str) -> str:
         """Returns a cleaner filepath by removing temp path from filepath"""
 
-        # we'll recieve a filepath similar to
+        # we'll receive a filepath similar to
         # /temp/anything/extractable_filename.extracted/folders/inside/file
         # We'll return /folders/inside/file to be scanned
 


Test patch:
diff --git a/test/README.md b/test/README.md
index 702f5ee7..e19d0fc1 100644
--- a/test/README.md
+++ b/test/README.md
@@ -160,7 +160,7 @@ To test the filename mappings, rather than making a bunch of empty files, we're
     )
 ```
 
-The function test_filename_is will then load the checker you have specified (and fail spectacularly if you specify a checker that does not exist), try to run get_version() with an empty file content and the filename you specified, then check that it "is" something (as opposed to "contains") and that the modulename that get_version returns is in fact the expected_result you specified.
+The function test_filename_is will then load the checker you have specified (and fail spectacularly if you specify a checker that does not exist), try to run get_versions() with an empty file content and the filename you specified, then check that it "is" something (as opposed to "contains") and that the modulename that get_version returns is in fact the expected_result you specified.
 
 For ease of maintenance, please keep the parametrize list in alphabetical order when you add a new tests.
 
diff --git a/test/test_checkers.py b/test/test_checkers.py
index caaa9135..e1d833d2 100644
--- a/test/test_checkers.py
+++ b/test/test_checkers.py
@@ -61,7 +61,7 @@ class TestCheckerVersionParser:
             ("binutils", "dlltool", ["binutils"]),
             ("bzip2", "bzip2.so", ["bzip2"]),
             ("cups", "cupsd.so", ["cups"]),
-            ("curl", "libcurl.so.4", ["curl"]),
+            ("curl", "curl", ["curl"]),
             ("emacs", "emacs", ["emacs"]),
             ("emacs", "emacs-nox", ["emacs"]),
             ("emacs", "emacs-gtk", ["emacs"]),
@@ -80,7 +80,7 @@ class TestCheckerVersionParser:
             ("libdb", "libdb-2.0.so", ["libdb"]),
             ("libexpat", "libexpat.so", ["libexpat"]),
             ("libgcrypt", "libgcrypt.so.1.0", ["libgcrypt"]),
-            ("libjpeg", "libjpg.so.2.0", ["libjpeg-turbo"]),
+            ("libjpeg-turbo", "libjpg.so.2.0", ["libjpeg-turbo"]),
             ("libnss", "libnss.so.1.0", ["nss"]),
             ("libtiff", "libtiff.so.1.0", ["tiff"]),
             ("lighttpd", "lighttpd", ["lighttpd"]),
@@ -135,16 +135,8 @@ class TestCheckerVersionParser:
                 Checker = checker.load()
                 checker = Checker()
 
-                result = checker.get_version("", file_name)
-
-                if "is_or_contains" in result:
-                    results = [dict()]
-                    results[0] = result
-                else:
-                    results = result
-
-                for result, expected_result in zip(results, expected_results):
-                    assert result["is_or_contains"] == "is"
+                version_results = checker.get_versions("", file_name)
+                assert version_results.matched_filename
 
     class MyFakeChecker(Checker):
         CONTAINS_PATTERNS: list[str] = []
@@ -157,8 +149,8 @@ class TestCheckerVersionParser:
     lines = string.split("\n")
     checker = MyFakeChecker()
 
-    result1 = checker.get_version(lines[1], "")
-    assert result1["version"] == "UNKNOWN"
+    result1 = checker.get_versions(lines[1], "")
+    assert result1.versions[0] == "UNKNOWN"
 
-    result2 = checker.get_version(lines[2], "")
-    assert result2["version"] == "5.8"
+    result2 = checker.get_versions(lines[2], "")
+    assert result2.versions[0] == "5.8"