Break sudoers transcript feature up into log_input and log_output.

This commit is contained in:
Todd C. Miller
2010-05-30 10:31:38 -04:00
parent 1a217bcc22
commit 2dd29bf64d
18 changed files with 515 additions and 444 deletions

13
INSTALL
View File

@@ -557,11 +557,10 @@ The following options are also configurable at runtime:
prompt as an argument and print the received password to
the standard output.
--with-transcript[=DIR]
By default, sudo stores transcript files in either
/var/log/sudo-transcript, /var/adm/sudo-transcript, or
/usr/log/sudo-transcript. If this option is specified,
transcripts will be stored in the indicated directory
--with-iologdir[=DIR]
By default, sudo stores I/O log files in either /var/log/sudo-io,
/var/adm/sudo-io, or /usr/log/sudo-io. If this option is
specified, I/O logs will be stored in the indicated directory
instead.
--disable-authentication
@@ -607,13 +606,13 @@ The following options are also configurable at runtime:
--enable-zlib[=DIR]
Enable the use of the zlib compress library when storing
transcript files. If specified, DIR is the base directory
I/O log files. If specified, DIR is the base directory
containing the zlib include and lib directories. By default
zlib is used if it is found on the system.
--disable-zlib
Disable the use of the zlib compress library when storing
transcript files.
I/O log files.
Shadow password and C2 support
==============================

View File

@@ -8,9 +8,10 @@ What's new in Sudo 1.8.0?
What's new in Sudo 1.7.3?
* Support for logging a transcript of the command being run.
For more information, see the documentation for the "transcript"
Defaults option in the sudoers manual and the sudoreplay manual.
* Support for logging I/O for the command being run.
For more information, see the documentation for the "log_input"
and "log_output" Defaults options in the sudoers manual. Also
see the sudoreplay manual for how to replay I/O log sessions.
* The passwd_timeout and timestamp_timeout options may now be
specified as floating point numbers for more granular timeout

20
aclocal.m4 vendored
View File

@@ -158,22 +158,22 @@ fi
])dnl
dnl
dnl Where the transcript files go, use /var/log/sudo-transcript if
dnl /var/log exists, else /{var,usr}/adm/sudo-transcript
dnl Where the I/O log files go, use /var/log/sudo-io if
dnl /var/log exists, else /{var,usr}/adm/sudo-io
dnl
AC_DEFUN(SUDO_TRANSCRIPT, [
AC_MSG_CHECKING(for transcript dir location)
if test "${with_transcript-yes}" != "yes"; then
AC_DEFUN(SUDO_IO_LOGDIR, [
AC_MSG_CHECKING(for I/O log dir location)
if test "${with_iologdir-yes}" != "yes"; then
:
elif test -d "/var/log"; then
with_transcript="/var/log/sudo-transcript"
with_iologdir="/var/log/sudo-io"
elif test -d "/var/adm"; then
with_transcript="/var/adm/sudo-transcript"
with_iologdir="/var/adm/sudo-io"
else
with_transcript="/usr/adm/sudo-transcript"
with_iologdir="/usr/adm/sudo-io"
fi
SUDO_DEFINE_UNQUOTED(_PATH_SUDO_TRANSCRIPT, "$with_transcript")
AC_MSG_RESULT($with_transcript)
SUDO_DEFINE_UNQUOTED(_PATH_SUDO_IO_LOGDIR, "$with_iologdir")
AC_MSG_RESULT($with_iologdir)
])dnl
dnl

30
configure vendored
View File

@@ -945,7 +945,7 @@ with_passprompt
with_badpass_message
with_fqdn
with_timedir
with_transcript
with_iologdir
with_sendmail
with_sudoers_mode
with_sudoers_uid
@@ -1699,7 +1699,7 @@ Optional Packages:
--with-badpass-message message the user sees when the password is wrong
--with-fqdn expect fully qualified hosts in sudoers
--with-timedir path to the sudo timestamp dir
--with-transcript=DIR directory to store sudo transcript files in
--with-iologdir=DIR directory to store sudo I/O log files in
--with-sendmail set path to sendmail
--without-sendmail do not send mail at all
--with-sudoers-mode mode of sudoers file (defaults to 0440)
@@ -3665,11 +3665,11 @@ fi
# Check whether --with-transcript was given.
if test "${with_transcript+set}" = set; then :
withval=$with_transcript; case $with_transcript in
# Check whether --with-iologdir was given.
if test "${with_iologdir+set}" = set; then :
withval=$with_iologdir; case $with_iologdir in
yes) ;;
no) as_fn_error "\"--without-transcript not supported.\"" "$LINENO" 5
no) as_fn_error "\"--without-iologfir not supported.\"" "$LINENO" 5
;;
esac
fi
@@ -17935,23 +17935,23 @@ EOF
fi
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for transcript dir location" >&5
$as_echo_n "checking for transcript dir location... " >&6; }
if test "${with_transcript-yes}" != "yes"; then
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for I/O log dir location" >&5
$as_echo_n "checking for I/O log dir location... " >&6; }
if test "${with_iologdir-yes}" != "yes"; then
:
elif test -d "/var/log"; then
with_transcript="/var/log/sudo-transcript"
with_iologdir="/var/log/sudo-io"
elif test -d "/var/adm"; then
with_transcript="/var/adm/sudo-transcript"
with_iologdir="/var/adm/sudo-io"
else
with_transcript="/usr/adm/sudo-transcript"
with_iologdir="/usr/adm/sudo-io"
fi
cat >>confdefs.h <<EOF
#define _PATH_SUDO_TRANSCRIPT "$with_transcript"
#define _PATH_SUDO_IO_LOGDIR "$with_iologdir"
EOF
{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $with_transcript" >&5
$as_echo "$with_transcript" >&6; }
{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $with_iologdir" >&5
$as_echo "$with_iologdir" >&6; }
case "$with_passwd" in

