Imran Khan Niazi

54 papers B 2C 5Misc 1Journal 33Unranked 10
YearRankTypeTitle / Venue / Authors
2025 J jnl
Biomed. Signal Process. Control.
Beenish Khalid, Ali Hassan, Muhammad Yasin, Muhammad Salman, Muhammad Fasih Uddin Butt, Wadood Abdul, Imran Khan Niazi
2025 B conf
IEEE Big Data
Shafiq Alam, Imran Khan Niazi, Hina Shafi, Waqar Ahmed Awan, Imran Amjad, Muhammad Sohaib Ayub, Mufti Mahmud
2024 C conf
BSN
Usman Qamar Shaikh, Anubha Manju Kalra, Andrew Lowe, Imran Khan Niazi
2024 J jnl
Sensors
Noman Naseer, Imran Khan Niazi, Hendrik Santosa
2024 conf
ACSW
Haider Ali, Samaneh Madanian, David White, Malik Naveed Akhter, Imran Khan Niazi
2023 J jnl
Sensors
Iman A. Ismail, Imran Khan Niazi, Heidi Haavik, Ernest Nlandu Kamavuako
2023 J jnl
Biomed. Signal Process. Control.
Syeda Noor-Ul-Huda Safder, Muhammad Usman Akram, Muhammad Najam Dar, Aliya Ashraf Khan, Sajid Gul Khawaja, Ahmed Rauf Subhani, Imran Khan Niazi, Sarah Gul
2023 J jnl
Sensors
Hina Shafi, Waqar Ahmed Awan, Sharon Olsen, Furqan Ahmed Siddiqi, Naureen Tassadaq, Usman Rashid, Imran Khan Niazi
2023 conf
EMBC
Usman Ghani, Mads Jochumsen, Mona Bruun Gyldenvang, Imran Khan Niazi
2023 B conf
CBMS
Ehsan Azam, Ali Hassan, Muhammad Abdul Basit Malik, Imran Khan Niazi
2022 J jnl
Sensors
Fredy Rojas, Imran Khan Niazi, Patricio Maturana-Russel, Denise Taylor
2022 J jnl
Sensors
Nayab Usama, Imran Khan Niazi, Kim Dremstrup, Mads Jochumsen
2022 J jnl
Sensors
Usman Rashid, David Barbado, Sharon Olsen, Gemma Alder, Jose L. L. Elvira, Sue Lord, Imran Khan Niazi, Denise Taylor
2021 J jnl
Sensors
Afaq Noor, Asim Waris, Syed Omer Gilani, Amer Sohail Kashif, Mads Jochumsen, Javaid Iqbal, Imran Khan Niazi
2021 J jnl
Sensors
Nayab Usama, Imran Khan Niazi, Kim Dremstrup, Mads Jochumsen
2021 C conf
BIBE
Muhammad Akmal, Muhammad Farrukh Qureshi, Faisal Amin, Muhammad Zia ur Rehman, Imran Khan Niazi
2020 J jnl
Sensors
Asim Waris, Muhammad Zia ur Rehman, Imran Khan Niazi, Mads Jochumsen, Kevin B. Englehart, Winnie Jensen, Heidi Haavik, Ernest Nlandu Kamavuako
2020 ed.
BODYNETS
Muhammad Mahtab Alam, Matti Hämäläinen, Lorenzo Mucchi, Imran Khan Niazi, Yannick Le Moullec
2020 J jnl
Medical Biol. Eng. Comput.
Nayab Usama, Kasper Leerskov, Imran Khan Niazi, Kim Dremstrup, Mads Jochumsen
2020 conf
EMBC
Xinqi Bao, Matthew Howard, Imran Khan Niazi, Ernest Nlandu Kamavuako
2020 J jnl
Sensors
Mads Jochumsen, Imran Khan Niazi, Muhammad Zia ur Rehman, Imran Amjad, Muhammad Shafique, Syed Omer Gilani, Asim Waris
2020 J jnl
IEEE Access
Hassan Ashraf, Asim Waris, Mohsin Jamil, Syed Omer Gilani, Imran Khan Niazi, Ernest Nlandu Kamavuako, Syed Hammad Nazeer Gilani
2020 J jnl
Sensors
Saba Anwer, Asim Waris, Hajrah Sultan, Shahid Ikramullah Butt, Muhammad Hamza Zafar, Moaz Sarwar, Imran Khan Niazi, Muhammad Shafique, Amit N. Pujari
2020 J jnl
Sensors
Gemma Alder, Nada Signal, Usman Rashid, Sharon Olsen, Imran Khan Niazi, Denise Taylor
2020 C conf
BIBE
Kasper Leerskov, Muhammad Zia ur Rehman, Imran Khan Niazi, Sylvain Cremoux, Mads Jochumsen
2020 J jnl
IEEE Access
Abdul Saboor, Triin Kask, Alar Kuusik, Muhammad Mahtab Alam, Yannick Le Moullec, Imran Khan Niazi, Ahmed Zoha, Rizwan Ahmad
2020 J jnl
Sensors
Ali Raza Asif, Asim Waris, Syed Omer Gilani, Mohsin Jamil, Hassan Ashraf, Muhammad Shafique, Imran Khan Niazi
2020 J jnl
Comput. Methods Programs Biomed.
Mahdieh Mohseni, Vahid Shalchyan, Mads Jochumsen, Imran Khan Niazi
2019 J jnl
IEEE Access
Muhammad Akmal, Syed Zubair, Mads Jochumsen, Ernest Nlandu Kamavuako, Imran Khan Niazi
2019 J jnl
IEEE J. Biomed. Health Informatics
Asim Waris, Imran Khan Niazi, Mohsin Jamil, Kevin B. Englehart, Winnie Jensen, Ernest Nlandu Kamavuako
2019 J jnl
Sensors
Muhammad Samran Navid, Imran Khan Niazi, Dina Lelic, Asbjørn Mohr Drewes, Heidi Haavik
2018 J jnl
Sensors
Usman Rashid, Imran Khan Niazi, Nada Signal, Denise Taylor
2018 J jnl
Sensors
Ernest Nlandu Kamavuako, Usman Ayub Sheikh, Syed Omer Gilani, Mohsin Jamil, Imran Khan Niazi
2018 J jnl
Biomed. Signal Process. Control.
Mads Jochumsen, Imran Khan Niazi, Rasmus Wiberg Nedergaard, Muhammad Samran Navid, Kim Dremstrup
2018 J jnl
Sensors
Mads Jochumsen, Sylvain Cremoux, Lucien Robinault, Jimmy Lauber, Juan Carlos Arceo, Muhammad Samran Navid, Rasmus Wiberg Nedergaard, Usman Rashid, Heidi Haavik, Imran Khan Niazi
2018 J jnl
Sensors
Muhammad Zia ur Rehman, Asim Waris, Syed Omer Gilani, Mads Jochumsen, Imran Khan Niazi, Mohsin Jamil, Dario Farina, Ernest Nlandu Kamavuako
2018 conf
EMBC
Muhammad Zia ur Rehman, Syed Omer Gilani, Asim Waris, Mads Jochumsen, Imran Khan Niazi, Ernest Nlandu Kamavuako
2017 conf
ISSPIT
Muhammad Zia ur Rehman, Syed Omer Gilani, Asim Waris, Imran Khan Niazi, Ernest Nlandu Kamavuako
2017 J jnl
Comput. Intell. Neurosci.
Mads Jochumsen, Cecilie Rovsing, Helene Rovsing, Imran Khan Niazi, Kim Dremstrup, Ernest Nlandu Kamavuako
2016 J jnl
Medical Biol. Eng. Comput.
Mads Jochumsen, Imran Khan Niazi, Kim Dremstrup, Ernest Nlandu Kamavuako
2016 conf
EMBC
Johannes Thorling Hadsund, Mathias Brønd Sørensen, Albert Cid-Royo, Imran Khan Niazi, Helene Rovsing, Cecilie Rovsing, Mads Jochumsen
2016 ch.
Advances in Neural Networks
Usman Rashid, Mohsin Jamil, Syed Omer Gilani, Imran Khan Niazi
2016 ch.
Advances in Neural Networks
Muhammad Akmal, Mads Jochumsen, Muhammad Samran Navid, Muhammad Shafique, Syed Muhammad Tahir Zaidi, Denise Taylor, Imran Khan Niazi
2015 J jnl
Comput. Math. Methods Medicine
Aqsa Shakeel, Muhammad Samran Navid, Muhammad Nabeel Anwar, Suleman Mazhar, Mads Jochumsen, Imran Khan Niazi
2015 conf
CCECE
Ali Hassan, Farhan Riaz, Saad Rehman, Mads Jochumsen, Imran Khan Niazi, Kim Dremstrup
2015 J jnl
Comput. Intell. Neurosci.
Ernest Nlandu Kamavuako, Mads Jochumsen, Imran Khan Niazi, Kim Dremstrup
2015 C conf
IDEAL
Ali Hassan, Usman Ghani, Farhan Riaz, Saad Rehman, Mads Jochumsen, Denise Taylor, Imran Khan Niazi
2014 Misc conf
ICASSP
Ali Hassan, Imran Khan Niazi, Mads Jochumsen, Farhan Riaz, Kim Dremstrup
2014 conf
EMBC
Farhan Riaz, Ali Hassan, Saad Rehman, Imran Khan Niazi, Mads Jochumsen, Kim Dremstrup
2014 C conf
IDEAL
Ali Hassan, Hassan Akhtar, Muhammad Junaid Khan, Farhan Riaz, Faiza Hassan, Imran Khan Niazi, Mads Jochumsen, Kim Dremstrup
2013 J jnl
Medical Biol. Eng. Comput.
Imran Khan Niazi, Ning Jiang, Mads Jochumsen, Jørgen Feldbæk Nielsen, Kim Dremstrup, Dario Farina
2011 conf
EMBC
Xavier Artusi, Imran Khan Niazi, Marie-Françoise Lucas, Dario Farina
2011 conf
EMBC
T. Lorrain, Imran Khan Niazi, O. Thibergien, Ning Jiang, Dario Farina
2011 J jnl
IEEE J. Emerg. Sel. Topics Circuits Syst.
Xavier Artusi, Imran Khan Niazi, Marie-Françoise Lucas, Dario Farina
tests/unit/test_apk_strings_ioc.py
← Index tests/unit/test_apk_strings_ioc.py python
"""Tests for APK string extraction and IOC extraction integration.

Covers:
- APKCodeAnalyzer._extract_strings() — string extraction from DEX objects
- APKCodeAnalyzer._string_entropy() — Shannon entropy computation
- DecompileAPK strings export table schema
- IOCExtractorFromResults with APK-format analysis_results
- Worker IOC wiring for APK
"""
import os
import tempfile
from dataclasses import asdict
from unittest.mock import MagicMock, patch

