├── .github └── workflows │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── TODO.md ├── go.mod ├── go.sum ├── godap.go ├── images └── godap.gif ├── pkg ├── adidns │ ├── colors.go │ ├── formats.go │ └── types.go ├── formats │ └── time.go ├── ldaputils │ ├── actions.go │ ├── emojis.go │ ├── formats.go │ ├── misc.go │ └── vars.go └── sdl │ ├── ADGuids.go │ ├── AceFieldMaps.go │ ├── AceTypeStructures.go │ ├── SDTypeStructures.go │ └── SecurityDescriptorFuncs.go └── tui ├── ace.go ├── cache.go ├── dacl.go ├── dns.go ├── dnsmodify.go ├── explorer.go ├── finder.go ├── gpo.go ├── group.go ├── help.go ├── interface.go ├── main.go ├── main_test.go ├── search.go ├── theme.go └── tree.go /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/release.yaml 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | releases-matrix: 9 | name: Release Go Binary 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | goos: [linux, windows, darwin] 14 | goarch: ["386", amd64, arm64] 15 | exclude: 16 | - goarch: "386" 17 | goos: darwin 18 | - goarch: arm64 19 | goos: windows 20 | steps: 21 | - uses: actions/checkout@v3 22 | - uses: wangyoucao577/go-release-action@v1 23 | with: 24 | github_token: ${{ secrets.GITHUB_TOKEN }} 25 | goos: ${{ matrix.goos }} 26 | goarch: ${{ matrix.goarch }} 27 | extra_files: LICENSE README.md 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | godap 2 | data/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Artur Henrique Marzano Gonzaga 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # godap 2 | 3 | ![GitHub Release](https://img.shields.io/github/v/release/Macmod/godap) ![](https://img.shields.io/github/go-mod/go-version/Macmod/godap) ![](https://img.shields.io/github/languages/code-size/Macmod/godap) ![](https://img.shields.io/github/license/Macmod/godap) ![](https://img.shields.io/github/actions/workflow/status/Macmod/godap/release.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/Macmod/godap)](https://goreportcard.com/report/github.com/Macmod/godap) ![GitHub Downloads](https://img.shields.io/github/downloads/Macmod/godap/total) [Twitter Follow](https://twitter.com/MacmodSec) 4 | 5 |

A complete TUI for LDAP.

