def tearDown(self):
     """Delete all persisted data after the tests."""
     self.aminer_config = load_config(self.__configFilePath)
     persistence_file_name = build_persistence_file_name(self.aminer_config)
     if os.path.exists(persistence_file_name):
         shutil.rmtree(persistence_file_name)
     if not os.path.exists(persistence_file_name):
         os.makedirs(persistence_file_name)
     SecureOSFunctions.close_base_directory()
示例#2
0
    def open(self, reopen_flag=False):
        """Open the given resource.
        @param reopen_flag when True, attempt to reopen the same resource and check if it differs from the previously opened one.
        @raise Exception if valid log_stream_fd was already provided, is still open and reopen_flag is False.
        @raise OSError when opening failed with unexpected error.
        @return True if the resource was really opened or False if opening was not yet possible but should be attempted again."""
        if not reopen_flag and (self.log_file_fd != -1):
            raise Exception('Cannot reopen stream still open when not instructed to do so')
        log_file_fd = -1
        stat_data = None
        try:
            log_file_fd = SecureOSFunctions.secure_open_file(self.log_resource_name[7:], os.O_RDONLY)
            stat_data = os.fstat(log_file_fd)
        except OSError as openOsError:
            if log_file_fd != -1:
                os.close(log_file_fd)
            if openOsError.errno == errno.ENOENT:
                return False
            raise
        if not stat.S_ISREG(stat_data.st_mode):
            os.close(log_file_fd)
            raise Exception('Attempting to open non-regular file %s as file' % encode_byte_string_as_string(self.log_resource_name))

        if reopen_flag and (self.stat_data is not None) and (stat_data.st_ino == self.stat_data.st_ino) and (
                stat_data.st_dev == self.stat_data.st_dev):
            # Reopening was requested, but we would reopen the file already opened, which is of no use.
            os.close(log_file_fd)
            return False
        # This is a new file or a successful reopen attempt.
        self.log_file_fd = log_file_fd
        self.stat_data = stat_data
        return True
def open_persistence_file(file_name, flags):
    """This function opens the given persistence file. When O_CREAT was specified, the function will attempt to create the directories
    too."""
    if isinstance(file_name, str):
        file_name = file_name.encode()
    try:
        fd = SecureOSFunctions.secure_open_file(file_name, flags)
        return fd
    except OSError as openOsError:
        if ((flags & os.O_CREAT) == 0) or (openOsError.errno != errno.ENOENT):
            raise openOsError

    # Find out, which directory is missing by stating our way up.
    dir_name_length = file_name.rfind(b'/')
    if dir_name_length > 0:
        os.makedirs(file_name[:dir_name_length])
    return SecureOSFunctions.secure_open_file(file_name, flags)
 def handle_master_control_socket_receive(self):
     """
     Receive information from the parent process via the master control socket.
     This method may only be invoked when receiving is guaranteed to be nonblocking and to return data.
     """
     # We cannot fail with None here as the socket was in the readList.
     (received_fd, received_type_info,
      annotation_data) = SecureOSFunctions.receive_annoted_file_descriptor(
          self.master_control_socket)
     if received_type_info == b'logstream':
         repositioning_data = self.repositioning_data_dict.get(
             annotation_data, None)
         if repositioning_data is not None:
             del self.repositioning_data_dict[annotation_data]
         res = None
         if annotation_data.startswith(b'file://'):
             from aminer.input.LogStream import FileLogDataResource
             res = FileLogDataResource(
                 annotation_data,
                 received_fd,
                 repositioning_data=repositioning_data)
         elif annotation_data.startswith(b'unix://'):
             from aminer.input.LogStream import UnixSocketLogDataResource
             res = UnixSocketLogDataResource(annotation_data, received_fd)
         else:
             msg = 'Filedescriptor of unknown type received'
             logging.getLogger(DEBUG_LOG_NAME).error(msg)
             raise Exception(msg)
         # Make fd nonblocking.
         fd_flags = fcntl.fcntl(res.get_file_descriptor(), fcntl.F_GETFL)
         fcntl.fcntl(res.get_file_descriptor(), fcntl.F_SETFL,
                     fd_flags | os.O_NONBLOCK)
         log_stream = self.log_streams_by_name.get(res.get_resource_name())
         if log_stream is None:
             stream_atomizer = self.analysis_context.atomizer_factory.get_atomizer_for_resource(
                 res.get_resource_name())
             log_stream = LogStream(res, stream_atomizer)
             self.tracked_fds_dict[res.get_file_descriptor()] = log_stream
             self.log_streams_by_name[res.get_resource_name()] = log_stream
         else:
             log_stream.add_next_resource(res)
     elif received_type_info == b'remotecontrol':
         if self.remote_control_socket is not None:
             msg = 'Received another remote control socket: multiple remote control not supported (yet?).'
             logging.getLogger(DEBUG_LOG_NAME).error(msg)
             raise Exception(msg)
         self.remote_control_socket = socket.fromfd(received_fd,
                                                    socket.AF_UNIX,
                                                    socket.SOCK_STREAM, 0)
         os.close(received_fd)
         self.tracked_fds_dict[self.remote_control_socket.fileno(
         )] = self.remote_control_socket
     else:
         msg = 'Unhandled type info on received fd: %s' % repr(
             received_type_info)
         logging.getLogger(DEBUG_LOG_NAME).error(msg)
         raise Exception(msg)
 def setUp(self):
     """Set up all needed variables and remove persisted data."""
     PersistenceUtil.persistable_components = []
     self.aminer_config = load_config(self.__configFilePath)
     self.analysis_context = AnalysisContext(self.aminer_config)
     self.output_stream = StringIO()
     self.stream_printer_event_handler = StreamPrinterEventHandler(
         self.analysis_context, self.output_stream)
     persistence_dir_name = build_persistence_file_name(self.aminer_config)
     if os.path.exists(persistence_dir_name):
         shutil.rmtree(persistence_dir_name)
     if not os.path.exists(persistence_dir_name):
         os.makedirs(persistence_dir_name)
     initialize_loggers(self.aminer_config, os.getuid(), os.getgid())
     if isinstance(persistence_dir_name, str):
         persistence_dir_name = persistence_dir_name.encode()
     SecureOSFunctions.secure_open_base_directory(
         persistence_dir_name, os.O_RDONLY | os.O_DIRECTORY | os.O_PATH)
     PersistenceUtil.SKIP_PERSISTENCE_ID_WARNING = True
