DACLhound 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.
- daclhound-0.1.0/DACLhound.egg-info/PKG-INFO +101 -0
- daclhound-0.1.0/DACLhound.egg-info/SOURCES.txt +13 -0
- daclhound-0.1.0/DACLhound.egg-info/dependency_links.txt +1 -0
- daclhound-0.1.0/DACLhound.egg-info/entry_points.txt +3 -0
- daclhound-0.1.0/DACLhound.egg-info/requires.txt +1 -0
- daclhound-0.1.0/DACLhound.egg-info/top_level.txt +3 -0
- daclhound-0.1.0/LICENSE +21 -0
- daclhound-0.1.0/PKG-INFO +101 -0
- daclhound-0.1.0/README.md +80 -0
- daclhound-0.1.0/daclgraph_server.py +150 -0
- daclhound-0.1.0/daclhound_assets/__init__.py +1 -0
- daclhound-0.1.0/daclhound_assets/viewer.html +494 -0
- daclhound-0.1.0/daclhound_cli.py +1695 -0
- daclhound-0.1.0/pyproject.toml +39 -0
- daclhound-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: DACLhound
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Collect Active Directory DACLs and explore them in an interactive graph
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Keywords: active-directory,acl,dacl,bloodhound,security
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Intended Audience :: Information Technology
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Security
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: bloodyAD==2.5.5
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# DACLhound
|
|
23
|
+
|
|
24
|
+
DACLhound collects Active Directory security descriptors over LDAP and turns the results into a graph you can inspect in a browser. The collector uses bloodyAD as a Python library; it does not run the `bloodyAD` command.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
Python 3.10 or newer is required.
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
pipx install DACLhound
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Upgrade later with:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
pipx upgrade DACLhound
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Collect a graph
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
DACLhound.py collect --host "$FQDN" -d "$DOMAIN" -u 'user' -p 'password' --dc-ip "$IP" --output "$DOMAIN.json"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The collector is read-only. By default it collects users, groups, computers, OUs, containers, GPOs, the domain, and ADCS data. Use `--types` to limit directory object types:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
DACLhound.py collect --host "$FQDN" -d "$DOMAIN" -u 'user' -p 'password' --types users,groups --output users-and-groups.json
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Use `--no-adcs` to skip ADCS collection. The normalized graph is written to `--output`. Add `--raw-output` to keep the bloodyAD response as well.
|
|
53
|
+
|
|
54
|
+
## Authentication
|
|
55
|
+
|
|
56
|
+
### NTLM password
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
DACLhound.py collect --host "$FQDN" -d "$DOMAIN" -u 'user' -p 'password' --dc-ip "$IP" -o "$DOMAIN.json"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### NTLM pass-the-hash
|
|
63
|
+
|
|
64
|
+
Use `LMHASH:NTHASH`; if you only have the NT hash, leave the LM field empty:
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
DACLhound.py collect --host "$FQDN" -d "$DOMAIN" -u 'user' -hashes ':<NTHASH>' --dc-ip "$IP" -o "$DOMAIN.json"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Kerberos
|
|
71
|
+
|
|
72
|
+
Use the current `KRB5CCNAME`, or pass a ccache path to `-k`:
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
DACLhound.py collect --host "$FQDN" -d "$DOMAIN" -u 'user' -k --dc-ip "$IP" -o "$DOMAIN.json"
|
|
76
|
+
DACLhound.py collect --host "$FQDN" -d "$DOMAIN" -u 'user' -k user.ccache --dc-ip "$IP" -o "$DOMAIN.json"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Kerberos can also use a password or NT hash with `-k`. For an AES key, use `-aesKey <AESKEY>`. Add `-s` to use LDAPS.
|
|
80
|
+
|
|
81
|
+
## Open the graph
|
|
82
|
+
|
|
83
|
+
Run the viewer in the foreground and stop it with `Ctrl+C`:
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
DACLhound.py graph --input "$DOMAIN.json"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Or run it in the background:
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
DACLhound.py graph start --input "$DOMAIN.json"
|
|
93
|
+
DACLhound.py graph status
|
|
94
|
+
DACLhound.py graph stop
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The server listens on `127.0.0.1:8765` by default. Uploaded JSON files stay in memory while the server is running and are discarded when it stops. Use `--bind` and `--port` to change the listener. Run `DACLhound.py collect --help` or `DACLhound.py graph --help` for all options.
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
daclgraph_server.py
|
|
4
|
+
daclhound_cli.py
|
|
5
|
+
pyproject.toml
|
|
6
|
+
DACLhound.egg-info/PKG-INFO
|
|
7
|
+
DACLhound.egg-info/SOURCES.txt
|
|
8
|
+
DACLhound.egg-info/dependency_links.txt
|
|
9
|
+
DACLhound.egg-info/entry_points.txt
|
|
10
|
+
DACLhound.egg-info/requires.txt
|
|
11
|
+
DACLhound.egg-info/top_level.txt
|
|
12
|
+
daclhound_assets/__init__.py
|
|
13
|
+
daclhound_assets/viewer.html
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
bloodyAD==2.5.5
|
daclhound-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gzzcoo
|
|
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.
|
daclhound-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: DACLhound
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Collect Active Directory DACLs and explore them in an interactive graph
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Keywords: active-directory,acl,dacl,bloodhound,security
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Intended Audience :: Information Technology
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Security
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: bloodyAD==2.5.5
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# DACLhound
|
|
23
|
+
|
|
24
|
+
DACLhound collects Active Directory security descriptors over LDAP and turns the results into a graph you can inspect in a browser. The collector uses bloodyAD as a Python library; it does not run the `bloodyAD` command.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
Python 3.10 or newer is required.
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
pipx install DACLhound
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Upgrade later with:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
pipx upgrade DACLhound
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Collect a graph
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
DACLhound.py collect --host "$FQDN" -d "$DOMAIN" -u 'user' -p 'password' --dc-ip "$IP" --output "$DOMAIN.json"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The collector is read-only. By default it collects users, groups, computers, OUs, containers, GPOs, the domain, and ADCS data. Use `--types` to limit directory object types:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
DACLhound.py collect --host "$FQDN" -d "$DOMAIN" -u 'user' -p 'password' --types users,groups --output users-and-groups.json
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Use `--no-adcs` to skip ADCS collection. The normalized graph is written to `--output`. Add `--raw-output` to keep the bloodyAD response as well.
|
|
53
|
+
|
|
54
|
+
## Authentication
|
|
55
|
+
|
|
56
|
+
### NTLM password
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
DACLhound.py collect --host "$FQDN" -d "$DOMAIN" -u 'user' -p 'password' --dc-ip "$IP" -o "$DOMAIN.json"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### NTLM pass-the-hash
|
|
63
|
+
|
|
64
|
+
Use `LMHASH:NTHASH`; if you only have the NT hash, leave the LM field empty:
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
DACLhound.py collect --host "$FQDN" -d "$DOMAIN" -u 'user' -hashes ':<NTHASH>' --dc-ip "$IP" -o "$DOMAIN.json"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Kerberos
|
|
71
|
+
|
|
72
|
+
Use the current `KRB5CCNAME`, or pass a ccache path to `-k`:
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
DACLhound.py collect --host "$FQDN" -d "$DOMAIN" -u 'user' -k --dc-ip "$IP" -o "$DOMAIN.json"
|
|
76
|
+
DACLhound.py collect --host "$FQDN" -d "$DOMAIN" -u 'user' -k user.ccache --dc-ip "$IP" -o "$DOMAIN.json"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Kerberos can also use a password or NT hash with `-k`. For an AES key, use `-aesKey <AESKEY>`. Add `-s` to use LDAPS.
|
|
80
|
+
|
|
81
|
+
## Open the graph
|
|
82
|
+
|
|
83
|
+
Run the viewer in the foreground and stop it with `Ctrl+C`:
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
DACLhound.py graph --input "$DOMAIN.json"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Or run it in the background:
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
DACLhound.py graph start --input "$DOMAIN.json"
|
|
93
|
+
DACLhound.py graph status
|
|
94
|
+
DACLhound.py graph stop
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The server listens on `127.0.0.1:8765` by default. Uploaded JSON files stay in memory while the server is running and are discarded when it stops. Use `--bind` and `--port` to change the listener. Run `DACLhound.py collect --help` or `DACLhound.py graph --help` for all options.
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# DACLhound
|
|
2
|
+
|
|
3
|
+
DACLhound collects Active Directory security descriptors over LDAP and turns the results into a graph you can inspect in a browser. The collector uses bloodyAD as a Python library; it does not run the `bloodyAD` command.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Python 3.10 or newer is required.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pipx install DACLhound
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Upgrade later with:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
pipx upgrade DACLhound
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Collect a graph
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
DACLhound.py collect --host "$FQDN" -d "$DOMAIN" -u 'user' -p 'password' --dc-ip "$IP" --output "$DOMAIN.json"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The collector is read-only. By default it collects users, groups, computers, OUs, containers, GPOs, the domain, and ADCS data. Use `--types` to limit directory object types:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
DACLhound.py collect --host "$FQDN" -d "$DOMAIN" -u 'user' -p 'password' --types users,groups --output users-and-groups.json
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Use `--no-adcs` to skip ADCS collection. The normalized graph is written to `--output`. Add `--raw-output` to keep the bloodyAD response as well.
|
|
32
|
+
|
|
33
|
+
## Authentication
|
|
34
|
+
|
|
35
|
+
### NTLM password
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
DACLhound.py collect --host "$FQDN" -d "$DOMAIN" -u 'user' -p 'password' --dc-ip "$IP" -o "$DOMAIN.json"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### NTLM pass-the-hash
|
|
42
|
+
|
|
43
|
+
Use `LMHASH:NTHASH`; if you only have the NT hash, leave the LM field empty:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
DACLhound.py collect --host "$FQDN" -d "$DOMAIN" -u 'user' -hashes ':<NTHASH>' --dc-ip "$IP" -o "$DOMAIN.json"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Kerberos
|
|
50
|
+
|
|
51
|
+
Use the current `KRB5CCNAME`, or pass a ccache path to `-k`:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
DACLhound.py collect --host "$FQDN" -d "$DOMAIN" -u 'user' -k --dc-ip "$IP" -o "$DOMAIN.json"
|
|
55
|
+
DACLhound.py collect --host "$FQDN" -d "$DOMAIN" -u 'user' -k user.ccache --dc-ip "$IP" -o "$DOMAIN.json"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Kerberos can also use a password or NT hash with `-k`. For an AES key, use `-aesKey <AESKEY>`. Add `-s` to use LDAPS.
|
|
59
|
+
|
|
60
|
+
## Open the graph
|
|
61
|
+
|
|
62
|
+
Run the viewer in the foreground and stop it with `Ctrl+C`:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
DACLhound.py graph --input "$DOMAIN.json"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Or run it in the background:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
DACLhound.py graph start --input "$DOMAIN.json"
|
|
72
|
+
DACLhound.py graph status
|
|
73
|
+
DACLhound.py graph stop
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The server listens on `127.0.0.1:8765` by default. Uploaded JSON files stay in memory while the server is running and are discarded when it stops. Use `--bind` and `--port` to change the listener. Run `DACLhound.py collect --help` or `DACLhound.py graph --help` for all options.
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Serve the DACL graph viewer and keep uploaded datasets in process memory."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
from importlib.resources import files
|
|
6
|
+
import json
|
|
7
|
+
import threading
|
|
8
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from urllib.parse import unquote, urlparse
|
|
11
|
+
|
|
12
|
+
VIEWER = files("daclhound_assets").joinpath("viewer.html")
|
|
13
|
+
MAX_BODY = 64 * 1024 * 1024
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DatasetStore:
|
|
17
|
+
"""Thread-safe, process-local storage for uploaded graph datasets."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, initial_json: str | None = None) -> None:
|
|
20
|
+
self._lock = threading.Lock()
|
|
21
|
+
self._next_id = 1
|
|
22
|
+
self._datasets: list[dict] = []
|
|
23
|
+
if initial_json:
|
|
24
|
+
path = Path(initial_json).expanduser().resolve()
|
|
25
|
+
self.add(path.name, json.loads(path.read_text(encoding="utf-8")))
|
|
26
|
+
|
|
27
|
+
def add(self, name: str, data: object) -> dict:
|
|
28
|
+
if not isinstance(data, dict) or not isinstance(data.get("nodes"), list) or not isinstance(data.get("edges"), list):
|
|
29
|
+
raise ValueError("Expected a DACL graph JSON object with nodes and edges arrays")
|
|
30
|
+
with self._lock:
|
|
31
|
+
key = str(self._next_id)
|
|
32
|
+
self._next_id += 1
|
|
33
|
+
item = {"key": key, "name": name or "daclgraph.json", "data": data}
|
|
34
|
+
self._datasets.append(item)
|
|
35
|
+
return item
|
|
36
|
+
|
|
37
|
+
def list(self) -> list[dict]:
|
|
38
|
+
with self._lock:
|
|
39
|
+
return list(self._datasets)
|
|
40
|
+
|
|
41
|
+
def remove(self, key: str) -> bool:
|
|
42
|
+
with self._lock:
|
|
43
|
+
remaining = [item for item in self._datasets if item["key"] != key]
|
|
44
|
+
if len(remaining) == len(self._datasets):
|
|
45
|
+
return False
|
|
46
|
+
self._datasets = remaining
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def make_handler(store: DatasetStore, log_requests: bool = False):
|
|
51
|
+
class Handler(BaseHTTPRequestHandler):
|
|
52
|
+
server_version = "DACLGraph/1.0"
|
|
53
|
+
|
|
54
|
+
def log_message(self, fmt: str, *args) -> None:
|
|
55
|
+
if log_requests:
|
|
56
|
+
print(f"[{self.log_date_time_string()}] {fmt % args}")
|
|
57
|
+
|
|
58
|
+
def send_bytes(self, status: int, body: bytes, content_type: str) -> None:
|
|
59
|
+
self.send_response(status)
|
|
60
|
+
self.send_header("Content-Type", content_type)
|
|
61
|
+
self.send_header("Content-Length", str(len(body)))
|
|
62
|
+
self.send_header("Cache-Control", "no-store")
|
|
63
|
+
self.end_headers()
|
|
64
|
+
self.wfile.write(body)
|
|
65
|
+
|
|
66
|
+
def send_json(self, status: int, value: object) -> None:
|
|
67
|
+
body = json.dumps(value, ensure_ascii=False).encode("utf-8")
|
|
68
|
+
self.send_bytes(status, body, "application/json; charset=utf-8")
|
|
69
|
+
|
|
70
|
+
def do_GET(self) -> None:
|
|
71
|
+
path = urlparse(self.path).path
|
|
72
|
+
if path == "/api/datasets":
|
|
73
|
+
self.send_json(200, store.list())
|
|
74
|
+
return
|
|
75
|
+
if path in ("/", "/daclgraph_viewer.html"):
|
|
76
|
+
try:
|
|
77
|
+
self.send_bytes(200, VIEWER.read_bytes(), "text/html; charset=utf-8")
|
|
78
|
+
except OSError as exc:
|
|
79
|
+
self.send_json(500, {"error": str(exc)})
|
|
80
|
+
return
|
|
81
|
+
self.send_json(404, {"error": "Not found"})
|
|
82
|
+
|
|
83
|
+
def do_POST(self) -> None:
|
|
84
|
+
path = urlparse(self.path).path
|
|
85
|
+
if path != "/api/datasets":
|
|
86
|
+
self.send_json(404, {"error": "Not found"})
|
|
87
|
+
return
|
|
88
|
+
try:
|
|
89
|
+
length = int(self.headers.get("Content-Length", "0"))
|
|
90
|
+
except ValueError:
|
|
91
|
+
length = 0
|
|
92
|
+
if length <= 0 or length > MAX_BODY:
|
|
93
|
+
self.send_json(400, {"error": "Request body must be between 1 byte and 64 MiB"})
|
|
94
|
+
return
|
|
95
|
+
try:
|
|
96
|
+
payload = json.loads(self.rfile.read(length).decode("utf-8"))
|
|
97
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
98
|
+
self.send_json(400, {"error": "Invalid JSON request body"})
|
|
99
|
+
return
|
|
100
|
+
if not isinstance(payload, dict):
|
|
101
|
+
self.send_json(400, {"error": "Expected an object containing name and data"})
|
|
102
|
+
return
|
|
103
|
+
try:
|
|
104
|
+
item = store.add(str(payload.get("name") or "daclgraph.json"), payload.get("data"))
|
|
105
|
+
except ValueError as exc:
|
|
106
|
+
self.send_json(400, {"error": str(exc)})
|
|
107
|
+
return
|
|
108
|
+
self.send_json(201, item)
|
|
109
|
+
|
|
110
|
+
def do_DELETE(self) -> None:
|
|
111
|
+
path = urlparse(self.path).path
|
|
112
|
+
prefix = "/api/datasets/"
|
|
113
|
+
if not path.startswith(prefix):
|
|
114
|
+
self.send_json(404, {"error": "Not found"})
|
|
115
|
+
return
|
|
116
|
+
key = unquote(path[len(prefix):])
|
|
117
|
+
if not store.remove(key):
|
|
118
|
+
self.send_json(404, {"error": "Dataset not found"})
|
|
119
|
+
return
|
|
120
|
+
self.send_json(200, {"removed": key})
|
|
121
|
+
|
|
122
|
+
return Handler
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def run_server(host: str = "127.0.0.1", port: int = 8765, initial_json: str | None = None, log_requests: bool = False) -> None:
|
|
126
|
+
store = DatasetStore(initial_json)
|
|
127
|
+
server = ThreadingHTTPServer((host, port), make_handler(store, log_requests))
|
|
128
|
+
print(f"[+] DACLhound server listening at http://{host}:{port}/")
|
|
129
|
+
print("[+] Uploaded datasets are kept in memory until this process exits")
|
|
130
|
+
try:
|
|
131
|
+
server.serve_forever()
|
|
132
|
+
except KeyboardInterrupt:
|
|
133
|
+
print("\n[*] Stopping DACLhound server")
|
|
134
|
+
finally:
|
|
135
|
+
server.server_close()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def main() -> int:
|
|
139
|
+
parser = argparse.ArgumentParser(description="Serve the interactive DACL graph viewer with in-memory datasets.")
|
|
140
|
+
parser.add_argument("--host", default="127.0.0.1", help="Bind address (default: 127.0.0.1)")
|
|
141
|
+
parser.add_argument("--port", type=int, default=8765, help="HTTP port (default: 8765)")
|
|
142
|
+
parser.add_argument("--initial-json", help="Load one graph JSON into the in-memory session on startup")
|
|
143
|
+
parser.add_argument("--verbose", action="store_true", help="Log each HTTP request")
|
|
144
|
+
args = parser.parse_args()
|
|
145
|
+
run_server(args.host, args.port, args.initial_json, args.verbose)
|
|
146
|
+
return 0
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Static resources shipped with DACLhound."""
|