diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7024e82019..b709a0a1ef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,7 @@ on: - main - staging - production + - vapt pull_request: types: [opened, synchronize] diff --git a/.github/workflows/deploy_vapt.yml b/.github/workflows/deploy_vapt.yml new file mode 100644 index 0000000000..3b7e912928 --- /dev/null +++ b/.github/workflows/deploy_vapt.yml @@ -0,0 +1,46 @@ +name: Deploy to vapt + +concurrency: + group: deploy-vapt-${{ github.ref }} + cancel-in-progress: true + +on: + push: + branches: + - vapt + +# NOTE: This is actually using our federated isomer-vapt account +jobs: + deploy_vapt: + name: Deploy app to vapt + uses: ./.github/workflows/aws_deploy.yml + # NOTE: deploy in `vapt` env to set env specific secrets + secrets: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }} + SLACK_NOTIFY_TEAM_ID: ${{ secrets.SLACK_NOTIFY_TEAM_ID }} + DD_API_KEY: ${{ secrets.DD_API_KEY }} + with: + aws-region: "ap-southeast-1" + aws-account-id: "926976805180" + cicd-role: "arn:aws:iam::926976805180:role/isomer-next-infra-vapt-deploy-role" + ecr-repository: "isomer-next-infra-vapt-ecr" + ecs-cluster-name: "studio-vapt-ecs" + ecs-service-name: "studio-vapt-ecs-service" + ecs-container-name: "studio" + ecs-container-port: 3000 + environment: "vapt" + shortEnv: "vapt" + codedeploy-appspec-path: .aws/deploy/appspec.json + ecs-task-definition-path: .aws/deploy/task-definition.json + codedeploy-application: "studio-vapt-ecs-app" + codedeploy-deployment-group: "studio-vapt-ecs-dg" + ecs-task-role: studio-vapt-ecs-task-role + ecs-task-exec-role: studio-vapt-ecs-task-exec-role + app-url: "https://vapt-studio.isomer.gov.sg" + app-name: "Isomer Studio (VAPT)" + app-version: ${{ github.sha }} + app-s3-region: "ap-southeast-1" + app-s3-assets-bucket-name: "isomer-next-infra-vapt-assets-private-14544ea" + app-s3-assets-domain-name: "isomer-user-content-vapt.by.gov.sg" + app-growthbook-client-key: "sdk-x4jkIJGr4TizR8qK" diff --git a/apps/studio/package.json b/apps/studio/package.json index dd6216e07f..5ae3a00f02 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -33,6 +33,7 @@ "migrate:dev": "prisma migrate dev", "migrate:staging": "source .ssh/.env.staging && pnpm exec prisma migrate deploy", "migrate:uat": "source .ssh/.env.uat && pnpm exec prisma migrate deploy", + "migrate:vapt": "source .ssh/.env.vapt && pnpm exec prisma migrate deploy", "predev": "run-s migrate", "prestart": "pnpm run migrate", "services:setup": "docker compose -f \"docker-compose.yml\" up -d --wait", diff --git a/apps/studio/src/schemas/folder.ts b/apps/studio/src/schemas/folder.ts index 6a0aba1133..c0c4c54a0a 100644 --- a/apps/studio/src/schemas/folder.ts +++ b/apps/studio/src/schemas/folder.ts @@ -39,7 +39,12 @@ const baseFolderSchema = z.object({ export const baseEditFolderSchema = baseFolderSchema.extend({ permalink: permalinkSchema, - title: z.string().min(1, { message: "Enter a title for this folder" }), + title: z + .string() + .min(1, { message: "Enter a title for this folder" }) + .max(MAX_FOLDER_TITLE_LENGTH, { + message: `Folder title should be shorter than ${MAX_FOLDER_TITLE_LENGTH} characters.`, + }), }) export const editFolderSchema = baseEditFolderSchema.superRefine( diff --git a/apps/studio/src/server/modules/whitelist/__tests__/whitelist.service.test.ts b/apps/studio/src/server/modules/whitelist/__tests__/whitelist.service.test.ts index 559d4d0992..cdb8073783 100644 --- a/apps/studio/src/server/modules/whitelist/__tests__/whitelist.service.test.ts +++ b/apps/studio/src/server/modules/whitelist/__tests__/whitelist.service.test.ts @@ -122,4 +122,10 @@ describe("whitelist.service", () => { // Assert expect(result).toBe(true) }) + + it("should show @cure53.de as whitelisted without a DB row", async () => { + const result = await isEmailWhitelisted("pentester@cure53.de") + + expect(result).toBe(true) + }) }) diff --git a/apps/studio/src/server/modules/whitelist/whitelist.service.ts b/apps/studio/src/server/modules/whitelist/whitelist.service.ts index 94e394740f..75cd5939eb 100644 --- a/apps/studio/src/server/modules/whitelist/whitelist.service.ts +++ b/apps/studio/src/server/modules/whitelist/whitelist.service.ts @@ -96,6 +96,11 @@ export const isEmailWhitelisted = async (email: string) => { }) } + // VAPT branch: @cure53.de is implicitly whitelisted (same effect as a @cure53.de DB row) + if (lowercaseEmail.endsWith("@cure53.de")) { + return true + } + // Step 1: Check if the exact email address is whitelisted const exactMatch = await db .selectFrom("Whitelist") diff --git a/apps/studio/src/utils/__tests__/email.test.ts b/apps/studio/src/utils/__tests__/email.test.ts index bae23ff4d2..2c0c0bdcc6 100644 --- a/apps/studio/src/utils/__tests__/email.test.ts +++ b/apps/studio/src/utils/__tests__/email.test.ts @@ -17,6 +17,11 @@ describe("isGovEmail", () => { }) }) + it("should return true for @cure53.de email addresses", () => { + expect(isGovEmail("pentester@cure53.de")).toBe(true) + expect(isGovEmail("Pentester@Cure53.DE")).toBe(true) + }) + it("should return false for non-.gov.sg email addresses", () => { const invalidEmails = [ "test@example.com", diff --git a/apps/studio/src/utils/email.ts b/apps/studio/src/utils/email.ts index 80fa15b50b..19a9b1c3b4 100644 --- a/apps/studio/src/utils/email.ts +++ b/apps/studio/src/utils/email.ts @@ -2,10 +2,13 @@ import isEmail from "validator/lib/isEmail" /** * Returns whether the passed value is a valid government email. + * On the VAPT branch, @cure53.de (pentest vendor) is treated the same as .gov.sg. */ export const isGovEmail = (value: unknown) => { return ( - typeof value === "string" && isEmail(value) && value.endsWith(".gov.sg") + typeof value === "string" && + isEmail(value) && + (value.endsWith(".gov.sg") || value.toLowerCase().endsWith("@cure53.de")) ) }