Example #1
0
    def __init__(
        self,
        socket,  # type: _SocketUnion
        reset_handler=None,  # type: Optional[Callable[[], None]]
        reconnect_handler=None,  # type: Optional[Callable[[], _SocketUnion]]  # noqa: E501
        test_cases=None,  # type: Optional[List[Union[AutomotiveTestCaseABC, Type[AutomotiveTestCaseABC]]]]  # noqa: E501
        **kwargs  # type: Optional[Dict[str, Any]]
    ):  # type: (...) -> None
        # The TesterPresentSender can interfere with a test_case, since a
        # target may only allow one request at a time.
        # The SingleConversationSocket prevents interleaving requests.
        if not isinstance(socket, SingleConversationSocket):
            self.socket = SingleConversationSocket(socket)
        else:
            self.socket = socket

        self.target_state = self.__initial_ecu_state
        self.reset_handler = reset_handler
        self.reconnect_handler = reconnect_handler

        self.cleanup_functions = list()  # type: List[_CleanupCallable]

        self.configuration = AutomotiveTestCaseExecutorConfiguration(
            test_cases or self.default_test_case_clss, **kwargs)

        self.configuration.state_graph.add_edge(
            (self.__initial_ecu_state, self.__initial_ecu_state))
Example #2
0
    def reconnect(self):
        # type: () -> None
        if self.reconnect_handler:
            try:
                self.socket.close()
            except Exception as e:
                log_interactive.debug("[i] Exception '%s' during socket.close",
                                      e)

            log_interactive.info("[i] Target reconnect")
            socket = self.reconnect_handler()
            if not isinstance(socket, SingleConversationSocket):
                self.socket = SingleConversationSocket(socket)
            else:
                self.socket = socket
Example #3
0
    def __init__(self, socket, reset_handler=None, enumerators=None, **kwargs):
        # The TesterPresentSender can interfere with a enumerator, since a
        # target may only allow one request at a time.
        # The SingleConversationSocket prevents interleaving requests.
        if not isinstance(socket, SingleConversationSocket):
            self.socket = SingleConversationSocket(socket)
        else:
            self.socket = socket
        self.tps = None  # TesterPresentSender
        self.target_state = ECU_State()
        self.reset_handler = reset_handler
        self.verbose = kwargs.get("verbose", False)
        if enumerators:
            # enumerators can be a mix of classes or instances
            self.enumerators = [
                e(self.socket)
                for e in enumerators if not isinstance(e, Enumerator)
            ] + [e for e in enumerators if isinstance(e, Enumerator)
                 ]  # noqa: E501
        else:
            self.enumerators = [
                e(self.socket) for e in self.default_enumerator_clss
            ]  # noqa: E501
        self.enumerator_classes = [e.__class__ for e in self.enumerators]
        self.state_graph = Graph()
        self.state_graph.add_edge(ECU_State(), ECU_State())
        self.configuration = \
            {"dynamic_timeout": kwargs.pop("dynamic_timeout", False),
             "enumerator_classes": self.enumerator_classes,
             "verbose": self.verbose,
             "state_graph": self.state_graph,
             "delay_state_change": kwargs.pop("delay_state_change", 0.5)}

        for e in self.enumerators:
            self.configuration[e.__class__] = kwargs.pop(
                e.__class__.__name__ + "_kwargs", dict())

        for conf_key in self.enumerators:
            conf_val = self.configuration[conf_key.__class__]
            for kwargs_key, kwargs_val in kwargs.items():
                if kwargs_key not in conf_val.keys():
                    conf_val[kwargs_key] = kwargs_val
            self.configuration[conf_key.__class__] = conf_val

        log_interactive.debug("The following configuration was created")
        log_interactive.debug(self.configuration)
