From d942a35a26ed55ab92ba721dada68f3974af02af Mon Sep 17 00:00:00 2001 From: skodak Date: Mon, 29 Dec 2008 19:13:56 +0000 Subject: [PATCH] MDL-17222 Security overview report - not finished yet, work in progress --- admin/report/security/db/access.php | 36 + admin/report/security/index.php | 113 +++ admin/report/security/lib.php | 1031 +++++++++++++++++++++++++++ admin/report/security/settings.php | 3 + admin/report/security/version.php | 29 + lang/en_utf8/report_security.php | 138 ++++ theme/standard/styles_color.css | 11 + 7 files changed, 1361 insertions(+) create mode 100644 admin/report/security/db/access.php create mode 100644 admin/report/security/index.php create mode 100644 admin/report/security/lib.php create mode 100644 admin/report/security/settings.php create mode 100644 admin/report/security/version.php create mode 100644 lang/en_utf8/report_security.php diff --git a/admin/report/security/db/access.php b/admin/report/security/db/access.php new file mode 100644 index 0000000000..8534dd81e4 --- /dev/null +++ b/admin/report/security/db/access.php @@ -0,0 +1,36 @@ + array( + 'riskbitmask' => RISK_CONFIG, + 'captype' => 'read', + 'contextlevel' => CONTEXT_SYSTEM, + 'legacy' => array( + 'admin' => CAP_ALLOW + ), + ) +); diff --git a/admin/report/security/index.php b/admin/report/security/index.php new file mode 100644 index 0000000000..19b626f361 --- /dev/null +++ b/admin/report/security/index.php @@ -0,0 +1,113 @@ +dirroot.'/'.$CFG->admin.'/report/security/lib.php'); +require_once($CFG->libdir.'/adminlib.php'); + +require_login(); + +$issue = optional_param('issue', '', PARAM_ALPHANUMEXT); // show detailed info about one issue only + +$issues = report_security_get_issue_list(); + +// test if issue valid string +if (array_search($issue, $issues, true) === false) { + $issue = ''; +} + +// Print the header. +admin_externalpage_setup('reportsecurity'); +admin_externalpage_print_header(); + +print_heading(get_string('reportsecurity', 'report_security')); + +$strok = ''.get_string('statusok', 'report_security').''; +$strinfo = ''.get_string('statusinfo', 'report_security').''; +$strwarning = ''.get_string('statuswarning', 'report_security').''; +$strserious = ''.get_string('statusserious', 'report_security').''; +$strcritical = ''.get_string('statuscritical', 'report_security').''; + +$strissue = get_string('issue', 'report_security'); +$strstatus = get_string('status', 'report_security'); +$strdesc = get_string('description', 'report_security'); +$strconfig = get_string('configuration', 'report_security'); + +$statusarr = array(REPORT_SECURITY_OK => $strok, + REPORT_SECURITY_INFO => $strinfo, + REPORT_SECURITY_WARNING => $strwarning, + REPORT_SECURITY_SERIOUS => $strserious, + REPORT_SECURITY_CRITICAL => $strcritical); + +$url = "$CFG->wwwroot/$CFG->admin/report/security/index.php"; + +if ($issue and ($result = $issue(true))) { + $table = new object(); + $table->head = array($strissue, $strstatus, $strdesc, $strconfig); + $table->size = array('30%', '10%', '50%', '10%' ); + $table->align = array('left', 'left', 'left', 'left'); + $table->width = '90%'; + $table->data = array(); + + // print detail of one issue only + $row = array(); + $row[0] = $result->name; + $row[1] = $statusarr[$result->status]; + $row[2] = $result->info; + $row[3] = is_null($result->link) ? '-' : "$strconfig"; + + $table->data[] = $row; + + print_table($table); + + print_box($result->details, 'generalbox boxwidthnormal boxaligncenter'); // TODO: add proper css + + print_continue($url); + +} else { + $table = new object(); + $table->head = array($strissue, $strstatus, $strdesc); + $table->size = array('30%', '10%', '60%' ); + $table->align = array('left', 'left', 'left'); + $table->width = '90%'; + $table->data = array(); + + foreach ($issues as $issue) { + $result = $issue(false); + if (!$result) { + // ignore this test + continue; + } + $row = array(); + $row[0] = "$result->name"; + $row[1] = $statusarr[$result->status]; + $row[2] = $result->info; + + $table->data[] = $row; + } + print_table($table); +} + +print_footer(); \ No newline at end of file diff --git a/admin/report/security/lib.php b/admin/report/security/lib.php new file mode 100644 index 0000000000..7cb17c060f --- /dev/null +++ b/admin/report/security/lib.php @@ -0,0 +1,1031 @@ +libdir/adminlib.php"); + + +define('REPORT_SECURITY_OK', 'ok'); +define('REPORT_SECURITY_INFO', 'info'); +define('REPORT_SECURITY_WARNING', 'warning'); +define('REPORT_SECURITY_SERIOUS', 'serious'); +define('REPORT_SECURITY_CRITICAL', 'critical'); + + +function report_security_get_issue_list() { + return array( + 'report_security_check_globals', + 'report_security_check_unsecuredataroot', + 'report_security_check_displayerrors', + 'report_security_check_noauth', + 'report_security_check_embed', + 'report_security_check_mediafilterswf', + 'report_security_check_openprofiles', + 'report_security_check_google', + 'report_security_check_passwordpolicy', + 'report_security_check_emailchangeconfirmation', + 'report_security_check_cookiesecure', + 'report_security_check_configrw', + 'report_security_check_riskxss', + 'report_security_check_riskadmin', + 'report_security_check_defaultuserrole', + 'report_security_check_guestrole', + 'report_security_check_frontpagerole', + 'report_security_check_defaultcourserole', + 'report_security_check_courserole', + + ); +} + +///============================================= +/// Issue checks +///============================================= + + +/** + * Verifies register globals PHP setting. + * @param bool $detailed + * @return object result + */ +function report_security_check_globals($detailed=false) { + $result = new object(); + $result->issue = 'report_security_check_globals'; + $result->name = get_string('check_globals_name', 'report_security'); + $result->info = null; + $result->details = null; + $result->status = null; + $result->link = null; + + if (ini_get_bool('register_globals')) { + $result->status = REPORT_SECURITY_CRITICAL; + $result->info = get_string('check_globals_error', 'report_security'); + } else { + $result->status = REPORT_SECURITY_OK; + $result->info = get_string('check_globals_ok', 'report_security'); + } + + if ($detailed) { + $result->details = get_string('check_globals_details', 'report_security'); + } + + return $result; +} + +/** + * Verifies unsupported noauth setting + * @param bool $detailed + * @return object result + */ +function report_security_check_noauth($detailed=false) { + global $CFG; + + $result = new object(); + $result->issue = 'report_security_check_noauth'; + $result->name = get_string('check_noauth_name', 'report_security'); + $result->info = null; + $result->details = null; + $result->status = null; + $result->link = null; + $result->link = "$CFG->wwwroot/$CFG->admin/settings.php?section=manageauths"; + + if (is_enabled_auth('none')) { + $result->status = REPORT_SECURITY_CRITICAL; + $result->info = get_string('check_noauth_error', 'report_security'); + } else { + $result->status = REPORT_SECURITY_OK; + $result->info = get_string('check_noauth_ok', 'report_security'); + } + + if ($detailed) { + $result->details = get_string('check_noauth_details', 'report_security'); + } + + return $result; +} + +/** + * Verifies if password policy set + * @param bool $detailed + * @return object result + */ +function report_security_check_passwordpolicy($detailed=false) { + global $CFG; + + $result = new object(); + $result->issue = 'report_security_check_passwordpolicy'; + $result->name = get_string('check_passwordpolicy_name', 'report_security'); + $result->info = null; + $result->details = null; + $result->status = null; + $result->link = "$CFG->wwwroot/$CFG->admin/settings.php?section=sitepolicies"; + + if (empty($CFG->passwordpolicy)) { + $result->status = REPORT_SECURITY_WARNING; + $result->info = get_string('check_passwordpolicy_error', 'report_security'); + } else { + $result->status = REPORT_SECURITY_OK; + $result->info = get_string('check_passwordpolicy_ok', 'report_security'); + } + + if ($detailed) { + $result->details = get_string('check_passwordpolicy_details', 'report_security'); + } + + return $result; +} + +/** + * Verifies sloppy embedding - this should have been removed long ago!! + * @param bool $detailed + * @return object result + */ +function report_security_check_embed($detailed=false) { + global $CFG; + + $result = new object(); + $result->issue = 'report_security_check_embed'; + $result->name = get_string('check_embed_name', 'report_security'); + $result->info = null; + $result->details = null; + $result->status = null; + $result->link = "$CFG->wwwroot/$CFG->admin/settings.php?section=sitepolicies"; + + if (!empty($CFG->allowobjectembed)) { + $result->status = REPORT_SECURITY_CRITICAL; + $result->info = get_string('check_embed_error', 'report_security'); + } else { + $result->status = REPORT_SECURITY_OK; + $result->info = get_string('check_embed_ok', 'report_security'); + } + + if ($detailed) { + $result->details = get_string('check_embed_details', 'report_security'); + } + + return $result; +} + +/** + * Verifies sloppy swf embedding - this should have been removed long ago!! + * @param bool $detailed + * @return object result + */ +function report_security_check_mediafilterswf($detailed=false) { + global $CFG; + + $result = new object(); + $result->issue = 'report_security_check_mediafilterswf'; + $result->name = get_string('check_mediafilterswf_name', 'report_security'); + $result->info = null; + $result->details = null; + $result->status = null; + $result->link = "$CFG->wwwroot/$CFG->admin/settings.php?section=filtersettingfiltermediaplugin"; + + if (!empty($CFG->textfilters)) { + $activefilters = explode(',', $CFG->textfilters); + } else { + $activefilters = array(); + } + + if (array_search('filter/mediaplugin', $activefilters) !== false and !empty($CFG->filter_mediaplugin_enable_swf)) { + $result->status = REPORT_SECURITY_CRITICAL; + $result->info = get_string('check_mediafilterswf_error', 'report_security'); + } else { + $result->status = REPORT_SECURITY_OK; + $result->info = get_string('check_mediafilterswf_ok', 'report_security'); + } + + if ($detailed) { + $result->details = get_string('check_mediafilterswf_details', 'report_security'); + } + + return $result; +} + +/** + * Verifies fatal misconfiguration of dataroot + * @param bool $detailed + * @return object result + */ +function report_security_check_unsecuredataroot($detailed=false) { + global $CFG; + + $result = new object(); + $result->issue = 'report_security_check_unsecuredataroot'; + $result->name = get_string('check_unsecuredataroot_name', 'report_security'); + $result->info = null; + $result->details = null; + $result->status = null; + $result->link = null; + + $insecuredataroot = is_dataroot_insecure(true); + + if ($insecuredataroot == INSECURE_DATAROOT_WARNING) { + $result->status = REPORT_SECURITY_SERIOUS; + $result->info = get_string('check_unsecuredataroot_warning', 'report_security', $CFG->dataroot); + + } else if ($insecuredataroot == INSECURE_DATAROOT_ERROR) { + $result->status = REPORT_SECURITY_CRITICAL; + $result->info = get_string('check_unsecuredataroot_error', 'report_security', $CFG->dataroot); + + } else { + $result->status = REPORT_SECURITY_OK; + $result->info = get_string('check_unsecuredataroot_ok', 'report_security'); + } + + if ($detailed) { + $result->details = get_string('check_unsecuredataroot_details', 'report_security'); + } + + return $result; +} + +/** + * Verifies disaplying of errors - problem for lib files and 3rd party code + * because we can not disable debugging in these scripts (they do not include config.php) + * @param bool $detailed + * @return object result + */ +function report_security_check_displayerrors($detailed=false) { + $result = new object(); + $result->issue = 'report_security_check_displayerrors'; + $result->name = get_string('check_displayerrors_name', 'report_security'); + $result->info = null; + $result->details = null; + $result->status = null; + $result->link = null; + + if (defined('WARN_DISPLAY_ERRORS_ENABLED')) { + $result->status = REPORT_SECURITY_WARNING; + $result->info = get_string('check_displayerrors_error', 'report_security'); + } else { + $result->status = REPORT_SECURITY_OK; + $result->info = get_string('check_displayerrors_ok', 'report_security'); + } + + if ($detailed) { + $result->details = get_string('check_displayerrors_details', 'report_security'); + } + + return $result; +} + +/** + * Verifies open profiles - originaly open by default, not anymore because spammer abused it a lot + * @param bool $detailed + * @return object result + */ +function report_security_check_openprofiles($detailed=false) { + global $CFG; + + $result = new object(); + $result->issue = 'report_security_check_openprofiles'; + $result->name = get_string('check_openprofiles_name', 'report_security'); + $result->info = null; + $result->details = null; + $result->status = null; + $result->link = "$CFG->wwwroot/$CFG->admin/settings.php?section=sitepolicies"; + + if (empty($CFG->forcelogin) and empty($CFG->forceloginforprofiles)) { + $result->status = REPORT_SECURITY_WARNING; + $result->info = get_string('check_openprofiles_error', 'report_security'); + } else { + $result->status = REPORT_SECURITY_OK; + $result->info = get_string('check_openprofiles_ok', 'report_security'); + } + + if ($detailed) { + $result->details = get_string('check_openprofiles_details', 'report_security'); + } + + return $result; +} + +/** + * Verifies google access not combined with disabled guest access + * because attackers might gain guest access by modifying browser signature. + * @param bool $detailed + * @return object result + */ +function report_security_check_google($detailed=false) { + global $CFG; + + $result = new object(); + $result->issue = 'report_security_check_google'; + $result->name = get_string('check_google_name', 'report_security'); + $result->info = null; + $result->details = null; + $result->status = null; + $result->link = "$CFG->wwwroot/$CFG->admin/settings.php?section=sitepolicies"; + + if (empty($CFG->opentogoogle)) { + $result->status = REPORT_SECURITY_OK; + $result->info = get_string('check_google_ok', 'report_security'); + } else if (!empty($CFG->guestloginbutton)) { + $result->status = REPORT_SECURITY_INFO; + $result->info = get_string('check_google_info', 'report_security'); + } else { + $result->status = REPORT_SECURITY_SERIOUS; + $result->info = get_string('check_google_error', 'report_security'); + } + + if ($detailed) { + $result->details = get_string('check_google_details', 'report_security'); + } + + return $result; +} + +/** + * Verifies email confirmation - spammers were changing mails very often + * @param bool $detailed + * @return object result + */ +function report_security_check_emailchangeconfirmation($detailed=false) { + global $CFG; + + $result = new object(); + $result->issue = 'report_security_check_emailchangeconfirmation'; + $result->name = get_string('check_emailchangeconfirmation_name', 'report_security'); + $result->info = null; + $result->details = null; + $result->status = null; + $result->link = "$CFG->wwwroot/$CFG->admin/settings.php?section=sitepolicies"; + + if (empty($CFG->emailchangeconfirmation)) { + $result->status = REPORT_SECURITY_WARNING; + $result->info = get_string('check_emailchangeconfirmation_error', 'report_security'); + } else { + $result->status = REPORT_SECURITY_OK; + $result->info = get_string('check_emailchangeconfirmation_ok', 'report_security'); + } + + if ($detailed) { + $result->details = get_string('check_emailchangeconfirmation_details', 'report_security'); + } + + return $result; +} + +/** + * Verifies if https enabled only secure cookies allowed, + * this prevents redirections and sending of cookies to unsecure port. + * @param bool $detailed + * @return object result + */ +function report_security_check_cookiesecure($detailed=false) { + global $CFG; + + if (strpos($CFG->wwwroot, 'https://') !== 0) { + return null; + } + + $result = new object(); + $result->issue = 'report_security_check_cookiesecure'; + $result->name = get_string('check_cookiesecure_name', 'report_security'); + $result->info = null; + $result->details = null; + $result->status = null; + $result->link = "$CFG->wwwroot/$CFG->admin/settings.php?section=httpsecurity"; + + if (empty($CFG->cookiesecure)) { + $result->status = REPORT_SECURITY_SERIOUS; + $result->info = get_string('check_cookiesecure_error', 'report_security'); + } else { + $result->status = REPORT_SECURITY_OK; + $result->info = get_string('check_cookiesecure_ok', 'report_security'); + } + + if ($detailed) { + $result->details = get_string('check_cookiesecure_details', 'report_security'); + } + + return $result; +} + +/** + * Verifies config.php is not writable anymore after installation, + * config files were changed on several outdated server. + * @param bool $detailed + * @return object result + */ +function report_security_check_configrw($detailed=false) { + global $CFG; + + $result = new object(); + $result->issue = 'report_security_check_configrw'; + $result->name = get_string('check_configrw_name', 'report_security'); + $result->info = null; + $result->details = null; + $result->status = null; + $result->link = null; + + if (is_writable($CFG->dirroot.'/config.php')) { + $result->status = REPORT_SECURITY_WARNING; + $result->info = get_string('check_configrw_warning', 'report_security'); + } else { + $result->status = REPORT_SECURITY_OK; + $result->info = get_string('check_configrw_ok', 'report_security'); + } + + if ($detailed) { + $result->details = get_string('check_configrw_details', 'report_security'); + } + + return $result; +} + +/** + * Lists all users with XSS risk, it would be great to combine this with risk trusts in user table, + * unfortunately nobody implemented user trust UI yet :-( + * @param bool $detailed + * @return object result + */ +function report_security_check_riskxss($detailed=false) { + global $DB; + + $result = new object(); + $result->issue = 'report_security_check_riskxss'; + $result->name = get_string('check_riskxss_name', 'report_security'); + $result->info = null; + $result->details = null; + $result->status = REPORT_SECURITY_WARNING; + $result->link = null; + + $params = array('capallow'=>CAP_ALLOW); + + $sqlfrom = "FROM {role_capabilities} rc + JOIN {capabilities} cap ON cap.name = rc.capability + JOIN {context} c ON c.id = rc.contextid + JOIN {context} sc ON (sc.path = c.path OR sc.path LIKE ".$DB->sql_concat('c.path', "'/%'").") + JOIN {role_assignments} ra ON (ra.contextid = sc.id AND ra.roleid = rc.roleid) + JOIN {user} u ON u.id = ra.userid + WHERE ".$DB->sql_bitand('cap.riskbitmask', RISK_XSS)." + AND rc.permission = :capallow + AND u.deleted = 0"; + + $count = $DB->count_records_sql("SELECT COUNT(DISTINCT u.id) $sqlfrom", $params); + + $result->info = get_string('check_riskxss_warning', 'report_security', $count); + + if ($detailed) { + $users = $DB->get_records_sql("SELECT DISTINCT u.id, u.firstname, u.lastname, u.picture, u.imagealt $sqlfrom", $params); + foreach ($users as $uid=>$user) { + $users[$uid] = fullname($user); + } + $users = implode(', ', $users); + $result->details = get_string('check_riskxss_details', 'report_security', $users); + } + + return $result; +} + +/** + * Verifies sanity of default user role. + * @param bool $detailed + * @return object result + */ +function report_security_check_defaultuserrole($detailed=false) { + global $DB, $CFG; + + $result = new object(); + $result->issue = 'report_security_check_defaultuserrole'; + $result->name = get_string('check_defaultuserrole_name', 'report_security'); + $result->info = null; + $result->details = null; + $result->status = null; + $result->link = "$CFG->wwwroot/$CFG->admin/settings.php?section=userpolicies"; + + if (!$default_role = $DB->get_record('role', array('id'=>$CFG->defaultuserroleid))) { + $result->status = REPORT_SECURITY_WARNING; + $result->info = get_string('check_defaultuserrole_notset', 'report_security'); + $result->details = $result->info; + + return $result; + } + + // first test if do anything enabled - that would be really crazy! + $params = array('doanything'=>'moodle/site:doanything', 'capallow'=>CAP_ALLOW, 'roleid'=>$default_role->id); + $sql = "SELECT COUNT(DISTINCT rc.contextid) + FROM {role_capabilities} rc + WHERE rc.capability = :doanything + AND rc.permission = :capallow + AND rc.roleid = :roleid"; + + $anythingcount = $DB->count_records_sql($sql, $params); + + // risky caps - usually very dangerous + $params = array('capallow'=>CAP_ALLOW, 'roleid'=>$default_role->id); + $sql = "SELECT COUNT(DISTINCT rc.contextid) + FROM {role_capabilities} rc + JOIN {capabilities} cap ON cap.name = rc.capability + WHERE ".$DB->sql_bitand('cap.riskbitmask', (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))." + AND rc.permission = :capallow + AND rc.roleid = :roleid"; + + $riskycount = $DB->count_records_sql($sql, $params); + + // default role can not have view cap in all courses - this would break moodle badly + $viewcap = $DB->record_exists('role_capabilities', array('roleid'=>$default_role->id, 'permission'=>CAP_ALLOW, 'capability'=>'moodle/course:view')); + + // it may have either no or 'user' legacy type - nothing else, or else it would break during upgrades badly + $legacyok = false; + $params = array('capallow'=>CAP_ALLOW, 'roleid'=>$default_role->id, 'legacy'=>'moodle/legacy:%'); + $sql = "SELECT rc.capability, 1 + FROM {role_capabilities} rc + WHERE rc.capability LIKE :legacy + AND rc.permission = :capallow + AND rc.roleid = :roleid"; + $legacycaps = $DB->get_records_sql($sql, $params); + if (!$legacycaps) { + $legacyok = true; + } else if (count($legacycaps) == 1 and isset($legacycaps['moodle/legacy:user'])) { + $legacyok = true; + } + + if ($anythingcount or $riskycount or $viewcap or !$legacyok) { + $result->status = REPORT_SECURITY_CRITICAL; + $result->info = get_string('check_defaultuserrole_error', 'report_security', format_string($default_role->name)); + + } else { + $result->status = REPORT_SECURITY_OK; + $result->info = get_string('check_defaultuserrole_ok', 'report_security'); + } + + if ($detailed) { + $result->details = get_string('check_defaultuserrole_details', 'report_security'); + } + + return $result; +} + +/** + * Verifies sanity of guest role + * @param bool $detailed + * @return object result + */ +function report_security_check_guestrole($detailed=false) { + global $DB, $CFG; + + $result = new object(); + $result->issue = 'report_security_check_guestrole'; + $result->name = get_string('check_guestrole_name', 'report_security'); + $result->info = null; + $result->details = null; + $result->status = null; + $result->link = "$CFG->wwwroot/$CFG->admin/settings.php?section=userpolicies"; + + if (!$guest_role = $DB->get_record('role', array('id'=>$CFG->guestroleid))) { + $result->status = REPORT_SECURITY_WARNING; + $result->info = get_string('check_guestrole_notset', 'report_security'); + $result->details = $result->info; + + return $result; + } + + // first test if do anything enabled - that would be really crazy! + $params = array('doanything'=>'moodle/site:doanything', 'capallow'=>CAP_ALLOW, 'roleid'=>$guest_role->id); + $sql = "SELECT COUNT(DISTINCT rc.contextid) + FROM {role_capabilities} rc + WHERE rc.capability = :doanything + AND rc.permission = :capallow + AND rc.roleid = :roleid"; + + $anythingcount = $DB->count_records_sql($sql, $params); + + // risky caps - usually very dangerous + $params = array('capallow'=>CAP_ALLOW, 'roleid'=>$guest_role->id); + $sql = "SELECT COUNT(DISTINCT rc.contextid) + FROM {role_capabilities} rc + JOIN {capabilities} cap ON cap.name = rc.capability + WHERE ".$DB->sql_bitand('cap.riskbitmask', (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))." + AND rc.permission = :capallow + AND rc.roleid = :roleid"; + + $riskycount = $DB->count_records_sql($sql, $params); + + // it may have either no or 'guest' legacy type - nothing else, or else it would break during upgrades badly + $legacyok = false; + $params = array('capallow'=>CAP_ALLOW, 'roleid'=>$guest_role->id, 'legacy'=>'moodle/legacy:%'); + $sql = "SELECT rc.capability, 1 + FROM {role_capabilities} rc + WHERE rc.capability LIKE :legacy + AND rc.permission = :capallow + AND rc.roleid = :roleid"; + $legacycaps = $DB->get_records_sql($sql, $params); + if (!$legacycaps) { + $legacyok = true; + } else if (count($legacycaps) == 1 and isset($legacycaps['moodle/legacy:guest'])) { + $legacyok = true; + } + + if ($anythingcount or $riskycount or !$legacyok) { + $result->status = REPORT_SECURITY_CRITICAL; + $result->info = get_string('check_guestrole_error', 'report_security', format_string($guest_role->name)); + + } else { + $result->status = REPORT_SECURITY_OK; + $result->info = get_string('check_guestrole_ok', 'report_security'); + } + + if ($detailed) { + $result->details = get_string('check_guestrole_details', 'report_security'); + } + + return $result; +} + +/** + * Verifies sanity of frontpage role + * @param bool $detailed + * @return object result + */ +function report_security_check_frontpagerole($detailed=false) { + global $DB, $CFG; + + $result = new object(); + $result->issue = 'report_security_check_frontpagerole'; + $result->name = get_string('check_frontpagerole_name', 'report_security'); + $result->info = null; + $result->details = null; + $result->status = null; + $result->link = "$CFG->wwwroot/$CFG->admin/settings.php?section=frontpagesettings"; + + if (!$frontpage_role = $DB->get_record('role', array('id'=>$CFG->defaultfrontpageroleid))) { + $result->status = REPORT_SECURITY_INFO; + $result->info = get_string('check_frontpagerole_notset', 'report_security'); + $result->details = get_string('check_frontpagerole_details', 'report_security'); + + return $result; + } + + // first test if do anything enabled - that would be really crazy! + $params = array('doanything'=>'moodle/site:doanything', 'capallow'=>CAP_ALLOW, 'roleid'=>$frontpage_role->id); + $sql = "SELECT COUNT(DISTINCT rc.contextid) + FROM {role_capabilities} rc + WHERE rc.capability = :doanything + AND rc.permission = :capallow + AND rc.roleid = :roleid"; + + $anythingcount = $DB->count_records_sql($sql, $params); + + // risky caps - usually very dangerous + $params = array('capallow'=>CAP_ALLOW, 'roleid'=>$frontpage_role->id); + $sql = "SELECT COUNT(DISTINCT rc.contextid) + FROM {role_capabilities} rc + JOIN {capabilities} cap ON cap.name = rc.capability + WHERE ".$DB->sql_bitand('cap.riskbitmask', (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))." + AND rc.permission = :capallow + AND rc.roleid = :roleid"; + + $riskycount = $DB->count_records_sql($sql, $params); + + // there is no legacy role type for frontpage yet - anyway we can not allow teachers or admins there! + $params = array('capallow'=>CAP_ALLOW, 'roleid'=>$frontpage_role->id, 'legacy'=>'moodle/legacy:%'); + $sql = "SELECT rc.capability, 1 + FROM {role_capabilities} rc + WHERE rc.capability LIKE :legacy + AND rc.permission = :capallow + AND rc.roleid = :roleid"; + $legacycaps = $DB->get_records_sql($sql, $params); + $legacyok = (!isset($legacycaps['moodle/legacy:teacher']) + and !isset($legacycaps['moodle/legacy:editingteacher']) + and !isset($legacycaps['moodle/legacy:coursecreator']) + and !isset($legacycaps['moodle/legacy:admin'])); + + if ($anythingcount or $riskycount or !$legacyok) { + $result->status = REPORT_SECURITY_CRITICAL; + $result->info = get_string('check_frontpagerole_error', 'report_security', format_string($frontpage_role->name)); + + } else { + $result->status = REPORT_SECURITY_OK; + $result->info = get_string('check_frontpagerole_ok', 'report_security'); + } + + if ($detailed) { + $result->details = get_string('check_frontpagerole_details', 'report_security'); + } + + return $result; +} + +/** + * Verifies sanity of site default course role. + * @param bool $detailed + * @return object result + */ +function report_security_check_defaultcourserole($detailed=false) { + global $DB, $CFG; + + $problems = array(); + + $result = new object(); + $result->issue = 'report_security_check_defaultcourserole'; + $result->name = get_string('check_defaultcourserole_name', 'report_security'); + $result->info = null; + $result->details = null; + $result->status = null; + $result->link = "$CFG->wwwroot/$CFG->admin/settings.php?section=userpolicies"; + + if ($detailed) { + $result->details = get_string('check_defaultcourserole_details', 'report_security'); + } + + if (!$student_role = $DB->get_record('role', array('id'=>$CFG->defaultcourseroleid))) { + $result->status = REPORT_SECURITY_WARNING; + $result->info = get_string('check_defaultcourserole_notset', 'report_security'); + $result->details = get_string('check_defaultcourserole_details', 'report_security'); + + return $result; + } + + // first test if do anything enabled - that would be really crazy! + $params = array('doanything'=>'moodle/site:doanything', 'capallow'=>CAP_ALLOW, 'roleid'=>$student_role->id); + $sql = "SELECT DISTINCT rc.contextid + FROM {role_capabilities} rc + WHERE rc.capability = :doanything + AND rc.permission = :capallow + AND rc.roleid = :roleid"; + + if ($anything_contexts = $DB->get_records_sql($sql, $params)) { + foreach($anything_contexts as $contextid) { + if ($contextid == SYSCONTEXTID) { + $a = "$CFG->wwwroot/$CFG->admin/roles/define.php?action=view&roleid=$CFG->defaultcourseroleid"; + } else { + $a = "$CFG->wwwroot/$CFG->admin/roles/override.php?contextid=$contextid&roleid=$CFG->defaultcourseroleid"; + } + $problems[] = get_string('check_defaultcourserole_anything', 'report_security', $a); + } + } + + // risky caps - usually very dangerous + $params = array('capallow'=>CAP_ALLOW, 'roleid'=>$student_role->id); + $sql = "SELECT DISTINCT rc.contextid + FROM {role_capabilities} rc + JOIN {capabilities} cap ON cap.name = rc.capability + WHERE ".$DB->sql_bitand('cap.riskbitmask', (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))." + AND rc.permission = :capallow + AND rc.roleid = :roleid"; + + if ($riskycontexts = $DB->get_records_sql($sql, $params)) { + foreach($riskycontexts as $contextid=>$unused) { + if ($contextid == SYSCONTEXTID) { + $a = "$CFG->wwwroot/$CFG->admin/roles/define.php?action=view&roleid=$CFG->defaultcourseroleid"; + } else { + $a = "$CFG->wwwroot/$CFG->admin/roles/override.php?contextid=$contextid&roleid=$CFG->defaultcourseroleid"; + } + $problems[] = get_string('check_defaultcourserole_risky', 'report_security', $a); + } + } + + // course creator or administrator does not make any sense here + $params = array('capallow'=>CAP_ALLOW, 'roleid'=>$student_role->id, 'legacy'=>'moodle/legacy:%'); + $sql = "SELECT rc.capability, 1 + FROM {role_capabilities} rc + WHERE rc.capability LIKE :legacy + AND rc.permission = :capallow + AND rc.roleid = :roleid"; + $legacycaps = $DB->get_records_sql($sql, $params); + if (isset($legacycaps['moodle/legacy:coursecreator']) or isset($legacycaps['moodle/legacy:admin'])) { + $problems[] = get_string('check_defaultcourserole_legacy', 'report_security'); + } + + if ($problems) { + $result->status = REPORT_SECURITY_CRITICAL; + $result->info = get_string('check_defaultcourserole_error', 'report_security', format_string($student_role->name)); + if ($detailed) { + $result->details .= ""; + } + + } else { + $result->status = REPORT_SECURITY_OK; + $result->info = get_string('check_defaultcourserole_ok', 'report_security'); + } + + return $result; +} + +/** + * Verifies sanity of default roles in courses. + * @param bool $detailed + * @return object result + */ +function report_security_check_courserole($detailed=false) { + global $DB, $CFG, $SITE; + + $problems = array(); + + $result = new object(); + $result->issue = 'report_security_check_courserole'; + $result->name = get_string('check_courserole_name', 'report_security'); + $result->info = null; + $result->details = null; + $result->status = null; + $result->link = null; + + if ($detailed) { + $result->details = get_string('check_courserole_details', 'report_security'); + } + + // get list of all student roles selected in courses excluding the default course role + $params = array('siteid'=>$SITE->id, 'defaultcourserole'=>$CFG->defaultcourseroleid); + $sql = "SELECT r.* + FROM {role} r + JOIN {course} c ON c.defaultrole = r.id + WHERE c.id <> :siteid AND r.id <> :defaultcourserole"; + + if (!$student_roles = $DB->get_records_sql($sql, $params)) { + $result->status = REPORT_SECURITY_OK; + $result->info = get_string('check_courserole_notyet', 'report_security'); + $result->details = get_string('check_courserole_details', 'report_security'); + + return $result; + } + + $roleids = array_keys($student_roles); + + // first test if do anything enabled - that would be really crazy!!!!!! + list($inroles, $params) = $DB->get_in_or_equal($roleids, SQL_PARAMS_NAMED, 'r0', true); + $params = array_merge($params, array('doanything'=>'moodle/site:doanything', 'capallow'=>CAP_ALLOW)); + $params['doanything'] = 'moodle/site:doanything'; + $params['capallow'] = CAP_ALLOW; + $sql = "SELECT rc.roleid, rc.contextid + FROM {role_capabilities} rc + WHERE rc.capability = :doanything + AND rc.permission = :capallow + AND rc.roleid $inroles + GROUP BY rc.roleid, rc.contextid + ORDER BY rc.roleid, rc.contextid"; + + $rs = $DB->get_recordset_sql($sql, $params); + foreach($rs as $res) { + $roleid = $res->roleid; + $contextid = $res->contextid; + if ($contextid == SYSCONTEXTID) { + $a = "$CFG->wwwroot/$CFG->admin/roles/define.php?action=view&roleid=$roleid"; + } else { + $a = "$CFG->wwwroot/$CFG->admin/roles/override.php?contextid=$contextid&roleid=$roleid"; + } + $problems[] = get_string('check_courserole_anything', 'report_security', $a); + } + $rs->close(); + + // risky caps in any level - usually very dangerous!! + list($inroles, $params) = $DB->get_in_or_equal($roleids, SQL_PARAMS_NAMED, 'r0', true); + $params = array_merge($params, array('capallow'=>CAP_ALLOW)); + $sql = "SELECT rc.roleid, rc.contextid + FROM {role_capabilities} rc + JOIN {capabilities} cap ON cap.name = rc.capability + WHERE ".$DB->sql_bitand('cap.riskbitmask', (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))." + AND rc.permission = :capallow + AND rc.roleid $inroles + GROUP BY rc.roleid, rc.contextid + ORDER BY rc.roleid, rc.contextid"; + $rs = $DB->get_recordset_sql($sql, $params); + foreach($rs as $res) { + $roleid = $res->roleid; + $contextid = $res->contextid; + if ($contextid == SYSCONTEXTID) { + $a = "$CFG->wwwroot/$CFG->admin/roles/define.php?action=view&roleid=$roleid"; + } else { + $a = "$CFG->wwwroot/$CFG->admin/roles/override.php?contextid=$contextid&roleid=$roleid"; + } + $problems[] = get_string('check_courserole_risky', 'report_security', $a); + } + $rs->close(); + + // course creator or administrator does not make any sense here! + list($inroles, $params) = $DB->get_in_or_equal($roleids, SQL_PARAMS_NAMED, 'r0', true); + $params = array_merge($params, array('capallow'=>CAP_ALLOW, 'creator'=>'moodle/legacy:coursecreator', 'admin'=>'moodle/legacy:admin')); + $sql = "SELECT DISTINCT rc.roleid + FROM {role_capabilities} rc + WHERE (rc.capability = :creator OR rc.capability = :admin) + AND rc.permission = :capallow + AND rc.roleid $inroles"; + if ($legacys = $DB->get_records_sql($sql, $params)) { + foreach ($legacys as $roleid=>$unused) { + $a = "$CFG->wwwroot/$CFG->admin/roles/define.php?action=view&roleid=$roleid"; + $problems[] = get_string('check_defaultcourserole_legacy', 'report_security', $a); + } + } + + + if ($problems) { + $result->status = REPORT_SECURITY_CRITICAL; + $result->info = get_string('check_courserole_error', 'report_security'); + if ($detailed) { + $result->details .= ""; + } + + } else { + $result->status = REPORT_SECURITY_OK; + $result->info = get_string('check_courserole_ok', 'report_security'); + } + + return $result; +} + +/** + * Lists all admins. + * @param bool $detailed + * @return object result + */ +function report_security_check_riskadmin($detailed=false) { + global $DB; + + $result = new object(); + $result->issue = 'report_security_check_riskadmin'; + $result->name = get_string('check_riskadmin_name', 'report_security'); + $result->info = null; + $result->details = null; + $result->status = null; + $result->link = null; + + $params = array('doanything'=>'moodle/site:doanything', 'syscontextid'=>SYSCONTEXTID, 'capallow'=>CAP_ALLOW); + + $sql = "SELECT DISTINCT u.id, u.firstname, u.lastname, u.picture, u.imagealt + FROM {role_capabilities} rc + JOIN {role_assignments} ra ON (ra.contextid = rc.contextid AND ra.roleid = rc.roleid) + JOIN {user} u ON u.id = ra.userid + WHERE rc.capability = :doanything + AND rc.permission = :capallow + AND u.deleted = 0 + AND rc.contextid = :syscontextid"; + + $admins = $DB->get_records_sql($sql, $params); + + $sqlfrom = "FROM {role_capabilities} rc + JOIN {context} c ON c.id = rc.contextid + JOIN {context} sc ON (sc.path = c.path OR sc.path LIKE ".$DB->sql_concat('c.path', "'/%'").") + JOIN {role_assignments} ra ON (ra.contextid = sc.id AND ra.roleid = rc.roleid) + JOIN {user} u ON u.id = ra.userid + WHERE rc.capability = :doanything + AND rc.permission = :capallow + AND u.deleted = 0 + AND ra.contextid <> :syscontextid"; + + $count = $DB->count_records_sql("SELECT COUNT(DISTINCT u.id) $sqlfrom", $params); + + if (!$count) { + $result->status = REPORT_SECURITY_OK; + $result->info = get_string('check_riskadmin_ok', 'report_security', count($admins)); + + if ($detailed) { + foreach ($admins as $uid=>$user) { + $admins[$uid] = fullname($user); + } + $admins = implode(', ', $admins); + $result->details = get_string('check_riskadmin_detailsok', 'report_security', $admins); + } + + } else { + $result->status = REPORT_SECURITY_WARNING; + $a = (object)array('admincount'=>count($admins), 'unsupcount'=>$count); + $result->info = get_string('check_riskadmin_warning', 'report_security', $a); + + if ($detailed) { + foreach ($admins as $uid=>$user) { + $admins[$uid] = fullname($user); + } + $admins = implode(', ', $admins); + $users = $DB->get_records_sql("SELECT DISTINCT u.id, u.firstname, u.lastname, u.picture, u.imagealt $sqlfrom", $params); + foreach ($users as $uid=>$user) { + $users[$uid] = fullname($user); + } + $users = implode(', ', $users); + $a = (object)array('admins'=>$admins, 'unsupported'=>$users); + $result->details = get_string('check_riskadmin_detailswarning', 'report_security', $a); + } + } + + return $result; +} diff --git a/admin/report/security/settings.php b/admin/report/security/settings.php new file mode 100644 index 0000000000..71bf1488f1 --- /dev/null +++ b/admin/report/security/settings.php @@ -0,0 +1,3 @@ +add('reports', new admin_externalpage('reportsecurity', get_string('reportsecurity', 'report_security'), "$CFG->wwwroot/$CFG->admin/report/security/index.php",'report/security:view')); diff --git a/admin/report/security/version.php b/admin/report/security/version.php new file mode 100644 index 0000000000..181e9afd36 --- /dev/null +++ b/admin/report/security/version.php @@ -0,0 +1,29 @@ +version = 2008122900; +$plugin->requires = 2008121701; + +?> diff --git a/lang/en_utf8/report_security.php b/lang/en_utf8/report_security.php new file mode 100644 index 0000000000..b853ecc2a2 --- /dev/null +++ b/lang/en_utf8/report_security.php @@ -0,0 +1,138 @@ +It is recommended to change file permissions of config.php script after installation so that the file can not be modified by web server. +Please note that this measure does not improve security of the server significantly, but on the other hand it might slow down or limit general exploits.

