bbot 2.4.2.6109rc0__py3-none-any.whl → 2.4.2.6596rc0__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 (67) hide show
  1. bbot/__init__.py +1 -1
  2. bbot/core/event/base.py +64 -4
  3. bbot/core/helpers/diff.py +10 -7
  4. bbot/core/helpers/helper.py +5 -1
  5. bbot/core/helpers/misc.py +48 -11
  6. bbot/core/helpers/regex.py +4 -0
  7. bbot/core/helpers/regexes.py +45 -8
  8. bbot/core/helpers/url.py +21 -5
  9. bbot/core/helpers/web/client.py +25 -5
  10. bbot/core/helpers/web/engine.py +9 -1
  11. bbot/core/helpers/web/envelopes.py +352 -0
  12. bbot/core/helpers/web/web.py +10 -2
  13. bbot/core/helpers/yara_helper.py +50 -0
  14. bbot/core/modules.py +23 -7
  15. bbot/defaults.yml +26 -1
  16. bbot/modules/base.py +4 -2
  17. bbot/modules/{deadly/dastardly.py → dastardly.py} +1 -1
  18. bbot/modules/{deadly/ffuf.py → ffuf.py} +1 -1
  19. bbot/modules/ffuf_shortnames.py +1 -1
  20. bbot/modules/httpx.py +14 -0
  21. bbot/modules/hunt.py +24 -6
  22. bbot/modules/internal/aggregate.py +1 -0
  23. bbot/modules/internal/excavate.py +356 -197
  24. bbot/modules/lightfuzz/lightfuzz.py +203 -0
  25. bbot/modules/lightfuzz/submodules/__init__.py +0 -0
  26. bbot/modules/lightfuzz/submodules/base.py +312 -0
  27. bbot/modules/lightfuzz/submodules/cmdi.py +106 -0
  28. bbot/modules/lightfuzz/submodules/crypto.py +474 -0
  29. bbot/modules/lightfuzz/submodules/nosqli.py +183 -0
  30. bbot/modules/lightfuzz/submodules/path.py +154 -0
  31. bbot/modules/lightfuzz/submodules/serial.py +179 -0
  32. bbot/modules/lightfuzz/submodules/sqli.py +187 -0
  33. bbot/modules/lightfuzz/submodules/ssti.py +39 -0
  34. bbot/modules/lightfuzz/submodules/xss.py +191 -0
  35. bbot/modules/{deadly/nuclei.py → nuclei.py} +1 -1
  36. bbot/modules/paramminer_headers.py +2 -0
  37. bbot/modules/reflected_parameters.py +80 -0
  38. bbot/modules/{deadly/vhost.py → vhost.py} +2 -2
  39. bbot/presets/web/lightfuzz-heavy.yml +16 -0
  40. bbot/presets/web/lightfuzz-light.yml +20 -0
  41. bbot/presets/web/lightfuzz-medium.yml +14 -0
  42. bbot/presets/web/lightfuzz-superheavy.yml +13 -0
  43. bbot/presets/web/lightfuzz-xss.yml +22 -0
  44. bbot/presets/web/paramminer.yml +8 -5
  45. bbot/scanner/preset/args.py +26 -0
  46. bbot/scanner/preset/path.py +12 -10
  47. bbot/scanner/preset/preset.py +42 -37
  48. bbot/scanner/scanner.py +6 -0
  49. bbot/scripts/docs.py +5 -5
  50. bbot/test/test_step_1/test__module__tests.py +1 -1
  51. bbot/test/test_step_1/test_helpers.py +7 -0
  52. bbot/test/test_step_1/test_presets.py +2 -2
  53. bbot/test/test_step_1/test_web.py +20 -0
  54. bbot/test/test_step_1/test_web_envelopes.py +343 -0
  55. bbot/test/test_step_2/module_tests/test_module_excavate.py +404 -29
  56. bbot/test/test_step_2/module_tests/test_module_httpx.py +29 -0
  57. bbot/test/test_step_2/module_tests/test_module_hunt.py +18 -1
  58. bbot/test/test_step_2/module_tests/test_module_lightfuzz.py +1947 -0
  59. bbot/test/test_step_2/module_tests/test_module_paramminer_getparams.py +4 -1
  60. bbot/test/test_step_2/module_tests/test_module_paramminer_headers.py +46 -2
  61. bbot/test/test_step_2/module_tests/test_module_reflected_parameters.py +226 -0
  62. bbot/wordlists/paramminer_parameters.txt +0 -8
  63. {bbot-2.4.2.6109rc0.dist-info → bbot-2.4.2.6596rc0.dist-info}/METADATA +2 -1
  64. {bbot-2.4.2.6109rc0.dist-info → bbot-2.4.2.6596rc0.dist-info}/RECORD +67 -45
  65. {bbot-2.4.2.6109rc0.dist-info → bbot-2.4.2.6596rc0.dist-info}/LICENSE +0 -0
  66. {bbot-2.4.2.6109rc0.dist-info → bbot-2.4.2.6596rc0.dist-info}/WHEEL +0 -0
  67. {bbot-2.4.2.6109rc0.dist-info → bbot-2.4.2.6596rc0.dist-info}/entry_points.txt +0 -0
@@ -308,7 +308,7 @@ class Preset(metaclass=BasePreset):
308
308
 
309
309
  @property
310
310
  def preset_dir(self):
