Example #1
0
def test_log_audit_with_object_diff(request_context):
    old = {
        "a": "b",
        "b": "c",
    }
    new = {
        "b": "c",
    }

    with on_time("2018-04-15 16:50", "CET"):
        log_audit(
            object_ref=None,
            action="bla",
            message="Message",
            user_id=UserId("calvin"),
            diff_text=make_diff_text(old, new),
        )

    store = AuditLogStore(AuditLogStore.make_path())
    assert store.read() == [
        AuditLogStore.Entry(
            time=1523811000,
            object_ref=None,
            user_id="calvin",
            action="bla",
            text="Message",
            diff_text='Attribute "a" with value "b" removed.',
        ),
    ]
Example #2
0
def test__migrate_pre_2_0_audit_log(tmp_path, old_audit_log, new_path):
    uc = update_config.UpdateConfig(cmk.utils.log.logger, argparse.Namespace())

    assert not new_path.exists()
    assert old_audit_log.exists()

    uc._migrate_pre_2_0_audit_log()

    assert new_path.exists()
    assert not old_audit_log.exists()

    # Now try to parse the migrated log with the new logic
    store = AuditLogStore(new_path)
    assert store.read() == [
        AuditLogStore.Entry(
            time=1604991356,
            object_ref=None,
            user_id='cmkadmin',
            action='liveproxyd-activate',
            text='Activating changes of Livestatus Proxy configuration',
            diff_text=None),
        AuditLogStore.Entry(
            time=1604991356,
            object_ref=None,
            user_id='cmkadmin',
            action='liveproxyd-activate',
            text='Activating changes of Livestatus Proxy configuration',
            diff_text=None),
        AuditLogStore.Entry(time=1604992040,
                            object_ref=ObjectRef(ObjectRefType.Host, "heute2"),
                            user_id='cmkadmin',
                            action='create-host',
                            text='Created new host heute2.',
                            diff_text=None),
        AuditLogStore.Entry(time=1604992159,
                            object_ref=ObjectRef(ObjectRefType.Host, "heute2"),
                            user_id='cmkadmin',
                            action='delete-host',
                            text='Deleted host heute2',
                            diff_text=None),
        AuditLogStore.Entry(time=1604992163,
                            object_ref=ObjectRef(ObjectRefType.Host, "heute1"),
                            user_id='cmkadmin',
                            action='create-host',
                            text='Created new host heute1.',
                            diff_text=None),
        AuditLogStore.Entry(time=1604992166,
                            object_ref=ObjectRef(ObjectRefType.Host,
                                                 "heute12"),
                            user_id='cmkadmin',
                            action='create-host',
                            text='Created new host heute12.',
                            diff_text=None),
    ]
Example #3
0
def test_log_audit_with_html_message(request_context):
    with on_time("2018-04-15 16:50", "CET"):
        log_audit(
            object_ref=None,
            user_id=UserId("calvin"),
            action="bla",
            message=HTML("Message <b>bla</b>"),
        )

    store = AuditLogStore(AuditLogStore.make_path())
    assert store.read() == [
        AuditLogStore.Entry(
            time=1523811000,
            object_ref=None,
            user_id="calvin",
            action="bla",
            text=HTML("Message <b>bla</b>"),
            diff_text=None,
        ),
    ]
Example #4
0
def test_log_audit_with_html_message(register_builtin_html):
    with on_time('2018-04-15 16:50', 'CET'):
        log_audit(
            object_ref=None,
            user_id=UserId('calvin'),
            action="bla",
            message=HTML("Message <b>bla</b>"),
        )

    store = AuditLogStore(AuditLogStore.make_path())
    assert store.read() == [
        AuditLogStore.Entry(
            time=1523811000,
            object_ref=None,
            user_id='calvin',
            action='bla',
            text=HTML("Message <b>bla</b>"),
            diff_text=None,
        ),
    ]
