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
Bug
introduced
by
![]() |
|||
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 | } |