Issues (45)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/Rule/GitRule.php (5 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/*
3
 * This file is part of project-quality-inspector.
4
 *
5
 * (c) Alexandre GESLIN <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace ProjectQualityInspector\Rule;
12
13
use ProjectQualityInspector\Application\ProcessHelper;
14
use ProjectQualityInspector\Exception\ExpectationFailedException;
15
16
/**
17
 * Class GitRule
18
 *
19
 * @package ProjectQualityInspector\Rule
20
 */
21
class GitRule extends AbstractRule
22
{
23
    private $commitFormat = '%H|%ci|%cr|%an';
24
    private $commitFormatKeys = ['commitHash', 'committerDate', 'relativeCommitterDate', 'authorName', 'branchName'];
25
26
    public function __construct(array $config, $baseDir)
27
    {
28
        parent::__construct($config, $baseDir);
29
    }
30
31
    /**
32
     * @inheritdoc
33
     */
34
    public function evaluate()
35
    {
36
        $expectationsFailedExceptions = [];
37
        $stableBranches = $this->getStableBranches($this->config['remote-branches']);
38
39
        try {
40
            $this->expectsNoMergedBranches($stableBranches, $this->config['threshold-too-many-merged-branches'], $this->config['remote-branches']);
0 ignored issues
show
It seems like $stableBranches defined by $this->getStableBranches...fig['remote-branches']) on line 37 can also be of type null; however, ProjectQualityInspector\...pectsNoMergedBranches() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
41
            $this->addAssertion('expectsNoMergedBranches');
42
        } catch (ExpectationFailedException $e) {
43
            $expectationsFailedExceptions[] = $e;
44
            $this->addAssertion('expectsNoMergedBranches', [['message' => $e->getMessage() . $e->getReason(), 'type' => 'expectsNoMergedBranches']]);
45
        }
46
47
        $notMergedBranchesInfo = $this->listMergedOrNotMergedBranches($stableBranches, false, $this->config['remote-branches']);
0 ignored issues
show
It seems like $stableBranches defined by $this->getStableBranches...fig['remote-branches']) on line 37 can also be of type null; however, ProjectQualityInspector\...edOrNotMergedBranches() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
48
49
        foreach ($notMergedBranchesInfo as $notMergedBranchInfo) {
50
            try {
51
                $this->expectsBranchNotTooBehind($notMergedBranchInfo, $stableBranches);
0 ignored issues
show
It seems like $stableBranches defined by $this->getStableBranches...fig['remote-branches']) on line 37 can also be of type null; however, ProjectQualityInspector\...ctsBranchNotTooBehind() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
52
                $this->addAssertion($notMergedBranchInfo['branchName']);
53
            } catch (ExpectationFailedException $e) {
54
                $expectationsFailedExceptions[] = $e;
55
                $this->addAssertion($notMergedBranchInfo['branchName'], [['message' => $e->getMessage() . $e->getReason(), 'type' => 'expectsBranchNotTooBehind']]);
56
            }
57
        }
58
59
        if (count($expectationsFailedExceptions)) {
60
            $this->throwRuleViolationException($expectationsFailedExceptions);
61
        }
62
    }
63
64
    /**
65
     * @param array $stableBranches
66
     * @param int $threshold
67
     * @param bool $remoteBranches
68
     *
69
     * @throws ExpectationFailedException
70
     */
71
    private function expectsNoMergedBranches(array $stableBranches, $threshold, $remoteBranches = true)
72
    {
73
        if ($mergedBranches = $this->listMergedOrNotMergedBranches($stableBranches, true, $remoteBranches)) {
74
            if (count($mergedBranches) >= $threshold) {
75
                $message = sprintf('there is too much remaining merged branches (%s) : %s', count($mergedBranches), $this->stringifyMergedBranches($mergedBranches));
76
                throw new ExpectationFailedException($mergedBranches, $message);
77
            }
78
        }
79
    }
80
81
    /**
82
     * Compare stable branch first commit after common ancestor of not merged branch and stable branch, and expects days delay do not override limit
83
     *
84
     * @param array $notMergedBranchInfo
85
     * @param array $stableBranches
86
     *
87
     * @throws ExpectationFailedException
88
     */
89
    private function expectsBranchNotTooBehind(array $notMergedBranchInfo, array $stableBranches)
90
    {
91
        foreach ($stableBranches as $stableBranch) {
92
            $failed = false;
93
            $lrAheadCommitsCount = $this->getLeftRightAheadCommitsCountAfterMergeBase($stableBranch, $notMergedBranchInfo['branchName']);
94
95
            if ($lrAheadCommitsCount[$stableBranch] > 0) {
96
                $commonAncestorCommitInfo = $this->getMergeBaseCommit($notMergedBranchInfo['branchName'], $stableBranch);
97
                $stableBranchLastCommitInfo = $this->getBranchLastCommitInfo($stableBranch);
98
99
                if ($lrAheadCommitsCount[$stableBranch] >= (int)$this->config['threshold-commits-behind']) {
100
                    $failed = true;
101
                }
102
103
                $interval = $this->compareCommitInfosDatesDiff($commonAncestorCommitInfo, $stableBranchLastCommitInfo);
104
                if ((int)$interval->format('%r%a') >= (int)$this->config['threshold-days-behind']) {
105
                    $failed = true;
106
                }
107
108
                if ($failed) {
109
                    $message = sprintf('The branch <fg=green>%s</> is behind <fg=green>%s</> by %s commits spread through %s days.', $notMergedBranchInfo['branchName'], $stableBranch, $lrAheadCommitsCount[$stableBranch], (int)$interval->format('%r%a'));
110
                    $message .= sprintf(' <fg=green>%s</> should update the branch %s', $notMergedBranchInfo['authorName'], $notMergedBranchInfo['branchName']);
111
                    throw new ExpectationFailedException($notMergedBranchInfo, $message);
112
                }
113
            }
114
        }
115
    }
