PFXAnalyzer

This script is designed to analyze a PFX (Personal Information Exchange) file for various suspicious or noteworthy characteristics from a security perspective.

python · Common Scripts

Source

import demistomock as demisto  # noqa: F401
from CommonServerPython import *  # noqa: F401
from cryptography.hazmat.primitives.serialization import pkcs12
from cryptography import x509
from cryptography.x509.oid import NameOID, ExtensionOID
from cryptography.hazmat.primitives.asymmetric import rsa, ec, padding
from cryptography.exceptions import InvalidSignature
from cryptography.x509.extensions import CRLDistributionPoints, AuthorityInformationAccess
import os
from datetime import datetime


# Suspicious CN keywords
SUSPICIOUS_KEYWORDS = [
    keyword.lower()
    for keyword in [
        "Microsoft",
        "Google",
        "Apple",
        "Adobe",
        "Facebook",
        "Amazon",
        "NVIDIA",
        "Cisco",
        "Intel",
        "Oracle",
        "Symantec",
    ]
]


def get_cn(cert_name_object):
    """Extracts the Common Name (CN) from a certificate subject or issuer."""
    for attr in cert_name_object:
        if attr.oid == NameOID.COMMON_NAME:
            return attr.value
    return ""


def is_self_signed(cert):
    """Returns True if the certificate is self-signed."""
    try:
        public_key = cert.public_key()
        signature = cert.signature
        tbs_cert = cert.tbs_certificate_bytes
        hash_algo = cert.signature_hash_algorithm

        if isinstance(public_key, rsa.RSAPublicKey):
            public_key.verify(
                signature,
                tbs_cert,
                padding.PKCS1v15(),
                hash_algo,
            )
        elif isinstance(public_key, ec.EllipticCurvePublicKey):
            public_key.verify(
                signature,
                tbs_cert,
                ec.ECDSA(hash_algo),
            )
        else:
            # Unsupported key type for signature verification
            return False

        return cert.issuer == cert.subject
    except InvalidSignature:
        return False
    except Exception:
        return False


def initialize_pfx_output() -> dict:
    """Returns the default structure for PFX analysis output."""
    return {
        "Private Key Present": False,
        "Key Type": "N/A",
        "Key Size": "N/A",
        "Certificate Present": False,
        "Common Name": "N/A",
        "Issuer": "N/A",
        "Validity Start": "N/A",
        "Validity End": "N/A",
        "Validity Days": "N/A",
        "Self-Signed": False,
        "CRL URIs": [],
        "OCSP URIs": [],
        "Suspicious Keywords in CN": False,
        "Reasons": [],
        "is_pfx_suspicious": False,
    }


def parse_pfx_file(pfx_data: bytes, pfx_password: Optional[str] = None):
    """
    Attempts to load private key, certificate, and additional certs from a PFX file.
    Handles multiple fallback strategies for empty or missing passwords.
    """
    password_bytes = pfx_password.encode("utf-8") if pfx_password else None

    try:
        if password_bytes is not None:
            return pkcs12.load_key_and_certificates(pfx_data, password=password_bytes)
        else:
            try:
                return pkcs12.load_key_and_certificates(pfx_data, password=None)
            except ValueError:
                try:
                    return pkcs12.load_key_and_certificates(pfx_data, password=b"")
                except ValueError:
                    raise ValueError("Password required or unknown password.")
    except Exception as e:
        raise ValueError(f"Unable to parse .pfx file: {str(e)}")


def analyze_private_key(private_key, pfx_output: dict, reasons: list):
    """Analyzes the private key and updates output with key type, size, and weaknesses."""
    pfx_output["Private Key Present"] = True
    reasons.append("Private key is present")

    if isinstance(private_key, rsa.RSAPrivateKey):
        pfx_output["Key Type"] = "RSA"
        pfx_output["Key Size"] = private_key.key_size
        if private_key.key_size < 2048:
            reasons.append(f"Weak RSA key size ({private_key.key_size} bits)")
    elif isinstance(private_key, ec.EllipticCurvePrivateKey):
        pfx_output["Key Type"] = "ECC"
        pfx_output["Key Size"] = private_key.curve.name
        if private_key.curve.name in ["secp192r1", "secp224r1", "prime192v1", "prime239v1"]:
            reasons.append(f"Weak ECC curve ({private_key.curve.name})")
    else:
        pfx_output["Key Type"] = str(type(private_key))


def analyze_common_name(pfx_output: dict, reasons: list):
    """Checks the CN for suspicious keywords."""
    cn = pfx_output.get("Common Name", "")
    if cn and any(keyword in str(cn).lower() for keyword in SUSPICIOUS_KEYWORDS):
        pfx_output["Suspicious Keywords in CN"] = True
        # Removed individual reason here intentionally


