bbot 2.6.0.6879rc0__py3-none-any.whl → 2.7.2.7254rc0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of bbot might be problematic. Click here for more details.

Files changed (75) hide show
  1. bbot/__init__.py +1 -1
  2. bbot/core/engine.py +1 -1
  3. bbot/core/flags.py +1 -0
  4. bbot/core/helpers/bloom.py +6 -7
  5. bbot/core/helpers/dns/dns.py +0 -1
  6. bbot/core/helpers/dns/engine.py +0 -2
  7. bbot/core/helpers/files.py +2 -2
  8. bbot/core/helpers/git.py +17 -0
  9. bbot/core/helpers/misc.py +1 -0
  10. bbot/core/helpers/ntlm.py +0 -2
  11. bbot/core/helpers/regex.py +1 -1
  12. bbot/core/modules.py +0 -54
  13. bbot/defaults.yml +4 -2
  14. bbot/modules/apkpure.py +1 -1
  15. bbot/modules/base.py +11 -5
  16. bbot/modules/dnsbimi.py +1 -4
  17. bbot/modules/dnsdumpster.py +35 -52
  18. bbot/modules/dnstlsrpt.py +0 -6
  19. bbot/modules/docker_pull.py +1 -1
  20. bbot/modules/emailformat.py +17 -1
  21. bbot/modules/filedownload.py +1 -1
  22. bbot/modules/git_clone.py +47 -22
  23. bbot/modules/gitdumper.py +4 -14
  24. bbot/modules/github_workflows.py +1 -1
  25. bbot/modules/gitlab_com.py +31 -0
  26. bbot/modules/gitlab_onprem.py +84 -0
  27. bbot/modules/gowitness.py +0 -6
  28. bbot/modules/graphql_introspection.py +5 -2
  29. bbot/modules/httpx.py +2 -0
  30. bbot/modules/iis_shortnames.py +0 -7
  31. bbot/modules/internal/unarchive.py +9 -3
  32. bbot/modules/lightfuzz/lightfuzz.py +5 -1
  33. bbot/modules/nuclei.py +1 -1
  34. bbot/modules/output/base.py +0 -5
  35. bbot/modules/postman_download.py +1 -1
  36. bbot/modules/retirejs.py +232 -0
  37. bbot/modules/securitytxt.py +0 -3
  38. bbot/modules/subdomaincenter.py +1 -16
  39. bbot/modules/telerik.py +6 -1
  40. bbot/modules/templates/gitlab.py +98 -0
  41. bbot/modules/trufflehog.py +1 -1
  42. bbot/scanner/manager.py +7 -4
  43. bbot/scanner/scanner.py +1 -1
  44. bbot/scripts/benchmark_report.py +433 -0
  45. bbot/test/benchmarks/__init__.py +2 -0
  46. bbot/test/benchmarks/test_bloom_filter_benchmarks.py +105 -0
  47. bbot/test/benchmarks/test_closest_match_benchmarks.py +76 -0
  48. bbot/test/benchmarks/test_event_validation_benchmarks.py +438 -0
  49. bbot/test/benchmarks/test_excavate_benchmarks.py +291 -0
  50. bbot/test/benchmarks/test_ipaddress_benchmarks.py +143 -0
  51. bbot/test/benchmarks/test_weighted_shuffle_benchmarks.py +70 -0
  52. bbot/test/test_step_1/test_bbot_fastapi.py +2 -2
  53. bbot/test/test_step_1/test_events.py +0 -1
  54. bbot/test/test_step_1/test_scan.py +1 -8
  55. bbot/test/test_step_2/module_tests/base.py +6 -1
  56. bbot/test/test_step_2/module_tests/test_module_dnsbimi.py +2 -1
  57. bbot/test/test_step_2/module_tests/test_module_dnsdumpster.py +3 -5
  58. bbot/test/test_step_2/module_tests/test_module_emailformat.py +1 -1
  59. bbot/test/test_step_2/module_tests/test_module_emails.py +2 -2
  60. bbot/test/test_step_2/module_tests/test_module_excavate.py +35 -6
  61. bbot/test/test_step_2/module_tests/test_module_gitlab_com.py +66 -0
  62. bbot/test/test_step_2/module_tests/{test_module_gitlab.py → test_module_gitlab_onprem.py} +4 -69
  63. bbot/test/test_step_2/module_tests/test_module_lightfuzz.py +2 -2
  64. bbot/test/test_step_2/module_tests/test_module_retirejs.py +159 -0
  65. bbot/test/test_step_2/module_tests/test_module_telerik.py +1 -1
  66. {bbot-2.6.0.6879rc0.dist-info → bbot-2.7.2.7254rc0.dist-info}/METADATA +7 -4
  67. {bbot-2.6.0.6879rc0.dist-info → bbot-2.7.2.7254rc0.dist-info}/RECORD +70 -60
  68. {bbot-2.6.0.6879rc0.dist-info → bbot-2.7.2.7254rc0.dist-info}/WHEEL +1 -1
  69. bbot/modules/censys.py +0 -98
  70. bbot/modules/gitlab.py +0 -141
  71. bbot/modules/zoomeye.py +0 -77
  72. bbot/test/test_step_2/module_tests/test_module_censys.py +0 -83
  73. bbot/test/test_step_2/module_tests/test_module_zoomeye.py +0 -35
  74. {bbot-2.6.0.6879rc0.dist-info → bbot-2.7.2.7254rc0.dist-info}/entry_points.txt +0 -0
  75. {bbot-2.6.0.6879rc0.dist-info → bbot-2.7.2.7254rc0.dist-info/licenses}/LICENSE +0 -0
