bhshell 0.1.0__tar.gz

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.
Files changed (78) hide show
  1. bhshell-0.1.0/LICENSE +21 -0
  2. bhshell-0.1.0/MANIFEST.in +3 -0
  3. bhshell-0.1.0/PKG-INFO +134 -0
  4. bhshell-0.1.0/README.md +99 -0
  5. bhshell-0.1.0/bhshell/__init__.py +3 -0
  6. bhshell-0.1.0/bhshell/__main__.py +3 -0
  7. bhshell-0.1.0/bhshell/cli.py +55 -0
  8. bhshell-0.1.0/bhshell/client/__init__.py +0 -0
  9. bhshell-0.1.0/bhshell/client/auth.py +74 -0
  10. bhshell-0.1.0/bhshell/client/client.py +337 -0
  11. bhshell-0.1.0/bhshell/config.py +162 -0
  12. bhshell-0.1.0/bhshell/errors.py +73 -0
  13. bhshell-0.1.0/bhshell/library/asrep-roastable-users.cyp +3 -0
  14. bhshell-0.1.0/bhshell/library/dcsync-principals.cyp +3 -0
  15. bhshell-0.1.0/bhshell/library/domain-admins-members.cyp +3 -0
  16. bhshell-0.1.0/bhshell/library/domain-trusts.cyp +3 -0
  17. bhshell-0.1.0/bhshell/library/gpos-linked-to-domain-controllers.cyp +3 -0
  18. bhshell-0.1.0/bhshell/library/kerberoastable-users.cyp +3 -0
  19. bhshell-0.1.0/bhshell/library/owned-objects.cyp +3 -0
  20. bhshell-0.1.0/bhshell/library/owned-to-tier-zero.cyp +3 -0
  21. bhshell-0.1.0/bhshell/library/sid-history.cyp +3 -0
  22. bhshell-0.1.0/bhshell/library/tier-zero-users.cyp +3 -0
  23. bhshell-0.1.0/bhshell/library/unconstrained-delegation.cyp +3 -0
  24. bhshell-0.1.0/bhshell/models/__init__.py +0 -0
  25. bhshell-0.1.0/bhshell/models/entities.py +402 -0
  26. bhshell-0.1.0/bhshell/models/results.py +83 -0
  27. bhshell-0.1.0/bhshell/queries/__init__.py +0 -0
  28. bhshell-0.1.0/bhshell/queries/cypher.py +155 -0
  29. bhshell-0.1.0/bhshell/queries/domains.py +136 -0
  30. bhshell-0.1.0/bhshell/queries/edges.py +109 -0
  31. bhshell-0.1.0/bhshell/queries/hunt.py +675 -0
  32. bhshell-0.1.0/bhshell/queries/library.py +94 -0
  33. bhshell-0.1.0/bhshell/queries/marking.py +121 -0
  34. bhshell-0.1.0/bhshell/queries/paths.py +369 -0
  35. bhshell-0.1.0/bhshell/queries/search.py +196 -0
  36. bhshell-0.1.0/bhshell/queries/service.py +529 -0
  37. bhshell-0.1.0/bhshell/render/__init__.py +0 -0
  38. bhshell-0.1.0/bhshell/render/export.py +79 -0
  39. bhshell-0.1.0/bhshell/render/graph.py +326 -0
  40. bhshell-0.1.0/bhshell/render/layout.py +138 -0
  41. bhshell-0.1.0/bhshell/render/png.py +44 -0
  42. bhshell-0.1.0/bhshell/render/styles.py +5 -0
  43. bhshell-0.1.0/bhshell/render/svg.py +495 -0
  44. bhshell-0.1.0/bhshell/render/views.py +337 -0
  45. bhshell-0.1.0/bhshell/shell/__init__.py +0 -0
  46. bhshell-0.1.0/bhshell/shell/app.py +189 -0
  47. bhshell-0.1.0/bhshell/shell/commands/__init__.py +0 -0
  48. bhshell-0.1.0/bhshell/shell/commands/base.py +60 -0
  49. bhshell-0.1.0/bhshell/shell/commands/connection.py +236 -0
  50. bhshell-0.1.0/bhshell/shell/commands/cypher.py +97 -0
  51. bhshell-0.1.0/bhshell/shell/commands/domains.py +80 -0
  52. bhshell-0.1.0/bhshell/shell/commands/entities.py +88 -0
  53. bhshell-0.1.0/bhshell/shell/commands/hunt.py +151 -0
  54. bhshell-0.1.0/bhshell/shell/commands/library.py +56 -0
  55. bhshell-0.1.0/bhshell/shell/commands/marking.py +59 -0
  56. bhshell-0.1.0/bhshell/shell/commands/paths.py +166 -0
  57. bhshell-0.1.0/bhshell/shell/commands/search.py +125 -0
  58. bhshell-0.1.0/bhshell.egg-info/PKG-INFO +134 -0
  59. bhshell-0.1.0/bhshell.egg-info/SOURCES.txt +76 -0
  60. bhshell-0.1.0/bhshell.egg-info/dependency_links.txt +1 -0
  61. bhshell-0.1.0/bhshell.egg-info/entry_points.txt +2 -0
  62. bhshell-0.1.0/bhshell.egg-info/requires.txt +8 -0
  63. bhshell-0.1.0/bhshell.egg-info/top_level.txt +1 -0
  64. bhshell-0.1.0/docs/USAGE.md +317 -0
  65. bhshell-0.1.0/pyproject.toml +69 -0
  66. bhshell-0.1.0/setup.cfg +4 -0
  67. bhshell-0.1.0/tests/__init__.py +0 -0
  68. bhshell-0.1.0/tests/conftest.py +174 -0
  69. bhshell-0.1.0/tests/test_analysis.py +339 -0
  70. bhshell-0.1.0/tests/test_client.py +184 -0
  71. bhshell-0.1.0/tests/test_disabled_paths.py +162 -0
  72. bhshell-0.1.0/tests/test_graph.py +48 -0
  73. bhshell-0.1.0/tests/test_hunt.py +216 -0
  74. bhshell-0.1.0/tests/test_png.py +61 -0
  75. bhshell-0.1.0/tests/test_search.py +123 -0
  76. bhshell-0.1.0/tests/test_service.py +338 -0
  77. bhshell-0.1.0/tests/test_shell.py +243 -0
  78. bhshell-0.1.0/tests/test_svg.py +171 -0
