You are here

security_review.module in Security Review 6

Same filename and directory in other branches
  1. 8 security_review.module
  2. 7 security_review.module

Site security review and reporting Drupal module.

File

security_review.module
View source
<?php

/**
 * @file
 * Site security review and reporting Drupal module.
 * 
 */

/**
 * Implementation of hook_perm().
 */
function security_review_perm() {
  return array(
    'access security review list',
    'run security checks',
  );
}

/**
 * Implementation of hook_menu().
 */
function security_review_menu() {
  $items = array();
  $items['admin/reports/security-review'] = array(
    'title' => 'Security review',
    'description' => 'Perform and review the security of your site.',
    'page callback' => 'security_review_page',
    'access arguments' => array(
      'access security review list',
    ),
    'type' => MENU_NORMAL_ITEM,
  );
  $items['admin/reports/security-review/run'] = array(
    'title' => 'Run & review',
    'access arguments' => array(
      'access security review list',
    ),
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
  $items['admin/reports/security-review/toggle/%'] = array(
    'title' => 'Security review toggle',
    'page callback' => 'security_review_toggle_check',
    'page arguments' => array(
      4,
    ),
    'access arguments' => array(
      'access security review list',
    ),
    'type' => MENU_CALLBACK,
  );
  $items['admin/reports/security-review/help'] = array(
    'title' => 'Help',
    'page callback' => 'security_review_check_help',
    'access arguments' => array(
      'access security review list',
    ),
    'type' => MENU_LOCAL_TASK,
    'weight' => 10,
  );
  $items['admin/reports/security-review/settings'] = array(
    'title' => 'Settings',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'security_review_settings',
    ),
    'access arguments' => array(
      'access security review list',
    ),
    'type' => MENU_LOCAL_TASK,
    'weight' => 15,
  );
  return $items;
}

/**
 * Implementation of hook_theme().
 */
function security_review_theme($existing, $type, $theme, $path) {
  return array(
    'security_review_reviewed' => array(
      'arguments' => array(
        'items' => array(),
      ),
    ),
    'security_review_help_options' => array(
      'arguments' => array(
        'element' => array(),
      ),
    ),
    'security_review_check_help' => array(
      'arguments' => array(
        'element' => array(),
      ),
    ),
  );
}

/**
 * Page callback for run & review.
 */
function security_review_page() {
  $checks = array();
  $output = '';

  // Retrieve the checklist.
  $checklist = module_invoke_all('security_checks');

  // Retrieve results from last run of the checklist.
  $results = db_query("SELECT namespace, reviewcheck, result, lastrun, skip, skiptime, skipuid FROM {security_review}");
  while ($result = db_fetch_array($results)) {
    $checks[] = $result;
  }

  // Only users with the proper permission can run the checklist.
  if (user_access('run security checks')) {
    $output .= drupal_get_form('security_review_run_form', $checks);
  }
  if (!empty($checks)) {

    // We have prior results, so display them.
    $output .= security_review_reviewed($checklist, $checks);
  }
  else {

    // If they haven't configured the site, prompt them to do so.
    $variable = variable_get('security_review_log', FALSE);
    if (!$variable) {
      drupal_set_message(t('It appears this is your first time using the Security Review checklist. Before running the checklist please review the settings page at !link to set which roles are untrusted.', array(
        '!link' => l('admin/reports/security-review/settings', 'admin/reports/security-review/settings'),
      )));
    }
  }
  return $output;
}

/**
 * Implementation of hook_security_checks().
 */
function security_review_security_checks() {
  module_load_include('inc', 'security_review');

  // security_review_get_checks() will include implementations of this hook
  // that Security Review defines on behalf of other modules.
  return security_review_get_checks();
}

/**
 * Retrive the result from the last run of a security check.
 */
function security_review_get_last_check($namespace, $check_name) {
  $check = db_fetch_array(db_query("SELECT namespace, reviewcheck, result, lastrun, skip, skiptime, skipuid FROM {security_review} WHERE namespace = '%s' AND reviewcheck = '%s'", $namespace, $check_name));
  return !empty($check) ? $check : NULL;
}

/**
 * Page callback provides general help and check specific help documentation.
 */