Example #5
0
class ModeAuditLog(WatoMode):
    @classmethod
    def name(cls):
        return "auditlog"

    @classmethod
    def permissions(cls):
        return ["auditlog"]

    def __init__(self):
        self._options = {
            key: vs.default_value()
            for key, vs in self._audit_log_options()
        }
        super().__init__()
        self._store = AuditLogStore(AuditLogStore.make_path())
        self._show_details = request.get_integer_input_mandatory(
            "show_details", 1) == 1

    def title(self):
        return _("Audit log")

    def page_menu(self, breadcrumb: Breadcrumb) -> PageMenu:
        menu = PageMenu(
            dropdowns=[
                PageMenuDropdown(
                    name="log",
                    title=_("Audit log"),
                    topics=[
                        PageMenuTopic(
                            title=_("Actions"),
                            entries=list(self._page_menu_entries_actions()),
                        ),
                    ],
                ),
                PageMenuDropdown(
                    name="export",
                    title=_("Export"),
                    topics=[
                        PageMenuTopic(
                            title=_("Export"),
                            entries=list(self._page_menu_entries_export()),
                        ),
                    ],
                ),
                PageMenuDropdown(
                    name="related",
                    title=_("Related"),
                    topics=[
                        PageMenuTopic(
                            title=_("Setup"),
                            entries=list(self._page_menu_entries_setup()),
                        ),
                    ],
                ),
            ],
            breadcrumb=breadcrumb,
        )

        self._extend_display_dropdown(menu)
        return menu

    def _page_menu_entries_setup(self) -> Iterator[PageMenuEntry]:
        if user.may("wato.sites"):
            yield PageMenuEntry(
                title=_("View changes"),
                icon_name="activate",
                item=make_simple_link(
                    watolib.folder_preserving_link([("mode", "changelog")])),
            )

    def _page_menu_entries_actions(self) -> Iterator[PageMenuEntry]:
        if not self._log_exists():
            return

        if not user.may("wato.auditlog"):
            return

        if not user.may("wato.edit"):
            return

        if user.may("wato.clear_auditlog"):
            yield PageMenuEntry(
                title=_("Clear log"),
                icon_name="delete",
                item=make_simple_link(
                    make_confirm_link(
                        url=makeactionuri(request, transactions,
                                          [("_action", "clear")]),
                        message=_(
                            "Do you really want to clear the audit log?"),
                    )),
            )

    def _page_menu_entries_export(self) -> Iterator[PageMenuEntry]:
        if not self._log_exists():
            return

        if not user.may("wato.auditlog"):
            return

        if not user.may("wato.edit"):
            return

        if not user.may("general.csv_export"):
            return

        yield PageMenuEntry(
            title=_("Export CSV"),
            icon_name="download_csv",
            item=make_simple_link(
                makeactionuri(request, transactions, [("_action", "csv")])),
        )

    def _extend_display_dropdown(self, menu: PageMenu) -> None:
        display_dropdown = menu.get_dropdown_by_name(
            "display", make_display_options_dropdown())

        display_dropdown.topics.insert(
            0,
            PageMenuTopic(
                title=_("Filter"),
                entries=[
                    PageMenuEntry(
                        title=_("Filter view"),
                        icon_name="filters_set"
                        if html.form_submitted("options") else "filter",
                        item=PageMenuSidePopup(self._render_filter_form()),
                        name="filters",
                        is_shortcut=True,
                    ),
                ],
            ),
        )

        display_dropdown.topics.insert(
            0,
            PageMenuTopic(
                title=_("Details"),
                entries=[
                    PageMenuEntry(
                        title=_("Show details"),
                        icon_name="checked_checkbox"
                        if self._show_details else "checkbox",
                        item=make_simple_link(
                            makeactionuri(
                                request,
                                transactions,
                                [
                                    ("show_details",
                                     "0" if self._show_details else "1"),
                                ],
                            )),
                        name="show_details",
                        css_classes=["toggle"],
                    )
                ],
            ),
        )

    def _render_filter_form(self) -> HTML:
        with output_funnel.plugged():
            self._display_audit_log_options()
            return HTML(output_funnel.drain())

    def _log_exists(self):
        return self._store.exists()

    def action(self) -> ActionResult:
        if request.var("_action") == "clear":
            user.need_permission("wato.auditlog")
            user.need_permission("wato.clear_auditlog")
            user.need_permission("wato.edit")
            return self._clear_audit_log_after_confirm()

        if html.request.var("_action") == "csv":
            user.need_permission("wato.auditlog")
            return self._export_audit_log(self._parse_audit_log())

        return None

    def page(self):
        self._options.update(self._get_audit_log_options_from_request())

        audit = self._parse_audit_log()

        if not audit:
            html.show_message(_("Found no matching entry."))

        elif self._options["display"] == "daily":
            self._display_daily_audit_log(audit)

        else:
            self._display_multiple_days_audit_log(audit)

    def _get_audit_log_options_from_request(self):
        options = {}
        for name, vs in self._audit_log_options():
            if not list(request.itervars("options_" + name)):
                continue

            try:
                value = vs.from_html_vars("options_" + name)
                vs.validate_value(value, "options_" + name)
                options[name] = value
            except MKUserError as e:
                user_errors.add(e)
        return options

    def _display_daily_audit_log(self, log):
        log, times = self._get_next_daily_paged_log(log)

        self._display_page_controls(*times)

        if display_options.enabled(display_options.T):
            html.h3(_("Audit log for %s") % render.date(times[0]))

        self._display_log(log)

        self._display_page_controls(*times)

    def _display_multiple_days_audit_log(self, log):
        log = self._get_multiple_days_log_entries(log)

        if display_options.enabled(display_options.T):
            html.h3(
                _("Audit log for %s and %d days ago") % (render.date(
                    self._get_start_date()), self._options["display"][1]))

        self._display_log(log)

    def _display_log(self, log):
        with table_element(css="data wato auditlog audit",
                           limit=None,
                           sortable=False,
                           searchable=False) as table:
            for entry in log:
                table.row()
                table.cell(
                    _("Time"),
                    html.render_nobr(render.date_and_time(float(entry.time))),
                    css="narrow",
                )
                user_txt = (
                    "<i>%s</i>" %
                    _("internal")) if entry.user_id == "-" else entry.user_id
                table.cell(_("User"), user_txt, css="nobreak narrow")

                table.cell(
                    _("Object type"),
                    entry.object_ref.object_type.name
                    if entry.object_ref else "",
                    css="narrow",
                )
                table.cell(_("Object"),
                           render_object_ref(entry.object_ref) or "",
                           css="narrow")

                text = HTML(
                    escaping.escape_text(entry.text).replace("\n", "<br>\n"))
                table.cell(_("Summary"), text)

                if self._show_details:
                    diff_text = HTML(
                        escaping.escape_text(entry.diff_text).
                        replace("\n", "<br>\n") if entry.diff_text else "")
                    table.cell(_("Details"), diff_text)

    def _get_next_daily_paged_log(self, log):
        start = self._get_start_date()

        while True:
            log_today, times = self._paged_log_from(log, start)
            if len(log) == 0 or len(log_today) > 0:
                return log_today, times
            # No entries today, but log not empty -> go back in time
            start -= 24 * 3600

    def _get_start_date(self):
        if self._options["start"] == "now":
            st = time.localtime()
            return int(
                time.mktime(
                    time.struct_time((st.tm_year, st.tm_mon, st.tm_mday, 0, 0,
                                      0, 0, 0, 0))))
        return int(self._options["start"][1])

    def _get_multiple_days_log_entries(self, log):
        start_time = self._get_start_date() + 86399
        end_time = start_time - ((self._options["display"][1] * 86400) + 86399)

        logs = []

        for entry in log:
            if entry[0] <= start_time and entry[0] >= end_time:
                logs.append(entry)

        return logs

    def _paged_log_from(self, log, start):
        start_time, end_time = self._get_timerange(start)
        previous_log_time = None
        next_log_time = None
        first_log_index = None
        last_log_index = None
        for index, entry in enumerate(log):
            if entry.time >= end_time:
                # This log is too new
                continue
            if first_log_index is None and start_time <= entry.time < end_time:
                # This is a log for this day. Save the first index
                if first_log_index is None:
                    first_log_index = index

                    # When possible save the timestamp of the previous log
                    if index > 0:
                        next_log_time = int(log[index - 1][0])

            elif entry.time < start_time and last_log_index is None:
                last_log_index = index
                # This is the next log after this day
                previous_log_time = int(log[index][0])
                # Finished!
                break

        if last_log_index is None:
            last_log_index = len(log)

        return log[first_log_index:last_log_index], (
            start_time,
            end_time,
            previous_log_time,
            next_log_time,
        )

    def _display_page_controls(self, start_time, end_time, previous_log_time,
                               next_log_time):
        html.open_div(class_="paged_controls")

        def time_url_args(t):
            return [
                ("options_start_1_day", time.strftime("%d",
                                                      time.localtime(t))),
                ("options_start_1_month",
                 time.strftime("%m", time.localtime(t))),
                ("options_start_1_year", time.strftime("%Y",
                                                       time.localtime(t))),
                ("options_start_sel", "1"),
            ]

        if next_log_time is not None:
            html.icon_button(
                makeactionuri(
                    request,
                    transactions,
                    [
                        ("options_start_sel", "0"),
                    ],
                ),
                _("Most recent events"),
                "start",
            )

            html.icon_button(
                makeactionuri(request, transactions,
                              time_url_args(next_log_time)),
                "%s: %s" % (_("Newer events"), render.date(next_log_time)),
                "back",
            )
        else:
            html.empty_icon_button()
            html.empty_icon_button()

        if previous_log_time is not None:
            html.icon_button(
                makeactionuri(request, transactions,
                              time_url_args(previous_log_time)),
                "%s: %s" % (_("Older events"), render.date(previous_log_time)),
                "forth",
            )
        else:
            html.empty_icon_button()

        html.close_div()

    def _get_timerange(self, t):
        st = time.localtime(int(t))
        start = int(
            time.mktime(
                time.struct_time(
                    (st[0], st[1], st[2], 0, 0, 0, st[6], st[7], st[8]))))
        end = start + 86399
        return start, end

    def _display_audit_log_options(self):
        if display_options.disabled(display_options.C):
            return

        html.begin_form("options", method="GET")

        self._show_audit_log_options_controls()

        html.open_div(class_="side_popup_content")
        html.show_user_errors()

        for name, vs in self._audit_log_options():
            html.render_floating_option(name, "single", "options_", vs,
                                        self._options[name])

        html.close_div()

        html.hidden_fields()
        html.end_form()

    def _show_audit_log_options_controls(self):
        html.open_div(class_="side_popup_controls")

        html.open_div(class_="update_buttons")
        html.button("apply", _("Apply"), "submit")
        html.buttonlink(makeuri(request, [], remove_prefix="options_"),
                        _("Reset"))
        html.close_div()

        html.close_div()

    def _audit_log_options(self):
        object_types: Choices = [
            ("", _("All object types")),
            (None, _("No object type")),
        ] + [(t.name, t.name) for t in ObjectRefType]

        return [
            (
                "object_type",
                DropdownChoice(
                    title=_("Object type"),
                    choices=object_types,
                ),
            ),
            (
                "object_ident",
                TextInput(title=_("Object"), ),
            ),
            (
                "user_id",
                UserSelection(
                    title=_("User"),
                    only_contacts=False,
                    none=_("All users"),
                ),
            ),
            (
                "filter_regex",
                RegExp(
                    title=_("Filter pattern (RegExp)"),
                    mode="infix",
                ),
            ),
            (
                "start",
                CascadingDropdown(
                    title=_("Start log from"),
                    default_value="now",
                    orientation="horizontal",
                    choices=[
                        ("now", _("Current date")),
                        ("time", _("Specific date"), AbsoluteDate()),
                    ],
                ),
            ),
            (
                "display",
                CascadingDropdown(
                    title=_("Display mode of entries"),
                    default_value="daily",
                    orientation="horizontal",
                    choices=[
                        ("daily", _("Daily paged display")),
                        (
                            "number_of_days",
                            _("Number of days from now (single page)"),
                            Integer(
                                minvalue=1,
                                unit=_("days"),
                                default_value=1,
                            ),
                        ),
                    ],
                ),
            ),
        ]

    def _clear_audit_log_after_confirm(self) -> ActionResult:
        self._clear_audit_log()
        flash(_("Cleared audit log."))
        return redirect(self.mode_url())

    def _clear_audit_log(self):
        self._store.clear()

    def _export_audit_log(self,
                          audit: List[AuditLogStore.Entry]) -> ActionResult:
        response.set_content_type("text/csv")

        if self._options["display"] == "daily":
            filename = "wato-auditlog-%s_%s.csv" % (
                render.date(time.time()),
                render.time_of_day(time.time()),
            )
        else:
            filename = "wato-auditlog-%s_%s_days.csv" % (
                render.date(time.time()),
                self._options["display"][1],
            )

        response.headers[
            "Content-Disposition"] = 'attachment; filename="%s"' % filename

        titles = [
            _("Date"),
            _("Time"),
            _("Object type"),
            _("Object"),
            _("User"),
            _("Action"),
            _("Summary"),
        ]

        if self._show_details:
            titles.append(_("Details"))

        resp = []

        resp.append(",".join(titles) + "\n")
        for entry in audit:
            columns = [
                render.date(int(entry.time)),
                render.time_of_day(int(entry.time)),
                entry.object_ref.object_type.name if entry.object_ref else "",
                entry.object_ref.ident if entry.object_ref else "",
                entry.user_id,
                entry.action,
                '"' + escaping.strip_tags(entry.text).replace('"', "'") + '"',
            ]

            if self._show_details:
                columns.append(
                    '"' +
                    escaping.strip_tags(entry.diff_text).replace('"', "'") +
                    '"')

            resp.append(",".join(columns) + "\n")

        response.set_data("".join(resp))

        return FinalizeRequest(code=200)

    def _parse_audit_log(self) -> List[AuditLogStore.Entry]:
        return list(
            reversed([e for e in self._store.read() if self._filter_entry(e)]))

    def _filter_entry(self, entry: AuditLogStore.Entry) -> bool:
        if self._options["object_type"] != "":
            if entry.object_ref is None and self._options[
                    "object_type"] is not None:
                return False
            if (entry.object_ref and entry.object_ref.object_type.name !=
                    self._options["object_type"]):
                return False

        if self._options["object_ident"] != "":
            if entry.object_ref is None and self._options[
                    "object_ident"] is not None:
                return False
            if entry.object_ref and entry.object_ref.ident != self._options[
                    "object_ident"]:
                return False

        if self._options["user_id"] is not None:
            if entry.user_id != self._options["user_id"]:
                return False

        filter_regex: str = self._options["filter_regex"]
        if filter_regex:
            return any(
                re.search(filter_regex, val)
                for val in [entry.user_id, entry.action,
                            str(entry.text)])

        return True
