diff --git a/doc/cvtsudoers.cat b/doc/cvtsudoers.cat
index 006581a48..e6361f58c 100644
--- a/doc/cvtsudoers.cat
+++ b/doc/cvtsudoers.cat
@@ -4,9 +4,9 @@ NNAAMMEE
      ccvvttssuuddooeerrss - convert between sudoers file formats
 
 SSYYNNOOPPSSIISS
-     ccvvttssuuddooeerrss [--eehhVV] [--bb _d_n] [--cc _c_o_n_f___f_i_l_e] [--II _i_n_c_r_e_m_e_n_t] [--ii _i_n_p_u_t___f_o_r_m_a_t]
-                [--ff _o_u_t_p_u_t___f_o_r_m_a_t] [--OO _s_t_a_r_t___p_o_i_n_t] [--oo _o_u_t_p_u_t___f_i_l_e]
-                [_i_n_p_u_t___f_i_l_e]
+     ccvvttssuuddooeerrss [--eehhVV] [--bb _d_n] [--cc _c_o_n_f___f_i_l_e] [--ff _o_u_t_p_u_t___f_o_r_m_a_t]
+                [--ii _i_n_p_u_t___f_o_r_m_a_t] [--II _i_n_c_r_e_m_e_n_t] [--mm _f_i_l_t_e_r] [--oo _o_u_t_p_u_t___f_i_l_e]
+                [--OO _s_t_a_r_t___p_o_i_n_t] [_i_n_p_u_t___f_i_l_e]
 
 DDEESSCCRRIIPPTTIIOONN
      ccvvttssuuddooeerrss can be used to convert between _s_u_d_o_e_r_s security policy file
@@ -52,14 +52,13 @@ DDEESSCCRRIIPPTTIIOONN
 
                            Conversion to LDIF has the following limitations:
 
-                           ++oo     Command, host, runas and user-specific
-                                 Defaults lines cannot be translated as they
-                                 don't have an equivalent in the sudoers LDAP
-                                 schema.
+                           ++oo  Command, host, runas and user-specific Defaults
+                              lines cannot be translated as they don't have an
+                              equivalent in the sudoers LDAP schema.
 
-                           ++oo     Command, host, runas and user aliases are not
-                                 supported by the sudoers LDAP schema so they
-                                 are expanded during the conversion.
+                           ++oo  Command, host, runas and user aliases are not
+                              supported by the sudoers LDAP schema so they are
+                              expanded during the conversion.
 
                  sudoers   Traditional sudoers format.  A new sudoers file
                            will be reconstructed from the parsed input file.
@@ -68,11 +67,6 @@ DDEESSCCRRIIPPTTIIOONN
 
      --hh, ----hheellpp  Display a short help message to the standard output and exit.
 
-     --II _i_n_c_r_e_m_e_n_t, ----iinnccrreemmeenntt=_i_n_c_r_e_m_e_n_t
-                 When generating LDIF output, increment each sudoOrder
-                 attribute by the specified number.  Defaults to an increment
-                 of 1.
-
      --ii _i_n_p_u_t___f_o_r_m_a_t, ----iinnppuutt--ffoorrmmaatt=_i_n_p_u_t___f_o_r_m_a_t
                  Specify the input format.  The following formats are
                  supported:
@@ -88,6 +82,28 @@ DDEESSCCRRIIPPTTIIOONN
                  sudoers   Traditional sudoers format.  This is the default
                            input format.
 
