@@ -171,6 +171,263 @@ describe('StixBundleSchema', () => {
171171
172172 expect ( ( ) => stixBundleSchema . parse ( invalidFirstObjectBundle ) ) . toThrow ( ) ;
173173 } ) ;
174+
175+ describe ( 'Uniqueness Constraint' , ( ) => {
176+ it ( 'should accept bundle with unique object IDs (true positive)' , ( ) => {
177+ const technique1 : Technique = {
178+ id : `attack-pattern--${ uuidv4 ( ) } ` ,
179+ type : 'attack-pattern' ,
180+ spec_version : '2.1' ,
181+ created : '2021-01-01T00:00:00.000Z' as StixCreatedTimestamp ,
182+ modified : '2021-01-01T00:00:00.000Z' as StixModifiedTimestamp ,
183+ name : 'Test Technique 1' ,
184+ x_mitre_attack_spec_version : '2.1.0' ,
185+ x_mitre_version : '1.0' ,
186+ x_mitre_domains : [ 'enterprise-attack' ] ,
187+ x_mitre_is_subtechnique : false ,
188+ external_references : [
189+ {
190+ source_name : 'mitre-attack' ,
191+ external_id : 'T1001' ,
192+ } ,
193+ ] ,
194+ } ;
195+
196+ const technique2 : Technique = {
197+ id : `attack-pattern--${ uuidv4 ( ) } ` ,
198+ type : 'attack-pattern' ,
199+ spec_version : '2.1' ,
200+ created : '2021-01-01T00:00:00.000Z' as StixCreatedTimestamp ,
201+ modified : '2021-01-01T00:00:00.000Z' as StixModifiedTimestamp ,
202+ name : 'Test Technique 2' ,
203+ x_mitre_attack_spec_version : '2.1.0' ,
204+ x_mitre_version : '1.0' ,
205+ x_mitre_domains : [ 'enterprise-attack' ] ,
206+ x_mitre_is_subtechnique : false ,
207+ external_references : [
208+ {
209+ source_name : 'mitre-attack' ,
210+ external_id : 'T1002' ,
211+ } ,
212+ ] ,
213+ } ;
214+
215+ const bundleWithUniqueObjects = {
216+ ...minimalBundle ,
217+ objects : [ minimalCollection , technique1 , technique2 ] ,
218+ } ;
219+
220+ expect ( ( ) => stixBundleSchema . parse ( bundleWithUniqueObjects ) ) . not . toThrow ( ) ;
221+ } ) ;
222+
223+ it ( 'should reject bundle with duplicate object IDs (true negative)' , ( ) => {
224+ const duplicateId = `attack-pattern--${ uuidv4 ( ) } ` ;
225+
226+ const technique1 : Technique = {
227+ id : duplicateId ,
228+ type : 'attack-pattern' ,
229+ spec_version : '2.1' ,
230+ created : '2021-01-01T00:00:00.000Z' as StixCreatedTimestamp ,
231+ modified : '2021-01-01T00:00:00.000Z' as StixModifiedTimestamp ,
232+ name : 'Test Technique 1' ,
233+ x_mitre_attack_spec_version : '2.1.0' ,
234+ x_mitre_version : '1.0' ,
235+ x_mitre_domains : [ 'enterprise-attack' ] ,
236+ x_mitre_is_subtechnique : false ,
237+ external_references : [
238+ {
239+ source_name : 'mitre-attack' ,
240+ external_id : 'T1001' ,
241+ } ,
242+ ] ,
243+ } ;
244+
245+ const technique2 : Technique = {
246+ id : duplicateId , // Same ID as technique1
247+ type : 'attack-pattern' ,
248+ spec_version : '2.1' ,
249+ created : '2021-01-01T00:00:00.000Z' as StixCreatedTimestamp ,
250+ modified : '2021-01-01T00:00:00.000Z' as StixModifiedTimestamp ,
251+ name : 'Test Technique 2' ,
252+ x_mitre_attack_spec_version : '2.1.0' ,
253+ x_mitre_version : '1.0' ,
254+ x_mitre_domains : [ 'enterprise-attack' ] ,
255+ x_mitre_is_subtechnique : false ,
256+ external_references : [
257+ {
258+ source_name : 'mitre-attack' ,
259+ external_id : 'T1002' ,
260+ } ,
261+ ] ,
262+ } ;
263+
264+ const bundleWithDuplicateObjects = {
265+ ...minimalBundle ,
266+ objects : [ minimalCollection , technique1 , technique2 ] ,
267+ } ;
268+
269+ expect ( ( ) => stixBundleSchema . parse ( bundleWithDuplicateObjects ) ) . toThrow (
270+ / D u p l i c a t e o b j e c t w i t h i d / ,
271+ ) ;
272+ } ) ;
273+
274+ it ( 'should report the duplicate ID in error message' , ( ) => {
275+ const duplicateId = `attack-pattern--${ uuidv4 ( ) } ` ;
276+
277+ const technique1 : Technique = {
278+ id : duplicateId ,
279+ type : 'attack-pattern' ,
280+ spec_version : '2.1' ,
281+ created : '2021-01-01T00:00:00.000Z' as StixCreatedTimestamp ,
282+ modified : '2021-01-01T00:00:00.000Z' as StixModifiedTimestamp ,
283+ name : 'Test Technique 1' ,
284+ x_mitre_attack_spec_version : '2.1.0' ,
285+ x_mitre_version : '1.0' ,
286+ x_mitre_domains : [ 'enterprise-attack' ] ,
287+ x_mitre_is_subtechnique : false ,
288+ external_references : [
289+ {
290+ source_name : 'mitre-attack' ,
291+ external_id : 'T1001' ,
292+ } ,
293+ ] ,
294+ } ;
295+
296+ const technique2 : Technique = {
297+ id : duplicateId ,
298+ type : 'attack-pattern' ,
299+ spec_version : '2.1' ,
300+ created : '2021-01-01T00:00:00.000Z' as StixCreatedTimestamp ,
301+ modified : '2021-01-01T00:00:00.000Z' as StixModifiedTimestamp ,
302+ name : 'Test Technique 2' ,
303+ x_mitre_attack_spec_version : '2.1.0' ,
304+ x_mitre_version : '1.0' ,
305+ x_mitre_domains : [ 'enterprise-attack' ] ,
306+ x_mitre_is_subtechnique : false ,
307+ external_references : [
308+ {
309+ source_name : 'mitre-attack' ,
310+ external_id : 'T1002' ,
311+ } ,
312+ ] ,
313+ } ;
314+
315+ const bundleWithDuplicateObjects = {
316+ ...minimalBundle ,
317+ objects : [ minimalCollection , technique1 , technique2 ] ,
318+ } ;
319+
320+ try {
321+ stixBundleSchema . parse ( bundleWithDuplicateObjects ) ;
322+ expect . fail ( 'Expected schema to throw for duplicate IDs' ) ;
323+ } catch ( error ) {
324+ if ( error instanceof z . ZodError ) {
325+ const errorMessage = error . issues [ 0 ] . message ;
326+ expect ( errorMessage ) . toContain ( duplicateId ) ;
327+ } else {
328+ throw error ;
329+ }
330+ }
331+ } ) ;
332+
333+ it ( 'should handle multiple duplicates in a single bundle' , ( ) => {
334+ const duplicateId1 = `attack-pattern--${ uuidv4 ( ) } ` ;
335+ const duplicateId2 = `attack-pattern--${ uuidv4 ( ) } ` ;
336+
337+ const technique1 : Technique = {
338+ id : duplicateId1 ,
339+ type : 'attack-pattern' ,
340+ spec_version : '2.1' ,
341+ created : '2021-01-01T00:00:00.000Z' as StixCreatedTimestamp ,
342+ modified : '2021-01-01T00:00:00.000Z' as StixModifiedTimestamp ,
343+ name : 'Test Technique 1' ,
344+ x_mitre_attack_spec_version : '2.1.0' ,
345+ x_mitre_version : '1.0' ,
346+ x_mitre_domains : [ 'enterprise-attack' ] ,
347+ x_mitre_is_subtechnique : false ,
348+ external_references : [
349+ {
350+ source_name : 'mitre-attack' ,
351+ external_id : 'T1001' ,
352+ } ,
353+ ] ,
354+ } ;
355+
356+ const technique2 : Technique = {
357+ id : duplicateId1 , // Duplicate of technique1
358+ type : 'attack-pattern' ,
359+ spec_version : '2.1' ,
360+ created : '2021-01-01T00:00:00.000Z' as StixCreatedTimestamp ,
361+ modified : '2021-01-01T00:00:00.000Z' as StixModifiedTimestamp ,
362+ name : 'Test Technique 2' ,
363+ x_mitre_attack_spec_version : '2.1.0' ,
364+ x_mitre_version : '1.0' ,
365+ x_mitre_domains : [ 'enterprise-attack' ] ,
366+ x_mitre_is_subtechnique : false ,
367+ external_references : [
368+ {
369+ source_name : 'mitre-attack' ,
370+ external_id : 'T1002' ,
371+ } ,
372+ ] ,
373+ } ;
374+
375+ const technique3 : Technique = {
376+ id : duplicateId2 ,
377+ type : 'attack-pattern' ,
378+ spec_version : '2.1' ,
379+ created : '2021-01-01T00:00:00.000Z' as StixCreatedTimestamp ,
380+ modified : '2021-01-01T00:00:00.000Z' as StixModifiedTimestamp ,
381+ name : 'Test Technique 3' ,
382+ x_mitre_attack_spec_version : '2.1.0' ,
383+ x_mitre_version : '1.0' ,
384+ x_mitre_domains : [ 'enterprise-attack' ] ,
385+ x_mitre_is_subtechnique : false ,
386+ external_references : [
387+ {
388+ source_name : 'mitre-attack' ,
389+ external_id : 'T1003' ,
390+ } ,
391+ ] ,
392+ } ;
393+
394+ const technique4 : Technique = {
395+ id : duplicateId2 , // Duplicate of technique3
396+ type : 'attack-pattern' ,
397+ spec_version : '2.1' ,
398+ created : '2021-01-01T00:00:00.000Z' as StixCreatedTimestamp ,
399+ modified : '2021-01-01T00:00:00.000Z' as StixModifiedTimestamp ,
400+ name : 'Test Technique 4' ,
401+ x_mitre_attack_spec_version : '2.1.0' ,
402+ x_mitre_version : '1.0' ,
403+ x_mitre_domains : [ 'enterprise-attack' ] ,
404+ x_mitre_is_subtechnique : false ,
405+ external_references : [
406+ {
407+ source_name : 'mitre-attack' ,
408+ external_id : 'T1004' ,
409+ } ,
410+ ] ,
411+ } ;
412+
413+ const bundleWithMultipleDuplicates = {
414+ ...minimalBundle ,
415+ objects : [ minimalCollection , technique1 , technique2 , technique3 , technique4 ] ,
416+ } ;
417+
418+ try {
419+ stixBundleSchema . parse ( bundleWithMultipleDuplicates ) ;
420+ expect . fail ( 'Expected schema to throw for multiple duplicate IDs' ) ;
421+ } catch ( error ) {
422+ if ( error instanceof z . ZodError ) {
423+ // Should have at least 2 errors (one for each duplicate pair)
424+ expect ( error . issues . length ) . toBeGreaterThanOrEqual ( 2 ) ;
425+ } else {
426+ throw error ;
427+ }
428+ }
429+ } ) ;
430+ } ) ;
174431 } ) ;
175432
176433 // GitHub Actions often fails without an increased timeout for this test
0 commit comments