def replace_persistence_file(file_name, new_file_handle):
    """Replace the named file with the file referred by the handle."""
    try:
        os.unlink(file_name,
                  dir_fd=SecureOSFunctions.secure_open_base_directory())
    except OSError as openOsError:
        if openOsError.errno != errno.ENOENT:
            logging.getLogger(DEBUG_LOG_NAME).error(openOsError)
            raise openOsError

    tmp_file_name = os.readlink('/proc/self/fd/%d' % new_file_handle)
    if SecureOSFunctions.base_dir_path.decode() in file_name:
        file_name = file_name.replace(SecureOSFunctions.base_dir_path.decode(),
                                      '').lstrip('/')
    os.link(tmp_file_name,
            file_name,
            src_dir_fd=SecureOSFunctions.tmp_base_dir_fd,
            dst_dir_fd=SecureOSFunctions.secure_open_base_directory())
    os.unlink(tmp_file_name, dir_fd=SecureOSFunctions.tmp_base_dir_fd)
示例#7
0
    def handleMasterControlSocketReceive(self):
        """Receive information from the parent process via the master
    control socket. This method may only be invoked when receiving
    is guaranteed to be nonblocking and to return data."""

        # We cannot fail with None here as the socket was in the readList.
        (receivedFd, receivedTypeInfo, annotationData) = \
            SecureOSFunctions.receiveAnnotedFileDescriptor(self.masterControlSocket)
        if receivedTypeInfo == b'logstream':
            repositioningData = self.repositioningDataDict.get(
                annotationData, None)
            if repositioningData != None:
                del self.repositioningDataDict[annotationData]
            resource = None
            if annotationData.startswith(b'file://'):
                from aminer.input.LogStream import FileLogDataResource
                resource = FileLogDataResource(annotationData, receivedFd, \
                    repositioningData=repositioningData)
            elif annotationData.startswith(b'unix://'):
                from aminer.input.LogStream import UnixSocketLogDataResource
                resource = UnixSocketLogDataResource(annotationData,
                                                     receivedFd)
            else:
                raise Exception('Filedescriptor of unknown type received')


# Make fd nonblocking.
            fdFlags = fcntl.fcntl(resource.getFileDescriptor(), fcntl.F_GETFL)
            fcntl.fcntl(resource.getFileDescriptor(), fcntl.F_SETFL,
                        fdFlags | os.O_NONBLOCK)
            logStream = self.logStreamsByName.get(resource.getResourceName())
            if logStream is None:
                streamAtomizer = self.analysisContext.atomizerFactory.getAtomizerForResource(
                    resource.getResourceName())
                logStream = LogStream(resource, streamAtomizer)
                self.trackedFdsDict[resource.getFileDescriptor()] = logStream
                self.logStreamsByName[resource.getResourceName()] = logStream
            else:
                logStream.addNextResource(resource)
        elif receivedTypeInfo == b'remotecontrol':
            if self.remoteControlSocket != None:
                raise Exception('Received another remote control ' \
                    'socket: multiple remote control not (yet?) supported.')
            self.remoteControlSocket = socket.fromfd(receivedFd,
                                                     socket.AF_UNIX,
                                                     socket.SOCK_STREAM, 0)
            os.close(receivedFd)
            self.trackedFdsDict[self.remoteControlSocket.fileno()] = \
                self.remoteControlSocket
        else:
            raise Exception('Unhandled type info on received fd: %s' %
                            (repr(receivedTypeInfo)))
def open_persistence_file(file_name, flags):
    """
    Open the given persistence file.
    When O_CREAT was specified, the function will attempt to create the directories too.
    """
    if isinstance(file_name, str):
        file_name = file_name.encode()
    try:
        fd = SecureOSFunctions.secure_open_file(file_name, flags)
        return fd
    except OSError as openOsError:
        if ((flags & os.O_CREAT) == 0) or (openOsError.errno != errno.ENOENT):
            logging.getLogger(DEBUG_LOG_NAME).error(openOsError)
            raise openOsError
    create_missing_directories(file_name)
def run_analysis_child(aminer_config, program_name):
    """Run the Analysis Child."""
    # Verify existance and ownership of persistence directory.
    logging.getLogger(
        AMinerConfig.REMOTE_CONTROL_LOG_NAME).info('aminer started.')
    logging.getLogger(AMinerConfig.DEBUG_LOG_NAME).info('aminer started.')
    persistence_dir_name = aminer_config.config_properties.get(
        AMinerConfig.KEY_PERSISTENCE_DIR, AMinerConfig.DEFAULT_PERSISTENCE_DIR)
    from aminer.util import SecureOSFunctions
    if isinstance(persistence_dir_name, str):
        persistence_dir_name = persistence_dir_name.encode()
    persistence_dir_fd = SecureOSFunctions.secure_open_base_directory(
        persistence_dir_name, os.O_RDONLY | os.O_DIRECTORY | os.O_PATH)
    stat_result = os.fstat(persistence_dir_fd)
    import stat
    if ((not stat.S_ISDIR(stat_result.st_mode))
            or ((stat_result.st_mode & stat.S_IRWXU) != 0o700)
            or (stat_result.st_uid != os.getuid())
            or (stat_result.st_gid != os.getgid())):
        msg = 'FATAL: persistence directory "%s" has to be owned by analysis process (uid %d!=%d, gid %d!=%d) and have access mode 0700 ' \
              'only!' % (repr(persistence_dir_name), stat_result.st_uid, os.getuid(), stat_result.st_gid, os.getgid())
        print(msg, file=sys.stderr)
        logging.getLogger(AMinerConfig.DEBUG_LOG_NAME).critical(msg)
        sys.exit(1)
    import posix1e
    # O_PATH is problematic when checking ACL. However it is possible to check the ACL using the file name.
    if posix1e.has_extended(persistence_dir_name):
        msg = 'WARNING: SECURITY: Extended POSIX ACLs are set in %s, but not supported by the aminer. Backdoor access could be possible.'\
              % persistence_dir_name.decode()
        print(msg, file=sys.stderr)
        logging.getLogger(AMinerConfig.DEBUG_LOG_NAME).warning(msg)

    from aminer.AnalysisChild import AnalysisChild
    child = AnalysisChild(program_name, aminer_config)
    # This function call will only return on error or signal induced normal termination.
    child_return_status = child.run_analysis(3)
    if child_return_status == 0:
        sys.exit(0)
    msg = '%s: run_analysis terminated with unexpected status %d' % (
        program_name, child_return_status)
    print(msg, file=sys.stderr)
    logging.getLogger(AMinerConfig.DEBUG_LOG_NAME).error(msg)
    sys.exit(1)
