Projects STRLCPY GraphSpy Commits d5cd3158
🤬
  • ■ ■ ■ ■ ■ ■
    .gitattributes
     1 +###############################################################################
     2 +# Set default behavior to automatically normalize line endings.
     3 +###############################################################################
     4 +* text=auto
     5 + 
     6 +###############################################################################
     7 +# Set default behavior for command prompt diff.
     8 +#
     9 +# This is need for earlier builds of msysgit that does not have it on by
     10 +# default for csharp files.
     11 +# Note: This is only used by command line
     12 +###############################################################################
     13 +#*.cs diff=csharp
     14 + 
     15 +###############################################################################
     16 +# Set the merge driver for project and solution files
     17 +#
     18 +# Merging from the command prompt will add diff markers to the files if there
     19 +# are conflicts (Merging from VS is not affected by the settings below, in VS
     20 +# the diff markers are never inserted). Diff markers may cause the following
     21 +# file extensions to fail to load in VS. An alternative would be to treat
     22 +# these files as binary and thus will always conflict and require user
     23 +# intervention with every merge. To do so, just uncomment the entries below
     24 +###############################################################################
     25 +#*.sln merge=binary
     26 +#*.csproj merge=binary
     27 +#*.vbproj merge=binary
     28 +#*.vcxproj merge=binary
     29 +#*.vcproj merge=binary
     30 +#*.dbproj merge=binary
     31 +#*.fsproj merge=binary
     32 +#*.lsproj merge=binary
     33 +#*.wixproj merge=binary
     34 +#*.modelproj merge=binary
     35 +#*.sqlproj merge=binary
     36 +#*.wwaproj merge=binary
     37 + 
     38 +###############################################################################
     39 +# behavior for image files
     40 +#
     41 +# image files are treated as binary by default.
     42 +###############################################################################
     43 +#*.jpg binary
     44 +#*.png binary
     45 +#*.gif binary
     46 + 
     47 +###############################################################################
     48 +# diff behavior for common document formats
     49 +#
     50 +# Convert binary document formats to text before diffing them. This feature
     51 +# is only available from the command line. Turn it on by uncommenting the
     52 +# entries below.
     53 +###############################################################################
     54 +#*.doc diff=astextplain
     55 +#*.DOC diff=astextplain
     56 +#*.docx diff=astextplain
     57 +#*.DOCX diff=astextplain
     58 +#*.dot diff=astextplain
     59 +#*.DOT diff=astextplain
     60 +#*.pdf diff=astextplain
     61 +#*.PDF diff=astextplain
     62 +#*.rtf diff=astextplain
     63 +#*.RTF diff=astextplain
     64 + 
  • ■ ■ ■ ■ ■ ■
    .gitignore
     1 +__pycache__/
     2 +.vs/
     3 +dist/
  • ■ ■ ■ ■ ■ ■
    GraphSpy/GraphSpy.py
     1 +#!/usr/bin/env python3
     2 +from flask import Flask,render_template,request,g,redirect
     3 +import flask.helpers
     4 +import requests
     5 +import jwt
     6 +import sqlite3
     7 +from datetime import datetime, timezone
     8 +import time
     9 +import os,sys,shutil
     10 +from threading import Thread
     11 +import json
     12 +import uuid
     13 + 
     14 +with open(os.path.join(os.path.dirname(os.path.abspath(__file__)),"version.txt")) as f:
     15 + __version__ = f.read()
     16 + 
     17 +# ========== Database ==========
     18 + 
     19 +def init_db():
     20 + con = sqlite3.connect(app.config['graph_spy_db_path'])
     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 + 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 + 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 settings (setting TEXT UNIQUE, value TEXT)')
     25 + # Valid Settings: active_access_token_id, active_refresh_token_id, schema_version
     26 + cur = con.cursor()
     27 + cur.execute("INSERT INTO settings (setting, value) VALUES ('schema_version', '1')")
     28 + con.commit()
     29 + con.close()
     30 + 
     31 +def get_db():
     32 + db = getattr(g, '_database', None)
     33 + if db is None:
     34 + db = g._database = sqlite3.connect(app.config['graph_spy_db_path'])
     35 + return db
     36 + 
     37 +def query_db(query, args=(), one=False):
     38 + con = get_db()
     39 + con.row_factory = sqlite3.Row
     40 + cur = con.execute(query, args)
     41 + rv = cur.fetchall()
     42 + cur.close()
     43 + return (rv[0] if rv else None) if one else rv
     44 +
     45 +def query_db_json(query, args=(), one=False):
     46 + con = get_db()
     47 + con.row_factory = make_dicts
     48 + cur = con.execute(query, args)
     49 + rv = cur.fetchall()
     50 + cur.close()
     51 + return (rv[0] if rv else None) if one else rv
     52 +
     53 +def execute_db(statement, args=()):
     54 + con = get_db()
     55 + cur = con.cursor()
     56 + cur.execute(statement, args)
     57 + con.commit()
     58 +
     59 +def make_dicts(cursor, row):
     60 + return dict((cursor.description[idx][0], value)
     61 + for idx, value in enumerate(row))
     62 +
     63 +def list_databases():
     64 + db_folder_content = os.scandir(app.config['graph_spy_db_folder'])
     65 + databases = [
     66 + {
     67 + 'name': db_file.name,
     68 + 'last_modified': f"{datetime.fromtimestamp(db_file.stat().st_mtime)}".split(".")[0],
     69 + 'size': f"{round(db_file.stat().st_size/1024)} KB",
     70 + 'state': "Active" if db_file.name.lower() == os.path.basename(app.config['graph_spy_db_path']).lower() else "Inactive"
     71 + } for db_file in db_folder_content if db_file.is_file() and db_file.name.endswith(".db")]
     72 + return databases
     73 + 
     74 +# ========== Helper Functions ==========
     75 + 
     76 +def graph_request(graph_uri, access_token_id):
     77 + access_token = query_db("SELECT accesstoken FROM accesstokens where id = ?",[access_token_id],one=True)[0]
     78 + headers = {"Authorization":f"Bearer {access_token}"}
     79 + response = requests.get(graph_uri, headers=headers)
     80 + resp_json = response.json()
     81 + return json.dumps(resp_json)
     82 + 
     83 +def graph_request_post(graph_uri, access_token_id, body):
     84 + access_token = query_db("SELECT accesstoken FROM accesstokens where id = ?",[access_token_id],one=True)[0]
     85 + headers = {"Authorization":f"Bearer {access_token}"}
     86 + response = requests.post(graph_uri, headers=headers, json=body)
     87 + resp_json = response.json()
     88 + return json.dumps(resp_json)
     89 +
     90 +def save_access_token(accesstoken, description):
     91 + decoded_accesstoken = jwt.decode(accesstoken, options={"verify_signature": False})
     92 +
     93 + execute_db("INSERT INTO accesstokens (stored_at, issued_at, expires_at, description, user, resource, accesstoken) VALUES (?,?,?,?,?,?,?)",(
     94 + f"{datetime.now()}".split(".")[0],
     95 + datetime.fromtimestamp(decoded_accesstoken["iat"]),
     96 + datetime.fromtimestamp(decoded_accesstoken["exp"]),
     97 + description,
     98 + decoded_accesstoken["unique_name"],
     99 + decoded_accesstoken["aud"],
     100 + accesstoken
     101 + )
     102 + )
     103 +
     104 +def save_refresh_token(refreshtoken, description, user, tenant, resource, foci):
     105 + # Used to convert potential boolean inputs to an integer, as the DB uses an integer to store this value
     106 + foci_int = 1 if foci else 0
     107 + tenant_id = tenant.strip('"{}-[]\\/\' ') if is_valid_uuid(tenant.strip('"{}-[]\\/\' ')) else get_tenant_id(tenant)
     108 + execute_db("INSERT INTO refreshtokens (stored_at, description, user, tenant_id, resource, foci, refreshtoken) VALUES (?,?,?,?,?,?,?)",(
     109 + f"{datetime.now()}".split(".")[0],
     110 + description,
     111 + user,
     112 + tenant_id,
     113 + resource,
     114 + foci_int,
     115 + refreshtoken
     116 + )
     117 + )
     118 + 
     119 +def is_valid_uuid(val):
     120 + try:
     121 + uuid.UUID(str(val))
     122 + return True
     123 + except ValueError:
     124 + return False
     125 + 
     126 +def get_tenant_id(tenant_domain):
     127 + response = requests.get(f"https://login.microsoftonline.com/{tenant_domain}/.well-known/openid-configuration")
     128 + resp_json = response.json()
     129 + tenant_id = resp_json["authorization_endpoint"].split("/")[3]
     130 + return tenant_id
     131 + 
     132 +def refresh_to_access_token(refresh_token_id, resource = "defined_in_token", client_id = "d3590ed6-52b3-4102-aeff-aad2292ab01c", store_refresh_token = True):
     133 + refresh_token = query_db("SELECT refreshtoken FROM refreshtokens where id = ?",[refresh_token_id],one=True)[0]
     134 + tenant_id = query_db("SELECT tenant_id FROM refreshtokens where id = ?",[refresh_token_id],one=True)[0]
     135 + resource = query_db("SELECT resource FROM refreshtokens where id = ?",[refresh_token_id],one=True)[0] if resource == "defined_in_token" else resource
     136 + body = {
     137 + "resource": resource,
     138 + "client_id": client_id,
     139 + "grant_type": "refresh_token",
     140 + "refresh_token": refresh_token,
     141 + "scope": "openid"
     142 + }
     143 + url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/token?api-version=1.0"
     144 + response = requests.post(url, data=body)
     145 + access_token = response.json()["access_token"]
     146 + save_access_token(access_token, f"Created using refresh token {refresh_token_id}")
     147 + access_token_id = query_db("SELECT id FROM accesstokens where accesstoken = ?",[access_token],one=True)[0]
     148 + if store_refresh_token:
     149 + decoded_accesstoken = jwt.decode(access_token, options={"verify_signature": False})
     150 + save_refresh_token(
     151 + response.json()["refresh_token"],
     152 + f"Created using refresh token {refresh_token_id}",
     153 + decoded_accesstoken["unique_name"],
     154 + tenant_id,
     155 + response.json()["resource"],
     156 + response.json()["foci"]
     157 + )
     158 + return access_token_id
     159 + 
     160 +def generate_device_code(resource = "https://graph.microsoft.com", client_id = "d3590ed6-52b3-4102-aeff-aad2292ab01c"):
     161 + body = {
     162 + "resource": resource,
     163 + "client_id": client_id
     164 + }
     165 + url = "https://login.microsoftonline.com/common/oauth2/devicecode?api-version=1.0"
     166 + response = requests.post(url, data=body)
     167 + 
     168 + execute_db("INSERT INTO devicecodes (generated_at, expires_at, user_code, device_code, interval, client_id, status, last_poll) VALUES (?,?,?,?,?,?,?,?)",(
     169 + int(datetime.now().timestamp()),
     170 + int(datetime.now().timestamp()) + int(response.json()["expires_in"]),
     171 + response.json()["user_code"],
     172 + response.json()["device_code"],
     173 + int(response.json()["interval"]),
     174 + client_id,
     175 + "CREATED",
     176 + 0
     177 + )
     178 + )
     179 + return response.json()["device_code"]
     180 + 
     181 +def poll_device_codes():
     182 + with app.app_context():
     183 + while True:
     184 + rows = query_db_json("SELECT * FROM devicecodes WHERE status IN ('CREATED','POLLING')")
     185 + if not rows:
     186 + return
     187 + sorted_rows = sorted(rows, key=lambda x: x["last_poll"])
     188 + for row in sorted_rows:
     189 + current_time_seconds = int(datetime.now().timestamp())
     190 + if current_time_seconds > row["expires_at"]:
     191 + execute_db("UPDATE devicecodes SET status = ? WHERE device_code = ?",("EXPIRED",row["device_code"]))
     192 + continue
     193 + next_poll = row["last_poll"] + row["interval"]
     194 + #print(f"[{current_time_seconds}] {row['user_code']} - {row['last_poll']} - {next_poll}", flush=True)
     195 + if current_time_seconds < next_poll:
     196 + time.sleep(next_poll - current_time_seconds)
     197 + if row["status"] == "CREATED":
     198 + execute_db("UPDATE devicecodes SET status = ? WHERE device_code = ?",("POLLING",row["device_code"]))
     199 + body = {
     200 + "client_id": row["client_id"],
     201 + "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
     202 + "code": row["device_code"]
     203 + }
     204 + url = "https://login.microsoftonline.com/Common/oauth2/token?api-version=1.0"
     205 + response = requests.post(url, data=body)
     206 + execute_db("UPDATE devicecodes SET last_poll = ? WHERE device_code = ?",(int(datetime.now().timestamp()),row["device_code"]))
     207 + if response.status_code == 200 and "access_token" in response.json():
     208 + access_token = response.json()["access_token"]
     209 + user_code = row["user_code"]
     210 + save_access_token(access_token, f"Created using device code auth ({user_code})")
     211 + decoded_accesstoken = jwt.decode(access_token, options={"verify_signature": False})
     212 + save_refresh_token(
     213 + response.json()["refresh_token"],
     214 + f"Created using device code auth ({user_code})",
     215 + decoded_accesstoken["unique_name"],
     216 + decoded_accesstoken["tid"],
     217 + response.json()["resource"],
     218 + int(response.json()["foci"]))
     219 + execute_db("UPDATE devicecodes SET status = ? WHERE device_code = ?",("SUCCESS",row["device_code"]))
     220 + 
     221 +def start_device_code_thread():
     222 + if "device_code_thread" in app.config:
     223 + if app.config["device_code_thread"].is_alive():
     224 + return "[Error] Device Code polling thread is still running."
     225 + app.config["device_code_thread"] = Thread(target=poll_device_codes)
     226 + app.config["device_code_thread"].start()
     227 + return "[Success] Started device code polling thread."
     228 + 
     229 +def device_code_flow(resource = "https://graph.microsoft.com", client_id = "d3590ed6-52b3-4102-aeff-aad2292ab01c"):
     230 + device_code = generate_device_code(resource, client_id)
     231 + row = query_db_json("SELECT * FROM devicecodes WHERE device_code = ?",[device_code],one=True)
     232 + user_code = row["user_code"]
     233 + start_device_code_thread()
     234 + return user_code
     235 + 
     236 +def safe_join(directory, filename):
     237 + # Safely join `directory` and `filename`.
     238 + os_seps = list(sep for sep in [os.path.sep, os.path.altsep] if sep != None)
     239 + filename = os.path.normpath(filename)
     240 + for sep in os_seps:
     241 + if sep in filename:
     242 + return False
     243 + if os.path.isabs(filename) or filename.startswith('../'):
     244 + return False
     245 + if not os.path.normpath(os.path.join(directory,filename)).startswith(directory):
     246 + return False
     247 + return os.path.join(directory, filename)
     248 + 
     249 +def init_routes():
     250 + 
     251 + # ========== Pages ==========
     252 + 
     253 + @app.route("/")
     254 + def settings():
     255 + return render_template('settings.html', title="Settings")
     256 + 
     257 + @app.route("/access_tokens")
     258 + def access_tokens():
     259 + return render_template('access_tokens.html', title="Access Tokens")
     260 + 
     261 + @app.route("/refresh_tokens")
     262 + def refresh_tokens():
     263 + return render_template('refresh_tokens.html', title="Refresh Tokens")
     264 + 
     265 + @app.route("/device_codes")
     266 + def device_codes():
     267 + return render_template('device_codes.html', title="Device Codes")
     268 + 
     269 + @app.route("/graph_requests")
     270 + def graph_requests():
     271 + return render_template('requests.html', title="Graph Requests")
     272 + 
     273 + @app.route("/generic_search")
     274 + def generic_search():
     275 + return render_template('generic_search.html', title="Generic Search")
     276 + 
     277 + @app.route("/recent_files")
     278 + def recent_files():
     279 + return render_template('recent_files.html', title="Recent Files")
     280 + 
     281 + @app.route("/shared_with_me")
     282 + def shared_with_me():
     283 + return render_template('shared_with_me.html', title="Files Shared With Me")
     284 + 
     285 + @app.route("/onedrive")
     286 + def onedrive():
     287 + return render_template('OneDrive.html', title="OneDrive")
     288 + 
     289 + @app.route("/sharepoint_sites")
     290 + def sharepoint_sites():
     291 + return render_template('SharePointSites.html', title="SharePoint Sites")
     292 +
     293 + @app.route("/sharepoint_drives")
     294 + def sharepoint_drives():
     295 + return render_template('SharePointDrives.html', title="SharePoint Drives")
     296 +
     297 + @app.route("/sharepoint")
     298 + def sharepoint():
     299 + return render_template('SharePoint.html', title="SharePoint")
     300 + 
     301 + @app.route("/outlook")
     302 + def outlook():
     303 + return render_template('outlook.html', title="Outlook")
     304 + 
     305 + # ========== API ==========
     306 + 
     307 + # ========== Device Codes ==========
     308 + 
     309 + @app.route("/api/list_device_codes")
     310 + def api_list_device_codes():
     311 + rows = query_db_json("select * from devicecodes")
     312 + # Convert unix timestamps to formated datetime strings before returning
     313 + [row.update(generated_at=f"{datetime.fromtimestamp(row['generated_at'])}") for row in rows]
     314 + [row.update(expires_at=f"{datetime.fromtimestamp(row['expires_at'])}") for row in rows]
     315 + [row.update(last_poll=f"{datetime.fromtimestamp(row['last_poll'])}") for row in rows]
     316 + return json.dumps(rows)
     317 + 
     318 + @app.post("/api/restart_device_code_polling")
     319 + def api_restart_device_code_polling():
     320 + return start_device_code_thread()
     321 + 
     322 + @app.post('/api/generate_device_code')
     323 + def api_generate_device_code():
     324 + resource = request.form['resource'] if "resource" in request.form else ""
     325 + client_id = request.form['client_id'] if "client_id" in request.form else ""
     326 + if resource and client_id:
     327 + user_code = device_code_flow(resource, client_id)
     328 + return user_code
     329 + 
     330 + @app.route("/api/delete_device_code/<id>")
     331 + def api_delete_device_code(id):
     332 + execute_db("DELETE FROM devicecodes where id = ?",[id])
     333 + return "true"
     334 +
     335 + # ========== Refresh Tokens ==========
     336 + 
     337 + @app.route("/api/list_refresh_tokens")
     338 + def api_list_refresh_tokens():
     339 + rows = query_db_json("select * from refreshtokens")
     340 + return json.dumps(rows)
     341 + 
     342 + @app.route("/api/get_refresh_token/<id>")
     343 + def api_get_refresh_token(id):
     344 + rows = query_db_json("select * from refreshtokens WHERE id = ?",[id],one=True)
     345 + return json.dumps(rows)
     346 + 
     347 + @app.post('/api/add_refresh_token')
     348 + def api_add_refresh_token():
     349 + refreshtoken = request.form['refreshtoken'] if "refreshtoken" in request.form else ""
     350 + user = request.form['user'] if "user" in request.form else ""
     351 + tenant = request.form['tenant_domain'] if "tenant_domain" in request.form else ""
     352 + resource = request.form['resource'] if "resource" in request.form else ""
     353 + description = request.form['description'] if "description" in request.form else ""
     354 + foci = 1 if "foci" in request.form else 0
     355 + if refreshtoken and tenant and resource:
     356 + save_refresh_token(refreshtoken, description, user, tenant, resource, foci)
     357 + return redirect('/refresh_tokens')
     358 + 
     359 + @app.post('/api/refresh_to_access_token')
     360 + def api_refresh_to_access_token():
     361 + refresh_token_id = request.form['refresh_token_id'] if "refresh_token_id" in request.form else ""
     362 + resource = request.form['resource'] if "resource" in request.form else ""
     363 + resource = resource if resource else "defined_in_token"
     364 + client_id = request.form['client_id'] if "client_id" in request.form else ""
     365 + client_id = client_id if client_id else "d3590ed6-52b3-4102-aeff-aad2292ab01c"
     366 + store_refresh_token = True if "store_refresh_token" in request.form else False
     367 + access_token_id = 0
     368 + if refresh_token_id and resource and client_id:
     369 + access_token_id = refresh_to_access_token(refresh_token_id, resource, client_id, store_refresh_token)
     370 + return f"{access_token_id}"
     371 + 
     372 + @app.route("/api/delete_refresh_token/<id>")
     373 + def api_delete_refresh_token(id):
     374 + execute_db("DELETE FROM refreshtokens where id = ?",[id])
     375 + return "true"
     376 + 
     377 + @app.route("/api/active_refresh_token/<id>")
     378 + def api_set_active_refresh_token(id):
     379 + previous_id = query_db("SELECT value FROM settings WHERE setting = 'active_refresh_token_id'",one=True)
     380 + if not previous_id:
     381 + execute_db("INSERT INTO settings (setting, value) VALUES ('active_refresh_token_id',?)",(id,))
     382 + else:
     383 + execute_db("UPDATE settings SET value = ? WHERE setting = 'active_refresh_token_id'",(id,))
     384 + return id
     385 + 
     386 + @app.route("/api/active_refresh_token")
     387 + def api_get_active_refresh_token():
     388 + active_refresh_token = query_db("SELECT value FROM settings WHERE setting = 'active_refresh_token_id'",one=True)
     389 + return f"{active_refresh_token[0]}" if active_refresh_token else "0"
     390 + 
     391 + # ========== Access Tokens ==========
     392 + 
     393 + @app.route("/api/list_access_tokens")
     394 + def api_list_access_tokens():
     395 + rows = query_db_json("select * from accesstokens")
     396 + return json.dumps(rows)
     397 +
     398 + @app.post('/api/add_access_token')
     399 + def api_add_access_token():
     400 + accesstoken = request.form['accesstoken']
     401 + description = request.form['description']
     402 + save_access_token(accesstoken, description)
     403 + return redirect('/access_tokens')
     404 + 
     405 + @app.route("/api/get_access_token/<id>")
     406 + def api_get_access_token(id):
     407 + rows = query_db_json("select * from accesstokens WHERE id = ?",[id],one=True)
     408 + return json.dumps(rows)
     409 + 
     410 + @app.route("/api/decode_token/<id>")
     411 + def api_decode_token(id):
     412 + rows = query_db("SELECT accesstoken FROM accesstokens WHERE id = ?",[id],one=True)
     413 + if rows:
     414 + decoded_accesstoken = jwt.decode(rows[0], options={"verify_signature": False})
     415 + return decoded_accesstoken
     416 + else:
     417 + return f"[Error] Could not find access token with id {id}"
     418 + 
     419 + @app.route("/api/delete_access_token/<id>")
     420 + def api_delete_access_token(id):
     421 + execute_db("DELETE FROM accesstokens WHERE id = ?",[id])
     422 + return "true"
     423 + 
     424 + @app.route("/api/active_access_token/<id>")
     425 + def api_set_active_access_token(id):
     426 + previous_id = query_db("SELECT value FROM settings WHERE setting = 'active_access_token_id'",one=True)
     427 + if not previous_id:
     428 + execute_db("INSERT INTO settings (setting, value) VALUES ('active_access_token_id',?)",(id,))
     429 + else:
     430 + execute_db("UPDATE settings SET value = ? WHERE setting = 'active_access_token_id'",(id,))
     431 + return id
     432 + 
     433 + @app.route("/api/active_access_token")
     434 + def api_get_active_access_token():
     435 + active_access_token = query_db("SELECT value FROM settings WHERE setting = 'active_access_token_id'",one=True)
     436 + return f"{active_access_token[0]}" if active_access_token else "0"
     437 +
     438 + # ========== Graph Requests ==========
     439 + 
     440 + @app.post("/api/generic_graph")
     441 + def api_generic_graph():
     442 + graph_uri = request.form['graph_uri']
     443 + access_token_id = request.form['access_token_id']
     444 + graph_response = graph_request(graph_uri, access_token_id)
     445 + return graph_response
     446 +
     447 + @app.post("/api/generic_graph_post")
     448 + def api_generic_graph_post():
     449 + graph_uri = request.form['graph_uri']
     450 + access_token_id = request.form['access_token_id']
     451 + body = json.loads(request.form['body'])
     452 + graph_response = graph_request_post(graph_uri, access_token_id, body)
     453 + return graph_response
     454 +
     455 + # ========== Database ==========
     456 +
     457 + @app.get("/api/list_databases")
     458 + def api_list_databases():
     459 + return list_databases()
     460 + 
     461 + @app.post("/api/create_database")
     462 + def api_create_database():
     463 + database_name = request.form['database']
     464 + if not database_name:
     465 + return f"[Error] Please specify a database name."
     466 + database_name = database_name if database_name.endswith(".db") else f"{database_name}.db"
     467 + db_path = safe_join(app.config['graph_spy_db_folder'],database_name)
     468 + if not db_path:
     469 + return f"[Error] Invalid database name '{database_name}'. Try again with another name."
     470 + if(os.path.exists(db_path)):
     471 + return f"[Error] Database '{database_name}' already exists. Try again with another name."
     472 + old_db = app.config['graph_spy_db_path']
     473 + app.config['graph_spy_db_path'] = db_path
     474 + init_db()
     475 + if(not os.path.exists(db_path)):
     476 + app.config['graph_spy_db_path'] = old_db
     477 + return f"[Error] Failed to create database '{database_name}'."
     478 + return f"[Success] Created and activated '{database_name}'."
     479 +
     480 + @app.post("/api/activate_database")
     481 + def api_activate_database():
     482 + database_name = request.form['database']
     483 + db_path = safe_join(app.config['graph_spy_db_folder'],database_name)
     484 + if(not os.path.exists(db_path)):
     485 + return f"[Error] Database file '{db_path}' not found."
     486 + app.config['graph_spy_db_path'] = db_path
     487 + return f"[Success] Activated database '{database_name}'."
     488 +
     489 + @app.post("/api/duplicate_database")
     490 + def api_duplicate_database():
     491 + database_name = request.form['database']
     492 + db_path = safe_join(app.config['graph_spy_db_folder'],database_name)
     493 + if(not os.path.exists(db_path)):
     494 + return f"[Error] Database file '{db_path}' not found."
     495 + for i in range(1,100):
     496 + new_path = f"{db_path.strip('.db')}_{i}.db"
     497 + if(not os.path.exists(new_path)):
     498 + shutil.copy2(db_path, new_path)
     499 + return f"[Success] Duplicated database '{database_name}' to '{new_path.split('/')[-1]}'."
     500 + return f"[Error] Could not duplicate database '{database_name}'."
     501 + 
     502 + @app.post("/api/delete_database")
     503 + def api_delete_database():
     504 + database_name = request.form['database']
     505 + db_path = safe_join(app.config['graph_spy_db_folder'],database_name)
     506 + if app.config['graph_spy_db_path'].lower() == db_path.lower():
     507 + return "[Error] Can't delete the active database. Select a different database first."
     508 + os.remove(db_path)
     509 + if(not os.path.exists(db_path)):
     510 + return f"[Success] Database '{database_name}' deleted."
     511 + else:
     512 + return f"[Error] Failed to delete '{database_name}' at '{db_path}'."
     513 +
     514 + # ========== Settings ==========
     515 +
     516 + @app.post("/api/set_table_error_messages")
     517 + def api_set_table_error_messages():
     518 + state = request.form['state']
     519 + if state not in ["enabled", "disabled"]:
     520 + return f"[Error] Invalid state '{state}'."
     521 + app.config['table_error_messages'] = state
     522 + return f"[Success] {state.capitalize()} datatable error messages."
     523 +
     524 + @app.get("/api/get_settings")
     525 + def api_get_settings():
     526 + settings_raw = query_db_json("SELECT * FROM settings")
     527 + #settings_json = [{setting["setting"] : setting["value"]} for setting in settings_raw]
     528 + settings_json = {setting["setting"] : setting["value"] for setting in settings_raw}
     529 + return settings_json
     530 + 
     531 + # ========== Other ==========
     532 + 
     533 + @app.teardown_appcontext
     534 + def close_connection(exception):
     535 + db = getattr(g, '_database', None)
     536 + if db is not None:
     537 + db.close()
     538 +def main():
     539 + # Banner
     540 + print(fr"""
     541 + ________ _________
     542 + / / by RedByte1337 __ / / v{__version__}
     543 + / _____/___________ ______ | |__ / _____/_____ ______
     544 +/ \ __\_ __ \__ \ \____ \| | \ \_____ \\____ \ | |
     545 +\ \_\ \ | \/ __ \| |_> | \ \/ \ |_> \___ |
     546 + \______ /__| |____ | __/|___| /_______ / ___/ ____|
     547 + \/ \/|__| \/ \/|__| \/
     548 + """)
     549 + # Argument Parser
     550 + import argparse
     551 + parser = argparse.ArgumentParser(prog="GraphSpy", description="Launches the GraphSpy Flask application", epilog="For more information, see https://github.com/RedByte1337/GraphSpy")
     552 + parser.add_argument("-i","--interface", type=str, help="The interface to bind to. Use 0.0.0.0 for all interfaces. (Default = 127.0.0.1)")
     553 + parser.add_argument("-p", "--port", type=int, help="The port to bind to. (Default = 5000)")
     554 + parser.add_argument("-d","--database", type=str, default="database.db", help="Database file to utilize. (Default = database.db)")
     555 + parser.add_argument("--debug", action="store_true", help="Enable flask debug mode. Will show detailed stack traces when an error occurs.")
     556 + args = parser.parse_args()
     557 + 
     558 + # Create global Flask app variable
     559 + global app
     560 + app = Flask(__name__)
     561 + init_routes()
     562 + 
     563 + # First time Use
     564 + graph_spy_folder = os.path.normpath(os.path.expanduser("~/.gspy/"))
     565 + if(not os.path.exists(graph_spy_folder)):
     566 + print("[*] First time use detected.")
     567 + print(f"[*] Creating directory '{graph_spy_folder}'.")
     568 + os.mkdir(graph_spy_folder)
     569 + if(not os.path.exists(graph_spy_folder)):
     570 + sys.exit(f"Failed creating directory '{graph_spy_folder}'. Unable to proceed.")
     571 + app.config['graph_spy_folder'] = graph_spy_folder
     572 + 
     573 + # Database
     574 + database = args.database
     575 + # Normalize db path
     576 + database = database if database.endswith(".db") else f"{database}.db"
     577 + # Create database folder if it doesn't exist yet
     578 + graph_spy_db_folder = os.path.normpath(os.path.join(graph_spy_folder,"databases/"))
     579 + if(not os.path.exists(graph_spy_db_folder)):
     580 + print(f"[*] Creating directory '{graph_spy_db_folder}'.")
     581 + os.mkdir(graph_spy_db_folder)
     582 + if(not os.path.exists(graph_spy_db_folder)):
     583 + sys.exit(f"Failed creating directory '{graph_spy_db_folder}'. Unable to proceed.")
     584 + app.config['graph_spy_db_folder'] = graph_spy_db_folder
     585 + graph_spy_db_path = safe_join(graph_spy_db_folder,database)
     586 + if not graph_spy_db_path:
     587 + sys.exit(f"Invalid database name '{database}'.")
     588 + app.config['graph_spy_db_path'] = graph_spy_db_path
     589 + # Initialize DB if it doesn't exist yet
     590 + if(not os.path.exists(graph_spy_db_path)):
     591 + print(f"[*] Database file '{graph_spy_db_path}' not found. Initializing new database.")
     592 + init_db()
     593 + if(not os.path.exists(graph_spy_db_path)):
     594 + sys.exit(f"Failed creating database file at '{graph_spy_db_path}'. Unable to proceed.")
     595 + print(f"[*] Utilizing database '{graph_spy_db_path}'.")
     596 +
     597 + # Disable datatable error messages by default.
     598 + app.config['table_error_messages'] = "disabled"
     599 + 
     600 + # Run flask
     601 + print(f"[*] Starting GraphSpy. Open in your browser by going to the url displayed below.\n")
     602 + app.run(debug=args.debug, host=args.interface, port=args.port)
     603 + 
     604 +if __name__ == '__main__':
     605 + main()
  • ■ ■ ■ ■ ■
    GraphSpy/__init__.py
     1 + 
  • ■ ■ ■ ■ ■ ■
    GraphSpy/static/css/style.css
     1 +/* Change color of every other table row to the secondary background color */
     2 +table.dataTable.display tbody tr.odd {
     3 + background-color: var(--bs-secondary-bg);
     4 +}
     5 +/* Required to overwrite the background color of select drop downs in datatables for visibility*/
     6 +.form-select {
     7 + background-color: var(--bs-body-bg) !important;
     8 +}
     9 + 
  • ■ ■ ■ ■ ■ ■
    GraphSpy/static/js/functions.js
     1 +// ========== Access Tokens ==========
     2 + 
     3 +function setActiveAccessToken(access_token_id) {
     4 + var active_access_token = access_token_id;
     5 + setCookie("access_token_id", active_access_token);
     6 + let response = $.ajax({
     7 + type: "GET",
     8 + async: false,
     9 + url: "/api/active_access_token/" + active_access_token
     10 + });
     11 + if (document.getElementById("access_token_id")) {
     12 + document.getElementById("access_token_id").value = active_access_token;
     13 + }
     14 + obtainAccessTokenInfo();
     15 + bootstrapToast("Activate Access Token", `[Succes] Activated access token with ID '${active_access_token}'`);
     16 +};
     17 + 
     18 +function getActiveAccessToken(access_token_field = null) {
     19 + let response = $.ajax({
     20 + type: "GET",
     21 + async: false,
     22 + url: "/api/active_access_token"
     23 + });
     24 + active_access_token = response.responseText
     25 + setCookie("access_token_id", active_access_token);
     26 + if (access_token_field) {
     27 + access_token_field.value = active_access_token;
     28 + }
     29 +};
     30 + 
     31 +function deleteAccessToken(token_id) {
     32 + let response = $.ajax({
     33 + type: "GET",
     34 + async: false,
     35 + url: "/api/delete_access_token/" + token_id
     36 + });
     37 + bootstrapToast("Delete access token", `[Success] Deleted access token with ID ${token_id}.`);
     38 +};
     39 + 
     40 +// ========== Refresh Tokens ==========
     41 + 
     42 +function setActiveRefreshToken(refresh_token_id) {
     43 + var active_refresh_token = refresh_token_id;
     44 + setCookie("refresh_token_id", active_refresh_token);
     45 + let response = $.ajax({
     46 + type: "GET",
     47 + async: false,
     48 + url: "/api/active_refresh_token/" + active_refresh_token
     49 + });
     50 + if (document.getElementById("refresh_token_id")) {
     51 + document.getElementById("refresh_token_id").value = active_refresh_token;
     52 + }
     53 + obtainRefreshTokenInfo();
     54 + bootstrapToast("Activate Refresh Token", `[Succes] Activated refresh token with ID '${active_refresh_token}'`);
     55 +};
     56 + 
     57 +function getActiveRefreshToken(refresh_token_field) {
     58 + let response = $.ajax({
     59 + type: "GET",
     60 + async: false,
     61 + url: "/api/active_refresh_token"
     62 + });
     63 + active_refresh_token = response.responseText
     64 + setCookie("refresh_token_id", active_refresh_token);
     65 + if (refresh_token_field) {
     66 + refresh_token_field.value = active_refresh_token
     67 + }
     68 +};
     69 + 
     70 +function refreshToAccessToken(refresh_token_id, resource, client_id, store_refresh_token = false, activate = false) {
     71 + var post_data = {
     72 + "refresh_token_id": refresh_token_id,
     73 + "resource": resource,
     74 + "client_id": client_id
     75 + };
     76 + if (store_refresh_token) {
     77 + post_data["store_refresh_token"] = 1;
     78 + }
     79 + let response = $.ajax({
     80 + type: "POST",
     81 + async: false,
     82 + url: "/api/refresh_to_access_token",
     83 + data: post_data
     84 + });
     85 + access_token_id = response.responseText;
     86 + if (Number.isInteger(parseInt(access_token_id))) {
     87 + bootstrapToast("Refresh To Access Token", `[Succes] Obtained access token with ID '${access_token_id}'`);
     88 + if (activate) {
     89 + setActiveAccessToken(access_token_id);
     90 + }
     91 + } else {
     92 + bootstrapToast("Refresh To Access Token", '[Error] Failed to obtain an access token.');
     93 + }
     94 +};
     95 + 
     96 +function deleteRefreshToken(token_id) {
     97 + let response = $.ajax({
     98 + type: "GET",
     99 + async: false,
     100 + url: "/api/delete_refresh_token/" + token_id
     101 + });
     102 + bootstrapToast("Delete refresh token", `[Success] Deleted refresh token with ID ${token_id}.`);
     103 +}
     104 + 
     105 +// ========== Device Codes ==========
     106 + 
     107 +function generateDeviceCode(resource, client_id) {
     108 + let response = $.ajax({
     109 + type: "POST",
     110 + async: false,
     111 + url: "/api/generate_device_code",
     112 + data: { "resource": resource, "client_id": client_id }
     113 + });
     114 + bootstrapToast("Device Code", `[Success] Generated Device Code with User Code '${response.responseText}'.`);
     115 + reloadTables();
     116 +}
     117 + 
     118 +function restartDeviceCodePolling() {
     119 + let response = $.ajax({
     120 + type: "POST",
     121 + async: false,
     122 + url: "/api/restart_device_code_polling"
     123 + });
     124 + $('#device_codes').DataTable().ajax.reload(null, false);
     125 + bootstrapToast("Restart polling", response.responseText);
     126 +}
     127 + 
     128 +// ========== Graph ==========
     129 + 
     130 + 
     131 +function graphDownload(drive_id, item_id, access_token_id) {
     132 + let graph_uri = "https://graph.microsoft.com/v1.0/drives/" + drive_id + "/items/" + item_id
     133 + let response = $.ajax({
     134 + type: "POST",
     135 + async: false,
     136 + url: "/api/generic_graph",
     137 + dataSrc: "",
     138 + data: { "graph_uri": graph_uri, "access_token_id": access_token_id },
     139 + });
     140 + let response_json = JSON.parse(response.responseText)
     141 + window.location = response_json["@microsoft.graph.downloadUrl"];
     142 +}
     143 + 
     144 +// ========== Database ==========
     145 + 
     146 +function deleteDatabase(database_name) {
     147 + let response = $.ajax({
     148 + type: "POST",
     149 + async: false,
     150 + url: "/api/delete_database",
     151 + data: { "database": database_name }
     152 + });
     153 + bootstrapToast("Delete database", response.responseText)
     154 +}
     155 + 
     156 +function activateDatabase(database_name) {
     157 + let response = $.ajax({
     158 + type: "POST",
     159 + async: false,
     160 + url: "/api/activate_database",
     161 + data: { "database": database_name }
     162 + });
     163 + obtainAccessTokenInfo();
     164 + obtainRefreshTokenInfo();
     165 + obtainPersistentSettings();
     166 + bootstrapToast("Acticate database", response.responseText)
     167 +}
     168 + 
     169 +function createDatabase(database_name) {
     170 + let response = $.ajax({
     171 + type: "POST",
     172 + async: false,
     173 + url: "/api/create_database",
     174 + data: { "database": database_name }
     175 + });
     176 + obtainAccessTokenInfo();
     177 + obtainRefreshTokenInfo();
     178 + obtainPersistentSettings();
     179 + bootstrapToast("Create database", response.responseText)
     180 +}
     181 + 
     182 +function duplicateDatabase(database_name) {
     183 + let response = $.ajax({
     184 + type: "POST",
     185 + async: false,
     186 + url: "/api/duplicate_database",
     187 + data: { "database": database_name }
     188 + });
     189 + bootstrapToast("Duplicate database", response.responseText)
     190 +}
     191 + 
     192 +// ========== Settings ==========
     193 + 
     194 +function setTableErorMessages(state) {
     195 + let response = $.ajax({
     196 + type: "POST",
     197 + async: false,
     198 + url: "/api/set_table_error_messages",
     199 + data: { "state": state }
     200 + });
     201 + bootstrapToast("DataTable Error Messages", response.responseText)
     202 + $('#dt-error-message-button-disabled').toggleClass("active")
     203 + $('#dt-error-message-button-enabled').toggleClass("active")
     204 +}
     205 + 
     206 +// ========== Cookies ==========
     207 + 
     208 +function getCookie(name) {
     209 + const value = `; ${document.cookie}`;
     210 + const parts = value.split(`; ${name}=`);
     211 + if (parts.length === 2) return parts.pop().split(';').shift();
     212 +};
     213 + 
     214 +function setCookie(name, value) {
     215 + var today = new Date();
     216 + var expiry = new Date(today.getTime() + 30 * 24 * 3600 * 1000);
     217 + document.cookie = name + "=" + escape(value) + "; path=/; expires=" + expiry.toGMTString();
     218 +};
     219 + 
     220 +// ========== Helpers ==========
     221 + 
     222 +function copyToClipboard(text) {
     223 + if (navigator.clipboard) {
     224 + navigator.clipboard.writeText(text);
     225 + } else {
     226 + const tmp = document.createElement('TEXTAREA');
     227 + const focus = document.activeElement;
     228 + 
     229 + tmp.value = text;
     230 + 
     231 + document.body.appendChild(tmp);
     232 + tmp.select();
     233 + document.execCommand('copy');
     234 + document.body.removeChild(tmp);
     235 + focus.focus();
     236 + }
     237 + var messageTruncated = ((text.length > 100) ? `${text.substr(0, 100)}...` : text)
     238 + bootstrapToast("Copy to clipboard", `Copied to clipboard: '${messageTruncated}'`);
     239 +}
     240 + 
     241 +function reloadTables() {
     242 + $('table.dataTable').DataTable().ajax.reload(null, false);
     243 +}
     244 + 
     245 +// ========== Messages ==========
     246 + 
     247 +function bootstrapAlert(message, type) {
     248 + // Types: primary, secondary, success, danger, warning, info, light, dark
     249 + var type_class = `alert-${type}`;
     250 + var dom = $('<div>');
     251 + dom.addClass("alert alert-dismissible");
     252 + dom.addClass(type_class);
     253 + dom.attr("role", "alert");
     254 + dom.text(message);
     255 + dom.append($('<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>'));
     256 + $('#alert_placeholder').append(dom);
     257 +}
     258 + 
     259 +function bootstrapToast(title, message) {
     260 + // Wrapper for new Toast Message
     261 + var toast_wrapper = $('<div class="toast" role="alert" aria-live="assertive" aria-atomic="true"></div>');
     262 + // Toast header
     263 + var toast_header = $('<div class="toast-header"><small>Just now</small><button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button></div>');
     264 + var toast_title = $('<strong class="me-auto"></strong>');
     265 + toast_title.text(title);
     266 + toast_header.prepend(toast_title);
     267 + // Toast body
     268 + var toast_body = $('<div class="toast-body"></div>');
     269 + toast_body.text(message);
     270 + // Append header and body to toast wrapper
     271 + toast_wrapper.append(toast_header);
     272 + toast_wrapper.append(toast_body);
     273 + // Append new Toast Message to the page
     274 + $('#toast_placeholder').append(toast_wrapper);
     275 + // Activate the last Toast Message
     276 + const toastList = [...$(".toast")].map(toastEl => new bootstrap.Toast(toastEl, "show"))
     277 + toastList[toastList.length - 1].show()
     278 +}
     279 + 
  • ■ ■ ■ ■ ■ ■
    GraphSpy/static/js/theme.js
     1 +/*!
     2 + * Color mode toggler for Bootstrap's docs (https://getbootstrap.com/)
     3 + * Copyright 2011-2023 The Bootstrap Authors
     4 + * Licensed under the Creative Commons Attribution 3.0 Unported License.
     5 + */
     6 + 
     7 +(() => {
     8 + 'use strict'
     9 + 
     10 + const getStoredTheme = () => localStorage.getItem('theme')
     11 + const setStoredTheme = theme => localStorage.setItem('theme', theme)
     12 + 
     13 + const getPreferredTheme = () => {
     14 + const storedTheme = getStoredTheme()
     15 + if (storedTheme) {
     16 + return storedTheme
     17 + }
     18 + 
     19 + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
     20 + }
     21 + 
     22 + const setTheme = theme => {
     23 + if (theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches) {
     24 + document.documentElement.setAttribute('data-bs-theme', 'dark')
     25 + } else {
     26 + document.documentElement.setAttribute('data-bs-theme', theme)
     27 + }
     28 + }
     29 + 
     30 + setTheme(getPreferredTheme())
     31 + 
     32 + const showActiveTheme = (theme, focus = false) => {
     33 + const themeSwitcher = document.querySelector('#bd-theme')
     34 + 
     35 + if (!themeSwitcher) {
     36 + return
     37 + }
     38 + 
     39 + const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`)
     40 + 
     41 + document.querySelectorAll('[data-bs-theme-value]').forEach(element => {
     42 + element.classList.remove('active')
     43 + element.setAttribute('aria-pressed', 'false')
     44 + })
     45 + 
     46 + btnToActive.classList.add('active')
     47 + btnToActive.setAttribute('aria-pressed', 'true')
     48 + 
     49 + if (focus) {
     50 + themeSwitcher.focus()
     51 + }
     52 + }
     53 + 
     54 + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
     55 + const storedTheme = getStoredTheme()
     56 + if (storedTheme !== 'light' && storedTheme !== 'dark') {
     57 + setTheme(getPreferredTheme())
     58 + }
     59 + })
     60 + 
     61 + window.addEventListener('DOMContentLoaded', () => {
     62 + showActiveTheme(getPreferredTheme())
     63 + 
     64 + document.querySelectorAll('[data-bs-theme-value]')
     65 + .forEach(toggle => {
     66 + toggle.addEventListener('click', () => {
     67 + const theme = toggle.getAttribute('data-bs-theme-value')
     68 + setStoredTheme(theme)
     69 + setTheme(theme)
     70 + showActiveTheme(theme, true)
     71 + })
     72 + })
     73 + })
     74 +})()
  • ■ ■ ■ ■ ■ ■
    GraphSpy/templates/OneDrive.html
     1 +{% extends 'layout.html'%}
     2 + 
     3 +{%block content%}
     4 + 
     5 +<br>
     6 +<div class="col-9">
     7 + <h1>OneDrive Files</h1>
     8 + <form id="onedrive_form" class="row g-3">
     9 + <div>
     10 + <label for="path" class="form-label">Path:</label>
     11 + <input type="text" id="path" name="path" value="/" class="form-control">
     12 + </div>
     13 + <div class="col-3">
     14 + <label for="access_token_id" class="form-label">Access token id *</label>
     15 + <input type="text" id="access_token_id" name="access_token_id" class="form-control" required>
     16 + </div>
     17 + <div>
     18 + <button type="Button" class="btn btn-primary" onclick="generateTable()">Browse</button>
     19 + </div>
     20 + </form>
     21 + <br>
     22 + <button onclick="openParentFolder()" class="btn btn-primary">Parent Folder</button>
     23 + <script>
     24 + getActiveAccessToken(document.getElementById("onedrive_form").access_token_id)
     25 + </script>
     26 +</div>
     27 +<br>
     28 +<div>
     29 + <h2>Files</h2>
     30 + <table id="response_table" class="display" style="word-wrap: break-word; word-break: break-all; width:100%">
     31 + <thead>
     32 + <tr>
     33 + <th></th>
     34 + <th></th>
     35 + <th>Created</th>
     36 + <th>Last Modified</th>
     37 + <th>File Name</th>
     38 + <th>File Size</th>
     39 + <th>URL</th>
     40 + </tr>
     41 + </thead>
     42 + </table>
     43 +</div>
     44 +<script>
     45 + generateTable();
     46 + // Populate the response_table table
     47 + function generateTable() {
     48 + // Remove potential trailing slashes in path
     49 + var temp_path = document.getElementById("onedrive_form").path.value
     50 + if (temp_path.slice(-1) == '/' && temp_path != "/") {
     51 + document.getElementById("onedrive_form").path.value = temp_path.slice(0, -1)
     52 + }
     53 + if ($.fn.dataTable.isDataTable("#response_table")) {
     54 + // If the DataTable already exists, just reload it
     55 + $('#response_table').DataTable().ajax.reload(null, false);
     56 + } else {
     57 + // Initialize datatable
     58 + let myTable = new DataTable('#response_table', {
     59 + ajax: {
     60 + type: "POST",
     61 + url: '/api/generic_graph',
     62 + dataSrc: function (json) {
     63 + if (json.hasOwnProperty("error")) {
     64 + bootstrapAlert(`[${json.error.code}] ${json.error.message}`, "danger");
     65 + return [];
     66 + }
     67 + return json.value
     68 + },
     69 + data: function (d) {
     70 + d.graph_uri = "https://graph.microsoft.com/v1.0/me/drive/root:/" + document.getElementById("onedrive_form").path.value + ":/children",
     71 + d.access_token_id = document.getElementById("onedrive_form").access_token_id.value
     72 + }
     73 + },
     74 + columns: [
     75 + {
     76 + className: 'dt-control',
     77 + orderable: false,
     78 + data: null,
     79 + defaultContent: '',
     80 + 'width': '20px'
     81 + },
     82 + {
     83 + className: 'action-control',
     84 + orderable: false,
     85 + data: null,
     86 + render: function (d, t, r) {
     87 + if (r.folder) {
     88 + // Folder icon
     89 + return '<i class="fi fi-sr-folder-open" style="cursor: pointer"></i>'
     90 + } else if (r.file) {
     91 + // Download icon
     92 + return '<i class="fi fi-br-download" style="cursor: pointer"></i>'
     93 + }
     94 + // Question mark icon
     95 + return '<i class="fi fi-br-question" style="cursor: pointer"></i>'
     96 + },
     97 + 'width': '20px'
     98 + },
     99 + {
     100 + data: 'createdDateTime',
     101 + width: '150px'
     102 + },
     103 + {
     104 + data: 'lastModifiedDateTime',
     105 + width: '150px'
     106 + },
     107 + { data: 'name' },
     108 + {
     109 + data: 'size',
     110 + width: '100px'
     111 + },
     112 + { data: 'webUrl' }
     113 + ]
     114 + })
     115 + 
     116 + myTable.on('click', 'td.dt-control', function (e) {
     117 + let tr = e.target.closest('tr');
     118 + let row = myTable.row(tr);
     119 + 
     120 + if (row.child.isShown()) {
     121 + // This row is already open - close it
     122 + row.child.hide();
     123 + }
     124 + else {
     125 + // Open this row
     126 + row.child(format(row.data())).show();
     127 + }
     128 + });
     129 + 
     130 + myTable.on('click', 'td.action-control', function (e) {
     131 + let tr = e.target.closest('tr');
     132 + let row = myTable.row(tr);
     133 + if (row.data().folder) {
     134 + // This is a folder
     135 + folder_name = row.data().name
     136 + openFolder(folder_name);
     137 + } else if (row.data().file) {
     138 + // This is a file
     139 + drive_id = row.data().parentReference.driveId
     140 + item_id = row.data().id
     141 + access_token_id = document.getElementById("onedrive_form").access_token_id.value
     142 + graphDownload(drive_id, item_id, access_token_id);
     143 + } else {
     144 + alert("No action defined for this type of entity.")
     145 + }
     146 + });
     147 + }
     148 + return false;
     149 + }
     150 + 
     151 + function format(d) {
     152 + // `d` is the original data object for the row
     153 + return (
     154 + '<dl style = "word-wrap: break-word; word-break: break-all; width:100%">' +
     155 + '<dt>Raw File Info:</dt>' +
     156 + '<dd><pre>' +
     157 + JSON.stringify(d, undefined, 4) +
     158 + '</pre></dd>' +
     159 + '</dl>'
     160 + );
     161 + }
     162 + 
     163 + function openFolder(folder_name) {
     164 + // Make sure the last character of the path is a '/' before appending the folder name
     165 + if (document.getElementById("onedrive_form").path.value.slice(-1) != '/') {
     166 + document.getElementById("onedrive_form").path.value += "/"
     167 + }
     168 + document.getElementById("onedrive_form").path.value += folder_name
     169 + $('#response_table').DataTable().ajax.reload(null, false);
     170 + }
     171 + 
     172 + function openParentFolder() {
     173 + // Make sure the last character of the path is a '/' before appending the folder name
     174 + var temp_path = document.getElementById("onedrive_form").path.value
     175 + if (temp_path.slice(-1) == '/') {
     176 + temp_path = temp_path.slice(0, -1)
     177 + }
     178 + temp_path = temp_path.split('/').slice(0, -1).join("/")
     179 + if (temp_path == '') {
     180 + temp_path = "/"
     181 + }
     182 + document.getElementById("onedrive_form").path.value = temp_path
     183 + $('#response_table').DataTable().ajax.reload(null, false);
     184 + }
     185 +</script>
     186 +{%endblock content%}
  • ■ ■ ■ ■ ■ ■
    GraphSpy/templates/SharePoint.html
     1 +{% extends 'layout.html'%}
     2 + 
     3 +{%block content%}
     4 + 
     5 +<br>
     6 +<div class="col-md-9">
     7 + <h1>SharePoint Files</h1>
     8 + <form id="sharepoint_form" class="row g-3">
     9 + <div class="col-md-9">
     10 + <label for="drive_id" class="form-label">Drive ID *</label>
     11 + <input type="text" id="drive_id" name="drive_id" class="form-control" required>
     12 + </div>
     13 + <div class="col-md-3">
     14 + <label for="access_token_id" class="form-label">Access token id *</label>
     15 + <input type="text" id="access_token_id" name="access_token_id" class="form-control" required>
     16 + </div>
     17 + <div>
     18 + <label for="path" class="form-label">Path *</label>
     19 + <input type="text" id="path" name="path" value="/" class="form-control" required>
     20 + </div>
     21 + <div>
     22 + <button type="Button" class="btn btn-primary" onclick="generateTable()">Browse</button>
     23 + </div>
     24 + </form>
     25 + <br>
     26 + <button class="btn btn-primary" onclick="openParentFolder()">Parent Folder</button>
     27 + <script>
     28 + getActiveAccessToken(document.getElementById("sharepoint_form").access_token_id)
     29 + </script>
     30 +</div>
     31 +<br>
     32 + 
     33 +<div>
     34 + <h2>Files Table</h2>
     35 + <table id="response_table" class="display" style="word-wrap: break-word; word-break: break-all; width:100%">
     36 + <thead>
     37 + <tr>
     38 + <th></th>
     39 + <th></th>
     40 + <th>Created</th>
     41 + <th>Last Modified</th>
     42 + <th>File Name</th>
     43 + <th>File Size</th>
     44 + <th>URL</th>
     45 + </tr>
     46 + </thead>
     47 + </table>
     48 +</div>
     49 +<script>
     50 + // If the URL contains a driveId parameter, it will automatically be filled in and the table will auto generate
     51 + function setDriveId() {
     52 + let params = (new URL(document.location)).searchParams;
     53 + if (!params.has("driveId")) { return }
     54 + document.getElementById("sharepoint_form").drive_id.value = params.get("driveId");
     55 + if (getCookie("access_token_id")) {
     56 + generateTable()
     57 + }
     58 + };
     59 + setDriveId()
     60 + // Populate the response_table table
     61 + function generateTable() {
     62 + // Remove potential trailing slashes in path
     63 + var temp_path = document.getElementById("sharepoint_form").path.value
     64 + if (temp_path.slice(-1) == '/' && temp_path != "/") {
     65 + document.getElementById("sharepoint_form").path.value = temp_path.slice(0, -1)
     66 + }
     67 + if ($.fn.dataTable.isDataTable("#response_table")) {
     68 + // If the DataTable already exists, just reload it
     69 + $('#response_table').DataTable().ajax.reload(null, false);
     70 + } else {
     71 + // Initialize datatable
     72 + let myTable = new DataTable('#response_table', {
     73 + ajax: {
     74 + type: "POST",
     75 + url: '/api/generic_graph',
     76 + dataSrc: function (json) {
     77 + if (json.hasOwnProperty("error")) {
     78 + bootstrapAlert(`[${json.error.code}] ${json.error.message}`, "danger");
     79 + return [];
     80 + }
     81 + return json.value
     82 + },
     83 + data: function (d) {
     84 + d.graph_uri = "https://graph.microsoft.com/v1.0/drives/" + document.getElementById("sharepoint_form").drive_id.value + "/root:/" + document.getElementById("sharepoint_form").path.value + ":/children",
     85 + d.access_token_id = document.getElementById("sharepoint_form").access_token_id.value
     86 + }
     87 + },
     88 + columns: [
     89 + {
     90 + className: 'dt-control',
     91 + orderable: false,
     92 + data: null,
     93 + defaultContent: '',
     94 + 'width': '20px'
     95 + },
     96 + {
     97 + className: 'action-control',
     98 + orderable: false,
     99 + data: null,
     100 + render: function (d, t, r) {
     101 + if (r.folder) {
     102 + // Folder icon
     103 + return '<i class="fi fi-sr-folder-open" style="cursor: pointer"></i>'
     104 + } else if (r.file) {
     105 + // Download icon
     106 + return '<i class="fi fi-br-download" style="cursor: pointer"></i>'
     107 + }
     108 + // Question mark icon
     109 + return '<i class="fi fi-br-question" style="cursor: pointer"></i>'
     110 + },
     111 + 'width': '20px'
     112 + },
     113 + {
     114 + data: 'createdDateTime',
     115 + width: '150px'
     116 + },
     117 + {
     118 + data: 'lastModifiedDateTime',
     119 + width: '150px'
     120 + },
     121 + { data: 'name' },
     122 + {
     123 + data: 'size',
     124 + width: '100px'
     125 + },
     126 + { data: 'webUrl' }
     127 + ]
     128 + })
     129 + 
     130 + 
     131 + myTable.on('click', 'td.dt-control', function (e) {
     132 + let tr = e.target.closest('tr');
     133 + let row = myTable.row(tr);
     134 + 
     135 + if (row.child.isShown()) {
     136 + // This row is already open - close it
     137 + row.child.hide();
     138 + }
     139 + else {
     140 + // Open this row
     141 + row.child(format(row.data())).show();
     142 + }
     143 + 
     144 + });
     145 + 
     146 + myTable.on('click', 'td.action-control', function (e) {
     147 + let tr = e.target.closest('tr');
     148 + let row = myTable.row(tr);
     149 + //debugger;
     150 + if (row.data().folder) {
     151 + // This is a folder
     152 + folder_name = row.data().name
     153 + openFolder(folder_name);
     154 + } else if (row.data().file) {
     155 + // This is a file
     156 + drive_id = row.data().parentReference.driveId
     157 + item_id = row.data().id
     158 + access_token_id = document.getElementById("sharepoint_form").access_token_id.value
     159 + graphDownload(drive_id, item_id, access_token_id);
     160 + } else {
     161 + alert("No action defined for this type of entity.")
     162 + }
     163 + });
     164 + }
     165 + 
     166 + return false;
     167 + }
     168 + 
     169 + function format(d) {
     170 + // `d` is the original data object for the row
     171 + return (
     172 + '<dl style = "word-wrap: break-word; word-break: break-all; width:100%">' +
     173 + '<dt>Raw File Info:</dt>' +
     174 + '<dd><pre>' +
     175 + JSON.stringify(d, undefined, 4) +
     176 + '</pre></dd>' +
     177 + '</dl>'
     178 + );
     179 + }
     180 + 
     181 + function openFolder(folder_name) {
     182 + // Make sure the last character of the path is a '/' before appending the folder name
     183 + if (document.getElementById("sharepoint_form").path.value.slice(-1) != '/') {
     184 + document.getElementById("sharepoint_form").path.value += "/"
     185 + }
     186 + document.getElementById("sharepoint_form").path.value += folder_name
     187 + $('#response_table').DataTable().ajax.reload(null, false);
     188 + }
     189 + 
     190 + function openParentFolder() {
     191 + // Make sure the last character of the path is a '/' before appending the folder name
     192 + var temp_path = document.getElementById("sharepoint_form").path.value
     193 + if (temp_path.slice(-1) == '/') {
     194 + temp_path = temp_path.slice(0, -1)
     195 + }
     196 + temp_path = temp_path.split('/').slice(0, -1).join("/")
     197 + if (temp_path == '') {
     198 + temp_path = "/"
     199 + }
     200 + document.getElementById("sharepoint_form").path.value = temp_path
     201 + $('#response_table').DataTable().ajax.reload(null, false);
     202 + }
     203 +</script>
     204 +{%endblock content%}
  • ■ ■ ■ ■ ■ ■
    GraphSpy/templates/SharePointDrives.html
     1 +{% extends 'layout.html'%}
     2 + 
     3 +{%block content%}
     4 + 
     5 +<br>
     6 +<div class="col-md-9">
     7 + <h1>SharePoint Drives</h1>
     8 + <form id="sharepoint_form" class="row g-3">
     9 + <div>
     10 + <label for="site_id" class="form-label">Site ID *</label>
     11 + <input type="text" id="site_id" name="site_id" class="form-control" required>
     12 + </div>
     13 + <div class="col-3">
     14 + <label for="access_token_id" class="form-label">Access token id *</label>
     15 + <input type="text" id="access_token_id" name="access_token_id" class="form-control" required>
     16 + </div>
     17 + <div>
     18 + <button type="Button" class="btn btn-primary" onclick="generateTable()">Browse</button>
     19 + </div>
     20 + </form>
     21 + <script>
     22 + getActiveAccessToken(document.getElementById("sharepoint_form").access_token_id)
     23 + </script>
     24 +</div>
     25 +<br>
     26 + 
     27 +<div>
     28 + <h2>Drives Table</h2>
     29 + <table id="response_table" class="display" style="word-wrap: break-word; word-break: break-all; width:100%">
     30 + <thead>
     31 + <tr>
     32 + <th></th>
     33 + <th></th>
     34 + <th>Created</th>
     35 + <th>Last Modified</th>
     36 + <th>Drive Name</th>
     37 + <th>URL</th>
     38 + </tr>
     39 + </thead>
     40 + </table>
     41 +</div>
     42 +<script>
     43 + // If the URL contains a siteId parameter, it will automatically be filled in and the table will auto generate
     44 + function setSiteId() {
     45 + let params = (new URL(document.location)).searchParams;
     46 + if (!params.has("siteId")) { return }
     47 + document.getElementById("sharepoint_form").site_id.value = params.get("siteId");
     48 + if (getCookie("access_token_id")) {
     49 + generateTable()
     50 + }
     51 + };
     52 + setSiteId()
     53 + // Populate the response_table table
     54 + function generateTable() {
     55 + if ($.fn.dataTable.isDataTable("#response_table")) {
     56 + // If the DataTable already exists, just reload it
     57 + $('#response_table').DataTable().ajax.reload(null, false);
     58 + } else {
     59 + // Initialize datatable
     60 + let myTable = new DataTable('#response_table', {
     61 + ajax: {
     62 + type: "POST",
     63 + url: '/api/generic_graph',
     64 + dataSrc: function (json) {
     65 + if (json.hasOwnProperty("error")) {
     66 + bootstrapAlert(`[${json.error.code}] ${json.error.message}`, "danger");
     67 + return [];
     68 + }
     69 + return json.value
     70 + },
     71 + data: function (d) {
     72 + d.graph_uri = "https://graph.microsoft.com/v1.0/sites/" + document.getElementById("sharepoint_form").site_id.value + "/drives",
     73 + d.access_token_id = document.getElementById("sharepoint_form").access_token_id.value
     74 + }
     75 + },
     76 + columns: [
     77 + {
     78 + className: 'dt-control',
     79 + orderable: false,
     80 + data: null,
     81 + defaultContent: '',
     82 + 'width': '20px'
     83 + },
     84 + {
     85 + className: 'action-control',
     86 + orderable: false,
     87 + data: null,
     88 + render: function (d, t, r) {
     89 + // Link icon
     90 + return '<i class="fi fi-br-link-alt" style="cursor: pointer"></i>'
     91 + // Question mark icon
     92 + // return '<i class="fi fi-br-question" style="cursor: pointer"></i>'
     93 + },
     94 + 'width': '20px'
     95 + },
     96 + {
     97 + data: 'createdDateTime',
     98 + width: '150px'
     99 + },
     100 + {
     101 + data: 'lastModifiedDateTime',
     102 + width: '150px'
     103 + },
     104 + { data: 'name' },
     105 + { data: 'webUrl' }
     106 + ]
     107 + })
     108 + 
     109 + myTable.on('click', 'td.dt-control', function (e) {
     110 + let tr = e.target.closest('tr');
     111 + let row = myTable.row(tr);
     112 + 
     113 + if (row.child.isShown()) {
     114 + // This row is already open - close it
     115 + row.child.hide();
     116 + }
     117 + else {
     118 + // Open this row
     119 + row.child(format(row.data())).show();
     120 + }
     121 + });
     122 + 
     123 + myTable.on('click', 'td.action-control', function (e) {
     124 + let tr = e.target.closest('tr');
     125 + let row = myTable.row(tr);
     126 + url = "/sharepoint?driveId=" + row.data().id
     127 + window.open(url, '_blank');
     128 + });
     129 + }
     130 + 
     131 + return false;
     132 + }
     133 + 
     134 + function format(d) {
     135 + // `d` is the original data object for the row
     136 + return (
     137 + '<dl style = "word-wrap: break-word; word-break: break-all; width:100%">' +
     138 + '<dt>Raw File Info:</dt>' +
     139 + '<dd><pre>' +
     140 + JSON.stringify(d, undefined, 4) +
     141 + '</pre></dd>' +
     142 + '</dl>'
     143 + );
     144 + }
     145 +</script>
     146 +{%endblock content%}
  • ■ ■ ■ ■ ■ ■
    GraphSpy/templates/SharePointSites.html
     1 +{% extends 'layout.html'%}
     2 + 
     3 +{%block content%}
     4 + 
     5 +<br>
     6 +<div class="col-md-6">
     7 + <h1>SharePoint Sites</h1>
     8 + <form id="sharepoint_form" class="row g-3">
     9 + <div class="col-sm-4">
     10 + <label for="search_query" class="form-label">Site Filter *</label>
     11 + <input type="text" id="search_query" name="search_query" class="form-control" value="*" required>
     12 + </div>
     13 + <div class="col-sm-4">
     14 + <label for="search_limit" class="form-label">Limit *</label>
     15 + <input type="text" id="search_limit" name="search_limit" class="form-control" value="100" required>
     16 + <i class="form-text">Max 1000</i>
     17 + </div>
     18 + <div class="col-sm-4">
     19 + <label for="access_token_id" class="form-label">Access token id *</label>
     20 + <input type="text" id="access_token_id" name="access_token_id" class="form-control" required>
     21 + </div>
     22 + <div>
     23 + <button type="Button" class="btn btn-primary" onclick="generateTable()">Browse</button>
     24 + </div>
     25 + </form>
     26 + <script>
     27 + getActiveAccessToken(document.getElementById("sharepoint_form").access_token_id)
     28 + </script>
     29 +</div>
     30 +<br>
     31 + 
     32 +<div>
     33 + <h2>Sites Table</h2>
     34 + <table id="response_table" class="display" style="word-wrap: break-word; word-break: break-all; width:100%">
     35 + <thead>
     36 + <tr>
     37 + <th></th>
     38 + <th></th>
     39 + <th>Created</th>
     40 + <th>Last Modified</th>
     41 + <th>Site Name</th>
     42 + <th>Display Name</th>
     43 + <th>URL</th>
     44 + </tr>
     45 + </thead>
     46 + </table>
     47 +</div>
     48 +<script>
     49 + // Populate the response_table table
     50 + function generateTable() {
     51 + if ($.fn.dataTable.isDataTable("#response_table")) {
     52 + // If the DataTable already exists, just reload it
     53 + $('#response_table').DataTable().ajax.reload(null, false);
     54 + } else {
     55 + // Initialize datatable
     56 + let myTable = new DataTable('#response_table', {
     57 + ajax: {
     58 + type: "POST",
     59 + url: '/api/generic_graph_post',
     60 + dataSrc: function (json) {
     61 + if (json.hasOwnProperty("error")) {
     62 + bootstrapAlert(`[${json.error.code}] ${json.error.message}`, "danger");
     63 + return [];
     64 + }
     65 + return json.value[0].hitsContainers[0].hits
     66 + },
     67 + data: function (d) {
     68 + d.graph_uri = "https://graph.microsoft.com/v1.0/search/query";
     69 + d.access_token_id = document.getElementById("sharepoint_form").access_token_id.value;
     70 + d.body = '{"requests": [{"entityTypes": ["site"], "query": {"queryString": "' + document.getElementById("sharepoint_form").search_query.value + '"}, "from": 0, "size": ' + document.getElementById("sharepoint_form").search_limit.value + '}]}';
     71 + }
     72 + },
     73 + columns: [
     74 + {
     75 + className: 'dt-control',
     76 + orderable: false,
     77 + data: null,
     78 + defaultContent: '',
     79 + 'width': '20px'
     80 + },
     81 + {
     82 + className: 'action-control',
     83 + orderable: false,
     84 + data: null,
     85 + render: function (d, t, r) {
     86 + if (r.resource["@odata.type"] == "#microsoft.graph.site") {
     87 + // Link icon
     88 + return '<i class="fi fi-br-link-alt" style="cursor: pointer"></i>'
     89 + }
     90 + // Question mark icon
     91 + return '<i class="fi fi-br-question" style="cursor: pointer"></i>'
     92 + },
     93 + 'width': '20px'
     94 + },
     95 + {
     96 + data: 'resource.createdDateTime',
     97 + width: '150px'
     98 + },
     99 + {
     100 + data: 'resource.lastModifiedDateTime',
     101 + width: '150px'
     102 + },
     103 + { data: 'resource.name' },
     104 + { data: 'resource.displayName' },
     105 + { data: 'resource.webUrl' }
     106 + ]
     107 + })
     108 + 
     109 + myTable.on('click', 'td.dt-control', function (e) {
     110 + let tr = e.target.closest('tr');
     111 + let row = myTable.row(tr);
     112 + 
     113 + if (row.child.isShown()) {
     114 + // This row is already open - close it
     115 + row.child.hide();
     116 + }
     117 + else {
     118 + // Open this row
     119 + row.child(format(row.data())).show();
     120 + }
     121 + });
     122 + 
     123 + myTable.on('click', 'td.action-control', function (e) {
     124 + let tr = e.target.closest('tr');
     125 + let row = myTable.row(tr);
     126 + if (row.data().resource["@odata.type"] == "#microsoft.graph.site") {
     127 + // This is a site
     128 + url = "/sharepoint_drives?siteId=" + row.data().resource.id
     129 + window.open(url, '_blank');
     130 + } else {
     131 + alert("No action defined for this type of entity.")
     132 + }
     133 + });
     134 + }
     135 + return false;
     136 + }
     137 + 
     138 + function format(d) {
     139 + // `d` is the original data object for the row
     140 + return (
     141 + '<dl style = "word-wrap: break-word; word-break: break-all; width:100%">' +
     142 + '<dt>Raw File Info:</dt>' +
     143 + '<dd><pre>' +
     144 + JSON.stringify(d, undefined, 4) +
     145 + '</pre></dd>' +
     146 + '</dl>'
     147 + );
     148 + }
     149 +</script>
     150 +{%endblock content%}
  • ■ ■ ■ ■ ■ ■
    GraphSpy/templates/access_tokens.html
     1 +{% extends 'layout.html'%}
     2 + 
     3 +{%block content%}
     4 + 
     5 +<br>
     6 +<div class="row g-3">
     7 + <div class="col-xl-6">
     8 + <div>
     9 + <h1>Add Access Token</h1>
     10 + <form action="/api/add_access_token" method="post" class="row g-3">
     11 + <div class="col-11">
     12 + <label for="accesstoken" class="form-label"><b>Access token *</b></label>
     13 + <textarea type="text" id="accesstoken" name="accesstoken" class="form-control" rows=5 required placeholder="eyJ..."></textarea>
     14 + </div>
     15 + <div class="col-11">
     16 + <label for="description" class="form-label">Description</label>
     17 + <input type="text" id="description" name="description" class="form-control" placeholder="My First Token">
     18 + </div>
     19 + <div>
     20 + <button type="submit" class="btn btn-primary">Submit</button>
     21 + </div>
     22 + </form>
     23 + </div>
     24 + <br>
     25 + <div>
     26 + <h1>Active Access Token</h1>
     27 + <form id="access_token_form" class="row row-cols-auto">
     28 + <div>
     29 + <label for="access_token_id" class="col-form-label">Active Access Token</label>
     30 + </div>
     31 + <div>
     32 + <input type="text" id="access_token_id" class="form-control" size="5" required>
     33 + </div>
     34 + <div>
     35 + <button type="Button" class="btn btn-primary" onclick="setActiveAccessToken(access_token_id.value)">Set active token</button>
     36 + </div>
     37 + </form>
     38 + </div>
     39 + </div>
     40 + <div class="col-xl-6">
     41 + <div class="col-11">
     42 + <h1>Refresh To Access Token</h1>
     43 + <form id="refresh_to_access_token_form" class="row g-3">
     44 + <div class="col-12">
     45 + <label for="resource" class="form-label">Resource</label>
     46 + <input list="resource_list" id="resource_input" class="form-control" placeholder="https://graph.microsoft.com">
     47 + <datalist id="resource_list">
     48 + <option value="defined_in_token">Defined in Refresh Token</option>
     49 + <option value="https://graph.microsoft.com">MSGraph</option>
     50 + <option value="https://graph.windows.net/">AAD Graph</option>
     51 + <option value="https://outlook.office365.com">Outlook</option>
     52 + <option value="https://api.spaces.skype.com/">MSTeams</option>
     53 + <option value="https://management.core.windows.net/">AzureCoreManagement</option>
     54 + <option value="https://management.azure.com">AzureManagement</option>
     55 + </datalist>
     56 + </div>
     57 + <div class="col-12">
     58 + <label for="client_id" class="form-label">Client ID</label>
     59 + <input list="client_id_list" id="client_id_input" class="form-control" placeholder="d3590ed6-52b3-4102-aeff-aad2292ab01c">
     60 + <datalist id="client_id_list">
     61 + <option value="d3590ed6-52b3-4102-aeff-aad2292ab01c">Microsoft Office</option>
     62 + <option value="1fec8e78-bce4-4aaf-ab1b-5451cc387264">Microsoft Teams</option>
     63 + <option value="27922004-5251-4030-b22d-91ecd9a37ea4">Outlook Mobile</option>
     64 + <option value="b26aadf8-566f-4478-926f-589f601d9c74">OneDrive</option>
     65 + <option value="d326c1ce-6cc6-4de2-bebc-4591e5e13ef0">SharePoint</option>
     66 + <option value="00b41c95-dab0-4487-9791-b9d2c32c80f2">Office 365 Management</option>
     67 + <option value="04b07795-8ddb-461a-bbee-02f9e1bf7b46">Microsoft Azure CLI</option>
     68 + <option value="1950a258-227b-4e31-a9cf-717495945fc2">Microsoft Azure PowerShell</option>
     69 + </datalist>
     70 + </div>
     71 + <div class="col-6">
     72 + <div class="col-12 pt-3">
     73 + <input type="checkbox" id="activate_access_token" class="form-check-input" checked value="1">
     74 + <label for="activate_access_token" class="form-check-label">Activate access token</label>
     75 + </div>
     76 + <div class="col-12">
     77 + <input type="checkbox" id="store_refresh_token" class="form-check-input" value="1">
     78 + <label for="store_refresh_token" class="form-check-label">Store refresh token</label>
     79 + </div>
     80 + </div>
     81 + <div class="col-6">
     82 + <label for="refresh_token_id" class="form-label"><b>Refresh Token id *</b></label>
     83 + <input type="text" id="refresh_token_id" name="refresh_token_id" class="form-control" required>
     84 + </div>
     85 + <div class="col-12">
     86 + <button type="Button" class="btn btn-primary" onclick="refreshToAccessToken(refresh_token_id.value, resource_input.value, client_id_input.value, store_refresh_token.checked, activate_access_token.checked);reloadTables()">Submit</button>
     87 + </div>
     88 + </form>
     89 + </div>
     90 + </div>
     91 +</div>
     92 +<script>
     93 + getActiveAccessToken(document.getElementById("access_token_form").access_token_id);
     94 + getActiveRefreshToken(document.getElementById("refresh_to_access_token_form").refresh_token_id);
     95 +</script>
     96 +<div>
     97 + <br>
     98 + <h1>Access Tokens</h1>
     99 + <table id="access_tokens" class="display" style="table-layout:fixed; width:100%">
     100 + <thead>
     101 + <tr>
     102 + <th></th>
     103 + <th></th>
     104 + <th></th>
     105 + <th></th>
     106 + <th>ID</th>
     107 + <th>Issued</th>
     108 + <th>Expires</th>
     109 + <th>User</th>
     110 + <th>Resource</th>
     111 + <th>Description</th>
     112 + </tr>
     113 + </thead>
     114 + </table>
     115 +</div>
     116 +<script type="text/javascript" class="init">
     117 + // Populate the access_tokens table
     118 + let myTable = new DataTable('#access_tokens', {
     119 + ajax: {
     120 + url: '/api/list_access_tokens', dataSrc: ""
     121 + },
     122 + columns: [
     123 + {
     124 + className: 'dt-control',
     125 + orderable: false,
     126 + data: null,
     127 + defaultContent: '',
     128 + 'width': '20px'
     129 + },
     130 + {
     131 + className: 'active-control',
     132 + orderable: false,
     133 + data: null,
     134 + defaultContent: '<i class="fi fi-br-check" style="cursor: pointer"></i>',
     135 + 'width': '20px'
     136 + },
     137 + {
     138 + className: 'copy-control',
     139 + orderable: false,
     140 + data: null,
     141 + defaultContent: '<i class="fi fi-rr-copy-alt" style="cursor: pointer"></i>',
     142 + 'width': '20px'
     143 + },
     144 + {
     145 + className: 'delete-control',
     146 + orderable: false,
     147 + data: null,
     148 + defaultContent: '<i class="fi fi-rr-trash" style="cursor: pointer"></i>',
     149 + 'width': '20px'
     150 + },
     151 + { data: 'id', 'width': '30px' },
     152 + { data: 'issued_at', 'width': '150px' },
     153 + { data: 'expires_at', 'width': '150px' },
     154 + { data: 'user', 'width': '350px' },
     155 + { data: 'resource', 'width': '350px' },
     156 + { data: 'description' }
     157 + ],
     158 + order: [[4, 'desc']]
     159 + })
     160 + 
     161 + myTable.on('click', 'td.dt-control', function (e) {
     162 + let tr = e.target.closest('tr');
     163 + let row = myTable.row(tr);
     164 + 
     165 + if (row.child.isShown()) {
     166 + // This row is already open - close it
     167 + row.child.hide();
     168 + }
     169 + else {
     170 + // Open this row
     171 + row.child(format(row.data())).show();
     172 + }
     173 + });
     174 + 
     175 + myTable.on('click', 'td.active-control', function (e) {
     176 + let tr = e.target.closest('tr');
     177 + let row = myTable.row(tr);
     178 + setActiveAccessToken(row.data().id);
     179 + });
     180 + 
     181 + myTable.on('click', 'td.copy-control', function (e) {
     182 + let tr = e.target.closest('tr');
     183 + let row = myTable.row(tr);
     184 + copyToClipboard(row.data().accesstoken);
     185 + });
     186 + 
     187 + 
     188 + myTable.on('click', 'td.delete-control', function (e) {
     189 + let tr = e.target.closest('tr');
     190 + let row = myTable.row(tr);
     191 + if (!confirm("Are you sure you want to delete access token with ID " + row.data().id + "?")) { return }
     192 + deleteAccessToken(row.data().id);
     193 + $('#access_tokens').DataTable().ajax.reload(null, false);
     194 + });
     195 + 
     196 + function format(d) {
     197 + // `d` is the original data object for the row
     198 + let response = $.ajax({
     199 + type: "GET",
     200 + async: false,
     201 + url: "/api/decode_token/" + d.id
     202 + });
     203 + return (
     204 + //'<dl style = "word-wrap: break-word; word-break: break-all; width:100%">' +
     205 + '<dl>' +
     206 + '<dt>Raw Token:</dt>' +
     207 + '<dd><code>' +
     208 + d.accesstoken +
     209 + '</code></dd>' +
     210 + '<dt>Decoded Token:</dt>' +
     211 + '<dd><pre>' +
     212 + JSON.stringify(JSON.parse(response.responseText), undefined, 4) +
     213 + '</pre></dd>' +
     214 + '</dl>'
     215 + );
     216 + }
     217 +</script>
     218 + 
     219 +{%endblock content%}
  • ■ ■ ■ ■ ■ ■
    GraphSpy/templates/device_codes.html
     1 +{% extends 'layout.html'%}
     2 + 
     3 +{%block content%}
     4 + 
     5 +<div class="col-sm-5">
     6 + <h1>Generate Device Code</h1>
     7 + <form class="row g-3">
     8 + <div>
     9 + <label for="resource" class="form-label">Resource *</label>
     10 + <input list="resource_list" id="resource" class="form-control" required placeholder="https://graph.microsoft.com">
     11 + <datalist id="resource_list">
     12 + <option value="https://graph.microsoft.com">MSGraph</option>
     13 + <option value="https://graph.windows.net/">AAD Graph</option>
     14 + <option value="https://outlook.office365.com">Outlook</option>
     15 + <option value="https://api.spaces.skype.com/">MSTeams</option>
     16 + <option value="https://management.core.windows.net/">AzureCoreManagement</option>
     17 + <option value="https://outlook.office365.com">AzureManagement</option>
     18 + </datalist>
     19 + </div>
     20 + <div>
     21 + <label for="client_id" class="form-label">Client ID *</label>
     22 + <input list="client_id_list" id="client_id" class="form-control" required placeholder="d3590ed6-52b3-4102-aeff-aad2292ab01c">
     23 + <datalist id="client_id_list">
     24 + <option value="d3590ed6-52b3-4102-aeff-aad2292ab01c">Microsoft Office</option>
     25 + <option value="1fec8e78-bce4-4aaf-ab1b-5451cc387264">Microsoft Teams</option>
     26 + <option value="27922004-5251-4030-b22d-91ecd9a37ea4">Outlook Mobile</option>
     27 + <option value="b26aadf8-566f-4478-926f-589f601d9c74">OneDrive</option>
     28 + <option value="d326c1ce-6cc6-4de2-bebc-4591e5e13ef0">SharePoint</option>
     29 + <option value="00b41c95-dab0-4487-9791-b9d2c32c80f2">Office 365 Management</option>
     30 + <option value="04b07795-8ddb-461a-bbee-02f9e1bf7b46">Microsoft Azure CLI</option>
     31 + <option value="1950a258-227b-4e31-a9cf-717495945fc2">Microsoft Azure PowerShell</option>
     32 + </datalist>
     33 + </div>
     34 + <div>
     35 + <button type="button" class="btn btn-primary" onclick="generateDeviceCode(this.closest('form').resource.value,this.closest('form').client_id.value);">Submit</button>
     36 + </div>
     37 + </form>
     38 +</div>
     39 +<br>
     40 +<div>
     41 + <h1>Device Code List</h1>
     42 + <button type="button" class="btn btn-primary" onclick="restartDeviceCodePolling()">Restart Polling</button>
     43 + <table id="device_codes" class="display" style="table-layout:fixed; width:100%">
     44 + <thead>
     45 + <tr>
     46 + <th></th>
     47 + <th></th>
     48 + <th></th>
     49 + <th>ID</th>
     50 + <th>Generated At</th>
     51 + <th>Expires At</th>
     52 + <th>Last Polled At</th>
     53 + <th>User Code</th>
     54 + <th>Client ID</th>
     55 + <th>Status</th>
     56 + </tr>
     57 + </thead>
     58 + </table>
     59 +</div>
     60 +<script type="text/javascript" class="init">
     61 + // Populate the device_codes table
     62 + let myTable = new DataTable('#device_codes', {
     63 + ajax: {
     64 + url: '/api/list_device_codes', dataSrc: ""
     65 + },
     66 + columns: [
     67 + {
     68 + className: 'dt-control',
     69 + orderable: false,
     70 + data: null,
     71 + defaultContent: '',
     72 + 'width': '20px'
     73 + },
     74 + {
     75 + className: 'copy-control',
     76 + orderable: false,
     77 + data: null,
     78 + defaultContent: '<i class="fi fi-rr-copy-alt" style="cursor: pointer"></i>',
     79 + 'width': '20px'
     80 + },
     81 + {
     82 + className: 'delete-control',
     83 + orderable: false,
     84 + data: null,
     85 + defaultContent: '<i class="fi fi-rr-trash" style="cursor: pointer"></i>',
     86 + 'width': '20px'
     87 + },
     88 + { data: 'id', 'width': '30px' },
     89 + { data: 'generated_at', 'width': '150px' },
     90 + { data: 'expires_at', 'width': '150px' },
     91 + { data: 'last_poll', 'width': '150px' },
     92 + { data: 'user_code', 'width': '125px' },
     93 + { data: 'client_id', 'width': '310px' },
     94 + { data: 'status' }
     95 + ],
     96 + order: [[3, 'desc']]
     97 + })
     98 + 
     99 + myTable.on('click', 'td.dt-control', function (e) {
     100 + let tr = e.target.closest('tr');
     101 + let row = myTable.row(tr);
     102 + 
     103 + if (row.child.isShown()) {
     104 + // This row is already open - close it
     105 + row.child.hide();
     106 + }
     107 + else {
     108 + // Open this row
     109 + row.child(format(row.data())).show();
     110 + }
     111 + });
     112 + 
     113 + myTable.on('click', 'td.copy-control', function (e) {
     114 + let tr = e.target.closest('tr');
     115 + let row = myTable.row(tr);
     116 + copyToClipboard(row.data().user_code);
     117 + });
     118 + 
     119 + myTable.on('click', 'td.delete-control', function (e) {
     120 + let tr = e.target.closest('tr');
     121 + let row = myTable.row(tr);
     122 + if (!confirm("Are you sure you want to delete device code with ID " + row.data().id + "?")) { return }
     123 + deleteDeviceCode(row.data().id);
     124 + });
     125 + 
     126 + function format(d) {
     127 + return (
     128 + //'<dl style = "word-wrap: break-word; word-break: break-all; width:100%">' +
     129 + '<dl>' +
     130 + '<dt>Device Code:</dt>' +
     131 + '<dd><code>' +
     132 + d.device_code +
     133 + '</code></dd>' +
     134 + '</dl>'
     135 + );
     136 + }
     137 + 
     138 + function deleteDeviceCode(id) {
     139 + let response = $.ajax({
     140 + type: "GET",
     141 + async: false,
     142 + url: "/api/delete_device_code/" + id
     143 + });
     144 + $('#device_codes').DataTable().ajax.reload(null, false);
     145 + }
     146 + // Auto refresh the table every 5 seconds
     147 + setInterval(function () {
     148 + $('#device_codes').DataTable().ajax.reload(null, false)
     149 + }, 5000);
     150 +</script>
     151 +{%endblock content%}
  • ■ ■ ■ ■ ■ ■
    GraphSpy/templates/generic_search.html
     1 +{% extends 'layout.html'%}
     2 + 
     3 +{%block content%}
     4 + 
     5 +<br>
     6 +<div class="col-sm-9">
     7 + <h1>Generic MSGraph Search</h1>
     8 + <form id="search_form" class="row g-3">
     9 + <div class="col-sm-4">
     10 + <label for="search_type" class="form-label">Search type *</label>
     11 + <input list="search_type" name="search_type" class="form-control" placeholder="driveItem" required>
     12 + <datalist name="search_type" id="search_type">
     13 + <option value="drive">Document libraries</option>
     14 + <option value="driveItem">Files, folders, pages, and news</option>
     15 + <option value="message">Email message</option>
     16 + <option value="chatMessage">Teams messages</option>
     17 + <option value="event">Calendar events</option>
     18 + <option value="site">Sharepoint Sites</option>
     19 + <option value="list">Lists</option>
     20 + <option value="listItem">List Items</option>
     21 + </datalist>
     22 + </div>
     23 + <div class="col-sm-4">
     24 + <label for="search_limit" class="form-label">Limit *</label>
     25 + <input type="text" id="search_limit" name="search_limit" class="form-control" value="25" required>
     26 + <i class="form-text">Max 1000</i>
     27 + </div>
     28 + <div class="col-sm-4">
     29 + <label for="access_token_id" class="form-label">Access token id *</label>
     30 + <input type="text" id="access_token_id" name="access_token_id" class="form-control" required>
     31 + </div>
     32 + <div>
     33 + <label for="search_query" class="form-label">Search Query *</label>
     34 + <input type="text" id="search_query" name="search_query" class="form-control" value="*" required>
     35 + </div>
     36 + <div>
     37 + <button type="Button" class="btn btn-primary" onclick="generateTable()">Request</button>
     38 + </div>
     39 + </form>
     40 + <script>
     41 + getActiveAccessToken(document.getElementById("search_form").access_token_id)
     42 + </script>
     43 +</div>
     44 +<br>
     45 + 
     46 +<div>
     47 + <h2>Response</h2>
     48 + <table id="response_table" class="display" style="word-wrap: break-word; word-break: break-all; width:100%">
     49 + <thead>
     50 + <tr>
     51 + <th></th>
     52 + <th></th>
     53 + <th>Created User</th>
     54 + <th>Name</th>
     55 + <th>Summary</th>
     56 + <th>URL</th>
     57 + </tr>
     58 + </thead>
     59 + </table>
     60 +</div>
     61 +<script>
     62 + //generateTable();
     63 + // Populate the response_table table
     64 + function generateTable() {
     65 + if ($.fn.dataTable.isDataTable("#response_table")) {
     66 + // If the DataTable already exists, just reload it
     67 + $('#response_table').DataTable().ajax.reload(null, false);
     68 + } else {
     69 + let myTable = new DataTable('#response_table', {
     70 + ajax: {
     71 + type: "POST",
     72 + url: '/api/generic_graph_post',
     73 + dataSrc: function (json) {
     74 + if (json.hasOwnProperty("error")){
     75 + bootstrapAlert(`[${json.error.code}] ${json.error.message}`, "danger");
     76 + return [];
     77 + }
     78 + return json.value[0].hitsContainers[0].hits
     79 + },
     80 + data: function (d) {
     81 + d.graph_uri = "https://graph.microsoft.com/v1.0/search/query",
     82 + d.access_token_id = document.getElementById("search_form").access_token_id.value,
     83 + d.body = '{"requests": [{"entityTypes": ["' + document.getElementById("search_form").search_type.value + '"], "query": {"queryString": "' + document.getElementById("search_form").search_query.value + '"}, "from": 0, "size": ' + document.getElementById("search_form").search_limit.value + '}]}'
     84 + }
     85 + },
     86 + colReorder: true,
     87 + columns: [
     88 + {
     89 + className: 'dt-control',
     90 + orderable: false,
     91 + data: null,
     92 + defaultContent: '',
     93 + 'width': '20px'
     94 + },
     95 + {
     96 + className: 'action-control',
     97 + orderable: false,
     98 + data: null,
     99 + render: function (d, t, r) {
     100 + if (r.resource.hasOwnProperty("parentReference") && r.resource.parentReference.driveId) {
     101 + // Download icon
     102 + return '<i class="fi fi-br-download" style="cursor: pointer"></i>'
     103 + } else if (r.resource["@odata.type"] == "#microsoft.graph.drive") {
     104 + // Link icon
     105 + return '<i class="fi fi-br-link-alt" style="cursor: pointer"></i>'
     106 + }
     107 + // Question mark icon
     108 + return '<i class="fi fi-br-question" style="cursor: pointer"></i>'
     109 + },
     110 + 'width': '20px'
     111 + },
     112 + { data: 'resource.createdBy.user.displayName' },
     113 + { data: 'resource.name' },
     114 + {
     115 + data: 'summary',
     116 + render: function (d, t, r) {
     117 + return d.replaceAll("<c0>", "<b>").replaceAll("</c0>", "</b>")
     118 + }
     119 + },
     120 + { data: 'resource.webUrl' }
     121 + ]
     122 + })
     123 + 
     124 + myTable.on('click', 'td.dt-control', function (e) {
     125 + let tr = e.target.closest('tr');
     126 + let row = myTable.row(tr);
     127 + 
     128 + if (row.child.isShown()) {
     129 + // This row is already open - close it
     130 + row.child.hide();
     131 + }
     132 + else {
     133 + // Open this row
     134 + row.child(format(row.data())).show();
     135 + }
     136 + });
     137 + 
     138 + myTable.on('click', 'td.action-control', function (e) {
     139 + let tr = e.target.closest('tr');
     140 + let row = myTable.row(tr);
     141 + if (row.data().resource["@odata.type"] == "#microsoft.graph.driveItem") {
     142 + // This is a file
     143 + drive_id = row.data().resource.parentReference.driveId
     144 + item_id = row.data().resource.id
     145 + access_token_id = document.getElementById("search_form").access_token_id.value
     146 + graphDownload(drive_id, item_id, access_token_id);
     147 + } else if (row.data().resource["@odata.type"] == "#microsoft.graph.drive") {
     148 + // This is a drive
     149 + url = "/sharepoint?driveId=" + row.data().resource.id
     150 + window.open(url, '_blank');
     151 + } else {
     152 + alert("No action defined for this type of entity.")
     153 + }
     154 + });
     155 + }
     156 + return false;
     157 + }
     158 + 
     159 + function format(d) {
     160 + // `d` is the original data object for the row
     161 + return (
     162 + '<dl style = "word-wrap: break-word; word-break: break-all; width:100%">' +
     163 + '<dt>Raw File Info:</dt>' +
     164 + '<dd><pre>' +
     165 + JSON.stringify(d, undefined, 4) +
     166 + '</pre></dd>' +
     167 + '</dl>'
     168 + );
     169 + }
     170 +</script>
     171 +{%endblock content%}
  • ■ ■ ■ ■ ■ ■
    GraphSpy/templates/index.html
     1 +{% extends 'layout.html'%}
     2 + 
     3 +{%block content%}
     4 +<br>
     5 +<main>
     6 + <a href = "/">Home page</a>
     7 + <br>
     8 + <a href = "/device_codes">Device Codes</a>
     9 + <br>
     10 + <a href = "/refresh_tokens">Refresh Tokens</a>
     11 + <br>
     12 + <a href = "/access_tokens">Access Tokens</a>
     13 + <br>
     14 + <a href = "/graph_requests">Graph Requests</a>
     15 + <br>
     16 + <a href = "/generic_search">Generic Search</a>
     17 + <br>
     18 + <a href = "/recent_files">Recent Files</a>
     19 + <br>
     20 + <a href = "/shared_with_me">Files Shared With Me</a>
     21 + <br>
     22 + <a href = "/onedrive">OneDrive</a>
     23 + <br>
     24 + <a href = "/sharepoint_sites">SharePoint Sites</a>
     25 + <br>
     26 + <a href = "/sharepoint_drives">SharePoint Drives</a>
     27 + <br>
     28 + <a href = "/sharepoint">SharePoint Files</a>
     29 + <br>
     30 + <a href = "/outlook">Outlook</a>
     31 +</main>
     32 +{%endblock content%}
  • ■ ■ ■ ■ ■ ■
    GraphSpy/templates/layout.html
     1 +<!doctype html>
     2 +<html data-bs-theme="dark">
     3 +<head>
     4 + <meta name="viewport" content="width=device-width, initial-scale=1">
     5 + <!-- Bootstrap -->
     6 + <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
     7 + <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
     8 + <!-- UIcons by Flaticon (https://www.flaticon.com/uicons) -->
     9 + <link rel='stylesheet' href='https://cdn-uicons.flaticon.com/2.0.0/uicons-regular-rounded/css/uicons-regular-rounded.css'>
     10 + <link rel='stylesheet' href='https://cdn-uicons.flaticon.com/2.0.0/uicons-solid-rounded/css/uicons-solid-rounded.css'>
     11 + <link rel='stylesheet' href='https://cdn-uicons.flaticon.com/2.0.0/uicons-bold-rounded/css/uicons-bold-rounded.css'>
     12 + <!-- DataTables & jQuery -->
     13 + <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.13.5/css/jquery.dataTables.min.css">
     14 + <script type="text/javascript" language="javascript" src="https://code.jquery.com/jquery-3.5.1.js"></script>
     15 + <script type="text/javascript" language="javascript" src="https://cdn.datatables.net/1.11.1/js/jquery.dataTables.min.js"></script>
     16 + <script type="text/javascript" language="javascript" src="https://cdn.datatables.net/1.11.1/js/dataTables.bootstrap5.min.js"></script>
     17 + <!-- Static JS $ CSS -->
     18 + <script type="text/javascript" language="javascript" src="{{url_for('static', filename='js/theme.js')}}"></script>
     19 + <script type="text/javascript" language="javascript" src="{{url_for('static', filename='js/functions.js')}}"></script>
     20 + <link rel="stylesheet" type="text/css" href="{{url_for('static', filename='css/style.css')}}">
     21 + 
     22 + <title>{{title}}</title>
     23 +</head>
     24 +<body>
     25 + <!-- Navbar -->
     26 + <nav class="navbar sticky-top navbar-expand-md bg-body-tertiary">
     27 + <div class="container-fluid">
     28 + <a class="navbar-brand" href="/">GraphSpy</a>
     29 + <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
     30 + <span class="navbar-toggler-icon"></span>
     31 + </button>
     32 + <div class="collapse navbar-collapse" id="navbarNavDropdown">
     33 + <ul class="navbar-nav">
     34 + <li class="nav-item">
     35 + <a class="nav-link" href="/">Settings</a>
     36 + </li>
     37 + <li class="nav-item dropdown">
     38 + <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
     39 + Tokens
     40 + </a>
     41 + <ul class="dropdown-menu">
     42 + <li><a class="dropdown-item" href="{{url_for('refresh_tokens')}}">Refresh Tokens</a></li>
     43 + <li><a class="dropdown-item" href="{{url_for('access_tokens')}}">Access Tokens</a></li>
     44 + </ul>
     45 + </li>
     46 + <li class="nav-item">
     47 + <a class="nav-link" href="{{url_for('device_codes')}}">Device Codes</a>
     48 + </li>
     49 + <li class="nav-item dropdown">
     50 + <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
     51 + Graph
     52 + </a>
     53 + <ul class="dropdown-menu">
     54 + <li><a class="dropdown-item" href="{{url_for('graph_requests')}}">Generic Graph Requests</a></li>
     55 + <li><a class="dropdown-item" href="{{url_for('generic_search')}}">Generic Search</a></li>
     56 + </ul>
     57 + </li>
     58 + <li class="nav-item dropdown">
     59 + <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
     60 + Files
     61 + </a>
     62 + <ul class="dropdown-menu">
     63 + <li><a class="dropdown-item" href="{{url_for('recent_files')}}">Recent Files</a></li>
     64 + <li><a class="dropdown-item" href="{{url_for('shared_with_me')}}">Files Shared With Me</a></li>
     65 + <li><a class="dropdown-item" href="{{url_for('onedrive')}}">OneDrive</a></li>
     66 + </ul>
     67 + </li>
     68 + <li class="nav-item dropdown">
     69 + <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
     70 + SharePoint
     71 + </a>
     72 + <ul class="dropdown-menu">
     73 + <li><a class="dropdown-item" href="{{url_for('sharepoint_sites')}}">SharePoint Sites</a></li>
     74 + <li><a class="dropdown-item" href="{{url_for('sharepoint_drives')}}">SharePoint Drives</a></li>
     75 + <li><a class="dropdown-item" href="{{url_for('sharepoint')}}">SharePoint Files</a></li>
     76 + </ul>
     77 + </li>
     78 + <li class="nav-item">
     79 + <a class="nav-link" href="{{url_for('outlook')}}">Outlook</a>
     80 + </li>
     81 + <li class="nav-item">
     82 + <span class="d-inline-block" id="nav-teams-disabled" data-bs-toggle="popover" data-bs-trigger="hover focus" data-bs-content="Coming soon!" data-bs-placement="bottom" >
     83 + <a class="nav-link disabled">Teams</a>
     84 + </span>
     85 + </li>
     86 + </ul>
     87 + </div>
     88 + <div>
     89 + <ul class="navbar-nav justify-content-end">
     90 + <li class="nav-item">
     91 + <a class="nav-link" data-bs-toggle="offcanvas" href="#offcanvas-side-menu" role="button">Token Options</a>
     92 + </li>
     93 + </ul>
     94 + </div>
     95 + </div>
     96 + </nav>
     97 + <script>
     98 + const disabledPopover = new bootstrap.Popover(document.getElementById('nav-teams-disabled'))
     99 + </script>
     100 + <!-- Placeholders -->
     101 + <div id="alert_placeholder"></div>
     102 + <div id="toast_placeholder" class="toast-container position-fixed end-0 p-3"></div>
     103 + <!-- Side Menu -->
     104 + <div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvas-side-menu" aria-labelledby="offcanvasLabel">
     105 + <div class="offcanvas-header">
     106 + <h2 class="offcanvas-title" id="offcanvasLabel">Token Options</h2>
     107 + <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
     108 + </div>
     109 + <div class="offcanvas-body">
     110 + <div>
     111 + <form id="side_menu_refresh_to_access_token_form" class="row g-3">
     112 + <div class="col-12">
     113 + <label for="resource_input_side" class="form-label">Resource</label>
     114 + <input list="resource_list_side" id="resource_input_side" class="form-control" placeholder="https://graph.microsoft.com">
     115 + <datalist id="resource_list_side">
     116 + <option value="defined_in_token">Defined in Refresh Token</option>
     117 + <option value="https://graph.microsoft.com">MSGraph</option>
     118 + <option value="https://graph.windows.net/">AAD Graph</option>
     119 + <option value="https://outlook.office365.com">Outlook</option>
     120 + <option value="https://api.spaces.skype.com/">MSTeams</option>
     121 + <option value="https://management.core.windows.net/">AzureCoreManagement</option>
     122 + <option value="https://management.azure.com">AzureManagement</option>
     123 + </datalist>
     124 + </div>
     125 + <div class="col-12">
     126 + <label for="client_id_input_side" class="form-label">Client ID</label>
     127 + <input list="client_id_list_side" id="client_id_input_side" class="form-control" placeholder="d3590ed6-52b3-4102-aeff-aad2292ab01c">
     128 + <datalist id="client_id_list_side">
     129 + <option value="d3590ed6-52b3-4102-aeff-aad2292ab01c">Microsoft Office</option>
     130 + <option value="1fec8e78-bce4-4aaf-ab1b-5451cc387264">Microsoft Teams</option>
     131 + <option value="27922004-5251-4030-b22d-91ecd9a37ea4">Outlook Mobile</option>
     132 + <option value="b26aadf8-566f-4478-926f-589f601d9c74">OneDrive</option>
     133 + <option value="d326c1ce-6cc6-4de2-bebc-4591e5e13ef0">SharePoint</option>
     134 + <option value="00b41c95-dab0-4487-9791-b9d2c32c80f2">Office 365 Management</option>
     135 + <option value="04b07795-8ddb-461a-bbee-02f9e1bf7b46">Microsoft Azure CLI</option>
     136 + <option value="1950a258-227b-4e31-a9cf-717495945fc2">Microsoft Azure PowerShell</option>
     137 + </datalist>
     138 + </div>
     139 + <div class="col-12">
     140 + <button type="Button" class="btn btn-primary" onclick="refreshToAccessToken(refresh_token_id_side.value, resource_input_side.value, client_id_input_side.value, false, true);reloadTables()">Refresh and activate</button>
     141 + </div>
     142 + </form>
     143 + <br>
     144 + </div>
     145 + <div>
     146 + <h4>Access Token</h4>
     147 + <form id="side_menu_access_token_form" class="row row-cols-auto">
     148 + <div class="input-group">
     149 + <input type="text" id="access_token_id_side" class="form-control" size="1" required>
     150 + <button type="Button" class="btn btn-outline-primary" onclick="setActiveAccessToken(access_token_id_side.value);">Set active access token</button>
     151 + </div>
     152 + </form>
     153 + <div class="card text-bg-secondary mb-3 mt-3" id="access_token_card">
     154 + <div class="card-body">
     155 + <dl>
     156 + <dt>User</dt>
     157 + <dd id="access_token_info_user"></dd>
     158 + <dt>Resource</dt>
     159 + <dd id="access_token_info_resource"></dd>
     160 + <dt>Client ID</dt>
     161 + <dd id="access_token_info_clientid"></dd>
     162 + <dt>Scope</dt>
     163 + <dd id="access_token_info_scope"></dd>
     164 + <dt>Expires At</dt>
     165 + <dd id="access_token_info_expires"></dd>
     166 + </dl>
     167 + </div>
     168 + </div>
     169 + </div>
     170 + <div>
     171 + <h4>Refresh Token</h4>
     172 + <form id="side_menu_refresh_token_form" class="row row-cols-auto">
     173 + <div class="input-group">
     174 + <input type="text" id="refresh_token_id_side" class="form-control" size="1" required>
     175 + <button type="Button" class="btn btn-outline-primary" onclick="setActiveRefreshToken(refresh_token_id_side.value);obtainRefreshTokenInfo()">Set active refresh token</button>
     176 + </div>
     177 + </form>
     178 + <div class="card text-bg-secondary mb-3 mt-3" id="refresh_token_card">
     179 + <div class="card-body">
     180 + <dl>
     181 + <dt>User</dt>
     182 + <dd id="refresh_token_info_user"></dd>
     183 + <dt>Resource</dt>
     184 + <dd id="refresh_token_info_resource"></dd>
     185 + <dt>Tenant ID</dt>
     186 + <dd id="refresh_token_info_tenant_id"></dd>
     187 + <dt>Foci</dt>
     188 + <dd id="refresh_token_info_foci"></dd>
     189 + <dt>Description</dt>
     190 + <dd id="refresh_token_info_description"></dd>
     191 + </dl>
     192 + </div>
     193 + </div>
     194 + </div>
     195 + </div>
     196 + </div>
     197 + <script>
     198 + // Obtain access token info
     199 + function obtainAccessTokenInfo() {
     200 + getActiveAccessToken(document.getElementById("side_menu_access_token_form").access_token_id_side);
     201 + if (document.getElementById("side_menu_access_token_form").access_token_id_side.value == 0) {
     202 + $('#access_token_info_user').text("");
     203 + $('#access_token_info_resource').text("");
     204 + $('#resource_input_side').val("");
     205 + $('#access_token_info_clientid').text("");
     206 + $('#client_id_input_side').val("");
     207 + $('#access_token_info_scope').text("");
     208 + $('#access_token_info_expires').text("");
     209 + $("#access_token_card").removeClass(function (index, className) {
     210 + return (className.match(/(^|\s)text-bg-\S+/g) || []).join(' ');
     211 + });
     212 + $("#access_token_card").addClass("text-bg-secondary")
     213 + return
     214 + }
     215 + let response_a = $.ajax({
     216 + type: "GET",
     217 + async: false,
     218 + url: "/api/decode_token/" + document.getElementById("side_menu_access_token_form").access_token_id_side.value
     219 + });
     220 + if (response_a.responseText.startsWith("[Error]")) {
     221 + bootstrapToast("Access Token Error", response_a.responseText);
     222 + return
     223 + }
     224 + access_token_info = JSON.parse(response_a.responseText);
     225 + $('#access_token_info_user').text(access_token_info.unique_name);
     226 + $('#access_token_info_resource').text(access_token_info.aud);
     227 + $('#resource_input_side').val(access_token_info.aud);
     228 + $('#access_token_info_clientid').text(access_token_info.appid);
     229 + $('#client_id_input_side').val(access_token_info.appid);
     230 + $('#access_token_info_scope').text(access_token_info.scp);
     231 + var expiry_date = new Date(access_token_info.exp * 1000);
     232 + $('#access_token_info_expires').text(`${expiry_date.toLocaleDateString()} ${expiry_date.toTimeString().split(" ")[0]}`);
     233 + $("#access_token_card").removeClass(function (index, className) {
     234 + return (className.match(/(^|\s)text-bg-\S+/g) || []).join(' ');
     235 + });
     236 + if (expiry_date > new Date()) {
     237 + $("#access_token_card").addClass("text-bg-success")
     238 + } else {
     239 + $("#access_token_card").addClass("text-bg-danger")
     240 + }
     241 + 
     242 + }
     243 + // Obtain refresh token info
     244 + function obtainRefreshTokenInfo() {
     245 + getActiveRefreshToken(document.getElementById("side_menu_refresh_token_form").refresh_token_id_side);
     246 + if (document.getElementById("side_menu_refresh_token_form").refresh_token_id_side.value == 0) {
     247 + $('#refresh_token_info_user').text("");
     248 + $('#refresh_token_info_resource').text("");
     249 + $('#refresh_token_info_description').text("");
     250 + $('#refresh_token_info_tenant_id').text("");
     251 + $('#refresh_token_info_foci').text("");
     252 + return
     253 + }
     254 + let response_r = $.ajax({
     255 + type: "GET",
     256 + async: false,
     257 + url: "/api/get_refresh_token/" + document.getElementById("side_menu_refresh_token_form").refresh_token_id_side.value
     258 + });
     259 + refresh_token_info = JSON.parse(response_r.responseText);
     260 + $('#refresh_token_info_user').text(refresh_token_info.user);
     261 + $('#refresh_token_info_resource').text(refresh_token_info.resource);
     262 + $('#refresh_token_info_description').text(refresh_token_info.description);
     263 + $('#refresh_token_info_tenant_id').text(refresh_token_info.tenant_id);
     264 + $('#refresh_token_info_foci').text(refresh_token_info.foci);
     265 + }
     266 + obtainAccessTokenInfo();
     267 + obtainRefreshTokenInfo();
     268 + </script>
     269 + <!-- Template Content -->
     270 + <div class="container-fluid">
     271 + {%block content%}
     272 + {%endblock content%}
     273 + </div>
     274 + <!-- Enable/Disable Datatable Error Messages -->
     275 + <script>
     276 + var table_error_messages = "{{ config['table_error_messages'] }}";
     277 + $.fn.dataTable.ext.errMode = 'none';
     278 + if (table_error_messages == "enabled") {
     279 + // Catch datatable error messages and convert them to toast messages
     280 + $('table').on('error.dt', function (e, settings, techNote, message) {
     281 + bootstrapToast("DataTables Error", message);
     282 + });
     283 + }
     284 + </script>
     285 +</body>
     286 +</html>
  • ■ ■ ■ ■ ■ ■
    GraphSpy/templates/outlook.html
     1 +{% extends 'layout.html'%}
     2 + 
     3 +{%block content%}
     4 + 
     5 +<br>
     6 +<div class="col-md-6">
     7 + <h1>Outlook</h1>
     8 + <form id="access_token_form" class="row g-3">
     9 + <div class="col-md-6">
     10 + <div class="input-group">
     11 + <input type="text" id="access_token_id" name="access_token_id" class="form-control" required>
     12 + <button type="Button" class="btn btn-outline-primary" onclick="fillAccessToken(this.closest('form'))">Set access token</button>
     13 + </div>
     14 + </div>
     15 + </form>
     16 + <br>
     17 + <form id="outlook_form" action="https://outlook.office365.com/owa/" method="POST" target="_blank" class="row g-3">
     18 + <div>
     19 + <label for="id_token" class="form-label">Access Token *</label>
     20 + <textarea type="text" id="id_token" name="id_token" class="form-control" rows=5 required placeholder="eyJ..."></textarea>
     21 + </div>
     22 + <div>
     23 + <input type="hidden" id="code" name="code" value="anything">
     24 + <button type="submit" class="btn btn-primary" id="submit">Open outlook</button>
     25 + </div>
     26 + </form>
     27 + <script>
     28 + function fillAccessToken(form) {
     29 + let response = $.ajax({
     30 + type: "GET",
     31 + async: false,
     32 + url: "/api/get_access_token/" + form.access_token_id.value
     33 + });
     34 + document.getElementById("outlook_form").id_token.value = JSON.parse(response.responseText).accesstoken
     35 + };
     36 + getActiveAccessToken(document.getElementById("access_token_form").access_token_id)
     37 + fillAccessToken(document.getElementById("access_token_form"))
     38 + </script>
     39 +</div>
     40 +{%endblock content%}
  • ■ ■ ■ ■ ■ ■
    GraphSpy/templates/recent_files.html
     1 +{% extends 'layout.html'%}
     2 + 
     3 +{%block content%}
     4 + 
     5 +<br>
     6 +<div class="col-6">
     7 + <h1>Recent Files</h1>
     8 + <form id="recent_file_form" class="row g-3">
     9 + <div class="col-6">
     10 + <label for="access_token_id" class="form-label">Access token id *</label>
     11 + <input type="text" id="access_token_id" name="access_token_id" class="form-control" required>
     12 + </div>
     13 + <div>
     14 + <button type="Button" class="btn btn-primary" onclick="generateTable()">Request</button>
     15 + </div>
     16 + </form>
     17 + <script>
     18 + getActiveAccessToken(document.getElementById("recent_file_form").access_token_id)
     19 + </script>
     20 +</div>
     21 +<br>
     22 +<div>
     23 + <h2>Files</h2>
     24 + <table id="response_table" class="display" style="word-wrap: break-word; word-break: break-all; width:100%">
     25 + <thead>
     26 + <tr>
     27 + <th></th>
     28 + <th></th>
     29 + <th>Created</th>
     30 + <th>Last Modified</th>
     31 + <th>File Name</th>
     32 + <th>File Size</th>
     33 + <th>URL</th>
     34 + </tr>
     35 + </thead>
     36 + </table>
     37 +</div>
     38 +<script>
     39 + generateTable();
     40 + // Populate the response_table table
     41 + function generateTable() {
     42 + let myTable = new DataTable('#response_table', {
     43 + "destroy": true,
     44 + ajax: {
     45 + type: "POST",
     46 + url: '/api/generic_graph',
     47 + dataSrc: function (json) {
     48 + if (json.hasOwnProperty("error")) {
     49 + bootstrapAlert(`[${json.error.code}] ${json.error.message}`, "danger");
     50 + return [];
     51 + }
     52 + return json.value
     53 + },
     54 + data: { "graph_uri": "https://graph.microsoft.com/v1.0/me/drive/recent", "access_token_id": document.getElementById("recent_file_form").access_token_id.value }
     55 + },
     56 + columns: [
     57 + {
     58 + className: 'dt-control',
     59 + orderable: false,
     60 + data: null,
     61 + defaultContent: '',
     62 + 'width': '20px'
     63 + },
     64 + {
     65 + className: 'action-control',
     66 + orderable: false,
     67 + data: null,
     68 + render: function (d, t, r) {
     69 + if (r.folder) {
     70 + // Folder icon
     71 + return '<i class="fi fi-sr-folder-open" style="cursor: pointer"></i>'
     72 + } else if (r.file) {
     73 + // Download icon
     74 + return '<i class="fi fi-br-download" style="cursor: pointer"></i>'
     75 + }
     76 + // Question mark icon
     77 + return '<i class="fi fi-br-question" style="cursor: pointer"></i>'
     78 + },
     79 + 'width': '20px'
     80 + },
     81 + {
     82 + data: 'createdDateTime',
     83 + width: '150px'
     84 + },
     85 + {
     86 + data: 'lastModifiedDateTime',
     87 + width: '150px'
     88 + },
     89 + { data: 'name' },
     90 + {
     91 + data: 'size',
     92 + width: '100px'
     93 + },
     94 + { data: 'webUrl' }
     95 + ],
     96 + order: [[2, 'desc']]
     97 + })
     98 + 
     99 + myTable.on('click', 'td.dt-control', function (e) {
     100 + let tr = e.target.closest('tr');
     101 + let row = myTable.row(tr);
     102 + 
     103 + if (row.child.isShown()) {
     104 + // This row is already open - close it
     105 + row.child.hide();
     106 + }
     107 + else {
     108 + // Open this row
     109 + row.child(format(row.data())).show();
     110 + }
     111 + 
     112 + });
     113 + 
     114 + myTable.on('click', 'td.action-control', function (e) {
     115 + let tr = e.target.closest('tr');
     116 + let row = myTable.row(tr);
     117 + drive_id = row.data().remoteItem.parentReference.driveId
     118 + item_id = row.data().id
     119 + access_token_id = document.getElementById("recent_file_form").access_token_id.value
     120 + graphDownload(drive_id, item_id, access_token_id);
     121 + });
     122 + return false;
     123 + }
     124 + 
     125 + function format(d) {
     126 + // `d` is the original data object for the row
     127 + return (
     128 + '<dl style = "word-wrap: break-word; word-break: break-all; width:100%">' +
     129 + '<dt>Raw File Info:</dt>' +
     130 + '<dd><pre>' +
     131 + JSON.stringify(d, undefined, 4) +
     132 + '</pre></dd>' +
     133 + '</dl>'
     134 + );
     135 + }
     136 +</script>
     137 +{%endblock content%}
  • ■ ■ ■ ■ ■ ■
    GraphSpy/templates/refresh_tokens.html
     1 +{% extends 'layout.html'%}
     2 + 
     3 +{%block content%}
     4 + 
     5 +<br>
     6 +<div class="col-md-6">
     7 + <h1>Add Refresh Token</h1>
     8 + <form action="/api/add_refresh_token" method="post" class="row g-3">
     9 + <div>
     10 + <label for="refreshtoken" class="form-label"><b>Refresh token *</b></label><br>
     11 + <textarea type="text" id="refreshtoken" name="refreshtoken" class="form-control" rows=5 required placeholder="0..."></textarea>
     12 + </div>
     13 + <div class="col-md-6">
     14 + <label for="user" class="form-label">User</label>
     15 + <input type="text" id="user" name="user" class="form-control" placeholder="[email protected]">
     16 + </div>
     17 + <div class="col-md-6">
     18 + <label for="tenant_domain" class="form-label"><b>Tenant Domain/ID *</b></label>
     19 + <input type="text" id="tenant_domain" name="tenant_domain" class="form-control" required placeholder="example.com">
     20 + </div>
     21 + <div class="col-md-6">
     22 + <label for="resource" class="form-label"><b>Resource *</b></label>
     23 + <input list="resource" name="resource" class="form-control" required placeholder="https://graph.microsoft.com">
     24 + <datalist name="resource" id="resource">
     25 + <option value="https://graph.microsoft.com">MSGraph</option>
     26 + <option value="https://graph.windows.net/">AAD Graph</option>
     27 + <option value="https://outlook.office365.com">Outlook</option>
     28 + <option value="https://api.spaces.skype.com/">MSTeams</option>
     29 + <option value="https://management.core.windows.net/">AzureCoreManagement</option>
     30 + <option value="https://management.azure.com">AzureManagement</option>
     31 + </datalist>
     32 + </div>
     33 + <div class="col-md-6">
     34 + <label for="description" class="form-label">Description</label>
     35 + <input type="text" id="description" name="description" class="form-control" placeholder="My First Token">
     36 + </div>
     37 + <div class="col-12">
     38 + <input type="checkbox" id="foci" name="foci" value="1" class="form-check-input">
     39 + <label for="foci" class="form-check-label">Family of Client ID (FOCI)?</label>
     40 + </div>
     41 + <div class="col-12">
     42 + <button type="submit" class="btn btn-primary">Submit</button>
     43 + </div>
     44 + </form>
     45 +</div>
     46 +<br>
     47 +<div class="col-md-6">
     48 + <h1>Active Refresh Token</h1>
     49 + <form id="refresh_token_form" class="row row-cols-auto">
     50 + <div>
     51 + <label for="refresh_token_id" class="col-form-label">Active Refresh Token</label>
     52 + </div>
     53 + <div>
     54 + <input type="text" id="refresh_token_id" size="5" name="refresh_token_id" class="form-control">
     55 + </div>
     56 + <div>
     57 + <button type="Button" class="btn btn-primary" onclick="setActiveRefreshToken(refresh_token_id.value)">Set active token</button>
     58 + </div>
     59 + </form>
     60 +</div>
     61 +<script>
     62 + getActiveRefreshToken(document.getElementById("refresh_token_form").refresh_token_id);
     63 +</script>
     64 +<br>
     65 +<div>
     66 + <h1>Refresh Tokens</h1>
     67 + <table id="refresh_tokens" class="display" style="table-layout:fixed; width:100%">
     68 + <thead>
     69 + <tr>
     70 + <th></th>
     71 + <th></th>
     72 + <th></th>
     73 + <th></th>
     74 + <th>ID</th>
     75 + <th>Stored At</th>
     76 + <th>User</th>
     77 + <th>Tenant ID</th>
     78 + <th>Resource</th>
     79 + <th>Foci</th>
     80 + <th>Description</th>
     81 + </tr>
     82 + </thead>
     83 + </table>
     84 +</div>
     85 +<script type="text/javascript" class="init">
     86 + // Populate the refresh_tokens table
     87 + let myTable = new DataTable('#refresh_tokens', {
     88 + ajax: {
     89 + url: '/api/list_refresh_tokens', dataSrc: ""
     90 + },
     91 + columns: [
     92 + {
     93 + className: 'dt-control',
     94 + orderable: false,
     95 + data: null,
     96 + defaultContent: '',
     97 + 'width': '20px'
     98 + },
     99 + {
     100 + className: 'active-control',
     101 + orderable: false,
     102 + data: null,
     103 + defaultContent: '<i class="fi fi-br-check" style="cursor: pointer"></i>',
     104 + 'width': '20px'
     105 + },
     106 + {
     107 + className: 'copy-control',
     108 + orderable: false,
     109 + data: null,
     110 + defaultContent: '<i class="fi fi-rr-copy-alt" style="cursor: pointer"></i>',
     111 + 'width': '20px'
     112 + },
     113 + {
     114 + className: 'delete-control',
     115 + orderable: false,
     116 + data: null,
     117 + defaultContent: '<i class="fi fi-rr-trash" style="cursor: pointer"></i>',
     118 + 'width': '20px'
     119 + },
     120 + { data: 'id', 'width': '30px' },
     121 + { data: 'stored_at', 'width': '150px' },
     122 + { data: 'user', 'width': '300px' },
     123 + { data: 'tenant_id', 'width': '300px' },
     124 + { data: 'resource', 'width': '300px' },
     125 + { data: 'foci', 'width': '30px' },
     126 + { data: 'description' }
     127 + ],
     128 + order: [[4, 'desc']]
     129 + })
     130 + 
     131 + myTable.on('click', 'td.dt-control', function (e) {
     132 + let tr = e.target.closest('tr');
     133 + let row = myTable.row(tr);
     134 + 
     135 + if (row.child.isShown()) {
     136 + // This row is already open - close it
     137 + row.child.hide();
     138 + }
     139 + else {
     140 + // Open this row
     141 + row.child(format(row.data())).show();
     142 + }
     143 + });
     144 + 
     145 + myTable.on('click', 'td.active-control', function (e) {
     146 + let tr = e.target.closest('tr');
     147 + let row = myTable.row(tr);
     148 + setActiveRefreshToken(row.data().id);
     149 + });
     150 + 
     151 + myTable.on('click', 'td.copy-control', function (e) {
     152 + let tr = e.target.closest('tr');
     153 + let row = myTable.row(tr);
     154 + copyToClipboard(row.data().refreshtoken);
     155 + });
     156 + 
     157 + myTable.on('click', 'td.delete-control', function (e) {
     158 + let tr = e.target.closest('tr');
     159 + let row = myTable.row(tr);
     160 + if (!confirm("Are you sure you want to delete refresh token with ID " + row.data().id + "?")) { return }
     161 + deleteRefreshToken(row.data().id);
     162 + $('#refresh_tokens').DataTable().ajax.reload(null, false);
     163 + });
     164 + 
     165 + function format(d) {
     166 + return (
     167 + //'<dl style = "word-wrap: break-word; word-break: break-all; width:100%">' +
     168 + '<dl>' +
     169 + '<dt>Raw Token:</dt>' +
     170 + '<dd><code>' +
     171 + d.refreshtoken +
     172 + '</code></dd>' +
     173 + '</dl>'
     174 + );
     175 + }
     176 +</script>
     177 +{%endblock content%}
  • ■ ■ ■ ■ ■ ■
    GraphSpy/templates/requests.html
     1 +{% extends 'layout.html'%}
     2 + 
     3 +{%block content%}
     4 + 
     5 +<br>
     6 +<div class="col-md-11">
     7 + <h1>Generic Graph Request</h1>
     8 + <form id="graph_generic_form" class="row g-3">
     9 + <div class="col-3">
     10 + <label for="method" class="form-label">Method *</label>
     11 + <input list="method" name="method" value="GET" class="form-control" required>
     12 + <datalist name="method" id="method">
     13 + <option value="GET">GET</option>
     14 + <option value="POST">POST</option>
     15 + </datalist>
     16 + </div>
     17 + <div class="col-3">
     18 + <label for="access_token_id" class="form-label">Access token id *</label>
     19 + <input type="text" id="access_token_id" name="access_token_id" class="form-control" required>
     20 + </div>
     21 + <div>
     22 + <label for="graph_uri" class="form-label">Graph Uri *</label>
     23 + <input type="text" id="graph_uri" name="graph_uri" value="https://graph.microsoft.com/v1.0/" class="form-control" required>
     24 + </div>
     25 + <div>
     26 + <label for="body" class="form-label">Body</label>
     27 + <textarea type="text" id="body" name="body" value="" rows=5 class="form-control">{"requests": [{"entityTypes": ["drive"], "query": {"queryString": "*"}, "from": 0, "size": 10}]}</textarea>
     28 + </div>
     29 + <div>
     30 + <button type="Button" class="btn btn-primary" onclick="generateRequest()">Request</button>
     31 + </div>
     32 + </form>
     33 + <script>
     34 + getActiveAccessToken(document.getElementById("graph_generic_form").access_token_id)
     35 + </script>
     36 +</div>
     37 +<br>
     38 +<div class="row" id="response-card" style="display: none">
     39 + <div class="col-auto">
     40 + <div class="card mt-3">
     41 + <div class="card-header" style="text-align: center">
     42 + <b>Response</b>
     43 + <i class="fi fi-rr-copy-alt" id="copy-icon" style="cursor: pointer; float: right"></i>
     44 + </div>
     45 + <div class="card-body">
     46 + <pre id="response_json" class="mb-0"></pre>
     47 + </div>
     48 + </div>
     49 + </div>
     50 +</div>
     51 +<script>
     52 + function generateRequest() {
     53 + let graph_form = document.getElementById("graph_generic_form");
     54 + let response;
     55 + if (graph_form.method.value == "GET") {
     56 + response = $.ajax({
     57 + type: "POST",
     58 + async: false,
     59 + url: "/api/generic_graph",
     60 + dataSrc: "",
     61 + data: { "graph_uri": graph_form.graph_uri.value, "access_token_id": graph_form.access_token_id.value },
     62 + });
     63 + } else if (graph_form.method.value == "POST") {
     64 + response = $.ajax({
     65 + type: "POST",
     66 + async: false,
     67 + url: "/api/generic_graph_post",
     68 + dataSrc: "",
     69 + data: { "graph_uri": graph_form.graph_uri.value, "access_token_id": graph_form.access_token_id.value, "body": graph_form.body.value },
     70 + });
     71 + } else {
     72 + alert("Invalid method.");
     73 + return false;
     74 + }
     75 + let responseJSON = JSON.parse(response.responseText);
     76 + $("#response_json").text(JSON.stringify(responseJSON, undefined, 4))
     77 + $("#response-card").show()
     78 + }
     79 + $("#response-card").on('click', 'i#copy-icon', function (e) {
     80 + copyToClipboard($("#response_json").text())
     81 + })
     82 +</script>
     83 + {%endblock content%}
     84 + 
  • ■ ■ ■ ■ ■ ■
    GraphSpy/templates/settings.html
     1 +{% extends 'layout.html'%}
     2 + 
     3 +{%block content%}
     4 + 
     5 +<div class="row g-5">
     6 + <div class="col-6">
     7 + <h1>Settings</h1>
     8 + <div class="row g-4">
     9 + <div class="dropdown col-12">
     10 + <button class="btn btn-primary dropdown-toggle" id="bd-theme" type="button" aria-expanded="false" data-bs-toggle="dropdown" data-bs-display="static" aria-label="Toggle theme (dark)">
     11 + Select theme
     12 + </button>
     13 + <ul class="dropdown-menu" aria-labelledby="bd-theme-text">
     14 + <li>
     15 + <button type="button" class="dropdown-item" data-bs-theme-value="light" aria-pressed="false">
     16 + <i class="fi fi-rr-sun"></i> Light Mode
     17 + </button>
     18 + </li>
     19 + <li>
     20 + <button type="button" class="dropdown-item active" data-bs-theme-value="dark" aria-pressed="true">
     21 + <i class="fi fi-rr-moon-stars"></i> Dark Mode
     22 + </button>
     23 + </li>
     24 + <li>
     25 + <button type="button" class="dropdown-item" data-bs-theme-value="auto" aria-pressed="false">
     26 + <i class="fi fi-rr-magic-wand"></i> Auto
     27 + </button>
     28 + </li>
     29 + </ul>
     30 + </div>
     31 + <div class="dropdown col-12">
     32 + <button class="btn btn-primary dropdown-toggle" id="dt-error-message-dropdown" type="button" data-bs-toggle="dropdown" aria-expanded="false">
     33 + DataTable Error Messages
     34 + </button>
     35 + <ul class="dropdown-menu">
     36 + <li><button class="dropdown-item" id="dt-error-message-button-enabled" type="button" onclick="setTableErorMessages('enabled')">Enabled</button></li>
     37 + <li><button class="dropdown-item" id="dt-error-message-button-disabled" type="button" onclick="setTableErorMessages('disabled')">Disabled</button></li>
     38 + </ul>
     39 + </div>
     40 + </div>
     41 + </div>
     42 + <div class="col-auto ms-auto">
     43 + <div class="card mt-3">
     44 + <div class="card-header" style="text-align: center"><b>Persistent Settings</b></div>
     45 + <div class="card-body">
     46 + <pre id="settings_json" class="mb-0"></pre>
     47 + <script>
     48 + function obtainPersistentSettings() {
     49 + response = $.ajax({
     50 + type: "GET",
     51 + async: false,
     52 + url: "/api/get_settings"
     53 + });
     54 + let responseJSON = JSON.parse(response.responseText);
     55 + $("#settings_json").text(JSON.stringify(responseJSON, undefined, 4));
     56 + }
     57 + obtainPersistentSettings();
     58 + </script>
     59 + </div>
     60 + </div>
     61 + </div>
     62 + <div class="col-lg-9">
     63 + <h1>Databases</h1>
     64 + <div class="col-md-6">
     65 + <div class="input-group">
     66 + <span class="input-group-text">Database Folder</span>
     67 + <input class="form-control" id="db_folder" type="text" value="{{ config['graph_spy_db_folder'] }}" readonly>
     68 + </div>
     69 + </div>
     70 + <table id="databases" class="display" style="table-layout:fixed; width:100%">
     71 + <thead>
     72 + <tr>
     73 + <th></th>
     74 + <th></th>
     75 + <th></th>
     76 + <th>Last Modified</th>
     77 + <th>Size</th>
     78 + <th>State</th>
     79 + <th>Database</th>
     80 + </tr>
     81 + </thead>
     82 + </table>
     83 + </div>
     84 + <div class="col-lg-3">
     85 + <h1>New Database</h1>
     86 + <form class="row g-3">
     87 + <div>
     88 + <label for="database" class="form-label">Database Name *</label>
     89 + <input type="text" id="database" name="database" class="form-control" required placeholder="database.db">
     90 + </div>
     91 + <div>
     92 + <button type="button" class="btn btn-primary" onclick="createDatabase(this.closest('form').database.value);$('#databases').DataTable().ajax.reload(null, false);">Create Database</button>
     93 + </div>
     94 + </form>
     95 + </div>
     96 +</div>
     97 +<script type="text/javascript" class="init">
     98 + // Populate the databases table
     99 + let myTable = new DataTable('#databases', {
     100 + ajax: {
     101 + url: '/api/list_databases', dataSrc: ""
     102 + },
     103 + columns: [
     104 + {
     105 + className: 'active-control',
     106 + orderable: false,
     107 + data: null,
     108 + defaultContent: '<i class="fi fi-br-check" style="cursor: pointer"></i>',
     109 + 'width': '20px'
     110 + },
     111 + {
     112 + className: 'duplicate-control',
     113 + orderable: false,
     114 + data: null,
     115 + defaultContent: '<i class="fi fi-rr-duplicate" style="cursor: pointer"></i>',
     116 + 'width': '20px'
     117 + },
     118 + {
     119 + className: 'delete-control',
     120 + orderable: false,
     121 + data: null,
     122 + defaultContent: '<i class="fi fi-rr-trash" style="cursor: pointer"></i>',
     123 + 'width': '20px'
     124 + },
     125 + { data: 'last_modified', 'width': '150px' },
     126 + { data: 'size', 'width': '75px' },
     127 + { data: 'state', 'width': '75px' },
     128 + { data: 'name' }
     129 + ],
     130 + order: [[5, 'asc'], [3, 'desc']]
     131 + })
     132 + 
     133 + myTable.on('click', 'td.active-control', function (e) {
     134 + let tr = e.target.closest('tr');
     135 + let row = myTable.row(tr);
     136 + activateDatabase(row.data().name);
     137 + $('#databases').DataTable().ajax.reload(null, false);
     138 + });
     139 + 
     140 + myTable.on('click', 'td.duplicate-control', function (e) {
     141 + let tr = e.target.closest('tr');
     142 + let row = myTable.row(tr);
     143 + duplicateDatabase(row.data().name);
     144 + $('#databases').DataTable().ajax.reload(null, false);
     145 + });
     146 + 
     147 + myTable.on('click', 'td.delete-control', function (e) {
     148 + let tr = e.target.closest('tr');
     149 + let row = myTable.row(tr);
     150 + if (!confirm("Are you sure you want to delete database '" + row.data().name + "'?")) { return }
     151 + deleteDatabase(row.data().name);
     152 + $('#databases').DataTable().ajax.reload(null, false);
     153 + });
     154 + 
     155 + // Activate the correct option in the dt-error-message-dropdown on page load
     156 + var table_error_messages = "{{ config['table_error_messages'] }}";
     157 + if (table_error_messages == "disabled") {
     158 + $('#dt-error-message-button-disabled').addClass("active")
     159 + } else {
     160 + $('#dt-error-message-button-enabled').addClass("active")
     161 + }
     162 +</script>
     163 + {%endblock content%}
     164 + 
  • ■ ■ ■ ■ ■ ■
    GraphSpy/templates/shared_with_me.html
     1 +{% extends 'layout.html'%}
     2 + 
     3 +{%block content%}
     4 + 
     5 +<br>
     6 +<div class="col-6">
     7 + <h1>Files Shared with Me</h1>
     8 + <form id="shared_file_form" class="row g-3">
     9 + <div class="col-6">
     10 + <label for="access_token_id" class="form-label">Access token id *</label>
     11 + <input type="text" id="access_token_id" name="access_token_id" class="form-control" required>
     12 + </div>
     13 + <div>
     14 + <button type="Button" class="btn btn-primary" onclick="generateTable()">Request</button>
     15 + </div>
     16 + </form>
     17 + <script>
     18 + getActiveAccessToken(document.getElementById("shared_file_form").access_token_id)
     19 + </script>
     20 +</div>
     21 +<br>
     22 +<div>
     23 + <h2>Files</h2>
     24 + <table id="response_table" class="display" style="word-wrap: break-word; word-break: break-all; width:100%">
     25 + <thead>
     26 + <tr>
     27 + <th></th>
     28 + <th></th>
     29 + <th>Created</th>
     30 + <th>Last Modified</th>
     31 + <th>File Name</th>
     32 + <th>File Size</th>
     33 + <th>URL</th>
     34 + </tr>
     35 + </thead>
     36 + </table>
     37 +</div>
     38 +<script>
     39 + generateTable();
     40 + // Populate the response_table table
     41 + function generateTable() {
     42 + let myTable = new DataTable('#response_table', {
     43 + "destroy": true,
     44 + ajax: {
     45 + type: "POST",
     46 + url: '/api/generic_graph',
     47 + dataSrc: function (json) {
     48 + if (json.hasOwnProperty("error")) {
     49 + bootstrapAlert(`[${json.error.code}] ${json.error.message}`, "danger");
     50 + return [];
     51 + }
     52 + return json.value
     53 + },
     54 + data: { "graph_uri": "https://graph.microsoft.com/v1.0/me/drive/sharedWithMe", "access_token_id": document.getElementById("shared_file_form").access_token_id.value }
     55 + },
     56 + columns: [
     57 + {
     58 + className: 'dt-control',
     59 + orderable: false,
     60 + data: null,
     61 + defaultContent: '',
     62 + 'width': '20px'
     63 + },
     64 + {
     65 + className: 'action-control',
     66 + orderable: false,
     67 + data: null,
     68 + render: function (d, t, r) {
     69 + if (r.folder) {
     70 + // Folder icon
     71 + return '<i class="fi fi-sr-folder-open" style="cursor: pointer"></i>'
     72 + } else if (r.file) {
     73 + // Download icon
     74 + return '<i class="fi fi-br-download" style="cursor: pointer"></i>'
     75 + }
     76 + // Question mark icon
     77 + return '<i class="fi fi-br-question" style="cursor: pointer"></i>'
     78 + },
     79 + 'width': '20px'
     80 + },
     81 + {
     82 + data: 'createdDateTime',
     83 + width: '150px'
     84 + },
     85 + {
     86 + data: 'lastModifiedDateTime',
     87 + width: '150px'
     88 + },
     89 + { data: 'name' },
     90 + {
     91 + data: 'size',
     92 + width: '100px'
     93 + },
     94 + { data: 'webUrl' }
     95 + ],
     96 + order: [[2, 'desc']]
     97 + })
     98 + 
     99 + myTable.on('click', 'td.dt-control', function (e) {
     100 + let tr = e.target.closest('tr');
     101 + let row = myTable.row(tr);
     102 + 
     103 + if (row.child.isShown()) {
     104 + // This row is already open - close it
     105 + row.child.hide();
     106 + }
     107 + else {
     108 + // Open this row
     109 + row.child(format(row.data())).show();
     110 + }
     111 + 
     112 + });
     113 + 
     114 + myTable.on('click', 'td.action-control', function (e) {
     115 + let tr = e.target.closest('tr');
     116 + let row = myTable.row(tr);
     117 + drive_id = row.data().remoteItem.parentReference.driveId
     118 + item_id = row.data().id
     119 + access_token_id = document.getElementById("shared_file_form").access_token_id.value
     120 + graphDownload(drive_id, item_id, access_token_id);
     121 + });
     122 + return false;
     123 + }
     124 + 
     125 + function format(d) {
     126 + // `d` is the original data object for the row
     127 + return (
     128 + '<dl style = "word-wrap: break-word; word-break: break-all; width:100%">' +
     129 + '<dt>Raw File Info:</dt>' +
     130 + '<dd><pre>' +
     131 + JSON.stringify(d, undefined, 4) +
     132 + '</pre></dd>' +
     133 + '</dl>'
     134 + );
     135 + }
     136 +</script>
     137 +{%endblock content%}
  • ■ ■ ■ ■ ■
    GraphSpy/version.txt
     1 +1.0.0
  • ■ ■ ■ ■ ■ ■
    LICENSE.txt
     1 +BSD 3-Clause License
     2 + 
     3 +Copyright (c) [year], [fullname]
     4 + 
     5 +Redistribution and use in source and binary forms, with or without
     6 +modification, are permitted provided that the following conditions are met:
     7 + 
     8 +1. Redistributions of source code must retain the above copyright notice, this
     9 + list of conditions and the following disclaimer.
     10 + 
     11 +2. Redistributions in binary form must reproduce the above copyright notice,
     12 + this list of conditions and the following disclaimer in the documentation
     13 + and/or other materials provided with the distribution.
     14 + 
     15 +3. Neither the name of the copyright holder nor the names of its
     16 + contributors may be used to endorse or promote products derived from
     17 + this software without specific prior written permission.
     18 + 
     19 +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
     20 +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
     21 +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
     22 +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
     23 +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
     24 +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
     25 +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
     26 +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
     27 +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     28 +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     29 + 
  • ■ ■ ■ ■ ■ ■
    README.md
     1 +# GraphSpy
     2 + 
     3 + 
     4 +```
     5 + ________ _________
     6 + / / by RedByte1337 __ / /
     7 + / _____/___________ ______ | |__ / _____/_____ ______
     8 +/ \ __\_ __ \__ \ \____ \| | \ \_____ \\____ \ | |
     9 +\ \_\ \ | \/ __ \| |_> | \ \/ \ |_> \___ |
     10 + \______ /__| |____ | __/|___| /_______ / ___/ ____|
     11 + \/ \/|__| \/ \/|__| \/
     12 +```
     13 + 
     14 +# Table of Contents
     15 + 
     16 +- [GraphSpy](#certipy)
     17 +- [Table of Contents](#table-of-contents)
     18 +- [Quick Start](#quick-start)
     19 +- [Features](#features)
     20 +- [Upcoming Features](#upcoming-features)
     21 +- [Credits](#credits)
     22 + 
     23 +# Quick Start
     24 + 
     25 +Simply running GraphSpy without any command line arguments will launch GraphSpy and make it available at `http://127.0.0.1:5000` by default.
     26 + 
     27 +Use the `-i` and `-p` arguments to modify the interface and port to listen on.
     28 + 
     29 +```bash
     30 +# Run GraphSpy on http://192.168.0.10
     31 +python .\GraphSpy.py -i 192.168.0.10 -p 80
     32 +# Run GraphSpy on port 8080 on all interfaces
     33 +python .\GraphSpy.py -i 0.0.0.0 -p 8080
     34 +```
     35 + 
     36 +# Features
     37 + 
     38 +## Access and Refresh Tokens
     39 + 
     40 +Store your access and refresh tokens for multiple users and scopes in one location.
     41 + 
     42 +![Access Tokens](images/access_tokens_1.png)
     43 + 
     44 +![Refresh Tokens](images/refresh_tokens.png)
     45 + 
     46 +Easily switch between them or request new access tokens from any page.
     47 + 
     48 +![Token Side Bar](images/token_side_bar_1.png)
     49 + 
     50 +## Device Codes
     51 + 
     52 +Easily create and poll multiple device codes at once. If a user used the device code to authenticate, GraphSpy will automatically store the access and refresh token in its database.
     53 + 
     54 +![Device Codes](images/device_codes.png)
     55 + 
     56 +## Files and SharePoint
     57 + 
     58 +Browse through files and folders in the user's OneDrive or any accessible SharePoint site through an intuitive file explorer interface.
     59 + 
     60 +Of course, files can also be directly downloaded.
     61 + 
     62 +![OneDrive](images/onedrive_2.png)
     63 + 
     64 +Additionally, list the user's recently accessed files or files shared with the user.
     65 + 
     66 +![Recent Files](images/recent_files.png)
     67 + 
     68 +## Outlook
     69 + 
     70 +Open the user's Outlook with a single click using just an Outlook access token (FOCI)!
     71 + 
     72 +![Outlook GraphSpy](images/outlook_1.png)
     73 + 
     74 +![Outlook](images/outlook_2.png)
     75 + 
     76 +## Graph Searching
     77 + 
     78 +Search for keywords through all Microsoft 365 applications using the Microsoft 365 API.
     79 + 
     80 +For instance, use this to search for any files or emails containing keywords such as "password", "secret", ...
     81 + 
     82 +![Graph Search](images/graph_search_2.png)
     83 + 
     84 +## Generic Graph Requests
     85 + 
     86 +Perform any other MS Graph requests and display the raw response.
     87 + 
     88 +![Graph Request](images/generic_graph_requests.png)
     89 + 
     90 +## Multiple Databases
     91 + 
     92 +GraphSpy supports multiple databases. This is useful when working on multiple assessments at once to keep your tokens and device codes organized.
     93 + 
     94 +![Graph Request](images/settings.png)
     95 + 
     96 +## Dark Mode
     97 + 
     98 +Use the dark mode by default, or switch to light mode.
     99 + 
     100 +# Upcoming Features
     101 + 
     102 +* Upload, Delete and Rename Files
     103 +* More authentication options
     104 + * Password, ESTSAuth Cookie, PRT, ...
     105 +* Advanced token customization options and optional v2 API support (CAE)
     106 +* Automatic Access Token Refreshing
     107 +* Set a custom user agent
     108 +* Microsoft Teams
     109 + * Sadly, most MSGrapgh scopes required for Microsoft Teams can not be obtained through a FOCI client id, limiting the usecases where it could be accessed.
     110 + * So the best option would be to use the Skype API, which is a FOCI resource, although this API is not documented by Microsoft or intended for public use
     111 +* Azure AD
     112 + * List Users, Groups, Applications, Devices, Conditional Access Policies, ...
     113 +* Cleaner exception handling
     114 + * While this should not have any direct impact on the user, edge cases might currently throw exceptions to the GraphSpy output instead of handling them in a cleaner way.
     115 + 
     116 +# Credits
     117 + 
     118 +The main motivation for creating GraphSpy was the lack of an easy to use way to perform post-compromise activities targetting Office365 applications (such as Outlook, Microsoft Teams, OneDrive, SharePoint, ...) with just an access token.
     119 + 
     120 +While several command-line tools existed which provided some basic functionality, none of them came close to the intuitive interactive experience which the original applications provide (such as the file explorer-like interface of OneDrive and SharePoint).
     121 + 
     122 +However, a lot of previous research was done by countless other persons (specifically regarding Device Code Phishing, which lead to the initial requirement for such a tool in the first place).
     123 + 
     124 +* Acknowledgements
     125 + * [TokenTactics](https://github.com/rvrsh3ll/TokenTactics) and [TokenTacticsV2](https://github.com/f-bader/TokenTacticsV2)
     126 + * [AADInternals](https://github.com/Gerenios/AADInternals)
     127 + * [Introducing a new phishing technique for compromising Office 365 accounts](https://aadinternals.com/post/phishing/)
     128 + * [The Art of the Device Code Phish](https://0xboku.com/2021/07/12/ArtOfDeviceCodePhish.html)
     129 + * [GraphRunner](https://github.com/dafthack/GraphRunner) is a PowerShell tool with a lot of similar features, which was released while GraphSpy was already in development. Regardless, both tools still have their distinguishing factors.
     130 +* Assets
     131 + * UIcons by [Flaticon](https://www.flaticon.com/uicons)
  • images/access_tokens_1.png
  • images/access_tokens_2.png
  • images/device_codes.png
  • images/files_shared_with_me.png
  • images/generic_graph_requests.png
  • images/graph_search_1.png
  • images/graph_search_2.png
  • images/onedrive_1.png
  • images/onedrive_2.png
  • images/outlook_1.png
  • images/outlook_2.png
  • images/recent_files.png
  • images/refresh_tokens.png
  • images/settings.png
  • images/sharepoint_drives.png
  • images/sharepoint_files.png
  • images/sharepoint_sites.png
  • images/token_side_bar_1.png
  • images/token_side_bar_2.png
  • ■ ■ ■ ■ ■ ■
    requirements.txt
     1 +Flask>=3.0.0
     2 +PyJWT
     3 +Requests
     4 + 
  • ■ ■ ■ ■ ■ ■
    setup.py
     1 +from setuptools import setup
     2 + 
     3 +with open("README.md", 'r', encoding='utf-8') as f:
     4 + readme = f.read()
     5 + 
     6 +with open("GraphSpy/version.txt", 'r', encoding='utf-8') as f:
     7 + __version__ = f.read()
     8 + 
     9 +with open('requirements.txt', 'r', encoding='utf-8') as f:
     10 + requirements = [x.strip() for x in f.readlines()]
     11 + 
     12 +setup(
     13 + name='GraphSpy',
     14 + version=__version__,
     15 + author='RedByte1337',
     16 + url='https://github.com/RedByte1337/GraphSpy',
     17 + description="Initial Access and Post-Exploitation Tool for AAD and O365 with a browser-based GUI",
     18 + long_description=readme,
     19 + long_description_content_type="text/markdown",
     20 + install_requires=requirements,
     21 + package_data={'': ['static/**/*','templates/*','version.txt']},
     22 + include_package_data=True,
     23 + packages=[
     24 + "GraphSpy"
     25 + ],
     26 + entry_points={
     27 + "console_scripts": ["graphspy=GraphSpy.GraphSpy:main"],
     28 + }
     29 +)
Please wait...
Page is in error, reload to recover