-
Notifications
You must be signed in to change notification settings - Fork 63
CM-67459: Enrich session context payload #477
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| import getpass | ||
| import platform | ||
| import re | ||
| import socket | ||
| import subprocess | ||
| from typing import Optional | ||
|
|
||
| from cycode.logger import get_logger | ||
|
|
||
| logger = get_logger('HOST INFO') | ||
|
|
||
| _SUBPROCESS_TIMEOUT_SEC = 5 | ||
|
|
||
| _PLATFORM_NAMES = {'Darwin': 'macOS', 'Windows': 'Windows', 'Linux': 'Linux'} | ||
|
|
||
|
|
||
| def _run(command: list, timeout: int = _SUBPROCESS_TIMEOUT_SEC) -> Optional[str]: | ||
| """Run a command and return its stripped stdout. Never raises; returns None on any error.""" | ||
| try: | ||
| result = subprocess.run(command, capture_output=True, text=True, timeout=timeout) # noqa: S603 | ||
| return result.stdout.strip() or None | ||
| except Exception as e: | ||
| logger.debug('Failed to run command %s', command, exc_info=e) | ||
| return None | ||
|
|
||
|
|
||
| def _read_text_file(path: str) -> Optional[str]: | ||
| """Read and strip a text file. Never raises; returns None if it can't be read.""" | ||
| try: | ||
| with open(path) as text_file: | ||
| return text_file.read().strip() or None | ||
| except OSError: | ||
| return None | ||
|
|
||
|
|
||
| def get_hostname() -> Optional[str]: | ||
| try: | ||
| return socket.gethostname() or None | ||
| except Exception as e: | ||
| logger.debug('Failed to resolve hostname', exc_info=e) | ||
| return None | ||
|
|
||
|
|
||
| def get_platform_name() -> Optional[str]: | ||
| try: | ||
| system = platform.system() | ||
| return _PLATFORM_NAMES.get(system, system or None) | ||
| except Exception as e: | ||
| logger.debug('Failed to resolve platform name', exc_info=e) | ||
| return None | ||
|
|
||
|
|
||
| def get_os_version() -> Optional[str]: | ||
| try: | ||
| system = platform.system() | ||
| if system == 'Darwin': | ||
| return platform.mac_ver()[0] or None | ||
| if system == 'Windows': | ||
| return platform.win32_ver()[1] or platform.version() or None | ||
| if system == 'Linux': | ||
| return _get_linux_os_version() | ||
| return platform.release() or None | ||
| except Exception as e: | ||
| logger.debug('Failed to resolve OS version', exc_info=e) | ||
| return None | ||
|
|
||
|
|
||
| def _get_linux_os_version() -> Optional[str]: | ||
| freedesktop_os_release = getattr(platform, 'freedesktop_os_release', None) # Python 3.10+ | ||
| if freedesktop_os_release is not None: | ||
| try: | ||
| version_id = freedesktop_os_release().get('VERSION_ID') | ||
| if version_id: | ||
| return version_id | ||
| except OSError: | ||
| pass | ||
|
|
||
| os_release = _read_text_file('/etc/os-release') # Python 3.9 fallback: parse manually | ||
| if os_release: | ||
| for line in os_release.splitlines(): | ||
| if line.startswith('VERSION_ID='): | ||
| return line.split('=', 1)[1].strip().strip('"') or None | ||
|
|
||
| return platform.release() or None | ||
|
|
||
|
|
||
| def get_last_login_user() -> Optional[str]: | ||
| try: | ||
| return getpass.getuser() or None | ||
| except Exception as e: | ||
| logger.debug('Failed to resolve last login user', exc_info=e) | ||
| return None | ||
|
|
||
|
|
||
| def get_serial_number() -> Optional[str]: | ||
| try: | ||
| system = platform.system() | ||
| if system == 'Darwin': | ||
| return _get_macos_serial_number() | ||
| if system == 'Windows': | ||
| return _run( | ||
| ['powershell', '-NoProfile', '-Command', '(Get-CimInstance -ClassName Win32_BIOS).SerialNumber'] | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i'm not sure but maybe we should avoid subprocess for this? (powershell takes time to spawn) (we can probably get it through python libraries) |
||
| ) | ||
| except Exception as e: | ||
| logger.debug('Failed to resolve serial number', exc_info=e) | ||
| return None | ||
|
|
||
|
|
||
| def _get_macos_serial_number() -> Optional[str]: | ||
| output = _run(['ioreg', '-c', 'IOPlatformExpertDevice', '-d', '2']) | ||
| if not output: | ||
| return None | ||
| match = re.search(r'"IOPlatformSerialNumber"\s*=\s*"([^"]+)"', output) | ||
| return match.group(1) if match else None | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| from pytest_mock import MockerFixture | ||
|
|
||
| from cycode.cli.utils import host_info | ||
|
|
||
| _MODULE = 'cycode.cli.utils.host_info' | ||
|
|
||
| _IOREG_SAMPLE = """ | ||
| "IOPlatformUUID" = "00000000-0000-0000-0000-000000000000" | ||
| "IOPlatformSerialNumber" = "AAAA888111" | ||
| """ | ||
| # platform_name mapping | ||
|
|
||
|
|
||
| def test_get_platform_name_maps_known_systems(mocker: MockerFixture) -> None: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. all of these tests are mocked and meaningless |
||
| for system, expected in (('Darwin', 'macOS'), ('Windows', 'Windows'), ('Linux', 'Linux')): | ||
| mocker.patch(f'{_MODULE}.platform.system', return_value=system) | ||
| assert host_info.get_platform_name() == expected | ||
|
|
||
|
|
||
| def test_get_platform_name_falls_back_to_raw_system(mocker: MockerFixture) -> None: | ||
| mocker.patch(f'{_MODULE}.platform.system', return_value='SunOS') | ||
| assert host_info.get_platform_name() == 'SunOS' | ||
|
|
||
|
|
||
| # serial_number per platform | ||
|
|
||
|
|
||
| def test_get_serial_number_macos_parses_ioreg(mocker: MockerFixture) -> None: | ||
| mocker.patch(f'{_MODULE}.platform.system', return_value='Darwin') | ||
| mocker.patch(f'{_MODULE}._run', return_value=_IOREG_SAMPLE) | ||
| assert host_info.get_serial_number() == 'AAAA888111' | ||
|
|
||
|
|
||
| def test_get_serial_number_windows_uses_command_output(mocker: MockerFixture) -> None: | ||
| mocker.patch(f'{_MODULE}.platform.system', return_value='Windows') | ||
| mocker.patch(f'{_MODULE}._run', return_value='ABC123XYZ') | ||
| assert host_info.get_serial_number() == 'ABC123XYZ' | ||
|
|
||
|
|
||
| def test_get_serial_number_unsupported_platform_returns_none(mocker: MockerFixture) -> None: | ||
| mocker.patch(f'{_MODULE}.platform.system', return_value='Linux') | ||
| assert host_info.get_serial_number() is None | ||
|
|
||
|
|
||
| # Robustness: getters never raise, returning None when their backing call fails. | ||
|
|
||
|
|
||
| def test_get_serial_number_returns_none_when_command_raises(mocker: MockerFixture) -> None: | ||
| mocker.patch(f'{_MODULE}.platform.system', return_value='Darwin') | ||
| mocker.patch(f'{_MODULE}._run', side_effect=RuntimeError('subprocess failed')) | ||
| assert host_info.get_serial_number() is None | ||
|
|
||
|
|
||
| def test_run_returns_none_on_failure(mocker: MockerFixture) -> None: | ||
| mocker.patch(f'{_MODULE}.subprocess.run', side_effect=FileNotFoundError('missing')) | ||
| assert host_info._run(['does-not-exist']) is None | ||
|
|
||
|
|
||
| def test_get_hostname_returns_none_on_error(mocker: MockerFixture) -> None: | ||
| mocker.patch(f'{_MODULE}.socket.gethostname', side_effect=OSError('hostname unavailable')) | ||
| assert host_info.get_hostname() is None | ||
|
|
||
|
|
||
| def test_get_last_login_user_returns_none_on_error(mocker: MockerFixture) -> None: | ||
| mocker.patch(f'{_MODULE}.getpass.getuser', side_effect=KeyError('no user')) | ||
| assert host_info.get_last_login_user() is None | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
did you check this is not returning root from hooks?