diff --git a/Dockerfile b/Dockerfile index da9ba693..47277b0b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=linux/amd64 node:20-alpine +FROM --platform=linux/amd64 public.ecr.aws/docker/library/node:20-alpine RUN apk add libcrypto3=3.3.0-r2 libssl3=3.3.0-r2 RUN apk upgrade RUN mkdir -p /opt/obsrv-api-service diff --git a/api-service/Dockerfile b/api-service/Dockerfile index 47f228fd..c4a8375b 100644 --- a/api-service/Dockerfile +++ b/api-service/Dockerfile @@ -1,9 +1,50 @@ -FROM --platform=linux/amd64 node:24.3.0-alpine -RUN mkdir -p /opt/api-service -COPY ./api-service ./opt/api-service +# Build stage +FROM --platform=linux/amd64 public.ecr.aws/docker/library/node:24.13.1-slim AS builder +RUN npm install -g npm@11.10.1 \ + && npm pack tar@7.5.11 \ + && tar -xzf tar-7.5.11.tgz -C /usr/local/lib/node_modules/npm/node_modules/tar --strip-components=1 \ + && rm tar-7.5.11.tgz \ + && npm install -g minimatch@10.2.2 \ + && npm cache clean --force \ + && rm -rf /usr/local/lib/node_modules/npm/node_modules/minimatch \ + && cp -r /usr/local/lib/node_modules/minimatch /usr/local/lib/node_modules/npm/node_modules/ +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates openssl \ + && apt-get upgrade -y \ + && rm -rf /var/lib/apt/lists/* WORKDIR /opt/api-service -RUN rm -rf postman-collection -RUN npm install -COPY . . +# Install all dependencies for building +COPY api-service/package.json api-service/package-lock.json ./ +RUN npm ci --ignore-scripts +# Copy source code and build +COPY api-service/tsconfig.json ./ +COPY api-service/src/ ./src/ +COPY api-service/@types/ ./@types/ +RUN npm run build +# Remove declaration files to avoid module resolution conflicts +RUN find dist -name "*.d.ts" -delete + +# Production stage +FROM --platform=linux/amd64 public.ecr.aws/docker/library/node:24.13.1-slim AS production +RUN npm install -g npm@11.10.1 \ + && npm pack tar@7.5.11 \ + && tar -xzf tar-7.5.11.tgz -C /usr/local/lib/node_modules/npm/node_modules/tar --strip-components=1 \ + && rm tar-7.5.11.tgz \ + && npm install -g minimatch@10.2.2 \ + && npm cache clean --force \ + && rm -rf /usr/local/lib/node_modules/npm/node_modules/minimatch \ + && cp -r /usr/local/lib/node_modules/minimatch /usr/local/lib/node_modules/npm/node_modules/ +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates openssl \ + && apt-get upgrade -y \ + && rm -rf /var/lib/apt/lists/* +WORKDIR /opt/api-service +# Copy package.json and install only production dependencies +COPY api-service/package.json api-service/package-lock.json ./ +RUN npm ci --omit=dev --ignore-scripts +# Copy built application from build stage +COPY --from=builder /opt/api-service/dist ./ +# Verify the structure and ensure no declaration files +RUN ls -la && echo "=== Checking for .d.ts files ===" && find . -name "*.d.ts" | head -5 || echo "No .d.ts files found" EXPOSE 3000 -CMD ["npm", "run", "start"] \ No newline at end of file +CMD ["node", "app.js"] \ No newline at end of file diff --git a/api-service/eslint.config.js b/api-service/eslint.config.js new file mode 100644 index 00000000..d36492af --- /dev/null +++ b/api-service/eslint.config.js @@ -0,0 +1,45 @@ +import js from '@eslint/js'; +import typescript from '@typescript-eslint/eslint-plugin'; +import typescriptParser from '@typescript-eslint/parser'; + +export default [ + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parser: typescriptParser, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module' + } + }, + plugins: { + '@typescript-eslint': typescript + }, + rules: { + ...js.configs.recommended.rules, + ...typescript.configs.recommended.rules, + '@typescript-eslint/no-unused-vars': ['error', { 'argsIgnorePattern': '^_' }], + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/quotes': [ + 'error', + 'double', + { + 'allowTemplateLiterals': true + } + ] + } + }, + { + files: ['**/*.js'], + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module' + }, + rules: { + ...js.configs.recommended.rules + } + }, + { + ignores: ['dist/', 'node_modules/', '**/*.d.ts'] + } +]; \ No newline at end of file diff --git a/api-service/package.json b/api-service/package.json index e80d65f7..386cde39 100644 --- a/api-service/package.json +++ b/api-service/package.json @@ -2,7 +2,8 @@ "name": "obsrv-api-service", "version": "1.0.0", "description": "API service for the obsrv api's", - "main": "dist/app.js", + "type": "commonjs", + "main": "app.js", "scripts": { "start": "ts-node ./src/app.ts", "test": "source .env.test && nyc mocha ./src/tests/**/*.spec.ts --exit", @@ -34,10 +35,10 @@ "@opentelemetry/sdk-trace-node": "^1.28.0", "@opentelemetry/semantic-conventions": "^1.28.0", "@project-sunbird/logger": "^0.0.9", - "ajv": "^8.11.2", + "ajv": "^8.18.0", "ajv-formats": "^2.1.1", "aws-sdk": "^2.1348.0", - "axios": "^1.6.0", + "axios": "^1.13.5", "body-parser": "^1.20.2", "busboy": "^1.6.0", "compression": "^1.7.4", @@ -61,7 +62,7 @@ "pg-hstore": "^2.3.4", "prom-client": "^14.2.0", "redis": "^4.6.15", - "sequelize": "^6.37.1", + "sequelize": "^6.37.8", "slug": "^9.0.0", "trino-client": "^0.2.2", "uuid": "3.1.0", @@ -70,7 +71,20 @@ }, "overrides": { "semver": "^7.5.3", - "@babel/traverse": "7.23.2" + "@babel/traverse": "7.23.2", + "diff": "^8.0.3", + "lodash": "^4.17.21", + "minimatch": "^10.2.3", + "axios": "^1.13.5", + "tar": "^7.5.11", + "formidable": "^3.5.3", + "eslint": "^9.26.0", + "fast-xml-parser": "^5.5.6", + "qs": "^6.15.0", + "dottie": "^2.0.7", + "underscore": "^1.13.8", + "flatted": "^3.4.0", + "serialize-javascript": "^7.0.3" }, "devDependencies": { "@types/busboy": "^1.5.4", @@ -92,15 +106,16 @@ "@types/sinon": "^17.0.3", "@types/slug": "^5.0.8", "@types/uuid": "^9.0.1", - "@typescript-eslint/eslint-plugin": "^7.1.1", - "@typescript-eslint/parser": "^7.1.1", + "@eslint/js": "^9.26.0", + "@typescript-eslint/eslint-plugin": "^8.19.0", + "@typescript-eslint/parser": "^8.19.0", "chai": "^4.3.6", "chai-as-promised": "^7.1.1", "chai-http": "^4.3.0", "chai-spies": "^1.0.0", "diff-json": "^2.0.0", - "eslint": "^8.57.0", - "mocha": "^10.1.0", + "eslint": "^9.26.0", + "mocha": "^11.3.0", "nock": "^13.2.9", "nodemon": "^3.0.1", "nyc": "^15.1.0", diff --git a/api-service/postman-collection/Obsrv v2 apis.postman_collection.json b/api-service/postman-collection/Obsrv v2 apis.postman_collection.json index cd8d4689..9ec695a6 100644 --- a/api-service/postman-collection/Obsrv v2 apis.postman_collection.json +++ b/api-service/postman-collection/Obsrv v2 apis.postman_collection.json @@ -7483,7 +7483,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"manager\": \"grafana\",\n \"name\": \"functional-metrics-slack\",\n \"type\": \"slack\",\n \"config\": {\n \"channel\": \"grafana-alerts\",\n \"webhookUrl\": \"https://hooks.slack.com/services/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\"\n }\n}", + "raw": "{\n \"manager\": \"grafana\",\n \"name\": \"functional-metrics-slack\",\n \"type\": \"slack\",\n \"config\": {\n \"channel\": \"grafana-alerts\",\n \"webhookUrl\": \"https://hooks.slack.com/services/****/****/*****\"\n }\n}", "options": { "raw": { "language": "json" diff --git a/api-service/src/configs/ConnectionsConfig.ts b/api-service/src/configs/ConnectionsConfig.ts index b21512c9..89679262 100644 --- a/api-service/src/configs/ConnectionsConfig.ts +++ b/api-service/src/configs/ConnectionsConfig.ts @@ -2,13 +2,12 @@ const env = process.env; export const connectionConfig = { - postgres: { - host: env.postgres_host || "localhost", - port: env.postgres_port || 5432, - database: env.postgres_database || "obsrv", - username: env.postgres_username || "postgres", - password: env.postgres_password || "postgres", - }, + postgres: Object.freeze({ + host: env['postgres_host'] || "localhost", + port: env['postgres_port'] || 5432, + database: env['postgres_database'] || "obsrv", + credentials: `${env['postgres_username'] || "postgres"}::${env['postgres_password'] || "postgres"}` + }), kafka: { "config": { "brokers": [`${env.kafka_host || "localhost"}:${env.kafka_port || 9092}`], diff --git a/api-service/src/connections/databaseConnection.ts b/api-service/src/connections/databaseConnection.ts index 4adc7be0..8e1e0db8 100644 --- a/api-service/src/connections/databaseConnection.ts +++ b/api-service/src/connections/databaseConnection.ts @@ -1,16 +1,27 @@ import { Sequelize } from "sequelize"; import { connectionConfig } from "../configs/ConnectionsConfig" -const { database, host, password, port, username } = connectionConfig.postgres +const { database, host, port } = connectionConfig.postgres; +const credentials = connectionConfig.postgres.credentials.split("::"); -export const sequelize = new Sequelize({ - database, password, username: username, dialect: "postgres", host, port: +port, pool: { - max: 2, - min: 1, - acquire: 30000, - idle: 10000 - } -}) +const decodedCredentials = Buffer.from(credentials[1], 'base64').toString('utf-8'); + +export const sequelize = new Sequelize( + database, + credentials[0], + decodedCredentials, + { + host, + port: +port, + dialect: "postgres", + pool: { + max: 2, + min: 1, + acquire: 30000, + idle: 10000 + } + } +) export const health = async () => { return sequelize.query("select 1") diff --git a/api-service/src/controllers/Alerts/Alerts.ts b/api-service/src/controllers/Alerts/Alerts.ts index 76bd9c3a..3d4768bc 100644 --- a/api-service/src/controllers/Alerts/Alerts.ts +++ b/api-service/src/controllers/Alerts/Alerts.ts @@ -12,7 +12,7 @@ const telemetryObject = { type: "alert", ver: "1.0.0" }; const createAlertHandler = async (req: Request, res: Response, next: NextFunction) => { try { - const alertPayload = getAlertPayload(req.body); + const alertPayload = getAlertPayload(_.get(req, "body")); const userID = (req as any)?.userID; _.set(alertPayload, "created_by", userID); _.set(alertPayload, "updated_by", userID); @@ -30,7 +30,7 @@ const createAlertHandler = async (req: Request, res: Response, next: NextFunctio const publishAlertHandler = async (req: Request, res: Response, next: NextFunction) => { try { - const { alertId } = req.params; + const alertId = _.get(req, 'params.alertId'); const ruleModel: Record | null = await getAlertRule(alertId); if (!ruleModel) return next({ message: httpStatus[httpStatus.NOT_FOUND], statusCode: httpStatus.NOT_FOUND }); const rulePayload = ruleModel.toJSON(); @@ -57,7 +57,7 @@ const transformAlerts = async (alertModel: any) => { const searchAlertHandler = async (req: Request, res: Response, next: NextFunction) => { try { - const { limit, filters, offset, options = {} } = req.body?.request || {}; + const { limit, filters, offset, options = {} } = _.get(req, 'body.request', {}); const alerts = await Alert.findAll({ limit: limit, offset: offset, ...(filters && { where: filters }), ...options }); const alertRulesWithStatus = await Promise.all(_.map(alerts, transformAlerts)); ResponseHandler.successResponse(req, res, { status: httpStatus.OK, data: { alerts: alertRulesWithStatus, count: alerts.length } }); @@ -105,8 +105,8 @@ const deleteAlertHandler = async (req: Request, res: Response, next: NextFunctio const updateAlertHandler = async (req: Request, res: Response, next: NextFunction) => { try { - const { alertId } = req.params; - const isEmpty = _.isEmpty(req.body); + const alertId = _.get(req, 'params.alertId'); + const isEmpty = _.isEmpty(_.get(req, "body")); if (isEmpty) throw new Error("Failed to update record"); const ruleModel = await getAlertRule(alertId); if (!ruleModel) { return next({ message: httpStatus[httpStatus.NOT_FOUND], statusCode: httpStatus.NOT_FOUND }) } @@ -117,7 +117,7 @@ const updateAlertHandler = async (req: Request, res: Response, next: NextFunctio await deleteAlertRule(rulePayload, false); await retireAlertSilence(alertId); } - const updatedPayload = getAlertPayload({ ...req.body, manager: rulePayload?.manager }); + const updatedPayload = getAlertPayload({ ..._.get(req, "body"), manager: rulePayload?.manager }); await Alert.update({ ...updatedPayload, status: "draft", updated_by: userID }, { where: { id: alertId } }); updateTelemetryAuditEvent({ request: req, currentRecord: rulePayload, object: { id: alertId, ...telemetryObject } }); ResponseHandler.successResponse(req, res, { status: httpStatus.OK, data: { id: alertId } }); @@ -132,7 +132,7 @@ const updateAlertHandler = async (req: Request, res: Response, next: NextFunctio const deleteSystemAlertsHandler = async (req: Request, res: Response, next: NextFunction) => { try { - const body = req.body; + const body = _.get(req, 'body', {}); const { filters } = body; if (!filters) throw new Error("Failed to update record"); await deleteSystemRules({ filters, manager: "grafana" }); diff --git a/api-service/src/controllers/Alerts/Metric.ts b/api-service/src/controllers/Alerts/Metric.ts index a4189cc0..74bcce35 100644 --- a/api-service/src/controllers/Alerts/Metric.ts +++ b/api-service/src/controllers/Alerts/Metric.ts @@ -10,8 +10,8 @@ const telemetryObject = { type: "metric", ver: "1.0.0" }; const createMetricHandler = async (req: Request, res: Response, next: NextFunction) => { try { - const { component } = req.body; - const metricsBody = await Metrics.create({ ...(req.body), component: component }); + const component = _.get(req, 'body.component'); + const metricsBody = await Metrics.create({ ...(_.get(req, 'body')), component: component }); updateTelemetryAuditEvent({ request: req, object: { id: metricsBody?.dataValues?.id, ...telemetryObject } }); ResponseHandler.successResponse(req, res, { status: httpStatus.OK, data: { id: metricsBody.dataValues.id } }); } catch (error: any) { @@ -36,8 +36,8 @@ const listMetricsHandler = async (req: Request, res: Response, next: NextFunctio const updateMetricHandler = async (req: Request, res: Response, next: NextFunction) => { try { - const { id } = req.params; - const toUpdatePayload = req.body; + const id = _.get(req, 'params.id'); + const toUpdatePayload = _.get(req, 'body'); const { component } = toUpdatePayload; const isEmpty = _.isEmpty(toUpdatePayload); if (isEmpty) throw new Error("Failed to update record"); @@ -59,7 +59,7 @@ const updateMetricHandler = async (req: Request, res: Response, next: NextFuncti const deleteMetricHandler = async (req: Request, res: Response, next: NextFunction) => { try { - const { id } = req.params; + const id = _.get(req, 'params.id'); const record = await Metrics.findOne({ where: { id } }); if (!record) throw new Error(httpStatus[httpStatus.NOT_FOUND]); await record.destroy(); @@ -72,7 +72,7 @@ const deleteMetricHandler = async (req: Request, res: Response, next: NextFuncti const deleteMultipleMetricHandler = async (req: Request, res: Response, next: NextFunction) => { try { - const { filters } = req.body; + const filters = _.get(req, 'body.filters'); if (!filters) throw new Error("Failed to update record"); await Metrics.destroy({ where: filters }); ResponseHandler.successResponse(req, res, { status: httpStatus.OK, data: {} }); diff --git a/api-service/src/controllers/Alerts/Silence.ts b/api-service/src/controllers/Alerts/Silence.ts index f424465a..35528f14 100644 --- a/api-service/src/controllers/Alerts/Silence.ts +++ b/api-service/src/controllers/Alerts/Silence.ts @@ -11,7 +11,7 @@ const telemetryObject = { type: "alert-silence", ver: "1.0.0" }; const createHandler = async (request: Request, response: Response, next: NextFunction) => { try { - const payload = request.body; + const payload = _.get(request,'body'); const { startDate, endDate, alertId } = payload; const existingSilence = await Silence.findOne({ where: { alert_id: alertId } }); if (existingSilence) existingSilence.destroy(); @@ -59,7 +59,7 @@ const listHandler = async (request: Request, response: Response, next: NextFunct const fetchHandler = async (request: Request, response: Response, next: NextFunction) => { try { - const id = request.params.id; + const id = _.get(request,'params.id'); const silenceModel = await Silence.findOne({ where: { id } }); const transformedSilence = await transformSilences(silenceModel); if (!silenceModel) return next({ message: httpStatus[httpStatus.NOT_FOUND], statusCode: httpStatus.NOT_FOUND }); @@ -72,8 +72,8 @@ const fetchHandler = async (request: Request, response: Response, next: NextFunc const updateHandler = async (request: Request, response: Response, next: NextFunction) => { try { - const id = request.params.id; - const payload = request.body; + const id = _.get(request,'params.id'); + const payload = _.get(request, 'body'); const silenceModel = await Silence.findOne({ where: { id } }); if (!silenceModel) return next({ message: httpStatus[httpStatus.NOT_FOUND], statusCode: httpStatus.NOT_FOUND }); const silenceObject = silenceModel?.toJSON(); @@ -98,7 +98,7 @@ const updateHandler = async (request: Request, response: Response, next: NextFun const deleteHandler = async (request: Request, response: Response, next: NextFunction) => { try { - const id = request.params.id; + const id = _.get(request,'params.id'); const silenceModel = await Silence.findOne({ where: { id } }); if (!silenceModel) return next({ message: httpStatus[httpStatus.NOT_FOUND], statusCode: httpStatus.NOT_FOUND }); const silenceObject = silenceModel?.toJSON(); diff --git a/api-service/src/controllers/DataMetrics/DataMetricsController.ts b/api-service/src/controllers/DataMetrics/DataMetricsController.ts index 98314878..367f7c5f 100644 --- a/api-service/src/controllers/DataMetrics/DataMetricsController.ts +++ b/api-service/src/controllers/DataMetrics/DataMetricsController.ts @@ -26,6 +26,7 @@ const dataMetrics = async (req: Request, res: Response) => { const { url, method, headers = {}, body = {}, params = {}, ...rest } = query; const apiResponse = await axios.request({ url, method, headers, params, data: body, ...rest }) const data = _.get(apiResponse, "data"); + res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); return res.json(data); } else { diff --git a/api-service/src/controllers/DatasetRead/DatasetRead.ts b/api-service/src/controllers/DatasetRead/DatasetRead.ts index d1c91209..462bd07d 100644 --- a/api-service/src/controllers/DatasetRead/DatasetRead.ts +++ b/api-service/src/controllers/DatasetRead/DatasetRead.ts @@ -19,8 +19,9 @@ export const defaultFields = ["dataset_id", "name", "type", "status", "tags", "v const validateRequest = (req: Request) => { - const { dataset_id } = req.params; - const { fields, mode } = req.query; + const dataset_id = _.get(req, 'params.dataset_id'); + const fields = _.get(req, 'query.fields'); + const mode = _.get(req, 'query.mode'); const fieldValues = fields ? _.split(fields as string, ",") : []; const invalidFields = mode === "edit" ? _.difference(fieldValues, Object.keys(DatasetDraft.getAttributes())) : _.difference(fieldValues, Object.keys(Dataset.getAttributes())); if (!_.isEmpty(invalidFields)) { @@ -32,8 +33,9 @@ const validateRequest = (req: Request) => { const datasetRead = async (req: Request, res: Response) => { validateRequest(req); - const { dataset_id } = req.params; - const { fields, mode } = req.query; + const dataset_id = _.get(req, 'params.dataset_id'); + const fields = _.get(req, 'query.fields'); + const mode = _.get(req, 'query.mode'); const userID = (req as any)?.userID; const attributes = !fields ? defaultFields : _.split(fields, ","); const dataset = (mode == "edit") ? await readDraftDataset(dataset_id, attributes, userID) : await readDataset(dataset_id, attributes) diff --git a/api-service/src/controllers/NotificationChannel/Notification.ts b/api-service/src/controllers/NotificationChannel/Notification.ts index d8850436..d9366c66 100644 --- a/api-service/src/controllers/NotificationChannel/Notification.ts +++ b/api-service/src/controllers/NotificationChannel/Notification.ts @@ -11,11 +11,11 @@ const telemetryObject = { type: "notificationChannel", ver: "1.0.0" }; const createHandler = async (request: Request, response: Response, next: NextFunction) => { try { - const payload = request.body; + const body = _.get(request, "body"); const userID = (request as any)?.userID; - _.set(payload, "created_by", userID); - _.set(payload, "updated_by", userID); - const notificationBody = await Notification.create(payload); + _.set(body, "created_by", userID); + _.set(body, "updated_by", userID); + const notificationBody = await Notification.create(body); updateTelemetryAuditEvent({ request, object: { id: notificationBody?.dataValues?.id, ...telemetryObject } }); ResponseHandler.successResponse(request, response, { status: httpStatus.OK, data: { id: notificationBody.dataValues.id } }) } catch (err) { @@ -26,8 +26,8 @@ const createHandler = async (request: Request, response: Response, next: NextFun const updateHandler = async (request: Request, response: Response, next: NextFunction) => { try { - const { id } = request.params; - const updatedPayload = request.body; + const id = _.get(request, 'params.id'); + const updatedPayload = _.get(request, 'body'); const notificationPayloadModel = await Notification.findOne({ where: { id } }); const notificationPayload = notificationPayloadModel?.toJSON(); if (!notificationPayload) return next({ message: httpStatus[httpStatus.NOT_FOUND], statusCode: httpStatus.NOT_FOUND }); @@ -47,7 +47,7 @@ const updateHandler = async (request: Request, response: Response, next: NextFun const listHandler = async (request: Request, response: Response, next: NextFunction) => { try { - const { limit, filters, offset } = request.body?.request || {}; + const { limit, filters, offset } = _.get(request.body, 'request', {}); const notifications = await Notification.findAll({ limit: limit, offset: offset, ...(filters && { where: filters }) }); const count = _.get(notifications, "length"); ResponseHandler.successResponse(request, response, { status: httpStatus.OK, data: { notifications, ...(count && { count }) } }); @@ -59,7 +59,7 @@ const listHandler = async (request: Request, response: Response, next: NextFunct const fetchHandler = async (request: Request, response: Response, next: NextFunction) => { try { - const { id } = request.params; + const id = _.get(request, 'params.id'); const notificationPayloadModel = await Notification.findOne({ where: { id } }); const notificationPayload = notificationPayloadModel?.toJSON(); if (!notificationPayloadModel) return next({ message: httpStatus[httpStatus.NOT_FOUND], statusCode: httpStatus.NOT_FOUND }); @@ -73,7 +73,7 @@ const fetchHandler = async (request: Request, response: Response, next: NextFunc const retireHandler = async (request: Request, response: Response, next: NextFunction) => { try { - const { id } = request.params; + const id = _.get(request, 'params.id'); const notificationPayloadModel = await Notification.findOne({ where: { id } }) const notificationPayload = notificationPayloadModel?.toJSON(); if (!notificationPayload) return next({ message: httpStatus[httpStatus.NOT_FOUND], statusCode: httpStatus.NOT_FOUND }); @@ -90,7 +90,7 @@ const retireHandler = async (request: Request, response: Response, next: NextFun const publishHandler = async (request: Request, response: Response, next: NextFunction) => { try { - const { id } = request.params; + const id = _.get(request, 'params.id'); const notificationPayloadModel = await Notification.findOne({ where: { id } }) const notificationPayload = notificationPayloadModel?.toJSON(); if (!notificationPayload) return next({ message: httpStatus[httpStatus.NOT_FOUND], statusCode: httpStatus.NOT_FOUND }); @@ -108,7 +108,7 @@ const publishHandler = async (request: Request, response: Response, next: NextFu const testNotifationChannelHandler = async (request: Request, response: Response, next: NextFunction) => { try { - const { message = "Hello Obsrv", payload = {} } = request.body; + const { message = "Hello Obsrv", payload = {} } = _.get(request, 'body'); const { id } = payload; if (id) { const notificationPayloadModel = await Notification.findOne({ where: { id } }) diff --git a/api-service/src/controllers/UpdateQueryTemplate/UpdateTemplateController.ts b/api-service/src/controllers/UpdateQueryTemplate/UpdateTemplateController.ts index 350b25f7..0f46811f 100644 --- a/api-service/src/controllers/UpdateQueryTemplate/UpdateTemplateController.ts +++ b/api-service/src/controllers/UpdateQueryTemplate/UpdateTemplateController.ts @@ -12,7 +12,7 @@ const apiId = "api.query.template.update"; const requiredVariables = _.get(config, "template_config.template_required_variables"); export const updateQueryTemplate = async (req: Request, res: Response) => { - const requestBody = req.body; + const requestBody = _.get(req, 'body'); const templateId = _.get(req, "params.templateId"); try { const msgid = _.get(req, "body.params.msgid"); diff --git a/api-service/src/helpers/ResponseHandler.ts b/api-service/src/helpers/ResponseHandler.ts index 67332b37..f3b0eb9f 100644 --- a/api-service/src/helpers/ResponseHandler.ts +++ b/api-service/src/helpers/ResponseHandler.ts @@ -12,6 +12,7 @@ const ResponseHandler = { const { body, entity } = req as any; const msgid = _.get(body, ["params", "msgid"]) const resmsgid = _.get(res, "resmsgid") + res.setHeader('STRICT-TRANSPORT-SECURITY', 'max-age=31536000; includeSubDomains'); res.status(result.status || 200).json(ResponseHandler.refactorResponse({ id: (req as any).id, result: result.data, msgid, resmsgid })); entity && onSuccess(req, res) }, @@ -34,6 +35,7 @@ const ResponseHandler = { const resmsgid = _.get(res, "resmsgid") const response = ResponseHandler.refactorResponse({ id, msgid, params: { status: "FAILED" }, responseCode: errCode || httpStatus["500_NAME"], resmsgid }) const modifiedErrorResponse = _.omit(response, ["result"]); + res.setHeader('STRICT-TRANSPORT-SECURITY', 'max-age=31536000; includeSubDomains'); res.status(statusCode || httpStatus.INTERNAL_SERVER_ERROR).json({ ...modifiedErrorResponse, error: { code, message, trace } }); entity && onFailure(req, res) }, @@ -46,6 +48,7 @@ const ResponseHandler = { const msgid = _.get(body, ["params", "msgid"]) const resmsgid = _.get(res, "resmsgid") const response = ResponseHandler.refactorResponse({ id, msgid, params: { status: "FAILED" }, responseCode: errCode || httpStatus["500_NAME"], resmsgid, result: data }) + res.setHeader('STRICT-TRANSPORT-SECURITY', 'max-age=31536000; includeSubDomains'); res.status(statusCode || httpStatus.INTERNAL_SERVER_ERROR).json({ ...response, error: { code, message } }); entity && onObsrvFailure(req, res, error) }, @@ -58,11 +61,13 @@ const ResponseHandler = { flatResponse: (req: Request, res: Response, result: Result) => { const { entity } = req as any; entity && onSuccess(req, res) + res.setHeader('STRICT-TRANSPORT-SECURITY', 'max-age=31536000; includeSubDomains'); res.status(result.status).send(result.data); }, goneResponse: (req: Request, res: Response) => { const { id } = req as any; + res.setHeader('STRICT-TRANSPORT-SECURITY', 'max-age=31536000; includeSubDomains'); res.status(httpStatus.GONE).json({ id: id, ver: "v1", ts: Date.now(), params: { status: "FAILED", errmsg: "v1 APIs have been replace by /v2 APIs. Please refer to this link for more information" }, responseCode: httpStatus["410_NAME"] }) } } diff --git a/api-service/src/metrics/prometheus/index.ts b/api-service/src/metrics/prometheus/index.ts index 173527ad..5659cda2 100644 --- a/api-service/src/metrics/prometheus/index.ts +++ b/api-service/src/metrics/prometheus/index.ts @@ -25,6 +25,7 @@ const metricsScrapeHandler = async (req: any, res: any, next: NextFunction) => { try { res.set("Content-Type", register.contentType); const metrics = await register.metrics() + res.setHeader('STRICT-TRANSPORT-SECURITY', 'max-age=31536000; includeSubDomains'); res.status(200).send(metrics); } catch (error) { next(error) diff --git a/api-service/tsconfig.json b/api-service/tsconfig.json index 9e73c4de..1bf38915 100644 --- a/api-service/tsconfig.json +++ b/api-service/tsconfig.json @@ -27,7 +27,7 @@ /* Modules */ "module": "commonjs", /* Specify what module code is generated. */ "rootDir": "./src", /* Specify the root folder within your source files. */ - // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ diff --git a/command-service/.DS_Store b/command-service/.DS_Store deleted file mode 100644 index 5008ddfc..00000000 Binary files a/command-service/.DS_Store and /dev/null differ diff --git a/command-service/Dockerfile b/command-service/Dockerfile index 4e7fe11a..37d4a984 100644 --- a/command-service/Dockerfile +++ b/command-service/Dockerfile @@ -1,4 +1,18 @@ -FROM --platform=linux/amd64 python:3.12-slim +# ---- Stage 1: Rebuild Go binaries with Go 1.25.7 to fix crypto/tls CVE ---- +FROM --platform=linux/amd64 public.ecr.aws/docker/library/golang:1.25.7-bookworm AS go-builder + +# Rebuild kubectl v1.35.1 from source with patched Go toolchain +WORKDIR /build/kubernetes +RUN git clone --depth 1 --branch v1.35.1 https://github.com/kubernetes/kubernetes.git . && \ + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" -o /out/kubectl ./cmd/kubectl + +# Rebuild helm v3.20.0 from source with patched Go toolchain +WORKDIR /build/helm +RUN git clone --depth 1 --branch v3.20.0 https://github.com/helm/helm.git . && \ + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" -o /out/helm ./cmd/helm + +# ---- Stage 2: Final application image ---- +FROM --platform=linux/amd64 public.ecr.aws/docker/library/python:3.12-slim RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ @@ -9,11 +23,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ vim \ && rm -rf /var/lib/apt/lists/* -RUN curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" && \ - chmod +x kubectl && \ - mv kubectl /usr/local/bin/ +# Fix MEDIUM: upgrade pip to >= 25.3 (CVE fix) +RUN pip install --no-cache-dir --upgrade "pip>=25.3" -RUN curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash +# Copy Go binaries rebuilt with Go 1.25.7 (all Go stdlib & module CVEs fixed) +COPY --from=go-builder /out/kubectl /usr/local/bin/kubectl +COPY --from=go-builder /out/helm /usr/local/bin/helm WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt diff --git a/command-service/requirements.txt b/command-service/requirements.txt index 8593fc9f..839762db 100644 --- a/command-service/requirements.txt +++ b/command-service/requirements.txt @@ -1,9 +1,10 @@ -fastapi==0.103.0 +fastapi~=0.129.0 +starlette>=0.49.1 uvicorn==0.20.0 dataclasses-json==0.5.7 # pydantic==1.10.5 pyyaml==6.0.1 -urllib3==2.0.7 +urllib3==2.6.3 # protobuf==3.20.* # pyhelm==2.14.5 # kubernetes==28.1.0 diff --git a/system-rules-ingestor/Dockerfile b/system-rules-ingestor/Dockerfile index 20a2c2fc..fc968252 100644 --- a/system-rules-ingestor/Dockerfile +++ b/system-rules-ingestor/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=linux/amd64 node:20.10-alpine +FROM --platform=linux/amd64 public.ecr.aws/docker/library/node:20.10-alpine WORKDIR /opt/app COPY ./package.json . RUN yarn install --silent