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