def analyze_certificate_policies(cert, extension_missing_flags: dict, reasons: list):
    """Analyzes the Certificate Policies extension."""
    try:
        policy_ext = cert.extensions.get_extension_for_oid(ExtensionOID.CERTIFICATE_POLICIES)
        if not getattr(policy_ext.value, "policy_information", []):
            reasons.append("Certificate Policy extension found but no policy information (unusual)")
    except x509.ExtensionNotFound:
        extension_missing_flags["CertPolicy"] = True
    except Exception as ex:
        reasons.append(f"Error processing Certificate Policies: {ex}")


def analyze_crl_distributions(cert, pfx_output: dict, reasons: list, extension_missing_flags: dict):
    """Analyzes the CRL Distribution Points extension and extracts CRL URIs."""
    try:
        crl_ext = cert.extensions.get_extension_for_oid(ExtensionOID.CRL_DISTRIBUTION_POINTS)
        if isinstance(crl_ext.value, CRLDistributionPoints):
            crl_uris = []
            for dp in crl_ext.value:
                if dp.full_name:
                    for name in dp.full_name:
                        if hasattr(name, "value") and name.value.startswith("http"):
                            crl_uris.append(name.value)
            if crl_uris:
                pfx_output["CRL URIs"] = crl_uris
            else:
                reasons.append("CRL Distribution Points extension present but no URIs (suspicious)")
        else:
            reasons.append("Unexpected structure in CRL Distribution Points")
    except x509.ExtensionNotFound:
        extension_missing_flags["CRL"] = True
    except Exception as ex:
        reasons.append(f"Error processing CRL Distribution Points: {ex}")


def analyze_ocsp_uris(cert, pfx_output: dict, reasons: list, extension_missing_flags: dict):
    """Analyzes the AIA extension and extracts OCSP URIs."""
    try:
        aia_ext = cert.extensions.get_extension_for_oid(ExtensionOID.AUTHORITY_INFORMATION_ACCESS)
        if isinstance(aia_ext.value, AuthorityInformationAccess):
            ocsp_uris = [
                desc.access_location.value
                for desc in aia_ext.value
                if desc.access_method == x509.AuthorityInformationAccessOID.OCSP and desc.access_location.value.startswith("http")
            ]
            if ocsp_uris:
                pfx_output["OCSP URIs"] = ocsp_uris
            else:
                reasons.append("OCSP URI not found in Authority Information Access (suspicious for public CAs)")
        else:
            reasons.append("Unexpected structure in Authority Information Access extension")
    except x509.ExtensionNotFound:
        extension_missing_flags["AIA"] = True
    except Exception as ex:
        reasons.append(f"Error processing OCSP URIs: {ex}")


def check_strong_suspicion(pfx_output: dict, reasons: list):
    """
    Adds a strong suspicion reason if the certificate is self-signed AND
    contains suspicious keywords in the Common Name.
    Otherwise, adds individual reasons.
    """
    if pfx_output.get("Self-Signed") and pfx_output.get("Suspicious Keywords in CN"):
        reasons.append("High-risk indicator: self-signed certificate with suspicious Common Name keywords")
        pfx_output["is_pfx_suspicious"] = True
    else:
        # Add individual reasons if combined not met
        if pfx_output.get("Self-Signed"):
            reasons.append("Certificate is self-signed")
        if pfx_output.get("Suspicious Keywords in CN"):
            reasons.append(f"Suspicious keyword in Common Name: '{pfx_output.get('Common Name', '')}'")


def analyze_certificate(cert, pfx_output: dict, reasons: list):
    """Analyzes the certificate for various fields and extensions."""
    now = datetime.utcnow()

    extension_missing_flags = {
        "CRL": False,
        "AIA": False,
        "CertPolicy": False,
    }

    pfx_output["Certificate Present"] = True
    pfx_output["Common Name"] = get_cn(cert.subject)
    pfx_output["Issuer"] = get_cn(cert.issuer)
    pfx_output["Validity Start"] = cert.not_valid_before.isoformat() + "Z"
    pfx_output["Validity End"] = cert.not_valid_after.isoformat() + "Z"

    self_signed = is_self_signed(cert)
    pfx_output["Self-Signed"] = self_signed

    if cert.not_valid_after < now:
        reasons.append("Certificate is expired")
    if cert.not_valid_before > now:
        reasons.append("Certificate not valid yet (starts in the future)")

    try:
        validity_days = (cert.not_valid_after - cert.not_valid_before).days
        pfx_output["Validity Days"] = validity_days
        if validity_days > 731:
            reasons.append(f"Certificate has an unusually long validity period ({validity_days} days)")
        if validity_days < 0:
            reasons.append("Invalid validity period calculation (negative days)")
    except Exception:
        reasons.append("Could not calculate certificate validity duration")

    analyze_common_name(pfx_output, reasons)
    analyze_certificate_policies(cert, extension_missing_flags, reasons)
    analyze_crl_distributions(cert, pfx_output, reasons, extension_missing_flags)
    analyze_ocsp_uris(cert, pfx_output, reasons, extension_missing_flags)

    if all(extension_missing_flags.values()):
        reasons.append("Certificate is missing standard extensions: CRL, AIA, and Certificate Policies (suspicious)")

    # Check combined strong suspicion and adjust reasons accordingly
    check_strong_suspicion(pfx_output, reasons)