311
- return self.bbot_home / "presets"
311
+ return (self.bbot_home / "presets").expanduser().resolve()
312
312
 
313
313
  @property
314
314
  def default_output_modules(self):
@@ -413,30 +413,32 @@ class Preset(metaclass=BasePreset):
413
413
  self.log_debug("Getting baked")
414
414
  # create a copy of self
415
415
  baked_preset = copy(self)
416
- baked_preset.scan = scan
416
+
417
417
  # copy core
418
418
  baked_preset.core = self.core.copy()
419
- # copy module loader
420
- baked_preset._module_loader = self.module_loader.copy()
421
- # prepare os environment
422
- os_environ = baked_preset.environ.prepare()
423
- # find and replace preloaded modules with os environ
424
- # this is different from the config variable substitution because it modifies
425
- # the preloaded modules, i.e. their ansible playbooks
426
- baked_preset.module_loader.find_and_replace(**os_environ)
427
- # update os environ
428
- os.environ.clear()
429
- os.environ.update(os_environ)
430
419
 
431
- # validate flags, config options
432
- baked_preset.validate()
420
+ if scan is not None:
421
+ baked_preset.scan = scan
422
+ # copy module loader
423
+ baked_preset._module_loader = self.module_loader.copy()
424
+ # prepare os environment
425
+ os_environ = baked_preset.environ.prepare()
426
+ # find and replace preloaded modules with os environ
427
+ # this is different from the config variable substitution because it modifies
428
+ # the preloaded modules, i.e. their ansible playbooks
429
+ baked_preset.module_loader.find_and_replace(**os_environ)
430
+ # update os environ
431
+ os.environ.clear()
432
+ os.environ.update(os_environ)
433
+
434
+ # assign baked preset to our scan
435
+ scan.preset = baked_preset
433
436
 
434
437
  # validate log level options
435
438
  baked_preset.apply_log_level(apply_core=scan is not None)
436
439
 
437
- # assign baked preset to our scan
438
- if scan is not None:
439
- scan.preset = baked_preset
440
+ # validate flags, config options
441
+ baked_preset.validate()
440
442
 
441
443
  # now that our requirements / exclusions are validated, we can start enabling modules
442
444
  # enable scan modules
@@ -483,15 +485,19 @@ class Preset(metaclass=BasePreset):
483
485
  from bbot.scanner.target import BBOTTarget
484
486
 
485
487
  baked_preset._target = BBOTTarget(
486
- *list(self._seeds), whitelist=self._whitelist, blacklist=self._blacklist, strict_scope=self.strict_scope
488
+ *list(self._seeds),
489
+ whitelist=self._whitelist,
490
+ blacklist=self._blacklist,
491
+ strict_scope=self.strict_scope,
487
492
  )
488
493
 
489
- # evaluate conditions
490
- if baked_preset.conditions:
491
- from .conditions import ConditionEvaluator
494
+ if scan is not None:
495
+ # evaluate conditions
496
+ if baked_preset.conditions:
497
+ from .conditions import ConditionEvaluator
492
498
 
493
- evaluator = ConditionEvaluator(baked_preset)
494
- evaluator.evaluate()
499
+ evaluator = ConditionEvaluator(baked_preset)
500
+ evaluator.evaluate()
495
501
 
496
502
  self._baked = True
497
503
  return baked_preset
@@ -562,6 +568,12 @@ class Preset(metaclass=BasePreset):
562
568
  return self.scope_config.get("strict", False)
563
569
 
564
570
  def apply_log_level(self, apply_core=False):
571
+ """
572
+ Apply the log level to the preset.
573
+
574
+ Args:
575
+ apply_core (bool, optional): If True, apply the log level to the core logger.
576
+ """
565
577
  # silent takes precedence
566
578
  if self.silent:
567
579
  self.verbose = False
@@ -920,20 +932,17 @@ class Preset(metaclass=BasePreset):
920
932
  """
921
933
  Recursively find all the presets and return them as a dictionary
922
934
  """
923
- preset_dir = self.preset_dir
924
- home_dir = Path.home()
925
-
926
935
  # first, add local preset dir to PRESET_PATH
927
936
  PRESET_PATH.add_path(self.preset_dir)
928
937
 
929
938
  # ensure local preset directory exists
930
- mkdir(preset_dir)
939
+ mkdir(self.preset_dir)
931
940
 
932
941
  global DEFAULT_PRESETS
933
942
  if DEFAULT_PRESETS is None:
934
943
  presets = {}
935
- for ext in ("yml", "yaml"):
936
- for preset_path in PRESET_PATH:
944
+ for preset_path in PRESET_PATH:
945
+ for ext in ("yml", "yaml"):
937
946
  # for every yaml file
938
947
  for original_filename in preset_path.rglob(f"**/*.{ext}"):
939
948
  # not including symlinks
@@ -957,18 +966,14 @@ class Preset(metaclass=BasePreset):
957
966
 
958
967
  local_preset = original_filename
959
968
  # populate symlinks in local preset dir
960
- if not original_filename.is_relative_to(preset_dir):
969
+ if not original_filename.is_relative_to(self.preset_dir):
961
970
  relative_preset = original_filename.relative_to(preset_path)
962
- local_preset = preset_dir / relative_preset
971
+ local_preset = self.preset_dir / relative_preset
963
972
  mkdir(local_preset.parent, check_writable=False)
