App-FargateStack

 view release on metacpan or  search on metacpan

lib/App/FargateStack/Checker.pm  view on Meta::CPAN

  my $ec2 = new_client( 'App::EC2', $opt );

  my ( $ok_ep, $eps, $err_ep ) = try_aws(
    sub {
      $ec2->command( 'describe-vpc-endpoints' => [ '--query', 'VpcEndpoints[].ServiceName' ] );
    }
  );

  if ( !$ok_ep ) {
    return row_warn( 'Secrets Manager', 'Could not confirm VPC endpoints for Secrets Manager' );
  }

  my %have = map { $_ => 1 } @{ $eps || [] };
  my $svc  = sprintf 'com.amazonaws.%s.secretsmanager', ( $opt->{region} || 'us-east-1' );

  if ( !$have{$svc} ) {
    return row_warn( 'Secrets Manager', 'Missing VPC endpoint for Secrets Manager or rely on NAT' );
  }

  return row_ok( 'Secrets Manager', 'Endpoint present' );
}
########################################################################
sub check_passrole {
########################################################################
  my ($opt) = @_;
  my $role_names = $opt->get_role_names;

  # role_names: arrayref of the exact role names your framework will create,
  # e.g., [ 'FargateStack/my-svc/TaskExecutionRole', 'FargateStack/my-svc/TaskRole',
  #         'FargateStack/my-svc/EventsInvokeRole' ]

  my $sts = new_client( 'App::STS', $opt );
  my $iam = new_client( 'App::IAM', $opt );

  my $id         = $sts->get_caller_identity();
  my $acct       = $id->{Account} || q{};
  my $caller_arn = $id->{Arn}     || q{};

  if ( !$acct || !$caller_arn ) {
    return row_fail( 'iam:PassRole', 'Cannot resolve caller identity' );
  }

  my $policy_source_arn = _principal_to_policy_source_arn( $caller_arn, $acct );
  if ( !$policy_source_arn ) {
    return row_fail( 'iam:PassRole', 'Cannot derive policy-source ARN for simulation' );
  }

  my $role_arns = _role_arns_from_names( $acct, $role_names || [] );
  if ( !$role_arns || !@{$role_arns} ) {
    return row_warn( 'iam:PassRole', 'No target roles specified; skipping simulation' );
  }

  my @services = qw(ecs-tasks.amazonaws.com events.amazonaws.com);

  my %decisions_by_service;

  foreach my $svc (@services) {
    my $result = eval { _simulate_passrole_once( $iam, $policy_source_arn, $role_arns, $svc ) };
    if ( $EVAL_ERROR || !$result ) {
      # If org blocks simulation, warn rather than fail
      return row_warn( 'iam:PassRole', 'Simulation not permitted; verify PassRole manually' );
    }

    # result is an arrayref of EvalDecision strings: allowed | explicitDeny | implicitDeny
    my @dec = @{ $result || [] };
    $decisions_by_service{$svc} = \@dec;
  }

  # Aggregate decisions: any explicitDeny -> FAIL
  # otherwise any implicitDeny -> WARN
  # otherwise all allowed -> PASS
  my $detail = q{};
  my $status = 'PASS';

  foreach my $svc (@services) {
    my $dec         = $decisions_by_service{$svc} || [];
    my $svc_summary = sprintf '%s: %s', $svc, ( join q{,}, @{$dec} );

    if ( grep { $_ eq 'explicitDeny' } @{$dec} ) {
      $status = 'FAIL';
    }
    elsif ( $status ne 'FAIL' && grep { $_ eq 'implicitDeny' } @{$dec} ) {
      $status = 'WARN';
    }

    $detail .= $svc_summary . q{ };
  }

  $detail =~ s/\s+\z//;

  if ( $status eq 'FAIL' ) { return row_fail( 'iam:PassRole', $detail ); }
  if ( $status eq 'WARN' ) { return row_warn( 'iam:PassRole', $detail ); }
  return row_ok( 'iam:PassRole', $detail || 'allowed' );
}

