Skip to content

Commit 9b77fde

Browse files
nkbeastNK
andauthored
fix(parsers): escape HTML, unique section ids, balanced divs in json2html.py (#675)
Co-authored-by: NK <nk@localhost.localdomain>
1 parent 9832541 commit 9b77fde

3 files changed

Lines changed: 185 additions & 72 deletions

File tree

.github/workflows/PR-tests.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,9 @@ jobs:
324324
- name: Run linPEAS module metadata tests
325325
run: python3 -m unittest linPEAS.tests.test_modules_metadata
326326

327+
- name: Run PEASS parsers tests
328+
run: python3 -m unittest discover -s parsers/tests -p "test_*.py"
329+
327330
- name: Run linPEAS builder tests
328331
run: python3 -m unittest discover -s linPEAS/tests -p "test_*.py"
329332

parsers/json2html.py

Lines changed: 95 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,136 @@
1+
import html
2+
import itertools
13
import json
24
import sys
3-
import random
45

6+
_section_ids = itertools.count(1)
57

6-
def parse_json(json_data : object) -> str:
8+
9+
def parse_json(json_data: dict) -> str:
710
"""Parse the given json adding it to the HTML file"""
8-
11+
912
body = ""
10-
i=1
13+
i = 1
1114
for key, value in json_data.items():
12-
body += """\t\t<button type="button" class="btn" data-toggle="collapse" data-target="#demo"""+ str(i) + "\"><b>" + key + """</button></b><br>\n
13-
<div id="demo"""+ str(i)+ """\" class="collapse">\n"""
14-
i=i+1
15+
body += (
16+
'\t\t<button type="button" class="btn" data-toggle="collapse" data-target="#demo'
17+
+ str(i)
18+
+ '"><b>'
19+
+ html.escape(key)
20+
+ "</b></button><br>\n"
21+
f' <div id="demo{i}" class="collapse">\n'
22+
)
23+
i = i + 1
1524
for key1, value1 in value.items():
16-
17-
if(type(value1)==list):
18-
body+=parse_list(value1)
19-
20-
if((type(value1)==dict)):
21-
body+=parse_dict(value1)
22-
body+="\t\t\t</div>\n"
23-
25+
26+
if type(value1) == list:
27+
body += parse_list(value1)
28+
29+
if type(value1) == dict:
30+
body += parse_dict(value1)
31+
body += "\t\t\t</div>\n"
32+
2433
return body
2534

2635

2736
def parse_dict(json_dict: dict) -> str:
2837
"""Parse the given dict from the given json adding it to the HTML file"""
2938

30-
dict_text=""
39+
dict_text = ""
3140
for key, value in json_dict.items():
32-
n=random.randint(0,999999)
41+
n = next(_section_ids)
3342
infos = []
3443
for info in value["infos"]:
3544
if info.startswith("http"):
36-
infos.append(f"<a href='{info}'>{info}</a><br>\n")
45+
infos.append(f"<a href='{html.escape(info)}'>{html.escape(info)}</a><br>\n")
3746
else:
38-
infos.append(str(info) + "<br>\n")
39-
40-
dict_text += f'\t\t<button type="button" class="btn1" data-toggle="collapse" data-target="#lines{n}">{key}</button><br>\n'
41-
dict_text += '<i>' + "".join(infos) + '</i>'
47+
infos.append(html.escape(str(info)) + "<br>\n")
48+
49+
dict_text += (
50+
f'\t\t<button type="button" class="btn1" data-toggle="collapse" data-target="#lines{n}">'
51+
+ html.escape(key)
52+
+ "</button><br>\n"
53+
)
54+
dict_text += "<i>" + "".join(infos) + "</i>"
4255
dict_text += f'<div id="lines{n}" class="collapse1">\n'
43-
56+
4457
if value["lines"]:
45-
dict_text+="\n" + parse_list(value["lines"]) + "\n"
58+
dict_text += "\n" + parse_list(value["lines"]) + "\n"
4659

4760
if value["sections"]:
48-
dict_text+=parse_dict(value["sections"])
49-
61+
dict_text += parse_dict(value["sections"])
62+
63+
dict_text += "\t\t\t</div>\n"
64+
5065
return dict_text
5166

5267

5368
def parse_list(json_list: list) -> str:
5469
"""Parse the given list from the given json adding it to the HTML file"""
55-
color_text=""
56-
color_class=""
70+
color_text = ""
71+
color_class = ""
5772

5873
for i in json_list:
59-
if "═══" not in i['clean_text']:
60-
if(i['clean_text']):
61-
color_text+= "<div class = \""
62-
text = str(i['clean_text'])
63-
for color in i['colors']:
64-
if(color=='BLUE'):
65-
style = "#0000FF"
66-
color_class = "blue"
67-
if(color=='LIGHT_GREY'):
68-
style = "#adadad"
69-
color_class = "light_grey"
70-
if(color=='REDYELLOW'):
71-
style = "#FF0000; background-color: #FFFF00;"
72-
color_class = "redyellow"
73-
if(color=='RED'):
74-
style = "#FF0000"
75-
color_class = "red"
76-
if(color=='GREEN'):
77-
style = "#008000"
78-
color_class = "green"
79-
if(color=='MAGENTA'):
80-
style = "#FF00FF"
81-
color_class = "magenta"
82-
if(color=='YELLOW'):
83-
style = "#FFFF00"
84-
color_class = "yellow"
85-
if(color=='DARKGREY'):
86-
style = "#A9A9A9"
87-
color_class = "darkgrey"
88-
if(color=='CYAN'):
89-
style = "#00FFFF"
90-
color_class = "cyan"
91-
for replacement in i['colors'][color]:
92-
text=text.replace(replacement," <b style=\"color:"+ style +"\">"+ replacement + "</b>")
93-
#class=\""+ color_class + "\" "+ "
94-
if "═╣" in text:
95-
text=text.replace("═╣","<li>")
96-
text+="</li>"
97-
color_text+= "" + color_class + " "
98-
color_text +="no_color\" >"+ text + "<br></div>\n"
99-
return color_text + "\t\t\t</div>\n"
74+
if "═══" in i["clean_text"]:
75+
continue
76+
if not i["clean_text"]:
77+
continue
78+
79+
color_text += '<div class = "'
80+
text = html.escape(str(i["clean_text"]))
81+
style = ""
82+
for color in i["colors"]:
83+
if color == "BLUE":
84+
style = "#0000FF"
85+
color_class = "blue"
86+
if color == "LIGHT_GREY":
87+
style = "#adadad"
88+
color_class = "light_grey"
89+
if color == "REDYELLOW":
90+
style = "#FF0000; background-color: #FFFF00;"
91+
color_class = "redyellow"
92+
if color == "RED":
93+
style = "#FF0000"
94+
color_class = "red"
95+
if color == "GREEN":
96+
style = "#008000"
97+
color_class = "green"
98+
if color == "MAGENTA":
99+
style = "#FF00FF"
100+
color_class = "magenta"
101+
if color == "YELLOW":
102+
style = "#FFFF00"
103+
color_class = "yellow"
104+
if color == "DARKGREY":
105+
style = "#A9A9A9"
106+
color_class = "darkgrey"
107+
if color == "CYAN":
108+
style = "#00FFFF"
109+
color_class = "cyan"
110+
for replacement in i["colors"][color]:
111+
escaped = html.escape(replacement)
112+
text = text.replace(
113+
escaped, f' <b style="color:{style}">{escaped}</b>'
114+
)
115+
color_text += "" + color_class + " "
116+
117+
if "═╣" in text:
118+
text = text.replace("═╣", "<li>") + "</li>"
119+
color_text += 'no_color" >' + text + "<br></div>\n"
120+
return color_text
100121

101122

102123
def main():
124+
global _section_ids
125+
_section_ids = itertools.count(1)
103126
with open(JSON_PATH) as JSON_file:
104127
json_data = json.load(JSON_file)
105128
html = HTML_HEADER
106129
html += HTML_INIT_BODY
107130
html += parse_json(json_data)
108131
html += HTML_END
109-
110-
with open(HTML_PATH, 'w') as f:
132+
133+
with open(HTML_PATH, "w") as f:
111134
f.write(html)
112135

113136

@@ -341,7 +364,7 @@ def main():
341364
JSON_PATH = sys.argv[1]
342365
HTML_PATH = sys.argv[2]
343366
except IndexError as err:
344-
print("Error: Please pass the peas.json file and the path to save the html\npeas2html.py <json_file.json> <HTML_file.html>")
367+
print("Error: Please pass the peas.json file and the path to save the html\njson2html.py <json_file.json> <HTML_file.html>")
345368
sys.exit(1)
346369

347370
main()

parsers/tests/test_json2html.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import json
2+
import tempfile
3+
import unittest
4+
from pathlib import Path
5+
6+
from parsers import json2html
7+
8+
SAMPLE_JSON = {
9+
"System Information": {
10+
"sections": {
11+
"Operative system": {
12+
"sections": {},
13+
"lines": [
14+
{
15+
"raw_text": "[+] nmap is available",
16+
"clean_text": "[+] nmap is available & ready <test>",
17+
"colors": {"GREEN": ["nmap"]},
18+
},
19+
{
20+
"raw_text": "[-] /usr/bin/echo",
21+
"clean_text": "[-] /usr/bin/echo && chmod 2>/dev/null",
22+
"colors": {},
23+
},
24+
],
25+
"infos": ["https://example.com/check?x=1&y=2"],
26+
}
27+
},
28+
"infos": [],
29+
}
30+
}
31+
32+
33+
class Json2HtmlTests(unittest.TestCase):
34+
def setUp(self):
35+
self._tmpdir = tempfile.TemporaryDirectory()
36+
self._json_path = Path(self._tmpdir.name) / "peas.json"
37+
self._html_path = Path(self._tmpdir.name) / "peas.html"
38+
39+
def tearDown(self):
40+
self._tmpdir.cleanup()
41+
42+
def _render(self, data) -> str:
43+
self._json_path.write_text(json.dumps(data))
44+
json2html.JSON_PATH = str(self._json_path)
45+
json2html.HTML_PATH = str(self._html_path)
46+
json2html.main()
47+
return self._html_path.read_text()
48+
49+
def test_escapes_html_in_line_text(self):
50+
html = self._render(SAMPLE_JSON)
51+
self.assertIn("&lt;test&gt;", html)
52+
self.assertNotIn("ready <test>", html)
53+
self.assertIn("&amp;&amp;", html)
54+
self.assertNotIn("&& chmod", html)
55+
56+
def test_escapes_html_in_infos(self):
57+
html = self._render(SAMPLE_JSON)
58+
self.assertIn("x=1&amp;y=2", html)
59+
self.assertNotIn("x=1&y=2", html)
60+
61+
def test_escapes_section_names(self):
62+
data = {
63+
"A & B": {"sections": {}, "infos": []},
64+
}
65+
html = self._render(data)
66+
self.assertIn("A &amp; B", html)
67+
self.assertNotIn(">A & B</b>", html)
68+
69+
def test_colored_replacement_is_escaped_and_kept(self):
70+
html = self._render(SAMPLE_JSON)
71+
self.assertIn('style="color:#008000">nmap</b>', html)
72+
self.assertIn('class = "green no_color"', html)
73+
74+
def test_ids_are_deterministic_and_unique(self):
75+
first = self._render(SAMPLE_JSON)
76+
second = self._render(SAMPLE_JSON)
77+
self.assertEqual(first, second)
78+
self.assertEqual(first.count('id="lines1"'), 1)
79+
self.assertNotIn('id="lines0"', first)
80+
81+
def test_divs_are_balanced(self):
82+
html = self._render(SAMPLE_JSON)
83+
self.assertEqual(html.count("<div"), html.count("</div>"))
84+
85+
86+
if __name__ == "__main__":
87+
unittest.main()

0 commit comments

Comments
 (0)