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:
318
main.cpp
318
main.cpp
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user