Skip to content

Commit e9f16ec

Browse files
authored
Merge pull request abrignoni#672 from mobasi-team/security/pillow-supply-chain-hardening
Require Python 3.10+ for Pillow 12.1.1
2 parents 9b2a9b8 + c1c4a67 commit e9f16ec

5 files changed

Lines changed: 226 additions & 2 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Python Runtime Contract
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- '.github/workflows/python_runtime_contract.yml'
7+
- 'README.md'
8+
- 'requirements.txt'
9+
- 'admin/test/scripts/test_dependency_compatibility.py'
10+
- 'admin/test/scripts/test_runtime_requirements.py'
11+
12+
jobs:
13+
runtime-contract:
14+
runs-on: ubuntu-latest
15+
strategy:
16+
fail-fast: false
17+
matrix:
18+
python-version: ['3.10', '3.11']
19+
20+
steps:
21+
- name: Checkout code
22+
uses: actions/checkout@v4
23+
24+
- name: Set up Python
25+
uses: actions/setup-python@v5
26+
with:
27+
python-version: ${{ matrix.python-version }}
28+
29+
- name: Install runtime dependencies
30+
run: python -m pip install -r requirements.txt
31+
32+
- name: Run admin compatibility tests
33+
run: python -m unittest discover -s admin/test/scripts -p 'test_*.py'

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Details in blog post here: https://abrignoni.blogspot.com/2020/02/aleapp-android
88

99
## Requirements
1010

11-
**Python 3.9 or above** (older versions of 3.x will also work with the exception of one or two modules)
11+
**Python 3.10 or above**
1212

