SecurityAlertCheckTask::setSecurityChecker()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace BringYourOwnIdeas\SecurityChecker\Tasks;
4
5
use SensioLabs\Security\SecurityChecker;
6
use BringYourOwnIdeas\SecurityChecker\Models\SecurityAlert;
7
use BringYourOwnIdeas\SecurityChecker\Extensions\SecurityAlertExtension;
8
use BringYourOwnIdeas\Maintenance\Model\Package;
0 ignored issues
show
Bug introduced by
The type BringYourOwnIdeas\Maintenance\Model\Package was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
9
use SilverStripe\ORM\Queries\SQLDelete;
10
use SilverStripe\ORM\DataObjectSchema;
11
use SilverStripe\Dev\SapphireTest;
12
use SilverStripe\Control\Director;
13
use SilverStripe\Dev\BuildTask;
14
15
/**
16
 * Checks if there are any insecure dependencies.
17
 */
18
class SecurityAlertCheckTask extends BuildTask
19
{
20
    private static $segment = 'SecurityAlertCheckTask';
0 ignored issues
show
introduced by
The private property $segment is not used, and could be removed.
Loading history...
21
22
    /**
23
     * @var SecurityChecker
24
     */
25
    protected $securityChecker;
26
27
    private static $dependencies = [
0 ignored issues
show
introduced by
The private property $dependencies is not used, and could be removed.
Loading history...
28
        'SecurityChecker' => '%$' . SecurityChecker::class,
29
    ];
30
31
    protected $title = 'Composer security checker';
32
33
    protected $description =
34
        'Checks if any modules managed through composer have known security vulnerabilities at the used version.';
35
36
    /**
37
     * @return SecurityChecker
38
     */
39
    public function getSecurityChecker()
40
    {
41
        return $this->securityChecker;
42
    }
43
44
    /**
45
     * @param SecurityChecker $securityChecker
46
     * @return $this
47
     */
48
    public function setSecurityChecker(SecurityChecker $securityChecker)
49
    {
50
        $this->securityChecker = $securityChecker;
51
        return $this;
52
    }
53
54
    /**
55
     * Most SilverStripe issued alerts are _not_ assiged CVEs.
56
     * However they have their own identifier in the form of a
57
     * prefix to the title - we can use this instead of a CVE ID.
58
     *
59
     * @param string $cve
60
     * @param string $title
61
     *
62
     * @return string
63
     */
64
    protected function discernIdentifier($cve, $title)
65
    {
66
        $identifier = $cve;
67
        if (!$identifier || $identifier === '~') {
68
            $identifier = explode(':', $title);
69
            $identifier = array_shift($identifier);
70
        }
71
        $this->extend('updateIdentifier', $identifier, $cve, $title);
72
        return $identifier;
73
    }
74
75
    public function run($request)
76
    {
77
        // to keep the list up to date while removing resolved issues we keep all of found issues
78
        $validEntries = array();
79
80
        // use the security checker of
81
        $checker = $this->getSecurityChecker();
82
        $result = $checker->check(BASE_PATH . DIRECTORY_SEPARATOR . 'composer.lock');
83
        $alerts = json_decode((string) $result, true);
84
85
        // go through all alerts for packages - each can contain multiple issues
86
        foreach ($alerts as $package => $packageDetails) {
87
            // go through each individual known security issue
88
            foreach ($packageDetails['advisories'] as $details) {
89
                $identifier = $this->discernIdentifier($details['cve'], $details['title']);
90
                $vulnerability = null;
91
92
                // check if this vulnerability is already known
93
                $existingVulns = SecurityAlert::get()->filter(array(
94
                    'PackageName' => $package,
95
                    'Version' => $packageDetails['version'],
96
                    'Identifier'   => $identifier,
97
                ));
98
99
                // Is this vulnerability known? No, lets add it.
100
                if (!$existingVulns->Count()) {
101
                    $vulnerability = SecurityAlert::create();
102
                    $vulnerability->PackageName  = $package;
103
                    $vulnerability->Version      = $packageDetails['version'];
104
                    $vulnerability->Title        = $details['title'];
105
                    $vulnerability->ExternalLink = $details['link'];
106
                    $vulnerability->Identifier   = $identifier;
107
108
                    $vulnerability->write();
109
110
                    // add the new entries to the list of valid entries
111
                    $validEntries[] = $vulnerability->ID;
112
                } else {
113
                    // add existing vulnerabilities (probably just 1) to the list of valid entries
114
                    $validEntries = array_merge($validEntries, $existingVulns->column('ID'));
115
                }
116
117
                // Relate this vulnerability to an existing Package, if the
118
                // bringyourownideas/silverstripe-maintenance module is installed
119
                if ($vulnerability && $vulnerability->hasExtension(SecurityAlertExtension::class)
120
                    && class_exists(Package::class)
121
                    && !$vulnerability->PackageRecordID
122
                    && $packageRecord = Package::get()->find('Name', $package)
123
                ) {
124
                    $vulnerability->PackageRecordID = $packageRecord->ID;
125
                    $vulnerability->write();
126
                }
127
            }
128
        }
129
130
        // remove all entries which are resolved (no longer $validEntries)
131
        $tableName = DataObjectSchema::create()->tableName(SecurityAlert::class);
132
        $removeOldSecurityAlerts = SQLDelete::create("\"$tableName\"");
133
        if (empty($validEntries)) {
134
            // There were no SecurityAlerts listed for our installation - so flush any old data
135
            $removeOldSecurityAlerts->execute();
136
        } else {
137
            $removable = SecurityAlert::get()->exclude(array('ID' => $validEntries));
138
            // Be careful not to remove all SecurityAlerts on the case that every entry is valid
139
            if ($removable->exists()) {
140
                // SQLConditionalExpression does not support IN() syntax via addWhere
141
                // so we have to build this up manually
142
                $convertIDsToQuestionMarks = function ($id) {
0 ignored issues
show
Unused Code introduced by
The parameter $id is not used and could be removed. ( Ignorable by Annotation )

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

142
                $convertIDsToQuestionMarks = function (/** @scrutinizer ignore-unused */ $id) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
143
                    return '?';
144
                };
145
                $queryArgs = $removable->column('ID');
146
                $paramPlaceholders = implode(',', array_map($convertIDsToQuestionMarks, $queryArgs));
147
148
                $removeOldSecurityAlerts = $removeOldSecurityAlerts->addWhere([
149
                    '"ID" IN(' . $paramPlaceholders . ')' => $queryArgs
150
                ]);
151
                $removeOldSecurityAlerts->execute();
152
            }
153
        }
154
155
        // notify that the task finished.
156
        $this->output('The task finished running. You can find the updated information in the database now.');
157
    }
158
159
    /**
160
     * prints a message during the run of the task
161
     *
162
     * @param string $text
163
     */
164
    protected function output($text)
165
    {
166
        echo Director::is_cli() ? $text . PHP_EOL : "<p>$text</p>\n";
167
    }
168
}
169