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:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -42,3 +42,8 @@ compile_commands.json
|
|||||||
# If you download large MaxMind .mmdb files, ignore them so they don't bloat Git
|
# If you download large MaxMind .mmdb files, ignore them so they don't bloat Git
|
||||||
*.mmdb
|
*.mmdb
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# Sensitive Files
|
||||||
|
# ==========================================
|
||||||
|
.env
|
||||||
|
|||||||
@@ -6,13 +6,15 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
|||||||
set(CMAKE_CXX_STANDARD 17)
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
|
include(FetchContent)
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 1. CORE LIBRARY TARGET
|
# 1. CORE LIBRARY TARGET
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# Compiles your core logic files into a shared library module
|
|
||||||
add_library(parselog_core
|
add_library(parselog_core
|
||||||
log_parsing/log_parsing.cpp
|
log_parsing/log_parsing.cpp
|
||||||
ip_to_geo/ip_to_geo.cpp
|
ip_to_geo/ip_to_geo.cpp
|
||||||
|
env_reader/env.cpp
|
||||||
third_party/src/GeoLite2PP.cpp
|
third_party/src/GeoLite2PP.cpp
|
||||||
third_party/src/GeoLite2PP_error_category.cpp
|
third_party/src/GeoLite2PP_error_category.cpp
|
||||||
)
|
)
|
||||||
@@ -24,10 +26,42 @@ target_include_directories(parselog_core PRIVATE
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/third_party/include
|
${CMAKE_CURRENT_SOURCE_DIR}/third_party/include
|
||||||
)
|
)
|
||||||
|
|
||||||
# Link the static third-party MaxMind binary
|
# CROSS-PLATFORM FIXED: Automatically links appropriate binary dependencies based on OS
|
||||||
target_link_libraries(parselog_core PRIVATE
|
if(WIN32)
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/third_party/lib/libmaxminddb.a
|
if(MSVC)
|
||||||
)
|
# Windows via Visual Studio Compiler
|
||||||
|
target_link_libraries(parselog_core PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/third_party/lib/maxminddb.lib
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
# Windows via MinGW/GCC Toolchain
|
||||||
|
target_link_libraries(parselog_core PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/third_party/lib/libmaxminddb.a
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Windows requires Windows Sockets API linked for libmaxminddb network resolutions
|
||||||
|
target_link_libraries(parselog_core PRIVATE ws2_32)
|
||||||
|
|
||||||
|
elseif(APPLE)
|
||||||
|
# macOS Specific Path Integrations (Handles Intel /opt/local and Apple Silicon /opt/homebrew)
|
||||||
|
find_library(MAXMIND_LIB maxminddb HINTS /opt/homebrew/lib /usr/local/lib /opt/local/lib)
|
||||||
|
|
||||||
|
if(MAXMIND_LIB)
|
||||||
|
target_link_libraries(parselog_core PRIVATE ${MAXMIND_LIB})
|
||||||
|
else()
|
||||||
|
# Fallback to local static file repository boundary if brew package is missing
|
||||||
|
target_link_libraries(parselog_core PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/third_party/lib/libmaxminddb.a
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
else()
|
||||||
|
# Standard Linux (Ubuntu, Arch Linux, Fedora, etc.)
|
||||||
|
target_link_libraries(parselog_core PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/third_party/lib/libmaxminddb.a
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
# Version definitions needed by your GeoLite2PP wrapper
|
# Version definitions needed by your GeoLite2PP wrapper
|
||||||
target_compile_definitions(parselog_core PRIVATE
|
target_compile_definitions(parselog_core PRIVATE
|
||||||
@@ -35,24 +69,36 @@ target_compile_definitions(parselog_core PRIVATE
|
|||||||
)
|
)
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 2. APPLICATION EXECUTABLE
|
# 2. FTXUI Library Download
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# Compiles your main user-facing CLI binary
|
FetchContent_Declare(
|
||||||
add_executable(parselog_cli main.cpp)
|
ftxui
|
||||||
target_link_libraries(parselog_cli PRIVATE parselog_core)
|
GIT_REPOSITORY https://github.com/ArthurSonzogni/FTXUI.git
|
||||||
|
GIT_TAG v6.1.9
|
||||||
|
)
|
||||||
|
FetchContent_MakeAvailable(ftxui)
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# 3. APPLICATION EXECUTABLE
|
||||||
|
# ==========================================
|
||||||
|
add_executable(parselog_cli main.cpp)
|
||||||
|
|
||||||
|
target_link_libraries(parselog_cli
|
||||||
|
PRIVATE
|
||||||
|
parselog_core
|
||||||
|
ftxui::screen
|
||||||
|
ftxui::dom
|
||||||
|
ftxui::component
|
||||||
|
)
|
||||||
|
|
||||||
|
# CROSS-PLATFORM FIXED: Output targets route uniformly into the root project space
|
||||||
set_target_properties(parselog_cli PROPERTIES
|
set_target_properties(parselog_cli PROPERTIES
|
||||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
|
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||||
|
RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||||
|
RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 3. TEST SUITE
|
# 4. TEST SUITE
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# Activates the built-in CTest engine
|
|
||||||
enable_testing()
|
enable_testing()
|
||||||
|
|
||||||
# add_executable(parselog_test tests.cpp)
|
|
||||||
# target_link_libraries(parselog_test PRIVATE parselog_core)
|
|
||||||
|
|
||||||
# Registers the testing executable under the "RunAllTests" banner
|
|
||||||
# add_test(NAME RunAllTests COMMAND parselog_test)
|
|
||||||
|
|||||||
10
README.md
Normal file
10
README.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
This TUI program is for taking in an Apache2 access log file and providing an interactive interface to check your logs.
|
||||||
|
|
||||||
|
REQUIREMENTS:
|
||||||
|
- Access to a GeoLite2-City.mmdb database file.
|
||||||
|
* The signup page is provided here:
|
||||||
|
https://dev.maxmind.com/geoip/geolite2-free-geolocation-data/
|
||||||
|
|
||||||
|
NOTES:
|
||||||
|
- The TUI will ask for the access.log file location and the GeoLite2-City.mmdb file location on startup.
|
||||||
|
* I have not tried using the GeoLite2-Country.mmdb file and do not know if it will crash the application...
|
||||||
83
env_reader/env.cpp
Normal file
83
env_reader/env.cpp
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* @file env.cpp
|
||||||
|
* @author Lewis Price (lewis.e.price@outlook.com)
|
||||||
|
* @brief Implementation for environment utilities and cross-platform .env loader
|
||||||
|
* @version 1.0.0
|
||||||
|
* @date 2026-06-09
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2026
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#include "env.hpp"
|
||||||
|
#include <iostream>
|
||||||
|
#include <fstream>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Helper utility to set environment variables across operating systems
|
||||||
|
*/
|
||||||
|
static int set_env_variable(const std::string& key, const std::string& value) {
|
||||||
|
#if defined(_WIN32)
|
||||||
|
return _putenv_s(key.c_str(), value.c_str());
|
||||||
|
#else
|
||||||
|
return setenv(key.c_str(), value.c_str(), 1);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool check_for_env() {
|
||||||
|
const std::string env_path = ".env";
|
||||||
|
// Returns true only if .env exists on disk and is a file (not a directory)
|
||||||
|
return std::filesystem::exists(env_path) && std::filesystem::is_regular_file(env_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void make_env() {
|
||||||
|
const std::string env_path = ".env";
|
||||||
|
if (std::filesystem::exists(env_path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ofstream file(env_path);
|
||||||
|
if (!file.is_open()) {
|
||||||
|
std::cerr << "Error: Could not generate a default " << env_path << " configuration template file." << std::endl;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write out the clean template configuration block
|
||||||
|
file << "# ParseLogCLI Global Environment Configuration\n";
|
||||||
|
file << "# Generated on 2026-06-09\n\n";
|
||||||
|
file << "# Path to the MaxMind GeoLite2 City Database binary\n";
|
||||||
|
file << "DB_PATH=data/GeoLite2-City.mmdb\n\n";
|
||||||
|
file << "# Absolute or relative path to the server access logs target file\n";
|
||||||
|
file << "LOG_PATH=test_logs/access.log.txt\n";
|
||||||
|
|
||||||
|
file.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void load_env_file(const std::string& env_path) {
|
||||||
|
std::ifstream file(env_path);
|
||||||
|
if (!file.is_open()) {
|
||||||
|
std::cerr << "Warning: Could not open " << env_path << " file." << std::endl;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(file, line)) {
|
||||||
|
// Skip completely empty lines or comment configurations
|
||||||
|
if (line.empty() || line[0] == '#') continue;
|
||||||
|
|
||||||
|
std::size_t delimiter_pos = line.find('=');
|
||||||
|
if (delimiter_pos != std::string::npos) {
|
||||||
|
std::string key = line.substr(0, delimiter_pos);
|
||||||
|
std::string value = line.substr(delimiter_pos + 1);
|
||||||
|
|
||||||
|
// Clean up hidden trailing Windows carriage returns (\r) safely
|
||||||
|
if (!value.empty() && value.back() == '\r') {
|
||||||
|
value.pop_back();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose the key-value pair globally to the active process lifecycle
|
||||||
|
set_env_variable(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
env_reader/env.hpp
Normal file
37
env_reader/env.hpp
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* @file env.hpp
|
||||||
|
* @author Lewis Price (lewis.e.price@outlook.com)
|
||||||
|
* @brief
|
||||||
|
* @version 1.0.0
|
||||||
|
* @date 2026-06-09
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2026
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Checks for an already existing .env file.
|
||||||
|
*
|
||||||
|
* @return true
|
||||||
|
* @return false
|
||||||
|
*/
|
||||||
|
bool check_for_env();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Creates a new .env file for the user.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
void make_env();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Loads the .env file information
|
||||||
|
*
|
||||||
|
* @param env_path
|
||||||
|
*/
|
||||||
|
void load_env_file(const std::string& env_path);
|
||||||
|
|
||||||
@@ -1,43 +1,16 @@
|
|||||||
#include "ip_to_geo.hpp"
|
|
||||||
#include <exception>
|
#include <exception>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <fstream>
|
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
|
||||||
|
#include "ip_to_geo.hpp"
|
||||||
#include "../third_party/include/GeoLite2PP.hpp"
|
#include "../third_party/include/GeoLite2PP.hpp"
|
||||||
|
|
||||||
void load_env_file(const std::string& env_path) {
|
|
||||||
std::ifstream file(env_path);
|
|
||||||
if (!file.is_open()) {
|
|
||||||
std::cerr << "Warning: Could not open " << env_path << " file." << std::endl;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string line;
|
|
||||||
while (std::getline(file, line)) {
|
|
||||||
// Skip empty lines or comments
|
|
||||||
if (line.empty() || line[0] == '#') continue;
|
|
||||||
|
|
||||||
std::size_t delimiter_pos = line.find('=');
|
|
||||||
if (delimiter_pos != std::string::npos) {
|
|
||||||
std::string key = line.substr(0, delimiter_pos);
|
|
||||||
std::string value = line.substr(delimiter_pos + 1);
|
|
||||||
|
|
||||||
// Set the variable globally in the execution environment
|
|
||||||
setenv(key.c_str(), value.c_str(), 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loc_data iplookup(const std::string& ip) {
|
loc_data iplookup(const std::string& ip) {
|
||||||
|
|
||||||
// 1. Load variables from the local .env file
|
|
||||||
load_env_file();
|
|
||||||
|
|
||||||
// 2. Fetch the path out of the environment variables
|
|
||||||
const char* env_db_path = std::getenv("DB_PATH");
|
const char* env_db_path = std::getenv("DB_PATH");
|
||||||
// Fallback to a default path if the environment variable wasn't set
|
|
||||||
std::string db_path = (env_db_path != nullptr) ? env_db_path : "data/GeoLite2-City.mmdb";
|
std::string db_path = (env_db_path != nullptr) ? env_db_path : "data/GeoLite2-City.mmdb";
|
||||||
|
|
||||||
|
|
||||||
@@ -53,7 +26,7 @@ loc_data iplookup(const std::string& ip) {
|
|||||||
location.longitude = geo_data["longitude"];
|
location.longitude = geo_data["longitude"];
|
||||||
}
|
}
|
||||||
catch (const std::exception& e) {
|
catch (const std::exception& e) {
|
||||||
std::cerr << "Database failed to load: " << e.what() << std::endl;
|
//std::cerr << "Database failed to load: " << e.what() << std::endl;
|
||||||
}
|
}
|
||||||
return location;
|
return location;
|
||||||
};
|
};
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#include <cstdlib>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <ostream>
|
#include <ostream>
|
||||||
@@ -9,7 +10,9 @@
|
|||||||
#include "../ip_to_geo/ip_to_geo.hpp"
|
#include "../ip_to_geo/ip_to_geo.hpp"
|
||||||
|
|
||||||
p_logs::p_logs(std::string log_path) {
|
p_logs::p_logs(std::string log_path) {
|
||||||
|
if(log_path.empty()){
|
||||||
|
log_path = std::getenv("LOG_PATH");
|
||||||
|
}
|
||||||
std::ifstream file(log_path);
|
std::ifstream file(log_path);
|
||||||
if (!file.is_open()) {
|
if (!file.is_open()) {
|
||||||
std::cerr << "Error loading " << log_path << std::endl;
|
std::cerr << "Error loading " << log_path << std::endl;
|
||||||
@@ -59,6 +62,10 @@ p_logs::p_logs(std::string log_path) {
|
|||||||
file.close();
|
file.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<Entry> p_logs::get_all_logs(){
|
||||||
|
return logs;
|
||||||
|
}
|
||||||
|
|
||||||
std::string p_logs::entryx_ip(int x){
|
std::string p_logs::entryx_ip(int x){
|
||||||
return logs[x].ip;
|
return logs[x].ip;
|
||||||
}
|
}
|
||||||
@@ -96,7 +103,7 @@ void p_logs::print_logs() {
|
|||||||
std::ios_base::sync_with_stdio(false);
|
std::ios_base::sync_with_stdio(false);
|
||||||
for (const auto& log : logs) {
|
for (const auto& log : logs) {
|
||||||
std::cout << "IP: " << log.ip.c_str() << "\n"
|
std::cout << "IP: " << log.ip.c_str() << "\n"
|
||||||
<< "Location:" << "\n"
|
<< "Location Data:" << "\n"
|
||||||
<< "\tCountry: " << log.location.country << "\n"
|
<< "\tCountry: " << log.location.country << "\n"
|
||||||
<< "\tSubdivision: " << log.location.subdivision << "\n"
|
<< "\tSubdivision: " << log.location.subdivision << "\n"
|
||||||
<< "\tCity: " << log.location.city << "\n"
|
<< "\tCity: " << log.location.city << "\n"
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ public:
|
|||||||
*/
|
*/
|
||||||
p_logs(std::string);
|
p_logs(std::string);
|
||||||
|
|
||||||
|
std::vector<Entry> get_all_logs();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Getter function for a specific Entry's IP
|
* @brief Getter function for a specific Entry's IP
|
||||||
*
|
*
|
||||||
|
|||||||
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 "log_parsing/log_parsing.hpp"
|
||||||
|
#include "env_reader/env.hpp"
|
||||||
|
|
||||||
int main(){
|
int main() {
|
||||||
p_logs logs("test_logs/access.log.txt");
|
using namespace ftxui;
|
||||||
logs.print_logs();
|
|
||||||
|
|
||||||
|
// ── 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;
|
return 0;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user