Skip to content

Commit e934f74

Browse files
authored
adding new output types (#1085)
* sarif fully done, dd.json little left * This is good to go now * pre-commit fixes * updated * removing redundancy and less i/o operations * ruff fixes * fixed tests for Path.open * rabbit suggestions * added relevant documentation * slight change in doc * removing empty files that were added by mistake * updated datatime format according to coderabbit's suggestions
1 parent 2fea1e4 commit e934f74

File tree

6 files changed

+149
-21
lines changed

6 files changed

+149
-21
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ logs.txt
2020
results.*
2121
.owasp-nettacker*
2222
.nettacker/data*
23+
.data*
24+
*.sarif
25+
*.dd.json
2326
*.DS_Store
2427
*.swp
2528

docs/Usage.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ Engine:
5555
--verbose-event enable verbose event to see state of each thread
5656
-V, --version show software version
5757
-o REPORT_PATH_FILENAME, --output REPORT_PATH_FILENAME
58-
save all logs in file (results.txt, results.csv, results.html, results.json)
58+
save all logs in file (results.txt, results.csv, results.html, results.json, results.sarif, results.dd.json)
5959
--graph GRAPH_NAME build a graph of all activities and information, you must use HTML output. available graphs:
6060
['d3_tree_v2_graph', 'd3_tree_v1_graph']
6161
-h, --help Show Nettacker Help Menu
@@ -203,7 +203,7 @@ usage: Nettacker [-L LANGUAGE] [-v] [--verbose-event] [-V] [-o REPORT_PATH_FILEN
203203
--verbose-event enable verbose event to see state of each thread
204204
-V, --version نمایش ورژن نرم افزار
205205
-o REPORT_PATH_FILENAME, --output REPORT_PATH_FILENAME
206-
ذخیره کردن کل لاگ ها در فایل (result.txt، result.html، results.json)
206+
ذخیره کردن کل لاگ ها در فایل (results.txt، results.html، results.csv, results.json, results.sarif, results.dd.json)
207207
--graph GRAPH_NAME ساخت گراف از همه فعالیت ها و اطلاعات، شما باید از خروجی HTML استفاده کنید. گراف های در دسترس:
208208
['d3_tree_v1_graph', 'd3_tree_v2_graph']
209209
-h, --help نشان دادن منوی کمک Nettacker
@@ -529,6 +529,22 @@ def nettacker_user_application_config():
529529
}
530530
```
531531

532+
* Nettacker supports five different output types for the final report
533+
534+
1. HTML (.html) -> This also renders the graph
535+
2. CSV (.csv)
536+
3. JSON (.json)
537+
4. SARIF (.sarif)
538+
5. DefectDojo compatible json (.dd.json)
539+
540+
These output types will help with integration with different softwares and dashboards. To set the output mode use the `-o` or `--output` flag
541+
542+
```
543+
python nettacker.py -i 192.168.1.1/24 --profile information_gathering -o report.sarif
544+
python nettacker.py -i 192.168.1.1/24 --profile information_gathering -o report.json
545+
python nettacker.py -i 192.168.1.1/24 --profile information_gathering --output report.dd.json
546+
```
547+
532548
# API and WebUI
533549
API and WebUI are new interfaces through which you can send your commands to Nettacker. Technically WebUI was developed based on the present API to demonstrate an example of the current API and can be used as another easier interface. To start using this feature, simply run `python nettacker.py --start-api`.
534550
```

nettacker/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
all_module_severity_and_desc = {}

nettacker/core/arg_parser.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import yaml
66

7+
from nettacker import all_module_severity_and_desc
78
from nettacker.config import version_info, Config
89
from nettacker.core.die import die_failure, die_success
910
from nettacker.core.ip import (
@@ -80,15 +81,18 @@ def load_modules(limit=-1, full_details=False):
8081
an array of all module names
8182
"""
8283
# Search for Modules
83-
8484
module_names = {}
8585
for module_name in sorted(Config.path.modules_dir.glob("**/*.yaml")):
8686
library = str(module_name).split("/")[-1].split(".")[0]
8787
category = str(module_name).split("/")[-2]
8888
module = f"{library}_{category}"
8989
contents = yaml.safe_load(TemplateLoader(module).open().split("payload:")[0])
9090
module_names[module] = contents["info"] if full_details else None
91-
91+
info = contents.get("info", {})
92+
all_module_severity_and_desc[module] = {
93+
"severity": info.get("severity", 0),
94+
"desc": info.get("description", ""),
95+
}
9296
if len(module_names) == limit:
9397
module_names["..."] = {}
9498
break

nettacker/core/graph.py

Lines changed: 116 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
import importlib
44
import json
55
import os
6+
import uuid
67
from datetime import datetime
8+
from pathlib import Path
79

810
import texttable
911

10-
from nettacker import logger
12+
from nettacker import logger, all_module_severity_and_desc
1113
from nettacker.config import Config, version_info
1214
from nettacker.core.die import die_failure
1315
from nettacker.core.messages import messages as _
@@ -119,6 +121,99 @@ def create_compare_text_table(results):
119121
return table.draw() + "\n\n"
120122

121123

124+
def create_dd_specific_json(all_scan_logs):
125+
severity_mapping = {1: "Info", 2: "Low", 3: "Medium", 4: "High", 5: "Critical"}
126+
127+
findings = []
128+
129+
for log in all_scan_logs:
130+
module_name = log["module_name"].strip()
131+
date = datetime.strptime(log["date"], "%Y-%m-%d %H:%M:%S.%f").strftime("%m/%d/%Y")
132+
port = str(log.get("port", "")).strip()
133+
impact = log.get("event", "").strip()
134+
severity_justification = log.get("json_event", "").strip()
135+
service = log.get("target", "").strip()
136+
unique_id = log.get("scan_id", uuid.uuid4().hex)
137+
138+
metadata = all_module_severity_and_desc.get(module_name, {})
139+
severity_raw = metadata.get("severity", 0)
140+
description = metadata.get("desc", "")
141+
if severity_raw >= 9:
142+
severity = severity_mapping[5]
143+
elif severity_raw >= 7:
144+
severity = severity_mapping[4]
145+
elif severity_raw >= 4:
146+
severity = severity_mapping[3]
147+
elif severity_raw > 0:
148+
severity = severity_mapping[2]
149+
else:
150+
severity = severity_mapping[1]
151+
152+
findings.append(
153+
{
154+
"date": date,
155+
"title": module_name,
156+
"description": description.strip(),
157+
"severity": severity,
158+
"param": port,
159+
"impact": impact,
160+
"severity_justification": severity_justification,
161+
"service": service,
162+
"unique_id_from_tool": unique_id,
163+
"static_finding": False,
164+
"dynamic_finding": True,
165+
}
166+
)
167+
168+
return json.dumps({"findings": findings}, indent=4)
169+
170+
171+
def create_sarif_report(all_scan_logs):
172+
"""
173+
Takes all_scan_logs and converts them to a SARIF based json
174+
format. The schema and version used are 2.1.0 linked below.
175+
The following conversions are made:
176+
ruleId: name of the module
177+
message: event value for each log in all_scan_logs
178+
locations.physicalLocations.artifactLocation.uri: target value
179+
webRequest.properties.json_event: json_event value for each log in all_scan_logs
180+
properties.scan_id: scan_id unique value for each run
181+
properties.date: date field specified in all_scan_logs
182+
"""
183+
184+
sarif_structure = {
185+
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
186+
"version": "2.1.0",
187+
"runs": [
188+
{
189+
"tool": {
190+
"driver": {
191+
"name": "Nettacker",
192+
"version": "0.4.0",
193+
"informationUri": "https://github.com/OWASP/Nettacker",
194+
}
195+
},
196+
"results": [],
197+
}
198+
],
199+
}
200+
201+
for log in all_scan_logs:
202+
sarif_result = {
203+
"ruleId": log["module_name"],
204+
"message": {"text": log["event"]},
205+
"locations": [{"physicalLocation": {"artifactLocation": {"uri": log["target"]}}}],
206+
"properties": {
207+
"scan_id": log["scan_id"],
208+
"date": log["date"],
209+
"json_event": log["json_event"],
210+
},
211+
}
212+
sarif_structure["runs"][0]["results"].append(sarif_result)
213+
214+
return json.dumps(sarif_structure, indent=2)
215+
216+
122217
def create_report(options, scan_id):
123218
"""
124219
sort all events, create log file in HTML/TEXT/JSON and remove old logs
@@ -179,25 +274,34 @@ def create_report(options, scan_id):
179274
+ "</p>"
180275
+ log_data.json_parse_js
181276
)
182-
with open(report_path_filename, "w", encoding="utf-8") as report_file:
277+
with Path(report_path_filename).open("w", encoding="utf-8") as report_file:
183278
report_file.write(html_table_content + "\n")
184-
report_file.close()
279+
280+
elif len(report_path_filename) >= 5 and report_path_filename[-8:].lower() == ".dd.json":
281+
with Path(report_path_filename).open("w", encoding="utf-8") as report_file:
282+
dd_content_json = create_dd_specific_json(all_scan_logs)
283+
report_file.write(dd_content_json + "\n")
284+
185285
elif len(report_path_filename) >= 5 and report_path_filename[-5:] == ".json":
186-
with open(report_path_filename, "w", encoding="utf-8") as report_file:
286+
with Path(report_path_filename).open("w", encoding="utf-8") as report_file:
187287
report_file.write(str(json.dumps(all_scan_logs)) + "\n")
188-
report_file.close()
288+
289+
elif len(report_path_filename) >= 6 and report_path_filename[-6:].lower() == ".sarif":
290+
with Path(report_path_filename).open("w", encoding="utf-8") as report_file:
291+
sarif_content = create_sarif_report(all_scan_logs)
292+
report_file.write(sarif_content + "\n")
293+
189294
elif len(report_path_filename) >= 5 and report_path_filename[-4:] == ".csv":
190295
keys = all_scan_logs[0].keys()
191-
with open(report_path_filename, "a") as csvfile:
296+
with Path(report_path_filename).open("a") as csvfile:
192297
writer = csv.DictWriter(csvfile, fieldnames=keys)
193298
writer.writeheader()
194299
for log_list in all_scan_logs:
195300
dict_data = {key: value for key, value in log_list.items() if key in keys}
196301
writer.writerow(dict_data)
197-
csvfile.close()
198302

199303
else:
200-
with open(report_path_filename, "w", encoding="utf-8") as report_file:
304+
with Path(report_path_filename).open("w", encoding="utf-8") as report_file:
201305
report_file.write(build_text_table(all_scan_logs))
202306

203307
log.write(build_text_table(all_scan_logs))
@@ -278,20 +382,20 @@ def get_modules_ports(item):
278382
len(fullpath) >= 4 and fullpath[-4:] == ".htm"
279383
):
280384
html_report = build_compare_report(compare_results)
281-
with open(fullpath, "w", encoding="utf-8") as compare_report:
385+
with Path(fullpath).open("w", encoding="utf-8") as compare_report:
282386
compare_report.write(html_report + "\n")
283387
elif len(fullpath) >= 5 and fullpath[-5:] == ".json":
284-
with open(fullpath, "w", encoding="utf-8") as compare_report:
388+
with Path(fullpath).open("w", encoding="utf-8") as compare_report:
285389
compare_report.write(str(json.dumps(compare_results)) + "\n")
286390
elif len(fullpath) >= 5 and fullpath[-4:] == ".csv":
287391
keys = compare_results.keys()
288-
with open(fullpath, "a") as csvfile:
392+
with Path(fullpath).open("a") as csvfile:
289393
writer = csv.DictWriter(csvfile, fieldnames=keys)
290394
if csvfile.tell() == 0:
291395
writer.writeheader()
292396
writer.writerow(compare_results)
293397
else:
294-
with open(fullpath, "w", encoding="utf-8") as compare_report:
398+
with Path(fullpath).open("w", encoding="utf-8") as compare_report:
295399
compare_report.write(create_compare_text_table(compare_results))
296400

