hq/app/aws/iam/CredentialsReport.scala (108 lines of code) (raw):

package aws.iam import java.io.StringReader import model.{AwsStack, CredentialReportDisplay, IAMCredential, IAMCredentialsReport} import com.github.tototoshi.csv._ import org.joda.time.{DateTime, Hours, Seconds} import logic.DateUtils import net.logstash.logback.marker.Markers.appendEntries import play.api.{Logging, MarkerContext} import utils.attempt.{Attempt, FailedAttempt} import scala.concurrent.ExecutionContext import scala.util.{Failure, Success, Try} import scala.jdk.CollectionConverters._ import software.amazon.awssdk.regions.Region import software.amazon.awssdk.services.iam.model.{GenerateCredentialReportResponse, GetCredentialReportResponse, ReportStateType} object CredentialsReport extends Logging { def isComplete(report: GenerateCredentialReportResponse): Boolean = report.state == ReportStateType.COMPLETE def credentialsReportReadyForRefresh(currentReport: Either[FailedAttempt, CredentialReportDisplay], currentTime: DateTime): Boolean = { currentReport match { case Left(_) => true case Right(credentialReportDisplay) => { val timeSinceLastReport = Seconds.secondsBetween(credentialReportDisplay.reportDate, currentTime) timeSinceLastReport.isGreaterThan(Hours.hours(4).toStandardSeconds) } } } private[iam] def enrichReportWithStackDetails(report: IAMCredentialsReport, stacks: List[AwsStack]): IAMCredentialsReport = { val updatedEntries = report.entries.map { cred => stacks .find(stack => cred.user.startsWith(stack.name) && cred.user.takeRight(12).matches("^[A-Z0-9]{12}$")) .fold(cred)(s => cred.copy(stack = Some(s))) } report.copy(entries = updatedEntries) } private[iam] def tryParsingReport(content: String) = { Try { parseCredentialsReport(content) } match { case Success(x) if x.nonEmpty => Attempt.Right(x) case Success(_) => Attempt.Left(utils.attempt.Failure(s"CREDENTIALS_PARSE_ERROR", "Credentials report is empty", 500)) case Failure(th) => Attempt.Left(utils.attempt.Failure(s"CREDENTIALS_PARSE_ERROR: ${th.getMessage}", "Cannot parse AWS credentials audit report", 500)) } } def extractReport(response: GetCredentialReportResponse)(implicit ec: ExecutionContext): Attempt[IAMCredentialsReport] = { val report = response.content.asUtf8String() tryParsingReport(report).map(IAMCredentialsReport(new DateTime(response.generatedTime.toEpochMilli), _)) } def parseBoolean(cell: String): Option[Boolean] = { if (cell == "true") Some(true) else if (cell == "false") Some(false) else None } def parseDateTimeOpt(cell: String): Option[DateTime] = { cell match { case "no_information" | "N/A" | "not_supported" => None case _ => Some(DateUtils.isoDateTimeParser.parseDateTime(cell)) } } def parseRegionOpt(cell: String): Option[Region] = { if (cell == "N/A") None else Some(Region.of(cell)) } def parseStrOpt(cell: String): Option[String] = { if (cell == "N/A") None else Some(cell) } def parseCredentialsReport(contents: String): List[IAMCredential] = { val iamCredentialsReport = CSVReader.open(new StringReader(contents)).allWithHeaders().map { row => IAMCredential( user = row.getOrElse("user", "no username available"), arn = row.getOrElse("arn", "no ARN available"), creationTime = row.get("user_creation_time").flatMap(parseDateTimeOpt).get, stack = None, passwordEnabled = row.get("password_enabled").flatMap(parseBoolean), passwordLastUsed = row.get("password_last_used").flatMap(parseDateTimeOpt), passwordLastChanged = row.get("password_last_changed").flatMap(parseDateTimeOpt), passwordNextRotation = row.get("password_next_rotation").flatMap(parseDateTimeOpt), mfaActive = row.get("mfa_active").flatMap(parseBoolean).get, accessKey1Active = row.get("access_key_1_active").flatMap(parseBoolean).get, accessKey1LastRotated = row.get("access_key_1_last_rotated").flatMap(parseDateTimeOpt), accessKey1LastUsedDate = row.get("access_key_1_last_used_date").flatMap(parseDateTimeOpt), accessKey1LastUsedRegion = row.get("access_key_1_last_used_region").flatMap(parseRegionOpt), accessKey1LastUsedService = row.get("access_key_1_last_used_service").flatMap(parseStrOpt), accessKey2Active = row.get("access_key_2_active").flatMap(parseBoolean).get, accessKey2LastRotated = row.get("access_key_2_last_rotated").flatMap(parseDateTimeOpt), accessKey2LastUsedDate = row.get("access_key_2_last_used_date").flatMap(parseDateTimeOpt), accessKey2LastUsedRegion = row.get("access_key_2_last_used_region").flatMap(parseRegionOpt), accessKey2LastUsedService = row.get("access_key_2_last_used_service").flatMap(parseStrOpt), cert1Active = row.get("cert_1_active").flatMap(parseBoolean).get, cert1LastRotated = row.get("cert_1_last_rotated").flatMap(parseDateTimeOpt), cert2Active = row.get("cert_2_active").flatMap(parseBoolean).get, cert2LastRotated = row.get("cert_2_last_rotated").flatMap(parseDateTimeOpt) ) } iamCredentialsReport.filter(x => x.passwordEnabled.contains(true)).foreach(iamCred => { val mandatoryMarkers = Map( "User" -> iamCred.user, "PasswordEnabled" -> iamCred.passwordEnabled.getOrElse(false), "Arn" -> iamCred.arn ) val markers = MarkerContext(appendEntries(mandatoryMarkers.asJava)) logger.info(s"${iamCred.user} user has non-Janus access to AWS: $iamCred")(markers) }) iamCredentialsReport } }