Browse code

Added additional ex_scan_callbacks test and fixed a couple related bugs

Improvements to the ex_scan_callbacks.c program:
- Print the verdict enum variant names to be more explicit.
- Add the file_props callback (aka metadata JSON) with --gen-json option.
- Add a --debug option.
- Use '-' in option names instead of '_' to be consistent with other programs.
- Add option to disable allmatch, which I named --one-match. :)

Tests: Add ex_scan_callbacks test where --allmatch is disabled.
Verify that CL_VIRUS is returned when a match occurs.
I found a few bugs and inconsistencies from this test and went and fixed
them, and improved the clamav.h function comments as well.
Largely this resulted in cleanup in `cli_magic_scan()` to make sure we
don't accidentally overwrite the return code.
But it also meant making sure that callback functions which are supposed
to trust a file actually clear the evidence/verdict and don't return
CL_VIRUS.

Valerie Snyder authored on 2025/07/29 13:30:47
Showing 4 changed files
... ...
@@ -1083,6 +1083,22 @@ const char *cl_error_t_to_string(cl_error_t clerror)
1083 1083
     }
1084 1084
 }
1085 1085
 
1086
+const char *cl_verdict_t_to_string(cl_verdict_t verdict)
1087
+{
1088
+    switch (verdict) {
1089
+        case CL_VERDICT_NOTHING_FOUND:
1090
+            return "CL_VERDICT_NOTHING_FOUND";
1091
+        case CL_VERDICT_TRUSTED:
1092
+            return "CL_VERDICT_TRUSTED";
1093
+        case CL_VERDICT_STRONG_INDICATOR:
1094
+            return "CL_VERDICT_STRONG_INDICATOR";
1095
+        case CL_VERDICT_POTENTIALLY_UNWANTED:
1096
+            return "CL_VERDICT_POTENTIALLY_UNWANTED";
1097
+        default:
1098
+            return "Unknown verdict value";
1099
+    }
1100
+}
1101
+
1086 1102
 cl_error_t pre_hash_callback(cl_scan_layer_t *layer, void *context)
1087 1103
 {
1088 1104
     cl_error_t status;
... ...
@@ -1206,6 +1222,23 @@ static void printBytes(uint64_t bytes)
1206 1206
     }
1207 1207
 }
1208 1208
 
1209
+int file_props_callback(const char *j_propstr, int rc, void *context)
1210
+{
1211
+    (void)context; // Unused in this example
1212
+
1213
+    printf("\n⭐In FILE_PROPS callback⭐\n");
1214
+
1215
+    if (j_propstr) {
1216
+        printf("%s\n", j_propstr);
1217
+    }
1218
+
1219
+    printf("Metadata JSON Return Code: %s (%d)\n", cl_error_t_to_string((cl_error_t)rc), rc);
1220
+
1221
+    // Pass through the return code so as not to alter the scan return code.
1222
+    // A real application might want to handle this differently.
1223
+    return rc;
1224
+}
1225
+
1209 1226
 /*
1210 1227
  * Exit codes:
1211 1228
  *  0: clean
... ...
@@ -1226,6 +1259,9 @@ int main(int argc, char **argv)
1226 1226
     const char *hash_hint       = NULL;
1227 1227
     const char *hash_alg        = NULL;
1228 1228
     const char *file_type_hint  = NULL;
1229
+    bool allmatch               = true;
1230
+    bool gen_json               = false;
1231
+    bool debug_mode             = false;
1229 1232
 
1230 1233
     script_context_t *script_context = NULL;
1231 1234
 
... ...
@@ -1248,16 +1284,18 @@ int main(int argc, char **argv)
1248 1248
         "Example: %s -d /path/to/clamav.db -f /path/to/file.txt\n"
1249 1249
         "\n"
1250 1250
         "Options:\n"
1251
-        "--help (-h)      : Help message.\n"
1252
-        "--database (-d)  : Path to the ClamAV database.\n"
1253
-        "--file (-f)      : Path to the file to scan.\n"
1254
-        "--hash_hint      : (optional) Hash of file to scan.\n"
1255
-        "--hash_alg       : (optional) Hash algorithm of hash_hint.\n"
1256
-        "                   Will also change the hash algorithm reported at end of scan.\n"
1257
-        "--file_type_hint : (optional) File type hint for the file to scan.\n"
1258
-        "--script         : (optional) Path for non-interactive test script.\n"
1259
-        "                   Script must be a new-line delimited list of integers from 1-to-5\n"
1260
-        "                   Corresponding to the interactive scan options.\n"
1251
+        "--help (-h)                : Help message.\n"
1252
+        "--database (-d) FILE       : Path to the ClamAV database.\n"
1253
+        "--file (-f)     FILE       : Path to the file to scan.\n"
1254
+        "--hash-hint     HASH       : (optional) Hash of file to scan.\n"
1255
+        "--hash-alg      ALGORITHM  : (optional) Hash algorithm of hash-hint.\n"
1256
+        "                             Will also change the hash algorithm reported at end of scan.\n"
1257
+        "--file-type-hint CL_TYPE_* : (optional) File type hint for the file to scan.\n"
1258
+        "--script        FILE       : (optional) Path for non-interactive test script.\n"
1259
+        "                             Script must be a new-line delimited list of integers from 1-to-5\n"
1260
+        "                             Corresponding to the interactive scan options.\n"
1261
+        "--one-match (-1)           : Disable allmatch (stops scans after one match).\n"
1262
+        "--gen-json                 : Generate scan metadata JSON.\n"
1261 1263
         "\n"
1262 1264
         "Scripted scan options are:\n"
1263 1265
         "%s";
... ...
@@ -1276,15 +1314,24 @@ int main(int argc, char **argv)
1276 1276
         } else if (strcmp(argv[i], "--script") == 0) {
1277 1277
             script_filepath = argv[++i];
1278 1278
             printf("Script file: %s\n", script_filepath);
1279
-        } else if (strcmp(argv[i], "--hash_hint") == 0) {
1279
+        } else if (strcmp(argv[i], "--hash-hint") == 0) {
1280 1280
             hash_hint = argv[++i];
1281 1281
             printf("Hash hint: %s\n", hash_hint);
1282
-        } else if (strcmp(argv[i], "--hash_alg") == 0) {
1282
+        } else if (strcmp(argv[i], "--hash-alg") == 0) {
1283 1283
             hash_alg = argv[++i];
1284 1284
             printf("Hash algorithm: %s\n", hash_alg);
1285
-        } else if (strcmp(argv[i], "--file_type_hint") == 0) {
1285
+        } else if (strcmp(argv[i], "--file-type-hint") == 0) {
1286 1286
             file_type_hint = argv[++i];
1287 1287
             printf("File type hint: %s\n", file_type_hint);
1288
+        } else if (strcmp(argv[i], "--one-match") == 0 || strcmp(argv[i], "-1") == 0) {
1289
+            allmatch = false;
1290
+            printf("Disabling allmatch (stops scans after one match).\n");
1291
+        } else if (strcmp(argv[i], "--gen-json") == 0) {
1292
+            gen_json = true;
1293
+            printf("Enabling scan metadata JSON feature.\n");
1294
+        } else if (strcmp(argv[i], "--debug") == 0) {
1295
+            debug_mode = true;
1296
+            printf("Enabling debug mode.\n");
1288 1297
         } else {
1289 1298
             printf("Unknown option: %s\n", argv[i]);
1290 1299
             printf(help_string, argv[0], argv[0], command_list);
... ...
@@ -1321,6 +1368,10 @@ int main(int argc, char **argv)
1321 1321
         goto done;
1322 1322
     }
1323 1323
 
1324
+    if (debug_mode) {
1325
+        cl_debug();
1326
+    }
1327
+
1324 1328
     if (!(engine = cl_engine_new())) {
1325 1329
         printf("Can't create new engine\n");
1326 1330
         goto done;
... ...
@@ -1352,10 +1403,14 @@ int main(int argc, char **argv)
1352 1352
 
1353 1353
     /* Enable all parsers plus heuristics, allmatch, and the gen-json metadata feature. */
