Browse code

libclamav: cl_scan*_ex() functions provide verdict separate from errors

It is a shortcoming of existing scan APIs that it is not possible
to return an error without masking a verdict.
We presently work around this limitation by counting up detections at
the end and then overriding the error code with `CL_VIRUS`, if necessary.

The `cl_scanfile_ex()`, `cl_scandesc_ex()`, and `cl_scanmap_ex()` functions
should provide the scan verdict separately from the error code.

This introduces a new enum for recording and reporting a verdict:
`cl_verdict_t` with options:

- `CL_VERDICT_NOTHING_FOUND`
- `CL_VERDICT_TRUSTED`
- `CL_VERDICT_STRONG_INDICATOR`
- `CL_VERDICT_POTENTIALLY_UNWANTED`

Notably, the newer scan APIs may set the verdict to `CL_VERDICT_TRUSTED`
if there is a (hash-based) FP signature for a file, or in the cause where
Authenticode or similar certificate-based verification was performed, or
in the case where an application scan callback returned `CL_VERIFIED`.

CLAM-763
CLAM-865

Valerie Snyder authored on 2025/07/28 11:47:29
Showing 14 changed files
... ...
@@ -181,7 +181,7 @@ static int print_chain(struct metachain *c, char *str, size_t len)
181 181
     return i == c->nchains - 1 ? 0 : 1;
182 182
 }
183 183
 
