From 7a70ae619d3f1f28474fd842598d2e4255b7e253 Mon Sep 17 00:00:00 2001 From: Amelia Vance Date: Tue, 24 Oct 2023 16:04:42 -0400 Subject: [PATCH 01/50] Rename current Vulnerability page to make room for new one --- .../pages/Vulnerability/Vulnerability_old.tsx | 555 ++++++++++++++++++ 1 file changed, 555 insertions(+) create mode 100644 frontend/src/pages/Vulnerability/Vulnerability_old.tsx diff --git a/frontend/src/pages/Vulnerability/Vulnerability_old.tsx b/frontend/src/pages/Vulnerability/Vulnerability_old.tsx new file mode 100644 index 000000000..0abbbe776 --- /dev/null +++ b/frontend/src/pages/Vulnerability/Vulnerability_old.tsx @@ -0,0 +1,555 @@ +import { + Paper, + TableContainer, + TextareaAutosize, + Chip, + MenuItem, + Button, + Menu +} from '@mui/material'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableRow +} from '@mui/material'; +import { classes, Root } from './vulnerabilityStyle'; +import { Flag, ChevronLeft, ArrowDropDown } from '@mui/icons-material'; +import { useAuthContext } from 'context'; +import { stateMap } from 'pages/Vulnerabilities/Vulnerabilities'; +import React, { useCallback, useEffect, useState } from 'react'; +import ReactMarkdown from 'react-markdown'; +import { Link, useParams, useHistory } from 'react-router-dom'; +import { Vulnerability as VulnerabilityType } from 'types'; +// @ts-ignore:next-line +import { formatDistanceToNow, parseISO, format } from 'date-fns'; +import { + Timeline, + TimelineItem, + TimelineSeparator, + TimelineDot, + TimelineConnector, + TimelineContent +} from '@mui/lab'; + +export const Vulnerability: React.FC = () => { + const { vulnerabilityId } = useParams(); + const { apiGet, apiPut } = useAuthContext(); + const [vulnerability, setVulnerability] = useState(); + const [comment, setComment] = useState(''); + const [showCommentForm, setShowCommentForm] = useState(false); + const [menuAnchor, setMenuAnchor] = React.useState(null); + + const history = useHistory(); + + const formatDate = (date: string) => { + return format(parseISO(date), 'MM-dd-yyyy'); + }; + + const fetchVulnerability = useCallback(async () => { + try { + const result = await apiGet( + `/vulnerabilities/${vulnerabilityId}` + ); + setVulnerability(result); + } catch (e) { + console.error(e); + } + }, [vulnerabilityId, apiGet]); + + const updateVulnerability = async (body: { [key: string]: string }) => { + try { + if (!vulnerability) return; + const res = await apiPut( + '/vulnerabilities/' + vulnerability.id, + { + body: body + } + ); + setVulnerability({ + ...vulnerability, + state: res.state, + substate: res.substate, + actions: res.actions + }); + } catch (e) { + console.error(e); + } + }; + + useEffect(() => { + fetchVulnerability(); + }, [fetchVulnerability]); + + if (!vulnerability) return ; + + const references = vulnerability.references.map((ref) => ref); + if (vulnerability.cve) + references.unshift({ + name: 'NIST National Vulnerability Database', + url: `https://nvd.nist.gov/vuln/detail/${vulnerability.cve}`, + source: '', + tags: [] + }); + + const states = [ + 'unconfirmed', + 'exploitable', + 'false-positive', + 'accepted-risk', + 'remediated' + ]; + interface dnstwist { + domain: string; + fuzzer: string; + dns_a?: string; + dns_aaaa?: string; + dns_mx?: string; + dns_ns?: string; + date_first_observed?: string; + } + + return ( + <> + {/* + This vulnerability is found on 17 domains you have access to. + */} + +

+ history.goBack()} + className={classes.backLink} + > + + Go back + +

+ +
+
+
+ +
+

{vulnerability.title}

+ + setMenuAnchor(null)} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'center' + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'center' + }} + > + {states.map((state) => ( + { + updateVulnerability({ + substate: state + }); + setMenuAnchor(null); + }} + style={{ outline: 'none' }} + > + {stateMap[state]} + + ))} + +
+ } + label={`${vulnerability.state[0].toUpperCase()}${vulnerability.state.slice( + 1 + )} (${stateMap[vulnerability.substate]})`} + color={ + vulnerability.state === 'open' ? 'secondary' : 'default' + } + /> +
+
+

Description

+ {vulnerability.description} +
+
+

References

+ {references && + references.map((ref, index) => ( +

+ + {ref.name ? ref.name : ref.url} + + {ref.tags.length > 0 + ? ' - ' + ref.tags.join(',') + : ''} +

+ ))} +
+ {vulnerability.source === 'hibp' && ( +
+

Data

+ + + + Exposed Emails + Breaches + + + + {Object.keys( + vulnerability.structuredData['emails'] + ).map((keyName, keyIndex) => ( + + + {keyName} + + + {vulnerability.structuredData['emails'][ + keyName + ].join(', ')} + + + ))} + +
+
+ )} + {vulnerability.source === 'lookingGlass' && ( +
+

Data

+ + + + First Seen + Last Seen + Vuln Name + Type + + + + {vulnerability.structuredData['lookingGlassData'].map( + (col: any) => ( + + + {formatDistanceToNow( + parseISO(col.firstSeen) + ) + ' ago'} + + + {formatDistanceToNow(parseISO(col.lastSeen)) + + ' ago'} + + + {col.right_name} + + + {col.vulnOrMal} + + + ) + )} + +
+
+ )} + {vulnerability.source === 'dnstwist' && ( +
+

Data

+ + + + + Domain Name + IP Address / A Record + MX Record + NS Record + Date Observed + Fuzzer + + + + {vulnerability.structuredData['domains'].map( + (dom: dnstwist) => ( + + + {dom['domain']} + + {dom['dns_a']} + {dom['dns_mx']} + {dom['dns_ns']} + + {dom['date_first_observed']} + + {dom['fuzzer']} + + ) + )} + +
+
+
+ )} +
+
+
+
+ +
+
+

Team notes

+ +
+ {showCommentForm && ( +
+ setComment(e.target.value)} + /> + +
+ )} + {vulnerability.actions && + vulnerability.actions + .filter((action) => action.type === 'comment') + .map((action, index) => ( +
+

+ {action.userName} +

+ + {formatDistanceToNow(parseISO(action.date))} ago + + + {action.value || ''} + +
+ ))} +
+
+ +
+
+

Vulnerability History

+
+ + {vulnerability.actions && + vulnerability.actions + .filter( + (action) => + action.type === 'state-change' && action.substate + ) + .map((action, index) => ( + + + + + {' '} + + State {action.automatic ? 'automatically ' : ''} + changed to {action.state} ( + {stateMap[action.substate!].toLowerCase()}) + {action.userName ? ' by ' + action.userName : ''}{' '} +

+ + {formatDate(action.date)} + +
+
+ ))} + + + + + + + Vulnerability opened

+ + {formatDate(vulnerability.createdAt)} + +
+
+
+
+
+ +
+
+

Provenance

+

+ Root Domain: + {vulnerability.domain.fromRootDomain} +

+

+ Subdomain: + {vulnerability.domain.name} ( + {vulnerability.domain.subdomainSource}) +

+ {vulnerability.service && ( +

+ Service/Port: + {vulnerability.service.service + ? vulnerability.service.service + : vulnerability.service.port}{' '} + ({vulnerability.service.serviceSource}) +

+ )} + {vulnerability.cpe && ( + <> +

+ Product: + {vulnerability.cpe} +

+ + )} +

+ Vulnerability: + {vulnerability.title} ({vulnerability.source}) +

+
+
+
+ {vulnerability.source === 'hibp' && ( + +
+
+

Breaches

+ + + + Breach Name + Date Added + + + + {Object.keys(vulnerability.structuredData['breaches']) + .sort( + (a, b) => + parseISO( + vulnerability.structuredData['breaches'][b][ + 'AddedDate' + ] + ).getTime() - + parseISO( + vulnerability.structuredData['breaches'][a][ + 'AddedDate' + ] + ).getTime() + ) + .map((keyName, keyIndex) => ( + + + {keyName} + + + {formatDistanceToNow( + parseISO( + vulnerability.structuredData['breaches'][ + keyName + ]['AddedDate'] + ) + ) + ' ago'} + + + ))} + +
+
+
+
+ )} +
+
+
+
+ + ); +}; From d3ecd73a4c43e0f121c6c19b340bf784b899739d Mon Sep 17 00:00:00 2001 From: Amelia Vance Date: Tue, 24 Oct 2023 16:05:50 -0400 Subject: [PATCH 02/50] Add new Vulnerability.tsx with start of redesign --- .../src/pages/Vulnerability/Vulnerability.tsx | 741 ++++++------------ 1 file changed, 255 insertions(+), 486 deletions(-) diff --git a/frontend/src/pages/Vulnerability/Vulnerability.tsx b/frontend/src/pages/Vulnerability/Vulnerability.tsx index 0abbbe776..10078c643 100644 --- a/frontend/src/pages/Vulnerability/Vulnerability.tsx +++ b/frontend/src/pages/Vulnerability/Vulnerability.tsx @@ -1,52 +1,37 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { Link, useParams, useHistory } from 'react-router-dom'; + +// material-ui +import { ChevronLeft, OpenInNew } from '@mui/icons-material'; import { - Paper, - TableContainer, - TextareaAutosize, - Chip, - MenuItem, + AppBar, + Box, Button, - Menu -} from '@mui/material'; -import { + Grid, + IconButton, + Link as LinkMui, + Paper, Table, TableBody, TableCell, + TableContainer, TableHead, - TableRow + TableRow, + Toolbar, + Typography } from '@mui/material'; -import { classes, Root } from './vulnerabilityStyle'; -import { Flag, ChevronLeft, ArrowDropDown } from '@mui/icons-material'; + +import { differenceInCalendarDays, parseISO } from 'date-fns'; +import { getSeverityColor } from 'pages/Risk/utils'; import { useAuthContext } from 'context'; -import { stateMap } from 'pages/Vulnerabilities/Vulnerabilities'; -import React, { useCallback, useEffect, useState } from 'react'; -import ReactMarkdown from 'react-markdown'; -import { Link, useParams, useHistory } from 'react-router-dom'; import { Vulnerability as VulnerabilityType } from 'types'; -// @ts-ignore:next-line -import { formatDistanceToNow, parseISO, format } from 'date-fns'; -import { - Timeline, - TimelineItem, - TimelineSeparator, - TimelineDot, - TimelineConnector, - TimelineContent -} from '@mui/lab'; export const Vulnerability: React.FC = () => { const { vulnerabilityId } = useParams(); const { apiGet, apiPut } = useAuthContext(); const [vulnerability, setVulnerability] = useState(); - const [comment, setComment] = useState(''); - const [showCommentForm, setShowCommentForm] = useState(false); - const [menuAnchor, setMenuAnchor] = React.useState(null); const history = useHistory(); - - const formatDate = (date: string) => { - return format(parseISO(date), 'MM-dd-yyyy'); - }; - const fetchVulnerability = useCallback(async () => { try { const result = await apiGet( @@ -82,7 +67,7 @@ export const Vulnerability: React.FC = () => { fetchVulnerability(); }, [fetchVulnerability]); - if (!vulnerability) return ; + if (!vulnerability) return 'No Vulnerabilities'; const references = vulnerability.references.map((ref) => ref); if (vulnerability.cve) @@ -93,463 +78,247 @@ export const Vulnerability: React.FC = () => { tags: [] }); - const states = [ - 'unconfirmed', - 'exploitable', - 'false-positive', - 'accepted-risk', - 'remediated' - ]; - interface dnstwist { - domain: string; - fuzzer: string; - dns_a?: string; - dns_aaaa?: string; - dns_mx?: string; - dns_ns?: string; - date_first_observed?: string; + const product = + vulnerability.service && + vulnerability.service.products.find( + (product) => vulnerability.cpe && product.cpe && vulnerability.cpe.includes(product.cpe) + ); + // Calculates the total number of days a vulnerability has been open + let daysOpen = 0; + let lastOpenDate = vulnerability.createdAt; + let lastState = 'open'; + vulnerability.actions.reverse(); + for (const action of vulnerability.actions) { + if (action.state === 'closed' && lastState === 'open') { + daysOpen += differenceInCalendarDays( + parseISO(action.date), + parseISO(lastOpenDate) + ); + lastState = 'closed'; + } else if (action.state === 'open' && lastState === 'closed') { + lastOpenDate = action.date; + lastState = 'open'; + } } return ( - <> - {/* - This vulnerability is found on 17 domains you have access to. - */} - -

- history.goBack()} - className={classes.backLink} - > - - Go back - -

- -
-
-
- -
-

{vulnerability.title}

- + Description + {vulnerability.description} + References + + + + + Hyperlink + Resource + + + + {vulnerability.references.map((row) => ( + + {row.url} + {row.source} + ))} - - - } - label={`${vulnerability.state[0].toUpperCase()}${vulnerability.state.slice( - 1 - )} (${stateMap[vulnerability.substate]})`} - color={ - vulnerability.state === 'open' ? 'secondary' : 'default' - } - /> -
-
-

Description

- {vulnerability.description} -
-
-

References

- {references && - references.map((ref, index) => ( -

- - {ref.name ? ref.name : ref.url} - - {ref.tags.length > 0 - ? ' - ' + ref.tags.join(',') - : ''} -

- ))} -
- {vulnerability.source === 'hibp' && ( -
-

Data

-
- - - Exposed Emails - Breaches - - - - {Object.keys( - vulnerability.structuredData['emails'] - ).map((keyName, keyIndex) => ( - - - {keyName} - - - {vulnerability.structuredData['emails'][ - keyName - ].join(', ')} - - - ))} - -
-
- )} - {vulnerability.source === 'lookingGlass' && ( -
-

Data

- - - - First Seen - Last Seen - Vuln Name - Type - - - - {vulnerability.structuredData['lookingGlassData'].map( - (col: any) => ( - - - {formatDistanceToNow( - parseISO(col.firstSeen) - ) + ' ago'} - - - {formatDistanceToNow(parseISO(col.lastSeen)) + - ' ago'} - - - {col.right_name} - - - {col.vulnOrMal} - - - ) - )} - -
-
- )} - {vulnerability.source === 'dnstwist' && ( -
-

Data

- - - - - Domain Name - IP Address / A Record - MX Record - NS Record - Date Observed - Fuzzer - - - - {vulnerability.structuredData['domains'].map( - (dom: dnstwist) => ( - - - {dom['domain']} - - {dom['dns_a']} - {dom['dns_mx']} - {dom['dns_ns']} - - {dom['date_first_observed']} - - {dom['fuzzer']} - - ) - )} - -
-
-
- )} -
- -
-
- -
-
-

Team notes

-
-
-
- )} -
-
- -
- + CWE-ID + CWE Name + Source + + + + + {vulnerability.cwe} + ? + {vulnerability.source} + + + + + + + More info + + + + + + ); -}; +} From 971801c638d7cad560ae496036990a2b9cc22386 Mon Sep 17 00:00:00 2001 From: Amelia Vance Date: Tue, 24 Oct 2023 16:04:42 -0400 Subject: [PATCH 03/50] Rename current Vulnerability page to make room for new one --- .../pages/Vulnerability/Vulnerability_old.tsx | 555 ++++++++++++++++++ 1 file changed, 555 insertions(+) create mode 100644 frontend/src/pages/Vulnerability/Vulnerability_old.tsx diff --git a/frontend/src/pages/Vulnerability/Vulnerability_old.tsx b/frontend/src/pages/Vulnerability/Vulnerability_old.tsx new file mode 100644 index 000000000..0abbbe776 --- /dev/null +++ b/frontend/src/pages/Vulnerability/Vulnerability_old.tsx @@ -0,0 +1,555 @@ +import { + Paper, + TableContainer, + TextareaAutosize, + Chip, + MenuItem, + Button, + Menu +} from '@mui/material'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableRow +} from '@mui/material'; +import { classes, Root } from './vulnerabilityStyle'; +import { Flag, ChevronLeft, ArrowDropDown } from '@mui/icons-material'; +import { useAuthContext } from 'context'; +import { stateMap } from 'pages/Vulnerabilities/Vulnerabilities'; +import React, { useCallback, useEffect, useState } from 'react'; +import ReactMarkdown from 'react-markdown'; +import { Link, useParams, useHistory } from 'react-router-dom'; +import { Vulnerability as VulnerabilityType } from 'types'; +// @ts-ignore:next-line +import { formatDistanceToNow, parseISO, format } from 'date-fns'; +import { + Timeline, + TimelineItem, + TimelineSeparator, + TimelineDot, + TimelineConnector, + TimelineContent +} from '@mui/lab'; + +export const Vulnerability: React.FC = () => { + const { vulnerabilityId } = useParams(); + const { apiGet, apiPut } = useAuthContext(); + const [vulnerability, setVulnerability] = useState(); + const [comment, setComment] = useState(''); + const [showCommentForm, setShowCommentForm] = useState(false); + const [menuAnchor, setMenuAnchor] = React.useState(null); + + const history = useHistory(); + + const formatDate = (date: string) => { + return format(parseISO(date), 'MM-dd-yyyy'); + }; + + const fetchVulnerability = useCallback(async () => { + try { + const result = await apiGet( + `/vulnerabilities/${vulnerabilityId}` + ); + setVulnerability(result); + } catch (e) { + console.error(e); + } + }, [vulnerabilityId, apiGet]); + + const updateVulnerability = async (body: { [key: string]: string }) => { + try { + if (!vulnerability) return; + const res = await apiPut( + '/vulnerabilities/' + vulnerability.id, + { + body: body + } + ); + setVulnerability({ + ...vulnerability, + state: res.state, + substate: res.substate, + actions: res.actions + }); + } catch (e) { + console.error(e); + } + }; + + useEffect(() => { + fetchVulnerability(); + }, [fetchVulnerability]); + + if (!vulnerability) return ; + + const references = vulnerability.references.map((ref) => ref); + if (vulnerability.cve) + references.unshift({ + name: 'NIST National Vulnerability Database', + url: `https://nvd.nist.gov/vuln/detail/${vulnerability.cve}`, + source: '', + tags: [] + }); + + const states = [ + 'unconfirmed', + 'exploitable', + 'false-positive', + 'accepted-risk', + 'remediated' + ]; + interface dnstwist { + domain: string; + fuzzer: string; + dns_a?: string; + dns_aaaa?: string; + dns_mx?: string; + dns_ns?: string; + date_first_observed?: string; + } + + return ( + <> + {/* + This vulnerability is found on 17 domains you have access to. + */} + +

