Sysdiagnose Analysis Framework (SAF)

repository·main·Indexed 42 days ago

https://github.com/ec-digit-csirc/sysdiagnose

A tool for parsing and analyzing Apple sysdiagnose diagnostic archives from iOS devices. SAF converts raw diagnostic data into structured JSON/JSONL formats for forensic and data analytics, featuring a two-tier architecture of parsers and analysers. It includes support for YARA scanning, Timesketch timeline generation, and integration with ELK and Splunk environments.

Tokens
28.8K
Snippets
71
Records
90
Agent score
54%

What's inside sysdiagnose

  1. Overview of the Sysdiagnose Analysis Framework (SAF)

    main
    The Sysdiagnose Analysis Framework (SAF) is designed for incident response and forensic analysis of Apple devices (iOS, macOS, iPadOS, watchOS, tvOS). Instead of relying on intrusive methods like jailbreaking—which can compromise the integrity of the device state—SAF leverages the native sysdiagnose process. This process collects system behavior data, including plist configuration files, command logs, and SQLite databases, providing a high-fidelity view of system activity for investigators.
  2. Extracting information and building timelines from sysdiagnose

    main

    SAF provides Python scripts to extract timestamped information from a sysdiagnose archive. The framework is designed to standardize this extracted data to facilitate timeline analysis in various tools.

    Key integration capabilities include:

    • Splunk: Standardized output formats for easy import into Splunk for SIEM-based forensic analysis.
    • Timesketch: A dedicated Timesketch module is available to import the generated timelines directly into the Timesketch platform.
    • Generic Timeline Tools: The parsed format is designed to be reusable for other timeline analysis tools.
  3. Analyze Application Usage from knowledgeC.db

    main

    The knowledge_app_usage module extracts application usage patterns from the knowledgeC.db database on iOS and macOS platforms. It identifies which applications were in use, their duration, and associated metadata.

    Supported Platforms and Versions

    • Platforms: IOS, MACOS
    • Versions: 12, 13, 10.14, 10.15, 10.16, 14

    Extracted Data Fields

    When running the knowledge_app_usage query, the following information is retrieved:

    • START: The timestamp when application usage began.
    • END: The timestamp when application usage ended.
    • BUNDLE ID: The unique identifier for the application.
    • USAGE IN SECONDS: Total duration of usage in seconds.
    • USAGE IN MINUTES: Total duration of usage in minutes.
    • DEVICE ID (HARDWARE UUID): The hardware UUID of the device.
    • NAME: Metadata name associated with the event.
    • VALUE: Metadata value associated with the event.
    • DAY OF WEEK: The human-readable day (e.g., Monday, Tuesday).
    • GMT OFFSET: The GMT offset in hours.
    • ENTRY CREATION: The timestamp when the database entry was created.
    • UUID: The unique identifier for the object.
    • ZMETADATAHASH: The metadata hash from ZSTRUCTUREDMETADATA.
    • ZOBJECT TABLE ID: The primary key (Z_PK) from the ZOBJECT table.
    SELECT
       DATETIME(ZOBJECT.ZSTARTDATE+978307200,'UNIXEPOCH') AS "START", 
       DATETIME(ZOBJECT.ZENDDATE+978307200,'UNIXEPOCH') AS "END",
       ZOBJECT.ZVALUESTRING AS "BUNDLE ID", 
       (ZOBJECT.ZENDDATE - ZOBJECT.ZSTARTDATE) AS "USAGE IN SECONDS",
       (ZOBJECT.ZENDDATE - ZOBJECT.ZSTARTDATE)/60.00 AS "USAGE IN MINUTES",  
       ZSOURCE.ZDEVICEID AS "DEVICE ID (HARDWARE UUID)", 
       ZCUSTOMMETADATA.ZNAME AS "NAME",
       ZCUSTOMMETADATA.ZDOUBLEVALUE AS "VALUE",
       CASE ZOBJECT.ZSTARTDAYOFWEEK 
          WHEN "1" THEN "Sunday"
          WHEN "2" THEN "Monday"
          WHEN "3" THEN "Tuesday"
          WHEN "4" THEN "Wednesday"
          WHEN "5" THEN "Thursday"
          WHEN "6" THEN "Friday"
          WHEN "7" THEN "Saturday"
       END "DAY OF WEEK",
       ZOBJECT.ZSECONDSFROMGMT/3600 AS "GMT OFFSET",
       DATETIME(ZOBJECT.ZCREATIONDATE+978307200,'UNIXEPOCH') AS "ENTRY CREATION", 
       ZOBJECT.ZUUID AS "UUID",
       ZSTRUCTUREDMETADATA.ZMETADATAHASH,
       ZOBJECT.Z_PK AS "ZOBJECT TABLE ID" 
    FROM ZOBJECT
          LEFT JOIN ZSTRUCTUREDMETADATA ON ZOBJECT.ZSTRUCTUREDMETADATA = ZSTRUCTUREDMETADATA.Z_PK 
          LEFT JOIN ZSOURCE ON ZOBJECT.ZSOURCE = ZSOURCE.Z_PK 
          LEFT JOIN Z_4EVENT ON ZOBJECT.Z_PK = Z_4EVENT.Z_11EVENT
          LEFT JOIN ZCUSTOMMETADATA ON Z_4EVENT.Z_4CUSTOMMETADATA = ZCUSTOMMETADATA.Z_PK
    WHERE ZSTREAMNAME = "/app/usage"
  4. Understand ResultSummary metadata

    main

    The framework automatically generates a ResultSummary for every execution, persisted as <module_name>.summary.json alongside the output file. This summary is managed by ResultSummaryExecutionHandler and includes:

    • status: ok, warning, error, or skipped (derived from log handler counts and compatibility).
    • start_time: The timestamp when execution began.
    • duration: The time taken for execution (not set for skipped status).
    • num_events: Total number of entries produced.
    • num_errors: Count of logger.error() calls during execution.
    • num_warnings: Count of logger.warning() calls during execution.

    Use Cases:

    1. Trust Signal: Consumers (CLI/API) can check the summary to assess data quality (e.g., error counts) when loading cached results.
    2. Staleness Detection: The start_time allows the CLI to detect if cached data is outdated, especially when analysers depend on previously parsed data.
  5. Configure Yarascan for threat detection

    main

    The yarascan analyser helps identify suspicious patterns using YARA rules. It looks for .yar files in one of two locations:

    1. A ./yara folder in the current directory.
    2. The path specified by the SYSDIAGNOSE_YARA_RULES_PATH environment variable.
  6. Analyze Notification Usage from knowledgeC.db

    main

    The knowledge_notification_usage module is an APOLLO module designed to extract notification usage patterns from the knowledgeC.db database on iOS. It identifies when notifications were interacted with, the associated Bundle ID, the notification type, and the device hardware UUID.

    Supported Platforms and Versions

    • Platform: iOS
    • Supported Versions: 12, 13, 10.15, 10.16, 14

    Extracted Data Fields

    When running this query, the following metadata is retrieved:

    • START / END: Timestamps of the notification usage activity.
    • BUNDLE ID: The identifier for the application that sent the notification.
    • NOTIFICATION TYPE: The specific type of notification interaction recorded.
    • DEVICE ID (HARDWARE UUID): The hardware UUID of the device.
    • ID: The notification identifier.
    • DAY OF WEEK: The human-readable day (e.g., Monday, Tuesday).
    • GMT OFFSET: The offset from GMT in hours.
    • ENTRY CREATION: When the entry was created in the database.
    • UUID: The unique identifier for the object.
    • ZMETADATAHASH: The metadata hash.
    • ZOBJECT TABLE ID: The primary key from the ZOBJECT table.
    -- Example of the logic used to extract notification usage
    SELECT
       DATETIME(ZOBJECT.ZSTARTDATE+978307200,'UNIXEPOCH') AS "START", 
       DATETIME(ZOBJECT.ZENDDATE+978307200,'UNIXEPOCH') AS "END",
       ZSTRUCTUREDMETADATA.Z_DKNOTIFICATIONUSAGEMETADATAKEY__BUNDLEID AS "BUNDLE ID",
       ZOBJECT.ZVALUESTRING AS "NOTIFICATION TYPE", 
       ZSOURCE.ZDEVICEID AS "DEVICE ID (HARDWARE UUID)",  
       ZSTRUCTUREDMETADATA.Z_DKNOTIFICATIONUSAGEMETADATAKEY__IDENTIFIER AS "ID", 
       -- ... (day of week logic) ...
    FROM ZOBJECT
         LEFT JOIN ZSTRUCTUREDMETADATA ON ZOBJECT.ZSTRUCTUREDMETADATA = ZSTRUCTUREDMETADATA.Z_PK 
         LEFT JOIN ZSOURCE ON ZOBJECT.ZSOURCE = ZSOURCE.Z_PK 
    WHERE ZSTREAMNAME = "/notification/usage"
  7. How parsers and analysers work together in SAF

    main

    The Sysdiagnose Analysis Framework (SAF) uses a two-tier architecture to process sysdiagnose files:

    1. Parsers (BaseParserInterface): Responsible for reading raw sysdiagnose files and converting them into structured data.
    2. Analysers (BaseAnalyserInterface): Responsible for consuming the structured data produced by parsers to generate high-level insights.

    The framework manages the heavy lifting like execution orchestration, file I/O, caching, iOS version compatibility checks, and result summary tracking. Developers only need to implement the core logic: execute() and, for parsers, get_log_files().

    BaseInterface (ABC)
    ├── BaseParserInterface    → Parses raw sysdiagnose files into structured data
    └── BaseAnalyserInterface  → Processes parsed data to produce insights
  8. Implement a new Parser

    main

    To create a parser, inherit from BaseParserInterface. You must implement:

    • get_log_files(): Returns a list of absolute paths to the files this parser needs to process. Use self.case_data_subfolder to locate files within the sysdiagnose directory.
    • execute(): The core logic. It should return data matching the specified format.

    Key Rules for Parsers:

    • Guard against missing files: If get_log_files() returns nothing, log a warning and return an empty list/dict. Do not raise exceptions for missing files.
    • Return types:
      • format = "json" $\rightarrow$ return dict or list.
      • format = "jsonl" $\rightarrow$ return list[dict] or a Generator[dict] (for large datasets).
      • Custom formats (e.g., "csv") $\rightarrow$ return str.
    • Logging: Use logger.warning() or logger.error() for diagnostics. Avoid print(), as it is forbidden and will be caught by linting rules.
    import os
    from sysdiagnose.utils.base import BaseParserInterface, Event, SysdiagnoseConfig, logger
    
    
    class MyParser(BaseParserInterface):
        description = "Parsing my_file.txt"
        format = "jsonl"  # "json" for structured data, "jsonl" for event-based timelines
    
        def __init__(self, config: SysdiagnoseConfig, case: dict):
            super().__init__(__file__, config, case)
    
        def get_log_files(self) -> list:
            log_files_globs = ["my_file.txt"]
            return [
                os.path.join(self.case_data_subfolder, f)
                for f in log_files_globs
                if os.path.exists(os.path.join(self.case_data_subfolder, f))
            ]
    
        def execute(self) -> list:
            log_files = self.get_log_files()
            if not log_files:
                logger.warning("No my_file.txt file present")
                return []
    
            result = []
            for log_file in log_files:
                # Parse the file and build Event objects
                ...
            return result
  9. Implement a new Analyser

    main

    To create an analyser, inherit from BaseAnalyserInterface. Analysers consume data from parsers.

    Implementation Steps:

    1. Instantiate Parsers: Use ParserClass(self.config, self.case) to pass through the case metadata.
    2. Retrieve Data: Call parser.get_result(). This method automatically handles caching, so it won't re-parse the file if it has already been processed.
    3. Execute Logic: Implement the execute() method to process the data and return the result in the format specified by self.format.

    Note: Analysers do not implement get_log_files(). Instead, they should guard against empty input data returned by the upstream parsers.

    from sysdiagnose.parsers.my_parser import MyParser
    from sysdiagnose.utils.base import BaseAnalyserInterface, SysdiagnoseConfig, logger
    
    
    class MyAnalyser(BaseAnalyserInterface):
        description = "Analyse something useful"
        format = "json"
    
        def __init__(self, config: SysdiagnoseConfig, case: dict):
            super().__init__(__file__, config, case)
    
        def execute(self) -> dict:
            parser = MyParser(self.config, self.case)
            data = parser.get_result()  # triggers parsing if not cached
            # Process data...
            return {"analysis": "result"}
  10. Manage sysdiagnose cases with sysdiag

    main

    SAF uses a 'case' system to organize processed sysdiagnose archives.

    Create a new case

    Use sysdiag create <path_to_tar_gz> to process a sysdiagnose file. Use the -c flag to specify a custom case ID (e.g., a UUID).

    List existing cases

    Use sysdiag cases to view a table of all processed cases, including acquisition dates, serial numbers, and iOS versions.

    Configure case storage path

    By default, cases are stored in the current directory. You can change this by setting the SYSDIAGNOSE_CASES_PATH environment variable.

    export SYSDIAGNOSE_CASES_PATH='/path/to/folder'
    sysdiag cases
    sysdiag create test-data/iOS12/sysdiagnose_2019.02.13_15-50-14+0100_iPhone_OS_iPhone_16C101.tar.gz