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 |