├── .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 |      [](https://goreportcard.com/report/github.com/Macmod/godap)  [
](https://twitter.com/MacmodSec)
4 |
5 |
A complete TUI for LDAP.
6 |
7 | 
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 |
--------------------------------------------------------------------------------