'; +$string['check_configrw_name'] = 'Writable config.php'; +$string['check_configrw_ok'] = 'config.php can not be modified by PHP scripts.'; +$string['check_configrw_warning'] = 'PHP scripts may modify config.php.'; + +$string['check_cookiesecure_details'] = '

If you enable https communication it is recommended to enable secure cookies. You should also add permanent redirection from http to https.

'; +$string['check_cookiesecure_error'] = 'Please enable secure cookies'; +$string['check_cookiesecure_name'] = 'Secure cookies'; +$string['check_cookiesecure_ok'] = 'Secure cookies enabled.'; + +$string['check_courserole_anything'] = 'Do anything capability must not be allowed in this context.'; +$string['check_courserole_details'] = '

Each course has one default enrolment role specified. Please make sure no risky capabilities are allowed in this role.

+

The only supported legacy type for course default role is Student.

'; +$string['check_courserole_error'] = 'Incorrectly defined course default roles detected!'; +$string['check_courserole_legacy'] = 'Unsupported legacy type detected in role.'; +$string['check_courserole_name'] = 'Course default roles'; +$string['check_courserole_notyet'] = 'Used only default course role.'; +$string['check_courserole_ok'] = 'Course default role definitions ok.'; +$string['check_courserole_risky'] = 'Risky capabilities detected in context.'; + +$string['check_defaultcourserole_anything'] = 'Do anything capability must not be allowed in this context.'; +$string['check_defaultcourserole_details'] = '

