ScanCommand::scan()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 17
rs 9.4285
cc 3
eloc 10
nc 4
nop 0
1
<?php
2
namespace PhpVirusScanner\Command;
3
4
use Symfony\Component\Console\Input\InputArgument;
5
use Symfony\Component\Console\Input\InputInterface;
6
use Symfony\Component\Console\Input\InputOption;
7
use Symfony\Component\Console\Output\OutputInterface;
8
use Symfony\Component\Finder\Finder;
9
use Symfony\Component\Finder\SplFileInfo;
10
use PhpVirusScanner\Helper\Table;
11
use Symfony\Component\Console\Helper\TableStyle;
12
13
/**
14
 *
15
 */
16
class ScanCommand extends AbstractCommand
17
{
18
    /**
19
     * @var array
20
     */
21
    protected $results = [];
22
23
    /**
24
     * @var string
25
     */
26
    protected $dir;
27
28
    /**
29
     * @var string
30
     */
31
    protected $signature;
32
33
    /**
34
     * @var boolean
35
     */
36
    protected $doDelete;
37
38
    /**
39
     *
40
     */
41
    protected function configure()
42
    {
43
        $this->setName('scan');
44
        $this->setDescription('scan directory for infected files');
45
46
        $this->configureArguments();
47
        $this->configureOptions();
48
    }
49
50
    /**
51
     *
52
     */
53
    protected function configureArguments()
54
    {
55
        $this->addArgument(
56
            'dir',
57
            InputArgument::REQUIRED,
58
            'Directory to scan'
59
        );
60
61
        $this->addArgument(
62
            'signature',
63
            InputArgument::REQUIRED,
64
            'Signature to search for'
65
        );
66
    }
67
68
    /**
69
     *
70
     */
71
    protected function configureOptions()
72
    {
73
        $this->addOption(
74
            'delete',
75
            null,
76
            InputOption::VALUE_NONE,
77
            'If set, command will delete all infected files'
78
        );
79
80
        $this->addOption(
81
            'show-full-paths',
82
            null,
83
            InputOption::VALUE_NONE,
84
            'If set, full file paths will be displayed'
85
        );
86
87
        $this->addOption(
88
            'size',
89
            null,
90
            InputOption::VALUE_REQUIRED,
91
            'If set, only files with specified size will be examined'
92
        );
93
    }
94
95
    /**
96
     * @param InputInterface  $input
97
     * @param OutputInterface $output
98
     *
99
     * @throws \Exception
100
     */
101
    protected function execute(InputInterface $input, OutputInterface $output)
102
    {
103
        set_time_limit(0);
104
105
        $this->input = $input;
106
        $this->output = $output;
107
108
        $this->dir = $this->getDir();
109
        $this->signature = $this->getSignature();
110
        $this->doDelete = (bool) $this->input->getOption('delete');
111
112
        $this->initResults();
113
114
        $this->scan();
115
116
        $this->deleteInfectedFiles();
117
118
        $this->outputInfectedFiles();
119
        $this->outputScanStats();
120
    }
121
122
    /**
123
     * @return mixed
124
     * @throws \Exception
125
     */
126
    protected function getDir()
127
    {
128
        $dir = $this->input->getArgument('dir');
129
        if (!is_dir($dir) || !is_readable($dir)) {
130
            throw new \Exception('Specified directory not exists or is not readable.');
131
        }
132
133
        return $dir;
134
    }
135
136
    /**
137
     * @return mixed
138
     * @throws \Exception
139
     */
140
    protected function getSignature()
141
    {
142
        $signature = $this->input->getArgument('signature');
143
        if (!$signature) {
144
            throw new \Exception('Invalid signature.');
145
        }
146
147
        return $signature;
148
    }
149
150
    /**
151
     *
152
     */
153
    protected function initResults()
154
    {
155
        $this->results = [
156
            'analyzed' => 0,
157
            'unreadable' => 0,
158
            'infected' => 0,
159
            'deleted' => 0,
160
            'deleteErrors' => 0,
161
            'files' => []
162
        ];
163
    }
164
165
    /**
166
     *
167
     */
168
    protected function deleteInfectedFiles()
169
    {
170
        if (!$this->doDelete) {
171
            return;
172
        }
173
174
        foreach ($this->results['files'] as $file) {
175
            if (@unlink($file['path'])) {
176
                $this->results['deleted']++;
177
            } else {
178
                $this->results['deleteErrors']++;
179
            }
180
        }
181
    }
182
183
    /**
184
     *
185
     */
186
    protected function outputInfectedFiles()
187
    {
188
        if ($this->results['infected'] == 0) {
189
            return;
190
        }
191
192
        $showFullPaths = (bool) $this->input->getOption('show-full-paths');
193
194
        $dirStrLength = strlen($this->dir);
195
196
        $table = $this->getTable();
197
        foreach ($this->results['files'] as $index => $file) {
198
            $filePath = $file['path'];
199
            if (!$showFullPaths) {
200
                $filePath = substr($filePath, $dirStrLength);
201
            }
202
            $table->addRow([$index + 1, $filePath, number_format($file['size'], 0, '.', ' ')]);
203
        }
204
        $table->render();
205
    }
206
207
    /**
208
     *
209
     */
210
    protected function outputScanStats()
211
    {
212
        if ($this->results['infected'] > 0) {
213
            $this->output->writeln('Total infected files: ' . $this->results['infected']);
214
215
            if ($this->doDelete) {
216
                $this->output->writeln('Deleted files: ' . $this->results['deleted']);
217
                $this->output->writeln('Failed to delete: ' . $this->results['deleteErrors']);
218
            }
219
        } else {
220
            $this->output->writeln('Nothing found!');
221
        }
222
223
        if ($this->results['unreadable'] > 0) {
224
            $this->output->writeln('Non-readable files: ' . $this->results['unreadable']);
225
        }
226
        $this->output->writeln('Total analyzed files: ' . $this->results['analyzed']);
227
228
        $this->printProfilerOutput();
229
    }
230
231
    /**
232
     * @return Table
233
     */
234
    protected function getTable()
235
    {
236
        $table = new Table($this->output);
237
        $table->setHeaders(['#', 'Path', 'Size']);
238
239
        $style = new TableStyle();
240
        $style->setPadType(STR_PAD_LEFT);
241
242
        $table->setColumnStyle(2, $style);
243
244
        return $table;
245
    }
246
247
    /**
248
     *
249
     */
250
    protected function scan()
251
    {
252
        $this->output->writeln("Target signature: {$this->signature}");
253
        $this->output->writeln("Scanning dir {$this->dir}...");
254
255
        $targetSize = (int) $this->input->getOption('size');
256
257
        $finder = new Finder();
258
        $finder->files()->followLinks()->in($this->dir)->name('*.php');
259
        if ($targetSize) {
260
            $finder->size('==' . $targetSize);
261
        }
262
263
        foreach ($finder as $file) {
264
            $this->processFile($file);
265
        }
266
    }
267
268
    /**
269
     * @param SplFileInfo $file
270
     */
271
    protected function processFile($file)
272
    {
273
        $this->results['analyzed']++;
274
275
        if (!$file->isReadable()) {
276
            $this->results['unreadable']++;
277
            return;
278
        }
279
280
        if (!$this->isInfected($file)) {
281
            return;
282
        }
283
284
        $this->results['infected']++;
285
        $this->results['files'][] = [
286
            'path' => $file->getRealPath(),
287
            'size' => $file->getSize()
288
        ];
289
    }
290
291
    /**
292
     * @param SplFileInfo $file
293
     *
294
     * @return bool
295
     */
296
    protected function isInfected(SplFileInfo $file)
297
    {
298
        if (!$file->isReadable()) {
299
            return true;
300
        }
301
302
        $content = $file->getContents();
303
        if (!$content) {
304
            return false;
305
        }
306
307
        $contains = strpos($content, $this->signature) !== false;
308
        return $contains;
309
    }
310
}
311