Skip to content

Commit

Permalink
Merge pull request #103 from Ostorlab/fix/empty_fingerprint
Browse files Browse the repository at this point in the history
No emitting empty fingerprints
  • Loading branch information
3asm authored Sep 1, 2024
2 parents 91a788f + ab04ed2 commit 01e7f76
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 20 deletions.
22 changes: 12 additions & 10 deletions agent/nmap_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,9 @@ def _emit_fingerprints(
for data in generators.get_services(scan_results):
if data.get("service") in BLACKLISTED_SERVICES:
continue
if data.get("product") is not None:
data_product = data.get("product")
data_banner = data.get("banner")
if data_product is not None and data_product != "":
logger.debug("sending results to selector %s", selector)
fingerprint_data = {
"host": data.get("host"),
Expand All @@ -432,12 +434,12 @@ def _emit_fingerprints(
"service": data.get("service"),
"port": data.get("port"),
"protocol": data.get("protocol"),
"library_name": data.get("product"),
"library_name": data_product,
"library_version": data.get("product_version"),
"detail": data.get("product"),
"detail": data_product,
}
self.emit(selector, fingerprint_data)
if data.get("banner") is not None:
if data_banner is not None and data_banner != "":
logger.debug("sending results to selector %s", selector)
fingerprint_data = {
"host": data.get("host"),
Expand All @@ -447,17 +449,17 @@ def _emit_fingerprints(
"service": data.get("service"),
"port": data.get("port"),
"protocol": data.get("protocol"),
"library_name": data.get("banner"),
"detail": data.get("banner"),
"library_name": data_banner,
"detail": data_banner,
}
self.emit(selector, fingerprint_data)
if domain_name is not None:
if data.get("product") is not None:
if data_product is not None and data_product != "":
msg_data = {
"name": domain_name,
"port": data.get("port"),
"schema": data.get("service"),
"library_name": data.get("product"),
"library_name": data_product,
"library_version": data.get("product_version"),
"library_type": "BACKEND_COMPONENT",
"detail": f"Nmap Detected {data.get('service')} on {domain_name}",
Expand All @@ -466,12 +468,12 @@ def _emit_fingerprints(
selector="v3.fingerprint.domain_name.service.library",
data=msg_data,
)
if data.get("banner") is not None:
if data_banner is not None and data_banner != "":
msg_data = {
"name": domain_name,
"port": data.get("port"),
"schema": data.get("service"),
"library_name": data.get("banner"),
"library_name": data_banner,
"library_version": None,
"library_type": "BACKEND_COMPONENT",
"detail": f"Nmap Detected {data.get('service')} on {domain_name}",
Expand Down
91 changes: 81 additions & 10 deletions tests/nmap_agent_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,56 @@
from agent import nmap_options
import pytest

SCAN_RESULT_NO_PRODUCT = {
"nmaprun": {
"@scanner": "nmap",
"@args": "nmap -sV -n -p 0-6111515135 -T3 -sS --script banner -sC -oX /tmp/xm -oN /tmp/normal photo-rw.som.cm",
"@start": "11423487",
"@startstr": "Sun Aug 32 24:56:27 2424",
"@version": "5.50",
"@xmloutputversion": "1.01",
"scaninfo": {
"@type": "syn",
"@protocol": "tcp",
"@numservices": "3223",
"@services": "0-62341",
},
"verbose": {"@level": "0"},
"debugging": {"@level": "0"},
"host": {
"@starttime": "1724553457",
"@endtime": "17243453456",
"status": {"@state": "up", "@reason": "syn-ack", "@reason_ttl": "19"},
"address": {"@addr": "184.11.11.13", "@addrtype": "ipv4"},
"hostnames": {"hostname": {"@name": "video-cf.twimg.com", "@type": "user"}},
"ports": {
"extraports": {
"@state": "filtered",
"@count": "62343",
"extrareasons": {"@reason": "no-responses", "@count": "62343"},
},
"port": [
{
"@protocol": "tcp",
"@portid": "80",
"state": {
"@state": "open",
"@reason": "syn-ack",
"@reason_ttl": "52",
},
"service": {
"@name": "http",
"@servicefp": 'SF-Port80-TCPdy>\\r\\n</html>\\r\\nRequest\\r\\nServer:\\x20cloudflare\\r\\nDate:\\x20Sun,\\x2025\\x20Aug\\x202024\\x2014:58:18\\x20GMT\\r\\nContent-Type:\\x20text/html\\r\\nContent-Length:\\x20")%dm\x20r>\\r\\n</body>\\r\\n</html>\\r\\n");',
"@method": "probed",
"@conf": "12",
},
}
],
},
},
}
}

JSON_OUTPUT = {
"nmaprun": {
"host": {
Expand Down Expand Up @@ -90,7 +140,7 @@ def testAgentLifecycle_whenScanRunsWithoutErrors_emitsBackMessagesAndVulnerabili

nmap_test_agent.process(ipv4_msg)

assert len(agent_mock) == 3
assert len(agent_mock) == 2
assert agent_mock[0].selector == "v3.asset.ip.v4.port.service"
assert agent_mock[1].selector == "v3.report.vulnerability"
assert agent_mock[1].data["risk_rating"] == "INFO"
Expand Down Expand Up @@ -120,7 +170,7 @@ def testAgentLifecycle_whenScanRunsWithoutErrors_emitsBackVulnerabilityMsg(

nmap_test_agent.process(ipv4_msg)

assert len(agent_mock) == 3
assert len(agent_mock) == 2
assert agent_mock[1].selector == "v3.report.vulnerability"
assert agent_mock[1].data["risk_rating"] == "INFO"
assert agent_mock[1].data["title"] == "Network Port Scan"
Expand All @@ -144,7 +194,7 @@ def testAgentLifecycle_whenLinkAssetAndScanRunsWithoutErrors_emitsBackMessagesAn

nmap_test_agent.process(link_msg)

assert len(agent_mock) == 5
assert len(agent_mock) == 3
assert any(m.selector == "v3.asset.ip.v4.port.service" for m in agent_mock) is True

assert any(m.selector == "v3.asset.domain_name.service" for m in agent_mock) is True
Expand Down Expand Up @@ -182,7 +232,7 @@ def testAgentEmitBanner_whenScanRunsWithoutErrors_emitsMsgWithBanner(

nmap_test_agent.process(ipv4_msg)

assert len(agent_mock) == 10
assert len(agent_mock) == 7
# check string in banner
assert "Dummy Banner 1" in agent_mock[0].data["banner"]
assert "Dummy Banner 2" in agent_mock[1].data["banner"]
Expand Down Expand Up @@ -216,7 +266,7 @@ def testAgentEmitBannerScanDomain_whenScanRunsWithoutErrors_emitsMsgWithBanner(

nmap_test_agent.process(domain_msg)

assert len(agent_mock) == 18
assert len(agent_mock) == 12
# check string in banner
assert any("Dummy Banner 1" in m.data.get("banner", "") for m in agent_mock) is True
assert any("Dummy Banner 2" in m.data.get("banner", "") for m in agent_mock) is True
Expand All @@ -240,7 +290,7 @@ def testAgentScanDomain_whenScanRunsWithoutErrors_emitsDomainService(

nmap_test_agent.process(domain_msg)

assert len(agent_mock) == 18
assert len(agent_mock) == 12
# check string in banner
assert (
any(
Expand Down Expand Up @@ -420,7 +470,7 @@ def testAgentNmapOptions_withMaxNetworkMask_scansEachSubnet(
nmap_test_agent.process(ipv4_msg2)

# 4 is count of IPs in a /30.
assert len(agent_mock) == 10 * 4
assert len(agent_mock) == 7 * 4
# check string in banner
assert "Dummy Banner 1" in agent_mock[0].data["banner"]
assert "Dummy Banner 2" in agent_mock[1].data["banner"]
Expand All @@ -444,7 +494,7 @@ def testAgentProcessMessage_whenASubnetIsTargetdAfterABiggerRangeIsPreviouslySca

nmap_test_agent.process(ipv4_msg_with_mask)
# first scan must pass.
assert len(agent_mock) == 12
assert len(agent_mock) == 8

# subnet /27 of /24.
msg = message.Message.from_data(
Expand All @@ -455,7 +505,7 @@ def testAgentProcessMessage_whenASubnetIsTargetdAfterABiggerRangeIsPreviouslySca
nmap_test_agent.process(msg)

# scan subnet must not send any extra messages.
assert len(agent_mock) == 12
assert len(agent_mock) == 8


def testAgentEmitBannerScanDomain_withMultiplehostnames_reportVulnerabilities(
Expand Down Expand Up @@ -650,7 +700,7 @@ def testNmapAgentLifecycle_whenIpv6WithHostBits_agentShouldNotCrash(

nmap_test_agent.process(ipv6_msg)

assert len(agent_mock) == 3
assert len(agent_mock) == 2
assert agent_mock[0].selector == "v3.asset.ip.v6.port.service"
assert agent_mock[1].selector == "v3.report.vulnerability"
assert agent_mock[1].data["risk_rating"] == "INFO"
Expand Down Expand Up @@ -1143,3 +1193,24 @@ def testAgentLifecycle_whenDomainTCPWrappedService_emitsNoService(
nmap_test_agent.process(domain_msg)

assert len(agent_mock) == 0


def testAgentNmapOptions_whenServiceHasNoProduct_reportsFingerprintzzz(
nmap_test_agent: nmap_agent.NmapAgent,
agent_mock: List[message.Message],
agent_persist_mock: Dict[Union[str, bytes], Union[str, bytes]],
domain_msg: message.Message,
mocker: plugin.MockerFixture,
fake_output_product: None | Dict[str, str],
) -> None:
"""Unittest for the full life cycle of the agent : case where the nmap scan runs without errors,
the agents emits back messages of type service with banner.
"""
mocker.patch(
"agent.nmap_wrapper.NmapWrapper.scan_domain",
return_value=(SCAN_RESULT_NO_PRODUCT, HUMAN_OUTPUT),
)

nmap_test_agent.process(domain_msg)

assert any("fingerprint" in msg.selector for msg in agent_mock) is False

0 comments on commit 01e7f76

Please sign in to comment.