mirror of
https://github.com/mvt-project/mvt.git
synced 2026-02-15 18:02:44 +00:00
Compare commits
1 Commits
tzdata-dep
...
fix_tests
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5ae729b65 |
2
Makefile
2
Makefile
@@ -23,7 +23,7 @@ install:
|
|||||||
python3 -m pip install --upgrade -e .
|
python3 -m pip install --upgrade -e .
|
||||||
|
|
||||||
test-requirements:
|
test-requirements:
|
||||||
python3 -m pip install --upgrade --group dev
|
python3 -m pip install --upgrade -r test-requirements.txt
|
||||||
|
|
||||||
generate-proto-parsers:
|
generate-proto-parsers:
|
||||||
# Generate python parsers for protobuf files
|
# Generate python parsers for protobuf files
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
mkdocs==1.6.1
|
mkdocs==1.6.1
|
||||||
mkdocs-autorefs==1.4.3
|
mkdocs-autorefs==1.4.2
|
||||||
mkdocs-material==9.6.20
|
mkdocs-material==9.6.16
|
||||||
mkdocs-material-extensions==1.3.1
|
mkdocs-material-extensions==1.3.1
|
||||||
mkdocstrings==0.30.1
|
mkdocstrings==0.30.0
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "mvt"
|
name = "mvt"
|
||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
authors = [{ name = "Claudio Guarnieri", email = "nex@nex.sx" }]
|
authors = [
|
||||||
|
{name = "Claudio Guarnieri", email = "nex@nex.sx"}
|
||||||
|
]
|
||||||
maintainers = [
|
maintainers = [
|
||||||
{ name = "Etienne Maynier", email = "tek@randhome.io" },
|
{name = "Etienne Maynier", email = "tek@randhome.io"},
|
||||||
{ name = "Donncha Ó Cearbhaill", email = "donncha.ocearbhaill@amnesty.org" },
|
{name = "Donncha Ó Cearbhaill", email = "donncha.ocearbhaill@amnesty.org"},
|
||||||
{ name = "Rory Flynn", email = "rory.flynn@amnesty.org" },
|
{name = "Rory Flynn", email = "rory.flynn@amnesty.org"}
|
||||||
]
|
]
|
||||||
description = "Mobile Verification Toolkit"
|
description = "Mobile Verification Toolkit"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -14,7 +16,7 @@ classifiers = [
|
|||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"Intended Audience :: Information Technology",
|
"Intended Audience :: Information Technology",
|
||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
"Programming Language :: Python",
|
"Programming Language :: Python"
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"click==8.2.1",
|
"click==8.2.1",
|
||||||
@@ -35,7 +37,6 @@ dependencies = [
|
|||||||
"pydantic-settings==2.10.1",
|
"pydantic-settings==2.10.1",
|
||||||
"NSKeyedUnArchiver==1.5.2",
|
"NSKeyedUnArchiver==1.5.2",
|
||||||
"python-dateutil==2.9.0.post0",
|
"python-dateutil==2.9.0.post0",
|
||||||
"tzdata==2025.2",
|
|
||||||
]
|
]
|
||||||
requires-python = ">= 3.10"
|
requires-python = ">= 3.10"
|
||||||
|
|
||||||
@@ -44,31 +45,20 @@ homepage = "https://docs.mvt.re/en/latest/"
|
|||||||
repository = "https://github.com/mvt-project/mvt"
|
repository = "https://github.com/mvt-project/mvt"
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
mvt-ios = "mvt.ios:cli"
|
mvt-ios = "mvt.ios:cli"
|
||||||
mvt-android = "mvt.android:cli"
|
mvt-android = "mvt.android:cli"
|
||||||
|
|
||||||
[dependency-groups]
|
|
||||||
dev = [
|
|
||||||
"requests>=2.31.0",
|
|
||||||
"pytest>=7.4.3",
|
|
||||||
"pytest-cov>=4.1.0",
|
|
||||||
"pytest-github-actions-annotate-failures>=0.2.0",
|
|
||||||
"pytest-mock>=3.14.0",
|
|
||||||
"stix2>=3.0.1",
|
|
||||||
"ruff>=0.1.6",
|
|
||||||
"mypy>=1.7.1",
|
|
||||||
"betterproto[compiler]",
|
|
||||||
]
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools>=61.0"]
|
requires = ["setuptools>=61.0"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[tool.coverage.run]
|
[tool.coverage.run]
|
||||||
omit = ["tests/*"]
|
omit = [
|
||||||
|
"tests/*",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.coverage.html]
|
[tool.coverage.html]
|
||||||
directory = "htmlcov"
|
directory= "htmlcov"
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
install_types = true
|
install_types = true
|
||||||
@@ -78,13 +68,15 @@ packages = "src"
|
|||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
addopts = "-ra -q --cov=mvt --cov-report html --junitxml=pytest.xml --cov-report=term-missing:skip-covered"
|
addopts = "-ra -q --cov=mvt --cov-report html --junitxml=pytest.xml --cov-report=term-missing:skip-covered"
|
||||||
testpaths = ["tests"]
|
testpaths = [
|
||||||
|
"tests"
|
||||||
|
]
|
||||||
|
|
||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
select = ["C90", "E", "F", "W"] # flake8 default set
|
select = ["C90", "E", "F", "W"] # flake8 default set
|
||||||
ignore = [
|
ignore = [
|
||||||
"E501", # don't enforce line length violations
|
"E501", # don't enforce line length violations
|
||||||
"C901", # complex-structure
|
"C901", # complex-structure
|
||||||
|
|
||||||
# These were previously ignored but don't seem to be required:
|
# These were previously ignored but don't seem to be required:
|
||||||
# "E265", # no-space-after-block-comment
|
# "E265", # no-space-after-block-comment
|
||||||
@@ -96,14 +88,14 @@ ignore = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[tool.ruff.lint.per-file-ignores]
|
[tool.ruff.lint.per-file-ignores]
|
||||||
"__init__.py" = ["F401"] # unused-import
|
"__init__.py" = ["F401"] # unused-import
|
||||||
|
|
||||||
[tool.ruff.lint.mccabe]
|
[tool.ruff.lint.mccabe]
|
||||||
max-complexity = 10
|
max-complexity = 10
|
||||||
|
|
||||||
[tool.setuptools]
|
[tool.setuptools]
|
||||||
include-package-data = true
|
include-package-data = true
|
||||||
package-dir = { "" = "src" }
|
package-dir = {"" = "src"}
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["src"]
|
where = ["src"]
|
||||||
@@ -112,4 +104,4 @@ where = ["src"]
|
|||||||
mvt = ["ios/data/*.json"]
|
mvt = ["ios/data/*.json"]
|
||||||
|
|
||||||
[tool.setuptools.dynamic]
|
[tool.setuptools.dynamic]
|
||||||
version = { attr = "mvt.common.version.MVT_VERSION" }
|
version = {attr = "mvt.common.version.MVT_VERSION"}
|
||||||
|
|||||||
@@ -21,12 +21,22 @@ class DumpsysADBArtifact(AndroidArtifact):
|
|||||||
stack = [res]
|
stack = [res]
|
||||||
cur_indent = 0
|
cur_indent = 0
|
||||||
in_multiline = False
|
in_multiline = False
|
||||||
for line in dump_data.strip(b"\n").split(b"\n"):
|
# Normalize line endings to handle both Unix (\n) and Windows (\r\n)
|
||||||
|
normalized_data = dump_data.replace(b"\r\n", b"\n").replace(b"\r", b"\n")
|
||||||
|
for line in normalized_data.strip(b"\n").split(b"\n"):
|
||||||
|
# Skip completely empty lines
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
|
||||||
# Track the level of indentation
|
# Track the level of indentation
|
||||||
indent = len(line) - len(line.lstrip())
|
indent = len(line) - len(line.lstrip())
|
||||||
if indent < cur_indent:
|
if indent < cur_indent:
|
||||||
# If the current line is less indented than the previous one, back out
|
# If the current line is less indented than the previous one, back out
|
||||||
stack.pop()
|
while len(stack) > 1 and indent < cur_indent:
|
||||||
|
stack.pop()
|
||||||
|
# Check if we were in multiline mode and need to exit it
|
||||||
|
if in_multiline and not isinstance(stack[-1], list):
|
||||||
|
in_multiline = False
|
||||||
cur_indent = indent
|
cur_indent = indent
|
||||||
else:
|
else:
|
||||||
cur_indent = indent
|
cur_indent = indent
|
||||||
@@ -38,12 +48,30 @@ class DumpsysADBArtifact(AndroidArtifact):
|
|||||||
|
|
||||||
# Annoyingly, some values are multiline and don't have a key on each line
|
# Annoyingly, some values are multiline and don't have a key on each line
|
||||||
if in_multiline:
|
if in_multiline:
|
||||||
if key == "":
|
if key == "" and len(vals) < 2:
|
||||||
# If the line is empty, it's the terminator for the multiline value
|
# If the line is empty, it's the terminator for the multiline value
|
||||||
in_multiline = False
|
in_multiline = False
|
||||||
stack.pop()
|
stack.pop()
|
||||||
|
current_dict = stack[-1]
|
||||||
|
elif len(vals) >= 2 and (key in self.multiline_fields or key == "}" or vals[1] == b"{"):
|
||||||
|
# If we encounter a new field while in multiline mode, exit multiline mode
|
||||||
|
# and process this line as a new field
|
||||||
|
in_multiline = False
|
||||||
|
stack.pop()
|
||||||
|
current_dict = stack[-1]
|
||||||
|
# Don't continue here - let the line be processed as a new field
|
||||||
else:
|
else:
|
||||||
current_dict.append(line.lstrip())
|
# When in multiline mode, the top of stack should be a list
|
||||||
|
if isinstance(stack[-1], list):
|
||||||
|
stack[-1].append(line.lstrip())
|
||||||
|
else:
|
||||||
|
# Something went wrong with the stack, exit multiline mode
|
||||||
|
in_multiline = False
|
||||||
|
current_dict = stack[-1]
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip lines that don't have a value after '='
|
||||||
|
if len(vals) < 2:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if key == "}":
|
if key == "}":
|
||||||
@@ -133,7 +161,16 @@ class DumpsysADBArtifact(AndroidArtifact):
|
|||||||
|
|
||||||
# TODO: Parse AdbDebuggingManager line in output.
|
# TODO: Parse AdbDebuggingManager line in output.
|
||||||
start_of_json = content.find(b"\n{") + 2
|
start_of_json = content.find(b"\n{") + 2
|
||||||
end_of_json = content.rfind(b"}\n") - 2
|
|
||||||
|
# Handle both Unix (\n) and Windows (\r\n) line endings
|
||||||
|
end_of_json = content.rfind(b"}\n")
|
||||||
|
if end_of_json == -1:
|
||||||
|
end_of_json = content.rfind(b"}\r\n")
|
||||||
|
if end_of_json == -1:
|
||||||
|
self.log.error("Unable to find end of JSON block in dumpsys output")
|
||||||
|
return
|
||||||
|
|
||||||
|
end_of_json -= 2
|
||||||
json_content = content[start_of_json:end_of_json].rstrip()
|
json_content = content[start_of_json:end_of_json].rstrip()
|
||||||
|
|
||||||
parsed = self.indented_dump_parser(json_content)
|
parsed = self.indented_dump_parser(json_content)
|
||||||
|
|||||||
@@ -107,7 +107,8 @@ class Packages(AndroidExtraction):
|
|||||||
result["matched_indicator"] = ioc
|
result["matched_indicator"] = ioc
|
||||||
self.detected.append(result)
|
self.detected.append(result)
|
||||||
|
|
||||||
def check_virustotal(self, packages: list) -> None:
|
@staticmethod
|
||||||
|
def check_virustotal(packages: list) -> None:
|
||||||
hashes = []
|
hashes = []
|
||||||
for package in packages:
|
for package in packages:
|
||||||
for file in package.get("files", []):
|
for file in package.get("files", []):
|
||||||
@@ -142,15 +143,8 @@ class Packages(AndroidExtraction):
|
|||||||
|
|
||||||
for package in packages:
|
for package in packages:
|
||||||
for file in package.get("files", []):
|
for file in package.get("files", []):
|
||||||
if "package_name" in package:
|
row = [package["package_name"], file["path"]]
|
||||||
row = [package["package_name"], file["path"]]
|
|
||||||
elif "name" in package:
|
|
||||||
row = [package["name"], file["path"]]
|
|
||||||
else:
|
|
||||||
self.log.error(
|
|
||||||
f"Package {package} has no name or package_name. packages.json or apks.json is malformed"
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
if file["sha256"] in detections:
|
if file["sha256"] in detections:
|
||||||
detection = detections[file["sha256"]]
|
detection = detections[file["sha256"]]
|
||||||
positives = detection.split("/")[0]
|
positives = detection.split("/")[0]
|
||||||
|
|||||||
@@ -112,10 +112,18 @@ class Files(AndroidQFModule):
|
|||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
if timezone := self._get_device_timezone():
|
if timezone := self._get_device_timezone():
|
||||||
device_timezone = zoneinfo.ZoneInfo(timezone)
|
try:
|
||||||
|
device_timezone = zoneinfo.ZoneInfo(timezone)
|
||||||
|
except zoneinfo.ZoneInfoNotFoundError:
|
||||||
|
self.log.warning("Device timezone '%s' not found, using UTC", timezone)
|
||||||
|
device_timezone = datetime.timezone.utc
|
||||||
else:
|
else:
|
||||||
self.log.warning("Unable to determine device timezone, using UTC")
|
self.log.warning("Unable to determine device timezone, using UTC")
|
||||||
device_timezone = zoneinfo.ZoneInfo("UTC")
|
try:
|
||||||
|
device_timezone = zoneinfo.ZoneInfo("UTC")
|
||||||
|
except zoneinfo.ZoneInfoNotFoundError:
|
||||||
|
# Fallback for Windows systems where zoneinfo might not have UTC
|
||||||
|
device_timezone = datetime.timezone.utc
|
||||||
|
|
||||||
for file in self._get_files_by_pattern("*/files.json"):
|
for file in self._get_files_by_pattern("*/files.json"):
|
||||||
rawdata = self._get_file_content(file).decode("utf-8", errors="ignore")
|
rawdata = self._get_file_content(file).decode("utf-8", errors="ignore")
|
||||||
|
|||||||
@@ -654,7 +654,8 @@ class Indicators:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
for ioc in self.get_iocs("processes"):
|
for ioc in self.get_iocs("processes"):
|
||||||
parts = file_path.split("/")
|
# Use os-agnostic path splitting to handle both Windows (\) and Unix (/) separators
|
||||||
|
parts = file_path.replace("\\", "/").split("/")
|
||||||
if ioc["value"] in parts:
|
if ioc["value"] in parts:
|
||||||
self.log.warning(
|
self.log.warning(
|
||||||
"Found known suspicious process name mentioned in file at "
|
"Found known suspicious process name mentioned in file at "
|
||||||
|
|||||||
@@ -895,10 +895,6 @@
|
|||||||
"version": "15.8.4",
|
"version": "15.8.4",
|
||||||
"build": "19H390"
|
"build": "19H390"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"version": "15.8.5",
|
|
||||||
"build": "19H394"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"build": "20A362",
|
"build": "20A362",
|
||||||
"version": "16.0"
|
"version": "16.0"
|
||||||
@@ -1004,10 +1000,6 @@
|
|||||||
"version": "16.7.11",
|
"version": "16.7.11",
|
||||||
"build": "20H360"
|
"build": "20H360"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"version": "16.7.12",
|
|
||||||
"build": "20H364"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"version": "17.0",
|
"version": "17.0",
|
||||||
"build": "21A327"
|
"build": "21A327"
|
||||||
@@ -1143,25 +1135,5 @@
|
|||||||
{
|
{
|
||||||
"version": "18.6",
|
"version": "18.6",
|
||||||
"build": "22G86"
|
"build": "22G86"
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "18.6.1",
|
|
||||||
"build": "22G90"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "18.6.2",
|
|
||||||
"build": "22G100"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "18.7",
|
|
||||||
"build": "22H20"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "26",
|
|
||||||
"build": "23A341"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "26.0.1",
|
|
||||||
"build": "23A355"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
9
test-requirements.txt
Normal file
9
test-requirements.txt
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
requests>=2.31.0
|
||||||
|
pytest>=7.4.3
|
||||||
|
pytest-cov>=4.1.0
|
||||||
|
pytest-github-actions-annotate-failures>=0.2.0
|
||||||
|
pytest-mock>=3.14.0
|
||||||
|
stix2>=3.0.1
|
||||||
|
ruff>=0.1.6
|
||||||
|
mypy>=1.7.1
|
||||||
|
betterproto[compiler]
|
||||||
Reference in New Issue
Block a user