1354 1354
     memset(&options, 0, sizeof(struct cl_scan_options));
1355
-    options.parse |= ~0;                                 /* enable all parsers */
1356
-    options.general |= CL_SCAN_GENERAL_HEURISTICS;       /* enable heuristic alert options */
1357
-    options.general |= CL_SCAN_GENERAL_ALLMATCHES;       /* run in all-match mode, so it keeps looking for alerts after the first one */
1358
-    options.general |= CL_SCAN_GENERAL_COLLECT_METADATA; /* collect metadata may enable collecting additional filenames (like in zip) */
1355
+    options.parse |= ~0;                           /* enable all parsers */
1356
+    options.general |= CL_SCAN_GENERAL_HEURISTICS; /* enable heuristic alert options */
1357
+    if (allmatch) {
1358
+        options.general |= CL_SCAN_GENERAL_ALLMATCHES; /* run in all-match mode, so it keeps looking for alerts after the first one */
1359
+    }
1360
+    if (gen_json) {
1361
+        options.general |= CL_SCAN_GENERAL_COLLECT_METADATA; /* collect metadata may enable collecting additional filenames (like in zip) */
1362
+    }
1359 1363
 
1360 1364
     /*
1361 1365
      * Set our callbacks.
... ...
@@ -1365,11 +1420,12 @@ int main(int argc, char **argv)
1365 1365
     cl_engine_set_scan_callback(engine, &post_scan_callback, CL_SCAN_CALLBACK_POST_SCAN);
1366 1366
     cl_engine_set_scan_callback(engine, &alert_callback, CL_SCAN_CALLBACK_ALERT);
1367 1367
     cl_engine_set_scan_callback(engine, &file_type_callback, CL_SCAN_CALLBACK_FILE_TYPE);
1368
+    if (gen_json) {
1369
+        cl_engine_set_clcb_file_props(engine, &file_props_callback);
1370
+    }
1368 1371
 
1369 1372
     printf("Testing scan layer callbacks on: %s (fd: %d)\n", filename, target_fd);
1370 1373
 
1371
-    // cl_debug();
1372
-
1373 1374
     /*
1374 1375
      * Run the scan.
1375 1376
      * Note that the callbacks will be called during this function.
... ...
@@ -1405,22 +1461,9 @@ int main(int argc, char **argv)
1405 1405
     } else {
1406 1406
         printf("No file type provided for this file.\n");
1407 1407
     }
1408
-    switch (verdict) {
1409
-        case CL_VERDICT_NOTHING_FOUND: {
1410
-            printf("Verdict:      Nothing found.\n");
1411
-        } break;
1412
-
1413
-        case CL_VERDICT_TRUSTED: {
1414
-            printf("Verdict:      Trusted.\n");
1415
-        } break;
1416
-
1417
-        case CL_VERDICT_STRONG_INDICATOR: {
1418
-            printf("Verdict:      Found Strong Indicator: %s\n", alert_name);
1419
-        } break;
1420
-
1421
-        case CL_VERDICT_POTENTIALLY_UNWANTED: {
1422
-            printf("Verdict:      Found Potentially Unwanted Indicator: %s\n", alert_name);
1423
-        } break;
1408
+    printf("Verdict:      %s\n", cl_verdict_t_to_string(verdict));
1409
+    if (alert_name) {
1410
+        printf("Alert Name:   %s\n", alert_name);
1424 1411
     }
1425 1412
     printf("Return Code:  %s (%d)\n", cl_error_t_to_string(ret), ret);
1426 1413
 
... ...
@@ -1123,9 +1123,9 @@ extern void cl_engine_set_clcb_pre_scan(struct cl_engine *engine, clcb_pre_scan
1123 1123
  * @param result    The scan result for the file.
1124 1124
  * @param virname   A signature name if there was one or more matches.
1125 1125
  * @param context   Opaque application provided data.
1126
- * @return          Scan result is not overridden.
1127
- * @return          CL_BREAK = Allowed by callback - scan result is set to CL_CLEAN.
1128
- * @return          Blocked by callback - scan result is set to CL_VIRUS.
1126
+ * @return          CL_CLEAN = File is scanned.
1127
+ * @return          CL_BREAK = Allowed by callback - file is skipped and marked as clean.
1128
+ * @return          CL_VIRUS = Blocked by callback - file is skipped and marked as infected.
1129 1129
  */