+ history.goBack()} + className={classes.backLink} + > + + Go back + +

+ +
+
+
+ +
+

{vulnerability.title}

+ + setMenuAnchor(null)} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'center' + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'center' + }} + > + {states.map((state) => ( + { + updateVulnerability({ + substate: state + }); + setMenuAnchor(null); + }} + style={{ outline: 'none' }} + > + {stateMap[state]} + + ))} + +
+ } + label={`${vulnerability.state[0].toUpperCase()}${vulnerability.state.slice( + 1 + )} (${stateMap[vulnerability.substate]})`} + color={ + vulnerability.state === 'open' ? 'secondary' : 'default' + } + /> +
+
+

Description

+ {vulnerability.description} +
+
+

References

+ {references && + references.map((ref, index) => ( +

+ + {ref.name ? ref.name : ref.url} + + {ref.tags.length > 0 + ? ' - ' + ref.tags.join(',') + : ''} +

+ ))} +
+ {vulnerability.source === 'hibp' && ( +
+

Data

+ + + + Exposed Emails + Breaches + + + + {Object.keys( + vulnerability.structuredData['emails'] + ).map((keyName, keyIndex) => ( + + + {keyName} + + + {vulnerability.structuredData['emails'][ + keyName + ].join(', ')} + + + ))} + +
+
+ )} + {vulnerability.source === 'lookingGlass' && ( +
+

Data

+ + + + First Seen + Last Seen + Vuln Name + Type + + + + {vulnerability.structuredData['lookingGlassData'].map( + (col: any) => ( + + + {formatDistanceToNow( + parseISO(col.firstSeen) + ) + ' ago'} + + + {formatDistanceToNow(parseISO(col.lastSeen)) + + ' ago'} + + + {col.right_name} + + + {col.vulnOrMal} + + + ) + )} + +
+
+ )} + {vulnerability.source === 'dnstwist' && ( +
+

Data

+ + + + + Domain Name + IP Address / A Record + MX Record + NS Record + Date Observed + Fuzzer + + + + {vulnerability.structuredData['domains'].map( + (dom: dnstwist) => ( + + + {dom['domain']} + + {dom['dns_a']} + {dom['dns_mx']} + {dom['dns_ns']} + + {dom['date_first_observed']} + + {dom['fuzzer']} + + ) + )} + +
+
+
+ )} +
+
+
+
+ +
+
+

Team notes

+ +
+ {showCommentForm && ( +
+ setComment(e.target.value)} + /> + +
+ )} + {vulnerability.actions && + vulnerability.actions + .filter((action) => action.type === 'comment') + .map((action, index) => ( +
+

+ {action.userName} +

+ + {formatDistanceToNow(parseISO(action.date))} ago + + + {action.value || ''} + +
+ ))} +
+
+ +
+
+

Vulnerability History

+
+ + {vulnerability.actions && + vulnerability.actions + .filter( + (action) => + action.type === 'state-change' && action.substate + ) + .map((action, index) => ( + + + + + {' '} + + State {action.automatic ? 'automatically ' : ''} + changed to {action.state} ( + {stateMap[action.substate!].toLowerCase()}) + {action.userName ? ' by ' + action.userName : ''}{' '} +

+ + {formatDate(action.date)} + +
+
+ ))} + + + + + + + Vulnerability opened

+ + {formatDate(vulnerability.createdAt)} + +
+
+
+
+
+ +
+
+

Provenance

+

+ Root Domain: + {vulnerability.domain.fromRootDomain} +

+

+ Subdomain: + {vulnerability.domain.name} ( + {vulnerability.domain.subdomainSource}) +

+ {vulnerability.service && ( +

+ Service/Port: + {vulnerability.service.service + ? vulnerability.service.service + : vulnerability.service.port}{' '} + ({vulnerability.service.serviceSource}) +

+ )} + {vulnerability.cpe && ( + <> +

+ Product: + {vulnerability.cpe} +

+ + )} +

+ Vulnerability: + {vulnerability.title} ({vulnerability.source}) +

+
+
+
+ {vulnerability.source === 'hibp' && ( + +
+
+

Breaches

+ + + + Breach Name + Date Added + + + + {Object.keys(vulnerability.structuredData['breaches']) + .sort( + (a, b) => + parseISO( + vulnerability.structuredData['breaches'][b][ + 'AddedDate' + ] + ).getTime() - + parseISO( + vulnerability.structuredData['breaches'][a][ + 'AddedDate' + ] + ).getTime() + ) + .map((keyName, keyIndex) => ( + + + {keyName} + + + {formatDistanceToNow( + parseISO( + vulnerability.structuredData['breaches'][ + keyName + ]['AddedDate'] + ) + ) + ' ago'} + + + ))} + +
+
+
+
+ )} +
+
+
+
+ + ); +}; From beb69f055e6ad1d50885fec01e58c5743c05dd70 Mon Sep 17 00:00:00 2001 From: Amelia Vance Date: Tue, 24 Oct 2023 16:05:50 -0400 Subject: [PATCH 04/50] Add new Vulnerability.tsx with start of redesign --- .../src/pages/Vulnerability/Vulnerability.tsx | 741 ++++++------------ 1 file changed, 255 insertions(+), 486 deletions(-) diff --git a/frontend/src/pages/Vulnerability/Vulnerability.tsx b/frontend/src/pages/Vulnerability/Vulnerability.tsx index 0abbbe776..10078c643 100644 --- a/frontend/src/pages/Vulnerability/Vulnerability.tsx +++ b/frontend/src/pages/Vulnerability/Vulnerability.tsx @@ -1,52 +1,37 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { Link, useParams, useHistory } from 'react-router-dom'; + +// material-ui +import { ChevronLeft, OpenInNew } from '@mui/icons-material'; import { - Paper, - TableContainer, - TextareaAutosize, - Chip, - MenuItem, + AppBar, + Box, Button, - Menu -} from '@mui/material'; -import { + Grid, + IconButton, + Link as LinkMui, + Paper, Table, TableBody, TableCell, + TableContainer, TableHead, - TableRow + TableRow, + Toolbar, + Typography } from '@mui/material'; -import { classes, Root } from './vulnerabilityStyle'; -import { Flag, ChevronLeft, ArrowDropDown } from '@mui/icons-material'; + +import { differenceInCalendarDays, parseISO } from 'date-fns'; +import { getSeverityColor } from 'pages/Risk/utils'; import { useAuthContext } from 'context'; -import { stateMap } from 'pages/Vulnerabilities/Vulnerabilities'; -import React, { useCallback, useEffect, useState } from 'react'; -import ReactMarkdown from 'react-markdown'; -import { Link, useParams, useHistory } from 'react-router-dom'; import { Vulnerability as VulnerabilityType } from 'types'; -// @ts-ignore:next-line -import { formatDistanceToNow, parseISO, format } from 'date-fns'; -import { - Timeline, - TimelineItem, - TimelineSeparator, - TimelineDot, - TimelineConnector, - TimelineContent -} from '@mui/lab'; export const Vulnerability: React.FC = () => { const { vulnerabilityId } = useParams(); const { apiGet, apiPut } = useAuthContext(); const [vulnerability, setVulnerability] = useState(); - const [comment, setComment] = useState(''); - const [showCommentForm, setShowCommentForm] = useState(false); - const [menuAnchor, setMenuAnchor] = React.useState(null); const history = useHistory(); - - const formatDate = (date: string) => { - return format(parseISO(date), 'MM-dd-yyyy'); - }; - const fetchVulnerability = useCallback(async () => { try { const result = await apiGet( @@ -82,7 +67,7 @@ export const Vulnerability: React.FC = () => { fetchVulnerability(); }, [fetchVulnerability]); - if (!vulnerability) return ; + if (!vulnerability) return 'No Vulnerabilities'; const references = vulnerability.references.map((ref) => ref); if (vulnerability.cve) @@ -93,463 +78,247 @@ export const Vulnerability: React.FC = () => { tags: [] }); - const states = [ - 'unconfirmed', - 'exploitable', - 'false-positive', - 'accepted-risk', - 'remediated' - ]; - interface dnstwist { - domain: string; - fuzzer: string; - dns_a?: string; - dns_aaaa?: string; - dns_mx?: string; - dns_ns?: string; - date_first_observed?: string; + const product = + vulnerability.service && + vulnerability.service.products.find( + (product) => vulnerability.cpe && product.cpe && vulnerability.cpe.includes(product.cpe) + ); + // Calculates the total number of days a vulnerability has been open + let daysOpen = 0; + let lastOpenDate = vulnerability.createdAt; + let lastState = 'open'; + vulnerability.actions.reverse(); + for (const action of vulnerability.actions) { + if (action.state === 'closed' && lastState === 'open') { + daysOpen += differenceInCalendarDays( + parseISO(action.date), + parseISO(lastOpenDate) + ); + lastState = 'closed'; + } else if (action.state === 'open' && lastState === 'closed') { + lastOpenDate = action.date; + lastState = 'open'; + } } return ( - <> - {/* - This vulnerability is found on 17 domains you have access to. - */} - -

- history.goBack()} - className={classes.backLink} - > - - Go back - -

- -
-
-
- -
-

{vulnerability.title}

- + Description + {vulnerability.description} + References + + + + + Hyperlink + Resource + + + + {vulnerability.references.map((row) => ( + + {row.url} + {row.source} + ))} - - - } - label={`${vulnerability.state[0].toUpperCase()}${vulnerability.state.slice( - 1 - )} (${stateMap[vulnerability.substate]})`} - color={ - vulnerability.state === 'open' ? 'secondary' : 'default' - } - /> -
-
-

Description

- {vulnerability.description} -
-
-

References

- {references && - references.map((ref, index) => ( -

- - {ref.name ? ref.name : ref.url} - - {ref.tags.length > 0 - ? ' - ' + ref.tags.join(',') - : ''} -

- ))} -
- {vulnerability.source === 'hibp' && ( -
-

Data

-
- - - Exposed Emails - Breaches - - - - {Object.keys( - vulnerability.structuredData['emails'] - ).map((keyName, keyIndex) => ( - - - {keyName} - - - {vulnerability.structuredData['emails'][ - keyName - ].join(', ')} - - - ))} - -
-
- )} - {vulnerability.source === 'lookingGlass' && ( -
-

Data

- - - - First Seen - Last Seen - Vuln Name - Type - - - - {vulnerability.structuredData['lookingGlassData'].map( - (col: any) => ( - - - {formatDistanceToNow( - parseISO(col.firstSeen) - ) + ' ago'} - - - {formatDistanceToNow(parseISO(col.lastSeen)) + - ' ago'} - - - {col.right_name} - - - {col.vulnOrMal} - - - ) - )} - -
-
- )} - {vulnerability.source === 'dnstwist' && ( -
-

Data

- - - - - Domain Name - IP Address / A Record - MX Record - NS Record - Date Observed - Fuzzer - - - - {vulnerability.structuredData['domains'].map( - (dom: dnstwist) => ( - - - {dom['domain']} - - {dom['dns_a']} - {dom['dns_mx']} - {dom['dns_ns']} - - {dom['date_first_observed']} - - {dom['fuzzer']} - - ) - )} - -
-
-
- )} -
- -
-
- -
-
-

Team notes