Default student role for course enrolment specifies the default role for courses. Please make sure no risky capabilities are allowed in this role.

+

The only supported legacy type for default role is Student.

'; +$string['check_defaultcourserole_error'] = 'Incorrectly defined default course role \"$a\" detected!'; +$string['check_defaultcourserole_legacy'] = 'Unsupported legacy type detected.'; +$string['check_defaultcourserole_name'] = 'Site default course role'; +$string['check_defaultcourserole_notset'] = 'Default role is not set.'; +$string['check_defaultcourserole_ok'] = 'Site default role definition ok.'; +$string['check_defaultcourserole_risky'] = 'Risky capabilities detected in context.'; + +$string['check_defaultuserrole_details'] = '

All logged in users are given capabilities of the default user role. Please make sure no risky capabilities are allowed in this role.

+

The only supported legacy type for default user role is Authenticated user. Course view capability must not be enabled.

'; +$string['check_defaultuserrole_error'] = 'Incorrectly defined default user role \"$a\" detected!'; +$string['check_defaultuserrole_name'] = 'Registered user role'; +$string['check_defaultuserrole_notset'] = 'Default role is not set.'; +$string['check_defaultuserrole_ok'] = 'Registered user role definition ok.'; + +$string['check_displayerrors_details'] = '

Enabling the PHP setting display_errors is not recommended on production sites because some error messages may reveal sensitive information about your server.