Example #4
0
class AutomotiveTestCaseExecutor:
    """
    Base class for different automotive scanners. This class handles
    the connection to a scan target, ensures the execution of all it's
    test cases, and stores the system state machine
    """
    @property
    def __initial_ecu_state(self):
        # type: () -> EcuState
        return EcuState(session=1)

    def __init__(
        self,
        socket,  # type: _SocketUnion
        reset_handler=None,  # type: Optional[Callable[[], None]]
        reconnect_handler=None,  # type: Optional[Callable[[], _SocketUnion]]  # noqa: E501
        test_cases=None,  # type: Optional[List[Union[AutomotiveTestCaseABC, Type[AutomotiveTestCaseABC]]]]  # noqa: E501
        **kwargs  # type: Optional[Dict[str, Any]]
    ):  # type: (...) -> None
        # The TesterPresentSender can interfere with a test_case, since a
        # target may only allow one request at a time.
        # The SingleConversationSocket prevents interleaving requests.
        if not isinstance(socket, SingleConversationSocket):
            self.socket = SingleConversationSocket(socket)
        else:
            self.socket = socket

        self.target_state = self.__initial_ecu_state
        self.reset_handler = reset_handler
        self.reconnect_handler = reconnect_handler

        self.cleanup_functions = list()  # type: List[_CleanupCallable]

        self.configuration = AutomotiveTestCaseExecutorConfiguration(
            test_cases or self.default_test_case_clss, **kwargs)

        self.configuration.state_graph.add_edge(
            (self.__initial_ecu_state, self.__initial_ecu_state))

    def __reduce__(self):  # type: ignore
        f, t, d = super(AutomotiveTestCaseExecutor,
                        self).__reduce__()  # type: ignore  # noqa: E501
        try:
            del d["socket"]
        except KeyError:
            pass
        try:
            del d["reset_handler"]
        except KeyError:
            pass
        try:
            del d["reconnect_handler"]
        except KeyError:
            pass
        return f, t, d

    @property
    @abc.abstractmethod
    def default_test_case_clss(self):
        # type: () -> List[Type[AutomotiveTestCaseABC]]
        raise NotImplementedError()

    @property
    def state_graph(self):
        # type: () -> Graph
        return self.configuration.state_graph

    @property
    def state_paths(self):
        # type: () -> List[List[EcuState]]
        """
        Returns all state paths. A path is represented by a list of EcuState
        objects.
        :return: A list of paths.
        """
        paths = [
            Graph.dijkstra(self.state_graph, self.__initial_ecu_state, s)
            for s in self.state_graph.nodes if s != self.__initial_ecu_state
        ]
        return sorted([p for p in paths if p is not None] +
                      [[self.__initial_ecu_state]],
                      key=lambda x: x[-1])

    @property
    def final_states(self):
        # type: () -> List[EcuState]
        """
        Returns a list with all final states. A final state is the last
        state of a path.
        :return:
        """
        return [p[-1] for p in self.state_paths]

    @property
    def scan_completed(self):
        # type: () -> bool
        return all(
            t.has_completed(s) for t, s in product(
                self.configuration.test_cases, self.final_states))

    def reset_target(self):
        # type: () -> None
        log_interactive.info("[i] Target reset")
        if self.reset_handler:
            self.reset_handler()
        self.target_state = self.__initial_ecu_state

    def reconnect(self):
        # type: () -> None
        if self.reconnect_handler:
            try:
                self.socket.close()
            except Exception as e:
                log_interactive.debug("[i] Exception '%s' during socket.close",
                                      e)

            log_interactive.info("[i] Target reconnect")
            socket = self.reconnect_handler()
            if not isinstance(socket, SingleConversationSocket):
                self.socket = SingleConversationSocket(socket)
            else:
                self.socket = socket

        if self.socket.closed:
            raise Scapy_Exception(
                "Socket closed even after reconnect. Stop scan!")

    def execute_test_case(self, test_case):
        # type: (AutomotiveTestCaseABC) -> None
        """
        This function ensures the correct execution of a testcase, including
        the pre_execute, execute and post_execute.
        Finally the testcase is asked if a new edge or a new testcase was
        generated.
        :param test_case: A test case to be executed
        :return: None
        """
        test_case.pre_execute(self.socket, self.target_state,
                              self.configuration)

        try:
            test_case_kwargs = self.configuration[test_case.__class__.__name__]
        except KeyError:
            test_case_kwargs = dict()

        log_interactive.debug("[i] Execute test_case %s with args %s",
                              test_case.__class__.__name__, test_case_kwargs)

        test_case.execute(self.socket, self.target_state, **test_case_kwargs)
        test_case.post_execute(self.socket, self.target_state,
                               self.configuration)

        if isinstance(test_case, StateGenerator):
            edge = test_case.get_new_edge(self.socket, self.configuration)
            if edge:
                log_interactive.debug("Edge found %s", edge)
                tf = test_case.get_transition_function(self.socket, edge)
                self.state_graph.add_edge(edge, tf)

        if isinstance(test_case, TestCaseGenerator):
            new_test_case = test_case.get_generated_test_case()
            if new_test_case:
                log_interactive.debug("Testcase generated %s", new_test_case)
                self.configuration.add_test_case(new_test_case)

    def scan(self, timeout=None):
        # type: (Optional[int]) -> None
        """
        Executes all testcases for a given time.
        :param timeout: Time for execution.
        :return: None
        """
        kill_time = time.time() + (timeout or 0xffffffff)
        while kill_time > time.time():
            test_case_executed = False
            log_interactive.debug("[i] Scan paths %s", self.state_paths)
            for p, test_case in product(self.state_paths,
                                        self.configuration.test_cases):
                log_interactive.info("[i] Scan path %s", p)
                terminate = kill_time < time.time()
                if terminate:
                    log_interactive.debug(
                        "[-] Execution time exceeded. Terminating scan!")
                    break

                final_state = p[-1]
                if test_case.has_completed(final_state):
                    log_interactive.debug("[+] State %s for %s completed",
                                          repr(final_state), test_case)
                    continue

                try:
                    if not self.enter_state_path(p):
                        log_interactive.error("[-] Error entering path %s", p)
                        continue
                    log_interactive.info("[i] Execute %s for path %s",
                                         str(test_case), p)
                    self.execute_test_case(test_case)
                    test_case_executed = True
                except (OSError, ValueError, Scapy_Exception) as e:
                    log_interactive.critical("[-] Exception: %s", e)
                    if self.configuration.debug:
                        raise e
                    if cast(SuperSocket, self.socket).closed and \
                            self.reconnect_handler is None:
                        log_interactive.critical(
                            "Socket went down. Need to leave scan")
                        raise e
                finally:
                    self.cleanup_state()

            if not test_case_executed:
                log_interactive.info(
                    "[i] Execute failure or scan completed. Exit scan!")
                break

        self.cleanup_state()
        self.reset_target()

    def enter_state_path(self, path):
        # type: (List[EcuState]) -> bool
        """
        Resets and reconnects to a target and applies all transition functions
        to traversal a given path.
        :param path: Path to be applied to the scan target.
        :return: True, if all transition functions could be executed.
        """
        if path[0] != self.__initial_ecu_state:
            raise Scapy_Exception(
                "Initial state of path not equal reset state of the target")

        self.reset_target()
        self.reconnect()

        if len(path) == 1:
            return True

        for next_state in path[1:]:
            edge = (self.target_state, next_state)
            if not self.enter_state(*edge):
                self.state_graph.downrate_edge(edge)
                self.cleanup_state()
                return False
        return True

    def enter_state(self, prev_state, next_state):
        # type: (EcuState, EcuState) -> bool
        """
        Obtains a transition function from the system state graph and executes
        it. On success, the cleanup function is added for a later cleanup of
        the new state.
        :param prev_state: Current state
        :param next_state: Desired state
        :return: True, if state could be changed successful
        """
        edge = (prev_state, next_state)
        funcs = self.state_graph.get_transition_tuple_for_edge(edge)

        if funcs is None:
            log_interactive.error("[!] No transition function for %s", edge)
            return False

        trans_func, trans_kwargs, clean_func = funcs
        state_changed = trans_func(self.socket, self.configuration,
                                   trans_kwargs)
        if state_changed:
            self.target_state = next_state

            if clean_func is not None:
                self.cleanup_functions += [clean_func]
            return True
        else:
            log_interactive.info("[-] Transition for edge %s failed", edge)
            return False

    def cleanup_state(self):
        # type: () -> None
        """
        Executes all collected cleanup functions from a traversed path
        :return: None
        """
        for f in self.cleanup_functions:
            if not callable(f):
                continue
            try:
                if not f(self.socket, self.configuration):
                    log_interactive.info("[-] Cleanup function %s failed",
                                         repr(f))
            except (OSError, ValueError, Scapy_Exception) as e:
                log_interactive.critical("[!] Exception during cleanup: %s", e)

        self.cleanup_functions = list()

    def show_testcases(self):
        # type: () -> None
        for t in self.configuration.test_cases:
            t.show()

    def show_testcases_status(self):
        # type: () -> None
        data = list()
        for t in self.configuration.test_cases:
            for s in self.state_graph.nodes:
                data += [(repr(s), t.__class__.__name__, t.has_completed(s))]
        make_lined_table(data, lambda tup: (tup[0], tup[1], tup[2]))

    @property
    def supported_responses(self):
        # type: () -> List[EcuResponse]
        """
        Returns a sorted list of supported responses, gathered from all
        enumerators. The sort is done in a way
        to provide the best possible results, if this list of supported
        responses is used to simulate an real world Ecu with the
        EcuAnsweringMachine object.
        :return: A sorted list of EcuResponse objects
        """
        supported_responses = list()
        for tc in self.configuration.test_cases:
            supported_responses += tc.supported_responses

        supported_responses.sort(key=Ecu.sort_key_func)
        return supported_responses