diff --git a/README.md b/README.md
index 61a5832..34255b8 100644
--- a/README.md
+++ b/README.md
@@ -61,6 +61,7 @@ Also, blah blah blah, don't use it for evil purposes.
 * Python 3.6
 * impacket >= 0.9.22
 * ldap3 >= 2.8.1
+* gssapi (Which requires `libkrb5-dev`)
 
 ## FUNCTIONALITIES
 
@@ -69,9 +70,9 @@ If you like living on the bleeding edge, check out the
 
 Here's the list of available commands:
 
-    $ ./pywerview.py --help
+    $ pywerview.py --help
     usage: pywerview.py [-h]
-                        {get-adobject,get-netuser,get-netgroup,get-netcomputer,get-netdomaincontroller,get-netfileserver,get-dfsshare,get-netou,get-netsite,get-netsubnet,get-netgpo,get-domainpolicy,get-gpttmpl,get-netgpogroup,get-netgroupmember,get-netsession,get-localdisks,get-netdomain,get-netshare,get-netloggedon,get-netlocalgroup,invoke-checklocaladminaccess,get-netprocess,get-userevent,invoke-userhunter,invoke-processhunter,invoke-eventhunter}
+                        {get-adobject,get-adserviceaccount,get-objectacl,get-netuser,get-netgroup,get-netcomputer,get-netdomaincontroller,get-netfileserver,get-dfsshare,get-netou,get-netsite,get-netsubnet,get-netdomaintrust,get-netgpo,get-netpso,get-domainpolicy,get-gpttmpl,get-netgpogroup,find-gpocomputeradmin,find-gpolocation,get-netgroupmember,get-netsession,get-localdisks,get-netdomain,get-netshare,get-netloggedon,get-netlocalgroup,invoke-checklocaladminaccess,get-netprocess,get-userevent,invoke-userhunter,invoke-processhunter,invoke-eventhunter}
                         ...
 
     Rewriting of some PowerView's functionalities in Python
@@ -82,68 +83,46 @@ Here's the list of available commands:
     Subcommands:
       Available subcommands
 
-      {get-adobject,get-netuser,get-netgroup,get-netcomputer,get-netdomaincontroller,get-netfileserver,get-dfsshare,get-netou,get-netsite,get-netsubnet,get-netgpo,get-domainpolicy,get-gpttmpl,get-netgpogroup,find-gpocomputeradmin,find-gpolocation,get-netgroupmember,get-netsession,get-localdisks,get-netdomain,get-netshare,get-netloggedon,get-netlocalgroup,invoke-checklocaladminaccess,get-netprocess,get-userevent,invoke-userhunter,invoke-processhunter,invoke-eventhunter}
-        get-adobject        Takes a domain SID, samAccountName or name, and return
-                            the associated object
+      {get-adobject,get-adserviceaccount,get-objectacl,get-netuser,get-netgroup,get-netcomputer,get-netdomaincontroller,get-netfileserver,get-dfsshare,get-netou,get-netsite,get-netsubnet,get-netdomaintrust,get-netgpo,get-netpso,get-domainpolicy,get-gpttmpl,get-netgpogroup,find-gpocomputeradmin,find-gpolocation,get-netgroupmember,get-netsession,get-localdisks,get-netdomain,get-netshare,get-netloggedon,get-netlocalgroup,invoke-checklocaladminaccess,get-netprocess,get-userevent,invoke-userhunter,invoke-processhunter,invoke-eventhunter}
+        get-adobject        Takes a domain SID, samAccountName or name, and return the associated object
+        get-adserviceaccount
+                            Returns a list of all the gMSA of the specified domain (you need privileged account to retrieve passwords)
+        get-objectacl       Takes a domain SID, samAccountName or name, and return the ACL of the associated object
         get-netuser         Queries information about a domain user
-        get-netgroup        Get a list of all current domain groups, or a list of
-                            groups a domain user is member of
+        get-netgroup        Get a list of all current domain groups, or a list of groups a domain user is member of
         get-netcomputer     Queries informations about domain computers
         get-netdomaincontroller
                             Get a list of domain controllers for the given domain
-        get-netfileserver   Return a list of file servers, extracted from the
-                            domain users' homeDirectory, scriptPath, and
-                            profilePath fields
-        get-dfsshare        Return a list of all fault tolerant distributed file
-                            systems for a given domain
+        get-netfileserver   Return a list of file servers, extracted from the domain users' homeDirectory, scriptPath, and profilePath fields
+        get-dfsshare        Return a list of all fault tolerant distributed file systems for a given domain
         get-netou           Get a list of all current OUs in the domain
         get-netsite         Get a list of all current sites in the domain
         get-netsubnet       Get a list of all current subnets in the domain
+        get-netdomaintrust  Returns a list of all the trusts of the specified domain
         get-netgpo          Get a list of all current GPOs in the domain
-        get-domainpolicy    Returns the default domain or DC policy for the
-                            queried domain or DC
-        get-gpttmpl         Helper to parse a GptTmpl.inf policy file path into a
-                            custom object
-        get-netgpogroup     Parses all GPOs in the domain that set "Restricted
-                            Group" or "Groups.xml"
+        get-netpso          Get a list of all current PSOs in the domain
+        get-domainpolicy    Returns the default domain or DC policy for the queried domain or DC
+        get-gpttmpl         Helper to parse a GptTmpl.inf policy file path into a custom object
+        get-netgpogroup     Parses all GPOs in the domain that set "Restricted Group" or "Groups.xml"
         find-gpocomputeradmin
-                            Takes a computer (or OU) and determine who has
-                            administrative access to it via GPO
-        find-gpolocation    Takes a username or a group name and determine the
-                            computers it has administrative access to via GPO
+                            Takes a computer (or OU) and determine who has administrative access to it via GPO
+        find-gpolocation    Takes a username or a group name and determine the computers it has administrative access to via GPO
         get-netgroupmember  Return a list of members of a domain group
-        get-netsession      Queries a host to return a list of active sessions on
-                            the host (you can use local credentials instead of
-                            domain credentials)
-        get-localdisks      Queries a host to return a list of active disks on the
-                            host (you can use local credentials instead of domain
-                            credentials)
+        get-netsession      Queries a host to return a list of active sessions on the host (you can use local credentials instead of domain credentials)
+        get-localdisks      Queries a host to return a list of active disks on the host (you can use local credentials instead of domain credentials)
         get-netdomain       Queries a host for available domains
-        get-netshare        Queries a host to return a list of available shares on
-                            the host (you can use local credentials instead of
-                            domain credentials)
-        get-netloggedon     This function will execute the NetWkstaUserEnum RPC
-                            call to query a given host for actively logged on
-                            users
-        get-netlocalgroup   Gets a list of members of a local group on a machine,
-                            or returns every local group. You can use local
-                            credentials instead of domain credentials, however,
-                            domain credentials are needed to resolve domain SIDs.
+        get-netshare        Queries a host to return a list of available shares on the host (you can use local credentials instead of domain credentials)
+        get-netloggedon     This function will execute the NetWkstaUserEnum RPC call to query a given host for actively logged on users
+        get-netlocalgroup   Gets a list of members of a local group on a machine, or returns every local group. You can use local credentials instead of domain credentials, however, domain credentials are needed
+                            to resolve domain SIDs.
         invoke-checklocaladminaccess
-                            Checks if the given user has local admin access on the
-                            given host
-        get-netprocess      This function will execute the 'Select * from
-                            Win32_Process' WMI query to a given host for a list of
-                            executed process
-        get-userevent       This function will execute the 'Select * from
-                            Win32_Process' WMI query to a given host for a list of
-                            executed process
+                            Checks if the given user has local admin access on the given host
+        get-netprocess      This function will execute the 'Select * from Win32_Process' WMI query to a given host for a list of executed process
+        get-userevent       This function will execute the 'SELECT * from Win32_NTLogEvent' WMI query to a given host for a list of executed process
         invoke-userhunter   Finds which machines domain users are logged into
         invoke-processhunter
-                            Searches machines for processes with specific name, or
-                            ran by specific users
-        invoke-eventhunter  Searches machines for events with specific name, or
-                            ran by specific users
+                            Searches machines for processes with specific name, or ran by specific users
+        invoke-eventhunter  Searches machines for events with specific name, or ran by specific users
 
 Take a look at the [wiki](https://github.com/the-useless-one/pywerview/wiki) to
 see a more detailed usage of every command.
@@ -155,14 +134,128 @@ For example, my domain name is `uselessdomain.local`. The Win2K compatible name
 is `USELESSDOMAIN`. In every command,  I must use __`uselessdomain.local`__ as
 an argument, and __not__ `USELESSDOMAIN`.
 
+## GLOBAL ARGUMENTS
+
+### LOGGING
+
+You can provide a logging level to `pywerview` modules by using `-l` or `--logging-level` options. Supported levels are:
+
+* `CRITICAL`: Only critical errors are displayed **(default)**
+* `WARNING` Warnings are displayed, along with citical errors
+* `DEBUG`: Debug level (caution: **very** verbose)
+* `ULTRA`: Extreme debugging level (caution: **very very** verbose)
+
+(level names are case insensitive)
+
+### Kerberos authentication
+
+Kerberos authentication is now (partially) supported, which means you can
+pass the ticket and other stuff. To authenticate via Kerberos:
+
+1. Point the `KRB5CCNAME` environment variable to your cache credential file.
+2. Use the `-k` option in your function call, or the `do_kerberos` in your
+   library call.
+
+```console
+$ klist stormtroopers.ccache
+Ticket cache: FILE:stormtroopers.ccache
+Default principal: stormtroopers@CONTOSO.COM
+
+Valid starting       Expires              Service principal
+10/03/2022 16:46:45  11/03/2022 02:46:45  ldap/srv-ad.contoso.com@CONTOSO.COM
+	renew until 11/03/2022 16:43:17
+$ KRB5CCNAME=stormtroopers.ccache python3 pywerview.py get-netcomputer -t srv-ad.contoso.com -u stormtroopers -k 
+dnshostname: centos.contoso.com 
+
+dnshostname: debian.contoso.com 
+
+dnshostname: Windows7.contoso.com 
+
+dnshostname: Windows10.contoso.com 
+
+dnshostname: SRV-MAIL.contoso.com 
+
+dnshostname: SRV-AD.contoso.com 
+```
+
+If your cache credential file contains a corresponding TGS, or a TGT for your
+calling user, Kerberos authentication will be used.
+
+__SPN patching is partial__. Right now, we're in a mixed configuration where we
+use `ldap3` for LDAP commands and `impacket` for the other protocols (SMB,
+RPC). That is because `impacket`'s LDAP implementation has several problems,
+such as mismanagement of non-ASCII characters (which is problematic for us
+baguette-eaters).
+
+`ldap3` uses `gssapi` to authenticate with Kerberos, and `gssapi` needs the
+full hostname in the SPN of a ticket, otherwise it throws an error. It would
+be possible to patch an SPN with an incomplete hostname, however it's not done
+for now.
+
+For any functions that only rely on `impacket` (SMB or RPC functions), you can
+use tickets with SPNs with an incomplete hostname. In the following example, we
+use an LDAP ticket with an incomplete hostname for an SMB function, without any
+trouble. You just have to make sure that the `--computername` argument matches
+this incomplete hostname in the SPN:
+
+```console
+$ klist skywalker.ccache
+Ticket cache: FILE:skywalker.ccache
+Default principal: skywalker@CONTOSO.COM
+
+Valid starting       Expires              Service principal
+13/04/2022 14:26:59  14/04/2022 00:26:58  ldap/srv-ad@CONTOSO.COM
+	renew until 14/04/2022 14:23:29
+$ KRB5CCNAME=skywalker.ccache python3 pywerview.py get-localdisks --computername srv-ad -u skywalker -k  
+disk: A: 
+
+disk: C: 
+
+disk: D:
+```
+
+To recap:
+
+|           SPN in the ticket           | Can be used with LDAP functions | Can be used with SMB/RPC functions |
+| :-----------------------------------: | :-----------------------------: | :--------------------------------: |
+| `ldap/srv-ad.contoso.com@CONTOSO.COM` |              ✔️                  |                 ✔️                  |
+| `cifs/srv-ad.contoso.com@CONTOSO.COm` |              ✔️                  |                 ✔️                  |
+|       `ldap/srv-ad@CONTOSO.COM`       |              ❌                 |                 ✔️                  |
+
+### TLS CONNECTION
+
+You can force a connection to the LDAPS port by using the `--tls` switch. It
+can be necessary with some functions, for example when retrieving gMSA
+passwords with `get-adserviceaccount`:
+
+```console
+$ python3 pywerview.py get-adserviceaccount -t srv-ad.contoso.com -u 'SRV-MAIL$' --hashes $NT_HASH --resolve-sids
+distinguishedname:       CN=gMSA-01,CN=Managed Service Accounts,DC=contoso,DC=com
+objectsid:               S-1-5-21-863927164-4106933278-53377030-3115
+samaccountname:          gMSA-01$
+msds-groupmsamembership: CN=SRV-MAIL,CN=Computers,DC=contoso,DC=com
+description:
+enabled:                 True
+$ python3 pywerview.py get-adserviceaccount -t srv-ad.contoso.com -u 'SRV-MAIL$' --hashes $NT_HASH --resolve-sids --tls
+distinguishedname:       CN=gMSA-01,CN=Managed Service Accounts,DC=contoso,DC=com
+objectsid:               S-1-5-21-863927164-4106933278-53377030-3115
+samaccountname:          gMSA-01$
+msds-managedpassword:    69730ce3914ac6[redacted]
+msds-groupmsamembership: CN=SRV-MAIL,CN=Computers,DC=contoso,DC=com
+description:
+enabled:                 True
+```
+
+### JSON OUTPUT
+
+Pywerview can print results in json format by using the `--json` switch.
+
 ## TODO
 
 * Many, many more PowerView functionalities to implement. I'll now focus on
   forest functions, then inter-forest trust functions
 * Lots of rewrite due to the last version of PowerView
-* Implement a debugging mode (for easier troubleshooting)
 * Gracefully fail against Unix machines running Samba
-* Support Kerberos authentication
 * Perform range cycling in `get-netgroupmember`
 * Manage request to the Global Catalog
 * Try to fall back to `tcp/139` for RPC communications if `tcp/445` is closed
@@ -186,7 +279,7 @@ an argument, and __not__ `USELESSDOMAIN`.
 
 PywerView - A Python rewriting of PowerSploit's PowerView
 
-Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021
+Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2022
 
 This program is free software: you can redistribute it and/or modify it
 under the terms of the GNU General Public License as published by the
diff --git a/debian/changelog b/debian/changelog
index ebcdbf3..81777f1 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+pywerview (0.4.0-0kali1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Kali Janitor <janitor@kali.org>  Thu, 17 Nov 2022 20:52:29 -0000
+
 pywerview (0.3.2-0kali1) kali-dev; urgency=medium
 
   [ Ben Wilson ]
diff --git a/pywerview.py b/pywerview.py
index d93e5a8..6d7be3a 100755
--- a/pywerview.py
+++ b/pywerview.py
@@ -15,7 +15,7 @@
 # You should have received a copy of the GNU General Public License
 # along with PywerView.  If not, see <http://www.gnu.org/licenses/>.
 
-# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021
+# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2022
 
 from pywerview.cli.main import main
 
diff --git a/pywerview/__init__.py b/pywerview/__init__.py
index e69de29..c812cf2 100644
--- a/pywerview/__init__.py
+++ b/pywerview/__init__.py
@@ -0,0 +1,3 @@
+import logging
+logging.getLogger().setLevel(100)
+
diff --git a/pywerview/cli/helpers.py b/pywerview/cli/helpers.py
index 911942a..7520b7b 100644
--- a/pywerview/cli/helpers.py
+++ b/pywerview/cli/helpers.py
@@ -15,7 +15,7 @@
 # You should have received a copy of the GNU General Public License
 # along with PywerView.  If not, see <http://www.gnu.org/licenses/>.
 
-# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021
+# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2022
 
 from pywerview.functions.net import NetRequester
 from pywerview.functions.gpo import GPORequester
@@ -23,23 +23,50 @@ from pywerview.functions.misc import Misc
 from pywerview.functions.hunting import UserHunter, ProcessHunter, EventHunter
 
 def get_adobject(domain_controller, domain, user, password=str(),
-                lmhash=str(), nthash=str(), queried_domain=str(), queried_sid=str(),
-                queried_name=str(), queried_sam_account_name=str(), ads_path=str(),
+                lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False,
+                queried_domain=str(), queried_sid=str(), queried_name=str(),
+                queried_sam_account_name=str(), ads_path=str(), attributes=list(),
                 custom_filter=str()):
     requester = NetRequester(domain_controller, domain, user, password,
-                                 lmhash, nthash)
+                                 lmhash, nthash, do_kerberos, do_tls)
     return requester.get_adobject(queried_domain=queried_domain,
                     queried_sid=queried_sid, queried_name=queried_name,
                     queried_sam_account_name=queried_sam_account_name,
-                    ads_path=ads_path, custom_filter=custom_filter)
+                    ads_path=ads_path, attributes=attributes, custom_filter=custom_filter)
+
+def get_adserviceaccount(domain_controller, domain, user, password=str(),
+                lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False,
+                queried_domain=str(), queried_sid=str(), queried_name=str(),
+                queried_sam_account_name=str(), ads_path=str(), resolve_sids=False):
+    requester = NetRequester(domain_controller, domain, user, password,
+                                 lmhash, nthash, do_kerberos, do_tls)
+    return requester.get_adserviceaccount(queried_domain=queried_domain,
+                    queried_sid=queried_sid, queried_name=queried_name,
+                    queried_sam_account_name=queried_sam_account_name,
+                    ads_path=ads_path, resolve_sids=resolve_sids)
+
+def get_objectacl(domain_controller, domain, user, password=str(),
+                lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False,
+                queried_domain=str(), queried_sid=str(), queried_name=str(),
+                queried_sam_account_name=str(), ads_path=str(), sacl=False,
+                rights_filter=str(), resolve_sids=False, resolve_guids=False,
+                custom_filter=str()):
+    requester = NetRequester(domain_controller, domain, user, password,
+                                 lmhash, nthash, do_kerberos, do_tls)
+    return requester.get_objectacl(queried_domain=queried_domain,
+                    queried_sid=queried_sid, queried_name=queried_name,
+                    queried_sam_account_name=queried_sam_account_name,
+                    ads_path=ads_path, sacl=sacl, rights_filter=rights_filter,
+                    resolve_sids=resolve_sids, resolve_guids=resolve_guids,
+                    custom_filter=custom_filter)
 
 def get_netuser(domain_controller, domain, user, password=str(), lmhash=str(),
-                nthash=str(), queried_username=str(), queried_domain=str(), ads_path=str(),
-                admin_count=False, spn=False, unconstrained=False, allow_delegation=False,
-                preauth_notreq=False, custom_filter=str(),
-                attributes=[]):
+                nthash=str(), do_kerberos=False, do_tls=False, queried_username=str(),
+                queried_domain=str(), ads_path=str(), admin_count=False, spn=False,
+                unconstrained=False, allow_delegation=False, preauth_notreq=False,
+                custom_filter=str(), attributes=[]):
     requester = NetRequester(domain_controller, domain, user, password,
-                             lmhash, nthash)
+                             lmhash, nthash, do_kerberos, do_tls)
     return requester.get_netuser(queried_username=queried_username,
                                     queried_domain=queried_domain, ads_path=ads_path, admin_count=admin_count,
                                     spn=spn, unconstrained=unconstrained, allow_delegation=allow_delegation,
@@ -47,23 +74,25 @@ def get_netuser(domain_controller, domain, user, password=str(), lmhash=str(),
                                     attributes=attributes)
 
 def get_netgroup(domain_controller, domain, user, password=str(),
-                lmhash=str(), nthash=str(), queried_groupname='*', queried_sid=str(),
-                queried_username=str(), queried_domain=str(), ads_path=str(),
-                admin_count=False, full_data=False, custom_filter=str()):
+                lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False,
+                queried_groupname='*', queried_sid=str(), queried_username=str(),
+                queried_domain=str(), ads_path=str(), admin_count=False,
+                full_data=False, custom_filter=str()):
     requester = NetRequester(domain_controller, domain, user, password,
-                                lmhash, nthash)
+                                lmhash, nthash, do_kerberos, do_tls)
     return requester.get_netgroup(queried_groupname=queried_groupname,
                                     queried_sid=queried_sid, queried_username=queried_username,
                                     queried_domain=queried_domain, ads_path=ads_path, admin_count=admin_count,
                                     full_data=full_data, custom_filter=custom_filter)
 
 def get_netcomputer(domain_controller, domain, user, password=str(),
-                    lmhash=str(), nthash=str(), queried_computername='*', queried_spn=str(),
-                    queried_os=str(), queried_sp=str(), queried_domain=str(), ads_path=str(),
+                    lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False,
+                    queried_computername='*', queried_spn=str(), queried_os=str(),
+                    queried_sp=str(), queried_domain=str(), ads_path=str(),
                     printers=False, unconstrained=False, ping=False, full_data=False,
                     custom_filter=str(), attributes=[]):
     requester = NetRequester(domain_controller, domain, user, password,
-                                 lmhash, nthash)
+                                 lmhash, nthash, do_kerberos, do_tls)
     return requester.get_netcomputer(queried_computername=queried_computername,
                                         queried_spn=queried_spn, queried_os=queried_os, queried_sp=queried_sp,
                                         queried_domain=queried_domain, ads_path=ads_path, printers=printers,
@@ -71,65 +100,69 @@ def get_netcomputer(domain_controller, domain, user, password=str(),
                                         custom_filter=custom_filter, attributes=attributes)
 
 def get_netdomaincontroller(domain_controller, domain, user, password=str(),
-                                 lmhash=str(), nthash=str(), queried_domain=str()):
+                                 lmhash=str(), nthash=str(), do_kerberos=False,
+                                 do_tls=False, queried_domain=str()):
     requester = NetRequester(domain_controller, domain, user, password,
-                                 lmhash, nthash)
+                                 lmhash, nthash, do_kerberos, do_tls)
     return requester.get_netdomaincontroller(queried_domain=queried_domain)
 
 def get_netfileserver(domain_controller, domain, user, password=str(),
-                                 lmhash=str(), nthash=str(), queried_domain=str(), target_users=list()):
+                                 lmhash=str(), nthash=str(), do_kerberos=False,
+                                 do_tls=False, queried_domain=str(), target_users=list()):
     requester = NetRequester(domain_controller, domain, user, password,
-                                 lmhash, nthash)
+                                 lmhash, nthash, do_kerberos, do_tls)
     return requester.get_netfileserver(queried_domain=queried_domain,
                                             target_users=target_users)
 
 def get_dfsshare(domain_controller, domain, user, password=str(),
-                 lmhash=str(), nthash=str(), version=['v1', 'v2'], queried_domain=str(),
-                 ads_path=str()):
+                 lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False,
+                 version=['v1', 'v2'], queried_domain=str(), ads_path=str()):
     requester = NetRequester(domain_controller, domain, user, password,
-                                 lmhash, nthash)
+                                 lmhash, nthash, do_kerberos, do_tls)
     return requester.get_dfsshare(version=version, queried_domain=queried_domain, ads_path=ads_path)
 
 def get_netou(domain_controller, domain, user, password=str(), lmhash=str(),
-              nthash=str(), queried_domain=str(), queried_ouname='*', queried_guid=str(),
-              ads_path=str(), full_data=False):
+              nthash=str(), do_kerberos=False, do_tls=False, queried_domain=str(),
+              queried_ouname='*', queried_guid=str(), ads_path=str(), full_data=False):
     requester = NetRequester(domain_controller, domain, user, password,
-                                 lmhash, nthash)
+                                 lmhash, nthash, do_kerberos, do_tls)
     return requester.get_netou(queried_domain=queried_domain,
                                    queried_ouname=queried_ouname, queried_guid=queried_guid, ads_path=ads_path,
                                    full_data=full_data)
 
 def get_netsite(domain_controller, domain, user, password=str(), lmhash=str(),
-                nthash=str(), queried_domain=str(), queried_sitename=str(),
-                queried_guid=str(), ads_path=str(), ads_prefix='CN=Sites,CN=Configuration',
-                full_data=False):
+                nthash=str(), do_kerberos=False, do_tls=False, queried_domain=str(),
+                queried_sitename=str(), queried_guid=str(), ads_path=str(),
+                ads_prefix='CN=Sites,CN=Configuration', full_data=False):
     requester = NetRequester(domain_controller, domain, user, password,
-                                 lmhash, nthash)
+                                 lmhash, nthash, do_kerberos, do_tls)
     return requester.get_netsite(queried_domain=queried_domain,
                                     queried_sitename=queried_sitename, queried_guid=queried_guid,
                                     ads_path=ads_path, ads_prefix=ads_prefix, full_data=full_data)
 
 def get_netsubnet(domain_controller, domain, user, password=str(),
-                  lmhash=str(), nthash=str(), queried_domain=str(), queried_sitename=str(),
-                  ads_path=str(), ads_prefix='CN=Sites,CN=Configuration', full_data=False):
+                  lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False,
+                  queried_domain=str(), queried_sitename=str(), ads_path=str(),
+                  ads_prefix='CN=Sites,CN=Configuration', full_data=False):
     requester = NetRequester(domain_controller, domain, user, password,
-                                 lmhash, nthash)
+                                 lmhash, nthash, do_kerberos, do_tls)
     return requester.get_netsubnet(queried_domain=queried_domain,
                                        queried_sitename=queried_sitename, ads_path=ads_path, ads_prefix=ads_prefix,
                                        full_data=full_data)
 
 def get_netdomaintrust(domain_controller, domain, user, password=str(),
-                  lmhash=str(), nthash=str(), queried_domain=str()):
+                  lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False, queried_domain=str()):
     requester = NetRequester(domain_controller, domain, user, password,
-                                 lmhash, nthash)
+                                 lmhash, nthash, do_kerberos, do_tls)
     return requester.get_netdomaintrust(queried_domain=queried_domain)
 
 def get_netgroupmember(domain_controller, domain, user, password=str(),
-                       lmhash=str(), nthash=str(), queried_groupname=str(), queried_sid=str(),
-                       queried_domain=str(), ads_path=str(), recurse=False, use_matching_rule=False,
+                       lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False,
+                       queried_groupname=str(), queried_sid=str(), queried_domain=str(),
+                       ads_path=str(), recurse=False, use_matching_rule=False,
                        full_data=False, custom_filter=str()):
     requester = NetRequester(domain_controller, domain, user, password,
-                                 lmhash, nthash)
+                                 lmhash, nthash, do_kerberos, do_tls)
     return requester.get_netgroupmember(queried_groupname=queried_groupname,
                                             queried_sid=queried_sid, queried_domain=queried_domain,
                                             ads_path=ads_path, recurse=recurse,
@@ -137,87 +170,101 @@ def get_netgroupmember(domain_controller, domain, user, password=str(),
                                             full_data=full_data, custom_filter=custom_filter)
 
 def get_netsession(target_computername, domain, user, password=str(),
-                   lmhash=str(), nthash=str()):
+                   lmhash=str(), nthash=str(), do_kerberos=False):
     requester = NetRequester(target_computername, domain, user, password,
-                                 lmhash, nthash)
+                                 lmhash, nthash, do_kerberos)
     return requester.get_netsession()
 
 def get_netshare(target_computername, domain, user, password=str(),
-                                 lmhash=str(), nthash=str()):
+                                 lmhash=str(), nthash=str(), do_kerberos=False):
     requester = NetRequester(target_computername, domain, user, password,
-                                 lmhash, nthash)
+                                 lmhash, nthash, do_kerberos)
     return requester.get_netshare()
 
 def get_localdisks(target_computername, domain, user, password=str(),
-                                 lmhash=str(), nthash=str()):
+                                 lmhash=str(), nthash=str(), do_kerberos=False):
     requester = NetRequester(target_computername, domain, user, password,
-                                 lmhash, nthash)
+                                 lmhash, nthash, do_kerberos)
     return requester.get_localdisks()
 
 def get_netdomain(domain_controller, domain, user, password=str(),
-                                 lmhash=str(), nthash=str()):
+                                 lmhash=str(), nthash=str(), do_kerberos=False,
+                                 do_tls=False):
     requester = NetRequester(domain_controller, domain, user, password,
-                                 lmhash, nthash)
+                                 lmhash, nthash, do_kerberos, do_tls)
     return requester.get_netdomain()
 
 def get_netloggedon(target_computername, domain, user, password=str(),
-                                 lmhash=str(), nthash=str()):
+                                 lmhash=str(), nthash=str(), do_kerberos=False):
     requester = NetRequester(target_computername, domain, user, password,
-                                 lmhash, nthash)
+                                 lmhash, nthash, do_kerberos)
     return requester.get_netloggedon()
 
 def get_netlocalgroup(target_computername, domain_controller, domain, user,
-                      password=str(), lmhash=str(), nthash=str(), queried_groupname=str(),
-                      list_groups=False, recurse=False):
+                      password=str(), lmhash=str(), nthash=str(), do_kerberos=False,
+                      do_tls=False, queried_groupname=str(), list_groups=False,
+                      recurse=False):
     requester = NetRequester(target_computername, domain, user, password,
-                                 lmhash, nthash, domain_controller)
+                                 lmhash, nthash, do_kerberos, do_tls, domain_controller)
     return requester.get_netlocalgroup(queried_groupname=queried_groupname,
                                            list_groups=list_groups, recurse=recurse)
 
 def get_netprocess(target_computername, domain, user, password=str(),
-                   lmhash=str(), nthash=str()):
+                   lmhash=str(), nthash=str(), do_kerberos=False):
     requester = NetRequester(target_computername, domain, user, password,
-                                 lmhash, nthash)
+                                 lmhash, nthash, do_kerberos)
     return requester.get_netprocess()
 
 def get_userevent(target_computername, domain, user, password=str(),
-                   lmhash=str(), nthash=str(), event_type=['logon', 'tgt'],
-                   date_start=5):
+                   lmhash=str(), nthash=str(), do_kerberos=False,
+                   event_type=['logon', 'tgt'], date_start=5):
     requester = NetRequester(target_computername, domain, user, password,
-                                 lmhash, nthash)
+                                 lmhash, nthash, do_kerberos)
     return requester.get_userevent(event_type=event_type,
                                        date_start=date_start)
 
 def get_netgpo(domain_controller, domain, user, password=str(),
-               lmhash=str(), nthash=str(), queried_gponame='*',
-               queried_displayname=str(), queried_domain=str(), ads_path=str()):
+               lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False,
+               queried_gponame='*', queried_displayname=str(), queried_domain=str(),
+               ads_path=str()):
     requester = GPORequester(domain_controller, domain, user, password,
-                                 lmhash, nthash)
+                                 lmhash, nthash, do_kerberos, do_tls)
     return requester.get_netgpo(queried_gponame=queried_gponame,
                                     queried_displayname=queried_displayname,
                                     queried_domain=queried_domain, ads_path=ads_path)
 