bbot/modules/censys.py DELETED
@@ -1,98 +0,0 @@
1
- from bbot.modules.templates.subdomain_enum import subdomain_enum_apikey
2
-
3
-
4
- class censys(subdomain_enum_apikey):
5
- """
6
- thanks to https://github.com/owasp-amass/amass/blob/master/resources/scripts/cert/censys.ads
7
- """
8
-
9
- watched_events = ["DNS_NAME"]
10
- produced_events = ["DNS_NAME"]
11
- flags = ["subdomain-enum", "passive", "safe"]
12
- meta = {
13
- "description": "Query the Censys API",
14
- "created_date": "2022-08-04",
15
- "author": "@TheTechromancer",
16
- "auth_required": True,
17
- }
18
- options = {"api_key": "", "max_pages": 5}
19
- options_desc = {
20
- "api_key": "Censys.io API Key in the format of 'key:secret'",
21
- "max_pages": "Maximum number of pages to fetch (100 results per page)",
22
- }
23
-
24
- base_url = "https://search.censys.io/api"
25
-
26
- async def setup(self):
27
- self.max_pages = self.config.get("max_pages", 5)
28
- return await super().setup()
29
-
30
- async def ping(self):
31
- url = f"{self.base_url}/v1/account"
32
- resp = await self.api_request(url, retry_on_http_429=False)
33
- d = resp.json()
34
- assert isinstance(d, dict), f"Invalid response from {url}: {resp}"
35
- quota = d.get("quota", {})
36
- used = int(quota.get("used", 0))
37
- allowance = int(quota.get("allowance", 0))
38
- assert used < allowance, "No quota remaining"
39
-
40
- def prepare_api_request(self, url, kwargs):
41
- api_id, api_secret = self.api_key.split(":", 1)
42
- kwargs["auth"] = (api_id, api_secret)
43
- return url, kwargs
44
-
45
- async def query(self, query):
46
- results = set()
47
- cursor = ""
48
- for i in range(self.max_pages):
49
- url = f"{self.base_url}/v2/certificates/search"
50
- json_data = {
51
- "q": f"names: {query}",
52
- "per_page": 100,
53
- }
54
- if cursor:
55
- json_data.update({"cursor": cursor})
56
- resp = await self.api_request(
57
- url,
58
- method="POST",
59
- json=json_data,
60
- )
61
-
62
- if resp is None:
63
- break
64
-
65
- try:
66
- d = resp.json()
67
- except Exception as e:
68
- self.warning(f"Failed to parse JSON from {url} (response: {resp}): {e}")
69
-
70
- if resp.status_code < 200 or resp.status_code >= 400:
71
- if isinstance(d, dict):
72
- error = d.get("error", "")
73
- if error:
74
- self.warning(error)
75
- self.verbose(f'Non-200 Status code: {resp.status_code} for query "{query}", page #{i + 1}')
76
- self.debug(f"Response: {resp.text}")
77
- break
78
- else:
79
- if d is None:
80
- break
81
- elif not isinstance(d, dict):
82
- break
83
- status = d.get("status", "").lower()
84
- result = d.get("result", {})
85
- hits = result.get("hits", [])
86
- if status != "ok" or not hits:
87
- break
88
-
89
- for h in hits:
90
- names = h.get("names", [])
91
- for n in names:
92
- results.add(n.strip(".*").lower())
93
-
94
- cursor = result.get("links", {}).get("next", "")
95
- if not cursor:
96
- break
97
-
98
- return results
bbot/modules/gitlab.py DELETED
@@ -1,141 +0,0 @@
1
- from bbot.modules.base import BaseModule
2
-
3
-
4
- class gitlab(BaseModule):
5
- watched_events = ["HTTP_RESPONSE", "TECHNOLOGY", "SOCIAL"]
6
- produced_events = ["TECHNOLOGY", "SOCIAL", "CODE_REPOSITORY", "FINDING"]
7
- flags = ["active", "safe", "code-enum"]
8
- meta = {
9
- "description": "Detect GitLab instances and query them for repositories",
10
- "created_date": "2024-03-11",
11
- "author": "@TheTechromancer",
12
- }
13
- options = {"api_key": ""}
14
- options_desc = {"api_key": "Gitlab access token"}
15
-
16
- scope_distance_modifier = 2
17
-
18
- async def setup(self):
19
- await self.require_api_key()
20
- return True
21
-
22
- async def filter_event(self, event):
23
- # only accept out-of-scope SOCIAL events
24
- if event.type == "HTTP_RESPONSE":
25
- if event.scope_distance > self.scan.scope_search_distance:
26
- return False, "event is out of scope distance"
27
- elif event.type == "TECHNOLOGY":
28
- if not event.data["technology"].lower().startswith("gitlab"):
29
- return False, "technology is not gitlab"
30
- if not self.helpers.is_ip(event.host) and self.helpers.tldextract(event.host).domain == "gitlab":
31
- return False, "gitlab instance is not self-hosted"
32
- elif event.type == "SOCIAL":
33
- if event.data["platform"] != "gitlab":
34
- return False, "platform is not gitlab"
35
- return True
36
-
37
- async def handle_event(self, event):
38
- if event.type == "HTTP_RESPONSE":
39
- await self.handle_http_response(event)
40
- elif event.type == "TECHNOLOGY":
41
- await self.handle_technology(event)
42
- elif event.type == "SOCIAL":
43
- await self.handle_social(event)
44
-
45
- async def handle_http_response(self, event):
46
- # identify gitlab instances from HTTP responses
47
- # HTTP_RESPONSE --> TECHNOLOGY
48
- # HTTP_RESPONSE --> FINDING
49
- headers = event.data.get("header", {})
50
- if "x_gitlab_meta" in headers:
51
- url = event.parsed_url._replace(path="/").geturl()
52
- await self.emit_event(
53
- {"host": str(event.host), "technology": "GitLab", "url": url},
54
- "TECHNOLOGY",
55
- parent=event,
56
- context=f"{{module}} detected {{event.type}}: GitLab at {url}",
57
- )
58
- description = f"GitLab server at {event.host}"
59
- await self.emit_event(
60
- {"host": str(event.host), "description": description},
61
- "FINDING",
62
- parent=event,
63
- context=f"{{module}} detected {{event.type}}: {description}",
64
- )
65
-
66
- async def handle_technology(self, event):
67
- # retrieve gitlab groups from gitlab instances
68
- # TECHNOLOGY --> SOCIAL
69
- # TECHNOLOGY --> URL
70
- # TECHNOLOGY --> CODE_REPOSITORY
71
- base_url = self.get_base_url(event)
72
- projects_url = self.helpers.urljoin(base_url, "api/v4/projects?simple=true")
73
- await self.handle_projects_url(projects_url, event)
74
- groups_url = self.helpers.urljoin(base_url, "api/v4/groups?simple=true")
75
- await self.handle_groups_url(groups_url, event)
76
-
77
- async def handle_social(self, event):
78
- # retrieve repositories from gitlab user
79
- # SOCIAL --> CODE_REPOSITORY
80
- # SOCIAL --> SOCIAL
81
- username = event.data.get("profile_name", "")
82
- if not username:
83
- return
84
- base_url = self.get_base_url(event)
85
- urls = [
86
- # group
87
- self.helpers.urljoin(base_url, f"api/v4/users/{username}/projects?simple=true"),
88
- # user
89
- self.helpers.urljoin(base_url, f"api/v4/groups/{username}/projects?simple=true"),
90
- ]
91
- for url in urls:
92
- await self.handle_projects_url(url, event)
93
-
94
- async def handle_projects_url(self, projects_url, event):
95
- for project in await self.gitlab_json_request(projects_url):
96
- project_url = project.get("web_url", "")
97
- if project_url:
98
- code_event = self.make_event({"url": project_url}, "CODE_REPOSITORY", tags="git", parent=event)
99
- await self.emit_event(
100
- code_event, context=f"{{module}} enumerated projects and found {{event.type}} at {project_url}"
101
- )
102
- namespace = project.get("namespace", {})
103
- if namespace:
104
- await self.handle_namespace(namespace, event)
105
-
106
- async def handle_groups_url(self, groups_url, event):
107
- for group in await self.gitlab_json_request(groups_url):
108
- await self.handle_namespace(group, event)
109
-
110
- async def gitlab_json_request(self, url):
111
- response = await self.api_request(url)
112
- if response is not None:
113
- try:
114
- json = response.json()
115
- except Exception:
116
- return []
117
- if json and isinstance(json, list):
118
- return json
119
- return []
120
-
121
- async def handle_namespace(self, namespace, event):
122
- namespace_name = namespace.get("path", "")
123
- namespace_url = namespace.get("web_url", "")
124
- namespace_path = namespace.get("full_path", "")
125
- if namespace_name and namespace_url and namespace_path:
126
- namespace_url = self.helpers.parse_url(namespace_url)._replace(path=f"/{namespace_path}").geturl()
127
- social_event = self.make_event(
128
- {"platform": "gitlab", "profile_name": namespace_path, "url": namespace_url},
129
- "SOCIAL",
130
- parent=event,
131
- )
132
- await self.emit_event(
133
- social_event,
134
- context=f'{{module}} found GitLab namespace ({{event.type}}) "{namespace_name}" at {namespace_url}',
135
- )
136
-
137
- def get_base_url(self, event):
138
- base_url = event.data.get("url", "")
139
- if not base_url:
140
- base_url = f"https://{event.host}"
141
- return self.helpers.urlparse(base_url)._replace(path="/").geturl()
bbot/modules/zoomeye.py DELETED
@@ -1,77 +0,0 @@
1
- from bbot.modules.templates.subdomain_enum import subdomain_enum_apikey
2
-
3
-
4
- class zoomeye(subdomain_enum_apikey):
5
- watched_events = ["DNS_NAME"]
6
- produced_events = ["DNS_NAME"]
7
- flags = ["affiliates", "subdomain-enum", "passive", "safe"]
8
- meta = {
9
- "description": "Query ZoomEye's API for subdomains",
10
- "created_date": "2022-08-03",
11
- "author": "@TheTechromancer",
12
- "auth_required": True,
13
- }
14
- options = {"api_key": "", "max_pages": 20, "include_related": False}
15
- options_desc = {
16
- "api_key": "ZoomEye API key",
17
- "max_pages": "How many pages of results to fetch",
18
- "include_related": "Include domains which may be related to the target",
19
- }
20
-
21
- base_url = "https://api.zoomeye.hk"
22
-
23
- async def setup(self):
24
- self.max_pages = self.config.get("max_pages", 20)
25
- self.include_related = self.config.get("include_related", False)
26
- return await super().setup()
27
-
28
- def prepare_api_request(self, url, kwargs):
29
- kwargs["headers"]["API-KEY"] = self.api_key
30
- return url, kwargs
31
-
32
- async def ping(self):
33
- url = f"{self.base_url}/resources-info"
34
- r = await self.api_request(url, retry_on_http_429=False)
35
- assert int(r.json()["quota_info"]["remain_total_quota"]) > 0, "No quota remaining"
36
-
37
- async def handle_event(self, event):
38
- query = self.make_query(event)
39
- results = await self.query(query)
40
- if results:
41
- for hostname in results:
42
- if hostname == event:
43
- continue
44
- tags = []
45
- if not hostname.endswith(f".{query}"):
46
- tags = ["affiliate"]
47
- await self.emit_event(
48
- hostname,
49
- "DNS_NAME",
50
- event,
51
- tags=tags,
52
- context=f'{{module}} searched ZoomEye API for "{query}" and found {{event.type}}: {{event.data}}',
53
- )
54
-
55
- async def query(self, query):
56
- results = set()
57
- query_type = 0 if self.include_related else 1
58
- url = f"{self.base_url}/domain/search?q={self.helpers.quote(query)}&type={query_type}&page=" + "{page}"
59
- i = 0
60
- agen = self.api_page_iter(url)
61
- try:
62
- async for j in agen:
63
- r = list(await self.parse_results(j))
64
- if r:
65
- results.update(set(r))
66
- if not r or i >= (self.max_pages - 1):
67
- break
68
- i += 1
69
- finally:
70
- await agen.aclose()
71
- return results
72
-
73
- async def parse_results(self, r):
74
- results = set()
75
- for entry in r.get("list", []):
76
- results.add(entry["name"])
77
- return results
@@ -1,83 +0,0 @@
1
- from .base import ModuleTestBase
2
-
3
-
4
- class TestCensys(ModuleTestBase):
5
- config_overrides = {"modules": {"censys": {"api_key": "api_id:api_secret"}}}
6
-
7
- async def setup_before_prep(self, module_test):
8
- module_test.httpx_mock.add_response(
9
- url="https://search.censys.io/api/v1/account",
10
- match_headers={"Authorization": "Basic YXBpX2lkOmFwaV9zZWNyZXQ="},
11
- json={
12
- "email": "info@blacklanternsecurity.com",
13
- "login": "nope",
14
- "first_login": "1917-08-03 20:03:55",
15
- "last_login": "1918-05-19 01:15:22",
16
- "quota": {"used": 26, "allowance": 250, "resets_at": "1919-06-03 16:30:32"},
17
- },
18
- )
19
- module_test.httpx_mock.add_response(
20
- url="https://search.censys.io/api/v2/certificates/search",
21
- match_headers={"Authorization": "Basic YXBpX2lkOmFwaV9zZWNyZXQ="},
22
- method="POST",
23
- match_json={"q": "names: blacklanternsecurity.com", "per_page": 100},
24
- json={
25
- "code": 200,
26
- "status": "OK",
27
- "result": {
28
- "query": "names: blacklanternsecurity.com",
29
- "total": 196,
30
- "duration_ms": 1046,
31
- "hits": [
32
- {
33
- "parsed": {
34
- "validity_period": {
35
- "not_before": "2021-11-18T00:09:46Z",
36
- "not_after": "2022-11-18T00:09:46Z",
37
- },
38
- "issuer_dn": "C=US, ST=Arizona, L=Scottsdale, O=GoDaddy.com\\, Inc., OU=http://certs.godaddy.com/repository/, CN=Go Daddy Secure Certificate Authority - G2",
39
- "subject_dn": "CN=asdf.blacklanternsecurity.com",
40
- },
41
- "fingerprint_sha256": "590ad51b8db62925f0fd3f300264c6a36692e20ceec2b5a22e7e4b41c1575cdc",
42
- "names": ["asdf.blacklanternsecurity.com", "asdf2.blacklanternsecurity.com"],
43
- },
44
- ],
45
- "links": {"next": "NextToken", "prev": ""},
46
- },
47
- },
48
- )
49
- module_test.httpx_mock.add_response(
50
- url="https://search.censys.io/api/v2/certificates/search",
51
- match_headers={"Authorization": "Basic YXBpX2lkOmFwaV9zZWNyZXQ="},
52
- method="POST",
53
- match_json={"q": "names: blacklanternsecurity.com", "per_page": 100, "cursor": "NextToken"},
54
- json={
55
- "code": 200,
56
- "status": "OK",
57
- "result": {
58
- "query": "names: blacklanternsecurity.com",
59
- "total": 196,
60
- "duration_ms": 1046,
61
- "hits": [
62
- {
63
- "parsed": {
64
- "validity_period": {
65
- "not_before": "2021-11-18T00:09:46Z",
66
- "not_after": "2022-11-18T00:09:46Z",
67
- },
68
- "issuer_dn": "C=US, ST=Arizona, L=Scottsdale, O=GoDaddy.com\\, Inc., OU=http://certs.godaddy.com/repository/, CN=Go Daddy Secure Certificate Authority - G2",
69
- "subject_dn": "CN=zzzz.blacklanternsecurity.com",
70
- },
71
- "fingerprint_sha256": "590ad51b8db62925f0fd3f300264c6a36692e20ceec2b5a22e7e4b41c1575cdc",
72
- "names": ["zzzz.blacklanternsecurity.com"],
73
- },
74
- ],
75
- "links": {"next": "", "prev": ""},
76
- },
77
- },
78
- )
79
-
80
- def check(self, module_test, events):
81
- assert any(e.data == "asdf.blacklanternsecurity.com" for e in events), "Failed to detect asdf subdomain"
82
- assert any(e.data == "asdf2.blacklanternsecurity.com" for e in events), "Failed to detect asdf2 subdomain"
83
- assert any(e.data == "zzzz.blacklanternsecurity.com" for e in events), "Failed to detect zzzz subdomain"
@@ -1,35 +0,0 @@
1
- from .base import ModuleTestBase
2
-
3
-
4
- class TestZoomEye(ModuleTestBase):
5
- config_overrides = {"modules": {"zoomeye": {"api_key": "asdf", "include_related": True, "max_pages": 3}}}
6
-
7
- async def setup_before_prep(self, module_test):
8
- module_test.httpx_mock.add_response(
9
- url="https://api.zoomeye.hk/resources-info",
10
- match_headers={"API-KEY": "asdf"},
11
- json={"quota_info": {"remain_total_quota": 5}},
12
- )
13
- module_test.httpx_mock.add_response(
14
- url="https://api.zoomeye.hk/domain/search?q=blacklanternsecurity.com&type=0&page=1",
15
- json={"list": [{"name": "asdf.blacklanternsecurity.com"}]},
16
- )
17
- module_test.httpx_mock.add_response(
18
- url="https://api.zoomeye.hk/domain/search?q=blacklanternsecurity.com&type=0&page=2",
19
- json={"list": [{"name": "zzzz.blacklanternsecurity.com"}]},
20
- )
21
- module_test.httpx_mock.add_response(
22
- url="https://api.zoomeye.hk/domain/search?q=blacklanternsecurity.com&type=0&page=3",
23
- json={"list": [{"name": "ffff.blacklanternsecurity.com"}, {"name": "affiliate.bls"}]},
24
- )
25
- module_test.httpx_mock.add_response(
26
- url="https://api.zoomeye.hk/domain/search?q=blacklanternsecurity.com&type=0&page=4",
27
- json={"list": [{"name": "nope.blacklanternsecurity.com"}]},
28
- )
29
-
30
- def check(self, module_test, events):
31
- assert any(e.data == "asdf.blacklanternsecurity.com" for e in events), "Failed to detect subdomain #1"
32
- assert any(e.data == "zzzz.blacklanternsecurity.com" for e in events), "Failed to detect subdomain #2"
33
- assert any(e.data == "ffff.blacklanternsecurity.com" for e in events), "Failed to detect subdomain #3"
34
- assert any(e.data == "affiliate.bls" and "affiliate" in e.tags for e in events), "Failed to detect affiliate"
35
- assert not any(e.data == "nope.blacklanternsecurity.com" for e in events), "Failed to obey max_pages"