Issues (7)

src/Commands/Waf/BlockIP.php (2 issues)

Labels
Severity
1
<?php
2
3
namespace Sebdesign\ArtisanCloudflare\Commands\Waf;
4
5
use Illuminate\Console\Command;
6
use Illuminate\Support\Collection;
7
use Sebdesign\ArtisanCloudflare\Client;
8
use Sebdesign\ArtisanCloudflare\Zone;
9
use Symfony\Component\Console\Helper\TableCell;
10
use Symfony\Component\Console\Helper\TableSeparator;
11
12
class BlockIP extends Command
13
{
14
    /**
15
     * The name of the console command.
16
     *
17
     * @var string
18
     */
19
    protected $name = 'cloudflare:waf:block-ip';
20
21
    /**
22
     * The name and signature of the console command.
23
     *
24
     * @var string
25
     */
26
    protected $signature = 'cloudflare:waf:block-ip {ip : The IP address to block.} 
27
      {zone? : A zone identifier.} 
28
      {--notes= : Notes that will be attached to the rule.}';
29
30
    /**
31
     * The name and signature of the console command.
32
     *
33
     * @var string
34
     */
35
    protected $description = 'Block an IP address in the CloudFlare WAF.';
36
37
    /**
38
     * CloudFlare API client.
39
     *
40
     * @var \Sebdesign\ArtisanCloudflare\Client
41
     */
42
    private $client;
43
44
    /**
45
     * API item identifier tags.
46
     *
47
     * @var \Illuminate\Support\Collection<string,\Sebdesign\ArtisanCloudflare\Zone>
48
     */
49
    private $zones;
50
51
    /**
52
     * Purge constructor.
53
     *
54
     * @param  array  $zones
55
     */
56 60
    public function __construct(array $zones)
57
    {
58 60
        parent::__construct();
59
60
        $this->zones = Collection::make($zones)->map(function (array $zone) {
61 54
            return new Zone(array_filter($zone));
62 60
        });
63 60
    }
64
65
    /**
66
     * Execute the console command.
67
     *
68
     * @param  \Sebdesign\ArtisanCloudflare\Client  $client
69
     * @return int
70
     */
71 27
    public function handle(Client $client)
72
    {
73 27
        $this->client = $client;
74
75 27
        $zones = $this->getZones();
76
77 27
        if ($zones->isEmpty()) {
78 3
            $this->error('Please supply a valid zone identifier in the input argument or the cloudflare config.');
79
80 3
            return 1;
81
        }
82
83 24
        $ip = $this->argument('ip');
84
85 24
        $target = $this->isIPv4($ip) ? 'ip' : ($this->isIPv6($ip) ? 'ip6' : null);
0 ignored issues
show
It seems like $ip can also be of type array; however, parameter $ip of Sebdesign\ArtisanCloudfl...s\Waf\BlockIP::isIPv6() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

85
        $target = $this->isIPv4($ip) ? 'ip' : ($this->isIPv6(/** @scrutinizer ignore-type */ $ip) ? 'ip6' : null);
Loading history...
It seems like $ip can also be of type array; however, parameter $ip of Sebdesign\ArtisanCloudfl...s\Waf\BlockIP::isIPv4() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

85
        $target = $this->isIPv4(/** @scrutinizer ignore-type */ $ip) ? 'ip' : ($this->isIPv6($ip) ? 'ip6' : null);
Loading history...
86
87 24
        if (! $target) {
88 3
            $this->error('Please supply a valid IP address.');
89
90 3
            return 1;
91
        }
92
93 21
        $zones = $this->applyParameters($zones, $target);
94
95 21
        $results = $this->block($zones);
96
97 21
        $this->displayResults($zones, $results);
98
99 21
        return $this->getExitCode($results);
100
    }
101
102
    /**
103
     * Apply the paremeters for each zone.
104
     *
105
     * Use the config for each zone, unless options are passed in the command.
106
     *
107
     * @param  \Illuminate\Support\Collection<string,\Sebdesign\ArtisanCloudflare\Zone>  $zones
108
     * @param  string  $target
109
     * @return \Illuminate\Support\Collection<string,\Sebdesign\ArtisanCloudflare\Zone>
110
     */
111 21
    private function applyParameters($zones, $target): Collection
112
    {
113
        $parameters = [
114 21
            'mode' => 'block',
115
            'configuration' => [
116 21
                'target' => $target,
117 21
                'value' => $this->argument('ip'),
118
            ],
119 21
            'notes' => $this->option('notes') ?? 'Blocked by artisan command.',
120
        ];
121
122
        return $zones->each(function (Zone $zone) use ($parameters) {
123 21
            $zone->replace($parameters);
124 21
        });
125
    }
126
127
    /**
128
     * Block the given IP address.
129
     *
130
     * @param  \Illuminate\Support\Collection<string,\Sebdesign\ArtisanCloudflare\Zone>  $zones
131
     */
132 21
    private function block($zones): Collection
133
    {
134 21
        return $this->client->blockIP($zones);
135
    }
136
137
    /**
138
     * Display a table with the results.
139
     *
140
     * @param  \Illuminate\Support\Collection<string,\Sebdesign\ArtisanCloudflare\Zone>  $zones
141
     * @param  \Illuminate\Support\Collection<string,\Sebdesign\ArtisanCloudflare\Zone>  $results
142
     * @return void
143
     */
144 21
    private function displayResults($zones, $results): void
145
    {
146 21
        $headers = ['Status', 'Zone', 'IP', 'Errors'];
147
148
        $title = [
149 21
            new TableCell(
150 21
                'CloudFlare WAF: Block IP',
151 21
                ['colspan' => count($headers)]
152
            ),
153
        ];
154
155
        // Get the status emoji
156
        $emoji = $results->map(function (Zone $zone) {
157 21
            return $zone->get('success') ? '✅' : '❌';
158 21
        });
159
160
        // Get the zone identifiers
161 21
        $identifiers = $zones->keys();
162
163
        // Get the ip as multiline strings
164
        $ip = $zones->map(function (Zone $zone) {
165 21
            return $this->formatItems([$zone->get('configuration', [])['value']]);
166 21
        });
167
168
        // Get the errors as red multiline strings
169
        $errors = $results->map(function (Zone $result) {
170 21
            return $this->formatErrors($result->get('errors', []));
171
        })->map(function (array $errors) {
172 21
            return $this->formatItems($errors);
173 21
        });
174
175 21
        $columns = Collection::make([
176 21
            'status' => $emoji,
177 21
            'identifier' => $identifiers,
178 21
            'ip' => $ip,
179 21
            'errors' => $errors,
180
        ]);
181
182 21
        $rows = $columns->_transpose()->insertBetween(new TableSeparator());
183
184 21
        $this->table([$title, $headers], $rows);
185 21
    }
186
187
    /**
188
     * Format an array into a multiline string.
189
     *
190
     * @param  array  $items
191
     * @return string
192
     */
193 21
    private function formatItems(array $items): string
194
    {
195 21
        return implode("\n", $items);
196
    }
197
198
    /**
199
     * Format the errors.
200
     *
201
     * @param  array[]  $errors
202
     * @return string[]
203
     */
204 21
    private function formatErrors(array $errors): array
205
    {
206
        return array_map(function (array $error) {
207 6
            if (isset($error['code'])) {
208 6
                return "<fg=red>{$error['code']}: {$error['message']}</>";
209
            }
210
211 3
            return "<fg=red>{$error['message']}</>";
212 21
        }, $errors);
213
    }
214
215
    /**
216
     * Get the zone identifier from the input argument or the configuration.
217
     *
218
     * @return \Illuminate\Support\Collection<string,\Sebdesign\ArtisanCloudflare\Zone>
219
     */
220 27
    private function getZones(): Collection
221
    {
222 27
        if (! $zone = $this->argument('zone')) {
223 21
            return $this->zones;
224
        }
225
226 6
        $zones = $this->zones->only($zone);
227
228 6
        if ($zones->count()) {
229 3
            return $zones;
230
        }
231
232 3
        return new Collection([
233 3
            $zone => new Zone(),
234
        ]);
235
    }
236
237
    /**
238
     * Return 1 if all successes are false, otherwise return 0.
239
     *
240
     * @param  \Illuminate\Support\Collection<string,\Sebdesign\ArtisanCloudflare\Zone>  $results
241
     * @return int
242
     */
243 21
    private function getExitCode($results): int
244
    {
245
        return (int) $results->filter(function (Zone $zone) {
246 21
            return $zone->get('success');
247 21
        })->isEmpty();
248
    }
249
250
    /**
251
     * Check if the given IP address is IPv4.
252
     *
253
     * @param  string  $ip
254
     * @return bool
255
     */
256 24
    private function isIPv4($ip): bool
257
    {
258 24
        return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
259
    }
260
261
    /**
262
     * Check if the given IP address is IPv6.
263
     *
264
     * @param  string  $ip
265
     * @return bool
266
     */
267 6
    private function isIPv6($ip): bool
268
    {
269 6
        return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
270
    }
271
}
272