+def get_netpso(domain_controller, domain, user, password=str(),
+               lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False,
+               queried_psoname='*', queried_displayname=str(), queried_domain=str(),
+               ads_path=str()):
+    requester = GPORequester(domain_controller, domain, user, password,
+                                 lmhash, nthash, do_kerberos, do_tls)
+    return requester.get_netpso(queried_psoname=queried_psoname,
+                                    queried_displayname=queried_displayname,
+                                    queried_domain=queried_domain, ads_path=ads_path)
+
 def get_domainpolicy(domain_controller, domain, user, password=str(),
-                     lmhash=str(), nthash=str(), source='domain', queried_domain=str(),
-                     resolve_sids=False):
+                     lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False,
+                     source='domain', queried_domain=str(), resolve_sids=False):
     requester = GPORequester(domain_controller, domain, user, password,
-                                 lmhash, nthash)
+                                 lmhash, nthash, do_kerberos, do_tls)
 
     return requester.get_domainpolicy(source=source, queried_domain=queried_domain,
                                           resolve_sids=resolve_sids)
 
 def get_gpttmpl(gpttmpl_path, domain_controller, domain, user, password=str(), lmhash=str(),
-                nthash=str()):
+                nthash=str(), do_kerberos=False, do_tls=False):
     requester = GPORequester(domain_controller, domain, user, password,
-                                 lmhash, nthash)
+                                 lmhash, nthash, do_kerberos, do_tls)
 
     return requester.get_gpttmpl(gpttmpl_path)
 
 def get_netgpogroup(domain_controller, domain, user, password=str(), lmhash=str(),
-                    nthash=str(), queried_gponame='*', queried_displayname=str(),
-                    queried_domain=str(), ads_path=str(), resolve_sids=False):
+                    nthash=str(), do_kerberos=False, do_tls=False, queried_gponame='*',
+                    queried_displayname=str(), queried_domain=str(), ads_path=str(),
+                    resolve_sids=False):
     requester = GPORequester(domain_controller, domain, user, password,
-                                 lmhash, nthash)
+                                 lmhash, nthash, do_kerberos, do_tls)
 
     return requester.get_netgpogroup(queried_gponame=queried_gponame,
                                          queried_displayname=queried_displayname,
@@ -226,11 +273,10 @@ def get_netgpogroup(domain_controller, domain, user, password=str(), lmhash=str(
                                          resolve_sids=resolve_sids)
 
 def find_gpocomputeradmin(domain_controller, domain, user, password=str(), lmhash=str(),
-                          nthash=str(), queried_computername=str(),
-                          queried_ouname=str(), queried_domain=str(),
-                          recurse=False):
+                          nthash=str(), do_kerberos=False, do_tls=False, queried_computername=str(),
+                          queried_ouname=str(), queried_domain=str(), recurse=False):
     requester = GPORequester(domain_controller, domain, user, password,
-                                 lmhash, nthash)
+                                 lmhash, nthash, do_kerberos, do_tls)
 
     return requester.find_gpocomputeradmin(queried_computername=queried_computername,
                                                queried_ouname=queried_ouname,
@@ -238,26 +284,27 @@ def find_gpocomputeradmin(domain_controller, domain, user, password=str(), lmhas
                                                recurse=recurse)
 
 def find_gpolocation(domain_controller, domain, user, password=str(), lmhash=str(),
-                     nthash=str(), queried_username=str(), queried_groupname=str(),
-                     queried_localgroup=str(), queried_domain=str()):
+                     nthash=str(), do_kerberos=False, do_tls=False, queried_username=str(),
+                     queried_groupname=str(), queried_localgroup=str(),
+                     queried_domain=str()):
     requester = GPORequester(domain_controller, domain, user, password,
-                                 lmhash, nthash)
+                                 lmhash, nthash, do_kerberos, do_tls)
     return requester.find_gpolocation(queried_username=queried_username,
                                           queried_groupname=queried_groupname,
                                           queried_localgroup=queried_localgroup,
                                           queried_domain=queried_domain)
 
 def invoke_checklocaladminaccess(target_computername, domain, user, password=str(),
-                                 lmhash=str(), nthash=str()):
-    misc = Misc(target_computername, domain, user, password, lmhash, nthash)
+                                 lmhash=str(), nthash=str(), do_kerberos=False):
+    misc = Misc(target_computername, domain, user, password, lmhash, nthash, do_kerberos)
 
     return misc.invoke_checklocaladminaccess()
 
 def invoke_userhunter(domain_controller, domain, user, password=str(),
-                      lmhash=str(), nthash=str(), queried_computername=list(),
-                      queried_computerfile=None, queried_computerfilter=str(),
-                      queried_computeradspath=str(), unconstrained=False,
-                      queried_groupname=str(), target_server=str(),
+                      lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False,
+                      queried_computername=list(), queried_computerfile=None,
+                      queried_computerfilter=str(), queried_computeradspath=str(),
+                      unconstrained=False, queried_groupname=str(), target_server=str(),
                       queried_username=str(), queried_useradspath=str(),
                       queried_userfilter=str(), queried_userfile=None,
                       threads=1, admin_count=False, allow_delegation=False,
@@ -265,8 +312,7 @@ def invoke_userhunter(domain_controller, domain, user, password=str(),
                       stealth=False, stealth_source=['dfs', 'dc', 'file'],
                       show_all=False, foreign_users=False):
     user_hunter = UserHunter(domain_controller, domain, user, password,
-                             lmhash, nthash)
-    
+                             lmhash, nthash, do_kerberos, do_tls)
     return user_hunter.invoke_userhunter(queried_computername=queried_computername,
                                          queried_computerfile=queried_computerfile,
                                          queried_computerfilter=queried_computerfilter,
@@ -282,15 +328,15 @@ def invoke_userhunter(domain_controller, domain, user, password=str(),
                                          foreign_users=foreign_users)
 
 def invoke_processhunter(domain_controller, domain, user, password=str(),
-                         lmhash=str(), nthash=str(), queried_computername=list(),
-                         queried_computerfile=None, queried_computerfilter=str(),
-                         queried_computeradspath=str(), queried_processname=list(),
-                         queried_groupname=str(), target_server=str(),
+                         lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False,
+                         queried_computername=list(), queried_computerfile=None,
+                         queried_computerfilter=str(), queried_computeradspath=str(),
+                         queried_processname=list(), queried_groupname=str(), target_server=str(),
                          queried_username=str(), queried_useradspath=str(),
                          queried_userfilter=str(), queried_userfile=None, threads=1,
                          stop_on_success=False, queried_domain=str(), show_all=False):
     process_hunter = ProcessHunter(domain_controller, domain, user, password,
-                                   lmhash, nthash)
+                                   lmhash, nthash, do_kerberos, do_tls)
 
     return process_hunter.invoke_processhunter(queried_computername=queried_computername,
                                                queried_computerfile=queried_computerfile,
@@ -305,15 +351,15 @@ def invoke_processhunter(domain_controller, domain, user, password=str(),
                                                queried_domain=queried_domain, show_all=show_all)
 
 def invoke_eventhunter(domain_controller, domain, user, password=str(),
-                       lmhash=str(), nthash=str(), queried_computername=list(),
-                       queried_computerfile=None, queried_computerfilter=str(),
-                       queried_computeradspath=str(), queried_groupname=str(),
-                       target_server=str(), queried_username=str(),
+                       lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False,
+                       queried_computername=list(), queried_computerfile=None,
+                       queried_computerfilter=str(), queried_computeradspath=str(),
+                       queried_groupname=str(), target_server=str(), queried_username=str(),
                        queried_useradspath=str(), queried_userfilter=str(),
                        queried_userfile=None, threads=1, queried_domain=str(),
                        search_days=3):
     event_hunter = EventHunter(domain_controller, domain, user, password,
-                                   lmhash, nthash)
+                                   lmhash, nthash, do_kerberos, do_tls)
 
     return event_hunter.invoke_eventhunter(queried_computername=queried_computername,
                                            queried_computerfile=queried_computerfile,
diff --git a/pywerview/cli/main.py b/pywerview/cli/main.py
index 0abe72a..df9260b 100644
--- a/pywerview/cli/main.py
+++ b/pywerview/cli/main.py
@@ -15,9 +15,12 @@
 # You should have received a copy of the GNU General Public License
 # along with PywerView.  If not, see <http://www.gnu.org/licenses/>.
 
-# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021
+# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2022
 
+import logging
 import argparse
+import json
+import datetime
 from pywerview.cli.helpers import *
 from pywerview.functions.hunting import *
 
@@ -25,26 +28,48 @@ def main():
     # Main parser
     parser = argparse.ArgumentParser(description='Rewriting of some PowerView\'s functionalities in Python')
     subparsers = parser.add_subparsers(title='Subcommands', description='Available subcommands', dest='submodule')
-    
+
     # hack for python < 3.9 : https://stackoverflow.com/questions/23349349/argparse-with-required-subparser
     subparsers.required = True
 
+    # Logging parser
+    logging_parser = argparse.ArgumentParser(add_help=False)
+    logging_parser.add_argument('-l', '--logging-level', dest='logging_level', type=str.upper,
+            choices=['CRITICAL', 'WARNING', 'DEBUG', 'ULTRA'], default='CRITICAL',
+            help='SDTERR logging level: '
+                 'CRITICAL: Only critical errors are displayed (default), '
+                 'WARNING: Warnings are displayed, along with citical errors, '
+                 'DEBUG: Debug level (caution: very verbose), '
+                 'ULTRA: Extreme debugging level (caution: very very verbose)')
+    
+    # json parser
+    json_output_parser = argparse.ArgumentParser(add_help=False)
+    json_output_parser.add_argument('--json', dest='json_output', action='store_true',
+            help='Print results in JSON format')
+
     # TODO: support keberos authentication
     # Credentials parser
     credentials_parser = argparse.ArgumentParser(add_help=False)
     credentials_parser.add_argument('-w', '--workgroup', dest='domain',
             default=str(), help='Name of the domain we authenticate with')
-    credentials_parser.add_argument('-u', '--user', required=True,
+    credentials_parser.add_argument('-u', '--user',
             help='Username used to connect to the Domain Controller')
     credentials_parser.add_argument('-p', '--password',
             help='Password associated to the username')
-    credentials_parser.add_argument('--hashes', action='store', metavar = 'LMHASH:NTHASH',
+    credentials_parser.add_argument('--hashes', action='store', metavar='LMHASH:NTHASH',
             help='NTLM hashes, format is [LMHASH:]NTHASH')
+    credentials_parser.add_argument('-k', action='store_true', dest='do_kerberos',
+            help='Use Kerberos authentication. Grabs credentials from ccache file '
+            '(KRB5CCNAME) based on target parameters. If valid credentials '
+            'cannot be found, it will use the ones specified in the command '
+            'line')
 
     # AD parser, used for net* functions running against a domain controller
     ad_parser = argparse.ArgumentParser(add_help=False, parents=[credentials_parser])
     ad_parser.add_argument('-t', '--dc-ip', dest='domain_controller',
             required=True, help='IP address of the Domain Controller to target')
+    ad_parser.add_argument('--tls', action='store_true', dest='do_tls',
+            help='Force TLS connection to the Domain Controller')
 
     # Target parser, used for net* functions running against a normal computer
     target_parser = argparse.ArgumentParser(add_help=False, parents=[credentials_parser])
@@ -80,7 +105,8 @@ def main():
 
     # Parser for the get-adobject command
     get_adobject_parser = subparsers.add_parser('get-adobject', help='Takes a domain SID, '\
-        'samAccountName or name, and return the associated object', parents=[ad_parser])
+        'samAccountName or name, and return the associated object',
+        parents=[ad_parser, logging_parser, json_output_parser])
     get_adobject_parser.add_argument('--sid', dest='queried_sid',
             help='SID to query (wildcards accepted)')
     get_adobject_parser.add_argument('--sam-account-name', dest='queried_sam_account_name',
@@ -91,11 +117,58 @@ def main():
             help='Domain to query')
     get_adobject_parser.add_argument('-a', '--ads-path',
             help='Additional ADS path')
+    get_adobject_parser.add_argument('--attributes', nargs='+', dest='attributes',
+            default=[], help='Object attributes to return')
     get_adobject_parser.set_defaults(func=get_adobject)
 
+    # Parser for the get-adserviceaccount command
+    get_adserviceaccount_parser = subparsers.add_parser('get-adserviceaccount', help='Returns a list of all the '\
+        'gMSA of the specified domain. To retrieve passwords, you need a privileged account and '\
+        'a TLS connection to the LDAP server (use the --tls switch).',
+        parents=[ad_parser, logging_parser, json_output_parser])
+    get_adserviceaccount_parser.add_argument('--sid', dest='queried_sid',
+            help='SID to query (wildcards accepted)')
+    get_adserviceaccount_parser.add_argument('--sam-account-name', dest='queried_sam_account_name',
+            help='samAccountName to query (wildcards accepted)')
+    get_adserviceaccount_parser.add_argument('--name', dest='queried_name',
+            help='Name to query (wildcards accepted)')
+    get_adserviceaccount_parser.add_argument('-d', '--domain', dest='queried_domain',
+            help='Domain to query')
+    get_adserviceaccount_parser.add_argument('-a', '--ads-path',
+            help='Additional ADS path')
+    get_adserviceaccount_parser.add_argument('--resolve-sids', dest='resolve_sids',
+            action='store_true', help='Resolve SIDs when querying PrincipalsAllowedToRetrieveManagedPassword')
+    get_adserviceaccount_parser.set_defaults(func=get_adserviceaccount)
+    
+    # Parser for the get-objectacl command
+    get_objectacl_parser = subparsers.add_parser('get-objectacl', help='Takes a domain SID, '\
+        'samAccountName or name, and return the ACL of the associated object',
+        parents=[ad_parser, logging_parser, json_output_parser])
+    get_objectacl_parser.add_argument('--sid', dest='queried_sid',
+            help='SID to query (wildcards accepted)')
+    get_objectacl_parser.add_argument('--sam-account-name', dest='queried_sam_account_name',
+            help='samAccountName to query (wildcards accepted)')
+    get_objectacl_parser.add_argument('--name', dest='queried_name',
+            help='Name to query (wildcards accepted)')
+    get_objectacl_parser.add_argument('-d', '--domain', dest='queried_domain',
+            help='Domain to query')
+    get_objectacl_parser.add_argument('-a', '--ads-path',
+            help='Additional ADS path')
+    get_objectacl_parser.add_argument('--sacl', action='store_true',
+            help='Return the SACL instead of the DACL for the object (requires '\
+            'a privileged account)')
+    get_objectacl_parser.add_argument('--rights-filter', dest='rights_filter',
+            choices=['reset-password', 'write-members', 'all'], help='A specific set of rights to return '\
+                    '(reset-password, write-members, all)')
+    get_objectacl_parser.add_argument('--resolve-sids', dest='resolve_sids',
+            action='store_true', help='Resolve SIDs when querying an ACL')
+    get_objectacl_parser.add_argument('--resolve-guids', action='store_true',
+            help='Resolve GUIDs to their display names')
+    get_objectacl_parser.set_defaults(func=get_objectacl)
+
     # Parser for the get-netuser command
     get_netuser_parser = subparsers.add_parser('get-netuser', help='Queries information about '\
-        'a domain user', parents=[ad_parser])
+        'a domain user', parents=[ad_parser, logging_parser, json_output_parser])
     get_netuser_parser.add_argument('--username', dest='queried_username',
             help='Username to query (wildcards accepted)')
     get_netuser_parser.add_argument('-d', '--domain', dest='queried_domain',
@@ -120,7 +193,8 @@ def main():
 
     # Parser for the get-netgroup command
     get_netgroup_parser = subparsers.add_parser('get-netgroup', help='Get a list of all current '\
-        'domain groups, or a list of groups a domain user is member of', parents=[ad_parser])
+        'domain groups, or a list of groups a domain user is member of',
+        parents=[ad_parser, logging_parser, json_output_parser])
     get_netgroup_parser.add_argument('--groupname', dest='queried_groupname',
             default='*', help='Group to query (wildcards accepted)')
     get_netgroup_parser.add_argument('--sid', dest='queried_sid',
@@ -139,7 +213,7 @@ def main():
 
     # Parser for the get-netcomputer command
     get_netcomputer_parser = subparsers.add_parser('get-netcomputer', help='Queries informations about '\
-        'domain computers', parents=[ad_parser])
+        'domain computers', parents=[ad_parser, logging_parser, json_output_parser])
     get_netcomputer_parser.add_argument('--computername', dest='queried_computername',
             default='*', help='Computer name to query')
     get_netcomputer_parser.add_argument('-os', '--operating-system', dest='queried_os',
@@ -166,14 +240,15 @@ def main():
 
     # Parser for the get-netdomaincontroller command
     get_netdomaincontroller_parser = subparsers.add_parser('get-netdomaincontroller', help='Get a list of '\
-        'domain controllers for the given domain', parents=[ad_parser])
+        'domain controllers for the given domain', parents=[ad_parser, logging_parser, json_output_parser])
     get_netdomaincontroller_parser.add_argument('-d', '--domain', dest='queried_domain',
             help='Domain to query')
     get_netdomaincontroller_parser.set_defaults(func=get_netdomaincontroller)
 
     # Parser for the get-netfileserver command
     get_netfileserver_parser = subparsers.add_parser('get-netfileserver', help='Return a list of '\
-        'file servers, extracted from the domain users\' homeDirectory, scriptPath, and profilePath fields', parents=[ad_parser])
+        'file servers, extracted from the domain users\' homeDirectory, scriptPath, and profilePath fields',
+        parents=[ad_parser, logging_parser, json_output_parser])
     get_netfileserver_parser.add_argument('--target-users', nargs='+',
             metavar='TARGET_USER', help='A list of users to target to find file servers (wildcards accepted)')
     get_netfileserver_parser.add_argument('-d', '--domain', dest='queried_domain',
@@ -182,7 +257,7 @@ def main():
 
     # Parser for the get-dfsshare command
     get_dfsshare_parser = subparsers.add_parser('get-dfsshare', help='Return a list of '\
-        'all fault tolerant distributed file systems for a given domain', parents=[ad_parser])
+        'all fault tolerant distributed file systems for a given domain', parents=[ad_parser, logging_parser, json_output_parser])
     get_dfsshare_parser.add_argument('-d', '--domain', dest='queried_domain',
             help='Domain to query')
     get_dfsshare_parser.add_argument('-v', '--version', nargs='+', choices=['v1', 'v2'],
@@ -193,7 +268,7 @@ def main():
 
     # Parser for the get-netou command
     get_netou_parser = subparsers.add_parser('get-netou', help='Get a list of all current '\
-        'OUs in the domain', parents=[ad_parser])
+        'OUs in the domain', parents=[ad_parser, logging_parser, json_output_parser])
     get_netou_parser.add_argument('--ouname', dest='queried_ouname',
             default='*', help='OU name to query (wildcards accepted)')
     get_netou_parser.add_argument('--guid', dest='queried_guid',
@@ -208,7 +283,7 @@ def main():
 
     # Parser for the get-netsite command
     get_netsite_parser = subparsers.add_parser('get-netsite', help='Get a list of all current '\
-        'sites in the domain', parents=[ad_parser])
+        'sites in the domain', parents=[ad_parser, logging_parser, json_output_parser])
     get_netsite_parser.add_argument('--sitename', dest='queried_sitename',
             help='Site name to query (wildcards accepted)')
     get_netsite_parser.add_argument('--guid', dest='queried_guid',
@@ -223,7 +298,7 @@ def main():
 
     # Parser for the get-netsubnet command
     get_netsubnet_parser = subparsers.add_parser('get-netsubnet', help='Get a list of all current '\
-        'subnets in the domain', parents=[ad_parser])
+        'subnets in the domain', parents=[ad_parser, logging_parser, json_output_parser])
     get_netsubnet_parser.add_argument('--sitename', dest='queried_sitename',
             help='Only return subnets for the specified site name (wildcards accepted)')
     get_netsubnet_parser.add_argument('-d', '--domain', dest='queried_domain',
@@ -236,14 +311,14 @@ def main():
 
     # Parser for the get-netdomaintrust command
     get_netdomaintrust_parser = subparsers.add_parser('get-netdomaintrust', help='Returns a list of all the '\
-        'trusts of the specified domain', parents=[ad_parser])
+        'trusts of the specified domain', parents=[ad_parser, logging_parser, json_output_parser])
     get_netdomaintrust_parser.add_argument('-d', '--domain', dest='queried_domain',
             help='Domain to query')
     get_netdomaintrust_parser.set_defaults(func=get_netdomaintrust)
 
     # Parser for the get-netgpo command
     get_netgpo_parser = subparsers.add_parser('get-netgpo', help='Get a list of all current '\
-        'GPOs in the domain', parents=[ad_parser])
+        'GPOs in the domain', parents=[ad_parser, logging_parser, json_output_parser])
     get_netgpo_parser.add_argument('--gponame', dest='queried_gponame',
             default='*', help='GPO name to query for (wildcards accepted)')
     get_netgpo_parser.add_argument('--displayname', dest='queried_displayname',
@@ -254,9 +329,22 @@ def main():
             help='Additional ADS path')
     get_netgpo_parser.set_defaults(func=get_netgpo)
 