297401
log.write(create_compare_text_table(compare_results))

tests/core/test_graph.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ def test_create_report_html(
130130
{"date": "now", "target": "x", "module_name": "mod", "port": 80, "json_event": "{}"}
131131
],
132132
)
133-
@patch("builtins.open", new_callable=mock_open)
133+
@patch("nettacker.core.graph.Path.open", new_callable=mock_open)
134134
@patch("nettacker.core.graph.submit_report_to_db")
135135
def test_json_report(mock_submit, mock_open_file, mock_get_logs):
136136
options = MagicMock()
@@ -148,7 +148,7 @@ def test_json_report(mock_submit, mock_open_file, mock_get_logs):
148148
],
149149
)
150150
@patch("csv.DictWriter")
151-
@patch("builtins.open", new_callable=mock_open)
151+
@patch("nettacker.core.graph.Path.open", new_callable=mock_open)
152152
@patch("nettacker.core.graph.submit_report_to_db")
153153
def test_csv_report(mock_submit, mock_open_file, mock_csv_writer, mock_get_logs):
154154
options = MagicMock()
@@ -168,7 +168,7 @@ def test_csv_report(mock_submit, mock_open_file, mock_csv_writer, mock_get_logs)
168168
],
169169
)
170170
@patch("nettacker.core.graph.build_text_table", return_value="text table")
171-
@patch("builtins.open", new_callable=mock_open)
171+
@patch("nettacker.core.graph.Path.open", new_callable=mock_open)
172172
@patch("nettacker.core.graph.submit_report_to_db")
173173
def test_text_report(mock_submit, mock_open_file, mock_build_text, mock_get_logs):
174174
options = MagicMock()
@@ -182,7 +182,7 @@ def test_text_report(mock_submit, mock_open_file, mock_build_text, mock_get_logs
182182
@patch("nettacker.core.graph.get_logs_by_scan_id")
183183
@patch("nettacker.core.graph.get_options_by_scan_id")
184184
@patch("nettacker.core.graph.build_compare_report", return_value="<html-report>")
185-
@patch("nettacker.core.graph.open", new_callable=mock_open)
185+
@patch("nettacker.core.graph.Path.open", new_callable=mock_open)
186186
@patch("nettacker.core.graph.os.path.normpath", side_effect=lambda x: x)
187187
@patch("nettacker.core.graph.os.path.join", side_effect=lambda *args: "/".join(args))
188188
@patch("nettacker.core.graph.create_compare_text_table", return_value="text-report")
@@ -267,7 +267,7 @@ def test_permission_error(mock_join, mock_norm, mock_opts, mock_logs):
267267
@patch("nettacker.core.graph.get_logs_by_scan_id")
268268
@patch("nettacker.core.graph.get_options_by_scan_id")
269269
@patch("nettacker.core.graph.create_compare_text_table", return_value="some-text")
270-
@patch("nettacker.core.graph.open", new_callable=mock_open)
270+
@patch("nettacker.core.graph.Path.open", new_callable=mock_open)
271271
def test_dict_options(mock_open_file, mock_text, mock_opts, mock_logs):
272272
dummy_log = {
273273
"target": "1.1.1.1",

0 commit comments

Comments
 (0)