diff --git a/main.go b/main.go index a03020b..a38d36a 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,7 @@ import ( "github.com/Macmod/godap/v2/utils" "github.com/gdamore/tcell/v2" + "github.com/go-ldap/ldap/v3" "github.com/rivo/tview" "github.com/spf13/cobra" "h12.io/socks" @@ -33,8 +34,10 @@ var ( formatAttrs bool expandAttrs bool cacheEntries bool + deleted bool loadSchema bool pagingSize uint32 + timeout int32 insecure bool ldaps bool searchFilter string @@ -54,12 +57,13 @@ var ( rootNode *tview.TreeNode logPanel *tview.TextView - formatFlagPanel *tview.TextView - emojiFlagPanel *tview.TextView - colorFlagPanel *tview.TextView - expandFlagPanel *tview.TextView - tlsPanel *tview.TextView - statusPanel *tview.TextView + statusPanel *tview.TextView + tlsPanel *tview.TextView + formatFlagPanel *tview.TextView + emojiFlagPanel *tview.TextView + colorFlagPanel *tview.TextView + expandFlagPanel *tview.TextView + deletedFlagPanel *tview.TextView ) var attrLimit int @@ -132,6 +136,135 @@ func appKeyHandler(event *tcell.EventKey) *tcell.EventKey { return event } +func toggleFlagF() { + formatAttrs = !formatAttrs + updateStateBox(formatFlagPanel, formatAttrs) + + nodeExplorer := treePanel.GetCurrentNode() + if nodeExplorer != nil { + reloadExplorerAttrsPanel(nodeExplorer, cacheEntries) + } + + nodeSearch := searchTreePanel.GetCurrentNode() + if nodeSearch != nil { + reloadSearchAttrsPanel(nodeSearch, cacheEntries) + } +} + +func toggleFlagE() { + emojis = !emojis + updateStateBox(emojiFlagPanel, emojis) + updateEmojis() +} + +func toggleFlagC() { + colors = !colors + updateStateBox(colorFlagPanel, colors) + + nodeExplorer := treePanel.GetCurrentNode() + if nodeExplorer != nil { + reloadExplorerAttrsPanel(nodeExplorer, cacheEntries) + } + + nodeSearch := searchTreePanel.GetCurrentNode() + if nodeSearch != nil { + reloadSearchAttrsPanel(nodeSearch, cacheEntries) + } +} + +func toggleFlagA() { + expandAttrs = !expandAttrs + updateStateBox(expandFlagPanel, expandAttrs) + nodeExplorer := treePanel.GetCurrentNode() + if nodeExplorer != nil { + reloadExplorerAttrsPanel(nodeExplorer, cacheEntries) + } + + nodeSearch := searchTreePanel.GetCurrentNode() + if nodeSearch != nil { + reloadSearchAttrsPanel(nodeSearch, cacheEntries) + } +} + +func toggleFlagD() { + deleted = !deleted + updateStateBox(deletedFlagPanel, deleted) +} + +func toggleHeader() { + showHeader = !showHeader + if showHeader { + appPanel.RemoveItem(headerPanel) + } else { + appPanel.RemoveItem(pages) + appPanel.AddItem(headerPanel, 3, 0, false) + appPanel.AddItem(pages, 0, 8, false) + } +} + +func upgradeStartTLS() { + // TODO: Check possible race conditions + go func() { + err = lc.UpgradeToTLS(tlsConfig) + if err != nil { + updateLog(fmt.Sprint(err), "red") + } else { + updateLog("StartTLS request successful", "green") + updateStateBox(tlsPanel, true) + } + + updateStateBox(statusPanel, err == nil) + }() +} + +func reconnectLdap() { + // TODO: Check possible race conditions + go setupLDAPConn() +} + +func openConfigForm() { + credsForm := NewXForm() + credsForm. + AddInputField("Server", ldapServer, 20, nil, nil). + AddInputField("Port", strconv.Itoa(ldapPort), 20, nil, nil). + AddInputField("Username", ldapUsername, 20, nil, nil). + AddPasswordField("Password", ldapPassword, 20, '*', nil). + AddCheckbox("LDAPS", ldaps, nil). + AddCheckbox("IgnoreCert", insecure, nil). + AddInputField("SOCKSProxy", socksServer, 20, nil, nil). + AddButton("Go Back", func() { + app.SetRoot(appPanel, false).SetFocus(treePanel) + }). + AddButton("Update", func() { + ldapServer = credsForm.GetFormItemByLabel("Server").(*tview.InputField).GetText() + ldapPort, _ = strconv.Atoi(credsForm.GetFormItemByLabel("Port").(*tview.InputField).GetText()) + ldapUsername = credsForm.GetFormItemByLabel("Username").(*tview.InputField).GetText() + ldapPassword = credsForm.GetFormItemByLabel("Password").(*tview.InputField).GetText() + + ldaps = credsForm.GetFormItemByLabel("LDAPS").(*tview.Checkbox).IsChecked() + insecure = credsForm.GetFormItemByLabel("IgnoreCert").(*tview.Checkbox).IsChecked() + + socksServer = credsForm.GetFormItemByLabel("SOCKSProxy").(*tview.InputField).GetText() + + app.SetRoot(appPanel, false).SetFocus(treePanel) + }) + + credsForm.SetTitle("Connection Config").SetBorder(true) + credsForm. + SetButtonBackgroundColor(formButtonBackgroundColor). + SetButtonTextColor(formButtonTextColor). + SetButtonActivatedStyle(formButtonActivatedStyle) + credsForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + if event.Key() == tcell.KeyEscape { + app.SetRoot(appPanel, true).SetFocus(appPanel) + return nil + } + return event + }) + + app.SetRoot(credsForm, true).SetFocus(credsForm) +} + func appPanelKeyHandler(event *tcell.EventKey) *tcell.EventKey { _, isTextArea := app.GetFocus().(*tview.TextArea) _, isInputField := app.GetFocus().(*tview.InputField) @@ -142,104 +275,34 @@ func appPanelKeyHandler(event *tcell.EventKey) *tcell.EventKey { switch event.Rune() { case 'f', 'F': - formatAttrs = !formatAttrs - updateStateBox(formatFlagPanel, formatAttrs) - - node := treePanel.GetCurrentNode() - if node != nil { - err = reloadAttributesPanel(node, cacheEntries) - } + toggleFlagF() case 'e', 'E': - emojis = !emojis - emojiFlagPanel.SetText("Emojis: " + strconv.FormatBool(emojis)) - updateStateBox(emojiFlagPanel, emojis) - updateEmojis() + toggleFlagE() case 'c', 'C': - colors = !colors - updateStateBox(colorFlagPanel, colors) - node := treePanel.GetCurrentNode() - if node != nil { - reloadAttributesPanel(node, cacheEntries) - } + toggleFlagC() case 'a', 'A': - expandAttrs = !expandAttrs - updateStateBox(expandFlagPanel, expandAttrs) - node := treePanel.GetCurrentNode() - if node != nil { - reloadAttributesPanel(node, cacheEntries) - } + toggleFlagA() case 'h', 'H': - showHeader = !showHeader - if showHeader { - appPanel.RemoveItem(headerPanel) - } else { - appPanel.RemoveItem(pages) - appPanel.AddItem(headerPanel, 3, 0, false) - appPanel.AddItem(pages, 0, 8, false) - } + toggleHeader() + case 'd', 'D': + toggleFlagD() case 'l', 'L': - credsForm := tview.NewForm() - credsForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - if event.Key() == tcell.KeyEscape { - app.SetRoot(appPanel, true).SetFocus(appPanel) - return nil - } - return event - }) - - credsForm = credsForm. - AddInputField("Server", ldapServer, 20, nil, nil). - AddInputField("Port", strconv.Itoa(ldapPort), 20, nil, nil). - AddInputField("Username", ldapUsername, 20, nil, nil). - AddPasswordField("Password", ldapPassword, 20, '*', nil). - AddCheckbox("LDAPS", ldaps, nil). - AddCheckbox("IgnoreCert", insecure, nil). - AddInputField("SOCKSProxy", socksServer, 20, nil, nil). - SetFieldBackgroundColor(tcell.GetColor("black")). - AddButton("Go Back", func() { - app.SetRoot(appPanel, false).SetFocus(treePanel) - }). - AddButton("Update", func() { - ldapServer = credsForm.GetFormItemByLabel("Server").(*tview.InputField).GetText() - ldapPort, _ = strconv.Atoi(credsForm.GetFormItemByLabel("Port").(*tview.InputField).GetText()) - ldapUsername = credsForm.GetFormItemByLabel("Username").(*tview.InputField).GetText() - ldapPassword = credsForm.GetFormItemByLabel("Password").(*tview.InputField).GetText() - - ldaps = credsForm.GetFormItemByLabel("LDAPS").(*tview.Checkbox).IsChecked() - insecure = credsForm.GetFormItemByLabel("IgnoreCert").(*tview.Checkbox).IsChecked() - - socksServer = credsForm.GetFormItemByLabel("SOCKSProxy").(*tview.InputField).GetText() - - app.SetRoot(appPanel, false).SetFocus(treePanel) - }) - - credsForm.SetTitle("Connection Config").SetBorder(true) - app.SetRoot(credsForm, true).SetFocus(credsForm) + openConfigForm() } switch event.Key() { case tcell.KeyCtrlU: - // TODO: Check possible race conditions - go func() { - err = lc.UpgradeToTLS(tlsConfig) - if err != nil { - updateLog(fmt.Sprint(err), "red") - } else { - updateLog("StartTLS request successful", "green") - updateStateBox(tlsPanel, true) - } - - updateStateBox(statusPanel, err == nil) - }() + upgradeStartTLS() case tcell.KeyCtrlR: - // TODO: Check possible race conditions - go setupLDAPConn() + reconnectLdap() } return event } func setupLDAPConn() error { + updateLog("Connecting to LDAP server...", "yellow") + if lc != nil && lc.Conn != nil { lc.Conn.Close() } @@ -261,9 +324,11 @@ func setupLDAPConn() error { } } + ldap.DefaultTimeout = time.Duration(timeout) * time.Second + lc, err = utils.NewLDAPConn( ldapServer, ldapPort, - ldaps, tlsConfig, pagingSize, + ldaps, tlsConfig, pagingSize, rootDN, proxyConn, ) @@ -302,28 +367,46 @@ func setupApp() { logPanel.SetTextAlign(tview.AlignCenter).SetBorder(true) tlsPanel = tview.NewTextView() - tlsPanel.SetTitle("TLS") - tlsPanel.SetTextAlign(tview.AlignCenter).SetBorder(true) + tlsPanel. + SetTextAlign(tview.AlignCenter). + SetTitle("TLS (C-u)"). + SetBorder(true) statusPanel = tview.NewTextView() - statusPanel.SetTitle("Conn") - statusPanel.SetTextAlign(tview.AlignCenter).SetBorder(true) + statusPanel. + SetTextAlign(tview.AlignCenter). + SetTitle("Conn (C-r)"). + SetBorder(true) formatFlagPanel = tview.NewTextView() - formatFlagPanel.SetTitle("Format") - formatFlagPanel.SetTextAlign(tview.AlignCenter).SetBorder(true) + formatFlagPanel. + SetTextAlign(tview.AlignCenter). + SetTitle("Format (f)"). + SetBorder(true) emojiFlagPanel = tview.NewTextView() - emojiFlagPanel.SetTitle("Emoji") - emojiFlagPanel.SetTextAlign(tview.AlignCenter).SetBorder(true) + emojiFlagPanel. + SetTextAlign(tview.AlignCenter). + SetTitle("Emoji (e)"). + SetBorder(true) colorFlagPanel = tview.NewTextView() - colorFlagPanel.SetTitle("Colors") - colorFlagPanel.SetTextAlign(tview.AlignCenter).SetBorder(true) + colorFlagPanel. + SetTextAlign(tview.AlignCenter). + SetTitle("Colors (c)"). + SetBorder(true) expandFlagPanel = tview.NewTextView() - expandFlagPanel.SetTitle("Expand") - expandFlagPanel.SetTextAlign(tview.AlignCenter).SetBorder(true) + expandFlagPanel. + SetTextAlign(tview.AlignCenter). + SetTitle("Expand (a)"). + SetBorder(true) + + deletedFlagPanel = tview.NewTextView() + deletedFlagPanel. + SetTextAlign(tview.AlignCenter). + SetTitle("Deleted (d)"). + SetBorder(true) err := setupLDAPConn() if err != nil { @@ -384,7 +467,8 @@ func setupApp() { AddItem(formatFlagPanel, 0, 1, false). AddItem(colorFlagPanel, 0, 1, false). AddItem(expandFlagPanel, 0, 1, false). - AddItem(emojiFlagPanel, 0, 1, false) + AddItem(emojiFlagPanel, 0, 1, false). + AddItem(deletedFlagPanel, 0, 1, false) appPanel = tview.NewFlex().SetDirection(tview.FlexRow). AddItem(info, 1, 1, false). @@ -402,6 +486,7 @@ func setupApp() { updateStateBox(colorFlagPanel, colors) updateStateBox(emojiFlagPanel, emojis) updateStateBox(expandFlagPanel, expandAttrs) + updateStateBox(deletedFlagPanel, deleted) if err := app.SetRoot(appPanel, true).SetFocus(treePanel).Run(); err != nil { log.Fatal(err) @@ -454,6 +539,8 @@ func main() { rootCmd.Flags().BoolVarP(&expandAttrs, "expand", "A", true, "Expand multi-value attributes") rootCmd.Flags().IntVarP(&attrLimit, "limit", "L", 20, "Number of attribute values to render for multi-value attributes when -expand is set true") rootCmd.Flags().BoolVarP(&cacheEntries, "cache", "M", true, "Keep loaded entries in memory while the program is open and don't query them again") + rootCmd.Flags().BoolVarP(&deleted, "deleted", "D", false, "Include deleted objects in all queries performed") + rootCmd.Flags().Int32VarP(&timeout, "timeout", "T", 10, "Timeout for LDAP connections in seconds") rootCmd.Flags().BoolVarP(&loadSchema, "schema", "k", false, "Load schema GUIDs from the LDAP server during initialization") rootCmd.Flags().Uint32VarP(&pagingSize, "paging", "G", 800, "Default paging size for regular queries") rootCmd.Flags().BoolVarP(&insecure, "insecure", "I", false, "Skip TLS verification for LDAPS/StartTLS") diff --git a/search.go b/search.go index 58b3b2e..a5d1873 100644 --- a/search.go +++ b/search.go @@ -1,7 +1,7 @@ package main import ( - "sort" + "fmt" "strconv" "strings" "sync" @@ -14,61 +14,138 @@ import ( ) var ( - searchTreePanel *tview.TreeView - searchQueryPanel *tview.InputField - searchLibraryPanel *tview.List - treeFlex *tview.Flex + searchTreePanel *tview.TreeView + searchQueryPanel *tview.InputField + searchAttrsPanel *tview.Table + + searchLibraryPanel *tview.TreeView + sidePanel *tview.Pages searchPage *tview.Flex runControl sync.Mutex running bool + + searchCache EntryCache ) var searchLoadedDNs map[string]*tview.TreeNode = make(map[string]*tview.TreeNode) +func reloadSearchAttrsPanel(node *tview.TreeNode, useCache bool) { + reloadAttributesPanel(node, searchAttrsPanel, useCache, &searchCache) +} + func initSearchPage() { - searchQueryPanel = tview.NewInputField(). - SetFieldBackgroundColor(tcell.GetColor("black")) - searchQueryPanel.SetTitle("Search Filter (Recursive)").SetBorder(true) + searchCache = EntryCache{ + entries: make(map[string]*ldap.Entry), + } - searchLibraryPanel = tview.NewList() - searchLibraryPanel.SetTitle("Search Library").SetBorder(true) - searchLibraryPanel.SetCurrentItem(0) + searchQueryPanel = tview.NewInputField() + searchQueryPanel. + SetPlaceholder("Type an LDAP search filter"). + SetPlaceholderStyle(placeholderStyle). + SetPlaceholderTextColor(placeholderTextColor). + SetFieldBackgroundColor(fieldBackgroundColor). + SetTitle("Search Filter (Recursive)"). + SetBorder(true) + + tabs := tview.NewTextView(). + SetTextAlign(tview.AlignCenter). + SetWrap(false). + SetRegions(true). + SetDynamicColors(true) + tabs.SetBackgroundColor(tcell.ColorBlack) + tabs.SetBorder(true) - predefinedLdapQueriesKeys := make([]string, 0) - for k, _ := range utils.PredefinedLdapQueries { - predefinedLdapQueriesKeys = append(predefinedLdapQueriesKeys, k) - } - sort.Strings(predefinedLdapQueriesKeys) + searchTreePanel = tview.NewTreeView() + searchTreePanel. + SetTitle("Search Results"). + SetBorder(true) + + searchTreePanel.SetChangedFunc(func(node *tview.TreeNode) { + searchAttrsPanel.Clear() + reloadSearchAttrsPanel(node, true) + }) + + searchAttrsPanel = tview.NewTable(). + SetSelectable(true, true). + SetEvaluateAllRows(true) + searchAttrsPanel.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + currentNode := searchTreePanel.GetCurrentNode() + if currentNode == nil || currentNode.GetReference() == nil { + return event + } + + return attrsPanelKeyHandler(event, currentNode, &searchCache, searchAttrsPanel) + }) + + searchLibraryPanel = tview.NewTreeView() + + searchLibraryRoot := tview.NewTreeNode("Queries").SetSelectable(false) + searchLibraryPanel.SetRoot(searchLibraryRoot) + + sidePanel = tview.NewPages(). + AddPage("page-0", searchLibraryPanel, true, true). + AddPage("page-1", searchAttrsPanel, true, false) + + sidePanel.SetBorder(true) + + predefinedLdapQueriesKeys := []string{"Security", "Users", "Computers", "Enum"} for _, key := range predefinedLdapQueriesKeys { - query := utils.PredefinedLdapQueries[key] - searchLibraryPanel.AddItem(key, query, 'o', nil) + children := utils.PredefinedLdapQueries[key] + + childNode := tview.NewTreeNode(key). + SetSelectable(false). + SetExpanded(true) + + for _, val := range children { + childNode.AddChild( + tview.NewTreeNode(val.Title). + SetReference(val.Filter). + SetSelectable(true)) + } + + searchLibraryRoot.AddChild(childNode) } - searchLibraryPanel.SetSelectedFunc(func(idx int, key string, query string, ch rune) { - runControl.Lock() - if running { + + searchLibraryPanel.SetSelectedFunc( + func(node *tview.TreeNode) { + runControl.Lock() + if running { + runControl.Unlock() + updateLog("Another query is still running...", "yellow") + return + } runControl.Unlock() - updateLog("Another query is still running...", "yellow") - return - } - runControl.Unlock() - nowTimestamp := strconv.FormatInt(time.Now().UnixNano(), 10) - editedQuery := strings.Replace( - strings.Replace( - query, "DC=domain,DC=com", lc.RootDN, -1, - ), - "", nowTimestamp, -1, - ) + searchQueryDoneHandler(tcell.KeyEnter) + }, + ) - searchQueryPanel.SetText(editedQuery) - searchQueryDoneHandler(tcell.KeyEnter) - }) + searchLibraryPanel.SetChangedFunc( + func(node *tview.TreeNode) { + ref := node.GetReference() + if ref == nil { + searchQueryPanel.SetText("") + return + } + + nowTimestamp := time.Now().UnixNano() + + nowTimestampStr := strconv.FormatInt(nowTimestamp, 10) + lastDayTimestampStr := strconv.FormatInt(nowTimestamp-86400, 10) + lastMonthTimestampStr := strconv.FormatInt(nowTimestamp-2592000, 10) + + editedQuery := strings.Replace(ref.(string), "DC=domain,DC=com", lc.RootDN, -1) + editedQuery = strings.Replace(editedQuery, "", nowTimestampStr, -1) + editedQuery = strings.Replace(editedQuery, "", lastDayTimestampStr, -1) + editedQuery = strings.Replace(editedQuery, "", lastMonthTimestampStr, -1) + + searchQueryPanel.SetText(editedQuery) + }, + ) searchQueryPanel.SetDoneFunc(searchQueryDoneHandler) - searchTreePanel = tview.NewTreeView() - searchTreePanel.SetTitle("Search Results").SetBorder(true) searchTreePanel.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { currentNode := searchTreePanel.GetCurrentNode() if currentNode == nil { @@ -77,24 +154,52 @@ func initSearchPage() { switch event.Key() { case tcell.KeyRight: - currentNode.SetExpanded(true) + if len(currentNode.GetChildren()) != 0 && !currentNode.IsExpanded() { + currentNode.SetExpanded(true) + } + return nil case tcell.KeyLeft: - currentNode.SetExpanded(false) + if currentNode.IsExpanded() { // Collapse current node + currentNode.SetExpanded(false) + searchTreePanel.SetCurrentNode(currentNode) + } else { // Collapse parent node + pathToCurrent := searchTreePanel.GetPath(currentNode) + if len(pathToCurrent) > 1 { + parentNode := pathToCurrent[len(pathToCurrent)-2] + parentNode.SetExpanded(false) + searchTreePanel.SetCurrentNode(parentNode) + } + } + return nil } return event }) + fmt.Fprintf(tabs, `["%s"][white]%s[black][""] `, "0", "Library") + fmt.Fprintf(tabs, `["%s"][white]%s[black][""]`, "1", "Attributes") + + tabs.SetHighlightedFunc(func(added, removed, remaining []string) { + if len(added) > 0 { + sidePanel.SwitchToPage("page-" + added[0]) + } else { + tabs.Highlight("0") + } + }) + + tabs.Highlight("0") + searchPage = tview.NewFlex().SetDirection(tview.FlexRow). AddItem( tview.NewFlex(). - AddItem(searchQueryPanel, 0, 1, false), + AddItem(searchQueryPanel, 0, 1, false). + AddItem(tabs, 20, 0, false), 3, 0, false, ). AddItem( tview.NewFlex(). - AddItem(searchTreePanel, 0, 2, false). - AddItem(searchLibraryPanel, 0, 1, false), + AddItem(searchTreePanel, 0, 1, false). + AddItem(sidePanel, 0, 1, false), 0, 8, false, ) @@ -104,9 +209,12 @@ func initSearchPage() { func searchQueryDoneHandler(key tcell.Key) { updateLog("Performing recursive query...", "yellow") - rootNode := tview.NewTreeNode(lc.RootDN).SetSelectable(false) - searchTreePanel.SetRoot(rootNode).SetCurrentNode(rootNode) + rootNode := tview.NewTreeNode(lc.RootDN).SetSelectable(true) + searchTreePanel. + SetRoot(rootNode). + SetCurrentNode(rootNode) + searchCache.Clear() clear(searchLoadedDNs) searchQuery := searchQueryPanel.GetText() @@ -120,7 +228,9 @@ func searchQueryDoneHandler(key tcell.Key) { running = true runControl.Unlock() - entries, _ := lc.Query(lc.RootDN, searchQuery, ldap.ScopeWholeSubtree) + entries, _ := lc.Query(lc.RootDN, searchQuery, ldap.ScopeWholeSubtree, deleted) + + firstLeaf := true for _, entry := range entries { if entry.DN == lc.RootDN { @@ -135,23 +245,34 @@ func searchQueryDoneHandler(key tcell.Key) { currentNode := searchTreePanel.GetRoot() for i := len(components) - 1; i >= 0; i-- { - //attribute := strings.Split(components[i], "=")[0] - //value := strings.Split(components[i], "=")[1] - partialDN := strings.Join(components[i:], ",") childNode, ok := searchLoadedDNs[partialDN] if !ok { if i == 0 { + // Leaf node nodeName = entryName + childNode = tview.NewTreeNode(nodeName). + SetReference(entry.DN). + SetExpanded(false). + SetSelectable(true) + currentNode.AddChild(childNode) + + if firstLeaf { + searchTreePanel.SetCurrentNode(childNode) + firstLeaf = false + } + + searchCache.Add(entry.DN, entry) } else { + // Non-leaf node nodeName = components[i] + childNode = tview.NewTreeNode(nodeName). + SetExpanded(true). + SetSelectable(true) + currentNode.AddChild(childNode) } - childNode = tview.NewTreeNode(nodeName). - SetSelectable(true). - SetExpanded(true) - currentNode.AddChild(childNode) app.Draw() searchLoadedDNs[partialDN] = childNode @@ -162,6 +283,7 @@ func searchQueryDoneHandler(key tcell.Key) { } updateLog("Query completed ("+strconv.Itoa(len(entries))+" objects found)", "green") + app.Draw() runControl.Lock() @@ -186,8 +308,8 @@ func searchRotateFocus() { case searchTreePanel: app.SetFocus(searchQueryPanel) case searchQueryPanel: - app.SetFocus(searchLibraryPanel) - case searchLibraryPanel: + app.SetFocus(sidePanel) + case searchLibraryPanel, searchAttrsPanel: app.SetFocus(searchTreePanel) } } diff --git a/tree.go b/tree.go index 3248a47..5a763f1 100644 --- a/tree.go +++ b/tree.go @@ -2,7 +2,9 @@ package main import ( "bytes" + "encoding/hex" "fmt" + "regexp" "sort" "strconv" "strings" @@ -14,7 +16,7 @@ import ( ) func createTreeNodeFromEntry(entry *ldap.Entry) *tview.TreeNode { - _, ok := cache.Get(entry.DN) + _, ok := explorerCache.Get(entry.DN) if !ok { nodeName := getNodeName(entry) @@ -23,14 +25,28 @@ func createTreeNodeFromEntry(entry *ldap.Entry) *tview.TreeNode { SetReference(entry.DN). SetSelectable(true) - uac := entry.GetAttributeValue("userAccountControl") - uacNum, err := strconv.Atoi(uac) + // Helpful node coloring for deleted and disabled objects + if colors { + isDeleted := strings.ToLower(entry.GetAttributeValue("isDeleted")) == "true" + isRecycled := strings.ToLower(entry.GetAttributeValue("isRecycled")) == "true" - if err == nil && colors && uacNum&2 != 0 { - node.SetColor(tcell.GetColor("red")) + if isDeleted { + if isRecycled { + node.SetColor(tcell.GetColor("red")) + } else { + node.SetColor(tcell.GetColor("gray")) + } + } else { + uac := entry.GetAttributeValue("userAccountControl") + uacNum, err := strconv.Atoi(uac) + + if err == nil && uacNum&2 != 0 { + node.SetColor(tcell.GetColor("yellow")) + } + } } - cache.Add(entry.DN, entry) + explorerCache.Add(entry.DN, entry) node.SetExpanded(false) return node @@ -51,7 +67,7 @@ func unloadChildren(parentNode *tview.TreeNode) { for _, child := range children { childDN := child.GetReference().(string) - cache.Delete(childDN) + explorerCache.Delete(childDN) parentNode.RemoveChild(child) } } @@ -59,7 +75,7 @@ func unloadChildren(parentNode *tview.TreeNode) { // Loads child nodes and their attributes directly from LDAP func loadChildren(node *tview.TreeNode) { baseDN := node.GetReference().(string) - entries, err := lc.Query(baseDN, searchFilter, ldap.ScopeSingleLevel) + entries, err := lc.Query(baseDN, searchFilter, ldap.ScopeSingleLevel, deleted) if err != nil { updateLog(fmt.Sprint(err), "red") return @@ -79,7 +95,254 @@ func loadChildren(node *tview.TreeNode) { } } -func reloadAttributesPanel(node *tview.TreeNode, useCache bool) error { +func handleAttrsKeyCtrlE(currentNode *tview.TreeNode, attrsPanel *tview.Table, cache *EntryCache) { + currentFocus := app.GetFocus() + attrRow, _ := attrsPanel.GetSelection() + attrName := attrsPanel.GetCell(attrRow, 0).Text + + baseDN := currentNode.GetReference().(string) + + entry, _ := cache.Get(baseDN) + attrVals := entry.GetAttributeValues(attrName) + if len(attrVals) == 0 { + return + } + + // Encode attribute values to hex + rawAttrVals := entry.GetRawAttributeValues(attrName) + + var attrValsHex []string + for idx := range rawAttrVals { + hexEncoded := hex.EncodeToString(rawAttrVals[idx]) + attrValsHex = append(attrValsHex, hexEncoded) + } + + valIndices := []string{} + for idx := range attrVals { + valIndices = append(valIndices, strconv.Itoa(idx)) + } + valIndices = append(valIndices, "New") + + selectedIndex := 0 + + useHexEncoding := false + + writeAttrValsForm := NewXForm() + writeAttrValsForm. + AddTextView("Base DN", baseDN, 0, 1, false, true). + AddTextView("Attribute Name", attrName, 0, 1, false, true). + AddTextView("Current Value", attrVals[0], 0, 1, false, true). + AddTextView("Current Value (HEX)", attrValsHex[0], 0, 1, false, true). + AddDropDown("Value Index", valIndices, 0, func(option string, optionIndex int) { + selectedIndex = optionIndex + + currentValItem := writeAttrValsForm.GetFormItemByLabel("Current Value").(*tview.TextView) + currentValItemHex := writeAttrValsForm.GetFormItemByLabel("Current Value (HEX)").(*tview.TextView) + + if selectedIndex < len(attrVals) { + currentValItem.SetText(attrVals[selectedIndex]) + currentValItemHex.SetText(attrValsHex[selectedIndex]) + } else { + currentValItem.SetText("") + currentValItemHex.SetText("") + } + }). + AddInputField("New Value", "", 0, nil, nil). + AddCheckbox("Use HEX encoding", false, func(checked bool) { + useHexEncoding = checked + }). + AddButton("Go Back", func() { + app.SetRoot(appPanel, false).SetFocus(currentFocus) + }). + AddButton("Update", func() { + newValue := writeAttrValsForm.GetFormItemByLabel("New Value").(*tview.InputField).GetText() + if useHexEncoding { + newValueBytes, err := hex.DecodeString(newValue) + if err == nil { + newValue = string(newValueBytes) + } else { + updateLog(fmt.Sprint(err), "red") + return + } + } + + if selectedIndex < len(attrVals) { + attrVals[selectedIndex] = newValue + } else { + attrVals = append(attrVals, newValue) + } + + err := lc.ModifyAttribute(baseDN, attrName, attrVals) + // TODO: Don't go back immediately so that the user can + // change multiple values at once + if err != nil { + updateLog(fmt.Sprint(err), "red") + } else { + updateLog("Attribute updated: '"+attrName+"' from '"+baseDN+"'", "green") + } + + reloadAttributesPanel(currentNode, attrsPanel, false, cache) + + /* + if parentNode != nil { + idx := findEntryInChildren(baseDN, parentNode) + + parent := reloadParentNode(currentNode) + siblings := parent.GetChildren() + + tree.SetCurrentNode(siblings[idx]) + } else { + // Update UI in this edge case + } + */ + + app.SetRoot(appPanel, false).SetFocus(currentFocus) + }) + + writeAttrValsForm. + SetButtonBackgroundColor(formButtonBackgroundColor). + SetButtonTextColor(formButtonTextColor). + SetButtonActivatedStyle(formButtonActivatedStyle) + writeAttrValsForm.SetInputCapture(handleEscapeToTree) + writeAttrValsForm.SetTitle("Attribute Editor").SetBorder(true) + app.SetRoot(writeAttrValsForm, true).SetFocus(writeAttrValsForm) +} + +func handleAttrsKeyDelete(currentNode *tview.TreeNode, attrsPanel *tview.Table, cache *EntryCache) { + currentFocus := app.GetFocus() + baseDN := currentNode.GetReference().(string) + + attrRow, _ := attrsPanel.GetSelection() + attrName := attrsPanel.GetCell(attrRow, 0).Text + + promptModal := tview.NewModal(). + SetText("Do you really want to delete attribute `" + attrName + "` of this object?\n" + baseDN). + AddButtons([]string{"No", "Yes"}). + SetDoneFunc(func(buttonIndex int, buttonLabel string) { + if buttonLabel == "Yes" { + err := lc.DeleteAttribute(baseDN, attrName) + if err != nil { + updateLog(fmt.Sprint(err), "red") + } else { + cache.Delete(baseDN) + reloadAttributesPanel(currentNode, attrsPanel, false, cache) + + updateLog("Attribute deleted: "+attrName+" from "+baseDN, "green") + } + } + + app.SetRoot(appPanel, true).SetFocus(currentFocus) + }) + + app.SetRoot(promptModal, false).SetFocus(promptModal) +} + +func handleAttrsKeyCtrlN(currentNode *tview.TreeNode, attrsPanel *tview.Table, cache *EntryCache) { + currentFocus := app.GetFocus() + createAttrForm := NewXForm(). + SetButtonBackgroundColor(formButtonBackgroundColor). + SetButtonTextColor(formButtonTextColor). + SetButtonActivatedStyle(formButtonActivatedStyle) + createAttrForm.SetInputCapture(handleEscapeToTree) + + baseDN := currentNode.GetReference().(string) + + createAttrForm. + AddTextView("Object DN", baseDN, 0, 1, false, true). + AddInputField("Attribute Name", "", 20, nil, nil). + AddInputField("Attribute Value", "", 20, nil, nil). + AddButton("Go Back", func() { + app.SetRoot(appPanel, false).SetFocus(currentFocus) + }). + AddButton("Create", func() { + attrName := createAttrForm.GetFormItemByLabel("Attribute Name").(*tview.InputField).GetText() + attrVal := createAttrForm.GetFormItemByLabel("Attribute Value").(*tview.InputField).GetText() + + err := lc.AddAttribute(baseDN, attrName, []string{attrVal}) + if err != nil { + updateLog(fmt.Sprint(err), "red") + } else { + cache.Delete(baseDN) + reloadAttributesPanel(currentNode, attrsPanel, false, cache) + + updateLog("Attribute added: "+attrName+" to "+baseDN, "green") + } + + app.SetRoot(appPanel, false).SetFocus(currentFocus) + }). + SetTitle("Attribute Creator"). + SetBorder(true) + + app.SetRoot(createAttrForm, true).SetFocus(createAttrForm) +} + +func handleAttrsKeyDown(attrsPanel *tview.Table) { + selectedRow, selectedCol := attrsPanel.GetSelection() + rowCount := attrsPanel.GetRowCount() + + if selectedCol == 0 { + s := selectedRow + 1 + for s < rowCount && attrsPanel.GetCell(s, 0).Text == "" { + s = s + 1 + } + + if s == rowCount { + attrsPanel.Select(selectedRow-1, 0) + } else if s != selectedRow { + attrsPanel.Select(s-1, 0) + } + } +} + +func handleAttrsKeyUp(attrsPanel *tview.Table) { + selectedRow, selectedCol := attrsPanel.GetSelection() + if selectedCol == 0 { + s := selectedRow - 1 + for s > 0 && attrsPanel.GetCell(s, 0).Text == "" { + s = s - 1 + } + + if s != selectedRow { + attrsPanel.Select(s+1, 0) + } + } +} + +func attrsPanelKeyHandler(event *tcell.EventKey, currentNode *tview.TreeNode, cache *EntryCache, attrsPanel *tview.Table) *tcell.EventKey { + switch event.Rune() { + case 'r', 'R': + baseDN := currentNode.GetReference().(string) + + updateLog("Reloading node "+baseDN, "yellow") + + cache.Delete(baseDN) + reloadAttributesPanel(currentNode, attrsPanel, false, cache) + + updateLog("Node "+baseDN+" reloaded", "green") + + go func() { + app.Draw() + }() + return event + } + + switch event.Key() { + case tcell.KeyDelete: + handleAttrsKeyDelete(currentNode, attrsPanel, cache) + case tcell.KeyCtrlE: + handleAttrsKeyCtrlE(currentNode, attrsPanel, cache) + case tcell.KeyCtrlN: + handleAttrsKeyCtrlN(currentNode, attrsPanel, cache) + case tcell.KeyDown: + handleAttrsKeyDown(attrsPanel) + case tcell.KeyUp: + handleAttrsKeyUp(attrsPanel) + } + + return event +} + +func reloadAttributesPanel(node *tview.TreeNode, attrsTable *tview.Table, useCache bool, cache *EntryCache) error { ref := node.GetReference() if ref == nil { return fmt.Errorf("Couldn't reload attributes: no node selected") @@ -89,7 +352,7 @@ func reloadAttributesPanel(node *tview.TreeNode, useCache bool) error { baseDN := ref.(string) - attrsPanel.Clear() + attrsTable.Clear() if useCache { entry, ok := cache.Get(baseDN) @@ -99,7 +362,7 @@ func reloadAttributesPanel(node *tview.TreeNode, useCache bool) error { return fmt.Errorf("Couldn't reload attributes: node not cached") } } else { - entries, err := lc.Query(baseDN, searchFilter, ldap.ScopeBaseObject) + entries, err := lc.Query(baseDN, searchFilter, ldap.ScopeBaseObject, deleted) if err != nil { updateLog(fmt.Sprint(err), "red") return err @@ -121,7 +384,7 @@ func reloadAttributesPanel(node *tview.TreeNode, useCache bool) error { var cellValues []string - attrsPanel.SetCell(row, 0, tview.NewTableCell(cellName)) + attrsTable.SetCell(row, 0, tview.NewTableCell(cellName)) if formatAttrs { cellValues = utils.FormatLDAPAttribute(attribute) @@ -139,7 +402,7 @@ func reloadAttributesPanel(node *tview.TreeNode, useCache bool) error { } } - attrsPanel.SetCell(row, 1, myCell) + attrsTable.SetCell(row, 1, myCell) row = row + 1 continue } @@ -163,13 +426,14 @@ func reloadAttributesPanel(node *tview.TreeNode, useCache bool) error { } if idx == 0 { - attrsPanel.SetCell(row, 1, myCell) + attrsTable.SetCell(row, 1, myCell) } else { if expandAttrs { if attrLimit == -1 || idx < attrLimit { - attrsPanel.SetCell(row, 1, myCell) + attrsTable.SetCell(row, 0, tview.NewTableCell("")) + attrsTable.SetCell(row, 1, myCell) if idx == attrLimit-1 { - attrsPanel.SetCell(row+1, 1, tview.NewTableCell("[entries hidden]")) + attrsTable.SetCell(row+1, 1, tview.NewTableCell("[entries hidden]")) row = row + 2 break } @@ -181,7 +445,7 @@ func reloadAttributesPanel(node *tview.TreeNode, useCache bool) error { } } - attrsPanel.ScrollToBeginning() + attrsTable.ScrollToBeginning() go func() { app.Draw() }() @@ -239,12 +503,14 @@ func getNodeName(entry *ldap.Entry) string { emojisPrefix = classEmojisBuf.String() + entryMarker := regexp.MustCompile("DEL:[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$") + if len(emojisPrefix) == 0 { emojisPrefix = utils.EmojiMap["container"] } if emojis { - return emojisPrefix + getName(entry) + return emojisPrefix + entryMarker.ReplaceAllString(getName(entry), "") } dn := getDN(entry) @@ -261,24 +527,41 @@ func getNodeName(entry *ldap.Entry) string { } func updateEmojis() { - rootNode := treePanel.GetRoot() + rootExplorer := treePanel.GetRoot() + if rootExplorer != nil { + rootExplorer.Walk(func(node *tview.TreeNode, parent *tview.TreeNode) bool { + ref := node.GetReference() + if ref != nil { + entry, ok := explorerCache.Get(ref.(string)) + + if ok { + node.SetText(getNodeName(entry)) + } + } - rootNode.Walk(func(node *tview.TreeNode, parent *tview.TreeNode) bool { - ref := node.GetReference() - if ref != nil { - entry, ok := cache.Get(ref.(string)) + return true + }) + } + + rootSearch := searchTreePanel.GetRoot() + if rootSearch != nil { + rootSearch.Walk(func(node *tview.TreeNode, parent *tview.TreeNode) bool { + ref := node.GetReference() + if ref != nil { + entry, ok := searchCache.Get(ref.(string)) - if ok { - node.SetText(getNodeName(entry)) + if ok { + node.SetText(getNodeName(entry)) + } } - } - return true - }) + return true + }) + } } func renderPartialTree(rootDN string, searchFilter string) *tview.TreeNode { - rootEntry, err := lc.Query(rootDN, "(objectClass=*)", ldap.ScopeBaseObject) + rootEntry, err := lc.Query(rootDN, "(objectClass=*)", ldap.ScopeBaseObject, deleted) if err != nil { updateLog(fmt.Sprint(err), "red") return nil @@ -289,7 +572,7 @@ func renderPartialTree(rootDN string, searchFilter string) *tview.TreeNode { return nil } - cache.Add(rootDN, rootEntry[0]) + explorerCache.Add(rootDN, rootEntry[0]) rootNodeName := getNodeName(rootEntry[0]) if rootDN == "" { @@ -305,7 +588,7 @@ func renderPartialTree(rootDN string, searchFilter string) *tview.TreeNode { } var rootEntries []*ldap.Entry - rootEntries, err = lc.Query(rootDN, searchFilter, ldap.ScopeSingleLevel) + rootEntries, err = lc.Query(rootDN, searchFilter, ldap.ScopeSingleLevel, deleted) if err != nil { updateLog(fmt.Sprint(err), "red") return nil @@ -326,9 +609,9 @@ func renderPartialTree(rootDN string, searchFilter string) *tview.TreeNode { return rootNode } -func reloadPage() { - attrsPanel.Clear() - cache.Clear() +func reloadExplorerPage() { + explorerAttrsPanel.Clear() + explorerCache.Clear() rootNode = renderPartialTree(lc.RootDN, searchFilter) if rootNode != nil { diff --git a/utils/ldapactions.go b/utils/ldapactions.go index 3ae6511..d18d1bb 100644 --- a/utils/ldapactions.go +++ b/utils/ldapactions.go @@ -33,7 +33,7 @@ func (lc *LDAPConn) UpgradeToTLS(tlsConfig *tls.Config) error { return nil } -func NewLDAPConn(ldapServer string, ldapPort int, ldaps bool, tlsConfig *tls.Config, pagingSize uint32, proxyConn net.Conn) (*LDAPConn, error) { +func NewLDAPConn(ldapServer string, ldapPort int, ldaps bool, tlsConfig *tls.Config, pagingSize uint32, rootDN string, proxyConn net.Conn) (*LDAPConn, error) { var conn *ldap.Conn var err error = nil @@ -59,6 +59,7 @@ func NewLDAPConn(ldapServer string, ldapPort int, ldaps bool, tlsConfig *tls.Con return &LDAPConn{ Conn: conn, PagingSize: pagingSize, + RootDN: rootDN, }, nil } @@ -73,13 +74,18 @@ func (lc *LDAPConn) NTLMBindWithHash(ntlmDomain string, ntlmUsername string, ntl } // Search -func (lc *LDAPConn) Query(baseDN string, searchFilter string, scope int) ([]*ldap.Entry, error) { +func (lc *LDAPConn) Query(baseDN string, searchFilter string, scope int, showDeleted bool) ([]*ldap.Entry, error) { + var controls []ldap.Control = nil + if showDeleted { + controls = []ldap.Control{ldap.NewControlMicrosoftShowDeleted()} + } + searchRequest := ldap.NewSearchRequest( baseDN, scope, ldap.NeverDerefAliases, 0, 0, false, searchFilter, []string{}, - nil, + controls, ) sr, err := lc.Conn.SearchWithPaging(searchRequest, lc.PagingSize) @@ -189,7 +195,7 @@ func (lc *LDAPConn) QueryGroupMembers(groupName string) (group []*ldap.Entry, er return nil, fmt.Errorf("Group '%s' not found", groupName) } - groupDN = groupResult.Entries[0].GetAttributeValue("distinguishedName") + groupDN = groupResult.Entries[0].DN } ldapQuery := fmt.Sprintf("(memberOf=%s)", groupDN) @@ -208,7 +214,7 @@ func (lc *LDAPConn) QueryGroupMembers(groupName string) (group []*ldap.Entry, er } if len(result.Entries) == 0 { - return nil, fmt.Errorf("Group '%s' not found", groupName) + return nil, fmt.Errorf("Group '%s' has 0 members", groupName) } return result.Entries, nil @@ -641,7 +647,7 @@ func (lc *LDAPConn) FindPrimaryGroupForSID(SID string) (groupSID string, err err func (lc *LDAPConn) FindSchemaControlAccessRights(filter string) (map[string]string, error) { extendedRights := make(map[string]string) - rootDSE, err := lc.Query("", "(objectClass=*)", ldap.ScopeBaseObject) + rootDSE, err := lc.Query("", "(objectClass=*)", ldap.ScopeBaseObject, false) if err != nil { return nil, err } @@ -652,6 +658,7 @@ func (lc *LDAPConn) FindSchemaControlAccessRights(filter string) (map[string]str "CN=Extended-Rights,"+configurationDN, filter, ldap.ScopeSingleLevel, + false, ) if err != nil { @@ -674,7 +681,7 @@ func (lc *LDAPConn) FindSchemaClassesAndAttributes() (map[string]string, map[str classesGuids := make(map[string]string) attrsGuids := make(map[string]string) - rootDSE, err := lc.Query("", "(objectClass=*)", ldap.ScopeBaseObject) + rootDSE, err := lc.Query("", "(objectClass=*)", ldap.ScopeBaseObject, false) if err != nil { return nil, nil, err } @@ -685,6 +692,7 @@ func (lc *LDAPConn) FindSchemaClassesAndAttributes() (map[string]string, map[str schemaDN, "(|(objectClass=attributeSchema)(objectClass=classSchema))", ldap.ScopeSingleLevel, + false, ) if err != nil { diff --git a/utils/ldapcolors.go b/utils/ldapcolors.go index d0554c5..f54e015 100644 --- a/utils/ldapcolors.go +++ b/utils/ldapcolors.go @@ -46,8 +46,10 @@ func GetAttrCellColor(cellName string, cellValue string) (string, bool) { switch cellValue { case "TRUE", "Enabled", "Normal", "PwdNotExpired": color = "green" - case "FALSE", "Disabled", "NotNormal", "PwdExpired": + case "FALSE", "NotNormal", "PwdExpired": color = "red" + case "Disabled": + color = "yellow" } if color != "" { diff --git a/utils/ldapvars.go b/utils/ldapvars.go index 31a3a25..46805d4 100644 --- a/utils/ldapvars.go +++ b/utils/ldapvars.go @@ -160,46 +160,55 @@ var InstanceTypeMap = map[int]string{ 32: "NamingContextRemovalFromDSA", } -var PredefinedLdapQueries = map[string]string{ - "DomainControllers": "(&(objectCategory=computer)(userAccountControl:1.2.840.113556.1.4.803:=8192))", - "NonDCServers": "(&(objectCategory=computer)(operatingSystem=*server*)(!(userAccountControl:1.2.840.113556.1.4.803:=8192)))", - "NonServerComputers": "(&(objectCategory=computer)(!(operatingSystem=*server*))(!(userAccountControl:1.2.840.113556.1.4.803:=8192)))", - "AllOrganizationalUnits": "(objectCategory=organizationalUnit)", - "AllContainers": "(objectCategory=container)", - "AllGroups": "(objectCategory=group)", - "AllComputers": "(objectClass=computer)", - "AllUsers": "(&(objectCategory=person)(objectClass=user))", - "UsersWithSPN": "(&(objectCategory=user)(servicePrincipalName=*))", - "UsersWithSIDHistory": "(&(objectCategory=person)(objectClass=user)(sidHistory=*))", - "KrbPreauthDisabledUsers": "(&(objectCategory=person)(userAccountControl:1.2.840.113556.1.4.803:=4194304))", - "KrbPreauthDisabledComputers": "(&(objectCategory=computer)(userAccountControl:1.2.840.113556.1.4.803:=4194304))", - "CertificatePublishers": "(CN=Cert Publishers*)", - "ConstrainedDelegationObjects": "(msDS-AllowedToDelegateTo=*)", - "UnconstrainedDelegationObjects": "(userAccountControl:1.2.840.113556.1.4.803:=524288)", - "RBCDObjects": "(msDS-AllowedToActOnBehalfOfOtherIdentity=*)", - "NotTrustedForDelegation": "(&(samaccountname=*)(userAccountControl:1.2.840.113556.1.4.803:=1048576))", - "ShadowCredentialsTargets": "(msDS-KeyCredentialLink=*)", - "UsersMustChangePassword": "(&(objectCategory=person)(objectClass=user)(pwdLastSet=0)(!(useraccountcontrol:1.2.840.113556.1.4.803:=2)))", - "UsersWithNeverExpirePasswords": "(&(objectCategory=user)(userAccountControl:1.2.840.113556.1.4.803:=65536))", - "UsersWithEmptyPasswords": "(&(objectCategory=user)(userAccountControl:1.2.840.113556.1.4.803:=32))", - "AdminAccounts": "(&(objectCategory=user)(memberOf=CN=Administrators,CN=Builtin,DC=domain,DC=com))", - "LockedOutUserAccounts": "(&(objectCategory=user)(lockoutTime>=1))", - "HighPrivilegeUsers": "(&(objectCategory=user)(adminCount=1))", - "MembersOfDomainAdminsGroup": "(&(objectCategory=user)(memberOf=CN=Domain Admins,CN=Users,DC=domain,DC=com))", - "UsersWithPasswordNeverChanged": "(&(objectCategory=user)(pwdLastSet=0))", - "UsersWithEmptyDescription": "(&(objectCategory=user)(description=*))", - "UsersWithNoEmailAddress": "(&(objectCategory=user)(!(mail=*)))", - "UnusualAccountNames": "(&(objectCategory=user)(sAMAccountName=*$*))", - "ServiceAccountNames": "(&(objectCategory=user)(sAMAccountName=*svc*))", - "DisabledUserAccounts": "(&(objectCategory=user)(userAccountControl:1.2.840.113556.1.4.803:=2))", - "StaleComputerAccounts": "(&(objectCategory=computer)(!lastLogonTimestamp=*))", - "UsersWithNonExpiringPasswords": "(&(objectCategory=user)(userAccountControl:1.2.840.113556.1.4.803:=66048))", - "EnabledUsersNotInGroup": "(&(objectCategory=user)(userAccountControl:1.2.840.113556.1.4.803:=512)(!(memberOf=*)))", - "ComputersWithOutdatedOS": "(&(objectCategory=computer)(operatingSystem=*Server 2008*))", - "UsersWithSensitiveInformation": "(&(objectCategory=user)(|(telephoneNumber=*)(pager=*)(homePhone=*)(mobile=*)(info=*)))", - "RecentlyCreatedUsers": "(&(objectCategory=user)(whenCreated>=))", - "InactiveUsersLastLogonTime": "(&(objectCategory=user)(lastLogonTimestamp<=))", - "ExpiredUserAccounts": "(&(objectCategory=user)(accountExpires<=))", +type LibQuery struct { + Title string + Filter string +} + +var PredefinedLdapQueries = map[string][]LibQuery{ + "Enum": []LibQuery{ + LibQuery{"All Organizational Units", "(objectCategory=organizationalUnit)"}, + LibQuery{"All Containers", "(objectCategory=container)"}, + LibQuery{"All Groups", "(objectCategory=group)"}, + LibQuery{"All Computers", "(objectClass=computer)"}, + LibQuery{"All Users", "(&(objectCategory=person)(objectClass=user))"}, + }, + "Users": []LibQuery{ + LibQuery{"Recently Created Users", "(&(objectCategory=user)(whenCreated>=))"}, + LibQuery{"Users With Description", "(&(objectCategory=user)(description=*))"}, + LibQuery{"Users Without Email", "(&(objectCategory=user)(!(mail=*)))"}, + LibQuery{"Likely Service Users", "(&(objectCategory=user)(sAMAccountName=*svc*))"}, + LibQuery{"Disabled Users", "(&(objectCategory=user)(userAccountControl:1.2.840.113556.1.4.803:=2))"}, + LibQuery{"Expired Users", "(&(objectCategory=user)(accountExpires<=))"}, + LibQuery{"Users With Sensitive Infos", "(&(objectCategory=user)(|(telephoneNumber=*)(pager=*)(homePhone=*)(mobile=*)(info=*)(streetAddress=*)))"}, + LibQuery{"Inactive Users", "(&(objectCategory=user)(lastLogonTimestamp<=))"}, + }, + "Computers": []LibQuery{ + LibQuery{"Domain Controllers", "(&(objectCategory=computer)(userAccountControl:1.2.840.113556.1.4.803:=8192))"}, + LibQuery{"Non-DC Servers", "(&(objectCategory=computer)(operatingSystem=*server*)(!(userAccountControl:1.2.840.113556.1.4.803:=8192)))"}, + LibQuery{"Non-Server Computers", "(&(objectCategory=computer)(!(operatingSystem=*server*))(!(userAccountControl:1.2.840.113556.1.4.803:=8192)))"}, + LibQuery{"Stale Computers", "(&(objectCategory=computer)(!lastLogonTimestamp=*))"}, + LibQuery{"Computers With Outdated OS", "(&(objectCategory=computer)(|(operatingSystem=*Server 2008*)(operatingSystem=*Server 2003*)(operatingSystem=*Windows XP*)(operatingSystem=*Windows 7*)))"}, + }, + "Security": []LibQuery{ + LibQuery{"Domain Admins", "(&(objectCategory=user)(memberOf=CN=Domain Admins,CN=Users,DC=domain,DC=com))"}, + LibQuery{"Administrators", "(&(objectCategory=user)(memberOf=CN=Administrators,CN=Builtin,DC=domain,DC=com))"}, + LibQuery{"High Privilege Users", "(&(objectCategory=user)(adminCount=1))"}, + LibQuery{"Users With SPN", "(&(objectCategory=user)(servicePrincipalName=*))"}, + LibQuery{"Users With SIDHistory", "(&(objectCategory=person)(objectClass=user)(sidHistory=*))"}, + LibQuery{"KrbPreauth Disabled Users", "(&(objectCategory=person)(userAccountControl:1.2.840.113556.1.4.803:=4194304))"}, + LibQuery{"KrbPreauth Disabled Computers", "(&(objectCategory=computer)(userAccountControl:1.2.840.113556.1.4.803:=4194304))"}, + LibQuery{"Constrained Delegation Objects", "(msDS-AllowedToDelegateTo=*)"}, + LibQuery{"Unconstrained Delegation Objects", "(userAccountControl:1.2.840.113556.1.4.803:=524288)"}, + LibQuery{"RBCD Objects", "(msDS-AllowedToActOnBehalfOfOtherIdentity=*)"}, + LibQuery{"Not Trusted For Delegation", "(&(samaccountname=*)(userAccountControl:1.2.840.113556.1.4.803:=1048576))"}, + LibQuery{"Shadow Credentials Targets", "(msDS-KeyCredentialLink=*)"}, + LibQuery{"Must Change Password Users", "(&(objectCategory=person)(objectClass=user)(pwdLastSet=0)(!(useraccountcontrol:1.2.840.113556.1.4.803:=2)))"}, + LibQuery{"Password Never Changed Users", "(&(objectCategory=user)(pwdLastSet=0))"}, + LibQuery{"Never Expire Password Users", "(&(objectCategory=user)(userAccountControl:1.2.840.113556.1.4.803:=65536))"}, + LibQuery{"Empty Password Users", "(&(objectCategory=user)(userAccountControl:1.2.840.113556.1.4.803:=32))"}, + LibQuery{"LockedOut Users", "(&(objectCategory=user)(lockoutTime>=1))"}, + }, } var WellKnownSIDsMap = map[string]string{