示例#10
0
def main():
    """Run the aminer main program."""
    # Extract program name, but only when sure to contain no problematic characters.
    warnings.filterwarnings('ignore', category=ImportWarning)
    program_name = sys.argv[0].split('/')[-1]
    if (program_name == '.') or (program_name == '..') or (re.match(
            '^[a-zA-Z0-9._-]+$', program_name) is None):
        print('Invalid program name, check your execution args',
              file=sys.stderr)
        sys.exit(1)

    # We will not read stdin from here on, so get rid of it immediately, thus aberrant child cannot manipulate caller's stdin using it.
    stdin_fd = os.open('/dev/null', os.O_RDONLY)
    os.dup2(stdin_fd, 0)
    os.close(stdin_fd)

    help_message = 'aminer - logdata-anomaly-miner\n'
    if supports_color():
        help_message += colflame
    else:
        help_message += flame
    parser = argparse.ArgumentParser(
        description=help_message,
        formatter_class=argparse.RawTextHelpFormatter)
    parser.add_argument('-v',
                        '--version',
                        action='version',
                        version=__version_string__)
    parser.add_argument('-u',
                        '--check-updates',
                        action='store_true',
                        help='check if updates for the aminer are available.')
    parser.add_argument('-c',
                        '--config',
                        default='/etc/aminer/config.yml',
                        type=str,
                        help='path to the config-file')
    parser.add_argument('-D',
                        '--daemon',
                        action='store_false',
                        help='run as a daemon process')
    parser.add_argument(
        '-s',
        '--stat',
        choices=["0", "1", "2"],
        type=str,
        help=
        'set the stat level. Possible stat-levels are 0 for no statistics, 1 for normal statistic level and 2 for '
        'verbose statistics.')
    parser.add_argument(
        '-d',
        '--debug',
        choices=["0", "1", "2"],
        type=str,
        help=
        'set the debug level. Possible debug-levels are 0 for no debugging, 1 for normal output (INFO and above), 2 '
        'for printing all debug information.')
    parser.add_argument('--run-analysis',
                        action='store_true',
                        help='enable/disable analysis')
    parser.add_argument('-C',
                        '--clear',
                        action='store_true',
                        help='removes all persistence directories')
    parser.add_argument('-r',
                        '--remove',
                        action='append',
                        type=str,
                        help='removes a specific persistence directory')
    parser.add_argument('-R',
                        '--restore',
                        type=str,
                        help='restore a persistence backup')
    parser.add_argument(
        '-f',
        '--from-begin',
        action='store_true',
        help='removes RepositioningData before starting the aminer')
    parser.add_argument(
        '-o',
        '--offline-mode',
        action='store_true',
        help='stop the aminer after all logs have been processed.')
    parser.add_argument(
        "--config-properties",
        metavar="KEY=VALUE",
        nargs='+',
        help=
        "Set a number of config_properties by using key-value pairs (do not put spaces before or after the = sign). "
        "If a value contains spaces, you should define it with double quotes: 'foo=\"this is a sentence\". Note that "
        "values are always treated as strings. If values are already defined in the config_properties, the input "
        "types are converted to the ones already existing.")

    args = parser.parse_args()

    if args.check_updates:
        import urllib3
        url = 'https://raw.githubusercontent.com/ait-aecid/logdata-anomaly-miner/main/source/root/usr/lib/logdata-anomaly-miner/metadata.py'
        http = urllib3.PoolManager()
        r = http.request('GET', url, preload_content=True)
        metadata = r.data.decode()
        http.clear()
        lines = metadata.split('\n')
        curr_version = None
        for line in lines:
            if '__version__ = ' in line:
                curr_version = line.split('__version__ = ')[1].strip('"')
                break
        if __version__ == curr_version:
            print("The current aminer version %s is installed." % curr_version)
        else:
            print(
                "A new aminer version exists (%s). Currently version %s is installed."
                % (curr_version, __version__))
            print("Use git pull to update the aminer version.")
        sys.exit(0)

    config_file_name = args.config
    run_in_foreground_flag = args.daemon
    run_analysis_child_flag = args.run_analysis
    clear_persistence_flag = args.clear
    remove_persistence_dirs = args.remove
    from_begin_flag = args.from_begin
    global offline_mode  # skipcq: PYL-W0603
    offline_mode = args.offline_mode
    if args.restore is not None and ('.' in args.restore
                                     or '/' in args.restore):
        parser.error('The restore path %s must not contain any . or /' %
                     args.restore)
    if args.remove is not None:
        for remove in args.remove:
            if '.' in remove or '/' in remove:
                parser.error('The remove path %s must not contain any . or /' %
                             remove)
    restore_relative_persistence_path = args.restore
    stat_level = 1
    debug_level = 1
    stat_level_console_flag = False
    debug_level_console_flag = False
    if args.stat is not None:
        stat_level = int(args.stat)
        stat_level_console_flag = True
    if args.debug is not None:
        debug_level = int(args.debug)
        debug_level_console_flag = True

    # Load the main configuration file.
    if not os.path.exists(config_file_name):
        print('%s: config "%s" not (yet) available!' %
              (program_name, config_file_name),
              file=sys.stderr)
        sys.exit(1)

    # using the solution here to override config_properties:
    # https://stackoverflow.com/questions/27146262/create-variable-key-value-pairs-with-argparse-python
    use_temp_config = False
    config_properties = parse_vars(args.config_properties)
    if args.config_properties and "LearnMode" in config_properties:
        ymlext = [".YAML", ".YML", ".yaml", ".yml"]
        extension = os.path.splitext(config_file_name)[1]
        if extension in ymlext:
            use_temp_config = True
            fd, temp_config = tempfile.mkstemp(suffix=".yml")
            with open(config_file_name) as f:
                for line in f:
                    if "LearnMode" in line:
                        line = "LearnMode: %s" % config_properties["LearnMode"]
                    os.write(fd, line.encode())
            config_file_name = temp_config
            os.close(fd)
        else:
            msg = "The LearnMode parameter does not exist in .py configs!"
            print(msg, sys.stderr)
            logging.getLogger(AminerConfig.DEBUG_LOG_NAME).error(msg)
            sys.exit(1)

    # Minimal import to avoid loading too much within the privileged process.
    try:
        aminer_config = AminerConfig.load_config(config_file_name)
        if use_temp_config:
            os.remove(config_file_name)
            config_file_name = args.config
    except ValueError:
        sys.exit(1)

    for config_property in config_properties:
        if config_property == "LearnMode":
            continue
        old_value = aminer_config.config_properties.get(config_property)
        value = config_properties[config_property]
        if old_value is not None:
            try:
                if isinstance(old_value, bool):
                    if value == "True":
                        value = True
                    elif value == "False":
                        value = False
                    else:
                        msg = "The %s parameter must be of type %s!" % (
                            config_property, type(old_value))
                        print(msg, sys.stderr)
                        logging.getLogger(
                            AminerConfig.DEBUG_LOG_NAME).error(msg)
                        sys.exit(1)
                elif isinstance(old_value, int):
                    value = int(value)
                elif isinstance(old_value, float):
                    value = float(value)
                elif isinstance(old_value, list):
                    value = ast.literal_eval(value)
            except ValueError:
                msg = "The %s parameter must be of type %s!" % (
                    config_property, type(old_value))
                print(msg, sys.stderr)
                logging.getLogger(AminerConfig.DEBUG_LOG_NAME).error(msg)
                sys.exit(1)
        else:
            msg = "The %s parameter is not set in the config. It will be treated as a string!" % config_property
            print("WARNING: " + msg, sys.stderr)
            logging.getLogger(AminerConfig.DEBUG_LOG_NAME).warning(msg)
        aminer_config.config_properties[config_property] = value

    persistence_dir = aminer_config.config_properties.get(
        AminerConfig.KEY_PERSISTENCE_DIR, AminerConfig.DEFAULT_PERSISTENCE_DIR)
    child_user_name = aminer_config.config_properties.get(
        AminerConfig.KEY_AMINER_USER)
    child_group_name = aminer_config.config_properties.get(
        AminerConfig.KEY_AMINER_GROUP)
    child_user_id = -1
    child_group_id = -1
    try:
        if child_user_name is not None:
            child_user_id = getpwnam(child_user_name).pw_uid
        if child_group_name is not None:
            child_group_id = getgrnam(child_group_name).gr_gid
    except:  # skipcq: FLK-E722
        print('Failed to resolve %s or %s' %
              (AminerConfig.KEY_AMINER_USER, AminerConfig.KEY_AMINER_GROUP),
              file=sys.stderr)
        sys.exit(1)

    if not stat_level_console_flag and AminerConfig.KEY_LOG_STAT_LEVEL in aminer_config.config_properties:
        stat_level = aminer_config.config_properties[
            AminerConfig.KEY_LOG_STAT_LEVEL]
    if not debug_level_console_flag and AminerConfig.KEY_LOG_DEBUG_LEVEL in aminer_config.config_properties:
        debug_level = aminer_config.config_properties[
            AminerConfig.KEY_LOG_DEBUG_LEVEL]
    if AminerConfig.CONFIG_KEY_ENCODING in aminer_config.config_properties:
        AminerConfig.ENCODING = aminer_config.config_properties[
            AminerConfig.CONFIG_KEY_ENCODING]

    AminerConfig.STAT_LEVEL = stat_level
    AminerConfig.DEBUG_LEVEL = debug_level

    initialize_loggers(aminer_config, child_user_id, child_group_id)

    if restore_relative_persistence_path is not None and (
            clear_persistence_flag or remove_persistence_dirs):
        msg = 'The --restore parameter removes all persistence files. Do not use this parameter with --Clear or --Remove!'
        print(msg, sys.stderr)
        logging.getLogger(AminerConfig.DEBUG_LOG_NAME).error(msg)
        sys.exit(1)

    if clear_persistence_flag:
        if remove_persistence_dirs:
            msg = 'The --clear and --remove arguments must not be used together!'
            print(msg, file=sys.stderr)
            logging.getLogger(AminerConfig.DEBUG_LOG_NAME).error(msg)
            sys.exit(1)
        clear_persistence(persistence_dir)

    if remove_persistence_dirs:
        persistence_dir_name = aminer_config.config_properties.get(
            AminerConfig.KEY_PERSISTENCE_DIR,
            AminerConfig.DEFAULT_PERSISTENCE_DIR)
        for filename in os.listdir(persistence_dir_name):
            file_path = os.path.join(persistence_dir_name, filename)
            try:
                if not os.path.isdir(file_path):
                    msg = 'The aminer persistence directory should not contain any files.'
                    print(msg, file=sys.stderr)
                    logging.getLogger(AminerConfig.DEBUG_LOG_NAME).warning(msg)
                    continue
                shutil.rmtree(file_path)
            except OSError as e:
                msg = 'Failed to delete %s. Reason: %s' % (file_path, e)
                print(msg, file=sys.stderr)
                logging.getLogger(AminerConfig.DEBUG_LOG_NAME).error(msg)

        for filename in remove_persistence_dirs:
            file_path = os.path.join(persistence_dir, filename)
            try:
                if not os.path.exists(file_path):
                    continue
                if not os.path.isdir(file_path):
                    msg = 'The aminer persistence directory should not contain any files.'
                    print(msg, file=sys.stderr)
                    logging.getLogger(AminerConfig.DEBUG_LOG_NAME).warning(msg)
                    continue
                shutil.rmtree(file_path)
            except OSError as e:
                msg = 'Failed to delete %s. Reason: %s' % (file_path, e)
                print(msg, file=sys.stderr)
                logging.getLogger(AminerConfig.DEBUG_LOG_NAME).error(msg)

    if restore_relative_persistence_path is not None:
        absolute_persistence_path = os.path.join(
            persistence_dir, 'backup', restore_relative_persistence_path)
        if not os.path.exists(absolute_persistence_path):
            msg = '%s does not exist. Continuing without restoring persistence.' % absolute_persistence_path
            print(msg, file=sys.stderr)
            logging.getLogger(AminerConfig.DEBUG_LOG_NAME).warning(msg)
        else:
            clear_persistence(persistence_dir)
            copytree(absolute_persistence_path, persistence_dir)
            persistence_dir_fd = SecureOSFunctions.secure_open_base_directory(
                persistence_dir, os.O_RDONLY | os.O_DIRECTORY | os.O_PATH)
            for dirpath, _dirnames, filenames in os.walk(persistence_dir):
                os.chown(dirpath,
                         child_user_id,
                         child_group_id,
                         dir_fd=persistence_dir_fd,
                         follow_symlinks=False)
                for filename in filenames:
                    os.chown(os.path.join(dirpath, filename),
                             child_user_id,
                             child_user_id,
                             dir_fd=persistence_dir_fd,
                             follow_symlinks=False)

    if from_begin_flag:
        repositioning_data_path = os.path.join(
            aminer_config.config_properties.get(
                AminerConfig.KEY_PERSISTENCE_DIR,
                AminerConfig.DEFAULT_PERSISTENCE_DIR), 'AnalysisChild',
            'RepositioningData')
        if os.path.exists(repositioning_data_path):
            os.remove(repositioning_data_path)

    if run_analysis_child_flag:
        # Call analysis process, this function will never return.
        run_analysis_child(aminer_config, program_name)

    # Start importing of aminer specific components after reading of "config.py" to allow replacement of components via sys.path
    # from within configuration.
    log_sources_list = aminer_config.config_properties.get(
        AminerConfig.KEY_LOG_SOURCES_LIST)
    if (log_sources_list is None) or not log_sources_list:
        msg = '%s: %s not defined' % (program_name,
                                      AminerConfig.KEY_LOG_SOURCES_LIST)
        print(msg, file=sys.stderr)
        logging.getLogger(AminerConfig.DEBUG_LOG_NAME).error(msg)
        sys.exit(1)

    # Now create the management entries for each logfile.
    log_data_resource_dict = {}
    for log_resource_name in log_sources_list:
        # From here on log_resource_name is a byte array.
        log_resource_name = decode_string_as_byte_string(log_resource_name)
        log_resource = None
        if log_resource_name.startswith(b'file://'):
            log_resource = FileLogDataResource(log_resource_name, -1)
        elif log_resource_name.startswith(b'unix://'):
            log_resource = UnixSocketLogDataResource(log_resource_name, -1)
        else:
            msg = 'Unsupported schema in %s: %s' % (
                AminerConfig.KEY_LOG_SOURCES_LIST, repr(log_resource_name))
            print(msg, file=sys.stderr)
            logging.getLogger(AminerConfig.DEBUG_LOG_NAME).error(msg)
            sys.exit(1)
        if not os.path.exists(log_resource_name[7:].decode()):
            msg = "WARNING: file or socket '%s' does not exist (yet)!" % log_resource_name[
                7:].decode()
            print(msg, file=sys.stderr)
            logging.getLogger(AminerConfig.DEBUG_LOG_NAME).warning(msg)
        try:
            log_resource.open()
        except OSError as open_os_error:
            if open_os_error.errno == errno.EACCES:
                msg = '%s: no permission to access %s' % (
                    program_name, repr(log_resource_name))
                print(msg, file=sys.stderr)
                logging.getLogger(AminerConfig.DEBUG_LOG_NAME).error(msg)
                sys.exit(1)
            else:
                msg = '%s: unexpected error opening %s: %d (%s)' % (
                    program_name, repr(log_resource_name), open_os_error.errno,
                    os.strerror(open_os_error.errno))
                print(msg, file=sys.stderr)
                logging.getLogger(AminerConfig.DEBUG_LOG_NAME).error(msg)
                sys.exit(1)
        log_data_resource_dict[log_resource_name] = log_resource

    # Create the remote control socket, if any. Do this in privileged mode to allow binding it at arbitrary locations and support restricted
    # permissions of any type for current (privileged) uid.
    remote_control_socket_name = aminer_config.config_properties.get(
        AminerConfig.KEY_REMOTE_CONTROL_SOCKET_PATH, None)
    remote_control_socket = None
    if remote_control_socket_name is not None:
        if os.path.exists(remote_control_socket_name):
            try:
                os.unlink(remote_control_socket_name)
            except OSError:
                msg = 'Failed to clean up old remote control socket at %s' % remote_control_socket_name
                print(msg, file=sys.stderr)
                logging.getLogger(AminerConfig.DEBUG_LOG_NAME).error(msg)
                sys.exit(1)
        # Create the local socket: there is no easy way to create it with correct permissions, hence a fork is needed, setting umask,
        # bind the socket. It is also recommended to create the socket in a directory having the correct permissions already.
        remote_control_socket = socket.socket(socket.AF_UNIX,
                                              socket.SOCK_STREAM)
        remote_control_socket.setblocking(False)
        bind_child_pid = os.fork()
        if bind_child_pid == 0:
            os.umask(0o177)
            remote_control_socket.bind(remote_control_socket_name)
            # Do not perform any cleanup, flushing of streams. Use _exit(0) to avoid interference with fork.
            os._exit(0)  # skipcq: PYL-W0212
        os.waitpid(bind_child_pid, 0)
        remote_control_socket.listen(4)

    # Now have checked all we can get from the configuration in the privileged process. Detach from the TTY when in daemon mode.
    if not run_in_foreground_flag:
        child_pid = 0
        try:
            # Fork a child to make sure, we are not the process group leader already.
            child_pid = os.fork()
        except Exception as fork_exception:  # skipcq: PYL-W0703
            msg = 'Failed to daemonize: %s' % fork_exception
            print(msg, file=sys.stderr)
            logging.getLogger(AminerConfig.DEBUG_LOG_NAME).error(msg)
            sys.exit(1)
        if child_pid != 0:
            # This is the parent.
            os._exit(0)  # skipcq: PYL-W0212
        # This is the child. Create a new session and become process group leader. Here we get rid of the controlling tty.
        os.setsid()
        # Fork again to become an orphaned process not being session leader, hence not able to get a controlling tty again.
        try:
            child_pid = os.fork()
        except Exception as fork_exception:  # skipcq: PYL-W0703
            msg = 'Failed to daemonize: %s' % fork_exception
            print(msg, file=sys.stderr)
            logging.getLogger(AminerConfig.DEBUG_LOG_NAME).error(msg)
            sys.exit(1)
        if child_pid != 0:
            # This is the parent.
            os._exit(0)  # skipcq: PYL-W0212
        # Move to root directory to avoid lingering in some cwd someone else might want to unmount.
        os.chdir('/')
        # Change the umask here to clean all group/other mask bits so that accidentially created files are not accessible by other.
        os.umask(0o77)

    # Install a signal handler catching common stop signals and relaying it to all children for sure.
    # skipcq: PYL-W0603
    global child_termination_triggered_flag
    child_termination_triggered_flag = False

    def graceful_shutdown_handler(_signo, _stackFrame):
        """React on typical shutdown signals."""
        msg = '%s: caught signal, shutting down' % program_name
        print(msg, file=sys.stderr)
        logging.getLogger(AminerConfig.DEBUG_LOG_NAME).info(msg)
        # Just set the flag. It is likely, that child received same signal also so avoid multiple signaling, which could interrupt the
        # shutdown procedure again.
        # skipcq: PYL-W0603
        global child_termination_triggered_flag
        child_termination_triggered_flag = True

    import signal
    signal.signal(signal.SIGHUP, graceful_shutdown_handler)
    signal.signal(signal.SIGINT, graceful_shutdown_handler)
    signal.signal(signal.SIGTERM, graceful_shutdown_handler)

    # Now create the socket to connect the analysis child.
    (parent_socket, child_socket) = socket.socketpair(socket.AF_UNIX,
                                                      socket.SOCK_DGRAM, 0)
    # Have it nonblocking from here on.
    parent_socket.setblocking(False)
    child_socket.setblocking(False)

    # Use normal fork, we should have been detached from TTY already. Flush stderr to avoid duplication of output if both child and
    # parent want to write something.
    sys.stderr.flush()
    child_pid = os.fork()
    if child_pid == 0:
        # Relocate the child socket fd to 3 if needed
        if child_socket.fileno() != 3:
            os.dup2(child_socket.fileno(), 3)
            child_socket.close()

        # Clear the supplementary groups before dropping privileges. This makes only sense when changing the uid or gid.
        if os.getuid() == 0:
            if ((child_user_id != -1) and (child_user_id != os.getuid())) or (
                (child_group_id != -1) and (child_group_id != os.getgid())):
                os.setgroups([])

            # Drop privileges before executing child. setuid/gid will raise an exception when call has failed.
            if child_group_id != -1:
                os.setgid(child_group_id)
            if child_user_id != -1:
                os.setuid(child_user_id)
        else:
            msg = 'INFO: No privilege separation when started as unprivileged user'
            print(msg, file=sys.stderr)
            tmp_username = aminer_config.config_properties.get(
                AminerConfig.KEY_AMINER_USER)
            tmp_group = aminer_config.config_properties.get(
                AminerConfig.KEY_AMINER_GROUP)
            aminer_user_id = -1
            aminer_group_id = -1
            try:
                if tmp_username is not None:
                    aminer_user_id = getpwnam(tmp_username).pw_uid
                if tmp_group is not None:
                    aminer_group_id = getgrnam(tmp_group).gr_gid
            except:  # skipcq: FLK-E722
                print('Failed to resolve %s or %s' %
                      (AminerConfig.KEY_AMINER_USER,
                       AminerConfig.KEY_AMINER_GROUP),
                      file=sys.stderr)
                sys.exit(1)

            initialize_loggers(aminer_config, aminer_user_id, aminer_group_id)
            logging.getLogger(AminerConfig.DEBUG_LOG_NAME).info(msg)

        # Now resolve the specific analysis configuration file (if any).
        analysis_config_file_name = aminer_config.config_properties.get(
            AminerConfig.KEY_ANALYSIS_CONFIG_FILE, None)
        if analysis_config_file_name is None:
            analysis_config_file_name = config_file_name
        elif not os.path.isabs(analysis_config_file_name):
            analysis_config_file_name = os.path.join(
                os.path.dirname(config_file_name), analysis_config_file_name)

        # This is the child. Close all parent file descriptors, we do not need. Perhaps this could be done more elegantly.
        for close_fd in range(4, 1 << 16):
            try:
                os.close(close_fd)
            except OSError as open_os_error:
                if open_os_error.errno == errno.EBADF:
                    continue
                msg = '%s: unexpected exception closing file descriptors: %s' % (
                    program_name, open_os_error)
                print(msg, file=sys.stderr)
                logging.getLogger(AminerConfig.DEBUG_LOG_NAME).error(msg)
                # Flush stderr before exit without any cleanup.
                sys.stderr.flush()
                os._exit(1)  # skipcq: PYL-W0212

        # Now execute the very same program again, but user might have moved or renamed it meanwhile. This would be problematic with
        # SUID-binaries (which we do not yet support). Do NOT just fork but also exec to avoid child circumventing
        # parent's ALSR due to cloned kernel VMA.
        exec_args = [
            'aminerChild', '--run-analysis', '--config',
            analysis_config_file_name, '--stat',
            str(stat_level), '--debug',
            str(debug_level)
        ]
        if offline_mode:
            exec_args.append("--offline-mode")
        if args.config_properties:
            exec_args.append("--config-properties")
            for config_property in args.config_properties:
                exec_args.append(config_property)
        os.execv(sys.argv[0], exec_args)  # skipcq: BAN-B606
        msg = 'Failed to execute child process'
        print(msg, file=sys.stderr)
        logging.getLogger(AminerConfig.DEBUG_LOG_NAME).error(msg)
        sys.stderr.flush()
        os._exit(1)  # skipcq: PYL-W0212
    child_socket.close()

    # Send all log resource information currently available to child process.
    for log_resource_name, log_resource in log_data_resource_dict.items():
        if (log_resource
                is not None) and (log_resource.get_file_descriptor() >= 0):
            SecureOSFunctions.send_logstream_descriptor(
                parent_socket, log_resource.get_file_descriptor(),
                log_resource_name)
            log_resource.close()

    # Send the remote control server socket, if any and close it afterwards. It is not needed any more on parent side.
    if remote_control_socket is not None:
        SecureOSFunctions.send_annotated_file_descriptor(
            parent_socket, remote_control_socket.fileno(), 'remotecontrol', '')
        remote_control_socket.close()

    exit_status = 0
    child_termination_triggered_count = 0
    while True:
        if child_termination_triggered_flag:
            if child_termination_triggered_count == 0:
                time.sleep(1)
            elif child_termination_triggered_count < 5:
                os.kill(child_pid, signal.SIGTERM)
            else:
                os.kill(0, signal.SIGKILL)
            child_termination_triggered_count += 1
        (sig_child_pid, sig_status) = os.waitpid(-1, os.WNOHANG)
        if sig_child_pid != 0:
            if sig_child_pid == child_pid:
                if child_termination_triggered_flag or offline_mode:
                    # This was expected, just terminate.
                    break
                msg = '%s: Analysis child process %d terminated unexpectedly with signal 0x%x' % (
                    program_name, sig_child_pid, sig_status)
                print(msg, file=sys.stderr)
                logging.getLogger(AminerConfig.DEBUG_LOG_NAME).error(msg)
                exit_status = 1
                break
            # So the child has been cloned, the clone has terminated. This should not happen either.
            msg = '%s: untracked child %d terminated with with signal 0x%x' % (
                program_name, sig_child_pid, sig_status)
            print(msg, file=sys.stderr)
            logging.getLogger(AminerConfig.DEBUG_LOG_NAME).error(msg)
            exit_status = 1

        # Child information handled, scan for rotated logfiles or other resources, where reopening might make sense.
        for log_resouce_name, log_data_resource in log_data_resource_dict.items(
        ):
            try:
                if not log_data_resource.open(reopen_flag=True):
                    continue
            except OSError as open_os_error:
                if open_os_error.errno == errno.EACCES:
                    msg = '%s: no permission to access %s' % (program_name,
                                                              log_resouce_name)
                    print(msg, file=sys.stderr)
                    logging.getLogger(AminerConfig.DEBUG_LOG_NAME).error(msg)
                else:
                    msg = '%s: unexpected error reopening %s: %d (%s)' % (
                        program_name, log_resouce_name, open_os_error.errno,
                        os.strerror(open_os_error.errno))
                    print(msg, file=sys.stderr)
                    logging.getLogger(AminerConfig.DEBUG_LOG_NAME).error(msg)
                exit_status = 2
                continue
            SecureOSFunctions.send_logstream_descriptor(
                parent_socket, log_data_resource.get_file_descriptor(),
                log_resouce_name)
            log_data_resource.close()
        time.sleep(1)
    parent_socket.close()
    SecureOSFunctions.close_base_directory()
    SecureOSFunctions.close_log_directory()
    sys.exit(exit_status)