+    # Parser for the get-netpso command
+    get_netpso_parser = subparsers.add_parser('get-netpso', help='Get a list of all current '\
+        'PSOs in the domain', parents=[ad_parser, logging_parser, json_output_parser])
+    get_netpso_parser.add_argument('--psoname', dest='queried_psoname',
+            default='*', help='pso name to query for (wildcards accepted)')
+    get_netpso_parser.add_argument('--displayname', dest='queried_displayname',
+            help='Display name to query for (wildcards accepted)')
+    get_netpso_parser.add_argument('-d', '--domain', dest='queried_domain',
+            help='Domain to query')
+    get_netpso_parser.add_argument('-a', '--ads-path',
+            help='Additional ADS path')
+    get_netpso_parser.set_defaults(func=get_netpso)
+
     # Parser for the get-domainpolicy command
     get_domainpolicy_parser = subparsers.add_parser('get-domainpolicy', help='Returns the default domain or DC '\
-        'policy for the queried domain or DC', parents=[ad_parser])
+        'policy for the queried domain or DC', parents=[ad_parser, logging_parser, json_output_parser])
     get_domainpolicy_parser.add_argument('--source', dest='source', default='domain',
             choices=['domain', 'dc'], help='Extract domain or DC policy (default: %(default)s)')
     get_domainpolicy_parser.add_argument('-d', '--domain', dest='queried_domain',
@@ -267,14 +355,14 @@ def main():
 
     # Parser for the get-gpttmpl command
     get_gpttmpl_parser = subparsers.add_parser('get-gpttmpl', help='Helper to parse a GptTmpl.inf policy '\
-            'file path into a custom object', parents=[ad_parser])
+            'file path into a custom object', parents=[ad_parser, logging_parser, json_output_parser])
     get_gpttmpl_parser.add_argument('--gpt-tmpl-path', type=str, required=True,
             dest='gpttmpl_path', help='The GptTmpl.inf file path name to parse')
     get_gpttmpl_parser.set_defaults(func=get_gpttmpl)
 
     # Parser for the get-netgpogroup command
     get_netgpogroup_parser = subparsers.add_parser('get-netgpogroup', help='Parses all GPOs in the domain '\
-        'that set "Restricted Group" or "Groups.xml"', parents=[ad_parser])
+        'that set "Restricted Group" or "Groups.xml"', parents=[ad_parser, logging_parser, json_output_parser])
     get_netgpogroup_parser.add_argument('--gponame', dest='queried_gponame',
             default='*', help='GPO name to query for (wildcards accepted)')
     get_netgpogroup_parser.add_argument('--displayname', dest='queried_displayname',
@@ -289,7 +377,7 @@ def main():
 
     # Parser for the find-gpocomputeradmin command
     find_gpocomputeradmin_parser = subparsers.add_parser('find-gpocomputeradmin', help='Takes a computer (or OU) and determine '\
-        'who has administrative access to it via GPO', parents=[ad_parser])
+        'who has administrative access to it via GPO', parents=[ad_parser, logging_parser, json_output_parser])
     find_gpocomputeradmin_parser.add_argument('--computername', dest='queried_computername',
             default=str(), help='The computer to determine who has administrative access to it')
     find_gpocomputeradmin_parser.add_argument('--ouname', dest='queried_ouname',
@@ -303,7 +391,7 @@ def main():
 
     # Parser for the find-gpolocation command
     find_gpolocation_parser = subparsers.add_parser('find-gpolocation', help='Takes a username or a group name and determine '\
-        'the computers it has administrative access to via GPO', parents=[ad_parser])
+        'the computers it has administrative access to via GPO', parents=[ad_parser, logging_parser, json_output_parser])
     find_gpolocation_parser.add_argument('--username', dest='queried_username',
             default=str(), help='The username to query for access (no wildcard)')
     find_gpolocation_parser.add_argument('--groupname', dest='queried_groupname',
@@ -316,7 +404,8 @@ def main():
     find_gpolocation_parser.set_defaults(func=find_gpolocation)
 
     # Parser for the get-netgroup command
-    get_netgroupmember_parser = subparsers.add_parser('get-netgroupmember', help='Return a list of members of a domain group', parents=[ad_parser])
+    get_netgroupmember_parser = subparsers.add_parser('get-netgroupmember', help='Return a list of members of a domain group',
+        parents=[ad_parser, logging_parser, json_output_parser])
     get_netgroupmember_parser.add_argument('--groupname', dest='queried_groupname',
             help='Group to query, defaults to the \'Domain Admins\' group (wildcards accepted)')
     get_netgroupmember_parser.add_argument('--sid', dest='queried_sid',
@@ -336,60 +425,66 @@ def main():
 
     # Parser for the get-netsession command
     get_netsession_parser = subparsers.add_parser('get-netsession', help='Queries a host to return a '\
-        'list of active sessions on the host (you can use local credentials instead of domain credentials)', parents=[target_parser])
+        'list of active sessions on the host (you can use local credentials instead of domain credentials)',
+        parents=[target_parser, logging_parser, json_output_parser])
     get_netsession_parser.set_defaults(func=get_netsession)
 
     #Parser for the get-localdisks command
     get_localdisks_parser = subparsers.add_parser('get-localdisks', help='Queries a host to return a '\
-        'list of active disks on the host (you can use local credentials instead of domain credentials)', parents=[target_parser])
+        'list of active disks on the host (you can use local credentials instead of domain credentials)',
+        parents=[target_parser, logging_parser, json_output_parser])
     get_localdisks_parser.set_defaults(func=get_localdisks)
 
     #Parser for the get-netdomain command
     get_netdomain_parser = subparsers.add_parser('get-netdomain', help='Queries a host for available domains',
-        parents=[ad_parser])
+        parents=[ad_parser, logging_parser, json_output_parser])
     get_netdomain_parser.set_defaults(func=get_netdomain)
 
     # Parser for the get-netshare command
     get_netshare_parser = subparsers.add_parser('get-netshare', help='Queries a host to return a '\
-        'list of available shares on the host (you can use local credentials instead of domain credentials)', parents=[target_parser])
+        'list of available shares on the host (you can use local credentials instead of domain credentials)',
+        parents=[target_parser, logging_parser, json_output_parser])
     get_netshare_parser.set_defaults(func=get_netshare)
 
     # Parser for the get-netloggedon command
     get_netloggedon_parser = subparsers.add_parser('get-netloggedon', help='This function will '\
         'execute the NetWkstaUserEnum RPC call to query a given host for actively logged on '\
-        'users', parents=[target_parser])
+        'users', parents=[target_parser, logging_parser, json_output_parser])
     get_netloggedon_parser.set_defaults(func=get_netloggedon)
 
     # Parser for the get-netlocalgroup command
     get_netlocalgroup_parser = subparsers.add_parser('get-netlocalgroup', help='Gets a list of '\
         'members of a local group on a machine, or returns every local group. You can use local '\
         'credentials instead of domain credentials, however, domain credentials are needed to '\
-        'resolve domain SIDs.', parents=[target_parser])
+        'resolve domain SIDs.', parents=[target_parser, logging_parser, json_output_parser])
     get_netlocalgroup_parser.add_argument('--groupname', dest='queried_groupname',
             help='Group to list the members of (defaults to the local \'Administrators\' group')
     get_netlocalgroup_parser.add_argument('--list-groups', action='store_true',
             help='If set, returns a list of the local groups on the targets')
     get_netlocalgroup_parser.add_argument('-t', '--dc-ip', dest='domain_controller',
             default=str(), help='IP address of the Domain Controller (used to resolve domain SIDs)')
+    get_netlocalgroup_parser.add_argument('--tls', action='store_true', dest='do_tls',
+            help='Force TLS connection to the Domain Controller')
     get_netlocalgroup_parser.add_argument('-r', '--recurse', action='store_true',
             help='If the group member is a domain group, try to resolve its members as well')
     get_netlocalgroup_parser.set_defaults(func=get_netlocalgroup)
 
     # Parser for the invoke-checklocaladminaccess command
     invoke_checklocaladminaccess_parser = subparsers.add_parser('invoke-checklocaladminaccess', help='Checks '\
-            'if the given user has local admin access on the given host', parents=[target_parser])
+            'if the given user has local admin access on the given host', 
+            parents=[target_parser, logging_parser, json_output_parser])
     invoke_checklocaladminaccess_parser.set_defaults(func=invoke_checklocaladminaccess)
 
     # Parser for the get-netprocess command
     get_netprocess_parser = subparsers.add_parser('get-netprocess', help='This function will '\
         'execute the \'Select * from Win32_Process\' WMI query to a given host for a list of '\
-        'executed process', parents=[target_parser])
+        'executed process', parents=[target_parser, logging_parser, json_output_parser])
     get_netprocess_parser.set_defaults(func=get_netprocess)
 
     # Parser for the get-userevent command
     get_userevent_parser = subparsers.add_parser('get-userevent', help='This function will '\
-        'execute the \'Select * from Win32_Process\' WMI query to a given host for a list of '\
-        'executed process', parents=[target_parser])
+        'execute the \'SELECT * from Win32_NTLogEvent\' WMI query to a given host for a list of '\
+        'executed process', parents=[target_parser, logging_parser, json_output_parser])
     get_userevent_parser.add_argument('--event-type', nargs='+', choices=['logon', 'tgt'],
             default=['logon', 'tgt'], help='The type of event to search for: logon, tgt, or all (default: all)')
     get_userevent_parser.add_argument('--date-start', type=int,
@@ -398,7 +493,7 @@ def main():
 
     # Parser for the invoke-userhunter command
     invoke_userhunter_parser = subparsers.add_parser('invoke-userhunter', help='Finds '\
-            'which machines domain users are logged into', parents=[ad_parser, hunter_parser])
+            'which machines domain users are logged into', parents=[ad_parser, hunter_parser, logging_parser])
     invoke_userhunter_parser.add_argument('--unconstrained', action='store_true',
             help='Query only computers with unconstrained delegation')
     invoke_userhunter_parser.add_argument('--admin-count', action='store_true',
@@ -424,7 +519,8 @@ def main():
 
     # Parser for the invoke-processhunter command
     invoke_processhunter_parser = subparsers.add_parser('invoke-processhunter', help='Searches machines '\
-            'for processes with specific name, or ran by specific users', parents=[ad_parser, hunter_parser])
+            'for processes with specific name, or ran by specific users',
+            parents=[ad_parser, hunter_parser, logging_parser])
     invoke_processhunter_parser.add_argument('--processname', dest='queried_processname',
             nargs='+', default=list(), help='Names of the process to hunt')
     invoke_processhunter_parser.add_argument('--stop-on-success', action='store_true',
@@ -435,12 +531,24 @@ def main():
 
     # Parser for the invoke-eventhunter command
     invoke_eventhunter_parser = subparsers.add_parser('invoke-eventhunter', help='Searches machines '\
-            'for events with specific name, or ran by specific users', parents=[ad_parser, hunter_parser])
+            'for events with specific name, or ran by specific users',
+            parents=[ad_parser, hunter_parser, logging_parser])
     invoke_eventhunter_parser.add_argument('--search-days', dest='search_days',
             type=int, default=3, help='Number of days back to search logs for (default: %(default)s)')
     invoke_eventhunter_parser.set_defaults(func=invoke_eventhunter)
 
     args = parser.parse_args()
