diff --git a/src/vuln_analysis/functions/cve_checker_report.py b/src/vuln_analysis/functions/cve_checker_report.py index 61e958404..5a6bb8df1 100644 --- a/src/vuln_analysis/functions/cve_checker_report.py +++ b/src/vuln_analysis/functions/cve_checker_report.py @@ -21,6 +21,7 @@ checker_context and produces the final ExploitIqOutput. """ +import os import re import warnings from dataclasses import dataclass, field @@ -508,16 +509,39 @@ def _format_target_build_check_md(blocks: "ReportBlocks") -> str: return "\n".join(lines).strip() +def _is_source_code_file(file_path: str) -> bool: + """Check if a file is RPM source code. + + Only RPM source code files, excluding tests and build files. + """ + basename = os.path.basename(file_path) + if basename in ("CMakeLists.txt", "Makefile", "Makefile.am", "Makefile.in"): + return False + + if file_path.startswith("test/") or "/test/" in file_path: + return False + + return file_path.endswith((".c", ".h", ".cpp", ".cc", ".cxx")) + + +def _filter_to_source_code_files(file_paths: list[str]) -> list[str]: + filtered = [f for f in file_paths if _is_source_code_file(f)] + if not filtered and file_paths: + logger.warning( + f"All {len(file_paths)} affected file(s) were tests/build files (filtered out). " + f"No actual source files affected - report will show 'not determined'" + ) + return filtered + + def _filter_review_snippets(snippets: list[CodeSnippet]) -> list[CodeSnippet]: - """Prefer primary source files over build-system noise when both are present.""" - primary = [ - s for s in snippets - if s.file_path.endswith((".c", ".h", ".cpp", ".cc", ".cxx")) - and "/test/" not in s.file_path - and "CMakeLists" not in s.file_path - and "Makefile" not in s.file_path - ] - return primary if primary else snippets + filtered = [s for s in snippets if _is_source_code_file(s.file_path)] + if not filtered and snippets: + logger.warning( + f"All {len(snippets)} code snippet(s) were from tests/build files (filtered out). " + f"No actual source code snippets - report will show 'not shown'" + ) + return filtered def _is_reference_tree_path(file_path: str) -> bool: @@ -831,7 +855,7 @@ def _build_report_blocks( justification_label=code_agent_report.justification_label, executive_summary=code_agent_report.executive_summary, evidence_chain=list(code_agent_report.evidence_chain), - affected_files=list(code_agent_report.affected_files), + affected_files=_filter_to_source_code_files(list(code_agent_report.affected_files)), patch_file_name=patch_file_name, spec_patch_directives=spec_patch_directives, build_log_evidence=build_log_evidence, diff --git a/tests/test_cve_checker_report_filtering.py b/tests/test_cve_checker_report_filtering.py new file mode 100644 index 000000000..3693efc4e --- /dev/null +++ b/tests/test_cve_checker_report_filtering.py @@ -0,0 +1,163 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for RPM report file filtering.""" + +import pytest + + +# Import helper to avoid sys.path mutation +def _import_filter_functions(): + """Import filter functions from local source.""" + import sys + import os + from pathlib import Path + + # Add src to path temporarily for this import + src_path = Path(__file__).parent.parent / "src" + sys.path.insert(0, str(src_path)) + + try: + from vuln_analysis.functions.cve_checker_report import ( + _is_source_code_file, + _filter_to_source_code_files, + ) + return _is_source_code_file, _filter_to_source_code_files + finally: + # Remove from path to avoid leaking to other tests + sys.path.pop(0) + + +@pytest.fixture(scope="module") +def filter_funcs(): + """Provide filter functions to all tests.""" + return _import_filter_functions() + + +def test_c_source_files_included(filter_funcs): + """C source files should pass the filter.""" + is_source, _ = filter_funcs + assert is_source("crypto/pkcs7/pk7_smime.c") is True + assert is_source("src/parser.c") is True + assert is_source("lib/util.c") is True + + +def test_c_header_files_included(filter_funcs): + """C header files should pass the filter.""" + is_source, _ = filter_funcs + assert is_source("include/ssl.h") is True + assert is_source("crypto/internal.h") is True + + +def test_cpp_files_included(filter_funcs): + """C++ files should be included.""" + is_source, _ = filter_funcs + assert is_source("src/engine.cpp") is True + assert is_source("lib/parser.cc") is True + assert is_source("util/helper.cxx") is True + + +def test_top_level_test_files_excluded(filter_funcs): + """Top-level test/*.c files should be filtered out.""" + is_source, _ = filter_funcs + # This is the critical case the reviewer pointed out + assert is_source("test/bad_dtls_frag.c") is False + assert is_source("test/unit/parser.c") is False + assert is_source("test/foo.c") is False + + +def test_nested_test_files_excluded(filter_funcs): + """Nested /test/ directory files should be filtered out.""" + is_source, _ = filter_funcs + assert is_source("src/test/helper.c") is False + assert is_source("lib/test/mock.cpp") is False + + +def test_non_c_test_files_excluded(filter_funcs): + """Non-C/C++ test files should be filtered out.""" + is_source, _ = filter_funcs + assert is_source("test/recipes/80-test_cms.t") is False + assert is_source("test/smime-eml/pkcs7-digest.eml") is False + + +def test_cmakelists_excluded(filter_funcs): + """CMakeLists files should be filtered out.""" + is_source, _ = filter_funcs + assert is_source("CMakeLists.txt") is False + assert is_source("src/CMakeLists.txt") is False + assert is_source("build/CMakeLists.txt") is False + + +def test_makefiles_excluded(filter_funcs): + """Makefile files should be filtered out.""" + is_source, _ = filter_funcs + assert is_source("Makefile") is False + assert is_source("src/Makefile") is False + assert is_source("Makefile.am") is False + assert is_source("build/Makefile.in") is False + + +def test_build_file_names_in_path_not_excluded(filter_funcs): + """Files with 'Makefile' or 'CMakeLists' in path but not basename should be included.""" + is_source, _ = filter_funcs + # These should NOT be excluded - only basename matters + assert is_source("src/MakefileParser.c") is True + assert is_source("util/CMakeListsWriter.cpp") is True + assert is_source("CMakeLists/generator.c") is True + + +def test_filter_to_source_code_files_openssl_case(filter_funcs): + """Test filtering with mixed source and test files.""" + _, filter_func = filter_funcs + + affected_files = [ + "crypto/pkcs7/pk7_smime.c", + "test/recipes/80-test_cms.t", + "test/smime-eml/pkcs7-empty-digest-set.eml", + ] + + result = filter_func(affected_files) + assert len(result) == 1 + assert result == ["crypto/pkcs7/pk7_smime.c"] + + +def test_filter_mixed_list(filter_funcs): + """Filter should correctly handle a mixed list of files.""" + _, filter_func = filter_funcs + + file_list = [ + "crypto/ssl.c", # Keep + "test/bad_dtls.c", # Remove - top-level test + "src/parser.cpp", # Keep + "CMakeLists.txt", # Remove - build file + "lib/test/mock.c", # Remove - nested test + "include/api.h", # Keep + ] + + result = filter_func(file_list) + assert len(result) == 3 + assert "crypto/ssl.c" in result + assert "src/parser.cpp" in result + assert "include/api.h" in result + + +def test_all_filtered_returns_empty(filter_funcs): + """When all files are tests/build files, should return empty list.""" + _, filter_func = filter_funcs + + # All test files - none are actual source files + file_list = [ + "test/unit_test.c", + "test/integration.c", + "lib/test/mock.c", + ] + + result = filter_func(file_list) + # Should return empty - no actual source files affected + assert result == [] + + +def test_empty_list_returns_empty(filter_funcs): + """Empty input should return empty output.""" + _, filter_func = filter_funcs + assert filter_func([]) == []