// This file is based on 
// https://code.amazon.com/packages/FultonConstructs/blobs/mainline/--/lib/ssm-bastion/spie-bastion.ts

import { execFileSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as cdk from "@aws-cdk/core";
import * as autoscaling from '@aws-cdk/aws-autoscaling';
import * as ec2 from '@aws-cdk/aws-ec2';
import * as events from '@aws-cdk/aws-events';
import * as iam from '@aws-cdk/aws-iam';
import * as logs from '@aws-cdk/aws-logs';

export interface SPIEProps {
  /**
   * The Hostclass that you've provisioned for your bastion.
   */
  readonly hostClass: string;
  /**
   * The VPC to deploy into.
   */
  readonly vpc: ec2.IVpc;
  /**
   * The instancePolicy from [SPIEBucketAndRoles]
   */
  readonly instancePolicy: iam.Policy;
  /**
   * Optional instance type for your bastion
   *
   * @default ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.SMALL) "t3.small"
   */
  readonly instanceType?: ec2.InstanceType;

  /**
   * The time in CRON format to run maintenance (patching, etc), in UTC.
   * @default {minute: '4', hour: '3'} - 0304 utc every day
   */
  readonly maintenanceTime?: events.CronOptions;
}

/**
 * A SPIEBastion deploys an EC2 instance in an ASG, and the proper configuration
 * to allow uses to connect over SSM via SSH and authenticate with SPIE.
 */
export class SPIEBastion extends cdk.Construct implements ec2.IConnectable {

  public connections: ec2.Connections;
  
  constructor(scope: cdk.Construct, id: string, props: SPIEProps) {
    super(scope, id);
    const region = cdk.Stack.of(this).region;
    const account = cdk.Stack.of(this).account;

    // All caps, numbers, or dashes, but not starting with or ending with a dash
    // Logic and regex construction preserved from upstream to be compatible
    // https://code.amazon.com/packages/InfAutoProvisioningLogic/blobs/c9b634a8fadeb7f5caf9e03b1b1305abb81c0235/--/perl-modules/lib/Amazon/InfAuto/ManageHostClass.pm#L390-L392
    if (!(!props.hostClass.match(/[^-A-Z0-9]/) && !props.hostClass.match(/^-/) && !props.hostClass.match(/-$/))) {
      throw Error(
        `${props.hostClass} doesn't seem valid, hostclass names can only contain capital letters, numbers and dashes,` +
          ` and may not start or end with dashes.`,
      );
    }
    const instanceType = props.instanceType || ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.SMALL);
    const maintenanceWindow = props.maintenanceTime || { minute: '4', hour: '3' };

    const spieASG = new autoscaling.AutoScalingGroup(this, 'SpieAsg', {
      instanceType: instanceType,
      vpc: props.vpc,
      machineImage: new ec2.AmazonLinuxImage({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2 }),
      maxCapacity: 1,
      minCapacity: 1,
      cooldown: cdk.Duration.seconds(60),
      vpcSubnets: {
        subnetType: ec2.SubnetType.PRIVATE,
      },
    });
    const loggroup = new logs.LogGroup(this, 'SyslogGroup', {
      retention: logs.RetentionDays.EIGHTEEN_MONTHS,
    });
    const spieBastionTag = 'Twitch-SSM-Bastion';
    spieASG.role.attachInlinePolicy(props.instancePolicy);
    spieASG.role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('CloudWatchAgentServerPolicy'));
    spieASG.role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'));
    spieASG.userData.addCommands(...this.spieBastionUserData(loggroup.logGroupName));

    cdk.Tags.of(spieASG).add(spieBastionTag, '1');
    cdk.Tags.of(spieASG).add('Name', 'ssm-bastion-host');
    cdk.Tags.of(spieASG).add('singlepass-ec2-client-hostclass', props.hostClass);

    const CloudWatchEventsSSMRole = new iam.Role(this, 'EventsSsmRole', {
      assumedBy: new iam.ServicePrincipal('events.amazonaws.com'),
    });
    CloudWatchEventsSSMRole.addToPolicy(
      new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        actions: ['ssm:SendCommand'],
        resources: [
          `arn:aws:ec2:${region}:${account}:instance/*`,
          `arn:aws:ec2:${region}::document/AWS-RunShellScript`,
          `arn:aws:ec2:${region}::document/AWS-RunPatchBaseline`,
        ],
      }),
    );
    const PatchRule = new events.Rule(this, 'PatchRule', {
      description: 'Patch remote bastion',
      schedule: events.Schedule.cron(maintenanceWindow),
    });
    const cfnPatchRule = PatchRule.node.defaultChild as events.CfnRule;
    cfnPatchRule.targets = [
      {
        id: 'bastion-patch',
        arn: `arn:aws:ssm:${region}::document/AWS-RunPatchBaseline`,
        roleArn: CloudWatchEventsSSMRole.roleArn,
        runCommandParameters: {
          runCommandTargets: [{ key: `tag:${spieBastionTag}`, values: ['1'] }],
        },
        input: '{"Operation":"Install"}',
      },
    ];
    this.connections = spieASG.connections;
  }

  private spieBastionUserData(logGroupName: string): string[] {
    const spieCloudwatchAgentConfig = `
{
    "agent": {
      "metrics_collection_interval": 60
    },
    "logs": {
      "logs_collected": {
        "files": {
          "collect_list": [
            {
              "file_path": "/var/log/messages",
              "log_group_name": "${logGroupName}",
              "log_stream_name": "{instance_id}/var/log/messages"
            },
            {
              "file_path": "/var/log/secure",
              "log_group_name": "${logGroupName}",
              "log_stream_name": "{instance_id}/var/log/secure"
            },
            {
              "file_path": "/var/log/audit/audit.log",
              "log_group_name": "${logGroupName}",
              "log_stream_name": "{instance_id}/var/log/audit/audit.log"
            }
          ]
        }
      }
    }
  }
`;
    // For overriding in snapshot tests
    const installer = this.SpieInstallerB64();
    return [
      'systemctl daemon-reload',
      'yum -y update --security',
      'yum -y install amazon-cloudwatch-agent',
      `echo '${spieCloudwatchAgentConfig}' > '/opt/aws/amazon-cloudwatch-agent/etc/config.json'`,
      '/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -c file:/opt/aws/amazon-cloudwatch-agent/etc/config.json -s',
      "echo '-a exit,always -F arch=b64 -S execve -k procmon' | tee -a /etc/audit/rules.d/twitch-exec-logging.rules",
      "echo '-a exit,always -F arch=b32 -S execve -k procmon' | tee -a /etc/audit/rules.d/twitch-exec-logging.rules",
      'augenrules --load',
      'systemctl force-reload auditd',
      `while :; do (echo ${installer} | base64 -d | zcat | /bin/bash -xe) && break || (echo "Setup failed, waiting one minute then trying again..."; sleep 60 ); done`,
      'yum -y install oddjob-mkhomedir',
      'authconfig --enablemkhomedir --update',
    ];
  }

  protected SpieInstallerB64(): string {
    const spieScript = fs.readFileSync(path.resolve(__dirname, './singlepass-prod.sh'), { encoding: 'utf-8' });
    return execFileSync('gzip', ['-9'], { input: spieScript }).toString('base64');
  }
}
