message: Avoid false cheating accusations for coupled challenges
diff: @@ -7,22 +7,29 @@ const colors = require('colors/safe')
  const logger = require('../lib/logger')
  const config = require('config')
  -const solves = [{ timestamp: new Date(), cheatScore: 0 }] // seed with server start timestamp
  +const coupledChallenges = { // TODO prevent also near-identical challenges (e.g. all null byte file access or dom xss + bonus payload etc.) from counting as cheating
  + loginAdminChallenge: ['weakPasswordChallenge'],
  + nullByteChallenge: ['easterEggLevelOneChallenge', 'forgottenDevBackupChallenge', 'forgottenBackupChallenge', 'misplacedSignatureFileChallenge'],
  + deprecatedInterfaceChallenge: ['xxeFileDisclosureChallenge', 'xxeDosChallenge']
  +}
  +
  +const solves = [{ challenge: {}, timestamp: new Date(), cheatScore: 0 }] // seed with server start timestamp
  -// TODO prevent multi-solves (e.g. login admin + weak password or in general improper error handling) from same request counting as cheating
  -// TODO prevent near-identical challenges (e.g. all null byte file access or dom xss + bonus payload etc.) from counting as cheating (e.g. declare clusters of schallenges? Could also work for multi-solves?)
  exports.calculateCheatScore = (challenge) => {
  const timestamp = new Date()
  let cheatScore = 0
  let timeFactor = 2
  timeFactor *= (config.get('challenges.showHints') ? 1 : 1.5)
  timeFactor *= (challenge.tutorialOrder && config.get('hackingInstructor.isEnabled') ? 0.5 : 1)
  + if (areCoupled(challenge, previous().challenge)) {
  + timeFactor = 0
  + }
  const minutesExpectedToSolve = challenge.difficulty * timeFactor
  - const minutesSincePreviousSolve = (timestamp.getTime() - solves[solves.length - 1].timestamp.getTime()) / 60000
  + const minutesSincePreviousSolve = (timestamp.getTime() - previous().timestamp.getTime()) / 60000
  cheatScore += Math.max(0, 1 - (minutesSincePreviousSolve / minutesExpectedToSolve))
  - logger.info(`Cheat score for ${challenge.tutorialOrder ? 'tutorial ' : ''}${colors.cyan(challenge.key)} solved in ${Math.round(minutesSincePreviousSolve)} / ${minutesExpectedToSolve} minutes with${config.get('challenges.showHints') ? '' : 'out'} hints allowed: ${cheatScore < 0.33 ? colors.green(cheatScore) : (cheatScore < 0.66 ? colors.yellow(cheatScore) : colors.red(cheatScore))}`)
  + logger.info(`Cheat score for ${areCoupled(challenge, previous().challenge) ? 'coupled ' : ''}${challenge.tutorialOrder ? 'tutorial ' : ''}${colors.cyan(challenge.key)} solved in ${Math.round(minutesSincePreviousSolve)} / ${minutesExpectedToSolve} minutes with${config.get('challenges.showHints') ? '' : 'out'} hints allowed: ${cheatScore < 0.33 ? colors.green(cheatScore) : (cheatScore < 0.66 ? colors.yellow(cheatScore) : colors.red(cheatScore))}`)
  solves.push({ challenge, timestamp, cheatScore })
  return cheatScore
  }
  @@ -30,3 +37,12 @@ exports.calculateCheatScore = (challenge) => {
  exports.totalCheatScore = () => {
  return solves.length > 0 ? solves.map(({ cheatScore }) => cheatScore).reduce((sum, score) => { return sum + score }) / (solves.length - 1) : 0
  }
  +
  +function areCoupled (challenge, previousChallenge) {
  + return (coupledChallenges[challenge.key] && coupledChallenges[challenge.key].indexOf(previousChallenge.key) > -1) ||
  + (coupledChallenges[previousChallenge.key] && coupledChallenges[previousChallenge.key].indexOf(challenge.key) > -1)
  +}
  +
  +function previous () {
  + return solves[solves.length - 1]
  +}
  