bhshell-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kr0no
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ graft tests
2
+ graft docs
3
+ global-exclude __pycache__ *.py[cod]
bhshell-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,134 @@
1
+ Metadata-Version: 2.4
2
+ Name: bhshell
3
+ Version: 0.1.0
4
+ Summary: Interactive shell for BloodHound
5
+ Author-email: kr0no <kr0no@ka0labs.net>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/kr0no/bloodhound-shell
8
+ Project-URL: Issues, https://github.com/kr0no/bloodhound-shell/issues
9
+ Project-URL: Author, https://kr0no.me
10
+ Keywords: bloodhound,bloodhound-ce,active-directory,attack-paths,cypher,pentest,red-team,security,cli,shell
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Information Technology
14
+ Classifier: Intended Audience :: System Administrators
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Security
23
+ Classifier: Topic :: System :: Systems Administration
24
+ Requires-Python: >=3.11
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: cmd2<5,>=4.3
28
+ Requires-Dist: rich>=13.0
29
+ Requires-Dist: requests>=2.31
30
+ Requires-Dist: resvg-py>=0.5
31
+ Provides-Extra: dev
32
+ Requires-Dist: pytest>=8.0; extra == "dev"
33
+ Requires-Dist: responses>=0.25; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ # bhshell
37
+
38
+ [![PyPI version](https://img.shields.io/pypi/v/bhshell.svg)](https://pypi.org/project/bhshell/)
39
+ [![Python versions](https://img.shields.io/pypi/pyversions/bhshell.svg)](https://pypi.org/project/bhshell/)
40
+ [![CI](https://github.com/kr0no/bloodhound-shell/actions/workflows/ci.yml/badge.svg)](https://github.com/kr0no/bloodhound-shell/actions/workflows/ci.yml)
41
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/kr0no/bloodhound-shell/blob/main/LICENSE)
42
+
43
+ An interactive command-line shell for BloodHound. Look up Active Directory objects,
44
+ hunt for weaknesses and find attack paths from the terminal, without opening the web UI.
45
+
46
+ ## Installation
47
+
48
+ Requires Python 3.11+ and a BloodHound CE v9 server (tested on v9.7.1).
49
+
50
+ ```bash
51
+ pipx install bhshell # recommended
52
+ # or
53
+ pip install bhshell
54
+ ```
55
+
56
+ ## Quick Start
57
+
58
+ ```console
59
+ $ bhshell --url http://127.0.0.1:8080
60
+
61
+ [bh: shell] > login -u admin
62
+ [+] Logged in as admin on 127.0.0.1:8080
63
+
64
+ [bh: shell] > hunt
65
+ Severity Check Count Example Description
66
+ critical dcsync 2 SVC_SQL@CORP.LOCAL Non-Tier-Zero principals with replication rights
67
+ high kerberoastable 2 JDOE@CORP.LOCAL Kerberoastable users
68
+ ...
69
+ [*] Details: 'hunt CHECK' (e.g. 'hunt kerberoastable'), or 'hunt --all'.
70
+
71
+ [bh: shell] > user CORP\svc_sql --all -o svc_sql.csv
72
+
73
+ [bh: shell] > path --owned -g
74
+ ```
75
+
76
+ The same commands run non-interactively for scripting: `bhshell -c "hunt --json" > hunt.json`.
77
+
78
+ ## What it does
79
+
80
+ | Command | Shows |
81
+ |---|---|
82
+ | `user`, `computer`, `group`, `lookup` | Kerberoastable / AS-REP roastable, delegation, password age, Tier Zero and owned flags, plus group nesting, inbound/outbound ACL control, sessions and local admin rights |
83
+ | `search` | Objects matching property filters (`search -t user hasspn enabled`), with dates, ages (`90d`) and wildcards |
84
+ | `path` | Shortest paths to a target or to Tier Zero, from everything owned, or through `--via` steps; printed as hop chains, a terminal graph, or SVG/PNG for reports |
85
+ | `hunt` | Domain-wide checks by severity: DCSync, control of Tier Zero, AD CS (ESC*), Kerberoasting, privileged sessions, LAPS/gMSA readers, passwords in attributes, takeover primitives (Shadow Credentials, RBCD), GPO control, NTLM relay, trusts, possible honeytokens...; `hunt --owned` limits them to what your owned objects can do |
86
+ | `cypher`, `query` | Raw Cypher, and a library of saved queries: bundled, your own `.cyp` files (`~/.bhshell/queries/` or any folder set with `config query_dirs`) and those saved on the server |
87
+ | `owned`, `hvt` | Mark or unmark owned and Tier Zero objects |
88
+ | `domains`, `trusts` | Per-domain counts, and the trust map with SID filtering status |
89
+
90
+ To add your own query, save it as a `.cyp` file in `~/.bhshell/queries/` and run it by name:
91
+
92
+ ```console
93
+ $ cat ~/.bhshell/queries/stale-admins.cyp
94
+ // description: Enabled adminCount users who have not logged on for a year
95
+ MATCH (u:User) WHERE u.admincount = true AND u.enabled = true
96
+ AND u.lastlogontimestamp < (datetime().epochseconds - 31536000) RETURN u
97
+
98
+ [bh: shell] > query run stale-admins -o stale-admins.csv
99
+ ```
100
+
101
+ See [Query library](https://github.com/kr0no/bloodhound-shell/blob/main/docs/USAGE.md#query-library) for the format and for
102
+ keeping queries in other folders (a team repository, one folder per engagement).
103
+
104
+ Most commands take `--json` and `-o FILE` (JSON or CSV). Login works with a password (plus MFA) or
105
+ an API token; secrets are prompted for or read from the environment, never passed as arguments.
106
+
107
+ ## Documentation
108
+
109
+ The full usage guide (every command, view flag, search filter, path option, hunt check and
110
+ configuration setting) is in [docs/USAGE.md](https://github.com/kr0no/bloodhound-shell/blob/main/docs/USAGE.md).
111
+ Inside the shell, run `help` or `help <command>`.
112
+
113
+ ## Contributing
114
+
115
+ Bug reports and pull requests are welcome at
116
+ [github.com/kr0no/bloodhound-shell/issues](https://github.com/kr0no/bloodhound-shell/issues).
117
+
118
+ ```bash
119
+ git clone https://github.com/kr0no/bloodhound-shell.git
120
+ cd bloodhound-shell
121
+ python3 -m venv .venv && . .venv/bin/activate
122
+ pip install -e '.[dev]'
123
+ python -m pytest
124
+ ```
125
+
126
+ The tests mock the BloodHound API with `responses`, so no BloodHound server is needed.
127
+
128
+ ## License
129
+
130
+ Distributed under the MIT License. See
131
+ [LICENSE](https://github.com/kr0no/bloodhound-shell/blob/main/LICENSE) for details.
132
+
133
+ BloodHound is a trademark of SpecterOps. bhshell is an independent project and is not affiliated
134
+ with or endorsed by SpecterOps.
@@ -0,0 +1,99 @@
1
+ # bhshell
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/bhshell.svg)](https://pypi.org/project/bhshell/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/bhshell.svg)](https://pypi.org/project/bhshell/)
5
+ [![CI](https://github.com/kr0no/bloodhound-shell/actions/workflows/ci.yml/badge.svg)](https://github.com/kr0no/bloodhound-shell/actions/workflows/ci.yml)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/kr0no/bloodhound-shell/blob/main/LICENSE)
7
+
8
+ An interactive command-line shell for BloodHound. Look up Active Directory objects,
9
+ hunt for weaknesses and find attack paths from the terminal, without opening the web UI.
10
+
11
+ ## Installation
12
+
13
+ Requires Python 3.11+ and a BloodHound CE v9 server (tested on v9.7.1).
14
+
15
+ ```bash
16
+ pipx install bhshell # recommended
17
+ # or
18
+ pip install bhshell
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ```console
24
+ $ bhshell --url http://127.0.0.1:8080
25
+
26
+ [bh: shell] > login -u admin
27
+ [+] Logged in as admin on 127.0.0.1:8080
28
+
29
+ [bh: shell] > hunt
30
+ Severity Check Count Example Description
31
+ critical dcsync 2 SVC_SQL@CORP.LOCAL Non-Tier-Zero principals with replication rights
32
+ high kerberoastable 2 JDOE@CORP.LOCAL Kerberoastable users
33
+ ...
34
+ [*] Details: 'hunt CHECK' (e.g. 'hunt kerberoastable'), or 'hunt --all'.
35
+
36
+ [bh: shell] > user CORP\svc_sql --all -o svc_sql.csv
37
+
38
+ [bh: shell] > path --owned -g
39
+ ```
40
+
41
+ The same commands run non-interactively for scripting: `bhshell -c "hunt --json" > hunt.json`.
42
+
43
+ ## What it does
44
+
45
+ | Command | Shows |
46
+ |---|---|
47
+ | `user`, `computer`, `group`, `lookup` | Kerberoastable / AS-REP roastable, delegation, password age, Tier Zero and owned flags, plus group nesting, inbound/outbound ACL control, sessions and local admin rights |
48
+ | `search` | Objects matching property filters (`search -t user hasspn enabled`), with dates, ages (`90d`) and wildcards |
49
+ | `path` | Shortest paths to a target or to Tier Zero, from everything owned, or through `--via` steps; printed as hop chains, a terminal graph, or SVG/PNG for reports |
50
+ | `hunt` | Domain-wide checks by severity: DCSync, control of Tier Zero, AD CS (ESC*), Kerberoasting, privileged sessions, LAPS/gMSA readers, passwords in attributes, takeover primitives (Shadow Credentials, RBCD), GPO control, NTLM relay, trusts, possible honeytokens...; `hunt --owned` limits them to what your owned objects can do |
51
+ | `cypher`, `query` | Raw Cypher, and a library of saved queries: bundled, your own `.cyp` files (`~/.bhshell/queries/` or any folder set with `config query_dirs`) and those saved on the server |
52
+ | `owned`, `hvt` | Mark or unmark owned and Tier Zero objects |
53
+ | `domains`, `trusts` | Per-domain counts, and the trust map with SID filtering status |
54
+
55
+ To add your own query, save it as a `.cyp` file in `~/.bhshell/queries/` and run it by name:
56
+
57
+ ```console
58
+ $ cat ~/.bhshell/queries/stale-admins.cyp
59
+ // description: Enabled adminCount users who have not logged on for a year
60
+ MATCH (u:User) WHERE u.admincount = true AND u.enabled = true
61
+ AND u.lastlogontimestamp < (datetime().epochseconds - 31536000) RETURN u
62
+
63
+ [bh: shell] > query run stale-admins -o stale-admins.csv
64
+ ```
65
+
66
+ See [Query library](https://github.com/kr0no/bloodhound-shell/blob/main/docs/USAGE.md#query-library) for the format and for
67
+ keeping queries in other folders (a team repository, one folder per engagement).
68
+
69
+ Most commands take `--json` and `-o FILE` (JSON or CSV). Login works with a password (plus MFA) or
70
+ an API token; secrets are prompted for or read from the environment, never passed as arguments.
71
+
72
+ ## Documentation
73
+
74
+ The full usage guide (every command, view flag, search filter, path option, hunt check and
75
+ configuration setting) is in [docs/USAGE.md](https://github.com/kr0no/bloodhound-shell/blob/main/docs/USAGE.md).
76
+ Inside the shell, run `help` or `help <command>`.
77
+
78
+ ## Contributing
79
+
80
+ Bug reports and pull requests are welcome at
81
+ [github.com/kr0no/bloodhound-shell/issues](https://github.com/kr0no/bloodhound-shell/issues).
82
+
83
+ ```bash
84
+ git clone https://github.com/kr0no/bloodhound-shell.git
85
+ cd bloodhound-shell
86
+ python3 -m venv .venv && . .venv/bin/activate
87
+ pip install -e '.[dev]'
88
+ python -m pytest
89
+ ```
90
+
91
+ The tests mock the BloodHound API with `responses`, so no BloodHound server is needed.
92
+
93
+ ## License
94
+
95
+ Distributed under the MIT License. See
96
+ [LICENSE](https://github.com/kr0no/bloodhound-shell/blob/main/LICENSE) for details.
97
+
98
+ BloodHound is a trademark of SpecterOps. bhshell is an independent project and is not affiliated
99
+ with or endorsed by SpecterOps.
@@ -0,0 +1,3 @@
1
+ """bhshell - an interactive shell for BloodHound."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from bhshell.cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,55 @@
1
+ """Command-line entry point."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from collections.abc import Sequence
8
+
9
+ from cmd2.string_utils import quote_if_needed
10
+
11
+ from bhshell import __version__
12
+ from bhshell.client.client import normalize_url
13
+ from bhshell.config import Config
14
+
15
+
16
+ def build_parser() -> argparse.ArgumentParser:
17
+ p = argparse.ArgumentParser(prog="bhshell", description="Interactive shell for BloodHound.")
18
+ p.add_argument("--url", help="BH backend URL (persisted; default http://127.0.0.1:8080/)")
19
+ p.add_argument("-k", "--insecure", action="store_true", help="disable TLS certificate verification (persisted; 'tls on' re-enables it)")
20
+ p.add_argument("--no-resume", action="store_true", help="do not resume the cached session")
21
+ p.add_argument("-c", "--command", action="append", default=[], metavar="CMD",
22
+ help="run CMD and exit (repeatable)")
23
+ p.add_argument("-s", "--script", metavar="FILE", help="run commands from FILE and exit")
24
+ p.add_argument("--debug", action="store_true", help="show tracebacks for errors")
25
+ p.add_argument("--version", action="version", version=f"bhshell {__version__}")
26
+ return p
27
+
28
+
29
+ def main(argv: Sequence[str] | None = None) -> int:
30
+ args = build_parser().parse_args(argv)
31
+ config = Config.load()
32
+ changed = False
33
+ if args.url:
34
+ try:
35
+ config.url = normalize_url(args.url)
36
+ except ValueError as exc:
37
+ print(f"error: {exc}", file=sys.stderr)
38
+ return 2
39
+ changed = True
40
+ if args.insecure:
41
+ config.verify_tls = False
42
+ changed = True
43
+ if changed:
44
+ config.save()
45
+
46
+ from bhshell.shell.app import BHShell # imported late so '--help' stays fast
47
+
48
+ app = BHShell(config, debug=args.debug, resume=not args.no_resume)
49
+ if args.command or args.script:
50
+ commands = list(args.command)
51
+ if args.script:
52
+ commands.append(f"run_script {quote_if_needed(args.script)}")
53
+ app.runcmds_plus_hooks(commands)
54
+ return app.exit_code or (1 if app.command_failures else 0)
55
+ return app.cmdloop() or 0
File without changes
@@ -0,0 +1,74 @@
1
+ """Request authenticators for the BH API.
2
+
3
+ Two schemes are supported:
4
+
5
+ * `JWTAuth` - `Authorization: Bearer <session_token>` obtained from
6
+ `POST /api/v2/login`.
7
+ * `HMACAuth` - BH API tokens (Token ID + Token Key). Each request is
8
+ signed with a chained HMAC-SHA256 as documented by SpecterOps:
9
+
10
+ 1. `k1 = HMAC(token_key, METHOD + URI)`
11
+ 2. `k2 = HMAC(k1, RequestDate[:13])` (RFC3339 date truncated to the hour)
12
+ 3. `signature = HMAC(k2, body)`
13
+
14
+ Headers: `Authorization: bhesignature <token_id>`, `RequestDate` and
15
+ `Signature` (base64).
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import base64
21
+ import hashlib
22
+ import hmac
23
+ from collections.abc import Callable
24
+ from datetime import datetime
25
+ from urllib.parse import urlsplit
26
+
27
+ from requests import PreparedRequest
28
+ from requests.auth import AuthBase
29
+
30
+
31
+ class JWTAuth(AuthBase):
32
+ def __init__(self, token: str) -> None:
33
+ self.token = token
34
+
35
+ def __call__(self, r: PreparedRequest) -> PreparedRequest:
36
+ r.headers["Authorization"] = f"Bearer {self.token}"
37
+ return r
38
+
39
+
40
+ class HMACAuth(AuthBase):
41
+ def __init__(
42
+ self,
43
+ token_id: str,
44
+ token_key: str,
45
+ *,
46
+ clock: Callable[[], datetime] | None = None,
47
+ ) -> None:
48
+ self.token_id = token_id
49
+ self._key = token_key.encode()
50
+ self._clock = clock or (lambda: datetime.now().astimezone())
51
+
52
+ @staticmethod
53
+ def _request_uri(url: str) -> str:
54
+ parts = urlsplit(url)
55
+ return parts.path + (f"?{parts.query}" if parts.query else "")
56
+
57
+ def sign(self, method: str, uri: str, body: bytes | None, request_date: str) -> str:
58
+ digester = hmac.new(self._key, None, hashlib.sha256)
59
+ digester.update(f"{method.upper()}{uri}".encode())
60
+ digester = hmac.new(digester.digest(), None, hashlib.sha256)
61
+ digester.update(request_date[:13].encode())
62
+ digester = hmac.new(digester.digest(), None, hashlib.sha256)
63
+ if body:
64
+ digester.update(body)
65
+ return base64.b64encode(digester.digest()).decode()
66
+
67
+ def __call__(self, r: PreparedRequest) -> PreparedRequest:
68
+ request_date = self._clock().isoformat("T")
69
+ body = r.body.encode() if isinstance(r.body, str) else r.body
70
+ signature = self.sign(r.method or "GET", self._request_uri(r.url or ""), body, request_date)
71
+ r.headers["Authorization"] = f"bhesignature {self.token_id}"
72
+ r.headers["RequestDate"] = request_date
73
+ r.headers["Signature"] = signature
74
+ return r