6 | 7 | ![Demo](images/godap.gif) 8 | 9 | # Summary 10 | 11 | * [Features](#features) 12 | * [Installation](#installation) 13 | * [Usage](#usage) 14 | * [Flags](#flags) 15 | * [Keybindings](#keybindings) 16 | * [Tree Colors](#tree-colors) 17 | * [Contributing](#contributing) 18 | * [Acknowledgements](#acknowledgements) 19 | * [Disclaimers](#disclaimers) 20 | 21 | # Features 22 | 23 | * 🧩 Supports authentication with password, NTLM hash, Kerberos ticket or PEM/PKCS#12 certificate 24 | * 🗒️ Formats date/time, boolean and other categorical attributes into readable text 25 | * 😎 Pretty colors & cool emojis 26 | * 🔐 LDAPS & StartTLS support 27 | * ⏩ Fast explorer that loads objects on demand 28 | * 🔎 Recursive object search bundled with useful saved searches 29 | * 👥 Flexible group members & user groups lookups 30 | * 🎡 Supports creation, editing and removal of objects and attributes 31 | * 🚙 Supports moving and renaming objects 32 | * 🗑️ Supports searching deleted & recycled objects 33 | * 📁 Supports exporting specific subtrees of the directory into JSON files 34 | * 🕹️ Interactive userAccountControl editor 35 | * 🔥 Interactive DACL viewer + editor 36 | * 🌐 Interactive ADIDNS viewer + editor (basic) 37 | * 📜 GPO Viewer 38 | * 🧦 SOCKS support 39 | 40 | # Installation 41 | 42 | ```bash 43 | $ git clone https://github.com/Macmod/godap 44 | $ cd godap 45 | $ go install . 46 | ``` 47 | 48 | ## Package Repositories 49 | 50 | Some members of the community have published `godap` in [package repositories](https://repology.org/project/godap/versions). The following packages are known to provide working releases of `godap`: 51 | 52 | * *Homebrew*. [godap](https://formulae.brew.sh/formula/godap) by `harpchad` 53 | * *Alpine Linux (community)*. [godap](https://pkgs.alpinelinux.org/package/edge/community/x86_64/godap) by `omni+alpine@hack.org` 54 | * *Arch Linux (AUR)*. [godap-bin](https://aur.archlinux.org/packages/godap-bin) by `killab33z` 55 | 56 | Remember to check `godap`'s version before using these packages, as some of them might not always be up to date. 57 | In case you need the latest features, godap also provides [automated releases](https://github.com/Macmod/godap/releases) for several platforms. 58 | 59 | # Usage 60 | 61 | **Bind with username and password** 62 | 63 | ```bash 64 | $ godap -u -p -d 65 | ``` 66 | 67 | or 68 | 69 | ```bash 70 | $ godap -u @ -p 71 | ``` 72 | 73 | **Bind with an NTLM hash** 74 | 75 | ```bash 76 | $ godap -u -H [-d ] 77 | ``` 78 | 79 | **Bind with a Kerberos ticket** 80 | 81 | ```bash 82 | $ KRB5CCNAME=ticket.ccache godap -k -d -t ldap/ 83 | ``` 84 | 85 | **Bind with a Certificate + Private Key** 86 | 87 | PEM: 88 | ```bash 89 | $ godap --crt --key -I 90 | ``` 91 | 92 | PKCS#12: 93 | ```bash 94 | $ godap --pfx -I 95 | ``` 96 | 97 | Note. This method will either pass the certificate directly when connecting with LDAPS (`-S`), or upgrade the unencrypted LDAP connection implicitly with StartTLS, therefore you must provide `-I` if you want to use it and your server certificate is not trusted by your client. 98 | 99 | **Anonymous Bind** 100 | 101 | ```bash 102 | $ godap 103 | ``` 104 | 105 | **LDAPS/StartTLS** 106 | 107 | To use LDAPS for the initial connection (ignoring certificate validation) run: 108 | 109 | ```bash 110 | $ godap [bind flags] -S -I 111 | ``` 112 | 113 | To use StartTLS to upgrade an existing connection to use TLS, use the `Ctrl + u` keybinding inside godap. 114 | 115 | Notice that, if the server certificate is not trusted by your client, you must either have started godap with `-I` to use the upgrade command properly or toggle the `IgnoreCert` checkbox using the `l` keybinding before upgrading. 116 | 117 | If LDAPS is available, you can also change the port using `l`, toggle the LDAPS checkbox, set the desired value for `IgnoreCert`, and reconnect with `Ctrl + r`. 118 | 119 | **SOCKS** 120 | 121 | To connect to LDAP through a SOCKS proxy include the flag `-x schema://ip:port`, where `schema` is one of `socks4`, `socks4a` or `socks5`. 122 | 123 | You can also change the address of your proxy using the `l` keybinding. 124 | 125 | For more usage information & examples check the [Wiki](https://github.com/Macmod/godap/wiki) 126 | 127 | ## Flags 128 | 129 | * `-u`,`--username` - Username for bind 130 | * `-p`,`--password` - Password for bind 131 | * `--passfile` - Path to a file containing the password for bind 132 | * `-P`,`--port` - Custom port for the connection (default: `389` or `636` when `-S` is provided) 133 | * `-r`,`--rootDN ` - Initial root DN (default: automatic) 134 | * `-f`,`--filter ` - Initial LDAP search filter (default: `(objectClass=*)`) 135 | * `-b`,`--backend` - Flavor of the LDAP server (`msad`, `basic` or `auto`) 136 | * `-E`,`--emojis` - Prefix objects with emojis (default: `true`, to change use `-emojis=false`) 137 | * `-C`,`--colors` - Colorize objects (default: `true`, to change use `-colors=false`) 138 | * `-A`,`--expand` - Expand multi-value attributes (default: `true`, to change use `-expand=false`) 139 | * `-L`,`--limit` - Number of attribute values to render for multi-value attributes when `-expand` is `true` (default: `20`) 140 | * `-F`,`--format` - Format attributes into human-readable values (default: `true`, to change use `-format=false`) 141 | * `-M`,`--cache` - Keep loaded entries in memory while the program is open and don't query them again (default: `true`) 142 | * `-D`,`--deleted` - Include deleted objects in all queries performed (default: `false`) 143 | * `-T`,`--timeout` - Timeout for LDAP connections in seconds (default: `10`) 144 | * `-I`,`--insecure` - Skip TLS verification for LDAPS/StartTLS (default: `false`) 145 | * `-S`,`--ldaps` - Use LDAPS for initial connection (default: `false`) 146 | * `-G`,`--paging` - Paging size for regular queries (default: `800`) 147 | * `-d`,`--domain` - Domain name for NTLM / Kerberos authentication 148 | * `-H`,`--hash` - Hashes for NTLM bind 149 | * `-k`,`--kerberos` - Use Kerberos ticket for authentication (CCACHE specified via `KRB5CCNAME` environment variable) 150 | * `-t`,`--spn` - Target SPN to use for Kerberos bind (usually `ldap/dchostname`) 151 | * `--hashfile` - Path to a file containing the hashes for NTLM bind 152 | * `-x`,`--socks` - URI of SOCKS proxy to use for connection (supports `socks4://`, `socks4a://` or `socks5://` schemas) 153 | * `-s`,`--schema` - Load GUIDs from schema on initialization (default: `false`) 154 | * `--kdc` - Address of the KDC to use with Kerberos authentication (optional: only if the KDC differs from the specified LDAP server) 155 | * `--timefmt` - Time format for LDAP timestamps. Options: eu, us, [iso8601](https://en.wikipedia.org/wiki/ISO_8601), or define your own using [go time format](https://go.dev/src/time/format.go) (default: `eu`) 156 | * `--attrsort` - Sort attributes by name: `none` (default), `asc` (ascending), or `desc` (descending) 157 | * `--crt` - Path to a file containing the certificate to use for the bind 158 | * `--key` - Path to a file containing the private key to use for the bind 159 | * `--pfx` - Path to a file containing the PKCS#12 certificate to use for the bind 160 | * `--exportdir` - Custom directory to save godap exports taken with Ctrl+S (defaults to `data`) 161 | 162 | ## Keybindings 163 | 164 | | Keybinding | Context | Action | 165 | | --------------------------------------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------------| 166 | | Ctrl + Enter (or Ctrl + J) | Global | Next panel | 167 | | f | Global | Toggle attribute formatting | 168 | | e | Global | Toggle emojis | 169 | | c | Global | Toggle colors | 170 | | a | Global | Toggle attribute expansion for multi-value attributes | 171 | | d | Global | Toggle "include deleted objects" flag | 172 | | l | Global | Change current server address & credentials | 173 | | Ctrl + r | Global | Reconnect to the server | 174 | | Ctrl + u | Global | Upgrade connection to use TLS (with StartTLS) | 175 | | Ctrl + f | Explorer & Search pages | Open the finder to search for cached objects & attributes with regex | 176 | | Right Arrow | Explorer panel | Expand the children of the selected object | 177 | | Left Arrow | Explorer panel | Collapse the children of the selected object | 178 | | r | Explorer panel | Reload the attributes and children of the selected object | 179 | | Ctrl + n | Explorer panel | Create a new object under the selected object | 180 | | Ctrl + s | Explorer panel | Export all loaded nodes in the selected subtree into a JSON file | 181 | | Ctrl + p | Explorer panel | Change the password of the selected user or computer account (requires TLS) | 182 | | Ctrl + a | Explorer panel | Update the userAccountControl of the object interactively | 183 | | Ctrl + l | Explorer panel | Move the selected object to another location | 184 | | Delete | Explorer panel | Delete the selected object | 185 | | r | Attributes panel | Reload the attributes for the selected object | 186 | | Ctrl + e | Attributes panel | Edit the selected attribute of the selected object | 187 | | Ctrl + n | Attributes panel | Create a new attribute in the selected object | 188 | | Delete | Attributes panel | Delete the selected attribute of the selected object | 189 | | Enter | Attributes panel (entries hidden) | Expand all hidden entries of an attribute | 190 | | Delete | Groups panels | Remove the selected member from the searched group or vice-versa | 191 | | Ctrl + s | Object groups panel | Export the current groups into a JSON file | 192 | | Ctrl + s | Group members panel | Export the current group members into a JSON file | 193 | | Ctrl + g | Groups panels / Explorer panel / Obj. Search panel | Add a member to the selected group / add the selected object into a group | 194 | | Ctrl + d | Groups panels / Explorer panel / Obj. Search panel | Inspect the DACL of the currently selected object | 195 | | Ctrl + o | DACL page | Change the owner of the current security descriptor | 196 | | Ctrl + k | DACL page | Change the control flags of the current security descriptor | 197 | | Ctrl + s | DACL page | Export the current security descriptor into a JSON file | 198 | | Ctrl + n | DACL entries panel | Create a new ACE in the current DACL | 199 | | Ctrl + e | DACL entries panel | Edit the selected ACE of the current DACL | 200 | | Delete | DACL entries panel | Deletes the selected ACE of the current DACL | 201 | | Ctrl + s | GPO page | Export the current GPOs and their links into a JSON file | 202 | | Ctrl + s | DNS zones panel | Export the selected zones and their child DNS nodes into a JSON file | 203 | | r | DNS zones panel | Reload the nodes of the selected zone / the records of the selected node | 204 | | Ctrl + n | DNS zones panel | Create a new node under the selected zone or a new zone if the root is selected | 205 | | Ctrl + e | DNS zones panel | Edit the records of the currently selected node | 206 | | Delete | DNS zones panel | Delete the selected DNS zone or DNS node | 207 | | Delete | Records Preview (in `Update ADIDNS Node`) | Delete the selected record of the ADIDNS node | 208 | | h | Global | Show/hide headers | 209 | | q | Global | Exit the program | 210 | 211 | ## Tree Colors 212 | 213 | The nodes in the explorer tree are colored as follows: 214 | 215 | | Scenario | Color | 216 | | --------------------------------------- | -------------- | 217 | | Object exists and is enabled | Default | 218 | | Object exists and is disabled | Yellow\* | 219 | | Object was deleted and not yet recycled | Gray\* | 220 | | Object was recycled already | Red\* | 221 | 222 | \* Before v2.2.0, disabled nodes were colored red. This was the only custom color in the tree panel; other nodes were colored with default colors (the "include deleted objects" flag had not been implemented yet). 223 | 224 | # Contributing 225 | 226 | Godap started as a fun side project, but has become a really useful tool since then. Unfortunately these days I only have limited time and there's much to be done, so if you like the tool and believe you can help please reach out to me directly at [@marzanol](https://t.me/marzanol) :-) 227 | 228 | Contributions are also welcome by [opening an issue](https://github.com/Macmod/godap/issues/new) or by [submitting a pull request](https://github.com/Macmod/godap/pulls). 229 | 230 | # Acknowledgements 231 | 232 | * DACL parsing code and SOCKS code were adapted from the tools below: 233 | 234 | * [ldapper](https://github.com/Synzack/ldapper) 235 | * [Darksteel](https://github.com/wjlab/Darksteel) 236 | 237 | * [BadBlood](https://github.com/davidprowe/BadBlood) was also very useful for testing during the development of the tool. 238 | 239 | * Thanks [@vysecurity](https://github.com/vysecurity), [@SamErde](https://github.com/samerde) & all the others that shared the tool :) 240 | 241 | # Disclaimers 242 | 243 | * The main focus of this tool is Active Directory. If your target is not an Active Directory LDAP server, it's advised to set the `--backend` flag (available in versions above v2.10.4) to specify the "flavor" of your server, hiding features that aren't useful to you, and adapting others to work with your backend. 244 | 245 | * All features were tested and seem to be working properly on a Windows Server 2019, but this tool is highly experimental and I cannot test it extensively - I don't take responsibility for modifications that you execute and end up impacting your environment. If you observe any unexpected behaviors please [let me know](https://github.com/Macmod/godap/issues/new) so I can try to fix it. 246 | 247 | # License 248 | 249 | The MIT License (MIT) 250 | 251 | Copyright (c) 2023 Artur Henrique Marzano Gonzaga 252 | 253 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 254 | 255 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 256 | 257 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 258 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO (priority) 2 | 3 | * Feature: Pivot to groups search 4 | * Feature: Options to manipulate (edit/create/delete) gpLinks visually 5 | * Fix: Warn user of wrong KRB5CCNAME formats 6 | * Feature: Basic page for ADCS enumeration 7 | 8 | # TODO (later) 9 | 10 | * Wish: Remove dependency on personal fork of gokrb5 (may be doable with go-ldap's PR537) 11 | * Wish: Remove dependency on personal fork of go-ldap (may be doable with go-ldap's PR537) 12 | * Feature: Modify ADIDNS zone properties 13 | * Feature: Improve object creation form (implement customizations) 14 | * Feature: Custom themes 15 | * Feature: Customizable keybindings 16 | * Feature: Load initial cache from file 17 | * Wish: Add tests for core functions to make sure everything is in order 18 | * Wish: Mini tool to convert godap exports into bloodhound dumps 19 | * Wish: Monitor object for real-time changes (DirSync/SyncRepl) 20 | * Wish: Some way to copy data from panels (not implemented in tview, only for the "textarea" primitive) -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Macmod/godap/v2 2 | 3 | go 1.21.4 4 | 5 | require ( 6 | github.com/gdamore/tcell/v2 v2.7.1 7 | github.com/go-asn1-ber/asn1-ber v1.5.5 8 | github.com/go-ldap/ldap v0.0.0-20240314174501-83a306c8f13f 9 | github.com/go-ldap/ldap/v3 v3.4.7-0.20240314174501-83a306c8f13f 10 | github.com/jcmturner/gokrb5/v8 v8.4.4 11 | github.com/rivo/tview v0.0.0-20240413115534-b0d41c484b95 12 | github.com/spf13/cobra v1.8.0 13 | golang.org/x/text v0.14.0 14 | h12.io/socks v1.0.3 15 | ) 16 | 17 | require ( 18 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect 19 | github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect 20 | github.com/gdamore/encoding v1.0.0 // indirect 21 | github.com/google/uuid v1.6.0 // indirect 22 | github.com/hashicorp/go-uuid v1.0.3 // indirect 23 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 24 | github.com/jcmturner/aescts/v2 v2.0.0 // indirect 25 | github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect 26 | github.com/jcmturner/gofork v1.7.6 // indirect 27 | github.com/jcmturner/goidentity/v6 v6.0.1 // indirect 28 | github.com/jcmturner/rpc/v2 v2.0.3 // indirect 29 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 30 | github.com/mattn/go-runewidth v0.0.15 // indirect 31 | github.com/rivo/uniseg v0.4.7 // indirect 32 | github.com/spf13/pflag v1.0.5 // indirect 33 | golang.org/x/crypto v0.21.0 // indirect 34 | golang.org/x/net v0.22.0 // indirect 35 | golang.org/x/sys v0.18.0 // indirect 36 | golang.org/x/term v0.18.0 // indirect 37 | software.sslmate.com/src/go-pkcs12 v0.5.0 // indirect 38 | ) 39 | 40 | replace github.com/jcmturner/gokrb5/v8 => github.com/Macmod/gokrb5/v8 v8.4.5-0.20240428143821-ea9a660f0f44 41 | 42 | replace github.com/go-ldap/ldap/v3 => github.com/Macmod/ldap/v3 v3.0.0-20240415020653-119bc6d73ac6 43 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= 2 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= 3 | github.com/Macmod/gokrb5/v8 v8.4.5-0.20240428143821-ea9a660f0f44 h1:+FClzEKhnNbIuF2mYQEkAao5LF4MPDtAcMDSKwGG7Gk= 4 | github.com/Macmod/gokrb5/v8 v8.4.5-0.20240428143821-ea9a660f0f44/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= 5 | github.com/Macmod/ldap/v3 v3.0.0-20240415020653-119bc6d73ac6 h1:HRMFt8yuQvKKuCUx4ImdjmowHMxmJwCiffyEqxB1Qx8= 6 | github.com/Macmod/ldap/v3 v3.0.0-20240415020653-119bc6d73ac6/go.mod h1:qS3Sjlu76eHfHGpUdWkAXQTw4beih+cHsco2jXlIXrk= 7 | github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= 8 | github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= 9 | github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= 10 | github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= 15 | github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= 16 | github.com/gdamore/tcell/v2 v2.7.1 h1:TiCcmpWHiAU7F0rA2I3S2Y4mmLmO9KHxJ7E1QhYzQbc= 17 | github.com/gdamore/tcell/v2 v2.7.1/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg= 18 | github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= 19 | github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA= 20 | github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= 21 | github.com/go-ldap/ldap v0.0.0-20240314174501-83a306c8f13f h1:Ut2osoWP+r8CybEuU7xg7AxOYQpM315k48IWZ6xOYCU= 22 | github.com/go-ldap/ldap v0.0.0-20240314174501-83a306c8f13f/go.mod h1:/VguTtt2Zcd7XOGAVyeAiwUd5vgfddrM8EzTUsWoQ2k= 23 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 24 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 25 | github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= 26 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 27 | github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= 28 | github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 29 | github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364 h1:5XxdakFhqd9dnXoAZy1Mb2R/DZ6D1e+0bGC/JhucGYI= 30 | github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364/go.mod h1:eDJQioIyy4Yn3MVivT7rv/39gAJTrA7lgmYr8EW950c= 31 | github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 32 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= 33 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 34 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 35 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 36 | github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= 37 | github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= 38 | github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= 39 | github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= 40 | github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= 41 | github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= 42 | github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= 43 | github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= 44 | github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= 45 | github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= 46 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 47 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 48 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 49 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 50 | github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= 51 | github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= 52 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 53 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 54 | github.com/rivo/tview v0.0.0-20240413115534-b0d41c484b95 h1:dPivHKc1ZAicSlawH/eAmGPSCfOuCYRQLl+Eq1eRKNU= 55 | github.com/rivo/tview v0.0.0-20240413115534-b0d41c484b95/go.mod h1:02iFIz7K/A9jGCvrizLPvoqr4cEIx7q54RH5Qudkrss= 56 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 57 | github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 58 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 59 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 60 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 61 | github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= 62 | github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= 63 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 64 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 65 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 66 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 67 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 68 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 69 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 70 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 71 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 72 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 73 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 74 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 75 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 76 | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= 77 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 78 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 79 | golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= 80 | golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= 81 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 82 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 83 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 84 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 85 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 86 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 87 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 88 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 89 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 90 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 91 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 92 | golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= 93 | golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 94 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 95 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 96 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 97 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 98 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 99 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 100 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 101 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 102 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 103 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 104 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 105 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 106 | golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= 107 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 108 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 109 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 110 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 111 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 112 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 113 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 114 | golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= 115 | golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= 116 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 117 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 118 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 119 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 120 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 121 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 122 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 123 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 124 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 125 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 126 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 127 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 128 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 129 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 130 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 131 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 132 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 133 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 134 | h12.io/socks v1.0.3 h1:Ka3qaQewws4j4/eDQnOdpr4wXsC//dXtWvftlIcCQUo= 135 | h12.io/socks v1.0.3/go.mod h1:AIhxy1jOId/XCz9BO+EIgNL2rQiPTBNnOfnVnQ+3Eck= 136 | software.sslmate.com/src/go-pkcs12 v0.5.0 h1:EC6R394xgENTpZ4RltKydeDUjtlM5drOYIG9c6TVj2M= 137 | software.sslmate.com/src/go-pkcs12 v0.5.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= 138 | -------------------------------------------------------------------------------- /godap.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/Macmod/godap/v2/tui" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func main() { 11 | rootCmd := &cobra.Command{ 12 | Use: "godap ", 13 | Short: "A complete TUI for LDAP.", 14 | Args: cobra.ExactArgs(1), 15 | Run: func(cmd *cobra.Command, args []string) { 16 | tui.LdapServer = args[0] 17 | 18 | if tui.LdapPort == 0 { 19 | if tui.Ldaps { 20 | tui.LdapPort = 636 21 | } else { 22 | tui.LdapPort = 389 23 | } 24 | } 25 | 26 | tui.SetupApp() 27 | }, 28 | } 29 | 30 | rootCmd.Flags().IntVarP(&tui.LdapPort, "port", "P", 0, "LDAP server port") 31 | rootCmd.Flags().StringVarP(&tui.LdapUsername, "username", "u", "", "LDAP username") 32 | rootCmd.Flags().StringVarP(&tui.LdapPassword, "password", "p", "", "LDAP password") 33 | rootCmd.Flags().StringVarP(&tui.LdapPasswordFile, "passfile", "", "", "Path to a file containing the LDAP password") 34 | rootCmd.Flags().StringVarP(&tui.DomainName, "domain", "d", "", "Domain for NTLM / Kerberos authentication") 35 | rootCmd.Flags().StringVarP(&tui.NtlmHash, "hash", "H", "", "NTLM hash") 36 | rootCmd.Flags().BoolVarP(&tui.Kerberos, "kerberos", "k", false, "Use Kerberos ticket for authentication (CCACHE specified via KRB5CCNAME environment variable)") 37 | rootCmd.Flags().StringVarP(&tui.TargetSpn, "spn", "t", "", "Target SPN to use for Kerberos bind (usually ldap/dchostname)") 38 | rootCmd.Flags().StringVarP(&tui.NtlmHashFile, "hashfile", "", "", "Path to a file containing the NTLM hash") 39 | rootCmd.Flags().StringVarP(&tui.RootDN, "rootDN", "r", "", "Initial root DN") 40 | rootCmd.Flags().StringVarP(&tui.SearchFilter, "filter", "f", "(objectClass=*)", "Initial LDAP search filter") 41 | rootCmd.Flags().BoolVarP(&tui.Emojis, "emojis", "E", true, "Prefix objects with emojis") 42 | rootCmd.Flags().BoolVarP(&tui.Colors, "colors", "C", true, "Colorize objects") 43 | rootCmd.Flags().BoolVarP(&tui.FormatAttrs, "format", "F", true, "Format attributes into human-readable values") 44 | rootCmd.Flags().BoolVarP(&tui.ExpandAttrs, "expand", "A", true, "Expand multi-value attributes") 45 | rootCmd.Flags().IntVarP(&tui.AttrLimit, "limit", "L", 20, "Number of attribute values to render for multi-value attributes when -expand is set true") 46 | rootCmd.Flags().BoolVarP(&tui.CacheEntries, "cache", "M", true, "Keep loaded entries in memory while the program is open and don't query them again") 47 | rootCmd.Flags().BoolVarP(&tui.Deleted, "deleted", "D", false, "Include deleted objects in all queries performed") 48 | rootCmd.Flags().Int32VarP(&tui.Timeout, "timeout", "T", 10, "Timeout for LDAP connections in seconds") 49 | rootCmd.Flags().BoolVarP(&tui.LoadSchema, "schema", "s", false, "Load schema GUIDs from the LDAP server during initialization") 50 | rootCmd.Flags().Uint32VarP(&tui.PagingSize, "paging", "G", 800, "Default paging size for regular queries") 51 | rootCmd.Flags().BoolVarP(&tui.Insecure, "insecure", "I", false, "Skip TLS verification for LDAPS/StartTLS") 52 | rootCmd.Flags().BoolVarP(&tui.Ldaps, "ldaps", "S", false, "Use LDAPS for initial connection") 53 | rootCmd.Flags().StringVarP(&tui.SocksServer, "socks", "x", "", "Use a SOCKS proxy for initial connection") 54 | rootCmd.Flags().StringVarP(&tui.KdcHost, "kdc", "", "", "Address of the KDC to use with Kerberos authentication (optional: only if the KDC differs from the specified LDAP server)") 55 | rootCmd.Flags().StringVarP(&tui.TimeFormat, "timefmt", "", "", "Time format for LDAP timestamps") 56 | rootCmd.Flags().StringVarP(&tui.CertFile, "crt", "", "", "Path to a file containing the certificate to use for the bind") 57 | rootCmd.Flags().StringVarP(&tui.KeyFile, "key", "", "", "Path to a file containing the private key to use for the bind") 58 | rootCmd.Flags().StringVarP(&tui.PfxFile, "pfx", "", "", "Path to a file containing the PFX to use for the bind") 59 | rootCmd.Flags().StringVarP(&tui.AttrSort, "attrsort", "", "none", "Sort attributes by name (none, asc, desc)") 60 | rootCmd.Flags().StringVarP(&tui.ExportDir, "exportdir", "", "data", "Custom directory to save godap exports taken with Ctrl+S") 61 | rootCmd.Flags().StringVarP(&tui.BackendFlavor, "backend", "b", "msad", "LDAP backend flavor (msad, basic or auto)") 62 | 63 | versionCmd := &cobra.Command{ 64 | Use: "version", 65 | Short: "Print the version number of the application", 66 | DisableFlagsInUseLine: true, 67 | Run: func(cmd *cobra.Command, args []string) { 68 | fmt.Println(tui.GodapVer) 69 | }, 70 | } 71 | 72 | rootCmd.AddCommand(versionCmd) 73 | 74 | if err := rootCmd.Execute(); err != nil { 75 | fmt.Println(err) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /images/godap.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macmod/godap/c7d36a6bbc3a79eb28437ff0e77f1458a3436232/images/godap.gif -------------------------------------------------------------------------------- /pkg/adidns/colors.go: -------------------------------------------------------------------------------- 1 | package adidns 2 | 3 | func GetPropCellColor(propId uint32, cellValue string) (string, bool) { 4 | switch cellValue { 5 | case "Enabled": 6 | return "green", true 7 | case "Disabled", "None": 8 | return "red", true 9 | case "Unknown", "Not specified": 10 | return "gray", true 11 | } 12 | 13 | switch propId { 14 | case 0x00000001: 15 | switch cellValue { 16 | case "PRIMARY": 17 | return "green", true 18 | case "CACHE": 19 | return "blue", true 20 | } 21 | case 0x00000002: 22 | switch cellValue { 23 | case "None": 24 | return "red", true 25 | case "Nonsecure and secure": 26 | return "yellow", true 27 | case "Secure only": 28 | return "green", true 29 | default: 30 | return "gray", true 31 | } 32 | } 33 | 34 | return "", false 35 | } 36 | -------------------------------------------------------------------------------- /pkg/adidns/formats.go: -------------------------------------------------------------------------------- 1 | package adidns 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "math" 7 | "net" 8 | "time" 9 | ) 10 | 11 | func ParseIP(data []byte) string { 12 | ip := net.IP(data) 13 | return ip.String() 14 | } 15 | 16 | func ParseAddrArray(data []byte) []string { 17 | if len(data) == 0 { 18 | return nil 19 | } 20 | 21 | numIPs := int(data[0]) 22 | if len(data) < 32*numIPs+32 { 23 | return nil 24 | } 25 | 26 | addrArr := data[32:] 27 | 28 | ips := make([]string, numIPs) 29 | for x := 0; x < numIPs; x += 1 { 30 | family := binary.LittleEndian.Uint16(addrArr[:4]) 31 | 32 | var ip net.IP 33 | if family == 0x0002 { 34 | // IPv4 35 | ip = net.IP(addrArr[x*32+4 : x*32+8]) 36 | } else if family == 0x0017 { 37 | // IPv6 38 | ip = net.IP(addrArr[x*32+8 : x*32+24]) 39 | } else { 40 | continue 41 | } 42 | 43 | ips[x] = ip.String() 44 | } 45 | 46 | return ips 47 | } 48 | 49 | func ParseIP4Array(data []byte) []string { 50 | if len(data) == 0 { 51 | return nil 52 | } 53 | 54 | numIP4s := int(data[0]) 55 | if len(data) < 4*numIP4s+1 { 56 | return nil 57 | } 58 | 59 | ip4s := make([]string, numIP4s) 60 | for x := 0; x < numIP4s; x += 1 { 61 | ip := net.IP(data[1+x*4 : 1+(x+1)*4]) 62 | ip4s = append(ip4s, ip.String()) 63 | } 64 | 65 | return ip4s 66 | } 67 | 68 | func FormatHours(val uint64) string { 69 | days := 0 70 | if val > 24 { 71 | days = int(math.Floor(float64(val / 24))) 72 | } 73 | 74 | text := "" 75 | if days > 0 { 76 | text = fmt.Sprintf("%d days", days) 77 | if val%24 != 0 { 78 | text += fmt.Sprintf(", %d hours", val%24) 79 | } 80 | } else { 81 | text = fmt.Sprintf("%d hours", val) 82 | } 83 | 84 | return text 85 | } 86 | 87 | // msTime is defined as the number of seconds since Jan 1th of 1601 88 | // to calculate it we just compute a unix timestamp after 89 | // removing the difference in seconds 90 | // between 01/01/1601 and 01/01/1970 91 | func MSTimeToUnixTimestamp(msTime uint64) int64 { 92 | if msTime == 0 { 93 | return -1 94 | } 95 | 96 | baseTime := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC) 97 | 98 | secondsSince := msTime - uint64(11644473600) 99 | 100 | elapsedDuration := time.Duration(secondsSince) * time.Second 101 | 102 | targetTime := baseTime.Add(elapsedDuration) 103 | 104 | unixTimestamp := targetTime.Unix() 105 | 106 | return unixTimestamp 107 | } 108 | 109 | func GetCurrentMSTime() uint32 { 110 | baseTime := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC) 111 | 112 | currentTime := time.Now().UTC() 113 | 114 | duration := currentTime.Sub(baseTime) 115 | 116 | targetTime := duration.Hours() 117 | 118 | msTime := uint32(targetTime) + uint32(3234576) 119 | 120 | return msTime 121 | } 122 | -------------------------------------------------------------------------------- /pkg/formats/time.go: -------------------------------------------------------------------------------- 1 | package formats 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | func GetTimeDistString(diff time.Duration) string { 9 | var distString string 10 | 11 | daysAgo := int(diff.Hours() / 24) 12 | if daysAgo == 0 { 13 | hoursAgo := int(diff.Hours()) 14 | if hoursAgo == 0 { 15 | minutesAgo := int(diff.Minutes()) 16 | if minutesAgo == 0 { 17 | distString = fmt.Sprintf("(%d seconds ago)", int(diff.Seconds())) 18 | } else if minutesAgo == 1 { 19 | distString = "(1 minute ago)" 20 | } else { 21 | distString = fmt.Sprintf("(%d minutes ago)", minutesAgo) 22 | } 23 | } else { 24 | distString = fmt.Sprintf("(%d hours ago)", hoursAgo) 25 | } 26 | } else if daysAgo == 1 { 27 | distString = "(yesterday)" 28 | } else { 29 | distString = fmt.Sprintf("(%d days ago)", daysAgo) 30 | } 31 | 32 | return distString 33 | } 34 | -------------------------------------------------------------------------------- /pkg/ldaputils/emojis.go: -------------------------------------------------------------------------------- 1 | package ldaputils 2 | 3 | var EmojiMap = map[string]string{ 4 | "root": "🌳", 5 | "user": "👤", // Human user 6 | "computer": "🖥️", // Computer account 7 | "group": "👥", // Group of users 8 | "organizationalUnit": "📂", // Organizational unit 9 | "container": "📁", // Container 10 | "person": "👨", // Generic person 11 | "organizationalPerson": "👔", // Organizational person 12 | "groupOfNames": "📇", // Group of names 13 | "domain": "🌐", // Domain 14 | "domainDNS": "🔗", // DNS Domain 15 | "builtinDomain": "🏠", // Built-in domain 16 | "groupPolicyContainer": "⚙️", // Group Policy Object 17 | "foreignSecurityPrincipal": "🌍", // Foreign security principal 18 | "contact": "📞", // Contact 19 | "printQueue": "🖨️", // Print queue 20 | "volume": "📦", // Volume 21 | "publicFolder": "📬", // Public folder 22 | "serviceConnectionPoint": "🔌", // Service connection point 23 | "msExchExchangeServer": "📧", // Exchange server 24 | "msExchStorageGroup": "🗃️", // Exchange storage group 25 | "subnet": "🕸️", // Subnet 26 | "site": "📍", // Site 27 | "groupOfUniqueNames": "📇", 28 | "device": "💻", 29 | "posixAccount": "🆔", 30 | "organization": "🏢", 31 | } 32 | -------------------------------------------------------------------------------- /pkg/ldaputils/formats.go: -------------------------------------------------------------------------------- 1 | package ldaputils 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/hex" 6 | "fmt" 7 | "sort" 8 | "strconv" 9 | "strings" 10 | "time" 11 | "unicode" 12 | 13 | "github.com/Macmod/godap/v2/pkg/formats" 14 | "github.com/go-ldap/ldap/v3" 15 | ) 16 | 17 | func HexToOffset(hex string) (integer int64) { 18 | integer, _ = strconv.ParseInt(EndianConvert(hex), 16, 64) 19 | integer = integer * 2 20 | return 21 | } 22 | 23 | func EndianConvert(sd string) (newSD string) { 24 | sdBytes, _ := hex.DecodeString(sd) 25 | 26 | for i, j := 0, len(sdBytes)-1; i < j; i, j = i+1, j-1 { 27 | sdBytes[i], sdBytes[j] = sdBytes[j], sdBytes[i] 28 | } 29 | 30 | newSD = hex.EncodeToString(sdBytes) 31 | 32 | return 33 | } 34 | 35 | func HexToDecimalString(hex string) (decimal string) { 36 | integer, _ := strconv.ParseInt(hex, 16, 64) 37 | decimal = strconv.Itoa(int(integer)) 38 | 39 | return 40 | } 41 | 42 | func HexToInt(hex string) (integer int) { 43 | integer64, _ := strconv.ParseInt(hex, 16, 64) 44 | integer = int(integer64) 45 | return 46 | } 47 | 48 | func Capitalize(str string) string { 49 | runes := []rune(str) 50 | if len(runes) > 0 { 51 | runes[0] = unicode.ToUpper(runes[0]) 52 | } 53 | return string(runes) 54 | } 55 | 56 | func ConvertSID(hexSID string) (SID string) { 57 | var fields []string 58 | fields = append(fields, hexSID[0:2]) 59 | if fields[0] == "01" { 60 | fields[0] = "S-1" 61 | } 62 | numDashes, _ := strconv.Atoi(HexToDecimalString(hexSID[2:4])) 63 | 64 | fields = append(fields, "-"+HexToDecimalString(hexSID[4:16])) 65 | 66 | lower, upper := 16, 24 67 | for i := 1; i <= numDashes; i++ { 68 | fields = append(fields, "-"+HexToDecimalString(EndianConvert(hexSID[lower:upper]))) 69 | lower += 8 70 | upper += 8 71 | } 72 | 73 | for i := 0; i < len(fields); i++ { 74 | SID += (fields[i]) 75 | } 76 | 77 | return 78 | } 79 | 80 | // TODO: Review correctness of this function 81 | func EncodeSID(sid string) (string, error) { 82 | if len(sid) < 2 { 83 | return "", fmt.Errorf("Invalid SID format") 84 | } 85 | 86 | parts := strings.Split(sid[2:], "-") 87 | if len(parts) < 3 { 88 | return "", fmt.Errorf("Invalid SID format") 89 | } 90 | 91 | hexSID := "" 92 | 93 | revision, err := strconv.Atoi(parts[0]) 94 | if err != nil { 95 | return "", fmt.Errorf("Error parsing revision: %v", err) 96 | } 97 | 98 | hexSID += fmt.Sprintf("%02X", revision) 99 | 100 | subAuthoritiesCount := len(parts) - 2 101 | hexSID += fmt.Sprintf("%02X", subAuthoritiesCount) 102 | 103 | identifierAuthority, _ := strconv.Atoi(parts[1]) 104 | for i := 0; i < 6; i++ { 105 | hexSID += fmt.Sprintf("%02X", byte(identifierAuthority>>(8*(5-i))&0xFF)) 106 | } 107 | 108 | for _, subAuthority := range parts[2:] { 109 | subAuthorityValue, err := strconv.Atoi(subAuthority) 110 | if err != nil { 111 | return "", fmt.Errorf("Error parsing subauthority: %v", err) 112 | } 113 | 114 | subAuthorityArr := make([]byte, 4) 115 | binary.LittleEndian.PutUint32(subAuthorityArr, uint32(subAuthorityValue)) 116 | 117 | hexSID += fmt.Sprintf("%08X", subAuthorityArr) 118 | } 119 | 120 | return hexSID, nil 121 | } 122 | 123 | func IsSID(s string) bool { 124 | return strings.HasPrefix(s, "S-") 125 | } 126 | 127 | func ConvertGUID(portion string) string { 128 | portion1 := EndianConvert(portion[0:8]) 129 | portion2 := EndianConvert(portion[8:12]) 130 | portion3 := EndianConvert(portion[12:16]) 131 | portion4 := portion[16:20] 132 | portion5 := portion[20:] 133 | return fmt.Sprintf("%s-%s-%s-%s-%s", portion1, portion2, portion3, portion4, portion5) 134 | } 135 | 136 | func EncodeGUID(guid string) (string, error) { 137 | tokens := strings.Split(guid, "-") 138 | if len(tokens) != 5 { 139 | return "", fmt.Errorf("Wrong GUID format") 140 | } 141 | 142 | result := "" 143 | result += EndianConvert(tokens[0]) 144 | result += EndianConvert(tokens[1]) 145 | result += EndianConvert(tokens[2]) 146 | result += tokens[3] 147 | result += tokens[4] 148 | return result, nil 149 | } 150 | 151 | func FormatLDAPTime(val, format string) string { 152 | layout := "20060102150405.0Z" 153 | t, err := time.Parse(layout, val) 154 | if err != nil { 155 | return "Invalid date format" 156 | } 157 | 158 | distString := formats.GetTimeDistString(time.Since(t)) 159 | 160 | return fmt.Sprintf("%s %s", t.Format(format), distString) 161 | } 162 | 163 | func FormatLDAPAttribute(attr *ldap.EntryAttribute, timeFormat string) []string { 164 | var formattedEntries = attr.Values 165 | 166 | if len(attr.Values) == 0 { 167 | return []string{"(Empty)"} 168 | } 169 | 170 | for idx, val := range attr.Values { 171 | switch attr.Name { 172 | case "objectSid": 173 | formattedEntries = []string{"SID{" + ConvertSID(hex.EncodeToString(attr.ByteValues[idx])) + "}"} 174 | case "objectGUID", "schemaIDGUID": 175 | formattedEntries = []string{"GUID{" + ConvertGUID(hex.EncodeToString(attr.ByteValues[idx])) + "}"} 176 | case "whenCreated", "whenChanged": 177 | formattedEntries = []string{ 178 | FormatLDAPTime(val, timeFormat), 179 | } 180 | case "lastLogonTimestamp", "accountExpires", "badPasswordTime", "lastLogoff", "lastLogon", "pwdLastSet", "creationTime", "lockoutTime": 181 | if val == "0" { 182 | return []string{"(Never)"} 183 | } 184 | 185 | if attr.Name == "accountExpires" && val == "9223372036854775807" { 186 | return []string{"(Never Expire)"} 187 | } 188 | 189 | intValue, err := strconv.ParseInt(val, 10, 64) 190 | if err != nil { 191 | return []string{"(Invalid)"} 192 | } 193 | 194 | unixTime := (intValue - 116444736000000000) / 10000000 195 | t := time.Unix(unixTime, 0).UTC() 196 | 197 | distString := formats.GetTimeDistString(time.Since(t)) 198 | 199 | formattedEntries = []string{fmt.Sprintf("%s %s", t.Format(timeFormat), distString)} 200 | case "userAccountControl": 201 | uacInt, _ := strconv.Atoi(val) 202 | 203 | formattedEntries = []string{} 204 | 205 | uacFlagKeys := make([]int, 0) 206 | for k := range UacFlags { 207 | uacFlagKeys = append(uacFlagKeys, k) 208 | } 209 | sort.Ints(uacFlagKeys) 210 | 211 | for _, flag := range uacFlagKeys { 212 | curFlag := UacFlags[flag] 213 | if uacInt&flag != 0 { 214 | if curFlag.Present != "" { 215 | formattedEntries = append(formattedEntries, curFlag.Present) 216 | } 217 | } else { 218 | if curFlag.NotPresent != "" { 219 | formattedEntries = append(formattedEntries, curFlag.NotPresent) 220 | } 221 | } 222 | } 223 | case "primaryGroupID": 224 | rId, _ := strconv.Atoi(val) 225 | 226 | groupName, ok := RidMap[rId] 227 | 228 | if ok { 229 | formattedEntries = []string{groupName} 230 | } 231 | case "sAMAccountType": 232 | sAMAccountTypeId, _ := strconv.Atoi(val) 233 | 234 | accountType, ok := SAMAccountTypeMap[sAMAccountTypeId] 235 | 236 | if ok { 237 | formattedEntries = []string{accountType} 238 | } 239 | case "groupType": 240 | groupTypeId, _ := strconv.Atoi(val) 241 | groupType, ok := GroupTypeMap[groupTypeId] 242 | 243 | if ok { 244 | formattedEntries = []string{groupType} 245 | } 246 | case "instanceType": 247 | instanceTypeId, _ := strconv.Atoi(val) 248 | instanceType, ok := InstanceTypeMap[instanceTypeId] 249 | 250 | if ok { 251 | formattedEntries = []string{instanceType} 252 | } 253 | default: 254 | formattedEntries = attr.Values 255 | } 256 | } 257 | 258 | return formattedEntries 259 | } 260 | -------------------------------------------------------------------------------- /pkg/ldaputils/misc.go: -------------------------------------------------------------------------------- 1 | package ldaputils 2 | 3 | func IndexOf[T comparable](collection []T, el T) int { 4 | for i, x := range collection { 5 | if x == el { 6 | return i 7 | } 8 | } 9 | return -1 10 | } 11 | -------------------------------------------------------------------------------- /pkg/ldaputils/vars.go: -------------------------------------------------------------------------------- 1 | package ldaputils 2 | 3 | // Constants for userAccountControl flags 4 | const ( 5 | UAC_SCRIPT = 0x00000001 6 | UAC_ACCOUNTDISABLE = 0x00000002 7 | UAC_HOMEDIR_REQUIRED = 0x00000008 8 | UAC_LOCKOUT = 0x00000010 9 | UAC_PASSWD_NOTREQD = 0x00000020 10 | UAC_PASSWD_CANT_CHANGE = 0x00000040 11 | UAC_ENCRYPTED_TEXT_PWD_ALLOWED = 0x00000080 12 | UAC_TEMP_DUPLICATE_ACCOUNT = 0x00000100 13 | UAC_NORMAL_ACCOUNT = 0x00000200 14 | UAC_INTERDOMAIN_TRUST_ACCOUNT = 0x00000800 15 | UAC_WORKSTATION_TRUST_ACCOUNT = 0x00001000 16 | UAC_SERVER_TRUST_ACCOUNT = 0x00002000 17 | UAC_DONT_EXPIRE_PASSWORD = 0x00010000 18 | UAC_MNS_LOGON_ACCOUNT = 0x00020000 19 | UAC_SMARTCARD_REQUIRED = 0x00040000 20 | UAC_TRUSTED_FOR_DELEGATION = 0x00080000 21 | UAC_NOT_DELEGATED = 0x00100000 22 | UAC_USE_DES_KEY_ONLY = 0x00200000 23 | UAC_DONT_REQ_PREAUTH = 0x00400000 24 | UAC_PASSWORD_EXPIRED = 0x00800000 25 | UAC_TRUSTED_TO_AUTH_FOR_DELEGATION = 0x01000000 26 | UAC_PARTIAL_SECRETS_ACCOUNT = 0x04000000 27 | ) 28 | 29 | // Constants for Security Descriptor's Control Flags 30 | const ( 31 | SE_DACL_AUTO_INHERIT_REQ = 0x00000100 32 | SE_DACL_AUTO_INHERITED = 0x00000400 33 | SE_DACL_SACL_DEFAULTED = 0x00000008 34 | SE_DACL_PRESENT = 0x00000004 35 | SE_DACL_PROTECTED = 0x00001000 36 | SE_GROUP_DEFAULTED = 0x00000002 37 | SE_OWNER_DEFAULTED = 0x00000001 38 | SE_RM_CONTROL_VALID = 0x00004000 39 | SE_SACL_AUTO_INHERIT_REQ = 0x00000200 40 | SE_SACL_AUTO_INHERITED = 0x00000800 41 | SE_SACL_PRESENT = 0x00000010 42 | SE_SACL_PROTECTED = 0x00002000 43 | SE_SELF_RELATIVE = 0x00008000 44 | ) 45 | 46 | type flagDesc struct { 47 | Present string 48 | NotPresent string 49 | } 50 | 51 | var UacFlags = map[int]flagDesc{ 52 | UAC_SCRIPT: {"Script", ""}, 53 | UAC_ACCOUNTDISABLE: {"Disabled", "Enabled"}, 54 | UAC_HOMEDIR_REQUIRED: {"HomeDirRequired", ""}, 55 | UAC_LOCKOUT: {"LockedOut", ""}, 56 | UAC_PASSWD_NOTREQD: {"PwdNotRequired", ""}, 57 | UAC_PASSWD_CANT_CHANGE: {"CannotChangePwd", ""}, 58 | UAC_ENCRYPTED_TEXT_PWD_ALLOWED: {"EncryptedTextPwdAllowed", ""}, 59 | UAC_TEMP_DUPLICATE_ACCOUNT: {"TmpDuplicateAccount", ""}, 60 | UAC_NORMAL_ACCOUNT: {"NormalAccount", ""}, 61 | UAC_INTERDOMAIN_TRUST_ACCOUNT: {"InterdomainTrustAccount", ""}, 62 | UAC_WORKSTATION_TRUST_ACCOUNT: {"WorkstationTrustAccount", ""}, 63 | UAC_SERVER_TRUST_ACCOUNT: {"ServerTrustAccount", ""}, 64 | UAC_DONT_EXPIRE_PASSWORD: {"DoNotExpirePwd", ""}, 65 | UAC_MNS_LOGON_ACCOUNT: {"MNSLogonAccount", ""}, 66 | UAC_SMARTCARD_REQUIRED: {"SmartcardRequired", ""}, 67 | UAC_TRUSTED_FOR_DELEGATION: {"TrustedForDelegation", ""}, 68 | UAC_NOT_DELEGATED: {"NotDelegated", ""}, 69 | UAC_USE_DES_KEY_ONLY: {"UseDESKeyOnly", ""}, 70 | UAC_DONT_REQ_PREAUTH: {"DoNotRequirePreauth", ""}, 71 | UAC_PASSWORD_EXPIRED: {"PwdExpired", "PwdNotExpired"}, 72 | UAC_TRUSTED_TO_AUTH_FOR_DELEGATION: {"TrustedToAuthForDelegation", ""}, 73 | UAC_PARTIAL_SECRETS_ACCOUNT: {"PartialSecretsAccount", ""}, 74 | } 75 | 76 | var SDControlFlags = map[int]string{ 77 | SE_DACL_AUTO_INHERIT_REQ: "SE_DACL_AUTO_INHERIT_REQ", 78 | SE_DACL_AUTO_INHERITED: "SE_DACL_AUTO_INHERITED", 79 | SE_DACL_SACL_DEFAULTED: "SE_DACL_SACL_DEFAULTED", 80 | SE_DACL_PRESENT: "SE_DACL_PRESENT", 81 | SE_DACL_PROTECTED: "SE_DACL_PROTECTED", 82 | SE_GROUP_DEFAULTED: "SE_GROUP_DEFAULTED", 83 | SE_OWNER_DEFAULTED: "SE_OWNER_DEFAULTED", 84 | SE_RM_CONTROL_VALID: "SE_RM_CONTROL_VALID", 85 | SE_SACL_AUTO_INHERIT_REQ: "SE_SACL_AUTO_INHERIT_REQ", 86 | SE_SACL_AUTO_INHERITED: "SE_SACL_AUTO_INHERITED", 87 | SE_SACL_PRESENT: "SE_SACL_PRESENT", 88 | SE_SACL_PROTECTED: "SE_SACL_PROTECTED", 89 | SE_SELF_RELATIVE: "SE_SELF_RELATIVE", 90 | } 91 | 92 | // Relative ID (RID) descriptions 93 | var RidMap = map[int]string{ 94 | 500: "Administrator", 95 | 501: "Guest", 96 | 502: "KRBTGT (Key Distribution Center Service Account)", 97 | 512: "Domain Admins", 98 | 513: "Domain Users", 99 | 514: "Domain Guests", 100 | 515: "Domain Computers", 101 | 516: "Domain Controllers", 102 | 517: "Cert Publishers", 103 | 518: "Schema Admins", 104 | 519: "Enterprise Admins", 105 | 520: "Group Policy Creator Owners", 106 | 526: "Key Admins", 107 | 527: "Enterprise Key Admins", 108 | 553: "RAS and IAS Servers", 109 | 554: "Trusted for Delegation Computers", 110 | 555: "Protected Users", 111 | 572: "Cloneable Domain Controllers", 112 | 573: "Read-only Domain Controllers", 113 | 590: "Backup Operators", 114 | 591: "Print Operators", 115 | 592: "Server Operators", 116 | 593: "Account Operators", 117 | 594: "Replicator", 118 | 596: "Incoming Forest Trust Builders", 119 | 597: "Performance Monitor Users", 120 | 598: "Performance Log Users", 121 | 599: "Windows Authorization Access Group", 122 | 600: "Network Configuration Operators", 123 | 601: "Incoming Forest Trust Builders", 124 | 606: "Cryptographic Operators", 125 | 607: "Event Log Readers", 126 | } 127 | 128 | // sAMAccountType descriptions 129 | var SAMAccountTypeMap = map[int]string{ 130 | 0x00000000: "Domain Object", 131 | 0x10000000: "Group Object", 132 | 0x10000001: "Non-Security Group Object", 133 | 0x30000000: "User Object", 134 | 0x30000001: "Machine Account", 135 | 0x20000000: "Alias Object", 136 | 0x20000001: "Non-Security Alias Object", 137 | 0x30000002: "Trust Account", 138 | 0x40000000: "App Basic Group", 139 | 0x40000001: "App Query Group", 140 | } 141 | 142 | // groupType descriptions 143 | var GroupTypeMap = map[int]string{ 144 | 2: "Global Distribution Group", 145 | 4: "Domain Local Distribution Group", 146 | 8: "Universal Distribution Group", 147 | -2147483646: "Global Security Group", 148 | -2147483644: "Domain Local Security Group", 149 | -2147483643: "Builtin Group", 150 | -2147483640: "Universal Security Group", 151 | } 152 | 153 | // instanceType descriptions 154 | var InstanceTypeMap = map[int]string{ 155 | 1: "NamingContextHead", 156 | 2: "NotInstantiatedReplica", 157 | 4: "WritableObject", 158 | 8: "ParentNamingContextHeld", 159 | 16: "FirstNamingContextConstruction", 160 | 32: "NamingContextRemovalFromDSA", 161 | } 162 | 163 | type LibQuery struct { 164 | Title string 165 | Filter string 166 | } 167 | 168 | var PredefinedLdapQueriesAD = map[string][]LibQuery{ 169 | "Enum": { 170 | {"All Organizational Units", "(objectCategory=organizationalUnit)"}, 171 | {"All Containers", "(objectCategory=container)"}, 172 | {"All Groups", "(objectCategory=group)"}, 173 | {"All Computers", "(objectClass=computer)"}, 174 | {"All Users", "(&(objectCategory=person)(objectClass=user))"}, 175 | {"All Objects", "(objectClass=*)"}, 176 | }, 177 | "Users": { 178 | {"Recently Created Users", "(&(objectCategory=user)(whenCreated>=))"}, 179 | {"Users With Description", "(&(objectCategory=user)(description=*))"}, 180 | {"Users Without Email", "(&(objectCategory=user)(!(mail=*)))"}, 181 | {"Likely Service Users", "(&(objectCategory=user)(sAMAccountName=*svc*))"}, 182 | {"Disabled Users", "(&(objectCategory=user)(userAccountControl:1.2.840.113556.1.4.803:=2))"}, 183 | {"Expired Users", "(&(objectCategory=user)(accountExpires<=))"}, 184 | {"Users With Sensitive Infos", "(&(objectCategory=user)(|(telephoneNumber=*)(pager=*)(homePhone=*)(mobile=*)(info=*)(streetAddress=*)))"}, 185 | {"Inactive Users", "(&(objectCategory=user)(lastLogonTimestamp<=))"}, 186 | }, 187 | "Computers": { 188 | {"Domain Controllers", "(&(objectCategory=computer)(userAccountControl:1.2.840.113556.1.4.803:=8192))"}, 189 | {"Non-DC Servers", "(&(objectCategory=computer)(operatingSystem=*server*)(!(userAccountControl:1.2.840.113556.1.4.803:=8192)))"}, 190 | {"Non-Server Computers", "(&(objectCategory=computer)(!(operatingSystem=*server*))(!(userAccountControl:1.2.840.113556.1.4.803:=8192)))"}, 191 | {"Stale Computers", "(&(objectCategory=computer)(!lastLogonTimestamp=*))"}, 192 | {"Computers With Outdated OS", "(&(objectCategory=computer)(|(operatingSystem=*Server 2008*)(operatingSystem=*Server 2003*)(operatingSystem=*Windows XP*)(operatingSystem=*Windows 7*)))"}, 193 | }, 194 | "Security": { 195 | {"Domain Admins", "(&(objectCategory=user)(memberOf=CN=Domain Admins,CN=Users,DC=domain,DC=com))"}, 196 | {"Administrators", "(&(objectCategory=user)(memberOf=CN=Administrators,CN=Builtin,DC=domain,DC=com))"}, 197 | {"High Privilege Users", "(&(objectCategory=user)(adminCount=1))"}, 198 | {"Users With SPN", "(&(objectCategory=user)(servicePrincipalName=*))"}, 199 | {"Users With SIDHistory", "(&(objectCategory=person)(objectClass=user)(sidHistory=*))"}, 200 | {"KrbPreauth Disabled Users", "(&(objectCategory=person)(userAccountControl:1.2.840.113556.1.4.803:=4194304))"}, 201 | {"KrbPreauth Disabled Computers", "(&(objectCategory=computer)(userAccountControl:1.2.840.113556.1.4.803:=4194304))"}, 202 | {"Constrained Delegation Objects", "(msDS-AllowedToDelegateTo=*)"}, 203 | {"Unconstrained Delegation Objects", "(userAccountControl:1.2.840.113556.1.4.803:=524288)"}, 204 | {"RBCD Objects", "(msDS-AllowedToActOnBehalfOfOtherIdentity=*)"}, 205 | {"Not Trusted For Delegation", "(&(samaccountname=*)(userAccountControl:1.2.840.113556.1.4.803:=1048576))"}, 206 | {"Shadow Credentials Targets", "(msDS-KeyCredentialLink=*)"}, 207 | {"Must Change Password Users", "(&(objectCategory=person)(objectClass=user)(pwdLastSet=0)(!(useraccountcontrol:1.2.840.113556.1.4.803:=2)))"}, 208 | {"Password Never Changed Users", "(&(objectCategory=user)(pwdLastSet=0))"}, 209 | {"Never Expire Password Users", "(&(objectCategory=user)(userAccountControl:1.2.840.113556.1.4.803:=65536))"}, 210 | {"Empty Password Users", "(&(objectCategory=user)(userAccountControl:1.2.840.113556.1.4.803:=32))"}, 211 | {"LockedOut Users", "(&(objectCategory=user)(lockoutTime>=1))"}, 212 | {"Trusted Domains", "(objectClass=trustedDomain)"}, 213 | }, 214 | } 215 | 216 | var PredefinedLdapQueriesBasic = map[string][]LibQuery{ 217 | "Enum": { 218 | {"All Organizations", "(objectClass=organization)"}, 219 | {"All Users", "(|(objectClass=inetOrgPerson)(objectClass=posixAccount)(objectClass=person))"}, 220 | {"All Groups", "(|(objectClass=posixGroup)(objectClass=groupOfNames)(objectClass=groupOfUniqueNames))"}, 221 | {"All Computers", "(|(objectClass=ipHost)(objectClass=device))"}, 222 | {"All Organizational Units", "(objectClass=organizationalUnit)"}, 223 | {"All Organizational Roles", "(objectClass=organizationalRole)"}, 224 | {"All Sudo Roles", "(objectClass=sudoRole)"}, 225 | {"All Netgroups", "(objectClass=nisNetgroup)"}, 226 | {"All Objects", "(objectClass=*)"}, 227 | }, 228 | "Users": { 229 | {"Users With Email", "(&(mail=*)(|(objectClass=inetOrgPerson)(objectClass=posixAccount)(objectClass=person)))"}, 230 | {"Users With Phone Number", "(&(telephoneNumber=*)(|(objectClass=inetOrgPerson)(objectClass=posixAccount)(objectClass=person)))"}, 231 | {"Users With Home Directory", "(&(homeDirectory=*)(|(objectClass=inetOrgPerson)(objectClass=posixAccount)(objectClass=person)))"}, 232 | {"Users With UID", "(&(uid=*)(|(objectClass=inetOrgPerson)(objectClass=posixAccount)(objectClass=person)))"}, 233 | {"Users With Password", "(userPassword=*)"}, 234 | {"Users With SSH Keys", "(sshPublicKey=*)"}, 235 | }, 236 | "Groups": { 237 | {"Groups With Members (groupOfNames)", "(&(objectClass=groupOfNames)(member=*))"}, 238 | {"Groups With Members (posixGroup)", "(&(objectClass=posixGroup)(memberUid=*))"}, 239 | {"Groups With Members (groupOfUniqueNames)", "(&(objectClass=groupOfUniqueNames)(uniqueMember=*))"}, 240 | }, 241 | } 242 | 243 | var WellKnownSIDsMap = map[string]string{ 244 | "S-1-0-0": "Null SID", 245 | "S-1-1-0": "Everyone", 246 | "S-1-2-0": "Local", 247 | "S-1-2-1": "Console Logon", 248 | "S-1-3-0": "Creator Owner ID", 249 | "S-1-3-1": "Creator Group ID", 250 | "S-1-3-2": "Creator Owner Server", 251 | "S-1-3-3": "Creator Group Server", 252 | "S-1-3-4": "Owner Rights", 253 | "S-1-4": "Non-Unique Authority", 254 | "S-1-5": "NT Authority", 255 | "S-1-5-80-0": "All Services", 256 | "S-1-5-1": "Dialup", 257 | "S-1-5-113": "Local Account", 258 | "S-1-5-114": "Local account and member of Administrators group", 259 | "S-1-5-2": "Network", 260 | "S-1-5-3": "Batch", 261 | "S-1-5-4": "Interactive", 262 | "S-1-5-6": "Serivce", 263 | "S-1-5-7": "Anonymous Logon", 264 | "S-1-5-8": "Proxy", 265 | "S-1-5-9": "Enterprise Domain Controllers", 266 | "S-1-5-10": "Self", 267 | "S-1-5-11": "Authenticated Users", 268 | "S-1-5-12": "Restricted Code", 269 | "S-1-5-13": "Terminal Server User", 270 | "S-1-5-14": "Remote Interactive Logon", 271 | "S-1-5-15": "This Organization", 272 | "S-1-5-17": "IUSR", 273 | "S-1-5-18": "SYSTEM", 274 | "S-1-5-19": "NT Authority (LocalService)", 275 | "S-1-5-20": "Network Service", 276 | } 277 | -------------------------------------------------------------------------------- /pkg/sdl/AceFieldMaps.go: -------------------------------------------------------------------------------- 1 | package sdl 2 | 3 | var AceTypeMap = map[int]string{ 4 | 0x00: "ACCESS_ALLOWED_ACE_TYPE", 5 | 0x01: "ACCESS_DENIED_ACE_TYPE", 6 | 0x02: "SYSTEM_AUDIT_ACE_TYPE", 7 | 0x03: "SYSTEM_ALARM_ACE_TYPE", 8 | 0x04: "ACCESS_ALLOWED_COMPOUND_ACE_TYPE", 9 | 0x05: "ACCESS_ALLOWED_OBJECT_ACE_TYPE", 10 | 0x06: "ACCESS_DENIED_OBJECT_ACE_TYPE", 11 | 0x07: "SYSTEM_AUDIT_OBJECT_ACE_TYPE", 12 | 0x08: "SYSTEM_ALARM_OBJECT_ACE_TYPE", 13 | 0x09: "ACCESS_ALLOWED_CALLBACK_ACE_TYPE", 14 | 0x0A: "ACCESS_DENIED_CALLBACK_ACE_TYPE", 15 | 0x0B: "ACCESS_ALLOWED_CALLBACK_OBJECT_ACE_TYPE", 16 | 0x0C: "ACCESS_DENIED_CALLBACK_OBJECT_ACE_TYPE", 17 | 0x0D: "SYSTEM_AUDIT_CALLBACK_ACE_TYPE", 18 | 0x0E: "SYSTEM_ALARM_CALLBACK_ACE_TYPE", 19 | 0x0F: "SYSTEM_AUDIT_CALLBACK_OBJECT_ACE_TYPE", 20 | 0x10: "SYSTEM_ALARM_CALLBACK_OBJECT_ACE_TYPE", 21 | 0x11: "SYSTEM_MANDATORY_LABEL_ACE_TYPE", 22 | 0x12: "SYSTEM_RESOURCE_ATTRIBUTE_ACE_TYPE", 23 | 0x13: "SYSTEM_SCOPED_POLICY_ID_ACE_TYPE", 24 | } 25 | 26 | var AceFlagsMap = map[string]int{ 27 | "CONTAINER_INHERIT_ACE": 0x02, 28 | "FAILED_ACCESS_ACE_FLAG": 0x80, 29 | "INHERIT_ONLY_ACE": 0x08, 30 | "INHERITED_ACE": 0x10, 31 | "NO_PROPAGATE_INHERIT_ACE": 0x04, 32 | "OBJECT_INHERIT_ACE": 0x01, 33 | "SUCCESSFUL_ACCESS_ACE_FLAG": 0x40, 34 | } 35 | 36 | var AccessRightsMap = map[string]int{ 37 | "RIGHT_DS_CREATE_CHILD": 0x00000001, 38 | "RIGHT_DS_DELETE_CHILD": 0x00000002, 39 | "RIGHT_DS_LIST_CONTENTS": 0x00000004, 40 | "RIGHT_DS_SELF": 0x00000008, 41 | "RIGHT_DS_READ_PROPERTY": 0x00000010, 42 | "RIGHT_DS_WRITE_PROPERTY": 0x00000020, 43 | "RIGHT_DS_DELETE_TREE": 0x00000040, 44 | "RIGHT_DS_LIST_OBJECT": 0x00000080, 45 | "RIGHT_DS_CONTROL_ACCESS": 0x00000100, 46 | "RIGHT_DELETE": 0x00010000, 47 | "RIGHT_READ_CONTROL": 0x00020000, 48 | "RIGHT_WRITE_DACL": 0x00040000, 49 | "RIGHT_WRITE_OWNER": 0x00080000, 50 | "GENERIC_ALL": 0x000F01FF, 51 | "GENERIC_WRITE": 0x00020028, 52 | "GENERIC_READ": 0x00020094, 53 | "GENERIC_EXECUTE": 0x00020004, 54 | } 55 | 56 | var ObjectTypeMap = map[int]string{ 57 | 0x00000100: "ADS_RIGHT_DS_CONTROL_ACCESS", 58 | 0x00000001: "ADS_RIGHT_DS_CREATE_CHILD", 59 | 0x00000002: "ADS_RIGHT_DS_DELETE_CHILD", 60 | 0x00000010: "ADS_RIGHT_DS_READ_PROP", 61 | 0x00000020: "ADS_RIGHT_DS_WRITE_PROP", 62 | 0x00000008: "ADS_RIGHT_DS_SELF", 63 | } 64 | 65 | var InheritedObjectTypeMap = map[int]string{ 66 | 0x00000000: "", 67 | 0x00000001: "ACE_OBJECT_TYPE_PRESENT", 68 | 0x00000002: "ACE_INHERITED_OBJECT_TYPE_PRESENT", 69 | } 70 | -------------------------------------------------------------------------------- /pkg/sdl/AceTypeStructures.go: -------------------------------------------------------------------------------- 1 | package sdl 2 | 3 | //ldaputils.HexToInt(ace.Header.ACEFlags) 4 | import ( 5 | "fmt" 6 | 7 | "github.com/Macmod/godap/v2/pkg/ldaputils" 8 | ) 9 | 10 | // ACE Header 11 | type ACEHEADER struct { 12 | ACEType string 13 | ACEFlags string 14 | AceSizeBytes string 15 | } 16 | 17 | func newACEHeader(SD string) *ACEHEADER { 18 | ACEHeader := new(ACEHEADER) 19 | ACEHeader.ACEType = SD[0:2] 20 | ACEHeader.ACEFlags = SD[2:4] 21 | ACEHeader.AceSizeBytes = SD[4:8] 22 | 23 | return ACEHeader 24 | } 25 | 26 | func (ah *ACEHEADER) Encode() string { 27 | return ah.ACEType + ah.ACEFlags + ah.AceSizeBytes 28 | } 29 | 30 | // ACE Interface 31 | type ACEInt interface { 32 | GetHeader() *ACEHEADER 33 | GetMask() int 34 | GetSID() string 35 | SetHeader(*ACEHEADER) 36 | SetMask(int) 37 | SetSID(string) error 38 | Parse(string) 39 | Encode() string 40 | } 41 | 42 | // Basic ACE (embedded in more advanced types) 43 | type BASIC_ACE struct { 44 | Header *ACEHEADER 45 | Mask string 46 | SID string 47 | } 48 | 49 | func (ace *BASIC_ACE) GetHeader() *ACEHEADER { 50 | return ace.Header 51 | } 52 | 53 | func (ace *BASIC_ACE) GetMask() int { 54 | return ldaputils.HexToInt(ldaputils.EndianConvert(ace.Mask)) 55 | } 56 | 57 | func (ace *BASIC_ACE) GetSID() string { 58 | return ldaputils.ConvertSID(ace.SID) 59 | } 60 | 61 | func (ace *BASIC_ACE) SetHeader(header *ACEHEADER) { 62 | ace.Header = header 63 | } 64 | 65 | func (ace *BASIC_ACE) SetMask(mask int) { 66 | ace.Mask = ldaputils.EndianConvert(fmt.Sprintf("%08x", mask)) 67 | } 68 | 69 | func (ace *BASIC_ACE) SetSID(sid string) error { 70 | encodedSid, err := ldaputils.EncodeSID(sid) 71 | 72 | if err == nil { 73 | ace.SID = encodedSid 74 | } 75 | 76 | return err 77 | } 78 | 79 | func (ace *BASIC_ACE) Parse(rawACE string) { 80 | ace.Header = newACEHeader(rawACE) 81 | ace.Mask = rawACE[8:16] 82 | ace.SID = rawACE[16:] 83 | } 84 | 85 | func (ace *BASIC_ACE) Encode() string { 86 | var s string 87 | s = ace.Header.Encode() 88 | s += ace.Mask 89 | s += ace.SID 90 | return s 91 | } 92 | 93 | // Object ACE (base type embedded in more advanced types) 94 | type OBJECT_ACE struct { 95 | BASIC_ACE 96 | Flags string 97 | ObjectType string 98 | InheritedObjectType string 99 | } 100 | 101 | func (ace *OBJECT_ACE) Parse(rawACE string) { 102 | ace.Header = newACEHeader(rawACE) 103 | ace.Mask = rawACE[8:16] 104 | ace.Flags = rawACE[16:24] 105 | 106 | ace.ObjectType = "" 107 | ace.InheritedObjectType = "" 108 | 109 | switch ldaputils.EndianConvert(ace.Flags) { 110 | case "00000001": 111 | ace.ObjectType = rawACE[24:56] 112 | case "00000002": 113 | ace.InheritedObjectType = rawACE[24:56] 114 | case "00000003": 115 | ace.ObjectType = rawACE[24:56] 116 | ace.InheritedObjectType = rawACE[56:88] 117 | } 118 | 119 | lengthBeforeSID := 24 120 | if len(ace.ObjectType) > 0 { 121 | lengthBeforeSID += 32 122 | } 123 | if len(ace.InheritedObjectType) > 0 { 124 | lengthBeforeSID += 32 125 | } 126 | ace.SID = rawACE[lengthBeforeSID:] 127 | } 128 | 129 | func (ace *OBJECT_ACE) Encode() string { 130 | var s string 131 | s = ace.Header.Encode() 132 | s += ace.Mask 133 | s += ace.Flags 134 | s += ace.ObjectType 135 | s += ace.InheritedObjectType 136 | s += ace.SID 137 | 138 | return s 139 | } 140 | 141 | func (ace *OBJECT_ACE) GetObjectAndInheritedType() (objectTypeGUID string, inheritedObjectTypeGUID string) { 142 | switch ldaputils.EndianConvert(ace.Flags) { 143 | case "00000001": 144 | objectTypeGUID = ldaputils.ConvertGUID(ace.ObjectType) 145 | inheritedObjectTypeGUID = "" 146 | case "00000002": 147 | inheritedObjectTypeGUID = ldaputils.ConvertGUID(ace.InheritedObjectType) 148 | objectTypeGUID = "" 149 | case "00000003": 150 | objectTypeGUID = ldaputils.ConvertGUID(ace.ObjectType) 151 | inheritedObjectTypeGUID = ldaputils.ConvertGUID(ace.InheritedObjectType) 152 | } 153 | 154 | return 155 | } 156 | 157 | // Placeholder type for ACES that were not implemented 158 | // They should be kept "as-is" when parsing 159 | type NOTIMPL_ACE struct { 160 | BASIC_ACE 161 | rawHex string 162 | } 163 | 164 | func (ace *NOTIMPL_ACE) Parse(rawACE string) { 165 | ace.rawHex = rawACE 166 | } 167 | 168 | func (ace *NOTIMPL_ACE) Encode() string { 169 | return ace.rawHex 170 | } 171 | 172 | // Specific definitions 173 | type ACCESS_ALLOWED_ACE struct { 174 | BASIC_ACE 175 | } 176 | 177 | type ACCESS_DENIED_ACE struct { 178 | BASIC_ACE 179 | } 180 | 181 | type ACCESS_ALLOWED_OBJECT_ACE struct { 182 | OBJECT_ACE 183 | } 184 | 185 | type ACCESS_DENIED_OBJECT_ACE struct { 186 | OBJECT_ACE 187 | } 188 | 189 | // The ACE types below seem currently useless, 190 | // if I figure out any use for them in the future I'll 191 | // consider implementing some additional logic 192 | 193 | /* 194 | // Callback Types 195 | type ACCESS_ALLOWED_CALLBACK_ACE struct { 196 | BASIC_ACE 197 | applicationData string 198 | } 199 | 200 | type ACCESS_DENIED_CALLBACK_ACE struct { 201 | BASIC_ACE 202 | applicationData string 203 | } 204 | 205 | type ACCESS_ALLOWED_CALLBACK_OBJECT_ACE struct { 206 | OBJECT_ACE 207 | applicationData string 208 | } 209 | 210 | type ACCESS_DENIED_CALLBACK_OBJECT_ACE struct { 211 | OBJECT_ACE 212 | applicationData string 213 | } 214 | 215 | // SACL Types 216 | type SYSTEM_AUDIT_ACE struct { 217 | BASIC_ACE 218 | } 219 | 220 | type SYSTEM_AUDIT_OBJECT_ACE struct { 221 | OBJECT_ACE 222 | applicationData string 223 | } 224 | 225 | type SYSTEM_AUDIT_CALLBACK_ACE struct { 226 | BASIC_ACE 227 | applicationData string 228 | } 229 | 230 | type SYSTEM_AUDIT_CALLBACK_OBJECT_ACE struct { 231 | OBJECT_ACE 232 | applicationData string 233 | } 234 | 235 | type SYSTEM_MANDATORY_LABEL_ACE struct { 236 | BASIC_ACE 237 | } 238 | 239 | type SYSTEM_RESOURCE_ATTRIBUTE_ACE struct { 240 | BASIC_ACE 241 | attributeData string 242 | } 243 | 244 | type SYSTEM_SCOPED_POLICY_ID_ACE struct { 245 | BASIC_ACE 246 | } 247 | */ 248 | -------------------------------------------------------------------------------- /pkg/sdl/SDTypeStructures.go: -------------------------------------------------------------------------------- 1 | package sdl 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/Macmod/godap/v2/pkg/ldaputils" 8 | ) 9 | 10 | // ACL 11 | type ACL struct { 12 | Header *ACLHEADER 13 | Aces []ACEInt 14 | } 15 | 16 | func (acl *ACL) Parse(aclStr string) { 17 | acl.Header = getACLHeader(aclStr[:16]) 18 | rawACES := aclStr[16:] 19 | 20 | aceCount, _ := strconv.Atoi(ldaputils.HexToDecimalString(ldaputils.EndianConvert(acl.Header.ACECount))) 21 | rawACESList := make([]string, aceCount) 22 | 23 | for i := 0; i < aceCount; i++ { 24 | rawACESList[i] = getACE(rawACES) 25 | rawACES = rawACES[len(rawACESList[i]):] 26 | } 27 | 28 | var resolvedACEType string 29 | for ace, _ := range rawACESList { 30 | aceHeader := newACEHeader(rawACESList[ace]) 31 | aceType, _ := strconv.Atoi(aceHeader.ACEType) 32 | 33 | for entry, _ := range AceTypeMap { 34 | if entry == aceType { 35 | resolvedACEType = AceTypeMap[entry] 36 | } 37 | } 38 | 39 | var ACE ACEInt 40 | switch resolvedACEType { 41 | case "ACCESS_ALLOWED_ACE_TYPE", "ACCESS_DENIED_ACE_TYPE": 42 | ACE = new(BASIC_ACE) 43 | ACE.Parse(rawACESList[ace]) 44 | case "ACCESS_ALLOWED_OBJECT_ACE_TYPE", "ACCESS_DENIED_OBJECT_ACE_TYPE": 45 | ACE = new(OBJECT_ACE) 46 | ACE.Parse(rawACESList[ace]) 47 | default: 48 | ACE = new(NOTIMPL_ACE) 49 | ACE.Parse(rawACESList[ace]) 50 | } 51 | 52 | acl.Aces = append(acl.Aces, ACE) 53 | } 54 | } 55 | 56 | func (acl *ACL) Encode() string { 57 | if len(acl.Aces) == 0 { 58 | return "" 59 | } 60 | 61 | s := acl.Header.Encode() 62 | for _, ace := range acl.Aces { 63 | s += ace.Encode() 64 | } 65 | 66 | return s 67 | } 68 | 69 | // SD HEADER 70 | type HEADER struct { 71 | Revision string 72 | Sbz1 string 73 | Control string 74 | OffsetOwner string 75 | OffsetGroup string 76 | OffsetSacl string 77 | OffsetDacl string 78 | } 79 | 80 | func NewHeader(sdStr string) *HEADER { 81 | header := new(HEADER) 82 | 83 | header.Revision = sdStr[0:2] 84 | header.Sbz1 = sdStr[2:4] 85 | header.Control = sdStr[4:8] 86 | header.OffsetOwner = sdStr[8:16] 87 | header.OffsetGroup = sdStr[16:24] 88 | header.OffsetSacl = sdStr[24:32] 89 | header.OffsetDacl = sdStr[32:40] 90 | 91 | return header 92 | } 93 | 94 | func (header *HEADER) Encode() string { 95 | s := header.Revision 96 | s += header.Sbz1 97 | s += header.Control 98 | s += header.OffsetOwner 99 | s += header.OffsetGroup 100 | s += header.OffsetSacl 101 | s += header.OffsetDacl 102 | return s 103 | } 104 | 105 | // SecurityDescriptor 106 | type SecurityDescriptor struct { 107 | Header *HEADER 108 | SACL *ACL 109 | DACL *ACL 110 | Owner string 111 | Group string 112 | } 113 | 114 | func NewSD(sdStr string) *SecurityDescriptor { 115 | if len(sdStr) < 40 { 116 | return nil 117 | } 118 | 119 | sd := new(SecurityDescriptor) 120 | 121 | sd.Header = NewHeader(sdStr) 122 | 123 | sd.Owner = "" 124 | sd.Group = "" 125 | 126 | ownerOffset := int(ldaputils.HexToOffset(sd.Header.OffsetOwner)) 127 | ownerLen := ldaputils.HexToInt(sdStr[ownerOffset+2:ownerOffset+4])*2*4 + 16 128 | if int(ownerOffset+ownerLen) <= len(sdStr) { 129 | sd.Owner = sdStr[ownerOffset : ownerOffset+ownerLen] 130 | } 131 | 132 | groupOffset := int(ldaputils.HexToOffset(sd.Header.OffsetGroup)) 133 | groupLen := ldaputils.HexToInt(sdStr[groupOffset+2:groupOffset+4])*2*4 + 16 134 | if int(groupOffset+groupLen) <= len(sdStr) { 135 | sd.Group = sdStr[groupOffset : groupOffset+groupLen] 136 | } 137 | 138 | // SACL 139 | sd.SACL = new(ACL) 140 | saclOffset := ldaputils.HexToOffset(sd.Header.OffsetSacl) 141 | if saclOffset != 0 { 142 | sd.SACL.Parse(sdStr[saclOffset:]) 143 | } 144 | 145 | // DACL 146 | sd.DACL = new(ACL) 147 | daclOffset := ldaputils.HexToOffset(sd.Header.OffsetDacl) 148 | if daclOffset != 0 { 149 | sd.DACL.Parse(sdStr[daclOffset:]) 150 | } 151 | 152 | return sd 153 | } 154 | 155 | func (sd *SecurityDescriptor) updateMetadata() { 156 | sd.DACL.Header.ACECount = ldaputils.EndianConvert(fmt.Sprintf("%04x", len(sd.DACL.Aces))) 157 | 158 | mainDaclPart := sd.Header.Encode() + sd.SACL.Encode() + sd.DACL.Encode() 159 | sd.Header.OffsetOwner = ldaputils.EndianConvert(fmt.Sprintf("%08x", int(len(mainDaclPart)/2))) 160 | sd.Header.OffsetGroup = ldaputils.EndianConvert(fmt.Sprintf("%08x", int(len(mainDaclPart+sd.Owner)/2))) 161 | 162 | sd.DACL.Header.ACLSizeBytes = ldaputils.EndianConvert(fmt.Sprintf("%04x", len(sd.DACL.Encode())/2)) 163 | } 164 | 165 | func (sd *SecurityDescriptor) GetControl() int { 166 | return ldaputils.HexToInt(ldaputils.EndianConvert(sd.Header.Control)) 167 | } 168 | 169 | func (sd *SecurityDescriptor) SetControl(control int) { 170 | sd.Header.Control = ldaputils.EndianConvert(fmt.Sprintf("%04x", control)) 171 | } 172 | 173 | func (sd *SecurityDescriptor) SetOwnerAndGroup(ownerSID string, groupSID string) { 174 | sd.Owner = ownerSID 175 | sd.Group = groupSID 176 | 177 | sd.updateMetadata() 178 | } 179 | 180 | func (sd *SecurityDescriptor) SetDaclACES(aces []ACEInt) { 181 | sd.DACL.Aces = aces 182 | sd.updateMetadata() 183 | } 184 | 185 | func (sd *SecurityDescriptor) Encode() string { 186 | var sdStr string 187 | sdStr = sd.Header.Encode() + sd.SACL.Encode() + sd.DACL.Encode() + sd.Owner + sd.Group 188 | return sdStr 189 | } 190 | 191 | // ACL Header 192 | type ACLHEADER struct { 193 | ACLRevision string 194 | Sbz1 string 195 | ACLSizeBytes string 196 | ACECount string 197 | Sbz2 string 198 | } 199 | 200 | func getACLHeader(ACL string) *ACLHEADER { 201 | ACLHeader := new(ACLHEADER) 202 | ACLHeader.ACLRevision = ACL[0:2] 203 | ACLHeader.Sbz1 = ACL[2:4] 204 | ACLHeader.ACLSizeBytes = ACL[4:8] 205 | ACLHeader.ACECount = ACL[8:12] 206 | ACLHeader.Sbz2 = ACL[12:16] 207 | 208 | return ACLHeader 209 | } 210 | 211 | func (aclheader *ACLHEADER) Encode() string { 212 | s := aclheader.ACLRevision 213 | s += aclheader.Sbz1 214 | s += aclheader.ACLSizeBytes 215 | s += aclheader.ACECount 216 | s += aclheader.Sbz2 217 | return s 218 | } 219 | -------------------------------------------------------------------------------- /pkg/sdl/SecurityDescriptorFuncs.go: -------------------------------------------------------------------------------- 1 | package sdl 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | 7 | "github.com/Macmod/godap/v2/pkg/ldaputils" 8 | ) 9 | 10 | // References 11 | // - http://www.selfadsi.org/deep-inside/ad-security-descriptors.htm 12 | // - https://www.itinsights.org/Process-low-level-NtSecurityDescriptor/ 13 | // - https://devblogs.microsoft.com/oldnewthing/20040315-00/?p=40253 14 | 15 | func getACE(rawACE string) (ACE string) { 16 | aceLengthBytes, _ := strconv.Atoi(ldaputils.HexToDecimalString(ldaputils.EndianConvert(rawACE[4:8]))) 17 | aceLength := aceLengthBytes * 2 18 | ACE = rawACE[:aceLength] 19 | 20 | return 21 | } 22 | 23 | func checkRightExact(mask int, right int) bool { 24 | return mask&right == right 25 | } 26 | 27 | func checkRight(mask int, right int) bool { 28 | return mask&right != 0 29 | } 30 | 31 | func combinePerms(rights []int, rightNames []string, mask int) string { 32 | var combined []string 33 | 34 | for idx, right := range rights { 35 | if checkRightExact(mask, right) { 36 | combined = append(combined, rightNames[idx]) 37 | } 38 | } 39 | 40 | return ldaputils.Capitalize(strings.Join(combined, "/")) 41 | } 42 | 43 | // At the moment this is an experimental & testing accuracy of the parser is hard. 44 | // There are probably some bugs, bug they can be solved in the future :-) 45 | func AceMaskToText(mask int, guid string) ([]string, int) { 46 | var ( 47 | classNeeded bool 48 | attributeNeeded bool 49 | extendedNeeded bool 50 | validatedNeeded bool 51 | objectclass string 52 | attribute string 53 | extended string 54 | validated string 55 | ok bool 56 | okClass bool 57 | okAttr bool 58 | ) 59 | 60 | classNeeded = checkRight(mask, AccessRightsMap["RIGHT_DS_CREATE_CHILD"]) || checkRight(mask, AccessRightsMap["RIGHT_DS_DELETE_CHILD"]) 61 | attributeNeeded = checkRight(mask, AccessRightsMap["RIGHT_DS_READ_PROPERTY"]) || checkRight(mask, AccessRightsMap["RIGHT_DS_WRITE_PROPERTY"]) 62 | extendedNeeded = checkRight(mask, AccessRightsMap["RIGHT_DS_CONTROL_ACCESS"]) 63 | validatedNeeded = checkRight(mask, AccessRightsMap["RIGHT_DS_SELF"]) 64 | 65 | if len(guid) > 0 { 66 | ok = true 67 | if classNeeded && attributeNeeded { 68 | objectclass, okClass = ClassGuids[guid] 69 | attribute, okAttr = AttributeGuids[guid] 70 | 71 | if !okClass { 72 | objectclass = "any class of" 73 | } else if !okAttr { 74 | attribute = "all properties" 75 | } 76 | } else if classNeeded { 77 | objectclass, ok = ClassGuids[guid] 78 | } else if attributeNeeded { 79 | attribute, ok = AttributeGuids[guid] 80 | if !ok { 81 | // Control rights case #2: read/write on property sets 82 | attribute, ok = PropertySetGuids[guid] 83 | } 84 | } else if extendedNeeded { 85 | // Control rights case #1: extended rights 86 | extended, ok = ExtendedGuids[guid] 87 | } else if validatedNeeded { 88 | // Control rights case #3: validated writes 89 | validated, ok = ValidatedWriteGuids[guid] 90 | } 91 | 92 | if !ok { 93 | objectclass = guid 94 | attribute = guid 95 | extended = guid 96 | validated = guid 97 | } 98 | } else { 99 | objectclass = "all child" 100 | attribute = "all properties" 101 | extended = " all extended rights" 102 | validated = "All validated rights" 103 | } 104 | 105 | if checkRightExact(mask, AccessRightsMap["GENERIC_ALL"]) { // 0x000F01FF 106 | return []string{"Full control"}, 3 107 | } 108 | 109 | var rightsSeverity int = 0 110 | var readableRights []string 111 | 112 | specificChildPermission := combinePerms( 113 | []int{ 114 | AccessRightsMap["RIGHT_DS_CREATE_CHILD"], // 0x01 115 | AccessRightsMap["RIGHT_DS_DELETE_CHILD"], // 0x02 116 | }, 117 | []string{"create", "delete"}, 118 | mask, 119 | ) 120 | 121 | specificPermission := combinePerms( 122 | []int{ 123 | AccessRightsMap["RIGHT_DS_READ_PROPERTY"], // 0x10 124 | AccessRightsMap["RIGHT_DS_WRITE_PROPERTY"], // 0x20 125 | }, 126 | []string{"read", "write"}, 127 | mask, 128 | ) 129 | 130 | genericPermission := combinePerms( 131 | []int{ 132 | AccessRightsMap["GENERIC_READ"], // 0x00020094 133 | AccessRightsMap["GENERIC_WRITE"], // 0x00020028 134 | }, 135 | []string{"read", "write"}, 136 | mask, 137 | ) 138 | 139 | changeRights := []string{ 140 | "GENERIC_WRITE", "RIGHT_DS_CREATE_CHILD", 141 | "RIGHT_DS_DELETE_CHILD", "RIGHT_DS_WRITE_PROPERTY", 142 | } 143 | 144 | for _, changeRight := range changeRights { 145 | if checkRightExact(mask, AccessRightsMap[changeRight]) { 146 | rightsSeverity = 1 147 | } 148 | } 149 | 150 | if validatedNeeded || extendedNeeded { 151 | rightsSeverity = 2 152 | } 153 | 154 | if genericPermission != "" { 155 | readableRights = append(readableRights, genericPermission) 156 | } else if specificPermission != "" { 157 | readableRights = append(readableRights, specificPermission+" "+attribute) 158 | } else if specificChildPermission != "" { 159 | readableRights = append(readableRights, specificChildPermission+" "+objectclass+" objects") 160 | } 161 | 162 | if checkRight(mask, AccessRightsMap["RIGHT_DS_LIST_CONTENTS"]) { // 0x04 163 | readableRights = append(readableRights, "List contents") 164 | } 165 | 166 | if checkRight(mask, AccessRightsMap["RIGHT_DS_SELF"]) { // 0x08 167 | readableRights = append(readableRights, validated) 168 | } 169 | 170 | if checkRight(mask, AccessRightsMap["RIGHT_DS_DELETE_TREE"]) { // 0x40 171 | readableRights = append(readableRights, "Delete tree") 172 | } 173 | 174 | if checkRight(mask, AccessRightsMap["RIGHT_DS_LIST_OBJECT"]) { // 0x80 175 | readableRights = append(readableRights, "List object") 176 | } 177 | 178 | if specificPermission == "" && checkRight(mask, AccessRightsMap["RIGHT_DS_CONTROL_ACCESS"]) { // 0x100 179 | readableRights = append(readableRights, extended) 180 | } 181 | 182 | if checkRight(mask, AccessRightsMap["RIGHT_DELETE"]) { // 0x10000 183 | readableRights = append(readableRights, "Delete") 184 | } 185 | 186 | if checkRight(mask, AccessRightsMap["RIGHT_READ_CONTROL"]) { // 0x20000 187 | readableRights = append(readableRights, "Read permissions") 188 | } 189 | 190 | if checkRight(mask, AccessRightsMap["RIGHT_WRITE_DACL"]) { // 0x40000 191 | readableRights = append(readableRights, "Modify permissions") 192 | } 193 | 194 | if checkRight(mask, AccessRightsMap["RIGHT_WRITE_OWNER"]) { // 80000 195 | readableRights = append(readableRights, "Modify owner") 196 | } 197 | 198 | return readableRights, rightsSeverity 199 | } 200 | 201 | func AceFlagsToText(flagsStr string, guidStr string) string { 202 | propagationString := "" 203 | flags := ldaputils.HexToInt(flagsStr) 204 | objectClassStr := "" 205 | if guidStr != "" { 206 | objectClassStr = ClassGuids[guidStr] + " " 207 | } 208 | 209 | if flags&AceFlagsMap["CONTAINER_INHERIT_ACE"] == 0 { 210 | return "This object only" 211 | } 212 | 213 | if flags&AceFlagsMap["INHERIT_ONLY_ACE"] == 0 { 214 | propagationString = "this object and " 215 | } 216 | 217 | if flags&AceFlagsMap["NO_PROPAGATE_INHERIT_ACE"] == 0 { 218 | propagationString += "all descendant " + objectClassStr + "objects" 219 | } else { 220 | propagationString += "descendant " + objectClassStr + "objects" 221 | } 222 | 223 | return ldaputils.Capitalize(propagationString) 224 | } 225 | -------------------------------------------------------------------------------- /tui/cache.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "regexp" 5 | "sync" 6 | 7 | "github.com/go-ldap/ldap/v3" 8 | ) 9 | 10 | type EntryCache struct { 11 | entries map[string]*ldap.Entry 12 | lock sync.Mutex 13 | } 14 | 15 | func (sc *EntryCache) Delete(key string) { 16 | sc.lock.Lock() 17 | delete(sc.entries, key) 18 | sc.lock.Unlock() 19 | } 20 | 21 | func (sc *EntryCache) Clear() { 22 | sc.lock.Lock() 23 | clear(sc.entries) 24 | sc.lock.Unlock() 25 | } 26 | 27 | func (sc *EntryCache) Add(key string, val *ldap.Entry) { 28 | sc.lock.Lock() 29 | sc.entries[key] = val 30 | sc.lock.Unlock() 31 | } 32 | 33 | func (sc *EntryCache) Get(key string) (*ldap.Entry, bool) { 34 | sc.lock.Lock() 35 | defer sc.lock.Unlock() 36 | entry, ok := sc.entries[key] 37 | return entry, ok 38 | } 39 | 40 | func (sc *EntryCache) Length() int { 41 | sc.lock.Lock() 42 | defer sc.lock.Unlock() 43 | return len(sc.entries) 44 | } 45 | 46 | type EntryMatch struct { 47 | MatchField string 48 | MatchDN string 49 | MatchAttrName string 50 | MatchAttrVal string 51 | MatchAttrValIdx int 52 | MatchPosBegin int 53 | MatchPosEnd int 54 | } 55 | 56 | func (sc *EntryCache) FindWithRegexp(needle *regexp.Regexp) []EntryMatch { 57 | sc.lock.Lock() 58 | defer sc.lock.Unlock() 59 | 60 | var match []int 61 | 62 | results := []EntryMatch{} 63 | for dn, entry := range sc.entries { 64 | match = needle.FindStringIndex(dn) 65 | if match != nil { 66 | results = append(results, EntryMatch{ 67 | "ObjectDN", dn, "", "", -1, match[0], match[1], 68 | }) 69 | } 70 | 71 | for _, attr := range entry.Attributes { 72 | attrName := attr.Name 73 | match = needle.FindStringIndex(attrName) 74 | if match != nil { 75 | results = append(results, EntryMatch{ 76 | "AttrName", dn, attrName, "", -1, match[0], match[1], 77 | }) 78 | } 79 | 80 | for idx, attrValue := range attr.Values { 81 | match = needle.FindStringIndex(attrValue) 82 | if match != nil { 83 | results = append(results, EntryMatch{ 84 | "AttrVal", dn, attrName, attrValue, idx, match[0], match[1], 85 | }) 86 | } 87 | } 88 | } 89 | } 90 | 91 | return results 92 | } 93 | -------------------------------------------------------------------------------- /tui/dacl.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "sort" 7 | "strconv" 8 | "sync" 9 | 10 | "github.com/Macmod/godap/v2/pkg/ldaputils" 11 | "github.com/Macmod/godap/v2/pkg/sdl" 12 | "github.com/gdamore/tcell/v2" 13 | "github.com/rivo/tview" 14 | ) 15 | 16 | var ( 17 | runControlDacl sync.Mutex 18 | runningDacl bool 19 | 20 | sd *sdl.SecurityDescriptor 21 | parsedAces []ParsedACE 22 | ) 23 | 24 | func parseAces(dst *[]ParsedACE, srcSD *sdl.SecurityDescriptor) { 25 | var samAccountName string 26 | var sidMap map[string]string = make(map[string]string) 27 | var ok bool 28 | 29 | for idx, ace := range srcSD.DACL.Aces { 30 | entry := ParsedACE{ 31 | Idx: idx, 32 | SamAccountName: "", 33 | Type: "", 34 | Inheritance: false, 35 | Scope: "This object only", 36 | NoPropagate: false, 37 | Severity: 0, 38 | Raw: ace, 39 | } 40 | 41 | var ACEFlags int 42 | switch aceVal := ace.(type) { 43 | case *sdl.BASIC_ACE: 44 | sid := ldaputils.ConvertSID(aceVal.SID) 45 | 46 | if aceVal.Header.ACEType == "00" { 47 | entry.Type = "Allow" 48 | } else { 49 | entry.Type = "Deny" 50 | } 51 | 52 | samAccountName, ok = sidMap[sid] 53 | if !ok { 54 | samAccountName, err = lc.FindSamForSID(sid) 55 | if err == nil { 56 | sidMap[sid] = samAccountName 57 | entry.SamAccountName = samAccountName 58 | } else { 59 | entry.SamAccountName = sid 60 | } 61 | } else { 62 | entry.SamAccountName = samAccountName 63 | } 64 | 65 | ACEFlags = ldaputils.HexToInt(aceVal.Header.ACEFlags) 66 | if ACEFlags&sdl.AceFlagsMap["INHERITED_ACE"] != 0 { 67 | entry.Inheritance = true 68 | } 69 | 70 | if ACEFlags&sdl.AceFlagsMap["NO_PROPAGATE_INHERIT_ACE"] != 0 { 71 | entry.NoPropagate = true 72 | } 73 | 74 | permissions := ldaputils.HexToInt(ldaputils.EndianConvert(aceVal.Mask)) 75 | 76 | entry.Mask, entry.Severity = sdl.AceMaskToText(permissions, "") 77 | case *sdl.OBJECT_ACE: 78 | sid := ldaputils.ConvertSID(aceVal.SID) 79 | 80 | if aceVal.Header.ACEType == "05" { 81 | entry.Type = "Allow" 82 | } else { 83 | entry.Type = "Deny" 84 | } 85 | samAccountName, ok = sidMap[sid] 86 | if !ok { 87 | samAccountName, err = lc.FindSamForSID(sid) 88 | if err == nil { 89 | sidMap[sid] = samAccountName 90 | entry.SamAccountName = samAccountName 91 | } else { 92 | entry.SamAccountName = sid 93 | } 94 | } else { 95 | entry.SamAccountName = samAccountName 96 | } 97 | 98 | ACEFlags = ldaputils.HexToInt(aceVal.Header.ACEFlags) 99 | if ACEFlags&sdl.AceFlagsMap["INHERITED_ACE"] != 0 { 100 | entry.Inheritance = true 101 | } 102 | 103 | if ACEFlags&sdl.AceFlagsMap["NO_PROPAGATE_INHERIT_ACE"] != 0 { 104 | entry.NoPropagate = true 105 | } 106 | 107 | permissions := ldaputils.HexToInt(ldaputils.EndianConvert(aceVal.Mask)) 108 | objectType, inheritedObjectType := aceVal.GetObjectAndInheritedType() 109 | entry.Mask, entry.Severity = sdl.AceMaskToText(permissions, objectType) 110 | entry.Scope = sdl.AceFlagsToText(aceVal.Header.ACEFlags, inheritedObjectType) 111 | case *sdl.NOTIMPL_ACE: 112 | // Should not happen under normal circumstances 113 | entry.Type = "NOTIMPL" 114 | } 115 | 116 | *dst = append(*dst, entry) 117 | } 118 | } 119 | 120 | var ( 121 | object string 122 | 123 | daclPage *tview.Flex 124 | objectNameInputDacl *tview.InputField 125 | daclEntriesPanel *tview.Table 126 | acePanel *tview.List 127 | daclOwnerTextView *tview.TextView 128 | controlFlagsTextView *tview.TextView 129 | aceMask *tview.TextView 130 | aceMaskBinary *tview.TextView 131 | ownerPrincipal string 132 | groupPrincipal string 133 | ) 134 | 135 | func queryDacl(target string) { 136 | updateLog("Fetching DACL for '"+target+"'", "yellow") 137 | go app.QueueUpdateDraw(updateDaclEntries) 138 | } 139 | 140 | func initDaclPage(includeCurSchema bool) { 141 | loadRightVars() 142 | loadSchemaVars(includeCurSchema) 143 | 144 | objectNameInputDacl = tview.NewInputField() 145 | objectNameInputDacl. 146 | SetPlaceholder("Type an object's sAMAccountName or DN"). 147 | SetTitle("Object"). 148 | SetBorder(true) 149 | assignInputFieldTheme(objectNameInputDacl) 150 | 151 | acePanel = tview.NewList() 152 | acePanel. 153 | SetTitle("ACE Explorer"). 154 | SetBorder(true) 155 | 156 | daclOwnerTextView = tview.NewTextView() 157 | daclOwnerTextView. 158 | SetTextAlign(tview.AlignCenter). 159 | SetTitle("Owner"). 160 | SetBorder(true) 161 | aceMask = tview.NewTextView() 162 | aceMask. 163 | SetTextAlign(tview.AlignCenter). 164 | SetTitle("ACE Mask"). 165 | SetBorder(true) 166 | 167 | aceMaskBinary = tview.NewTextView() 168 | aceMaskBinary. 169 | SetTextAlign(tview.AlignCenter). 170 | SetTitle("ACE Mask (Binary)"). 171 | SetBorder(true) 172 | 173 | controlFlagsTextView = tview.NewTextView() 174 | controlFlagsTextView. 175 | SetTextAlign(tview.AlignCenter). 176 | SetTitle("ControlFlags"). 177 | SetBorder(true) 178 | 179 | daclEntriesPanel = tview.NewTable() 180 | daclEntriesPanel. 181 | SetFixed(1, 0). 182 | SetSelectable(true, false). 183 | SetEvaluateAllRows(true). 184 | SetTitle("DACL"). 185 | SetBorder(true) 186 | 187 | daclEntriesPanel.SetSelectionChangedFunc(func(row, column int) { 188 | if sd != nil && row <= len(parsedAces) && row > 0 { 189 | ace := parsedAces[row-1] 190 | maskInt := ace.Raw.GetMask() 191 | 192 | aceMask.SetText(strconv.Itoa(maskInt)) 193 | aceMaskBinary.SetText(fmt.Sprintf("%032b", maskInt)) 194 | 195 | acePanel.Clear() 196 | 197 | for _, right := range ace.Mask { 198 | currentRight := right 199 | acePanel.AddItem(currentRight, "", 'x', nil) 200 | } 201 | } 202 | }) 203 | 204 | daclPage = tview.NewFlex().SetDirection(tview.FlexRow). 205 | AddItem( 206 | tview.NewFlex(). 207 | AddItem(objectNameInputDacl, 0, 2, false). 208 | AddItem(daclOwnerTextView, 0, 1, false), 209 | 3, 0, false). 210 | AddItem(tview.NewFlex(). 211 | AddItem(controlFlagsTextView, 14, 0, false). 212 | AddItem(aceMask, 12, 0, false). 213 | AddItem(aceMaskBinary, 0, 1, false), 3, 0, false). 214 | AddItem(daclEntriesPanel, 0, 8, false) 215 | 216 | daclEntriesPanel.SetInputCapture(daclEntriesPanelKeyHandler) 217 | daclPage.SetInputCapture(daclPageKeyHandler) 218 | objectNameInputDacl.SetDoneFunc(func(tcell.Key) { 219 | queryDacl(objectNameInputDacl.GetText()) 220 | }) 221 | } 222 | 223 | type ParsedACE struct { 224 | Idx int 225 | SamAccountName string 226 | Type string 227 | Mask []string 228 | Inheritance bool 229 | Scope string 230 | NoPropagate bool 231 | Severity int 232 | Raw sdl.ACEInt 233 | } 234 | 235 | func selectDaclEntry(aceToSelect sdl.ACEInt) { 236 | for idx, ace := range parsedAces { 237 | if aceToSelect.Encode() == ace.Raw.Encode() { 238 | daclEntriesPanel.Select(idx+1, 0) 239 | } 240 | } 241 | } 242 | 243 | func updateDaclEntries() { 244 | runControlDacl.Lock() 245 | if runningDacl { 246 | runControlDacl.Unlock() 247 | updateLog("Another query is still running...", "yellow") 248 | return 249 | } 250 | runningDacl = true 251 | runControlDacl.Unlock() 252 | 253 | defer func() { 254 | runControlDacl.Lock() 255 | runningDacl = false 256 | runControlDacl.Unlock() 257 | }() 258 | 259 | daclEntriesPanel.Clear() 260 | daclOwnerTextView.SetText("") 261 | controlFlagsTextView.SetText("") 262 | aceMask.SetText("") 263 | aceMaskBinary.SetText("") 264 | 265 | daclEntriesPanel.SetCell(0, 0, tview.NewTableCell("Type").SetSelectable(false)) 266 | daclEntriesPanel.SetCell(0, 1, tview.NewTableCell("Principal").SetSelectable(false)) 267 | daclEntriesPanel.SetCell(0, 2, tview.NewTableCell("Access").SetSelectable(false).SetAlign(tview.AlignCenter)) 268 | daclEntriesPanel.SetCell(0, 3, tview.NewTableCell("Inherited").SetSelectable(false).SetAlign(tview.AlignCenter)) 269 | daclEntriesPanel.SetCell(0, 4, tview.NewTableCell("Scope").SetSelectable(false).SetAlign(tview.AlignCenter)) 270 | daclEntriesPanel.SetCell(0, 5, tview.NewTableCell("No Propagate").SetSelectable(false).SetAlign(tview.AlignCenter)) 271 | 272 | var hexSD string 273 | var readableMask string 274 | var aceType string 275 | var aceInheritance string 276 | var aceNoPropagate string 277 | 278 | object = objectNameInputDacl.GetText() 279 | hexSD, err = lc.GetSecurityDescriptor(object) 280 | 281 | sd = nil 282 | parsedAces = nil 283 | 284 | if err == nil { 285 | sd = sdl.NewSD(hexSD) 286 | 287 | numAces := strconv.Itoa(len(sd.DACL.Aces)) 288 | 289 | updateLog("DACL obtained for '"+object+"' ("+numAces+" ACEs)", "green") 290 | app.SetFocus(daclEntriesPanel) 291 | daclEntriesPanel.ScrollToBeginning() 292 | 293 | controlFlags := sd.GetControl() 294 | controlFlagsTextView.SetText(strconv.Itoa(controlFlags)) 295 | 296 | ownerSID := ldaputils.ConvertSID(sd.Owner) 297 | ownerPrincipal, err = lc.FindSamForSID(ownerSID) 298 | if err == nil { 299 | daclOwnerTextView.SetText(ownerPrincipal) 300 | } else { 301 | daclOwnerTextView.SetText("[red]" + ownerSID) 302 | } 303 | groupPrincipal, err = lc.FindSamForSID(ldaputils.ConvertSID(sd.Group)) 304 | // For AD, groupPrincipal is not relevant, 305 | // so there's no need to show it in the UI 306 | 307 | // Parse the ACEs from the DACL in sd into parsedAces 308 | parseAces(&parsedAces, sd) 309 | 310 | for idx, entry := range parsedAces { 311 | if len(entry.Mask) == 1 { 312 | readableMask = entry.Mask[0] 313 | } else { 314 | readableMask = "Special" 315 | } 316 | 317 | if entry.Severity == 1 { 318 | readableMask = "[purple]" + readableMask 319 | } else if entry.Severity == 2 { 320 | readableMask = "[blue]" + readableMask 321 | } else if entry.Severity == 3 { 322 | readableMask = "[red]" + readableMask 323 | } 324 | 325 | if entry.Type == "Allow" { 326 | aceType = "[green]" + entry.Type 327 | } else { 328 | aceType = "[red]" + entry.Type 329 | } 330 | 331 | if entry.Inheritance { 332 | aceInheritance = "[green]True" 333 | } else { 334 | aceInheritance = "[red]False" 335 | } 336 | 337 | if entry.NoPropagate { 338 | aceNoPropagate = "[green]True" 339 | } else { 340 | aceNoPropagate = "[red]False" 341 | } 342 | 343 | principalName := entry.SamAccountName 344 | if ldaputils.IsSID(principalName) { 345 | principalName = "[red]" + principalName 346 | } 347 | 348 | daclEntriesPanel.SetCell(idx+1, 0, tview.NewTableCell(aceType)) 349 | 350 | daclEntriesPanel.SetCell(idx+1, 1, tview.NewTableCell(principalName)) 351 | 352 | readableMaskCell := tview.NewTableCell(readableMask).SetAlign(tview.AlignCenter) 353 | daclEntriesPanel.SetCell(idx+1, 2, readableMaskCell) 354 | 355 | daclEntriesPanel.SetCell( 356 | idx+1, 3, tview.NewTableCell(aceInheritance).SetAlign(tview.AlignCenter)) 357 | 358 | daclEntriesPanel.SetCell( 359 | idx+1, 4, tview.NewTableCell(entry.Scope).SetAlign(tview.AlignCenter)) 360 | 361 | daclEntriesPanel.SetCell( 362 | idx+1, 5, tview.NewTableCell(aceNoPropagate).SetAlign(tview.AlignCenter)) 363 | } 364 | 365 | daclEntriesPanel.Select(1, 1) 366 | } else { 367 | updateLog(fmt.Sprint(err), "red") 368 | } 369 | } 370 | 371 | func daclRotateFocus() { 372 | currentFocus := app.GetFocus() 373 | 374 | switch currentFocus { 375 | case objectNameInputDacl: 376 | app.SetFocus(daclEntriesPanel) 377 | case daclEntriesPanel: 378 | app.SetFocus(objectNameInputDacl) 379 | } 380 | } 381 | 382 | func loadChangeOwnerForm() { 383 | changeOwnerForm := NewXForm() 384 | changeOwnerForm. 385 | AddTextView("Owner", ownerPrincipal, 0, 1, true, true). 386 | AddTextView("Group", groupPrincipal, 0, 1, true, true). 387 | AddInputField("New Owner", "", 0, nil, nil). 388 | AddInputField("New Owner SID", "", 0, nil, nil). 389 | AddInputField("New Group SID", "", 0, nil, nil) 390 | 391 | newOwnerFormItem := changeOwnerForm.GetFormItemByLabel("New Owner") 392 | newOwnerSIDFormItem := changeOwnerForm.GetFormItemByLabel("New Owner SID") 393 | newGroupSIDFormItem := changeOwnerForm.GetFormItemByLabel("New Group SID") 394 | newOwnerFormItem.(*tview.InputField).SetDoneFunc(func(key tcell.Key) { 395 | newOwnerSID := "" 396 | newGroupSID := "" 397 | 398 | text := newOwnerFormItem.(*tview.InputField).GetText() 399 | if ldaputils.IsSID(text) { 400 | _, err := lc.FindSamForSID(text) 401 | if err == nil { 402 | newOwnerSID = text 403 | newGroupSID, err = lc.FindPrimaryGroupForSID(newOwnerSID) 404 | if err != nil { 405 | newGroupSID, _ = lc.FindSIDForObject(groupPrincipal) 406 | } 407 | } 408 | } else { 409 | // If it's not a SID, it's a sAMAccountName or DN 410 | foundOwnerSid, err := lc.FindSIDForObject(text) 411 | if err == nil { 412 | newOwnerSID = foundOwnerSid 413 | newGroupSID, err = lc.FindPrimaryGroupForSID(newOwnerSID) 414 | if err != nil { 415 | newGroupSID, _ = lc.FindSIDForObject(groupPrincipal) 416 | } 417 | } 418 | } 419 | 420 | newOwnerSIDFormItem.(*tview.InputField).SetText(newOwnerSID) 421 | newGroupSIDFormItem.(*tview.InputField).SetText(newGroupSID) 422 | }) 423 | 424 | changeOwnerForm. 425 | AddButton("Go Back", func() { 426 | app.SetRoot(appPanel, true).SetFocus(daclEntriesPanel) 427 | }). 428 | AddButton("Update", func() { 429 | newOwnerSID := newOwnerSIDFormItem.(*tview.InputField).GetText() 430 | newGroupSID := newGroupSIDFormItem.(*tview.InputField).GetText() 431 | if newOwnerSID == "" || newGroupSID == "" { 432 | updateLog("Owner SID and Group SID can't be empty", "red") 433 | app.SetRoot(appPanel, true).SetFocus(daclEntriesPanel) 434 | return 435 | } 436 | 437 | encodedOwnerSID, err := ldaputils.EncodeSID(newOwnerSID) 438 | if err != nil { 439 | updateLog(fmt.Sprint(err), "red") 440 | app.SetRoot(appPanel, true).SetFocus(daclEntriesPanel) 441 | return 442 | } 443 | 444 | encodedGroupSID, err := ldaputils.EncodeSID(newGroupSID) 445 | if err != nil { 446 | updateLog(fmt.Sprint(err), "red") 447 | app.SetRoot(appPanel, true).SetFocus(daclEntriesPanel) 448 | return 449 | } 450 | 451 | sd.SetOwnerAndGroup( 452 | string(encodedOwnerSID), 453 | string(encodedGroupSID), 454 | ) 455 | 456 | newSd, _ := hex.DecodeString(sd.Encode()) 457 | 458 | err = lc.ModifyDACL(object, string(newSd)) 459 | 460 | if err == nil { 461 | newOwner := changeOwnerForm.GetFormItemByLabel("New Owner").(*tview.InputField).GetText() 462 | 463 | updateLog("Owner for '"+object+"' changed to '"+newOwner+"'", "green") 464 | 465 | go app.QueueUpdateDraw(updateDaclEntries) 466 | } else { 467 | updateLog(fmt.Sprint(err), "red") 468 | } 469 | app.SetRoot(appPanel, true).SetFocus(daclEntriesPanel) 470 | }) 471 | 472 | changeOwnerForm. 473 | SetTitle("Change DACL Owner (" + object + ")"). 474 | SetBorder(true) 475 | 476 | //assignFormTheme(changeOwnerForm) 477 | 478 | changeOwnerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { 479 | if event.Key() == tcell.KeyEscape { 480 | app.SetRoot(appPanel, true).SetFocus(daclEntriesPanel) 481 | return nil 482 | } 483 | return event 484 | }) 485 | 486 | app.SetRoot(changeOwnerForm, true).SetFocus(changeOwnerForm) 487 | } 488 | 489 | func loadChangeControlFlagsForm() { 490 | if sd == nil { 491 | return 492 | } 493 | 494 | updateControlFlagsForm := NewXForm() 495 | 496 | controlFlags := sd.GetControl() 497 | 498 | checkboxState := controlFlags 499 | 500 | updateControlFlagsForm. 501 | AddTextView("Raw ControlFlag Value", strconv.Itoa(checkboxState), 0, 1, false, true) 502 | 503 | controlFlagsKeys := make([]int, 0) 504 | for key := range ldaputils.SDControlFlags { 505 | controlFlagsKeys = append(controlFlagsKeys, key) 506 | } 507 | sort.Ints(controlFlagsKeys) 508 | 509 | for _, val := range controlFlagsKeys { 510 | flagVal := val 511 | updateControlFlagsForm.AddCheckbox( 512 | ldaputils.SDControlFlags[flagVal], 513 | controlFlags&flagVal != 0, 514 | func(checked bool) { 515 | if checked { 516 | checkboxState |= flagVal 517 | } else { 518 | checkboxState &^= flagVal 519 | } 520 | 521 | flagPreview := updateControlFlagsForm.GetFormItemByLabel("Raw ControlFlag Value").(*tview.TextView) 522 | if flagPreview != nil { 523 | flagPreview.SetText(strconv.Itoa(checkboxState)) 524 | } 525 | }) 526 | } 527 | 528 | updateControlFlagsForm. 529 | AddButton("Go Back", func() { 530 | app.SetRoot(appPanel, true).SetFocus(daclEntriesPanel) 531 | }). 532 | AddButton("Update", func() { 533 | 534 | sd.Header.Control = ldaputils.EndianConvert(fmt.Sprintf("%04x", checkboxState)) 535 | newSd, _ := hex.DecodeString(sd.Encode()) 536 | 537 | err = lc.ModifyDACL(object, string(newSd)) 538 | 539 | if err == nil { 540 | updateLog("Control flags updated for '"+object+"'", "green") 541 | go app.QueueUpdateDraw(updateDaclEntries) 542 | } else { 543 | updateLog(fmt.Sprint(err), "red") 544 | } 545 | 546 | app.SetRoot(appPanel, true).SetFocus(daclEntriesPanel) 547 | }) 548 | 549 | updateControlFlagsForm.SetTitle("ControlFlags Editor").SetBorder(true) 550 | 551 | //assignFormTheme(updateControlFlagsForm) 552 | updateControlFlagsForm.SetItemPadding(0) 553 | 554 | app.SetRoot(updateControlFlagsForm, true).SetFocus(updateControlFlagsForm) 555 | } 556 | 557 | func exportCurrentSD() { 558 | if sd == nil { 559 | updateLog("An object was not queried yet", "red") 560 | return 561 | } 562 | 563 | encodedSD := sd.Encode() 564 | 565 | exportMap := make(map[string]any) 566 | exportMap["Query"] = object 567 | exportMap["HexSD"] = encodedSD 568 | 569 | exportMap["ParsedDACL"] = parsedAces 570 | 571 | writeDataExport(exportMap, "sd", "security_descriptor") 572 | } 573 | 574 | func daclPageKeyHandler(event *tcell.EventKey) *tcell.EventKey { 575 | if event.Key() == tcell.KeyTab || event.Key() == tcell.KeyBacktab { 576 | daclRotateFocus() 577 | return nil 578 | } 579 | 580 | if sd == nil { 581 | return event 582 | } 583 | 584 | switch event.Key() { 585 | case tcell.KeyCtrlO: 586 | loadChangeOwnerForm() 587 | return nil 588 | case tcell.KeyCtrlK: 589 | loadChangeControlFlagsForm() 590 | return nil 591 | case tcell.KeyCtrlS: 592 | exportCurrentSD() 593 | return nil 594 | } 595 | 596 | return event 597 | } 598 | 599 | func daclEntriesPanelKeyHandler(event *tcell.EventKey) *tcell.EventKey { 600 | if sd == nil { 601 | return event 602 | } 603 | 604 | switch event.Key() { 605 | case tcell.KeyDelete: 606 | selectionIdx, _ := daclEntriesPanel.GetSelection() 607 | loadDeleteAceForm(selectionIdx) 608 | return nil 609 | case tcell.KeyCtrlN: 610 | loadAceEditorForm(-1) 611 | return nil 612 | case tcell.KeyCtrlE: 613 | selectionIdx, _ := daclEntriesPanel.GetSelection() 614 | loadAceEditorForm(selectionIdx) 615 | return nil 616 | } 617 | 618 | return event 619 | } 620 | -------------------------------------------------------------------------------- /tui/dns.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | /* 4 | {Reference} 5 | - [MS-DNSP]: Domain Name Service (DNS) Server Management Protocol 6 | https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/f97756c9-3783-428b-9451-b376f877319a 7 | */ 8 | 9 | import ( 10 | "fmt" 11 | "regexp" 12 | "strings" 13 | "sync" 14 | "time" 15 | 16 | "github.com/Macmod/godap/v2/pkg/adidns" 17 | "github.com/gdamore/tcell/v2" 18 | "github.com/rivo/tview" 19 | ) 20 | 21 | var ( 22 | dnsTreePanel *tview.TreeView 23 | dnsQueryPanel *tview.InputField 24 | 25 | dnsSidePanel *tview.Pages 26 | dnsZoneProps *tview.Table 27 | dnsNodeRecords *tview.TreeView 28 | 29 | dnsNodeFilter *tview.InputField 30 | dnsZoneFilter *tview.InputField 31 | 32 | dnsPage *tview.Flex 33 | 34 | dnsRunControl sync.Mutex 35 | dnsRunning bool 36 | ) 37 | 38 | var domainZones []adidns.DNSZone 39 | var forestZones []adidns.DNSZone 40 | 41 | var zoneCache = make(map[string]adidns.DNSZone, 0) 42 | var nodeCache = make(map[string]adidns.DNSNode, 0) 43 | 44 | func getParentZone(objectDN string) (adidns.DNSZone, error) { 45 | objectDNParts := strings.Split(objectDN, ",") 46 | 47 | zone, zoneOk := zoneCache[objectDN] 48 | if zoneOk { 49 | return zone, nil 50 | } 51 | 52 | if len(objectDNParts) > 1 { 53 | parentZoneDN := strings.Join(objectDNParts[1:], ",") 54 | parentZone, zoneOk := zoneCache[parentZoneDN] 55 | if zoneOk { 56 | return parentZone, nil 57 | } else { 58 | return adidns.DNSZone{}, fmt.Errorf("Parent zone not found in the cache") 59 | } 60 | } 61 | 62 | return adidns.DNSZone{}, fmt.Errorf("Malformed object DN") 63 | } 64 | 65 | func exportADIDNSToFile(currentNode *tview.TreeNode) { 66 | exportMap := make(map[string]any) 67 | 68 | currentNode.Walk(func(node, parent *tview.TreeNode) bool { 69 | if node.GetReference() != nil { 70 | objectDN := node.GetReference().(string) 71 | 72 | zone, zoneOk := zoneCache[objectDN] 73 | node, nodeOk := nodeCache[objectDN] 74 | 75 | nodesMap := make(map[string]any, 0) 76 | 77 | if zoneOk { 78 | zoneProps := make(map[string]any, 0) 79 | for _, prop := range zone.Props { 80 | propName := adidns.FindPropName(prop.Id) 81 | zoneProps[propName] = prop.ExportFormat() 82 | } 83 | 84 | exportMap[objectDN] = map[string]any{ 85 | "Zone": map[string]any{ 86 | "Name": zone.Name, 87 | "DN": zone.DN, 88 | "Props": zoneProps, 89 | }, 90 | "Nodes": nodesMap, 91 | } 92 | } else if nodeOk { 93 | records := node.Records 94 | 95 | recordsObj := make([]any, 0) 96 | for _, rec := range records { 97 | recordType := rec.PrintType() 98 | recordsObj = append(recordsObj, map[string]any{ 99 | "Type": recordType, 100 | "Value": rec, 101 | "Data": rec.GetRecordData(), 102 | }) 103 | } 104 | 105 | parentZone, err := getParentZone(objectDN) 106 | if err == nil { 107 | // Since we're walking the tree it's safe to assume that 108 | // zones will come before their child nodes, therefore 109 | // all zone exports will fall in the alreadyExported branch 110 | // The only way to get the other branch (alreadyExported) is if 111 | // the user exports a node itself, in which case 112 | // we must fetch the parent zone's properties 113 | // to include in the export 114 | _, alreadyExported := exportMap[parentZone.DN] 115 | if !alreadyExported { 116 | parentZoneProps := make(map[string]any, 0) 117 | for _, prop := range parentZone.Props { 118 | propName := adidns.FindPropName(prop.Id) 119 | parentZoneProps[propName] = prop.ExportFormat() 120 | } 121 | 122 | exportMap[parentZone.DN] = map[string]any{ 123 | "Zone": map[string]any{ 124 | "Name": parentZone.Name, 125 | "DN": parentZone.DN, 126 | "Props": parentZoneProps, 127 | }, 128 | "Nodes": nodesMap, 129 | } 130 | } 131 | 132 | parentZone := (exportMap[parentZone.DN]).(map[string]any) 133 | parentZoneNodes := parentZone["Nodes"].(map[string]any) 134 | parentZoneNodes[node.DN] = map[string]any{ 135 | "Name": node.Name, 136 | "Records": recordsObj, 137 | } 138 | } 139 | } 140 | } 141 | return true 142 | }) 143 | 144 | writeDataExport(exportMap, "dns", "adidns") 145 | } 146 | 147 | func showZoneDetails(zone *adidns.DNSZone) { 148 | dnsSidePanel.SetTitle("Zone Properties") 149 | dnsSidePanel.SwitchToPage("zone-props") 150 | 151 | propsMap := make(map[uint32]adidns.DNSProperty, 0) 152 | for _, prop := range zone.Props { 153 | propsMap[prop.Id] = prop 154 | } 155 | 156 | dnsZoneProps.SetCell(0, 0, tview.NewTableCell("Id").SetSelectable(false)) 157 | dnsZoneProps.SetCell(0, 1, tview.NewTableCell("Description").SetSelectable(false)) 158 | dnsZoneProps.SetCell(0, 2, tview.NewTableCell("Value").SetSelectable(false)) 159 | 160 | idx := 1 161 | for _, prop := range adidns.DnsPropertyIds { 162 | dnsZoneProps.SetCell(idx, 0, tview.NewTableCell(fmt.Sprint(prop.Id))) 163 | dnsZoneProps.SetCell(idx, 1, tview.NewTableCell(prop.Name)) 164 | 165 | mappedProp, ok := propsMap[prop.Id] 166 | if ok { 167 | mappedPropStr := fmt.Sprintf("%v", mappedProp.Data) 168 | if FormatAttrs { 169 | mappedPropStr = mappedProp.PrintFormat(TimeFormat) 170 | } 171 | 172 | if Colors { 173 | color, change := adidns.GetPropCellColor(mappedProp.Id, mappedPropStr) 174 | if change { 175 | mappedPropStr = fmt.Sprintf("[%s]%s[c]", color, mappedPropStr) 176 | } 177 | } 178 | 179 | dnsZoneProps.SetCell(idx, 2, tview.NewTableCell(mappedPropStr)) 180 | } else { 181 | notSpecifiedVal := "Not specified" 182 | if Colors { 183 | notSpecifiedVal = fmt.Sprintf("[gray]%s[c]", notSpecifiedVal) 184 | } 185 | 186 | dnsZoneProps.SetCell(idx, 2, tview.NewTableCell(notSpecifiedVal)) 187 | } 188 | idx += 1 189 | } 190 | } 191 | 192 | type recordRef struct { 193 | nodeDN string 194 | idx int 195 | } 196 | 197 | func reloadADIDNSZone(currentNode *tview.TreeNode) { 198 | objectDN := currentNode.GetReference().(string) 199 | 200 | updateLog("Fetching nodes for zone '"+objectDN+"'...", "yellow") 201 | 202 | numLoadedNodes := loadZoneNodes(currentNode) 203 | 204 | if numLoadedNodes >= 0 { 205 | updateLog(fmt.Sprintf("Loaded %d nodes (%s)", numLoadedNodes, objectDN), "green") 206 | } 207 | 208 | if len(currentNode.GetChildren()) != 0 && !currentNode.IsExpanded() { 209 | currentNode.SetExpanded(true) 210 | } 211 | } 212 | 213 | func reloadADIDNSNode(currentNode *tview.TreeNode) { 214 | objectDN := currentNode.GetReference().(string) 215 | 216 | node, err := lc.GetADIDNSNode(objectDN) 217 | nodeCache[node.DN] = node 218 | 219 | if err == nil { 220 | updateLog(fmt.Sprintf("Loaded node '%s'", node.DN), "green") 221 | } else { 222 | updateLog(fmt.Sprint(err), "red") 223 | } 224 | 225 | showDetails(node.DN) 226 | } 227 | 228 | func showDNSNodeDetails(node *adidns.DNSNode, targetTree *tview.TreeView) { 229 | rootNode := tview.NewTreeNode(node.Name) 230 | 231 | for idx, record := range node.Records { 232 | unixTimestamp := record.UnixTimestamp() 233 | timeObj := time.Unix(unixTimestamp, 0) 234 | 235 | formattedTime := fmt.Sprintf("%d", unixTimestamp) 236 | timeDistance := time.Since(timeObj) 237 | if FormatAttrs { 238 | if unixTimestamp != -1 { 239 | formattedTime = timeObj.Format(TimeFormat) 240 | } else { 241 | formattedTime = "static" 242 | } 243 | } 244 | 245 | if Colors { 246 | daysDiff := timeDistance.Hours() / 24 247 | color := "gray" 248 | if unixTimestamp != -1 { 249 | if daysDiff <= 7 { 250 | color = "green" 251 | } else if daysDiff <= 90 { 252 | color = "yellow" 253 | } else { 254 | color = "red" 255 | } 256 | } 257 | 258 | formattedTime = fmt.Sprintf("[%s]%s[c]", color, formattedTime) 259 | } 260 | 261 | recordName := fmt.Sprintf( 262 | "%s [TTL=%d] (%s)", 263 | record.PrintType(), 264 | record.TTLSeconds, 265 | formattedTime, 266 | ) 267 | 268 | recordTreeNode := tview.NewTreeNode(recordName). 269 | SetReference(recordRef{node.DN, idx}) 270 | 271 | parsedRecord := record.GetRecordData() 272 | recordFields := adidns.DumpRecordFields(parsedRecord) 273 | for idx, field := range recordFields { 274 | fieldName := tview.Escape(fmt.Sprintf("%s=%v", field.Name, field.Value)) 275 | fieldTreeNode := tview.NewTreeNode(fieldName).SetReference(idx) 276 | recordTreeNode.AddChild(fieldTreeNode) 277 | } 278 | 279 | rootNode.AddChild(recordTreeNode) 280 | } 281 | 282 | targetTree.SetRoot(rootNode) 283 | go func() { 284 | app.Draw() 285 | }() 286 | } 287 | 288 | func showDetails(objectDN string) { 289 | zone, ok := zoneCache[objectDN] 290 | if ok { 291 | showZoneDetails(&zone) 292 | } 293 | 294 | node, ok := nodeCache[objectDN] 295 | if ok { 296 | parentZone, err := getParentZone(objectDN) 297 | if err == nil { 298 | dnsSidePanel.SetTitle(fmt.Sprintf("Records (%s)", parentZone.Name)) 299 | } else { 300 | dnsSidePanel.SetTitle("Records") 301 | } 302 | dnsSidePanel.SwitchToPage("node-records") 303 | 304 | showDNSNodeDetails(&node, dnsNodeRecords) 305 | } 306 | } 307 | 308 | func loadZoneNodes(zoneNode *tview.TreeNode) int { 309 | zoneDN := zoneNode.GetReference().(string) 310 | _, isZone := zoneCache[zoneDN] 311 | if !isZone { 312 | updateLog("The selected tree node is not a DNS zone", "red") 313 | return -1 314 | } 315 | 316 | nodes, err := lc.GetADIDNSNodes(zoneDN) 317 | if err != nil { 318 | updateLog(fmt.Sprint(err), "red") 319 | return -1 320 | } 321 | 322 | zoneNode.ClearChildren() 323 | 324 | nodeFilter := dnsNodeFilter.GetText() 325 | nodeRegexp, _ := regexp.Compile(nodeFilter) 326 | 327 | for _, node := range nodes { 328 | nodeCache[node.DN] = node 329 | 330 | nodeMatch := nodeRegexp.FindStringIndex(node.Name) 331 | if nodeMatch == nil { 332 | continue 333 | } 334 | 335 | nodeName := node.Name 336 | if Emojis { 337 | nodeName = "📃" + nodeName 338 | } 339 | 340 | treeNode := tview.NewTreeNode(nodeName). 341 | SetReference(node.DN). 342 | SetSelectable(true). 343 | SetExpanded(false) 344 | 345 | zoneNode.AddChild(treeNode) 346 | } 347 | 348 | return len(nodes) 349 | } 350 | 351 | func initADIDNSPage() { 352 | dnsQueryPanel = tview.NewInputField() 353 | dnsQueryPanel. 354 | SetPlaceholder("Type a DNS zone or leave it blank and hit enter to query all zones"). 355 | SetTitle("Zone Search"). 356 | SetBorder(true) 357 | assignInputFieldTheme(dnsQueryPanel) 358 | 359 | dnsNodeFilter = tview.NewInputField() 360 | dnsNodeFilter. 361 | SetPlaceholder("Regex for dnsNode name"). 362 | SetTitle("dnsNode Filter"). 363 | SetBorder(true) 364 | assignInputFieldTheme(dnsNodeFilter) 365 | 366 | dnsZoneFilter = tview.NewInputField() 367 | dnsZoneFilter. 368 | SetPlaceholder("Regex for dnsZone name"). 369 | SetTitle("dnsZone Filter"). 370 | SetBorder(true) 371 | assignInputFieldTheme(dnsZoneFilter) 372 | 373 | dnsZoneProps = tview.NewTable(). 374 | SetSelectable(true, true). 375 | SetEvaluateAllRows(true) 376 | 377 | dnsNodeRecords = tview.NewTreeView() 378 | 379 | dnsTreePanel = tview.NewTreeView() 380 | dnsTreePanel. 381 | SetTitle("Zones & Nodes"). 382 | SetBorder(true) 383 | 384 | dnsTreePanel.SetChangedFunc(func(objNode *tview.TreeNode) { 385 | dnsZoneProps.Clear() 386 | 387 | objNodeRef := objNode.GetReference() 388 | if objNodeRef == nil { 389 | return 390 | } 391 | 392 | nodeDN := objNodeRef.(string) 393 | showDetails(nodeDN) 394 | }) 395 | 396 | dnsZoneFilter.SetChangedFunc(func(text string) { 397 | rebuildDnsTree(dnsTreePanel.GetRoot()) 398 | }) 399 | 400 | dnsNodeFilter.SetChangedFunc(func(text string) { 401 | rebuildDnsTree(dnsTreePanel.GetRoot()) 402 | }) 403 | 404 | dnsQueryPanel.SetDoneFunc(dnsQueryDoneHandler) 405 | 406 | dnsTreePanel.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { 407 | currentNode := dnsTreePanel.GetCurrentNode() 408 | if currentNode == nil || currentNode.GetReference() == nil { 409 | return event 410 | } 411 | 412 | level := currentNode.GetLevel() 413 | 414 | switch event.Rune() { 415 | case 'r', 'R': 416 | go app.QueueUpdateDraw(func() { 417 | if level == 0 { 418 | go queryDnsZones(dnsQueryPanel.GetText()) 419 | } else if level == 1 { 420 | reloadADIDNSZone(currentNode) 421 | } else if level == 2 { 422 | reloadADIDNSNode(currentNode) 423 | } 424 | }) 425 | 426 | return nil 427 | } 428 | 429 | switch event.Key() { 430 | case tcell.KeyRight: 431 | if len(currentNode.GetChildren()) != 0 && !currentNode.IsExpanded() { 432 | currentNode.SetExpanded(true) 433 | } 434 | return nil 435 | case tcell.KeyLeft: 436 | if currentNode.IsExpanded() { // Collapse current node 437 | currentNode.SetExpanded(false) 438 | dnsTreePanel.SetCurrentNode(currentNode) 439 | } else { // Collapse parent node 440 | pathToCurrent := dnsTreePanel.GetPath(currentNode) 441 | if len(pathToCurrent) > 1 { 442 | parentNode := pathToCurrent[len(pathToCurrent)-2] 443 | parentNode.SetExpanded(false) 444 | dnsTreePanel.SetCurrentNode(parentNode) 445 | } 446 | } 447 | return nil 448 | case tcell.KeyDelete: 449 | if currentNode.GetReference() == nil { 450 | return nil 451 | } 452 | 453 | openDeleteObjectForm(currentNode, func() { 454 | if level == 1 { 455 | go queryDnsZones(dnsQueryPanel.GetText()) 456 | } else if level == 2 { 457 | pathToCurrent := dnsTreePanel.GetPath(currentNode) 458 | if len(pathToCurrent) > 1 { 459 | parentNode := pathToCurrent[len(pathToCurrent)-2] 460 | reloadADIDNSZone(parentNode) 461 | } 462 | } 463 | }) 464 | 465 | return nil 466 | case tcell.KeyCtrlS: 467 | exportADIDNSToFile(currentNode) 468 | case tcell.KeyCtrlN: 469 | if currentNode == dnsTreePanel.GetRoot() { 470 | openCreateZoneForm() 471 | } else { 472 | if level == 1 { 473 | openCreateNodeForm(currentNode) 474 | } else if level == 2 { 475 | parentZone := getParentNode(currentNode, dnsTreePanel) 476 | openCreateNodeForm(parentZone) 477 | } 478 | } 479 | case tcell.KeyCtrlE: 480 | if level == 1 { 481 | // TODO: Edit zone properties 482 | } else if level == 2 { 483 | openUpdateNodeForm(currentNode) 484 | } 485 | } 486 | 487 | return event 488 | }) 489 | 490 | dnsNodeRecords.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { 491 | currentNode := dnsNodeRecords.GetCurrentNode() 492 | if currentNode == nil || currentNode.GetReference() == nil { 493 | return event 494 | } 495 | 496 | switch event.Key() { 497 | case tcell.KeyCtrlE: 498 | node := dnsTreePanel.GetCurrentNode() 499 | openUpdateNodeForm(node) 500 | return nil 501 | case tcell.KeyDelete: 502 | if currentNode.GetLevel() == 1 { 503 | openDeleteRecordForm(currentNode) 504 | } else if currentNode.GetLevel() == 2 { 505 | pathToCurrent := dnsNodeRecords.GetPath(currentNode) 506 | if len(pathToCurrent) > 1 { 507 | parentNode := pathToCurrent[len(pathToCurrent)-2] 508 | openDeleteRecordForm(parentNode) 509 | } 510 | } 511 | 512 | return nil 513 | } 514 | 515 | return event 516 | }) 517 | 518 | dnsSidePanel = tview.NewPages() 519 | dnsSidePanel. 520 | AddPage("zone-props", dnsZoneProps, true, true). 521 | AddPage("node-records", dnsNodeRecords, true, true). 522 | SetBorder(true) 523 | 524 | dnsPage = tview.NewFlex().SetDirection(tview.FlexRow). 525 | AddItem( 526 | tview.NewFlex(). 527 | AddItem(dnsQueryPanel, 0, 2, false). 528 | AddItem(dnsNodeFilter, 0, 1, false). 529 | AddItem(dnsZoneFilter, 0, 1, false), 530 | 3, 0, false, 531 | ). 532 | AddItem( 533 | tview.NewFlex(). 534 | AddItem(dnsTreePanel, 0, 1, false). 535 | AddItem(dnsSidePanel, 0, 1, false), 536 | 0, 8, false, 537 | ) 538 | 539 | dnsPage.SetInputCapture(dnsPageKeyHandler) 540 | } 541 | 542 | func dnsPageKeyHandler(event *tcell.EventKey) *tcell.EventKey { 543 | if event.Key() == tcell.KeyTab || event.Key() == tcell.KeyBacktab { 544 | dnsRotateFocus() 545 | return nil 546 | } 547 | 548 | return event 549 | } 550 | 551 | func rebuildDnsTree(rootNode *tview.TreeNode) int { 552 | if rootNode == nil { 553 | return 0 554 | } 555 | 556 | expandedZones := make(map[string]bool) 557 | childrenZones := rootNode.GetChildren() 558 | for _, child := range childrenZones { 559 | ref, ok := child.GetReference().(string) 560 | if ok && child.IsExpanded() { 561 | expandedZones[ref] = true 562 | } 563 | } 564 | rootNode.ClearChildren() 565 | 566 | zoneFilter := dnsZoneFilter.GetText() 567 | zoneRegexp, err := regexp.Compile(zoneFilter) 568 | if err != nil { 569 | updateLog("Invalid zone filter '"+zoneFilter+"' specified", "red") 570 | return -1 571 | } 572 | 573 | totalNodes := 0 574 | for _, zone := range domainZones { 575 | zoneCache[zone.DN] = zone 576 | zoneMatch := zoneRegexp.FindStringIndex(zone.Name) 577 | 578 | if zoneMatch == nil { 579 | continue 580 | } 581 | 582 | zoneNodeName := zone.Name 583 | if Emojis { 584 | zoneNodeName = "🌐" + zoneNodeName 585 | } 586 | 587 | childNode := tview.NewTreeNode(zoneNodeName). 588 | SetReference(zone.DN). 589 | SetExpanded(expandedZones[zone.DN]). 590 | SetSelectable(true) 591 | 592 | totalNodes += loadZoneNodes(childNode) 593 | rootNode.AddChild(childNode) 594 | } 595 | 596 | for _, zone := range forestZones { 597 | zoneCache[zone.DN] = zone 598 | zoneMatch := zoneRegexp.FindStringIndex(zone.Name) 599 | 600 | if zoneMatch == nil { 601 | continue 602 | } 603 | 604 | zoneNodeName := zone.Name 605 | if Emojis { 606 | zoneNodeName = "🌲" + zoneNodeName 607 | } 608 | 609 | childNode := tview.NewTreeNode(zoneNodeName). 610 | SetReference(zone.DN). 611 | SetExpanded(expandedZones[zone.DN]). 612 | SetSelectable(true) 613 | 614 | totalNodes += loadZoneNodes(childNode) 615 | rootNode.AddChild(childNode) 616 | } 617 | 618 | go func() { 619 | app.Draw() 620 | }() 621 | return totalNodes 622 | } 623 | 624 | func queryDnsZones(targetZone string) { 625 | dnsRunControl.Lock() 626 | if dnsRunning { 627 | dnsRunControl.Unlock() 628 | updateLog("Another query is still running...", "yellow") 629 | return 630 | } 631 | dnsRunning = true 632 | dnsRunControl.Unlock() 633 | 634 | clear(nodeCache) 635 | clear(zoneCache) 636 | clear(domainZones) 637 | clear(forestZones) 638 | 639 | app.QueueUpdateDraw(func() { 640 | updateLog("Querying ADIDNS zones...", "yellow") 641 | 642 | domainZones, _ = lc.GetADIDNSZones(targetZone, false) 643 | forestZones, _ = lc.GetADIDNSZones(targetZone, true) 644 | 645 | totalZones := len(domainZones) + len(forestZones) 646 | if totalZones == 0 { 647 | updateLog("No ADIDNS zones found", "red") 648 | rootNode.ClearChildren() 649 | 650 | dnsRunControl.Lock() 651 | dnsRunning = false 652 | dnsRunControl.Unlock() 653 | return 654 | } 655 | 656 | // Setting up root node 657 | rootNode := tview.NewTreeNode(lc.RootDN). 658 | SetReference(lc.RootDN). 659 | SetSelectable(true) 660 | dnsTreePanel. 661 | SetRoot(rootNode). 662 | SetCurrentNode(rootNode) 663 | 664 | totalNodes := rebuildDnsTree(rootNode) 665 | 666 | updateLog(fmt.Sprintf("Found %d ADIDNS zones and %d nodes", totalZones, totalNodes), "green") 667 | app.SetFocus(dnsTreePanel) 668 | }) 669 | 670 | dnsRunControl.Lock() 671 | dnsRunning = false 672 | dnsRunControl.Unlock() 673 | } 674 | 675 | func dnsQueryDoneHandler(key tcell.Key) { 676 | go queryDnsZones(dnsQueryPanel.GetText()) 677 | } 678 | 679 | func dnsRotateFocus() { 680 | currentFocus := app.GetFocus() 681 | 682 | switch currentFocus { 683 | case dnsTreePanel: 684 | app.SetFocus(dnsQueryPanel) 685 | case dnsQueryPanel: 686 | app.SetFocus(dnsNodeFilter) 687 | case dnsNodeFilter: 688 | app.SetFocus(dnsZoneFilter) 689 | case dnsZoneFilter: 690 | app.SetFocus(dnsZoneProps) 691 | case dnsZoneProps: 692 | app.SetFocus(dnsTreePanel) 693 | } 694 | } 695 | -------------------------------------------------------------------------------- /tui/dnsmodify.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/Macmod/godap/v2/pkg/adidns" 9 | "github.com/gdamore/tcell/v2" 10 | "github.com/rivo/tview" 11 | ) 12 | 13 | var supportedRecordTypes []string = []string{ 14 | "A", "AAAA", "CNAME", "TXT", "NS", "SOA", "SRV", "MX", "PTR", 15 | "MD", "MF", "MB", "MG", "MR", "DNAME", 16 | "HINFO", "ISDN", "X25", "LOC", "AFSDB", "RT", 17 | } 18 | 19 | func addZoneHandler(zoneForm *XForm, currentFocus tview.Primitive) func() { 20 | return func() { 21 | zoneName := zoneForm.GetFormItemByLabel("Zone Name").(*tview.InputField).GetText() 22 | zoneAllowUpdate, _ := zoneForm.GetFormItemByLabel("Updates").(*tview.DropDown).GetCurrentOption() 23 | zoneContainer, _ := zoneForm.GetFormItemByLabel("Container").(*tview.DropDown).GetCurrentOption() 24 | zoneNS := zoneForm.GetFormItemByLabel("NameServer").(*tview.InputField).GetText() 25 | zoneEmail := zoneForm.GetFormItemByLabel("AdminEmail").(*tview.InputField).GetText() 26 | 27 | propType := adidns.MakeProp(0x1, []byte{1, 0, 0, 0}) 28 | propAllowUpdate := adidns.MakeProp(0x2, []byte{byte(zoneAllowUpdate)}) 29 | propNoRefresh := adidns.MakeProp(0x10, []byte{168}) 30 | propRefresh := adidns.MakeProp(0x20, []byte{168}) 31 | propAging := adidns.MakeProp(0x40, []byte{0}) 32 | propScavDa := adidns.MakeProp(0x90, []byte{}) 33 | propAutoNsDa := adidns.MakeProp(0x92, []byte{}) 34 | 35 | defaultProps := []adidns.DNSProperty{ 36 | propType, 37 | propAllowUpdate, 38 | propNoRefresh, 39 | propRefresh, 40 | propAging, 41 | propScavDa, 42 | propAutoNsDa, 43 | } 44 | 45 | isForest := zoneContainer == 1 46 | zoneDN, err := lc.AddADIDNSZone(zoneName, defaultProps, isForest) 47 | 48 | if err != nil { 49 | updateLog(fmt.Sprint(err), "red") 50 | app.SetRoot(appPanel, true).SetFocus(currentFocus) 51 | return 52 | } 53 | 54 | // Basic records required so that 55 | // the DNS will synchronize the zone from 56 | // Active Directory 57 | recSOA := adidns.MakeDNSRecord( 58 | &adidns.SOARecord{1, 900, 600, 86400, 3600, zoneNS, zoneEmail}, 59 | 0x06, 60 | 3600, 61 | ) 62 | 63 | recNS := adidns.MakeDNSRecord(&adidns.NSRecord{zoneNS}, 0x02, 3600) 64 | 65 | defaultRecords := []adidns.DNSRecord{ 66 | recSOA, 67 | recNS, 68 | } 69 | 70 | _, err = lc.AddADIDNSNode("@", zoneDN, defaultRecords) 71 | if err == nil { 72 | updateLog(fmt.Sprintf("Zone '%s' created successfully!", zoneName), "green") 73 | } else { 74 | updateLog(fmt.Sprintf("Zone '%s' created without SOA & NS records - a problem might have occurred.", zoneName), "yellow") 75 | } 76 | 77 | go queryDnsZones(dnsQueryPanel.GetText()) 78 | app.SetRoot(appPanel, true).SetFocus(currentFocus) 79 | } 80 | } 81 | 82 | func openCreateZoneForm() { 83 | currentFocus := app.GetFocus() 84 | 85 | zoneForm := NewXForm(). 86 | AddInputField("Zone Name", "", 0, nil, nil). 87 | AddDropDown("Container", []string{"DomainDnsZones", "ForestDnsZones"}, 0, nil). 88 | AddDropDown("Updates", []string{"None", "Nonsecure and secure", "Secure only"}, 0, nil). 89 | AddInputField("NameServer", "", 0, nil, nil). 90 | AddInputField("AdminEmail", "", 0, nil, nil) 91 | 92 | zoneNameFormItem := zoneForm.GetFormItemByLabel("Zone Name").(*tview.InputField) 93 | zoneNameFormItem.SetPlaceholder("example.com") 94 | assignInputFieldTheme(zoneNameFormItem) 95 | 96 | zoneForm.SetInputCapture(handleEscape(dnsTreePanel)) 97 | 98 | zoneForm. 99 | AddButton("Go Back", func() { 100 | app.SetRoot(appPanel, true).SetFocus(currentFocus) 101 | }). 102 | AddButton("Add", addZoneHandler(zoneForm, currentFocus)) 103 | 104 | zoneForm.SetTitle("Create ADIDNS Zone").SetBorder(true) 105 | app.SetRoot(zoneForm, true).SetFocus(zoneForm) 106 | } 107 | 108 | func openActionNodeForm(target *tview.TreeNode, update bool) { 109 | currentFocus := app.GetFocus() 110 | 111 | targetDN := target.GetReference().(string) 112 | targetDNParts := strings.Split(targetDN, ",") 113 | firstDNComponents := strings.Split(targetDNParts[0], "=") 114 | firstDNValue := firstDNComponents[1] 115 | 116 | var ( 117 | title string 118 | ) 119 | 120 | if update { 121 | title = "Update" 122 | } else { 123 | title = "Create" 124 | } 125 | 126 | // Left panels 127 | nodeInfoPanel := NewXForm() 128 | nodeInfoPanel.SetTitle("Node") 129 | nodeInfoPanel.SetBorder(true) 130 | 131 | recordValuePages := tview.NewPages() 132 | 133 | // Right panels 134 | recordsPreview := tview.NewTreeView() 135 | recordsPreview. 136 | SetRoot(tview.NewTreeNode("")). 137 | SetTitle("Records Preview"). 138 | SetBorder(true) 139 | 140 | nodeNameInput := tview.NewInputField(). 141 | SetLabel("Node Name"). 142 | SetChangedFunc(func(text string) { 143 | root := recordsPreview.GetRoot() 144 | if root != nil { 145 | root.SetText(text) 146 | } 147 | }) 148 | nodeNameInput.SetPlaceholder("The node name is usually the subdomain you want to create") 149 | assignInputFieldTheme(nodeNameInput) 150 | 151 | // Preview area internal structure 152 | var stagedParsedRecords []adidns.RecordData 153 | var stagedRecords []adidns.DNSRecord 154 | 155 | if update { 156 | // Prefill the existing records 157 | // of the node into the staging area 158 | node, ok := nodeCache[targetDN] 159 | if !ok { 160 | return 161 | } 162 | existingRecords := node.Records 163 | 164 | stagedParsedRecords = make([]adidns.RecordData, len(existingRecords)) 165 | stagedRecords = make([]adidns.DNSRecord, len(existingRecords)) 166 | 167 | copy(stagedRecords, existingRecords) 168 | 169 | for idx, record := range existingRecords { 170 | parsedRecord := record.GetRecordData() 171 | stagedParsedRecords[idx] = parsedRecord 172 | } 173 | 174 | // Show the existing records in the preview 175 | showDNSNodeDetails(&node, recordsPreview) 176 | } else { 177 | // Set up an empty staging area 178 | stagedParsedRecords = make([]adidns.RecordData, 0) 179 | stagedRecords = make([]adidns.DNSRecord, 0) 180 | } 181 | 182 | recordsPreview.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { 183 | switch event.Key() { 184 | case tcell.KeyDelete: 185 | currentNode := recordsPreview.GetCurrentNode() 186 | if currentNode == nil { 187 | return nil 188 | } 189 | 190 | level := currentNode.GetLevel() 191 | 192 | var nodeToDelete *tview.TreeNode 193 | 194 | if level == 1 { 195 | nodeToDelete = currentNode 196 | } else if level == 2 { 197 | pathToCurrent := recordsPreview.GetPath(currentNode) 198 | if len(pathToCurrent) > 1 { 199 | nodeToDelete = pathToCurrent[len(pathToCurrent)-2] 200 | } 201 | } 202 | 203 | if nodeToDelete != nil { 204 | recIdx := -1 205 | siblings := recordsPreview.GetRoot().GetChildren() 206 | for idx, node := range siblings { 207 | if node == nodeToDelete { 208 | recIdx = idx 209 | } 210 | } 211 | 212 | if recIdx != -1 { 213 | stagedParsedRecords = append(stagedParsedRecords[:recIdx], stagedParsedRecords[recIdx+1:]...) 214 | stagedRecords = append(stagedRecords[:recIdx], stagedRecords[recIdx+1:]...) 215 | 216 | recordsPreview.GetRoot().RemoveChild(nodeToDelete) 217 | } 218 | 219 | go func() { 220 | app.Draw() 221 | }() 222 | } 223 | 224 | return nil 225 | } 226 | 227 | return event 228 | }) 229 | 230 | // nodeInfoPanel setup 231 | parentZone, err := getParentZone(targetDN) 232 | if err == nil { 233 | nodeInfoPanel.AddTextView("Zone DN", parentZone.DN, 0, 1, false, true) 234 | } 235 | if update { 236 | nodeInfoPanel.AddTextView("Node Name", firstDNValue, 0, 1, false, true) 237 | } 238 | 239 | // recordContent setup 240 | recordTypeInput := tview.NewDropDown(). 241 | SetLabel("Record Type"). 242 | SetOptions(supportedRecordTypes, func(text string, index int) { 243 | switch text { 244 | case "HINFO", "ISDN", "TXT", "X25", "LOC": 245 | recordValuePages.SwitchToPage("multiple") 246 | case "MX", "AFSDB", "RT": 247 | recordValuePages.SwitchToPage("namepref") 248 | case "SRV": 249 | recordValuePages.SwitchToPage("srv") 250 | case "SOA": 251 | recordValuePages.SwitchToPage("soa") 252 | default: 253 | recordValuePages.SwitchToPage("default") 254 | } 255 | }) 256 | assignDropDownTheme(recordTypeInput) 257 | 258 | recordTypeInput. 259 | SetCurrentOption(0). 260 | SetLabelWidth(12). 261 | SetBorderPadding(1, 0, 1, 1) 262 | 263 | recordTTLInput := tview.NewInputField().SetText("3600") 264 | recordTTLInput. 265 | SetLabel("Record TTL"). 266 | SetLabelWidth(12). 267 | SetBorderPadding(1, 0, 1, 1) 268 | assignInputFieldTheme(recordTTLInput) 269 | 270 | nameprefRecordValueInput := NewXForm(). 271 | AddInputField("Preference", "", 0, numericAcceptanceFunc, nil). 272 | AddInputField("Exchange", "", 0, nil, nil) 273 | 274 | soaRecordValueInput := NewXForm(). 275 | AddInputField("Serial", "", 0, numericAcceptanceFunc, nil). 276 | AddInputField("Refresh", "", 0, numericAcceptanceFunc, nil). 277 | AddInputField("Retry", "", 0, numericAcceptanceFunc, nil). 278 | AddInputField("Expire", "", 0, numericAcceptanceFunc, nil). 279 | AddInputField("MinimumTTL", "", 0, numericAcceptanceFunc, nil). 280 | AddInputField("NamePrimaryServer", "", 0, nil, nil). 281 | AddInputField("ZoneAdminEmail", "", 0, nil, nil) 282 | 283 | srvRecordValueInput := NewXForm(). 284 | AddInputField("Priority", "", 0, numericAcceptanceFunc, nil). 285 | AddInputField("Weight", "", 0, numericAcceptanceFunc, nil). 286 | AddInputField("Port", "", 0, numericAcceptanceFunc, nil). 287 | AddInputField("NameTarget", "", 0, nil, nil) 288 | 289 | defaultRecordValueInput := NewXForm(). 290 | AddInputField("Record Value", "", 0, nil, nil) 291 | defaultRecordValueInput.GetFormItem(0).(*tview.InputField).SetPlaceholder("Type the record value and add it to the preview") 292 | 293 | multipleRecordValueInput := NewXForm(). 294 | AddTextArea("Record Values", "", 0, 0, 0, nil) 295 | multipleRecordValueInput.GetFormItem(0).(*tview.TextArea).SetPlaceholder("Type in the values for the record line-by-line\nand add it to the preview") 296 | 297 | cancelBtn := tview.NewButton("Go Back").SetSelectedFunc(func() { 298 | app.SetRoot(appPanel, true).SetFocus(currentFocus) 299 | }) 300 | assignButtonTheme(cancelBtn) 301 | 302 | updateBtn := tview.NewButton(title).SetSelectedFunc(func() { 303 | var nodeDN string 304 | var action string 305 | var err error 306 | if !update { 307 | nodeName := nodeNameInput.GetText() 308 | nodeDN, err = lc.AddADIDNSNode( 309 | nodeName, 310 | targetDN, 311 | stagedRecords, 312 | ) 313 | action = "created" 314 | } else { 315 | nodeDN = targetDN 316 | err = lc.ReplaceADIDNSRecords( 317 | nodeDN, 318 | stagedRecords, 319 | ) 320 | action = "updated" 321 | } 322 | 323 | if err != nil { 324 | updateLog(fmt.Sprint(err), "red") 325 | app.SetRoot(appPanel, true).SetFocus(currentFocus) 326 | return 327 | } 328 | 329 | go app.QueueUpdateDraw(func() { 330 | if !update { 331 | reloadADIDNSZone(target) 332 | } else { 333 | reloadADIDNSNode(target) 334 | } 335 | }) 336 | 337 | updateLog(fmt.Sprintf("Node '%s' %s successfully", nodeDN, action), "green") 338 | app.SetRoot(appPanel, true).SetFocus(currentFocus) 339 | }) 340 | assignButtonTheme(updateBtn) 341 | 342 | addToPreview := tview.NewButton("Add To Preview").SetSelectedFunc(func() { 343 | _, recordTypeVal := recordTypeInput.GetCurrentOption() 344 | recordTTLVal := recordTTLInput.GetText() 345 | recordTTLInt, err := strconv.Atoi(recordTTLVal) 346 | 347 | if err != nil { 348 | return 349 | } 350 | 351 | // Append the new record to the preview area 352 | var recordValue any 353 | 354 | switch recordTypeVal { 355 | case "HINFO", "ISDN", "TXT", "X25", "LOC": 356 | recordValue = strings.Split(multipleRecordValueInput.GetFormItem(0).(*tview.TextArea).GetText(), "\n") 357 | case "MX", "AFSDB", "RT": 358 | recordValue = map[string]string{ 359 | "Preference": nameprefRecordValueInput.GetFormItemByLabel("Preference").(*tview.InputField).GetText(), 360 | "Exchange": nameprefRecordValueInput.GetFormItemByLabel("Exchange").(*tview.InputField).GetText(), 361 | } 362 | case "SOA": 363 | recordValue = map[string]string{ 364 | "Serial": soaRecordValueInput.GetFormItemByLabel("Serial").(*tview.InputField).GetText(), 365 | "Refresh": soaRecordValueInput.GetFormItemByLabel("Refresh").(*tview.InputField).GetText(), 366 | "Retry": soaRecordValueInput.GetFormItemByLabel("Retry").(*tview.InputField).GetText(), 367 | "Expire": soaRecordValueInput.GetFormItemByLabel("Expire").(*tview.InputField).GetText(), 368 | "MinimumTTL": soaRecordValueInput.GetFormItemByLabel("MinimumTTL").(*tview.InputField).GetText(), 369 | "NamePrimaryServer": soaRecordValueInput.GetFormItemByLabel("NamePrimaryServer").(*tview.InputField).GetText(), 370 | "ZoneAdminEmail": soaRecordValueInput.GetFormItemByLabel("ZoneAdminEmail").(*tview.InputField).GetText(), 371 | } 372 | case "SRV": 373 | recordValue = map[string]string{ 374 | "Priority": srvRecordValueInput.GetFormItemByLabel("Priority").(*tview.InputField).GetText(), 375 | "Weight": srvRecordValueInput.GetFormItemByLabel("Weight").(*tview.InputField).GetText(), 376 | "Port": srvRecordValueInput.GetFormItemByLabel("Port").(*tview.InputField).GetText(), 377 | "NameTarget": srvRecordValueInput.GetFormItemByLabel("NameTarget").(*tview.InputField).GetText(), 378 | } 379 | default: 380 | recordValue = defaultRecordValueInput.GetFormItem(0).(*tview.InputField).GetText() 381 | } 382 | 383 | record := adidns.RecordFromInput(recordTypeVal, recordValue) 384 | stagedParsedRecords = append(stagedParsedRecords, record) 385 | 386 | recordTypeInt := adidns.FindRecordType(recordTypeVal) 387 | recordToStore := adidns.MakeDNSRecord(record, recordTypeInt, uint32(recordTTLInt)) 388 | stagedRecords = append(stagedRecords, recordToStore) 389 | 390 | // Make a new node to add to the preview 391 | var newNode adidns.DNSNode 392 | if update { 393 | node, ok := nodeCache[targetDN] 394 | if !ok { 395 | return 396 | } 397 | newNode = adidns.DNSNode{targetDN, node.Name, stagedRecords} 398 | } else { 399 | nodeName := nodeNameInput.GetText() 400 | newNode = adidns.DNSNode{"", nodeName, stagedRecords} 401 | } 402 | 403 | // Show preview 404 | showDNSNodeDetails(&newNode, recordsPreview) 405 | }) 406 | assignButtonTheme(addToPreview) 407 | 408 | // Page setup 409 | actionNodePanel := tview.NewFlex() 410 | actionNodePanel. 411 | SetInputCapture(handleEscape(dnsTreePanel)). 412 | SetTitle(fmt.Sprintf("%s ADIDNS Node", title)). 413 | SetBorder(true) 414 | 415 | actionNodePanel.SetDirection(tview.FlexRow) 416 | 417 | recordValuePages.AddPage("default", defaultRecordValueInput, true, true) 418 | recordValuePages.AddPage("multiple", multipleRecordValueInput, true, false) 419 | recordValuePages.AddPage("namepref", nameprefRecordValueInput, true, false) 420 | recordValuePages.AddPage("soa", soaRecordValueInput, true, false) 421 | recordValuePages.AddPage("srv", srvRecordValueInput, true, false) 422 | 423 | recordContentPanel := tview.NewFlex().SetDirection(tview.FlexRow). 424 | AddItem(recordTypeInput, 2, 0, false). 425 | AddItem(recordTTLInput, 2, 0, false). 426 | AddItem(recordValuePages, 0, 1, false). 427 | AddItem(addToPreview, 1, 0, false) 428 | 429 | recordContentPanel. 430 | SetTitle("Record Contents"). 431 | SetBorder(true) 432 | 433 | leftPanel := tview.NewFlex().SetDirection(tview.FlexRow) 434 | 435 | // If it's node creation, 436 | // show an input to specify the node name. 437 | // Otherwise just keep it hidden. 438 | if !update { 439 | nodeInfoPanel.AddFormItem(nodeNameInput) 440 | } 441 | 442 | actionNodePanel.AddItem( 443 | tview.NewFlex(). 444 | AddItem( 445 | leftPanel. 446 | AddItem(nodeInfoPanel, 7, 0, false). 447 | AddItem(recordContentPanel, 0, 1, false), 448 | 0, 1, false). 449 | AddItem(recordsPreview, 0, 1, false), 450 | 0, 1, false). 451 | AddItem( 452 | tview.NewFlex(). 453 | AddItem(tview.NewBox(), 1, 0, false). // Spacing 454 | AddItem(cancelBtn, 10, 0, false). 455 | AddItem(tview.NewBox(), 0, 1, false). // Spacing 456 | AddItem(updateBtn, 10, 0, false). 457 | AddItem(tview.NewBox(), 1, 0, false), // Spacing 458 | 1, 0, false) 459 | 460 | app.SetRoot(actionNodePanel, true).SetFocus(actionNodePanel) 461 | } 462 | 463 | func openUpdateNodeForm(node *tview.TreeNode) { 464 | openActionNodeForm(node, true) 465 | } 466 | 467 | func openCreateNodeForm(zone *tview.TreeNode) { 468 | openActionNodeForm(zone, false) 469 | } 470 | 471 | func openDeleteRecordForm(record *tview.TreeNode) { 472 | currentFocus := app.GetFocus() 473 | recRef := record.GetReference().(recordRef) 474 | 475 | nodeDN := recRef.nodeDN 476 | recIdx := recRef.idx 477 | 478 | node, ok := nodeCache[nodeDN] 479 | if !ok { 480 | return 481 | } 482 | records := node.Records 483 | 484 | confirmText := fmt.Sprintf("Do you really want to delete this record?\nRecordIdx: %d\nNode: %s", recIdx, nodeDN) 485 | promptModal := tview.NewModal(). 486 | SetText(confirmText). 487 | AddButtons([]string{"No", "Yes"}). 488 | SetDoneFunc(func(buttonIndex int, buttonLabel string) { 489 | if buttonLabel == "Yes" { 490 | // TODO: Add safety check for changes outside Godap 491 | updateRecords := append(records[:recIdx], records[recIdx+1:]...) 492 | 493 | err = lc.ReplaceADIDNSRecords(nodeDN, updateRecords) 494 | if err != nil { 495 | updateLog(fmt.Sprint(err), "red") 496 | app.SetRoot(appPanel, true).SetFocus(currentFocus) 497 | return 498 | } 499 | 500 | node := dnsTreePanel.GetCurrentNode() 501 | reloadADIDNSNode(node) 502 | 503 | updateLog("Record deleted successfully", "green") 504 | app.SetRoot(appPanel, true).SetFocus(currentFocus) 505 | } else { 506 | app.SetRoot(appPanel, true).SetFocus(currentFocus) 507 | } 508 | }) 509 | 510 | app.SetRoot(promptModal, true).SetFocus(promptModal) 511 | } 512 | 513 | /* 514 | Records can also be added instead of replaced with: 515 | 516 | ``` 517 | rec := recordFromInput(recordType, recordValue) 518 | 519 | recordsToAdd := []adidns.DNSRecord{ 520 | adidns.MakeDNSRecord( 521 | rec, 522 | adidns.FindRecordType(recordType), 523 | uint32(recordTTLInt)), 524 | } 525 | 526 | err = lc.AddADIDNSRecords(nodeDN, recordsToAdd) 527 | if err != nil { 528 | updateLog(fmt.Sprint(err), "red") 529 | app.SetRoot(appPanel, true).SetFocus(currentFocus) 530 | return 531 | } 532 | 533 | reloadADIDNSNode(node) 534 | ```` 535 | */ 536 | -------------------------------------------------------------------------------- /tui/explorer.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "fmt" 5 | "slices" 6 | "sort" 7 | "strconv" 8 | 9 | "github.com/Macmod/godap/v2/pkg/ldaputils" 10 | "github.com/gdamore/tcell/v2" 11 | "github.com/go-ldap/ldap/v3" 12 | "github.com/rivo/tview" 13 | ) 14 | 15 | var ( 16 | explorerCache EntryCache 17 | explorerPage *tview.Flex 18 | treePanel *tview.TreeView 19 | explorerAttrsPanel *tview.Table 20 | rootDNInput *tview.InputField 21 | searchFilterInput *tview.InputField 22 | treeFlex *tview.Flex 23 | 24 | addMemberToGroupFormValidation bool 25 | ) 26 | 27 | func initExplorerPage() { 28 | explorerCache = EntryCache{ 29 | entries: make(map[string]*ldap.Entry), 30 | } 31 | 32 | treePanel = tview.NewTreeView() 33 | 34 | rootNode = renderPartialTree(RootDN, SearchFilter) 35 | treePanel.SetRoot(rootNode).SetCurrentNode(rootNode) 36 | 37 | explorerAttrsPanel = tview.NewTable().SetSelectable(true, true) 38 | 39 | searchFilterInput = tview.NewInputField(). 40 | SetText(SearchFilter) 41 | searchFilterInput.SetTitle("Expand Filter") 42 | searchFilterInput.SetBorder(true) 43 | assignInputFieldTheme(searchFilterInput) 44 | 45 | rootDNInput = tview.NewInputField(). 46 | SetText(RootDN) 47 | rootDNInput.SetTitle("Root DN") 48 | rootDNInput.SetBorder(true) 49 | assignInputFieldTheme(rootDNInput) 50 | 51 | explorerAttrsPanel. 52 | SetEvaluateAllRows(true). 53 | SetTitle("Attributes"). 54 | SetBorder(true) 55 | 56 | // Event Handlers 57 | searchFilterInput.SetDoneFunc(func(key tcell.Key) { 58 | SearchFilter = searchFilterInput.GetText() 59 | reloadExplorerPage() 60 | }) 61 | 62 | rootDNInput.SetDoneFunc(func(key tcell.Key) { 63 | lc.RootDN = rootDNInput.GetText() 64 | reloadExplorerPage() 65 | }) 66 | 67 | treeFlex = tview.NewFlex().SetDirection(tview.FlexRow). 68 | AddItem( 69 | tview.NewFlex(). 70 | AddItem(searchFilterInput, 0, 1, false). 71 | AddItem(rootDNInput, 0, 1, false), 72 | 3, 0, false, 73 | ). 74 | AddItem(treePanel, 0, 1, false) 75 | 76 | treeFlex.SetBorder(true) 77 | treeFlex.SetTitle("Tree View") 78 | explorerPage = tview.NewFlex().SetDirection(tview.FlexRow). 79 | AddItem( 80 | tview.NewFlex(). 81 | AddItem(treeFlex, 0, 1, false). 82 | AddItem(explorerAttrsPanel, 0, 1, false), 0, 1, false, 83 | ) 84 | 85 | explorerPage.SetInputCapture(explorerPageKeyHandler) 86 | 87 | explorerAttrsPanel.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { 88 | currentNode := treePanel.GetCurrentNode() 89 | if currentNode == nil || currentNode.GetReference() == nil { 90 | return event 91 | } 92 | 93 | return attrsPanelKeyHandler(event, currentNode, &explorerCache, explorerAttrsPanel) 94 | }) 95 | 96 | explorerAttrsPanel.SetSelectionChangedFunc(storeAnchoredAttribute(explorerAttrsPanel)) 97 | 98 | treePanel.SetInputCapture(treePanelKeyHandler) 99 | 100 | treePanel.SetChangedFunc(treePanelChangeHandler) 101 | } 102 | 103 | func expandTreeNode(node *tview.TreeNode) { 104 | if !node.IsExpanded() { 105 | if len(node.GetChildren()) == 0 { 106 | go app.QueueUpdateDraw(func() { 107 | updateLog("Loading children ("+node.GetReference().(string)+")", "yellow") 108 | loadChildren(node) 109 | 110 | n := len(node.GetChildren()) 111 | 112 | if n != 0 { 113 | node.SetExpanded(true) 114 | updateLog("Loaded "+strconv.Itoa(n)+" children ("+node.GetReference().(string)+")", "green") 115 | } else { 116 | updateLog("Node "+node.GetReference().(string)+" has no children", "green") 117 | } 118 | }) 119 | } else { 120 | node.SetExpanded(true) 121 | } 122 | } 123 | } 124 | 125 | func collapseTreeNode(node *tview.TreeNode) { 126 | node.SetExpanded(false) 127 | if !CacheEntries { 128 | unloadChildren(node) 129 | } 130 | } 131 | 132 | func reloadParentNode(node *tview.TreeNode) *tview.TreeNode { 133 | parent := getParentNode(node, treePanel) 134 | 135 | if parent != nil { 136 | unloadChildren(parent) 137 | loadChildren(parent) 138 | } 139 | 140 | go func() { 141 | app.Draw() 142 | }() 143 | 144 | return parent 145 | } 146 | 147 | func reloadExplorerAttrsPanel(node *tview.TreeNode, useCache bool) { 148 | reloadAttributesPanel(node, explorerAttrsPanel, useCache, &explorerCache) 149 | } 150 | 151 | func exportCacheToFile(currentNode *tview.TreeNode, cache *EntryCache, fileSuffix string) { 152 | exportMap := make(map[string]any) 153 | currentNode.Walk(func(node, parent *tview.TreeNode) bool { 154 | if node.GetReference() != nil { 155 | nodeDN := node.GetReference().(string) 156 | exportMap[nodeDN], _ = cache.Get(nodeDN) 157 | } 158 | return true 159 | }) 160 | 161 | writeDataExport(exportMap, fileSuffix, "tree_objects") 162 | } 163 | 164 | func openDeleteObjectForm(node *tview.TreeNode, done func()) { 165 | currentFocus := app.GetFocus() 166 | baseDN := node.GetReference().(string) 167 | promptModal := tview.NewModal(). 168 | SetText("Do you really want to delete this object?\n" + baseDN). 169 | AddButtons([]string{"No", "Yes"}). 170 | SetDoneFunc(func(buttonIndex int, buttonLabel string) { 171 | if buttonLabel == "Yes" { 172 | err := lc.DeleteObject(baseDN) 173 | if err != nil { 174 | updateLog(fmt.Sprintf("%s", err), "red") 175 | } else { 176 | if done != nil { 177 | done() 178 | } 179 | 180 | updateLog("Object deleted: "+baseDN, "green") 181 | } 182 | } 183 | 184 | app.SetRoot(appPanel, true).SetFocus(currentFocus) 185 | }) 186 | 187 | app.SetRoot(promptModal, false).SetFocus(promptModal) 188 | } 189 | 190 | func openUpdateUacForm(node *tview.TreeNode, cache *EntryCache, done func()) { 191 | currentFocus := app.GetFocus() 192 | baseDN := node.GetReference().(string) 193 | 194 | updateUacForm := NewXForm() 195 | //assignFormTheme(updateUacForm) 196 | updateUacForm.SetInputCapture(handleEscape(treePanel)) 197 | updateUacForm.SetItemPadding(0) 198 | 199 | var checkboxState int = 0 200 | obj, _ := cache.Get(baseDN) 201 | if obj != nil { 202 | uacValue, err := strconv.Atoi(obj.GetAttributeValue("userAccountControl")) 203 | if err == nil { 204 | checkboxState = uacValue 205 | } else { 206 | return 207 | } 208 | } 209 | 210 | updateUacForm. 211 | AddTextView("Raw UAC Value", strconv.Itoa(checkboxState), 0, 1, false, true) 212 | 213 | uacValues := make([]int, 0) 214 | for key := range ldaputils.UacFlags { 215 | uacValues = append(uacValues, key) 216 | } 217 | sort.Ints(uacValues) 218 | 219 | for _, val := range uacValues { 220 | uacValue := val 221 | updateUacForm.AddCheckbox( 222 | ldaputils.UacFlags[uacValue].Present, 223 | checkboxState&uacValue != 0, 224 | func(checked bool) { 225 | if checked { 226 | checkboxState |= uacValue 227 | } else { 228 | checkboxState &^= uacValue 229 | } 230 | 231 | uacPreview := updateUacForm.GetFormItemByLabel("Raw UAC Value").(*tview.TextView) 232 | if uacPreview != nil { 233 | uacPreview.SetText(strconv.Itoa(checkboxState)) 234 | } 235 | }) 236 | } 237 | 238 | updateUacForm. 239 | AddButton("Go Back", func() { 240 | app.SetRoot(appPanel, true).SetFocus(currentFocus) 241 | }). 242 | AddButton("Update", func() { 243 | strCheckboxState := strconv.Itoa(checkboxState) 244 | err := lc.ModifyAttribute(baseDN, "userAccountControl", []string{strCheckboxState}) 245 | 246 | if err != nil { 247 | updateLog(fmt.Sprintf("%s", err), "red") 248 | } else { 249 | if done != nil { 250 | done() 251 | } 252 | 253 | updateLog("Object's UAC updated to "+strCheckboxState+" at: "+baseDN, "green") 254 | } 255 | 256 | app.SetRoot(appPanel, true).SetFocus(currentFocus) 257 | }) 258 | 259 | updateUacForm.SetTitle("userAccountControl Editor").SetBorder(true) 260 | app.SetRoot(updateUacForm, true).SetFocus(updateUacForm) 261 | } 262 | 263 | func openCreateObjectForm(node *tview.TreeNode, done func()) { 264 | currentFocus := app.GetFocus() 265 | baseDN := node.GetReference().(string) 266 | 267 | createObjectForm := NewXForm(). 268 | AddDropDown("Object Type", []string{"OrganizationalUnit", "Container", "User", "Group", "Computer"}, 0, nil). 269 | AddInputField("Object Name", "", 0, nil, nil). 270 | AddInputField("Entry TTL", "-1", 0, nil, nil). 271 | AddInputField("Parent DN", baseDN, 0, nil, nil) 272 | createObjectForm. 273 | SetInputCapture(handleEscape(treePanel)) 274 | 275 | createObjectForm. 276 | AddButton("Go Back", func() { 277 | app.SetRoot(appPanel, true).SetFocus(currentFocus) 278 | }). 279 | AddButton("Create", func() { 280 | // Note: It should be possible to walk upwards in the tree 281 | // to find the first place where it's possible to place the object 282 | // but it makes sense that the user should 283 | // have full control over this behavior 284 | // rather than automatically detecting 285 | // an appropriate DN 286 | 287 | // pathToCurrent := treePanel.GetPath(currentNode) 288 | // lastNode := len(pathToCurrent) - 1 289 | // for nodeInPathIdx := range pathToCurrent { 290 | // currentNodeIdx := lastNode - nodeInPathIdx 291 | // } 292 | 293 | _, objectType := createObjectForm.GetFormItemByLabel("Object Type").(*tview.DropDown).GetCurrentOption() 294 | 295 | objectName := createObjectForm.GetFormItemByLabel("Object Name").(*tview.InputField).GetText() 296 | 297 | entryTTL := createObjectForm.GetFormItemByLabel("Entry TTL").(*tview.InputField).GetText() 298 | entryTTLInt, err := strconv.Atoi(entryTTL) 299 | if err != nil { 300 | entryTTLInt = -1 301 | } 302 | 303 | switch objectType { 304 | case "OrganizationalUnit": 305 | err = lc.AddOrganizationalUnit(objectName, baseDN, entryTTLInt) 306 | case "Container": 307 | err = lc.AddContainer(objectName, baseDN, entryTTLInt) 308 | case "User": 309 | err = lc.AddUser(objectName, baseDN, entryTTLInt) 310 | case "Group": 311 | err = lc.AddGroup(objectName, baseDN, entryTTLInt) 312 | case "Computer": 313 | err = lc.AddComputer(objectName, baseDN, entryTTLInt) 314 | default: 315 | err = fmt.Errorf("Invalid object type") 316 | } 317 | 318 | if err != nil { 319 | updateLog(fmt.Sprintf("%s", err), "red") 320 | } else { 321 | if done != nil { 322 | done() 323 | } 324 | updateLog("Object created successfully at: "+baseDN, "green") 325 | } 326 | 327 | app.SetRoot(appPanel, true).SetFocus(currentFocus) 328 | }) 329 | 330 | createObjectForm.SetTitle("Object Creator").SetBorder(true) 331 | app.SetRoot(createObjectForm, true).SetFocus(createObjectForm) 332 | } 333 | 334 | func openAddMemberToGroupForm(targetDN string, isGroup bool) { 335 | currentFocus := app.GetFocus() 336 | 337 | addMemberForm := NewXForm(). 338 | AddInputField("Group DN", "", 0, nil, nil). 339 | AddInputField("Object Name", "", 0, nil, nil). 340 | AddTextView("Object DN", "", 0, 1, false, true) 341 | 342 | objectNameFormItem := addMemberForm.GetFormItemByLabel("Object Name").(*tview.InputField) 343 | groupDNFormItem := addMemberForm.GetFormItemByLabel("Group DN").(*tview.InputField) 344 | 345 | groupDNFormItem.SetPlaceholder("Group DN") 346 | if isGroup { 347 | groupDNFormItem.SetText(targetDN) 348 | } else { 349 | objectNameFormItem.SetText(targetDN) 350 | } 351 | 352 | objectNameFormItem.SetPlaceholder("sAMAccountName or DN") 353 | assignInputFieldTheme(objectNameFormItem) 354 | assignInputFieldTheme(groupDNFormItem) 355 | 356 | objectDNFormItem := addMemberForm.GetFormItemByLabel("Object DN").(*tview.TextView) 357 | objectDNFormItem.SetDynamicColors(true) 358 | 359 | objectNameFormItem.SetDoneFunc(func(key tcell.Key) { 360 | object, err := lc.FindFirst(objectNameFormItem.GetText()) 361 | if err == nil { 362 | addMemberToGroupFormValidation = true 363 | objectDNFormItem.SetText(object.DN) 364 | } else { 365 | addMemberToGroupFormValidation = false 366 | objectDNFormItem.SetText("[red]Object not found") 367 | } 368 | }) 369 | 370 | //assignFormTheme(addMemberForm) 371 | 372 | addMemberForm. 373 | SetInputCapture(handleEscape(treePanel)) 374 | 375 | addMemberForm. 376 | AddButton("Go Back", func() { 377 | app.SetRoot(appPanel, true).SetFocus(currentFocus) 378 | }). 379 | AddButton("Add", func() { 380 | if !addMemberToGroupFormValidation { 381 | // TODO: Provide some feedback to the user? 382 | return 383 | } 384 | 385 | objectDN := addMemberForm.GetFormItemByLabel("Object DN").(*tview.TextView).GetText(true) 386 | 387 | err = lc.AddMemberToGroup(objectDN, targetDN) 388 | if err != nil { 389 | updateLog(fmt.Sprintf("%s", err), "red") 390 | } else { 391 | updateLog("Member '"+objectDN+"' added to group '"+targetDN+"'", "green") 392 | } 393 | 394 | app.SetRoot(appPanel, true).SetFocus(currentFocus) 395 | }) 396 | 397 | addMemberForm.SetTitle("Add Group Member").SetBorder(true) 398 | app.SetRoot(addMemberForm, true).SetFocus(addMemberForm) 399 | } 400 | 401 | func treePanelKeyHandler(event *tcell.EventKey) *tcell.EventKey { 402 | currentNode := treePanel.GetCurrentNode() 403 | if currentNode == nil { 404 | return event 405 | } 406 | 407 | parentNode := getParentNode(currentNode, treePanel) 408 | baseDN := currentNode.GetReference().(string) 409 | 410 | switch event.Rune() { 411 | case 'r', 'R': 412 | go app.QueueUpdateDraw(func() { 413 | updateLog("Reloading node "+baseDN, "yellow") 414 | 415 | explorerCache.Delete(baseDN) 416 | reloadAttributesPanel(currentNode, explorerAttrsPanel, false, &explorerCache) 417 | 418 | unloadChildren(currentNode) 419 | loadChildren(currentNode) 420 | 421 | updateLog("Node "+baseDN+" reloaded", "green") 422 | }) 423 | 424 | return event 425 | } 426 | 427 | switch event.Key() { 428 | case tcell.KeyRight: 429 | expandTreeNode(currentNode) 430 | return nil 431 | case tcell.KeyLeft: 432 | if currentNode.IsExpanded() { // Collapse current node 433 | collapseTreeNode(currentNode) 434 | treePanel.SetCurrentNode(currentNode) 435 | return nil 436 | } else { // Collapse parent node 437 | pathToCurrent := treePanel.GetPath(currentNode) 438 | if len(pathToCurrent) > 1 { 439 | parentNode := pathToCurrent[len(pathToCurrent)-2] 440 | collapseTreeNode(parentNode) 441 | treePanel.SetCurrentNode(parentNode) 442 | } 443 | return nil 444 | } 445 | case tcell.KeyDelete: 446 | openDeleteObjectForm(currentNode, func() { 447 | explorerCache.Delete(baseDN) 448 | 449 | if parentNode != nil { 450 | idx := findEntryInChildren(baseDN, parentNode) 451 | 452 | parent := reloadParentNode(currentNode) 453 | otherNodeToSelect := parent 454 | 455 | if idx > 0 { 456 | siblings := parent.GetChildren() 457 | otherNodeToSelect = siblings[idx-1] 458 | } 459 | 460 | treePanel.SetCurrentNode(otherNodeToSelect) 461 | } else { 462 | reloadExplorerPage() 463 | } 464 | }) 465 | case tcell.KeyCtrlN: 466 | openCreateObjectForm(currentNode, func() { 467 | reloadExplorerAttrsPanel(currentNode, CacheEntries) 468 | 469 | unloadChildren(currentNode) 470 | loadChildren(currentNode) 471 | treePanel.SetCurrentNode(currentNode) 472 | }) 473 | case tcell.KeyCtrlS: 474 | exportCacheToFile(currentNode, &explorerCache, "objects") 475 | case tcell.KeyCtrlA: 476 | openUpdateUacForm(currentNode, &explorerCache, func() { 477 | if parentNode != nil { 478 | idx := findEntryInChildren(baseDN, parentNode) 479 | 480 | parent := reloadParentNode(currentNode) 481 | siblings := parent.GetChildren() 482 | 483 | reloadExplorerAttrsPanel(currentNode, false) 484 | 485 | treePanel.SetCurrentNode(siblings[idx]) 486 | } else { 487 | reloadExplorerPage() 488 | } 489 | }) 490 | case tcell.KeyCtrlG: 491 | entry := explorerCache.entries[baseDN] 492 | objClasses := entry.GetAttributeValues("objectClass") 493 | isGroup := slices.Contains(objClasses, "group") 494 | openAddMemberToGroupForm(baseDN, isGroup) 495 | case tcell.KeyCtrlD: 496 | if lc.Flavor == ldaputils.MicrosoftADFlavor { 497 | info.Highlight("3") 498 | objectNameInputDacl.SetText(baseDN) 499 | queryDacl(baseDN) 500 | } 501 | } 502 | 503 | return event 504 | } 505 | 506 | func treePanelChangeHandler(node *tview.TreeNode) { 507 | go app.QueueUpdateDraw(func() { 508 | // TODO: Implement cancellation 509 | reloadExplorerAttrsPanel(node, CacheEntries) 510 | selectAnchoredAttribute(explorerAttrsPanel) 511 | }) 512 | } 513 | 514 | func explorerRotateFocus() { 515 | currentFocus := app.GetFocus() 516 | 517 | switch currentFocus { 518 | case treePanel: 519 | app.SetFocus(explorerAttrsPanel) 520 | case explorerAttrsPanel: 521 | app.SetFocus(treePanel) 522 | } 523 | } 524 | 525 | func openPasswordChangeForm(node *tview.TreeNode) { 526 | currentFocus := app.GetFocus() 527 | changePasswordForm := NewXForm() 528 | 529 | baseDN := node.GetReference().(string) 530 | changePasswordForm. 531 | AddTextView("Object DN", baseDN, 0, 1, false, true). 532 | AddPasswordField("New Password", "", 20, '*', nil). 533 | AddButton("Go Back", func() { 534 | app.SetRoot(appPanel, false).SetFocus(currentFocus) 535 | }). 536 | AddButton("Update", func() { 537 | newPassword := changePasswordForm.GetFormItemByLabel("New Password").(*tview.InputField).GetText() 538 | 539 | err := lc.ResetPassword(baseDN, newPassword) 540 | if err != nil { 541 | updateLog(fmt.Sprint(err), "red") 542 | } else { 543 | updateLog("Password changed: "+baseDN, "green") 544 | } 545 | 546 | app.SetRoot(appPanel, false).SetFocus(currentFocus) 547 | }) 548 | 549 | changePasswordForm.SetTitle("Password Editor").SetBorder(true) 550 | 551 | //assignFormTheme(changePasswordForm) 552 | 553 | changePasswordForm.SetInputCapture(handleEscape(treePanel)) 554 | 555 | app.SetRoot(changePasswordForm, true).SetFocus(changePasswordForm) 556 | } 557 | 558 | func openMoveObjectForm(node *tview.TreeNode, done func(string)) { 559 | baseDN := node.GetReference().(string) 560 | currentFocus := app.GetFocus() 561 | 562 | moveObjectForm := NewXForm() 563 | moveObjectForm. 564 | AddTextView("Object DN", baseDN, 0, 1, false, true). 565 | AddInputField("New Object DN", baseDN, 0, nil, nil). 566 | AddButton("Go Back", func() { 567 | app.SetRoot(appPanel, false).SetFocus(currentFocus) 568 | }). 569 | AddButton("Update", func() { 570 | newObjectDN := moveObjectForm.GetFormItemByLabel("New Object DN").(*tview.InputField).GetText() 571 | 572 | err := lc.MoveObject(baseDN, newObjectDN) 573 | 574 | if err != nil { 575 | updateLog(fmt.Sprint(err), "red") 576 | } else { 577 | updateLog("Object moved from '"+baseDN+"' to '"+newObjectDN+"'", "green") 578 | if done != nil { 579 | done(newObjectDN) 580 | } 581 | } 582 | 583 | app.SetRoot(appPanel, false).SetFocus(currentFocus) 584 | }) 585 | 586 | moveObjectForm.SetTitle("Move Object").SetBorder(true) 587 | moveObjectForm.SetInputCapture(handleEscape(treePanel)) 588 | //assignFormTheme(moveObjectForm) 589 | app.SetRoot(moveObjectForm, true).SetFocus(moveObjectForm) 590 | } 591 | 592 | func explorerPageKeyHandler(event *tcell.EventKey) *tcell.EventKey { 593 | if event.Key() == tcell.KeyTab || event.Key() == tcell.KeyBacktab { 594 | explorerRotateFocus() 595 | return nil 596 | } 597 | 598 | currentNode := treePanel.GetCurrentNode() 599 | if currentNode == nil { 600 | return event 601 | } 602 | 603 | switch event.Key() { 604 | case tcell.KeyCtrlF: 605 | openFinder(&explorerCache, "LDAP Explorer") 606 | case tcell.KeyCtrlP: 607 | openPasswordChangeForm(currentNode) 608 | case tcell.KeyCtrlL: 609 | openMoveObjectForm(currentNode, func(newObjectDN string) { 610 | newParentNode := reloadParentNode(currentNode) 611 | 612 | idx := findEntryInChildren(newObjectDN, newParentNode) 613 | 614 | otherNodeToSelect := newParentNode 615 | 616 | if idx > 0 { 617 | siblings := newParentNode.GetChildren() 618 | otherNodeToSelect = siblings[idx] 619 | } 620 | 621 | treePanel.SetCurrentNode(otherNodeToSelect) 622 | reloadExplorerAttrsPanel(otherNodeToSelect, CacheEntries) 623 | }) 624 | } 625 | 626 | return event 627 | } 628 | -------------------------------------------------------------------------------- /tui/finder.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | 7 | "github.com/gdamore/tcell/v2" 8 | "github.com/rivo/tview" 9 | ) 10 | 11 | func highlightStr(base string, idxBegin int, idxEnd int, color string) string { 12 | return base[:idxBegin] + "[" + color + "]" + base[idxBegin:idxEnd] + "[white]" + base[idxEnd:] 13 | } 14 | 15 | func openFinder(cache *EntryCache, titleLabel string) { 16 | currentFocus := app.GetFocus() 17 | 18 | numCache := tview.NewTextView(). 19 | SetTextAlign(tview.AlignCenter) 20 | numCache.SetTitle("# CachedObjs") 21 | numCache.SetBorder(true) 22 | numCache.SetText(strconv.Itoa(cache.Length())) 23 | 24 | numResults := tview.NewTextView(). 25 | SetTextAlign(tview.AlignCenter) 26 | numResults.SetTitle("# Results") 27 | numResults.SetBorder(true) 28 | 29 | inputField := tview.NewInputField() 30 | inputField. 31 | SetPlaceholder("Enter a regexp to search here"). 32 | SetTitle("Search Query"). 33 | SetBorder(true) 34 | assignInputFieldTheme(inputField) 35 | 36 | table := tview.NewTable(). 37 | SetSelectable(true, false). 38 | SetFixed(1, 0). 39 | SetBorders(false) 40 | table. 41 | SetTitle("Search Results"). 42 | SetBorder(true) 43 | 44 | inputField.SetDoneFunc(func(tcell.Key) { 45 | table.Clear() 46 | 47 | queryRegexp, err := regexp.Compile(inputField.GetText()) 48 | if err == nil { 49 | results := cache.FindWithRegexp(queryRegexp) 50 | if len(results) != 0 { 51 | table.SetCell(0, 0, tview.NewTableCell("Match").SetSelectable(false)) 52 | table.SetCell(0, 1, tview.NewTableCell("Object").SetSelectable(false)) 53 | table.SetCell(0, 2, tview.NewTableCell("AttrName").SetSelectable(false)) 54 | table.SetCell(0, 3, tview.NewTableCell("AttrValue").SetSelectable(false)) 55 | table.SetCell(0, 4, tview.NewTableCell("ValIdx").SetSelectable(false)) 56 | } 57 | 58 | numResults.SetText(strconv.Itoa(len(results))) 59 | if len(results) == 0 { 60 | numResults.SetTextColor(tcell.ColorRed) 61 | } else { 62 | numResults.SetTextColor(tcell.ColorDefault) 63 | } 64 | 65 | for idx, val := range results { 66 | matchField := val.MatchField 67 | matchDN := val.MatchDN 68 | matchAttrName := val.MatchAttrName 69 | matchAttrVal := val.MatchAttrVal 70 | matchAttrValIdx := strconv.Itoa(val.MatchAttrValIdx) 71 | matchBegin := val.MatchPosBegin 72 | matchEnd := val.MatchPosEnd 73 | 74 | switch matchField { 75 | case "ObjectDN": 76 | matchDN = highlightStr(matchDN, matchBegin, matchEnd, "green") 77 | matchField = "[blue]" + matchField 78 | matchAttrValIdx = "" 79 | case "AttrName": 80 | matchAttrName = highlightStr(matchAttrName, matchBegin, matchEnd, "green") 81 | matchField = "[violet]" + matchField 82 | matchAttrValIdx = "" 83 | case "AttrVal": 84 | matchAttrVal = highlightStr(matchAttrVal, matchBegin, matchEnd, "green") 85 | matchField = "[purple]" + matchField 86 | } 87 | 88 | table.SetCell(idx+1, 0, tview.NewTableCell(matchField)) 89 | table.SetCell(idx+1, 1, tview.NewTableCell(matchDN)) 90 | table.SetCell(idx+1, 2, tview.NewTableCell(matchAttrName)) 91 | table.SetCell(idx+1, 3, tview.NewTableCell(matchAttrVal)) 92 | table.SetCell(idx+1, 4, tview.NewTableCell(matchAttrValIdx)) 93 | } 94 | } 95 | }) 96 | 97 | cancelBtn := tview.NewButton("Go Back") 98 | cancelBtn.SetSelectedFunc(func() { 99 | app.SetRoot(appPanel, true).SetFocus(currentFocus) 100 | }) 101 | assignButtonTheme(cancelBtn) 102 | 103 | finderPanel := tview.NewFlex().SetDirection(tview.FlexRow) 104 | finderPanel. 105 | AddItem( 106 | tview.NewFlex(). 107 | AddItem(inputField, 0, 1, false). 108 | AddItem(numCache, 14, 0, false). 109 | AddItem(numResults, 13, 0, false), 3, 0, false). 110 | AddItem(table, 0, 3, false). 111 | AddItem(cancelBtn, 1, 0, false) 112 | 113 | finderPanel.SetTitle("Cache Finder (" + titleLabel + ")") 114 | finderPanel.SetBorder(true) 115 | 116 | finderPanel.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { 117 | if event.Key() == tcell.KeyEscape { 118 | app.SetRoot(appPanel, true).SetFocus(currentFocus) 119 | return nil 120 | } 121 | 122 | return event 123 | }) 124 | 125 | app.SetRoot(finderPanel, true).SetFocus(inputField) 126 | } 127 | -------------------------------------------------------------------------------- /tui/gpo.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | "sync" 9 | 10 | "github.com/Macmod/godap/v2/pkg/ldaputils" 11 | "github.com/gdamore/tcell/v2" 12 | "github.com/go-ldap/ldap/v3" 13 | "github.com/rivo/tview" 14 | ) 15 | 16 | var ( 17 | runControlGpo sync.Mutex 18 | runningGpo bool 19 | 20 | gpoTarget string 21 | 22 | gpoTargetInput *tview.InputField 23 | gpoPath *tview.TextView 24 | gpoPage *tview.Flex 25 | gpoListPanel *tview.Table 26 | gpoLinksPanel *tview.Table 27 | gpoFlex *tview.Flex 28 | 29 | gpLinks map[string][]GPOLink 30 | containerLinks map[string][]string 31 | gpEntry map[string]*ldap.Entry 32 | ) 33 | 34 | type GPOLink struct { 35 | Target string 36 | GUID string 37 | Path string 38 | Enabled bool 39 | Enforced bool 40 | } 41 | 42 | func ParseGPLinks(gpoLinks string, target string) ([]GPOLink, error) { 43 | var links []GPOLink 44 | 45 | re := regexp.MustCompile(`\[LDAP://[cC][nN]=({[A-Fa-f0-9\-]+}),[^;]+;(\d+)\]`) 46 | 47 | matches := re.FindAllStringSubmatch(gpoLinks, -1) 48 | 49 | for _, match := range matches { 50 | guid := match[1] 51 | path := match[0][8 : len(match[0])-len(match[2])-1] 52 | flags, _ := strconv.Atoi(match[2]) 53 | 54 | link := GPOLink{ 55 | Target: target, 56 | GUID: guid, 57 | Path: path, 58 | Enabled: (flags & 0x00000001) == 0, 59 | Enforced: (flags & 0x00000002) != 0, 60 | } 61 | links = append(links, link) 62 | } 63 | 64 | return links, nil 65 | } 66 | 67 | func initGPOPage() { 68 | gpoTargetInput = tview.NewInputField() 69 | gpoTargetInput. 70 | SetPlaceholder("Type a target (DN or cn) or just leave it blank and hit enter"). 71 | SetTitle("GPO Target"). 72 | SetBorder(true) 73 | assignInputFieldTheme(gpoTargetInput) 74 | 75 | gpoListPanel = tview.NewTable().SetSelectable(true, false) 76 | 77 | gpoLinksPanel = tview.NewTable().SetSelectable(true, false) 78 | 79 | gpoPath = tview.NewTextView() 80 | gpoPath.SetWrap(false) 81 | 82 | gpoPath. 83 | SetTitle("GPO Path"). 84 | SetBorder(true) 85 | 86 | gpoListPanel. 87 | SetEvaluateAllRows(true). 88 | SetTitle("Applied GPOs"). 89 | SetBorder(true) 90 | 91 | gpoLinksPanel. 92 | SetEvaluateAllRows(true). 93 | SetTitle("Links"). 94 | SetBorder(true) 95 | 96 | gpoFlex = tview.NewFlex(). 97 | AddItem(gpoListPanel, 0, 1, false). 98 | AddItem( 99 | tview.NewFlex().SetDirection(tview.FlexRow). 100 | AddItem(gpoPath, 3, 0, false). 101 | AddItem(gpoLinksPanel, 0, 1, false), 102 | 0, 1, false) 103 | 104 | gpoPage = tview.NewFlex().SetDirection(tview.FlexRow). 105 | AddItem(gpoTargetInput, 3, 0, false). 106 | AddItem(gpoFlex, 0, 1, false) 107 | 108 | gpoTargetInput.SetDoneFunc(func(key tcell.Key) { 109 | go updateGPOEntries() 110 | }) 111 | 112 | gpoListPanel.SetSelectedFunc(func(row, col int) { 113 | app.SetFocus(gpoLinksPanel) 114 | }) 115 | 116 | gpoListPanel.SetSelectionChangedFunc(func(row, col int) { 117 | if row <= 0 { 118 | row = 1 119 | } 120 | 121 | gpoLinksPanel.Clear() 122 | gpoPath.SetText("") 123 | 124 | guid := gpoListPanel.GetCell(row, 3).Text 125 | 126 | entry, ok := gpEntry[guid] 127 | if ok { 128 | gpPath := entry.GetAttributeValue("gPCFileSysPath") 129 | gpoPath.SetText(gpPath) 130 | } 131 | 132 | val, ok := gpLinks[guid] 133 | if ok { 134 | gpoLinksPanel.SetCell(0, 0, tview.NewTableCell("Target").SetSelectable(false)) 135 | gpoLinksPanel.SetCell(0, 1, tview.NewTableCell("Enforced").SetSelectable(false)) 136 | gpoLinksPanel.SetCell(0, 2, tview.NewTableCell("Enabled").SetSelectable(false)) 137 | 138 | idx := 0 139 | for linkIdx := range val { 140 | enforced := "[red]No" 141 | if val[linkIdx].Enforced { 142 | enforced = "[green]Yes" 143 | } 144 | 145 | enabled := "[red]No" 146 | if val[linkIdx].Enabled { 147 | enabled = "[green]Yes" 148 | } 149 | 150 | gpoLinksPanel.SetCellSimple(idx+1, 0, val[linkIdx].Target) 151 | gpoLinksPanel.SetCellSimple(idx+1, 1, enforced) 152 | gpoLinksPanel.SetCellSimple(idx+1, 2, enabled) 153 | idx += 1 154 | } 155 | } 156 | }) 157 | 158 | gpoLinksPanel.SetSelectedFunc(func(row, col int) { 159 | targetDN := gpoLinksPanel.GetCell(row, 0).Text 160 | 161 | gpoTargetInput.SetText(targetDN) 162 | app.SetFocus(gpoTargetInput) 163 | }) 164 | 165 | gpoPage.SetInputCapture(gpoPageKeyHandler) 166 | } 167 | 168 | func gpoRotateFocus() { 169 | currentFocus := app.GetFocus() 170 | 171 | switch currentFocus { 172 | case gpoListPanel: 173 | app.SetFocus(gpoTargetInput) 174 | case gpoTargetInput: 175 | app.SetFocus(gpoPath) 176 | case gpoPath: 177 | app.SetFocus(gpoLinksPanel) 178 | default: 179 | app.SetFocus(gpoListPanel) 180 | } 181 | } 182 | 183 | func updateGPOEntries() { 184 | runControlGpo.Lock() 185 | if runningGpo { 186 | runControlGpo.Unlock() 187 | updateLog("Another query is still running...", "yellow") 188 | return 189 | } 190 | runningGpo = true 191 | runControlGpo.Unlock() 192 | 193 | defer func() { 194 | runControlGpo.Lock() 195 | runningGpo = false 196 | runControlGpo.Unlock() 197 | }() 198 | 199 | gpLinks = make(map[string][]GPOLink) 200 | gpEntry = make(map[string]*ldap.Entry) 201 | containerLinks = make(map[string][]string) 202 | 203 | gpoListPanel.SetTitle("Applied GPOs") 204 | gpoLinksPanel.Clear() 205 | gpoListPanel.Clear() 206 | gpoPath.Clear() 207 | 208 | app.QueueUpdateDraw(func() { 209 | gpoListPanel.SetCell(0, 0, tview.NewTableCell("Name").SetSelectable(false)) 210 | gpoListPanel.SetCell(0, 1, tview.NewTableCell("Created").SetSelectable(false)) 211 | gpoListPanel.SetCell(0, 2, tview.NewTableCell("Changed").SetSelectable(false)) 212 | gpoListPanel.SetCell(0, 3, tview.NewTableCell("GUID").SetSelectable(false)) 213 | 214 | // Load all gpLinks 215 | updateLog("Querying all gpLinks", "yellow") 216 | 217 | gpLinkObjs, _ := lc.Query(lc.DefaultRootDN, "(gpLink=*)", ldap.ScopeWholeSubtree, false) 218 | 219 | for _, gpLinkObj := range gpLinkObjs { 220 | gpLinkVals := gpLinkObj.GetAttributeValue("gPLink") 221 | 222 | links, _ := ParseGPLinks(gpLinkVals, gpLinkObj.DN) 223 | 224 | for _, link := range links { 225 | gpLinks[link.GUID] = append(gpLinks[link.GUID], link) 226 | containerLinks[link.Target] = append(containerLinks[link.Target], link.GUID) 227 | } 228 | } 229 | updateLog("gpLinks loaded successfully", "green") 230 | 231 | // Load all GPOs from corresponding links 232 | gpoQuery := "(objectClass=groupPolicyContainer)" 233 | gpoTarget = gpoTargetInput.GetText() 234 | 235 | gpoTargetDN := gpoTarget 236 | if gpoTarget != "" { 237 | gpoTargetQuery := fmt.Sprintf("(distinguishedName=%s)", ldap.EscapeFilter(gpoTarget)) 238 | if !strings.Contains(gpoTarget, "=") { 239 | gpoTargetQuery = fmt.Sprintf("(cn=%s)", ldap.EscapeFilter(gpoTarget)) 240 | } 241 | 242 | entries, err := lc.Query(lc.DefaultRootDN, gpoTargetQuery, ldap.ScopeWholeSubtree, false) 243 | 244 | updateLog("Querying for '"+gpoTargetQuery+"'", "yellow") 245 | if err != nil { 246 | updateLog(fmt.Sprint(err), "red") 247 | return 248 | } 249 | 250 | if len(entries) > 0 { 251 | updateLog("GPO target found ("+entries[0].DN+")", "green") 252 | gpoTargetDN = entries[0].DN 253 | } else { 254 | updateLog("GPO target not found", "red") 255 | return 256 | } 257 | } 258 | 259 | var applicableGPOs []string 260 | 261 | dnParts := strings.Split(gpoTargetDN, ",") 262 | for idx := len(dnParts) - 1; idx >= 0; idx -= 1 { 263 | candidateDN := strings.Join(dnParts[idx:], ",") 264 | 265 | candidateGuids, ok := containerLinks[candidateDN] 266 | if ok { 267 | applicableGPOs = append(applicableGPOs, candidateGuids...) 268 | } 269 | } 270 | 271 | gpoQuerySuffix := "" 272 | if len(applicableGPOs) > 0 { 273 | gpoQuerySuffix = "name=" + ldap.EscapeFilter(applicableGPOs[0]) 274 | for _, gpoGuid := range applicableGPOs[1:] { 275 | gpoQuerySuffix = "(|(" + gpoQuerySuffix + ")(name=" + ldap.EscapeFilter(gpoGuid) + "))" 276 | } 277 | } 278 | 279 | if gpoQuerySuffix != "" { 280 | gpoQuery = "(&(" + gpoQuery + ")(" + gpoQuerySuffix + "))" 281 | } 282 | 283 | updateLog("Searching applicable GPOs...", "yellow") 284 | 285 | entries, err := lc.Query(lc.DefaultRootDN, gpoQuery, ldap.ScopeWholeSubtree, false) 286 | if err != nil { 287 | updateLog(fmt.Sprint(err), "red") 288 | return 289 | } 290 | 291 | if len(entries) > 0 { 292 | updateLog("GPOs query completed ("+strconv.Itoa(len(entries))+" GPOs found)", "green") 293 | } else { 294 | updateLog("No applicable GPOs found", "red") 295 | } 296 | 297 | for idx, entry := range entries { 298 | gpoGuid := entry.GetAttributeValue("cn") 299 | gpEntry[gpoGuid] = entry 300 | 301 | gpoName := entry.GetAttributeValue("displayName") 302 | 303 | gpoCreated := entry.GetAttributeValue("whenCreated") 304 | gpoChanged := entry.GetAttributeValue("whenChanged") 305 | 306 | gpoListPanel.SetCellSimple(idx+1, 0, gpoName) 307 | gpoListPanel.SetCellSimple(idx+1, 1, ldaputils.FormatLDAPTime(gpoCreated, TimeFormat)) 308 | gpoListPanel.SetCellSimple(idx+1, 2, ldaputils.FormatLDAPTime(gpoChanged, TimeFormat)) 309 | gpoListPanel.SetCellSimple(idx+1, 3, gpoGuid) 310 | } 311 | 312 | if len(entries) > 0 { 313 | gpoListPanel.SetTitle("Applied GPOs (" + strconv.Itoa(len(entries)) + ")") 314 | gpoListPanel.Select(1, 0) 315 | 316 | app.SetFocus(gpoListPanel) 317 | } 318 | }) 319 | } 320 | 321 | func exportCurrentGpos() { 322 | if gpEntry == nil { 323 | updateLog("An object was not queried yet", "red") 324 | return 325 | } 326 | 327 | exportMap := make(map[string]any) 328 | 329 | exportMap["Links"] = gpLinks 330 | exportMap["Gpos"] = gpEntry 331 | exportMap["Query"] = gpoTarget 332 | 333 | writeDataExport(exportMap, "gpos", "gpos") 334 | } 335 | 336 | func gpoPageKeyHandler(event *tcell.EventKey) *tcell.EventKey { 337 | if event.Key() == tcell.KeyTab || event.Key() == tcell.KeyBacktab { 338 | gpoRotateFocus() 339 | return nil 340 | } 341 | 342 | switch event.Key() { 343 | case tcell.KeyCtrlS: 344 | exportCurrentGpos() 345 | return nil 346 | } 347 | 348 | return event 349 | } 350 | -------------------------------------------------------------------------------- /tui/group.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/Macmod/godap/v2/pkg/ldaputils" 9 | "github.com/gdamore/tcell/v2" 10 | "github.com/go-ldap/ldap/v3" 11 | "github.com/rivo/tview" 12 | ) 13 | 14 | var ( 15 | groupPage *tview.Flex 16 | groupNameInput *tview.InputField 17 | membersPanel *tview.Table 18 | objectNameInput *tview.InputField 19 | groupsPanel *tview.Table 20 | depthInput *tview.InputField 21 | 22 | groups []*ldap.Entry 23 | members []*ldap.Entry 24 | membersSimple []string 25 | maxDepth int 26 | 27 | queryGroup string 28 | queryObject string 29 | groupDN string 30 | objectDN string 31 | ) 32 | 33 | func openRemoveMemberFromGroupForm(targetDN string, groupDN string) { 34 | currentFocus := app.GetFocus() 35 | 36 | confirmText := fmt.Sprintf( 37 | "Do you really want to remove this member from this group?\nMember: %s\nGroup: %s", 38 | targetDN, groupDN, 39 | ) 40 | 41 | promptModal := tview.NewModal(). 42 | SetText(confirmText). 43 | AddButtons([]string{"No", "Yes"}). 44 | SetDoneFunc(func(buttonIndex int, buttonLabel string) { 45 | if buttonLabel == "Yes" { 46 | err := lc.RemoveMemberFromGroup(targetDN, groupDN) 47 | if err != nil { 48 | updateLog(fmt.Sprint(err), "red") 49 | } else { 50 | updateLog(fmt.Sprintf("Member %s removed from group %s", targetDN, groupDN), "green") 51 | } 52 | } 53 | 54 | app.SetRoot(appPanel, true).SetFocus(currentFocus) 55 | }) 56 | 57 | app.SetRoot(promptModal, true).SetFocus(promptModal) 58 | } 59 | 60 | func updateMaxDepth() { 61 | depthStr := depthInput.GetText() 62 | 63 | if depthStr == "" { 64 | maxDepth = 0 65 | } else { 66 | maxDepth, err = strconv.Atoi(depthStr) 67 | if err != nil { 68 | updateLog(fmt.Sprint(err), "red") 69 | return 70 | } 71 | } 72 | } 73 | 74 | func searchGroupMembersAD(groupDN string) { 75 | updateMaxDepth() 76 | membersPanel.Clear() 77 | 78 | members, err = lc.QueryGroupMembersDeep(groupDN, maxDepth) 79 | if err != nil { 80 | updateLog(fmt.Sprint(err), "red") 81 | return 82 | } 83 | 84 | updateLog("Found "+strconv.Itoa(len(members))+" members of '"+groupDN+"'", "green") 85 | 86 | for idx, entry := range members { 87 | sAMAccountName := entry.GetAttributeValue("sAMAccountName") 88 | categoryDN := strings.Split(entry.GetAttributeValue("objectCategory"), ",") 89 | var category string 90 | if len(categoryDN) > 0 { 91 | category = categoryDN[0] 92 | if Emojis { 93 | switch category { 94 | case "CN=Person": 95 | category = ldaputils.EmojiMap["person"] 96 | case "CN=Group": 97 | category = ldaputils.EmojiMap["group"] 98 | case "CN=Computer": 99 | category = ldaputils.EmojiMap["computer"] 100 | } 101 | } 102 | } else { 103 | category = "Unknown" 104 | } 105 | 106 | membersPanel.SetCell(idx, 0, tview.NewTableCell(sAMAccountName).SetReference(entry.DN)) 107 | membersPanel.SetCell(idx, 1, tview.NewTableCell(category).SetReference(entry.DN)) 108 | membersPanel.SetCell(idx, 2, tview.NewTableCell(entry.DN).SetReference(entry.DN)) 109 | } 110 | 111 | membersPanel.Select(0, 0) 112 | membersPanel.ScrollToBeginning() 113 | 114 | app.SetFocus(membersPanel) 115 | } 116 | 117 | func searchGroupMembersBasic(groupDN string) { 118 | membersPanel.Clear() 119 | 120 | membersSimple, err = lc.QueryGroupMembersBasic(groupDN) 121 | if err != nil { 122 | updateLog(fmt.Sprint(err), "red") 123 | return 124 | } 125 | 126 | updateLog("Found "+strconv.Itoa(len(membersSimple))+" members of '"+groupDN+"'", "green") 127 | 128 | for idx, entry := range membersSimple { 129 | membersPanel.SetCell(idx, 0, tview.NewTableCell(entry).SetReference(entry)) 130 | } 131 | 132 | membersPanel.Select(0, 0) 133 | membersPanel.ScrollToBeginning() 134 | 135 | app.SetFocus(membersPanel) 136 | } 137 | 138 | func searchObjectGroupsAD(objectDN string) { 139 | updateMaxDepth() 140 | groupsPanel.Clear() 141 | 142 | groups, err = lc.QueryObjectGroupsDeep(objectDN, maxDepth) 143 | if err != nil { 144 | updateLog(fmt.Sprint(err), "red") 145 | return 146 | } 147 | 148 | updateLog("Found "+strconv.Itoa(len(groups))+" groups containing '"+objectDN+"'", "green") 149 | 150 | for idx, group := range groups { 151 | groupName := group.GetAttributeValue("name") 152 | groupDN := group.DN 153 | groupsPanel.SetCell(idx, 0, tview.NewTableCell(groupName).SetReference(group.DN)) 154 | groupsPanel.SetCell(idx, 1, tview.NewTableCell("👥").SetReference(group.DN)) 155 | groupsPanel.SetCell(idx, 2, tview.NewTableCell(groupDN).SetReference(group.DN)) 156 | } 157 | 158 | groupsPanel.Select(0, 0) 159 | groupsPanel.ScrollToBeginning() 160 | app.SetFocus(groupsPanel) 161 | } 162 | 163 | func searchObjectGroupsBasic(objectDN string) { 164 | groupsPanel.Clear() 165 | 166 | groups, err = lc.QueryObjectGroupsBasic(objectDN) 167 | if err != nil { 168 | updateLog(fmt.Sprint(err), "red") 169 | return 170 | } 171 | 172 | updateLog("Found "+strconv.Itoa(len(groups))+" groups containing '"+objectDN+"'", "green") 173 | 174 | for idx, group := range groups { 175 | groupName := group.GetAttributeValue("cn") 176 | groupDN := group.DN 177 | groupsPanel.SetCell(idx, 0, tview.NewTableCell(groupName).SetReference(group.DN)) 178 | groupsPanel.SetCell(idx, 1, tview.NewTableCell("👥").SetReference(group.DN)) 179 | groupsPanel.SetCell(idx, 2, tview.NewTableCell(groupDN).SetReference(group.DN)) 180 | } 181 | 182 | groupsPanel.Select(0, 0) 183 | groupsPanel.ScrollToBeginning() 184 | app.SetFocus(groupsPanel) 185 | } 186 | 187 | func initGroupPage() { 188 | groupNameInput = tview.NewInputField() 189 | groupNameInput. 190 | SetPlaceholder("Type a group's name or DN"). 191 | SetTitle("Group"). 192 | SetBorder(true) 193 | assignInputFieldTheme(groupNameInput) 194 | 195 | objectNameInput = tview.NewInputField() 196 | objectNameInput. 197 | SetPlaceholder("Type an object's sAMAccountName or DN"). 198 | SetTitle("Object"). 199 | SetBorder(true) 200 | assignInputFieldTheme(objectNameInput) 201 | 202 | depthInput = tview.NewInputField() 203 | depthInput. 204 | SetText("0"). 205 | SetAcceptanceFunc(tview.InputFieldInteger). 206 | SetPlaceholder("Maximum depth to query for nested groups (-1 for all nested members)"). 207 | SetTitle("MaxDepth"). 208 | SetBorder(true) 209 | assignInputFieldTheme(depthInput) 210 | 211 | membersPanel = tview.NewTable() 212 | membersPanel. 213 | SetSelectable(true, false). 214 | SetTitle("Group Members"). 215 | SetBorder(true) 216 | 217 | membersPanel.SetSelectedFunc(func(row, col int) { 218 | cell := membersPanel.GetCell(row, col) 219 | cellId, ok := cell.GetReference().(string) 220 | if ok { 221 | objectNameInput.SetText(cellId) 222 | app.SetFocus(objectNameInput) 223 | } 224 | }) 225 | 226 | groupsPanel = tview.NewTable() 227 | groupsPanel. 228 | SetSelectable(true, false). 229 | SetTitle("Object Groups"). 230 | SetBorder(true) 231 | groupsPanel.SetSelectedFunc(func(row, col int) { 232 | cell := groupsPanel.GetCell(row, col) 233 | cellId, ok := cell.GetReference().(string) 234 | if ok { 235 | groupNameInput.SetText(cellId) 236 | app.SetFocus(groupNameInput) 237 | } 238 | }) 239 | groupPage = tview.NewFlex().SetDirection(tview.FlexRow) 240 | 241 | if lc.Flavor == ldaputils.MicrosoftADFlavor { 242 | groupPage.AddItem(depthInput, 3, 0, false) 243 | } 244 | 245 | groupPage.AddItem( 246 | tview.NewFlex(). 247 | AddItem(groupNameInput, 0, 1, false). 248 | AddItem(objectNameInput, 0, 1, false), 249 | 3, 0, false, 250 | ). 251 | AddItem( 252 | tview.NewFlex(). 253 | AddItem(membersPanel, 0, 1, false). 254 | AddItem(groupsPanel, 0, 1, false), 255 | 0, 1, false, 256 | ) 257 | 258 | groupPage.SetInputCapture(groupPageKeyHandler) 259 | membersPanel.SetInputCapture(membersKeyHandler) 260 | groupsPanel.SetInputCapture(groupsKeyHandler) 261 | 262 | groupNameInput.SetDoneFunc(func(key tcell.Key) { 263 | queryGroup = groupNameInput.GetText() 264 | 265 | groupDN = queryGroup 266 | 267 | if lc.Flavor == ldaputils.MicrosoftADFlavor { 268 | samOrDn, isSam := ldaputils.SamOrDN(queryGroup) 269 | if isSam { 270 | groupDNQuery := fmt.Sprintf("(&(objectCategory=group)%s)", samOrDn) 271 | result, err := lc.QueryFirst(groupDNQuery) 272 | if err != nil { 273 | updateLog(fmt.Sprintf("Group '%s' not found", queryGroup), "red") 274 | return 275 | } 276 | 277 | groupDN = result.DN 278 | } 279 | 280 | searchGroupMembersAD(groupDN) 281 | } else if lc.Flavor == ldaputils.BasicLDAPFlavor { 282 | cnUidOrDN, isCnOrUid := ldaputils.CnUidOrDN(queryGroup) 283 | if isCnOrUid { 284 | groupDNQuery := fmt.Sprintf( 285 | "(&(|(objectClass=groupOfNames)(objectClass=groupOfUniqueNames)(objectClass=posixGroup))%s)", 286 | cnUidOrDN, 287 | ) 288 | 289 | result, err := lc.QueryFirst(groupDNQuery) 290 | if err != nil { 291 | updateLog(fmt.Sprintf("Group '%s' not found", queryGroup), "red") 292 | return 293 | } 294 | 295 | groupDN = result.DN 296 | } 297 | 298 | searchGroupMembersBasic(groupDN) 299 | } 300 | }) 301 | 302 | objectNameInput.SetDoneFunc(func(key tcell.Key) { 303 | queryObject = objectNameInput.GetText() 304 | objectDN = queryObject 305 | 306 | queryFilter := ldaputils.GuessQueryFilter(queryObject, lc.Flavor) 307 | result, err := lc.QueryFirst(queryFilter) 308 | if err != nil { 309 | updateLog(fmt.Sprintf("Object '%s' not found", queryObject), "red") 310 | return 311 | } else { 312 | objectDN = result.DN 313 | } 314 | 315 | if lc.Flavor == ldaputils.MicrosoftADFlavor { 316 | searchObjectGroupsAD(objectDN) 317 | } else { 318 | searchObjectGroupsBasic(objectDN) 319 | } 320 | }) 321 | } 322 | 323 | func groupRotateFocus() { 324 | currentFocus := app.GetFocus() 325 | 326 | switch currentFocus { 327 | case membersPanel: 328 | app.SetFocus(groupNameInput) 329 | case groupNameInput: 330 | app.SetFocus(objectNameInput) 331 | case objectNameInput: 332 | app.SetFocus(groupsPanel) 333 | case groupsPanel: 334 | app.SetFocus(membersPanel) 335 | } 336 | } 337 | 338 | func exportCurrentGroups() { 339 | if groups == nil { 340 | updateLog("An object was not queried yet", "red") 341 | return 342 | } 343 | 344 | exportMap := make(map[string]any) 345 | exportMap["Groups"] = groups 346 | exportMap["DN"] = objectDN 347 | exportMap["Query"] = queryObject 348 | exportMap["MaxDepth"] = maxDepth 349 | 350 | writeDataExport(exportMap, "groups", "object_groups") 351 | } 352 | 353 | func exportCurrentMembers() { 354 | if members == nil { 355 | updateLog("An object was not queried yet", "red") 356 | return 357 | } 358 | 359 | exportMap := make(map[string]any) 360 | exportMap["Members"] = members 361 | exportMap["DN"] = groupDN 362 | exportMap["Query"] = queryGroup 363 | exportMap["MaxDepth"] = maxDepth 364 | 365 | writeDataExport(exportMap, "members", "group_members") 366 | } 367 | 368 | func groupsKeyHandler(event *tcell.EventKey) *tcell.EventKey { 369 | row, col := groupsPanel.GetSelection() 370 | 371 | switch event.Key() { 372 | case tcell.KeyCtrlS: 373 | exportCurrentGroups() 374 | return nil 375 | case tcell.KeyDelete: 376 | selCell := groupsPanel.GetCell(row, col) 377 | if selCell != nil && selCell.GetReference() != nil { 378 | otherGroupDN := selCell.GetReference().(string) 379 | openRemoveMemberFromGroupForm(objectDN, otherGroupDN) 380 | } 381 | case tcell.KeyCtrlG: 382 | selCell := groupsPanel.GetCell(row, col) 383 | if selCell != nil && selCell.GetReference() != nil { 384 | baseDN := selCell.GetReference().(string) 385 | openAddMemberToGroupForm(baseDN, true) 386 | } 387 | return nil 388 | case tcell.KeyCtrlD: 389 | selCell := groupsPanel.GetCell(row, col) 390 | if selCell != nil && selCell.GetReference() != nil { 391 | baseDN := selCell.GetReference().(string) 392 | info.Highlight("3") 393 | objectNameInputDacl.SetText(baseDN) 394 | queryDacl(baseDN) 395 | } 396 | } 397 | 398 | return event 399 | } 400 | 401 | func membersKeyHandler(event *tcell.EventKey) *tcell.EventKey { 402 | row, col := membersPanel.GetSelection() 403 | 404 | switch event.Key() { 405 | case tcell.KeyCtrlS: 406 | exportCurrentMembers() 407 | return nil 408 | case tcell.KeyDelete: 409 | selCell := membersPanel.GetCell(row, col) 410 | if selCell != nil && selCell.GetReference() != nil { 411 | baseDN := selCell.GetReference().(string) 412 | openRemoveMemberFromGroupForm(baseDN, groupDN) 413 | } 414 | case tcell.KeyCtrlG: 415 | selCell := membersPanel.GetCell(row, col) 416 | if selCell != nil && selCell.GetReference() != nil { 417 | baseDN := selCell.GetReference().(string) 418 | openAddMemberToGroupForm(baseDN, false) 419 | } 420 | return nil 421 | case tcell.KeyCtrlD: 422 | selCell := membersPanel.GetCell(row, col) 423 | if selCell != nil && selCell.GetReference() != nil { 424 | baseDN := selCell.GetReference().(string) 425 | info.Highlight("3") 426 | objectNameInputDacl.SetText(baseDN) 427 | queryDacl(baseDN) 428 | } 429 | } 430 | 431 | return event 432 | } 433 | 434 | func groupPageKeyHandler(event *tcell.EventKey) *tcell.EventKey { 435 | if event.Key() == tcell.KeyTab || event.Key() == tcell.KeyBacktab { 436 | groupRotateFocus() 437 | return nil 438 | } 439 | 440 | return event 441 | } 442 | -------------------------------------------------------------------------------- /tui/help.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "github.com/rivo/tview" 5 | ) 6 | 7 | var ( 8 | helpPage *tview.Flex 9 | keybindingsPanel *tview.Table 10 | ) 11 | 12 | func initHelpPage() { 13 | helpText := `[blue]` + GodapVer 14 | 15 | keybindings := [][]string{ 16 | {"Ctrl + Enter", "Global", "Next panel"}, 17 | {"f", "Global", "Toggle attribute formatting"}, 18 | {"e", "Global", "Toggle emojis"}, 19 | {"c", "Global", "Toggle colors"}, 20 | {"a", "Global", "Toggle attribute expansion for multi-value attributes"}, 21 | {"d", "Global", "Toggle \"include deleted objects\" flag"}, 22 | {"s", "Global", "Toggle attribute sorting mode"}, 23 | {"l", "Global", "Change current server address & credentials"}, 24 | {"Ctrl + r", "Global", "Reconnect to the server"}, 25 | {"Ctrl + u", "Global", "Upgrade connection to use TLS (with StartTLS)"}, 26 | {"Ctrl + f", "Explorer & Object Search pages", "Open the finder to search for cached objects & attributes with regex"}, 27 | {"Left Arrow", "Explorer panel", "Collapse the children of the selected object"}, 28 | {"Right Arrow", "Explorer panel", "Expand the children of the selected object"}, 29 | {"r", "Explorer panel", "Reload the attributes and children of the selected object"}, 30 | {"Ctrl + n", "Explorer panel", "Create a new object under the selected object"}, 31 | {"Ctrl + s", "Explorer panel", "Export all loaded nodes in the selected subtree into a JSON file"}, 32 | {"Ctrl + p", "Explorer panel", "Change the password of the selected user or computer account"}, 33 | {"Ctrl + a", "Explorer panel", "Update the userAccountControl of the object interactively"}, 34 | {"Ctrl + l", "Explorer panel", "Move the selected object to another location"}, 35 | {"Delete", "Explorer panel", "Delete the selected object"}, 36 | {"Ctrl + e", "Attributes panel", "Edit the selected attribute of the selected object"}, 37 | {"Ctrl + n", "Attributes panel", "Create a new attribute in the selected object"}, 38 | {"Delete", "Attributes panel", "Delete the selected attribute of the selected object"}, 39 | {"Enter", "Attributes panel (entries hidden)", "Expand all hidden entries of an attribute"}, 40 | {"Delete", "Groups panels", "Remove the selected member from the searched group or vice-versa"}, 41 | {"Ctrl + s", "Object groups panel", "Export the current groups innto a JSON file"}, 42 | {"Ctrl + s", "Group members panel", "Export the current group members into a JSON file"}, 43 | {"Ctrl + g", "Groups panels / Explorer panel / Obj. Search panel", "Add a member to the selected group / add the selected object into a group"}, 44 | {"Ctrl + d", "Groups panels / Explorer panel / Obj. Search panel", "Inspect the DACL of the currently selected object"}, 45 | {"Ctrl + o", "DACL page", "Change the owner of the current security descriptor"}, 46 | {"Ctrl + k", "DACL page", "Change the control flags of the current security descriptor"}, 47 | {"Ctrl + s", "DACL page", "Export the current security descriptor into a JSON file"}, 48 | {"Ctrl + n", "DACL entries panel", "Create a new ACE in the current DACL"}, 49 | {"Ctrl + e", "DACL entries panel", "Edit the selected ACE of the current DACL"}, 50 | {"Delete", "DACL entries panel", "Deletes the selected ACE of the current DACL"}, 51 | {"Ctrl + s", "GPO page", "Export the current GPOs and their links into a JSON file"}, 52 | {"Ctrl + s", "DNS zones panel", "Export the selected zones and their child DNS nodes into a JSON file"}, 53 | {"r", "DNS zones panel", "Reload the nodes of the selected zone / the records of the selected node"}, 54 | {"Ctrl + n", "DNS zones panel", "Create a new node under the selected zone or a new zone if the root is selected"}, 55 | {"Ctrl + e", "DNS zones panel", "Edit the records of the currently selected node"}, 56 | {"Delete", "DNS zones panel", "Delete the selected DNS zone or DNS node"}, 57 | {"Delete", "Records Preview (in ADIDNS Node Editor)", "Delete the selected record of the ADIDNS node"}, 58 | {"h", "Global", "Show/hide headers"}, 59 | {"q", "Global", "Exit the program"}, 60 | } 61 | 62 | // Create a table 63 | keybindingsPanel = tview.NewTable(). 64 | SetSelectable(true, false). 65 | SetEvaluateAllRows(true). 66 | SetFixed(1, 0) 67 | 68 | headers := []string{"Keybinding", "Context", "Action"} 69 | for col, header := range headers { 70 | cell := tview.NewTableCell(header). 71 | SetTextColor(tview.Styles.SecondaryTextColor). 72 | SetAlign(tview.AlignCenter).SetSelectable(false) 73 | keybindingsPanel.SetCell(0, col, cell) 74 | } 75 | 76 | for row, binding := range keybindings { 77 | for col, value := range binding { 78 | cell := tview.NewTableCell(value). 79 | SetTextColor(tview.Styles.PrimaryTextColor). 80 | SetAlign(tview.AlignLeft) 81 | keybindingsPanel.SetCell(row+1, col, cell) 82 | } 83 | } 84 | 85 | keybindingsPanel.Select(1, 0) 86 | 87 | helpTextView := tview.NewTextView(). 88 | SetText(helpText). 89 | SetTextAlign(tview.AlignCenter). 90 | SetDynamicColors(true) 91 | 92 | frame := tview.NewFrame(keybindingsPanel) 93 | frame.SetBorders(0, 0, 0, 0, 0, 0). 94 | SetTitleAlign(tview.AlignCenter). 95 | SetTitle(" Keybindings ") 96 | 97 | helpPage = tview.NewFlex(). 98 | SetDirection(tview.FlexRow). 99 | AddItem(helpTextView, 2, 0, true). 100 | AddItem(frame, 0, 2, false) 101 | } 102 | -------------------------------------------------------------------------------- /tui/interface.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/gdamore/tcell/v2" 7 | "github.com/rivo/tview" 8 | ) 9 | 10 | func handleEscape(returnFocus tview.Primitive) func(*tcell.EventKey) *tcell.EventKey { 11 | return func(event *tcell.EventKey) *tcell.EventKey { 12 | if event.Key() == tcell.KeyEscape { 13 | app.SetRoot(appPanel, true).SetFocus(returnFocus) 14 | return nil 15 | } 16 | return event 17 | } 18 | } 19 | 20 | func getParentNode(node *tview.TreeNode, tree *tview.TreeView) *tview.TreeNode { 21 | pathToCurrent := tree.GetPath(node) 22 | 23 | if len(pathToCurrent) > 1 { 24 | return pathToCurrent[len(pathToCurrent)-2] 25 | } 26 | 27 | return nil 28 | } 29 | 30 | func findEntryInChildren(dn string, parent *tview.TreeNode) int { 31 | siblings := parent.GetChildren() 32 | 33 | for idx, loopNode := range siblings { 34 | if loopNode.GetReference().(string) == dn { 35 | return idx 36 | } 37 | } 38 | 39 | return -1 40 | } 41 | 42 | func numericAcceptanceFunc(textToCheck string, lastChar rune) bool { 43 | _, err := strconv.Atoi(textToCheck) 44 | return err == nil 45 | } 46 | -------------------------------------------------------------------------------- /tui/main_test.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestSetupTimeFormat(t *testing.T) { 8 | tests := []struct { 9 | name string 10 | input string 11 | expected string 12 | }{ 13 | { 14 | name: "EU format", 15 | input: "eu", 16 | expected: "02/01/2006 15:04:05", 17 | }, 18 | { 19 | name: "Empty string", 20 | input: "", 21 | expected: "02/01/2006 15:04:05", 22 | }, 23 | { 24 | name: "US format", 25 | input: "US", 26 | expected: "01/02/2006 15:04:05", 27 | }, 28 | { 29 | name: "ISO format", 30 | input: "ISO8601", 31 | expected: "2006-01-02 15:04:05", 32 | }, 33 | { 34 | name: "Custom format", 35 | input: "20060102150405", 36 | expected: "20060102150405", 37 | }, 38 | } 39 | 40 | for _, tt := range tests { 41 | t.Run(tt.name, func(t *testing.T) { 42 | result := setupTimeFormat(tt.input) 43 | if result != tt.expected { 44 | t.Errorf("got %q, want %q", result, tt.expected) 45 | } 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tui/search.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "fmt" 5 | "slices" 6 | "strconv" 7 | "strings" 8 | "sync" 9 | "time" 10 | 11 | "github.com/Macmod/godap/v2/pkg/ldaputils" 12 | "github.com/gdamore/tcell/v2" 13 | "github.com/go-ldap/ldap/v3" 14 | "github.com/rivo/tview" 15 | ) 16 | 17 | var ( 18 | searchTreePanel *tview.TreeView 19 | searchQueryPanel *tview.InputField 20 | searchAttrsPanel *tview.Table 21 | 22 | searchLibraryPanel *tview.TreeView 23 | sidePanel *tview.Pages 24 | searchPage *tview.Flex 25 | runControl sync.Mutex 26 | running bool 27 | 28 | searchCache EntryCache 29 | searchHistoryEntries []SearchHistoryEntry 30 | 31 | searchHistoryPanel *tview.Table 32 | ) 33 | 34 | type SearchHistoryEntry struct { 35 | Timestamp time.Time 36 | Query string 37 | Duration time.Duration 38 | Results int 39 | } 40 | 41 | func addToSearchHistory(query string, duration time.Duration, results int) { 42 | // Don't add empty queries to history 43 | if query == "" { 44 | return 45 | } 46 | 47 | entry := SearchHistoryEntry{ 48 | Timestamp: time.Now(), 49 | Query: query, 50 | Duration: duration, 51 | Results: results, 52 | } 53 | 54 | // Add to beginning of slice (most recent first) 55 | searchHistoryEntries = append([]SearchHistoryEntry{entry}, searchHistoryEntries...) 56 | } 57 | 58 | var searchLoadedDNs map[string]*tview.TreeNode = make(map[string]*tview.TreeNode) 59 | 60 | func reloadSearchAttrsPanel(node *tview.TreeNode, useCache bool) { 61 | reloadAttributesPanel(node, searchAttrsPanel, useCache, &searchCache) 62 | } 63 | 64 | func reloadSearchNode(currentNode *tview.TreeNode) { 65 | baseDN := currentNode.GetReference().(string) 66 | 67 | updateLog("Reloading node "+baseDN, "yellow") 68 | reloadSearchAttrsPanel(currentNode, false) 69 | selectAnchoredAttribute(searchAttrsPanel) 70 | 71 | updatedEntry := searchCache.entries[baseDN] 72 | entryName := getNodeName(updatedEntry) 73 | 74 | if Colors { 75 | color, _ := GetEntryColor(updatedEntry) 76 | currentNode.SetColor(color) 77 | } 78 | 79 | currentNode.SetText(entryName) 80 | 81 | // TODO: 82 | // Maybe there should be a separate option to also reload the children of the node 83 | 84 | updateLog("Node "+baseDN+" reloaded", "green") 85 | } 86 | 87 | func updateSearchHistoryPanel() { 88 | searchHistoryPanel.Clear() 89 | 90 | searchHistoryPanel.SetCell(0, 0, tview.NewTableCell("StartTime").SetSelectable(false)) 91 | searchHistoryPanel.SetCell(0, 1, tview.NewTableCell("Duration").SetSelectable(false)) 92 | searchHistoryPanel.SetCell(0, 2, tview.NewTableCell("Results").SetSelectable(false)) 93 | searchHistoryPanel.SetCell(0, 3, tview.NewTableCell("Query").SetSelectable(false)) 94 | 95 | for i, entry := range searchHistoryEntries { 96 | row := i + 1 97 | 98 | timestamp := entry.Timestamp.Format(TimeFormat) 99 | duration := fmt.Sprintf("%.4fs", entry.Duration.Seconds()) 100 | results := strconv.Itoa(entry.Results) 101 | 102 | searchHistoryPanel.SetCell(row, 0, tview.NewTableCell(timestamp)) 103 | searchHistoryPanel.SetCell(row, 1, tview.NewTableCell(duration)) 104 | searchHistoryPanel.SetCell(row, 2, tview.NewTableCell(results)) 105 | searchHistoryPanel.SetCell(row, 3, tview.NewTableCell(entry.Query)) 106 | } 107 | } 108 | 109 | func initSearchPage() { 110 | searchCache = EntryCache{ 111 | entries: make(map[string]*ldap.Entry), 112 | } 113 | 114 | searchQueryPanel = tview.NewInputField() 115 | searchQueryPanel. 116 | SetPlaceholder("Type an LDAP search filter or the name of an object"). 117 | SetTitle("Search Filter (Recursive)"). 118 | SetBorder(true) 119 | assignInputFieldTheme(searchQueryPanel) 120 | 121 | tabs := tview.NewTextView(). 122 | SetTextAlign(tview.AlignCenter). 123 | SetWrap(false). 124 | SetRegions(true). 125 | SetDynamicColors(true) 126 | tabs.SetBackgroundColor(tcell.ColorBlack) 127 | tabs.SetBorder(true) 128 | 129 | searchTreePanel = tview.NewTreeView() 130 | searchTreePanel. 131 | SetTitle("Search Results"). 132 | SetBorder(true) 133 | 134 | searchTreePanel.SetChangedFunc(func(node *tview.TreeNode) { 135 | searchAttrsPanel.Clear() 136 | reloadSearchAttrsPanel(node, true) 137 | selectAnchoredAttribute(searchAttrsPanel) 138 | }) 139 | 140 | searchAttrsPanel = tview.NewTable(). 141 | SetSelectable(true, true). 142 | SetEvaluateAllRows(true) 143 | searchAttrsPanel.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { 144 | currentNode := searchTreePanel.GetCurrentNode() 145 | if currentNode == nil || currentNode.GetReference() == nil { 146 | return event 147 | } 148 | 149 | return attrsPanelKeyHandler(event, currentNode, &searchCache, searchAttrsPanel) 150 | }) 151 | searchAttrsPanel.SetSelectionChangedFunc(storeAnchoredAttribute(searchAttrsPanel)) 152 | 153 | searchLibraryPanel = tview.NewTreeView() 154 | 155 | searchLibraryRoot := tview.NewTreeNode("Queries").SetSelectable(false) 156 | searchLibraryPanel.SetRoot(searchLibraryRoot) 157 | 158 | searchHistoryPanel = tview.NewTable(). 159 | SetSelectable(true, false). 160 | SetBorders(false). 161 | SetFixed(1, 0) 162 | 163 | searchHistoryPanel. 164 | SetTitle("Search History") 165 | 166 | searchHistoryPanel.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { 167 | switch event.Key() { 168 | case tcell.KeyEnter: 169 | row, _ := searchHistoryPanel.GetSelection() 170 | 171 | if row > 0 && row <= len(searchHistoryEntries) { 172 | entry := searchHistoryEntries[row-1] 173 | searchQueryPanel.SetText(entry.Query) 174 | app.SetFocus(searchQueryPanel) 175 | 176 | return nil 177 | } 178 | } 179 | 180 | return event 181 | }) 182 | 183 | sidePanel = tview.NewPages(). 184 | AddPage("page-0", searchLibraryPanel, true, true). 185 | AddPage("page-1", searchAttrsPanel, true, false). 186 | AddPage("page-2", searchHistoryPanel, true, false) 187 | 188 | sidePanel.SetBorder(true) 189 | 190 | var predefinedLdapQueriesKeys []string 191 | 192 | var chosenLibrary map[string][]ldaputils.LibQuery 193 | 194 | if lc.Flavor == ldaputils.MicrosoftADFlavor { 195 | predefinedLdapQueriesKeys = []string{"Security", "Users", "Computers", "Enum"} 196 | chosenLibrary = ldaputils.PredefinedLdapQueriesAD 197 | } else { 198 | predefinedLdapQueriesKeys = []string{"Users", "Groups", "Enum"} 199 | chosenLibrary = ldaputils.PredefinedLdapQueriesBasic 200 | } 201 | 202 | for _, key := range predefinedLdapQueriesKeys { 203 | children := chosenLibrary[key] 204 | 205 | childNode := tview.NewTreeNode(key). 206 | SetSelectable(false). 207 | SetExpanded(true) 208 | 209 | for _, val := range children { 210 | childNode.AddChild( 211 | tview.NewTreeNode(val.Title). 212 | SetReference(val.Filter). 213 | SetSelectable(true)) 214 | } 215 | 216 | searchLibraryRoot.AddChild(childNode) 217 | } 218 | 219 | searchLibraryPanel.SetSelectedFunc( 220 | func(node *tview.TreeNode) { 221 | runControl.Lock() 222 | if running { 223 | runControl.Unlock() 224 | updateLog("Another query is still running...", "yellow") 225 | return 226 | } 227 | runControl.Unlock() 228 | 229 | searchQueryDoneHandler(tcell.KeyEnter) 230 | }, 231 | ) 232 | 233 | searchLibraryPanel.SetChangedFunc( 234 | func(node *tview.TreeNode) { 235 | ref := node.GetReference() 236 | if ref == nil { 237 | searchQueryPanel.SetText("") 238 | return 239 | } 240 | 241 | nowTimestamp := time.Now().UnixNano() 242 | 243 | nowTimestampStr := strconv.FormatInt(nowTimestamp, 10) 244 | lastDayTimestampStr := strconv.FormatInt(nowTimestamp-86400, 10) 245 | lastMonthTimestampStr := strconv.FormatInt(nowTimestamp-2592000, 10) 246 | 247 | editedQuery := strings.Replace(ref.(string), "DC=domain,DC=com", lc.DefaultRootDN, -1) 248 | editedQuery = strings.Replace(editedQuery, "", nowTimestampStr, -1) 249 | editedQuery = strings.Replace(editedQuery, "", lastDayTimestampStr, -1) 250 | editedQuery = strings.Replace(editedQuery, "", lastMonthTimestampStr, -1) 251 | 252 | searchQueryPanel.SetText(editedQuery) 253 | }, 254 | ) 255 | 256 | searchQueryPanel.SetDoneFunc(searchQueryDoneHandler) 257 | 258 | searchTreePanel.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { 259 | currentNode := searchTreePanel.GetCurrentNode() 260 | if currentNode == nil { 261 | return event 262 | } 263 | 264 | switch event.Key() { 265 | case tcell.KeyRight: 266 | if len(currentNode.GetChildren()) != 0 && !currentNode.IsExpanded() { 267 | currentNode.SetExpanded(true) 268 | } 269 | return nil 270 | case tcell.KeyLeft: 271 | if currentNode.IsExpanded() { // Collapse current node 272 | currentNode.SetExpanded(false) 273 | searchTreePanel.SetCurrentNode(currentNode) 274 | } else { // Collapse parent node 275 | pathToCurrent := searchTreePanel.GetPath(currentNode) 276 | if len(pathToCurrent) > 1 { 277 | parentNode := pathToCurrent[len(pathToCurrent)-2] 278 | parentNode.SetExpanded(false) 279 | searchTreePanel.SetCurrentNode(parentNode) 280 | } 281 | } 282 | return nil 283 | case tcell.KeyDelete: 284 | if currentNode.GetReference() != nil { 285 | openDeleteObjectForm(currentNode, nil) 286 | } 287 | case tcell.KeyCtrlS: 288 | exportCacheToFile(currentNode, &searchCache, "results") 289 | case tcell.KeyCtrlP: 290 | if currentNode.GetReference() != nil { 291 | openPasswordChangeForm(currentNode) 292 | } 293 | case tcell.KeyCtrlL: 294 | if currentNode.GetReference() != nil { 295 | openMoveObjectForm(currentNode, nil) 296 | } 297 | case tcell.KeyCtrlA: 298 | if currentNode.GetReference() != nil { 299 | openUpdateUacForm(currentNode, &searchCache, func() { 300 | go app.QueueUpdateDraw(func() { 301 | reloadSearchNode(currentNode) 302 | }) 303 | }) 304 | } 305 | case tcell.KeyCtrlN: 306 | if currentNode.GetReference() != nil { 307 | openCreateObjectForm(currentNode, nil) 308 | } 309 | case tcell.KeyCtrlG: 310 | if currentNode.GetReference() != nil { 311 | baseDN := currentNode.GetReference().(string) 312 | entry := searchCache.entries[baseDN] 313 | objClasses := entry.GetAttributeValues("objectClass") 314 | isGroup := slices.Contains(objClasses, "group") 315 | openAddMemberToGroupForm(baseDN, isGroup) 316 | } 317 | case tcell.KeyCtrlD: 318 | if currentNode.GetReference() != nil { 319 | baseDN := currentNode.GetReference().(string) 320 | info.Highlight("3") 321 | objectNameInputDacl.SetText(baseDN) 322 | queryDacl(baseDN) 323 | } 324 | } 325 | 326 | switch event.Rune() { 327 | case 'r', 'R': 328 | if currentNode.GetReference() != nil { 329 | go app.QueueUpdateDraw(func() { 330 | reloadSearchNode(currentNode) 331 | }) 332 | } 333 | } 334 | 335 | return event 336 | }) 337 | 338 | fmt.Fprintf(tabs, `["%s"][white]%s[black][""] `, "0", "Library") 339 | fmt.Fprintf(tabs, `["%s"][white]%s[black][""] `, "1", "Attrs") 340 | fmt.Fprintf(tabs, `["%s"][white]%s[black][""]`, "2", "History") 341 | 342 | tabs.SetHighlightedFunc(func(added, removed, remaining []string) { 343 | if len(added) > 0 { 344 | sidePanel.SwitchToPage("page-" + added[0]) 345 | } else { 346 | tabs.Highlight("0") 347 | } 348 | }) 349 | 350 | tabs.Highlight("0") 351 | 352 | searchPage = tview.NewFlex().SetDirection(tview.FlexRow). 353 | AddItem( 354 | tview.NewFlex(). 355 | AddItem(searchQueryPanel, 0, 1, false). 356 | AddItem(tabs, 23, 0, false), 357 | 3, 0, false, 358 | ). 359 | AddItem( 360 | tview.NewFlex(). 361 | AddItem(searchTreePanel, 0, 1, false). 362 | AddItem(sidePanel, 0, 1, false), 363 | 0, 8, false, 364 | ) 365 | 366 | searchPage.SetInputCapture(searchPageKeyHandler) 367 | } 368 | 369 | func searchQueryDoneHandler(key tcell.Key) { 370 | updateLog("Performing recursive query...", "yellow") 371 | 372 | rootNode := tview.NewTreeNode(lc.DefaultRootDN).SetSelectable(true) 373 | searchTreePanel. 374 | SetRoot(rootNode). 375 | SetCurrentNode(rootNode) 376 | 377 | searchCache.Clear() 378 | clear(searchLoadedDNs) 379 | 380 | searchQuery := searchQueryPanel.GetText() 381 | 382 | go func() { 383 | runControl.Lock() 384 | if running { 385 | runControl.Unlock() 386 | return 387 | } 388 | running = true 389 | runControl.Unlock() 390 | 391 | if searchQuery != "" && !strings.Contains(searchQuery, "(") { 392 | searchQuery = fmt.Sprintf( 393 | "(|(samAccountName=%s)(cn=%s)(ou=%s)(name=%s))", 394 | searchQuery, searchQuery, searchQuery, searchQuery, 395 | ) 396 | } 397 | 398 | startTime := time.Now() 399 | 400 | entries, _ := lc.Query(lc.DefaultRootDN, searchQuery, ldap.ScopeWholeSubtree, Deleted) 401 | 402 | duration := time.Since(startTime) 403 | 404 | firstLeaf := true 405 | 406 | for _, entry := range entries { 407 | if entry.DN == lc.DefaultRootDN { 408 | continue 409 | } 410 | 411 | var nodeName string 412 | entryName := getNodeName(entry) 413 | dnPath := strings.TrimSuffix(entry.DN, ","+lc.DefaultRootDN) 414 | 415 | components := strings.Split(dnPath, ",") 416 | currentNode := searchTreePanel.GetRoot() 417 | 418 | for i := len(components) - 1; i >= 0; i-- { 419 | partialDN := strings.Join(components[i:], ",") 420 | 421 | childNode, ok := searchLoadedDNs[partialDN] 422 | if !ok { 423 | app.QueueUpdateDraw(func() { 424 | if i == 0 { 425 | // Leaf node 426 | nodeName = entryName 427 | childNode = tview.NewTreeNode(nodeName). 428 | SetReference(entry.DN). 429 | SetExpanded(false). 430 | SetSelectable(true) 431 | 432 | if Colors { 433 | color, changed := GetEntryColor(entry) 434 | if changed { 435 | childNode.SetColor(color) 436 | } 437 | } 438 | currentNode.AddChild(childNode) 439 | 440 | if firstLeaf { 441 | searchTreePanel.SetCurrentNode(childNode) 442 | firstLeaf = false 443 | } 444 | 445 | searchCache.Add(entry.DN, entry) 446 | } else { 447 | // Non-leaf node 448 | nodeName = components[i] 449 | childNode = tview.NewTreeNode(nodeName). 450 | SetExpanded(true). 451 | SetSelectable(true) 452 | currentNode.AddChild(childNode) 453 | } 454 | }) 455 | 456 | searchLoadedDNs[partialDN] = childNode 457 | } 458 | 459 | currentNode = childNode 460 | } 461 | } 462 | 463 | app.QueueUpdateDraw(func() { 464 | updateLog( 465 | fmt.Sprintf("Query completed (%d objects found in %.4fs)", len(entries), duration.Seconds()), "green") 466 | }) 467 | 468 | addToSearchHistory(searchQuery, duration, len(entries)) 469 | app.QueueUpdateDraw(func() { 470 | updateSearchHistoryPanel() 471 | }) 472 | 473 | runControl.Lock() 474 | running = false 475 | runControl.Unlock() 476 | }() 477 | } 478 | 479 | func searchPageKeyHandler(event *tcell.EventKey) *tcell.EventKey { 480 | if event.Key() == tcell.KeyTab || event.Key() == tcell.KeyBacktab { 481 | searchRotateFocus() 482 | return nil 483 | } 484 | 485 | switch event.Key() { 486 | case tcell.KeyCtrlF: 487 | openFinder(&searchCache, "Object Search") 488 | } 489 | 490 | return event 491 | } 492 | 493 | func searchRotateFocus() { 494 | currentFocus := app.GetFocus() 495 | 496 | switch currentFocus { 497 | case searchTreePanel: 498 | app.SetFocus(searchQueryPanel) 499 | case searchQueryPanel: 500 | app.SetFocus(sidePanel) 501 | case searchLibraryPanel, searchAttrsPanel, searchHistoryPanel: 502 | app.SetFocus(searchTreePanel) 503 | } 504 | } 505 | -------------------------------------------------------------------------------- /tui/theme.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "time" 7 | 8 | "github.com/gdamore/tcell/v2" 9 | "github.com/go-ldap/ldap/v3" 10 | "github.com/rivo/tview" 11 | ) 12 | 13 | type GodapTheme struct { 14 | TViewTheme tview.Theme 15 | 16 | // Input fields 17 | FieldBackgroundColor tcell.Color 18 | PlaceholderStyle tcell.Style 19 | PlaceholderTextColor tcell.Color 20 | 21 | // Form buttons 22 | FormButtonStyle tcell.Style 23 | FormButtonTextColor tcell.Color 24 | FormButtonBackgroundColor tcell.Color 25 | FormButtonActivatedStyle tcell.Style 26 | 27 | // Tree node colors 28 | RecycledNodeColor tcell.Color 29 | DeletedNodeColor tcell.Color 30 | DisabledNodeColor tcell.Color 31 | } 32 | 33 | // Theme definitions - controls the colors of all Godap pages 34 | var baseTheme tview.Theme = tview.Theme{ 35 | PrimitiveBackgroundColor: tcell.ColorBlack, 36 | ContrastBackgroundColor: tcell.ColorBlue, 37 | MoreContrastBackgroundColor: tcell.ColorGreen, 38 | BorderColor: tcell.ColorWhite, 39 | TitleColor: tcell.ColorWhite, 40 | GraphicsColor: tcell.ColorWhite, 41 | PrimaryTextColor: tcell.ColorWhite, 42 | SecondaryTextColor: tcell.ColorYellow, 43 | TertiaryTextColor: tcell.ColorGreen, 44 | InverseTextColor: tcell.ColorBlue, 45 | ContrastSecondaryTextColor: tcell.ColorNavy, 46 | } 47 | 48 | var DefaultTheme = GodapTheme{ 49 | // Base TView theme 50 | TViewTheme: baseTheme, 51 | 52 | // Input fields for main pages 53 | FieldBackgroundColor: tcell.ColorBlack, 54 | PlaceholderStyle: tcell.Style{}.Foreground(tcell.ColorGray).Background(tcell.ColorBlack), 55 | PlaceholderTextColor: tcell.ColorGray, 56 | 57 | // Form buttons 58 | FormButtonStyle: tcell.Style{}.Background(tcell.ColorWhite), 59 | FormButtonTextColor: tcell.ColorBlack, 60 | FormButtonBackgroundColor: tcell.ColorWhite, 61 | FormButtonActivatedStyle: tcell.StyleDefault.Background(tcell.ColorGray), 62 | 63 | // Tree node colors 64 | RecycledNodeColor: tcell.ColorRed, 65 | DeletedNodeColor: tcell.ColorGray, 66 | DisabledNodeColor: tcell.ColorYellow, 67 | } 68 | 69 | // Helpers to assign themes manually to primitives 70 | // TODO: Refactor again 71 | func assignInputFieldTheme(input *tview.InputField) { 72 | input.SetPlaceholderStyle(DefaultTheme.PlaceholderStyle). 73 | SetPlaceholderTextColor(DefaultTheme.PlaceholderTextColor). 74 | SetFieldBackgroundColor(DefaultTheme.FieldBackgroundColor) 75 | } 76 | 77 | func assignButtonTheme(btn *tview.Button) { 78 | btn.SetStyle(DefaultTheme.FormButtonStyle). 79 | SetLabelColor(DefaultTheme.FormButtonTextColor). 80 | SetActivatedStyle(DefaultTheme.FormButtonActivatedStyle) 81 | } 82 | 83 | func assignDropDownTheme(dropdown *tview.DropDown) { 84 | dropdown.SetFieldBackgroundColor(DefaultTheme.FieldBackgroundColor) 85 | } 86 | 87 | func assignFormTheme(form *tview.Form) { 88 | form. 89 | SetButtonBackgroundColor(DefaultTheme.FormButtonBackgroundColor). 90 | SetButtonTextColor(DefaultTheme.FormButtonTextColor). 91 | SetButtonActivatedStyle(DefaultTheme.FormButtonActivatedStyle) 92 | } 93 | 94 | // Form customizations 95 | type XForm struct { 96 | *tview.Form 97 | } 98 | 99 | func NewXForm() *XForm { 100 | return &XForm{ 101 | tview.NewForm(). 102 | SetFieldBackgroundColor(DefaultTheme.FieldBackgroundColor). 103 | SetButtonBackgroundColor(DefaultTheme.FormButtonBackgroundColor). 104 | SetButtonTextColor(DefaultTheme.FormButtonTextColor). 105 | SetButtonActivatedStyle(DefaultTheme.FormButtonActivatedStyle), 106 | } 107 | } 108 | 109 | func (f *XForm) AddTextView(label, text string, fieldWidth, fieldHeight int, dynamicColors, scrollable bool) *XForm { 110 | if fieldHeight == 0 { 111 | fieldHeight = tview.DefaultFormFieldHeight 112 | } 113 | 114 | textView := tview.NewTextView() 115 | f.AddFormItem(textView. 116 | SetLabel(label). 117 | SetSize(fieldHeight, fieldWidth). 118 | SetDynamicColors(dynamicColors). 119 | SetScrollable(scrollable). 120 | SetText(text)) 121 | 122 | return f 123 | } 124 | 125 | func (f *XForm) AddInputField(label, value string, fieldWidth int, accept func(textToCheck string, lastChar rune) bool, changed func(text string)) *XForm { 126 | inputField := tview.NewInputField() 127 | f.AddFormItem(inputField. 128 | SetFieldStyle(tcell.StyleDefault.Background(tcell.ColorWhite).Foreground(tcell.ColorBlack)). 129 | SetPlaceholderStyle(DefaultTheme.PlaceholderStyle). 130 | SetPlaceholderTextColor(DefaultTheme.PlaceholderTextColor). 131 | SetLabel(label). 132 | SetText(value). 133 | SetFieldWidth(fieldWidth). 134 | SetAcceptanceFunc(accept). 135 | SetChangedFunc(changed)) 136 | 137 | return f 138 | } 139 | 140 | func (f *XForm) AddTextArea(label string, text string, fieldWidth, fieldHeight int, maxLength int, changed func(text string)) *XForm { 141 | if fieldHeight == 0 { 142 | fieldHeight = tview.DefaultFormFieldHeight 143 | } 144 | 145 | textArea := tview.NewTextArea() 146 | textArea. 147 | SetLabel(label). 148 | SetSize(fieldHeight, fieldWidth). 149 | SetMaxLength(maxLength). 150 | SetPlaceholderStyle(DefaultTheme.PlaceholderStyle) 151 | 152 | if text != "" { 153 | textArea.SetText(text, true) 154 | } 155 | 156 | if changed != nil { 157 | textArea.SetChangedFunc(func() { 158 | changed(textArea.GetText()) 159 | }) 160 | } 161 | 162 | f.AddFormItem(textArea) 163 | 164 | return f 165 | } 166 | 167 | func (f *XForm) AddPasswordField(label, value string, fieldWidth int, mask rune, changed func(text string)) *XForm { 168 | if mask == 0 { 169 | mask = '*' 170 | } 171 | 172 | f.AddFormItem(tview.NewInputField(). 173 | SetFieldTextColor(tcell.ColorBlack). 174 | SetLabel(label). 175 | SetText(value). 176 | SetFieldWidth(fieldWidth). 177 | SetMaskCharacter(mask). 178 | SetChangedFunc(changed)) 179 | 180 | return f 181 | } 182 | 183 | func (f *XForm) AddDropDown(label string, options []string, initialOption int, selected func(option string, optionIndex int)) *XForm { 184 | dropdown := tview.NewDropDown() 185 | dropdown. 186 | SetFieldBackgroundColor(DefaultTheme.FieldBackgroundColor). 187 | SetLabel(label). 188 | SetOptions(options, selected). 189 | SetCurrentOption(initialOption) 190 | 191 | f.AddFormItem(dropdown) 192 | 193 | return f 194 | } 195 | 196 | func (f *XForm) AddCheckbox(label string, checked bool, changed func(checked bool)) *XForm { 197 | f.AddFormItem( 198 | tview.NewCheckbox(). 199 | //SetCheckedStyle(tcell.StyleDefault.Background(tcell.ColorGreen).Foreground(tcell.ColorWhite)). 200 | //SetUncheckedStyle(tcell.StyleDefault.Background(tcell.ColorRed).Foreground(tcell.ColorWhite)). 201 | SetCheckedString("True"). 202 | SetUncheckedString("False"). 203 | SetLabel(label). 204 | SetChecked(checked). 205 | SetChangedFunc(changed), 206 | ) 207 | 208 | return f 209 | } 210 | 211 | func GetEntryColor(entry *ldap.Entry) (tcell.Color, bool) { 212 | isDeleted := strings.ToLower(entry.GetAttributeValue("isDeleted")) == "true" 213 | isRecycled := strings.ToLower(entry.GetAttributeValue("isRecycled")) == "true" 214 | 215 | if isDeleted { 216 | if isRecycled { 217 | return DefaultTheme.RecycledNodeColor, true 218 | } else { 219 | return DefaultTheme.DeletedNodeColor, true 220 | } 221 | } else { 222 | uac := entry.GetAttributeValue("userAccountControl") 223 | uacNum, err := strconv.Atoi(uac) 224 | 225 | if err == nil && uacNum&2 != 0 { 226 | return DefaultTheme.DisabledNodeColor, true 227 | } 228 | } 229 | 230 | return baseTheme.PrimaryTextColor, false 231 | } 232 | 233 | func GetAttrCellColor(cellName string, cellValue string) (string, bool) { 234 | var color string = "" 235 | 236 | switch cellName { 237 | case "lastLogonTimestamp", "accountExpires", "badPasswordTime", "lastLogoff", "lastLogon", "pwdLastSet", "creationTime", "lockoutTime": 238 | intValue, err := strconv.ParseInt(cellValue, 10, 64) 239 | if err == nil { 240 | unixTime := (intValue - 116444736000000000) / 10000000 241 | t := time.Unix(unixTime, 0).UTC() 242 | 243 | daysDiff := int(time.Since(t).Hours() / 24) 244 | 245 | if daysDiff <= 7 { 246 | color = "green" 247 | } else if daysDiff <= 90 { 248 | color = "yellow" 249 | } else { 250 | color = "red" 251 | } 252 | } 253 | case "objectGUID", "objectSid": 254 | color = "gray" 255 | case "whenCreated", "whenChanged": 256 | layout := "20060102150405.0Z" 257 | t, err := time.Parse(layout, cellValue) 258 | if err == nil { 259 | daysDiff := int(time.Since(t).Hours() / 24) 260 | 261 | if daysDiff <= 7 { 262 | color = "green" 263 | } else if daysDiff <= 90 { 264 | color = "yellow" 265 | } else { 266 | color = "red" 267 | } 268 | } 269 | } 270 | 271 | switch cellValue { 272 | case "TRUE", "Enabled", "Normal", "PwdNotExpired": 273 | color = "green" 274 | case "FALSE", "NotNormal", "PwdExpired": 275 | color = "red" 276 | case "Disabled": 277 | color = "yellow" 278 | } 279 | 280 | if color != "" { 281 | return color, true 282 | } 283 | 284 | return "", false 285 | } 286 | 287 | // Strangely, tview.Button does not implement FormItem yet, 288 | // otherwise we could do button customizations 289 | // with the same abstraction 290 | /* 291 | func (f *XForm) AddButton(label string, selected func()) *Form { 292 | return f.AddFormItem( 293 | tview.NewButton(label). 294 | SetSelectedFunc(selected), 295 | ) 296 | } 297 | */ 298 | --------------------------------------------------------------------------------