diff --git a/backend/src/api/vulnerabilities.ts b/backend/src/api/vulnerabilities.ts index 1e0cbe6ad..7e433a529 100644 --- a/backend/src/api/vulnerabilities.ts +++ b/backend/src/api/vulnerabilities.ts @@ -124,11 +124,12 @@ class VulnerabilitySearch { if (this.filters?.severity) { if (this.filters.severity === 'N/A') { qs.andWhere( - "vulnerability.severity IS NULL OR vulnerability.severity = ''" + "vulnerability.severity IS NULL OR vulnerability.severity = '' OR vulnerability.severity ILIKE 'N/A' OR vulnerability.severity ILIKE 'NULL'" ); } else if (this.filters.severity === 'Other') { qs.andWhere( - `vulnerability.severity NOT ILIKE 'N/A' AND + `vulnerability.severity NOT ILIKE 'NULL' AND + vulnerability.severity NOT ILIKE 'N/A' AND vulnerability.severity NOT ILIKE 'Low' AND vulnerability.severity NOT ILIKE 'Medium' AND vulnerability.severity NOT ILIKE 'High' AND diff --git a/backend/src/xfd_django/xfd_api/api_methods/vulnerability.py b/backend/src/xfd_django/xfd_api/api_methods/vulnerability.py index 4bedb1e3b..e982b520c 100644 --- a/backend/src/xfd_django/xfd_api/api_methods/vulnerability.py +++ b/backend/src/xfd_django/xfd_api/api_methods/vulnerability.py @@ -134,13 +134,21 @@ def search_vulnerabilities(vulnerability_search: VulnerabilitySearch, current_us ) # Permissions check - if not is_global_view_admin(current_user): + if ( + not is_global_view_admin(current_user) + and not current_user.userType == "regionalAdmin" + ): org_ids = get_org_memberships(current_user) if not org_ids: return [], 0 # User has no accessible organizations vulnerabilities = vulnerabilities.filter( domain__organization_id__in=org_ids ) + # Regional Admins can only view vulnerabilities in their region + if current_user.userType == "regionalAdmin" and current_user.regionId: + vulnerabilities = vulnerabilities.filter( + domain__organization__regionId=current_user.regionId + ) # Apply custom FCEB and CIDR filter vulnerabilities = vulnerabilities.filter( diff --git a/backend/src/xfd_django/xfd_api/auth.py b/backend/src/xfd_django/xfd_api/auth.py index b4627ed5a..5aa90f42b 100644 --- a/backend/src/xfd_django/xfd_api/auth.py +++ b/backend/src/xfd_django/xfd_api/auth.py @@ -541,6 +541,7 @@ def get_stats_org_ids(current_user, filters): is_global_view_admin(current_user) or (is_regional_admin_for_organization(current_user, org_id)) or (is_org_admin(current_user, org_id)) + or (get_org_memberships(current_user)) ): organization_ids.add(org_id) diff --git a/backend/src/xfd_django/xfd_api/helpers/filter_helpers.py b/backend/src/xfd_django/xfd_api/helpers/filter_helpers.py index 3366e542f..598d23439 100644 --- a/backend/src/xfd_django/xfd_api/helpers/filter_helpers.py +++ b/backend/src/xfd_django/xfd_api/helpers/filter_helpers.py @@ -6,6 +6,20 @@ from ..models import Vulnerability from ..schema_models.vulnerability import VulnerabilityFilters +# Define the severity levels +SEVERITY_LEVELS = ["Low", "Medium", "High", "Critical"] +NULL_VALUES = ["None", "Null", "N/A", "Undefined", ""] + + +def format_severity(severity: str) -> str: + """Format severity to classify as 'N/A', standard severity, or 'Other'.""" + if severity is None or severity in NULL_VALUES: + return "N/A" + elif severity.title() in SEVERITY_LEVELS: + return severity.title() + else: + return "Other" + def sort_direction(sort, order): """ @@ -100,7 +114,34 @@ def apply_vuln_filters( # Partial match on severity if vulnerability_filters.severity: - q &= Q(severity__icontains=vulnerability_filters.severity) + severity_category = format_severity(vulnerability_filters.severity) + + if severity_category == "N/A": + q &= ( + Q(severity=None) + | Q(severity__icontains="none") + | Q(severity__icontains="null") + | Q(severity__icontains="n/a") + | Q(severity__icontains="undefined") + | Q(severity="") + ) + + elif severity_category == "Other": + q &= ~( + Q(severity=None) + | Q(severity__icontains="none") + | Q(severity__icontains="null") + | Q(severity__icontains="undefined") + | Q(severity="") + | Q(severity__icontains="N/A") + | Q(severity__icontains="Low") + | Q(severity__icontains="Medium") + | Q(severity__icontains="High") + | Q(severity__icontains="Critical") + ) + + elif severity_category in SEVERITY_LEVELS: + q &= Q(severity__icontains=severity_category) # Partial match on cpe if vulnerability_filters.cpe: diff --git a/backend/src/xfd_django/xfd_api/helpers/stats_helpers.py b/backend/src/xfd_django/xfd_api/helpers/stats_helpers.py index fb7cead76..6f00cbf1a 100644 --- a/backend/src/xfd_django/xfd_api/helpers/stats_helpers.py +++ b/backend/src/xfd_django/xfd_api/helpers/stats_helpers.py @@ -34,7 +34,14 @@ async def get_stats_count_from_cache(redis_client, redis_key_prefix, filtered_or if response: stats_list = json.loads(response) for stat in stats_list: - aggregated_stats[stat["id"]] += stat["value"] + stat_id = stat["id"] + # Handle the case where the stat ID is None. + # None/Null values come from Redis as "0" and cannot be incremented. + if stat_id in [None, "None"]: + stat_id = "None" + if stat["value"] == 0: + stat["value"] = 1 + aggregated_stats[stat_id] += stat["value"] return [ {"id": stat_id, "value": value, "label": stat_id} diff --git a/backend/src/xfd_django/xfd_api/tasks/syncdb_helpers.py b/backend/src/xfd_django/xfd_api/tasks/syncdb_helpers.py index e626fb982..dd9743de0 100644 --- a/backend/src/xfd_django/xfd_api/tasks/syncdb_helpers.py +++ b/backend/src/xfd_django/xfd_api/tasks/syncdb_helpers.py @@ -173,7 +173,30 @@ def create_sample_services_and_vulnerabilities(domain): domain=domain, service=None, description="Sample description", - severity=random.choice(["Low", "Medium", "High"]), + severity=random.choice( + [ + None, + "N/A", + "n/a", + "Null", + "null", + "Undefined", + "undefined", + "", + "Low", + "Medium", + "High", + "Critical", + "Other", + "!@#$%^&*()", + 1234, + "low", + "medium", + "high", + "critical", + "other", + ] + ), needsPopulation=True, state="open", substate="unconfirmed", diff --git a/frontend/src/hooks/useDomainApi.ts b/frontend/src/hooks/useDomainApi.ts index 0b98071ef..f8aee316c 100644 --- a/frontend/src/hooks/useDomainApi.ts +++ b/frontend/src/hooks/useDomainApi.ts @@ -39,8 +39,6 @@ export const useDomainApi = (showAll?: boolean) => { tableFilters['organization'] = currentOrganization.id; } - console.log('filters here', tableFilters); - const { result, count, url } = await apiPost( doExport ? '/domain/export' : '/domain/search', { diff --git a/frontend/src/pages/Risk/VulnerabilityBarChart.tsx b/frontend/src/pages/Risk/VulnerabilityBarChart.tsx index 2a14a806b..2cb963fa9 100644 --- a/frontend/src/pages/Risk/VulnerabilityBarChart.tsx +++ b/frontend/src/pages/Risk/VulnerabilityBarChart.tsx @@ -73,19 +73,15 @@ const VulnerabilityBarChart = (props: { ); - // Place null values in "N/A" and capitalize the first letter of each word in the data. + // Capitalize the first letter of each id. const titleCaseData: BarData[] = data.map((d) => { - if (d.id === 'null' || d.id === null || d.id === '') { - return { id: 'N/A', value: d.value }; - } else { - return { - id: d.id[0]?.toUpperCase() + d.id.slice(1)?.toLowerCase(), - value: d.value - }; - } + return { + id: d.id[0]?.toUpperCase() + d.id.slice(1)?.toLowerCase(), + value: d.value + }; }); - // Group the data by severity level and "Other". Sum the values for each group. + // Sort irregular ids into N/A and Other. Sum the values for each group. const groupedData = titleCaseData .map((d) => { const severityLevels = [ @@ -98,6 +94,12 @@ const VulnerabilityBarChart = (props: { ]; if (severityLevels.includes(d.id)) { return d; + } + if ( + !d.id || + ['None', 'Null', 'N/a', 'Undefined', 'undefined', ''].includes(d.id) + ) { + return { id: 'N/A', value: d.value }; } else { return { id: 'Other', value: d.value }; } diff --git a/frontend/src/pages/Vulnerabilities/Vulnerabilities.tsx b/frontend/src/pages/Vulnerabilities/Vulnerabilities.tsx index 09c2bdd91..f1fd04152 100644 --- a/frontend/src/pages/Vulnerabilities/Vulnerabilities.tsx +++ b/frontend/src/pages/Vulnerabilities/Vulnerabilities.tsx @@ -77,7 +77,7 @@ export const Vulnerabilities: React.FC<{ groupBy?: string }> = ({ children?: React.ReactNode; groupBy?: string; }) => { - const { currentOrganization, apiPost, apiPut } = useAuthContext(); + const { currentOrganization, apiPost, apiPut, user } = useAuthContext(); const [vulnerabilities, setVulnerabilities] = useState([]); const [totalResults, setTotalResults] = useState(0); const [loadingError, setLoadingError] = useState(false); @@ -150,7 +150,12 @@ export const Vulnerabilities: React.FC<{ groupBy?: string }> = ({ userOrgIsExcluded = true; } }); - if (currentOrganization && !userOrgIsExcluded) { + + if ( + currentOrganization && + !userOrgIsExcluded && + user?.userType === 'standard' + ) { tableFilters['organization'] = currentOrganization.id; } if (tableFilters['isKev']) { @@ -174,7 +179,7 @@ export const Vulnerabilities: React.FC<{ groupBy?: string }> = ({ return; } }, - [apiPost, currentOrganization] + [apiPost, currentOrganization, user?.userType] ); const fetchVulnerabilities = useCallback( @@ -312,19 +317,33 @@ export const Vulnerabilities: React.FC<{ groupBy?: string }> = ({ const titleCase = (str: string) => str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); - const severityLevels: string[] = ['Low', 'Medium', 'High', 'Critical']; + const severityLevels: string[] = [ + 'N/A', + 'Low', + 'Medium', + 'High', + 'Critical', + 'Other' + ]; - const formatSeverity = (severity: string) => { - if (severity === null || severity === '' || severity === 'N/A') { + const formatSeverity = (severity?: any) => { + const titleCaseSev = titleCase(severity); + if (severityLevels.includes(titleCaseSev)) { + return titleCaseSev; + } + if ( + !titleCaseSev || + ['None', 'Null', 'N/a', 'Undefined', 'undefined', ''].includes( + titleCaseSev + ) + ) { return 'N/A'; - } else if (severityLevels.includes(titleCase(severity))) { - return titleCase(severity); } else { return 'Other'; } }; - const severity = formatSeverity(vuln.severity ?? ''); + const severity = formatSeverity(vuln.severity ?? 'N/A'); return { id: vuln.id, @@ -388,10 +407,12 @@ export const Vulnerabilities: React.FC<{ groupBy?: string }> = ({ flex: 0.5, sortComparator: (v1, v2, cellParams1, cellParams2) => { const severityLevels: Record = { - Low: 1, - Medium: 2, - High: 3, - Critical: 4 + 'N/A': 1, + Low: 2, + Medium: 3, + High: 4, + Critical: 5, + Other: 6 }; return ( severityLevels[cellParams1.value] - severityLevels[cellParams2.value] @@ -399,10 +420,12 @@ export const Vulnerabilities: React.FC<{ groupBy?: string }> = ({ }, renderCell: (cellValues: GridRenderCellParams) => { const severityLevels: Record = { + NA: 0, Low: 1, Medium: 2, High: 3, - Critical: 4 + Critical: 4, + Other: 5 }; return (