reg-dump

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

main.go (8026B)


      1 package main
      2 
      3 import (
      4 	"flag"
      5 	"fmt"
      6 	"hash/maphash"
      7 	"io"
      8 	"log/slog"
      9 	"os"
     10 	"path/filepath"
     11 	"regexp"
     12 	"slices"
     13 	"strconv"
     14 	"strings"
     15 	"sync"
     16 
     17 	"github.com/carlmjohnson/flowmatic"
     18 	"golang.org/x/sys/windows/registry"
     19 
     20 	_ "net/http/pprof"
     21 )
     22 
     23 func main() {
     24 	args := os.Args[1:]
     25 
     26 	cmd := "search"
     27 
     28 	if v, ok := take(&args); ok {
     29 		cmd = v
     30 	}
     31 
     32 	switch cmd {
     33 	case "search":
     34 		Search(args)
     35 	case "show":
     36 		Show(args)
     37 	case "purge":
     38 		Purge(args)
     39 	}
     40 }
     41 
     42 // take returns the first element.
     43 func take[S ~[]E, E any](s *S) (e E, ok bool) {
     44 	if len(*s) == 0 {
     45 		return e, false
     46 	}
     47 	defer func() { *s = (*s)[1:] }()
     48 	return (*s)[0], true
     49 }
     50 
     51 // Show outputs the given registry key values.
     52 func Show(args []string) {
     53 	var path string
     54 
     55 	flags := flag.NewFlagSet("show", flag.ExitOnError)
     56 	flags.StringVar(&path, "path", "", "path to show values for")
     57 	flags.Parse(args)
     58 
     59 	positional := flags.Args()
     60 
     61 	if path == "" && len(positional) > 0 {
     62 		path, _ = take(&positional)
     63 	}
     64 
     65 	show(path)
     66 }
     67 
     68 // Search the registry outputting each entry that contains the given pattern.
     69 // If the pattern is empty search will output all entries.
     70 func Search(args []string) {
     71 	var (
     72 		log     string
     73 		pattern string
     74 		root    string
     75 	)
     76 
     77 	flags := flag.NewFlagSet("search", flag.ExitOnError)
     78 	flags.StringVar(&log, "log", "", "log file")
     79 	flags.StringVar(&pattern, "pattern", "", "regex pattern to match against")
     80 	flags.StringVar(&root, "path", "", "root search path")
     81 	flags.Parse(args)
     82 
     83 	var exp *regexp.Regexp
     84 
     85 	if pattern != "" {
     86 		exp = regexp.MustCompile(pattern)
     87 	}
     88 
     89 	var output io.Writer
     90 
     91 	if log != "" {
     92 		f, err := os.OpenFile(log, os.O_CREATE|os.O_WRONLY, 0o644)
     93 		if err != nil {
     94 			panic(fmt.Errorf("opening log file: %w", err))
     95 		}
     96 		defer f.Close()
     97 		output = f
     98 	} else {
     99 		output = os.Stderr
    100 	}
    101 
    102 	manager := Manager{
    103 		Logger:  slog.New(slog.NewTextHandler(output, nil)),
    104 		Pattern: exp,
    105 	}
    106 
    107 	flowmatic.ManageTasks(-1, walk, manager.Search, root)
    108 }
    109 
    110 // Purge the registry of all entries that match the pattern.
    111 func Purge(args []string) {
    112 	var (
    113 		log     string
    114 		pattern string
    115 		root    string
    116 	)
    117 
    118 	flags := flag.NewFlagSet("search", flag.ExitOnError)
    119 	flags.StringVar(&log, "log", "", "log file")
    120 	flags.StringVar(&pattern, "pattern", "", "regex pattern to match against")
    121 	flags.StringVar(&root, "path", "", "root search path")
    122 	flags.Parse(args)
    123 
    124 	if pattern == "" {
    125 		fmt.Println("-pattern is required")
    126 		os.Exit(1)
    127 	}
    128 
    129 	var exp *regexp.Regexp
    130 
    131 	if pattern != "" {
    132 		exp = regexp.MustCompile(pattern)
    133 	}
    134 
    135 	var output io.Writer
    136 
    137 	if log != "" {
    138 		f, err := os.OpenFile(log, os.O_CREATE|os.O_WRONLY, 0o644)
    139 		if err != nil {
    140 			panic(fmt.Errorf("opening log file: %w", err))
    141 		}
    142 		defer f.Close()
    143 		output = f
    144 	} else {
    145 		output = os.Stderr
    146 	}
    147 
    148 	manager := Manager{
    149 		Logger:  slog.New(slog.NewTextHandler(output, nil)),
    150 		Pattern: exp,
    151 	}
    152 
    153 	flowmatic.ManageTasks(-1, walk, manager.Purge, root)
    154 }
    155 
    156 type Manager struct {
    157 	*slog.Logger
    158 	Pattern *regexp.Regexp
    159 
    160 	seedInit sync.Once
    161 	seed     maphash.Seed
    162 
    163 	seen []uint64
    164 }
    165 
    166 // Search outputs matched registry entries.
    167 func (m *Manager) Search(path string, subkeys []string, err error) ([]string, bool) {
    168 	if err != nil {
    169 		m.Error("task", path, err)
    170 	}
    171 
    172 	if m.redundant(path) {
    173 		m.Warn("skipping", "path", path)
    174 		return nil, true
    175 	}
    176 
    177 	if m.match(path) {
    178 		if err == nil {
    179 			fmt.Println(path)
    180 		} else {
    181 			fmt.Printf("%s (%s)", path, err.Error())
    182 		}
    183 	}
    184 
    185 	for ii, subkey := range subkeys {
    186 		subkeys[ii] = filepath.Join(path, subkey)
    187 	}
    188 
    189 	return subkeys, true
    190 }
    191 
    192 // Purge deletes matched registry entries.
    193 func (m *Manager) Purge(path string, subkeys []string, err error) ([]string, bool) {
    194 	if err != nil {
    195 		m.Error("task", path, err)
    196 	}
    197 
    198 	if m.redundant(path) {
    199 		m.Warn("skipping", "path", path)
    200 		return nil, true
    201 	}
    202 
    203 	if m.match(path) && err == nil {
    204 		if err := m.purge(path); err != nil {
    205 			m.Error("purge", path, err)
    206 		}
    207 		return nil, true
    208 	}
    209 
    210 	for ii, subkey := range subkeys {
    211 		subkeys[ii] = filepath.Join(path, subkey)
    212 	}
    213 
    214 	return subkeys, true
    215 }
    216 
    217 // purge the given path.
    218 // If the path contains the pattern, delete the key.
    219 // Else if the key's values contain the pattern in name or value, delete them.
    220 func (m *Manager) purge(path string) error {
    221 	if m.Pattern.MatchString(path) {
    222 		if err := registry.DeleteKey(registry.CURRENT_USER, path); err != nil {
    223 			return fmt.Errorf("deleting key: %w", err)
    224 		}
    225 		return nil
    226 	}
    227 
    228 	key, err := registry.OpenKey(registry.CURRENT_USER, path, registry.ALL_ACCESS)
    229 	if err != nil {
    230 		return fmt.Errorf("opening key with write permissions: %w", err)
    231 	}
    232 
    233 	defer key.Close()
    234 
    235 	forEachValue(key, func(name, value string, err error) bool {
    236 		if err != nil {
    237 			m.Error("iterating", fmt.Sprintf("%s.%s", path, name), err)
    238 		}
    239 		if m.Pattern.MatchString(name) || m.Pattern.MatchString(value) {
    240 			if err := key.DeleteValue(name); err != nil {
    241 				m.Error("deleting", fmt.Sprintf("%s.%s", path, name), err)
    242 			}
    243 		}
    244 		return true
    245 	})
    246 
    247 	return nil
    248 }
    249 
    250 // redundant is true if the path has already been seen.
    251 //
    252 // The check is done using a binary search over sorted hashes to avoid retaining
    253 // many heap allocated strings. The registry has a lot of entries and can easily
    254 // consume gigabytes of memory if we heap allocate each path.
    255 func (m *Manager) redundant(path string) bool {
    256 	m.seedInit.Do(func() {
    257 		m.seed = maphash.MakeSeed()
    258 	})
    259 
    260 	hash := maphash.String(m.seed, path)
    261 
    262 	if _, ok := slices.BinarySearch(m.seen, hash); ok {
    263 		return true
    264 	}
    265 
    266 	m.seen = append(m.seen, hash)
    267 	slices.Sort(m.seen)
    268 
    269 	return false
    270 }
    271 
    272 // match the path and its values against the pattern.
    273 func (m *Manager) match(path string) bool {
    274 	if m.Pattern == nil {
    275 		return true
    276 	}
    277 
    278 	if m.Pattern.MatchString(path) {
    279 		return true
    280 	}
    281 
    282 	key, err := registry.OpenKey(registry.CURRENT_USER, path, registry.READ|registry.ENUMERATE_SUB_KEYS|registry.QUERY_VALUE)
    283 	if err != nil {
    284 		return false
    285 	}
    286 
    287 	defer key.Close()
    288 
    289 	return !forEachValue(key, func(name, value string, err error) bool {
    290 		if err != nil {
    291 			m.Error("matching", fmt.Sprintf("%s.%s", path, name), err)
    292 			return true
    293 		}
    294 		if m.Pattern.MatchString(name) || m.Pattern.MatchString(value) {
    295 			return false
    296 		}
    297 		return true
    298 	})
    299 }
    300 
    301 func walk(path string) ([]string, error) {
    302 	key, err := registry.OpenKey(registry.CURRENT_USER, path, registry.READ|registry.ENUMERATE_SUB_KEYS|registry.QUERY_VALUE)
    303 	if err != nil {
    304 		return nil, fmt.Errorf("opening key: %w", err)
    305 	}
    306 
    307 	defer key.Close()
    308 
    309 	subkeys, err := key.ReadSubKeyNames(-1)
    310 	if err != nil {
    311 		return nil, fmt.Errorf("reading subkeys: %w", err)
    312 	}
    313 
    314 	return subkeys, nil
    315 }
    316 
    317 func show(path string) {
    318 	key, err := registry.OpenKey(registry.CURRENT_USER, path, registry.READ|registry.ENUMERATE_SUB_KEYS|registry.QUERY_VALUE)
    319 	if err != nil {
    320 		panic(err)
    321 	}
    322 
    323 	defer key.Close()
    324 
    325 	forEachValue(key, func(name, value string, err error) bool {
    326 		if name == "" {
    327 			name = "Default"
    328 		}
    329 		if err != nil {
    330 			fmt.Fprintf(os.Stdout, "%v: %q (%v)\n", name, value, err.Error())
    331 		} else {
    332 			fmt.Fprintf(os.Stdout, "%v: %q\n", name, value)
    333 		}
    334 		return true
    335 	})
    336 }
    337 
    338 // forEachValue invokes [fn] on each value [key], returning true if all values were processed.
    339 func forEachValue(key registry.Key, fn func(name string, value string, err error) bool) bool {
    340 	names, err := key.ReadValueNames(-1)
    341 	if err != nil {
    342 		panic(fmt.Errorf("reading value names: %w", err))
    343 	}
    344 
    345 	for _, name := range names {
    346 		_, t, err := key.GetValue(name, nil)
    347 		if err != nil {
    348 			panic(err)
    349 		}
    350 
    351 		var value string
    352 
    353 		switch t {
    354 		case registry.SZ, registry.EXPAND_SZ:
    355 			value, _, err = key.GetStringValue(name)
    356 			if err != nil {
    357 				fn(name, value, err)
    358 			}
    359 
    360 		case registry.DWORD, registry.QWORD:
    361 			n, _, err := key.GetIntegerValue(name)
    362 			if err != nil {
    363 				fn(name, value, err)
    364 			}
    365 			value = strconv.Itoa(int(n))
    366 
    367 		case registry.MULTI_SZ:
    368 			values, _, err := key.GetStringsValue(name)
    369 			if err != nil {
    370 				fn(name, value, err)
    371 			}
    372 			value = strings.Join(values, " | ")
    373 		}
    374 
    375 		if value != "" {
    376 			if !fn(name, value, err) {
    377 				return false
    378 			}
    379 		}
    380 	}
    381 
    382 	return true
    383 }