import pytest

from redb.extractors.ioc_extractor.ioc_extractor import IOCExtractorFromResults
from redb.extractors.ioc_extractor.standalone_ioc_extractor import (
    IOCType,
    SourceType,
)

pytestmark = [pytest.mark.unit, pytest.mark.apk]


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_analyzer(min_instructions=1):
    """Create an APKCodeAnalyzer with mocked dependencies."""
    with patch(
        "redb.extractors.decompiler.apk.analyzer.JADXDecompiler"
    ), patch(
        "redb.extractors.decompiler.apk.analyzer.ApktoolDisassembler"
    ):
        from redb.extractors.decompiler.apk.analyzer import APKCodeAnalyzer
        analyzer = APKCodeAnalyzer("/fake/path.apk")
        analyzer.min_instructions = min_instructions
        return analyzer


def _make_mock_dex(strings):
    """Create a mock DEX object that returns the given strings."""
    dex = MagicMock()
    dex.get_strings.return_value = strings
    return dex


def _create_fake_apk_file():
    """Create a temporary file to act as a fake APK."""
    fd, path = tempfile.mkstemp(suffix=".apk")
    os.write(fd, b"PK\x03\x04fake apk content for hashing")
    os.close(fd)
    return path


# ---------------------------------------------------------------------------
# Tests: _string_entropy
# ---------------------------------------------------------------------------

