diff --git a/docs/node-scraper-external/ext_nodescraper_plugins/sample/sample_collector.py b/docs/node-scraper-external/ext_nodescraper_plugins/sample/sample_collector.py index 3a860ab9..85f82912 100644 --- a/docs/node-scraper-external/ext_nodescraper_plugins/sample/sample_collector.py +++ b/docs/node-scraper-external/ext_nodescraper_plugins/sample/sample_collector.py @@ -1,3 +1,5 @@ +from typing import Optional + from nodescraper.base import InBandDataCollector from nodescraper.enums import ExecutionStatus from nodescraper.models import TaskResult @@ -9,7 +11,7 @@ class SampleCollector(InBandDataCollector[SampleDataModel, None]): DATA_MODEL = SampleDataModel - def collect_data(self, args=None) -> tuple[TaskResult, SampleDataModel | None]: + def collect_data(self, args=None) -> tuple[TaskResult, Optional[SampleDataModel]]: sample_data = SampleDataModel(some_str="example123") self.result.message = "Collector ran successfully" self.result.status = ExecutionStatus.OK diff --git a/nodescraper/plugins/inband/kernel/analyzer_args.py b/nodescraper/plugins/inband/kernel/analyzer_args.py index e8f4cd61..eab86c95 100644 --- a/nodescraper/plugins/inband/kernel/analyzer_args.py +++ b/nodescraper/plugins/inband/kernel/analyzer_args.py @@ -61,4 +61,4 @@ def build_from_model(cls, datamodel: KernelDataModel) -> "KernelAnalyzerArgs": Returns: KernelAnalyzerArgs: instance of analyzer args class """ - return cls(exp_kernel=datamodel.kernel_version) + return cls(exp_kernel=datamodel.kernel_info) diff --git a/nodescraper/plugins/inband/kernel/kernel_collector.py b/nodescraper/plugins/inband/kernel/kernel_collector.py index e84973ff..a9ac81ad 100644 --- a/nodescraper/plugins/inband/kernel/kernel_collector.py +++ b/nodescraper/plugins/inband/kernel/kernel_collector.py @@ -23,6 +23,7 @@ # SOFTWARE. # ############################################################################### +import re from typing import Optional from nodescraper.base import InBandDataCollector @@ -37,7 +38,31 @@ class KernelCollector(InBandDataCollector[KernelDataModel, None]): DATA_MODEL = KernelDataModel CMD_WINDOWS = "wmic os get Version /Value" - CMD = "sh -c 'uname -r'" + CMD = "sh -c 'uname -a'" + + def _parse_kernel_version(self, uname_a: str) -> Optional[str]: + """Extract the kernel release from `uname -a` output. + + Args: + uname_a (str): The full output string from the `uname -a` command. + + Returns: + Optional[str]: The parsed kernel release (e.g., "5.13.0-30-generic") + if found, otherwise None. + """ + if not uname_a: + return None + + result = uname_a.strip().split() + if len(result) >= 3: + return result[2] + + # if some change in output look for a version-like string (e.g. 4.18.0-553.el8_10.x86_64) + match = re.search(r"\d+\.\d+\.\d+[\w\-\.]*", uname_a) + if match: + return match.group(0) + + return None def collect_data( self, @@ -51,16 +76,28 @@ def collect_data( """ kernel = None + kernel_info = None + if self.system_info.os_family == OSFamily.WINDOWS: res = self._run_sut_cmd(self.CMD_WINDOWS) if res.exit_code == 0: + kernel_info = res.stdout kernel = [line for line in res.stdout.splitlines() if "Version=" in line][0].split( "=" )[1] else: res = self._run_sut_cmd(self.CMD) if res.exit_code == 0: - kernel = res.stdout + kernel_info = res.stdout + kernel = self._parse_kernel_version(kernel_info) + if not kernel: + self._log_event( + category=EventCategory.OS, + description="Could not extract kernel version from 'uname -a'", + data={"command": res.command, "exit_code": res.exit_code}, + priority=EventPriority.ERROR, + console_log=True, + ) if res.exit_code != 0: self._log_event( @@ -71,8 +108,9 @@ def collect_data( console_log=True, ) - if kernel: - kernel_data = KernelDataModel(kernel_version=kernel) + if kernel_info and kernel: + + kernel_data = KernelDataModel(kernel_info=kernel_info, kernel_version=kernel) self._log_event( category="KERNEL_READ", description="Kernel version read", @@ -82,6 +120,10 @@ def collect_data( else: kernel_data = None - self.result.message = f"Kernel: {kernel}" if kernel else "Kernel not found" - self.result.status = ExecutionStatus.OK if kernel else ExecutionStatus.ERROR + self.result.message = ( + "Kernel not found" + if not kernel_info + else f"Kernel info: {kernel_info} | Kernel version: {kernel if kernel else 'Kernel version not found'}" + ) + self.result.status = ExecutionStatus.OK if kernel_info else ExecutionStatus.ERROR return self.result, kernel_data diff --git a/nodescraper/plugins/inband/kernel/kerneldata.py b/nodescraper/plugins/inband/kernel/kerneldata.py index edaa30e5..45f521eb 100644 --- a/nodescraper/plugins/inband/kernel/kerneldata.py +++ b/nodescraper/plugins/inband/kernel/kerneldata.py @@ -23,8 +23,10 @@ # SOFTWARE. # ############################################################################### + from nodescraper.models import DataModel class KernelDataModel(DataModel): + kernel_info: str kernel_version: str diff --git a/test/unit/plugin/test_kernel_analyzer.py b/test/unit/plugin/test_kernel_analyzer.py index 7d9c7d0a..81785abd 100644 --- a/test/unit/plugin/test_kernel_analyzer.py +++ b/test/unit/plugin/test_kernel_analyzer.py @@ -35,7 +35,10 @@ @pytest.fixture def model_obj(): - return KernelDataModel(kernel_version="5.13.0-30-generic") + return KernelDataModel( + kernel_info="Linux MockSystem 5.13.0-30-generic #1 XYZ Day Month 10 15:19:13 EDT 2024 x86_64 x86_64 x86_64 GNU/Linux", + kernel_version="5.13.0-30-generic", + ) @pytest.fixture @@ -118,14 +121,14 @@ def test_invalid_kernel_config(system_info, model_obj, config): def test_match_regex(system_info, model_obj): - args = KernelAnalyzerArgs(exp_kernel=[r"5.13.\d-\d+-[\w]+"], regex_match=True) + args = KernelAnalyzerArgs(exp_kernel=[r".*5\.13\.\d+-\d+-[\w-]+.*"], regex_match=True) analyzer = KernelAnalyzer(system_info) result = analyzer.analyze_data(model_obj, args) assert result.status == ExecutionStatus.OK def test_mismatch_regex(system_info, model_obj): - args = KernelAnalyzerArgs(exp_kernel=[r"4.3.\d-\d+-[\w]+"], regex_match=True) + args = KernelAnalyzerArgs(exp_kernel=[r".*4\.13\.\d+-\d+-[\w-]+.*"], regex_match=True) analyzer = KernelAnalyzer(system_info) result = analyzer.analyze_data(model_obj, args) diff --git a/test/unit/plugin/test_kernel_collector.py b/test/unit/plugin/test_kernel_collector.py index d9261ad9..b1f26257 100644 --- a/test/unit/plugin/test_kernel_collector.py +++ b/test/unit/plugin/test_kernel_collector.py @@ -53,7 +53,9 @@ def test_run_windows(collector, conn_mock): result, data = collector.collect_data() - assert data == KernelDataModel(kernel_version="10.0.19041.1237") + assert data == KernelDataModel( + kernel_info="Version=10.0.19041.1237", kernel_version="10.0.19041.1237" + ) assert result.status == ExecutionStatus.OK @@ -61,14 +63,17 @@ def test_run_linux(collector, conn_mock): collector.system_info.os_family = OSFamily.LINUX conn_mock.run_command.return_value = CommandArtifact( exit_code=0, - stdout="5.4.0-88-generic", + stdout="Linux MockSystem 5.13.0-30-generic #1 XYZ Day Month 10 15:19:13 EDT 2024 x86_64 x86_64 x86_64 GNU/Linux", stderr="", - command="sh -c 'uname -r'", + command="sh -c 'uname -a'", ) result, data = collector.collect_data() - assert data == KernelDataModel(kernel_version="5.4.0-88-generic") + assert data == KernelDataModel( + kernel_info="Linux MockSystem 5.13.0-30-generic #1 XYZ Day Month 10 15:19:13 EDT 2024 x86_64 x86_64 x86_64 GNU/Linux", + kernel_version="5.13.0-30-generic", + ) assert result.status == ExecutionStatus.OK