auditreduce.c revision 293161
1/*-
2 * Copyright (c) 2004-2008 Apple Inc.
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 * 1.  Redistributions of source code must retain the above copyright
9 *     notice, this list of conditions and the following disclaimer.
10 * 2.  Redistributions in binary form must reproduce the above copyright
11 *     notice, this list of conditions and the following disclaimer in the
12 *     documentation and/or other materials provided with the distribution.
13 * 3.  Neither the name of Apple Inc. ("Apple") nor the names of
14 *     its contributors may be used to endorse or promote products derived
15 *     from this software without specific prior written permission.
16 *
17 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND
18 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 * ARE DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR
21 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
23 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
24 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
25 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
26 * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 * POSSIBILITY OF SUCH DAMAGE.
28 */
29
30/*
31 * Tool used to merge and select audit records from audit trail files
32 */
33
34/*
35 * XXX Currently we do not support merging of records from multiple
36 * XXX audit trail files
37 * XXX We assume that records are sorted chronologically - both wrt to
38 * XXX the records present within the file and between the files themselves
39 */
40
41#include <config/config.h>
42
43#define	_GNU_SOURCE		/* Required for strptime() on glibc2. */
44
45#ifdef HAVE_FULL_QUEUE_H
46#include <sys/queue.h>
47#else
48#include <compat/queue.h>
49#endif
50
51#include <bsm/libbsm.h>
52
53#include <err.h>
54#include <grp.h>
55#include <pwd.h>
56#include <stdio.h>
57#include <stdlib.h>
58#include <sysexits.h>
59#include <string.h>
60#include <time.h>
61#include <unistd.h>
62#include <regex.h>
63#include <errno.h>
64
65#ifndef HAVE_STRLCPY
66#include <compat/strlcpy.h>
67#endif
68
69#include "auditreduce.h"
70
71static TAILQ_HEAD(tailhead, re_entry) re_head =
72    TAILQ_HEAD_INITIALIZER(re_head);
73
74extern char		*optarg;
75extern int		 optind, optopt, opterr,optreset;
76
77static au_mask_t	 maskp;		/* Class. */
78static time_t		 p_atime;	/* Created after this time. */
79static time_t		 p_btime;	/* Created before this time. */
80static int		 p_auid;	/* Audit id. */
81static int		 p_euid;	/* Effective user id. */
82static int		 p_egid;	/* Effective group id. */
83static int		 p_rgid;	/* Real group id. */
84static int		 p_ruid;	/* Real user id. */
85static int		 p_subid;	/* Subject id. */
86
87/*
88 * Maintain a dynamically sized array of events for -m
89 */
90static uint16_t		*p_evec;	/* Event type list */
91static int		 p_evec_used;	/* Number of events used */
92static int		 p_evec_alloc;	/* Number of events allocated */
93
94/*
95 * Following are the objects (-o option) that we can select upon.
96 */
97static char	*p_fileobj = NULL;
98static char	*p_msgqobj = NULL;
99static char	*p_pidobj = NULL;
100static char	*p_semobj = NULL;
101static char	*p_shmobj = NULL;
102static char	*p_sockobj = NULL;
103
104static uint32_t opttochk = 0;
105
106static void
107parse_regexp(char *re_string)
108{
109	char *orig, *copy, re_error[64];
110	struct re_entry *rep;
111	int error, nstrs, i, len;
112
113	copy = strdup(re_string);
114	orig = copy;
115	len = strlen(copy);
116	for (nstrs = 0, i = 0; i < len; i++) {
117		if (copy[i] == ',' && i > 0) {
118			if (copy[i - 1] == '\\')
119				strlcpy(&copy[i - 1], &copy[i], len);
120			else {
121				nstrs++;
122				copy[i] = '\0';
123			}
124		}
125	}
126	TAILQ_INIT(&re_head);
127	for (i = 0; i < nstrs + 1; i++) {
128		rep = calloc(1, sizeof(*rep));
129		if (rep == NULL) {
130			(void) fprintf(stderr, "calloc: %s\n",
131			    strerror(errno));
132			exit(1);
133		}
134		if (*copy == '~') {
135			copy++;
136			rep->re_negate = 1;
137		}
138		rep->re_pattern = strdup(copy);
139		error = regcomp(&rep->re_regexp, rep->re_pattern,
140		    REG_EXTENDED | REG_NOSUB);
141		if (error != 0) {
142			regerror(error, &rep->re_regexp, re_error, 64);
143			(void) fprintf(stderr, "regcomp: %s\n", re_error);
144			exit(1);
145		}
146		TAILQ_INSERT_TAIL(&re_head, rep, re_glue);
147		len = strlen(copy);
148		copy += len + 1;
149	}
150	free(orig);
151}
152
153static void
154usage(const char *msg)
155{
156	fprintf(stderr, "%s\n", msg);
157	fprintf(stderr, "Usage: auditreduce [options] [file ...]\n");
158	fprintf(stderr, "\tOptions are : \n");
159	fprintf(stderr, "\t-A : all records\n");
160	fprintf(stderr, "\t-a YYYYMMDD[HH[[MM[SS]]] : after date\n");
161	fprintf(stderr, "\t-b YYYYMMDD[HH[[MM[SS]]] : before date\n");
162	fprintf(stderr, "\t-c <flags> : matching class\n");
163	fprintf(stderr, "\t-d YYYYMMDD : on date\n");
164	fprintf(stderr, "\t-e <uid|name>  : effective user\n");
165	fprintf(stderr, "\t-f <gid|group> : effective group\n");
166	fprintf(stderr, "\t-g <gid|group> : real group\n");
167	fprintf(stderr, "\t-j <pid> : subject id \n");
168	fprintf(stderr, "\t-m <evno|evname> : matching event\n");
169	fprintf(stderr, "\t-o objecttype=objectvalue\n");
170	fprintf(stderr, "\t\t file=<pathname>\n");
171	fprintf(stderr, "\t\t msgqid=<ID>\n");
172	fprintf(stderr, "\t\t pid=<ID>\n");
173	fprintf(stderr, "\t\t semid=<ID>\n");
174	fprintf(stderr, "\t\t shmid=<ID>\n");
175	fprintf(stderr, "\t-r <uid|name> : real user\n");
176	fprintf(stderr, "\t-u <uid|name> : audit user\n");
177	fprintf(stderr, "\t-v : select non-matching records\n");
178	exit(EX_USAGE);
179}
180
181/*
182 * Check if the given auid matches the selection criteria.
183 */
184static int
185select_auid(int au)
186{
187
188	/* Check if we want to select on auid. */
189	if (ISOPTSET(opttochk, OPT_u)) {
190		if (au != p_auid)
191			return (0);
192	}
193	return (1);
194}
195
196/*
197 * Check if the given euid matches the selection criteria.
198 */
199static int
200select_euid(int euser)
201{
202
203	/* Check if we want to select on euid. */
204	if (ISOPTSET(opttochk, OPT_e)) {
205		if (euser != p_euid)
206			return (0);
207	}
208	return (1);
209}
210
211/*
212 * Check if the given egid matches the selection criteria.
213 */
214static int
215select_egid(int egrp)
216{
217
218	/* Check if we want to select on egid. */
219	if (ISOPTSET(opttochk, OPT_f)) {
220		if (egrp != p_egid)
221			return (0);
222	}
223	return (1);
224}
225
226/*
227 * Check if the given rgid matches the selection criteria.
228 */
229static int
230select_rgid(int grp)
231{
232
233	/* Check if we want to select on rgid. */
234	if (ISOPTSET(opttochk, OPT_g)) {
235		if (grp != p_rgid)
236			return (0);
237	}
238	return (1);
239}
240
241/*
242 * Check if the given ruid matches the selection criteria.
243 */
244static int
245select_ruid(int user)
246{
247
248	/* Check if we want to select on rgid. */
249	if (ISOPTSET(opttochk, OPT_r)) {
250		if (user != p_ruid)
251			return (0);
252	}
253	return (1);
254}
255
256/*
257 * Check if the given subject id (pid) matches the selection criteria.
258 */
259static int
260select_subid(int subid)
261{
262
263	/* Check if we want to select on subject uid. */
264	if (ISOPTSET(opttochk, OPT_j)) {
265		if (subid != p_subid)
266			return (0);
267	}
268	return (1);
269}
270
271
272/*
273 * Check if object's pid maches the given pid.
274 */
275static int
276select_pidobj(uint32_t pid)
277{
278
279	if (ISOPTSET(opttochk, OPT_op)) {
280		if (pid != (uint32_t)strtol(p_pidobj, (char **)NULL, 10))
281			return (0);
282	}
283	return (1);
284}
285
286/*
287 * Check if the given ipc object with the given type matches the selection
288 * criteria.
289 */
290static int
291select_ipcobj(u_char type, uint32_t id, uint32_t *optchkd)
292{
293
294	if (type == AT_IPC_MSG) {
295		SETOPT((*optchkd), OPT_om);
296		if (ISOPTSET(opttochk, OPT_om)) {
297			if (id != (uint32_t)strtol(p_msgqobj, (char **)NULL,
298			    10))
299				return (0);
300		}
301		return (1);
302	} else if (type == AT_IPC_SEM) {
303		SETOPT((*optchkd), OPT_ose);
304		if (ISOPTSET(opttochk, OPT_ose)) {
305			if (id != (uint32_t)strtol(p_semobj, (char **)NULL, 10))
306				return (0);
307		}
308		return (1);
309	} else if (type == AT_IPC_SHM) {
310		SETOPT((*optchkd), OPT_osh);
311		if (ISOPTSET(opttochk, OPT_osh)) {
312			if (id != (uint32_t)strtol(p_shmobj, (char **)NULL, 10))
313				return (0);
314		}
315		return (1);
316	}
317
318	/* Unknown type -- filter if *any* ipc filtering is required. */
319	if (ISOPTSET(opttochk, OPT_om) || ISOPTSET(opttochk, OPT_ose)
320	    || ISOPTSET(opttochk, OPT_osh))
321		return (0);
322
323	return (1);
324}
325
326
327/*
328 * Check if the file name matches selection criteria.
329 */
330static int
331select_filepath(char *path, uint32_t *optchkd)
332{
333	struct re_entry *rep;
334	int match;
335
336	SETOPT((*optchkd), OPT_of);
337	match = 1;
338	if (ISOPTSET(opttochk, OPT_of)) {
339		match = 0;
340		TAILQ_FOREACH(rep, &re_head, re_glue) {
341			if (regexec(&rep->re_regexp, path, 0, NULL,
342			    0) != REG_NOMATCH)
343				return (!rep->re_negate);
344		}
345	}
346	return (match);
347}
348
349/*
350 * Returns 1 if the following pass the selection rules:
351 *
352 * before-time,
353 * after time,
354 * date,
355 * class,
356 * event
357 */
358static int
359select_hdr32(tokenstr_t tok, uint32_t *optchkd)
360{
361	uint16_t *ev;
362	int match;
363
364	SETOPT((*optchkd), (OPT_A | OPT_a | OPT_b | OPT_c | OPT_m | OPT_v));
365
366	/* The A option overrides a, b and d. */
367	if (!ISOPTSET(opttochk, OPT_A)) {
368		if (ISOPTSET(opttochk, OPT_a)) {
369			if (difftime((time_t)tok.tt.hdr32.s, p_atime) < 0) {
370				/* Record was created before p_atime. */
371				return (0);
372			}
373		}
374
375		if (ISOPTSET(opttochk, OPT_b)) {
376			if (difftime(p_btime, (time_t)tok.tt.hdr32.s) < 0) {
377				/* Record was created after p_btime. */
378				return (0);
379			}
380		}
381	}
382
383	if (ISOPTSET(opttochk, OPT_c)) {
384		/*
385		 * Check if the classes represented by the event matches
386		 * given class.
387		 */
388		if (au_preselect(tok.tt.hdr32.e_type, &maskp, AU_PRS_BOTH,
389		    AU_PRS_USECACHE) != 1)
390			return (0);
391	}
392
393	/* Check if event matches. */
394	if (ISOPTSET(opttochk, OPT_m)) {
395		match = 0;
396		for (ev = p_evec; ev < &p_evec[p_evec_used]; ev++)
397			if (tok.tt.hdr32.e_type == *ev)
398				match = 1;
399		if (match == 0)
400			return (0);
401	}
402
403	return (1);
404}
405
406static int
407select_return32(tokenstr_t tok_ret32, tokenstr_t tok_hdr32, uint32_t *optchkd)
408{
409	int sorf;
410
411	SETOPT((*optchkd), (OPT_c));
412	if (tok_ret32.tt.ret32.status == 0)
413		sorf = AU_PRS_SUCCESS;
414	else
415		sorf = AU_PRS_FAILURE;
416	if (ISOPTSET(opttochk, OPT_c)) {
417		if (au_preselect(tok_hdr32.tt.hdr32.e_type, &maskp, sorf,
418		    AU_PRS_USECACHE) != 1)
419			return (0);
420	}
421	return (1);
422}
423
424/*
425 * Return 1 if checks for the the following succeed
426 * auid,
427 * euid,
428 * egid,
429 * rgid,
430 * ruid,
431 * process id
432 */
433static int
434select_proc32(tokenstr_t tok, uint32_t *optchkd)
435{
436
437	SETOPT((*optchkd), (OPT_u | OPT_e | OPT_f | OPT_g | OPT_r | OPT_op));
438
439	if (!select_auid(tok.tt.proc32.auid))
440		return (0);
441	if (!select_euid(tok.tt.proc32.euid))
442		return (0);
443	if (!select_egid(tok.tt.proc32.egid))
444		return (0);
445	if (!select_rgid(tok.tt.proc32.rgid))
446		return (0);
447	if (!select_ruid(tok.tt.proc32.ruid))
448		return (0);
449	if (!select_pidobj(tok.tt.proc32.pid))
450		return (0);
451	return (1);
452}
453
454/*
455 * Return 1 if checks for the the following succeed
456 * auid,
457 * euid,
458 * egid,
459 * rgid,
460 * ruid,
461 * subject id
462 */
463static int
464select_subj32(tokenstr_t tok, uint32_t *optchkd)
465{
466
467	SETOPT((*optchkd), (OPT_u | OPT_e | OPT_f | OPT_g | OPT_r | OPT_j));
468
469	if (!select_auid(tok.tt.subj32.auid))
470		return (0);
471	if (!select_euid(tok.tt.subj32.euid))
472		return (0);
473	if (!select_egid(tok.tt.subj32.egid))
474		return (0);
475	if (!select_rgid(tok.tt.subj32.rgid))
476		return (0);
477	if (!select_ruid(tok.tt.subj32.ruid))
478		return (0);
479	if (!select_subid(tok.tt.subj32.pid))
480		return (0);
481	return (1);
482}
483
484/*
485 * Read each record from the audit trail.  Check if it is selected after
486 * passing through each of the options
487 */
488static int
489select_records(FILE *fp)
490{
491	tokenstr_t tok_hdr32_copy;
492	u_char *buf;
493	tokenstr_t tok;
494	int reclen;
495	int bytesread;
496	int selected;
497	uint32_t optchkd;
498	int print;
499
500	int err = 0;
501	while ((reclen = au_read_rec(fp, &buf)) != -1) {
502		optchkd = 0;
503		bytesread = 0;
504		selected = 1;
505		while ((selected == 1) && (bytesread < reclen)) {
506			if (-1 == au_fetch_tok(&tok, buf + bytesread,
507			    reclen - bytesread)) {
508				/* Is this an incomplete record? */
509				err = 1;
510				break;
511			}
512
513			/*
514			 * For each token type we have have different
515			 * selection criteria.
516			 */
517			switch(tok.id) {
518			case AUT_HEADER32:
519					selected = select_hdr32(tok,
520					    &optchkd);
521					bcopy(&tok, &tok_hdr32_copy,
522					    sizeof(tok));
523					break;
524
525			case AUT_PROCESS32:
526					selected = select_proc32(tok,
527					    &optchkd);
528					break;
529
530			case AUT_SUBJECT32:
531					selected = select_subj32(tok,
532					    &optchkd);
533					break;
534
535			case AUT_IPC:
536					selected = select_ipcobj(
537					    tok.tt.ipc.type, tok.tt.ipc.id,
538					    &optchkd);
539					break;
540
541			case AUT_PATH:
542					selected = select_filepath(
543					    tok.tt.path.path, &optchkd);
544					break;
545
546			case AUT_RETURN32:
547				selected = select_return32(tok,
548				    tok_hdr32_copy, &optchkd);
549				break;
550
551			default:
552				break;
553			}
554			bytesread += tok.len;
555		}
556		/* Check if all the options were matched. */
557		print = ((selected == 1) && (!err) && (!(opttochk & ~optchkd)));
558		if (ISOPTSET(opttochk, OPT_v))
559			print = !print;
560		if (print)
561			(void) fwrite(buf, 1, reclen, stdout);
562		free(buf);
563	}
564	return (0);
565}
566
567/*
568 * The -o option has the form object_type=object_value.  Identify the object
569 * components.
570 */
571static void
572parse_object_type(char *name, char *val)
573{
574	if (val == NULL)
575		return;
576
577	if (!strcmp(name, FILEOBJ)) {
578		p_fileobj = val;
579		parse_regexp(val);
580		SETOPT(opttochk, OPT_of);
581	} else if (!strcmp(name, MSGQIDOBJ)) {
582		p_msgqobj = val;
583		SETOPT(opttochk, OPT_om);
584	} else if (!strcmp(name, PIDOBJ)) {
585		p_pidobj = val;
586		SETOPT(opttochk, OPT_op);
587	} else if (!strcmp(name, SEMIDOBJ)) {
588		p_semobj = val;
589		SETOPT(opttochk, OPT_ose);
590	} else if (!strcmp(name, SHMIDOBJ)) {
591		p_shmobj = val;
592		SETOPT(opttochk, OPT_osh);
593	} else if (!strcmp(name, SOCKOBJ)) {
594		p_sockobj = val;
595		SETOPT(opttochk, OPT_oso);
596	} else
597		usage("unknown value for -o");
598}
599
600int
601main(int argc, char **argv)
602{
603	struct group *grp;
604	struct passwd *pw;
605	struct tm tm;
606	au_event_t *n;
607	FILE *fp;
608	int i;
609	char *objval, *converr;
610	int ch;
611	char timestr[128];
612	char *fname;
613	uint16_t *etp;
614
615	converr = NULL;
616
617	while ((ch = getopt(argc, argv, "Aa:b:c:d:e:f:g:j:m:o:r:u:v")) != -1) {
618		switch(ch) {
619		case 'A':
620			SETOPT(opttochk, OPT_A);
621			break;
622
623		case 'a':
624			if (ISOPTSET(opttochk, OPT_a)) {
625				usage("d is exclusive with a and b");
626			}
627			SETOPT(opttochk, OPT_a);
628			bzero(&tm, sizeof(tm));
629			strptime(optarg, "%Y%m%d%H%M%S", &tm);
630			strftime(timestr, sizeof(timestr), "%Y%m%d%H%M%S",
631			    &tm);
632			/* fprintf(stderr, "Time converted = %s\n", timestr); */
633			p_atime = mktime(&tm);
634			break;
635
636		case 'b':
637			if (ISOPTSET(opttochk, OPT_b)) {
638				usage("d is exclusive with a and b");
639			}
640			SETOPT(opttochk, OPT_b);
641			bzero(&tm, sizeof(tm));
642			strptime(optarg, "%Y%m%d%H%M%S", &tm);
643			strftime(timestr, sizeof(timestr), "%Y%m%d%H%M%S",
644			    &tm);
645			/* fprintf(stderr, "Time converted = %s\n", timestr); */
646			p_btime = mktime(&tm);
647			break;
648
649		case 'c':
650			if (0 != getauditflagsbin(optarg, &maskp)) {
651				/* Incorrect class */
652				usage("Incorrect class");
653			}
654			SETOPT(opttochk, OPT_c);
655			break;
656
657		case 'd':
658			if (ISOPTSET(opttochk, OPT_b) || ISOPTSET(opttochk,
659			    OPT_a))
660				usage("'d' is exclusive with 'a' and 'b'");
661			SETOPT(opttochk, OPT_d);
662			bzero(&tm, sizeof(tm));
663			strptime(optarg, "%Y%m%d", &tm);
664			strftime(timestr, sizeof(timestr), "%Y%m%d", &tm);
665			/* fprintf(stderr, "Time converted = %s\n", timestr); */
666			p_atime = mktime(&tm);
667			tm.tm_hour = 23;
668			tm.tm_min = 59;
669			tm.tm_sec = 59;
670			strftime(timestr, sizeof(timestr), "%Y%m%d", &tm);
671			/* fprintf(stderr, "Time converted = %s\n", timestr); */
672			p_btime = mktime(&tm);
673			break;
674
675		case 'e':
676			p_euid = strtol(optarg, &converr, 10);
677			if (*converr != '\0') {
678				/* Try the actual name */
679				if ((pw = getpwnam(optarg)) == NULL)
680					break;
681				p_euid = pw->pw_uid;
682			}
683			SETOPT(opttochk, OPT_e);
684			break;
685
686		case 'f':
687			p_egid = strtol(optarg, &converr, 10);
688			if (*converr != '\0') {
689				/* Try actual group name. */
690				if ((grp = getgrnam(optarg)) == NULL)
691					break;
692				p_egid = grp->gr_gid;
693			}
694			SETOPT(opttochk, OPT_f);
695			break;
696
697		case 'g':
698			p_rgid = strtol(optarg, &converr, 10);
699			if (*converr != '\0') {
700				/* Try actual group name. */
701				if ((grp = getgrnam(optarg)) == NULL)
702					break;
703				p_rgid = grp->gr_gid;
704			}
705			SETOPT(opttochk, OPT_g);
706			break;
707
708		case 'j':
709			p_subid = strtol(optarg, (char **)NULL, 10);
710			SETOPT(opttochk, OPT_j);
711			break;
712
713		case 'm':
714			if (p_evec == NULL) {
715				p_evec_alloc = 32;
716				p_evec = malloc(sizeof(*etp) * p_evec_alloc);
717				if (p_evec == NULL)
718					err(1, "malloc");
719			} else if (p_evec_alloc == p_evec_used) {
720				p_evec_alloc <<= 1;
721				p_evec = realloc(p_evec,
722				    sizeof(*p_evec) * p_evec_alloc);
723				if (p_evec == NULL)
724					err(1, "realloc");
725			}
726			etp = &p_evec[p_evec_used++];
727			*etp = strtol(optarg, (char **)NULL, 10);
728			if (*etp == 0) {
729				/* Could be the string representation. */
730				n = getauevnonam(optarg);
731				if (n == NULL)
732					usage("Incorrect event name");
733				*etp = *n;
734			}
735			SETOPT(opttochk, OPT_m);
736			break;
737
738		case 'o':
739			objval = strchr(optarg, '=');
740			if (objval != NULL) {
741				*objval = '\0';
742				objval += 1;
743				parse_object_type(optarg, objval);
744			}
745			break;
746
747		case 'r':
748			p_ruid = strtol(optarg, &converr, 10);
749			if (*converr != '\0') {
750				if ((pw = getpwnam(optarg)) == NULL)
751					break;
752				p_ruid = pw->pw_uid;
753			}
754			SETOPT(opttochk, OPT_r);
755			break;
756
757		case 'u':
758			p_auid = strtol(optarg, &converr, 10);
759			if (*converr != '\0') {
760				if ((pw = getpwnam(optarg)) == NULL)
761					break;
762				p_auid = pw->pw_uid;
763			}
764			SETOPT(opttochk, OPT_u);
765			break;
766
767		case 'v':
768			SETOPT(opttochk, OPT_v);
769			break;
770
771		case '?':
772		default:
773			usage("Unknown option");
774		}
775	}
776	argv += optind;
777	argc -= optind;
778
779	if (argc == 0) {
780		if (select_records(stdin) == -1)
781			errx(EXIT_FAILURE,
782			    "Couldn't select records from stdin");
783		exit(EXIT_SUCCESS);
784	}
785
786	/*
787	 * XXX: We should actually be merging records here.
788	 */
789	for (i = 0; i < argc; i++) {
790		fname = argv[i];
791		fp = fopen(fname, "r");
792		if (fp == NULL)
793			errx(EXIT_FAILURE, "Couldn't open %s", fname);
794		if (select_records(fp) == -1) {
795			errx(EXIT_FAILURE, "Couldn't select records %s",
796			    fname);
797		}
798		fclose(fp);
799	}
800	exit(EXIT_SUCCESS);
801}
802