bbot 2.0.1.4720rc0__py3-none-any.whl → 2.3.0.5401rc0__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.
- bbot/__init__.py +1 -1
- bbot/cli.py +3 -7
- bbot/core/config/files.py +0 -1
- bbot/core/config/logger.py +34 -4
- bbot/core/core.py +21 -4
- bbot/core/engine.py +9 -8
- bbot/core/event/base.py +131 -52
- bbot/core/helpers/bloom.py +10 -3
- bbot/core/helpers/command.py +8 -7
- bbot/core/helpers/depsinstaller/installer.py +31 -13
- bbot/core/helpers/diff.py +10 -10
- bbot/core/helpers/dns/brute.py +7 -4
- bbot/core/helpers/dns/dns.py +1 -2
- bbot/core/helpers/dns/engine.py +4 -6
- bbot/core/helpers/dns/helpers.py +2 -2
- bbot/core/helpers/dns/mock.py +0 -1
- bbot/core/helpers/files.py +1 -1
- bbot/core/helpers/helper.py +7 -4
- bbot/core/helpers/interactsh.py +3 -3
- bbot/core/helpers/libmagic.py +65 -0
- bbot/core/helpers/misc.py +65 -22
- bbot/core/helpers/names_generator.py +17 -3
- bbot/core/helpers/process.py +0 -20
- bbot/core/helpers/regex.py +1 -1
- bbot/core/helpers/regexes.py +12 -6
- bbot/core/helpers/validators.py +1 -2
- bbot/core/helpers/web/client.py +1 -1
- bbot/core/helpers/web/engine.py +1 -2
- bbot/core/helpers/web/web.py +4 -114
- bbot/core/helpers/wordcloud.py +5 -5
- bbot/core/modules.py +36 -27
- bbot/core/multiprocess.py +58 -0
- bbot/core/shared_deps.py +46 -3
- bbot/db/sql/models.py +147 -0
- bbot/defaults.yml +12 -10
- bbot/modules/anubisdb.py +2 -2
- bbot/modules/apkpure.py +63 -0
- bbot/modules/azure_tenant.py +2 -2
- bbot/modules/baddns.py +35 -19
- bbot/modules/baddns_direct.py +92 -0
- bbot/modules/baddns_zone.py +3 -8
- bbot/modules/badsecrets.py +4 -3
- bbot/modules/base.py +195 -51
- bbot/modules/bevigil.py +7 -7
- bbot/modules/binaryedge.py +7 -4
- bbot/modules/bufferoverrun.py +47 -0
- bbot/modules/builtwith.py +6 -10
- bbot/modules/bypass403.py +5 -5
- bbot/modules/c99.py +10 -7
- bbot/modules/censys.py +9 -13
- bbot/modules/certspotter.py +5 -3
- bbot/modules/chaos.py +9 -7
- bbot/modules/code_repository.py +1 -0
- bbot/modules/columbus.py +3 -3
- bbot/modules/crt.py +5 -3
- bbot/modules/deadly/dastardly.py +1 -1
- bbot/modules/deadly/ffuf.py +9 -9
- bbot/modules/deadly/nuclei.py +3 -3
- bbot/modules/deadly/vhost.py +4 -3
- bbot/modules/dehashed.py +1 -1
- bbot/modules/digitorus.py +1 -1
- bbot/modules/dnsbimi.py +145 -0
- bbot/modules/dnscaa.py +3 -3
- bbot/modules/dnsdumpster.py +4 -4
- bbot/modules/dnstlsrpt.py +144 -0
- bbot/modules/docker_pull.py +7 -5
- bbot/modules/dockerhub.py +2 -2
- bbot/modules/dotnetnuke.py +20 -21
- bbot/modules/emailformat.py +1 -1
- bbot/modules/extractous.py +122 -0
- bbot/modules/filedownload.py +9 -7
- bbot/modules/fullhunt.py +7 -4
- bbot/modules/generic_ssrf.py +5 -5
- bbot/modules/github_codesearch.py +3 -2
- bbot/modules/github_org.py +4 -4
- bbot/modules/github_workflows.py +4 -4
- bbot/modules/gitlab.py +2 -5
- bbot/modules/google_playstore.py +93 -0
- bbot/modules/gowitness.py +48 -50
- bbot/modules/hackertarget.py +5 -3
- bbot/modules/host_header.py +5 -5
- bbot/modules/httpx.py +1 -4
- bbot/modules/hunterio.py +3 -9
- bbot/modules/iis_shortnames.py +19 -30
- bbot/modules/internal/cloudcheck.py +29 -12
- bbot/modules/internal/dnsresolve.py +22 -22
- bbot/modules/internal/excavate.py +97 -59
- bbot/modules/internal/speculate.py +41 -32
- bbot/modules/internetdb.py +4 -2
- bbot/modules/ip2location.py +3 -5
- bbot/modules/ipneighbor.py +1 -1
- bbot/modules/ipstack.py +3 -8
- bbot/modules/jadx.py +87 -0
- bbot/modules/leakix.py +11 -10
- bbot/modules/myssl.py +2 -2
- bbot/modules/newsletters.py +2 -2
- bbot/modules/otx.py +5 -3
- bbot/modules/output/asset_inventory.py +7 -7
- bbot/modules/output/base.py +1 -1
- bbot/modules/output/csv.py +1 -1
- bbot/modules/output/http.py +20 -14
- bbot/modules/output/mysql.py +51 -0
- bbot/modules/output/neo4j.py +7 -2
- bbot/modules/output/postgres.py +49 -0
- bbot/modules/output/slack.py +0 -1
- bbot/modules/output/sqlite.py +29 -0
- bbot/modules/output/stdout.py +2 -2
- bbot/modules/output/teams.py +107 -6
- bbot/modules/paramminer_headers.py +8 -11
- bbot/modules/passivetotal.py +13 -13
- bbot/modules/portscan.py +32 -6
- bbot/modules/postman.py +50 -126
- bbot/modules/postman_download.py +220 -0
- bbot/modules/rapiddns.py +3 -8
- bbot/modules/report/asn.py +18 -11
- bbot/modules/robots.py +3 -3
- bbot/modules/securitytrails.py +7 -10
- bbot/modules/securitytxt.py +1 -1
- bbot/modules/shodan_dns.py +7 -9
- bbot/modules/sitedossier.py +1 -1
- bbot/modules/skymem.py +2 -2
- bbot/modules/social.py +2 -1
- bbot/modules/subdomaincenter.py +1 -1
- bbot/modules/subdomainradar.py +160 -0
- bbot/modules/telerik.py +8 -8
- bbot/modules/templates/bucket.py +1 -1
- bbot/modules/templates/github.py +22 -14
- bbot/modules/templates/postman.py +21 -0
- bbot/modules/templates/shodan.py +14 -13
- bbot/modules/templates/sql.py +95 -0
- bbot/modules/templates/subdomain_enum.py +51 -16
- bbot/modules/templates/webhook.py +2 -4
- bbot/modules/trickest.py +8 -37
- bbot/modules/trufflehog.py +10 -12
- bbot/modules/url_manipulation.py +3 -3
- bbot/modules/urlscan.py +1 -1
- bbot/modules/viewdns.py +1 -1
- bbot/modules/virustotal.py +8 -30
- bbot/modules/wafw00f.py +1 -1
- bbot/modules/wayback.py +1 -1
- bbot/modules/wpscan.py +17 -11
- bbot/modules/zoomeye.py +11 -6
- bbot/presets/baddns-thorough.yml +12 -0
- bbot/presets/fast.yml +16 -0
- bbot/presets/kitchen-sink.yml +1 -2
- bbot/presets/spider.yml +4 -0
- bbot/presets/subdomain-enum.yml +7 -7
- bbot/presets/web/dotnet-audit.yml +0 -1
- bbot/scanner/manager.py +5 -16
- bbot/scanner/preset/args.py +46 -26
- bbot/scanner/preset/environ.py +7 -2
- bbot/scanner/preset/path.py +7 -4
- bbot/scanner/preset/preset.py +36 -23
- bbot/scanner/scanner.py +172 -62
- bbot/scanner/target.py +236 -434
- bbot/scripts/docs.py +1 -1
- bbot/test/bbot_fixtures.py +13 -3
- bbot/test/conftest.py +132 -100
- bbot/test/fastapi_test.py +17 -0
- bbot/test/owasp_mastg.apk +0 -0
- bbot/test/run_tests.sh +4 -4
- bbot/test/test.conf +2 -0
- bbot/test/test_step_1/test__module__tests.py +0 -1
- bbot/test/test_step_1/test_bbot_fastapi.py +79 -0
- bbot/test/test_step_1/test_bloom_filter.py +2 -1
- bbot/test/test_step_1/test_cli.py +138 -64
- bbot/test/test_step_1/test_dns.py +61 -27
- bbot/test/test_step_1/test_engine.py +17 -19
- bbot/test/test_step_1/test_events.py +183 -30
- bbot/test/test_step_1/test_helpers.py +64 -29
- bbot/test/test_step_1/test_manager_deduplication.py +1 -1
- bbot/test/test_step_1/test_manager_scope_accuracy.py +333 -330
- bbot/test/test_step_1/test_modules_basic.py +68 -70
- bbot/test/test_step_1/test_presets.py +183 -100
- bbot/test/test_step_1/test_python_api.py +7 -2
- bbot/test/test_step_1/test_regexes.py +35 -5
- bbot/test/test_step_1/test_scan.py +39 -5
- bbot/test/test_step_1/test_scope.py +4 -3
- bbot/test/test_step_1/test_target.py +242 -145
- bbot/test/test_step_1/test_web.py +14 -10
- bbot/test/test_step_2/module_tests/base.py +15 -7
- bbot/test/test_step_2/module_tests/test_module_anubisdb.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_apkpure.py +71 -0
- bbot/test/test_step_2/module_tests/test_module_asset_inventory.py +0 -1
- bbot/test/test_step_2/module_tests/test_module_azure_realm.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_baddns.py +6 -6
- bbot/test/test_step_2/module_tests/test_module_baddns_direct.py +62 -0
- bbot/test/test_step_2/module_tests/test_module_bevigil.py +29 -2
- bbot/test/test_step_2/module_tests/test_module_binaryedge.py +4 -2
- bbot/test/test_step_2/module_tests/test_module_bucket_amazon.py +2 -2
- bbot/test/test_step_2/module_tests/test_module_bucket_azure.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_bufferoverrun.py +35 -0
- bbot/test/test_step_2/module_tests/test_module_builtwith.py +2 -2
- bbot/test/test_step_2/module_tests/test_module_bypass403.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_c99.py +126 -0
- bbot/test/test_step_2/module_tests/test_module_censys.py +4 -1
- bbot/test/test_step_2/module_tests/test_module_cloudcheck.py +4 -0
- bbot/test/test_step_2/module_tests/test_module_code_repository.py +11 -1
- bbot/test/test_step_2/module_tests/test_module_columbus.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_credshed.py +3 -3
- bbot/test/test_step_2/module_tests/test_module_dastardly.py +2 -1
- bbot/test/test_step_2/module_tests/test_module_dehashed.py +2 -2
- bbot/test/test_step_2/module_tests/test_module_digitorus.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_discord.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_dnsbimi.py +103 -0
- bbot/test/test_step_2/module_tests/test_module_dnsbrute.py +9 -10
- bbot/test/test_step_2/module_tests/test_module_dnsbrute_mutations.py +1 -2
- bbot/test/test_step_2/module_tests/test_module_dnscommonsrv.py +1 -2
- bbot/test/test_step_2/module_tests/test_module_dnsdumpster.py +4 -4
- bbot/test/test_step_2/module_tests/test_module_dnstlsrpt.py +64 -0
- bbot/test/test_step_2/module_tests/test_module_dotnetnuke.py +0 -8
- bbot/test/test_step_2/module_tests/test_module_excavate.py +28 -48
- bbot/test/test_step_2/module_tests/test_module_extractous.py +54 -0
- bbot/test/test_step_2/module_tests/test_module_ffuf_shortnames.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_filedownload.py +14 -14
- bbot/test/test_step_2/module_tests/test_module_git_clone.py +2 -2
- bbot/test/test_step_2/module_tests/test_module_github_org.py +19 -8
- bbot/test/test_step_2/module_tests/test_module_github_workflows.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_gitlab.py +9 -4
- bbot/test/test_step_2/module_tests/test_module_google_playstore.py +83 -0
- bbot/test/test_step_2/module_tests/test_module_gowitness.py +4 -6
- bbot/test/test_step_2/module_tests/test_module_host_header.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_http.py +4 -4
- bbot/test/test_step_2/module_tests/test_module_httpx.py +10 -8
- bbot/test/test_step_2/module_tests/test_module_hunterio.py +68 -4
- bbot/test/test_step_2/module_tests/test_module_jadx.py +55 -0
- bbot/test/test_step_2/module_tests/test_module_json.py +22 -9
- bbot/test/test_step_2/module_tests/test_module_leakix.py +7 -3
- bbot/test/test_step_2/module_tests/test_module_mysql.py +76 -0
- bbot/test/test_step_2/module_tests/test_module_myssl.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_neo4j.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_newsletters.py +16 -16
- bbot/test/test_step_2/module_tests/test_module_ntlm.py +8 -7
- bbot/test/test_step_2/module_tests/test_module_oauth.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_otx.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_paramminer_cookies.py +1 -2
- bbot/test/test_step_2/module_tests/test_module_paramminer_getparams.py +0 -6
- bbot/test/test_step_2/module_tests/test_module_paramminer_headers.py +2 -9
- bbot/test/test_step_2/module_tests/test_module_passivetotal.py +3 -1
- bbot/test/test_step_2/module_tests/test_module_pgp.py +2 -2
- bbot/test/test_step_2/module_tests/test_module_portscan.py +9 -8
- bbot/test/test_step_2/module_tests/test_module_postgres.py +74 -0
- bbot/test/test_step_2/module_tests/test_module_postman.py +84 -253
- bbot/test/test_step_2/module_tests/test_module_postman_download.py +439 -0
- bbot/test/test_step_2/module_tests/test_module_rapiddns.py +93 -1
- bbot/test/test_step_2/module_tests/test_module_shodan_dns.py +20 -1
- bbot/test/test_step_2/module_tests/test_module_sitedossier.py +2 -2
- bbot/test/test_step_2/module_tests/test_module_smuggler.py +14 -14
- bbot/test/test_step_2/module_tests/test_module_social.py +11 -1
- bbot/test/test_step_2/module_tests/test_module_speculate.py +4 -8
- bbot/test/test_step_2/module_tests/test_module_splunk.py +4 -4
- bbot/test/test_step_2/module_tests/test_module_sqlite.py +18 -0
- bbot/test/test_step_2/module_tests/test_module_sslcert.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_stdout.py +5 -3
- bbot/test/test_step_2/module_tests/test_module_subdomaincenter.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_subdomainradar.py +208 -0
- bbot/test/test_step_2/module_tests/test_module_subdomains.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_teams.py +8 -6
- bbot/test/test_step_2/module_tests/test_module_telerik.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_trufflehog.py +317 -14
- bbot/test/test_step_2/module_tests/test_module_viewdns.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_wayback.py +1 -1
- bbot/test/test_step_2/template_tests/test_template_subdomain_enum.py +2 -2
- bbot/wordlists/devops_mutations.txt +1 -1
- bbot/wordlists/ffuf_shortname_candidates.txt +1 -1
- bbot/wordlists/nameservers.txt +1 -1
- bbot/wordlists/paramminer_headers.txt +1 -1
- bbot/wordlists/paramminer_parameters.txt +1 -1
- bbot/wordlists/raft-small-extensions-lowercase_CLEANED.txt +1 -1
- bbot/wordlists/valid_url_schemes.txt +1 -1
- {bbot-2.0.1.4720rc0.dist-info → bbot-2.3.0.5401rc0.dist-info}/METADATA +48 -18
- bbot-2.3.0.5401rc0.dist-info/RECORD +421 -0
- {bbot-2.0.1.4720rc0.dist-info → bbot-2.3.0.5401rc0.dist-info}/WHEEL +1 -1
- bbot/modules/unstructured.py +0 -163
- bbot/test/test_step_2/module_tests/test_module_unstructured.py +0 -102
- bbot-2.0.1.4720rc0.dist-info/RECORD +0 -387
- {bbot-2.0.1.4720rc0.dist-info → bbot-2.3.0.5401rc0.dist-info}/LICENSE +0 -0
- {bbot-2.0.1.4720rc0.dist-info → bbot-2.3.0.5401rc0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from bbot.modules.templates.sql import SQLTemplate
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SQLite(SQLTemplate):
|
|
7
|
+
watched_events = ["*"]
|
|
8
|
+
meta = {"description": "Output scan data to a SQLite database"}
|
|
9
|
+
options = {
|
|
10
|
+
"database": "",
|
|
11
|
+
}
|
|
12
|
+
options_desc = {
|
|
13
|
+
"database": "The path to the sqlite database file",
|
|
14
|
+
}
|
|
15
|
+
deps_pip = ["sqlmodel", "aiosqlite"]
|
|
16
|
+
|
|
17
|
+
async def setup(self):
|
|
18
|
+
db_file = self.config.get("database", "")
|
|
19
|
+
if not db_file:
|
|
20
|
+
db_file = self.scan.home / "output.sqlite"
|
|
21
|
+
db_file = Path(db_file)
|
|
22
|
+
if not db_file.is_absolute():
|
|
23
|
+
db_file = self.scan.home / db_file
|
|
24
|
+
self.db_file = db_file
|
|
25
|
+
self.db_file.parent.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
return await super().setup()
|
|
27
|
+
|
|
28
|
+
def connection_string(self, mask_password=False):
|
|
29
|
+
return f"sqlite+aiosqlite:///{self.db_file}"
|
bbot/modules/output/stdout.py
CHANGED
|
@@ -20,7 +20,7 @@ class Stdout(BaseOutputModule):
|
|
|
20
20
|
|
|
21
21
|
async def setup(self):
|
|
22
22
|
self.text_format = self.config.get("format", "text").strip().lower()
|
|
23
|
-
if
|
|
23
|
+
if self.text_format not in self.format_choices:
|
|
24
24
|
return (
|
|
25
25
|
False,
|
|
26
26
|
f'Invalid text format choice, "{self.text_format}" (choices: {",".join(self.format_choices)})',
|
|
@@ -33,7 +33,7 @@ class Stdout(BaseOutputModule):
|
|
|
33
33
|
|
|
34
34
|
async def filter_event(self, event):
|
|
35
35
|
if self.accept_event_types:
|
|
36
|
-
if
|
|
36
|
+
if event.type not in self.accept_event_types:
|
|
37
37
|
return False, f'Event type "{event.type}" is not in the allowed event_types'
|
|
38
38
|
return True
|
|
39
39
|
|
bbot/modules/output/teams.py
CHANGED
|
@@ -10,14 +10,115 @@ class Teams(WebhookOutputModule):
|
|
|
10
10
|
}
|
|
11
11
|
options = {"webhook_url": "", "event_types": ["VULNERABILITY", "FINDING"], "min_severity": "LOW"}
|
|
12
12
|
options_desc = {
|
|
13
|
-
"webhook_url": "
|
|
13
|
+
"webhook_url": "Teams webhook URL",
|
|
14
14
|
"event_types": "Types of events to send",
|
|
15
15
|
"min_severity": "Only allow VULNERABILITY events of this severity or higher",
|
|
16
16
|
}
|
|
17
17
|
_module_threads = 5
|
|
18
|
-
good_status_code = 200
|
|
19
|
-
content_key = "text"
|
|
20
18
|
|
|
21
|
-
def
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
async def handle_event(self, event):
|
|
20
|
+
while 1:
|
|
21
|
+
data = self.format_message(event)
|
|
22
|
+
|
|
23
|
+
response = await self.helpers.request(
|
|
24
|
+
url=self.webhook_url,
|
|
25
|
+
method="POST",
|
|
26
|
+
json=data,
|
|
27
|
+
)
|
|
28
|
+
status_code = getattr(response, "status_code", 0)
|
|
29
|
+
if self.evaluate_response(response):
|
|
30
|
+
break
|
|
31
|
+
else:
|
|
32
|
+
response_data = getattr(response, "text", "")
|
|
33
|
+
try:
|
|
34
|
+
retry_after = response.json().get("retry_after", 1)
|
|
35
|
+
except Exception:
|
|
36
|
+
retry_after = 1
|
|
37
|
+
self.verbose(
|
|
38
|
+
f"Error sending {event}: status code {status_code}, response: {response_data}, retrying in {retry_after} seconds"
|
|
39
|
+
)
|
|
40
|
+
await self.helpers.sleep(retry_after)
|
|
41
|
+
|
|
42
|
+
def trim_message(self, message):
|
|
43
|
+
if len(message) > self.message_size_limit:
|
|
44
|
+
message = message[: self.message_size_limit - 3] + "..."
|
|
45
|
+
return message
|
|
46
|
+
|
|
47
|
+
def format_message_str(self, event):
|
|
48
|
+
items = []
|
|
49
|
+
msg = self.trim_message(event.data)
|
|
50
|
+
items.append({"type": "TextBlock", "text": f"{msg}", "wrap": True})
|
|
51
|
+
items.append({"type": "FactSet", "facts": [{"title": "Tags:", "value": ", ".join(event.tags)}]})
|
|
52
|
+
return items
|
|
53
|
+
|
|
54
|
+
def format_message_other(self, event):
|
|
55
|
+
items = [{"type": "FactSet", "facts": []}]
|
|
56
|
+
for key, value in event.data.items():
|
|
57
|
+
if key != "severity":
|
|
58
|
+
msg = self.trim_message(str(value))
|
|
59
|
+
items[0]["facts"].append({"title": f"{key}:", "value": msg})
|
|
60
|
+
return items
|
|
61
|
+
|
|
62
|
+
def get_severity_color(self, event):
|
|
63
|
+
color = "Accent"
|
|
64
|
+
if event.type == "VULNERABILITY":
|
|
65
|
+
severity = event.data.get("severity", "UNKNOWN")
|
|
66
|
+
if severity == "CRITICAL":
|
|
67
|
+
color = "Attention"
|
|
68
|
+
elif severity == "HIGH":
|
|
69
|
+
color = "Attention"
|
|
70
|
+
elif severity == "MEDIUM":
|
|
71
|
+
color = "Warning"
|
|
72
|
+
elif severity == "LOW":
|
|
73
|
+
color = "Good"
|
|
74
|
+
return color
|
|
75
|
+
|
|
76
|
+
def format_message(self, event):
|
|
77
|
+
adaptive_card = {
|
|
78
|
+
"type": "message",
|
|
79
|
+
"attachments": [
|
|
80
|
+
{
|
|
81
|
+
"contentType": "application/vnd.microsoft.card.adaptive",
|
|
82
|
+
"contentUrl": None,
|
|
83
|
+
"content": {
|
|
84
|
+
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
|
85
|
+
"type": "AdaptiveCard",
|
|
86
|
+
"version": "1.2",
|
|
87
|
+
"msteams": {"width": "full"},
|
|
88
|
+
"body": [],
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
],
|
|
92
|
+
}
|
|
93
|
+
heading = {"type": "TextBlock", "text": f"{event.type}", "wrap": True, "size": "Large", "style": "heading"}
|
|
94
|
+
body = adaptive_card["attachments"][0]["content"]["body"]
|
|
95
|
+
body.append(heading)
|
|
96
|
+
if event.type in ("VULNERABILITY", "FINDING"):
|
|
97
|
+
subheading = {
|
|
98
|
+
"type": "TextBlock",
|
|
99
|
+
"text": event.data.get("severity", "UNKNOWN"),
|
|
100
|
+
"spacing": "None",
|
|
101
|
+
"size": "Large",
|
|
102
|
+
"wrap": True,
|
|
103
|
+
}
|
|
104
|
+
subheading["color"] = self.get_severity_color(event)
|
|
105
|
+
body.append(subheading)
|
|
106
|
+
main_text = {
|
|
107
|
+
"type": "ColumnSet",
|
|
108
|
+
"separator": True,
|
|
109
|
+
"spacing": "Medium",
|
|
110
|
+
"columns": [
|
|
111
|
+
{
|
|
112
|
+
"type": "Column",
|
|
113
|
+
"width": "stretch",
|
|
114
|
+
"items": [],
|
|
115
|
+
}
|
|
116
|
+
],
|
|
117
|
+
}
|
|
118
|
+
if isinstance(event.data, str):
|
|
119
|
+
items = self.format_message_str(event)
|
|
120
|
+
else:
|
|
121
|
+
items = self.format_message_other(event)
|
|
122
|
+
main_text["columns"][0]["items"] = items
|
|
123
|
+
body.append(main_text)
|
|
124
|
+
return adaptive_card
|
|
@@ -15,7 +15,7 @@ class paramminer_headers(BaseModule):
|
|
|
15
15
|
meta = {
|
|
16
16
|
"description": "Use smart brute-force to check for common HTTP header parameters",
|
|
17
17
|
"created_date": "2022-04-15",
|
|
18
|
-
"author": "@
|
|
18
|
+
"author": "@liquidsec",
|
|
19
19
|
}
|
|
20
20
|
options = {
|
|
21
21
|
"wordlist": "", # default is defined within setup function
|
|
@@ -82,7 +82,6 @@ class paramminer_headers(BaseModule):
|
|
|
82
82
|
header_regex = re.compile(r"^[!#$%&\'*+\-.^_`|~0-9a-zA-Z]+: [^\r\n]+$")
|
|
83
83
|
|
|
84
84
|
async def setup(self):
|
|
85
|
-
|
|
86
85
|
self.recycle_words = self.config.get("recycle_words", True)
|
|
87
86
|
self.event_dict = {}
|
|
88
87
|
self.already_checked = set()
|
|
@@ -90,11 +89,11 @@ class paramminer_headers(BaseModule):
|
|
|
90
89
|
if not wordlist:
|
|
91
90
|
wordlist = f"{self.helpers.wordlist_dir}/{self.default_wordlist}"
|
|
92
91
|
self.debug(f"Using wordlist: [{wordlist}]")
|
|
93
|
-
self.wl =
|
|
92
|
+
self.wl = {
|
|
94
93
|
h.strip().lower()
|
|
95
94
|
for h in self.helpers.read_file(await self.helpers.wordlist(wordlist))
|
|
96
95
|
if len(h) > 0 and "%" not in h
|
|
97
|
-
|
|
96
|
+
}
|
|
98
97
|
|
|
99
98
|
# check against the boring list (if the option is set)
|
|
100
99
|
if self.config.get("skip_boring_words", True):
|
|
@@ -157,7 +156,6 @@ class paramminer_headers(BaseModule):
|
|
|
157
156
|
)
|
|
158
157
|
|
|
159
158
|
async def handle_event(self, event):
|
|
160
|
-
|
|
161
159
|
# If recycle words is enabled, we will collect WEB_PARAMETERS we find to build our list in finish()
|
|
162
160
|
# We also collect any parameters of type "SPECULATIVE"
|
|
163
161
|
if event.type == "WEB_PARAMETER":
|
|
@@ -174,7 +172,7 @@ class paramminer_headers(BaseModule):
|
|
|
174
172
|
self.debug(f"Error initializing compare helper: {e}")
|
|
175
173
|
return
|
|
176
174
|
batch_size = await self.count_test(url)
|
|
177
|
-
if batch_size
|
|
175
|
+
if batch_size is None or batch_size <= 0:
|
|
178
176
|
self.debug(f"Failed to get baseline max {self.compare_mode} count, aborting")
|
|
179
177
|
return
|
|
180
178
|
self.debug(f"Resolved batch_size at {str(batch_size)}")
|
|
@@ -197,11 +195,11 @@ class paramminer_headers(BaseModule):
|
|
|
197
195
|
baseline = await self.helpers.request(url)
|
|
198
196
|
if baseline is None:
|
|
199
197
|
return
|
|
200
|
-
if str(baseline.status_code)[0] in
|
|
198
|
+
if str(baseline.status_code)[0] in {"4", "5"}:
|
|
201
199
|
return
|
|
202
200
|
for count, args, kwargs in self.gen_count_args(url):
|
|
203
201
|
r = await self.helpers.request(*args, **kwargs)
|
|
204
|
-
if r is not None and
|
|
202
|
+
if r is not None and str(r.status_code)[0] not in {"4", "5"}:
|
|
205
203
|
return count
|
|
206
204
|
|
|
207
205
|
def gen_count_args(self, url):
|
|
@@ -224,7 +222,7 @@ class paramminer_headers(BaseModule):
|
|
|
224
222
|
elif len(group) > 1 or (len(group) == 1 and len(reasons) == 0):
|
|
225
223
|
for group_slice in self.helpers.split_list(group):
|
|
226
224
|
match, reasons, reflection, subject_response = await self.check_batch(compare_helper, url, group_slice)
|
|
227
|
-
if match
|
|
225
|
+
if match is False:
|
|
228
226
|
async for r in self.binary_search(compare_helper, url, group_slice, reasons, reflection):
|
|
229
227
|
yield r
|
|
230
228
|
else:
|
|
@@ -240,8 +238,7 @@ class paramminer_headers(BaseModule):
|
|
|
240
238
|
return await compare_helper.compare(url, headers=test_headers, check_reflection=(len(header_list) == 1))
|
|
241
239
|
|
|
242
240
|
async def finish(self):
|
|
243
|
-
|
|
244
|
-
untested_matches = sorted(list(self.extracted_words_master.copy()))
|
|
241
|
+
untested_matches = sorted(self.extracted_words_master.copy())
|
|
245
242
|
for url, (event, batch_size) in list(self.event_dict.items()):
|
|
246
243
|
try:
|
|
247
244
|
compare_helper = self.helpers.http_compare(url)
|
bbot/modules/passivetotal.py
CHANGED
|
@@ -11,36 +11,36 @@ class passivetotal(subdomain_enum_apikey):
|
|
|
11
11
|
"author": "@TheTechromancer",
|
|
12
12
|
"auth_required": True,
|
|
13
13
|
}
|
|
14
|
-
options = {"
|
|
15
|
-
options_desc = {"
|
|
14
|
+
options = {"api_key": ""}
|
|
15
|
+
options_desc = {"api_key": "PassiveTotal API Key in the format of 'username:api_key'"}
|
|
16
16
|
|
|
17
17
|
base_url = "https://api.passivetotal.org/v2"
|
|
18
18
|
|
|
19
19
|
async def setup(self):
|
|
20
|
-
self.username = self.config.get("username", "")
|
|
21
|
-
self.api_key = self.config.get("api_key", "")
|
|
22
|
-
self.auth = (self.username, self.api_key)
|
|
23
20
|
return await super().setup()
|
|
24
21
|
|
|
25
22
|
async def ping(self):
|
|
26
23
|
url = f"{self.base_url}/account/quota"
|
|
27
|
-
j = (await self.
|
|
24
|
+
j = (await self.api_request(url)).json()
|
|
28
25
|
limit = j["user"]["limits"]["search_api"]
|
|
29
26
|
used = j["user"]["counts"]["search_api"]
|
|
30
27
|
assert used < limit, "No quota remaining"
|
|
31
28
|
|
|
29
|
+
def prepare_api_request(self, url, kwargs):
|
|
30
|
+
api_username, api_key = self.api_key.split(":", 1)
|
|
31
|
+
kwargs["auth"] = (api_username, api_key)
|
|
32
|
+
return url, kwargs
|
|
33
|
+
|
|
32
34
|
async def abort_if(self, event):
|
|
33
35
|
# RiskIQ is famous for their junk data
|
|
34
36
|
return await super().abort_if(event) or "unresolved" in event.tags
|
|
35
37
|
|
|
36
38
|
async def request_url(self, query):
|
|
37
39
|
url = f"{self.base_url}/enrichment/subdomains?query={self.helpers.quote(query)}"
|
|
38
|
-
return await self.
|
|
40
|
+
return await self.api_request(url)
|
|
39
41
|
|
|
40
|
-
def parse_results(self, r, query):
|
|
42
|
+
async def parse_results(self, r, query):
|
|
43
|
+
results = set()
|
|
41
44
|
for subdomain in r.json().get("subdomains", []):
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
@property
|
|
45
|
-
def auth_secret(self):
|
|
46
|
-
return self.username and self.api_key
|
|
45
|
+
results.add(f"{subdomain}.{query}")
|
|
46
|
+
return results
|
bbot/modules/portscan.py
CHANGED
|
@@ -6,6 +6,9 @@ from radixtarget import RadixTarget
|
|
|
6
6
|
from bbot.modules.base import BaseModule
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
# TODO: this module is getting big. It should probably be two modules: one for ping and one for SYN.
|
|
10
|
+
|
|
11
|
+
|
|
9
12
|
class portscan(BaseModule):
|
|
10
13
|
flags = ["active", "portscan", "safe"]
|
|
11
14
|
watched_events = ["IP_ADDRESS", "IP_RANGE", "DNS_NAME"]
|
|
@@ -27,6 +30,8 @@ class portscan(BaseModule):
|
|
|
27
30
|
"adapter_ip": "",
|
|
28
31
|
"adapter_mac": "",
|
|
29
32
|
"router_mac": "",
|
|
33
|
+
"cdn_tags": "cdn-",
|
|
34
|
+
"allowed_cdn_ports": None,
|
|
30
35
|
}
|
|
31
36
|
options_desc = {
|
|
32
37
|
"top_ports": "Top ports to scan (default 100) (to override, specify 'ports')",
|
|
@@ -39,6 +44,8 @@ class portscan(BaseModule):
|
|
|
39
44
|
"adapter_ip": "Send packets using this IP address. Not needed unless masscan's autodetection fails",
|
|
40
45
|
"adapter_mac": "Send packets using this as the source MAC address. Not needed unless masscan's autodetection fails",
|
|
41
46
|
"router_mac": "Send packets to this MAC address as the destination. Not needed unless masscan's autodetection fails",
|
|
47
|
+
"cdn_tags": "Comma-separated list of tags to skip, e.g. 'cdn,cloud'",
|
|
48
|
+
"allowed_cdn_ports": "Comma-separated list of ports that are allowed to be scanned for CDNs",
|
|
42
49
|
}
|
|
43
50
|
deps_common = ["masscan"]
|
|
44
51
|
batch_size = 1000000
|
|
@@ -60,7 +67,15 @@ class portscan(BaseModule):
|
|
|
60
67
|
try:
|
|
61
68
|
self.helpers.parse_port_string(self.ports)
|
|
62
69
|
except ValueError as e:
|
|
63
|
-
return False, f"Error parsing ports: {e}"
|
|
70
|
+
return False, f"Error parsing ports '{self.ports}': {e}"
|
|
71
|
+
self.cdn_tags = [t.strip() for t in self.config.get("cdn_tags", "").split(",")]
|
|
72
|
+
self.allowed_cdn_ports = self.config.get("allowed_cdn_ports", None)
|
|
73
|
+
if self.allowed_cdn_ports is not None:
|
|
74
|
+
try:
|
|
75
|
+
self.allowed_cdn_ports = [int(p.strip()) for p in self.allowed_cdn_ports.split(",")]
|
|
76
|
+
except Exception as e:
|
|
77
|
+
return False, f"Error parsing allowed CDN ports '{self.allowed_cdn_ports}': {e}"
|
|
78
|
+
|
|
64
79
|
# whether we've finished scanning our original scan targets
|
|
65
80
|
self.scanned_initial_targets = False
|
|
66
81
|
# keeps track of individual scanned IPs and their open ports
|
|
@@ -84,17 +99,17 @@ class portscan(BaseModule):
|
|
|
84
99
|
return False, "Masscan failed to run"
|
|
85
100
|
returncode = getattr(ipv6_result, "returncode", 0)
|
|
86
101
|
if returncode and "failed to detect IPv6 address" in ipv6_result.stderr:
|
|
87
|
-
self.warning(
|
|
102
|
+
self.warning("It looks like you are not set up for IPv6. IPv6 targets will not be scanned.")
|
|
88
103
|
self.ipv6_support = False
|
|
89
104
|
return True
|
|
90
105
|
|
|
91
106
|
async def handle_batch(self, *events):
|
|
92
|
-
# on our first run, we automatically include all our
|
|
107
|
+
# on our first run, we automatically include all our initial scan targets
|
|
93
108
|
if not self.scanned_initial_targets:
|
|
94
109
|
self.scanned_initial_targets = True
|
|
95
110
|
events = set(events)
|
|
96
111
|
events.update(
|
|
97
|
-
|
|
112
|
+
{e for e in self.scan.target.seeds.events if e.type in ("DNS_NAME", "IP_ADDRESS", "IP_RANGE")}
|
|
98
113
|
)
|
|
99
114
|
|
|
100
115
|
# ping scan
|
|
@@ -227,9 +242,20 @@ class portscan(BaseModule):
|
|
|
227
242
|
parent=parent_event,
|
|
228
243
|
context=f"{{module}} executed a {scan_type} scan against {parent_event.data} and found: {{event.type}}: {{event.data}}",
|
|
229
244
|
)
|
|
230
|
-
|
|
245
|
+
|
|
246
|
+
await self.emit_event(event, abort_if=self.abort_if)
|
|
231
247
|
return event
|
|
232
248
|
|
|
249
|
+
def abort_if(self, event):
|
|
250
|
+
if self.allowed_cdn_ports is not None:
|
|
251
|
+
# if the host is a CDN
|
|
252
|
+
for cdn_tag in self.cdn_tags:
|
|
253
|
+
if any(t.startswith(str(cdn_tag)) for t in event.tags):
|
|
254
|
+
# and if its port isn't in the list of allowed CDN ports
|
|
255
|
+
if event.port not in self.allowed_cdn_ports:
|
|
256
|
+
return True, "event is a CDN and port is not in the allowed list"
|
|
257
|
+
return False
|
|
258
|
+
|
|
233
259
|
def parse_json_line(self, line):
|
|
234
260
|
try:
|
|
235
261
|
j = json.loads(line)
|
|
@@ -308,7 +334,7 @@ class portscan(BaseModule):
|
|
|
308
334
|
if "FAIL" in s:
|
|
309
335
|
self.warning(s)
|
|
310
336
|
self.warning(
|
|
311
|
-
|
|
337
|
+
'Masscan failed to detect interface. Recommend passing "adapter_ip", "adapter_mac", and "router_mac" config options to portscan module.'
|
|
312
338
|
)
|
|
313
339
|
else:
|
|
314
340
|
self.verbose(s)
|
bbot/modules/postman.py
CHANGED
|
@@ -1,36 +1,62 @@
|
|
|
1
|
-
from bbot.modules.templates.
|
|
1
|
+
from bbot.modules.templates.postman import postman
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
class postman(
|
|
5
|
-
watched_events = ["
|
|
6
|
-
produced_events = ["
|
|
4
|
+
class postman(postman):
|
|
5
|
+
watched_events = ["ORG_STUB", "SOCIAL"]
|
|
6
|
+
produced_events = ["CODE_REPOSITORY"]
|
|
7
7
|
flags = ["passive", "subdomain-enum", "safe", "code-enum"]
|
|
8
8
|
meta = {
|
|
9
|
-
"description": "Query Postman's API for related workspaces, collections, requests",
|
|
10
|
-
"created_date": "
|
|
9
|
+
"description": "Query Postman's API for related workspaces, collections, requests and download them",
|
|
10
|
+
"created_date": "2024-09-07",
|
|
11
11
|
"author": "@domwhewell-sage",
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
base_url = "https://www.postman.com/_api"
|
|
15
|
-
|
|
16
|
-
headers = {
|
|
17
|
-
"Content-Type": "application/json",
|
|
18
|
-
"X-App-Version": "10.18.8-230926-0808",
|
|
19
|
-
"X-Entity-Team-Id": "0",
|
|
20
|
-
"Origin": "https://www.postman.com",
|
|
21
|
-
"Referer": "https://www.postman.com/search?q=&scope=public&type=all",
|
|
22
|
-
}
|
|
23
|
-
|
|
24
14
|
reject_wildcards = False
|
|
25
15
|
|
|
26
16
|
async def handle_event(self, event):
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
17
|
+
# Handle postman profile
|
|
18
|
+
if event.type == "SOCIAL":
|
|
19
|
+
await self.handle_profile(event)
|
|
20
|
+
elif event.type == "ORG_STUB":
|
|
21
|
+
await self.handle_org_stub(event)
|
|
22
|
+
|
|
23
|
+
async def handle_profile(self, event):
|
|
24
|
+
profile_name = event.data.get("profile_name", "")
|
|
25
|
+
self.verbose(f"Searching for postman workspaces, collections, requests belonging to {profile_name}")
|
|
26
|
+
for item in await self.query(profile_name):
|
|
27
|
+
workspace = item["document"]
|
|
28
|
+
name = workspace["slug"]
|
|
29
|
+
profile = workspace["publisherHandle"]
|
|
30
|
+
if profile_name.lower() == profile.lower():
|
|
31
|
+
self.verbose(f"Got {name}")
|
|
32
|
+
workspace_url = f"{self.html_url}/{profile}/{name}"
|
|
33
|
+
await self.emit_event(
|
|
34
|
+
{"url": workspace_url},
|
|
35
|
+
"CODE_REPOSITORY",
|
|
36
|
+
tags="postman",
|
|
37
|
+
parent=event,
|
|
38
|
+
context=f'{{module}} searched postman.com for workspaces belonging to "{profile_name}" and found "{name}" at {{event.type}}: {workspace_url}',
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
async def handle_org_stub(self, event):
|
|
42
|
+
org_name = event.data
|
|
43
|
+
self.verbose(f"Searching for any postman workspaces, collections, requests for {org_name}")
|
|
44
|
+
for item in await self.query(org_name):
|
|
45
|
+
workspace = item["document"]
|
|
46
|
+
name = workspace["slug"]
|
|
47
|
+
profile = workspace["publisherHandle"]
|
|
48
|
+
self.verbose(f"Got {name}")
|
|
49
|
+
workspace_url = f"{self.html_url}/{profile}/{name}"
|
|
50
|
+
await self.emit_event(
|
|
51
|
+
{"url": workspace_url},
|
|
52
|
+
"CODE_REPOSITORY",
|
|
53
|
+
tags="postman",
|
|
54
|
+
parent=event,
|
|
55
|
+
context=f'{{module}} searched postman.com for "{org_name}" and found matching workspace "{name}" at {{event.type}}: {workspace_url}',
|
|
56
|
+
)
|
|
31
57
|
|
|
32
58
|
async def query(self, query):
|
|
33
|
-
|
|
59
|
+
data = []
|
|
34
60
|
url = f"{self.base_url}/ws/proxy"
|
|
35
61
|
json = {
|
|
36
62
|
"service": "search",
|
|
@@ -39,11 +65,6 @@ class postman(subdomain_enum):
|
|
|
39
65
|
"body": {
|
|
40
66
|
"queryIndices": [
|
|
41
67
|
"collaboration.workspace",
|
|
42
|
-
"runtime.collection",
|
|
43
|
-
"runtime.request",
|
|
44
|
-
"adp.api",
|
|
45
|
-
"flow.flow",
|
|
46
|
-
"apinetwork.team",
|
|
47
68
|
],
|
|
48
69
|
"queryText": self.helpers.quote(query),
|
|
49
70
|
"size": 100,
|
|
@@ -57,108 +78,11 @@ class postman(subdomain_enum):
|
|
|
57
78
|
}
|
|
58
79
|
r = await self.helpers.request(url, method="POST", json=json, headers=self.headers)
|
|
59
80
|
if r is None:
|
|
60
|
-
return
|
|
81
|
+
return data
|
|
61
82
|
status_code = getattr(r, "status_code", 0)
|
|
62
83
|
try:
|
|
63
84
|
json = r.json()
|
|
64
85
|
except Exception as e:
|
|
65
86
|
self.warning(f"Failed to decode JSON for {r.url} (HTTP status: {status_code}): {e}")
|
|
66
|
-
return
|
|
67
|
-
|
|
68
|
-
for item in json.get("data", {}):
|
|
69
|
-
for workspace in item.get("document", {}).get("workspaces", []):
|
|
70
|
-
if workspace not in workspaces:
|
|
71
|
-
workspaces.append(workspace)
|
|
72
|
-
for item in workspaces:
|
|
73
|
-
id = item.get("id", "")
|
|
74
|
-
name = item.get("name", "")
|
|
75
|
-
tldextract = self.helpers.tldextract(query)
|
|
76
|
-
if tldextract.domain.lower() in name.lower():
|
|
77
|
-
self.verbose(f"Discovered workspace {name} ({id})")
|
|
78
|
-
workspace_url = f"{self.base_url}/workspace/{id}"
|
|
79
|
-
interesting_urls.append(
|
|
80
|
-
(
|
|
81
|
-
workspace_url,
|
|
82
|
-
f'{{module}} searched postman.com for "{query}" and found matching workspace "{name}" at {{event.type}}: {workspace_url}',
|
|
83
|
-
)
|
|
84
|
-
)
|
|
85
|
-
environments, collections = await self.search_workspace(id)
|
|
86
|
-
globals_url = f"{self.base_url}/workspace/{id}/globals"
|
|
87
|
-
interesting_urls.append(
|
|
88
|
-
(
|
|
89
|
-
globals_url,
|
|
90
|
-
f'{{module}} searched postman.com for "{query}", found matching workspace "{name}" at {workspace_url}, and found globals at {{event.type}}: {globals_url}',
|
|
91
|
-
)
|
|
92
|
-
)
|
|
93
|
-
for e_id in environments:
|
|
94
|
-
env_url = f"{self.base_url}/environment/{e_id}"
|
|
95
|
-
interesting_urls.append(
|
|
96
|
-
(
|
|
97
|
-
env_url,
|
|
98
|
-
f'{{module}} searched postman.com for "{query}", found matching workspace "{name}" at {workspace_url}, enumerated environments, and found {{event.type}}: {env_url}',
|
|
99
|
-
)
|
|
100
|
-
)
|
|
101
|
-
for c_id in collections:
|
|
102
|
-
collection_url = f"{self.base_url}/collection/{c_id}"
|
|
103
|
-
interesting_urls.append(
|
|
104
|
-
(
|
|
105
|
-
collection_url,
|
|
106
|
-
f'{{module}} searched postman.com for "{query}", found matching workspace "{name}" at {workspace_url}, enumerated collections, and found {{event.type}}: {collection_url}',
|
|
107
|
-
)
|
|
108
|
-
)
|
|
109
|
-
requests = await self.search_collections(id)
|
|
110
|
-
for r_id in requests:
|
|
111
|
-
request_url = f"{self.base_url}/request/{r_id}"
|
|
112
|
-
interesting_urls.append(
|
|
113
|
-
(
|
|
114
|
-
request_url,
|
|
115
|
-
f'{{module}} searched postman.com for "{query}", found matching workspace "{name}" at {workspace_url}, enumerated requests, and found {{event.type}}: {request_url}',
|
|
116
|
-
)
|
|
117
|
-
)
|
|
118
|
-
else:
|
|
119
|
-
self.verbose(f"Skipping workspace {name} ({id}) as it does not appear to be in scope")
|
|
120
|
-
return interesting_urls
|
|
121
|
-
|
|
122
|
-
async def search_workspace(self, id):
|
|
123
|
-
url = f"{self.base_url}/workspace/{id}"
|
|
124
|
-
r = await self.helpers.request(url)
|
|
125
|
-
if r is None:
|
|
126
|
-
return [], []
|
|
127
|
-
status_code = getattr(r, "status_code", 0)
|
|
128
|
-
try:
|
|
129
|
-
json = r.json()
|
|
130
|
-
if not isinstance(json, dict):
|
|
131
|
-
raise ValueError(f"Got unexpected value for JSON: {json}")
|
|
132
|
-
except Exception as e:
|
|
133
|
-
self.warning(f"Failed to decode JSON for {r.url} (HTTP status: {status_code}): {e}")
|
|
134
|
-
return [], []
|
|
135
|
-
environments = json.get("data", {}).get("dependencies", {}).get("environments", [])
|
|
136
|
-
collections = json.get("data", {}).get("dependencies", {}).get("collections", [])
|
|
137
|
-
return environments, collections
|
|
138
|
-
|
|
139
|
-
async def search_collections(self, id):
|
|
140
|
-
request_ids = []
|
|
141
|
-
url = f"{self.base_url}/list/collection?workspace={id}"
|
|
142
|
-
r = await self.helpers.request(url, method="POST")
|
|
143
|
-
if r is None:
|
|
144
|
-
return request_ids
|
|
145
|
-
status_code = getattr(r, "status_code", 0)
|
|
146
|
-
try:
|
|
147
|
-
json = r.json()
|
|
148
|
-
except Exception as e:
|
|
149
|
-
self.warning(f"Failed to decode JSON for {r.url} (HTTP status: {status_code}): {e}")
|
|
150
|
-
return request_ids
|
|
151
|
-
for item in json.get("data", {}):
|
|
152
|
-
request_ids.extend(await self.parse_collection(item))
|
|
153
|
-
return request_ids
|
|
154
|
-
|
|
155
|
-
async def parse_collection(self, json):
|
|
156
|
-
request_ids = []
|
|
157
|
-
folders = json.get("folders", [])
|
|
158
|
-
requests = json.get("requests", [])
|
|
159
|
-
for folder in folders:
|
|
160
|
-
request_ids.extend(await self.parse_collection(folder))
|
|
161
|
-
for request in requests:
|
|
162
|
-
r_id = request.get("id", "")
|
|
163
|
-
request_ids.append(r_id)
|
|
164
|
-
return request_ids
|
|
87
|
+
return None
|
|
88
|
+
return json.get("data", [])
|