+
+    # setup the main logger
+    logger = logging.getLogger('pywerview_main_logger')
+    logging.addLevelName(5, 'ULTRA')
+    logger.setLevel(args.logging_level)
+    console_handler = logging.StreamHandler()
+    console_handler.setLevel(args.logging_level)
+    formatter = logging.Formatter('[%(levelname)s] %(name)s - %(funcName)s : %(message)s')
+    console_handler.setFormatter(formatter)
+    logger.addHandler(console_handler)
+
     if args.hashes:
         try:
             args.lmhash, args.nthash = args.hashes.split(':')
@@ -451,26 +559,40 @@ def main():
     else:
         args.lmhash = args.nthash = str()
 
-    if args.password is None and not args.hashes:
+    if args.password is None and args.hashes is None and not args.do_kerberos:
         from getpass import getpass
         args.password = getpass('Password:')
 
     parsed_args = dict()
     for k, v in vars(args).items():
-        if k not in ('func', 'hashes', 'submodule'):
+        if k not in ('func', 'hashes', 'submodule', 'logging_level', 'json_output'):
             parsed_args[k] = v
 
-    #try:
+    starting_time = datetime.datetime.now()
     results = args.func(**parsed_args)
-    #except Exception, e:
-        #print >>sys.stderr, repr(e)
-        #sys.exit(-1)
+    ending_time = datetime.datetime.now()
+
+    try:
+        json_output = args.json_output
+    except AttributeError:
+        json_output = False
 
     if results is not None:
-        try:
-            for x in results:
-                    print(x)
-        # for example, invoke_checklocaladminaccess returns a bool 
-        except TypeError:
-            print(results)
+        if json_output:
+            results_json = {'cmd' : {'submodule' : args.submodule, 'args' : parsed_args,
+                'starting_time': starting_time, 'ending_time': ending_time}}
+            try:
+                objects_json = [x.to_json() for x in results]
+            except TypeError:
+                try:
+                    objects_json = [results.to_json()]
+                except AttributeError:
+                    objects_json = results
+            results_json['results'] = objects_json
+            print(json.dumps(results_json, default=str))
+        else:
+            try:
+                print('\n\n'.join(str(x) for x in results))
+            except TypeError:
+                print(results)
 
diff --git a/pywerview/formatters.py b/pywerview/formatters.py
new file mode 100644
index 0000000..839574e
--- /dev/null
+++ b/pywerview/formatters.py
@@ -0,0 +1,147 @@
+# This file is part of PywerView.
+
+# PywerView is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# PywerView is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with PywerView.  If not, see <http://www.gnu.org/licenses/>.
+
+# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2022
+
+import logging
+import binascii
+from Cryptodome.Hash import MD4
+from impacket.examples.ntlmrelayx.attacks.ldapattack import MSDS_MANAGEDPASSWORD_BLOB
+from impacket.ldap.ldaptypes import SR_SECURITY_DESCRIPTOR
+
+__uac_flags = {0x0000001: 'SCRIPT',
+               0x0000002: 'ACCOUNTDISABLE',
+               0x0000008: 'HOMEDIR_REQUIRED',
+               0x0000010: 'LOCKOUT',
+               0x0000020: 'PASSWD_NOTREQD',
+               0x0000040: 'PASSWD_CANT_CHANGE',
+               0x0000080: 'ENCRYPTED_TEXT_PWD_ALLOWED',
+               0x0000100: 'TEMP_DUPLICATE_ACCOUNT',
+               0x0000200: 'NORMAL_ACCOUNT',
+               0x0000800: 'INTERDOMAIN_TRUST_ACCOUNT',
+               0x0001000: 'WORKSTATION_TRUST_ACCOUNT',
+               0x0002000: 'SERVER_TRUST_ACCOUNT',
+               0x0010000: 'DONT_EXPIRE_PASSWORD',
+               0x0020000: 'MNS_LOGON_ACCOUNT',
+               0x0040000: 'SMARTCARD_REQUIRED',
+               0x0080000: 'TRUSTED_FOR_DELEGATION',
+               0x0100000: 'NOT_DELEGATED',
+               0x0200000: 'USE_DES_KEY_ONLY',
+               0x0400000: 'DONT_REQ_PREAUTH',
+               0x0800000: 'PASSWORD_EXPIRED',
+               0x1000000: 'TRUSTED_TO_AUTH_FOR_DELEGATION',
+               0x4000000: 'PARTIAL_SECRETS_ACCOUNT'}
+
+__ace_flags = {0x1: 'object_inherit', 0x2: 'container_inherit',
+               0x4: 'non_propagate_inherit', 0x8: 'inherit_only',
+               0x10: 'inherited_ace', 0x20: 'audit_successful_accesses',
+               0x40: 'audit_failed_access'}
+
+__object_ace_flags = {0x1: 'object_ace_type_present', 0x2: 'inherited_object_ace_type_present'}
+
+# Resources: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/990fb975-ab31-4bc1-8b75-5da132cd4584
+__access_mask = {0x1: 'create_child', 0x2: 'delete_child',
+                 0x4: 'list_children', 0x08: 'self',
+                 0x10: 'read_property', 0x20: 'write_property',
+                 0x40: 'delete_tree', 0x80: 'list_object',
+                 0x100: 'extended_right', 0x10000: 'delete',
+                 0x20000: 'read_control', 0x40000: 'write_dacl',
+                 0x80000: 'write_owner'}
+
+__access_mask_generic = {0xf01ff: 'generic_all', 0x20094: 'generic_read',
+                         0x20028: 'generic_write', 0x20004: 'generic_execute'}
+
+__trust_attrib = {0x1: 'non_transitive', 0x2: 'uplevel_only',
+                  0x4: 'filter_sids', 0x8: 'forest_transitive',
+                  0x10: 'cross_organization', 0x20: 'within_forest',
+                  0x40: 'treat_as_external',
+                  0x80: 'trust_uses_rc4_encryption',
+                  0x100: 'trust_uses_aes_keys',
+                  0X200: 'cross_organization_no_tgt_delegation',
+                  0x400: 'pim_trust'}
+
+__trust_direction = {0: 'disabled', 1: 'inbound',
+                     2: 'outbound', 3: 'bidirectional'}
+
+__trust_type = {1: 'windows_non_active_directory',
+                2: 'windows_active_directory', 3: 'mit'}
+
+def __format_flag(raw_value, flag_dict):
+    try:
+        int_value = int(raw_value)
+    except ValueError:
+        self._logger.warning('Unable to convert raw flag value to int')
+        return raw_value
+
+    parsed_flags = list()
+    for flag, flag_label in flag_dict.items():
+        if (int_value & flag) == flag:
+            parsed_flags.append(flag_label)
+    return parsed_flags
+
+def __format_dict_lookup(raw_value, dictionary):
+    try:
+        return dictionary[int(raw_value)]
+    except (ValueError, KeyError):
+        self._logger.warning('Unable to convert raw value to int')
+        return raw_value
+
+def format_useraccountcontrol(raw_value):
+    return __format_flag(raw_value, __uac_flags)
+
+def format_ace_access_mask(raw_value):
+    try:
+        int_value = int(raw_value)
+    except ValueError:
+        self._logger.warning('Unable to convert raw ace acess mask value to int')
+        return raw_value
+
+    activedirectoryrights = list()
+    for flag, flag_label in __access_mask_generic.items():
+        if (int_value & flag) == flag:
+            activedirectoryrights.append(flag_label)
+            int_value ^= flag
+    activedirectoryrights += __format_flag(raw_value, __access_mask)
+
+    return activedirectoryrights
+
+
+def format_managedpassword(raw_value):
+    blob = MSDS_MANAGEDPASSWORD_BLOB()
+    blob.fromString(raw_value)
+    return binascii.hexlify(MD4.new(blob['CurrentPassword'][:-2]).digest()).decode('utf8')
+
+def format_groupmsamembership(raw_value):
+    sid = list()
+    sr = SR_SECURITY_DESCRIPTOR(data=raw_value)
+    for dacl in sr['Dacl']['Data']:
+        sid.append(dacl['Ace']['Sid'].formatCanonical())
+    return sid
+
+def format_ace_flags(raw_value):
+    return __format_flag(raw_value, __ace_flags)
+
+def format_object_ace_flags(raw_value):
+    return __format_flag(raw_value, __object_ace_flags)
+
+def format_trustdirection(raw_value):
+    return __format_dict_lookup(raw_value, __trust_direction)
+
+def format_trusttype(raw_value):
+    return __format_dict_lookup(raw_value, __trust_type)
+
+def format_trustattributes(raw_value):
+    return __format_flag(raw_value, __trust_attrib)
+ 
diff --git a/pywerview/functions/gpo.py b/pywerview/functions/gpo.py
index f850bb5..09e754e 100644
--- a/pywerview/functions/gpo.py
+++ b/pywerview/functions/gpo.py
@@ -13,9 +13,8 @@
 # You should have received a copy of the GNU General Public License
 # along with PywerView.  If not, see <http://www.gnu.org/licenses/>.
 
-# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021
+# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2022
 
-import codecs
 from bs4 import BeautifulSoup
 from io import BytesIO
 
@@ -42,6 +41,21 @@ class GPORequester(LDAPRequester):
 
         return self._ldap_search(gpo_search_filter, GPO)
 
+    @LDAPRequester._ldap_connection_init
+    def get_netpso(self, queried_psoname='*', queried_displayname=str(),
+                   queried_domain=str(), ads_path=str()):
+
+        pso_search_filter = '(objectClass=msDS-PasswordSettings)'
+
+        if queried_displayname:
+            pso_search_filter += '(displayname={})'.format(queried_displayname)
+        else:
+            pso_search_filter += '(name={})'.format(queried_psoname)
+
+        pso_search_filter = '(&{})'.format(pso_search_filter)
+
+        return self._ldap_search(pso_search_filter, PSO)
+
     def get_gpttmpl(self, gpttmpl_path):
         content_io = BytesIO()
 
@@ -51,31 +65,32 @@ class GPORequester(LDAPRequester):
         file_name = '\\'.join(gpttmpl_path_split[4:])
 
         smb_connection = SMBConnection(remoteName=target, remoteHost=target)
-        # TODO: kerberos login
-        smb_connection.login(self._user, self._password, self._domain,
-                             self._lmhash, self._nthash)
+        if self._do_kerberos:
+            smb_connection.kerberosLogin(self._user, self._password, self._domain,
+                                 self._lmhash, self._nthash)
+        else:
+            smb_connection.login(self._user, self._password, self._domain,
+                                 self._lmhash, self._nthash)
 
+        self._logger.debug('Get File: Share = {0}, file_name ={1}'.format(share, file_name))
         smb_connection.connectTree(share)
         smb_connection.getFile(share, file_name, content_io.write)
         try:
-            content = codecs.decode(content_io.getvalue(), 'utf-16le')[1:].replace('\r', '')
+            content = content_io.getvalue().decode('utf-16le')[1:].replace('\r', '')
         except UnicodeDecodeError:
-            content = str(content_io.getvalue()).replace('\r', '')
+            self._logger.warning('Unicode error: trying utf-8')
+            content = content_io.getvalue().decode('utf-8').replace('\r', '')
 
         gpttmpl_final = GptTmpl(list())
         for l in content.split('\n'):
             if l.startswith('['):
                 section_name = l.strip('[]').replace(' ', '').lower()
-                setattr(gpttmpl_final, section_name, Policy(list()))
+                gpttmpl_final._attributes_dict[section_name] = Policy(list())
             elif '=' in l:
                 property_name, property_values = [x.strip() for x in l.split('=')]
                 if ',' in property_values:
                     property_values = property_values.split(',')
-                try:
-                    setattr(getattr(gpttmpl_final, section_name), property_name, property_values)
-                except UnicodeEncodeError:
-                    property_name = property_name.encode('utf-8')
-                    setattr(getattr(gpttmpl_final, section_name), property_name, property_values)
+                gpttmpl_final._attributes_dict[section_name]._attributes_dict[property_name] = property_values
 
         return gpttmpl_final
 
@@ -100,34 +115,37 @@ class GPORequester(LDAPRequester):
                 try:
                     privilege_rights_policy = gpttmpl.privilegerights
                 except AttributeError:
+                    self._logger.critical('Could not parse privilegerights from the DC policy, SIDs will not be resolved')
                     return gpttmpl
 
                 members = inspect.getmembers(privilege_rights_policy, lambda x: not(inspect.isroutine(x)))
                 with NetRequester(self._domain_controller, self._domain, self._user,
-                                  self._password, self._lmhash, self._nthash) as net_requester:
-                    for member in members:
-                        if member[0].startswith('_'):
-                            continue
-                        if not isinstance(member[1], list):
-                            sids = [member[1]]
+                                  self._password, self._lmhash, self._nthash, self._do_kerberos, self._do_tls) as net_requester:
+                    for attr in privilege_rights_policy._attributes_dict:
+                        attribute = privilege_rights_policy._attributes_dict[attr]
+                        if not isinstance(attribute, list):
+                            sids = [attribute]
                         else:
-                            sids = member[1]
+                            sids = attribute
                         resolved_sids = list()
                         for sid in sids:
                             if not sid:
                                 continue
+                            sid = sid.replace('*', '')
                             try:
-                                resolved_sid = net_requester.get_adobject(queried_sid=sid, queried_domain=queried_domain)[0]
+                                resolved_sid = net_requester.get_adobject(queried_sid=sid, queried_domain=self._queried_domain)[0]
                             except IndexError:
+                                self._logger.warning('We did not manage to resolve this SID ({}) against the DC'.format(sid))
                                 resolved_sid = sid
                             else:
                                 resolved_sid = resolved_sid.distinguishedname.split(',')[:2]
-                                resolved_sid = '{}\\{}'.format(resolved_sid[1], resolved_sid[0])
+                                resolved_sid = resolved_sid[1] + '\\' + resolved_sid[0]
                                 resolved_sid = resolved_sid.replace('CN=', '')
+                            finally:
                                 resolved_sids.append(resolved_sid)
                         if len(resolved_sids) == 1:
                             resolved_sids = resolved_sids[0]
-                        setattr(privilege_rights_policy, member[0], resolved_sids)
+                        privilege_rights_policy._attributes_dict[attr] = resolved_sids
 
                 gpttmpl.privilegerights = privilege_rights_policy
 
@@ -145,23 +163,33 @@ class GPORequester(LDAPRequester):
         file_name = '\\'.join(groupsxml_path_split[4:])
 
         smb_connection = SMBConnection(remoteName=target, remoteHost=target)
-        # TODO: kerberos login
-        smb_connection.login(self._user, self._password, self._domain,
-                             self._lmhash, self._nthash)
+        if self._do_kerberos:
+            smb_connection.kerberosLogin(self._user, self._password, self._domain,
+                                 self._lmhash, self._nthash)
+        else:
+            smb_connection.login(self._user, self._password, self._domain,
+                                 self._lmhash, self._nthash)
 
+        self._logger.debug('Get File: Share = {0}, file_name ={1}'.format(share, file_name))
         smb_connection.connectTree(share)
         try:
             smb_connection.getFile(share, file_name, content_io.write)
         except SessionError:
+            self._logger.warning('Error while getting the file {}, skipping...'.format(file_name))
             return list()
 
         content = content_io.getvalue().replace(b'\r', b'')
-        groupsxml_soup = BeautifulSoup(content, 'xml')
-
+        groupsxml_soup = BeautifulSoup(content.decode('utf-8'), 'xml')
         for group in groupsxml_soup.find_all('Group'):
             members = list()
             memberof = list()
+
+            raw_xml_member = group.Properties.find_all('Member')
+            if not raw_xml_member:
+                continue
+
             local_sid = group.Properties.get('groupSid', str())
+
             if not local_sid:
                 if 'administrators' in group.Properties['groupName'].lower():
                     local_sid = 'S-1-5-32-544'
@@ -171,7 +199,7 @@ class GPORequester(LDAPRequester):
                     local_sid = group.Properties['groupName']
             memberof.append(local_sid)
 
-            for member in group.Properties.find_all('Member'):
+            for member in raw_xml_member:
                 if not member['action'].lower() == 'add':
                     continue
                 if member['sid']:
@@ -185,18 +213,17 @@ class GPORequester(LDAPRequester):
                 # have the barest support for filters, so ¯\_(ツ)_/¯
 
                 gpo_group = GPOGroup(list())
-                setattr(gpo_group, 'gpodisplayname', gpo_display_name)
-                setattr(gpo_group, 'gponame', gpo_name)
-                setattr(gpo_group, 'gpopath', groupsxml_path)
-                setattr(gpo_group, 'members', members)
-                setattr(gpo_group, 'memberof', memberof)
+                gpo_group._attributes_dict['gpodisplayname'] = gpo_display_name
+                gpo_group._attributes_dict['gponame'] = gpo_name
+                gpo_group._attributes_dict['gpopath'] = groupsxml_path
+                gpo_group._attributes_dict['members'] = members
+                gpo_group._attributes_dict['memberof'] = memberof
 
                 gpo_groups.append(gpo_group)
 
         return gpo_groups
 
     def _get_groupsgpttmpl(self, gpttmpl_path, gpo_display_name):
-        import inspect
         gpo_groups = list()
 
         gpt_tmpl = self.get_gpttmpl(gpttmpl_path)
@@ -207,34 +234,35 @@ class GPORequester(LDAPRequester):
         except AttributeError:
             return list()
 
-        membership = inspect.getmembers(group_membership, lambda x: not(inspect.isroutine(x)))
-        for m in membership:
-            if not m[1]:
+        membership = group_membership._attributes_dict
+
+        for ma,mv in membership.items():
+            if not mv:
                 continue
             members = list()
             memberof = list()
-            if m[0].lower().endswith('__memberof'):
-                members.append(m[0].upper().lstrip('*').replace('__MEMBEROF', ''))
-                if not isinstance(m[1], list):
-                    memberof_list = [m[1]]
+            if ma.lower().endswith('__memberof'):
+                members.append(ma.upper().lstrip('*').replace('__MEMBEROF', ''))
+                if not isinstance(mv, list):
+                    memberof_list = [mv]
                 else:
-                    memberof_list = m[1]
+                    memberof_list = mv
                 memberof += [x.lstrip('*') for x in memberof_list]
-            elif m[0].lower().endswith('__members'):
-                memberof.append(m[0].upper().lstrip('*').replace('__MEMBERS', ''))
-                if not isinstance(m[1], list):
-                    members_list = [m[1]]
+            elif ma.lower().endswith('__members'):
+                memberof.append(ma.upper().lstrip('*').replace('__MEMBERS', ''))
+                if not isinstance(mv, list):
+                    members_list = [mv]
                 else:
-                    members_list = m[1]
+                    members_list = mv
                 members += [x.lstrip('*') for x in members_list]
 
             if members and memberof:
                 gpo_group = GPOGroup(list())
-                setattr(gpo_group, 'gpodisplayname', gpo_display_name)
-                setattr(gpo_group, 'gponame', gpo_name)
-                setattr(gpo_group, 'gpopath', gpttmpl_path)
-                setattr(gpo_group, 'members', members)
-                setattr(gpo_group, 'memberof', memberof)
+                gpo_group.add_attributes({'gpodisplayname' : gpo_display_name})
+                gpo_group.add_attributes({'gponame' : gpo_name})
+                gpo_group.add_attributes({'gpopath' : gpttmpl_path})
+                gpo_group.add_attributes({'members' : members})
+                gpo_group.add_attributes({'memberof' : memberof})
 
                 gpo_groups.append(gpo_group)
 
@@ -259,6 +287,7 @@ class GPORequester(LDAPRequester):
                 results += self._get_groupsgpttmpl(gpttmpl_path, gpo_display_name)
             except SessionError:
                 # If the GptTmpl file doesn't exist, we skip this
+                self._logger.warning('Error while getting the file {}, skipping...'.format(gpttmpl_path,))
                 pass
 
         if resolve_sids:
@@ -269,32 +298,28 @@ class GPORequester(LDAPRequester):
                 resolved_members = list()
                 resolved_memberof = list()
                 with NetRequester(self._domain_controller, self._domain, self._user,
-                                  self._password, self._lmhash, self._nthash) as net_requester:
+                                  self._password, self._lmhash, self._nthash, self._do_kerberos, self._do_tls) as net_requester:
                     for member in members:
                         try:
