Skip to content

Commit b5f87a9

Browse files
committed
feat(tests): comprehensive unit tests for common helpers
1 parent 6510b99 commit b5f87a9

4 files changed

Lines changed: 253 additions & 4 deletions

File tree

jiraffe/__main__.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import sys
55
import argparse
66
import json
7-
import textwrap
87

98
# version guard
109
if sys.version_info[0] < 3:
@@ -18,7 +17,14 @@
1817
from jiraffe.http import HttpClient
1918
from jiraffe.exploits import load_exploits
2019
from jiraffe.recons import load_recon_modules
21-
from jiraffe.common import isjira, getversion, uparse, get_deployment_type, host_info, color_severity
20+
from jiraffe.common import (
21+
isjira,
22+
getversion,
23+
uparse,
24+
get_deployment_type,
25+
host_info,
26+
color_severity
27+
)
2228
from jiraffe.compat import is_compatible
2329
from jiraffe.enums import Severity
2430

jiraffe/compat.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"CVE-2017-9506": (Version("6.0.0"), Version("7.13.0")),
1010
"CVE-2019-8449": (Version("7.0.0"), Version("8.4.0")),
1111
"CVE-2019-8451": (Version("7.0.0"), Version("8.4.0")),
12-
"CVE-2019-11581": (Version("8.0.0"), Version("8.5.0")),
12+
"CVE-2019-11581": (Version("8.0.0"), Version("8.5.0"))
1313
}
1414

1515
def is_compatible(cve: str, jira_version: str) -> bool:

