├── .gitignore ├── AttackMapServer ├── AttackMapServer.py ├── index.html └── static │ ├── flags.zip │ ├── index.css │ └── map.js ├── DataServer ├── DataServer.py ├── const.py ├── syslog-gen.py └── syslog-gen.sh ├── DataServerDB └── db-dl.sh ├── LICENSE ├── README.md ├── deploy.sh └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | 3 | env/ 4 | 5 | dump.rdb 6 | DataServer/dump.rdb 7 | AttackMapServer/dump.rdb 8 | 9 | DataServerDB/GeoLite2-City.mmdb 10 | DataServerDB/GeoLite2-City.mmdb.gz 11 | DataServer/__pycache__/ 12 | 13 | AttackMapServer/static/flags/* 14 | -------------------------------------------------------------------------------- /AttackMapServer/AttackMapServer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | """ 4 | AUTHOR: Matthew May - mcmay.web@gmail.com 5 | """ 6 | 7 | # Imports 8 | import json 9 | import redis 10 | import tornadoredis 11 | #import tornado.httpserver 12 | import tornado.ioloop 13 | import tornado.web 14 | import tornado.websocket 15 | #import re 16 | 17 | from os import getuid, path 18 | from sys import exit 19 | 20 | 21 | # Look up service colors 22 | service_rgb = { 23 | 'FTP':'#ff0000', 24 | 'SSH':'#ff8000', 25 | 'TELNET':'#ffff00', 26 | 'EMAIL':'#80ff00', 27 | 'WHOIS':'#00ff00', 28 | 'DNS':'#00ff80', 29 | 'HTTP':'#00ffff', 30 | 'HTTPS':'#0080ff', 31 | 'SQL':'#0000ff', 32 | 'SNMP':'#8000ff', 33 | 'SMB':'#bf00ff', 34 | 'AUTH':'#ff00ff', 35 | 'RDP':'#ff0060', 36 | 'DoS':'#ff0000', 37 | 'ICMP':'#ffcccc', 38 | 'OTHER':'#6600cc' 39 | } 40 | 41 | 42 | class IndexHandler(tornado.web.RequestHandler): 43 | @tornado.web.asynchronous 44 | def get(request): 45 | request.render('index.html') 46 | 47 | 48 | class WebSocketChatHandler(tornado.websocket.WebSocketHandler): 49 | def __init__(self, *args, **kwargs): 50 | super(WebSocketChatHandler, self).__init__(*args,**kwargs) 51 | self.listen() 52 | 53 | def check_origin(self, origin): 54 | return True 55 | 56 | @tornado.gen.engine 57 | def listen(self): 58 | 59 | print('[*] WebSocketChatHandler opened') 60 | 61 | try: 62 | # This is the IP address of the DataServer 63 | self.client = tornadoredis.Client('127.0.0.1') 64 | self.client.connect() 65 | print('[*] Connected to Redis server') 66 | yield tornado.gen.Task(self.client.subscribe, 'attack-map-production') 67 | self.client.listen(self.on_message) 68 | except Exception as ex: 69 | print('[*] Could not connect to Redis server.') 70 | print('[*] {}'.format(str(ex))) 71 | 72 | def on_close(self): 73 | print('[*] Closing connection.') 74 | 75 | # This function is called everytime a Redis message is received 76 | def on_message(self, msg): 77 | 78 | if len(msg) == 0: 79 | print ("msg == 0\n") 80 | return None 81 | 82 | if 'ip_blocked' in msg: 83 | ip = re.split(":",msg) 84 | #fp = open('/mnt/map_attack_blk/LOG4.log','a') 85 | #fp.write(ip[1]+"\n") 86 | #fp.close() 87 | 88 | try: 89 | json_data = json.loads(msg.body) 90 | except Exception as ex: 91 | return None 92 | 93 | if 'msg_type' in json_data: 94 | msg_type = json_data['msg_type'] 95 | else: 96 | msg_type = None 97 | if 'msg_type2' in json_data: 98 | msg_type2 = json_data['msg_type2'] 99 | else: 100 | msg_type2 = None 101 | if 'msg_type3' in json_data: 102 | msg_type3 = json_data['msg_type3'] 103 | else: 104 | msg_type3 = None 105 | if 'protocol' in json_data: 106 | protocol = json_data['protocol'] 107 | else: 108 | protocol = None 109 | if 'src_ip' in json_data: 110 | src_ip = json_data['src_ip'] 111 | else: 112 | src_ip = None 113 | if 'dst_ip' in json_data: 114 | dst_ip = json_data['dst_ip'] 115 | else: 116 | dst_ip = None 117 | if 'src_port' in json_data: 118 | src_port = json_data['src_port'] 119 | else: 120 | src_port = None 121 | if 'dst_port' in json_data: 122 | dst_port = json_data['dst_port'] 123 | else: 124 | dst_port = None 125 | if 'latitude' in json_data: 126 | src_lat = json_data['latitude'] 127 | else: 128 | src_lat = None 129 | if 'longitude' in json_data: 130 | src_long = json_data['longitude'] 131 | else: 132 | src_long = None 133 | if 'dst_lat' in json_data: 134 | dst_lat = json_data['dst_lat'] 135 | else: 136 | dst_lat = None 137 | if 'dst_long' in json_data: 138 | dst_long = json_data['dst_long'] 139 | else: 140 | dst_long = None 141 | if 'city' in json_data: 142 | city = json_data['city'] 143 | else: 144 | city = None 145 | if 'continent' in json_data: 146 | continent = json_data['continent'] 147 | else: 148 | continent = None 149 | if 'continent_code' in json_data: 150 | continent_code = json_data['continent_code'] 151 | else: 152 | continent_code = None 153 | if 'country' in json_data: 154 | country = json_data['country'] 155 | else: 156 | country = None 157 | if 'iso_code' in json_data: 158 | iso_code = json_data['iso_code'] 159 | else: 160 | iso_code = None 161 | if 'postal_code' in json_data: 162 | postal_code = json_data['postal_code'] 163 | else: 164 | postal_code = None 165 | if protocol: 166 | color = service_rgb[protocol] 167 | else: 168 | color = '#000000' 169 | if 'event_count' in json_data: 170 | event_count = json_data['event_count'] 171 | else: 172 | event_count = None 173 | if 'continents_tracked' in json_data: 174 | continents_tracked = json_data['continents_tracked'] 175 | else: 176 | continents_tracked = None 177 | if 'countries_tracked' in json_data: 178 | countries_tracked = json_data['countries_tracked'] 179 | else: 180 | countries_tracked = None 181 | if 'ips_tracked' in json_data: 182 | ips_tracked = json_data['ips_tracked'] 183 | else: 184 | ips_tracked = None 185 | if 'unknowns' in json_data: 186 | unknowns = json_data['unknowns'] 187 | else: 188 | unknowns = None 189 | if 'event_time' in json_data: 190 | event_time = json_data['event_time'] 191 | else: 192 | event_time = None 193 | if 'country_to_code' in json_data: 194 | country_to_code = json_data['country_to_code'] 195 | else: 196 | country_to_code = None 197 | if 'ip_to_code' in json_data: 198 | ip_to_code = json_data['ip_to_code'] 199 | else: 200 | ip_to_code = None 201 | 202 | msg_to_send = { 203 | 'type': msg_type, 204 | 'type2': msg_type2, 205 | 'type3': msg_type3, 206 | 'protocol': protocol, 207 | 'src_ip': src_ip, 208 | 'dst_ip': dst_ip, 209 | 'src_port': src_port, 210 | 'dst_port': dst_port, 211 | 'src_lat': src_lat, 212 | 'src_long': src_long, 213 | 'dst_lat': dst_lat, 214 | 'dst_long': dst_long, 215 | 'city': city, 216 | 'continent': continent, 217 | 'continent_code': continent_code, 218 | 'country': country, 219 | 'iso_code': iso_code, 220 | 'postal_code': postal_code, 221 | 'color': color, 222 | 'event_count': event_count, 223 | 'continents_tracked': continents_tracked, 224 | 'countries_tracked': countries_tracked, 225 | #'ips_tracked': "" + str(ips_tracked) + "", 226 | 'ips_tracked': ips_tracked, 227 | 'unknowns': unknowns, 228 | 'event_time': event_time, 229 | 'country_to_code': country_to_code, 230 | 'ip_to_code': ip_to_code, 231 | } 232 | 233 | 234 | self.write_message(json.dumps(msg_to_send)) 235 | 236 | def main(): 237 | # Register handler pages 238 | handlers = [ 239 | (r'/websocket', WebSocketChatHandler), 240 | (r'/static/(.*)', tornado.web.StaticFileHandler, {'path': 'static'}), 241 | (r'/flags/(.*)', tornado.web.StaticFileHandler, {'path': 'static/flags'}), 242 | (r'/', IndexHandler) 243 | ] 244 | 245 | # Define the static path 246 | #static_path = path.join( path.dirname(__file__), 'static' ) 247 | 248 | # Define static settings 249 | settings = { 250 | #'static_path': static_path 251 | } 252 | 253 | # Create and start app listening on port 8888 254 | try: 255 | app = tornado.web.Application(handlers, **settings) 256 | app.listen(8888) 257 | print('[*] Waiting on browser connections...') 258 | tornado.ioloop.IOLoop.instance().start() 259 | except Exception as appFail: 260 | print(appFail) 261 | 262 | if __name__ == '__main__': 263 | try: 264 | main() 265 | except KeyboardInterrupt: 266 | print('\nSHUTTING DOWN') 267 | exit() 268 | -------------------------------------------------------------------------------- /AttackMapServer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | GeoIP Attack Map 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 |
31 | 32 |
33 |
34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 |
Service
FTP
SSH
TELNET
EMAIL
WHOIS
DNS
HTTP
HTTPS
SQL
SNMP
SMB
AUTH
RDP
DOS
ICMP
OTHER
125 |
126 | 127 |
128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 |
QtdIP
140 |
141 | 142 |
143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 |
QtdCountry
155 |
156 | 157 |
158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 |
TimestampIPCountryCityService
173 |
174 |
175 |
176 | 177 |
178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 |
Attack Types
188 |
189 | 190 | 191 | 192 | 193 |
194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 |
TimestampExploitIP
207 |
208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | -------------------------------------------------------------------------------- /AttackMapServer/static/flags.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatthewClarkMay/geoip-attack-map/58ac0af0fced187291e600704bcf9a15f7ce6e03/AttackMapServer/static/flags.zip -------------------------------------------------------------------------------- /AttackMapServer/static/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | background:black; 3 | } 4 | body { 5 | height:100%; 6 | width:100%; 7 | margin:0; 8 | padding:0; 9 | } 10 | #map { 11 | height:100%; 12 | width:100%; 13 | position:absolute; 14 | top:0; 15 | bottom:0; 16 | } 17 | .leaflet-container { 18 | background:black; 19 | } 20 | .circle { 21 | height:18px; 22 | width:18px; 23 | border-radius:50%; 24 | margin:0px; 25 | } 26 | 27 | #informIP { 28 | display:none; 29 | height:auto; 30 | width:250px; 31 | position:absolute; 32 | bottom:30%; 33 | left:10%; 34 | padding:15px; 35 | border-radius:25px; 36 | background:#666161; 37 | opacity:0.9; 38 | } 39 | 40 | #informIP #fechar{ 41 | float:right; 42 | left:90%; 43 | border-radius:25px; 44 | color:red; 45 | font-style:oblique; 46 | } 47 | 48 | #informIP #blockIP{ 49 | position:relative; 50 | left:30%; 51 | padding:10px; 52 | font-weight:bold; 53 | box-shadow:1px 1px; 54 | } 55 | #informIP h3{ 56 | font-size:1.4em; 57 | padding:0px; 58 | color:#ffffff; 59 | } 60 | 61 | .container-fluid { 62 | height:25%; 63 | width:100%; 64 | position:absolute; 65 | bottom:3.5%; 66 | background:black; 67 | opacity:0.7; 68 | } 69 | .row { 70 | height:100%; 71 | } 72 | .row table { 73 | height:100%; 74 | margin:0px 75 | } 76 | tbody { 77 | height:100%; 78 | } 79 | table th, td{ 80 | text-align:center; 81 | } 82 | th, td { 83 | color:white; 84 | } 85 | .col-md-1 { 86 | height:100%; 87 | overflow-y:scroll; 88 | overflow-x:auto; 89 | } 90 | .col-md-2 { 91 | height:100%; 92 | overflow-y:scroll; 93 | overflow-x:auto; 94 | } 95 | .col-md-7 { 96 | height:100%; 97 | overflow-y:scroll; 98 | overflow-x:auto; 99 | } 100 | 101 | .col-md-8 { 102 | height:158px; 103 | overflow-y:scroll; 104 | overflow-x:auto; 105 | } 106 | 107 | .col-md-4 { 108 | height:154px; 109 | overflow-y:scroll; 110 | overflow-x:auto; 111 | } 112 | -------------------------------------------------------------------------------- /AttackMapServer/static/map.js: -------------------------------------------------------------------------------- 1 | // To access by a browser in another computer, use the external IP of machine running AttackMapServer 2 | // from the same computer(only), you can use the internal IP. 3 | // Example: 4 | // - AttackMapServer machine: 5 | // - Internal IP: 127.0.0.1 6 | // - External IP: 192.168.11.106 7 | var webSock = new WebSocket("ws:/127.0.0.1:8888/websocket"); // Internal 8 | //var webSock = new WebSocket("ws:/192.168.1.100:8888/websocket"); // External 9 | 10 | // link map 11 | 12 | L.mapbox.accessToken = "pk.eyJ1IjoibW1heTYwMSIsImEiOiJjaWgyYWU3NWQweWx2d3ltMDl4eGk5eWY1In0.9YoOkALPP7zaoim34ZITxw"; 13 | var map = L.mapbox.map("map", "mapbox.dark", { 14 | center: [0, 0], // lat, long 15 | zoom: 2 16 | }); 17 | 18 | // add full screen option 19 | L.control.fullscreen().addTo(map); 20 | 21 | // hq coords 22 | var hqLatLng = new L.LatLng(37.3845, -122.0881); 23 | 24 | // hq marker 25 | L.circle(hqLatLng, 110000, { 26 | color: 'red', 27 | fillColor: 'yellow', 28 | fillOpacity: 0.5, 29 | }).addTo(map); 30 | 31 | // Append to map 32 | var svg = d3.select(map.getPanes().overlayPane).append("svg") 33 | .attr("class", "leaflet-zoom-animated") 34 | .attr("width", window.innerWidth) 35 | .attr("height", window.innerHeight); 36 | 37 | // Append to svg 38 | //var g = svg.append("g").attr("class", "leaflet-zoom-hide"); 39 | 40 | function translateSVG() { 41 | var viewBoxLeft = document.querySelector("svg.leaflet-zoom-animated").viewBox.animVal.x; 42 | var viewBoxTop = document.querySelector("svg.leaflet-zoom-animated").viewBox.animVal.y; 43 | 44 | // Resizing width and height in case of window resize 45 | svg.attr("width", window.innerWidth); 46 | svg.attr("height", window.innerHeight); 47 | 48 | // Adding the ViewBox attribute to our SVG to contain it 49 | svg.attr("viewBox", function () { 50 | return "" + viewBoxLeft + " " + viewBoxTop + " " + window.innerWidth + " " + window.innerHeight; 51 | }); 52 | 53 | // Adding the style attribute to our SVG to translate it 54 | svg.attr("style", function () { 55 | return "transform: translate3d(" + viewBoxLeft + "px, " + viewBoxTop + "px, 0px);"; 56 | }); 57 | } 58 | 59 | function update() { 60 | translateSVG(); 61 | // additional stuff 62 | } 63 | 64 | // Re-draw on reset, this keeps the markers where they should be on reset/zoom 65 | map.on("moveend", update); 66 | 67 | function calcMidpoint(x1, y1, x2, y2, bend) { 68 | if(y2= 50) { 212 | circles.removeLayer(circleArray[0]); 213 | } 214 | 215 | L.circle(srcLatLng, 50000, { 216 | color: msg.color, 217 | fillColor: msg.color, 218 | fillOpacity: 0.2, 219 | }).addTo(circles); 220 | } 221 | 222 | function prependAttackRow(id, args) { 223 | var tr = document.createElement('tr'); 224 | count = args.length; 225 | 226 | for (var i = 0; i < count; i++) { 227 | var td = document.createElement('td'); 228 | if (args[i] === args[2]) { 229 | var path = 'flags/' + args[i] + '.png'; 230 | var img = document.createElement('img'); 231 | img.src = path; 232 | td.appendChild(img); 233 | tr.appendChild(td); 234 | } else { 235 | var textNode = document.createTextNode(args[i]); 236 | td.appendChild(textNode); 237 | tr.appendChild(td); 238 | } 239 | } 240 | 241 | var element = document.getElementById(id); 242 | var rowCount = element.rows.length; 243 | 244 | // Only allow 50 rows 245 | if (rowCount >= 50) { 246 | element.deleteRow(rowCount -1); 247 | } 248 | 249 | element.insertBefore(tr, element.firstChild); 250 | } 251 | 252 | function prependTypeRow(id, args) { 253 | var tr = document.createElement('tr'); 254 | count = args.length; 255 | 256 | for (var i = 0; i < count; i++) { 257 | var td = document.createElement('td'); 258 | var textNode = document.createTextNode(args[i]); 259 | td.appendChild(textNode); 260 | tr.appendChild(td); 261 | } 262 | 263 | var element = document.getElementById(id); 264 | var rowCount = element.rows.length; 265 | 266 | // Only allow 50 rows 267 | if (rowCount >= 50) { 268 | element.deleteRow(rowCount -1); 269 | } 270 | 271 | element.insertBefore(tr, element.firstChild); 272 | } 273 | 274 | function prependCVERow(id, args) { 275 | var tr = document.createElement('tr'); 276 | 277 | //count = args.length; 278 | count = 1; 279 | 280 | for (var i = 0; i < count; i++) { 281 | var td1 = document.createElement('td'); 282 | var td2 = document.createElement('td'); 283 | var td3 = document.createElement('td'); 284 | var td4 = document.createElement('td'); 285 | 286 | // Timestamp 287 | var textNode2 = document.createTextNode(args[0]); 288 | td1.appendChild(textNode2); 289 | tr.appendChild(td1); 290 | 291 | // Exploit 292 | var textNode = document.createTextNode(args[1]); 293 | 294 | var alink = document.createElement('a'); 295 | alink.setAttribute("href",args[1]); 296 | alink.setAttribute("target","_blank") 297 | alink.style.color = "white"; 298 | alink.appendChild(textNode); 299 | 300 | td2.appendChild(alink); 301 | tr.appendChild(td2); 302 | 303 | // Flag 304 | var path = 'flags/' + args[2] + '.png'; 305 | var img = document.createElement('img'); 306 | img.src = path; 307 | td3.appendChild(img); 308 | tr.appendChild(td3); 309 | 310 | // IP 311 | var textNode3 = document.createTextNode(args[3]); 312 | td4.appendChild(textNode3); 313 | tr.appendChild(td4); 314 | } 315 | 316 | var element = document.getElementById(id); 317 | var rowCount = element.rows.length; 318 | 319 | // Only allow 50 rows 320 | if (rowCount >= 50) { 321 | element.deleteRow(rowCount -1); 322 | } 323 | 324 | element.insertBefore(tr, element.firstChild); 325 | } 326 | 327 | 328 | function redrawCountIP(hashID, id, countList, codeDict) { 329 | $(hashID).empty(); 330 | var element = document.getElementById(id); 331 | 332 | // Sort ips greatest to least 333 | // Create items array from dict 334 | var items = Object.keys(countList[0]).map(function(key) { 335 | return [key, countList[0][key]]; 336 | }); 337 | // Sort the array based on the second element 338 | items.sort(function(first, second) { 339 | return second[1] - first[1]; 340 | }); 341 | // Create new array with only the first 50 items 342 | var sortedItems = items.slice(0, 50); 343 | var itemsLength = sortedItems.length; 344 | 345 | for (var i = 0; i < itemsLength; i++) { 346 | tr = document.createElement('tr'); 347 | td1 = document.createElement('td'); 348 | td2 = document.createElement('td'); 349 | td3 = document.createElement('td'); 350 | var key = sortedItems[i][0]; 351 | value = sortedItems[i][1]; 352 | var keyNode = document.createTextNode(key); 353 | var valueNode = document.createTextNode(value); 354 | var path = 'flags/' + codeDict[key] + '.png'; 355 | var img = document.createElement('img'); 356 | img.src = path; 357 | td1.appendChild(valueNode); 358 | td2.appendChild(img); 359 | 360 | var alink = document.createElement('a'); 361 | alink.setAttribute("href","#"); 362 | alink.setAttribute("class","showInfo"); 363 | alink.style.color = "white"; 364 | alink.appendChild(keyNode); 365 | 366 | td3.appendChild(alink); 367 | tr.appendChild(td1); 368 | tr.appendChild(td2); 369 | tr.appendChild(td3); 370 | element.appendChild(tr); 371 | } 372 | } 373 | 374 | function redrawCountIP2(hashID, id, countList, codeDict) { 375 | $(hashID).empty(); 376 | var element = document.getElementById(id); 377 | 378 | // Sort ips greatest to least 379 | // Create items array from dict 380 | var items = Object.keys(countList[0]).map(function(key) { 381 | return [key, countList[0][key]]; 382 | }); 383 | // Sort the array based on the second element 384 | items.sort(function(first, second) { 385 | return second[1] - first[1]; 386 | }); 387 | // Create new array with only the first 50 items 388 | var sortedItems = items.slice(0, 50); 389 | var itemsLength = sortedItems.length; 390 | 391 | for (var i = 0; i < itemsLength; i++) { 392 | tr = document.createElement('tr'); 393 | td1 = document.createElement('td'); 394 | td2 = document.createElement('td'); 395 | td3 = document.createElement('td'); 396 | var key = sortedItems[i][0]; 397 | value = sortedItems[i][1]; 398 | var keyNode = document.createTextNode(key); 399 | var valueNode = document.createTextNode(value); 400 | var path = 'flags/' + codeDict[key] + '.png'; 401 | var img = document.createElement('img'); 402 | img.src = path; 403 | td1.appendChild(valueNode); 404 | td2.appendChild(img); 405 | 406 | td3.appendChild(keyNode); 407 | tr.appendChild(td1); 408 | tr.appendChild(td2); 409 | tr.appendChild(td3); 410 | element.appendChild(tr); 411 | } 412 | } 413 | 414 | function handleLegend(msg) { 415 | var ipCountList = [msg.ips_tracked, 416 | msg.iso_code]; 417 | var countryCountList = [msg.countries_tracked, 418 | msg.iso_code]; 419 | var attackList = [msg.event_time, 420 | msg.src_ip, 421 | msg.iso_code, 422 | msg.country, 423 | msg.city, 424 | msg.protocol]; 425 | redrawCountIP('#ip-tracking','ip-tracking', ipCountList, msg.ip_to_code); 426 | redrawCountIP2('#country-tracking', 'country-tracking', countryCountList, msg.country_to_code); 427 | prependAttackRow('attack-tracking', attackList); 428 | } 429 | 430 | function handleLegendType(msg) { 431 | var attackType = [msg.type2]; 432 | var attackCve = [msg.event_time, 433 | msg.type3, 434 | msg.iso_code, 435 | msg.src_ip, 436 | //msg.country, 437 | //msg.city, 438 | //msg.protocol 439 | ]; 440 | 441 | if (attackType != "___") { 442 | prependTypeRow('attack-type', attackType); 443 | } 444 | 445 | if (attackCve[1] != "___"){ 446 | prependCVERow('attack-cveresp', attackCve); 447 | } 448 | } 449 | 450 | // WEBSOCKET STUFF 451 | 452 | webSock.onmessage = function (e) { 453 | console.log("Got a websocket message..."); 454 | try { 455 | var msg = JSON.parse(e.data); 456 | console.log(msg); 457 | switch(msg.type) { 458 | case "Traffic": 459 | console.log("Traffic!"); 460 | var srcLatLng = new L.LatLng(msg.src_lat, msg.src_long); 461 | var hqPoint = map.latLngToLayerPoint(hqLatLng); 462 | var srcPoint = map.latLngToLayerPoint(srcLatLng); 463 | console.log(''); 464 | addCircle(msg, srcLatLng); 465 | handleParticle(msg, srcPoint); 466 | handleTraffic(msg, srcPoint, hqPoint, srcLatLng); 467 | handleLegend(msg); 468 | handleLegendType(msg) 469 | break; 470 | // Add support for other message types? 471 | } 472 | } catch(err) { 473 | console.log(err) 474 | } 475 | }; 476 | 477 | $(document).on("click","#informIP #exit", function (e) { 478 | $("#informIP").hide(); 479 | }); 480 | 481 | $(document).on("click", '.container-fluid .showInfo', function(e) { 482 | var iplink = $(this).text(); 483 | $("#informIP").show(); 484 | $("#informIP").html( "

"+iplink+"



"); 485 | }); 486 | 487 | 488 | $(document).on("click","#informIP #blockIP", function (e) { 489 | var ip= $(this).attr('alt'); 490 | var ipBlocked = "ip_blocked:"+ip; 491 | console.log("Sending message: "+ipBlocked); 492 | webSock.send(ipBlocked); 493 | }); 494 | -------------------------------------------------------------------------------- /DataServer/DataServer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | """ 4 | AUTHOR: Matthew May - mcmay.web@gmail.com 5 | """ 6 | 7 | # Imports 8 | import json 9 | #import logging 10 | import maxminddb 11 | #import re 12 | import redis 13 | import io 14 | 15 | from const import META, PORTMAP 16 | 17 | from argparse import ArgumentParser, RawDescriptionHelpFormatter 18 | from os import getuid 19 | from sys import exit 20 | #from textwrap import dedent 21 | from time import gmtime, localtime, sleep, strftime 22 | 23 | # start the Redis server if it isn't started already. 24 | # $ redis-server 25 | # default port is 6379 26 | # make sure system can use a lot of memory and overcommit memory 27 | 28 | redis_ip = '127.0.0.1' 29 | redis_instance = None 30 | 31 | # required input paths 32 | syslog_path = '/var/log/syslog' 33 | #syslog_path = '/var/log/reverse-proxy.log' 34 | db_path = '../DataServerDB/GeoLite2-City.mmdb' 35 | 36 | # file to log data 37 | #log_file_out = '/var/log/map_data_server.out' 38 | 39 | # ip for headquarters 40 | hq_ip = '8.8.8.8' 41 | 42 | # stats 43 | server_start_time = strftime("%d-%m-%Y %H:%M:%S", localtime()) # local time 44 | event_count = 0 45 | continents_tracked = {} 46 | countries_tracked = {} 47 | country_to_code = {} 48 | ip_to_code = {} 49 | ips_tracked = {} 50 | unknowns = {} 51 | 52 | # @IDEA 53 | #--------------------------------------------------------- 54 | # Use a class to nicely wrap everything: 55 | # Could attempt to do an access here 56 | # now without worrying about key errors, 57 | # or just keep the filled data structure 58 | # 59 | #class Instance(dict): 60 | # 61 | # defaults = { 62 | # 'city': {'names':{'en':None}}, 63 | # 'continent': {'names':{'en':None}}, 64 | # 'continent': {'code':None}, 65 | # 'country': {'names':{'en':None}}, 66 | # 'country': {'iso_code':None}, 67 | # 'location': {'latitude':None}, 68 | # 'location': {'longitude':None}, 69 | # 'location': {'metro_code':None}, 70 | # 'postal': {'code':None} 71 | # } 72 | # 73 | # def __init__(self, seed): 74 | # self(seed) 75 | # backfill() 76 | # 77 | # def backfill(self): 78 | # for default in self.defaults: 79 | # if default not in self: 80 | # self[default] = defaults[default] 81 | #--------------------------------------------------------- 82 | 83 | # Create clean dictionary using unclean db dictionary contents 84 | def clean_db(unclean): 85 | selected = {} 86 | for tag in META: 87 | head = None 88 | if tag['tag'] in unclean: 89 | head = unclean[tag['tag']] 90 | for node in tag['path']: 91 | if node in head: 92 | head = head[node] 93 | else: 94 | head = None 95 | break 96 | selected[tag['lookup']] = head 97 | 98 | return selected 99 | 100 | 101 | def connect_redis(redis_ip): 102 | r = redis.StrictRedis(host=redis_ip, port=6379, db=0) 103 | return r 104 | 105 | 106 | def get_msg_type(): 107 | # @TODO 108 | # Add support for more message types later 109 | return "Traffic" 110 | 111 | # Check to see if packet is using an interesting TCP/UDP protocol based on source or destination port 112 | def get_tcp_udp_proto(src_port, dst_port): 113 | src_port = int(src_port) 114 | dst_port = int(dst_port) 115 | 116 | if src_port in PORTMAP: 117 | return PORTMAP[src_port] 118 | if dst_port in PORTMAP: 119 | return PORTMAP[dst_port] 120 | 121 | return "OTHER" 122 | 123 | 124 | def find_hq_lat_long(hq_ip): 125 | hq_ip_db_unclean = parse_maxminddb(db_path, hq_ip) 126 | if hq_ip_db_unclean: 127 | hq_ip_db_clean = clean_db(hq_ip_db_unclean) 128 | dst_lat = hq_ip_db_clean['latitude'] 129 | dst_long = hq_ip_db_clean['longitude'] 130 | hq_dict = { 131 | 'dst_lat': dst_lat, 132 | 'dst_long': dst_long 133 | } 134 | return hq_dict 135 | else: 136 | print('Please provide a valid IP address for headquarters') 137 | exit() 138 | 139 | 140 | def parse_maxminddb(db_path, ip): 141 | try: 142 | reader = maxminddb.open_database(db_path) 143 | response = reader.get(ip) 144 | reader.close() 145 | return response 146 | except FileNotFoundError: 147 | print('DB not found') 148 | print('SHUTTING DOWN') 149 | exit() 150 | except ValueError: 151 | return False 152 | 153 | 154 | # @TODO 155 | # Refactor/improve parsing 156 | # This function depends heavily on which appliances are generating logs 157 | # For now it is only here for testing 158 | 159 | def parse_syslog(line): 160 | line = line.split() 161 | data = line[-1] 162 | data = data.split(',') 163 | 164 | if len(data) != 6: 165 | print('NOT A VALID LOG') 166 | return False 167 | else: 168 | src_ip = data[0] 169 | dst_ip = data[1] 170 | src_port = data[2] 171 | dst_port = data[3] 172 | type_attack = data[4] 173 | cve_attack = data[5] 174 | data_dict = { 175 | 'src_ip': src_ip, 176 | 'dst_ip': dst_ip, 177 | 'src_port': src_port, 178 | 'dst_port': dst_port, 179 | 'type_attack': type_attack, 180 | 'cve_attack': cve_attack 181 | } 182 | return data_dict 183 | 184 | 185 | def shutdown_and_report_stats(): 186 | print('\nSHUTTING DOWN') 187 | # Report stats tracked 188 | print('\nREPORTING STATS...') 189 | print('\nEvent Count: {}'.format(event_count)) # report event count 190 | print('\nContinent Stats...') # report continents stats 191 | for key in continents_tracked: 192 | print('{}: {}'.format(key, continents_tracked[key])) 193 | print('\nCountry Stats...') # report country stats 194 | for country in countries_tracked: 195 | print('{}: {}'.format(country, countries_tracked[country])) 196 | print('\nCountries to iso_codes...') 197 | for key in country_to_code: 198 | print('{}: {}'.format(key, country_to_code[key])) 199 | print('\nIP Stats...') # report IP stats 200 | for ip in ips_tracked: 201 | print('{}: {}'.format(ip, ips_tracked[ip])) 202 | print('\nIPs to iso_codes...') 203 | for key in ip_to_code: 204 | print('{}: {}'.format(key, ip_to_code[key])) 205 | print('\nUnknowns...') 206 | for key in unknowns: 207 | print('{}: {}'.format(key, unknowns[key])) 208 | exit() 209 | 210 | 211 | #def menu(): 212 | # Instantiate parser 213 | #parser = ArgumentParser( 214 | # prog='DataServer.py', 215 | # usage='%(progs)s [OPTIONS]', 216 | # formatter_class=RawDescriptionHelpFormatter, 217 | # description=dedent('''\ 218 | # -------------------------------------------------------------- 219 | # Data server for attack map application. 220 | # --------------------------------------------------------------''')) 221 | 222 | # @TODO --> Add support for command line args? 223 | #define command line arguments 224 | #parser.add_argument('-db', '--database', dest='db_path', required=True, type=str, help='path to maxmind database') 225 | #parser.add_argument('-m', '--readme', dest='readme', help='print readme') 226 | #parser.add_argument('-o', '--output', dest='output', help='file to write logs to') 227 | #parser.add_argument('-r', '--random', action='store_true', dest='randomize', help='generate random IPs/protocols for demo') 228 | #parser.add_argument('-rs', '--redis-server-ip', dest='redis_ip', type=str, help='redis server ip address') 229 | #parser.add_argument('-sp', '--syslog-path', dest='syslog_path', type=str, help='path to syslog file') 230 | #parser.add_argument('-v', '--verbose', action='store_true', dest='verbose', help='run server in verbose mode') 231 | 232 | # Parse arguments/options 233 | #args = parser.parse_args() 234 | #return args 235 | 236 | 237 | def merge_dicts(*args): 238 | super_dict = {} 239 | for arg in args: 240 | super_dict.update(arg) 241 | return super_dict 242 | 243 | 244 | def track_flags(super_dict, tracking_dict, key1, key2): 245 | if key1 in super_dict: 246 | if key2 in super_dict: 247 | if key1 in tracking_dict: 248 | return None 249 | else: 250 | tracking_dict[super_dict[key1]] = super_dict[key2] 251 | else: 252 | return None 253 | else: 254 | return None 255 | 256 | 257 | def track_stats(super_dict, tracking_dict, key): 258 | if key in super_dict: 259 | node = super_dict[key] 260 | if node in tracking_dict: 261 | tracking_dict[node] += 1 262 | else: 263 | tracking_dict[node] = 1 264 | else: 265 | if key in unknowns: 266 | unknowns[key] += 1 267 | else: 268 | unknowns[key] = 1 269 | 270 | 271 | def main(): 272 | if getuid() != 0: 273 | print('Please run this script as root') 274 | print('SHUTTING DOWN') 275 | exit() 276 | 277 | global db_path, log_file_out, redis_ip, redis_instance, syslog_path, hq_ip 278 | global continents_tracked, countries_tracked, ips_tracked, postal_codes_tracked, event_count, unknown, ip_to_code, country_to_code 279 | 280 | #args = menu() 281 | 282 | # Connect to Redis 283 | redis_instance = connect_redis(redis_ip) 284 | 285 | # Find HQ lat/long 286 | hq_dict = find_hq_lat_long(hq_ip) 287 | 288 | # Follow/parse/format/publish syslog data 289 | with io.open(syslog_path, "r", encoding='ISO-8859-1') as syslog_file: 290 | syslog_file.readlines() 291 | while True: 292 | where = syslog_file.tell() 293 | line = syslog_file.readline() 294 | if not line: 295 | sleep(.1) 296 | syslog_file.seek(where) 297 | else: 298 | syslog_data_dict = parse_syslog(line) 299 | if syslog_data_dict: 300 | ip_db_unclean = parse_maxminddb(db_path, syslog_data_dict['src_ip']) 301 | if ip_db_unclean: 302 | event_count += 1 303 | ip_db_clean = clean_db(ip_db_unclean) 304 | 305 | msg_type = {'msg_type': get_msg_type()} 306 | msg_type2 = {'msg_type2': syslog_data_dict['type_attack']} 307 | msg_type3 = {'msg_type3': syslog_data_dict['cve_attack']} 308 | 309 | proto = {'protocol': get_tcp_udp_proto( 310 | syslog_data_dict['src_port'], 311 | syslog_data_dict['dst_port'] 312 | )} 313 | super_dict = merge_dicts( 314 | hq_dict, 315 | ip_db_clean, 316 | msg_type, 317 | msg_type2, 318 | msg_type3, 319 | proto, 320 | syslog_data_dict 321 | ) 322 | 323 | # Track Stats 324 | track_stats(super_dict, continents_tracked, 'continent') 325 | track_stats(super_dict, countries_tracked, 'country') 326 | track_stats(super_dict, ips_tracked, 'src_ip') 327 | event_time = strftime("%d-%m-%Y %H:%M:%S", localtime()) # local time 328 | #event_time = strftime("%Y-%m-%d %H:%M:%S", gmtime()) # UTC time 329 | track_flags(super_dict, country_to_code, 'country', 'iso_code') 330 | track_flags(super_dict, ip_to_code, 'src_ip', 'iso_code') 331 | 332 | # Append stats to super_dict 333 | super_dict['event_count'] = event_count 334 | super_dict['continents_tracked'] = continents_tracked 335 | super_dict['countries_tracked'] = countries_tracked 336 | super_dict['ips_tracked'] = ips_tracked 337 | super_dict['unknowns'] = unknowns 338 | super_dict['event_time'] = event_time 339 | super_dict['country_to_code'] = country_to_code 340 | super_dict['ip_to_code'] = ip_to_code 341 | 342 | json_data = json.dumps(super_dict) 343 | redis_instance.publish('attack-map-production', json_data) 344 | 345 | #if args.verbose: 346 | # print(ip_db_unclean) 347 | # print('------------------------') 348 | # print(json_data) 349 | # print('Event Count: {}'.format(event_count)) 350 | # print('------------------------') 351 | 352 | print('Event Count: {}'.format(event_count)) 353 | print('------------------------') 354 | 355 | else: 356 | continue 357 | 358 | 359 | if __name__ == '__main__': 360 | try: 361 | main() 362 | except KeyboardInterrupt: 363 | shutdown_and_report_stats() 364 | -------------------------------------------------------------------------------- /DataServer/const.py: -------------------------------------------------------------------------------- 1 | META = [{ 2 | 'lookup': 'city', 3 | 'tag': 'city', 4 | 'path': ['names','en'], 5 | },{ 6 | 'lookup': 'continent', 7 | 'tag': 'continent', 8 | 'path': ['names','en'], 9 | },{ 10 | 'lookup': 'continent_code', 11 | 'tag': 'continent', 12 | 'path': ['code'], 13 | },{ 14 | 'lookup': 'country', 15 | 'tag': 'country', 16 | 'path': ['names','en'], 17 | },{ 18 | 'lookup': 'iso_code', 19 | 'tag': 'country', 20 | 'path': ['iso_code'], 21 | },{ 22 | 'lookup': 'latitude', 23 | 'tag': 'location', 24 | 'path': ['latitude'], 25 | },{ 26 | 'lookup': 'longitude', 27 | 'tag': 'location', 28 | 'path': ['longitude'], 29 | },{ 30 | 'lookup': 'metro_code', 31 | 'tag': 'location', 32 | 'path': ['metro_code'], 33 | },{ 34 | 'lookup': 'postal_code', 35 | 'tag': 'postal', 36 | 'path': ['code'], 37 | }] 38 | 39 | PORTMAP = { 40 | 0:"DoS", # Denial of Service 41 | 1:"ICMP", # ICMP 42 | 20:"FTP", # FTP Data 43 | 21:"FTP", # FTP Control 44 | 22:"SSH", # SSH 45 | 23:"TELNET", # Telnet 46 | 25:"EMAIL", # SMTP 47 | 43:"WHOIS", # Whois 48 | 53:"DNS", # DNS 49 | 80:"HTTP", # HTTP 50 | 88:"AUTH", # Kerberos 51 | 109:"EMAIL", # POP v2 52 | 110:"EMAIL", # POP v3 53 | 115:"FTP", # SFTP 54 | 118:"SQL", # SQL 55 | 143:"EMAIL", # IMAP 56 | 156:"SQL", # SQL 57 | 161:"SNMP", # SNMP 58 | 220:"EMAIL", # IMAP v3 59 | 389:"AUTH", # LDAP 60 | 443:"HTTPS", # HTTPS 61 | 445:"SMB", # SMB 62 | 636:"AUTH", # LDAP of SSL/TLS 63 | 1433:"SQL", # MySQL Server 64 | 1434:"SQL", # MySQL Monitor 65 | 3306:"SQL", # MySQL 66 | 3389:"RDP", # RDP 67 | 5900:"RDP", # VNC:0 68 | 5901:"RDP", # VNC:1 69 | 5902:"RDP", # VNC:2 70 | 5903:"RDP", # VNC:3 71 | 8080:"HTTP", # HTTP Alternative 72 | } 73 | -------------------------------------------------------------------------------- /DataServer/syslog-gen.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import random, syslog 4 | from const import PORTMAP 5 | from sys import exit 6 | from time import sleep 7 | 8 | def main(): 9 | 10 | port_list = [] 11 | type_attack_list = [] 12 | 13 | for port in PORTMAP: 14 | port_list.append(port) 15 | type_attack_list.append(PORTMAP[port]) 16 | 17 | while True: 18 | port = random.choice(port_list) 19 | type_attack = random.choice(type_attack_list) 20 | cve_attack = 'CVE:{}:{}'.format( 21 | random.randrange(1,2000), 22 | random.randrange(100,1000) 23 | ) 24 | 25 | rand_data = '{}.{}.{}.{},{}.{}.{}.{},{},{},{},{}'.format( 26 | random.randrange(1, 256), 27 | random.randrange(1, 256), 28 | random.randrange(1, 256), 29 | random.randrange(1, 256), 30 | random.randrange(1, 256), 31 | random.randrange(1, 256), 32 | random.randrange(1, 256), 33 | random.randrange(1, 256), 34 | port, 35 | port, 36 | type_attack, 37 | cve_attack 38 | ) 39 | 40 | syslog.syslog(rand_data) 41 | print(rand_data) 42 | sleep(1) 43 | 44 | if __name__ == '__main__': 45 | try: 46 | main() 47 | except KeyboardInterrupt: 48 | exit() 49 | -------------------------------------------------------------------------------- /DataServer/syslog-gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | possible_ports=(0 1 20 21 22 23 25 40 43 53 80 88 109 110 115 118 143 156 161 220 389 443 445 636 1433 1434 3306 3389 5900 5901 5902 5903 8080 9999) 4 | 5 | while true; 6 | do 7 | for port in ${possible_ports[@]}; 8 | do 9 | src_ip=$((RANDOM%=255))"."$((RANDOM%=255))"."$((RANDOM%=255))"."$((RANDOM%=255)) 10 | #dst_ip=$((RANDOM%=255))"."$((RANDOM%=255))"."$((RANDOM%=255))"."$((RANDOM%=255)) 11 | dst_ip="8.8.8.8" 12 | #port=${possible_ports[$RANDOM % ${#possible_ports[@]}]} 13 | src_port=$port 14 | dst_port=$port 15 | logger -t attack-map-sample "$src_ip,$dst_ip,$src_port,$dst_port,ATTACK!!!$port,JOOMLA$port" 16 | sleep .2 17 | done 18 | done 19 | -------------------------------------------------------------------------------- /DataServerDB/db-dl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | wget http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz 4 | gunzip GeoLite2-City.mmdb.gz 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### First and Foremost 2 | I do not have much time in my life right now to maintain this project. I undertook this challenge as a means to learn JavaScript, and to improve upon the Python I already knew. I'm sure there are several things I could have done differently in my implementation, and I won't be offended if I receive constructive criticism from someone who downloads and views my code. I know I learned a ton when working on this, and I hope the open source community will continue to teach me things :) 3 | 4 | NOTE: Because I don't maintain this project there are a few features that probably don't work out the box, for example, the map probably won't display because I don't pay for a legitimate MapBox API key. To fix this you will probably have to create your own MapBox account and use your own key. 5 | 6 | ### Cyber Security GeoIP Attack Map Visualization 7 | This geoip attack map visualizer was developed to display network attacks on your organization in real time. The data server follows a syslog file, and parses out source IP, destination IP, source port, and destination port. Protocols are determined via common ports, and the visualizations vary in color based on protocol type. [CLICK HERE](https://www.youtube.com/watch?v=zTvLJjTzJnU) for a demo video. This project would not be possible if it weren't for Sam Cappella, who created a cyber defense competition network traffic visualizer for the 2015 Palmetto Cyber Defense Competition. I mainly used his code as a reference, but I did borrow a few functions while creating the display server, and visual aspects of the webapp. I would also like to give special thanks to [Dylan Madisetti](http://www.dylanmadisetti.com/) as well for giving me advice about certain aspects of my implementation. 8 | 9 | ### Important 10 | This program relies entirely on syslog, and because all appliances format logs differently, you will need to customize the log parsing function(s). If your organization uses a security information and event management system (SIEM), it can probably normalize logs to save you a ton of time writing regex. 11 | 1. Send all syslog to SIEM. 12 | 2. Use SIEM to normalize logs. 13 | 3. Send normalized logs to the box (any Linux machine running syslog-ng will work) running this software so the data server can parse them. 14 | 15 | ### Configs 16 | 1. Make sure in **/etc/redis/redis.conf** to change **bind 127.0.0.1** to **bind 0.0.0.0** if you plan on running the DataServer on a different machine than the AttackMapServer. 17 | 2. Make sure that the WebSocket address in **/AttackMapServer/index.html** points back to the IP address of the **AttackMapServer** so the browser knows the address of the WebSocket. 18 | 3. Download the MaxMind GeoLite2 database, and change the db_path variable in **DataServer.py** to the wherever you store the database. 19 | * ./db-dl.sh 20 | 4. Add headquarters latitude/longitude to hqLatLng variable in **index.html** 21 | 5. Use syslog-gen.py, or syslog-gen.sh to simulate dummy traffic "out of the box." 22 | 6. **IMPORTANT: Remember, this code will only run correctly in a production environment after personalizing the parsing functions. The default parsing function is only written to parse ./syslog-gen.sh traffic.** 23 | 24 | ### Bugs, Feedback, and Questions 25 | If you find any errors or bugs, please let me know. Questions and feedback are also welcome, and can be sent to mcmay.web@gmail.com, or open an issue in this repository. 26 | 27 | 28 | ### Deploy example 29 | Tested on Ubuntu 16.04 LTS. 30 | 31 | * Clone the application: 32 | 33 | ```sh 34 | git clone https://github.com/matthewclarkmay/geoip-attack-map.git 35 | ``` 36 | 37 | * Install system dependencies: 38 | 39 | ```sh 40 | sudo apt install python3-pip redis-server 41 | 42 | ``` 43 | 44 | * Install python requirements: 45 | 46 | ```sh 47 | cd geoip-attack-map 48 | sudo pip3 install -U -r requirements.txt 49 | 50 | ``` 51 | 52 | * Start Redis Server: 53 | 54 | ```sh 55 | redis-server 56 | 57 | ``` 58 | * Configure the Data Server DB: 59 | 60 | ```sh 61 | cd DataServerDB 62 | ./db-dl.sh 63 | cd .. 64 | 65 | ``` 66 | * Start the Data Server: 67 | 68 | ```sh 69 | cd DataServer 70 | sudo python3 DataServer.py 71 | 72 | ``` 73 | 74 | * Start the Syslog Gen Script, inside DataServer directory: 75 | 76 | * Open a new terminal tab (Ctrl+Shift+T, on Ubuntu). 77 | 78 | ```sh 79 | ./syslog-gen.py 80 | ./syslog-gen.sh 81 | ``` 82 | 83 | * Configure the Attack Map Server, extract the flags to the right place: 84 | 85 | * Open a new terminal tab (Ctrl+Shift+T, on Ubuntu). 86 | 87 | ```sh 88 | cd AttackMapServer/ 89 | unzip static/flags.zip 90 | ``` 91 | 92 | * Start the Attack Map Server: 93 | 94 | ```sh 95 | sudo python3 AttackMapServer.py 96 | ``` 97 | 98 | * Access the Attack Map Server from browser: 99 | 100 | * [http://localhost:8888/](http://localhost:8888/) or [http://127.0.0.1:8888/](http://127.0.0.1:8888/) 101 | 102 | * To access via browser on another computer, use the external IP of the machine running the AttackMapServer. 103 | 104 | * Edit the IP Address in the file "/static/map.js" at "AttackMapServer" directory. From: 105 | 106 | ```javascript 107 | var webSock = new WebSocket("ws:/127.0.0.1:8888/websocket"); 108 | ``` 109 | * To, for example: 110 | 111 | ```javascript 112 | var webSock = new WebSocket("ws:/192.168.1.100:8888/websocket"); 113 | ``` 114 | * Restart the Attack Map Server: 115 | 116 | ```sh 117 | sudo python3 AttackMapServer.py 118 | ``` 119 | * On the other computer, points the browser to: 120 | 121 | ```sh 122 | http://192.168.1.100:8888/ 123 | ``` 124 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | # Install system dependencies 4 | echo "Installing system dependencies..." 5 | sudo apt install python3-pip redis-server; 6 | 7 | # Install python requirements 8 | echo "Installing python dependencies..." 9 | sudo pip3 install -U -r requirements.txt 10 | 11 | # Configure the DataServer DB 12 | echo "Downloading geoip database..." 13 | cd DataServerDB 14 | ./db-dl.sh 15 | cd .. 16 | 17 | # Configure AttackMapServer, extract flags to the correct place 18 | echo "Configuring AttackMapServer..." 19 | cd AttackMapServer/static/ 20 | unzip flags.zip 21 | cd ../.. 22 | 23 | echo "" 24 | echo "Done configuring stuff!" 25 | echo "Don't forget to start the redis-server before starting DataServer.py, or AttackMapServer.py" 26 | echo "Enjoy!" 27 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | maxminddb==1.2.2 2 | redis==2.10.5 3 | tornado==4.4.2 4 | tornado-redis==2.4.18 5 | --------------------------------------------------------------------------------