diff --git a/.commitlintrc.js b/.commitlintrc.js new file mode 100644 index 0000000..66b23ed --- /dev/null +++ b/.commitlintrc.js @@ -0,0 +1,41 @@ +module.exports = { + extends: ["@commitlint/config-conventional"], + rules: { + "type-enum": [ + 2, + "always", + [ + "feat", + "fix", + "chore", + "docs", + "style", + "refactor", + "perf", + "test", + "ci", + "build", + "revert", + ], + ], + "scope-enum": [ + 1, + "always", + [ + "backend", + "frontend", + "infra", + "api", + "worker", + "ai", + "scanner", + "graph", + "ci", + "deps", + ], + ], + "subject-case": [2, "always", "lower-case"], + "subject-empty": [2, "never"], + "type-empty": [2, "never"], + }, +}; diff --git a/.commitlintrc.json b/.commitlintrc.json deleted file mode 100644 index c76876c..0000000 --- a/.commitlintrc.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "extends": [ - "@commitlint/config-conventional" - ], - "rules": { - "type-enum": [ - 2, - "always", - [ - "feat", - "fix", - "chore", - "docs", - "style", - "refactor", - "perf", - "test", - "ci", - "build", - "revert" - ] - ], - "scope-enum": [ - 1, - "always", - [ - "backend", - "frontend", - "infra", - "api", - "worker", - "ai", - "scanner", - "graph", - "ci", - "deps" - ] - ], - "subject-case": [ - 2, - "always", - "lower-case" - ], - "subject-empty": [ - 2, - "never" - ], - "type-empty": [ - 2, - "never" - ] - } -} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0169452..5f85dc8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,13 +25,18 @@ jobs: with: node-version: 20 + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Install commitlint run: | - npm install -g @commitlint/cli @commitlint/config-conventional + bun install - name: Validate PR commits run: | - npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose + bunx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose go-checks: runs-on: ubuntu-latest @@ -147,29 +152,27 @@ jobs: - name: Checkout repo uses: actions/checkout@v4 - - name: Set up Node - uses: actions/setup-node@v4 + - name: Set up Bun + uses: oven-sh/setup-bun@v2 with: - node-version: 20 - cache: "npm" - cache-dependency-path: frontend/package-lock.json + bun-version: latest - name: Frontend lint and format run: | cd frontend - npm ci - npm run lint - npm run format:check + bun install + bun run lint + bun run format:check - name: Frontend tests run: | cd frontend - npm run test + bun run test - name: Security scan (Frontend) run: | cd frontend - npm audit --omit=dev || true + bun audit --omit=dev || true docker-build: runs-on: ubuntu-latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3042e5f..8495818 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -101,14 +101,14 @@ repos: # Frontend checks - id: eslint-check name: eslint (frontend) - entry: bash -c 'cd frontend && npm run lint' + entry: bash -c 'cd frontend && bun run lint' language: system files: ^frontend/.*\.(ts|tsx|js|jsx)$ pass_filenames: false - id: prettier-check name: prettier check (frontend) - entry: bash -c 'cd frontend && npm run format:check' + entry: bash -c 'cd frontend && bun run format:check' language: system files: ^frontend/.*\.(ts|tsx|js|jsx|json|css|md)$ pass_filenames: false @@ -124,7 +124,7 @@ repos: # Commitlint - id: commitlint name: commitlint - entry: bash -c 'npx --yes @commitlint/cli --edit $1' + entry: bunx commitlint --edit language: system stages: [commit-msg] always_run: true diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..4dd9a7d --- /dev/null +++ b/bun.lock @@ -0,0 +1,220 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "cloudcop", + "devDependencies": { + "@commitlint/cli": "^19.5.0", + "@commitlint/config-conventional": "^19.5.0", + }, + }, + }, + "packages": { + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@commitlint/cli": ["@commitlint/cli@19.8.1", "", { "dependencies": { "@commitlint/format": "^19.8.1", "@commitlint/lint": "^19.8.1", "@commitlint/load": "^19.8.1", "@commitlint/read": "^19.8.1", "@commitlint/types": "^19.8.1", "tinyexec": "^1.0.0", "yargs": "^17.0.0" }, "bin": { "commitlint": "./cli.js" } }, "sha512-LXUdNIkspyxrlV6VDHWBmCZRtkEVRpBKxi2Gtw3J54cGWhLCTouVD/Q6ZSaSvd2YaDObWK8mDjrz3TIKtaQMAA=="], + + "@commitlint/config-conventional": ["@commitlint/config-conventional@19.8.1", "", { "dependencies": { "@commitlint/types": "^19.8.1", "conventional-changelog-conventionalcommits": "^7.0.2" } }, "sha512-/AZHJL6F6B/G959CsMAzrPKKZjeEiAVifRyEwXxcT6qtqbPwGw+iQxmNS+Bu+i09OCtdNRW6pNpBvgPrtMr9EQ=="], + + "@commitlint/config-validator": ["@commitlint/config-validator@19.8.1", "", { "dependencies": { "@commitlint/types": "^19.8.1", "ajv": "^8.11.0" } }, "sha512-0jvJ4u+eqGPBIzzSdqKNX1rvdbSU1lPNYlfQQRIFnBgLy26BtC0cFnr7c/AyuzExMxWsMOte6MkTi9I3SQ3iGQ=="], + + "@commitlint/ensure": ["@commitlint/ensure@19.8.1", "", { "dependencies": { "@commitlint/types": "^19.8.1", "lodash.camelcase": "^4.3.0", "lodash.kebabcase": "^4.1.1", "lodash.snakecase": "^4.1.1", "lodash.startcase": "^4.4.0", "lodash.upperfirst": "^4.3.1" } }, "sha512-mXDnlJdvDzSObafjYrOSvZBwkD01cqB4gbnnFuVyNpGUM5ijwU/r/6uqUmBXAAOKRfyEjpkGVZxaDsCVnHAgyw=="], + + "@commitlint/execute-rule": ["@commitlint/execute-rule@19.8.1", "", {}, "sha512-YfJyIqIKWI64Mgvn/sE7FXvVMQER/Cd+s3hZke6cI1xgNT/f6ZAz5heND0QtffH+KbcqAwXDEE1/5niYayYaQA=="], + + "@commitlint/format": ["@commitlint/format@19.8.1", "", { "dependencies": { "@commitlint/types": "^19.8.1", "chalk": "^5.3.0" } }, "sha512-kSJj34Rp10ItP+Eh9oCItiuN/HwGQMXBnIRk69jdOwEW9llW9FlyqcWYbHPSGofmjsqeoxa38UaEA5tsbm2JWw=="], + + "@commitlint/is-ignored": ["@commitlint/is-ignored@19.8.1", "", { "dependencies": { "@commitlint/types": "^19.8.1", "semver": "^7.6.0" } }, "sha512-AceOhEhekBUQ5dzrVhDDsbMaY5LqtN8s1mqSnT2Kz1ERvVZkNihrs3Sfk1Je/rxRNbXYFzKZSHaPsEJJDJV8dg=="], + + "@commitlint/lint": ["@commitlint/lint@19.8.1", "", { "dependencies": { "@commitlint/is-ignored": "^19.8.1", "@commitlint/parse": "^19.8.1", "@commitlint/rules": "^19.8.1", "@commitlint/types": "^19.8.1" } }, "sha512-52PFbsl+1EvMuokZXLRlOsdcLHf10isTPlWwoY1FQIidTsTvjKXVXYb7AvtpWkDzRO2ZsqIgPK7bI98x8LRUEw=="], + + "@commitlint/load": ["@commitlint/load@19.8.1", "", { "dependencies": { "@commitlint/config-validator": "^19.8.1", "@commitlint/execute-rule": "^19.8.1", "@commitlint/resolve-extends": "^19.8.1", "@commitlint/types": "^19.8.1", "chalk": "^5.3.0", "cosmiconfig": "^9.0.0", "cosmiconfig-typescript-loader": "^6.1.0", "lodash.isplainobject": "^4.0.6", "lodash.merge": "^4.6.2", "lodash.uniq": "^4.5.0" } }, "sha512-9V99EKG3u7z+FEoe4ikgq7YGRCSukAcvmKQuTtUyiYPnOd9a2/H9Ak1J9nJA1HChRQp9OA/sIKPugGS+FK/k1A=="], + + "@commitlint/message": ["@commitlint/message@19.8.1", "", {}, "sha512-+PMLQvjRXiU+Ae0Wc+p99EoGEutzSXFVwQfa3jRNUZLNW5odZAyseb92OSBTKCu+9gGZiJASt76Cj3dLTtcTdg=="], + + "@commitlint/parse": ["@commitlint/parse@19.8.1", "", { "dependencies": { "@commitlint/types": "^19.8.1", "conventional-changelog-angular": "^7.0.0", "conventional-commits-parser": "^5.0.0" } }, "sha512-mmAHYcMBmAgJDKWdkjIGq50X4yB0pSGpxyOODwYmoexxxiUCy5JJT99t1+PEMK7KtsCtzuWYIAXYAiKR+k+/Jw=="], + + "@commitlint/read": ["@commitlint/read@19.8.1", "", { "dependencies": { "@commitlint/top-level": "^19.8.1", "@commitlint/types": "^19.8.1", "git-raw-commits": "^4.0.0", "minimist": "^1.2.8", "tinyexec": "^1.0.0" } }, "sha512-03Jbjb1MqluaVXKHKRuGhcKWtSgh3Jizqy2lJCRbRrnWpcM06MYm8th59Xcns8EqBYvo0Xqb+2DoZFlga97uXQ=="], + + "@commitlint/resolve-extends": ["@commitlint/resolve-extends@19.8.1", "", { "dependencies": { "@commitlint/config-validator": "^19.8.1", "@commitlint/types": "^19.8.1", "global-directory": "^4.0.1", "import-meta-resolve": "^4.0.0", "lodash.mergewith": "^4.6.2", "resolve-from": "^5.0.0" } }, "sha512-GM0mAhFk49I+T/5UCYns5ayGStkTt4XFFrjjf0L4S26xoMTSkdCf9ZRO8en1kuopC4isDFuEm7ZOm/WRVeElVg=="], + + "@commitlint/rules": ["@commitlint/rules@19.8.1", "", { "dependencies": { "@commitlint/ensure": "^19.8.1", "@commitlint/message": "^19.8.1", "@commitlint/to-lines": "^19.8.1", "@commitlint/types": "^19.8.1" } }, "sha512-Hnlhd9DyvGiGwjfjfToMi1dsnw1EXKGJNLTcsuGORHz6SS9swRgkBsou33MQ2n51/boIDrbsg4tIBbRpEWK2kw=="], + + "@commitlint/to-lines": ["@commitlint/to-lines@19.8.1", "", {}, "sha512-98Mm5inzbWTKuZQr2aW4SReY6WUukdWXuZhrqf1QdKPZBCCsXuG87c+iP0bwtD6DBnmVVQjgp4whoHRVixyPBg=="], + + "@commitlint/top-level": ["@commitlint/top-level@19.8.1", "", { "dependencies": { "find-up": "^7.0.0" } }, "sha512-Ph8IN1IOHPSDhURCSXBz44+CIu+60duFwRsg6HqaISFHQHbmBtxVw4ZrFNIYUzEP7WwrNPxa2/5qJ//NK1FGcw=="], + + "@commitlint/types": ["@commitlint/types@19.8.1", "", { "dependencies": { "@types/conventional-commits-parser": "^5.0.0", "chalk": "^5.3.0" } }, "sha512-/yCrWGCoA1SVKOks25EGadP9Pnj0oAIHGpl2wH2M2Y46dPM2ueb8wyCVOD7O3WCTkaJ0IkKvzhl1JY7+uCT2Dw=="], + + "@types/conventional-commits-parser": ["@types/conventional-commits-parser@5.0.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g=="], + + "@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + + "JSONStream": ["JSONStream@1.3.5", "", { "dependencies": { "jsonparse": "^1.2.0", "through": ">=2.2.7 <3" }, "bin": { "JSONStream": "./bin.js" } }, "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ=="], + + "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "array-ify": ["array-ify@1.0.0", "", {}, "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "compare-func": ["compare-func@2.0.0", "", { "dependencies": { "array-ify": "^1.0.0", "dot-prop": "^5.1.0" } }, "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA=="], + + "conventional-changelog-angular": ["conventional-changelog-angular@7.0.0", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ=="], + + "conventional-changelog-conventionalcommits": ["conventional-changelog-conventionalcommits@7.0.2", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w=="], + + "conventional-commits-parser": ["conventional-commits-parser@5.0.0", "", { "dependencies": { "JSONStream": "^1.3.5", "is-text-path": "^2.0.0", "meow": "^12.0.1", "split2": "^4.0.0" }, "bin": { "conventional-commits-parser": "cli.mjs" } }, "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA=="], + + "cosmiconfig": ["cosmiconfig@9.0.0", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg=="], + + "cosmiconfig-typescript-loader": ["cosmiconfig-typescript-loader@6.2.0", "", { "dependencies": { "jiti": "^2.6.1" }, "peerDependencies": { "@types/node": "*", "cosmiconfig": ">=9", "typescript": ">=5" } }, "sha512-GEN39v7TgdxgIoNcdkRE3uiAzQt3UXLyHbRHD6YoL048XAeOomyxaP+Hh/+2C6C2wYjxJ2onhJcsQp+L4YEkVQ=="], + + "dargs": ["dargs@8.1.0", "", {}, "sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw=="], + + "dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], + + "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + + "find-up": ["find-up@7.0.0", "", { "dependencies": { "locate-path": "^7.2.0", "path-exists": "^5.0.0", "unicorn-magic": "^0.1.0" } }, "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "git-raw-commits": ["git-raw-commits@4.0.0", "", { "dependencies": { "dargs": "^8.0.0", "meow": "^12.0.1", "split2": "^4.0.0" }, "bin": { "git-raw-commits": "cli.mjs" } }, "sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ=="], + + "global-directory": ["global-directory@4.0.1", "", { "dependencies": { "ini": "4.1.1" } }, "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], + + "ini": ["ini@4.1.1", "", {}, "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g=="], + + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="], + + "is-text-path": ["is-text-path@2.0.0", "", { "dependencies": { "text-extensions": "^2.0.0" } }, "sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw=="], + + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "jsonparse": ["jsonparse@1.3.1", "", {}, "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "locate-path": ["locate-path@7.2.0", "", { "dependencies": { "p-locate": "^6.0.0" } }, "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA=="], + + "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], + + "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="], + + "lodash.kebabcase": ["lodash.kebabcase@4.1.1", "", {}, "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "lodash.mergewith": ["lodash.mergewith@4.6.2", "", {}, "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ=="], + + "lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="], + + "lodash.startcase": ["lodash.startcase@4.4.0", "", {}, "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg=="], + + "lodash.uniq": ["lodash.uniq@4.5.0", "", {}, "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ=="], + + "lodash.upperfirst": ["lodash.upperfirst@4.3.1", "", {}, "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg=="], + + "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + + "meow": ["meow@12.1.1", "", {}, "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "p-limit": ["p-limit@4.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ=="], + + "p-locate": ["p-locate@6.0.0", "", { "dependencies": { "p-limit": "^4.0.0" } }, "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + + "semver": ["semver@7.6.0", "", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" } }, "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg=="], + + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "text-extensions": ["text-extensions@2.4.0", "", {}, "sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g=="], + + "through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="], + + "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "unicorn-magic": ["unicorn-magic@0.1.0", "", {}, "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ=="], + + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], + + "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + } +} diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json deleted file mode 100644 index 97a2bb8..0000000 --- a/frontend/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": ["next", "next/core-web-vitals"] -} diff --git a/frontend/.prettierignore b/frontend/.prettierignore deleted file mode 100644 index e09dd9d..0000000 --- a/frontend/.prettierignore +++ /dev/null @@ -1,8 +0,0 @@ -eslint.config.mjs -next.config.ts -postcss.config.mjs -package*.json -tsconfig.json -.eslintrc.json -.prettierrc -README.md \ No newline at end of file diff --git a/frontend/.prettierrc b/frontend/.prettierrc deleted file mode 100644 index b2095be..0000000 --- a/frontend/.prettierrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "semi": false, - "singleQuote": true -} diff --git a/frontend/Dockerfile b/frontend/Dockerfile index e3e632f..93d5282 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,22 +1,22 @@ -FROM node:20-alpine AS base +FROM oven/bun:alpine AS base FROM base AS deps WORKDIR /app -COPY package.json package-lock.json ./ -RUN npm ci +COPY package.json bun.lock ./ +RUN bun install --frozen-lockfile FROM base AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . -RUN npm run build +RUN bun run build FROM base AS development WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . EXPOSE 3000 -CMD ["npm", "run", "dev"] +CMD ["bun", "run", "dev"] FROM base AS production WORKDIR /app @@ -26,4 +26,4 @@ COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static EXPOSE 3000 ENV PORT 3000 -CMD ["node", "server.js"] +CMD ["bun", "server.js"] diff --git a/frontend/README.md b/frontend/README.md index ba76396..e215bc4 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,28 +1,36 @@ -Frontend application built with Next.js, providing the user interface for CloudCop. +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). -## Development +## Getting Started -Install dependencies: +First, run the development server: ```bash -npm install +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev ``` -Lint and format: +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. -```bash -npm run lint -npm run format -``` +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. -Run tests: +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. -```bash -npm run test -``` +## Learn More -Run locally: +To learn more about Next.js, take a look at the following resources: -```bash -npm run dev -``` +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/frontend/app/(dashboard)/accounts/_components/accounts-list.tsx b/frontend/app/(dashboard)/accounts/_components/accounts-list.tsx new file mode 100644 index 0000000..cd9081a --- /dev/null +++ b/frontend/app/(dashboard)/accounts/_components/accounts-list.tsx @@ -0,0 +1,192 @@ +"use client"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + CheckCircleIcon, + WarningCircleIcon, + DotsThreeIcon, + PlayIcon, + TrashIcon, + ArrowClockwiseIcon, + CloudIcon, +} from "@phosphor-icons/react"; +import Link from "next/link"; + +// Mock data - will be replaced with real API calls +const accounts = [ + { + id: "1", + accountId: "123456789012", + name: "Production", + roleArn: "arn:aws:iam::123456789012:role/CloudCopSecurityScanRole", + externalId: "cc-ext-abc123", + verified: true, + lastVerifiedAt: "2024-12-22T10:00:00Z", + lastScanAt: "2024-12-22T10:30:00Z", + findingsCount: 32, + }, + { + id: "2", + accountId: "987654321098", + name: "Development", + roleArn: "arn:aws:iam::987654321098:role/CloudCopSecurityScanRole", + externalId: "cc-ext-def456", + verified: true, + lastVerifiedAt: "2024-12-21T14:00:00Z", + lastScanAt: "2024-12-21T15:00:00Z", + findingsCount: 15, + }, + { + id: "3", + accountId: "456789123456", + name: "Staging", + roleArn: "arn:aws:iam::456789123456:role/CloudCopSecurityScanRole", + externalId: "cc-ext-ghi789", + verified: false, + lastVerifiedAt: null, + lastScanAt: null, + findingsCount: 0, + }, +]; + +function formatDate(dateString: string | null) { + if (!dateString) return "Never"; + return new Date(dateString).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +} + +export function AccountsList() { + if (accounts.length === 0) { + return ( +
+ +