116
117
    /**
118
     * Get merged/not merged branches compared to $stablesBranches.
119
     * If there is multiple stable branches, and $merged = true : list branches that are merged in at least one of the stable branches
120
     * If there is multiple stable branches, and $merged = false : list branches that are not merged for any of the stable branches
121
     *
122
     * @param array $stableBranches
123
     * @param bool $merged
124
     * @param bool $remoteBranches
125
     * @return array
126
     */
127
    private function listMergedOrNotMergedBranches(array $stableBranches, $merged = true, $remoteBranches = true)
128
    {
129
        $branches = [];
130
        $mergedOption = ($merged) ? '--merged' : '--no-merged';
131
        $refsBase = ($remoteBranches) ? 'refs/remotes' : 'refs/heads';
132
133
        foreach ($stableBranches as $stableBranch) {
134
            $result = ProcessHelper::execute(sprintf('for branch in `git for-each-ref %s %s --shell --format=\'%%(refname)\' %s | tr -d \\\' | grep -ve "/HEAD" | grep -ve "%s" | grep -ve "%s"`; do echo `git show --format="%s" $branch | head -n 1`\|$branch; done | sort -r', $mergedOption, $stableBranch, $refsBase, $this->getBranchesRegex('stable-branches-regex'), $this->getBranchesRegex('ignored-branches-regex'), $this->commitFormat), $this->baseDir);
135
136
            $branches[$stableBranch] = $this->explodeCommitsArrays($result);
0 ignored issues
show
It seems like $result defined by \ProjectQualityInspector...ormat), $this->baseDir) on line 134 can also be of type null; however, ProjectQualityInspector\...:explodeCommitsArrays() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
137
            if ($merged) {
138
                foreach ($branches[$stableBranch] as $branchHash => $mergedBranchCommit) {
139
                    $branches[$stableBranch][$branchHash]['mergeCommit'] = $this->getMergeCommitInfo($stableBranch, $mergedBranchCommit['branchName']);
140
                }
141
            }
142
        }
143
144
        if (count($branches) > 1) {
145
            $branches = ($merged) ? call_user_func_array('array_merge', $branches) : call_user_func_array('array_intersect_key', $branches);
146
        } else {
147
            $branches = $branches[$stableBranches[0]];
148
        }
149
150
        return $branches;
151
    }
152
153
    /**
154
     * Get numbers of commits after common ancestor for $branchLeft and $branchRight (ex: $branchLeft => 2,  $branchRight => 5)
155
     *
156
     * @param $branchLeft
157
     * @param $branchRight
158
     * @return array
159
     */
160
    private function getLeftRightAheadCommitsCountAfterMergeBase($branchLeft, $branchRight)
161
    {
162
        $result = ProcessHelper::execute(sprintf('git rev-list --left-right --count %s...%s', $branchLeft, $branchRight), $this->baseDir);
163
        $result = explode("\t", $result[0]);
164
165
        $result = [
166
            $branchLeft => $result[0],
167
            $branchRight => $result[1]
168
        ];
169
170
        return $result;
171
    }
172
173
    /**
174
     * Get common ancestor commit
175
     *
176
     * @param string $branchLeft
177
     * @param string $branchRight
178
     * @return array
179
     */
180
    private function getMergeBaseCommit($branchLeft, $branchRight)
181
    {
182
        $commitInfo = null;
183
        $result = ProcessHelper::execute(sprintf('git merge-base %s %s', $branchLeft, $branchRight), $this->baseDir);
184
185
        if (count($result)) {
186
            $commitInfo = $this->getBranchLastCommitInfo($result[0]);
187
        }
188
189
        return $commitInfo;
190
    }
191
192
    /**
193
     * Get first commit of $branch after common ancestor commit of $baseBranch and $branch
194
     *
195
     * @param string $baseBranch
196
     * @param string $branch
197
     * @return array
198
     */
199
    private function getBranchFirstCommitInfo($baseBranch, $branch)
0 ignored issues
show
This method is not used, and could be removed.
Loading history...
200
    {
201
        $branchInfo = null;
202
        $result = ProcessHelper::execute(sprintf('git log --format="%s" %s..%s | tail -1', $this->commitFormat, $baseBranch, $branch), $this->baseDir);
203
        if (count($result)) {
204
            $explodedCommit = explode('|', $result[0]);
205
            $branchInfo = array_combine(array_slice($this->commitFormatKeys, 0, count($explodedCommit)), $explodedCommit);
206
            $branchInfo['branchName'] = $branch;
207
        }
208
209
        return $branchInfo;
210
    }