964
973
  if not local_preset.exists():
965
974
  local_preset.symlink_to(original_filename)
966
975
 
967
- # collapse home directory into "~"
968
- if local_preset.is_relative_to(home_dir):
969
- local_preset = Path("~") / local_preset.relative_to(home_dir)
970
-
971
- presets[local_preset] = (loaded_preset, category, preset_path, original_filename)
976
+ presets[local_preset.stem] = (loaded_preset, category, preset_path, original_filename)
972
977
 
973
978
  # sort by name
974
979
  DEFAULT_PRESETS = dict(sorted(presets.items(), key=lambda x: x[-1][0].name))
bbot/scanner/scanner.py CHANGED
@@ -214,6 +214,12 @@ class Scanner:
214
214
  self.warning(
215
215
  "You have enabled custom HTTP headers. These will be attached to all in-scope requests and all requests made by httpx."
216
216
  )
217
+ # custom HTTP cookies warning
218
+ self.custom_http_cookies = self.web_config.get("http_cookies", {})
219
+ if self.custom_http_cookies:
220
+ self.warning(
221
+ "You have enabled custom HTTP cookies. These will be attached to all in-scope requests and all requests made by httpx."
222
+ )
217
223
 
218
224
  # url file extensions
219
225
  self.url_extension_blacklist = {e.lower() for e in self.config.get("url_extension_blacklist", [])}
bbot/scripts/docs.py CHANGED
@@ -198,15 +198,15 @@ def update_docs():
198
198
  update_md_files("BBOT PRESETS", bbot_presets_table)
199
199
 
200
200
  # BBOT presets
201
- for yaml_file, (loaded_preset, category, preset_path, original_filename) in DEFAULT_PRESET.all_presets.items():
201
+ for _, (loaded_preset, category, preset_path, original_filename) in DEFAULT_PRESET.all_presets.items():
202
202
  preset_yaml = f"""
203
- ```yaml title={yaml_file.name}
203
+ ```yaml title={preset_path.name}
204
204
  {loaded_preset._yaml_str}
205
205
  ```
206
206
  """
207
207
  preset_yaml_expandable = f"""
208
208
  <details>
209
- <summary><b><code>{yaml_file.name}</code></b></summary>
209
+ <summary><b><code>{preset_path.name}</code></b></summary>
210
210
 
211
211
  ```yaml
212
212
  {loaded_preset._yaml_str}
@@ -218,11 +218,11 @@ def update_docs():
218
218
  update_md_files(f"BBOT {loaded_preset.name.upper()} PRESET EXPANDABLE", preset_yaml_expandable)
219
219
 
220
220
  content = []
221
- for yaml_file, (loaded_preset, category, preset_path, original_filename) in DEFAULT_PRESET.all_presets.items():
221
+ for _, (loaded_preset, category, preset_path, original_filename) in DEFAULT_PRESET.all_presets.items():
222
222
  yaml_str = loaded_preset._yaml_str
223
223
  indent = " " * 4
224
224
  yaml_str = f"\n{indent}".join(yaml_str.splitlines())
225
- filename = homedir_collapseuser(yaml_file)
225
+ filename = homedir_collapseuser(preset_path)
226
226
 
227
227
  num_modules = len(loaded_preset.scan_modules)
228
228
  modules = ", ".join(sorted([f"`{m}`" for m in loaded_preset.scan_modules]))
@@ -18,7 +18,7 @@ def test__module__tests():
18
18
  preset = Preset()
19
19
 
20
20
  # make sure each module has a .py file
21
- for module_name in preset.module_loader.preloaded():
21
+ for module_name, preloaded in preset.module_loader.preloaded().items():
22
22
  module_name = module_name.lower()
23
23
  assert module_name in module_test_files, f'No test file found for module "{module_name}"'
24
24
 
@@ -460,6 +460,13 @@ async def test_helpers_misc(helpers, scan, bbot_scanner, bbot_httpserver):
460
460
  s = "asdf {unused} {used}"
461
461
  assert helpers.safe_format(s, used="fdsa") == "asdf {unused} fdsa"
462
462
 
463
+ # is_printable
464
+ assert helpers.is_printable("asdf") is True
465
+ assert helpers.is_printable(r"""~!@#$^&*()_+=-<>:"?,./;'[]\{}|""") is True
466
+ assert helpers.is_printable("ドメイン.テスト") is True
467
+ assert helpers.is_printable("4") is True
468
+ assert helpers.is_printable("asdf\x00") is False
469
+
463
470
  # punycode
464
471
  assert helpers.smart_encode_punycode("ドメイン.テスト") == "xn--eckwd4c7c.xn--zckzah"
465
472
  assert helpers.smart_decode_punycode("xn--eckwd4c7c.xn--zckzah") == "ドメイン.テスト"
@@ -596,7 +596,7 @@ class TestModule1(BaseModule):
596
596
  from bbot.modules.output.base import BaseOutputModule
597
597
 
598
598
  class TestModule2(BaseOutputModule):
599
- pass
599
+ watched_events = []
600
600
  """
601
601
  )
602
602
 
@@ -607,7 +607,7 @@ class TestModule2(BaseOutputModule):
607
607
  from bbot.modules.internal.base import BaseInternalModule
608
608
 
609
609
  class TestModule3(BaseInternalModule):
610
- pass
610
+ watched_events = []
611
611
  """
612
612
  )
