@@ -820,14 +820,84 @@ def generate_cdx_vex(advs, ov_map, advisory_id, timestamp):
820820
821821# --------------------------------------------------------------------------- #
822822
823+ # Overlay vocabulary, mirroring advisory-vex-overlay.schema.json. The state and
824+ # justification enums are the maps' own keys; the two below and the allowed-key
825+ # sets complete the schema. test_overlay_vocab_matches_schema guards drift.
826+ _OVERLAY_RESPONSES = {'can_not_fix' , 'will_not_fix' , 'update' , 'rollback' ,
827+ 'workaround_available' }
828+ _OVERLAY_DEFAULT_STATUS = {'on' , 'off' , 'enabled' , 'disabled' }
829+ _OVERLAY_ENTRY_KEYS = {'state' , 'justification' , 'response' , 'detail' ,
830+ 'fixed_versions' , 'remediation' , 'requires_defines' ,
831+ 'default_status' , 'fips' }
832+ _OVERLAY_FIPS_KEYS = {'name' , 'module_version' , 'cmvp_cert' , 'status' ,
833+ 'justification' , 'fixed_versions' , 'remediation' }
834+
835+
836+ def _check_analysis (cve , obj , allowed_keys , state_key , state_required , kind ):
837+ """Validate one overlay object (entry or its fips sub-object) against the
838+ schema invariants that, if violated, would silently degrade the VEX output
839+ -- above all a not_affected determination that omits its justification."""
840+ if not isinstance (obj , dict ):
841+ sys .exit (f"ERROR: overlay { cve } { kind } must be a JSON object" )
842+ unknown = set (obj ) - allowed_keys
843+ if unknown :
844+ sys .exit (f"ERROR: overlay { cve } { kind } has unknown key(s): "
845+ f"{ ', ' .join (sorted (unknown ))} " )
846+ state = obj .get (state_key )
847+ if state_required and state is None :
848+ sys .exit (f"ERROR: overlay { cve } { kind } is missing required "
849+ f"{ state_key !r} " )
850+ if state is not None and state not in _STATE_TO_BUCKET :
851+ sys .exit (f"ERROR: overlay { cve } { kind } { state_key } ={ state !r} is not a "
852+ f"valid state ({ ', ' .join (sorted (_STATE_TO_BUCKET ))} )" )
853+ just = obj .get ('justification' )
854+ if state == 'not_affected' and not just :
855+ sys .exit (f"ERROR: overlay { cve } { kind } has { state_key } =not_affected "
856+ f"but no 'justification' (required so a CSAF flag / CycloneDX "
857+ f"analysis.justification can be emitted)" )
858+ if just is not None and just not in _JUSTIFICATION_TO_CSAF_FLAG :
859+ sys .exit (f"ERROR: overlay { cve } { kind } justification={ just !r} is not "
860+ f"valid ({ ', ' .join (sorted (_JUSTIFICATION_TO_CSAF_FLAG ))} )" )
861+ resp = obj .get ('response' )
862+ if resp is not None and (not isinstance (resp , list )
863+ or any (r not in _OVERLAY_RESPONSES for r in resp )):
864+ sys .exit (f"ERROR: overlay { cve } { kind } response must be a list drawn "
865+ f"from { ', ' .join (sorted (_OVERLAY_RESPONSES ))} " )
866+ ds = obj .get ('default_status' )
867+ if ds is not None and ds not in _OVERLAY_DEFAULT_STATUS :
868+ sys .exit (f"ERROR: overlay { cve } { kind } default_status={ ds !r} is not "
869+ f"valid ({ ', ' .join (sorted (_OVERLAY_DEFAULT_STATUS ))} )" )
870+
871+
872+ def _validate_overlay (ov_map ):
873+ """Enforce the overlay schema's structural invariants at load time
874+ (stdlib-only; the authoritative jsonschema pass runs in CI). Without this a
875+ hand-edited overlay silently produces a wrong/omitted VEX justification."""
876+ if not isinstance (ov_map , dict ):
877+ sys .exit ("ERROR: --vex-overlay must be a JSON object keyed by CVE id" )
878+ for cve , entry in ov_map .items ():
879+ if cve == '_comment' :
880+ continue
881+ if not _CVE_ID_RE .match (cve ):
882+ sys .exit (f"ERROR: overlay key { cve !r} is not a CVE id "
883+ f"(expected { _CVE_ID_RE .pattern } )" )
884+ _check_analysis (cve , entry , _OVERLAY_ENTRY_KEYS , 'state' , True , 'entry' )
885+ fips = entry .get ('fips' )
886+ if fips is not None :
887+ _check_analysis (cve , fips , _OVERLAY_FIPS_KEYS , 'status' , False ,
888+ 'fips' )
889+
890+
823891def load_overlay (path ):
824892 if not path :
825893 return {}
826894 try :
827895 with open (path ) as f :
828- return json .load (f )
896+ data = json .load (f )
829897 except (OSError , json .JSONDecodeError ) as e :
830898 sys .exit (f"ERROR: cannot read --vex-overlay { path !r} : { e } " )
899+ _validate_overlay (data )
900+ return data
831901
832902
833903def _write_json (obj , path ):
0 commit comments