From 5793e694d07b9d4ff552a191479f1d76886dd787 Mon Sep 17 00:00:00 2001 From: Philippe Damoune Date: Fri, 7 Nov 2025 09:28:35 +0100 Subject: [PATCH] [client] Implement the X509V3ExtensionsType fields on X509Certificate --- .../create_observable_x509_certificate.py | 37 +++++++++ pycti/entities/opencti_stix_core_object.py | 32 ++++++++ .../entities/opencti_stix_cyber_observable.py | 80 +++++++++++++++++++ ...pencti_stix_cyber_observable_properties.py | 32 ++++++++ .../entities/test_observables.py | 32 ++++++++ tests/data/certificate.json | 34 ++++++++ 6 files changed, 247 insertions(+) create mode 100644 examples/create_observable_x509_certificate.py create mode 100644 tests/data/certificate.json diff --git a/examples/create_observable_x509_certificate.py b/examples/create_observable_x509_certificate.py new file mode 100644 index 000000000..c39904b04 --- /dev/null +++ b/examples/create_observable_x509_certificate.py @@ -0,0 +1,37 @@ +# coding: utf-8 +import os + +from pycti import OpenCTIApiClient + +# Variables +api_url = os.getenv("OPENCTI_API_URL", "http://opencti:4000") +api_token = os.getenv("OPENCTI_API_TOKEN", "bfa014e0-e02e-4aa6-a42b-603b19dcf159") + +# OpenCTI initialization +opencti_api_client = OpenCTIApiClient(api_url, api_token) + +observable_certificate = opencti_api_client.stix_cyber_observable.create( + observableData={ + "type": "x509-certificate", + "hashes": { + "SHA-1": "3ba7e9f806eb30d2f4e3f905e53f07e9acf08e1e", + "SHA-256": "73b8ed5becf1ba6493d2e2215a42dfdc7877e91e311ff5e59fb43d094871e699", + "MD5": "956f4b8a30ec423d4bbec9ec60df71df", + }, + "serial_number": "3311565258528077731295218946714536456", + "signature_algorithm": "SHA256-RSA", + "issuer": "C=US, O=DigiCert Inc, CN=DigiCert Global G2 TLS RSA SHA256 2020 CA1", + "validity_not_before": "2025-01-02T00:00:00Z", + "validity_not_after": "2026-01-21T23:59:59Z", + "subject": "C=US, ST=California, L=San Francisco, O=Cloudflare\\, Inc., CN=cloudflare-dns.com", + "subject_public_key_algorithm": "ECDSA", + "authority_key_identifier": "748580c066c7df37decfbd2937aa031dbeedcd17", + "basic_constraints": '{"is_ca":null,"max_path_len":null}', + "certificate_policies": "[CertificatePolicy(cps=['http://www.digicert.com/CPS'], id='2.23.140.1.2.2', user_notice=Unset())]", + "crl_distribution_points": "['http://crl3.digicert.com/DigiCertGlobalG2TLSRSASHA2562020CA1-1.crl', 'http://crl4.digicert.com/DigiCertGlobalG2TLSRSASHA2562020CA1-1.crl']", + "extended_key_usage": '{"client_auth":true,"server_auth":true}', + "key_usage": '{"certificate_sign":null,"content_commitment":null,"crl_sign":null,"data_encipherment":null,"decipher_only":null,"digital_signature":true,"encipher_only":null,"key_agreement":true,"key_encipherment":null,"value":17}', + } +) + +print(observable_certificate) diff --git a/pycti/entities/opencti_stix_core_object.py b/pycti/entities/opencti_stix_core_object.py index f0a92e225..7ea1a37f0 100644 --- a/pycti/entities/opencti_stix_core_object.py +++ b/pycti/entities/opencti_stix_core_object.py @@ -579,6 +579,22 @@ def __init__(self, opencti, file): algorithm hash } + basic_constraints + name_constraints + policy_constraints + key_usage + extended_key_usage + subject_key_identifier + authority_key_identifier + subject_alternative_name + issuer_alternative_name + subject_directory_attributes + crl_distribution_points + inhibit_any_policy + private_key_usage_period_not_before + private_key_usage_period_not_after + certificate_policies + policy_mappings } ... on IPv4Addr { value @@ -1290,6 +1306,22 @@ def __init__(self, opencti, file): algorithm hash } + basic_constraints + name_constraints + policy_constraints + key_usage + extended_key_usage + subject_key_identifier + authority_key_identifier + subject_alternative_name + issuer_alternative_name + subject_directory_attributes + crl_distribution_points + inhibit_any_policy + private_key_usage_period_not_before + private_key_usage_period_not_after + certificate_policies + policy_mappings } ... on IPv4Addr { value diff --git a/pycti/entities/opencti_stix_cyber_observable.py b/pycti/entities/opencti_stix_cyber_observable.py index 3c2f47c06..e8f852a81 100644 --- a/pycti/entities/opencti_stix_cyber_observable.py +++ b/pycti/entities/opencti_stix_cyber_observable.py @@ -716,6 +716,86 @@ def create(self, **kwargs): if "subject_public_key_exponent" in observable_data else None ), + "basic_constraints": ( + observable_data["basic_constraints"] + if "basic_constraints" in observable_data + else None + ), + "name_constraints": ( + observable_data["name_constraints"] + if "name_constraints" in observable_data + else None + ), + "policy_constraints": ( + observable_data["policy_constraints"] + if "policy_constraints" in observable_data + else None + ), + "key_usage": ( + observable_data["key_usage"] + if "key_usage" in observable_data + else None + ), + "extended_key_usage": ( + observable_data["extended_key_usage"] + if "extended_key_usage" in observable_data + else None + ), + "subject_key_identifier": ( + observable_data["subject_key_identifier"] + if "subject_key_identifier" in observable_data + else None + ), + "authority_key_identifier": ( + observable_data["authority_key_identifier"] + if "authority_key_identifier" in observable_data + else None + ), + "subject_alternative_name": ( + observable_data["subject_alternative_name"] + if "subject_alternative_name" in observable_data + else None + ), + "issuer_alternative_name": ( + observable_data["issuer_alternative_name"] + if "issuer_alternative_name" in observable_data + else None + ), + "subject_directory_attributes": ( + observable_data["subject_directory_attributes"] + if "subject_directory_attributes" in observable_data + else None + ), + "crl_distribution_points": ( + observable_data["crl_distribution_points"] + if "crl_distribution_points" in observable_data + else None + ), + "inhibit_any_policy": ( + observable_data["inhibit_any_policy"] + if "inhibit_any_policy" in observable_data + else None + ), + "private_key_usage_period_not_before": ( + observable_data["private_key_usage_period_not_before"] + if "private_key_usage_period_not_before" in observable_data + else None + ), + "private_key_usage_period_not_after": ( + observable_data["private_key_usage_period_not_after"] + if "private_key_usage_period_not_after" in observable_data + else None + ), + "certificate_policies": ( + observable_data["certificate_policies"] + if "certificate_policies" in observable_data + else None + ), + "policy_mappings": ( + observable_data["policy_mappings"] + if "policy_mappings" in observable_data + else None + ), } elif type == "SSH-Key" or type.lower() == "ssh-key": input_variables["SSHKey"] = { diff --git a/pycti/entities/stix_cyber_observable/opencti_stix_cyber_observable_properties.py b/pycti/entities/stix_cyber_observable/opencti_stix_cyber_observable_properties.py index 371f0f6f1..72569b638 100644 --- a/pycti/entities/stix_cyber_observable/opencti_stix_cyber_observable_properties.py +++ b/pycti/entities/stix_cyber_observable/opencti_stix_cyber_observable_properties.py @@ -173,6 +173,22 @@ algorithm hash } + basic_constraints + name_constraints + policy_constraints + key_usage + extended_key_usage + subject_key_identifier + authority_key_identifier + subject_alternative_name + issuer_alternative_name + subject_directory_attributes + crl_distribution_points + inhibit_any_policy + private_key_usage_period_not_before + private_key_usage_period_not_after + certificate_policies + policy_mappings } ... on SSHKey { key_type @@ -488,6 +504,22 @@ algorithm hash } + basic_constraints + name_constraints + policy_constraints + key_usage + extended_key_usage + subject_key_identifier + authority_key_identifier + subject_alternative_name + issuer_alternative_name + subject_directory_attributes + crl_distribution_points + inhibit_any_policy + private_key_usage_period_not_before + private_key_usage_period_not_after + certificate_policies + policy_mappings } ... on SSHKey { key_type diff --git a/tests/02-integration/entities/test_observables.py b/tests/02-integration/entities/test_observables.py index ea8e083ee..a5579303a 100644 --- a/tests/02-integration/entities/test_observables.py +++ b/tests/02-integration/entities/test_observables.py @@ -1,4 +1,6 @@ # coding: utf-8 +import datetime +import json def test_promote_observable_to_indicator_deprecated(api_client): @@ -11,3 +13,33 @@ def test_promote_observable_to_indicator_deprecated(api_client): ) assert observable is not None, "Returned observable is NoneType" assert observable.get("id") == obs1.get("id") + + +def test_certificate_creation_mapping(api_client): + with open("tests/data/certificate.json", "r") as content_file: + content = json.loads(content_file.read()) + + result = api_client.stix_cyber_observable.create(observableData=content) + assert result is not None + + certificate = api_client.stix_cyber_observable.read(id=result["id"]) + + for key in content: + if key == "type": + assert certificate["entity_type"] == "X509-Certificate" + elif key == "hashes": + assert { + item["algorithm"]: item["hash"] for item in certificate["hashes"] + } == content["hashes"] + elif key in [ + "validity_not_before", + "validity_not_after", + "private_key_usage_period_not_before", + "private_key_usage_period_not_after", + ]: + assert datetime.datetime.fromisoformat( + certificate[key].replace("Z", "+00:00") + ) == datetime.datetime.fromisoformat(content[key].replace("Z", "+00:00")) + + else: + assert certificate[key] == content[key] diff --git a/tests/data/certificate.json b/tests/data/certificate.json new file mode 100644 index 000000000..44e462727 --- /dev/null +++ b/tests/data/certificate.json @@ -0,0 +1,34 @@ +{ + "type": "x509-certificate", + "is_self_signed": false, + "hashes": { + "SHA-1": "3ba7e9f806eb30d2f4e3f905e53f07e9acf08e1e", + "SHA-256": "73b8ed5becf1ba6493d2e2215a42dfdc7877e91e311ff5e59fb43d094871e699", + "MD5": "956f4b8a30ec423d4bbec9ec60df71df" + }, + "serial_number": "3311565258528077731295218946714536456", + "signature_algorithm": "SHA256-RSA", + "issuer": "C=US, O=DigiCert Inc, CN=DigiCert Global G2 TLS RSA SHA256 2020 CA1", + "validity_not_before": "2025-01-02T00:00:00Z", + "validity_not_after": "2026-01-21T23:59:59Z", + "subject": "C=US, ST=California, L=San Francisco, O=Cloudflare\\, Inc., CN=cloudflare-dns.com", + "subject_public_key_algorithm": "ECDSA", + "subject_public_key_modulus": "04b0fc3e2f6d8c5e8f8e8c3d6c7a4e5f6b7c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f80", + "subject_public_key_exponent": 65537, + "authority_key_identifier": "748580c066c7df37decfbd2937aa031dbeedcd17", + "basic_constraints": "{\"is_ca\":null,\"max_path_len\":null}", + "name_constraints": "{\"excluded_subtrees\":null,\"permitted_subtrees\":null}", + "policy_constraints": "{\"require_explicit_policy\":null,\"inhibit_policy_mapping\":null}", + "subject_key_identifier": "d4c8e1f3b5a67c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f80", + "subject_alternative_name": "{\"dns_names\":[\"cloudflare-dns.com\",\"www.cloudflare-dns.com\"],\"email_addresses\":null,\"ip_addresses\":null,\"uris\":null}", + "issuer_alternative_name": "Unset()", + "subject_directory_attributes": "Unset()", + "inhibit_any_policy": "Unset()", + "private_key_usage_period_not_before": "2025-01-02T00:00:00Z", + "private_key_usage_period_not_after": "2026-01-21T23:59:59Z", + "certificate_policies": "[CertificatePolicy(cps=['http://www.digicert.com/CPS'], id='2.23.140.1.2.2', user_notice=Unset())]", + "policy_mappings": "Unset()", + "crl_distribution_points": "['http://crl3.digicert.com/DigiCertGlobalG2TLSRSASHA2562020CA1-1.crl', 'http://crl4.digicert.com/DigiCertGlobalG2TLSRSASHA2562020CA1-1.crl']", + "extended_key_usage": "{\"client_auth\":true,\"server_auth\":true}", + "key_usage": "{\"certificate_sign\":null,\"content_commitment\":null,\"crl_sign\":null,\"data_encipherment\":null,\"decipher_only\":null,\"digital_signature\":true,\"encipher_only\":null,\"key_agreement\":true,\"key_encipherment\":null,\"value\":17}" +}