-                            resolved_member = net_requester.get_adobject(queried_sid=member, queried_domain=queried_domain)[0]
-                            resolved_member = resolved_member.distinguishedname.split(',')
-                            resolved_member_domain = '.'.join(resolved_member[1:])
-                            resolved_member = '{}\\{}'.format(resolved_member_domain, resolved_member[0])
-                            resolved_member = resolved_member.replace('CN=', '').replace('DC=', '')
+                            resolved_member = net_requester.get_adobject(queried_sid=member, queried_domain=self._queried_domain)[0]
+                            resolved_member = resolved_member.distinguishedname
                         except IndexError:
+                            self._logger.warning('We did not manage to resolve this SID ({}) against the DC'.format(member))
                             resolved_member = member
                         finally:
                             resolved_members.append(resolved_member)
-                    gpo_group.members = resolved_members
+                    gpo_group._attributes_dict['members'] = resolved_members
 
                     for member in memberof:
                         try:
-                            resolved_member = net_requester.get_adobject(queried_sid=member, queried_domain=queried_domain)[0]
-                            resolved_member = resolved_member.distinguishedname.split(',')[:2]
-                            resolved_member = '{}\\{}'.format(resolved_member[1], resolved_member[0])
-                            resolved_member = resolved_member.replace('CN=', '').replace('DC=', '')
+                            resolved_member = net_requester.get_adobject(queried_sid=member, queried_domain=self._queried_domain)[0]
+                            resolved_member = resolved_member.distinguishedname
                         except IndexError:
+                            self._logger.warning('We did not manage to resolve this SID ({}) against the DC'.format(member))
                             resolved_member = member
                         finally:
                             resolved_memberof.append(resolved_member)
-                    gpo_group.memberof = memberof = resolved_memberof
-
+                    gpo_group._attributes_dict['memberof'] = memberof = resolved_memberof
         return results
 
     def find_gpocomputeradmin(self, queried_computername=str(),
@@ -306,7 +331,8 @@ class GPORequester(LDAPRequester):
             raise ValueError('You must specify either a computer name or an OU name')
 
         net_requester = NetRequester(self._domain_controller, self._domain, self._user,
-                                     self._password, self._lmhash, self._nthash)
+                                     self._password, self._lmhash, self._nthash, self._do_kerberos,
+                                     self._do_tls)
         if queried_computername:
             computers = net_requester.get_netcomputer(queried_computername=queried_computername,
                                                       queried_domain=queried_domain,
@@ -327,25 +353,28 @@ class GPORequester(LDAPRequester):
         for target_ou in target_ous:
             ous = net_requester.get_netou(ads_path=target_ou, queried_domain=queried_domain,
                                           full_data=True)
-
             for ou in ous:
-                for gplink in ou.gplink.strip('[]').split(']['):
+                try:
+                    gplinks = ou.gplink.strip('[]').split('][')
+                except AttributeError:
+                    continue
+                for gplink in gplinks:
                     gplink = gplink.split(';')[0]
                     gpo_groups = self.get_netgpogroup(queried_domain=queried_domain,
                                                       ads_path=gplink)
                     for gpo_group in gpo_groups:
                         for member in gpo_group.members:
                             obj = net_requester.get_adobject(queried_sid=member,
-                                                             queried_domain=queried_domain)[0]
+                                                             queried_domain=self._queried_domain)[0]
                             gpo_computer_admin = GPOComputerAdmin(list())
-                            setattr(gpo_computer_admin, 'computername', queried_computername)
-                            setattr(gpo_computer_admin, 'ou', target_ou)
-                            setattr(gpo_computer_admin, 'gpodisplayname', gpo_group.gpodisplayname)
-                            setattr(gpo_computer_admin, 'gpopath', gpo_group.gpopath)
-                            setattr(gpo_computer_admin, 'objectname', obj.name)
-                            setattr(gpo_computer_admin, 'objectdn', obj.distinguishedname)
-                            setattr(gpo_computer_admin, 'objectsid', member)
-                            setattr(gpo_computer_admin, 'isgroup', (obj.samaccounttype != '805306368'))
+                            gpo_computer_admin.add_attributes({'computername' : queried_computername})
+                            gpo_computer_admin.add_attributes({'ou' : target_ou})
+                            gpo_computer_admin.add_attributes({'gpodisplayname' : gpo_group.gpodisplayname})
+                            gpo_computer_admin.add_attributes({'gpopath' : gpo_group.gpopath})
+                            gpo_computer_admin.add_attributes({'objectname' : obj.name})
+                            gpo_computer_admin.add_attributes({'objectdn' : obj.distinguishedname})
+                            gpo_computer_admin.add_attributes({'objectsid' : obj.objectsid})
+                            gpo_computer_admin.add_attributes({'isgroup' : (obj.samaccounttype != 805306368)})
 
                             results.append(gpo_computer_admin)
 
@@ -353,19 +382,20 @@ class GPORequester(LDAPRequester):
                                 groups_to_resolve = [gpo_computer_admin.objectsid]
                                 while groups_to_resolve:
                                     group_to_resolve = groups_to_resolve.pop(0)
+
                                     group_members = net_requester.get_netgroupmember(queried_sid=group_to_resolve,
-                                                                                     queried_domain=queried_domain,
+                                                                                     queried_domain=self._queried_domain,
                                                                                      full_data=True)
                                     for group_member in group_members:
                                         gpo_computer_admin = GPOComputerAdmin(list())
-                                        setattr(gpo_computer_admin, 'computername', queried_computername)
-                                        setattr(gpo_computer_admin, 'ou', target_ou)
-                                        setattr(gpo_computer_admin, 'gpodisplayname', gpo_group.gpodisplayname)
-                                        setattr(gpo_computer_admin, 'gpopath', gpo_group.gpopath)
-                                        setattr(gpo_computer_admin, 'objectname', group_member.samaccountname)
-                                        setattr(gpo_computer_admin, 'objectdn', group_member.distinguishedname)
-                                        setattr(gpo_computer_admin, 'objectsid', member)
-                                        setattr(gpo_computer_admin, 'isgroup', (group_member.samaccounttype != '805306368'))
+                                        gpo_computer_admin.add_attributes({'computername' : queried_computername})
+                                        gpo_computer_admin.add_attributes({'ou' : target_ou})
+                                        gpo_computer_admin.add_attributes({'gpodisplayname' : gpo_group.gpodisplayname})
+                                        gpo_computer_admin.add_attributes({'gpopath' : gpo_group.gpopath})
+                                        gpo_computer_admin.add_attributes({'objectname' : group_member.samaccountname})
+                                        gpo_computer_admin.add_attributes({'objectdn' : group_member.distinguishedname})
+                                        gpo_computer_admin.add_attributes({'objectsid' : group_member.objectsid})
+                                        gpo_computer_admin.add_attributes({'isgroup' : (group_member.samaccounttype != 805306368)})
 
                                         results.append(gpo_computer_admin)
 
@@ -378,11 +408,12 @@ class GPORequester(LDAPRequester):
                          queried_localgroup=str(), queried_domain=str()):
         results = list()
         net_requester = NetRequester(self._domain_controller, self._domain, self._user,
-                                     self._password, self._lmhash, self._nthash)
+                                     self._password, self._lmhash, self._nthash, self._do_kerberos,
+                                     self._do_tls)
         if queried_username:
                 try:
                     user = net_requester.get_netuser(queried_username=queried_username,
-                                                     queried_domain=queried_domain)[0]
+                                                     queried_domain=self._queried_domain)[0]
                 except IndexError:
                     raise ValueError('Username \'{}\' was not found'.format(queried_username))
                 else:
@@ -392,7 +423,7 @@ class GPORequester(LDAPRequester):
         elif queried_groupname:
                 try:
                     group = net_requester.get_netgroup(queried_groupname=queried_groupname,
-                                                       queried_domain=queried_domain,
+                                                       queried_domain=self._queried_domain,
                                                        full_data=True)[0]
                 except IndexError:
                     raise ValueError('Group name \'{}\' was not found'.format(queried_groupname))
@@ -418,15 +449,19 @@ class GPORequester(LDAPRequester):
         for object_group in object_groups:
             try:
                 object_group_sid = net_requester.get_adobject(queried_sam_account_name=object_group.samaccountname,
-                                                              queried_domain=queried_domain)[0].objectsid
+                                                              queried_domain=self._queried_domain)[0].objectsid
             except IndexError:
                 # We may have the name of the group, but not its sam account name
+                self._logger.warning('We may have the name of the group, but not its sam account name.')
                 try:
                     object_group_sid = net_requester.get_adobject(queried_name=object_group.samaccountname,
-                                                                  queried_domain=queried_domain)[0].objectsid
+                                                                  queried_domain=self._queried_domain)[0].objectsid
                 except IndexError:
                     # Freak accident when someone is a member of a group, but
                     # we can't find the group in the AD
+                    self._logger.warning('Freak accident when someone is a member of a group, but we can\'t find the group in the AD,'
+                                         'see DEBUG level for more info')
+                    self._logger.debug('Dumping the mysterious object = {}'.format(object_group))
                     continue
 
             target_sid.append(object_group_sid)
@@ -435,10 +470,11 @@ class GPORequester(LDAPRequester):
         for gpo_group in self.get_netgpogroup(queried_domain=queried_domain):
             try:
                 for member in gpo_group.members:
+                    member = member
                     if not member.upper().startswith('S-1-5'):
                         try:
                             member = net_requester.get_adobject(queried_sam_account_name=member,
-                                                                queried_domain=queried_domain)[0].objectsid
+                                                                queried_domain=self._queried_domain)[0].objectsid
                         except (IndexError, AttributeError):
                             continue
                     if (member.upper() in target_sid) or (member.lower() in target_sid):
@@ -451,19 +487,20 @@ class GPORequester(LDAPRequester):
 
         for gpo_group in gpo_groups:
             gpo_guid = gpo_group.gponame
-            ous = net_requester.get_netou(queried_domain=queried_domain,
+            ous = net_requester.get_netou(queried_domain=self._queried_domain,
                                           queried_guid=gpo_guid, full_data=True)
             for ou in ous:
+                ou_distinguishedname = 'LDAP://{}'.format(ou.distinguishedname)
                 # TODO: support filters for GPO
                 ou_computers = [x.dnshostname for x in \
-                        net_requester.get_netcomputer(queried_domain=queried_domain,
-                                                      ads_path=ou.distinguishedname)]
+                        net_requester.get_netcomputer(queried_domain=self._queried_domain,
+                                                      ads_path=ou_distinguishedname)]
                 gpo_location = GPOLocation(list())
-                setattr(gpo_location, 'objectname', object_distinguished_name)
-                setattr(gpo_location, 'gponame', gpo_group.gpodisplayname)
-                setattr(gpo_location, 'gpoguid', gpo_guid)
-                setattr(gpo_location, 'containername', ou.distinguishedname)
-                setattr(gpo_location, 'computers', ou_computers)
+                gpo_location.add_attributes({'objectname' : object_distinguished_name})
+                gpo_location.add_attributes({'gponame' : gpo_group.gpodisplayname})
+                gpo_location.add_attributes({'gpoguid' : gpo_guid})
+                gpo_location.add_attributes({'containername' : ou.distinguishedname})
+                gpo_location.add_attributes({'computers' : ou_computers})
 
                 results.append(gpo_location)
 
diff --git a/pywerview/functions/hunting.py b/pywerview/functions/hunting.py
index d1393c4..ee2be62 100644
--- a/pywerview/functions/hunting.py
+++ b/pywerview/functions/hunting.py
@@ -13,7 +13,7 @@
 # You should have received a copy of the GNU General Public License
 # along with PywerView.  If not, see <http://www.gnu.org/licenses/>.
 
-# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021
+# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2022
 
 import random
 import multiprocessing
@@ -26,9 +26,10 @@ from pywerview.worker.hunting import UserHunterWorker, ProcessHunterWorker, Even
 
 class Hunter(NetRequester):
     def __init__(self, target_computer, domain=str(), user=(), password=str(),
-                 lmhash=str(), nthash=str(), domain_controller=str(), queried_domain=str()):
+                 lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False,
+                 domain_controller=str(), queried_domain=str()):
         NetRequester.__init__(self, target_computer, domain, user, password,
-                              lmhash, nthash, domain_controller)
+                              lmhash, nthash, do_kerberos, do_tls, domain_controller)
         self._target_domains = list()
         self._target_computers = list()
         self._target_users = list()
@@ -89,7 +90,7 @@ class Hunter(NetRequester):
             self._target_users.append(rpcobj.TargetUser(attributes))
         elif target_server:
             with NetRequester(target_server, domain, user, password, lmhash,
-                              nthash, domain_controller) as target_server_requester:
+                              nthash, do_kerberos, do_tls, domain_controller) as target_server_requester:
                 for x in target_server_requester.get_netlocalgroup(recurse=True):
                     if x.isdomain and not x.isgroup:
                         attributes = {'memberdomain': x.name.split('/')[0].lower(),
@@ -139,7 +140,7 @@ class Hunter(NetRequester):
             self._parent_pipes.append(parent_pipe)
             worker = worker_class(worker_pipe, self._domain, self._user,
                                             self._password, self._lmhash, self._nthash,
-                                            *worker_args)
+                                            self._do_kerberos, self._do_tls, *worker_args)
 
             worker.start()
             self._workers.append(worker)
@@ -155,7 +156,7 @@ class Hunter(NetRequester):
                 rlist, wlist, _ = select.select(self._parent_pipes, write_watch_list, list())
 
                 for readable in rlist:
-                    jobs_done += 1 
+                    jobs_done += 1
                     results = readable.recv()
                     for result in results:
                         yield result
@@ -200,7 +201,7 @@ class UserHunter(Hunter):
 
         if foreign_users:
             with Misc(self._domain_controller, self._domain, self._user,
-                      self._password, self._lmhash, self._nthash) as misc_requester:
+                      self._password, self._lmhash, self._nthash, self._do_kerberos, self._do_tls) as misc_requester:
                 domain_sid = misc_requester.get_domainsid(queried_domain)
                 domain_short_name = misc_requester.convert_sidtont4(domain_sid).split('\\')[0]
         else:
diff --git a/pywerview/functions/misc.py b/pywerview/functions/misc.py
index 8b772ec..f906da7 100644
--- a/pywerview/functions/misc.py
+++ b/pywerview/functions/misc.py
@@ -13,7 +13,7 @@
 # You should have received a copy of the GNU General Public License
 # along with PywerView.  If not, see <http://www.gnu.org/licenses/>.
 
-# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021
+# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2022
 
 from impacket.dcerpc.v5.rpcrt import DCERPCException
 from impacket.dcerpc.v5 import scmr, drsuapi
@@ -50,12 +50,16 @@ class Misc(LDAPRPCRequester):
     def get_domainsid(self, queried_domain=str()):
 
         with pywerview.functions.net.NetRequester(self._domain_controller, self._domain, self._user,
-                                                  self._password, self._lmhash, self._nthash) as r:
+                                                  self._password, self._lmhash, self._nthash,
+                                                  self._do_kerberos, self._do_tls) as r:
             domain_controllers = r.get_netdomaincontroller(queried_domain=queried_domain)
 
         if domain_controllers:
             primary_dc = domain_controllers[0]
-            domain_sid = '-'.join(primary_dc.objectsid.split('-')[:-1])
+            domain_sid = primary_dc.objectsid
+
+            # we need to retrieve the domain sid from the controller sid
+            domain_sid = '-'.join(domain_sid.split('-')[:-1])
         else:
             domain_sid = None
 
diff --git a/pywerview/functions/net.py b/pywerview/functions/net.py
index f704348..b8d2794 100644
--- a/pywerview/functions/net.py
+++ b/pywerview/functions/net.py
@@ -13,10 +13,9 @@
 # You should have received a copy of the GNU General Public License
 # along with PywerView.  If not, see <http://www.gnu.org/licenses/>.
 
-# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021
+# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2022
 
-import socket
-import datetime
+from datetime import datetime, timedelta
 from impacket.dcerpc.v5.ndr import NULL
 from impacket.dcerpc.v5 import wkst, srvs, samr
 from impacket.dcerpc.v5.samr import DCERPCSessionError
@@ -24,25 +23,187 @@ from impacket.dcerpc.v5.rpcrt import DCERPCException
 from impacket.dcerpc.v5.dcom.wmi import WBEM_FLAG_FORWARD_ONLY
 from bs4 import BeautifulSoup
 from ldap3.utils.conv import escape_filter_chars
+from ldap3.protocol.microsoft import security_descriptor_control
+from ldap3.protocol.formatters.formatters import *
+from impacket.ldap.ldaptypes import ACE, ACCESS_ALLOWED_OBJECT_ACE, ACCESS_MASK, LDAP_SID, SR_SECURITY_DESCRIPTOR
 
 from pywerview.requester import LDAPRPCRequester
 import pywerview.objects.adobjects as adobj
 import pywerview.objects.rpcobjects as rpcobj
 import pywerview.functions.misc
+import pywerview.formatters as fmt
 
 class NetRequester(LDAPRPCRequester):
     @LDAPRPCRequester._ldap_connection_init
     def get_adobject(self, queried_domain=str(), queried_sid=str(),
                      queried_name=str(), queried_sam_account_name=str(),
-                     ads_path=str(), custom_filter=str()):
+                     ads_path=str(), attributes=list(), custom_filter=str()):
+        for attr_desc, attr_value in (('objectSid', queried_sid), ('name', escape_filter_chars(queried_name)),
+                                      ('samAccountName', escape_filter_chars(queried_sam_account_name))):
+            if attr_value:
+                object_filter = '(&({}={}){})'.format(attr_desc, attr_value, custom_filter)
+                break
+        else:
+            object_filter = '(&(name=*){})'.format(custom_filter)
+
+        return self._ldap_search(object_filter, adobj.ADObject, attributes=attributes)
+
+    @LDAPRPCRequester._ldap_connection_init
+    def get_adserviceaccount(self, queried_domain=str(), queried_sid=str(),
+                     queried_name=str(), queried_sam_account_name=str(),
+                     ads_path=str(), resolve_sids=False):
+        filter_objectclass = '(ObjectClass=msDS-GroupManagedServiceAccount)'
+        attributes = ['samaccountname', 'distinguishedname', 'objectsid', 'description',
+                      'msds-managedpassword', 'msds-groupmsamembership', 'useraccountcontrol']
+
+        if not self._ldap_connection.server.ssl:
+            self._logger.warning('LDAP connection is not encrypted, we can\'t ask '\
+                    'for msds-managedpassword, removing from list of attributes')
+            attributes.remove('msds-managedpassword')
+
+        for attr_desc, attr_value in (('objectSid', queried_sid), ('name', escape_filter_chars(queried_name)),
+                                      ('samAccountName', escape_filter_chars(queried_sam_account_name))):
+            if attr_value:
+                object_filter = '(&({}={}){})'.format(attr_desc, attr_value, filter_objectclass)
+                break
+        else:
+            object_filter = '(&(name=*){})'.format(filter_objectclass)
+
+        adserviceaccounts = self._ldap_search(object_filter, adobj.GMSAAccount, attributes=attributes)
+        sid_resolver = NetRequester(self._domain_controller, self._domain, self._user, self._password, self._lmhash, self._nthash)
+
+        # In this loop, we resolve SID (if true) and we populate 'enabled' attribute
+        for i, adserviceaccount in enumerate(adserviceaccounts):
+            if resolve_sids:
+                results = list()
+                for sid in getattr(adserviceaccount, 'msds-groupmsamembership'):
+                    try:
+                        resolved_sid = sid_resolver.get_adobject(queried_sid=sid, queried_domain=self._queried_domain, 
+                                                                  attributes=['distinguishedname'])[0].distinguishedname
+                    except IndexError:
+                        self._logger.warning('We did not manage to resolve this SID ({}) against the DC'.format(sid))
+                        resolved_sid = sid
+                    results.append(resolved_sid)
+                adserviceaccounts[i].add_attributes({'msds-groupmsamembership': results})
+            adserviceaccounts[i].add_attributes({'Enabled': 'ACCOUNTDISABLE' not in adserviceaccount.useraccountcontrol})
+            adserviceaccounts[i]._attributes_dict.pop('useraccountcontrol')
+        return adserviceaccounts
 
-        for attr_desc, attr_value in (('objectSid', queried_sid), ('name', queried_name),
-                                      ('samAccountName', queried_sam_account_name)):
+    @LDAPRPCRequester._ldap_connection_init
+    def get_objectacl(self, queried_domain=str(), queried_sid=str(),
+                     queried_name=str(), queried_sam_account_name=str(),
+                     ads_path=str(), sacl=False, rights_filter=str(),
+                     resolve_sids=False, resolve_guids=False, custom_filter=str()):
+        for attr_desc, attr_value in (('objectSid', queried_sid), ('name', escape_filter_chars(queried_name)),
+                                      ('samAccountName', escape_filter_chars(queried_sam_account_name))):
             if attr_value:
                 object_filter = '(&({}={}){})'.format(attr_desc, attr_value, custom_filter)
                 break
