Skip to content

Commit 3ddf25c

Browse files
author
r0BIT
committed
feat: Rich table-based CLI help with grouped options
- Add rich-argparse dependency for Rich-formatted help output - Create custom TableHelpAction for table-based argument display - Group arguments by category with styled headers and descriptions - Change --timeout default from 60 to 5 seconds for faster scans - Update tool description to match README tagline - Sync rich-argparse to both requirements.txt and pyproject.toml
1 parent c7e3f3c commit 3ddf25c

File tree

3 files changed

+112
-4
lines changed

3 files changed

+112
-4
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ dependencies = [
2121
"pyasn1>=0.5.0",
2222
"pyasn1-modules>=0.3.0",
2323
"rich>=13.0.0",
24+
"rich-argparse>=1.0.0",
2425
]
2526

2627
[project.optional-dependencies]

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ bhopengraph>=1.1.0 # BloodHound OpenGraph library
77
pyasn1>=0.5.0 # ASN.1 parsing for LAPS encrypted password decryption
88
pyasn1-modules>=0.3.0 # PKCS#7/CMS support for LAPS decryption
99
rich>=13.0.0 # Terminal UI: colors, progress bars, tables
10+
rich-argparse>=1.0.0 # Rich formatted CLI help

taskhound/config.py

Lines changed: 110 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
import traceback
66
from typing import Any, Dict
77

8+
from rich.console import Console
9+
from rich.table import Table
10+
from rich.text import Text
11+
from rich_argparse import RichHelpFormatter
12+
813
try:
914
import tomllib
1015
except ImportError:
@@ -17,6 +22,101 @@
1722
from .utils.helpers import is_ipv4
1823

1924

25+
class TableRichHelpFormatter(RichHelpFormatter):
26+
"""
27+
Custom help formatter that displays argument groups with Rich styling.
28+
Uses uppercase group names and custom color scheme.
29+
"""
30+
31+
# Customize styles for better appearance
32+
styles = {
33+
**RichHelpFormatter.styles,
34+
"argparse.groups": "bold cyan",
35+
"argparse.args": "green",
36+
"argparse.metavar": "yellow",
37+
"argparse.help": "white",
38+
}
39+
40+
# Format group names in uppercase
41+
group_name_formatter = str.upper
42+
43+
44+
class TableHelpAction(argparse.Action):
45+
"""
46+
Custom help action that displays arguments in Rich tables.
47+
"""
48+
49+
def __init__(self, option_strings, dest=argparse.SUPPRESS, default=argparse.SUPPRESS, help=None):
50+
super().__init__(
51+
option_strings=option_strings,
52+
dest=dest,
53+
default=default,
54+
nargs=0,
55+
help=help,
56+
)
57+
58+
def __call__(self, parser, namespace, values, option_string=None):
59+
console = Console()
60+
61+
# Print description
62+
if parser.description:
63+
console.print(f"\n[bold white]{parser.description}[/]\n")
64+
65+
# Print usage
66+
console.print(f"[dim]Usage:[/] [bold]{parser.prog}[/] [OPTIONS] [TARGETS]\n")
67+
68+
# Group arguments by their group
69+
for group in parser._action_groups:
70+
# Skip empty groups
71+
actions = [a for a in group._group_actions if not isinstance(a, argparse._HelpAction) and not isinstance(a, TableHelpAction)]
72+
if not actions:
73+
continue
74+
75+
# Print title
76+
console.print(f"[bold cyan]{group.title.upper()}[/]")
77+
78+
# Print description if present
79+
if group.description:
80+
console.print(f"[dim]{group.description}[/]")
81+
82+
# Create table for this group
83+
table = Table(
84+
border_style="dim",
85+
show_header=True,
86+
header_style="bold white",
87+
padding=(0, 1),
88+
expand=False,
89+
)
90+
91+
table.add_column("Option", style="green", no_wrap=True)
92+
table.add_column("Description", style="white")
93+
94+
for action in actions:
95+
# Build option string
96+
opts = ", ".join(action.option_strings) if action.option_strings else action.dest
97+
98+
# Add metavar if present
99+
if action.metavar:
100+
opts += f" [yellow]{action.metavar}[/]"
101+
elif action.type and action.type != bool:
102+
opts += f" [yellow]{action.dest.upper()}[/]"
103+
104+
# Get help text - don't add default if already mentioned in help
105+
help_text = action.help or ""
106+
if action.default and action.default != argparse.SUPPRESS and action.default is not None:
107+
if action.default is not True and action.default is not False:
108+
# Only add default if not already in help text
109+
if "default:" not in help_text.lower():
110+
help_text += f" [dim](default: {action.default})[/]"
111+
112+
table.add_row(opts, help_text)
113+
114+
console.print(table)
115+
console.print()
116+
117+
parser.exit()
118+
119+
20120
class OnceOnly(argparse.Action):
21121
"""
22122
Custom argparse Action to prevent arguments from being specified multiple times.
@@ -226,8 +326,14 @@ def load_config() -> Dict[str, Any]:
226326

227327
def build_parser() -> argparse.ArgumentParser:
228328
ap = argparse.ArgumentParser(
229-
prog="taskhound", description="TaskHound - Scheduled Task privilege checker with optional High Value enrichment"
230-
)
329+
prog="taskhound",
330+
description="Windows Privileged Scheduled Task Discovery Tool for fun and profit.",
331+
formatter_class=TableRichHelpFormatter,
332+
add_help=False, # Disable default help
333+
)
334+
# Add custom table-based help
335+
ap.add_argument("-h", "--help", action=TableHelpAction, help="Show this help message")
336+
231337
# Authentication options
232338
auth = ap.add_argument_group("Authentication options")
233339
auth.add_argument("-u", "--username", action=OnceOnly, help="Username (required for online mode)")
@@ -248,8 +354,8 @@ def build_parser() -> argparse.ArgumentParser:
248354
target.add_argument(
249355
"--timeout",
250356
type=int,
251-
default=60,
252-
help="Connection timeout in seconds (default: 60). Lower this to speed up scans of dead hosts.",
357+
default=5,
358+
help="Connection timeout in seconds (default: 5). Lower values speed up scans with unreachable hosts.",
253359
)
254360
target.add_argument(
255361
"--threads",

0 commit comments

Comments
 (0)