Skip to content

Commit cfae090

Browse files
committed
added queries subcommand which imports and exports custom queries
closes exploide#3
1 parent 0ebabd0 commit cfae090

3 files changed

Lines changed: 84 additions & 0 deletions

File tree

src/bloodhound_cli/api/__init__.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,26 @@ def add_to_asset_group(self, asset_group_id, sids):
178178
return self._send("POST", endpoint, data)
179179

180180

181+
def get_saved_queries(self, sort_by=None):
182+
"""Get saved custom queries."""
183+
184+
endpoint = "/api/v2/saved-queries"
185+
if sort_by is not None:
186+
endpoint += f"?sort_by={urllib.parse.quote_plus(sort_by)}"
187+
return self._send("GET", endpoint)
188+
189+
190+
def add_saved_query(self, name, query):
191+
"""Add a custom query."""
192+
193+
endpoint = "/api/v2/saved-queries"
194+
data = {
195+
"name": name,
196+
"query": query,
197+
}
198+
return self._send("POST", endpoint, data)
199+
200+
181201
def domains(self, collected=None):
182202
"""Return available domains."""
183203

src/bloodhound_cli/cli/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from .domains import domains
1010
from .groups import groups
1111
from .owned import owned
12+
from .queries import queries
1213
from .stats import stats
1314
from .upload import upload
1415
from .users import users
@@ -30,6 +31,7 @@ def bloodhound_cli(debug=False):
3031
bloodhound_cli.add_command(domains)
3132
bloodhound_cli.add_command(groups)
3233
bloodhound_cli.add_command(owned)
34+
bloodhound_cli.add_command(queries)
3335
bloodhound_cli.add_command(stats)
3436
bloodhound_cli.add_command(upload)
3537
bloodhound_cli.add_command(users)

src/bloodhound_cli/cli/queries.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import json
2+
import sys
3+
4+
import click
5+
6+
from bloodhound_cli.api.exceptions import ApiException
7+
from bloodhound_cli.api.from_config import api
8+
from bloodhound_cli.logger import log
9+
10+
11+
@click.command()
12+
@click.argument("file", type=click.Path(dir_okay=False))
13+
@click.option("--save", is_flag=True, help="Save existing custom queries to file.")
14+
def queries(file, save):
15+
"""Import and export custom queries.
16+
17+
Imports custom Cypher queries from a file to the BloodHound database.
18+
If --save is specified, existing queries are exported to a file instead of imported.
19+
20+
For import, the file must either be in the format that --save produces or in the legacy Bloodhound's customqueries.json format.
21+
However, not everything from the latter might be compatible.
22+
"""
23+
24+
if save:
25+
# export queries
26+
result = api.get_saved_queries(sort_by="name")
27+
queries_to_export = [{key: entry[key] for key in ("name", "query")} for entry in result]
28+
with open(file, "w", encoding="UTF-8") as f:
29+
json.dump(queries_to_export, f, indent=4)
30+
log.info("Saved %d queries to %s", len(queries_to_export), file)
31+
32+
else:
33+
# import queries
34+
with open(file, "r", encoding="UTF-8") as f:
35+
try:
36+
queries_to_import = json.load(f)
37+
except json.decoder.JSONDecodeError:
38+
log.error("Cannot parse file %s. A JSON file containing custom queries is required.", file)
39+
sys.exit(1)
40+
41+
# compatibility import of legacy BloodHound's customqueries.json
42+
if isinstance(queries_to_import, dict) and "queries" in queries_to_import:
43+
log.warning("Detected legacy BloodHound's customqueries.json format. Some queries might not be compatible with BloodHound CE.")
44+
compatible_queries = []
45+
for entry in queries_to_import["queries"]:
46+
if len(entry["queryList"]) > 1 or not entry["queryList"][0]["final"]:
47+
log.error('Query "%s" requires node selection, which is not compatible with BloodHound CE.', entry["name"])
48+
continue
49+
compatible_queries.append({"name": entry["name"], "query": entry["queryList"][0]["query"]})
50+
queries_to_import = compatible_queries
51+
52+
num_queries = 0
53+
for query in queries_to_import:
54+
try:
55+
result = api.add_saved_query(query["name"], query["query"])
56+
num_queries += 1
57+
except ApiException as e:
58+
if e.response.status_code == 400:
59+
log.error('Could not import query "%s": %s', query["name"], '\n'.join(error["message"] for error in e.response.json()["errors"]))
60+
else:
61+
raise
62+
log.info("Imported %d custom queries.", num_queries)

0 commit comments

Comments
 (0)