Example #6
0
def test__migrate_pre_2_0_audit_log(
    uc: update_config.UpdateConfig,
    old_audit_log: Path,
    new_path: Path,
) -> None:
    assert not new_path.exists()
    assert old_audit_log.exists()

    uc._migrate_pre_2_0_audit_log()

    assert new_path.exists()
    assert not old_audit_log.exists()

    # Now try to parse the migrated log with the new logic
    log_store = AuditLogStore(new_path)
    assert log_store.read() == [
        AuditLogStore.Entry(
            time=1604991356,
            object_ref=None,
            user_id="cmkadmin",
            action="liveproxyd-activate",
            text="Activating changes of Livestatus Proxy configuration",
            diff_text=None,
        ),
        AuditLogStore.Entry(
            time=1604991356,
            object_ref=None,
            user_id="cmkadmin",
            action="liveproxyd-activate",
            text="Activating changes of Livestatus Proxy configuration",
            diff_text=None,
        ),
        AuditLogStore.Entry(
            time=1604992040,
            object_ref=ObjectRef(ObjectRefType.Host, "heute2"),
            user_id="cmkadmin",
            action="create-host",
            text="Created new host heute2.",
            diff_text=None,
        ),
        AuditLogStore.Entry(
            time=1604992159,
            object_ref=ObjectRef(ObjectRefType.Host, "heute2"),
            user_id="cmkadmin",
            action="delete-host",
            text="Deleted host heute2",
            diff_text=None,
        ),
        AuditLogStore.Entry(
            time=1604992163,
            object_ref=ObjectRef(ObjectRefType.Host, "heute1"),
            user_id="cmkadmin",
            action="create-host",
            text="Created new host heute1.",
            diff_text=None,
        ),
        AuditLogStore.Entry(
            time=1604992166,
            object_ref=ObjectRef(ObjectRefType.Host, "heute12"),
            user_id="cmkadmin",
            action="create-host",
            text="Created new host heute12.",
            diff_text=None,
        ),
    ]