tests/test_common_helpers.py

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
4+
import unittest
5+
from unittest.mock import MagicMock, patch
6+
from jiraffe.common import (
7+
normalize_version,
8+
color_severity,
9+
uparse,
10+
host_info,
11+
isaws,
12+
get_deployment_type,
13+
getversion
14+
)
15+
from jiraffe.compat import is_compatible
16+
from jiraffe.enums import Severity
17+
from jiraffe.style import Style
18+
19+
20+
class TestCommonHelpers(unittest.TestCase):
21+
"""
22+
Tests for helpers in jiraffe.common.
23+
24+
These tests focus on pure helpers and heuristic logic.
25+
Network, DNS, and HTTP calls are mocked to ensure predictable behaviour.
26+
"""
27+
28+
def test_normalize_version_extracts_semver_from_parentheses(self):
29+
"""
30+
normalize_version() should extract a semantic version
31+
from strings containing parentheses or prefixes.
32+
"""
33+
self.assertEqual(normalize_version("(v8.13.4)"), "8.13.4")
34+
35+
def test_normalize_version_extracts_major_minor(self):
36+
"""
37+
normalize_version() should extract major.minor versions
38+
when patch versions are not present.
39+
"""
40+
self.assertEqual(normalize_version("JIRA 7.6"), "7.6")
41+
42+
def test_normalize_version_empty_string(self):
43+
"""
44+
normalize_version() should return None when given
45+
an empty string.
46+
"""
47+
self.assertIsNone(normalize_version(""))
48+
49+
def test_normalize_version_none(self):
50+
"""
51+
normalize_version() should return None when input is None.
52+
"""
53+
self.assertIsNone(normalize_version(None))
54+
55+
def test_normalize_version_no_match(self):
56+
"""
57+
normalize_version() should return None when
58+
no semantic version is present.
59+
"""
60+
self.assertIsNone(normalize_version("no version here"))
61+
62+
def test_color_severity_known_levels(self):
63+
"""
64+
color_severity() should return the correct Style
65+
wrapper for known severity levels.
66+
"""
67+
self.assertIs(color_severity("HIGH"), Style.ORANGE)
68+
self.assertIs(color_severity("MEDIUM"), Style.YELLOW)
69+
self.assertIs(color_severity("LOW"), Style.GREEN)
70+
self.assertIs(color_severity("INFO"), Style.CYAN)
71+
self.assertIs(color_severity("CRITICAL"), Style.MAGENTA)
72+
73+
def test_color_severity_unknown_level(self):
74+
"""
75+
color_severity() should default to Style.RESET
76+
for unknown severity values.
77+
"""
78+
self.assertIs(color_severity("UNKNOWN"), Style.RESET)
79+
80+
def test_style_wrap_disabled(self):
81+
"""
82+
Style._wrap() should return raw text
83+
when styling is disabled.
84+
"""
85+
Style.ENABLED = False
86+
self.assertEqual(Style.RED("test"), "test")
87+
Style.ENABLED = True
88+
89+
def test_uparse_preserves_valid_url(self):
90+
"""
91+
uparse() should return a normalized URL
92+
without modifying a valid Jira URL.
93+
"""
94+
url = "https://jira.example.com/secure/Dashboard.jspa"
95+
self.assertEqual(uparse(url), url)
96+
97+
def test_uparse_missing_scheme(self):
98+
"""
99+
uparse() should return the original string
100+
if the URL is missing a scheme or netloc.
101+
"""
102+
self.assertEqual(uparse("jira.example.com"), "jira.example.com")
103+
104+
@patch("socket.gethostbyname", side_effect=Exception())
105+
def test_host_info_resolution_failure(self, _):
106+
"""
107+
host_info() should return (None, None)
108+
if DNS resolution fails.
109+
"""
110+
ip, rdns = host_info("https://nonexistent.invalid")
111+
self.assertIsNone(ip)
112+
self.assertIsNone(rdns)
113+
114+
def test_isaws_detects_aws_hostname(self):
115+
"""
116+
isaws() should return True when the hostname
117+
matches known AWS patterns.
118+
"""
119+
client = MagicMock()
120+
self.assertTrue(
121+
isaws("https://ec2-1-2-3-4.compute.amazonaws.com", client)
122+
)
123+
124+
@patch("jiraffe.common.host_info", return_value=("1.2.3.4", "ec2.amazonaws.com"))
125+
def test_isaws_detects_aws_reverse_dns(self, _):
126+
"""
127+
isaws() should return True when reverse DNS
128+
indicates an Amazon AWS host.
129+
"""
130+
client = MagicMock()
131+
self.assertTrue(isaws("https://example.com", client))
132+
133+
def test_isaws_http_server_header(self):
134+
"""
135+
isaws() should return True when the HTTP Server
136+
header indicates an AWS ELB/ALB.
137+
"""
138+
client = MagicMock()
139+
response = MagicMock()
140+
response.headers = {"Server": "awselb/2.0"}
141+
client.get.return_value = response
142+
143+
self.assertTrue(isaws("https://example.com", client))
144+
145+
def test_isaws_non_aws_target(self):
146+
"""
147+
isaws() should return False for non-AWS targets.
148+
"""
149+
client = MagicMock()
150+
client.get.side_effect = Exception()
151+
self.assertFalse(isaws("https://example.com", client))
152+
153+
def test_get_deployment_type_success(self):
154+
"""
155+
get_deployment_type() should return the deploymentType
156+
field when the Jira serverInfo endpoint responds successfully.
157+
"""
158+
client = MagicMock()
159+
response = MagicMock()
160+
response.status_code = 200
161+
response.json.return_value = {"deploymentType": "Server"}
162+
client.get.return_value = response
163+
164+
result = get_deployment_type("https://jira.example.com", client)
165+
self.assertEqual(result, "Server")
166+
167+
def test_get_deployment_type_failure(self):
168+
"""
169+
get_deployment_type() should return None
170+
if the request fails or an exception is raised.
171+
"""
172+
client = MagicMock()
173+
client.get.side_effect = Exception()
174+
175+
self.assertIsNone(
176+
get_deployment_type("https://jira.example.com", client)
177+
)
178+
179+
def test_getversion_from_meta_tag(self):
180+
"""
181+
getversion() should extract the Jira version
182+
from HTML meta tags when present.
183+
"""
184+
client = MagicMock()
185+
response = MagicMock()
186+
response.text = """
187+
<html>
188+
<head>
189+
<meta name="ajs-version-number" content="8.20.1">
190+
</head>
191+
</html>
192+
"""
193+
client.get.return_value = response
194+
195+
result = getversion("https://jira.example.com", client)
196+
self.assertEqual(result, "8.20.1")
197+
198+
def test_getversion_returns_none_when_no_version_found(self):
199+
"""
200+
getversion() should return None when no
201+
version indicators are found.
202+
"""
203+
client = MagicMock()
204+
response = MagicMock()
205+
response.text = "<html><body>No Jira here</body></html>"
206+
client.get.return_value = response
207+
208+
self.assertIsNone(getversion("https://jira.example.com", client))
209+
210+
def test_severity_enum_values(self):
211+
"""
212+
Severity enum should expose all expected values.
213+
"""
214+
self.assertEqual(Severity.LOW.value, "LOW")
215+
self.assertEqual(Severity.CRITICAL.value, "CRITICAL")
216+
217+
def test_is_compatible_unknown_cve(self):
218+
"""
219+
is_compatible() should return True
220+
for unknown CVE identifiers.
221+
"""
222+
self.assertTrue(is_compatible("CVE-2099-9999", "9.0.0"))
223+
224+
225+
def test_is_compatible_in_range(self):
226+
"""
227+
is_compatible() should return True
228+
when Jira version is within the vulnerable range.
229+
"""
230+
self.assertTrue(is_compatible("CVE-2019-8449", "8.2.0"))
231+
232+
233+
def test_is_compatible_out_of_range(self):
234+
"""
235+
is_compatible() should return False
236+
when Jira version is outside the vulnerable range.
237+
"""
238+
self.assertFalse(is_compatible("CVE-2019-8449", "8.10.0"))

tests/test_recon.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22
# -*- coding: utf-8 -*-
33

44
import unittest
5-
from jiraffe.common import uparse, getversion, isjira, isaws
5+
from jiraffe.common import (
6+
uparse,
7+
getversion,
8+
isjira,
9+
isaws
10+
)
611
from jiraffe.http import HttpClient
712

813

0 commit comments

Comments
 (0)