Files
2021-08-06 10:35:01 +02:00

523 lines
19 KiB
HTML

<!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>