function security_review_check_help($module = NULL, $check_name = NULL) {

  // Include checks and help files.
  $checklist = module_invoke_all('security_checks');
  module_load_include('inc', 'security_review', 'security_review.help');
  $output = '';
  if (!is_null($module) && !is_null($check_name)) {
    $check = $checklist[$module][$check_name];
    if (isset($check['help'])) {
      $output = $check['help'];
    }
    elseif (isset($check['callback'])) {
      if (isset($check['file'])) {

        // Handle Security Review defining checks for other modules.
        if (isset($check['module'])) {
          $module = $check['module'];
        }
        module_load_include('inc', $module, $check['file']);
      }
      $function = $check['callback'] . '_help';
      if (function_exists($function)) {
        $element = $function();
        $output = theme('security_review_check_help', $element);
      }
    }
  }
  else {
    $output = _security_review_help();

    // List all checks as links to specific help.
    $output .= '<h3>' . t('Check-specfic help') . '</h3>';
    $output .= '<p>' . t("Details and help on the security review checks. Checks are not always perfectly correct in their procedure and result. Refer to drupal.org handbook documentation if you are unsure how to make the recommended alterations to your configuration or consult the module's README.txt for support.") . '</p>';
    $checklist = module_invoke_all('security_checks');
    foreach ($checklist as $module => $checks) {
      foreach ($checks as $reviewcheck => $check) {
        $items[] = l($check['title'], 'admin/reports/security-review/help/' . $module . '/' . $reviewcheck);
      }
    }
    if ($items) {
      $output .= theme('item_list', $items);
    }
  }
  if (empty($output)) {
    return drupal_not_found();
  }
  return $output;
}
function security_review_reviewed($checklist, $checks, $namespace = NULL) {
  $output = '';
  $header = t('Review results from last run');
  $desc = t("Here you can review the results from the last run of the checklist. Checks are not always perfectly correct in their procedure and result. You can keep a check from running by clicking the 'Skip' link beside it. You can run the checklist again by expanding the fieldset above.");
  foreach ($checks as $check) {
    $message = $check['result'] ? $checklist[$check['namespace']][$check['reviewcheck']]['success'] : $checklist[$check['namespace']][$check['reviewcheck']]['failure'];
    $class = $check['skip'] ? 'info' : ($check['result'] ? 'ok' : 'error');
    $toggle = $check['skip'] ? t('Enable') : t('Skip');
    $token = drupal_get_token($check['reviewcheck']);
    $link_options = array(
      'query' => array(
        'token' => $token,
      ),
      'attributes' => array(
        'class' => 'sec-rev-dyn',
      ),
    );
    $items[] = array(
      'value' => $check['result'],
      'class' => $class,
      'message' => $message,
      'help_link' => l(t('Details'), 'admin/reports/security-review/help/' . $check['namespace'] . '/' . $check['reviewcheck']),
      'toggle_link' => l($toggle, 'admin/reports/security-review/toggle/' . $check['reviewcheck'], $link_options),
    );
  }
  $output .= theme('security_review_reviewed', $items, $header, $desc);
  return $output;
}
function theme_security_review_reviewed($items = array(), $header = '', $desc = '') {
  drupal_add_js(drupal_get_path('module', 'security_review') . '/security_review.js', 'module', 'footer');
  $output = '<h3>' . $header . '</h3>';
  $output .= '<p>' . $desc . '</p>';
  $output .= '<table class="system-status-report">';
  foreach ($items as $item) {
    $output .= '<tr class="' . $item['class'] . '"><th>' . $item['message'] . '</th>';
    $output .= '<td>' . $item['help_link'] . '</td>';
    $output .= '<td>' . $item['toggle_link'] . '</td>';
    $output .= '</tr>';
  }
  $output .= '</table>';
  return $output;
}

/**
 * Theme function for help on a security check.
 *
 * Calling function should filter and sanitize.
 */
