Add security check (#7247)

* added Parse Server security option

* added SecurityRouter

* added Check class

* added CheckGroup class

* moved parameter validation to Utils

* added CheckRunner class

* added auto-run on server start

* added custom security checks as Parse Server option

* renamed script to check

* reformat log output

* added server config check

* improved contributing guideline

* improved contribution guide

* added check security log

* improved log format

* added checks

* fixed log fomat typo

* added database checks

* fixed database check

* removed database auth check in initial version

* improved contribution guide

* added security check tests

* fixed typo

* improved wording guidelines

* improved wording guidelines
This commit is contained in:
Manuel
2021-03-10 20:19:28 +01:00
committed by GitHub
parent 36c2608400
commit bee889a329
17 changed files with 1096 additions and 2 deletions

205
src/Security/CheckRunner.js Normal file
View File

@@ -0,0 +1,205 @@
/**
* @module SecurityCheck
*/
import Utils from '../Utils';
import { CheckState } from './Check';
import * as CheckGroups from './CheckGroups/CheckGroups';
import logger from '../logger';
import { isArray, isBoolean } from 'lodash';
/**
* The security check runner.
*/
class CheckRunner {
/**
* The security check runner.
* @param {Object} [config] The configuration options.
* @param {Boolean} [config.enableCheck=false] Is true if Parse Server should report weak security settings.
* @param {Boolean} [config.enableCheckLog=false] Is true if the security check report should be written to logs.
* @param {Object} [config.checkGroups] The check groups to run. Default are the groups defined in `./CheckGroups/CheckGroups.js`.
*/
constructor(config = {}) {
this._validateParams(config);
const { enableCheck = false, enableCheckLog = false, checkGroups = CheckGroups } = config;
this.enableCheck = enableCheck;
this.enableCheckLog = enableCheckLog;
this.checkGroups = checkGroups;
}
/**
* Runs all security checks and returns the results.
* @params
* @returns {Object} The security check report.
*/
async run({ version = '1.0.0' } = {}) {
// Instantiate check groups
const groups = Object.values(this.checkGroups)
.filter(c => typeof c === 'function')
.map(CheckGroup => new CheckGroup());
// Run checks
groups.forEach(group => group.run());
// Generate JSON report
const report = this._generateReport({ groups, version });
// If report should be written to logs
if (this.enableCheckLog) {
this._logReport(report)
}
return report;
}
/**
* Generates a security check report in JSON format with schema:
* ```
* {
* report: {
* version: "1.0.0", // The report version, defines the schema
* state: "fail" // The disjunctive indicator of failed checks in all groups.
* groups: [ // The check groups
* {
* name: "House", // The group name
* state: "fail" // The disjunctive indicator of failed checks in this group.
* checks: [ // The checks
* title: "Door locked", // The check title
* state: "fail" // The check state
* warning: "Anyone can enter your house." // The warning.
* solution: "Lock your door." // The solution.
* ]
* },
* ...
* ]
* }
* }
* ```
* @param {Object} params The parameters.
* @param {Array<CheckGroup>} params.groups The check groups.
* @param {String} params.version: The report schema version.
* @returns {Object} The report.
*/
_generateReport({ groups, version }) {
// Create report template
const report = {
report: {
version,
state: CheckState.success,
groups: []
}
};
// Identify report version
switch (version) {
case '1.0.0':
default:
// For each check group
for (const group of groups) {
// Create group report
const groupReport = {
name: group.name(),
state: CheckState.success,
checks: [],
}
// Create check reports
groupReport.checks = group.checks().map(check => {
const checkReport = {
title: check.title,
state: check.checkState(),
};
if (check.checkState() == CheckState.fail) {
checkReport.warning = check.warning;
checkReport.solution = check.solution;
report.report.state = CheckState.fail;
groupReport.state = CheckState.fail;
}
return checkReport;
});
report.report.groups.push(groupReport);
}
}
return report;
}
/**
* Logs the security check report.
* @param {Object} report The report to log.
*/
_logReport(report) {
// Determine log level depending on whether any check failed
const log = report.report.state == CheckState.success ? (s) => logger.info(s) : (s) => logger.warn(s);
// Declare output
const indent = ' ';
let output = '';
let checksCount = 0;
let failedChecksCount = 0;
let skippedCheckCount = 0;
// Traverse all groups and checks for compose output
for (const group of report.report.groups) {
output += `\n- ${group.name}`
for (const check of group.checks) {
checksCount++;
output += `\n${indent}${this._getLogIconForState(check.state)} ${check.title}`;
if (check.state == CheckState.fail) {
failedChecksCount++;
output += `\n${indent}${indent}Warning: ${check.warning}`;
output += ` ${check.solution}`;
} else if (check.state == CheckState.none) {
skippedCheckCount++;
output += `\n${indent}${indent}Test did not execute, this is likely an internal server issue, please report.`;
}
}
}
output =
`\n###################################` +
`\n# #` +
`\n# Parse Server Security Check #` +
`\n# #` +
`\n###################################` +
`\n` +
`\n${failedChecksCount > 0 ? 'Warning: ' : ''}${failedChecksCount} weak security setting(s) found${failedChecksCount > 0 ? '!' : ''}` +
`\n${checksCount} check(s) executed` +
`\n${skippedCheckCount} check(s) skipped` +
`\n` +
`${output}`;
// Write log
log(output);
}
/**
* Returns an icon for use in the report log output.
* @param {CheckState} state The check state.
* @returns {String} The icon.
*/
_getLogIconForState(state) {
switch (state) {
case CheckState.success: return '✅';
case CheckState.fail: return '❌';
default: return '';
}
}
/**
* Validates the constructor parameters.
* @param {Object} params The parameters to validate.
*/
_validateParams(params) {
Utils.validateParams(params, {
enableCheck: { t: 'boolean', v: isBoolean, o: true },
enableCheckLog: { t: 'boolean', v: isBoolean, o: true },
checkGroups: { t: 'array', v: isArray, o: true },
});
}
}
module.exports = CheckRunner;