5 * Contains \Drupal\security_review\Check.
8 namespace Drupal\security_review;
10 use Drupal\Core\DependencyInjection\DependencySerializationTrait;
11 use Drupal\Core\Logger\RfcLogLevel;
12 use Drupal\Core\Routing\LinkGeneratorTrait;
13 use Drupal\Core\Routing\UrlGeneratorTrait;
14 use Drupal\Core\StringTranslation\StringTranslationTrait;
15 use Drupal\user\Entity\User;
18 * Defines a security check.
20 abstract class Check {
22 use DependencySerializationTrait;
23 use LinkGeneratorTrait;
24 use UrlGeneratorTrait;
25 use StringTranslationTrait;
28 * The configuration storage for this check.
30 * @var \Drupal\Core\Config\Config $config
35 * The service container.
37 * @var \Symfony\Component\DependencyInjection\ContainerInterface
42 * Settings handler for this check.
44 * @var \Drupal\security_review\CheckSettingsInterface $settings
51 * @var \Drupal\Core\State\State
56 * The check's prefix in the State system.
60 protected $statePrefix;
63 * Initializes the configuration storage and the settings handler.
65 public function __construct() {
66 $this->container = \Drupal::getContainer();
68 $this->config = $this->configFactory()
69 ->getEditable('security_review.check.' . $this->id());
70 $this->settings = new CheckSettings($this, $this->config);
71 $this->state = $this->container->get('state');
72 $this->statePrefix = 'security_review.check.' . $this->id() . '.';
74 // Set check ID in config.
75 if ($this->config->get('id') != $this->id()) {
76 $this->config->set('id', $this->id());
77 $this->config->save();
82 * Returns the namespace of the check.
84 * Usually it's the same as the module's name.
86 * Naming rules (if overridden):
87 * - All characters should be lowerspace.
88 * - Use characters only from the english alphabet.
89 * - Don't use spaces (use "_" instead).
92 * Machine namespace of the check.
94 public function getMachineNamespace() {
95 $namespace = strtolower($this->getNamespace());
96 $namespace = preg_replace("/[^a-z0-9 ]/", '', $namespace);
97 $namespace = str_replace(' ', '_', $namespace);
103 * Returns the namespace of the check.
105 * Usually it's the same as the module's name.
108 * Human-readable namespace of the check.
110 public abstract function getNamespace();
113 * Returns the machine name of the check.
115 * Naming rules (if overridden):
116 * - All characters should be lowerspace.
117 * - Use characters only from the english alphabet.
118 * - Don't use spaces (use "_" instead).
123 public function getMachineTitle() {
124 $title = strtolower($this->getTitle());
125 $title = preg_replace("/[^a-z0-9 ]/", '', $title);
126 $title = str_replace(' ', '_', $title);
132 * Returns the human-readable title of the check.
137 public abstract function getTitle();
140 * Returns the identifier constructed using the namespace and title values.
143 * Unique identifier of the check.
145 public final function id() {
146 return $this->getMachineNamespace() . '-' . $this->getMachineTitle();
150 * Returns whether the findings should be stored or reproduced when needed.
152 * The only case when this function should return false is if the check can
153 * generate a lot of findings (like the File permissions check for example).
154 * Turning this off for checks that don't generate findings at all or just a
155 * few of them actually means more overhead as the check has to be re-run
156 * in order to get its last result.
159 * Boolean indicating whether findings will be stored.
161 public function storesFindings() {
166 * Returns the check-specific settings' handler.
168 * @return \Drupal\security_review\CheckSettingsInterface
169 * The settings interface of the check.
171 public function settings() {
172 return $this->settings;
176 * The actual procedure of carrying out the check.
178 * @return \Drupal\security_review\CheckResult
179 * The result of running the check.
181 public abstract function run();
184 * Same as run(), but used in CLI context such as Drush.
186 * @return \Drupal\security_review\CheckResult
187 * The result of running the check.
189 public function runCli() {
194 * Returns the check-specific help page.
197 * The render array of the check's help page.
199 public abstract function help();
202 * Returns the evaluation page of a result.
204 * Usually this is a list of the findings and an explanation.
206 * @param \Drupal\security_review\CheckResult $result
207 * The check result to evaluate.
210 * The render array of the evaluation page.
212 public function evaluate(CheckResult $result) {
217 * Evaluates a CheckResult and returns a plaintext output.
219 * @param \Drupal\security_review\CheckResult $result
220 * The check result to evaluate.
223 * The evaluation string.
225 public function evaluatePlain(CheckResult $result) {
230 * Converts a result integer to a human-readable result message.
232 * @param int $result_const
233 * The result integer.
236 * The human-readable result message.
238 public abstract function getMessage($result_const);
241 * Returns the last stored result of the check.
243 * Returns null if no results have been stored yet.
245 * @param bool $get_findings
246 * Whether to get the findings too.
248 * @return \Drupal\security_review\CheckResult|null
249 * The last stored result (or null).
251 public function lastResult($get_findings = FALSE) {
252 // Get stored data from State system.
253 $state_prefix = $this->statePrefix . 'last_result.';
254 $result = $this->state->get($state_prefix . 'result');
256 $findings = $this->state->get($state_prefix . 'findings');
261 $time = $this->state->get($state_prefix . 'time');
262 // Force boolean value.
263 $visible = $this->state->get($state_prefix . 'visible') == TRUE;
265 // Check validity of stored data.
266 $valid_result = is_int($result)
267 && $result >= CheckResult::SUCCESS
268 && $result <= CheckResult::INFO;
269 $valid_findings = is_array($findings);
270 $valid_time = is_int($time) && $time > 0;
272 // If invalid, return NULL.
273 if (!$valid_result || !$valid_findings || !$valid_time) {
277 // Construct the CheckResult.
278 $last_result = new CheckResult($this, $result, $findings, $visible, $time);
280 // Do a check run for acquiring findings if required.
281 if ($get_findings && !$this->storesFindings()) {
282 // Run the check to get the findings.
283 $fresh_result = $this->run();
285 // If it malfunctioned return the last known good result.
286 if (!($fresh_result instanceof CheckResult)) {
290 if ($fresh_result->result() != $last_result->result()) {
291 // If the result is not the same store the new result and return it.
292 $this->storeResult($fresh_result);
293 $this->securityReview()->logCheckResult($fresh_result);
294 return $fresh_result;
297 // Else return the old result with the fresh one's findings.
298 return CheckResult::combine($last_result, $fresh_result);
306 * Returns the timestamp the check was last run.
308 * Returns 0 if it has not been run yet.
311 * The timestamp of the last stored result.
313 public function lastRun() {
314 $last_result_time = $this->state
315 ->get($this->statePrefix . 'last_result.time');
317 if (!is_int($last_result_time)) {
320 return $last_result_time;
324 * Returns whether the check is skipped. Checks are not skipped by default.
327 * Boolean indicating whether the check is skipped.
329 public function isSkipped() {
330 $is_skipped = $this->config->get('skipped');
332 if (!is_bool($is_skipped)) {
339 * Returns the user the check was skipped by.
341 * Returns null if it hasn't been skipped yet or the user that skipped the
342 * check is not valid anymore.
344 * @return \Drupal\user\Entity\User|null
345 * The user the check was last skipped by (or null).
347 public function skippedBy() {
348 $skipped_by = $this->config->get('skipped_by');
350 if (!is_int($skipped_by)) {
353 return User::load($skipped_by);
357 * Returns the timestamp the check was last skipped on.
359 * Returns 0 if it hasn't been skipped yet.
362 * The UNIX timestamp the check was last skipped on (or 0).
364 public function skippedOn() {
365 $skipped_on = $this->config->get('skipped_on');
367 if (!is_int($skipped_on)) {
374 * Enables the check. Has no effect if the check was not skipped.
376 public function enable() {
377 if ($this->isSkipped()) {
378 $this->config->set('skipped', FALSE);
379 $this->config->save();
382 $context = ['@name' => $this->getTitle()];
383 $this->securityReview()->log($this, '@name check no longer skipped', $context, RfcLogLevel::NOTICE);
388 * Marks the check as skipped.
390 * It still can be ran manually, but will remain skipped on the Run & Review
393 public function skip() {
394 if (!$this->isSkipped()) {
396 $this->config->set('skipped', TRUE);
397 $this->config->set('skipped_by', $this->currentUser()->id());
398 $this->config->set('skipped_on', time());
399 $this->config->save();
402 $context = ['@name' => $this->getTitle()];
403 $this->securityReview()->log($this, '@name check skipped', $context, RfcLogLevel::NOTICE);
408 * Stores a result in the state system.
410 * @param \Drupal\security_review\CheckResult $result
411 * The result to store.
413 public function storeResult(CheckResult $result) {
414 if ($result == NULL) {
416 '@reviewcheck' => $this->getTitle(),
417 '@namespace' => $this->getNamespace(),
419 $this->securityReview()->log($this, 'Unable to store check @reviewcheck for @namespace', $context, RfcLogLevel::CRITICAL);
423 $findings = $this->storesFindings() ? $result->findings() : [];
424 $this->state->setMultiple([
425 $this->statePrefix . 'last_result.result' => $result->result(),
426 $this->statePrefix . 'last_result.time' => $result->time(),
427 $this->statePrefix . 'last_result.visible' => $result->isVisible(),
428 $this->statePrefix . 'last_result.findings' => $findings,
433 * Creates a new CheckResult for this Check.
436 * The result integer (see the constants defined in CheckResult).
437 * @param array $findings
439 * @param bool $visible
440 * The visibility of the result.
442 * The time the test was run.
444 * @return \Drupal\security_review\CheckResult
445 * The created CheckResult.
447 public function createResult($result, array $findings = [], $visible = TRUE, $time = NULL) {
448 return new CheckResult($this, $result, $findings, $visible, $time);
452 * Returns the Security Review Checklist service.
454 * @return \Drupal\security_review\Checklist
455 * Security Review's Checklist service.
457 protected function checklist() {
458 return $this->container->get('security_review.checklist');
462 * Returns the Config factory.
464 * @return \Drupal\Core\Config\ConfigFactory
467 protected function configFactory() {
468 return $this->container->get('config.factory');
472 * Returns the service container.
474 * @return \Symfony\Component\DependencyInjection\ContainerInterface
477 protected function container() {
478 return $this->container;
482 * Returns the current Drupal user.
484 * @return \Drupal\Core\Session\AccountProxy
485 * Current Drupal user.
487 protected function currentUser() {
488 return $this->container->get('current_user');
492 * Returns the database connection.
494 * @return \Drupal\Core\Database\Connection
495 * Database connection.
497 protected function database() {
498 return $this->container->get('database');
502 * Returns the entity manager.
504 * @return \Drupal\Core\Entity\EntityManagerInterface
507 protected function entityManager() {
508 return $this->container->get('entity.manager');
512 * Returns the Drupal Kernel.
514 * @return \Drupal\Core\DrupalKernel
517 protected function kernel() {
518 return $this->container->get('kernel');
522 * Returns the module handler.
524 * @return \Drupal\Core\Extension\ModuleHandler
527 protected function moduleHandler() {
528 return $this->container->get('module_handler');
532 * Returns the Security Review Security service.
534 * @return \Drupal\security_review\Security
535 * Security Review's Security service.
537 protected function security() {
538 return $this->container->get('security_review.security');
542 * Returns the Security Review service.
544 * @return \Drupal\security_review\SecurityReview
545 * Security Review service.
547 protected function securityReview() {
548 return $this->container->get('security_review');