From c553894357d7141948ba262eff06e3af85232d17 Mon Sep 17 00:00:00 2001 From: Chris Brown <1731074+ccbrown@users.noreply.github.com> Date: Fri, 18 Apr 2025 23:49:45 -0400 Subject: [PATCH] vastly improved integration settings page --- aws/lib/regional-stack.ts | 4 +- backend/api/aws_integration.go | 1 + ...on-v2.cfn.yaml => integration-v3.cfn.yaml} | 71 ++++++++- .../app/(public-area)/cookie-policy/page.tsx | 2 +- .../src/app/(public-area)/pricing/page.tsx | 13 +- .../app/(public-area)/privacy-policy/page.tsx | 2 +- .../(public-area)/terms-of-service/page.tsx | 2 +- .../app/(user-area)/teams/[teamId]/Header.tsx | 4 +- .../app/(user-area)/teams/[teamId]/page.tsx | 8 +- .../teams/[teamId]/settings/billing/page.tsx | 8 +- .../[teamId]/settings/integrations/page.tsx | 135 ++++++++++++++---- .../teams/[teamId]/settings/page.tsx | 2 +- frontend/src/app/globals.css | 5 + frontend/src/components/Checkbox.tsx | 6 +- frontend/src/components/Select.tsx | 4 +- frontend/src/components/TextArea.tsx | 4 +- frontend/src/components/TextField.tsx | 4 +- frontend/src/components/Tooltip.tsx | 4 +- frontend/src/integration.ts | 2 +- frontend/src/models/aws.ts | 17 ++- 20 files changed, 230 insertions(+), 68 deletions(-) rename frontend/public/{integration-v2.cfn.yaml => integration-v3.cfn.yaml} (72%) diff --git a/aws/lib/regional-stack.ts b/aws/lib/regional-stack.ts index 8fba9227..914a7ec2 100644 --- a/aws/lib/regional-stack.ts +++ b/aws/lib/regional-stack.ts @@ -87,8 +87,8 @@ export class RegionalStack extends Stack { const integrationTemplateUpload = new s3deploy.BucketDeployment(this, 'UploadIntegrationTemplate', { sources: [ s3deploy.Source.data( - 'integration-v2.cfn.yaml', - fs.readFileSync(path.join(__dirname, '../../frontend/public/integration-v2.cfn.yaml'), 'utf8'), + 'integration-v3.cfn.yaml', + fs.readFileSync(path.join(__dirname, '../../frontend/public/integration-v3.cfn.yaml'), 'utf8'), ), ], destinationBucket: publicS3Bucket, diff --git a/backend/api/aws_integration.go b/backend/api/aws_integration.go index 80e7d710..0341c284 100644 --- a/backend/api/aws_integration.go +++ b/backend/api/aws_integration.go @@ -15,6 +15,7 @@ func AWSIntegrationFromModel(integration *model.AWSIntegration) apispec.AWSInteg CreationTime: integration.CreationTime, TeamId: integration.TeamId.String(), Name: integration.Name, + RoleArn: integration.RoleARN, GetAccountNamesFromOrganizations: integration.GetAccountNamesFromOrganizations, ManageScps: integration.ManageSCPs, } diff --git a/frontend/public/integration-v2.cfn.yaml b/frontend/public/integration-v3.cfn.yaml similarity index 72% rename from frontend/public/integration-v2.cfn.yaml rename to frontend/public/integration-v3.cfn.yaml index 428d5d76..77071b1e 100644 --- a/frontend/public/integration-v2.cfn.yaml +++ b/frontend/public/integration-v3.cfn.yaml @@ -44,7 +44,7 @@ # Typically we would use the AWS CDK instead of writing CloudFormation templates directly, but CDK # output is much less readable, and we feel it's important that you can review the templates you're # deploying with ease. -Description: Cloud Snitch AWS Integration (V1) +Description: Cloud Snitch AWS Integration (V3) # # These are the input parameters you'll need to provide us. Parameters: @@ -61,6 +61,13 @@ Parameters: - 'Yes' - 'No' Description: Whether to allow read access to AWS Organizations data such as account names. + AllowSCPManagement: + Type: String + Default: 'No' + AllowedValues: + - 'Yes' + - 'No' + Description: Whether to allow the creation of additional service control policies for accounts. S3BucketName: Type: String Default: '' @@ -75,6 +82,7 @@ Parameters: Conditions: HasOrganizationsAccess: !Equals [!Ref AllowOrganizationsAccess, 'Yes'] HasS3BucketName: !Not [!Equals [!Ref S3BucketName, '']] + HasSCPManagement: !Equals [!Ref AllowSCPManagement, 'Yes'] # # Now for the resources... Resources: @@ -116,6 +124,67 @@ Resources: PolicyName: organizations Roles: - !Ref IntegrationRole + # If you specify that you want to allow Cloud Snitch to manage service control policies, we + # create a policy and attach it to the role. + IntegrationSCMManagementPolicy: + Condition: HasSCPManagement + Type: AWS::IAM::Policy + Properties: + PolicyDocument: + Statement: + # These actions allow us to get general information about the organization. + - Action: + - organizations:ListAccounts + - organizations:ListParents + - organizations:ListRoots + - iam:GenerateOrganizationsAccessReport + - iam:GetOrganizationsAccessReport + Effect: Allow + Resource: '*' + # Allow enumerating and attaching service control policies to accounts. Note + # that attaching and detaching policies also requires permissions on the policy + # resource, which are granted in the next statement. + - Action: + - organizations:AttachPolicy + - organizations:DetachPolicy + - organizations:ListPoliciesForTarget + Effect: Allow + Resource: !Sub 'arn:aws:organizations::${AWS::AccountId}:account/*' + Condition: + StringEquals: + organizations:PolicyType: SERVICE_CONTROL_POLICY + # Allow operating on service control policies that are tagged as being managed + # by Cloud Snitch. Permission is not granted to operate on any other policies. + - Action: + - organizations:AttachPolicy + - organizations:DetachPolicy + - organizations:CreatePolicy + - organizations:DescribePolicy + - organizations:DeletePolicy + - organizations:UpdatePolicy + Effect: Allow + Resource: '*' + Condition: + StringEquals: + aws:ResourceTag/CloudSnitchManaged: 'true' + organizations:PolicyType: SERVICE_CONTROL_POLICY + # Lastly, we need to be able to assign the Cloud Snitch managed tag to policies, + # but only on creation. We cannot add the tag to existing policies. + - Action: organizations:TagResource + Effect: Allow + Resource: !Sub 'arn:aws:organizations::${AWS::AccountId}:policy/*' + Condition: + # There's an important subtlety at play here: When the policy is created, + # aws:ResourceTag is based on the tags in the request. Otherwise, the + # aws:ResourceTag is based on the tags already on the resource. This + # allows us to have full control over our policies without being able to + # touch existing policies. + StringEquals: + aws:ResourceTag/CloudSnitchManaged: 'true' + Version: '2012-10-17' + PolicyName: scp-management + Roles: + - !Ref IntegrationRole # If you specify an S3 bucket name, we create a policy for read access and attach it to the role. IntegrationS3Policy: Condition: HasS3BucketName diff --git a/frontend/src/app/(public-area)/cookie-policy/page.tsx b/frontend/src/app/(public-area)/cookie-policy/page.tsx index 9ce0f68e..66d2277f 100644 --- a/frontend/src/app/(public-area)/cookie-policy/page.tsx +++ b/frontend/src/app/(public-area)/cookie-policy/page.tsx @@ -14,7 +14,7 @@ const Page = () => {

Cookie Policy

- Revision: {REVISION} + Revision: {REVISION}

diff --git a/frontend/src/app/(public-area)/pricing/page.tsx b/frontend/src/app/(public-area)/pricing/page.tsx index 3eef6e71..b6a76c97 100644 --- a/frontend/src/app/(public-area)/pricing/page.tsx +++ b/frontend/src/app/(public-area)/pricing/page.tsx @@ -80,13 +80,12 @@ const Page = () => { credits.

- Example: You sign up at the begining of - the month and create your one-person team using an Individual plan. You then set up an AWS - integration and begin ingesting CloudTrail data for 3 AWS accounts. This results in a charge of 3 x - $0.99 = $2.97. Halfway through the month, you upgrade to the Team plan, resulting in a charge of 3 x - ($9.99 - $0.99) x 50% = $13.50. You also decide to add another account, which results in an - additional $9.99 x 50% = $4.98 charge. At the beginning of future billing cycles, you simply pay 4 x - $9.99 = $39.96. + Example: You sign up at the begining of the month and + create your one-person team using an Individual plan. You then set up an AWS integration and begin + ingesting CloudTrail data for 3 AWS accounts. This results in a charge of 3 x $0.99 = $2.97. Halfway + through the month, you upgrade to the Team plan, resulting in a charge of 3 x ($9.99 - $0.99) x 50% + = $13.50. You also decide to add another account, which results in an additional $9.99 x 50% = $4.98 + charge. At the beginning of future billing cycles, you simply pay 4 x $9.99 = $39.96.

*This example excludes any applicable taxes.

diff --git a/frontend/src/app/(public-area)/privacy-policy/page.tsx b/frontend/src/app/(public-area)/privacy-policy/page.tsx index 5feb0db5..5c6054a8 100644 --- a/frontend/src/app/(public-area)/privacy-policy/page.tsx +++ b/frontend/src/app/(public-area)/privacy-policy/page.tsx @@ -15,7 +15,7 @@ const Page = () => {

Privacy Policy

- Revision: {REVISION} + Revision: {REVISION}

diff --git a/frontend/src/app/(public-area)/terms-of-service/page.tsx b/frontend/src/app/(public-area)/terms-of-service/page.tsx index 2016fe95..86c40fd3 100644 --- a/frontend/src/app/(public-area)/terms-of-service/page.tsx +++ b/frontend/src/app/(public-area)/terms-of-service/page.tsx @@ -14,7 +14,7 @@ const Page = () => {

Terms of Service

- Revision: {REVISION} + Revision: {REVISION}

diff --git a/frontend/src/app/(user-area)/teams/[teamId]/Header.tsx b/frontend/src/app/(user-area)/teams/[teamId]/Header.tsx index 8882c0d5..d6ef5570 100644 --- a/frontend/src/app/(user-area)/teams/[teamId]/Header.tsx +++ b/frontend/src/app/(user-area)/teams/[teamId]/Header.tsx @@ -55,9 +55,7 @@ export const Header = (props: Props) => { )} {memberships && memberships.length > 1 && (

-
- Switch Team -
+
Switch Team
{memberships .filter((membership) => membership.team.id !== teamId) .map((membership) => ( diff --git a/frontend/src/app/(user-area)/teams/[teamId]/page.tsx b/frontend/src/app/(user-area)/teams/[teamId]/page.tsx index 7b97a169..6e2a9599 100644 --- a/frontend/src/app/(user-area)/teams/[teamId]/page.tsx +++ b/frontend/src/app/(user-area)/teams/[teamId]/page.tsx @@ -339,9 +339,7 @@ const Page = () => {

There's nothing here, but we can fix that!

{needsSubscriptionSetup && ( <> -

- Activate a subscription -

+

Activate a subscription

Head over to your team's{' '} @@ -354,9 +352,7 @@ const Page = () => { )} {needsAwsIntegrationSetup && ( <> -

- Integrate with your AWS account -

+

Integrate with your AWS account

Go to your team's{' '} diff --git a/frontend/src/app/(user-area)/teams/[teamId]/settings/billing/page.tsx b/frontend/src/app/(user-area)/teams/[teamId]/settings/billing/page.tsx index cc8c2bf3..eb50ba9c 100644 --- a/frontend/src/app/(user-area)/teams/[teamId]/settings/billing/page.tsx +++ b/frontend/src/app/(user-area)/teams/[teamId]/settings/billing/page.tsx @@ -373,7 +373,7 @@ const Page = () => {

{billingProfile?.balance && (
- Balance:{' '} + Balance:{' '} @@ -393,7 +393,7 @@ const Page = () => { ) : ( <>
- Accounts:{' '} + Accounts:{' '} @@ -407,8 +407,8 @@ const Page = () => {
{subscription.price?.accountMonth && (
- Monthly Price:{' '} - {subscription.price.accountMonth.text} per AWS account + Monthly Price: {subscription.price.accountMonth.text}{' '} + per AWS account
)} diff --git a/frontend/src/app/(user-area)/teams/[teamId]/settings/integrations/page.tsx b/frontend/src/app/(user-area)/teams/[teamId]/settings/integrations/page.tsx index e61bc2d7..bf997b92 100644 --- a/frontend/src/app/(user-area)/teams/[teamId]/settings/integrations/page.tsx +++ b/frontend/src/app/(user-area)/teams/[teamId]/settings/integrations/page.tsx @@ -2,11 +2,12 @@ import { clsx } from 'clsx'; import Link from 'next/link'; -import { PlusCircleIcon, TrashIcon } from '@heroicons/react/24/outline'; +import { PencilIcon, PlusCircleIcon, TrashIcon } from '@heroicons/react/24/outline'; import { useState } from 'react'; import { Transition } from '@headlessui/react'; -import { Button, Checkbox, Dialog, ErrorMessage, TextField } from '@/components'; +import { Button, Checkbox, Dialog, ErrorMessage, TextField, Tooltip } from '@/components'; +import { AWSIntegration } from '@/generated/api'; import { useCurrentTeam, useCurrentTeamId, useTeamAwsIntegrations } from '@/hooks'; import { INTEGRATION_TEMPLATE_S3_URL } from '@/integration'; import { useDispatch } from '@/store'; @@ -26,6 +27,7 @@ const CreateIntegrationForm = (props: CreateIntegrationFormProps) => { const [s3BucketName, setS3BucketName] = useState(''); const [s3KeyPrefix, setS3KeyPrefix] = useState(''); const [getAccountNamesFromOrganizations, setGetAccountNamesFromOrganizations] = useState(false); + const [manageScps, setManageScps] = useState(false); const [isBusy, setIsBusy] = useState(false); const [queueReportGeneration, setQueueReportGeneration] = useState(false); const [errorMessage, setErrorMessage] = useState(''); @@ -47,6 +49,7 @@ const CreateIntegrationForm = (props: CreateIntegrationFormProps) => { s3KeyPrefix, }, getAccountNamesFromOrganizations, + manageScps, queueReportGeneration, }, }); @@ -61,6 +64,7 @@ const CreateIntegrationForm = (props: CreateIntegrationFormProps) => { ['CloudSnitchAWSAccountId', process.env.NEXT_PUBLIC_AWS_ACCOUNT_ID], ['TeamId', props.teamId], ['AllowOrganizationsAccess', getAccountNamesFromOrganizations ? 'Yes' : 'No'], + ['AllowSCPManagement', manageScps ? 'Yes' : 'No'], ['S3BucketName', s3BucketName], ['S3KeyPrefix', s3KeyPrefix], ]; @@ -120,9 +124,21 @@ const CreateIntegrationForm = (props: CreateIntegrationFormProps) => { disabled={isBusy} checked={getAccountNamesFromOrganizations} onChange={setGetAccountNamesFromOrganizations} - label="Get account names from AWS Organizations" + label="Enable AWS Organizations integration" subLabel="If you're using an organization trail, we recommend checking this box as this will allow us to show your account names in the UI." /> + + +