hq/app/logic/CredentialsReportDisplay.scala (137 lines of code) (raw):

package logic import logic.DateUtils.dayDiff import model._ import org.joda.time.{DateTime, DateTimeZone, Days} import IamUnrecognisedUsers.isTaggedForUnrecognisedUser import utils.attempt.FailedAttempt import java.net.URLEncoder object CredentialsReportDisplay { case class ReportSummary(warnings: Int, errors: Int, other: Int) private[logic] def lastActivityDate(cred: IAMCredential): Option[DateTime] = { val allDates = cred.passwordLastUsed.toSeq ++ cred.accessKey1LastUsedDate.toSeq ++ cred.accessKey2LastUsedDate.toSeq allDates.sortWith(_.isAfter(_)).collectFirst { case date if date.isBefore(DateTime.now(DateTimeZone.UTC)) => date } } private[logic] def accessKey1Details(cred: IAMCredential): AccessKey = { if (cred.accessKey1Active) AccessKey(AccessKeyEnabled, cred.accessKey1LastRotated) else if (!cred.accessKey1Active && cred.accessKey1LastUsedDate.nonEmpty) AccessKey(AccessKeyDisabled, cred.accessKey1LastRotated) else AccessKey(NoKey, None) } private[logic] def accessKey2Details(cred: IAMCredential): AccessKey = { if (cred.accessKey2Active) AccessKey(AccessKeyEnabled, cred.accessKey2LastRotated) else if (!cred.accessKey2Active && cred.accessKey2LastUsedDate.nonEmpty) AccessKey(AccessKeyDisabled, cred.accessKey2LastRotated) else AccessKey(NoKey, None) } private[logic] def machineReportStatus(cred: IAMCredential): ReportStatus = { val keys = List(accessKey1Details(cred), accessKey2Details(cred)) if (VulnerableAccessKeys.hasOutdatedMachineKeyIncludingDisabled(keys)) Red(Seq(OutdatedKey)) else if (!keys.exists(_.keyStatus == AccessKeyEnabled)) Amber() else if (Days.daysBetween(lastActivityDate(cred).getOrElse(DateTime.now), DateTime.now).getDays > 365) Blue else Green } private[logic] def humanReportStatus(cred: IAMCredential): ReportStatus = { val keys = List(accessKey1Details(cred), accessKey2Details(cred)) //TODO: Scala 2.13 has Option builder `when` which is a nicer syntax than Some(...).filter val redStatusReasons: Seq[ReportStatusReason] = Seq( Some(MissingMfa).filterNot(_ => cred.mfaActive), Some(OutdatedKey).filter(_ => VulnerableAccessKeys.hasOutdatedHumanKeyIncludingDisabled(keys)) ).flatten val amberStatusReasons: Seq[ReportStatusReason] = Seq( Some(ActiveAccessKey).filter(_ => keys.exists(_.keyStatus == AccessKeyEnabled)), Some(MissingUsernameTag).filterNot(_ => IamUnrecognisedUsers.isTaggedForUnrecognisedUser(cred.tags)) ).flatten if (redStatusReasons.nonEmpty) Red(redStatusReasons) else if (amberStatusReasons.nonEmpty) Amber(amberStatusReasons) else if (Days.daysBetween(lastActivityDate(cred).getOrElse(DateTime.now), DateTime.now).getDays > 365) Blue else Green } def linkForAwsConsole(stack: AwsStack): String = { s"https://${stack.region}.console.aws.amazon.com/cloudformation/home?${stack.region}#/stack/detail?stackId=${URLEncoder.encode(stack.id, "utf-8")}" } def toCredentialReportDisplay(report: IAMCredentialsReport): CredentialReportDisplay = { val humanUsers = report.entries.filterNot(_.rootUser).collect { case cred if cred.passwordEnabled.contains(true) => HumanUser( cred.user, cred.mfaActive, accessKey1Details(cred), accessKey2Details(cred), humanReportStatus(cred), dayDiff(lastActivityDate(cred)), stack = cred.stack, tags = cred.tags ) } val machineUsers = report.entries.filterNot(_.rootUser).collect { case cred if !cred.passwordEnabled.contains(true) => MachineUser( cred.user, accessKey1Details(cred), accessKey2Details(cred), machineReportStatus(cred), dayDiff(lastActivityDate(cred)), stack = cred.stack, tags = cred.tags ) } CredentialReportDisplay(report.generatedAt, machineUsers, humanUsers) } def checkNoKeyExists(keyStatuses: AccessKey*): Boolean = { keyStatuses.forall(_.keyStatus == NoKey) } def toDayString(day: Option[Long]): String = day match { case Some(0) => "Today" case Some(1) => "Yesterday" case Some(d) => s"${d.toString} days ago" case _ => "" } def reportStatusSummary(report: CredentialReportDisplay): ReportSummary = { val reportStatuses = (report.humanUsers ++ report.machineUsers) .map(_.reportStatus) val warnings = reportStatuses.collect({ case Amber(_) => }).size val errors = reportStatuses.collect({ case Red(_) => }).size val others = reportStatuses.collect({ case Blue => }).size ReportSummary(warnings, errors, others) } def exposedKeysSummary(allReports: Map[AwsAccount, Either[FailedAttempt, List[ExposedIAMKeyDetail]]]): Map[AwsAccount, Boolean] = { allReports.view.mapValues { case Right(keys) if keys.nonEmpty => true case _ => false }.toMap } def sortAccountsByReportSummary[L](reports: List[(AwsAccount, Either[L, CredentialReportDisplay])]): List[(AwsAccount, Either[L, CredentialReportDisplay])] = { reports.sortBy { case (account, Right(report)) if reportStatusSummary(report).errors + reportStatusSummary(report).warnings != 0 => (0, reportStatusSummary(report).errors * -1, reportStatusSummary(report).warnings * -1, account.name) case (account, Left(_)) => (0, 1, 0, account.name) case (account, Right(_)) => (1, 0, 0, account.name) } } def sortUsersByReportSummary(report: CredentialReportDisplay): CredentialReportDisplay = { report.copy( machineUsers = report.machineUsers.sortBy(user => (user.reportStatus, user.username)), humanUsers = report.humanUsers.sortBy(user => (user.reportStatus, user.username)) ) } implicit val reportStatusOrdering: Ordering[ReportStatus] = new Ordering[ReportStatus] { private def statusCode(status: ReportStatus): Int = status match { case Red(_) => 0 case Amber(_) => 1 case _ => 99 } override def compare(x: ReportStatus, y: ReportStatus): Int = { statusCode(x) - statusCode(y) } } }