Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add site audit logs generator #1181

Merged
merged 11 commits into from
Mar 6, 2024
4 changes: 4 additions & 0 deletions .aws/deploy/backend-task-definition.prod.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@
{
"name": "SITE_CHECKER_FORM_KEY",
"valueFrom": "PROD_SITE_CHECKER_FORM_KEY"
},
{
"name": "SITE_AUDIT_LOGS_FORM_KEY",
"valueFrom": "PROD_SITE_AUDIT_LOGS_FORM_KEY"
}
],
"logConfiguration": {
Expand Down
4 changes: 4 additions & 0 deletions .aws/deploy/backend-task-definition.staging.json
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@
{
"name": "SITE_CHECKER_FORM_KEY",
"valueFrom": "STAGING_SITE_CHECKER_FORM_KEY"
},
{
"name": "SITE_AUDIT_LOGS_FORM_KEY",
"valueFrom": "STAGING_SITE_AUDIT_LOGS_FORM_KEY"
}
],
"logConfiguration": {
Expand Down
1 change: 1 addition & 0 deletions .platform/hooks/predeploy/06_fetch_ssm_parameters.sh
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ ENV_VARS=(
"UPTIME_ROBOT_API_KEY"
"GGS_REPAIR_FORM_KEY"
"SITE_CHECKER_FORM_KEY"
"SITE_AUDIT_LOGS_FORM_KEY"
)

echo "Set AWS region"
Expand Down
7 changes: 7 additions & 0 deletions src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

convict.addFormat({
name: "required-string",
validate: (val: any) => {

Check warning on line 5 in src/config/config.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
if (!val) throw new Error("value cannot be empty, null or undefined")
if (typeof val !== "string") throw new Error("value must be a string")
},
Expand All @@ -10,14 +10,14 @@

convict.addFormat({
name: "required-positive-number",
validate: (val: any) => {

Check warning on line 13 in src/config/config.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
if (val === null || val === undefined || val === "")
throw new Error("value cannot be empty, null or undefined")
if (typeof val !== "number") throw new Error("value must be a number")
},
coerce: (val: string) => {
const coercedVal = Number(val)
if (isNaN(coercedVal)) {

Check warning on line 20 in src/config/config.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected use of 'isNaN'. Use Number.isNaN instead https://github.com/airbnb/javascript#standard-library--isnan
throw new Error(
"value provided is not a positive number. please provide a valid positive number"
)
Expand All @@ -31,7 +31,7 @@

convict.addFormat({
name: "required-boolean",
validate: (val: any) => {

Check warning on line 34 in src/config/config.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
if (val === null || val === undefined)
throw new Error("value cannot be empty, null or undefined")
if (typeof val !== "boolean") throw new Error("value must be a boolean")
Expand Down Expand Up @@ -314,6 +314,13 @@
format: "required-string",
default: "",
},
siteAuditLogsFormKey: {
doc: "FormSG API key for site audit logs form",
env: "SITE_AUDIT_LOGS_FORM_KEY",
sensitive: true,
format: "required-string",
default: "",
},
},
postman: {
apiKey: {
Expand Down
4 changes: 4 additions & 0 deletions src/constants/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,24 @@

import { config } from "@config/config"

export enum JobStatus {

Check warning on line 5 in src/constants/constants.ts

View workflow job for this annotation

GitHub Actions / lint

'JobStatus' is already declared in the upper scope on line 5 column 13
Ready = "READY", // Ready to run jobs
Running = "RUNNING", // A job is running
Failed = "FAILED", // A job has failed and recovery is needed
}

export enum SiteStatus {

Check warning on line 11 in src/constants/constants.ts

View workflow job for this annotation

GitHub Actions / lint

'SiteStatus' is already declared in the upper scope on line 11 column 13
Empty = "EMPTY", // A site record site is being initialized
Initialized = "INITIALIZED",
Launched = "LAUNCHED",
}

export enum RedirectionTypes {

Check warning on line 17 in src/constants/constants.ts

View workflow job for this annotation

GitHub Actions / lint

'RedirectionTypes' is already declared in the upper scope on line 17 column 13
CNAME = "CNAME",
A = "A",
}

export enum CollaboratorRoles {

Check warning on line 22 in src/constants/constants.ts

View workflow job for this annotation

GitHub Actions / lint

'CollaboratorRoles' is already declared in the upper scope on line 22 column 13
Admin = "ADMIN",
Contributor = "CONTRIBUTOR",
IsomerAdmin = "ISOMERADMIN",
Expand All @@ -30,7 +30,7 @@
CollaboratorRoles.IsomerAdmin
>

export enum ReviewRequestStatus {

Check warning on line 33 in src/constants/constants.ts

View workflow job for this annotation

GitHub Actions / lint

'ReviewRequestStatus' is already declared in the upper scope on line 33 column 13
Approved = "APPROVED",
Open = "OPEN",
Merged = "MERGED",
Expand Down Expand Up @@ -87,6 +87,10 @@
config.get("aws.efs.volPath"),
"repos-lite"
)
export const EFS_VOL_PATH_AUDIT_LOGS = path.join(
config.get("aws.efs.volPath"),
"audit-logs"
)
export const STAGING_BRANCH = "staging"
export const STAGING_LITE_BRANCH = "staging-lite"
export const PLACEHOLDER_FILE_NAME = ".keep"
Expand Down
11 changes: 11 additions & 0 deletions src/errors/AuditLogsError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { BaseIsomerError } from "./BaseError"

export default class AuditLogsError extends BaseIsomerError {
constructor(message: string) {
super({
status: 500,
code: "AuditLogsError",
message,
})
}
}
128 changes: 128 additions & 0 deletions src/routes/formsg/formsgSiteAuditLogs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/* eslint-disable import/prefer-default-export */
import type { DecryptedContentAndAttachments } from "@opengovsg/formsg-sdk/dist/types"
import express, { RequestHandler } from "express"

import { config } from "@root/config/config"
import InitializationError from "@root/errors/InitializationError"
import logger from "@root/logger/logger"
import { attachFormSGHandler } from "@root/middleware"
import AuditLogsService from "@root/services/admin/AuditLogsService"
import { getField, getFieldsFromTable } from "@root/utils/formsg-utils"

interface FormsgSiteAuditLogsRouterProps {
auditLogsService: AuditLogsService
}

const SITE_AUDIT_LOGS_FORM_KEY = config.get("formSg.siteAuditLogsFormKey")

const REQUESTER_EMAIL_FIELD = "Where should we send the email address to?"
const REPO_NAME_FIELD =
"What is the name of the Isomer site that you need logs for? (Repo Name (in GitHub))"
const LOGS_TIMEFRAME_FIELD = "I need a log of edits made in:"
const LOGS_TIMEFRAME_START_FIELD = "Start date"
const LOGS_TIMEFRAME_END_FIELD = "End date"

export class FormsgSiteAuditLogsRouter {
private readonly auditLogsService: FormsgSiteAuditLogsRouterProps["auditLogsService"]

constructor({ auditLogsService }: FormsgSiteAuditLogsRouterProps) {
this.auditLogsService = auditLogsService
}

getAuditLogsHandler: RequestHandler<
never,
Record<string, never>,
{ data: { submissionId: string } },
never,
{ submission: DecryptedContentAndAttachments }
> = async (req, res) => {
let startDate = "1970-01-01"
kishore03109 marked this conversation as resolved.
Show resolved Hide resolved
let endDate = new Date().toISOString().split("T")[0]
const repoNames: Set<string> = new Set()

const { responses } = res.locals.submission.content

const requesterEmail = getField(responses, REQUESTER_EMAIL_FIELD)

if (!requesterEmail) {
logger.error(
"No requester email was provided in site audit logs form submission"
)
return res.sendStatus(400)
}

const repoNamesFromTable = getFieldsFromTable(responses, REPO_NAME_FIELD)

if (!repoNamesFromTable) {
logger.error(
"No repo names were provided in site audit logs form submission"
)
return res.sendStatus(400)
}

repoNamesFromTable.forEach((repoName) => {
if (typeof repoName === "string") {
// actually wont happen based on our formsg form, but this code
// is added defensively
repoNames.add(repoName)
} else {
repoNames.add(repoName[0])
kishore03109 marked this conversation as resolved.
Show resolved Hide resolved
}
})

const logsTimeframe = getField(responses, LOGS_TIMEFRAME_FIELD)

if (logsTimeframe === "The past calendar year") {
startDate = `${new Date().getFullYear() - 1}-01-01`
endDate = `${new Date().getFullYear() - 1}-12-31`
} else if (logsTimeframe === "The past calendar month") {
const startDateObject = new Date()
startDateObject.setMonth(startDateObject.getMonth() - 1)
const endDateObject = new Date()
endDateObject.setDate(0)

startDate = `${startDateObject.getFullYear()}-${startDateObject
.getMonth()
.toString()
.padStart(2, "0")}-01`
endDate = `${endDateObject.getFullYear()}-${endDateObject
.getMonth()
.toString()
.padStart(2, "0")}-${endDateObject.getDate()}`
} else {
const startDateField = getField(responses, LOGS_TIMEFRAME_START_FIELD)
const endDateField = getField(responses, LOGS_TIMEFRAME_END_FIELD)
if (startDateField && endDateField) {
startDate = startDateField
endDate = endDateField
}
kishore03109 marked this conversation as resolved.
Show resolved Hide resolved
}

res.sendStatus(200)

this.auditLogsService.getAuditLogsViaFormsg(
requesterEmail,
Array.from(repoNames),
startDate,
endDate,
req.body.data.submissionId
)
}

getRouter() {
const router = express.Router({ mergeParams: true })
if (!SITE_AUDIT_LOGS_FORM_KEY) {
throw new InitializationError(
"Required SITE_AUDIT_LOGS_FORM_KEY environment variable is not defined"
)
}

router.post(
"/audit-logs",
attachFormSGHandler(SITE_AUDIT_LOGS_FORM_KEY),
this.getAuditLogsHandler
)

return router
}
}
18 changes: 17 additions & 1 deletion src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,12 @@ import { mailer } from "@services/utilServices/MailClient"
import { database } from "./database/config"
import { apiLogger } from "./middleware/apiLogger"
import { NotificationOnEditHandler } from "./middleware/notificationOnEditHandler"
import { FormsgSiteAuditLogsRouter } from "./routes/formsg/formsgSiteAuditLogs"
import getAuthenticatedSubrouter from "./routes/v2/authenticated"
import { ReviewsRouter } from "./routes/v2/authenticated/review"
import getAuthenticatedSitesSubrouter from "./routes/v2/authenticatedSites"
import { SgidAuthRouter } from "./routes/v2/sgidAuth"
import AuditLogsService from "./services/admin/AuditLogsService"
import RepoManagementService from "./services/admin/RepoManagementService"
import GitFileCommitService from "./services/db/GitFileCommitService"
import GitFileSystemService from "./services/db/GitFileSystemService"
Expand All @@ -88,8 +90,8 @@ import CollaboratorsService from "./services/identity/CollaboratorsService"
import LaunchClient from "./services/identity/LaunchClient"
import LaunchesService from "./services/identity/LaunchesService"
import DynamoDBDocClient from "./services/infra/DynamoDBClient"
import ReviewCommentService from "./services/review/ReviewCommentService"
import RepoCheckerService from "./services/review/RepoCheckerService"
import ReviewCommentService from "./services/review/ReviewCommentService"
import { rateLimiter } from "./services/utilServices/RateLimiter"
import SgidAuthService from "./services/utilServices/SgidAuthService"
import { isSecure } from "./utils/auth-utils"
Expand Down Expand Up @@ -315,6 +317,15 @@ const repoCheckerService = new RepoCheckerService({
git: simpleGitInstance,
})

const auditLogsService = new AuditLogsService({
collaboratorsService,
isomerAdminsService,
notificationsService,
reviewRequestService,
sitesService,
usersService,
})

// poller site launch updates
infraService.pollMessages()

Expand Down Expand Up @@ -399,6 +410,10 @@ const formsgSiteCheckerRouter = new FormsgSiteCheckerRouter({
repoCheckerService,
})

const formsgSiteAuditLogsRouter = new FormsgSiteAuditLogsRouter({
auditLogsService,
})

const app = express()

if (isSecure) {
Expand Down Expand Up @@ -440,6 +455,7 @@ app.use("/formsg", formsgSiteCreateRouter.getRouter())
app.use("/formsg", formsgSiteLaunchRouter.getRouter())
app.use("/formsg", formsgGGsRepairRouter.getRouter())
app.use("/formsg", formsgSiteCheckerRouter.getRouter())
app.use("/formsg", formsgSiteAuditLogsRouter.getRouter())

// catch unknown routes
app.use((req, res, next) => {
Expand Down
Loading
Loading