-
-
-
- )} -
-
- -
- + CWE-ID + CWE Name + Source + + + + + {vulnerability.cwe} + ? + {vulnerability.source} + + + + + + + More info + + + + + + ); -}; +} From 5be6a18309f81a863661f06308e388767a2e96bb Mon Sep 17 00:00:00 2001 From: Amelia Vance Date: Tue, 24 Oct 2023 16:32:11 -0400 Subject: [PATCH 05/50] Update frame responsiveness in Vulnerability.tsx --- .../src/pages/Vulnerability/Vulnerability.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/frontend/src/pages/Vulnerability/Vulnerability.tsx b/frontend/src/pages/Vulnerability/Vulnerability.tsx index 10078c643..f89a4e76d 100644 --- a/frontend/src/pages/Vulnerability/Vulnerability.tsx +++ b/frontend/src/pages/Vulnerability/Vulnerability.tsx @@ -119,8 +119,8 @@ export const Vulnerability: React.FC = () => { - - + + Vulnerability Details @@ -189,9 +189,9 @@ export const Vulnerability: React.FC = () => { - - - + + + @@ -248,11 +248,11 @@ export const Vulnerability: React.FC = () => {
- - - + + + - + {vulnerability.cve} - Description - {vulnerability.description} - References + + Description + + + {vulnerability.description} + + + References + Hyperlink Resource @@ -283,7 +312,9 @@ export const Vulnerability: React.FC = () => { {vulnerability.references.map((row) => ( - {row.url} + + {row.url} + {row.source} ))} @@ -295,7 +326,9 @@ export const Vulnerability: React.FC = () => {
CWE-ID CWE Name @@ -304,7 +337,9 @@ export const Vulnerability: React.FC = () => { - {vulnerability.cwe} + + {vulnerability.cwe} + ? {vulnerability.source} @@ -317,8 +352,8 @@ export const Vulnerability: React.FC = () => { - + ); -} +}; From 0c2e8dc90aa1c000faa3c74e5b97f5ca27895847 Mon Sep 17 00:00:00 2001 From: Amelia Vance Date: Tue, 24 Oct 2023 17:07:41 -0400 Subject: [PATCH 07/50] Update negative conditional return in Vulnerability.tsx --- frontend/src/pages/Vulnerability/Vulnerability.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/Vulnerability/Vulnerability.tsx b/frontend/src/pages/Vulnerability/Vulnerability.tsx index 4b9ae9b89..a7895b16c 100644 --- a/frontend/src/pages/Vulnerability/Vulnerability.tsx +++ b/frontend/src/pages/Vulnerability/Vulnerability.tsx @@ -67,7 +67,9 @@ export const Vulnerability: React.FC = () => { fetchVulnerability(); }, [fetchVulnerability]); - if (!vulnerability) return 'No Vulnerabilities'; + if (!vulnerability) return ( + <>No Vulnerabilities + ); const references = vulnerability.references.map((ref) => ref); if (vulnerability.cve) From deff6871f9e84b896e0623e370197fd5d3dfefb1 Mon Sep 17 00:00:00 2001 From: Amelia Vance Date: Tue, 24 Oct 2023 17:10:16 -0400 Subject: [PATCH 08/50] Update typescript format --- frontend/src/pages/Vulnerability/Vulnerability.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/pages/Vulnerability/Vulnerability.tsx b/frontend/src/pages/Vulnerability/Vulnerability.tsx index a7895b16c..fbd0e9e5f 100644 --- a/frontend/src/pages/Vulnerability/Vulnerability.tsx +++ b/frontend/src/pages/Vulnerability/Vulnerability.tsx @@ -67,9 +67,7 @@ export const Vulnerability: React.FC = () => { fetchVulnerability(); }, [fetchVulnerability]); - if (!vulnerability) return ( - <>No Vulnerabilities - ); + if (!vulnerability) return <>No Vulnerabilities; const references = vulnerability.references.map((ref) => ref); if (vulnerability.cve) From cb3295af8542412d8c6a7dfc0fc87c5bce7922b2 Mon Sep 17 00:00:00 2001 From: Amelia Vance Date: Tue, 24 Oct 2023 17:20:20 -0400 Subject: [PATCH 09/50] Comment unused code in Vulnerability.tsx --- .../src/pages/Vulnerability/Vulnerability.tsx | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/frontend/src/pages/Vulnerability/Vulnerability.tsx b/frontend/src/pages/Vulnerability/Vulnerability.tsx index fbd0e9e5f..348711bac 100644 --- a/frontend/src/pages/Vulnerability/Vulnerability.tsx +++ b/frontend/src/pages/Vulnerability/Vulnerability.tsx @@ -28,7 +28,7 @@ import { Vulnerability as VulnerabilityType } from 'types'; export const Vulnerability: React.FC = () => { const { vulnerabilityId } = useParams(); - const { apiGet, apiPut } = useAuthContext(); + const { apiGet } = useAuthContext(); const [vulnerability, setVulnerability] = useState(); const history = useHistory(); @@ -43,25 +43,25 @@ export const Vulnerability: React.FC = () => { } }, [vulnerabilityId, apiGet]); - const updateVulnerability = async (body: { [key: string]: string }) => { - try { - if (!vulnerability) return; - const res = await apiPut( - '/vulnerabilities/' + vulnerability.id, - { - body: body - } - ); - setVulnerability({ - ...vulnerability, - state: res.state, - substate: res.substate, - actions: res.actions - }); - } catch (e) { - console.error(e); - } - }; + // const updateVulnerability = async (body: { [key: string]: string }) => { + // try { + // if (!vulnerability) return; + // const res = await apiPut( + // '/vulnerabilities/' + vulnerability.id, + // { + // body: body + // } + // ); + // setVulnerability({ + // ...vulnerability, + // state: res.state, + // substate: res.substate, + // actions: res.actions + // }); + // } catch (e) { + // console.error(e); + // } + // }; useEffect(() => { fetchVulnerability(); From a511fe5c9edf1ee71eca668dcf12b818bf25c7b7 Mon Sep 17 00:00:00 2001 From: Amelia Vance Date: Wed, 25 Oct 2023 13:17:02 -0400 Subject: [PATCH 10/50] Update span to Box in Vulnerability.tsx --- frontend/src/pages/Vulnerability/Vulnerability.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/Vulnerability/Vulnerability.tsx b/frontend/src/pages/Vulnerability/Vulnerability.tsx index 348711bac..936b50435 100644 --- a/frontend/src/pages/Vulnerability/Vulnerability.tsx +++ b/frontend/src/pages/Vulnerability/Vulnerability.tsx @@ -225,8 +225,9 @@ export const Vulnerability: React.FC = () => { - { }} > {vulnerability.severity} - + {vulnerability.isKev ? 'Yes' : 'No'} From 43b0543f66f00ef271379610a697f027481ef734 Mon Sep 17 00:00:00 2001 From: Amelia Vance Date: Wed, 25 Oct 2023 15:02:15 -0400 Subject: [PATCH 11/50] Update style to sx in Vulnerability.tsx --- frontend/src/pages/Vulnerability/Vulnerability.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/Vulnerability/Vulnerability.tsx b/frontend/src/pages/Vulnerability/Vulnerability.tsx index 936b50435..6b1b55a0b 100644 --- a/frontend/src/pages/Vulnerability/Vulnerability.tsx +++ b/frontend/src/pages/Vulnerability/Vulnerability.tsx @@ -109,7 +109,7 @@ export const Vulnerability: React.FC = () => { history.goBack()}> Date: Mon, 18 Dec 2023 10:37:49 -0400 Subject: [PATCH 12/50] Add CVC Highlight section links in Vulnerability.tsx --- frontend/src/pages/Vulnerability/Vulnerability.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/Vulnerability/Vulnerability.tsx b/frontend/src/pages/Vulnerability/Vulnerability.tsx index 6b1b55a0b..208038afd 100644 --- a/frontend/src/pages/Vulnerability/Vulnerability.tsx +++ b/frontend/src/pages/Vulnerability/Vulnerability.tsx @@ -220,9 +220,13 @@ export const Vulnerability: React.FC = () => { - - {vulnerability.cve} - + + {vulnerability.cve} + { {vulnerability.isKev ? 'Yes' : 'No'} - {vulnerability.domain.name} + + {vulnerability.domain.name} + {product From e2beebdad5ea5a6096f511e638c13004810367d2 Mon Sep 17 00:00:00 2001 From: Amelia Vance Date: Mon, 18 Dec 2023 12:41:29 -0400 Subject: [PATCH 13/50] Update CWE and CVE links in Vulnerability.tsx --- .../src/pages/Vulnerability/Vulnerability.tsx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/Vulnerability/Vulnerability.tsx b/frontend/src/pages/Vulnerability/Vulnerability.tsx index 208038afd..46bb3f91e 100644 --- a/frontend/src/pages/Vulnerability/Vulnerability.tsx +++ b/frontend/src/pages/Vulnerability/Vulnerability.tsx @@ -78,6 +78,15 @@ export const Vulnerability: React.FC = () => { tags: [] }); + function cweUrl(cwe: string | null): string { + // Check if the input starts with "CWE-" and extract the ID part + if (cwe && cwe.startsWith('CWE-')) { + const id = cwe.replace('CWE-', ''); + return `https://cwe.mitre.org/data/definitions/${id}`; + } + return 'https://cwe.mitre.org/data'; + } + const product = vulnerability.service && vulnerability.service.products.find( @@ -220,13 +229,13 @@ export const Vulnerability: React.FC = () => { - {vulnerability.cve} - + { - {vulnerability.cwe} + + {vulnerability.cwe} + ? {vulnerability.source} From 04ae18d2ca01f4db5855ec95f83746c90e18c0b6 Mon Sep 17 00:00:00 2001 From: Amelia Vance Date: Mon, 18 Dec 2023 16:32:10 -0400 Subject: [PATCH 14/50] Add empty Severity, Affected, History & Notes sections in Vulnerability.tsx --- .../src/pages/Vulnerability/Vulnerability.tsx | 167 +++++++++++++++--- 1 file changed, 147 insertions(+), 20 deletions(-) diff --git a/frontend/src/pages/Vulnerability/Vulnerability.tsx b/frontend/src/pages/Vulnerability/Vulnerability.tsx index 46bb3f91e..55e76de3c 100644 --- a/frontend/src/pages/Vulnerability/Vulnerability.tsx +++ b/frontend/src/pages/Vulnerability/Vulnerability.tsx @@ -21,11 +21,18 @@ import { Typography } from '@mui/material'; -import { differenceInCalendarDays, parseISO } from 'date-fns'; +import { differenceInCalendarDays, format, parseISO } from 'date-fns'; import { getSeverityColor } from 'pages/Risk/utils'; import { useAuthContext } from 'context'; import { Vulnerability as VulnerabilityType } from 'types'; +const formatDate = (date: string | null) => { + if (date) { + return format(parseISO(date), 'MM/dd/yyyy'); + } + return 'Date not found'; +}; + export const Vulnerability: React.FC = () => { const { vulnerabilityId } = useParams(); const { apiGet } = useAuthContext(); @@ -86,7 +93,13 @@ export const Vulnerability: React.FC = () => { } return 'https://cwe.mitre.org/data'; } - + const vulnState = + vulnerability.state[0].toUpperCase() + + vulnerability.state.slice(1) + + ' (' + + vulnerability.substate[0].toUpperCase() + + vulnerability.substate.slice(1) + + ')'; const product = vulnerability.service && vulnerability.service.products.find( @@ -270,14 +283,7 @@ export const Vulnerability: React.FC = () => { )) : daysOpen} - - {vulnerability.state[0].toUpperCase() + - vulnerability.state.slice(1) + - ' (' + - vulnerability.substate[0].toUpperCase() + - vulnerability.substate.slice(1) + - ')'} - + {vulnState}
@@ -297,12 +303,7 @@ export const Vulnerability: React.FC = () => { sx={{ borderRadius: 28, textTransform: 'none' }} onClick={() => console.log()} > - {vulnerability.state[0].toUpperCase() + - vulnerability.state.slice(1) + - ' (' + - vulnerability.substate[0].toUpperCase() + - vulnerability.substate.slice(1) + - ')'} + {vulnState} Description @@ -337,8 +338,7 @@ export const Vulnerability: React.FC = () => {
-
- + {
- - More info + + + + CVSS 3.x Severity & Metrics + + + + + + NIST: + + + + + Base Score: + + + + + Vector: + + + + + CVSS 2.0 Severity & Metrics + + + + + + NIST: + + + + + Base Score: + + + + + Vector: + + + + + + Products Affected + opensuse + +
    +
  • +
+
+ canonical + +
    +
  • +
+
+ debian + +
    +
  • +
+
+ oracle + +
    +
  • +
+
+ fedoraproject + +
    +
  • +
+
+ apache + +
    +
  • +
+
+
+ + + Vulnerability Detection History + + + First Detected + + + Vulnerability Opened: {formatDate(vulnerability.createdAt)} + + + Last Detected + + + State automatically changed to {vulnState.toLowerCase()}:{' '} + {formatDate(vulnerability.lastSeen)} + + +
+ + + Team Notes + Add new note +
From 63d2a54f5d9f9bf3cfa0ccfea6381e9db602e20d Mon Sep 17 00:00:00 2001 From: Amelia Vance Date: Mon, 18 Dec 2023 17:02:07 -0400 Subject: [PATCH 15/50] Update iconButton with link in Vulnerability.tsx --- frontend/src/pages/Vulnerability/Vulnerability.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/pages/Vulnerability/Vulnerability.tsx b/frontend/src/pages/Vulnerability/Vulnerability.tsx index 55e76de3c..bfb6d48f1 100644 --- a/frontend/src/pages/Vulnerability/Vulnerability.tsx +++ b/frontend/src/pages/Vulnerability/Vulnerability.tsx @@ -157,6 +157,7 @@ export const Vulnerability: React.FC = () => { edge="start" color="inherit" aria-label="menu" + href={`/inventory/domain/${vulnerability.domain.id}`} > From 9d5dcc301afeddc92402ddc6e45e657bcc451b36 Mon Sep 17 00:00:00 2001 From: Amelia Vance Date: Mon, 18 Dec 2023 17:15:19 -0400 Subject: [PATCH 16/50] Update Typography with div component in Vulnerability.tsx --- .../src/pages/Vulnerability/Vulnerability.tsx | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/frontend/src/pages/Vulnerability/Vulnerability.tsx b/frontend/src/pages/Vulnerability/Vulnerability.tsx index bfb6d48f1..1861c58d0 100644 --- a/frontend/src/pages/Vulnerability/Vulnerability.tsx +++ b/frontend/src/pages/Vulnerability/Vulnerability.tsx @@ -432,37 +432,37 @@ export const Vulnerability: React.FC = () => { Products Affected opensuse - +
canonical - +
debian - +
oracle - +
fedoraproject - +
apache - +
@@ -488,10 +488,7 @@ export const Vulnerability: React.FC = () => {
- + Team Notes Add new note From b75a7db113b7d6deb3c30a5f81f94cf80649472d Mon Sep 17 00:00:00 2001 From: Amelia Vance Date: Mon, 8 Jan 2024 14:29:54 -0500 Subject: [PATCH 17/50] Organize Vulnerability.tsx file --- .../src/pages/Vulnerability/Vulnerability.tsx | 687 +++++++++--------- 1 file changed, 349 insertions(+), 338 deletions(-) diff --git a/frontend/src/pages/Vulnerability/Vulnerability.tsx b/frontend/src/pages/Vulnerability/Vulnerability.tsx index 1861c58d0..e6a8a4d8b 100644 --- a/frontend/src/pages/Vulnerability/Vulnerability.tsx +++ b/frontend/src/pages/Vulnerability/Vulnerability.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useEffect, useState } from 'react'; import { Link, useParams, useHistory } from 'react-router-dom'; - -// material-ui +import { differenceInCalendarDays, format, parseISO } from 'date-fns'; import { ChevronLeft, OpenInNew } from '@mui/icons-material'; import { AppBar, @@ -20,8 +19,6 @@ import { Toolbar, Typography } from '@mui/material'; - -import { differenceInCalendarDays, format, parseISO } from 'date-fns'; import { getSeverityColor } from 'pages/Risk/utils'; import { useAuthContext } from 'context'; import { Vulnerability as VulnerabilityType } from 'types'; @@ -126,6 +123,350 @@ export const Vulnerability: React.FC = () => { } } + const OverviewSection = () => { + return ( + + + + + {vulnerability.domain.name} + + + + + + + + + + Overview + + IP: {vulnerability.domain.ip} +
+ First Seen:{' '} + {differenceInCalendarDays( + Date.now(), + parseISO(vulnerability.domain.createdAt) + )}{' '} + days ago +
+ Last Seen:{' '} + {differenceInCalendarDays( + Date.now(), + parseISO(vulnerability.domain.updatedAt) + )}{' '} + days ago +
+ Country:{' '} + {vulnerability.domain.country + ? vulnerability.domain.country + : 'Not found'} +
+ Organization: {vulnerability.domain.organization.name} +
+
+
+ Installed (Known) Products + + Misc: +
+ Marketing: +
+ Networking: +
+ Web Framework: +
+ Web Server: +
+
+
+ + + Provenance + + + + Root Domain: {vulnerability.domain.fromRootDomain} +
+ Subdomain: {vulnerability.domain.name} ( + {vulnerability.domain.subdomainSource})
+ Service/Port:{' '} + {vulnerability.service.service + ? vulnerability.service.service + : vulnerability.service.port}{' '} + ({vulnerability.service.serviceSource})
+ Product: {vulnerability.cpe} +
+ Vulnerability: {vulnerability.title} ( + {vulnerability.source}) +
+
+
+
+
+
+ ); + }; + const CVEHighlightSection = () => { + return ( + + + + + + + + {vulnerability.cve} + + + + + {vulnerability.severity} + + + {vulnerability.isKev ? 'Yes' : 'No'} + + + {vulnerability.domain.name} + + + + {product + ? product.name + + (product.version ? ' ' + product.version : '') + : vulnerability.cpe} + + + {lastState === 'open' + ? (daysOpen += differenceInCalendarDays( + new Date(), + parseISO(lastOpenDate) + )) + : daysOpen} + + {vulnState} + + +
+
+
+ ); + }; + const CVEReferencesCWESection = () => { + return ( + + + {vulnerability.cve} + + + + Description + + {vulnerability.description} + + References + + + + + + Hyperlink + Resource + + + + {vulnerability.references.map((row) => ( + + + {row.url} + + {row.source} + + ))} + +
+
+ + + + + CWE-ID + CWE Name + Source + + + + + + + {vulnerability.cwe} + + + ? + {vulnerability.source} + + +
+
+
+ ); + }; + const SeverityAffectedHistorySection = () => { + return ( + + + + CVSS 3.x Severity & Metrics + + + + + + NIST: + + + + + Base Score: + + + + + Vector: + + + + + CVSS 2.0 Severity & Metrics + + + + + + NIST: + + + + + Base Score: + + + + + Vector: + + + + + + Products Affected + opensuse + +
    +
  • +
+
+ canonical + +
    +
  • +
+
+ debian + +
    +
  • +
+
+ oracle + +
    +
  • +
+
+ fedoraproject + +
    +
  • +
+
+ apache + +
    +
  • +
+
+
+ + Vulnerability Detection History + + First Detected + + + Vulnerability Opened: {formatDate(vulnerability.createdAt)} + + + Last Detected + + + State automatically changed to {vulnState.toLowerCase()}:{' '} + {formatDate(vulnerability.lastSeen)} + + +
+ ); + }; return ( @@ -146,347 +487,17 @@ export const Vulnerability: React.FC = () => { Vulnerability Details - - - - - {vulnerability.domain.name} - - - - - - - - - - Overview - - IP: {vulnerability.domain.ip} -
- First Seen:{' '} - {differenceInCalendarDays( - Date.now(), - parseISO(vulnerability.domain.createdAt) - )}{' '} - days ago -
- Last Seen:{' '} - {differenceInCalendarDays( - Date.now(), - parseISO(vulnerability.domain.updatedAt) - )}{' '} - days ago -
- Country:{' '} - {vulnerability.domain.country - ? vulnerability.domain.country - : 'Not found'} -
- Organization: {vulnerability.domain.organization.name} -
-
-
- - Installed (Known) Products - - - Misc: -
- Marketing: -
- Networking: -
- Web Framework: -
- Web Server: -
-
-
- - - Provenance - - - - Root Domain: {vulnerability.domain.fromRootDomain} -
- Subdomain: {vulnerability.domain.name} ( - {vulnerability.domain.subdomainSource})
- Service/Port:{' '} - {vulnerability.service.service - ? vulnerability.service.service - : vulnerability.service.port}{' '} - ({vulnerability.service.serviceSource})
- Product: {vulnerability.cpe} -
- Vulnerability: {vulnerability.title} ( - {vulnerability.source}) -
-
-
-
-
-
+
- - - - - - - - {vulnerability.cve} - - - - - {vulnerability.severity} - - - {vulnerability.isKev ? 'Yes' : 'No'} - - - {vulnerability.domain.name} - - - - {product - ? product.name + - (product.version ? ' ' + product.version : '') - : vulnerability.cpe} - - - {lastState === 'open' - ? (daysOpen += differenceInCalendarDays( - new Date(), - parseISO(lastOpenDate) - )) - : daysOpen} - - {vulnState} - - -
-
-
+ - - - {vulnerability.cve} - - - - Description - - - {vulnerability.description} - - - References - - - - - - Hyperlink - Resource - - - - {vulnerability.references.map((row) => ( - - - {row.url} - - {row.source} - - ))} - -
-
- - - - - CWE-ID - CWE Name - Source - - - - - - - {vulnerability.cwe} - - - ? - {vulnerability.source} - - -
-
-
- - - - CVSS 3.x Severity & Metrics - - - - - - NIST: - - - - - Base Score: - - - - - Vector: - - - - - CVSS 2.0 Severity & Metrics - - - - - - NIST: - - - - - Base Score: - - - - - Vector: - - - - - - Products Affected - opensuse - -
    -
  • -
-
- canonical - -
    -
  • -
-
- debian - -
    -
  • -
-
- oracle - -
    -
  • -
-
- fedoraproject - -
    -
  • -
-
- apache - -
    -
  • -
-
-
- - - Vulnerability Detection History - - - First Detected - - - Vulnerability Opened: {formatDate(vulnerability.createdAt)} - - - Last Detected - - - State automatically changed to {vulnState.toLowerCase()}:{' '} - {formatDate(vulnerability.lastSeen)} - - -
+ + Team Notes From d2f6b4043525dfcaa605f39a78a85ec9d818bfe4 Mon Sep 17 00:00:00 2001 From: Amelia Vance Date: Mon, 5 Feb 2024 14:14:50 -0500 Subject: [PATCH 18/50] Remove unused files in the pages/Vulnerability folder --- .../pages/Vulnerability/Vulnerability_old.tsx | 555 ------------------ .../pages/Vulnerability/vulnerabilityStyle.ts | 112 ---- 2 files changed, 667 deletions(-) delete mode 100644 frontend/src/pages/Vulnerability/Vulnerability_old.tsx delete mode 100644 frontend/src/pages/Vulnerability/vulnerabilityStyle.ts diff --git a/frontend/src/pages/Vulnerability/Vulnerability_old.tsx b/frontend/src/pages/Vulnerability/Vulnerability_old.tsx deleted file mode 100644 index 0abbbe776..000000000 --- a/frontend/src/pages/Vulnerability/Vulnerability_old.tsx +++ /dev/null @@ -1,555 +0,0 @@ -import { - Paper, - TableContainer, - TextareaAutosize, - Chip, - MenuItem, - Button, - Menu -} from '@mui/material'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableRow -} from '@mui/material'; -import { classes, Root } from './vulnerabilityStyle'; -import { Flag, ChevronLeft, ArrowDropDown } from '@mui/icons-material'; -import { useAuthContext } from 'context'; -import { stateMap } from 'pages/Vulnerabilities/Vulnerabilities'; -import React, { useCallback, useEffect, useState } from 'react'; -import ReactMarkdown from 'react-markdown'; -import { Link, useParams, useHistory } from 'react-router-dom'; -import { Vulnerability as VulnerabilityType } from 'types'; -// @ts-ignore:next-line -import { formatDistanceToNow, parseISO, format } from 'date-fns'; -import { - Timeline, - TimelineItem, - TimelineSeparator, - TimelineDot, - TimelineConnector, - TimelineContent -} from '@mui/lab'; - -export const Vulnerability: React.FC = () => { - const { vulnerabilityId } = useParams(); - const { apiGet, apiPut } = useAuthContext(); - const [vulnerability, setVulnerability] = useState(); - const [comment, setComment] = useState(''); - const [showCommentForm, setShowCommentForm] = useState(false); - const [menuAnchor, setMenuAnchor] = React.useState(null); - - const history = useHistory(); - - const formatDate = (date: string) => { - return format(parseISO(date), 'MM-dd-yyyy'); - }; - - const fetchVulnerability = useCallback(async () => { - try { - const result = await apiGet( - `/vulnerabilities/${vulnerabilityId}` - ); - setVulnerability(result); - } catch (e) { - console.error(e); - } - }, [vulnerabilityId, apiGet]); - - const updateVulnerability = async (body: { [key: string]: string }) => { - try { - if (!vulnerability) return; - const res = await apiPut( - '/vulnerabilities/' + vulnerability.id, - { - body: body - } - ); - setVulnerability({ - ...vulnerability, - state: res.state, - substate: res.substate, - actions: res.actions - }); - } catch (e) { - console.error(e); - } - }; - - useEffect(() => { - fetchVulnerability(); - }, [fetchVulnerability]); - - if (!vulnerability) return ; - - const references = vulnerability.references.map((ref) => ref); - if (vulnerability.cve) - references.unshift({ - name: 'NIST National Vulnerability Database', - url: `https://nvd.nist.gov/vuln/detail/${vulnerability.cve}`, - source: '', - tags: [] - }); - - const states = [ - 'unconfirmed', - 'exploitable', - 'false-positive', - 'accepted-risk', - 'remediated' - ]; - interface dnstwist { - domain: string; - fuzzer: string; - dns_a?: string; - dns_aaaa?: string; - dns_mx?: string; - dns_ns?: string; - date_first_observed?: string; - } - - return ( - <> - {/* - This vulnerability is found on 17 domains you have access to. - */} - -

- history.goBack()} - className={classes.backLink} - > - - Go back - -

- -
-
-
- -
-

{vulnerability.title}

- - setMenuAnchor(null)} - anchorOrigin={{ - vertical: 'bottom', - horizontal: 'center' - }} - transformOrigin={{ - vertical: 'top', - horizontal: 'center' - }} - > - {states.map((state) => ( - { - updateVulnerability({ - substate: state - }); - setMenuAnchor(null); - }} - style={{ outline: 'none' }} - > - {stateMap[state]} - - ))} - -
- } - label={`${vulnerability.state[0].toUpperCase()}${vulnerability.state.slice( - 1 - )} (${stateMap[vulnerability.substate]})`} - color={ - vulnerability.state === 'open' ? 'secondary' : 'default' - } - /> -
-
-

Description

- {vulnerability.description} -
-
-

References

- {references && - references.map((ref, index) => ( -

- - {ref.name ? ref.name : ref.url} - - {ref.tags.length > 0 - ? ' - ' + ref.tags.join(',') - : ''} -

- ))} -
- {vulnerability.source === 'hibp' && ( -
-

Data

- - - - Exposed Emails - Breaches - - - - {Object.keys( - vulnerability.structuredData['emails'] - ).map((keyName, keyIndex) => ( - - - {keyName} - - - {vulnerability.structuredData['emails'][ - keyName - ].join(', ')} - - - ))} - -
-
- )} - {vulnerability.source === 'lookingGlass' && ( -
-

Data

- - - - First Seen - Last Seen - Vuln Name - Type - - - - {vulnerability.structuredData['lookingGlassData'].map( - (col: any) => ( - - - {formatDistanceToNow( - parseISO(col.firstSeen) - ) + ' ago'} - - - {formatDistanceToNow(parseISO(col.lastSeen)) + - ' ago'} - - - {col.right_name} - - - {col.vulnOrMal} - - - ) - )} - -
-
- )} - {vulnerability.source === 'dnstwist' && ( -
-

Data

- - - - - Domain Name - IP Address / A Record - MX Record - NS Record - Date Observed - Fuzzer - - - - {vulnerability.structuredData['domains'].map( - (dom: dnstwist) => ( - - - {dom['domain']} - - {dom['dns_a']} - {dom['dns_mx']} - {dom['dns_ns']} - - {dom['date_first_observed']} - - {dom['fuzzer']} - - ) - )} - -
-
-
- )} -
-
-
-
- -
-
-

Team notes

- -
- {showCommentForm && ( -
- setComment(e.target.value)} - /> - -
- )} - {vulnerability.actions && - vulnerability.actions - .filter((action) => action.type === 'comment') - .map((action, index) => ( -
-

- {action.userName} -

- - {formatDistanceToNow(parseISO(action.date))} ago - - - {action.value || ''} - -
- ))} -
-
- -
-
-

