mirror of
https://github.com/dobin/RedEdr
synced 2026-08-09 12:30:51 +00:00
360 lines
12 KiB
C++
360 lines
12 KiB
C++
#include <iostream>
|
|
#include <vector>
|
|
#include <windows.h>
|
|
|
|
#include "httplib.h" // Needs to be on top?
|
|
|
|
#include "event_aggregator.h"
|
|
#include "config.h"
|
|
#include "logging.h"
|
|
#include "utils.h"
|
|
#include "json.hpp"
|
|
#include "webserver.h"
|
|
#include "process_resolver.h"
|
|
#include "manager.h"
|
|
#include "event_processor.h"
|
|
#include "etwreader.h"
|
|
|
|
#pragma comment(lib, "wtsapi32.lib")
|
|
#pragma comment(lib, "userenv.lib")
|
|
|
|
|
|
/* Webserver.cpp: Webserver with REST api to the whole thing
|
|
* UI to interact with rededr
|
|
* REST interface (e.g. for RedEdrUi)
|
|
*/
|
|
|
|
|
|
using json = nlohmann::json;
|
|
|
|
|
|
HANDLE webserver_thread;
|
|
httplib::Server svr;
|
|
int webserver_port;
|
|
|
|
bool in_use = false;
|
|
|
|
|
|
std::wstring StripToFirstDot(const std::wstring& input) {
|
|
size_t dot_position = input.find(L'.');
|
|
if (dot_position != std::wstring::npos) {
|
|
return input.substr(0, dot_position);
|
|
}
|
|
return input;
|
|
}
|
|
|
|
|
|
std::vector<std::wstring> GetFilesInDirectory(const std::wstring& directory) {
|
|
std::vector<std::wstring> files;
|
|
WIN32_FIND_DATA findFileData;
|
|
HANDLE hFind = FindFirstFile(directory.c_str(), &findFileData);
|
|
|
|
if (hFind == INVALID_HANDLE_VALUE) {
|
|
// Can be empty, ignore
|
|
if (g_Config.debug) {
|
|
LOG_W(LOG_INFO, L"No files found in %s", directory.c_str());
|
|
}
|
|
return files;
|
|
}
|
|
|
|
do {
|
|
const std::wstring fileOrDir = findFileData.cFileName;
|
|
if (fileOrDir != L"." && fileOrDir != L"..") {
|
|
files.push_back(StripToFirstDot(fileOrDir));
|
|
}
|
|
} while (FindNextFile(hFind, &findFileData) != 0);
|
|
|
|
FindClose(hFind);
|
|
return files;
|
|
}
|
|
|
|
|
|
std::string getRecordingsAsJson() {
|
|
try {
|
|
std::stringstream output;
|
|
output << "[";
|
|
std::vector<std::wstring> names = GetFilesInDirectory(L"C:\\RedEdr\\Data\\*.events.json");
|
|
for (auto it = names.begin(); it != names.end(); ++it) {
|
|
std::wstring name = *it; // Create a proper lvalue for wstring2string
|
|
output << "\"" << wstring2string(name) << ".events.json" << "\"";
|
|
if (std::next(it) != names.end()) {
|
|
output << ","; // Add comma only if it's not the last element
|
|
}
|
|
}
|
|
output << "]";
|
|
return output.str();
|
|
} catch (const std::exception& e) {
|
|
LOG_A(LOG_ERROR, "Error in getRecordingsAsJson: %s", e.what());
|
|
return "[]"; // Return empty array on error
|
|
}
|
|
}
|
|
|
|
|
|
std::vector<std::string> GetPplLogs() {
|
|
std::vector<std::string> logs;
|
|
std::ifstream file("C:\\RedEdr\\pplservice.log");
|
|
if (!file.is_open()) {
|
|
return logs;
|
|
}
|
|
std::string line;
|
|
while (std::getline(file, line)) {
|
|
if (!line.empty()) {
|
|
logs.push_back(line);
|
|
}
|
|
}
|
|
file.close();
|
|
return logs;
|
|
}
|
|
|
|
bool HasAllowedExtension(const std::string& filename, const std::vector<std::string>& extensions) {
|
|
for (const auto& ext : extensions) {
|
|
if (filename.length() >= ext.length() &&
|
|
filename.compare(filename.length() - ext.length(), ext.length(), ext) == 0) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
DWORD WINAPI WebserverThread(LPVOID param) {
|
|
// The UI
|
|
svr.Get("/", [](const httplib::Request&, httplib::Response& res) {
|
|
try {
|
|
std::string indexhtml = read_file("C:\\RedEdr\\index.html");
|
|
if (indexhtml.empty()) {
|
|
res.status = 404;
|
|
res.set_content("File not found", "text/plain");
|
|
return;
|
|
}
|
|
res.set_content(indexhtml, "text/html");
|
|
} catch (const std::exception& e) {
|
|
LOG_A(LOG_ERROR, "Error serving index.html: %s", e.what());
|
|
res.status = 500;
|
|
res.set_content("Internal server error", "text/plain");
|
|
}
|
|
});
|
|
svr.Get("/static/design.css", [](const httplib::Request&, httplib::Response& res) {
|
|
try {
|
|
std::string indexhtml = read_file("C:\\RedEdr\\design.css");
|
|
if (indexhtml.empty()) {
|
|
res.status = 404;
|
|
res.set_content("File not found", "text/plain");
|
|
return;
|
|
}
|
|
res.set_content(indexhtml, "text/css");
|
|
} catch (const std::exception& e) {
|
|
LOG_A(LOG_ERROR, "Error serving design.css: %s", e.what());
|
|
res.status = 500;
|
|
res.set_content("Internal server error", "text/plain");
|
|
}
|
|
});
|
|
svr.Get("/static/shared.js", [](const httplib::Request&, httplib::Response& res) {
|
|
try {
|
|
std::string indexhtml = read_file("C:\\RedEdr\\shared.js");
|
|
if (indexhtml.empty()) {
|
|
res.status = 404;
|
|
res.set_content("File not found", "text/plain");
|
|
return;
|
|
}
|
|
res.set_content(indexhtml, "text/javascript");
|
|
} catch (const std::exception& e) {
|
|
LOG_A(LOG_ERROR, "Error serving shared.js: %s", e.what());
|
|
res.status = 500;
|
|
res.set_content("Internal server error", "text/plain");
|
|
}
|
|
});
|
|
svr.Get("/api/save", [](const httplib::Request&, httplib::Response& res) {
|
|
g_EventProcessor.SaveToFile();
|
|
});
|
|
svr.Get("/api/stats", [](const httplib::Request&, httplib::Response& res) {
|
|
nlohmann::json stats = {
|
|
{"events_count", g_EventAggregator.GetCount()},
|
|
{"num_kernel", g_EventProcessor.num_kernel},
|
|
{"num_etw", g_EventProcessor.num_etw},
|
|
{"num_etwti", g_EventProcessor.num_etwti},
|
|
{"num_dll", g_EventProcessor.num_dll},
|
|
{"num_process_cache", g_ProcessResolver.GetCacheCount()}
|
|
};
|
|
res.set_content(stats.dump(), "application/json; charset=UTF-8");
|
|
});
|
|
|
|
// Provide Logs
|
|
svr.Get("/api/logs/rededr", [](const httplib::Request&, httplib::Response& res) {
|
|
// Arry of Dicts
|
|
// Like:
|
|
/*
|
|
[
|
|
{ "date":"2025-07-20-10-36-24",
|
|
"do_etw":false,
|
|
"do_etwti":false,
|
|
"do_hook":false,
|
|
"do_hook_callstack": true,
|
|
"func":"init",
|
|
"target":"otepad",
|
|
"trace_id":41,
|
|
"type":"meta",
|
|
"version":"0.4"
|
|
},
|
|
...
|
|
]
|
|
*/
|
|
try {
|
|
res.set_content(g_EventProcessor.GetAllAsJson(), "application/json");
|
|
} catch (const std::exception& e) {
|
|
LOG_A(LOG_ERROR, "Error getting events: %s", e.what());
|
|
res.status = 500;
|
|
json error_response = {
|
|
{ "status", "error" },
|
|
{ "message", "Internal server error" }
|
|
};
|
|
res.set_content(error_response.dump(), "application/json");
|
|
}
|
|
});
|
|
svr.Get("/api/logs/agent", [](const httplib::Request& req, httplib::Response& res) {
|
|
// Array of Strings
|
|
// Like.
|
|
/*
|
|
[
|
|
"RedEdr 0.4",
|
|
"Config: tracing otepad",
|
|
"Permissions: Enabled PRIVILEGED & DEBUG",
|
|
]
|
|
*/
|
|
std::vector agentLogs = GetAgentLogs(); // List of srings
|
|
std::vector pplLogs = GetPplLogs();
|
|
|
|
// return both logs in a single array
|
|
json response = json::array();
|
|
for (const auto& log : agentLogs) {
|
|
response.push_back(log);
|
|
}
|
|
for (const auto& log : pplLogs) {
|
|
response.push_back(log);
|
|
}
|
|
res.set_content(response.dump(), "application/json");
|
|
});
|
|
|
|
// Functions
|
|
svr.Get("/api/trace/info", [](const httplib::Request& req, httplib::Response& res) {
|
|
json response = { {"trace", g_Config.targetProcessNames } };
|
|
res.set_content(response.dump(), "application/json");
|
|
});
|
|
svr.Post("/api/trace/start", [](const httplib::Request& req, httplib::Response& res) {
|
|
try {
|
|
auto data = json::parse(req.body);
|
|
if (data.contains("trace")) {
|
|
if (! data["trace"].is_array()) {
|
|
LOG_A(LOG_ERROR, "Trace start: Targets should be an array, but is %s", data["trace"]);
|
|
json error_response = { {"error", "trace should be an array"} };
|
|
res.status = 400;
|
|
res.set_content(error_response.dump(), "application/json");
|
|
}
|
|
std::vector<std::string> traceNames = data["trace"].get<std::vector<std::string>>();
|
|
LOG_A(LOG_INFO, "Trace targets: %zu targets", traceNames.size());
|
|
for (const auto& target : traceNames) {
|
|
LOG_A(LOG_INFO, " - %s", target.c_str());
|
|
}
|
|
g_Config.targetProcessNames = traceNames;
|
|
ManagerApplyNewTargets();
|
|
|
|
/*
|
|
std::string use_additional_etw;
|
|
if (req.has_file("use_additional_etw")) {
|
|
auto use_additional_etw_field = req.get_file_value("use_additional_etw");
|
|
use_additional_etw = use_additional_etw_field.content;
|
|
}
|
|
*/
|
|
trace_in_progress(true);
|
|
|
|
json response = { {"result", "ok"} };
|
|
res.set_content(response.dump(), "application/json");
|
|
}
|
|
else {
|
|
json error_response = { {"error", "No 'trace' key provided"} };
|
|
res.status = 400;
|
|
res.set_content(error_response.dump(), "application/json");
|
|
}
|
|
}
|
|
catch (const json::parse_error& e) {
|
|
json error_response = { {"error", "Invalid JSON data: " + std::string(e.what())} };
|
|
res.status = 400;
|
|
res.set_content(error_response.dump(), "application/json");
|
|
}
|
|
});
|
|
svr.Post("/api/trace/reset", [](const httplib::Request&, httplib::Response& res) {
|
|
trace_in_progress(false);
|
|
g_EventAggregator.ResetData();
|
|
g_EventProcessor.ResetData();
|
|
});
|
|
svr.Post("/api/trace/stop", [](const httplib::Request&, httplib::Response& res) {
|
|
trace_in_progress(false);
|
|
});
|
|
|
|
// Lock management endpoints
|
|
svr.Post("/api/lock/acquire", [](const httplib::Request&, httplib::Response& res) {
|
|
if (in_use) {
|
|
res.status = 409; // Conflict
|
|
json error_response = {
|
|
{ "status", "error" },
|
|
{ "message", "Resource is already in use" },
|
|
};
|
|
res.set_content(error_response.dump(), "application/json");
|
|
} else {
|
|
in_use = true;
|
|
// Returns 200 OK
|
|
}
|
|
});
|
|
svr.Post("/api/lock/release", [](const httplib::Request&, httplib::Response& res) {
|
|
if (! in_use) {
|
|
// We dont really care
|
|
LOG_A(LOG_INFO, "Release lock even tho it was not aquired");
|
|
}
|
|
in_use = false;
|
|
// Returns 200 OK
|
|
});
|
|
svr.Get("/api/lock/status", [](const httplib::Request&, httplib::Response& res) {
|
|
json response = {
|
|
{ "in_use", in_use }
|
|
};
|
|
res.set_content(response.dump(), "application/json");
|
|
});
|
|
|
|
|
|
LOG_A(LOG_INFO, "WEB: Web Server listening on http://0.0.0.0:%i", webserver_port);
|
|
|
|
bool listen_result = false;
|
|
try {
|
|
listen_result = svr.listen("0.0.0.0", webserver_port);
|
|
} catch (const std::exception& e) {
|
|
LOG_A(LOG_ERROR, "WEB: Server listen failed: %s", e.what());
|
|
} catch (...) {
|
|
LOG_A(LOG_ERROR, "WEB: Server listen failed with unknown exception");
|
|
}
|
|
|
|
if (!listen_result) {
|
|
LOG_A(LOG_INFO, "WEB: Server listen returned false (normal during shutdown)");
|
|
}
|
|
|
|
LOG_A(LOG_INFO, "!WEB: Thread finished");
|
|
return 0;
|
|
}
|
|
|
|
|
|
int InitializeWebServer(std::vector<HANDLE>& threads, int port) {
|
|
webserver_port = port;
|
|
webserver_thread = CreateThread(NULL, 0, WebserverThread, NULL, 0, NULL);
|
|
if (webserver_thread == NULL) {
|
|
LOG_A(LOG_ERROR, "WEB: Failed to create thread for webserver");
|
|
return 1;
|
|
}
|
|
LOG_A(LOG_INFO, "!Web: Started Thread (handle %p)", webserver_thread);
|
|
threads.push_back(webserver_thread);
|
|
return 0;
|
|
}
|
|
|
|
|
|
void StopWebServer() {
|
|
svr.stop();
|
|
}
|
|
|