Issues (35)

src/Service/Aggregator.php (1 issue)

Labels
Severity
1
<?php
2
3
namespace Xima\DepmonBundle\Service;
4
5
use Psr\Log\LoggerInterface;
6
use Symfony\Component\Cache\Adapter\AdapterInterface;
7
use Symfony\Component\Process\Exception\ProcessFailedException;
8
use Symfony\Component\Process\Process;
9
use Xima\DepmonBundle\Util\VersionHelper;
10
11
/**
12
 * Class Aggregator
13
 * @package Xima\DepmonBundle\Service
14
 */
15
class Aggregator
16
{
17
18
    /**
19
     * @var array
20
     * ToDo: Move them to the config
21
     * ToDo: Not really good coverage at all ...
22
     */
23
    private $projectTypes = [
24
        'typo3/cms' => 'TYPO3',
25
        'symfony/symfony' => 'Symfony',
26
        'drupal/drupal' => 'Drupal'
27
    ];
28
29
30
    /**
31
     * Get dependency data for a specific project by cloning the project composer files in the cache dir, installing
32
     * all dependencies (necessary for using composer show) and fetching the dependency information by "composer show"
33
     * ToDo: Check if git/composer is installed
34
     * ToDo: Optionally remove vendors after composer show was running
35
     *
36
     * @param array $project
37
     * @throws \Exception
38
     * @return array
39
     */
40
    public function fetchProjectData($project, LoggerInterface $logger): array
41
    {
42
        $projectName = $project['name'];
43
44
        // Check if git is installed
45
        if ($this->runProcess('command -v \'git\' || which \'git\' || type -p \'git\'') == '') {
46
            throw new \Exception('Git is not installed!');
47
        }
48
49
        // Check if composer is installed
50
        if ($this->runProcess('command -v \'composer\' || which \'composer\' || type -p \'composer\'') == '') {
51
            throw new \Exception('Composer is not installed!');
52
        }
53
54
        // If project already exists, just pull updates. Otherwise clone the repository.
55
        // ToDo: "git reset" pulls every file of the git
56
        if (is_dir('var/data/' . $project['name'])) {
57
            $this->runProcess('cd var/data/' . $project['name'] . ' && git reset --hard origin/master');
58
        } else {
59
            $logger->info('Clone project');
60
            $this->runProcess('git clone -n ' . $project['git'] . ' var/data/' . $project['name'] . ' --depth 1 -b master --single-branch');
61
        }
62
63
        // Check if composer.json exists in project
64
        $process = $this->runProcess(
65
            'cd var/data/' . $project['name'] . '/ && ' .
66
            'git cat-file -e origin/master:' . $project['path'] . 'composer.json && echo true'
67
        );
68
69
        if (trim($process) != 'true') {
70
            throw new \Exception('No composer.json found in the project ' . $projectName);
71
        }
72
73
        // Check if composer.lock exists in project
74
        $process = $this->runProcess(
75
            'cd var/data/' . $project['name'] . '/ && ' .
76
            'git cat-file -e origin/master:' . $project['path'] . 'composer.lock && echo true'
77
        );
78
        $composerLock = false;
79
80
        if (trim($process) == 'true') {
81
            $composerLock = true;
82
        }
83
84
        // Preparing composer project setup
85
        // ToDo: Is it possible to combine multiple processes in a better way?
86
        $this->runProcess(
87
            'cd var/data/' . $project['name'] . '/ && ' .
88
            // Checkout composer.json file
89
            'git checkout HEAD ' . $project['path'] . 'composer.json && ' .
90
            // Checkout composer.lock file
91
            (($composerLock) ? 'git checkout HEAD ' . $project['path'] . 'composer.lock && ' : '') .
92
            // Change directory
93
            (($project['path'] != '') ? 'cd ' . $project['path'] . ' && ' : '') .
94
            // Install composer dependencies
95
            'composer install --no-dev --no-autoloader --no-scripts --ignore-platform-reqs --prefer-dist'
96
        );
97
98
        $gitTag = $this->runProcess(
99
            'cd var/data/' . $project['name'] . ' && ' .
100
            'git describe --tags $(git rev-list --tags --max-count=1)'
101
        );
102
103
        $data = json_decode($this->runProcess(
104
            'cd var/data/' . $project['name'] . '/' . $project['path'] . ' && ' .
105
            'composer show --latest --minor-only --format json'
106
        ));
107
108
        if (empty($data)) {
109
            throw new \Exception('Empty result of "composer show" for project ' . $projectName);
110
        }
111
112
        $vulnerabilities = json_decode($this->runProcess(
113
            'curl -H "Accept: application/json" https://security.sensiolabs.org/check_lock -F lock=@var/data/' . $project['name'] . '/' . $project['path'] . 'composer.lock'
114
        ));
115
116
        //
117
        // Evaluate data
118
        //
119
120
        $result = [];
121
122
        // Saving composer information about project to result
123
        $result['composer'] = json_decode(file_get_contents('var/data/' . $project['name'] . '/' . $project['path'] . 'composer.json'));
124
        $result['self'] = $project;
125
126
        $requiredPackagesCount = 0;
127
        $statesCount = [
128
            VersionHelper::STATE_UP_TO_DATE => 0,
129
            VersionHelper::STATE_PINNED_OUT_OF_DATE => 0,
130
            VersionHelper::STATE_OUT_OF_DATE => 0,
131
            VersionHelper::STATE_INSECURE => 0
132
        ];
133
        $requiredStatesCount = [
134
            VersionHelper::STATE_UP_TO_DATE => 0,
135
            VersionHelper::STATE_PINNED_OUT_OF_DATE => 0,
136
            VersionHelper::STATE_OUT_OF_DATE => 0,
137
            VersionHelper::STATE_INSECURE => 0
138
        ];
139
        $projectState = VersionHelper::STATE_UP_TO_DATE;
140
141
        foreach ($data->installed as $dependency) {
142
            // Workaround for adding requirement information to dependencies
143
            foreach ($result['composer']->require as $name => $require) {
144
                if ($dependency->name == $name) {
145
                    $dependency->required = $require;
146
                    $requiredPackagesCount++;
147
                }
148
149
                // Which project type?
150
                if (array_key_exists($name, $this->projectTypes)) {
151
                    $result['self']['projectType'] = $this->projectTypes[$name];
152
                }
153
            }
154
155
            // Is dependency outdated?
156
            if (isset($dependency->latest)) {
157
                $require = isset($dependency->required) ? $dependency->required : null;
158
                $state = VersionHelper::compareVersions($dependency->version, $dependency->latest, $require);
159
                $statesCount[$state]++;
160
                if ($require) {
161
                    $requiredStatesCount[$state]++;
162
                }
163
                $dependency->state = $state;
164
            } else {
165
                $dependency->state = VersionHelper::STATE_UP_TO_DATE;
166
            }
167
168
            // Is dependency a security issue?
169
            foreach ($vulnerabilities as $name => $vulnerability) {
170
                if ($name == $dependency->name) {
171
                    $dependency->state = VersionHelper::STATE_INSECURE;
172
                    $array = [];
173
                    foreach ($vulnerability->advisories as $advisory) {
174
                        array_push($array, $advisory);
175
                    }
176
                    $dependency->vulnerability = $array;
177
                    if (isset($dependency->required)) {
178
                        $requiredStatesCount[VersionHelper::STATE_INSECURE]++;
179
                    } else {
180
                        $statesCount[VersionHelper::STATE_INSECURE]++;
181
                    }
182
                }
183
            }
184
185
            $result['dependencies'][] = $dependency;
186
        }
187
188
        if ($requiredStatesCount[4] > 0) {
189
            $projectState = VersionHelper::STATE_INSECURE;
190
        } elseif ($requiredStatesCount[3] > 2) {
191
            $projectState = VersionHelper::STATE_OUT_OF_DATE;
192
        } elseif ($requiredStatesCount[3] <= 2 && $requiredStatesCount[2] >= 1) {
193
            $projectState = VersionHelper::STATE_PINNED_OUT_OF_DATE;
194
        }
195
196
        $metadata = [
197
            'requiredPackagesCount' => $requiredPackagesCount,
198
            'statesCount' => $statesCount,
199
            'requiredStatesCount' => $requiredStatesCount,
200
            'projectState' => $projectState,
201
            'gitTag' => $gitTag
202
        ];
203
204
        $result['meta'] = $metadata;
205
206
        return $result;
207
    }
208
209
    /**
210
     * @param $project
211
     */
212
    public function clearProjectData($project)
213
    {
214
        $process = new Process(
215
            'rm -rf var/data/' . $project['name']
0 ignored issues
show
'rm -rf var/data/' . $project['name'] of type string is incompatible with the type array expected by parameter $command of Symfony\Component\Process\Process::__construct(). ( Ignorable by Annotation )

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

215
            /** @scrutinizer ignore-type */ 'rm -rf var/data/' . $project['name']
Loading history...
216
        );
217
        $process->run();
218
    }
219
220
    /**
221
     * @param $p
222
     * @return string
223
     */
224
    private function runProcess($p) {
225
        $process = Process::fromShellCommandline($p);
226
227
        $process->setTimeout(3600);
228
        $process->run();
229
230
        if (!$process->isSuccessful()) {
231
            throw new ProcessFailedException($process);
232
        }
233
        return $process->getOutput();
234
    }
235
}