Skip to content

Commit 8d1b863

Browse files
authored
add host page and endpoint deletion logic (#143)
1 parent a1baa84 commit 8d1b863

File tree

12 files changed

+723
-3
lines changed

12 files changed

+723
-3
lines changed

backend/src/api/get-endpoints/index.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { Response } from "express"
22
import validator from "validator"
33
import { GetEndpointsService } from "services/get-endpoints"
4-
import { GetEndpointParams } from "@common/types"
4+
import { GetEndpointParams, GetHostParams } from "@common/types"
55
import ApiResponseHandler from "api-response-handler"
66
import Error404NotFound from "errors/error-404-not-found"
77
import { MetloRequest } from "types"
8+
import Error400BadRequest from "errors/error-400-bad-request"
89

910
export const getEndpointsHandler = async (
1011
req: MetloRequest,
@@ -72,6 +73,9 @@ export const updateEndpointIsAuthenticated = async (
7273
): Promise<void> => {
7374
try {
7475
const { endpointId } = req.params
76+
if (!validator.isUUID(endpointId)) {
77+
throw new Error404NotFound("Endpoint does not exist.")
78+
}
7579
const params: { authenticated: boolean } = req.body
7680
await GetEndpointsService.updateIsAuthenticated(
7781
req.ctx,
@@ -83,3 +87,48 @@ export const updateEndpointIsAuthenticated = async (
8387
await ApiResponseHandler.error(res, err)
8488
}
8589
}
90+
91+
export const deleteEndpointHandler = async (
92+
req: MetloRequest,
93+
res: Response,
94+
): Promise<void> => {
95+
try {
96+
const { endpointId } = req.params
97+
if (!validator.isUUID(endpointId)) {
98+
throw new Error404NotFound("Endpoint does not exist.")
99+
}
100+
await GetEndpointsService.deleteEndpoint(req.ctx, endpointId)
101+
await ApiResponseHandler.success(res, "Success")
102+
} catch (err) {
103+
await ApiResponseHandler.error(res, err)
104+
}
105+
}
106+
107+
export const deleteHostHandler = async (
108+
req: MetloRequest,
109+
res: Response,
110+
): Promise<void> => {
111+
try {
112+
const { host } = req.body
113+
if (!host) {
114+
throw new Error400BadRequest("Must provide host.")
115+
}
116+
await GetEndpointsService.deleteHost(req.ctx, host)
117+
await ApiResponseHandler.success(res, "Success")
118+
} catch (err) {
119+
await ApiResponseHandler.error(res, err)
120+
}
121+
}
122+
123+
export const getHostsListHandler = async (
124+
req: MetloRequest,
125+
res: Response,
126+
): Promise<void> => {
127+
const hostsParams: GetHostParams = req.query
128+
try {
129+
const resp = await GetEndpointsService.getHostsList(req.ctx, hostsParams)
130+
await ApiResponseHandler.success(res, resp)
131+
} catch (err) {
132+
await ApiResponseHandler.error(res, err)
133+
}
134+
}

backend/src/index.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ dotenv.config()
44
import express, { Express, Response } from "express"
55
import { InstanceSettings } from "models"
66
import {
7+
deleteEndpointHandler,
8+
deleteHostHandler,
79
getEndpointHandler,
810
getEndpointsHandler,
911
getHostsHandler,
12+
getHostsListHandler,
1013
getUsageHandler,
1114
updateEndpointIsAuthenticated,
1215
} from "api/get-endpoints"
@@ -83,8 +86,15 @@ apiRouter.put(
8386
"/api/v1/endpoint/:endpointId/authenticated",
8487
updateEndpointIsAuthenticated,
8588
)
89+
apiRouter.delete("/api/v1/endpoint/:endpointId", deleteEndpointHandler)
90+
apiRouter.delete("/api/v1/host", deleteHostHandler)
91+
apiRouter.get("/api/v1/hosts", getHostsListHandler)
8692

87-
apiRouter.post("/api/v1/spec/new", MulterSource.single("file"), uploadNewSpecHandler)
93+
apiRouter.post(
94+
"/api/v1/spec/new",
95+
MulterSource.single("file"),
96+
uploadNewSpecHandler,
97+
)
8898
apiRouter.delete("/api/v1/spec/:specFileName", deleteSpecHandler)
8999
apiRouter.put(
90100
"/api/v1/spec/:specFileName",

backend/src/services/get-endpoints/index.ts

Lines changed: 177 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { QueryRunner } from "typeorm"
12
import { AppDataSource } from "data-source"
23
import {
34
ApiEndpoint,
@@ -7,12 +8,15 @@ import {
78
Alert,
89
DataField,
910
OpenApiSpec,
11+
Attack,
1012
} from "models"
1113
import {
1214
GetEndpointParams,
1315
ApiEndpoint as ApiEndpointResponse,
1416
ApiEndpointDetailed as ApiEndpointDetailedResponse,
1517
Usage as UsageResponse,
18+
GetHostParams,
19+
HostResponse,
1620
} from "@common/types"
1721
import Error500InternalServer from "errors/error-500-internal-server"
1822
import { Test } from "@metlo/testing"
@@ -50,6 +54,137 @@ ORDER BY
5054
`
5155

5256
export class GetEndpointsService {
57+
static async deleteEndpoint(
58+
ctx: MetloContext,
59+
apiEndpointUuid: string,
60+
): Promise<void> {
61+
const queryRunner = AppDataSource.createQueryRunner()
62+
try {
63+
await queryRunner.connect()
64+
const endpoint = await getEntityManager(ctx, queryRunner).findOneBy(
65+
ApiEndpoint,
66+
{ uuid: apiEndpointUuid },
67+
)
68+
if (!endpoint) {
69+
throw new Error404NotFound("Endpoint not found.")
70+
}
71+
await queryRunner.startTransaction()
72+
await getQB(ctx, queryRunner)
73+
.delete()
74+
.from(AggregateTraceDataHourly)
75+
.andWhere(`"apiEndpointUuid" = :id`, { id: apiEndpointUuid })
76+
.execute()
77+
await getQB(ctx, queryRunner)
78+
.delete()
79+
.from(Alert)
80+
.andWhere(`"apiEndpointUuid" = :id`, { id: apiEndpointUuid })
81+
.execute()
82+
await getQB(ctx, queryRunner)
83+
.delete()
84+
.from(ApiEndpointTest)
85+
.andWhere(`"apiEndpointUuid" = :id`, { id: apiEndpointUuid })
86+
.execute()
87+
await getQB(ctx, queryRunner)
88+
.delete()
89+
.from(ApiTrace)
90+
.andWhere(`"apiEndpointUuid" = :id`, { id: apiEndpointUuid })
91+
.execute()
92+
await getQB(ctx, queryRunner)
93+
.delete()
94+
.from(Attack)
95+
.andWhere(`"apiEndpointUuid" = :id`, { id: apiEndpointUuid })
96+
.execute()
97+
await getQB(ctx, queryRunner)
98+
.delete()
99+
.from(DataField)
100+
.andWhere(`"apiEndpointUuid" = :id`, { id: apiEndpointUuid })
101+
.execute()
102+
await getQB(ctx, queryRunner)
103+
.delete()
104+
.from(ApiEndpoint)
105+
.andWhere("uuid = :id", { id: apiEndpointUuid })
106+
.execute()
107+
await queryRunner.commitTransaction()
108+
} catch (err) {
109+
if (queryRunner.isTransactionActive) {
110+
await queryRunner.rollbackTransaction()
111+
}
112+
throw err
113+
} finally {
114+
await queryRunner.release()
115+
}
116+
}
117+
118+
static async deleteEndpointsBatch(
119+
ctx: MetloContext,
120+
apiEndpointUuids: string[],
121+
queryRunner: QueryRunner,
122+
): Promise<void> {
123+
await getQB(ctx, queryRunner)
124+
.delete()
125+
.from(AggregateTraceDataHourly)
126+
.andWhere(`"apiEndpointUuid" IN(:...ids)`, { ids: apiEndpointUuids })
127+
.execute()
128+
await getQB(ctx, queryRunner)
129+
.delete()
130+
.from(Alert)
131+
.andWhere(`"apiEndpointUuid" IN(:...ids)`, { ids: apiEndpointUuids })
132+
.execute()
133+
await getQB(ctx, queryRunner)
134+
.delete()
135+
.from(ApiEndpointTest)
136+
.andWhere(`"apiEndpointUuid" IN(:...ids)`, { ids: apiEndpointUuids })
137+
.execute()
138+
await getQB(ctx, queryRunner)
139+
.delete()
140+
.from(ApiTrace)
141+
.andWhere(`"apiEndpointUuid" IN(:...ids)`, { ids: apiEndpointUuids })
142+
.execute()
143+
await getQB(ctx, queryRunner)
144+
.delete()
145+
.from(Attack)
146+
.andWhere(`"apiEndpointUuid" IN(:...ids)`, { ids: apiEndpointUuids })
147+
.execute()
148+
await getQB(ctx, queryRunner)
149+
.delete()
150+
.from(DataField)
151+
.andWhere(`"apiEndpointUuid" IN(:...ids)`, { ids: apiEndpointUuids })
152+
.execute()
153+
await getQB(ctx, queryRunner)
154+
.delete()
155+
.from(ApiEndpoint)
156+
.andWhere("uuid IN(:...ids)", { ids: apiEndpointUuids })
157+
.execute()
158+
}
159+
160+
static async deleteHost(ctx: MetloContext, host: string): Promise<void> {
161+
const queryRunner = AppDataSource.createQueryRunner()
162+
try {
163+
await queryRunner.connect()
164+
const endpoints = await getQB(ctx, queryRunner)
165+
.select(["uuid"])
166+
.from(ApiEndpoint, "endpoint")
167+
.andWhere("host = :host", { host })
168+
.getRawMany()
169+
if (endpoints?.length > 0) {
170+
await queryRunner.startTransaction()
171+
await this.deleteEndpointsBatch(
172+
ctx,
173+
endpoints?.map(e => e.uuid),
174+
queryRunner,
175+
)
176+
await queryRunner.commitTransaction()
177+
}
178+
} catch (err) {
179+
if (queryRunner.isTransactionActive) {
180+
await queryRunner.rollbackTransaction()
181+
}
182+
throw err
183+
} finally {
184+
await queryRunner.release()
185+
}
186+
}
187+
53188
static async updateIsAuthenticated(
54189
ctx: MetloContext,
55190
apiEndpointUuid: string,
@@ -134,7 +269,7 @@ export class GetEndpointsService {
134269
whereFilterString = `WHERE ${whereFilter.join(" AND ")}`
135270
}
136271
const limitFilter = `LIMIT ${getEndpointParams?.limit ?? 10}`
137-
const offsetFilter = `OFFSET ${getEndpointParams?.offset ?? 10}`
272+
const offsetFilter = `OFFSET ${getEndpointParams?.offset ?? 0}`
138273

139274
const endpointResults = await queryRunner.query(
140275
getEndpointsQuery(ctx, whereFilterString, limitFilter, offsetFilter),
@@ -224,6 +359,47 @@ export class GetEndpointsService {
224359
}
225360
}
226361

362+
static async getHostsList(
363+
ctx: MetloContext,
364+
getHostsParams: GetHostParams,
365+
): Promise<[HostResponse[], any]> {
366+
const queryRunner = AppDataSource.createQueryRunner()
367+
try {
368+
await queryRunner.connect()
369+
370+
let qb = getQB(ctx, queryRunner)
371+
.select(["host", `COUNT(uuid) as "numEndpoints"`])
372+
.from(ApiEndpoint, "endpoint")
373+
.distinct(true)
374+
.groupBy("host")
375+
let totalHostsQb = await getQB(ctx, queryRunner)
376+
.select([`COUNT(DISTINCT(host))::int as "numHosts"`])
377+
.from(ApiEndpoint, "endpoint")
378+
379+
if (getHostsParams?.searchQuery) {
380+
qb = qb.andWhere("host ILIKE :searchQuery", {
381+
searchQuery: `%${getHostsParams.searchQuery}%`,
382+
})
383+
totalHostsQb = totalHostsQb.andWhere("host ILIKE :searchQuery", {
384+
searchQuery: `%${getHostsParams.searchQuery}%`,
385+
})
386+
}
387+
388+
qb = qb
389+
.limit(getHostsParams?.limit ?? 10)
390+
.offset(getHostsParams?.offset ?? 0)
391+
392+
const hostsResp = await qb.getRawMany()
393+
const totalHosts = await totalHostsQb.getRawOne()
394+
395+
return [hostsResp, totalHosts?.numHosts ?? 0]
396+
} catch (err) {
397+
throw new Error500InternalServer(err)
398+
} finally {
399+
await queryRunner.release()
400+
}
401+
}
402+
227403
static async getUsage(
228404
ctx: MetloContext,
229405
endpointId: string,

common/src/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,12 @@ export interface GetEndpointParams {
109109
limit?: number
110110
}
111111

112+
export interface GetHostParams {
113+
offset?: number
114+
limit?: number
115+
searchQuery?: string
116+
}
117+
112118
export interface GetAlertParams {
113119
uuid?: string
114120
apiEndpointUuid?: string
@@ -228,6 +234,11 @@ export interface ApiEndpointDetailed extends ApiEndpoint {
228234
dataFields: DataField[]
229235
}
230236

237+
export interface HostResponse {
238+
host: string
239+
numEndpoints: number
240+
}
241+
231242
export interface TestDetailed {
232243
uuid: string
233244
name: string

frontend/src/api/endpoints/index.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import {
33
ApiEndpoint,
44
ApiEndpointDetailed,
55
GetEndpointParams,
6+
GetHostParams,
7+
HostResponse,
68
Usage,
79
} from "@common/types"
810
import { getAPIURL } from "~/constants"
@@ -96,3 +98,25 @@ export const updateEndpointAuthenticated = async (
9698
{ headers },
9799
)
98100
}
101+
102+
export const getHostsList = async (
103+
params: GetHostParams,
104+
headers?: AxiosRequestHeaders,
105+
): Promise<[HostResponse[], number]> => {
106+
const resp = await axios.get<[HostResponse[], number]>(
107+
`${getAPIURL()}/hosts`,
108+
{ params, headers },
109+
)
110+
return resp.data
111+
}
112+
113+
export const deleteHost = async (
114+
host: string,
115+
headers?: AxiosRequestHeaders,
116+
): Promise<any> => {
117+
const resp = await axios.delete(`${getAPIURL()}/host`, {
118+
data: { host },
119+
headers,
120+
})
121+
return resp.data
122+
}

0 commit comments

Comments
 (0)