ntopng/http_src/vue/page-alert-stats.vue

735 lines
29 KiB
Vue

<!-- (C) 2022 - ntop.org -->
<template>
<Navbar id="navbar" :main_title="context.navbar.main_title" :base_url="context.navbar.base_url"
:help_link="context.navbar.help_link" :items_table="context.navbar.items_table" @click_item="click_navbar_item">
</Navbar>
<div class='row'>
<div class='col-12'>
<div class="mb-2">
<div class="w-100">
<div clas="range-container d-flex flex-wrap">
<div class="range-picker d-flex m-auto flex-wrap">
<AlertInfo id="alert_info" :global="true" ref="alert_info"></AlertInfo>
<ModalTrafficExtraction id="modal_traffic_extraction" ref="modal_traffic_extraction">
</ModalTrafficExtraction>
<ModalSnapshot ref="modal_snapshot" :csrf="context.csrf">
</ModalSnapshot>
<RangePicker v-if="mount_range_picker" ref="range_picker" id="range_picker">
<template v-slot:begin>
<div v-if="query_presets.length > 0" class="ms-1 me-2">
<select class="me-2 form-select" v-model="selected_query_preset"
@change="update_select_query_presets()">
<template v-for="item in query_presets">
<option v-if="item.builtin == true" :value="item">{{ item.name }}
</option>
</template>
<optgroup v-if="page != 'analysis'" :label="_i18n('queries.queries')">
<template v-for="item in query_presets">
<option v-if="!item.builtin" :value="item">{{ item.name }}</option>
</template>
</optgroup>
</select>
</div>
</template>
<template v-slot:extra_range_buttons>
<button v-if="context.show_permalink" class="btn btn-link btn-sm"
@click="get_permanent_link" :title="_i18n('graphs.get_permanent_link')"
ref="permanent_link_button"><i class="fas fa-lg fa-link"></i></button>
<a v-if="context.show_download" class="btn btn-link btn-sm" id="dt-btn-download"
:title="_i18n('graphs.download_records')" :href="href_download_records"><i
class="fas fa-lg fa-file"></i></a>
<button v-if="context.show_pcap_download" class="btn btn-link btn-sm"
@click="show_modal_traffic_extraction"
:title="_i18n('traffic_recording.pcap_download')"><i
class="fas fa-lg fa-download"></i></button>
<button v-if="context.is_ntop_enterprise_m" class="btn btn-link btn-sm"
@click="show_modal_snapshot" :title="_i18n('datatable.manage_snapshots')"><i
class="fas fa-lg fa-camera-retro"></i></button>
</template>
</RangePicker>
</div>
</div>
</div>
</div>
</div>
<div class='col-12'>
<div class="card card-shadow">
<div class="card-body">
<div v-if="context.show_chart" class="row">
<div class="col-12 mb-2" id="chart-vue">
<div class="card h-100 overflow-hidden">
<Chart ref="chart" id="chart_alert_stats" :chart_type="chart_type"
:base_url_request="chart_data_url" :register_on_status_change="false">
</Chart>
</div>
</div>
<div></div>
<TableWithConfig ref="table_alerts" :table_config_id="table_config_id" :table_id="table_id"
:csrf="context.csrf" :f_map_columns="map_table_def_columns"
:get_extra_params_obj="get_extra_params_obj" :display_message="display_message"
:message_to_display="message_to_display" @loaded="on_table_loaded"
@custom_event="on_table_custom_event" @rows_loaded="rows_loaded">
<template v-slot:custom_header>
<Dropdown v-for="(t, t_index) in top_table_array"
:f_on_open="get_open_top_table_dropdown(t, t_index)"
:ref="el => { top_table_dropdown_array[t_index] = el }"> <!-- Dropdown columns -->
<template v-slot:title>
<Spinner :show="t.show_spinner" size="1rem" class="me-1"></Spinner>
<a class="ntopng-truncate" :title="t.title">{{ t.label }}</a>
</template>
<template v-slot:menu>
<a v-for="opt in t.options" style="cursor:pointer; display: block;"
@click="add_top_table_filter(opt, $event)"
class="ntopng-truncate tag-filter " :title="opt.value">{{ opt.label + " (" +
opt.count + "%)" }}</a>
</template>
</Dropdown> <!-- Dropdown columns -->
</template> <!-- custom_header -->
</TableWithConfig>
</div>
</div> <!-- card body -->
<div v-show="page != 'all'" class="card-footer">
<button v-if="context.show_acknowledge_all" @click="show_modal_acknowledge_alerts"
class="btn btn-primary me-1">
<i class="fas fa fa-user-check"></i> {{ _i18n("acknowledge_alerts") }}
</button>
<button v-if="context.show_delete_all" @click="show_modal_delete_alerts" class="btn btn-danger">
<i class="fas fa fa-trash"></i> {{ _i18n("delete_alerts") }}
</button>
</div> <!-- card footer -->
</div> <!-- card-shadow -->
</div> <!-- div col -->
<NoteList :note_list="note_list"></NoteList>
</div> <!-- div row -->
<ModalAcknowledgeAlert ref="modal_acknowledge" :context="context" :page="page"
@acknowledge="refresh_page_components">
</ModalAcknowledgeAlert>
<ModalDeleteAlert ref="modal_delete" :context="context" :page="page" @delete_alert="refresh_page_components">
</ModalDeleteAlert>
<ModalAcknowledgeAlerts ref="modal_acknowledge_alerts" :context="context" :page="page"
@acknowledge_alerts="refresh_page_components">
</ModalAcknowledgeAlerts>
<ModalDeleteAlerts ref="modal_delete_alerts" :context="context" :page="page"
@delete_alerts="refresh_page_components">
</ModalDeleteAlerts>
<ModalAlertsFilter :alert="current_alert" :page="page" @exclude="add_exclude" ref="modal_alerts_filter">
</ModalAlertsFilter>
</template>
<script setup>
import { ref, onMounted, onBeforeMount, computed, nextTick } from "vue";
import { ntopng_status_manager, ntopng_custom_events, ntopng_url_manager, ntopng_utility, ntopng_sync } from "../services/context/ntopng_globals_services";
import { ntopChartApex } from "../components/ntopChartApex.js";
import { DataTableRenders } from "../utilities/datatable/sprymedia-datatable-utils.js";
import filtersManager from "../utilities/filters-manager.js";
import formatterUtils from "../utilities/formatter-utils";
import { default as Navbar } from "./page-navbar.vue";
import { default as AlertInfo } from "./alert-info.vue";
import { default as Chart } from "./chart.vue";
import { default as RangePicker } from "./range-picker.vue";
import { default as TableWithConfig } from "./table-with-config.vue";
import { default as Dropdown } from "./dropdown.vue";
import { default as Spinner } from "./spinner.vue";
import { default as NoteList } from "./note-list.vue";
import { default as ModalTrafficExtraction } from "./modal-traffic-extraction.vue";
import { default as ModalSnapshot } from "./modal-snapshot.vue";
import { default as ModalAlertsFilter } from "./modal-alerts-filter.vue";
import { default as ModalAcknowledgeAlert } from "./modal-acknowledge-alert.vue";
import { default as ModalDeleteAlert } from "./modal-delete-alert.vue";
import { default as ModalAcknowledgeAlerts } from "./modal-acknowledge-alerts.vue";
import { default as ModalDeleteAlerts } from "./modal-delete-alerts.vue";
const _i18n = (t) => i18n(t);
const props = defineProps({
context: Object,
});
const alert_info = ref(null);
const chart = ref(null);
const table_alerts = ref(null);
const modal_traffic_extraction = ref(null);
const modal_snapshot = ref(null);
const range_picker = ref(null);
const permanent_link_button = ref(null);
const modal_alerts_filter = ref(null);
const modal_acknowledge = ref(null);
const modal_delete = ref(null);
const modal_acknowledge_alerts = ref(null);
const modal_delete_alerts = ref(null);
const count_page_components_reloaded = ref(0);
const display_message = ref(false);
const message_to_display = ref('');
const current_alert = ref(null);
const default_ifid = props.context.ifid;
let page;
const table_config_id = ref("");
const table_id = ref("");
let chart_data_url = `${http_prefix}/lua/pro/rest/v2/get/db/ts.lua`;
const chart_type = ntopChartApex.typeChart.TS_COLUMN;
const top_table_array = ref([]);
const top_table_dropdown_array = ref([]);
const note_list = ref([_i18n('show_alerts.alerts_info')]);
const selected_query_preset = ref({});
const query_presets = ref([]);
const mount_range_picker = ref(false);
const href_download_records = computed(() => {
if (!props.context.show_chart || table_alerts.value == null) {
return ``;
}
// add impossible if on ref variable to reload this expression every time count_page_components_reloaded.value change
if (count_page_components_reloaded.value < 0) { throw "never run"; }
const download_endpoint = props.context.download.endpoint.replace('PAGE', page);
let params = ntopng_url_manager.get_url_object();
let columns = table_alerts.value.get_columns_defs();
let visible_columns = columns.filter((c) => c.visible).map((c) => c.id).join(",");
params.format = "txt";
params.visible_columns = visible_columns;
const url_params = ntopng_url_manager.obj_to_url_params(params);
return `${location.origin}/${download_endpoint}?${url_params}`;
});
onBeforeMount(async () => {
message_to_display.value = `<div class="alert alert-success alert-dismissable"><span>${i18n('no_alerts_require_attention')}</span></div>`;
if (props.context.is_va) {
ntopng_utility.check_and_set_default_time_interval("day");
}
init_params();
init_url_params();
await set_query_presets();
mount_range_picker.value = true;
await load_top_table_array_overview();
});
onMounted(async () => {
register_components_on_status_update();
});
async function init_params() {
page = ntopng_url_manager.get_url_entry("page");
const status = ntopng_url_manager.get_url_entry("status");
if (page == null) { page = "all"; }
if (status == 'engaged' && page == "flow") { ntopng_url_manager.set_key_to_url("status", "historical"); }
chart_data_url = (page == "snmp_device") ? `${http_prefix}/lua/pro/rest/v2/get/snmp/device/alert/ts.lua` : `${http_prefix}/lua/rest/v2/get/${page}/alert/ts.lua`;
selected_query_preset.value = {
value: ntopng_url_manager.get_url_entry("query_preset"),
count: ntopng_url_manager.get_url_entry("count"),
};
if (selected_query_preset.value.value == null) {
selected_query_preset.value.value = "";
}
table_config_id.value = `alert_${page}`;
table_id.value = `${table_config_id.value}_${selected_query_preset.value.value}`;
}
function init_url_params() {
if (ntopng_url_manager.get_url_entry("ifid") == null) {
ntopng_url_manager.set_key_to_url("ifid", default_ifid);
}
if (ntopng_url_manager.get_url_entry("epoch_begin") == null
|| ntopng_url_manager.get_url_entry("epoch_end") == null) {
let default_epoch_begin = Number.parseInt((Date.now() - 1000 * 30 * 60) / 1000);
let default_epoch_end = Number.parseInt(Date.now() / 1000);
ntopng_url_manager.set_key_to_url("epoch_begin", default_epoch_begin);
ntopng_url_manager.set_key_to_url("epoch_end", default_epoch_end);
}
if (ntopng_url_manager.get_url_entry("page") == "flow"
&& ntopng_url_manager.get_url_entry("status") == "engaged") {
ntopng_url_manager.set_key_to_url("status", "historical");
}
}
async function set_query_presets() {
if (!props.context.is_ntop_enterprise_l || ntopng_url_manager.get_url_entry("status") == "engaged") {
ntopng_sync.ready(get_query_presets_sync_key());
return;
}
let url_request = `${http_prefix}/lua/pro/rest/v2/get/alert/preset/consts.lua?page=${page}`;
let res = await ntopng_utility.http_request(url_request);
if (res == null || res.length == 0) {
query_presets.value = [];
ntopng_url_manager.set_key_to_url("query_preset", "");
ntopng_url_manager.set_key_to_url("count", "");
ntopng_sync.ready(get_query_presets_sync_key());
return;
}
query_presets.value = res[0].list.map((el) => {
return {
value: el.id,
name: el.name,
count: el.count,
builtin: true,
};
});
if (res.length > 1) {
res[1].list.forEach((el) => {
let query = {
value: el.id,
name: el.name,
count: el.count,
is_preset: true,
};
query_presets.value.push(query);
});
}
if (selected_query_preset.value == null || selected_query_preset.value.value == "") {
selected_query_preset.value = query_presets.value[0];
} else {
let q = query_presets.value.find((i) => i.value == selected_query_preset.value.value);
selected_query_preset.value = q || query_presets.value[0];
}
ntopng_url_manager.set_key_to_url("query_preset", selected_query_preset.value.value);
ntopng_url_manager.set_key_to_url("count", selected_query_preset.value.count);
ntopng_sync.ready(get_query_presets_sync_key());
}
const page_id = "page-alert-stats";
function get_query_presets_sync_key() {
return `${page_id}_query_presets`;
}
async function load_top_table_array_overview(action) {
if (props.context.show_cards != true || selected_query_preset.value.is_preset == true) { return; }
top_table_array.value = await load_top_table_array("overview");
}
async function load_top_table_details(top, top_index) {
top.show_spinner = true;
await nextTick();
if (top.data_loaded == false) {
let new_top_array = await load_top_table_array(top.id, top);
top.options = new_top_array.find((t) => t.id == top.id).options;
await nextTick();
let dropdown = top_table_dropdown_array.value[top_index];
dropdown.load_menu();
}
top.show_spinner = false;
}
async function load_top_table_array(action, top) {
// top_table.value = [];
const url_params = ntopng_url_manager.get_url_params();
const url = `${props.context.endpoint_cards}?${url_params}&action=${action}`;
let res = await ntopng_utility.http_request(url);
return res.map((t) => {
if (t.value) {
t.value.forEach((e, index) => {
if (e.count < 1) {
t.value[index].count = '<1'
}
})
}
return {
id: t.name,
label: t.label,
title: t.tooltip,
show_spinner: false,
data_loaded: action != 'overview',
options: t.value,
};
});
}
const get_open_top_table_dropdown = (top, top_index) => {
return (d) => {
load_top_table_details(top, top_index);
};
};
async function register_components_on_status_update() {
await ntopng_sync.on_ready("range_picker");
//if (show_chart) {
chart.value.register_status();
//}
//updateDownloadButton();
ntopng_status_manager.on_status_change(page, (new_status) => {
let url_params = ntopng_url_manager.get_url_params();
table_alerts.value.refresh_table();
load_top_table_array_overview();
}, false);
}
function on_table_loaded() {
register_table_alerts_events();
}
function register_table_alerts_events() {
let jquery_table_alerts = $(`#${table_id.value}`);
jquery_table_alerts.on('click', `a.tag-filter`, async function (e) {
add_table_row_filter(e, $(this));
});
}
function update_select_query_presets() {
let url = ntopng_url_manager.get_url_params();
ntopng_url_manager.set_key_to_url("query_preset", selected_query_preset.value.value);
ntopng_url_manager.set_key_to_url("count", selected_query_preset.value.count);
ntopng_url_manager.reload_url();
}
const map_table_def_columns = async (columns) => {
await ntopng_sync.on_ready(get_query_presets_sync_key());
let map_columns = {
"l7_proto": (proto, row) => {
let confidence = "";
if (proto.confidence !== undefined) {
const title = proto.confidence;
(title == "DPI") ? confidence = `<span class="badge bg-success" title="${title}">${title}</span>` : confidence = `<span class="badge bg-warning" title="${title}">${title}</span>`
}
if (row.proto.label !== proto.label) {
return DataTableRenders.filterize('l4proto', row.proto.value, row.proto.label) + ":" + DataTableRenders.filterize('l7proto', proto.value, proto.label.split(":")[1]) + " " + `${confidence}`;
}
return DataTableRenders.filterize('l4proto', row.proto.value, row.proto.label) + " " + `${confidence}`;
},
"info": (info, row) => {
return `${DataTableRenders.filterize('info', info.value, info.label)}`;
},
"address": (address, row) => {
return `${DataTableRenders.filterize('mac', address.value, address.label)}`;
},
"cli2srv_bytes": (info, row) => {
return `${DataTableRenders.filterize('cli2srv_bytes', row.total_bytes.bytes_sent, formatterUtils.getFormatter("bytes")(row.total_bytes.bytes_sent))}`;
},
"srv2cli_bytes": (info, row) => {
return `${DataTableRenders.filterize('srv2cli_bytes', row.total_bytes.bytes_rcvd, formatterUtils.getFormatter("bytes")(row.total_bytes.bytes_rcvd))}`;
}
};
let set_query_preset_columns = selected_query_preset.value.is_preset && columns.length > 0;
if (set_query_preset_columns) {
// add action button that is the first button
columns = [columns[0]].concat(props.context.columns_def);
}
columns.forEach((c) => {
c.render_func = map_columns[c.data_field];
if (c.id == "actions") {
if (set_query_preset_columns == true) {
c.button_def_array = [
{
"id": "expand",
"icon": "fas fa fa-search-plus",
"class": [],
"title_i18n": "db_search.expand_button",
"event_id": "click_button_expand"
},
];
return;
}
const visible_dict = {
snmp_info: props.context.actions.show_snmp_info,
info: props.context.actions.show_info,
historical_data: props.context.actions.show_historical,
acknowledge: props.context.actions.show_acknowledge,
disable: props.context.actions.show_disable,
settings: props.context.actions.show_settings,
remove: props.context.actions.show_delete,
};
c.button_def_array.forEach((b) => {
if (!visible_dict[b.id]) {
b.class.push("disabled");
return;
}
if (b.id == "snmp_info") {
b.f_map_class = (current_class, row) => {
current_class = current_class.filter((class_item) => class_item != "disabled");
if (row.disable_info) {
current_class.push("disabled");
}
return current_class;
}
} else if (b.id == "acknowledge" || b.id == "remove") {
/* Engaged alerts have no acknowledge nor remove */
b.f_map_class = (current_class, row) => {
current_class = current_class.filter((class_item) => class_item != "disabled");
if (row.is_engaged) {
current_class.push("disabled");
}
return current_class;
}
}
});
}
});
return columns;
};
const add_table_row_filter = (e, a) => {
e.stopPropagation();
let key = undefined;
let displayValue = undefined;
let realValue = undefined;
let operator = 'eq';
// Read tag key and value from the <a> itself if provided
if (a.data('tagKey') != undefined) key = a.data('tagKey');
if (a.data('tagRealvalue') != undefined) realValue = a.data('tagRealvalue');
else if (a.data('tagValue') != undefined) realValue = a.data('tagValue');
if (a.data('tagOperator') != undefined) operator = a.data('tagOperator');
let filter = {
id: key,
value: realValue,
operator: operator,
};
add_filter(filter);
}
function add_top_table_filter(opt, event) {
event.stopPropagation();
let filter = {
id: opt.key,
value: opt.value,
operator: opt.operator,
};
add_filter(filter);
}
function add_filter(filter) {
if (range_picker.value.is_filter_defined(filter)) {
ntopng_events_manager.emit_custom_event(ntopng_custom_events.SHOW_MODAL_FILTERS, filter);
} else {
throw `Filter ${filter.value} not defined`;
}
}
const get_extra_params_obj = () => {
let extra_params = ntopng_url_manager.get_url_object();
return extra_params;
};
function click_navbar_item(item) {
ntopng_url_manager.set_key_to_url('page', item.page_name);
ntopng_url_manager.reload_url();
}
function remove_filters_from_url() {
let status = ntopng_status_manager.get_status();
let filters = status.filters;
if (filters == null) { return; }
ntopng_url_manager.delete_params(filters.map((f) => f.id));
}
function show_modal_alerts_filter(alert) {
current_alert.value = alert;
modal_alerts_filter.value.show();
}
function get_permanent_link() {
const $this = $(permanent_link_button.value);
const placeholder = document.createElement('input');
placeholder.value = location.href;
document.body.appendChild(placeholder);
placeholder.select();
// copy the url to the clipboard from the placeholder
document.execCommand("copy");
document.body.removeChild(placeholder);
$this.attr("title", `${_i18n('copied')}!`)
.tooltip("dispose")
.tooltip()
.tooltip("show");
}
function show_modal_traffic_extraction() {
modal_traffic_extraction.value.show();
}
function show_modal_snapshot() {
modal_snapshot.value.show();
}
async function add_exclude(params) {
params.csrf = props.context.csrf;
let url = `${http_prefix}/lua/pro/rest/v2/add/alert/exclusion.lua`;
try {
let headers = {
'Content-Type': 'application/json'
};
await ntopng_utility.http_request(url, { method: 'post', headers, body: JSON.stringify(params) });
let url_params = ntopng_url_manager.get_url_params();
setTimeout(() => {
//todo reloadTable($table, url_params);
ntopng_events_manager.emit_custom_event(ntopng_custom_events.SHOW_GLOBAL_ALERT_INFO, { text_html: _i18n('check_exclusion.disable_warn'), type: "alert-info", timeout: 2 });
}, 1000);
} catch (err) {
console.error(err);
}
}
function refresh_page_components() {
let t = table_alerts.value;
let c = chart.value;
setTimeout(() => {
t.refresh_table();
c.update_chart();
}, 1 * 1000);
}
/* In case no rows are printed, then the message has to be displayed */
function rows_loaded(res) {
if (res?.rows != null) {
display_message.value = (res.rows.length == 0);
}
}
function on_table_custom_event(event) {
let events_managed = {
"click_button_snmp_info": click_button_snmp_info,
"click_button_info": click_button_info,
"click_button_historical_flows": click_button_historical_flows,
"click_button_acknowledge": click_button_acknowledge,
"click_button_disable": click_button_disable,
"click_button_settings": click_button_settings,
"click_button_remove": click_button_remove,
"click_button_expand": click_button_expand,
};
if (events_managed[event.event_id] == null) {
return;
}
events_managed[event.event_id](event);
}
function click_button_expand(event) {
const alert = event.row;
ntopng_url_manager.set_key_to_url("query_preset", "");
ntopng_url_manager.set_key_to_url("count", "");
let status = ntopng_status_manager.get_status();
let filters = status.filters;
let row_filters = alert?.filter?.tag_filters;
if (row_filters?.length > 0) {
row_filters = row_filters.map((f) => {
return {
id: f.id,
operator: f.op,
value: f.value,
};
});
filters = filters.concat(row_filters);
}
// remove duplicate filters
let filters_dict = {};
filters.forEach((f) => filters_dict[`${f.id}_${f.operator}_${f.value}`] = f);
filters = ntopng_utility.object_to_array(filters_dict);
let filters_object = filtersManager.get_filters_object(filters);
ntopng_url_manager.add_obj_to_url(filters_object);
ntopng_url_manager.reload_url();
}
function show_modal_acknowledge_alerts() {
let status = ntopng_status_manager.get_status();
modal_acknowledge_alerts.value.show(status);
}
function show_modal_delete_alerts() {
let status = ntopng_status_manager.get_status();
modal_delete_alerts.value.show(status);
}
function click_button_remove(event) {
const alert = event.row;
let status_view = get_status_view();
modal_delete.value.show(alert, status_view);
}
function click_button_settings(event) {
const alert = event.row;
const check_settings_href = $(alert.msg.configset_ref).attr('href');
window.location.href = check_settings_href;
}
function click_button_disable(event) {
const alert = event.row;
show_modal_alerts_filter(alert);
}
function click_button_acknowledge(event) {
const alert = event.row;
modal_acknowledge.value.show(alert, props.context);
}
function click_button_historical_flows(event) {
const alert = event.row;
let url = `${http_prefix}/lua/pro/db_search.lua&`
let host_ip = alert?.ip?.value; // get alert host ip
// If host is attacker set it as filter for historical value
if (alert.is_attacker) {
url += `cli_ip=${host_ip};eq`
}
if (alert.is_victim) {
url += `srv_ip=${host_ip};eq`
}
if (alert.link_to_past_flows) {
window.location.href = alert.link_to_past_flows;
} else {
window.location.href = url;
}
}
function click_button_snmp_info(event) {
const alert = event.row;
let href = ``;
if (alert.port.value != null) {
href = `${http_prefix}/lua/pro/enterprise/snmp_interface_details.lua?host=${alert.ip}&snmp_port_idx=${alert.port.value}`;
} else {
href = `${http_prefix}/lua/pro/enterprise/snmp_device_details.lua?host=${alert.ip}`;
}
window.open(href, "_blank");
}
function click_button_info(event) {
const alert = event.row;
let status_view = get_status_view();
let params_obj = {
page: page,
status: status_view,
row_id: alert.row_id,
tstamp: alert.tstamp.value,
};
let url_params = ntopng_url_manager.obj_to_url_params(params_obj);
const href = `${props.context.alert_details_url}?${url_params}`;
window.open(href, "_blank");
}
function get_status_view() {
let status_view = ntopng_url_manager.get_url_entry("status");
if (status_view == null || status_view == "") {
status_view = "historical";
}
return status_view;
}
</script>
<style scoped></style>