211
212
    /**
213
     * @param $branchInfoLeft
214
     * @param $branchInfoRight
215
     * @return bool|\DateInterval
216
     */
217
    private function compareCommitInfosDatesDiff($branchInfoLeft, $branchInfoRight)
218
    {
219
        $format = 'Y-m-d H:i:s O';
220
        $dateLeft = \DateTime::createFromFormat($format, $branchInfoLeft['committerDate']);
221
        $dateRight = \DateTime::createFromFormat($format, $branchInfoRight['committerDate']);
222
223
        return $dateLeft->diff($dateRight);
224
    }
225
226
    /**
227
     * @param $branch
228
     * @return array
229
     */
230
    private function getBranchLastCommitInfo($branch)
231
    {
232
        $result = ProcessHelper::execute(sprintf('git show --format="%s" %s | head -n 1', $this->commitFormat, $branch), $this->baseDir);
233
        $explodedCommit = explode('|', $result[0]);
234
        $branchInfo = array_combine(array_slice($this->commitFormatKeys, 0, count($explodedCommit)), $explodedCommit);
235
        $branchInfo['branchName'] = $branch;
236
237
        return $branchInfo;
238
    }
239
240
    /**
241
     * @param $baseBranch
242
     * @param $mergedBranch
243
     * @return array
244
     */
245
    private function getMergeCommitInfo($baseBranch, $mergedBranch)
246
    {
247
        $branchInfo = [
248
            'authorName' => 'fast-forward'
249
        ];
250
251
        if ($result = ProcessHelper::execute(sprintf('git show --format="%s" %s ^%s --ancestry-path | head -n 1', $this->commitFormat, $baseBranch, $mergedBranch), $this->baseDir)) {
252
            $explodedCommit = explode('|', $result[0]);
253
            $branchInfo = array_combine(array_slice($this->commitFormatKeys, 0, count($explodedCommit)), $explodedCommit);
254
        }
255
        $branchInfo['branchName'] = $baseBranch;
256
257
        return $branchInfo;
258
    }
259
260
    /**
261
     * @param bool $remoteBranches
262
     * @return array
263
     */
264
    private function getStableBranches($remoteBranches = true)
265
    {
266
        $refsBase = ($remoteBranches) ? 'refs/remotes' : 'refs/heads';
267
268
        $result = ProcessHelper::execute(sprintf('git for-each-ref --shell --format=\'%%(refname)\' %s | tr -d \\\' | grep -e "%s"', $refsBase, $this->getBranchesRegex('stable-branches-regex')), $this->baseDir);
269
270
        return $result;
271
    }
272
273
    /**
274
     * @param string $configKey
275
     * @return string
276
     */
277
    private function getBranchesRegex($configKey)
278
    {
279
        $branchesRegex = ['^$'];
280
281
        if (is_array($this->config[$configKey]) && count($this->config[$configKey])) {
282
            $branchesRegex = array_map(function ($element) {
283
                return '\(^[ ]*'.$element.'$\)';
284
            }, $this->config[$configKey]);
285
        }
286
287
        return implode('\|', $branchesRegex);
288
    }
289
290
    /**
291
     * @param array $mergedBranches
292
     * @return string
293
     */
294
    private function stringifyMergedBranches(array $mergedBranches)
295
    {
296
        $mergedBranches = array_map(function($mergedBranch) {
297
            return ($mergedBranch['mergeCommit']['authorName'] == 'fast-forward')
298
                ? sprintf('<fg=green>%s</> - %s by %s, merged in <fg=green>%s</> by %s', $mergedBranch['branchName'], $mergedBranch['relativeCommitterDate'], $mergedBranch['authorName'], $mergedBranch['mergeCommit']['branchName'], $mergedBranch['mergeCommit']['authorName'])
299
                : sprintf('<fg=green>%s</> - %s by %s, merged %s in <fg=green>%s</> by %s', $mergedBranch['branchName'], $mergedBranch['relativeCommitterDate'], $mergedBranch['authorName'], $mergedBranch['mergeCommit']['relativeCommitterDate'], $mergedBranch['mergeCommit']['branchName'], $mergedBranch['mergeCommit']['authorName']);
300
        }, $mergedBranches);
301
        return "\n\t" . implode("\n\t", $mergedBranches);
302
    }
303
304
    /**
305
     * @param  array  $commits
306
     * @return array
307
     */
308
    private function explodeCommitsArrays(array $commits)
309
    {
310
          $explodedCommits = [];
311
312
          foreach ($commits as $commit) {
313
              $explodedCommit = explode('|', $commit);
314
              $explodedCommit = array_combine(array_slice($this->commitFormatKeys, 0, count($explodedCommit)), $explodedCommit);
315
              $explodedCommits[$explodedCommit['commitHash']] = $explodedCommit;
316
          }
317
318
          return $explodedCommits;
319
    }
320
}