Vulnerability History

-
- - {vulnerability.actions && - vulnerability.actions - .filter( - (action) => - action.type === 'state-change' && action.substate - ) - .map((action, index) => ( - - - - - {' '} - - State {action.automatic ? 'automatically ' : ''} - changed to {action.state} ( - {stateMap[action.substate!].toLowerCase()}) - {action.userName ? ' by ' + action.userName : ''}{' '} -

- - {formatDate(action.date)} - -
-
- ))} - - - - - - - Vulnerability opened

- - {formatDate(vulnerability.createdAt)} - -
-
-
-
-
- -
-
-

Provenance

-

- Root Domain: - {vulnerability.domain.fromRootDomain} -

-

- Subdomain: - {vulnerability.domain.name} ( - {vulnerability.domain.subdomainSource}) -

- {vulnerability.service && ( -

- Service/Port: - {vulnerability.service.service - ? vulnerability.service.service - : vulnerability.service.port}{' '} - ({vulnerability.service.serviceSource}) -

- )} - {vulnerability.cpe && ( - <> -

- Product: - {vulnerability.cpe} -

- - )} -

- Vulnerability: - {vulnerability.title} ({vulnerability.source}) -

-
-
-
- {vulnerability.source === 'hibp' && ( - -
-
-

Breaches

- - - - Breach Name - Date Added - - - - {Object.keys(vulnerability.structuredData['breaches']) - .sort( - (a, b) => - parseISO( - vulnerability.structuredData['breaches'][b][ - 'AddedDate' - ] - ).getTime() - - parseISO( - vulnerability.structuredData['breaches'][a][ - 'AddedDate' - ] - ).getTime() - ) - .map((keyName, keyIndex) => ( - - - {keyName} - - - {formatDistanceToNow( - parseISO( - vulnerability.structuredData['breaches'][ - keyName - ]['AddedDate'] - ) - ) + ' ago'} - - - ))} - -
-
-
-
- )} -
-
-
-
- - ); -}; diff --git a/frontend/src/pages/Vulnerability/vulnerabilityStyle.ts b/frontend/src/pages/Vulnerability/vulnerabilityStyle.ts deleted file mode 100644 index eea6b583e..000000000 --- a/frontend/src/pages/Vulnerability/vulnerabilityStyle.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { styled } from '@mui/material/styles'; - -const PREFIX = 'Vulnerability'; - -export const classes = { - root: `${PREFIX}-root`, - cardRoot: `${PREFIX}-cardRoot`, - title: `${PREFIX}-title`, - section: `${PREFIX}-section`, - subtitle: `${PREFIX}-subtitle`, - inner: `${PREFIX}-inner`, - contentWrapper: `${PREFIX}-contentWrapper`, - content: `${PREFIX}-content`, - panel: `${PREFIX}-panel`, - backLink: `${PREFIX}-backLink`, - linkSmall: `${PREFIX}-linkSmall` -}; - -export const Root = styled('div')(({ theme }) => ({ - [`& .${classes.root}`]: { - width: '100%', - padding: '1.5rem' - }, - - [`& .${classes.cardRoot}`]: { - border: `1px solid #E5E5E5`, - marginBottom: '1rem', - '& *:focus': { - outline: 'none !important' - } - }, - - [`& .${classes.title}`]: { - color: '#3D4551', - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - padding: '1.5rem 1.5rem', - fontSize: '2rem', - textDecoration: 'none', - - '& > h4': { - wordBreak: 'break-all', - paddingRight: '2rem', - margin: '0' - }, - - '& > a, & > h4 a': { - color: 'white', - textDecoration: 'none' - } - }, - - [`& .${classes.section}`]: { - marginBottom: '1.5rem' - }, - - [`& .${classes.subtitle}`]: { - margin: 0, - padding: '0 0 0.2rem 0', - fontSize: '1.2rem', - fontWeight: 500, - color: '#3D4551' - }, - - [`& .${classes.inner}`]: { - padding: '1.5rem' - }, - - [`& .${classes.contentWrapper}`]: { - position: 'relative', - flex: '1 1 auto', - height: '100%', - display: 'flex', - flexFlow: 'column nowrap', - overflowY: 'hidden' - }, - - [`& .${classes.content}`]: { - display: 'flex', - flexFlow: 'row nowrap', - alignItems: 'stretch', - flex: '1', - overflowY: 'hidden', - justifyContent: 'center' - }, - - [`& .${classes.panel}`]: { - position: 'relative', - height: '100%', - overflowY: 'auto', - padding: '0 1rem 2rem 1rem' - }, - - [`& .${classes.backLink}`]: { - color: '#A9AEB1', - textDecoration: 'none', - fontWeight: 'bold', - fontSize: '16px', - marginLeft: '1rem' - }, - - [`& .${classes.linkSmall}`]: { - fontSize: '12px', - color: '#3D4551', - textDecoration: 'underline', - cursor: 'pointer', - background: 'none', - border: 'none', - padding: 0 - } -})); From 1054adb1eab475506259ccd2f295a2e5f2b32770 Mon Sep 17 00:00:00 2001 From: "Grayson, Matthew" Date: Mon, 5 Feb 2024 13:56:54 -0600 Subject: [PATCH 19/50] Add basic cve endpoints and test suite. --- backend/src/api/cve.ts | 134 +++++++++++++++++++++++++++++++++++++++ backend/test/cve.test.ts | 43 +++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 backend/src/api/cve.ts create mode 100644 backend/test/cve.test.ts diff --git a/backend/src/api/cve.ts b/backend/src/api/cve.ts new file mode 100644 index 000000000..1d614dc7a --- /dev/null +++ b/backend/src/api/cve.ts @@ -0,0 +1,134 @@ +import { + IsInt, + IsPositive, + IsString, + ValidateNested, + IsOptional, + IsUUID +} from 'class-validator'; +import { Type } from 'class-transformer'; +import { Cve, connectToDatabase } from '../models'; +import { validateBody, wrapHandler, NotFound } from './helpers'; + +class CveFilters { + @IsUUID() + @IsOptional() + uuid?: string; + + @IsOptional() + @IsString() + cve_name?: string; + + @IsOptional() + @IsString() + vuln_status?: string; +} + +class CveSearch { + @IsUUID() + @IsOptional() + uuid?: string; + + @IsOptional() + @IsString() + cve_name?: string; + + @IsOptional() + @IsString() + vuln_status?: string; + + @Type(() => CveFilters) + @ValidateNested() + @IsOptional() + filters?: CveFilters; + + @IsInt() + @IsPositive() + @IsOptional() + pageSize?: number; + + @IsInt() + @IsPositive() + @IsOptional() + page?: number; + + async getResults(event): Promise<[Cve[], number]> { + const filters = this.filters || new CveFilters(); + const query = Cve.createQueryBuilder('cve'); + + if (filters.cve_name) { + query.andWhere('cve.cve_name = :cve_name', { + cve_name: filters.cve_name + }); + } + if (filters.vuln_status) { + query.andWhere('cve.vuln_status = :vuln_status', { + vuln_status: filters.vuln_status + }); + } + + // Apply pagination to the query + const pageSize = this.pageSize || 25; + const skip = ((this.page || 1) - 1) * pageSize; + query.skip(skip).take(pageSize); + + // Execute the query + const [result, count] = await query.getManyAndCount(); + + return [result, count]; + } +} + +/** + * @swagger + * + * /cve: + * get: + * description: List CVE records by specifying a filter. + * tags: + * - CVE + */ +export const list = wrapHandler(async (event) => { + await connectToDatabase(); + const search = await validateBody(CveSearch, event.body); + const [result, count] = await search.getResults(event); + return { + statusCode: 200, + body: JSON.stringify({ + result, + count + }) + }; +}); + +//TODO: Refactor the get endpoint to search by id instead of cve_name. +// This requires creating a relationship between the cve and vulnerability tables. +/** + * @swagger + * + * /cve/{cve_name}: + * get: + * description: Retrieve a single CVE record by its name. + * tags: + * - CVE + * parameters: + * - name: cve_name + * in: path + * required: true + * schema: + * type: string + */ +export const get = wrapHandler(async (event) => { + await connectToDatabase(); + const cve_name = event.pathParameters?.cve_name; + const cve = await Cve.findOne({ where: { cve_name } }); + + if (!cve) { + return NotFound; + } + + return { + statusCode: 200, + body: JSON.stringify(cve) + }; +}); diff --git a/backend/test/cve.test.ts b/backend/test/cve.test.ts new file mode 100644 index 000000000..1c6de92ba --- /dev/null +++ b/backend/test/cve.test.ts @@ -0,0 +1,43 @@ +import * as request from 'supertest'; +import app from '../src/api/app'; +import { Cve, Organization, connectToDatabase } from '../src/models'; +import { createUserToken } from './util'; + +describe('cve', () => { + let connection; + let cve: Cve; + let organization: Organization; + beforeAll(async () => { + connection = await connectToDatabase(); + cve = Cve.create({ + cve_name: 'CVE-0001-0001' + }); + const organization = await Organization.create({ + name: 'test-' + Math.random(), + rootDomains: ['test-' + Math.random()], + ipBlocks: [], + isPassive: false + }).save(); + }); + + afterAll(async () => { + await Cve.delete(cve); + await Organization.delete(organization.id); + await connection.close(); + }); + describe('CVE API', () => { + it('should return a single CVE by cve_name', async () => { + const res = await request(app) + .get(`/api/cve/${cve.cve_name}`) + .set( + 'Authorization', + createUserToken({ + roles: [{ org: organization.id, role: 'user' }] + }) + ); + expect(res.statusCode).toEqual(200); + expect(res.body).toHaveProperty('cve'); + expect(res.body.cve.cve_name).toEqual(cve.cve_name); + }); + }); +}); From 33295731439271a2124720d0e16f477c7751c656 Mon Sep 17 00:00:00 2001 From: "Grayson, Matthew" Date: Mon, 5 Feb 2024 14:07:53 -0600 Subject: [PATCH 20/50] Hardcode organization and cve id used for testing. --- backend/test/cve.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/test/cve.test.ts b/backend/test/cve.test.ts index 1c6de92ba..34da2c71a 100644 --- a/backend/test/cve.test.ts +++ b/backend/test/cve.test.ts @@ -10,9 +10,11 @@ describe('cve', () => { beforeAll(async () => { connection = await connectToDatabase(); cve = Cve.create({ + cve_uid: 'test-' + Math.random(), cve_name: 'CVE-0001-0001' }); const organization = await Organization.create({ + id: 'test-' + Math.random(), name: 'test-' + Math.random(), rootDomains: ['test-' + Math.random()], ipBlocks: [], @@ -21,7 +23,7 @@ describe('cve', () => { }); afterAll(async () => { - await Cve.delete(cve); + await Cve.delete(cve.cve_uid); await Organization.delete(organization.id); await connection.close(); }); From 15c99822b26ccd52e366fcc4763a0aa966777786 Mon Sep 17 00:00:00 2001 From: "Grayson, Matthew" Date: Mon, 5 Feb 2024 16:26:11 -0600 Subject: [PATCH 21/50] Add authenticatedRoute for cves endpoint; rename api/cve.ts to api/cves.ts to match other files. --- backend/src/api/app.ts | 2 ++ backend/src/api/{cve.ts => cves.ts} | 32 ++++++---------------- backend/test/{cve.test.ts => cves.test.ts} | 25 +++++++++-------- 3 files changed, 24 insertions(+), 35 deletions(-) rename backend/src/api/{cve.ts => cves.ts} (82%) rename backend/test/{cve.test.ts => cves.test.ts} (65%) diff --git a/backend/src/api/app.ts b/backend/src/api/app.ts index b6b52f2e7..6ebc5971e 100644 --- a/backend/src/api/app.ts +++ b/backend/src/api/app.ts @@ -5,6 +5,7 @@ import * as cors from 'cors'; import * as helmet from 'helmet'; import { handler as healthcheck } from './healthcheck'; import * as auth from './auth'; +import * as cves from './cves'; import * as domains from './domains'; import * as search from './search'; import * as vulnerabilities from './vulnerabilities'; @@ -287,6 +288,7 @@ authenticatedRoute.delete('/api-keys/:keyId', handlerToExpress(apiKeys.del)); authenticatedRoute.post('/search', handlerToExpress(search.search)); authenticatedRoute.post('/search/export', handlerToExpress(search.export_)); +authenticatedRoute.get('/cves/:cve_name', handlerToExpress(cves.get)); authenticatedRoute.post('/domain/search', handlerToExpress(domains.list)); authenticatedRoute.post('/domain/export', handlerToExpress(domains.export_)); authenticatedRoute.get('/domain/:domainId', handlerToExpress(domains.get)); diff --git a/backend/src/api/cve.ts b/backend/src/api/cves.ts similarity index 82% rename from backend/src/api/cve.ts rename to backend/src/api/cves.ts index 1d614dc7a..a150701d7 100644 --- a/backend/src/api/cve.ts +++ b/backend/src/api/cves.ts @@ -79,34 +79,12 @@ class CveSearch { } } -/** - * @swagger - * - * /cve: - * get: - * description: List CVE records by specifying a filter. - * tags: - * - CVE - */ -export const list = wrapHandler(async (event) => { - await connectToDatabase(); - const search = await validateBody(CveSearch, event.body); - const [result, count] = await search.getResults(event); - return { - statusCode: 200, - body: JSON.stringify({ - result, - count - }) - }; -}); - //TODO: Refactor the get endpoint to search by id instead of cve_name. // This requires creating a relationship between the cve and vulnerability tables. /** * @swagger * - * /cve/{cve_name}: + * /cves/{cve_name}: * get: * description: Retrieve a single CVE record by its name. * tags: @@ -121,10 +99,16 @@ export const list = wrapHandler(async (event) => { export const get = wrapHandler(async (event) => { await connectToDatabase(); const cve_name = event.pathParameters?.cve_name; + console.log('cve_name:', cve_name); + const cve = await Cve.findOne({ where: { cve_name } }); + console.log('Cve.findOne result:', cve); if (!cve) { - return NotFound; + return { + statusCode: 404, + body: JSON.stringify(Error) + }; } return { diff --git a/backend/test/cve.test.ts b/backend/test/cves.test.ts similarity index 65% rename from backend/test/cve.test.ts rename to backend/test/cves.test.ts index 34da2c71a..cd028487f 100644 --- a/backend/test/cve.test.ts +++ b/backend/test/cves.test.ts @@ -3,23 +3,25 @@ import app from '../src/api/app'; import { Cve, Organization, connectToDatabase } from '../src/models'; import { createUserToken } from './util'; -describe('cve', () => { +describe('cves', () => { let connection; let cve: Cve; let organization: Organization; beforeAll(async () => { connection = await connectToDatabase(); cve = Cve.create({ - cve_uid: 'test-' + Math.random(), + cve_uid: '00000000-0000-0000-0000-000000000000', cve_name: 'CVE-0001-0001' }); - const organization = await Organization.create({ - id: 'test-' + Math.random(), + await cve.save(); + organization = Organization.create({ + id: '00000000-0000-0000-0000-000000000000', name: 'test-' + Math.random(), rootDomains: ['test-' + Math.random()], ipBlocks: [], isPassive: false - }).save(); + }); + await organization.save(); }); afterAll(async () => { @@ -29,17 +31,18 @@ describe('cve', () => { }); describe('CVE API', () => { it('should return a single CVE by cve_name', async () => { - const res = await request(app) - .get(`/api/cve/${cve.cve_name}`) + const response = await request(app) + .get(`/cves/${cve.cve_name}`) .set( 'Authorization', createUserToken({ roles: [{ org: organization.id, role: 'user' }] }) - ); - expect(res.statusCode).toEqual(200); - expect(res.body).toHaveProperty('cve'); - expect(res.body.cve.cve_name).toEqual(cve.cve_name); + ) + .send({}) + .expect(200); + expect(response.body).toHaveProperty('cve'); + expect(response.body.cve.cve_name).toEqual(cve.cve_name); }); }); }); From 801ad229c804059534a05c25188db57f3d20bf1a Mon Sep 17 00:00:00 2001 From: "Grayson, Matthew" Date: Mon, 5 Feb 2024 17:24:53 -0600 Subject: [PATCH 22/50] Update expect statements to match cve endpoint response. --- backend/test/cves.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/test/cves.test.ts b/backend/test/cves.test.ts index cd028487f..667aeb46b 100644 --- a/backend/test/cves.test.ts +++ b/backend/test/cves.test.ts @@ -41,8 +41,7 @@ describe('cves', () => { ) .send({}) .expect(200); - expect(response.body).toHaveProperty('cve'); - expect(response.body.cve.cve_name).toEqual(cve.cve_name); + expect(response.body.cve_name).toEqual(cve.cve_name); }); }); }); From b19ccd3a6e3b9e7153e5b7d0bb4ae023803f7c14 Mon Sep 17 00:00:00 2001 From: "Grayson, Matthew" Date: Tue, 6 Feb 2024 09:23:34 -0600 Subject: [PATCH 23/50] Add get by id endpoint and rename get by name endpoint. --- backend/src/api/app.ts | 6 +++++- backend/src/api/cves.ts | 43 +++++++++++++++++++++++++++++++++---- backend/src/api/products.ts | 0 3 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 backend/src/api/products.ts diff --git a/backend/src/api/app.ts b/backend/src/api/app.ts index 6ebc5971e..a2c8cacdf 100644 --- a/backend/src/api/app.ts +++ b/backend/src/api/app.ts @@ -288,7 +288,11 @@ authenticatedRoute.delete('/api-keys/:keyId', handlerToExpress(apiKeys.del)); authenticatedRoute.post('/search', handlerToExpress(search.search)); authenticatedRoute.post('/search/export', handlerToExpress(search.export_)); -authenticatedRoute.get('/cves/:cve_name', handlerToExpress(cves.get)); +authenticatedRoute.get('/cves/:cve_uid', handlerToExpress(cves.get)); +authenticatedRoute.get( + '/cves/name/:cve_name', + handlerToExpress(cves.getByName) +); authenticatedRoute.post('/domain/search', handlerToExpress(domains.list)); authenticatedRoute.post('/domain/export', handlerToExpress(domains.export_)); authenticatedRoute.get('/domain/:domainId', handlerToExpress(domains.get)); diff --git a/backend/src/api/cves.ts b/backend/src/api/cves.ts index a150701d7..e4937a880 100644 --- a/backend/src/api/cves.ts +++ b/backend/src/api/cves.ts @@ -79,12 +79,47 @@ class CveSearch { } } -//TODO: Refactor the get endpoint to search by id instead of cve_name. -// This requires creating a relationship between the cve and vulnerability tables. /** * @swagger * - * /cves/{cve_name}: + * /cves/{cve_uid}: + * get: + * description: Retrieve a single CVE record by its ID. + * tags: + * - CVE + * parameters: + * - name: cve_uid + * in: path + * required: true + * schema: + * type: string + */ +export const get = wrapHandler(async (event) => { + await connectToDatabase(); + const cve_uid = event.pathParameters?.cve_uid; + console.log('cve_uid:', cve_uid); + + const cve = await Cve.findOne({ where: { cve_uid: cve_uid } }); + console.log('Cve.findOne result:', cve); + + if (!cve) { + return { + statusCode: 404, + body: JSON.stringify(Error) + }; + } + + return { + statusCode: 200, + body: JSON.stringify(cve) + }; +}); + +//TODO: Remove getByName endpoint once the vulnerability and cve tables are related +/** + * @swagger + * + * /cves/name/{cve_name}: * get: * description: Retrieve a single CVE record by its name. * tags: @@ -96,7 +131,7 @@ class CveSearch { * schema: * type: string */ -export const get = wrapHandler(async (event) => { +export const getByName = wrapHandler(async (event) => { await connectToDatabase(); const cve_name = event.pathParameters?.cve_name; console.log('cve_name:', cve_name); diff --git a/backend/src/api/products.ts b/backend/src/api/products.ts new file mode 100644 index 000000000..e69de29bb From fe1948d7e8b7dba94a6b639bc29c7a130aee10ef Mon Sep 17 00:00:00 2001 From: "Grayson, Matthew" Date: Tue, 6 Feb 2024 09:25:03 -0600 Subject: [PATCH 24/50] Add future todos. --- backend/src/models/cve.ts | 3 ++- backend/src/models/product-info.ts | 7 +++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/src/models/cve.ts b/backend/src/models/cve.ts index 03f068c44..3240e012e 100644 --- a/backend/src/models/cve.ts +++ b/backend/src/models/cve.ts @@ -11,11 +11,12 @@ import { } from 'typeorm'; import { ProductInfo } from './product-info'; +//TODO: Refactor column names to camelCase to match the rest of the codebase? @Entity() @Unique(['cve_name']) export class Cve extends BaseEntity { @PrimaryGeneratedColumn('uuid') - cve_uid: string; + cve_uid: string; //TODO: Refactor to id to match other UUIDs? @Column({ nullable: true }) cve_name: string; diff --git a/backend/src/models/product-info.ts b/backend/src/models/product-info.ts index 91e5397a2..581215c3a 100644 --- a/backend/src/models/product-info.ts +++ b/backend/src/models/product-info.ts @@ -1,21 +1,20 @@ -import { mapping } from 'src/tasks/censys/mapping'; import { Entity, PrimaryGeneratedColumn, Column, - CreateDateColumn, - UpdateDateColumn, ManyToMany, BaseEntity, Unique } from 'typeorm'; import { Cve } from './cve'; +//TODO: Refactor column names to camelCase to match the rest of the codebase? +//TODO: Refactor table name to product for brevity? @Entity() @Unique(['cpe_product_name', 'version_number', 'vender']) export class ProductInfo extends BaseEntity { @PrimaryGeneratedColumn('uuid') - id: string; //TODO: change this to something else?? + id: string; @Column() cpe_product_name: string; From 232fbbf51d893036cbcae80c6203e2b9656019fd Mon Sep 17 00:00:00 2001 From: "Grayson, Matthew" Date: Tue, 6 Feb 2024 09:35:03 -0600 Subject: [PATCH 25/50] Add test for get cves/:cve_uid endpoint. --- backend/test/cves.test.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/backend/test/cves.test.ts b/backend/test/cves.test.ts index 667aeb46b..2d98a3686 100644 --- a/backend/test/cves.test.ts +++ b/backend/test/cves.test.ts @@ -29,10 +29,25 @@ describe('cves', () => { await Organization.delete(organization.id); await connection.close(); }); + describe('CVE API', () => { + it('should return a single CVE by cve_uid', async () => { + const response = await request(app) + .get(`/cves/${cve.cve_uid}`) + .set( + 'Authorization', + createUserToken({ + roles: [{ org: organization.id, role: 'user' }] + }) + ) + .send({}) + .expect(200); + expect(response.body.cve_uid).toEqual(cve.cve_uid); + }); + }); describe('CVE API', () => { it('should return a single CVE by cve_name', async () => { const response = await request(app) - .get(`/cves/${cve.cve_name}`) + .get(`/cves/name/${cve.cve_name}`) .set( 'Authorization', createUserToken({ From 276fc93a99637ad9d1835c307a036b480b44116e Mon Sep 17 00:00:00 2001 From: "Grayson, Matthew" Date: Tue, 6 Feb 2024 13:47:17 -0600 Subject: [PATCH 26/50] Create API and asociated test for product_info table; update todos in models/product-info.ts. --- backend/src/api/app.ts | 2 + backend/src/api/cpes.ts | 133 +++++++++++++++++++++++++++++ backend/src/api/products.ts | 0 backend/src/models/product-info.ts | 2 +- backend/test/cpes.test.ts | 50 +++++++++++ 5 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 backend/src/api/cpes.ts delete mode 100644 backend/src/api/products.ts create mode 100644 backend/test/cpes.test.ts diff --git a/backend/src/api/app.ts b/backend/src/api/app.ts index a2c8cacdf..b9e4e99fd 100644 --- a/backend/src/api/app.ts +++ b/backend/src/api/app.ts @@ -5,6 +5,7 @@ import * as cors from 'cors'; import * as helmet from 'helmet'; import { handler as healthcheck } from './healthcheck'; import * as auth from './auth'; +import * as cpes from './cpes'; import * as cves from './cves'; import * as domains from './domains'; import * as search from './search'; @@ -288,6 +289,7 @@ authenticatedRoute.delete('/api-keys/:keyId', handlerToExpress(apiKeys.del)); authenticatedRoute.post('/search', handlerToExpress(search.search)); authenticatedRoute.post('/search/export', handlerToExpress(search.export_)); +authenticatedRoute.get('/cpes/:id', handlerToExpress(cpes.get)); authenticatedRoute.get('/cves/:cve_uid', handlerToExpress(cves.get)); authenticatedRoute.get( '/cves/name/:cve_name', diff --git a/backend/src/api/cpes.ts b/backend/src/api/cpes.ts new file mode 100644 index 000000000..90bd26144 --- /dev/null +++ b/backend/src/api/cpes.ts @@ -0,0 +1,133 @@ +import { + IsInt, + IsPositive, + IsString, + ValidateNested, + IsOptional, + IsUUID +} from 'class-validator'; +import { Type } from 'class-transformer'; +import { ProductInfo, connectToDatabase } from '../models'; +import { wrapHandler, NotFound } from './helpers'; +import { SelectQueryBuilder } from 'typeorm'; + +class CpeFilters { + @IsOptional() + @IsUUID() + id?: string; + + @IsOptional() + @IsString() + cpe_product_name?: string; + + @IsOptional() + @IsString() + version_number?: string; + + @IsOptional() + @IsString() + vender?: string; +} + +class CpeSearch { + @ValidateNested() + @Type(() => CpeFilters) + filters: CpeFilters; + + @IsOptional() + @IsInt() + @IsPositive() + page?: number; + + @IsOptional() + @IsInt() + @IsPositive() + pageSize?: number; + + constructor(filters: CpeFilters, page?: number, pageSize?: number) { + this.filters = filters; + this.page = page; + this.pageSize = pageSize; + } + + async filterResultQueryset(qs: SelectQueryBuilder) { + if (this.filters.id) { + qs = qs.andWhere('product_info.id = :id', { id: this.filters.id }); + } + if (this.filters.cpe_product_name) { + qs = qs.andWhere('product_info.cpe_product_name = :cpe_product_name', { + cpe_product_name: this.filters.cpe_product_name + }); + } + if (this.filters.version_number) { + qs = qs.andWhere('product_info.version_number = :version_number', { + version_number: this.filters.version_number + }); + } + if (this.filters.vender) { + qs = qs.andWhere('product_info.vender = :vender', { + vender: this.filters.vender + }); + } + return qs; + } + + async getResults(): Promise<[ProductInfo[], number]> { + const connection = await connectToDatabase(); + let qs = connection + .getRepository(ProductInfo) + .createQueryBuilder('product_info'); + qs = await this.filterResultQueryset(qs); + + const total = await qs.getCount(); + + if (this.page && this.pageSize) { + qs = qs.skip((this.page - 1) * this.pageSize).take(this.pageSize); + } + + const results = await qs.getMany(); + + return [results, total]; + } +} + +/** + * @swagger + * /cpes/{id}: + * get: + * summary: Retrieve a CPE by ID + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: A CPE object + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ProductInfo' + * 404: + * description: CPE not found + */ +export const get = wrapHandler(async (event) => { + const connection = await connectToDatabase(); + const repository = connection.getRepository(ProductInfo); + + const id = event.pathParameters?.id; + if (!id) { + return NotFound; + } + + const productInfo = await repository.findOne(id); + if (!productInfo) { + return NotFound; + } + + return { + statusCode: 200, + body: JSON.stringify(productInfo) + }; +}); diff --git a/backend/src/api/products.ts b/backend/src/api/products.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/backend/src/models/product-info.ts b/backend/src/models/product-info.ts index 581215c3a..8df42b09c 100644 --- a/backend/src/models/product-info.ts +++ b/backend/src/models/product-info.ts @@ -9,7 +9,7 @@ import { import { Cve } from './cve'; //TODO: Refactor column names to camelCase to match the rest of the codebase? -//TODO: Refactor table name to product for brevity? +//TODO: Refactor table name to product or cpe for brevity? @Entity() @Unique(['cpe_product_name', 'version_number', 'vender']) export class ProductInfo extends BaseEntity { diff --git a/backend/test/cpes.test.ts b/backend/test/cpes.test.ts new file mode 100644 index 000000000..8d1f9010d --- /dev/null +++ b/backend/test/cpes.test.ts @@ -0,0 +1,50 @@ +import * as request from 'supertest'; +import app from '../src/api/app'; +import { Organization, ProductInfo, connectToDatabase } from '../src/models'; +import { createUserToken } from './util'; + +describe('cpes', () => { + let connection; + let organization: Organization; + let productInfo: ProductInfo; + beforeAll(async () => { + connection = await connectToDatabase(); + productInfo = ProductInfo.create({ + id: '00000000-0000-0000-0000-000000000000', + last_seen: new Date(), + cpe_product_name: 'Test Product', + version_number: '1.0.0', + vender: 'Test Vender' + }); + await productInfo.save(); + organization = Organization.create({ + id: '00000000-0000-0000-0000-000000000000', + name: 'test-' + Math.random(), + rootDomains: ['test-' + Math.random()], + ipBlocks: [], + isPassive: false + }); + await organization.save(); + }); + + afterAll(async () => { + await ProductInfo.delete(productInfo.id); + await connection.close(); + }); + + describe('CPE API', () => { + it('should return a single CPE by id', async () => { + const response = await request(app) + .get(`/cpes/${productInfo.id}`) + .set( + 'Authorization', + createUserToken({ + roles: [{ org: organization.id, role: 'user' }] + }) + ) + .send({}) + .expect(200); + expect(response.body.id).toEqual(productInfo.id); + }); + }); +}); From cb3e0477e29b45a59ea0c355f2544055e121fa67 Mon Sep 17 00:00:00 2001 From: "Grayson, Matthew" Date: Tue, 6 Feb 2024 14:04:37 -0600 Subject: [PATCH 27/50] Join product_info entries to results from /vulnerabilities/:id endpoint. --- backend/src/api/cves.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/backend/src/api/cves.ts b/backend/src/api/cves.ts index e4937a880..5947be57b 100644 --- a/backend/src/api/cves.ts +++ b/backend/src/api/cves.ts @@ -56,6 +56,8 @@ class CveSearch { const filters = this.filters || new CveFilters(); const query = Cve.createQueryBuilder('cve'); + query.leftJoinAndSelect('cve.product_info', 'product_info'); + if (filters.cve_name) { query.andWhere('cve.cve_name = :cve_name', { cve_name: filters.cve_name @@ -97,25 +99,28 @@ class CveSearch { export const get = wrapHandler(async (event) => { await connectToDatabase(); const cve_uid = event.pathParameters?.cve_uid; - console.log('cve_uid:', cve_uid); - const cve = await Cve.findOne({ where: { cve_uid: cve_uid } }); - console.log('Cve.findOne result:', cve); + // Create an instance of CveSearch and call getResults + const cveSearch = new CveSearch(); + cveSearch.uuid = cve_uid; + const [cves, count] = await cveSearch.getResults(event); - if (!cve) { + // Check if any CVEs were found + if (count === 0) { return { statusCode: 404, body: JSON.stringify(Error) }; } + // Return the first CVE found return { statusCode: 200, - body: JSON.stringify(cve) + body: JSON.stringify(cves[0]) }; }); -//TODO: Remove getByName endpoint once the vulnerability and cve tables are related +//TODO: Remove getByName endpoint once a one-to-one relationship is established between vulnerability.cve and cve.cve_id /** * @swagger * From 084c36e7955ff01817e5d2b79fa20452a7e2166e Mon Sep 17 00:00:00 2001 From: "Grayson, Matthew" Date: Tue, 6 Feb 2024 14:04:37 -0600 Subject: [PATCH 28/50] Join product_info entries to results from /cves/:cve_uid endpoint. --- backend/src/api/cves.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/backend/src/api/cves.ts b/backend/src/api/cves.ts index e4937a880..5947be57b 100644 --- a/backend/src/api/cves.ts +++ b/backend/src/api/cves.ts @@ -56,6 +56,8 @@ class CveSearch { const filters = this.filters || new CveFilters(); const query = Cve.createQueryBuilder('cve'); + query.leftJoinAndSelect('cve.product_info', 'product_info'); + if (filters.cve_name) { query.andWhere('cve.cve_name = :cve_name', { cve_name: filters.cve_name @@ -97,25 +99,28 @@ class CveSearch { export const get = wrapHandler(async (event) => { await connectToDatabase(); const cve_uid = event.pathParameters?.cve_uid; - console.log('cve_uid:', cve_uid); - const cve = await Cve.findOne({ where: { cve_uid: cve_uid } }); - console.log('Cve.findOne result:', cve); + // Create an instance of CveSearch and call getResults + const cveSearch = new CveSearch(); + cveSearch.uuid = cve_uid; + const [cves, count] = await cveSearch.getResults(event); - if (!cve) { + // Check if any CVEs were found + if (count === 0) { return { statusCode: 404, body: JSON.stringify(Error) }; } + // Return the first CVE found return { statusCode: 200, - body: JSON.stringify(cve) + body: JSON.stringify(cves[0]) }; }); -//TODO: Remove getByName endpoint once the vulnerability and cve tables are related +//TODO: Remove getByName endpoint once a one-to-one relationship is established between vulnerability.cve and cve.cve_id /** * @swagger * From 8afb1df6b012b0b389254015360ae4d859d6d7b3 Mon Sep 17 00:00:00 2001 From: "Grayson, Matthew" Date: Tue, 6 Feb 2024 14:37:40 -0600 Subject: [PATCH 29/50] Add left join for service table to /vulnerabilities/:id endpoint response; fix left join for /cves/:cve_uid endpoint. --- backend/src/api/cves.ts | 7 ++++--- backend/src/api/vulnerabilities.ts | 3 ++- backend/test/cves.test.ts | 1 + 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/backend/src/api/cves.ts b/backend/src/api/cves.ts index 5947be57b..dd73c6f45 100644 --- a/backend/src/api/cves.ts +++ b/backend/src/api/cves.ts @@ -54,9 +54,10 @@ class CveSearch { async getResults(event): Promise<[Cve[], number]> { const filters = this.filters || new CveFilters(); - const query = Cve.createQueryBuilder('cve'); - - query.leftJoinAndSelect('cve.product_info', 'product_info'); + const query = Cve.createQueryBuilder('cve').leftJoinAndSelect( + 'cve.product_info', + 'product_info' + ); if (filters.cve_name) { query.andWhere('cve.cve_name = :cve_name', { diff --git a/backend/src/api/vulnerabilities.ts b/backend/src/api/vulnerabilities.ts index 290647350..7ad0e9242 100644 --- a/backend/src/api/vulnerabilities.ts +++ b/backend/src/api/vulnerabilities.ts @@ -170,7 +170,8 @@ class VulnerabilitySearch { : `vulnerability.${this.sort}`; let qs = Vulnerability.createQueryBuilder('vulnerability') .leftJoinAndSelect('vulnerability.domain', 'domain') - .leftJoinAndSelect('domain.organization', 'organization'); + .leftJoinAndSelect('domain.organization', 'organization') + .leftJoinAndSelect('vulnerability.service', 'service'); if (groupBy) { qs = qs diff --git a/backend/test/cves.test.ts b/backend/test/cves.test.ts index 2d98a3686..9de98ae25 100644 --- a/backend/test/cves.test.ts +++ b/backend/test/cves.test.ts @@ -3,6 +3,7 @@ import app from '../src/api/app'; import { Cve, Organization, connectToDatabase } from '../src/models'; import { createUserToken } from './util'; +// TODO: Add test for joining product_info describe('cves', () => { let connection; let cve: Cve; From c38f76a09a9c59417a63e31d4dcbcc65f00343c1 Mon Sep 17 00:00:00 2001 From: Amelia Vance Date: Tue, 6 Feb 2024 15:56:38 -0500 Subject: [PATCH 30/50] Add cves name api call to Vulnerability.tsx --- .../src/pages/Vulnerability/Vulnerability.tsx | 117 ++++++++++++------ 1 file changed, 82 insertions(+), 35 deletions(-) diff --git a/frontend/src/pages/Vulnerability/Vulnerability.tsx b/frontend/src/pages/Vulnerability/Vulnerability.tsx index e6a8a4d8b..77b6353ff 100644 --- a/frontend/src/pages/Vulnerability/Vulnerability.tsx +++ b/frontend/src/pages/Vulnerability/Vulnerability.tsx @@ -22,6 +22,7 @@ import { import { getSeverityColor } from 'pages/Risk/utils'; import { useAuthContext } from 'context'; import { Vulnerability as VulnerabilityType } from 'types'; +import { Cve } from 'types/cve'; const formatDate = (date: string | null) => { if (date) { @@ -34,41 +35,40 @@ export const Vulnerability: React.FC = () => { const { vulnerabilityId } = useParams(); const { apiGet } = useAuthContext(); const [vulnerability, setVulnerability] = useState(); - + const [cve, setCve] = useState(); const history = useHistory(); + + const getCve = useCallback( + (cveName: string | null) => { + return apiGet(`/cves/name/${cveName}`).then( + (result) => { + // getProductInfo(result.cve_uid); + setCve(result); + return true; + }, + (e) => { + return false; + } + ); + }, // eslint-disable-next-line react-hooks/exhaustive-deps + [apiGet] + ); + const fetchVulnerability = useCallback(async () => { try { const result = await apiGet( `/vulnerabilities/${vulnerabilityId}` ); setVulnerability(result); + getCve(result.cve); } catch (e) { console.error(e); } - }, [vulnerabilityId, apiGet]); - - // const updateVulnerability = async (body: { [key: string]: string }) => { - // try { - // if (!vulnerability) return; - // const res = await apiPut( - // '/vulnerabilities/' + vulnerability.id, - // { - // body: body - // } - // ); - // setVulnerability({ - // ...vulnerability, - // state: res.state, - // substate: res.substate, - // actions: res.actions - // }); - // } catch (e) { - // console.error(e); - // } - // }; + }, [vulnerabilityId, apiGet, getCve]); useEffect(() => { fetchVulnerability(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [fetchVulnerability]); if (!vulnerability) return <>No Vulnerabilities; @@ -348,14 +348,14 @@ export const Vulnerability: React.FC = () => { return ( - + CVSS 3.x Severity & Metrics - + @@ -364,28 +364,51 @@ export const Vulnerability: React.FC = () => { variant="subtitle2" sx={{ verticalAlign: 'top' }} > - NIST: + NIST:{' '} + + + {cve?.cvss_v3_source != null + ? cve?.cvss_v3_source.split('@')[0].toUpperCase() + : null} Base Score: + + {' ' + cve?.cvss_v3_base_score} + - - - Vector: + + + Vector: + {cve?.cvss_v3_vector_string} - + CVSS 2.0 Severity & Metrics @@ -394,17 +417,41 @@ export const Vulnerability: React.FC = () => { variant="subtitle2" sx={{ verticalAlign: 'top' }} > - NIST: + NIST:{' '} + + + {cve?.cvss_v2_source != null + ? cve?.cvss_v2_source.split('@')[0].toUpperCase() + : null}
- Base Score: + Base Score:{' '} + + + {cve?.cvss_v2_base_score} - + - Vector: + Vector:{' '} + + + {cve?.cvss_v2_vector_string} From 1931f3d83c3677f9d62a4550f2e679605dfe280f Mon Sep 17 00:00:00 2001 From: Amelia Vance Date: Tue, 6 Feb 2024 16:16:55 -0500 Subject: [PATCH 31/50] Add cve interface to types/cve.ts --- frontend/src/pages/Vulnerability/Vulnerability.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frontend/src/pages/Vulnerability/Vulnerability.tsx b/frontend/src/pages/Vulnerability/Vulnerability.tsx index 77b6353ff..966f4347c 100644 --- a/frontend/src/pages/Vulnerability/Vulnerability.tsx +++ b/frontend/src/pages/Vulnerability/Vulnerability.tsx @@ -397,6 +397,13 @@ export const Vulnerability: React.FC = () => { sx={{ overflowWrap: 'break-word' }} > Vector: + + {cve?.cvss_v3_vector_string}
From 2d6e6eceb972dec41ffdb9907f1f0b8f16822cf7 Mon Sep 17 00:00:00 2001 From: Amelia Vance Date: Tue, 6 Feb 2024 16:17:49 -0500 Subject: [PATCH 32/50] Add cve interface to types/cve.ts --- frontend/src/types/cve.ts | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 frontend/src/types/cve.ts diff --git a/frontend/src/types/cve.ts b/frontend/src/types/cve.ts new file mode 100644 index 000000000..f3e8bd9b4 --- /dev/null +++ b/frontend/src/types/cve.ts @@ -0,0 +1,36 @@ +export interface Cve { + cve_uid: string; + cve_name: string; + published_date: string; + last_modified_date: string; + vuln_status: string; + description: string; + cvss_v2_source: string | null; + cvss_v2_type: string | null; + cvss_v2_version: string | null; + cvss_v2_vector_string: string | null; + cvss_v2_base_score: string | null; + cvss_v2_base_severity: string | null; + cvss_v2_exploitability_score: string | null; + cvss_v2_impact_score: string | null; + cvss_v3_source: string | null; + cvss_v3_type: string | null; + cvss_v3_version: string | null; + cvss_v3_vector_string: string | null; + cvss_v3_base_score: string | null; + cvss_v3_base_severity: string | null; + cvss_v3_exploitability_score: string | null; + cvss_v3_impact_score: string | null; + cvss_v4_source: string | null; + cvss_v4_type: string | null; + cvss_v4_version: string | null; + cvss_v4_vector_string: string | null; + cvss_v4_base_score: string | null; + cvss_v4_base_severity: string | null; + cvss_v4_exploitability_score: string | null; + cvss_v4_impact_score: string | null; + weaknesses: string; + reference_urls: string; + cpe_list: string; +} + From b6e1b6522a028d1f6a6c1bfd9a8548c0126600dd Mon Sep 17 00:00:00 2001 From: "Grayson, Matthew" Date: Tue, 6 Feb 2024 15:42:50 -0600 Subject: [PATCH 33/50] Create cve interface in frontend/src/types. --- .../src/pages/Vulnerability/Vulnerability.tsx | 5 ++--- frontend/src/types/cpe.ts | 9 +++++++++ frontend/src/types/cve.ts | 19 ++++++++++--------- frontend/src/types/index.ts | 2 ++ 4 files changed, 23 insertions(+), 12 deletions(-) create mode 100644 frontend/src/types/cpe.ts diff --git a/frontend/src/pages/Vulnerability/Vulnerability.tsx b/frontend/src/pages/Vulnerability/Vulnerability.tsx index 966f4347c..d43559667 100644 --- a/frontend/src/pages/Vulnerability/Vulnerability.tsx +++ b/frontend/src/pages/Vulnerability/Vulnerability.tsx @@ -21,8 +21,7 @@ import { } from '@mui/material'; import { getSeverityColor } from 'pages/Risk/utils'; import { useAuthContext } from 'context'; -import { Vulnerability as VulnerabilityType } from 'types'; -import { Cve } from 'types/cve'; +import { Cve as CveType, Vulnerability as VulnerabilityType } from 'types'; const formatDate = (date: string | null) => { if (date) { @@ -35,7 +34,7 @@ export const Vulnerability: React.FC = () => { const { vulnerabilityId } = useParams(); const { apiGet } = useAuthContext(); const [vulnerability, setVulnerability] = useState(); - const [cve, setCve] = useState(); + const [cve, setCve] = useState(); const history = useHistory(); const getCve = useCallback( diff --git a/frontend/src/types/cpe.ts b/frontend/src/types/cpe.ts new file mode 100644 index 000000000..37d81d50e --- /dev/null +++ b/frontend/src/types/cpe.ts @@ -0,0 +1,9 @@ +import { Cve } from './cve'; +export interface Cpe { + id: string; + cpe_product_name: string; + version_number: string; + vender: string; + last_seen: Date; + cve: Cve[]; +} diff --git a/frontend/src/types/cve.ts b/frontend/src/types/cve.ts index f3e8bd9b4..ec14e9bfc 100644 --- a/frontend/src/types/cve.ts +++ b/frontend/src/types/cve.ts @@ -1,10 +1,11 @@ +import { Cpe } from './cpe'; export interface Cve { cve_uid: string; - cve_name: string; - published_date: string; - last_modified_date: string; - vuln_status: string; - description: string; + cve_name: string | null; + published_date: Date; + last_modified_date: Date; + vuln_status: string | null; + description: string | null; cvss_v2_source: string | null; cvss_v2_type: string | null; cvss_v2_version: string | null; @@ -29,8 +30,8 @@ export interface Cve { cvss_v4_base_severity: string | null; cvss_v4_exploitability_score: string | null; cvss_v4_impact_score: string | null; - weaknesses: string; - reference_urls: string; - cpe_list: string; + weaknesses: string[] | null; + reference_urls: string[] | null; + cpe_list: string[] | null; + product_info: Cpe[]; } - diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index fe364c739..67b495b9b 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -1,4 +1,6 @@ import { SortingRule, Filters } from 'react-table'; +export * from './cpe'; +export * from './cve'; export * from './domain'; export * from './vulnerability'; export * from './scan'; From 4a0163385505f4c4947c0bafa000c662ef4d29e0 Mon Sep 17 00:00:00 2001 From: "Grayson, Matthew" Date: Tue, 6 Feb 2024 15:50:55 -0600 Subject: [PATCH 34/50] Remove redundant left join of service table in vulnerabilities api. --- backend/src/api/vulnerabilities.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/backend/src/api/vulnerabilities.ts b/backend/src/api/vulnerabilities.ts index 7ad0e9242..218df49b6 100644 --- a/backend/src/api/vulnerabilities.ts +++ b/backend/src/api/vulnerabilities.ts @@ -186,9 +186,7 @@ class VulnerabilitySearch { ]) .orderBy('cnt', 'DESC'); } else { - qs = qs - .leftJoinAndSelect('vulnerability.service', 'service') - .orderBy(sort, this.order); + qs = qs.orderBy(sort, this.order); } if (pageSize !== -1) { From af63f1e9aaa8d13f761667cd1cee921f72ab882c Mon Sep 17 00:00:00 2001 From: "Grayson, Matthew" Date: Tue, 6 Feb 2024 16:32:45 -0600 Subject: [PATCH 35/50] Refactor types/cpe to product-info to match database model. --- frontend/src/types/cve.ts | 4 ++-- frontend/src/types/index.ts | 2 +- frontend/src/types/{cpe.ts => product-info.ts} | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename frontend/src/types/{cpe.ts => product-info.ts} (82%) diff --git a/frontend/src/types/cve.ts b/frontend/src/types/cve.ts index ec14e9bfc..3a32216be 100644 --- a/frontend/src/types/cve.ts +++ b/frontend/src/types/cve.ts @@ -1,4 +1,4 @@ -import { Cpe } from './cpe'; +import { ProductInfo } from './product-info'; export interface Cve { cve_uid: string; cve_name: string | null; @@ -33,5 +33,5 @@ export interface Cve { weaknesses: string[] | null; reference_urls: string[] | null; cpe_list: string[] | null; - product_info: Cpe[]; + product_info: ProductInfo[]; } diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 67b495b9b..88f210a31 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -1,5 +1,5 @@ import { SortingRule, Filters } from 'react-table'; -export * from './cpe'; +export * from './product-info'; export * from './cve'; export * from './domain'; export * from './vulnerability'; diff --git a/frontend/src/types/cpe.ts b/frontend/src/types/product-info.ts similarity index 82% rename from frontend/src/types/cpe.ts rename to frontend/src/types/product-info.ts index 37d81d50e..e563ef848 100644 --- a/frontend/src/types/cpe.ts +++ b/frontend/src/types/product-info.ts @@ -1,5 +1,5 @@ import { Cve } from './cve'; -export interface Cpe { +export interface ProductInfo { id: string; cpe_product_name: string; version_number: string; From 1957b48cfcb6b4ca75efc4d88ddfeb41c6ac58cc Mon Sep 17 00:00:00 2001 From: "Grayson, Matthew" Date: Tue, 6 Feb 2024 16:44:57 -0600 Subject: [PATCH 36/50] Apply getResults method to /cves/name endpoint to ensure response contains product_info. --- backend/src/api/cves.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/backend/src/api/cves.ts b/backend/src/api/cves.ts index dd73c6f45..23a72e8fe 100644 --- a/backend/src/api/cves.ts +++ b/backend/src/api/cves.ts @@ -142,18 +142,22 @@ export const getByName = wrapHandler(async (event) => { const cve_name = event.pathParameters?.cve_name; console.log('cve_name:', cve_name); - const cve = await Cve.findOne({ where: { cve_name } }); - console.log('Cve.findOne result:', cve); + // Create an instance of CveSearch and call getResults + const cveSearch = new CveSearch(); + cveSearch.cve_name = cve_name; + const [cves, count] = await cveSearch.getResults(event); - if (!cve) { + // Check if any CVEs were found + if (count === 0) { return { statusCode: 404, body: JSON.stringify(Error) }; } + // Return the first CVE found return { statusCode: 200, - body: JSON.stringify(cve) + body: JSON.stringify(cves[0]) }; }); From 64258a688f64a271891fc5f37ebef3de699e4eb5 Mon Sep 17 00:00:00 2001 From: "Grayson, Matthew" Date: Tue, 6 Feb 2024 17:17:17 -0600 Subject: [PATCH 37/50] Revert /cves/name endpoint to use findOne and add join to product_info table. --- backend/src/api/cves.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/backend/src/api/cves.ts b/backend/src/api/cves.ts index 23a72e8fe..dd2c0eacf 100644 --- a/backend/src/api/cves.ts +++ b/backend/src/api/cves.ts @@ -140,24 +140,21 @@ export const get = wrapHandler(async (event) => { export const getByName = wrapHandler(async (event) => { await connectToDatabase(); const cve_name = event.pathParameters?.cve_name; - console.log('cve_name:', cve_name); - // Create an instance of CveSearch and call getResults - const cveSearch = new CveSearch(); - cveSearch.cve_name = cve_name; - const [cves, count] = await cveSearch.getResults(event); + const cve = await Cve.createQueryBuilder('cve') + .leftJoinAndSelect('cve.product_info', 'product_info') + .where('cve.cve_name = :cve_name', { cve_name }) + .getOne(); - // Check if any CVEs were found - if (count === 0) { + if (!cve) { return { statusCode: 404, body: JSON.stringify(Error) }; } - // Return the first CVE found return { statusCode: 200, - body: JSON.stringify(cves[0]) + body: JSON.stringify(cve) }; }); From f237384d289f56b2a61e7a5d5af1e5c05d27784e Mon Sep 17 00:00:00 2001 From: Amelia Vance Date: Tue, 6 Feb 2024 19:10:19 -0500 Subject: [PATCH 38/50] Add product info to Vulnerability.tsx --- .../src/pages/Vulnerability/Vulnerability.tsx | 91 ++++++++++--------- frontend/src/types/product-info.ts | 2 +- 2 files changed, 48 insertions(+), 45 deletions(-) diff --git a/frontend/src/pages/Vulnerability/Vulnerability.tsx b/frontend/src/pages/Vulnerability/Vulnerability.tsx index d43559667..8f8ca88c6 100644 --- a/frontend/src/pages/Vulnerability/Vulnerability.tsx +++ b/frontend/src/pages/Vulnerability/Vulnerability.tsx @@ -21,7 +21,11 @@ import { } from '@mui/material'; import { getSeverityColor } from 'pages/Risk/utils'; import { useAuthContext } from 'context'; -import { Cve as CveType, Vulnerability as VulnerabilityType } from 'types'; +import { + Cve as CveType, + ProductInfo as ProductInfoType, + Vulnerability as VulnerabilityType +} from 'types'; const formatDate = (date: string | null) => { if (date) { @@ -30,6 +34,10 @@ const formatDate = (date: string | null) => { return 'Date not found'; }; +interface GroupedByVendor { + [key: string]: ProductInfoType[]; +} + export const Vulnerability: React.FC = () => { const { vulnerabilityId } = useParams(); const { apiGet } = useAuthContext(); @@ -41,11 +49,11 @@ export const Vulnerability: React.FC = () => { (cveName: string | null) => { return apiGet(`/cves/name/${cveName}`).then( (result) => { - // getProductInfo(result.cve_uid); setCve(result); return true; }, (e) => { + console.error(e); return false; } ); @@ -72,6 +80,21 @@ export const Vulnerability: React.FC = () => { if (!vulnerability) return <>No Vulnerabilities; + const groupedByVendor: GroupedByVendor = (cve?.product_info ?? []).reduce( + (acc: GroupedByVendor, current: ProductInfoType) => { + const { vender, ...rest } = current; + // If the vendor exists, push the current object to its array + if (acc[vender]) { + acc[vender].push(rest); + } else { + // Create a new array with the current object + acc[vender] = [rest]; + } + return acc; + }, + {} as GroupedByVendor + ); + const references = vulnerability.references.map((ref) => ref); if (vulnerability.cve) references.unshift({ @@ -464,42 +487,18 @@ export const Vulnerability: React.FC = () => { Products Affected - opensuse - -
    -
  • -
-
- canonical - -
    -
  • -
-
- debian - -
    -
  • -
-
- oracle - -
    -
  • -
-
- fedoraproject - -
    -
  • -
-
- apache - -
    -
  • -
-
+ {Object.entries(groupedByVendor).map(([vendor, values]) => ( +
+ {vendor} + +
    + {values.map((value, index) => ( +
  • {value.cpe_product_name}
  • // Replace value with the appropriate property from your ProductInfo object + ))} +
+
+
+ ))}
Vulnerability Detection History @@ -520,6 +519,15 @@ export const Vulnerability: React.FC = () => {
); }; + // TODO: Discuss options to add manual notes. + // const NotesSection = () => { + // + // + // Team Notes + // Add new note + // + // ; + // }; return ( @@ -551,12 +559,7 @@ export const Vulnerability: React.FC = () => { - - - Team Notes - Add new note - - + {/* */}
diff --git a/frontend/src/types/product-info.ts b/frontend/src/types/product-info.ts index e563ef848..05c6db863 100644 --- a/frontend/src/types/product-info.ts +++ b/frontend/src/types/product-info.ts @@ -3,7 +3,7 @@ export interface ProductInfo { id: string; cpe_product_name: string; version_number: string; - vender: string; + vender?: string | any; last_seen: Date; cve: Cve[]; } From 5b246ee0741d8b61f9da8375db1768f8923415fc Mon Sep 17 00:00:00 2001 From: Amelia Vance Date: Wed, 7 Feb 2024 09:58:55 -0500 Subject: [PATCH 39/50] Add Misc known products to Vulnerability.tsx --- .../src/pages/Vulnerability/Vulnerability.tsx | 68 +++++++++++++++++-- 1 file changed, 62 insertions(+), 6 deletions(-) diff --git a/frontend/src/pages/Vulnerability/Vulnerability.tsx b/frontend/src/pages/Vulnerability/Vulnerability.tsx index 8f8ca88c6..0078ccf12 100644 --- a/frontend/src/pages/Vulnerability/Vulnerability.tsx +++ b/frontend/src/pages/Vulnerability/Vulnerability.tsx @@ -38,6 +38,42 @@ interface GroupedByVendor { [key: string]: ProductInfoType[]; } +interface Product { + cpe?: string | null; + name: string; + version?: string; + tags: string[]; +} + +interface Service { + id: string | number; + createdAt?: string | null; + updatedAt?: string | null; + serviceSource?: string | null; + port: number; + service: string; + lastSeen?: string | null; + banner?: string | null; + products: Product[]; + censysMetadata?: Record | null; + censysIpv4Results?: Record | null; + intrigueIdentResults?: Record | null; + shodanResult?: Record | null; + wappalyzerResults?: + | { + version?: string | null; + technology?: { + cpe?: string | null; + } | null; + }[] + | null; +} + +interface WebInfoItem { + label?: string | null; + value?: string; +} + export const Vulnerability: React.FC = () => { const { vulnerabilityId } = useParams(); const { apiGet } = useAuthContext(); @@ -95,6 +131,15 @@ export const Vulnerability: React.FC = () => { {} as GroupedByVendor ); + function generateWebInfo(service: Service): WebInfoItem[] { + const webInfo: WebInfoItem[] = service.products.map((product) => ({ + label: product.cpe, + value: product.name + (product.version ? ` ${product.version}` : '') + })); + + return webInfo; + } + const references = vulnerability.references.map((ref) => ref); if (vulnerability.cve) references.unshift({ @@ -194,18 +239,29 @@ export const Vulnerability: React.FC = () => {

- Installed (Known) Products + {generateWebInfo(vulnerability.service).length > 0 && ( + <> + + Installed (Known) Products + + {generateWebInfo(vulnerability.service).map((item, index) => ( +
+ + Misc: {item.value && item.value.split(', ').join(', ')} + +
+ ))} + + )} - Misc: -
- Marketing: + {/* Marketing:
Networking:
Web Framework:
Web Server: -
+
*/}
@@ -493,7 +549,7 @@ export const Vulnerability: React.FC = () => {
    {values.map((value, index) => ( -
  • {value.cpe_product_name}
  • // Replace value with the appropriate property from your ProductInfo object +
  • {value.cpe_product_name}
  • ))}
From aa9521b20432cd86bfb9ff9b7015d648fd8356db Mon Sep 17 00:00:00 2001 From: "Grayson, Matthew" Date: Wed, 7 Feb 2024 09:21:59 -0600 Subject: [PATCH 40/50] Remove hard-coded UUIDs from tests for support with GitHub Actions. --- backend/test/cpes.test.ts | 5 +++-- backend/test/cves.test.ts | 12 ++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/backend/test/cpes.test.ts b/backend/test/cpes.test.ts index 8d1f9010d..b2c1675e4 100644 --- a/backend/test/cpes.test.ts +++ b/backend/test/cpes.test.ts @@ -10,7 +10,6 @@ describe('cpes', () => { beforeAll(async () => { connection = await connectToDatabase(); productInfo = ProductInfo.create({ - id: '00000000-0000-0000-0000-000000000000', last_seen: new Date(), cpe_product_name: 'Test Product', version_number: '1.0.0', @@ -18,7 +17,6 @@ describe('cpes', () => { }); await productInfo.save(); organization = Organization.create({ - id: '00000000-0000-0000-0000-000000000000', name: 'test-' + Math.random(), rootDomains: ['test-' + Math.random()], ipBlocks: [], @@ -45,6 +43,9 @@ describe('cpes', () => { .send({}) .expect(200); expect(response.body.id).toEqual(productInfo.id); + expect(response.body.cpe_product_name).toEqual( + productInfo.cpe_product_name + ); }); }); }); diff --git a/backend/test/cves.test.ts b/backend/test/cves.test.ts index 9de98ae25..b2d830f0c 100644 --- a/backend/test/cves.test.ts +++ b/backend/test/cves.test.ts @@ -11,12 +11,10 @@ describe('cves', () => { beforeAll(async () => { connection = await connectToDatabase(); cve = Cve.create({ - cve_uid: '00000000-0000-0000-0000-000000000000', cve_name: 'CVE-0001-0001' }); await cve.save(); organization = Organization.create({ - id: '00000000-0000-0000-0000-000000000000', name: 'test-' + Math.random(), rootDomains: ['test-' + Math.random()], ipBlocks: [], @@ -31,9 +29,9 @@ describe('cves', () => { await connection.close(); }); describe('CVE API', () => { - it('should return a single CVE by cve_uid', async () => { + it('should return a single CVE by cve_name', async () => { const response = await request(app) - .get(`/cves/${cve.cve_uid}`) + .get(`/cves/name/${cve.cve_name}`) .set( 'Authorization', createUserToken({ @@ -43,12 +41,13 @@ describe('cves', () => { .send({}) .expect(200); expect(response.body.cve_uid).toEqual(cve.cve_uid); + expect(response.body.cve_name).toEqual(cve.cve_name); }); }); describe('CVE API', () => { - it('should return a single CVE by cve_name', async () => { + it('should return a single CVE by cve_uid', async () => { const response = await request(app) - .get(`/cves/name/${cve.cve_name}`) + .get(`/cves/${cve.cve_uid}`) .set( 'Authorization', createUserToken({ @@ -57,6 +56,7 @@ describe('cves', () => { ) .send({}) .expect(200); + expect(response.body.cve_uid).toEqual(cve.cve_uid); expect(response.body.cve_name).toEqual(cve.cve_name); }); }); From ec126d0c23bb43d358e391666029c85a2e3dcdb7 Mon Sep 17 00:00:00 2001 From: "Grayson, Matthew" Date: Wed, 7 Feb 2024 09:57:40 -0600 Subject: [PATCH 41/50] Refactor /cves/:cve_uid endpoint to use getOne; remove unused CveFilters and CveSearch classes. --- backend/src/api/cves.ts | 98 ++++------------------------------------- 1 file changed, 9 insertions(+), 89 deletions(-) diff --git a/backend/src/api/cves.ts b/backend/src/api/cves.ts index dd2c0eacf..09d4e643f 100644 --- a/backend/src/api/cves.ts +++ b/backend/src/api/cves.ts @@ -1,86 +1,8 @@ -import { - IsInt, - IsPositive, - IsString, - ValidateNested, - IsOptional, - IsUUID -} from 'class-validator'; -import { Type } from 'class-transformer'; import { Cve, connectToDatabase } from '../models'; -import { validateBody, wrapHandler, NotFound } from './helpers'; +import { wrapHandler } from './helpers'; -class CveFilters { - @IsUUID() - @IsOptional() - uuid?: string; - - @IsOptional() - @IsString() - cve_name?: string; - - @IsOptional() - @IsString() - vuln_status?: string; -} - -class CveSearch { - @IsUUID() - @IsOptional() - uuid?: string; - - @IsOptional() - @IsString() - cve_name?: string; - - @IsOptional() - @IsString() - vuln_status?: string; - - @Type(() => CveFilters) - @ValidateNested() - @IsOptional() - filters?: CveFilters; - - @IsInt() - @IsPositive() - @IsOptional() - pageSize?: number; - - @IsInt() - @IsPositive() - @IsOptional() - page?: number; - - async getResults(event): Promise<[Cve[], number]> { - const filters = this.filters || new CveFilters(); - const query = Cve.createQueryBuilder('cve').leftJoinAndSelect( - 'cve.product_info', - 'product_info' - ); - - if (filters.cve_name) { - query.andWhere('cve.cve_name = :cve_name', { - cve_name: filters.cve_name - }); - } - if (filters.vuln_status) { - query.andWhere('cve.vuln_status = :vuln_status', { - vuln_status: filters.vuln_status - }); - } - - // Apply pagination to the query - const pageSize = this.pageSize || 25; - const skip = ((this.page || 1) - 1) * pageSize; - query.skip(skip).take(pageSize); - - // Execute the query - const [result, count] = await query.getManyAndCount(); - - return [result, count]; - } -} +// TODO: Add test for joining product_info +// TODO: Create CveFilters and CveSearch classes to handle filtering and pagination of additional fields /** * @swagger @@ -101,23 +23,21 @@ export const get = wrapHandler(async (event) => { await connectToDatabase(); const cve_uid = event.pathParameters?.cve_uid; - // Create an instance of CveSearch and call getResults - const cveSearch = new CveSearch(); - cveSearch.uuid = cve_uid; - const [cves, count] = await cveSearch.getResults(event); + const cve = await Cve.createQueryBuilder('cve') + .leftJoinAndSelect('cve.product_info', 'product_info') + .where('cve.cve_uid = :cve_uid', { cve_uid: cve_uid }) + .getOne(); - // Check if any CVEs were found - if (count === 0) { + if (!cve) { return { statusCode: 404, body: JSON.stringify(Error) }; } - // Return the first CVE found return { statusCode: 200, - body: JSON.stringify(cves[0]) + body: JSON.stringify(cve) }; }); From aae93f5d62e8c02c3bf93d799f84bc28ac855c89 Mon Sep 17 00:00:00 2001 From: "Grayson, Matthew" Date: Wed, 7 Feb 2024 10:02:05 -0600 Subject: [PATCH 42/50] Remove unused CpeFilters and CpeSearch classes. --- backend/src/api/cpes.ts | 91 +---------------------------------------- 1 file changed, 2 insertions(+), 89 deletions(-) diff --git a/backend/src/api/cpes.ts b/backend/src/api/cpes.ts index 90bd26144..9d25b75d4 100644 --- a/backend/src/api/cpes.ts +++ b/backend/src/api/cpes.ts @@ -1,95 +1,8 @@ -import { - IsInt, - IsPositive, - IsString, - ValidateNested, - IsOptional, - IsUUID -} from 'class-validator'; -import { Type } from 'class-transformer'; import { ProductInfo, connectToDatabase } from '../models'; import { wrapHandler, NotFound } from './helpers'; -import { SelectQueryBuilder } from 'typeorm'; -class CpeFilters { - @IsOptional() - @IsUUID() - id?: string; - - @IsOptional() - @IsString() - cpe_product_name?: string; - - @IsOptional() - @IsString() - version_number?: string; - - @IsOptional() - @IsString() - vender?: string; -} - -class CpeSearch { - @ValidateNested() - @Type(() => CpeFilters) - filters: CpeFilters; - - @IsOptional() - @IsInt() - @IsPositive() - page?: number; - - @IsOptional() - @IsInt() - @IsPositive() - pageSize?: number; - - constructor(filters: CpeFilters, page?: number, pageSize?: number) { - this.filters = filters; - this.page = page; - this.pageSize = pageSize; - } - - async filterResultQueryset(qs: SelectQueryBuilder) { - if (this.filters.id) { - qs = qs.andWhere('product_info.id = :id', { id: this.filters.id }); - } - if (this.filters.cpe_product_name) { - qs = qs.andWhere('product_info.cpe_product_name = :cpe_product_name', { - cpe_product_name: this.filters.cpe_product_name - }); - } - if (this.filters.version_number) { - qs = qs.andWhere('product_info.version_number = :version_number', { - version_number: this.filters.version_number - }); - } - if (this.filters.vender) { - qs = qs.andWhere('product_info.vender = :vender', { - vender: this.filters.vender - }); - } - return qs; - } - - async getResults(): Promise<[ProductInfo[], number]> { - const connection = await connectToDatabase(); - let qs = connection - .getRepository(ProductInfo) - .createQueryBuilder('product_info'); - qs = await this.filterResultQueryset(qs); - - const total = await qs.getCount(); - - if (this.page && this.pageSize) { - qs = qs.skip((this.page - 1) * this.pageSize).take(this.pageSize); - } - - const results = await qs.getMany(); - - return [results, total]; - } -} +// TODO: Join cves to get method +// TODO: Create CpeFilters and CpeSearch classes to handle filtering and pagination of additional fields /** * @swagger From 5c6fd104562a5511e7b4741839972fcebe498d94 Mon Sep 17 00:00:00 2001 From: Amelia Vance Date: Wed, 7 Feb 2024 14:24:45 -0500 Subject: [PATCH 43/50] Add CVSS scoring and color to utils.ts and Vulnerability.tsx --- frontend/src/pages/Risk/utils.ts | 8 +++++++ .../src/pages/Vulnerability/Vulnerability.tsx | 24 +++++++++++++------ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/frontend/src/pages/Risk/utils.ts b/frontend/src/pages/Risk/utils.ts index 12f2dc5ef..2c204c0a7 100644 --- a/frontend/src/pages/Risk/utils.ts +++ b/frontend/src/pages/Risk/utils.ts @@ -11,6 +11,14 @@ export const getSeverityColor = ({ id }: { id: string }) => { else if (id === 'High') return '#B51D09'; else return '#540C03'; }; +export const getCVSSColor = (score: number) => { + if (!score || score === 0) return ['#EFF1F5', 'NONE']; + else if (0.1 <= score && score <= 3.9) return ['#99cc33', 'LOW']; + else if (4 <= score && score <= 6.9) return ['#ffcc00', 'MEDIUM']; + else if (7 <= score && score <= 8.9) return ['#ff9966', 'HIGH']; + else if (9 <= score && score <= 10) return ['#ff6254', 'CRITICAL']; + else return '#540C03'; +}; export const offsets: any = { Vermont: [50, -8], 'New Hampshire': [34, 2], diff --git a/frontend/src/pages/Vulnerability/Vulnerability.tsx b/frontend/src/pages/Vulnerability/Vulnerability.tsx index 0078ccf12..8cecbf10f 100644 --- a/frontend/src/pages/Vulnerability/Vulnerability.tsx +++ b/frontend/src/pages/Vulnerability/Vulnerability.tsx @@ -19,7 +19,7 @@ import { Toolbar, Typography } from '@mui/material'; -import { getSeverityColor } from 'pages/Risk/utils'; +import { getSeverityColor, getCVSSColor } from 'pages/Risk/utils'; import { useAuthContext } from 'context'; import { Cve as CveType, @@ -413,7 +413,7 @@ export const Vulnerability: React.FC = () => { {vulnerability.cwe} - ? + {vulnerability.source} @@ -457,14 +457,19 @@ export const Vulnerability: React.FC = () => {
- Base Score: + Base Score:{' '} - {' ' + cve?.cvss_v3_base_score} +   {cve?.cvss_v3_base_score}  + {getCVSSColor(Number(cve?.cvss_v3_base_score))[1]}   @@ -522,9 +527,14 @@ export const Vulnerability: React.FC = () => { - {cve?.cvss_v2_base_score} +   {cve?.cvss_v2_base_score}  + {getCVSSColor(Number(cve?.cvss_v2_base_score))[1]}   From e093d47ef38ac0ca46a0614900c13af5e3a157c2 Mon Sep 17 00:00:00 2001 From: Amelia Vance Date: Wed, 7 Feb 2024 14:41:03 -0500 Subject: [PATCH 44/50] Add TODOs to Vulnerability.tsx --- frontend/src/pages/Vulnerability/Vulnerability.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/Vulnerability/Vulnerability.tsx b/frontend/src/pages/Vulnerability/Vulnerability.tsx index 8cecbf10f..576a33759 100644 --- a/frontend/src/pages/Vulnerability/Vulnerability.tsx +++ b/frontend/src/pages/Vulnerability/Vulnerability.tsx @@ -254,6 +254,7 @@ export const Vulnerability: React.FC = () => { )} + {/* TODO: Find the rest of the Installed Products info */} {/* Marketing:
Networking: @@ -413,7 +414,7 @@ export const Vulnerability: React.FC = () => { {vulnerability.cwe} - + {/* TODO: Get CWE Name */} {vulnerability.source} From edf0694877a5ab41052de24d68344951417c4c47 Mon Sep 17 00:00:00 2001 From: Amelia Vance Date: Wed, 7 Feb 2024 17:14:30 -0500 Subject: [PATCH 45/50] Update installed known products and remove cwe name in Vulnerability.tsx --- .../src/pages/Vulnerability/Vulnerability.tsx | 54 ++++++++++--------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/frontend/src/pages/Vulnerability/Vulnerability.tsx b/frontend/src/pages/Vulnerability/Vulnerability.tsx index 576a33759..43874a4b2 100644 --- a/frontend/src/pages/Vulnerability/Vulnerability.tsx +++ b/frontend/src/pages/Vulnerability/Vulnerability.tsx @@ -131,14 +131,29 @@ export const Vulnerability: React.FC = () => { {} as GroupedByVendor ); - function generateWebInfo(service: Service): WebInfoItem[] { - const webInfo: WebInfoItem[] = service.products.map((product) => ({ - label: product.cpe, - value: product.name + (product.version ? ` ${product.version}` : '') - })); - - return webInfo; - } + const generateWebInfo = (service: Service): WebInfoItem[] => { + const categoriesToProducts: Record> = {}; + for (const product of service.products) { + const version = product.version ? ` ${product.version}` : ''; + const value = product.name + version; + const name = + product.tags && product.tags.length > 0 ? product.tags[0] : 'Misc'; + if (!categoriesToProducts[name]) { + categoriesToProducts[name] = new Set(); + } + categoriesToProducts[name].add(value); + } + return Object.entries(categoriesToProducts).reduce( + (acc, [name, value]) => [ + ...acc, + { + label: name, + value: Array.from(value).join(', ') + } + ], + [] as any + ); + }; const references = vulnerability.references.map((ref) => ref); if (vulnerability.cve) @@ -244,25 +259,16 @@ export const Vulnerability: React.FC = () => { Installed (Known) Products - {generateWebInfo(vulnerability.service).map((item, index) => ( -
- - Misc: {item.value && item.value.split(', ').join(', ')} + {generateWebInfo(vulnerability.service).map( + ({ label, value }) => ( + + {label}: {value} -
- ))} + ) + )} )} - {/* TODO: Find the rest of the Installed Products info */} - {/* Marketing: -
- Networking: -
- Web Framework: -
- Web Server: -
*/}
@@ -403,7 +409,6 @@ export const Vulnerability: React.FC = () => { }} > CWE-ID - CWE Name Source @@ -414,7 +419,6 @@ export const Vulnerability: React.FC = () => { {vulnerability.cwe} - {/* TODO: Get CWE Name */} {vulnerability.source} From 630c2cac08862a74d16937ed4362341ff517928e Mon Sep 17 00:00:00 2001 From: Amelia Vance Date: Wed, 7 Feb 2024 17:18:16 -0500 Subject: [PATCH 46/50] Remove unused typography tag in Vulnerability.tsx --- frontend/src/pages/Vulnerability/Vulnerability.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/pages/Vulnerability/Vulnerability.tsx b/frontend/src/pages/Vulnerability/Vulnerability.tsx index 43874a4b2..fdf9ad075 100644 --- a/frontend/src/pages/Vulnerability/Vulnerability.tsx +++ b/frontend/src/pages/Vulnerability/Vulnerability.tsx @@ -268,8 +268,6 @@ export const Vulnerability: React.FC = () => { )} )} - - From c4a5c3a98a7615469134a08f4cf992d53e16a8cd Mon Sep 17 00:00:00 2001 From: "Grayson, Matthew" Date: Thu, 8 Feb 2024 15:12:14 -0600 Subject: [PATCH 47/50] Remove invalid reference from cpe get swagger comment. --- backend/src/api/cpes.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/backend/src/api/cpes.ts b/backend/src/api/cpes.ts index 9d25b75d4..f59b63288 100644 --- a/backend/src/api/cpes.ts +++ b/backend/src/api/cpes.ts @@ -1,7 +1,7 @@ import { ProductInfo, connectToDatabase } from '../models'; import { wrapHandler, NotFound } from './helpers'; -// TODO: Join cves to get method +// TODO: Join cves to cpe get method // TODO: Create CpeFilters and CpeSearch classes to handle filtering and pagination of additional fields /** @@ -15,15 +15,6 @@ import { wrapHandler, NotFound } from './helpers'; * required: true * schema: * type: string - * responses: - * 200: - * description: A CPE object - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ProductInfo' - * 404: - * description: CPE not found */ export const get = wrapHandler(async (event) => { const connection = await connectToDatabase(); From bdedb983db5c34f0480462c5eb9390d4ae39bf03 Mon Sep 17 00:00:00 2001 From: "Grayson, Matthew" Date: Thu, 8 Feb 2024 15:32:25 -0600 Subject: [PATCH 48/50] Add tags to CPE endpoint to improve organization in API docs. --- backend/src/api/cpes.ts | 4 +++- backend/src/api/cves.ts | 21 ++++++++++----------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/backend/src/api/cpes.ts b/backend/src/api/cpes.ts index f59b63288..565c4c9dd 100644 --- a/backend/src/api/cpes.ts +++ b/backend/src/api/cpes.ts @@ -8,7 +8,9 @@ import { wrapHandler, NotFound } from './helpers'; * @swagger * /cpes/{id}: * get: - * summary: Retrieve a CPE by ID + * description: Retrieve a CPE by ID + * tags: + * - CPEs * parameters: * - in: path * name: id diff --git a/backend/src/api/cves.ts b/backend/src/api/cves.ts index 09d4e643f..b7415c5ff 100644 --- a/backend/src/api/cves.ts +++ b/backend/src/api/cves.ts @@ -6,18 +6,17 @@ import { wrapHandler } from './helpers'; /** * @swagger - * * /cves/{cve_uid}: - * get: - * description: Retrieve a single CVE record by its ID. - * tags: - * - CVE - * parameters: - * - name: cve_uid - * in: path - * required: true - * schema: - * type: string + * get: + * description: Retrieve a CVE by ID. + * tags: + * - CVEs + * parameters: + * - in: path + * name: cve_uid + * required: true + * schema: + * type: string */ export const get = wrapHandler(async (event) => { await connectToDatabase(); From 49829946ecf4dd093e45f2ab8d4bfda9b86f07f6 Mon Sep 17 00:00:00 2001 From: "Grayson, Matthew" Date: Fri, 9 Feb 2024 10:57:48 -0600 Subject: [PATCH 49/50] Partially alphabetize type interfaces for cve and product-info keeping important info at the the top and keeping arrays and joins at the bottom. --- frontend/src/types/cve.ts | 8 ++++---- frontend/src/types/product-info.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/src/types/cve.ts b/frontend/src/types/cve.ts index 3a32216be..843ed718d 100644 --- a/frontend/src/types/cve.ts +++ b/frontend/src/types/cve.ts @@ -2,10 +2,10 @@ import { ProductInfo } from './product-info'; export interface Cve { cve_uid: string; cve_name: string | null; - published_date: Date; + description: string | null; last_modified_date: Date; + published_date: Date; vuln_status: string | null; - description: string | null; cvss_v2_source: string | null; cvss_v2_type: string | null; cvss_v2_version: string | null; @@ -30,8 +30,8 @@ export interface Cve { cvss_v4_base_severity: string | null; cvss_v4_exploitability_score: string | null; cvss_v4_impact_score: string | null; - weaknesses: string[] | null; - reference_urls: string[] | null; cpe_list: string[] | null; + reference_urls: string[] | null; + weaknesses: string[] | null; product_info: ProductInfo[]; } diff --git a/frontend/src/types/product-info.ts b/frontend/src/types/product-info.ts index 05c6db863..260e8ca82 100644 --- a/frontend/src/types/product-info.ts +++ b/frontend/src/types/product-info.ts @@ -2,8 +2,8 @@ import { Cve } from './cve'; export interface ProductInfo { id: string; cpe_product_name: string; - version_number: string; - vender?: string | any; last_seen: Date; + vender?: string | any; + version_number: string; cve: Cve[]; } From f6a743f81d949c32aad9442707fee6b2af0596f0 Mon Sep 17 00:00:00 2001 From: Amelia Vance Date: Fri, 9 Feb 2024 12:51:13 -0500 Subject: [PATCH 50/50] Add issue number to Add Notes TODO in Vulnerability.tsx --- frontend/src/pages/Vulnerability/Vulnerability.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/Vulnerability/Vulnerability.tsx b/frontend/src/pages/Vulnerability/Vulnerability.tsx index fdf9ad075..b32c5a267 100644 --- a/frontend/src/pages/Vulnerability/Vulnerability.tsx +++ b/frontend/src/pages/Vulnerability/Vulnerability.tsx @@ -588,7 +588,7 @@ export const Vulnerability: React.FC = () => { ); }; - // TODO: Discuss options to add manual notes. + // TODO: Discuss options to add manual notes. https://github.com/cisagov/crossfeed/issues/2519 // const NotesSection = () => { // //