├── linkedin.py ├── pic.jpg ├── .gitignore ├── templates ├── 1x1.gif ├── template.pdf ├── static │ ├── aws.png │ ├── dns.png │ ├── exe.png │ ├── logo.png │ ├── pdf.png │ ├── svn.png │ ├── web.png │ ├── word.png │ ├── email.png │ ├── folder.png │ ├── qrcode.png │ ├── favicon.png │ ├── goodtick.png │ ├── redirect.png │ ├── sqlserver.png │ ├── web_image.png │ ├── clonedsite.png │ ├── slack_icon.png │ ├── clonedsite copy.png │ ├── famfamfam-flags.png │ ├── site.js │ ├── sitemap.xml │ ├── clippy.svg │ ├── perfect-scrollbar.css │ ├── styles.min.css │ └── perfect-scrollbar.min.js ├── template.docx ├── robots.txt ├── emails │ ├── reset.txt │ ├── notification.txt │ ├── reset.html │ └── notification.html ├── error.html ├── error_http.html ├── fortune.html └── manage.html ├── constants.py ├── requirements.txt ├── authenticode.py ├── exception.py ├── setup_db.py ├── frontend.tac ├── redismanager.py ├── caa_monkeypatch.py ├── channel_output_twilio.py ├── channel_output_webhook.py ├── msword.py ├── pdfgen.py ├── users.py ├── tokens.py ├── sign_file.py ├── switchboard.py ├── channel_input_bitcoin.py ├── switchboard.tac ├── README.md ├── channel_input_linkedin.py ├── root-ca.conf ├── settings.py ├── channel_input_imgur.py ├── smtpd.tac ├── ziplib.py ├── t-sql.txt ├── channel_input_smtp.py ├── channel.py ├── channel_output_email.py ├── channel_http.py ├── canarydrop.py └── channel_dns.py /linkedin.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | -------------------------------------------------------------------------------- /pic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/product/canarytokens/master/pic.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | tokensvenv/* 4 | *.log 5 | *.pid 6 | .vscode 7 | -------------------------------------------------------------------------------- /templates/1x1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/product/canarytokens/master/templates/1x1.gif -------------------------------------------------------------------------------- /templates/template.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/product/canarytokens/master/templates/template.pdf -------------------------------------------------------------------------------- /templates/static/aws.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/product/canarytokens/master/templates/static/aws.png -------------------------------------------------------------------------------- /templates/static/dns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/product/canarytokens/master/templates/static/dns.png -------------------------------------------------------------------------------- /templates/static/exe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/product/canarytokens/master/templates/static/exe.png -------------------------------------------------------------------------------- /templates/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/product/canarytokens/master/templates/static/logo.png -------------------------------------------------------------------------------- /templates/static/pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/product/canarytokens/master/templates/static/pdf.png -------------------------------------------------------------------------------- /templates/static/svn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/product/canarytokens/master/templates/static/svn.png -------------------------------------------------------------------------------- /templates/static/web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/product/canarytokens/master/templates/static/web.png -------------------------------------------------------------------------------- /templates/static/word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/product/canarytokens/master/templates/static/word.png -------------------------------------------------------------------------------- /templates/template.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/product/canarytokens/master/templates/template.docx -------------------------------------------------------------------------------- /templates/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | Sitemap: http://canarytokens.org/resources/sitemap.xml 4 | -------------------------------------------------------------------------------- /templates/static/email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/product/canarytokens/master/templates/static/email.png -------------------------------------------------------------------------------- /templates/static/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/product/canarytokens/master/templates/static/folder.png -------------------------------------------------------------------------------- /templates/static/qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/product/canarytokens/master/templates/static/qrcode.png -------------------------------------------------------------------------------- /templates/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/product/canarytokens/master/templates/static/favicon.png -------------------------------------------------------------------------------- /templates/static/goodtick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/product/canarytokens/master/templates/static/goodtick.png -------------------------------------------------------------------------------- /templates/static/redirect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/product/canarytokens/master/templates/static/redirect.png -------------------------------------------------------------------------------- /templates/static/sqlserver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/product/canarytokens/master/templates/static/sqlserver.png -------------------------------------------------------------------------------- /templates/static/web_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/product/canarytokens/master/templates/static/web_image.png -------------------------------------------------------------------------------- /templates/static/clonedsite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/product/canarytokens/master/templates/static/clonedsite.png -------------------------------------------------------------------------------- /templates/static/slack_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/product/canarytokens/master/templates/static/slack_icon.png -------------------------------------------------------------------------------- /templates/static/clonedsite copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/product/canarytokens/master/templates/static/clonedsite copy.png -------------------------------------------------------------------------------- /templates/static/famfamfam-flags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/product/canarytokens/master/templates/static/famfamfam-flags.png -------------------------------------------------------------------------------- /templates/static/site.js: -------------------------------------------------------------------------------- 1 | if (!(/^(www\.|)canarytokens\.(com|org)$/i.test(document.domain))){ 2 | $('#mainsite').removeClass('hidden'); 3 | } 4 | -------------------------------------------------------------------------------- /constants.py: -------------------------------------------------------------------------------- 1 | 2 | OUTPUT_CHANNEL_EMAIL = 'Email' 3 | OUTPUT_CHANNEL_WEBHOOK = 'Webhook' 4 | OUTPUT_CHANNEL_TWILIO_SMS = 'TwilioSMS' 5 | 6 | INPUT_CHANNEL_HTTP = 'HTTP' 7 | INPUT_CHANNEL_DNS = 'DNS' 8 | INPUT_CHANNEL_IMGUR = 'Imgur' 9 | INPUT_CHANNEL_LINKEDIN = 'LinkedIn' 10 | INPUT_CHANNEL_BITCOIN = 'Bitcoin' 11 | INPUT_CHANNEL_SMTP = 'SMTP' 12 | -------------------------------------------------------------------------------- /templates/emails/reset.txt: -------------------------------------------------------------------------------- 1 | Hi! 2 | 3 | Someone (possibly you) initiated a password reset for Canary. 4 | 5 | To continue with the reset, please click this link: 6 | 7 | {{ url_for('reset_pass', hash=hash, _external=True) }} 8 | 9 | If you did not initiate the reset then you can ignore this 10 | as the reset link expires in 24 hours. 11 | 12 | Thanks 13 | Canary Team 14 | -------------------------------------------------------------------------------- /templates/static/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | http://canarytokens.org/generate 9 | 10 | 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyOpenSSL==17.5.0 2 | Jinja2==2.11.0 3 | MarkupSafe==0.23 4 | Twisted==19.10.0 5 | cssselect==0.9.1 6 | docopt==0.4.0 7 | gnureadline==6.3.3 8 | httplib2==0.9.1 9 | lxml==3.4.4 10 | mandrill==1.0.57 11 | pytz==2015.4 12 | redis==2.10.3 13 | requests==2.20.0 14 | simplejson==3.16.0 15 | six==1.10.0 16 | twilio==4.4.0 17 | twill==1.8.0 18 | wsgiref==0.1.2 19 | zope.interface==4.6.0 20 | PyQRCode==1.2.1 21 | pypng==0.0.18 22 | htmlmin==0.1.10 23 | sendgrid==3.6.5 24 | service_identity 25 | -------------------------------------------------------------------------------- /templates/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Canary Tokens 4 | 5 | 6 | 7 | 8 |

An error occurred!

9 |
10 | {{error}} 11 |
12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /templates/static/clippy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /templates/emails/notification.txt: -------------------------------------------------------------------------------- 1 | ====================================================================== 2 | ALERT 3 | ====================================================================== 4 | 5 | {{Intro}} 6 | 7 | Basic Information: 8 | Incident: {{Description}} 9 | Time : {{Timestamp}} 10 | {%if SourceIP %} 11 | Source : {{SourceIP}} 12 | {%endif%} 13 | Target : {{CanaryIP}} ({{CanaryName}}, id {{CanaryID}}) 14 | 15 | {% if AdditionalDetails %} 16 | Additional Details: 17 | {% for add_det in AdditionalDetails %} 18 | {{add_det[0]}}: {{add_det[1]}} 19 | {% endfor %} 20 | {%endif%} 21 | 22 | ====================================================================== 23 | -------------------------------------------------------------------------------- /authenticode.py: -------------------------------------------------------------------------------- 1 | from sign_file import authenticode_sign_binary 2 | 3 | import tempfile 4 | from os import unlink 5 | 6 | def make_canary_authenticode_binary(hostname=None, filebody=[]): 7 | unsigned_file = tempfile.NamedTemporaryFile(mode='w', delete=False) 8 | unsigned_file.write(filebody) 9 | unsigned_file.close() 10 | 11 | signed_file = tempfile.NamedTemporaryFile(delete=False) 12 | signed_file.close() 13 | 14 | authenticode_sign_binary(hostname, unsigned_file.name, signed_file.name) 15 | 16 | with open(signed_file.name) as f: 17 | contents = f.read() 18 | 19 | unlink(signed_file.name) 20 | unlink(unsigned_file.name) 21 | 22 | if len(contents) == 0: 23 | raise Exception('Could not sign this file.') 24 | return contents 25 | -------------------------------------------------------------------------------- /exception.py: -------------------------------------------------------------------------------- 1 | class UnknownAttribute(Exception): 2 | def __init__(self, attribute=None): 3 | self.message = '{attribute} is unrecognized'.format(attribute=attribute) 4 | 5 | class MissingAttribute(Exception): 6 | def __init__(self, attribute=None): 7 | self.message = '{attribute} is missing'.format(attribute=attribute) 8 | 9 | class NoCanarytokenPresent(Exception): 10 | def __init__(self, attribute=None): 11 | self.message = '{attribute} is unrecognized'.format(attribute=attribute) 12 | 13 | class NoCanarytokenFound(Exception): 14 | def __init__(self, haystack): 15 | self._haystack = haystack 16 | 17 | def __str__(self,): 18 | return 'No Canarytoken found in %s' % self._haystack 19 | 20 | class DuplicateChannel(Exception): 21 | pass 22 | 23 | class InvalidChannel(Exception): 24 | pass 25 | 26 | class NoUser(Exception): 27 | pass 28 | 29 | class LinkedInFailure(Exception): 30 | pass 31 | 32 | class BitcoinFailure(Exception): 33 | pass 34 | -------------------------------------------------------------------------------- /setup_db.py: -------------------------------------------------------------------------------- 1 | from queries import * 2 | import settings 3 | 4 | 5 | domains = settings.DOMAINS 6 | nxdomains = settings.NXDOMAINS 7 | google_api_key = settings.GOOGLE_API_KEY 8 | path_elements = ['about','feedback','static','terms','articles','images',\ 9 | 'tags','traffic'] 10 | pages = ['index.html','contact.php','post.jsp','submit.aspx'] 11 | 12 | print '[x] Adding domains' 13 | for d in domains: 14 | add_canary_domain(domain=d) 15 | print '\t{domain}'.format(domain=d) 16 | 17 | print '[x] Adding NX domains' 18 | for d in nxdomains: 19 | add_canary_nxdomain(domain=d) 20 | print '\t{domain}'.format(domain=d) 21 | 22 | print '[x] Adding path elements' 23 | for pe in path_elements: 24 | add_canary_path_element(path_element=pe) 25 | print '\t{pe}'.format(pe=pe) 26 | 27 | print '[x] Adding pages' 28 | for p in pages: 29 | add_canary_page(page=p) 30 | print '\t{p}'.format(p=p) 31 | 32 | print '[x] Adding google api key' 33 | for k in google_api_key: 34 | add_canary_google_api_key(key=k) 35 | print '\t{k}'.format(k=k) 36 | -------------------------------------------------------------------------------- /frontend.tac: -------------------------------------------------------------------------------- 1 | import sys, os 2 | import logging 3 | sys.path.append(os.path.abspath(os.path.dirname(__file__))) 4 | from twisted.names import server 5 | from twisted.application import service, internet 6 | 7 | from httpd_site import CanarytokensHttpd 8 | from switchboard import Switchboard 9 | 10 | import setup_db 11 | 12 | from twisted.logger import ILogObserver, textFileLogObserver 13 | from twisted.python import logfile 14 | import settings 15 | 16 | logging.basicConfig() 17 | logger = logging.getLogger('generator_httpd') 18 | logger.setLevel(logging.DEBUG) 19 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 20 | 21 | logger.debug('Canarydrops generator HTTPd') 22 | 23 | application = service.Application("Canarydrops Generator Web Server") 24 | f = logfile.LogFile.fromFullPath(settings.LOG_FILE, rotateLength=settings.FRONTEND_LOG_SIZE, 25 | maxRotatedFiles=settings.FRONTEND_LOG_COUNT) 26 | application.setComponent(ILogObserver, textFileLogObserver(f)) 27 | 28 | canarytokens_httpd = CanarytokensHttpd(port=settings.CANARYTOKENS_HTTP_PORT) 29 | canarytokens_httpd.service.setServiceParent(application) 30 | -------------------------------------------------------------------------------- /redismanager.py: -------------------------------------------------------------------------------- 1 | import redis 2 | 3 | import settings 4 | 5 | db = redis.StrictRedis(host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=settings.REDIS_DB, socket_timeout=10) 6 | try: 7 | db.ping() 8 | except Exception as e: 9 | print 'Could not connect to redis, bailing: {e}'.format(e=e) 10 | import sys 11 | sys.exit(1) 12 | 13 | #db.DEFAULT_EXPIRY = 120 14 | 15 | KEY_CANARYDROP = 'canarydrop:' 16 | KEY_CANARYDROPS_TIMELINE = 'canarydrops_timeline:' 17 | KEY_CANARY_DOMAINS = 'canary_domains' 18 | KEY_CANARY_NXDOMAINS = 'canary_nxdomains' 19 | KEY_CANARY_GOOGLE_API_KEY = 'canary_google_api_key' 20 | KEY_CANARY_PATH_ELEMENTS = 'canary_path_elements' 21 | KEY_CANARY_PAGES = 'canary_pages' 22 | KEY_USER_ACCOUNT = 'account:' 23 | KEY_CANARYTOKEN_ALERT_COUNT = 'canarytoken_alert_count:' 24 | KEY_IMGUR_TOKEN = 'imgur_token:' 25 | KEY_IMGUR_TOKENS = 'imgur_tokens' 26 | KEY_LINKEDIN_ACCOUNTS = 'linkedin_accounts' 27 | KEY_LINKEDIN_ACCOUNT = 'linkedin_account:' 28 | KEY_BITCOIN_ACCOUNTS = 'bitcoin_accounts' 29 | KEY_BITCOIN_ACCOUNT = 'bitcoin_account:' 30 | KEY_CLONEDSITE_TOKEN = 'cloned_site:' 31 | KEY_CLONEDSITE_TOKENS = 'cloned_sites' 32 | KEY_CANARY_IP_CACHE = 'geo_ip_cache:' 33 | KEY_TOR_EXIT_NODES = 'tor_exit_nodes' 34 | -------------------------------------------------------------------------------- /caa_monkeypatch.py: -------------------------------------------------------------------------------- 1 | # We need to monkey patch support for CAA Records because it is a newish DNS record type 2 | # that is required by the lets encrypt process. Basically we need to respond to CAA record 3 | # requests with anything besides a SERVFAIL, hence we respond with an NXDomain response. 4 | 5 | def monkey_patch_caa_support(): 6 | patchDNSModule() 7 | patchCommonModule() 8 | patchResolveModule() 9 | 10 | 11 | def patchDNSModule(): 12 | import twisted.names.dns 13 | twisted.names.dns.CAA = 257 14 | twisted.names.dns.QUERY_TYPES[twisted.names.dns.CAA] = "CAA" 15 | 16 | 17 | def patchCommonModule(): 18 | import twisted.names.common 19 | twisted.names.common.typeToMethod[twisted.names.dns.CAA] = 'lookupCAA' 20 | def lookupCAA(self, name, timeout=None): 21 | return self._lookup(name, twisted.names.dns.IN, twisted.names.dns.CAA, timeout) 22 | 23 | 24 | twisted.names.common.ResolverBase.lookupCAA = lookupCAA 25 | 26 | 27 | def patchResolveModule(): 28 | import twisted.names.resolve 29 | from twisted.names import error 30 | from twisted.internet import defer 31 | def lookupCAA(self, name, timeout=None): 32 | if not self.resolvers: 33 | return defer.fail(error.DomainError()) 34 | d = self.resolvers[0].lookupCAA(name, timeout) 35 | for r in self.resolvers[1:]: 36 | d = d.addErrback( 37 | twisted.names.resolve.FailureHandler(r.lookupCAA, name, timeout) 38 | ) 39 | return d 40 | 41 | twisted.names.resolve.ResolverChain.lookupCAA = lookupCAA -------------------------------------------------------------------------------- /channel_output_twilio.py: -------------------------------------------------------------------------------- 1 | """ 2 | Output channel that sends SMSs. Relies on Twilio to actually send SMSs. 3 | """ 4 | import settings 5 | import pprint 6 | 7 | from twisted.python import log 8 | from twilio.rest import TwilioRestClient 9 | 10 | from channel import OutputChannel 11 | from constants import OUTPUT_CHANNEL_TWILIO_SMS 12 | import settings 13 | 14 | class TwilioOutputChannel(OutputChannel): 15 | CHANNEL = OUTPUT_CHANNEL_TWILIO_SMS 16 | 17 | 18 | def do_send_alert(self, input_channel=None, canarydrop=None, **kwargs): 19 | try: 20 | msg = input_channel.format_canaryalert( 21 | params={'body_length':140}, 22 | canarydrop=canarydrop, 23 | **kwargs) 24 | 25 | client = TwilioRestClient(settings.TWILIO_ACCOUNT_SID, settings.TWILIO_AUTH_TOKEN) 26 | 27 | if len(canarydrop['alert_sms_recipient']) == 0: 28 | raise Exception('No SMS recipient for token: {token}' 29 | .format(token=canarydrop['canarytoken'])) 30 | 31 | if settings.DEBUG: 32 | pprint.pprint(msg) 33 | else: 34 | client.messages.create( 35 | to=canarydrop['alert_sms_recipient'], 36 | from_=settings.TWILIO_FROM_NUMBER, 37 | body=msg['body'] 38 | ) 39 | log.msg('Sent SMS alert to {recipient} for token {token}' 40 | .format(recipient=canarydrop['alert_sms_recipient'], 41 | token=canarydrop.canarytoken.value())) 42 | except Exception as e: 43 | log.err('Twilio send failed: {error}'.format(error=e)) 44 | -------------------------------------------------------------------------------- /channel_output_webhook.py: -------------------------------------------------------------------------------- 1 | """ 2 | Output channel that sends to webhooks. 3 | """ 4 | import settings 5 | import pprint 6 | 7 | from twisted.python import log 8 | import requests 9 | import simplejson 10 | 11 | from channel import OutputChannel 12 | from constants import OUTPUT_CHANNEL_WEBHOOK 13 | 14 | class WebhookOutputChannel(OutputChannel): 15 | CHANNEL = OUTPUT_CHANNEL_WEBHOOK 16 | 17 | 18 | def do_send_alert(self, input_channel=None, canarydrop=None, **kwargs): 19 | 20 | slack = "https://hooks.slack.com" 21 | 22 | try: 23 | if (slack in canarydrop['alert_webhook_url']): 24 | payload = input_channel.format_slack_canaryalert( 25 | canarydrop=canarydrop, 26 | **kwargs) 27 | else: 28 | payload = input_channel.format_webhook_canaryalert( 29 | canarydrop=canarydrop, 30 | **kwargs) 31 | 32 | self.generic_webhook_send(simplejson.dumps(payload), canarydrop) 33 | except Exception as e: 34 | log.err(e) 35 | 36 | def generic_webhook_send(self, payload=None, canarydrop=None): 37 | 38 | try: 39 | response = requests.post(canarydrop['alert_webhook_url'], payload, headers={'content-type': 'application/json'}) 40 | response.raise_for_status() 41 | log.msg('Webhook sent to {url}'.format(url=canarydrop['alert_webhook_url'])) 42 | return None 43 | except requests.exceptions.RequestException as e: 44 | log.err("Failed sending request to webhook {url} with error {error}".format(url=canarydrop['alert_webhook_url'],error=e)) 45 | return e 46 | -------------------------------------------------------------------------------- /msword.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | import shutil 3 | import datetime 4 | import random 5 | from zipfile import ZipFile, ZipInfo 6 | from ziplib import MODE_DIRECTORY 7 | from cStringIO import StringIO 8 | 9 | import settings 10 | 11 | WORD_TEMPLATE=settings.CANARY_WORD_TEMPLATE 12 | 13 | def zipinfo_contents_replace(zipfile=None, zipinfo=None, 14 | search=None, replace=None): 15 | """Given an entry in a zip file, extract the file and perform a search 16 | and replace on the contents. Returns the contents as a string.""" 17 | dirname = tempfile.mkdtemp() 18 | fname = zipfile.extract(zipinfo, dirname) 19 | with open(fname, 'r') as fd: 20 | contents = fd.read().replace(search, replace) 21 | shutil.rmtree(dirname) 22 | return contents 23 | 24 | def make_canary_msword(url=None, template=WORD_TEMPLATE): 25 | with open(template, 'r') as f: 26 | input_buf = StringIO(f.read()) 27 | output_buf = StringIO() 28 | output_zip = ZipFile(output_buf, 'w') 29 | now = datetime.datetime.now() 30 | now_ts = format_time_for_doc(now) 31 | created_ts = format_time_for_doc(now - datetime.timedelta( 32 | days=random.randint(1,25), 33 | hours=random.randint(1,24), 34 | seconds=random.randint(1,60))) 35 | with ZipFile(input_buf, 'r') as doc: 36 | for entry in doc.filelist: 37 | if entry.external_attr & MODE_DIRECTORY: 38 | continue 39 | 40 | contents = zipinfo_contents_replace(zipfile=doc, zipinfo=entry, 41 | search="HONEYDROP_TOKEN_URL", replace=url) 42 | contents = contents.replace("aaaaaaaaaaaaaaaaaaaa", created_ts) 43 | contents = contents.replace("bbbbbbbbbbbbbbbbbbbb", now_ts) 44 | output_zip.writestr(entry, contents) 45 | output_zip.close() 46 | return output_buf.getvalue() 47 | 48 | def format_time_for_doc(time): 49 | return time.strftime('%Y-%m-%d')+'T'+ time.strftime('%H:%M:%S')+'Z' 50 | 51 | if __name__ == '__main__': 52 | with open('testdoc.docx', 'w+') as f: 53 | f.write(make_canary_msword(url="http://iamatesturlforcanarys.net/blah.png")) 54 | -------------------------------------------------------------------------------- /pdfgen.py: -------------------------------------------------------------------------------- 1 | import zlib 2 | from cStringIO import StringIO 3 | import re 4 | import sys 5 | import random 6 | 7 | import settings 8 | 9 | PDF_FILE=settings.CANARY_PDF_TEMPLATE 10 | STREAM_OFFSET=settings.CANARY_PDF_TEMPLATE_OFFSET 11 | 12 | def _substitute_stream(header=None, stream=None, search='abcdefghijklmnopqrstuvwxyz.zyxwvutsrqponmlkjihgfedcba.aceegikmoqsuwy.bdfhjlnprtvxz', replace=None): 13 | #Ohhhh, this is nasty. Instead of trying to get the xref positions right, 14 | #we're going to brute-force a URL that's the right size after compression. 15 | #Give up after 1000 attempts. 16 | old_len = len(stream) 17 | candidate_stream = zlib.compress(zlib.decompress(stream).replace(search, replace)) 18 | count = 1 19 | while len(candidate_stream) < old_len and count < 1000: 20 | padding = ''.join([ chr(random.randrange(65,90)) for x in range(0,count)]) 21 | candidate_stream = zlib.compress(zlib.decompress(stream).replace(search, replace+'/'+padding)) 22 | count += 1 23 | 24 | #header should be the same size, no need to recalc 25 | #header = re.sub(r'Length [0-9]+', 'Length {len}'.format(len=len(new_stream)), header) 26 | if old_len != len(candidate_stream): 27 | raise Exception('Dammit, new PDF is too big ({new_len} > {old_len})' 28 | .format(new_len=len(candidate_stream), old_len=old_len)) 29 | 30 | return (header, candidate_stream) 31 | 32 | def make_canary_pdf(hostname=None): 33 | f = open(PDF_FILE, 'r') 34 | contents = f.read() 35 | f.close() 36 | 37 | stream_size = int(re.match(r'.*\/Length ([0-9]+)\/.*', contents[STREAM_OFFSET:]).group(1)) 38 | stream_start = STREAM_OFFSET+contents[STREAM_OFFSET:].index('stream\r\n')+8 39 | stream_header = contents[STREAM_OFFSET:stream_start] 40 | stream = contents[stream_start:stream_start+stream_size] 41 | 42 | (stream_header, stream) = _substitute_stream(header=stream_header, 43 | stream=stream, 44 | replace=hostname) 45 | 46 | output = StringIO() 47 | output.write(contents[0:STREAM_OFFSET]) 48 | output.write(stream_header) 49 | output.write(stream) 50 | output.write(contents[stream_start+stream_size:]) 51 | new_contents = output.getvalue() 52 | output.close() 53 | 54 | return new_contents 55 | 56 | 57 | -------------------------------------------------------------------------------- /users.py: -------------------------------------------------------------------------------- 1 | """ 2 | Class that encapsulates a user identity. Unused for now. 3 | """ 4 | 5 | from exception import UnknownAttribute, MissingAttribute 6 | from queries import lookup_canarytoken_alert_count, save_canarytoken_alert_count 7 | import settings 8 | 9 | class User(object): 10 | allowed_attrs = ['username', 'alert_count'] 11 | 12 | def __init__(self, alert_expiry=1, alert_limit=100, **kwargs): 13 | """Return a new UserPolicy object. 14 | 15 | Arguments: 16 | 17 | alert_expiry -- The delay after a successful alert after which 18 | the limit no longer applies. 19 | alert_limit -- The number of alerts allowed. 20 | """ 21 | 22 | self.alert_expiry = alert_expiry 23 | self.alert_limit = alert_limit 24 | 25 | self._user = {} 26 | for k, v in kwargs.iteritems(): 27 | if k not in self.allowed_attrs: 28 | raise UnknownAttribute(attribute=k) 29 | self._user[k] = v 30 | 31 | if 'username' not in self._user: 32 | raise MissingAttribute(attribute=username) 33 | 34 | def is_anonymous(self,): 35 | return self._user['username'] == 'Anonymous' 36 | 37 | def can_send_alert(self, canarydrop=None): 38 | try: 39 | alert_count = int(lookup_canarytoken_alert_count( 40 | canarydrop.canarytoken)) 41 | except TypeError: 42 | return True 43 | 44 | if alert_count + 1 <= self.alert_limit: 45 | return True 46 | 47 | return False 48 | 49 | def do_accounting(self, canarydrop=None): 50 | try: 51 | alert_count = int(lookup_canarytoken_alert_count( 52 | canarydrop.canarytoken))+1 53 | except TypeError: 54 | alert_count = 1 55 | 56 | save_canarytoken_alert_count(canarydrop.canarytoken, alert_count, 57 | self.alert_expiry) 58 | 59 | @property 60 | def username(self,): 61 | return self._user['username'] 62 | 63 | class AnonymousUser(User): 64 | """Represents an anonymous user. These users have lower limits than 65 | regular users.""" 66 | def __init__(self): 67 | User.__init__(self, username='Anonymous', 68 | alert_expiry=(5 if settings.DEBUG else 60), 69 | alert_limit=1) 70 | -------------------------------------------------------------------------------- /tokens.py: -------------------------------------------------------------------------------- 1 | import re 2 | import random 3 | from exception import NoCanarytokenFound 4 | 5 | canarytoken_ALPHABET=['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 6 | 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 7 | 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', 8 | '4', '5', '6', '7', '8', '9'] 9 | canarytoken_LENGTH=25 # equivalent to 128-bit id 10 | 11 | class Canarytoken(object): 12 | CANARY_RE = re.compile('.*(['+''.join(canarytoken_ALPHABET)+']{'+ 13 | str(canarytoken_LENGTH)+'}).*', re.IGNORECASE) 14 | def __init__(self, value=None): 15 | """Create a new Canarytoken instance. If no value was provided, 16 | generate a new canarytoken. 17 | 18 | Arguments: 19 | value -- A user-provided canarytoken. It's format will be validated. 20 | 21 | Exceptions: 22 | NoCanarytokenFound - Thrown if the supplied canarytoken is not in the 23 | correct format. 24 | """ 25 | 26 | if value: 27 | self._value = self.find_canarytoken(value).lower() 28 | else: 29 | self._value = Canarytoken.generate() 30 | 31 | @staticmethod 32 | def generate(): 33 | """Return a new canarytoken.""" 34 | return ''.join([ canarytoken_ALPHABET[ 35 | random.randint(0,len(canarytoken_ALPHABET)-1) 36 | ] for x in range(0, canarytoken_LENGTH)]) 37 | 38 | @staticmethod 39 | def find_canarytoken(haystack): 40 | """Return the canarytoken found in haystack. 41 | 42 | Arguments: 43 | haystack -- A string that might include a canarytoken. 44 | 45 | Exceptions: 46 | NoCanarytokenFound 47 | """ 48 | m = Canarytoken.CANARY_RE.match(haystack) 49 | if not m: 50 | raise NoCanarytokenFound(haystack) 51 | 52 | return m.group(1) 53 | 54 | def value(self,): 55 | return self._value 56 | 57 | def __repr__(self,): 58 | return '' % self._value 59 | 60 | if __name__ == '__main__': 61 | print Canarytoken() 62 | token = Canarytoken().value() 63 | print token 64 | print Canarytoken(value=token) 65 | 66 | bad_tokens = [] 67 | #short value 68 | bad_tokens.append(token[:1]) 69 | 70 | #invalid char token 71 | bad_tokens.append('!'+token[1:]) 72 | 73 | for t in bad_tokens: 74 | try: 75 | print Canarytoken(value=t) 76 | assert False 77 | except NoCanarytokenFound: 78 | print 'Invalid token %s detected' % t 79 | -------------------------------------------------------------------------------- /sign_file.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import getopt 3 | import os 4 | import tempfile 5 | import shutil 6 | 7 | def authenticode_sign_binary(token, inputfile, outputfile): 8 | try: 9 | tmpdir = tempfile.mkdtemp() 10 | 11 | os.system('cp root-ca.conf {tmpdir}/root-ca.conf'.format(tmpdir=tmpdir)) 12 | 13 | f = open('{tmpdir}/root-ca.conf'.format(tmpdir=tmpdir),'r') 14 | filedata = f.read() 15 | f.close() 16 | 17 | newdata = filedata.replace('_TOKEN_', token) 18 | newdata = newdata.replace('TMPDIR', tmpdir) 19 | 20 | f = open('{tmpdir}/root-ca.conf'.format(tmpdir=tmpdir),'w') 21 | f.write(newdata) 22 | f.close() 23 | 24 | os.system('echo "00"> {tmpdir}/ser'.format(tmpdir=tmpdir)) 25 | os.system('touch {tmpdir}/db {tmpdir}/db.attr'.format(tmpdir=tmpdir)) 26 | os.system('openssl req -x509 -new -keyout {tmpdir}/rootCA.key -out {tmpdir}/rootCA.crt -config {tmpdir}/root-ca.conf -days 365 -nodes'.format(tmpdir=tmpdir)) 27 | os.system('openssl req -new -keyout {tmpdir}/cert.key -out {tmpdir}/cert.csr -nodes -subj "/C=US/ST=Washington/L=Redmond/O=Microsoft Corporation/CN=Microsoft Windows"'.format(tmpdir=tmpdir)) 28 | os.system('openssl ca -batch -config {tmpdir}/root-ca.conf -cert {tmpdir}/rootCA.crt -keyfile {tmpdir}/rootCA.key -in {tmpdir}/cert.csr -out {tmpdir}/cert.crt'.format(tmpdir=tmpdir,)) 29 | os.system('osslsigncode sign -certs {tmpdir}/cert.crt -key {tmpdir}/cert.key -in {inputfile} -out {outputfile}'.format(tmpdir=tmpdir,inputfile=inputfile,outputfile=outputfile)) 30 | except Exception as e: 31 | print e 32 | finally: 33 | shutil.rmtree(tmpdir, ignore_errors=True) 34 | 35 | def main(argv): 36 | token = None 37 | inputfile = None 38 | outputfile = None 39 | try: 40 | opts, args = getopt.getopt(argv,"ht:f:o:",["token=","inputfile=","outputfile="]) 41 | except getopt.GetoptError: 42 | print 'usage: sign_file.py -t -f -o ' 43 | sys.exit(2) 44 | for opt, arg in opts: 45 | if opt == '-h': 46 | print 'usage: sign_file.py -t -f -o ' 47 | sys.exit() 48 | elif opt in ("-t", "--token"): 49 | token = arg 50 | elif opt in ("-f", "--inputfile"): 51 | inputfile = arg 52 | elif opt in ("-o", "--outputfile"): 53 | outputfile = arg 54 | 55 | if not inputfile or not token or not outputfile: 56 | print 'usage: sign_file.py -t -f -o ' 57 | sys.exit() 58 | 59 | if not os.path.isfile(inputfile): 60 | print 'File does not exist' 61 | sys.exit() 62 | 63 | if not any(x == os.path.splitext(inputfile)[1] for x in ['.dll','.exe']): 64 | print 'File can only be dll or exe' 65 | sys.exit() 66 | 67 | authenticode_sign_binary(token, inputfile, outputfile) 68 | 69 | if __name__ == "__main__": 70 | main(sys.argv[1:]) 71 | -------------------------------------------------------------------------------- /templates/static/perfect-scrollbar.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Container style 3 | */ 4 | .ps { 5 | overflow: hidden !important; 6 | overflow-anchor: none; 7 | -ms-overflow-style: none; 8 | touch-action: auto; 9 | -ms-touch-action: auto; 10 | } 11 | 12 | /* 13 | * Scrollbar rail styles 14 | */ 15 | .ps__rail-x { 16 | display: none; 17 | opacity: 0; 18 | transition: background-color .2s linear, opacity .2s linear; 19 | -webkit-transition: background-color .2s linear, opacity .2s linear; 20 | height: 15px; 21 | /* there must be 'bottom' or 'top' for ps__rail-x */ 22 | bottom: 0px; 23 | /* please don't change 'position' */ 24 | position: absolute; 25 | } 26 | 27 | .ps__rail-y { 28 | display: none; 29 | opacity: 0; 30 | transition: background-color .2s linear, opacity .2s linear; 31 | -webkit-transition: background-color .2s linear, opacity .2s linear; 32 | width: 15px; 33 | /* there must be 'right' or 'left' for ps__rail-y */ 34 | right: 0; 35 | /* please don't change 'position' */ 36 | position: absolute; 37 | } 38 | 39 | .ps--active-x > .ps__rail-x, 40 | .ps--active-y > .ps__rail-y { 41 | display: block; 42 | background-color: transparent; 43 | } 44 | 45 | .ps:hover > .ps__rail-x, 46 | .ps:hover > .ps__rail-y, 47 | .ps--focus > .ps__rail-x, 48 | .ps--focus > .ps__rail-y, 49 | .ps--scrolling-x > .ps__rail-x, 50 | .ps--scrolling-y > .ps__rail-y { 51 | opacity: 0.6; 52 | } 53 | 54 | .ps__rail-x:hover, 55 | .ps__rail-y:hover, 56 | .ps__rail-x:focus, 57 | .ps__rail-y:focus { 58 | background-color: #eee; 59 | opacity: 0.9; 60 | } 61 | 62 | /* 63 | * Scrollbar thumb styles 64 | */ 65 | .ps__thumb-x { 66 | background-color: #aaa; 67 | border-radius: 6px; 68 | transition: background-color .2s linear, height .2s ease-in-out; 69 | -webkit-transition: background-color .2s linear, height .2s ease-in-out; 70 | height: 6px; 71 | /* there must be 'bottom' for ps__thumb-x */ 72 | bottom: 2px; 73 | /* please don't change 'position' */ 74 | position: absolute; 75 | } 76 | 77 | .ps__thumb-y { 78 | background-color: #aaa; 79 | border-radius: 6px; 80 | transition: background-color .2s linear, width .2s ease-in-out; 81 | -webkit-transition: background-color .2s linear, width .2s ease-in-out; 82 | width: 6px; 83 | /* there must be 'right' for ps__thumb-y */ 84 | right: 2px; 85 | /* please don't change 'position' */ 86 | position: absolute; 87 | } 88 | 89 | .ps__rail-x:hover > .ps__thumb-x, 90 | .ps__rail-x:focus > .ps__thumb-x { 91 | background-color: #999; 92 | height: 11px; 93 | } 94 | 95 | .ps__rail-y:hover > .ps__thumb-y, 96 | .ps__rail-y:focus > .ps__thumb-y { 97 | background-color: #999; 98 | width: 11px; 99 | } 100 | 101 | /* MS supports */ 102 | @supports (-ms-overflow-style: none) { 103 | .ps { 104 | overflow: auto !important; 105 | } 106 | } 107 | 108 | @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { 109 | .ps { 110 | overflow: auto !important; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /switchboard.py: -------------------------------------------------------------------------------- 1 | """ 2 | Class that receives alerts, and dispatches them to the registered endpoint. 3 | """ 4 | 5 | from twisted.python import log 6 | 7 | from exception import DuplicateChannel, InvalidChannel 8 | 9 | class Switchboard(object): 10 | def __init__(self,): 11 | """Return a new Switchboard instance.""" 12 | self.input_channels = {} 13 | self.output_channels = {} 14 | 15 | def add_input_channel(self, name=None, channel=None): 16 | """Register a new input channel with the switchboard. 17 | 18 | Arguments: 19 | name -- unique name for the input channel 20 | formatters -- a dict in the form { TYPE: METHOD,...} used to lookup 21 | the channel's format method depending on the alert type 22 | """ 23 | if self.input_channels.has_key(name): 24 | raise DuplicateChannel() 25 | 26 | self.input_channels[name] = channel 27 | 28 | def add_output_channel(self, name=None, channel=None): 29 | """Register a new output channel with the switchboard. 30 | 31 | Arguments: 32 | name -- unique name for the input channel 33 | formatters -- a dict in the form { TYPE: METHOD,...} used to lookup 34 | the channel's format method depending on the alert type 35 | """ 36 | if self.output_channels.has_key(name): 37 | raise DuplicateChannel() 38 | 39 | self.output_channels[name] = channel 40 | 41 | def dispatch(self, input_channel=None, canarydrop=None, **kwargs): 42 | """Calls the correct alerting method for the trigger and channel combination. 43 | 44 | For now it prints to stdout. 45 | 46 | TODO: this spawns threads to actually do the alerting 47 | 48 | Arguments: 49 | input_channel -- name of the channel on which the alert originated 50 | canarydrop -- a Canarydrop instance 51 | **kwargs -- passed to the channel instance's formatter methods 52 | """ 53 | try: 54 | if not self.input_channels.has_key(input_channel): 55 | raise InvalidChannel() 56 | 57 | canarydrop.add_canarydrop_hit(input_channel=input_channel, **kwargs) 58 | 59 | if not canarydrop.alertable(): 60 | log.err('Token {token} is not alertable at this stage.'\ 61 | .format(token=canarydrop.canarytoken.value())) 62 | return 63 | 64 | #update accounting info 65 | canarydrop.alerting(input_channel=input_channel,**kwargs) 66 | 67 | for requested_output_channel in canarydrop.get_requested_output_channels(): 68 | try: 69 | output_channel = self.output_channels[requested_output_channel] 70 | output_channel.send_alert(canarydrop=canarydrop, 71 | input_channel=self.input_channels[input_channel], 72 | **kwargs) 73 | except KeyError as e: 74 | raise Exception('Error sending alert: {err}'.format(err=e.message)) 75 | except Exception as e: 76 | log.err('Exception occurred in switchboard dispatch: {err}'.format(err=e)) 77 | -------------------------------------------------------------------------------- /channel_input_bitcoin.py: -------------------------------------------------------------------------------- 1 | import simplejson 2 | import twill 3 | 4 | from twisted.application.internet import TimerService 5 | from twisted.python import log 6 | 7 | from canarydrop import Canarydrop 8 | from channel import InputChannel 9 | from queries import get_canarydrop, get_all_bitcoin_accounts,\ 10 | save_bitcoin_account, get_bitcoin_address_balance 11 | from exception import BitcoinFailure 12 | from constants import INPUT_CHANNEL_BITCOIN 13 | 14 | class ChannelBitcoin(InputChannel): 15 | """Input channel that polls for payments from a Bitcoin address, and 16 | alerts when they climb.""" 17 | CHANNEL = INPUT_CHANNEL_BITCOIN 18 | 19 | def __init__(self, min_delay=3600*24, switchboard=None): 20 | log.msg('Started channel {name}'.format(name=self.CHANNEL)) 21 | super(ChannelBitcoin, self).__init__(switchboard=switchboard, 22 | name=self.CHANNEL) 23 | self.min_delay = min_delay 24 | self.service = TimerService(self.min_delay, self.schedule_polling) 25 | 26 | def schedule_polling(self,): 27 | """A dummy method. For now, all balance polls are run immediately. 28 | In the future they'll be spread out over the interval.""" 29 | try: 30 | for bitcoin_account in get_all_bitcoin_accounts(): 31 | self.poll(bitcoin_account=bitcoin_account) 32 | except Exception as e: 33 | log.err('Bitcoin error: {error}'.format(error=e)) 34 | 35 | def poll(self, bitcoin_account=None): 36 | try: 37 | current_balance = get_bitcoin_address_balance( 38 | address=bitcoin_account['address']) 39 | except BitcoinFailure as e: 40 | log.err('Could not retrieve bitcoin balance: {error}'\ 41 | .format(error=e)) 42 | return 43 | 44 | if current_balance> bitcoin_account['balance']: 45 | canarydrop = Canarydrop(**get_canarydrop( 46 | canarytoken=bitcoin_account['canarytoken'])) 47 | self.dispatch(canarydrop=canarydrop, new_balance=current_balance, 48 | old_balance=bitcoin_account['balance'], 49 | address=bitcoin_account['address']) 50 | bitcoin_account['balance'] = current_balance 51 | save_bitcoin_account(bitcoin_account=bitcoin_account) 52 | 53 | def format_additional_data(self, **kwargs): 54 | log.msg('%r' % kwargs) 55 | additional_report = '' 56 | if kwargs.has_key('address') and kwargs['address']: 57 | additional_report += 'Bitcoin Address: {address}\r\n'.format( 58 | address=kwargs['address']) 59 | if kwargs.has_key('new_balance') and kwargs['new_balance'] and\ 60 | kwargs.has_key('old_balance') and kwargs['old_balance']: 61 | additional_report += 'Balance Changed: from {old_balance} to {new_balance}\r\n'.format( 62 | old_balance=kwargs['old_balance'], 63 | new_balance=kwargs['new_balance'], 64 | ) 65 | return additional_report 66 | -------------------------------------------------------------------------------- /switchboard.tac: -------------------------------------------------------------------------------- 1 | import sys, os 2 | sys.path.append(os.path.abspath(os.path.dirname(__file__))) 3 | from twisted.names import dns, server 4 | from caa_monkeypatch import monkey_patch_caa_support 5 | monkey_patch_caa_support() 6 | from twisted.application import service, internet 7 | from twisted.python import log 8 | from twisted.logger import ILogObserver, textFileLogObserver 9 | from twisted.python import logfile 10 | 11 | import settings 12 | from channel_dns import DNSServerFactory, ChannelDNS 13 | from channel_http import ChannelHTTP 14 | from channel_input_imgur import ChannelImgur 15 | from channel_input_linkedin import ChannelLinkedIn 16 | from channel_input_bitcoin import ChannelBitcoin 17 | from channel_input_smtp import ChannelSMTP 18 | from channel_output_email import EmailOutputChannel 19 | from channel_output_twilio import TwilioOutputChannel 20 | from channel_output_webhook import WebhookOutputChannel 21 | from switchboard import Switchboard 22 | 23 | from queries import update_tor_exit_nodes_loop 24 | 25 | log.msg('Canarydrops switchboard started') 26 | 27 | application = service.Application("Canarydrops Switchboard") 28 | 29 | f = logfile.LogFile.fromFullPath(settings.LOG_FILE, rotateLength=settings.SWITCHBOARD_LOG_SIZE, 30 | maxRotatedFiles=settings.SWITCHBOARD_LOG_COUNT) 31 | application.setComponent(ILogObserver, textFileLogObserver(f)) 32 | 33 | switchboard = Switchboard() 34 | 35 | email_output_channel = EmailOutputChannel(switchboard=switchboard) 36 | twilio_output_channel = TwilioOutputChannel(switchboard=switchboard) 37 | webhook_output_channel = WebhookOutputChannel(switchboard=switchboard) 38 | 39 | dns_service = service.MultiService() 40 | 41 | factory = DNSServerFactory( 42 | clients=[ChannelDNS(listen_domain=settings.LISTEN_DOMAIN, 43 | switchboard=switchboard)] 44 | ) 45 | udp_factory = dns.DNSDatagramProtocol(factory) 46 | internet.TCPServer(settings.CHANNEL_DNS_PORT, factory)\ 47 | .setServiceParent(dns_service) 48 | internet.UDPServer(settings.CHANNEL_DNS_PORT, udp_factory)\ 49 | .setServiceParent(dns_service) 50 | dns_service.setServiceParent(application) 51 | 52 | canarytokens_httpd = ChannelHTTP(port=settings.CHANNEL_HTTP_PORT, 53 | switchboard=switchboard) 54 | canarytokens_httpd.service.setServiceParent(application) 55 | 56 | canarytokens_imgur = ChannelImgur(min_delay=settings.CHANNEL_IMGUR_MIN_DELAY, 57 | switchboard=switchboard) 58 | canarytokens_imgur.service.setServiceParent(application) 59 | 60 | canarytokens_linkedin = ChannelLinkedIn(min_delay=settings.CHANNEL_LINKEDIN_MIN_DELAY, 61 | switchboard=switchboard) 62 | canarytokens_linkedin.service.setServiceParent(application) 63 | 64 | canarytokens_bitcoin = ChannelBitcoin(min_delay=settings.CHANNEL_BITCOIN_MIN_DELAY, 65 | switchboard=switchboard) 66 | canarytokens_bitcoin.service.setServiceParent(application) 67 | 68 | canarytokens_smtp = ChannelSMTP(port=settings.CHANNEL_SMTP_PORT, 69 | switchboard=switchboard) 70 | canarytokens_smtp.service.setServiceParent(application) 71 | 72 | 73 | #loop to update tor exit nodes every 30 min 74 | loop_http = internet.task.LoopingCall(update_tor_exit_nodes_loop) 75 | loop_http.start(1800) 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Canarytokens 4 | ============= 5 | by Thinkst Applied Research 6 | 7 | Overview 8 | -------- 9 | Canarytokens helps track activity and actions on your network. 10 | 11 | Installation 12 | ------------ 13 | We recommend [the Docker image installation process](https://github.com/thinkst/canarytokens-docker). 14 | 15 | Configuration 16 | ------------- 17 | 18 | The Canarytokens server can use many different settings configurations. You can find them in `settings.py`. There are two 19 | main settings files: `frontend.env` and `switchboard.env`. 20 | 21 | The `frontend.env` contains the frontend process settings such as: 22 | - CANARY_DOMAINS=mytesttokensdomain.com 23 | - CANARY_NXDOMAINS=pdf.demo.canarytokens.net 24 | - CANARY_AWSID_URL= 25 | - CANARY_WEB_IMAGE_UPLOAD_PATH=/uploads 26 | - CANARY_GOOGLE_API_KEY= 27 | - LOG_FILE=frontend.log 28 | 29 | The `switchboard.env` contains the switchboard process settings such as: 30 | - CANARY_MAILGUN_DOMAIN_NAME= 31 | - CANARY_MAILGUN_API_KEY= 32 | - CANARY_MANDRILL_API_KEY= 33 | - CANARY_SENDGRID_API_KEY= 34 | - CANARY_PUBLIC_IP= 35 | - CANARY_PUBLIC_DOMAIN= 36 | - CANARY_ALERT_EMAIL_FROM_ADDRESS=noreply@yourdomain.com 37 | - CANARY_ALERT_EMAIL_FROM_DISPLAY="Canarytoken Mailer" 38 | - CANARY_ALERT_EMAIL_SUBJECT="Alert" 39 | - CANARY_SMTP_USERNAME= 40 | - CANARY_SMTP_PASSWORD= 41 | - CANARY_SMTP_SERVER=smtp.gmail.com 42 | - CANARY_SMTP_PORT=587 43 | - CANARY_WEB_IMAGE_UPLOAD_PATH=/uploads 44 | - LOG_FILE=switchboard.log 45 | 46 | Please note that when choosing which email provider you would like to use, you **MUST** only provide 47 | information related to that provider. E.g. if you have `CANARY_MAILGUN_API_KEY` then you must remove the others such as 48 | `CANARY_SENDGRID_API_KEY` and `CANARY_MANDRILL_API_KEY`. 49 | 50 | Lastly, we have added the ability to specify your own AWSID lambda so that you may host your own. The setting is placed in 51 | `frontend.env` under `CANARY_AWSID_URL`. If this value is not specified, it will use our default hosted lambda. 52 | 53 | ### Configuration of Outgoing SMTP 54 | When configuring outgoing SMTP please consider the following: 55 | 56 | Restrictions: 57 | * no other provider like Mailgun or Sendgrid must be configured for this to work 58 | * only supports StartTLS right now (you have to use the corresponding port) 59 | * no anonymous SMTP supported right now (you have to use username/password to authenticate) 60 | 61 | The following settings have to be configured in `switchboard.env` for SMTP to work: 62 | * CANARY_SMTP_SERVER: the SMTP server 63 | * CANARY_SMTP_PORT: the port number of the SMTP server (must be a StartTLS enabled port!) 64 | * CANARY_SMTP_USERNAME: Username for the SMTP server (no anonymous SMTP supported right now) 65 | * CANARY_SMTP_PASSWORD: the password that corresponds to the username 66 | 67 | A complete example config in `switchboard.env` then looks like this: 68 | ``` 69 | CANARY_SMTP_SERVER=smtp.yourserver.com 70 | CANARY_SMTP_PORT=587 71 | CANARY_SMTP_USERNAME= 72 | CANARY_SMTP_PASSWORD= 73 | CANARY_ALERT_EMAIL_FROM_ADDRESS=canary@yourdomain.com 74 | CANARY_ALERT_EMAIL_SUBJECT="Canary Alert via SMTP" 75 | ``` 76 | -------------------------------------------------------------------------------- /channel_input_linkedin.py: -------------------------------------------------------------------------------- 1 | import simplejson 2 | import twill 3 | 4 | from twisted.application.internet import TimerService 5 | from twisted.python import log 6 | 7 | from canarydrop import Canarydrop 8 | from channel import InputChannel 9 | from queries import get_canarydrop, get_all_imgur_tokens, save_imgur_token,\ 10 | get_imgur_count, get_all_linkedin_accounts,\ 11 | save_linkedin_account, get_linkedin_viewer_count 12 | from exception import LinkedInFailure 13 | from constants import INPUT_CHANNEL_LINKEDIN 14 | 15 | 16 | #create_linkedin_account(username='blah@blah.com', password='mooo') 17 | #ht = get_canarydrop(canarytoken=get_linkedin_account(username='blah@blah.com')['canarytoken']) 18 | #ht['alert_email_enabled'] = True 19 | #ht['alert_email_recipient'] = 'foo@foo.com' 20 | #save_canarydrop(ht) 21 | 22 | 23 | class ChannelLinkedIn(InputChannel): 24 | """Input channel that polls LinkedIn for changes to the profile view count, and 25 | alerts when they climb.""" 26 | CHANNEL = INPUT_CHANNEL_LINKEDIN 27 | 28 | def __init__(self, min_delay=3600*24, switchboard=None): 29 | log.msg('Started channel {name}'.format(name=self.CHANNEL)) 30 | super(ChannelLinkedIn, self).__init__(switchboard=switchboard, 31 | name=self.CHANNEL) 32 | self.min_delay = min_delay 33 | self.service = TimerService(self.min_delay, self.schedule_polling) 34 | 35 | def schedule_polling(self,): 36 | """A dummy method. For now, all view count polls are run immediately. 37 | In the future they'll be spread out over the interval.""" 38 | try: 39 | for linkedin_account in get_all_linkedin_accounts(): 40 | self.poll(linkedin_account=linkedin_account) 41 | except Exception as e: 42 | log.err('LinkedIn error: {error}'.format(error=e)) 43 | 44 | def poll(self, linkedin_account=None): 45 | try: 46 | current_count = get_linkedin_viewer_count( 47 | username=linkedin_account['username'], 48 | password=linkedin_account['password']) 49 | except LinkedInFailure as e: 50 | log.err('Could not retrieve linkedin view count: {error}'\ 51 | .format(error=e)) 52 | return 53 | 54 | if current_count > linkedin_account['count']: 55 | canarydrop = Canarydrop(**get_canarydrop( 56 | canarytoken=linkedin_account['canarytoken'])) 57 | self.dispatch(canarydrop=canarydrop, count=current_count, 58 | linkedin_username=linkedin_account['username']) 59 | linkedin_account['count'] = current_count 60 | save_linkedin_account(linkedin_account=linkedin_account) 61 | 62 | def format_additional_data(self, **kwargs): 63 | log.msg('%r' % kwargs) 64 | additional_report = '' 65 | if kwargs.has_key('count') and kwargs['count']: 66 | additional_report += 'View Count: {count}\r\n'.format( 67 | count=kwargs['count']) 68 | if kwargs.has_key('linkedin_username') and kwargs['linkedin_username']: 69 | additional_report += 'LinkedIn User: {username}\r\n'.format( 70 | username=kwargs['linkedin_username']) 71 | return additional_report 72 | -------------------------------------------------------------------------------- /root-ca.conf: -------------------------------------------------------------------------------- 1 | [ default ] 2 | ca = rootCA # CA name 3 | aia_url = _TOKEN_/any_path.cer?any=params # CA certificate URL 4 | crl_url = _TOKEN_/any_path.crl?any=params # CRL distribution point 5 | ocsp_url = _TOKEN_/any_path.oscp?any=params # OCSP responder URL 6 | name_opt = multiline,-esc_msb,utf8 # Display UTF-8 characters 7 | 8 | # CA certificate request 9 | [ req ] 10 | default_bits = 2048 # RSA key size 11 | encrypt_key = yes # Protect private key 12 | default_md = sha256 # MD to use 13 | utf8 = yes # Input is UTF-8 14 | string_mask = utf8only # Emit UTF-8 strings 15 | prompt = no # Don't prompt for DN 16 | distinguished_name = ca_dn # DN section 17 | req_extensions = ca_reqext # Desired extensions 18 | 19 | [ ca_dn ] 20 | countryName = "ZA" 21 | organizationName = "Thinkst Applied Research" 22 | organizationalUnitName = "Thinkst Applied Research CA" 23 | commonName = "Thinkst Root CA" 24 | 25 | [ ca_reqext ] 26 | keyUsage = critical,keyCertSign,cRLSign 27 | basicConstraints = critical,CA:true 28 | subjectKeyIdentifier = hash 29 | 30 | # CA operational settings 31 | [ ca ] 32 | default_ca = root_ca # The default CA section 33 | 34 | [ root_ca ] 35 | certificate = $ca.crt # The CA cert 36 | private_key = $ca.key # CA private key 37 | new_certs_dir = TMPDIR 38 | database = TMPDIR/db 39 | serial = TMPDIR/ser 40 | default_days = 3652 # How long to certify for 41 | unique_subject = no 42 | default_md = sha256 # MD to use 43 | policy = match_pol # Default naming policy 44 | email_in_dn = no # Add email to cert DN 45 | preserve = no # Keep passed DN ordering 46 | name_opt = $name_opt # Subject DN display options 47 | cert_opt = ca_default # Certificate display options 48 | copy_extensions = copy # Copy extensions from CSR 49 | x509_extensions = ca_ext # Default cert extensions 50 | default_crl_days = 1 # How long before next CRL 51 | crl_extensions = crl_ext # CRL extensions 52 | 53 | [ match_pol ] 54 | countryName = optional 55 | organizationName = optional 56 | organizationalUnitName = optional 57 | commonName = supplied 58 | 59 | # Extensions 60 | 61 | [ ca_ext ] 62 | keyUsage = critical,digitalSignature,keyEncipherment,keyAgreement,dataEncipherment 63 | basicConstraints = CA:false 64 | extendedKeyUsage = serverAuth,clientAuth,codeSigning 65 | subjectKeyIdentifier = hash 66 | #authorityKeyIdentifier = keyid:always 67 | authorityInfoAccess = @ocsp_info 68 | crlDistributionPoints = @crl_info 69 | 70 | [ crl_ext ] 71 | authorityKeyIdentifier = keyid:always 72 | authorityInfoAccess = @issuer_info 73 | 74 | [ ocsp_info ] 75 | caIssuers;URI.0 = $aia_url 76 | OCSP;URI.0 = $ocsp_url 77 | 78 | [ issuer_info ] 79 | caIssuers;URI.0 = $aia_url 80 | 81 | [ crl_info ] 82 | URI.0 = $crl_url 83 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | settingsmodule = sys.modules[__name__] 4 | 5 | DEBUG=False 6 | 7 | LISTEN_DOMAIN="" 8 | 9 | if DEBUG: 10 | CHANNEL_DNS_PORT=5354 11 | CHANNEL_HTTP_PORT=8083 12 | CHANNEL_SMTP_PORT=2500 13 | else: 14 | CHANNEL_HTTP_PORT=8083 15 | CHANNEL_SMTP_PORT=25 16 | CHANNEL_DNS_PORT=53 17 | 18 | CHANNEL_LINKEDIN_MIN_DELAY=60*60*24 19 | CHANNEL_IMGUR_MIN_DELAY=60*60 20 | CHANNEL_BITCOIN_MIN_DELAY=60*60 21 | 22 | CANARYTOKENS_HTTP_PORT=8082 23 | 24 | CANARY_PDF_TEMPLATE="templates/template.pdf" 25 | CANARY_PDF_TEMPLATE_OFFSET=793 26 | CANARY_WORD_TEMPLATE="templates/template.docx" 27 | 28 | TOKEN_RETURN="gif" #could be gif, fortune 29 | 30 | MAX_UPLOAD_SIZE=1024 * 1024 * 1 31 | WEB_IMAGE_UPLOAD_PATH='/uploads' 32 | 33 | CANARY_AWSID_URL = "https://1luncdvp6l.execute-api.us-east-2.amazonaws.com/prod/CreateUserAPITokens" 34 | CANARY_SLACKAPI_URL = "https://0dt5l1kj92.execute-api.eu-west-1.amazonaws.com/prod" 35 | 36 | for envvar in ['SMTP_PORT', 'SMTP_USERNAME', 'SMTP_PASSWORD', 'SMTP_SERVER', 'AWSID_URL','SLACKAPI_URL', 37 | 'MAILGUN_DOMAIN_NAME', 'MAILGUN_API_KEY','MANDRILL_API_KEY','SENDGRID_API_KEY', 38 | 'PUBLIC_IP','PUBLIC_DOMAIN','ALERT_EMAIL_FROM_ADDRESS','ALERT_EMAIL_FROM_DISPLAY', 39 | 'ALERT_EMAIL_SUBJECT','DOMAINS','NXDOMAINS', 'TOKEN_RETURN', 'MAX_UPLOAD_SIZE', 40 | 'WEB_IMAGE_UPLOAD_PATH', 'DEBUG', 'IPINFO_API_KEY', 'SWITCHBOARD_LOG_COUNT', 41 | 'SWITCHBOARD_LOG_SIZE', 'FRONTEND_LOG_COUNT', 'FRONTEND_LOG_SIZE', 'MAX_HISTORY']: 42 | try: 43 | setattr(settingsmodule, envvar, os.environ['CANARY_'+envvar]) 44 | except KeyError: 45 | if not hasattr(settingsmodule, envvar): 46 | setattr(settingsmodule, envvar, '') 47 | 48 | if type(DEBUG) == str: 49 | DEBUG = (DEBUG == "True") 50 | 51 | for envvar in ['DOMAINS', 'NXDOMAINS','GOOGLE_API_KEY']: 52 | try: 53 | setattr(settingsmodule, envvar, os.environ['CANARY_'+envvar].split(',')) 54 | except KeyError: 55 | setattr(settingsmodule, envvar, []) 56 | 57 | try: 58 | setattr(settingsmodule, 'LOG_FILE', os.environ['LOG_FILE']) 59 | except KeyError: 60 | if not hasattr(settingsmodule, 'LOG_FILE'): 61 | setattr(settingsmodule, 'LOG_FILE', []) 62 | 63 | for log_config in ['SWITCHBOARD_LOG_COUNT', 'SWITCHBOARD_LOG_SIZE', 'FRONTEND_LOG_COUNT', 64 | 'FRONTEND_LOG_SIZE']: 65 | val = getattr(settingsmodule, log_config) 66 | if log_config.endswith('COUNT') and val == '': 67 | val = 5 68 | elif log_config.endswith('SIZE') and val == '': 69 | val = 5000000 70 | 71 | setattr(settingsmodule, log_config, int(val)) 72 | 73 | # Configure the maximum number of saved hits on any token. Default list size is 10 74 | try: 75 | setattr(settingsmodule, 'MAX_HISTORY', int(getattr(settingsmodule, 'MAX_HISTORY'))-1) 76 | except: 77 | setattr(settingsmodule, 'MAX_HISTORY', 9) # The off-by-one is intentional, due to Python slicing notation 78 | 79 | try: 80 | setattr(settingsmodule, 'PROTOCOL', os.environ['PROTOCOL']) 81 | except KeyError: 82 | if not hasattr(settingsmodule, 'PROTOCOL'): 83 | setattr(settingsmodule, 'PROTOCOL', 'http') 84 | 85 | if WEB_IMAGE_UPLOAD_PATH and not os.path.exists(WEB_IMAGE_UPLOAD_PATH): 86 | os.mkdir(WEB_IMAGE_UPLOAD_PATH) 87 | 88 | REDIS_HOST='redis' 89 | REDIS_PORT=6379 90 | REDIS_DB='0' 91 | 92 | TWILIO_ENABLED=False 93 | TWILIO_FROM_NUMBER="" 94 | TWILIO_ACCOUNT_SID="" 95 | TWILIO_AUTH_TOKEN="" 96 | -------------------------------------------------------------------------------- /templates/error_http.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Canarytokens: %(code)s 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
32 |
33 | 43 |

44 | 45 | 46 | 47 |

48 | What is this and why should I care? 49 |
50 | 51 |
52 |
53 |

Oh dear, an error occured!

54 |

%(code)s: %(brief)s

55 | 56 | Head back this way 57 |
58 |
59 | 60 | 61 |
62 |

Brought to you by Thinkst Canary, our insanely easy-to-use honeypot solution that deploys in just four minutes. Know. When it matters.

63 |

© Thinkst Applied Research 2015–2020

64 | 65 |
66 | 67 |
68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /channel_input_imgur.py: -------------------------------------------------------------------------------- 1 | import simplejson 2 | 3 | from twisted.application.internet import TimerService 4 | from twisted.internet.task import deferLater 5 | from twisted.internet import reactor 6 | from twisted.python import log 7 | from twisted.web.client import getPage 8 | 9 | from canarydrop import Canarydrop 10 | from channel import InputChannel 11 | from queries import get_canarydrop, get_all_imgur_tokens, save_imgur_token,\ 12 | get_imgur_count 13 | from constants import INPUT_CHANNEL_IMGUR 14 | 15 | class ChannelImgur(InputChannel): 16 | """Input channel that polls imgur for changes to the view count, and 17 | alerts when they climb.""" 18 | CHANNEL = INPUT_CHANNEL_IMGUR 19 | 20 | def __init__(self, min_delay=3600, switchboard=None): 21 | super(ChannelImgur, self).__init__(switchboard=switchboard, 22 | name=self.CHANNEL) 23 | self.min_delay = min_delay 24 | self.service = TimerService(self.min_delay, self.schedule_polling) 25 | 26 | def schedule_polling(self,): 27 | """A dummy method. For now, all view count polls are run immediately. 28 | In the future they'll be spread out over the interval.""" 29 | delay = 0 30 | for imgur_token in get_all_imgur_tokens(): 31 | self.schedule_poll(imgur_token=imgur_token, delay=delay) 32 | delay+=5 33 | 34 | # for imgur_token in get_all_imgur_tokens(): 35 | # self.poll(imgur_token=imgur_token) 36 | 37 | def poll(self, imgur_token=None): 38 | try: 39 | count = get_imgur_count(imgur_id=imgur_token['id']) 40 | if count > imgur_token['count']: 41 | canarydrop = Canarydrop(**get_canarydrop( 42 | canarytoken=imgur_token['canarytoken'])) 43 | self.dispatch(canarydrop=canarydrop, count=count, 44 | imgur_id=imgur_token['id']) 45 | imgur_token['count'] = count 46 | save_imgur_token(imgur_token=imgur_token) 47 | except Exception as e: 48 | log.err('Imgur error: {error}'.format(error=e)) 49 | 50 | def schedule_poll(self, imgur_token=None, delay=None): 51 | d = deferLater(reactor, delay, self.request_imgur_count, imgur_token) 52 | 53 | def request_imgur_count(self, imgur_token): 54 | d = getPage( 55 | 'http://imgur.com/ajax/views?images={imgur_id}'\ 56 | .format(imgur_id=imgur_token['id']), 57 | agent='Canarytokens Imgur Check') 58 | d.addCallback(self.received_imgur_count, imgur_token) 59 | return d 60 | 61 | def received_imgur_count(self, body, imgur_token): 62 | try: 63 | body = simplejson.loads(body) 64 | count = int(body['data'][imgur_token['id']]) 65 | log.msg('Count for imgur token '+imgur_token['id']+ ' was '+str(count)) 66 | if count > imgur_token['count']: 67 | canarydrop = Canarydrop(**get_canarydrop( 68 | canarytoken=imgur_token['canarytoken'])) 69 | self.dispatch(canarydrop=canarydrop, count=count, 70 | imgur_id=imgur_token['id']) 71 | imgur_token['count'] = count 72 | save_imgur_token(imgur_token=imgur_token) 73 | except Exception as e: 74 | log.err('Imgur error: {error}'.format(error=e)) 75 | 76 | def format_additional_data(self, **kwargs): 77 | log.msg('%r' % kwargs) 78 | additional_report = '' 79 | if kwargs.has_key('count') and kwargs['count']: 80 | additional_report += 'View Count: {count}\r\n'.format( 81 | count=kwargs['count']) 82 | if kwargs.has_key('imgur_id') and kwargs['imgur_id']: 83 | additional_report += 'Link: http://imgur.com/{imgur_id}\r\n'.format( 84 | imgur_id=kwargs['imgur_id']) 85 | return additional_report 86 | -------------------------------------------------------------------------------- /smtpd.tac: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from zope.interface import implements 4 | 5 | from twisted.internet import defer 6 | from twisted.mail import smtp 7 | from twisted.cred.portal import IRealm 8 | from twisted.cred.portal import Portal 9 | 10 | 11 | 12 | class CanaryMessageDelivery: 13 | implements(smtp.IMessageDelivery) 14 | 15 | def __init__(self): 16 | print 'Created CanaryMessageDelivery()' 17 | 18 | def receivedHeader(self, helo, origin, recipients): 19 | return "Received: CanaryMessageDelivery" 20 | 21 | def validateFrom(self, helo, origin): 22 | return origin 23 | 24 | def validateTo(self, user): 25 | # Only messages directed to the "console" user are accepted. 26 | if user.dest.local == "console": 27 | return lambda: CanaryMessage() 28 | raise smtp.SMTPBadRcpt(user) 29 | 30 | 31 | 32 | class CanaryMessage: 33 | implements(smtp.IMessage) 34 | 35 | def __init__(self): 36 | self.headers = [] 37 | self.headers_finished = False 38 | self.attachments = [] 39 | self.links = [] 40 | self.links_re = re.compile('.*(http[s]?://[^\s\'"]+).*', re.I) 41 | self.mime_boundary = None 42 | self.mime_boundary_re = re.compile('.*boundary="([^"]*)"') 43 | self.in_mime_header = True 44 | self.lines = [] 45 | self.stored_byte_count = 0 46 | 47 | def lineReceived(self, line): 48 | if line == '' and not self.headers_finished: 49 | self.headers_finished = True 50 | 51 | if not self.headers_finished: 52 | self.headers.append(line) 53 | m = self.mime_boundary_re.match(line) 54 | if m: 55 | self.mime_boundary = m.group(1) 56 | else: 57 | if self.mime_boundary: 58 | if self.in_mime_header: 59 | if line == '': 60 | self.in_mime_header = False 61 | else: 62 | self.attachments[-1].append(line) 63 | 64 | if self.mime_boundary in line: 65 | self.in_mime_header = True 66 | self.attachments.append([]) 67 | 68 | if self.stored_byte_count < 5*2**20: #5MB limit 69 | self.lines.append(line) 70 | self.stored_byte_count ++ len(line) 71 | 72 | def eomReceived(self): 73 | print "New message received:" 74 | print "\n".join(self.headers) 75 | print "\n".join(self.links) 76 | print '\n\n'.join([ '\n'.join(x) for x in self.attachments]) 77 | self.lines = None 78 | #return defer.succeed(None) 79 | d = defer.Deferred() 80 | d.callback("Success") 81 | return d 82 | 83 | def connectionLost(self): 84 | # There was an error, throw away the stored lines 85 | self.lines = None 86 | 87 | 88 | class CanaryESMTP(smtp.ESMTP): 89 | def __init__(self, **kwargs): 90 | smtp.ESMTP.__init__(self, **kwargs) 91 | 92 | def greeting(self,): 93 | try: 94 | return self.factory.responses['greeting'] 95 | except KeyError: 96 | return smtp.ESMTP.greeting(self) 97 | 98 | def receivedHeader(self, helo, origin, recipients): 99 | return "Received: CanaryMessageDelivery" 100 | 101 | def validateFrom(self, helo, origin): 102 | return origin 103 | 104 | def validateTo(self, user): 105 | # Only messages directed to the "console" user are accepted. 106 | if user.dest.local == "console": 107 | return lambda: CanaryMessage() 108 | raise smtp.SMTPBadRcpt(user) 109 | 110 | 111 | class CanarySMTPFactory(smtp.SMTPFactory): 112 | protocol = CanaryESMTP 113 | 114 | def __init__(self, *a, **kw): 115 | self.responses = kw.pop('responses') 116 | smtp.SMTPFactory.__init__(self, *a, **kw) 117 | # self.delivery = CanaryMessageDelivery() 118 | 119 | 120 | def buildProtocol(self, addr): 121 | p = smtp.SMTPFactory.buildProtocol(self, addr) 122 | # p.delivery = self.delivery 123 | # p.challengers = {"LOGIN": LOGINCredentials, "PLAIN": PLAINCredentials} 124 | return p 125 | 126 | 127 | 128 | class SimpleRealm: 129 | implements(IRealm) 130 | 131 | def requestAvatar(self, avatarId, mind, *interfaces): 132 | 133 | if smtp.IMessageDelivery in interfaces: 134 | return smtp.IMessageDelivery, CanaryMessageDelivery(), lambda: None 135 | raise NotImplementedError() 136 | 137 | 138 | 139 | def main(): 140 | from twisted.application import internet 141 | from twisted.application import service 142 | 143 | portal = Portal(SimpleRealm()) 144 | # checker = InMemoryUsernamePasswordDatabaseDontUse() 145 | # checker.addUser("guest", "password") 146 | # portal.registerChecker(checker) 147 | 148 | responses = {'data_success': 'Finished', 'greeting': 'Hello there'} 149 | a = service.Application("Canary SMTP Server") 150 | internet.TCPServer(2500, CanarySMTPFactory(portal, responses=responses)).setServiceParent(a) 151 | 152 | return a 153 | 154 | application = main() 155 | -------------------------------------------------------------------------------- /templates/emails/reset.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Canary Password Reset 8 | 27 | 28 | 29 | 30 | 31 | 32 | 102 | 103 |
33 | 34 | 35 | 49 | 50 | 51 | 99 | 100 |
36 | 37 | 38 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
52 | 53 | 54 | 80 | 81 | 82 | 96 | 97 |
55 | 56 | 57 | 60 | 75 | 76 | 77 | 78 |
58 |

Password Reset

59 |
61 | 62 | 63 | 72 | 73 |
64 |

Hi!

65 |

Someone (possibly you) initiated a password reset for Canary.

66 |

To continue with the reset, please click this link:

67 |

{{ url_for('reset_pass', hash=hash, _external=True) }}

68 |

If you did not initiate the reset then you can ignore this as the reset link expires in 24 hours.

69 |

Thanks 70 |
Canary Team

71 |
74 |
79 |
98 |
101 |
104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /ziplib.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import tempfile 3 | import datetime 4 | from os import unlink, close 5 | from zipfile import ZipFile, ZipInfo 6 | 7 | def printzip(zip): 8 | print '\t{extattr:b}\t{extattr:02x}\t{intattr:b}\t-\t{name}'.format(extattr=zip.external_attr,intattr=zip.internal_attr,name=zip.filename) 9 | 10 | MODE_READONLY = 0x01 11 | MODE_HIDDEN = 0x02 12 | MODE_SYSTEM = 0x04 13 | MODE_DIRECTORY = 0x10 14 | MODE_ARCHIVE = 0x20 15 | MODE_FILE = 0x80 16 | 17 | def make_canary_desktop_ini(hostname=None,dummyfile='resource.dll'): 18 | return (u'\r\n[.ShellClassInfo]\r\nIconResource=\\\\%USERNAME%.%COMPUTERNAME%.%USERDOMAIN%.INI.'\ 19 | +unicode(hostname)\ 20 | +unicode('\\'+dummyfile+'\r\n')).encode('utf16') 21 | 22 | def make_dir_entry(name=None, date_time=None, mode=MODE_DIRECTORY): 23 | tt = date_time.timetuple() 24 | dir = ZipInfo() 25 | 26 | dir.filename = name+('/' if name[-1] != '/' else '') 27 | dir.orig_filename = dir.filename 28 | dir.date_time = date_time.isocalendar() + (tt.tm_hour, 29 | tt.tm_min, tt.tm_sec) 30 | dir.compress_type = 0 31 | dir.create_system = 0 32 | dir.create_version = 20 33 | dir.extract_version = 10 34 | dir.external_attr = mode 35 | 36 | return dir 37 | 38 | def make_file_entry(name=None, date_time=None, mode=MODE_FILE | MODE_ARCHIVE): 39 | tt = date_time.timetuple() 40 | file = ZipInfo() 41 | 42 | file.filename = name 43 | file.orig_filename = file.filename 44 | file.date_time = date_time.isocalendar() + (tt.tm_hour, 45 | tt.tm_min, tt.tm_sec) 46 | file.compress_type = 8 47 | file.create_system = 0 48 | file.create_version = 20 49 | file.extract_version = 20 50 | file.flag_bits = 2 51 | file.external_attr = mode 52 | 53 | return file 54 | 55 | def create_zip(name=None): 56 | return ZipFile(name, 'w') 57 | 58 | def write_file(zip=None, name=None, system=False, hidden=False, readonly=False, 59 | archive=True, date_time=datetime.datetime.utcnow(), contents='', ): 60 | mode = MODE_FILE 61 | mode |= MODE_HIDDEN if hidden else 0 62 | mode |= MODE_SYSTEM if system else 0 63 | mode |= MODE_READONLY if readonly else 0 64 | mode |= MODE_ARCHIVE if archive else 0 65 | entry = make_file_entry(name=name, mode=mode, date_time=date_time) 66 | zip.writestr(entry, contents) 67 | 68 | def write_dir(zip=None, name=None, system=False, hidden=False, readonly=False, 69 | archive=False, date_time=datetime.datetime.utcnow()): 70 | mode = MODE_DIRECTORY 71 | mode |= MODE_HIDDEN if hidden else 0 72 | mode |= MODE_SYSTEM if system else 0 73 | mode |= MODE_READONLY if readonly else 0 74 | mode |= MODE_ARCHIVE if archive else 0 75 | entry = make_dir_entry(name=name, mode=mode, date_time=date_time) 76 | zip.writestr(entry, '') 77 | 78 | def write_weird(zip=None, name=None, system=False, hidden=False, readonly=False, 79 | archive=False, directory=False, date_time=datetime.datetime.utcnow(), 80 | file=False, contents=''): 81 | mode = 0 82 | mode |= MODE_HIDDEN if hidden else 0 83 | mode |= MODE_SYSTEM if system else 0 84 | mode |= MODE_READONLY if readonly else 0 85 | mode |= MODE_ARCHIVE if archive else 0 86 | mode |= MODE_DIRECTORY if directory else 0 87 | mode |= MODE_FILE if file else 0 88 | entry = make_file_entry(name=name, mode=mode, date_time=date_time) 89 | zip.writestr(entry, contents) 90 | 91 | def make_canary_zip(hostname=None): 92 | (fd, fname) = tempfile.mkstemp() 93 | archive = create_zip(name=fname) 94 | write_dir(zip=archive, name='My Documents/', system=True) 95 | write_file(zip=archive, name='My Documents/desktop.ini', 96 | contents=make_canary_desktop_ini(hostname=hostname), 97 | system=True, hidden=True) 98 | archive.close() 99 | close(fd) 100 | 101 | with open(fname, 'r') as f: 102 | contents = f.read() 103 | unlink(fname) 104 | 105 | return contents 106 | 107 | if __name__ == '__main__': 108 | archive = create_zip(name='test1.zip') 109 | write_dir(zip=archive, name='test') 110 | write_dir(zip=archive, name='test/normal-dir') 111 | write_file(zip=archive, name='test/normal-dir/file1.txt', contents='hello!') 112 | write_dir(zip=archive, name='test/sysdir') 113 | write_file(zip=archive, name='test/sysdir/file1.txt', contents='i am system!', system=True, hidden=True) 114 | write_dir(zip=archive, name='test/weirddir/') 115 | write_weird(zip=archive, name='test/weirddir/file2.txt', contents='i am weird!', system=True, hidden=True, archive=True, readonly=True, directory=True, file=True) 116 | write_weird(zip=archive, name='test/weirddir/file2.txt/fooo.txt', contents='i am weird 2!', system=True, hidden=True, archive=True, readonly=True, directory=True, file=True) 117 | archive.close() 118 | 119 | 120 | 121 | archive = create_zip(name='test1.zip') 122 | write_dir(zip=archive, name='test/', system=True) 123 | write_file(zip=archive, name='test/desktop.ini', contents=make_canary_desktop_ini(hostname='xxxx5.canarydrops.net'), system=True, hidden=True) 124 | archive.close() 125 | -------------------------------------------------------------------------------- /t-sql.txt: -------------------------------------------------------------------------------- 1 | CREATE TRIGGER t_t_1 2 | ON trace1 3 | AFTER INSERT 4 | AS 5 | BEGIN 6 | declare @username varchar(max), @base64 varchar(max), @tokendomain varchar(128), @unc varchar(128), @size int, @done int, @random varchar(3); 7 | 8 | --setup the variables 9 | set @tokendomain = 'tcvqnrqidezba68kfu0dpa3ts.honeydrops.net'; 10 | set @size = 128; 11 | set @done = 0; 12 | set @random = cast(round(rand()*100,0) as varchar(2)); 13 | set @random = concat(@random, '.'); 14 | --set @username = SUSER_SNAME(); 15 | set @username = SELECT LOGINNAME FROM INSERTED; 16 | 17 | --loop runs until the UNC path is 128 chars or less 18 | while @done <= 0 19 | begin 20 | --convert username into base64 21 | select @base64 = (SELECT 22 | CAST(N'' AS XML).value( 23 | 'xs:base64Binary(xs:hexBinary(sql:column("bin")))' 24 | , 'VARCHAR(MAX)' 25 | ) Base64Encoding 26 | FROM ( 27 | SELECT CAST(@username AS VARBINARY(MAX)) AS bin 28 | ) AS bin_sql_server_temp); 29 | 30 | --replace base64 padding as dns will choke on = 31 | select @base64 = replace(@base64,'=','-') 32 | 33 | --construct the UNC path 34 | select @unc = concat('\\',@base64,'.',@random,@tokendomain,'\a') 35 | 36 | -- if too big, trim the username and try again 37 | if len(@unc) <= @size 38 | set @done = 1 39 | else 40 | --trim from the front, to keep the username and lose domain details 41 | select @username = substring(@username, 2, len(@username)-1) 42 | end 43 | exec master.dbo.xp_dirtree @unc; 44 | END 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | --create a stored proc that'll ping honeydrops 54 | CREATE proc ping_honeydrop 55 | AS 56 | BEGIN 57 | declare @username varchar(max), @base64 varchar(max), @tokendomain varchar(128), @unc varchar(128), @size int, @done int, @random varchar(3); 58 | 59 | --setup the variables 60 | set @tokendomain = 'tcvqnrqidezba68kfu0dpa3ts.honeydrops.net'; 61 | set @size = 128; 62 | set @done = 0; 63 | set @random = cast(round(rand()*100,0) as varchar(2)); 64 | set @random = concat(@random, '.'); 65 | set @username = SUSER_SNAME(); 66 | 67 | --loop runs until the UNC path is 128 chars or less 68 | while @done <= 0 69 | begin 70 | --convert username into base64 71 | select @base64 = (SELECT 72 | CAST(N'' AS XML).value( 73 | 'xs:base64Binary(xs:hexBinary(sql:column("bin")))' 74 | , 'VARCHAR(MAX)' 75 | ) Base64Encoding 76 | FROM ( 77 | SELECT CAST(@username AS VARBINARY(MAX)) AS bin 78 | ) AS bin_sql_server_temp); 79 | 80 | --replace base64 padding as dns will choke on = 81 | select @base64 = replace(@base64,'=','-') 82 | 83 | --construct the UNC path 84 | select @unc = concat('\\',@base64,'.',@random,@tokendomain,'\a') 85 | 86 | -- if too big, trim the username and try again 87 | if len(@unc) <= @size 88 | set @done = 1 89 | else 90 | --trim from the front, to keep the username and lose domain details 91 | select @username = substring(@username, 2, len(@username)-1) 92 | end 93 | exec master.dbo.xp_fileexist @unc; 94 | END 95 | 96 | --add a trigger if data is altered 97 | CREATE TRIGGER trigger2 98 | ON table1 99 | AFTER UPDATE 100 | AS 101 | BEGIN 102 | exec ping_honeydrop 103 | end 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | --create a table-view function to query the honey hostname 115 | ALTER function innocuous_name(@RAND FLOAT) returns @output table (col1 varchar(max)) 116 | AS 117 | BEGIN 118 | declare @username varchar(max), @base64 varchar(max), @tokendomain varchar(128), @unc varchar(128), @size int, @done int, @random varchar(3); 119 | 120 | --setup the variables 121 | set @tokendomain = 'tcvqnrqidezba68kfu0dpa3ts.honeydrops.net'; 122 | set @size = 128; 123 | set @done = 0; 124 | set @random = cast(round(@RAND*100,0) as varchar(2)); 125 | set @random = concat(@random, '.'); 126 | set @username = SUSER_SNAME(); 127 | 128 | --loop runs until the UNC path is 128 chars or less 129 | while @done <= 0 130 | begin 131 | --convert username into base64 132 | select @base64 = (SELECT 133 | CAST(N'' AS XML).value( 134 | 'xs:base64Binary(xs:hexBinary(sql:column("bin")))' 135 | , 'VARCHAR(MAX)' 136 | ) Base64Encoding 137 | FROM ( 138 | SELECT CAST(@username AS VARBINARY(MAX)) AS bin 139 | ) AS bin_sql_server_temp); 140 | 141 | --replace base64 padding as dns will choke on = 142 | select @base64 = replace(@base64,'=','0') 143 | 144 | --construct the UNC path 145 | select @unc = concat('\\',@base64,'.',@random,@tokendomain,'\a') 146 | 147 | -- if too big, trim the username and try again 148 | if len(@unc) <= @size 149 | set @done = 1 150 | else 151 | --trim from the front, to keep the username and lose domain details 152 | select @username = substring(@username, 2, len(@username)-1) 153 | end 154 | exec master.dbo.xp_dirtree @unc-- WITH RESULT SETS (([result] varchar(max))); 155 | return 156 | END 157 | 158 | --create a view that calls the function 159 | alter view view1 as select * from master.dbo.innocuous_name(rand()); 160 | 161 | --change permissions on innocuous_name to SELECT for [public] 162 | --change permissions on lucrative_name to SELECT for [public] 163 | --don't allow [public] to view the definitions 164 | 165 | 166 | 167 | --return IP address 168 | SELECT CONVERT(char(15), CONNECTIONPROPERTY('client_net_address')) 169 | 170 | 171 | --approach to finding failed logins: 172 | --http://blogs.technet.com/b/sql_server_isv/archive/2011/03/07/adding-failed-sql-server-logon-support-to-a-plm-sql-server.aspx 173 | 174 | 175 | -------------------------------------------------------------------------------- /channel_input_smtp.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from zope.interface import implements 4 | 5 | from twisted.internet import defer 6 | from twisted.mail import smtp 7 | from twisted.cred.portal import IRealm 8 | from twisted.cred.portal import Portal 9 | from twisted.python import log 10 | from twisted.application import internet 11 | from twisted.application import service 12 | 13 | from constants import INPUT_CHANNEL_SMTP 14 | from tokens import Canarytoken 15 | from canarydrop import Canarydrop 16 | from exception import NoCanarytokenPresent, NoCanarytokenFound 17 | from channel import InputChannel 18 | from queries import get_canarydrop 19 | 20 | 21 | 22 | class CanaryMessageDelivery: 23 | implements(smtp.IMessageDelivery) 24 | 25 | def __init__(self): 26 | print 'Created CanaryMessageDelivery()' 27 | 28 | def receivedHeader(self, helo, origin, recipients): 29 | return "Received: CanaryMessageDelivery" 30 | 31 | def validateFrom(self, helo, origin): 32 | return origin 33 | 34 | def validateTo(self, user): 35 | # Only messages directed to the "console" user are accepted. 36 | if user.dest.local == "console": 37 | return lambda: CanaryMessage() 38 | raise smtp.SMTPBadRcpt(user) 39 | 40 | 41 | 42 | class CanaryMessage: 43 | implements(smtp.IMessage) 44 | 45 | def __init__(self, esmtp=None): 46 | self.esmtp = esmtp 47 | self.headers = [] 48 | self.headers_finished = False 49 | self.attachments = [] 50 | self.links = [] 51 | self.links_re = re.compile('(http[s]?://[^\s\'"]+)', re.I) 52 | self.mime_boundary = None 53 | self.mime_boundary_re = re.compile('.*boundary[ ]*=[" ]?([^" ]+)') 54 | self.in_mime_header = True 55 | self.lines = [] 56 | self.stored_byte_count = 0 57 | 58 | def lineReceived(self, line): 59 | if line == '' and not self.headers_finished: 60 | self.headers_finished = True 61 | 62 | if not self.headers_finished: 63 | self.headers.append(line) 64 | m = self.mime_boundary_re.match(line) 65 | if m: 66 | self.mime_boundary = m.group(1) 67 | else: 68 | if self.mime_boundary: 69 | if self.in_mime_header: 70 | if line == '': 71 | self.in_mime_header = False 72 | else: 73 | self.attachments[-1].append(line) 74 | 75 | if self.mime_boundary in line: 76 | self.in_mime_header = True 77 | self.attachments.append([]) 78 | 79 | if self.stored_byte_count < 5*2**20: #5MB limit 80 | self.lines.append(line) 81 | self.stored_byte_count ++ len(line) 82 | 83 | def eomReceived(self): 84 | print "New message received:" 85 | self.esmtp.mail['headers'] = self.headers 86 | print "\n".join(self.headers) 87 | self.esmtp.mail['links'] = self.links_re.findall( 88 | '\r\n'.join(self.lines)) 89 | print "\n".join(self.links) 90 | self.esmtp.mail['attachments'] = [ '\n'.join(x) for x in self.attachments] 91 | print '\n\n'.join([ '\n'.join(x) for x in self.attachments]) 92 | self.lines = None 93 | self.esmtp.dispatch() 94 | #return defer.succeed(None) 95 | d = defer.Deferred() 96 | d.callback("Success") 97 | return d 98 | 99 | def connectionLost(self): 100 | # There was an error, throw away the stored lines 101 | self.lines = None 102 | 103 | 104 | class CanaryESMTP(smtp.ESMTP): 105 | def __init__(self, **kwargs): 106 | smtp.ESMTP.__init__(self, **kwargs) 107 | self.mail = {'recipients': [], 108 | 'sender': '', 109 | 'helo': {}, 110 | 'headers': [], 111 | 'links': [], 112 | 'attachments': []} 113 | 114 | def greeting(self,): 115 | self.src_ip = self.transport.getPeer().host 116 | try: 117 | return self.factory.responses['greeting'] 118 | except KeyError: 119 | return smtp.ESMTP.greeting(self) 120 | 121 | def receivedHeader(self, helo, origin, recipients): 122 | self.mail['helo']['client_name'] = helo[0] 123 | self.mail['helo']['client_ip'] = helo[1] 124 | self.mail['sender'] = str(origin) 125 | for r in recipients: 126 | address = r.dest.addrstr 127 | self.mail['recipients'].append(address) 128 | 129 | def validateFrom(self, helo, origin): 130 | return origin 131 | 132 | def validateTo(self, user): 133 | # Only messages directed to the "console" user are accepted. 134 | try: 135 | token = Canarytoken(value=user.dest.local) 136 | self.canarydrop = Canarydrop(**get_canarydrop(canarytoken=token.value())) 137 | return lambda: CanaryMessage(esmtp=self) 138 | except (NoCanarytokenPresent, NoCanarytokenFound): 139 | log.err('No token in recipient address: {address}'\ 140 | .format(address=user.dest.local)) 141 | except Exception as e: 142 | log.err(e) 143 | 144 | raise smtp.SMTPBadRcpt(user) 145 | 146 | def dispatch(self,): 147 | self.factory.dispatch(canarydrop=self.canarydrop, src_ip=self.src_ip, 148 | mail=self.mail) 149 | 150 | class CanarySMTPFactory(smtp.SMTPFactory, InputChannel): 151 | protocol = CanaryESMTP 152 | CHANNEL = INPUT_CHANNEL_SMTP 153 | 154 | def __init__(self, *a, **kw): 155 | self.responses = {'data_success': 'Finished', 'greeting': 'Hello there'} 156 | self.switchboard = kw.pop('switchboard') 157 | smtp.SMTPFactory.__init__(self, *a, **kw) 158 | InputChannel.__init__(self, switchboard=self.switchboard, 159 | name=self.CHANNEL, 160 | unique_channel=False) 161 | 162 | def buildProtocol(self, addr): 163 | p = smtp.SMTPFactory.buildProtocol(self, addr) 164 | # p.delivery = self.delivery 165 | # p.challengers = {"LOGIN": LOGINCredentials, "PLAIN": PLAINCredentials} 166 | return p 167 | 168 | def format_additional_data(self, **kwargs): 169 | log.msg('%r' % kwargs) 170 | if kwargs.has_key('src_ip') and kwargs['src_ip']: 171 | additional_report = 'Source IP : {ip}'.format(ip=kwargs['src_ip']) 172 | if kwargs.has_key('mail') and kwargs['mail']: 173 | mail = kwargs['mail'] 174 | additional_report += """ 175 | Client Name : {client_name} 176 | Client IP : {client_ip} 177 | Sender : {sender} 178 | Recipients : {recipients} 179 | Links : {links} 180 | Attachments : 181 | {attachments} 182 | 183 | 184 | Headers : 185 | {headers}""".format( 186 | recipients = ', '.join(mail['recipients']), 187 | sender = mail['sender'], 188 | client_ip= mail['helo']['client_ip'], 189 | client_name = mail['helo']['client_name'], 190 | links = ', '.join(mail['links']), 191 | attachments = '\n\n'.join(mail['attachments']), 192 | headers = '\n'.join(mail['headers'])) 193 | 194 | return additional_report 195 | 196 | 197 | class ChannelSMTP(): 198 | def __init__(self, port=25, switchboard=None): 199 | self.service = internet.TCPServer(port, CanarySMTPFactory(None, switchboard=switchboard)) 200 | -------------------------------------------------------------------------------- /channel.py: -------------------------------------------------------------------------------- 1 | """" 2 | Base class for all canarydrop channels. 3 | """ 4 | 5 | import datetime 6 | 7 | import simplejson 8 | 9 | import settings 10 | from exception import DuplicateChannel 11 | from twisted.python import log 12 | 13 | class Channel(object): 14 | CHANNEL = 'Base' 15 | 16 | def __init__(self, switchboard=None, name=None): 17 | self.switchboard = switchboard 18 | self.name = name or self.CHANNEL 19 | log.msg('Started channel {name}'.format(name=self.name)) 20 | 21 | class InputChannel(Channel): 22 | CHANNEL = 'InputChannel' 23 | 24 | def __init__(self, switchboard=None, name=None, unique_channel=False): 25 | super(InputChannel, self).__init__(switchboard=switchboard, 26 | name=name) 27 | try: 28 | self.register_input_channel() 29 | except DuplicateChannel as e: 30 | if unique_channel: 31 | raise e 32 | 33 | def register_input_channel(self,): 34 | self.switchboard.add_input_channel(name=self.name, channel=self) 35 | 36 | def format_additional_data(self, **kwargs): 37 | return '' 38 | 39 | def format_webhook_canaryalert(self,canarydrop=None, protocol=settings.PROTOCOL, 40 | host=settings.PUBLIC_DOMAIN, **kwargs): 41 | payload = {} 42 | if not host or host == '': 43 | host=settings.PUBLIC_IP 44 | 45 | payload['channel'] = self.name 46 | payload['time'] = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S (UTC)") 47 | payload['memo'] = canarydrop.memo 48 | payload['manage_url'] = '{protocol}://{host}/manage?token={token}&auth={auth}'\ 49 | .format(protocol=protocol, 50 | host=host, 51 | token=canarydrop['canarytoken'], 52 | auth=canarydrop['auth']) 53 | payload['additional_data'] = kwargs 54 | 55 | return payload 56 | 57 | def format_slack_canaryalert(self,canarydrop=None, protocol=settings.PROTOCOL, 58 | host=settings.PUBLIC_DOMAIN, **kwargs): 59 | payload = {} 60 | fields = [] 61 | if not host or host == '': 62 | host=settings.PUBLIC_IP 63 | manage_link = '{protocol}://{host}/manage?token={token}&auth={auth}'\ 64 | .format(protocol=protocol, 65 | host=host, 66 | token=canarydrop['canarytoken'], 67 | auth= canarydrop['auth']) 68 | attachment = { 69 | 'title':'Canarytoken Triggered\n', 70 | 'title_link': manage_link, 71 | 'mrkdwn_in': ['title'], 72 | 'fallback' : 'Canarytoken Triggered: {link}'.format(link=manage_link) 73 | } 74 | fields.append({'title':'Channel','value':self.name}) 75 | fields.append({'title':'Memo', 'value': canarydrop.memo}) 76 | fields.append({'title':'Time', 'value': datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S (UTC)")}) 77 | fields.append({'title':'Manage','value': manage_link}) 78 | attachment['fields'] = fields 79 | payload['attachments'] = [attachment] 80 | return payload 81 | 82 | def format_canaryalert(self, canarydrop=None, protocol=settings.PROTOCOL, 83 | host=settings.PUBLIC_DOMAIN, params=None, **kwargs): 84 | msg = {} 85 | if not host or host == '': 86 | host=settings.PUBLIC_IP 87 | 88 | if 'useragent' in kwargs: 89 | msg['useragent'] = kwargs['useragent'] 90 | 91 | if 'referer' in kwargs: 92 | msg['referer'] = kwargs['referer'] 93 | 94 | if 'location' in kwargs: 95 | msg['location'] = kwargs['location'] 96 | 97 | if 'src_ip' in kwargs: 98 | msg['src_ip'] = kwargs['src_ip'] 99 | 100 | msg['time'] = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S (UTC)") 101 | msg['channel'] = self.name 102 | 103 | if 'src_data' in kwargs and 'aws_keys_event_source_ip' in kwargs['src_data']: 104 | msg['src_ip'] = kwargs['src_data']['aws_keys_event_source_ip'] 105 | msg['channel'] = 'AWS API Key Token' 106 | 107 | if 'src_data' in kwargs and 'aws_keys_event_user_agent' in kwargs['src_data']: 108 | msg['useragent'] = kwargs['src_data']['aws_keys_event_user_agent'] 109 | 110 | if params.get('body_length', 999999999) <= 140: 111 | msg['body'] = """Canarydrop@{time} via {channel_name}: """\ 112 | .format(channel_name=self.name, 113 | time=msg['time']) 114 | capacity = 140 - len(msg['body']) 115 | msg['body'] += canarydrop.memo[:capacity] 116 | else: 117 | msg['body'] = """ 118 | One of your canarydrops was triggered. 119 | Channel: {channel_name} 120 | Time : {time} 121 | Memo : {memo} 122 | {additional_data} 123 | Manage your settings for this Canarydrop: 124 | {protocol}://{host}/manage?token={token}&auth={auth}""".format( 125 | channel_name=self.name, 126 | time=msg['time'], 127 | memo=canarydrop.memo, 128 | additional_data=self.format_additional_data(**kwargs), 129 | protocol=protocol, 130 | host=host, 131 | token=canarydrop['canarytoken'], 132 | auth=canarydrop['auth'] 133 | ) 134 | msg['manage'] = '{protocol}://{host}/manage?token={token}&auth={auth}'\ 135 | .format(protocol=protocol, 136 | host=host, 137 | token=canarydrop['canarytoken'], 138 | auth=canarydrop['auth']) 139 | msg['history'] = '{protocol}://{host}/history?token={token}&auth={auth}'\ 140 | .format(protocol=protocol, 141 | host=host, 142 | token=canarydrop['canarytoken'], 143 | auth=canarydrop['auth']) 144 | 145 | if params.get('subject_required', False): 146 | msg['subject'] = settings.ALERT_EMAIL_SUBJECT 147 | if params.get('from_display_required', False): 148 | msg['from_display'] = settings.ALERT_EMAIL_FROM_DISPLAY 149 | if params.get('from_address_required', False): 150 | msg['from_address'] = settings.ALERT_EMAIL_FROM_ADDRESS 151 | return msg 152 | 153 | def dispatch(self, **kwargs): 154 | self.switchboard.dispatch(input_channel=self.name, **kwargs) 155 | 156 | 157 | class OutputChannel(Channel): 158 | CHANNEL = 'OutputChannel' 159 | 160 | def __init__(self, switchboard=None, name=None): 161 | super(OutputChannel, self).__init__(switchboard=switchboard, 162 | name=name) 163 | self.register_output_channel() 164 | 165 | def register_output_channel(self,): 166 | self.switchboard.add_output_channel(name=self.name, channel=self) 167 | 168 | def send_alert(self, input_channel=None, canarydrop=None, **kwargs): 169 | if not input_channel: 170 | raise Exception('Cannot send an alert when the input_channel is None') 171 | 172 | if not canarydrop: 173 | raise Exception('Cannot send an alert when the canarydrop is None') 174 | 175 | self.do_send_alert(input_channel=input_channel, 176 | canarydrop=canarydrop, 177 | **kwargs) 178 | 179 | def do_send_alert(self, **kwargs): 180 | pass 181 | -------------------------------------------------------------------------------- /templates/static/styles.min.css: -------------------------------------------------------------------------------- 1 | .logo{height:32px}.hidden{display:none!important}.step{border-bottom:5px solid #fff;padding:1rem 0}.error-outline{outline:0;border-color:#1ecaed;box-shadow:0 0 10px red}.success-outline{outline:0;border-color:#1ecaed;box-shadow:0 0 10px green}a.fileupload-clear:hover{text-decoration:none}.jumbotron{padding:2rem 1rem;position:relative}.create{z-index:1;position:relative}.create-hidden{transition:opacity .5s ease;opacity:0}.history-header{text-align:center;padding:2rem 1rem;margin-bottom:2rem;background-color:#eceeef;border-radius:.3rem}.details-header{padding:.7rem;text-align:center}.footer{text-align:center}.incident-size{margin:0 auto;display:none}.incident-list{overflow:auto;position:relative;margin-bottom:10px;margin-top:10px}.incident-item{width:100%;transition:opacity .5s ease;background-color:#f0f8ff;color:#2f4f4f;border-radius:.3rem;margin-bottom:.5rem;cursor:pointer}.incident-item-details{display:none;padding:1rem}.incident-item:hover{outline:0;border-color:#1ecaed;box-shadow:0 0 10px green}.header_row{font-weight:700}.incident-item:first-child{margin-top:.5rem}.success{position:absolute;top:0;left:0;width:100%;opacity:0;z-index:0;transition:opacity .5s ease;background-color:#f0f8ff;color:#2f4f4f;padding-top:1rem;padding-bottom:2rem;border-radius:.3rem}.success-visible{z-index:2;opacity:1}.results{padding-top:1rem}input.fileupload,input.form-control,textarea{margin-top:10px;margin-bottom:10px;text-align:center}input.form-control::-webkit-input-placeholder,textarea.form-control::-webkit-input-placeholder{color:#bbb}input.form-control:-moz-placeholder,textarea.form-control:-moz-placeholder{color:#bbb}input.form-control::-moz-placeholder,textarea.form-control::-moz-placeholder{color:#bbb}input.form-control:placeholder,textarea.form-control:placeholder{color:#bbb}.form-control{border:2px solid #dadada;border-radius:7px}.form-control:focus{outline:0;border-color:#9ecaed;box-shadow:0 0 10px #9ecaed}.step:last-child{border-bottom:none;padding-bottom:0}.wrapper-dropdown{position:relative;margin:0 auto;margin-bottom:10px;padding:12px 15px;background:#fff;border-radius:5px;cursor:pointer;outline:0;transition:all .3s ease-out}.wrapper-dropdown:after{content:"";width:0;height:0;position:absolute;top:50%;right:15px;margin-top:-3px;border-width:6px 6px 0 6px;border-style:solid;border-color:#4cbeff transparent}.success-outline.wrapper-dropdown:after{border-color:green transparent}.wrapper-dropdown .dropdown{position:absolute;top:100%;left:0;right:0;background:#fff;border-radius:0 0 5px 5px;border:1px solid rgba(0,0,0,.2);border-top:none;border-bottom:none;list-style:none;transition:all .3s ease-out;padding-left:0;max-height:0;overflow:hidden;z-index:999}.wrapper-dropdown .dropdown li a{display:block;text-decoration:none;color:#333;padding:10px 0;transition:all .3s ease-out;border-bottom:1px solid #e6e8ea}.wrapper-dropdown .dropdown li:last-of-type a{border:none}.wrapper-dropdown .dropdown li i{margin-right:5px;color:inherit;vertical-align:middle}.wrapper-dropdown .dropdown li:hover a{color:#57a9d9;background-color:#eee}.wrapper-dropdown.active{border-radius:5px 5px 0 0;background:#4cbeff;box-shadow:none;border-bottom:2px solid #eceeef;color:#fff}.wrapper-dropdown.active:after{border-color:#82d1ff transparent}.wrapper-dropdown.active .dropdown{border-bottom:1px solid rgba(0,0,0,.2);max-height:400px;overflow:scroll}.explanation{font-size:smaller;color:#999}.fileupload-wrapper{display:block;position:relative;cursor:pointer;overflow:hidden;width:100%;max-width:100%;padding:5px 10px;margin-top:15px;font-size:14px;line-height:22px;color:#777;background-color:#fff;background-image:none;text-align:center;border:2px solid #e5e5e5;-webkit-transition:border-color .15s linear;transition:border-color .15s linear}.fileupload-wrapper .fileupload-message{position:relative;-webkit-transform:translateY(35%);transform:translateY(35%);text-align:center;font-family:sans-serif;font-size:16px;color:#bbb}.fileupload-wrapper input{position:absolute;top:0;right:0;bottom:0;left:0;height:100%;width:100%;opacity:0;cursor:pointer;z-index:5}.fileupload-wrapper .fileupload-filename{font-size:16px;position:relative;transform:translateY(35%)}.fileupload-wrapper p{padding-bottom:0}#create-token-p{margin-top:1rem;margin-bottom:0}#save.btn-fullwidth{width:100%}@media (min-width:500px){#save.btn-fullwidth{font-size:2rem}}#save.btn-disabled{font-size:1rem;width:14rem;background-color:#faa;border:none}a.btn-success.btn:focus,a.btn-success.btn:hover{color:#fff}.result{display:none}#result_cloned_website{height:10rem}.jumbotron .btn.btn-clipboard{background-color:#9d8;padding:.5rem;font-size:inherit}.btn-clipboard>img{max-width:15px}.result-data{width:80%;border:none;font-family:monospace}.advice{text-align:left;margin-top:2rem}.create-link{text-align:center;margin-bottom:15px}a.refresh{font-size:1.5rem;vertical-align:middle;text-decoration:none}a.refresh:hover{cursor:pointer}.pre-like{display:block;font-family:monospace;white-space:pre;margin:1em 0;word-break:break-all;overflow:scroll;text-align:left}.error-field{display:none;color:red;border-bottom:1px solid red;border-radius:6px;margin-bottom:24px}#endpoints_errors{border:1px solid red;margin:24px 0;padding:0 5px}#endpoints_errors pre{color:#eceeef;background:#b22222}.map{position:relative;margin-bottom:10px;margin-top:10px;height:400px;border-radius:5px;border:7px solid #dadada}.setting .key{text-align:right}.setting .value{text-align:left}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-primary:hover,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-primary:hover{background-color:#449d44;border-color:#419641}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-primary,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-primary{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.jumbotron .btn.btn-clipboard{background-color:#5cb85c;border-color:#5cb85c;color:#fff}.setting .key small{font-size:.2rem;color:#999;vertical-align:top}@media (min-width:48em){.container{max-width:100%}}.jumbotron{max-width:46rem;margin:auto;margin-bottom:2rem}a.icon:before{display:block;position:absolute;margin-right:10px;margin-left:10px;content:"";height:40px;width:40px;background-size:contain;-webkit-transition:all .4s ease-in-out;-moz-transition:all .4s ease-in-out;transition:all .4s ease-in-out;-webkit-transform:translate3d(0,0,0)}@media (max-width:768px){a.icon:before{width:20px;height:20px}}a.icon-web:before{background-image:url(/resources/web.png)}a.icon-dns:before{background-image:url(/resources/dns.png)}a.icon-web-image:before{background-image:url(/resources/web_image.png)}a.icon-email:before{background-image:url(/resources/email.png)}a.icon-word:before{background-image:url(/resources/word.png)}a.icon-pdf:before{background-image:url(/resources/pdf.png)}a.icon-folder:before{background-image:url(/resources/folder.png)}a.icon-exe:before{background-image:url(/resources/exe.png)}a.icon-clonedsite:before{background-image:url(/resources/clonedsite.png)}a.icon-sqlserver:before{background-image:url(/resources/sqlserver.png)}a.icon-qrcode:before{background-image:url(/resources/qrcode.png)}a.icon-svn:before{background-image:url(/resources/svn.png)}a.icon-aws:before{background-image:url(/resources/aws.png)}a.icon-slackapi:before{background-image:url(/resources/slack_icon.png)}a.icon-redirect:before{background-image:url(/resources/redirect.png)}@media (max-width:768px){input.form-control:-moz-placeholder,textarea.form-control:-moz-placeholder{font-size:.8rem}input.form-control::-moz-placeholder,textarea.form-control::-moz-placeholder{font-size:.8rem}input.form-control:placeholder,textarea.form-control:placeholder{font-size:.8rem}input.form-control::-webkit-input-placeholder,textarea.form-control::-webkit-input-placeholder{font-size:.8rem}.setting .key{text-align:center}.setting .value{text-align:center;margin-bottom:1rem}.artifacts{margin-left:10px;margin-right:10px}.row.results{margin-left:10px;margin-right:10px}}@media (max-width:500px){input.form-control:-moz-placeholder,textarea.form-control:-moz-placeholder{font-size:.5rem}input.form-control::-moz-placeholder,textarea.form-control::-moz-placeholder{font-size:.5rem}input.form-control:placeholder,textarea.form-control:placeholder{font-size:.5rem}input.form-control::-webkit-input-placeholder,textarea.form-control::-webkit-input-placeholder{font-size:.5rem}.token-length{font-size:1.2rem}.setting .key{text-align:center}.setting .value{text-align:center;margin-bottom:1rem}.artifacts{margin-left:10px;margin-right:10px}.row.results{margin-left:5px;margin-right:5px}a.btn.btn-large.btn-success{font-size:.9rem}}#mainsite{font-size:smaller;padding-top:1.5rem} -------------------------------------------------------------------------------- /channel_output_email.py: -------------------------------------------------------------------------------- 1 | """ 2 | Output channel that sends emails. Relies on Mandrill, Sendgrid or SMTP to actually send mails. 3 | """ 4 | import settings 5 | import pprint 6 | from twisted.python import log 7 | import mandrill 8 | import requests 9 | from htmlmin import minify 10 | from httpd_site import env 11 | from channel import OutputChannel 12 | from constants import OUTPUT_CHANNEL_EMAIL 13 | import sendgrid 14 | from sendgrid.helpers.mail import * 15 | from email.MIMEText import MIMEText 16 | import smtplib 17 | 18 | try: 19 | # Python 3 20 | import urllib.request as urllib 21 | except ImportError: 22 | # Python 2 23 | import urllib2 as urllib 24 | 25 | class EmailOutputChannel(OutputChannel): 26 | CHANNEL = OUTPUT_CHANNEL_EMAIL 27 | 28 | DESCRIPTION = 'Canarytoken triggered' 29 | TIME_FORMAT = '%Y-%m-%d %H:%M:%S (UTC)' 30 | 31 | def format_report_html(self,): 32 | """Returns a string containing an incident report in HTML, 33 | suitable for emailing""" 34 | 35 | # Use the Flask app context to render the emails 36 | # (this generates the urls + schemes correctly) 37 | rendered_html = env.get_template('emails/notification.html').render( 38 | Title=self.DESCRIPTION, 39 | Intro=self.format_report_intro(), 40 | BasicDetails=self.get_basic_details(), 41 | ManageLink=self.data['manage'], 42 | HistoryLink=self.data['history'] 43 | ) 44 | return minify(rendered_html) 45 | 46 | def format_report_intro(self,): 47 | if self.data['channel'] == 'HTTP' or self.data['channel'] == 'AWS API Key Token': 48 | template = ("An {Type} Canarytoken has been triggered") 49 | else: 50 | template = ("A {Type} Canarytoken has been triggered") 51 | 52 | if 'src_ip' in self.data: 53 | template += " by the Source IP {src}.".format(src=self.data['src_ip']) 54 | 55 | if self.data['channel'] == 'DNS': 56 | template += "\n\nPlease note that the source IP refers to a DNS server," \ 57 | " rather than the host that triggered the token. " 58 | 59 | return template.format( 60 | Type=self.data['channel']) 61 | 62 | 63 | def get_basic_details(self,): 64 | 65 | vars = { 'Description' : self.data['description'], 66 | 'Channel' : self.data['channel'], 67 | 'Time' : self.data['time'], 68 | 'Canarytoken' : self.data['canarytoken'] 69 | } 70 | 71 | if 'src_ip' in self.data: 72 | vars['src_ip'] = self.data['src_ip'] 73 | vars['SourceIP'] = self.data['src_ip'] 74 | 75 | if 'useragent' in self.data: 76 | vars['User-Agent'] = self.data['useragent'] 77 | 78 | if 'tokentype' in self.data: 79 | vars['TokenType'] = self.data['tokentype'] 80 | 81 | if 'referer' in self.data: 82 | vars['Referer'] = self.data['referer'] 83 | 84 | if 'location' in self.data: 85 | try: 86 | vars['Location'] = self.data['location'].decode('utf-8') 87 | except Exception: 88 | vars['Location'] = self.data['location'] 89 | 90 | return vars 91 | 92 | def do_send_alert(self, input_channel=None, canarydrop=None, **kwargs): 93 | msg = input_channel.format_canaryalert( 94 | params={'subject_required':True, 95 | 'from_display_required':True, 96 | 'from_address_required':True}, 97 | canarydrop=canarydrop, 98 | **kwargs) 99 | self.data = msg 100 | if 'type' in canarydrop._drop: 101 | self.data['tokentype'] = canarydrop._drop['type'] 102 | 103 | self.data['canarytoken'] = canarydrop['canarytoken'] 104 | self.data['description'] = unicode(canarydrop['memo'], "utf8") if canarydrop['memo'] is not None else '' 105 | if settings.MAILGUN_DOMAIN_NAME and settings.MAILGUN_API_KEY: 106 | self.mailgun_send(msg=msg,canarydrop=canarydrop) 107 | elif settings.MANDRILL_API_KEY: 108 | self.mandrill_send(msg=msg,canarydrop=canarydrop) 109 | elif settings.SENDGRID_API_KEY: 110 | self.sendgrid_send(msg=msg,canarydrop=canarydrop) 111 | elif settings.SMTP_SERVER: 112 | self.smtp_send(msg=msg,canarydrop=canarydrop) 113 | else: 114 | log.err("No email settings found") 115 | 116 | def mailgun_send(self, msg=None, canarydrop=None): 117 | try: 118 | url = 'https://api.mailgun.net/v3/{}/messages'.format(settings.MAILGUN_DOMAIN_NAME) 119 | auth = ('api', settings.MAILGUN_API_KEY) 120 | data = { 121 | 'from': '{name} <{address}>'.format(name=msg['from_display'],address=msg['from_address']), 122 | 'to': canarydrop['alert_email_recipient'], 123 | 'subject': msg['subject'], 124 | 'text': msg['body'], 125 | 'html': self.format_report_html() 126 | } 127 | 128 | if settings.DEBUG: 129 | pprint.pprint(data) 130 | else: 131 | result = requests.post(url, auth=auth, data=data) 132 | #Raise an error if the returned status is 4xx or 5xx 133 | result.raise_for_status() 134 | 135 | log.msg('Sent alert to {recipient} for token {token}'\ 136 | .format(recipient=canarydrop['alert_email_recipient'], 137 | token=canarydrop.canarytoken.value())) 138 | 139 | except requests.exceptions.HTTPError as e: 140 | log.err('A mailgun error occurred: %s - %s' % (e.__class__, e)) 141 | 142 | 143 | 144 | def mandrill_send(self, msg=None, canarydrop=None): 145 | try: 146 | mandrill_client = mandrill.Mandrill(settings.MANDRILL_API_KEY) 147 | message = { 148 | 'auto_html': None, 149 | 'auto_text': None, 150 | 'from_email': msg['from_address'], 151 | 'from_name': msg['from_display'], 152 | 'text': msg['body'], 153 | 'html':self.format_report_html(), 154 | 'subject': msg['subject'], 155 | 'to': [{'email': canarydrop['alert_email_recipient'], 156 | 'name': '', 157 | 'type': 'to'}], 158 | } 159 | if settings.DEBUG: 160 | pprint.pprint(message) 161 | else: 162 | result = mandrill_client.messages.send(message=message, 163 | async=False, 164 | ip_pool='Main Pool') 165 | log.msg('Sent alert to {recipient} for token {token}'\ 166 | .format(recipient=canarydrop['alert_email_recipient'], 167 | token=canarydrop.canarytoken.value())) 168 | 169 | except mandrill.Error, e: 170 | # Mandrill errors are thrown as exceptions 171 | log.err('A mandrill error occurred: %s - %s' % (e.__class__, e)) 172 | # A mandrill error occurred: - No subaccount exists with the id 'customer-123'.... 173 | 174 | def sendgrid_send(self, msg=None, canarydrop=None): 175 | try: 176 | sg = sendgrid.SendGridAPIClient(apikey=settings.SENDGRID_API_KEY) 177 | from_email = Email(msg['from_address'], msg['from_display']) 178 | subject = msg['subject'] 179 | to_email = Email(canarydrop['alert_email_recipient']) 180 | text = msg['body'] 181 | content = Content("text/html", self.format_report_html()) 182 | mail = Mail(from_email, subject, to_email, content) 183 | 184 | if settings.DEBUG: 185 | pprint.pprint(mail) 186 | else: 187 | response = sg.client.mail.send.post(request_body=mail.get()) 188 | 189 | log.msg('Sent alert to {recipient} for token {token}'\ 190 | .format(recipient=canarydrop['alert_email_recipient'], 191 | token=canarydrop.canarytoken.value())) 192 | 193 | except urllib.HTTPError as e: 194 | log.err('A sendgrid error occurred: %s - %s' % (e.__class__, e)) 195 | 196 | def smtp_send(self, msg=None, canarydrop=None): 197 | try: 198 | fromaddr = msg['from_address'] 199 | toaddr = canarydrop['alert_email_recipient'] 200 | 201 | smtpmsg = MIMEText(msg['body']) 202 | smtpmsg['From'] = fromaddr 203 | smtpmsg['To'] = toaddr 204 | smtpmsg['Subject'] = msg['subject'] 205 | 206 | if settings.DEBUG: 207 | pprint.pprint(message) 208 | else: 209 | server = smtplib.SMTP(settings.SMTP_SERVER, settings.SMTP_PORT) 210 | server.ehlo() 211 | server.starttls() 212 | server.ehlo() 213 | server.login(settings.SMTP_USERNAME, settings.SMTP_PASSWORD) 214 | text = smtpmsg.as_string() 215 | server.sendmail(fromaddr, toaddr, text) 216 | 217 | log.msg('Sent alert to {recipient} for token {token}'\ 218 | .format(recipient=canarydrop['alert_email_recipient'], 219 | token=canarydrop.canarytoken.value())) 220 | except smtplib.SMTPException as e: 221 | log.err('A smtp error occurred: %s - %s' % (e.__class__, e)) 222 | -------------------------------------------------------------------------------- /templates/fortune.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Fortune 4 | 5 | 6 | 85 | 86 | 87 |
88 |
{{fortune}}
89 | 90 | 91 | 100 | -------------------------------------------------------------------------------- /channel_http.py: -------------------------------------------------------------------------------- 1 | import simplejson 2 | import datetime 3 | import os 4 | import hashlib 5 | import cgi 6 | 7 | from twisted.web import server, resource 8 | from twisted.application import internet 9 | from twisted.web.server import Site, GzipEncoderFactory 10 | from twisted.web.resource import Resource, EncodingResourceWrapper, ForbiddenResource 11 | from twisted.web.util import Redirect, redirectTo 12 | from twisted.python import log 13 | from jinja2 import Environment, FileSystemLoader 14 | import subprocess 15 | 16 | from tokens import Canarytoken 17 | from canarydrop import Canarydrop 18 | from channel import InputChannel 19 | from queries import get_canarydrop, add_canarydrop_hit, add_additional_info_to_hit 20 | from constants import INPUT_CHANNEL_HTTP 21 | from settings import TOKEN_RETURN, MAX_UPLOAD_SIZE, WEB_IMAGE_UPLOAD_PATH 22 | 23 | env = Environment(loader=FileSystemLoader('templates')) 24 | 25 | class CanarytokenPage(resource.Resource, InputChannel): 26 | CHANNEL = INPUT_CHANNEL_HTTP 27 | isLeaf = True 28 | GIF = '\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x00\x00\xff\xff\xff'+\ 29 | '\xff\xff\xff\x21\xf9\x04\x01\x0a\x00\x01\x00\x2c\x00\x00\x00\x00'+\ 30 | '\x01\x00\x01\x00\x00\x02\x02\x4c\x01\x00\x3b' #1x1 GIF 31 | 32 | def getChild(self, name, request): 33 | if name == '': 34 | return self 35 | return Resource.getChild(self, name, request) 36 | 37 | def render_GET(self, request): 38 | #A GET request to a token URL can trigger one of a few responses: 39 | # 1. Check if link has been clicked on (rather than loaded from an 40 | # ) by looking at the Accept header, then: 41 | # 1a. If browser security if enabled, serve that page and stop. 42 | # 1b. If fortune in enabled, serve a fortune and stop. 43 | # 2. Otherwise we'll serve an image: 44 | # 2a. If a custom image is attached to the canarydrop, serve that and stop. 45 | # 2b. Serve our default 1x1 gif 46 | 47 | request.setHeader("Server", "Apache") 48 | try: 49 | token = Canarytoken(value=request.path) 50 | canarydrop = Canarydrop(**get_canarydrop(canarytoken=token.value())) 51 | if request.args.get('ts_key',[None])[0]: 52 | canarydrop._drop['hit_time'] = request.args.get('ts_key', [None])[0] 53 | else: 54 | canarydrop._drop['hit_time'] = datetime.datetime.utcnow().strftime("%s.%f") 55 | useragent = request.getHeader('User-Agent') 56 | src_ip = request.getHeader('x-forwarded-for') 57 | #location and refere are for cloned sites 58 | location = request.args.get('l', [None])[0] 59 | referer = request.args.get('r', [None])[0] 60 | self.dispatch(canarydrop=canarydrop, src_ip=src_ip, 61 | useragent=useragent, location=location, 62 | referer=referer) 63 | 64 | if 'redirect_url' in canarydrop._drop and canarydrop._drop['redirect_url']: 65 | # if fast redirect 66 | if canarydrop._drop['type'] == 'fast_redirect': 67 | return redirectTo(canarydrop._drop['redirect_url'], request) 68 | #template = env.get_template('browser_scanner.html') 69 | #return template.render(key=canarydrop._drop['hit_time'], 70 | # canarytoken=token.value()).encode('utf8') 71 | elif canarydrop._drop['type'] == 'slow_redirect': 72 | template = env.get_template('browser_scanner.html') 73 | return template.render(key=canarydrop._drop['hit_time'], 74 | canarytoken=token.value(), 75 | redirect_url=canarydrop._drop['redirect_url']).encode('utf8') 76 | 77 | if request.getHeader('Accept') and "text/html" in request.getHeader('Accept'): 78 | if canarydrop['browser_scanner_enabled']: 79 | template = env.get_template('browser_scanner.html') 80 | return template.render(key=canarydrop._drop['hit_time'], 81 | canarytoken=token.value(), 82 | redirect_url='').encode('utf8') 83 | 84 | elif TOKEN_RETURN == 'fortune': 85 | try: 86 | fortune = subprocess.check_output('/usr/games/fortune') 87 | template = env.get_template('fortune.html') 88 | return template.render(fortune=fortune).encode('utf8') 89 | except Exception as e: 90 | log.err('Could not get a fortune: {e}'.format(e=e)) 91 | if canarydrop['web_image_enabled'] and os.path.exists(canarydrop['web_image_path']): 92 | mimetype = "image/"+canarydrop['web_image_path'][-3:] 93 | with open(canarydrop['web_image_path'], "r") as f: 94 | contents = f.read() 95 | request.setHeader("Content-Type", mimetype) 96 | return contents 97 | 98 | except Exception as e: 99 | log.err('Error in render GET: {error}'.format(error=e)) 100 | 101 | request.setHeader("Content-Type", "image/gif") 102 | return self.GIF 103 | 104 | def render_POST(self, request): 105 | try: 106 | token = Canarytoken(value=request.path) 107 | canarydrop = Canarydrop(**get_canarydrop(canarytoken=token.value())) 108 | #if key and token args are present, we are either: 109 | # -posting browser info 110 | # -getting an aws trigger (key == aws_s3) 111 | # otherwise, slack api token data perhaps 112 | #store the info and don't re-render 113 | if canarydrop._drop['type'] == 'slack_api': 114 | canarydrop._drop['hit_time'] = datetime.datetime.utcnow().strftime("%s.%f") 115 | useragent = request.args.get('user_agent', [None])[0] 116 | src_ip = request.args.get('ip', [None])[0] 117 | additional_info = {'Slack Log Data': {k:v for k,v in request.args.iteritems() if k not in ['user_agent', 'ip']}} 118 | self.dispatch(canarydrop=canarydrop, src_ip=src_ip, useragent=useragent, additional_info=additional_info) 119 | return self.GIF 120 | 121 | if canarydrop._drop['type'] == 'aws_keys': 122 | canarydrop._drop['hit_time'] = datetime.datetime.utcnow().strftime("%s.%f") 123 | useragent = request.args.get('user_agent', [None])[0] 124 | src_ip = request.args.get('ip', [None])[0] 125 | additional_info = {'AWS Key Log Data': {k:v for k,v in request.args.iteritems() if k not in ['user_agent', 'ip']}} 126 | self.dispatch(canarydrop=canarydrop, src_ip=src_ip, useragent=useragent, additional_info=additional_info) 127 | return self.GIF 128 | 129 | key = request.args['key'][0] 130 | if key and token: 131 | if key == 'aws_s3': 132 | try: 133 | canarydrop._drop['hit_time'] = datetime.datetime.utcnow().strftime("%s.%f") 134 | src_ip = request.args['RemoteIP'][0] 135 | additional_info = {'AWS Log Data': {k:v for k,v in request.args.iteritems() if k not in ['key','src_ip']}} 136 | self.dispatch(canarydrop=canarydrop, src_ip=src_ip, 137 | additional_info=additional_info) 138 | except Exception as e: 139 | log.err('Error in s3 post: {error}'.format(error=e)) 140 | elif 'secretkeeper_photo' in request.args: 141 | log.err('Saving secretkeeper_photo') 142 | try: 143 | fields = cgi.FieldStorage( 144 | fp = request.content, 145 | headers = request.getAllHeaders(), 146 | environ = {'REQUEST_METHOD':'POST', 147 | 'CONTENT_TYPE': request.getAllHeaders()['content-type'], 148 | } 149 | )#hacky way to parse out file contents and filenames 150 | filename = fields['secretkeeper_photo'].filename 151 | filebody = fields['secretkeeper_photo'].value 152 | 153 | if len(filebody) > MAX_UPLOAD_SIZE: 154 | raise Exception('File too large') 155 | 156 | r = hashlib.md5(os.urandom(32)).hexdigest() 157 | filepath = os.path.join(WEB_IMAGE_UPLOAD_PATH, 158 | r[:2], 159 | r[2:])+'.png' 160 | if not os.path.exists(os.path.dirname(filepath)): 161 | try: 162 | os.makedirs(os.path.dirname(filepath)) 163 | except OSError as exc: # Guard against race condition 164 | if exc.errno != errno.EEXIST: 165 | raise 166 | 167 | with open(filepath, "w") as f: 168 | f.write(filebody) 169 | 170 | canarydrop.add_additional_info_to_hit(hit_time=key, additional_info={'secretkeeper_photo':filepath}) 171 | except Exception as e: 172 | log.err('Error in secretkeeper_photo post: {error}'.format(error=e)) 173 | else: 174 | additional_info = {k:v for k,v in request.args.iteritems() if k not in ['key','canarytoken','name']} 175 | canarydrop.add_additional_info_to_hit(hit_time=key,additional_info={request.args['name'][0]:additional_info}) 176 | return 'success' 177 | else: 178 | return self.render_GET(request) 179 | except Exception as e: 180 | return self.render_GET(request) 181 | 182 | def format_additional_data(self, **kwargs): 183 | log.msg('%r' % kwargs) 184 | additional_report = '' 185 | if kwargs.has_key('src_ip') and kwargs['src_ip']: 186 | additional_report += 'Source IP: {ip}'.format(ip=kwargs['src_ip']) 187 | if kwargs.has_key('useragent') and kwargs['useragent']: 188 | additional_report += '\nUser-agent: {useragent}'.format(useragent=kwargs['useragent']) 189 | if kwargs.has_key('location') and kwargs['location']: 190 | additional_report += '\nCloned site is at: {location}'.format(location=kwargs['location']) 191 | if kwargs.has_key('referer') and kwargs['referer']: 192 | additional_report += '\nReferring site: {referer}'.format(referer=kwargs['referer']) 193 | return additional_report 194 | 195 | def init(self, switchboard=None): 196 | InputChannel.__init__(self, switchboard=switchboard, name=self.CHANNEL) 197 | class ChannelHTTP(): 198 | def __init__(self, port=80, switchboard=None): 199 | self.port = port 200 | 201 | canarytoken_page = CanarytokenPage() 202 | canarytoken_page.init(switchboard=switchboard) 203 | wrapped = EncodingResourceWrapper(canarytoken_page, [GzipEncoderFactory()]) 204 | site = server.Site(wrapped) 205 | self.service = internet.TCPServer(self.port, site) 206 | return None 207 | -------------------------------------------------------------------------------- /canarydrop.py: -------------------------------------------------------------------------------- 1 | """ 2 | A Canarydrop ties a canarytoken to an alerting mechanisms, 3 | and records accounting information about the Canarytoken. 4 | 5 | Maps to the object stored in Redis. 6 | """ 7 | 8 | import datetime 9 | import random 10 | import md5 11 | import os 12 | import base64 13 | import pyqrcode 14 | import simplejson 15 | 16 | from constants import OUTPUT_CHANNEL_EMAIL, OUTPUT_CHANNEL_TWILIO_SMS,\ 17 | OUTPUT_CHANNEL_WEBHOOK 18 | from queries import get_all_canary_sites, get_all_canary_path_elements,\ 19 | get_all_canary_pages, get_all_canary_domains, get_all_canary_nxdomains,\ 20 | load_user, add_canarydrop_hit, add_additional_info_to_hit, get_canarydrop_triggered_list 21 | from tokens import Canarytoken 22 | from users import User, AnonymousUser 23 | from exception import NoUser, NoCanarytokenPresent, UnknownAttribute 24 | 25 | class Canarydrop(object): 26 | allowed_attrs = ['alert_email_enabled', 'alert_email_recipient',\ 27 | 'alert_sms_enabled', 'alert_sms_recipient',\ 28 | 'alert_webhook_enabled', 'alert_webhook_url','canarytoken',\ 29 | 'triggered_count', 'triggered_list','memo', 'generated_url',\ 30 | 'generated_email', 'generated_hostname','timestamp', 'user', 31 | 'imgur_token' ,'imgur', 'auth', 'browser_scanner_enabled', 'web_image_path',\ 32 | 'web_image_enabled', 'type', 'clonedsite', 'aws_secret_access_key',\ 33 | 'aws_access_key_id', 'redirect_url', 'region', 'output', 'slack_api_key'] 34 | 35 | def __init__(self, generate=False, **kwargs): 36 | self._drop = {} 37 | for k, v in kwargs.iteritems(): 38 | if k not in self.allowed_attrs: 39 | raise UnknownAttribute(attribute=k) 40 | self._drop[k] = v 41 | 42 | if 'canarytoken' not in self._drop: 43 | raise NoCanarytokenPresent() 44 | 45 | if 'timestamp' not in self._drop: 46 | self._drop['timestamp'] = datetime.datetime.utcnow()\ 47 | .strftime("%s.%f") 48 | 49 | if 'imgur_token' in self._drop and not self._drop['imgur_token']: 50 | raise Exception('Missing imgur_token from Canarydrop') 51 | 52 | if 'user' not in self._drop or self._drop['user'] in ('None', 'Anonymous'): 53 | self._drop['user'] = AnonymousUser() 54 | else: 55 | self._drop['user'] = load_user(self._drop['user']) 56 | if not self._drop['user']: 57 | raise NoUser() 58 | 59 | if 'auth' not in self._drop: 60 | self._drop['auth'] = md5.md5(str(random.SystemRandom()\ 61 | .randrange(1,2**128))).hexdigest() 62 | 63 | if self._drop.get('browser_scanner_enabled', '') in ('True', True): 64 | self._drop['browser_scanner_enabled'] = True 65 | else: 66 | self._drop['browser_scanner_enabled'] = False 67 | 68 | if self._drop.get('alert_email_enabled', '') in ('True', True): 69 | self._drop['alert_email_enabled'] = True 70 | else: 71 | self._drop['alert_email_enabled'] = False 72 | 73 | if self._drop.get('alert_webhook_enabled', '') in ('True', True): 74 | self._drop['alert_webhook_enabled'] = True 75 | else: 76 | self._drop['alert_webhook_enabled'] = False 77 | 78 | if self._drop.get('alert_sms_enabled', '') in ('True', True): 79 | self._drop['alert_sms_enabled'] = True 80 | else: 81 | self._drop['alert_sms_enabled'] = False 82 | 83 | if self._drop.get('web_image_enabled', '') in ('True', True): 84 | self._drop['web_image_enabled'] = True 85 | else: 86 | self._drop['web_image_enabled'] = False 87 | 88 | if generate: 89 | self.generate_random_url() 90 | self.generate_random_hostname() 91 | 92 | def add_additional_info_to_hit(self,hit_time=None, additional_info={}): 93 | try: 94 | hit_time = hit_time or self._drop['hit_time'] 95 | except: 96 | hit_time = self._drop['hit_time'] = datetime.datetime.utcnow().strftime("%s.%f") 97 | 98 | if hit_time not in get_canarydrop_triggered_list(self.canarytoken): 99 | self.add_canarydrop_hit() 100 | 101 | add_additional_info_to_hit(self.canarytoken, hit_time, additional_info) 102 | 103 | def add_canarydrop_hit(self, input_channel="http", **kwargs): 104 | if 'hit_time' in self._drop.keys(): 105 | hit_time = self._drop['hit_time'] 106 | else: 107 | hit_time = None 108 | 109 | add_canarydrop_hit(self.canarytoken, input_channel=input_channel, 110 | hit_time=hit_time, **kwargs) 111 | 112 | def get_url_components(self,): 113 | return (get_all_canary_sites(), get_all_canary_path_elements(), get_all_canary_pages()) 114 | 115 | def generate_random_url(self,): 116 | """Return a URL generated at random with the saved Canarytoken. 117 | The random URL is also saved into the Canarydrop.""" 118 | (sites, path_elements, pages) = self.get_url_components() 119 | 120 | 121 | generated_url = sites[random.randint(0,len(sites)-1)]+'/' 122 | path = [] 123 | for count in range(0,random.randint(1,3)): 124 | if len(path_elements) == 0: 125 | break 126 | 127 | elem = path_elements[random.randint(0,len(path_elements)-1)] 128 | path.append(elem) 129 | path_elements.remove(elem) 130 | path.append(self._drop['canarytoken']) 131 | 132 | path.append(pages[random.randint(0,len(pages)-1)]) 133 | 134 | generated_url += '/'.join(path) 135 | 136 | self._drop['generated_url'] = generated_url 137 | 138 | return self._drop['generated_url'] 139 | 140 | def get_random_site(self,): 141 | sites = get_all_canary_sites() 142 | return sites[random.randint(0,len(sites)-1)] 143 | 144 | def get_url(self,): 145 | if 'generated_url' in self._drop: 146 | return self._drop['generated_url'] 147 | return self.generate_random_url() 148 | 149 | def generate_random_hostname(self, with_random=False, nxdomain=False): 150 | """Return a hostname generated at random with the saved Canarytoken. 151 | The random hostname is also saved into the Canarydrop.""" 152 | if nxdomain: 153 | domains = get_all_canary_nxdomains() 154 | else: 155 | domains = get_all_canary_domains() 156 | 157 | if with_random: 158 | generated_hostname = str(random.randint(1,2**24))+'.' 159 | else: 160 | generated_hostname = '' 161 | 162 | generated_hostname += self._drop['canarytoken']+'.'+\ 163 | domains[random.randint(0,len(domains)-1)] 164 | 165 | return generated_hostname 166 | 167 | def get_hostname(self, with_random=False, as_url=False, nxdomain=False): 168 | if nxdomain: 169 | if 'generated_nx_hostname' not in self._drop: 170 | self._drop['generated_nx_hostname'] = \ 171 | self.generate_random_hostname(with_random=with_random, nxdomain=True) 172 | return ('http://' if as_url else '')+self._drop['generated_nx_hostname'] 173 | else: 174 | if 'generated_hostname' not in self._drop: 175 | self._drop['generated_hostname'] = \ 176 | self.generate_random_hostname(with_random=with_random, nxdomain=False) 177 | return ('http://' if as_url else '')+self._drop['generated_hostname'] 178 | 179 | def get_requested_output_channels(self,): 180 | """Return a list containing the output channels configured in this 181 | Canarydrop.""" 182 | channels = [] 183 | if (self._drop.get('alert_email_enabled', False) and 184 | self._drop.get('alert_email_recipient', None)): 185 | channels.append(OUTPUT_CHANNEL_EMAIL) 186 | if (self._drop.get('alert_webhook_enabled', False) and 187 | self._drop.get('alert_webhook_url', None)): 188 | channels.append(OUTPUT_CHANNEL_WEBHOOK) 189 | if (self._drop.get('alert_sms_enabled', False) and 190 | self._drop.get('alert_sms_recipient', None)): 191 | channels.append(OUTPUT_CHANNEL_TWILIO_SMS) 192 | return channels 193 | 194 | def _get_image_as_base64(self, path): 195 | if os.path.exists(path): 196 | with open(path, "r") as f: 197 | contents = f.read() 198 | return base64.b64encode(contents) 199 | 200 | def get_web_image_as_base64(self,): 201 | return self._get_image_as_base64(self['web_image_path']) 202 | 203 | def get_secretkeeper_photo_as_base64(self, item): 204 | return self._get_image_as_base64(self['triggered_list'][item]['additional_info']['secretkeeper_photo']) 205 | 206 | def get_cloned_site_javascript(self,): 207 | CLONED_SITE_JS = """ 208 | if (document.domain != "CLONED_SITE_DOMAIN") { 209 | var l = location.href; 210 | var r = document.referrer; 211 | var m = new Image(); 212 | m.src = "CANARYTOKEN_SITE/"+ 213 | "CANARYTOKEN.jpg?l="+ 214 | encodeURI(l) + "&r=" + encodeURI(r); 215 | } 216 | """ 217 | return CLONED_SITE_JS\ 218 | .replace('CLONED_SITE_DOMAIN', self['clonedsite'])\ 219 | .replace('CANARYTOKEN_SITE', self.get_random_site())\ 220 | .replace('CANARYTOKEN', self['canarytoken']) 221 | 222 | def get_qrcode_data_uri_png(self,): 223 | qrcode = pyqrcode.create(self.get_url()).png_as_base64_str(scale=5) 224 | return "data:image/png;base64,{qrcode}".format(qrcode=qrcode) 225 | 226 | @property 227 | def canarytoken(self): 228 | """Return the Canarydrop's Canarytoken object.""" 229 | return Canarytoken(value=self._drop['canarytoken']) 230 | 231 | @property 232 | def memo(self): 233 | """Return the Canarydrop's memo.""" 234 | return self._drop['memo'] 235 | 236 | @property 237 | def user(self): 238 | return self._drop['user'] 239 | 240 | @property 241 | def imgur_token(self): 242 | return self._drop['imgur_token'] 243 | 244 | @imgur_token.setter 245 | def imgur_token(self, value): 246 | self._drop['imgur_token'] = value 247 | 248 | def serialize(self,): 249 | """Return a representation of this Canarydrop suitable for saving 250 | into redis.""" 251 | serialized = self._drop.copy() 252 | 253 | if serialized['user']: 254 | serialized['user'] = serialized['user'].username 255 | 256 | if 'triggered_list' in serialized.keys(): 257 | serialized['triggered_list'] = simplejson.dumps(serialized['triggered_list']) 258 | 259 | return serialized 260 | 261 | def alertable(self,): 262 | if self.user.can_send_alert(canarydrop=self): 263 | return True 264 | else: 265 | return False 266 | 267 | def alerting(self, input_channel=None, **kwargs): 268 | self.user.do_accounting(canarydrop=self) 269 | 270 | def __getitem__(self, key): 271 | return self._drop[key] 272 | 273 | def __setitem__(self, key, value): 274 | self._drop[key] = value 275 | 276 | def get(self, *args): 277 | try: 278 | return self._drop[args[0]] 279 | except KeyError: 280 | if len(args) == 2: 281 | return args[1] 282 | raise KeyError(args[0]) 283 | -------------------------------------------------------------------------------- /templates/emails/notification.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ Title }} 8 | 28 | 29 | 30 | 31 | 32 | 33 | 199 | 200 |
34 | 35 | 36 | 196 | 197 |
37 | 38 | 39 | 183 | 184 | 185 | 192 | 193 |
40 | 41 | 42 | 50 | 57 | 58 | 59 | 178 | 179 | 180 | 181 |
43 |

{{ Title }}

44 | 45 | 46 | 47 | 48 |
Alert
49 |
51 | 52 | 53 | 54 | 55 |
{{ Intro }}
56 |
60 | 61 | 62 | 64 | 65 | 66 | 157 | 158 | {% if ManageLink or HistoryLink %} 159 | 160 | 162 | 163 | 164 | 174 | 175 | {% endif %} 176 |
63 |

Basic Details:

67 | 68 | {% if BasicDetails['Channel'] %} 69 | 70 | 71 | 72 | 73 | {% endif %} 74 | {% if BasicDetails['Time'] %} 75 | 76 | 77 | 78 | 79 | {% endif %} 80 | {% if BasicDetails['Canarytoken'] %} 81 | 82 | 83 | 84 | 85 | {% endif %} 86 | {% if BasicDetails['Description'] %} 87 | 88 | 89 | 90 | 91 | {% endif %} 92 | {% if BasicDetails['TokenType'] %} 93 | 94 | 95 | 96 | 97 | {% endif %} 98 | {% if BasicDetails['SourceIP'] %} 99 | 100 | 101 | 102 | 103 | {% endif %} 104 | {% if BasicDetails['User-Agent'] %} 105 | 106 | 107 | 108 | 109 | {% endif %} 110 | {% if BasicDetails['Referer'] %} 111 | 112 | 113 | 114 | 115 | {% endif %} 116 | {% if BasicDetails['Location'] %} 117 | 118 | 119 | 120 | 121 | {% endif %} 122 | {% if BasicDetails['CanaryIP'] or BasicDetails['CanaryName'] %} 123 | 124 | 125 | 132 | 133 | {% endif %} 134 | {% if BasicDetails.get('CanaryLocation', '') %} 135 | 136 | 137 | 141 | 142 | {% endif %} 143 | {% if BasicDetails[''] %} 144 | 145 | 146 | 147 | 148 | {% endif %} 149 | {% if BasicDetails[''] %} 150 | 151 | 152 | 153 | 154 | {% endif %} 155 |
Channel{{ BasicDetails['Channel'] }}
Time{{ BasicDetails['Time'] }}
Canarytoken{{ BasicDetails['Canarytoken'] }}
Token Reminder{{ BasicDetails['Description'] | e}}
Token Type{{ BasicDetails['TokenType'] }}
Source IP{{ BasicDetails['SourceIP'] }}
User Agent{{ BasicDetails['User-Agent'] | e}}
Referer{{ BasicDetails['Referer'] | e}}
Location{{ BasicDetails['Location'] | e}}
Canary 126 | 127 | {% if BasicDetails['CanaryIP'] %}{{ BasicDetails['CanaryIP'] }}{%endif%} 128 | {% if BasicDetails['CanaryName'] %}({{BasicDetails['CanaryName'] | e}} 129 | {%- if BasicDetails['CanaryID'] %}, ID:{{BasicDetails['CanaryID']}}{%endif-%} 130 | ){%endif%} 131 |
Canary Location 138 | 139 | {{ BasicDetails['CanaryLocation'] | e}} 140 |
{{ BasicDetails[''] }}
{{ BasicDetails[''] }}
156 |
161 |

Canarytoken Management Details:

165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 |
Manage this Canarytoken here
More info on this token here
173 |
177 |
182 |
194 | Canary 195 |
198 |
201 | 202 | 203 | 204 | -------------------------------------------------------------------------------- /templates/manage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | canarytokens.net 4 | 5 | 6 | 7 | 8 | 9 | 10 | 56 | 57 | 58 | 64 |
65 | {% if saved %} 66 |
67 | Saved settings. 68 |
69 | {%endif%} 70 |
71 | Token: {{canarydrop['canarytoken']}}
72 | URL: {{canarydrop.get_url()}}
73 | DNS: {{canarydrop.get_hostname()}}
74 | Email Enabled:
75 | 76 | {% if canarydrop['alert_webhook_url'] %} 77 | Webook Enabled:
78 | {%endif%} 79 | {% if canarydrop['web_image_path'] %} 80 | Alternative Image:
81 | 82 | {%endif%} 83 | {% if canarydrop['alert_sms_recipient'] %} 84 | SMS Enabled:
85 | {%endif%} 86 |
87 |
88 | 89 |
90 |
91 | {% if error %} 92 |
93 | {{error|e}} 94 |
95 | {%endif%} 96 | 97 |
98 | {% if canarydrop['triggered_list'] %} 99 | {% if API_KEY %} 100 | Map: 101 |
102 |
103 |
104 | {%endif%} 105 | History: 106 |
    107 | {% for item in canarydrop['triggered_list']|sort(reverse=True) %} 108 |
    109 |

    Date: {{ item|e }} 110 | IP: {{ canarydrop['triggered_list'][item]['src_ip'] }} 111 | Channel: {{ canarydrop['triggered_list'][item]['input_channel'] }} 112 | {%if canarydrop['triggered_list'][item]['geo_info'] != None and 113 | canarydrop['triggered_list'][item]['geo_info'] is defined and 114 | canarydrop['triggered_list'][item]['geo_info']['country'] != None and 115 | canarydrop['triggered_list'][item]['geo_info']['country'] is defined %} 116 | Country:{{canarydrop['triggered_list'][item]['geo_info']['country']}} 117 |

    118 | {%else%} 119 | Country: Unknown 120 | {%endif%} 121 |
    122 | {% if canarydrop['triggered_list'][item]['geo_info'] %} 123 | 124 | 125 | 126 | 127 | 130 | 131 | {% if canarydrop['triggered_list'][item]['geo_info']['country'] %} 132 | 133 | 134 | 138 | 139 | {% endif %} 140 | {% if canarydrop['triggered_list'][item]['geo_info']['city'] %} 141 | 142 | 143 | 146 | 147 | {% endif %} 148 | {% if canarydrop['triggered_list'][item]['geo_info']['region'] %} 149 | 150 | 151 | 154 | 155 | {% endif %} 156 | {% if canarydrop['triggered_list'][item]['geo_info']['org'] %} 157 | 158 | 159 | 162 | 163 | {% endif %} 164 | {% if canarydrop['triggered_list'][item]['geo_info']['hostname'] %} 165 | 166 | 167 | 170 | 171 | {% endif %} 172 | 175 |
    128 | Geo Info 129 |
    Country 135 | {{ canarydrop['triggered_list'][item]['geo_info']['country'] }} 136 | 137 |
    City 144 | {{ canarydrop['triggered_list'][item]['geo_info']['city'] }} 145 |
    Region 152 | {{ canarydrop['triggered_list'][item]['geo_info']['region'] }} 153 |
    Organisation 160 | {{ canarydrop['triggered_list'][item]['geo_info']['org'] }} 161 |
    Hostname 168 | {{ canarydrop['triggered_list'][item]['geo_info']['hostname'] }} 169 |
    176 | {%endif%} 177 | {% if canarydrop['triggered_list'][item]['is_tor_relay'] != None %} 178 | 179 | 180 | 181 | 182 | 185 | 186 | 187 | 190 | 193 | 194 |
    183 | 184 |
    188 | Known Exit Node 189 | 191 | {{ canarydrop['triggered_list'][item]['is_tor_relay']==True }} 192 |
    195 | {%endif%} 196 | 197 | {% for field in canarydrop['triggered_list'][item] %} 198 | {% if canarydrop['triggered_list'][item][field] != None and 199 | canarydrop['triggered_list'][item][field] is defined and 200 | field not in ['geo_info','is_tor_relay','additional_info','secretkeeper_photo'] %} 201 | {% if field not in ['src_ip','input_channel'] %} 202 | 203 | 204 | 205 | 206 | 209 | 210 | 211 | 214 | 217 | 218 |
    207 | Basic Info 208 |
    212 | {{field}} 213 | 215 | {{ canarydrop['triggered_list'][item][field]|e }} 216 |
    219 | {%endif%} 220 | {%endif%} 221 | {% endfor %} 222 | {% if canarydrop['triggered_list'][item]['additional_info'] %} 223 | 224 | Additional Info 225 | 226 | 227 | {% for info in canarydrop['triggered_list'][item]['additional_info'] %} 228 | 229 | 232 | {%if info == 'iOS-App'%} 233 | 236 | {% else %} 237 | {% if info == 'secretkeeper_photo'%} 238 | 239 | 242 | 245 | 246 | {% endif %} 247 | {%endif%} 248 | 249 | {% if canarydrop['triggered_list'][item]['additional_info'][info].__class__.__name__ == 'str' %} 250 | {% continue %} 251 | {% endif %} 252 | {% for info_item in canarydrop['triggered_list'][item]['additional_info'][info] %} 253 | 254 | 257 | 262 | 263 | {% endfor %} 264 | {% endfor %} 265 |
    230 | {{info}} 231 |
    240 | Photo 241 | 243 | 244 |
    255 | {{info_item}} 256 | 258 | {{ canarydrop['triggered_list'][item]['additional_info'][info][info_item][0] == '1' 259 | if info_item in ['enabled','installed'] 260 | else canarydrop['triggered_list'][item]['additional_info'][info][info_item]|join(', ') }} 261 |
    266 | {% endif %} 267 |
    268 |

    Expand

    269 |
    270 | {% endfor %} 271 | 272 |
273 | 348 | {% if API_KEY %} 349 | 350 | {%endif%} 351 | {%endif%} 352 | 353 | 354 | -------------------------------------------------------------------------------- /channel_dns.py: -------------------------------------------------------------------------------- 1 | from twisted.internet import reactor, defer 2 | from twisted.names import dns, server, error 3 | from twisted.python import log 4 | 5 | from constants import INPUT_CHANNEL_DNS 6 | from tokens import Canarytoken 7 | from canarydrop import Canarydrop 8 | from exception import NoCanarytokenPresent, NoCanarytokenFound 9 | from channel import InputChannel 10 | from queries import get_canarydrop, get_all_canary_domains 11 | 12 | import settings 13 | import math 14 | import base64 15 | import re 16 | 17 | class DNSServerFactory(server.DNSServerFactory, object): 18 | def handleQuery(self, message, protocol, address): 19 | if message.answer: 20 | return 21 | 22 | query = message.queries[0] 23 | src_ip = address[0] 24 | 25 | log.msg('Query: {} sent {}'.format(src_ip, query)) 26 | return self.resolver.query(query, src_ip).addCallback( 27 | self.gotResolverResponse, protocol, message, address 28 | ).addErrback( 29 | self.gotResolverError, protocol, message, address 30 | ) 31 | 32 | def gotResolverError(self, failure, protocol, message, address): 33 | if failure.check(error.DNSQueryRefusedError): 34 | response = self._responseFromMessage(message=message, rCode=dns.EREFUSED) 35 | 36 | self.sendReply(protocol, response, address) 37 | self._verboseLog("Lookup failed") 38 | else: 39 | super(DNSServerFactory, self).gotResolverError(failure, protocol, message, address) 40 | 41 | 42 | 43 | class ChannelDNS(InputChannel): 44 | CHANNEL = INPUT_CHANNEL_DNS 45 | 46 | def __init__(self, listen_domain='canary.thinknest.com', switchboard=None): 47 | super(ChannelDNS, self).__init__(switchboard=switchboard, 48 | name=self.CHANNEL) 49 | self.listen_domain = listen_domain 50 | self.canary_domains = get_all_canary_domains() 51 | 52 | def _do_ns_response(self, name=None): 53 | """ 54 | Calculate the response to a query. 55 | """ 56 | answer = dns.RRHeader( 57 | name=name, 58 | payload=dns.Record_NS(ttl=10, name='ns1.'+name), 59 | type=dns.NS) 60 | additional = dns.RRHeader( 61 | name='ns1.'+name, 62 | payload=dns.Record_A(ttl=10, address=settings.PUBLIC_IP), 63 | type=dns.A) 64 | answers = [answer] 65 | authority = [] 66 | additional = [additional] 67 | return answers, authority, additional 68 | 69 | def _do_soa_response(self, name=None): 70 | """ 71 | Ensure a standard response to a SOA query. 72 | """ 73 | answer = dns.RRHeader( 74 | name=name, 75 | payload=dns.Record_SOA(mname=name.lower(), 76 | rname='info.'+name.lower(), 77 | serial=0, refresh=300, retry=300, expire=300, minimum=300, 78 | ttl=300), 79 | type=dns.SOA) 80 | answers = [answer] 81 | authority = [] 82 | additional = [] 83 | 84 | return answers, authority, additional 85 | 86 | def _do_dynamic_response(self, name=None): 87 | """ 88 | Calculate the response to a query. 89 | """ 90 | payload = dns.Record_A(ttl=10, address=settings.PUBLIC_IP) 91 | answer = dns.RRHeader( 92 | name=name, 93 | payload=payload, 94 | type=dns.A) 95 | answers = [answer] 96 | authority = [] 97 | additional = [] 98 | return answers, authority, additional 99 | 100 | def _do_no_response(self, query=None): 101 | """ 102 | Calculate the response to a query. 103 | """ 104 | answers = [] 105 | authority = [] 106 | additional = [] 107 | return answers, authority, additional 108 | 109 | def _sql_server_data(self, username=None): 110 | data = {} 111 | data['sql_username'] = base64.b64decode(username.replace('.','').replace('-','=')) 112 | return data 113 | 114 | def _mysql_data(self, username=None): 115 | data = {} 116 | data['mysql_username'] = base64.b32decode(username.replace('.','').replace('-','=').upper()) 117 | return data 118 | 119 | def _linux_inotify_data(self, filename=None): 120 | data = {} 121 | filename = filename.replace('.', '').upper() 122 | #this channel doesn't have padding, add if needed 123 | filename += '='*int( 124 | ( 125 | math.ceil(float(len(filename)) / 8) * 8 126 | - len(filename) 127 | ) 128 | ) 129 | data['linux_inotify_filename_access'] = base64.b32decode(filename) 130 | return data 131 | 132 | def _generic(self, generic_data=None): 133 | data = {} 134 | generic_data = generic_data.replace('.', '').upper() 135 | #this channel doesn't have padding, add if needed 136 | generic_data += '='*int( 137 | ( 138 | math.ceil(float(len(generic_data)) / 8) * 8 139 | - len(generic_data) 140 | ) 141 | ) 142 | try: 143 | data['generic_data'] = base64.b32decode(generic_data) 144 | except TypeError: 145 | data['generic_data'] = 'Unrecoverable data: {}'.format(generic_data) 146 | return data 147 | 148 | def _dtrace_process_data(self, uid=None, hostname=None, command=None): 149 | data = {} 150 | try: 151 | data['dtrace_uid'] = base64.b64decode(uid) 152 | except: 153 | log.err('Could not retrieve uid from dtrace '+\ 154 | 'process alert: {uid}'.format(uid=uid)) 155 | try: 156 | data['dtrace_hostname'] = base64.b64decode(hostname.replace('.', '')) 157 | except: 158 | log.err('Could not retrieve hostname from dtrace '+\ 159 | 'process alert: {hostname}'.format(hostname=hostname)) 160 | try: 161 | data['dtrace_command'] = base64.b64decode(command.replace('.', '')) 162 | except: 163 | log.err('Could not retrieve command from dtrace '+\ 164 | 'process alert: {command}'.format(command=command)) 165 | 166 | return data 167 | 168 | def _dtrace_file_open(self, uid=None, hostname=None, filename=None): 169 | data = {} 170 | try: 171 | data['dtrace_uid'] = base64.b64decode(uid) 172 | except: 173 | log.err('Could not retrieve uid from dtrace '+\ 174 | 'file open alert: {uid}'.format(uid=uid)) 175 | 176 | try: 177 | data['dtrace_hostname'] = base64.b64decode(hostname.replace('.', '')) 178 | except: 179 | log.err('Could not retrieve hostname from dtrace '+\ 180 | 'process alert: {hostname}'.format(hostname=hostname)) 181 | try: 182 | data['dtrace_filename'] = base64.b64decode(filename.replace('.', '')) 183 | except: 184 | log.err('Could not retrieve filename from dtrace '+\ 185 | 'file open alert: {filename}'.format(filename=filename)) 186 | 187 | return data 188 | 189 | def _desktop_ini_browsing(self, username=None, hostname=None, domain=None): 190 | data = {} 191 | data['windows_desktopini_access_username'] = username 192 | data['windows_desktopini_access_hostname'] = hostname 193 | data['windows_desktopini_access_domain'] = domain 194 | return data 195 | 196 | def look_for_source_data(self, token=None, value=None): 197 | try: 198 | value = value.lower() 199 | (haystack, domain) = value.split(token) 200 | sql_server_username = re.compile('([A-Za-z0-9.-]*)\.[0-9]{2}\.', re.IGNORECASE) 201 | mysql_username = re.compile('([A-Za-z0-9.-]*)\.M[0-9]{3}\.', re.IGNORECASE) 202 | linux_inotify = re.compile('([A-Za-z0-9.-]*)\.L[0-9]{2}\.', re.IGNORECASE) 203 | generic = re.compile('([A-Za-z0-9.-]*)\.G[0-9]{2}\.', re.IGNORECASE) 204 | dtrace_process = re.compile('([0-9]+)\.([A-Za-z0-9-=]+)\.h\.([A-Za-z0-9.-=]+)\.c\.([A-Za-z0-9.-=]+)\.D1\.', re.IGNORECASE) 205 | dtrace_file_open = re.compile('([0-9]+)\.([A-Za-z0-9-=]+)\.h\.([A-Za-z0-9.-=]+)\.f\.([A-Za-z0-9.-=]+)\.D2\.', re.IGNORECASE) 206 | desktop_ini_browsing = re.compile('([^\.]+)\.([^\.]+)\.?([^\.]*)\.ini\.', re.IGNORECASE) 207 | 208 | m = desktop_ini_browsing.match(value) 209 | if m: 210 | if m.group(3): 211 | return self._desktop_ini_browsing(username=m.group(1), hostname=m.group(2), domain=m.group(3)) 212 | else: 213 | return self._desktop_ini_browsing(username=m.group(1), domain=m.group(2)) 214 | 215 | m = sql_server_username.match(value) 216 | if m: 217 | return self._sql_server_data(username=m.group(1)) 218 | 219 | m = mysql_username.match(value) 220 | if m: 221 | return self._mysql_data(username=m.group(1)) 222 | 223 | m = linux_inotify.match(value) 224 | if m: 225 | return self._linux_inotify_data(filename=m.group(1)) 226 | 227 | m = generic.match(value) 228 | if m: 229 | return self._generic(generic_data=m.group(1)) 230 | 231 | m = dtrace_process.match(value) 232 | if m: 233 | return self._dtrace_process_data(uid=m.group(2), hostname=m.group(3), command=m.group(4)) 234 | 235 | m = dtrace_file_open.match(value) 236 | if m: 237 | return self._dtrace_file_open(uid=m.group(2), hostname=m.group(3), filename=m.group(4)) 238 | 239 | except Exception as e: 240 | log.err(e) 241 | return {} 242 | 243 | def query(self, query, src_ip): 244 | """ 245 | Check if the query should be answered dynamically, otherwise dispatch to 246 | the fallback resolver. 247 | """ 248 | 249 | IS_NX_DOMAIN = True in [query.name.name.lower().endswith(d) 250 | for d in settings.NXDOMAINS] 251 | 252 | if (not True in [query.name.name.lower().endswith(d) for d in self.canary_domains] 253 | and not IS_NX_DOMAIN): 254 | return defer.fail(error.DNSQueryRefusedError()) 255 | 256 | if query.type == dns.NS: 257 | return defer.succeed(self._do_ns_response(name=query.name.name)) 258 | 259 | if query.type == dns.SOA: 260 | return defer.succeed(self._do_soa_response(name=query.name.name)) 261 | 262 | if query.type != dns.A: 263 | return defer.succeed(self._do_no_response(query=query)) 264 | 265 | try: 266 | token = Canarytoken(value=query.name.name) 267 | 268 | canarydrop = Canarydrop(**get_canarydrop(canarytoken=token.value())) 269 | 270 | src_data = self.look_for_source_data(token=token.value(), value=query.name.name) 271 | 272 | self.dispatch(canarydrop=canarydrop, src_ip=src_ip, src_data=src_data) 273 | 274 | except (NoCanarytokenPresent, NoCanarytokenFound): 275 | # If we dont find a canarytoken, lets just continue. No need to log. 276 | pass 277 | except Exception as e: 278 | log.err(e) 279 | 280 | if IS_NX_DOMAIN: 281 | return defer.fail(error.DomainError()) 282 | 283 | return defer.succeed(self._do_dynamic_response(name=query.name.name)) 284 | #return defer.fail(error.DomainError()) 285 | 286 | def lookupCAA(self, name, timeout): 287 | """Respond with NXdomain to a -t CAA lookup.""" 288 | return defer.fail(error.DomainError()) 289 | 290 | def lookupAllRecords(self, name, timeout): 291 | """Respond with error to a -t ANY lookup.""" 292 | return defer.fail(error.DomainError()) 293 | 294 | def format_additional_data(self, **kwargs): 295 | log.msg('%r' % kwargs) 296 | additional_report = 'Source IP : {ip}'.format(ip=kwargs['src_ip']) 297 | 298 | if 'src_data' in kwargs: 299 | 300 | if 'sql_username' in kwargs['src_data']: 301 | additional_report += '\nSQL Server User: {username}'\ 302 | .format(username=kwargs['src_data']['sql_username']) 303 | 304 | if 'mysql_username' in kwargs['src_data']: 305 | additional_report += '\nMySQL User: {username}'\ 306 | .format(username=kwargs['src_data']['mysql_username']) 307 | 308 | if 'linux_inotify_filename_access' in kwargs['src_data']: 309 | additional_report += '\nLinux File Access: {filename}'\ 310 | .format(filename=kwargs['src_data']['linux_inotify_filename_access']) 311 | 312 | if 'generic_data' in kwargs['src_data']: 313 | additional_report += '\nGeneric data: {generic_data}'\ 314 | .format(generic_data=kwargs['src_data']['generic_data']) 315 | 316 | if 'dtrace_uid' in kwargs['src_data']: 317 | additional_report += '\nDTrace UID: {uid}'\ 318 | .format(uid=kwargs['src_data']['dtrace_uid']) 319 | 320 | if 'dtrace_hostname' in kwargs['src_data']: 321 | additional_report += '\nDTrace hostname: {hostname}'\ 322 | .format(hostname=kwargs['src_data']['dtrace_hostname']) 323 | 324 | if 'dtrace_command' in kwargs['src_data']: 325 | additional_report += '\nDTrace command: {command}'\ 326 | .format(command=kwargs['src_data']['dtrace_command']) 327 | 328 | if 'dtrace_filename' in kwargs['src_data']: 329 | additional_report += '\nDTrace filename: {filename}'\ 330 | .format(filename=kwargs['src_data']['dtrace_filename']) 331 | 332 | if 'windows_desktopini_access_username' in kwargs['src_data']\ 333 | and 'windows_desktopini_access_domain' in kwargs['src_data']: 334 | if 'windows_desktopini_access_hostname' in kwargs['src_data']: 335 | additional_report += '\nWindows Directory Browsing By: {domain}\{username} from {hostname}'\ 336 | .format(username=kwargs['src_data']['windows_desktopini_access_username'], 337 | domain=kwargs['src_data']['windows_desktopini_access_domain'], 338 | hostname=kwargs['src_data']['windows_desktopini_access_hostname']) 339 | else: 340 | additional_report += '\nWindows Directory Browsing By: {domain}\{username}'\ 341 | .format(username=kwargs['src_data']['windows_desktopini_access_username'], 342 | domain=kwargs['src_data']['windows_desktopini_access_domain']) 343 | 344 | if 'aws_keys_event_source_ip' in kwargs['src_data']: 345 | additional_report += '\nAWS Keys used by: {ip}'\ 346 | .format(ip=kwargs['src_data']['aws_keys_event_source_ip']) 347 | 348 | return additional_report 349 | -------------------------------------------------------------------------------- /templates/static/perfect-scrollbar.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * perfect-scrollbar v1.3.0 3 | * (c) 2017 Hyunje Jun 4 | * @license MIT 5 | */ 6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.PerfectScrollbar=e()}(this,function(){"use strict";function t(t){return getComputedStyle(t)}function e(t,e){for(var i in e){var r=e[i];"number"==typeof r&&(r+="px"),t.style[i]=r}return t}function i(t){var e=document.createElement("div");return e.className=t,e}function r(t,e){if(!v)throw new Error("No element matching method supported");return v.call(t,e)}function l(t){t.remove?t.remove():t.parentNode&&t.parentNode.removeChild(t)}function n(t,e){return Array.prototype.filter.call(t.children,function(t){return r(t,e)})}function o(t,e){var i=t.element.classList,r=m.state.scrolling(e);i.contains(r)?clearTimeout(Y[e]):i.add(r)}function s(t,e){Y[e]=setTimeout(function(){return t.isAlive&&t.element.classList.remove(m.state.scrolling(e))},t.settings.scrollingThreshold)}function a(t,e){o(t,e),s(t,e)}function c(t){if("function"==typeof window.CustomEvent)return new CustomEvent(t);var e=document.createEvent("CustomEvent");return e.initCustomEvent(t,!1,!1,void 0),e}function h(t,e,i,r,l){var n=i[0],o=i[1],s=i[2],h=i[3],u=i[4],d=i[5];void 0===r&&(r=!0),void 0===l&&(l=!1);var f=t.element;t.reach[h]=null,f[s]<1&&(t.reach[h]="start"),f[s]>t[n]-t[o]-1&&(t.reach[h]="end"),e&&(f.dispatchEvent(c("ps-scroll-"+h)),e<0?f.dispatchEvent(c("ps-scroll-"+u)):e>0&&f.dispatchEvent(c("ps-scroll-"+d)),r&&a(t,h)),t.reach[h]&&(e||l)&&f.dispatchEvent(c("ps-"+h+"-reach-"+t.reach[h]))}function u(t){return parseInt(t,10)||0}function d(t){return r(t,"input,[contenteditable]")||r(t,"select,[contenteditable]")||r(t,"textarea,[contenteditable]")||r(t,"button,[contenteditable]")}function f(e){var i=t(e);return u(i.width)+u(i.paddingLeft)+u(i.paddingRight)+u(i.borderLeftWidth)+u(i.borderRightWidth)}function p(t,e){return t.settings.minScrollbarLength&&(e=Math.max(e,t.settings.minScrollbarLength)),t.settings.maxScrollbarLength&&(e=Math.min(e,t.settings.maxScrollbarLength)),e}function b(t,i){var r={width:i.railXWidth};i.isRtl?r.left=i.negativeScrollAdjustment+t.scrollLeft+i.containerWidth-i.contentWidth:r.left=t.scrollLeft,i.isScrollbarXUsingBottom?r.bottom=i.scrollbarXBottom-t.scrollTop:r.top=i.scrollbarXTop+t.scrollTop,e(i.scrollbarXRail,r);var l={top:t.scrollTop,height:i.railYHeight};i.isScrollbarYUsingRight?i.isRtl?l.right=i.contentWidth-(i.negativeScrollAdjustment+t.scrollLeft)-i.scrollbarYRight-i.scrollbarYOuterWidth:l.right=i.scrollbarYRight-t.scrollLeft:i.isRtl?l.left=i.negativeScrollAdjustment+t.scrollLeft+2*i.containerWidth-i.contentWidth-i.scrollbarYLeft-i.scrollbarYOuterWidth:l.left=i.scrollbarYLeft+t.scrollLeft,e(i.scrollbarYRail,l),e(i.scrollbarX,{left:i.scrollbarXLeft,width:i.scrollbarXWidth-i.railBorderXWidth}),e(i.scrollbarY,{top:i.scrollbarYTop,height:i.scrollbarYHeight-i.railBorderYWidth})}function g(t,e){function i(e){p[d]=b+v*(e[a]-g),o(t,f),T(t),e.stopPropagation(),e.preventDefault()}function r(){s(t,f),t.event.unbind(t.ownerDocument,"mousemove",i)}var l=e[0],n=e[1],a=e[2],c=e[3],h=e[4],u=e[5],d=e[6],f=e[7],p=t.element,b=null,g=null,v=null;t.event.bind(t[h],"mousedown",function(e){b=p[d],g=e[a],v=(t[n]-t[l])/(t[c]-t[u]),t.event.bind(t.ownerDocument,"mousemove",i),t.event.once(t.ownerDocument,"mouseup",r),e.stopPropagation(),e.preventDefault()})}var v="undefined"!=typeof Element&&(Element.prototype.matches||Element.prototype.webkitMatchesSelector||Element.prototype.msMatchesSelector),m={main:"ps",element:{thumb:function(t){return"ps__thumb-"+t},rail:function(t){return"ps__rail-"+t},consuming:"ps__child--consume"},state:{focus:"ps--focus",active:function(t){return"ps--active-"+t},scrolling:function(t){return"ps--scrolling-"+t}}},Y={x:null,y:null},X=function(t){this.element=t,this.handlers={}},w={isEmpty:{configurable:!0}};X.prototype.bind=function(t,e){void 0===this.handlers[t]&&(this.handlers[t]=[]),this.handlers[t].push(e),this.element.addEventListener(t,e,!1)},X.prototype.unbind=function(t,e){var i=this;this.handlers[t]=this.handlers[t].filter(function(r){return!(!e||r===e)||(i.element.removeEventListener(t,r,!1),!1)})},X.prototype.unbindAll=function(){var t=this;for(var e in t.handlers)t.unbind(e)},w.isEmpty.get=function(){var t=this;return Object.keys(this.handlers).every(function(e){return 0===t.handlers[e].length})},Object.defineProperties(X.prototype,w);var y=function(){this.eventElements=[]};y.prototype.eventElement=function(t){var e=this.eventElements.filter(function(e){return e.element===t})[0];return e||(e=new X(t),this.eventElements.push(e)),e},y.prototype.bind=function(t,e,i){this.eventElement(t).bind(e,i)},y.prototype.unbind=function(t,e,i){var r=this.eventElement(t);r.unbind(e,i),r.isEmpty&&this.eventElements.splice(this.eventElements.indexOf(r),1)},y.prototype.unbindAll=function(){this.eventElements.forEach(function(t){return t.unbindAll()}),this.eventElements=[]},y.prototype.once=function(t,e,i){var r=this.eventElement(t),l=function(t){r.unbind(e,l),i(t)};r.bind(e,l)};var W=function(t,e,i,r,l){void 0===r&&(r=!0),void 0===l&&(l=!1);var n;if("top"===e)n=["contentHeight","containerHeight","scrollTop","y","up","down"];else{if("left"!==e)throw new Error("A proper axis should be provided");n=["contentWidth","containerWidth","scrollLeft","x","left","right"]}h(t,i,n,r,l)},L={isWebKit:"undefined"!=typeof document&&"WebkitAppearance"in document.documentElement.style,supportsTouch:"undefined"!=typeof window&&("ontouchstart"in window||window.DocumentTouch&&document instanceof window.DocumentTouch),supportsIePointer:"undefined"!=typeof navigator&&navigator.msMaxTouchPoints,isChrome:"undefined"!=typeof navigator&&/Chrome/i.test(navigator&&navigator.userAgent)},T=function(t){var e=t.element;t.containerWidth=e.clientWidth,t.containerHeight=e.clientHeight,t.contentWidth=e.scrollWidth,t.contentHeight=e.scrollHeight,e.contains(t.scrollbarXRail)||(n(e,m.element.rail("x")).forEach(function(t){return l(t)}),e.appendChild(t.scrollbarXRail)),e.contains(t.scrollbarYRail)||(n(e,m.element.rail("y")).forEach(function(t){return l(t)}),e.appendChild(t.scrollbarYRail)),!t.settings.suppressScrollX&&t.containerWidth+t.settings.scrollXMarginOffset=t.railXWidth-t.scrollbarXWidth&&(t.scrollbarXLeft=t.railXWidth-t.scrollbarXWidth),t.scrollbarYTop>=t.railYHeight-t.scrollbarYHeight&&(t.scrollbarYTop=t.railYHeight-t.scrollbarYHeight),b(e,t),t.scrollbarXActive?e.classList.add(m.state.active("x")):(e.classList.remove(m.state.active("x")),t.scrollbarXWidth=0,t.scrollbarXLeft=0,e.scrollLeft=0),t.scrollbarYActive?e.classList.add(m.state.active("y")):(e.classList.remove(m.state.active("y")),t.scrollbarYHeight=0,t.scrollbarYTop=0,e.scrollTop=0)},R={"click-rail":function(t){t.event.bind(t.scrollbarY,"mousedown",function(t){return t.stopPropagation()}),t.event.bind(t.scrollbarYRail,"mousedown",function(e){var i=e.pageY-window.pageYOffset-t.scrollbarYRail.getBoundingClientRect().top>t.scrollbarYTop?1:-1;t.element.scrollTop+=i*t.containerHeight,T(t),e.stopPropagation()}),t.event.bind(t.scrollbarX,"mousedown",function(t){return t.stopPropagation()}),t.event.bind(t.scrollbarXRail,"mousedown",function(e){var i=e.pageX-window.pageXOffset-t.scrollbarXRail.getBoundingClientRect().left>t.scrollbarXLeft?1:-1;t.element.scrollLeft+=i*t.containerWidth,T(t),e.stopPropagation()})},"drag-thumb":function(t){g(t,["containerWidth","contentWidth","pageX","railXWidth","scrollbarX","scrollbarXWidth","scrollLeft","x"]),g(t,["containerHeight","contentHeight","pageY","railYHeight","scrollbarY","scrollbarYHeight","scrollTop","y"])},keyboard:function(t){function e(e,r){var l=i.scrollTop;if(0===e){if(!t.scrollbarYActive)return!1;if(0===l&&r>0||l>=t.contentHeight-t.containerHeight&&r<0)return!t.settings.wheelPropagation}var n=i.scrollLeft;if(0===r){if(!t.scrollbarXActive)return!1;if(0===n&&e<0||n>=t.contentWidth-t.containerWidth&&e>0)return!t.settings.wheelPropagation}return!0}var i=t.element,l=function(){return r(i,":hover")},n=function(){return r(t.scrollbarX,":focus")||r(t.scrollbarY,":focus")};t.event.bind(t.ownerDocument,"keydown",function(r){if(!(r.isDefaultPrevented&&r.isDefaultPrevented()||r.defaultPrevented)&&(l()||n())){var o=document.activeElement?document.activeElement:t.ownerDocument.activeElement;if(o){if("IFRAME"===o.tagName)o=o.contentDocument.activeElement;else for(;o.shadowRoot;)o=o.shadowRoot.activeElement;if(d(o))return}var s=0,a=0;switch(r.which){case 37:s=r.metaKey?-t.contentWidth:r.altKey?-t.containerWidth:-30;break;case 38:a=r.metaKey?t.contentHeight:r.altKey?t.containerHeight:30;break;case 39:s=r.metaKey?t.contentWidth:r.altKey?t.containerWidth:30;break;case 40:a=r.metaKey?-t.contentHeight:r.altKey?-t.containerHeight:-30;break;case 32:a=r.shiftKey?t.containerHeight:-t.containerHeight;break;case 33:a=t.containerHeight;break;case 34:a=-t.containerHeight;break;case 36:a=t.contentHeight;break;case 35:a=-t.contentHeight;break;default:return}t.settings.suppressScrollX&&0!==s||t.settings.suppressScrollY&&0!==a||(i.scrollTop-=a,i.scrollLeft+=s,T(t),e(s,a)&&r.preventDefault())}})},wheel:function(e){function i(t,i){var r=0===o.scrollTop,l=o.scrollTop+o.offsetHeight===o.scrollHeight,n=0===o.scrollLeft,s=o.scrollLeft+o.offsetWidth===o.offsetWidth;return!(Math.abs(i)>Math.abs(t)?r||l:n||s)||!e.settings.wheelPropagation}function r(t){var e=t.deltaX,i=-1*t.deltaY;return void 0!==e&&void 0!==i||(e=-1*t.wheelDeltaX/6,i=t.wheelDeltaY/6),t.deltaMode&&1===t.deltaMode&&(e*=10,i*=10),e!==e&&i!==i&&(e=0,i=t.wheelDelta),t.shiftKey?[-i,-e]:[e,i]}function l(e,i,r){if(!L.isWebKit&&o.querySelector("select:focus"))return!0;if(!o.contains(e))return!1;for(var l=e;l&&l!==o;){if(l.classList.contains(m.element.consuming))return!0;var n=t(l);if([n.overflow,n.overflowX,n.overflowY].join("").match(/(scroll|auto)/)){var s=l.scrollHeight-l.clientHeight;if(s>0&&!(0===l.scrollTop&&r>0||l.scrollTop===s&&r<0))return!0;var a=l.scrollLeft-l.clientWidth;if(a>0&&!(0===l.scrollLeft&&i<0||l.scrollLeft===a&&i>0))return!0}l=l.parentNode}return!1}function n(t){var n=r(t),s=n[0],a=n[1];if(!l(t.target,s,a)){var c=!1;e.settings.useBothWheelAxes?e.scrollbarYActive&&!e.scrollbarXActive?(a?o.scrollTop-=a*e.settings.wheelSpeed:o.scrollTop+=s*e.settings.wheelSpeed,c=!0):e.scrollbarXActive&&!e.scrollbarYActive&&(s?o.scrollLeft+=s*e.settings.wheelSpeed:o.scrollLeft-=a*e.settings.wheelSpeed,c=!0):(o.scrollTop-=a*e.settings.wheelSpeed,o.scrollLeft+=s*e.settings.wheelSpeed),T(e),(c=c||i(s,a))&&!t.ctrlKey&&(t.stopPropagation(),t.preventDefault())}}var o=e.element;void 0!==window.onwheel?e.event.bind(o,"wheel",n):void 0!==window.onmousewheel&&e.event.bind(o,"mousewheel",n)},touch:function(e){function i(t,i){var r=h.scrollTop,l=h.scrollLeft,n=Math.abs(t),o=Math.abs(i);if(o>n){if(i<0&&r===e.contentHeight-e.containerHeight||i>0&&0===r)return 0===window.scrollY&&i>0&&L.isChrome}else if(n>o&&(t<0&&l===e.contentWidth-e.containerWidth||t>0&&0===l))return!0;return!0}function r(t,i){h.scrollTop-=i,h.scrollLeft-=t,T(e)}function l(t){return t.targetTouches?t.targetTouches[0]:t}function n(t){return!(t.pointerType&&"pen"===t.pointerType&&0===t.buttons||(!t.targetTouches||1!==t.targetTouches.length)&&(!t.pointerType||"mouse"===t.pointerType||t.pointerType===t.MSPOINTER_TYPE_MOUSE))}function o(t){if(n(t)){var e=l(t);u.pageX=e.pageX,u.pageY=e.pageY,d=(new Date).getTime(),null!==p&&clearInterval(p)}}function s(e,i,r){if(!h.contains(e))return!1;for(var l=e;l&&l!==h;){if(l.classList.contains(m.element.consuming))return!0;var n=t(l);if([n.overflow,n.overflowX,n.overflowY].join("").match(/(scroll|auto)/)){var o=l.scrollHeight-l.clientHeight;if(o>0&&!(0===l.scrollTop&&r>0||l.scrollTop===o&&r<0))return!0;var s=l.scrollLeft-l.clientWidth;if(s>0&&!(0===l.scrollLeft&&i<0||l.scrollLeft===s&&i>0))return!0}l=l.parentNode}return!1}function a(t){if(n(t)){var e=l(t),o={pageX:e.pageX,pageY:e.pageY},a=o.pageX-u.pageX,c=o.pageY-u.pageY;if(s(t.target,a,c))return;r(a,c),u=o;var h=(new Date).getTime(),p=h-d;p>0&&(f.x=a/p,f.y=c/p,d=h),i(a,c)&&t.preventDefault()}}function c(){e.settings.swipeEasing&&(clearInterval(p),p=setInterval(function(){e.isInitialized?clearInterval(p):f.x||f.y?Math.abs(f.x)<.01&&Math.abs(f.y)<.01?clearInterval(p):(r(30*f.x,30*f.y),f.x*=.8,f.y*=.8):clearInterval(p)},10))}if(L.supportsTouch||L.supportsIePointer){var h=e.element,u={},d=0,f={},p=null;L.supportsTouch?(e.event.bind(h,"touchstart",o),e.event.bind(h,"touchmove",a),e.event.bind(h,"touchend",c)):L.supportsIePointer&&(window.PointerEvent?(e.event.bind(h,"pointerdown",o),e.event.bind(h,"pointermove",a),e.event.bind(h,"pointerup",c)):window.MSPointerEvent&&(e.event.bind(h,"MSPointerDown",o),e.event.bind(h,"MSPointerMove",a),e.event.bind(h,"MSPointerUp",c)))}}},H=function(r,l){var n=this;if(void 0===l&&(l={}),"string"==typeof r&&(r=document.querySelector(r)),!r||!r.nodeName)throw new Error("no element is specified to initialize PerfectScrollbar");this.element=r,r.classList.add(m.main),this.settings={handlers:["click-rail","drag-thumb","keyboard","wheel","touch"],maxScrollbarLength:null,minScrollbarLength:null,scrollingThreshold:1e3,scrollXMarginOffset:0,scrollYMarginOffset:0,suppressScrollX:!1,suppressScrollY:!1,swipeEasing:!0,useBothWheelAxes:!1,wheelPropagation:!1,wheelSpeed:1};for(var o in l)n.settings[o]=l[o];this.containerWidth=null,this.containerHeight=null,this.contentWidth=null,this.contentHeight=null;var s=function(){return r.classList.add(m.state.focus)},a=function(){return r.classList.remove(m.state.focus)};this.isRtl="rtl"===t(r).direction,this.isNegativeScroll=function(){var t=r.scrollLeft,e=null;return r.scrollLeft=-1,e=r.scrollLeft<0,r.scrollLeft=t,e}(),this.negativeScrollAdjustment=this.isNegativeScroll?r.scrollWidth-r.clientWidth:0,this.event=new y,this.ownerDocument=r.ownerDocument||document,this.scrollbarXRail=i(m.element.rail("x")),r.appendChild(this.scrollbarXRail),this.scrollbarX=i(m.element.thumb("x")),this.scrollbarXRail.appendChild(this.scrollbarX),this.scrollbarX.setAttribute("tabindex",0),this.event.bind(this.scrollbarX,"focus",s),this.event.bind(this.scrollbarX,"blur",a),this.scrollbarXActive=null,this.scrollbarXWidth=null,this.scrollbarXLeft=null;var c=t(this.scrollbarXRail);this.scrollbarXBottom=parseInt(c.bottom,10),isNaN(this.scrollbarXBottom)?(this.isScrollbarXUsingBottom=!1,this.scrollbarXTop=u(c.top)):this.isScrollbarXUsingBottom=!0,this.railBorderXWidth=u(c.borderLeftWidth)+u(c.borderRightWidth),e(this.scrollbarXRail,{display:"block"}),this.railXMarginWidth=u(c.marginLeft)+u(c.marginRight),e(this.scrollbarXRail,{display:""}),this.railXWidth=null,this.railXRatio=null,this.scrollbarYRail=i(m.element.rail("y")),r.appendChild(this.scrollbarYRail),this.scrollbarY=i(m.element.thumb("y")),this.scrollbarYRail.appendChild(this.scrollbarY),this.scrollbarY.setAttribute("tabindex",0),this.event.bind(this.scrollbarY,"focus",s),this.event.bind(this.scrollbarY,"blur",a),this.scrollbarYActive=null,this.scrollbarYHeight=null,this.scrollbarYTop=null;var h=t(this.scrollbarYRail);this.scrollbarYRight=parseInt(h.right,10),isNaN(this.scrollbarYRight)?(this.isScrollbarYUsingRight=!1,this.scrollbarYLeft=u(h.left)):this.isScrollbarYUsingRight=!0,this.scrollbarYOuterWidth=this.isRtl?f(this.scrollbarY):null,this.railBorderYWidth=u(h.borderTopWidth)+u(h.borderBottomWidth),e(this.scrollbarYRail,{display:"block"}),this.railYMarginHeight=u(h.marginTop)+u(h.marginBottom),e(this.scrollbarYRail,{display:""}),this.railYHeight=null,this.railYRatio=null,this.reach={x:r.scrollLeft<=0?"start":r.scrollLeft>=this.contentWidth-this.containerWidth?"end":null,y:r.scrollTop<=0?"start":r.scrollTop>=this.contentHeight-this.containerHeight?"end":null},this.isAlive=!0,this.settings.handlers.forEach(function(t){return R[t](n)}),this.lastScrollTop=r.scrollTop,this.lastScrollLeft=r.scrollLeft,this.event.bind(this.element,"scroll",function(t){return n.onScroll(t)}),T(this)};return H.prototype.update=function(){this.isAlive&&(this.negativeScrollAdjustment=this.isNegativeScroll?this.element.scrollWidth-this.element.clientWidth:0,e(this.scrollbarXRail,{display:"block"}),e(this.scrollbarYRail,{display:"block"}),this.railXMarginWidth=u(t(this.scrollbarXRail).marginLeft)+u(t(this.scrollbarXRail).marginRight),this.railYMarginHeight=u(t(this.scrollbarYRail).marginTop)+u(t(this.scrollbarYRail).marginBottom),e(this.scrollbarXRail,{display:"none"}),e(this.scrollbarYRail,{display:"none"}),T(this),W(this,"top",0,!1,!0),W(this,"left",0,!1,!0),e(this.scrollbarXRail,{display:""}),e(this.scrollbarYRail,{display:""}))},H.prototype.onScroll=function(t){this.isAlive&&(T(this),W(this,"top",this.element.scrollTop-this.lastScrollTop),W(this,"left",this.element.scrollLeft-this.lastScrollLeft),this.lastScrollTop=this.element.scrollTop,this.lastScrollLeft=this.element.scrollLeft)},H.prototype.destroy=function(){this.isAlive&&(this.event.unbindAll(),l(this.scrollbarX),l(this.scrollbarY),l(this.scrollbarXRail),l(this.scrollbarYRail),this.removePsClasses(),this.element=null,this.scrollbarX=null,this.scrollbarY=null,this.scrollbarXRail=null,this.scrollbarYRail=null,this.isAlive=!1)},H.prototype.removePsClasses=function(){this.element.className=this.element.className.split(" ").filter(function(t){return!t.match(/^ps([-_].+|)$/)}).join(" ")},H}); --------------------------------------------------------------------------------