184
-static cl_error_t post(int fd, int result, const char *virname, void *context)
184
+static cl_error_t post(int fd, int result, const char *alert_name, void *context)
185 185
 {
186 186
     struct clamscan_cb_data *d = context;
187 187
     struct metachain *c        = NULL;
... ...
@@ -196,10 +196,10 @@ static cl_error_t post(int fd, int result, const char *virname, void *context)
196 196
     if (c && c->nchains) {
197 197
         print_chain(c, str, sizeof(str));
198 198
 
199
-        if (c->level == c->lastadd && !virname)
199
+        if (c->level == c->lastadd && !alert_name)
200 200
             free(c->chains[--c->nchains]);
201 201
 
202
-        if (virname && !c->lastvir)
202
+        if (alert_name && !c->lastvir)
203 203
             c->lastvir = c->level;
204 204
     }
205 205
 
... ...
@@ -275,7 +275,7 @@ static cl_error_t meta(const char *container_type, unsigned long fsize_container
275 275
     return CL_CLEAN;
276 276
 }
277 277
 
278
-static void clamscan_virus_found_cb(int fd, const char *virname, void *context)
278
+static void clamscan_virus_found_cb(int fd, const char *alert_name, void *context)
279 279
 {
280 280
     struct clamscan_cb_data *data = (struct clamscan_cb_data *)context;
281 281
     const char *filename;
... ...
@@ -288,7 +288,7 @@ static void clamscan_virus_found_cb(int fd, const char *virname, void *context)
288 288
         filename = data->filename;
289 289
     else
290 290
         filename = "(filename not set)";
291
-    logg(LOGG_INFO, "%s: %s FOUND\n", filename, virname);
291
+    logg(LOGG_INFO, "%s: %s FOUND\n", filename, alert_name);
292 292
     return;
293 293
 }
294 294
 
... ...
@@ -299,7 +299,8 @@ static void scanfile(const char *filename, struct cl_engine *engine, const struc
299 299
     int included   = 0;
300 300
     unsigned i;
301 301
     const struct optstruct *opt;
302
-    const char *virname = NULL;
302
+    cl_verdict_t verdict   = CL_VERDICT_NOTHING_FOUND;
303
+    const char *alert_name = NULL;
303 304
     STATBUF sb;
304 305
     struct metachain chain       = {0};
305 306
     struct clamscan_cb_data data = {0};
... ...
@@ -433,43 +434,63 @@ static void scanfile(const char *filename, struct cl_engine *engine, const struc
433 433
 
434 434
     data.chain    = &chain;
435 435
     data.filename = filename;
436
-    if (CL_VIRUS == (ret = cl_scandesc_ex(
437
-                         fd,
438
-                         filename,
439
-                         &virname,
440
-                         &info.bytes_scanned,
441
-                         engine, options,
442
-                         &data,
443
-                         hash_hint,
444
-                         hash_out,
445
-                         hash_alg,
446
-                         file_type_hint,
447
-                         file_type_out))) {
448
-        if (optget(opts, "archive-verbose")->enabled) {
449
-            if (chain.nchains > 1) {
450
-                char str[128];
451
-                int toolong = print_chain(&chain, str, sizeof(str));
452
-
453
-                logg(LOGG_INFO, "%s%s!(%llu)%s: %s FOUND\n", str, toolong ? "..." : "", (long long unsigned)(chain.lastvir - 1), chain.chains[chain.nchains - 1], virname);
454
-            } else if (chain.lastvir) {
455
-                logg(LOGG_INFO, "%s!(%llu): %s FOUND\n", filename, (long long unsigned)(chain.lastvir - 1), virname);
456
-            }
457
-        }
458
-        info.files++;
459
-        info.ifiles++;
460 436
 
461
-        if (bell)
462
-            fprintf(stderr, "\007");
463
-    } else if (ret == CL_CLEAN) {
464
-        if (!printinfected && printclean)
465
-            mprintf(LOGG_INFO, "%s: OK\n", filename);
437
+    ret = cl_scandesc_ex(
438
+        fd,
439
+        filename,
440
+        &verdict,
441
+        &alert_name,
442
+        &info.bytes_scanned,
443
+        engine, options,
444
+        &data,
445
+        hash_hint,
446
+        hash_out,
447
+        hash_alg,
448
+        file_type_hint,
449
+        file_type_out);
450
+
451
+    switch (verdict) {
452
+        case CL_VERDICT_NOTHING_FOUND: {
453
+            if (CL_SUCCESS == ret) {
454
+                if (!printinfected && printclean) {
455
+                    mprintf(LOGG_INFO, "%s: OK\n", filename);
456
+                }
457
+                info.files++;
458
+            } else {
459
+                if (!printinfected)
460
+                    logg(LOGG_INFO, "%s: %s ERROR\n", filename, cl_strerror(ret));
466 461
 
467
-        info.files++;
468
-    } else {
469
-        if (!printinfected)
470
-            logg(LOGG_INFO, "%s: %s ERROR\n", filename, cl_strerror(ret));
462
+                info.errors++;
463
+            }
464
+        } break;
471 465
 
472
-        info.errors++;
466
+        case CL_VERDICT_TRUSTED: {
467
+            // TODO: Option to print "TRUSTED" verdict instead of "OK"?
468
+            if (!printinfected && printclean) {
469
+                mprintf(LOGG_INFO, "%s: OK\n", filename);
470
+            }
471
+            info.files++;
472
+        } break;
473
+
474
+        case CL_VERDICT_STRONG_INDICATOR:
475
+        case CL_VERDICT_POTENTIALLY_UNWANTED: {
476
+            if (optget(opts, "archive-verbose")->enabled) {
477
+                if (chain.nchains > 1) {
478
+                    char str[128];
479
+                    int toolong = print_chain(&chain, str, sizeof(str));
480
+
481
+                    logg(LOGG_INFO, "%s%s!(%llu)%s: %s FOUND\n", str, toolong ? "..." : "", (long long unsigned)(chain.lastvir - 1), chain.chains[chain.nchains - 1], alert_name);
482
+                } else if (chain.lastvir) {
483
+                    logg(LOGG_INFO, "%s!(%llu): %s FOUND\n", filename, (long long unsigned)(chain.lastvir - 1), alert_name);
484
+                }
485
+            }
486
+            info.files++;
487
+            info.ifiles++;
488
+
489
+            if (bell) {
490
+                fprintf(stderr, "\007");
491
+            }
492
+        } break;
473 493
     }
474 494
 
475 495
     if (NULL != hash) {
... ...
@@ -488,7 +509,7 @@ done:
488 488
     /*
489 489
      * Run the action callback if the file was infected.
490 490
      */
491
-    if (ret == CL_VIRUS && action) {
491
+    if (((verdict == CL_VERDICT_STRONG_INDICATOR) || (verdict == CL_VERDICT_POTENTIALLY_UNWANTED)) && action) {
492 492
         action(filename);
493 493
     }
494 494
 
... ...
@@ -630,9 +651,10 @@ static int scanstdin(const struct cl_engine *engine, const struct optstruct *opt
630 630
 {
631 631
     cl_error_t ret;
632 632
 
633
-    size_t fsize        = 0;
634
-    const char *virname = NULL;
635
-    const char *tmpdir  = NULL;
633
+    size_t fsize           = 0;
634
+    cl_verdict_t verdict   = CL_VERDICT_NOTHING_FOUND;
635
+    const char *alert_name = NULL;
636
+    const char *tmpdir     = NULL;
636 637
     char *filename, buff[FILEBUFF];
637 638
     size_t bread;
638 639
     FILE *fs;
... ...
@@ -702,30 +724,51 @@ static int scanstdin(const struct cl_engine *engine, const struct optstruct *opt
702 702
 
703 703
     data.filename = "stdin";
704 704
     data.chain    = NULL;
705
-    if (CL_VIRUS == (ret = cl_scanfile_ex(
706
-                         filename,
707
-                         &virname,
708
-                         &info.bytes_scanned,
709
-                         engine,
710
-                         options,
711
-                         &data,
712
-                         hash_hint,
713
-                         hash_out,
714
-                         hash_alg,
715
-                         file_type_hint,
716
-                         file_type_out))) {
717
-        info.ifiles++;
718
-
719
-        if (bell)
720
-            fprintf(stderr, "\007");
721
-    } else if (ret == CL_CLEAN) {
722
-        if (!printinfected)
723
-            mprintf(LOGG_INFO, "stdin: OK\n");
724
-    } else {
725
-        if (!printinfected)
726
-            logg(LOGG_INFO, "stdin: %s ERROR\n", cl_strerror(ret));
727 705
 
728
-        info.errors++;
706
+    ret = cl_scanfile_ex(
707
+        filename,
708
+        &verdict,
709
+        &alert_name,
710
+        &info.bytes_scanned,
711
+        engine,
712
+        options,
713
+        &data,
714
+        hash_hint,
715
+        hash_out,
716
+        hash_alg,
717
+        file_type_hint,
718
+        file_type_out);
719
+
720
+    switch (verdict) {
721
+        case CL_VERDICT_NOTHING_FOUND: {
722
+            if (CL_SUCCESS == ret) {
723
+                if (!printinfected) {
724
+                    mprintf(LOGG_INFO, "stdin: OK\n");
725
+                }
726
+                info.files++;
727
+            } else {
728
+                if (!printinfected) {
729
+                    logg(LOGG_INFO, "stdin: %s ERROR\n", cl_strerror(ret));
730
+                }
731
+                info.errors++;
732
+            }
733
+        } break;
734
+
735
+        case CL_VERDICT_TRUSTED: {
736
+            // TODO: Option to print "TRUSTED" verdict instead of "OK"?
737
+            if (!printinfected) {
738
+                mprintf(LOGG_INFO, "stdin: OK\n");
739
+            }
740
+        } break;
741
+
742
+        case CL_VERDICT_STRONG_INDICATOR:
743
+        case CL_VERDICT_POTENTIALLY_UNWANTED: {
744
+            info.ifiles++;
745
+
746
+            if (bell) {
747
+                fprintf(stderr, "\007");
748
+            }
749
+        } break;
729 750
     }
730 751
 
731 752
     if (NULL != hash) {
... ...
@@ -533,8 +533,10 @@ int scanfile(const char *filename, scanmem_data *scan_data, struct mem_info *inf
533 533
 {
534 534
     int fd;
535 535
     int scantype;
536
-    int ret             = CL_CLEAN;
537
-    const char *virname = NULL;
536
+    int ret = CL_CLEAN;
537
+
538
+    cl_verdict_t verdict   = CL_VERDICT_NOTHING_FOUND;
539
+    const char *alert_name = NULL;
538 540
 
539 541
     logg(LOGG_DEBUG, "Scanning %s\n", filename);
540 542
 
... ...
@@ -565,7 +567,8 @@ int scanfile(const char *filename, scanmem_data *scan_data, struct mem_info *inf
565 565
         ret = cl_scandesc_ex(
566 566
             fd,
567 567
             filename,
568
-            &virname,
568
+            &verdict,
569
+            &alert_name,
569 570
             &info->bytes_scanned,
570 571
             info->engine,
571 572
             info->options,
... ...
@@ -576,11 +579,23 @@ int scanfile(const char *filename, scanmem_data *scan_data, struct mem_info *inf
576 576
             NULL,
577 577
             NULL,
578 578
             NULL);
579
-        if (ret == CL_VIRUS) {
580
-            logg(LOGG_INFO, "%s: %s FOUND\n", filename, virname);
581
-            info->ifiles++;
582
-        } else if (scan_data->printclean) {
583
-            logg(LOGG_INFO, "%s: OK    \n", filename);
579
+
580
+        switch (verdict) {
581
+            case CL_VERDICT_NOTHING_FOUND: {
582
+                logg(LOGG_INFO, "%s: OK    \n", filename);
583
+                ret = CL_CLEAN;
584
+            } break;
585
+            case CL_VERDICT_TRUSTED: {
586
+                // TODO: Option to print "TRUSTED" verdict instead of "OK"?
587
+                logg(LOGG_INFO, "%s: OK    \n", filename);
588
+                ret = CL_CLEAN;
589
+            } break;
590
+            case CL_VERDICT_STRONG_INDICATOR:
591
+            case CL_VERDICT_POTENTIALLY_UNWANTED: {
592
+                logg(LOGG_INFO, "%s: %s FOUND\n", filename, alert_name);
593
+                info->ifiles++;
594
+                ret = CL_VIRUS;
595
+            } break;
584 596
         }
585 597
     }
586 598
 
... ...
@@ -1008,41 +1008,76 @@ done:
1008 1008
 const char *cl_error_t_to_string(cl_error_t clerror)
1009 1009
 {
1010 1010
     switch (clerror) {
1011
-        case CL_SUCCESS: return "CL_SUCCESS";
1012
-        case CL_VIRUS: return "CL_VIRUS";
1013
-        case CL_ENULLARG: return "CL_ENULLARG";
1014
-        case CL_EARG: return "CL_EARG";
1015
-        case CL_EMALFDB: return "CL_EMALFDB";
1016
-        case CL_ECVD: return "CL_ECVD";
1017
-        case CL_EVERIFY: return "CL_EVERIFY";
1018
-        case CL_EUNPACK: return "CL_EUNPACK";
1019
-        case CL_EPARSE: return "CL_EPARSE";
1020
-        case CL_EOPEN: return "CL_EOPEN";
1021
-        case CL_ECREAT: return "CL_ECREAT";
1022
-        case CL_EUNLINK: return "CL_EUNLINK";
1023
-        case CL_ESTAT: return "CL_ESTAT";
1024
-        case CL_EREAD: return "CL_EREAD";
1025
-        case CL_ESEEK: return "CL_ESEEK";
1026
-        case CL_EWRITE: return "CL_EWRITE";
1027
-        case CL_EDUP: return "CL_EDUP";
1028
-        case CL_EACCES: return "CL_EACCES";
1029
-        case CL_ETMPFILE: return "CL_ETMPFILE";
1030
-        case CL_ETMPDIR: return "CL_ETMPDIR";
1031
-        case CL_EMAP: return "CL_EMAP";
1032
-        case CL_EMEM: return "CL_EMEM";
1033
-        case CL_ETIMEOUT: return "CL_ETIMEOUT";
1034
-        case CL_EMAXREC: return "CL_EMAXREC";
1035
-        case CL_EMAXSIZE: return "CL_EMAXSIZE";
1036
-        case CL_EMAXFILES: return "CL_EMAXFILES";
1037
-        case CL_EFORMAT: return "CL_EFORMAT";
1038
-        case CL_EBYTECODE: return "CL_EBYTECODE";
1039
-        case CL_EBYTECODE_TESTFAIL: return "CL_EBYTECODE_TESTFAIL";
1040
-        case CL_ELOCK: return "CL_ELOCK";
1041
-        case CL_EBUSY: return "CL_EBUSY";
1042
-        case CL_ESTATE: return "CL_ESTATE";
1043
-        case CL_ERROR: return "CL_ERROR";
1044
-        case CL_VERIFIED: return "CL_VERIFIED";
1045
-        case CL_BREAK: return "CL_BREAK";
1011
+        case CL_SUCCESS:
1012
+            return "CL_SUCCESS";
1013
+        case CL_VIRUS:
1014
+            return "CL_VIRUS";
1015
+        case CL_ENULLARG:
1016
+            return "CL_ENULLARG";
1017
+        case CL_EARG:
1018
+            return "CL_EARG";
1019
+        case CL_EMALFDB:
1020
+            return "CL_EMALFDB";
1021
+        case CL_ECVD:
1022
+            return "CL_ECVD";
1023
+        case CL_EVERIFY:
1024
+            return "CL_EVERIFY";
1025
+        case CL_EUNPACK:
1026
+            return "CL_EUNPACK";
1027
+        case CL_EPARSE:
1028
+            return "CL_EPARSE";
1029
+        case CL_EOPEN:
1030
+            return "CL_EOPEN";
1031
+        case CL_ECREAT:
1032
+            return "CL_ECREAT";
1033
+        case CL_EUNLINK:
1034
+            return "CL_EUNLINK";
1035
+        case CL_ESTAT:
1036
+            return "CL_ESTAT";
1037
+        case CL_EREAD:
1038
+            return "CL_EREAD";
1039
+        case CL_ESEEK:
1040
+            return "CL_ESEEK";
1041
+        case CL_EWRITE:
1042
+            return "CL_EWRITE";
1043
+        case CL_EDUP:
1044
+            return "CL_EDUP";
1045
+        case CL_EACCES:
1046
+            return "CL_EACCES";
1047
+        case CL_ETMPFILE:
1048
+            return "CL_ETMPFILE";
1049
+        case CL_ETMPDIR:
1050
+            return "CL_ETMPDIR";
1051
+        case CL_EMAP:
1052
+            return "CL_EMAP";
1053
+        case CL_EMEM:
1054
+            return "CL_EMEM";
1055
+        case CL_ETIMEOUT:
1056
+            return "CL_ETIMEOUT";
1057
+        case CL_EMAXREC:
1058
+            return "CL_EMAXREC";
1059
+        case CL_EMAXSIZE:
1060
+            return "CL_EMAXSIZE";
1061
+        case CL_EMAXFILES:
1062
+            return "CL_EMAXFILES";
1063
+        case CL_EFORMAT:
1064
+            return "CL_EFORMAT";
1065
+        case CL_EBYTECODE:
1066
+            return "CL_EBYTECODE";
1067
+        case CL_EBYTECODE_TESTFAIL:
1068
+            return "CL_EBYTECODE_TESTFAIL";
1069
+        case CL_ELOCK:
1070
+            return "CL_ELOCK";
1071
+        case CL_EBUSY:
1072
+            return "CL_EBUSY";
1073
+        case CL_ESTATE:
1074
+            return "CL_ESTATE";
1075
+        case CL_ERROR:
1076
+            return "CL_ERROR";
1077
+        case CL_VERIFIED:
1078
+            return "CL_VERIFIED";
1079
+        case CL_BREAK:
1080
+            return "CL_BREAK";
1046 1081
         default:
1047 1082
             return "Unknown error code";
1048 1083
     }
... ...
@@ -1195,7 +1230,10 @@ int main(int argc, char **argv)
1195 1195
     script_context_t *script_context = NULL;
1196 1196
 
1197 1197
     unsigned long int size = 0;
1198
-    const char *virname;
1198
+
1199
+    cl_verdict_t verdict = CL_VERDICT_NOTHING_FOUND;
1200
+    const char *alert_name;
1201
+
1199 1202
     struct cl_engine *engine = NULL;
1200 1203
     struct cl_scan_options options;
1201 1204
     unsigned int signo = 0;
... ...
@@ -1336,27 +1374,21 @@ int main(int argc, char **argv)
1336 1336
      * Run the scan.
1337 1337
      * Note that the callbacks will be called during this function.
1338 1338
      */
1339
-    if (CL_VIRUS == (ret = cl_scandesc_ex(
1340
-                         target_fd,
1341
-                         filename,
1342
-                         &virname,
1343
-                         &size,
1344
-                         engine,
1345
-                         &options,
1346
-                         script_context, // context,
1347
-                         hash_hint,      // hash_hint,
1348
-                         &hash_out,      // hash_out,
1349
-                         hash_alg,       // hash_alg,
1350
-                         file_type_hint, // file_type_hint,
1351
-                         &file_type_out  // file_type_out
1352
-                         ))) {
1353
-        printf("Virus detected: %s\n", virname);
1354
-    } else {
1355
-        if (ret != CL_SUCCESS) {
1356
-            printf("Error: %s\n", cl_strerror(ret));
1357
-            goto done;
1358
-        }
1359
-    }
1339
+    ret = cl_scandesc_ex(
1340
+        target_fd,
1341
+        filename,
1342
+        &verdict,
1343
+        &alert_name,
1344
+        &size,
1345
+        engine,
1346
+        &options,
1347
+        script_context, // context,
1348
+        hash_hint,      // hash_hint,
1349
+        &hash_out,      // hash_out,
1350
+        hash_alg,       // hash_alg,
1351
+        file_type_hint, // file_type_hint,
1352
+        &file_type_out  // file_type_out
1353
+    );
1360 1354
 
1361 1355
     /* Calculate size of scanned data */
1362 1356
     printf("\n");
... ...
@@ -1373,7 +1405,24 @@ int main(int argc, char **argv)
1373 1373
     } else {
1374 1374
         printf("No file type provided for this file.\n");
1375 1375
     }
1376
-    printf("Return code:  %s (%d)\n", cl_strerror(ret), ret);
1376
+    switch (verdict) {
1377
+        case CL_VERDICT_NOTHING_FOUND: {
1378
+            printf("Verdict:      Nothing found.\n");
1379
+        } break;
1380
+
1381
+        case CL_VERDICT_TRUSTED: {
1382
+            printf("Verdict:      Trusted.\n");
1383
+        } break;
1384
+
1385
+        case CL_VERDICT_STRONG_INDICATOR: {
1386
+            printf("Verdict:      Found Strong Indicator: %s\n", alert_name);
1387
+        } break;
1388
+
1389
+        case CL_VERDICT_POTENTIALLY_UNWANTED: {
1390
+            printf("Verdict:      Found Potentially Unwanted Indicator: %s\n", alert_name);
1391
+        } break;
1392
+    }
1393
+    printf("Return Code:  %s (%d)\n", cl_error_t_to_string(ret), ret);
1377 1394
 
1378 1395
     status = ret == CL_VIRUS ? 1 : 0;
1379 1396
 
... ...
@@ -2440,5 +2440,9 @@ cl_error_t asn1_check_mscat(struct cl_engine *engine, fmap_t *map, size_t offset
2440 2440
     }
2441 2441
 
2442 2442
     cli_dbgmsg("asn1_check_mscat: file with valid authenticode signature, trusted\n");
2443
+
2444
+    // Remove any evidence for this layer and set the verdict to trusted.
2445
+    (void)cli_trust_this_layer(ctx);
2446
+
2443 2447
     return CL_VERIFIED;
2444 2448
 }
... ...
@@ -81,7 +81,19 @@ extern "C" {
81 81
 
82 82
 #define CL_COUNT_PRECISION 4096
83 83
 
84
-/* return codes */
84
+/**
85
+ * @brief Scan verdicts for cl_scanmap_ex(), cl_scanfile_ex(), and cl_scandesc_ex().
86
+ */
87
+typedef enum cl_verdict_t {
88
+    CL_VERDICT_NOTHING_FOUND = 0,    /**< No alerting signatures matched. */
89
+    CL_VERDICT_TRUSTED,              /**< The scan target has been deemed trusted (e.g. by FP signature or Authenticode). */
90
+    CL_VERDICT_STRONG_INDICATOR,                /**< One or more strong indicator signatures matched. */
91
+    CL_VERDICT_POTENTIALLY_UNWANTED, /**< One or more potentially unwanted signatures matched. */
92
+} cl_verdict_t;
93
+
94
+/**
95
+ * @brief Return codes used by libclamav functions.
96
+ */
85 97
 typedef enum cl_error_t {
86 98
     /* libclamav specific */
87 99
     CL_CLEAN   = 0,
... ...
@@ -123,7 +135,7 @@ typedef enum cl_error_t {
123 123
     CL_EBUSY,
124 124
     CL_ESTATE,
125 125
 
126
-    CL_VERIFIED, /** The binary has been deemed trusted */
126
+    CL_VERIFIED, /** The scan target has been deemed trusted */
127 127
     CL_ERROR,    /** Unspecified / generic error */
128 128
 
129 129
     /* no error codes below this line please */
... ...
@@ -1630,9 +1642,12 @@ extern cl_error_t cl_scandesc_callback(
1630 1630
  * This variant also upgrades the `scanned` output parameter to a 64-bit integer.
1631 1631
  *
1632 1632
  * @param desc               File descriptor of an open file. The caller must provide this or the map.
1633
- * @param filename           (optional) Filepath of the open file descriptor or file map.
1634
- * @param[out] virname       Will be set to a statically allocated (i.e. needs not be freed) signature name if the scan matches against a signature.
1635
- * @param[out] scanned       The (exact) number of bytes scanned.
1633
+ * @param filename           (Optional) Filepath of the open file descriptor or file map.
1634
+ * @param[out] verdict_out   A pointer to a cl_verdict_t that will be set to the scan verdict.
1635
+ *                           You should check the verdict even if the function returns an error.
1636
+ * @param[out] last_alert_out Will be set to a statically allocated (i.e. needs not be freed) signature name if the scan
1637
+ *                           matches against a signature.
1638
+ * @param[out] scanned_out   The (exact) number of bytes scanned.
1636 1639
  * @param engine             The scanning engine.
1637 1640
  * @param scanoptions        Scanning options.
1638 1641
  * @param[in,out] context    (Optional) An application-defined context struct, opaque to libclamav.
... ...
@@ -1653,15 +1668,16 @@ extern cl_error_t cl_scandesc_callback(
1653 1653
  *                           of the top layer as determined by ClamAV.
1654 1654
  *                           Will take the form of the standard ClamAV file type format. E.g. "CL_TYPE_PE".
1655 1655
  *                           See also: https://docs.clamav.net/appendix/FileTypes.html#file-types
1656
- * @return cl_error_t        CL_CLEAN if no signature matched.
1657
- *                           CL_VIRUS if a signature matched.
1658
- *                           Another CL_E* error code if an error occurred.
1656
+ * @return cl_error_t        CL_SUCCESS if no error occured.
1657
+ *                           Otherwise a CL_E* error code.
1658
+ *                           Does NOT return CL_VIRUS for a signature match. Check the `verdict_out` parameter instead.
1659 1659
  */
1660 1660
 extern cl_error_t cl_scandesc_ex(
1661 1661
     int desc,
1662 1662
     const char *filename,
1663
-    const char **virname,
1664
-    uint64_t *scanned,
1663
+    cl_verdict_t *verdict_out,
1664
+    const char **last_alert_out,
1665
+    uint64_t *scanned_out,
1665 1666
     const struct cl_engine *engine,
1666 1667
     struct cl_scan_options *scanoptions,
1667 1668
     void *context,
... ...
@@ -1727,8 +1743,11 @@ extern cl_error_t cl_scanfile_callback(
1727 1727
  * This variant also upgrades the `scanned` output parameter to a 64-bit integer.
1728 1728
  *
1729 1729
  * @param filename           Filepath of the file to be scanned.
1730
- * @param[out] virname       Will be set to a statically allocated (i.e. needs not be freed) signature name if the scan matches against a signature.
1731
- * @param[out] scanned       The exact number of bytes scanned.
1730
+ * @param[out] verdict_out   A pointer to a cl_verdict_t that will be set to the scan verdict.
1731
+ *                           You should check the verdict even if the function returns an error.
1732
+ * @param[out] last_alert_out Will be set to a statically allocated (i.e. needs not be freed) signature name if the scan
1733
+ *                           matches against a signature.
1734
+ * @param[out] scanned_out   The (exact) number of bytes scanned.
1732 1735
  * @param engine             The scanning engine.
1733 1736
  * @param scanoptions        Scanning options.
1734 1737
  * @param[in,out] context    (Optional) An application-defined context struct, opaque to libclamav.
... ...
@@ -1749,14 +1768,15 @@ extern cl_error_t cl_scanfile_callback(
1749 1749
  *                           of the top layer as determined by ClamAV.
1750 1750
  *                           Will take the form of the standard ClamAV file type format. E.g. "CL_TYPE_PE".
1751 1751
  *                           See also: https://docs.clamav.net/appendix/FileTypes.html#file-types
1752
- * @return cl_error_t        CL_CLEAN if no signature matched.
1753
- *                           CL_VIRUS if a signature matched.
1754
- *                           Another CL_E* error code if an error occurred.
1752
+ * @return cl_error_t        CL_SUCCESS if no error occured.
1753
+ *                           Otherwise a CL_E* error code.
1754
+ *                           Does NOT return CL_VIRUS for a signature match. Check the `verdict_out` parameter instead.
1755 1755
  */
1756 1756
 extern cl_error_t cl_scanfile_ex(
1757 1757
     const char *filename,
1758
-    const char **virname,
1759
-    uint64_t *scanned,
1758
+    cl_verdict_t *verdict_out,
1759
+    const char **last_alert_out,
1760
+    uint64_t *scanned_out,
1760 1761
     const struct cl_engine *engine,
1761 1762
     struct cl_scan_options *scanoptions,
1762 1763
     void *context,
... ...
@@ -1819,13 +1839,15 @@ extern cl_error_t cl_scanmap_callback(
1819 1819
  * This variant also upgrades the `scanned` output parameter to a 64-bit integer.
1820 1820
  *
1821 1821
  * @param map                Buffer to be scanned, in form of a cl_fmap_t.
1822
- * @param filename           Name of data origin. Does not need to be an actual
1823
- *                           file on disk. May be NULL if a name is not available.
1824
- * @param[out] virname       Pointer to receive the signature match name name if a
1825
- *                           signature matched.
1826
- * @param[out] scanned       The exact number of bytes scanned.
1822
+ * @param filename           (Optional) Name of data origin. Does not need to be an actual file on disk.
1823
+ *                           May be NULL if a name is not available.
1824
+ * @param[out] verdict_out   A pointer to a cl_verdict_t that will be set to the scan verdict.
1825
+ *                           You should check the verdict even if the function returns an error.
1826
+ * @param[out] last_alert_out Will be set to a statically allocated (i.e. needs not be freed) signature name if the scan
1827
+ *                           matches against a signature.
1828
+ * @param[out] scanned_out   The (exact) number of bytes scanned.
1827 1829
  * @param engine             The scanning engine.
1828
- * @param scanoptions        The scanning options struct.
1830
+ * @param scanoptions        Scanning options.
1829 1831
  * @param[in,out] context    (Optional) An application-defined context struct, opaque to libclamav.
1830 1832
  *                           May be used within your callback functions.
1831 1833
  * @param hash_hint          (Optional) A NULL terminated string of the file hash so that
... ...
@@ -1844,15 +1866,16 @@ extern cl_error_t cl_scanmap_callback(
1844 1844
  *                           of the top layer as determined by ClamAV.
1845 1845
  *                           Will take the form of the standard ClamAV file type format. E.g. "CL_TYPE_PE".
1846 1846
  *                           See also: https://docs.clamav.net/appendix/FileTypes.html#file-types
1847
- * @return cl_error_t        CL_CLEAN if no signature matched.
1848
- *                           CL_VIRUS if a signature matched.
1849
- *                           Another CL_E* error code if an error occurred.
1847
+ * @return cl_error_t        CL_SUCCESS if no error occured.
1848
+ *                           Otherwise a CL_E* error code.
1849
+ *                           Does NOT return CL_VIRUS for a signature match. Check the `verdict_out` parameter instead.
1850 1850
  */
1851 1851
 extern cl_error_t cl_scanmap_ex(
1852 1852
     cl_fmap_t *map,
1853 1853
     const char *filename,
1854
-    const char **virname,
1855
-    uint64_t *scanned,
1854
+    cl_verdict_t *verdict_out,
1855
+    const char **last_alert_out,
1856
+    uint64_t *scanned_out,
1856 1857
     const struct cl_engine *engine,
1857 1858
     struct cl_scan_options *scanoptions,
1858 1859
     void *context,
... ...
@@ -668,12 +668,20 @@ cl_error_t cli_check_fp(cli_ctx *ctx, const char *vname)
668 668
 
669 669
                 if (cli_hm_scan(hash, map->len, &virname, ctx->engine->hm_fp, hash_type) == CL_VIRUS) {
670 670
                     cli_dbgmsg("cli_check_fp: Found false positive detection for %s (fp sig: %s)\n", cli_hash_name(hash_type), virname);
671
-                    status = CL_CLEAN;
671
+
672
+                    // Remove any evidence and set the verdict to trusted for the layer where the FP hash matched, and for all contained layers.
673
+                    (void)cli_trust_layers(ctx, (uint32_t)stack_index, ctx->recursion_level);
674
+
675
+                    status = CL_VERIFIED;
672 676
                     goto done;
673 677
                 }
674 678
                 if (cli_hm_scan_wild(hash, &virname, ctx->engine->hm_fp, hash_type) == CL_VIRUS) {
675 679
                     cli_dbgmsg("cli_check_fp: Found false positive detection for %s (fp sig: %s)\n", cli_hash_name(hash_type), virname);
676
-                    status = CL_CLEAN;
680
+
681
+                    // Remove any evidence and set the verdict to trusted for the layer where the FP hash matched, and for all contained layers.
682
+                    (void)cli_trust_layers(ctx, (uint32_t)stack_index, ctx->recursion_level);
683
+
684
+                    status = CL_VERIFIED;
677 685
                     goto done;
678 686
                 }
679 687
 
... ...
@@ -682,7 +690,11 @@ cl_error_t cli_check_fp(cli_ctx *ctx, const char *vname)
682 682
                      * (associated with the .CAB file type) */
683 683
                     if (cli_hm_scan(hash, 1, &virname, ctx->engine->hm_fp, hash_type) == CL_VIRUS) {
684 684
                         cli_dbgmsg("cli_check_fp: Found .CAB false positive detection for %s via catalog file\n", cli_hash_name(hash_type));
685
-                        status = CL_CLEAN;
685
+
686
+                        // Remove any evidence and set the verdict to trusted for the layer where the FP hash matched, and for all contained layers.
687
+                        (void)cli_trust_layers(ctx, (uint32_t)stack_index, ctx->recursion_level);
688
+
689
+                        status = CL_VERIFIED;
686 690
                         goto done;
687 691
                     }
688 692
                 }
... ...
@@ -1521,11 +1521,13 @@ static cl_error_t append_virus(cli_ctx *ctx, const char *virname, IndicatorType
1521 1521
     ctx->this_layer_evidence = ctx->recursion_stack[ctx->recursion_level].evidence;
1522 1522
 
1523 1523
     if ((ctx->fmap != NULL) &&
1524
-        (ctx->recursion_stack != NULL) &&
1525
-        (CL_VIRUS != cli_check_fp(ctx, virname))) {
1526
-        // FP signature found for one of the layers. Ignore indicator.
1527
-        status = CL_SUCCESS;
1528
-        goto done;
1524
+        (ctx->recursion_stack != NULL)) {
1525
+
1526
+        status = cli_check_fp(ctx, virname);
1527
+        if (CL_VERIFIED == status) {
1528
+            // FP signature found for one of the layers. Ignore indicator.
1529
+            goto done;
1530
+        }
1529 1531
     }
1530 1532
 
1531 1533
     add_successful = evidence_add_indicator(
... ...
@@ -2621,10 +2623,8 @@ cl_error_t cli_dispatch_scan_callback(cli_ctx *ctx, cl_scan_callback_t location)
2621 2621
             // So we need to remove any alerts for this layer and return CL_VERIFIED (will stop scanning this layer).
2622 2622
             cli_dbgmsg("dispatch_scan_callback: Layer verified clean by callback\n");
2623 2623
 
2624
-            evidence_free(ctx->recursion_stack[ctx->recursion_level].evidence);
2625
-            ctx->recursion_stack[ctx->recursion_level].evidence = NULL;
2626
-            ctx->this_layer_evidence                            = NULL;
2627
-
2624
+            // Remove any evidence for this layer and set the verdict to trusted.
2625
+            (void)cli_trust_this_layer(ctx);
2628 2626
             status = CL_VERIFIED;
2629 2627
         } break;
2630 2628
 
... ...
@@ -2726,3 +2726,55 @@ uint8_t cli_set_debug_flag(uint8_t debug_flag)
2726 2726
 
2727 2727
     return was;
2728 2728
 }
2729
+
2730
+cl_error_t cli_trust_this_layer(cli_ctx *ctx)
2731
+{
2732
+    cl_error_t status = CL_ERROR;
2733
+
2734
+    if (!ctx) {
2735
+        cli_errmsg("cli_trust_this_layer: invalid context\n");
2736
+        status = CL_ENULLARG;
2737
+        goto done;
2738
+    }
2739
+
2740
+    if (NULL != ctx->recursion_stack[ctx->recursion_level].evidence) {
2741
+        evidence_free(ctx->recursion_stack[ctx->recursion_level].evidence);
2742
+        ctx->recursion_stack[ctx->recursion_level].evidence = NULL;
2743
+        ctx->this_layer_evidence                            = NULL;
2744
+    }
2745
+
2746
+    ctx->recursion_stack[ctx->recursion_level].verdict = CL_VERDICT_TRUSTED;
2747
+
2748
+    status = CL_SUCCESS;
2749
+
2750
+done:
2751
+    return status;
2752
+}
2753
+
2754
+cl_error_t cli_trust_layers(cli_ctx *ctx, uint32_t start_layer, uint32_t end_layer)
2755
+{
2756
+    cl_error_t status = CL_ERROR;
2757
+    size_t i;
2758
+
2759
+    if (!ctx) {
2760
+        cli_errmsg("cli_trust_layers: invalid context\n");
2761
+        status = CL_ENULLARG;
2762
+        goto done;
2763
+    }
2764
+
2765
+    for (i = start_layer; i <= end_layer; i++) {
2766
+
2767
+        if (NULL != ctx->recursion_stack[i].evidence) {
2768
+            evidence_free(ctx->recursion_stack[i].evidence);
2769
+            ctx->recursion_stack[i].evidence = NULL;
2770
+            ctx->this_layer_evidence         = NULL;
2771
+        }
2772
+
2773
+        ctx->recursion_stack[i].verdict = CL_VERDICT_TRUSTED;
2774
+    }
2775
+
2776
+    status = CL_SUCCESS;
2777
+
2778
+done:
2779
+    return status;
2780
+}
... ...
@@ -1323,6 +1323,24 @@ uint8_t cli_get_debug_flag(void);
1323 1323
  */
1324 1324
 uint8_t cli_set_debug_flag(uint8_t debug_flag);
1325 1325
 
1326
+/**
1327
+ * @brief Trust the current layer by removing any evidence and setting the verdict to trusted.
1328
+ *
1329
+ * @param ctx           The scan context.
1330
+ * @return cl_error_t   CL_SUCCESS on success, or an error code.
1331
+ */
1332
+cl_error_t cli_trust_this_layer(cli_ctx *ctx);
1333
+
1334
+/**
1335
+ * @brief Trust a range of layers by removing any evidence and setting the verdict to trusted.
1336
+ *
1337
+ * @param ctx           The scan context.
1338
+ * @param start_layer   The layer to start trusting from (inclusive).
1339
+ * @param end_layer     The layer to stop trusting at (inclusive).
1340
+ * @return cl_error_t   CL_SUCCESS on success, or an error code.
1341
+ */
1342
+cl_error_t cli_trust_layers(cli_ctx *ctx, uint32_t start_layer, uint32_t end_layer);
1343
+
1326 1344
 #ifndef CLI_SAFER_STRDUP_OR_GOTO_DONE
1327 1345
 /**
1328 1346
  * @brief Wrapper around strdup that does a NULL check.
... ...
@@ -5655,6 +5655,10 @@ cl_error_t cli_check_auth_header(cli_ctx *ctx, struct cli_exe_info *peinfo)
5655 5655
 
5656 5656
         if (cli_hm_scan(authsha, 2, NULL, ctx->engine->hm_fp, hashtype) == CL_VIRUS) {
5657 5657
             cli_dbgmsg("cli_check_auth_header: PE file trusted by catalog file (%s)\n", hashctx_name);
5658
+
5659
+            // Remove any evidence for this layer and set the verdict to trusted.
5660
+            (void)cli_trust_this_layer(ctx);
5661
+
5658 5662
             ret = CL_VERIFIED;
5659 5663
             goto finish;
5660 5664
         }
... ...
@@ -44,6 +44,7 @@ typedef struct cli_scan_layer {
44 44
     size_t object_id;                     /* Unique ID for this object. */
45 45
     json_object *metadata_json;           /* JSON object for this recursion level, e.g. for JSON metadata. */
46 46
     evidence_t evidence;                  /* Store signature matches for this layer and its children. */
47
+    cl_verdict_t verdict;                 /* Verdict for this layer, e.g. CL_VERDICT_STRONG_INDICATOR, CL_VERDICT_NOTHING_FOUND, CL_VERDICT_TRUSTED. */
47 48
     char *tmpdir;                         /* The directory to store tmp files created when processing this layer. */
48 49
     struct cli_scan_layer *parent;        /* Pointer to the parent layer, if any. */
49 50
 } cli_scan_layer_t;
... ...
@@ -4380,6 +4380,10 @@ static cl_error_t dispatch_prescan_callback(clcb_pre_scan cb, cli_ctx *ctx, cons
4380 4380
         switch (status) {
4381 4381
             case CL_BREAK:
4382 4382
                 cli_dbgmsg("dispatch_prescan_callback: file allowed by callback\n");
4383
+
4384
+                // Remove any evidence for this layer and set the verdict to trusted.
4385
+                (void)cli_trust_this_layer(ctx);
4386
+
4383 4387
                 status = CL_VERIFIED;
4384 4388
                 break;
4385 4389
             case CL_VIRUS:
... ...
@@ -4548,9 +4552,9 @@ done:
4548 4548
 
4549 4549
 cl_error_t cli_magic_scan(cli_ctx *ctx, cli_file_t type)
4550 4550
 {
4551
-    cl_error_t ret                   = CL_CLEAN;
4552
-    cl_error_t cache_check_result    = CL_VIRUS;
4553
-    cl_error_t verdict_at_this_level = CL_CLEAN;
4551
+    cl_error_t ret                     = CL_CLEAN;
4552
+    cl_error_t cache_check_result      = CL_VIRUS;
4553
+    cl_verdict_t verdict_at_this_level = CL_VERDICT_NOTHING_FOUND;
4554 4554
 
4555 4555
     bool cache_enabled              = true;
4556 4556
     cli_file_t dettype              = 0;
... ...
@@ -5352,15 +5356,6 @@ done:
5352 5352
     (void)result_should_goto_done(ctx, ret, &ret);
5353 5353
 
5354 5354
     /*
5355
-     * Determine if there was an alert for this layer (or its children).
5356
-     */
5357
-    if (0 < evidence_num_alerts(ctx->this_layer_evidence)) {
5358
-        verdict_at_this_level = CL_VIRUS;
5359
-    } else {
5360
-        verdict_at_this_level = ret;
5361
-    }
5362
-
5363
-    /*
5364 5355
      * Run the deprecated post-scan callback (if one exists) and provide the verdict for this layer.
5365 5356
      */
5366 5357
     cli_dbgmsg("cli_magic_scan: returning %d %s\n", ret, __AT__);
... ...
@@ -5369,7 +5364,7 @@ done:
5369 5369
         const char *virusname = NULL;
5370 5370
 
5371 5371
         // Get the last signature that matched (if any).
5372
-        if (verdict_at_this_level == CL_VIRUS) {
5372
+        if (0 < evidence_num_alerts(ctx->this_layer_evidence)) {
5373 5373
             virusname = cli_get_last_virus(ctx);
5374 5374
         }
5375 5375
 
... ...
@@ -5397,6 +5392,25 @@ done:
5397 5397
         }
5398 5398
     }
5399 5399
 
5400
+    if (CL_VERDICT_TRUSTED == ctx->recursion_stack[ctx->recursion_level].verdict) {
5401
+        /* Remove any alerts for this layer. */
5402
+        if (NULL != ctx->recursion_stack[ctx->recursion_level].evidence) {
5403
+            evidence_free(ctx->recursion_stack[ctx->recursion_level].evidence);
5404
+            ctx->recursion_stack[ctx->recursion_level].evidence = NULL;
5405
+            ctx->this_layer_evidence                            = NULL;
5406
+        }
5407
+    } else {
5408
+        /*
5409
+         * Update the verdict for this layer based on the scan results.
5410
+         * If the verdict is CL_VERDICT_TRUSTED, then we don't change it.
5411
+         */
5412
+        if (0 < evidence_num_indicators_type(ctx->this_layer_evidence, IndicatorType_Strong)) {
5413
+            ctx->recursion_stack[ctx->recursion_level].verdict = CL_VERDICT_STRONG_INDICATOR;
5414
+        } else if (0 < evidence_num_indicators_type(ctx->this_layer_evidence, IndicatorType_PotentiallyUnwanted)) {
5415
+            ctx->recursion_stack[ctx->recursion_level].verdict = CL_VERDICT_POTENTIALLY_UNWANTED;
5416
+        }
5417
+    }
5418
+
5400 5419
     /*
5401 5420
      * If the verdict for this layer is "clean", we can cache it.
5402 5421
      *
... ...
@@ -5404,10 +5418,15 @@ done:
5404 5404
      * so this may not actually cache if we exceeded limits earlier.
5405 5405
      * It will also check if caching is disabled.
5406 5406
      */
5407
-    if (verdict_at_this_level == CL_CLEAN) {
5408
-        perf_start(ctx, PERFT_CACHE);
5409
-        clean_cache_add(ctx);
5410
-        perf_stop(ctx, PERFT_CACHE);
5407
+    if ((CL_VERDICT_TRUSTED == ctx->recursion_stack[ctx->recursion_level].verdict) ||
5408
+        (CL_VERDICT_NOTHING_FOUND == ctx->recursion_stack[ctx->recursion_level].verdict)) {
5409
+        // Also verify we have no weak indicators before adding to the clean cache.
5410
+        // Weak indicators may be used in the future to match a strong indicator.
5411
+        if (evidence_num_indicators_type(ctx->this_layer_evidence, IndicatorType_Weak) == 0) {
5412
+            perf_start(ctx, PERFT_CACHE);
5413
+            clean_cache_add(ctx);
5414
+            perf_stop(ctx, PERFT_CACHE);
5415
+        }
5411 5416
     }
5412 5417
 
5413 5418
 early_ret:
... ...
@@ -5646,38 +5665,41 @@ cl_error_t cli_magic_scan_buff(const void *buffer, size_t length, cli_ctx *ctx,
5646 5646
 /**
5647 5647
  * @brief   The main function to initiate a scan of an fmap.
5648 5648
  *
5649
- * @param map             File map.
5650
- * @param filepath        (optional, recommended) filepath of the open file descriptor or file map.
5651
- * @param[out] virname    Will be set to a statically allocated (i.e. needs not be freed) signature name if the scan matches against a signature.
5652
- * @param[out] scanned    (Optional) The number of bytes scanned.
5653
- * @param engine          The scanning engine.
5654
- * @param scanoptions     Scanning options.
5655
- * @param[in,out] context (Optional) An application-defined context struct, opaque to libclamav.
5656
- *                        May be used within your callback functions.
5657
- * @param hash_hint       (Optional) A NULL terminated string of the file hash so that
5658
- *                        libclamav does not need to calculate it.
5659
- * @param[out] hash_out   (Optional) A NULL terminated string of the file hash.
5660
- *                        The caller is responsible for freeing this string.
5661
- * @param hash_alg        The hashing algorithm used for either `hash_hint` or `hash_out`.
5662
- *                        Supported algorithms are "md5", "sha1", "sha2-256".
5663
- *                        Required only if you provide a `hash_hint` or want to receive a `hash_out`.
5664
- * @param file_type_hint  (Optional) A NULL terminated string of the file type hint.
5665
- *                        E.g. "pe", "elf", "zip", etc.
5666
- *                        You may also use ClamAV type names such as "CL_TYPE_PE".
5667
- *                        ClamAV will ignore the hint if it is not familiar with the specified type.
5668
- * @param file_type_out   (Optional) A NULL terminated string of the file type
5669
- *                        of the top layer as determined by ClamAV.
5670
- *                        Will take the form of the standard ClamAV file type format. E.g. "CL_TYPE_PE".
5671
- *                        The caller is responsible for freeing this string.
5672
- * @return cl_error_t     CL_CLEAN if no signature matched.
5673
- *                        CL_VIRUS if a signature matched.
5674
- *                        Another CL_E* error code if an error occurred.
5649
+ * @param map                 File map.
5650
+ * @param filepath            (optional, recommended) filepath of the open file descriptor or file map.
5651
+ * @param[out] verdict_out    A pointer to a cl_verdict_t that will be set to the scan verdict.
5652
+ *                            You should check the verdict even if the function returns an error.
5653
+ * @param[out] last_alert_out Will be set to a statically allocated (i.e. needs not be freed) signature name if the scan matches against a signature.
5654
+ * @param[out] scanned_out    (Optional) The number of bytes scanned.
5655
+ * @param engine              The scanning engine.
5656
+ * @param scanoptions         Scanning options.
5657
+ * @param[in,out] context     (Optional) An application-defined context struct, opaque to libclamav.
5658
+ *                            May be used within your callback functions.
5659
+ * @param hash_hint           (Optional) A NULL terminated string of the file hash so that
5660
+ *                            libclamav does not need to calculate it.
5661
+ * @param[out] hash_out       (Optional) A NULL terminated string of the file hash.
5662
+ *                            The caller is responsible for freeing this string.
5663
+ * @param hash_alg            The hashing algorithm used for either `hash_hint` or `hash_out`.
5664
+ *                            Supported algorithms are "md5", "sha1", "sha2-256".
5665
+ *                            Required only if you provide a `hash_hint` or want to receive a `hash_out`.
5666
+ * @param file_type_hint      (Optional) A NULL terminated string of the file type hint.
5667
+ *                            E.g. "pe", "elf", "zip", etc.
5668
+ *                            You may also use ClamAV type names such as "CL_TYPE_PE".
5669
+ *                            ClamAV will ignore the hint if it is not familiar with the specified type.
5670
+ * @param file_type_out       (Optional) A NULL terminated string of the file type
5671
+ *                            of the top layer as determined by ClamAV.
5672
+ *                            Will take the form of the standard ClamAV file type format. E.g. "CL_TYPE_PE".
5673
+ *                            The caller is responsible for freeing this string.
5674
+ * @return cl_error_t         CL_SUCCESS if no error occured.
5675
+ *                            Otherwise a CL_E* error code.
5676
+ *                            Does NOT return CL_VIRUS for a signature match. Check the `verdict_out` parameter instead.
5675 5677
  */
5676 5678
 static cl_error_t scan_common(
5677 5679
     cl_fmap_t *map,
5678 5680
     const char *filepath,
5679
-    const char **virname,
5680
-    uint64_t *scanned,
5681
+    cl_verdict_t *verdict_out,
5682
+    const char **last_alert_out,
5683
+    uint64_t *scanned_out,
5681 5684
     const struct cl_engine *engine,
5682 5685
     struct cl_scan_options *scanoptions,
5683 5686
     void *context,
... ...
@@ -5689,7 +5711,6 @@ static cl_error_t scan_common(
5689 5689
 {
5690 5690
     cl_error_t status = CL_SUCCESS;
5691 5691
     cl_error_t ret;
5692
-    cl_error_t verdict = CL_CLEAN;
5693 5692
 
5694 5693
     cli_ctx ctx = {0};
5695 5694
 
... ...
@@ -5710,12 +5731,13 @@ static cl_error_t scan_common(
5710 5710
     // The type of the file being scanned.
5711 5711
     cli_file_t file_type = CL_TYPE_ANY;
5712 5712
 
5713
-    if (NULL == map || NULL == scanoptions || NULL == virname) {
5713
+    if (NULL == map || NULL == scanoptions || NULL == verdict_out || NULL == last_alert_out || NULL == engine) {
5714 5714
         return CL_ENULLARG;
5715 5715
     }
5716 5716
 
5717 5717
     /* Initialize output variables */
5718
-    *virname = NULL;
5718
+    *verdict_out    = CL_VERDICT_NOTHING_FOUND;
5719
+    *last_alert_out = NULL;
5719 5720
 
5720 5721
     // If the caller provided a file type hint, we make a best effort to use it.
5721 5722
     if (file_type_hint) {
... ...
@@ -5776,7 +5798,7 @@ static cl_error_t scan_common(
5776 5776
     }
5777 5777
 
5778 5778
     ctx.engine  = engine;
5779
-    ctx.scanned = scanned;
5779
+    ctx.scanned = scanned_out;
5780 5780
     CLI_MALLOC_OR_GOTO_DONE(ctx.options, sizeof(struct cl_scan_options), status = CL_EMEM);
5781 5781
 
5782 5782
     memcpy(ctx.options, scanoptions, sizeof(struct cl_scan_options));
... ...
@@ -6043,8 +6065,8 @@ static cl_error_t scan_common(
6043 6043
 
6044 6044
     // If any alerts occurred, set the output pointer to the "latest" alert signature name.
6045 6045
     if (0 < evidence_num_alerts(ctx.this_layer_evidence)) {
6046
-        *virname = cli_get_last_virus_str(&ctx);
6047
-        verdict  = CL_VIRUS;
6046
+        *last_alert_out = cli_get_last_virus_str(&ctx);
6047
+        *verdict_out    = CL_VERDICT_STRONG_INDICATOR;
6048 6048
     }
6049 6049
 
6050 6050
     /*
... ...
@@ -6152,12 +6174,6 @@ static cl_error_t scan_common(
6152 6152
         }
6153 6153
     }
6154 6154
 
6155
-    if (verdict != CL_CLEAN) {
6156
-        // Reporting "VIRUS" is more important than reporting and error,
6157
-        // because... unfortunately we can only do one with the current API.
6158
-        status = verdict;
6159
-    }
6160
-
6161 6155
 done:
6162 6156
 
6163 6157
     if (logg_initialized) {
... ...
@@ -6222,10 +6238,12 @@ cl_error_t cl_scandesc(
6222 6222
 {
6223 6223
     cl_error_t status;
6224 6224
     uint64_t scanned_out;
6225
+    cl_verdict_t verdict_out = CL_VERDICT_NOTHING_FOUND;
6225 6226
 
6226 6227
     status = cl_scandesc_ex(
6227 6228
         desc,
6228 6229
         filename,
6230
+        &verdict_out,
6229 6231
         virname,
6230 6232
         &scanned_out,
6231 6233
         engine,
... ...
@@ -6247,6 +6265,12 @@ cl_error_t cl_scandesc(
6247 6247
         }
6248 6248
     }
6249 6249
 
6250
+    if (verdict_out == CL_VERDICT_STRONG_INDICATOR || verdict_out == CL_VERDICT_POTENTIALLY_UNWANTED) {
6251
+        // Reporting "CL_VIRUS" is more important than reporting an error,
6252
+        // because... unfortunately we can only do one with this API.
6253
+        status = CL_VIRUS;
6254
+    }
6255
+
6250 6256
     return status;
6251 6257
 }
6252 6258
 
... ...
@@ -6260,13 +6284,15 @@ cl_error_t cl_scandesc_callback(
6260 6260
     void *context)
6261 6261
 {
6262 6262
     cl_error_t status;
6263
-    uint64_t scanned_out;
6263
+    uint64_t scanned_bytes;
6264
+    cl_verdict_t verdict_out = CL_VERDICT_NOTHING_FOUND;
6264 6265
 
6265 6266
     status = cl_scandesc_ex(
6266 6267
         desc,
6267 6268
         filename,
6269
+        &verdict_out,
6268 6270
         virname,
6269
-        &scanned_out,
6271
+        &scanned_bytes,
6270 6272
         engine,
6271 6273
         scanoptions,
6272 6274
         context,
... ...
@@ -6278,22 +6304,29 @@ cl_error_t cl_scandesc_callback(
6278 6278
 
6279 6279
     if (NULL != scanned) {
6280 6280
         if ((SIZEOF_LONG == 4) &&
6281
-            (scanned_out / CL_COUNT_PRECISION > UINT32_MAX)) {
6282
-            cli_warnmsg("cl_scanfile_callback: scanned_out exceeds UINT32_MAX, setting to UINT32_MAX\n");
6281
+            (scanned_bytes / CL_COUNT_PRECISION > UINT32_MAX)) {
6282
+            cli_warnmsg("cl_scanfile_callback: scanned_bytes exceeds UINT32_MAX, setting to UINT32_MAX\n");
6283 6283
             *scanned = UINT32_MAX;
6284 6284
         } else {
6285
-            *scanned = (unsigned long int)(scanned_out / CL_COUNT_PRECISION);
6285
+            *scanned = (unsigned long int)(scanned_bytes / CL_COUNT_PRECISION);
6286 6286
         }
6287 6287
     }
6288 6288
 
6289
+    if (verdict_out == CL_VERDICT_STRONG_INDICATOR || verdict_out == CL_VERDICT_POTENTIALLY_UNWANTED) {
6290
+        // Reporting "CL_VIRUS" is more important than reporting an error,
6291
+        // because... unfortunately we can only do one with this API.
6292
+        status = CL_VIRUS;
6293
+    }
6294
+
6289 6295
     return status;
6290 6296
 }
6291 6297
 
6292 6298
 cl_error_t cl_scandesc_ex(
6293 6299
     int desc,
6294 6300
     const char *filename,
6295
-    const char **virname,
6296
-    uint64_t *scanned,
6301
+    cl_verdict_t *verdict_out,
6302
+    const char **last_alert_out,
6303
+    uint64_t *scanned_out,
6297 6304
     const struct cl_engine *engine,
6298 6305
     struct cl_scan_options *scanoptions,
6299 6306
     void *context,
... ...
@@ -6323,8 +6356,8 @@ cl_error_t cl_scandesc_ex(
6323 6323
         if (scanoptions->heuristic & CL_SCAN_HEURISTIC_EXCEEDS_MAX) {
6324 6324
             if (engine->cb_virus_found) {
6325 6325
                 engine->cb_virus_found(desc, "Heuristics.Limits.Exceeded.MaxFileSize", context);
6326
-                if (virname) {
6327
-                    *virname = "Heuristics.Limits.Exceeded.MaxFileSize";
6326
+                if (last_alert_out) {
6327
+                    *last_alert_out = "Heuristics.Limits.Exceeded.MaxFileSize";
6328 6328
                 }
6329 6329
             }
6330 6330
             status = CL_VIRUS;
... ...
@@ -6347,8 +6380,9 @@ cl_error_t cl_scandesc_ex(
6347 6347
     status = scan_common(
6348 6348
         map,
6349 6349
         filename,
6350
-        virname,
6351
-        scanned,
6350
+        verdict_out,
6351
+        last_alert_out,
6352
+        scanned_out,
6352 6353
         engine,
6353 6354
         scanoptions,
6354 6355
         context,
... ...
@@ -6379,13 +6413,15 @@ cl_error_t cl_scanmap_callback(
6379 6379
     void *context)
6380 6380
 {
6381 6381
     cl_error_t status;
6382
-    uint64_t scanned_out;
6382
+    uint64_t scanned_bytes;
6383
+    cl_verdict_t verdict_out = CL_VERDICT_NOTHING_FOUND;
6383 6384
 
6384 6385
     status = cl_scanmap_ex(
6385 6386
         map,
6386 6387
         filename,
6388
+        &verdict_out,
6387 6389
         virname,
6388
-        &scanned_out,
6390
+        &scanned_bytes,
6389 6391
         engine,
6390 6392
         scanoptions,
6391 6393
         context,
... ...
@@ -6397,22 +6433,29 @@ cl_error_t cl_scanmap_callback(
6397 6397
 
6398 6398
     if (NULL != scanned) {
6399 6399
         if ((SIZEOF_LONG == 4) &&
6400
-            (scanned_out / CL_COUNT_PRECISION > UINT32_MAX)) {
6401
-            cli_warnmsg("cl_scanfile_callback: scanned_out exceeds UINT32_MAX, setting to UINT32_MAX\n");
6400
+            (scanned_bytes / CL_COUNT_PRECISION > UINT32_MAX)) {
6401
+            cli_warnmsg("cl_scanfile_callback: scanned_bytes exceeds UINT32_MAX, setting to UINT32_MAX\n");
6402 6402
             *scanned = UINT32_MAX;
6403 6403
         } else {
6404
-            *scanned = (unsigned long int)(scanned_out / CL_COUNT_PRECISION);
6404
+            *scanned = (unsigned long int)(scanned_bytes / CL_COUNT_PRECISION);
6405 6405
         }
6406 6406
     }
6407 6407
 
6408
+    if (verdict_out == CL_VERDICT_STRONG_INDICATOR || verdict_out == CL_VERDICT_POTENTIALLY_UNWANTED) {
6409
+        // Reporting "CL_VIRUS" is more important than reporting an error,
6410
+        // because... unfortunately we can only do one with this API.
6411
+        status = CL_VIRUS;
6412
+    }
6413
+
6408 6414
     return status;
6409 6415
 }
6410 6416
 
6411 6417
 cl_error_t cl_scanmap_ex(
6412 6418
     cl_fmap_t *map,
6413 6419
     const char *filename,
6414
-    const char **virname,
6415
-    uint64_t *scanned,
6420
+    cl_verdict_t *verdict_out,
6421
+    const char **last_alert_out,
6422
+    uint64_t *scanned_out,
6416 6423
     const struct cl_engine *engine,
6417 6424
     struct cl_scan_options *scanoptions,
6418 6425
     void *context,
... ...
@@ -6427,8 +6470,8 @@ cl_error_t cl_scanmap_ex(
6427 6427
         if (scanoptions->heuristic & CL_SCAN_HEURISTIC_EXCEEDS_MAX) {
6428 6428
             if (engine->cb_virus_found) {
6429 6429
                 engine->cb_virus_found(fmap_fd(map), "Heuristics.Limits.Exceeded.MaxFileSize", context);
6430
-                if (virname) {
6431
-                    *virname = "Heuristics.Limits.Exceeded.MaxFileSize";
6430
+                if (last_alert_out) {
6431
+                    *last_alert_out = "Heuristics.Limits.Exceeded.MaxFileSize";
6432 6432
                 }
6433 6433
             }
6434 6434
             return CL_VIRUS;
... ...
@@ -6444,8 +6487,9 @@ cl_error_t cl_scanmap_ex(
6444 6444
     return scan_common(
6445 6445
         map,
6446 6446
         filename,
6447
-        virname,
6448
-        scanned,
6447
+        verdict_out,
6448
+        last_alert_out,
6449
+        scanned_out,
6449 6450
         engine,
6450 6451
         scanoptions,
6451 6452
         context,
... ...
@@ -6485,12 +6529,14 @@ cl_error_t cl_scanfile(
6485 6485
     struct cl_scan_options *scanoptions)
6486 6486
 {
6487 6487
     cl_error_t status;
6488
-    uint64_t scanned_out;
6488
+    uint64_t scanned_bytes;
6489
+    cl_verdict_t verdict_out = CL_VERDICT_NOTHING_FOUND;
6489 6490
 
6490 6491
     status = cl_scanfile_ex(
6491 6492
         filename,
6493
+        &verdict_out,
6492 6494
         virname,
6493
-        &scanned_out,
6495
+        &scanned_bytes,
6494 6496
         engine,
6495 6497
         scanoptions,
6496 6498
         NULL,
... ...
@@ -6501,14 +6547,20 @@ cl_error_t cl_scanfile(
6501 6501
         NULL);
6502 6502
 
6503 6503
     if (NULL != scanned) {
6504
-        if (SIZEOF_LONG == 4 && scanned_out > UINT32_MAX) {
6505
-            cli_warnmsg("cl_scanfile_callback: scanned_out exceeds UINT32_MAX, setting to UINT32_MAX\n");
6504
+        if (SIZEOF_LONG == 4 && scanned_bytes > UINT32_MAX) {
6505
+            cli_warnmsg("cl_scanfile_callback: scanned_bytes exceeds UINT32_MAX, setting to UINT32_MAX\n");
6506 6506
             *scanned = UINT32_MAX;
6507 6507
         } else {
6508
-            *scanned = (unsigned long int)scanned_out;
6508
+            *scanned = (unsigned long int)scanned_bytes;
6509 6509
         }
6510 6510
     }
6511 6511
 
6512
+    if (verdict_out == CL_VERDICT_STRONG_INDICATOR || verdict_out == CL_VERDICT_POTENTIALLY_UNWANTED) {
6513
+        // Reporting "CL_VIRUS" is more important than reporting an error,
6514
+        // because... unfortunately we can only do one with this API.
6515
+        status = CL_VIRUS;
6516
+    }
6517
+
6512 6518
     return status;
6513 6519
 }
6514 6520
 
... ...
@@ -6522,9 +6574,11 @@ cl_error_t cl_scanfile_callback(
6522 6522
 {
6523 6523
     cl_error_t status;
6524 6524
     uint64_t scanned_out;
6525
+    cl_verdict_t verdict_out = CL_VERDICT_NOTHING_FOUND;
6525 6526
 
6526 6527
     status = cl_scanfile_ex(
6527 6528
         filename,
6529
+        &verdict_out,
6528 6530
         virname,
6529 6531
         &scanned_out,
6530 6532
         engine,
... ...
@@ -6545,13 +6599,20 @@ cl_error_t cl_scanfile_callback(
6545 6545
         }
6546 6546
     }
6547 6547
 
6548
+    if (verdict_out == CL_VERDICT_STRONG_INDICATOR || verdict_out == CL_VERDICT_POTENTIALLY_UNWANTED) {
6549
+        // Reporting "CL_VIRUS" is more important than reporting an error,
6550
+        // because... unfortunately we can only do one with this API.
6551
+        status = CL_VIRUS;
6552
+    }
6553
+
6548 6554
     return status;
6549 6555
 }
6550 6556
 
6551 6557
 cl_error_t cl_scanfile_ex(
6552 6558
     const char *filename,
6553
-    const char **virname,
6554
-    uint64_t *scanned,
6559
+    cl_verdict_t *verdict_out,
6560
+    const char **last_alert_out,
6561
+    uint64_t *scanned_out,
6555 6562
     const struct cl_engine *engine,
6556 6563
     struct cl_scan_options *scanoptions,
6557 6564
     void *context,
... ...
@@ -6582,8 +6643,9 @@ cl_error_t cl_scanfile_ex(
6582 6582
     ret = cl_scandesc_ex(
6583 6583
         fd,
6584 6584
         filename,
6585
-        virname,
6586
-        scanned,
6585
+        verdict_out,
6586
+        last_alert_out,
6587
+        scanned_out,
6587 6588
         engine,
6588 6589
         scanoptions,
6589 6590
         context,
... ...
@@ -154,9 +154,7 @@ pub unsafe extern "C" fn _evidence_add_child_evidence(
154 154
 /// Free the evidence
155 155
 #[no_mangle]
156 156
 pub extern "C" fn evidence_free(evidence: sys::evidence_t) {
157
-    if evidence.is_null() {
158
-        warn!("Attempted to free a NULL evidence pointer. Please report this at: https://github.com/Cisco-Talos/clamav/issues");
159
-    } else {
157
+    if !evidence.is_null() {
160 158
         let _ = unsafe { Box::from_raw(evidence as *mut Evidence) };
161 159
     }
162 160
 }
... ...
@@ -18,6 +18,16 @@ pub struct bignum_st {
18 18
     _unused: [u8; 0],
19 19
 }
20 20
 pub type BIGNUM = bignum_st;
21
+#[doc = "< No alerting signatures matched."]
22
+pub const cl_verdict_t_CL_VERDICT_NOTHING_FOUND: cl_verdict_t = 0;
23
+#[doc = "< The scan target has been deemed trusted (e.g. by FP signature or Authenticode)."]
24
+pub const cl_verdict_t_CL_VERDICT_TRUSTED: cl_verdict_t = 1;
25
+#[doc = "< One or more strong indicator signatures matched."]
26
+pub const cl_verdict_t_CL_VERDICT_STRONG_INDICATOR: cl_verdict_t = 2;
27
+#[doc = "< One or more potentially unwanted signatures matched."]
28
+pub const cl_verdict_t_CL_VERDICT_POTENTIALLY_UNWANTED: cl_verdict_t = 3;
29
+#[doc = " @brief Scan verdicts for cl_scanmap_ex(), cl_scanfile_ex(), and cl_scandesc_ex()."]
30
+pub type cl_verdict_t = ::std::os::raw::c_uint;
21 31
 pub const cl_error_t_CL_CLEAN: cl_error_t = 0;
22 32
 pub const cl_error_t_CL_SUCCESS: cl_error_t = 0;
23 33
 pub const cl_error_t_CL_VIRUS: cl_error_t = 1;
... ...
@@ -58,10 +68,11 @@ pub const cl_error_t_CL_EBUSY: cl_error_t = 31;
58 58
 pub const cl_error_t_CL_ESTATE: cl_error_t = 32;
59 59
 #[doc = " may be reported in testmode"]
60 60
 pub const cl_error_t_CL_VERIFIED: cl_error_t = 33;
61
-#[doc = " The binary has been deemed trusted"]
61
+#[doc = " The scan target has been deemed trusted"]
62 62
 pub const cl_error_t_CL_ERROR: cl_error_t = 34;
63 63
 #[doc = " Unspecified / generic error"]
64 64
 pub const cl_error_t_CL_ELAST_ERROR: cl_error_t = 35;
65
+#[doc = " @brief Return codes used by libclamav functions."]
65 66
 pub type cl_error_t = ::std::os::raw::c_uint;
66 67
 #[doc = " scan options"]
67 68
 #[repr(C)]
... ...
@@ -651,6 +662,7 @@ pub struct cli_scan_layer {
651 651
     pub object_id: usize,
652 652
     pub metadata_json: *mut json_object,
653 653
     pub evidence: evidence_t,
654
+    pub verdict: cl_verdict_t,
654 655
     pub tmpdir: *mut ::std::os::raw::c_char,
655 656
     pub parent: *mut cli_scan_layer,
656 657
 }
... ...
@@ -176,7 +176,8 @@ class TC(testcase.TestCase):
176 176
                 'Data scanned: 948 B',
177 177
                 'Hash:         21495c3a579d537dc63b0df710f63e60a0bfbc74d1c2739a313dbd42dd31e1fa',
178 178
                 'File Type:    CL_TYPE_ZIP',
179
-                'Return code:  Virus(es) detected (1)',
179
+                'Verdict:      Found Strong Indicator',
180
+                'Return Code:  CL_SUCCESS (0)',
180 181
             ]
181 182
 
182 183
         command = '{valgrind} {valgrind_args} {example} -d {database} -f {target} --script {script}'.format(
... ...
@@ -187,7 +188,8 @@ class TC(testcase.TestCase):
187 187
         )
188 188
         output = self.execute_command(command)
189 189
 
190
-        assert output.ec == 1  # virus(es) found
190
+        # Check for success
191
+        assert output.ec == 0
191 192
 
192 193
         # Custom logic to verify the output making sure that all expected results are found in the output in order.
193 194
         #
... ...
@@ -288,7 +290,7 @@ class TC(testcase.TestCase):
288 288
                 'Data scanned: 948 B',
289 289
                 'Hash:         21495c3a579d537dc63b0df710f63e60a0bfbc74d1c2739a313dbd42dd31e1fa',
290 290
                 'File Type:    CL_TYPE_ZIP',
291
-                'Return code:  No viruses detected (0)',
291
+                'Return Code:  CL_SUCCESS (0)',
292 292
             ]
293 293
 
294 294
         command = '{valgrind} {valgrind_args} {example} -d {database} -f {target} --script {script}'.format(
... ...
@@ -299,7 +301,8 @@ class TC(testcase.TestCase):
299 299
         )
300 300
         output = self.execute_command(command)
301 301
 
302
-        assert output.ec == 0  # no virus(es) found
302
+        # Check for success
303
+        assert output.ec == 0
303 304
 
304 305
         # Custom logic to verify the output making sure that all expected results are found in the output in order.
305 306
         #
... ...
@@ -336,7 +339,7 @@ class TC(testcase.TestCase):
336 336
                 'Data scanned: 0 B',
337 337
                 'Hash:         21495c3a579d537dc63b0df710f63e60a0bfbc74d1c2739a313dbd42dd31e1fa',
338 338
                 'File Type:    CL_TYPE_ZIP',
339
-                'Return code:  No viruses detected (0)',
339
+                'Return Code:  CL_SUCCESS (0)',
340 340
             ]
341 341
 
342 342
         command = '{valgrind} {valgrind_args} {example} -d {database} -f {target} --script {script}'.format(
... ...
@@ -347,7 +350,8 @@ class TC(testcase.TestCase):
347 347
         )
348 348
         output = self.execute_command(command)
349 349
 
350
-        assert output.ec == 0  # virus(es) found
350
+        # Check for success
351
+        assert output.ec == 0
351 352
 
352 353
         # Custom logic to verify the output making sure that all expected results are found in the output in order.
353 354
         #
... ...
@@ -398,7 +402,8 @@ class TC(testcase.TestCase):
398 398
                 'Data scanned: 0 B',
399 399
                 'Hash:         21495c3a579d537dc63b0df710f63e60a0bfbc74d1c2739a313dbd42dd31e1fa',
400 400
                 'File Type:    CL_TYPE_ZIP',
401
-                'Return code:  Virus(es) detected (1)',
401
+                'Verdict:      Found Strong Indicator',
402
+                'Return Code:  CL_SUCCESS (0)',
402 403
             ]
403 404
 
404 405
         command = '{valgrind} {valgrind_args} {example} -d {database} -f {target} --script {script}'.format(
... ...
@@ -409,7 +414,8 @@ class TC(testcase.TestCase):
409 409
         )
410 410
         output = self.execute_command(command)
411 411
 
412
-        assert output.ec == 1  # virus(es) found
412
+        # Check for success
413
+        assert output.ec == 0
413 414
 
414 415
         # Custom logic to verify the output making sure that all expected results are found in the output in order.
415 416
         #
... ...
@@ -526,7 +532,7 @@ class TC(testcase.TestCase):
526 526
                 'Data scanned: 948 B',
527 527
                 'Hash:         21495c3a579d537dc63b0df710f63e60a0bfbc74d1c2739a313dbd42dd31e1fa',
528 528
                 'File Type:    CL_TYPE_ZIP',
529
-                'Return code:  No viruses detected (0)',
529
+                'Return Code:  CL_SUCCESS (0)',
530 530
             ]
531 531
 
532 532
         command = '{valgrind} {valgrind_args} {example} -d {database} -f {target} --script {script}'.format(
... ...
@@ -537,7 +543,8 @@ class TC(testcase.TestCase):
537 537
         )
538 538
         output = self.execute_command(command)
539 539
 
540
-        assert output.ec == 0  # no virus(es) found
540
+        # Check for success
541
+        assert output.ec == 0
541 542
 
542 543
         # Custom logic to verify the output making sure that all expected results are found in the output in order.
543 544
         #