Skip to content

Commit 91cc9dc

Browse files
[nmap] fix: correct parser when target selector key is manual (#4412)
Signed-off-by: Antoine MAZEAS <antoine.mazeas@filigran.io> Co-authored-by: Antoine MAZEAS <antoine.mazeas@filigran.io>
1 parent 70b19c8 commit 91cc9dc

File tree

8 files changed

+153
-18
lines changed

8 files changed

+153
-18
lines changed

.circleci/config.yml

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ jobs:
3838
- slack/notify:
3939
event: fail
4040
template: basic_fail_1
41-
test:
41+
test-nuclei:
4242
working_directory: ~/openaev
4343
docker:
4444
- image: cimg/python:3.13
@@ -62,6 +62,30 @@ jobs:
6262
working_directory: ~/openaev/nuclei
6363
name: Tests for nuclei injector
6464
command: python -m unittest
65+
test-nmap:
66+
working_directory: ~/openaev
67+
docker:
68+
- image: cimg/python:3.13
69+
steps:
70+
- checkout
71+
- setup_remote_docker
72+
- run:
73+
working_directory: ~/openaev/nmap
74+
name: Install dependencies for nmap injector
75+
command: pip install -r requirements.txt
76+
- run:
77+
working_directory: ~/openaev/nmap
78+
name: Overwrite pyoaev with correct version from CI
79+
command: |
80+
if [ "${CIRCLE_BRANCH}" = "main" ]; then
81+
pip install --force-reinstall git+https://github.com/OpenAEV-Platform/client-python.git@main
82+
else
83+
pip install --force-reinstall git+https://github.com/OpenAEV-Platform/client-python.git@release/current
84+
fi
85+
- run:
86+
working_directory: ~/openaev/nmap
87+
name: Tests for nmap injector
88+
command: python -m unittest
6589
build_1:
6690
working_directory: ~/openaev
6791
docker:
@@ -245,7 +269,8 @@ workflows:
245269
jobs:
246270
- ensure_formatting
247271
- linter
248-
- test
272+
- test-nmap
273+
- test-nuclei
249274
- build_1:
250275
filters:
251276
tags:
@@ -256,7 +281,8 @@ workflows:
256281
requires:
257282
- ensure_formatting
258283
- linter
259-
- test
284+
- test-nmap
285+
- test-nuclei
260286
filters:
261287
branches:
262288
only:
@@ -265,7 +291,8 @@ workflows:
265291
requires:
266292
- ensure_formatting
267293
- linter
268-
- test
294+
- test-nmap
295+
- test-nuclei
269296
filters:
270297
branches:
271298
only:

injector_common/injector_common/targets.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,11 @@ def extract_targets(
6464
)
6565

6666
elif selector_key == "manual":
67-
targets = [t.strip() for t in content[TARGETS_KEY].split(",") if t.strip()]
67+
targets = list(
68+
dict.fromkeys(
69+
[t.strip() for t in content[TARGETS_KEY].split(",") if t.strip()]
70+
)
71+
)
6872

6973
else:
7074
raise ValueError("No targets provided for this injection")

nmap/src/helpers/nmap_output_parser.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
from typing import Dict
22

3+
from injector_common.targets import TargetExtractionResult
4+
35

46
class NmapOutputParser:
5-
def parse(data: Dict, result: str, asset_list: []) -> Dict:
7+
@staticmethod
8+
def parse(data: Dict, result: str, target_results: TargetExtractionResult) -> Dict:
69
"""Parse nmap results and extract open ports."""
10+
asset_list = list(target_results.ip_to_asset_id_map.values())
11+
targets = target_results.targets or []
12+
713
run = result["nmaprun"]
814
if not isinstance(run["host"], list):
915
run["host"] = [run["host"]]
@@ -27,7 +33,10 @@ def parse(data: Dict, result: str, asset_list: []) -> Dict:
2733
port_result["host"] = host["address"]["@addr"]
2834
else:
2935
port_result["asset_id"] = None
30-
port_result["host"] = asset_list[idx]
36+
if idx < len(targets):
37+
port_result["host"] = targets[idx]
38+
else:
39+
port_result["host"] = None
3140
ports_scans_results.append(port_result)
3241

3342
return {

nmap/src/openaev_nmap.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,15 @@ def nmap_execution(self, start: float, data: Dict) -> Dict:
5353
target_results = Targets.extract_targets(
5454
selector_key, selector_property, data, self.helper
5555
)
56-
asset_list = list(target_results.ip_to_asset_id_map.values())
5756
# Deduplicate targets
58-
unique_targets = list(dict.fromkeys(target_results.targets))
57+
targets = target_results.targets
5958
# Handle empty targets as an error
60-
if not unique_targets:
59+
if not targets:
6160
message = f"No target identified for the property {TargetProperty[selector_property.upper()].value}"
6261
raise ValueError(message)
6362

6463
# Build Arguments to execute
65-
nmap_args = NmapCommandBuilder.build_args(contract_id, unique_targets)
64+
nmap_args = NmapCommandBuilder.build_args(contract_id, targets)
6665

6766
self.helper.injector_logger.info(
6867
"Executing nmap with command: " + " ".join(nmap_args)
@@ -90,7 +89,7 @@ def nmap_execution(self, start: float, data: Dict) -> Dict:
9089
jc = NmapProcess.js_execute(["jc", "--xml", "-p"], nmap_result)
9190
result = json.loads(jc.stdout.decode("utf-8").strip())
9291

93-
return NmapOutputParser.parse(data, result, asset_list)
92+
return NmapOutputParser.parse(data, result, target_results)
9493

9594
def process_message(self, data: Dict) -> None:
9695
start = time.time()

nmap/test/__init__.py

Whitespace-only changes.

nmap/test/helpers/__init__.py

Whitespace-only changes.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
from unittest import TestCase
2+
3+
from src.helpers.nmap_output_parser import NmapOutputParser
4+
5+
from injector_common.targets import TargetExtractionResult
6+
7+
8+
class NmapOutputParserTest(TestCase):
9+
def setUp(self):
10+
self.result_single_host = {
11+
"nmaprun": {
12+
"host": {
13+
"address": {"@addr": "172.16.5.10"},
14+
"ports": {
15+
"port": [
16+
{
17+
"@portid": "21",
18+
"state": {"@state": "open"},
19+
"service": {"@name": "ftp"},
20+
}
21+
]
22+
},
23+
}
24+
}
25+
}
26+
27+
# -------------------------------
28+
# Tests
29+
# -------------------------------
30+
31+
def test_parse_target_assets(self):
32+
"""Ensure target_selector='assets' uses asset_list and sets asset_id."""
33+
data = {"injection": {"inject_content": {"target_selector": "assets"}}}
34+
35+
result = NmapOutputParser.parse(
36+
data,
37+
self.result_single_host,
38+
TargetExtractionResult(
39+
ip_to_asset_id_map={"172.16.5.10": "asset-123"}, targets=[]
40+
),
41+
)
42+
43+
scan = result["outputs"]["scan_results"][0]
44+
45+
self.assertEqual(scan["asset_id"], "asset-123")
46+
self.assertEqual(scan["host"], "172.16.5.10")
47+
self.assertEqual(scan["port"], 21)
48+
self.assertEqual(scan["service"], "ftp")
49+
50+
def test_parse_target_asset_groups(self):
51+
"""Ensure target_selector='asset-groups' also uses asset_list."""
52+
data = {"injection": {"inject_content": {"target_selector": "asset-groups"}}}
53+
54+
result = NmapOutputParser.parse(
55+
data,
56+
self.result_single_host,
57+
TargetExtractionResult(
58+
ip_to_asset_id_map={"172.16.5.10": "group-asset-555"},
59+
targets=["172.16.5.10"],
60+
),
61+
)
62+
63+
scan = result["outputs"]["scan_results"][0]
64+
65+
self.assertEqual(scan["asset_id"], "group-asset-555")
66+
self.assertEqual(scan["host"], "172.16.5.10")
67+
self.assertEqual(scan["port"], 21)
68+
self.assertEqual(scan["service"], "ftp")
69+
70+
def test_parse_target_manual(self):
71+
"""Ensure target_selector='manual' sets asset_id=None."""
72+
data = {"injection": {"inject_content": {"target_selector": "manual"}}}
73+
74+
result = NmapOutputParser.parse(
75+
data,
76+
self.result_single_host,
77+
TargetExtractionResult(ip_to_asset_id_map={}, targets=["172.16.5.10"]),
78+
)
79+
80+
scan = result["outputs"]["scan_results"][0]
81+
82+
self.assertIsNone(scan["asset_id"])
83+
self.assertEqual(scan["host"], "172.16.5.10")
84+
85+
def test_parse_target_manual_None_values(self):
86+
"""Ensure target_selector='manual' sets asset_id=None."""
87+
data = {"injection": {"inject_content": {"target_selector": "manual"}}}
88+
89+
result = NmapOutputParser.parse(
90+
data,
91+
self.result_single_host,
92+
TargetExtractionResult(ip_to_asset_id_map={}, targets=[]),
93+
)
94+
95+
scan = result["outputs"]["scan_results"][0]
96+
97+
self.assertIsNone(scan["asset_id"])
98+
self.assertIsNone(scan["host"])

nuclei/nuclei/openaev_nuclei.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,16 +79,14 @@ def nuclei_execution(self, start: float, data: Dict) -> Dict:
7979
selector_key, selector_property, data, self.helper
8080
)
8181
# Deduplicate targets
82-
unique_targets = list(dict.fromkeys(target_results.targets))
82+
targets = target_results.targets
8383
# Handle empty targets as an error
84-
if not unique_targets:
84+
if not targets:
8585
message = f"No target identified for the property {TargetProperty[selector_property.upper()].value}"
8686
raise ValueError(message)
8787
# Build Arguments to execute
88-
nuclei_args = self.command_builder.build_args(
89-
contract_id, content, unique_targets
90-
)
91-
input_data = "\n".join(unique_targets).encode("utf-8")
88+
nuclei_args = self.command_builder.build_args(contract_id, content, targets)
89+
input_data = "\n".join(targets).encode("utf-8")
9290

9391
self.helper.injector_logger.info(
9492
"Executing nuclei with: " + " ".join(nuclei_args)

0 commit comments

Comments
 (0)