├── .gitignore ├── server ├── requirements.txt ├── honeybadger │ ├── static │ │ ├── honey.jar │ │ ├── honeybadger.png │ │ ├── common.js │ │ ├── badger.css │ │ ├── badger.js │ │ ├── normalize.css │ │ ├── skeleton.css │ │ └── sorttable.js │ ├── templates │ │ ├── log.html │ │ ├── map.html │ │ ├── login.html │ │ ├── profile_activate.html │ │ ├── profile.html │ │ ├── register.html │ │ ├── beacons.html │ │ ├── demo.html │ │ ├── layout.html │ │ ├── admin.html │ │ └── targets.html │ ├── validators.py │ ├── utils.py │ ├── decorators.py │ ├── constants.py │ ├── __init__.py │ ├── plugins.py │ ├── models.py │ ├── processors.py │ ├── views.py │ └── parsers.py └── honeybadger.py ├── util ├── wireless_survey.sh └── wireless_survey.ps1 ├── README.md └── LICENSE.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *sublime* 3 | venv/ 4 | agents/ 5 | server_old/ 6 | data.db 7 | -------------------------------------------------------------------------------- /server/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | Flask-Bcrypt 3 | Flask-SQLAlchemy 4 | Flask-CORS 5 | requests 6 | -------------------------------------------------------------------------------- /server/honeybadger/static/honey.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adhdproject/honeybadger/HEAD/server/honeybadger/static/honey.jar -------------------------------------------------------------------------------- /server/honeybadger.py: -------------------------------------------------------------------------------- 1 | from honeybadger import app 2 | 3 | if __name__ == '__main__': 4 | app.run(host='0.0.0.0')#threaded=True) 5 | -------------------------------------------------------------------------------- /server/honeybadger/static/honeybadger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adhdproject/honeybadger/HEAD/server/honeybadger/static/honeybadger.png -------------------------------------------------------------------------------- /server/honeybadger/templates/log.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 |
5 |
6 |
{{ content }}
7 |
8 |
9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /server/honeybadger/validators.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | # 1 upper, 1 lower, 1 special, 1 number, minimim 10 chars 4 | PASSWORD_REGEX = r'(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])(?=.*[!@#$%^&*\(\)]).{10,}' 5 | EMAIL_REGEX = r'[^@]+@[a-zA-Z\d-]+(?:\.[a-zA-Z\d-]+)+' 6 | 7 | def is_valid_email(email): 8 | if not re.match(r'^{}$'.format(EMAIL_REGEX), email): 9 | return False 10 | return True 11 | 12 | def is_valid_password(password): 13 | if not re.match(r'^{}$'.format(PASSWORD_REGEX), password): 14 | return False 15 | return True 16 | -------------------------------------------------------------------------------- /server/honeybadger/templates/map.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 |
targets
5 |
6 |
7 |
8 |
agents
9 |
10 |
11 |
12 |
Loading beacons...
13 | 14 | 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /server/honeybadger/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 |
5 | 6 |
7 |
8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /server/honeybadger/static/common.js: -------------------------------------------------------------------------------- 1 | function flash(msg) { 2 | $('#flash').html(msg); 3 | $('#flash').css('visibility', 'visible'); 4 | setTimeout(function() { $('#flash').css('visibility', 'hidden'); }, 5000); 5 | } 6 | 7 | function copy2clip(s) { 8 | var dummy = document.createElement("input"); 9 | document.body.appendChild(dummy); 10 | dummy.setAttribute("id", "dummy_id"); 11 | document.getElementById("dummy_id").value = s; 12 | dummy.select(); 13 | try { 14 | document.execCommand("copy"); 15 | } catch (e) { 16 | console.log("Copy failed."); 17 | } 18 | document.body.removeChild(dummy); 19 | flash("Link copied."); 20 | } 21 | 22 | $(document).ready(function() { 23 | 24 | // flash on load if needed 25 | if($('#flash').html().length > 0) { 26 | flash($('#flash').html()); 27 | } 28 | 29 | }); 30 | -------------------------------------------------------------------------------- /server/honeybadger/templates/profile_activate.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 |
5 |
6 |
7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /server/honeybadger/templates/profile.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 |
5 |
6 |
7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /server/honeybadger/templates/register.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 |
5 | 6 |
7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |
18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /server/honeybadger/utils.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import os 3 | import base64 4 | import uuid 5 | 6 | def generate_guid(): 7 | return str(uuid.uuid4()) 8 | 9 | def generate_token(n=40): 10 | return binascii.hexlify(os.urandom(n)) 11 | 12 | def generate_nonce(n): 13 | return base64.b64encode(os.urandom(n)).decode() 14 | 15 | from honeybadger.constants import CHANNELS 16 | 17 | def freq2channel(freq): 18 | for channel in CHANNELS: 19 | if freq in CHANNELS[channel]: 20 | return channel 21 | 22 | from honeybadger.models import Log 23 | from honeybadger import db 24 | 25 | class Logger(object): 26 | 27 | def _log(self, l, s): 28 | log = Log( 29 | level=l, 30 | message=s, 31 | ) 32 | db.session.add(log) 33 | db.session.commit() 34 | 35 | def debug(self, s): 36 | self._log(10, s) 37 | 38 | def info(self, s): 39 | self._log(20, s) 40 | 41 | def warn(self, s): 42 | self._log(30, s) 43 | 44 | def error(self, s): 45 | self._log(40, s) 46 | 47 | def critical(self, s): 48 | self._log(50, s) 49 | -------------------------------------------------------------------------------- /util/wireless_survey.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Input error checking: Valid argument count? 4 | if [[ "$#" -ne 1 ]]; then 5 | echo "Usage: ./wireless_survey.sh " 6 | exit 1 7 | fi 8 | 9 | # Get wireless information 10 | wireless_data=$(iwlist scan 2>&1 | egrep 'Address|ESSID|Signal') 11 | 12 | # Create post data string 13 | post_data="os=linux&data=$(echo "$wireless_data" | base64 -w 0)" 14 | 15 | # Send the data to the URL supplied via command line argument. Store the response code. 16 | response_code=$(curl -d "$post_data" "$1/CMD" --write-out %{http_code} --silent --output /dev/null) 17 | curl_code="$?" 18 | 19 | # Output error checking: Expected response code? 20 | if [[ $response_code -eq "000" ]]; then 21 | echo "Unable to reach the specified URL. Curl response code: $curl_code." 22 | exit 1; 23 | elif [[ $response_code -eq "404" ]]; then 24 | echo "The requested server responded with 404." 25 | echo "Either the page really was not found, or the request was successful." 26 | echo "Check the HoneyBadger web client for data to verify." 27 | exit 0; 28 | else 29 | echo "The URL responded with an unexpected response code." 30 | echo "Check the URL and the HoneyBadger server, and try again." 31 | exit 1; 32 | fi 33 | -------------------------------------------------------------------------------- /server/honeybadger/decorators.py: -------------------------------------------------------------------------------- 1 | from flask import g, redirect, url_for, abort, make_response 2 | from honeybadger.constants import ROLES 3 | from functools import wraps 4 | from threading import Thread 5 | 6 | def login_required(func): 7 | @wraps(func) 8 | def wrapped(*args, **kwargs): 9 | if g.user: 10 | return func(*args, **kwargs) 11 | return redirect(url_for('login')) 12 | return wrapped 13 | 14 | def roles_required(*roles): 15 | def wrapper(func): 16 | @wraps(func) 17 | def wrapped(*args, **kwargs): 18 | if ROLES[g.user.role] not in roles: 19 | return abort(403) 20 | return func(*args, **kwargs) 21 | return wrapped 22 | return wrapper 23 | 24 | def async_wrapper(func): 25 | def wrapper(*args, **kwargs): 26 | thr = Thread(target=func, args=args, kwargs=kwargs) 27 | thr.start() 28 | return wrapper 29 | 30 | def no_cache(func): 31 | @wraps(func) 32 | def wrapped(*args, **kwargs): 33 | response = make_response(func(*args, **kwargs)) 34 | response.headers['Pragma'] = 'no-cache' 35 | response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' 36 | response.headers['Expires'] = '0' 37 | return response 38 | return wrapped 39 | -------------------------------------------------------------------------------- /server/honeybadger/templates/beacons.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 |
5 |
6 | {% if beacons|length > 0 %} 7 | 8 | 9 | 10 | {% for column in columns %} 11 | 12 | {% endfor %} 13 | 14 | 15 | 16 | 17 | {% for beacon in beacons %} 18 | 19 | {% for column in columns %} 20 | 21 | {% endfor %} 22 | 27 | 28 | {% endfor %} 29 | 30 |
{{ column }}action
{{ beacon[column] }} 23 | {% if g.user.is_admin %} 24 | 25 | {% endif %} 26 |
31 | {% else %} 32 |
No beacons.
33 | {% endif %} 34 |
35 |
36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /server/honeybadger/constants.py: -------------------------------------------------------------------------------- 1 | ROLES = { 2 | 0: 'admin', 3 | 1: 'analyst', 4 | } 5 | 6 | STATUSES = { 7 | 0: 'initialized', 8 | 1: 'active', 9 | 2: 'inactive', 10 | 3: 'reset', 11 | } 12 | 13 | LEVELS = { 14 | 10: 'DEBUG', 15 | 20: 'INFO', 16 | 30: 'WARN', 17 | 40: 'ERROR', 18 | 50: 'CRITICAL', 19 | } 20 | 21 | CHANNELS = { 22 | 1: range(2401, 2423+1), 23 | 2: range(2406, 2428+1), 24 | 3: range(2411, 2433+1), 25 | 4: range(2416, 2438+1), 26 | 5: range(2421, 2443+1), 27 | 6: range(2426, 2448+1), 28 | 7: range(2431, 2453+1), 29 | 8: range(2436, 2458+1), 30 | 9: range(2441, 2463+1), 31 | 10: range(446, 2468+1), 32 | 11: range(451, 2473+1), 33 | 12: range(456, 2478+1), 34 | 13: range(461, 2483+1), 35 | 14: range(473, 2495+1), 36 | 36: range(5180, 5180+1), 37 | 40: range(5200, 5200+1), 38 | 44: range(5220, 5220+1), 39 | 48: range(5240, 5240+1), 40 | 52: range(5260, 5260+1), 41 | 56: range(5280, 5280+1), 42 | 60: range(5300, 5300+1), 43 | 64: range(5320, 5320+1), 44 | 100: range(5500, 5500+1), 45 | 104: range(5520, 5520+1), 46 | 108: range(5540, 5540+1), 47 | 112: range(5560, 5560+1), 48 | 116: range(5580, 5580+1), 49 | 120: range(5600, 5600+1), 50 | 124: range(5620, 5620+1), 51 | 128: range(5640, 5640+1), 52 | 132: range(5660, 5660+1), 53 | 136: range(5680, 5680+1), 54 | 140: range(5700, 5700+1), 55 | 149: range(5745, 5745+1), 56 | 153: range(5765, 5765+1), 57 | 157: range(5785, 5785+1), 58 | 161: range(5805, 5805+1), 59 | 165: range(5825, 5825+1), 60 | } 61 | -------------------------------------------------------------------------------- /util/wireless_survey.ps1: -------------------------------------------------------------------------------- 1 | # Accept a command-line argument for the URI 2 | param ( 3 | [string]$uri = "" 4 | ) 5 | 6 | #Input error checking: Valid argument name/value? 7 | if($uri -eq "") { 8 | echo "Usage: .\wireless_survey.ps1 -uri " 9 | exit 1 10 | } 11 | 12 | # Poll for wireless network information with Netsh 13 | $wifi = netsh wlan show networks mode=bssid | findstr "SSID Signal Channel" 14 | 15 | # Fix data before Base64-encoding to preserve line breaks. 16 | # The server-side parser breaks without this. 17 | echo $wifi > wifidat.txt # Write netsh results to temporary file 18 | $wifi = Get-Content wifidat.txt -Encoding UTF8 -Raw # Update wifi variable with Raw switch 19 | rm wifidat.txt # Remove temporary file 20 | 21 | # Set encoding of wifi data 22 | $wifibytes = [System.Text.Encoding]::UTF8.GetBytes($wifi) 23 | 24 | # Base64-encode the wifi data bytes 25 | $wifienc = [Convert]::ToBase64String($wifibytes) 26 | 27 | # Assemble post data 28 | $postdat = @{os='windows';data=$wifienc} 29 | 30 | # Send POST request to server, using CMD as agent 31 | try { 32 | $statuscode = (Invoke-WebRequest -Uri "$uri/CMD" -Method POST -Body $postdat).statuscode 33 | } catch { 34 | $statuscode = $_.Exception.Response.StatusCode.Value__ 35 | } 36 | 37 | # Output error checking: Expected response code? 38 | if ([string]::IsNullOrEmpty($statuscode)){ 39 | echo "Unable to reach the specified URI." 40 | echo "Check the URI and the HoneyBadger server, and try again." 41 | exit 1 42 | } elseif($statuscode -eq 404) { 43 | echo "The requested server responded with 404." 44 | echo "Either the page really was not found, or the request was successful." 45 | echo "Check the HoneyBadger web client for data to verify." 46 | exit 0 47 | } else { 48 | echo "The requested server responded with an unexpected response code." 49 | echo "Check the URI and the HoneyBadger server, and try again." 50 | exit 1 51 | } 52 | -------------------------------------------------------------------------------- /server/honeybadger/templates/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Honey Badger Demo 5 | 47 | 48 | 49 |

Honey Badger demo page.

50 |

XSS me, please.

51 |
52 |

53 |

54 |

55 |
56 | {% if text %} 57 |

{{ text|safe }}

58 | {% endif %} 59 | 60 | 61 | -------------------------------------------------------------------------------- /server/honeybadger/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Honey Badger 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
{{ get_flashed_messages()|join(', ') }}
19 | 48 | {% block body %}{% endblock %} 49 | 50 | 51 | -------------------------------------------------------------------------------- /server/honeybadger/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_bcrypt import Bcrypt 3 | from flask_sqlalchemy import SQLAlchemy 4 | import logging 5 | import os 6 | import argparse 7 | 8 | parser = argparse.ArgumentParser() 9 | parser.add_argument("-gk", "--googlekey", dest="googlekey", type=str, default='', 10 | help="Google API Key") 11 | parser.add_argument("-ik", "--ipstackkey", dest="ipstackkey", type=str, default='', 12 | help="IPStack API Key") 13 | 14 | opts = parser.parse_args() 15 | 16 | basedir = os.path.abspath(os.path.dirname(__file__)) 17 | 18 | # configuration 19 | SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'data.db') 20 | DEBUG = True 21 | SECRET_KEY = 'development key' 22 | SQLALCHEMY_TRACK_MODIFICATIONS = False 23 | GOOGLE_API_KEY = opts.googlekey # Provide your google api key via command-line argument 24 | IPSTACK_API_KEY = opts.ipstackkey 25 | 26 | app = Flask(__name__) 27 | app.config.from_object(__name__) 28 | bcrypt = Bcrypt(app) 29 | db = SQLAlchemy(app) 30 | # Logger cannot be imported until the db is initialized 31 | from honeybadger.utils import Logger 32 | logger = Logger() 33 | 34 | if __name__ != '__main__': 35 | gunicorn_logger = logging.getLogger('gunicorn.error') 36 | # only use handler if gunicorn detected, otherwise default 37 | if gunicorn_logger.handlers: 38 | app.logger.handlers = gunicorn_logger.handlers 39 | app.logger.setLevel(gunicorn_logger.level) 40 | 41 | from honeybadger import models 42 | from honeybadger import views 43 | 44 | def initdb(username, password): 45 | db.create_all() 46 | import binascii 47 | u = models.User(email=username, password_hash=bcrypt.generate_password_hash(binascii.hexlify(password.encode())), role=0, status=1) 48 | db.session.add(u) 49 | db.session.commit() 50 | print('Database initialized.') 51 | # remove below for production 52 | t = models.Target(name='demo', guid='aedc4c63-8d13-4a22-81c5-d52d32293867') 53 | db.session.add(t) 54 | db.session.commit() 55 | b = models.Beacon(target_guid='aedc4c63-8d13-4a22-81c5-d52d32293867', agent='HTML', ip='1.2.3.4', port='80', useragent='Mac OS X', comment='this is a comment.', lat='38.2531419', lng='-85.7564855', acc='5') 56 | db.session.add(b) 57 | db.session.commit() 58 | b = models.Beacon(target_guid='aedc4c63-8d13-4a22-81c5-d52d32293867', agent='HTML', ip='5.6.7.8', port='80', useragent='Mac OS X', comment='this is a comment.', lat='34.855117', lng='-82.114192', acc='1') 59 | db.session.add(b) 60 | db.session.commit() 61 | 62 | def dropdb(): 63 | db.drop_all() 64 | print('Database dropped.') 65 | -------------------------------------------------------------------------------- /server/honeybadger/templates/admin.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 |
5 |
6 |
7 | 8 | 9 |
10 |
11 |
12 |
13 |
14 | {% if users|length > 0 %} 15 | 16 | 17 | 18 | {% for column in columns %} 19 | 20 | {% endfor %} 21 | 22 | 23 | 24 | 25 | {% for user in users %} 26 | 27 | {% for column in columns %} 28 | 29 | {% endfor %} 30 | 43 | 44 | {% endfor %} 45 | 46 |
{{ column }}action
{{ user[column] }} 31 | {% if user.status == 0 %} 32 | 33 | {% elif user.status == 1 %} 34 | 35 | 36 | {% elif user.status == 2 %} 37 | 38 | {% elif user.status == 3 %} 39 | 40 | {% endif %} 41 | 42 |
47 | {% else %} 48 |
No users.
49 | {% endif %} 50 |
51 |
52 | {% endblock %} 53 | -------------------------------------------------------------------------------- /server/honeybadger/plugins.py: -------------------------------------------------------------------------------- 1 | from honeybadger import app, logger 2 | import requests 3 | import json 4 | 5 | def get_coords_from_google(aps): 6 | logger.info('Geolocating via Google Geolocation API.') 7 | url = 'https://www.googleapis.com/geolocation/v1/geolocate?key={}'.format(app.config['GOOGLE_API_KEY']) 8 | data = {"wifiAccessPoints": []} 9 | for ap in aps: 10 | data['wifiAccessPoints'].append(ap.serialized_for_google) 11 | data_json = json.dumps(data) 12 | headers = {'Content-Type': 'application/json'} 13 | response = requests.post(url=url, data=data_json, headers=headers) 14 | logger.info("Google API response: {}".format(response.content)) 15 | jsondata = None 16 | try: 17 | jsondata = response.json() 18 | except ValueError as e: 19 | logger.error('{}.'.format(e)) 20 | data = {'lat':None, 'lng':None, 'acc':None} 21 | if jsondata: 22 | data['acc'] = jsondata['accuracy'] 23 | data['lat'] = jsondata['location']['lat'] 24 | data['lng'] = jsondata['location']['lng'] 25 | return data 26 | 27 | def get_coords_from_ipstack(ip): 28 | logger.info('Geolocating via Ipstack API.') 29 | url = 'http://api.ipstack.com/{0}?access_key={1}'.format(ip, app.config['IPSTACK_API_KEY']) 30 | response = requests.get(url) 31 | logger.info('Ipstack API response:\n{}'.format(response.content)) 32 | jsondata = None 33 | try: 34 | jsondata = response.json() 35 | except ValueError as e: 36 | logger.error('{}.'.format(e)) 37 | 38 | data = {'lat':None, 'lng':None} 39 | 40 | # Avoid the KeyError. For some reason, a successful API call to Ipstack doesn't include 41 | # the 'success' key in the json result, but a failed call does, and the value is False 42 | if 'success' in jsondata and not jsondata['success']: 43 | logger.info('Ipstack API call failed: {}'.format(jsondata['error']['type'])) 44 | # Return with empty data so the caller knows to default to the fallback API 45 | return data 46 | 47 | if jsondata: 48 | data['lat'] = jsondata['latitude'] 49 | data['lng'] = jsondata['longitude'] 50 | return data 51 | 52 | def get_coords_from_ipinfo(ip): 53 | # New fallback, ipinfo doesn't require an API key for a certain number of API calls 54 | logger.info('Geolocating via Ipinfo.io API.') 55 | url = 'https://ipinfo.io/{}'.format(ip) 56 | response = requests.get(url) 57 | logger.info('Ipinfo.io API response:\n{}'.format(response.content)) 58 | jsondata = None 59 | try: 60 | jsondata = response.json() 61 | except ValueError as e: 62 | logger.error('{}.'.format(e)) 63 | data = {'lat':None, 'lng':None} 64 | if jsondata and 'loc' in jsondata: 65 | data['lat'] = jsondata['loc'].split(',')[0] 66 | data['lng'] = jsondata['loc'].split(',')[1] 67 | if 'bogon' in jsondata and jsondata['bogon']: 68 | logger.info('Ipinfo.io cannot geolocate IP {}'.format(ip)) 69 | return data 70 | -------------------------------------------------------------------------------- /server/honeybadger/models.py: -------------------------------------------------------------------------------- 1 | from honeybadger import db, bcrypt 2 | from honeybadger.constants import ROLES, STATUSES, LEVELS 3 | from honeybadger.utils import generate_guid 4 | import binascii 5 | import datetime 6 | 7 | def stringify_datetime(value): 8 | """Deserialize datetime object into string form for JSON processing.""" 9 | if value is None: 10 | return None 11 | return value.strftime("%Y-%m-%d %H:%M:%S") 12 | 13 | class BaseModel(db.Model): 14 | __abstract__ = True 15 | id = db.Column(db.Integer, primary_key=True) 16 | created = db.Column(db.DateTime, nullable=False, default=datetime.datetime.now) 17 | 18 | @property 19 | def created_as_string(self): 20 | return stringify_datetime(self.created) 21 | 22 | class Log(BaseModel): 23 | __tablename__ = 'logs' 24 | level = db.Column(db.Integer, nullable=False) 25 | message = db.Column(db.String) 26 | 27 | @property 28 | def level_as_string(self): 29 | return LEVELS[self.level] 30 | 31 | class Beacon(BaseModel): 32 | __tablename__ = 'beacons' 33 | target_guid = db.Column(db.String, db.ForeignKey('targets.guid'), nullable=False) 34 | agent = db.Column(db.String) 35 | ip = db.Column(db.String) 36 | port = db.Column(db.String) 37 | useragent = db.Column(db.String) 38 | comment = db.Column(db.String) 39 | lat = db.Column(db.String) 40 | lng = db.Column(db.String) 41 | acc = db.Column(db.String) 42 | 43 | @property 44 | def serialized(self): 45 | """Return object data in easily serializeable format""" 46 | return { 47 | 'id': self.id, 48 | 'created': stringify_datetime(self.created), 49 | 'target': self.target.name, 50 | 'agent': self.agent, 51 | 'ip': self.ip, 52 | 'port': self.port, 53 | 'useragent': self.useragent, 54 | 'comment': self.comment, 55 | 'lat': self.lat, 56 | 'lng': self.lng, 57 | 'acc': self.acc, 58 | } 59 | 60 | def __repr__(self): 61 | return "".format(self.target.name) 62 | 63 | class Target(BaseModel): 64 | __tablename__ = 'targets' 65 | name = db.Column(db.String) 66 | guid = db.Column(db.String, default=generate_guid) 67 | beacons = db.relationship('Beacon', cascade="all,delete", backref='target', lazy='dynamic') 68 | 69 | @property 70 | def beacon_count(self): 71 | return len(self.beacons.all()) 72 | 73 | def __repr__(self): 74 | return "".format(self.name) 75 | 76 | class User(BaseModel): 77 | __tablename__ = 'users' 78 | email = db.Column(db.String, nullable=False, unique=True) 79 | password_hash = db.Column(db.String) 80 | role = db.Column(db.Integer, nullable=False, default=1) 81 | status = db.Column(db.Integer, nullable=False, default=0) 82 | token = db.Column(db.String) 83 | 84 | @property 85 | def role_as_string(self): 86 | return ROLES[self.role] 87 | 88 | @property 89 | def status_as_string(self): 90 | return STATUSES[self.status] 91 | 92 | @property 93 | def password(self): 94 | raise AttributeError('password: write-only field') 95 | 96 | @password.setter 97 | def password(self, password): 98 | self.password_hash = bcrypt.generate_password_hash(binascii.hexlify(password.encode())) 99 | 100 | def check_password(self, password): 101 | return bcrypt.check_password_hash(self.password_hash, binascii.hexlify(password.encode())) 102 | 103 | @property 104 | def is_admin(self): 105 | if self.role == 0: 106 | return True 107 | return False 108 | 109 | @staticmethod 110 | def get_by_email(email): 111 | return User.query.filter_by(email=email).first() 112 | 113 | def __repr__(self): 114 | return "".format(self.email) 115 | -------------------------------------------------------------------------------- /server/honeybadger/static/badger.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | font-family: Raleway, Arial, Helvetica, sans-serif; 7 | font-size: 1.5rem; 8 | height: 100%; 9 | margin: 0; 10 | padding: 0; 11 | } 12 | 13 | td input, td select { 14 | vertical-align: middle; 15 | margin-top: .5rem; 16 | margin-bottom: .5rem; 17 | } 18 | 19 | /* overwrite skeleton button style */ 20 | .button, 21 | button, 22 | input[type="submit"], 23 | input[type="reset"], 24 | input[type="button"] { 25 | font-size: 1.25rem; 26 | color: white; 27 | background-color: #f69741; 28 | border-color: #bbb; 29 | padding: 0 1rem; 30 | } 31 | .button:hover, 32 | button:hover, 33 | input[type="submit"]:hover, 34 | input[type="reset"]:hover, 35 | input[type="button"]:hover, 36 | .button:focus, 37 | button:focus, 38 | input[type="submit"]:focus, 39 | input[type="reset"]:focus, 40 | input[type="button"]:focus { 41 | color: white; 42 | background-color: #f4821c; 43 | border-color: 0; 44 | } 45 | 46 | .flash { 47 | color: white; 48 | position: absolute; 49 | z-index: 100; 50 | top: 10px; 51 | left: 50%; 52 | transform: translateX(-50%); 53 | height: 2rem; 54 | padding: 5px 10px; 55 | font-size: 1.5rem; 56 | text-align: center; 57 | line-height: 2rem; 58 | visibility: hidden; 59 | } 60 | 61 | .nav { 62 | display: table; 63 | /*color: red;*/ 64 | width: 100%; 65 | padding: 0; 66 | text-align: right; 67 | } 68 | 69 | .nav div{ 70 | display: table-cell; 71 | vertical-align: middle; 72 | } 73 | 74 | .nav .brand { 75 | float: left; 76 | font-size: 3rem; 77 | margin-left: 1rem 78 | } 79 | 80 | .nav .links { 81 | font-weight: bolder; 82 | padding-right: 2rem 83 | } 84 | 85 | .nav ul { 86 | list-style-type: none; 87 | margin: 0; 88 | padding: 0; 89 | } 90 | 91 | .nav li { 92 | display: inline; 93 | } 94 | 95 | .nav a { 96 | color: inherit; 97 | text-decoration: none; 98 | } 99 | 100 | .login { 101 | padding: 50px 20px; 102 | } 103 | 104 | .login img { 105 | display: block; 106 | margin-left: auto; 107 | margin-right: auto; 108 | width: 100%; 109 | height: auto; 110 | } 111 | 112 | .login div { 113 | margin-left: auto; 114 | margin-right: auto; 115 | padding: 10px; 116 | } 117 | 118 | .login form { 119 | margin: 0; 120 | } 121 | 122 | .login label { 123 | color: white; 124 | } 125 | 126 | .login a { 127 | text-decoration: underline; 128 | } 129 | 130 | .login p { 131 | margin-bottom: 1.5rem; 132 | } 133 | 134 | .map { 135 | display: block; 136 | position: absolute; 137 | height: auto; 138 | bottom: 0; 139 | top: 0; 140 | left: 0; 141 | right: 0; 142 | margin-top: 5rem; 143 | border-top: 2px solid #f69741; 144 | } 145 | 146 | .iw-content { 147 | max-width: 300px; 148 | margin-bottom: 0; 149 | } 150 | 151 | .iw-content caption { 152 | background-color: #f69741; 153 | color: white; 154 | font-size: 2.5rem; 155 | font-weight: 400; 156 | } 157 | 158 | .iw-content td { 159 | padding-top: 0; 160 | padding-bottom: 0; 161 | } 162 | 163 | .beacons th, 164 | .beacons td, 165 | .targets th, 166 | .targets td, 167 | .users th, 168 | .users td { 169 | text-align: center; 170 | } 171 | 172 | .log pre { 173 | display: inline-block; 174 | text-align: left; 175 | /*white-space: pre-wrap;*/ 176 | width: 100%; 177 | max-width: 1200px; 178 | height: 100vh; 179 | padding: 1rem 1.5rem; 180 | margin: 0 1rem; 181 | border: 1px solid #bbb; 182 | } 183 | 184 | .filter { 185 | color: white; 186 | position: absolute; 187 | top: 6rem; 188 | left: 1rem; 189 | z-index: 99; 190 | padding: 10px; 191 | } 192 | 193 | .filter hr { 194 | border-color: #f69741; 195 | margin: 0; 196 | } 197 | 198 | .filter input { 199 | margin: 0; 200 | } 201 | 202 | .form { 203 | border: 2px solid #f69741; 204 | /* Fallback for web browsers that doesn't support RGBa */ 205 | background: rgb(0, 0, 0); 206 | /* RGBa opacity */ 207 | background: rgba(0, 0, 0, 0.6); 208 | } 209 | 210 | .rounded { 211 | -webkit-border-radius: 10px; 212 | -moz-border-radius: 10px; 213 | border-radius: 10px; 214 | } 215 | 216 | .shaded { 217 | border: 1px solid orange; 218 | -webkit-box-shadow: 3px 3px 3px gray; 219 | -moz-box-shadow: 3px 3px 3px gray; 220 | box-shadow: 3px 3px 3px gray; 221 | } 222 | 223 | .orange { 224 | color: #f69741; 225 | } 226 | 227 | .center { 228 | margin-left: auto; 229 | margin-right: auto; 230 | } 231 | 232 | .center-content { 233 | text-align: center; 234 | } 235 | -------------------------------------------------------------------------------- /server/honeybadger/processors.py: -------------------------------------------------------------------------------- 1 | from honeybadger import db, logger 2 | from honeybadger.models import Beacon 3 | from honeybadger.parsers import parse_airport, parse_netsh, parse_iwlist, parse_google 4 | from honeybadger.plugins import get_coords_from_google, get_coords_from_ipstack, get_coords_from_ipinfo 5 | from base64 import b64decode as b64d 6 | import re 7 | 8 | def add_beacon(*args, **kwargs): 9 | b = Beacon(**kwargs) 10 | db.session.add(b) 11 | db.session.commit() 12 | logger.info('Target location identified as Lat: {}, Lng: {}'.format(kwargs['lat'], kwargs['lng'])) 13 | 14 | def process_json(data, jsondata): 15 | logger.info('Processing JSON data.') 16 | logger.info('Data received:\n{}'.format(jsondata)) 17 | # process Google device data 18 | if jsondata.get('scan_results'): 19 | aps = parse_google(jsondata['scan_results']) 20 | if aps: 21 | logger.info('Parsed access points: {}'.format(aps)) 22 | coords = get_coords_from_google(aps) 23 | if all([x for x in coords.values()]): 24 | add_beacon( 25 | target_guid=data['target'], 26 | agent=data['agent'], 27 | ip=data['ip'], 28 | port=data['port'], 29 | useragent=data['useragent'], 30 | comment=data['comment'], 31 | lat=coords['lat'], 32 | lng=coords['lng'], 33 | acc=coords['acc'], 34 | ) 35 | return True 36 | else: 37 | logger.error('Invalid coordinates data.') 38 | else: 39 | # handle empty data 40 | logger.info('No AP data received.') 41 | else: 42 | # handle unrecognized data 43 | logger.info('Unrecognized data received from the agent.') 44 | 45 | def process_known_coords(data): 46 | logger.info('Processing known coordinates.') 47 | add_beacon( 48 | target_guid=data['target'], 49 | agent=data['agent'], 50 | ip=data['ip'], 51 | port=data['port'], 52 | useragent=data['useragent'], 53 | comment=data['comment'], 54 | lat=data['lat'], 55 | lng=data['lng'], 56 | acc=data['acc'], 57 | ) 58 | return True 59 | 60 | def process_wlan_survey(data): 61 | logger.info('Processing wireless survey data.') 62 | os = data['os'] 63 | _data = data['data'] 64 | content = b64d(_data).decode() 65 | logger.info('Data received:\n{}'.format(_data)) 66 | logger.info('Decoded Data:\n{}'.format(content)) 67 | if _data: 68 | aps = [] 69 | if re.search('^mac os x', os.lower()): 70 | aps = parse_airport(content) 71 | elif re.search('^windows', os.lower()): 72 | aps = parse_netsh(content) 73 | elif re.search('^linux', os.lower()): 74 | aps = parse_iwlist(content) 75 | # handle recognized data 76 | if aps: 77 | logger.info('Parsed access points: {}'.format(aps)) 78 | coords = get_coords_from_google(aps) 79 | if all([x for x in coords.values()]): 80 | add_beacon( 81 | target_guid=data['target'], 82 | agent=data['agent'], 83 | ip=data['ip'], 84 | port=data['port'], 85 | useragent=data['useragent'], 86 | comment=data['comment'], 87 | lat=coords['lat'], 88 | lng=coords['lng'], 89 | acc=coords['acc'], 90 | ) 91 | return True 92 | else: 93 | logger.error('Invalid coordinates data.') 94 | else: 95 | # handle unrecognized data 96 | logger.info('No parsable WLAN data received.') 97 | else: 98 | # handle blank data 99 | logger.info('No data received from the agent.') 100 | return False 101 | 102 | def process_ip(data): 103 | logger.info('Processing IP address.') 104 | coords = get_coords_from_ipstack(data['ip']) 105 | if not all([x for x in coords.values()]): 106 | # No data. try again with the fallback. 107 | logger.info('Using fallback API.') 108 | coords = get_coords_from_ipinfo(data['ip']) 109 | 110 | if all([x for x in coords.values()]): 111 | add_beacon( 112 | target_guid=data['target'], 113 | agent=data['agent'], 114 | ip=data['ip'], 115 | port=data['port'], 116 | useragent=data['useragent'], 117 | comment=data['comment'], 118 | lat=coords['lat'], 119 | lng=coords['lng'], 120 | acc='Unknown', 121 | ) 122 | return True 123 | else: 124 | logger.error('Invalid coordinates data.') 125 | return False 126 | -------------------------------------------------------------------------------- /server/honeybadger/templates/targets.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 | {% if g.user.is_admin %} 5 |
6 |
7 |
8 | 9 | 10 |
11 |
12 |
13 | {% endif %} 14 |
15 |
16 | 40 | {% if targets|length > 0 %} 41 | 42 | 43 | 44 | {% for column in columns %} 45 | 46 | {% endfor %} 47 | 48 | 49 | 50 | 51 | {% for target in targets %} 52 | 53 | {% for column in columns %} 54 | 55 | {% endfor %} 56 | 111 | 112 | {% endfor %} 113 | 114 |
{{ column }}action
{{ target[column] }} 57 | 76 | 103 | 104 | 105 | 106 | 107 | {% if g.user.is_admin %} 108 | 109 | {% endif %} 110 |
115 | {% else %} 116 |
No targets.
117 | {% endif %} 118 |
119 |
120 | {% endblock %} 121 | -------------------------------------------------------------------------------- /server/honeybadger/static/badger.js: -------------------------------------------------------------------------------- 1 | var map 2 | var bounds 3 | 4 | function load_map() { 5 | var coords = new google.maps.LatLng(0,0); 6 | var mapOptions = { 7 | zoom: 5, 8 | center: coords, 9 | mapTypeId: google.maps.MapTypeId.ROADMAP, 10 | disableDefaultUI: true, 11 | mapTypeControl: true, 12 | mapTypeControlOptions: { 13 | style: google.maps.MapTypeControlStyle.DROPDOWN_MENU, 14 | position: google.maps.ControlPosition.RIGHT_TOP 15 | }, 16 | panControl: true, 17 | panControlOptions: { 18 | position: google.maps.ControlPosition.RIGHT_BOTTOM 19 | }, 20 | streetViewControl: true, 21 | streetViewControlOptions: { 22 | position: google.maps.ControlPosition.RIGHT_BOTTOM 23 | }, 24 | zoomControl: true, 25 | zoomControlOptions: { 26 | position: google.maps.ControlPosition.RIGHT_BOTTOM 27 | }, 28 | }; 29 | map = new google.maps.Map(document.getElementById("map"), mapOptions); 30 | bounds = new google.maps.LatLngBounds(); 31 | } 32 | 33 | function add_marker(opts, place, beacon) { 34 | var marker = new google.maps.Marker(opts); 35 | var infowindow = new google.maps.InfoWindow({ 36 | autoScroll: false, 37 | content: place.details 38 | }); 39 | google.maps.event.addListener(marker, 'click', function() { 40 | infowindow.open(map,marker); 41 | }); 42 | // add the beacon data to its marker object 43 | marker.beacon = beacon; 44 | window['markers'].push(marker); 45 | bounds.extend(opts.position); 46 | return marker; 47 | } 48 | 49 | function load_markers(json) { 50 | var targets = []; 51 | var agents = []; 52 | for (var i = 0; i < json['beacons'].length; i++) { 53 | beacon = json['beacons'][i]; 54 | // add the marker to the map 55 | var coords = beacon.lat+','+beacon.lng 56 | var comment = beacon.comment || ''; 57 | var marker = add_marker({ 58 | position: new google.maps.LatLng(beacon.lat,beacon.lng), 59 | title:beacon.ip+":"+beacon.port, 60 | map:map 61 | },{ 62 | details:'' 63 | + '' 64 | + '' 65 | + '' 66 | + '' 67 | + '' 68 | + '' 69 | + '' 70 | + '
'+beacon.target+'
Agent:'+beacon.agent+' @ '+beacon.ip+':'+beacon.port+'
Time:'+beacon.created+'
User-Agent:'+beacon.useragent+'
Coordinates:'+coords+'
Accuracy:'+beacon.acc+'
Comment:'+comment+'
' 71 | }, 72 | beacon 73 | ); 74 | // add filter checkboxes for each unique target 75 | if (targets.indexOf(beacon.target) === -1) { 76 | var checkbox = document.createElement('input'); 77 | checkbox.type = 'checkbox'; 78 | checkbox.name = 'target'; 79 | checkbox.value = beacon.target; 80 | checkbox.setAttribute('checked', 'checked'); 81 | checkbox.checked = true; 82 | checkbox.addEventListener('change', function(e) { 83 | toggle_marker(e.target); 84 | }); 85 | var filter = document.getElementById('filter-target'); 86 | filter.appendChild(checkbox); 87 | filter.appendChild(document.createTextNode(' '+beacon.target)); 88 | filter.appendChild(document.createElement('br')); 89 | targets.push(beacon.target); 90 | } 91 | // add filter checkboxes for each unique agent 92 | if (agents.indexOf(beacon.agent) === -1) { 93 | var checkbox = document.createElement('input'); 94 | checkbox.type = 'checkbox'; 95 | checkbox.name = 'agent'; 96 | checkbox.value = beacon.agent; 97 | checkbox.setAttribute('checked', 'checked'); 98 | checkbox.checked = true; 99 | checkbox.addEventListener('change', function(e) { 100 | toggle_marker(e.target); 101 | }); 102 | var filter = document.getElementById('filter-agent'); 103 | filter.appendChild(checkbox); 104 | filter.appendChild(document.createTextNode(' '+beacon.agent)); 105 | filter.appendChild(document.createElement('br')); 106 | agents.push(beacon.agent); 107 | } 108 | } 109 | map.fitBounds(bounds); 110 | } 111 | 112 | // set the map on all markers in the array 113 | function toggle_marker(element) { 114 | _map = null; 115 | if(element.checked) { 116 | _map = map; 117 | } 118 | for (var i = 0; i < window['markers'].length; i++) { 119 | if (window['markers'][i].beacon[element.name] === element.value) { 120 | window['markers'][i].setMap(_map); 121 | } 122 | } 123 | } 124 | 125 | $(document).ready(function() { 126 | 127 | // load the map 128 | load_map(); 129 | 130 | // load the beacons 131 | $.ajax({ 132 | type: "GET", 133 | url: "/api/beacons", 134 | success: function(data) { 135 | // declare a storage array for markers 136 | window['markers'] = []; 137 | load_markers(data); 138 | flash("Markers loaded successfully."); 139 | }, 140 | error: function(error) { 141 | console.log(error) 142 | flash(error.message); 143 | } 144 | }); 145 | 146 | /*var sse = new EventSource("/subscribe"); 147 | sse.onmessage = function(e) { 148 | console.log(e.data); 149 | load_markers(JSON.parse(e.data)); 150 | };*/ 151 | 152 | }); 153 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HoneyBadger v3 2 | 3 | HoneyBadger is a framework for targeted geolocation. While honeypots are traditionally used to passively detect malicious actors, HoneyBadger is an Active Defense tool to determine who the malicious actor is and where they are located. HoneyBadger leverages "agents", built in various technologies that harvest the requisite information from the target host in order to geolocate them. These agents report back to the HoneyBadger API, where the data is stored and made available in the HoneyBadger user interface. 4 | 5 | An early prototype of HoneyBadger (v1) can be seen in the presentation "[Hide and Seek: Post-Exploitation Style](http://youtu.be/VJTrRMqHU5U)" from ShmooCon 2013. The associated Metasploit Framework modules mentioned in the above presentation can be found [here](https://github.com/v10l3nt/metasploit-framework/tree/master/modules/auxiliary/badger). Note: These modules have not been updated to work with v2 of the API. 6 | 7 | ## Getting Started 8 | 9 | ### Pre-requisites 10 | 11 | * Python 3.x 12 | 13 | ### Installation (Ubuntu and OS X) 14 | 15 | 1. Install [pip](https://pip.pypa.io/en/stable/installing/). (Make sure to use `pip3` if you also have Python2 installed) 16 | 2. Clone the HoneyBadger repository. 17 | 18 | ``` 19 | $ git clone https://github.com/adhdproject/honeybadger.git 20 | ``` 21 | 22 | 3. Install the dependencies. 23 | 24 | ``` 25 | $ cd honeybadger/server 26 | $ pip install -r requirements.txt 27 | ``` 28 | 29 | 4. Initialize the database. The provided username and password will become the administrator account. 30 | 31 | ``` 32 | $ python 33 | >>> import honeybadger 34 | >>> honeybadger.initdb("", "") 35 | ``` 36 | 37 | 5. Start the HoneyBadger server. API keys are required to use maps and geolocation services. 38 | 39 | ``` 40 | $ python ./honeybadger.py -gk -ik 41 | ``` 42 | 43 | Honeybadger will still run without the API keys, but mapping and geolocation functionality will be limited as a result. 44 | 45 | View usage information with either of the following: 46 | 47 | ``` 48 | $ python ./honeybadger.py -h 49 | $ python ./honeybadger.py --help 50 | ``` 51 | 52 | 53 | 6. Visit the application and authenticate. 54 | 7. Add users and targets as needed using their respective pages. 55 | 8. Deploy agents for the desired target. 56 | 57 | Clicking the "demo" button next to any of the targets will launch a demo web page containing an `HTML`, `JavaScript`, and `Applet` agent for that target. 58 | 59 | ### Fresh Start 60 | 61 | Make a mess and want to start over fresh? Do this. 62 | 63 | ``` 64 | $ python 65 | >>> import honeybadger 66 | >>> honeybadger.dropdb() 67 | >>> honeybadger.initdb(, ) 68 | ``` 69 | 70 | ## API Usage 71 | 72 | ### IP Geolocation 73 | 74 | This method geolocates the target based on the source IP of the request and assigns the resolved location to the given target and agent. 75 | 76 | Example: (Method: `GET`) 77 | 78 | ``` 79 | http:///api/beacon// 80 | ``` 81 | 82 | ### Known Coordinates 83 | 84 | This method accepts previously resolved location data for the given target and agent. 85 | 86 | Example: (Method: `GET`) 87 | 88 | ``` 89 | http:///api/beacon//?lat=&lng=&acc= 90 | ``` 91 | 92 | ### Wireless Survey 93 | 94 | This method accepts wireless survey data and parses the information on the server-side, extracting what is needed to make a Google API geolocation call. The resolved geolocation data is then assigned to the given target. Parsers currently exist for survey data from Windows, Linux and OS X using the following commands: 95 | 96 | Windows: 97 | 98 | ``` 99 | cmd.exe /c netsh wlan show networks mode=bssid | findstr "SSID Signal Channel" 100 | ``` 101 | 102 | There is a powershell script in the util directory that can be used to automatically send data to the server: 103 | ``` 104 | powershell .\wireless_survey.ps1 -uri 105 | ``` 106 | 107 | Linux: 108 | 109 | ``` 110 | /bin/sh -c iwlist scan | egrep 'Address|ESSID|Signal' 111 | ``` 112 | 113 | There is a shell script in the util directory that can be used to automatically send data to the server: 114 | ``` 115 | bash ./wireless_survey.sh 116 | ``` 117 | 118 | OS X: 119 | 120 | ``` 121 | /System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -s 122 | ``` 123 | 124 | Example: (Method: `POST`) 125 | 126 | ``` 127 | http:///api/beacon// 128 | ``` 129 | 130 | POST Payload: 131 | 132 | ``` 133 | os=&data= 134 | ``` 135 | 136 | The `os` parameter must match one of the following regular expressions: 137 | 138 | * `re.search('^mac os x', os.lower())` 139 | * `re.search('^windows', os.lower())` 140 | * `re.search('^linux', os.lower())` 141 | 142 | ### Universal Parameters 143 | 144 | All requests can include an optional `comment` parameter. This parameter is sanitized and displayed within the UI as miscellaneous information about the target or agent. 145 | 146 | ## Example Web Agents 147 | 148 | ### HTML 149 | 150 | ``` 151 | img = new Image(); 152 | img.src = "http:///api/beacon//HTML"; 153 | ``` 154 | 155 | or 156 | 157 | ``` 158 | 159 | ``` 160 | 161 | ### JavaScript 162 | 163 | Note: JavaScript (HTML5) geolocation agents will not work unless deployed in a secure context (HTTPS), or local host. 164 | 165 | ``` 166 | function showPosition(position) { 167 | img = new Image(); 168 | img.src = "http:///api/beacon//JavaScript?lat=" + position.coords.latitude + "&lng=" + position.coords.longitude + "&acc=" + position.coords.accuracy; 169 | } 170 | 171 | if (navigator.geolocation) { 172 | navigator.geolocation.getCurrentPosition(showPosition); 173 | } 174 | ``` 175 | 176 | ### Content Security Policy 177 | 178 | ``` 179 | response.headers['X-XSS-Protection'] = '0' 180 | response.headers['Content-Security-Policy-Report-Only'] = '; report-uri http:///api/beacon//Content-Security-Policy' 181 | ``` 182 | 183 | ### XSS Auditor 184 | 185 | ``` 186 | response.headers['X-XSS-Protection'] = '1; report=http:///api/beacon//XSS-Protection' 187 | ``` 188 | -------------------------------------------------------------------------------- /server/honeybadger/static/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | * and Firefox. 30 | * Correct `block` display not defined for `main` in IE 11. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | */ 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; /* 1 */ 59 | vertical-align: baseline; /* 2 */ 60 | } 61 | 62 | /** 63 | * Prevent modern browsers from displaying `audio` without controls. 64 | * Remove excess height in iOS 5 devices. 65 | */ 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 75 | */ 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | /* Links 83 | ========================================================================== */ 84 | 85 | /** 86 | * Remove the gray background color from active links in IE 10. 87 | */ 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | /** 94 | * Improve readability when focused and also mouse hovered in all browsers. 95 | */ 96 | 97 | a:active, 98 | a:hover { 99 | outline: 0; 100 | } 101 | 102 | /* Text-level semantics 103 | ========================================================================== */ 104 | 105 | /** 106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 107 | */ 108 | 109 | abbr[title] { 110 | border-bottom: 1px dotted; 111 | } 112 | 113 | /** 114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 115 | */ 116 | 117 | b, 118 | strong { 119 | font-weight: bold; 120 | } 121 | 122 | /** 123 | * Address styling not present in Safari and Chrome. 124 | */ 125 | 126 | dfn { 127 | font-style: italic; 128 | } 129 | 130 | /** 131 | * Address variable `h1` font-size and margin within `section` and `article` 132 | * contexts in Firefox 4+, Safari, and Chrome. 133 | */ 134 | 135 | h1 { 136 | font-size: 2em; 137 | margin: 0.67em 0; 138 | } 139 | 140 | /** 141 | * Address styling not present in IE 8/9. 142 | */ 143 | 144 | mark { 145 | background: #ff0; 146 | color: #000; 147 | } 148 | 149 | /** 150 | * Address inconsistent and variable font size in all browsers. 151 | */ 152 | 153 | small { 154 | font-size: 80%; 155 | } 156 | 157 | /** 158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 159 | */ 160 | 161 | sub, 162 | sup { 163 | font-size: 75%; 164 | line-height: 0; 165 | position: relative; 166 | vertical-align: baseline; 167 | } 168 | 169 | sup { 170 | top: -0.5em; 171 | } 172 | 173 | sub { 174 | bottom: -0.25em; 175 | } 176 | 177 | /* Embedded content 178 | ========================================================================== */ 179 | 180 | /** 181 | * Remove border when inside `a` element in IE 8/9/10. 182 | */ 183 | 184 | img { 185 | border: 0; 186 | } 187 | 188 | /** 189 | * Correct overflow not hidden in IE 9/10/11. 190 | */ 191 | 192 | svg:not(:root) { 193 | overflow: hidden; 194 | } 195 | 196 | /* Grouping content 197 | ========================================================================== */ 198 | 199 | /** 200 | * Address margin not present in IE 8/9 and Safari. 201 | */ 202 | 203 | figure { 204 | margin: 1em 40px; 205 | } 206 | 207 | /** 208 | * Address differences between Firefox and other browsers. 209 | */ 210 | 211 | hr { 212 | -moz-box-sizing: content-box; 213 | box-sizing: content-box; 214 | height: 0; 215 | } 216 | 217 | /** 218 | * Contain overflow in all browsers. 219 | */ 220 | 221 | pre { 222 | overflow: auto; 223 | } 224 | 225 | /** 226 | * Address odd `em`-unit font size rendering in all browsers. 227 | */ 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | /* Forms 238 | ========================================================================== */ 239 | 240 | /** 241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | * styling of `select`, unless a `border` property is set. 243 | */ 244 | 245 | /** 246 | * 1. Correct color not being inherited. 247 | * Known issue: affects color of disabled elements. 248 | * 2. Correct font properties not being inherited. 249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | color: inherit; /* 1 */ 258 | font: inherit; /* 2 */ 259 | margin: 0; /* 3 */ 260 | } 261 | 262 | /** 263 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | */ 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | /** 271 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | * All other form control elements do not inherit `text-transform` values. 273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | * Correct `select` style inheritance in Firefox. 275 | */ 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | /** 283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | * and `video` controls. 285 | * 2. Correct inability to style clickable `input` types in iOS. 286 | * 3. Improve usability and consistency of cursor style between image-type 287 | * `input` and others. 288 | */ 289 | 290 | button, 291 | html input[type="button"], /* 1 */ 292 | input[type="reset"], 293 | input[type="submit"] { 294 | -webkit-appearance: button; /* 2 */ 295 | cursor: pointer; /* 3 */ 296 | } 297 | 298 | /** 299 | * Re-set default cursor for disabled elements. 300 | */ 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | /** 308 | * Remove inner padding and border in Firefox 4+. 309 | */ 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | border: 0; 314 | padding: 0; 315 | } 316 | 317 | /** 318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 319 | * the UA stylesheet. 320 | */ 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | /** 327 | * It's recommended that you don't attempt to style these elements. 328 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | * 330 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | * 2. Remove excess padding in IE 8/9/10. 332 | */ 333 | 334 | input[type="checkbox"], 335 | input[type="radio"] { 336 | box-sizing: border-box; /* 1 */ 337 | padding: 0; /* 2 */ 338 | } 339 | 340 | /** 341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | * `font-size` values of the `input`, it causes the cursor style of the 343 | * decrement button to change from `default` to `text`. 344 | */ 345 | 346 | input[type="number"]::-webkit-inner-spin-button, 347 | input[type="number"]::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | /** 352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 354 | * (include `-moz` to future-proof). 355 | */ 356 | 357 | input[type="search"] { 358 | -webkit-appearance: textfield; /* 1 */ 359 | -moz-box-sizing: content-box; 360 | -webkit-box-sizing: content-box; /* 2 */ 361 | box-sizing: content-box; 362 | } 363 | 364 | /** 365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 366 | * Safari (but not Chrome) clips the cancel button when the search input has 367 | * padding (and `textfield` appearance). 368 | */ 369 | 370 | input[type="search"]::-webkit-search-cancel-button, 371 | input[type="search"]::-webkit-search-decoration { 372 | -webkit-appearance: none; 373 | } 374 | 375 | /** 376 | * Define consistent border, margin, and padding. 377 | */ 378 | 379 | fieldset { 380 | border: 1px solid #c0c0c0; 381 | margin: 0 2px; 382 | padding: 0.35em 0.625em 0.75em; 383 | } 384 | 385 | /** 386 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 388 | */ 389 | 390 | legend { 391 | border: 0; /* 1 */ 392 | padding: 0; /* 2 */ 393 | } 394 | 395 | /** 396 | * Remove default vertical scrollbar in IE 8/9/10/11. 397 | */ 398 | 399 | textarea { 400 | overflow: auto; 401 | } 402 | 403 | /** 404 | * Don't inherit the `font-weight` (applied by a rule above). 405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 406 | */ 407 | 408 | optgroup { 409 | font-weight: bold; 410 | } 411 | 412 | /* Tables 413 | ========================================================================== */ 414 | 415 | /** 416 | * Remove most spacing between table cells. 417 | */ 418 | 419 | table { 420 | border-collapse: collapse; 421 | border-spacing: 0; 422 | } 423 | 424 | td, 425 | th { 426 | padding: 0; 427 | } -------------------------------------------------------------------------------- /server/honeybadger/views.py: -------------------------------------------------------------------------------- 1 | from flask import request, make_response, session, g, redirect, url_for, render_template, jsonify, flash, abort 2 | from flask_cors import cross_origin 3 | from honeybadger import app, db, logger 4 | from honeybadger.processors import process_known_coords, process_wlan_survey, process_ip, process_json 5 | from honeybadger.validators import is_valid_email, is_valid_password 6 | from honeybadger.decorators import login_required, roles_required 7 | from honeybadger.constants import ROLES 8 | from honeybadger.utils import generate_token, generate_nonce 9 | from honeybadger.models import User, Target, Beacon, Log 10 | import os 11 | from base64 import b64decode as b64d 12 | 13 | # request preprocessors 14 | 15 | @app.before_request 16 | def load_user(): 17 | g.user = None 18 | if session.get('user_id'): 19 | g.user = User.query.filter_by(id=session["user_id"]).first() 20 | 21 | # control panel ui views 22 | 23 | @app.route('/') 24 | @app.route('/index') 25 | @login_required 26 | def index(): 27 | return redirect(url_for('map')) 28 | 29 | @app.route('/map') 30 | @login_required 31 | def map(): 32 | return render_template('map.html', key=app.config['GOOGLE_API_KEY']) 33 | 34 | @app.route('/beacons') 35 | @login_required 36 | def beacons(): 37 | beacons = [b.serialized for t in Target.query.all() for b in t.beacons.all()] 38 | columns = ['id', 'target', 'agent', 'lat', 'lng', 'acc', 'ip', 'created'] 39 | return render_template('beacons.html', columns=columns, beacons=beacons) 40 | 41 | @app.route('/beacon/delete/') 42 | @login_required 43 | @roles_required('admin') 44 | def beacon_delete(id): 45 | beacon = Beacon.query.get(id) 46 | if beacon: 47 | db.session.delete(beacon) 48 | db.session.commit() 49 | flash('Beacon deleted.') 50 | else: 51 | flash('Invalid beacon ID.') 52 | return redirect(url_for('beacons')) 53 | 54 | @app.route('/targets') 55 | @login_required 56 | def targets(): 57 | targets = Target.query.all() 58 | columns = ['id', 'name', 'guid', 'beacon_count'] 59 | return render_template('targets.html', columns=columns, targets=targets) 60 | 61 | @app.route('/target/add', methods=['POST']) 62 | @login_required 63 | @roles_required('admin') 64 | def target_add(): 65 | name = request.form['target'] 66 | if name: 67 | target = Target( 68 | name=name, 69 | ) 70 | db.session.add(target) 71 | db.session.commit() 72 | flash('Target added.') 73 | return redirect(url_for('targets')) 74 | 75 | @app.route('/target/delete/') 76 | @login_required 77 | @roles_required('admin') 78 | def target_delete(guid): 79 | target = Target.query.filter_by(guid=guid).first() 80 | if target: 81 | db.session.delete(target) 82 | db.session.commit() 83 | flash('Target deleted.') 84 | else: 85 | flash('Invalid target GUID.') 86 | return redirect(url_for('targets')) 87 | 88 | @app.route('/profile', methods=['GET', 'POST']) 89 | @login_required 90 | def profile(): 91 | if request.method == 'POST': 92 | if g.user.check_password(request.form['current_password']): 93 | new_password = request.form['new_password'] 94 | if new_password == request.form['confirm_password']: 95 | if is_valid_password(new_password): 96 | g.user.password = new_password 97 | db.session.add(g.user) 98 | db.session.commit() 99 | flash('Profile updated.') 100 | else: 101 | flash('Password does not meet complexity requirements.') 102 | else: 103 | flash('Passwords do not match.') 104 | else: 105 | flash('Incorrect current password.') 106 | return render_template('profile.html', user=g.user) 107 | 108 | # use an alternate route for reset as long as the logic is similar to init 109 | @app.route('/password/reset/', methods=['GET', 'POST'], endpoint='password_reset') 110 | @app.route('/profile/activate/', methods=['GET', 'POST']) 111 | def profile_activate(token): 112 | user = User.query.filter_by(token=token).first() 113 | if user and user.status in (0, 3): 114 | if request.method == 'POST': 115 | new_password = request.form['new_password'] 116 | if new_password == request.form['confirm_password']: 117 | if is_valid_password(new_password): 118 | user.password = new_password 119 | user.status = 1 120 | user.token = None 121 | db.session.add(user) 122 | db.session.commit() 123 | flash('Profile activated.') 124 | return redirect(url_for('login')) 125 | else: 126 | flash('Password does not meet complexity requirements.') 127 | else: 128 | flash('Passwords do not match.') 129 | return render_template('profile_activate.html', user=user) 130 | # abort to 404 for obscurity 131 | abort(404) 132 | 133 | @app.route('/admin') 134 | @login_required 135 | @roles_required('admin') 136 | def admin(): 137 | users = User.query.all() 138 | columns = ['email', 'role_as_string', 'status_as_string'] 139 | return render_template('admin.html', columns=columns, users=users, roles=ROLES) 140 | 141 | @app.route('/admin/user/init', methods=['POST']) 142 | @login_required 143 | @roles_required('admin') 144 | def admin_user_init(): 145 | email = request.form['email'] 146 | if is_valid_email(email): 147 | if not User.query.filter_by(email=email).first(): 148 | user = User( 149 | email=email, 150 | token=generate_token(), 151 | ) 152 | db.session.add(user) 153 | db.session.commit() 154 | flash('User initialized.') 155 | else: 156 | flash('Username already exists.') 157 | else: 158 | flash('Invalid email address.') 159 | # send notification to user 160 | return redirect(url_for('admin')) 161 | 162 | @app.route('/admin/user//') 163 | @login_required 164 | @roles_required('admin') 165 | def admin_user(action, id): 166 | user = User.query.get(id) 167 | if user: 168 | if user != g.user: 169 | if action == 'activate' and user.status == 2: 170 | user.status = 1 171 | db.session.add(user) 172 | db.session.commit() 173 | flash('User activated.') 174 | elif action == 'deactivate' and user.status == 1: 175 | user.status = 2 176 | db.session.add(user) 177 | db.session.commit() 178 | flash('User deactivated.') 179 | elif action == 'reset' and user.status == 1: 180 | user.status = 3 181 | user.token = generate_token() 182 | db.session.add(user) 183 | db.session.commit() 184 | flash('User reset.') 185 | elif action == 'delete': 186 | db.session.delete(user) 187 | db.session.commit() 188 | flash('User deleted.') 189 | else: 190 | flash('Invalid user action.') 191 | else: 192 | flash('Self-modification denied.') 193 | else: 194 | flash('Invalid user ID.') 195 | return redirect(url_for('admin')) 196 | 197 | @app.route('/login', methods=['GET', 'POST']) 198 | def login(): 199 | # redirect to home if already logged in 200 | if session.get('user_id'): 201 | return redirect(url_for('index')) 202 | if request.method == 'POST': 203 | user = User.get_by_email(request.form['email']) 204 | if user and user.status == 1 and user.check_password(request.form['password']): 205 | session['user_id'] = user.id 206 | flash('You have successfully logged in.') 207 | return redirect(url_for('index')) 208 | flash('Invalid username or password.') 209 | return render_template('login.html') 210 | 211 | @app.route('/logout') 212 | @login_required 213 | def logout(): 214 | session.pop('user_id', None) 215 | flash('You have been logged out') 216 | return redirect(url_for('index')) 217 | 218 | @app.route('/demo/', methods=['GET', 'POST']) 219 | def demo(guid): 220 | text = None 221 | if request.method == 'POST': 222 | text = request.values['text'] 223 | key = request.values['key'] 224 | if g.user.check_password(key): 225 | if text and 'alert(' in text: 226 | text = 'Congrats! You entered: {}'.format(text) 227 | else: 228 | text = 'Nope. Try again.' 229 | else: 230 | text = 'Incorrect password.' 231 | nonce = generate_nonce(24) 232 | response = make_response(render_template('demo.html', target=guid, text=text, nonce=nonce)) 233 | response.headers['X-XSS-Protection'] = '0'#'1; report=https://hb.lanmaster53.com/api/beacon/{}/X-XSS-Protection'.format(guid) 234 | uri = url_for('api_beacon', target=guid, agent='Content-Security-Policy') 235 | response.headers['Content-Security-Policy-Report-Only'] = 'script-src \'nonce-{}\'; report-uri {}'.format(nonce, uri) 236 | return response 237 | 238 | @app.route('/log') 239 | @login_required 240 | def log(): 241 | # hidden capability to clear logs 242 | if request.values.get('clear'): 243 | Log.query.delete() 244 | db.session.commit() 245 | return redirect(url_for('log')) 246 | content = '' 247 | logs = Log.query.order_by(Log.created).all() 248 | for log in logs: 249 | content += '[{}] [{}] {}{}'.format(log.created_as_string, log.level_as_string, log.message, os.linesep) 250 | return render_template('log.html', content=content) 251 | 252 | # control panel api views 253 | 254 | @app.route('/api/beacons') 255 | @login_required 256 | def api_beacons(): 257 | beacons = [b.serialized for t in Target.query.all() for b in t.beacons.all()] 258 | return jsonify(beacons=beacons) 259 | 260 | # agent api views 261 | 262 | @app.route('/api/beacon//', methods=['GET', 'POST']) 263 | @cross_origin() 264 | def api_beacon(target, agent): 265 | logger.info('{}'.format('='*50)) 266 | data = {'target': target, 'agent': agent} 267 | logger.info('Target: {}'.format(target)) 268 | logger.info('Agent: {}'.format(agent)) 269 | # check if target is valid 270 | if target not in [x.guid for x in Target.query.all()]: 271 | logger.error('Invalid target GUID.') 272 | abort(404) 273 | # extract universal parameters 274 | comment = b64d(request.values.get('comment', '')).decode() or None 275 | ip = request.environ['REMOTE_ADDR'] 276 | port = request.environ['REMOTE_PORT'] 277 | useragent = request.environ['HTTP_USER_AGENT'] 278 | logger.info('Connection from {} @ {}:{} via {}'.format(target, ip, port, agent)) 279 | logger.info('Parameters: {}'.format(request.values.to_dict())) 280 | logger.info('User-Agent: {}'.format(useragent)) 281 | logger.info('Comment: {}'.format(comment)) 282 | data.update(request.values.to_dict()) 283 | data.update({'comment': comment, 'ip': ip, 'port': port, 'useragent': useragent}) 284 | # process json payloads 285 | if request.json: 286 | if process_json(data, request.json): 287 | abort(404) 288 | # process known coordinates 289 | if all(k in data for k in ('lat', 'lng', 'acc')): 290 | if process_known_coords(data): 291 | abort(404) 292 | # process wireless survey 293 | elif all(k in data for k in ('os', 'data')): 294 | if process_wlan_survey(data): 295 | abort(404) 296 | # process ip geolocation (includes fallback) 297 | process_ip(data) 298 | abort(404) 299 | -------------------------------------------------------------------------------- /server/honeybadger/static/skeleton.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Skeleton V2.0.4 3 | * Copyright 2014, Dave Gamache 4 | * www.getskeleton.com 5 | * Free to use under the MIT license. 6 | * http://www.opensource.org/licenses/mit-license.php 7 | * 12/29/2014 8 | */ 9 | 10 | 11 | /* Table of contents 12 | –––––––––––––––––––––––––––––––––––––––––––––––––– 13 | - Grid 14 | - Base Styles 15 | - Typography 16 | - Links 17 | - Buttons 18 | - Forms 19 | - Lists 20 | - Code 21 | - Tables 22 | - Spacing 23 | - Utilities 24 | - Clearing 25 | - Media Queries 26 | */ 27 | 28 | 29 | /* Grid 30 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 31 | .container { 32 | position: relative; 33 | width: 100%; 34 | max-width: 960px; 35 | margin: 0 auto; 36 | padding: 0 20px; 37 | box-sizing: border-box; } 38 | .column, 39 | .columns { 40 | width: 100%; 41 | float: left; 42 | box-sizing: border-box; } 43 | 44 | /* For devices larger than 400px */ 45 | @media (min-width: 400px) { 46 | .container { 47 | width: 85%; 48 | padding: 0; } 49 | } 50 | 51 | /* For devices larger than 550px */ 52 | @media (min-width: 550px) { 53 | .container { 54 | width: 80%; } 55 | .column, 56 | .columns { 57 | margin-left: 4%; } 58 | .column:first-child, 59 | .columns:first-child { 60 | margin-left: 0; } 61 | 62 | .one.column, 63 | .one.columns { width: 4.66666666667%; } 64 | .two.columns { width: 13.3333333333%; } 65 | .three.columns { width: 22%; } 66 | .four.columns { width: 30.6666666667%; } 67 | .five.columns { width: 39.3333333333%; } 68 | .six.columns { width: 48%; } 69 | .seven.columns { width: 56.6666666667%; } 70 | .eight.columns { width: 65.3333333333%; } 71 | .nine.columns { width: 74.0%; } 72 | .ten.columns { width: 82.6666666667%; } 73 | .eleven.columns { width: 91.3333333333%; } 74 | .twelve.columns { width: 100%; margin-left: 0; } 75 | 76 | .one-third.column { width: 30.6666666667%; } 77 | .two-thirds.column { width: 65.3333333333%; } 78 | 79 | .one-half.column { width: 48%; } 80 | 81 | /* Offsets */ 82 | .offset-by-one.column, 83 | .offset-by-one.columns { margin-left: 8.66666666667%; } 84 | .offset-by-two.column, 85 | .offset-by-two.columns { margin-left: 17.3333333333%; } 86 | .offset-by-three.column, 87 | .offset-by-three.columns { margin-left: 26%; } 88 | .offset-by-four.column, 89 | .offset-by-four.columns { margin-left: 34.6666666667%; } 90 | .offset-by-five.column, 91 | .offset-by-five.columns { margin-left: 43.3333333333%; } 92 | .offset-by-six.column, 93 | .offset-by-six.columns { margin-left: 52%; } 94 | .offset-by-seven.column, 95 | .offset-by-seven.columns { margin-left: 60.6666666667%; } 96 | .offset-by-eight.column, 97 | .offset-by-eight.columns { margin-left: 69.3333333333%; } 98 | .offset-by-nine.column, 99 | .offset-by-nine.columns { margin-left: 78.0%; } 100 | .offset-by-ten.column, 101 | .offset-by-ten.columns { margin-left: 86.6666666667%; } 102 | .offset-by-eleven.column, 103 | .offset-by-eleven.columns { margin-left: 95.3333333333%; } 104 | 105 | .offset-by-one-third.column, 106 | .offset-by-one-third.columns { margin-left: 34.6666666667%; } 107 | .offset-by-two-thirds.column, 108 | .offset-by-two-thirds.columns { margin-left: 69.3333333333%; } 109 | 110 | .offset-by-one-half.column, 111 | .offset-by-one-half.columns { margin-left: 52%; } 112 | 113 | } 114 | 115 | 116 | /* Base Styles 117 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 118 | /* NOTE 119 | html is set to 62.5% so that all the REM measurements throughout Skeleton 120 | are based on 10px sizing. So basically 1.5rem = 15px :) */ 121 | html { 122 | font-size: 62.5%; } 123 | body { 124 | font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */ 125 | line-height: 1.6; 126 | font-weight: 400; 127 | font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; 128 | color: #222; } 129 | 130 | 131 | /* Typography 132 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 133 | h1, h2, h3, h4, h5, h6 { 134 | margin-top: 0; 135 | margin-bottom: 2rem; 136 | font-weight: 300; } 137 | h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;} 138 | h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; } 139 | h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; } 140 | h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; } 141 | h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; } 142 | h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; } 143 | 144 | /* Larger than phablet */ 145 | @media (min-width: 550px) { 146 | h1 { font-size: 5.0rem; } 147 | h2 { font-size: 4.2rem; } 148 | h3 { font-size: 3.6rem; } 149 | h4 { font-size: 3.0rem; } 150 | h5 { font-size: 2.4rem; } 151 | h6 { font-size: 1.5rem; } 152 | } 153 | 154 | p { 155 | margin-top: 0; } 156 | 157 | 158 | /* Links 159 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 160 | a { 161 | color: #1EAEDB; } 162 | a:hover { 163 | color: #0FA0CE; } 164 | 165 | 166 | /* Buttons 167 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 168 | .button, 169 | button, 170 | input[type="submit"], 171 | input[type="reset"], 172 | input[type="button"] { 173 | display: inline-block; 174 | height: 38px; 175 | padding: 0 30px; 176 | color: #555; 177 | text-align: center; 178 | font-size: 11px; 179 | font-weight: 600; 180 | line-height: 38px; 181 | letter-spacing: .1rem; 182 | text-transform: uppercase; 183 | text-decoration: none; 184 | white-space: nowrap; 185 | background-color: transparent; 186 | border-radius: 4px; 187 | border: 1px solid #bbb; 188 | cursor: pointer; 189 | box-sizing: border-box; } 190 | .button:hover, 191 | button:hover, 192 | input[type="submit"]:hover, 193 | input[type="reset"]:hover, 194 | input[type="button"]:hover, 195 | .button:focus, 196 | button:focus, 197 | input[type="submit"]:focus, 198 | input[type="reset"]:focus, 199 | input[type="button"]:focus { 200 | color: #333; 201 | border-color: #888; 202 | outline: 0; } 203 | .button.button-primary, 204 | button.button-primary, 205 | input[type="submit"].button-primary, 206 | input[type="reset"].button-primary, 207 | input[type="button"].button-primary { 208 | color: #FFF; 209 | background-color: #33C3F0; 210 | border-color: #33C3F0; } 211 | .button.button-primary:hover, 212 | button.button-primary:hover, 213 | input[type="submit"].button-primary:hover, 214 | input[type="reset"].button-primary:hover, 215 | input[type="button"].button-primary:hover, 216 | .button.button-primary:focus, 217 | button.button-primary:focus, 218 | input[type="submit"].button-primary:focus, 219 | input[type="reset"].button-primary:focus, 220 | input[type="button"].button-primary:focus { 221 | color: #FFF; 222 | background-color: #1EAEDB; 223 | border-color: #1EAEDB; } 224 | 225 | 226 | /* Forms 227 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 228 | input[type="email"], 229 | input[type="number"], 230 | input[type="search"], 231 | input[type="text"], 232 | input[type="tel"], 233 | input[type="url"], 234 | input[type="password"], 235 | textarea, 236 | select { 237 | height: 38px; 238 | padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ 239 | background-color: #fff; 240 | border: 1px solid #D1D1D1; 241 | border-radius: 4px; 242 | box-shadow: none; 243 | box-sizing: border-box; } 244 | /* Removes awkward default styles on some inputs for iOS */ 245 | input[type="email"], 246 | input[type="number"], 247 | input[type="search"], 248 | input[type="text"], 249 | input[type="tel"], 250 | input[type="url"], 251 | input[type="password"], 252 | textarea { 253 | -webkit-appearance: none; 254 | -moz-appearance: none; 255 | appearance: none; } 256 | textarea { 257 | min-height: 65px; 258 | padding-top: 6px; 259 | padding-bottom: 6px; } 260 | input[type="email"]:focus, 261 | input[type="number"]:focus, 262 | input[type="search"]:focus, 263 | input[type="text"]:focus, 264 | input[type="tel"]:focus, 265 | input[type="url"]:focus, 266 | input[type="password"]:focus, 267 | textarea:focus, 268 | select:focus { 269 | border: 1px solid #33C3F0; 270 | outline: 0; } 271 | label, 272 | legend { 273 | display: block; 274 | margin-bottom: .5rem; 275 | font-weight: 600; } 276 | fieldset { 277 | padding: 0; 278 | border-width: 0; } 279 | input[type="checkbox"], 280 | input[type="radio"] { 281 | display: inline; } 282 | label > .label-body { 283 | display: inline-block; 284 | margin-left: .5rem; 285 | font-weight: normal; } 286 | 287 | 288 | /* Lists 289 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 290 | ul { 291 | list-style: circle inside; } 292 | ol { 293 | list-style: decimal inside; } 294 | ol, ul { 295 | padding-left: 0; 296 | margin-top: 0; } 297 | ul ul, 298 | ul ol, 299 | ol ol, 300 | ol ul { 301 | margin: 1.5rem 0 1.5rem 3rem; 302 | font-size: 90%; } 303 | li { 304 | margin-bottom: 1rem; } 305 | 306 | 307 | /* Code 308 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 309 | code { 310 | padding: .2rem .5rem; 311 | margin: 0 .2rem; 312 | font-size: 90%; 313 | white-space: nowrap; 314 | background: #F1F1F1; 315 | border: 1px solid #E1E1E1; 316 | border-radius: 4px; } 317 | pre > code { 318 | display: block; 319 | padding: 1rem 1.5rem; 320 | white-space: pre; } 321 | 322 | 323 | /* Tables 324 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 325 | th, 326 | td { 327 | padding: 12px 15px; 328 | text-align: left; 329 | border-bottom: 1px solid #E1E1E1; } 330 | th:first-child, 331 | td:first-child { 332 | padding-left: 0; } 333 | th:last-child, 334 | td:last-child { 335 | padding-right: 0; } 336 | 337 | 338 | /* Spacing 339 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 340 | button, 341 | .button { 342 | margin-bottom: 1rem; } 343 | input, 344 | textarea, 345 | select, 346 | fieldset { 347 | margin-bottom: 1.5rem; } 348 | pre, 349 | blockquote, 350 | dl, 351 | figure, 352 | table, 353 | p, 354 | ul, 355 | ol, 356 | form { 357 | margin-bottom: 2.5rem; } 358 | 359 | 360 | /* Utilities 361 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 362 | .u-full-width { 363 | width: 100%; 364 | box-sizing: border-box; } 365 | .u-max-full-width { 366 | max-width: 100%; 367 | box-sizing: border-box; } 368 | .u-pull-right { 369 | float: right; } 370 | .u-pull-left { 371 | float: left; } 372 | 373 | 374 | /* Misc 375 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 376 | hr { 377 | margin-top: 3rem; 378 | margin-bottom: 3.5rem; 379 | border-width: 0; 380 | border-top: 1px solid #E1E1E1; } 381 | 382 | 383 | /* Clearing 384 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 385 | 386 | /* Self Clearing Goodness */ 387 | .container:after, 388 | .row:after, 389 | .u-cf { 390 | content: ""; 391 | display: table; 392 | clear: both; } 393 | 394 | 395 | /* Media Queries 396 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 397 | /* 398 | Note: The best way to structure the use of media queries is to create the queries 399 | near the relevant code. For example, if you wanted to change the styles for buttons 400 | on small devices, paste the mobile query code up in the buttons section and style it 401 | there. 402 | */ 403 | 404 | 405 | /* Larger than mobile */ 406 | @media (min-width: 400px) {} 407 | 408 | /* Larger than phablet (also point when grid becomes active) */ 409 | @media (min-width: 550px) {} 410 | 411 | /* Larger than tablet */ 412 | @media (min-width: 750px) {} 413 | 414 | /* Larger than desktop */ 415 | @media (min-width: 1000px) {} 416 | 417 | /* Larger than Desktop HD */ 418 | @media (min-width: 1200px) {} 419 | -------------------------------------------------------------------------------- /server/honeybadger/parsers.py: -------------------------------------------------------------------------------- 1 | from honeybadger.utils import freq2channel 2 | import os 3 | 4 | class AP(object): 5 | 6 | def __init__(self, ssid=None, bssid=None, ss=None, channel=None): 7 | self.ssid = ssid 8 | self.bssid = bssid 9 | self.ss = ss 10 | self.channel = channel 11 | 12 | @property 13 | def serialized_for_google(self): 14 | return { 15 | 'macAddress': self.bssid, 16 | 'signalStrength': self.ss, 17 | 'channel': self.channel, 18 | } 19 | 20 | def __repr__(self): 21 | return ''.format(self.ssid, self.bssid, self.ss, self.channel) 22 | 23 | def parse_google(jsondata): 24 | aps = [] 25 | for ap in jsondata[0]['ap_list']: 26 | aps.append(AP(bssid=ap['bssid'], ss=ap['signal_level'], channel=freq2channel(ap['frequency']))) 27 | return aps 28 | 29 | def parse_airport(content): 30 | aps = [] 31 | lines = [l.strip() for l in content.strip().split(os.linesep)] 32 | for line in lines[1:]: 33 | words = line.split() 34 | aps.append(AP(ssid=words[0], bssid=words[1], ss=int(words[2]), channel=int(words[3]))) 35 | return aps 36 | 37 | def parse_netsh(content): 38 | aps = [] 39 | lastssid = None 40 | lines = [l.strip() for l in content.strip().split(os.linesep)] 41 | for line in lines: 42 | words = line.split() 43 | # use startswith to avoid index errors 44 | if line.startswith('SSID'): 45 | lastssid = ' '.join(words[3:]) 46 | elif line.startswith('BSSID'): 47 | ap = AP(ssid=lastssid) 48 | ap.bssid = words[3] 49 | elif line.startswith('Signal'): 50 | dbm = int(words[2][:-1]) - 100 51 | ap.ss = dbm 52 | elif line.startswith('Channel'): 53 | ap.channel = int(words[2]) 54 | aps.append(ap) 55 | return aps 56 | 57 | def parse_iwlist(content): 58 | aps = [] 59 | lines = [l.strip() for l in content.strip().split(os.linesep)] 60 | for line in lines: 61 | words = line.split() 62 | if line.startswith('Cell'): 63 | ap = AP(bssid=words[4]) 64 | elif line.startswith('Channel:'): 65 | ap.channel = int(words[0].split(':')[-1]) 66 | elif line.startswith('Quality='): 67 | ap.ss = int(words[2][6:]) 68 | elif line.startswith('ESSID:'): 69 | ap.ssid = line[7:-1] 70 | aps.append(ap) 71 | return aps 72 | 73 | airport_test = ''' SSID BSSID RSSI CHANNEL HT CC SECURITY (auth/unicast/group) 74 | gogoinflight 00:3a:9a:ea:1f:42 -78 40 N -- NONE 75 | gogoinflight 00:24:c3:50:54:22 -47 36 N -- NONE 76 | gogoinflight 00:3a:9a:ec:e6:02 -69 6 N -- NONE 77 | gogoinflight 00:24:c3:31:cd:d2 -41 1 N -- NONE 78 | ''' 79 | 80 | netsh_test = '''SSID 1 : Home 81 | Network type : Infrastructure 82 | Authentication : WPA2-Personal 83 | Encryption : CCMP 84 | BSSID 1 : 00:1e:c2:f6:7e:98 85 | Signal : 99% 86 | Radio type : 802.11n 87 | Channel : 157 88 | Basic rates (Mbps) : 24 39 156 89 | Other rates (Mbps) : 18 19.5 36 48 54 90 | BSSID 2 : 00:1c:10:08:b7:a5 91 | Signal : 33% 92 | Radio type : 802.11g 93 | Channel : 11 94 | Basic rates (Mbps) : 1 2 5.5 11 95 | Other rates (Mbps) : 6 9 12 18 24 36 48 54 96 | BSSID 3 : 00:1e:c2:f6:7e:97 97 | Signal : 90% 98 | Radio type : 802.11n 99 | Channel : 1 100 | Basic rates (Mbps) : 1 2 5.5 11 101 | Other rates (Mbps) : 6 9 12 18 24 36 48 54 102 | 103 | SSID 2 : 104 | Network type : Infrastructure 105 | Authentication : Open 106 | Encryption : WEP 107 | BSSID 1 : 62:45:b0:34:d3:53 108 | Signal : 33% 109 | Radio type : Any Radio Type 110 | Channel : 44 111 | Basic rates (Mbps) : 112 | 113 | SSID 3 : ATT5727 114 | Network type : Infrastructure 115 | Authentication : WPA2-Personal 116 | Encryption : CCMP 117 | BSSID 1 : 80:37:73:7b:7c:1f 118 | Signal : 31% 119 | Radio type : 802.11n 120 | Channel : 11 121 | Basic rates (Mbps) : 1 2 5.5 11 122 | Other rates (Mbps) : 6 9 12 18 24 36 48 54 123 | 124 | SSID 4 : 320Burbridge 125 | Network type : Infrastructure 126 | Authentication : WPA2-Personal 127 | Encryption : CCMP 128 | BSSID 1 : 00:1f:33:48:7f:f0 129 | Signal : 40% 130 | Radio type : 802.11n 131 | Channel : 6 132 | Basic rates (Mbps) : 1 2 5.5 11 133 | Other rates (Mbps) : 6 9 12 18 24 36 48 54 134 | 135 | SSID 5 : 320Burbridge_EXT 136 | Network type : Infrastructure 137 | Authentication : WPA2-Personal 138 | Encryption : CCMP 139 | BSSID 1 : c0:ff:d4:c2:07:b6 140 | Signal : 30% 141 | Radio type : 802.11n 142 | Channel : 6 143 | Basic rates (Mbps) : 1 2 144 | Other rates (Mbps) : 5.5 6 9 11 12 18 24 36 48 54 145 | 146 | SSID 6 : ATT3600 147 | Network type : Infrastructure 148 | Authentication : WPA2-Personal 149 | Encryption : CCMP 150 | BSSID 1 : e8:fc:af:d9:d1:14 151 | Signal : 31% 152 | Radio type : 802.11n 153 | Channel : 1 154 | Basic rates (Mbps) : 1 2 5.5 11 155 | Other rates (Mbps) : 6 9 12 18 24 36 48 54 156 | 157 | ''' 158 | 159 | iwlist_test = '''wlan1 Scan completed : 160 | Cell 01 - Address: 00:1E:C2:F6:7E:97 161 | Channel:1 162 | Frequency:2.412 GHz (Channel 1) 163 | Quality=61/70 Signal level=-49 dBm 164 | Encryption key:on 165 | ESSID:"Home" 166 | Bit Rates:1 Mb/s; 2 Mb/s; 5.5 Mb/s; 11 Mb/s; 6 Mb/s 167 | 9 Mb/s; 12 Mb/s; 18 Mb/s 168 | Bit Rates:24 Mb/s; 36 Mb/s; 48 Mb/s; 54 Mb/s 169 | Mode:Master 170 | Extra:tsf=000002b198bd7947 171 | Extra: Last beacon: 3092ms ago 172 | IE: Unknown: 0004486F6D65 173 | IE: Unknown: 010882848B960C121824 174 | IE: Unknown: 030101 175 | IE: Unknown: 0706555320010B1E 176 | IE: Unknown: 2A0102 177 | IE: Unknown: 32043048606C 178 | IE: IEEE 802.11i/WPA2 Version 1 179 | Group Cipher : TKIP 180 | Pairwise Ciphers (2) : CCMP TKIP 181 | Authentication Suites (1) : PSK 182 | IE: Unknown: 2D1AAC4117FFFFFF0000000000000000000000000000000000000000 183 | IE: Unknown: 33027E9D 184 | IE: Unknown: 3D1601001100000000000000000000000000000000000000 185 | IE: Unknown: 46050200010000 186 | IE: WPA Version 1 187 | Group Cipher : TKIP 188 | Pairwise Ciphers (1) : TKIP 189 | Authentication Suites (1) : PSK 190 | IE: Unknown: DD180050F2020101010003A4000027A4000042435E0062322F00 191 | IE: Unknown: DD0700039301720320 192 | IE: Unknown: DD0E0017F20700010106001EC2F67E97 193 | IE: Unknown: DD0B0017F20100010100000007 194 | Cell 02 - Address: C8:B3:73:02:F0:3B 195 | Channel:1 196 | Frequency:2.412 GHz (Channel 1) 197 | Quality=33/70 Signal level=-77 dBm 198 | Encryption key:on 199 | ESSID:"319 Burbridge Ct" 200 | Bit Rates:1 Mb/s; 2 Mb/s; 5.5 Mb/s; 11 Mb/s; 18 Mb/s 201 | 24 Mb/s; 36 Mb/s; 54 Mb/s 202 | Bit Rates:6 Mb/s; 9 Mb/s; 12 Mb/s; 48 Mb/s 203 | Mode:Master 204 | Extra:tsf=0000000ac57bf5a0 205 | Extra: Last beacon: 16024ms ago 206 | IE: Unknown: 001033313920427572627269646765204374 207 | IE: Unknown: 010882848B962430486C 208 | IE: Unknown: 030101 209 | IE: Unknown: 2A0104 210 | IE: Unknown: 2F0104 211 | IE: IEEE 802.11i/WPA2 Version 1 212 | Group Cipher : TKIP 213 | Pairwise Ciphers (2) : CCMP TKIP 214 | Authentication Suites (1) : PSK 215 | IE: Unknown: 32040C121860 216 | IE: Unknown: 2D1AFC181BFFFF000000000000000000000000000000000000000000 217 | IE: Unknown: 3D1601081500000000000000000000000000000000000000 218 | IE: Unknown: 4A0E14000A002C01C800140005001900 219 | IE: Unknown: 7F0101 220 | IE: Unknown: DD840050F204104A0001101044000102103B000103104700101C18BB447704CE8C3AAB08F3407263FF10210005436973636F1023000D4C696E6B7379732045323530301024000776312E302E30371042000234321054000800060050F20400011011000D4C696E6B737973204532353030100800022688103C0001031049000600372A000120 221 | IE: Unknown: DD090010180203F0040000 222 | IE: WPA Version 1 223 | Group Cipher : TKIP 224 | Pairwise Ciphers (2) : CCMP TKIP 225 | Authentication Suites (1) : PSK 226 | IE: Unknown: DD180050F2020101800003A4000027A4000042435E0062322F00 227 | Cell 03 - Address: 00:1C:10:08:B7:A5 228 | Channel:11 229 | Frequency:2.462 GHz (Channel 11) 230 | Quality=37/70 Signal level=-73 dBm 231 | Encryption key:on 232 | ESSID:"Home" 233 | Bit Rates:1 Mb/s; 2 Mb/s; 5.5 Mb/s; 11 Mb/s; 18 Mb/s 234 | 24 Mb/s; 36 Mb/s; 54 Mb/s 235 | Bit Rates:6 Mb/s; 9 Mb/s; 12 Mb/s; 48 Mb/s 236 | Mode:Master 237 | Extra:tsf=00000029f37bf68e 238 | Extra: Last beacon: 924ms ago 239 | IE: Unknown: 0004486F6D65 240 | IE: Unknown: 010882848B962430486C 241 | IE: Unknown: 03010B 242 | IE: Unknown: 2A0100 243 | IE: Unknown: 2F0100 244 | IE: IEEE 802.11i/WPA2 Version 1 245 | Group Cipher : CCMP 246 | Pairwise Ciphers (1) : CCMP 247 | Authentication Suites (1) : PSK 248 | IE: Unknown: 32040C121860 249 | IE: Unknown: DD090010180201F0000000 250 | IE: Unknown: DD180050F2020101800003A4000027A4000042435E0062322F00 251 | Cell 04 - Address: C8:B3:73:02:F0:3D 252 | Channel:1 253 | Frequency:2.412 GHz (Channel 1) 254 | Quality=31/70 Signal level=-79 dBm 255 | Encryption key:off 256 | ESSID:"319 Burbridge Ct-guest" 257 | Bit Rates:1 Mb/s; 2 Mb/s; 5.5 Mb/s; 11 Mb/s; 18 Mb/s 258 | 24 Mb/s; 36 Mb/s; 54 Mb/s 259 | Bit Rates:6 Mb/s; 9 Mb/s; 12 Mb/s; 48 Mb/s 260 | Mode:Master 261 | Extra:tsf=0000000ac57c3254 262 | Extra: Last beacon: 16024ms ago 263 | IE: Unknown: 0016333139204275726272696467652043742D6775657374 264 | IE: Unknown: 010882848B962430486C 265 | IE: Unknown: 030101 266 | IE: Unknown: 2A0104 267 | IE: Unknown: 2F0104 268 | IE: Unknown: 32040C121860 269 | IE: Unknown: 2D1AFC181BFFFF000000000000000000000000000000000000000000 270 | IE: Unknown: 3D1601081500000000000000000000000000000000000000 271 | IE: Unknown: 4A0E14000A002C01C800140005001900 272 | IE: Unknown: 7F0101 273 | IE: Unknown: DD090010180203F0040000 274 | IE: Unknown: DD180050F2020101800003A4000027A4000042435E0062322F00 275 | Cell 05 - Address: C0:83:0A:CE:D2:C9 276 | Channel:11 277 | Frequency:2.462 GHz (Channel 11) 278 | Quality=27/70 Signal level=-83 dBm 279 | Encryption key:on 280 | ESSID:"2WIRE698" 281 | Bit Rates:1 Mb/s; 2 Mb/s; 5.5 Mb/s; 11 Mb/s; 6 Mb/s 282 | 9 Mb/s; 12 Mb/s; 18 Mb/s 283 | Bit Rates:24 Mb/s; 36 Mb/s; 48 Mb/s; 54 Mb/s 284 | Mode:Master 285 | Extra:tsf=00000333ab61d181 286 | Extra: Last beacon: 17172ms ago 287 | IE: Unknown: 00083257495245363938 288 | IE: Unknown: 010882848B960C121824 289 | IE: Unknown: 03010B 290 | IE: Unknown: 050400010000 291 | IE: Unknown: 0706555320010B1B 292 | IE: Unknown: 2A0100 293 | IE: Unknown: 32043048606C 294 | Cell 06 - Address: 00:24:B2:91:BB:1C 295 | Channel:6 296 | Frequency:2.437 GHz (Channel 6) 297 | Quality=29/70 Signal level=-81 dBm 298 | Encryption key:on 299 | ESSID:"Toadstool" 300 | Bit Rates:1 Mb/s; 2 Mb/s; 5.5 Mb/s; 11 Mb/s; 18 Mb/s 301 | 24 Mb/s; 36 Mb/s; 54 Mb/s 302 | Bit Rates:6 Mb/s; 9 Mb/s; 12 Mb/s; 48 Mb/s 303 | Mode:Master 304 | Extra:tsf=00000001f5c0458a 305 | Extra: Last beacon: 2108ms ago 306 | IE: Unknown: 0009546F616473746F6F6C 307 | IE: Unknown: 010882848B962430486C 308 | IE: Unknown: 030106 309 | IE: Unknown: 2A0100 310 | IE: Unknown: 2F0100 311 | IE: IEEE 802.11i/WPA2 Version 1 312 | Group Cipher : CCMP 313 | Pairwise Ciphers (1) : CCMP 314 | Authentication Suites (1) : PSK 315 | IE: Unknown: 32040C121860 316 | IE: Unknown: DD090010180200F0000000 317 | IE: Unknown: DD180050F2020101800003A4000027A4000042435E0062322F00 318 | 319 | eth0 Interface doesn't support scanning. 320 | 321 | lo Interface doesn't support scanning. 322 | 323 | ''' 324 | -------------------------------------------------------------------------------- /server/honeybadger/static/sorttable.js: -------------------------------------------------------------------------------- 1 | /* 2 | SortTable 3 | version 2 4 | 7th April 2007 5 | Stuart Langridge, http://www.kryogenix.org/code/browser/sorttable/ 6 | 7 | Instructions: 8 | Download this file 9 | Add to your HTML 10 | Add class="sortable" to any table you'd like to make sortable 11 | Click on the headers to sort 12 | 13 | Thanks to many, many people for contributions and suggestions. 14 | Licenced as X11: http://www.kryogenix.org/code/browser/licence.html 15 | This basically means: do what you want with it. 16 | */ 17 | 18 | /* jshint -W051, -W083, -W027 */ 19 | 20 | var stIsIE = /*@cc_on!@*/false; 21 | 22 | sorttable = { 23 | init: function() { 24 | // quit if this function has already been called 25 | if (arguments.callee.done) return; 26 | // flag this function so we don't do the same thing twice 27 | arguments.callee.done = true; 28 | // kill the timer 29 | if (_timer) clearInterval(_timer); 30 | 31 | if (!document.createElement || !document.getElementsByTagName) return; 32 | 33 | sorttable.DATE_RE = /^(\d\d?)[\/\.-](\d\d?)[\/\.-]((\d\d)?\d\d)$/; 34 | 35 | forEach(document.getElementsByTagName('table'), function(table) { 36 | if (table.className.search(/\bsortable\b/) != -1) { 37 | sorttable.makeSortable(table); 38 | } 39 | }); 40 | 41 | }, 42 | 43 | makeSortable: function(table) { 44 | if (table.getElementsByTagName('thead').length === 0) { 45 | // table doesn't have a tHead. Since it should have, create one and 46 | // put the first table row in it. 47 | the = document.createElement('thead'); 48 | the.appendChild(table.rows[0]); 49 | table.insertBefore(the,table.firstChild); 50 | } 51 | // Safari doesn't support table.tHead, sigh 52 | if (table.tHead === null) table.tHead = table.getElementsByTagName('thead')[0]; 53 | 54 | if (table.tHead.rows.length != 1) return; // can't cope with two header rows 55 | 56 | // Sorttable v1 put rows with a class of "sortbottom" at the bottom (as 57 | // "total" rows, for example). This is B&R, since what you're supposed 58 | // to do is put them in a tfoot. So, if there are sortbottom rows, 59 | // for backwards compatibility, move them to tfoot (creating it if needed). 60 | sortbottomrows = []; 61 | for (var i=0; i5' : ' ▴'; 105 | this.appendChild(sortrevind); 106 | return; 107 | } 108 | if (this.className.search(/\bsorttable_sorted_reverse\b/) != -1) { 109 | // if we're already sorted by this column in reverse, just 110 | // re-reverse the table, which is quicker 111 | sorttable.reverse(this.sorttable_tbody); 112 | this.className = this.className.replace('sorttable_sorted_reverse', 113 | 'sorttable_sorted'); 114 | this.removeChild(document.getElementById('sorttable_sortrevind')); 115 | sortfwdind = document.createElement('span'); 116 | sortfwdind.id = "sorttable_sortfwdind"; 117 | sortfwdind.innerHTML = stIsIE ? ' 6' : ' ▾'; 118 | this.appendChild(sortfwdind); 119 | return; 120 | } 121 | 122 | // remove sorttable_sorted classes 123 | theadrow = this.parentNode; 124 | forEach(theadrow.childNodes, function(cell) { 125 | if (cell.nodeType == 1) { // an element 126 | cell.className = cell.className.replace('sorttable_sorted_reverse',''); 127 | cell.className = cell.className.replace('sorttable_sorted',''); 128 | } 129 | }); 130 | sortfwdind = document.getElementById('sorttable_sortfwdind'); 131 | if (sortfwdind) { sortfwdind.parentNode.removeChild(sortfwdind); } 132 | sortrevind = document.getElementById('sorttable_sortrevind'); 133 | if (sortrevind) { sortrevind.parentNode.removeChild(sortrevind); } 134 | 135 | this.className += ' sorttable_sorted'; 136 | sortfwdind = document.createElement('span'); 137 | sortfwdind.id = "sorttable_sortfwdind"; 138 | sortfwdind.innerHTML = stIsIE ? ' 6' : ' ▾'; 139 | this.appendChild(sortfwdind); 140 | 141 | // build an array to sort. This is a Schwartzian transform thing, 142 | // i.e., we "decorate" each row with the actual sort key, 143 | // sort based on the sort keys, and then put the rows back in order 144 | // which is a lot faster because you only do getInnerText once per row 145 | row_array = []; 146 | col = this.sorttable_columnindex; 147 | rows = this.sorttable_tbody.rows; 148 | for (var j=0; j 12) { 185 | // definitely dd/mm 186 | return sorttable.sort_ddmm; 187 | } else if (second > 12) { 188 | return sorttable.sort_mmdd; 189 | } else { 190 | // looks like a date, but we can't tell which, so assume 191 | // that it's dd/mm (English imperialism!) and keep looking 192 | sortfn = sorttable.sort_ddmm; 193 | } 194 | } 195 | } 196 | } 197 | return sortfn; 198 | }, 199 | 200 | getInnerText: function(node) { 201 | // gets the text we want to use for sorting for a cell. 202 | // strips leading and trailing whitespace. 203 | // this is *not* a generic getInnerText function; it's special to sorttable. 204 | // for example, you can override the cell text with a customkey attribute. 205 | // it also gets .value for fields. 206 | 207 | if (!node) return ""; 208 | 209 | hasInputs = (typeof node.getElementsByTagName == 'function') && 210 | node.getElementsByTagName('input').length; 211 | 212 | if (node.nodeType == 1 && node.getAttribute("sorttable_customkey") !== null) { 213 | return node.getAttribute("sorttable_customkey"); 214 | } 215 | else if (typeof node.textContent != 'undefined' && !hasInputs) { 216 | return node.textContent.replace(/^\s+|\s+$/g, ''); 217 | } 218 | else if (typeof node.innerText != 'undefined' && !hasInputs) { 219 | return node.innerText.replace(/^\s+|\s+$/g, ''); 220 | } 221 | else if (typeof node.text != 'undefined' && !hasInputs) { 222 | return node.text.replace(/^\s+|\s+$/g, ''); 223 | } 224 | else { 225 | switch (node.nodeType) { 226 | case 3: 227 | if (node.nodeName.toLowerCase() == 'input') { 228 | return node.value.replace(/^\s+|\s+$/g, ''); 229 | } 230 | break; 231 | case 4: 232 | return node.nodeValue.replace(/^\s+|\s+$/g, ''); 233 | break; 234 | case 1: 235 | case 11: 236 | var innerText = ''; 237 | for (var i = 0; i < node.childNodes.length; i++) { 238 | innerText += sorttable.getInnerText(node.childNodes[i]); 239 | } 240 | return innerText.replace(/^\s+|\s+$/g, ''); 241 | break; 242 | default: 243 | return ''; 244 | } 245 | } 246 | }, 247 | 248 | reverse: function(tbody) { 249 | // reverse the rows in a tbody 250 | newrows = []; 251 | for (var i=0; i=0; i--) { 255 | tbody.appendChild(newrows[i]); 256 | } 257 | delete newrows; 258 | }, 259 | 260 | /* sort functions 261 | each sort function takes two parameters, a and b 262 | you are comparing a[0] and b[0] */ 263 | sort_numeric: function(a,b) { 264 | aa = parseFloat(a[0].replace(/[^0-9.-]/g,'')); 265 | if (isNaN(aa)) aa = 0; 266 | bb = parseFloat(b[0].replace(/[^0-9.-]/g,'')); 267 | if (isNaN(bb)) bb = 0; 268 | return aa-bb; 269 | }, 270 | sort_alpha: function(a,b) { 271 | return a[0].localeCompare(b[0]); 272 | /* 273 | if (a[0]==b[0]) return 0; 274 | if (a[0] 0 ) { 322 | q = list[i]; list[i] = list[i+1]; list[i+1] = q; 323 | swap = true; 324 | } 325 | } // for 326 | t--; 327 | 328 | if (!swap) break; 329 | 330 | for(i = t; i > b; --i) { 331 | if ( comp_func(list[i], list[i-1]) < 0 ) { 332 | q = list[i]; list[i] = list[i-1]; list[i-1] = q; 333 | swap = true; 334 | } 335 | } // for 336 | b++; 337 | 338 | } // while(swap) 339 | } 340 | }; 341 | 342 | /* ****************************************************************** 343 | Supporting functions: bundled here to avoid depending on a library 344 | ****************************************************************** */ 345 | 346 | // Dean Edwards/Matthias Miller/John Resig 347 | 348 | /* for Mozilla/Opera9 */ 349 | if (document.addEventListener) { 350 | document.addEventListener("DOMContentLoaded", sorttable.init, false); 351 | } 352 | 353 | /* for Internet Explorer */ 354 | /*@cc_on @*/ 355 | /*@if (@_win32) 356 | document.write("