"""Read scheduler metadata only; never run jobs or report their arguments.""" from pathlib import Path import re import shlex SAFE_PATH = re.compile(r'^[A-Za-z0-9_./+-]+$') def cron_rows(text, source, system_file=False): rows = [] for number, raw in enumerate(text.splitlines(), 1): line = raw.strip() if not line or line.startswith('#') or re.match(r'^[A-Za-z_][A-Za-z0-9_]*\s*=', line): continue parts = line.split(None, 1 if line.startswith('@') else 5) count = 1 if line.startswith('@') else 5 if count == 1 and parts[0] not in {'@reboot', '@yearly', '@annually', '@monthly', '@weekly', '@daily', '@midnight', '@hourly'}: continue if len(parts) <= count: continue schedule, command = ' '.join(parts[:count]), parts[count] account = 'Current user' if system_file: user_command = command.split(None, 1) if len(user_command) != 2: continue account, command = user_command # A safe executable/script filename is enough to identify the workload. # Arguments, embedded scripts and environment assignments never leave here. try: tokens = shlex.split(command) except ValueError: tokens = [] executable = tokens[0] if tokens and SAFE_PATH.fullmatch(tokens[0]) else '[command omitted]' script = executable if Path(executable).name.startswith(('python', 'perl', 'ruby', 'bash', 'sh')) and len(tokens) > 1: if SAFE_PATH.fullmatch(tokens[1]) and not tokens[1].startswith('-'): script = tokens[1] rows.append({'name': f'{Path(script).name} ({source}:{number})', 'path': source, 'enabled': True, 'account': account, 'executable': executable, 'triggers': schedule + ' (cron; timezone not resolved)', 'state': 'Configured schedule; execution unverified'}) return rows def read_system_cron(cancelled=lambda: False): paths = [Path('/etc/crontab')] directory = Path('/etc/cron.d') if directory.exists(): paths.extend(sorted(directory.iterdir())) rows, incomplete = [], False incomplete = len(paths) > 256 for path in paths[:256]: if cancelled(): from samsara.collectors.linux import CollectionCancelled raise CollectionCancelled() if not path.exists(): continue # Avoid following symlinks or special files. Bound every file read. if path.is_symlink() or not path.is_file(): incomplete = True continue if path.name.startswith('.') or path.name.endswith(('~', '.bak', '.dpkg-old', '.rpmnew', '.rpmsave')): continue try: with path.open('r', encoding='utf-8', errors='replace') as source: text = source.read(65537) if len(text) > 65536: incomplete = True continue rows.extend(cron_rows(text, str(path), system_file=True)) except OSError: incomplete = True return rows, incomplete def enrich_timers(rows, command): names = [row['name'] for row in rows if re.fullmatch(r'[A-Za-z0-9_.@:-]+\.timer', row['name'])] if not names: return output = command(['systemctl', 'show', '--property=Id', '--property=TimersCalendar', '--property=NextElapseUSecRealtime', '--property=Triggers', '--no-pager', '--', *names[:200]]) details = {} for block in output.strip().split('\n\n'): properties = dict(line.split('=', 1) for line in block.splitlines() if '=' in line) if 'Id' in properties: details[properties['Id']] = properties for row in rows: properties = details.get(row['name'], {}) schedule = properties.get('TimersCalendar', '') next_run = properties.get('NextElapseUSecRealtime', '') if schedule or next_run: row['triggers'] = '; '.join(filter(None, [schedule, 'Next: ' + next_run if next_run else ''])) if properties.get('Triggers'): row['associated_unit'] = properties['Triggers']