-
Notifications
You must be signed in to change notification settings - Fork 75
Add support for ESXI auth, hostd and shell logs. #1385
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
william-billaud
wants to merge
39
commits into
fox-it:main
Choose a base branch
from
william-billaud:esxi_hostd_log
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
39 commits
Select commit
Hold shift + click to select a range
3e4e6f9
Add esxi hostd plugins (6&7)
william-billaud 85f03ee
Support esxi 8/9
william-billaud 3e084b3
Fix tests
william-billaud eb16998
Refactoring plugin
william-billaud 79c46d8
move log files
william-billaud 42fbd66
Format
william-billaud 5f0bc2c
Fix record type inconsistency
william-billaud ea2dde8
Auth
william-billaud 95b1fa3
Merge branch 'main' into esxi_hostd_log
william-billaud d8676d0
Read hostd files
william-billaud 53b061d
Change path
william-billaud bbceab4
Add log auth
william-billaud d308218
Add support for auth.log
william-billaud 0e3f0ac
Add shell.log ESXi parsing
william-billaud 27ef6d6
Remove unused code
william-billaud 9a8d5fd
typo
william-billaud 60b8c4b
Fix gitignore
william-billaud 840254a
Typo
william-billaud d5e97de
Documentation
william-billaud aaebb49
Merge branch 'main' into esxi_hostd_log
william-billaud 3d9d773
Merge branch 'main' into esxi_hostd_log
william-billaud 62f9d83
Fix podt merge
william-billaud c78a718
Merge branch 'main' into esxi_hostd_log
william-billaud 78e369a
Merge branch 'main' into esxi_hostd_log
william-billaud 38514b9
Revert some change on .gitignore
william-billaud d271b8f
Merge branch 'main' into esxi_hostd_log
william-billaud 9db79c0
Merge branch 'main' into esxi_hostd_log
william-billaud bceb2b5
Merge branch 'main' into esxi_hostd_log
william-billaud 1ab065c
Merge branch 'main' into esxi_hostd_log
william-billaud 837320c
Merge branch 'main' into esxi_hostd_log
william-billaud d78dad6
Refactor ESXi log plugins into something more functional
william-billaud 1e011d8
Merge branch 'main' into esxi_hostd_log
william-billaud 52278f6
Symlink osdata into /scratch in esxi os plugins.
william-billaud a10503c
Merge branch 'main' into esxi_hostd_log
william-billaud 5ff012a
Modify comment
william-billaud 09f47ec
Merge branch 'main' into esxi_hostd_log
william-billaud 5237050
Revert change related to ESXi plugin.
william-billaud bcd8648
Revert change related to ESXi plugin.
william-billaud 03fbaeb
Change log path to /var/run/log to match modification made in #1509
william-billaud File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,3 +9,4 @@ __pycache__/ | |
| tests/_docs/api | ||
| tests/_docs/build | ||
| .tox/ | ||
| uv.lock | ||
124 changes: 124 additions & 0 deletions
124
dissect/target/plugins/os/unix/esxi/esxi_log/__init__.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import re | ||
| import typing | ||
| from datetime import datetime | ||
| from re import Pattern | ||
|
|
||
| from dissect.target.helpers.fsutil import open_decompress | ||
| from dissect.target.helpers.record import TargetRecordDescriptor | ||
|
|
||
| if typing.TYPE_CHECKING: | ||
| from collections.abc import Iterator | ||
| from pathlib import Path | ||
|
|
||
| from dissect.target import Target | ||
|
|
||
| ESXiLogRecord = TargetRecordDescriptor( | ||
| "esxi/log", | ||
| [ | ||
| ("datetime", "ts"), | ||
| ("string", "type"), | ||
| ("string", "log_level"), | ||
| ("string", "application"), | ||
| ("varint", "pid"), | ||
| ("string", "op_id"), | ||
| ("string", "user"), | ||
| ("string", "event_metadata"), | ||
| ("string", "message"), | ||
| ("path", "source"), | ||
| ], | ||
| ) | ||
|
|
||
| RE_LOG_FORMAT: Pattern = re.compile( | ||
| r""" | ||
| ((?P<ts>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z)\s)? # ts, moslty including milliseconds, but not always | ||
| ( | ||
| ((?P<log_level>[\w()]+)\s)? # info, warning, of In(166), Wa(164), Er(163) in esxi8+, sometime missing | ||
| ((?P<application>(\w+|-))\[(?P<pid>(\d+))\]|-):?\s # hostd[pid] < esxi8, Hostd[pid]: esxi8+ | ||
|
|
||
| )? | ||
| (?P<newline_delimiter>--> ?)? # in Exi8+, newline marker is positionned after the ts loglevel application part | ||
| # but for some log this marker is missing... | ||
| (\[(?P<metadata>(.*?))\]\s)? | ||
| (?P<message>.*?)""", | ||
| re.VERBOSE, | ||
| ) | ||
|
|
||
|
|
||
| def get_esxi_log_path(target: Target, logname: str) -> Iterator[Path]: | ||
| """ | ||
| Get log location, looking in most usual location, as well as in the osdata partition | ||
|
|
||
| References: | ||
| - https://knowledge.broadcom.com/external/article/306962/location-of-esxi-log-files.html | ||
| :return: | ||
| """ | ||
| # Esxi/loaders should ensure that logs are symlinked to /var/run/log, as on a live ESXi hosts. | ||
| if (var_run_log := target.fs.path("/var/run/log")).exists(): | ||
| print("HERE") | ||
| for path in var_run_log.glob(f"{logname}.*"): | ||
| try: | ||
| yield path.resolve(strict=True) | ||
| except FilesystemError as e: # noqa PERF203 | ||
| target.info.warning("Fail to resolve path to %s : %s", path, str(e)) | ||
| return | ||
|
|
||
|
|
||
| def yield_log_records( | ||
| target: Target, log_paths: list[Path], re_log_format: re.Pattern, logname: str | ||
| ) -> Iterator[ESXiLogRecord]: | ||
| """Yield parsed log entries, iterate on identified log files""" | ||
| for path in log_paths: | ||
| try: | ||
| current_record = None | ||
| for line in open_decompress(path, "rt"): | ||
| if not line: | ||
| continue | ||
| line = line.strip("\n") | ||
| if match := re_log_format.fullmatch(line): | ||
| log = match.groupdict() | ||
| # For multiline event, line start with --> Before Esxi8 | ||
| # For Esxi8+, --> is after the Date loglevel application[pid] block | ||
| # but sometime --> is missing but it's still previous line continuation | ||
| if log.get("newline_delimiter") == "-->" or log.get("ts") is None: | ||
| if current_record: | ||
| current_record.message = current_record.message + "\n" + log.get("message") | ||
| else: | ||
| target.log.warning("log file contains unrecognized format in %s", path) | ||
| target.log.debug("log file contains unrecognized format in %s : %s", path, line) | ||
| else: | ||
| if current_record: | ||
| yield current_record | ||
| current_record = None | ||
| if metadata := log.get("metadata"): | ||
| user = re.search(r"user=(\S+)", metadata) | ||
| op_id = re.search(r"opID=(\S+)", metadata) | ||
| else: | ||
| user = None | ||
| op_id = None | ||
| ts = log["ts"] | ||
| current_record = ESXiLogRecord( | ||
| _target=target, | ||
| type=logname, | ||
| message=log.get("message", ""), | ||
| log_level=log.get("log_level", None), | ||
| application=log.get("application", None), | ||
| pid=log.get("pid", None), | ||
| user=None if user is None else user.groups()[0], | ||
| op_id=None if op_id is None else op_id.groups()[0], | ||
| event_metadata=log.get("metadata", ""), | ||
| ts=datetime.strptime(ts, "%Y-%m-%dT%H:%M:%S.%f%z") | ||
| if "." in ts | ||
| else datetime.strptime(ts, "%Y-%m-%dT%H:%M:%S%z"), | ||
| source=path, | ||
| ) | ||
| else: | ||
| target.log.warning("log file contains unrecognized format in %s", path) | ||
| target.log.debug("log file contains unrecognized format in %s : %s", path, line) | ||
| continue | ||
| if current_record: | ||
| yield current_record | ||
| except Exception as e: | ||
| target.log.warning("An error occurred parsing %s log file %s: %s", logname, path, str(e)) | ||
| target.log.debug("", exc_info=e) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| from collections.abc import Iterator | ||
| from pathlib import Path | ||
|
|
||
| from dissect.target import Target | ||
| from dissect.target.exceptions import UnsupportedPluginError | ||
| from dissect.target.plugin import OperatingSystem, Plugin, export | ||
| from dissect.target.plugins.os.unix.esxi.esxi_log import ( | ||
| RE_LOG_FORMAT, | ||
| ESXiLogRecord, | ||
| get_esxi_log_path, | ||
| yield_log_records, | ||
| ) | ||
|
|
||
|
|
||
| class EsxiAuthPlugin(Plugin): | ||
| """ESXi auth.log plugins""" | ||
|
|
||
| def __init__(self, target: Target): | ||
| super().__init__(target) | ||
| self.log_paths: list[Path] = list(self.get_paths()) | ||
|
|
||
| def check_compatible(self) -> None: | ||
| # Log path as the same as on other unix target, so we fail fast | ||
| if not self.target.os == OperatingSystem.ESXI: | ||
| raise UnsupportedPluginError("Not an ESXi host") | ||
| if not len(self.log_paths): | ||
| raise UnsupportedPluginError("No auth logs found") | ||
|
|
||
| def _get_paths(self) -> Iterator[Path]: | ||
| yield from get_esxi_log_path(self.target, "auth") | ||
|
|
||
| @export(record=ESXiLogRecord) | ||
| def auth(self) -> Iterator[ESXiLogRecord]: | ||
| """ | ||
| Records for auth log file (ESXi Shell authentication success and failure.) Seems to be empty in ESXi8+ | ||
| """ | ||
| yield from yield_log_records(self.target, self.log_paths, RE_LOG_FORMAT, "auth") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| from collections.abc import Iterator | ||
| from pathlib import Path | ||
|
|
||
| from dissect.target import Target | ||
| from dissect.target.exceptions import UnsupportedPluginError | ||
| from dissect.target.plugin import OperatingSystem, Plugin, export | ||
| from dissect.target.plugins.os.unix.esxi.esxi_log import ( | ||
| RE_LOG_FORMAT, | ||
| ESXiLogRecord, | ||
| get_esxi_log_path, | ||
| yield_log_records, | ||
| ) | ||
|
|
||
|
|
||
| class HostdPlugin(Plugin): | ||
| """ESXi hostd logs plugins""" | ||
|
|
||
| def __init__(self, target: Target): | ||
| super().__init__(target) | ||
| self.log_paths: list[Path] = list(self.get_paths()) | ||
|
|
||
| def check_compatible(self) -> None: | ||
| # Log path as the same as on other unix target, so we fail fast | ||
| if not self.target.os == OperatingSystem.ESXI: | ||
| raise UnsupportedPluginError("Not an ESXi host") | ||
| if not len(self.log_paths): | ||
| raise UnsupportedPluginError("No hostd logs found") | ||
|
|
||
| def _get_paths(self) -> Iterator[Path]: | ||
| yield from get_esxi_log_path(self.target, "hostd") | ||
|
|
||
| @export(record=ESXiLogRecord) | ||
| def hostd(self) -> Iterator[ESXiLogRecord]: | ||
| """ | ||
| Records for hostd log file (Host management service logs, including virtual machine and host Task and Events) | ||
| """ | ||
| yield from yield_log_records(self.target, self.log_paths, RE_LOG_FORMAT, "hostd") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import re | ||
| from collections.abc import Iterator | ||
| from pathlib import Path | ||
|
|
||
| from dissect.target import Target | ||
| from dissect.target.exceptions import UnsupportedPluginError | ||
| from dissect.target.plugin import OperatingSystem, Plugin, export | ||
| from dissect.target.plugins.os.unix.esxi.esxi_log import ( | ||
| ESXiLogRecord, | ||
| get_esxi_log_path, | ||
| yield_log_records, | ||
| ) | ||
|
|
||
|
|
||
| class ShellLogPlugin(Plugin): | ||
| """ESXi shell.log plugins""" | ||
|
|
||
| # Mostly equal to EsxiLogBasePlugin.RE_LOG_FORMAT, but some difference in metadata part | ||
| RE_LOG_FORMAT: re.Pattern = re.compile( | ||
| r""" | ||
| ((?P<ts>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z)\s)? # ts, moslty including milliseconds, but not always | ||
| ( | ||
| ((?P<log_level>[\w()]+)\s)? # info, warning, of In(166), Wa(164), Er(163) in esxi8+, sometime missing | ||
| ((?P<application>(\w+))\[(?P<pid>(\d+))\]):?\s # hostd[pid] < esxi8, Hostd[pid]: esxi8+ | ||
|
|
||
| )? | ||
| (?P<newline_delimiter>--> ?)? # in Exi8+, newline marker is positionned after the ts loglevel application part | ||
| # but for some log this marker is missing... | ||
| (\[(?P<metadata>(.+?))\]:\s)? # Metadata = user. Instead of \s, metadata is followed by a ":" | ||
| (?P<message>.*?)""", | ||
| re.VERBOSE, | ||
| ) | ||
|
|
||
| def __init__(self, target: Target): | ||
| super().__init__(target) | ||
| self.log_paths: list[Path] = list(self.get_paths()) | ||
|
|
||
| def check_compatible(self) -> None: | ||
| # Log path as the same as on other unix target, so we fail fast | ||
| if not self.target.os == OperatingSystem.ESXI: | ||
| raise UnsupportedPluginError("Not an ESXi host") | ||
| if not len(self.log_paths): | ||
| raise UnsupportedPluginError("No log found") | ||
|
|
||
| def _get_paths(self) -> Iterator[Path]: | ||
| yield from get_esxi_log_path(self.target, "shell") | ||
|
|
||
| @export(record=ESXiLogRecord) | ||
| def shell_log(self) -> Iterator[ESXiLogRecord]: | ||
| """ | ||
| Records for shell.log files (ESXi Shell usage logs, including enable/disable and every command entered). | ||
|
|
||
| References: | ||
| - https://knowledge.broadcom.com/external/article/321910 | ||
| """ | ||
| for record in yield_log_records(self.target, self.log_paths, self.RE_LOG_FORMAT, "shell"): | ||
| record.user = record.event_metadata | ||
| record.event_metadata = None | ||
| yield record |
Git LFS file not shown
Git LFS file not shown
Git LFS file not shown
Git LFS file not shown
Git LFS file not shown
Git LFS file not shown
Git LFS file not shown
Git LFS file not shown
Git LFS file not shown
Git LFS file not shown
Git LFS file not shown
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.