21 #include <nlohmann/json.hpp>
22 #include <fmt/format.h>
28 #include "version.hpp"
30 namespace fs = std::filesystem;
36 using defcat_t = std::vector<int32_t>;
37 using lookup_t = std::map<std::string, int32_t>;
42 using std::error_code;
45 using std::istreambuf_iterator;
49 using std::string_view;
50 using std::system_error;
58 using svec_t = vector<string>;
64 static bool parseArgs(int32_t argc,
char** argv);
84 {
"apache-auth", 21 },
85 {
"apache-batbots", 19 },
86 {
"apache-overflows", 21 },
87 {
"apache-nohome", 21 },
88 {
"apache-fakegooglebot", 19 },
89 {
"apache-modsecurity", 21 },
90 {
"apache-shellshock", 21 },
91 {
"php-url-fopen", 21 },
92 {
"roundcube-auth", 21 },
94 {
"sendmail-auth", 20 },
95 {
"sendmail-reject", -1 },
97 {
"mysqld-auth", 21 },
98 {
"pam-generic", 20 },
99 {
"postfix-flood-attack", 04 }
116 int main(int32_t argc,
char** argv) {
127 cerr <<
"Insufficent permissions! To execute fail2ban directly, elevated permissions are required." << endl;
131 cerr <<
"Searching for fail2ban..." << endl;
132 cerr <<
"!! WARNING !! Search may or may not be broken!" << endl;
134 cerr <<
"Failed to find fail2ban! Aborting." << endl;
139 cerr <<
"Failed to get output from fail2ban" << endl;
170 int32_t returnCode = 0;
171 g_fileToRead = fmt::format(
"{0:s}/{1:d}.f2b", fs::temp_directory_path().string(), time(
nullptr));
172 exec(fmt::format(R"({0:s} banned 2>/dev/null > {1:s})", g_fail2banExe, g_fileToRead), returnCode);
174 return returnCode == 0;
184 const static string PATH =
"PATH";
185 const static string FAIL2BAN_EXE =
"fail2ban-client";
187 if (getenv(PATH.c_str()) ==
nullptr) {
return false; }
191 auto pathCharPtr = getenv(PATH.c_str());
192 pathVar = string(pathCharPtr, strnlen(pathCharPtr, 4096));
195 for (
const auto path : StringSplit(pathVar,
":")) {
196 const auto iterator = fs::directory_iterator(path);
197 const auto iteratorEnd = fs::directory_iterator();
198 const auto iterPosition = std::find_if(iterator, iteratorEnd, [&](
const auto& file) {
199 return file.is_regular_file() && file.path().filename() == FAIL2BAN_EXE;
202 if (iterPosition != iteratorEnd) {
203 g_fail2banExe = iterPosition->path().string();
220 string fileContents{};
223 if (!fs::exists(g_fileToRead) || !fs::is_regular_file(g_fileToRead)) {
224 cerr <<
"File " <<
g_fileToRead <<
" cannot be read! Aborting..." << endl;
229 ifstream fStream(
g_fileToRead, ifstream::openmode::_S_in);
230 if (!fStream.good()) {
231 cerr <<
"Failed to open file " <<
g_fileToRead <<
". Aborting..." << endl;
235 fStream.unsetf(std::ios_base::skipws);
236 vector<
char> buffer((istreambuf_iterator<
char>(fStream)), (istreambuf_iterator<
char>()));
237 fileContents = string(buffer.begin(), buffer.end());
242 entries = json::parse(fileContents);
243 }
catch (
const exception& ex) {
244 cerr <<
"Failed to parse JSON! Invalid format?" << endl
245 <<
"Error description: " << ex.what() << endl;
250 rval = outputCsv(entries);
268 for (string line{}; std::getline(cin, line);) {
269 f2bOutput.append(line).append(
"\n");
272 if (f2bOutput.empty()) {
274 cerr <<
"Failed to read input from stdin!" << endl;
280 entries = json::parse(f2bOutput);
281 }
catch (
const exception& ex) {
282 cerr <<
"Failed to parse JSON! Invalid format?" << endl
283 <<
"Error description: " << ex.what() << endl;
288 rval = outputCsv(entries);
302 bool outputCsv(
const json& entries) {
305 const time_t timeNow = time(
nullptr);
306 struct tm tStruct{0};
307 localtime_r(&timeNow, &tStruct);
308 string timeString(256, 0);
309 strftime(&timeString[0], timeString.size(),
"%F %T%z", &tStruct);
310 timeString.shrink_to_fit();
312 vector<string> csvLines{
313 "IP,Categories,ReportDate,Comment"
317 if (!entries.is_array()) {
318 cerr <<
"Invalid input. Expected array, got " << entries.type_name() << endl;
324 const auto newLines = getLinesFromJson(entries, timeString);
325 csvLines.insert(csvLines.end(), newLines.begin(), newLines.end());
328 for (
const auto& line : csvLines) {
329 cout << line << endl;
344 svec_t getLinesFromJson(
const json& entries,
const string& timeString) {
345 vector<string> lines;
347 for (
const auto& entry : entries) {
352 if (!entry.is_object() && entry.is_string()) {
353 currentIp = entry.get<string>();
355 if (alreadyReported(currentIp)) {
continue; }
359 lines.push_back(format(
360 R"({0:s},"{1:s}",{2:s},"{3:s}")",
362 getCategoriesForJail(),
364 format(g_reportComment, g_jailName.empty() ?
"UNKNOWN" : g_jailName)
366 }
else if (entry.is_object()) {
367 const auto& obj = entry.get<json::object_t>();
368 g_jailName = obj.begin()->first;
369 const auto newLines = getLinesFromJson(*entry.begin(), timeString);
370 lines.insert(lines.end(), newLines.begin(), newLines.end());
385 for (size_t i = 0; i < g_defaultCategories.size(); i++) {
387 categories.push_back(
',');
390 categories.append(std::to_string(g_defaultCategories[i]));
393 const auto posInMap = std::find_if(g_categoryLookup.begin(), g_categoryLookup.end(), [](
const auto x) {
394 return x.first == g_jailName;
397 if (posInMap != g_categoryLookup.end() && posInMap->second != -1) {
398 categories.append(format(
",{0:d}", posInMap->second));
424 while ((optVal = getopt_long(argc, argv, getShortArgs().data(), getOptions(), &curIdx)) != -1) {
428 printHelpText(argv[0]);
436 if (optarg ==
nullptr) {
437 cerr <<
"Warning: will use default file " <<
g_fileToRead <<
"." << endl;
443 cerr <<
"Not yet implemented. Sorry." << endl;
446 if (optarg ==
nullptr) {
447 cerr <<
"Error: missing required argument for comment!" << endl;
453 if (optarg ==
nullptr) {
454 cerr <<
"Error: missing required argument for jail name!" << endl;
460 if (optarg ==
nullptr) {
461 cerr <<
"Error: missing required argument for fail2ban executable!" << endl;
482 string commandOutput;
484 FILE* pipe = popen(cmd.c_str(),
"r");
488 throw system_error(error_code(errno, std::generic_category()),
"Failed to start process!");
492 char outputBuffer[512] = {0};
493 while (!feof(pipe) && fgets(outputBuffer,
sizeof(outputBuffer) /
sizeof(outputBuffer[0]), pipe) != NULL) {
494 commandOutput += outputBuffer;
497 returnCode = pclose(pipe);
501 returnCode = pclose(pipe);
502 return commandOutput;
511 const static string RAW = R"(
512 {0} v{1} - A simple utility for converting fail2ban entries into an abuseipdb CSV format
515 {0} -f[/path/to/file] # Use [file] to parse fail2ban jail contents
516 {0} -% # to attempt to get output directly from fail2ban (requires elevated privileges!)
517 {0} --stdin # to read input from stdin
520 --help, -h Prints this text and exits
521 --stdin, -s Reads input from stdin
522 --file=, -f[file] Reads input from [file] or fail2ban.json if optarg is empty
523 --version, -v Prints the version information and exits
524 --comment, -c[text] Sets the value for the comment field. Variables are available below
525 --jail-name, -j[jail] Sets the name of the jail (useful if exporting specific jails from fail2ban)
526 --f2b, -e[f2b-client] Sets the location of the fail2ban-client executable (local system will not be searched)
527 --call-f2b, -% No, that's not a typo. Calls fail2ban directly. !! WARNING: REQUIRES ELEVATED PRIVILEGES. NOT RECOMMENDED !!
535 1 Failed to parse input from file
536 2 Failed to parse input from stdin
537 3 Failed to parse input from fail2ban exec
538 4 Insufficent execution rights
539 5 Could not find fail2ban-client
542 cout << format(RAW, binName, getProjectVersion()) << endl;
550 constexpr string_view getShortArgs() {
return "hsf:vc:j:e:%"; }
558 const static option OPTIONS[] = {
559 {
"help", no_argument,
nullptr,
'h' },
560 {
"stdin", no_argument,
nullptr,
's' },
561 {
"file", optional_argument,
nullptr,
'f' },
562 {
"version", no_argument,
nullptr,
'v' },
563 {
"comment", required_argument,
nullptr,
'c' },
564 {
"jail-name", required_argument,
nullptr,
'j' },
565 {
"f2b", required_argument,
nullptr,
'e' },
566 {
"call-f2b", no_argument,
nullptr,
'%' },
567 {
nullptr, no_argument,
nullptr, 0 }
580 transform(input.begin(), input.end(), std::back_inserter(output), [&](
const char c) {
581 if (c ==
'\'') {
return '"'; }
594 ofstream cacheOutput(
g_cacheFile, std::ios_base::openmode::_S_app);
595 cacheOutput << ip <<
":" << time(
nullptr) << endl;