From 8c47822f4fd33dd32c2c17ff515b3a0bda16b504 Mon Sep 17 00:00:00 2001 From: Kishore <42832651+kishore03109@users.noreply.github.com> Date: Thu, 27 Jun 2024 14:08:52 +0800 Subject: [PATCH] feat(monitoring): add dns reporter (#1376) ## Problem This is a first pr that is up to add some level of sane reporting. While scheduling is part of this feature, it is not within the scope of this pr. This pr only adds (currently dead code) logic to grab the domains that we own in isomer, and do a dns dig. This is meant to be verbose, and in the future alarms can be added based on the results of this. This is not meant to replace monitoring, it is just meant to fine tune some blind spots that uptime robot currently has + some sane checker during incident response to show history of dns records for a site that we manage. I am opting to log it directly in our backend to keep things simple. will add alarms + the scheduler in subsequent prs. ## Solution grab ALL domains from keycdn + amplify + redirection records + log dns records on them. **Breaking Changes** - [ ] Yes - this PR contains breaking changes - Details ... - [X] No - this PR is backwards compatible with ALL of the following feature flags in this [doc](https://www.notion.so/opengov/Existing-feature-flags-518ad2cdc325420893a105e88c432be5) ## Tests in server.ts add: `monitoringService.driver()` should see this in the logs: ![Screenshot 2024-05-15 at 5.48.05 PM.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/4JosFH65rhzwIvkZw2J6/2bf61e7f-0ec4-466f-87b7-ec7e1d84993e.png) ## Deploy Notes **New environment variables**: - `KEYCDN_API_KEY` : to get all the zones that we own in keycdn - `S3_BUCKET_NAME`: bucket name - [ ] HAVE NOT added env var to 1PW + SSM script (`fetch_ssm_parameters.sh`) **New scripts**: - `script` : script details **New dependencies**: - `dependency` : dependency details **New dev dependencies**: - `dependency` : dependency details --- .aws/deploy/support-task-definition.prod.json | 1 + .../support-task-definition.staging.json | 1 + .env.test | 2 + common/index.ts | 5 + package-lock.json | 181 +++++-------- package.json | 2 +- src/config/config.ts | 9 + src/errors/MonitoringError.ts | 11 + src/monitoring/index.ts | 244 ++++++++++++++++++ src/server.ts | 1 + src/services/identity/LaunchesService.ts | 6 + src/services/identity/ReposService.ts | 2 +- src/utils/papa-parse.ts | 19 ++ support/index.ts | 10 +- 14 files changed, 377 insertions(+), 117 deletions(-) create mode 100644 src/errors/MonitoringError.ts create mode 100644 src/monitoring/index.ts create mode 100644 src/utils/papa-parse.ts diff --git a/.aws/deploy/support-task-definition.prod.json b/.aws/deploy/support-task-definition.prod.json index 97ed65ab2..7db06bc19 100644 --- a/.aws/deploy/support-task-definition.prod.json +++ b/.aws/deploy/support-task-definition.prod.json @@ -98,6 +98,7 @@ "valueFrom": "PROD_ISOMERPAGES_REPO_PAGE_COUNT" }, { "name": "JWT_SECRET", "valueFrom": "PROD_JWT_SECRET" }, + { "name": "KEYCDN_API_KEY", "valueFrom": "PROD_KEYCDN_API_KEY" }, { "name": "MAX_NUM_OTP_ATTEMPTS", "valueFrom": "PROD_MAX_NUM_OTP_ATTEMPTS" diff --git a/.aws/deploy/support-task-definition.staging.json b/.aws/deploy/support-task-definition.staging.json index dc77dbed4..c77efa8fc 100644 --- a/.aws/deploy/support-task-definition.staging.json +++ b/.aws/deploy/support-task-definition.staging.json @@ -107,6 +107,7 @@ "valueFrom": "STAGING_ISOMERPAGES_REPO_PAGE_COUNT" }, { "name": "JWT_SECRET", "valueFrom": "STAGING_JWT_SECRET" }, + { "name": "KEYCDN_API_KEY", "valueFrom": "STAGING_KEYCDN_API_KEY" }, { "name": "MAX_NUM_OTP_ATTEMPTS", "valueFrom": "STAGING_MAX_NUM_OTP_ATTEMPTS" diff --git a/.env.test b/.env.test index 16075fe12..5d4a4ec13 100644 --- a/.env.test +++ b/.env.test @@ -85,3 +85,5 @@ export SGID_REDIRECT_URI="http://localhost:8081/v2/auth/sgid/auth-redirect" # GrowthBook export GROWTHBOOK_CLIENT_KEY="some random key" + +export KEYCDN_API_KEY="secret" \ No newline at end of file diff --git a/common/index.ts b/common/index.ts index 21f22bbc1..d0e7e976c 100644 --- a/common/index.ts +++ b/common/index.ts @@ -27,6 +27,7 @@ import { Reviewer, ReviewRequestView, } from "@database/models" +import MonitoringService from "@root/monitoring" import AuditLogsService from "@root/services/admin/AuditLogsService" import RepoManagementService from "@root/services/admin/RepoManagementService" import GitFileCommitService from "@root/services/db/GitFileCommitService" @@ -248,3 +249,7 @@ export const auditLogsService = new AuditLogsService({ sitesService, usersService, }) + +export const monitoringService = new MonitoringService({ + launchesService, +}) diff --git a/package-lock.json b/package-lock.json index d8dcd7f06..cce7c7ed4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@aws-sdk/lib-dynamodb": "^3.577.0", "@growthbook/growthbook": "^0.36.0", "@octokit/plugin-retry": "^6.0.0", - "@octokit/rest": "^18.12.0", + "@octokit/rest": "^20.1.1", "@opengovsg/formsg-sdk": "^0.11.0", "@opengovsg/sgid-client": "^2.0.0", "@slack/bolt": "^3.19.0", @@ -4519,7 +4519,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", - "peer": true, "engines": { "node": ">= 18" } @@ -4528,7 +4527,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.1.0.tgz", "integrity": "sha512-BDa2VAMLSh3otEiaMJ/3Y36GU4qf6GI+VivQ/P41NC6GHcdxpKlqV0ikSZ5gdQsmS3ojXeRx5vasgNTinF0Q4g==", - "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.0.0", @@ -4545,14 +4543,12 @@ "node_modules/@octokit/core/node_modules/@octokit/openapi-types": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-19.1.0.tgz", - "integrity": "sha512-6G+ywGClliGQwRsjvqVYpklIfa7oRPA0vyhPQG/1Feh+B+wU0vGH1JiJ5T25d3g1JZYBHzR2qefLi9x8Gt+cpw==", - "peer": true + "integrity": "sha512-6G+ywGClliGQwRsjvqVYpklIfa7oRPA0vyhPQG/1Feh+B+wU0vGH1JiJ5T25d3g1JZYBHzR2qefLi9x8Gt+cpw==" }, "node_modules/@octokit/core/node_modules/@octokit/types": { "version": "12.4.0", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.4.0.tgz", "integrity": "sha512-FLWs/AvZllw/AGVs+nJ+ELCDZZJk+kY0zMen118xhL2zD0s1etIUHm1odgjP7epxYU1ln7SZxEUWYop5bhsdgQ==", - "peer": true, "dependencies": { "@octokit/openapi-types": "^19.1.0" } @@ -4561,7 +4557,6 @@ "version": "9.0.4", "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.4.tgz", "integrity": "sha512-DWPLtr1Kz3tv8L0UvXTDP1fNwM0S+z6EJpRcvH66orY6Eld4XBMCSYsaWp4xIm61jTWxK68BrR7ibO+vSDnZqw==", - "peer": true, "dependencies": { "@octokit/types": "^12.0.0", "universal-user-agent": "^6.0.0" @@ -4573,14 +4568,12 @@ "node_modules/@octokit/endpoint/node_modules/@octokit/openapi-types": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-19.1.0.tgz", - "integrity": "sha512-6G+ywGClliGQwRsjvqVYpklIfa7oRPA0vyhPQG/1Feh+B+wU0vGH1JiJ5T25d3g1JZYBHzR2qefLi9x8Gt+cpw==", - "peer": true + "integrity": "sha512-6G+ywGClliGQwRsjvqVYpklIfa7oRPA0vyhPQG/1Feh+B+wU0vGH1JiJ5T25d3g1JZYBHzR2qefLi9x8Gt+cpw==" }, "node_modules/@octokit/endpoint/node_modules/@octokit/types": { "version": "12.4.0", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.4.0.tgz", "integrity": "sha512-FLWs/AvZllw/AGVs+nJ+ELCDZZJk+kY0zMen118xhL2zD0s1etIUHm1odgjP7epxYU1ln7SZxEUWYop5bhsdgQ==", - "peer": true, "dependencies": { "@octokit/openapi-types": "^19.1.0" } @@ -4589,7 +4582,6 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.0.2.tgz", "integrity": "sha512-OJ2iGMtj5Tg3s6RaXH22cJcxXRi7Y3EBqbHTBRq+PQAqfaS8f/236fUrWhfSn8P4jovyzqucxme7/vWSSZBX2Q==", - "peer": true, "dependencies": { "@octokit/request": "^8.0.1", "@octokit/types": "^12.0.0", @@ -4602,14 +4594,12 @@ "node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-19.1.0.tgz", - "integrity": "sha512-6G+ywGClliGQwRsjvqVYpklIfa7oRPA0vyhPQG/1Feh+B+wU0vGH1JiJ5T25d3g1JZYBHzR2qefLi9x8Gt+cpw==", - "peer": true + "integrity": "sha512-6G+ywGClliGQwRsjvqVYpklIfa7oRPA0vyhPQG/1Feh+B+wU0vGH1JiJ5T25d3g1JZYBHzR2qefLi9x8Gt+cpw==" }, "node_modules/@octokit/graphql/node_modules/@octokit/types": { "version": "12.4.0", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.4.0.tgz", "integrity": "sha512-FLWs/AvZllw/AGVs+nJ+ELCDZZJk+kY0zMen118xhL2zD0s1etIUHm1odgjP7epxYU1ln7SZxEUWYop5bhsdgQ==", - "peer": true, "dependencies": { "@octokit/openapi-types": "^19.1.0" } @@ -4617,37 +4607,72 @@ "node_modules/@octokit/openapi-types": { "version": "12.11.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", - "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==" + "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==", + "dev": true }, "node_modules/@octokit/plugin-paginate-rest": { - "version": "2.21.3", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.21.3.tgz", - "integrity": "sha512-aCZTEf0y2h3OLbrgKkrfFdjRL6eSOo8komneVQJnYecAxIej7Bafor2xhuDJOIFau4pk0i/P28/XgtbyPF0ZHw==", + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.3.1.tgz", + "integrity": "sha512-ryqobs26cLtM1kQxqeZui4v8FeznirUsksiA+RYemMPJ7Micju0WSkv50dBksTuZks9O5cg4wp+t8fZ/cLY56g==", "dependencies": { - "@octokit/types": "^6.40.0" + "@octokit/types": "^13.5.0" + }, + "engines": { + "node": ">= 18" }, "peerDependencies": { - "@octokit/core": ">=2" + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", + "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", + "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", + "dependencies": { + "@octokit/openapi-types": "^22.2.0" } }, "node_modules/@octokit/plugin-request-log": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", - "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-4.0.1.tgz", + "integrity": "sha512-GihNqNpGHorUrO7Qa9JbAl0dbLnqJVrV8OXe2Zm5/Y4wFkZQDfTreBzVmiRfJVfE4mClXdihHnbpyyO9FSX4HA==", + "engines": { + "node": ">= 18" + }, "peerDependencies": { - "@octokit/core": ">=3" + "@octokit/core": "5" } }, "node_modules/@octokit/plugin-rest-endpoint-methods": { - "version": "5.16.2", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.16.2.tgz", - "integrity": "sha512-8QFz29Fg5jDuTPXVtey05BLm7OB+M8fnvE64RNegzX7U+5NUXcOcnpTIK0YfSHBg8gYd0oxIq3IZTe9SfPZiRw==", + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.2.2.tgz", + "integrity": "sha512-EI7kXWidkt3Xlok5uN43suK99VWqc8OaIMktY9d9+RNKl69juoTyxmLoWPIZgJYzi41qj/9zU7G/ljnNOJ5AFA==", "dependencies": { - "@octokit/types": "^6.39.0", - "deprecation": "^2.3.1" + "@octokit/types": "^13.5.0" + }, + "engines": { + "node": ">= 18" }, "peerDependencies": { - "@octokit/core": ">=3" + "@octokit/core": "^5" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", + "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", + "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", + "dependencies": { + "@octokit/openapi-types": "^22.2.0" } }, "node_modules/@octokit/plugin-retry": { @@ -4683,7 +4708,6 @@ "version": "8.1.6", "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.1.6.tgz", "integrity": "sha512-YhPaGml3ncZC1NfXpP3WZ7iliL1ap6tLkAp6MvbK2fTTPytzVUyUesBBogcdMm86uRYO5rHaM1xIWxigWZ17MQ==", - "peer": true, "dependencies": { "@octokit/endpoint": "^9.0.0", "@octokit/request-error": "^5.0.0", @@ -4723,98 +4747,35 @@ "node_modules/@octokit/request/node_modules/@octokit/openapi-types": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-19.1.0.tgz", - "integrity": "sha512-6G+ywGClliGQwRsjvqVYpklIfa7oRPA0vyhPQG/1Feh+B+wU0vGH1JiJ5T25d3g1JZYBHzR2qefLi9x8Gt+cpw==", - "peer": true + "integrity": "sha512-6G+ywGClliGQwRsjvqVYpklIfa7oRPA0vyhPQG/1Feh+B+wU0vGH1JiJ5T25d3g1JZYBHzR2qefLi9x8Gt+cpw==" }, "node_modules/@octokit/request/node_modules/@octokit/types": { "version": "12.4.0", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.4.0.tgz", "integrity": "sha512-FLWs/AvZllw/AGVs+nJ+ELCDZZJk+kY0zMen118xhL2zD0s1etIUHm1odgjP7epxYU1ln7SZxEUWYop5bhsdgQ==", - "peer": true, "dependencies": { "@octokit/openapi-types": "^19.1.0" } }, "node_modules/@octokit/rest": { - "version": "18.12.0", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-18.12.0.tgz", - "integrity": "sha512-gDPiOHlyGavxr72y0guQEhLsemgVjwRePayJ+FcKc2SJqKUbxbkvf5kAZEWA/MKvsfYlQAMVzNJE3ezQcxMJ2Q==", - "dependencies": { - "@octokit/core": "^3.5.1", - "@octokit/plugin-paginate-rest": "^2.16.8", - "@octokit/plugin-request-log": "^1.0.4", - "@octokit/plugin-rest-endpoint-methods": "^5.12.0" - } - }, - "node_modules/@octokit/rest/node_modules/@octokit/auth-token": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz", - "integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==", - "dependencies": { - "@octokit/types": "^6.0.3" - } - }, - "node_modules/@octokit/rest/node_modules/@octokit/core": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.6.0.tgz", - "integrity": "sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==", - "dependencies": { - "@octokit/auth-token": "^2.4.4", - "@octokit/graphql": "^4.5.8", - "@octokit/request": "^5.6.3", - "@octokit/request-error": "^2.0.5", - "@octokit/types": "^6.0.3", - "before-after-hook": "^2.2.0", - "universal-user-agent": "^6.0.0" - } - }, - "node_modules/@octokit/rest/node_modules/@octokit/endpoint": { - "version": "6.0.12", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", - "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==", - "dependencies": { - "@octokit/types": "^6.0.3", - "is-plain-object": "^5.0.0", - "universal-user-agent": "^6.0.0" - } - }, - "node_modules/@octokit/rest/node_modules/@octokit/graphql": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz", - "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==", - "dependencies": { - "@octokit/request": "^5.6.0", - "@octokit/types": "^6.0.3", - "universal-user-agent": "^6.0.0" - } - }, - "node_modules/@octokit/rest/node_modules/@octokit/request": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz", - "integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==", - "dependencies": { - "@octokit/endpoint": "^6.0.1", - "@octokit/request-error": "^2.1.0", - "@octokit/types": "^6.16.1", - "is-plain-object": "^5.0.0", - "node-fetch": "^2.6.7", - "universal-user-agent": "^6.0.0" - } - }, - "node_modules/@octokit/rest/node_modules/@octokit/request-error": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", - "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", + "version": "20.1.1", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-20.1.1.tgz", + "integrity": "sha512-MB4AYDsM5jhIHro/dq4ix1iWTLGToIGk6cWF5L6vanFaMble5jTX/UBQyiv05HsWnwUtY8JrfHy2LWfKwihqMw==", "dependencies": { - "@octokit/types": "^6.0.3", - "deprecation": "^2.0.0", - "once": "^1.4.0" + "@octokit/core": "^5.0.2", + "@octokit/plugin-paginate-rest": "11.3.1", + "@octokit/plugin-request-log": "^4.0.0", + "@octokit/plugin-rest-endpoint-methods": "13.2.2" + }, + "engines": { + "node": ">= 18" } }, "node_modules/@octokit/types": { "version": "6.41.0", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", + "dev": true, "dependencies": { "@octokit/openapi-types": "^12.11.0" } @@ -12508,14 +12469,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", diff --git a/package.json b/package.json index c655eae21..e90cc5838 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "@aws-sdk/lib-dynamodb": "^3.577.0", "@growthbook/growthbook": "^0.36.0", "@octokit/plugin-retry": "^6.0.0", - "@octokit/rest": "^18.12.0", + "@octokit/rest": "^20.1.1", "@opengovsg/formsg-sdk": "^0.11.0", "@opengovsg/sgid-client": "^2.0.0", "@slack/bolt": "^3.19.0", diff --git a/src/config/config.ts b/src/config/config.ts index 5cf91ce63..4b4d60888 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -195,6 +195,7 @@ const config = convict({ }, }, }, + github: { orgName: { doc: "GitHub organization that owns all site repositories", @@ -472,6 +473,14 @@ const config = convict({ default: "", }, }, + keyCdn: { + apiKey: { + doc: "KeyCDN API key", + env: "KEYCDN_API_KEY", + format: "required-string", + default: "", + }, + }, }) // Perform validation diff --git a/src/errors/MonitoringError.ts b/src/errors/MonitoringError.ts new file mode 100644 index 000000000..431aa5479 --- /dev/null +++ b/src/errors/MonitoringError.ts @@ -0,0 +1,11 @@ +import { BaseIsomerError } from "./BaseError" + +export default class MonitoringError extends BaseIsomerError { + constructor(message: string) { + super({ + status: 500, + code: "MonitoringError", + message, + }) + } +} diff --git a/src/monitoring/index.ts b/src/monitoring/index.ts new file mode 100644 index 000000000..de6e43ec0 --- /dev/null +++ b/src/monitoring/index.ts @@ -0,0 +1,244 @@ +import dns from "dns/promises" + +import { retry } from "@octokit/plugin-retry" +import { Octokit } from "@octokit/rest" +import autoBind from "auto-bind" +import axios from "axios" +import _ from "lodash" +import { errAsync, okAsync, ResultAsync } from "neverthrow" + +import parentLogger from "@logger/logger" + +import config from "@root/config/config" +import MonitoringError from "@root/errors/MonitoringError" +import LaunchesService from "@root/services/identity/LaunchesService" +import promisifyPapaParse from "@root/utils/papa-parse" + +interface MonitoringServiceProps { + launchesService: LaunchesService +} + +const IsomerHostedDomainType = { + REDIRECTION: "redirection", + INDIRECTION: "indirection", + KEYCDN: "keycdn", + AMPLIFY: "amplify", +} as const + +interface IsomerHostedDomain { + domain: string + type: typeof IsomerHostedDomainType[keyof typeof IsomerHostedDomainType] +} + +type KeyCdnZoneAlias = { + name: string +} + +interface RedirectionDomain { + source: string + target: string +} + +interface ReportCard { + domain: string + type: typeof IsomerHostedDomainType[keyof typeof IsomerHostedDomainType] + aRecord: string[] + quadArecord: string[] + cNameRecord: string[] + caaRecord: string[] +} + +function isKeyCdnZoneAlias(object: unknown): object is KeyCdnZoneAlias { + return "name" in (object as KeyCdnZoneAlias) +} + +function isKeyCdnResponse(object: unknown): object is KeyCdnZoneAlias[] { + if (!object) return false + if (Array.isArray(object)) return object.every(isKeyCdnZoneAlias) + return false +} + +export default class MonitoringService { + private readonly launchesService: MonitoringServiceProps["launchesService"] + + private readonly monitoringServiceLogger = parentLogger.child({ + module: "monitoringService", + }) + + constructor({ launchesService }: MonitoringServiceProps) { + autoBind(this) + this.launchesService = launchesService + } + + getKeyCdnDomains() { + const keyCdnApiKey = config.get("keyCdn.apiKey") + + return ResultAsync.fromPromise( + axios.get(`https://api.keycdn.com/zonealiases.json`, { + headers: { + Authorization: `Basic ${btoa(`${keyCdnApiKey}:`)}`, + }, + }), + (error) => + new MonitoringError(`Failed to fetch zones from KeyCDN: ${error}`) + ) + .map((response) => response.data.data.zonealiases) + .andThen((data) => { + if (!isKeyCdnResponse(data)) { + return errAsync( + new MonitoringError("Failed to parse response from KeyCDN") + ) + } + + const domains = data + .map((zone) => zone.name) + .map((domain) => ({ + domain, + type: IsomerHostedDomainType.KEYCDN, + })) + return okAsync(domains) + }) + } + + getAmplifyDeployments() { + return this.launchesService.getAllDomains().map((domains) => + domains.map((domain) => ({ + domain, + type: IsomerHostedDomainType.AMPLIFY, + })) + ) + } + + /** + * While most of our redirections are in our DB, we do have ad-hoc redirections. + * @returns List of redirection domains that are listed in the isomer-redirection repository + */ + getRedirectionDomains() { + const SYSTEM_GITHUB_TOKEN = config.get("github.systemToken") + const OctokitRetry = Octokit.plugin(retry) + const octokitWithRetry: Octokit = new OctokitRetry({ + auth: SYSTEM_GITHUB_TOKEN, + request: { retries: 5 }, + }) + + return ResultAsync.fromPromise( + octokitWithRetry.request( + "GET /repos/opengovsg/isomer-redirection/contents/src/certbot-websites.csv" + ), + (error) => + new MonitoringError(`Failed to fetch redirection domains: ${error}`) + ) + .andThen((response) => { + const content = Buffer.from(response.data.content, "base64").toString( + "utf-8" + ) + + return ResultAsync.fromPromise( + promisifyPapaParse(content), + (error) => new MonitoringError(`Failed to parse csv: ${error}`) + ) + }) + .map((redirectionDomains) => + redirectionDomains + .map((domain) => domain.source) + .map((domain) => ({ + domain, + type: IsomerHostedDomainType.REDIRECTION, + })) + ) + } + + /** + * This is in charge of fetching all the domains that are are under Isomer, inclusive + * of any subdomains and redirects. + */ + getAllDomains() { + this.monitoringServiceLogger.info("Fetching all domains") + return ResultAsync.combine([ + this.getAmplifyDeployments().mapErr( + (error) => new MonitoringError(error.message) + ), + this.getRedirectionDomains(), + this.getKeyCdnDomains(), + ]).andThen(([amplifyDeployments, redirectionDomains, keyCdnDomains]) => { + this.monitoringServiceLogger.info("Fetched all domains") + return okAsync( + _.sortBy( + [...amplifyDeployments, ...redirectionDomains, ...keyCdnDomains], + (val) => (val.domain.startsWith("www.") ? val.domain.slice(4) : val) + ) + ) + }) + } + + // todo: once /siteup logic is merged into dev, we can add that as to alert isomer team + generateReportCard(domains: IsomerHostedDomain[]) { + const reportCard: ReportCard[] = [] + + const domainResolvers = domains.map(({ domain, type }) => { + const aRecord = ResultAsync.fromPromise( + dns.resolve(domain, "A"), + (e) => e + ).orElse(() => okAsync([])) + const quadArecord = ResultAsync.fromPromise( + dns.resolve(domain, "AAAA"), + (e) => e + ).orElse(() => okAsync([])) + + const cNameRecord = ResultAsync.fromPromise( + dns.resolve(domain, "CNAME"), + (e) => e + ).orElse(() => okAsync([])) + + const caaRecord = ResultAsync.fromPromise( + dns.resolve(domain, "CAA"), + (e) => e + ) + .orElse(() => okAsync([])) + .map((records) => records.map((record) => record.toString())) + + return ResultAsync.combineWithAllErrors([ + aRecord, + quadArecord, + cNameRecord, + caaRecord, + ]) + .andThen((resolvedDns) => + okAsync({ + domain, + type, + aRecord: resolvedDns[0], + quadArecord: resolvedDns[1], + cNameRecord: resolvedDns[2], + caaRecord: resolvedDns[3], + }) + ) + .map((value) => + reportCard.push({ + ...value, + }) + ) + .andThen(() => okAsync(reportCard)) + }) + + return ResultAsync.combineWithAllErrors(domainResolvers).map( + () => reportCard + ) + } + + driver() { + this.monitoringServiceLogger.info("Monitoring service started") + return this.getAllDomains() + .andThen(this.generateReportCard) + .andThen((reportCard) => { + this.monitoringServiceLogger.info({ + message: "Report card generated", + meta: { + reportCard, + date: new Date(), + }, + }) + return okAsync(reportCard) + }) + } +} diff --git a/src/server.ts b/src/server.ts index 204637a20..4c1614333 100644 --- a/src/server.ts +++ b/src/server.ts @@ -80,6 +80,7 @@ import { mailer } from "@services/utilServices/MailClient" import { apiLogger } from "./middleware/apiLogger" import { NotificationOnEditHandler } from "./middleware/notificationOnEditHandler" +import MonitoringService from "./monitoring" import getAuthenticatedSubrouter from "./routes/v2/authenticated" import { ReviewsRouter } from "./routes/v2/authenticated/review" import getAuthenticatedSitesSubrouter from "./routes/v2/authenticatedSites" diff --git a/src/services/identity/LaunchesService.ts b/src/services/identity/LaunchesService.ts index 1f6c30715..4e713d648 100644 --- a/src/services/identity/LaunchesService.ts +++ b/src/services/identity/LaunchesService.ts @@ -353,6 +353,12 @@ export class LaunchesService { new SiteLaunchError(`Failed to update site status for ${siteName}`) ) }) + + getAllDomains = () => + ResultAsync.fromPromise( + this.launchesRepository.findAll(), + () => new SiteLaunchError("Failed to fetch launches") + ).map((launch) => launch.map((l) => l.primaryDomainSource)) } export default LaunchesService diff --git a/src/services/identity/ReposService.ts b/src/services/identity/ReposService.ts index 7175b5648..ac07c2213 100644 --- a/src/services/identity/ReposService.ts +++ b/src/services/identity/ReposService.ts @@ -25,7 +25,7 @@ import { doesDirectoryExist } from "@root/utils/fs-utils" const SYSTEM_GITHUB_TOKEN = config.get("github.systemToken") const octokit = new Octokit({ auth: SYSTEM_GITHUB_TOKEN }) -const OctokitRetry = Octokit.plugin(retry as any) +const OctokitRetry = Octokit.plugin(retry) const octokitWithRetry = new OctokitRetry({ auth: SYSTEM_GITHUB_TOKEN, request: { retries: 5 }, diff --git a/src/utils/papa-parse.ts b/src/utils/papa-parse.ts new file mode 100644 index 000000000..e40723e20 --- /dev/null +++ b/src/utils/papa-parse.ts @@ -0,0 +1,19 @@ +import Papa from "papaparse" + +export default function promisifyPapaParse(content: string) { + return new Promise((resolve, reject) => { + Papa.parse(content, { + header: true, + complete(results) { + // validate the csv + if (!results.data) { + reject(new Error("Failed to parse csv")) + } + resolve(results.data as T) + }, + error(error: unknown) { + reject(error) + }, + }) + }) +} diff --git a/support/index.ts b/support/index.ts index ee0dffcd9..3c8e454d5 100644 --- a/support/index.ts +++ b/support/index.ts @@ -2,10 +2,16 @@ import "module-alias/register" import express from "express" -import { infraService, sequelize } from "@common/index" +import { + infraService, + launchesService, + monitoringService, + sequelize, +} from "@common/index" import { useSharedMiddleware } from "@common/middleware" import { config } from "@root/config/config" import logger from "@root/logger/logger" +import MonitoringService from "@root/monitoring" import { ROUTE_VERSION } from "./constants" import { v2Router } from "./routes" @@ -18,6 +24,8 @@ const app = express() // poller site launch updates infraService.pollMessages() +monitoringService.driver() + const ROUTE_PREFIX_ISOBOT = `/${ROUTE_VERSION}/isobot` app.use(ROUTE_PREFIX_ISOBOT, isobotRouter)