示例#11
0
def initialize_loggers(aminer_config, aminer_user_id, aminer_grp_id):
    """Initialize all loggers."""
    datefmt = '%d/%b/%Y:%H:%M:%S %z'

    log_dir = aminer_config.config_properties.get(AminerConfig.KEY_LOG_DIR,
                                                  AminerConfig.DEFAULT_LOG_DIR)
    if log_dir == AminerConfig.DEFAULT_LOG_DIR:
        try:
            if not os.path.isdir(log_dir):
                persistence_dir_path = aminer_config.config_properties.get(
                    AminerConfig.KEY_PERSISTENCE_DIR,
                    AminerConfig.DEFAULT_PERSISTENCE_DIR)
                persistence_dir_fd = SecureOSFunctions.secure_open_base_directory(
                    persistence_dir_path,
                    os.O_RDONLY | os.O_DIRECTORY | os.O_PATH)
                if SecureOSFunctions.base_dir_path.decode(
                ) == AminerConfig.DEFAULT_PERSISTENCE_DIR:
                    relative_path_log_dir = os.path.split(
                        AminerConfig.DEFAULT_LOG_DIR)[1]
                    os.mkdir(relative_path_log_dir, dir_fd=persistence_dir_fd)
                    os.chown(relative_path_log_dir,
                             aminer_user_id,
                             aminer_grp_id,
                             dir_fd=persistence_dir_fd,
                             follow_symlinks=False)
        except OSError as e:
            if e.errno != errno.EEXIST:
                msg = 'Unable to create log-directory: %s' % log_dir
            else:
                msg = e
            logging.getLogger(AminerConfig.DEBUG_LOG_NAME).error(
                msg.strip('\n'))
            print(msg, file=sys.stderr)

    tmp_value = aminer_config.config_properties.get(
        AminerConfig.KEY_REMOTE_CONTROL_LOG_FILE)
    if tmp_value is not None and b'/' in tmp_value:
        print(
            '%s attribute must not contain a full directory path, but only the filename.'
            % AminerConfig.KEY_REMOTE_CONTROL_LOG_FILE,
            file=sys.stderr)
        sys.exit(1)
    tmp_value = aminer_config.config_properties.get(
        AminerConfig.KEY_STAT_LOG_FILE)
    if tmp_value is not None and b'/' in tmp_value:
        print(
            '%s attribute must not contain a full directory path, but only the filename.'
            % AminerConfig.KEY_STAT_LOG_FILE,
            file=sys.stderr)
        sys.exit(1)
    tmp_value = aminer_config.config_properties.get(
        AminerConfig.KEY_DEBUG_LOG_FILE)
    if tmp_value is not None and b'/' in tmp_value:
        print(
            '%s attribute must not contain a full directory path, but only the filename.'
            % AminerConfig.KEY_DEBUG_LOG_FILE,
            file=sys.stderr)
        sys.exit(1)

    max_bytes = aminer_config.config_properties.get(
        AminerConfig.KEY_LOG_ROTATION_MAX_BYTES,
        AminerConfig.DEFAULT_LOG_ROTATION_MAX_BYTES)
    backup_count = aminer_config.config_properties.get(
        AminerConfig.KEY_LOG_ROTATION_BACKUP_COUNT,
        AminerConfig.DEFAULT_LOG_ROTATION_BACKUP_COUNT)
    log_dir_fd = SecureOSFunctions.secure_open_log_directory(
        log_dir, os.O_RDONLY | os.O_DIRECTORY | os.O_PATH)
    rc_logger = logging.getLogger(AminerConfig.REMOTE_CONTROL_LOG_NAME)
    rc_logger.setLevel(logging.DEBUG)
    remote_control_log_file = aminer_config.config_properties.get(
        AminerConfig.KEY_REMOTE_CONTROL_LOG_FILE,
        os.path.join(log_dir, AminerConfig.DEFAULT_REMOTE_CONTROL_LOG_FILE))
    if not remote_control_log_file.startswith(log_dir):
        remote_control_log_file = os.path.join(log_dir,
                                               remote_control_log_file)
    try:
        rc_file_handler = RotatingFileHandler(remote_control_log_file,
                                              maxBytes=max_bytes,
                                              backupCount=backup_count)
        os.chown(remote_control_log_file,
                 aminer_user_id,
                 aminer_grp_id,
                 dir_fd=log_dir_fd,
                 follow_symlinks=False)
    except OSError as e:
        print('Could not create or open %s: %s. Stopping..' %
              (remote_control_log_file, e),
              file=sys.stderr)
        sys.exit(1)
    rc_file_handler.setFormatter(
        logging.Formatter(fmt='%(asctime)s %(levelname)s %(message)s',
                          datefmt=datefmt))
    rc_logger.addHandler(rc_file_handler)
    logging.addLevelName(15, "REMOTECONTROL")

    stat_logger = logging.getLogger(AminerConfig.STAT_LOG_NAME)
    stat_logger.setLevel(logging.INFO)
    stat_log_file = aminer_config.config_properties.get(
        AminerConfig.KEY_STAT_LOG_FILE,
        os.path.join(log_dir, AminerConfig.DEFAULT_STAT_LOG_FILE))
    if not stat_log_file.startswith(log_dir):
        stat_log_file = os.path.join(log_dir, stat_log_file)
    try:
        stat_file_handler = RotatingFileHandler(stat_log_file,
                                                maxBytes=max_bytes,
                                                backupCount=backup_count)
        os.chown(stat_log_file,
                 aminer_user_id,
                 aminer_grp_id,
                 dir_fd=log_dir_fd,
                 follow_symlinks=False)
    except OSError as e:
        print('Could not create or open %s: %s. Stopping..' %
              (stat_log_file, e),
              file=sys.stderr)
        sys.exit(1)
    stat_file_handler.setFormatter(
        logging.Formatter(fmt='%(asctime)s %(message)s', datefmt=datefmt))
    stat_logger.addHandler(stat_file_handler)

    debug_logger = logging.getLogger(AminerConfig.DEBUG_LOG_NAME)
    if AminerConfig.DEBUG_LEVEL == 0:
        debug_logger.setLevel(logging.ERROR)
    elif AminerConfig.DEBUG_LEVEL == 1:
        debug_logger.setLevel(logging.INFO)
    else:
        debug_logger.setLevel(logging.DEBUG)
    debug_log_file = aminer_config.config_properties.get(
        AminerConfig.KEY_DEBUG_LOG_FILE,
        os.path.join(log_dir, AminerConfig.DEFAULT_DEBUG_LOG_FILE))
    if not debug_log_file.startswith(log_dir):
        debug_log_file = os.path.join(log_dir, debug_log_file)
    try:
        debug_file_handler = RotatingFileHandler(debug_log_file,
                                                 maxBytes=max_bytes,
                                                 backupCount=backup_count)
        os.chown(debug_log_file,
                 aminer_user_id,
                 aminer_grp_id,
                 dir_fd=log_dir_fd,
                 follow_symlinks=False)
    except OSError as e:
        print('Could not create or open %s: %s. Stopping..' %
              (debug_log_file, e),
              file=sys.stderr)
        sys.exit(1)
    debug_file_handler.setFormatter(
        logging.Formatter(fmt='%(asctime)s %(levelname)s %(message)s',
                          datefmt=datefmt))
    debug_logger.addHandler(debug_file_handler)