From 3724c2e900a46c4e436c08e4df0fda45feec849f Mon Sep 17 00:00:00 2001 From: "Todd C. Miller" Date: Sun, 13 Sep 2009 22:02:07 +0000 Subject: [PATCH] Implement search expressions in sudoreplay similar in concept to what find or tcpdump uses. TODO: date ranges --- Makefile.in | 6 +- sudoreplay.c | 256 +++++++++++++++++++++++++++++++++++++--------- sudoreplay.cat | 126 +++++++++++++++++------ sudoreplay.man.in | 56 ++++++---- sudoreplay.pod | 62 +++++++---- 5 files changed, 386 insertions(+), 120 deletions(-) diff --git a/Makefile.in b/Makefile.in index 30c3b54ea..870b0b236 100644 --- a/Makefile.in +++ b/Makefile.in @@ -137,7 +137,7 @@ SUDO_OBJS = $(COMMON_OBJS) $(AUTH_OBJS) @SUDO_OBJS@ audit.o check.o env.o \ VISUDO_OBJS = $(COMMON_OBJS) visudo.o fileops.o gettime.o goodpath.o \ find_path.o pwutil.o -REPLAY_OBJS = sudoreplay.o error.o +REPLAY_OBJS = sudoreplay.o error.o alloc.o TEST_OBJS = $(COMMON_OBJS) interfaces.o testsudoers.o tsgetgrpw.o tspwutil.o @@ -154,8 +154,8 @@ DISTFILES = $(SRCS) $(HDRS) ChangeLog HISTORY INSTALL INSTALL.configure \ schema.ActiveDirectory schema.OpenLDAP schema.iPlanet sudo.cat \ sudo.man.in sudo.pod sudo.psf sudo_usage.h.in sudoers sudoers.cat \ sudoers.man.in sudoers.pod sudoers.ldap.cat sudoers.ldap.man.in \ - sudoers.ldap.pod sudoers2ldif sudoreplay.cat sudoreplay.man.in \ - sudoreplay.pod visudo.cat visudo.man.in visudo.pod auth/API + sudoers.ldap.pod sudoers2ldif sudoreplay.cat sudoreplay.man.in \ + sudoreplay.pod visudo.cat visudo.man.in visudo.pod auth/API BINFILES= ChangeLog HISTORY LICENSE README TROUBLESHOOTING \ UPGRADE install-sh mkinstalldirs sample.syslog.conf sample.sudoers \ diff --git a/sudoreplay.c b/sudoreplay.c index 3d997e2cf..09492cd67 100644 --- a/sudoreplay.c +++ b/sudoreplay.c @@ -94,9 +94,58 @@ int Argc; char **Argv; const char *session_dir = _PATH_SUDO_SESSDIR; -void usage __P((void)); -void delay __P((double)); -int list_sessions __P((int, char **, const char *, const char *, const char *)); +/* + * Info present in the transcript log file + */ +struct log_info { + char *user; + char *runas_user; + char *runas_group; + char *tty; + char *cmd; + time_t tstamp; +}; + +/* + * Handle expressions like: + * ( user millert or user root ) and tty console and command /bin/sh + * XXX - also time-based + */ +struct search_node { + struct search_node *next; +#define ST_EXPR 1 +#define ST_TTY 2 +#define ST_USER 3 +#define ST_PATTERN 4 +#define ST_RUNASUSER 5 +#define ST_RUNASGROUP 6 + char type; + char negated; + char or; + char pad; + union { +#ifdef HAVE_REGCOMP + regex_t cmdre; +#endif + char *tty; + char *user; + char *pattern; + char *runas_group; + char *runas_user; + struct search_node *expr; + void *ptr; + } u; +} *search_expr; + +#define STACK_NODE_SIZE 32 +static struct search_node *node_stack[32]; +static int stack_top; + +extern void *emalloc __P((size_t)); +static int list_sessions __P((int, char **, const char *, const char *, const char *)); +static int parse_expr __P((struct search_node **, char **)); +static void delay __P((double)); +static void usage __P((void)); #ifdef HAVE_REGCOMP # define REGEX_T regex_t @@ -136,7 +185,7 @@ main(argc, argv) Argv = argv; /* XXX - timestamp option? (begin,end) */ - while ((ch = getopt(argc, argv, "d:lm:p:s:t:u:V")) != -1) { + while ((ch = getopt(argc, argv, "d:lm:s:V")) != -1) { switch(ch) { case 'd': session_dir = optarg; @@ -150,21 +199,12 @@ main(argc, argv) if (*ep != '\0' || errno != 0) error(1, "invalid max wait: %s", optarg); break; - case 'p': - pattern = optarg; - break; case 's': errno = 0; speed = strtod(optarg, &ep); if (*ep != '\0' || errno != 0) error(1, "invalid speed factor: %s", optarg); break; - case 't': - tty = optarg; - break; - case 'u': - user = optarg; - break; case 'V': (void) printf("%s version %s\n", getprogname(), PACKAGE_VERSION); exit(0); @@ -177,9 +217,8 @@ main(argc, argv) argc -= optind; argv += optind; - if (listonly) { + if (listonly) exit(list_sessions(argc, argv, pattern, user, tty)); - } if (argc != 1) usage(); @@ -279,7 +318,7 @@ nanosleep(ts, rts) } #endif -void +static void delay(secs) double secs; { @@ -303,14 +342,149 @@ delay(secs) error(1, "nanosleep: tv_sec %ld, tv_nsec %ld", ts.tv_sec, ts.tv_nsec); } -struct log_info { - char *user; - char *runas_user; - char *runas_group; - char *tty; - char *cmd; - time_t tstamp; -}; +/* + * Build expression list from search args + * XXX - add additional search terms + */ +static int +parse_expr(headp, argv) + struct search_node **headp; + char **argv; +{ + struct search_node *sn, *newsn; + char or = 0, not = 0, type; + char **av; + + sn = *headp; + for (av = argv; *av; av++) { + switch (*av[0]) { + case 'a': /* and (ignore) */ + continue; + case 'o': /* or */ + or = 1; + continue; + case '!': /* negate */ + not = 1; + continue; + case 'c': /* command */ + type = ST_PATTERN; + break; + case 'g': /* runas group */ + type = ST_RUNASGROUP; + break; + case 'r': /* runas user */ + type = ST_RUNASUSER; + break; + case 't': /* tty */ + type = ST_TTY; + break; + case 'u': /* user */ + type = ST_USER; + break; + case '(': /* start sub-expression */ + if (stack_top + 1 == STACK_NODE_SIZE) { + errorx(1, "too many parenthesized expressions, max %d", + STACK_NODE_SIZE); + } + node_stack[stack_top++] = sn; + type = ST_EXPR; + break; + case ')': /* end sub-expression */ + /* pop */ + if (--stack_top < 0) + errorx(1, "unmatched ')' in expression"); + if (node_stack[stack_top]) + sn->next = node_stack[stack_top]->next; + return(av - argv + 1); + default: + errorx(1, "unknown search term \"%s\"", *av); + /* NOTREACHED */ + } + + /* Allocate new search node */ + newsn = emalloc(sizeof(*newsn)); + newsn->next = NULL; + newsn->type = type; + newsn->or = or; + newsn->negated = not; + if (type == ST_EXPR) { + av += parse_expr(&newsn->u.expr, av + 1); + } else { + if (*(++av) == NULL) + errorx(1, "%s requires an argument", av[-1]); +#ifdef HAVE_REGCOMP + if (type == ST_PATTERN) { + if (regcomp(&newsn->u.cmdre, *av, REG_EXTENDED|REG_NOSUB) != 0) + errorx(1, "invalid regex: %s", *av); + } else +#endif + newsn->u.ptr = *av; + } + not = or = 0; /* reset state */ + if (sn) + sn->next = newsn; + else + *headp = newsn; + sn = newsn; + } + if (stack_top) + errorx(1, "unmatched '(' in expression"); + if (or) + errorx(1, "illegal trailing \"or\""); + if (not) + errorx(1, "illegal trailing \"!\""); + + return(av - argv); +} + +static int +match_expr(head, log) + struct search_node *head; + struct log_info *log; +{ + struct search_node *sn; + int matched = 1, rc; + + for (sn = head; sn; sn = sn->next) { + /* If we have no match, skip up to the next OR entry. */ + if (!matched && !sn->or) + continue; + + switch (sn->type) { + case ST_EXPR: + matched = match_expr(sn->u.expr, log); + break; + case ST_TTY: + matched = strcmp(sn->u.tty, log->tty) == 0; + break; + case ST_RUNASGROUP: + matched = strcmp(sn->u.runas_group, log->runas_group) == 0; + break; + case ST_RUNASUSER: + matched = strcmp(sn->u.runas_user, log->runas_user) == 0; + break; + case ST_USER: + matched = strcmp(sn->u.user, log->user) == 0; + break; + case ST_PATTERN: +#ifdef HAVE_REGCOMP + rc = regexec(&sn->u.cmdre, log->cmd, 0, NULL, 0); + if (rc && rc != REG_NOMATCH) { + char buf[BUFSIZ]; + regerror(rc, &sn->u.cmdre, buf, sizeof(buf)); + errorx(1, "%s", buf); + } + matched = rc == REG_NOMATCH ? 0 : 1; +#else + matched = strstr(log.cmd, sn->u.pattern) != NULL; +#endif + break; + } + if (sn->negated) + matched = !matched; + } + return(matched); +} static int list_session_dir(pathbuf, re, user, tty) @@ -385,28 +559,9 @@ list_session_dir(pathbuf, re, user, tty) cmdbuf[strcspn(cmdbuf, "\n")] = '\0'; li.cmd = cmdbuf; - /* - * Select based on user/tty/regex if applicable. - * XXX - select on time and/or runas bits too? - */ - if (user && strcmp(user, li.user) != 0) + /* Match on search expression if there is one. */ + if (search_expr && !match_expr(search_expr, &li)) continue; - if (tty && strcmp(tty, li.tty) != 0) - continue; - if (re) { -#ifdef HAVE_REGCOMP - int rc = regexec(re, li.cmd, 0, NULL, 0); - if (rc) { - if (rc == REG_NOMATCH) - continue; - regerror(rc, re, buf, sizeof(buf)); - errorx(1, "%s", buf); - } -#else - if (strstr(li.cmd, re) == NULL) - continue; -#endif /* HAVE_REGCOMP */ - } /* Convert from /var/log/sudo-sessions/00/00/01 to 000001 */ idstr[0] = pathbuf[plen - 5]; @@ -417,13 +572,13 @@ list_session_dir(pathbuf, re, user, tty) idstr[5] = pathbuf[plen + 2]; idstr[6] = '\0'; /* XXX - better format (timestamp?) */ - printf("%s: %s %d (%s:%s) %s\n", idstr, li.user, li.tstamp, + printf("%s: %s %ld (%s:%s) %s\n", idstr, li.user, (long)li.tstamp, li.runas_user, li.runas_group, li.cmd); } return(0); } -int +static int list_sessions(argc, argv, pattern, user, tty) int argc; char **argv; @@ -437,6 +592,9 @@ list_sessions(argc, argv, pattern, user, tty) size_t sdlen; char pathbuf[PATH_MAX]; + /* Parse search expression if present */ + parse_expr(&search_expr, argv); + d1 = opendir(session_dir); if (d1 == NULL) error(1, "unable to open %s", session_dir); @@ -488,14 +646,14 @@ list_sessions(argc, argv, pattern, user, tty) return(0); } -void +static void usage() { fprintf(stderr, "usage: %s [-d directory] [-m max_wait] [-s speed_factor] ID\n", getprogname()); fprintf(stderr, - "usage: %s [-d directory] [-p pattern] [-t tty] [-u username] -l\n", + "usage: %s [-d directory] -l [search expression]\n", getprogname()); exit(1); } diff --git a/sudoreplay.cat b/sudoreplay.cat index 7ebd8f30b..fcee651ca 100644 --- a/sudoreplay.cat +++ b/sudoreplay.cat @@ -10,7 +10,7 @@ NNAAMMEE SSYYNNOOPPSSIISS ssuuddoorreeppllaayy [--dd _d_i_r_e_c_t_o_r_y] [--mm _m_a_x___w_a_i_t] [--ss _s_p_e_e_d___f_a_c_t_o_r] ID - ssuuddoorreeppllaayy [--dd _d_i_r_e_c_t_o_r_y] [--pp _p_a_t_t_e_r_n] [--tt _t_t_y] [--uu _u_s_e_r] -l + ssuuddoorreeppllaayy [--dd _d_i_r_e_c_t_o_r_y] -l [search expression] DDEESSCCRRIIPPTTIIOONN ssuuddoorreeppllaayy plays back or lists the session logs created by ssuuddoo. When @@ -31,8 +31,56 @@ OOPPTTIIOONNSS default, _/_v_a_r_/_l_o_g_/_s_u_d_o_-_s_e_s_s_i_o_n_s. -l Enable "list mode". In this mode, ssuuddoorreeppllaayy will list - available session IDs. The -p, <-t> and <-u> options can - be used to restrict the IDs that are displayed. + available session IDs. If a _s_e_a_r_c_h _e_x_p_r_e_s_s_i_o_n is + specified, it will be used to restrict the IDs that are + displayed. An expression is composed of the following + predicates: + + user _u_s_e_r_n_a_m_e + Evaluates to true if the ID matches a command run + by _u_s_e_r_n_a_m_e. + + command _c_o_m_m_a_n_d _p_a_t_t_e_r_n + Evaluates to true if the command run matches + _c_o_m_m_a_n_d _p_a_t_t_e_r_n. On systems with POSIX regular + expression support, the pattern may be an extended + regular expression. On systems without POSIX + regular expression support, a simple substring + match is performed instead. + + tty _t_t_y Evaluates to true if the command was run on the + specified terminal device. The _t_t_y should be + specified without the _/_d_e_v_/ prefix, e.g. _t_t_y_0_1 + instead of _/_d_e_v_/_t_t_y_0_1. + + runas _r_u_n_a_s___u_s_e_r + Evaluates to true if the command was run as the + specified _r_u_n_a_s___u_s_e_r. Note that ssuuddoo runs commands + as user _r_o_o_t by default. + + + + +1.7.2 September 13, 2009 1 + + + + + +SUDOREPLAY(1m) MAINTENANCE COMMANDS SUDOREPLAY(1m) + + + runas _r_u_n_a_s___g_r_o_u_p + Evaluates to true if the command was run with the + specified _r_u_n_a_s___g_r_o_u_p. Note that unless a + _r_u_n_a_s___g_r_o_u_p was explicitly specified when ssuuddoo was + run this field will be empty in the log. + + Predicates may be combined using _a_n_d, _o_r and _! operators as + well as '(' and ')' for grouping (note that parentheses + must generally be escaped from the shell). The _a_n_d + operator is optional, adjacent predicates have an implied + _a_n_d unless separated by an _o_r. -m _m_a_x___w_a_i_t Specify an upper bound on how long to wait between key presses or output data. By default, ssuuddoo__rreeppllaayy will @@ -43,12 +91,6 @@ OOPPTTIIOONNSS _m_a_x___w_a_i_t seconds. The value may be specified as a floating point number, .e.g. _2_._5. - -p _p_a_t_t_e_r_n Restrict list output to sessions where the command matches - _p_a_t_t_e_r_n. On systems with POSIX regular expression support, - the pattern may be an extended regular expression. On - systems without POSIX regular expression support, a simple - substring match is performed instead. - -s _s_p_e_e_d___f_a_c_t_o_r This option causes ssuuddoorreeppllaayy to adjust the number of seconds it will wait between key presses or program output. @@ -57,26 +99,6 @@ OOPPTTIIOONNSS fast whereas a _s_p_e_e_d___f_a_c_t_o_r of <.5> would make the output twice as slow. - -t _t_t_y Restrict list output to sessions where the command was run - - - -1.7.2 August 30, 2009 1 - - - - - -SUDOREPLAY(1m) MAINTENANCE COMMANDS SUDOREPLAY(1m) - - - on the specified terming device. The _t_t_y should be - specified without the _/_d_e_v_/ prefix, e.g. _t_t_y_0_1 instead of - _/_d_e_v_/_t_t_y_0_1. - - -u _u_s_e_r Restrict list output to sessions where the command was run - by _u_s_e_r. - -V The --VV (version) option causes ssuuddoorreeppllaayy to print its version number and exit. @@ -102,6 +124,18 @@ BBUUGGSS If you feel you have found a bug in ssuuddoorreeppllaayy, please submit a bug report at http://www.sudo.ws/sudo/bugs/ + + + +1.7.2 September 13, 2009 2 + + + + + +SUDOREPLAY(1m) MAINTENANCE COMMANDS SUDOREPLAY(1m) + + SSUUPPPPOORRTT Limited free support is available via the sudo-users mailing list, see http://www.sudo.ws/mailman/listinfo/sudo-users to subscribe or search @@ -127,6 +161,38 @@ DDIISSCCLLAAIIMMEERR -1.7.2 August 30, 2009 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +1.7.2 September 13, 2009 3 diff --git a/sudoreplay.man.in b/sudoreplay.man.in index 9b2c7e3b3..158764e81 100644 --- a/sudoreplay.man.in +++ b/sudoreplay.man.in @@ -148,7 +148,7 @@ .\" ======================================================================== .\" .IX Title "SUDOREPLAY @mansectsu@" -.TH SUDOREPLAY @mansectsu@ "August 30, 2009" "1.7.2" "MAINTENANCE COMMANDS" +.TH SUDOREPLAY @mansectsu@ "September 13, 2009" "1.7.2" "MAINTENANCE COMMANDS" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l @@ -159,7 +159,7 @@ sudoreplay \- replay sudo session logs .IX Header "SYNOPSIS" \&\fBsudoreplay\fR [\fB\-d\fR \fIdirectory\fR] [\fB\-m\fR \fImax_wait\fR] [\fB\-s\fR \fIspeed_factor\fR] \s-1ID\s0 .PP -\&\fBsudoreplay\fR [\fB\-d\fR \fIdirectory\fR] [\fB\-p\fR \fIpattern\fR] [\fB\-t\fR \fItty\fR] [\fB\-u\fR \fIuser\fR] \-l +\&\fBsudoreplay\fR [\fB\-d\fR \fIdirectory\fR] \-l [search expression] .SH "DESCRIPTION" .IX Header "DESCRIPTION" \&\fBsudoreplay\fR plays back or lists the session logs created by @@ -182,8 +182,42 @@ Use \fIdirectory\fR to for the session logs instead of the default, .IP "\-l" 12 .IX Item "-l" Enable \*(L"list mode\*(R". In this mode, \fBsudoreplay\fR will list available -session IDs. The \f(CW\*(C`\-p\*(C'\fR, <\-t> and <\-u> options can be used to -restrict the IDs that are displayed. +session IDs. If a \fIsearch expression\fR is specified, it will be +used to restrict the IDs that are displayed. An expression is +composed of the following predicates: +.RS 12 +.IP "user \fIusername\fR" 8 +.IX Item "user username" +Evaluates to true if the \s-1ID\s0 matches a command run by \fIusername\fR. +.IP "command \fIcommand pattern\fR" 8 +.IX Item "command command pattern" +Evaluates to true if the command run matches \fIcommand pattern\fR. +On systems with \s-1POSIX\s0 regular expression support, the pattern may +be an extended regular expression. On systems without \s-1POSIX\s0 regular +expression support, a simple substring match is performed instead. +.IP "tty \fItty\fR" 8 +.IX Item "tty tty" +Evaluates to true if the command was run on the specified terminal +device. The \fItty\fR should be specified without the \fI/dev/\fR prefix, +e.g. \fItty01\fR instead of \fI/dev/tty01\fR. +.IP "runas \fIrunas_user\fR" 8 +.IX Item "runas runas_user" +Evaluates to true if the command was run as the specified \fIrunas_user\fR. +Note that \fBsudo\fR runs commands as user \fIroot\fR by default. +.IP "runas \fIrunas_group\fR" 8 +.IX Item "runas runas_group" +Evaluates to true if the command was run with the specified +\&\fIrunas_group\fR. Note that unless a \fIrunas_group\fR was explicitly +specified when \fBsudo\fR was run this field will be empty in the log. +.RE +.RS 12 +.Sp +Predicates may be combined using \fIand\fR, \fIor\fR and \fI!\fR operators +as well as \f(CW\*(Aq(\*(Aq\fR and \f(CW\*(Aq)\*(Aq\fR for grouping (note that parentheses +must generally be escaped from the shell). The \fIand\fR operator is +optional, adjacent predicates have an implied \fIand\fR unless separated +by an \fIor\fR. +.RE .IP "\-m \fImax_wait\fR" 12 .IX Item "-m max_wait" Specify an upper bound on how long to wait between key presses or @@ -193,12 +227,6 @@ can be tedious when the session includes long pauses. When the \&\fI\-m\fR option is specified, \fBsudoreplay\fR will limit these pauses to at most \fImax_wait\fR seconds. The value may be specified as a floating point number, .e.g. \fI2.5\fR. -.IP "\-p \fIpattern\fR" 12 -.IX Item "-p pattern" -Restrict list output to sessions where the command matches \fIpattern\fR. -On systems with \s-1POSIX\s0 regular expression support, the pattern may -be an extended regular expression. On systems without \s-1POSIX\s0 regular -expression support, a simple substring match is performed instead. .IP "\-s \fIspeed_factor\fR" 12 .IX Item "-s speed_factor" This option causes \fBsudoreplay\fR to adjust the number of seconds @@ -206,14 +234,6 @@ it will wait between key presses or program output. This can be used to slow down or speed up the display. For example, a \&\fIspeed_factor\fR of \fI2\fR would make the output twice as fast whereas a \fIspeed_factor\fR of <.5> would make the output twice as slow. -.IP "\-t \fItty\fR" 12 -.IX Item "-t tty" -Restrict list output to sessions where the command was run on the -specified terming device. The \fItty\fR should be specified without the -\&\fI/dev/\fR prefix, e.g. \fItty01\fR instead of \fI/dev/tty01\fR. -.IP "\-u \fIuser\fR" 12 -.IX Item "-u user" -Restrict list output to sessions where the command was run by \fIuser\fR. .IP "\-V" 12 .IX Item "-V" The \fB\-V\fR (version) option causes \fBsudoreplay\fR to print its version number diff --git a/sudoreplay.pod b/sudoreplay.pod index 5e3d8daec..aebbcd952 100644 --- a/sudoreplay.pod +++ b/sudoreplay.pod @@ -24,7 +24,7 @@ sudoreplay - replay sudo session logs B [B<-d> I] [B<-m> I] [B<-s> I] ID -B [B<-d> I] [B<-p> I] [B<-t> I] [B<-u> I] -l +B [B<-d> I] -l [search expression] =head1 DESCRIPTION @@ -53,8 +53,47 @@ F. =item -l Enable "list mode". In this mode, B will list available -session IDs. The C<-p>, <-t> and <-u> options can be used to -restrict the IDs that are displayed. +session IDs. If a I is specified, it will be +used to restrict the IDs that are displayed. An expression is +composed of the following predicates: + +=over 8 + +=item user I + +Evaluates to true if the ID matches a command run by I. + +=item command I + +Evaluates to true if the command run matches I. +On systems with POSIX regular expression support, the pattern may +be an extended regular expression. On systems without POSIX regular +expression support, a simple substring match is performed instead. + +=item tty I + +Evaluates to true if the command was run on the specified terminal +device. The I should be specified without the F prefix, +e.g. F instead of F. + +=item runas I + +Evaluates to true if the command was run as the specified I. +Note that B runs commands as user I by default. + +=item runas I + +Evaluates to true if the command was run with the specified +I. Note that unless a I was explicitly +specified when B was run this field will be empty in the log. + +=back + +Predicates may be combined using I, I and I operators +as well as C<'('> and C<')'> for grouping (note that parentheses +must generally be escaped from the shell). The I operator is +optional, adjacent predicates have an implied I unless separated +by an I. =item -m I @@ -66,13 +105,6 @@ I<-m> option is specified, B will limit these pauses to at most I seconds. The value may be specified as a floating point number, .e.g. I<2.5>. -=item -p I - -Restrict list output to sessions where the command matches I. -On systems with POSIX regular expression support, the pattern may -be an extended regular expression. On systems without POSIX regular -expression support, a simple substring match is performed instead. - =item -s I This option causes B to adjust the number of seconds @@ -81,16 +113,6 @@ used to slow down or speed up the display. For example, a I of I<2> would make the output twice as fast whereas a I of <.5> would make the output twice as slow. -=item -t I - -Restrict list output to sessions where the command was run on the -specified terming device. The I should be specified without the -F prefix, e.g. F instead of F. - -=item -u I - -Restrict list output to sessions where the command was run by I. - =item -V The B<-V> (version) option causes B to print its version number -- 2.40.0