mirror of
https://github.com/stefanpejcic/openpanel
synced 2025-06-26 18:28:26 +00:00
840 lines
32 KiB
HTML
840 lines
32 KiB
HTML
{% extends 'base.html' %}
|
|
|
|
{% block content %}
|
|
<!-- Page header -->
|
|
<div class="page-header mt-0 d-print-none">
|
|
<div class="container-xl">
|
|
<div class="row g-2 align-items-center">
|
|
<div class="col">
|
|
<!-- Page pre-title -->
|
|
<div class="page-pretitle">
|
|
Settings
|
|
</div>
|
|
<h2 class="page-title">
|
|
ModSecurity
|
|
</h2>
|
|
</div>
|
|
<!-- Page title actions -->
|
|
<div class="col-auto ms-auto d-print-none">
|
|
<div class="btn-list">
|
|
{% if is_modsec_installed == 'YES' %}
|
|
<!-- Tab navigation -->
|
|
<div class="nav-tabs-wrap mb-0">
|
|
<ul class="nav nav-tabs">
|
|
<li class="nav-item">
|
|
<a class="nav-link active" data-bs-toggle="tab" href="#settings">Settings</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" data-bs-toggle="tab" href="#logs">Logs</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" data-bs-toggle="tab" href="#rules">Rules</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
{% elif is_modsec_installed == 'NO' %}
|
|
{% if modsec_install_log %}
|
|
<button type="button" class="btn btn-primary" disabled>Instalallation in progress..</button>
|
|
{% else %}
|
|
<form method="post">
|
|
<input type="hidden" name="action" value="install">
|
|
<button class="btn btn-primary" type="submit">Install ModSecurity</button>
|
|
</form>
|
|
{% endif %}
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Page body -->
|
|
<div class="page-body">
|
|
<div class="container-xl">
|
|
<div class="row row-deck row-cards">
|
|
|
|
{% if is_modsec_installed == 'YES' %}
|
|
|
|
|
|
<!-- Tab content -->
|
|
<div class="tab-content">
|
|
<!-- Settings Tab -->
|
|
<div class="tab-pane fade show active" id="settings">
|
|
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<form method="post">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<h3 class="card-title">Enable ModSecurity for all domains owned by user</h3>
|
|
<p class="card-subtitle"><p></p>
|
|
<select class="form-select" id="userSelect" name="username" placeholder="Select a user">
|
|
<option value="" disabled selected hidden>Select a user</option>
|
|
{% for user in users %}
|
|
<option value="{{ user[1] }}" {% if "SUSPENDED_" in user[1] %}disabled{% endif %}>
|
|
{{ user[1].split('_')[-1] }} {% if "SUSPENDED_" in user[1] %}(SUSPENDED){% endif %}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="card-footer">
|
|
<div class="row align-items-center">
|
|
<div class="col">Learn more about <a href="https://dev.openpanel.com/services.html#ModSecurity" target="_blank">ModSecurity</a></div>
|
|
<div class="col-auto">
|
|
<input type="hidden" name="action" value="activate_for_user">
|
|
<button class="btn btn-primary" type="submit">Enable</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
|
|
<div class="col-md-6">
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
<!-- END Settings Tab -->
|
|
|
|
|
|
<!-- Logs Tab -->
|
|
<div class="tab-pane fade" id="logs">
|
|
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Logs</h3>
|
|
</div>
|
|
<div class="card-body border-bottom py-3">
|
|
<div class="d-flex">
|
|
<div class="text-secondary">
|
|
Show
|
|
<div class="mx-2 d-inline-block">
|
|
<input type="number" class="form-control w-6 form-control-sm" id="entriesCount" min="10" value="20" size="3" aria-label="Logs count">
|
|
</div>
|
|
entries
|
|
</div>
|
|
<div class="ms-auto text-secondary">
|
|
Search:
|
|
<div class="ms-2 d-inline-block">
|
|
<input type="text" id="ModsearchInput" class="form-control form-control-sm" aria-label="Search ModSecurity Logs">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table card-table table-vcenter table-striped text-nowrap datatable" id="logsTable">
|
|
<thead>
|
|
<tr>
|
|
<th>Date</th>
|
|
<th>Domain</th>
|
|
<th>Source</th>
|
|
<th>Severity</th>
|
|
<th>Status</th>
|
|
<th>Rule ID</th>
|
|
<th>Reason</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="card-footer d-flex align-items-center">
|
|
<p class="m-0 text-secondary">Showing <span id="showingEntries">1 to 20 of 100</span> entries</p>
|
|
<ul class="pagination pagination_for_logs m-0 ms-auto">
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
$(document).ready(function(){
|
|
var perPage = parseInt($('#entriesCount').val(), 10);
|
|
var searchWord = $('#ModsearchInput').val();
|
|
loadLogs(1, perPage, searchWord);
|
|
|
|
$('#ModsearchInput').on('input', function() {
|
|
searchWord = $(this).val();
|
|
loadLogs(1, perPage, searchWord);
|
|
});
|
|
|
|
// Event handler for changing entries count
|
|
$('#entriesCount').on('change', function() {
|
|
perPage = parseInt($(this).val(), 10) || 20;
|
|
|
|
// Validate perPage to have a minimum of 10
|
|
if (perPage < 10) {
|
|
perPage = 10;
|
|
$(this).val(perPage); // Update the value in the input field
|
|
}
|
|
|
|
loadLogs(1, perPage, searchWord);
|
|
});
|
|
|
|
// Event listener for view log details button
|
|
$('#logsTable').on('click', '.view-log-details', function() {
|
|
var $row = $(this).closest('tr');
|
|
|
|
var $logDetailsRow = $row.next('.log-details-row');
|
|
|
|
// Close any open log details rows
|
|
$('.log-details-row').not($logDetailsRow).remove();
|
|
$('.view-log-details').html('<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-down"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M6 9l6 6l6 -6" /></svg> View Details');
|
|
|
|
// If the clicked row is already open, close it and return
|
|
if ($logDetailsRow.length && $logDetailsRow.is(':visible')) {
|
|
$logDetailsRow.remove();
|
|
return;
|
|
}
|
|
|
|
// Find the log content from the data attribute
|
|
var logContent = $(this).data('log');
|
|
|
|
// Create a new row for log details
|
|
$logDetailsRow = $('<tr class="log-details-row text-wrap"></tr>');
|
|
|
|
// Create a cell for log details and set colspan to match the number of columns
|
|
var $logDetailsCell = $('<td colspan="7" class="log-details-cell"></td>');
|
|
|
|
// Buttons
|
|
var $logDetailsButtonsCell = $('<td></td>');
|
|
|
|
// Create a div to hold the log content
|
|
var $logDetailsContent = $('<pre class="log-details text-wrap"></pre>');
|
|
|
|
// Select all IP address elements
|
|
const ipElements = document.querySelectorAll('.ip-address');
|
|
|
|
|
|
var ipAddress = $(this).data('ip');
|
|
|
|
// Buttons
|
|
var $logDetailsButtonsContent = $('<div class="btn-list flex-wrap"> <a href="#" class="btn"> <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-edit" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1"></path><path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415z"></path><path d="M16 5l3 3"></path></svg> Edit Rule </a> <a href="/security/firewall?deny=' + ipAddress +'" class="btn btn-danger"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-circle-off"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M20.042 16.045a9 9 0 0 0 -12.087 -12.087m-2.318 1.677a9 9 0 1 0 12.725 12.73" /><path d="M3 3l18 18" /></svg> Block IP </a></div>');
|
|
|
|
// Set the log content
|
|
$logDetailsContent.text(logContent);
|
|
|
|
// Append log content to the cell
|
|
$logDetailsCell.append($logDetailsContent);
|
|
$logDetailsButtonsCell.append($logDetailsButtonsContent);
|
|
|
|
// Append cell to the row
|
|
$logDetailsRow.append($logDetailsCell);
|
|
$logDetailsRow.append($logDetailsButtonsCell);
|
|
|
|
// Insert the log details row below the corresponding row
|
|
$row.after($logDetailsRow);
|
|
|
|
// Change button to "Hide Details"
|
|
$(this).html('<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-up"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M6 15l6 -6l6 6" /></svg> Hide Details');
|
|
});
|
|
|
|
|
|
|
|
$(document).on('click', '#showAllLogsBtn', function() {
|
|
$('#ModsearchInput').val('');
|
|
loadLogs(1, perPage, '');
|
|
});
|
|
|
|
|
|
|
|
// Function to handle clicking on parsedLog.host, parsedLog.source, and parsedLog.ruleId
|
|
function handleLogClick(element) {
|
|
var searchText = $(element).text(); // Get the text of the clicked element
|
|
$('#ModsearchInput').val(searchText); // Set the search field value to the clicked text
|
|
loadLogs(1, perPage, searchText); // Trigger the call to fetch logs with the new search text
|
|
}
|
|
|
|
// Event delegation to handle clicks on parsedLog.host, parsedLog.source, and parsedLog.ruleId
|
|
$('#logsTable').on('click', '.host-link', function() {
|
|
handleLogClick(this);
|
|
});
|
|
|
|
$('#logsTable').on('click', '.source-link', function() {
|
|
handleLogClick(this);
|
|
});
|
|
|
|
$('#logsTable').on('click', '.ruleId-link', function() {
|
|
handleLogClick(this);
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
function loadLogs(page, perPage, searchWord) {
|
|
$.ajax({
|
|
url: '/security/modsecurity/waf_logs',
|
|
data: {
|
|
'per_page': perPage,
|
|
'page': page,
|
|
'search': searchWord // Include search word in the request
|
|
},
|
|
success: function(data) {
|
|
|
|
var logs = data.logs.split('\n').filter(function(log) { return log.trim() !== ''; }); // Filter out empty lines
|
|
var $tbody = $('#logsTable tbody');
|
|
$tbody.empty(); // Clear current table body
|
|
|
|
$.each(logs, function(i, log) {
|
|
var parsedLog = parseLogEntry(log);
|
|
var row = '<tr>' +
|
|
'<td>' + parsedLog.date + '</td>' +
|
|
'<td><a href="#" class="host-link">' + parsedLog.host + '</a></td>' +
|
|
'<td><span class="ip-address" data-ip="' + parsedLog.source + '"><a href="#" class="source-link">' + parsedLog.source + '</a></span></td>' +
|
|
'<td>' + parsedLog.severity + '</td>' +
|
|
'<td><span class="badge bg-danger me-1"></span>' + parsedLog.status + '</td>' +
|
|
'<td><a href="#" class="ruleId-link">' + parsedLog.ruleId + '</a></td>' +
|
|
'<td>' + parsedLog.message + '</td>' +
|
|
'<td><button type="button" data-ip="' + parsedLog.source + '" class="btn btn-link view-log-details" data-log="' + log.replace(/"/g, '"') + '"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-down"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M6 9l6 6l6 -6" /></svg> View Details</button></td>' +
|
|
'</tr>';
|
|
$tbody.append(row);
|
|
});
|
|
|
|
// get flags
|
|
detect_flags_now();
|
|
|
|
// Update pagination
|
|
var $pagination = $('.pagination_for_logs');
|
|
$pagination.empty(); // Clear current pagination
|
|
|
|
var totalPages = data.pagination.total_pages;
|
|
for(var i = 1; i <= totalPages; i++) {
|
|
var $li = $('<li class="page-item"><a class="page-link" href="#">' + i + '</a></li>');
|
|
(function (pageNumber) {
|
|
$li.find('a').on('click', function(e) {
|
|
e.preventDefault();
|
|
loadLogs(pageNumber, perPage, searchWord); // Updated to pass 'searchWord' parameter
|
|
});
|
|
})(i);
|
|
if(i === page) {
|
|
$li.addClass('active');
|
|
}
|
|
$pagination.append($li);
|
|
}
|
|
|
|
// Update 'Showing X to Y of Z entries'
|
|
var startEntry = (page - 1) * perPage + 1;
|
|
var endEntry = Math.min(startEntry + logs.length - 1, data.pagination.total_entries); // Ensure end entry does not exceed total entries
|
|
var showingText = `Showing ${startEntry} to ${endEntry} of ${data.pagination.total_entries} entries`;
|
|
$('.card-footer .text-secondary').html(showingText);
|
|
},
|
|
error: function(xhr, status, error) {
|
|
// Handle AJAX error
|
|
console.error("AJAX request failed:", status, error);
|
|
var $tbody = $('#logsTable tbody');
|
|
$tbody.empty();
|
|
var row = '<tr>' +
|
|
'<td colspan="8"><div class="empty"><p class="empty-title">No results found</p> <p class="empty-subtitle text-secondary"> Try adjusting your search or filter to find what you're looking for. </p> <div class="empty-action"> <button class="btn btn-secondary" id="showAllLogsBtn">Show All Logs</button> </div> </div></td>' +
|
|
'</tr>';
|
|
$tbody.append(row);
|
|
|
|
// Clear current pagination
|
|
var $pagination = $('.pagination');
|
|
$pagination.empty();
|
|
// Clear showing X of Y text
|
|
var showingText = `0 entries`;
|
|
$('.card-footer .text-secondary').html(showingText);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
}
|
|
|
|
function parseLogEntry(log) {
|
|
var date = log.match(/(\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2})/)[0];
|
|
var host = log.match(/host: "([^"]+)"/)[1];
|
|
var source = log.match(/client: (\d+\.\d+\.\d+\.\d+)/)[1];
|
|
var severity = log.match(/severity "(\d+)"/)[1];
|
|
var status = log.match(/code (\d{3})/)[1];
|
|
var ruleId = log.match(/id "(\d+)"/)[1];
|
|
var message = log.match(/msg "([^"]+)"/)[1];
|
|
|
|
return {
|
|
date: date,
|
|
host: host,
|
|
source: source,
|
|
severity: severity,
|
|
status: status,
|
|
ruleId: ruleId,
|
|
message: message
|
|
};
|
|
}
|
|
|
|
|
|
|
|
function detect_flags_now() {
|
|
// Select all IP address elements
|
|
const ipElements = document.querySelectorAll('.ip-address');
|
|
|
|
// Create an object to store country codes for each IP address to avoid redundant API calls
|
|
const countryCodes = {};
|
|
|
|
// Create a queue to track IP addresses that need to be checked
|
|
const ipQueue = [];
|
|
|
|
ipElements.forEach((element) => {
|
|
const ipAddress = element.getAttribute('data-ip'); // Get the IP address from data-ip attribute
|
|
|
|
// Check if country code for the current IP address is already fetched
|
|
if (countryCodes[ipAddress]) {
|
|
// If country code is already fetched, prepend the flag icon to the IP address
|
|
prependFlagIcon(element, countryCodes[ipAddress], ipAddress);
|
|
} else if (!ipQueue.includes(ipAddress)) {
|
|
// If country code is not fetched and IP address is not in the queue, add it to the queue
|
|
ipQueue.push(ipAddress);
|
|
}
|
|
});
|
|
|
|
// Process the IP address queue
|
|
processQueue(ipQueue, countryCodes);
|
|
}
|
|
|
|
function processQueue(ipQueue, countryCodes) {
|
|
// Iterate through the IP address queue
|
|
ipQueue.forEach(ipAddress => {
|
|
// Make a GET request to the API for each IP address in the queue
|
|
fetch(`https://api.country.is/${ipAddress}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data && data.country) {
|
|
// Extract the country code from the API response
|
|
const countryCode = data.country.toLowerCase();
|
|
|
|
// Store the country code in the countryCodes object
|
|
countryCodes[ipAddress] = countryCode;
|
|
|
|
// Select IP address elements with the current IP address
|
|
const ipElements = document.querySelectorAll(`.ip-address[data-ip="${ipAddress}"]`);
|
|
|
|
// Prepend the flag icon to each IP address element
|
|
ipElements.forEach(element => {
|
|
prependFlagIcon(element, countryCode, ipAddress);
|
|
});
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error fetching country info:', error);
|
|
});
|
|
});
|
|
}
|
|
|
|
function prependFlagIcon(element, countryCode, ipAddress) {
|
|
// Prepend the flag icon to the IP address
|
|
element.innerHTML = `<span class="flag flag-xs flag-country-${countryCode} me-2"></span><a href="#" class="source-link">${ipAddress}</a>`;
|
|
}
|
|
|
|
|
|
|
|
|
|
</script>
|
|
</div>
|
|
<!-- END Logs Tab -->
|
|
|
|
|
|
|
|
<!-- Rules Tab -->
|
|
<div class="tab-pane fade" id="rules">
|
|
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="col card-title">Rules</h3>
|
|
|
|
<form class="col-auto" method="post">
|
|
<input type="hidden" name="action" value="update_rules">
|
|
<button class="btn" type="submit"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-a-b-2"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M16 21h3c.81 0 1.48 -.67 1.48 -1.48l.02 -.02c0 -.82 -.69 -1.5 -1.5 -1.5h-3v3z" /><path d="M16 15h2.5c.84 -.01 1.5 .66 1.5 1.5s-.66 1.5 -1.5 1.5h-2.5v-3z" /><path d="M4 9v-4c0 -1.036 .895 -2 2 -2s2 .964 2 2v4" /><path d="M2.99 11.98a9 9 0 0 0 9 9m9 -9a9 9 0 0 0 -9 -9" /><path d="M8 7h-4" /></svg> Update rules</button>
|
|
</form>
|
|
</div>
|
|
<div class="card-body border-bottom py-3">
|
|
<div class="d-flex">
|
|
<div class="text-secondary">
|
|
<span id="count_of_rule_files"></span>
|
|
</div>
|
|
<div class="ms-auto text-secondary">
|
|
Search:
|
|
<div class="ms-2 d-inline-block">
|
|
<input type="text" id="searchRulesInput" class="form-control form-control-sm" aria-label="Search Rules">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table id="wafRulesTable" class="table card-table table-vcenter table-striped text-nowrap datatable">
|
|
<thead>
|
|
<tr>
|
|
<th>Config File</th>
|
|
<th>Provider</th>
|
|
<th>Status</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td colspan="4"><div class="empty"><p class="empty-title">Loading rules..</p> <p class="empty-subtitle text-secondary"> </p> <div class="empty-action"></div> </div></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="card-footer d-flex align-items-center">
|
|
<p class="m-0 text-secondary" id="count_of_rule_files_bottom"></p>
|
|
<ul class="pagination m-0 ms-auto">
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<style>
|
|
|
|
.suspended-user-row {
|
|
background-color: #f4c6cb;
|
|
background-color: rgba(var(--tblr-secondary-rgb),.08);
|
|
border-left: .25rem var(--tblr-border-style) red;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
$(document).ready(function() {
|
|
// Send AJAX request to the server
|
|
$.ajax({
|
|
url: "/security/modsecurity/waf_rules",
|
|
type: "GET",
|
|
dataType: "json",
|
|
success: function(response) {
|
|
$('#wafRulesTable tbody').empty();
|
|
|
|
// Iterate over each rule and create table rows
|
|
$.each(response, function(index, rule) {
|
|
var filePath = rule.file;
|
|
var fileName = rule.file.split('/').pop();
|
|
fileNameForDisplay = fileName.replace('.disabled', '');
|
|
var status = fileName.endsWith('.disabled') ? 'Disabled' : 'Enabled';
|
|
var rowClass = fileName.endsWith('.disabled') ? 'suspended-user-row' : '';
|
|
var statusClass = fileName.endsWith('.disabled') ? '<span class="badge bg-danger me-1"></span>' : '<span class="badge bg-success me-1"></span>';
|
|
|
|
|
|
var buttonText = status === 'Disabled' ? '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-circle-check"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" /><path d="M9 12l2 2l4 -4" /></svg> Enable ' : '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-circle-off"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M20.042 16.045a9 9 0 0 0 -12.087 -12.087m-2.318 1.677a9 9 0 1 0 12.725 12.73"></path><path d="M3 3l18 18"></path></svg> Disable';
|
|
var buttonClass = status === 'Disabled' ? 'btn btn-primary' : 'btn btn-danger';
|
|
|
|
// Extract version and format it for link
|
|
var version = rule.version;
|
|
var versionLink;
|
|
if (!version) {
|
|
versionName = 'Custom';
|
|
} else if (version.endsWith("-dev")) {
|
|
versionLink = 'https://github.com/coreruleset/coreruleset/tree/nightly/rules';
|
|
versionName = 'nightly';
|
|
} else {
|
|
versionName = version;
|
|
versionLink = 'https://github.com/coreruleset/coreruleset/tree/v' + versionName + '/rules';
|
|
}
|
|
var versionText = versionLink ? '<a href="' + versionLink + '" target="_blank alt="View rules from the version '+ version +'"">OWASP ModSecurity Core Rule Set ' + version + ' <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-external-link"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 6h-6a2 2 0 0 0 -2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-6" /><path d="M11 13l9 -9" /><path d="M15 4h5v5" /></svg></a>' : versionName;
|
|
|
|
var tableRow = '<tr class="' + rowClass +'"><td>' + fileNameForDisplay + '<a style="display:none;" href="#" onclick="handleClick() data-path="' + filePath.replace(/ /g, '/') + '" class="ms-1 copy-path" aria-label="Copy path"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M9 15l6 -6"></path><path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464"></path><path d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463"></path></svg></a></td><td class="text-secondary">' + versionText + '</td><td>' + statusClass + status + '</td>' +
|
|
'<td><div class="btn-list flex-nowrap"><button class="btn button_to_triger_modal_file_content_view" data-path="' + filePath + '">View file</button>' +
|
|
'<button class="'+ buttonClass + ' toggle-status" data-path="' + filePath + '">'+ buttonText +'</button></div></td></tr>';
|
|
|
|
|
|
$('#wafRulesTable tbody').append(tableRow);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
count_rows();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
});
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error("Error fetching data:", error);
|
|
// Optionally, display an error message to the user
|
|
$('#wafRulesTable tbody').append('<tr><td colspan="4">Error fetching data</td></tr>');
|
|
}
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Event listener for toggle status button
|
|
$(document).on('click', '.toggle-status', function() {
|
|
var filePath = $(this).data('path');
|
|
var newStatus = 'Enabled'; // Assuming toggle always enables the rule
|
|
|
|
// Check if filePath ends with .conf or .disabled
|
|
if (filePath.endsWith('.conf')) {
|
|
// Make GET request to enable the rule
|
|
$.get('/security/modsecurity/waf_rules?disable=' + encodeURIComponent(filePath), function(data) {
|
|
// Handle success response
|
|
alert("Disabled conf. file: " + filePath + "\nNew status: " + newStatus);
|
|
// Reload the page after the alert is closed
|
|
window.location.href = '/security/modsecurity?after_disable#rules';
|
|
|
|
}).fail(function() {
|
|
// Handle failure response
|
|
alert("Failed to disable conf file: " + filePath);
|
|
});
|
|
} else if (filePath.endsWith('.disabled')) {
|
|
// Make GET request to disable the rule
|
|
$.get('/security/modsecurity/waf_rules?enable=' + encodeURIComponent(filePath), function(data) {
|
|
// Handle success response
|
|
alert("Enabled conf. file: " + filePath + "\nNew status: " + newStatus);
|
|
window.location.href = '/security/modsecurity?after_enable#rules';
|
|
}).fail(function() {
|
|
// Handle failure response
|
|
alert("Failed to enable conf file: " + filePath);
|
|
});
|
|
}
|
|
});
|
|
|
|
|
|
// Event listener for View File button
|
|
$(document).on('click', '.button_to_triger_modal_file_content_view', function() {
|
|
var button = $(this);
|
|
var originalText = button.html();
|
|
var filePath = button.data('path');
|
|
|
|
// Change button text to loading indicator
|
|
button.html('<div class="spinner-border spinner-border-sm text-secondary" role="status"></div> Wait..');
|
|
|
|
// Send AJAX request to get file content
|
|
$.ajax({
|
|
url: "/security/modsecurity/waf_rules?file=" + encodeURIComponent(filePath),
|
|
type: "GET",
|
|
dataType: "text",
|
|
success: function(fileContent) {
|
|
// Update modal content with file content
|
|
var modalHeader = $('#fileModal .modal-header');
|
|
var fileName = filePath.substring(filePath.lastIndexOf('/') + 1);
|
|
|
|
var modalTitle = '<h5 class="modal-title">' + fileName + '</h5>';
|
|
|
|
if (filePath.endsWith('.conf')) {
|
|
modalTitle += '<span class="badge bg-green-lt">ENABLED</span>';
|
|
} else if (filePath.endsWith('.conf.disabled')) {
|
|
modalTitle += '<span class="badge bg-red text-red-fg">DISABLED</span>';
|
|
}
|
|
|
|
modalHeader.html(modalTitle);
|
|
|
|
$('#fileModal .modal-body').html('<pre>' + fileContent + '</pre>');
|
|
|
|
// Show the modal
|
|
$('#fileModal').modal('show');
|
|
|
|
// Restore button text
|
|
button.html(originalText);
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error("Error fetching file content:", error);
|
|
// Optionally, display an error message to the user
|
|
$('#fileModal .modal-header').html('<h5 class="modal-title">Error</h5>');
|
|
$('#fileModal .modal-body').html('<p>Error fetching file content</p>');
|
|
|
|
// Show the modal
|
|
$('#fileModal').modal('show');
|
|
|
|
// Restore button text
|
|
button.html(originalText);
|
|
}
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// FILTER TABLE
|
|
$('#searchRulesInput').on('keyup', function(){
|
|
var searchText = $(this).val().toLowerCase();
|
|
$('#wafRulesTable tbody tr').filter(function(){
|
|
$(this).toggle($(this).text().toLowerCase().indexOf(searchText) > -1);
|
|
});
|
|
count_rows();
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
function count_rows() {
|
|
//var table = document.getElementById("wafRulesTable");
|
|
//var tbody = table.getElementsByTagName("tbody")[0];
|
|
//var rowCount = tbody.rows.length;
|
|
|
|
var rowCount = $('#wafRulesTable tbody tr:visible').length;
|
|
|
|
// Clear the content first
|
|
document.getElementById("count_of_rule_files").textContent = '';
|
|
document.getElementById("count_of_rule_files_bottom").textContent = '';
|
|
|
|
// Set the count of rows
|
|
document.getElementById("count_of_rule_files").textContent = 'Showing ' + rowCount + ' files';
|
|
document.getElementById("count_of_rule_files_bottom").textContent = 'Showing ' + rowCount + ' files';
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function handleClick() {
|
|
var filePath = event.target.getAttribute('data-path');
|
|
if(filePath) {
|
|
navigator.clipboard.writeText(filePath)
|
|
.then(function() {
|
|
alert("Copied to clipboard: " + filePath);
|
|
})
|
|
.catch(function(error) {
|
|
console.error('Error copying text: ', error);
|
|
});
|
|
} else {
|
|
console.error('Data-path attribute not found.');
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<!-- Modal to view rule content -->
|
|
<div class="modal modal-blur fade" id="fileModal" tabindex="-1" aria-labelledby="fileModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered modal-lg modal-dialog-scrollable">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="fileModalLabel">File Content</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p>COntent</p>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
</div>
|
|
<!-- END Rules Tab -->
|
|
|
|
|
|
<!-- Files Tab -->
|
|
<div class="tab-pane fade" id="files">
|
|
</div>
|
|
<!-- END Files Tab -->
|
|
|
|
<div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{% elif is_modsec_installed == 'NO' %}
|
|
<div class="col-lg-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h4 class="card-title">ModSecurity is not installed</h4>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-lg-12">
|
|
{% if modsec_install_log %}
|
|
<p>ModSecurity installation is in progress, please allow up to 15 minutes for the process to finish. You can monitor progress from the terminal by running command:</p>
|
|
<pre>tail -f {{modsec_install_log}}</pre>
|
|
{% else %}
|
|
<p>Click on the button bellow to install NGINX ModSecurity web application firewall (WAF).</p>
|
|
<form method="post">
|
|
<input type="hidden" name="action" value="install">
|
|
<button class="btn btn-primary" type="submit">Install ModSecurity</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
|
|
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{% endblock %}
|