########################################################################
sub _principal_to_policy_source_arn {
########################################################################
  my ( $sts_arn, $account_id ) = @_;

  # Examples:
  # arn:aws:sts::123456789012:assumed-role/DeployerRole/SESSION -> arn:aws:iam::123456789012:role/DeployerRole
  # arn:aws:iam::123456789012:user/someuser                     -> arn:aws:iam::123456789012:user/someuser

  return q{} if !$sts_arn || !$account_id;

  if ( $sts_arn =~ m{\A arn:aws:sts::\Q$account_id\E:assumed-role/([^/]+)/}xsm ) {
    my $role = $1;
    return sprintf 'arn:aws:iam::%s:role/%s', $account_id, $role;
  }

  if ( $sts_arn =~ m{\A arn:aws:iam::\Q$account_id\E:(user|role)/}xsm ) {
    return $sts_arn;
  }

  # Fallback: try to coerce to iam::role if it looks like an sts assumed role
  return $sts_arn;
}

########################################################################
sub _role_arns_from_names {

lib/App/FargateStack/Checker.pm  view on Meta::CPAN

    log_level => 'info',
    dns       => $TRUE,
    secrets   => $TRUE,
    https     => $TRUE,
  );

  my @option_specs = qw(
    profile=s
    region=s
    https!
    help|h
    dns!
    secrets!
    dns-profile=s
    log_level=s
  );

  my %commands = ( default => \&check_fargate_env );

  my $cli = App::FargateStack::Checker->new(
    commands        => \%commands,
    default_options => \%default_options,
    option_specs    => \@option_specs,
    extra_options   => [qw(account global_options role_names route53_profile)],
  );

  my @expected_roles = (
    # Execution role and task role your builder will create
    'FargateStack/my-svc/TaskExecutionRole',
    'FargateStack/my-svc/TaskRole',
    'FargateStack/my-svc/EventsInvokeRole',
  );

  $cli->set_role_names( \@expected_roles );

  $cli->set_global_options(
    { profile => $cli->get_profile,
      region  => $cli->get_region
    }
  );

  return $cli->run();
}

1;

__END__

=pod

=head1 NAME

app-FargateStack-env.pl - Preflight checker for ECS Fargate environments

=head1 USAGE

  app-FargateStack-env.pl [options]

=head1 DESCRIPTION

Runs read-only checks against the target AWS account and region to verify
that common ECS Fargate deployment scenarios are feasible. Produces an
ASCII table with PASS/WARN/FAIL rows and a capabilities summary for:

  - HTTP services
  - HTTPS service
  - Scheduled tasks
  - One-shot tasks
  - Daemon services

No resources are created or modified. Intended as a fast “can I deploy here?”
probe for humans and CI.

=head2 Options

=over 4

=item B<--profile> I<STR>

AWS config/credentials profile to use. Defaults to C<$ENV{AWS_PROFILE}> or the
SDK’s default behavior if unset.

=item B<--region> I<STR>

AWS region to target (e.g. C<us-east-1>). Defaults to C<$ENV{AWS_REGION}> if set.

=item B<--dns> | B<--no-dns>

Enable or disable Route 53 checks. Default: B<enabled>.
Use B<--no-dns> (or B<--nodns>) to skip DNS checks.

=item B<--dns-profile> I<STR>

Alternate AWS profile for Route 53 lookups. Useful when DNS is managed in a
separate account. Falls back to C<--profile> if not provided.

=item B<--https> | B<--no-https>

Enable or disable ACM certificate checks (same region as the load balancer).
Default: B<disabled>. Turn on if you plan to deploy HTTPS.

=item B<--secrets> | B<--no-secrets>

Enable or disable Secrets Manager reachability checks. Default: B<disabled>.
When enabled, the checker verifies control-plane reachability (e.g., VPC
endpoint present or NAT available). It does not validate individual
C<GetSecretValue> permissions for task roles.

=back

=head1 OUTPUT

The main table includes rows like:

  Credentials
  Service-linked roles
  VPC/Subnets
  Egress
  ECS perms
  ELBv2 perms
  CloudWatch Logs perms



( run in 0.501 second using v1.01-cache-2.11-cpan-e1769b4cff6 )