View File

@@ -704,11 +704,11 @@ AC_ARG_WITH(timedir, [AS_HELP_STRING([--with-timedir], [path to the sudo timesta
;;
esac])
AC_ARG_WITH(transcript, [AS_HELP_STRING([--with-transcript=DIR], [directory
to store sudo transcript files in])],
[case $with_transcript in
AC_ARG_WITH(iologdir, [AS_HELP_STRING([--with-iologdir=DIR], [directory
to store sudo I/O log files in])],
[case $with_iologdir in
yes) ;;
no) AC_MSG_ERROR(["--without-transcript not supported."])
no) AC_MSG_ERROR(["--without-iologfir not supported."])
;;
esac])
@@ -2663,11 +2663,11 @@ if test -n "$blibpath"; then
fi
dnl
dnl Check for log file, timestamp and transcript locations
dnl Check for log file, timestamp and iolog locations
dnl
SUDO_LOGFILE
SUDO_TIMEDIR
SUDO_TRANSCRIPT
SUDO_IOLOGDIR
dnl
dnl Use passwd (and secureware) auth modules?

View File

@@ -74,12 +74,12 @@
#endif /* _PATH_SUDO_TIMEDIR */
/*
* Where to put the session files. Defaults to /var/log/sudo-session,
* /var/adm/sudo-session or /usr/adm/sudo-session depending on what exists.
* Where to put the I/O log files. Defaults to /var/log/sudo-io,
* /var/adm/sudo-io or /usr/adm/sudo-io depending on what exists.
*/
#ifndef _PATH_SUDO_TRANSCRIPT
#undef _PATH_SUDO_TRANSCRIPT
#endif /* _PATH_SUDO_TRANSCRIPT */
#ifndef _PATH_SUDO_IO_LOGDIR
#undef _PATH_SUDO_IO_LOGDIR
#endif /* _PATH_SUDO_IO_LOGDIR */
/*
* Where to put the sudo log file when logging to a file. Defaults to

View File

@@ -315,12 +315,16 @@ struct sudo_defs_types sudo_defs_table[] = {
"The umask specified in sudoers will override the user's, even if it is more permissive",
NULL,
}, {
"transcript", T_FLAG,
"Log a transcript of the command being run",
"log_input", T_FLAG,
"Log user's input for the command being run",
NULL,
}, {
"compress_transcript", T_FLAG,
"Compress session transcripts with zlib",
"log_output", T_FLAG,
"Log the output of the command being run",
NULL,
}, {
"compress_io", T_FLAG,
"Compress I/O logs using zlib",
NULL,
}, {
NULL, 0, NULL

View File

@@ -144,10 +144,12 @@
#define I_FAST_GLOB 71
#define def_umask_override (sudo_defs_table[72].sd_un.flag)
#define I_UMASK_OVERRIDE 72
#define def_transcript (sudo_defs_table[73].sd_un.flag)
#define I_TRANSCRIPT 73
#define def_compress_transcript (sudo_defs_table[74].sd_un.flag)
#define I_COMPRESS_TRANSCRIPT 74
#define def_log_input (sudo_defs_table[73].sd_un.flag)
#define I_LOG_INPUT 73
#define def_log_output (sudo_defs_table[74].sd_un.flag)
#define I_LOG_OUTPUT 74
#define def_compress_io (sudo_defs_table[75].sd_un.flag)
#define I_COMPRESS_IO 75
enum def_tupple {
never,

View File

@@ -232,9 +232,12 @@ fast_glob
umask_override
T_FLAG
"The umask specified in sudoers will override the user's, even if it is more permissive"
transcript
log_input
T_FLAG
"Log a transcript of the command being run"
compress_transcript
"Log user's input for the command being run"
log_output
T_FLAG
"Compress session transcripts with zlib"
"Log the output of the command being run"
compress_io
T_FLAG
"Compress I/O logs using zlib"

View File

@@ -477,7 +477,7 @@ init_defaults(void)
def_passwd_timeout = PASSWORD_TIMEOUT;
def_passwd_tries = TRIES_FOR_PASSWORD;
#ifdef HAVE_ZLIB
def_compress_transcript = TRUE;
def_compress_io = TRUE;
#endif
/* Now do the strings */

File diff suppressed because it is too large Load Diff

View File

@@ -16,17 +16,19 @@
#define EXEC 272
#define SETENV 273
#define NOSETENV 274
#define TRANSCRIPT 275
#define NOTRANSCRIPT 276
#define ALL 277
#define COMMENT 278
#define HOSTALIAS 279
#define CMNDALIAS 280
#define USERALIAS 281
#define RUNASALIAS 282
#define ERROR 283
#define TYPE 284
#define ROLE 285
#define LOG_INPUT 275
#define NOLOG_INPUT 276
#define LOG_OUTPUT 277
#define NOLOG_OUTPUT 278
#define ALL 279
#define COMMENT 280
#define HOSTALIAS 281
#define CMNDALIAS 282
#define USERALIAS 283
#define RUNASALIAS 284
#define ERROR 285
#define TYPE 286
#define ROLE 287
#ifndef YYSTYPE_DEFINED
#define YYSTYPE_DEFINED
typedef union {

View File

@@ -142,8 +142,10 @@ yyerror(s)
%token <tok> EXEC /* don't preload dummy execve() */
%token <tok> SETENV /* user may set environment for cmnd */
%token <tok> NOSETENV /* user may not set environment */
%token <tok> TRANSCRIPT /* log a transcript of the cmnd */
%token <tok> NOTRANSCRIPT /* don't log a transcript of the cmnd */
%token <tok> LOG_INPUT /* log user's cmnd input */
%token <tok> NOLOG_INPUT /* don't log user's cmnd input */
%token <tok> LOG_OUTPUT /* log cmnd output */
%token <tok> NOLOG_OUTPUT /* don't log cmnd output */
%token <tok> ALL /* ALL keyword */
%token <tok> COMMENT /* comment and/or carriage return */
%token <tok> HOSTALIAS /* Host_Alias keyword */
@@ -315,8 +317,10 @@ cmndspeclist : cmndspec
if ($3->tags.setenv == UNSPEC &&
$3->prev->tags.setenv != IMPLIED)
$3->tags.setenv = $3->prev->tags.setenv;
if ($3->tags.transcript == UNSPEC)
$3->tags.transcript = $3->prev->tags.transcript;
if ($3->tags.log_input == UNSPEC)
$3->tags.log_input = $3->prev->tags.log_input;
if ($3->tags.log_output == UNSPEC)
$3->tags.log_output = $3->prev->tags.log_output;
if ((tq_empty(&$3->runasuserlist) &&
tq_empty(&$3->runasgrouplist)) &&
(!tq_empty(&$3->prev->runasuserlist) ||
@@ -422,7 +426,8 @@ runaslist : userlist {
;
cmndtag : /* empty */ {
$$.nopasswd = $$.noexec = $$.setenv = $$.transcript = UNSPEC;
$$.nopasswd = $$.noexec = $$.setenv =
$$.log_input = $$.log_output = UNSPEC;
}
| cmndtag NOPASSWD {
$$.nopasswd = TRUE;
@@ -442,11 +447,17 @@ cmndtag : /* empty */ {
| cmndtag NOSETENV {
$$.setenv = FALSE;
}
| cmndtag TRANSCRIPT {
$$.transcript = TRUE;
| cmndtag LOG_INPUT {
$$.log_input = TRUE;
}
| cmndtag NOTRANSCRIPT {
$$.transcript = FALSE;
| cmndtag NOLOG_INPUT {
$$.log_input = FALSE;
}
| cmndtag LOG_OUTPUT {
$$.log_output = TRUE;
}
| cmndtag NOLOG_OUTPUT {
$$.log_output = FALSE;
}
;

View File

@@ -91,20 +91,20 @@ io_nextid(void)
char pathbuf[PATH_MAX];
/*
* Create _PATH_SUDO_TRANSCRIPT if it doesn't already exist.
* Create _PATH_SUDO_IO_LOGDIR if it doesn't already exist.
*/
if (stat(_PATH_SUDO_TRANSCRIPT, &sb) != 0) {
if (mkdir(_PATH_SUDO_TRANSCRIPT, S_IRWXU) != 0)
log_error(USE_ERRNO, "Can't mkdir %s", _PATH_SUDO_TRANSCRIPT);
if (stat(_PATH_SUDO_IO_LOGDIR, &sb) != 0) {
if (mkdir(_PATH_SUDO_IO_LOGDIR, S_IRWXU) != 0)
log_error(USE_ERRNO, "Can't mkdir %s", _PATH_SUDO_IO_LOGDIR);
} else if (!S_ISDIR(sb.st_mode)) {
log_error(0, "%s exists but is not a directory (0%o)",
_PATH_SUDO_TRANSCRIPT, (unsigned int) sb.st_mode);
_PATH_SUDO_IO_LOGDIR, (unsigned int) sb.st_mode);
}
/*
* Open sequence file
*/
len = snprintf(pathbuf, sizeof(pathbuf), "%s/seq", _PATH_SUDO_TRANSCRIPT);
len = snprintf(pathbuf, sizeof(pathbuf), "%s/seq", _PATH_SUDO_IO_LOGDIR);
if (len <= 0 || len >= sizeof(pathbuf)) {
errno = ENAMETOOLONG;
log_error(USE_ERRNO, "%s/seq", pathbuf);
@@ -158,12 +158,12 @@ build_idpath(char *pathbuf, size_t pathsize)
/*
* Path is of the form /var/log/sudo-session/00/00/01.
*/
len = snprintf(pathbuf, pathsize, "%s/%c%c/%c%c/%c%c", _PATH_SUDO_TRANSCRIPT,
len = snprintf(pathbuf, pathsize, "%s/%c%c/%c%c/%c%c", _PATH_SUDO_IO_LOGDIR,
sudo_user.sessid[0], sudo_user.sessid[1], sudo_user.sessid[2],
sudo_user.sessid[3], sudo_user.sessid[4], sudo_user.sessid[5]);
if (len <= 0 && len >= pathsize) {
errno = ENAMETOOLONG;
log_error(USE_ERRNO, "%s/%s", _PATH_SUDO_TRANSCRIPT, sudo_user.sessid);
log_error(USE_ERRNO, "%s/%s", _PATH_SUDO_IO_LOGDIR, sudo_user.sessid);
}
/*
@@ -223,7 +223,7 @@ static sudoers_io_open(unsigned int version, sudo_conv_t conversation,
if (argc == 0)
return TRUE;
if (!def_transcript)
if (!def_log_input && !def_log_output)
return FALSE;
/*
@@ -244,27 +244,27 @@ static sudoers_io_open(unsigned int version, sudo_conv_t conversation,
if (io_logfile == NULL)
log_error(USE_ERRNO, "Can't create %s", pathbuf);
io_fds[IOFD_TIMING].v = open_io_fd(pathbuf, len, "/timing", def_compress_transcript);
io_fds[IOFD_TIMING].v = open_io_fd(pathbuf, len, "/timing", def_compress_io);
if (io_fds[IOFD_TIMING].v == NULL)
log_error(USE_ERRNO, "Can't create %s", pathbuf);
io_fds[IOFD_TTYIN].v = open_io_fd(pathbuf, len, "/ttyin", def_compress_transcript);
io_fds[IOFD_TTYIN].v = open_io_fd(pathbuf, len, "/ttyin", def_compress_io);
if (io_fds[IOFD_TTYIN].v == NULL)
log_error(USE_ERRNO, "Can't create %s", pathbuf);
io_fds[IOFD_TTYOUT].v = open_io_fd(pathbuf, len, "/ttyout", def_compress_transcript);
io_fds[IOFD_TTYOUT].v = open_io_fd(pathbuf, len, "/ttyout", def_compress_io);
if (io_fds[IOFD_TTYOUT].v == NULL)
log_error(USE_ERRNO, "Can't create %s", pathbuf);
io_fds[IOFD_STDIN].v = open_io_fd(pathbuf, len, "/stdin", def_compress_transcript);
io_fds[IOFD_STDIN].v = open_io_fd(pathbuf, len, "/stdin", def_compress_io);
if (io_fds[IOFD_STDIN].v == NULL)
log_error(USE_ERRNO, "Can't create %s", pathbuf);
io_fds[IOFD_STDOUT].v = open_io_fd(pathbuf, len, "/stdout", def_compress_transcript);
io_fds[IOFD_STDOUT].v = open_io_fd(pathbuf, len, "/stdout", def_compress_io);
if (io_fds[IOFD_STDOUT].v == NULL)
log_error(USE_ERRNO, "Can't create %s", pathbuf);
io_fds[IOFD_STDERR].v = open_io_fd(pathbuf, len, "/stderr", def_compress_transcript);
io_fds[IOFD_STDERR].v = open_io_fd(pathbuf, len, "/stderr", def_compress_io);
if (io_fds[IOFD_STDERR].v == NULL)
log_error(USE_ERRNO, "Can't create %s", pathbuf);
@@ -297,7 +297,7 @@ static sudoers_io_close(int exit_status, int error)
for (i = 0; i < IOFD_MAX; i++) {
#ifdef HAVE_ZLIB
if (def_compress_transcript)
if (def_compress_io)
gzclose(io_fds[i].g);
else
#endif
@@ -336,14 +336,14 @@ sudoers_io_log(const char *buf, unsigned int len, int idx)
sigprocmask(SIG_BLOCK, &ttyblock, &omask);
#ifdef HAVE_ZLIB
if (def_compress_transcript)
if (def_compress_io)
gzwrite(io_fds[idx].g, buf, len);
else
#endif
fwrite(buf, 1, len, io_fds[idx].f);
timersub(&now, &last_time, &tv);
#ifdef HAVE_ZLIB
if (def_compress_transcript)
if (def_compress_io)
gzprintf(io_fds[IOFD_TIMING].g, "%d %f %d\n", idx,
tv.tv_sec + ((double)tv.tv_usec / 1000000), len);
else

View File

@@ -243,8 +243,10 @@ sudo_file_lookup(struct sudo_nss *nss, int validated, int pwflag)
def_noexec = tags->noexec;
if (tags->setenv != UNSPEC)
def_setenv = tags->setenv;
if (tags->transcript != UNSPEC)
def_transcript = tags->transcript;
if (tags->log_input != UNSPEC)
def_log_input = tags->log_input;
if (tags->log_output != UNSPEC)
def_log_output = tags->log_output;
}
} else if (match == DENY) {
SET(validated, VALIDATE_NOT_OK);
@@ -284,10 +286,15 @@ sudo_file_append_cmnd(struct cmndspec *cs, struct cmndtag *tags,
"PASSWD: ", NULL);
tags->nopasswd = cs->tags.nopasswd;
}
if (TAG_CHANGED(transcript)) {
lbuf_append(lbuf, cs->tags.transcript ? "SCRIPT: " :
"NOSCRIPT: ", NULL);
tags->transcript = cs->tags.transcript;
if (TAG_CHANGED(log_input)) {
lbuf_append(lbuf, cs->tags.log_input ? "LOG_INPUT: " :
"NOLOG_INPUT: ", NULL);
tags->log_input = cs->tags.log_input;
}
if (TAG_CHANGED(log_output)) {
lbuf_append(lbuf, cs->tags.log_output ? "LOG_OUTPUT: " :
"NOLOG_OUTPUT: ", NULL);
tags->log_output = cs->tags.log_output;
}
m = cs->cmnd;
print_member(lbuf, m->name, m->type, m->negated,
@@ -310,7 +317,8 @@ sudo_file_display_priv_short(struct passwd *pw, struct userspec *us,
tags.noexec = UNSPEC;
tags.setenv = UNSPEC;
tags.nopasswd = UNSPEC;
tags.transcript = UNSPEC;
tags.log_input = UNSPEC;
tags.log_output = UNSPEC;
lbuf_append(lbuf, " ", NULL);
tq_foreach_fwd(&priv->cmndlist, cs) {
if (cs != tq_first(&priv->cmndlist))
@@ -362,7 +370,8 @@ sudo_file_display_priv_long(struct passwd *pw, struct userspec *us,
tags.noexec = UNSPEC;
tags.setenv = UNSPEC;
tags.nopasswd = UNSPEC;
tags.transcript = UNSPEC;
tags.log_input = UNSPEC;
tags.log_output = UNSPEC;
lbuf_append(lbuf, "\nSudoers entry:\n", NULL);
tq_foreach_fwd(&priv->cmndlist, cs) {
lbuf_append(lbuf, " RunAsUsers: ", NULL);

View File

@@ -40,10 +40,11 @@ struct sudo_command {
* Possible valus: TRUE, FALSE, UNSPEC.
*/
struct cmndtag {
__signed char nopasswd;
__signed char noexec;
__signed char setenv;
__signed char transcript;
__signed int nopasswd: 3;
__signed int noexec: 3;
__signed int setenv: 3;
__signed int log_input: 3;
__signed int log_output: 3;
};
/*

View File

@@ -518,7 +518,7 @@ sudoers_policy_main(int argc, char * const argv[], int pwflag, char *env_add[],
validate_env_vars(sudo_user.env_vars);
}
if (def_transcript && (sudo_mode & (MODE_RUN | MODE_EDIT)))
if (ISSET(sudo_mode, (MODE_RUN| MODE_EDIT)) && (def_log_input || def_log_output))
io_nextid();
log_allowed(validated);
if (ISSET(sudo_mode, MODE_CHECK))

View File

@@ -110,7 +110,7 @@ union io_fd {
};
/*
* Info present in the transcript log file
* Info present in the I/O log file
*/
struct log_info {
char *cwd;
@@ -161,7 +161,7 @@ struct search_node {
static struct search_node *node_stack[32];
static int stack_top;
static const char *session_dir = _PATH_SUDO_TRANSCRIPT;
static const char *session_dir = _PATH_SUDO_IO_LOGDIR;
static union io_fd io_fds[IOFD_MAX];
static const char *io_fnames[IOFD_MAX] = {
@@ -642,7 +642,11 @@ list_session_dir(char *pathbuf, REGEX_T *re, const char *user, const char *tty)
pathbuf[plen + 0] = '/';
pathbuf[plen + 1] = dp->d_name[0];
pathbuf[plen + 2] = dp->d_name[1];
pathbuf[plen + 3] = '\0';
pathbuf[plen + 3] = '/';
pathbuf[plen + 4] = 'l';
pathbuf[plen + 5] = 'o';
pathbuf[plen + 6] = 'g';
pathbuf[plen + 7] = '\0';
fp = fopen(pathbuf, "r");
if (fp == NULL) {
warning("unable to open %s", pathbuf);
@@ -744,6 +748,10 @@ list_sessions(int argc, char **argv, const char *pattern, const char *user,
#endif /* HAVE_REGCOMP */
sdlen = strlcpy(pathbuf, session_dir, sizeof(pathbuf));
if (sdlen + sizeof("/00/00/00/log") >= sizeof(pathbuf)) {
errno = ENAMETOOLONG;
error(1, "%s/00/00/00/log", session_dir);
}
/*
* Three levels of directory, e.g. 00/00/00 .. ZZ/ZZ/ZZ