-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathScanCore.php
More file actions
2890 lines (2794 loc) · 189 KB
/
Copy pathScanCore.php
File metadata and controls
2890 lines (2794 loc) · 189 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?php
// / -----------------------------------------------------------------------------------
// / COPYRIGHT INFORMATION ...
// / ScanCore, Copyright on 3/31/2024 by Justin Grimes, www.github.com/zelon88
// /
// / LICENSE INFORMATION ...
// / This project is protected by the GNU GPLv3 Open-Source license.
// / BSD or MIT licensing is available. Reach out to @zelon88 for more information.
// / https://www.gnu.org/licenses/gpl-3.0.html
// /
// / APPLICATION INFORMATION ...
// / This application is designed to scan files & folders for viruses.
// /
// / FILE INFORMATION ...
// / v1.8.
// / This file contains the core logic of the ScanCore application.
// /
// / HARDWARE REQUIREMENTS ...
// / This application requires at least a Raspberry Pi Model B+ or greater.
// / This application will run on just about any x86 or x64 computer.
// /
// / DEPENDENCY REQUIREMENTS ...
// / This application should run on Linux or Windows systems with PHP 8.0 (or later).
// / Git is preferred for performing automatic update operations, but not required.
// /
// / VALID SWITCHES / ARGUMENTS / USAGE ...
// / Quick Start Example:
// / C:\Path-To-PHP-Binary.exe C:\Path-To-ScanCore.php C:\Path-To-Scan\ -m [integer] -c [integer] -v -d
// /
// / Start by opening a command-prompt.
// / Type the absolute path to a portable PHP 8.0+ binary. Don't press enter just yet.
// / Now type the absolute path to this PHP file as the only argument for the PHP binary.
// / Everything after the path to this script will be passed to this file as an argument.
// / The first Argument Must be a valid absolute path to the file or folder being scanned.
// / Optional arguments can be specified after the scan path. Separate them with spaces.
// /
// / Reqiured Arguments Include:
// /
// / File or folder to scan: /path/to/scan
// /
// / Optional Arguments Include:
// /
// / Show version information: -version
// / -ver
// /
// / Show help information: -help
// / -h
// /
// / Force recursion: -recursion
// / -r
// /
// / Force no recursion: -norecursion
// / -nr
// /
// / Specify memory limit (in bytes): -memorylimit ####
// / -m ####
// /
// / Specify chunk size (in bytes); -chunksize ####
// / -c ####
// /
// / Enable "debug" mode (more logging): -debug
// / -d
// /
// / Enable "verbose" mode (more console): -verbose
// / -v
// /
// / Force a specific report file: -reportfile /path/to/file
// / -rf path/to/file
// / -logfile /path/to/file
// / -lf path/to/file
// /
// / Force a specific configuration file: -configfile /path/to/file
// / -cf path/to/file
// /
// / Force a specific definitions file: -defsfile /path/to/file
// / -df path/to/file
// /
// / Force maximum log size (in bytes): -maxlogsize ###
// / -ml ###
// /
// / Force maximum scan depth (in folders): -maxdepth ###
// / -md ###
// /
// / Follow symbolic links while scanning: -followsymlinks
// / -fs
// /
// / Perform definition update: -updatedefinitions
// / -ud
// /
// / Perform application update: -updateapplication
// / -ua
// /
// / EXIT STATUS ...
// / A status of 0 is returned when the requested operation completed.
// / A status of 1 is returned when a fatal error stopped the requested operation.
// / A clean scan & an infected scan both return 0, exactly as previous versions did.
// /
// / NAMING CONVENTIONS ...
// / Upper case first letter variables are global in scope.
// / Lower case first letter variables never leave the function that initialized them.
// / The only documented exception is $argv, which PHP itself defines for us.
// /
// / <3 Open-Source
// / -----------------------------------------------------------------------------------
// / -----------------------------------------------------------------------------------
// / A function to release sensitive values from memory as soon as they stop being needed.
// / Every target must be passed as its own argument so no copy of the value is created.
// / Collecting targets into an array before the call would copy each value first.
function purgeSensitiveMemory(&...$variables) {
// / Set variables.
$MemoryPurged = FALSE;
$purgeCount = 0;
// / Overwrite each supplied reference so the caller's value is released immediately.
foreach ($variables as $purgeKey => $purgeValue) {
$variables[$purgeKey] = NULL;
$purgeCount++; }
if ($purgeCount > 0) $MemoryPurged = TRUE;
// / Manually clean up sensitive memory. Helps to keep track of variable assignments.
$purgeKey = $purgeValue = $purgeCount = NULL;
unset($purgeKey, $purgeValue, $purgeCount);
return $MemoryPurged; }
// / -----------------------------------------------------------------------------------
// / -----------------------------------------------------------------------------------
// / A function to reset a narrow scope variable to a known value inside a loop body.
// / Assigning NULL before the new value releases the previous value straight away.
function redeclare(&$variable, $value = NULL) {
$variable = NULL;
$variable = $value; }
// / -----------------------------------------------------------------------------------
// / -----------------------------------------------------------------------------------
// / A function to sanitize any supplied input before that input gets used.
// / Arrays are walked recursively so a nested value cannot slip past unsanitized.
// / Strings lose null bytes & control characters, which are never valid in our inputs.
// / Other scalar types are already safe & are returned unaltered.
function sanitize($input) {
// / Set variables.
$SanitizedInput = $input;
// / Walk arrays so every member is sanitized individually.
if (is_array($input)) {
$SanitizedInput = array();
foreach ($input as $inputKey => $inputValue) $SanitizedInput[sanitize($inputKey)] = sanitize($inputValue); }
// / Strip characters from strings that no legitimate argument would ever carry.
if (is_string($input)) {
$SanitizedInput = str_replace(chr(0), '', $input);
$SanitizedInput = preg_replace('/[\x01-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $SanitizedInput);
$SanitizedInput = trim($SanitizedInput); }
// / Objects & resources are never expected here, so they are reduced to an empty string.
if (is_object($input) or is_resource($input)) $SanitizedInput = '';
// / Manually clean up sensitive memory. Helps to keep track of variable assignments.
purgeSensitiveMemory($inputKey, $inputValue, $input);
return $SanitizedInput; }
// / -----------------------------------------------------------------------------------
// / -----------------------------------------------------------------------------------
// / A function to normalize a command line switch before it gets compared.
// / Switches only ever contain a leading dash & lower case letters.
// / Values such as file paths are never passed through this function.
function normalizeSwitch($argument) {
// / Set variables.
$NormalizedSwitch = '';
$workingArgument = sanitize($argument);
if (is_string($workingArgument)) $NormalizedSwitch = strtolower($workingArgument);
$NormalizedSwitch = preg_replace('/[^a-z\-]/', '', $NormalizedSwitch);
// / Manually clean up sensitive memory. Helps to keep track of variable assignments.
purgeSensitiveMemory($workingArgument, $argument);
return $NormalizedSwitch; }
// / -----------------------------------------------------------------------------------
// / -----------------------------------------------------------------------------------
// / A function to compare two version numbers numerically rather than as strings.
// / A string comparison ranks 24.2 below 7.6 & ranks 3.10 below 3.9, so we never use one.
// / A leading v is stripped first because casting 'v3' to an integer yields 0.
// / The supplied version passes when it is equal to or newer than the required version.
function compareVersions($requiredVersion, $suppliedVersion) {
// / Set variables.
$VersionsCompared = $SuppliedIsAcceptable = FALSE;
$requiredParts = $suppliedParts = array();
$requiredClean = strtolower(trim(sanitize($requiredVersion)));
$suppliedClean = strtolower(trim(sanitize($suppliedVersion)));
$requiredClean = ltrim($requiredClean, 'v');
$suppliedClean = ltrim($suppliedClean, 'v');
// / A build that reports no parseable version cannot be cleared, so it is refused.
if (preg_match('/^[0-9]+(\.[0-9]+)*$/', $requiredClean) && preg_match('/^[0-9]+(\.[0-9]+)*$/', $suppliedClean)) {
$VersionsCompared = TRUE;
$requiredParts = explode('.', $requiredClean);
$suppliedParts = explode('.', $suppliedClean);
// / Pad the shorter version so a two part version compares fairly against a three part one.
while (count($requiredParts) < count($suppliedParts)) $requiredParts[] = '0';
while (count($suppliedParts) < count($requiredParts)) $suppliedParts[] = '0';
$SuppliedIsAcceptable = TRUE;
$partIndex = 0;
$partTop = count($requiredParts);
while ($partIndex < $partTop) {
if (intval($suppliedParts[$partIndex]) > intval($requiredParts[$partIndex])) $partIndex = $partTop;
else {
if (intval($suppliedParts[$partIndex]) < intval($requiredParts[$partIndex])) {
$SuppliedIsAcceptable = FALSE;
$partIndex = $partTop; }
else $partIndex++; } } }
// / Manually clean up sensitive memory. Helps to keep track of variable assignments.
purgeSensitiveMemory($requiredParts, $suppliedParts, $requiredClean, $suppliedClean, $partIndex, $partTop, $requiredVersion, $suppliedVersion);
return array($VersionsCompared, $SuppliedIsAcceptable); }
// / -----------------------------------------------------------------------------------
// / -----------------------------------------------------------------------------------
// / A function to resolve a path against the installation directory when it is relative.
// / A microservice gets invoked from whatever directory the caller happens to occupy.
// / Resolving against the installation directory keeps logs & definitions where expected.
function resolvePath($path, $baseDirectory) {
// / Set variables.
global $SEP;
$ResolvedPath = sanitize($path);
$pathIsAbsolute = FALSE;
// / Detect a POSIX absolute path, a Windows drive letter path & a Windows network path.
if (strlen($ResolvedPath) > 0) {
if (substr($ResolvedPath, 0, 1) === '/' or substr($ResolvedPath, 0, 1) === '\\') $pathIsAbsolute = TRUE;
if (preg_match('/^[A-Za-z]:[\\\\\/]/', $ResolvedPath)) $pathIsAbsolute = TRUE; }
if (!$pathIsAbsolute && strlen($ResolvedPath) > 0) $ResolvedPath = $baseDirectory.$SEP.$ResolvedPath;
// / Collapse any doubled separator that the concatenation may have introduced.
$ResolvedPath = str_replace($SEP.$SEP, $SEP, $ResolvedPath);
// / Manually clean up sensitive memory. Helps to keep track of variable assignments.
purgeSensitiveMemory($pathIsAbsolute, $path, $baseDirectory);
return $ResolvedPath; }
// / -----------------------------------------------------------------------------------
// / -----------------------------------------------------------------------------------
// / A function to verify that this installation can actually run before anything starts.
// / Previous versions declared the installation verified without inspecting anything.
function verifyInstallation() {
global $Date, $Time, $Version, $InstallationVerified, $FileCount, $DirCount, $Infected, $Suspicious, $DetectionCount, $HandlerFileCount, $CodeFileCount, $EOL, $SEP, $RP, $CoreFile, $DefaultConfigFile, $LogAvailable, $LogBytesWritten, $VerificationFault;
// / Time related variables.
$Date = date("m_d_y");
$Time = date("F j, Y, g:i a");
// / Application related variables.
$Version = 'v1.8';
$DefaultConfigFile = 'ScanCore_Config.php';
$FileCount = $DirCount = $Infected = $Suspicious = $DetectionCount = $LogBytesWritten = 0;
$HandlerFileCount = $CodeFileCount = 0;
$EOL = PHP_EOL;
$SEP = DIRECTORY_SEPARATOR;
$RP = realpath(dirname(__FILE__));
$CoreFile = 'ScanCore.php';
$LogAvailable = TRUE;
$InstallationVerified = TRUE;
$VerificationFault = '';
$requiredFunctions = array('hash_file', 'hash_init', 'hash_update', 'hash_final', 'scandir', 'fopen', 'fread', 'file_put_contents', 'file_get_contents');
// / Refuse to run anywhere except a command line, because there is no web interface here.
if (PHP_SAPI !== 'cli') {
$InstallationVerified = FALSE;
$VerificationFault = 'ScanCore only runs from a command line interpreter'; }
// / Refuse to run on an interpreter older than the documented dependency requirement.
if (version_compare(PHP_VERSION, '8.0.0', '<')) {
$InstallationVerified = FALSE;
$VerificationFault = 'ScanCore requires PHP 8.0 or later, found '.PHP_VERSION; }
// / Refuse to run when a required function has been disabled by the interpreter.
foreach ($requiredFunctions as $requiredFunction) if (!function_exists($requiredFunction)) {
$InstallationVerified = FALSE;
$VerificationFault = 'A required PHP function is unavailable: '.$requiredFunction; }
// / Refuse to run when the installation directory cannot be resolved.
if ($RP === FALSE or $RP === '') {
$InstallationVerified = FALSE;
$VerificationFault = 'Cannot resolve the ScanCore installation directory'; }
// / Manually clean up sensitive memory. Helps to keep track of variable assignments.
purgeSensitiveMemory($requiredFunctions, $requiredFunction);
return array($InstallationVerified, $Version, $VerificationFault); }
// / -----------------------------------------------------------------------------------
// / -----------------------------------------------------------------------------------
// / A function to apply a hardcoded default whenever a configuration entry is unusable.
// / A subsystem that can fall back must fall back before it errors.
// / Faults are collected rather than logged because logging is not available this early.
// / The type may be boolean, string, integer, array or list.
function applyConfigFallback(&$setting, $defaultValue, $settingName, $settingType) {
global $ConfigFaults;
// / Set variables.
$SettingAccepted = TRUE;
// / Detect a setting that was never defined by the configuration file at all.
if (!isset($setting)) $SettingAccepted = FALSE;
else {
if ($settingType === 'boolean' && !is_bool($setting)) $SettingAccepted = FALSE;
if ($settingType === 'string' && (!is_string($setting) or $setting === '')) $SettingAccepted = FALSE;
if ($settingType === 'array' && (!is_array($setting) or count($setting) === 0)) $SettingAccepted = FALSE;
// / A list differs from an array in that an empty list is a real answer, not a mistake.
// / Naming no classifiers means wanting none of them, which is not the same as
// / forgetting to name any, so an empty list is accepted exactly as it was written.
if ($settingType === 'list' && !is_array($setting)) $SettingAccepted = FALSE;
if ($settingType === 'integer' && (!is_numeric($setting) or intval($setting) < 1)) $SettingAccepted = FALSE; }
// / Fall back to the hardcoded default & record why, so the administrator can see it.
if (!$SettingAccepted) {
$setting = $defaultValue;
$ConfigFaults[] = 'Configuration entry $'.$settingName.' is missing or invalid, using the built in default instead.'; }
if ($SettingAccepted && $settingType === 'integer') $setting = intval($setting);
// / Manually clean up sensitive memory. Helps to keep track of variable assignments.
purgeSensitiveMemory($defaultValue, $settingName, $settingType);
return $SettingAccepted; }
// / -----------------------------------------------------------------------------------
// / -----------------------------------------------------------------------------------
// / A function to load a specified configuration file.
// / If neither the -configfile nor the -cf argument is set, ScanCore_Config.php is used.
// / If neither the -defsfile nor the -df argument is set, the configured file is used.
// / The core version arrives as an argument & is never pulled back out of global scope.
function loadConfig($coreVersion) {
global $argv, $ConfigFilePath, $RP, $SEP, $EOL, $ConfigFile, $ScanLoc, $DefsFile, $ConfigVersion, $ConfigLoaded, $DefsExist, $ReportFile, $ReportDir, $ReportFileName, $RequiredDirs, $InstallDir, $MaxLogSize, $MemoryLimit, $ChunkSize, $DefaultMemoryLimit, $DefaultChunkSize, $DefaultMaxLogSize, $DefinitionRepositoryName, $DefinitionUpdates, $DefinitionUpdateDomain, $DefinitionUpdateURL, $DefInstallDir, $DefGitDir, $ApplicationRepositoryName, $ApplicationUpdates, $ApplicationUpdateDomain, $ApplicationUpdateURL, $AppInstallDir, $AppGitDir, $DefinitionsUpdateSubscriptions, $DefsFileName, $Verbose, $Debug, $UpdateMethod, $DefinitionBranchName, $ApplicationBranchName, $ApplicationUpdateSubscriptions, $VersionsMatch, $ConfigFaults, $MaxScanDepth, $FollowSymlinks, $ConnectionTimeout, $MinimumDataSignatureLength, $ClassifyContent, $ClassifierFileName, $ClassifierFile, $MinimumLanguageSignatures, $EnabledClassifiers, $InspectArchivedContent, $MaxArchiveEntries, $MaxArchiveEntrySize, $MaxArchiveTotalSize, $MaxArchiveCompressionRatio, $ReportSuspicious, $PreserveOwnership, $OwnerUser, $OwnerGroup, $FilePermissions, $DirectoryPermissions, $UseHashIndex, $HashIndexThreshold;
// / Set variables.
$ConfigLoaded = $DefsExist = $VersionsMatch = FALSE;
$ConfigFaults = array();
$ConfigFile = 'ScanCore_Config.php';
$ConfigFilePath = $RP.$SEP.$ConfigFile;
$requireResult = FALSE;
$overrideDefsFile = '';
$createBlankDefs = FALSE;
// / Initialize an empty array if no arguments are set.
if (!isset($argv)) $argv = array();
// / Briefly iterate the arguments just to see if a special configuration file is wanted.
foreach ($argv as $configKey => $configArg) if (normalizeSwitch($configArg) === '-configfile' or normalizeSwitch($configArg) === '-cf') {
if (isset($argv[$configKey + 1])) $ConfigFilePath = resolvePath(sanitize($argv[$configKey + 1]), $RP);
else $ConfigFaults[] = 'The -configfile argument was supplied without a path, using the default instead.'; }
// / Load the configuration file located at $ConfigFilePath.
if (file_exists($ConfigFilePath) && !is_dir($ConfigFilePath)) {
$requireResult = require_once ($ConfigFilePath);
if ($requireResult !== FALSE) $ConfigLoaded = TRUE; }
// / Record the configuration file that actually loaded so later functions can find it.
if ($ConfigLoaded) $ConfigFile = $ConfigFilePath;
// / Validate every configuration entry & fall back to a working default when needed.
if ($ConfigLoaded) list($VersionsMatch) = validateConfig($coreVersion);
// / Briefly iterate the arguments to see if a special definitions file is wanted.
foreach ($argv as $defsKey => $defsArg) if (normalizeSwitch($defsArg) === '-defsfile' or normalizeSwitch($defsArg) === '-df') {
if (isset($argv[$defsKey + 1])) $overrideDefsFile = resolvePath(sanitize($argv[$defsKey + 1]), $RP);
else $ConfigFaults[] = 'The -defsfile argument was supplied without a path, using the configured file instead.'; }
if ($overrideDefsFile !== '') $DefsFile = $overrideDefsFile;
// / Create a blank definitions file when an update was requested against a missing one.
foreach ($argv as $updateKey => $updateArg) if (normalizeSwitch($updateArg) === '-updatedefinitions' or normalizeSwitch($updateArg) === '-ud') $createBlankDefs = TRUE;
if ($createBlankDefs && is_string($DefsFile) && $DefsFile !== '' && !file_exists($DefsFile)) @file_put_contents($DefsFile, '');
// / Check whether the definitions file is present & readable.
if (is_string($DefsFile) && $DefsFile !== '') if (file_exists($DefsFile) && !is_dir($DefsFile)) $DefsExist = TRUE;
// / Manually clean up sensitive memory. Helps to keep track of variable assignments.
purgeSensitiveMemory($configKey, $configArg, $defsKey, $defsArg, $updateKey, $updateArg, $requireResult, $overrideDefsFile, $createBlankDefs, $coreVersion);
return array($ConfigLoaded, $DefsExist, $ConfigFilePath, $VersionsMatch); }
// / -----------------------------------------------------------------------------------
// / -----------------------------------------------------------------------------------
// / A function to validate configuration entries & derive the paths that depend on them.
// / The configuration file is a minimum match, so a newer configuration is acceptable.
// / A configuration that is older than the core is refused, because settings may be absent.
function validateConfig($coreVersion) {
global $RP, $SEP, $ConfigVersion, $ScanLoc, $DefsFile, $DefsFileName, $ReportFile, $ReportDir, $ReportFileName, $RequiredDirs, $InstallDir, $MaxLogSize, $MemoryLimit, $ChunkSize, $DefaultMemoryLimit, $DefaultChunkSize, $DefaultMaxLogSize, $DefinitionRepositoryName, $DefinitionUpdates, $DefinitionUpdateDomain, $DefinitionUpdateURL, $DefInstallDir, $DefGitDir, $ApplicationRepositoryName, $ApplicationUpdates, $ApplicationUpdateDomain, $ApplicationUpdateURL, $AppInstallDir, $AppGitDir, $DefinitionsUpdateSubscriptions, $Verbose, $Debug, $UpdateMethod, $DefinitionBranchName, $ApplicationBranchName, $ApplicationUpdateSubscriptions, $ConfigFaults, $MaxScanDepth, $FollowSymlinks, $ConnectionTimeout, $MinimumDataSignatureLength, $ClassifyContent, $ClassifierFileName, $ClassifierFile, $MinimumLanguageSignatures, $EnabledClassifiers, $InspectArchivedContent, $MaxArchiveEntries, $MaxArchiveEntrySize, $MaxArchiveTotalSize, $MaxArchiveCompressionRatio, $ReportSuspicious, $PreserveOwnership, $OwnerUser, $OwnerGroup, $FilePermissions, $DirectoryPermissions, $UseHashIndex, $HashIndexThreshold;
// / Set variables.
$VersionsMatch = $versionsCompared = FALSE;
// / Compare the configuration version numerically against the core version.
list($versionsCompared, $VersionsMatch) = compareVersions($coreVersion, isset($ConfigVersion) ? $ConfigVersion : '');
if (!$versionsCompared) $ConfigFaults[] = 'The configuration file reports no parseable version number.';
// / Validate the entries that control update behaviour.
applyConfigFallback($ApplicationUpdates, TRUE, 'ApplicationUpdates', 'boolean');
applyConfigFallback($ApplicationUpdateURL, 'https://github.com/zelon88/ScanCore', 'ApplicationUpdateURL', 'string');
applyConfigFallback($ApplicationUpdateDomain, 'github.com', 'ApplicationUpdateDomain', 'string');
applyConfigFallback($ApplicationRepositoryName, 'ScanCore', 'ApplicationRepositoryName', 'string');
applyConfigFallback($ApplicationBranchName, 'master', 'ApplicationBranchName', 'string');
applyConfigFallback($ApplicationUpdateSubscriptions, array('README.md', 'ScanCore.php', 'ScanCore_Config.php', 'index.html', 'Documentation/CHANGELOG.txt', 'Documentation/index.html'), 'ApplicationUpdateSubscriptions', 'array');
applyConfigFallback($DefinitionUpdates, TRUE, 'DefinitionUpdates', 'boolean');
applyConfigFallback($DefinitionUpdateURL, 'https://github.com/zelon88/ScanCore_Definitions', 'DefinitionUpdateURL', 'string');
applyConfigFallback($DefinitionUpdateDomain, 'github.com', 'DefinitionUpdateDomain', 'string');
applyConfigFallback($DefinitionRepositoryName, 'ScanCore_Definitions', 'DefinitionRepositoryName', 'string');
applyConfigFallback($DefinitionBranchName, 'main', 'DefinitionBranchName', 'string');
applyConfigFallback($DefinitionsUpdateSubscriptions, array('Virus', 'Malware', 'PUP'), 'DefinitionsUpdateSubscriptions', 'array');
applyConfigFallback($UpdateMethod, 'git', 'UpdateMethod', 'string');
// / Validate the entries that control scanning behaviour.
applyConfigFallback($DefaultMaxLogSize, 1024*1024*32, 'DefaultMaxLogSize', 'integer');
applyConfigFallback($DefaultMemoryLimit, 1024*1024*512, 'DefaultMemoryLimit', 'integer');
applyConfigFallback($DefaultChunkSize, 1024*1024*8, 'DefaultChunkSize', 'integer');
applyConfigFallback($MaxScanDepth, 64, 'MaxScanDepth', 'integer');
applyConfigFallback($ConnectionTimeout, 15, 'ConnectionTimeout', 'integer');
applyConfigFallback($MinimumDataSignatureLength, 4, 'MinimumDataSignatureLength', 'integer');
applyConfigFallback($FollowSymlinks, FALSE, 'FollowSymlinks', 'boolean');
applyConfigFallback($ClassifyContent, TRUE, 'ClassifyContent', 'boolean');
applyConfigFallback($EnabledClassifiers, array('Language', 'SCAD', 'PDF', 'Document', 'Spreadsheet', 'Presentation', 'XPS', 'SVG', 'Markup', 'Stream', 'Transport', 'Model', 'Drawing', 'Ebook', 'Subtitle'), 'EnabledClassifiers', 'list');
applyConfigFallback($MinimumLanguageSignatures, 2, 'MinimumLanguageSignatures', 'integer');
applyConfigFallback($InspectArchivedContent, TRUE, 'InspectArchivedContent', 'boolean');
applyConfigFallback($MaxArchiveEntries, 512, 'MaxArchiveEntries', 'integer');
applyConfigFallback($MaxArchiveEntrySize, 1024*1024*8, 'MaxArchiveEntrySize', 'integer');
applyConfigFallback($MaxArchiveTotalSize, 1024*1024*64, 'MaxArchiveTotalSize', 'integer');
applyConfigFallback($MaxArchiveCompressionRatio, 200, 'MaxArchiveCompressionRatio', 'integer');
applyConfigFallback($ReportSuspicious, TRUE, 'ReportSuspicious', 'boolean');
applyConfigFallback($UseHashIndex, TRUE, 'UseHashIndex', 'boolean');
applyConfigFallback($HashIndexThreshold, 10000, 'HashIndexThreshold', 'integer');
applyConfigFallback($PreserveOwnership, TRUE, 'PreserveOwnership', 'boolean');
applyConfigFallback($FilePermissions, 0644, 'FilePermissions', 'integer');
applyConfigFallback($DirectoryPermissions, 0755, 'DirectoryPermissions', 'integer');
if (!isset($OwnerUser) or !is_string($OwnerUser)) $OwnerUser = '';
if (!isset($OwnerGroup) or !is_string($OwnerGroup)) $OwnerGroup = '';
applyConfigFallback($ClassifierFileName, 'ScanCore_Classifiers.def', 'ClassifierFileName', 'string');
applyConfigFallback($Debug, FALSE, 'Debug', 'boolean');
applyConfigFallback($Verbose, FALSE, 'Verbose', 'boolean');
// / Validate the entries that describe where things live on disk.
applyConfigFallback($ReportDir, 'Logs', 'ReportDir', 'string');
applyConfigFallback($ReportFileName, 'ScanCore_Report.txt', 'ReportFileName', 'string');
applyConfigFallback($DefsFileName, 'ScanCore_Combined_Definitions.def', 'DefsFileName', 'string');
applyConfigFallback($InstallDir, $RP, 'InstallDir', 'string');
if (!isset($ScanLoc) or !is_string($ScanLoc)) $ScanLoc = '';
// / Derive every path from the installation directory so the working directory never matters.
$InstallDir = resolvePath($InstallDir, $RP);
// / A configuration file loaded from elsewhere reports its own folder as the installation.
// / Fall back to the folder holding this core file whenever the core is not where it says.
if (!file_exists($InstallDir.$SEP.'ScanCore.php')) {
$ConfigFaults[] = 'Configuration entry $InstallDir does not contain ScanCore.php, using '.$RP.' instead.';
$InstallDir = $RP; }
$ReportDir = resolvePath($ReportDir, $InstallDir);
$ReportFile = $ReportDir.$SEP.basename($ReportFileName);
$RequiredDirs = array($ReportDir);
$UpdateMethod = strtolower($UpdateMethod);
// / Fall back to the git update method when the configured method is not recognized.
if ($UpdateMethod !== 'git' && $UpdateMethod !== 'raw') {
$ConfigFaults[] = 'Configuration entry $UpdateMethod is not git or raw, using git instead.';
$UpdateMethod = 'git'; }
$MaxLogSize = $DefaultMaxLogSize;
$MemoryLimit = $DefaultMemoryLimit;
$ChunkSize = $DefaultChunkSize;
// / Resolve the definitions file only when the configuration did not already do it.
if (!isset($DefsFile) or !is_string($DefsFile) or $DefsFile === '') $DefsFile = $InstallDir.$SEP.$DefsFileName;
$DefsFile = resolvePath($DefsFile, $InstallDir);
// / Fall back to the definitions beside the core when the configured file is not there.
if (!file_exists($DefsFile)) if (file_exists($InstallDir.$SEP.$DefsFileName)) {
$ConfigFaults[] = 'Configuration entry $DefsFile does not exist, using the file beside the core instead.';
$DefsFile = $InstallDir.$SEP.$DefsFileName; }
// / The classifier definitions ship with the application, beside the core.
$ClassifierFile = $InstallDir.$SEP.basename($ClassifierFileName);
// / Build the temporary update directories from the repository names.
// / An empty repository name would aim the cleaner at the installation directory itself.
$DefInstallDir = $InstallDir.$SEP.basename($DefinitionRepositoryName);
$AppInstallDir = $InstallDir.$SEP.basename($ApplicationRepositoryName);
$DefGitDir = $DefInstallDir.$SEP.'.git';
$AppGitDir = $AppInstallDir.$SEP.'.git';
// / Manually clean up sensitive memory. Helps to keep track of variable assignments.
purgeSensitiveMemory($versionsCompared, $coreVersion);
return array($VersionsMatch); }
// / -----------------------------------------------------------------------------------
// / -----------------------------------------------------------------------------------
// / A function to work out which user & group should own anything ScanCore creates.
// / An administrator running an update under sudo would otherwise leave every file owned
// / by root, & the web server user that runs ScanCore the rest of the time could no
// / longer write its own report file or install its own definition update.
// / The installation directory is the reference, because whoever owns that is whoever
// / this installation belongs to.
function resolveOwnership($announce) {
global $InstallDir, $OwnerUser, $OwnerGroup, $PreserveOwnership, $OwnershipUid, $OwnershipGid, $RunningAsRoot;
// / Set variables.
$OwnershipResolved = FALSE;
$OwnershipUid = $OwnershipGid = -1;
$RunningAsRoot = FALSE;
$referenceStat = array();
$passwordEntry = $groupEntry = array();
if (function_exists('posix_geteuid')) if (posix_geteuid() === 0) $RunningAsRoot = TRUE;
if ($RunningAsRoot && $PreserveOwnership) {
// / Inherit from the installation directory unless the configuration names an owner.
$referenceStat = @stat($InstallDir);
if (is_array($referenceStat)) {
$OwnershipUid = intval($referenceStat['uid']);
$OwnershipGid = intval($referenceStat['gid']);
$OwnershipResolved = TRUE; }
if ($OwnerUser !== '' && function_exists('posix_getpwnam')) {
$passwordEntry = @posix_getpwnam($OwnerUser);
if (is_array($passwordEntry)) {
$OwnershipUid = intval($passwordEntry['uid']);
$OwnershipResolved = TRUE; }
else { if ($announce) processOutput('Cannot resolve the configured OwnerUser, falling back to the installation directory owner: '.$OwnerUser, 'warning', 0, TRUE, FALSE, FALSE); } }
if ($OwnerGroup !== '' && function_exists('posix_getgrnam')) {
$groupEntry = @posix_getgrnam($OwnerGroup);
if (is_array($groupEntry)) {
$OwnershipGid = intval($groupEntry['gid']);
$OwnershipResolved = TRUE; }
else { if ($announce) processOutput('Cannot resolve the configured OwnerGroup, falling back to the installation directory group: '.$OwnerGroup, 'warning', 0, TRUE, FALSE, FALSE); } }
if ($announce) {
if ($OwnershipResolved) processOutput('Running as root. Anything created will be handed to uid '.$OwnershipUid.' & gid '.$OwnershipGid.'.', 'log', 0, TRUE, FALSE, FALSE);
else processOutput('Running as root & cannot work out who should own new files, so they will stay owned by root.', 'warning', 0, TRUE, FALSE, FALSE); } }
// / Manually clean up sensitive memory. Helps to keep track of variable assignments.
purgeSensitiveMemory($referenceStat, $passwordEntry, $groupEntry, $announce);
return $OwnershipResolved; }
// / -----------------------------------------------------------------------------------
// / -----------------------------------------------------------------------------------
// / A function to hand a newly created path to the owner this installation belongs to.
// / Does nothing at all unless we are root, because nobody else may give a file away.
function applyOwnership($path, $isDirectory) {
global $RunningAsRoot, $PreserveOwnership, $OwnershipUid, $OwnershipGid, $FilePermissions, $DirectoryPermissions;
// / Set variables.
$OwnershipApplied = FALSE;
if ($RunningAsRoot && $PreserveOwnership && $OwnershipUid >= 0 && file_exists($path)) {
@chown($path, $OwnershipUid);
@chgrp($path, $OwnershipGid);
if ($isDirectory) @chmod($path, $DirectoryPermissions);
else @chmod($path, $FilePermissions);
$OwnershipApplied = TRUE; }
// / Manually clean up sensitive memory. Helps to keep track of variable assignments.
purgeSensitiveMemory($path, $isDirectory);
return $OwnershipApplied; }
// / -----------------------------------------------------------------------------------
// / -----------------------------------------------------------------------------------
// / A function to record who owns a file & what its mode is, before it gets replaced.
// / An update must not quietly change either. A file that was group writable so the web
// / server could update it has to still be group writable afterwards.
function captureFileIdentity($path) {
// / Set variables.
$FileIdentity = array('exists' => FALSE, 'uid' => -1, 'gid' => -1, 'mode' => -1);
$pathStat = array();
if (file_exists($path)) {
$pathStat = @stat($path);
if (is_array($pathStat)) $FileIdentity = array('exists' => TRUE, 'uid' => intval($pathStat['uid']), 'gid' => intval($pathStat['gid']), 'mode' => intval($pathStat['mode']) & 0777); }
// / Manually clean up sensitive memory. Helps to keep track of variable assignments.
purgeSensitiveMemory($pathStat, $path);
return $FileIdentity; }
// / -----------------------------------------------------------------------------------
// / -----------------------------------------------------------------------------------
// / A function to put a recorded owner & mode back onto a replaced file.
// / The mode is restored whoever we are, because the owner of a file may always set it.
// / The owner is only restored when we are root, because nobody else may give a file away.
function restoreFileIdentity($path, $fileIdentity) {
global $RunningAsRoot, $PreserveOwnership;
// / Set variables.
$IdentityRestored = FALSE;
if ($fileIdentity['exists'] && file_exists($path)) {
if ($fileIdentity['mode'] >= 0) @chmod($path, $fileIdentity['mode']);
if ($RunningAsRoot && $PreserveOwnership && $fileIdentity['uid'] >= 0) {
@chown($path, $fileIdentity['uid']);
@chgrp($path, $fileIdentity['gid']); }
$IdentityRestored = TRUE; }
// / Manually clean up sensitive memory. Helps to keep track of variable assignments.
purgeSensitiveMemory($path, $fileIdentity);
return $IdentityRestored; }
// / -----------------------------------------------------------------------------------
// / -----------------------------------------------------------------------------------
// / A function to create required directories when they do not already exist.
function createDirs($RequiredDirs) {
global $RP, $SEP;
// / Set variables.
$RequiredDirsExist = TRUE;
// / Iterate through each required directory.
foreach ($RequiredDirs as $reqdDir) {
// / Detect if the directory already exists & create it if required.
if (!file_exists($reqdDir)) {
@mkdir($reqdDir, 0755, TRUE);
applyOwnership($reqdDir, TRUE); }
// / Copy an index.html file into the new directory as document root protection.
if (!file_exists($reqdDir.$SEP.'index.html')) if (file_exists($RP.$SEP.'index.html')) {
@copy($RP.$SEP.'index.html', $reqdDir.$SEP.'index.html');
applyOwnership($reqdDir.$SEP.'index.html', FALSE); }
if (!is_dir($reqdDir)) $RequiredDirsExist = FALSE; }
// / Manually clean up sensitive memory. Helps to keep track of variable assignments.
purgeSensitiveMemory($reqdDir, $RequiredDirs);
return array($RequiredDirsExist); }
// / -----------------------------------------------------------------------------------
// / -----------------------------------------------------------------------------------
// / A function to start a new report file once the current one reaches the maximum size.
// / Previous versions accepted a maximum log size but never acted upon it.
function rotateLog($pendingBytes) {
global $ReportFile, $MaxLogSize, $Date, $LogBytesWritten;
// / Set variables.
$LogRotated = FALSE;
$rotationIndex = 0;
$rotatedFile = '';
$currentSize = 0;
// / Measure the existing report file only once per session, then track our own writes.
if ($LogBytesWritten === 0) if (file_exists($ReportFile)) $currentSize = intval(@filesize($ReportFile));
if ($currentSize > 0) $LogBytesWritten = $currentSize;
// / Rename the current report file out of the way once it would exceed the maximum size.
if (($LogBytesWritten + $pendingBytes) > $MaxLogSize) if (file_exists($ReportFile)) {
$rotatedFile = $ReportFile.'_'.$Date.'_'.$rotationIndex.'.old';
while (file_exists($rotatedFile)) {
$rotationIndex++;
redeclare($rotatedFile, $ReportFile.'_'.$Date.'_'.$rotationIndex.'.old'); }
$LogRotated = @rename($ReportFile, $rotatedFile);
if ($LogRotated) {
applyOwnership($rotatedFile, FALSE);
$LogBytesWritten = 0; } }
// / Manually clean up sensitive memory. Helps to keep track of variable assignments.
purgeSensitiveMemory($rotationIndex, $rotatedFile, $currentSize, $pendingBytes);
return $LogRotated; }
// / -----------------------------------------------------------------------------------
// / -----------------------------------------------------------------------------------
// / A function to add an entry to the logs.
// / The level must be one of log, warning or error.
// / A log entry records normal activity & carries no number.
// / A warning tells the administrator something happened that did not stop the work.
// / An error records a failure that altered behaviour & always carries a documented number.
function addLogEntry($entry, $level, $errorNumber) {
global $ReportFile, $EOL, $LogAvailable, $LogBytesWritten;
// / Set variables.
$LogCreated = FALSE;
$preText = '';
$entryTime = date("F j, Y, g:i a");
$writeResult = FALSE;
$reportExisted = FALSE;
if (!is_numeric($errorNumber)) $errorNumber = 0;
if ($level === 'error') $preText = 'ERROR!!! ScanCore-'.intval($errorNumber).' on '.$entryTime.', ';
if ($level === 'warning') $preText = 'WARNING!!! ScanCore on '.$entryTime.', ';
if ($level !== 'error' && $level !== 'warning') $preText = $entryTime.', ';
// / Write the entry only while the report file remains usable.
if ($LogAvailable) {
rotateLog(strlen($preText.$entry.$EOL));
$reportExisted = file_exists($ReportFile);
$writeResult = @file_put_contents($ReportFile, $preText.$entry.$EOL, FILE_APPEND);
// / A report created while running as root would otherwise lock out the service user.
if (!$reportExisted && $writeResult !== FALSE) applyOwnership($ReportFile, FALSE);
if ($writeResult === FALSE) {
// / A scanner that cannot write a log must still scan, so logging is simply disabled.
$LogAvailable = FALSE;
echo 'WARNING!!! ScanCore-910, Cannot write to the report file: '.$ReportFile.$EOL; }
else {
$LogBytesWritten = $LogBytesWritten + intval($writeResult);
$LogCreated = TRUE; } }
// / Manually clean up sensitive memory. Helps to keep track of variable assignments.
purgeSensitiveMemory($preText, $entryTime, $writeResult, $reportExisted, $entry, $level, $errorNumber);
return array($LogCreated); }
// / -----------------------------------------------------------------------------------
// / -----------------------------------------------------------------------------------
// / A function to handle important messages to the console & log file.
// / The level argument replaces the boolean that previous versions used.
// / Passing a boolean still works, so no call site can silently change meaning.
function processOutput($txt, $level, $errorNumber, $requiredLog, $requiredConsole, $fatal) {
global $EOL, $Debug, $Verbose;
// / Set variables.
$OutputProcessed = FALSE;
$entryLevel = 'log';
// / Verify that all inputs are of the correct type.
if (!is_string($txt)) $txt = '';
if (!is_numeric($errorNumber)) $errorNumber = 0;
// / Translate the level argument, accepting the boolean that older call sites passed.
if ($level === TRUE or $level === 'error') $entryLevel = 'error';
if ($level === 'warning') $entryLevel = 'warning';
// / An error without a documented number is really a warning, so it gets recorded as one.
if ($entryLevel === 'error' && intval($errorNumber) === 0) $entryLevel = 'warning';
// / Log the provided text if the $Debug variable (-d switch) is set.
if ($Debug or $requiredLog) list ($OutputProcessed) = addLogEntry($txt, $entryLevel, $errorNumber);
// / Output the summary text to the terminal if the $Verbose (-v switch) variable is set.
if ($Verbose or $requiredConsole) echo $txt.$EOL;
// / Manually clean up sensitive memory. Helps to keep track of variable assignments.
purgeSensitiveMemory($txt, $entryLevel, $level, $errorNumber, $requiredLog, $requiredConsole);
// / Stop execution as needed, reporting a failing status to whatever invoked us.
if ($fatal) exit(1);
return array($OutputProcessed); }
// / -----------------------------------------------------------------------------------
// / -----------------------------------------------------------------------------------
// / A function to render a subscription list as readable text.
// / Both the updaters & the version screen need this, so it lives in one place.
function buildSubscriptionText($subscriptions) {
// / Set variables.
$SubscriptionText = '';
if (is_array($subscriptions)) foreach ($subscriptions as $subscriptionName) $SubscriptionText = $SubscriptionText.' '.$subscriptionName.',';
$SubscriptionText = trim(trim($SubscriptionText, ','), ' ');
// / Manually clean up sensitive memory. Helps to keep track of variable assignments.
purgeSensitiveMemory($subscriptionName, $subscriptions);
return $SubscriptionText; }
// / -----------------------------------------------------------------------------------
// / -----------------------------------------------------------------------------------
// / A function to reliably build help & version information.
// / Every path is absolute so this works no matter which directory invoked ScanCore.
function buildHelpInformation() {
global $DefinitionsUpdateSubscriptions, $SubText, $VersionText, $HelpText, $ApplicationUpdateURL, $DefinitionUpdateURL, $DefsFile, $ConfigFile, $Version, $EOL;
// / Set variables.
$InformationBuilt = FALSE;
$SubText = $VersionText = $HelpText = '';
$coreFilePath = realpath(__FILE__);
$configFilePath = realpath($ConfigFile);
$defsFilePath = realpath($DefsFile);
$SubText = buildSubscriptionText($DefinitionsUpdateSubscriptions);
// / Report an unresolved path plainly rather than printing the word false to the console.
if ($configFilePath === FALSE) $configFilePath = 'Not found: '.$ConfigFile;
if ($defsFilePath === FALSE) $defsFilePath = 'Not found: '.$DefsFile;
// / The core file is the running script, so it is always present by definition.
if ($coreFilePath !== FALSE) {
$InformationBuilt = TRUE;
$originalRepo = 'https://github.com/zelon88/ScanCore';
$licenseText = 'GPLv3';
$verText1 = 'ScanCore '.$Version.' by Justin Grimes (@zelon88), licensed under '.$licenseText.'.'.$EOL;
$verText2 = 'The original source code for this application can be found at: '.$originalRepo.$EOL;
$verText3 = 'This installation is located at: '.$coreFilePath.$EOL;
$verText4 = 'This installation is using a definitions file located at: '.$defsFilePath.$EOL;
$verText5 = 'This installation is using a configuration file located at: '.$configFilePath.$EOL;
$verText6 = 'This installation downloads Application updates from: '.$ApplicationUpdateURL.$EOL;
$verText7 = 'This installation downloads Definition updates from: '.$DefinitionUpdateURL.$EOL;
$verText8 = 'Configuration file was last updated on: '.date("F d Y H:i:s.", intval(@filectime($ConfigFile))).$EOL;
$verText9 = 'Application update was last installed on: '.date("F d Y H:i:s.", intval(@filectime($coreFilePath))).$EOL;
$verText10 = 'Definition update was last installed on: '.date("F d Y H:i:s.", intval(@filectime($DefsFile))).$EOL;
$verText11 = 'This installation has the following Definition Subscriptions: '.$SubText;
// / The switch table is the one place a switch is described, so adding a switch
// / means adding a row rather than another numbered variable in a chain of forty.
$switchTable = array(
array('Show version information', '-version', '-ver'),
array('Show help information', '-help', '-h'),
array('Force recursion', '-recursion', '-r'),
array('Force no recursion', '-norecursion', '-nr'),
array('Follow symbolic links while scanning', '-followsymlinks', '-fs'),
array('Do not follow symbolic links', '-nofollowsymlinks', '-nfs'),
array('Specify memory limit (in bytes)', '-memorylimit ####', '-m ####'),
array('Specify chunk size (in bytes)', '-chunksize ####', '-c ####'),
array('Force maximum scan depth (in folders)', '-maxdepth ###', '-md ###'),
array('Enable "debug" mode (more logging)', '-debug', '-d'),
array('Enable "verbose" mode (more console)', '-verbose', '-v'),
array('Force a specific report file', '-reportfile /path/to/file', '-rf /path/to/file'),
array('Force a specific configuration file', '-configfile /path/to/file', '-cf /path/to/file'),
array('Force a specific definitions file', '-defsfile /path/to/file', '-df /path/to/file'),
array('Force maximum log size (in bytes)', '-maxlogsize ###', '-ml ###'),
array('Report low confidence matches', '-suspicious', '-sus'),
array('Report confirmed matches only', '-nosuspicious', '-nsus'),
array('Enable content classification', '-classify', '-cy'),
array('Disable content classification', '-noclassify', '-ncy'),
array('Choose which classifiers run', '-classifiers scad,pdf', '-cl scad,pdf'),
array('Inspect inside zip containers', '-inspectarchives', '-ia'),
array('Do not open zip containers', '-noinspectarchives', '-nia'),
array('Container entry count budget', '-maxarchiveentries ###', '-mae ###'),
array('Container entry size budget (bytes)', '-maxarchiveentrysize ###', '-maes ###'),
array('Container total size budget (bytes)', '-maxarchivetotalsize ###', '-mats ###'),
array('Container expansion ratio limit', '-maxarchiveratio ###', '-mar ###'),
array('Language signatures needed to decide', '-minlanguagesignatures ###', '-mls ###'),
array('Shortest usable data signature', '-mindatasignature ###', '-mds ###'),
array('Own created files as this user', '-owner www-data', '-ow www-data'),
array('Own created files as this group', '-group www-data', '-gr www-data'),
array('Leave created files owned by root', '-nopreserveownership', '-npo'),
array('Do not use a packed hash index', '-nohashindex', '-nhi'),
array('Rebuild the packed hash index', '-rebuildindex', '-ri'),
array('Hashes needed before indexing', '-hashindexthreshold ###', '-hit ###'),
array('Seconds to wait for an update host', '-connectiontimeout ###', '-ct ###'),
array('Choose the update method', '-updatemethod git', '-um raw'),
array('Perform definition update', '-updatedefinitions', '-ud'),
array('Perform application update', '-updateapplication', '-ua'));
$switchText = '';
foreach ($switchTable as $switchRow) {
$switchText = $switchText.$EOL.sprintf(' %-38s %s', $switchRow[0].':', $switchRow[1]).$EOL;
if ($switchRow[2] !== '') $switchText = $switchText.sprintf(' %-38s %s', '', $switchRow[2]).$EOL; }
$qsText1 = $EOL.'Quick Start Example:'.$EOL;
$qsText2 = $EOL.' C:\Path-To-PHP-Binary.exe C:\Path-To-ScanCore.php C:\Path-To-Scan\ -m [integer] -c [integer] -v -d'.$EOL;
$HelpText = $verText1.$qsText1.$qsText2.$EOL.'Reqiured Arguments Include:'.$EOL
.$EOL.' File or folder to scan: /path/to/scan'.$EOL
.$EOL.'Optional Arguments Include:'.$EOL.$switchText;
$VersionText = $verText1.$verText2.$verText3.$verText4.$verText5.$verText6.$verText7.$verText8.$verText9.$verText10.$verText11; }
// / Manually clean up sensitive memory. Helps to keep track of variable assignments.
purgeSensitiveMemory($coreFilePath, $configFilePath, $defsFilePath, $originalRepo, $licenseText);
purgeSensitiveMemory($switchTable, $switchRow, $switchText, $qsText1, $qsText2);
purgeSensitiveMemory($verText1, $verText2, $verText3, $verText4, $verText5, $verText6);
purgeSensitiveMemory($verText7, $verText8, $verText9, $verText10, $verText11);
return array($InformationBuilt, $SubText, $VersionText, $HelpText); }
// / -----------------------------------------------------------------------------------
// / -----------------------------------------------------------------------------------
// / A function to read the value that follows a command line switch.
// / Previous versions read the next element without checking that it existed.
// / Reading past the end of the argument list produced an undefined array key warning.
function readArgumentValue($argumentList, $switchKey, $switchName) {
// / Set variables.
$ValueFound = FALSE;
$ArgumentValue = '';
if (isset($argumentList[$switchKey + 1])) {
$ArgumentValue = sanitize($argumentList[$switchKey + 1]);
if ($ArgumentValue !== '') $ValueFound = TRUE; }
// / Warn rather than error, because the configured default remains perfectly usable.
if (!$ValueFound) processOutput('The '.$switchName.' argument was supplied without a value, using the configured value instead.', 'warning', 0, TRUE, FALSE, FALSE);
// / Manually clean up sensitive memory. Helps to keep track of variable assignments.
purgeSensitiveMemory($argumentList, $switchKey, $switchName);
return array($ValueFound, $ArgumentValue); }
// / -----------------------------------------------------------------------------------
// / -----------------------------------------------------------------------------------
// / A function to validate a numeric setting & clamp it into a workable range.
// / A chunk size of zero would make the file reader loop without ever advancing.
function validateNumericSetting($candidate, $defaultValue, $minimumValue, $settingName) {
// / Set variables.
$ValidatedSetting = intval($defaultValue);
$candidateIsUsable = FALSE;
if (is_numeric($candidate)) if (intval($candidate) >= $minimumValue) $candidateIsUsable = TRUE;
if ($candidateIsUsable) $ValidatedSetting = intval($candidate);
else processOutput('The '.$settingName.' value is missing or below the minimum of '.$minimumValue.', using '.intval($defaultValue).' instead.', 'warning', 0, TRUE, FALSE, FALSE);
// / Manually clean up sensitive memory. Helps to keep track of variable assignments.
purgeSensitiveMemory($candidateIsUsable, $candidate, $defaultValue, $minimumValue, $settingName);
return $ValidatedSetting; }
// / -----------------------------------------------------------------------------------
// / -----------------------------------------------------------------------------------
// / A function to parse supplied command-line arguments.
// / The -configfile & -cf arguments are processed by the loadConfig() function.
// / The -defsfile & -df arguments are processed by the loadConfig() function.
function parseArgs($argumentList) {
global $ArgsParsed, $ReportFile, $ReportDir, $MaxLogSize, $Debug, $Verbose, $ChunkSize, $MemoryLimit, $DefaultMemoryLimit, $DefaultChunkSize, $DefaultMaxLogSize, $PerformDefUpdate, $PerformAppUpdate, $VersionText, $HelpText, $ConfigFilePath, $PerformScan, $ScanLoc, $InstallDir, $MaxScanDepth, $FollowSymlinks, $RP;
global $ClassifyContent, $EnabledClassifiers, $MinimumLanguageSignatures, $MinimumDataSignatureLength, $InspectArchivedContent;
global $MaxArchiveEntries, $MaxArchiveEntrySize, $MaxArchiveTotalSize, $MaxArchiveCompressionRatio, $ConnectionTimeout, $UpdateMethod;
global $ReportSuspicious, $PreserveOwnership, $OwnerUser, $OwnerGroup, $UseHashIndex, $HashIndexThreshold, $RebuildHashIndex;
// / Set variables.
$ArgsParsed = $PerformScan = $Recursion = FALSE;
$RebuildHashIndex = FALSE;
$PerformDefUpdate = $PerformAppUpdate = $showVersion = $showHelp = FALSE;
$PathToScan = '';
$valueFound = FALSE;
$argumentValue = $currentSwitch = '';
$scanPathSupplied = FALSE;
// / Iterate the arguments once, recording every switch that was supplied.
foreach ($argumentList as $argKey => $argValue) {
redeclare($currentSwitch, normalizeSwitch($argValue));
if ($currentSwitch === '-version' or $currentSwitch === '-ver') $showVersion = TRUE;
if ($currentSwitch === '-h' or $currentSwitch === '-help') $showHelp = TRUE;
if ($currentSwitch === '-debug' or $currentSwitch === '-d') $Debug = TRUE;
if ($currentSwitch === '-verbose' or $currentSwitch === '-v') $Verbose = TRUE;
if ($currentSwitch === '-recursion' or $currentSwitch === '-r') $Recursion = TRUE;
if ($currentSwitch === '-norecursion' or $currentSwitch === '-nr') $Recursion = FALSE;
if ($currentSwitch === '-followsymlinks' or $currentSwitch === '-fs') $FollowSymlinks = TRUE;
if ($currentSwitch === '-nofollowsymlinks' or $currentSwitch === '-nfs') $FollowSymlinks = FALSE;
if ($currentSwitch === '-classify' or $currentSwitch === '-cy') $ClassifyContent = TRUE;
if ($currentSwitch === '-noclassify' or $currentSwitch === '-ncy') $ClassifyContent = FALSE;
if ($currentSwitch === '-inspectarchives' or $currentSwitch === '-ia') $InspectArchivedContent = TRUE;
if ($currentSwitch === '-noinspectarchives' or $currentSwitch === '-nia') $InspectArchivedContent = FALSE;
if ($currentSwitch === '-suspicious' or $currentSwitch === '-sus') $ReportSuspicious = TRUE;
if ($currentSwitch === '-nosuspicious' or $currentSwitch === '-nsus') $ReportSuspicious = FALSE;
if ($currentSwitch === '-nopreserveownership' or $currentSwitch === '-npo') $PreserveOwnership = FALSE;
if ($currentSwitch === '-nohashindex' or $currentSwitch === '-nhi') $UseHashIndex = FALSE;
if ($currentSwitch === '-rebuildindex' or $currentSwitch === '-ri') $RebuildHashIndex = TRUE;
if ($currentSwitch === '-hashindexthreshold' or $currentSwitch === '-hit') {
list($valueFound, $argumentValue) = readArgumentValue($argumentList, $argKey, $currentSwitch);
if ($valueFound) $HashIndexThreshold = $argumentValue; }
if ($currentSwitch === '-classifiers' or $currentSwitch === '-cl') {
list($valueFound, $argumentValue) = readArgumentValue($argumentList, $argKey, $currentSwitch);
// / An empty list is a real answer here, so a lone comma switches everything off.
if ($valueFound) $EnabledClassifiers = array_filter(array_map('trim', explode(',', $argumentValue)), 'strlen'); }
if ($currentSwitch === '-owner' or $currentSwitch === '-ow') {
list($valueFound, $argumentValue) = readArgumentValue($argumentList, $argKey, $currentSwitch);
if ($valueFound) $OwnerUser = $argumentValue; }
if ($currentSwitch === '-group' or $currentSwitch === '-gr') {
list($valueFound, $argumentValue) = readArgumentValue($argumentList, $argKey, $currentSwitch);
if ($valueFound) $OwnerGroup = $argumentValue; }
if ($currentSwitch === '-updatemethod' or $currentSwitch === '-um') {
list($valueFound, $argumentValue) = readArgumentValue($argumentList, $argKey, $currentSwitch);
if ($valueFound) $UpdateMethod = strtolower($argumentValue); }
if ($currentSwitch === '-minlanguagesignatures' or $currentSwitch === '-mls') {
list($valueFound, $argumentValue) = readArgumentValue($argumentList, $argKey, $currentSwitch);
if ($valueFound) $MinimumLanguageSignatures = $argumentValue; }
if ($currentSwitch === '-mindatasignature' or $currentSwitch === '-mds') {
list($valueFound, $argumentValue) = readArgumentValue($argumentList, $argKey, $currentSwitch);
if ($valueFound) $MinimumDataSignatureLength = $argumentValue; }
if ($currentSwitch === '-maxarchiveentries' or $currentSwitch === '-mae') {
list($valueFound, $argumentValue) = readArgumentValue($argumentList, $argKey, $currentSwitch);
if ($valueFound) $MaxArchiveEntries = $argumentValue; }
if ($currentSwitch === '-maxarchiveentrysize' or $currentSwitch === '-maes') {
list($valueFound, $argumentValue) = readArgumentValue($argumentList, $argKey, $currentSwitch);
if ($valueFound) $MaxArchiveEntrySize = $argumentValue; }
if ($currentSwitch === '-maxarchivetotalsize' or $currentSwitch === '-mats') {
list($valueFound, $argumentValue) = readArgumentValue($argumentList, $argKey, $currentSwitch);
if ($valueFound) $MaxArchiveTotalSize = $argumentValue; }
if ($currentSwitch === '-maxarchiveratio' or $currentSwitch === '-mar') {
list($valueFound, $argumentValue) = readArgumentValue($argumentList, $argKey, $currentSwitch);
if ($valueFound) $MaxArchiveCompressionRatio = $argumentValue; }
if ($currentSwitch === '-connectiontimeout' or $currentSwitch === '-ct') {
list($valueFound, $argumentValue) = readArgumentValue($argumentList, $argKey, $currentSwitch);
if ($valueFound) $ConnectionTimeout = $argumentValue; }
if ($currentSwitch === '-updatedefinitions' or $currentSwitch === '-ud') $PerformDefUpdate = TRUE;
if ($currentSwitch === '-updateapplication' or $currentSwitch === '-ua') $PerformAppUpdate = TRUE;
if ($currentSwitch === '-memorylimit' or $currentSwitch === '-m') {
list($valueFound, $argumentValue) = readArgumentValue($argumentList, $argKey, $currentSwitch);
if ($valueFound) $MemoryLimit = $argumentValue; }
if ($currentSwitch === '-chunksize' or $currentSwitch === '-c') {
list($valueFound, $argumentValue) = readArgumentValue($argumentList, $argKey, $currentSwitch);
if ($valueFound) $ChunkSize = $argumentValue; }
if ($currentSwitch === '-maxlogsize' or $currentSwitch === '-ml') {
list($valueFound, $argumentValue) = readArgumentValue($argumentList, $argKey, $currentSwitch);
if ($valueFound) $MaxLogSize = $argumentValue; }
if ($currentSwitch === '-maxdepth' or $currentSwitch === '-md') {
list($valueFound, $argumentValue) = readArgumentValue($argumentList, $argKey, $currentSwitch);
if ($valueFound) $MaxScanDepth = $argumentValue; }
if ($currentSwitch === '-reportfile' or $currentSwitch === '-rf' or $currentSwitch === '-logfile' or $currentSwitch === '-lf') {
list($valueFound, $argumentValue) = readArgumentValue($argumentList, $argKey, $currentSwitch);
if ($valueFound) $ReportFile = resolvePath($argumentValue, $ReportDir); } }
// / Validate every numeric setting & clamp it into a range the scanner can actually use.
$MemoryLimit = validateNumericSetting($MemoryLimit, $DefaultMemoryLimit, 65536, 'MemoryLimit');
$ChunkSize = validateNumericSetting($ChunkSize, $DefaultChunkSize, 4096, 'ChunkSize');
$MaxLogSize = validateNumericSetting($MaxLogSize, $DefaultMaxLogSize, 4096, 'MaxLogSize');
$MaxScanDepth = validateNumericSetting($MaxScanDepth, 64, 1, 'MaxScanDepth');
$HashIndexThreshold = validateNumericSetting($HashIndexThreshold, 10000, 1, 'HashIndexThreshold');
// / Validate every override the same way, so a bad argument falls back rather than breaks.
$MinimumLanguageSignatures = validateNumericSetting($MinimumLanguageSignatures, 2, 1, 'MinimumLanguageSignatures');
$MinimumDataSignatureLength = validateNumericSetting($MinimumDataSignatureLength, 4, 1, 'MinimumDataSignatureLength');
$MaxArchiveEntries = validateNumericSetting($MaxArchiveEntries, 512, 1, 'MaxArchiveEntries');
$MaxArchiveEntrySize = validateNumericSetting($MaxArchiveEntrySize, 1024*1024*8, 1024, 'MaxArchiveEntrySize');
$MaxArchiveTotalSize = validateNumericSetting($MaxArchiveTotalSize, 1024*1024*64, 1024, 'MaxArchiveTotalSize');
$MaxArchiveCompressionRatio = validateNumericSetting($MaxArchiveCompressionRatio, 200, 2, 'MaxArchiveCompressionRatio');
$ConnectionTimeout = validateNumericSetting($ConnectionTimeout, 15, 1, 'ConnectionTimeout');
// / Fall back to the git update method when an override names something else.
if ($UpdateMethod !== 'git' && $UpdateMethod !== 'raw') {
processOutput('The UpdateMethod override is not git or raw, using git instead.', 'warning', 0, TRUE, FALSE, FALSE);
$UpdateMethod = 'git'; }
// / A chunk larger than the memory limit would defeat the point of chunking at all.
if ($ChunkSize > $MemoryLimit) {
processOutput('The ChunkSize exceeds the MemoryLimit, reducing the ChunkSize to match.', 'warning', 0, TRUE, FALSE, FALSE);
$ChunkSize = $MemoryLimit; }
// / Detect if version or help information is being requested.
if ($showVersion or $showHelp) {
list ($InformationBuilt, $SubText, $VersionText, $HelpText) = buildHelpInformation();
if ($InformationBuilt) processOutput('Built version information.', 'log', 0, FALSE, FALSE, FALSE);
else processOutput('Cannot build version information!', 'warning', 0, TRUE, TRUE, FALSE);
if ($showVersion) processOutput($VersionText, 'log', 0, TRUE, TRUE, FALSE);
if ($showHelp) processOutput($HelpText, 'log', 0, TRUE, TRUE, FALSE);
$ArgsParsed = TRUE; }
// / Detect if an update is being requested.
if ($PerformDefUpdate or $PerformAppUpdate) {
processOutput('Starting ScanCore updater!', 'log', 0, TRUE, TRUE, FALSE);
$ArgsParsed = TRUE; }
// / Work out what should be scanned when no other operation was requested.
if (!$PerformDefUpdate && !$PerformAppUpdate && !$showVersion && !$showHelp) {
if (isset($argumentList[1])) {
$PathToScan = sanitize($argumentList[1]);
$scanPathSupplied = TRUE; }
// / Fall back to the configured scan location before reporting that nothing was supplied.
if (!$scanPathSupplied && is_string($ScanLoc) && $ScanLoc !== '') {
$PathToScan = resolvePath($ScanLoc, $InstallDir);
$scanPathSupplied = TRUE;
processOutput('No scan path was supplied, falling back to the configured ScanLoc.', 'warning', 0, TRUE, FALSE, FALSE); }
if (!$scanPathSupplied) processOutput('There were no arguments set!', 'error', 100, TRUE, TRUE, FALSE);
else {
// / Detect if a valid path to scan was supplied.
if (!file_exists($PathToScan)) processOutput('The specified file was not found! The first argument must be a valid file or directory path!', 'error', 300, TRUE, TRUE, FALSE);
else {
if (!is_readable($PathToScan)) processOutput('The specified path cannot be read: '.$PathToScan, 'error', 310, TRUE, TRUE, FALSE);
else {
// / Output status information.
processOutput('Starting ScanCore!', 'log', 0, TRUE, TRUE, FALSE);
processOutput('Loaded configuration file: '.$ConfigFilePath, 'log', 0, TRUE, FALSE, FALSE);
processOutput('The ChunkSize is: '.$ChunkSize, 'log', 0, FALSE, FALSE, FALSE);
processOutput('The MemoryLimit is: '.$MemoryLimit, 'log', 0, FALSE, FALSE, FALSE);
$ArgsParsed = $PerformScan = TRUE; } } } }
// / Manually clean up sensitive memory. Helps to keep track of variable assignments.
purgeSensitiveMemory($argKey, $argValue, $currentSwitch, $showVersion, $showHelp, $valueFound);
purgeSensitiveMemory($argumentValue, $scanPathSupplied, $InformationBuilt, $SubText, $argumentList);
return array($ArgsParsed, $PerformScan, $PathToScan, $MemoryLimit, $ChunkSize, $Debug, $Verbose, $Recursion, $ReportFile, $MaxLogSize, $PerformDefUpdate, $PerformAppUpdate); }
// / -----------------------------------------------------------------------------------
// / -----------------------------------------------------------------------------------
// / A function to confirm that a path is safe to delete before anything gets deleted.
// / The cleaner is only ever aimed at the temporary update directories.
// / A blank or misconfigured repository name must never point it somewhere else.
function cleanTargetIsSafe($location) {
global $InstallDir, $SEP;
// / Set variables.
$TargetIsSafe = FALSE;
$resolvedLocation = '';
$resolvedInstall = realpath($InstallDir);
if (is_string($location) && $location !== '') $resolvedLocation = realpath($location);
// / A path that does not exist is safe, because there is nothing there to remove.
if ($resolvedLocation === FALSE) $TargetIsSafe = TRUE;
if (is_string($resolvedLocation) && $resolvedLocation !== '' && $resolvedInstall !== FALSE) {
// / The target must sit underneath the installation directory & must not be it.
if (str_starts_with($resolvedLocation.$SEP, $resolvedInstall.$SEP)) if ($resolvedLocation !== $resolvedInstall) $TargetIsSafe = TRUE; }
// / Manually clean up sensitive memory. Helps to keep track of variable assignments.
purgeSensitiveMemory($resolvedLocation, $resolvedInstall, $location);
return $TargetIsSafe; }
// / -----------------------------------------------------------------------------------
// / -----------------------------------------------------------------------------------
// / A function to remove files & folders.
// / A symbolic link is removed as a link so the cleaner never walks outside its target.
function clean($Location) {
global $SEP;
// / Set variables.
$LocationCleaned = FALSE;
$entryName = '';
$entryList = array();
$scanResult = FALSE;
// / Refuse to touch anything outside the installation directory.
if (!cleanTargetIsSafe($Location)) processOutput('Refused to clean a path outside the installation directory: '.$Location, 'warning', 0, TRUE, FALSE, FALSE);
else {
// / Detect if the location is a folder that is not merely a link to one.
if (is_dir($Location) && !is_link($Location)) {
$scanResult = @scandir($Location);
// / Scan the folder for contents, tolerating a folder we are not allowed to read.
if (is_array($scanResult)) {
$entryList = array_diff($scanResult, array('..', '.'));
// / Iterate through the contents of the folder.
foreach ($entryList as $entryName) {
// / If this object is a folder, run this function on it.
if (is_dir($Location.$SEP.$entryName) && !is_link($Location.$SEP.$entryName)) clean($Location.$SEP.$entryName);
// / If this object is a file or a link, delete it.
else @unlink($Location.$SEP.$entryName); } }
// / Try to delete the folder now that we've deleted the contents.
if (is_dir($Location)) @rmdir($Location); }
// / If the location is a file or a link, delete it.
if (file_exists($Location) && !is_dir($Location)) @unlink($Location);
if (is_link($Location)) @unlink($Location);
// / Check if the location was deleted.
if (!is_dir($Location) && !file_exists($Location)) $LocationCleaned = TRUE; }
// / Manually clean up sensitive memory. Helps to keep track of variable assignments.
purgeSensitiveMemory($entryName, $entryList, $scanResult);
return array($LocationCleaned); }
// / -----------------------------------------------------------------------------------