| # Copyright 2022 The ChromiumOS Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Run upstart-specific lint checks on the specified .conf files.""" |
| |
| import functools |
| import json |
| import logging |
| import os |
| from pathlib import Path |
| import re |
| from typing import Dict, Generator, List |
| |
| from chromite.lint.linters import whitespace |
| |
| |
| DOC_RESOURCE_URL = ( |
| "https://dev.chromium.org/chromium-os/chromiumos-design-docs/" |
| "boot-design/#runtime-resource-limits" |
| ) |
| AUDITED_SHELL_COMMAND_REGEX = re.compile( |
| # Match comment lines so they can be excluded. |
| r"(?P<comment>^\s*#.*$)|" |
| # Match common command delimiters. |
| r"(?:^\s*|;|&&|[|][|]|(?P<prefix>(?:[$][(]|`)+)\s*)" |
| # Match command name. |
| r"\b(?P<command>chown|chgrp|chmod|mkdir|ln|rm|mv|cp|touch)\s+" |
| # Match command args across line splits. |
| r"(?P<args>(?:\\\n|[^\n;])*)", |
| re.MULTILINE, |
| ) |
| SHELL_TOKEN_SPLIT_REGEX = re.compile(r"(?:\\\n|\s)+", re.MULTILINE) |
| IGNORE_LINT_REGEX = re.compile(r"#\s+croslint:\s+disable") |
| |
| # TODO(python3.9): Change to functools.cache. |
| @functools.lru_cache(maxsize=None) |
| def GetIgnoreLookup() -> Dict[str, List[str]]: |
| """Returns the lookup table of upstart config lines to ignore. |
| |
| On first invocation this loads the list from upstart_exceptions.json. |
| Otherwise the cached copy is used. |
| |
| This is intended to be removed once the call sites are either migrated to |
| tmpfiles.d or have '# croslint: disable' added. |
| """ |
| FILE = Path(__file__).resolve() |
| exceptions_path = FILE.parent / "upstart_exceptions.json" |
| with exceptions_path.open("rb") as fp: |
| return json.load(fp) |
| |
| |
| def ExtractCommands(text: str) -> Generator[List[str], None, None]: |
| """Finds and normalizes audited commands.""" |
| for match in AUDITED_SHELL_COMMAND_REGEX.finditer(text, re.S): |
| # Skip comments. |
| if match.group("comment"): |
| continue |
| |
| cmd_prefix = match.group("prefix") |
| cmd_name = match.group("command") |
| cmd_args = match.group("args") |
| |
| # Skip if 'croslint: disable' is set. |
| if IGNORE_LINT_REGEX.search(cmd_args): |
| continue |
| |
| if cmd_prefix: |
| cmd = [SHELL_TOKEN_SPLIT_REGEX.sub(cmd_prefix, " ") + cmd_name] |
| else: |
| cmd = [cmd_name] |
| cmd.extend(x for x in SHELL_TOKEN_SPLIT_REGEX.split(cmd_args) if x) |
| yield cmd |
| |
| |
| def CheckForRequiredLines( |
| text: str, full_path: Path, tokens_to_find=None |
| ) -> bool: |
| """Check the upstart config for required clause.""" |
| ret = True |
| if not tokens_to_find: |
| tokens_to_find = { |
| "author", |
| "description", |
| "oom", |
| } |
| for line in text.splitlines(): |
| tokens = line.split() |
| try: |
| token = tokens[0] |
| except IndexError: |
| continue |
| |
| if tokens[0:3] == ["oom", "score", "-1000"]: |
| logging.error('Use "oom score never" instead of "oom score -1000".') |
| ret = False |
| |
| try: |
| tokens_to_find.remove(token) |
| except KeyError: |
| continue |
| |
| if not tokens_to_find: |
| break |
| if tokens_to_find: |
| logging.error( |
| 'Missing clauses from upstart script "%s": %s\nPlease see:\n%s', |
| full_path, |
| ", ".join(tokens_to_find), |
| DOC_RESOURCE_URL, |
| ) |
| ret = False |
| return ret |
| |
| |
| def CheckInitConf(full_path: Path, relaxed: bool) -> bool: |
| """Check an upstart conf file for linter errors.""" |
| ret = True |
| text = full_path.read_text(encoding="utf-8") |
| if not CheckForRequiredLines(text, full_path) and not relaxed: |
| ret = False |
| |
| label = os.path.basename(full_path) |
| ignore_set = set(GetIgnoreLookup().get(label, [])) if relaxed else () |
| |
| found = [] |
| for cmd in ExtractCommands(text): |
| norm_cmd = " ".join(cmd) |
| if norm_cmd not in ignore_set: |
| found.append(norm_cmd) |
| |
| if found: |
| logging.error('Init script "%s" has unsafe commands:', full_path) |
| for cmd in found: |
| logging.error(" %s", cmd) |
| logging.error( |
| "Please use a tmpfiles.d config for the commands or have " |
| 'them reviewed by security and add "# croslint: disable:"' |
| ) |
| ret = False |
| |
| if not whitespace.LintData(str(full_path), text) and not relaxed: |
| ret = False |
| |
| return ret |