'; +$string['check_displayerrors_error'] = 'PHP errors displaying is enabled. It is recommended to disable displaying of errors in PHP configuration.'; +$string['check_displayerrors_name'] = 'Displaying of PHP errors'; +$string['check_displayerrors_ok'] = 'Displaying of PHP errors disabled.'; + +$string['check_emailchangeconfirmation_details'] = '

It is recommended to require email confirmation step when user enters a new email address in user profile. If disabled spammers might try to exploit server for resending of spam.

'; +$string['check_emailchangeconfirmation_error'] = 'Users may enter any email address.'; +$string['check_emailchangeconfirmation_name'] = 'Email change confirmation'; +$string['check_emailchangeconfirmation_ok'] = 'Changing of email must be confirmed.'; + +$string['check_embed_details'] = '

Unlimited object embedding is very dangerous - any registered user may launch XSS attack against other server users. Please disable it on production servers.

'; +$string['check_embed_error'] = 'Unlimited object embedding enabled - this is very dangerous for majority of servers.'; +$string['check_embed_name'] = 'Allow EMBED and OBJECT'; +$string['check_embed_ok'] = 'Unlimited object embedding not allowed.'; + +$string['check_frontpagerole_details'] = '

Frontpage role is give to all registered users on frontpage. Please make sure no risky capabilities are allowed in this role.

