@@ -62,6 +62,47 @@ interface KevEnvelope {
6262 vulnerabilities : KevVulnerability [ ] ;
6363}
6464
65+ const isRecord = ( value : unknown ) : value is Record < string , unknown > =>
66+ typeof value === 'object' && value !== null && ! Array . isArray ( value ) ;
67+
68+ const readOptionalStringArray = ( value : unknown ) : string [ ] | undefined => {
69+ if ( value === undefined ) return undefined ;
70+ if ( ! Array . isArray ( value ) ) return undefined ;
71+ return value . every ( ( entry ) => typeof entry === 'string' ) ? value : undefined ;
72+ } ;
73+
74+ const parseKevVulnerability = ( value : unknown ) : KevVulnerability | undefined => {
75+ if ( ! isRecord ( value ) ) return undefined ;
76+ const cwes = readOptionalStringArray ( value . cwes ) ;
77+ if ( value . cwes !== undefined && cwes === undefined ) return undefined ;
78+ const vuln = {
79+ cveID : value . cveID ,
80+ vendorProject : value . vendorProject ,
81+ product : value . product ,
82+ vulnerabilityName : value . vulnerabilityName ,
83+ dateAdded : value . dateAdded ,
84+ shortDescription : value . shortDescription ,
85+ requiredAction : value . requiredAction ,
86+ dueDate : value . dueDate ,
87+ knownRansomwareCampaignUse : value . knownRansomwareCampaignUse ,
88+ notes : value . notes ,
89+ cwes,
90+ } ;
91+ if (
92+ typeof vuln . cveID !== 'string' ||
93+ typeof vuln . vendorProject !== 'string' ||
94+ typeof vuln . product !== 'string' ||
95+ typeof vuln . vulnerabilityName !== 'string' ||
96+ typeof vuln . dateAdded !== 'string' ||
97+ typeof vuln . shortDescription !== 'string' ||
98+ typeof vuln . requiredAction !== 'string' ||
99+ typeof vuln . dueDate !== 'string'
100+ ) {
101+ return undefined ;
102+ }
103+ return vuln as KevVulnerability ;
104+ } ;
105+
65106const readFeedUrl = ( source : SourceHit ) : string => {
66107 const url = source . _source . config . url ;
67108 return typeof url === 'string' && url . length > 0 ? url : KEV_FEED_URL ;
@@ -159,7 +200,11 @@ export const kevAdapter: FetchAdapter = {
159200
160201 let envelope : KevEnvelope ;
161202 try {
162- envelope = JSON . parse ( response . body ) as KevEnvelope ;
203+ const parsed = JSON . parse ( response . body ) as unknown ;
204+ if ( ! isRecord ( parsed ) || ! Array . isArray ( parsed . vulnerabilities ) ) {
205+ throw new Error ( 'KEV feed missing vulnerabilities array' ) ;
206+ }
207+ envelope = parsed as unknown as KevEnvelope ;
163208 } catch ( err ) {
164209 throw new Error ( `KEV feed response is not valid JSON: ${ ( err as Error ) . message } ` ) ;
165210 }
@@ -172,13 +217,14 @@ export const kevAdapter: FetchAdapter = {
172217 }
173218
174219 const reports : NormalizedReport [ ] = [ ] ;
175- for ( const vuln of vulnerabilities ) {
176- if ( isCompleteKevEntry ( vuln ) ) {
220+ for ( const rawEntry of vulnerabilities ) {
221+ const vuln = parseKevVulnerability ( rawEntry ) ;
222+ if ( vuln && isCompleteKevEntry ( vuln ) ) {
177223 reports . push ( buildKevReport ( vuln , provenanceUrl , ingestedAt , spaceId , source . _id ) ) ;
178224 } else {
179225 log . warn (
180226 `kev-adapter: skipping malformed entry (missing required fields): ${ JSON . stringify (
181- vuln
227+ rawEntry
182228 ) . slice ( 0 , 200 ) } `
183229 ) ;
184230 }
0 commit comments