mirror of
https://github.com/mvt-project/mvt.git
synced 2026-04-21 03:06:41 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 74dd566ee6 |
@@ -7,13 +7,14 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v6
|
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: 3.9
|
python-version: 3.9
|
||||||
cache: 'pip'
|
cache: 'pip'
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@master
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: |
|
run: |
|
||||||
pip install mypy
|
pip install mypy
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ name: Create and publish a Docker image
|
|||||||
# Configures this workflow to run every time a release is published.
|
# Configures this workflow to run every time a release is published.
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
|
||||||
@@ -25,18 +23,9 @@ jobs:
|
|||||||
attestations: write
|
attestations: write
|
||||||
id-token: write
|
id-token: write
|
||||||
#
|
#
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
platform:
|
|
||||||
- dockerfile: "Dockerfile"
|
|
||||||
tag-suffix: ""
|
|
||||||
- dockerfile: "Dockerfile.ios"
|
|
||||||
tag-suffix: "-ios"
|
|
||||||
- dockerfile: "Dockerfile.android"
|
|
||||||
tag-suffix: "-android"
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
# Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
|
# Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
|
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
|
||||||
@@ -47,33 +36,26 @@ jobs:
|
|||||||
# This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
|
# This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
flavor: |
|
|
||||||
latest=false
|
|
||||||
tags: |
|
|
||||||
type=raw,value=latest,enable={{ is_default_branch }},suffix=${{ matrix.platform.tag-suffix }}
|
|
||||||
type=raw,enable=${{ github.event_name == 'release' || github.ref_type == 'tag' }},value=stable,suffix=${{ matrix.platform.tag-suffix }}
|
|
||||||
type=raw,enable=${{ github.event_name == 'release' }},value=${{ github.event.release.tag_name }},suffix=${{ matrix.platform.tag-suffix }}
|
|
||||||
type=raw,enable=${{ github.ref_type == 'tag' }},value=${{ github.ref_name }},suffix=${{ matrix.platform.tag-suffix }}
|
|
||||||
type=sha,suffix=${{ matrix.platform.tag-suffix }}
|
|
||||||
type=sha,format=long,suffix=${{ matrix.platform.tag-suffix }}
|
|
||||||
# This step sets up some additional capabilities to generate the provenance and sbom attestations
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
# This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages.
|
# This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages.
|
||||||
# It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository.
|
# It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository.
|
||||||
# It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
|
# It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
id: push
|
id: push
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
|
||||||
with:
|
with:
|
||||||
file: ${{ matrix.platform.dockerfile }}
|
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
provenance: mode=max
|
|
||||||
sbom: true
|
# This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see "[AUTOTITLE](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds)."
|
||||||
|
- name: Generate artifact attestation
|
||||||
|
uses: actions/attest-build-provenance@v1
|
||||||
|
with:
|
||||||
|
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
|
||||||
|
subject-digest: ${{ steps.push.outputs.digest }}
|
||||||
|
push-to-registry: true
|
||||||
|
|
||||||
|
|||||||
@@ -11,13 +11,14 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v6
|
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: 3.9
|
python-version: 3.9
|
||||||
cache: 'pip'
|
cache: 'pip'
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@master
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: |
|
run: |
|
||||||
pip install ruff
|
pip install ruff
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ jobs:
|
|||||||
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
|
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- name: Install Python dependencies
|
- name: Install Python dependencies
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
- name: Run script to fetch latest iOS releases from Apple RSS feed.
|
- name: Run script to fetch latest iOS releases from Apple RSS feed.
|
||||||
run: python3 .github/workflows/scripts/update-ios-releases.py
|
run: python3 .github/workflows/scripts/update-ios-releases.py
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@v8
|
uses: peter-evans/create-pull-request@v5
|
||||||
with:
|
with:
|
||||||
title: '[auto] Update iOS releases and versions'
|
title: '[auto] Update iOS releases and versions'
|
||||||
commit-message: Add new iOS versions and build numbers
|
commit-message: Add new iOS versions and build numbers
|
||||||
@@ -27,4 +27,4 @@ jobs:
|
|||||||
add-paths: |
|
add-paths: |
|
||||||
*.json
|
*.json
|
||||||
labels: |
|
labels: |
|
||||||
automated pr
|
automated pr
|
||||||
+8
-8
@@ -1,6 +1,6 @@
|
|||||||
# Base image for building libraries
|
# Base image for building libraries
|
||||||
# ---------------------------------
|
# ---------------------------------
|
||||||
FROM ubuntu:22.04 AS build-base
|
FROM ubuntu:22.04 as build-base
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ RUN apt-get update \
|
|||||||
|
|
||||||
# libplist
|
# libplist
|
||||||
# --------
|
# --------
|
||||||
FROM build-base AS build-libplist
|
FROM build-base as build-libplist
|
||||||
|
|
||||||
# Build
|
# Build
|
||||||
RUN git clone https://github.com/libimobiledevice/libplist && cd libplist \
|
RUN git clone https://github.com/libimobiledevice/libplist && cd libplist \
|
||||||
@@ -32,7 +32,7 @@ RUN git clone https://github.com/libimobiledevice/libplist && cd libplist \
|
|||||||
|
|
||||||
# libimobiledevice-glue
|
# libimobiledevice-glue
|
||||||
# ---------------------
|
# ---------------------
|
||||||
FROM build-base AS build-libimobiledevice-glue
|
FROM build-base as build-libimobiledevice-glue
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
COPY --from=build-libplist /build /
|
COPY --from=build-libplist /build /
|
||||||
@@ -45,7 +45,7 @@ RUN git clone https://github.com/libimobiledevice/libimobiledevice-glue && cd li
|
|||||||
|
|
||||||
# libtatsu
|
# libtatsu
|
||||||
# --------
|
# --------
|
||||||
FROM build-base AS build-libtatsu
|
FROM build-base as build-libtatsu
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
COPY --from=build-libplist /build /
|
COPY --from=build-libplist /build /
|
||||||
@@ -58,7 +58,7 @@ RUN git clone https://github.com/libimobiledevice/libtatsu && cd libtatsu \
|
|||||||
|
|
||||||
# libusbmuxd
|
# libusbmuxd
|
||||||
# ----------
|
# ----------
|
||||||
FROM build-base AS build-libusbmuxd
|
FROM build-base as build-libusbmuxd
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
COPY --from=build-libplist /build /
|
COPY --from=build-libplist /build /
|
||||||
@@ -72,7 +72,7 @@ RUN git clone https://github.com/libimobiledevice/libusbmuxd && cd libusbmuxd \
|
|||||||
|
|
||||||
# libimobiledevice
|
# libimobiledevice
|
||||||
# ----------------
|
# ----------------
|
||||||
FROM build-base AS build-libimobiledevice
|
FROM build-base as build-libimobiledevice
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
COPY --from=build-libplist /build /
|
COPY --from=build-libplist /build /
|
||||||
@@ -88,7 +88,7 @@ RUN git clone https://github.com/libimobiledevice/libimobiledevice && cd libimob
|
|||||||
|
|
||||||
# usbmuxd
|
# usbmuxd
|
||||||
# -------
|
# -------
|
||||||
FROM build-base AS build-usbmuxd
|
FROM build-base as build-usbmuxd
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
COPY --from=build-libplist /build /
|
COPY --from=build-libplist /build /
|
||||||
@@ -103,7 +103,7 @@ RUN git clone https://github.com/libimobiledevice/usbmuxd && cd usbmuxd \
|
|||||||
|
|
||||||
|
|
||||||
# Create main image
|
# Create main image
|
||||||
FROM ubuntu:24.04 AS main
|
FROM ubuntu:24.04 as main
|
||||||
|
|
||||||
LABEL org.opencontainers.image.url="https://mvt.re"
|
LABEL org.opencontainers.image.url="https://mvt.re"
|
||||||
LABEL org.opencontainers.image.documentation="https://docs.mvt.re"
|
LABEL org.opencontainers.image.documentation="https://docs.mvt.re"
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
# Create main image
|
# Create main image
|
||||||
FROM python:3.10.14-alpine3.20 AS main
|
FROM python:3.10.14-alpine3.20 as main
|
||||||
|
|
||||||
LABEL org.opencontainers.image.url="https://mvt.re"
|
LABEL org.opencontainers.image.url="https://mvt.re"
|
||||||
LABEL org.opencontainers.image.documentation="https://docs.mvt.re"
|
LABEL org.opencontainers.image.documentation="https://docs.mvt.re"
|
||||||
|
|||||||
+8
-8
@@ -1,6 +1,6 @@
|
|||||||
# Base image for building libraries
|
# Base image for building libraries
|
||||||
# ---------------------------------
|
# ---------------------------------
|
||||||
FROM ubuntu:22.04 AS build-base
|
FROM ubuntu:22.04 as build-base
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ RUN apt-get update \
|
|||||||
|
|
||||||
# libplist
|
# libplist
|
||||||
# --------
|
# --------
|
||||||
FROM build-base AS build-libplist
|
FROM build-base as build-libplist
|
||||||
|
|
||||||
# Build
|
# Build
|
||||||
RUN git clone https://github.com/libimobiledevice/libplist && cd libplist \
|
RUN git clone https://github.com/libimobiledevice/libplist && cd libplist \
|
||||||
@@ -32,7 +32,7 @@ RUN git clone https://github.com/libimobiledevice/libplist && cd libplist \
|
|||||||
|
|
||||||
# libimobiledevice-glue
|
# libimobiledevice-glue
|
||||||
# ---------------------
|
# ---------------------
|
||||||
FROM build-base AS build-libimobiledevice-glue
|
FROM build-base as build-libimobiledevice-glue
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
COPY --from=build-libplist /build /
|
COPY --from=build-libplist /build /
|
||||||
@@ -45,7 +45,7 @@ RUN git clone https://github.com/libimobiledevice/libimobiledevice-glue && cd li
|
|||||||
|
|
||||||
# libtatsu
|
# libtatsu
|
||||||
# --------
|
# --------
|
||||||
FROM build-base AS build-libtatsu
|
FROM build-base as build-libtatsu
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
COPY --from=build-libplist /build /
|
COPY --from=build-libplist /build /
|
||||||
@@ -58,7 +58,7 @@ RUN git clone https://github.com/libimobiledevice/libtatsu && cd libtatsu \
|
|||||||
|
|
||||||
# libusbmuxd
|
# libusbmuxd
|
||||||
# ----------
|
# ----------
|
||||||
FROM build-base AS build-libusbmuxd
|
FROM build-base as build-libusbmuxd
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
COPY --from=build-libplist /build /
|
COPY --from=build-libplist /build /
|
||||||
@@ -72,7 +72,7 @@ RUN git clone https://github.com/libimobiledevice/libusbmuxd && cd libusbmuxd \
|
|||||||
|
|
||||||
# libimobiledevice
|
# libimobiledevice
|
||||||
# ----------------
|
# ----------------
|
||||||
FROM build-base AS build-libimobiledevice
|
FROM build-base as build-libimobiledevice
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
COPY --from=build-libplist /build /
|
COPY --from=build-libplist /build /
|
||||||
@@ -88,7 +88,7 @@ RUN git clone https://github.com/libimobiledevice/libimobiledevice && cd libimob
|
|||||||
|
|
||||||
# usbmuxd
|
# usbmuxd
|
||||||
# -------
|
# -------
|
||||||
FROM build-base AS build-usbmuxd
|
FROM build-base as build-usbmuxd
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
COPY --from=build-libplist /build /
|
COPY --from=build-libplist /build /
|
||||||
@@ -104,7 +104,7 @@ RUN git clone https://github.com/libimobiledevice/usbmuxd && cd usbmuxd \
|
|||||||
|
|
||||||
# Main image
|
# Main image
|
||||||
# ----------
|
# ----------
|
||||||
FROM python:3.10.14-alpine3.20 AS main
|
FROM python:3.10.14-alpine3.20 as main
|
||||||
|
|
||||||
LABEL org.opencontainers.image.url="https://mvt.re"
|
LABEL org.opencontainers.image.url="https://mvt.re"
|
||||||
LABEL org.opencontainers.image.documentation="https://docs.mvt.re"
|
LABEL org.opencontainers.image.documentation="https://docs.mvt.re"
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
PWD = $(shell pwd)
|
PWD = $(shell pwd)
|
||||||
|
|
||||||
|
autofix:
|
||||||
|
ruff format .
|
||||||
|
ruff check --fix .
|
||||||
|
|
||||||
check: ruff mypy
|
check: ruff mypy
|
||||||
|
|
||||||
ruff:
|
ruff:
|
||||||
ruff check .
|
ruff format --check .
|
||||||
|
ruff check -q .
|
||||||
|
|
||||||
mypy:
|
mypy:
|
||||||
mypy
|
mypy
|
||||||
@@ -23,7 +28,7 @@ test-requirements:
|
|||||||
generate-proto-parsers:
|
generate-proto-parsers:
|
||||||
# Generate python parsers for protobuf files
|
# Generate python parsers for protobuf files
|
||||||
PROTO_FILES=$$(find src/mvt/android/parsers/proto/ -iname "*.proto"); \
|
PROTO_FILES=$$(find src/mvt/android/parsers/proto/ -iname "*.proto"); \
|
||||||
protoc -Isrc/mvt/android/parsers/proto/ --python_betterproto2_out=src/mvt/android/parsers/proto/ $$PROTO_FILES
|
protoc -Isrc/mvt/android/parsers/proto/ --python_betterproto_out=src/mvt/android/parsers/proto/ $$PROTO_FILES
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf $(PWD)/build $(PWD)/dist $(PWD)/src/mvt.egg-info
|
rm -rf $(PWD)/build $(PWD)/dist $(PWD)/src/mvt.egg-info
|
||||||
|
|||||||
@@ -4,9 +4,6 @@
|
|||||||
|
|
||||||
# Mobile Verification Toolkit
|
# Mobile Verification Toolkit
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> Soon we will merge the v3 pull request which will result in breaking changes. If you rely on mvt output in other script make sure to the the branch before we merge. More details: https://github.com/mvt-project/mvt/issues/757
|
|
||||||
|
|
||||||
[](https://pypi.org/project/mvt/)
|
[](https://pypi.org/project/mvt/)
|
||||||
[](https://docs.mvt.re/en/latest/?badge=latest)
|
[](https://docs.mvt.re/en/latest/?badge=latest)
|
||||||
[](https://github.com/mvt-project/mvt/actions/workflows/tests.yml)
|
[](https://github.com/mvt-project/mvt/actions/workflows/tests.yml)
|
||||||
|
|||||||
+1
-58
@@ -2,61 +2,4 @@
|
|||||||
|
|
||||||
Thank you for your interest in reporting security issues and vulnerabilities! Security research is of utmost importance and we take all reports seriously. If you discover an issue please report it to us right away!
|
Thank you for your interest in reporting security issues and vulnerabilities! Security research is of utmost importance and we take all reports seriously. If you discover an issue please report it to us right away!
|
||||||
|
|
||||||
Please DO NOT file a public issue, instead send your report privately to the MVT maintainers at Amnesty International via `security [at] amnesty [dot] tech`.
|
Please DO NOT file a public issue, instead send your report privately to *nex [at] nex [dot] sx*. You can also write PGP-encrypted emails to [this key](https://keybase.io/nex/pgp_keys.asc?fingerprint=05216f3b86848a303c2fe37dd166f1667359d880).
|
||||||
|
|
||||||
You can also write PGP-encrypted emails to key `CFBF9698DCA8EB2A80F48ADEA035A030FA04ED13`. The corresponding PGP public key is lited below.
|
|
||||||
|
|
||||||
```
|
|
||||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
|
||||||
|
|
||||||
mQINBGlFPwsBEADQ+d7SeHrFPYv3wPOjWs2oMpp0DPdfIyGbg+iYWOC36FegZhKY
|
|
||||||
+WeK96GqJWt8wD6kwFUVwQI795WZrjSd1q4a7wR+kj/h7xlRB6ZfVICA6O5DOOm6
|
|
||||||
GNMvqy7ESm8g1XZDpb2u1BXmSS9X8f6rjB0e86kYsF1mB5/2USTM63jgDs0GGTkZ
|
|
||||||
Q1z4Mq4gYyqH32b3gvXkbb68LeQmONUIM3cgmec9q8/pNc1l7fcoLWhOVADRj17Q
|
|
||||||
plisa/EUf/SYqdtk9w7EHGggNenKNwVM235mkPcMqmE72bTpjT6XCxvZY3ByG5yi
|
|
||||||
7L+tHJU45ZuXtt62EvX03azxThVfSmH/WbRk8lH8+CW8XMmiWZphG4ydPWqgVKCB
|
|
||||||
2UOXm+6CQnKA+7Dt1AeK2t5ciATrv9LvwgSxk5WKc3288XFLA6eGMrTdQygYlLjJ
|
|
||||||
+42RSdK/7fCt/qk4q13oUw8ZTVcCia98uZFi704XuuYTH6NrntIB7j/0oucIS4Y9
|
|
||||||
cTWNO5LBerez4v8VI4YHcYESPeIWGFkXhvJzo0VMg1zidBLtiPoGF2JKZGwaK7/p
|
|
||||||
yY1xALskLp4H+5OY4eB1kf8kl4vGsEK8xA/NNzOiapVmwBXpvVvmXIQJE2k+olNf
|
|
||||||
sAuyB8+aO1Ws7tFYt3D+olC7iaprOdK7uA4GCgmYYhq6QQPg+cxfczgHfwARAQAB
|
|
||||||
tD1TZWN1cml0eSBMYWIgYXQgQW1uZXN0eSBJbnRlcm5hdGlvbmFsIDxzZWN1cml0
|
|
||||||
eUBhbW5lc3R5LnRlY2g+iQJRBBMBCAA7FiEEz7+WmNyo6yqA9IreoDWgMPoE7RMF
|
|
||||||
AmlFPwsCGwMFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQoDWgMPoE7RNr
|
|
||||||
2w//a88uP90uSN6lgeIwKsHr1ri27QIBbzCV6hLN/gZBFR2uaiOn/xfFDbnR0Cjo
|
|
||||||
5nMCJCT1k4nrPbMTlfmWLCD+YKELBzVqWlw4J2SOg3nznPl2JrL8QBKjwts0sF+h
|
|
||||||
QbRWDsT54wBZnl6ZJJ79eLShNTokBbKnQ7071dMrENr5e2P2sClQXyiIc51ga4FM
|
|
||||||
fHyhsx+GsrdiZNd2AH8912ljW1GuEi3epTO7KMZprmr37mjpZSUToiV59Yhl1Gbo
|
|
||||||
2pixkYJqi62DG02/gTpCjq9NH3cEMxcxjh4E7yCA8ggLG6+IN6woIvPIdOsnQ+Yj
|
|
||||||
d3H4rMNBjPSKoL+bdHILkCnp5HokcbVjNY3QAyOAF4qWhk4GtgpTshwxUmb4Tbay
|
|
||||||
tWLJC2bzjuUBxLkGzMVFfU3B96sVS4Fi0sBaEMBtHskl2f45X8LJhSq//Lw/2L/8
|
|
||||||
34uP/RxDSn+DPvj/yqMpekdCcmeFSTX1A19xkPcc0rVhMRde4VL338R86vzh0gMI
|
|
||||||
1LySDAhXZyVWzrQ5s3n6N3EvCaHCn3qu7ieyFJifCSR7gZqevCEznMQRVpkMTzUt
|
|
||||||
rk13Z6NOOb4IlTW7HFoY3omJG8Z5jV4kMIE7n6nb0qpNYQiG+YvjenQ3VrMoISyh
|
|
||||||
lpS2De8+oOtwrxBVX3+qKWvQqzufeE3416kw2Z+5mxH7bx25Ag0EaUU/CwEQALyZ
|
|
||||||
b+kwLN1yHObTm2yDBEn5HbCT3H1GremvPNmbAaTnfrjUngoKa8MuWWzbX5ptgmZR
|
|
||||||
UpYY/ylOYcgGydz58vUNrPlhIZT9UhmiifPgZLEXyd0uFpr/NsbRajHMkK10iEZf
|
|
||||||
h5bHNobiB7pGCu4Uj9e1cMiIZ4yEaYeyXYUoNHf6ISP39mJhHy6ov5yIpm9q0wzm
|
|
||||||
tGUQPupxGXmEZlOPr3lxqXQ3Ekdv6cWDY5r/oOq71QJ/HUQ13QUuGFIbhnMbT8zd
|
|
||||||
zaS6f/v772YKsWPc4NNUhtlf25VnQ4FuUtjCe3p6iYP4OVD8gJm0GvXyvyTuiQbL
|
|
||||||
CSk/378JiNT7nZzYXxrWchMwvEoMIU55+/UaBc50HI5xvDQ858CX7PYGiimcdsO1
|
|
||||||
EkQzhVxRfjlILfWrC2lgt+H5qhTn4Fah250Xe1PnLjXGHVUQnY/f3MFeiWQgf92b
|
|
||||||
02+MfvOeC5OKttP1z5lcx6RFWCIa1E/u8Nj7YrH9hk0ZBRAnBaeAncDFY8dfX2zX
|
|
||||||
VMoc0dV16gM7RrZ6i7D3CG3eLLkQlX0jbW9dzTuG/3f098EWB1p8vOfS/RbNCBRX
|
|
||||||
jqGiqacL/aFF3Ci3nQ4O5tSv1XipbgrUhvXnwm9pxrLPS/45iaO59WN4RRGWLLQ7
|
|
||||||
LHmeBxoa9avv0SdBYUL+eBxY46GXb/j5VLzHYhSnABEBAAGJAjYEGAEIACAWIQTP
|
|
||||||
v5aY3KjrKoD0it6gNaAw+gTtEwUCaUU/CwIbDAAKCRCgNaAw+gTtEyvsEACnyFFD
|
|
||||||
alOZTrrJTXNnUejuiExLh+qTO3T91p5bte597jpwCZnYGwkxEfffsqqhlY6ftEOf
|
|
||||||
d5tNWE5isai4v8XCbplWomz4KBpepxcn2b+9o5dSyr1vohEFuCJziZDsta1J2DX5
|
|
||||||
IE9U48kTgLDfdIBhuOyHNRkvXRHP2OVLCaiw4d9q+hlrraR8pehHt2BJSxh+QZoe
|
|
||||||
n0iHvIZCBIUA45zLEGmXFpNTGeEf2dKPp3xOkAXOhAMPptE0V1itkF3R7kEW4aFO
|
|
||||||
SZo8L3C1aWSz/gQ4/vvW5t1IJxirNMUgTMQFvqEkAwX3fm6GCxlgRSvTTRXdcrS8
|
|
||||||
6qyFdH1nkCNsavPahN3N2RGGIlWtODEMTO1Hjy0kZtTYdW+JH9sendliCoJES+yN
|
|
||||||
DjM125SgdAgrqlSYm/g8n9knWpxZv1QM6jU/sVz1J+l6/ixugL2i+CAL2d6uv4tT
|
|
||||||
QmXnu7Ei4/2kHBUu3Lf59MNgmLHm6F7AhOWErszSeoJKsp+3yA1oTT/npz67sRzY
|
|
||||||
VVyxz4NBIollna59a1lz0RhlWzNKqNB27jhylyM4ltdzHB7r4VMAVJyttozmIIOC
|
|
||||||
35ucYxl5BHLuapaRSaYHdUId1LOccYyaOOFF/PSyCu9dKzXk7zEz2HNcIboWSkAE
|
|
||||||
8ZDExMYM4WVpVCOj+frdsaBvzItHacRWuijtkw==
|
|
||||||
=JAXX
|
|
||||||
-----END PGP PUBLIC KEY BLOCK-----
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ mkdocs==1.6.1
|
|||||||
mkdocs-autorefs==1.4.3
|
mkdocs-autorefs==1.4.3
|
||||||
mkdocs-material==9.6.20
|
mkdocs-material==9.6.20
|
||||||
mkdocs-material-extensions==1.3.1
|
mkdocs-material-extensions==1.3.1
|
||||||
mkdocstrings==1.0.0
|
mkdocstrings==0.30.1
|
||||||
+15
-16
@@ -17,26 +17,25 @@ classifiers = [
|
|||||||
"Programming Language :: Python",
|
"Programming Language :: Python",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"click==8.3.2",
|
"click==8.2.1",
|
||||||
"rich==14.3.3",
|
"rich==14.1.0",
|
||||||
"tld==0.13.1",
|
"tld==0.13.1",
|
||||||
"requests==2.33.1",
|
"requests==2.32.5",
|
||||||
"simplejson==3.20.2",
|
"simplejson==3.20.1",
|
||||||
"packaging==26.0",
|
"packaging==25.0",
|
||||||
"appdirs==1.4.4",
|
"appdirs==1.4.4",
|
||||||
"iphone_backup_decrypt==0.9.0",
|
"iOSbackup==0.9.925",
|
||||||
"pycryptodome>=3.18",
|
|
||||||
"adb-shell[usb]==0.4.4",
|
"adb-shell[usb]==0.4.4",
|
||||||
"libusb1==3.3.1",
|
"libusb1==3.3.1",
|
||||||
"cryptography==46.0.6",
|
"cryptography==45.0.6",
|
||||||
"PyYAML>=6.0.2",
|
"PyYAML>=6.0.2",
|
||||||
"pyahocorasick==2.2.0",
|
"pyahocorasick==2.2.0",
|
||||||
"betterproto2==0.9.1",
|
"betterproto==1.2.5",
|
||||||
"pydantic==2.12.5",
|
"pydantic==2.12.3",
|
||||||
"pydantic-settings==2.13.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==2026.1",
|
"tzdata==2025.2",
|
||||||
]
|
]
|
||||||
requires-python = ">= 3.10"
|
requires-python = ">= 3.10"
|
||||||
|
|
||||||
@@ -58,7 +57,7 @@ dev = [
|
|||||||
"stix2>=3.0.1",
|
"stix2>=3.0.1",
|
||||||
"ruff>=0.1.6",
|
"ruff>=0.1.6",
|
||||||
"mypy>=1.7.1",
|
"mypy>=1.7.1",
|
||||||
"betterproto2-compiler",
|
"betterproto[compiler]",
|
||||||
]
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
@@ -81,9 +80,9 @@ packages = "src"
|
|||||||
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]
|
[tool.ruff.lint]
|
||||||
lint.select = ["C90", "E", "F", "W"] # flake8 default set
|
select = ["C90", "E", "F", "W"] # flake8 default set
|
||||||
lint.ignore = [
|
ignore = [
|
||||||
"E501", # don't enforce line length violations
|
"E501", # don't enforce line length violations
|
||||||
"C901", # complex-structure
|
"C901", # complex-structure
|
||||||
|
|
||||||
|
|||||||
@@ -14,23 +14,12 @@ class DumpsysBatteryDailyArtifact(AndroidArtifact):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def serialize(self, record: dict) -> Union[dict, list]:
|
def serialize(self, record: dict) -> Union[dict, list]:
|
||||||
action = record.get("action", "update")
|
|
||||||
package_name = record["package_name"]
|
|
||||||
vers = record["vers"]
|
|
||||||
|
|
||||||
if vers == "0":
|
|
||||||
data = f"Recorded uninstall of package {package_name} (vers 0)"
|
|
||||||
elif action == "downgrade":
|
|
||||||
prev_vers = record.get("previous_vers", "unknown")
|
|
||||||
data = f"Recorded downgrade of package {package_name} from vers {prev_vers} to vers {vers}"
|
|
||||||
else:
|
|
||||||
data = f"Recorded update of package {package_name} with vers {vers}"
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"timestamp": record["from"],
|
"timestamp": record["from"],
|
||||||
"module": self.__class__.__name__,
|
"module": self.__class__.__name__,
|
||||||
"event": "battery_daily",
|
"event": "battery_daily",
|
||||||
"data": data,
|
"data": f"Recorded update of package {record['package_name']} "
|
||||||
|
f"with vers {record['vers']}",
|
||||||
}
|
}
|
||||||
|
|
||||||
def check_indicators(self) -> None:
|
def check_indicators(self) -> None:
|
||||||
@@ -47,7 +36,6 @@ class DumpsysBatteryDailyArtifact(AndroidArtifact):
|
|||||||
def parse(self, output: str) -> None:
|
def parse(self, output: str) -> None:
|
||||||
daily = None
|
daily = None
|
||||||
daily_updates = []
|
daily_updates = []
|
||||||
package_versions = {} # Track package versions to detect downgrades
|
|
||||||
for line in output.splitlines():
|
for line in output.splitlines():
|
||||||
if line.startswith(" Daily from "):
|
if line.startswith(" Daily from "):
|
||||||
if len(daily_updates) > 0:
|
if len(daily_updates) > 0:
|
||||||
@@ -76,44 +64,15 @@ class DumpsysBatteryDailyArtifact(AndroidArtifact):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if not already_seen:
|
if not already_seen:
|
||||||
update_record = {
|
daily_updates.append(
|
||||||
"action": "update",
|
{
|
||||||
"from": daily["from"],
|
"action": "update",
|
||||||
"to": daily["to"],
|
"from": daily["from"],
|
||||||
"package_name": package_name,
|
"to": daily["to"],
|
||||||
"vers": vers_nr,
|
"package_name": package_name,
|
||||||
}
|
"vers": vers_nr,
|
||||||
|
}
|
||||||
# Check for uninstall (version 0)
|
)
|
||||||
if vers_nr == "0":
|
|
||||||
self.log.warning(
|
|
||||||
"Detected uninstall of package %s (vers 0) on %s",
|
|
||||||
package_name,
|
|
||||||
daily["from"],
|
|
||||||
)
|
|
||||||
# Check for downgrade
|
|
||||||
elif package_name in package_versions:
|
|
||||||
try:
|
|
||||||
current_vers = int(vers_nr)
|
|
||||||
previous_vers = int(package_versions[package_name])
|
|
||||||
if current_vers < previous_vers:
|
|
||||||
update_record["action"] = "downgrade"
|
|
||||||
update_record["previous_vers"] = str(previous_vers)
|
|
||||||
self.log.warning(
|
|
||||||
"Detected downgrade of package %s from vers %d to vers %d on %s",
|
|
||||||
package_name,
|
|
||||||
previous_vers,
|
|
||||||
current_vers,
|
|
||||||
daily["from"],
|
|
||||||
)
|
|
||||||
except ValueError:
|
|
||||||
# If version numbers aren't integers, skip comparison
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Update tracking dictionary
|
|
||||||
package_versions[package_name] = vers_nr
|
|
||||||
|
|
||||||
daily_updates.append(update_record)
|
|
||||||
|
|
||||||
if len(daily_updates) > 0:
|
if len(daily_updates) > 0:
|
||||||
self.results.extend(daily_updates)
|
self.results.extend(daily_updates)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import datetime
|
|||||||
from typing import List, Optional, Union
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
import pydantic
|
import pydantic
|
||||||
import betterproto2
|
import betterproto
|
||||||
from dateutil import parser
|
from dateutil import parser
|
||||||
|
|
||||||
from mvt.common.utils import convert_datetime_to_iso
|
from mvt.common.utils import convert_datetime_to_iso
|
||||||
@@ -124,7 +124,7 @@ class TombstoneCrashArtifact(AndroidArtifact):
|
|||||||
"""Parse Android tombstone crash files from a protobuf object."""
|
"""Parse Android tombstone crash files from a protobuf object."""
|
||||||
tombstone_pb = Tombstone().parse(data)
|
tombstone_pb = Tombstone().parse(data)
|
||||||
tombstone_dict = tombstone_pb.to_dict(
|
tombstone_dict = tombstone_pb.to_dict(
|
||||||
casing=betterproto2.Casing.SNAKE, include_default_values=True
|
betterproto.Casing.SNAKE, include_default_values=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add some extra metadata
|
# Add some extra metadata
|
||||||
|
|||||||
@@ -84,17 +84,13 @@ class BugReportModule(MVTModule):
|
|||||||
return self._get_file_content(main_content.decode().strip())
|
return self._get_file_content(main_content.decode().strip())
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return None
|
return None
|
||||||
|
else:
|
||||||
|
dumpstate_logs = self._get_files_by_pattern("dumpState_*.log")
|
||||||
|
if not dumpstate_logs:
|
||||||
|
return None
|
||||||
|
|
||||||
dumpstate_logs = self._get_files_by_pattern("dumpState_*.log")
|
|
||||||
if dumpstate_logs:
|
|
||||||
return self._get_file_content(dumpstate_logs[0])
|
return self._get_file_content(dumpstate_logs[0])
|
||||||
|
|
||||||
dumpsys_files = self._get_files_by_pattern("*/dumpsys.txt")
|
|
||||||
if dumpsys_files:
|
|
||||||
return self._get_file_content(dumpsys_files[0])
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _get_file_modification_time(self, file_path: str) -> dict:
|
def _get_file_modification_time(self, file_path: str) -> dict:
|
||||||
if self.zip_archive:
|
if self.zip_archive:
|
||||||
file_timetuple = self.zip_archive.getinfo(file_path).date_time
|
file_timetuple = self.zip_archive.getinfo(file_path).date_time
|
||||||
|
|||||||
@@ -34,20 +34,6 @@ class DumpsysReceivers(DumpsysReceiversArtifact, BugReportModule):
|
|||||||
|
|
||||||
self.results = results if results else {}
|
self.results = results if results else {}
|
||||||
|
|
||||||
def check_indicators(self) -> None:
|
|
||||||
for result in self.results:
|
|
||||||
if self.indicators:
|
|
||||||
receiver_name = self.results[result][0]["receiver"]
|
|
||||||
|
|
||||||
# return IoC if the stix2 process name a substring of the receiver name
|
|
||||||
ioc = self.indicators.check_receiver_prefix(receiver_name)
|
|
||||||
if ioc:
|
|
||||||
self.results[result][0]["matched_indicator"] = ioc
|
|
||||||
self.detected.append(result)
|
|
||||||
continue
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
content = self._get_dumpstate_file()
|
content = self._get_dumpstate_file()
|
||||||
if not content:
|
if not content:
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||||
# sources: tombstone.proto
|
# sources: tombstone.proto
|
||||||
# plugin: python-betterproto2
|
# plugin: python-betterproto
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
import betterproto2
|
import betterproto
|
||||||
|
|
||||||
|
|
||||||
class Architecture(betterproto2.Enum):
|
class Architecture(betterproto.Enum):
|
||||||
ARM32 = 0
|
ARM32 = 0
|
||||||
ARM64 = 1
|
ARM64 = 1
|
||||||
X86 = 2
|
X86 = 2
|
||||||
@@ -15,12 +16,12 @@ class Architecture(betterproto2.Enum):
|
|||||||
NONE = 5
|
NONE = 5
|
||||||
|
|
||||||
|
|
||||||
class MemoryErrorTool(betterproto2.Enum):
|
class MemoryErrorTool(betterproto.Enum):
|
||||||
GWP_ASAN = 0
|
GWP_ASAN = 0
|
||||||
SCUDO = 1
|
SCUDO = 1
|
||||||
|
|
||||||
|
|
||||||
class MemoryErrorType(betterproto2.Enum):
|
class MemoryErrorType(betterproto.Enum):
|
||||||
UNKNOWN = 0
|
UNKNOWN = 0
|
||||||
USE_AFTER_FREE = 1
|
USE_AFTER_FREE = 1
|
||||||
DOUBLE_FREE = 2
|
DOUBLE_FREE = 2
|
||||||
@@ -29,179 +30,179 @@ class MemoryErrorType(betterproto2.Enum):
|
|||||||
BUFFER_UNDERFLOW = 5
|
BUFFER_UNDERFLOW = 5
|
||||||
|
|
||||||
|
|
||||||
@dataclass(eq=False, repr=False)
|
@dataclass
|
||||||
class CrashDetail(betterproto2.Message):
|
class CrashDetail(betterproto.Message):
|
||||||
"""
|
"""
|
||||||
NOTE TO OEMS: If you add custom fields to this proto, do not use numbers in
|
NOTE TO OEMS: If you add custom fields to this proto, do not use numbers in
|
||||||
the reserved range.
|
the reserved range.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name: "bytes" = betterproto2.field(1, betterproto2.TYPE_BYTES)
|
name: bytes = betterproto.bytes_field(1)
|
||||||
data: "bytes" = betterproto2.field(2, betterproto2.TYPE_BYTES)
|
data: bytes = betterproto.bytes_field(2)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(eq=False, repr=False)
|
@dataclass
|
||||||
class StackHistoryBufferEntry(betterproto2.Message):
|
class StackHistoryBufferEntry(betterproto.Message):
|
||||||
addr: "BacktraceFrame | None" = betterproto2.field(1, betterproto2.TYPE_MESSAGE, optional=True)
|
addr: "BacktraceFrame" = betterproto.message_field(1)
|
||||||
fp: "int" = betterproto2.field(2, betterproto2.TYPE_UINT64)
|
fp: int = betterproto.uint64_field(2)
|
||||||
tag: "int" = betterproto2.field(3, betterproto2.TYPE_UINT64)
|
tag: int = betterproto.uint64_field(3)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(eq=False, repr=False)
|
@dataclass
|
||||||
class StackHistoryBuffer(betterproto2.Message):
|
class StackHistoryBuffer(betterproto.Message):
|
||||||
tid: "int" = betterproto2.field(1, betterproto2.TYPE_UINT64)
|
tid: int = betterproto.uint64_field(1)
|
||||||
entries: "list[StackHistoryBufferEntry]" = betterproto2.field(2, betterproto2.TYPE_MESSAGE, repeated=True)
|
entries: List["StackHistoryBufferEntry"] = betterproto.message_field(2)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(eq=False, repr=False)
|
@dataclass
|
||||||
class Tombstone(betterproto2.Message):
|
class Tombstone(betterproto.Message):
|
||||||
arch: "Architecture" = betterproto2.field(1, betterproto2.TYPE_ENUM, default_factory=lambda: Architecture(0))
|
arch: "Architecture" = betterproto.enum_field(1)
|
||||||
guest_arch: "Architecture" = betterproto2.field(24, betterproto2.TYPE_ENUM, default_factory=lambda: Architecture(0))
|
guest_arch: "Architecture" = betterproto.enum_field(24)
|
||||||
build_fingerprint: "str" = betterproto2.field(2, betterproto2.TYPE_STRING)
|
build_fingerprint: str = betterproto.string_field(2)
|
||||||
revision: "str" = betterproto2.field(3, betterproto2.TYPE_STRING)
|
revision: str = betterproto.string_field(3)
|
||||||
timestamp: "str" = betterproto2.field(4, betterproto2.TYPE_STRING)
|
timestamp: str = betterproto.string_field(4)
|
||||||
pid: "int" = betterproto2.field(5, betterproto2.TYPE_UINT32)
|
pid: int = betterproto.uint32_field(5)
|
||||||
tid: "int" = betterproto2.field(6, betterproto2.TYPE_UINT32)
|
tid: int = betterproto.uint32_field(6)
|
||||||
uid: "int" = betterproto2.field(7, betterproto2.TYPE_UINT32)
|
uid: int = betterproto.uint32_field(7)
|
||||||
selinux_label: "str" = betterproto2.field(8, betterproto2.TYPE_STRING)
|
selinux_label: str = betterproto.string_field(8)
|
||||||
command_line: "list[str]" = betterproto2.field(9, betterproto2.TYPE_STRING, repeated=True)
|
command_line: List[str] = betterproto.string_field(9)
|
||||||
# Process uptime in seconds.
|
# Process uptime in seconds.
|
||||||
process_uptime: "int" = betterproto2.field(20, betterproto2.TYPE_UINT32)
|
process_uptime: int = betterproto.uint32_field(20)
|
||||||
signal_info: "Signal | None" = betterproto2.field(10, betterproto2.TYPE_MESSAGE, optional=True)
|
signal_info: "Signal" = betterproto.message_field(10)
|
||||||
abort_message: "str" = betterproto2.field(14, betterproto2.TYPE_STRING)
|
abort_message: str = betterproto.string_field(14)
|
||||||
crash_details: "list[CrashDetail]" = betterproto2.field(21, betterproto2.TYPE_MESSAGE, repeated=True)
|
crash_details: List["CrashDetail"] = betterproto.message_field(21)
|
||||||
causes: "list[Cause]" = betterproto2.field(15, betterproto2.TYPE_MESSAGE, repeated=True)
|
causes: List["Cause"] = betterproto.message_field(15)
|
||||||
threads: "dict[int, Thread]" = betterproto2.field(
|
threads: Dict[int, "Thread"] = betterproto.map_field(
|
||||||
16, betterproto2.TYPE_MAP, map_meta=betterproto2.map_meta(betterproto2.TYPE_UINT32, betterproto2.TYPE_MESSAGE)
|
16, betterproto.TYPE_UINT32, betterproto.TYPE_MESSAGE
|
||||||
)
|
)
|
||||||
guest_threads: "dict[int, Thread]" = betterproto2.field(
|
guest_threads: Dict[int, "Thread"] = betterproto.map_field(
|
||||||
25, betterproto2.TYPE_MAP, map_meta=betterproto2.map_meta(betterproto2.TYPE_UINT32, betterproto2.TYPE_MESSAGE)
|
25, betterproto.TYPE_UINT32, betterproto.TYPE_MESSAGE
|
||||||
)
|
)
|
||||||
memory_mappings: "list[MemoryMapping]" = betterproto2.field(17, betterproto2.TYPE_MESSAGE, repeated=True)
|
memory_mappings: List["MemoryMapping"] = betterproto.message_field(17)
|
||||||
log_buffers: "list[LogBuffer]" = betterproto2.field(18, betterproto2.TYPE_MESSAGE, repeated=True)
|
log_buffers: List["LogBuffer"] = betterproto.message_field(18)
|
||||||
open_fds: "list[FD]" = betterproto2.field(19, betterproto2.TYPE_MESSAGE, repeated=True)
|
open_fds: List["FD"] = betterproto.message_field(19)
|
||||||
page_size: "int" = betterproto2.field(22, betterproto2.TYPE_UINT32)
|
page_size: int = betterproto.uint32_field(22)
|
||||||
has_been_16kb_mode: "bool" = betterproto2.field(23, betterproto2.TYPE_BOOL)
|
has_been_16kb_mode: bool = betterproto.bool_field(23)
|
||||||
stack_history_buffer: "StackHistoryBuffer | None" = betterproto2.field(26, betterproto2.TYPE_MESSAGE, optional=True)
|
stack_history_buffer: "StackHistoryBuffer" = betterproto.message_field(26)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(eq=False, repr=False)
|
@dataclass
|
||||||
class Signal(betterproto2.Message):
|
class Signal(betterproto.Message):
|
||||||
number: "int" = betterproto2.field(1, betterproto2.TYPE_INT32)
|
number: int = betterproto.int32_field(1)
|
||||||
name: "str" = betterproto2.field(2, betterproto2.TYPE_STRING)
|
name: str = betterproto.string_field(2)
|
||||||
code: "int" = betterproto2.field(3, betterproto2.TYPE_INT32)
|
code: int = betterproto.int32_field(3)
|
||||||
code_name: "str" = betterproto2.field(4, betterproto2.TYPE_STRING)
|
code_name: str = betterproto.string_field(4)
|
||||||
has_sender: "bool" = betterproto2.field(5, betterproto2.TYPE_BOOL)
|
has_sender: bool = betterproto.bool_field(5)
|
||||||
sender_uid: "int" = betterproto2.field(6, betterproto2.TYPE_INT32)
|
sender_uid: int = betterproto.int32_field(6)
|
||||||
sender_pid: "int" = betterproto2.field(7, betterproto2.TYPE_INT32)
|
sender_pid: int = betterproto.int32_field(7)
|
||||||
has_fault_address: "bool" = betterproto2.field(8, betterproto2.TYPE_BOOL)
|
has_fault_address: bool = betterproto.bool_field(8)
|
||||||
fault_address: "int" = betterproto2.field(9, betterproto2.TYPE_UINT64)
|
fault_address: int = betterproto.uint64_field(9)
|
||||||
# Note, may or may not contain the dump of the actual memory contents.
|
# Note, may or may not contain the dump of the actual memory contents.
|
||||||
# Currently, on arm64, we only include metadata, and not the contents.
|
# Currently, on arm64, we only include metadata, and not the contents.
|
||||||
fault_adjacent_metadata: "MemoryDump | None" = betterproto2.field(10, betterproto2.TYPE_MESSAGE, optional=True)
|
fault_adjacent_metadata: "MemoryDump" = betterproto.message_field(10)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(eq=False, repr=False)
|
@dataclass
|
||||||
class HeapObject(betterproto2.Message):
|
class HeapObject(betterproto.Message):
|
||||||
address: "int" = betterproto2.field(1, betterproto2.TYPE_UINT64)
|
address: int = betterproto.uint64_field(1)
|
||||||
size: "int" = betterproto2.field(2, betterproto2.TYPE_UINT64)
|
size: int = betterproto.uint64_field(2)
|
||||||
allocation_tid: "int" = betterproto2.field(3, betterproto2.TYPE_UINT64)
|
allocation_tid: int = betterproto.uint64_field(3)
|
||||||
allocation_backtrace: "list[BacktraceFrame]" = betterproto2.field(4, betterproto2.TYPE_MESSAGE, repeated=True)
|
allocation_backtrace: List["BacktraceFrame"] = betterproto.message_field(4)
|
||||||
deallocation_tid: "int" = betterproto2.field(5, betterproto2.TYPE_UINT64)
|
deallocation_tid: int = betterproto.uint64_field(5)
|
||||||
deallocation_backtrace: "list[BacktraceFrame]" = betterproto2.field(6, betterproto2.TYPE_MESSAGE, repeated=True)
|
deallocation_backtrace: List["BacktraceFrame"] = betterproto.message_field(6)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(eq=False, repr=False)
|
@dataclass
|
||||||
class MemoryError(betterproto2.Message):
|
class MemoryError(betterproto.Message):
|
||||||
tool: "MemoryErrorTool" = betterproto2.field(1, betterproto2.TYPE_ENUM, default_factory=lambda: MemoryErrorTool(0))
|
tool: "MemoryErrorTool" = betterproto.enum_field(1)
|
||||||
type: "MemoryErrorType" = betterproto2.field(2, betterproto2.TYPE_ENUM, default_factory=lambda: MemoryErrorType(0))
|
type: "MemoryErrorType" = betterproto.enum_field(2)
|
||||||
heap: "HeapObject | None" = betterproto2.field(3, betterproto2.TYPE_MESSAGE, optional=True, group="location")
|
heap: "HeapObject" = betterproto.message_field(3, group="location")
|
||||||
|
|
||||||
|
|
||||||
@dataclass(eq=False, repr=False)
|
@dataclass
|
||||||
class Cause(betterproto2.Message):
|
class Cause(betterproto.Message):
|
||||||
human_readable: "str" = betterproto2.field(1, betterproto2.TYPE_STRING)
|
human_readable: str = betterproto.string_field(1)
|
||||||
memory_error: "MemoryError | None" = betterproto2.field(2, betterproto2.TYPE_MESSAGE, optional=True, group="details")
|
memory_error: "MemoryError" = betterproto.message_field(2, group="details")
|
||||||
|
|
||||||
|
|
||||||
@dataclass(eq=False, repr=False)
|
@dataclass
|
||||||
class Register(betterproto2.Message):
|
class Register(betterproto.Message):
|
||||||
name: "str" = betterproto2.field(1, betterproto2.TYPE_STRING)
|
name: str = betterproto.string_field(1)
|
||||||
u64: "int" = betterproto2.field(2, betterproto2.TYPE_UINT64)
|
u64: int = betterproto.uint64_field(2)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(eq=False, repr=False)
|
@dataclass
|
||||||
class Thread(betterproto2.Message):
|
class Thread(betterproto.Message):
|
||||||
id: "int" = betterproto2.field(1, betterproto2.TYPE_INT32)
|
id: int = betterproto.int32_field(1)
|
||||||
name: "str" = betterproto2.field(2, betterproto2.TYPE_STRING)
|
name: str = betterproto.string_field(2)
|
||||||
registers: "list[Register]" = betterproto2.field(3, betterproto2.TYPE_MESSAGE, repeated=True)
|
registers: List["Register"] = betterproto.message_field(3)
|
||||||
backtrace_note: "list[str]" = betterproto2.field(7, betterproto2.TYPE_STRING, repeated=True)
|
backtrace_note: List[str] = betterproto.string_field(7)
|
||||||
unreadable_elf_files: "list[str]" = betterproto2.field(9, betterproto2.TYPE_STRING, repeated=True)
|
unreadable_elf_files: List[str] = betterproto.string_field(9)
|
||||||
current_backtrace: "list[BacktraceFrame]" = betterproto2.field(4, betterproto2.TYPE_MESSAGE, repeated=True)
|
current_backtrace: List["BacktraceFrame"] = betterproto.message_field(4)
|
||||||
memory_dump: "list[MemoryDump]" = betterproto2.field(5, betterproto2.TYPE_MESSAGE, repeated=True)
|
memory_dump: List["MemoryDump"] = betterproto.message_field(5)
|
||||||
tagged_addr_ctrl: "int" = betterproto2.field(6, betterproto2.TYPE_INT64)
|
tagged_addr_ctrl: int = betterproto.int64_field(6)
|
||||||
pac_enabled_keys: "int" = betterproto2.field(8, betterproto2.TYPE_INT64)
|
pac_enabled_keys: int = betterproto.int64_field(8)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(eq=False, repr=False)
|
@dataclass
|
||||||
class BacktraceFrame(betterproto2.Message):
|
class BacktraceFrame(betterproto.Message):
|
||||||
rel_pc: "int" = betterproto2.field(1, betterproto2.TYPE_UINT64)
|
rel_pc: int = betterproto.uint64_field(1)
|
||||||
pc: "int" = betterproto2.field(2, betterproto2.TYPE_UINT64)
|
pc: int = betterproto.uint64_field(2)
|
||||||
sp: "int" = betterproto2.field(3, betterproto2.TYPE_UINT64)
|
sp: int = betterproto.uint64_field(3)
|
||||||
function_name: "str" = betterproto2.field(4, betterproto2.TYPE_STRING)
|
function_name: str = betterproto.string_field(4)
|
||||||
function_offset: "int" = betterproto2.field(5, betterproto2.TYPE_UINT64)
|
function_offset: int = betterproto.uint64_field(5)
|
||||||
file_name: "str" = betterproto2.field(6, betterproto2.TYPE_STRING)
|
file_name: str = betterproto.string_field(6)
|
||||||
file_map_offset: "int" = betterproto2.field(7, betterproto2.TYPE_UINT64)
|
file_map_offset: int = betterproto.uint64_field(7)
|
||||||
build_id: "str" = betterproto2.field(8, betterproto2.TYPE_STRING)
|
build_id: str = betterproto.string_field(8)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(eq=False, repr=False)
|
@dataclass
|
||||||
class ArmMTEMetadata(betterproto2.Message):
|
class ArmMTEMetadata(betterproto.Message):
|
||||||
# One memory tag per granule (e.g. every 16 bytes) of regular memory.
|
# One memory tag per granule (e.g. every 16 bytes) of regular memory.
|
||||||
memory_tags: "bytes" = betterproto2.field(1, betterproto2.TYPE_BYTES)
|
memory_tags: bytes = betterproto.bytes_field(1)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(eq=False, repr=False)
|
@dataclass
|
||||||
class MemoryDump(betterproto2.Message):
|
class MemoryDump(betterproto.Message):
|
||||||
register_name: "str" = betterproto2.field(1, betterproto2.TYPE_STRING)
|
register_name: str = betterproto.string_field(1)
|
||||||
mapping_name: "str" = betterproto2.field(2, betterproto2.TYPE_STRING)
|
mapping_name: str = betterproto.string_field(2)
|
||||||
begin_address: "int" = betterproto2.field(3, betterproto2.TYPE_UINT64)
|
begin_address: int = betterproto.uint64_field(3)
|
||||||
memory: "bytes" = betterproto2.field(4, betterproto2.TYPE_BYTES)
|
memory: bytes = betterproto.bytes_field(4)
|
||||||
arm_mte_metadata: "ArmMTEMetadata | None" = betterproto2.field(6, betterproto2.TYPE_MESSAGE, optional=True, group="metadata")
|
arm_mte_metadata: "ArmMTEMetadata" = betterproto.message_field(6, group="metadata")
|
||||||
|
|
||||||
|
|
||||||
@dataclass(eq=False, repr=False)
|
@dataclass
|
||||||
class MemoryMapping(betterproto2.Message):
|
class MemoryMapping(betterproto.Message):
|
||||||
begin_address: "int" = betterproto2.field(1, betterproto2.TYPE_UINT64)
|
begin_address: int = betterproto.uint64_field(1)
|
||||||
end_address: "int" = betterproto2.field(2, betterproto2.TYPE_UINT64)
|
end_address: int = betterproto.uint64_field(2)
|
||||||
offset: "int" = betterproto2.field(3, betterproto2.TYPE_UINT64)
|
offset: int = betterproto.uint64_field(3)
|
||||||
read: "bool" = betterproto2.field(4, betterproto2.TYPE_BOOL)
|
read: bool = betterproto.bool_field(4)
|
||||||
write: "bool" = betterproto2.field(5, betterproto2.TYPE_BOOL)
|
write: bool = betterproto.bool_field(5)
|
||||||
execute: "bool" = betterproto2.field(6, betterproto2.TYPE_BOOL)
|
execute: bool = betterproto.bool_field(6)
|
||||||
mapping_name: "str" = betterproto2.field(7, betterproto2.TYPE_STRING)
|
mapping_name: str = betterproto.string_field(7)
|
||||||
build_id: "str" = betterproto2.field(8, betterproto2.TYPE_STRING)
|
build_id: str = betterproto.string_field(8)
|
||||||
load_bias: "int" = betterproto2.field(9, betterproto2.TYPE_UINT64)
|
load_bias: int = betterproto.uint64_field(9)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(eq=False, repr=False)
|
@dataclass
|
||||||
class FD(betterproto2.Message):
|
class FD(betterproto.Message):
|
||||||
fd: "int" = betterproto2.field(1, betterproto2.TYPE_INT32)
|
fd: int = betterproto.int32_field(1)
|
||||||
path: "str" = betterproto2.field(2, betterproto2.TYPE_STRING)
|
path: str = betterproto.string_field(2)
|
||||||
owner: "str" = betterproto2.field(3, betterproto2.TYPE_STRING)
|
owner: str = betterproto.string_field(3)
|
||||||
tag: "int" = betterproto2.field(4, betterproto2.TYPE_UINT64)
|
tag: int = betterproto.uint64_field(4)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(eq=False, repr=False)
|
@dataclass
|
||||||
class LogBuffer(betterproto2.Message):
|
class LogBuffer(betterproto.Message):
|
||||||
name: "str" = betterproto2.field(1, betterproto2.TYPE_STRING)
|
name: str = betterproto.string_field(1)
|
||||||
logs: "list[LogMessage]" = betterproto2.field(2, betterproto2.TYPE_MESSAGE, repeated=True)
|
logs: List["LogMessage"] = betterproto.message_field(2)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(eq=False, repr=False)
|
@dataclass
|
||||||
class LogMessage(betterproto2.Message):
|
class LogMessage(betterproto.Message):
|
||||||
timestamp: "str" = betterproto2.field(1, betterproto2.TYPE_STRING)
|
timestamp: str = betterproto.string_field(1)
|
||||||
pid: "int" = betterproto2.field(2, betterproto2.TYPE_UINT32)
|
pid: int = betterproto.uint32_field(2)
|
||||||
tid: "int" = betterproto2.field(3, betterproto2.TYPE_UINT32)
|
tid: int = betterproto.uint32_field(3)
|
||||||
priority: "int" = betterproto2.field(4, betterproto2.TYPE_UINT32)
|
priority: int = betterproto.uint32_field(4)
|
||||||
tag: "str" = betterproto2.field(5, betterproto2.TYPE_STRING)
|
tag: str = betterproto.string_field(5)
|
||||||
message: "str" = betterproto2.field(6, betterproto2.TYPE_STRING)
|
message: str = betterproto.string_field(6)
|
||||||
|
|||||||
@@ -52,7 +52,9 @@ class Indicators:
|
|||||||
if os.path.isfile(path) and path.lower().endswith(".stix2"):
|
if os.path.isfile(path) and path.lower().endswith(".stix2"):
|
||||||
self.parse_stix2(path)
|
self.parse_stix2(path)
|
||||||
elif os.path.isdir(path):
|
elif os.path.isdir(path):
|
||||||
for file in glob.glob(os.path.join(path, "**", "*.stix2"), recursive=True):
|
for file in glob.glob(
|
||||||
|
os.path.join(path, "**", "*.stix2", recursive=True)
|
||||||
|
):
|
||||||
self.parse_stix2(file)
|
self.parse_stix2(file)
|
||||||
else:
|
else:
|
||||||
self.log.error(
|
self.log.error(
|
||||||
@@ -100,17 +102,6 @@ class Indicators:
|
|||||||
key, value = indicator.get("pattern", "").strip("[]").split("=")
|
key, value = indicator.get("pattern", "").strip("[]").split("=")
|
||||||
key = key.strip()
|
key = key.strip()
|
||||||
|
|
||||||
# Normalize hash algorithm keys so that both the STIX2-spec-compliant
|
|
||||||
# form (e.g. file:hashes.'SHA-256', which requires quotes around
|
|
||||||
# algorithm names that contain hyphens) and the non-standard lowercase
|
|
||||||
# form (e.g. file:hashes.sha256) are accepted. Strip single quotes and
|
|
||||||
# hyphens from the algorithm name only, then lowercase it.
|
|
||||||
for sep in ("hashes.", "cert."):
|
|
||||||
if sep in key:
|
|
||||||
prefix, _, algo = key.partition(sep)
|
|
||||||
key = prefix + sep + algo.replace("'", "").replace("-", "").lower()
|
|
||||||
break
|
|
||||||
|
|
||||||
if key == "domain-name:value":
|
if key == "domain-name:value":
|
||||||
# We force domain names to lower case.
|
# We force domain names to lower case.
|
||||||
self._add_indicator(
|
self._add_indicator(
|
||||||
@@ -777,30 +768,6 @@ class Indicators:
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def check_receiver_prefix(self, receiver_name: str) -> Union[dict, None]:
|
|
||||||
"""Check the provided receiver name against the list of indicators.
|
|
||||||
An IoC match is detected when a substring of the receiver matches the indicator
|
|
||||||
:param app_id: App ID to check against the list of indicators
|
|
||||||
:type app_id: str
|
|
||||||
:returns: Indicator details if matched, otherwise None
|
|
||||||
|
|
||||||
"""
|
|
||||||
if not receiver_name:
|
|
||||||
return None
|
|
||||||
|
|
||||||
for ioc in self.get_iocs("app_ids"):
|
|
||||||
if ioc["value"].lower() in receiver_name.lower():
|
|
||||||
self.log.warning(
|
|
||||||
'Found a known suspicious receiver with name "%s" '
|
|
||||||
'matching indicators from "%s"',
|
|
||||||
receiver_name,
|
|
||||||
ioc["name"],
|
|
||||||
)
|
|
||||||
return ioc
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def check_android_property_name(self, property_name: str) -> Optional[dict]:
|
def check_android_property_name(self, property_name: str) -> Optional[dict]:
|
||||||
"""Check the android property name against the list of indicators.
|
"""Check the android property name against the list of indicators.
|
||||||
|
|
||||||
|
|||||||
@@ -3,4 +3,4 @@
|
|||||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||||
# https://license.mvt.re/1.1/
|
# https://license.mvt.re/1.1/
|
||||||
|
|
||||||
MVT_VERSION = "2.7.0"
|
MVT_VERSION = "2.6.1"
|
||||||
|
|||||||
@@ -631,10 +631,6 @@
|
|||||||
"build": "16H81",
|
"build": "16H81",
|
||||||
"version": "12.5.7"
|
"version": "12.5.7"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"version": "12.5.8",
|
|
||||||
"build": "16H88"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"build": "17A577",
|
"build": "17A577",
|
||||||
"version": "13.0"
|
"version": "13.0"
|
||||||
@@ -903,14 +899,6 @@
|
|||||||
"version": "15.8.5",
|
"version": "15.8.5",
|
||||||
"build": "19H394"
|
"build": "19H394"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"version": "15.8.6",
|
|
||||||
"build": "19H402"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "15.8.7",
|
|
||||||
"build": "19H411"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"build": "20A362",
|
"build": "20A362",
|
||||||
"version": "16.0"
|
"version": "16.0"
|
||||||
@@ -1020,14 +1008,6 @@
|
|||||||
"version": "16.7.12",
|
"version": "16.7.12",
|
||||||
"build": "20H364"
|
"build": "20H364"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"version": "16.7.14",
|
|
||||||
"build": "20H370"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "16.7.15",
|
|
||||||
"build": "20H380"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"version": "17.0",
|
"version": "17.0",
|
||||||
"build": "21A327"
|
"build": "21A327"
|
||||||
@@ -1184,22 +1164,6 @@
|
|||||||
"version": "18.7.3",
|
"version": "18.7.3",
|
||||||
"build": "22H217"
|
"build": "22H217"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"version": "18.7.4",
|
|
||||||
"build": "22H218"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "18.7.5",
|
|
||||||
"build": "22H311"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "18.7.6",
|
|
||||||
"build": "22H320"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "18.7.7",
|
|
||||||
"build": "22H333"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"version": "26",
|
"version": "26",
|
||||||
"build": "23A341"
|
"build": "23A341"
|
||||||
@@ -1215,21 +1179,5 @@
|
|||||||
{
|
{
|
||||||
"version": "26.2",
|
"version": "26.2",
|
||||||
"build": "23C55"
|
"build": "23C55"
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "26.2.1",
|
|
||||||
"build": "23C71"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "26.3",
|
|
||||||
"build": "23D127"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "26.3.1",
|
|
||||||
"build": "23D8133"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "26.4",
|
|
||||||
"build": "23E246"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
+58
-180
@@ -6,146 +6,17 @@
|
|||||||
import binascii
|
import binascii
|
||||||
import glob
|
import glob
|
||||||
import logging
|
import logging
|
||||||
|
import multiprocessing
|
||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
import plistlib
|
|
||||||
import shutil
|
import shutil
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import tempfile
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from iphone_backup_decrypt import EncryptedBackup
|
from iOSbackup import iOSbackup
|
||||||
from iphone_backup_decrypt import google_iphone_dataprotection
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Import pbkdf2_hmac from the same source iphone_backup_decrypt uses internally,
|
|
||||||
# so our key derivation is consistent with theirs.
|
|
||||||
try:
|
|
||||||
from fastpbkdf2 import pbkdf2_hmac
|
|
||||||
except ImportError:
|
|
||||||
import Crypto.Hash.SHA1
|
|
||||||
import Crypto.Hash.SHA256
|
|
||||||
import Crypto.Protocol.KDF
|
|
||||||
|
|
||||||
_HASH_FNS = {"sha1": Crypto.Hash.SHA1, "sha256": Crypto.Hash.SHA256}
|
|
||||||
|
|
||||||
def pbkdf2_hmac(hash_name, password, salt, iterations, dklen=None):
|
|
||||||
return Crypto.Protocol.KDF.PBKDF2(
|
|
||||||
password, salt, dklen, iterations, hmac_hash_module=_HASH_FNS[hash_name]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MVTEncryptedBackup(EncryptedBackup):
|
|
||||||
"""Extends EncryptedBackup with derived key export/import.
|
|
||||||
|
|
||||||
NOTE: This subclass relies on internal APIs of iphone_backup_decrypt
|
|
||||||
(specifically _read_and_unlock_keybag, _keybag, and the Keybag class
|
|
||||||
internals). Pinned to iphone_backup_decrypt==0.9.0.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *, backup_directory, passphrase=None, derived_key=None):
|
|
||||||
if passphrase:
|
|
||||||
super().__init__(backup_directory=backup_directory, passphrase=passphrase)
|
|
||||||
self._derived_key = None # Will be set after keybag unlock
|
|
||||||
elif derived_key:
|
|
||||||
self._init_without_passphrase(backup_directory, derived_key)
|
|
||||||
else:
|
|
||||||
raise ValueError("Either passphrase or derived_key must be provided")
|
|
||||||
|
|
||||||
def _init_without_passphrase(self, backup_directory, derived_key):
|
|
||||||
"""Replicate parent __init__ state without requiring a passphrase."""
|
|
||||||
self.decrypted = False
|
|
||||||
self._backup_directory = os.path.expandvars(backup_directory)
|
|
||||||
self._passphrase = None
|
|
||||||
self._manifest_plist_path = os.path.join(
|
|
||||||
self._backup_directory, "Manifest.plist"
|
|
||||||
)
|
|
||||||
self._manifest_plist = None
|
|
||||||
self._manifest_db_path = os.path.join(self._backup_directory, "Manifest.db")
|
|
||||||
self._keybag = None
|
|
||||||
self._unlocked = False
|
|
||||||
self._temporary_folder = tempfile.mkdtemp()
|
|
||||||
self._temp_decrypted_manifest_db_path = os.path.join(
|
|
||||||
self._temporary_folder, "Manifest.db"
|
|
||||||
)
|
|
||||||
self._temp_manifest_db_conn = None
|
|
||||||
self._derived_key = derived_key # 32 raw bytes
|
|
||||||
|
|
||||||
def _read_and_unlock_keybag(self):
|
|
||||||
"""Override to capture derived key on password unlock, or use
|
|
||||||
a pre-derived key to skip PBKDF2."""
|
|
||||||
if self._unlocked:
|
|
||||||
return self._unlocked
|
|
||||||
|
|
||||||
with open(self._manifest_plist_path, "rb") as infile:
|
|
||||||
self._manifest_plist = plistlib.load(infile)
|
|
||||||
self._keybag = google_iphone_dataprotection.Keybag(
|
|
||||||
self._manifest_plist["BackupKeyBag"]
|
|
||||||
)
|
|
||||||
|
|
||||||
if self._derived_key:
|
|
||||||
# Skip PBKDF2, unwrap class keys directly with pre-derived key
|
|
||||||
self._unlocked = _unlock_keybag_with_derived_key(
|
|
||||||
self._keybag, self._derived_key
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Normal path: full PBKDF2 derivation, capturing the intermediate key
|
|
||||||
self._unlocked, self._derived_key = _unlock_keybag_and_capture_key(
|
|
||||||
self._keybag, self._passphrase
|
|
||||||
)
|
|
||||||
self._passphrase = None
|
|
||||||
|
|
||||||
if not self._unlocked:
|
|
||||||
raise ValueError("Failed to decrypt keys: incorrect passphrase?")
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_decryption_key(self):
|
|
||||||
"""Return derived key as hex string (64 chars / 32 bytes)."""
|
|
||||||
if self._derived_key is None:
|
|
||||||
raise ValueError("No derived key available")
|
|
||||||
return self._derived_key.hex()
|
|
||||||
|
|
||||||
|
|
||||||
def _unlock_keybag_with_derived_key(keybag, passphrase_key):
|
|
||||||
"""Unlock keybag class keys using a pre-derived passphrase_key,
|
|
||||||
skipping the expensive PBKDF2 rounds."""
|
|
||||||
WRAP_PASSPHRASE = 2
|
|
||||||
for classkey in keybag.classKeys.values():
|
|
||||||
if b"WPKY" not in classkey:
|
|
||||||
continue
|
|
||||||
if classkey[b"WRAP"] & WRAP_PASSPHRASE:
|
|
||||||
k = google_iphone_dataprotection._AESUnwrap(
|
|
||||||
passphrase_key, classkey[b"WPKY"]
|
|
||||||
)
|
|
||||||
if not k:
|
|
||||||
return False
|
|
||||||
classkey[b"KEY"] = k
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def _unlock_keybag_and_capture_key(keybag, passphrase):
|
|
||||||
"""Run full PBKDF2 key derivation and AES unwrap, returning
|
|
||||||
(success, passphrase_key) so the derived key can be exported."""
|
|
||||||
passphrase_round1 = pbkdf2_hmac(
|
|
||||||
"sha256", passphrase, keybag.attrs[b"DPSL"], keybag.attrs[b"DPIC"], 32
|
|
||||||
)
|
|
||||||
passphrase_key = pbkdf2_hmac(
|
|
||||||
"sha1", passphrase_round1, keybag.attrs[b"SALT"], keybag.attrs[b"ITER"], 32
|
|
||||||
)
|
|
||||||
WRAP_PASSPHRASE = 2
|
|
||||||
for classkey in keybag.classKeys.values():
|
|
||||||
if b"WPKY" not in classkey:
|
|
||||||
continue
|
|
||||||
if classkey[b"WRAP"] & WRAP_PASSPHRASE:
|
|
||||||
k = google_iphone_dataprotection._AESUnwrap(
|
|
||||||
passphrase_key, classkey[b"WPKY"]
|
|
||||||
)
|
|
||||||
if not k:
|
|
||||||
return False, None
|
|
||||||
classkey[b"KEY"] = k
|
|
||||||
return True, passphrase_key
|
|
||||||
|
|
||||||
|
|
||||||
class DecryptBackup:
|
class DecryptBackup:
|
||||||
"""This class provides functions to decrypt an encrypted iTunes backup
|
"""This class provides functions to decrypt an encrypted iTunes backup
|
||||||
@@ -184,27 +55,41 @@ class DecryptBackup:
|
|||||||
log.critical("The backup does not seem encrypted!")
|
log.critical("The backup does not seem encrypted!")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _process_file(
|
||||||
|
self, relative_path: str, domain: str, item, file_id: str, item_folder: str
|
||||||
|
) -> None:
|
||||||
|
self._backup.getFileDecryptedCopy(
|
||||||
|
manifestEntry=item, targetName=file_id, targetFolder=item_folder
|
||||||
|
)
|
||||||
|
log.info(
|
||||||
|
"Decrypted file %s [%s] to %s/%s",
|
||||||
|
relative_path,
|
||||||
|
domain,
|
||||||
|
item_folder,
|
||||||
|
file_id,
|
||||||
|
)
|
||||||
|
|
||||||
def process_backup(self) -> None:
|
def process_backup(self) -> None:
|
||||||
if not os.path.exists(self.dest_path):
|
if not os.path.exists(self.dest_path):
|
||||||
os.makedirs(self.dest_path)
|
os.makedirs(self.dest_path)
|
||||||
|
|
||||||
manifest_path = os.path.join(self.dest_path, "Manifest.db")
|
manifest_path = os.path.join(self.dest_path, "Manifest.db")
|
||||||
# Extract a decrypted Manifest.db to the destination folder.
|
# We extract a decrypted Manifest.db.
|
||||||
self._backup.save_manifest_file(output_filename=manifest_path)
|
self._backup.getManifestDB()
|
||||||
|
# We store it to the destination folder.
|
||||||
|
shutil.copy(self._backup.manifestDB, manifest_path)
|
||||||
|
|
||||||
|
pool = multiprocessing.Pool(multiprocessing.cpu_count())
|
||||||
|
|
||||||
|
for item in self._backup.getBackupFilesList():
|
||||||
|
try:
|
||||||
|
file_id = item["backupFile"]
|
||||||
|
relative_path = item["relativePath"]
|
||||||
|
domain = item["domain"]
|
||||||
|
|
||||||
# Iterate over all files in the backup and decrypt them,
|
|
||||||
# preserving the XX/file_id directory structure that downstream
|
|
||||||
# modules expect.
|
|
||||||
with self._backup.manifest_db_cursor() as cur:
|
|
||||||
cur.execute(
|
|
||||||
"SELECT fileID, domain, relativePath, file FROM Files WHERE flags=1"
|
|
||||||
)
|
|
||||||
for file_id, domain, relative_path, file_bplist in cur:
|
|
||||||
# This may be a partial backup. Skip files from the manifest
|
# This may be a partial backup. Skip files from the manifest
|
||||||
# which do not exist locally.
|
# which do not exist locally.
|
||||||
source_file_path = os.path.join(
|
source_file_path = os.path.join(self.backup_path, file_id[0:2], file_id)
|
||||||
self.backup_path, file_id[:2], file_id
|
|
||||||
)
|
|
||||||
if not os.path.exists(source_file_path):
|
if not os.path.exists(source_file_path):
|
||||||
log.debug(
|
log.debug(
|
||||||
"Skipping file %s. File not found in encrypted backup directory.",
|
"Skipping file %s. File not found in encrypted backup directory.",
|
||||||
@@ -212,26 +97,24 @@ class DecryptBackup:
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
item_folder = os.path.join(self.dest_path, file_id[:2])
|
item_folder = os.path.join(self.dest_path, file_id[0:2])
|
||||||
os.makedirs(item_folder, exist_ok=True)
|
if not os.path.exists(item_folder):
|
||||||
|
os.makedirs(item_folder)
|
||||||
|
|
||||||
try:
|
# iOSBackup getFileDecryptedCopy() claims to read a "file"
|
||||||
decrypted = self._backup._decrypt_inner_file(
|
# parameter but the code actually is reading the "manifest" key.
|
||||||
file_id=file_id, file_bplist=file_bplist
|
# Add manifest plist to both keys to handle this.
|
||||||
)
|
item["manifest"] = item["file"]
|
||||||
with open(
|
|
||||||
os.path.join(item_folder, file_id), "wb"
|
pool.apply_async(
|
||||||
) as handle:
|
self._process_file,
|
||||||
handle.write(decrypted)
|
args=(relative_path, domain, item, file_id, item_folder),
|
||||||
log.info(
|
)
|
||||||
"Decrypted file %s [%s] to %s/%s",
|
except Exception as exc:
|
||||||
relative_path,
|
log.error("Failed to decrypt file %s: %s", relative_path, exc)
|
||||||
domain,
|
|
||||||
item_folder,
|
pool.close()
|
||||||
file_id,
|
pool.join()
|
||||||
)
|
|
||||||
except Exception as exc:
|
|
||||||
log.error("Failed to decrypt file %s: %s", relative_path, exc)
|
|
||||||
|
|
||||||
# Copying over the root plist files as well.
|
# Copying over the root plist files as well.
|
||||||
for file_name in os.listdir(self.backup_path):
|
for file_name in os.listdir(self.backup_path):
|
||||||
@@ -272,23 +155,20 @@ class DecryptBackup:
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._backup = MVTEncryptedBackup(
|
self._backup = iOSbackup(
|
||||||
backup_directory=self.backup_path,
|
udid=os.path.basename(self.backup_path),
|
||||||
passphrase=password,
|
cleartextpassword=password,
|
||||||
|
backuproot=os.path.dirname(self.backup_path),
|
||||||
)
|
)
|
||||||
# Eagerly trigger keybag unlock so wrong-password errors
|
|
||||||
# surface here rather than later during process_backup().
|
|
||||||
self._backup.test_decryption()
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self._backup = None
|
|
||||||
if (
|
if (
|
||||||
isinstance(exc, ValueError)
|
isinstance(exc, KeyError)
|
||||||
and "passphrase" in str(exc).lower()
|
and len(exc.args) > 0
|
||||||
|
and exc.args[0] == b"KEY"
|
||||||
):
|
):
|
||||||
log.critical("Failed to decrypt backup. Password is probably wrong.")
|
log.critical("Failed to decrypt backup. Password is probably wrong.")
|
||||||
elif (
|
elif (
|
||||||
isinstance(exc, FileNotFoundError)
|
isinstance(exc, FileNotFoundError)
|
||||||
and hasattr(exc, "filename")
|
|
||||||
and os.path.basename(exc.filename) == "Manifest.plist"
|
and os.path.basename(exc.filename) == "Manifest.plist"
|
||||||
):
|
):
|
||||||
log.critical(
|
log.critical(
|
||||||
@@ -331,14 +211,12 @@ class DecryptBackup:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
key_bytes_raw = binascii.unhexlify(key_bytes)
|
key_bytes_raw = binascii.unhexlify(key_bytes)
|
||||||
self._backup = MVTEncryptedBackup(
|
self._backup = iOSbackup(
|
||||||
backup_directory=self.backup_path,
|
udid=os.path.basename(self.backup_path),
|
||||||
derived_key=key_bytes_raw,
|
derivedkey=key_bytes_raw,
|
||||||
|
backuproot=os.path.dirname(self.backup_path),
|
||||||
)
|
)
|
||||||
# Eagerly trigger keybag unlock so wrong-key errors surface here.
|
|
||||||
self._backup.test_decryption()
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self._backup = None
|
|
||||||
log.exception(exc)
|
log.exception(exc)
|
||||||
log.critical(
|
log.critical(
|
||||||
"Failed to decrypt backup. Did you provide the correct key file?"
|
"Failed to decrypt backup. Did you provide the correct key file?"
|
||||||
@@ -349,7 +227,7 @@ class DecryptBackup:
|
|||||||
if not self._backup:
|
if not self._backup:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._decryption_key = self._backup.get_decryption_key()
|
self._decryption_key = self._backup.getDecryptionKey()
|
||||||
log.info(
|
log.info(
|
||||||
'Derived decryption key for backup at path %s is: "%s"',
|
'Derived decryption key for backup at path %s is: "%s"',
|
||||||
self.backup_path,
|
self.backup_path,
|
||||||
|
|||||||
@@ -123,11 +123,6 @@ class SMS(IOSExtraction):
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
items = list(cur)
|
items = list(cur)
|
||||||
elif "no such table" in str(exc):
|
|
||||||
self.log.info(
|
|
||||||
"No SMS tables found in the database, skipping: %s", exc
|
|
||||||
)
|
|
||||||
return
|
|
||||||
else:
|
else:
|
||||||
raise exc
|
raise exc
|
||||||
names = [description[0] for description in cur.description]
|
names = [description[0] for description in cur.description]
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
# https://license.mvt.re/1.1/
|
# https://license.mvt.re/1.1/
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import sqlite3
|
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
|
|
||||||
@@ -80,29 +79,21 @@ class SMSAttachments(IOSExtraction):
|
|||||||
|
|
||||||
conn = self._open_sqlite_db(self.file_path)
|
conn = self._open_sqlite_db(self.file_path)
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
try:
|
cur.execute(
|
||||||
cur.execute(
|
|
||||||
"""
|
|
||||||
SELECT
|
|
||||||
attachment.ROWID as "attachment_id",
|
|
||||||
attachment.*,
|
|
||||||
message.service as "service",
|
|
||||||
handle.id as "phone_number"
|
|
||||||
FROM attachment
|
|
||||||
LEFT JOIN message_attachment_join ON
|
|
||||||
message_attachment_join.attachment_id = attachment.ROWID
|
|
||||||
LEFT JOIN message ON
|
|
||||||
message.ROWID = message_attachment_join.message_id
|
|
||||||
LEFT JOIN handle ON handle.ROWID = message.handle_id;
|
|
||||||
"""
|
"""
|
||||||
)
|
SELECT
|
||||||
except sqlite3.OperationalError as exc:
|
attachment.ROWID as "attachment_id",
|
||||||
self.log.info(
|
attachment.*,
|
||||||
"No SMS attachment tables found in the database, skipping: %s", exc
|
message.service as "service",
|
||||||
)
|
handle.id as "phone_number"
|
||||||
cur.close()
|
FROM attachment
|
||||||
conn.close()
|
LEFT JOIN message_attachment_join ON
|
||||||
return
|
message_attachment_join.attachment_id = attachment.ROWID
|
||||||
|
LEFT JOIN message ON
|
||||||
|
message.ROWID = message_attachment_join.message_id
|
||||||
|
LEFT JOIN handle ON handle.ROWID = message.handle_id;
|
||||||
|
"""
|
||||||
|
)
|
||||||
names = [description[0] for description in cur.description]
|
names = [description[0] for description in cur.description]
|
||||||
|
|
||||||
for item in cur:
|
for item in cur:
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ def generate_test_stix_file(file_path):
|
|||||||
for h in sha256:
|
for h in sha256:
|
||||||
i = Indicator(
|
i = Indicator(
|
||||||
indicator_types=["malicious-activity"],
|
indicator_types=["malicious-activity"],
|
||||||
pattern="[file:hashes.'SHA-256'='{}']".format(h),
|
pattern="[file:hashes.sha256='{}']".format(h),
|
||||||
pattern_type="stix",
|
pattern_type="stix",
|
||||||
)
|
)
|
||||||
res.append(i)
|
res.append(i)
|
||||||
@@ -91,7 +91,7 @@ def generate_test_stix_file(file_path):
|
|||||||
for h in sha1:
|
for h in sha1:
|
||||||
i = Indicator(
|
i = Indicator(
|
||||||
indicator_types=["malicious-activity"],
|
indicator_types=["malicious-activity"],
|
||||||
pattern="[file:hashes.'SHA-1'='{}']".format(h),
|
pattern="[file:hashes.sha1='{}']".format(h),
|
||||||
pattern_type="stix",
|
pattern_type="stix",
|
||||||
)
|
)
|
||||||
res.append(i)
|
res.append(i)
|
||||||
|
|||||||
@@ -94,78 +94,6 @@ class TestIndicators:
|
|||||||
)
|
)
|
||||||
assert ind.check_file_hash("da0611a300a9ce9aa7a09d1212f203fca5856794")
|
assert ind.check_file_hash("da0611a300a9ce9aa7a09d1212f203fca5856794")
|
||||||
|
|
||||||
def test_parse_stix2_hash_key_variants(self, tmp_path):
|
|
||||||
"""STIX2 spec requires single-quoted algorithm names that contain hyphens,
|
|
||||||
e.g. file:hashes.'SHA-256'. Verify MVT accepts both spec-compliant and
|
|
||||||
non-standard lowercase spellings for MD5, SHA-1 and SHA-256."""
|
|
||||||
import json
|
|
||||||
|
|
||||||
sha256_hash = "570cd76bf49cf52e0cb347a68bdcf0590b2eaece134e1b1eba7e8d66261bdbe6"
|
|
||||||
sha1_hash = "da0611a300a9ce9aa7a09d1212f203fca5856794"
|
|
||||||
md5_hash = "d41d8cd98f00b204e9800998ecf8427e"
|
|
||||||
|
|
||||||
variants = [
|
|
||||||
# (pattern_key, expected_bucket)
|
|
||||||
("file:hashes.'SHA-256'", "files_sha256"),
|
|
||||||
("file:hashes.SHA-256", "files_sha256"),
|
|
||||||
("file:hashes.SHA256", "files_sha256"),
|
|
||||||
("file:hashes.sha256", "files_sha256"),
|
|
||||||
("file:hashes.'SHA-1'", "files_sha1"),
|
|
||||||
("file:hashes.SHA-1", "files_sha1"),
|
|
||||||
("file:hashes.SHA1", "files_sha1"),
|
|
||||||
("file:hashes.sha1", "files_sha1"),
|
|
||||||
("file:hashes.MD5", "files_md5"),
|
|
||||||
("file:hashes.'MD5'", "files_md5"),
|
|
||||||
("file:hashes.md5", "files_md5"),
|
|
||||||
]
|
|
||||||
|
|
||||||
hash_for = {
|
|
||||||
"files_sha256": sha256_hash,
|
|
||||||
"files_sha1": sha1_hash,
|
|
||||||
"files_md5": md5_hash,
|
|
||||||
}
|
|
||||||
|
|
||||||
for pattern_key, bucket in variants:
|
|
||||||
h = hash_for[bucket]
|
|
||||||
stix = {
|
|
||||||
"type": "bundle",
|
|
||||||
"id": "bundle--test",
|
|
||||||
"objects": [
|
|
||||||
{
|
|
||||||
"type": "malware",
|
|
||||||
"id": "malware--test",
|
|
||||||
"name": "TestMalware",
|
|
||||||
"is_family": False,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "indicator",
|
|
||||||
"id": "indicator--test",
|
|
||||||
"indicator_types": ["malicious-activity"],
|
|
||||||
"pattern": f"[{pattern_key}='{h}']",
|
|
||||||
"pattern_type": "stix",
|
|
||||||
"valid_from": "2024-01-01T00:00:00Z",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "relationship",
|
|
||||||
"id": "relationship--test",
|
|
||||||
"relationship_type": "indicates",
|
|
||||||
"source_ref": "indicator--test",
|
|
||||||
"target_ref": "malware--test",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
stix_file = tmp_path / "test.stix2"
|
|
||||||
stix_file.write_text(json.dumps(stix))
|
|
||||||
|
|
||||||
ind = Indicators(log=logging)
|
|
||||||
ind.load_indicators_files([str(stix_file)], load_default=False)
|
|
||||||
assert len(ind.ioc_collections[0][bucket]) == 1, (
|
|
||||||
f"Pattern key '{pattern_key}' was not parsed into '{bucket}'"
|
|
||||||
)
|
|
||||||
assert ind.check_file_hash(h) is not None, (
|
|
||||||
f"check_file_hash failed for pattern key '{pattern_key}'"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_check_android_property(self, indicator_file):
|
def test_check_android_property(self, indicator_file):
|
||||||
ind = Indicators(log=logging)
|
ind = Indicators(log=logging)
|
||||||
ind.load_indicators_files([indicator_file], load_default=False)
|
ind.load_indicators_files([indicator_file], load_default=False)
|
||||||
|
|||||||
Reference in New Issue
Block a user