Browse code

freshclam: add support for submitting detection statistics (bb#777)

git-svn: trunk@4241

Tomasz Kojm authored on 2008/10/10 23:22:00
Showing 8 changed files
... ...
@@ -1,3 +1,8 @@
1
+Fri Oct 10 16:38:45 CEST 2008 (tk)
2
+----------------------------------
3
+  * freshclam: add support for submitting detection statistics (bb#777)
4
+	       New options: SubmitDetectionStats and --submit-stats
5
+
1 6
 Thu Oct  9 12:17:45 EEST 2008 (edwin)
2 7
 -------------------------------------
3 8
  * ChangeLog, clamav-config.h.in, configure, configure.in,
... ...
@@ -67,6 +67,9 @@ Execute COMMAND when freshclam reports outdated version. In the command string %
67 67
 .TP 
68 68
 \fB\-\-list\-mirrors\fR
69 69
 Print mirror details from mirrors.dat (cache file for the mirror manager).
70
+.TP
71
+\fB\-\-submit\-stats[=/path/to/clamd.conf]\fR
72
+Upload detection statistics to the ClamAV Project (see freshclam.conf(5):SubmitDetectionStats for more details). No database update will be performed. This option only works in the interactive mode.
70 73
 .SH "EXAMPLES"
71 74
 .LP 
72 75
 .TP 
... ...
@@ -143,6 +143,11 @@ Default: 10
143 143
 Timeout in seconds when reading from database server.
144 144
 .br 
145 145
 Default: 30
146
+.TP
147
+\fBSubmitDetectionStats STRING\fR
148
+When enabled freshclam will submit statistics to the ClamAV Project about the latest virus detections in your environment. The ClamAV maintainers will then use this data to determine what types of malware are the most detected in the field and in what geographic area they are. This feature requires LogTime and LogFile to be enabled in clamd.conf. The path for clamd.conf file must be provided.
149
+.br
150
+Default: disabled
146 151
 .SH "FILES"
147 152
 .LP 
148 153
 @CFGDIR@/freshclam.conf
... ...
@@ -141,3 +141,11 @@ DatabaseMirror database.clamav.net
141 141
 # Timeout in seconds when reading from database server.
142 142
 # Default: 30
143 143
 #ReceiveTimeout 60
144
+
145
+# When enabled freshclam will submit statistics to the ClamAV Project about
146
+# the latest virus detections in your environment. The ClamAV maintainers
147
+# will then use this data to determine what types of malware are the most
148
+# detected in the field and in what geographic area they are.
149
+# This feature requires LogTime and LogFile to be enabled in clamd.conf.
150
+# Default: no
151
+#SubmitDetectionStats /path/to/clamd.conf
... ...
@@ -144,6 +144,7 @@ static void help(void)
144 144
     mprintf("    --on-error-execute=COMMAND           execute COMMAND if errors occured\n");
145 145
     mprintf("    --on-outdated-execute=COMMAND        execute COMMAND when software is outdated\n");
146 146
     mprintf("    --list-mirrors                       print mirrors from mirrors.dat\n");
147
+    mprintf("    --submit-stats[=/path/clamd.conf]    only submit detection statistics\n");
147 148
 
148 149
     mprintf("\n");
149 150
 }
... ...
@@ -233,6 +234,7 @@ int main(int argc, char **argv)
233 233
 	    {"on-error-execute", 1, 0, 0},
234 234
 	    {"on-outdated-execute", 1, 0, 0},
235 235
 	    {"list-mirrors", 0, 0, 0},
236
+	    {"submit-stats", 2, 0, 0},
236 237
 	    {0, 0, 0, 0}
237 238
     	};
238 239
 
... ...
@@ -526,7 +528,10 @@ int main(int argc, char **argv)
526 526
 	while(!terminate) {
527 527
 	    ret = download(copt, opt, newdir, cfgfile);
528 528
 
529
-            if(ret > 1) {
529
+	    if(ret <= 1) {
530
+		if((cpt = cfgopt(copt, "SubmitDetectionStats"))->enabled)
531
+		    submitstats(cpt->strarg, copt);
532
+            } else  {
530 533
 	        if(opt_check(opt, "on-error-execute"))
531 534
 		    arg = opt_arg(opt, "on-error-execute");
532 535
 		else if((cpt = cfgopt(copt, "OnErrorExecute"))->enabled)
... ...
@@ -575,8 +580,19 @@ int main(int argc, char **argv)
575 575
 #endif	    
576 576
 	}
577 577
 
578
-    } else
579
-	ret = download(copt, opt, newdir, cfgfile);
578
+    } else {
579
+	if(opt_check(opt, "submit-stats")) {
580
+	    cfgfile = opt_arg(opt, "submit-stats");
581
+	    if(!cfgfile)
582
+		cfgfile = CONFDIR"/clamd.conf";
583
+	    ret = submitstats(cfgfile, copt);
584
+	} else {
585
+	    ret = download(copt, opt, newdir, cfgfile);
586
+
587
+	    if((cpt = cfgopt(copt, "SubmitDetectionStats"))->enabled)
588
+		submitstats(cpt->strarg, copt);
589
+	}
590
+    }
580 591
 
