eris

- Elasticsearch Recon Ingestion Scripts (ERIS) 🔎
git clone git://git.acid.vegas/eris.git
Log | Files | Refs | Archive | README | LICENSE

ingest_certstream.py (5500B)

      1 #!/usr/bin/env python
      2 # Elasticsearch Recon Ingestion Scripts (ERIS) - Developed by Acidvegas (https://git.acid.vegas/eris)
      3 # ingest_certstream.py
      4 
      5 import asyncio
      6 import json
      7 import logging
      8 import time
      9 
     10 try:
     11 	import websockets
     12 except ImportError:
     13 	raise ImportError('Missing required \'websockets\' library. (pip install websockets)')
     14 
     15 
     16 # Set a default elasticsearch index if one is not provided
     17 default_index = 'eris-certstream'
     18 
     19 # Set the cache size for the Certstream records to prevent duplicates
     20 cache      = []
     21 cache_size = 5000
     22 
     23 
     24 def construct_map() -> dict:
     25 	'''Construct the Elasticsearch index mapping for Certstream records.'''
     26 
     27 	# Match on exact value or full text search
     28 	keyword_mapping = { 'type': 'text', 'fields': { 'keyword': { 'type': 'keyword', 'ignore_above': 256 } } }
     29 
     30 	# Construct the index mapping
     31 	mapping = {
     32 		'mappings': {
     33 			'properties' : {
     34 				'domain' : keyword_mapping,
     35 				'seen'   : { 'type': 'date' }
     36 			}
     37 		}
     38 	}
     39 
     40 	return mapping
     41 
     42 
     43 async def process_data(place_holder: str = None):
     44 	'''
     45 	Read and process Certsream records live from the Websocket stream.
     46 
     47 	:param place_holder: Placeholder parameter to match the process_data function signature of other ingestors.
     48 	'''
     49 
     50 	async for websocket in websockets.connect('wss://certstream.calidog.io', ping_interval=15, ping_timeout=60):
     51 		try:
     52 			# Read the websocket stream
     53 			async for line in websocket:
     54 
     55 				# Parse the JSON record
     56 				try:
     57 					record = json.loads(line)
     58 				except json.decoder.JSONDecodeError:
     59 					logging.error(f'Invalid line from the websocket: {line}')
     60 					continue
     61 
     62 				# Grab the unique domains from the records
     63 				all_domains = set(record['data']['leaf_cert']['all_domains'])
     64 				domains     =  list()
     65 
     66 				# We only care about subdomains (excluding www. and wildcards)
     67 				for domain in all_domains:
     68 					if domain.startswith('*.'):
     69 						domain = domain[2:]
     70 					if domain.startswith('www.') and domain.count('.') == 2:
     71 						continue
     72 					if domain.count('.') > 1:
     73 						# TODO: Add a check for PSL TLDs...domain.co.uk, domain.com.au, etc. (we want to ignore these if they are not subdomains)
     74 						if domain not in domains:
     75 							domains.append(domain)
     76 
     77 				# Construct the document
     78 				for domain in domains:
     79 					if domain in cache:
     80 						continue # Skip the domain if it is already in the cache
     81 
     82 					if len(cache) >= cache_size:
     83 						cache.pop(0) # Remove the oldest domain from the cache
     84 
     85 					cache.append(domain) # Add the domain to the cache
     86 
     87 					struct = {
     88 						'domain' : domain,
     89 						'seen'   : time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
     90 					}
     91 
     92 					yield {'_index': default_index, '_source': struct}
     93 
     94 		except websockets.ConnectionClosed as e	:
     95 			logging.error(f'Connection to Certstream was closed. Attempting to reconnect... ({e})')
     96 			await asyncio.sleep(3)
     97 
     98 		except Exception as e:
     99 			logging.error(f'Error processing Certstream data: {e}')
    100 			await asyncio.sleep(3)
    101 
    102 
    103 async def test():
    104 	'''Test the ingestion process.'''
    105 
    106 	async for document in process_data():
    107 		print(document)
    108 
    109 
    110 
    111 if __name__ == '__main__':
    112 	asyncio.run(test())
    113 
    114 
    115 
    116 '''
    117 Output:
    118 	{
    119 		"data": {
    120 			"cert_index": 43061646,
    121 			"cert_link": "https://yeti2025.ct.digicert.com/log/ct/v1/get-entries?start=43061646&end=43061646",
    122 			"leaf_cert": {
    123 				"all_domains": [
    124 					"*.d7zdnegbre53n.amplifyapp.com",
    125 					"d7zdnegbre53n.amplifyapp.com"
    126 				],
    127 				"extensions": {
    128 					"authorityInfoAccess"    : "CA Issuers - URI:http://crt.r2m02.amazontrust.com/r2m02.cer\nOCSP - URI:http://ocsp.r2m02.amazontrust.com\n",
    129 					"authorityKeyIdentifier" : "keyid:C0:31:52:CD:5A:50:C3:82:7C:74:71:CE:CB:E9:9C:F9:7A:EB:82:E2\n",
    130 					"basicConstraints"       : "CA:FALSE",
    131 					"certificatePolicies"    : "Policy: 2.23.140.1.2.1",
    132 					"crlDistributionPoints"  : "Full Name:\n URI:http://crl.r2m02.amazontrust.com/r2m02.crl",
    133 					"ctlPoisonByte"          : true,
    134 					"extendedKeyUsage"       : "TLS Web server authentication, TLS Web client authentication",
    135 					"keyUsage"               : "Digital Signature, Key Encipherment",
    136 					"subjectAltName"         : "DNS:d7zdnegbre53n.amplifyapp.com, DNS:*.d7zdnegbre53n.amplifyapp.com",
    137 					"subjectKeyIdentifier"   : "59:32:78:2A:11:03:62:55:BB:3B:B9:80:24:76:28:90:2E:D1:A4:56"
    138 				},
    139 				"fingerprint": "D9:05:A3:D5:AA:F9:68:BC:0C:0A:15:69:C9:5E:11:92:32:67:4F:FA",
    140 				"issuer": {
    141 					"C"            : "US",
    142 					"CN"           : "Amazon RSA 2048 M02",
    143 					"L"            : null,
    144 					"O"            : "Amazon",
    145 					"OU"           : null,
    146 					"ST"           : null,
    147 					"aggregated"   : "/C=US/CN=Amazon RSA 2048 M02/O=Amazon",
    148 					"emailAddress" : null
    149 				},
    150 				"not_after"           : 1743811199,
    151 				"not_before"          : 1709596800,
    152 				"serial_number"       : "FDB450C1942E3D30A18737063449E62",
    153 				"signature_algorithm" : "sha256, rsa",
    154 				"subject": {
    155 					"C"            : null,
    156 					"CN"           : "*.d7zdnegbre53n.amplifyapp.com",
    157 					"L"            : null,
    158 					"O"            : null,
    159 					"OU"           : null,
    160 					"ST"           : null,
    161 					"aggregated"   : "/CN=*.d7zdnegbre53n.amplifyapp.com",
    162 					"emailAddress" : null
    163 				}
    164 			},
    165 			"seen": 1709651773.594684,
    166 			"source": {
    167 				"name" : "DigiCert Yeti2025 Log",
    168 				"url"  : "https://yeti2025.ct.digicert.com/log/"
    169 			},
    170 			"update_type": "PrecertLogEntry"
    171 		},
    172 		"message_type": "certificate_update"
    173 	}
    174 
    175 	
    176 Input:
    177 	{
    178 		"domain" : "d7zdnegbre53n.amplifyapp.com",
    179 		"seen"   : "2022-01-02T12:00:00Z"
    180 	}
    181 
    182 Notes:
    183 	- Fix the "no close frame received or sent" error
    184 '''