1130 1130
 typedef cl_error_t (*clcb_post_scan)(int fd, int result, const char *virname, void *context);
1131 1131
 /**
... ...
@@ -4302,6 +4302,7 @@ void emax_reached(cli_ctx *ctx)
4302 4302
 static cl_error_t dispatch_file_inspection_callback(clcb_file_inspection cb, cli_ctx *ctx, const char *filetype)
4303 4303
 {
4304 4304
     cl_error_t status = CL_CLEAN;
4305
+    cl_error_t append_ret;
4305 4306
 
4306 4307
     int fd              = -1;
4307 4308
     uint32_t fmap_index = ctx->recursion_level; /* index of current file */
... ...
@@ -4347,18 +4348,24 @@ static cl_error_t dispatch_file_inspection_callback(clcb_file_inspection cb, cli
4347 4347
 
4348 4348
     switch (status) {
4349 4349
         case CL_BREAK:
4350
-            cli_dbgmsg("dispatch_file_inspection_callback: scan cancelled by callback\n");
4351
-            status = CL_BREAK;
4350
+            cli_dbgmsg("dispatch_file_inspection_callback: file trusted by callback\n");
4351
+
4352
+            // Remove any evidence for this layer and set the verdict to trusted.
4353
+            (void)cli_trust_this_layer(ctx);
4354
+
4352 4355
             break;
4353 4356
         case CL_VIRUS:
4354 4357
             cli_dbgmsg("dispatch_file_inspection_callback: file blocked by callback\n");
4355
-            cli_append_virus(ctx, "Detected.By.Callback.Inspection");
4356
-            status = CL_VIRUS;
4358
+            append_ret = cli_append_virus(ctx, "Detected.By.Callback.Inspection");
4359
+            if (append_ret == CL_VIRUS) {
4360
+                status = CL_VIRUS;
4361
+            }
4357 4362
             break;
4358
-        case CL_CLEAN:
4363
+        case CL_SUCCESS:
4364
+            // No action requested by callback. Keep scanning.
4359 4365
             break;
4360 4366
         default:
4361
-            status = CL_CLEAN;
4367
+            status = CL_SUCCESS;
4362 4368
             cli_warnmsg("dispatch_file_inspection_callback: ignoring bad return code from callback\n");
4363 4369
     }
4364 4370
 
... ...
@@ -4371,6 +4378,7 @@ done:
4371 4371
 static cl_error_t dispatch_prescan_callback(clcb_pre_scan cb, cli_ctx *ctx, const char *filetype)
4372 4372
 {
4373 4373
     cl_error_t status = CL_CLEAN;
4374
+    cl_error_t append_ret;
4374 4375
 
4375 4376
     if (cb) {
4376 4377
         perf_start(ctx, PERFT_PRECB);
... ...
@@ -4388,12 +4396,16 @@ static cl_error_t dispatch_prescan_callback(clcb_pre_scan cb, cli_ctx *ctx, cons
4388 4388
                 break;
4389 4389
             case CL_VIRUS:
4390 4390
                 cli_dbgmsg("dispatch_prescan_callback: file blocked by callback\n");
4391
-                status = cli_append_virus(ctx, "Detected.By.Callback");
4391
+                append_ret = cli_append_virus(ctx, "Detected.By.Callback");
4392
+                if (append_ret == CL_VIRUS) {
4393
+                    status = CL_VIRUS;
4394
+                }
4392 4395
                 break;
4393
-            case CL_CLEAN:
4396
+            case CL_SUCCESS:
4397
+                // No action requested by callback. Keep scanning.
4394 4398
                 break;
4395 4399
             default:
4396
-                status = CL_CLEAN;
4400
+                status = CL_SUCCESS;
4397 4401
                 cli_warnmsg("dispatch_prescan_callback: ignoring bad return code from callback\n");
4398 4402
         }
4399 4403
     }
... ...
@@ -4552,38 +4564,40 @@ done:
4552 4552
 
4553 4553
 cl_error_t cli_magic_scan(cli_ctx *ctx, cli_file_t type)
4554 4554
 {
4555
-    cl_error_t ret                     = CL_CLEAN;
4555
+    cl_error_t status = CL_SUCCESS;
4556
+    cl_error_t ret;
4557
+
4556 4558
     cl_error_t cache_check_result      = CL_VIRUS;
4557 4559
     cl_verdict_t verdict_at_this_level = CL_VERDICT_NOTHING_FOUND;
4558 4560
 
4559 4561
     bool cache_enabled              = true;
4560
-    cli_file_t dettype              = 0;
4562
+    cli_file_t dettype              = CL_TYPE_ANY;
4561 4563
     uint8_t typercg                 = 1;
4562 4564
     bitset_t *old_hook_lsig_matches = NULL;
4563 4565
     const char *filetype;
4564 4566
 
4565 4567
     if (!ctx->engine) {
4566 4568
         cli_errmsg("CRITICAL: engine == NULL\n");
4567
-        ret = CL_ENULLARG;
4569
+        status = CL_ENULLARG;
4568 4570
         goto early_ret;
4569 4571
     }
4570 4572
 
4571 4573
     if (!(ctx->engine->dboptions & CL_DB_COMPILED)) {
4572 4574
         cli_errmsg("CRITICAL: engine not compiled\n");
4573
-        ret = CL_EMALFDB;
4575
+        status = CL_EMALFDB;
4574 4576
         goto early_ret;
4575 4577
     }
4576 4578
 
4577 4579
     if (ctx->fmap->len <= 5) {
4578
-        ret = CL_CLEAN;
4580
+        status = CL_SUCCESS;
4579 4581
         cli_dbgmsg("cli_magic_scan: File is too small (%zu bytes), ignoring.\n", ctx->fmap->len);
4580 4582
         goto early_ret;
4581 4583
     }
4582 4584
 
4583
-    if (cli_updatelimits(ctx, ctx->fmap->len) != CL_CLEAN) {
4585
+    if (cli_updatelimits(ctx, ctx->fmap->len) != CL_SUCCESS) {
4584 4586
         emax_reached(ctx);
4585
-        ret = CL_CLEAN;
4586
-        cli_dbgmsg("cli_magic_scan: returning %d %s (no post, no cache)\n", ret, __AT__);
4587
+        status = CL_SUCCESS;
4588
+        cli_dbgmsg("cli_magic_scan: returning %d %s (no post, no cache)\n", status, __AT__);
4587 4589
         goto early_ret;
4588 4590
     }
4589 4591
 
... ...
@@ -4604,9 +4618,9 @@ cl_error_t cli_magic_scan(cli_ctx *ctx, cli_file_t type)
4604 4604
     }
4605 4605
     perf_stop(ctx, PERFT_FT);
4606 4606
     if (type == CL_TYPE_ERROR) {
4607
-        ret = CL_EREAD;
4607
+        status = CL_EREAD;
4608 4608
         cli_dbgmsg("cli_magic_scan: cli_determine_fmap_type returned CL_TYPE_ERROR\n");
4609
-        cli_dbgmsg("cli_magic_scan: returning %d %s (no post, no cache)\n", ret, __AT__);
4609
+        cli_dbgmsg("cli_magic_scan: returning %d %s (no post, no cache)\n", status, __AT__);
4610 4610
         goto early_ret;
4611 4611
     }
4612 4612
     filetype = cli_ftname(type);
... ...
@@ -4615,8 +4629,9 @@ cl_error_t cli_magic_scan(cli_ctx *ctx, cli_file_t type)
4615 4615
     ret = cli_recursion_stack_change_type(ctx, type, true /* ? */);
4616 4616
     if (CL_SUCCESS != ret) {
4617 4617
         cli_dbgmsg("cli_magic_scan: cli_recursion_stack_change_type returned %d\n", ret);
4618
-        cli_dbgmsg("cli_magic_scan: returning %d %s (no post, no cache)\n", ret, __AT__);
4619
-        goto early_ret;
4618
+        // We must go to done here (and not early_ret), because `ret` needs to be tidied up before returning.
4619
+        status = ret;
4620
+        goto done;
4620 4621
     }
4621 4622
 
4622 4623
     /*
... ...
@@ -4624,6 +4639,7 @@ cl_error_t cli_magic_scan(cli_ctx *ctx, cli_file_t type)
4624 4624
      */
4625 4625
     ret = cli_dispatch_scan_callback(ctx, CL_SCAN_CALLBACK_PRE_HASH);
4626 4626
     if (CL_SUCCESS != ret) {
4627
+        status = ret;
4627 4628
         goto done;
4628 4629
     }
4629 4630
 
... ...
@@ -4632,6 +4648,7 @@ cl_error_t cli_magic_scan(cli_ctx *ctx, cli_file_t type)
4632 4632
      */
4633 4633
     ret = dispatch_prescan_callback(ctx->engine->cb_pre_cache, ctx, filetype);
4634 4634
     if (CL_VERIFIED == ret || CL_VIRUS == ret) {
4635
+        status = ret;
4635 4636
         goto done;
4636 4637
     }
4637 4638
 
... ...
@@ -4645,6 +4662,7 @@ cl_error_t cli_magic_scan(cli_ctx *ctx, cli_file_t type)
4645 4645
         } else {
4646 4646
             ret = CL_CLEAN;
4647 4647
         }
4648
+        status = ret;
4648 4649
         goto done;
4649 4650
     }
4650 4651
 
... ...
@@ -4670,6 +4688,7 @@ cl_error_t cli_magic_scan(cli_ctx *ctx, cli_file_t type)
4670 4670
                 ret = fmap_will_need_hash_later(ctx->fmap, hash_type);
4671 4671
                 if (CL_SUCCESS != ret) {
4672 4672
                     cli_dbgmsg("cli_check_fp: Failed to set fmap to need the %s hash later\n", cli_hash_name(hash_type));
4673
+                    status = ret;
4673 4674
                     goto done;
4674 4675
                 }
4675 4676
             }