No AWS accounts connected

+

+ Connect your first AWS account to start scanning for security issues. +

+
+ ); + } + + return ( + + + + Account + Account ID + Status + Last Scan + Findings + Actions + + + + {accounts.map((account) => ( + + +
+
+ +
+
+
{account.name}
+
+ {account.roleArn} +
+
+
+
+ + + {account.accountId} + + + + {account.verified ? ( + + + Verified + + ) : ( + + + Pending + + )} + + + + {formatDate(account.lastScanAt)} + + + + {account.findingsCount > 0 ? ( + {account.findingsCount} + ) : ( + - + )} + + + + + + + + {account.verified ? ( + <> + + + + Run Scan + + + + + Re-verify Connection + + + ) : ( + + + Verify Connection + + )} + + + + Remove Account + + + + +
+ ))} +
+
+ ); +} diff --git a/frontend/app/(dashboard)/accounts/_components/add-account-dialog.tsx b/frontend/app/(dashboard)/accounts/_components/add-account-dialog.tsx new file mode 100644 index 0000000..97429be --- /dev/null +++ b/frontend/app/(dashboard)/accounts/_components/add-account-dialog.tsx @@ -0,0 +1,291 @@ +"use client"; + +import * as React from "react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"; +import { + PlusIcon, + CheckIcon, + CloudIcon, + ArrowRightIcon, + CheckCircleIcon, + CircleNotchIcon, +} from "@phosphor-icons/react"; + +const cloudFormationTemplate = `https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/create/review?templateURL=https://cloudcop-templates.s3.amazonaws.com/guard-scan-role.yaml&stackName=cloudcop-security-role`; + +export function AddAccountDialog() { + const [step, setStep] = React.useState(1); + const [accountId, setAccountId] = React.useState(""); + const [externalId, setExternalId] = React.useState(""); + const [roleArn, setRoleArn] = React.useState(""); + const [isVerifying, setIsVerifying] = React.useState(false); + const [isVerified, setIsVerified] = React.useState(false); + const [open, setOpen] = React.useState(false); + + const handleVerify = async () => { + setIsVerifying(true); + // TODO: Call GraphQL mutation to verify account + setTimeout(() => { + setIsVerifying(false); + setIsVerified(true); + }, 2000); + }; + + const handleConnect = async () => { + // TODO: Call GraphQL mutation to connect account + setOpen(false); + // Reset state + setStep(1); + setAccountId(""); + setExternalId(""); + setRoleArn(""); + setIsVerified(false); + }; + + const resetDialog = () => { + setStep(1); + setAccountId(""); + setExternalId(""); + setRoleArn(""); + setIsVerified(false); + }; + + return ( + { + setOpen(o); + if (!o) resetDialog(); + }} + > + + + + + + Connect AWS Account + + Follow these steps to securely connect your AWS account + + + + {/* Progress Steps */} +
+ {[1, 2, 3].map((s) => ( + +
= s + ? "bg-primary text-primary-foreground" + : "bg-muted text-muted-foreground" + }`} + > + {step > s ? : s} +
+ {s < 3 && ( +
s ? "bg-primary" : "bg-muted" + }`} + /> + )} + + ))} +
+ + {/* Step 1: Deploy CloudFormation */} + {step === 1 && ( +
+
+

Deploy CloudFormation Stack

+

+ Click the button below to open AWS CloudFormation with our + pre-configured template. This creates a read-only IAM role for + CloudCop. +

+ +
+ +
+

What this creates:

+
    +
  • + + Read-only IAM role with security scanning permissions +
  • +
  • + + External ID for secure cross-account access +
  • +
  • + + No write permissions - CloudCop cannot modify your resources +
  • +
+
+ +
+ + +
+
+ )} + + {/* Step 2: Enter Details */} + {step === 2 && ( +
+

+ Enter the details from your CloudFormation stack outputs. +

+ + + + AWS Account ID + setAccountId(e.target.value)} + /> + + + + Role ARN + setRoleArn(e.target.value)} + /> + + + + External ID + setExternalId(e.target.value)} + /> + + + +
+ + +
+
+ )} + + {/* Step 3: Verify Connection */} + {step === 3 && ( +
+
+

Account Details

+
+
+ Account ID: + + {accountId} + +
+
+ Role ARN: + + {roleArn} + +
+
+ External ID: + + {externalId} + +
+
+
+ + {isVerified ? ( +
+ +
+

+ Connection Verified +

+

+ Successfully assumed role in your AWS account +

+
+
+ ) : ( + + )} + +
+ + +
+
+ )} + +
+ ); +} diff --git a/frontend/app/(dashboard)/accounts/page.tsx b/frontend/app/(dashboard)/accounts/page.tsx new file mode 100644 index 0000000..d5592c4 --- /dev/null +++ b/frontend/app/(dashboard)/accounts/page.tsx @@ -0,0 +1,88 @@ +"use client"; + +import { PageHeader } from "@/components/page-header"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { AccountsList } from "./_components/accounts-list"; +import { AddAccountDialog } from "./_components/add-account-dialog"; + +export default function AccountsPage() { + return ( + <> + } + /> + +
+ + + Connected AWS Accounts + + Manage your connected AWS accounts for security scanning + + + + + + + + + + How it Works + + CloudCop uses AWS IAM roles for secure, read-only access to your + accounts + + + +
+
+
+
+ 1 +
+

Deploy CloudFormation

+
+

+ Deploy our CloudFormation stack that creates a read-only IAM + role with security scanning permissions. +

+
+
+
+
+ 2 +
+

Add Account Details

+
+

+ Enter your AWS Account ID and the External ID from the + CloudFormation outputs. +

+
+
+
+
+ 3 +
+

Verify & Scan

+
+

+ We verify the connection using STS AssumeRole and you're + ready to run security scans. +

+
+
+
+
+
+ + ); +} diff --git a/frontend/app/(dashboard)/chat/_components/chat-message.tsx b/frontend/app/(dashboard)/chat/_components/chat-message.tsx new file mode 100644 index 0000000..cd173d9 --- /dev/null +++ b/frontend/app/(dashboard)/chat/_components/chat-message.tsx @@ -0,0 +1,157 @@ +"use client"; + +import * as React from "react"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { Button } from "@/components/ui/button"; +import { + RobotIcon, + UserIcon, + CopyIcon, + CheckIcon, +} from "@phosphor-icons/react"; + +interface Message { + id: string; + role: "user" | "assistant"; + content: string; + timestamp: Date; +} + +interface ChatMessageProps { + message: Message; +} + +export function ChatMessage({ message }: ChatMessageProps) { + const [copiedCode, setCopiedCode] = React.useState(null); + const isAssistant = message.role === "assistant"; + + const copyToClipboard = (code: string) => { + navigator.clipboard.writeText(code); + setCopiedCode(code); + setTimeout(() => setCopiedCode(null), 2000); + }; + + return ( +
+ + + {isAssistant ? ( + + ) : ( + + )} + + + +
+
+ {isAssistant ? "CloudCop AI" : "You"} + + {message.timestamp.toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + })} + +
+ +
+ {isAssistant ? ( +
+ {message.content.split("```").map((part, index) => { + if (index % 2 === 1) { + // This is a code block + const [lang, ...codeLines] = part.split("\n"); + const code = codeLines.join("\n").trim(); + return ( +
+
+ + {lang || "bash"} + + +
+
+                        {code}
+                      
+
+ ); + } + // Regular text - render with markdown-like formatting + return ( +
+ {part.split("\n").map((line, lineIndex) => { + // Handle bold text + const formattedLine = line.replace( + /\*\*(.*?)\*\*/g, + "$1", + ); + // Handle inline code + const withCode = formattedLine.replace( + /`([^`]+)`/g, + '$1', + ); + + if (line.startsWith("- ")) { + return ( +
${withCode.slice(2)}`, + }} + /> + ); + } + return ( +

+ ); + })} +

+ ); + })} +
+ ) : ( +

{message.content}

+ )} +
+
+
+ ); +} diff --git a/frontend/app/(dashboard)/chat/_components/suggested-questions.tsx b/frontend/app/(dashboard)/chat/_components/suggested-questions.tsx new file mode 100644 index 0000000..2a413b2 --- /dev/null +++ b/frontend/app/(dashboard)/chat/_components/suggested-questions.tsx @@ -0,0 +1,81 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + ShieldWarningIcon, + PathIcon, + DatabaseIcon, +} from "@phosphor-icons/react"; + +const suggestedQuestions = [ + { + category: "Resources", + icon: DatabaseIcon, + questions: [ + "How many EC2 instances are running?", + "List all S3 buckets in my account", + "Which IAM users have console access?", + ], + }, + { + category: "Security", + icon: ShieldWarningIcon, + questions: [ + "Find S3 buckets with public access", + "Which security groups allow SSH from anywhere?", + "Show me critical security findings", + ], + }, + { + category: "Attack Paths", + icon: PathIcon, + questions: [ + "Find attack paths to my database", + "How can an attacker reach sensitive S3 buckets?", + "Show privilege escalation paths", + ], + }, +]; + +interface SuggestedQuestionsProps { + onSelect: (question: string) => void; +} + +export function SuggestedQuestions({ onSelect }: SuggestedQuestionsProps) { + return ( +
+
+

How can I help?

+

+ Here are some things you can ask me about your AWS environment +

+
+ +
+ {suggestedQuestions.map((category) => ( + + + + + {category.category} + + + + {category.questions.map((question) => ( + + ))} + + + ))} +
+
+ ); +} diff --git a/frontend/app/(dashboard)/chat/page.tsx b/frontend/app/(dashboard)/chat/page.tsx new file mode 100644 index 0000000..0cf31e2 --- /dev/null +++ b/frontend/app/(dashboard)/chat/page.tsx @@ -0,0 +1,285 @@ +"use client"; + +import * as React from "react"; +import { PageHeader } from "@/components/page-header"; +import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Badge } from "@/components/ui/badge"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { + PaperPlaneTiltIcon, + RobotIcon, + SparkleIcon, +} from "@phosphor-icons/react"; +import { ChatMessage } from "./_components/chat-message"; +import { SuggestedQuestions } from "./_components/suggested-questions"; + +interface Message { + id: string; + role: "user" | "assistant"; + content: string; + timestamp: Date; +} + +const INITIAL_MESSAGE_CONTENT = + "Hello! I'm CloudCop AI, your cloud security assistant. I can help you understand your AWS environment, find security issues, and generate remediation commands. What would you like to know?"; + +export default function ChatPage() { + const [messages, setMessages] = React.useState([]); + const [input, setInput] = React.useState(""); + const [isLoading, setIsLoading] = React.useState(false); + const [isInitialized, setIsInitialized] = React.useState(false); + const scrollRef = React.useRef(null); + + // Initialize messages on client side only to avoid hydration mismatch + React.useEffect(() => { + if (!isInitialized) { + setMessages([ + { + id: "1", + role: "assistant", + content: INITIAL_MESSAGE_CONTENT, + timestamp: new Date(), + }, + ]); + setIsInitialized(true); + } + }, [isInitialized]); + + const handleSend = async () => { + if (!input.trim() || isLoading) return; + + const userMessage = { + id: Date.now().toString(), + role: "user" as const, + content: input, + timestamp: new Date(), + }; + + setMessages((prev) => [...prev, userMessage]); + setInput(""); + setIsLoading(true); + + // Simulate AI response - will be replaced with real API call + setTimeout(() => { + const aiResponse = { + id: (Date.now() + 1).toString(), + role: "assistant" as const, + content: getAIResponse(input), + timestamp: new Date(), + }; + setMessages((prev) => [...prev, aiResponse]); + setIsLoading(false); + }, 1500); + }; + + const handleSuggestedQuestion = (question: string) => { + setInput(question); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }; + + React.useEffect(() => { + if (scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }, [messages]); + + return ( + <> + + + AI Powered + + } + /> + +
+
+ +
+ {messages.map((message) => ( + + ))} + {isLoading && ( +
+ + + + + +
+
+ CloudCop AI + is thinking... +
+
+ + + +
+
+
+ )} + + {messages.length === 1 && !isLoading && ( + + )} +
+
+
+ + {/* Input Area */} +
+
+
+