Skip to content

Commit

Permalink
Merge branch 'main' of github.com:sizzldev/ctrlplane into aks-scanner
Browse files Browse the repository at this point in the history
  • Loading branch information
adityachoudhari26 committed Jan 3, 2025
2 parents a6220d6 + 141f048 commit acc7eb2
Show file tree
Hide file tree
Showing 17 changed files with 4,803 additions and 66 deletions.
1 change: 1 addition & 0 deletions apps/docs/pages/integrations/aws/compute-scanner.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ creates resources for them automatically.
Currently, the AWS compute scanner supports importing the following resources:

- Amazon Elastic Kubernetes Service Clusters (EKS)
- Amazon Virtual Private Cloud (VPC)

## Managed AWS Compute Scanner

Expand Down
30 changes: 16 additions & 14 deletions apps/event-worker/src/resource-scan/aws/eks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,20 +161,22 @@ export const getEksResources = async (
const credentials = await assumeWorkspaceRole(workspaceRoleArn);
const workspaceStsClient = credentials.sts();

const resources = await _.chain(config.awsRoleArns)
.map((customerRoleArn) =>
scanEksClustersByAssumedRole(workspaceStsClient, customerRoleArn),
)
.thru((promises) => Promise.all(promises))
.value()
.then((results) => results.flat())
.then((resources) =>
resources.map((resource) => ({
...resource,
workspaceId: workspace.id,
providerId: config.resourceProviderId,
})),
);
const resources = config.importEks
? await _.chain(config.awsRoleArns)
.map((customerRoleArn) =>
scanEksClustersByAssumedRole(workspaceStsClient, customerRoleArn),
)
.thru((promises) => Promise.all(promises))
.value()
.then((results) => results.flat())
.then((resources) =>
resources.map((resource) => ({
...resource,
workspaceId: workspace.id,
providerId: config.resourceProviderId,
})),
)
: [];

const resourceTypes = _.countBy(resources, (resource) =>
[resource.kind, resource.version].join("/"),
Expand Down
207 changes: 207 additions & 0 deletions apps/event-worker/src/resource-scan/aws/vpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import type { Vpc } from "@aws-sdk/client-ec2";
import type { STSClient } from "@aws-sdk/client-sts";
import type { ResourceProviderAws, Workspace } from "@ctrlplane/db/schema";
import type { CloudVPCV1 } from "@ctrlplane/validators/resources";
import {
DescribeRegionsCommand,
DescribeSubnetsCommand,
DescribeVpcsCommand,
} from "@aws-sdk/client-ec2";
import _ from "lodash";
import { isPresent } from "ts-is-present";

import { logger } from "@ctrlplane/logger";
import { ReservedMetadataKey } from "@ctrlplane/validators/conditions";

import type { AwsCredentials } from "./aws.js";
import { omitNullUndefined } from "../../utils.js";
import { assumeRole, assumeWorkspaceRole } from "./aws.js";

const log = logger.child({ label: "resource-scan/aws/vpc" });

const convertVpcToCloudResource = (
accountId: string,
region: string,
vpc: Vpc,
subnets: {
name: string;
region: string;
cidr: string;
type: "public" | "private";
availabilityZone?: string;
}[] = [],
): CloudVPCV1 => {
const partition = region.startsWith("us-gov-") ? "aws-us-gov" : "aws";
const appUrl = `https://${
partition === "aws-us-gov"
? `console.${region}.${partition}`
: "console.aws.amazon"
}.com/vpcconsole/home?region=${region}#vpcs:search=${vpc.VpcId}`;

const name = vpc.Tags?.find((tag) => tag.Key === "Name")?.Value ?? vpc.VpcId!;

return {
name,
identifier: `aws/${accountId}/vpc/${vpc.VpcId}`,
version: "cloud/v1",
kind: "VPC",
config: {
name,
id: vpc.VpcId!,
provider: "aws",
region,
accountId: accountId,
cidr: vpc.CidrBlock,
subnets,
secondaryCidrs: vpc.CidrBlockAssociationSet?.filter(
(assoc) => assoc.CidrBlock !== vpc.CidrBlock,
).map((assoc) => ({
cidr: assoc.CidrBlock ?? "",
state: assoc.CidrBlockState?.State?.toLowerCase() ?? "",
})),
},
metadata: omitNullUndefined({
[ReservedMetadataKey.ExternalId]: vpc.VpcId,
[ReservedMetadataKey.Links]: JSON.stringify({ "AWS Console": appUrl }),
"aws/region": region,
"aws/state": vpc.State,
"aws/is-default": vpc.IsDefault,
"aws/dhcp-options-id": vpc.DhcpOptionsId,
"aws/instance-tenancy": vpc.InstanceTenancy,
...(vpc.Tags?.reduce(
(acc, tag) => ({
...acc,
[`aws/tag/${tag.Key}`]: tag.Value,
}),
{},
) ?? {}),
}),
};
};

const getAwsRegions = async (credentials: AwsCredentials) =>
credentials
.ec2()
.send(new DescribeRegionsCommand({}))
.then(({ Regions = [] }) => Regions.map((region) => region.RegionName));

const getVpcs = async (client: AwsCredentials, region: string) => {
const ec2Client = client.ec2(region);
const { Vpcs = [] } = await ec2Client.send(new DescribeVpcsCommand({}));
return Vpcs;
};

const getSubnets = async (
client: AwsCredentials,
region: string,
vpcId: string,
): Promise<
{
name: string;
region: string;
cidr: string;
type: "public" | "private";
availabilityZone: string;
}[]
> => {
const ec2Client = client.ec2(region);
const { Subnets = [] } = await ec2Client.send(
new DescribeSubnetsCommand({
Filters: [{ Name: "vpc-id", Values: [vpcId] }],
}),
);
return Subnets.map((subnet) => ({
name: subnet.SubnetId ?? "",
region,
cidr: subnet.CidrBlock ?? "",
type: subnet.MapPublicIpOnLaunch ? "public" : "private",
availabilityZone: subnet.AvailabilityZone ?? "",
}));
};

const createVpcScannerForRegion = (
client: AwsCredentials,
customerRoleArn: string,
) => {
const accountId = /arn:aws:iam::(\d+):/.exec(customerRoleArn)?.[1];
if (accountId == null) throw new Error("Missing account ID");

return async (region: string) => {
const vpcs = await getVpcs(client, region);
log.info(
`Found ${vpcs.length} VPCs for ${customerRoleArn} in region ${region}`,
);

const vpcResources = await Promise.all(
vpcs.map(async (vpc) => {
if (!vpc.VpcId) return null;
const subnets = await getSubnets(client, region, vpc.VpcId);
return convertVpcToCloudResource(accountId, region, vpc, subnets);
}),
);

return vpcResources.filter(isPresent);
};
};

const scanVpcsByAssumedRole = async (
workspaceClient: STSClient,
customerRoleArn: string,
) => {
const client = await assumeRole(workspaceClient, customerRoleArn);
const regions = await getAwsRegions(client);

log.info(
`Scanning ${regions.length} AWS regions for VPCs in account ${customerRoleArn}`,
);

const regionalVpcScanner = createVpcScannerForRegion(client, customerRoleArn);

return _.chain(regions)
.filter(isPresent)
.map(regionalVpcScanner)
.thru((promises) => Promise.all(promises))
.value()
.then((results) => results.flat());
};

export const getVpcResources = async (
workspace: Workspace,
config: ResourceProviderAws,
) => {
const { awsRoleArn: workspaceRoleArn } = workspace;
if (workspaceRoleArn == null) return [];

log.info(
`Scanning for VPCs with assumed role arns ${config.awsRoleArns.join(", ")} using role ${workspaceRoleArn}`,
{
workspaceId: workspace.id,
config,
workspaceRoleArn,
},
);

const credentials = await assumeWorkspaceRole(workspaceRoleArn);
const workspaceStsClient = credentials.sts();

const resources = config.importVpc
? await _.chain(config.awsRoleArns)
.map((customerRoleArn) =>
scanVpcsByAssumedRole(workspaceStsClient, customerRoleArn),
)
.thru((promises) => Promise.all(promises))
.value()
.then((results) => results.flat())
.then((resources) =>
resources.map((resource) => ({
...resource,
workspaceId: workspace.id,
providerId: config.resourceProviderId,
})),
)
: [];

log.info(`Found ${resources.length} VPC resources`);

return resources;
};
26 changes: 0 additions & 26 deletions apps/event-worker/src/resource-scan/azure/index.ts

This file was deleted.

24 changes: 19 additions & 5 deletions apps/event-worker/src/resource-scan/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ import { Channel } from "@ctrlplane/validators/events";

import { redis } from "../redis.js";
import { getEksResources } from "./aws/eks.js";
import { getVpcResources as getAwsVpcResources } from "./aws/vpc.js";
import { getAksResources } from "./azure/aks.js";
import { getGkeResources } from "./google/gke.js";
import { getVpcResources as getGoogleVpcResources } from "./google/vpc.js";

const log = logger.child({ label: "resource-scan" });

Expand All @@ -31,11 +33,23 @@ const removeResourceJob = (job: Job) =>
? resourceScanQueue.removeRepeatableByKey(job.repeatJobKey)
: null;

const getResource = (rp: any) => {
if (rp.resource_provider_google != null)
return getGkeResources(rp.workspace, rp.resource_provider_google);
if (rp.resource_provider_aws != null)
return getEksResources(rp.workspace, rp.resource_provider_aws);
const getResource = async (rp: any) => {
if (rp.resource_provider_google != null) {
const [gkeResources, vpcResources] = await Promise.all([
getGkeResources(rp.workspace, rp.resource_provider_google),
getGoogleVpcResources(rp.workspace, rp.resource_provider_google),
]);
return [...gkeResources, ...vpcResources];
}

if (rp.resource_provider_aws != null) {
const [eksResources, vpcResources] = await Promise.all([
getEksResources(rp.workspace, rp.resource_provider_aws),
getAwsVpcResources(rp.workspace, rp.resource_provider_aws),
]);
return [...eksResources, ...vpcResources];
}

if (rp.resource_provider_azure != null)
return getAksResources(rp.workspace, rp.resource_provider_azure);
throw new Error("Invalid resource provider");
Expand Down
Loading

0 comments on commit acc7eb2

Please sign in to comment.