/opt/cloudlinux/venv/lib/python3.11/site-packages
NameSizeModeActions
aiohttp/-0755rm
aiohttp-3.9.2.dist-info/-0755rm
aiohttp_jinja2/-0755rm
aiohttp_jinja2-1.5.dist-info/-0755rm
aiohttp_security/-0755rm
aiohttp_security-0.4.0.dist-info/-0755rm
aiohttp_session/-0755rm
aiohttp_session-2.9.0.dist-info/-0755rm
aiosignal/-0755rm
aiosignal-1.3.1.dist-info/-0755rm
alembic/-0755rm
alembic-1.11.1.dist-info/-0755rm
astroid/-0755rm
astroid-2.15.6.dist-info/-0755rm
attr/-0755rm
attrs/-0755rm
attrs-23.1.0.dist-info/-0755rm
backports/-0755rm
certifi/-0755rm
certifi-2023.7.22.dist-info/-0755rm
cffi/-0755rm
cffi-1.15.1.dist-info/-0755rm
chardet/-0755rm
chardet-5.2.0.dist-info/-0755rm
charset_normalizer/-0755rm
charset_normalizer-2.1.1.dist-info/-0755rm
clcagefslib/-0755rm
clcommon/-0755rm
clconfig/-0755rm
clconfigure/-0755rm
cldashboard/-0755rm
clevents/-0755rm
clflags/-0755rm
cllicense/-0755rm
cllimits/-0755rm
cllimitslib_v2/-0755rm
cllimits_validator/-0755rm
cllvectl/-0755rm
clpackages/-0755rm
clquota/-0755rm
clselect/-0755rm
clselector/-0755rm
clsentry/-0755rm
clsummary/-0755rm
clveconfig/-0755rm
clwizard/-0755rm
clwpos/-0755rm
cl_dom_collector/-0755rm
cl_website_collector/-0755rm
configparser-5.0.2.dist-info/-0755rm
contextlib2/-0755rm
contextlib2-21.6.0.dist-info/-0755rm
coverage/-0755rm
coverage-7.2.7.dist-info/-0755rm
cryptography/-0755rm
cryptography-41.0.2.dist-info/-0755rm
ddt-1.4.4.dist-info/-0755rm
dill/-0755rm
dill-0.3.7.dist-info/-0755rm
distlib/-0755rm
distlib-0.3.8.dist-info/-0755rm
docopt-0.6.2.dist-info/-0755rm
dodgy/-0755rm
dodgy-0.2.1.dist-info/-0755rm
filelock/-0755rm
filelock-3.13.1.dist-info/-0755rm
flake8/-0755rm
flake8-5.0.4.dist-info/-0755rm
flake8_polyfill/-0755rm
flake8_polyfill-1.0.2.dist-info/-0755rm
frozenlist/-0755rm
frozenlist-1.4.0.dist-info/-0755rm
future/-0755rm
future-0.18.3.dist-info/-0755rm
git/-0755rm
gitdb/-0755rm
gitdb-4.0.10.dist-info/-0755rm
GitPython-3.1.32.dist-info/-0755rm
guppy/-0755rm
guppy3-3.1.3.dist-info/-0755rm
idna/-0755rm
idna-3.4.dist-info/-0755rm
iniconfig/-0755rm
iniconfig-2.0.0.dist-info/-0755rm
isort/-0755rm
isort-5.12.0.dist-info/-0755rm
jinja2/-0755rm
Jinja2-3.0.3.dist-info/-0755rm
jsonschema/-0755rm
jsonschema-3.2.0.dist-info/-0755rm
jwt/-0755rm
lazy_object_proxy/-0755rm
lazy_object_proxy-1.9.0.dist-info/-0755rm
libfuturize/-0755rm
libpasteurize/-0755rm
lvemanager/-0755rm
lvestats/-0755rm
lve_stats-2.0.dist-info/-0755rm
lve_utils/-0755rm
lxml/-0755rm
lxml-4.9.2.dist-info/-0755rm
mako/-0755rm
Mako-1.2.4.dist-info/-0755rm
markupsafe/-0755rm
MarkupSafe-2.1.3.dist-info/-0755rm
mccabe-0.7.0.dist-info/-0755rm
mock/-0755rm
mock-5.1.0.dist-info/-0755rm
multidict/-0755rm
multidict-6.0.4.dist-info/-0755rm
numpy/-0755rm
numpy-1.25.1.dist-info/-0755rm
numpy.libs/-0755rm
packaging/-0755rm
packaging-23.1.dist-info/-0755rm
past/-0755rm
pep8_naming-0.10.0.dist-info/-0755rm
pip/-0755rm
pip-24.3.1.dist-info/-0755rm
pkg_resources/-0755rm
platformdirs/-0755rm
platformdirs-3.11.0.dist-info/-0755rm
pluggy/-0755rm
pluggy-1.2.0.dist-info/-0755rm
prettytable/-0755rm
prettytable-3.8.0.dist-info/-0755rm
prometheus_client/-0755rm
prometheus_client-0.8.0.dist-info/-0755rm
prospector/-0755rm
prospector-1.10.2.dist-info/-0755rm
psutil/-0755rm
psutil-5.9.5.dist-info/-0755rm
psycopg2/-0755rm
psycopg2_binary-2.9.6.dist-info/-0755rm
psycopg2_binary.libs/-0755rm
pycodestyle-2.9.1.dist-info/-0755rm
pycparser/-0755rm
pycparser-2.21.dist-info/-0755rm
pydocstyle/-0755rm
pydocstyle-6.3.0.dist-info/-0755rm
pyfakefs/-0755rm
pyfakefs-5.10.2.dist-info/-0755rm
pyflakes/-0755rm
pyflakes-2.5.0.dist-info/-0755rm
PyJWT-2.8.0.dist-info/-0755rm
pylint/-0755rm
pylint-2.17.4.dist-info/-0755rm
pylint_celery/-0755rm
pylint_celery-0.3.dist-info/-0755rm
pylint_django/-0755rm
pylint_django-2.5.3.dist-info/-0755rm
pylint_flask/-0755rm
pylint_flask-0.6.dist-info/-0755rm
pylint_plugin_utils/-0755rm
pylint_plugin_utils-0.7.dist-info/-0755rm
pylve-2.1-py3.11.egg-info/-0755rm
pymysql/-0755rm
PyMySQL-1.1.0.dist-info/-0755rm
pyparsing/-0755rm
pyparsing-3.0.9.dist-info/-0755rm
pyrsistent/-0755rm
pyrsistent-0.19.3.dist-info/-0755rm
pytest/-0755rm
pytest-7.4.0.dist-info/-0755rm
pytest_check/-0755rm
pytest_check-2.5.3.dist-info/-0755rm
pytest_snapshot/-0755rm
pytest_snapshot-0.9.0.dist-info/-0755rm
pytest_subprocess/-0755rm
pytest_subprocess-1.5.3.dist-info/-0755rm
pytest_tap/-0755rm
pytest_tap-3.5.dist-info/-0755rm
python_pam-1.8.4.dist-info/-0755rm
pyvirtualdisplay/-0755rm
PyVirtualDisplay-3.0.dist-info/-0755rm
PyYAML-6.0.1.dist-info/-0755rm
raven/-0755rm
raven-6.10.0.dist-info/-0755rm
requests/-0755rm
requests-2.31.0.dist-info/-0755rm
requirements_detector/-0755rm
requirements_detector-1.2.2.dist-info/-0755rm
schema-0.7.5.dist-info/-0755rm
semver/-0755rm
semver-3.0.1.dist-info/-0755rm
sentry_sdk/-0755rm
sentry_sdk-1.29.2.dist-info/-0755rm
setoptconf/-0755rm
setoptconf_tmp-0.3.1.dist-info/-0755rm
setuptools/-0755rm
setuptools-81.0.0.dist-info/-0755rm
simplejson/-0755rm
simplejson-3.19.1.dist-info/-0755rm
six-1.16.0.dist-info/-0755rm
smmap/-0755rm
smmap-5.0.0.dist-info/-0755rm
snowballstemmer/-0755rm
snowballstemmer-2.2.0.dist-info/-0755rm
sqlalchemy/-0755rm
sqlalchemy-1.3.24.dist-info/-0755rm
ssa/-0755rm
svgwrite/-0755rm
svgwrite-1.4.3.dist-info/-0755rm
tap/-0755rm
tap_py-3.2.1.dist-info/-0755rm
testfixtures/-0755rm
testfixtures-7.1.0.dist-info/-0755rm
toml/-0755rm
toml-0.10.2.dist-info/-0755rm
tomlkit/-0755rm
tomlkit-0.11.8.dist-info/-0755rm
typing_extensions-4.7.1.dist-info/-0755rm
unshare-0.22.dist-info/-0755rm
urllib3/-0755rm
urllib3-2.0.4.dist-info/-0755rm
vendors_api/-0755rm
virtualenv/-0755rm
virtualenv-20.21.1.dist-info/-0755rm
wcwidth/-0755rm
wcwidth-0.2.6.dist-info/-0755rm
websiteisolation/-0755rm
wmt/-0755rm
wrapt/-0755rm
wrapt-1.15.0.dist-info/-0755rm
xray/-0755rm
yaml/-0755rm
yarl/-0755rm
yarl-1.9.2.dist-info/-0755rm
_distutils_hack/-0755rm
_pytest/-0755rm
_yaml/-0755rm
__pycache__/-0755rm
clcontrollib.py530420644editdlrm
cldetectlib.py188430644editdlrm
cldiaglib.py496600644editdlrm
clhooklib.py12960644editdlrm
cli_utils.py16980644editdlrm
cllicenselib.py118640644editdlrm
clsetuplib.py51930644editdlrm
clsudo.py235860644editdlrm
cl_proc_hidepid.py46380644editdlrm
configparser.py15460644editdlrm
ddt.py127330644editdlrm
distutils-precedence.pth1510644editdlrm
docopt.py199460644editdlrm
lveapi.py232940644editdlrm
lvectllib.py1364790644editdlrm
lvestat.py69970644editdlrm
mccabe.py106540644editdlrm
pam.py75560644editdlrm
pep8ext_naming.py190520644editdlrm
py.py2630644editdlrm
pycodestyle.py1035010644editdlrm
pylve.cpython-311-x86_64-linux-gnu.so295280755editdlrm
remove_ubc.py58640755editdlrm
schema.py302210644editdlrm
secureio.py200370644editdlrm
simple_rpm.so155440755editdlrm
six.py345490644editdlrm
typing_extensions.py1110820644editdlrm
unshare.cpython-311-x86_64-linux-gnu.so167040755editdlrm
_cffi_backend.cpython-311-x86_64-linux-gnu.so2740480755editdlrm
_lvdmap.cpython-311-x86_64-linux-gnu.so188000755editdlrm
_pyrsistent_version.py230644editdlrm
Edit: /opt/cloudlinux/venv/lib/python3.11/site-packages/clsudo.py (23586B)
# coding=utf-8 # Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2018 All Rights Reserved # # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENSE.TXT import logging import os import pwd import grp import re import subprocess import tempfile from stat import S_IRUSR, S_IRGRP logger = logging.getLogger(__name__) class NoSuchUser(Exception): def __init__(self, user): Exception.__init__(self, f'No such user ({user})') class NoSuchGroup(Exception): def __init__(self, group): Exception.__init__(self, f'No such group ({group})') class UnableToReadFile(Exception): def __init__(self): Exception.__init__(self, 'Cannot read sudoers file') class UnableToWriteFile(Exception): def __init__(self): Exception.__init__(self, 'Cannot modify sudoers file') SUDOERS_FILE = '/etc/sudoers' # argv-restricted: bare command paths grant arbitrary argv as root (sudoers # semantics). ps/grep/service have no in-tree sudo caller -> pin to no args (""); # lvectl's only sudo caller is `lvectl lve-version`. ALIAS_LVECTL_CMDS = ['/bin/ps ""', '/bin/grep ""', '/sbin/service ""', "/usr/bin/getcontrolpaneluserspackages", "/usr/sbin/lvectl lve-version", "/usr/local/directadmin/plugins/new_lvemanager/admin/GetDomains", "/usr/share/l.v.e-manager/utils/cloudlinux-cli.py"] ALIAS_LVECTL_USER_CMDS = ["/usr/share/l.v.e-manager/utils/cloudlinux-cli-user.py"] # argv-restricted (same rationale as ALIAS_LVECTL_CMDS): no in-tree consumer # invokes these via sudo, so pin to no args ("") rather than arbitrary argv as root. ALIAS_CAGEFS_CMDS = ['/usr/sbin/cagefsctl ""', '/bin/ps ""', '/bin/grep ""', '/sbin/service ""'] # Detect a CAGEFS_CMDS / SELECTOR_CMDS alias definition regardless of the # (visudo-legal) whitespace around Cmnd_Alias, the alias name, and `=`. A # substring match on a single-space form misses a valid non-canonical line, so # the upgrade-rewrite would skip it and append a duplicate alias that # `visudo -c` then rejects. CAGEFS_ALIAS_RE = re.compile(r'^\s*Cmnd_Alias\s+CAGEFS_CMDS\s*=') SELECTOR_ALIAS_RE = re.compile(r'^\s*Cmnd_Alias\s+SELECTOR_CMDS\s*=') LVECTL_ALIAS_RE = re.compile(r'^\s*Cmnd_Alias\s+LVECTL_CMDS\s*=') # argv-restricted (same rationale as ALIAS_LVECTL_CMDS/ALIAS_CAGEFS_CMDS): the # only in-tree sudo caller named cl_selector.py runs /usr/sbin/cloudlinux-selector # (a different path) as the target user, not these paths as root; lveps runs from # the lve-stats root daemon without sudo; piniset has no caller (deprecated). No # consumer invokes these via the NOPASSWD-root grant, so pin to no args ("") # rather than arbitrary argv as root. ALIAS_SELECTOR_CMDS = ['/usr/bin/cl-selector ""', '/usr/bin/piniset ""', '/usr/sbin/lveps ""', '/usr/bin/selectorctl ""'] DEFAULTS_REQUIRETTY = 'Defaults:%s !requiretty' # Patterns for group GROUP_LVECTL_SELECTOR = '%%%s ALL=NOPASSWD: LVECTL_CMDS, SELECTOR_CMDS' GROUP_DEFAULTS_REQUIRETTY = 'Defaults:%%%s !requiretty' class Clsudo: """ Adds CloudLinux users to sudoers file """ # Fail-closed allowlist for the write destination. `sudoers_file` is a # public parameter that flows into `filepath`; the only path the product # ever writes is /etc/sudoers. Restricting the destination here ensures a # caller that forwarded an attacker-influenced path is refused rather than # dropping a sudoers file at an arbitrary location. Tests override this. _ALLOWED_SUDOERS_PATHS = (SUDOERS_FILE,) filepath = None sudoers_list = [] has_action = False has_group_action = False has_alias = False has_user_alias = False has_rights = False has_user_rights = False has_selector_alias = False has_selector_rights = False has_cagefs_alias = False has_cagefs_rights = False @staticmethod def add_user(user, sudoers_file=SUDOERS_FILE): """ Adds username to sudoers file (for lvemanager) """ logger.info("add_user: adding user '%s' to sudoers file '%s' " "(uid=%d, pid=%d)", user, sudoers_file, os.getuid(), os.getpid()) # Update command lists for lvemanager Clsudo.update_commands_list(sudoers_file) Clsudo._check_user(user) Clsudo._get_contents(user) if not Clsudo.has_alias: Clsudo.sudoers_list.append('Cmnd_Alias LVECTL_CMDS = ' + ", ".join(ALIAS_LVECTL_CMDS)) if not Clsudo.has_user_alias: Clsudo.sudoers_list.append('Cmnd_Alias LVECTL_USER_CMDS = ' + ", ".join(ALIAS_LVECTL_USER_CMDS)) if not Clsudo.has_selector_alias: Clsudo.sudoers_list.append('Cmnd_Alias SELECTOR_CMDS = ' + ", ".join(ALIAS_SELECTOR_CMDS)) if not Clsudo.has_rights: Clsudo.sudoers_list.append(f'{user} ALL=NOPASSWD: LVECTL_CMDS') if not Clsudo.has_user_rights: Clsudo.sudoers_list.append(f'{user} ALL=(ALL) NOPASSWD: LVECTL_USER_CMDS') if not Clsudo.has_selector_rights: Clsudo.sudoers_list.append(f'{user} ALL=NOPASSWD: SELECTOR_CMDS') if not Clsudo.has_action: Clsudo.sudoers_list.append(DEFAULTS_REQUIRETTY % (user,)) Clsudo._write_contents() logger.info("add_user: successfully added user '%s' to '%s'", user, sudoers_file) @staticmethod def add_cagefs_user(user, sudoers_file=SUDOERS_FILE): """ Adds username to sudoers file (for cagefs) """ logger.info("add_cagefs_user: adding user '%s' to sudoers file '%s' " "(uid=%d, pid=%d)", user, sudoers_file, os.getuid(), os.getpid()) Clsudo.filepath = sudoers_file Clsudo._check_user(user) Clsudo._get_contents(user) if not Clsudo.has_cagefs_alias: Clsudo.sudoers_list.append('Cmnd_Alias CAGEFS_CMDS = ' + ", ".join(ALIAS_CAGEFS_CMDS)) if not Clsudo.has_cagefs_rights: Clsudo.sudoers_list.append(f'{user} ALL=NOPASSWD: CAGEFS_CMDS') if not Clsudo.has_action: Clsudo.sudoers_list.append(DEFAULTS_REQUIRETTY % (user,)) Clsudo._write_contents() logger.info("add_cagefs_user: successfully added user '%s' to '%s'", user, sudoers_file) @staticmethod def add_lvemanager_group(group_name, sudoers_file=SUDOERS_FILE): """ Adds group to sudoers file, grants access to LVE Manager """ logger.info("add_lvemanager_group: adding group '%s' to sudoers file '%s' " "(uid=%d, pid=%d)", group_name, sudoers_file, os.getuid(), os.getpid()) # Update command lists for lvemanager Clsudo.update_commands_list(sudoers_file) Clsudo._check_group(group_name) Clsudo._get_contents_group(group_name) if not Clsudo.has_alias: Clsudo.sudoers_list.append('Cmnd_Alias LVECTL_CMDS = ' + ", ".join(ALIAS_LVECTL_CMDS)) if not Clsudo.has_selector_alias: Clsudo.sudoers_list.append('Cmnd_Alias SELECTOR_CMDS = ' + ", ".join(ALIAS_SELECTOR_CMDS)) if not Clsudo.has_action: Clsudo.sudoers_list.append(GROUP_LVECTL_SELECTOR % (group_name,)) if not Clsudo.has_group_action: Clsudo.sudoers_list.append(GROUP_DEFAULTS_REQUIRETTY % (group_name,)) # writes file Clsudo._write_contents() logger.info("add_lvemanager_group: successfully added group '%s' to '%s'", group_name, sudoers_file) @staticmethod def remove_user(user, sudoers_file=SUDOERS_FILE): """ Removes username from sudoers file """ logger.info("remove_user: removing user '%s' from sudoers file '%s' " "(uid=%d, pid=%d)", user, sudoers_file, os.getuid(), os.getpid()) Clsudo.filepath = sudoers_file try: with open(Clsudo.filepath, encoding='utf-8') as f: Clsudo.sudoers_list = f.read().splitlines() idx = 0 removed = False while idx < len(Clsudo.sudoers_list): line = Clsudo.sudoers_list[idx] # Anchor matches at line start: a substring match would let # remove_user() also strip the rules of a different enrolled # admin whose name ends with (remove_user('alice') must not # touch 'malice'). add_user writes both the plain ` ALL=NOPASSWD:` # rules and the ` ALL=(ALL) NOPASSWD: LVECTL_USER_CMDS` rule # (line 94); match the `(ALL) ` form too so it is not orphaned (CLOS-4593 [28]). if (line.startswith(f'{user} ALL=NOPASSWD:') or line.startswith(f'{user} ALL=(ALL) NOPASSWD:') or line.startswith(DEFAULTS_REQUIRETTY % (user,))): logger.info("remove_user: removing sudoers rule for user '%s': %s", user, line) Clsudo.sudoers_list.remove(line) removed = True continue idx += 1 if removed: Clsudo._write_contents() logger.info("remove_user: successfully removed user '%s' from '%s'", user, sudoers_file) else: logger.info("remove_user: no rules found for user '%s' in '%s'", user, sudoers_file) except (IOError, OSError) as e: raise UnableToReadFile() from e @staticmethod def update_user(user, sudoers_file=SUDOERS_FILE): """ updates username in sudoers file :param user: username for caching :param sudoers_file: path to /etc/sudoers (only for tests) :return: None """ logger.info("update_user: updating user '%s' in sudoers file '%s' " "(uid=%d, pid=%d)", user, sudoers_file, os.getuid(), os.getpid()) # Update command lists Clsudo.update_commands_list(sudoers_file) # For backward compatibility # Check user presence in system Clsudo._check_user(user) Clsudo._get_contents(user) @staticmethod def update_commands_list(sudoers_file=SUDOERS_FILE): """ Update command lists for lvemanager plugin If any required command absent in file, add it :param sudoers_file: path to /etc/sudoers :return: None """ # Read /etc/sudoers Clsudo.filepath = sudoers_file Clsudo.temp_dir = os.path.dirname(Clsudo.filepath) Clsudo._read_sudoers() # (regex, alias name, restricted command list) per upgradeable alias. # The anchored regexes tolerate any visudo-legal whitespace around # Cmnd_Alias / the alias name / `=`; _rewrite_alias collapses a # backslash line-continuation span and no-ops on an unrecognised form, # so an existing lax (or continuation) definition is upgraded in place # without duplication or corruption (same machinery as the CAGEFS_CMDS # upgrade in _get_contents). alias_specs = ((LVECTL_ALIAS_RE, 'LVECTL_CMDS', ALIAS_LVECTL_CMDS), (SELECTOR_ALIAS_RE, 'SELECTOR_CMDS', ALIAS_SELECTOR_CMDS)) is_sudoer_change = False for idx, command_string in enumerate(Clsudo.sudoers_list): for alias_re, alias_name, alias_list in alias_specs: if alias_re.match(command_string): before = Clsudo.sudoers_list[idx] Clsudo._rewrite_alias(idx, alias_name, alias_list) if Clsudo.sudoers_list[idx] != before: is_sudoer_change = True break if is_sudoer_change: logger.info("update_commands_list: updating command aliases in '%s' " "(uid=%d, pid=%d)", sudoers_file, os.getuid(), os.getpid()) Clsudo._write_contents() @staticmethod def _check_user(user): """ Checks passwd database for username presence @param user: string """ try: pwd.getpwnam(user) except KeyError as e: raise NoSuchUser(user) from e @staticmethod def _check_group(group_name): """ Checks grp database for group_name presence @param group_name: string """ try: grp.getgrnam(group_name) except KeyError as e: raise NoSuchGroup(group_name) from e @staticmethod def _read_sudoers(): with open(Clsudo.filepath, encoding='utf-8') as f: Clsudo.sudoers_list = f.read().splitlines() @staticmethod def _rewrite_alias(idx, alias_name, alias_cmds): """Rewrite the existing `alias_name` Cmnd_Alias definition at sudoers_list[idx] to the single canonical argv-restricted line, in place. Shared by the CAGEFS_CMDS and SELECTOR_CMDS upgrade rewrites - both are lax bare-command aliases whose pre-fix on-disk form must be tightened to the argv-restricted `alias_cmds` join without duplicating or corrupting the definition. The definition may span several physical lines via visudo-legal backslash line-continuation (_read_sudoers splits on physical lines, so each continuation line is its own list element). Consume the whole span - the matched line plus every following line the preceding line continued onto - and replace it with one restricted line. Never-corrupt invariant: if the definition cannot be confidently and safely collapsed (a continuation whose trailing backslash runs off the end of the file - i.e. the file is already malformed), leave it exactly as-is. The worst case of the upgrade rewrite is a safe no-op (the file stays as valid as it was), never a corrupted sudoers file. The caller still sets the has_*_alias flag, so no duplicate definition is appended. """ lines = Clsudo.sudoers_list end = idx # Walk every physical line the preceding line continued onto (trailing # backslash). A dangling continuation (last line of the span ends in a # backslash with no successor) is an already-malformed form we do not # recognise -> bail out, leaving the definition untouched. while lines[end].endswith('\\'): if end + 1 >= len(lines): return end += 1 restricted = f'Cmnd_Alias {alias_name} = ' + ", ".join(alias_cmds) if end == idx and lines[idx].strip() == restricted: return # Collapse [idx, end] (matched line + its continuation lines) to the # single restricted line. Only elements after idx are removed, so the # enumerate() in the caller resumes correctly at the next real line. lines[idx:end + 1] = [restricted] @staticmethod def _get_contents(user): """ Reads file into list of strings @param user: string """ # Clear all status flags Clsudo.has_action = False Clsudo.has_group_action = False Clsudo.has_alias = False Clsudo.has_user_alias = False Clsudo.has_rights = False Clsudo.has_user_rights = False Clsudo.has_selector_alias = False Clsudo.has_selector_rights = False Clsudo.has_cagefs_alias = False Clsudo.has_cagefs_rights = False require_tty_pattern = re.compile(rf'Defaults:\s*{user}\s*!requiretty') try: # Read sudoers file Clsudo._read_sudoers() for idx, command_string in enumerate(Clsudo.sudoers_list): if "Cmnd_Alias LVECTL_CMDS" in command_string: Clsudo.has_alias = True continue if "Cmnd_Alias LVECTL_USER_CMDS" in command_string: Clsudo.has_user_alias = True continue if CAGEFS_ALIAS_RE.match(command_string): # Upgrade: rewrite a pre-existing lax CAGEFS_CMDS definition # (bare command paths grant arbitrary argv as root) to the # argv-restricted form in place. Matched whitespace-tolerantly # and continuation-aware so a valid non-canonical or multi-line # definition is rewritten, not duplicated or corrupted. Clsudo._rewrite_alias(idx, 'CAGEFS_CMDS', ALIAS_CAGEFS_CMDS) Clsudo.has_cagefs_alias = True continue # Anchor to the start of the line so we don't false-positive on # a longer user whose name ends with `user` — e.g. searching for # `admin ALL=NOPASSWD: CAGEFS_CMDS` must not match a pre-existing # `newadmin ALL=NOPASSWD: CAGEFS_CMDS` line, or add_cagefs_user # silently skips granting the rights and DA admins land without # the sudo entry the hook is supposed to install. if command_string.lstrip().startswith(f"{user} ALL=NOPASSWD: LVECTL_CMDS"): Clsudo.has_rights = True continue if command_string.lstrip().startswith(f"{user} ALL=(ALL) NOPASSWD: LVECTL_USER_CMDS"): Clsudo.has_user_rights = True continue if command_string.lstrip().startswith(f"{user} ALL=NOPASSWD: CAGEFS_CMDS"): Clsudo.has_cagefs_rights = True continue if "requiretty" in command_string: pattern_match = require_tty_pattern.search(command_string) if pattern_match: Clsudo.has_action = True continue if SELECTOR_ALIAS_RE.match(command_string): # Upgrade: rewrite a pre-existing lax SELECTOR_CMDS definition # (bare command paths grant arbitrary argv as root) to the # argv-restricted form in place, same whitespace-tolerant, # continuation-aware, never-corrupt rewrite as CAGEFS_CMDS. Clsudo._rewrite_alias(idx, 'SELECTOR_CMDS', ALIAS_SELECTOR_CMDS) Clsudo.has_selector_alias = True continue if command_string.lstrip().startswith(f"{user} ALL=NOPASSWD: SELECTOR_CMDS"): Clsudo.has_selector_rights = True continue except (IOError, OSError) as e: raise UnableToReadFile() from e @staticmethod def _get_contents_group(group_name): """ Reads file into list of strings @param group_name: string """ # Clear all status flags Clsudo.has_action = False Clsudo.has_group_action = False Clsudo.has_alias = False Clsudo.has_rights = False Clsudo.has_selector_alias = False Clsudo.has_selector_rights = False Clsudo.has_cagefs_alias = False Clsudo.has_cagefs_rights = False group_prefix = f"%{group_name}" group_action = f"Defaults:%{group_name}" group_pattern = re.compile(rf'{group_name}\s*ALL=NOPASSWD:\s*LVECTL_CMDS,\s*SELECTOR_CMDS') try: # Read sudoers file Clsudo._read_sudoers() for idx, command_string in enumerate(Clsudo.sudoers_list): if SELECTOR_ALIAS_RE.match(command_string): # Upgrade: rewrite a pre-existing lax SELECTOR_CMDS definition # to the argv-restricted form in place (see _get_contents). Clsudo._rewrite_alias(idx, 'SELECTOR_CMDS', ALIAS_SELECTOR_CMDS) Clsudo.has_selector_alias = True continue if "Cmnd_Alias LVECTL_CMDS" in command_string: Clsudo.has_alias = True continue if CAGEFS_ALIAS_RE.match(command_string): # Upgrade: rewrite a pre-existing lax CAGEFS_CMDS definition # to the argv-restricted form in place (see _get_contents). Clsudo._rewrite_alias(idx, 'CAGEFS_CMDS', ALIAS_CAGEFS_CMDS) Clsudo.has_cagefs_alias = True continue if command_string.startswith(group_prefix): pattern_match = group_pattern.search(command_string) if pattern_match: Clsudo.has_action = True if command_string.startswith(group_action): Clsudo.has_group_action = True except (IOError, OSError) as e: raise UnableToReadFile() from e @staticmethod def _write_contents(): """ Writes data to temporary file then checks it and rewrites sudoers file """ # Fail-closed destination allowlist: visudo -c (below) validates sudoers # SYNTAX, not LOCATION, so the write target itself must be constrained. # Refuse any destination not on the approved list before creating or # renaming any file. allowed = {os.path.realpath(p) for p in Clsudo._ALLOWED_SUDOERS_PATHS} if os.path.realpath(Clsudo.filepath) not in allowed: logger.warning("_write_contents: refusing to write sudoers to " "disallowed destination '%s' (uid=%d, pid=%d)", Clsudo.filepath, os.getuid(), os.getpid()) raise UnableToWriteFile() try: temp_dir = os.path.dirname(Clsudo.filepath) temp_prefix = 'lve_sudoers_' fd, temp_path = tempfile.mkstemp(prefix=temp_prefix, dir=temp_dir) fo = os.fdopen(fd, 'w') fo.write('\n'.join(Clsudo.sudoers_list) + '\n') fo.close() mask = S_IRUSR | S_IRGRP os.chmod(temp_path, mask) if not Clsudo._is_file_valid(temp_path): raise IOError except (IOError, OSError) as e: logger.warning("_write_contents: failed to write sudoers file '%s' " "(uid=%d, pid=%d)", Clsudo.filepath, os.getuid(), os.getpid()) try: if os.path.exists(temp_path): os.unlink(temp_path) except Exception: pass raise UnableToWriteFile() from e try: os.rename(temp_path, Clsudo.filepath) except OSError as e: logger.warning("_write_contents: failed to rename temp file to '%s' " "(uid=%d, pid=%d)", Clsudo.filepath, os.getuid(), os.getpid()) raise UnableToWriteFile() from e @staticmethod def _is_file_valid(filename): cmd = [ '/usr/sbin/visudo', '-c', '-f', filename ] with subprocess.Popen( cmd, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) as proc: proc.communicate() if proc.returncode != 0: return False return True