def analyze_pfx_file(pfx_data: bytes, pfx_password: Optional[str] = None) -> dict:
    """
    Analyzes a .pfx file for indicators of suspicious or weak certificate/key characteristics.

    High-Level Steps:
    1. Parse the PFX file and extract private key and certificate.
    2. Analyze private key: type and strength.
    3. Analyze certificate: validity, extensions, issuer, CN, etc.
    4. Flag suspicious indicators based on known heuristics.
    """
    reasons: list[str] = []
    pfx_output = initialize_pfx_output()

    private_key, cert, _ = parse_pfx_file(pfx_data, pfx_password)

    if private_key:
        analyze_private_key(private_key, pfx_output, reasons)

    if cert:
        analyze_certificate(cert, pfx_output, reasons)

    pfx_output["Reasons"] = reasons
    # Only set is_pfx_suspicious here if not already set by strong suspicion
    if "is_pfx_suspicious" not in pfx_output or not pfx_output["is_pfx_suspicious"]:
        pfx_output["is_pfx_suspicious"] = len(reasons) > 2

    return pfx_output


def main():
    try:
        args = demisto.args()
        file_entry_id = args.get("fileEntryId")
        pfx_password = args.get("pfxPassword")

        if not file_entry_id:
            raise ValueError("fileEntryId argument is missing.")

        file_path_info = demisto.getFilePath(file_entry_id)
        file_path = file_path_info.get("path")

        if not file_path or not os.path.exists(file_path):
            raise FileNotFoundError(f"File not found at: {file_path}")

        with open(file_path, "rb") as f:
            pfx_bytes = f.read()

        analysis_results = analyze_pfx_file(pfx_bytes, pfx_password)

        list_fields = ["Reasons", "CRL URIs", "OCSP URIs"]
        flat_fields = {k: v for k, v in analysis_results.items() if k not in list_fields}

        readable_output = tableToMarkdown("PFX Analysis Summary", [flat_fields], removeNull=True)

        for field in list_fields:
            values = analysis_results.get(field)
            if values:
                readable_output += f"\n### {field}\n" + "\n".join(f"- {v}" for v in values)

        return_results(
            CommandResults(
                readable_output=readable_output,
                outputs=analysis_results,
                outputs_prefix="PFXAnalysis",
            )
        )

    except Exception as e:
        return_error(f"PFX Analysis failed: {str(e)}")


if __name__ in ("__main__", "__builtin__", "builtins"):
    main()

README

This Python script is designed to analyze a PFX (Personal Information Exchange) file for various suspicious or noteworthy characteristics from a security perspective.

Script Data


Name Description
Script Type python3
Cortex XSOAR Version 6.10.0

Inputs


Argument Name Description
fileEntryId The ID of the file entry from the incident context that contains the PFX file.
pfxPassword Password for the PFX file (if encrypted).

Outputs


Path Description Type
PFXAnalysis.Private_Key_Present True if a private key was found in the PFX. boolean
PFXAnalysis.Key_Type Type of the private key (e.g., RSA, ECC). string
PFXAnalysis.Key_Size Size of the private key in bits (for RSA) or curve name (for ECC). number
PFXAnalysis.Certificate_Present True if a certificate was found in the PFX. boolean
PFXAnalysis.Common_Name Common Name from the certificate’s subject. string
PFXAnalysis.Issuer Common Name of the certificate’s issuer. string
PFXAnalysis.Validity_Start Certificate validity start date/time (UTC). date
PFXAnalysis.Validity_End Certificate validity end date/time (UTC). date
PFXAnalysis.Validity_Days Total number of days the certificate is valid for. number
PFXAnalysis.Self_Signed True if the certificate is self-signed. boolean
PFXAnalysis.Trusted_Issuer True if the certificate’s issuer is in the predefined trusted list. boolean
PFXAnalysis.CRL_URIs List of CRL Distribution Point URIs. string
PFXAnalysis.OCSP_URIs List of OCSP Access Method URIs. string
PFXAnalysis.Suspicious_Keywords_in_CN True if suspicious keywords were found in the Common Name. boolean
PFXAnalysis.Reasons A list of all identified suspicious reasons. string
PFXAnalysis.Is_Suspicious Overall boolean indicator if the PFX is considered suspicious. boolean