diff --git a/boefjes/Makefile b/boefjes/Makefile index 09e5fc21b47..d95c05088e1 100644 --- a/boefjes/Makefile +++ b/boefjes/Makefile @@ -40,7 +40,7 @@ images: # Build the images for the containerized boefjes # docker build -f images/base.Dockerfile -t ghcr.io/minvws/openkat/dns-records --build-arg BOEFJE_PATH=./boefjes/plugins/kat_dns . docker build -f ./boefjes/plugins/kat_dnssec/boefje.Dockerfile -t ghcr.io/minvws/openkat/dns-sec:latest . docker build -f ./boefjes/plugins/kat_nmap_tcp/boefje.Dockerfile -t ghcr.io/minvws/openkat/nmap:latest . - + docker build -f ./images/base.Dockerfile -t ghcr.io/minvws/openkat/ssdp:latest --build-arg BOEFJE_PATH=./boefjes/plugins/kat_ssdp . ## ##|------------------------------------------------------------------------| diff --git a/boefjes/boefjes/plugins/kat_ssdp/__init__.py b/boefjes/boefjes/plugins/kat_ssdp/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/boefjes/boefjes/plugins/kat_ssdp/boefje.json b/boefjes/boefjes/plugins/kat_ssdp/boefje.json new file mode 100644 index 00000000000..a4a367a6fec --- /dev/null +++ b/boefjes/boefjes/plugins/kat_ssdp/boefje.json @@ -0,0 +1,10 @@ +{ + "id": "ssdp", + "name": "SSDP discovery", + "description": "Scan a local network using SSDP", + "consumes": [ + "Network" + ], + "scan_level": 1, + "oci_image": "ghcr.io/minvws/openkat/ssdp:latest" +} diff --git a/boefjes/boefjes/plugins/kat_ssdp/cover.jpg b/boefjes/boefjes/plugins/kat_ssdp/cover.jpg new file mode 100644 index 00000000000..2df117373c4 Binary files /dev/null and b/boefjes/boefjes/plugins/kat_ssdp/cover.jpg differ diff --git a/boefjes/boefjes/plugins/kat_ssdp/description.md b/boefjes/boefjes/plugins/kat_ssdp/description.md new file mode 100644 index 00000000000..f3a610025c2 --- /dev/null +++ b/boefjes/boefjes/plugins/kat_ssdp/description.md @@ -0,0 +1,13 @@ +# SSDP Scanner + +The Simple Service Discovery Protocol (SSDP) is a network protocol based on the Internet protocol suite for advertisement and discovery of network services and presence information. It accomplishes this without assistance of server-based configuration mechanisms, such as Dynamic Host Configuration Protocol (DHCP) or Domain Name System (DNS), and without special static configuration of a network host. SSDP is the basis of the discovery protocol of Universal Plug and Play (UPnP) and is intended for use in residential or small office environments + +https://en.wikipedia.org/wiki/Simple_Service_Discovery_Protocol + +### Input OOIs + +The SSDP scanner runs in a local network only, and needs no input. + +### Output OOIs + +SSDP returns devices, Urls and IPAddresses. diff --git a/boefjes/boefjes/plugins/kat_ssdp/main.py b/boefjes/boefjes/plugins/kat_ssdp/main.py new file mode 100644 index 00000000000..0004a18f988 --- /dev/null +++ b/boefjes/boefjes/plugins/kat_ssdp/main.py @@ -0,0 +1,26 @@ +import json +from os import getenv + +SEARCHTARGET_DEFAULT = "ssdp:all" +TIMEOUT_DEFAULT = 10 + + +def run_ssdp(search_targets: str, timeout: int) -> list[dict[str, str]]: + from ssdpy import SSDPClient + + client = SSDPClient() + return client.m_search(st=search_targets, mx=timeout) + + +def run(_) -> list[tuple[set, bytes | str]]: + return [ + ( + set(), + json.dumps( + run_ssdp( + getenv("SEARCHTARGET", SEARCHTARGET_DEFAULT), + int(getenv("TIMEOUT", TIMEOUT_DEFAULT)), + ) + ), + ) + ] diff --git a/boefjes/boefjes/plugins/kat_ssdp/normalize.py b/boefjes/boefjes/plugins/kat_ssdp/normalize.py new file mode 100644 index 00000000000..bd809351202 --- /dev/null +++ b/boefjes/boefjes/plugins/kat_ssdp/normalize.py @@ -0,0 +1,118 @@ +import json +import logging +from collections.abc import Iterator +from ipaddress import ip_address +from urllib.parse import urlparse + +from octopoes.models import OOI +from octopoes.models.ooi.dns.zone import Hostname +from octopoes.models.ooi.network import IPAddressV4, IPAddressV6, IPPort, Network, Protocol +from octopoes.models.ooi.scans import SSDPResponse +from octopoes.models.ooi.service import IPService, Service +from octopoes.models.ooi.web import HostnameHTTPURL, HTTPResource, IPAddressHTTPURL, WebScheme, Website + + +def run(input_ooi: dict, raw: bytes | str) -> Iterator[OOI]: + """parse SSDP output and yield relevant devices, urls and ips.""" + + ssdp_responses: list[dict[str, str]] = json.loads(raw) + + network = Network(name=input_ooi["name"]) + yield network + network_reference = network.reference + + logging.info("Parsing SSDP output for %s.", network) + for response in ssdp_responses: + url = None + try: + url = urlparse(response["location"]) + logging.info(url) + except KeyError as e: + logging.info("Probably found a response without location. Missing key: %s", e) + + yield SSDPResponse( + network=network.reference, + nt=response["nt"], + nts=response["nts"], + server=response["host"], + usn=response["usn"], + ) + + continue + + ip = None + hostname = None + + try: + service = Service(name=url.scheme) + yield service + + ip = ip_address(url.netloc.split(":")[0]) + + ip_ooi = ( + IPAddressV4(network=network_reference, address=ip) + if ip.version == 4 + else IPAddressV6(network=network_reference, address=ip) + ) + yield ip_ooi + + if url.port: + port = url.port + else: + port = 443 if url.scheme == "https" else 80 + + # Create the accompanying port + ip_port = IPPort(address=ip_ooi.reference, protocol=Protocol.TCP, port=port) + yield ip_port + + # create the service + ip_service = IPService(ip_port=ip_port.reference, service=service.reference) + yield ip_service + + except ValueError as e: + logging.info("Response probably contains a hostname instead of an ip. %s", e) + + hostname = Hostname(name=url.netloc, network=network_reference) + yield hostname + + if ip and ip_ooi: # These should always be both assigned or neither should be assigned + url_ooi = IPAddressHTTPURL( + network=network_reference, + scheme=WebScheme(url.scheme), + port=port, + path=url.path, + netloc=ip_ooi.reference, + ) + else: + if not hostname: + logging.error( + "Hostname didn't exist while ip also did not exist. This should not be possible. " + "With the location: %s", + response["location"], + ) + continue + + url_ooi = HostnameHTTPURL( + network=network_reference, + scheme=WebScheme(url.scheme), + port=port, + path=url.path, + netloc=hostname.reference, + ) + + website = Website(hostname=hostname.reference, ip_service=ip_service.reference) + yield website + + httpresource = HTTPResource(website=website.reference, web_url=url_ooi.reference) + yield httpresource + + yield url_ooi + + yield SSDPResponse( + web_url=url_ooi.reference, + network=network.reference, + nt=response["nt"], + nts=response["nts"], + server=response["host"], + usn=response["usn"], + ) diff --git a/boefjes/boefjes/plugins/kat_ssdp/normalizer.json b/boefjes/boefjes/plugins/kat_ssdp/normalizer.json new file mode 100644 index 00000000000..8cc03c9c7e2 --- /dev/null +++ b/boefjes/boefjes/plugins/kat_ssdp/normalizer.json @@ -0,0 +1,19 @@ +{ + "id": "kat_ssdp_normalize", + "name": "SSDP normalizer", + "consumes": [ + "boefje/ssdp" + ], + "produces": [ + "Network", + "SSDPResponse", + "Service", + "IPAddressV4", + "IPAddressV6", + "IPService", + "Hostname", + "Website", + "HTTPResource", + "HostnameHTTPURL" + ] +} diff --git a/boefjes/boefjes/plugins/kat_ssdp/requirements.txt b/boefjes/boefjes/plugins/kat_ssdp/requirements.txt new file mode 100644 index 00000000000..53b6687f1e0 --- /dev/null +++ b/boefjes/boefjes/plugins/kat_ssdp/requirements.txt @@ -0,0 +1 @@ +ssdpy==0.4.1 diff --git a/boefjes/boefjes/plugins/kat_ssdp/schema.json b/boefjes/boefjes/plugins/kat_ssdp/schema.json new file mode 100644 index 00000000000..76f01ea387f --- /dev/null +++ b/boefjes/boefjes/plugins/kat_ssdp/schema.json @@ -0,0 +1,17 @@ +{ + "title": "Arguments", + "type": "object", + "properties": { + "SEARCHTARGET": { + "title": "SEARCHTARGET", + "type": "string", + "description": "What search target to use. Defaults to ssdp:all, needs to be a valid ssdp query." + }, + "TIMEOUT": { + "title": "TIMEOUT", + "maxLength": 3, + "type": "string", + "description": "How long in seconds should we wait for answers, defaults to 10." + } + } +} diff --git a/octopoes/octopoes/models/ooi/scans.py b/octopoes/octopoes/models/ooi/scans.py index 73b156b8b20..68cf8850d32 100644 --- a/octopoes/octopoes/models/ooi/scans.py +++ b/octopoes/octopoes/models/ooi/scans.py @@ -1,6 +1,9 @@ from typing import Literal from octopoes.models import OOI, Reference +from octopoes.models.ooi.network import Network +from octopoes.models.ooi.web import WebURL +from octopoes.models.persistence import ReferenceField class ExternalScan(OOI): @@ -15,3 +18,19 @@ class ExternalScan(OOI): @classmethod def format_reference_human_readable(cls, reference: Reference) -> str: return reference.tokenized.name + + +class SSDPResponse(OOI): + """OOI holding information about a found response from SSDP. Example response https://wiki.wireshark.org/SSDP""" + + object_type: Literal["SSDPService"] = "SSDPService" + + _natural_key_attrs = ["network", "server", "usn"] + + web_url: Reference | None = ReferenceField(WebURL, default=None) + network: Reference = ReferenceField(Network) + + nt: str + nts: str + server: str + usn: str diff --git a/octopoes/octopoes/models/types.py b/octopoes/octopoes/models/types.py index 871620eb47d..db50f66bbfc 100644 --- a/octopoes/octopoes/models/types.py +++ b/octopoes/octopoes/models/types.py @@ -59,8 +59,8 @@ Network, ) from octopoes.models.ooi.question import Question +from octopoes.models.ooi.scans import ExternalScan, SSDPResponse from octopoes.models.ooi.reports import Report, ReportData, ReportRecipe -from octopoes.models.ooi.scans import ExternalScan from octopoes.models.ooi.service import IPService, Service, TLSCipher from octopoes.models.ooi.software import Software, SoftwareInstance from octopoes.models.ooi.web import ( @@ -138,7 +138,7 @@ MonitoringType = Application | Incident ConfigType = Config ReportsType = ReportData -ScanType = ExternalScan +ScanType = ExternalScan | SSDPResponse ConcreteOOIType = ( CertificateType