From 8bdb00502faa1f49ac3605c1a53f8c9ab830bdea Mon Sep 17 00:00:00 2001 From: Manabu Niseki Date: Tue, 24 Mar 2026 16:56:11 +0900 Subject: [PATCH 1/3] feat: support malicious API endpoint --- src/urlscan/pro/__init__.py | 19 +++++++++++++++++++ src/urlscan/types.py | 1 + tests/integration/pro/test_pro.py | 13 +++++++++++++ tests/unit/pro/test_pro.py | 20 ++++++++++++++++++++ 4 files changed, 53 insertions(+) create mode 100644 tests/integration/pro/test_pro.py diff --git a/src/urlscan/pro/__init__.py b/src/urlscan/pro/__init__.py index 3ac11de..2c6786d 100644 --- a/src/urlscan/pro/__init__.py +++ b/src/urlscan/pro/__init__.py @@ -2,10 +2,12 @@ from functools import cached_property from typing import BinaryIO +from urllib.parse import quote_plus from urlscan.client import BaseClient from urlscan.iterator import SearchIterator from urlscan.pro.visibility import Visibility +from urlscan.types import MaliciousObservableType from urlscan.utils import _compact from .brand import Brand @@ -276,3 +278,20 @@ def get_user(self) -> dict: """ return self.get_json("/api/v1/pro/username") + + def lookup_malicious_observable( + self, + type_: MaliciousObservableType, + value: str, + ): + """Look up how often an observable has been seen in malicious scan results. + + Returns: + dict: Malicious observable lookup result. + + Reference: + https://docs.urlscan.io/apis/urlscan-openapi/malicious/maliciouslookup + + """ + path = f"/api/v1/malicious/{type_}/{quote_plus(value)}" + return self.get_json(path) diff --git a/src/urlscan/types.py b/src/urlscan/types.py index dba64ef..04b6006 100644 --- a/src/urlscan/types.py +++ b/src/urlscan/types.py @@ -35,3 +35,4 @@ WatchedAttributeType = Literal[ "detections", "tls", "dns", "labels", "page", "meta", "ip" ] +MaliciousObservableType = Literal["url", "domain", "ip", "hostname"] diff --git a/tests/integration/pro/test_pro.py b/tests/integration/pro/test_pro.py new file mode 100644 index 0000000..e50635b --- /dev/null +++ b/tests/integration/pro/test_pro.py @@ -0,0 +1,13 @@ +import pytest + +from urlscan import Pro + + +@pytest.mark.integration +def test_lookup_malicious_observable(pro: Pro): + type_ = "url" + value = "https://example.com" + result = pro.lookup_malicious_observable(type_="url", value=value) + assert isinstance(result, dict) + assert result["type"] == type_ + assert result["observable"] in value diff --git a/tests/unit/pro/test_pro.py b/tests/unit/pro/test_pro.py index 81216be..c94c64e 100644 --- a/tests/unit/pro/test_pro.py +++ b/tests/unit/pro/test_pro.py @@ -35,3 +35,23 @@ def test_get_user(pro: Pro, httpserver: HTTPServer): got = pro.get_user() assert got == data + + +def test_lookup_malicious_observable(pro: Pro, httpserver: HTTPServer): + type_ = "url" + value = "https://example.com" + data = { + "type": type_, + "value": value, + "count": 5, + "lastSeen": "2024-06-01T12:00:00.000Z", + "firstSeen": "2024-01-01T08:00:00.000Z", + } + httpserver.expect_request( + # pytest-httpserver (werkzeug) normalizes URL path so no need to quote_plus it in the test + f"/api/v1/malicious/{type_}/{value}", + method="GET", + ).respond_with_json(data) + + got = pro.lookup_malicious_observable(type_, value) # type: ignore + assert got == data From 3de4687fbcd956d95ff3f4b4bcd9d39f7e5b76e3 Mon Sep 17 00:00:00 2001 From: Manabu Niseki Date: Tue, 24 Mar 2026 16:57:57 +0900 Subject: [PATCH 2/3] fix: ignore arg-type --- tests/unit/pro/test_pro.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/pro/test_pro.py b/tests/unit/pro/test_pro.py index c94c64e..7bf2eea 100644 --- a/tests/unit/pro/test_pro.py +++ b/tests/unit/pro/test_pro.py @@ -53,5 +53,5 @@ def test_lookup_malicious_observable(pro: Pro, httpserver: HTTPServer): method="GET", ).respond_with_json(data) - got = pro.lookup_malicious_observable(type_, value) # type: ignore + got = pro.lookup_malicious_observable(type_, value) # type: ignore[arg-type] assert got == data From 45c8c0ff50e1fcb0f7a409f968ededdab6d733e5 Mon Sep 17 00:00:00 2001 From: Manabu Niseki Date: Wed, 25 Mar 2026 09:17:21 +0900 Subject: [PATCH 3/3] Update src/urlscan/pro/__init__.py Co-authored-by: Florian Weingarten --- src/urlscan/pro/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/urlscan/pro/__init__.py b/src/urlscan/pro/__init__.py index 2c6786d..eeef013 100644 --- a/src/urlscan/pro/__init__.py +++ b/src/urlscan/pro/__init__.py @@ -284,7 +284,7 @@ def lookup_malicious_observable( type_: MaliciousObservableType, value: str, ): - """Look up how often an observable has been seen in malicious scan results. + """Look up how often an observable has been seen in malicious scan results and when it was first and last seen. Returns: dict: Malicious observable lookup result.