+        else:
+            object_filter = '(&(name=*){})'.format(custom_filter)
+
+        guid_map = dict()
+        # This works on a mono-domain forest, must be tested on a more complex one
+        if resolve_guids:
+            # Dirty fix to get base DN even if custom ADS path was given
+            base_dn = ','.join(self._base_dn.split(',')[-2:])
+            guid_map = {'{00000000-0000-0000-0000-000000000000}': 'All'}
+            with NetRequester(self._domain_controller, self._domain, self._user, self._password,
+                  self._lmhash, self._nthash, self._do_kerberos, self._do_tls) as net_requester:
+                for o in net_requester.get_adobject(ads_path='CN=Schema,CN=Configuration,{}'.format(base_dn),
+                        attributes=['name', 'schemaIDGUID'], custom_filter='(schemaIDGUID=*)'):
+                    guid_map['{{{}}}'.format(o.schemaidguid)] = o.name
+
+                for o in net_requester.get_adobject(ads_path='CN=Extended-Rights,CN=Configuration,{}'.format(base_dn),
+                        attributes=['name', 'rightsGuid'], custom_filter='(objectClass=controlAccessRight)'):
+                    guid_map['{{{}}}'.format(o.rightsguid.lower())] = o.name
+
+        attributes = ['distinguishedname', 'objectsid', 'ntsecuritydescriptor']
+        if sacl:
+            controls = list()
+            acl_type = 'Sacl'
+        else:
+            # The control is used to get access to ntSecurityDescriptor with an
+            # unprivileged user, see https://stackoverflow.com/questions/40771503/selecting-the-ad-ntsecuritydescriptor-attribute-as-a-non-admin/40773088
+            # /!\ May break pagination from what I've read (see Stack Overflow answer)
+            controls = security_descriptor_control(criticality=True, sdflags=0x07)
+            acl_type = 'Dacl'
+
+        security_descriptors = self._ldap_search(object_filter, adobj.ADObject,
+                attributes=attributes, controls=controls)
+
+        acl = list()
+
+        rights_to_guid = {'reset-password': '{00299570-246d-11d0-a768-00aa006e0529}',
+                'write-members': '{bf9679c0-0de6-11d0-a285-00aa003049e2}',
+                'all': '{00000000-0000-0000-0000-000000000000}'}
+        guid_filter = rights_to_guid.get(rights_filter, None)
+
+        if resolve_sids:
+            sid_resolver = NetRequester(self._domain_controller, self._domain,
+                    self._user, self._password, self._lmhash, self._nthash,
+                    self._do_kerberos, self._do_tls)
+            sid_mapping = adobj.ADObject._well_known_sids.copy()
+        else:
+            sid_resolver = None
 
-        return self._ldap_search(object_filter, adobj.ADObject)
+        for security_descriptor in security_descriptors:
+            sd = SR_SECURITY_DESCRIPTOR()
+            try:
+                sd.fromString(security_descriptor.ntsecuritydescriptor)
+            except TypeError:
+                continue
+            for ace in sd[acl_type]['Data']:
+                if guid_filter:
+                    try:
+                        object_type = format_uuid_le(ace['Ace']['ObjectType']) if ace['Ace']['ObjectType'] else '{00000000-0000-0000-0000-000000000000}'
+                    except KeyError:
+                        continue
+                    if object_type != guid_filter:
+                        continue
+                attributes = dict()
+                attributes['objectdn'] = security_descriptor.distinguishedname
+                attributes['objectsid'] = security_descriptor.objectsid
+                attributes['acetype'] = ace['TypeName']
+                attributes['binarysize'] = ace['AceSize']
+                attributes['aceflags'] = fmt.format_ace_flags(ace['AceFlags'])
+                attributes['accessmask'] = ace['Ace']['Mask']['Mask']
+                attributes['activedirectoryrights'] = fmt.format_ace_access_mask(ace['Ace']['Mask']['Mask'])
+                attributes['isinherited'] = bool(ace['AceFlags'] & 0x10)
+                attributes['securityidentifier'] = format_sid(ace['Ace']['Sid'].getData())
+                if sid_resolver:
+                    converted_sid = attributes['securityidentifier']
+                    try:
+                        resolved_sid = sid_mapping[converted_sid]
+                    except KeyError:
+                        try:
+                            resolved_sid = sid_resolver.get_adobject(queried_sid=converted_sid,
+                                    queried_domain=self._queried_domain, attributes=['distinguishedname'])[0]
+                            resolved_sid = resolved_sid.distinguishedname
+                        except IndexError:
+                            self._logger.warning('We did not manage to resolve this SID ({}) against the DC'.format(converted_sid))
+                            resolved_sid = attributes['securityidentifier']
+                    finally:
+                        sid_mapping[converted_sid] = resolved_sid
+                        attributes['securityidentifier'] = resolved_sid
+                try:
+                    attributes['objectaceflags'] = fmt.format_object_ace_flags(ace['Ace']['Flags'])
+                except KeyError:
+                    pass
+                try:
+                    attributes['objectacetype'] = format_uuid_le(ace['Ace']['ObjectType']) if ace['Ace']['ObjectType'] else '{00000000-0000-0000-0000-000000000000}'
+                    attributes['objectacetype'] = guid_map[attributes['objectacetype']]
+                except KeyError:
+                    pass
+                try:
+                    attributes['inheritedobjectacetype'] = format_uuid_le(ace['Ace']['InheritedObjectType']) if ace['Ace']['InheritedObjectType'] else '{00000000-0000-0000-0000-000000000000}'
+                    attributes['inheritedobjectacetype'] = guid_map[attributes['inheritedobjectacetype']]
+                except KeyError:
+                    pass
+
+                acl.append(adobj.ACE(attributes))
+
+        return acl
 
     @LDAPRPCRequester._ldap_connection_init
     def get_netuser(self, queried_username=str(), queried_domain=str(),
@@ -80,11 +241,15 @@ class NetRequester(LDAPRPCRequester):
 
         # RFC 4515, section 3
         # However if we escape *, we can no longer use wildcard within `--groupname`
-        # Maybe we can raise a warning here ? 
         if not '*' in queried_groupname:
             queried_groupname = escape_filter_chars(queried_groupname)
+        else:
+            self._logger.warning('"*" detected in "{}", if it also contains "(",")" or "\\", '
+                                 'script will probably crash ("invalid filter"). '
+                                 'Don\'t use wildcard with these characters'.format(queried_groupname))
 
         if queried_username:
+            self._logger.debug('Queried username = {}'.format(queried_username))
             results = list()
             sam_account_name_to_resolve = [queried_username]
             first_run = True
@@ -120,7 +285,7 @@ class NetRequester(LDAPRPCRequester):
             final_results = list()
             for group_sam_account_name in results:
                 obj_member_of = adobj.Group(list())
-                setattr(obj_member_of, 'samaccountname', group_sam_account_name)
+                obj_member_of._attributes_dict['samaccountname'] = group_sam_account_name
                 final_results.append(obj_member_of)
             return final_results
         else:
@@ -131,8 +296,10 @@ class NetRequester(LDAPRPCRequester):
             group_search_filter += '(objectCategory=group)'
 
             if queried_sid:
+                self._logger.debug('Queried SID = {}'.format(queried_username))
                 group_search_filter += '(objectSid={})'.format(queried_sid)
             elif queried_groupname:
+                self._logger.debug('Queried groupname = {}'.format(queried_groupname))
                 group_search_filter += '(name={})'.format(queried_groupname)
 
             if full_data:
@@ -209,8 +376,8 @@ class NetRequester(LDAPRPCRequester):
 
         final_results = list()
         for file_server_name in results:
-            attributes = list()
-            attributes.append({'type': 'dnshostname', 'vals': [file_server_name]})
+            attributes = dict()
+            attributes['dnshostname'] = file_server_name
             final_results.append(adobj.FileServer(attributes))
 
         return final_results
@@ -228,8 +395,8 @@ class NetRequester(LDAPRPCRequester):
                 for remote_server in dfs.remoteservername:
                     remote_server = str(remote_server)
                     if '\\' in remote_server:
-                        attributes = {'name': [dfs.name.encode('utf-8')],
-                                'remoteservername': [remote_server.split('\\')[2].encode('utf-8')]}
+                        attributes = {'name': dfs.name,
+                                'remoteservername': remote_server.split('\\')[2]}
                         results.append(adobj.DFS(attributes))
 
             return results
@@ -250,8 +417,8 @@ class NetRequester(LDAPRPCRequester):
                 for target in soup_target_list.targets.contents:
                     if '\\' in target.string:
                         server_name, dfs_root = target.string.split('\\')[2:4]
-                        attributes = {'name': ['{}{}'.format(dfs_root, share_name).encode('utf-8')],
-                                'remoteservername': [server_name.encode('utf-8')]}
+                        attributes = {'name': '{}{}'.format(dfs_root, share_name),
+                                'remoteservername': server_name}
 
                 results.append(adobj.DFS(attributes))
 
@@ -338,22 +505,26 @@ class NetRequester(LDAPRPCRequester):
             try:
                 # `--groupname` option is supplied
                 if _groupname:
+                    self._logger.debug('Queried groupname = {}'.format(queried_groupname))
                     groups = self.get_netgroup(queried_groupname=_groupname,
-                                               queried_domain=queried_domain,
+                                               queried_domain=self._queried_domain,
                                                full_data=True)
 
                 # `--groupname` option is missing, falling back to the "Domain Admins"
                 else:
+                    self._logger.debug('No groupname provided, falling back to the "Domain Admins"'.format(queried_groupname))
                     if _sid:
                         queried_sid = _sid
                     else:
                         with pywerview.functions.misc.Misc(self._domain_controller,
                                                            self._domain, self._user,
                                                            self._password, self._lmhash,
-                                                           self._nthash) as misc_requester:
+                                                           self._nthash, self._do_kerberos,
+                                                           self._do_tls) as misc_requester:
                             queried_sid = misc_requester.get_domainsid(queried_domain) + '-512'
+                            self._logger.debug('Found Domains Admins SID = {}'.format(queried_sid))
                     groups = self.get_netgroup(queried_sid=queried_sid,
-                                               queried_domain=queried_domain,
+                                               queried_domain=self._queried_domain,
                                                full_data=True)
             except IndexError:
                 raise ValueError('The group {} was not found'.format(_groupname))
@@ -366,18 +537,20 @@ class NetRequester(LDAPRPCRequester):
                     group_memberof_filter = '(&(samAccountType=805306368)(memberof:1.2.840.113556.1.4.1941:={}){})'.format(group.distinguishedname, custom_filter)
 
                     members = self.get_netuser(custom_filter=group_memberof_filter,
-                                               queried_domain=queried_domain)
+                                               queried_domain=self._queried_domain)
                 else:
                     # TODO: range cycling
                     try:
                         for member in group.member:
                             # RFC 4515, section 3
+                            self._logger.warning('Member name = "{}" will be escaped'.format(member))
                             member = escape_filter_chars(member, encoding='utf-8')
                             dn_filter = '(distinguishedname={}){}'.format(member, custom_filter)
-                            members += self.get_netuser(custom_filter=dn_filter, queried_domain=queried_domain)
-                            members += self.get_netgroup(custom_filter=dn_filter, queried_domain=queried_domain, full_data=True)
+                            members += self.get_netuser(custom_filter=dn_filter, queried_domain=self._queried_domain)
+                            members += self.get_netgroup(custom_filter=dn_filter, queried_domain=self._queried_domain, full_data=True)
                     # The group doesn't have any members
                     except AttributeError:
+                        self._logger.debug('The group doesn\'t have any members')
                         continue
 
                 for member in members:
@@ -390,20 +563,21 @@ class NetRequester(LDAPRPCRequester):
                     try:
                         member_domain = member_dn[member_dn.index('DC='):].replace('DC=', '').replace(',', '.')
                     except IndexError:
+                        self._logger.warning('Exception was raised while handling member_dn, falling back to empty string')
                         member_domain = str()
-                    is_group = (member.samaccounttype != '805306368')
+                    is_group = (member.samaccounttype != 805306368)
 
                     attributes = dict()
                     if queried_domain:
                         attributes['groupdomain'] = queried_domain
                     else:
-                        attributes['groupdomain'] = self._domain
+                        attributes['groupdomain'] = self._queried_domain
                     attributes['groupname'] = group.name
                     attributes['membername'] = member.samaccountname
                     attributes['memberdomain'] = member_domain
                     attributes['isgroup'] = is_group
                     attributes['memberdn'] = member_dn
-                    attributes['membersid'] = member.objectsid
+                    attributes['objectsid'] = member.objectsid
 
                     final_member.add_attributes(attributes)
 
@@ -604,14 +778,14 @@ class NetRequester(LDAPRPCRequester):
                             member_handle = resp['UserHandle']
                             attributes['isgroup'] = False
                             resp = samr.hSamrQueryInformationUser(self._rpc_connection, member_handle)
-                            attributes['name'] = '{}/{}'.format(member_domain, resp['Buffer']['General']['UserName'])
+                            attributes['name'] = '{}\\{}'.format(member_domain, resp['Buffer']['General']['UserName'])
                         except DCERPCSessionError:
                             resp = samr.hSamrOpenAlias(self._rpc_connection, domain_handle, aliasId=member_rid)
                             member_handle = resp['AliasHandle']
                             attributes['isgroup'] = True
                             resp = samr.hSamrQueryInformationAlias(self._rpc_connection, member_handle)
-                            attributes['name'] = '{}/{}'.format(member_domain, resp['Buffer']['General']['Name'])
-                        attributes['lastlogin'] = str()
+                            attributes['name'] = '{}\\{}'.format(member_domain, resp['Buffer']['General']['Name'])
+                        attributes['lastlogon'] = str()
                         break
                 # It's a domain member
                 else:
@@ -622,26 +796,29 @@ class NetRequester(LDAPRPCRequester):
                             member_dn = ad_object.distinguishedname
                             member_domain = member_dn[member_dn.index('DC='):].replace('DC=', '').replace(',', '.')
                             try:
-                                attributes['name'] = '{}/{}'.format(member_domain, ad_object.samaccountname)
+                                attributes['name'] = '{}\\{}'.format(member_domain, ad_object.samaccountname)
                             except AttributeError:
                                 # Here, the member is a foreign security principal
                                 # TODO: resolve it properly
-                                attributes['name'] = '{}/{}'.format(member_domain, ad_object.objectsid)
-                            attributes['isgroup'] = ad_object.isgroup
+                                self._logger.warning('The member is a foreign security principal, SID will not be resolved')
+                                attributes['name'] = '{}\\{}'.format(member_domain, ad_object.objectsid)
+                            attributes['isgroup'] = 'group' in ad_object.objectclass
                             try:
-                                attributes['lastlogin'] = ad_object.lastlogon
+                                attributes['lastlogon'] = ad_object.lastlogon
                             except AttributeError:
-                                attributes['lastlogin'] = str()
+                                self._logger.warning('lastlogon is not set, falling back to empty string')
+                                attributes['lastlogon'] = str()
                         except IndexError:
                             # We did not manage to resolve this SID against the DC
+                            self._logger.warning('We did not manage to resolve this SID ({}) against the DC'.format(member_sid))
                             attributes['isdomain'] = False
                             attributes['isgroup'] = False
                             attributes['name'] = attributes['sid']
-                            attributes['lastlogin'] = str()
+                            attributes['lastlogon'] = str()
                     else:
                         attributes['isgroup'] = False
                         attributes['name'] = str()
-                        attributes['lastlogin'] = str()
+                        attributes['lastlogon'] = str()
 
                 results.append(rpcobj.RPCObject(attributes))
 
@@ -653,14 +830,17 @@ class NetRequester(LDAPRPCRequester):
                         domain_member_attributes['isdomain'] = True
                         member_dn = domain_member.distinguishedname
                         member_domain = member_dn[member_dn.index('DC='):].replace('DC=', '').replace(',', '.')
-                        domain_member_attributes['name'] = '{}/{}'.format(member_domain, domain_member.samaccountname)
+                        domain_member_attributes['name'] = '{}\\{}'.format(member_domain, domain_member.samaccountname)
                         domain_member_attributes['isgroup'] = domain_member.isgroup
                         domain_member_attributes['isdomain'] = True
+                        # TODO: Nope, maybe here we can call get-netdomaincontroller ?
+                        # Need to check in powerview
                         domain_member_attributes['server'] = attributes['name']
                         domain_member_attributes['sid'] = domain_member.objectsid
                         try:
                             domain_member_attributes['lastlogin'] = ad_object.lastlogon
                         except AttributeError:
+                            self._logger.warning('lastlogon is not set, falling back to empty string')
                             domain_member_attributes['lastlogin'] = str()
                         results.append(rpcobj.RPCObject(domain_member_attributes))
 
@@ -691,7 +871,7 @@ class NetRequester(LDAPRPCRequester):
 
     @LDAPRPCRequester._wmi_connection_init()
     def get_userevent(self, event_type=['logon', 'tgt'], date_start=5):
-        limit_date = (datetime.datetime.today() - datetime.timedelta(days=date_start)).strftime('%Y%m%d%H%M%S.%f-000')
+        limit_date = (datetime.today() - timedelta(days=date_start)).strftime('%Y%m%d%H%M%S.%f-000')
         if event_type == ['logon']:
             where_clause = 'EventCode=4624'
         elif event_type == ['tgt']:
@@ -708,7 +888,7 @@ class NetRequester(LDAPRPCRequester):
                 wmi_event = wmi_enum_event.Next(0xffffffff, 1)[0]
                 wmi_event_type = wmi_event.EventIdentifier
                 wmi_event_info = wmi_event.InsertionStrings
-                time = datetime.datetime.strptime(wmi_event.TimeGenerated, '%Y%m%d%H%M%S.%f-000')
+                time = datetime.strptime(wmi_event.TimeGenerated, '%Y%m%d%H%M%S.%f-000')
                 if wmi_event_type == 4624:
                     logon_type = int(wmi_event_info[8])
                     user = wmi_event_info[5]
diff --git a/pywerview/objects/adobjects.py b/pywerview/objects/adobjects.py
index 908fee8..41d23bf 100644
--- a/pywerview/objects/adobjects.py
+++ b/pywerview/objects/adobjects.py
@@ -13,141 +13,105 @@
 # You should have received a copy of the GNU General Public License
 # along with PywerView.  If not, see <http://www.gnu.org/licenses/>.
 
-# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021
+# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2022
 
-from datetime import datetime, timedelta
 import inspect
-import struct
-import pyasn1
-import codecs
+import logging
+
+from impacket.ldap.ldaptypes import ACE, ACCESS_ALLOWED_OBJECT_ACE, ACCESS_MASK, LDAP_SID, SR_SECURITY_DESCRIPTOR
+
+import pywerview.functions.misc as misc
 
 class ADObject:
