Hang Ren

69 papers A* 4B 2C 8Misc 2Journal 43Unranked 10
YearRankTypeTitle / Venue / Authors
2026 J jnl
IEEE Trans. Ind. Electron.
Hang Ren, Hanwen Zhang, Yanbo Wang, Haoyuan Yu, Pingyang Sun, Zhe Chen
2026 J jnl
IEEE Access
Xin Jiang, Zheng Sun, Hang Ren, Sitong Zhai
2025 A* conf
DAC
Jinglei Cheng, Yuchen Zhu, Yidong Zhou, Hang Ren, Zhixin Song, Zhiding Liang
2025 J jnl
Algorithms
Binglin Liu, Liwen Li, Hang Ren, Jianwan Qin, Weijiang Liu
2025 J jnl
Comput. Aided Des.
Jie Zhang, Luwei Chen, Hang Ren, Yan Luximon, Ping Li
2025 conf
CVCBT
Ioannis Bellos, Jiasun Li, Hang Ren
2025 J jnl
Neurocomputing
Jin Yang, Huijia Liang, Hang Ren, Dongqing Jia, Xin Wang
2025 J jnl
IEEE Access
Xin Wang, Dejiang Wang, Mingchao Sun, Hang Ren, Yulian Zhang, Songwei Han, Ligang Liu
2025 J jnl
Syst.
Hang Ren, Zhenke Zhang, Shengnan Jiang
2024 J jnl
Remote. Sens.
Hang Ren, Zheng Lu, Gaopeng Li, Yun Zhang, Xueying Yang, Yalin Guo, Long Li, Xin Qi, Qinglong Hua, Chang Ding, Huilin Mu, Yong Du
2024 B conf
IJCNN
Hang Ren, Jin Yang, Shengqiao Ni, Qin Yang, Jing Zhang, Nuo Qun
2024 C conf
IGARSS
Yun Zhang, Niezipeng Kang, Bin Zhao, Hang Ren, Qinglong Hua, Jin Tian
2024 J jnl
Artif. Intell. Eng. Des. Anal. Manuf.
Hang Ren, Shaogang Liu, Bo Qiu, Hong Guo, Dan Zhao
2024 J jnl
Axioms
Jiayi Qiu, Guanggui Chen, Yanyan Xu, Ying Luo, Hang Ren
2024 A* conf
DAC
Zhiding Liang, Zhixin Song, Jinglei Cheng, Hang Ren, Tianyi Hao, Rui Yang, Yiyu Shi, Tongyang Li
2024 J jnl
CoRR
Hong-Ye Hu, Andi Gu, Swarnadeep Majumder, Hang Ren, Yipei Zhang, Derek S. Wang, Yi-Zhuang You, Zlatko Minev, Susanne F. Yelin, Alireza Seif
2024 J jnl
Scientometrics
Xiaoli Cao, Xiang Chen, Lu Huang, Lijie Deng, Yijie Cai, Hang Ren
2024 J jnl
IEEE Trans. Geosci. Remote. Sens.
Yun Zhang, Hang Ren, Zheng Lu, Xueying Yang, Gaopeng Li
2024 J jnl
CoRR
Zhiding Liang, Gang Liu, Zheyuan Liu, Jinglei Cheng, Tianyi Hao, Kecheng Liu, Hang Ren, Zhixin Song, Ji Liu, Fanny Ye, Yiyu Shi
2024 conf
EEKE/AII@iConference
Lu Huang, Xiaoli Cao, Hang Ren, Chunze Zhang, Zhenxin Wu
2024 A* conf
DAC
Zhiding Liang, Gang Liu, Zheyuan Liu, Jinglei Cheng, Tianyi Hao, Kecheng Liu, Hang Ren, Zhixin Song, Ji Liu, Fanny Ye, Yiyu Shi
2024 J jnl
IEEE Trans. Comput. Aided Des. Integr. Circuits Syst.
Zhiding Liang, Jinglei Cheng, Hang Ren, Hanrui Wang, Fei Hua, Zhixin Song, Yongshan Ding, Frederic T. Chong, Song Han, Xuehai Qian, Yiyu Shi
2024 C conf
IGARSS
Qinglong Hua, Yun Zhang, Hang Ren, Niezipeng Kang, Haotian Wang, Yicheng Jiang
2024 B conf
IJCNN
Qin Yang, Jin Yang, Shengqiao Ni, Jing Zhang, Hang Ren, Nuo Qun
2024 C conf
IGARSS
Gaopeng Li, Xueying Yang, Yun Zhang, Bin Liu, Hang Ren
2024 C conf
IGARSS
Yun Zhang, Long Li, Wanling Liao, Xin Qi, Hang Ren
2023 C conf
IGARSS
Niezipeng Kang, Yun Zhang, Gaopeng Li, Hang Ren
2023 J jnl
Sensors
Hang Ren, Tongyou Liu, Jinwu Wang
2023 J jnl
Inf. Process. Manag.
Xiang Chen, Peifeng Ye, Lu Huang, Changtian Wang, Yijie Cai, Lijie Deng, Hang Ren
2023 J jnl
J. Vis. Commun. Image Represent.
Ying Zhao, Kechen Song, Wenqi Cui, Hang Ren, Yunhui Yan
2023 J jnl
Quantum Inf. Process.
Shuang Lu, Taotao Hu, Hang Ren, Xiaoxuan Gu, Xiaodan Li, Yiwen Gao, Yining Zhang, Jiameng Hong, Yuting Li
2023 J jnl
Quantum
Ryan Shaffer, Hang Ren, Emiliia Dyrenkova, Christopher G. Yale, Daniel S. Lobser, Ashlyn D. Burch, Matthew N. H. Chow, Melissa C. Revelle, Susan M. Clark, Hartmut Häffner
2023 C conf
IGARSS
Hang Ren, Yun Zhang, Gaopeng Li, Niezipeng Kang, Chenyue Lu
2022 J jnl
Sensors
Yining Mu, Yanzheng Li, Peng Du, Hang Ren, Idelfonso Tafur Monroy, Makram Ibrahim, Guanyu Wen, Dong Liang, Jianshang Feng, Jiayu Ao, Xiangyue Xie, Yumeng Li
2022 conf
EEKE@JCDL
Lu Huang, Xiaoli Cao, Yijie Cai, Hang Ren, Tianbin Xing, Peifeng Ye
2022 J jnl
Geo spatial Inf. Sci.
Priyanko Das, Zhenke Zhang, Hang Ren
2022 J jnl
Manuf. Serv. Oper. Manag.
Hang Ren, Tingliang Huang
2022 J jnl
CoRR
Zhiding Liang, Jinglei Cheng, Hang Ren, Hanrui Wang, Fei Hua, Yongshan Ding, Fred Chong, Song Han, Yiyu Shi, Xuehai Qian
2022 A* conf
ICLR
Hang Ren, Aivar Sootla, Taher Jafferjee, Junxiao Shen, Jun Wang, Haitham Bou-Ammar
2022 J jnl
CoRR
Hang Ren, Aivar Sootla, Taher Jafferjee, Junxiao Shen, Jun Wang, Haitham Bou-Ammar
2022 C conf
IGARSS
Yun Zhang, Hang Ren, Chenyue Lu, Gaopeng Li
2022 Misc conf
QCE
Zhiding Liang, Hanrui Wang, Jinglei Cheng, Yongshan Ding, Hang Ren, Zhengqi Gao, Zhirui Hu, Duane S. Boning, Xuehai Qian, Song Han, Weiwen Jiang, Yiyu Shi
2021 J jnl
CoRR
Vincent Moens, Hang Ren, Alexandre Maraval, Rasul Tutunov, Jun Wang, Haitham Ammar
2021 J jnl
Manag. Sci.
Kenan Arifoglu, Hang Ren, Tolga Tezcan
2021 J jnl
ISPRS Int. J. Geo Inf.
Shengnan Jiang, Zhenke Zhang, Hang Ren, Guoen Wei, Minghui Xu, Binglin Liu
2021 conf
ICAIT
Bowen Chen, Hang Ren, Wanzhuo Ma, Tianshu Wang
2021 conf
SPAC
Hongbo Nie, Junsheng Ren, Hang Ren
2020 J jnl
Sensors
Hang Ren, Taotao Hu
2020 conf
ML4CS (3)
Rui Zhang, Hui Xia, Jufu Cui, Yizhe Li, Shu-shu Shao, Hang Ren
2020 J jnl
Sensors
Hang Ren, Taotao Hu
2020 J jnl
Mob. Inf. Syst.
Rui Zhang, Hui Xia, Shu-shu Shao, Hang Ren, Shuai Xu, Xiangguo Cheng
2019 J jnl
CoRR
Mohammed Amin Abdullah, Hang Ren, Haitham Bou-Ammar, Vladimir Milenkovic, Rui Luo, Mingtian Zhang, Jun Wang
2018 J jnl
Sensors
Hang Ren, Taotao Hu, YuLong Song, Hui Sun, Bochao Liu, Minghe Gao
2018 J jnl
Comput. Oper. Res.
Hang Ren, Tingliang Huang
2017 J jnl
Quantum Inf. Process.
Taotao Hu, Qi Yang, Kang Xue, Gangcheng Wang, Yan Zhang, Xiaodan Li, Hang Ren
2016 J jnl
CoRR
Hang Ren, Weiqun Xu, Yonghong Yan
2016 J jnl
IEICE Trans. Inf. Syst.
Hang Ren, Qingwei Zhao, Yonghong Yan
2016 J jnl
IEEE Signal Process. Lett.
Hang Ren, Yonghong Yan
2015 C conf
ASRU
Hang Ren, Weiqun Xu, Yonghong Yan
2014 J jnl
Quantum Inf. Process.
Taotao Hu, Hang Ren, Kang Xue
2014 conf
SIGDIAL Conference
Hang Ren, Weiqun Xu, Yonghong Yan
2014 conf
SLT
Hang Ren, Weiqun Xu, Yonghong Yan
2014 J jnl
Quantum Inf. Process.
Taotao Hu, Hang Ren, Kang Xue
2014 J jnl
Quantum Inf. Process.
Taotao Hu, Hang Ren, Kang Xue
2013 conf
SIGDIAL Conference
Hang Ren, Weiqun Xu, Yan Zhang, Yonghong Yan
2013 Misc conf
ICNC
Haiqing Huang, Xiangzhong Fang, Xiangyang Li, Qingchun Lu, Hang Ren
2013 J jnl
Quantum Inf. Process.
Taotao Hu, Kang Xue, Chunfang Sun, Gangcheng Wang, Hang Ren
2013 conf
FSKD
Haiqing Huang, Xiangzhong Fang, Xiangyang Li, Qingchun Lu, Hang Ren
2011 J jnl
Quantum Inf. Process.
Taotao Hu, Hang Ren, Kang Xue
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"