class TestStringEntropy:
    """Tests for Shannon entropy computation."""

    def test_empty_string(self):
        analyzer = _make_analyzer()
        assert analyzer._string_entropy("") == 0.0

    def test_single_char(self):
        analyzer = _make_analyzer()
        assert analyzer._string_entropy("a") == 0.0

    def test_repeated_char(self):
        analyzer = _make_analyzer()
        assert analyzer._string_entropy("aaaa") == 0.0

    def test_two_equal_chars(self):
        analyzer = _make_analyzer()
        # "ab" -> entropy = 1.0
        assert abs(analyzer._string_entropy("ab") - 1.0) < 0.01

    def test_higher_entropy(self):
        analyzer = _make_analyzer()
        # More variety = higher entropy
        low = analyzer._string_entropy("aabb")
        high = analyzer._string_entropy("abcd")
        assert high > low

    def test_url_string_entropy(self):
        analyzer = _make_analyzer()
        entropy = analyzer._string_entropy("https://evil.com/payload")
        assert entropy > 2.0  # URLs have moderate entropy


# ---------------------------------------------------------------------------
# Tests: _extract_strings
# ---------------------------------------------------------------------------

class TestExtractStrings:
    """Tests for DEX string extraction."""

    def test_basic_extraction(self):
        analyzer = _make_analyzer()
        dex = _make_mock_dex(["hello", "world"])
        result = analyzer._extract_strings([dex])

        assert len(result) == 2
        assert result[0]["string"] == "hello"
        assert result[1]["string"] == "world"

    def test_string_fields_present(self):
        analyzer = _make_analyzer()
        dex = _make_mock_dex(["test_string"])
        result = analyzer._extract_strings([dex])

        entry = result[0]
        assert "string" in entry
        assert "string_encoding" in entry
        assert "string_offset" in entry
        assert "string_length" in entry
        assert "string_entropy" in entry
        assert entry["string_encoding"] == "UTF8"
        assert entry["string_length"] == len("test_string")

    def test_deduplication_across_dex(self):
        """Same string in multiple DEX files is only extracted once."""
        analyzer = _make_analyzer()
        dex1 = _make_mock_dex(["shared", "unique1"])
        dex2 = _make_mock_dex(["shared", "unique2"])
        result = analyzer._extract_strings([dex1, dex2])

        string_values = [s["string"] for s in result]
        assert string_values.count("shared") == 1
        assert "unique1" in string_values
        assert "unique2" in string_values
        assert len(result) == 3

    def test_empty_strings_filtered(self):
        analyzer = _make_analyzer()
        dex = _make_mock_dex(["", "valid", None, "also_valid"])
        result = analyzer._extract_strings([dex])

        string_values = [s["string"] for s in result]
        assert "" not in string_values
        assert None not in string_values
        assert "valid" in string_values
        assert "also_valid" in string_values

    def test_no_dex_files(self):
        analyzer = _make_analyzer()
        result = analyzer._extract_strings([])
        assert result == []

    def test_dex_get_strings_returns_none(self):
        analyzer = _make_analyzer()
        dex = MagicMock()
        dex.get_strings.return_value = None
        result = analyzer._extract_strings([dex])
        assert result == []

    def test_dex_get_strings_exception(self):
        analyzer = _make_analyzer()
        dex = MagicMock()
        dex.get_strings.side_effect = RuntimeError("corrupt DEX")
        result = analyzer._extract_strings([dex])
        assert result == []

    def test_entropy_computed(self):
        analyzer = _make_analyzer()
        dex = _make_mock_dex(["abcdefgh"])
        result = analyzer._extract_strings([dex])
        assert result[0]["string_entropy"] > 0.0

    def test_incremental_offsets(self):
        """String offsets are incremented sequentially."""
        analyzer = _make_analyzer()
        dex = _make_mock_dex(["a", "b", "c"])
        result = analyzer._extract_strings([dex])

        offsets = [s["string_offset"] for s in result]
        assert offsets == [0, 1, 2]

    def test_url_strings_preserved(self):
        """URL strings are extracted without modification."""
        analyzer = _make_analyzer()
        dex = _make_mock_dex([
            "https://evil.com/payload",
            "http://c2.malware.org/gate",
        ])
        result = analyzer._extract_strings([dex])
        strings = [s["string"] for s in result]
        assert "https://evil.com/payload" in strings
        assert "http://c2.malware.org/gate" in strings