-    __uac_flags = {0x0000001: 'SCRIPT',
-                   0x0000002: 'ACCOUNTDISABLE',
-                   0x0000008: 'HOMEDIR_REQUIRED',
-                   0x0000010: 'LOCKOUT',
-                   0x0000020: 'PASSWD_NOTREQD',
-                   0x0000040: 'PASSWD_CANT_CHANGE',
-                   0x0000080: 'ENCRYPTED_TEXT_PWD_ALLOWED',
-                   0x0000100: 'TEMP_DUPLICATE_ACCOUNT',
-                   0x0000200: 'NORMAL_ACCOUNT',
-                   0x0000800: 'INTERDOMAIN_TRUST_ACCOUNT',
-                   0x0001000: 'WORKSTATION_TRUST_ACCOUNT',
-                   0x0002000: 'SERVER_TRUST_ACCOUNT',
-                   0x0010000: 'DONT_EXPIRE_PASSWORD',
-                   0x0020000: 'MNS_LOGON_ACCOUNT',
-                   0x0040000: 'SMARTCARD_REQUIRED',
-                   0x0080000: 'TRUSTED_FOR_DELEGATION',
-                   0x0100000: 'NOT_DELEGATED',
-                   0x0200000: 'USE_DES_KEY_ONLY',
-                   0x0400000: 'DONT_REQ_PREAUTH',
-                   0x0800000: 'PASSWORD_EXPIRED',
-                   0x1000000: 'TRUSTED_TO_AUTH_FOR_DELEGATION',
-                   0x4000000: 'PARTIAL_SECRETS_ACCOUNT'}
+    _well_known_sids = {'S-1-0-0': 'Nobody', 'S-1-0': 'Null Authority', 'S-1-1-0': 'Everyone',
+                        'S-1-1': 'World Authority', 'S-1-2-0': 'Local', 'S-1-2-1': 'Console Logon',
+                        'S-1-2': 'Local Authority', 'S-1-3-0': 'Creator Owner', 'S-1-3-1': 'Creator Group',
+                        'S-1-3-2': 'Creator Owner Server', 'S-1-3-3': 'Creator Group Server', 'S-1-3-4': 'Owner Rights',
+                        'S-1-3': 'Creator Authority', 'S-1-4': 'Non-unique Authority', 'S-1-5-10': 'Principal Self',
+                        'S-1-5-11': 'Authenticated Users', 'S-1-5-12': 'Restricted Code', 'S-1-5-13': 'Terminal Server Users',
+                        'S-1-5-14': 'Remote Interactive Logon', 'S-1-5-17': 'This Organization', 'S-1-5-18': 'Local System',
+                        'S-1-5-19': 'NT Authority', 'S-1-5-1': 'Dialup', 'S-1-5-20': 'NT Authority',
+                        'S-1-5-2': 'Network', 'S-1-5-32-546': 'Guests', 'S-1-5-32-547': 'Power Users',
+                        'S-1-5-32-551': 'Backup Operators', 'S-1-5-32-555': 'Builtin\\Remote Desktop Users',
+                        'S-1-5-32-556': 'Builtin\\Network Configuration Operators',
+                        'S-1-5-32-557': 'Builtin\\Incoming Forest Trust Builders',
+                        'S-1-5-32-558': 'Builtin\\Performance Monitor Users',
+                        'S-1-5-32-559': 'Builtin\\Performance Log Users',
+                        'S-1-5-32-560': 'Builtin\\Windows Authorization Access Group',
+                        'S-1-5-32-561': 'Builtin\\Terminal Server License Servers',
+                        'S-1-5-32-562': 'Builtin\\Distributed COM Users',
+                        'S-1-5-32-569': 'Builtin\\Cryptographic Operators',
+                        'S-1-5-32-573': 'Builtin\\Event Log Readers',
+                        'S-1-5-32-574': 'Builtin\\Certificate Service DCOM Access',
+                        'S-1-5-32-575': 'Builtin\\RDS Remote Access Servers',
+                        'S-1-5-32-576': 'Builtin\\RDS Endpoint Servers',
+                        'S-1-5-32-577': 'Builtin\\RDS Management Servers',
+                        'S-1-5-32-578': 'Builtin\\Hyper-V Administrators',
+                        'S-1-5-32-579': 'Builtin\\Access Control Assistance Operators',
+                        'S-1-5-32-580': 'Builtin\\Remote Management Users',
+                        'S-1-5-32-582': 'Storage Replica Administrators',
+                        'S-1-5-3': 'Batch', 'S-1-5-4': 'Interactive', 'S-1-5-64-10': 'NTLM Authentication',
+                        'S-1-5-64-14': 'SChannel Authentication', 'S-1-5-64-21': 'Digest Authentication',
+                        'S-1-5-6': 'Service', 'S-1-5-7': 'Anonymous', 'S-1-5-80-0': 'NT Services\\All Services',
+                        'S-1-5-80': 'NT Service', 'S-1-5-8': 'Proxy', 'S-1-5-9': 'Enterprise Domain Controllers',
+                        'S-1-5': 'NT Authority'}
 
     def __init__(self, attributes):
+        logger = logging.getLogger('pywerview_main_logger.ADObject')
+        logger.ULTRA = 5
+        self._logger = logger
+
+        self._attributes_dict = dict()
         self.add_attributes(attributes)
 
     def add_attributes(self, attributes):
+        self._logger.log(self._logger.ULTRA,'ADObject instancied with the following attributes : {}'.format(attributes))
         for attr in attributes:
-            #print(attr)
-            #print(attributes[attr], attr)
-            t = str(attr).lower()
-            if t in ('logonhours', 'msds-generationid'):
-                value = bytes(attributes[attr][0])
-                value = [x for x in value]
-            elif t in ('trustattributes', 'trustdirection', 'trusttype'):
-                value = int(attributes[attr][0])
-            elif t in ('objectsid', 'ms-ds-creatorsid'):
-                value = codecs.encode(bytes(attributes[attr][0]),'hex')
-                init_value = bytes(attributes[attr][0])
-                value = 'S-{0}-{1}'.format(init_value[0], init_value[1])
-                for i in range(8, len(init_value), 4):
-                    value += '-{}'.format(str(struct.unpack('<I', init_value[i:i+4])[0]))
-            elif t == 'objectguid':
-                init_value = bytes(attributes[attr][0])
-                value = str()
-                value += '{}-'.format(hex(struct.unpack('<I', init_value[0:4])[0])[2:].zfill(8))
-                value += '{}-'.format(hex(struct.unpack('<H', init_value[4:6])[0])[2:].zfill(4))
-                value += '{}-'.format(hex(struct.unpack('<H', init_value[6:8])[0])[2:].zfill(4))
-                value += '{}-'.format((codecs.encode(init_value,'hex')[16:20]).decode('utf-8'))
-                value += init_value.hex()[20:]
-            elif t in ('dscorepropagationdata', 'whenchanged', 'whencreated'):
-                value = list()
-                for val in attributes[attr]:
-                    value.append(str(datetime.strptime(str(val.decode('utf-8')), '%Y%m%d%H%M%S.0Z')))
-            elif t in ('accountexpires', 'pwdlastset', 'badpasswordtime', 'lastlogontimestamp', 'lastlogon', 'lastlogoff'):
-                try:
-                    filetimestamp = int(attributes[attr][0].decode('utf-8'))
-                    if filetimestamp != 9223372036854775807:
-                        timestamp = (filetimestamp - 116444736000000000)/10000000
-                        value = datetime.fromtimestamp(0) + timedelta(seconds=timestamp)
-                    else:
-                        value = 'never'
-                except IndexError:
-                    value = 'empty'
-            elif t == 'isgroup':
-                value = attributes[attr]
-            elif t == 'objectclass':
-                value = [x.decode('utf-8') for x in attributes[attr]]
-                setattr(self, 'isgroup', ('group' in value))
-            elif len(attributes[attr]) > 1:
-                try:
-                    value = [x.decode('utf-8') for x in attributes[attr]]
-                except (UnicodeDecodeError):
-                    value = [x for x in attributes[attr]]
-                except (AttributeError):
-                    value = attributes[attr]
-            else:
-                try:
-                    value = attributes[attr][0].decode('utf-8')
-                except (IndexError):
-                    value = str()
-                except (UnicodeDecodeError):
-                    value = attributes[attr][0]
+            self._attributes_dict[attr.lower()] = attributes[attr]
 
-            setattr(self, t, value)
+    def __getattr__(self, attr):
+        try:
+            return self._attributes_dict[attr]
+        except KeyError:
+            if attr == 'isgroup':
+                try:
+                    return 'group' in self._attributes_dict['objectclass']
+                except KeyError:
+                    return False
+            raise AttributeError
 
+    # In this method, we try to pretty print common AD attributes
     def __str__(self):
         s = str()
-        members = inspect.getmembers(self, lambda x: not(inspect.isroutine(x)))
         max_length = 0
-        for member in members:
-            if not member[0].startswith('_'):
-                if len(member[0]) > max_length:
-                    max_length = len(member[0])
-        for member in members:
-            if not member[0].startswith('_'):
-                if member[0] == 'msmqdigests':
-                    member_value = (',\n' + ' ' * (max_length + 2)).join(x.hex() for x in member[1])
-                elif member[0] == 'useraccountcontrol':
-                    member_value = list()
-                    for uac_flag, uac_label in ADObject.__uac_flags.items():
-                        if int(member[1]) & uac_flag == uac_flag:
-                            member_value.append(uac_label)
-                elif isinstance(member[1], list):
-                    if member[0] in ('logonhours',):
-                        member_value = member[1]
-                    elif member[0] in ('usercertificate',
-                                       'protocom-sso-entries', 'protocom-sso-security-prefs',):
-                        member_value = (',\n' + ' ' * (max_length + 2)).join(
-                                '{}...'.format(x.hex()[:100]) for x in member[1])
-                    else:
-                        member_value = (',\n' + ' ' * (max_length + 2)).join(str(x) for x in member[1])
-                elif member[0] in('msmqsigncertificates', 'userparameters',
-                                  'jpegphoto', 'thumbnailphoto', 'usercertificate',
-                                  'msexchmailboxguid', 'msexchmailboxsecuritydescriptor',
-                                  'msrtcsip-userroutinggroupid', 'msexchumpinchecksum',
-                                  'protocom-sso-auth-data', 'protocom-sso-entries-checksum',
-                                  'protocom-sso-security-prefs-checksum', ):
-                    # Attribut exists but it is empty
-                    try:
-                        member_value = '{}...'.format(member[1].hex()[:100])
-                    except AttributeError:
-                        member_value = ''
-                else:
-                    member_value = member[1]
-                s += '{}: {}{}\n'.format(member[0], ' ' * (max_length - len(member[0])), member_value)
+        for attr in self._attributes_dict:
+            if len(attr) > max_length:
+                max_length = len(attr)
+        for attr in self._attributes_dict:
+            attribute = self._attributes_dict[attr]
+            self._logger.log(self._logger.ULTRA,'Trying to print : attribute name = {0} / value = {1}'.format(attr, attribute))
+            if isinstance(attribute, list):
+                if any(isinstance(x, bytes) for x in attribute):
+                    attribute = ['{}...'.format(x.hex()[:97]) for x in attribute]
+                attribute_temp = ', '.join(str(x) for x in attribute)
+                if len(attribute_temp) > 100:
+                    attribute_temp = str()
+                    line = str()
+                    for x in attribute:
+                        if len(line) + len(str(x)) <= 100:
+                            line += '{}, '.format(x)
+                        else:
+                            attribute_temp += line + '\n' + ' ' * (max_length + 2)
+                            line = str()
+                            line += '{}, '.format(x)
+                    attribute_temp += line + '\n' + ' ' * (max_length + 2)
+                attribute = attribute_temp.rstrip().rstrip(',')
+            elif isinstance(attribute, bytes):
+                attribute = '{}...'.format(attribute.hex()[:100])
+            elif isinstance(attribute, ADObject):
+                attribute = ('\n' + str(attribute)).replace('\n', '\n\t')
+
+            s += '{}: {}{}\n'.format(attr, ' ' * (max_length - len(attr)), attribute)
 
         s = s[:-1]
         return s
@@ -155,23 +119,22 @@ class ADObject:
     def __repr__(self):
         return str(self)
 
-class User(ADObject):
+    def to_json(self):
+        return self._attributes_dict
+
+class ACE(ADObject):
+
     def __init__(self, attributes):
         ADObject.__init__(self, attributes)
-        for attr in filter(lambda _: _ in attributes, ('homedirectory',
-                                                       'scriptpath',
-                                                       'profilepath')):
-            if not hasattr(self, attr):
-                setattr(self, attr, str())
+
+        # We set iscallback, depending on the type of ACE
+        self._attributes_dict['iscallbak'] = ('CALLBACK' in self.acetype)
+
+class User(ADObject):
+    pass
 
 class Group(ADObject):
-    def __init__(self, attributes):
-        ADObject.__init__(self, attributes)
-        try:
-            if not isinstance(self.member, list):
-                self.member = [self.member]
-        except AttributeError:
-            pass
+    pass
 
 class Computer(ADObject):
     pass
@@ -183,9 +146,7 @@ class DFS(ADObject):
     pass
 
 class OU(ADObject):
-    def __init__(self, attributes):
-        ADObject.__init__(self, attributes)
-        self.distinguishedname = 'LDAP://{}'.format(self.distinguishedname)
+    pass
 
 class Site(ADObject):
     pass
@@ -194,62 +155,58 @@ class Subnet(ADObject):
     pass
 
 class Trust(ADObject):
-    __trust_attrib = {0x1: 'non_transitive', 0x2: 'uplevel_only',
-                      0x4: 'filter_sids', 0x8: 'forest_transitive',
-                      0x10: 'cross_organization', 0x20: 'within_forest',
-                      0x40: 'treat_as_external',
-                      0x80: 'trust_uses_rc4_encryption',
-                      0x100: 'trust_uses_aes_keys',
-                      0X200: 'cross_organization_no_tgt_delegation',
-                      0x400: 'pim_trust'}
-
-    __trust_direction = {0: 'disabled', 1: 'inbound',
-                         2: 'outbound', 3: 'bidirectional'}
-
-    __trust_type = {1: 'windows_non_active_directory',
-                    2: 'windows_active_directory', 3: 'mit'}
 
     def __init__(self, attributes):
-        ad_obj = ADObject(attributes)
-        self.targetname = ad_obj.name
-
-        self.trustdirection = Trust.__trust_direction.get(ad_obj.trustdirection, 'unknown')
-        self.trusttype = Trust.__trust_type.get(ad_obj.trusttype, 'unknown')
-        self.whencreated = ad_obj.whencreated
-        self.whenchanged = ad_obj.whenchanged
-
-        self.trustattributes = list()
-        for attrib_flag, attrib_label in Trust.__trust_attrib.items():
-            if ad_obj.trustattributes & attrib_flag:
-                self.trustattributes.append(attrib_label)
-
+        logger = logging.getLogger('pywerview_main_logger.Trust')
+        self._logger = logger
+        ADObject.__init__(self, attributes)
+        trust_attributes = self.trustattributes
+        trust_direction = self.trustdirection
         # If the filter SIDs attribute is not manually set, we check if we're
         # not in a use case where SIDs are implicitly filtered
         # Based on https://github.com/vletoux/pingcastle/blob/master/Healthcheck/TrustAnalyzer.cs
-        if 'filter_sids' not in self.trustattributes:
-            if not (self.trustdirection == 'disabled' or \
-                    self.trustdirection == 'inbound' or \
-                    'within_forest' in self.trustattributes or \
-                    'pim_trust' in self.trustattributes):
-                if 'forest_transitive' in self.trustattributes and 'treat_as_external' not in self.trustattributes:
-                    self.trustattributes.append('filter_sids')
-
-class GPO(ADObject):
-    pass
-
-class GptTmpl(ADObject):
+        if 'filter_sids' not in trust_attributes:
+            if not (trust_direction == 'disabled' or \
+                    trust_direction == 'inbound' or \
+                    'within_forest' in trust_attributes or \
+                    'pim_trust' in trust_attributes):
+                if 'forest_transitive' in trust_attributes and 'treat_as_external' not in trust_attributes:
+                    self._attributes_dict['trustattributes'].append('filter_sids')
+
+    # Pretty printing Trust object, we don't want to print all the attributes
+    # so we only print useful ones (trustattributes, trustdirection, trustpartner
+    # trusttype, whenchanged, whencreated)
     def __str__(self):
         s = str()
-        members = inspect.getmembers(self, lambda x: not(inspect.isroutine(x)))
-        for member in members:
-            if not member[0].startswith('_'):
-                s += '{}:\n'.format(member[0])
-                member_value_str = str(member[1])
-                for line in member_value_str.split('\n'):
-                    s += '\t{}\n'.format(line)
+        max_length = len('trustattributes')
+
+        for attr in self._attributes_dict:
+            self._logger.log(self._logger.ULTRA,'Trying to print : attribute name = {0} / value = {1}'.format(attr, self._attributes_dict[attr]))
+            if attr in ('trustpartner', 'trustdirection', 'trusttype', 'whenchanged', 'whencreated'):
+                attribute = self._attributes_dict[attr]
+            elif attr == 'trustattributes':
+                attribute = ', '.join(self._attributes_dict[attr])
+            else:
+                self._logger.debug('Ignoring : attribute name = {0}'.format(attr, self._attributes_dict[attr]))
+                continue
+            s += '{}: {}{}\n'.format(attr, ' ' * (max_length - len(attr)), attribute)
 
         s = s[:-1]
         return s
+    pass
+
+class GPO(ADObject):
+    pass
+
+class PSO(ADObject):
+    pass
+
+class GptTmpl(ADObject):
+    def to_json(self):
+        json_dict = {}
+        for k, v in self._attributes_dict.items():
+            json_dict[k] = v.to_json()
+        return json_dict
 
 class GPOGroup(ADObject):
     pass
@@ -263,3 +220,6 @@ class GPOComputerAdmin(ADObject):
 class GPOLocation(ADObject):
     pass
 
+class GMSAAccount(ADObject):
+    pass
+
diff --git a/pywerview/objects/rpcobjects.py b/pywerview/objects/rpcobjects.py
index 67bbb43..bcc3e53 100644
--- a/pywerview/objects/rpcobjects.py
+++ b/pywerview/objects/rpcobjects.py
@@ -13,14 +13,17 @@
 # You should have received a copy of the GNU General Public License
 # along with PywerView.  If not, see <http://www.gnu.org/licenses/>.
 
-# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021
-
-from __future__ import unicode_literals
+# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2022
 
+import logging
 import inspect
 
 class RPCObject:
     def __init__(self, obj):
+        logger = logging.getLogger('pywerview_main_logger.RPCObject')
+        logger.ULTRA = 5
+        self._logger = logger
+
         attributes = dict()
         try:
             for key in obj.fields.keys():
@@ -30,13 +33,14 @@ class RPCObject:
         self.add_attributes(attributes)
 
     def add_attributes(self, attributes):
+        self._logger.log(self._logger.ULTRA,'RPCObject instancied with the following attributes : {}'.format(attributes))
         for key, value in attributes.items():
             key = key.lower()
             if key in ('wkui1_logon_domain', 'wkui1_logon_server',
                        'wkui1_oth_domains', 'wkui1_username',
                        'sesi10_cname', 'sesi10_username'):
                 value = value.rstrip('\x00')
-            
+
             setattr(self, key.lower(), value)
 
     def __str__(self):
@@ -48,6 +52,7 @@ class RPCObject:
                 if len(member[0]) > max_length:
                     max_length = len(member[0])
         for member in members:
+            self._logger.log(self._logger.ULTRA,'Trying to print : attribute name = {0} / value = {1}'.format(member[0], member[1]))
             if not member[0].startswith('_'):
                 s += '{}: {}{}\n'.format(member[0], ' ' * (max_length - len(member[0])), member[1])
 
@@ -57,6 +62,14 @@ class RPCObject:
     def __repr__(self):
         return str(self)
 
+    def to_json(self):
+        members = inspect.getmembers(self, lambda x: not(inspect.isroutine(x)))
+        results = dict()
+        for member in members:
+            if not member[0].startswith('_'):
+                results[member[0]]=member[1]
+        return(results)
+
 class TargetUser(RPCObject):
     pass
 
diff --git a/pywerview/requester.py b/pywerview/requester.py
index c923b44..f9f7aed 100644
--- a/pywerview/requester.py
+++ b/pywerview/requester.py
@@ -13,13 +13,23 @@
 # You should have received a copy of the GNU General Public License
 # along with PywerView.  If not, see <http://www.gnu.org/licenses/>.
 
-# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021
+# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2022
 
+import sys
+import logging
 import socket
 import ntpath
 import ldap3
+import os
+import tempfile
+
+from ldap3.protocol.formatters.formatters import *
 
 from impacket.smbconnection import SMBConnection
+from impacket.smbconnection import SessionError
+from impacket.krb5.ccache import CCache, Credential, CountedOctetString
+from impacket.krb5 import constants
+from impacket.krb5.types import Principal
 from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY
 from impacket.dcerpc.v5 import transport, wkst, srvs, samr, scmr, drsuapi, epm
 from impacket.dcerpc.v5.dcom import wmi
@@ -27,27 +37,40 @@ from impacket.dcerpc.v5.dtypes import NULL
 from impacket.dcerpc.v5.dcomrt import DCOMConnection
 from impacket.dcerpc.v5.rpcrt import DCERPCException
 
+import pywerview.formatters as fmt
+
 class LDAPRequester():
     def __init__(self, domain_controller, domain=str(), user=(), password=str(),
-                 lmhash=str(), nthash=str()):
+                 lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False):
         self._domain_controller = domain_controller
         self._domain = domain
         self._user = user
         self._password = password
         self._lmhash = lmhash
         self._nthash = nthash
+        self._do_kerberos = do_kerberos
+        self._do_tls = do_tls
         self._queried_domain = None
         self._ads_path = None
         self._ads_prefix = None
         self._ldap_connection = None
         self._base_dn = None
 
+        logger = logging.getLogger('pywerview_main_logger.LDAPRequester')
+        self._logger = logger
+
     def _get_netfqdn(self):
         try:
             smb = SMBConnection(self._domain_controller, self._domain_controller)
         except socket.error:
+            self._logger.warning('Socket error when opening the SMB connection')
             return str()
 
+        self._logger.debug('SMB loging parameters : user = {0}  / password = {1} / domain = {2} '
+                           '/ LM hash = {3} / NT hash = {4}'.format(self._user, self._password,
+                                                                    self._domain, self._lmhash,
+                                                                    self._nthash))
+
         smb.login(self._user, self._password, domain=self._domain,
                 lmhash=self._lmhash, nthash=self._nthash)
         fqdn = smb.getServerDNSDomainName()
@@ -55,13 +78,50 @@ class LDAPRequester():
 
         return fqdn
 