+

It is recommended to create a special role only for this purpose and not set any legacy type.

'; +$string['check_frontpagerole_error'] = 'Incorrectly defined frontpage role \"$a\" detected!'; +$string['check_frontpagerole_name'] = 'Frontpage role'; +$string['check_frontpagerole_notset'] = 'Frontpage role is not set.'; +$string['check_frontpagerole_ok'] = 'Frontpage role definition ok.'; + +$string['check_globals_details'] = '

Register globals is considered to be a highly insecure PHP setting, there is no reason why it should be enabled. Moodle is not compatible with register globals.

+

register_globals=off must be set in PHP configuration. This setting is controlled by editing your php.ini, Apache/IIS configuration or .htaccess file.

'; +$string['check_globals_error'] = 'Register globals MUST be disabled. Please fix server PHP settings immediately!'; +$string['check_globals_name'] = 'Register globals'; +$string['check_globals_ok'] = 'Register globals are disabled.'; + +$string['check_google_details'] = '

Open to Google settings helps search engines enter courses with guest access. Please note this settings is not expected to be enabled if guest login not allowed.

'; +$string['check_google_error'] = 'Search engines guest access allowed and guest access disabled.'; +$string['check_google_info'] = 'Search engines may enter as guests.'; +$string['check_google_name'] = 'Open to Google'; +$string['check_google_ok'] = 'Search engines guest access not enabled.'; + +$string['check_guestrole_details'] = '