613
613
 
@@ -478,3 +478,23 @@ async def test_web_cookies(bbot_scanner, httpx_mock):
478
478
  assert not client2.cookies
479
479
 
480
480
  await scan._cleanup()
481
+
482
+
483
+ @pytest.mark.asyncio
484
+ async def test_http_sendcookies(bbot_scanner, bbot_httpserver):
485
+ endpoint = "/"
486
+ url = bbot_httpserver.url_for(endpoint)
487
+ from werkzeug.wrappers import Response
488
+
489
+ def echo_cookies_handler(request):
490
+ cookies = request.cookies
491
+ cookie_str = "; ".join([f"{key}={value}" for key, value in cookies.items()])
492
+ return Response(f"Echoed Cookies: {cookie_str}\nEchoed Headers: {request.headers}")
493
+
494
+ bbot_httpserver.expect_request(uri=endpoint).respond_with_handler(echo_cookies_handler)
495
+ scan1 = bbot_scanner("127.0.0.1", config={"web": {"debug": True}})
496
+ r1 = await scan1.helpers.request(url, cookies={"foo": "bar"})
497
+
498
+ assert r1 is not None, "Request to self-signed SSL server went through even with ssl_verify=True"
499
+ assert "bar" in r1.text
500
+ await scan1._cleanup()
@@ -0,0 +1,343 @@
1
+ import pytest
2
+
3
+
4
+ async def test_web_envelopes():
5
+ from bbot.core.helpers.web.envelopes import (
6
+ BaseEnvelope,
7
+ TextEnvelope,
8
+ HexEnvelope,
9
+ B64Envelope,
10
+ JSONEnvelope,
11
+ XMLEnvelope,
12
+ URLEnvelope,
13
+ )
14
+
15
+ # simple text
16
+ text_envelope = BaseEnvelope.detect("foo")
17
+ assert isinstance(text_envelope, TextEnvelope)
18
+ assert text_envelope.unpacked_data() == "foo"
19
+ assert text_envelope.subparams == {"__default__": "foo"}
20
+ expected_subparams = [([], "foo")]
21
+ assert list(text_envelope.get_subparams()) == expected_subparams
22
+ for subparam, value in expected_subparams:
23
+ assert text_envelope.get_subparam(subparam) == value
24
+ assert text_envelope.pack() == "foo"
25
+ assert text_envelope.num_envelopes == 0
26
+ assert text_envelope.get_subparam() == "foo"
27
+ text_envelope.set_subparam(value="bar")
28
+ assert text_envelope.get_subparam() == "bar"
29
+ assert text_envelope.unpacked_data() == "bar"
30
+
31
+ # simple binary
32
+ # binary_envelope = BaseEnvelope.detect("foo\x00")
33
+ # assert isinstance(binary_envelope, BinaryEnvelope)
34
+ # assert binary_envelope.unpacked_data == "foo\x00"
35
+ # assert binary_envelope.packed_data == "foo\x00"
36
+ # assert binary_envelope.subparams == {"__default__": "foo\x00"}
37
+
38
+ # text encoded as hex
39
+ hex_envelope = BaseEnvelope.detect("706172616d")
40
+ assert isinstance(hex_envelope, HexEnvelope)
41
+ assert hex_envelope.unpacked_data(recursive=True) == "param"
42
+ hex_inner_envelope = hex_envelope.unpacked_data(recursive=False)
43
+ assert isinstance(hex_inner_envelope, TextEnvelope)
44
+ assert hex_inner_envelope.unpacked_data(recursive=False) == "param"
45
+ assert hex_inner_envelope.unpacked_data(recursive=True) == "param"
46
+ assert list(hex_envelope.get_subparams(recursive=False)) == [([], hex_inner_envelope)]
47
+ assert list(hex_envelope.get_subparams(recursive=True)) == [([], "param")]
48
+ assert hex_inner_envelope.unpacked_data() == "param"
49
+ assert hex_inner_envelope.subparams == {"__default__": "param"}
50
+ expected_subparams = [([], "param")]
51
+ assert list(hex_inner_envelope.get_subparams()) == expected_subparams
52
+ for subparam, value in expected_subparams:
53
+ assert hex_inner_envelope.get_subparam(subparam) == value
54
+ assert hex_envelope.pack() == "706172616d"
55
+ assert hex_envelope.num_envelopes == 1
56
+ assert hex_envelope.get_subparam() == "param"
57
+ hex_envelope.set_subparam(value="asdf")
58
+ assert hex_envelope.get_subparam() == "asdf"
59
+ assert hex_envelope.unpacked_data() == "asdf"
60
+ assert hex_envelope.pack() == "61736466"
61
+
62
+ # text encoded as base64
63
+ base64_envelope = BaseEnvelope.detect("cGFyYW0=")
64
+ assert isinstance(base64_envelope, B64Envelope)
65
+ assert base64_envelope.unpacked_data() == "param"
66
+ base64_inner_envelope = base64_envelope.unpacked_data(recursive=False)
67
+ assert isinstance(base64_inner_envelope, TextEnvelope)
68
+ assert list(base64_envelope.get_subparams(recursive=False)) == [([], base64_inner_envelope)]
69
+ assert list(base64_envelope.get_subparams()) == [([], "param")]
70
+ assert base64_inner_envelope.pack() == "param"
71
+ assert base64_inner_envelope.unpacked_data() == "param"
72
+ assert base64_inner_envelope.subparams == {"__default__": "param"}
73
+ expected_subparams = [([], "param")]
74
+ assert list(base64_inner_envelope.get_subparams()) == expected_subparams
75
+ for subparam, value in expected_subparams:
76
+ assert base64_inner_envelope.get_subparam(subparam) == value
77
+ assert base64_envelope.num_envelopes == 1
78
+ base64_envelope.set_subparam(value="asdf")
79
+ assert base64_envelope.get_subparam() == "asdf"
80
+ assert base64_envelope.unpacked_data() == "asdf"
81
+ assert base64_envelope.pack() == "YXNkZg=="
82
+
83
+ # test inside hex inside base64
84
+ hex_envelope = BaseEnvelope.detect("634746795957303d")
85
+ assert isinstance(hex_envelope, HexEnvelope)
86
+ assert hex_envelope.get_subparam() == "param"
87
+ assert hex_envelope.unpacked_data() == "param"
88
+ base64_envelope = hex_envelope.unpacked_data(recursive=False)
89
+ assert isinstance(base64_envelope, B64Envelope)
90
+ assert base64_envelope.get_subparam() == "param"
91
+ assert base64_envelope.unpacked_data() == "param"
92
+ text_envelope = base64_envelope.unpacked_data(recursive=False)
93
+ assert isinstance(text_envelope, TextEnvelope)
94
+ assert text_envelope.get_subparam() == "param"
95
+ assert text_envelope.unpacked_data() == "param"
96
+ hex_envelope.set_subparam(value="asdf")
97
+ assert hex_envelope.get_subparam() == "asdf"
98
+ assert hex_envelope.unpacked_data() == "asdf"
99
+ assert text_envelope.get_subparam() == "asdf"
100
+ assert text_envelope.unpacked_data() == "asdf"
101
+ assert base64_envelope.get_subparam() == "asdf"
102
+ assert base64_envelope.unpacked_data() == "asdf"
103
+
104
+ # URL-encoded text
105
+ url_encoded_envelope = BaseEnvelope.detect("a%20b%20c")
106
+ assert isinstance(url_encoded_envelope, URLEnvelope)
107
+ assert url_encoded_envelope.pack() == "a%20b%20c"
108
+ assert url_encoded_envelope.unpacked_data() == "a b c"
109
+ url_inner_envelope = url_encoded_envelope.unpacked_data(recursive=False)
110
+ assert isinstance(url_inner_envelope, TextEnvelope)
111
+ assert url_inner_envelope.unpacked_data(recursive=False) == "a b c"
112
+ assert url_inner_envelope.unpacked_data(recursive=True) == "a b c"
113
+ assert list(url_encoded_envelope.get_subparams(recursive=False)) == [([], url_inner_envelope)]
114
+ assert list(url_encoded_envelope.get_subparams(recursive=True)) == [([], "a b c")]
115
+ assert url_inner_envelope.pack() == "a b c"
116
+ assert url_inner_envelope.unpacked_data() == "a b c"
117
+ assert url_inner_envelope.subparams == {"__default__": "a b c"}
118
+ expected_subparams = [([], "a b c")]
119
+ assert list(url_inner_envelope.get_subparams()) == expected_subparams
120
+ for subparam, value in expected_subparams:
121
+ assert url_inner_envelope.get_subparam(subparam) == value
122
+ assert url_encoded_envelope.num_envelopes == 1
123
+ url_encoded_envelope.set_subparam(value="a s d f")
124
+ assert url_encoded_envelope.get_subparam() == "a s d f"
125
+ assert url_encoded_envelope.unpacked_data() == "a s d f"
126
+ assert url_encoded_envelope.pack() == "a%20s%20d%20f"
127
+
128
+ # json
129
+ json_envelope = BaseEnvelope.detect('{"param1": "val1", "param2": {"param3": "val3"}}')
130
+ assert isinstance(json_envelope, JSONEnvelope)
131
+ assert json_envelope.pack() == '{"param1": "val1", "param2": {"param3": "val3"}}'
132
+ assert json_envelope.unpacked_data() == {"param1": "val1", "param2": {"param3": "val3"}}
133
+ assert json_envelope.unpacked_data(recursive=False) == {"param1": "val1", "param2": {"param3": "val3"}}
134
+ assert json_envelope.unpacked_data(recursive=True) == {"param1": "val1", "param2": {"param3": "val3"}}
135
+ assert json_envelope.subparams == {"param1": "val1", "param2": {"param3": "val3"}}
136
+ expected_subparams = [
137
+ (["param1"], "val1"),
138
+ (["param2", "param3"], "val3"),
139
+ ]
140
+ assert list(json_envelope.get_subparams()) == expected_subparams
141
+ for subparam, value in expected_subparams:
142
+ assert json_envelope.get_subparam(subparam) == value
143
+ json_envelope.selected_subparam = ["param2", "param3"]
144
+ assert json_envelope.get_subparam() == "val3"
145
+ assert json_envelope.num_envelopes == 1
146
+
147
+ # prevent json over-detection
148
+ just_a_string = BaseEnvelope.detect("10")
149
+ assert not isinstance(just_a_string, JSONEnvelope)
150
+
151
+ # xml
152
+ xml_envelope = BaseEnvelope.detect(
153
+ '<root><param1 attr="attr1">val1</param1><param2><param3>val3</param3></param2></root>'
154
+ )
155
+ assert isinstance(xml_envelope, XMLEnvelope)
156
+ assert (
157
+ xml_envelope.pack()
158
+ == '<?xml version="1.0" encoding="utf-8"?>\n<root><param1 attr="attr1">val1</param1><param2><param3>val3</param3></param2></root>'
159
+ )
160
+ assert xml_envelope.unpacked_data() == {
161
+ "root": {"param1": {"@attr": "attr1", "#text": "val1"}, "param2": {"param3": "val3"}}
162
+ }
163
+ assert xml_envelope.unpacked_data(recursive=False) == {
164
+ "root": {"param1": {"@attr": "attr1", "#text": "val1"}, "param2": {"param3": "val3"}}
165
+ }
166
+ assert xml_envelope.unpacked_data(recursive=True) == {
167
+ "root": {"param1": {"@attr": "attr1", "#text": "val1"}, "param2": {"param3": "val3"}}
168
+ }
169
+ assert xml_envelope.subparams == {
170
+ "root": {"param1": {"@attr": "attr1", "#text": "val1"}, "param2": {"param3": "val3"}}
171
+ }
172
+ expected_subparams = [
173
+ (["root", "param1", "@attr"], "attr1"),
174
+ (["root", "param1", "#text"], "val1"),
175
+ (["root", "param2", "param3"], "val3"),
176
+ ]
177
+ assert list(xml_envelope.get_subparams()) == expected_subparams
178
+ for subparam, value in expected_subparams:
179
+ assert xml_envelope.get_subparam(subparam) == value
180
+ assert xml_envelope.num_envelopes == 1
181
+
182
+ # json inside base64
183
+ base64_json_envelope = BaseEnvelope.detect("eyJwYXJhbTEiOiAidmFsMSIsICJwYXJhbTIiOiB7InBhcmFtMyI6ICJ2YWwzIn19")
184
+ assert isinstance(base64_json_envelope, B64Envelope)
185
+ assert base64_json_envelope.pack() == "eyJwYXJhbTEiOiAidmFsMSIsICJwYXJhbTIiOiB7InBhcmFtMyI6ICJ2YWwzIn19"
186
+ assert base64_json_envelope.unpacked_data() == {"param1": "val1", "param2": {"param3": "val3"}}
187
+ base64_inner_envelope = base64_json_envelope.unpacked_data(recursive=False)
188
+ assert isinstance(base64_inner_envelope, JSONEnvelope)
189
+ assert base64_inner_envelope.pack() == '{"param1": "val1", "param2": {"param3": "val3"}}'
190
+ assert base64_inner_envelope.unpacked_data() == {"param1": "val1", "param2": {"param3": "val3"}}
191
+ assert base64_inner_envelope.subparams == {"param1": "val1", "param2": {"param3": "val3"}}
192
+ expected_subparams = [
193
+ (["param1"], "val1"),
194
+ (["param2", "param3"], "val3"),
195
+ ]
196
+ assert list(base64_json_envelope.get_subparams()) == expected_subparams
197
+ for subparam, value in expected_subparams:
198
+ assert base64_json_envelope.get_subparam(subparam) == value
199
+ assert base64_json_envelope.num_envelopes == 2
200
+ with pytest.raises(ValueError):
201
+ assert base64_json_envelope.get_subparam()
202
+ base64_json_envelope.selected_subparam = ["param2", "param3"]
203
+ assert base64_json_envelope.get_subparam() == "val3"
204
+
205
+ # xml inside url inside hex inside base64
206
+ nested_xml_envelope = BaseEnvelope.detect(
207
+ "MjUzMzYzMjUzNzMyMjUzNjY2MjUzNjY2MjUzNzM0MjUzMzY1MjUzMzYzMjUzNzMwMjUzNjMxMjUzNzMyMjUzNjMxMjUzNjY0MjUzMzMxMjUzMjMwMjUzNjMxMjUzNzM0MjUzNzM0MjUzNzMyMjUzMzY0MjUzMjMyMjUzNzM2MjUzNjMxMjUzNjYzMjUzMzMxMjUzMjMyMjUzMzY1MjUzNzM2MjUzNjMxMjUzNjYzMjUzMzMxMjUzMzYzMjUzMjY2MjUzNzMwMjUzNjMxMjUzNzMyMjUzNjMxMjUzNjY0MjUzMzMxMjUzMzY1MjUzMzYzMjUzNzMwMjUzNjMxMjUzNzMyMjUzNjMxMjUzNjY0MjUzMzMyMjUzMzY1MjUzMzYzMjUzNzMwMjUzNjMxMjUzNzMyMjUzNjMxMjUzNjY0MjUzMzMzMjUzMzY1MjUzNzM2MjUzNjMxMjUzNjYzMjUzMzMzMjUzMzYzMjUzMjY2MjUzNzMwMjUzNjMxMjUzNzMyMjUzNjMxMjUzNjY0MjUzMzMzMjUzMzY1MjUzMzYzMjUzMjY2MjUzNzMwMjUzNjMxMjUzNzMyMjUzNjMxMjUzNjY0MjUzMzMyMjUzMzY1MjUzMzYzMjUzMjY2MjUzNzMyMjUzNjY2MjUzNjY2MjUzNzM0MjUzMzY1"
208
+ )
209
+ assert isinstance(nested_xml_envelope, B64Envelope)
210
+ assert nested_xml_envelope.unpacked_data() == {
211
+ "root": {"param1": {"@attr": "val1", "#text": "val1"}, "param2": {"param3": "val3"}}
212
+ }
213
+ assert (
214
+ nested_xml_envelope.pack()
215
+ == "MjUzMzQzMjUzMzQ2Nzg2ZDZjMjUzMjMwNzY2NTcyNzM2OTZmNmUyNTMzNDQyNTMyMzIzMTJlMzAyNTMyMzIyNTMyMzA2NTZlNjM2ZjY0Njk2ZTY3MjUzMzQ0MjUzMjMyNzU3NDY2MmQzODI1MzIzMjI1MzM0NjI1MzM0NTI1MzA0MTI1MzM0MzcyNmY2Zjc0MjUzMzQ1MjUzMzQzNzA2MTcyNjE2ZDMxMjUzMjMwNjE3NDc0NzIyNTMzNDQyNTMyMzI3NjYxNmMzMTI1MzIzMjI1MzM0NTc2NjE2YzMxMjUzMzQzMmY3MDYxNzI2MTZkMzEyNTMzNDUyNTMzNDM3MDYxNzI2MTZkMzIyNTMzNDUyNTMzNDM3MDYxNzI2MTZkMzMyNTMzNDU3NjYxNmMzMzI1MzM0MzJmNzA2MTcyNjE2ZDMzMjUzMzQ1MjUzMzQzMmY3MDYxNzI2MTZkMzIyNTMzNDUyNTMzNDMyZjcyNmY2Zjc0MjUzMzQ1"
216
+ )
217
+ inner_hex_envelope = nested_xml_envelope.unpacked_data(recursive=False)
218
+ assert isinstance(inner_hex_envelope, HexEnvelope)
219
+ assert (
220
+ inner_hex_envelope.pack()
221
+ == "253343253346786d6c25323076657273696f6e253344253232312e30253232253230656e636f64696e672533442532327574662d38253232253346253345253041253343726f6f74253345253343706172616d312532306174747225334425323276616c3125323225334576616c312533432f706172616d31253345253343706172616d32253345253343706172616d3325334576616c332533432f706172616d332533452533432f706172616d322533452533432f726f6f74253345"
222
+ )
223
+ inner_url_envelope = inner_hex_envelope.unpacked_data(recursive=False)
224
+ assert isinstance(inner_url_envelope, URLEnvelope)
225
+ assert (
226
+ inner_url_envelope.pack()
227
+ == r"%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0A%3Croot%3E%3Cparam1%20attr%3D%22val1%22%3Eval1%3C/param1%3E%3Cparam2%3E%3Cparam3%3Eval3%3C/param3%3E%3C/param2%3E%3C/root%3E"
228
+ )
229
+ inner_xml_envelope = inner_url_envelope.unpacked_data(recursive=False)
230
+ assert isinstance(inner_xml_envelope, XMLEnvelope)
231
+ assert (
232
+ inner_xml_envelope.pack()
233
+ == '<?xml version="1.0" encoding="utf-8"?>\n<root><param1 attr="val1">val1</param1><param2><param3>val3</param3></param2></root>'
234
+ )
235
+ assert inner_xml_envelope.unpacked_data() == {
236
+ "root": {"param1": {"@attr": "val1", "#text": "val1"}, "param2": {"param3": "val3"}}
237
+ }
238
+ assert inner_xml_envelope.subparams == {
239
+ "root": {"param1": {"@attr": "val1", "#text": "val1"}, "param2": {"param3": "val3"}}
240
+ }
241
+ expected_subparams = [
242
+ (["root", "param1", "@attr"], "val1"),
243
+ (["root", "param1", "#text"], "val1"),
244
+ (["root", "param2", "param3"], "val3"),
245
+ ]
246
+ assert list(nested_xml_envelope.get_subparams()) == expected_subparams
247
+ for subparam, value in expected_subparams:
248
+ assert nested_xml_envelope.get_subparam(subparam) == value
249
+ assert nested_xml_envelope.num_envelopes == 4
250
+
251
+ # manipulating text inside hex
252
+ hex_envelope = BaseEnvelope.detect("706172616d")
253
+ expected_subparams = [([], "param")]
254
+ assert list(hex_envelope.get_subparams()) == expected_subparams
255
+ for subparam, value in expected_subparams:
256
+ assert hex_envelope.get_subparam(subparam) == value
257
+ hex_envelope.set_subparam([], "asdf")
258
+ expected_subparams = [([], "asdf")]
259
+ assert list(hex_envelope.get_subparams()) == expected_subparams
260
+ for subparam, value in expected_subparams:
261
+ assert hex_envelope.get_subparam(subparam) == value
262
+ assert hex_envelope.unpacked_data() == "asdf"
263
+
264
+ # manipulating json inside base64
265
+ base64_json_envelope = BaseEnvelope.detect("eyJwYXJhbTEiOiAidmFsMSIsICJwYXJhbTIiOiB7InBhcmFtMyI6ICJ2YWwzIn19")
266
+ expected_subparams = [
267
+ (["param1"], "val1"),
268
+ (["param2", "param3"], "val3"),
269
+ ]
270
+ assert list(base64_json_envelope.get_subparams()) == expected_subparams
271
+ for subparam, value in expected_subparams:
272
+ assert base64_json_envelope.get_subparam(subparam) == value
273
+ base64_json_envelope.set_subparam(["param1"], {"asdf": [None], "fdsa": 1.0})
274
+ expected_subparams = [
275
+ (["param1", "asdf"], [None]),
276
+ (["param1", "fdsa"], 1.0),
277
+ (["param2", "param3"], "val3"),
278
+ ]
279
+ assert list(base64_json_envelope.get_subparams()) == expected_subparams
280
+ for subparam, value in expected_subparams:
281
+ assert base64_json_envelope.get_subparam(subparam) == value
282
+ base64_json_envelope.set_subparam(["param2", "param3"], {"1234": [None], "4321": 1.0})
283
+ expected_subparams = [
284
+ (["param1", "asdf"], [None]),
285
+ (["param1", "fdsa"], 1.0),
286
+ (["param2", "param3", "1234"], [None]),
287
+ (["param2", "param3", "4321"], 1.0),
288
+ ]
289
+ assert list(base64_json_envelope.get_subparams()) == expected_subparams
290
+ base64_json_envelope.set_subparam(["param2"], None)
291
+ expected_subparams = [
292
+ (["param1", "asdf"], [None]),
293
+ (["param1", "fdsa"], 1.0),
294
+ (["param2"], None),
295
+ ]
296
+ assert list(base64_json_envelope.get_subparams()) == expected_subparams
297
+
298
+ # xml inside url inside base64
299
+ xml_envelope = BaseEnvelope.detect(
300
+ "JTNDP3htbCUyMHZlcnNpb249JTIyMS4wJTIyJTIwZW5jb2Rpbmc9JTIydXRmLTglMjI/JTNFJTBBJTNDcm9vdCUzRSUzQ3BhcmFtMSUyMGF0dHI9JTIydmFsMSUyMiUzRXZhbDElM0MvcGFyYW0xJTNFJTNDcGFyYW0yJTNFJTNDcGFyYW0zJTNFdmFsMyUzQy9wYXJhbTMlM0UlM0MvcGFyYW0yJTNFJTNDL3Jvb3QlM0U="
301
+ )
302
+ assert (
303
+ xml_envelope.pack()
304
+ == "JTNDJTNGeG1sJTIwdmVyc2lvbiUzRCUyMjEuMCUyMiUyMGVuY29kaW5nJTNEJTIydXRmLTglMjIlM0YlM0UlMEElM0Nyb290JTNFJTNDcGFyYW0xJTIwYXR0ciUzRCUyMnZhbDElMjIlM0V2YWwxJTNDL3BhcmFtMSUzRSUzQ3BhcmFtMiUzRSUzQ3BhcmFtMyUzRXZhbDMlM0MvcGFyYW0zJTNFJTNDL3BhcmFtMiUzRSUzQy9yb290JTNF"
305
+ )
306
+ expected_subparams = [
307
+ (["root", "param1", "@attr"], "val1"),
308
+ (["root", "param1", "#text"], "val1"),
309
+ (["root", "param2", "param3"], "val3"),
310
+ ]
311
+ assert list(xml_envelope.get_subparams()) == expected_subparams
312
+ xml_envelope.set_subparam(["root", "param1", "@attr"], "asdf")
313
+ expected_subparams = [
314
+ (["root", "param1", "@attr"], "asdf"),
315
+ (["root", "param1", "#text"], "val1"),
316
+ (["root", "param2", "param3"], "val3"),
317
+ ]
318
+ assert list(xml_envelope.get_subparams()) == expected_subparams
319
+ assert (
320
+ xml_envelope.pack()
321
+ == "JTNDJTNGeG1sJTIwdmVyc2lvbiUzRCUyMjEuMCUyMiUyMGVuY29kaW5nJTNEJTIydXRmLTglMjIlM0YlM0UlMEElM0Nyb290JTNFJTNDcGFyYW0xJTIwYXR0ciUzRCUyMmFzZGYlMjIlM0V2YWwxJTNDL3BhcmFtMSUzRSUzQ3BhcmFtMiUzRSUzQ3BhcmFtMyUzRXZhbDMlM0MvcGFyYW0zJTNFJTNDL3BhcmFtMiUzRSUzQy9yb290JTNF"
322
+ )
323
+ xml_envelope.set_subparam(["root", "param2", "param3"], {"1234": [None], "4321": 1.0})
324
+ expected_subparams = [
325
+ (["root", "param1", "@attr"], "asdf"),
326
+ (["root", "param1", "#text"], "val1"),
327
+ (["root", "param2", "param3", "1234"], [None]),
328
+ (["root", "param2", "param3", "4321"], 1.0),
329
+ ]
330
+ assert list(xml_envelope.get_subparams()) == expected_subparams
331
+
332
+ # null
333
+ null_envelope = BaseEnvelope.detect("null")
334
+ assert isinstance(null_envelope, JSONEnvelope)
335
+ assert null_envelope.unpacked_data() is None
336
+ assert null_envelope.pack() == "null"
337
+ expected_subparams = [([], None)]
338
+ assert list(null_envelope.get_subparams()) == expected_subparams
339
+ for subparam, value in expected_subparams:
340
+ assert null_envelope.get_subparam(subparam) == value
341
+
342
+ tiny_base64 = BaseEnvelope.detect("YWJi")
343
+ assert isinstance(tiny_base64, TextEnvelope)