... ...
@@ -4686,7 +4705,7 @@ cl_error_t cli_magic_scan(cli_ctx *ctx, cli_file_t type)
4686 4686
                     cli_dbgmsg("cli_magic_scan: Failed to get a hash for the current fmap.\n");
4687 4687
                     // It may be that the file was truncated between the time we started the scan and the time we got the hash.
4688 4688
                     // Not a reason to print an error message.
4689
-                    ret = CL_SUCCESS;
4689
+                    status = CL_SUCCESS;
4690 4690
                     goto done;
4691 4691
                 }
4692 4692
 
... ...
@@ -4698,8 +4717,9 @@ cl_error_t cli_magic_scan(cli_ctx *ctx, cli_file_t type)
4698 4698
 
4699 4699
                 ret = cli_jsonstr(ctx->this_layer_metadata_json, cli_hash_name(hash_type), hash_string);
4700 4700
                 if (ret != CL_SUCCESS) {
4701
-                    cli_dbgmsg("cli_magic_scan: returning %d %s (no post, no cache)\n", ret, __AT__);
4702
-                    goto early_ret;
4701
+                    cli_dbgmsg("cli_magic_scan: Failed to store the %s hash in the metadata JSON.\n", cli_hash_name(hash_type));
4702
+                    status = ret;
4703
+                    goto done;
4703 4704
                 }