+    def _patch_spn(self, creds, principal):
+        self._logger.debug('Patching principal to {}'.format(principal))
+
+        from pyasn1.codec.der import decoder, encoder
+        from impacket.krb5.asn1 import TGS_REP, Ticket
+
+        # Code is ~~based on~~ stolen from https://github.com/SecureAuthCorp/impacket/pull/1256
+        tgs = creds.toTGS(principal)
+        decoded_st = decoder.decode(tgs['KDC_REP'], asn1Spec=TGS_REP())[0]
+        decoded_st['ticket']['sname']['name-string'][0] = 'ldap'
+        decoded_st['ticket']['sname']['name-string'][1] = self._domain_controller.lower()
+        decoded_st['ticket']['realm'] = self._queried_domain.upper()
+
+        new_creds = Credential(data=creds.getData())
+        new_creds.ticket = CountedOctetString()
+        new_creds.ticket['data'] = encoder.encode(decoded_st['ticket'].clone(tagSet=Ticket.tagSet, cloneValueFlag=True))
+        new_creds.ticket['length'] = len(new_creds.ticket['data'])
+        new_creds['server'].fromPrincipal(Principal(principal, type=constants.PrincipalNameType.NT_PRINCIPAL.value))
+
+        return new_creds
+
     def _create_ldap_connection(self, queried_domain=str(), ads_path=str(),
                                 ads_prefix=str()):
         if not self._domain:
-            self._domain = self._get_netfqdn()
+            if self._do_kerberos:
+                ccache = CCache.loadFile(os.getenv('KRB5CCNAME'))
+                self._domain = ccache.principal.realm['data'].decode('utf-8')
+            else:
+                try:
+                    self._domain = self._get_netfqdn()
+                except SessionError as e:
+                    self._logger.critical(e)
+                    sys.exit(-1)
 
         if not queried_domain:
-            queried_domain = self._get_netfqdn()
+            if self._do_kerberos:
+                ccache = CCache.loadFile(os.getenv('KRB5CCNAME'))
+                queried_domain = ccache.principal.realm['data'].decode('utf-8')
+            else:
+                try:
+                    queried_domain = self._get_netfqdn()
+                except SessionError as e:
+                    self._logger.critical(e)
+                    sys.exit(-1)
         self._queried_domain = queried_domain
 
         base_dn = str()
@@ -82,68 +142,146 @@ class LDAPRequester():
         # base_dn is no longer used within `_create_ldap_connection()`, but I don't want to break
         # the function call. So we store it in an attriute and use it in `_ldap_search()`
         self._base_dn = base_dn
-        
+
         # Format the username and the domain
         # ldap3 seems not compatible with USER@DOMAIN format
-        user = '{}\\{}'.format(self._domain, self._user)
-        
-        # Choose between password or pth  
-        if self._lmhash and self._nthash:
-            lm_nt_hash  = '{}:{}'.format(self._lmhash, self._nthash)
-            
-            ldap_server = ldap3.Server('ldap://{}'.format(self._domain_controller))
-            ldap_connection = ldap3.Connection(ldap_server, user, lm_nt_hash, 
-                                               authentication=ldap3.NTLM, raise_exceptions=True)
-            
-            try:
-                ldap_connection.bind()
-            except ldap3.core.exceptions.LDAPStrongerAuthRequiredResult:
-                # We need to try SSL (pth version)
-                ldap_server = ldap3.Server('ldaps://{}'.format(self._domain_controller))
-                ldap_connection = ldap3.Connection(ldap_server, user, lm_nt_hash, 
-                                                   authentication=ldap3.NTLM, raise_exceptions=True)
-
-                ldap_connection.bind()
-
+        if self._do_kerberos:
+            user = '{}@{}'.format(self._user, self._domain.upper())
+        else:
+            user = '{}\\{}'.format(self._domain, self._user)
+
+        # Call custom formatters for several AD attributes
+        formatter = {'userAccountControl': fmt.format_useraccountcontrol,
+                'trustType': fmt.format_trusttype,
+                'trustDirection': fmt.format_trustdirection,
+                'trustAttributes': fmt.format_trustattributes,
+                'msDS-MaximumPasswordAge': format_ad_timedelta,
+                'msDS-MinimumPasswordAge': format_ad_timedelta,
+                'msDS-LockoutDuration': format_ad_timedelta,
+                'msDS-LockoutObservationWindow': format_ad_timedelta,
+                'msDS-GroupMSAMembership': fmt.format_groupmsamembership,
+                'msDS-ManagedPassword': fmt.format_managedpassword}
+
+        if self._do_tls:
+            ldap_scheme = 'ldaps'
+            self._logger.debug('LDAPS connection forced')
         else:
-            ldap_server = ldap3.Server('ldap://{}'.format(self._domain_controller))
-            ldap_connection = ldap3.Connection(ldap_server, user, self._password,
-                                               authentication=ldap3.NTLM, raise_exceptions=True)
+            ldap_scheme = 'ldap'
+        ldap_server = ldap3.Server('{}://{}'.format(ldap_scheme, self._domain_controller), formatter=formatter)
+        ldap_connection_kwargs = {'user': user, 'raise_exceptions': True}
+
+        # We build the authentication arguments depending on auth mode
+        if self._do_kerberos:
+            self._logger.debug('LDAP authentication with Keberos')
+            ldap_connection_kwargs['authentication'] = ldap3.SASL
+            ldap_connection_kwargs['sasl_mechanism'] = ldap3.KERBEROS
+
+            # Verifying if we have the correct TGS/TGT to interrogate the LDAP server
+            ccache = CCache.loadFile(os.getenv('KRB5CCNAME'))
+            principal = 'ldap/{}@{}'.format(self._domain_controller.lower(), self._queried_domain.upper())
+
+            # We look for the TGS with the right SPN
+            creds = ccache.getCredential(principal, anySPN=False)
+            if creds:
+                self._logger.debug('TGS found in KRB5CCNAME file')
+                if creds['server'].prettyPrint().lower() != creds['server'].prettyPrint():
+                    self._logger.debug('SPN not in lowercase, patching SPN')
+                    new_creds = self._patch_spn(creds, principal)
+                    # We build a new CCache with the new ticket
+                    ccache.credentials.append(new_creds)
+                    temp_ccache = tempfile.NamedTemporaryFile()
+                    ccache.saveFile(temp_ccache.name)
+                    cred_store = {'ccache': 'FILE:{}'.format(temp_ccache.name)}
+                else:
+                    cred_store = dict()
+            else:
+                self._logger.debug('TGS not found in KRB5CCNAME, looking for '
+                        'TGS with alternative SPN')
+                # If we don't find it, we search for any SPN
+                creds = ccache.getCredential(principal, anySPN=True)
+                if creds:
+                    # If we find one, we build a custom TGS
+                    self._logger.debug('Alternative TGS found, patching SPN')
+                    new_creds = self._patch_spn(creds, principal)
+                    # We build a new CCache with the new ticket
+                    ccache.credentials.append(new_creds)
+                    temp_ccache = tempfile.NamedTemporaryFile()
+                    ccache.saveFile(temp_ccache.name)
+                    cred_store = {'ccache': 'FILE:{}'.format(temp_ccache.name)}
+                else:
+                    # If we don't find any, we hope for the best (TGT in cache)
+                    self._logger.debug('Alternative TGS not found, using KRB5CCNAME as is '
+                            'while hoping it contains a TGT')
+                    cred_store = dict()
+            ldap_connection_kwargs['cred_store'] = cred_store
+            self._logger.debug('LDAP binding parameters: server = {0} / user = {1} '
+                   '/ Kerberos auth'.format(self._domain_controller, user))
+        else:
+            self._logger.debug('LDAP authentication with NTLM')
+            ldap_connection_kwargs['authentication'] = ldap3.NTLM
+            if self._lmhash and self._nthash:
+                ldap_connection_kwargs['password'] = '{}:{}'.format(self._lmhash, self._nthash)
+                self._logger.debug('LDAP binding parameters: server = {0} / user = {1} '
+                   '/ hash = {2}'.format(self._domain_controller, user, ldap_connection_kwargs['password']))
+            else:
+                ldap_connection_kwargs['password'] = self._password
+                self._logger.debug('LDAP binding parameters: server = {0} / user = {1} '
+                   '/ password = {2}'.format(self._domain_controller, user, ldap_connection_kwargs['password']))
 
+        try:
+            ldap_connection = ldap3.Connection(ldap_server, **ldap_connection_kwargs)
             try:
                 ldap_connection.bind()
-            except ldap3.core.exceptions.LDAPStrongerAuthRequiredResult:
-                # We nedd to try SSL (password version)
-                ldap_server = ldap3.Server('ldaps://{}'.format(self._domain_controller))
-                ldap_connection = ldap3.Connection(ldap_server, user, self._password,
-                                                   authentication=ldap3.NTLM, raise_exceptions=True)        
-                
+            except ldap3.core.exceptions.LDAPSocketOpenError as e:
+                self._logger.critical(e)
+                if self._do_tls:
+                    self._logger.critical('TLS negociation failed, this error is mostly due to your host '
+                                          'not supporting SHA1 as signing algorithm for certificates')
+                sys.exit(-1)
+        except ldap3.core.exceptions.LDAPStrongerAuthRequiredResult:
+            # We need to try TLS
+            self._logger.warning('Server returns LDAPStrongerAuthRequiredResult, falling back to LDAPS')
+            ldap_server = ldap3.Server('ldaps://{}'.format(self._domain_controller), formatter=formatter)
+            ldap_connection = ldap3.Connection(ldap_server, **ldap_connection_kwargs)
+            try:
                 ldap_connection.bind()
+            except ldap3.core.exceptions.LDAPSocketOpenError as e:
+                self._logger.critical(e)
+                self._logger.critical('TLS negociation failed, this error is mostly due to your host '
+                                      'not supporting SHA1 as signing algorithm for certificates')
+                sys.exit(-1)
 
         self._ldap_connection = ldap_connection
 
-    def _ldap_search(self, search_filter, class_result, attributes=list()):
+    def _ldap_search(self, search_filter, class_result, attributes=list(), controls=list()):
         results = list()
-       
-        # if no attribute name specified, we return all attributes 
+
+        # if no attribute name specified, we return all attributes
         if not attributes:
-            attributes =  ldap3.ALL_ATTRIBUTES 
-
-        try: 
-            # Microsoft Active Directory set an hard limit of 1000 entries returned by any search
-            search_results=self._ldap_connection.extend.standard.paged_search(search_base=self._base_dn,
-                    search_filter=search_filter, attributes=attributes,
-                    paged_size=1000, generator=True)
-        # TODO: for debug only
-        except Exception as e:
-            import sys
-            print('Except: ', sys.exc_info()[0])
+            attributes =  ldap3.ALL_ATTRIBUTES
+
+        self._logger.debug('search_base = {0} / search_filter = {1} / attributes = {2}'.format(self._base_dn,
+                                                                                               search_filter,
+                                                                                               attributes))
+
+        # Microsoft Active Directory set an hard limit of 1000 entries returned by any search
+        search_results=self._ldap_connection.extend.standard.paged_search(search_base=self._base_dn,
+                search_filter=search_filter, attributes=attributes,
+                controls=controls, paged_size=1000, generator=True)
+
+        try:
+            # Skip searchResRef
+            for result in search_results:
+                if result['type'] != 'searchResEntry':
+                    continue
+                results.append(class_result(result['attributes']))
+
+        except ldap3.core.exceptions.LDAPAttributeError as e:
+            self._logger.critical(e)
+            sys.exit(-1)
 
-        # Skip searchResRef
-        for result in search_results:
-            if result['type'] is not 'searchResEntry':
-                continue
-            results.append(class_result(result['raw_attributes']))
+        if not results:
+            self._logger.debug('Query returned an empty result')
 
         return results
 
@@ -173,23 +311,28 @@ class LDAPRequester():
         try:
             self._ldap_connection.unbind()
         except AttributeError:
+            self._logger.warning('Error when unbinding')
             pass
         self._ldap_connection = None
 
 class RPCRequester():
     def __init__(self, target_computer, domain=str(), user=(), password=str(),
-                 lmhash=str(), nthash=str()):
+                 lmhash=str(), nthash=str(), do_kerberos=False):
         self._target_computer = target_computer
         self._domain = domain
         self._user = user
         self._password = password
         self._lmhash = lmhash
         self._nthash = nthash
+        self._do_kerberos = do_kerberos
         self._pipe = None
         self._rpc_connection = None
         self._dcom = None
         self._wmi_connection = None
 
+        logger = logging.getLogger('pywerview_main_logger.RPCRequester')
+        self._logger = logger
+
     def _create_rpc_connection(self, pipe):
         # Here we build the DCE/RPC connection
         self._pipe = pipe
@@ -213,7 +356,7 @@ class RPCRequester():
             rpctransport = transport.SMBTransport(self._target_computer, 445, self._pipe,
                                                   username=self._user, password=self._password,
                                                   domain=self._domain, lmhash=self._lmhash,
-                                                  nthash=self._nthash)
+                                                  nthash=self._nthash, doKerberos=self._do_kerberos)
 
         rpctransport.set_connect_timeout(10)
         dce = rpctransport.get_dce_rpc()
@@ -223,7 +366,9 @@ class RPCRequester():
 
         try:
             dce.connect()
-        except socket.error:
+        except Exception as e:
+            self._logger.critical('Error when creating RPC connection')
+            self._logger.critical(e)
             self._rpc_connection = None
         else:
             dce.bind(binding_strings[self._pipe[1:]])
@@ -232,8 +377,10 @@ class RPCRequester():
     def _create_wmi_connection(self, namespace='root\\cimv2'):
         try:
             self._dcom = DCOMConnection(self._target_computer, self._user, self._password,
-                                        self._domain, self._lmhash, self._nthash)
-        except DCERPCException:
+                                        self._domain, self._lmhash, self._nthash, doKerberos=self._do_kerberos)
+        except Exception as e:
+            self._logger.critical('Error when creating WMI connection')
+            self._logger.critical(e)
             self._dcom = None
         else:
             i_interface = self._dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login,
@@ -284,15 +431,20 @@ class RPCRequester():
 
 class LDAPRPCRequester(LDAPRequester, RPCRequester):
     def __init__(self, target_computer, domain=str(), user=(), password=str(),
-                 lmhash=str(), nthash=str(), domain_controller=str()):
+                 lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False,
+                 domain_controller=str()):
         # If no domain controller was given, we assume that the user wants to
         # target a domain controller to perform LDAP requests against
         if not domain_controller:
             domain_controller = target_computer
         LDAPRequester.__init__(self, domain_controller, domain, user, password,
-                               lmhash, nthash)
+                               lmhash, nthash, do_kerberos, do_tls)
         RPCRequester.__init__(self, target_computer, domain, user, password,
-                               lmhash, nthash)
+                               lmhash, nthash, do_kerberos)
+
+        logger = logging.getLogger('pywerview_main_logger.LDAPRPCRequester')
+        self._logger = logger
+
     def __enter__(self):
         try:
             LDAPRequester.__enter__(self)
diff --git a/pywerview/worker/hunting.py b/pywerview/worker/hunting.py
index e052204..bfe9030 100644
--- a/pywerview/worker/hunting.py
+++ b/pywerview/worker/hunting.py
@@ -13,7 +13,7 @@
 # You should have received a copy of the GNU General Public License
 # along with PywerView.  If not, see <http://www.gnu.org/licenses/>.
 
-# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021
+# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2022
 
 from multiprocessing import Process, Pipe
 
@@ -22,7 +22,7 @@ from pywerview.functions.misc import Misc
 import pywerview.objects.rpcobjects as rpcobj
 
 class HunterWorker(Process):
-    def __init__(self, pipe, domain, user, password, lmhash, nthash):
+    def __init__(self, pipe, domain, user, password, lmhash, nthash, do_kerberos):
         Process.__init__(self)
         self._pipe = pipe
         self._domain = domain
@@ -30,6 +30,7 @@ class HunterWorker(Process):
         self._password = password
         self._lmhash = lmhash
         self._nthash = nthash
+        self._do_kerberos = do_kerberos
 
     def terminate(self):
         self._pipe.close()
@@ -42,9 +43,10 @@ class HunterWorker(Process):
             self._pipe.send(result)
 
 class UserHunterWorker(HunterWorker):
-    def __init__(self, pipe, domain, user, password, lmhash, nthash, foreign_users,
-                 stealth, target_users, domain_short_name, check_access):
-        HunterWorker.__init__(self, pipe, domain, user, password, lmhash, nthash)
+    def __init__(self, pipe, domain, user, password, lmhash, nthash, do_kerberos,
+            foreign_users, stealth, target_users, domain_short_name, check_access):
+        HunterWorker.__init__(self, pipe, domain, user, password, lmhash,
+                nthash, do_kerberos)
         self._foreign_users = foreign_users
         self._stealth = stealth
         self._target_users = target_users
@@ -58,7 +60,7 @@ class UserHunterWorker(HunterWorker):
         # First, we get every distant session on the target computer
         distant_sessions = list()
         with NetRequester(target_computer, self._domain, self._user, self._password,
-                          self._lmhash, self._nthash) as net_requester:
+                          self._lmhash, self._nthash, self._do_kerberos) as net_requester:
             if not self._foreign_users:
                 distant_sessions += net_requester.get_netsession()
             if not self._stealth:
@@ -99,7 +101,7 @@ class UserHunterWorker(HunterWorker):
 
                         if self._check_access:
                             with Misc(target_computer, self._domain, self._user, self._password,
-                                              self._lmhash, self._nthash) as misc_requester:
+                                              self._lmhash, self._nthash, self._do_kerberos) as misc_requester:
                                 attributes['localadmin'] = misc_requester.invoke_checklocaladminaccess()
                         else:
                             attributes['localadmin'] = str()
@@ -109,9 +111,9 @@ class UserHunterWorker(HunterWorker):
         return results
 
 class ProcessHunterWorker(HunterWorker):
-    def __init__(self, pipe, domain, user, password, lmhash, nthash, process_name,
-                 target_users):
-        HunterWorker.__init__(self, pipe, domain, user, password, lmhash, nthash)
+    def __init__(self, pipe, domain, user, password, lmhash, nthash, do_kerberos,
+            process_name, target_users):
+        HunterWorker.__init__(self, pipe, domain, user, password, lmhash, nthash, do_kerberos)
         self._process_name = process_name
         self._target_users = target_users
 
@@ -120,7 +122,7 @@ class ProcessHunterWorker(HunterWorker):
 
         distant_processes = list()
         with NetRequester(target_computer, self._domain, self._user, self._password,
-                          self._lmhash, self._nthash) as net_requester:
+                          self._lmhash, self._nthash, self._do_kerberos) as net_requester:
             distant_processes = net_requester.get_netprocess()
 
         for process in distant_processes:
@@ -136,9 +138,9 @@ class ProcessHunterWorker(HunterWorker):
         return results
 
 class EventHunterWorker(HunterWorker):
-    def __init__(self, pipe, domain, user, password, lmhash, nthash, search_days,
-                 target_users):
-        HunterWorker.__init__(self, pipe, domain, user, password, lmhash, nthash)
+    def __init__(self, pipe, domain, user, password, lmhash, nthash, do_kerberos,
+            search_days, target_users):
+        HunterWorker.__init__(self, pipe, domain, user, password, lmhash, nthash, do_kerberos)
         self._target_users = target_users
         self._search_days = search_days
 
@@ -147,7 +149,7 @@ class EventHunterWorker(HunterWorker):
 
         distant_processes = list()
         with NetRequester(target_computer, self._domain, self._user, self._password,
-                          self._lmhash, self._nthash) as net_requester:
+                          self._lmhash, self._nthash, self._do_kerberos) as net_requester:
             distant_events = net_requester.get_userevent(date_start=self._search_days)
 
         for event in distant_events:
@@ -157,3 +159,4 @@ class EventHunterWorker(HunterWorker):
                         results.append(event)
 
         return results
+
diff --git a/requirements.txt b/requirements.txt
index b39a686..d5fff5d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,6 @@
 impacket>=0.9.22
 bs4
 lxml
+pyasn1
+ldap3>=2.8.1
+gssapi
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
index b88034e..08aedd7 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,2 +1,2 @@
 [metadata]
-description-file = README.md
+description_file = README.md
diff --git a/setup.py b/setup.py
index f9e6a30..4a10b07 100644
--- a/setup.py
+++ b/setup.py
@@ -2,16 +2,13 @@
 
 from setuptools import setup, find_packages
 
-try:
-    import pypandoc
-    long_description = pypandoc.convert_file('README.md', 'rst')
-except(IOError, ImportError):
-    long_description = open('README.md').read()
+long_description = open('README.md').read()
 
 setup(name='pywerview',
-    version='0.3.2',
+    version='0.4.0',
     description='A Python port of PowerSploit\'s PowerView',
     long_description=long_description,
+    long_description_content_type='text/markdown',
     dependency_links = ['https://github.com/SecureAuthCorp/impacket/tarball/master#egg=impacket-0.9.22'],
     classifiers=[
         'Environment :: Console',
@@ -30,7 +27,10 @@ setup(name='pywerview',
     install_requires=[
         'impacket>=0.9.22',
         'bs4',
-        'lxml'
+        'lxml',
+        'pyasn1',
+        'ldap3>=2.8.1',
+        'gssapi'
     ],
     entry_points = {
         'console_scripts': ['pywerview=pywerview.cli.main:main'],