Guest role is used for guests, not logged in users and temporary guest course access. Please make sure no risky capabilities are allowed in this role.

+

The only supported legacy type for guest role is Guest.

'; +$string['check_guestrole_error'] = 'Incorrectly defined guest role \"$a\" detected!'; +$string['check_guestrole_name'] = 'Guest role'; +$string['check_guestrole_notset'] = 'Guest role is not set.'; +$string['check_guestrole_ok'] = 'Guest role definition ok.'; + +$string['check_mediafilterswf_details'] = '

Automatic swf embedding is very dangerous - any registered user may launch XSS attack against other server users. Please disable it on production servers.

'; +$string['check_mediafilterswf_error'] = 'Flash media filter is enabled - this is very dangerous for majority of servers.'; +$string['check_mediafilterswf_name'] = 'Enabled .swf media filter'; +$string['check_mediafilterswf_ok'] = 'Flash media filter is not enabled.'; + +$string['check_noauth_details'] = '

No authentication plugin is not intended for any production sites. Please disable it unless this is a development test site.

'; +$string['check_noauth_error'] = 'No authentication pluing can not be used on production sites.'; +$string['check_noauth_name'] = 'No authentication'; +$string['check_noauth_ok'] = 'No authentication plugin is disabled.'; + +$string['check_openprofiles_details'] = '

