diff --git a/bbot/modules/base.py b/bbot/modules/base.py index c24fd9251a..f3ba6eb211 100644 --- a/bbot/modules/base.py +++ b/bbot/modules/base.py @@ -1176,10 +1176,17 @@ async def api_request(self, *args, **kwargs): return r + def _prepare_api_iter_req(self, url, page, page_size, offset, **requests_kwargs): + """ + Default function for preparing an API request for iterating through paginated data. + """ + url = self.helpers.safe_format(url, page=page, page_size=page_size, offset=offset) + return url, requests_kwargs + def _api_response_is_success(self, r): return r.is_success - async def api_page_iter(self, url, page_size=100, json=True, next_key=None, **requests_kwargs): + async def api_page_iter(self, url, page_size=100, _json=True, next_key=None, iter_key=None, **requests_kwargs): """ An asynchronous generator function for iterating through paginated API data. @@ -1192,6 +1199,7 @@ async def api_page_iter(self, url, page_size=100, json=True, next_key=None, **re page_size (int, optional): The number of items per page. Defaults to 100. json (bool, optional): If True, attempts to deserialize the response content to a JSON object. Defaults to True. next_key (callable, optional): A function that takes the last page's data and returns the URL for the next page. Defaults to None. + iter_key (callable, optional): A function that builds each new request based on the page number, page size, and offset. Defaults to a simple implementation that autoreplaces {page} and {page_size} in the url. **requests_kwargs: Arbitrary keyword arguments that will be forwarded to the HTTP request function. Yields: @@ -1214,6 +1222,8 @@ async def api_page_iter(self, url, page_size=100, json=True, next_key=None, **re page = 1 offset = 0 result = None + if iter_key is None: + iter_key = self._prepare_api_iter_req while 1: if result and callable(next_key): try: @@ -1222,13 +1232,13 @@ async def api_page_iter(self, url, page_size=100, json=True, next_key=None, **re self.debug(f"Failed to extract next page of results from {url}: {e}") self.debug(traceback.format_exc()) else: - new_url = self.helpers.safe_format(url, page=page, page_size=page_size, offset=offset) - result = await self.api_request(new_url, **requests_kwargs) + new_url, new_kwargs = iter_key(url, page, page_size, offset, **requests_kwargs) + result = await self.api_request(new_url, **new_kwargs) if result is None: self.verbose(f"api_page_iter() got no response for {url}") break try: - if json: + if _json: result = result.json() yield result except Exception: diff --git a/bbot/modules/dehashed.py b/bbot/modules/dehashed.py index d8ef7d003e..9fbefa9bb8 100644 --- a/bbot/modules/dehashed.py +++ b/bbot/modules/dehashed.py @@ -90,7 +90,7 @@ async def query(self, domain): url = f"{self.base_url}?query={query}&size=10000&page=" + "{page}" page = 0 num_entries = 0 - agen = self.api_page_iter(url=url, auth=self.auth, headers=self.headers, json=False) + agen = self.api_page_iter(url=url, auth=self.auth, headers=self.headers, _json=False) async for result in agen: result_json = {} with suppress(Exception): diff --git a/bbot/modules/dockerhub.py b/bbot/modules/dockerhub.py index 23f45ab756..2b8efafb5d 100644 --- a/bbot/modules/dockerhub.py +++ b/bbot/modules/dockerhub.py @@ -64,7 +64,7 @@ async def handle_social(self, event): async def get_repos(self, username): repos = [] url = f"{self.api_url}/repositories/{username}?page_size=25&page=" + "{page}" - agen = self.api_page_iter(url, json=False) + agen = self.api_page_iter(url, _json=False) try: async for r in agen: if r is None: diff --git a/bbot/modules/github_codesearch.py b/bbot/modules/github_codesearch.py index 4c838a1c5b..9cb8fb5fef 100644 --- a/bbot/modules/github_codesearch.py +++ b/bbot/modules/github_codesearch.py @@ -42,7 +42,7 @@ async def handle_event(self, event): async def query(self, query): repos = {} url = f"{self.base_url}/search/code?per_page=100&type=Code&q={self.helpers.quote(query)}&page=" + "{page}" - agen = self.api_page_iter(url, headers=self.headers, json=False) + agen = self.api_page_iter(url, headers=self.headers, _json=False) num_results = 0 try: async for r in agen: diff --git a/bbot/modules/github_org.py b/bbot/modules/github_org.py index 229b4671bd..604b9feb27 100644 --- a/bbot/modules/github_org.py +++ b/bbot/modules/github_org.py @@ -108,7 +108,7 @@ async def handle_event(self, event): async def query_org_repos(self, query): repos = [] url = f"{self.base_url}/orgs/{self.helpers.quote(query)}/repos?per_page=100&page=" + "{page}" - agen = self.api_page_iter(url, json=False) + agen = self.api_page_iter(url, _json=False) try: async for r in agen: if r is None: @@ -136,7 +136,7 @@ async def query_org_repos(self, query): async def query_org_members(self, query): members = [] url = f"{self.base_url}/orgs/{self.helpers.quote(query)}/members?per_page=100&page=" + "{page}" - agen = self.api_page_iter(url, json=False) + agen = self.api_page_iter(url, _json=False) try: async for r in agen: if r is None: @@ -164,7 +164,7 @@ async def query_org_members(self, query): async def query_user_repos(self, query): repos = [] url = f"{self.base_url}/users/{self.helpers.quote(query)}/repos?per_page=100&page=" + "{page}" - agen = self.api_page_iter(url, json=False) + agen = self.api_page_iter(url, _json=False) try: async for r in agen: if r is None: diff --git a/bbot/modules/github_workflows.py b/bbot/modules/github_workflows.py index 53712c32e1..d77e2c03c8 100644 --- a/bbot/modules/github_workflows.py +++ b/bbot/modules/github_workflows.py @@ -92,7 +92,7 @@ async def handle_event(self, event): async def get_workflows(self, owner, repo): workflows = [] url = f"{self.base_url}/repos/{owner}/{repo}/actions/workflows?per_page=100&page=" + "{page}" - agen = self.api_page_iter(url, json=False) + agen = self.api_page_iter(url, _json=False) try: async for r in agen: if r is None: diff --git a/bbot/modules/postman.py b/bbot/modules/postman.py index a7374a5715..f6eafc6ff9 100644 --- a/bbot/modules/postman.py +++ b/bbot/modules/postman.py @@ -10,52 +10,64 @@ class postman(postman): "created_date": "2024-09-07", "author": "@domwhewell-sage", } - + options = {"api_key": ""} + options_desc = {"api_key": "Postman API Key"} reject_wildcards = False async def handle_event(self, event): # Handle postman profile if event.type == "SOCIAL": - await self.handle_profile(event) + owner = event.data.get("profile_name", "") + in_scope_workspaces = await self.process_workspaces(user=owner) elif event.type == "ORG_STUB": - await self.handle_org_stub(event) - - async def handle_profile(self, event): - profile_name = event.data.get("profile_name", "") - self.verbose(f"Searching for postman workspaces, collections, requests belonging to {profile_name}") - for item in await self.query(profile_name): - workspace = item["document"] - name = workspace["slug"] - profile = workspace["publisherHandle"] - if profile_name.lower() == profile.lower(): - self.verbose(f"Got {name}") - workspace_url = f"{self.html_url}/{profile}/{name}" + owner = event.data + in_scope_workspaces = await self.process_workspaces(org=owner) + if in_scope_workspaces: + for workspace in in_scope_workspaces: + repo_url = workspace["url"] + repo_name = workspace["repo_name"] + if event.type == "SOCIAL": + context = f'{{module}} searched postman.com for workspaces belonging to "{owner}" and found "{repo_name}" at {{event.type}}: {repo_url}' + elif event.type == "ORG_STUB": + context = f'{{module}} searched postman.com for "{owner}" and found matching workspace "{repo_name}" at {{event.type}}: {repo_url}' await self.emit_event( - {"url": workspace_url}, + {"url": repo_url}, "CODE_REPOSITORY", tags="postman", parent=event, - context=f'{{module}} searched postman.com for workspaces belonging to "{profile_name}" and found "{name}" at {{event.type}}: {workspace_url}', + context=context, ) - async def handle_org_stub(self, event): - org_name = event.data - self.verbose(f"Searching for any postman workspaces, collections, requests for {org_name}") - for item in await self.query(org_name): - workspace = item["document"] - name = workspace["slug"] - profile = workspace["publisherHandle"] - self.verbose(f"Got {name}") - workspace_url = f"{self.html_url}/{profile}/{name}" - await self.emit_event( - {"url": workspace_url}, - "CODE_REPOSITORY", - tags="postman", - parent=event, - context=f'{{module}} searched postman.com for "{org_name}" and found matching workspace "{name}" at {{event.type}}: {workspace_url}', - ) + async def process_workspaces(self, user=None, org=None): + in_scope_workspaces = [] + owner = user or org + if owner: + self.verbose(f"Searching for postman workspaces, collections, requests for {owner}") + for item in await self.query(owner): + workspace = item["document"] + slug = workspace["slug"] + profile = workspace["publisherHandle"] + repo_url = f"{self.html_url}/{profile}/{slug}" + workspace_id = await self.get_workspace_id(repo_url) + if (org and workspace_id) or (user and owner.lower() == profile.lower()): + self.verbose(f"Found workspace ID {workspace_id} for {repo_url}") + data = await self.request_workspace(workspace_id) + in_scope = await self.validate_workspace( + data["workspace"], data["environments"], data["collections"] + ) + if in_scope: + in_scope_workspaces.append({"url": repo_url, "repo_name": slug}) + else: + self.verbose( + f"Failed to validate {repo_url} is in our scope as it does not contain any in-scope dns_names / emails" + ) + return in_scope_workspaces async def query(self, query): + def api_page_iter(url, page, page_size, offset, **kwargs): + kwargs["json"]["body"]["from"] = offset + return url, kwargs + data = [] url = f"{self.base_url}/ws/proxy" json = { @@ -67,7 +79,7 @@ async def query(self, query): "collaboration.workspace", ], "queryText": self.helpers.quote(query), - "size": 100, + "size": 25, "from": 0, "clientTraceId": "", "requestOrigin": "srp", @@ -76,13 +88,19 @@ async def query(self, query): "domain": "public", }, } - r = await self.helpers.request(url, method="POST", json=json, headers=self.headers) - if r is None: - return data - status_code = getattr(r, "status_code", 0) - try: - json = r.json() - except Exception as e: - self.warning(f"Failed to decode JSON for {r.url} (HTTP status: {status_code}): {e}") - return None - return json.get("data", []) + + agen = self.api_page_iter( + url, page_size=25, method="POST", iter_key=api_page_iter, json=json, _json=False, headers=self.headers + ) + async for r in agen: + status_code = getattr(r, "status_code", 0) + if status_code != 200: + self.debug(f"Reached end of postman search results (url: {r.url}) with status code {status_code}") + break + try: + data.extend(r.json().get("data", [])) + except Exception as e: + self.warning(f"Failed to decode JSON for {r.url} (HTTP status: {status_code}): {e}") + return None + + return data diff --git a/bbot/modules/postman_download.py b/bbot/modules/postman_download.py index 104f4261b8..727c655772 100644 --- a/bbot/modules/postman_download.py +++ b/bbot/modules/postman_download.py @@ -24,11 +24,7 @@ async def setup(self): else: self.output_dir = self.scan.home / "postman_workspaces" self.helpers.mkdir(self.output_dir) - return await self.require_api_key() - - def prepare_api_request(self, url, kwargs): - kwargs["headers"]["X-Api-Key"] = self.api_key - return url, kwargs + return await super().setup() async def filter_event(self, event): if event.type == "CODE_REPOSITORY": @@ -45,149 +41,16 @@ async def handle_event(self, event): workspace = data["workspace"] environments = data["environments"] collections = data["collections"] - in_scope = await self.validate_workspace(workspace, environments, collections) - if in_scope: - workspace_path = self.save_workspace(workspace, environments, collections) - if workspace_path: - self.verbose(f"Downloaded workspace from {repo_url} to {workspace_path}") - codebase_event = self.make_event( - {"path": str(workspace_path)}, "FILESYSTEM", tags=["postman", "workspace"], parent=event - ) - await self.emit_event( - codebase_event, - context=f"{{module}} downloaded postman workspace at {repo_url} to {{event.type}}: {workspace_path}", - ) - else: - self.verbose( - f"Failed to validate {repo_url} is in our scope as it does not contain any in-scope dns_names / emails, skipping download" + workspace_path = self.save_workspace(workspace, environments, collections) + if workspace_path: + self.verbose(f"Downloaded workspace from {repo_url} to {workspace_path}") + codebase_event = self.make_event( + {"path": str(workspace_path)}, "FILESYSTEM", tags=["postman", "workspace"], parent=event + ) + await self.emit_event( + codebase_event, + context=f"{{module}} downloaded postman workspace at {repo_url} to {{event.type}}: {workspace_path}", ) - - async def get_workspace_id(self, repo_url): - workspace_id = "" - profile = repo_url.split("/")[-2] - name = repo_url.split("/")[-1] - url = f"{self.base_url}/ws/proxy" - json = { - "service": "workspaces", - "method": "GET", - "path": f"/workspaces?handle={profile}&slug={name}", - } - r = await self.helpers.request(url, method="POST", json=json, headers=self.headers) - if r is None: - return workspace_id - status_code = getattr(r, "status_code", 0) - try: - json = r.json() - except Exception as e: - self.warning(f"Failed to decode JSON for {r.url} (HTTP status: {status_code}): {e}") - return workspace_id - data = json.get("data", []) - if len(data) == 1: - workspace_id = data[0]["id"] - return workspace_id - - async def request_workspace(self, id): - data = {"workspace": {}, "environments": [], "collections": []} - workspace = await self.get_workspace(id) - if workspace: - # Main Workspace - name = workspace["name"] - data["workspace"] = workspace - - # Workspace global variables - self.verbose(f"Downloading globals for workspace {name}") - globals = await self.get_globals(id) - data["environments"].append(globals) - - # Workspace Environments - workspace_environments = workspace.get("environments", []) - if workspace_environments: - self.verbose(f"Downloading environments for workspace {name}") - for _ in workspace_environments: - environment_id = _["uid"] - environment = await self.get_environment(environment_id) - data["environments"].append(environment) - - # Workspace Collections - workspace_collections = workspace.get("collections", []) - if workspace_collections: - self.verbose(f"Downloading collections for workspace {name}") - for _ in workspace_collections: - collection_id = _["uid"] - collection = await self.get_collection(collection_id) - data["collections"].append(collection) - return data - - async def get_workspace(self, workspace_id): - workspace = {} - workspace_url = f"{self.api_url}/workspaces/{workspace_id}" - r = await self.api_request(workspace_url) - if r is None: - return workspace - status_code = getattr(r, "status_code", 0) - try: - json = r.json() - except Exception as e: - self.warning(f"Failed to decode JSON for {r.url} (HTTP status: {status_code}): {e}") - return workspace - workspace = json.get("workspace", {}) - return workspace - - async def get_globals(self, workspace_id): - globals = {} - globals_url = f"{self.base_url}/workspace/{workspace_id}/globals" - r = await self.helpers.request(globals_url, headers=self.headers) - if r is None: - return globals - status_code = getattr(r, "status_code", 0) - try: - json = r.json() - except Exception as e: - self.warning(f"Failed to decode JSON for {r.url} (HTTP status: {status_code}): {e}") - return globals - globals = json.get("data", {}) - return globals - - async def get_environment(self, environment_id): - environment = {} - environment_url = f"{self.api_url}/environments/{environment_id}" - r = await self.api_request(environment_url) - if r is None: - return environment - status_code = getattr(r, "status_code", 0) - try: - json = r.json() - except Exception as e: - self.warning(f"Failed to decode JSON for {r.url} (HTTP status: {status_code}): {e}") - return environment - environment = json.get("environment", {}) - return environment - - async def get_collection(self, collection_id): - collection = {} - collection_url = f"{self.api_url}/collections/{collection_id}" - r = await self.api_request(collection_url) - if r is None: - return collection - status_code = getattr(r, "status_code", 0) - try: - json = r.json() - except Exception as e: - self.warning(f"Failed to decode JSON for {r.url} (HTTP status: {status_code}): {e}") - return collection - collection = json.get("collection", {}) - return collection - - async def validate_workspace(self, workspace, environments, collections): - name = workspace.get("name", "") - full_wks = str([workspace, environments, collections]) - in_scope_hosts = await self.scan.extract_in_scope_hostnames(full_wks) - if in_scope_hosts: - self.verbose( - f'Found in-scope hostname(s): "{in_scope_hosts}" in workspace {name}, it appears to be in-scope' - ) - return True - return False def save_workspace(self, workspace, environments, collections): zip_path = None diff --git a/bbot/modules/templates/postman.py b/bbot/modules/templates/postman.py index 2048abc32d..490c6e62eb 100644 --- a/bbot/modules/templates/postman.py +++ b/bbot/modules/templates/postman.py @@ -14,8 +14,170 @@ class postman(BaseModule): headers = { "Content-Type": "application/json", - "X-App-Version": "10.18.8-230926-0808", + "X-App-Version": "11.27.4-250109-2338", "X-Entity-Team-Id": "0", "Origin": "https://www.postman.com", "Referer": "https://www.postman.com/search?q=&scope=public&type=all", } + auth_required = True + + async def setup(self): + await super().setup() + self.headers = {} + api_keys = set() + modules_config = self.scan.config.get("modules", {}) + postman_modules = [m for m in modules_config if str(m).startswith("postman")] + for module_name in postman_modules: + module_config = modules_config.get(module_name, {}) + api_key = module_config.get("api_key", "") + if isinstance(api_key, str): + api_key = [api_key] + for key in api_key: + key = key.strip() + if key: + api_keys.add(key) + if not api_keys: + if self.auth_required: + return None, "No API key set" + self.api_key = api_keys + if self.api_key: + try: + await self.ping() + self.hugesuccess("API is ready") + return True + except Exception as e: + self.trace() + return None, f"Error with API ({str(e).strip()})" + return True + + def prepare_api_request(self, url, kwargs): + if self.api_key: + kwargs["headers"]["X-Api-Key"] = self.api_key + return url, kwargs + + async def get_workspace_id(self, repo_url): + workspace_id = "" + profile = repo_url.split("/")[-2] + name = repo_url.split("/")[-1] + url = f"{self.base_url}/ws/proxy" + json = { + "service": "workspaces", + "method": "GET", + "path": f"/workspaces?handle={profile}&slug={name}", + } + r = await self.helpers.request(url, method="POST", json=json, headers=self.headers) + if r is None: + return workspace_id + status_code = getattr(r, "status_code", 0) + try: + json = r.json() + except Exception as e: + self.warning(f"Failed to decode JSON for {r.url} (HTTP status: {status_code}): {e}") + return workspace_id + data = json.get("data", []) + if len(data) == 1: + workspace_id = data[0]["id"] + return workspace_id + + async def request_workspace(self, id): + data = {"workspace": {}, "environments": [], "collections": []} + workspace = await self.get_workspace(id) + if workspace: + # Main Workspace + name = workspace["name"] + data["workspace"] = workspace + + # Workspace global variables + self.verbose(f"Searching globals for workspace {name}") + globals = await self.get_globals(id) + data["environments"].append(globals) + + # Workspace Environments + workspace_environments = workspace.get("environments", []) + if workspace_environments: + self.verbose(f"Searching environments for workspace {name}") + for _ in workspace_environments: + environment_id = _["uid"] + environment = await self.get_environment(environment_id) + data["environments"].append(environment) + + # Workspace Collections + workspace_collections = workspace.get("collections", []) + if workspace_collections: + self.verbose(f"Searching collections for workspace {name}") + for _ in workspace_collections: + collection_id = _["uid"] + collection = await self.get_collection(collection_id) + data["collections"].append(collection) + return data + + async def get_workspace(self, workspace_id): + workspace = {} + workspace_url = f"{self.api_url}/workspaces/{workspace_id}" + r = await self.api_request(workspace_url) + if r is None: + return workspace + status_code = getattr(r, "status_code", 0) + try: + json = r.json() + except Exception as e: + self.warning(f"Failed to decode JSON for {r.url} (HTTP status: {status_code}): {e}") + return workspace + workspace = json.get("workspace", {}) + return workspace + + async def get_globals(self, workspace_id): + globals = {} + globals_url = f"{self.base_url}/workspace/{workspace_id}/globals" + r = await self.helpers.request(globals_url, headers=self.headers) + if r is None: + return globals + status_code = getattr(r, "status_code", 0) + try: + json = r.json() + except Exception as e: + self.warning(f"Failed to decode JSON for {r.url} (HTTP status: {status_code}): {e}") + return globals + globals = json.get("data", {}) + return globals + + async def get_environment(self, environment_id): + environment = {} + environment_url = f"{self.api_url}/environments/{environment_id}" + r = await self.api_request(environment_url) + if r is None: + return environment + status_code = getattr(r, "status_code", 0) + try: + json = r.json() + except Exception as e: + self.warning(f"Failed to decode JSON for {r.url} (HTTP status: {status_code}): {e}") + return environment + environment = json.get("environment", {}) + return environment + + async def get_collection(self, collection_id): + collection = {} + collection_url = f"{self.api_url}/collections/{collection_id}" + r = await self.api_request(collection_url) + if r is None: + return collection + status_code = getattr(r, "status_code", 0) + try: + json = r.json() + except Exception as e: + self.warning(f"Failed to decode JSON for {r.url} (HTTP status: {status_code}): {e}") + return collection + collection = json.get("collection", {}) + return collection + + async def validate_workspace(self, workspace, environments, collections): + name = workspace.get("name", "") + full_wks = str([workspace, environments, collections]) + in_scope_hosts = await self.scan.extract_in_scope_hostnames(full_wks) + if in_scope_hosts: + self.verbose( + f'Found in-scope hostname(s): "{in_scope_hosts}" in workspace {name}, it appears to be in-scope' + ) + return True + return False diff --git a/bbot/test/test_step_1/test_web.py b/bbot/test/test_step_1/test_web.py index e07ed3d7d4..b88e4eaa8c 100644 --- a/bbot/test/test_step_1/test_web.py +++ b/bbot/test/test_step_1/test_web.py @@ -263,7 +263,7 @@ async def test_web_helpers(bbot_scanner, bbot_httpserver, httpx_mock): finally: await agen.aclose() assert not results - agen = module.api_page_iter(template_url, json=False) + agen = module.api_page_iter(template_url, _json=False) try: async for result in agen: if result and result.text.startswith("page"): diff --git a/bbot/test/test_step_2/module_tests/test_module_postman.py b/bbot/test/test_step_2/module_tests/test_module_postman.py index 1066c5d731..d5b9cb3f2c 100644 --- a/bbot/test/test_step_2/module_tests/test_module_postman.py +++ b/bbot/test/test_step_2/module_tests/test_module_postman.py @@ -2,14 +2,50 @@ class TestPostman(ModuleTestBase): + config_overrides = {"modules": {"postman": {"api_key": "asdf"}}} modules_overrides = ["postman", "speculate"] + async def setup_before_prep(self, module_test): + module_test.httpx_mock.add_response( + url="https://api.getpostman.com/me", + match_headers={"X-Api-Key": "asdf"}, + json={ + "user": { + "id": 000000, + "username": "test_key", + "email": "blacklanternsecurity@test.com", + "fullName": "Test Key", + "avatar": "", + "isPublic": True, + "teamId": 0, + "teamDomain": "", + "roles": ["user"], + }, + "operations": [ + {"name": "api_object_usage", "limit": 3, "usage": 0, "overage": 0}, + {"name": "collection_run_limit", "limit": 25, "usage": 0, "overage": 0}, + {"name": "file_storage_limit", "limit": 20, "usage": 0, "overage": 0}, + {"name": "flow_count", "limit": 5, "usage": 0, "overage": 0}, + {"name": "flow_requests", "limit": 5000, "usage": 0, "overage": 0}, + {"name": "performance_test_limit", "limit": 25, "usage": 0, "overage": 0}, + {"name": "postbot_calls", "limit": 50, "usage": 0, "overage": 0}, + {"name": "reusable_packages", "limit": 3, "usage": 0, "overage": 0}, + {"name": "test_data_retrieval", "limit": 1000, "usage": 0, "overage": 0}, + {"name": "test_data_storage", "limit": 10, "usage": 0, "overage": 0}, + {"name": "mock_usage", "limit": 1000, "usage": 0, "overage": 0}, + {"name": "monitor_request_runs", "limit": 1000, "usage": 0, "overage": 0}, + {"name": "api_usage", "limit": 1000, "usage": 0, "overage": 0}, + ], + }, + ) + async def setup_after_prep(self, module_test): await module_test.mock_dns( {"blacklanternsecurity.com": {"A": ["127.0.0.99"]}, "github.com": {"A": ["127.0.0.99"]}} ) module_test.httpx_mock.add_response( url="https://www.postman.com/_api/ws/proxy", + match_content=b'{"service": "search", "method": "POST", "path": "/search-all", "body": {"queryIndices": ["collaboration.workspace"], "queryText": "blacklanternsecurity", "size": 25, "from": 0, "clientTraceId": "", "requestOrigin": "srp", "mergeEntities": "true", "nonNestedRequests": "true", "domain": "public"}}', json={ "data": [ { @@ -62,14 +98,65 @@ async def setup_after_prep(self, module_test): "documentType": "workspace", }, "highlight": {"summary": "BLS BBOT api test."}, - } + }, + { + "score": 611.41156, + "normalizedScore": 23, + "document": { + "watcherCount": 6, + "apiCount": 0, + "forkCount": 0, + "isblacklisted": "false", + "createdAt": "2021-06-15T14:03:51", + "publishertype": "team", + "publisherHandle": "testteam", + "id": "11498add-357d-4bc5-a008-0a2d44fb8829", + "slug": "testing-bbot-api", + "updatedAt": "2024-07-30T11:00:35", + "entityType": "workspace", + "visibilityStatus": "public", + "forkcount": "0", + "tags": [], + "createdat": "2021-06-15T14:03:51", + "forkLabel": "", + "publisherName": "testteam", + "name": "Test BlackLanternSecurity API Team Workspace", + "dependencyCount": 7, + "collectionCount": 6, + "warehouse__updated_at": "2024-07-30 11:00:00", + "privateNetworkFolders": [], + "isPublisherVerified": False, + "publisherType": "team", + "curatedInList": [], + "creatorId": "6900157", + "description": "", + "forklabel": "", + "publisherId": "299401", + "publisherLogo": "", + "popularity": 5, + "isPublic": True, + "categories": [], + "universaltags": "", + "views": 5788, + "summary": "Private test of BBOTs public API", + "memberCount": 2, + "isBlacklisted": False, + "publisherid": "299401", + "isPrivateNetworkEntity": False, + "isDomainNonTrivial": True, + "privateNetworkMeta": "", + "updatedat": "2021-10-20T16:19:29", + "documentType": "workspace", + }, + "highlight": {"summary": "Private test of BBOTs Public API"}, + }, ], "meta": { "queryText": "blacklanternsecurity", "total": { "collection": 0, "request": 0, - "workspace": 1, + "workspace": 2, "api": 0, "team": 0, "user": 0, @@ -78,7 +165,7 @@ async def setup_after_prep(self, module_test): "privateNetworkFolder": 0, }, "state": "AQ4", - "spellCorrection": {"count": {"all": 1, "workspace": 1}, "correctedQueryText": None}, + "spellCorrection": {"count": {"all": 2, "workspace": 2}, "correctedQueryText": None}, "featureFlags": { "enabledPublicResultCuration": True, "boostByPopularity": True, @@ -88,6 +175,253 @@ async def setup_after_prep(self, module_test): }, }, ) + module_test.httpx_mock.add_response( + url="https://www.postman.com/_api/ws/proxy", + match_content=b'{"service": "workspaces", "method": "GET", "path": "/workspaces?handle=blacklanternsecurity&slug=bbot-public"}', + json={ + "meta": {"model": "workspace", "action": "find", "nextCursor": ""}, + "data": [ + { + "id": "3a7e4bdc-7ff7-4dd4-8eaa-61ddce1c3d1b", + "name": "BlackLanternSecurity BBOT [Public]", + "description": None, + "summary": "BLS public workspaces.", + "createdBy": "299401", + "updatedBy": "299401", + "team": None, + "createdAt": "2021-10-20T16:19:29", + "updatedAt": "2021-10-20T16:19:29", + "visibilityStatus": "public", + "profileInfo": { + "slug": "bbot-public", + "profileType": "team", + "profileId": "000000", + "publicHandle": "https://www.postman.com/blacklanternsecurity", + "publicImageURL": "", + "publicName": "BlackLanternSecurity", + "isVerified": False, + }, + } + ], + }, + ) + module_test.httpx_mock.add_response( + url="https://www.postman.com/_api/ws/proxy", + match_content=b'{"service": "workspaces", "method": "GET", "path": "/workspaces?handle=testteam&slug=testing-bbot-api"}', + json={ + "meta": {"model": "workspace", "action": "find", "nextCursor": ""}, + "data": [ + { + "id": "a4dfe981-2593-4f0b-b4c3-5145e8640f7d", + "name": "Test BlackLanternSecurity API Team Workspace", + "description": None, + "summary": "Private test of BBOTs public API", + "createdBy": "299401", + "updatedBy": "299401", + "team": None, + "createdAt": "2021-10-20T16:19:29", + "updatedAt": "2021-10-20T16:19:29", + "visibilityStatus": "public", + "profileInfo": { + "slug": "bbot-public", + "profileType": "team", + "profileId": "000000", + "publicHandle": "https://www.postman.com/testteam", + "publicImageURL": "", + "publicName": "testteam", + "isVerified": False, + }, + } + ], + }, + ) + module_test.httpx_mock.add_response( + url="https://api.getpostman.com/workspaces/3a7e4bdc-7ff7-4dd4-8eaa-61ddce1c3d1b", + match_headers={"X-Api-Key": "asdf"}, + json={ + "workspace": { + "id": "3a7e4bdc-7ff7-4dd4-8eaa-61ddce1c3d1b", + "name": "BlackLanternSecurity BBOT [Public]", + "type": "personal", + "description": None, + "visibility": "public", + "createdBy": "00000000", + "updatedBy": "00000000", + "createdAt": "2021-11-17T06:09:01.000Z", + "updatedAt": "2021-11-17T08:57:16.000Z", + "collections": [ + { + "id": "2aab9fd0-3715-4abe-8bb0-8cb0264d023f", + "name": "BBOT Public", + "uid": "10197090-2aab9fd0-3715-4abe-8bb0-8cb0264d023f", + }, + ], + "environments": [ + { + "id": "f770f816-9c6a-40f7-bde3-c0855d2a1089", + "name": "BBOT Test", + "uid": "10197090-f770f816-9c6a-40f7-bde3-c0855d2a1089", + } + ], + "apis": [], + } + }, + ) + module_test.httpx_mock.add_response( + url="https://api.getpostman.com/workspaces/a4dfe981-2593-4f0b-b4c3-5145e8640f7d", + json={ + "workspace": { + "id": "a4dfe981-2593-4f0b-b4c3-5145e8640f7d", + "name": "Test BlackLanternSecurity API Team Workspace", + "type": "personal", + "description": None, + "visibility": "public", + "createdBy": "00000000", + "updatedBy": "00000000", + "createdAt": "2021-11-17T06:09:01.000Z", + "updatedAt": "2021-11-17T08:57:16.000Z", + "collections": [ + { + "id": "f46bebfd-420a-4adf-97d1-6fb5a02cf7fc", + "name": "BBOT Public", + "uid": "10197090-f46bebfd-420a-4adf-97d1-6fb5a02cf7fc", + }, + ], + "environments": [], + "apis": [], + } + }, + ) + module_test.httpx_mock.add_response( + url="https://www.postman.com/_api/workspace/3a7e4bdc-7ff7-4dd4-8eaa-61ddce1c3d1b/globals", + json={ + "model_id": "8be7574b-219f-49e0-8d25-da447a882e4e", + "meta": {"model": "globals", "action": "find"}, + "data": { + "workspace": "3a7e4bdc-7ff7-4dd4-8eaa-61ddce1c3d1b", + "lastUpdatedBy": "00000000", + "lastRevision": 1637239113000, + "id": "8be7574b-219f-49e0-8d25-da447a882e4e", + "values": [ + { + "key": "endpoint_url", + "value": "https://api.blacklanternsecurity.com/", + "enabled": True, + }, + ], + "createdAt": "2021-11-17T06:09:01.000Z", + "updatedAt": "2021-11-18T12:38:33.000Z", + }, + }, + ) + module_test.httpx_mock.add_response( + url="https://www.postman.com/_api/workspace/a4dfe981-2593-4f0b-b4c3-5145e8640f7d/globals", + json={ + "model_id": "8be7574b-219f-49e0-8d25-da447a882e4e", + "meta": {"model": "globals", "action": "find"}, + "data": { + "workspace": "a4dfe981-2593-4f0b-b4c3-5145e8640f7d", + "lastUpdatedBy": "00000000", + "lastRevision": 1637239113000, + "id": "8be7574b-219f-49e0-8d25-da447a882e4e", + "values": [], + "createdAt": "2021-11-17T06:09:01.000Z", + "updatedAt": "2021-11-18T12:38:33.000Z", + }, + }, + ) + module_test.httpx_mock.add_response( + url="https://api.getpostman.com/environments/10197090-f770f816-9c6a-40f7-bde3-c0855d2a1089", + match_headers={"X-Api-Key": "asdf"}, + json={ + "environment": { + "id": "f770f816-9c6a-40f7-bde3-c0855d2a1089", + "name": "BBOT Test", + "owner": "00000000", + "createdAt": "2021-11-17T06:29:54.000Z", + "updatedAt": "2021-11-23T07:06:53.000Z", + "values": [ + { + "key": "temp_session_endpoint", + "value": "https://api.blacklanternsecurity.com/", + "enabled": True, + }, + ], + "isPublic": True, + } + }, + ) + module_test.httpx_mock.add_response( + url="https://api.getpostman.com/collections/10197090-2aab9fd0-3715-4abe-8bb0-8cb0264d023f", + match_headers={"X-Api-Key": "asdf"}, + json={ + "collection": { + "info": { + "_postman_id": "62b91565-d2e2-4bcd-8248-4dba2e3452f0", + "name": "BBOT Public", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "updatedAt": "2021-11-17T07:13:16.000Z", + "createdAt": "2021-11-17T07:13:15.000Z", + "lastUpdatedBy": "00000000", + "uid": "00000000-62b91565-d2e2-4bcd-8248-4dba2e3452f0", + }, + "item": [ + { + "name": "Generate API Session", + "id": "c1bac38c-dfc9-4cc0-9c19-828cbc8543b1", + "protocolProfileBehavior": {"disableBodyPruning": True}, + "request": { + "method": "POST", + "header": [{"key": "Content-Type", "value": "application/json"}], + "body": { + "mode": "raw", + "raw": '{"username": "test", "password": "Test"}', + }, + "url": {"raw": "{{endpoint_url}}", "host": ["{{endpoint_url}}"]}, + "description": "", + }, + "response": [], + "uid": "10197090-c1bac38c-dfc9-4cc0-9c19-828cbc8543b1", + }, + ], + } + }, + ) + module_test.httpx_mock.add_response( + url="https://api.getpostman.com/collections/10197090-f46bebfd-420a-4adf-97d1-6fb5a02cf7fc", + json={ + "collection": { + "info": { + "_postman_id": "f46bebfd-420a-4adf-97d1-6fb5a02cf7fc", + "name": "BBOT Public", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "updatedAt": "2021-11-17T07:13:16.000Z", + "createdAt": "2021-11-17T07:13:15.000Z", + "lastUpdatedBy": "00000000", + "uid": "00000000-f46bebfd-420a-4adf-97d1-6fb5a02cf7fc", + }, + "item": [ + { + "name": "Out of Scope API request", + "id": "c1bac38c-dfc9-4cc0-9c19-828cbc8543b1", + "protocolProfileBehavior": {"disableBodyPruning": True}, + "request": { + "method": "POST", + "header": [{"key": "Content-Type", "value": "application/json"}], + "body": { + "mode": "raw", + "raw": '{"username": "test", "password": "Test"}', + }, + "url": {"raw": "https://www.outofscope.com", "host": ["www.outofscope.com"]}, + "description": "", + }, + "response": [], + "uid": "10197090-c1bac38c-dfc9-4cc0-9c19-828cbc8543b1", + }, + ], + } + }, + ) def check(self, module_test, events): assert len(events) == 5 @@ -101,6 +435,7 @@ def check(self, module_test, events): assert 1 == len( [e for e in events if e.type == "ORG_STUB" and e.data == "blacklanternsecurity" and e.scope_distance == 0] ), "Failed to find ORG_STUB" + # Find only 1 in-scope workspace the other will be out of scope assert 1 == len( [ e diff --git a/bbot/test/test_step_2/module_tests/test_module_postman_download.py b/bbot/test/test_step_2/module_tests/test_module_postman_download.py index f937ebc20f..4a893601a7 100644 --- a/bbot/test/test_step_2/module_tests/test_module_postman_download.py +++ b/bbot/test/test_step_2/module_tests/test_module_postman_download.py @@ -45,7 +45,7 @@ async def setup_after_prep(self, module_test): ) module_test.httpx_mock.add_response( url="https://www.postman.com/_api/ws/proxy", - match_content=b'{"service": "search", "method": "POST", "path": "/search-all", "body": {"queryIndices": ["collaboration.workspace"], "queryText": "blacklanternsecurity", "size": 100, "from": 0, "clientTraceId": "", "requestOrigin": "srp", "mergeEntities": "true", "nonNestedRequests": "true", "domain": "public"}}', + match_content=b'{"service": "search", "method": "POST", "path": "/search-all", "body": {"queryIndices": ["collaboration.workspace"], "queryText": "blacklanternsecurity", "size": 25, "from": 0, "clientTraceId": "", "requestOrigin": "srp", "mergeEntities": "true", "nonNestedRequests": "true", "domain": "public"}}', json={ "data": [ { @@ -99,64 +99,13 @@ async def setup_after_prep(self, module_test): }, "highlight": {"summary": "BLS BBOT api test."}, }, - { - "score": 611.41156, - "normalizedScore": 23, - "document": { - "watcherCount": 6, - "apiCount": 0, - "forkCount": 0, - "isblacklisted": "false", - "createdAt": "2021-06-15T14:03:51", - "publishertype": "team", - "publisherHandle": "testteam", - "id": "11498add-357d-4bc5-a008-0a2d44fb8829", - "slug": "testing-bbot-api", - "updatedAt": "2024-07-30T11:00:35", - "entityType": "workspace", - "visibilityStatus": "public", - "forkcount": "0", - "tags": [], - "createdat": "2021-06-15T14:03:51", - "forkLabel": "", - "publisherName": "testteam", - "name": "Test BlackLanternSecurity API Team Workspace", - "dependencyCount": 7, - "collectionCount": 6, - "warehouse__updated_at": "2024-07-30 11:00:00", - "privateNetworkFolders": [], - "isPublisherVerified": False, - "publisherType": "team", - "curatedInList": [], - "creatorId": "6900157", - "description": "", - "forklabel": "", - "publisherId": "299401", - "publisherLogo": "", - "popularity": 5, - "isPublic": True, - "categories": [], - "universaltags": "", - "views": 5788, - "summary": "Private test of BBOTs public API", - "memberCount": 2, - "isBlacklisted": False, - "publisherid": "299401", - "isPrivateNetworkEntity": False, - "isDomainNonTrivial": True, - "privateNetworkMeta": "", - "updatedat": "2021-10-20T16:19:29", - "documentType": "workspace", - }, - "highlight": {"summary": "Private test of BBOTs Public API"}, - }, ], "meta": { "queryText": "blacklanternsecurity", "total": { "collection": 0, "request": 0, - "workspace": 2, + "workspace": 1, "api": 0, "team": 0, "user": 0, @@ -165,7 +114,7 @@ async def setup_after_prep(self, module_test): "privateNetworkFolder": 0, }, "state": "AQ4", - "spellCorrection": {"count": {"all": 2, "workspace": 2}, "correctedQueryText": None}, + "spellCorrection": {"count": {"all": 1, "workspace": 1}, "correctedQueryText": None}, "featureFlags": { "enabledPublicResultCuration": True, "boostByPopularity": True, @@ -205,36 +154,6 @@ async def setup_after_prep(self, module_test): ], }, ) - module_test.httpx_mock.add_response( - url="https://www.postman.com/_api/ws/proxy", - match_content=b'{"service": "workspaces", "method": "GET", "path": "/workspaces?handle=testteam&slug=testing-bbot-api"}', - json={ - "meta": {"model": "workspace", "action": "find", "nextCursor": ""}, - "data": [ - { - "id": "a4dfe981-2593-4f0b-b4c3-5145e8640f7d", - "name": "Test BlackLanternSecurity API Team Workspace", - "description": None, - "summary": "Private test of BBOTs public API", - "createdBy": "299401", - "updatedBy": "299401", - "team": None, - "createdAt": "2021-10-20T16:19:29", - "updatedAt": "2021-10-20T16:19:29", - "visibilityStatus": "public", - "profileInfo": { - "slug": "bbot-public", - "profileType": "team", - "profileId": "000000", - "publicHandle": "https://www.postman.com/testteam", - "publicImageURL": "", - "publicName": "testteam", - "isVerified": False, - }, - } - ], - }, - ) module_test.httpx_mock.add_response( url="https://api.getpostman.com/workspaces/3a7e4bdc-7ff7-4dd4-8eaa-61ddce1c3d1b", match_headers={"X-Api-Key": "asdf"}, @@ -267,31 +186,6 @@ async def setup_after_prep(self, module_test): } }, ) - module_test.httpx_mock.add_response( - url="https://api.getpostman.com/workspaces/a4dfe981-2593-4f0b-b4c3-5145e8640f7d", - json={ - "workspace": { - "id": "a4dfe981-2593-4f0b-b4c3-5145e8640f7d", - "name": "Test BlackLanternSecurity API Team Workspace", - "type": "personal", - "description": None, - "visibility": "public", - "createdBy": "00000000", - "updatedBy": "00000000", - "createdAt": "2021-11-17T06:09:01.000Z", - "updatedAt": "2021-11-17T08:57:16.000Z", - "collections": [ - { - "id": "f46bebfd-420a-4adf-97d1-6fb5a02cf7fc", - "name": "BBOT Public", - "uid": "10197090-f46bebfd-420a-4adf-97d1-6fb5a02cf7fc", - }, - ], - "environments": [], - "apis": [], - } - }, - ) module_test.httpx_mock.add_response( url="https://www.postman.com/_api/workspace/3a7e4bdc-7ff7-4dd4-8eaa-61ddce1c3d1b/globals", json={ @@ -314,22 +208,6 @@ async def setup_after_prep(self, module_test): }, }, ) - module_test.httpx_mock.add_response( - url="https://www.postman.com/_api/workspace/a4dfe981-2593-4f0b-b4c3-5145e8640f7d/globals", - json={ - "model_id": "8be7574b-219f-49e0-8d25-da447a882e4e", - "meta": {"model": "globals", "action": "find"}, - "data": { - "workspace": "a4dfe981-2593-4f0b-b4c3-5145e8640f7d", - "lastUpdatedBy": "00000000", - "lastRevision": 1637239113000, - "id": "8be7574b-219f-49e0-8d25-da447a882e4e", - "values": [], - "createdAt": "2021-11-17T06:09:01.000Z", - "updatedAt": "2021-11-18T12:38:33.000Z", - }, - }, - ) module_test.httpx_mock.add_response( url="https://api.getpostman.com/environments/10197090-f770f816-9c6a-40f7-bde3-c0855d2a1089", match_headers={"X-Api-Key": "asdf"}, @@ -387,44 +265,9 @@ async def setup_after_prep(self, module_test): } }, ) - module_test.httpx_mock.add_response( - url="https://api.getpostman.com/collections/10197090-f46bebfd-420a-4adf-97d1-6fb5a02cf7fc", - json={ - "collection": { - "info": { - "_postman_id": "f46bebfd-420a-4adf-97d1-6fb5a02cf7fc", - "name": "BBOT Public", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "updatedAt": "2021-11-17T07:13:16.000Z", - "createdAt": "2021-11-17T07:13:15.000Z", - "lastUpdatedBy": "00000000", - "uid": "00000000-f46bebfd-420a-4adf-97d1-6fb5a02cf7fc", - }, - "item": [ - { - "name": "Out of Scope API request", - "id": "c1bac38c-dfc9-4cc0-9c19-828cbc8543b1", - "protocolProfileBehavior": {"disableBodyPruning": True}, - "request": { - "method": "POST", - "header": [{"key": "Content-Type", "value": "application/json"}], - "body": { - "mode": "raw", - "raw": '{"username": "test", "password": "Test"}', - }, - "url": {"raw": "https://www.outofscope.com", "host": ["www.outofscope.com"]}, - "description": "", - }, - "response": [], - "uid": "10197090-c1bac38c-dfc9-4cc0-9c19-828cbc8543b1", - }, - ], - } - }, - ) def check(self, module_test, events): - assert 2 == len( + assert 1 == len( [e for e in events if e.type == "CODE_REPOSITORY" and "postman" in e.tags and e.scope_distance == 1] ), "Failed to find blacklanternsecurity postman workspace" assert 1 == len( diff --git a/bbot/test/test_step_2/module_tests/test_module_trufflehog.py b/bbot/test/test_step_2/module_tests/test_module_trufflehog.py index 7f78977f1e..d8594cd34c 100644 --- a/bbot/test/test_step_2/module_tests/test_module_trufflehog.py +++ b/bbot/test/test_step_2/module_tests/test_module_trufflehog.py @@ -848,7 +848,7 @@ async def setup_before_prep(self, module_test): async def setup_after_prep(self, module_test): module_test.httpx_mock.add_response( url="https://www.postman.com/_api/ws/proxy", - match_content=b'{"service": "search", "method": "POST", "path": "/search-all", "body": {"queryIndices": ["collaboration.workspace"], "queryText": "blacklanternsecurity", "size": 100, "from": 0, "clientTraceId": "", "requestOrigin": "srp", "mergeEntities": "true", "nonNestedRequests": "true", "domain": "public"}}', + match_content=b'{"service": "search", "method": "POST", "path": "/search-all", "body": {"queryIndices": ["collaboration.workspace"], "queryText": "blacklanternsecurity", "size": 25, "from": 0, "clientTraceId": "", "requestOrigin": "srp", "mergeEntities": "true", "nonNestedRequests": "true", "domain": "public"}}', json={ "data": [ {