331 lines
14 KiB
C++
331 lines
14 KiB
C++
/**
|
|
* @file main.cpp
|
|
* @author Lewis Price (lewis.e.price@outlook.com)
|
|
* @brief The main run file for LumberJack TUI
|
|
* @version 1.0.0
|
|
* @date 2026-06-09
|
|
*
|
|
* @copyright Copyright (c) 2026
|
|
*
|
|
*/
|
|
|
|
#include <vector>
|
|
#include <string>
|
|
#include <algorithm>
|
|
#include <array>
|
|
|
|
#include <ftxui/component/component.hpp>
|
|
#include <ftxui/component/screen_interactive.hpp>
|
|
#include <ftxui/dom/elements.hpp>
|
|
|
|
#include "log_parsing/log_parsing.hpp"
|
|
#include "env_reader/env.hpp"
|
|
|
|
int main() {
|
|
using namespace ftxui;
|
|
|
|
// ── 1. App State ─────────────────────────────────────────────────────────
|
|
bool setup_complete = false;
|
|
std::string log_file_path = "";
|
|
std::string mmdb_path = "ip_data/GeoLite2-City.mmdb";
|
|
std::string search_query = "";
|
|
|
|
if (check_for_env()) {
|
|
load_env_file(".env");
|
|
|
|
// Safely extract environment strings
|
|
char* env_log = std::getenv("LOG_PATH");
|
|
char* env_db = std::getenv("DB_PATH");
|
|
if (env_log) log_file_path = env_log;
|
|
if (env_db) mmdb_path = env_db;
|
|
}
|
|
|
|
std::vector<Entry> active_logs;
|
|
int selected_row = 0;
|
|
|
|
// Order: IP | GEOLOCATION | STATUS | USER-AGENT
|
|
std::array<int, 4> col_widths = {20, 35, 8, 26};
|
|
constexpr int COL_MIN_WIDTH = 6;
|
|
constexpr int COL_MAX_WIDTH = 60;
|
|
|
|
// Which column is currently targeted by Shift+Arrow resize.
|
|
// -1 means none highlighted.
|
|
int resize_col = 0;
|
|
|
|
// ── 2. Filtering helper ───────────────────────────────────────────────────
|
|
// Returns the filtered subset and resets selected_row if out-of-bounds.
|
|
auto get_filtered = [&]() -> std::vector<Entry> {
|
|
std::vector<Entry> out;
|
|
for (const auto& log : active_logs) {
|
|
std::string geo = log.location.city + ", " + log.location.country;
|
|
if (search_query.empty() ||
|
|
log.ip.find(search_query) != std::string::npos ||
|
|
log.status.find(search_query) != std::string::npos ||
|
|
log.request.find(search_query) != std::string::npos ||
|
|
geo.find(search_query) != std::string::npos) {
|
|
out.push_back(log);
|
|
}
|
|
}
|
|
if (!out.empty() && selected_row >= (int)out.size())
|
|
selected_row = (int)out.size() - 1;
|
|
return out;
|
|
};
|
|
|
|
// ── 3. Log parsing lambda ─────────────────────────────────────────────────
|
|
auto run_log_parsing = [&]() {
|
|
setup_complete = true;
|
|
|
|
//.env file check
|
|
if (!check_for_env()) {
|
|
// Update process environment variables first so make_env() saves them correctly
|
|
#if defined(_WIN32)
|
|
_putenv_s("LOG_PATH", log_file_path.c_str());
|
|
_putenv_s("DB_PATH", mmdb_path.c_str());
|
|
#else
|
|
setenv("LOG_PATH", log_file_path.c_str(), 1);
|
|
setenv("DB_PATH", mmdb_path.c_str(), 1);
|
|
#endif
|
|
make_env();
|
|
}
|
|
|
|
p_logs parsed_logs(log_file_path);
|
|
active_logs = parsed_logs.get_all_logs();
|
|
|
|
if (active_logs.empty()) {
|
|
loc_data mock_loc{"United States", "North Carolina", "Charlotte", "35.227", "-80.843"};
|
|
active_logs.push_back({
|
|
"192.168.1.50", mock_loc, "2026-06-09T12:00:00",
|
|
"GET /api/v1/status HTTP/1.1", "200", "512",
|
|
"https://github.com", "Linux", "Firefox"
|
|
});
|
|
active_logs.push_back({
|
|
"8.8.8.8", {"United States", "California", "Mountain View", "37.386", "-122.083"},
|
|
"2026-06-09T12:01:15",
|
|
"POST /login HTTP/1.1", "401", "1024",
|
|
"direct", "Android", "Chrome"
|
|
});
|
|
for (int i = 1; i <= 40; ++i) {
|
|
active_logs.push_back({
|
|
"10.0.0." + std::to_string(i),
|
|
{"Canada", "Ontario", "Toronto", "43.653", "-79.383"},
|
|
"2026-06-09T12:05:00",
|
|
"GET /static/style.css HTTP/1.1", "200", "2048",
|
|
"https://github.com", "macOS", "Safari"
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
//Checks for log file and mmdb path
|
|
if (!log_file_path.empty() && !mmdb_path.empty()) {
|
|
run_log_parsing();
|
|
}
|
|
|
|
// ── 4. Setup-screen components ────────────────────────────────────────────
|
|
Component input_log = Input(&log_file_path, "Enter path to log file...");
|
|
Component input_mmdb = Input(&mmdb_path, "Enter path to MaxMind MMDB...");
|
|
Component submit_button = Button(" Start Log Monitor ", run_log_parsing, ButtonOption::Animated());
|
|
|
|
auto setup_container = Container::Vertical({input_log, input_mmdb, submit_button});
|
|
|
|
// ── 5. Search input (dashboard) ───────────────────────────────────────────
|
|
Component search_input = Input(&search_query, "Type to filter records (IP, request, status, etc.)...");
|
|
|
|
// ── 6. Main renderer ──────────────────────────────────────────────────────
|
|
auto main_components = Container::Vertical({
|
|
setup_container,
|
|
search_input
|
|
});
|
|
|
|
auto main_renderer = Renderer(main_components, [&]() {
|
|
if (!setup_complete) {
|
|
// ── VIEW 1: Configuration ─────────────────────────────────────────
|
|
return vbox({
|
|
text(" ParseLogCLI Configuration ") | bold | color(Color::Blue) | border,
|
|
separator(),
|
|
vbox({
|
|
hbox({ text(" Log File Path: ") | bold | color(Color::Cyan), input_log->Render() }),
|
|
vbox() | size(HEIGHT, EQUAL, 1),
|
|
hbox({ text(" MaxMind DB Path: ") | bold | color(Color::Cyan), input_mmdb->Render() }),
|
|
}) | borderLight,
|
|
separator(),
|
|
center(submit_button->Render()),
|
|
});
|
|
}
|
|
|
|
// ── VIEW 2: Dashboard ─────────────────────────────────────────────────
|
|
auto filtered_logs = get_filtered();
|
|
std::vector<std::vector<Element>> grid_matrix;
|
|
|
|
// Column headers
|
|
auto header_cell = [&](int col_idx, const std::string& label) -> Element {
|
|
bool active = (col_idx == resize_col);
|
|
std::string display = active ? label + " ↔" : label;
|
|
Element e = text(display)
|
|
| bold
|
|
| color(active ? Color::Yellow : Color::Cyan)
|
|
| size(WIDTH, EQUAL, col_widths[col_idx]);
|
|
if (active) e = e | underlined;
|
|
return e;
|
|
};
|
|
|
|
grid_matrix.push_back({
|
|
header_cell(0, " IP ADDRESS"),
|
|
header_cell(1, "GEOLOCATION"),
|
|
header_cell(2, "STATUS"),
|
|
header_cell(3, "CLIENT USER-AGENT"),
|
|
text("HTTP REQUEST") | bold | color(Color::Cyan) | flex,
|
|
});
|
|
|
|
// Data rows
|
|
for (int i = 0; i < (int)filtered_logs.size(); ++i) {
|
|
const auto& log = filtered_logs[i];
|
|
std::string geo = log.location.city.empty()
|
|
? log.location.country
|
|
: log.location.city + ", " + log.location.country;
|
|
|
|
bool sel = (i == selected_row);
|
|
|
|
Color status_color = Color::Red;
|
|
if (!log.status.empty()) {
|
|
char first = log.status[0];
|
|
if (first == '2') status_color = Color::Green;
|
|
else if (first == '3') status_color = Color::GrayLight;
|
|
}
|
|
|
|
auto ip_cell = text(" " + log.ip) | size(WIDTH, EQUAL, col_widths[0]);
|
|
auto geo_cell = text(geo) | size(WIDTH, EQUAL, col_widths[1]);
|
|
auto status_cell = text(log.status) | size(WIDTH, EQUAL, col_widths[2])
|
|
| color(sel ? Color::Cyan : status_color);
|
|
auto agent_cell = text(log.browser + " (" + log.os + ")") | size(WIDTH, EQUAL, col_widths[3]);
|
|
auto request_cell = text(log.request) | flex;
|
|
|
|
if (sel) {
|
|
ip_cell = ip_cell | inverted | color(Color::Cyan) | focus;
|
|
geo_cell = geo_cell | inverted | color(Color::Cyan);
|
|
status_cell = status_cell | inverted | color(Color::Cyan);
|
|
agent_cell = agent_cell | inverted | color(Color::Cyan);
|
|
request_cell = request_cell | inverted | color(Color::Cyan);
|
|
}
|
|
|
|
grid_matrix.push_back({ip_cell, geo_cell, status_cell, agent_cell, request_cell});
|
|
}
|
|
|
|
if (filtered_logs.empty()) {
|
|
grid_matrix.push_back({
|
|
text(" No records matching filter criteria found. ") | dim
|
|
});
|
|
}
|
|
|
|
auto table_grid = gridbox(std::move(grid_matrix));
|
|
auto box_layout = vbox({ table_grid }) | vscroll_indicator | frame | flex;
|
|
|
|
std::string resize_hint =
|
|
" Resize columns: Shift+← / Shift+→ "
|
|
"│ Switch column: ← / → "
|
|
"│ Scroll: ↑↓ or wheel "
|
|
"│ q / esc = quit ";
|
|
|
|
return vbox({
|
|
text(" ParseLogCLI Live Monitor ") | bold | color(Color::Blue) | border,
|
|
|
|
vbox({
|
|
hbox({ text(" DATA SOURCE: ") | bold | color(Color::Green), text(log_file_path) }),
|
|
hbox({ text(" DATABASE: ") | bold | color(Color::Green), text(mmdb_path) }),
|
|
}) | borderLight,
|
|
|
|
hbox({
|
|
text(" FILTER: ") | bold | color(Color::Yellow),
|
|
search_input->Render() | flex,
|
|
}) | borderLight,
|
|
|
|
text(resize_hint) | dim,
|
|
separator(),
|
|
box_layout | flex,
|
|
});
|
|
});
|
|
|
|
// ── 7. Event loop ─────────────────────────────────────────────────────────
|
|
auto screen = ScreenInteractive::Fullscreen();
|
|
|
|
auto global_events = CatchEvent(main_renderer, [&](Event event) {
|
|
if (event == Event::Character('q') || event == Event::Escape) {
|
|
screen.Exit();
|
|
return true;
|
|
}
|
|
|
|
if (event == Event::Return && !setup_complete) {
|
|
run_log_parsing();
|
|
return true;
|
|
}
|
|
|
|
if (!setup_complete) return false;
|
|
|
|
// Mouse wheel scrolling
|
|
if (event.is_mouse()) {
|
|
auto& m = event.mouse();
|
|
auto filtered_size = [&]() {
|
|
int n = 0;
|
|
for (const auto& log : active_logs) {
|
|
std::string geo = log.location.city + ", " + log.location.country;
|
|
if (search_query.empty() ||
|
|
log.ip.find(search_query) != std::string::npos ||
|
|
log.status.find(search_query) != std::string::npos ||
|
|
log.request.find(search_query) != std::string::npos ||
|
|
geo.find(search_query) != std::string::npos)
|
|
++n;
|
|
}
|
|
return n;
|
|
};
|
|
|
|
if (m.button == Mouse::WheelDown) {
|
|
int sz = filtered_size();
|
|
if (sz > 0) selected_row = std::min(selected_row + 3, sz - 1);
|
|
return true;
|
|
}
|
|
if (m.button == Mouse::WheelUp) {
|
|
selected_row = std::max(selected_row - 3, 0);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Keyboard row navigation
|
|
if (!active_logs.empty()) {
|
|
if (event == Event::ArrowDown) {
|
|
auto fl = get_filtered();
|
|
if (!fl.empty()) selected_row = std::min(selected_row + 1, (int)fl.size() - 1);
|
|
return true;
|
|
}
|
|
if (event == Event::ArrowUp) {
|
|
selected_row = std::max(selected_row - 1, 0);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Column resizing
|
|
if (event == Event::Special("\033[1;2C")) {
|
|
col_widths[resize_col] = std::min(col_widths[resize_col] + 2, COL_MAX_WIDTH);
|
|
return true;
|
|
}
|
|
if (event == Event::Special("\033[1;2D")) {
|
|
col_widths[resize_col] = std::max(col_widths[resize_col] - 2, COL_MIN_WIDTH);
|
|
return true;
|
|
}
|
|
|
|
// Target columns selection
|
|
if (event == Event::Special("\033[1;3C") || event == Event::ArrowRight) {
|
|
resize_col = (resize_col + 1) % (int)col_widths.size();
|
|
return true;
|
|
}
|
|
if (event == Event::Special("\033[1;3D") || event == Event::ArrowLeft) {
|
|
resize_col = (resize_col - 1 + (int)col_widths.size()) % (int)col_widths.size();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
screen.Loop(global_events);
|
|
return 0;
|
|
} |