Open user profiles are often abused by spammers, it is usually recommended to enable Force users to login for profiles or Force users to login if you require login before any access.

'; +$string['check_openprofiles_error'] = 'Anybody may view user profiles without logging in.'; +$string['check_openprofiles_name'] = 'Open user profiles'; +$string['check_openprofiles_ok'] = 'Login is required before viewing user profile.'; + +$string['check_passwordpolicy_details'] = '

It is recommended to enforce user password policy because password guessing is very often the easiest way to gain unauthorised access. +Do not make the requirements too strict, because users would not be able to remember their passwords and would keep forgetting them or write them down.

'; +$string['check_passwordpolicy_error'] = 'Password policy not set.'; +$string['check_passwordpolicy_name'] = 'Password policy'; +$string['check_passwordpolicy_ok'] = 'Password policy enabled.'; + +$string['check_riskadmin_detailsok'] = '

Please verify following list of administrators.
$a

'; +$string['check_riskadmin_detailswarning'] = '

Please verify following list of administrators:
$a->admins

+

It is recommended to assign administrator role in system context only. Following users have unsuported admin role assignments:
$a->unsupported

'; +$string['check_riskadmin_name'] = 'Administrators'; +$string['check_riskadmin_ok'] = 'Found $a server administrators.'; +$string['check_riskadmin_warning'] = 'Found $a->admincount server administrators and $a->unsupcount unsuported admin role assignments.'; + +$string['check_riskxss_details'] = '

