added Malcolm
This commit is contained in:
56
Vagrant/resources/malcolm/name-map-ui/config/fpm-pool.conf
Normal file
56
Vagrant/resources/malcolm/name-map-ui/config/fpm-pool.conf
Normal file
@@ -0,0 +1,56 @@
|
||||
[global]
|
||||
; Log to /dev/fd/1 for docker
|
||||
error_log = /dev/fd/1
|
||||
|
||||
[www]
|
||||
; The address on which to accept FastCGI requests.
|
||||
; Valid syntaxes are:
|
||||
; 'ip.add.re.ss:port' - to listen on a TCP socket to a specific IPv4 address on
|
||||
; a specific port;
|
||||
; '[ip:6:addr:ess]:port' - to listen on a TCP socket to a specific IPv6 address on
|
||||
; a specific port;
|
||||
; 'port' - to listen on a TCP socket to all addresses
|
||||
; (IPv6 and IPv4-mapped) on a specific port;
|
||||
; '/path/to/unix/socket' - to listen on a unix socket.
|
||||
; Note: This value is mandatory.
|
||||
listen = 127.0.0.1:9000
|
||||
|
||||
; Enable status page
|
||||
pm.status_path = /fpm-status
|
||||
|
||||
; Ondemand process manager
|
||||
pm = ondemand
|
||||
|
||||
; The number of child processes to be created when pm is set to 'static' and the
|
||||
; maximum number of child processes when pm is set to 'dynamic' or 'ondemand'.
|
||||
; This value sets the limit on the number of simultaneous requests that will be
|
||||
; served. Equivalent to the ApacheMaxClients directive with mpm_prefork.
|
||||
; Equivalent to the PHP_FCGI_CHILDREN environment variable in the original PHP
|
||||
; CGI. The below defaults are based on a server without much resources. Don't
|
||||
; forget to tweak pm.* to fit your needs.
|
||||
; Note: Used when pm is set to 'static', 'dynamic' or 'ondemand'
|
||||
; Note: This value is mandatory.
|
||||
pm.max_children = 100
|
||||
|
||||
; The number of seconds after which an idle process will be killed.
|
||||
; Note: Used only when pm is set to 'ondemand'
|
||||
; Default Value: 10s
|
||||
pm.process_idle_timeout = 10s;
|
||||
|
||||
; The number of requests each child process should execute before respawning.
|
||||
; This can be useful to work around memory leaks in 3rd party libraries. For
|
||||
; endless request processing specify '0'. Equivalent to PHP_FCGI_MAX_REQUESTS.
|
||||
; Default Value: 0
|
||||
pm.max_requests = 1000
|
||||
|
||||
; Make sure the FPM workers can reach the environment variables for configuration
|
||||
clear_env = no
|
||||
|
||||
; Catch output from PHP
|
||||
catch_workers_output = yes
|
||||
|
||||
; Remove the 'child 10 said into stderr' prefix in the log and only show the actual message
|
||||
decorate_workers_output = no
|
||||
|
||||
; Enable ping page to use in healthcheck
|
||||
ping.path = /fpm-ping
|
||||
92
Vagrant/resources/malcolm/name-map-ui/config/nginx.conf
Normal file
92
Vagrant/resources/malcolm/name-map-ui/config/nginx.conf
Normal file
@@ -0,0 +1,92 @@
|
||||
worker_processes 1;
|
||||
error_log stderr warn;
|
||||
pid /tmp/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Define custom log format to include reponse times
|
||||
log_format main_timed '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for" '
|
||||
'$request_time $upstream_response_time $pipe $upstream_cache_status';
|
||||
|
||||
access_log /var/log/nginx/access.log main_timed;
|
||||
error_log /var/log/nginx/error.log notice;
|
||||
|
||||
keepalive_timeout 65;
|
||||
|
||||
# Write temporary files to /tmp so they can be created as a non-privileged user
|
||||
client_body_temp_path /tmp/client_temp;
|
||||
proxy_temp_path /tmp/proxy_temp_path;
|
||||
fastcgi_temp_path /tmp/fastcgi_temp;
|
||||
uwsgi_temp_path /tmp/uwsgi_temp;
|
||||
scgi_temp_path /tmp/scgi_temp;
|
||||
|
||||
# Default server definition
|
||||
server {
|
||||
listen 8080 default_server;
|
||||
server_name _;
|
||||
|
||||
sendfile off;
|
||||
|
||||
root /var/www/html;
|
||||
index index.php index.html;
|
||||
|
||||
location / {
|
||||
# First attempt to serve request as file, then
|
||||
# as directory, then fall back to index.php
|
||||
try_files $uri $uri/ /index.php?q=$uri&$args;
|
||||
}
|
||||
|
||||
location ~* maps/.+\.(txt|json)$ {
|
||||
expires -1;
|
||||
add_header 'Cache-Control' 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
|
||||
}
|
||||
|
||||
# Redirect server error pages to the static page /50x.html
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /var/lib/nginx/html;
|
||||
}
|
||||
|
||||
# Pass the PHP scripts to PHP-FPM listening on 127.0.0.1:9000
|
||||
location ~ \.php$ {
|
||||
try_files $uri =404;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass 127.0.0.1:9000;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi_params;
|
||||
}
|
||||
|
||||
location ~* \.(jpg|jpeg|gif|png|css|js|ico|xml)$ {
|
||||
expires 5d;
|
||||
}
|
||||
|
||||
# Deny access to . files, for security
|
||||
location ~ /\. {
|
||||
log_not_found off;
|
||||
deny all;
|
||||
}
|
||||
|
||||
# Allow fpm ping and status from localhost
|
||||
location ~ ^/(fpm-status|fpm-ping)$ {
|
||||
access_log off;
|
||||
allow 127.0.0.1;
|
||||
deny all;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
include fastcgi_params;
|
||||
fastcgi_pass 127.0.0.1:9000;
|
||||
}
|
||||
}
|
||||
|
||||
# Include other server configs
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
||||
7
Vagrant/resources/malcolm/name-map-ui/config/php.ini
Normal file
7
Vagrant/resources/malcolm/name-map-ui/config/php.ini
Normal file
@@ -0,0 +1,7 @@
|
||||
[PHP]
|
||||
file_uploads = On
|
||||
upload_max_filesize = 64M
|
||||
max_file_uploads = 4
|
||||
|
||||
[Date]
|
||||
date.timezone="UTC"
|
||||
@@ -0,0 +1,10 @@
|
||||
; Copyright (c) 2021 Battelle Energy Alliance, LLC. All rights reserved.
|
||||
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
logfile=/dev/null
|
||||
logfile_maxbytes=0
|
||||
pidfile=/tmp/supervisord-logstash.pid
|
||||
|
||||
[supervisorctl]
|
||||
serverurl=http://logstash:9001
|
||||
@@ -0,0 +1,72 @@
|
||||
; Copyright (c) 2021 Battelle Energy Alliance, LLC. All rights reserved.
|
||||
|
||||
[unix_http_server]
|
||||
file=/tmp/supervisor-main.sock ; (the path to the socket file)
|
||||
chmod=0700
|
||||
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
logfile=/dev/null
|
||||
logfile_maxbytes=0
|
||||
pidfile=/tmp/supervisor-main.pid
|
||||
|
||||
[rpcinterface:supervisor]
|
||||
supervisor.rpcinterface_factory=supervisor.rpcinterface:make_main_rpcinterface
|
||||
|
||||
[supervisorctl]
|
||||
serverurl=unix:///tmp/supervisor-main.sock
|
||||
|
||||
[program:php-fpm]
|
||||
command=/usr/sbin/php-fpm7 -F
|
||||
stopasgroup=true
|
||||
killasgroup=true
|
||||
stdout_logfile=/dev/fd/1
|
||||
stdout_logfile_maxbytes=0
|
||||
redirect_stderr=true
|
||||
autorestart=false
|
||||
startretries=0
|
||||
|
||||
[program:nginx]
|
||||
command=/usr/sbin/nginx -g 'daemon off;'
|
||||
stopasgroup=true
|
||||
killasgroup=true
|
||||
stdout_logfile=/dev/fd/1
|
||||
stdout_logfile_maxbytes=0
|
||||
redirect_stderr=true
|
||||
autorestart=false
|
||||
startretries=0
|
||||
|
||||
[program:logaccess]
|
||||
command=/usr/bin/tail -F /var/log/nginx/access.log
|
||||
startsecs=10
|
||||
startretries=2000000000
|
||||
stopasgroup=true
|
||||
killasgroup=true
|
||||
stdout_logfile=/dev/fd/1
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/null
|
||||
stdout_logfile_maxbytes=0
|
||||
redirect_stderr=false
|
||||
|
||||
[program:logerrors]
|
||||
command=/usr/bin/tail -F /var/log/nginx/error.log
|
||||
startsecs=10
|
||||
startretries=2000000000
|
||||
stopasgroup=true
|
||||
killasgroup=true
|
||||
stdout_logfile=/dev/fd/1
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/null
|
||||
stdout_logfile_maxbytes=0
|
||||
redirect_stderr=false
|
||||
|
||||
[program:watch-upload]
|
||||
command=/bin/bash -c "sleep 15 && /usr/local/bin/name-map-save-watch.sh"
|
||||
startsecs=20
|
||||
startretries=1
|
||||
stopasgroup=true
|
||||
killasgroup=true
|
||||
directory=/var/www/html/upload
|
||||
stdout_logfile=/dev/fd/1
|
||||
stdout_logfile_maxbytes=0
|
||||
redirect_stderr=true
|
||||
22
Vagrant/resources/malcolm/name-map-ui/scripts/name-map-save-watch.sh
Executable file
22
Vagrant/resources/malcolm/name-map-ui/scripts/name-map-save-watch.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Copyright (c) 2021 Battelle Energy Alliance, LLC. All rights reserved.
|
||||
|
||||
PROCESS_DIR=${NAME_MAP_SAVE_DIR:-/var/www/html/upload/}
|
||||
DEST_MAP=${NAME_MAP_JSON:-/var/www/html/maps/net-map.json}
|
||||
|
||||
inotifywait -m -e close_write --format '%w%f' "${PROCESS_DIR}" | while read NEWFILE
|
||||
do
|
||||
sleep 0.1
|
||||
# verify that the file is json|txt and that it is valid JSON
|
||||
FILEMIME=$(file -b --mime-type "$NEWFILE")
|
||||
if ( echo "$FILEMIME" | grep -q -e "\(application/json\|text/plain\)" ) && ( python3 -mjson.tool "$NEWFILE" >/dev/null 2>&1 ); then
|
||||
# move the new net-map.json file into its final location
|
||||
# (done like this with "tee" since we may not be able to cp overwrite a volume-mounted file)
|
||||
(>/dev/null tee "$DEST_MAP" < "$NEWFILE") && echo "\"$NEWFILE\" -> \"$DEST_MAP\""
|
||||
rm -f "$NEWFILE"
|
||||
else
|
||||
# invalid or unhandled file type uploaded, delete it
|
||||
(>&2 rm -f "$NEWFILE") && echo "Removed \"$NEWFILE\" (\"$FILEMIME\"): invalid file type or format"
|
||||
fi
|
||||
done
|
||||
523
Vagrant/resources/malcolm/name-map-ui/site/index.html
Normal file
523
Vagrant/resources/malcolm/name-map-ui/site/index.html
Normal file
@@ -0,0 +1,523 @@
|
||||
<!doctype html>
|
||||
|
||||
<!-- Copyright (c) 2021 Battelle Energy Alliance, LLC. All rights reserved. -->
|
||||
|
||||
<!--[if lt IE 7 ]> <html lang="en" class="no-js ie6"> <![endif]-->
|
||||
<!--[if IE 7 ]> <html lang="en" class="no-js ie7"> <![endif]-->
|
||||
<!--[if IE 8 ]> <html lang="en" class="no-js ie8"> <![endif]-->
|
||||
<!--[if IE 9 ]> <html lang="en" class="no-js ie9"> <![endif]-->
|
||||
<!--[if (gt IE 9)|!(IE)]><!--> <html lang="en" class="no-js"> <!--<![endif]-->
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<link rel="stylesheet" href="name-map-ui/mapping.css">
|
||||
<script src="name-map-ui/jquery.min.js"></script>
|
||||
<title>Host and Network Segment Name Mapping</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="container" class="mapping-page">
|
||||
<div id="main">
|
||||
<div class="c1">
|
||||
<img class="center" src="name-map-ui/Malcolm_banner.png" alt="">
|
||||
<h1>Host and Network Segment Name Mapping</h1>
|
||||
<div id="mapping">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="sort" title="Mapping type (host or network segment)" data-sort="type">Type</th>
|
||||
<th class="sort" title="Host address (IP or MAC) or CIDR-formatted IP range" data-sort="address">Address</th>
|
||||
<th class="sort" title="Host or network segment name" data-sort="name">Name</th>
|
||||
<th class="sort" title="Assign name only if the event contains this tag (optional)" data-sort="tag">Tag</th>
|
||||
<th colspan="2">
|
||||
<input type="text" class="search" placeholder="🔎 Search mappings" />
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="list"/>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td class="type">
|
||||
<input type="hidden" id="id-field" />
|
||||
<select id="type-field">
|
||||
<option value="host">🖳 host</option>
|
||||
<option value="segment">🖧 segment</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="address">
|
||||
<input type="text" id="address-field" placeholder="Address" />
|
||||
</td>
|
||||
<td class="name">
|
||||
<input type="text" id="name-field" placeholder="Name" />
|
||||
</td>
|
||||
<td class="tag">
|
||||
<input type="text" id="tag-field" placeholder="Tag (optional)" />
|
||||
</td>
|
||||
<td class="add" colspan="2">
|
||||
<button title="Create new mapping" class="add-btn" id="add-btn">💾</button>
|
||||
<button title="Modify mapping" class="update-btn" id="update-btn">💾</button>
|
||||
<button title="Cancel changes" class="cancel-btn" id="cancel-btn">❌</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="foot">
|
||||
<input type="file" id="import-file" name="import-file" style="display:none;"/>
|
||||
<button title="Import mappings from a JSON file" class="import-btn" id="import-btn">📥 Import</button>
|
||||
</td>
|
||||
<td class="foot">
|
||||
<button title="Download mappings as a JSON file" class="export-btn" id="export-btn">📤 Export</button>
|
||||
</td>
|
||||
<td class="foot" colspan="2">
|
||||
<button title="Write mappings to Malcolm's net-map.json" class="save-btn" id="save-btn">💾 Save Mappings</button>
|
||||
<input type="hidden" value=0 id="save-state"/>
|
||||
</td>
|
||||
<td class="foot" colspan="2">
|
||||
<button title="Restart log ingestion, parsing and enrichment" class="restart-btn" id="restart-btn">🔁 Restart Logstash</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- end of #container .mapping-page -->
|
||||
|
||||
<script src="name-map-ui/list.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
|
||||
// define value names and template for new list items
|
||||
var options = {
|
||||
valueNames: [ 'id', 'type', 'address', 'name', 'tag' ],
|
||||
item: '<tr><td class="id" style="display:none;"><td class="type"></td></td><td class="address"></td><td class="name"></td><td class="tag"></td><td class="update"><button title="Modify this mapping" class="edit-item-btn">📝</button></td><td class="remove"><button title="Remove this entry" class="remove-item-btn">🚫</button></td></tr>'
|
||||
};
|
||||
|
||||
// initialize list and other elements
|
||||
var mappingList = new List('mapping', options);
|
||||
|
||||
var idField = $('#id-field'),
|
||||
typeField = $('#type-field'),
|
||||
addressField = $('#address-field'),
|
||||
nameField = $('#name-field'),
|
||||
tagField = $('#tag-field'),
|
||||
addBtn = $('#add-btn'),
|
||||
updateBtn = $('#update-btn').hide(),
|
||||
cancelBtn = $('#cancel-btn').hide(),
|
||||
saveBtn = $('#save-btn'),
|
||||
saveState = $('#save-state'),
|
||||
exportBtn = $('#export-btn'),
|
||||
importBtn = $('#import-btn'),
|
||||
importFile = $('#import-file'),
|
||||
restartBtn = $('#restart-btn'),
|
||||
removeBtns = $('.remove-item-btn'),
|
||||
editBtns = $('.edit-item-btn');
|
||||
|
||||
// sets callbacks to the buttons in the list
|
||||
refreshCallbacks();
|
||||
|
||||
function focusItem(itemId) {
|
||||
// scroll back up to item that was updated
|
||||
// todo: is there a more efficient way to do this? there's got to be with list.js
|
||||
for (const editBtnKey in editBtns) {
|
||||
if (editBtnKey && (editBtns[editBtnKey])) {
|
||||
entry = (editBtns[editBtnKey].closest) ? editBtns[editBtnKey].closest('tr').firstChild : null;
|
||||
if (entry && entry.firstChild && entry.firstChild.data && (String(entry.firstChild.data) === String(itemId))) {
|
||||
editBtns[editBtnKey].focus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} // focusItem
|
||||
|
||||
// store a new value from the edit inputs into the list
|
||||
addBtn.click(function() {
|
||||
const type = typeField.val();
|
||||
const address = addressField.val().trim();
|
||||
const name = nameField.val().trim();
|
||||
const tag = tagField.val().trim();
|
||||
if ((file_types_valid.has(type)) &&
|
||||
(address) && (address.length > 0) &&
|
||||
(name) && (name.length > 0)) {
|
||||
mappingList.add({
|
||||
// todo: better random ID generator (if necessary)
|
||||
id: Math.floor(Math.random()*110000),
|
||||
type: type,
|
||||
address: address,
|
||||
name: name,
|
||||
tag: tag
|
||||
});
|
||||
saveState.val(parseInt(saveState.val()) + 1);
|
||||
clearFields();
|
||||
refreshCallbacks();
|
||||
}
|
||||
});
|
||||
|
||||
// update an item being edited back into the list
|
||||
updateBtn.click(function() {
|
||||
const itemId = idField.val();
|
||||
const type = typeField.val();
|
||||
const address = addressField.val().trim();
|
||||
const name = nameField.val().trim();
|
||||
const tag = tagField.val().trim();
|
||||
if ((file_types_valid.has(type)) &&
|
||||
(address) && (address.length > 0) &&
|
||||
(name) && (name.length > 0)) {
|
||||
var item = mappingList.get('id', itemId)[0];
|
||||
item.values({
|
||||
id: itemId,
|
||||
type: type,
|
||||
address: addressField.val(),
|
||||
name: nameField.val(),
|
||||
tag: tagField.val()
|
||||
});
|
||||
saveState.val(parseInt(saveState.val()) + 1);
|
||||
clearFields();
|
||||
updateBtn.hide();
|
||||
cancelBtn.hide();
|
||||
addBtn.show();
|
||||
focusItem(itemId);
|
||||
}
|
||||
});
|
||||
|
||||
// revert edits without updating
|
||||
cancelBtn.click(function() {
|
||||
const itemId = idField.val();
|
||||
clearFields();
|
||||
updateBtn.hide();
|
||||
cancelBtn.hide();
|
||||
addBtn.show();
|
||||
focusItem(itemId);
|
||||
});
|
||||
|
||||
function hasJsonStructure(str) {
|
||||
if (typeof str !== 'string') return false;
|
||||
try {
|
||||
const result = JSON.parse(str);
|
||||
const type = Object.prototype.toString.call(result);
|
||||
return type === '[object Object]'
|
||||
|| type === '[object Array]';
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function download(filename, text, contentType="text/plain") {
|
||||
var element = document.createElement('a');
|
||||
element.setAttribute('href', 'data:' + contentType + ';charset=utf-8,' + encodeURIComponent(text));
|
||||
element.setAttribute('download', filename);
|
||||
element.style.display = 'none';
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
|
||||
async function uploadNameMap(jsonStr) {
|
||||
let formData = new FormData();
|
||||
let upBlob = new Blob([jsonStr], { type: 'application/json' });
|
||||
formData.append("upfile", upBlob);
|
||||
try {
|
||||
let r = await fetch('name-map-ui/upload.php', {method: "POST", body: formData});
|
||||
} catch(e) {
|
||||
console.log('uploadNameMap error: ', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function restartLogstash() {
|
||||
let formData = new FormData();
|
||||
formData.append("save-state", saveState.val());
|
||||
try {
|
||||
let r = await fetch('name-map-ui/restart-logstash.php', {method: "POST", body: formData});
|
||||
} catch(e) {
|
||||
console.log('restartLogstash error: ', e);
|
||||
}
|
||||
}
|
||||
|
||||
saveBtn.click(function() {
|
||||
if ((mappingList) && (confirm("Save name mappings?") === true )) {
|
||||
// create list of all items (minus the random "id" index field)
|
||||
let items = mappingList.items.map(
|
||||
function(item) { return (({ id, ...o }) => o)(item.values()); }
|
||||
);
|
||||
uploadNameMap(JSON.stringify(items, null, 2));
|
||||
saveState.val(0);
|
||||
}
|
||||
});
|
||||
|
||||
exportBtn.click(function() {
|
||||
if (mappingList) {
|
||||
// create list of all items (minus the random "id" index field)
|
||||
let items = mappingList.items.map(
|
||||
function(item) { return (({ id, ...o }) => o)(item.values()); }
|
||||
);
|
||||
download('net-map.json', JSON.stringify(items, null, 2), "application/json");
|
||||
}
|
||||
});
|
||||
|
||||
importFile.change(function (event) {
|
||||
if ((event) && (event.target) && (event.target.files) && (event.target.files.length > 0)) {
|
||||
let f = event.target.files[0];
|
||||
if (((f.type === 'application/json') || (f.type === 'text/plain')) && (f.size <= 67108864)) {
|
||||
let reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
if (hasJsonStructure(e.target.result)) {
|
||||
// replace current map list with that from imported file
|
||||
populateMapList([{fileType: null,
|
||||
fileSrc: file_spec_in_memory,
|
||||
filePath: e.target.result}], -1);
|
||||
|
||||
} else {
|
||||
alert('Invalid file format: ' + f.name + ', ' + f.type + ', ' + f.size + ' bytes');
|
||||
}
|
||||
};
|
||||
reader.readAsText(f);
|
||||
} else {
|
||||
alert('Invalid file: ' + f.name + ', ' + f.type + ', ' + f.size + ' bytes');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
importBtn.click(function() {
|
||||
if ((mappingList) && (confirm("Clear and replace current name mappings with those from a file?") === true )) {
|
||||
importFile.click();
|
||||
}
|
||||
});
|
||||
|
||||
restartBtn.click(function() {
|
||||
let confirmMsg = '';
|
||||
if (parseInt(saveState.val()) > 0) {
|
||||
saveBtn.click();
|
||||
}
|
||||
confirmMsg = confirmMsg.concat("Apply the saved name mappings and restart Logstash?");
|
||||
if (confirm(confirmMsg) == true ) {
|
||||
restartLogstash();
|
||||
alert("Logstash is restarting in the background.\nLog ingestion will be resumed in a few minutes.");
|
||||
}
|
||||
});
|
||||
|
||||
// apply callbacks for the buttons on the list items
|
||||
function refreshCallbacks() {
|
||||
|
||||
removeBtns = $(removeBtns.selector);
|
||||
editBtns = $(editBtns.selector);
|
||||
|
||||
// delete item
|
||||
removeBtns.click(function() {
|
||||
var itemId = $(this).closest('tr').find('.id').text();
|
||||
mappingList.remove('id', itemId);
|
||||
saveState.val(parseInt(saveState.val()) + 1);
|
||||
});
|
||||
|
||||
// populate the edit inputs from the values of row where Edit was clicked
|
||||
editBtns.click(function() {
|
||||
var itemId = $(this).closest('tr').find('.id').text();
|
||||
var itemValues = mappingList.get('id', itemId)[0].values();
|
||||
idField.val(itemValues.id);
|
||||
typeField.val(itemValues.type);
|
||||
addressField.val(itemValues.address);
|
||||
nameField.val(itemValues.name);
|
||||
tagField.val(itemValues.tag);
|
||||
|
||||
updateBtn.show();
|
||||
cancelBtn.show();
|
||||
addBtn.hide();
|
||||
window.scrollTo(0,document.body.scrollHeight);
|
||||
// focus and scroll to editing fields
|
||||
addressField.focus();
|
||||
addressField.select();
|
||||
});
|
||||
}
|
||||
|
||||
// clear edit inputs
|
||||
function clearFields() {
|
||||
typeField.val('host');
|
||||
addressField.val('');
|
||||
nameField.val('');
|
||||
tagField.val('');
|
||||
}
|
||||
|
||||
const file_type_txt_cidr = 'segment';
|
||||
const file_type_txt_host = 'host';
|
||||
const file_types_valid = new Set([file_type_txt_cidr, file_type_txt_host]);
|
||||
|
||||
const file_spec_on_server = 'SERVER';
|
||||
const file_spec_in_memory = 'LOCALSTR';
|
||||
const file_specs_valid = new Set([file_spec_on_server, file_spec_in_memory]);
|
||||
|
||||
|
||||
// given the text of a mapping file (eg., net-map.json, cidr-map.txt, host-map.txt)
|
||||
// populate an array containing the mapping entries from that file
|
||||
function parseMapFileText(fileTxt, mapType=null) {
|
||||
let result;
|
||||
|
||||
if (hasJsonStructure(fileTxt)) {
|
||||
// is already JSON text, should be the format we want
|
||||
result = JSON.parse(fileTxt);
|
||||
|
||||
} else {
|
||||
// parse the lines from the cidr-map.txt/host-map.txt format
|
||||
result = [];
|
||||
const lines = fileTxt.split(/\r?\n/);
|
||||
for (lineIdx in lines) {
|
||||
let line = lines[lineIdx];
|
||||
if (!line.startsWith("#")) {
|
||||
const vals = line.split("|");
|
||||
const valsLen = vals.length;
|
||||
if ((valsLen >= 2) && (valsLen < 4)) {
|
||||
const name = vals[1].trim();
|
||||
const tag = (valsLen > 2) ? vals[2].trim() : "";
|
||||
const addrs = vals[0].trim().split(",");
|
||||
for (addrIdx in addrs) {
|
||||
result.push({
|
||||
type: mapType,
|
||||
address: addrs[addrIdx],
|
||||
name: name,
|
||||
tag: tag
|
||||
});
|
||||
}
|
||||
} // line has the right number of delimited fields
|
||||
} // line is not a # comment
|
||||
} // for (lineIdx in lines)
|
||||
} // if/else hasJsonStructure
|
||||
|
||||
if (!Array.isArray(result)) {
|
||||
result = null;
|
||||
}
|
||||
return result;
|
||||
|
||||
} // parseMapFileText
|
||||
|
||||
|
||||
// given an array of filespecs (eg.,
|
||||
// [{fileType: file_type_txt_cidr, filePath: 'maps/cidr-map.txt'},
|
||||
// {fileType: file_type_txt_host, filePath: 'maps/host-map.txt'},
|
||||
// {fileType: null, filePath: 'maps/net-map.json'}]
|
||||
// read and return an array of the mappings within those files
|
||||
|
||||
function loadMapsFromFiles(fileSpecs, cb) {
|
||||
|
||||
let result = []
|
||||
|
||||
if ((fileSpecs.length > 0) && (fileSpecs[0]) && (file_specs_valid.has(fileSpecs[0].fileSrc))) {
|
||||
|
||||
// if this is a delimited file (not JSON) mark the type
|
||||
let mapType = null;
|
||||
if (fileSpecs[0].fileType === file_type_txt_cidr) {
|
||||
mapType = "segment";
|
||||
} else if (fileSpecs[0].fileType === file_type_txt_host) {
|
||||
mapType = "host";
|
||||
}
|
||||
// else the type is per-item in the JSON
|
||||
|
||||
if (fileSpecs[0].fileSrc === file_spec_in_memory) {
|
||||
// the text of the file is already in memory
|
||||
result = parseMapFileText(fileSpecs[0].filePath, mapType);
|
||||
return cb(result);
|
||||
|
||||
} else {
|
||||
// GET the file from the server
|
||||
var txtFile = new XMLHttpRequest();
|
||||
txtFile.open("GET", fileSpecs[0].filePath, true);
|
||||
txtFile.send();
|
||||
txtFile.onreadystatechange = function() {
|
||||
if (txtFile.status === 200) {
|
||||
if (txtFile.readyState === 4) {
|
||||
result = parseMapFileText(txtFile.responseText, mapType);
|
||||
if (fileSpecs.length > 1) {
|
||||
// we have processed this fileSpec, process the next
|
||||
loadMapsFromFiles(fileSpecs.slice(1), function(nextFileResult) {
|
||||
return cb(result.concat(nextFileResult));
|
||||
});
|
||||
} else {
|
||||
// we have processed this fileSpec, and there are no more to process
|
||||
return cb(result);
|
||||
}
|
||||
} // txtFile.readyState is ready
|
||||
|
||||
} else if (fileSpecs.length > 1) {
|
||||
// the GET returned an error, process the next fileSpec
|
||||
loadMapsFromFiles(fileSpecs.slice(1), function(nextFileResult) {
|
||||
return cb(result.concat(nextFileResult));
|
||||
});
|
||||
|
||||
} else {
|
||||
// the GET returned an error, and there are no more fileSpecs to process
|
||||
return cb(result);
|
||||
}
|
||||
|
||||
} // txtFile.onreadystatechange
|
||||
}
|
||||
|
||||
} else if (fileSpecs.length > 1) {
|
||||
// the first fileSpec is invalid, process the next
|
||||
loadMapsFromFiles(fileSpecs.slice(1), function(nextFileResult) {
|
||||
return cb(result.concat(nextFileResult));
|
||||
});
|
||||
|
||||
} else {
|
||||
// the first fileSpec is missing or invalid, and there are no more to process
|
||||
return cb(result);
|
||||
}
|
||||
|
||||
} // loadMapsFromFiles
|
||||
|
||||
function populateMapList(fileSpecs, newSaveState=0) {
|
||||
// load old delimited plain text format
|
||||
// IP or MAC address to host name map:
|
||||
// address|host name|required tag
|
||||
// CIDR to network segment format:
|
||||
// IP(s)|segment name|required tag
|
||||
// and JSON-formatted native format:
|
||||
// [
|
||||
// {
|
||||
// "type": "segment",
|
||||
// "address": "172.16.0.0/24",
|
||||
// "name": "home",
|
||||
// "tag": ""
|
||||
// }, ...
|
||||
mappingList.clear();
|
||||
loadMapsFromFiles(fileSpecs, function (mapsArray) {
|
||||
|
||||
// convert to a hash to resolve any duplicates
|
||||
var mapsHash = mapsArray.reduce(function(acc, cur) {
|
||||
acc[cur.type + '|' + cur.address] = cur;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// populate the list.js List object
|
||||
for (mapKey in mapsHash) {
|
||||
let map = mapsHash[mapKey];
|
||||
if ((file_types_valid.has(map.type)) &&
|
||||
(map.address) && (map.address.length > 0) &&
|
||||
(map.name) && (map.name.length > 0)) {
|
||||
mappingList.add({
|
||||
// todo: better random ID generator (if necessary)
|
||||
id: Math.floor(Math.random()*110000),
|
||||
type: map.type,
|
||||
address: map.address,
|
||||
name: map.name,
|
||||
tag: map.tag ? map.tag : ""
|
||||
});
|
||||
}
|
||||
}
|
||||
mappingList.sort('address');
|
||||
refreshCallbacks();
|
||||
saveState.val((newSaveState >= 0) ? newSaveState : Math.max(Object.keys(mapsHash).length, 1));
|
||||
});
|
||||
} // populateMapList
|
||||
|
||||
// initial page load from cidr-map.txt, host-map.txt, and/or net-map.json
|
||||
populateMapList([{fileType: file_type_txt_cidr,
|
||||
fileSrc: file_spec_on_server,
|
||||
filePath: 'name-map-ui/maps/cidr-map.txt',},
|
||||
{fileType: file_type_txt_host,
|
||||
fileSrc: file_spec_on_server,
|
||||
filePath: 'name-map-ui/maps/host-map.txt'},
|
||||
{fileType: null,
|
||||
fileSrc: file_spec_on_server,
|
||||
filePath: 'name-map-ui/maps/net-map.json'}]);
|
||||
|
||||
</script> <!-- end of ListJS-related scripts -->
|
||||
|
||||
</body>
|
||||
</html>
|
||||
229
Vagrant/resources/malcolm/name-map-ui/site/mapping.css
Normal file
229
Vagrant/resources/malcolm/name-map-ui/site/mapping.css
Normal file
@@ -0,0 +1,229 @@
|
||||
/* Copyright (c) 2021 Battelle Energy Alliance, LLC. All rights reserved. */
|
||||
|
||||
html {
|
||||
font-family: sans-serif;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #cecece;
|
||||
text-align: left;
|
||||
background-color: #272B30;
|
||||
}
|
||||
|
||||
body,
|
||||
div,
|
||||
dl,
|
||||
dt,
|
||||
dd,
|
||||
ul,
|
||||
ol,
|
||||
li,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
pre,
|
||||
form,
|
||||
fieldset,
|
||||
input,
|
||||
textarea,
|
||||
p,
|
||||
blockquote,
|
||||
th,
|
||||
td {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
fieldset,
|
||||
img {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
img.center {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem
|
||||
}
|
||||
|
||||
caption,
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
header {
|
||||
float: left;
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
line-height: inherit
|
||||
}
|
||||
|
||||
#container {
|
||||
width: 800px;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
width: 800px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
width: 355px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
width: 235px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem
|
||||
}
|
||||
|
||||
h1 {
|
||||
letter-spacing: 1px;
|
||||
text-align: center;
|
||||
text-shadow: #262729 0 -1px 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #bbb;
|
||||
text-shadow: #262729 0 -1px 0;
|
||||
font-weight: 300;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 2px 10px;
|
||||
border-radius: 10px;
|
||||
border: solid 1px #555;
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
border: none;
|
||||
outline: none;
|
||||
color: #007bff;
|
||||
font-weight: bold;
|
||||
border-top: solid 1px #aaa;
|
||||
border-right: solid 1px #e6e6e6;
|
||||
border-bottom: solid 1px #e6e6e6;
|
||||
border-left: solid 1px #aaa;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: rgba(23, 25, 28, .8);
|
||||
border: solid 1px rgba(255, 255, 255, .1);
|
||||
padding: 10px 10px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
color: #ddd;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background-color: #454c54;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 10px 10px;
|
||||
border: solid 1px rgba(255, 255, 255, .1);
|
||||
width: auto;
|
||||
}
|
||||
|
||||
td.update,
|
||||
td.remove,
|
||||
td.type {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
td.address {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
button.add-btn,
|
||||
button.update-btn,
|
||||
button.cancel-btn,
|
||||
button.edit-item-btn,
|
||||
button.remove-item-btn {
|
||||
font-size: 1.33rem;
|
||||
border: 2px #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
button.save-btn,
|
||||
button.export-btn,
|
||||
button.import-btn,
|
||||
button.restart-btn {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
#container.mapping-page {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#mapping {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#mapping td.update,
|
||||
#mapping td.remove {
|
||||
width: 130px;
|
||||
}
|
||||
|
||||
#mapping td.add {
|
||||
width: 300px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#mapping td.foot {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#mapping input {
|
||||
width: 130px;
|
||||
margin: 0;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#mapping input.search {
|
||||
width: 185px;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
if (isset($_POST['save-state'])) {
|
||||
$output = shell_exec('/usr/bin/supervisorctl -c /etc/supervisor/logstash/supervisord.conf restart logstash');
|
||||
echo "<pre>$output</pre>";
|
||||
}
|
||||
?>
|
||||
8
Vagrant/resources/malcolm/name-map-ui/site/upload.html
Normal file
8
Vagrant/resources/malcolm/name-map-ui/site/upload.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<html>
|
||||
<body>
|
||||
<form enctype="multipart/form-data" action="upload.php" method="post">
|
||||
Choose a file to upload: <input name="upfile" type="file" />
|
||||
<input type="submit" value="Upload" />
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
57
Vagrant/resources/malcolm/name-map-ui/site/upload.php
Normal file
57
Vagrant/resources/malcolm/name-map-ui/site/upload.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
|
||||
try {
|
||||
|
||||
// invalid multiple files / $_FILES corruption attack
|
||||
if (!isset($_FILES['upfile']['error']) ||
|
||||
is_array($_FILES['upfile']['error'])) {
|
||||
throw new RuntimeException('Invalid parameters');
|
||||
}
|
||||
|
||||
// validate $_FILES['upfile']['error']
|
||||
switch ($_FILES['upfile']['error']) {
|
||||
case UPLOAD_ERR_OK:
|
||||
break;
|
||||
case UPLOAD_ERR_NO_FILE:
|
||||
throw new RuntimeException('No file sent');
|
||||
case UPLOAD_ERR_INI_SIZE:
|
||||
case UPLOAD_ERR_FORM_SIZE:
|
||||
throw new RuntimeException('Exceeded filesize limit');
|
||||
default:
|
||||
throw new RuntimeException('Unknown error');
|
||||
}
|
||||
|
||||
// maximum upload filesize
|
||||
if ($_FILES['upfile']['size'] > 67108864) {
|
||||
throw new RuntimeException('Exceeded filesize limit');
|
||||
}
|
||||
|
||||
// check upload MIME type
|
||||
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||||
$fmime = $finfo->file($_FILES['upfile']['tmp_name']);
|
||||
if (false === $ext = array_search($fmime,
|
||||
array('json' => 'application/json',
|
||||
'txt' => 'text/plain'),
|
||||
true)) {
|
||||
throw new RuntimeException(sprintf('Invalid file format: "%s"', $fmime));
|
||||
}
|
||||
|
||||
// give file unique name based on sha
|
||||
$ftmpname = $_FILES['upfile']['tmp_name'];
|
||||
$fdstname = sprintf('./upload/%s.%s',
|
||||
sha1_file($_FILES['upfile']['tmp_name']),
|
||||
$ext);
|
||||
if (!move_uploaded_file($ftmpname, $fdstname)) {
|
||||
throw new RuntimeException(sprintf('Failed to move uploaded file ("%s" -> "%s")', $ftmpname, $fdstname));
|
||||
}
|
||||
|
||||
echo 'Success';
|
||||
|
||||
} catch (RuntimeException $e) {
|
||||
error_log ($e->getMessage());
|
||||
echo $e->getMessage();
|
||||
}
|
||||
|
||||
?>
|
||||
Reference in New Issue
Block a user