3 namespace Drupal\security_review;
5 use Drupal\Core\DependencyInjection\DependencySerializationTrait;
6 use Drupal\Core\Logger\RfcLogLevel;
7 use Drupal\Core\StringTranslation\StringTranslationTrait;
8 use Drupal\user\Entity\User;
11 * Defines a security check.
13 abstract class Check {
15 use DependencySerializationTrait;
16 use StringTranslationTrait;
19 * The configuration storage for this check.
21 * @var \Drupal\Core\Config\Config $config
26 * The service container.
28 * @var \Symfony\Component\DependencyInjection\ContainerInterface
33 * Settings handler for this check.
35 * @var \Drupal\security_review\CheckSettingsInterface $settings
42 * @var \Drupal\Core\State\State
47 * The check's prefix in the State system.
51 protected $statePrefix;
54 * Initializes the configuration storage and the settings handler.
56 public function __construct() {
57 $this->container = \Drupal::getContainer();
59 $this->config = $this->configFactory()
60 ->getEditable('security_review.check.' . $this->id());
61 $this->settings = new CheckSettings($this, $this->config);
62 $this->state = $this->container->get('state');
63 $this->statePrefix = 'security_review.check.' . $this->id() . '.';
65 // Set check ID in config.
66 if ($this->config->get('id') != $this->id()) {
67 $this->config->set('id', $this->id());
68 $this->config->save();
73 * Returns the namespace of the check.
75 * Usually it's the same as the module's name.
77 * Naming rules (if overridden):
78 * - All characters should be lowerspace.
79 * - Use characters only from the english alphabet.
80 * - Don't use spaces (use "_" instead).
83 * Machine namespace of the check.
85 public function getMachineNamespace() {
86 $namespace = strtolower($this->getNamespace());
87 $namespace = preg_replace("/[^a-z0-9 ]/", '', $namespace);
88 $namespace = str_replace(' ', '_', $namespace);
94 * Returns the namespace of the check.
96 * Usually it's the same as the module's name.
99 * Human-readable namespace of the check.
101 public abstract function getNamespace();
104 * Returns the machine name of the check.
106 * Naming rules (if overridden):
107 * - All characters should be lowerspace.
108 * - Use characters only from the english alphabet.
109 * - Don't use spaces (use "_" instead).
114 public function getMachineTitle() {
115 $title = strtolower($this->getTitle());
116 $title = preg_replace("/[^a-z0-9 ]/", '', $title);
117 $title = str_replace(' ', '_', $title);
123 * Returns the human-readable title of the check.
128 public abstract function getTitle();
131 * Returns the identifier constructed using the namespace and title values.
134 * Unique identifier of the check.
136 public final function id() {
137 return $this->getMachineNamespace() . '-' . $this->getMachineTitle();
141 * Returns whether the findings should be stored or reproduced when needed.
143 * The only case when this function should return false is if the check can
144 * generate a lot of findings (like the File permissions check for example).
145 * Turning this off for checks that don't generate findings at all or just a
146 * few of them actually means more overhead as the check has to be re-run
147 * in order to get its last result.
150 * Boolean indicating whether findings will be stored.
152 public function storesFindings() {
157 * Returns the check-specific settings' handler.
159 * @return \Drupal\security_review\CheckSettingsInterface
160 * The settings interface of the check.
162 public function settings() {
163 return $this->settings;
167 * The actual procedure of carrying out the check.
169 * @return \Drupal\security_review\CheckResult
170 * The result of running the check.
172 public abstract function run();
175 * Same as run(), but used in CLI context such as Drush.
177 * @return \Drupal\security_review\CheckResult
178 * The result of running the check.
180 public function runCli() {
185 * Returns the check-specific help page.
188 * The render array of the check's help page.
190 public abstract function help();
193 * Returns the evaluation page of a result.
195 * Usually this is a list of the findings and an explanation.
197 * @param \Drupal\security_review\CheckResult $result
198 * The check result to evaluate.
201 * The render array of the evaluation page.
203 public function evaluate(CheckResult $result) {
208 * Evaluates a CheckResult and returns a plaintext output.
210 * @param \Drupal\security_review\CheckResult $result
211 * The check result to evaluate.
214 * The evaluation string.
216 public function evaluatePlain(CheckResult $result) {
221 * Converts a result integer to a human-readable result message.
223 * @param int $result_const
224 * The result integer.
227 * The human-readable result message.
229 public abstract function getMessage($result_const);
232 * Returns the last stored result of the check.
234 * Returns null if no results have been stored yet.
236 * @param bool $get_findings
237 * Whether to get the findings too.
239 * @return \Drupal\security_review\CheckResult|null
240 * The last stored result (or null).
242 public function lastResult($get_findings = FALSE) {
243 // Get stored data from State system.
244 $state_prefix = $this->statePrefix . 'last_result.';
245 $result = $this->state->get($state_prefix . 'result');
247 $findings = $this->state->get($state_prefix . 'findings');
252 $time = $this->state->get($state_prefix . 'time');
253 // Force boolean value.
254 $visible = $this->state->get($state_prefix . 'visible') == TRUE;
256 // Check validity of stored data.
257 $valid_result = is_int($result)
258 && $result >= CheckResult::SUCCESS
259 && $result <= CheckResult::INFO;
260 $valid_findings = is_array($findings);
261 $valid_time = is_int($time) && $time > 0;
263 // If invalid, return NULL.
264 if (!$valid_result || !$valid_findings || !$valid_time) {
268 // Construct the CheckResult.
269 $last_result = new CheckResult($this, $result, $findings, $visible, $time);
271 // Do a check run for acquiring findings if required.
272 if ($get_findings && !$this->storesFindings()) {
273 // Run the check to get the findings.
274 $fresh_result = $this->run();
276 // If it malfunctioned return the last known good result.
277 if (!($fresh_result instanceof CheckResult)) {
281 if ($fresh_result->result() != $last_result->result()) {
282 // If the result is not the same store the new result and return it.
283 $this->storeResult($fresh_result);
284 $this->securityReview()->logCheckResult($fresh_result);
285 return $fresh_result;
288 // Else return the old result with the fresh one's findings.
289 return CheckResult::combine($last_result, $fresh_result);
297 * Returns the timestamp the check was last run.
299 * Returns 0 if it has not been run yet.
302 * The timestamp of the last stored result.
304 public function lastRun() {
305 $last_result_time = $this->state
306 ->get($this->statePrefix . 'last_result.time');
308 if (!is_int($last_result_time)) {
311 return $last_result_time;
315 * Returns whether the check is skipped. Checks are not skipped by default.
318 * Boolean indicating whether the check is skipped.
320 public function isSkipped() {
321 $is_skipped = $this->config->get('skipped');
323 if (!is_bool($is_skipped)) {
330 * Returns the user the check was skipped by.
332 * Returns null if it hasn't been skipped yet or the user that skipped the
333 * check is not valid anymore.
335 * @return \Drupal\user\Entity\User|null
336 * The user the check was last skipped by (or null).
338 public function skippedBy() {
339 $skipped_by = $this->config->get('skipped_by');
341 if (!is_int($skipped_by)) {
344 return User::load($skipped_by);
348 * Returns the timestamp the check was last skipped on.
350 * Returns 0 if it hasn't been skipped yet.
353 * The UNIX timestamp the check was last skipped on (or 0).
355 public function skippedOn() {
356 $skipped_on = $this->config->get('skipped_on');
358 if (!is_int($skipped_on)) {
365 * Enables the check. Has no effect if the check was not skipped.
367 public function enable() {
368 if ($this->isSkipped()) {
369 $this->config->set('skipped', FALSE);
370 $this->config->save();
373 $context = ['@name' => $this->getTitle()];
374 $this->securityReview()->log($this, '@name check no longer skipped', $context, RfcLogLevel::NOTICE);
379 * Marks the check as skipped.
381 * It still can be ran manually, but will remain skipped on the Run & Review
384 public function skip() {
385 if (!$this->isSkipped()) {
387 $this->config->set('skipped', TRUE);
388 $this->config->set('skipped_by', $this->currentUser()->id());
389 $this->config->set('skipped_on', time());
390 $this->config->save();
393 $context = ['@name' => $this->getTitle()];
394 $this->securityReview()->log($this, '@name check skipped', $context, RfcLogLevel::NOTICE);
399 * Stores a result in the state system.
401 * @param \Drupal\security_review\CheckResult $result
402 * The result to store.
404 public function storeResult(CheckResult $result) {
405 if ($result == NULL) {
407 '@reviewcheck' => $this->getTitle(),
408 '@namespace' => $this->getNamespace(),
410 $this->securityReview()->log($this, 'Unable to store check @reviewcheck for @namespace', $context, RfcLogLevel::CRITICAL);
414 $findings = $this->storesFindings() ? $result->findings() : [];
415 $this->state->setMultiple([
416 $this->statePrefix . 'last_result.result' => $result->result(),
417 $this->statePrefix . 'last_result.time' => $result->time(),
418 $this->statePrefix . 'last_result.visible' => $result->isVisible(),
419 $this->statePrefix . 'last_result.findings' => $findings,
424 * Creates a new CheckResult for this Check.
427 * The result integer (see the constants defined in CheckResult).
428 * @param array $findings
430 * @param bool $visible
431 * The visibility of the result.
433 * The time the test was run.
435 * @return \Drupal\security_review\CheckResult
436 * The created CheckResult.
438 public function createResult($result, array $findings = [], $visible = TRUE, $time = NULL) {
439 return new CheckResult($this, $result, $findings, $visible, $time);
443 * Returns the Security Review Checklist service.
445 * @return \Drupal\security_review\Checklist
446 * Security Review's Checklist service.
448 protected function checklist() {
449 return $this->container->get('security_review.checklist');
453 * Returns the Config factory.
455 * @return \Drupal\Core\Config\ConfigFactory
458 protected function configFactory() {
459 return $this->container->get('config.factory');
463 * Returns the service container.
465 * @return \Symfony\Component\DependencyInjection\ContainerInterface
468 protected function container() {
469 return $this->container;
473 * Returns the current Drupal user.
475 * @return \Drupal\Core\Session\AccountProxy
476 * Current Drupal user.
478 protected function currentUser() {
479 return $this->container->get('current_user');
483 * Returns the database connection.
485 * @return \Drupal\Core\Database\Connection
486 * Database connection.
488 protected function database() {
489 return $this->container->get('database');
493 * Returns the entity manager.
495 * @return \Drupal\Core\Entity\EntityManagerInterface
498 protected function entityManager() {
499 return $this->container->get('entity.manager');
503 * Returns the Drupal Kernel.
505 * @return \Drupal\Core\DrupalKernel
508 protected function kernel() {
509 return $this->container->get('kernel');
513 * Returns the module handler.
515 * @return \Drupal\Core\Extension\ModuleHandler
518 protected function moduleHandler() {
519 return $this->container->get('module_handler');
523 * Returns the Security Review Security service.
525 * @return \Drupal\security_review\Security
526 * Security Review's Security service.
528 protected function security() {
529 return $this->container->get('security_review.security');
533 * Returns the Security Review service.
535 * @return \Drupal\security_review\SecurityReview
536 * Security Review service.
538 protected function securityReview() {
539 return $this->container->get('security_review');