# ---------------------------------------------------------------------------
# Tests: strings in extract() integration
# ---------------------------------------------------------------------------

class TestExtractStringsIntegration:
    """Tests that strings are included in extract() results."""

    @patch("redb.extractors.decompiler.apk.analyzer.APKCodeAnalyzer._run_androguard")
    @patch("redb.extractors.decompiler.apk.analyzer.ApktoolDisassembler")
    @patch("redb.extractors.decompiler.apk.analyzer.JADXDecompiler")
    def test_extract_includes_strings(self, mock_jadx, mock_apktool, mock_androguard):
        from redb.extractors.decompiler.apk.analyzer import APKCodeAnalyzer

        mock_dex = _make_mock_dex(["hello", "world"])
        mock_analysis = MagicMock()
        mock_analysis.get_methods.return_value = []
        mock_androguard.return_value = (MagicMock(), [mock_dex], mock_analysis)

        analyzer = APKCodeAnalyzer("/fake/path.apk", log=MagicMock())
        results = analyzer.extract()

        assert "strings" in results
        assert len(results["strings"]) == 2
        assert results["strings"][0]["string"] == "hello"
        analyzer.cleanup()

    @patch("redb.extractors.decompiler.apk.analyzer.APKCodeAnalyzer._run_androguard")
    @patch("redb.extractors.decompiler.apk.analyzer.ApktoolDisassembler")
    @patch("redb.extractors.decompiler.apk.analyzer.JADXDecompiler")
    def test_extract_strings_error_is_non_fatal(self, mock_jadx, mock_apktool, mock_androguard):
        from redb.extractors.decompiler.apk.analyzer import APKCodeAnalyzer

        # Return a bad dex list that will cause _extract_strings to fail
        mock_analysis = MagicMock()
        mock_analysis.get_methods.return_value = []
        # dexs is not iterable — triggers the except block
        mock_androguard.return_value = (MagicMock(), 42, mock_analysis)

        analyzer = APKCodeAnalyzer("/fake/path.apk", log=MagicMock())
        results = analyzer.extract()

        # Should not crash, strings should be empty, error recorded
        assert results["strings"] == []
        assert any(
            e.get("error_location") == "strings"
            for e in results["analysis_errors"]
        )
        analyzer.cleanup()