4704 4705
             }
4705 4706
         }
... ...
@@ -4715,8 +4735,10 @@ cl_error_t cli_magic_scan(cli_ctx *ctx, cli_file_t type)
4715 4715
     }
4716 4716
 
4717 4717
     if (cache_enabled && (cache_check_result != CL_VIRUS)) {
4718
-        ret = CL_SUCCESS;
4719
-        cli_dbgmsg("cli_magic_scan: returning %d %s (no post, no cache)\n", ret, __AT__);
4718
+        status = CL_SUCCESS;
4719
+        cli_dbgmsg("cli_magic_scan: returning %d %s (no post, no cache)\n", status, __AT__);
4720
+        // We can go to early_ret here, because we know status is CL_SUCCESS, and we obviously add to the cache.
4721
+        // This does mean, however, that we do not run the post-scan callback for layers that are cached.
4720 4722
         goto early_ret;
4721 4723
     }
4722 4724
 
... ...
@@ -4729,6 +4751,7 @@ cl_error_t cli_magic_scan(cli_ctx *ctx, cli_file_t type)
4729 4729
      */
4730 4730
     ret = cli_dispatch_scan_callback(ctx, CL_SCAN_CALLBACK_PRE_SCAN);
4731 4731
     if (CL_SUCCESS != ret) {
4732
+        status = ret;
4732 4733
         goto done;
4733 4734
     }
4734 4735
 
... ...
@@ -4737,13 +4760,14 @@ cl_error_t cli_magic_scan(cli_ctx *ctx, cli_file_t type)
4737 4737
      */
4738 4738
     ret = dispatch_prescan_callback(ctx->engine->cb_pre_scan, ctx, filetype);
4739 4739
     if (CL_VERIFIED == ret || CL_VIRUS == ret) {
4740
+        status = ret;
4740 4741
         goto done;
4741 4742
     }
4742 4743
 
4743 4744
     // If none of the scan options are enabled, then we can skip parsing and just do a raw pattern match.
4744 4745
     // For this check, we don't care if the CL_SCAN_GENERAL_ALLMATCHES option is enabled, hence the `~`.
4745 4746
     if (!((ctx->options->general & ~CL_SCAN_GENERAL_ALLMATCHES) || (ctx->options->parse) || (ctx->options->heuristic) || (ctx->options->mail) || (ctx->options->dev))) {
4746
-        ret = cli_scan_fmap(ctx, CL_TYPE_ANY, false, NULL, AC_SCAN_VIR, NULL);
4747
+        status = cli_scan_fmap(ctx, CL_TYPE_ANY, false, NULL, AC_SCAN_VIR, NULL);
4747 4748
         // It doesn't matter what was returned, always go to the end after this. Raw mode! No parsing files!
4748 4749
         goto done;
4749 4750
     }
... ...
@@ -4752,7 +4776,7 @@ cl_error_t cli_magic_scan(cli_ctx *ctx, cli_file_t type)
4752 4752
     // The ctx one is NULL at present.
4753 4753
     ctx->hook_lsig_matches = cli_bitset_init();
4754 4754
     if (NULL == ctx->hook_lsig_matches) {
4755
-        ret = CL_EMEM;
4755
+        status = CL_EMEM;
4756 4756
         goto done;
4757 4757
     }
4758 4758
 
... ...
@@ -4765,7 +4789,7 @@ cl_error_t cli_magic_scan(cli_ctx *ctx, cli_file_t type)
4765 4765
 
4766 4766
         // Evaluate the result from the scan to see if it end the scan of this layer early,
4767 4767
         // and to decid if we should propagate an error or not.
4768
-        if (result_should_goto_done(ctx, ret, &ret)) {
4768
+        if (result_should_goto_done(ctx, ret, &status)) {
4769 4769
             goto done;
4770 4770
         }
4771 4771
     }
... ...
@@ -5196,7 +5220,7 @@ cl_error_t cli_magic_scan(cli_ctx *ctx, cli_file_t type)
5196 5196
 
5197 5197
     // Evaluate the result from the parsers to see if it end the scan of this layer early,
