Test Failed
Push — master ( e3c39f...fe570d )
by Mihail
07:20
created

Apps/Model/Basic/Antivirus.php (1 issue)

Labels
Severity
1
<?php
2
3
namespace Apps\Model\Basic;
4
5
use Ffcms\Core\Exception\NativeException;
6
use Ffcms\Core\Exception\SyntaxException;
7
use Ffcms\Core\Helper\FileSystem\File;
8
9
class Antivirus
10
{
11
    const EXECUTE_LIMIT = 5; // in seconds
12
    const DEPRECATED_TIME = 3600; // actuality of scan list
13
    // file extensions to be scanned
14
    public $affectedExt = [
15
        '.php', '.php3', '.phtml',
16
        '.htm', '.html',
17
        '.txt', '.js', '.pl', '.cgi', '.py', '.bash', '.sh', '.ssi', '.inc', '.pm', '.tpl'
18
    ];
19
20
    private $signatures;
21
    private $beginTime;
22
    private $scanFiles;
23
24
    private $infected;
25
26
    public function __construct()
27
    {
28
        if (!File::exist('/Private/Antivirus/Signatures.xml')) {
29
            throw new NativeException('Antivirus signatures is not founded in: {root}/Private/Antivirus/Signatures.xml');
30
        }
31
32
        $this->beginTime = time();
33
34
        $this->signatures = new \DOMDocument();
35
        $this->signatures->load(root . '/Private/Antivirus/Signatures.xml');
36
37
        // list of files is not prepared, 1st iteration
38
        if (!File::exist('/Private/Antivirus/ScanFiles.json')) {
39
            $this->prepareScanlist();
40
        }
41
42
        if (!File::exist('/Private/Antivirus/ScanFiles.json')) {
43
            throw new SyntaxException('Directory /Private/Antivirus/ can not be writed!');
44
        }
45
46
        $this->scanFiles = json_decode(File::read('/Private/Antivirus/ScanFiles.json'));
0 ignored issues
show
It seems like Ffcms\Core\Helper\FileSy...ivirus/ScanFiles.json') can also be of type false; however, parameter $json of json_decode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

46
        $this->scanFiles = json_decode(/** @scrutinizer ignore-type */ File::read('/Private/Antivirus/ScanFiles.json'));
Loading history...
47
        if (File::exist('/Private/Antivirus/Infected.json')) {
48
            $this->infected = (array)json_decode(File::read('/Private/Antivirus/Infected.json'));
49
        }
50
    }
51
52
    /**
53
     * Make scan of files with preparation and time limit
54
     * @return array
55
     */
56
    public function make()
57
    {
58
        foreach ($this->scanFiles as $idx => $file) {
59
            $now = time();
60
            // calculate time limit for scan per file
61
            if ($now - $this->beginTime >= static::EXECUTE_LIMIT) {
62
                break;
63
            }
64
65
            $this->scanContent($file);
66
            unset($this->scanFiles->$idx);
67
        }
68
69
        // write infected info
70
        File::write('/Private/Antivirus/Infected.json', json_encode($this->infected));
71
        // refresh file list to scan ;)
72
        File::write('/Private/Antivirus/ScanFiles.json', json_encode($this->scanFiles));
73
74
        return [
75
            'left' => count(get_object_vars($this->scanFiles)),
76
            'detect' => count($this->infected)
77
        ];
78
    }
79
80
    /**
81
     * Scan signle file via defined $path
82
     * @param string $path
83
     * @return bool
84
     */
85
    private function scanContent($path)
86
    {
87
        // get file content plain
88
        $content = File::read($path);
89
90
        // nothing to check
91
        if ($content === null || $content === false) {
92
            return false;
93
        }
94
95
        $normalized = $this->normalizeContent($content);
96
97
        // list malware signatures
98
        $db = $this->signatures->getElementsByTagName('signature');
99
        $detected = false;
100
        foreach ($db as $sig) {
101
            $sigContent = $sig->nodeValue;
102
            $attr = $sig->attributes;
103
            $attrId = $attr->getNamedItem('id')->nodeValue;
104
            $attrFormat = $attr->getNamedItem('format')->nodeValue;
105
            $attrTitle = $attr->getNamedItem('title')->nodeValue;
106
            $attrSever = $attr->getNamedItem('sever')->nodeValue;
107
108
            switch ($attrFormat) {
109
                case 're':
110
                    if ((preg_match('#(' . $sigContent . ')#smi', $content, $found, PREG_OFFSET_CAPTURE)) ||
111
                        (preg_match('#(' . $sigContent . ')#smi', $normalized, $found, PREG_OFFSET_CAPTURE))
112
                    ) {
113
                        $detected = true;
114
                        $pos = $found[0][1];
115
                        $this->infected[$path][] = [
116
                            'pos' => (int)$pos,
117
                            'sigId' => $attrId,
118
                            'sigRule' => $sigContent,
119
                            'sever' => $attrSever,
120
                            'title' => $attrTitle
121
                        ];
122
                    }
123
124
                    break;
125
                case 'const':
126
                    if ((($pos = strpos($content, $sigContent)) !== false) ||
127
                        (($pos = strpos($normalized, $sigContent)) !== false)
128
                    ) {
129
                        $this->infected[$path][] = [
130
                            'pos' => (int)$pos,
131
                            'sigId' => $attrId,
132
                            'sigRule' => $sigContent,
133
                            'sever' => $attrSever,
134
                            'title' => $attrTitle
135
                        ];
136
                        $detected = true;
137
                    }
138
139
                    break;
140
            }
141
        }
142
        return $detected;
143
    }
144
145
    /**
146
     * Prepare scan list on first run. Scan directory's and save as JSON
147
     */
148
    private function prepareScanlist()
149
    {
150
        $files = (object)File::listFiles(root, $this->affectedExt);
151
        File::write('/Private/Antivirus/ScanFiles.json', json_encode($files));
152
    }
153
154
    /**
155
     * Prepare content, replacing any encoding of it, based on Hex, OctDec or other offset's
156
     * @param string $content
157
     * @return mixed
158
     */
159
    private function normalizeContent($content)
160
    {
161
        $content = @preg_replace_callback('/\\\\x([a-fA-F0-9]{1,2})/i', 'escapedHexToHex', $content); // strip hex ascii notation
162
        $content = @preg_replace_callback('/\\\\([0-9]{1,3})/i', 'escapedOctDec', $content); // strip dec ascii notation
163
        $content = preg_replace('/[\'"]\s*?\.\s*?[\'"]/smi', '', $content); // concat fragmented strings
164
        $content = preg_replace('|/\*.*?\*/|smi', '', $content); // remove comments to detect fragmented pieces of malware
165
166
        return $content;
167
    }
168
}
169