////////////////////////////////////////////////////////////////////// // Basic Access Log report // Written by Jonathan A. Foster // Started August 20th, 2021 // Copyright JF Possibilities, Inc. All rights reserved. // // To start with this gather data for a specific access period and // create a report of domain names and addresses that accessed to and // from the net. All "accepted" and "blocked" accesses will be // ignored. For the moment we're expecting blocked traffic is actually // blocked. // // The report will contain three columns: // 1. domain name or address if a domain is not known. // 2. list of ports that were connected to. // 3. count of total connections ////////////////////////////////////////////////////////////////////// #include #include #include #include #include #include #include "../strutil.h" #include "../cli.h" #include "../miniini.h" using namespace std; ////////////////////////////////////////////////////////////////////// // A "line" of a report - a single domain/address ////////////////////////////////////////////////////////////////////// struct ReportLine { int count; map ports; // initialize ReportLine(): count(0) {} // add an item void add(const string &port, int _count) { ports[port]+=_count; count+=_count; } // ports -> string string port_list() const { string r; map::const_iterator it = ports.begin(); if(it==ports.end()) return r; r = it->first; for(it++; it!=ports.end(); it++) r+=","+it->first; return r; } }; ////////////////////////////////////////////////////////////////////// // A full reports worth of data - domain names / addresses and their // associated stats. ////////////////////////////////////////////////////////////////////// struct ReportData: map { // Add an item to the report void add(const string &address, const string &port, int count) { (*this)[address].add(port, count); } // Render report in an ASCII table string ascii() const { int widths[3] = {0,0,0}; // max column widths: 0: DNS, 1: ports, 3: counts int x; char l[256]; l[255]=0; string s, r, bk; ReportData::const_iterator it; for(it = begin(); it!=end(); it++) { if(it->first.size()>widths[0]) widths[0] = it->first.size(); s = it->second.port_list(); if(s.size()>widths[1]) widths[1]=s.size(); if(it->second.count>widths[2]) widths[2] = it->second.count; } // Now conver count max to cols for(x=0; widths[2]; x++) widths[2] /= 10; widths[2] = x ? x : 1; // min col widths for titles if(widths[0]<6) widths[0]=6; if(widths[1]<7) widths[1]=7; if(widths[2]<2) widths[2]=2; // render report bk = "+"+string(widths[0]+2, '-')+ "+"+string(widths[1]+2, '-')+ "+"+string(widths[2]+2, '-')+ "+\n"; s = "| %-"+str(widths[0])+"s | %-"+str(widths[1])+"s | %"+str(widths[2])+"d |\n"; r = bk; snprintf(l, sizeof(l)-1, ("| %-"+str(widths[0])+"s | %-"+str(widths[1])+"s | %-"+str(widths[2])+"s |\n").c_str(), (const char *)"Remote", (const char *)"Port(s)", (const char *)"Ct" ); r+= l; r+= bk; for(it = begin(); it!=end(); it++) { snprintf(l, sizeof(l)-1, s.c_str(), it->first.c_str(), it->second.port_list().c_str(), it->second.count ); r+=l; } r+=bk; return r; } }; inline ostream &operator<<(ostream &out, const ReportData &r){ return out << r.ascii(); } namespace cppdb { result &operator>>(result &qry, ::ReportData &rpt) { string name, addr, port; int ct; qry >> name >> addr >> port >> ct; if(name=="") name=addr; rpt.add(name, port, ct); } } ////////////////////////////////////////////////////////////////////// // Our config file. // // This is designed so that all parts can use the same config. Tools // ignore the parts they aren't interested in. ////////////////////////////////////////////////////////////////////// /* TODO: refactor this as a base class... */ struct ReportConf: public MiniINI { MiniINIvars traffic_mon; // This app's config variables ReportConf() { groups["Traffic Mon"] = &traffic_mon; } }; ////////////////////////////////////////////////////////////////////// // Connection Report Generator Application Class ////////////////////////////////////////////////////////////////////// struct appConnectionReport: cBaseApp { ReportConf config; ReportData rpt; cppdb::session db; string start_stamp; string end_stamp; int cli_mode; // which non-switch are we processing appConnectionReport(): cli_mode(0) {} int help() { cerr << " FORMAT: " << basename(command_args[0]) << " -c {config} {start} {end}\n" << '\n' << "The config file must have a [Traffic Mon] section with the database\n" << "credentials in it. 'start' and 'stop' are the SQL timestamps for\n" << "report time span." << endl; } virtual unsigned do_switch(const char *arg) { if(*arg=='c' && !arg[1]) return 1; throw CLIerror("Invalid switch "+string(arg)); } virtual void do_switch_arg(const char *sw, const std::string &val) { // The only way we get here is with -c config.load(val); // TODO: config validity checks } virtual void do_arg(const char *arg) { switch(cli_mode++) { case 0: start_stamp = arg; return; case 1: end_stamp = arg; return; default: throw CLIerror("Invalid arguments"); } } int main() { cppdb::result qry; /// SETUP & VALIDATE CLI /// try { cBaseApp::main(); // Parse CLI args if(!config.traffic_mon.vals.size()) throw CLIerror( "You need to load a config file with a [Traffic Mon] section" ); if(cli_mode!=2) throw CLIerror("Invlaid arguments"); } catch(const CLIerror &e) { cerr << e.what() << "\n\n"; return help(); } db.open("mysql:user="+qesc(config.traffic_mon.get("db user"))+ ";password="+qesc(config.traffic_mon.get("db password"))+ ";host="+qesc(config.traffic_mon.get("db host"))+ ";database="+qesc(config.traffic_mon.get("db name"))+ ";@opt_reconnect=1"); /// Query & load data /// qry = db << "SELECT c.them_name, c.them, c.them_port, count(*) " "FROM connections c LEFT OUTER JOIN dns d ON c.them_name=d.name " "WHERE c.inbound=0 AND (d.status IS NULL or d.status=0) " "AND tstamp>=? AND tstamp<=? " "GROUP BY c.them_name, c.them, c.them_port" << start_stamp << (end_stamp + " 23:59:59"); // include to the end of the day while(qry.next()) qry >> rpt; /// spit out the report /// cout << "Web access report for " << start_stamp << " - " << end_stamp << "\n\n" << rpt; return 0; } }; ////////////////////////////////////////////////////////////////////// // Lets run the report and dump it out ////////////////////////////////////////////////////////////////////// MAIN(appConnectionReport)