5198 5198
     // and to decide if we should propagate an error or not.
5199
-    if (result_should_goto_done(ctx, ret, &ret)) {
5199
+    if (result_should_goto_done(ctx, ret, &status)) {
5200 5200
         goto done;
5201 5201
     }
5202 5202
 
... ...
@@ -5228,7 +5252,7 @@ cl_error_t cli_magic_scan(cli_ctx *ctx, cli_file_t type)
5228 5228
 
5229 5229
         // Evaluate the result from the scan to see if it end the scan of this layer early,
5230 5230
         // and to decid if we should propagate an error or not.
5231
-        if (result_should_goto_done(ctx, ret, &ret)) {
5231
+        if (result_should_goto_done(ctx, ret, &status)) {
5232 5232
             goto done;
5233 5233
         }
5234 5234
     }
... ...
@@ -5350,17 +5374,22 @@ done:
5350 5350
      * Run the post_scan callback.
5351 5351
      */
5352 5352
     ret = cli_dispatch_scan_callback(ctx, CL_SCAN_CALLBACK_POST_SCAN);
5353
+    if (CL_SUCCESS != ret) {
5354
+        cli_dbgmsg("cli_magic_scan: POST_SCAN callback returned %d\n", ret);
5355
+        status = ret;
5356
+    }
5353 5357
 
5354 5358
     // Filter the result from the parsers so we don't propagate non-fatal errors.
5355
-    // And to convert CL_VERIFIED -> CL_CLEAN
5356
-    (void)result_should_goto_done(ctx, ret, &ret);
5359
+    // And to convert CL_VERIFIED -> CL_SUCCESS
5360
+    (void)result_should_goto_done(ctx, status, &status);
5357 5361
 
5358 5362
     /*
5359 5363
      * Run the deprecated post-scan callback (if one exists) and provide the verdict for this layer.
5360 5364
      */
5361
-    cli_dbgmsg("cli_magic_scan: returning %d %s\n", ret, __AT__);
5365
+    cli_dbgmsg("cli_magic_scan: returning %d %s\n", status, __AT__);
5362 5366
     if (ctx->engine->cb_post_scan) {
5363 5367
         cl_error_t callback_ret;
5368
+        cl_error_t append_ret;
5364 5369
         const char *virusname = NULL;
5365 5370
 
5366 5371
         // Get the last signature that matched (if any).
... ...
@@ -5375,23 +5404,34 @@ done:
5375 5375
         switch (callback_ret) {
5376 5376
             case CL_BREAK:
5377 5377
                 cli_dbgmsg("cli_magic_scan: file allowed by post_scan callback\n");
5378
-                ret = CL_CLEAN;
5378
+
5379
+                // Remove any evidence for this layer and set the verdict to trusted.
5380
+                (void)cli_trust_this_layer(ctx);
5381
+
5382
+                //status = CL_SUCCESS; // Do override the status here.
5383
+                // If status == CL_VIRUS, we'll fix when we look at the verdict.
5379 5384
                 break;
5380 5385
             case CL_VIRUS:
5381 5386
                 cli_dbgmsg("cli_magic_scan: file blocked by post_scan callback\n");
5382
-                callback_ret = cli_append_virus(ctx, "Detected.By.Callback");
5383
-                if (callback_ret == CL_VIRUS) {
5384
-                    ret = CL_VIRUS;
5387
+                append_ret = cli_append_virus(ctx, "Detected.By.Callback");
5388
+                if (append_ret == CL_VIRUS) {
5389
+                    status = CL_VIRUS;
5385 5390
                 }
5386 5391
                 break;
5387
-            case CL_CLEAN:
5392
+            case CL_SUCCESS:
5393
+                // No action requested by callback. Keep scanning.
5388 5394
                 break;
5389 5395
             default:
5390
-                ret = CL_CLEAN;
5396
+                //status = CL_SUCCESS; // Do override the status here, just log a warning.
5391 5397
                 cli_warnmsg("cli_magic_scan: ignoring bad return code from post_scan callback\n");
5392 5398
         }
5393 5399
     }
5394 5400
 
5401
+    /*
5402
+     * Check the verdict for this layer.
5403
+     * If the verdict is CL_VERDICT_TRUSTED, remove any evidence for this layer and clear CL_VIRUS status (if set)
5404
+     * Otherwise, we'll update the verdict based on the evidence.
5405
+     */
5395 5406
     if (CL_VERDICT_TRUSTED == ctx->recursion_stack[ctx->recursion_level].verdict) {
5396 5407
         /* Remove any alerts for this layer. */
5397 5408
         if (NULL != ctx->recursion_stack[ctx->recursion_level].evidence) {
... ...
@@ -5399,6 +5439,9 @@ done:
5399 5399
             ctx->recursion_stack[ctx->recursion_level].evidence = NULL;
5400 5400
             ctx->this_layer_evidence                            = NULL;
5401 5401
         }
5402
+        if (CL_VIRUS == status) {
5403
+            status = CL_SUCCESS; // If we have a CL_VERDICT_TRUSTED, we should not return CL_VIRUS.
5404
+        }
5402 5405
     } else {
5403 5406
         /*
5404 5407
          * Update the verdict for this layer based on the scan results.
... ...
@@ -5437,7 +5480,7 @@ early_ret:
5437 5437
         ctx->hook_lsig_matches = old_hook_lsig_matches;
5438 5438
     }
5439 5439
 
5440
-    return ret;
5440
+    return status;
5441 5441
 }
5442 5442
 
5443 5443
 cl_error_t cli_magic_scan_desc_type(int desc, const char *filepath, cli_ctx *ctx, cli_file_t type,
... ...
@@ -9,16 +9,18 @@ For reference:
9 9
     Example: ./install/bin/ex_scan_callbacks -d /path/to/clamav.db -f /path/to/file.txt
10 10
 
11 11
     Options:
12
-    --help (-h)      : Help message.
13
-    --database (-d)  : Path to the ClamAV database.
14
-    --file (-f)      : Path to the file to scan.
15
-    --hash_hint      : (optional) Hash of file to scan.
16
-    --hash_alg       : (optional) Hash algorithm of hash_hint.
17
-                    Will also change the hash algorithm reported at end of scan.
18
-    --file_type_hint : (optional) File type hint for the file to scan.
19
-    --script         : (optional) Path for non-interactive test script.
20
-                    Script must be a new-line delimited list of integers from 1-to-5
21
-                    Corresponding to the interactive scan options.
12
+    --help (-h)                : Help message.
13
+    --database (-d) FILE       : Path to the ClamAV database.
14
+    --file (-f)     FILE       : Path to the file to scan.
15
+    --hash-hint     HASH       : (optional) Hash of file to scan.
16
+    --hash-alg      ALGORITHM  : (optional) Hash algorithm of hash-hint.
17
+                                 Will also change the hash algorithm reported at end of scan.
18
+    --file-type-hint CL_TYPE_* : (optional) File type hint for the file to scan.
19
+    --script        FILE       : (optional) Path for non-interactive test script.
20
+                                 Script must be a new-line delimited list of integers from 1-to-5
21
+                                 Corresponding to the interactive scan options.
22
+    --one-match (-1)           : Disable allmatch (stops scans after one match).
23
+    --gen-json                 : Generate scan metadata JSON.
22 24
 
23 25
     Scripted scan options are:
24 26
     1  - Return CL_BREAK to abort scanning. Will still encounter POST_SCAN-callbacks on the way out.
... ...
@@ -32,6 +34,7 @@ For reference:
32 32
     9  - Get sha1 hash. Does not return from the callback!
33 33
     10 - Get sha2-256 hash. Does not return from the callback!
34 34
     11 - Print all hashes that have already been calculated. Does not return from the callback!
35
+
35 36
 """
36 37
 
37 38
 import os
... ...
@@ -176,7 +179,7 @@ class TC(testcase.TestCase):
176 176
                 'Data scanned: 948 B',
177 177
                 'Hash:         21495c3a579d537dc63b0df710f63e60a0bfbc74d1c2739a313dbd42dd31e1fa',
178 178
                 'File Type:    CL_TYPE_ZIP',
179
-                'Verdict:      Found Strong Indicator',
179
+                'Verdict:      CL_VERDICT_STRONG_INDICATOR',
180 180
                 'Return Code:  CL_SUCCESS (0)',
181 181
             ]
182 182
 
... ...
@@ -188,7 +191,7 @@ class TC(testcase.TestCase):
188 188
         )
189 189
         output = self.execute_command(command)
190 190
 
191
-        # Check for success
191
+        # Check for CL_SUCCESS return code
192 192
         assert output.ec == 0
193 193
 
194 194
         # Custom logic to verify the output making sure that all expected results are found in the output in order.
... ...
@@ -204,6 +207,125 @@ class TC(testcase.TestCase):
204 204
 
205 205
             remaining_output = parts[1]
206 206
 
207
+    def test_cl_scan_callbacks_clam_zip_basic_one_match(self):
208
+        self.step_name('Same as basic test with clam.zip that just keeps scanning--but disables allmatch mode.')
209
+
210
+        # Notably, the return code at the end should be CL_VIRUS (1) instead of CL_SUCCESS (0).
211
+        # This is because the reason the scan ended "early" is because of the alert in the clam.exe file.
212
+
213
+        path_db = TC.path_source / 'unit_tests' / 'input' / 'clamav.hdb'
214
+
215
+        # Build up expected results as we define the test script.
216
+        expected_results = []
217
+
218
+        test_script = TC.path_tmp / 'zip_basic.txt'
219
+        with open(test_script, 'w') as f:
220
+            expected_results += [
221
+                'In FILE_TYPE callback',
222
+                'Recursion Level:    0',
223
+                'File Name:          clam.zip',
224
+                'File Type:          CL_TYPE_ZIP',
225
+            ]
226
+            f.write('2\n') # Return CL_SUCCESS to keep scanning
227
+
228
+            expected_results += [
229
+                'In PRE_HASH callback',
230
+                'Recursion Level:    0',
231
+                'File Name:          clam.zip',
232
+                'File Type:          CL_TYPE_ZIP',
233
+            ]
234
+            f.write('2\n') # Return CL_SUCCESS to keep scanning
235
+
236
+            expected_results += [
237
+                'In PRE_SCAN callback',
238
+                'Recursion Level:    0',
239
+                'File Name:          clam.zip',
240
+                'File Type:          CL_TYPE_ZIP',
241
+            ]
242
+            f.write('2\n') # Return CL_SUCCESS to keep scanning
243
+
244
+            expected_results += [
245
+                'In FILE_TYPE callback',
246
+                'Recursion Level:    1',
247
+                'File Name:          clam.exe',
248
+                'File Type:          CL_TYPE_MSEXE',
249
+            ]
250
+            f.write('2\n') # Return CL_SUCCESS to keep scanning
251
+
252
+            expected_results += [
253
+                'In PRE_HASH callback',
254
+                'Recursion Level:    1',
255
+                'File Name:          clam.exe',
256
+                'File Type:          CL_TYPE_MSEXE',
257
+            ]
258
+            f.write('2\n') # Return CL_SUCCESS to keep scanning
259
+
260
+            expected_results += [
261
+                'In PRE_SCAN callback',
262
+                'Recursion Level:    1',
263
+                'File Name:          clam.exe',
264
+                'File Type:          CL_TYPE_MSEXE',
265
+            ]
266
+            f.write('2\n') # Return CL_SUCCESS to keep scanning
267
+
268
+            expected_results += [
269
+                'In ALERT callback',
270
+                'Recursion Level:    1',
271
+                'File Name:          clam.exe',
272
+                'File Type:          CL_TYPE_MSEXE',
273
+                'Last Alert:         ClamAV-Test-File.UNOFFICIAL',
274
+            ]
275
+            f.write('3\n') # Return CL_VIRUS to keep scanning and accept the alert
276
+
277
+            expected_results += [
278
+                'In POST_SCAN callback',
279
+                'Recursion Level:    1',
280
+                'File Name:          clam.exe',
281
+                'File Type:          CL_TYPE_MSEXE',
282
+                'Last Alert:         ClamAV-Test-File.UNOFFICIAL',
283
+            ]
284
+            f.write('2\n') # Return CL_SUCCESS to keep scanning
285
+
286
+            expected_results += [
287
+                'Recursion Level:    0',
288
+                'In POST_SCAN callback',
289
+                'File Name:          clam.zip',
290
+                'File Type:          CL_TYPE_ZIP'
291
+            ]
292
+            f.write('2\n') # Return CL_SUCCESS to keep scanning
293
+
294
+            expected_results += [
295
+                'Data scanned: 544 B',  # Note this is less, because allmatch disabled so stopped after clam.exe matched.
296
+                'Hash:         21495c3a579d537dc63b0df710f63e60a0bfbc74d1c2739a313dbd42dd31e1fa',
297
+                'File Type:    CL_TYPE_ZIP',
298
+                'Verdict:      CL_VERDICT_STRONG_INDICATOR',
299
+                'Return Code:  CL_VIRUS (1)',
300
+            ]
301
+
302
+        command = '{valgrind} {valgrind_args} {example} -d {database} -f {target} --script {script} --one-match'.format(
303
+            valgrind=TC.valgrind, valgrind_args=TC.valgrind_args, example=TC.example_program,
304
+            database=path_db,
305
+            target=TC.path_build / 'unit_tests' / 'input' / 'clamav_hdb_scanfiles' / 'clam.zip',
306
+            script=test_script
307
+        )
308
+        output = self.execute_command(command)
309
+
310
+        # Check for CL_VIRUS return code
311
+        assert output.ec == 1
312
+
313
+        # Custom logic to verify the output making sure that all expected results are found in the output in order.
314
+        #
315
+        # This is necessary because the STRICT_ORDER option gets confused when expected results have multiple of the
316
+        # same string, but in different contexts.
317
+        remaining_output = output.out
318
+
319
+        for expected in expected_results:
320
+            # find the first occurrence of the expected string in remaining_output, splitting into two parts
321
+            parts = remaining_output.split(expected, 1)
322
+            assert len(parts) == 2, f"Expected '{expected}' in output, but it was not found:\n{remaining_output}"
323
+
324
+            remaining_output = parts[1]
325
+
207 326
     def test_cl_scan_callbacks_clam_zip_ignore_alert(self):
208 327
         self.step_name('Ignore alert in clam.exe (within clam.zip) and keep scanning.')
209 328
 
... ...
@@ -301,7 +423,7 @@ class TC(testcase.TestCase):
301 301
         )
302 302
         output = self.execute_command(command)
303 303
 
304
-        # Check for success
304
+        # Check for CL_SUCCESS return code
305 305
         assert output.ec == 0
306 306
 
307 307
         # Custom logic to verify the output making sure that all expected results are found in the output in order.
... ...
@@ -350,7 +472,7 @@ class TC(testcase.TestCase):
350 350
         )
351 351
         output = self.execute_command(command)
352 352
 
353
-        # Check for success
353
+        # Check for CL_SUCCESS return code
354 354
         assert output.ec == 0
355 355
 
356 356
         # Custom logic to verify the output making sure that all expected results are found in the output in order.
... ...
@@ -402,7 +524,7 @@ class TC(testcase.TestCase):
402 402
                 'Data scanned: 0 B',
403 403
                 'Hash:         21495c3a579d537dc63b0df710f63e60a0bfbc74d1c2739a313dbd42dd31e1fa',
404 404
                 'File Type:    CL_TYPE_ZIP',
405
-                'Verdict:      Found Strong Indicator',
405
+                'Verdict:      CL_VERDICT_STRONG_INDICATOR',
406 406
                 'Return Code:  CL_SUCCESS (0)',
407 407
             ]
408 408
 
... ...
@@ -414,7 +536,7 @@ class TC(testcase.TestCase):
414 414
         )
415 415
         output = self.execute_command(command)
416 416
 
417
-        # Check for success
417
+        # Check for CL_SUCCESS return code
418 418
         assert output.ec == 0
419 419
 
420 420
         # Custom logic to verify the output making sure that all expected results are found in the output in order.
... ...
@@ -543,7 +665,7 @@ class TC(testcase.TestCase):
543 543
         )
544 544
         output = self.execute_command(command)
545 545
 
546
-        # Check for success
546
+        # Check for CL_SUCCESS return code
547 547
         assert output.ec == 0
548 548
 
549 549
         # Custom logic to verify the output making sure that all expected results are found in the output in order.