# ---------------------------------------------------------------------------
# Tests: DecompileAPK strings export table
# ---------------------------------------------------------------------------

class TestDecompileAPKStringsExport:
    """Tests for strings table in DecompileAPK.prepare_export_data()."""

    def _make_extractor_with_strings(self, strings_data):
        from redb.extractors.decompiler.DecompileAPK import DecompileAPK
        apk_path = _create_fake_apk_file()
        ext = DecompileAPK(apk_path, MagicMock())
        ext.analysis_results = {
            "sha256": "a" * 64,
            "sha1": "b" * 40,
            "md5": "c" * 32,
            "decompiled_content": [],
            "decompiled_refs": [],
            "smali_content": [],
            "smali_refs": [],
            "similarity_metrics": [],
            "strings": strings_data,
            "analysis_errors": [],
        }
        return ext, apk_path

    def test_strings_table_present(self):
        ext, path = self._make_extractor_with_strings([
            {"string": "hello", "string_encoding": "UTF8",
             "string_offset": 0, "string_length": 5, "string_entropy": 2.32}
        ])
        try:
            export = ext.prepare_export_data("ClickHouseExporter")
            assert "strings_raw" in export
        finally:
            os.unlink(path)

    def test_strings_table_schema(self):
        ext, path = self._make_extractor_with_strings([
            {"string": "hello", "string_encoding": "UTF8",
             "string_offset": 0, "string_length": 5, "string_entropy": 2.32}
        ])
        try:
            export = ext.prepare_export_data("ClickHouseExporter")
            sr = export["strings_raw"]
            assert sr["table"] == "code_binja_strings_raw"
            assert len(sr["column_names"]) == 8
            assert len(sr["column_type_names"]) == 8
            assert "sha256" in sr["column_names"]
            assert "string" in sr["column_names"]
            assert "string_raw" in sr["column_names"]
            assert "string_encoding" in sr["column_names"]
            assert "string_offset" in sr["column_names"]
            assert "string_length" in sr["column_names"]
            assert "string_raw_length" in sr["column_names"]
            assert "string_entropy" in sr["column_names"]
            assert len(sr["data"]) == 1
            assert len(sr["data"][0]) == 8
        finally:
            os.unlink(path)

    def test_strings_sha256_propagated(self):
        ext, path = self._make_extractor_with_strings([
            {"string": "test", "string_encoding": "UTF8",
             "string_offset": 0, "string_length": 4, "string_entropy": 2.0}
        ])
        try:
            export = ext.prepare_export_data("ClickHouseExporter")
            row = export["strings_raw"]["data"][0]
            assert row[0] == "a" * 64  # sha256 is first column
        finally:
            os.unlink(path)

    def test_empty_strings_not_exported(self):
        ext, path = self._make_extractor_with_strings([])
        try:
            export = ext.prepare_export_data("ClickHouseExporter")
            assert "strings_raw" not in export
        finally:
            os.unlink(path)


