reg-dump

Fuzzy search the Windows registry.
Log | Files | Refs | README | LICENSE

commit 8a0fd0b6a5df5ec969fcf1d6301d089f3766727b
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date:   Wed, 14 Aug 2024 11:06:03 +0800

reg-dump: quickly search the Windows Registry

Signed-off-by: Jack Mordaunt <jackmordaunt.dev@gmail.com>

Diffstat:
ALICENSE | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AREADME.md | 19+++++++++++++++++++
Ago.mod | 10++++++++++
Ago.sum | 6++++++
Amain.go | 276+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 372 insertions(+), 0 deletions(-)

diff --git a/LICENSE b/LICENSE @@ -0,0 +1,61 @@ +This project is provided under the terms of the UNLICENSE or +the MIT license denoted by the following SPDX identifier: + +SPDX-License-Identifier: Unlicense OR MIT + +You may use the project under the terms of either license. + +Both licenses are reproduced below. + +---- +The MIT License (MIT) + +Copyright (c) 2024 Jack Mordaunt + +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: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +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. +--- + +--- +The UNLICENSE + +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +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 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. + +For more information, please refer to <https://unlicense.org/> +--- diff --git a/README.md b/README.md @@ -0,0 +1,19 @@ +# `reg-dump` + +This utility provides fast searching within the Windows registry. + +## Install + +`go install git.sr.ht/~jackmordaunt/reg-dump@latest` + +## Usage + +`reg-dump search` + +Supply a regex to match paths and values. + +`reg-dump search -pattern "*.exe"` + +Combine with `fzf` to fuzzy search the result. + +`reg-dump search -pattern "*.exe" -log log.txt | fzf --preview "reg-dump show {}"` diff --git a/go.mod b/go.mod @@ -0,0 +1,10 @@ +module git.sr.ht/~jackmordaunt/reg-dump + +go 1.22.3 + +require ( + github.com/carlmjohnson/flowmatic v0.23.4 + golang.org/x/sys v0.24.0 +) + +require github.com/carlmjohnson/deque v0.23.1 // indirect diff --git a/go.sum b/go.sum @@ -0,0 +1,6 @@ +github.com/carlmjohnson/deque v0.23.1 h1:X2HOJM9xcglY03deMZ0oZ1V2xtbqYV7dJDnZiSZN4Ak= +github.com/carlmjohnson/deque v0.23.1/go.mod h1:LF5NJjICBrEOPx84pxPL4nCimy5n9NQjxKi5cXkh+8U= +github.com/carlmjohnson/flowmatic v0.23.4 h1:SfK6f+zKUlw4aga1ph+7/csqVeUAWnBxfqKN5gvQzzs= +github.com/carlmjohnson/flowmatic v0.23.4/go.mod h1:Jpvyl591Dvkt9chYpnVupjxlKvqkZ9CtCmqL4wfQD7U= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/main.go b/main.go @@ -0,0 +1,276 @@ +package main + +import ( + "flag" + "fmt" + "hash/maphash" + "io" + "log/slog" + "os" + "path/filepath" + "regexp" + "slices" + "strconv" + "strings" + "sync" + + "github.com/carlmjohnson/flowmatic" + "golang.org/x/sys/windows/registry" + + _ "net/http/pprof" +) + +func main() { + args := os.Args[1:] + + cmd := "search" + + if v, ok := take(&args); ok { + cmd = v + } + + switch cmd { + case "search": + Search(args) + case "show": + Show(args) + } +} + +// take returns the first element. +func take[S ~[]E, E any](s *S) (e E, ok bool) { + if len(*s) == 0 { + return e, false + } + defer func() { *s = (*s)[1:] }() + return (*s)[0], true +} + +// Show outputs the given registry key values. +func Show(args []string) { + var path string + + flags := flag.NewFlagSet("show", flag.ExitOnError) + flags.StringVar(&path, "path", "", "path to show values for") + flags.Parse(args) + + positional := flags.Args() + + if path == "" && len(positional) > 0 { + path, _ = take(&positional) + } + + show(path) +} + +// Search the registry outputting each entry that contains the given pattern. +// If the pattern is empty search will output all entries. +func Search(args []string) { + var ( + log string + pattern string + root string + ) + + flags := flag.NewFlagSet("search", flag.ExitOnError) + flags.StringVar(&log, "log", "", "log file") + flags.StringVar(&pattern, "pattern", "", "regex pattern to match against") + flags.StringVar(&root, "path", "", "root search path") + flags.Parse(args) + + var exp *regexp.Regexp + + if pattern != "" { + exp = regexp.MustCompile(pattern) + } + + var output io.Writer + + if log != "" { + f, err := os.OpenFile(log, os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + panic(fmt.Errorf("opening log file: %w", err)) + } + defer f.Close() + output = f + } else { + output = os.Stderr + } + + manager := Manager{ + Logger: slog.New(slog.NewTextHandler(output, nil)), + Pattern: exp, + } + + flowmatic.ManageTasks(-1, walk, manager.Manage, root) +} + +type Manager struct { + *slog.Logger + Pattern *regexp.Regexp + + seedInit sync.Once + seed maphash.Seed + + seen []uint64 +} + +func (m *Manager) Manage(path string, subkeys []string, err error) ([]string, bool) { + if err != nil { + m.Error("task", path, err) + } + + if m.redundant(path) { + m.Warn("skipping", "path", path) + return nil, true + } + + if m.match(path) { + if err == nil { + fmt.Println(path) + } else { + fmt.Printf("%s (%s)", path, err.Error()) + } + } + + for ii, subkey := range subkeys { + subkeys[ii] = filepath.Join(path, subkey) + } + + return subkeys, true +} + +// redundant is true if the path has already been seen. +// +// The check is done using a binary search over sorted hashes to avoid retaining +// many heap allocated strings. The registry has a lot of entries and can easily +// consume gigabytes of memory if we heap allocate each path. +func (m *Manager) redundant(path string) bool { + m.seedInit.Do(func() { + m.seed = maphash.MakeSeed() + }) + + hash := maphash.String(m.seed, path) + + if _, ok := slices.BinarySearch(m.seen, hash); ok { + return true + } + + m.seen = append(m.seen, hash) + slices.Sort(m.seen) + + return false +} + +// match the path and its values against the pattern. +func (m *Manager) match(path string) bool { + if m.Pattern == nil { + return true + } + + if m.Pattern.MatchString(path) { + return true + } + + key, err := registry.OpenKey(registry.CURRENT_USER, path, registry.READ|registry.ENUMERATE_SUB_KEYS|registry.QUERY_VALUE) + if err != nil { + return false + } + + defer key.Close() + + return !forEachValue(key, func(name, value string, err error) bool { + if err != nil { + m.Error("matching", fmt.Sprintf("%s.%s", path, name), err) + return true + } + if m.Pattern.MatchString(name) || m.Pattern.MatchString(value) { + return false + } + return true + }) +} + +func walk(path string) ([]string, error) { + key, err := registry.OpenKey(registry.CURRENT_USER, path, registry.READ|registry.ENUMERATE_SUB_KEYS|registry.QUERY_VALUE) + if err != nil { + return nil, fmt.Errorf("opening key: %w", err) + } + + defer key.Close() + + subkeys, err := key.ReadSubKeyNames(-1) + if err != nil { + return nil, fmt.Errorf("reading subkeys: %w", err) + } + + return subkeys, nil +} + +func show(path string) { + key, err := registry.OpenKey(registry.CURRENT_USER, path, registry.READ|registry.ENUMERATE_SUB_KEYS|registry.QUERY_VALUE) + if err != nil { + panic(err) + } + + defer key.Close() + + forEachValue(key, func(name, value string, err error) bool { + if name == "" { + name = "Default" + } + if err != nil { + fmt.Fprintf(os.Stdout, "%v: %q (%v)\n", name, value, err.Error()) + } else { + fmt.Fprintf(os.Stdout, "%v: %q\n", name, value) + } + return true + }) +} + +// forEachValue invokes [fn] on each value [key], returning true if all values were processed. +func forEachValue(key registry.Key, fn func(name string, value string, err error) bool) bool { + names, err := key.ReadValueNames(-1) + if err != nil { + panic(fmt.Errorf("reading value names: %w", err)) + } + + for _, name := range names { + _, t, err := key.GetValue(name, nil) + if err != nil { + panic(err) + } + + var value string + + switch t { + case registry.SZ, registry.EXPAND_SZ: + value, _, err = key.GetStringValue(name) + if err != nil { + fn(name, value, err) + } + + case registry.DWORD, registry.QWORD: + n, _, err := key.GetIntegerValue(name) + if err != nil { + fn(name, value, err) + } + value = strconv.Itoa(int(n)) + + case registry.MULTI_SZ: + values, _, err := key.GetStringsValue(name) + if err != nil { + fn(name, value, err) + } + value = strings.Join(values, " | ") + } + + if value != "" { + if !fn(name, value, err) { + return false + } + } + } + + return true +}