1313
### Dependencies
1414

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import importlib
2+
import os
3+
import subprocess
4+
import sys
5+
import tempfile
6+
import unittest
7+
from pathlib import Path
8+
9+
import bcrypt
10+
import bencoding
11+
import blackboxprotobuf
12+
import fitdecode
13+
import folium
14+
import polyline
15+
import pytz
16+
import simplekml
17+
import xlsxwriter
18+
import xmltodict
19+
from bs4 import BeautifulSoup
20+
from geopy.geocoders import Nominatim
21+
from packaging import version
22+
from PIL import Image
23+
from google.protobuf import descriptor
24+
from Crypto.Cipher import AES
25+
from Crypto.Util.Padding import pad, unpad
26+
27+
28+
THIRD_PARTY_IMPORTS = (
29+
"bcrypt",
30+
"bs4",
31+
"bencoding",
32+
"blackboxprotobuf",
33+
"fitdecode",
34+
"folium",
35+
"geopy",
36+
"packaging",
37+
"PIL",
38+
"polyline",
39+
"google.protobuf",
40+
"Crypto",
41+
"pytz",
42+
"simplekml",
43+
"wheel",
44+
"xlsxwriter",
45+
"xmltodict",
46+
)
47+
48+
REPO_ROOT = Path(__file__).resolve().parents[3]
49+
50+
CORE_MODULES = (
51+
"aleapp",
52+
"scripts.ilapfuncs",
53+
"scripts.plugin_loader",
54+
"scripts.artifacts.notification_history_pb.notificationhistory_pb2",
55+
"scripts.artifacts.usagestats_pb.usagestatsservice_pb2",
56+
)
57+
58+
59+
class TestDependencyCompatibility(unittest.TestCase):
60+
def test_declared_packages_are_importable(self):
61+
for module_name in THIRD_PARTY_IMPORTS:
62+
with self.subTest(module_name=module_name):
63+
importlib.import_module(module_name)
64+
65+
def test_core_modules_import_under_supported_python(self):
66+
for module_name in CORE_MODULES:
67+
with self.subTest(module_name=module_name):
68+
importlib.import_module(module_name)
69+
70+
def test_plugin_loader_imports_artifact_modules(self):
71+
from scripts.plugin_loader import PluginLoader
72+
73+
loader = PluginLoader()
74+
self.assertGreater(len(loader), 0)
75+
76+
def test_dependency_runtime_smoke_behaviors(self):
77+
self.assertTrue(bcrypt.checkpw(b"pw", bcrypt.hashpw(b"pw", bcrypt.gensalt())))
78+
79+
self.assertEqual(bencoding.bdecode(bencoding.bencode({b"a": 1})), {b"a": 1})
80+
81+
message, types = blackboxprotobuf.decode_message(b"\x08\x96\x01")
82+
self.assertEqual(message["1"], 150)
83+
self.assertEqual(types["1"]["type"], "int")
84+
85+
self.assertTrue(hasattr(fitdecode, "FitReader"))
86+
87+
with tempfile.NamedTemporaryFile(suffix=".html", delete=False) as html_file:
88+
try:
89+
folium.Map(location=[0, 0], zoom_start=1).save(html_file.name)
90+
self.assertGreater(os.path.getsize(html_file.name), 0)
91+
finally:
92+
os.unlink(html_file.name)
93+
94+
self.assertEqual(Nominatim(user_agent="aleapp-test").scheme, "https")
95+
self.assertLess(version.parse("1.2.3"), version.parse("2.0.0"))
96+
97+
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as image_file:
98+
try:
99+
Image.new("RGB", (2, 2), color="red").save(image_file.name)
100+
with Image.open(image_file.name) as reopened:
101+
self.assertEqual(reopened.size, (2, 2))
102+
finally:
103+
os.unlink(image_file.name)
104+
105+
encoded = polyline.encode([(38.5, -120.2), (40.7, -120.95)])
106+
self.assertEqual(polyline.decode(encoded), [(38.5, -120.2), (40.7, -120.95)])
107+
108+
self.assertIsNotNone(descriptor)
109+
110+
key = b"0123456789abcdef"
111+
cipher = AES.new(key, AES.MODE_ECB)
112+
ciphertext = cipher.encrypt(pad(b"hello world", AES.block_size))
113+
self.assertEqual(unpad(cipher.decrypt(ciphertext), AES.block_size), b"hello world")
114+
115+
self.assertEqual(pytz.timezone("UTC").zone, "UTC")
116+
117+
with tempfile.NamedTemporaryFile(suffix=".kml", delete=False) as kml_file:
118+
try:
119+
kml = simplekml.Kml()
120+
kml.newpoint(name="x", coords=[(0, 0)])
121+
kml.save(kml_file.name)
122+
self.assertGreater(os.path.getsize(kml_file.name), 0)
123+
finally:
124+
os.unlink(kml_file.name)
125+
126+
with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as xlsx_file:
127+
try:
128+
workbook = xlsxwriter.Workbook(xlsx_file.name)
129+
worksheet = workbook.add_worksheet("Sheet1")
130+
worksheet.write(0, 0, "ok")
131+
workbook.close()
132+
self.assertGreater(os.path.getsize(xlsx_file.name), 0)
133+
finally:
134+
os.unlink(xlsx_file.name)
135+
136+
self.assertEqual(xmltodict.parse("<root><a>1</a></root>")["root"]["a"], "1")
137+
self.assertEqual(BeautifulSoup("<p>hi</p>", "html.parser").p.text, "hi")
138+
139+
def test_cli_help_runs_under_supported_python(self):
140+
result = subprocess.run(
141+
[sys.executable, "aleapp.py", "--help"],
142+
cwd=REPO_ROOT,
143+
capture_output=True,
144+
text=True,
145+
check=False,
146+
)
147+
148+
self.assertEqual(result.returncode, 0, msg=result.stderr)
149+
self.assertIn("ALEAPP: Android Logs, Events, and Protobuf Parser.", result.stdout)
150+
151+
def test_imagetk_imports_when_tkinter_is_available(self):
152+
try:
153+
importlib.import_module("tkinter")
154+
except ModuleNotFoundError:
155+
self.skipTest("tkinter is not available in this Python build")
156+
157+
importlib.import_module("PIL.ImageTk")
158+
159+
160+
if __name__ == "__main__":
161+
unittest.main()
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import unittest
2+
from pathlib import Path
3+
4+
5+
REPO_ROOT = Path(__file__).resolve().parents[3]
6+
7+
8+
class TestRuntimeRequirements(unittest.TestCase):
9+
def test_readme_documents_python_3_10_or_above(self):
10+
readme = (REPO_ROOT / "README.md").read_text(encoding="utf-8")
11+
self.assertIn("**Python 3.10 or above**", readme)
12+
13+
def test_requirements_pin_latest_pillow(self):
14+
requirement_lines = (REPO_ROOT / "requirements.txt").read_text(encoding="utf-8").splitlines()
15+
pillow_lines = [line.strip() for line in requirement_lines if line.strip().lower().startswith("pillow")]
16+
self.assertEqual(pillow_lines, ["pillow==12.1.1"])
17+
18+
def test_runtime_contract_workflow_covers_python_3_10_and_3_11(self):
19+
workflow_path = REPO_ROOT / ".github" / "workflows" / "python_runtime_contract.yml"
20+
self.assertTrue(workflow_path.exists(), "runtime contract workflow should exist")
21+
22+
workflow = workflow_path.read_text(encoding="utf-8")
23+
self.assertIn("'3.10'", workflow)
24+
self.assertIn("'3.11'", workflow)
25+
self.assertIn("python -m pip install -r requirements.txt", workflow)
26+
self.assertIn("test_*.py", workflow)
27+
28+
29+
if __name__ == "__main__":
30+
unittest.main()

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ fitdecode==0.10.0
66
folium==0.14.0
77
geopy==2.3.0
88
packaging==20.1
9-
pillow>=10.3.0,<12.0.0
9+
pillow==12.1.1
1010
polyline==2.0.0
1111
protobuf==3.10.0
1212
PyCryptodome

0 commit comments

Comments
 (0)