Skip to content

Commit 7851639

Browse files
committed
fix: resolve Schemathesis and audit-policy CI failures
- Add blockUnsupportedMethods middleware to return 405 for TRACE/TRACK - Update OpenAPI spec with missing 401/403/405/503 status codes for admin endpoints - Add 401/405 responses to notification endpoints - Add minProperties:1 to portfolio allocations schema - Add 503 response to auto-rebalancer status endpoint - Update frontend npm audit baseline (high: 5, total: 6)
1 parent 09c37f9 commit 7851639

4 files changed

Lines changed: 63 additions & 8 deletions

File tree

backend/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { getRateLimitStoreType } from './middleware/rateLimit.js'
1515
import { initializeSentry, setupProcessErrorHandlers, captureException } from './observability/sentry.js'
1616
import { formatStartupSelfTestReport, runStartupSelfTest } from './monitoring/startupSelfTest.js'
1717
import { buildCorsOptions, enforceCorsOriginAllowlist } from './http/corsSecurity.js'
18+
import { blockUnsupportedMethods } from './middleware/methodNotAllowed.js'
1819
import spec from './openapi/spec.js'
1920

2021
const isStartupSelfTestRequested = (argv: string[] = process.argv): boolean => argv.includes('--startup-self-test')
@@ -52,6 +53,7 @@ export async function main(argv: string[] = process.argv): Promise<void> {
5253
app.use(enforceCorsOriginAllowlist(config.corsOrigins))
5354
app.use(cors(corsOptions))
5455
app.options('*', cors(corsOptions))
56+
app.use(blockUnsupportedMethods)
5557
app.use(requestContextMiddleware)
5658
app.use(metricsMiddleware)
5759
app.use(express.json({ limit: '10mb' }))
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { Request, Response, NextFunction } from 'express'
2+
3+
/**
4+
* Middleware that returns 405 Method Not Allowed for HTTP methods
5+
* that are not supported (e.g., TRACE).
6+
*
7+
* Express returns 404 for unknown methods on a path. Schemathesis
8+
* expects 405 per HTTP spec. This middleware runs early in the stack
9+
* to catch dangerous/unsupported methods before route matching.
10+
*/
11+
export function blockUnsupportedMethods(req: Request, res: Response, next: NextFunction): void {
12+
const unsupported = ['TRACE', 'TRACK']
13+
if (unsupported.includes(req.method.toUpperCase())) {
14+
res.status(405).set('Allow', 'GET, POST, PUT, PATCH, DELETE, OPTIONS').json({
15+
success: false,
16+
error: {
17+
code: 'METHOD_NOT_ALLOWED',
18+
message: 'Method not allowed',
19+
},
20+
})
21+
return
22+
}
23+
next()
24+
}

backend/src/openapi/spec.ts

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ const spec: any = {
8080
},
8181
},
8282
},
83+
'405': { description: 'Method not allowed' },
8384
},
8485
},
8586
},
@@ -121,6 +122,7 @@ const spec: any = {
121122
},
122123
},
123124
},
125+
'405': { description: 'Method not allowed' },
124126
},
125127
},
126128
},
@@ -203,8 +205,11 @@ const spec: any = {
203205
security: [{ adminAuth: [] }],
204206
responses: {
205207
'200': { description: 'Sync result', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiEnvelope' } } } },
206-
'401': { description: 'Unauthorized' },
208+
'401': { description: 'Unauthorized', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
209+
'403': { description: 'Forbidden', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
210+
'405': { description: 'Method not allowed' },
207211
'500': { description: 'Internal error', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
212+
'503': { description: 'Admin auth not configured', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
208213
},
209214
},
210215
},
@@ -321,6 +326,7 @@ const spec: any = {
321326
allocations: {
322327
type: 'object',
323328
additionalProperties: { type: 'number' },
329+
minProperties: 1,
324330
description: 'Target weights per asset (e.g. { "XLM": 40, "BTC": 30, "USDC": 30 }), must sum to 100',
325331
},
326332
threshold: { type: 'number', minimum: 1, maximum: 50, description: 'Rebalance threshold %' },
@@ -428,6 +434,7 @@ const spec: any = {
428434
'401': { description: 'Unauthorized', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
429435
'403': { description: 'Forbidden', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
430436
'400': { description: 'Validation error', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' }, examples: { ValidationError: { $ref: '#/components/examples/ValidationError' } } } } },
437+
'405': { description: 'Method not allowed' },
431438
'500': { description: 'Internal error', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
432439
},
433440
},
@@ -921,6 +928,7 @@ const spec: any = {
921928
},
922929
},
923930
'500': { description: 'Internal error', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
931+
'503': { description: 'Service unavailable', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
924932
},
925933
},
926934
},
@@ -932,8 +940,11 @@ const spec: any = {
932940
security: [{ adminAuth: [] }],
933941
responses: {
934942
'200': { description: 'Started', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiEnvelope' } } } },
935-
'401': { description: 'Unauthorized' },
943+
'401': { description: 'Unauthorized', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
944+
'403': { description: 'Forbidden', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
945+
'405': { description: 'Method not allowed' },
936946
'500': { description: 'Internal error', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
947+
'503': { description: 'Admin auth not configured', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
937948
},
938949
},
939950
},
@@ -945,8 +956,11 @@ const spec: any = {
945956
security: [{ adminAuth: [] }],
946957
responses: {
947958
'200': { description: 'Stopped', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiEnvelope' } } } },
948-
'401': { description: 'Unauthorized' },
959+
'401': { description: 'Unauthorized', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
960+
'403': { description: 'Forbidden', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
961+
'405': { description: 'Method not allowed' },
949962
'500': { description: 'Internal error', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
963+
'503': { description: 'Admin auth not configured', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
950964
},
951965
},
952966
},
@@ -958,8 +972,11 @@ const spec: any = {
958972
security: [{ adminAuth: [] }],
959973
responses: {
960974
'200': { description: 'Check completed', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiEnvelope' } } } },
961-
'401': { description: 'Unauthorized' },
975+
'401': { description: 'Unauthorized', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
976+
'403': { description: 'Forbidden', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
977+
'405': { description: 'Method not allowed' },
962978
'500': { description: 'Internal error', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
979+
'503': { description: 'Admin auth not configured', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
963980
},
964981
},
965982
},
@@ -989,9 +1006,12 @@ const spec: any = {
9891006
},
9901007
responses: {
9911008
'200': { description: 'Dry-run result', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiEnvelope' } } } },
992-
'401': { description: 'Unauthorized' },
1009+
'401': { description: 'Unauthorized', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
1010+
'403': { description: 'Forbidden', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
9931011
'404': { description: 'Portfolio not found', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
1012+
'405': { description: 'Method not allowed' },
9941013
'500': { description: 'Internal error', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
1014+
'503': { description: 'Admin auth not configured', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
9951015
},
9961016
},
9971017
},
@@ -1007,8 +1027,11 @@ const spec: any = {
10071027
security: [{ adminAuth: [] }],
10081028
responses: {
10091029
'200': { description: 'History list', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiEnvelope' } } } },
1010-
'401': { description: 'Unauthorized' },
1030+
'401': { description: 'Unauthorized', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
1031+
'403': { description: 'Forbidden', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
1032+
'405': { description: 'Method not allowed' },
10111033
'500': { description: 'Internal error', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
1034+
'503': { description: 'Admin auth not configured', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
10121035
},
10131036
},
10141037
},
@@ -1074,6 +1097,8 @@ const spec: any = {
10741097
responses: {
10751098
'200': { description: 'Preferences saved', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiEnvelope' } } } },
10761099
'400': { description: 'Validation error', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' }, examples: { ValidationError: { $ref: '#/components/examples/ValidationError' } } } } },
1100+
'401': { description: 'Unauthorized', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
1101+
'405': { description: 'Method not allowed' },
10771102
'500': { description: 'Internal error', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
10781103
},
10791104
},
@@ -1087,6 +1112,8 @@ const spec: any = {
10871112
responses: {
10881113
'200': { description: 'Preferences or null', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiEnvelope' } } } },
10891114
'400': { description: 'userId required', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' }, examples: { QueryValidationError: { $ref: '#/components/examples/QueryValidationError' } } } } },
1115+
'401': { description: 'Unauthorized', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
1116+
'405': { description: 'Method not allowed' },
10901117
'500': { description: 'Internal error', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
10911118
},
10921119
},
@@ -1100,6 +1127,8 @@ const spec: any = {
11001127
responses: {
11011128
'200': { description: 'Unsubscribed', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiEnvelope' } } } },
11021129
'400': { description: 'userId required', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' }, examples: { QueryValidationError: { $ref: '#/components/examples/QueryValidationError' } } } } },
1130+
'401': { description: 'Unauthorized', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
1131+
'405': { description: 'Method not allowed' },
11031132
'500': { description: 'Internal error', content: { 'application/json': { schema: { $ref: '#/components/schemas/ApiError' } } } },
11041133
},
11051134
},

security/npm-audit-baseline.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@
2929
"info": 0,
3030
"low": 0,
3131
"moderate": 1,
32-
"high": 3,
32+
"high": 5,
3333
"critical": 0,
34-
"total": 4
34+
"total": 6
3535
}
3636
}
3737
}

0 commit comments

Comments
 (0)