hq/app/config/Config.scala (176 lines of code) (raw):

package config import com.gu.play.secretrotation.aws.parameterstore.SecretSupplier import com.gu.play.secretrotation.aws.parameterstore.AwsSdkV2 import aws.AwsClient import com.google.auth.oauth2.{ServiceAccountCredentials} import com.gu.googleauth.{AntiForgeryChecker, GoogleAuthConfig, GoogleGroupChecker} import com.gu.play.secretrotation.{RotatingSecretComponents, SnapshotProvider, TransitionTiming} import model._ import play.api.Configuration import play.api.http.HttpConfiguration import utils.attempt.{Attempt, FailedAttempt, Failure} import java.io.FileInputStream import java.time.Duration.{ofHours, ofMinutes} import scala.jdk.CollectionConverters._ import scala.concurrent.ExecutionContext import scala.util.Try import software.amazon.awssdk.regions.Region import software.amazon.awssdk.services.ssm.SsmClient object Config { val iamHumanUserRotationCadence: Long = 90 val iamMachineUserRotationCadence: Long = 365 val outdatedCredentialOptOutUserTag = "SecurityHQ::OutdatedCredentialOptOut" val daysBetweenWarningAndFinalNotification = 7 val daysBetweenFinalNotificationAndRemediation = 7 val app = "security-hq" // TODO fetch the region dynamically from the instance val region: Region = Region.of("eu-west-1") val documentationLinks: List[Documentation] = List ( Documentation("SSH", "Use SSM-Scala for SSH access.", "code", "ssh-access"), Documentation("Wazuh", "Guide to installing the Wazuh agent.", "scanner", "wazuh"), Documentation("Vulnerabilities", "Developer guide to addressing vulnerabilities.", "format_list_numbered", "vulnerability-management") ) def getStage(config: Configuration): Stage = { config.getAndValidate("stage", Set("DEV", "PROD")) match { case "DEV" => DEV case "PROD" => PROD case _ => throw config.reportError("stage", s"Missing application stage, expected one of DEV, PROD") } } def googleSettings(stage: Stage, stack: String, config: Configuration, ssmClient: SsmClient): GoogleAuthConfig = { val clientId = requiredString(config, "auth.google.clientId") val clientSecret = requiredString(config, "auth.google.clientSecret") val domain = requiredString(config, "auth.domain") val redirectUrl = s"${requiredString(config, "host")}/oauthCallback" val secretStateSupplier: SnapshotProvider = { new SecretSupplier( TransitionTiming(usageDelay = ofMinutes(3), overlapDuration = ofHours(2)), s"/${stage.toString}/$stack/$app/play.http.secret.key", AwsSdkV2(ssmClient) ) } GoogleAuthConfig( clientId, clientSecret, redirectUrl, List(domain), antiForgeryChecker = AntiForgeryChecker(secretStateSupplier) ) } def googleGroupChecker(implicit config: Configuration): GoogleGroupChecker = { val twoFAUser = requiredString(config, "auth.google.2faUser") val serviceAccountCertPath = requiredString(config, "auth.google.serviceAccountCertPath") val credentials: ServiceAccountCredentials = { val jsonCertStream = Try(new FileInputStream(serviceAccountCertPath)) .getOrElse(throw new RuntimeException(s"Could not load service account JSON from $serviceAccountCertPath")) ServiceAccountCredentials.fromStream(jsonCertStream) } new GoogleGroupChecker(twoFAUser, credentials) } def twoFAGroup(implicit config: Configuration): String = { requiredString(config, "auth.google.2faGroupId") } def departmentGroup(implicit config: Configuration): String = { requiredString(config, "auth.google.departmentGroupId") } private def requiredString(config: Configuration, key: String): String = { config.getOptional[String](key).getOrElse { throw new RuntimeException(s"Missing required config property $key") } } def getAwsAccounts(config: Configuration): List[AwsAccount] = { val accounts : List[AwsAccount] = for { //underlying.getConfigList(path)).map { configs => configs.asScala.map(Configuration(_)) accountConfig <- config.underlying.getConfigList("hq.accounts").asScala.map(Configuration(_)).toList //accountConfig <- accountConfigs awsAccount <- getAwsAccount(accountConfig) } yield awsAccount accounts.sortBy(_.name) } private[config] def getAwsAccount(config: Configuration): Option[AwsAccount] = { for { id <- config.getOptional[String]("id") name <- config.getOptional[String]("name") roleArn <- config.getOptional[String]("roleArn") number <- config.getOptional[String]("number") } yield AwsAccount(id, name, roleArn, number) } def getIamDynamoTableName(config: Configuration): Attempt[String] = { Attempt.fromOption( config.getOptional[String]("alert.iamDynamoTableName"), FailedAttempt(Failure("unable to get dynamo table name", "unable to get dynamo table name for IAM jobs", 500 )) ) } def getIamUnrecognisedUserConfig(config: Configuration)(implicit ec: ExecutionContext): Attempt[UnrecognisedJobConfigProperties] = { for { accounts <- getAllowedAccountsForStage(config) key <- getJanusDataFileKey(config) bucket <- getIamUnrecognisedUserBucket(config) securityAccount <- getSecurityAccount(config) anghammaradSnsTopicArn <- getAnghammaradSNSTopicArn(config) } yield UnrecognisedJobConfigProperties(accounts, key, bucket, securityAccount, anghammaradSnsTopicArn) } def getAnghammaradSNSTopicArn(config: Configuration): Attempt[String] = { Attempt.fromOption( config.getOptional[String]("alert.anghammaradSnsArn"), FailedAttempt(Failure("unable to get Anghammarad topic ARN", "unable to get Anghammarad topic ARN for IAM jobs", 500 )) ) } def getAccountsForIamRemediationService(config: Configuration): Attempt[List[String]] = { Attempt.fromOption( config.getOptional[Seq[String]]("alert.accountIdsForIamRemediationService").map(_.toList), FailedAttempt(Failure("unable to get list of accounts to run the IAM Remediation Service on. Rectify this by adding account ids to config.", "Add account Ids for Iam Remediation service to ~/.gu/security-hq.local.conf or for PROD, check S3 for security-hq.conf.", 500 )) ) } def getAllowedAccountsForStage(config: Configuration): Attempt[List[String]] = { Attempt.fromOption( config.getOptional[Seq[String]]("alert.allowedAccountIds").map(_.toList), FailedAttempt(Failure("unable to get list of accounts allowed to make changes to AWS. Rectify this by adding allowed accounts to config.", "I haven't been able to get a list of allowed AWS accounts, which should be in Security HQ's config. Check ~/.gu/security-hq.local.conf or for PROD, check S3 for security-hq.conf.", 500 )) ) } def getJanusDataFileKey(config: Configuration): Attempt[String] = { Attempt.fromOption( config.getOptional[String]("alert.iamUnrecognisedUserS3Key"), FailedAttempt(Failure("unable to get janus data file key from config for the IAM unrecognised job", "I haven't been able to get the Janus S3 file key from config. Please check ~/.gu/security-hq.local.conf for local conf or security-hq.conf in S3 for PROD conf.", 500) ) ) } def getIamUnrecognisedUserBucket(config: Configuration): Attempt[String] = { Attempt.fromOption( config.getOptional[String]("alert.iamUnrecognisedUserS3Bucket"), FailedAttempt(Failure("unable to get IAM unrecognised user bucket from config", "I haven't been able to get the S3 bucket, which contains the janus data used for the unrecognised user job. Please check ~/.gu/security-hq.local.conf for local conf or security-hq.conf in S3 for PROD conf.", 500) ) ) } def getIamUnrecognisedUserBucketRegion(config: Configuration): Attempt[String] = { Attempt.fromOption( config.getOptional[String]("alert.iamUnrecognisedUserS3BucketRegion"), FailedAttempt(Failure("unable to get IAM unrecognised user bucket region from config", "I haven't been able to get the S3 bucket region for the unrecognised user job. Please check ~/.gu/security-hq.local.conf for local conf or security-hq.conf in S3 for PROD conf.", 500) ) ) } def getSecurityAccount(config: Configuration): Attempt[AwsAccount] = { Attempt.fromOption( Config.getAwsAccounts(config).find(_.id == "security"), FailedAttempt(Failure("unable to find security account details from config", "I haven't been able to get the security account details from config", 500)) ) } }