function theme_security_review_check_help($element) {
  $output = "<h3>" . $element['title'] . "</h3>\n";
  foreach ($element['descriptions'] as $description) {
    $output .= "<p>" . $description . "</p>\n";
  }
  if (!empty($element['findings'])) {
    foreach ($element['findings']['descriptions'] as $description) {
      $output .= "<p>" . $description . "</p>\n";
    }
    if (!empty($element['findings']['items'])) {
      $items = $element['findings']['items'];
      $output .= "<ul>\n";

      // Loop through items outputting the best value HTML, safe, or raw if thats all there is.
      foreach ($items as $item) {
        if (is_array($item)) {
          if (isset($item['html'])) {
            $data = $item['html'];
          }
          elseif (isset($item['safe'])) {
            $data = $item['safe'];
          }
          else {
            $data = $item['raw'];
          }
        }
        else {
          $data = $item;
        }
        $output .= "<li>" . $data . "</li>\n";
      }
      $output .= "</ul>\n";
    }
    if (!empty($element['findings']['pager'])) {
      $output .= $element['findings']['pager'];
    }
  }
  return $output;
}
function security_review_run_form($form_state, $checks = NULL) {
  $form['run_form'] = array(
    '#type' => 'fieldset',
    '#title' => t('Run'),
    '#description' => t('Click the button below to run the security checklist and review the results.'),
    '#collapsible' => TRUE,
    '#collapsed' => empty($checks) ? FALSE : TRUE,
  );
  $form['run_form']['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Run checklist'),
  );
  return $form;
}
function security_review_run_form_submit($form, &$form_state) {
  $checklist = module_invoke_all('security_checks');
  $skipped = security_review_skipped_checks();

  // Remove checks that are being skipped.
  if (!empty($skipped)) {
    foreach ($skipped as $module => $checks) {
      foreach ($checks as $check_name => $check) {
        unset($checklist[$module][$check_name]);
      }
      if (empty($checklist[$module])) {
        unset($checklist[$module]);
      }
    }
  }

  // Use Batch to process the checklist.
  $batch = array(
    'operations' => array(),
    'title' => t('Performing Security Review'),
    'progress_message' => t('Progress @current out of @total.'),
    'error_message' => t('An error occurred. Rerun the process or consult the logs.'),
    'finished' => '_security_review_batch_finished',
  );
  $log = variable_get('security_review_log', TRUE);
  foreach ($checklist as $module => $checks) {
    foreach ($checks as $check_name => $check) {

      // Each check is its own operation. There could be a case where a single
      // check needs to run itself a batch operation, perhaps @todo?
      $batch['operations'][] = array(
        '_security_review_batch_op',
        array(
          $module,
          $check_name,
          $check,
          $log,
        ),
      );
    }
  }
  batch_set($batch);
  return;
}

/**
 * Run the security review checklist and store the results.
 */
function security_review_run_store($checklist, $log = NULL) {

  // Allow callers like a drush command to decide not to log.
  if (is_null($log)) {
    $log = variable_get('security_review_log', TRUE);
  }

  // Perform the actual review.
  $results = _security_review_run($checklist, $log);

  // Store results and return.
  return security_review_store_results($results);
}

/**
 * Store checklist results.
 */
function security_review_store_results($results) {
  $log = variable_get('security_review_log', TRUE);

  // Store all results in the table.
  $saved = $to_save = 0;
  foreach ($results as $module => $checks) {
    foreach ($checks as $check_name => $check) {

      // Clear previous entry.
      db_query("DELETE FROM {security_review} WHERE namespace = '%s' AND reviewcheck = '%s'", $module, $check_name);
      if (db_affected_rows() && is_null($check['result']) && $log) {

        // Last check was deleted and current check returns null so check is
        // now no longer applicable.
        $message = '!name no longer applicable for checking';
        _security_review_log($module, $check_name, $message, array(
          '!name' => $check['title'],
        ), WATCHDOG_INFO);
      }
      elseif (!is_null($check['result'])) {
        $to_save++;
        $record = array(
          'namespace' => $module,
          'reviewcheck' => $check_name,
          'result' => $check['result'],
          'lastrun' => $check['lastrun'] ? $check['lastrun'] : time(),
        );
        if (drupal_write_record('security_review', $record) == SAVED_NEW) {
          $saved++;
        }
        elseif ($log) {
          _security_review_log($module, $check_name, 'Unable to store check !reviewcheck for !namespace', array(
            '!reviewcheck' => $check_name,
            '!namespace' => $module,
          ), WATCHDOG_ERROR);
        }
      }
    }
  }
  return $to_save == $saved ? TRUE : FALSE;
}

/**
 * Operation function called by Batch.
 */
function _security_review_batch_op($module, $check_name, $check, $log, &$context) {
  module_load_include('inc', 'security_review');
  $context['message'] = $check['title'];

  // Run the check.
  $check_result = _security_review_run_check($module, $check_name, $check, $log, TRUE);
  if (!empty($check_result)) {
    $context['results'][$module][$check_name] = $check_result;
  }
}

/**
 * Finished callback for Batch processing the checklist.
 */
function _security_review_batch_finished($success, $results, $operations) {
  module_load_include('inc', 'security_review');
  if ($success) {
    if (!empty($results)) {

      // Store results in our present table.
      $storage_result = security_review_store_results($results);
    }
    drupal_set_message(t('Review complete'));
  }
  else {
    $error_operation = reset($operations);
    $message = 'An error occurred while processing ' . $error_operation[0] . ' with arguments :' . print_r($error_operation[0], TRUE);
    _security_review_log('', '', $message, array(), WATCHDOG_ERROR);
    drupal_set_message(t('The review did not store all results, please run again or check the logs for details.'));
  }
}

