├── README.md ├── config.json └── main.py /README.md: -------------------------------------------------------------------------------- 1 | # Timeline Builder for Carbon Black Response 2 | This tool is a quick script written to export tagged items within a specific Carbon Black Response investigation into timelines. 3 | These timelines can be ingested by the SOC/IR team for further analysis. 4 | 5 | Writeup on this tool can be foud here: https://blog.stillztech.com/2018/09/carbon-black-response-timeliner.html 6 | 7 | > Outputs all tagged events into a timeline (two export methods): 8 | - All events per your investigation 9 | - Only events for the hostname specified 10 | 11 | ##### Usage 12 | > Update `config.json` with your CBR URL, CBR API token and your investigation ID. 13 | 14 | ##### Running the script 15 | > python3 main.py 16 | 17 | ##### Output 18 | 1) Hostname Specific 19 | _childproc.csv 20 | crossprocess.csv 21 | _filemod.csv 22 | _modload.csv 23 | _regmod.csv 24 | _timeline.csv -> All Events combined for a single host 25 | 26 | 2) All items in investigation 27 | all_items_childproc.csv 28 | all_items_crossprocess.csv 29 | all_items_filemod.csv 30 | all_items_modload.csv 31 | all_items_regmod.csv 32 | all_items_timeline.csv -> All Events combined for all tagged items in your investigation 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "cb_url": "", 3 | "cb_api": "", 4 | "investigation_id": "" 5 | } 6 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | requests.packages.urllib3.disable_warnings() 4 | 5 | with open('config.json', 'r') as fd: 6 | cfg = json.load(fd) 7 | 8 | cb_api = cfg.get('cb_api') 9 | url = cfg.get('cb_url') 10 | 11 | payload = {'X-Auth-Token': cb_api} 12 | investigation = cfg.get('investigation_id') 13 | full_url = url + "/api/tagged_event/" + investigation 14 | 15 | ############## 16 | 17 | def host_lookup(hostname): 18 | try: 19 | get_sensor = requests.get(url + "/api/v1/sensor", headers=payload, verify=False) 20 | snr_lookup = get_sensor.json() 21 | 22 | for line in snr_lookup: 23 | snr_name = line.get('computer_name').lower() 24 | snr_id = str(line.get('id')) 25 | 26 | if (hostname == snr_name) or (hostname == snr_id): 27 | jj = {"result": 1, 28 | "hostname": snr_name, 29 | "id": snr_id 30 | } 31 | return jj 32 | 33 | jj = {"result": 0} 34 | return jj 35 | 36 | except Exception as e: 37 | print(e) 38 | 39 | def generate_timeline(hostname=None): 40 | 41 | if hostname == None: 42 | hostname = "all_items" 43 | 44 | master_timeline = hostname + "_master.csv" 45 | file_cldprc = hostname + "_childproc.csv" 46 | file_crsproc = hostname + "_crossprocess.csv" 47 | file_ml = hostname + "_modload.csv" 48 | file_fm = hostname + "_filemod.csv" 49 | file_rm = hostname + "_regmod.csv" 50 | file_nc = hostname + "_netconn.csv" 51 | 52 | with open(master_timeline, 'w') as mas: 53 | mas.write("Timestamp | Type | hostname | Description \n") 54 | 55 | with open(file_cldprc, 'w') as cldp: 56 | cldp.write( "hostname| start time | PID | process name | process path| arguments | MD5| username | PPID| parent name| parent path \n") 57 | 58 | with open(file_crsproc, 'w') as crossprc: 59 | crossprc.write("hostname| timestamp| accessor name| accessor path| access type| accessing | md5 | description\n") 60 | 61 | with open(file_ml, 'w') as ml: 62 | ml.write("hostname| timestamp| process name| process path| modload file| MD5| signature| description \n") 63 | 64 | with open(file_fm, 'w') as fm: 65 | fm.write("hostname| timestamp| file path| action| writer| writer name| description \n") 66 | 67 | with open(file_rm, 'w') as rm: 68 | rm.write("hostname| timestamp| action| path| process name| process path| description \n") 69 | 70 | with open(file_nc, 'w') as nc: 71 | nc.write("hostname| timestamp| process name| process path| dst address| port| protocol| type| description \n") 72 | 73 | get_items = requests.get(full_url, headers=payload, verify=False) 74 | get_done = get_items.json() 75 | 76 | for items in get_done: 77 | event_type = items.get('event_type') 78 | event_data = items.get('event_data') 79 | ddx = json.loads(event_data) 80 | host = ddx.get('hostname').lower() 81 | 82 | if hostname != "all_items": 83 | if hostname != host: 84 | continue 85 | 86 | if event_type == "childproc": 87 | start1 = ddx.get('start_date').replace('T', ' ').replace('Z', '') 88 | process_md5 = ddx.get('md5') 89 | proc_pid = ddx.get('pid') 90 | proc_name = ddx.get('pathWithMarker').replace('PREPREPRE', '').replace('POSTPOSTPOST', '') 91 | path = ddx.get('fields').get('path') 92 | md5 = ddx.get('fields').get('md5') 93 | commandLine = ddx.get('fields').get('commandLine') 94 | username = ddx.get('fields').get('username') 95 | 96 | ## Let try and fetch the orginal process data, else we just use the data from the tagged event itself 97 | try: 98 | alz_id = ddx.get('analyze_link') 99 | alz_clean = alz_id.replace("#/analyze", "").replace("/1?cb.legacy_5x_mode=false", "") 100 | 101 | get_args_go = requests.get(url + "/api/v1/process" + alz_clean, headers=payload, verify=False) 102 | get_args = get_args_go.json() 103 | 104 | parent_process_pid = get_args.get('process').get('parent_pid') 105 | parent_process_name = get_args.get('parent').get('process_name').replace('PREPREPRE', '').replace('POSTPOSTPOST', '') 106 | parent_path = get_args.get('parent').get('path') 107 | 108 | process_pid = get_args.get('process').get('process_pid') 109 | username = get_args.get('process').get('username') 110 | process_name = get_args.get('process').get('process_name').replace('PREPREPRE', '').replace('POSTPOSTPOST', '') 111 | path = get_args.get('process').get('path') 112 | start2 = get_args.get('process').get('start').replace('T', ' ').replace('Z', '') 113 | process_md5 = get_args.get('process').get('process_md5') 114 | cmdline = get_args.get('process').get('cmdline') 115 | 116 | ## Write to its own file per event type 117 | with open(file_cldprc, 'a+') as cld: 118 | cld.write('|'.join(map(str, [host, start2, process_pid, process_name, path, cmdline, process_md5, username, parent_process_pid, parent_process_name, parent_path]))+ "\n") 119 | 120 | ## Write to the master timeline 121 | with open(master_timeline, 'a+') as ma: 122 | master_desc = "Process_PID: "+str(proc_pid)+" Process_Name: "+proc_name+" Cmdline: " + cmdline+" Process_MD5: "+process_md5+" Parent_Process_Name: " + parent_process_name 123 | ma.write('|'.join(map(str, [start1, event_type, host, master_desc]))+ "\n") 124 | 125 | except Exception as e: 126 | print("Original childproc process event is gone, using the process investigation data instead.") 127 | ## Write to its own file with limited fields 128 | with open(file_cldprc, 'a+') as cld: 129 | cld.write('|'.join(map(str, [host, start1, proc_pid, proc_name,path,commandLine, md5, username]))+ "\n") 130 | 131 | ## Write to master with limited fields 132 | cp_desc_bucket = "Process_PID: "+proc_pid+" Process_Name: "+proc_name+" Process_MD5: "+process_md5 133 | with open(master_timeline, 'a+') as ma: 134 | ma.write('|'.join(map(str, [start1, event_type, host, cp_desc_bucket]))+ "\n") 135 | 136 | elif event_type == "modload": 137 | sig = ddx.get('signature_str') 138 | description = ddx.get('description') 139 | process_path = ddx.get('path') 140 | process_name = ddx.get('process_name').replace('PREPREPRE', '').replace('POSTPOSTPOST', '') 141 | timestamp = ddx.get('start_date').replace('T', ' ').replace('Z', '') 142 | md5 = ddx.get('md5') 143 | modload_filename = ddx.get('fields')[2] 144 | 145 | # Individual file modloads 146 | with open(file_ml, 'a') as ml: 147 | ml.write('|'.join(map(str, [host, timestamp, process_name, process_path, modload_filename, md5, sig, description]))+"\n") 148 | 149 | # Write to master timeline 150 | ml_mast_desc = "Modload File: " + modload_filename + " MD5: " + md5 + " Process Path: " + process_path 151 | with open(master_timeline, 'a') as ma: 152 | ma.write('|'.join(map(str, [timestamp, event_type, host, ml_mast_desc]))+ "\n") 153 | 154 | elif event_type == "crossproc": 155 | timestamp = ddx.get('start').replace('T', ' ').replace('Z', '') 156 | accessor_name = ddx.get('process_name').replace('PREPREPRE', '').replace('POSTPOSTPOST', '') 157 | accessor_path = ddx.get('path').replace('PREPREPRE', '').replace('POSTPOSTPOST', '') 158 | access_type = ddx.get('procTypeWithMarker') 159 | accessing = ddx.get('pathForRender') 160 | description = ddx.get('description') 161 | md5 = ddx.get('md5') 162 | 163 | with open(file_crsproc, 'a') as cp: 164 | cp.write('|'.join(map(str, [host, timestamp, accessor_name, accessor_path, access_type, accessing, md5, description]))+ "\n") 165 | 166 | #event_type, hostname, start, description, md5 167 | crossp_mast_desc = "Accessor Name: " + accessor_name + " Accessor Path: " + accessor_path + " Access Type: " + access_type + " Accessing: " + accessing 168 | 169 | with open(master_timeline, 'a') as ma: 170 | ma.write('|'.join(map(str, [timestamp, event_type, host, crossp_mast_desc]))+ "\n") 171 | 172 | elif event_type == "filemod": 173 | start_date = ddx.get('start_date').replace('T', ' ').replace('Z', '') 174 | action = ddx.get('fmAction') 175 | description = ddx.get('description').replace('PREPREPRE', '').replace('POSTPOSTPOST', '') 176 | writer_name = ddx.get('process_name').replace('PREPREPRE', '').replace('POSTPOSTPOST', '') 177 | file_path = ddx.get('pathWithMarker').replace('PREPREPRE', '').replace('POSTPOSTPOST', '') 178 | writer = ddx.get('path') 179 | 180 | with open(file_fm, 'a') as fm: 181 | fm.write('|'.join(map(str, [host, start_date, file_path, action, writer, writer_name, description]))+ "\n") 182 | 183 | #event_type, hostname, start, description, md5 184 | with open(master_timeline, 'a') as ma: 185 | fm_master = "Action: " + action + " File Path: " + file_path + " Writer: " + writer 186 | ma.write('|'.join(map(str, [start_date, event_type, host, fm_master]))+ "\n") 187 | 188 | elif event_type == "regmod": 189 | timestamp = ddx.get('start_date').replace('T', ' ').replace('Z', '') 190 | action = ddx.get('rmAction') 191 | reg_path = ddx.get('pathWithMarker').replace('PREPREPRE', '').replace('POSTPOSTPOST', '') 192 | proc_path = ddx.get('path') 193 | proc_name = ddx.get('process_name').replace('PREPREPRE', '').replace('POSTPOSTPOST', '') 194 | description = ddx.get('description').replace('PREPREPRE', '').replace('POSTPOSTPOST', '') 195 | 196 | ### add to only the specific event type 197 | with open(file_rm, 'a') as rm: 198 | rm.write('|'.join(map(str, [host, timestamp, action, reg_path, proc_name, proc_path, description]))+ "\n") 199 | 200 | #add to master timeline 201 | with open(master_timeline, 'a') as ma: 202 | reg_mastr = "Action: " +action+ " Path: " +reg_path + " Process Path: " + proc_path 203 | ma.write('|'.join(map(str, [timestamp, event_type, host, reg_mastr]))+ "\n") 204 | 205 | elif event_type == "netconn": 206 | timestamp = ddx.get('start').replace('T', ' ').replace('Z', '') 207 | process_name = ddx.get('process_name').replace('PREPREPRE', '').replace('POSTPOSTPOST', '') 208 | process_path = ddx.get('path').replace('PREPREPRE', '').replace('POSTPOSTPOST', '') 209 | dst_ip = ddx.get('address') 210 | dst_port = ddx.get('remotePort') 211 | dst_proto = ddx.get('protocol_str') 212 | conn_type = ddx.get('outbound') 213 | 214 | if conn_type == True: 215 | conn = "outbound" 216 | elif conn_type == False: 217 | conn = "Inbound" 218 | else: 219 | conn = "unknown" 220 | 221 | desc = ddx.get('description').replace('PREPREPRE', '').replace('POSTPOSTPOST', '') 222 | 223 | ## Write to single event section 224 | with open(file_nc, 'a') as nc: 225 | nc.write('|'.join(map(str, [host, timestamp, process_name, process_path, dst_ip, dst_port, dst_proto, conn, desc]))+ "\n") 226 | 227 | #Write to master timeline 228 | nc_master = ("DstSocket: " + str(dst_ip) + ":" + str(dst_port) + " Protocol: " + str(dst_proto) + " Type: " +str(conn_type)+ " Process Path:" + str(process_path)) 229 | with open(master_timeline, 'a') as ma: 230 | ma.write('|'.join(map(str, [timestamp, event_type, host, nc_master]))+ "\n") 231 | 232 | def main(): 233 | print('Carbon Black Response Timeline Builder \n ') 234 | print('1) Single Host Timeline') 235 | print('2) Master Timeline - "ALL TAGGED EVENTS" ') 236 | 237 | all_or_one = input("Selection:") 238 | 239 | if all_or_one == "1": 240 | hostname = input("Please enter the hostname or sensor ID:") 241 | hostExists = host_lookup(hostname) 242 | if hostExists.get("result") == 1: 243 | generate_timeline(hostname) 244 | else: 245 | print("Hostname not found.\n") 246 | main() 247 | 248 | elif all_or_one == "2": 249 | generate_timeline() 250 | 251 | else: 252 | print("Invalid choice.") 253 | main() 254 | 255 | if __name__ == '__main__': 256 | main() 257 | 258 | 259 | 260 | 261 | --------------------------------------------------------------------------------