ui - add requests key to reports data

This commit is contained in:
fl0ppy-d1sk 2024-02-02 12:36:40 +01:00
commit e1cf32ff09
No known key found for this signature in database
GPG key ID: 93EE47CC3D061500
30 changed files with 265 additions and 66 deletions

View file

@ -260,10 +260,39 @@ The first step is to install the plugin by putting the plugin files inside the c
## Writing a plugin
### Structure
!!! tip "Existing plugins"
If the documentation is not enough, you can have a look at the existing source code of [official plugins](https://github.com/bunkerity/bunkerweb-plugins) and the [core plugins](https://github.com/bunkerity/bunkerweb/tree/v1.5.6/src/common/core) (already included in BunkerWeb but they are plugins, technically speaking).
What a plugin structure looks like :
```
plugin /
confs / conf_type.conf
ui / actions.py
template.html
plugin.lua
plugin.json
```
- **conf_type.conf** : add a [custom NGINX configurations.](/quickstart-guide/#custom-configurations)
- **actions.py** : script to execute on flask server.
This script is running on flask context, you have access to lib and utils like `jinja2`, `requests`, etc...
- **template.html** : custom plugin page you can access from ui.
- **plugin.lua** : code to execute on NGINX using [NGING LUA modile.](https://github.com/openresty/lua-nginx-module)
- **plugin.json** : metadata, settings and jobs for your settings.
!!! info "Optional files"
Files like `confs` and `ui` ones are optional. Add them only to fit your needs.
### Getting started
The first step is to create a folder that will contain the plugin :
```shell
@ -515,9 +544,9 @@ BunkerWeb uses an internal job scheduler for periodic tasks like renewing certif
### Plugin page
Plugin pages are used to display information about your plugin and interact with the user inside the plugins section of the [web UI](web-ui.md).
Everything related to the web UI is located inside the subfolder **ui** as we seen in the [previous structure section.](#structure)
Everything related to the web UI is located inside a subfolder named **ui** at the root directory of your plugin. A template file named **template.html** and located inside the **ui** subfolder contains the client code and logic to display your page. Another file named **actions.py** and also located inside the **ui** subfolder contains code that will be executed when the user is interacting with your page (filling a form for example).
#### Basic example
!!! info "Jinja 2 template"
The **template.html** file is a Jinja2 template, please refer to the [Jinja2 documentation](https://jinja.palletsprojects.com) if needed.
@ -555,3 +584,40 @@ def myplugin() :
!!! info "Python libraries"
You can use Python libraries that are already available like :
`Flask`, `Flask-Login`, `Flask-WTF`, `beautifulsoup4`, `docker`, `Jinja2`, `python-magic` and `requests`. To see the full list, you can have a look at the Web UI [requirements.txt](https://github.com/bunkerity/bunkerweb/blobsrc/ui/requirements.txt). If you need external libraries, you can install them inside the **ui** folder of your plugin and then use the classical **import** directive.
#### Utils
To easily update the content of a template inside the UI with JSON, a **SetupPlugin class** is available in `src/ui/static/js/plugins/setup.js` and will be executed when the plugin template is load.
For example, in case **actions.py** return this JSON :
```python
def plugin():
return {"message": "ok", "data": {"name": "test"}}
```
I need to add this on my **template.html** :
```html
<script>
new SetupPlugin({
name: {
el: document.querySelector('[data-name]'),
value: '',
type: 'text',
},
});
</script>
```
!!! info "Check core plugins"
Core plugins are using this utils. Feel free to look at them in order to see in details how each `key` is working.
| key | Type | Description |
| :--------: | :----: | :------------------------------------------------------------------------------------------- |
| `name ` | string | Replace `name` by the JSON key to extract the related value. |
| `el` | element| Select element you want the value to be updated. |
| `value` | any | Default value on template load or in case retrieving JSON failed. |
| `type` | string | Define the script behavior with the incoming value. Available : `text`, `list` and `status`. |
| `textEl` | element| Optional additionnal text content when type is `status`. |
| `listNames`| string | List of data keys when type is `list`. |

View file

@ -1,4 +1,11 @@
{% extends "base.html" %} {% block content %}
<input
type="csrf_token"
name="csrf_token"
value="{{ csrf_token }}"
class="hidden"
hidden
/>
<div class="col-span-12 grid grid-cols-12 gap-4">
<!-- info-->
@ -66,9 +73,7 @@
const setPlugin = new SetupPlugin({
info: {
el: document.querySelector("[data-info]"),
value: `Anti-bot technology is designed to detect and mitigate suspicious or
malicious bots, preventing them from reaching an organization's websites
or IT ecosystem.`,
value: "{{ plugin['description'] or ''}}",
type: "text",
},
count: {

View file

@ -1,4 +1,11 @@
{% extends "base.html" %} {% block content %}
<input
type="csrf_token"
name="csrf_token"
value="{{ csrf_token }}"
class="hidden"
hidden
/>
<div class="col-span-12 grid grid-cols-12 gap-4">
<!-- info-->
@ -61,9 +68,7 @@
const setPlugin = new SetupPlugin({
info: {
el: document.querySelector("[data-info]"),
value: `Basic Auth is a method for an HTTP user agent
(e.g. a web browser) to provide a user name and password when making a
request.`,
value: "{{ plugin['description'] or ''}}",
type: "text",
},
count: {

View file

@ -1,4 +1,12 @@
{% extends "base.html" %} {% block content %}
<input
type="csrf_token"
name="csrf_token"
value="{{ csrf_token }}"
class="hidden"
hidden
/>
<div class="col-span-12 grid grid-cols-12 gap-4">
<!-- info-->
<div class="col-span-12 grid grid-cols-12 gap-4">
@ -110,8 +118,7 @@
const setPlugin = new SetupPlugin({
info: {
el: document.querySelector("[data-info]"),
value: `Ban IP generating too much 'bad' HTTP status
code in a period of time.`,
value: "{{ plugin['description'] or ''}}",
type: "text",
},
count: {

View file

@ -1,4 +1,11 @@
{% extends "base.html" %} {% block content %}
<input
type="csrf_token"
name="csrf_token"
value="{{ csrf_token }}"
class="hidden"
hidden
/>
<div class="col-span-12 grid grid-cols-12 gap-4">
<div class="col-span-12 grid grid-cols-12 gap-4">
@ -225,8 +232,7 @@
const setPlugin = new SetupPlugin({
info: {
el: document.querySelector("[data-info]"),
value: `Deny access based on internal and external
IP/network/rDNS/ASN blacklists.`,
value: "{{ plugin['description'] or ''}}",
type: "text",
},
count_url: {

View file

@ -1,4 +1,11 @@
{% extends "base.html" %} {% block content %}
<input
type="csrf_token"
name="csrf_token"
value="{{ csrf_token }}"
class="hidden"
hidden
/>
<div class="col-span-12 grid grid-cols-12 gap-4">
<!-- status -->
@ -152,8 +159,7 @@
const setPlugin = new SetupPlugin({
info: {
el: document.querySelector("[data-info]"),
value: `BunkerNet is a crowdsourced database of malicious
requests shared between all BunkerWeb instances over the world. `,
value: "{{ plugin['description'] or ''}}",
type: "text",
},
// value : active / inactive / unknown

View file

@ -1,5 +1,13 @@
{% extends "base.html" %} {% block content %}
<input
type="csrf_token"
name="csrf_token"
value="{{ csrf_token }}"
class="hidden"
hidden
/>
<div class="col-span-12 grid grid-cols-12 gap-4">
<!-- info-->
<div
@ -63,8 +71,7 @@
const setPlugin = new SetupPlugin({
info: {
el: document.querySelector("[data-info]"),
value: `Cross-Origin Resource Sharing lets you manage how your
service can be contacted from different origins.`,
value: "{{ plugin['description'] or ''}}",
type: "text",
},
count: {

View file

@ -1,4 +1,11 @@
{% extends "base.html" %} {% block content %}
<input
type="csrf_token"
name="csrf_token"
value="{{ csrf_token }}"
class="hidden"
hidden
/>
<div class="col-span-12 grid grid-cols-12 gap-4">
<!-- info-->
@ -105,9 +112,7 @@
const setPlugin = new SetupPlugin({
info: {
el: document.querySelector("[data-info]"),
value: `The country security feature allows you to apply
policy based on the country of the IP address of clients (blacklist /
whitelist).`,
value: "{{ plugin['description'] or ''}}",
type: "text",
},
blacklist_count: {

View file

@ -1,4 +1,12 @@
{% extends "base.html" %} {% block content %}
<input
type="csrf_token"
name="csrf_token"
value="{{ csrf_token }}"
class="hidden"
hidden
/>
<div class="col-span-12 grid grid-cols-12 gap-4">
<!-- info-->
<div
@ -76,7 +84,7 @@
const setPlugin = new SetupPlugin({
info: {
el: document.querySelector("[data-info]"),
value: `Custom certificates allow you to get HTTPS / SSL / TLS on your requests.`,
value: "{{ plugin['description'] or ''}}",
type: "text",
},
items: {

View file

@ -1,4 +1,11 @@
{% extends "base.html" %} {% block content %}
<input
type="csrf_token"
name="csrf_token"
value="{{ csrf_token }}"
class="hidden"
hidden
/>
<div class="col-span-12 grid grid-cols-12 gap-4">
<!-- info-->
@ -103,9 +110,7 @@
const setPlugin = new SetupPlugin({
info: {
el: document.querySelector("[data-info]"),
value: `BunkerWeb securely stores its current configuration in
a backend database, which contains essential data for smooth
operation.`,
value: "{{ plugin['description'] or ''}}",
type: "text",
},
driver: {

View file

@ -1,4 +1,11 @@
{% extends "base.html" %} {% block content %}
<input
type="csrf_token"
name="csrf_token"
value="{{ csrf_token }}"
class="hidden"
hidden
/>
<div class="col-span-12 grid grid-cols-12 gap-4">
<!-- info-->
@ -65,7 +72,7 @@
const setPlugin = new SetupPlugin({
info: {
el: document.querySelector("[data-info]"),
value: `Deny access based on external DNSBL servers.`,
value: "{{ plugin['description'] or ''}}",
type: "text",
},
items: {

View file

@ -1,4 +1,11 @@
{% extends "base.html" %} {% block content %}
<input
type="csrf_token"
name="csrf_token"
value="{{ csrf_token }}"
class="hidden"
hidden
/>
<div class="col-span-12 grid grid-cols-12 gap-4">
<!-- info-->
@ -65,7 +72,7 @@
const setPlugin = new SetupPlugin({
info: {
el: document.querySelector("[data-info]"),
value: `Show number of occurences of each error code.`,
value: "{{ plugin['description'] or ''}}",
type: "text",
},
items: {

View file

@ -1,4 +1,11 @@
{% extends "base.html" %} {% block content %}
<input
type="csrf_token"
name="csrf_token"
value="{{ csrf_token }}"
class="hidden"
hidden
/>
<div class="col-span-12 grid grid-cols-12 gap-4">
<div class="col-span-12 grid grid-cols-12 gap-4">
@ -225,8 +232,7 @@
const setPlugin = new SetupPlugin({
info: {
el: document.querySelector("[data-info]"),
value: `Deny access based on internal and external
IP/network/rDNS/ASN greylists.`,
value: "{{ plugin['description'] or ''}}",
type: "text",
},
count_url: {

View file

@ -1,4 +1,12 @@
{% extends "base.html" %} {% block content %}
<input
type="csrf_token"
name="csrf_token"
value="{{ csrf_token }}"
class="hidden"
hidden
/>
<div class="col-span-12 grid grid-cols-12 gap-4">
<!-- info-->
<div
@ -76,7 +84,7 @@
const setPlugin = new SetupPlugin({
info: {
el: document.querySelector("[data-info]"),
value: `Let's encrypt certificates allow you to get HTTPS / SSL / TLS on your requests.`,
value: "{{ plugin['description'] or ''}}",
type: "text",
},
items: {

View file

@ -1,4 +1,12 @@
{% extends "base.html" %} {% block content %}
<input
type="csrf_token"
name="csrf_token"
value="{{ csrf_token }}"
class="hidden"
hidden
/>
<div class="col-span-12 grid grid-cols-12 gap-4">
<!-- info-->
<div
@ -71,7 +79,7 @@
const setPlugin = new SetupPlugin({
info: {
el: document.querySelector("[data-info]"),
value: `Limit maximum number of requests and connections.`,
value: "{{ plugin['description'] or ''}}",
type: "text",
},
items: {

View file

@ -1,4 +1,11 @@
{% extends "base.html" %} {% block content %}
<input
type="csrf_token"
name="csrf_token"
value="{{ csrf_token }}"
class="hidden"
hidden
/>
<div class="col-span-12 grid grid-cols-12 gap-4">
<div class="col-span-12 grid grid-cols-12 gap-4">
@ -110,7 +117,7 @@
const setPlugin = new SetupPlugin({
info: {
el: document.querySelector("[data-info]"),
value: `Miscellaneous settings (methods, servers...).`,
value: "{{ plugin['description'] or ''}}",
type: "text",
},
count_disabled_servers: {

View file

@ -1,4 +1,11 @@
{% extends "base.html" %} {% block content %}
<input
type="csrf_token"
name="csrf_token"
value="{{ csrf_token }}"
class="hidden"
hidden
/>
<div class="col-span-12 grid grid-cols-12 gap-4">
<!-- info-->
@ -65,12 +72,12 @@
const setPlugin = new SetupPlugin({
info: {
el: document.querySelector("[data-info]"),
value: `ModSecurity is an open source, cross platform web application firewall (WAF) engine for Apache, IIS and Nginx that is developed by Trustwave's SpiderLabs.`,
value: "{{ plugin['description'] or ''}}",
type: "text",
},
count: {
el: document.querySelector("[data-count]"),
value: `ModSecurity is an open source, cross platform web application firewall (WAF) engine for Apache, IIS and Nginx that is developed by Trustwave's SpiderLabs.`,
value: 'unknown',
type: "text",
},
});

View file

@ -1,4 +1,11 @@
{% extends "base.html" %} {% block content %}
<input
type="csrf_token"
name="csrf_token"
value="{{ csrf_token }}"
class="hidden"
hidden
/>
<div class="col-span-12 grid grid-cols-12 gap-4">
<!-- status -->
@ -150,8 +157,7 @@
const setPlugin = new SetupPlugin({
info: {
el: document.querySelector("[data-info]"),
value: `Redis server configuration when using BunkerWeb in
cluster mode.`,
value: "{{ plugin['description'] or ''}}",
type: "text",
},
// value : active / inactive / unknown

View file

@ -1,4 +1,12 @@
{% extends "base.html" %} {% block content %}
<input
type="csrf_token"
name="csrf_token"
value="{{ csrf_token }}"
class="hidden"
hidden
/>
<div class="col-span-12 grid grid-cols-12 gap-4">
<!-- info-->
<div
@ -67,10 +75,7 @@
const setPlugin = new SetupPlugin({
info: {
el: document.querySelector("[data-info]"),
value: `Reverse scan is a feature designed to detect
open ports by establishing TCP connections with clients' IP addresses.
Consider adding this feature if you want to detect possible open proxies
or connections from servers.`,
value: "{{ plugin['description'] or ''}}",
type: "text",
},
items: {

View file

@ -1,4 +1,12 @@
{% extends "base.html" %} {% block content %}
<input
type="csrf_token"
name="csrf_token"
value="{{ csrf_token }}"
class="hidden"
hidden
/>
<div class="col-span-12 grid grid-cols-12 gap-4">
<!-- info-->
<div
@ -76,7 +84,7 @@
const setPlugin = new SetupPlugin({
info: {
el: document.querySelector("[data-info]"),
value: `Selfsigned certificates allow you to get HTTPS / SSL / TLS on your requests.`,
value: "{{ plugin['description'] or ''}}",
type: "text",
},
items: {

View file

@ -1,4 +1,5 @@
{% if USE_UI == "yes" +%}
SecRule REQUEST_FILENAME "@rx /services$" "id:7771,ctl:ruleRemoveByTag=attack-rce,ctl:ruleRemoveByTag=attack-xss,ctl:ruleRemoveByTag=attack-generic,nolog"
SecRule REQUEST_FILENAME "@rx /global_config$" "id:7772,ctl:ruleRemoveByTag=platform-pgsql,nolog"
SecRule REQUEST_FILENAME "@rx /configs$" "id:7773,ctl:ruleRemoveByTag=language-shell,ctl:ruleRemoveByTag=attack-lfi,nolog"
{% endif +%}

View file

@ -1,4 +1,11 @@
{% extends "base.html" %} {% block content %}
<input
type="csrf_token"
name="csrf_token"
value="{{ csrf_token }}"
class="hidden"
hidden
/>
<div class="col-span-12 grid grid-cols-12 gap-4">
<div class="col-span-12 grid grid-cols-12 gap-4">
@ -225,8 +232,7 @@
const setPlugin = new SetupPlugin({
info: {
el: document.querySelector("[data-info]"),
value: `Allow access based on internal and external
IP/network/rDNS/ASN whitelists.`,
value: "{{ plugin['description'] or ''}}",
type: "text",
},
count_url: {

View file

@ -1259,6 +1259,13 @@ def upload_plugin():
@app.route("/plugins/<plugin>", methods=["GET", "POST"])
@login_required
def custom_plugin(plugin: str):
plugins = app.config["CONFIG"].get_plugins()
curr_plugin = {}
for plug in plugins:
if plug["id"] == plugin:
curr_plugin = plug
break
message = ""
if not plugin_id_rx.match(plugin):
message = f'Invalid plugin id, "{plugin}" (must be between 1 and 64 characters, only letters, numbers, underscores and hyphens)'
@ -1276,6 +1283,7 @@ def custom_plugin(plugin: str):
dark_mode=app.config["DARK_MODE"],
username=current_user.get_id(),
current_endpoint=plugin,
plugin=curr_plugin,
**app.jinja_env.globals,
)
@ -1574,9 +1582,11 @@ def logs_container(container_id):
logs.append(
{
"content": log,
"type": "error"
if "[error]" in log_lower or "[crit]" in log_lower or "[alert]" in log_lower or "" in log_lower
else ("warn" if "[warn]" in log_lower or "⚠️" in log_lower else ("info" if "[info]" in log_lower or "" in log_lower else "message")),
"type": (
"error"
if "[error]" in log_lower or "[crit]" in log_lower or "[alert]" in log_lower or "" in log_lower
else ("warn" if "[warn]" in log_lower or "⚠️" in log_lower else ("info" if "[info]" in log_lower or "" in log_lower else "message"))
),
}
)

View file

@ -360,7 +360,7 @@ class Instances:
resp, instance_reports = instance.reports()["requests"]
except :
continue
if not resp:
continue
reports.extend(instance_reports[instance.name if instance.name != "local" else "127.0.0.1"].get("msg", []))

File diff suppressed because one or more lines are too long

View file

@ -31,7 +31,7 @@ class SetupPlugin {
*/
// Hidden elements that will be shown on success, like ping buttons or list rendering
this.showOnSuccessEls = document.querySelectorAll(
"[data-fetch-success-show]"
"[data-fetch-success-show]",
);
this.init();
@ -47,7 +47,8 @@ class SetupPlugin {
fetch(location.href, {
method: "POST",
headers: {
"X-CSRFToken": "{{ csrf_token() }}",
"X-CSRFToken": document.querySelector('input[name="csrf_token"]')
.value,
},
})
.then((res) => res.json())
@ -76,7 +77,7 @@ class SetupPlugin {
],
"bg-sky-500 p-4 mb-1 md:mb-3 md:mr-3 z-[1001] flex flex-col fixed bottom-0 right-0 w-full md:w-1/2 max-w-[300px] min-h-20 rounded-lg dark:brightness-110 hover:scale-102 transition shadow-md break-words dark:bg-slate-850 dark:shadow-dark-xl bg-clip-border",
"",
""
"",
);
this.alertCloseEl = this.createEl(
@ -84,7 +85,7 @@ class SetupPlugin {
[["data-fetch-close", ""]],
"absolute right-7 top-1.5",
"",
this.alertEl
this.alertEl,
);
this.alertCloseIconEl = this.createEl(
@ -95,7 +96,7 @@ class SetupPlugin {
],
"cursor-pointer fill-white dark:fill-gray-300 dark:opacity-80 absolute h-5 w-5",
"",
this.alertCloseEl
this.alertCloseEl,
);
// Close icon paths
@ -115,7 +116,7 @@ class SetupPlugin {
[["data-fetch-status", ""]],
"text-lg mb-0 text-white dark:text-gray-300",
"Fetching",
this.alertEl
this.alertEl,
);
this.alertMsgEl = this.createEl(
@ -123,7 +124,7 @@ class SetupPlugin {
[["data-fetch-msg", ""]],
"text-white dark:text-gray-300 mb-0 text-sm",
"Please wait...",
this.alertEl
this.alertEl,
);
document.body.appendChild(this.alertEl);

View file

@ -259,13 +259,7 @@ class FolderDropdown {
class FolderEditor {
constructor() {
this.editor = ace.edit("editor", {
backwards: false,
wrap: false,
caseSensitive: false,
wholeWord: false,
regExp: false,
});
this.editor = ace.edit("editor");
this.darkMode = document.querySelector("[data-dark-toggle]");
this.initEditor();
this.listenDarkToggle();
@ -274,8 +268,6 @@ class FolderEditor {
initEditor() {
//editor options
this.editor.setShowPrintMargin(false);
this.editor.session.setUseSoftTabs(true);
this.editor.setOption("showInvisibles", true);
this.setDarkMode();
}

View file

@ -429,7 +429,7 @@ data-{{current_endpoint}}-modal
<!-- editor-->
<div class="mt-4 w-full justify-end flex">
<button
<button type="button"
data-{{current_endpoint}}-modal-close
class="close-btn text-xs mr-2"

View file

@ -50,7 +50,7 @@
</div>
<!-- action button -->
<div class="w-full justify-center flex mt-10">
<button
<button type="button"
data-plugins-modal-close
class="close-btn mb-4 mr-3 text-base"
>

View file

@ -363,7 +363,7 @@ url_for(request.endpoint)[1:].split("/")[-1].strip() %}
<div
class="col-span-12 overflow-y-auto overflow-x-auto"
> <!-- list container-->
<div class="min-w-[1200px] w-full grid grid-cols-12 rounded p-2">
<div class="min-w-[1300px] w-full grid grid-cols-12 rounded p-2">
<!-- header-->
<p
class="dark:text-gray-300 flex justify-center h-8 text-sm font-bold col-span-1 m-0 pb-2 border-b border-gray-400"
@ -420,7 +420,7 @@ url_for(request.endpoint)[1:].split("/")[-1].strip() %}
>
<p
class="flex justify-center dark:text-gray-400 dark:opacity-80 text-sm col-span-1 m-0 my-1"
class="text-center flex justify-center dark:text-gray-400 dark:opacity-80 text-sm col-span-1 m-0 my-1"
data-{{current_endpoint}}-date="{{report['date']}}"
>
{{report['date']}}