diff --git a/analyse_postman.py b/analyse_postman.py new file mode 100644 index 0000000..94b6c84 --- /dev/null +++ b/analyse_postman.py @@ -0,0 +1,185 @@ +import json +import sys +import re +from collections import defaultdict + + +JWT_REGEX = re.compile(r"eyJ[\w-]{10,}\.[\w-]{10,}\.[\w-]{10,}") + +SENSITIVE_NAMES = { + "token", "access_token", "refresh_token", "id_token", "password", "passwd", + "secret", "client_secret", "credential", "credentials", "api_key", "apikey", + "authorization", "auth", "x-api-key", "signature", "jwt", "bearer", "session" +} + + +ONLY_BEARER = {"token", "value", "string"} + + +def mask_value(val: str, head: int = 4, tail: int = 4) -> str: + s = str(val) + if len(s) <= head + tail: + return "***" + return f"{s[:head]}***{s[-tail:]}" + + +def looks_like_secret(val: str) -> bool: + if not isinstance(val, str): + return False + if JWT_REGEX.search(val): + return True + if val.strip().lower().startswith("bearer "): + return True + if val.strip().lower().startswith("basic "): + return True + compact = val.replace("-", "").replace("_", "").replace("=", "") + if len(compact) >= 32 and compact.isalnum(): + return True + return False + + +def analyse_postman_collection(file_path): + with open(file_path, 'r', encoding='utf-8') as f: + collection = json.load(f) + + endpoints_seen = [] + stats = defaultdict(lambda: { + "GET_params": 0, + "POST_params": 0, + "methods": set(), + "secret_names": set() + }) + + def flag_kv(endpoint: str, key_name: str, value): + key_l = str(key_name).lower() if key_name is not None else "" + val_s = str(value) if value is not None else "" + name_is_sensitive = any(n in key_l for n in SENSITIVE_NAMES) + value_is_secret = looks_like_secret(val_s) + if name_is_sensitive or value_is_secret: + if key_l in ONLY_BEARER or value_is_secret: + stats[endpoint]["secret_names"].add("bearer") + else: + stats[endpoint]["secret_names"].add(key_name) + + def analyse_auth(endpoint: str, auth_obj): + if not isinstance(auth_obj, dict): + return + a_type = auth_obj.get('type') + if a_type and isinstance(auth_obj.get(a_type), list): + for p in auth_obj.get(a_type, []): + k = p.get('key') + v = p.get('value') + flag_kv(endpoint, k, v) + + def scan_headers(endpoint: str, headers): + if not isinstance(headers, list): + return + for h in headers: + name = h.get('key') or h.get('name') + val = h.get('value') + flag_kv(endpoint, name, val) + + def scan_queries(endpoint: str, queries): + if not isinstance(queries, list): + return + for q in queries: + flag_kv(endpoint, q.get('key'), q.get('value')) + + def scan_body(endpoint: str, body): + if not isinstance(body, dict): + return + mode = body.get('mode') + if mode == 'urlencoded': + for p in body.get('urlencoded', []): + flag_kv(endpoint, p.get('key'), p.get('value')) + elif mode == 'formdata': + for p in body.get('formdata', []): + if p.get('type') == 'file': + continue + flag_kv(endpoint, p.get('key'), p.get('value')) + elif mode == 'raw': + raw = body.get('raw') + if isinstance(raw, str): + try: + obj = json.loads(raw) + scan_structure(endpoint, obj) + except Exception: + if looks_like_secret(raw): + flag_kv(endpoint, 'raw', raw) + + def scan_structure(endpoint: str, data): + if isinstance(data, dict): + if set(data.keys()) >= {"key", "value"}: + flag_kv(endpoint, data.get('key'), data.get('value')) + for k, v in data.items(): + flag_kv(endpoint, k, v if isinstance(v, str) else None) + scan_structure(endpoint, v) + elif isinstance(data, list): + for v in data: + scan_structure(endpoint, v) + elif isinstance(data, str): + if looks_like_secret(data): + flag_kv(endpoint, None, data) + + def build_endpoint(url_obj): + if isinstance(url_obj, dict): + path = "/".join(url_obj.get('path', [])).strip('/') + return f"/{path}" if path else "/" + return str(url_obj) + + def extract_items(items, inherited_auth=None): + for item in items or []: + current_auth = item.get('auth', inherited_auth) + if 'request' in item: + request = item['request'] + method = request.get('method', 'UNKNOWN') + url = request.get('url', {}) + endpoint = build_endpoint(url) + endpoints_seen.append(endpoint) + + stats[endpoint]["methods"].add(method) + + queries = url.get('query', []) if isinstance(url, dict) else [] + if method == 'GET': + stats[endpoint]["GET_params"] += len(queries) + elif method == 'POST': + stats[endpoint]["POST_params"] += 1 + + scan_queries(endpoint, queries) + scan_headers(endpoint, request.get('header', [])) + effective_auth = request.get('auth', current_auth) + analyse_auth(endpoint, effective_auth) + scan_body(endpoint, request.get('body', {})) + scan_structure(endpoint, request) + + extract_items(item.get('item'), current_auth) + + extract_items(collection.get('item'), collection.get('auth')) + + header = "Endpoint | GET | POST | Methods | Secrets" + print(header) + print("-" * len(header)) + + total_get = 0 + total_post = 0 + + for endpoint in sorted(stats.keys()): + data = stats[endpoint] + methods = ",".join(sorted(data['methods'])) if data['methods'] else "-" + secret_names = sorted(list(data['secret_names'])) + secrets_cell = f"{len(secret_names)}" if not secret_names else f"{len(secret_names)} [" + ", ".join(secret_names[:4]) + ("]" if len(secret_names) <= 4 else ", …]") + print(f"{endpoint:<38} | {data['GET_params']:^3} | {data['POST_params']:^4} | {methods:<13} | {secrets_cell}") + total_get += data['GET_params'] + total_post += data['POST_params'] + + unique_endpoints = len(set(endpoints_seen)) + + print(f"Total unique endpoints: {unique_endpoints} (GET: {total_get} POST: {total_post})") + + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python analyse_postman.py ") + sys.exit(1) + + analyse_postman_collection(sys.argv[1]) diff --git a/analyse_postman.py b/analyse_postman.py new file mode 100644 index 0000000..94b6c84 --- /dev/null +++ b/analyse_postman.py @@ -0,0 +1,185 @@ +import json +import sys +import re +from collections import defaultdict + + +JWT_REGEX = re.compile(r"eyJ[\w-]{10,}\.[\w-]{10,}\.[\w-]{10,}") + +SENSITIVE_NAMES = { + "token", "access_token", "refresh_token", "id_token", "password", "passwd", + "secret", "client_secret", "credential", "credentials", "api_key", "apikey", + "authorization", "auth", "x-api-key", "signature", "jwt", "bearer", "session" +} + + +ONLY_BEARER = {"token", "value", "string"} + + +def mask_value(val: str, head: int = 4, tail: int = 4) -> str: + s = str(val) + if len(s) <= head + tail: + return "***" + return f"{s[:head]}***{s[-tail:]}" + + +def looks_like_secret(val: str) -> bool: + if not isinstance(val, str): + return False + if JWT_REGEX.search(val): + return True + if val.strip().lower().startswith("bearer "): + return True + if val.strip().lower().startswith("basic "): + return True + compact = val.replace("-", "").replace("_", "").replace("=", "") + if len(compact) >= 32 and compact.isalnum(): + return True + return False + + +def analyse_postman_collection(file_path): + with open(file_path, 'r', encoding='utf-8') as f: + collection = json.load(f) + + endpoints_seen = [] + stats = defaultdict(lambda: { + "GET_params": 0, + "POST_params": 0, + "methods": set(), + "secret_names": set() + }) + + def flag_kv(endpoint: str, key_name: str, value): + key_l = str(key_name).lower() if key_name is not None else "" + val_s = str(value) if value is not None else "" + name_is_sensitive = any(n in key_l for n in SENSITIVE_NAMES) + value_is_secret = looks_like_secret(val_s) + if name_is_sensitive or value_is_secret: + if key_l in ONLY_BEARER or value_is_secret: + stats[endpoint]["secret_names"].add("bearer") + else: + stats[endpoint]["secret_names"].add(key_name) + + def analyse_auth(endpoint: str, auth_obj): + if not isinstance(auth_obj, dict): + return + a_type = auth_obj.get('type') + if a_type and isinstance(auth_obj.get(a_type), list): + for p in auth_obj.get(a_type, []): + k = p.get('key') + v = p.get('value') + flag_kv(endpoint, k, v) + + def scan_headers(endpoint: str, headers): + if not isinstance(headers, list): + return + for h in headers: + name = h.get('key') or h.get('name') + val = h.get('value') + flag_kv(endpoint, name, val) + + def scan_queries(endpoint: str, queries): + if not isinstance(queries, list): + return + for q in queries: + flag_kv(endpoint, q.get('key'), q.get('value')) + + def scan_body(endpoint: str, body): + if not isinstance(body, dict): + return + mode = body.get('mode') + if mode == 'urlencoded': + for p in body.get('urlencoded', []): + flag_kv(endpoint, p.get('key'), p.get('value')) + elif mode == 'formdata': + for p in body.get('formdata', []): + if p.get('type') == 'file': + continue + flag_kv(endpoint, p.get('key'), p.get('value')) + elif mode == 'raw': + raw = body.get('raw') + if isinstance(raw, str): + try: + obj = json.loads(raw) + scan_structure(endpoint, obj) + except Exception: + if looks_like_secret(raw): + flag_kv(endpoint, 'raw', raw) + + def scan_structure(endpoint: str, data): + if isinstance(data, dict): + if set(data.keys()) >= {"key", "value"}: + flag_kv(endpoint, data.get('key'), data.get('value')) + for k, v in data.items(): + flag_kv(endpoint, k, v if isinstance(v, str) else None) + scan_structure(endpoint, v) + elif isinstance(data, list): + for v in data: + scan_structure(endpoint, v) + elif isinstance(data, str): + if looks_like_secret(data): + flag_kv(endpoint, None, data) + + def build_endpoint(url_obj): + if isinstance(url_obj, dict): + path = "/".join(url_obj.get('path', [])).strip('/') + return f"/{path}" if path else "/" + return str(url_obj) + + def extract_items(items, inherited_auth=None): + for item in items or []: + current_auth = item.get('auth', inherited_auth) + if 'request' in item: + request = item['request'] + method = request.get('method', 'UNKNOWN') + url = request.get('url', {}) + endpoint = build_endpoint(url) + endpoints_seen.append(endpoint) + + stats[endpoint]["methods"].add(method) + + queries = url.get('query', []) if isinstance(url, dict) else [] + if method == 'GET': + stats[endpoint]["GET_params"] += len(queries) + elif method == 'POST': + stats[endpoint]["POST_params"] += 1 + + scan_queries(endpoint, queries) + scan_headers(endpoint, request.get('header', [])) + effective_auth = request.get('auth', current_auth) + analyse_auth(endpoint, effective_auth) + scan_body(endpoint, request.get('body', {})) + scan_structure(endpoint, request) + + extract_items(item.get('item'), current_auth) + + extract_items(collection.get('item'), collection.get('auth')) + + header = "Endpoint | GET | POST | Methods | Secrets" + print(header) + print("-" * len(header)) + + total_get = 0 + total_post = 0 + + for endpoint in sorted(stats.keys()): + data = stats[endpoint] + methods = ",".join(sorted(data['methods'])) if data['methods'] else "-" + secret_names = sorted(list(data['secret_names'])) + secrets_cell = f"{len(secret_names)}" if not secret_names else f"{len(secret_names)} [" + ", ".join(secret_names[:4]) + ("]" if len(secret_names) <= 4 else ", …]") + print(f"{endpoint:<38} | {data['GET_params']:^3} | {data['POST_params']:^4} | {methods:<13} | {secrets_cell}") + total_get += data['GET_params'] + total_post += data['POST_params'] + + unique_endpoints = len(set(endpoints_seen)) + + print(f"Total unique endpoints: {unique_endpoints} (GET: {total_get} POST: {total_post})") + + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python analyse_postman.py ") + sys.exit(1) + + analyse_postman_collection(sys.argv[1]) diff --git a/xss.svg b/xss.svg new file mode 100644 index 0000000..666a7e1 --- /dev/null +++ b/xss.svg @@ -0,0 +1,8 @@ + + + + + +