diff --git a/CHANGELOG.md b/CHANGELOG.md index 65f41439..675f6185 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - ### Bug Fixes +- fix: correct off-by-one in get_dotnet_table_row so row_index=1 (first valid .NET metadata row) is no longer rejected @williballenthin - fix: add missing import for assert_never in cape extractor.py to avoid NameError when call argument has unexpected type @williballenthin - fix: stop mutating call.api in cape thread.get_calls; yield one CallHandle per call so the original API name is preserved for all handles @williballenthin - fix: use instruction_indices in is_security_cookie to handle single-instruction basic blocks where end_index is omitted, preventing KeyError on -1 @williballenthin diff --git a/capa/features/extractors/dnfile/helpers.py b/capa/features/extractors/dnfile/helpers.py index fa103ad1..86aaf8ef 100644 --- a/capa/features/extractors/dnfile/helpers.py +++ b/capa/features/extractors/dnfile/helpers.py @@ -346,7 +346,7 @@ def get_dotnet_table_row(pe: dnfile.dnPE, table_index: int, row_index: int) -> O assert pe.net is not None assert pe.net.mdtables is not None - if row_index - 1 <= 0: + if row_index <= 0: return None table: Optional[dnfile.base.ClrMetaDataTable] = pe.net.mdtables.tables.get(table_index) diff --git a/tests/test_dnfile_features.py b/tests/test_dnfile_features.py index 1916c542..524ce36c 100644 --- a/tests/test_dnfile_features.py +++ b/tests/test_dnfile_features.py @@ -12,8 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. +from pathlib import Path + +import dnfile import fixtures +from capa.features.extractors.dnfile.helpers import get_dotnet_table_row + +DOTNET_DIR = Path(__file__).resolve().parent / "data" / "dotnet" + @fixtures.parametrize( "sample,scope,feature,expected", @@ -31,3 +38,38 @@ def test_dnfile_features(sample, scope, feature, expected): ) def test_dnfile_feature_counts(sample, scope, feature, expected): fixtures.do_test_feature_count(fixtures.get_dnfile_extractor, sample, scope, feature, expected) + + +def test_get_dotnet_table_row_first_row(): + """row_index=1 is the first valid .NET metadata row; it must not be rejected.""" + pe = dnfile.dnPE(DOTNET_DIR / "dd9098ff91717f4906afe9dafdfa2f52.exe_") + row = get_dotnet_table_row(pe, dnfile.mdtable.TypeDef.number, 1) + assert row is not None + assert str(row.TypeName) == "" + + +def test_get_dotnet_table_row_invalid_zero(): + """row_index=0 is the null token; the function must return None.""" + pe = dnfile.dnPE(DOTNET_DIR / "dd9098ff91717f4906afe9dafdfa2f52.exe_") + assert get_dotnet_table_row(pe, dnfile.mdtable.TypeDef.number, 0) is None + + +def test_get_dotnet_table_row_valid_rows(): + """All valid row indices 1..N return a row from the real PE.""" + pe = dnfile.dnPE(DOTNET_DIR / "dd9098ff91717f4906afe9dafdfa2f52.exe_") + assert pe.net is not None + assert pe.net.mdtables is not None + table = pe.net.mdtables.tables.get(dnfile.mdtable.TypeDef.number) + assert table is not None + for row_index in range(1, len(table.rows) + 1): + assert get_dotnet_table_row(pe, dnfile.mdtable.TypeDef.number, row_index) is not None + + +def test_get_dotnet_table_row_out_of_bounds(): + """row_index beyond the table size returns None.""" + pe = dnfile.dnPE(DOTNET_DIR / "dd9098ff91717f4906afe9dafdfa2f52.exe_") + assert pe.net is not None + assert pe.net.mdtables is not None + table = pe.net.mdtables.tables.get(dnfile.mdtable.TypeDef.number) + assert table is not None + assert get_dotnet_table_row(pe, dnfile.mdtable.TypeDef.number, len(table.rows) + 1) is None