# ---------------------------------------------------------------------------
# Tests: IOCExtractorFromResults with APK format
# ---------------------------------------------------------------------------

class TestIOCExtractorAPKFormat:
    """Tests that IOCExtractorFromResults handles APK analysis_results."""

    def setup_method(self):
        self.log = MagicMock()

    def test_extract_iocs_from_apk_strings(self):
        """IOCs extracted from APK strings (same format as Binja)."""
        results = {
            "strings": [
                {"string": "https://evil.com/payload", "string_offset": 0},
                {"string": "Contact [email protected]", "string_offset": 1},
            ],
            "decompiled": [],
            "decompiled_content": [],
        }
        extractor = IOCExtractorFromResults(results, sha256="a" * 64, log=self.log)
        iocs = extractor.extract()

        url_iocs = [i for i in iocs if i.ioc_type == IOCType.URL]
        assert len(url_iocs) >= 1
        assert any("evil.com" in i.ioc_value for i in url_iocs)

    def test_extract_iocs_from_apk_decompiled_content(self):
        """IOCs extracted from APK's decompiled_content key."""
        results = {
            "strings": [],
            "decompiled": [],  # Binja format — empty
            "decompiled_content": [  # APK format
                {
                    "decompiled_method": "connect('https://c2.malware.org/gate');",
                    "decompiled_method_hash": "hash123",
                    "method_type": "USER",
                }
            ],
        }
        extractor = IOCExtractorFromResults(results, sha256="a" * 64, log=self.log)
        iocs = extractor.extract()

        url_iocs = [i for i in iocs if i.ioc_type == IOCType.URL]
        assert len(url_iocs) >= 1

    def test_apk_library_methods_skipped(self):
        """LIBRARY methods in decompiled_content are skipped."""
        results = {
            "strings": [],
            "decompiled": [],
            "decompiled_content": [
                {
                    "decompiled_method": "https://should-skip.com",
                    "decompiled_method_hash": "lib_hash",
                    "method_type": "LIBRARY",
                }
            ],
        }
        extractor = IOCExtractorFromResults(results, sha256="a" * 64, log=self.log)
        iocs = extractor.extract()
        assert len(iocs) == 0

    def test_combined_binja_and_apk_sources(self):
        """Both Binja and APK format decompiled entries are processed."""
        results = {
            "strings": [],
            "decompiled": [
                {
                    "decompiled_function": "call('https://binja.example.com');",
                    "decompiled_function_hash": "binja_hash",
                    "function_type": "USER",
                }
            ],
            "decompiled_content": [
                {
                    "decompiled_method": "send('https://apk.example.com');",
                    "decompiled_method_hash": "apk_hash",
                    "method_type": "USER",
                }
            ],
        }
        extractor = IOCExtractorFromResults(results, sha256="a" * 64, log=self.log)
        iocs = extractor.extract()

        url_iocs = [i for i in iocs if i.ioc_type == IOCType.URL]
        urls = [i.ioc_value for i in url_iocs]
        assert any("binja.example.com" in u for u in urls)
        assert any("apk.example.com" in u for u in urls)

    def test_apk_results_only(self):
        """Works when only APK keys are present (no 'decompiled' key)."""
        results = {
            "strings": [
                {"string": "https://evil.com", "string_offset": 0},
            ],
            "decompiled_content": [
                {
                    "decompiled_method": "x = 1",
                    "decompiled_method_hash": "h",
                    "method_type": "USER",
                }
            ],
        }
        extractor = IOCExtractorFromResults(results, sha256="a" * 64, log=self.log)
        iocs = extractor.extract()
        # Should not crash, and should find URL from strings
        url_iocs = [i for i in iocs if i.ioc_type == IOCType.URL]
        assert len(url_iocs) >= 1

    def test_source_type_is_string_for_strings(self):
        results = {
            "strings": [
                {"string": "https://evil.com/test", "string_offset": 42},
            ],
            "decompiled_content": [],
        }
        extractor = IOCExtractorFromResults(results, sha256="a" * 64, log=self.log)
        iocs = extractor.extract()
        url_iocs = [i for i in iocs if i.ioc_type == IOCType.URL]
        assert url_iocs[0].source_type == SourceType.STRING
        assert url_iocs[0].source_identifier == "42"

    def test_source_type_is_decompiled_for_methods(self):
        results = {
            "strings": [],
            "decompiled_content": [
                {
                    "decompiled_method": "https://evil.com/method",
                    "decompiled_method_hash": "myhash",
                    "method_type": "USER",
                }
            ],
        }
        extractor = IOCExtractorFromResults(results, sha256="a" * 64, log=self.log)
        iocs = extractor.extract()
        url_iocs = [i for i in iocs if i.ioc_type == IOCType.URL]
        assert url_iocs[0].source_type == SourceType.DECOMPILED_FUNCTION
        assert url_iocs[0].source_identifier == "myhash"


# ---------------------------------------------------------------------------
# Tests: Worker IOC wiring for APK
# ---------------------------------------------------------------------------

class TestWorkerAPKIOCWiring:
    """Tests that workers.py wires IOC extraction for APK."""

    def test_worker_source_contains_ioc_for_apk(self):
        """Verify workers.py source has IOC extraction in APK branch."""
        import inspect
        from redb import workers
        source = inspect.getsource(workers)
        # APK branch should import IOCExtractorFromResults
        assert "IOCExtractorFromResults" in source
        # Should be in the APK section (near DecompileAPK)
        apk_section = source[source.index("DecompileAPK("):]
        ioc_pos = apk_section.find("IOCExtractorFromResults")
        assert ioc_pos != -1, "IOCExtractorFromResults not found after DecompileAPK"