/**
 * Implementation of hook_security_review_log().
 */
function security_review_security_review_log($module, $check_name, $message, $variables, $type) {

  // Log using watchdog().
  watchdog('security_review', $message, $variables, $type);
}

/**
 * Menu callback and Javascript callback for check skip toggling.
 */
function security_review_toggle_check($check_name) {
  global $user;
  module_load_include('inc', 'security_review');
  if (!drupal_valid_token($_GET['token'], $check_name)) {
    return drupal_access_denied();
  }
  $result = FALSE;

  // To be sure, we compare the user-provided check with available checks.
  $checklist = module_invoke_all('security_checks');
  foreach ($checklist as $module => $checks) {
    if (in_array($check_name, array_keys($checks))) {
      $sql = "SELECT namespace, reviewcheck, result, lastrun, skip, skiptime, skipuid FROM {security_review} WHERE namespace = '%s' AND reviewcheck = '%s'";
      $record = db_fetch_object(db_query($sql, $module, $check_name));

      // Toggle the skip.
      if ($record->skip) {

        // We were skipping, so stop skipping and clear skip identifiers.
        $record->skip = FALSE;
        $record->skiptime = 0;
        $record->skipuid = NULL;
        $message = '!name check no longer skipped';
      }
      else {

        // Start skipping and record who made the decision and when.
        $record->skip = TRUE;
        $record->skiptime = time();
        $record->skipuid = $user->uid;
        $message = '!name check skipped';
      }
      $result = drupal_write_record('security_review', $record, array(
        'namespace',
        'reviewcheck',
      ));

      // To log, or not to log?
      $log = variable_get('security_review_log', TRUE);
      if ($log) {
        $variables = array(
          '!name' => $checks[$check_name]['title'],
        );
        _security_review_log($module, $check_name, $message, $variables, WATCHDOG_INFO);
      }
      break;
    }
  }
  if (isset($_GET['js']) && intval($_GET['js']) == 1) {
    drupal_json($record);
    return;
  }

  // We weren't invoked via JS so set a message and return to the review page.
  drupal_set_message(t('Check will be skipped'));
  drupal_goto('admin/reports/security-review');
}

/**
 * Helper function returns skipped checks.
 */
function security_review_skipped_checks() {
  $skipped = array();
  $sql = "SELECT namespace, reviewcheck, result, lastrun, skip, skiptime, skipuid FROM {security_review} WHERE skip = 1";
  $results = db_query($sql);
  while ($record = db_fetch_array($results)) {
    $skipped[$record['namespace']][$record['reviewcheck']] = $record;
  }
  return $skipped;
}

/**
 * Helper function creates message for reporting check skip information.
 */
function _security_review_check_skipped($last_check) {
  $account = user_load(array(
    'uid' => $last_check['skipuid'],
  ));
  $time = format_date($last_check['skiptime'], 'medium');
  $message = t('Check marked for skipping on !time by !user', array(
    '!time' => $time,
    '!user' => theme('username', $account),
  ));
  return $message;
}

/**
 * Module settings form.
 */
