skipped 5 lines 6 6 import sqlite3 7 7 from datetime import datetime, timezone 8 8 import time 9 - import os,sys,shutil 9 + import os,sys,shutil, traceback 10 10 from threading import Thread 11 11 import json 12 12 import uuid skipped 8 lines 21 21 con.execute('CREATE TABLE accesstokens (id INTEGER PRIMARY KEY AUTOINCREMENT, stored_at TEXT, issued_at TEXT, expires_at TEXT, description TEXT, user TEXT, resource TEXT, accesstoken TEXT)') 22 22 con.execute('CREATE TABLE refreshtokens (id INTEGER PRIMARY KEY AUTOINCREMENT, stored_at TEXT, description TEXT, user TEXT, tenant_id TEXT, resource TEXT, foci INTEGER, refreshtoken TEXT)') 23 23 con.execute('CREATE TABLE devicecodes (id INTEGER PRIMARY KEY AUTOINCREMENT, generated_at INTEGER, expires_at INTEGER, user_code TEXT, device_code TEXT, interval INTEGER, client_id TEXT, status TEXT, last_poll INTEGER)') 24 + con.execute('CREATE TABLE request_templates (id INTEGER PRIMARY KEY AUTOINCREMENT, template_name TEXT, uri TEXT, method TEXT, request_type TEXT, body TEXT, headers TEXT, variables TEXT)') 24 25 con.execute('CREATE TABLE settings (setting TEXT UNIQUE, value TEXT)') 25 26 # Valid Settings: active_access_token_id, active_refresh_token_id, schema_version 26 27 cur = con.cursor() 27 - cur.execute("INSERT INTO settings (setting, value) VALUES ('schema_version', '1 ')") 28 + cur.execute("INSERT INTO settings (setting, value) VALUES ('schema_version', '2 ')") 28 29 con.commit() 29 30 con.close() 30 31 skipped 40 lines 71 72 } for db_file in db_folder_content if db_file.is_file() and db_file.name.endswith(".db")] 72 73 return databases 73 74 75 + def update_db(): 76 + latest_schema_version = "2" 77 + current_schema_version = query_db("SELECT value FROM settings where setting = 'schema_version'",one=True)[0] 78 + if current_schema_version == "1": 79 + print("[*] Current database is schema version 1, updating to schema version 2") 80 + execute_db("CREATE TABLE request_templates (id INTEGER PRIMARY KEY AUTOINCREMENT, template_name TEXT, uri TEXT, method TEXT, request_type TEXT, body TEXT, headers TEXT, variables TEXT)") 81 + execute_db("UPDATE settings SET value = '2' WHERE setting = 'schema_version'") 82 + print("[*] Updated database to schema version 2") 83 + 74 84 # ========== Helper Functions ========== 75 85 76 86 def graph_request(graph_uri, access_token_id): skipped 5 lines 82 92 83 93 def graph_request_post(graph_uri, access_token_id, body): 84 94 access_token = query_db("SELECT accesstoken FROM accesstokens where id = ?",[access_token_id],one=True)[0] 85 - headers = {"Authorization":f"Bearer {access_token}"} 95 + headers = {"Authorization":f"Bearer {access_token}"} 86 96 response = requests.post(graph_uri, headers=headers, json=body) 87 97 resp_json = response.json() 88 98 return json.dumps(resp_json) 89 99 100 + def generic_request(uri, access_token_id, method, request_type, body, headers): 101 + access_token = query_db("SELECT accesstoken FROM accesstokens where id = ?",[access_token_id],one=True)[0] 102 + headers["Authorization"] = f"Bearer {access_token}" 103 + 104 + # Empty body 105 + if not body: 106 + response = requests.request(method, uri, headers=headers) 107 + # Text, XML or urlencoded request 108 + elif request_type in ["text", "urlencoded", "xml"]: 109 + if request_type == "urlencoded" and not "Content-Type" in headers: 110 + headers["Content-Type"] = "application/x-www-form-urlencoded" 111 + if request_type == "xml" and not "Content-Type" in headers: 112 + headers["Content-Type"] = "application/xml" 113 + response = requests.request(method, uri, headers=headers, data=body) 114 + # Json request 115 + elif request_type == "json": 116 + try: 117 + response = requests.request(method, uri, headers=headers, json=json.loads(body)) 118 + except ValueError as e: 119 + return f"[Error] The body message does not contain valid JSON, but a body type of JSON was specified.", 400 120 + else: 121 + return f"[Error] Invalid request type.", 400 122 + 123 + # Format json if the Content-Type contains json 124 + response_type = "json" if ("Content-Type" in response.headers and "json" in response.headers["Content-Type"]) else "xml" if ("Content-Type" in response.headers and "xml" in response.headers["Content-Type"]) else "text" 125 + try: 126 + response_text = json.dumps(response.json()) if response_type == "json" else response.text 127 + except ValueError as e: 128 + response_text = response.text 129 + return {"response_status_code": response.status_code ,"response_type": response_type ,"response_text": response_text} 130 + 90 131 def save_access_token(accesstoken, description): 91 132 decoded_accesstoken = jwt.decode(accesstoken, options={"verify_signature": False}) 92 133 user = "unknown" skipped 201 lines 294 335 def device_codes(): 295 336 return render_template('device_codes.html', title="Device Codes") 296 337 297 - @app.route("/graph_requests ") 298 - def graph_requests(): 299 - return render_template('requests .html', title="Graph Requests") 338 + @app.route("/custom_requests ") 339 + def custom_requests(): 340 + return render_template('custom_requests .html', title="Custom Requests") 300 341 301 342 @app.route("/generic_search") 302 343 def generic_search(): 303 - return render_template('generic_search.html', title="Generic Search") 344 + return render_template('generic_search.html', title="Generic MSGraph Search") 304 345 305 346 @app.route("/recent_files") 306 347 def recent_files(): skipped 159 lines 466 507 active_access_token = query_db("SELECT value FROM settings WHERE setting = 'active_access_token_id'",one=True) 467 508 return f"{active_access_token[0]}" if active_access_token else "0" 468 509 469 - # ========== Graph Requests ========== 510 + # ========== Generic Requests ========== 470 511 471 512 @app.post("/api/generic_graph") 472 513 def api_generic_graph(): skipped 10 lines 483 524 graph_response = graph_request_post(graph_uri, access_token_id, body) 484 525 return graph_response 485 526 527 + @app.post("/api/custom_api_request") 528 + def api_custom_api_request(): 529 + if not request.is_json: 530 + return f"[Error] Expecting JSON input.", 400 531 + request_json = request.get_json() 532 + print(request_json) 533 + uri = request_json['uri'] if 'uri' in request_json else '' 534 + access_token_id = request_json['access_token_id'] if 'access_token_id' in request_json else 0 535 + method = request_json['method'] if 'method' in request_json else 'GET' 536 + request_type = request_json['request_type'] if 'request_type' in request_json else 'text' 537 + body = request_json['body'] if 'body' in request_json else '' 538 + headers = request_json['headers'] if 'headers' in request_json else {} 539 + variables = request_json['variables'] if 'variables' in request_json else {} 540 + 541 + if not (uri and access_token_id and method): 542 + return f"[Error] URI, Access Token ID and Method are mandatory!", 400 543 + elif request_type not in ["text", "json", "urlencoded", "xml"]: 544 + return f"[Error] Invalid request type '{request_type}'. Should be one of the following values: text, json, urlencoded, xml", 400 545 + elif type(headers) != dict or type(variables) != dict: 546 + return f"[Error] Expecting json input for headers and variables. Received '{type(headers)}' and '{type(variables)}' respectively.", 400 547 + 548 + for variable_name, variable_value in variables.items(): 549 + uri = uri.replace(variable_name, variable_value) 550 + body = body.replace(variable_name, variable_value) 551 + temp_headers = {} 552 + for header_name, header_value in headers.items(): 553 + new_header_name = header_name.replace(variable_name, variable_value) if type(header_name) == str else header_name 554 + new_header_value = header_value.replace(variable_name, variable_value) if type(header_value) == str else header_value 555 + temp_headers[new_header_name] =new_header_value 556 + headers = temp_headers 557 + try: 558 + api_response = generic_request(uri, access_token_id, method, request_type, body, headers) 559 + except Exception as e: 560 + traceback.print_exc() 561 + return f"[Error] Unexpected error occurred. Check your input for any issues. Exception: {repr(e)}", 400 562 + return api_response 563 + 564 + @app.post("/api/save_request_template") 565 + def api_save_request_template(): 566 + if not request.is_json: 567 + return f"[Error] Expecting JSON input.", 400 568 + request_json = request.get_json() 569 + print(request_json) 570 + template_name = request_json['template_name'] if 'template_name' in request_json else '' 571 + uri = request_json['uri'] if 'uri' in request_json else '' 572 + method = request_json['method'] if 'method' in request_json else 'GET' 573 + request_type = request_json['request_type'] if 'request_type' in request_json else 'text' 574 + body = request_json['body'] if 'body' in request_json else '' 575 + headers = request_json['headers'] if 'headers' in request_json else {} 576 + variables = request_json['variables'] if 'variables' in request_json else {} 577 + 578 + if not (template_name and uri and method): 579 + return f"[Error] Template Name, URI and Method are mandatory!", 400 580 + elif request_type not in ["text", "json", "urlencoded", "xml"]: 581 + return f"[Error] Invalid request type '{request_type}'. Should be one of the following values: text, json, urlencoded, xml", 400 582 + elif type(headers) != dict or type(variables) != dict: 583 + return f"[Error] Expecting json input for headers and variables. Received '{type(headers)}' and '{type(variables)}' respectively.", 400 584 + 585 + template_exists = False 586 + try: 587 + # If a request template with the same name already exists, delete it first 588 + existing_request_template = query_db_json("SELECT * FROM request_templates WHERE template_name = ?",[template_name],one=True) 589 + if existing_request_template: 590 + template_exists = True 591 + execute_db("DELETE FROM request_templates where id = ?",[existing_request_template["id"]]) 592 + # Save the new request template 593 + execute_db("INSERT INTO request_templates (template_name, uri, method, request_type, body, headers, variables) VALUES (?,?,?,?,?,?,?)",( 594 + template_name, 595 + uri, 596 + method, 597 + request_type, 598 + body, 599 + json.dumps(headers), 600 + json.dumps(variables) 601 + ) 602 + ) 603 + except Exception as e: 604 + traceback.print_exc() 605 + return f"[Error] Unexpected error occurred. Check your input for any issues. Exception: {repr(e)}", 400 606 + if template_exists: 607 + return f"[Success] Updated configuration for request template '{template_name}'." 608 + return f"[Success] Saved template '{template_name}' to database." 609 + 610 + @app.route("/api/get_request_templates/<template_id>") 611 + def api_request_templates(template_id): 612 + request_template = query_db_json("SELECT * FROM request_templates WHERE id = ?",[template_id],one=True) 613 + if request_template: 614 + request_template['headers'] = json.loads(request_template['headers']) 615 + request_template['variables'] = json.loads(request_template['variables']) 616 + if not request_template: 617 + return f"[Error] Unable to find request template with ID '{template_id}'.", 400 618 + return request_template 619 + 620 + @app.route("/api/list_request_templates") 621 + def api_list_request_templates(): 622 + request_templates = query_db_json("SELECT * FROM request_templates") 623 + print(request_templates) 624 + for i in range(len(request_templates)): 625 + request_templates[i]['headers'] = json.loads( request_templates[i]['headers']) 626 + request_templates[i]['variables'] = json.loads(request_templates[i]['variables']) 627 + return request_templates 628 + 629 + @app.post("/api/delete_request_template") 630 + def api_delete_request_template(): 631 + if not "template_id" in request.form: 632 + return f"[Error] No template_id specified.", 400 633 + template_id = request.form['template_id'] 634 + existing_request_template = query_db_json("SELECT * FROM request_templates WHERE id = ?",[template_id],one=True) 635 + if not existing_request_template: 636 + return f"[Error] Unable to find request template with ID '{template_id}'.", 400 637 + execute_db("DELETE FROM request_templates where id = ?",[template_id]) 638 + return f"[Success] Deleted request template '{existing_request_template['template_name']}' from database." 639 + 640 + 486 641 # ========== Database ========== 487 642 488 643 @app.get("/api/list_databases") skipped 26 lines 515 670 if(not os.path.exists(db_path)): 516 671 return f"[Error] Database file '{db_path}' not found." 517 672 app.config['graph_spy_db_path'] = db_path 673 + update_db() 518 674 return f"[Success] Activated database '{database_name}'." 519 675 520 676 @app.post("/api/duplicate_database") skipped 103 lines 624 780 if(not os.path.exists(graph_spy_db_path)): 625 781 sys.exit(f"Failed creating database file at '{graph_spy_db_path}'. Unable to proceed.") 626 782 print(f"[*] Utilizing database '{graph_spy_db_path}'.") 627 - 783 + # Update the database to the latest schema version if required 784 + with app.app_context(): 785 + update_db() 628 786 # Disable datatable error messages by default. 629 787 app.config['table_error_messages'] = "disabled" 630 - 631 788 # Run flask 632 789 print(f"[*] Starting GraphSpy. Open in your browser by going to the url displayed below.\n") 633 790 app.run(debug=args.debug, host=args.interface, port=args.port) skipped 3 lines