From 40b79d0531578c63cd6f2630d1f37cf896734ce4 Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Mon, 12 Oct 2026 13:37:38 +0000 Subject: [PATCH 1/2] new: [modat] Add Magnify infrastructure enrichment module --- .../expansion/modat-attribute-coverage.csv | 195 +++ .../expansion/modat-dns-field-mapping.csv | 24 + .../expansion/modat-service-field-mapping.csv | 131 ++ documentation/expansion/modat.json | 273 ++++ .../modat/modat-dns-record/definition.json | 219 +++ .../data/modat/modat-service/definition.json | 1226 +++++++++++++++++ misp_modules/data/modat/service_mapping.json | 131 ++ misp_modules/modules/expansion/modat.py | 640 +++++++++ tests/test_files/modat_openapi.json | 1 + tests/test_modat.py | 415 ++++++ 10 files changed, 3255 insertions(+) create mode 100644 documentation/expansion/modat-attribute-coverage.csv create mode 100644 documentation/expansion/modat-dns-field-mapping.csv create mode 100644 documentation/expansion/modat-service-field-mapping.csv create mode 100644 documentation/expansion/modat.json create mode 100644 misp_modules/data/modat/modat-dns-record/definition.json create mode 100644 misp_modules/data/modat/modat-service/definition.json create mode 100644 misp_modules/data/modat/service_mapping.json create mode 100644 misp_modules/modules/expansion/modat.py create mode 100644 tests/test_files/modat_openapi.json create mode 100644 tests/test_modat.py diff --git a/documentation/expansion/modat-attribute-coverage.csv b/documentation/expansion/modat-attribute-coverage.csv new file mode 100644 index 000000000..991ccf393 --- /dev/null +++ b/documentation/expansion/modat-attribute-coverage.csv @@ -0,0 +1,195 @@ +MISP attribute type,Input support,MODAT query or reason,Notes +AS,supported,asn.number, +aba-rtn,unsupported,No corresponding documented infrastructure search field in this API., +anonymised,unsupported,No corresponding documented infrastructure search field in this API., +attachment,unsupported,No corresponding documented infrastructure search field in this API., +authentihash,unsupported,No corresponding documented infrastructure search field in this API., +azure-application-id,unsupported,No corresponding documented infrastructure search field in this API., +bank-account-nr,unsupported,No corresponding documented infrastructure search field in this API., +bic,unsupported,No corresponding documented infrastructure search field in this API., +bin,unsupported,No corresponding documented infrastructure search field in this API., +boolean,unsupported,No corresponding documented infrastructure search field in this API., +bro,unsupported,No corresponding documented infrastructure search field in this API., +btc,unsupported,No corresponding documented infrastructure search field in this API., +campaign-id,unsupported,No corresponding documented infrastructure search field in this API., +campaign-name,unsupported,No corresponding documented infrastructure search field in this API., +cc-number,unsupported,No corresponding documented infrastructure search field in this API., +cdhash,unsupported,No corresponding documented infrastructure search field in this API., +chrome-extension-id,unsupported,No corresponding documented infrastructure search field in this API., +comment,unsupported,No corresponding documented infrastructure search field in this API., +community-id,unsupported,No corresponding documented infrastructure search field in this API., +cookie,unsupported,No corresponding documented infrastructure search field in this API., +cortex,unsupported,No corresponding documented infrastructure search field in this API., +counter,unsupported,No corresponding documented infrastructure search field in this API., +country-of-residence,unsupported,No corresponding documented infrastructure search field in this API., +cpe,unsupported,No corresponding documented infrastructure search field in this API., +dash,unsupported,No corresponding documented infrastructure search field in this API., +date-of-birth,unsupported,No corresponding documented infrastructure search field in this API., +datetime,unsupported,No corresponding documented infrastructure search field in this API., +dkim,unsupported,No corresponding documented infrastructure search field in this API., +dkim-signature,unsupported,No corresponding documented infrastructure search field in this API., +dns-soa-email,unsupported,No corresponding documented infrastructure search field in this API., +dom-hash,unsupported,No corresponding documented infrastructure search field in this API., +domain,supported,fqdns, +domain|ip,supported,fqdns AND ip, +edge-extension-id,unsupported,No corresponding documented infrastructure search field in this API., +email,supported,tls.extensions.subject_alt_name.email OR tls.extensions.issuer_alt_name.email,TLS alternative-name email pivot; no mail traffic or WHOIS lookup. +email-attachment,unsupported,No corresponding documented infrastructure search field in this API., +email-body,unsupported,No corresponding documented infrastructure search field in this API., +email-dst,supported,tls.extensions.subject_alt_name.email OR tls.extensions.issuer_alt_name.email,TLS alternative-name email pivot; no mail traffic or WHOIS lookup. +email-dst-display-name,unsupported,No corresponding documented infrastructure search field in this API., +email-header,unsupported,No corresponding documented infrastructure search field in this API., +email-message-id,unsupported,No corresponding documented infrastructure search field in this API., +email-mime-boundary,unsupported,No corresponding documented infrastructure search field in this API., +email-reply-to,unsupported,No corresponding documented infrastructure search field in this API., +email-src,supported,tls.extensions.subject_alt_name.email OR tls.extensions.issuer_alt_name.email,TLS alternative-name email pivot; no mail traffic or WHOIS lookup. +email-src-display-name,unsupported,No corresponding documented infrastructure search field in this API., +email-subject,unsupported,No corresponding documented infrastructure search field in this API., +email-thread-index,unsupported,No corresponding documented infrastructure search field in this API., +email-x-mailer,unsupported,No corresponding documented infrastructure search field in this API., +eppn,unsupported,No corresponding documented infrastructure search field in this API., +favicon-mmh3,supported,http.files.mmh3, +filename,unsupported,No corresponding documented infrastructure search field in this API., +filename-pattern,unsupported,No corresponding documented infrastructure search field in this API., +filename|authentihash,unsupported,No corresponding documented infrastructure search field in this API., +filename|impfuzzy,unsupported,No corresponding documented infrastructure search field in this API., +filename|imphash,unsupported,No corresponding documented infrastructure search field in this API., +filename|md5,unsupported,"No documented MD5 web-file, banner or body hash search.", +filename|pehash,unsupported,No corresponding documented infrastructure search field in this API., +filename|sha1,supported,banner_sha1 OR http.body_sha1 OR http.files.sha1,Hash pivot only; filename is retained in input but not used as a search constraint. +filename|sha224,unsupported,No corresponding documented infrastructure search field in this API., +filename|sha256,supported,banner_sha256 OR http.body_sha256 OR http.files.sha256 OR http.headers_sha256,Hash pivot only; filename is retained in input but not used as a search constraint. +filename|sha3-224,unsupported,No corresponding documented infrastructure search field in this API., +filename|sha3-256,unsupported,No corresponding documented infrastructure search field in this API., +filename|sha3-384,unsupported,No corresponding documented infrastructure search field in this API., +filename|sha3-512,unsupported,No corresponding documented infrastructure search field in this API., +filename|sha384,unsupported,No corresponding documented infrastructure search field in this API., +filename|sha512,unsupported,No corresponding documented infrastructure search field in this API., +filename|sha512/224,unsupported,No corresponding documented infrastructure search field in this API., +filename|sha512/256,unsupported,No corresponding documented infrastructure search field in this API., +filename|ssdeep,unsupported,No corresponding documented infrastructure search field in this API., +filename|tlsh,unsupported,No corresponding documented infrastructure search field in this API., +filename|vhash,unsupported,No corresponding documented infrastructure search field in this API., +first-name,unsupported,No corresponding documented infrastructure search field in this API., +float,unsupported,No corresponding documented infrastructure search field in this API., +frequent-flyer-number,unsupported,No corresponding documented infrastructure search field in this API., +full-name,unsupported,No corresponding documented infrastructure search field in this API., +gender,unsupported,No corresponding documented infrastructure search field in this API., +gene,unsupported,No corresponding documented infrastructure search field in this API., +git-commit-id,unsupported,No corresponding documented infrastructure search field in this API., +github-organisation,unsupported,No corresponding documented infrastructure search field in this API., +github-repository,unsupported,No corresponding documented infrastructure search field in this API., +github-username,unsupported,No corresponding documented infrastructure search field in this API., +hassh-md5,unsupported,Client fingerprint; MODAT exposes server observations. Mapping would change its meaning., +hasshserver-md5,supported,ssh.hassh,Server observation HASSH; client hassh-md5 remains unsupported. +hex,supported,banner_hex, +hostname,supported,fqdns OR rdns, +hostname|port,supported,fqdns AND port, +http-method,unsupported,No corresponding documented infrastructure search field in this API., +iban,unsupported,No corresponding documented infrastructure search field in this API., +identity-card-number,unsupported,No corresponding documented infrastructure search field in this API., +impfuzzy,unsupported,No corresponding documented infrastructure search field in this API., +imphash,unsupported,No corresponding documented infrastructure search field in this API., +integer,unsupported,No corresponding documented infrastructure search field in this API., +ip-dst,supported,GET /host/{ip}/v1; CIDR: subnet, +ip-dst|port,supported,ip AND port, +ip-src,supported,GET /host/{ip}/v1; CIDR: subnet, +ip-src|port,supported,ip AND port, +issue-date-of-the-visa,unsupported,No corresponding documented infrastructure search field in this API., +ja3-fingerprint-md5,unsupported,Client fingerprint; MODAT exposes server observations. Mapping would change its meaning., +jabber-id,unsupported,No corresponding documented infrastructure search field in this API., +jarm-fingerprint,supported,tls.jarm, +kusto-query,unsupported,No corresponding documented infrastructure search field in this API., +last-name,unsupported,No corresponding documented infrastructure search field in this API., +link,supported,fqdns or ip AND port,HTTP(S) infrastructure pivot; path and query are not matched. +mac-address,unsupported,No corresponding documented infrastructure search field in this API., +mac-eui-64,unsupported,No corresponding documented infrastructure search field in this API., +malware-sample,unsupported,No corresponding documented infrastructure search field in this API., +malware-type,unsupported,No corresponding documented infrastructure search field in this API., +md5,unsupported,"No documented MD5 web-file, banner or body hash search.", +middle-name,unsupported,No corresponding documented infrastructure search field in this API., +mime-type,unsupported,No corresponding documented infrastructure search field in this API., +mobile-application-id,unsupported,No corresponding documented infrastructure search field in this API., +mutex,unsupported,No corresponding documented infrastructure search field in this API., +named pipe,unsupported,No corresponding documented infrastructure search field in this API., +nationality,unsupported,No corresponding documented infrastructure search field in this API., +onion-address,unsupported,No corresponding documented infrastructure search field in this API., +other,unsupported,No corresponding documented infrastructure search field in this API., +passenger-name-record-locator-number,unsupported,No corresponding documented infrastructure search field in this API., +passport-country,unsupported,No corresponding documented infrastructure search field in this API., +passport-expiration,unsupported,No corresponding documented infrastructure search field in this API., +passport-number,unsupported,No corresponding documented infrastructure search field in this API., +pattern-in-file,unsupported,No corresponding documented infrastructure search field in this API., +pattern-in-memory,unsupported,No corresponding documented infrastructure search field in this API., +pattern-in-traffic,supported,banner substring, +payment-details,unsupported,No corresponding documented infrastructure search field in this API., +pdb,unsupported,No corresponding documented infrastructure search field in this API., +pehash,unsupported,No corresponding documented infrastructure search field in this API., +pgp-private-key,unsupported,No corresponding documented infrastructure search field in this API., +pgp-public-key,unsupported,No corresponding documented infrastructure search field in this API., +phone-number,unsupported,No corresponding documented infrastructure search field in this API., +place-of-birth,unsupported,No corresponding documented infrastructure search field in this API., +place-port-of-clearance,unsupported,No corresponding documented infrastructure search field in this API., +place-port-of-onward-foreign-destination,unsupported,No corresponding documented infrastructure search field in this API., +place-port-of-original-embarkation,unsupported,No corresponding documented infrastructure search field in this API., +port,supported,port, +primary-residence,unsupported,No corresponding documented infrastructure search field in this API., +process-state,unsupported,No corresponding documented infrastructure search field in this API., +prtn,unsupported,No corresponding documented infrastructure search field in this API., +redress-number,unsupported,No corresponding documented infrastructure search field in this API., +regkey,unsupported,No corresponding documented infrastructure search field in this API., +regkey|value,unsupported,No corresponding documented infrastructure search field in this API., +sha1,supported,banner_sha1 OR http.body_sha1 OR http.files.sha1,Observed web-file/body/banner hashes; not a malware hash database. +sha224,unsupported,No corresponding documented infrastructure search field in this API., +sha256,supported,banner_sha256 OR http.body_sha256 OR http.files.sha256 OR http.headers_sha256,Observed web-file/body/banner hashes; not a malware hash database. +sha3-224,unsupported,No corresponding documented infrastructure search field in this API., +sha3-256,unsupported,No corresponding documented infrastructure search field in this API., +sha3-384,unsupported,No corresponding documented infrastructure search field in this API., +sha3-512,unsupported,No corresponding documented infrastructure search field in this API., +sha384,unsupported,No corresponding documented infrastructure search field in this API., +sha512,unsupported,No corresponding documented infrastructure search field in this API., +sha512/224,unsupported,No corresponding documented infrastructure search field in this API., +sha512/256,unsupported,No corresponding documented infrastructure search field in this API., +sigma,unsupported,No corresponding documented infrastructure search field in this API., +size-in-bytes,unsupported,No corresponding documented infrastructure search field in this API., +snort,unsupported,No corresponding documented infrastructure search field in this API., +special-service-request,unsupported,No corresponding documented infrastructure search field in this API., +ssdeep,unsupported,No corresponding documented infrastructure search field in this API., +ssh-fingerprint,supported,ssh.key_exchange.server_host_key.fingerprint_sha256,64 hex characters required; OpenSSH SHA256:base64 notation is rejected. +stix2-pattern,unsupported,No corresponding documented infrastructure search field in this API., +suricata,unsupported,No corresponding documented infrastructure search field in this API., +target-email,supported,tls.extensions.subject_alt_name.email OR tls.extensions.issuer_alt_name.email,TLS alternative-name email pivot; no mail traffic or WHOIS lookup. +target-external,unsupported,No corresponding documented infrastructure search field in this API., +target-location,unsupported,No corresponding documented infrastructure search field in this API., +target-machine,unsupported,No corresponding documented infrastructure search field in this API., +target-org,supported,asn.org OR tls.subject.organization OR tls.issuer.organization, +target-user,unsupported,No corresponding documented infrastructure search field in this API., +telfhash,unsupported,No corresponding documented infrastructure search field in this API., +text,supported,quoted free-text phrase, +threat-actor,unsupported,No corresponding documented infrastructure search field in this API., +tlsh,unsupported,No corresponding documented infrastructure search field in this API., +travel-details,unsupported,No corresponding documented infrastructure search field in this API., +twitter-id,unsupported,No corresponding documented infrastructure search field in this API., +uri,supported,fqdns or ip AND port,HTTP(S) infrastructure pivot; path and query are not matched. +url,supported,fqdns or ip AND port,HTTP(S) infrastructure pivot; path and query are not matched. +user-agent,unsupported,No corresponding documented infrastructure search field in this API., +uuid,unsupported,No corresponding documented infrastructure search field in this API., +vhash,unsupported,No corresponding documented infrastructure search field in this API., +visa-number,unsupported,No corresponding documented infrastructure search field in this API., +vulnerability,supported,cves.id,"CVE IDs only; exposure is inferred, not proof of exploitation." +weakness,unsupported,No corresponding documented infrastructure search field in this API., +whois-creation-date,unsupported,WHOIS endpoints are absent from the supplied OpenAPI specification., +whois-registrant-email,unsupported,WHOIS endpoints are absent from the supplied OpenAPI specification., +whois-registrant-name,unsupported,WHOIS endpoints are absent from the supplied OpenAPI specification., +whois-registrant-org,unsupported,WHOIS endpoints are absent from the supplied OpenAPI specification., +whois-registrant-phone,unsupported,WHOIS endpoints are absent from the supplied OpenAPI specification., +whois-registrar,unsupported,WHOIS endpoints are absent from the supplied OpenAPI specification., +windows-scheduled-task,unsupported,No corresponding documented infrastructure search field in this API., +windows-service-displayname,unsupported,No corresponding documented infrastructure search field in this API., +windows-service-name,unsupported,No corresponding documented infrastructure search field in this API., +x509-fingerprint-md5,unsupported,"MODAT certificate fingerprints expose SHA1 and SHA256, not MD5.", +x509-fingerprint-sha1,supported,tls.fingerprint_sha1, +x509-fingerprint-sha256,supported,tls.fingerprint_sha256, +xmr,unsupported,No corresponding documented infrastructure search field in this API., +yara,unsupported,No corresponding documented infrastructure search field in this API., +zeek,unsupported,No corresponding documented infrastructure search field in this API., diff --git a/documentation/expansion/modat-dns-field-mapping.csv b/documentation/expansion/modat-dns-field-mapping.csv new file mode 100644 index 000000000..5903f59c6 --- /dev/null +++ b/documentation/expansion/modat-dns-field-mapping.csv @@ -0,0 +1,24 @@ +MODAT field,MISP object,Object relation,MISP attribute type +fqdn,modat-dns-record,fqdn,domain +rrtype,modat-dns-record,rrtype,text +ttl,modat-dns-record,ttl,integer +created_at,modat-dns-record,created-at,datetime +deleted_at,modat-dns-record,deleted-at,datetime +ip,modat-dns-record,ip,ip-dst +cname,modat-dns-record,cname,domain +host,modat-dns-record,host,hostname +ns,modat-dns-record,ns,hostname +priority,modat-dns-record,priority,integer +target,modat-dns-record,target,text +params,modat-dns-record,params,text +mname,modat-dns-record,mname,hostname +rname,modat-dns-record,rname,text +serial,modat-dns-record,serial,integer +refresh,modat-dns-record,refresh,integer +retry,modat-dns-record,retry,integer +expire,modat-dns-record,expire,integer +minimum,modat-dns-record,minimum,integer +weight,modat-dns-record,weight,integer +port,modat-dns-record,port,port +value,modat-dns-record,value,text +raw-json,modat-dns-record,raw-json,attachment diff --git a/documentation/expansion/modat-service-field-mapping.csv b/documentation/expansion/modat-service-field-mapping.csv new file mode 100644 index 000000000..3ada95fb5 --- /dev/null +++ b/documentation/expansion/modat-service-field-mapping.csv @@ -0,0 +1,131 @@ +MODAT field,MISP object,Object relation,MISP attribute type +transport,modat-service,transport,text +ports,modat-service,ports,port +last_scanned_port,modat-service,last-scanned-port,port +protocol,modat-service,protocol,text +scanned_at,modat-service,scanned-at,datetime +discovered_at,modat-service,discovered-at,datetime +is_match,modat-service,is-match,boolean +banner,modat-service,banner,text +banner_hex,modat-service,banner-hex,text +banner_sha1,modat-service,banner-sha1,sha1 +banner_sha256,modat-service,banner-sha256,sha256 +banner_mmh3,modat-service,banner-mmh3,integer +ssh.hassh,modat-service,ssh-hassh,hasshserver-md5 +ssh.server_id,modat-service,ssh-server-id,text +ssh.server_key_exchange.cookie,modat-service,ssh-server-key-exchange-cookie,text +ssh.server_key_exchange.kex_algorithms,modat-service,ssh-server-key-exchange-kex-algorithms,text +ssh.server_key_exchange.host_key_algorithms,modat-service,ssh-server-key-exchange-host-key-algorithms,text +ssh.server_key_exchange.client_to_server_ciphers,modat-service,ssh-server-key-exchange-client-to-server-ciphers,text +ssh.server_key_exchange.server_to_client_ciphers,modat-service,ssh-server-key-exchange-server-to-client-ciphers,text +ssh.server_key_exchange.client_to_server_macs,modat-service,ssh-server-key-exchange-client-to-server-macs,text +ssh.server_key_exchange.server_to_client_macs,modat-service,ssh-server-key-exchange-server-to-client-macs,text +ssh.server_key_exchange.client_to_server_compression,modat-service,ssh-server-key-exchange-client-to-server-compression,text +ssh.server_key_exchange.server_to_client_compression,modat-service,ssh-server-key-exchange-server-to-client-compression,text +ssh.server_key_exchange.first_kex_follows,modat-service,ssh-server-key-exchange-first-kex-follows,boolean +ssh.server_key_exchange.reserved,modat-service,ssh-server-key-exchange-reserved,integer +ssh.algorithm_selection.dh_kex_algorithm,modat-service,ssh-algorithm-selection-dh-kex-algorithm,text +ssh.algorithm_selection.host_key_algorithm,modat-service,ssh-algorithm-selection-host-key-algorithm,text +ssh.algorithm_selection.client_to_server_alg_group.cipher,modat-service,ssh-algorithm-selection-client-to-server-alg-group-cipher,text +ssh.algorithm_selection.client_to_server_alg_group.mac,modat-service,ssh-algorithm-selection-client-to-server-alg-group-mac,text +ssh.algorithm_selection.client_to_server_alg_group.compression,modat-service,ssh-algorithm-selection-client-to-server-alg-group-compression,text +ssh.algorithm_selection.server_to_client_alg_group.cipher,modat-service,ssh-algorithm-selection-server-to-client-alg-group-cipher,text +ssh.algorithm_selection.server_to_client_alg_group.mac,modat-service,ssh-algorithm-selection-server-to-client-alg-group-mac,text +ssh.algorithm_selection.server_to_client_alg_group.compression,modat-service,ssh-algorithm-selection-server-to-client-alg-group-compression,text +ssh.key_exchange.curve25519_sha256_params.server_public,modat-service,ssh-key-exchange-curve25519-sha256-params-server-public,text +ssh.key_exchange.server_signature.parsed.algorithm,modat-service,ssh-key-exchange-server-signature-parsed-algorithm,text +ssh.key_exchange.server_signature.parsed.value,modat-service,ssh-key-exchange-server-signature-parsed-value,text +ssh.key_exchange.server_signature.raw,modat-service,ssh-key-exchange-server-signature-raw,text +ssh.key_exchange.server_signature.h,modat-service,ssh-key-exchange-server-signature-h,text +ssh.key_exchange.server_host_key.rsa_public_key.exponent,modat-service,ssh-key-exchange-server-host-key-rsa-public-key-exponent,integer +ssh.key_exchange.server_host_key.rsa_public_key.modulus,modat-service,ssh-key-exchange-server-host-key-rsa-public-key-modulus,text +ssh.key_exchange.server_host_key.rsa_public_key.length,modat-service,ssh-key-exchange-server-host-key-rsa-public-key-length,integer +ssh.key_exchange.server_host_key.ecdsa_public_key.b,modat-service,ssh-key-exchange-server-host-key-ecdsa-public-key-b,text +ssh.key_exchange.server_host_key.ecdsa_public_key.curve,modat-service,ssh-key-exchange-server-host-key-ecdsa-public-key-curve,text +ssh.key_exchange.server_host_key.ecdsa_public_key.gx,modat-service,ssh-key-exchange-server-host-key-ecdsa-public-key-gx,text +ssh.key_exchange.server_host_key.ecdsa_public_key.gy,modat-service,ssh-key-exchange-server-host-key-ecdsa-public-key-gy,text +ssh.key_exchange.server_host_key.ecdsa_public_key.length,modat-service,ssh-key-exchange-server-host-key-ecdsa-public-key-length,integer +ssh.key_exchange.server_host_key.ecdsa_public_key.n,modat-service,ssh-key-exchange-server-host-key-ecdsa-public-key-n,text +ssh.key_exchange.server_host_key.ecdsa_public_key.p,modat-service,ssh-key-exchange-server-host-key-ecdsa-public-key-p,text +ssh.key_exchange.server_host_key.ecdsa_public_key.x,modat-service,ssh-key-exchange-server-host-key-ecdsa-public-key-x,text +ssh.key_exchange.server_host_key.ecdsa_public_key.y,modat-service,ssh-key-exchange-server-host-key-ecdsa-public-key-y,text +ssh.key_exchange.server_host_key.raw,modat-service,ssh-key-exchange-server-host-key-raw,text +ssh.key_exchange.server_host_key.algorithm,modat-service,ssh-key-exchange-server-host-key-algorithm,text +ssh.key_exchange.server_host_key.fingerprint_sha256,modat-service,ssh-key-exchange-server-host-key-fingerprint-sha256,ssh-fingerprint +tcp.window_size,modat-service,tcp-window-size,integer +tls.is_self_signed,modat-service,tls-is-self-signed,boolean +tls.is_trusted,modat-service,tls-is-trusted,boolean +tls.valid_from,modat-service,tls-valid-from,datetime +tls.expires_at,modat-service,tls-expires-at,datetime +tls.validity_seconds,modat-service,tls-validity-seconds,integer +tls.supported_versions,modat-service,tls-supported-versions,text +tls.fingerprint_sha256,modat-service,tls-fingerprint-sha256,x509-fingerprint-sha256 +tls.fingerprint_sha1,modat-service,tls-fingerprint-sha1,x509-fingerprint-sha1 +tls.jarm,modat-service,tls-jarm,jarm-fingerprint +tls.ja3s,modat-service,tls-ja3s,text +tls.ja4s,modat-service,tls-ja4s,text +tls.ja4x,modat-service,tls-ja4x,text +tls.ja4scan_tls,modat-service,tls-ja4scan-tls,text +tls.serial_number,modat-service,tls-serial-number,text +tls.issuer.common_name,modat-service,tls-issuer-common-name,text +tls.issuer.country,modat-service,tls-issuer-country,text +tls.issuer.organization,modat-service,tls-issuer-organization,text +tls.issuer.organizational_unit,modat-service,tls-issuer-organizational-unit,text +tls.issuer.province,modat-service,tls-issuer-province,text +tls.issuer.locality,modat-service,tls-issuer-locality,text +tls.issuer.email_address,modat-service,tls-issuer-email-address,email +tls.subject.common_name,modat-service,tls-subject-common-name,text +tls.subject.country,modat-service,tls-subject-country,text +tls.subject.organization,modat-service,tls-subject-organization,text +tls.subject.organizational_unit,modat-service,tls-subject-organizational-unit,text +tls.subject.province,modat-service,tls-subject-province,text +tls.subject.locality,modat-service,tls-subject-locality,text +tls.subject.email_address,modat-service,tls-subject-email-address,email +tls.extensions.subject_alt_name.dns,modat-service,tls-extensions-subject-alt-name-dns,domain +tls.extensions.subject_alt_name.ipv4,modat-service,tls-extensions-subject-alt-name-ipv4,ip-dst +tls.extensions.subject_alt_name.ipv6,modat-service,tls-extensions-subject-alt-name-ipv6,ip-dst +tls.extensions.subject_alt_name.email,modat-service,tls-extensions-subject-alt-name-email,email +tls.extensions.subject_alt_name.uri,modat-service,tls-extensions-subject-alt-name-uri,uri +tls.extensions.subject_alt_name.directory,modat-service,tls-extensions-subject-alt-name-directory,text +tls.extensions.subject_alt_name.other_names.id,modat-service,tls-extensions-subject-alt-name-other-names-id,text +tls.extensions.subject_alt_name.other_names.value,modat-service,tls-extensions-subject-alt-name-other-names-value,text +tls.extensions.issuer_alt_name.dns,modat-service,tls-extensions-issuer-alt-name-dns,domain +tls.extensions.issuer_alt_name.ipv4,modat-service,tls-extensions-issuer-alt-name-ipv4,ip-dst +tls.extensions.issuer_alt_name.ipv6,modat-service,tls-extensions-issuer-alt-name-ipv6,ip-dst +tls.extensions.issuer_alt_name.email,modat-service,tls-extensions-issuer-alt-name-email,email +tls.extensions.issuer_alt_name.uri,modat-service,tls-extensions-issuer-alt-name-uri,uri +tls.extensions.issuer_alt_name.directory,modat-service,tls-extensions-issuer-alt-name-directory,text +tls.extensions.issuer_alt_name.other_names.id,modat-service,tls-extensions-issuer-alt-name-other-names-id,text +tls.extensions.issuer_alt_name.other_names.value,modat-service,tls-extensions-issuer-alt-name-other-names-value,text +tls.raw,modat-service,tls-raw,text +tls.is_valid,modat-service,tls-is-valid,boolean +fingerprints.os.product,modat-service,fingerprints-os-product,text +fingerprints.os.version,modat-service,fingerprints-os-version,text +fingerprints.os.arch,modat-service,fingerprints-os-arch,text +fingerprints.os.icon_url,modat-service,fingerprints-os-icon-url,link +fingerprints.service.product,modat-service,fingerprints-service-product,text +fingerprints.service.version,modat-service,fingerprints-service-version,text +fingerprints.service.icon_url,modat-service,fingerprints-service-icon-url,link +fingerprints.technologies.name,modat-service,fingerprints-technologies-name,text +fingerprints.technologies.version,modat-service,fingerprints-technologies-version,text +fingerprints.technologies.icon_url,modat-service,fingerprints-technologies-icon-url,link +fingerprints.tags,modat-service,fingerprints-tags,text +http.title,modat-service,http-title,text +http.status_code,modat-service,http-status-code,integer +http.headers,modat-service,http-headers,text +http.headers_sha256,modat-service,http-headers-sha256,sha256 +http.body,modat-service,http-body,text +http.body_mmh3,modat-service,http-body-mmh3,integer +http.body_sha1,modat-service,http-body-sha1,sha1 +http.body_sha256,modat-service,http-body-sha256,sha256 +http.css_mmh3,modat-service,http-css-mmh3,integer +http.js_mmh3,modat-service,http-js-mmh3,integer +http.files.path,modat-service,http-files-path,text +http.files.category,modat-service,http-files-category,text +http.files.sha256,modat-service,http-files-sha256,sha256 +http.files.sha1,modat-service,http-files-sha1,sha1 +http.files.mmh3,modat-service,http-files-mmh3,favicon-mmh3 +cves.id,modat-service,cves-id,text +cves.cvss,modat-service,cves-cvss,float +cves.is_kev,modat-service,cves-is-kev,boolean +port (service search),modat-service,ports,port diff --git a/documentation/expansion/modat.json b/documentation/expansion/modat.json new file mode 100644 index 000000000..a557da113 --- /dev/null +++ b/documentation/expansion/modat.json @@ -0,0 +1,273 @@ +{ + "name": "MODAT Magnify", + "module": "modat", + "module-type": [ + "expansion" + ], + "description": "Enrich internet infrastructure using MODAT Magnify host, service and DNS observations.", + "requirements": [ + "requests", + "PyMISP", + "MODAT Magnify API key", + "Install the bundled modat-service and modat-dns-record object templates in MISP" + ], + "references": [ + "https://api.magnify.modat.io/docs", + "https://magnify.modat.io/docs" + ], + "api_contract": { + "openapi_version": "1.65.2", + "source": "https://api.magnify.modat.io/openapi.json", + "retrieved": "2026-10-12" + }, + "features": [ + "32 meaningful MISP input types; reject unsupported types explicitly.", + "Map all 129 leaf fields of the detailed-service schema, including deprecated fields when present; normalize singular search port into ports.", + "Retain all nested service and host data as original JSON attachments, including unknown fields.", + "Emit existing ip-port, asn, geolocation, x509, software, file, vulnerability, domain-ip and dns-record objects.", + "Bundle new modat-service and modat-dns-record templates; validate them against the MISP object schema.", + "Keep DNS TTL, created_at and deleted_at distinct. Never treat deleted_at as last_seen.", + "Preserve array nesting in JSON; repeat scalar leaves in the typed observation. Web files also get individual file objects.", + "Link observations to input and related infrastructure; deduplicate shared objects and set discovered attributes to_ids=false.", + "Bound search pagination; report rate limits and authentication errors without exposing response bodies or credentials." + ], + "input": { + "format": "misp_standard", + "required_attribute_fields": [ + "type", + "value", + "uuid" + ], + "types": [ + { + "type": "AS", + "query": "asn.number", + "notes": "" + }, + { + "type": "domain", + "query": "fqdns", + "notes": "" + }, + { + "type": "domain|ip", + "query": "fqdns AND ip", + "notes": "" + }, + { + "type": "email", + "query": "tls.extensions.subject_alt_name.email OR tls.extensions.issuer_alt_name.email", + "notes": "TLS alternative-name email pivot; no mail traffic or WHOIS lookup." + }, + { + "type": "email-dst", + "query": "tls.extensions.subject_alt_name.email OR tls.extensions.issuer_alt_name.email", + "notes": "TLS alternative-name email pivot; no mail traffic or WHOIS lookup." + }, + { + "type": "email-src", + "query": "tls.extensions.subject_alt_name.email OR tls.extensions.issuer_alt_name.email", + "notes": "TLS alternative-name email pivot; no mail traffic or WHOIS lookup." + }, + { + "type": "favicon-mmh3", + "query": "http.files.mmh3", + "notes": "" + }, + { + "type": "filename|sha1", + "query": "banner_sha1 OR http.body_sha1 OR http.files.sha1", + "notes": "Hash pivot only; filename is retained in input but not used as a search constraint." + }, + { + "type": "filename|sha256", + "query": "banner_sha256 OR http.body_sha256 OR http.files.sha256 OR http.headers_sha256", + "notes": "Hash pivot only; filename is retained in input but not used as a search constraint." + }, + { + "type": "hasshserver-md5", + "query": "ssh.hassh", + "notes": "Server observation HASSH; client hassh-md5 remains unsupported." + }, + { + "type": "hex", + "query": "banner_hex", + "notes": "" + }, + { + "type": "hostname", + "query": "fqdns OR rdns", + "notes": "" + }, + { + "type": "hostname|port", + "query": "fqdns AND port", + "notes": "" + }, + { + "type": "ip-dst", + "query": "GET /host/{ip}/v1; CIDR: subnet", + "notes": "" + }, + { + "type": "ip-dst|port", + "query": "ip AND port", + "notes": "" + }, + { + "type": "ip-src", + "query": "GET /host/{ip}/v1; CIDR: subnet", + "notes": "" + }, + { + "type": "ip-src|port", + "query": "ip AND port", + "notes": "" + }, + { + "type": "jarm-fingerprint", + "query": "tls.jarm", + "notes": "" + }, + { + "type": "link", + "query": "fqdns or ip AND port", + "notes": "HTTP(S) infrastructure pivot; path and query are not matched." + }, + { + "type": "pattern-in-traffic", + "query": "banner substring", + "notes": "" + }, + { + "type": "port", + "query": "port", + "notes": "" + }, + { + "type": "sha1", + "query": "banner_sha1 OR http.body_sha1 OR http.files.sha1", + "notes": "Observed web-file/body/banner hashes; not a malware hash database." + }, + { + "type": "sha256", + "query": "banner_sha256 OR http.body_sha256 OR http.files.sha256 OR http.headers_sha256", + "notes": "Observed web-file/body/banner hashes; not a malware hash database." + }, + { + "type": "ssh-fingerprint", + "query": "ssh.key_exchange.server_host_key.fingerprint_sha256", + "notes": "64 hex characters required; OpenSSH SHA256:base64 notation is rejected." + }, + { + "type": "target-email", + "query": "tls.extensions.subject_alt_name.email OR tls.extensions.issuer_alt_name.email", + "notes": "TLS alternative-name email pivot; no mail traffic or WHOIS lookup." + }, + { + "type": "target-org", + "query": "asn.org OR tls.subject.organization OR tls.issuer.organization", + "notes": "" + }, + { + "type": "text", + "query": "quoted free-text phrase", + "notes": "" + }, + { + "type": "uri", + "query": "fqdns or ip AND port", + "notes": "HTTP(S) infrastructure pivot; path and query are not matched." + }, + { + "type": "url", + "query": "fqdns or ip AND port", + "notes": "HTTP(S) infrastructure pivot; path and query are not matched." + }, + { + "type": "vulnerability", + "query": "cves.id", + "notes": "CVE IDs only; exposure is inferred, not proof of exploitation." + }, + { + "type": "x509-fingerprint-sha1", + "query": "tls.fingerprint_sha1", + "notes": "" + }, + { + "type": "x509-fingerprint-sha256", + "query": "tls.fingerprint_sha256", + "notes": "" + } + ] + }, + "output": { + "format": "misp_standard", + "response": "results.Attribute includes original input and host JSON evidence; results.Object contains linked observations and derived infrastructure objects.", + "custom_templates": [ + "misp_modules/data/modat/modat-service/definition.json", + "misp_modules/data/modat/modat-dns-record/definition.json" + ], + "no_results": "Return original attribute and empty Object array.", + "partial_failure": "A failed API call returns an error for the enrichment; partial results are not silently returned." + }, + "config": { + "apikey": { + "required": true, + "description": "MODAT API key sent in the Authorization header; Bearer prefix is optional." + }, + "max_results": { + "default": 10, + "range": "1-1000", + "description": "Maximum service results and, separately, reverse-DNS search results; not a cap on emitted objects. Direct host lookups return all available services." + }, + "page_size": { + "default": 10, + "range": "10-100", + "description": "Search page size. Final page is sliced to max_results." + }, + "timeout": { + "default": 30, + "range": "1-120", + "description": "Per HTTP request timeout, in seconds." + }, + "include_dns": { + "default": true, + "description": "Fetch historical zone records for domain/hostname inputs and domain-bearing composites/URLs." + }, + "reverse_dns": { + "default": false, + "description": "When include_dns is true, direct IP inputs also search A/AAAA history and fetch matching zones. Additional API calls and quota apply." + } + }, + "limitations": [ + "No WHOIS endpoint exists in the pinned OpenAPI specification. Website WHOIS features are not treated as API capabilities.", + "JA3S, JA4S, JA4X and JA4Scan-TLS stay text-valued relations in modat-service; no client JA3 or JA4 type is substituted.", + "Raw certificate text is human-readable certificate output, not PEM/base64 DER; it is stored as text.", + "No CPE, malware-file hashes beyond documented web hashes, mail-message, registry, banking or personal/travel data are inferred.", + "DNS zone lookups return their full record history; max_results does not limit the number of records in a zone.", + "Quoted input values containing quotes, backslashes or control characters are rejected because DSL escape behavior is undocumented.", + "No live authenticated API call was made. Offline contract and object validation tests passed." + ], + "object_recommendations": { + "new": [ + "modat-service: service observation, protocol data, fingerprints and scan lifecycle.", + "modat-dns-record: individual historical DNS resource record, including HTTPS, SOA and SRV." + ], + "existing_updates": "None required for this implementation. Reuse current standard templates.", + "upstream_candidates": [ + "A generic DNS observation template or extension of dns-record for rrtype, TTL, record lifecycle, HTTPS priority/target/params and SOA/SRV components.", + "Dedicated server TLS fingerprint attribute types and a generic network-service observation template. These require MISP core/template coordination; local text relations preserve their current meaning." + ] + }, + "installation": [ + "Keep modat.py in misp_modules/modules/expansion/ and the entire data/modat directory in misp_modules/data/. Reinstall the package if running an installed distribution.", + "Copy modat-service/ and modat-dns-record/ from misp_modules/data/modat/ into the MISP object-template repository at app/files/misp-objects/objects/ and refresh templates using the normal administrator update process.", + "Restart misp-modules, enable modat enrichment in MISP and set apikey. Configure optional result limits, timeout and DNS behavior as needed." + ], + "validation": { + "offline_tests": 77, + "command": "python -m pytest tests/test_modat.py -q", + "live_authenticated": false + } +} diff --git a/misp_modules/data/modat/modat-dns-record/definition.json b/misp_modules/data/modat/modat-dns-record/definition.json new file mode 100644 index 000000000..91c93ecab --- /dev/null +++ b/misp_modules/data/modat/modat-dns-record/definition.json @@ -0,0 +1,219 @@ +{ + "attributes": { + "cname": { + "description": "MODAT API field: cname.", + "misp-attribute": "domain", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "created-at": { + "categories": [ + "Other" + ], + "description": "MODAT API field: created_at.", + "misp-attribute": "datetime", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "deleted-at": { + "categories": [ + "Other" + ], + "description": "MODAT API field: deleted_at.", + "misp-attribute": "datetime", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "expire": { + "categories": [ + "Other" + ], + "description": "MODAT API field: expire.", + "misp-attribute": "integer", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "fqdn": { + "description": "MODAT API field: fqdn.", + "misp-attribute": "domain", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "host": { + "description": "MODAT API field: host.", + "misp-attribute": "hostname", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ip": { + "description": "MODAT API field: ip.", + "misp-attribute": "ip-dst", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "minimum": { + "categories": [ + "Other" + ], + "description": "MODAT API field: minimum.", + "misp-attribute": "integer", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "mname": { + "description": "MODAT API field: mname.", + "misp-attribute": "hostname", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ns": { + "description": "MODAT API field: ns.", + "misp-attribute": "hostname", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "params": { + "categories": [ + "Other" + ], + "description": "MODAT API field: params.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "port": { + "description": "MODAT API field: port.", + "misp-attribute": "port", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "priority": { + "categories": [ + "Other" + ], + "description": "MODAT API field: priority.", + "misp-attribute": "integer", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "raw-json": { + "description": "MODAT API field: raw-json.", + "misp-attribute": "attachment", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "refresh": { + "categories": [ + "Other" + ], + "description": "MODAT API field: refresh.", + "misp-attribute": "integer", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "retry": { + "categories": [ + "Other" + ], + "description": "MODAT API field: retry.", + "misp-attribute": "integer", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "rname": { + "categories": [ + "Other" + ], + "description": "MODAT API field: rname.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "rrtype": { + "categories": [ + "Other" + ], + "description": "MODAT API field: rrtype.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "serial": { + "categories": [ + "Other" + ], + "description": "MODAT API field: serial.", + "misp-attribute": "integer", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "target": { + "categories": [ + "Other" + ], + "description": "MODAT API field: target.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ttl": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ttl.", + "misp-attribute": "integer", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "value": { + "categories": [ + "Other" + ], + "description": "MODAT API field: value.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "weight": { + "categories": [ + "Other" + ], + "description": "MODAT API field: weight.", + "misp-attribute": "integer", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + } + }, + "description": "A single historical MODAT DNS resource record, including lifecycle and TTL. SOA rname remains DNS notation.", + "meta-category": "network", + "name": "modat-dns-record", + "required": [ + "fqdn", + "rrtype" + ], + "uuid": "9621682b-8bed-5aee-8d46-f208c96ca825", + "version": 1 +} \ No newline at end of file diff --git a/misp_modules/data/modat/modat-service/definition.json b/misp_modules/data/modat/modat-service/definition.json new file mode 100644 index 000000000..ac80c29d2 --- /dev/null +++ b/misp_modules/data/modat/modat-service/definition.json @@ -0,0 +1,1226 @@ +{ + "attributes": { + "banner": { + "categories": [ + "Other" + ], + "description": "MODAT API field: banner.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "banner-hex": { + "categories": [ + "Other" + ], + "description": "MODAT API field: banner_hex.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "banner-mmh3": { + "categories": [ + "Other" + ], + "description": "MODAT API field: banner_mmh3.", + "misp-attribute": "integer", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "banner-sha1": { + "description": "MODAT API field: banner_sha1.", + "misp-attribute": "sha1", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "banner-sha256": { + "description": "MODAT API field: banner_sha256.", + "misp-attribute": "sha256", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "cves-cvss": { + "categories": [ + "Other" + ], + "description": "MODAT API field: cves.cvss.", + "misp-attribute": "float", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "cves-id": { + "categories": [ + "Other" + ], + "description": "MODAT API field: cves.id.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "cves-is-kev": { + "categories": [ + "Other" + ], + "description": "MODAT API field: cves.is_kev.", + "misp-attribute": "boolean", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "discovered-at": { + "categories": [ + "Other" + ], + "description": "MODAT API field: discovered_at.", + "misp-attribute": "datetime", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "fingerprints-os-arch": { + "categories": [ + "Other" + ], + "description": "MODAT API field: fingerprints.os.arch.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "fingerprints-os-icon-url": { + "description": "MODAT API field: fingerprints.os.icon_url.", + "misp-attribute": "link", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "fingerprints-os-product": { + "categories": [ + "Other" + ], + "description": "MODAT API field: fingerprints.os.product.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "fingerprints-os-version": { + "categories": [ + "Other" + ], + "description": "MODAT API field: fingerprints.os.version.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "fingerprints-service-icon-url": { + "description": "MODAT API field: fingerprints.service.icon_url.", + "misp-attribute": "link", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "fingerprints-service-product": { + "categories": [ + "Other" + ], + "description": "MODAT API field: fingerprints.service.product.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "fingerprints-service-version": { + "categories": [ + "Other" + ], + "description": "MODAT API field: fingerprints.service.version.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "fingerprints-tags": { + "categories": [ + "Other" + ], + "description": "MODAT API field: fingerprints.tags.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "fingerprints-technologies-icon-url": { + "description": "MODAT API field: fingerprints.technologies.icon_url.", + "misp-attribute": "link", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "fingerprints-technologies-name": { + "categories": [ + "Other" + ], + "description": "MODAT API field: fingerprints.technologies.name.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "fingerprints-technologies-version": { + "categories": [ + "Other" + ], + "description": "MODAT API field: fingerprints.technologies.version.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "http-body": { + "categories": [ + "Other" + ], + "description": "MODAT API field: http.body.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "http-body-mmh3": { + "categories": [ + "Other" + ], + "description": "MODAT API field: http.body_mmh3.", + "misp-attribute": "integer", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "http-body-sha1": { + "description": "MODAT API field: http.body_sha1.", + "misp-attribute": "sha1", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "http-body-sha256": { + "description": "MODAT API field: http.body_sha256.", + "misp-attribute": "sha256", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "http-css-mmh3": { + "categories": [ + "Other" + ], + "description": "MODAT API field: http.css_mmh3.", + "misp-attribute": "integer", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "http-files-category": { + "categories": [ + "Other" + ], + "description": "MODAT API field: http.files.category.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "http-files-mmh3": { + "description": "MODAT API field: http.files.mmh3.", + "misp-attribute": "favicon-mmh3", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "http-files-path": { + "categories": [ + "Other" + ], + "description": "MODAT API field: http.files.path.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "http-files-sha1": { + "description": "MODAT API field: http.files.sha1.", + "misp-attribute": "sha1", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "http-files-sha256": { + "description": "MODAT API field: http.files.sha256.", + "misp-attribute": "sha256", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "http-headers": { + "categories": [ + "Other" + ], + "description": "MODAT API field: http.headers.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "http-headers-sha256": { + "description": "MODAT API field: http.headers_sha256.", + "misp-attribute": "sha256", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "http-js-mmh3": { + "categories": [ + "Other" + ], + "description": "MODAT API field: http.js_mmh3.", + "misp-attribute": "integer", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "http-status-code": { + "categories": [ + "Other" + ], + "description": "MODAT API field: http.status_code.", + "misp-attribute": "integer", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "http-title": { + "categories": [ + "Other" + ], + "description": "MODAT API field: http.title.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ip": { + "description": "MODAT API field: ip.", + "misp-attribute": "ip-dst", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "is-match": { + "categories": [ + "Other" + ], + "description": "MODAT API field: is_match.", + "misp-attribute": "boolean", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "last-scanned-port": { + "description": "MODAT API field: last_scanned_port.", + "misp-attribute": "port", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ports": { + "description": "MODAT API field: ports.", + "misp-attribute": "port", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "protocol": { + "categories": [ + "Other" + ], + "description": "MODAT API field: protocol.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "raw-json": { + "description": "MODAT API field: raw-json.", + "misp-attribute": "attachment", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "scanned-at": { + "categories": [ + "Other" + ], + "description": "MODAT API field: scanned_at.", + "misp-attribute": "datetime", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-algorithm-selection-client-to-server-alg-group-cipher": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.algorithm_selection.client_to_server_alg_group.cipher.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-algorithm-selection-client-to-server-alg-group-compression": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.algorithm_selection.client_to_server_alg_group.compression.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-algorithm-selection-client-to-server-alg-group-mac": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.algorithm_selection.client_to_server_alg_group.mac.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-algorithm-selection-dh-kex-algorithm": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.algorithm_selection.dh_kex_algorithm.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-algorithm-selection-host-key-algorithm": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.algorithm_selection.host_key_algorithm.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-algorithm-selection-server-to-client-alg-group-cipher": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.algorithm_selection.server_to_client_alg_group.cipher.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-algorithm-selection-server-to-client-alg-group-compression": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.algorithm_selection.server_to_client_alg_group.compression.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-algorithm-selection-server-to-client-alg-group-mac": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.algorithm_selection.server_to_client_alg_group.mac.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-hassh": { + "description": "MODAT API field: ssh.hassh.", + "misp-attribute": "hasshserver-md5", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-key-exchange-curve25519-sha256-params-server-public": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.key_exchange.curve25519_sha256_params.server_public.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-key-exchange-server-host-key-algorithm": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.key_exchange.server_host_key.algorithm.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-key-exchange-server-host-key-ecdsa-public-key-b": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.key_exchange.server_host_key.ecdsa_public_key.b.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-key-exchange-server-host-key-ecdsa-public-key-curve": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.key_exchange.server_host_key.ecdsa_public_key.curve.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-key-exchange-server-host-key-ecdsa-public-key-gx": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.key_exchange.server_host_key.ecdsa_public_key.gx.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-key-exchange-server-host-key-ecdsa-public-key-gy": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.key_exchange.server_host_key.ecdsa_public_key.gy.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-key-exchange-server-host-key-ecdsa-public-key-length": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.key_exchange.server_host_key.ecdsa_public_key.length.", + "misp-attribute": "integer", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-key-exchange-server-host-key-ecdsa-public-key-n": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.key_exchange.server_host_key.ecdsa_public_key.n.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-key-exchange-server-host-key-ecdsa-public-key-p": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.key_exchange.server_host_key.ecdsa_public_key.p.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-key-exchange-server-host-key-ecdsa-public-key-x": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.key_exchange.server_host_key.ecdsa_public_key.x.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-key-exchange-server-host-key-ecdsa-public-key-y": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.key_exchange.server_host_key.ecdsa_public_key.y.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-key-exchange-server-host-key-fingerprint-sha256": { + "description": "MODAT API field: ssh.key_exchange.server_host_key.fingerprint_sha256.", + "misp-attribute": "ssh-fingerprint", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-key-exchange-server-host-key-raw": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.key_exchange.server_host_key.raw.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-key-exchange-server-host-key-rsa-public-key-exponent": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.key_exchange.server_host_key.rsa_public_key.exponent.", + "misp-attribute": "integer", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-key-exchange-server-host-key-rsa-public-key-length": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.key_exchange.server_host_key.rsa_public_key.length.", + "misp-attribute": "integer", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-key-exchange-server-host-key-rsa-public-key-modulus": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.key_exchange.server_host_key.rsa_public_key.modulus.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-key-exchange-server-signature-h": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.key_exchange.server_signature.h.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-key-exchange-server-signature-parsed-algorithm": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.key_exchange.server_signature.parsed.algorithm.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-key-exchange-server-signature-parsed-value": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.key_exchange.server_signature.parsed.value.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-key-exchange-server-signature-raw": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.key_exchange.server_signature.raw.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-server-id": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.server_id.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-server-key-exchange-client-to-server-ciphers": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.server_key_exchange.client_to_server_ciphers.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-server-key-exchange-client-to-server-compression": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.server_key_exchange.client_to_server_compression.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-server-key-exchange-client-to-server-macs": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.server_key_exchange.client_to_server_macs.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-server-key-exchange-cookie": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.server_key_exchange.cookie.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-server-key-exchange-first-kex-follows": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.server_key_exchange.first_kex_follows.", + "misp-attribute": "boolean", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-server-key-exchange-host-key-algorithms": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.server_key_exchange.host_key_algorithms.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-server-key-exchange-kex-algorithms": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.server_key_exchange.kex_algorithms.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-server-key-exchange-reserved": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.server_key_exchange.reserved.", + "misp-attribute": "integer", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-server-key-exchange-server-to-client-ciphers": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.server_key_exchange.server_to_client_ciphers.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-server-key-exchange-server-to-client-compression": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.server_key_exchange.server_to_client_compression.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "ssh-server-key-exchange-server-to-client-macs": { + "categories": [ + "Other" + ], + "description": "MODAT API field: ssh.server_key_exchange.server_to_client_macs.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tcp-window-size": { + "categories": [ + "Other" + ], + "description": "MODAT API field: tcp.window_size.", + "misp-attribute": "integer", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-expires-at": { + "categories": [ + "Other" + ], + "description": "MODAT API field: tls.expires_at.", + "misp-attribute": "datetime", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-extensions-issuer-alt-name-directory": { + "categories": [ + "Other" + ], + "description": "MODAT API field: tls.extensions.issuer_alt_name.directory.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-extensions-issuer-alt-name-dns": { + "description": "MODAT API field: tls.extensions.issuer_alt_name.dns.", + "misp-attribute": "domain", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-extensions-issuer-alt-name-email": { + "description": "MODAT API field: tls.extensions.issuer_alt_name.email.", + "misp-attribute": "email", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-extensions-issuer-alt-name-ipv4": { + "description": "MODAT API field: tls.extensions.issuer_alt_name.ipv4.", + "misp-attribute": "ip-dst", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-extensions-issuer-alt-name-ipv6": { + "description": "MODAT API field: tls.extensions.issuer_alt_name.ipv6.", + "misp-attribute": "ip-dst", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-extensions-issuer-alt-name-other-names-id": { + "categories": [ + "Other" + ], + "description": "MODAT API field: tls.extensions.issuer_alt_name.other_names.id.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-extensions-issuer-alt-name-other-names-value": { + "categories": [ + "Other" + ], + "description": "MODAT API field: tls.extensions.issuer_alt_name.other_names.value.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-extensions-issuer-alt-name-uri": { + "description": "MODAT API field: tls.extensions.issuer_alt_name.uri.", + "misp-attribute": "uri", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-extensions-subject-alt-name-directory": { + "categories": [ + "Other" + ], + "description": "MODAT API field: tls.extensions.subject_alt_name.directory.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-extensions-subject-alt-name-dns": { + "description": "MODAT API field: tls.extensions.subject_alt_name.dns.", + "misp-attribute": "domain", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-extensions-subject-alt-name-email": { + "description": "MODAT API field: tls.extensions.subject_alt_name.email.", + "misp-attribute": "email", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-extensions-subject-alt-name-ipv4": { + "description": "MODAT API field: tls.extensions.subject_alt_name.ipv4.", + "misp-attribute": "ip-dst", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-extensions-subject-alt-name-ipv6": { + "description": "MODAT API field: tls.extensions.subject_alt_name.ipv6.", + "misp-attribute": "ip-dst", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-extensions-subject-alt-name-other-names-id": { + "categories": [ + "Other" + ], + "description": "MODAT API field: tls.extensions.subject_alt_name.other_names.id.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-extensions-subject-alt-name-other-names-value": { + "categories": [ + "Other" + ], + "description": "MODAT API field: tls.extensions.subject_alt_name.other_names.value.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-extensions-subject-alt-name-uri": { + "description": "MODAT API field: tls.extensions.subject_alt_name.uri.", + "misp-attribute": "uri", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-fingerprint-sha1": { + "description": "MODAT API field: tls.fingerprint_sha1.", + "misp-attribute": "x509-fingerprint-sha1", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-fingerprint-sha256": { + "description": "MODAT API field: tls.fingerprint_sha256.", + "misp-attribute": "x509-fingerprint-sha256", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-is-self-signed": { + "categories": [ + "Other" + ], + "description": "MODAT API field: tls.is_self_signed.", + "misp-attribute": "boolean", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-is-trusted": { + "categories": [ + "Other" + ], + "description": "MODAT API field: tls.is_trusted.", + "misp-attribute": "boolean", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-is-valid": { + "categories": [ + "Other" + ], + "description": "MODAT API field: tls.is_valid.", + "misp-attribute": "boolean", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-issuer-common-name": { + "categories": [ + "Other" + ], + "description": "MODAT API field: tls.issuer.common_name.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-issuer-country": { + "categories": [ + "Other" + ], + "description": "MODAT API field: tls.issuer.country.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-issuer-email-address": { + "description": "MODAT API field: tls.issuer.email_address.", + "misp-attribute": "email", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-issuer-locality": { + "categories": [ + "Other" + ], + "description": "MODAT API field: tls.issuer.locality.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-issuer-organization": { + "categories": [ + "Other" + ], + "description": "MODAT API field: tls.issuer.organization.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-issuer-organizational-unit": { + "categories": [ + "Other" + ], + "description": "MODAT API field: tls.issuer.organizational_unit.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-issuer-province": { + "categories": [ + "Other" + ], + "description": "MODAT API field: tls.issuer.province.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-ja3s": { + "categories": [ + "Other" + ], + "description": "MODAT API field: tls.ja3s.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-ja4s": { + "categories": [ + "Other" + ], + "description": "MODAT API field: tls.ja4s.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-ja4scan-tls": { + "categories": [ + "Other" + ], + "description": "MODAT API field: tls.ja4scan_tls.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-ja4x": { + "categories": [ + "Other" + ], + "description": "MODAT API field: tls.ja4x.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-jarm": { + "description": "MODAT API field: tls.jarm.", + "misp-attribute": "jarm-fingerprint", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-raw": { + "categories": [ + "Other" + ], + "description": "MODAT API field: tls.raw.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-serial-number": { + "categories": [ + "Other" + ], + "description": "MODAT API field: tls.serial_number.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-subject-common-name": { + "categories": [ + "Other" + ], + "description": "MODAT API field: tls.subject.common_name.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-subject-country": { + "categories": [ + "Other" + ], + "description": "MODAT API field: tls.subject.country.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-subject-email-address": { + "description": "MODAT API field: tls.subject.email_address.", + "misp-attribute": "email", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-subject-locality": { + "categories": [ + "Other" + ], + "description": "MODAT API field: tls.subject.locality.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-subject-organization": { + "categories": [ + "Other" + ], + "description": "MODAT API field: tls.subject.organization.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-subject-organizational-unit": { + "categories": [ + "Other" + ], + "description": "MODAT API field: tls.subject.organizational_unit.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-subject-province": { + "categories": [ + "Other" + ], + "description": "MODAT API field: tls.subject.province.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-supported-versions": { + "categories": [ + "Other" + ], + "description": "MODAT API field: tls.supported_versions.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-valid-from": { + "categories": [ + "Other" + ], + "description": "MODAT API field: tls.valid_from.", + "misp-attribute": "datetime", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "tls-validity-seconds": { + "categories": [ + "Other" + ], + "description": "MODAT API field: tls.validity_seconds.", + "misp-attribute": "integer", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + }, + "transport": { + "categories": [ + "Other" + ], + "description": "MODAT API field: transport.", + "misp-attribute": "text", + "multiple": true, + "to_ids": false, + "ui-priority": 1 + } + }, + "description": "An observed MODAT Magnify service. Array leaf values share this observation; raw JSON retains their exact nesting.", + "meta-category": "network", + "name": "modat-service", + "required": [ + "ip" + ], + "uuid": "773a95c7-3072-5fb6-a840-9d26e7e2e990", + "version": 1 +} \ No newline at end of file diff --git a/misp_modules/data/modat/service_mapping.json b/misp_modules/data/modat/service_mapping.json new file mode 100644 index 000000000..5bf653add --- /dev/null +++ b/misp_modules/data/modat/service_mapping.json @@ -0,0 +1,131 @@ +{ + "transport": "transport", + "ports": "ports", + "last_scanned_port": "last-scanned-port", + "protocol": "protocol", + "scanned_at": "scanned-at", + "discovered_at": "discovered-at", + "is_match": "is-match", + "banner": "banner", + "banner_hex": "banner-hex", + "banner_sha1": "banner-sha1", + "banner_sha256": "banner-sha256", + "banner_mmh3": "banner-mmh3", + "ssh.hassh": "ssh-hassh", + "ssh.server_id": "ssh-server-id", + "ssh.server_key_exchange.cookie": "ssh-server-key-exchange-cookie", + "ssh.server_key_exchange.kex_algorithms": "ssh-server-key-exchange-kex-algorithms", + "ssh.server_key_exchange.host_key_algorithms": "ssh-server-key-exchange-host-key-algorithms", + "ssh.server_key_exchange.client_to_server_ciphers": "ssh-server-key-exchange-client-to-server-ciphers", + "ssh.server_key_exchange.server_to_client_ciphers": "ssh-server-key-exchange-server-to-client-ciphers", + "ssh.server_key_exchange.client_to_server_macs": "ssh-server-key-exchange-client-to-server-macs", + "ssh.server_key_exchange.server_to_client_macs": "ssh-server-key-exchange-server-to-client-macs", + "ssh.server_key_exchange.client_to_server_compression": "ssh-server-key-exchange-client-to-server-compression", + "ssh.server_key_exchange.server_to_client_compression": "ssh-server-key-exchange-server-to-client-compression", + "ssh.server_key_exchange.first_kex_follows": "ssh-server-key-exchange-first-kex-follows", + "ssh.server_key_exchange.reserved": "ssh-server-key-exchange-reserved", + "ssh.algorithm_selection.dh_kex_algorithm": "ssh-algorithm-selection-dh-kex-algorithm", + "ssh.algorithm_selection.host_key_algorithm": "ssh-algorithm-selection-host-key-algorithm", + "ssh.algorithm_selection.client_to_server_alg_group.cipher": "ssh-algorithm-selection-client-to-server-alg-group-cipher", + "ssh.algorithm_selection.client_to_server_alg_group.mac": "ssh-algorithm-selection-client-to-server-alg-group-mac", + "ssh.algorithm_selection.client_to_server_alg_group.compression": "ssh-algorithm-selection-client-to-server-alg-group-compression", + "ssh.algorithm_selection.server_to_client_alg_group.cipher": "ssh-algorithm-selection-server-to-client-alg-group-cipher", + "ssh.algorithm_selection.server_to_client_alg_group.mac": "ssh-algorithm-selection-server-to-client-alg-group-mac", + "ssh.algorithm_selection.server_to_client_alg_group.compression": "ssh-algorithm-selection-server-to-client-alg-group-compression", + "ssh.key_exchange.curve25519_sha256_params.server_public": "ssh-key-exchange-curve25519-sha256-params-server-public", + "ssh.key_exchange.server_signature.parsed.algorithm": "ssh-key-exchange-server-signature-parsed-algorithm", + "ssh.key_exchange.server_signature.parsed.value": "ssh-key-exchange-server-signature-parsed-value", + "ssh.key_exchange.server_signature.raw": "ssh-key-exchange-server-signature-raw", + "ssh.key_exchange.server_signature.h": "ssh-key-exchange-server-signature-h", + "ssh.key_exchange.server_host_key.rsa_public_key.exponent": "ssh-key-exchange-server-host-key-rsa-public-key-exponent", + "ssh.key_exchange.server_host_key.rsa_public_key.modulus": "ssh-key-exchange-server-host-key-rsa-public-key-modulus", + "ssh.key_exchange.server_host_key.rsa_public_key.length": "ssh-key-exchange-server-host-key-rsa-public-key-length", + "ssh.key_exchange.server_host_key.ecdsa_public_key.b": "ssh-key-exchange-server-host-key-ecdsa-public-key-b", + "ssh.key_exchange.server_host_key.ecdsa_public_key.curve": "ssh-key-exchange-server-host-key-ecdsa-public-key-curve", + "ssh.key_exchange.server_host_key.ecdsa_public_key.gx": "ssh-key-exchange-server-host-key-ecdsa-public-key-gx", + "ssh.key_exchange.server_host_key.ecdsa_public_key.gy": "ssh-key-exchange-server-host-key-ecdsa-public-key-gy", + "ssh.key_exchange.server_host_key.ecdsa_public_key.length": "ssh-key-exchange-server-host-key-ecdsa-public-key-length", + "ssh.key_exchange.server_host_key.ecdsa_public_key.n": "ssh-key-exchange-server-host-key-ecdsa-public-key-n", + "ssh.key_exchange.server_host_key.ecdsa_public_key.p": "ssh-key-exchange-server-host-key-ecdsa-public-key-p", + "ssh.key_exchange.server_host_key.ecdsa_public_key.x": "ssh-key-exchange-server-host-key-ecdsa-public-key-x", + "ssh.key_exchange.server_host_key.ecdsa_public_key.y": "ssh-key-exchange-server-host-key-ecdsa-public-key-y", + "ssh.key_exchange.server_host_key.raw": "ssh-key-exchange-server-host-key-raw", + "ssh.key_exchange.server_host_key.algorithm": "ssh-key-exchange-server-host-key-algorithm", + "ssh.key_exchange.server_host_key.fingerprint_sha256": "ssh-key-exchange-server-host-key-fingerprint-sha256", + "tcp.window_size": "tcp-window-size", + "tls.is_self_signed": "tls-is-self-signed", + "tls.is_trusted": "tls-is-trusted", + "tls.valid_from": "tls-valid-from", + "tls.expires_at": "tls-expires-at", + "tls.validity_seconds": "tls-validity-seconds", + "tls.supported_versions": "tls-supported-versions", + "tls.fingerprint_sha256": "tls-fingerprint-sha256", + "tls.fingerprint_sha1": "tls-fingerprint-sha1", + "tls.jarm": "tls-jarm", + "tls.ja3s": "tls-ja3s", + "tls.ja4s": "tls-ja4s", + "tls.ja4x": "tls-ja4x", + "tls.ja4scan_tls": "tls-ja4scan-tls", + "tls.serial_number": "tls-serial-number", + "tls.issuer.common_name": "tls-issuer-common-name", + "tls.issuer.country": "tls-issuer-country", + "tls.issuer.organization": "tls-issuer-organization", + "tls.issuer.organizational_unit": "tls-issuer-organizational-unit", + "tls.issuer.province": "tls-issuer-province", + "tls.issuer.locality": "tls-issuer-locality", + "tls.issuer.email_address": "tls-issuer-email-address", + "tls.subject.common_name": "tls-subject-common-name", + "tls.subject.country": "tls-subject-country", + "tls.subject.organization": "tls-subject-organization", + "tls.subject.organizational_unit": "tls-subject-organizational-unit", + "tls.subject.province": "tls-subject-province", + "tls.subject.locality": "tls-subject-locality", + "tls.subject.email_address": "tls-subject-email-address", + "tls.extensions.subject_alt_name.dns": "tls-extensions-subject-alt-name-dns", + "tls.extensions.subject_alt_name.ipv4": "tls-extensions-subject-alt-name-ipv4", + "tls.extensions.subject_alt_name.ipv6": "tls-extensions-subject-alt-name-ipv6", + "tls.extensions.subject_alt_name.email": "tls-extensions-subject-alt-name-email", + "tls.extensions.subject_alt_name.uri": "tls-extensions-subject-alt-name-uri", + "tls.extensions.subject_alt_name.directory": "tls-extensions-subject-alt-name-directory", + "tls.extensions.subject_alt_name.other_names.id": "tls-extensions-subject-alt-name-other-names-id", + "tls.extensions.subject_alt_name.other_names.value": "tls-extensions-subject-alt-name-other-names-value", + "tls.extensions.issuer_alt_name.dns": "tls-extensions-issuer-alt-name-dns", + "tls.extensions.issuer_alt_name.ipv4": "tls-extensions-issuer-alt-name-ipv4", + "tls.extensions.issuer_alt_name.ipv6": "tls-extensions-issuer-alt-name-ipv6", + "tls.extensions.issuer_alt_name.email": "tls-extensions-issuer-alt-name-email", + "tls.extensions.issuer_alt_name.uri": "tls-extensions-issuer-alt-name-uri", + "tls.extensions.issuer_alt_name.directory": "tls-extensions-issuer-alt-name-directory", + "tls.extensions.issuer_alt_name.other_names.id": "tls-extensions-issuer-alt-name-other-names-id", + "tls.extensions.issuer_alt_name.other_names.value": "tls-extensions-issuer-alt-name-other-names-value", + "tls.raw": "tls-raw", + "tls.is_valid": "tls-is-valid", + "fingerprints.os.product": "fingerprints-os-product", + "fingerprints.os.version": "fingerprints-os-version", + "fingerprints.os.arch": "fingerprints-os-arch", + "fingerprints.os.icon_url": "fingerprints-os-icon-url", + "fingerprints.service.product": "fingerprints-service-product", + "fingerprints.service.version": "fingerprints-service-version", + "fingerprints.service.icon_url": "fingerprints-service-icon-url", + "fingerprints.technologies.name": "fingerprints-technologies-name", + "fingerprints.technologies.version": "fingerprints-technologies-version", + "fingerprints.technologies.icon_url": "fingerprints-technologies-icon-url", + "fingerprints.tags": "fingerprints-tags", + "http.title": "http-title", + "http.status_code": "http-status-code", + "http.headers": "http-headers", + "http.headers_sha256": "http-headers-sha256", + "http.body": "http-body", + "http.body_mmh3": "http-body-mmh3", + "http.body_sha1": "http-body-sha1", + "http.body_sha256": "http-body-sha256", + "http.css_mmh3": "http-css-mmh3", + "http.js_mmh3": "http-js-mmh3", + "http.files.path": "http-files-path", + "http.files.category": "http-files-category", + "http.files.sha256": "http-files-sha256", + "http.files.sha1": "http-files-sha1", + "http.files.mmh3": "http-files-mmh3", + "cves.id": "cves-id", + "cves.cvss": "cves-cvss", + "cves.is_kev": "cves-is-kev" +} diff --git a/misp_modules/modules/expansion/modat.py b/misp_modules/modules/expansion/modat.py new file mode 100644 index 000000000..4ad3e4ee0 --- /dev/null +++ b/misp_modules/modules/expansion/modat.py @@ -0,0 +1,640 @@ +"""MODAT Magnify enrichment using the documented v1 API (OpenAPI 1.65.2).""" + +import base64 +import ipaddress +import json +import math +import re +from pathlib import Path +from urllib.parse import quote, urlsplit +from uuid import NAMESPACE_URL, UUID, uuid5 + +import requests +from pymisp import MISPAttribute, MISPEvent, MISPObject +from pymisp.exceptions import PyMISPError + +BASE_URL = "https://api.magnify.modat.io" +DATA_PATH = Path(__file__).resolve().parents[2] / "data" / "modat" +SERVICE_MAPPING = json.loads((DATA_PATH / "service_mapping.json").read_text()) +FIELD_INPUTS = { + "AS": ("asn.number",), + "domain": ("fqdns",), + "hostname": ("fqdns", "rdns"), + "port": ("port",), + "x509-fingerprint-sha1": ("tls.fingerprint_sha1",), + "x509-fingerprint-sha256": ("tls.fingerprint_sha256",), + "jarm-fingerprint": ("tls.jarm",), + "hasshserver-md5": ("ssh.hassh",), + "ssh-fingerprint": ("ssh.key_exchange.server_host_key.fingerprint_sha256",), + "favicon-mmh3": ("http.files.mmh3",), + "vulnerability": ("cves.id",), + "sha1": ("banner_sha1", "http.body_sha1", "http.files.sha1"), + "sha256": ("banner_sha256", "http.body_sha256", "http.files.sha256", "http.headers_sha256"), + "email": ("tls.extensions.subject_alt_name.email", "tls.extensions.issuer_alt_name.email"), + "target-org": ("asn.org", "tls.subject.organization", "tls.issuer.organization"), +} +for _type in ("email-src", "email-dst", "target-email"): + FIELD_INPUTS[_type] = FIELD_INPUTS["email"] +mispattributes = { + "input": sorted( + set(FIELD_INPUTS) + | { + "ip-src", + "ip-dst", + "ip-src|port", + "ip-dst|port", + "domain|ip", + "hostname|port", + "url", + "uri", + "link", + "filename|sha1", + "filename|sha256", + "text", + "hex", + "pattern-in-traffic", + } + ), + "format": "misp_standard", +} +moduleconfig = ["apikey", "max_results", "page_size", "timeout", "include_dns", "reverse_dns"] +moduleinfo = { + "version": "1.0", + "author": "MISP", + "description": "Enrich internet infrastructure using MODAT Magnify host, service and DNS observations.", + "module-type": ["expansion"], + "name": "MODAT Magnify", + "requirements": [ + "requests", + "PyMISP", + "MODAT Magnify API key", + "Install the bundled modat-service and modat-dns-record object templates in MISP", + ], + "references": ["https://api.magnify.modat.io/docs", "https://magnify.modat.io/docs"], + "features": "Typed service and DNS observations linked to existing MISP infrastructure objects.", + "input": "IPs, domains, network composites, certificate/SSH fingerprints, web hashes and CVEs.", + "output": "MISP objects and source JSON attachments; discovered attributes have to_ids disabled.", +} + + +class ModatError(ValueError): + pass + + +def _quote(value): + # Keep untrusted values in one literal; forbid escapes whose DSL behavior is undocumented. + if any(c in value for c in ('"', "\\")) or any(ord(c) < 32 for c in value): + raise ModatError("Values containing quotes, backslashes or control characters are unsupported.") + return f'"{value}"' + + +def _ip(value): + return str(ipaddress.ip_address(value.replace("[.]", ".").replace("{.}", "."))) + + +def _domain(value): + value = value.replace("[.]", ".").replace("{.}", ".").rstrip(".") + value = value.encode("idna").decode("ascii").lower() + if len(value) > 253 or not all( + re.fullmatch(r"[a-z0-9_](?:[a-z0-9_-]{0,61}[a-z0-9_])?", p) for p in value.split(".") + ): + raise ModatError("Invalid domain or hostname.") + return value + + +def _port(value): + if not str(value).isdigit() or not 1 <= int(value) <= 65535: + raise ModatError("Port must be between 1 and 65535.") + return str(int(value)) + + +def _term(fields, value, operator="="): + terms = [f"{field}{operator}{_quote(value)}" for field in fields] + return terms[0] if len(terms) == 1 else f'({" OR ".join(terms)})' + + +def build_query(attribute): + """Return (direct IP lookup, service query, DNS zone name).""" + typ, value = attribute["type"], str(attribute["value"]).strip() + if typ not in mispattributes["input"]: + raise ModatError("Unsupported attribute type.") + if not value: + raise ModatError("Attribute value is empty.") + if typ in ("ip-src", "ip-dst"): + if "/" in value: + network = str(ipaddress.ip_network(value, strict=False)) + return None, _term(("subnet",), network), None + return _ip(value), None, None + if typ in ("ip-src|port", "ip-dst|port", "hostname|port", "domain|ip"): + parts = value.split("|") + if len(parts) != 2: + raise ModatError("Composite attributes require exactly two components.") + left, right = parts + if typ == "domain|ip": + zone = _domain(left) + return None, f'{_term(("fqdns",), zone)} AND {_term(("ip",), _ip(right))}', zone + zone = _domain(left) if typ == "hostname|port" else None + field = "fqdns" if zone else "ip" + return None, f"{_term((field,), zone or _ip(left))} AND port={_port(right)}", zone + if typ in ("url", "uri", "link"): + parsed = urlsplit(value) + if parsed.scheme not in ("http", "https") or not parsed.hostname or parsed.username or parsed.password: + raise ModatError("Provide an absolute HTTP(S) URL without credentials.") + port = _port(parsed.port or (443 if parsed.scheme == "https" else 80)) + try: + host, zone, field = _ip(parsed.hostname), None, "ip" + except ValueError: + host, zone, field = _domain(parsed.hostname), _domain(parsed.hostname), "fqdns" + return None, f"{_term((field,), host)} AND port={port}", zone + if typ.startswith("filename|"): + parts = value.rsplit("|", 1) + if len(parts) != 2 or not parts[0]: + raise ModatError("Invalid filename/hash composite.") + typ, value = typ.split("|")[1], parts[1] + if typ in ("domain", "hostname"): + value = _domain(value) + return None, _term(FIELD_INPUTS[typ], value), value + if typ == "AS": + value = value.upper().removeprefix("AS") + if not value.isdigit() or not 0 <= int(value) <= 4294967295: + raise ModatError("Invalid autonomous system number.") + if typ == "port": + value = _port(value) + hash_lengths = { + "sha1": 40, + "sha256": 64, + "x509-fingerprint-sha1": 40, + "x509-fingerprint-sha256": 64, + "hasshserver-md5": 32, + } + if typ in hash_lengths: + if not re.fullmatch(rf"[0-9a-fA-F]{{{hash_lengths[typ]}}}", value): + raise ModatError("Invalid fingerprint or hash.") + value = value.lower() + if typ == "ssh-fingerprint" and not re.fullmatch(r"[0-9a-fA-F]{64}", value): + raise ModatError("MODAT SSH fingerprints require a 64-character hexadecimal SHA256 digest.") + if typ == "ssh-fingerprint": + value = value.lower() + if typ == "jarm-fingerprint" and not re.fullmatch(r"[0-9a-fA-F]{62}", value): + raise ModatError("Invalid JARM fingerprint.") + if typ == "favicon-mmh3": + if not re.fullmatch(r"-?\d+", value) or not -(2**31) <= int(value) < 2**31: + raise ModatError("Invalid signed 32-bit favicon MMH3 hash.") + if typ == "vulnerability": + if not re.fullmatch(r"CVE-\d{4}-\d{4,}", value, re.I): + raise ModatError("Only CVE vulnerability identifiers are supported.") + value = value.upper() + if typ == "text": + return None, _quote(value), None + if typ == "pattern-in-traffic": + return None, _term(("banner",), value, "~"), None + if typ == "hex": + if not re.fullmatch(r"(?:[0-9a-fA-F]{2})+", value): + raise ModatError("Invalid banner hexadecimal bytes.") + return None, _term(("banner_hex",), value.lower()), None + query = _term(FIELD_INPUTS[typ], value) + if len(query) > 2000: + raise ModatError("Query exceeds the API's 2000-character limit.") + return None, query, None + + +def _integer(config, key, default, minimum, maximum): + value = config.get(key, default) + if isinstance(value, bool) or not re.fullmatch(r"\d+", str(value)): + raise ModatError(f"{key} must be an integer.") + value = int(value) + if not minimum <= value <= maximum: + raise ModatError(f"{key} must be between {minimum} and {maximum}.") + return value + + +def _boolean(config, key, default): + value = config.get(key, default) + if isinstance(value, bool): + return value + if str(value).lower() in ("true", "1", "yes"): + return True + if str(value).lower() in ("false", "0", "no"): + return False + raise ModatError(f"{key} must be true or false.") + + +class ModatClient: + def __init__(self, key, timeout): + self.session = requests.Session() + self.session.headers.update({"Authorization": key, "Accept": "application/json"}) + self.timeout = timeout + + def request(self, path, payload=None): + response = self.session.request( + "GET" if payload is None else "POST", + f"{BASE_URL}{path}", + json=payload, + timeout=self.timeout, + allow_redirects=False, + ) + if response.status_code == 404: + return None + if response.status_code == 429: + retry = response.headers.get("Retry-After", "") + suffix = f" Retry after {retry} seconds." if re.fullmatch(r"\d+(?:\.\d+)?", retry) else "" + raise ModatError(f"MODAT rate limit exceeded.{suffix}") + if response.status_code in (401, 403): + raise ModatError("MODAT authentication failed or subscription does not permit this endpoint.") + if response.status_code != 200: + raise ModatError(f"MODAT returned HTTP {response.status_code}.") + try: + data = response.json() + except ValueError as exc: + raise ModatError("MODAT returned invalid JSON.") from exc + if not isinstance(data, dict) or "errors" in data: + raise ModatError("MODAT returned an invalid response or rejected the query.") + return data + + def search(self, endpoint, query, limit, page_size): + results = [] + for page in range(1, math.ceil(limit / page_size) + 1): + data = self.request(endpoint, {"query": query, "page": page, "page_size": page_size}) + if data is None: + raise ModatError("MODAT search endpoint was not found.") + rows = data.get("page") + total_pages = data.get("total_pages") + if not isinstance(rows, list) or not all(isinstance(row, dict) for row in rows): + raise ModatError("MODAT returned an invalid search page.") + if not isinstance(total_pages, int) or total_pages < 0: + raise ModatError("MODAT returned invalid pagination metadata.") + results.extend(rows[: limit - len(results)]) + if len(results) >= limit or not rows or page >= total_pages: + break + return results + + +def _leaves(data, prefix=""): + if isinstance(data, dict): + for key, value in data.items(): + yield from _leaves(value, f"{prefix}.{key}" if prefix else key) + elif isinstance(data, list): + for item in data: + yield from _leaves(item, prefix) + elif data is not None and data != "": + yield prefix, data + + +class ModatParser: + def __init__(self, attribute): + self.event = MISPEvent() + self.source = MISPAttribute() + self.source.from_dict(**attribute) + self.event.add_attribute(**self.source.to_dict()) + self.objects = {} + self.zones = set() + + @staticmethod + def add(obj, relation, value, **kwargs): + if value is None or value == "": + return + values = value if isinstance(value, list) else [value] + for item in values: + if item is None or item == "": + continue + if not any(a.object_relation == relation and str(a.value) == str(item) for a in obj.attributes): + obj.add_attribute(relation, value=item, to_ids=False, **kwargs) + + @staticmethod + def raw(obj, value): + payload = json.dumps(value, sort_keys=True, ensure_ascii=False).encode() + obj.add_attribute( + "raw-json", value="modat-observation.json", data=base64.b64encode(payload).decode(), to_ids=False + ) + + def put(self, obj, parent=None, relationship="associated-to"): + if not obj.attributes: + return None + required_one_of = obj._definition.get("requiredOneOf", []) + if required_one_of and not any(a.object_relation in required_one_of for a in obj.attributes): + return None + obj._validate() + # Attribute content identifies shared objects; references do not participate in deduplication. + signature = json.dumps( + [(a.object_relation, str(a.value), a.to_dict().get("data")) for a in obj.attributes], sort_keys=True + ) + key = (obj.name, signature) + if key not in self.objects: + obj.uuid = uuid5(NAMESPACE_URL, obj.name + signature) + self.objects[key] = obj + self.event.add_object(obj) + obj = self.objects[key] + target = str(parent or self.source.uuid) + if not any(str(r.referenced_uuid) == target and r.relationship_type == relationship for r in obj.references): + obj.add_reference(target, relationship) + return obj + + @staticmethod + def custom(name): + return MISPObject(name, strict=True, misp_objects_path_custom=DATA_PATH) + + def infrastructure(self, data, parent): + asn = data.get("asn") or {} + if asn.get("number") is not None: + obj = MISPObject("asn", strict=True) + for rel, val in ( + ("asn", f"AS{asn['number']}"), + ("description", asn.get("org")), + ("subnet-announced", asn.get("netblock")), + ): + self.add(obj, rel, val) + self.put(obj, parent) + geo = data.get("geo") or {} + if geo: + obj = MISPObject("geolocation", strict=True) + for field, rel in ( + ("city_name", "city"), + ("country_name", "country"), + ("country_iso_code", "countrycode"), + ("accuracy_radius", "accuracy-radius"), + ): + self.add(obj, rel, geo.get(field)) + coords = geo.get("location") + if isinstance(coords, list) and len(coords) == 2: + self.add(obj, "longitude", coords[0]) + self.add(obj, "latitude", coords[1]) + self.put(obj, parent) + + def vulnerabilities(self, values, parent): + for cve in values or []: + if not isinstance(cve, dict) or not cve.get("id"): + continue + obj = MISPObject("vulnerability", strict=True) + self.add(obj, "id", cve["id"]) + self.add(obj, "cvss-score", cve.get("cvss")) + if cve.get("is_kev") is not None: + self.add( + obj, + "description", + f"MODAT reports is_kev={str(cve['is_kev']).lower()}; inferred exposure, not verified exploitation.", + ) + self.put(obj, parent, "vulnerability-of") + + def host(self, data): + if not isinstance(data, dict) or not data.get("ip"): + raise ModatError("MODAT host/service response has no IP address.") + ip = _ip(data["ip"]) + obj = MISPObject("ip-port", strict=True) + self.add(obj, "ip", ip) + self.add(obj, "domain", data.get("fqdns")) + self.add(obj, "ptr-record", data.get("rdns")) + if data.get("is_anycast") is not None: + self.add(obj, "text", f"MODAT is_anycast={str(data['is_anycast']).lower()}") + self.add(obj, "text", [f"MODAT classification: {tag}" for tag in data.get("tags", [])]) + host = self.put(obj) + self.infrastructure(data, host.uuid) + self.vulnerabilities(data.get("cves"), host.uuid) + # Preserve extensions and all original nested data as evidence, without inventing attribute semantics. + payload = base64.b64encode(json.dumps(data, sort_keys=True).encode()).decode() + raw_uuid = uuid5(NAMESPACE_URL, "modat-host-json" + payload) + if not any(str(a.uuid) == str(raw_uuid) for a in self.event.attributes): + self.event.add_attribute( + "attachment", + value="modat-host.json", + data=payload, + uuid=raw_uuid, + to_ids=False, + comment=f"MODAT observation for {ip}", + ) + if not any(str(r.referenced_uuid) == str(raw_uuid) for r in host.references): + host.add_reference(str(raw_uuid), "derived-from") + services = data.get("services", [data["service"]] if "service" in data else []) + if not isinstance(services, list): + raise ModatError("MODAT returned an invalid services list.") + for service in services: + if not isinstance(service, dict): + raise ModatError("MODAT returned an invalid service.") + self.service(ip, service, host.uuid) + + def service(self, ip, data, parent): + obj = self.custom("modat-service") + self.add(obj, "ip", ip) + for path, value in _leaves(data): + if path in SERVICE_MAPPING: + self.add(obj, SERVICE_MAPPING[path], value) + # Search responses use a singular port; host details cluster equivalent observations into ports. + self.add(obj, "ports", data.get("port")) + self.raw(obj, data) + service = self.put(obj, parent) + ports = data.get("ports", [data["port"]] if "port" in data else []) + for port in ports: + endpoint = MISPObject("ip-port", strict=True) + self.add(endpoint, "ip", ip) + self.add(endpoint, "dst-port", port) + self.add(endpoint, "protocol", data.get("transport")) + self.add(endpoint, "first-seen", data.get("discovered_at")) + self.add(endpoint, "last-seen", data.get("scanned_at")) + self.put(endpoint, service.uuid) + tls = data.get("tls") or {} + if tls: + cert = MISPObject("x509", strict=True) + for field, relation in ( + ("fingerprint_sha1", "x509-fingerprint-sha1"), + ("fingerprint_sha256", "x509-fingerprint-sha256"), + ("serial_number", "serial-number"), + ("valid_from", "validity-not-before"), + ("expires_at", "validity-not-after"), + ("is_self_signed", "is_self_signed"), + ("raw", "text"), + ): + self.add(cert, relation, tls.get(field)) + dn_mapping = { + "common_name": "common-name", + "country": "country", + "organization": "organization", + "organizational_unit": "organizational-unit", + "province": "state-or-province-name", + "locality": "locality", + } + for side in ("issuer", "subject"): + dn = tls.get(side) or {} + for field, relation in dn_mapping.items(): + self.add(cert, f"{side}-{relation}", dn.get(field)) + if dn: + # Render an X.509 distinguished name, retaining repeated RDN values. + labels = { + "common_name": "CN", + "country": "C", + "organization": "O", + "organizational_unit": "OU", + "province": "ST", + "locality": "L", + "email_address": "emailAddress", + } + components = [] + for field, label in labels.items(): + for value in dn.get(field) or []: + escaped = re.sub(r'([,+=<>#;"\\])', r"\\\1", str(value)) + if escaped.startswith(" "): + escaped = "\\" + escaped + if escaped.endswith(" "): + escaped = escaped[:-1] + "\\ " + components.append(f"{label}={escaped}") + self.add(cert, side, ",".join(components)) + san = (tls.get("extensions") or {}).get("subject_alt_name") or {} + for field, relation in ( + ("dns", "dns_names"), + ("ipv4", "ip"), + ("ipv6", "ip"), + ("email", "email"), + ("uri", "uri"), + ): + self.add(cert, relation, san.get(field)) + # Fingerprint-only and issuer-only objects are valid; TLS handshake-only data stays in modat-service. + if any(a.object_relation in cert._definition["requiredOneOf"] for a in cert.attributes): + self.put(cert, service.uuid, "used-by") + fingerprints = data.get("fingerprints") or {} + for kind, item in [("os", fingerprints.get("os")), ("service", fingerprints.get("service"))] + [ + ("technology", t) for t in fingerprints.get("technologies", []) + ]: + if not isinstance(item, dict) or not (item.get("product") or item.get("name")): + continue + software = MISPObject("software", strict=True) + software.comment = f"MODAT {kind} fingerprint" + self.add(software, "name", item.get("product") or item.get("name")) + self.add(software, "version", item.get("version")) + self.put(software, service.uuid) + for item in (data.get("http") or {}).get("files") or []: + file_obj = MISPObject("file", strict=True) + self.add(file_obj, "filename", item.get("path")) + self.add(file_obj, "sha1", item.get("sha1")) + self.add(file_obj, "sha256", item.get("sha256")) + self.add(file_obj, "text", f"MODAT category: {item['category']}" if item.get("category") else None) + if any(a.object_relation in ("filename", "sha1", "sha256") for a in file_obj.attributes): + self.put(file_obj, service.uuid) + self.vulnerabilities(data.get("cves"), service.uuid) + + def dns(self, data): + if not isinstance(data, dict) or not data.get("fqdn") or not isinstance(data.get("records"), dict): + raise ModatError("MODAT returned an invalid DNS zone.") + fqdn = _domain(data["fqdn"]) + if fqdn in self.zones: + return + self.zones.add(fqdn) + for rrtype, records in data["records"].items(): + if not isinstance(records, list): + raise ModatError("MODAT returned invalid DNS records.") + for record in records: + if not isinstance(record, dict): + raise ModatError("MODAT returned an invalid DNS record.") + obj = self.custom("modat-dns-record") + self.add(obj, "fqdn", fqdn) + self.add(obj, "rrtype", rrtype.upper()) + for key, value in record.items(): + relation = key.replace("_", "-") + if relation in obj._definition["attributes"] and key != "raw-json": + self.add(obj, relation, value) + self.raw( + obj, + { + "fqdn": fqdn, + "rrtype": rrtype, + "record": record, + "discovered_at": data.get("discovered_at"), + "modified_at": data.get("modified_at"), + }, + ) + observation = self.put(obj) + self.infrastructure(record, observation.uuid) + if rrtype in ("a", "aaaa") and record.get("ip"): + domain_ip = MISPObject("domain-ip", strict=True) + self.add(domain_ip, "domain", fqdn) + self.add(domain_ip, "ip", record["ip"]) + self.add(domain_ip, "first-seen", record.get("created_at")) + # deleted_at is lifecycle state, not the last observation time. + self.put(domain_ip, observation.uuid, "derived-from") + # Preserve original DNS target notation, including root '.' and trailing dots. + standard = { + "a": ("a-record", "ip"), + "aaaa": ("aaaa-record", "ip"), + "cname": ("cname-record", "cname"), + "mx": ("mx-record", "host"), + "ns": ("ns-record", "ns"), + "txt": ("txt-record", "value"), + } + if rrtype in standard: + relation, key = standard[rrtype] + if record.get(key): + dns = MISPObject("dns-record", strict=True) + self.add(dns, "queried-domain", fqdn) + self.add(dns, relation, record[key]) + self.put(dns, observation.uuid, "derived-from") + + def result(self): + event = json.loads(self.event.to_json()) + return {"results": {key: event.get(key, []) for key in ("Attribute", "Object")}} + + +def handler(q=False): + if q is False: + return False + client = None + try: + request = json.loads(q) + if not isinstance(request, dict): + raise ModatError("Request must be a JSON object.") + attribute = request.get("attribute") + if not isinstance(attribute, dict) or not all(k in attribute for k in ("type", "value", "uuid")): + raise ModatError("Provide an attribute with type, value and uuid.") + if not isinstance(attribute["type"], str) or not isinstance(attribute["value"], (str, int)): + raise ModatError("Attribute type and value must be strings (numeric values are also accepted).") + UUID(str(attribute["uuid"])) + config = request.get("config") or {} + if not isinstance(config, dict) or not isinstance(config.get("apikey"), str) or not config["apikey"].strip(): + raise ModatError("Provide a MODAT API key in config.apikey.") + limit = _integer(config, "max_results", 10, 1, 1000) + page_size = _integer(config, "page_size", 10, 10, 100) + timeout = _integer(config, "timeout", 30, 1, 120) + include_dns = _boolean(config, "include_dns", True) + reverse_dns = _boolean(config, "reverse_dns", False) + ip, query, zone = build_query(attribute) + if query and len(query) > 2000: + raise ModatError("Query exceeds the API's 2000-character limit.") + parser = ModatParser(attribute) + client = ModatClient(config["apikey"].strip(), timeout) + if ip: + data = client.request(f"/host/{quote(ip, safe='')}/v1") + if data is not None: + if not isinstance(data.get("data"), dict): + raise ModatError("MODAT returned invalid host details.") + parser.host(data["data"]) + else: + for row in client.search("/service/search/v1", query, limit, page_size): + if not isinstance(row.get("service"), dict): + raise ModatError("MODAT returned an invalid service search record.") + parser.host(row) + zones = [zone] if include_dns and zone else [] + if include_dns and reverse_dns and ip: + rrtype = "a" if ipaddress.ip_address(ip).version == 4 else "aaaa" + for row in client.search("/dns/search/v1", _term((f"{rrtype}.ip",), ip), limit, min(page_size, 100)): + if not isinstance(row.get("fqdn"), str): + raise ModatError("MODAT returned an invalid DNS search record.") + zones.append(_domain(row["fqdn"])) + for fqdn in dict.fromkeys(zones): + data = client.request(f"/dns/zones/{quote(fqdn, safe='')}/v1") + if data is not None: + parser.dns(data) + return parser.result() + except ModatError as exc: + return {"error": str(exc)} + except requests.RequestException: + # Exceptions may contain request headers or API response bodies; do not echo them. + return {"error": "Unable to reach MODAT (network error or timeout)."} + except (ValueError, TypeError, KeyError, AttributeError, PyMISPError): + return {"error": "Invalid request, attribute, or MODAT response data."} + finally: + if client is not None: + client.session.close() + + +def introspection(): + return mispattributes + + +def version(): + return {**moduleinfo, "config": moduleconfig} diff --git a/tests/test_files/modat_openapi.json b/tests/test_files/modat_openapi.json new file mode 100644 index 000000000..2bdb03684 --- /dev/null +++ b/tests/test_files/modat_openapi.json @@ -0,0 +1 @@ +{"openapi":"3.1.0","info":{"title":"Modat Magnify API","description":"\n\nIntroducing the Modat Magnify API. This powerful tool is designed for proactive cybersecurity efforts. It allows you\nto search, analyse, and retrieve detailed information about internet-connected IP infrastructure and DNS records.\nFor the data model, the query language, and worked examples, see the\n[Modat Resource Center](https://magnify.modat.io/docs).\n\nPlease note, API access follows [Modat’s Terms of Service](https://magnify.modat.io/terms-of-service).\nWe kindly ask that all automated interactions go through this API.\n\n### Authentication\n\nAll API endpoints require authentication using an API key that should be included in the `Authorization` header of each\nrequest. Prefixing the API key with `Bearer ` is optional.\n\n### Rate Limiting\n\nSearch and data-retrieval endpoints enforce rate limiting based on your subscription level. Management endpoints use a\nseparate bucket with a higher limit of 10 requests per second for\nlightweight requests such as quota usage, query history and bulk export. The API uses a sliding time window of\n3 seconds to calculate your request rate. If you exceed your allowed request rate,\nrequests are rejected with HTTP 429. Rate-limit responses include a `Retry-After` header containing the number of seconds\nto wait before retrying. For optimal performance, distribute your requests evenly rather than sending them in bursts.\n\n### Deprecation Policy\n\nWhen a field or endpoint is scheduled for removal, it is first marked as deprecated in this OpenAPI specification\n(`deprecated: true`) and remains fully functional for a transition period of at least one month. After that period it\nmay be removed without further notice. If you generate client code from this specification, regenerate it periodically\nand migrate away from deprecated fields as soon as they appear.\n\n### Usage Guidelines\n\nPlease refer to the endpoint documentation below for specific request and response formats.\n","version":"1.65.2"},"paths":{"/service/search/v1":{"post":{"tags":["service"],"summary":"Service level search","description":"Search for services using our extensive query language","operationId":"do_search_service_search_v1_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceSearchResponse","title":"Response Do Search Service Search V1 Post"}}}},"422":{"description":"Unprocessable Content","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvalidQueryResponse"}}}}},"security":[{"APIKeyHeader":[]}]}},"/service/search/facets/v1":{"get":{"tags":["service"],"summary":"Service level search facets","description":"The facet paths the search endpoint accepts. Requesting any of them needs the dynamic-facets entitlement.","operationId":"get_search_facets_service_search_facets_v1_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AvailableFacetsResponse"}}}}},"security":[{"APIKeyHeader":[]}]}},"/service/history/v1":{"post":{"tags":["service"],"summary":"Get Service History","description":"Get history of a service by IP, transport protocol and port. Max 100 records.","operationId":"get_service_history_service_history_v1_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceHistoryRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceHistoryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"APIKeyHeader":[]}]}},"/host/search/v1":{"post":{"tags":["host"],"summary":"Host level search","description":"Perform a host level search","operationId":"do_search_host_search_v1_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HostSearchResponse","title":"Response Do Search Host Search V1 Post"}}}},"422":{"description":"Unprocessable Content","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvalidQueryResponse"}}}}},"security":[{"APIKeyHeader":[]}]}},"/host/search/facets/v1":{"get":{"tags":["host"],"summary":"Host level search facets","description":"The facet paths the search endpoint accepts. Requesting any of them needs the dynamic-facets entitlement.","operationId":"get_search_facets_host_search_facets_v1_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AvailableFacetsResponse"}}}}},"security":[{"APIKeyHeader":[]}]}},"/host/{ip}/v1":{"get":{"tags":["host"],"summary":"Host details by IP","description":"Get host details by IP.","operationId":"get_host_details_by_ip_host__ip__v1_get","security":[{"APIKeyHeader":[]}],"parameters":[{"name":"ip","in":"path","required":true,"schema":{"$ref":"#/components/schemas/IPvAnyDefangedIP","description":"IPv4 or IPv6 address, including defanged IP notation."},"description":"IPv4 or IPv6 address, including defanged IP notation."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HostDetailsLookup"}}}},"404":{"description":"Host not found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/export/v1":{"post":{"tags":["export"],"summary":"Bulk Export","description":"(Preview/unstable): Bulk Export","operationId":"perform_export_export_v1_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExportRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ExportState"},{"$ref":"#/components/schemas/InvalidQueryResponse"}],"title":"Response Perform Export Export V1 Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"APIKeyHeader":[]}]}},"/export/state/v1":{"get":{"tags":["export"],"summary":"Bulk Export: get state","description":"(Preview/unstable): Bulk Export: get state","operationId":"get_export_state_export_state_v1_get","security":[{"APIKeyHeader":[]}],"parameters":[{"name":"export_id","in":"query","required":true,"schema":{"type":"string","format":"uuid","title":"Export Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExportState"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/export/history/v1":{"get":{"tags":["export"],"summary":"Bulk Export: get history","description":"(Preview/unstable): Bulk Export: get history","operationId":"get_export_history_export_history_v1_get","security":[{"APIKeyHeader":[]}],"parameters":[{"name":"page_nr","in":"query","required":false,"schema":{"type":"integer","default":1,"title":"Page Nr"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","default":10,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExportHistoryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/search/queries/v1":{"get":{"tags":["search"],"summary":"Search query history","description":"See your recent search queries, up to one month back.","operationId":"get_search_query_history_search_queries_v1_get","security":[{"APIKeyHeader":[]}],"parameters":[{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":100,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SearchQueryHistoryItem"},"title":"Response Get Search Query History Search Queries V1 Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/search/queries/{query_id}/v1":{"get":{"tags":["search"],"summary":"Search query history item","description":"See one query from your recent search history, up to one month back.","operationId":"get_search_query_history_item_search_queries__query_id__v1_get","security":[{"APIKeyHeader":[]}],"parameters":[{"name":"query_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Query Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchQueryHistoryItem"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/search/quotas/v1":{"get":{"tags":["search"],"summary":"Search quota usage","description":"See how much search quota you have left in the current quota period.","operationId":"get_search_quota_usage_search_quotas_v1_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchQuotaState"}}}}},"security":[{"APIKeyHeader":[]}]}},"/dns/zones/{fqdn}/v1":{"get":{"tags":["dns"],"summary":"Retrieve zone by FQDN","operationId":"get_zone_dns_zones__fqdn__v1_get","security":[{"APIKeyHeader":[]}],"parameters":[{"name":"fqdn","in":"path","required":true,"schema":{"type":"string","title":"Fqdn"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DNSZoneResponse"}}}},"404":{"description":"Zone not found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dns/search/v1":{"post":{"tags":["dns"],"summary":"Search DNS history observations","operationId":"search_dns_dns_search_v1_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DNSSearchRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DNSSearchResponse","title":"Response Search Dns Dns Search V1 Post"}}}},"422":{"description":"Unprocessable Content","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvalidQueryResponse"}}}}},"security":[{"APIKeyHeader":[]}]}},"/dns/search/facets/v1":{"get":{"tags":["dns"],"summary":"DNS search facets","description":"The facet paths the search endpoint accepts. Requesting any of them needs the dynamic-facets entitlement.","operationId":"get_search_facets_dns_search_facets_v1_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AvailableFacetsResponse"}}}}},"security":[{"APIKeyHeader":[]}]}}},"components":{"schemas":{"AAAARecordModel":{"properties":{"ttl":{"type":"integer","title":"Ttl"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"deleted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Deleted At"},"ip":{"type":"string","title":"Ip"},"asn":{"anyOf":[{"$ref":"#/components/schemas/ASNModel"},{"type":"null"}]},"geo":{"anyOf":[{"$ref":"#/components/schemas/GEOModel"},{"type":"null"}]}},"type":"object","required":["ttl","created_at","ip"],"title":"AAAARecordModel"},"ARecordModel":{"properties":{"ttl":{"type":"integer","title":"Ttl"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"deleted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Deleted At"},"ip":{"type":"string","title":"Ip"},"asn":{"anyOf":[{"$ref":"#/components/schemas/ASNModel"},{"type":"null"}]},"geo":{"anyOf":[{"$ref":"#/components/schemas/GEOModel"},{"type":"null"}]}},"type":"object","required":["ttl","created_at","ip"],"title":"ARecordModel"},"ASNModel":{"properties":{"number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Number"},"org":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Org"}},"type":"object","title":"ASNModel"},"AvailableFacetsResponse":{"properties":{"facets":{"items":{"type":"string"},"type":"array","title":"Facets"}},"additionalProperties":true,"type":"object","required":["facets"],"title":"AvailableFacetsResponse","description":"The facet paths a caller may request on a search endpoint."},"CNAMERecordModel":{"properties":{"ttl":{"type":"integer","title":"Ttl"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"deleted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Deleted At"},"cname":{"type":"string","title":"Cname"}},"type":"object","required":["ttl","created_at","cname"],"title":"CNAMERecordModel"},"CVEHit":{"properties":{"id":{"type":"string","title":"Id","examples":["CVE-2023-44487"]},"cvss":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Cvss","examples":[7.5]},"is_kev":{"type":"boolean","title":"Is Kev","examples":[true]}},"additionalProperties":true,"type":"object","required":["id","is_kev"],"title":"CVEHit"},"DNSQueryModel":{"properties":{"fqdn":{"type":"string","title":"Fqdn"},"discovered_at":{"type":"string","format":"date-time","title":"Discovered At"},"modified_at":{"type":"string","format":"date-time","title":"Modified At","description":"Latest time any record in the zone became active or stopped being active; this is not a scan or ingestion timestamp."},"total_records":{"type":"integer","title":"Total Records"},"record_types":{"items":{"$ref":"#/components/schemas/DNSRecordType"},"type":"array","title":"Record Types"}},"additionalProperties":true,"type":"object","required":["fqdn","discovered_at","modified_at","total_records","record_types"],"title":"DNSQueryModel"},"DNSRecordType":{"type":"string","enum":["a","aaaa","cname","https","mx","ns","soa","srv","txt"],"title":"DNSRecordType"},"DNSRecordsModel":{"properties":{"a":{"items":{"$ref":"#/components/schemas/ARecordModel"},"type":"array","title":"A"},"aaaa":{"items":{"$ref":"#/components/schemas/AAAARecordModel"},"type":"array","title":"Aaaa"},"cname":{"items":{"$ref":"#/components/schemas/CNAMERecordModel"},"type":"array","title":"Cname"},"https":{"items":{"$ref":"#/components/schemas/HTTPSRecordModel"},"type":"array","title":"Https"},"mx":{"items":{"$ref":"#/components/schemas/MXRecordModel"},"type":"array","title":"Mx"},"ns":{"items":{"$ref":"#/components/schemas/NSRecordModel"},"type":"array","title":"Ns"},"soa":{"items":{"$ref":"#/components/schemas/SOARecordModel"},"type":"array","title":"Soa"},"srv":{"items":{"$ref":"#/components/schemas/SRVRecordModel"},"type":"array","title":"Srv"},"txt":{"items":{"$ref":"#/components/schemas/TXTRecordModel"},"type":"array","title":"Txt"}},"type":"object","title":"DNSRecordsModel"},"DNSSearchRequest":{"properties":{"query":{"type":"string","maxLength":2000,"title":"Query"},"page":{"type":"integer","minimum":1.0,"title":"Page","default":1},"page_size":{"type":"integer","maximum":100.0,"minimum":1.0,"title":"Page Size","default":10},"facets":{"items":{"type":"string"},"type":"array","title":"Facets"},"facet_size":{"type":"integer","maximum":50.0,"minimum":1.0,"title":"Facet Size","default":10}},"additionalProperties":false,"type":"object","required":["query"],"title":"DNSSearchRequest"},"DNSSearchResponse":{"properties":{"page_nr":{"type":"integer","title":"Page Nr"},"total_pages":{"type":"integer","title":"Total Pages"},"total_records":{"type":"integer","title":"Total Records"},"page":{"items":{"$ref":"#/components/schemas/DNSQueryModel"},"type":"array","title":"Page"},"facets":{"additionalProperties":{"items":{"$ref":"#/components/schemas/FacetBucket"},"type":"array"},"type":"object","title":"Facets","default":{}}},"additionalProperties":true,"type":"object","required":["page_nr","total_pages","total_records","page"],"title":"DNSSearchResponse"},"DNSZoneResponse":{"properties":{"fqdn":{"type":"string","title":"Fqdn"},"records":{"$ref":"#/components/schemas/DNSRecordsModel"},"discovered_at":{"type":"string","format":"date-time","title":"Discovered At"},"modified_at":{"type":"string","format":"date-time","title":"Modified At","description":"Latest time any record in the zone became active or stopped being active; this is not a scan or ingestion timestamp."}},"additionalProperties":true,"type":"object","required":["fqdn","records","discovered_at","modified_at"],"title":"DNSZoneResponse"},"ExportHistoryEntry":{"properties":{"export_id":{"type":"string","format":"uuid","title":"Export Id"},"query":{"type":"string","title":"Query"},"search_level":{"$ref":"#/components/schemas/SearchLevel"},"max_results":{"type":"integer","title":"Max Results"},"state":{"$ref":"#/components/schemas/JobState"},"requested_at":{"type":"string","format":"date-time","title":"Requested At"}},"additionalProperties":true,"type":"object","required":["export_id","query","search_level","max_results","state","requested_at"],"title":"ExportHistoryEntry"},"ExportHistoryResponse":{"properties":{"page_nr":{"type":"integer","title":"Page Nr"},"total_pages":{"type":"integer","title":"Total Pages"},"total_records":{"type":"integer","title":"Total Records"},"page":{"items":{"$ref":"#/components/schemas/ExportHistoryEntry"},"type":"array","title":"Page"}},"additionalProperties":true,"type":"object","required":["page_nr","total_pages","total_records","page"],"title":"ExportHistoryResponse"},"ExportRequest":{"properties":{"query":{"type":"string","title":"Query"},"search_level":{"type":"string","enum":["service","host","whois"],"title":"Search Level"},"max_results":{"type":"integer","exclusiveMinimum":0.0,"title":"Max Results"},"fields":{"anyOf":[{"items":{"type":"string"},"type":"array","maxItems":100,"minItems":1},{"type":"null"}],"title":"Fields","description":"Dotted paths of the fields to export, matched with fnmatch (e.g. `service.tls.*`). `service` and `services` are aliases at both search levels. Null exports all fields.","examples":[["ip","service.port","service.tls.*"]]}},"additionalProperties":false,"type":"object","required":["query","search_level","max_results"],"title":"ExportRequest"},"ExportState":{"properties":{"export_id":{"type":"string","format":"uuid","title":"Export Id"},"state":{"$ref":"#/components/schemas/JobState"},"progress":{"type":"number","title":"Progress"},"download_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Download Url"},"message":{"type":"string","title":"Message","default":""}},"additionalProperties":true,"type":"object","required":["export_id","state","progress"],"title":"ExportState"},"FacetBucket":{"properties":{"key":{"type":"string","title":"Key"},"count":{"type":"integer","title":"Count"}},"additionalProperties":false,"type":"object","required":["key","count"],"title":"FacetBucket","description":"One term-facet bucket as returned by the faceted search endpoints."},"FingerprintTags":{"type":"string","enum":["Honeypot","Malicious","IoT","OT","C2","Security Tool","Onion link","Open Directory","Open Bucket","Redirect","Camera","Parking System","Power Plant","DevOps","Database","Compromised","Solar Panel","SDR","Lighting System","Home Automation","Climate Control","Bug Bounty","Smart Energy","Smart Garden","Geo Position","3D Printer","Alarm","Healthcare","Fuel System","Access Management","Password Manager","SCADA","Cloud","Radar System","Printer","VNC","VoIP","ICS","Cellular","Building Automation","AI Interface","LLM Prompt","Charging Station","Maritime","TV Streaming","BitTorrent","Remote Access","Traffic Controller","Evidence Collector","Billboard","KVM","BMC","Case Management","Water Management","SMS Gateway","HMI","PQC","PQC SSH","PQC TLS","VPN","Proxy Server","AI Framework","WebSocket","MLOps"],"title":"FingerprintTags"},"GEOModel":{"properties":{"city_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"City Name"},"country_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country Name"},"country_iso_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country Iso Code"},"continent_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Continent Code"}},"type":"object","title":"GEOModel"},"HTTPSRecordModel":{"properties":{"ttl":{"type":"integer","title":"Ttl"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"deleted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Deleted At"},"priority":{"type":"integer","title":"Priority"},"target":{"type":"string","title":"Target"},"params":{"type":"string","title":"Params"}},"type":"object","required":["ttl","created_at","priority","target","params"],"title":"HTTPSRecordModel"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"HostDetailsLookup":{"properties":{"data":{"$ref":"#/components/schemas/HostDetailsModel"}},"additionalProperties":true,"type":"object","required":["data"],"title":"HostDetailsLookup"},"HostDetailsModel":{"properties":{"ip":{"type":"string","format":"ipvanyaddress","title":"Ip","examples":["203.0.113.14"]},"geo":{"anyOf":[{"$ref":"#/components/schemas/_GEO"},{"type":"null"}]},"asn":{"anyOf":[{"$ref":"#/components/schemas/_ASN"},{"type":"null"}]},"fqdns":{"items":{"type":"string"},"type":"array","title":"Fqdns","default":[],"examples":[["example.com","www.example.com"]]},"rdns":{"items":{"type":"string"},"type":"array","title":"Rdns","default":[],"examples":[["example.com"]]},"is_anycast":{"type":"boolean","title":"Is Anycast","default":false,"examples":[false]},"tags":{"items":{"$ref":"#/components/schemas/FingerprintTags"},"type":"array","title":"Tags","default":[],"examples":[["Cloud"]]},"cves":{"items":{"$ref":"#/components/schemas/CVEHit"},"type":"array","title":"Cves","default":[]},"services":{"items":{"$ref":"#/components/schemas/_DetailedService"},"type":"array","title":"Services"}},"additionalProperties":true,"type":"object","required":["ip","services"],"title":"HostDetailsModel"},"HostQueryModel":{"properties":{"ip":{"type":"string","format":"ipvanyaddress","title":"Ip"},"geo":{"anyOf":[{"$ref":"#/components/schemas/_GEO"},{"type":"null"}]},"asn":{"anyOf":[{"$ref":"#/components/schemas/_ASN"},{"type":"null"}]},"fqdns":{"items":{"type":"string"},"type":"array","title":"Fqdns","default":[]},"rdns":{"items":{"type":"string"},"type":"array","title":"Rdns","default":[]},"is_anycast":{"type":"boolean","title":"Is Anycast","default":false},"tags":{"items":{"$ref":"#/components/schemas/FingerprintTags"},"type":"array","title":"Tags","default":[]},"cves":{"items":{"$ref":"#/components/schemas/CVEHit"},"type":"array","title":"Cves","default":[]},"services":{"items":{"$ref":"#/components/schemas/modat__backend__api__shared__models__host___Service"},"type":"array","title":"Services"}},"additionalProperties":true,"type":"object","required":["ip","services"],"title":"HostQueryModel"},"HostSearchResponse":{"properties":{"page_nr":{"type":"integer","title":"Page Nr"},"total_pages":{"type":"integer","title":"Total Pages"},"total_records":{"type":"integer","title":"Total Records"},"page":{"items":{"$ref":"#/components/schemas/HostQueryModel"},"type":"array","title":"Page"},"facets":{"additionalProperties":{"items":{"$ref":"#/components/schemas/FacetBucket"},"type":"array"},"type":"object","title":"Facets","default":{}}},"additionalProperties":true,"type":"object","required":["page_nr","total_pages","total_records","page"],"title":"HostSearchResponse"},"IPvAnyDefangedIP":{"type":"string","format":"ipvanyaddress"},"InvalidQueryResponse":{"properties":{"errors":{"items":{"type":"string"},"type":"array","title":"Errors"}},"additionalProperties":true,"type":"object","required":["errors"],"title":"InvalidQueryResponse"},"JobState":{"type":"string","enum":["queued","running","done","failed","unknown"],"title":"JobState"},"MXRecordModel":{"properties":{"ttl":{"type":"integer","title":"Ttl"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"deleted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Deleted At"},"priority":{"type":"integer","title":"Priority"},"host":{"type":"string","title":"Host"}},"type":"object","required":["ttl","created_at","priority","host"],"title":"MXRecordModel"},"NSRecordModel":{"properties":{"ttl":{"type":"integer","title":"Ttl"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"deleted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Deleted At"},"ns":{"type":"string","title":"Ns"}},"type":"object","required":["ttl","created_at","ns"],"title":"NSRecordModel"},"NetworkProtocol":{"type":"string","enum":["unknown","tls","ftp","ssh","http","smtp","bacnet","mysql","rdp","vnc","telnet","dnp3","amqp091","rwhois","fox","mssql","modbus","mongodb","siemens","oracle","postgres","memcached","imap","redis","ipp","smb","pop3","rtsp","rsync","dns","enip","pcworx","adb","mqtt","proconos","snmp","openvpn","ike","ntp","pptp","socks5","socks4","atg","iec104","mdns","codesys","opcua","cr3","fins","pcom","dicom","hl7","pigeonhole","checkmk_agent","observium_agent","junoscript","bittorrent_dht","nmea","kvm","slp","sip","msgr2","teamtalk","dcerpc","svn","irc","multistream","nats","xmpp","bittorrent","icap","ntrip","cobaltstrike"],"title":"NetworkProtocol"},"SOARecordModel":{"properties":{"ttl":{"type":"integer","title":"Ttl"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"deleted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Deleted At"},"mname":{"type":"string","title":"Mname"},"rname":{"type":"string","title":"Rname"},"serial":{"type":"integer","title":"Serial"},"refresh":{"type":"integer","title":"Refresh"},"retry":{"type":"integer","title":"Retry"},"expire":{"type":"integer","title":"Expire"},"minimum":{"type":"integer","title":"Minimum"}},"type":"object","required":["ttl","created_at","mname","rname","serial","refresh","retry","expire","minimum"],"title":"SOARecordModel"},"SRVRecordModel":{"properties":{"ttl":{"type":"integer","title":"Ttl"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"deleted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Deleted At"},"priority":{"type":"integer","title":"Priority"},"weight":{"type":"integer","title":"Weight"},"port":{"type":"integer","title":"Port"},"target":{"type":"string","title":"Target"}},"type":"object","required":["ttl","created_at","priority","weight","port","target"],"title":"SRVRecordModel"},"ScannedServiceModel":{"properties":{"ip":{"anyOf":[{"type":"string","format":"ipv4"},{"type":"string","format":"ipv6"}],"title":"Ip","examples":["203.0.113.14"]},"service":{"$ref":"#/components/schemas/modat__backend__api__shared__models__scanned_service___Service"}},"additionalProperties":true,"type":"object","required":["ip","service"],"title":"ScannedServiceModel","description":"One service exactly as a single scan saw it, with nothing enriched around it.\n\nThe enriched view (`ServiceQueryModel`) adds what the ingest pipeline derives per host —\nfqdns, asn, geo, fingerprints, CVEs — none of which a scan produces. Served on its own\nwherever the answer must be one scan and not the current state of the service: a point in\nthe service's history, or the outcome of a live scan."},"SearchLevel":{"type":"string","enum":["service","host","dns","whois","cert"],"title":"SearchLevel","description":"The level that the search query is executed at."},"SearchQueryHistoryItem":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"query":{"type":"string","title":"Query"},"num_hits":{"type":"integer","title":"Num Hits"},"cost":{"type":"integer","title":"Cost"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","query","num_hits","cost","created_at"],"title":"SearchQueryHistoryItem"},"SearchQuotaState":{"properties":{"search_quota":{"type":"integer","title":"Search Quota"},"search_results_quota":{"type":"integer","title":"Search Results Quota"},"remaining_search_quota":{"type":"integer","title":"Remaining Search Quota"},"remaining_results_quota":{"type":"integer","title":"Remaining Results Quota"}},"type":"object","required":["search_quota","search_results_quota","remaining_search_quota","remaining_results_quota"],"title":"SearchQuotaState"},"SearchRequest":{"properties":{"query":{"type":"string","maxLength":2000,"title":"Query"},"page":{"type":"integer","minimum":1.0,"title":"Page","default":1},"page_size":{"type":"integer","maximum":100.0,"minimum":10.0,"title":"Page Size","default":10},"facets":{"items":{"type":"string"},"type":"array","title":"Facets","default":[]},"facet_size":{"type":"integer","maximum":50.0,"minimum":1.0,"title":"Facet Size","default":10}},"type":"object","required":["query"],"title":"SearchRequest"},"ServiceHistoryRequest":{"properties":{"since":{"type":"string","format":"date-time","title":"Since"},"ip":{"type":"string","format":"ipvanyaddress","title":"Ip"},"port":{"type":"integer","title":"Port"},"transport":{"$ref":"#/components/schemas/TransportProtocol"}},"additionalProperties":false,"type":"object","required":["since","ip","port","transport"],"title":"ServiceHistoryRequest"},"ServiceHistoryResponse":{"properties":{"history":{"items":{"$ref":"#/components/schemas/ScannedServiceModel"},"type":"array","title":"History"}},"additionalProperties":true,"type":"object","required":["history"],"title":"ServiceHistoryResponse"},"ServiceQueryModel":{"properties":{"ip":{"anyOf":[{"type":"string","format":"ipv4"},{"type":"string","format":"ipv6"}],"title":"Ip","examples":["203.0.113.14"]},"service":{"$ref":"#/components/schemas/modat__backend__api__shared__models__service___Service"},"fqdns":{"items":{"type":"string"},"type":"array","uniqueItems":true,"title":"Fqdns","default":[],"examples":[["www.example.com"]]},"rdns":{"items":{"type":"string"},"type":"array","uniqueItems":true,"title":"Rdns","default":[],"examples":[["example.com"]]},"asn":{"anyOf":[{"$ref":"#/components/schemas/_ASN"},{"type":"null"}]},"geo":{"anyOf":[{"$ref":"#/components/schemas/_GEO"},{"type":"null"}]},"is_anycast":{"type":"boolean","title":"Is Anycast","default":false,"examples":[false]}},"additionalProperties":true,"type":"object","required":["ip","service"],"title":"ServiceQueryModel"},"ServiceSearchResponse":{"properties":{"page_nr":{"type":"integer","title":"Page Nr"},"total_pages":{"type":"integer","title":"Total Pages"},"total_records":{"type":"integer","title":"Total Records"},"page":{"items":{"$ref":"#/components/schemas/ServiceQueryModel"},"type":"array","title":"Page"},"facets":{"additionalProperties":{"items":{"$ref":"#/components/schemas/FacetBucket"},"type":"array"},"type":"object","title":"Facets","default":{}}},"additionalProperties":true,"type":"object","required":["page_nr","total_pages","total_records","page"],"title":"ServiceSearchResponse"},"TXTRecordModel":{"properties":{"ttl":{"type":"integer","title":"Ttl"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"deleted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Deleted At"},"value":{"type":"string","title":"Value"}},"type":"object","required":["ttl","created_at","value"],"title":"TXTRecordModel"},"TransportProtocol":{"type":"string","enum":["tcp","udp"],"title":"TransportProtocol"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"_ASN":{"properties":{"number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Number","examples":[64496]},"org":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Org","examples":["Example Networks"]},"netblock":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Netblock","examples":["203.0.113.0/24"]}},"additionalProperties":true,"type":"object","title":"_ASN"},"_AlgorithmSelection":{"properties":{"dh_kex_algorithm":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Dh Kex Algorithm","examples":["curve25519-sha256"]},"host_key_algorithm":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Host Key Algorithm","examples":["ecdsa-sha2-nistp256"]},"client_to_server_alg_group":{"anyOf":[{"$ref":"#/components/schemas/_ClientToServerAlgGroup"},{"type":"null"}]},"server_to_client_alg_group":{"anyOf":[{"$ref":"#/components/schemas/_ServerToClientAlgGroup"},{"type":"null"}]}},"additionalProperties":true,"type":"object","title":"_AlgorithmSelection"},"_ClientToServerAlgGroup":{"properties":{"cipher":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cipher","examples":["aes128-ctr"]},"mac":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mac","examples":["hmac-sha2-256"]},"compression":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Compression","examples":["none"]}},"additionalProperties":true,"type":"object","title":"_ClientToServerAlgGroup"},"_Curve25519Sha256Params":{"properties":{"server_public":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Server Public","examples":["9R5r27pEEOoK..."]}},"additionalProperties":true,"type":"object","title":"_Curve25519Sha256Params"},"_DetailedService":{"properties":{"transport":{"$ref":"#/components/schemas/TransportProtocol","examples":["tcp"]},"ports":{"items":{"type":"integer"},"type":"array","minItems":1,"uniqueItems":true,"title":"Ports","examples":[[443]]},"last_scanned_port":{"type":"integer","title":"Last Scanned Port","examples":[443]},"protocol":{"$ref":"#/components/schemas/NetworkProtocol","examples":["http"]},"scanned_at":{"type":"string","format":"date-time","title":"Scanned At","examples":["2026-10-01T08:15:42Z"]},"discovered_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Discovered At","examples":["2025-03-14T09:26:53Z"]},"is_match":{"type":"boolean","title":"Is Match","default":false,"examples":[false]},"banner":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Banner","examples":["HTTP/1.1 200 OK\r\nServer: nginx/1.24.0\r\n..."]},"banner_hex":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Banner Hex","examples":["485454502f31..."]},"banner_sha1":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Banner Sha1","examples":["b4e0fe1b4ecd..."]},"banner_sha256":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Banner Sha256","examples":["9370b66484d7..."]},"banner_mmh3":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Banner Mmh3","examples":[-460245392]},"ssh":{"anyOf":[{"$ref":"#/components/schemas/_SSH"},{"type":"null"}]},"tcp":{"anyOf":[{"$ref":"#/components/schemas/_TCP"},{"type":"null"}]},"tls":{"anyOf":[{"$ref":"#/components/schemas/_TLS"},{"type":"null"}]},"fingerprints":{"anyOf":[{"$ref":"#/components/schemas/_Fingerprints"},{"type":"null"}]},"http":{"anyOf":[{"$ref":"#/components/schemas/modat__backend__api__shared__models__service___HTTP"},{"type":"null"}]},"cves":{"items":{"$ref":"#/components/schemas/CVEHit"},"type":"array","title":"Cves","default":[]}},"additionalProperties":true,"type":"object","required":["transport","ports","last_scanned_port","protocol","scanned_at"],"title":"_DetailedService"},"_EcdsaPublicKey":{"properties":{"b":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"B","examples":["WsY12Ko6k+ez..."]},"curve":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Curve","examples":["P-256"]},"gx":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Gx","examples":["axfR8uEsQkf4..."]},"gy":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Gy","examples":["T+NC4v4af5uO..."]},"length":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Length","examples":[256]},"n":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"N","examples":["/////wAAAAD/..."]},"p":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"P","examples":["/////wAAAAEA..."]},"x":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X","examples":["LXEWQrcmsEQB..."]},"y":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Y","examples":["ofzkNjhU/4iM..."]}},"additionalProperties":true,"type":"object","title":"_EcdsaPublicKey"},"_FileHash":{"properties":{"path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Path","examples":["favicon.ico"]},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category","examples":["favicon"]},"sha256":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sha256","examples":["6478f183c80c..."]},"sha1":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sha1","examples":["6d5ebb360c23..."]},"mmh3":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Mmh3","examples":[-2042163332]}},"additionalProperties":true,"type":"object","title":"_FileHash"},"_Fingerprints":{"properties":{"os":{"anyOf":[{"$ref":"#/components/schemas/_OSFingerprint"},{"type":"null"}]},"service":{"anyOf":[{"$ref":"#/components/schemas/_ServiceFingerprint"},{"type":"null"}]},"technologies":{"items":{"$ref":"#/components/schemas/_TechnologyFingerprint"},"type":"array","title":"Technologies","default":[]},"tags":{"items":{"$ref":"#/components/schemas/FingerprintTags"},"type":"array","title":"Tags","default":[],"examples":[["Cloud"]]}},"additionalProperties":true,"type":"object","title":"_Fingerprints"},"_GEO":{"properties":{"city_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"City Name","examples":["Los Angeles"]},"country_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country Name","examples":["United States"]},"country_iso_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country Iso Code","examples":["US"]},"continent_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Continent Code","examples":["NA"]},"accuracy_radius":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Accuracy Radius","examples":[20]},"location":{"anyOf":[{"prefixItems":[{"type":"number"},{"type":"number"}],"type":"array","maxItems":2,"minItems":2},{"type":"null"}],"title":"Location","description":"Coordinates as [longitude, latitude].","examples":[[-118.2437,34.0522]]}},"additionalProperties":true,"type":"object","title":"_GEO"},"_KeyExchange":{"properties":{"curve25519_sha256_params":{"anyOf":[{"$ref":"#/components/schemas/_Curve25519Sha256Params"},{"type":"null"}]},"server_signature":{"anyOf":[{"$ref":"#/components/schemas/_ServerSignature"},{"type":"null"}]},"server_host_key":{"anyOf":[{"$ref":"#/components/schemas/_ServerHostKey"},{"type":"null"}]}},"additionalProperties":true,"type":"object","title":"_KeyExchange"},"_OSFingerprint":{"properties":{"product":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Product","examples":["Linux"]},"version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Version","examples":["4.15.0-91-generic"]},"arch":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Arch","deprecated":true,"examples":["x86_64"]},"icon_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Icon Url","readOnly":true}},"additionalProperties":true,"type":"object","required":["icon_url"],"title":"_OSFingerprint"},"_ParsedServerSignature":{"properties":{"algorithm":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Algorithm","examples":["ecdsa-sha2-nistp256"]},"value":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Value","examples":["Y7yXTpEmH3JN..."]}},"additionalProperties":true,"type":"object","title":"_ParsedServerSignature"},"_RsaPublicKey":{"properties":{"exponent":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Exponent","examples":[65537]},"modulus":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Modulus","examples":["yD+3FHgp5G3T..."]},"length":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Length","examples":[3072]}},"additionalProperties":true,"type":"object","title":"_RsaPublicKey"},"_SSH":{"properties":{"hassh":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Hassh","examples":["1a569b9923ea..."]},"server_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Server Id","examples":["SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.5"]},"server_key_exchange":{"anyOf":[{"$ref":"#/components/schemas/_ServerKeyExchange"},{"type":"null"}]},"algorithm_selection":{"anyOf":[{"$ref":"#/components/schemas/_AlgorithmSelection"},{"type":"null"}]},"key_exchange":{"anyOf":[{"$ref":"#/components/schemas/_KeyExchange"},{"type":"null"}]}},"additionalProperties":true,"type":"object","title":"_SSH"},"_ServerHostKey":{"properties":{"rsa_public_key":{"anyOf":[{"$ref":"#/components/schemas/_RsaPublicKey"},{"type":"null"}]},"ecdsa_public_key":{"anyOf":[{"$ref":"#/components/schemas/_EcdsaPublicKey"},{"type":"null"}]},"raw":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Raw","examples":["AAAAE2VjZHNh..."]},"algorithm":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Algorithm","examples":["ecdsa-sha2-nistp256"]},"fingerprint_sha256":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fingerprint Sha256","examples":["5693912237c0..."]}},"additionalProperties":true,"type":"object","title":"_ServerHostKey"},"_ServerKeyExchange":{"properties":{"cookie":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cookie","examples":["6RNPQ21jLp8C..."]},"kex_algorithms":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Kex Algorithms","examples":[["curve25519-sha256","ecdh-sha2-nistp256"]]},"host_key_algorithms":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Host Key Algorithms","examples":[["rsa-sha2-512","ecdsa-sha2-nistp256","ssh-ed25519"]]},"client_to_server_ciphers":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Client To Server Ciphers","examples":[["chacha20-poly1305@openssh.com","aes128-ctr"]]},"server_to_client_ciphers":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Server To Client Ciphers","examples":[["chacha20-poly1305@openssh.com","aes128-ctr"]]},"client_to_server_macs":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Client To Server Macs","examples":[["hmac-sha2-256-etm@openssh.com","hmac-sha2-256"]]},"server_to_client_macs":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Server To Client Macs","examples":[["hmac-sha2-256-etm@openssh.com","hmac-sha2-256"]]},"client_to_server_compression":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Client To Server Compression","examples":[["none","zlib@openssh.com"]]},"server_to_client_compression":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Server To Client Compression","examples":[["none","zlib@openssh.com"]]},"first_kex_follows":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"First Kex Follows","examples":[false]},"reserved":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Reserved","examples":[0]}},"additionalProperties":true,"type":"object","title":"_ServerKeyExchange"},"_ServerSignature":{"properties":{"parsed":{"anyOf":[{"$ref":"#/components/schemas/_ParsedServerSignature"},{"type":"null"}]},"raw":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Raw","examples":["AAAAE2VjZHNh..."]},"h":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"H","examples":["e94DthdLBqwi..."]}},"additionalProperties":true,"type":"object","title":"_ServerSignature"},"_ServerToClientAlgGroup":{"properties":{"cipher":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cipher","examples":["aes128-ctr"]},"mac":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mac","examples":["hmac-sha2-256"]},"compression":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Compression","examples":["none"]}},"additionalProperties":true,"type":"object","title":"_ServerToClientAlgGroup"},"_ServiceFingerprint":{"properties":{"product":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Product","examples":["nginx"]},"version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Version","examples":["1.24.0"]},"icon_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Icon Url","readOnly":true}},"additionalProperties":true,"type":"object","required":["icon_url"],"title":"_ServiceFingerprint"},"_TCP":{"properties":{"window_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Window Size","examples":[64240]}},"additionalProperties":true,"type":"object","title":"_TCP"},"_TLS":{"properties":{"is_self_signed":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Self Signed","examples":[false]},"is_trusted":{"type":"boolean","title":"Is Trusted","readOnly":true,"examples":[true]},"valid_from":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Valid From","examples":["2026-01-15T00:00:00Z"]},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At","examples":["2027-01-15T23:59:59Z"]},"validity_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Validity Seconds","examples":[31622399]},"supported_versions":{"items":{"type":"string"},"type":"array","title":"Supported Versions","default":[],"examples":[["TLSv1.2","TLSv1.3"]]},"fingerprint_sha256":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fingerprint Sha256","examples":["8f5d3efc29c1..."]},"fingerprint_sha1":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fingerprint Sha1","examples":["45093a77af1f..."]},"jarm":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Jarm","examples":["27d40d40d29d..."]},"ja3s":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ja3S","examples":["16ef95294706..."]},"ja4s":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ja4S","examples":["t130200_1302_a56c5b993250"]},"ja4x":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ja4X","examples":["a373a9f83c6b_7022c563de38_7bf9a7bf7029"]},"ja4scan_tls":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ja4Scan Tls","examples":["23100h0s02s0_7c13a278e8c0_1c9e3a2f5b70_se1s006a1441"]},"serial_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Serial Number","examples":["107493012886579137111675518179296345329"]},"issuer":{"anyOf":[{"$ref":"#/components/schemas/_TLSIssuer"},{"type":"null"}]},"subject":{"anyOf":[{"$ref":"#/components/schemas/_TLSSubject"},{"type":"null"}]},"extensions":{"anyOf":[{"$ref":"#/components/schemas/_TLSExtensions"},{"type":"null"}]},"raw":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Raw","examples":["Certificate:\n Data:\n Version: 3 (0x2)\n Issuer: C=US, O=Example Trust Services, CN=Example TLS ECC CA 2025\n Subject: C=US, O=Example Corp, CN=example.com\n"]},"is_valid":{"type":"boolean","title":"Is Valid","readOnly":true}},"additionalProperties":true,"type":"object","required":["is_valid","is_trusted"],"title":"_TLS"},"_TLSExtensions":{"properties":{"subject_alt_name":{"anyOf":[{"$ref":"#/components/schemas/_TLSGeneralNames"},{"type":"null"}]},"issuer_alt_name":{"anyOf":[{"$ref":"#/components/schemas/_TLSGeneralNames"},{"type":"null"}]}},"additionalProperties":true,"type":"object","title":"_TLSExtensions"},"_TLSGeneralNames":{"properties":{"dns":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Dns","examples":[["example.com","www.example.com"]]},"ipv4":{"anyOf":[{"items":{"type":"string","format":"ipv4"},"type":"array"},{"type":"null"}],"title":"Ipv4","examples":[["203.0.113.14"]]},"ipv6":{"anyOf":[{"items":{"type":"string","format":"ipv6"},"type":"array"},{"type":"null"}],"title":"Ipv6","examples":[["2001:db8::14"]]},"email":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Email","examples":[["hostmaster@example.com"]]},"uri":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Uri","examples":[["https://example.com/"]]},"directory":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Directory","examples":[["CN=example.com,O=Example Corp,C=US"]]},"other_names":{"anyOf":[{"items":{"$ref":"#/components/schemas/_TLSOtherName"},"type":"array"},{"type":"null"}],"title":"Other Names"}},"additionalProperties":true,"type":"object","title":"_TLSGeneralNames"},"_TLSIssuer":{"properties":{"common_name":{"items":{"type":"string"},"type":"array","title":"Common Name","examples":[["Example TLS ECC CA 2025"]]},"country":{"items":{"type":"string"},"type":"array","title":"Country","examples":[["US"]]},"organization":{"items":{"type":"string"},"type":"array","title":"Organization","examples":[["Example Trust Services"]]},"organizational_unit":{"items":{"type":"string"},"type":"array","title":"Organizational Unit","examples":[["Certification Authority"]]},"province":{"items":{"type":"string"},"type":"array","title":"Province","examples":[["Utah"]]},"locality":{"items":{"type":"string"},"type":"array","title":"Locality","examples":[["Salt Lake City"]]},"email_address":{"items":{"type":"string"},"type":"array","title":"Email Address","examples":[["pki@example.org"]]}},"additionalProperties":true,"type":"object","required":["common_name","country","organization","organizational_unit","province","locality","email_address"],"title":"_TLSIssuer"},"_TLSOtherName":{"properties":{"id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Id","examples":["1.3.6.1.4.1.311.20.2.3"]},"value":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Value","examples":["hostmaster@example.com"]}},"additionalProperties":true,"type":"object","title":"_TLSOtherName"},"_TLSSubject":{"properties":{"common_name":{"items":{"type":"string"},"type":"array","title":"Common Name","examples":[["Example TLS ECC CA 2025"]]},"country":{"items":{"type":"string"},"type":"array","title":"Country","examples":[["US"]]},"organization":{"items":{"type":"string"},"type":"array","title":"Organization","examples":[["Example Trust Services"]]},"organizational_unit":{"items":{"type":"string"},"type":"array","title":"Organizational Unit","examples":[["Certification Authority"]]},"province":{"items":{"type":"string"},"type":"array","title":"Province","examples":[["Utah"]]},"locality":{"items":{"type":"string"},"type":"array","title":"Locality","examples":[["Salt Lake City"]]},"email_address":{"items":{"type":"string"},"type":"array","title":"Email Address","examples":[["pki@example.org"]]}},"additionalProperties":true,"type":"object","required":["common_name","country","organization","organizational_unit","province","locality","email_address"],"title":"_TLSSubject"},"_TechnologyFingerprint":{"properties":{"name":{"type":"string","title":"Name","examples":["Nginx"]},"version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Version","examples":["1.24.0"]},"icon_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Icon Url","readOnly":true}},"additionalProperties":true,"type":"object","required":["name","icon_url"],"title":"_TechnologyFingerprint"},"modat__backend__api__shared__models__host___Service":{"properties":{"transport":{"$ref":"#/components/schemas/TransportProtocol"},"port":{"type":"integer","title":"Port","description":"Deprecated, use ports and last_scanned_port instead","deprecated":true},"ports":{"items":{"type":"integer"},"type":"array","minItems":1,"uniqueItems":true,"title":"Ports"},"last_scanned_port":{"type":"integer","title":"Last Scanned Port"},"protocol":{"$ref":"#/components/schemas/NetworkProtocol"},"scanned_at":{"type":"string","format":"date-time","title":"Scanned At"},"discovered_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Discovered At"},"is_match":{"type":"boolean","title":"Is Match","default":false}},"additionalProperties":true,"type":"object","required":["transport","port","ports","last_scanned_port","protocol","scanned_at"],"title":"_Service"},"modat__backend__api__shared__models__scanned_service___HTTP":{"properties":{"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title","examples":["Example Domain"]},"status_code":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Status Code","examples":[200]},"headers":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Headers","deprecated":true,"examples":["HTTP/1.1 200 OK\r\nServer: nginx/1.24.0\r\n..."]},"headers_sha256":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Headers Sha256","deprecated":true,"examples":["9370b66484d7..."]},"body":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Body","examples":["Example Domain

Example Domain

"]},"body_mmh3":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Body Mmh3","examples":[-347679834]},"body_sha1":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Body Sha1","examples":["07eef685b33b..."]},"body_sha256":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Body Sha256","examples":["1d1674606fc8..."]}},"additionalProperties":true,"type":"object","title":"_HTTP"},"modat__backend__api__shared__models__scanned_service___Service":{"properties":{"transport":{"$ref":"#/components/schemas/TransportProtocol","examples":["tcp"]},"port":{"type":"integer","title":"Port","examples":[443]},"banner":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Banner","examples":["HTTP/1.1 200 OK\r\nServer: nginx/1.24.0\r\n..."]},"banner_hex":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Banner Hex","examples":["485454502f31..."]},"banner_sha1":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Banner Sha1","examples":["b4e0fe1b4ecd..."]},"banner_sha256":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Banner Sha256","examples":["9370b66484d7..."]},"banner_mmh3":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Banner Mmh3","examples":[-460245392]},"protocol":{"$ref":"#/components/schemas/NetworkProtocol","examples":["http"]},"http":{"anyOf":[{"$ref":"#/components/schemas/modat__backend__api__shared__models__scanned_service___HTTP"},{"type":"null"}]},"ssh":{"anyOf":[{"$ref":"#/components/schemas/_SSH"},{"type":"null"}]},"tcp":{"anyOf":[{"$ref":"#/components/schemas/_TCP"},{"type":"null"}]},"tls":{"anyOf":[{"$ref":"#/components/schemas/_TLS"},{"type":"null"}]},"scanned_at":{"type":"string","format":"date-time","title":"Scanned At","examples":["2026-10-01T08:15:42Z"]},"discovered_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Discovered At","examples":["2025-03-14T09:26:53Z"]}},"additionalProperties":true,"type":"object","required":["transport","port","protocol","scanned_at"],"title":"_Service"},"modat__backend__api__shared__models__service___HTTP":{"properties":{"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title","examples":["Example Domain"]},"status_code":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Status Code","examples":[200]},"headers":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Headers","deprecated":true,"examples":["HTTP/1.1 200 OK\r\nServer: nginx/1.24.0\r\n..."]},"headers_sha256":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Headers Sha256","deprecated":true,"examples":["9370b66484d7..."]},"body":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Body","examples":["Example Domain

Example Domain

"]},"body_mmh3":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Body Mmh3","examples":[-347679834]},"body_sha1":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Body Sha1","examples":["07eef685b33b..."]},"body_sha256":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Body Sha256","examples":["1d1674606fc8..."]},"css_mmh3":{"anyOf":[{"items":{"type":"integer"},"type":"array"},{"type":"null"}],"title":"Css Mmh3","default":[],"deprecated":true,"examples":[[1438560789]]},"js_mmh3":{"items":{"type":"integer"},"type":"array","title":"Js Mmh3","default":[],"deprecated":true,"examples":[[797494100]]},"files":{"anyOf":[{"items":{"$ref":"#/components/schemas/_FileHash"},"type":"array"},{"type":"null"}],"title":"Files"}},"additionalProperties":true,"type":"object","title":"_HTTP"},"modat__backend__api__shared__models__service___Service":{"properties":{"transport":{"$ref":"#/components/schemas/TransportProtocol","examples":["tcp"]},"port":{"type":"integer","title":"Port","examples":[443]},"banner":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Banner","examples":["HTTP/1.1 200 OK\r\nServer: nginx/1.24.0\r\n..."]},"banner_hex":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Banner Hex","examples":["485454502f31..."]},"banner_sha1":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Banner Sha1","examples":["b4e0fe1b4ecd..."]},"banner_sha256":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Banner Sha256","examples":["9370b66484d7..."]},"banner_mmh3":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Banner Mmh3","examples":[-460245392]},"protocol":{"$ref":"#/components/schemas/NetworkProtocol","examples":["http"]},"http":{"anyOf":[{"$ref":"#/components/schemas/modat__backend__api__shared__models__service___HTTP"},{"type":"null"}]},"ssh":{"anyOf":[{"$ref":"#/components/schemas/_SSH"},{"type":"null"}]},"tcp":{"anyOf":[{"$ref":"#/components/schemas/_TCP"},{"type":"null"}]},"tls":{"anyOf":[{"$ref":"#/components/schemas/_TLS"},{"type":"null"}]},"scanned_at":{"type":"string","format":"date-time","title":"Scanned At","examples":["2026-10-01T08:15:42Z"]},"discovered_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Discovered At","examples":["2025-03-14T09:26:53Z"]},"fingerprints":{"anyOf":[{"$ref":"#/components/schemas/_Fingerprints"},{"type":"null"}]},"cves":{"items":{"$ref":"#/components/schemas/CVEHit"},"type":"array","title":"Cves","default":[]}},"additionalProperties":true,"type":"object","required":["transport","port","protocol","scanned_at"],"title":"_Service"}},"securitySchemes":{"APIKeyHeader":{"type":"apiKey","in":"header","name":"Authorization"}}}} \ No newline at end of file diff --git a/tests/test_modat.py b/tests/test_modat.py new file mode 100644 index 000000000..41fc52c3b --- /dev/null +++ b/tests/test_modat.py @@ -0,0 +1,415 @@ +"""Offline contract tests for MODAT; no API credentials or module server required.""" + +import base64 +import json +from unittest.mock import Mock, patch + +import pytest +from pymisp import MISPObject + +from misp_modules.modules.expansion import modat + +UUID = "12345678-1234-4234-8234-123456789012" + + +def attribute(typ="ip-dst", value="203.0.113.14"): + return {"type": typ, "value": value, "uuid": UUID} + + +def request(typ="ip-dst", value="203.0.113.14", **config): + return json.dumps({"attribute": attribute(typ, value), "config": {"apikey": "test-secret", **config}}) + + +def response(data, status=200, headers=None): + result = Mock(status_code=status, headers=headers or {}) + result.json.return_value = data + return result + + +VALID = { + "AS": "AS64496", + "domain": "example.com", + "hostname": "example.com", + "port": "443", + "x509-fingerprint-sha1": "a" * 40, + "x509-fingerprint-sha256": "a" * 64, + "jarm-fingerprint": "a" * 62, + "hasshserver-md5": "a" * 32, + "ssh-fingerprint": "a" * 64, + "favicon-mmh3": "-1234", + "vulnerability": "CVE-2023-44487", + "sha1": "a" * 40, + "sha256": "a" * 64, + "email": "admin@example.com", + "email-src": "admin@example.com", + "email-dst": "admin@example.com", + "target-email": "admin@example.com", + "target-org": "Example Org", + "ip-src": "203.0.113.14", + "ip-dst": "2001:db8::1", + "ip-src|port": "2001:db8::1|443", + "ip-dst|port": "203.0.113.14|443", + "domain|ip": "example.com|203.0.113.14", + "hostname|port": "example.com|443", + "url": "https://example.com/path", + "uri": "http://[2001:db8::1]:8080/x", + "link": "https://example.com/", + "filename|sha1": "favicon.ico|" + "a" * 40, + "filename|sha256": "favicon.ico|" + "a" * 64, + "text": "Example Webserver", + "hex": "0001ab", + "pattern-in-traffic": "OpenSSH", +} + + +@pytest.mark.parametrize("typ,value", VALID.items()) +def test_supported_inputs(typ, value): + assert typ in modat.introspection()["input"] + ip, query, zone = modat.build_query(attribute(typ, value)) + assert ip or query + assert query is None or len(query) <= 2000 + + +def test_every_advertised_type_has_a_valid_case(): + assert set(VALID) == set(modat.introspection()["input"]) + + +@pytest.mark.parametrize( + "typ,value", + [ + ("md5", "a" * 32), + ("ja3-fingerprint-md5", "a" * 32), + ("hassh-md5", "a" * 32), + ("x509-fingerprint-md5", "a" * 32), + ("ip-dst|port", "203.0.113.1|70000"), + ("ip-dst|port", "1.2.3.4|80|443"), + ("domain|ip", "evil.invalid|not-an-ip"), + ("domain", 'x" OR port=22'), + ("text", 'x" OR port=22'), + ("text", "x\\y"), + ("text", "hello\nworld"), + ("url", "file:///etc/passwd"), + ("uri", "relative/path"), + ("url", "https://user:pass@example.com"), + ("sha256", "abc"), + ("ssh-fingerprint", "SHA256:abc"), + ("AS", "AS-1"), + ("favicon-mmh3", "999999999999"), + ("vulnerability", "not-a-cve"), + ], +) +def test_invalid_inputs(typ, value): + with pytest.raises(ValueError): + modat.build_query(attribute(typ, value)) + + +def test_composite_constraints_and_network(): + assert modat.build_query(attribute("ip-dst|port", "2001:db8::1|8443"))[1] == 'ip="2001:db8::1" AND port=8443' + assert ( + modat.build_query(attribute("domain|ip", "example.com|203.0.113.1"))[1] + == 'fqdns="example.com" AND ip="203.0.113.1"' + ) + assert modat.build_query(attribute("ip-src", "203.0.113.1/24"))[1] == 'subnet="203.0.113.0/24"' + assert modat.build_query(attribute("domain", "example[.]com"))[2] == "example.com" + assert modat.build_query(attribute("domain", "bücher.example"))[2] == "xn--bcher-kva.example" + + +def test_search_pagination_and_bound(): + client = modat.ModatClient("secret", 5) + client.session.request = Mock( + side_effect=[ + response({"page": [{"n": i} for i in range(10)], "total_pages": 20}), + response({"page": [{"n": i} for i in range(10, 20)], "total_pages": 20}), + ] + ) + rows = client.search("/service/search/v1", "port=443", 12, 10) + assert len(rows) == 12 + assert client.session.request.call_count == 2 + args = client.session.request.call_args.kwargs + assert args["json"]["page"] == 2 + assert args["timeout"] == 5 + assert args["allow_redirects"] is False + client.session.close() + + +@pytest.mark.parametrize( + "status,message", + [ + (401, "authentication"), + (403, "subscription"), + (429, "Retry after 7"), + (422, "HTTP 422"), + (500, "HTTP 500"), + (302, "HTTP 302"), + ], +) +def test_errors_are_sanitized(status, message): + with patch.object( + modat.requests.Session, + "request", + return_value=response({"secret": "test-secret"}, status, {"Retry-After": "7"}), + ): + result = modat.handler(request()) + assert message in result["error"] + assert "test-secret" not in result["error"] + + +def test_invalid_requests_and_network_failures(): + assert modat.handler(False) is False + for raw in ("bad JSON", "[]", "{}", json.dumps({"attribute": attribute(), "config": {}})): + assert "error" in modat.handler(raw) + with patch.object(modat.requests.Session, "request", side_effect=modat.requests.Timeout("test-secret")): + assert "test-secret" not in modat.handler(request())["error"] + + +@pytest.mark.parametrize( + "config", + [{"page_size": 9}, {"max_results": 1001}, {"timeout": 0}, {"include_dns": "maybe"}, {"max_results": False}], +) +def test_invalid_config(config): + with patch.object(modat.requests.Session, "request") as call: + assert "error" in modat.handler(request(**config)) + call.assert_not_called() + + +def service(): + return { + "transport": "tcp", + "ports": [443, 8443], + "last_scanned_port": 443, + "protocol": "http", + "scanned_at": "2026-10-01T08:15:42Z", + "discovered_at": "2026-01-01T00:00:00Z", + "banner_mmh3": 0, + "tcp": {"window_size": 0}, + "tls": { + "fingerprint_sha256": "a" * 64, + "is_self_signed": False, + "is_trusted": True, + "subject": {"common_name": ["example.com"]}, + "extensions": {"subject_alt_name": {"dns": ["example.com"], "ipv6": ["2001:db8::1"]}}, + }, + "ssh": {"hassh": "b" * 32, "key_exchange": {"server_host_key": {"fingerprint_sha256": "c" * 64}}}, + "http": { + "status_code": 200, + "body_sha256": "d" * 64, + "files": [ + {"path": "favicon.ico", "category": "favicon", "sha256": "e" * 64, "mmh3": 0}, + {"path": "other.ico", "category": "favicon", "sha256": "f" * 64, "mmh3": 42}, + ], + }, + "fingerprints": {"service": {"product": "nginx", "version": "1.24.0"}, "tags": ["Cloud"]}, + "cves": [{"id": "CVE-2023-44487", "cvss": 0.0, "is_kev": False}], + "future_field": {"nested": "preserve-me"}, + } + + +def test_objects_references_and_original_data(): + parser = modat.ModatParser(attribute()) + host = { + "ip": "203.0.113.14", + "asn": {"number": 64496, "org": "Example", "netblock": "203.0.113.0/24"}, + "geo": {"location": [-118.24, 34.05], "country_iso_code": "US"}, + "is_anycast": False, + "fqdns": ["example.com"], + "services": [service(), service()], + } + parser.host(host) + result = parser.result()["results"] + names = [o["name"] for o in result["Object"]] + assert names.count("modat-service") == 1 + assert names.count("file") == 2 + assert set(("ip-port", "asn", "geolocation", "x509", "vulnerability", "software")) <= set(names) + custom = next(o for o in result["Object"] if o["name"] == "modat-service") + attrs = {a["object_relation"]: a for a in custom["Attribute"]} + assert attrs["tls-is-self-signed"]["value"] is False + assert str(attrs["banner-mmh3"]["value"]) == "0" + raw = json.loads(base64.b64decode(attrs["raw-json"]["data"])) + assert raw == service() + geo = next(o for o in result["Object"] if o["name"] == "geolocation") + coords = { + a["object_relation"]: float(a["value"]) + for a in geo["Attribute"] + if a["object_relation"] in ("latitude", "longitude") + } + assert coords["longitude"] == -118.24 and coords["latitude"] == 34.05 + ids = {a["uuid"] for a in result["Attribute"]} | {o["uuid"] for o in result["Object"]} + for obj in result["Object"]: + for ref in obj.get("ObjectReference", []): + assert ref["referenced_uuid"] in ids + for a in obj["Attribute"]: + assert a["to_ids"] is False + loaded = MISPObject( + obj["name"], + strict=True, + misp_objects_path_custom=modat.DATA_PATH if obj["name"].startswith("modat-") else None, + ) + loaded.from_dict(**obj) + loaded._validate() + assert len(result["Attribute"]) == 2 # input and raw host evidence + + +def test_dns_history_and_https_root_target(): + parser = modat.ModatParser(attribute("domain", "example.com")) + zone = { + "fqdn": "example.com", + "records": { + "a": [ + { + "ttl": 0, + "created_at": "2025-01-01T00:00:00Z", + "deleted_at": "2026-01-01T00:00:00Z", + "ip": "203.0.113.14", + } + ], + "https": [ + {"ttl": 3600, "created_at": "2025-01-01T00:00:00Z", "priority": 1, "target": ".", "params": 'alpn="h2"'} + ], + "soa": [ + { + "ttl": 3600, + "created_at": "2025-01-01T00:00:00Z", + "mname": "ns.example.com.", + "rname": "hostmaster.example.com.", + "serial": 1, + "refresh": 2, + "retry": 3, + "expire": 4, + "minimum": 0, + } + ], + }, + } + parser.dns(zone) + parser.dns(zone) + objects = parser.result()["results"]["Object"] + observations = [o for o in objects if o["name"] == "modat-dns-record"] + assert len(observations) == 3 + https = next(o for o in observations if any(a["value"] == "HTTPS" for a in o["Attribute"])) + assert next(a["value"] for a in https["Attribute"] if a["object_relation"] == "target") == "." + domain_ip = next(o for o in objects if o["name"] == "domain-ip") + assert not any(a["object_relation"] == "last-seen" for a in domain_ip["Attribute"]) + for obj in parser.event.objects: + obj._validate() + + +def test_direct_lookup_missing_and_dns_success(): + zone = { + "fqdn": "example.com", + "records": {"txt": [{"ttl": 0, "created_at": "2026-01-01T00:00:00Z", "value": "v=spf1 -all"}]}, + } + with patch.object( + modat.requests.Session, "request", side_effect=[response({"page": [], "total_pages": 0}), response(zone)] + ) as call: + result = modat.handler(request("domain", "example.com")) + assert "error" not in result + assert any(o["name"] == "modat-dns-record" for o in result["results"]["Object"]) + assert call.call_count == 2 + with patch.object(modat.requests.Session, "request", return_value=response({}, 404)): + result = modat.handler(request()) + assert result["results"]["Object"] == [] + + +def test_reverse_dns_lookup(): + zone = {"fqdn": "example.com", "records": {}} + with patch.object( + modat.requests.Session, + "request", + side_effect=[ + response({}, 404), + response({"page": [{"fqdn": "example.com"}], "total_pages": 1}), + response(zone), + ], + ) as call: + result = modat.handler(request(reverse_dns=True)) + assert "error" not in result + assert call.call_args_list[1].kwargs["json"]["query"] == 'a.ip="203.0.113.14"' + + +@pytest.mark.parametrize( + "data", [{"page": []}, {"page": "bad", "total_pages": 1}, {"errors": ["bad query"]}, {"data": []}] +) +def test_malformed_api_responses(data): + with patch.object(modat.requests.Session, "request", return_value=response(data)): + assert "error" in modat.handler(request("sha256", "a" * 64)) + + +def test_pinned_openapi_service_schema_is_fully_mapped(): + """Detect dropped fields, invalid attribute types and template drift against the vendor contract.""" + from pathlib import Path + + spec = json.loads((Path(__file__).parent / "test_files" / "modat_openapi.json").read_text()) + schemas = spec["components"]["schemas"] + paths = set() + + def sample(schema, path=""): + if "$ref" in schema: + return sample(schemas[schema["$ref"].split("/")[-1]], path) + if "anyOf" in schema: + return sample(next(item for item in schema["anyOf"] if item.get("type") != "null"), path) + if "properties" in schema: + return {key: sample(value, f"{path}.{key}" if path else key) for key, value in schema["properties"].items()} + if schema.get("type") == "array": + return [sample(schema["items"], path)] + paths.add(path) + if schema.get("format") == "date-time": + return "2026-01-01T00:00:00Z" + if schema.get("enum"): + return schema["enum"][0] + if path.endswith(("sha1", "sha256")): + return "a" * (40 if path.endswith("sha1") else 64) + if path.endswith(("ipv4", "ipv6")): + return "2001:db8::1" if path.endswith("ipv6") else "203.0.113.1" + if path.endswith(".dns"): + return "example.com" + if path.endswith((".email", ".email_address")): + return "admin@example.com" + if path.endswith(".uri") or path.endswith("icon_url"): + return "https://example.com/" + if path in ("port", "ports", "last_scanned_port"): + return 443 + if path == "ssh.hassh": + return "b" * 32 + if path == "tls.jarm": + return "c" * 62 + return {"boolean": False, "integer": 0, "number": 0.0}.get(schema.get("type"), "example") + + data = sample(schemas["_DetailedService"]) + assert paths == set(modat.SERVICE_MAPPING) + parser = modat.ModatParser(attribute()) + parser.host({"ip": "203.0.113.14", "services": [data]}) + service_object = next(o for o in parser.event.objects if o.name == "modat-service") + assert {a.object_relation for a in service_object.attributes} == { + path.replace(".", "-").replace("_", "-") for path in paths + } | {"ip", "raw-json"} + known_types = set(parser.source.describe_types["types"]) + for obj in parser.event.objects: + obj._validate() + for a in obj.attributes: + assert a.type in known_types + + +def test_object_templates_conform_to_misp_schema(): + from pathlib import Path + + import jsonschema + import pymisp + + schema = json.loads((Path(pymisp.__file__).parent / "data/misp-objects/schema_objects.json").read_text()) + for file in modat.DATA_PATH.glob("*/definition.json"): + jsonschema.Draft4Validator(schema).validate(json.loads(file.read_text())) + + +def test_search_single_port_and_incomplete_geo(): + row = { + "ip": "203.0.113.14", + "geo": {"accuracy_radius": 1}, + "service": {"port": 8443, "transport": "tcp", "protocol": "http", "scanned_at": "2026-01-01T00:00:00Z"}, + } + with patch.object(modat.requests.Session, "request", return_value=response({"page": [row], "total_pages": 1})): + result = modat.handler(request("port", "8443")) + assert "error" not in result + observations = [o for o in result["results"]["Object"] if o["name"] == "modat-service"] + assert len(observations) == 1 + assert any(a["object_relation"] == "ports" and str(a["value"]) == "8443" for a in observations[0]["Attribute"]) + assert not any(o["name"] == "geolocation" for o in result["results"]["Object"]) From 7032a3153f21552bc6b2ae5a6185be6e33dbe33d Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Mon, 12 Oct 2026 13:52:32 +0000 Subject: [PATCH 2/2] fix: [modat] Keep generated identities local to each enrichment --- documentation/expansion/modat.json | 5 +-- misp_modules/modules/expansion/modat.py | 13 +++---- tests/test_modat.py | 48 +++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/documentation/expansion/modat.json b/documentation/expansion/modat.json index a557da113..a9896508c 100644 --- a/documentation/expansion/modat.json +++ b/documentation/expansion/modat.json @@ -29,7 +29,8 @@ "Keep DNS TTL, created_at and deleted_at distinct. Never treat deleted_at as last_seen.", "Preserve array nesting in JSON; repeat scalar leaves in the typed observation. Web files also get individual file objects.", "Link observations to input and related infrastructure; deduplicate shared objects and set discovered attributes to_ids=false.", - "Bound search pagination; report rate limits and authentication errors without exposing response bodies or credentials." + "Bound search pagination; report rate limits and authentication errors without exposing response bodies or credentials.", + "Use fresh PyMISP UUIDs for generated objects and host attachments across enrichments; deduplicate identical evidence only within the current response." ], "input": { "format": "misp_standard", @@ -266,7 +267,7 @@ "Restart misp-modules, enable modat enrichment in MISP and set apikey. Configure optional result limits, timeout and DNS behavior as needed." ], "validation": { - "offline_tests": 77, + "offline_tests": 80, "command": "python -m pytest tests/test_modat.py -q", "live_authenticated": false } diff --git a/misp_modules/modules/expansion/modat.py b/misp_modules/modules/expansion/modat.py index 4ad3e4ee0..4319df396 100644 --- a/misp_modules/modules/expansion/modat.py +++ b/misp_modules/modules/expansion/modat.py @@ -7,7 +7,7 @@ import re from pathlib import Path from urllib.parse import quote, urlsplit -from uuid import NAMESPACE_URL, UUID, uuid5 +from uuid import UUID import requests from pymisp import MISPAttribute, MISPEvent, MISPObject @@ -287,6 +287,7 @@ def __init__(self, attribute): self.source.from_dict(**attribute) self.event.add_attribute(**self.source.to_dict()) self.objects = {} + self.host_attachments = {} self.zones = set() @staticmethod @@ -320,7 +321,7 @@ def put(self, obj, parent=None, relationship="associated-to"): ) key = (obj.name, signature) if key not in self.objects: - obj.uuid = uuid5(NAMESPACE_URL, obj.name + signature) + # Keep PyMISP's fresh identity; the cache only deduplicates this response. self.objects[key] = obj self.event.add_object(obj) obj = self.objects[key] @@ -391,16 +392,16 @@ def host(self, data): self.vulnerabilities(data.get("cves"), host.uuid) # Preserve extensions and all original nested data as evidence, without inventing attribute semantics. payload = base64.b64encode(json.dumps(data, sort_keys=True).encode()).decode() - raw_uuid = uuid5(NAMESPACE_URL, "modat-host-json" + payload) - if not any(str(a.uuid) == str(raw_uuid) for a in self.event.attributes): - self.event.add_attribute( + if payload not in self.host_attachments: + attachment = self.event.add_attribute( "attachment", value="modat-host.json", data=payload, - uuid=raw_uuid, to_ids=False, comment=f"MODAT observation for {ip}", ) + self.host_attachments[payload] = attachment.uuid + raw_uuid = self.host_attachments[payload] if not any(str(r.referenced_uuid) == str(raw_uuid) for r in host.references): host.add_reference(str(raw_uuid), "derived-from") services = data.get("services", [data["service"]] if "service" in data else []) diff --git a/tests/test_modat.py b/tests/test_modat.py index 41fc52c3b..3cccf2649 100644 --- a/tests/test_modat.py +++ b/tests/test_modat.py @@ -413,3 +413,51 @@ def test_search_single_port_and_incomplete_geo(): assert len(observations) == 1 assert any(a["object_relation"] == "ports" and str(a["value"]) == "8443" for a in observations[0]["Attribute"]) assert not any(o["name"] == "geolocation" for o in result["results"]["Object"]) + + +@pytest.mark.parametrize("source_uuid", [UUID, "abcdefab-1234-4234-8234-abcdefabcdef"]) +def test_identical_evidence_has_distinct_ids_across_enrichments(source_uuid): + host = {"ip": "203.0.113.14", "asn": {"number": 64496}, "services": [service()]} + zone = { + "fqdn": "example.com", + "records": {"a": [{"ip": "203.0.113.14", "ttl": 300, "created_at": "2026-01-01T00:00:00Z"}]}, + } + results = [] + for uuid in (UUID, source_uuid): + source = {**attribute(), "uuid": uuid} + parser = modat.ModatParser(source) + parser.host(host) + parser.dns(zone) + results.append(parser.result()["results"]) + + def generated_ids(result, source_id): + return ( + {o["uuid"] for o in result["Object"]} + | {a["uuid"] for o in result["Object"] for a in o["Attribute"]} + | {a["uuid"] for a in result["Attribute"] if a["uuid"] != source_id} + ) + + first_ids = generated_ids(results[0], UUID) + second_ids = generated_ids(results[1], source_uuid) + assert first_ids.isdisjoint(second_ids) + for result, source_id in zip(results, (UUID, source_uuid)): + local_ids = generated_ids(result, source_id) | {source_id} + assert any(a["type"] == "attachment" for a in result["Attribute"]) + for obj in result["Object"]: + for ref in obj.get("ObjectReference", []): + assert ref["referenced_uuid"] in local_ids + + +def test_duplicate_host_evidence_is_reused_within_response(): + parser = modat.ModatParser(attribute()) + host = {"ip": "203.0.113.14", "services": [service()]} + parser.host(host) + first = parser.result()["results"] + parser.host(host) + second = parser.result()["results"] + assert {o["uuid"] for o in first["Object"]} == {o["uuid"] for o in second["Object"]} + attachments = [a for a in second["Attribute"] if a["type"] == "attachment"] + assert len(attachments) == 1 + for obj in second["Object"]: + refs = [(r["referenced_uuid"], r["relationship_type"]) for r in obj.get("ObjectReference", [])] + assert len(refs) == len(set(refs))