+     --II _i_n_c_r_e_m_e_n_t, ----iinnccrreemmeenntt=_i_n_c_r_e_m_e_n_t
+                 When generating LDIF output, increment each sudoOrder
+                 attribute by the specified number.  Defaults to an increment
+                 of 1.
+
+     --mm _f_i_l_t_e_r, ----mmaattcchh=_f_i_l_t_e_r
+                 Only output rules that match the specified _f_i_l_t_e_r.  A _f_i_l_t_e_r
+                 expression is made up of one or more kkeeyy == _v_a_l_u_e pairs,
+                 separated by a comma (`,').  The kkeeyy may be "user", "group"
+                 or "host".  For example, uusseerr = _o_p_e_r_a_t_o_r or hhoosstt = _w_w_w.
+
+                 The password and group databases are not consulted when
+                 matching against the filter so the users and groups do not
+                 need to be present on the local system.  Only aliases that
+                 are referenced by the filtered policy rules will be
+                 displayed.
+
+     --oo _o_u_t_p_u_t___f_i_l_e, ----oouuttppuutt=_o_u_t_p_u_t___f_i_l_e
+                 Write the converted output to _o_u_t_p_u_t___f_i_l_e.  If no _o_u_t_p_u_t___f_i_l_e
+                 is specified, or if it is `-', the converted _s_u_d_o_e_r_s policy
+                 will be written to the standard output.
+
      --OO _s_t_a_r_t___p_o_i_n_t, ----oorrddeerr--ssttaarrtt=_s_t_a_r_t___p_o_i_n_t
                  When generating LDIF output, use the number specified by
                  _s_t_a_r_t___p_o_i_n_t in the sudoOrder attribute of the first sudoRole
@@ -97,11 +113,6 @@ DDEESSCCRRIIPPTTIIOONN
                  point of 0 will disable the generation of sudoOrder
                  attributes in the resulting LDIF file.
 
-     --oo _o_u_t_p_u_t___f_i_l_e, ----oouuttppuutt=_o_u_t_p_u_t___f_i_l_e
-                 Write the converted output to _o_u_t_p_u_t___f_i_l_e.  If no _o_u_t_p_u_t___f_i_l_e
-                 is specified, or if it is `-', the converted _s_u_d_o_e_r_s policy
-                 will be written to the standard output.
-
      --VV, ----vveerrssiioonn
                  Print the ccvvttssuuddooeerrss and _s_u_d_o_e_r_s grammar versions and exit.
 
@@ -115,6 +126,9 @@ DDEESSCCRRIIPPTTIIOONN
      iinnppuutt__ffoorrmmaatt == _l_d_i_f | _s_u_d_o_e_r_s
            See the description of the --ii command line option.
 
+     mmaattcchh == _f_i_l_t_e_r
+           See the description of the --mm command line option.
+
      oorrddeerr__iinnccrreemmeenntt == _i_n_c_r_e_m_e_n_t
            See the description of the --II command line option.
 
@@ -162,4 +176,4 @@ DDIISSCCLLAAIIMMEERR
      file distributed with ssuuddoo or https://www.sudo.ws/license.html for
      complete details.
 
-Sudo 1.8.23                    February 23, 2018                   Sudo 1.8.23
+Sudo 1.8.23                     March 21, 2018                     Sudo 1.8.23
diff --git a/doc/cvtsudoers.man.in b/doc/cvtsudoers.man.in
index 28c23bcd6..599f64e9d 100644
--- a/doc/cvtsudoers.man.in
+++ b/doc/cvtsudoers.man.in
@@ -16,7 +16,7 @@
 .\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 .\" ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 .\"
-.TH "CVTSUDOERS" "8" "February 23, 2018" "Sudo @PACKAGE_VERSION@" "System Manager's Manual"
+.TH "CVTSUDOERS" "8" "March 21, 2018" "Sudo @PACKAGE_VERSION@" "System Manager's Manual"
 .nh
 .if n .ad l
 .SH "NAME"
@@ -28,11 +28,12 @@
 [\fB\-ehV\fR]
 [\fB\-b\fR\ \fIdn\fR]
 [\fB\-c\fR\ \fIconf_file\fR]
-[\fB\-I\fR\ \fIincrement\fR]
-[\fB\-i\fR\ \fIinput_format\fR]
 [\fB\-f\fR\ \fIoutput_format\fR]
-[\fB\-O\fR\ \fIstart_point\fR]
+[\fB\-i\fR\ \fIinput_format\fR]
+[\fB\-I\fR\ \fIincrement\fR]
+[\fB\-m\fR\ \fIfilter\fR]
 [\fB\-o\fR\ \fIoutput_file\fR]
+[\fB\-O\fR\ \fIstart_point\fR]
 [\fIinput_file\fR]
 .SH "DESCRIPTION"
 \fBcvtsudoers\fR
@@ -105,12 +106,12 @@ Conversion to LDIF has the following limitations:
 .PP
 .RS 10n
 .PD 0
-.TP 6n
+.TP 3n
 \fB\(bu\fR
 Command, host, runas and user-specific Defaults lines cannot be
 translated as they don't have an equivalent in the sudoers LDAP schema.
 .PD
-.TP 6n
+.TP 3n
 \fB\(bu\fR
 Command, host, runas and user aliases are not supported by the
 sudoers LDAP schema so they are expanded during the conversion.
@@ -132,11 +133,6 @@ output inline.
 \fB\-h\fR, \fB\--help\fR
 Display a short help message to the standard output and exit.
 .TP 12n
-\fB\-I\fR \fIincrement\fR, \fB\--increment\fR=\fIincrement\fR
-When generating LDIF output, increment each sudoOrder attribute by
-the specified number.
-Defaults to an increment of 1.
-.TP 12n
 \fB\-i\fR \fIinput_format\fR, \fB\--input-format\fR=\fIinput_format\fR
 Specify the input format.
 The following formats are supported:
@@ -162,6 +158,49 @@ This is the default input format.
 .RE
 .PD
 .TP 12n
+\fB\-I\fR \fIincrement\fR, \fB\--increment\fR=\fIincrement\fR
+When generating LDIF output, increment each sudoOrder attribute by
+the specified number.
+Defaults to an increment of 1.
+.TP 12n
+\fB\-m\fR \fIfilter\fR, \fB\--match\fR=\fIfilter\fR
+Only output rules that match the specified
+\fIfilter\fR.
+A
+\fIfilter\fR
+expression is made up of one or more
+\fBkey =\fR \fIvalue\fR
+pairs, separated by a comma
+(\(oq\&,\(cq).
+The
+\fBkey\fR
+may be
+\(Lquser\(Rq,
+\(Lqgroup\(Rq
+or
+\(Lqhost\(Rq.
+For example,
+\fBuser\fR = \fIoperator\fR
+or
+\fBhost\fR = \fIwww\fR.
+.sp
+The password and group databases are not consulted when matching
+against the filter so the users and groups do not need to be present
+on the local system.
+Only aliases that are referenced by the filtered policy rules will
+be displayed.
+.TP 12n
+\fB\-o\fR \fIoutput_file\fR, \fB\--output\fR=\fIoutput_file\fR
+Write the converted output to
+\fIoutput_file\fR.
+If no
+\fIoutput_file\fR
+is specified, or if it is
+\(oq-\(cq,
+the converted
+\fIsudoers\fR
+policy will be written to the standard output.
+.TP 12n
 \fB\-O\fR \fIstart_point\fR, \fB\--order-start\fR=\fIstart_point\fR
 When generating LDIF output, use the number specified by
 \fIstart_point\fR
@@ -175,17 +214,6 @@ Defaults to a starting point of 1.
 A starting point of 0 will disable the generation of sudoOrder
 attributes in the resulting LDIF file.
 .TP 12n
-\fB\-o\fR \fIoutput_file\fR, \fB\--output\fR=\fIoutput_file\fR
-Write the converted output to
-\fIoutput_file\fR.
-If no
-\fIoutput_file\fR
-is specified, or if it is
-\(oq-\(cq,
-the converted
-\fIsudoers\fR
-policy will be written to the standard output.
-.TP 12n
 \fB\-V\fR, \fB\--version\fR
 Print the
 \fBcvtsudoers\fR
@@ -210,6 +238,11 @@ See the description of the
 \fB\-i\fR
 command line option.
 .TP 6n
+\fBmatch =\fR \fIfilter\fR
+See the description of the
+\fB\-m\fR
+command line option.
+.TP 6n
 \fBorder_increment =\fR \fIincrement\fR
 See the description of the
 \fB\-I\fR
diff --git a/doc/cvtsudoers.mdoc.in b/doc/cvtsudoers.mdoc.in
index 6a1df133d..81e492c1d 100644
--- a/doc/cvtsudoers.mdoc.in
+++ b/doc/cvtsudoers.mdoc.in
@@ -14,7 +14,7 @@
 .\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 .\" ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 .\"
-.Dd February 23, 2018
+.Dd March 21, 2018
 .Dt CVTSUDOERS @mansectsu@
 .Os Sudo @PACKAGE_VERSION@
 .Sh NAME
@@ -25,11 +25,12 @@
 .Op Fl ehV
 .Op Fl b Ar dn
 .Op Fl c Ar conf_file
-.Op Fl I Ar increment
-.Op Fl i Ar input_format
 .Op Fl f Ar output_format
-.Op Fl O Ar start_point
+.Op Fl i Ar input_format
+.Op Fl I Ar increment
+.Op Fl m Ar filter
 .Op Fl o Ar output_file
+.Op Fl O Ar start_point
 .Op Ar input_file
 .Sh DESCRIPTION
 .Nm
@@ -91,7 +92,7 @@ server for use with
 .Xr sudoers.ldap @mansectform@ .
 .Pp
 Conversion to LDIF has the following limitations:
-.Bl -bullet -width 4n
+.Bl -bullet -width 1n
 .It
 Command, host, runas and user-specific Defaults lines cannot be
 translated as they don't have an equivalent in the sudoers LDAP schema.
@@ -107,10 +108,6 @@ output inline.
 .El
 .It Fl h , Fl -help
 Display a short help message to the standard output and exit.
-.It Fl I Ar increment , Fl -increment Ns = Ns Ar increment
-When generating LDIF output, increment each sudoOrder attribute by
-the specified number.
-Defaults to an increment of 1.
 .It Fl i Ar input_format , Fl -input-format Ns = Ns Ar input_format
 Specify the input format.
 The following formats are supported:
@@ -127,6 +124,46 @@ LDIF to sudoers format.
 Traditional sudoers format.
 This is the default input format.
 .El
+.It Fl I Ar increment , Fl -increment Ns = Ns Ar increment
+When generating LDIF output, increment each sudoOrder attribute by
+the specified number.
+Defaults to an increment of 1.
+.It Fl m Ar filter , Fl -match Ns = Ns Ar filter
+Only output rules that match the specified
+.Ar filter .
+A
+.Ar filter
+expression is made up of one or more
+.Sy key = Ar value
+pairs, separated by a comma
+.Pq Ql \&, .
+The
+.Sy key
+may be
+.Dq user ,
+.Dq group
+or
+.Dq host .
+For example,
+.Sy user No = Ar operator
+or
+.Sy host No = Ar www .
+.Pp
+The password and group databases are not consulted when matching
+against the filter so the users and groups do not need to be present
+on the local system.
+Only aliases that are referenced by the filtered policy rules will
+be displayed.
+.It Fl o Ar output_file , Fl -output Ns = Ns Ar output_file
+Write the converted output to
+.Ar output_file .
+If no
+.Ar output_file
+is specified, or if it is
+.Ql - ,
+the converted
+.Em sudoers
+policy will be written to the standard output.
 .It Fl O Ar start_point , Fl -order-start Ns = Ns Ar start_point
 When generating LDIF output, use the number specified by
 .Ar start_point
@@ -139,16 +176,6 @@ option for details.
 Defaults to a starting point of 1.
 A starting point of 0 will disable the generation of sudoOrder
 attributes in the resulting LDIF file.
-.It Fl o Ar output_file , Fl -output Ns = Ns Ar output_file
-Write the converted output to
-.Ar output_file .
-If no
-.Ar output_file
-is specified, or if it is
-.Ql - ,
-the converted
-.Em sudoers
-policy will be written to the standard output.
 .It Fl V , -version
 Print the
 .Nm
@@ -172,6 +199,10 @@ command line option.
 See the description of the
 .Fl i
 command line option.
+.It Sy match = Ar filter
+See the description of the
+.Fl m
+command line option.
 .It Sy order_increment = Ar increment
 See the description of the
 .Fl I
diff --git a/plugins/sudoers/Makefile.in b/plugins/sudoers/Makefile.in
index 9bc6fd046..c540f1645 100644
--- a/plugins/sudoers/Makefile.in
+++ b/plugins/sudoers/Makefile.in
@@ -163,7 +163,8 @@ SUDOERS_OBJS = $(AUTH_OBJS) boottime.lo check.lo editor.lo env.lo \
 VISUDO_OBJS = editor.o find_path.o goodpath.o locale.o stubs.o sudo_printf.o visudo.o
 
 CVTSUDOERS_OBJS = cvtsudoers.o cvtsudoers_json.o cvtsudoers_ldif.o \
-		  fmtsudoers.o locale.o stubs.o sudo_printf.o ldap_util.o
+		  cvtsudoers_pwutil.o fmtsudoers.o locale.o stubs.o \
+		  sudo_printf.o ldap_util.o
 
 REPLAY_OBJS = getdate.o sudoreplay.o
 
@@ -747,14 +748,26 @@ cvtsudoers_ldif.o: $(srcdir)/cvtsudoers_ldif.c $(devdir)/def_data.h \
                    $(devdir)/gram.h $(incdir)/compat/stdbool.h \
                    $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \
                    $(incdir)/sudo_debug.h $(incdir)/sudo_fatal.h \
-                   $(incdir)/sudo_gettext.h $(incdir)/sudo_plugin.h \
-                   $(incdir)/sudo_queue.h $(incdir)/sudo_util.h \
-                   $(srcdir)/cvtsudoers.h $(srcdir)/defaults.h \
-                   $(srcdir)/logging.h $(srcdir)/parse.h $(srcdir)/redblack.h \
-                   $(srcdir)/sudo_ldap.h $(srcdir)/sudo_nss.h \
-                   $(srcdir)/sudoers.h $(srcdir)/sudoers_debug.h \
-                   $(top_builddir)/config.h $(top_builddir)/pathnames.h
+                   $(incdir)/sudo_gettext.h $(incdir)/sudo_lbuf.h \
+                   $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \
+                   $(incdir)/sudo_util.h $(srcdir)/cvtsudoers.h \
+                   $(srcdir)/defaults.h $(srcdir)/logging.h $(srcdir)/parse.h \
+                   $(srcdir)/redblack.h $(srcdir)/sudo_ldap.h \
+                   $(srcdir)/sudo_nss.h $(srcdir)/sudoers.h \
+                   $(srcdir)/sudoers_debug.h $(top_builddir)/config.h \
+                   $(top_builddir)/pathnames.h
 	$(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/cvtsudoers_ldif.c
+cvtsudoers_pwutil.o: $(srcdir)/cvtsudoers_pwutil.c $(devdir)/def_data.h \
+                     $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \
+                     $(incdir)/sudo_conf.h $(incdir)/sudo_debug.h \
+                     $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \
+                     $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \
+                     $(incdir)/sudo_util.h $(srcdir)/cvtsudoers.h \
+                     $(srcdir)/defaults.h $(srcdir)/logging.h \
+                     $(srcdir)/pwutil.h $(srcdir)/sudo_nss.h \
+                     $(srcdir)/sudoers.h $(srcdir)/sudoers_debug.h \
+                     $(top_builddir)/config.h $(top_builddir)/pathnames.h
+	$(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/cvtsudoers_pwutil.c
 dce.lo: $(authdir)/dce.c $(devdir)/def_data.h $(incdir)/compat/stdbool.h \
         $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h $(incdir)/sudo_debug.h \
         $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \
@@ -1321,10 +1334,10 @@ sudoreplay.o: $(srcdir)/sudoreplay.c $(incdir)/compat/getopt.h \
               $(top_builddir)/pathnames.h
 	$(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/sudoreplay.c
 testsudoers.o: $(srcdir)/testsudoers.c $(devdir)/def_data.h $(devdir)/gram.h \
-               $(incdir)/compat/fnmatch.h $(incdir)/compat/stdbool.h \
-               $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \
-               $(incdir)/sudo_debug.h $(incdir)/sudo_fatal.h \
-               $(incdir)/sudo_gettext.h $(incdir)/sudo_plugin.h \
+               $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \
+               $(incdir)/sudo_conf.h $(incdir)/sudo_debug.h \
+               $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \
+               $(incdir)/sudo_lbuf.h $(incdir)/sudo_plugin.h \
                $(incdir)/sudo_queue.h $(incdir)/sudo_util.h \
                $(srcdir)/defaults.h $(srcdir)/interfaces.h $(srcdir)/logging.h \
                $(srcdir)/parse.h $(srcdir)/sudo_nss.h $(srcdir)/sudoers.h \
diff --git a/plugins/sudoers/alias.c b/plugins/sudoers/alias.c
index b8e70fc7a..efd380f7e 100644
--- a/plugins/sudoers/alias.c
+++ b/plugins/sudoers/alias.c
@@ -39,7 +39,7 @@
 /*
  * Globals
  */
-struct rbtree *aliases;
+static struct rbtree *aliases;
 
 /*
  * Comparison function for the red-black tree.
@@ -166,6 +166,20 @@ no_aliases(void)
     debug_return_bool(rbisempty(aliases));
 }
 
+/*
+ * Replace the aliases tree with a new one, returns the old.
+ */
+struct rbtree *
+replace_aliases(struct rbtree *new_aliases)
+{
+    struct rbtree *old_aliases = aliases;
+    debug_decl(replace_aliases, SUDOERS_DEBUG_ALIAS)
+
+    aliases = new_aliases;
+
+    debug_return_ptr(old_aliases);
+}
+
 /*
  * Free memory used by an alias struct and its members.
  */
diff --git a/plugins/sudoers/cvtsudoers.c b/plugins/sudoers/cvtsudoers.c
index e3fd02153..cbbc0803e 100644
--- a/plugins/sudoers/cvtsudoers.c
+++ b/plugins/sudoers/cvtsudoers.c
@@ -32,6 +32,7 @@
 #include <ctype.h>
 #include <errno.h>
 #include <fcntl.h>
+#include <pwd.h>
 #include <unistd.h>
 
 #include "sudoers.h"
@@ -39,6 +40,7 @@
 #include "sudoers_version.h"
 #include "sudo_conf.h"
 #include "sudo_lbuf.h"
+#include "redblack.h"
 #include "cvtsudoers.h"
 #include <gram.h>
 
@@ -51,9 +53,10 @@
 /*
  * Globals
  */
+struct cvtsudoers_filter *filters;
 struct sudo_user sudo_user;
 struct passwd *list_pw;
-static const char short_opts[] =  "b:c:ef:hi:I:o:O:V";
+static const char short_opts[] =  "b:c:ef:hi:I:m:o:O:V";
 static struct option long_opts[] = {
     { "base",		required_argument,	NULL,	'b' },
     { "config",		required_argument,	NULL,	'c' },
@@ -62,6 +65,7 @@ static struct option long_opts[] = {
     { "help",		no_argument,		NULL,	'h' },
     { "input-format",	required_argument,	NULL,	'i' },
     { "increment",	required_argument,	NULL,	'I' },
+    { "match",		required_argument,	NULL,	'm' },
     { "order-start",	required_argument,	NULL,	'O' },
     { "output",		required_argument,	NULL,	'o' },
     { "version",	no_argument,		NULL,	'V' },
@@ -73,8 +77,12 @@ static void help(void) __attribute__((__noreturn__));
 static void usage(int);
 static bool convert_sudoers_sudoers(const char *output_file, struct cvtsudoers_config *conf);
 static bool parse_sudoers(const char *input_file, struct cvtsudoers_config *conf);
+static bool parse_filter(char *expression);
+static bool alias_remove_unused(void);
 static struct cvtsudoers_config *cvtsudoers_conf_read(const char *conf_file);
 static void cvtsudoers_conf_free(struct cvtsudoers_config *conf);
+static void filter_userspecs(void);
+static void filter_defaults(void);
 
 int
 main(int argc, char *argv[])
@@ -174,6 +182,9 @@ main(int argc, char *argv[])
 		usage(1);
 	    }
 	    break;
+	case 'm':
+	    conf->filter = optarg;
+	    break;
 	case 'o':
 	    output_file = optarg;
 	    break;
@@ -223,6 +234,11 @@ main(int argc, char *argv[])
 	    usage(1);
 	}
     }
+    if (conf->filter != NULL) {
+	/* We always expand aliases when filtering (may change in future). */
+	if (!parse_filter(conf->filter))
+	    usage(1);
+    }
 
     /* If no base DN specified, check SUDOERS_BASE. */
     if (conf->sudoers_base == NULL) {
@@ -269,6 +285,15 @@ main(int argc, char *argv[])
 	sudo_fatalx("error: unhandled input %d", input_format);
     }
 
+    /* Apply filters. */
+    if (conf->filter != NULL) {
+	    filter_userspecs();
+
+	    filter_defaults();
+
+	    alias_remove_unused();
+    }
+
     switch (output_format) {
     case format_json:
 	exitcode = !convert_sudoers_json(output_file, conf);
@@ -299,6 +324,7 @@ static struct cvtsudoers_conf_table cvtsudoers_conf_vars[] = {
     { "sudoers_base", CONF_STR, &cvtsudoers_config.sudoers_base },
     { "input_format", CONF_STR, &cvtsudoers_config.input_format },
     { "output_format", CONF_STR, &cvtsudoers_config.output_format },
+    { "match", CONF_STR, &cvtsudoers_config.filter },
     { "expand_aliases", CONF_BOOL, &cvtsudoers_config.expand_aliases }
 };
 
@@ -410,6 +436,60 @@ cvtsudoers_conf_free(struct cvtsudoers_config *conf)
     debug_return;
 }
 
+static bool
+parse_filter(char *expression)
+{
+    char *last = NULL, *cp = expression;
+    debug_decl(parse_filter, SUDOERS_DEBUG_UTIL)
+
+    if (filters == NULL) {
+	if ((filters = malloc(sizeof(*filters))) == NULL) {
+	    sudo_fatalx(U_("%s: %s"), __func__,
+		U_("unable to allocate memory"));
+	}
+	STAILQ_INIT(&filters->users);
+	STAILQ_INIT(&filters->groups);
+	STAILQ_INIT(&filters->hosts);
+    }
+
+    for ((cp = strtok_r(cp, ",", &last)); cp != NULL; (cp = strtok_r(NULL, ",", &last))) {
+	/*
+	 * Filter expression:
+	 *	user=foo,group=bar,host=baz
+	 */
+	char *keyword;
+	struct cvtsudoers_string *s;
+
+	if ((s = malloc(sizeof(*s))) == NULL) {
+	    sudo_fatalx(U_("%s: %s"), __func__,
+		U_("unable to allocate memory"));
+	}
+
+	/* Parse keyword = value */
+	keyword = cp;
+	if ((cp = strchr(cp, '=')) == NULL) {
+	    sudo_warnx(U_("invalid filter: %s"), keyword);;
+	    debug_return_bool(false);
+	}
+	*cp++ = '\0';
+	s->str = cp;
+
+	if (strcmp(keyword, "user") == 0 ){
+	    STAILQ_INSERT_TAIL(&filters->users, s, entries);
+	} else if (strcmp(keyword, "group") == 0 ){
+	    STAILQ_INSERT_TAIL(&filters->groups, s, entries);
+	} else if (strcmp(keyword, "host") == 0 ){
+	    STAILQ_INSERT_TAIL(&filters->hosts, s, entries);
+	} else {
+	    sudo_warnx(U_("invalid filter: %s"), keyword);;
+	    free(s);
+	    debug_return_bool(false);
+	}
+    }
+
+    debug_return_bool(true);
+}
+
 static bool
 parse_sudoers(const char *input_file, struct cvtsudoers_config *conf)
 {
@@ -446,6 +526,89 @@ open_sudoers(const char *sudoers, bool doedit, bool *keepopen)
     return fopen(sudoers, "r");
 }
 
+bool
+userlist_matches_filter(struct member_list *userlist)
+{
+    struct cvtsudoers_string *s;
+    bool matches = false;
+    debug_decl(userlist_matches_filter, SUDOERS_DEBUG_UTIL)
+
+    if (filters == NULL ||
+	(STAILQ_EMPTY(&filters->users) && STAILQ_EMPTY(&filters->groups)))
+	debug_return_bool(true);
+
+    if (STAILQ_EMPTY(&filters->users)) {
+	struct passwd pw;
+
+	/*
+	 * Only groups in filter, make a dummy user so userlist_matches()
+	 * can do its thing.
+	 */
+	memset(&pw, 0, sizeof(pw));
+	pw.pw_name = "_nobody";
+	pw.pw_uid = (uid_t)-1;
+	pw.pw_gid = (gid_t)-1;
+	if (userlist_matches(&pw, userlist) == true)
+	    matches = true;
+    }
+
+    STAILQ_FOREACH(s, &filters->users, entries) {
+	struct passwd *pw = NULL;
+        if (s->str[0] == '#') {
+	    const char *errstr;
+            uid_t uid = sudo_strtoid(s->str + 1, NULL, NULL, &errstr);
+            if (errstr == NULL)
+                pw = sudo_getpwuid(uid);
+	}
+	if (pw == NULL)
+	    pw = sudo_getpwnam(s->str);
+	if (pw == NULL)
+	    continue;
+	if (userlist_matches(pw, userlist) == true)
+	    matches = true;
+	sudo_pw_delref(pw);
+	if (matches)
+	    break;
+    }
+
+    debug_return_bool(matches);
+}
+
+bool
+hostlist_matches_filter(struct member_list *hostlist)
+{
+    struct cvtsudoers_string *s;
+    bool matches = false;
+    debug_decl(hostlist_matches_filter, SUDOERS_DEBUG_UTIL)
+
+    if (filters == NULL || STAILQ_EMPTY(&filters->hosts))
+	debug_return_bool(true);
+
+    STAILQ_FOREACH(s, &filters->hosts, entries) {
+	user_runhost = s->str;
+	if ((user_srunhost = strchr(user_runhost, '.')) != NULL) {
+	    user_srunhost = strndup(user_runhost,
+		(size_t)(user_srunhost - user_runhost));
+	    if (user_srunhost == NULL) {
+		sudo_fatalx(U_("%s: %s"), __func__,
+		    U_("unable to allocate memory"));
+	    }
+	} else {
+	    user_srunhost = user_runhost;
+	}
+	/* XXX - can't use netgroup_tuple with NULL pw */
+	if (hostlist_matches(NULL, hostlist) == true)
+	    matches = true;
+	if (user_srunhost != user_runhost)
+	    free(user_srunhost);
+	user_runhost = user_host;
+	user_srunhost = user_shost;
+	if (matches)
+	    break;
+    }
+    debug_return_bool(matches);
+}
+
 /*
  * Display Defaults entries
  */
@@ -502,6 +665,195 @@ convert_sudoers_output(const char *buf)
     return fputs(buf, output_fp);
 }
 
+/*
+ * Apply filters to userspecs, removing non-matching entries.
+ */
+static void
+filter_userspecs(void)
+{
+    struct userspec *us, *next_us;
+    struct privilege *priv, *next_priv;
+    debug_decl(filter_userspecs, SUDOERS_DEBUG_UTIL)
+
+    /*
+     * Does not currently prune out non-matching entries in the user or
+     * host lists.  It acts more like a grep than a true filter.
+     * In the future, we may want to add a prune option.
+     */
+    TAILQ_FOREACH_SAFE(us, &userspecs, entries, next_us) {
+	if (!userlist_matches_filter(&us->users)) {
+	    TAILQ_REMOVE(&userspecs, us, entries);
+	    free_userspec(us);
+	    continue;
+	}
+	TAILQ_FOREACH_SAFE(priv, &us->privileges, entries, next_priv) {
+	    if (!hostlist_matches_filter(&priv->hostlist)) {
+		TAILQ_REMOVE(&us->privileges, priv, entries);
+		free_privilege(priv);
+	    }
+	}
+	if (TAILQ_EMPTY(&us->privileges)) {
+	    TAILQ_REMOVE(&userspecs, us, entries);
+	    free_userspec(us);
+	    continue;
+	}
+    }
+    debug_return;
+}
+
+/*
+ * Apply filters to host/user-based Defaults, removing non-matching entries.
+ */
+static void
+filter_defaults(void)
+{
+    struct defaults *def, *next;
+    struct member_list *binding = NULL;
+    debug_decl(filter_defaults, SUDOERS_DEBUG_DEFAULTS)
+
+    TAILQ_FOREACH_SAFE(def, &defaults, entries, next) {
+	switch (def->type) {
+	case DEFAULTS_USER:
+	    if (!userlist_matches_filter(def->binding)) {
+		TAILQ_REMOVE(&defaults, def, entries);
+		binding = free_default(def, binding);
+	    } else {
+		binding = def->binding;
+	    }
+	    break;
+	case DEFAULTS_HOST:
+	    if (!hostlist_matches_filter(def->binding)) {
+		TAILQ_REMOVE(&defaults, def, entries);
+		binding = free_default(def, binding);
+	    } else {
+		binding = def->binding;
+	    }
+	    break;
+	default:
+	    break;
+	}
+    }
+    debug_return;
+}
+
+/*
+ * Remove the alias of the specified type as well as any other aliases
+ * referenced by that alias.
+ * XXX - share with visudo
+ */
+static bool
+alias_remove_recursive(char *name, int type, struct rbtree *freelist)
+{
+    struct member *m;
+    struct alias *a;
+    bool ret = true;
+    debug_decl(alias_remove_recursive, SUDOERS_DEBUG_ALIAS)
+
+    if ((a = alias_remove(name, type)) != NULL) {
+	TAILQ_FOREACH(m, &a->members, entries) {
+	    if (m->type == ALIAS) {
+		if (!alias_remove_recursive(m->name, type, freelist))
+		    ret = false;
+	    }
+	}
+	if (rbinsert(freelist, a, NULL) != 0)
+	    ret = false;
+    }
+    debug_return_bool(ret);
+}
+
+/*
+ * Remove unreferenced aliases.
+ * XXX - share with visudo
+ */
+static bool
+alias_remove_unused(void)
+{
+    struct cmndspec *cs;
+    struct member *m;
+    struct privilege *priv;
+    struct userspec *us;
+    struct defaults *d;
+    int atype, errors = 0;
+    struct rbtree *used_aliases;
+    struct rbtree *unused_aliases;
+    debug_decl(alias_remove_unused, SUDOERS_DEBUG_ALIAS)
+
+    used_aliases = rbcreate(alias_compare);
+    if (used_aliases == NULL) {
+	sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
+	debug_return_int(-1);
+    }
+
+    /* Move referenced aliases to used_aliases. */
+    TAILQ_FOREACH(us, &userspecs, entries) {
+	TAILQ_FOREACH(m, &us->users, entries) {
+	    if (m->type == ALIAS) {
+		if (!alias_remove_recursive(m->name, USERALIAS, used_aliases))
+		    errors++;
+	    }
+	}
+	TAILQ_FOREACH(priv, &us->privileges, entries) {
+	    TAILQ_FOREACH(m, &priv->hostlist, entries) {
+		if (m->type == ALIAS) {
+		    if (!alias_remove_recursive(m->name, HOSTALIAS, used_aliases))
+			errors++;
+		}
+	    }
+	    TAILQ_FOREACH(cs, &priv->cmndlist, entries) {
+		if (cs->runasuserlist != NULL) {
+		    TAILQ_FOREACH(m, cs->runasuserlist, entries) {
+			if (m->type == ALIAS) {
+			    if (!alias_remove_recursive(m->name, RUNASALIAS, used_aliases))
+				errors++;
+			}
+		    }
+		}
+		if (cs->runasgrouplist != NULL) {
+		    TAILQ_FOREACH(m, cs->runasgrouplist, entries) {
+			if (m->type == ALIAS) {
+			    if (!alias_remove_recursive(m->name, RUNASALIAS, used_aliases))
+				errors++;
+			}
+		    }
+		}
+		if ((m = cs->cmnd)->type == ALIAS) {
+		    if (!alias_remove_recursive(m->name, CMNDALIAS, used_aliases))
+			errors++;
+		}
+	    }
+	}
+    }
+    TAILQ_FOREACH(d, &defaults, entries) {
+	switch (d->type) {
+	    case DEFAULTS_HOST:
+		atype = HOSTALIAS;
+		break;
+	    case DEFAULTS_USER:
+		atype = USERALIAS;
+		break;
+	    case DEFAULTS_RUNAS:
+		atype = RUNASALIAS;
+		break;
+	    case DEFAULTS_CMND:
+		atype = CMNDALIAS;
+		break;
+	    default:
+		continue; /* not an alias */
+	}
+	TAILQ_FOREACH(m, d->binding, entries) {
+	    if (m->type == ALIAS) {
+		if (!alias_remove_recursive(m->name, atype, used_aliases))
+		    errors++;
+	    }
+	}
+    }
+    unused_aliases = replace_aliases(used_aliases);
+    rbdestroy(unused_aliases, alias_free);
+
+    debug_return_int(errors ? false : true);
+}
+
 /*
  * Convert back to sudoers.
  */
@@ -571,7 +923,7 @@ usage(int fatal)
 {
     (void) fprintf(fatal ? stderr : stdout, "usage: %s [-ehV] [-b dn] "
 	"[-c conf_file ] [-f output_format] [-i input_format] [-I increment] "
-	"[-o output_file] [-O start_point] [input_file]\n",
+	"[-m filter] [-o output_file] [-O start_point] [input_file]\n",
 	getprogname());
     if (fatal)
 	exit(1);
@@ -586,11 +938,12 @@ help(void)
 	"  -b, --base=dn              the base DN for sudo LDAP queries\n"
 	"  -e, --expand-aliases       expand aliases when converting\n"
 	"  -f, --output-format=format set output format: JSON, LDIF or sudoers\n"
-	"  -I, --increment=num        amount to increase each sudoOrder by\n"
 	"  -i, --input-format=format  set input format: LDIF or sudoers\n"
+	"  -I, --increment=num        amount to increase each sudoOrder by\n"
 	"  -h, --help                 display help message and exit\n"
-	"  -O, --order-start=num      starting point for first sudoOrder\n"
+	"  -m, --match=filter         only convert entries that match the filter expression\n"
 	"  -o, --output=output_file   write converted sudoers to output_file\n"
+	"  -O, --order-start=num      starting point for first sudoOrder\n"
 	"  -V, --version              display version information and exit"));
     exit(0);
 }
diff --git a/plugins/sudoers/cvtsudoers.h b/plugins/sudoers/cvtsudoers.h
index f24f15472..efce454fd 100644
--- a/plugins/sudoers/cvtsudoers.h
+++ b/plugins/sudoers/cvtsudoers.h
@@ -24,11 +24,26 @@ enum sudoers_formats {
     format_sudoers
 };
 
+/*
+ * Simple string list with optional reference count.
+ * XXX - move this so fmtsudoers can use it
+ */
+struct cvtsudoers_string {
+    STAILQ_ENTRY(cvtsudoers_string) entries;
+    char *str;
+};
+struct cvtsudoers_str_list {
+    struct cvtsudoers_string *stqh_first;
+    struct cvtsudoers_string **stqh_last;
+    unsigned int refcnt;
+};
+
 /* cvtsudoers.conf settings */
 struct cvtsudoers_config {
     char *sudoers_base;
     char *input_format;
     char *output_format;
+    char *filter;
     unsigned int sudo_order;
     unsigned int order_increment;
     bool expand_aliases;
@@ -36,7 +51,7 @@ struct cvtsudoers_config {
 };
 
 /* Initial config settings for above. */
-#define INITIAL_CONFIG { NULL, NULL, NULL, 1, 1, false, true }
+#define INITIAL_CONFIG { NULL, NULL, NULL, NULL, 1, 1, false, true }
 
 #define CONF_BOOL	0
 #define CONF_UINT	1
@@ -48,9 +63,27 @@ struct cvtsudoers_conf_table {
     void *valp;			/* pointer into cvtsudoers_config */
 };
 
+struct cvtsudoers_filter {
+    struct cvtsudoers_str_list users;
+    struct cvtsudoers_str_list groups;
+    struct cvtsudoers_str_list hosts;
+};
+
 bool convert_sudoers_json(const char *output_file, struct cvtsudoers_config *conf);
 bool convert_sudoers_ldif(const char *output_file, struct cvtsudoers_config *conf);
 bool parse_ldif(const char *input_file, struct cvtsudoers_config *conf);
 void get_hostname(void);
 
+struct member_list;
+struct userspec_list;
+bool userlist_matches_filter(struct member_list *userlist);
+bool hostlist_matches_filter(struct member_list *hostlist);
+
+struct cvtsudoers_str_list *str_list_alloc(void);
+void str_list_free(void *v);
+struct cvtsudoers_string *cvtsudoers_string_alloc(const char *s);
+void cvtsudoers_string_free(struct cvtsudoers_string *ls);
+
+extern struct cvtsudoers_filter *filters;
+
 #endif /* SUDOERS_CVTSUDOERS_H */
diff --git a/plugins/sudoers/cvtsudoers_ldif.c b/plugins/sudoers/cvtsudoers_ldif.c
index 3209a03e7..c9ef0731f 100644
--- a/plugins/sudoers/cvtsudoers_ldif.c
+++ b/plugins/sudoers/cvtsudoers_ldif.c
@@ -465,7 +465,7 @@ print_userspecs_ldif(FILE *fp, struct cvtsudoers_config *conf)
 {
     struct userspec *us;
     debug_decl(print_userspecs_ldif, SUDOERS_DEBUG_UTIL)
-
+ 
     TAILQ_FOREACH(us, &userspecs, entries) {
 	if (!print_userspec_ldif(fp, us, conf))
 	    debug_return_bool(false);
@@ -513,36 +513,27 @@ convert_sudoers_ldif(const char *output_file, struct cvtsudoers_config *conf)
     debug_return_bool(ret);
 }
 
-struct ldif_string {
-    STAILQ_ENTRY(ldif_string) entries;
-    char *str;
-};
-struct ldif_str_list {
-    struct ldif_string *stqh_first;
-    struct ldif_string **stqh_last;
-    unsigned int refcnt;
-};
-
 struct sudo_role {
     STAILQ_ENTRY(sudo_role) entries;
     char *cn;
     char *notbefore;
     char *notafter;
     double order;
-    struct ldif_str_list *cmnds;
-    struct ldif_str_list *hosts;
-    struct ldif_str_list *users;
-    struct ldif_str_list *runasusers;
-    struct ldif_str_list *runasgroups;
-    struct ldif_str_list *options;
+    struct cvtsudoers_str_list *cmnds;
+    struct cvtsudoers_str_list *hosts;
+    struct cvtsudoers_str_list *users;
+    struct cvtsudoers_str_list *runasusers;
+    struct cvtsudoers_str_list *runasgroups;
+    struct cvtsudoers_str_list *options;
 };
 STAILQ_HEAD(sudo_role_list, sudo_role);
 
-static struct ldif_string *
-ldif_string_alloc(const char *s)
+/* XXX - move to cvtsudoers.c */
+struct cvtsudoers_string *
+cvtsudoers_string_alloc(const char *s)
 {
-    struct ldif_string *ls;
-    debug_decl(ldif_string_alloc, SUDOERS_DEBUG_UTIL)
+    struct cvtsudoers_string *ls;
+    debug_decl(cvtsudoers_string_alloc, SUDOERS_DEBUG_UTIL)
 
     if ((ls = malloc(sizeof(*ls))) != NULL) {
 	if ((ls->str = strdup(s)) == NULL) {
@@ -554,17 +545,17 @@ ldif_string_alloc(const char *s)
     debug_return_ptr(ls);
 }
 
-static void
-ldif_string_free(struct ldif_string *ls)
+void
+cvtsudoers_string_free(struct cvtsudoers_string *ls)
 {
     free(ls->str);
     free(ls);
 }
 
-static struct ldif_str_list *
+struct cvtsudoers_str_list *
 str_list_alloc(void)
 {
-    struct ldif_str_list *strlist;
+    struct cvtsudoers_str_list *strlist;
     debug_decl(str_list_alloc, SUDOERS_DEBUG_UTIL)
 
     strlist = malloc(sizeof(*strlist));
@@ -574,17 +565,17 @@ str_list_alloc(void)
     debug_return_ptr(strlist);
 }
 
-static void
+void
 str_list_free(void *v)
 {
-    struct ldif_str_list *strlist = v;
-    struct ldif_string *first;
+    struct cvtsudoers_str_list *strlist = v;
+    struct cvtsudoers_string *first;
     debug_decl(str_list_free, SUDOERS_DEBUG_UTIL)
 
     if (--strlist->refcnt == 0) {
 	while ((first = STAILQ_FIRST(strlist)) != NULL) {
 	    STAILQ_REMOVE_HEAD(strlist, entries);
-	    ldif_string_free(first);
+	    cvtsudoers_string_free(first);
 	}
 	free(strlist);
     }
@@ -644,25 +635,25 @@ sudo_role_free(struct sudo_role *role)
 }
 
 /*
- * Allocate a struct ldif_string, store str in it and
+ * Allocate a struct cvtsudoers_string, store str in it and
  * insert into the specified strlist.
  */
 static void
-ldif_store_string(const char *str, struct ldif_str_list *strlist, bool sorted)
+ldif_store_string(const char *str, struct cvtsudoers_str_list *strlist, bool sorted)
 {
-    struct ldif_string *ls;
+    struct cvtsudoers_string *ls;
     debug_decl(ldif_store_string, SUDOERS_DEBUG_UTIL)
 
     while (isblank((unsigned char)*str))
 	str++;
-    if ((ls = ldif_string_alloc(str)) == NULL) {
+    if ((ls = cvtsudoers_string_alloc(str)) == NULL) {
 	sudo_fatalx(U_("%s: %s"), __func__,
 	    U_("unable to allocate memory"));
     }
     if (!sorted) {
 	STAILQ_INSERT_TAIL(strlist, ls, entries);
     } else {
-	struct ldif_string *prev, *next;
+	struct cvtsudoers_string *prev, *next;
 
 	/* Insertion sort, list is small. */
 	prev = STAILQ_FIRST(strlist);
@@ -683,13 +674,13 @@ ldif_store_string(const char *str, struct ldif_str_list *strlist, bool sorted)
 
 /*
  * Iterator for sudo_ldap_role_to_priv().
- * Takes a pointer to a struct ldif_string *.
+ * Takes a pointer to a struct cvtsudoers_string *.
  * Returns the string or NULL if we've reached the end.
  */
-static char *
-ldif_string_iter(void **vp)
+char *
+cvtsudoers_string_iter(void **vp)
 {
-    struct ldif_string *ls = *vp;
+    struct cvtsudoers_string *ls = *vp;
 
     if (ls == NULL)
 	return NULL;
@@ -714,10 +705,10 @@ role_order_cmp(const void *va, const void *vb)
  * Parse list of sudoOption and store in global defaults list.
  */
 static void
-ldif_store_options(struct ldif_str_list *options)
+ldif_store_options(struct cvtsudoers_str_list *options)
 {
     struct defaults *d;
-    struct ldif_string *ls;
+    struct cvtsudoers_string *ls;
     char *var, *val;
     debug_decl(ldif_store_options, SUDOERS_DEBUG_UTIL)
 
@@ -748,10 +739,10 @@ ldif_store_options(struct ldif_str_list *options)
 static int
 str_list_cmp(const void *aa, const void *bb)
 {
-    const struct ldif_str_list *a = aa;
-    const struct ldif_str_list *b = bb;
-    const struct ldif_string *lsa = STAILQ_FIRST(a);
-    const struct ldif_string *lsb = STAILQ_FIRST(b);
+    const struct cvtsudoers_str_list *a = aa;
+    const struct cvtsudoers_str_list *b = bb;
+    const struct cvtsudoers_string *lsa = STAILQ_FIRST(a);
+    const struct cvtsudoers_string *lsb = STAILQ_FIRST(b);
     int ret;
 
     while (lsa != NULL && lsb != NULL) {
@@ -764,9 +755,9 @@ str_list_cmp(const void *aa, const void *bb)
 }
 
 static int
-str_list_cache(struct rbtree *cache, struct ldif_str_list **strlistp)
+str_list_cache(struct rbtree *cache, struct cvtsudoers_str_list **strlistp)
 {
-    struct ldif_str_list *strlist = *strlistp;
+    struct cvtsudoers_str_list *strlist = *strlistp;
     struct rbnode *node;
     int ret;
     debug_decl(str_list_cache, SUDOERS_DEBUG_UTIL)
@@ -797,7 +788,7 @@ role_to_sudoers(struct sudo_role *role, bool store_options,
     bool reuse_userspec, bool reuse_privilege, bool reuse_runas)
 {
     struct privilege *priv;
-    struct ldif_string *ls;
+    struct cvtsudoers_string *ls;
     struct userspec *us;
     struct member *m;
     debug_decl(role_to_sudoers, SUDOERS_DEBUG_UTIL)
@@ -882,7 +873,7 @@ role_to_sudoers(struct sudo_role *role, bool store_options,
 	STAILQ_FIRST(role->runasusers), STAILQ_FIRST(role->runasgroups),
 	STAILQ_FIRST(role->cmnds), STAILQ_FIRST(role->options),
 	role->notbefore, role->notafter, true, store_options,
-	ldif_string_iter);
+	cvtsudoers_string_iter);
     if (priv == NULL) {
 	sudo_fatalx(U_("%s: %s"), __func__,
 	    U_("unable to allocate memory"));
diff --git a/plugins/sudoers/cvtsudoers_pwutil.c b/plugins/sudoers/cvtsudoers_pwutil.c
new file mode 100644
index 000000000..4f8de07b5
--- /dev/null
+++ b/plugins/sudoers/cvtsudoers_pwutil.c
@@ -0,0 +1,479 @@
+/*
+ * Copyright (c) 1996, 1998-2005, 2007-2017
+ *	Todd C. Miller <Todd.Miller@sudo.ws>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ *
+ * Sponsored in part by the Defense Advanced Research Projects
+ * Agency (DARPA) and Air Force Research Laboratory, Air Force
+ * Materiel Command, USAF, under agreement number F39502-99-1-0512.
+ */
+
+#include <config.h>
+
+#include <sys/types.h>
+#include <stdio.h>
+#include <stdlib.h>
+#ifdef HAVE_STRING_H
+# include <string.h>
+#endif /* HAVE_STRING_H */
+#ifdef HAVE_STRINGS_H
+# include <strings.h>
+#endif /* HAVE_STRINGS_H */
+#include <unistd.h>
+#include <errno.h>
+#include <limits.h>
+#include <pwd.h>
+#include <grp.h>
+
+#include "sudoers.h"
+#include "cvtsudoers.h"
+#include "pwutil.h"
+
+#ifndef LOGIN_NAME_MAX
+# ifdef _POSIX_LOGIN_NAME_MAX
+#  define LOGIN_NAME_MAX _POSIX_LOGIN_NAME_MAX
+# else
+#  define LOGIN_NAME_MAX 9
+# endif
+#endif /* LOGIN_NAME_MAX */
+
+#define FIELD_SIZE(src, name, size)			\
+do {							\
+	if ((src)->name) {				\
+		size = strlen((src)->name) + 1;		\
+		total += size;				\
+	}                                               \
+} while (0)
+
+#define FIELD_COPY(src, dst, name, size)		\
+do {							\
+	if ((src)->name) {				\
+		memcpy(cp, (src)->name, size);		\
+		(dst)->name = cp;				\
+		cp += size;				\
+	}						\
+} while (0)
+
+/*
+ * Dynamically allocate space for a struct item plus the key and data
+ * elements.  If name is non-NULL it is used as the key, else the
+ * uid is the key.  Fills in datum from struct password.
+ * Returns NULL on calloc error or unknown name/id, setting errno
+ * to ENOMEM or ENOENT respectively.
+ */
+struct cache_item *
+sudo_make_pwitem(uid_t uid, const char *name)
+{
+    char *cp, uidstr[MAX_UID_T_LEN + 2];
+    size_t nsize, psize, csize, gsize, dsize, ssize, total;
+    struct cache_item_pw *pwitem;
+    struct passwd pw, *newpw;
+    struct cvtsudoers_string *s = NULL;
+    debug_decl(sudo_make_pwitem, SUDOERS_DEBUG_NSS)
+
+    /* Look up name or uid in filter list. */
+    if (name != NULL) {
+	STAILQ_FOREACH(s, &filters->users, entries) {
+	    if (strcasecmp(name, s->str) == 0) {
+		uid = (uid_t)-1;
+		break;
+	    }
+	}
+    } else {
+	STAILQ_FOREACH(s, &filters->users, entries) {
+	    const char *errstr;
+	    uid_t filter_uid;
+
+	    if (s->str[0] != '#')
+		continue;
+
+	    filter_uid = sudo_strtoid(s->str + 1, NULL, NULL, &errstr);
+	    if (errstr == NULL) {
+		if (uid != filter_uid)
+		    continue;
+		snprintf(uidstr, sizeof(uidstr), "#%u", (unsigned int)uid);
+		break;
+	    }
+	}
+    }
+    if (s == NULL) {
+	errno = ENOENT;
+	debug_return_ptr(NULL);
+    }
+
+    /* Fake up a passwd struct. */
+    memset(&pw, 0, sizeof(pw));
+    pw.pw_name = name ? s->str : uidstr;
+    pw.pw_passwd = "*";
+    pw.pw_uid = uid;
+    pw.pw_gid = (gid_t)-1;
+    pw.pw_shell = _PATH_BSHELL;
+    pw.pw_dir = "/";
+
+    /* Allocate in one big chunk for easy freeing. */
+    nsize = psize = csize = gsize = dsize = ssize = 0;
+    total = sizeof(*pwitem);
+    FIELD_SIZE(&pw, pw_name, nsize);
+    FIELD_SIZE(&pw, pw_passwd, psize);
+#ifdef HAVE_LOGIN_CAP_H
+    FIELD_SIZE(&pw, pw_class, csize);
+#endif
+    FIELD_SIZE(&pw, pw_gecos, gsize);
+    FIELD_SIZE(&pw, pw_dir, dsize);
+    FIELD_SIZE(&pw, pw_shell, ssize);
+    if (name != NULL)
+	total += strlen(name) + 1;
+
+    /* Allocate space for struct item, struct passwd and the strings. */
+    if ((pwitem = calloc(1, total)) == NULL) {
+	sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+	    "unable to allocate memory");
+	debug_return_ptr(NULL);
+    }
+    newpw = &pwitem->pw;
+
+    /*
+     * Copy in passwd contents and make strings relative to space
+     * at the end of the struct.
+     */
+    memcpy(newpw, &pw, sizeof(pw));
+    cp = (char *)(pwitem + 1);
+    FIELD_COPY(&pw, newpw, pw_name, nsize);
+    FIELD_COPY(&pw, newpw, pw_passwd, psize);
+#ifdef HAVE_LOGIN_CAP_H
+    FIELD_COPY(&pw, newpw, pw_class, csize);
+#endif
+    FIELD_COPY(&pw, newpw, pw_gecos, gsize);
+    FIELD_COPY(&pw, newpw, pw_dir, dsize);
+    FIELD_COPY(&pw, newpw, pw_shell, ssize);
+
+    /* Set key and datum. */
+    if (name != NULL) {
+	memcpy(cp, name, strlen(name) + 1);
+	pwitem->cache.k.name = cp;
+    } else {
+	pwitem->cache.k.uid = pw.pw_uid;
+    }
+    pwitem->cache.d.pw = newpw;
+    pwitem->cache.refcnt = 1;
+
+    debug_return_ptr(&pwitem->cache);
+}
+
+/*
+ * Dynamically allocate space for a struct item plus the key and data
+ * elements.  If name is non-NULL it is used as the key, else the
+ * gid is the key.  Fills in datum from struct group.
+ * Returns NULL on calloc error or unknown name/id, setting errno
+ * to ENOMEM or ENOENT respectively.
+ */
+struct cache_item *
+sudo_make_gritem(gid_t gid, const char *name)
+{
+    char *cp, gidstr[MAX_UID_T_LEN + 2];
+    size_t nsize, psize, nmem, total, len;
+    struct cache_item_gr *gritem;
+    struct group gr, *newgr;
+    struct cvtsudoers_string *s = NULL;
+    debug_decl(sudo_make_gritem, SUDOERS_DEBUG_NSS)
+
+    /* Look up name or gid in filter list. */
+    if (name != NULL) {
+	STAILQ_FOREACH(s, &filters->users, entries) {
+	    if (strcasecmp(name, s->str) == 0) {
+		gid = (gid_t)-1;
+		break;
+	    }
+	}
+    } else {
+	STAILQ_FOREACH(s, &filters->users, entries) {
+	    const char *errstr;
+	    gid_t filter_gid;
+
+	    if (s->str[0] != '#')
+		continue;
+
+	    filter_gid = sudo_strtoid(s->str + 1, NULL, NULL, &errstr);
+	    if (errstr == NULL) {
+		if (gid != filter_gid)
+		    continue;
+		snprintf(gidstr, sizeof(gidstr), "#%u", (unsigned int)gid);
+		break;
+	    }
+	}
+    }
+    if (s == NULL) {
+	errno = ENOENT;
+	debug_return_ptr(NULL);
+    }
+
+    /* Fake up a group struct with all filter users as members. */
+    memset(&gr, 0, sizeof(gr));
+    gr.gr_name = name ? s->str : gidstr;
+    gr.gr_gid = gid;
+
+    /* Allocate in one big chunk for easy freeing. */
+    nsize = psize = nmem = 0;
+    total = sizeof(*gritem);
+    FIELD_SIZE(&gr, gr_name, nsize);
+    FIELD_SIZE(&gr, gr_passwd, psize);
+    if (!STAILQ_EMPTY(&filters->users)) {
+	STAILQ_FOREACH(s, &filters->users, entries) {
+	    total += strlen(s->str) + 1;
+	    nmem++;
+	}
+	total += sizeof(char *) * nmem;
+    }
+    if (name != NULL)
+	total += strlen(name) + 1;
+
+    if ((gritem = calloc(1, total)) == NULL) {
+	sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+	    "unable to allocate memory");
+	debug_return_ptr(NULL);
+    }
+
+    /*
+     * Copy in group contents and make strings relative to space
+     * at the end of the buffer.  Note that gr_mem must come
+     * immediately after struct group to guarantee proper alignment.
+     */
+    newgr = &gritem->gr;
+    memcpy(newgr, &gr, sizeof(gr));
+    cp = (char *)(gritem + 1);
+    if (nmem != 0) {
+	STAILQ_FOREACH(s, &filters->groups, entries) {
+	    total += strlen(s->str) + 1;
+	}
+    }
+    if (nmem != 0) {
+	newgr->gr_mem = (char **)cp;
+	cp += sizeof(char *) * nmem;
+	nmem = 0;
+	STAILQ_FOREACH(s, &filters->groups, entries) {
+	    len = strlen(s->str) + 1;
+	    memcpy(cp, s->str, len);
+	    newgr->gr_mem[nmem++] = cp;
+	    cp += len;
+	}
+	newgr->gr_mem[nmem] = NULL;
+    }
+    FIELD_COPY(&gr, newgr, gr_passwd, psize);
+    FIELD_COPY(&gr, newgr, gr_name, nsize);
+
+    /* Set key and datum. */
+    if (name != NULL) {
+	memcpy(cp, name, strlen(name) + 1);
+	gritem->cache.k.name = cp;
+    } else {
+	gritem->cache.k.gid = gr.gr_gid;
+    }
+    gritem->cache.d.gr = newgr;
+    gritem->cache.refcnt = 1;
+
+    debug_return_ptr(&gritem->cache);
+}
+
+static struct cache_item_gidlist *gidlist_item;
+
+/*
+ * Dynamically allocate space for a struct item plus the key and data
+ * elements.  Fills in datum from user_gids or from getgrouplist(3).
+ */
+struct cache_item *
+sudo_make_gidlist_item(const struct passwd *pw, char * const *unused1,
+    unsigned int type)
+{
+    char *cp;
+    size_t nsize, total;
+    struct cache_item_gidlist *glitem;
+    struct cvtsudoers_string *s;
+    struct gid_list *gidlist;
+    GETGROUPS_T *gids = NULL;
+    int i, ngids = 0;
+    debug_decl(sudo_make_gidlist_item, SUDOERS_DEBUG_NSS)
+
+    /*
+     * There's only a single gid list.
+     */
+    if (gidlist_item != NULL) {
+	gidlist_item->cache.refcnt++;
+	debug_return_ptr(&gidlist_item->cache);
+    }
+
+    /* Count number of possible gids in the filter. */
+    STAILQ_FOREACH(s, &filters->groups, entries) {
+	if (s->str[0] == '#')
+		ngids++;
+    }
+
+    /* Allocate gids[] array and fill it with parsed gids. */
+    if (ngids != 0) {
+	gids = reallocarray(NULL, ngids, sizeof(GETGROUPS_T));
+	if (gids == NULL) {
+	    sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+		"unable to allocate memory");
+	    debug_return_ptr(NULL);
+	}
+	ngids = 0;
+	STAILQ_FOREACH(s, &filters->groups, entries) {
+	    if (s->str[0] == '#') {
+		const char *errstr;
+		gid_t gid = sudo_strtoid(s->str + 1, NULL, NULL, &errstr);
+		if (errstr == NULL) {
+		    /* Valid gid. */
+		    gids[ngids++] = gid;
+		}
+	    }
+	}
+    }
+    if (ngids == 0) {
+	free(gids);
+	errno = ENOENT;
+	debug_return_ptr(NULL);
+    }
+
+    /* Allocate in one big chunk for easy freeing. */
+    nsize = strlen(pw->pw_name) + 1;
+    total = sizeof(*glitem) + nsize;
+    total += sizeof(gid_t *) * ngids;
+
+    if ((glitem = calloc(1, total)) == NULL) {
+	sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+	    "unable to allocate memory");
+	free(gids);
+	debug_return_ptr(NULL);
+    }
+
+    /*
+     * Copy in group list and make pointers relative to space
+     * at the end of the buffer.  Note that the groups array must come
+     * immediately after struct group to guarantee proper alignment.
+     */
+    gidlist = &glitem->gidlist;
+    cp = (char *)(glitem + 1);
+    gidlist->gids = (gid_t *)cp;
+    cp += sizeof(gid_t) * ngids;
+
+    /* Set key and datum. */
+    memcpy(cp, pw->pw_name, nsize);
+    glitem->cache.k.name = cp;
+    glitem->cache.d.gidlist = gidlist;
+    glitem->cache.refcnt = 1;
+    glitem->cache.type = type;
+
+    /*
+     * Store group IDs.
+     */
+    for (i = 0; i < ngids; i++)
+	gidlist->gids[i] = gids[i];
+    gidlist->ngids = ngids;
+    free(gids);
+
+    debug_return_ptr(&glitem->cache);
+}
+
+static struct cache_item_gidlist *grlist_item;
+
+/*
+ * Dynamically allocate space for a struct item plus the key and data
+ * elements.  Fills in group names from a call to sudo_get_gidlist().
+ */
+struct cache_item *
+sudo_make_grlist_item(const struct passwd *pw, char * const *unused1)
+{
+    char *cp;
+    size_t nsize, ngroups, total, len;
+    struct cache_item_grlist *grlitem;
+    struct cvtsudoers_string *s;
+    struct group_list *grlist;
+    int groupname_len;
+    debug_decl(sudo_make_grlist_item, SUDOERS_DEBUG_NSS)
+
+    /*
+     * There's only a single group list.
+     */
+    if (grlist_item != NULL) {
+	grlist_item->cache.refcnt++;
+	debug_return_ptr(&grlist_item->cache);
+    }
+
+    /* Count number of groups in the filter. */
+    ngroups = 0;
+    STAILQ_FOREACH(s, &filters->groups, entries) {
+	ngroups++;
+    }
+
+#ifdef _SC_LOGIN_NAME_MAX
+    groupname_len = MAX((int)sysconf(_SC_LOGIN_NAME_MAX), 32);
+#else
+    groupname_len = MAX(LOGIN_NAME_MAX, 32);
+#endif
+
+    /* Allocate in one big chunk for easy freeing. */
+    nsize = strlen(pw->pw_name) + 1;
+    total = sizeof(*grlitem) + nsize;
+    total += groupname_len * ngroups;
+
+again:
+    if ((grlitem = calloc(1, total)) == NULL) {
+	sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+	    "unable to allocate memory");
+	debug_return_ptr(NULL);
+    }
+
+    /*
+     * Copy in group list and make pointers relative to space
+     * at the end of the buffer.  Note that the groups array must come
+     * immediately after struct group to guarantee proper alignment.
+     */
+    grlist = &grlitem->grlist;
+    cp = (char *)(grlitem + 1);
+    grlist->groups = (char **)cp;
+    cp += sizeof(char *) * ngroups;
+
+    /* Set key and datum. */
+    memcpy(cp, pw->pw_name, nsize);
+    grlitem->cache.k.name = cp;
+    grlitem->cache.d.grlist = grlist;
+    grlitem->cache.refcnt = 1;
+    cp += nsize;
+
+    /*
+     * Copy groups from filter.
+     */
+    ngroups = 0;
+    STAILQ_FOREACH(s, &filters->groups, entries) {
+	if (s->str[0] == '#') {
+	    const char *errstr;
+	    sudo_strtoid(s->str + 1, NULL, NULL, &errstr);
+	    if (errstr == NULL) {
+		/* Group ID not name, ignore it. */
+		continue;
+	    }
+	}
+	len = strlen(s->str) + 1;
+	if (cp - (char *)grlitem + len > total) {
+	    total += len + groupname_len;
+	    free(grlitem);
+	    goto again;
+	}
+	memcpy(cp, s->str, len);
+	grlist->groups[ngroups++] = cp;
+	cp += len;
+    }
+    grlist->ngroups = ngroups;
+
+    debug_return_ptr(&grlitem->cache);
+}
diff --git a/plugins/sudoers/parse.h b/plugins/sudoers/parse.h
index 347baa77c..1d7770ff0 100644
--- a/plugins/sudoers/parse.h
+++ b/plugins/sudoers/parse.h
@@ -252,6 +252,7 @@ extern struct defaults_list defaults;
 
 /* alias.c */
 bool no_aliases(void);
+struct rbtree *replace_aliases(struct rbtree *new_aliases);
 const char *alias_add(char *name, int type, char *file, int lineno, struct member *members);
 const char *alias_type_to_string(int alias_type);
 int alias_compare(const void *a1, const void *a2);
