From 1e471a96dfbe8162ef0678cf39e8a416992e972c Mon Sep 17 00:00:00 2001 From: Amine Date: Mon, 19 May 2025 19:33:06 +0100 Subject: [PATCH 1/7] feat(heuristics): add Whitespace Check to detect excessive spacing and invisible characters Signed-off-by: Amine --- .../sourcecode/white_spaces.py | 98 +++++++++++++++++++ .../checks/detect_malicious_metadata_check.py | 1 + .../pypi/test_white_spaces.py | 70 +++++++++++++ 3 files changed, 169 insertions(+) create mode 100644 src/macaron/malware_analyzer/pypi_heuristics/sourcecode/white_spaces.py create mode 100644 tests/malware_analyzer/pypi/test_white_spaces.py diff --git a/src/macaron/malware_analyzer/pypi_heuristics/sourcecode/white_spaces.py b/src/macaron/malware_analyzer/pypi_heuristics/sourcecode/white_spaces.py new file mode 100644 index 000000000..16521dba6 --- /dev/null +++ b/src/macaron/malware_analyzer/pypi_heuristics/sourcecode/white_spaces.py @@ -0,0 +1,98 @@ +# Copyright (c) 2024 - 2025, Oracle and/or its affiliates. All rights reserved. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. + +"""This analyzer checks if the package has white spaces or invisible characters in the code.""" + +import logging +import re + +from macaron.config.defaults import defaults +from macaron.json_tools import JsonType +from macaron.malware_analyzer.pypi_heuristics.base_analyzer import BaseHeuristicAnalyzer +from macaron.malware_analyzer.pypi_heuristics.heuristics import HeuristicResult, Heuristics +from macaron.slsa_analyzer.package_registry.pypi_registry import PyPIPackageJsonAsset + +logger: logging.Logger = logging.getLogger(__name__) + + +class WhiteSpacesAnalyzer(BaseHeuristicAnalyzer): + """Check whether the code has successive white spaces or invisible characters.""" + + INVISIBLE_CHARS = [ + "\u200b", + "\u200c", + "\u200d", + "\ufeff", + "\u200e", + "\u200f", + "\u00a0", + "\u00ad", + " ", + ] + + def __init__(self) -> None: + super().__init__( + name="white_spaces_analyzer", + heuristic=Heuristics.WHITE_SPACES, + depends_on=None, + ) + + self.repeated_spaces_threshold = self._load_defaults() + + def _load_defaults(self) -> int: + """Load default settings from defaults.ini. + + Returns + ------- + int: + The repeated spaces threshold. + """ + section_name = "heuristic.pypi" + if defaults.has_section(section_name): + section = defaults[section_name] + return section.getint("repeated_spaces_threshold", 50) + + return 50 + + def analyze(self, pypi_package_json: PyPIPackageJsonAsset) -> tuple[HeuristicResult, dict[str, JsonType]]: + """Analyze the package. + + Parameters + ---------- + pypi_package_json: PyPIPackageJsonAsset + The PyPI package JSON asset object. + + Returns + ------- + tuple[HeuristicResult, dict[str, JsonType]]: + The result and related information collected during the analysis. + """ + scripts: dict[str, str] | None = pypi_package_json.get_sourcecode() + if scripts is None: + return HeuristicResult.SKIP, {} + + for file, content in scripts.items(): + if file.endswith(".py") and self.has_white_spaces(content): + return HeuristicResult.FAIL, { + "file": file, + } + return HeuristicResult.PASS, {} + + def has_white_spaces(self, code_string: str) -> bool: + """Check for excessive or invisible whitespace characters in a code string. + + Parameters + ---------- + code_string: str + The code string to check. + + Returns + ------- + bool: + True if suspicious patterns are found, False otherwise. + """ + char_class = "".join(self.INVISIBLE_CHARS) + regex_pattern = f"[{char_class}]{{{self.repeated_spaces_threshold},}}" + if re.search(regex_pattern, code_string, re.DOTALL): + return True + return False diff --git a/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py b/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py index da49be6bb..5bc46561b 100644 --- a/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py +++ b/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py @@ -34,6 +34,7 @@ from macaron.malware_analyzer.pypi_heuristics.metadata.wheel_absence import WheelAbsenceAnalyzer from macaron.malware_analyzer.pypi_heuristics.sourcecode.pypi_sourcecode_analyzer import PyPISourcecodeAnalyzer from macaron.malware_analyzer.pypi_heuristics.sourcecode.suspicious_setup import SuspiciousSetupAnalyzer +from macaron.malware_analyzer.pypi_heuristics.sourcecode.white_spaces import WhiteSpacesAnalyzer from macaron.slsa_analyzer.analyze_context import AnalyzeContext from macaron.slsa_analyzer.checks.base_check import BaseCheck from macaron.slsa_analyzer.checks.check_result import CheckResultData, CheckResultType, Confidence, JustificationType diff --git a/tests/malware_analyzer/pypi/test_white_spaces.py b/tests/malware_analyzer/pypi/test_white_spaces.py new file mode 100644 index 000000000..500ef00b5 --- /dev/null +++ b/tests/malware_analyzer/pypi/test_white_spaces.py @@ -0,0 +1,70 @@ +# Copyright (c) 2024 - 2025, Oracle and/or its affiliates. All rights reserved. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. + +"""Tests for the WhiteSpacesAnalyzer heuristic.""" +# pylint: disable=redefined-outer-name + + +from unittest.mock import MagicMock + +import pytest + +from macaron.malware_analyzer.pypi_heuristics.heuristics import HeuristicResult +from macaron.malware_analyzer.pypi_heuristics.sourcecode.white_spaces import WhiteSpacesAnalyzer + + +@pytest.fixture() +def analyzer() -> WhiteSpacesAnalyzer: + """Pytest fixture to create a WhiteSpacesAnalyzer instance.""" + analyzer_instance = WhiteSpacesAnalyzer() + return analyzer_instance + + +def test_analyze_no_sourcecode(analyzer: WhiteSpacesAnalyzer, pypi_package_json: MagicMock) -> None: + """Test the analyzer skips when there is no source code.""" + pypi_package_json.get_sourcecode.return_value = None + result, info = analyzer.analyze(pypi_package_json) + assert result == HeuristicResult.SKIP + assert info == {} + + +def test_analyze_pass(analyzer: WhiteSpacesAnalyzer, pypi_package_json: MagicMock) -> None: + """Test the analyzer passes when no suspicious whitespace is found.""" + pypi_package_json.get_sourcecode.return_value = {"test.py": "print('hello')"} + result, info = analyzer.analyze(pypi_package_json) + assert result == HeuristicResult.PASS + assert info == {} + + +def test_analyze_fail_long_spaces(analyzer: WhiteSpacesAnalyzer, pypi_package_json: MagicMock) -> None: + """Test the analyzer fails when long spaces are found.""" + repeated_spaces_threshold = analyzer.repeated_spaces_threshold + code = f"print('hello')\n{' ' * (repeated_spaces_threshold + 1)}print('world')" + pypi_package_json.get_sourcecode.return_value = {"test.py": code} + result, info = analyzer.analyze(pypi_package_json) + assert result == HeuristicResult.FAIL + assert info["file"] == "test.py" + + +def test_analyze_fail_invisible_chars(analyzer: WhiteSpacesAnalyzer, pypi_package_json: MagicMock) -> None: + """Test the analyzer fails when invisible characters are found.""" + repeated_spaces_threshold = analyzer.repeated_spaces_threshold + invisible_char = "\u200b" # Zero-width space. + code = f"print('hello'){invisible_char * repeated_spaces_threshold}print('world')" + pypi_package_json.get_sourcecode.return_value = {"test.py": code} + result, info = analyzer.analyze(pypi_package_json) + assert result == HeuristicResult.FAIL + assert info["file"] == "test.py" + + +def test_has_white_spaces_long_spaces(analyzer: WhiteSpacesAnalyzer) -> None: + """Test has_white_spaces method with long spaces.""" + repeated_spaces_threshold = analyzer.repeated_spaces_threshold + code = f"print('hello')\n{' ' * repeated_spaces_threshold}print('world')" + assert analyzer.has_white_spaces(code) + + +def test_has_white_spaces_no_suspicious(analyzer: WhiteSpacesAnalyzer) -> None: + """Test has_white_spaces method with no suspicious whitespace.""" + code = "print('hello')\nprint('world')" + assert not analyzer.has_white_spaces(code) From 7133401c18b2150e078aa711aa49f559f5d59db7 Mon Sep 17 00:00:00 2001 From: Amine Date: Mon, 26 May 2025 10:51:13 +0100 Subject: [PATCH 2/7] chore: add config variable to defaults.ini and minor cleanup Signed-off-by: Amine --- .gitignore | 2 +- .../pypi_heuristics/sourcecode/white_spaces.py | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 758a3d0cb..ddf49dfd0 100644 --- a/.gitignore +++ b/.gitignore @@ -181,4 +181,4 @@ docs/_build bin/ requirements.txt .macaron_env_file -**/.DS_Store +.DS_Store diff --git a/src/macaron/malware_analyzer/pypi_heuristics/sourcecode/white_spaces.py b/src/macaron/malware_analyzer/pypi_heuristics/sourcecode/white_spaces.py index 16521dba6..0807afd80 100644 --- a/src/macaron/malware_analyzer/pypi_heuristics/sourcecode/white_spaces.py +++ b/src/macaron/malware_analyzer/pypi_heuristics/sourcecode/white_spaces.py @@ -48,11 +48,16 @@ def _load_defaults(self) -> int: The repeated spaces threshold. """ section_name = "heuristic.pypi" + default_threshold = 50 + if defaults.has_section(section_name): section = defaults[section_name] - return section.getint("repeated_spaces_threshold", 50) + value_str = section.get("repeated_spaces_threshold", fallback=str(default_threshold)) + if value_str is not None and value_str.isdigit(): + return int(value_str) + return default_threshold - return 50 + return default_threshold def analyze(self, pypi_package_json: PyPIPackageJsonAsset) -> tuple[HeuristicResult, dict[str, JsonType]]: """Analyze the package. From e06b0414ec2d2f3523b420b7bbafa5471ef64bb8 Mon Sep 17 00:00:00 2001 From: Amine Date: Mon, 19 May 2025 19:33:06 +0100 Subject: [PATCH 3/7] feat(heuristics): add Whitespace Check to detect excessive spacing and invisible characters Signed-off-by: Amine --- .../pypi_heuristics/sourcecode/white_spaces.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/macaron/malware_analyzer/pypi_heuristics/sourcecode/white_spaces.py b/src/macaron/malware_analyzer/pypi_heuristics/sourcecode/white_spaces.py index 0807afd80..16521dba6 100644 --- a/src/macaron/malware_analyzer/pypi_heuristics/sourcecode/white_spaces.py +++ b/src/macaron/malware_analyzer/pypi_heuristics/sourcecode/white_spaces.py @@ -48,16 +48,11 @@ def _load_defaults(self) -> int: The repeated spaces threshold. """ section_name = "heuristic.pypi" - default_threshold = 50 - if defaults.has_section(section_name): section = defaults[section_name] - value_str = section.get("repeated_spaces_threshold", fallback=str(default_threshold)) - if value_str is not None and value_str.isdigit(): - return int(value_str) - return default_threshold + return section.getint("repeated_spaces_threshold", 50) - return default_threshold + return 50 def analyze(self, pypi_package_json: PyPIPackageJsonAsset) -> tuple[HeuristicResult, dict[str, JsonType]]: """Analyze the package. From 41b27d7470da938d2717ac108da59fdd0c85a4dd Mon Sep 17 00:00:00 2001 From: Amine Date: Mon, 26 May 2025 10:51:13 +0100 Subject: [PATCH 4/7] chore: add config variable to defaults.ini and minor cleanup Signed-off-by: Amine --- .gitignore | 2 +- .../pypi_heuristics/sourcecode/white_spaces.py | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index ddf49dfd0..ce53f7dcf 100644 --- a/.gitignore +++ b/.gitignore @@ -181,4 +181,4 @@ docs/_build bin/ requirements.txt .macaron_env_file -.DS_Store +**/.DS_Store \ No newline at end of file diff --git a/src/macaron/malware_analyzer/pypi_heuristics/sourcecode/white_spaces.py b/src/macaron/malware_analyzer/pypi_heuristics/sourcecode/white_spaces.py index 16521dba6..0807afd80 100644 --- a/src/macaron/malware_analyzer/pypi_heuristics/sourcecode/white_spaces.py +++ b/src/macaron/malware_analyzer/pypi_heuristics/sourcecode/white_spaces.py @@ -48,11 +48,16 @@ def _load_defaults(self) -> int: The repeated spaces threshold. """ section_name = "heuristic.pypi" + default_threshold = 50 + if defaults.has_section(section_name): section = defaults[section_name] - return section.getint("repeated_spaces_threshold", 50) + value_str = section.get("repeated_spaces_threshold", fallback=str(default_threshold)) + if value_str is not None and value_str.isdigit(): + return int(value_str) + return default_threshold - return 50 + return default_threshold def analyze(self, pypi_package_json: PyPIPackageJsonAsset) -> tuple[HeuristicResult, dict[str, JsonType]]: """Analyze the package. From 9d614de93daf9800bdcd17296c13f418cb6b5524 Mon Sep 17 00:00:00 2001 From: Amine Date: Tue, 17 Jun 2025 16:17:45 +0100 Subject: [PATCH 5/7] feat(heuristics): add Whitespace Check to Semgrep Signed-off-by: Amine --- .../sourcecode/white_spaces.py | 103 ------------------ .../pypi_malware_rules/obfuscation.yaml | 9 ++ .../checks/detect_malicious_metadata_check.py | 3 +- .../obfuscation/excessive_spacing.py | 25 +++++ .../obfuscation/expected_results.json | 15 +++ .../obfuscation/inline_imports.py | 2 +- .../pypi/test_white_spaces.py | 70 ------------ 7 files changed, 51 insertions(+), 176 deletions(-) delete mode 100644 src/macaron/malware_analyzer/pypi_heuristics/sourcecode/white_spaces.py create mode 100644 tests/malware_analyzer/pypi/resources/sourcecode_samples/obfuscation/excessive_spacing.py delete mode 100644 tests/malware_analyzer/pypi/test_white_spaces.py diff --git a/src/macaron/malware_analyzer/pypi_heuristics/sourcecode/white_spaces.py b/src/macaron/malware_analyzer/pypi_heuristics/sourcecode/white_spaces.py deleted file mode 100644 index 0807afd80..000000000 --- a/src/macaron/malware_analyzer/pypi_heuristics/sourcecode/white_spaces.py +++ /dev/null @@ -1,103 +0,0 @@ -# Copyright (c) 2024 - 2025, Oracle and/or its affiliates. All rights reserved. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. - -"""This analyzer checks if the package has white spaces or invisible characters in the code.""" - -import logging -import re - -from macaron.config.defaults import defaults -from macaron.json_tools import JsonType -from macaron.malware_analyzer.pypi_heuristics.base_analyzer import BaseHeuristicAnalyzer -from macaron.malware_analyzer.pypi_heuristics.heuristics import HeuristicResult, Heuristics -from macaron.slsa_analyzer.package_registry.pypi_registry import PyPIPackageJsonAsset - -logger: logging.Logger = logging.getLogger(__name__) - - -class WhiteSpacesAnalyzer(BaseHeuristicAnalyzer): - """Check whether the code has successive white spaces or invisible characters.""" - - INVISIBLE_CHARS = [ - "\u200b", - "\u200c", - "\u200d", - "\ufeff", - "\u200e", - "\u200f", - "\u00a0", - "\u00ad", - " ", - ] - - def __init__(self) -> None: - super().__init__( - name="white_spaces_analyzer", - heuristic=Heuristics.WHITE_SPACES, - depends_on=None, - ) - - self.repeated_spaces_threshold = self._load_defaults() - - def _load_defaults(self) -> int: - """Load default settings from defaults.ini. - - Returns - ------- - int: - The repeated spaces threshold. - """ - section_name = "heuristic.pypi" - default_threshold = 50 - - if defaults.has_section(section_name): - section = defaults[section_name] - value_str = section.get("repeated_spaces_threshold", fallback=str(default_threshold)) - if value_str is not None and value_str.isdigit(): - return int(value_str) - return default_threshold - - return default_threshold - - def analyze(self, pypi_package_json: PyPIPackageJsonAsset) -> tuple[HeuristicResult, dict[str, JsonType]]: - """Analyze the package. - - Parameters - ---------- - pypi_package_json: PyPIPackageJsonAsset - The PyPI package JSON asset object. - - Returns - ------- - tuple[HeuristicResult, dict[str, JsonType]]: - The result and related information collected during the analysis. - """ - scripts: dict[str, str] | None = pypi_package_json.get_sourcecode() - if scripts is None: - return HeuristicResult.SKIP, {} - - for file, content in scripts.items(): - if file.endswith(".py") and self.has_white_spaces(content): - return HeuristicResult.FAIL, { - "file": file, - } - return HeuristicResult.PASS, {} - - def has_white_spaces(self, code_string: str) -> bool: - """Check for excessive or invisible whitespace characters in a code string. - - Parameters - ---------- - code_string: str - The code string to check. - - Returns - ------- - bool: - True if suspicious patterns are found, False otherwise. - """ - char_class = "".join(self.INVISIBLE_CHARS) - regex_pattern = f"[{char_class}]{{{self.repeated_spaces_threshold},}}" - if re.search(regex_pattern, code_string, re.DOTALL): - return True - return False diff --git a/src/macaron/resources/pypi_malware_rules/obfuscation.yaml b/src/macaron/resources/pypi_malware_rules/obfuscation.yaml index 6d6ea066b..12b164614 100644 --- a/src/macaron/resources/pypi_malware_rules/obfuscation.yaml +++ b/src/macaron/resources/pypi_malware_rules/obfuscation.yaml @@ -311,3 +311,12 @@ rules: - pattern: os.writev(...) - pattern: os.pwrite(...) - pattern: os.pwritev(...) + +- id: obfuscation_excessive-spacing + metadata: + description: Detects the use of excessive spacing in code, which may indicate obfuscation or hidden code. + message: Hidden code after excessive spacing + languages: + - python + severity: WARNING + pattern-regex: ' {50,}[^ ]+' diff --git a/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py b/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py index 5bc46561b..527e611b1 100644 --- a/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py +++ b/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py @@ -34,7 +34,6 @@ from macaron.malware_analyzer.pypi_heuristics.metadata.wheel_absence import WheelAbsenceAnalyzer from macaron.malware_analyzer.pypi_heuristics.sourcecode.pypi_sourcecode_analyzer import PyPISourcecodeAnalyzer from macaron.malware_analyzer.pypi_heuristics.sourcecode.suspicious_setup import SuspiciousSetupAnalyzer -from macaron.malware_analyzer.pypi_heuristics.sourcecode.white_spaces import WhiteSpacesAnalyzer from macaron.slsa_analyzer.analyze_context import AnalyzeContext from macaron.slsa_analyzer.checks.base_check import BaseCheck from macaron.slsa_analyzer.checks.check_result import CheckResultData, CheckResultType, Confidence, JustificationType @@ -372,7 +371,7 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: FakeEmailAnalyzer, SimilarProjectAnalyzer, PackageDescriptionIntentAnalyzer, - TypeStubFileAnalyzer, + TypeStubFileAnalyzer ] # name used to query the result of all problog rules, so it can be accessed outside the model. diff --git a/tests/malware_analyzer/pypi/resources/sourcecode_samples/obfuscation/excessive_spacing.py b/tests/malware_analyzer/pypi/resources/sourcecode_samples/obfuscation/excessive_spacing.py new file mode 100644 index 000000000..22ea38a6f --- /dev/null +++ b/tests/malware_analyzer/pypi/resources/sourcecode_samples/obfuscation/excessive_spacing.py @@ -0,0 +1,25 @@ +# Copyright (c) 2025 - 2025, Oracle and/or its affiliates. All rights reserved. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. + +""" +Running this code will not produce any malicious behavior, but code isolation measures are +in place for safety. +""" + +import sys + +# ensure no symbols are exported so this code cannot accidentally be used +__all__ = [] +sys.exit() + +def test_function(): + """ + All code to be tested will be defined inside this function, so it is all local to it. This is + to isolate the code to be tested, as it exists to replicate the patterns present in malware + samples. + """ + sys.exit() + + # excessive spacing obfuscation + def excessive_spacing_flow(): + print("Hello world!") diff --git a/tests/malware_analyzer/pypi/resources/sourcecode_samples/obfuscation/expected_results.json b/tests/malware_analyzer/pypi/resources/sourcecode_samples/obfuscation/expected_results.json index aabf72e18..218c6acbe 100644 --- a/tests/malware_analyzer/pypi/resources/sourcecode_samples/obfuscation/expected_results.json +++ b/tests/malware_analyzer/pypi/resources/sourcecode_samples/obfuscation/expected_results.json @@ -229,6 +229,21 @@ "end": 68 } ] + }, + "src.macaron.resources.pypi_malware_rules.obfuscation_excessive-spacing": { + "message": "Hidden code after excessive spacing", + "detections": [ + { + "file": "obfuscation/excessive_spacing.py", + "start": 25, + "end": 25 + }, + { + "file": "obfuscation/inline_imports.py", + "start": 27, + "end": 28 + } + ] } }, "disabled_sourcecode_rule_findings": {} diff --git a/tests/malware_analyzer/pypi/resources/sourcecode_samples/obfuscation/inline_imports.py b/tests/malware_analyzer/pypi/resources/sourcecode_samples/obfuscation/inline_imports.py index 80e006781..4e37c7c02 100644 --- a/tests/malware_analyzer/pypi/resources/sourcecode_samples/obfuscation/inline_imports.py +++ b/tests/malware_analyzer/pypi/resources/sourcecode_samples/obfuscation/inline_imports.py @@ -24,7 +24,7 @@ def test_function(): __import__('builtins') __import__('subprocess') __import__('sys') - __import__('os') + print("Hello world!") ;__import__('os') __import__('zlib') __import__('marshal') # these both just import builtins diff --git a/tests/malware_analyzer/pypi/test_white_spaces.py b/tests/malware_analyzer/pypi/test_white_spaces.py deleted file mode 100644 index 500ef00b5..000000000 --- a/tests/malware_analyzer/pypi/test_white_spaces.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright (c) 2024 - 2025, Oracle and/or its affiliates. All rights reserved. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. - -"""Tests for the WhiteSpacesAnalyzer heuristic.""" -# pylint: disable=redefined-outer-name - - -from unittest.mock import MagicMock - -import pytest - -from macaron.malware_analyzer.pypi_heuristics.heuristics import HeuristicResult -from macaron.malware_analyzer.pypi_heuristics.sourcecode.white_spaces import WhiteSpacesAnalyzer - - -@pytest.fixture() -def analyzer() -> WhiteSpacesAnalyzer: - """Pytest fixture to create a WhiteSpacesAnalyzer instance.""" - analyzer_instance = WhiteSpacesAnalyzer() - return analyzer_instance - - -def test_analyze_no_sourcecode(analyzer: WhiteSpacesAnalyzer, pypi_package_json: MagicMock) -> None: - """Test the analyzer skips when there is no source code.""" - pypi_package_json.get_sourcecode.return_value = None - result, info = analyzer.analyze(pypi_package_json) - assert result == HeuristicResult.SKIP - assert info == {} - - -def test_analyze_pass(analyzer: WhiteSpacesAnalyzer, pypi_package_json: MagicMock) -> None: - """Test the analyzer passes when no suspicious whitespace is found.""" - pypi_package_json.get_sourcecode.return_value = {"test.py": "print('hello')"} - result, info = analyzer.analyze(pypi_package_json) - assert result == HeuristicResult.PASS - assert info == {} - - -def test_analyze_fail_long_spaces(analyzer: WhiteSpacesAnalyzer, pypi_package_json: MagicMock) -> None: - """Test the analyzer fails when long spaces are found.""" - repeated_spaces_threshold = analyzer.repeated_spaces_threshold - code = f"print('hello')\n{' ' * (repeated_spaces_threshold + 1)}print('world')" - pypi_package_json.get_sourcecode.return_value = {"test.py": code} - result, info = analyzer.analyze(pypi_package_json) - assert result == HeuristicResult.FAIL - assert info["file"] == "test.py" - - -def test_analyze_fail_invisible_chars(analyzer: WhiteSpacesAnalyzer, pypi_package_json: MagicMock) -> None: - """Test the analyzer fails when invisible characters are found.""" - repeated_spaces_threshold = analyzer.repeated_spaces_threshold - invisible_char = "\u200b" # Zero-width space. - code = f"print('hello'){invisible_char * repeated_spaces_threshold}print('world')" - pypi_package_json.get_sourcecode.return_value = {"test.py": code} - result, info = analyzer.analyze(pypi_package_json) - assert result == HeuristicResult.FAIL - assert info["file"] == "test.py" - - -def test_has_white_spaces_long_spaces(analyzer: WhiteSpacesAnalyzer) -> None: - """Test has_white_spaces method with long spaces.""" - repeated_spaces_threshold = analyzer.repeated_spaces_threshold - code = f"print('hello')\n{' ' * repeated_spaces_threshold}print('world')" - assert analyzer.has_white_spaces(code) - - -def test_has_white_spaces_no_suspicious(analyzer: WhiteSpacesAnalyzer) -> None: - """Test has_white_spaces method with no suspicious whitespace.""" - code = "print('hello')\nprint('world')" - assert not analyzer.has_white_spaces(code) From e19d07dae9108e0b256f4545fae10c5e2bcfc298 Mon Sep 17 00:00:00 2001 From: Amine Date: Mon, 23 Jun 2025 23:29:31 +0100 Subject: [PATCH 6/7] refactor(semgrep): update excessive spacing regex and clarify obfuscation threshold Signed-off-by: Amine --- src/macaron/resources/pypi_malware_rules/obfuscation.yaml | 6 ++++-- .../sourcecode_samples/obfuscation/expected_results.json | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/macaron/resources/pypi_malware_rules/obfuscation.yaml b/src/macaron/resources/pypi_malware_rules/obfuscation.yaml index 12b164614..68bd7d54b 100644 --- a/src/macaron/resources/pypi_malware_rules/obfuscation.yaml +++ b/src/macaron/resources/pypi_malware_rules/obfuscation.yaml @@ -318,5 +318,7 @@ rules: message: Hidden code after excessive spacing languages: - python - severity: WARNING - pattern-regex: ' {50,}[^ ]+' + severity: ERROR + patterns: + - pattern-regex: '[\s]{50,}(\S)+' # The 50 here is the threshold for excessive spacing , more than that is considered obfuscation + - pattern-not-regex: '"""[\s\S]*"""' diff --git a/tests/malware_analyzer/pypi/resources/sourcecode_samples/obfuscation/expected_results.json b/tests/malware_analyzer/pypi/resources/sourcecode_samples/obfuscation/expected_results.json index 218c6acbe..78b1467a2 100644 --- a/tests/malware_analyzer/pypi/resources/sourcecode_samples/obfuscation/expected_results.json +++ b/tests/malware_analyzer/pypi/resources/sourcecode_samples/obfuscation/expected_results.json @@ -235,13 +235,13 @@ "detections": [ { "file": "obfuscation/excessive_spacing.py", - "start": 25, + "start": 24, "end": 25 }, { "file": "obfuscation/inline_imports.py", "start": 27, - "end": 28 + "end": 27 } ] } From 667c4f09803e37e15fef280c67e8f7465b69a032 Mon Sep 17 00:00:00 2001 From: Carl Flottmann Date: Fri, 19 Sep 2025 17:02:40 +1000 Subject: [PATCH 7/7] chore: fix errors made in rebase process Signed-off-by: Carl Flottmann --- .gitignore | 2 +- .../slsa_analyzer/checks/detect_malicious_metadata_check.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index ce53f7dcf..758a3d0cb 100644 --- a/.gitignore +++ b/.gitignore @@ -181,4 +181,4 @@ docs/_build bin/ requirements.txt .macaron_env_file -**/.DS_Store \ No newline at end of file +**/.DS_Store diff --git a/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py b/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py index 527e611b1..da49be6bb 100644 --- a/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py +++ b/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py @@ -371,7 +371,7 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: FakeEmailAnalyzer, SimilarProjectAnalyzer, PackageDescriptionIntentAnalyzer, - TypeStubFileAnalyzer + TypeStubFileAnalyzer, ] # name used to query the result of all problog rules, so it can be accessed outside the model.