581 592
     if(ret > 1) {
582 593
 	if(opt_check(opt, "on-error-execute"))
... ...
@@ -26,6 +26,8 @@
26 26
 #include <winsock.h>	/* only needed in CL_EXPERIMENTAL */
27 27
 #endif
28 28
 
29
+#define _XOPEN_SOURCE 500
30
+
29 31
 #if HAVE_CONFIG_H
30 32
 #include "clamav-config.h"
31 33
 #endif
... ...
@@ -36,6 +38,7 @@
36 36
 #include <unistd.h>
37 37
 #endif
38 38
 #include <string.h>
39
+#include <strings.h>
39 40
 #include <ctype.h>
40 41
 #ifndef C_WINDOWS
41 42
 #include <netinet/in.h>
... ...
@@ -260,7 +263,7 @@ static int wwwconnect(const char *server, const char *proxy, int pport, char *ip
260 260
 	    return -1;
261 261
 	}
262 262
 
263
-	if((ret = mirman_check(addr, rp->ai_family, mdat, &md))) {
263
+	if(mdat && (ret = mirman_check(addr, rp->ai_family, mdat, &md))) {
264 264
 	    if(ret == 1)
265 265
 		logg("Ignoring mirror %s (due to previous errors)\n", ipaddr);
266 266
 	    else
... ...
@@ -271,7 +274,7 @@ static int wwwconnect(const char *server, const char *proxy, int pport, char *ip
271 271
 		continue;
272 272
 	}
273 273
 
274
-	if(loadbal) {
274
+	if(mdat && loadbal) {
275 275
 	    if(!ret) {
276 276
 		if(!md) {
277 277
 		    loadbal_rp = rp;
... ...
@@ -327,11 +330,13 @@ static int wwwconnect(const char *server, const char *proxy, int pport, char *ip
327 327
 	    }
328 328
 	    continue;
329 329
 	} else {
330
-	    if(rp->ai_family == AF_INET)
331
-		mdat->currip[0] = *((uint32_t *) addr);
332
-	    else
333
-		memcpy(mdat->currip, addr, 4 * sizeof(uint32_t));
334
-	    mdat->af = rp->ai_family;
330
+	    if(mdat) {
331
+		if(rp->ai_family == AF_INET)
332
+		    mdat->currip[0] = *((uint32_t *) addr);
333
+		else
334
+		    memcpy(mdat->currip, addr, 4 * sizeof(uint32_t));
335
+		mdat->af = rp->ai_family;
336
+	    }
335 337
 	    freeaddrinfo(res);
336 338
 	    return socketfd;
337 339
 	}
... ...
@@ -351,7 +356,7 @@ static int wwwconnect(const char *server, const char *proxy, int pport, char *ip
351 351
 	sprintf(ipaddr, "%u.%u.%u.%u", ia[0], ia[1], ia[2], ia[3]);
352 352
 
353 353
 	ips++;
354
-	if((ret = mirman_check(&((struct in_addr *) ia)->s_addr, AF_INET, mdat, NULL))) {
354
+	if(mdat && (ret = mirman_check(&((struct in_addr *) ia)->s_addr, AF_INET, mdat, NULL))) {
355 355
 	    if(ret == 1)
356 356
 		logg("Ignoring mirror %s (due to previous errors)\n", ipaddr);
357 357
 	    else
... ...
@@ -384,19 +389,309 @@ static int wwwconnect(const char *server, const char *proxy, int pport, char *ip
384 384
 	    closesocket(socketfd);
385 385
 	    continue;
386 386
 	} else {
387
-	    mdat->currip[0] = ((struct in_addr *) ia)->s_addr;
388
-	    mdat->af = AF_INET;
387
+	    if(mdat) {
388
+		mdat->currip[0] = ((struct in_addr *) ia)->s_addr;
389
+		mdat->af = AF_INET;
390
+	    }
389 391
 	    return socketfd;
390 392
 	}
391 393
     }
392 394
 #endif
393 395
 
394
-    if(can_whitelist && ips && (ips == ignored))
396
+    if(mdat && can_whitelist && ips && (ips == ignored))
395 397
 	mirman_whitelist(mdat);
396 398
 
397 399
     return -2;
398 400
 }
399 401
 
402
+static const char *readbline(int fd, char *buf, int bufsize, int filesize, int *bread)
403
+{
404
+	char *pt;
405
+	int ret, end;
406
+
407
+    if(!*bread) {
408
+	if(bufsize < filesize)
409
+	    lseek(fd, -bufsize, SEEK_END);
410
+	*bread = read(fd, buf, bufsize - 1);
411
+	if(!*bread || *bread == -1)
412
+	    return NULL;
413
+	buf[*bread] = 0;
414
+    }
415
+
416
+    pt = strrchr(buf, '\n');
417
+    if(!pt)
418
+	return NULL;
419
+    *pt = 0;
420
+    pt = strrchr(buf, '\n');
421
+    if(pt) {
422
+	return ++pt;
423
+    } else if(*bread == filesize) {
424
+	return buf;
425
+    } else {
426
+	*bread -= strlen(buf) + 1;
427
+	end = filesize - *bread;
428
+	if(end < bufsize) {
429
+	    if((ret = lseek(fd, 0, SEEK_SET)) != -1)
430
+		ret = read(fd, buf, end);
431
+	} else {
432
+	    if((ret = lseek(fd, end - bufsize, SEEK_SET)) != -1)
433
+		ret = read(fd, buf, bufsize - 1);
434
+	}
435
+	if(!ret || ret == -1)
436
+	    return NULL;
437
+	buf[ret] = 0;
438
+	*bread += ret;
439
+	pt = strrchr(buf, '\n');
440
+	if(!pt)
441
+	    return buf;
442
+	*pt = 0;
443
+	pt = strrchr(buf, '\n');
444
+	if(pt)
445
+	    return ++pt;
446
+	else if(strlen(buf))
447
+	    return buf;
448
+	else 
449
+	    return NULL;
450
+    }
451
+}
452
+
453
+/*
454
+ * TODO:
455
+ * - strptime() is most likely not portable enough
456
+ * - proxy support
457
+ */
458
+int submitstats(const char *clamdcfg, const struct cfgstruct *copt)
459
+{
460
+	int fd, sd, bread, lread = 0, cnt, ret;
461
+	char post[SUBMIT_MIN_ENTRIES * 256 + 512];
462
+	char query[SUBMIT_MIN_ENTRIES * 256];
463
+	char buff[512], statsdat[512], newstatsdat[512], uastr[128];
464
+	char logfile[256], fbuff[FILEBUFF];
465
+	char *pt, *pt2;
466
+	const char *line;
467
+	struct cfgstruct *clamdopt;
468
+	const struct cfgstruct *cpt;
469
+	struct stat sb;
470
+	struct tm tms;
471
+	time_t epoch;
472
+	unsigned int qcnt, entries, submitted = 0, permfail = 0;
473
+
474
+
475
+    if(!(clamdopt = getcfg(clamdcfg, 1))) {
476
+	logg("!SubmitDetectionStats: Can't open or parse configuration file %s\n", clamdcfg);
477
+	return 56;
478
+    }
479
+
480
+    if(!(cpt = cfgopt(clamdopt, "LogFile"))->enabled) {
481
+	logg("!SubmitDetectionStats: LogFile needs to be enabled in %s\n", clamdcfg);
482
+	freecfg(clamdopt);
483
+	return 56;
484
+    }
485
+    strncpy(logfile, cpt->strarg, sizeof(logfile));
486
+    logfile[sizeof(logfile) - 1] = 0;
487
+
488
+    if(!cfgopt(clamdopt, "LogTime")->enabled) {
489
+	logg("!SubmitDetectionStats: LogTime needs to be enabled in %s\n", clamdcfg);
490
+	freecfg(clamdopt);
491
+	return 56;
492
+    }
493
+    freecfg(clamdopt);
494
+
495
+    if((fd = open("stats.dat", O_RDONLY)) != -1) {
496
+	if((bread = read(fd, statsdat, sizeof(statsdat) - 1)) == -1) {
497
+	    logg("^SubmitDetectionStats: Can't read stats.dat\n");
498
+	    bread = 0;
499
+	}
500
+	statsdat[bread] = 0;
501
+	close(fd);
502
+    } else {
503
+	*statsdat = 0;
504
+    }
505
+
506
+    if((fd = open(logfile, O_RDONLY)) == -1) {
507
+	logg("!SubmitDetectionStats: Can't open %s for reading\n", logfile);
508
+	return 56;
509
+    }
510
+
511
+    if(fstat(fd, &sb) == -1) {
512
+	logg("!SubmitDetectionStats: fstat() failed\n");
513
+	close(fd);
514
+	return 56;
515
+    }
516
+
517
+    while((line = readbline(fd, fbuff, FILEBUFF, sb.st_size, &lread)))
518
+	if(strstr(line, "FOUND"))
519
+	    break;
520
+
521
+    if(!line) {
522
+	logg("SubmitDetectionStats: No detection records found\n");
523
+	close(fd);
524
+	return 1;
525
+    }
526
+
527
+    if(*statsdat && !strcmp(line, statsdat)) {
528
+	logg("SubmitDetectionStats: No new detection records found\n");
529
+	close(fd);
530
+	return 1;
531
+    } else {
532
+	strncpy(newstatsdat, line, sizeof(newstatsdat));
533
+    }
534
+
535
+    if((cpt = cfgopt(copt, "HTTPUserAgent"))->enabled)
536
+	strncpy(uastr, cpt->strarg, sizeof(uastr));
537
+    else
538
+	snprintf(uastr, sizeof(uastr), PACKAGE"/%s (OS: "TARGET_OS_TYPE", ARCH: "TARGET_ARCH_TYPE", CPU: "TARGET_CPU_TYPE")", get_version());
539
+    uastr[sizeof(uastr) - 1] = 0;
540
+
541
+    ret = 0;
542
+    memset(query, 0, sizeof(query));
543
+    qcnt = 0;
544
+    entries = 0;
545
+    do {
546
+	if(!strstr(line, " FOUND"))
547
+	    continue;
548
+
549
+	if(*statsdat && !strcmp(line, statsdat))
550
+	    break;
551
+
552
+	strncpy(buff, line, sizeof(buff));
553
+	buff[sizeof(buff) - 1] = 0;
554
+
555
+	if(!(pt = strstr(buff, " -> "))) {
556
+	    logg("*SubmitDetectionStats: Skipping detection entry logged without time\b");
557
+	    continue;
558
+	}
559
+	*pt = 0;
560
+	pt += 4;
561
+
562
+	if(!strptime(buff, "%a %b  %d %H:%M:%S %Y", &tms) || (epoch = mktime(&tms)) == -1) {
563
+	    logg("!SubmitDetectionStats: Failed to convert date string\n");
564
+	    ret = 1;
565
+	    break;
566
+	}
567
+
568
+	pt2 = strstr(pt, " FOUND");
569
+	*pt2 = 0;
570
+
571
+	if(!(pt2 = strrchr(pt, ':'))) {
572
+	    logg("!SubmitDetectionStats: Incorrect format of the log file (1)\n");
573
+	    ret = 1;
574
+	    break;
575
+	}
576
+	*pt2 = 0;
577
+	pt2 += 2;
578
+
579
+#ifdef C_WINDOWS
580
+	if(!(pt = strrchr(pt, '\\'))) {
581
+#else
582
+	if(!(pt = strrchr(pt, '/'))) {
583
+#endif
584
+	    logg("!SubmitDetectionStats: Incorrect format of the log file (2)\n");
585
+	    ret = 1;
586
+	    break;
587
+	}
588
+	*pt++ = 0;
589
+
590
+	qcnt += snprintf(&query[qcnt], sizeof(query) - qcnt, "ts[]=%u&fname[]=%s&virus[]=%s&", (unsigned int) epoch, pt, pt2);
591
+	entries++;
592
+
593
+	if(entries == SUBMIT_MIN_ENTRIES) {
594
+	    sd = wwwconnect("stats.clamav.net", NULL, 0, NULL, cfgopt(copt, "LocalIPAddress")->strarg, cfgopt(copt, "ConnectTimeout")->numarg, NULL, 0, 0);
595
+	    if(sd == -1) {
596
+		logg("!SubmitDetectionStats: Can't connect to server\n");
597
+		ret = 52;
598
+		break;
599
+	    }
600
+
601
+	    query[sizeof(query) - 1] = 0;
602
+	    snprintf(post, sizeof(post),
603
+		"POST /submit.php HTTP/1.0\r\n"
604
+		"Host: stats.clamav.net\r\n"
605
+		"Content-Type: application/x-www-form-urlencoded\r\n"
606
+		"User-Agent: %s\r\n"
607
+		"Content-Length: %u\r\n\n"
608
+		"%s",
609
+	    uastr, (unsigned int) strlen(query), query);
610
+
611
+	    if(send(sd, post, strlen(post), 0) < 0) {
612
+		logg("!SubmitDetectionStats: Can't write to socket\n");
613
+		ret = 52;
614
+		closesocket(sd);
615
+		break;
616
+	    }
617
+
618
+	    pt = post;
619
+	    cnt = sizeof(post) - 1;
620
+#ifdef SO_ERROR
621
+	    while((bread = wait_recv(sd, pt, cnt, 0, cfgopt(copt, "ReceiveTimeout")->numarg)) > 0) {
622
+#else
623
+	    while((bread = recv(sd, pt, cnt, 0)) > 0) {
624
+#endif
625
+		pt += bread;
626
+		cnt -= bread;
627
+		if(cnt <= 0)
628
+		    break;
629
+	    }
630
+	    *pt = 0;
631
+	    closesocket(sd);
632
+
633
+	    if(bread < 0) {
634
+		logg("!SubmitDetectionStats: Can't read from socket\n");
635
+		ret = 52;
636
+		break;
637
+	    }
638
+
639
+	    if(strstr(post, "SUBMIT_OK")) {
640
+		submitted += entries;
641
+		if(submitted + SUBMIT_MIN_ENTRIES > SUBMIT_MAX_ENTRIES)
642
+		    break;
643
+		qcnt = 0;
644
+		entries = 0;
645
+		memset(query, 0, sizeof(query));
646
+		continue;
647
+	    }
648
+
649
+	    ret = 52;
650
+	    if(strstr(post, "SUBMIT_PERMANENT_FAILURE")) {
651
+		if(!submitted) {
652
+		    logg("!SubmitDetectionStats: Permanent failure\n");
653
+		    permfail = 1;
654
+		}
655
+	    } else if(strstr(post, "SUBMIT_TEMPORARY_FAILURE")) {
656
+		if(!submitted)
657
+		    logg("!SubmitDetectionStats: Temporary failure\n");
658
+	    } else {
659
+		if(!submitted)
660
+		    logg("!SubmitDetectionStats: Incorrect answer from server\n");
661
+	    }
662
+
663
+	    break;
664
+	}
665
+
666
+    } while((line = readbline(fd, fbuff, FILEBUFF, sb.st_size, &lread)));
667
+
668
+    close(fd);
669
+
670
+    if(submitted || permfail) {
671
+	if((fd = open("stats.dat", O_WRONLY)) == -1) {
672
+	    logg("^SubmitDetectionStats: Can't open stats.dat for writing\n");
673
+	} else {
674
+	    if((bread = write(fd, newstatsdat, sizeof(newstatsdat))) != sizeof(newstatsdat))
675
+		logg("^SubmitDetectionStats: Can't write to stats.dat\n");
676
+	    close(fd);
677
+	}
678
+    }
679
+
680
+    if(ret == 0) {
681
+	if(!submitted)
682
+	    logg("SubmitDetectionStats: Not enough recent data for submission\n");
683
+	else
684
+	    logg("SubmitDetectionStats: Submitted %u records\n", submitted);
685
+    }
686
+
687
+    return ret;
688
+}
689
+
400 690
 static int Rfc2822DateTime(char *buf, time_t mtime)
401 691
 {
402 692
 	struct tm *gmt;
... ...
@@ -20,9 +20,14 @@
20 20
 #ifndef __MANAGER_H
21 21
 #define __MANAGER_H
22 22
 
23
+#define SUBMIT_MIN_ENTRIES    10
24
+#define SUBMIT_MAX_ENTRIES    50
25
+
23 26
 #include "shared/cfgparser.h"
24 27
 #include "shared/options.h"
25 28
 
26 29
 int downloadmanager(const struct cfgstruct *copt, const struct optstruct *opt, const char *hostname, const char *dbdir, int logerr);
27 30
 
31
+int submitstats(const char *clamdcfg, const struct cfgstruct *copt);
32
+
28 33
 #endif
... ...
@@ -125,6 +125,7 @@ struct cfgoption cfg_options[] = {
125 125
     {"LocalIPAddress", OPT_QUOTESTR, -1, NULL, 0, OPT_FRESHCLAM},
126 126
     {"ConnectTimeout", OPT_NUM, 30, NULL, 0, OPT_FRESHCLAM},
127 127
     {"ReceiveTimeout", OPT_NUM, 30, NULL, 0, OPT_FRESHCLAM},
128
+    {"SubmitDetectionStats", OPT_QUOTESTR, -1, NULL, 0, OPT_FRESHCLAM},
128 129
 
129 130
     {"DevACOnly", OPT_BOOL, -1, NULL, 0, OPT_CLAMD},
130 131
     {"DevACDepth", OPT_NUM, -1, NULL, 0, OPT_CLAMD},