Example #7
0
class ModeAuditLog(WatoMode):
    @classmethod
    def name(cls):
        return "auditlog"

    @classmethod
    def permissions(cls):
        return ["auditlog"]

    def __init__(self):
        self._options = self._vs_audit_log_options().default_value()
        super(ModeAuditLog, self).__init__()
        self._store = AuditLogStore(AuditLogStore.make_path())

    def title(self):
        return _("Audit log")

    def page_menu(self, breadcrumb: Breadcrumb) -> PageMenu:
        menu = PageMenu(
            dropdowns=[
                PageMenuDropdown(
                    name="log",
                    title=_("Actions"),
                    topics=[
                        PageMenuTopic(
                            title=_("Actions"),
                            entries=list(self._page_menu_entries_actions()),
                        ),
                    ],
                ),
                PageMenuDropdown(
                    name="export",
                    title=_("Export"),
                    topics=[
                        PageMenuTopic(
                            title=_("Export"),
                            entries=list(self._page_menu_entries_export()),
                        ),
                    ],
                ),
                PageMenuDropdown(
                    name="related",
                    title=_("Related"),
                    topics=[
                        PageMenuTopic(
                            title=_("Setup"),
                            entries=list(self._page_menu_entries_setup()),
                        ),
                    ],
                ),
            ],
            breadcrumb=breadcrumb,
        )

        self._extend_display_dropdown(menu)
        return menu

    def _page_menu_entries_setup(self) -> Iterator[PageMenuEntry]:
        if config.user.may("wato.sites"):
            yield PageMenuEntry(
                title=_("View changes"),
                icon_name="activate",
                item=make_simple_link(
                    watolib.folder_preserving_link([("mode", "changelog")])),
            )

    def _page_menu_entries_actions(self) -> Iterator[PageMenuEntry]:
        if not self._log_exists():
            return

        if not config.user.may("wato.auditlog"):
            return

        if not config.user.may("wato.edit"):
            return

        if config.user.may("wato.clear_auditlog"):
            yield PageMenuEntry(
                title=_("Clear log"),
                icon_name="trash",
                item=make_simple_link(
                    make_confirm_link(
                        url=html.makeactionuri([("_action", "clear")]),
                        message=_(
                            "Do you really want to clear the audit log?"),
                    )),
            )

    def _page_menu_entries_export(self) -> Iterator[PageMenuEntry]:
        if not self._log_exists():
            return

        if not config.user.may("wato.auditlog"):
            return

        if not config.user.may("wato.edit"):
            return

        if not config.user.may("general.csv_export"):
            return

        yield PageMenuEntry(
            title=_("Export CSV"),
            icon_name="download_csv",
            item=make_simple_link(html.makeactionuri([("_action", "csv")])),
        )

    def _extend_display_dropdown(self, menu: PageMenu) -> None:
        display_dropdown = menu.get_dropdown_by_name(
            "display", make_display_options_dropdown())

        display_dropdown.topics.insert(
            0,
            PageMenuTopic(
                title=_("Filter"),
                entries=[
                    PageMenuEntry(
                        title=_("Filter view"),
                        icon_name="filters_set"
                        if html.form_submitted("options") else "filters",
                        item=PageMenuPopup(self._render_filter_form()),
                        name="filters",
                        is_shortcut=True,
                    ),
                ],
            ))

    def _render_filter_form(self) -> str:
        with html.plugged():
            self._display_audit_log_options()
            return html.drain()

    def _log_exists(self):
        return self._store.exists()

    def action(self) -> ActionResult:
        if html.request.var("_action") == "clear":
            config.user.need_permission("wato.auditlog")
            config.user.need_permission("wato.clear_auditlog")
            config.user.need_permission("wato.edit")
            return self._clear_audit_log_after_confirm()

        vs_options = self._vs_audit_log_options()
        value = vs_options.from_html_vars("options")
        vs_options.validate_value(value, "options")
        self._options = value

        if html.request.var("_action") == "csv":
            config.user.need_permission("wato.auditlog")
            return self._export_audit_log()

        return None

    def page(self):
        audit = self._parse_audit_log()

        if not audit:
            html.show_message(_("The audit log is empty."))

        elif self._options["display"] == "daily":
            self._display_daily_audit_log(audit)

        else:
            self._display_multiple_days_audit_log(audit)

    def _display_daily_audit_log(self, log):
        log, times = self._get_next_daily_paged_log(log)

        self._display_page_controls(*times)

        if display_options.enabled(display_options.T):
            html.h3(_("Audit log for %s") % render.date(times[0]))

        self._display_log(log)

        self._display_page_controls(*times)

    def _display_multiple_days_audit_log(self, log):
        log = self._get_multiple_days_log_entries(log)

        if display_options.enabled(display_options.T):
            html.h3(
                _("Audit log for %s and %d days ago") % (render.date(
                    self._get_start_date()), self._options["display"][1]))

        self._display_log(log)

    def _display_log(self, log):
        with table_element(css="data wato auditlog audit",
                           limit=None,
                           sortable=False,
                           searchable=False) as table:
            for entry in log:
                table.row()
                table.cell(_("Object"),
                           self._render_logfile_linkinfo(entry.linkinfo))
                table.cell(
                    _("Time"),
                    html.render_nobr(render.date_and_time(float(entry.time))))
                user = (
                    '<i>%s</i>' %
                    _('internal')) if entry.user_id == '-' else entry.user_id
                table.cell(_("User"), html.render_text(user), css="nobreak")

                # This must not be attrencoded: The entries are encoded when writing to the log.
                table.cell(_("Change"),
                           entry.text.replace("\\n", "<br>\n"),
                           css="fill")

    def _get_next_daily_paged_log(self, log):
        start = self._get_start_date()

        while True:
            log_today, times = self._paged_log_from(log, start)
            if len(log) == 0 or len(log_today) > 0:
                return log_today, times
            # No entries today, but log not empty -> go back in time
            start -= 24 * 3600

    def _get_start_date(self):
        if self._options["start"] == "now":
            st = time.localtime()
            return int(
                time.mktime(
                    time.struct_time((st.tm_year, st.tm_mon, st.tm_mday, 0, 0,
                                      0, 0, 0, 0))))
        return int(self._options["start"][1])

    def _get_multiple_days_log_entries(self, log):
        start_time = self._get_start_date() + 86399
        end_time   = start_time \
                     - ((self._options["display"][1] * 86400) + 86399)

        logs = []

        for entry in log:
            if entry[0] <= start_time and entry[0] >= end_time:
                logs.append(entry)

        return logs

    def _paged_log_from(self, log, start):
        start_time, end_time = self._get_timerange(start)
        previous_log_time = None
        next_log_time = None
        first_log_index = None
        last_log_index = None
        for index, entry in enumerate(log):
            if entry.time >= end_time:
                # This log is too new
                continue
            if first_log_index is None and start_time <= entry.time < end_time:
                # This is a log for this day. Save the first index
                if first_log_index is None:
                    first_log_index = index

                    # When possible save the timestamp of the previous log
                    if index > 0:
                        next_log_time = int(log[index - 1][0])

            elif entry.time < start_time and last_log_index is None:
                last_log_index = index
                # This is the next log after this day
                previous_log_time = int(log[index][0])
                # Finished!
                break

        if last_log_index is None:
            last_log_index = len(log)

        return log[first_log_index:last_log_index], (start_time, end_time,
                                                     previous_log_time,
                                                     next_log_time)

    def _display_page_controls(self, start_time, end_time, previous_log_time,
                               next_log_time):
        html.open_div(class_="paged_controls")

        def time_url_args(t):
            return [
                ("options_p_start_1_day",
                 time.strftime("%d", time.localtime(t))),
                ("options_p_start_1_month",
                 time.strftime("%m", time.localtime(t))),
                ("options_p_start_1_year",
                 time.strftime("%Y", time.localtime(t))),
                ("options_p_start_sel", "1"),
            ]

        if next_log_time is not None:
            html.icon_button(
                html.makeactionuri([
                    ("options_p_start_sel", "0"),
                ]), _("Most recent events"), "start")

            html.icon_button(
                html.makeactionuri(time_url_args(next_log_time)),
                "%s: %s" % (_("Newer events"), render.date(next_log_time)),
                "back")
        else:
            html.empty_icon_button()
            html.empty_icon_button()

        if previous_log_time is not None:
            html.icon_button(
                html.makeactionuri(time_url_args(previous_log_time)),
                "%s: %s" % (_("Older events"), render.date(previous_log_time)),
                "forth")
        else:
            html.empty_icon_button()

        html.close_div()

    def _get_timerange(self, t):
        st = time.localtime(int(t))
        start = int(
            time.mktime(
                time.struct_time(
                    (st[0], st[1], st[2], 0, 0, 0, st[6], st[7], st[8]))))
        end = start + 86399
        return start, end

    def _display_audit_log_options(self):
        if display_options.disabled(display_options.C):
            return

        valuespec = self._vs_audit_log_options()

        html.begin_form("options", method="GET")
        valuespec.render_input_as_form("options", {})

        html.button("options", _("Apply"))
        html.hidden_fields()
        html.end_form()

    def _vs_audit_log_options(self):
        return Dictionary(
            title=_("Options"),
            elements=[
                ("filter_regex",
                 RegExp(
                     title=_("Filter pattern (RegExp)"),
                     mode="infix",
                 )),
                ("start",
                 CascadingDropdown(
                     title=_("Start log from"),
                     default_value="now",
                     orientation="horizontal",
                     choices=[
                         ("now", _("Current date")),
                         ("time", _("Specific date"), AbsoluteDate()),
                     ],
                 )),
                ("display",
                 CascadingDropdown(
                     title=_("Display mode of entries"),
                     default_value="daily",
                     orientation="horizontal",
                     choices=[
                         ("daily", _("Daily paged display")),
                         ("number_of_days",
                          _("Number of days from now (single page)"),
                          Integer(
                              minvalue=1,
                              unit=_("days"),
                              default_value=1,
                          )),
                     ],
                 )),
            ],
            optional_keys=[],
        )

    def _clear_audit_log_after_confirm(self) -> ActionResult:
        self._clear_audit_log()
        flash(_("Cleared audit log."))
        return redirect(self.mode_url())

    def _clear_audit_log(self):
        self._store.clear()

    def _render_logfile_linkinfo(self, linkinfo):
        if ':' in linkinfo:  # folder:host
            path, host_name = linkinfo.split(':', 1)
            if watolib.Folder.folder_exists(path):
                folder = watolib.Folder.folder(path)
                if host_name:
                    if folder.has_host(host_name):
                        host = folder.host(host_name)
                        url = host.edit_url()
                        title = host_name
                    else:
                        return host_name
                else:  # only folder
                    url = folder.url()
                    title = folder.title()
            else:
                return linkinfo
        else:
            return ""

        return html.render_a(title, href=url)

    def _export_audit_log(self) -> ActionResult:
        html.set_output_format("csv")

        if self._options["display"] == "daily":
            filename = "wato-auditlog-%s_%s.csv" % (render.date(
                time.time()), render.time_of_day(time.time()))
        else:
            filename = "wato-auditlog-%s_%s_days.csv" % (render.date(
                time.time()), self._options["display"][1])
        html.write(filename)

        html.response.headers[
            "Content-Disposition"] = "attachment; filename=\"%s\"" % filename

        titles = (
            _('Date'),
            _('Time'),
            _('Linkinfo'),
            _('User'),
            _('Action'),
            _('Text'),
        )
        html.write(','.join(titles) + '\n')
        for t, linkinfo, user, action, text in self._parse_audit_log():
            if linkinfo == '-':
                linkinfo = ''

            html.write_text(','.join((render.date(int(t)),
                                      render.time_of_day(int(t)), linkinfo,
                                      user, action, '"' + text + '"')) + '\n')
        return FinalizeRequest(code=200)

    def _parse_audit_log(self) -> List[AuditLogStore.Entry]:
        return list(
            reversed([
                e for e in self._store.read()
                if not self._filter_entry(e[2], e[3], e[4])
            ]))

    def _filter_entry(self, user, action, text):
        if not self._options["filter_regex"]:
            return False

        for val in [user, action, text]:
            if re.search(self._options["filter_regex"], val):
                return False

        return True