function security_review_settings() {
  $checklist = module_invoke_all('security_checks');
  $roles = user_roles();
  foreach ($roles as $rid => $role) {
    $options[$rid] = check_plain($role);
  }
  $message = '';
  $defaults = security_review_default_untrusted_roles();
  if (in_array(DRUPAL_AUTHENTICATED_RID, $defaults)) {
    $message = 'You have allowed anonymous users to create accounts without approval so the authenticated role defaults to untrusted.';
  }
  $form['security_review_untrusted_roles'] = array(
    '#type' => 'checkboxes',
    '#title' => t('Untrusted roles'),
    '#description' => t('Mark which roles are not trusted. The anonymous role defaults to untrusted. @message Read more about the idea behind trusted and untrusted roles on <a href="!url">DrupalScout.com</a>. Most Security Review checks look for resources usable by untrusted roles.', array(
      '@message' => $message,
      '!url' => url('http://drupalscout.com/knowledge-base/importance-user-roles-and-permissions-site-security'),
    )),
    '#options' => $options,
    '#default_value' => variable_get('security_review_untrusted_roles', $defaults),
  );
  $form['security_review_adv'] = array(
    '#type' => 'fieldset',
    '#title' => t('Advanced'),
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
  );
  $form['security_review_adv']['security_review_log'] = array(
    '#type' => 'checkbox',
    '#title' => t('Log checklist results and skips'),
    '#description' => t('The result of each check and skip can be logged to watchdog for tracking.'),
    '#default_value' => variable_get('security_review_log', TRUE),
  );
  $options = $values = array();
  $skipped = security_review_skipped_checks();
  foreach ($checklist as $module => $checks) {
    foreach ($checks as $check_name => $check) {

      // Determine if check is being skipped.
      if (!empty($skipped) && array_key_exists($check_name, $skipped[$module])) {
        $values[] = $check_name;
        $label = t('!name <em>skipped by UID !uid on !date</em>', array(
          '!name' => $check['title'],
          '!uid' => $skipped[$module][$check_name]['skipuid'],
          '!date' => format_date($skipped[$module][$check_name]['skiptime']),
        ));
      }
      else {
        $label = $check['title'];
      }
      $options[$check_name] = $label;
    }
  }
  $form['security_review_adv']['security_review_skip'] = array(
    '#type' => 'checkboxes',
    '#title' => t('Checks to skip'),
    '#description' => t('Skip running certain checks. This can also be set on the <em>Run & review</em> page. It is recommended that you do not skip any checks unless you know the result is wrong or the process times out while running.'),
    '#options' => $options,
    '#default_value' => $values,
  );

  // Add a submit handler to set the skipped checks.
  $form['#submit'][] = '_security_review_settings_submit';
  return system_settings_form($form);
}
function _security_review_settings_submit($form, &$form_state) {
  global $user;
  $log = $form_state['values']['security_review_log'];

  // Set checked.
  $checklist = module_invoke_all('security_checks');
  $stored = array();
  $results = db_query("SELECT namespace, reviewcheck, result, lastrun, skip, skiptime, skipuid FROM {security_review}");
  while ($record = db_fetch_array($results)) {
    $stored[$record['namespace']][$record['reviewcheck']] = $record;
  }
  foreach ($checklist as $module => $checks) {
    foreach ($checks as $check_name => $check) {
      $record = new stdClass();
      $update = array();

      // Toggle the skip.
      if (isset($stored[$module][$check_name]) && $stored[$module][$check_name]['skip'] == 1 && $form_state['values']['security_review_skip'][$check_name] === 0) {

        // We were skipping, so stop skipping and clear skip identifiers.
        $record->namespace = $module;
        $record->reviewcheck = $check_name;
        $record->skip = FALSE;
        $record->skiptime = 0;
        $record->skipuid = NULL;
        $message = '!name check no longer skipped';
        $result = drupal_write_record('security_review', $record, array(
          'namespace',
          'reviewcheck',
        ));
        if ($log) {
          $variables = array(
            '!name' => $check['title'],
          );
          _security_review_log($module, $check_name, $message, $variables, WATCHDOG_INFO);
        }
      }
      elseif ($form_state['values']['security_review_skip'][$check_name] !== 0) {

        // Start skipping and record who made the decision and when.
        if (isset($stored[$module][$check_name])) {
          $update = array(
            'namespace',
            'reviewcheck',
          );
        }
        $record->namespace = $module;
        $record->reviewcheck = $check_name;
        $record->skip = TRUE;
        $record->skiptime = time();
        $record->skipuid = $user->uid;
        $message = '!name check skipped';
        $result = drupal_write_record('security_review', $record, $update);
        if ($log) {
          $variables = array(
            '!name' => $check['title'],
          );
          _security_review_log($module, $check_name, $message, $variables, WATCHDOG_INFO);
        }
      }
    }
  }

  // Unset security_review_skip to keep it from being written to a variable.
  unset($form_state['values']['security_review_skip']);
}

Functions

Namesort descending Description
security_review_check_help Page callback provides general help and check specific help documentation.
security_review_get_last_check Retrive the result from the last run of a security check.
security_review_menu Implementation of hook_menu().
security_review_page Page callback for run & review.
security_review_perm Implementation of hook_perm().
security_review_reviewed
security_review_run_form
security_review_run_form_submit
security_review_run_store Run the security review checklist and store the results.
security_review_security_checks Implementation of hook_security_checks().
security_review_security_review_log Implementation of hook_security_review_log().
security_review_settings Module settings form.
security_review_skipped_checks Helper function returns skipped checks.
security_review_store_results Store checklist results.
security_review_theme Implementation of hook_theme().
security_review_toggle_check Menu callback and Javascript callback for check skip toggling.
theme_security_review_check_help Theme function for help on a security check.
theme_security_review_reviewed
_security_review_batch_finished Finished callback for Batch processing the checklist.
_security_review_batch_op Operation function called by Batch.
_security_review_check_skipped Helper function creates message for reporting check skip information.
_security_review_settings_submit