@@ -4,11 +4,12 @@ const { z, ZodError } = require('zod');
44const { StatusCodes } = require ( 'http-status-codes' ) ;
55const logger = require ( '../lib/logger' ) ;
66const { processValidationIssues } = require ( '../services/system/validate-service' ) ;
7+ const { getSchema } = require ( '../services/system/validate-service' ) ;
78const {
89 createAttackIdSchema,
910 stixTypeToAttackIdMapping,
1011} = require ( '@mitre-attack/attack-data-model/dist/schemas/common/property-schemas/attack-id' ) ;
11-
12+ const { BadRequestError } = require ( '../exceptions' ) ;
1213/**
1314 * Basic workspace schema (without rigid attack ID validation)
1415 * @type {z.ZodObject }
@@ -57,111 +58,15 @@ function createWorkspaceSchema(stixType) {
5758 return workspaceSchema ;
5859}
5960
60- function extractStringLiteralFromStixTypeZodSchema ( zodSchema ) {
61- // Method 1: Direct shape access (works for most schemas)
62- if ( zodSchema . shape ?. type ?. def ?. values ?. [ 0 ] ) {
63- return zodSchema . shape . type . def . values [ 0 ] ;
64- }
65- // Method 2: Through _zod.def.in.def (works for schemas with .transform())
66- else if ( zodSchema . _zod ?. def ?. in ?. def ?. shape ?. type ?. def ?. values ?. [ 0 ] ) {
67- return zodSchema . _zod . def . in . def . shape . type . def . values [ 0 ] ;
68- }
69- // Method 3: Works for schemas that support multiple types, i.e., softwareSchema -> [tool, malware]
70- else if ( zodSchema . shape ?. type . def . options ) {
71- const stixTypes = [ ] ;
72- for ( const opt of zodSchema . shape . type . def . options ) {
73- stixTypes . push ( opt . def . values [ 0 ] ) ;
74- }
75- return stixTypes ;
76- } else {
77- throw new Error ( 'Could not extract STIX type from schema' ) ;
78- }
79- }
80-
81- /**
82- * Factory function that creates a combined workspace+STIX schema with conditional partial validation
83- * @param {z.ZodObject } stixSchema - The STIX object schema to validate against
84- * @param {string } workflowState - The workflow state to determine validation strictness
85- * @param {string[] } omitStixFields - Array of STIX field names to omit from validation
86- * @returns {z.ZodObject } Combined schema with workspace and conditional stix validation
87- */
8861/**
89- * Factory function that creates a combined workspace+STIX schema with conditional partial validation
90- * @param {z.ZodObject } stixSchema - The STIX object schema to validate against
91- * @param {string } workflowState - The workflow state to determine validation strictness
92- * @param {string[] } omitStixFields - Array of STIX field names to omit from validation
93- * @returns {z.ZodObject } Combined schema with workspace and conditional stix validation
94- */
95- function createWorkspaceStixSchema (
96- stixSchema ,
97- workflowState ,
98- omitStixFields = [ 'x_mitre_attack_spec_version' , 'external_references' ] ,
99- ) {
100- logger . debug ( 'Creating combined workspace+STIX schema:' , { workflowState, omitStixFields } ) ;
101-
102- try {
103- // Extract the STIX type from the schema with fallback for transformed schemas
104- const stixTypeStringLiteral = extractStringLiteralFromStixTypeZodSchema ( stixSchema ) ;
105-
106- logger . debug ( 'Extracted STIX type from schema:' , { stixTypeStringLiteral } ) ;
107-
108- // Apply partial validation for work-in-progress, full validation otherwise
109- const usePartialValidation = workflowState === 'work-in-progress' ;
110- logger . debug ( 'Validation mode:' , { workflowState, usePartialValidation } ) ;
111-
112- let stixValidationSchema = usePartialValidation ? stixSchema . partial ( ) : stixSchema ;
113-
114- // Build omit object from array of field names
115- if ( omitStixFields . length > 0 ) {
116- const omitObject = omitStixFields . reduce ( ( acc , field ) => {
117- acc [ field ] = true ;
118- return acc ;
119- } , { } ) ;
120- stixValidationSchema = stixValidationSchema . omit ( omitObject ) ;
121- }
122-
123- const combinedSchema = z . object ( {
124- workspace : createWorkspaceSchema ( stixTypeStringLiteral ) ,
125- stix : stixValidationSchema ,
126- } ) ;
127-
128- logger . debug ( 'Successfully created combined schema' ) ;
129- return combinedSchema ;
130- } catch ( error ) {
131- logger . warn ( 'Could not extract STIX type from schema, using basic validation:' , {
132- error : error . message ,
133- workflowState,
134- omitStixFields,
135- } ) ;
136-
137- let stixValidationSchema =
138- workflowState === 'work-in-progress' ? stixSchema . partial ( ) : stixSchema ;
139-
140- // Apply omit in error case as well
141- if ( omitStixFields . length > 0 ) {
142- const omitObject = omitStixFields . reduce ( ( acc , field ) => {
143- acc [ field ] = true ;
144- return acc ;
145- } , { } ) ;
146- stixValidationSchema = stixValidationSchema . omit ( omitObject ) ;
147- }
148-
149- return z . object ( {
150- workspace : workspaceSchema ,
151- stix : stixValidationSchema ,
152- } ) ;
153- }
154- }
155-
156- /**
157- * Middleware for parsing the request body using a specified STIX schema from the ATT&CK Data Model.
158- * Both the `workspace` and `stix` keys are checked.
159- * @param {z.ZodObject|z.ZodObject[] } oneOrMoreZodSchemas - Single schema or array of schemas to validate against
62+ * Middleware for validating the request body against a pre-composed STIX schema.
63+ * Wraps the STIX schema with a workspace schema and parses the request body.
64+ * @param {z.ZodObject } stixSchema - Pre-composed STIX schema (with omit/partial/checks already applied)
16065 * @param {Object } options - Configuration options
16166 * @param {boolean } options.enabled - Whether validation is enabled (defaults to true)
16267 * @returns {Function } Express middleware function
16368 */
164- function middleware ( oneOrMoreZodSchemas , options = { } ) {
69+ function middleware ( stixSchema , options = { } ) {
16570 const { enabled = true } = options ;
16671
16772 return ( req , res , next ) => {
@@ -181,59 +86,13 @@ function middleware(oneOrMoreZodSchemas, options = {}) {
18186 } ) ;
18287
18388 try {
184- // Extract workflow state from request body
185- const workflowState = req . body ?. workspace ?. workflow ?. state || 'reviewed' ; // Default to strict validation
186- logger . debug ( 'Determined workflow state:' , {
187- workflowState,
188- isDefault : ! req . body ?. workspace ?. workflow ?. state ,
189- } ) ;
190-
191- // Determine which schema to use based on request STIX type
192- const requestStixType = req . body ?. stix ?. type ;
193- logger . debug ( 'Request STIX type:' , { requestStixType } ) ;
194-
195- let finalSchema ;
196-
197- // Handle array of schemas - find the one that matches the request type
198- if ( Array . isArray ( oneOrMoreZodSchemas ) ) {
199- logger . debug ( 'Multiple schemas provided, finding matching schema for request type' ) ;
200-
201- for ( const schema of oneOrMoreZodSchemas ) {
202- try {
203- const schemaStixType = extractStringLiteralFromStixTypeZodSchema ( schema ) ;
204- logger . debug ( 'Checking schema with type:' , { schemaStixType } ) ;
205-
206- // Check if this schema matches the request type
207- if (
208- ( typeof schemaStixType === 'string' && schemaStixType === requestStixType ) ||
209- ( Array . isArray ( schemaStixType ) && schemaStixType . includes ( requestStixType ) )
210- ) {
211- logger . debug ( 'Found matching schema for request type:' , {
212- requestStixType,
213- schemaStixType,
214- } ) ;
215- finalSchema = schema ;
216- break ;
217- }
218- } catch ( error ) {
219- logger . debug ( 'Could not extract type from schema, skipping:' , { error : error . message } ) ;
220- continue ;
221- }
222- }
89+ const stixType = req . body ?. stix ?. type ;
22390
224- if ( ! finalSchema ) {
225- throw new Error (
226- `No matching schema found for STIX type: ${ requestStixType } . Available schemas: ${ oneOrMoreZodSchemas . length } ` ,
227- ) ;
228- }
229- } else {
230- // Single schema - use it directly
231- logger . debug ( 'Single schema provided, using directly' ) ;
232- finalSchema = oneOrMoreZodSchemas ;
233- }
234-
235- // Create schema with conditional validation based on workflow state
236- const combinedSchema = createWorkspaceStixSchema ( finalSchema , workflowState ) ;
91+ // Wrap the pre-composed STIX schema with the workspace schema
92+ const combinedSchema = z . object ( {
93+ workspace : createWorkspaceSchema ( stixType ) ,
94+ stix : stixSchema ,
95+ } ) ;
23796
23897 logger . debug ( 'Attempting to parse request body with combined schema' ) ;
23998 combinedSchema . parse ( req . body ) ;
@@ -303,22 +162,47 @@ function middleware(oneOrMoreZodSchemas, options = {}) {
303162/**
304163 * Pre-configured validation middleware factory that uses runtime configuration.
305164 * The middleware reads the config value at request time to support dynamic config changes (e.g., during tests).
165+ *
166+ * @param {string|string[] } expectedStixType - The STIX type(s) this endpoint accepts
167+ * (e.g. "attack-pattern" or ["tool", "malware"] for software)
168+ * @returns {Function } Express middleware function
306169 */
307- function validateWorkspaceStixData ( oneOrMoreZodSchemas ) {
170+ function validateWorkspaceStixData ( expectedStixType ) {
171+ const allowedTypes = Array . isArray ( expectedStixType ) ? expectedStixType : [ expectedStixType ] ;
172+
308173 return ( req , res , next ) => {
309174 // Read config at request time to allow dynamic changes
310175 const config = require ( '../config/config' ) ;
311176 const enabled = config . validateRequests . withAttackDataModel ;
312- const middlewareFn = middleware ( oneOrMoreZodSchemas , { enabled } ) ;
177+ const requestStixType = req . body ?. stix ?. type ;
178+ const workflowState = req . body ?. workspace ?. workflow ?. state || 'reviewed' ;
179+
180+ // Verify the request's STIX type is one this endpoint accepts
181+ if ( ! allowedTypes . includes ( requestStixType ) ) {
182+ return next (
183+ new BadRequestError (
184+ `Unexpected STIX type "${ requestStixType } ". This endpoint accepts: ${ allowedTypes . join ( ', ' ) } ` ,
185+ ) ,
186+ ) ;
187+ }
188+
189+ const finalSchema = getSchema ( requestStixType , workflowState ) ;
190+ if ( ! finalSchema ) {
191+ return next (
192+ new BadRequestError (
193+ `No schema found for STIX type "${ requestStixType } ". Request body is probably invalid.` ,
194+ ) ,
195+ ) ;
196+ }
197+
198+ const middlewareFn = middleware ( finalSchema , { enabled } ) ;
313199 return middlewareFn ( req , res , next ) ;
314200 } ;
315201}
316202
317203module . exports = {
318204 /** Express middleware factory for workspace+STIX validation */
319205 validateWorkspaceStixData,
320- /** Factory function for creating combined workspace+STIX schemas */
321- createWorkspaceStixSchema,
322206 /** Basic workspace schema without dynamic attackId validation */
323207 workspaceSchema,
324208} ;
0 commit comments