1) First draft of the TUI functionality

2) Added env_reader functions (env.hpp and env.cpp)
3) Program looks for a .env on startup and creates one based on user input if not found.
4) Refactored log_parsing and ip_to_geo to use the global env variables for parsing and ip lookup from the local .mmdb database
5) CMakeLists.txt is now cross platform functional
6) Added various cross platform checks for creating .env variables
This commit is contained in:
rapturate
2026-06-09 14:29:42 -04:00
parent bdaf8451a0
commit 42f50c9e9a
9 changed files with 537 additions and 62 deletions

318
main.cpp
View File

@@ -1,8 +1,320 @@
#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(){
p_logs logs("test_logs/access.log.txt");
logs.print_logs();
int main() {
using namespace ftxui;
// ── 1. App State ─────────────────────────────────────────────────────────
bool setup_complete = false;
std::string log_file_path = "";
std::string mmdb_path = "";
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 = 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;
}