RISK_XSS marks all dangerous capabilities that only trusted users may use.

+

Please verify following list of users and make sure that you trust them completely on this server:
$a

'; +$string['check_riskxss_name'] = 'XSS trusted users'; +$string['check_riskxss_warning'] = 'RISK_XSS - found $a users that have to be trusted.'; + +$string['check_unsecuredataroot_details'] = '

Dataroot directory must not be accessible via web. The best way to make sure the directory is not accessible is to use directory outside of public web directory.

+

If you move the directory you need to update \$CFG->dataroot setting in config.php accordingly.

'; +$string['check_unsecuredataroot_error'] = 'Your dataroot directory $a is in the wrong location and is exposed to the web!'; +$string['check_unsecuredataroot_name'] = 'Unsecure dataroot'; +$string['check_unsecuredataroot_ok'] = 'Dataroot directory must not be accessible via web.'; +$string['check_unsecuredataroot_warning'] = 'Your dataroot directory $a is in the wrong location and might be exposed to the web.'; + +?> diff --git a/theme/standard/styles_color.css b/theme/standard/styles_color.css index f464a9205a..d5115443f9 100644 --- a/theme/standard/styles_color.css +++ b/theme/standard/styles_color.css @@ -322,6 +322,17 @@ table.flexible .r1 { background-color: green; } +#admin-report-security-index .statuswarning { + background-color: #f0e000; +} + +#admin-report-security-index .statusserious { + background-color: #f07000; +} + +#admin-report-security-index .statuscritical { + background-color: #f00000; +} .plugincompattable td.ok { color: #008000; -- 2.39.5