Skip to content

Commit

Permalink
New Parser: Qualys Hacker Guardian
Browse files Browse the repository at this point in the history
  • Loading branch information
Maffooch committed Sep 19, 2024
1 parent a643544 commit 7c1807a
Show file tree
Hide file tree
Showing 8 changed files with 142 additions and 4 deletions.
5 changes: 1 addition & 4 deletions docker/entrypoint-unit-tests-devDocker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,7 @@ echo "Unit Tests"
echo "------------------------------------------------------------"

# Removing parallel and shuffle for now to maintain stability
python3 manage.py test unittests -v 3 --keepdb --no-input --exclude-tag="non-parallel" || {
exit 1;
}
python3 manage.py test unittests -v 3 --keepdb --no-input --tag="non-parallel" || {
python3 manage.py test unittests.test_parsers.TestParsers -v 3 --keepdb --no-input --exclude-tag="non-parallel" || {
exit 1;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: "Qualys Hacker Guardian Scan"
toc_hide: true
---
Qualys Hacker Guardian CSV export

### Sample Scan Data

Sample Qualys Scan scans can be found [here](https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans/qualys_hacker_guardiang ).
Empty file.
77 changes: 77 additions & 0 deletions dojo/tools/qualys_hacker_guardian/parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import csv
import io

from dateutil import parser as date_parser

from dojo.models import Finding, Endpoint


class QualysHackerGuardianParser:

Check failure on line 9 in dojo/tools/qualys_hacker_guardian/parser.py

View workflow job for this annotation

GitHub Actions / ruff-linting

Ruff (I001)

dojo/tools/qualys_hacker_guardian/parser.py:1:1: I001 Import block is un-sorted or un-formatted
"""Parser for Qualys HackerGuardian"""

# Severity mapping taken from
# https://qualysguard.qg2.apps.qualys.com/portal-help/en/malware/knowledgebase/severity_levels.htm
qualys_severity_lookup = {
"1": "Low",
"2": "Low",
"3": "Medium",
"4": "High",
"5": "High",
}

def get_scan_types(self):
return ["Qualys Hacker Guardian Scan"]

def get_label_for_scan_types(self, scan_type):
return "Qualys Hacker Guardian Scan"

def get_description_for_scan_types(self, scan_type):
return "Qualys Hacker Guardian report file can be imported in CSV format."

def get_endpoint(self, row):
host = row.get("HOSTNAME", row.get("IP"))
if (port := row.get("PORT")) is not None:
host += f":{port}"
if (protocol := row.get("PROTOCOL")) is not None:
host = f"{protocol}://{host}"

return host

def get_findings(self, filename, test):
if filename is None:
return ()
content = filename.read()
if isinstance(content, bytes):
content = content.decode("utf-8")
reader = csv.DictReader(io.StringIO(content), delimiter=",", quotechar='"')
dupes = {}
for row in reader:
endpoint = Endpoint.from_uri(self.get_endpoint(row))
finding = Finding(
title=row.get("VULN TITLE"),
severity=self.qualys_severity_lookup[row.get("Q_SEVERITY", 1)],
description=(
f'**Category**: {row.get("CATEGORY", "Unknown")}\n'
f'**Threat**: {row.get("THREAT", "No threat detected")}\n'
f'**Result**: {row.get("RESULT", "No threat detected")}\n'
),
date=date_parser.parse(row.get("LAST SCAN")),
impact=row.get("IMPACT"),
mitigation=row.get("SOLUTION"),
unique_id_from_tool=row.get("QID"),
dynamic_finding=True,
active=True,
nb_occurences=1,
)
finding.unsaved_endpoints.append(endpoint)

dupe_key = finding.unique_id_from_tool
if dupe_key in dupes:
finding = dupes[dupe_key]
if endpoint not in finding.unsaved_endpoints:
finding.unsaved_endpoints.append(endpoint)
finding.nb_occurences += 1
else:
dupes[dupe_key] = finding

return list(dupes.values())
5 changes: 5 additions & 0 deletions unittests/scans/qualys_hacker_guardian/many_finding.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"IP","HOSTNAME","LAST SCAN","QID","VULN TITLE","TYPE","SEVERITY","PORT","PROTOCOL","OPERATING SYSTEM","IS_PCI","FALSE POSITIVE STATUS","CVSS_BASE","Q_SEVERITY","THREAT","IMPACT","SOLUTION","CVSS_TEMPORAL","CATEGORY","RESULT","BUGTRAQID","CVEID"
"18.238.109.17","help.example.co","2024-09-16 04:00:30","150059","Reference to Windows file path is present in HTML","POTENTIAL","M","80","tcp","","Y","-","5.3","1","Windows specific file path was detected in the response.","The response may be an error response that disclosed a local file path. This may potentially be a sensitive information.","The content should be reviewed to determine whether it could be masked or removed.","4.7","Web Application","url: https://help.example.co/ matched: .toLowerCase().split(\ -\ ) c=b.join(\ _\ );return c}} {key:\ fetchQuery\ value:function i(a){var b=this c=this.props d=c.org e=c.domain f=this.getTransformedNavigatorLang() g=f?\ &lang=\ +f:\ \ h=\ https://\ +d.name+\ .api.\ +e+\ /p/v1/kb/deflection/search?term=\ +encodeURIComponent(a)+g;return fetch(h).then(function(a){return a.json()}).then(function(a){var c=a.data;if(c){var d=c.slice(0 5);b.setState({articl url: https://help.example.co/. matched: .toLowerCase().split(\ -\ ) c=b.join(\ _\ );return c}} {key:\ fetchQuery\ value:function i(a){var b=this c=this.props d=c.org e=c.domain f=this.getTransformedNavigatorLang() g=f?\ &lang=\ +f:\ \ h=\ https://\ +d.name+\ .api.\ +e+\ /p/v1/kb/deflection/search?term=\ +encodeURIComponent(a)+g;return fetch(h).then(function(a){return a.json()}).then(function(a){var c=a.data;if(c){var d=c.slice(0 5);b.setState({articl","-",""
"18.238.109.17","help.example.co","2024-09-16 04:00:30","150059","Reference to Windows file path is present in HTML","POTENTIAL","M","443","tcp","","Y","-","5.3","1","Windows specific file path was detected in the response.","The response may be an error response that disclosed a local file path. This may potentially be a sensitive information.","The content should be reviewed to determine whether it could be masked or removed.","4.7","Web Application","url: https://help.example.co/ matched: .toLowerCase().split(\ -\ ) c=b.join(\ _\ );return c}} {key:\ fetchQuery\ value:function i(a){var b=this c=this.props d=c.org e=c.domain f=this.getTransformedNavigatorLang() g=f?\ &lang=\ +f:\ \ h=\ https://\ +d.name+\ .api.\ +e+\ /p/v1/kb/deflection/search?term=\ +encodeURIComponent(a)+g;return fetch(h).then(function(a){return a.json()}).then(function(a){var c=a.data;if(c){var d=c.slice(0 5);b.setState({articl","-",""
"44.207.58.177","jt.example.co","2024-09-15 09:00:18","11827","HTTP Security Header Not Detected","CONFIRMED","M","443","tcp","","Y","-","5.3","2","This QID reports the absence of the following <A HREF= https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#tab=Headers TARGET= _blank >HTTP headers</A> according to <A HREF= https://cwe.mitre.org/data/definitions/693.html TARGET= _blank >CWE-693: Protection Mechanism Failure</A>:<BR> X-Content-Type-Options: This HTTP header will prevent the browser from interpreting files as a different MIME type to what is specified in the Content-Type HTTP header. <BR> Strict-Transport-Security: The HTTP Strict-Transport-Security response header (HSTS) allows web servers to declare that web browsers (or other complying user agents) should only interact with it using secure HTTPS connections and never via the insecure HTTP protocol.<P> <P>QID Detection Logic:<BR> This unauthenticated QID looks for the presence of the following HTTP responses:<BR> The Valid directives are as belows: X-Content-Type-Options: nosniff<P> Strict-Transport-Security: max-age=&lt; [;includeSubDomains]<P> ","Depending on the vulnerability being exploited an unauthenticated remote attacker could conduct cross-site scripting clickjacking or MIME-type sniffing attacks.","<B>Note:</B> To better debug the results of this QID it is requested that customers execute commands to simulate the following functionality: curl -lkL --verbose.<P> CWE-693: Protection Mechanism Failure mentions the following - The product does not use or incorrectly uses a protection mechanism that provides sufficient defense against directed attacks against the product. A &quot;missing&quot; protection mechanism occurs when the application does not define any mechanism against a certain class of attack. An &quot;insufficient&quot; protection mechanism might provide some defenses - for example against the most common attacks - but it does not protect against everything that is intended. Finally an &quot;ignored&quot; mechanism occurs when a mechanism is available and in active use within the product but the developer has not applied it in some code path.<P> Customers are advised to set proper <A HREF= https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options TARGET= _blank >X-Content-Type-Options</A> and <A HREF= https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security TARGET= _blank >Strict-Transport-Security</A> HTTP response headers.<P> Depending on their server software customers can set directives in their site configuration or Web.config files. Few examples are:<P> X-Content-Type-Options:<BR> Apache: Header always set X-Content-Type-Options: nosniff<P> HTTP Strict-Transport-Security:<BR> Apache: Header always set Strict-Transport-Security &quot;max-age=31536000; includeSubDomains&quot;<BR> Nginx: add_header Strict-Transport-Security max-age=31536000;<P> <B>Note: Network devices that include a HTTP/HTTPS console for administrative/management purposes often do not include all/some of the security headers. This is a known issue and it is recommend to contact the vendor for a solution. </B><P>","4.7","CGI","X-Content-Type-Options HTTP Header missing on port 443. GET / HTTP/1.1 Host: jt.example.co Connection: Keep-Alive User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:93.0) Gecko/20100101 Firefox/93.0 <html><head><title>jt edge server ver v1.42.0-1-g7a7022e4 / 2022-06-09T21:08:14.000000Z</title></head><body> <small> jt edge server ver v1.42.0-1-g7a7022e4 / 2022-06-09T21:08:14.000000Z. Configure jt (/configurator) </small> </body></html>Strict-Transport-Security HTTP Header missing on port 443. HTTP/1.1 200 OK Date: Sun 15 Sep 2024 09:12:26 GMT Content-Type: text/html; charset=utf-8 Content-Length: 274 Connection: keep-alive Server: nginx/1.21.6","-",""
"44.220.118.158","data.example.co","2024-09-16 04:00:30","150004","Predictable Resource Location Via Forced Browsing","CONFIRMED","M","80","tcp","","Y","-","5.3","2","A file directory or directory listing was discovered on the Web server. These resources are confirmed to be present based on our logic. Some of the content on these files might have sensitive information. <P>NOTE: Links found in 150004 are found by forced crawling so will not automatically be added to 150009 Links Crawled or the application site map. If links found in 150004 need to be tested they must be added as Explicit URI so they are included in scope and then will be reported in 150009. Once the link is added to be in scope (i.e. Explicit URI) this same link will no longer be reported for 150004.","The contents of this file or directory may disclose sensitive information.","It is advised to review the contents of the disclosed files. If the contents contain sensitive information please verify that access to this file or directory is permitted. If necessary remove it or apply access controls to it.","4.7","Web Application","url: https://data.example.co/wp-content/uploads/2023/01/image.png Payload: https://data.example.co/feed/image/ comment: Found this Vulnerability for redirect link: https://data.example.co/wp-content/uploads/2023/01/image.png. It was redirected from: https://data.example.co/feed/image/. Original URL is: https://data.example.co/feed/ matched: HTTP/1.1 200 OK url: https://data.example.co/wp-content/uploads/2023/08/download.svg Payload: https://data.example.co/feed/download/ comment: Found this Vulnerability for redirect link: https://data.example.co/wp-content/uploads/2023/08/download.svg. It was redirected from: https://data.example.co/feed/download/. Original URL is: https://data.example.co/feed/ matched: HTTP/1.1 200 OK url: https://data.example.co/test-flow-shopify-bw/ Payload: https://data.example.co:443/test/ comment: Found this Vulnerability for redirect link: https://data.example.co/test-flow-shopify-bw/. It was redirected from: https://data.example.co:443/test/. Original URL is: https://data.example.co:443/. matched: HTTP/1.1 200 OK url: https://data.example.co/wp-content/uploads/2023/08/users.svg Payload: https://data.example.co/feed/users/ comment: Found this Vulnerability for redirect link: https://data.example.co/wp-content/uploads/2023/08/users.svg. It was redirected from: https://data.example.co/feed/users/. Original URL is: https://data.example.co/feed/ matched: HTTP/1.1 200 OK","-",""
3 changes: 3 additions & 0 deletions unittests/scans/qualys_hacker_guardian/one_finding.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"IP","HOSTNAME","LAST SCAN","QID","VULN TITLE","TYPE","SEVERITY","PORT","PROTOCOL","OPERATING SYSTEM","IS_PCI","FALSE POSITIVE STATUS","CVSS_BASE","Q_SEVERITY","THREAT","IMPACT","SOLUTION","CVSS_TEMPORAL","CATEGORY","RESULT","BUGTRAQID","CVEID"
"18.238.109.17","help.example.co","2024-09-16 04:00:30","150059","Reference to Windows file path is present in HTML","POTENTIAL","M","80","tcp","","Y","-","5.3","1","Windows specific file path was detected in the response.","The response may be an error response that disclosed a local file path. This may potentially be a sensitive information.","The content should be reviewed to determine whether it could be masked or removed.","4.7","Web Application","url: https://help.example.co/ matched: .toLowerCase().split(\ -\ ) c=b.join(\ _\ );return c}} {key:\ fetchQuery\ value:function i(a){var b=this c=this.props d=c.org e=c.domain f=this.getTransformedNavigatorLang() g=f?\ &lang=\ +f:\ \ h=\ https://\ +d.name+\ .api.\ +e+\ /p/v1/kb/deflection/search?term=\ +encodeURIComponent(a)+g;return fetch(h).then(function(a){return a.json()}).then(function(a){var c=a.data;if(c){var d=c.slice(0 5);b.setState({articl url: https://help.example.co/. matched: .toLowerCase().split(\ -\ ) c=b.join(\ _\ );return c}} {key:\ fetchQuery\ value:function i(a){var b=this c=this.props d=c.org e=c.domain f=this.getTransformedNavigatorLang() g=f?\ &lang=\ +f:\ \ h=\ https://\ +d.name+\ .api.\ +e+\ /p/v1/kb/deflection/search?term=\ +encodeURIComponent(a)+g;return fetch(h).then(function(a){return a.json()}).then(function(a){var c=a.data;if(c){var d=c.slice(0 5);b.setState({articl","-",""
"18.238.109.17","help.example.co","2024-09-16 04:00:30","150059","Reference to Windows file path is present in HTML","POTENTIAL","M","443","tcp","","Y","-","5.3","1","Windows specific file path was detected in the response.","The response may be an error response that disclosed a local file path. This may potentially be a sensitive information.","The content should be reviewed to determine whether it could be masked or removed.","4.7","Web Application","url: https://help.example.co/ matched: .toLowerCase().split(\ -\ ) c=b.join(\ _\ );return c}} {key:\ fetchQuery\ value:function i(a){var b=this c=this.props d=c.org e=c.domain f=this.getTransformedNavigatorLang() g=f?\ &lang=\ +f:\ \ h=\ https://\ +d.name+\ .api.\ +e+\ /p/v1/kb/deflection/search?term=\ +encodeURIComponent(a)+g;return fetch(h).then(function(a){return a.json()}).then(function(a){var c=a.data;if(c){var d=c.slice(0 5);b.setState({articl","-",""
1 change: 1 addition & 0 deletions unittests/scans/qualys_hacker_guardian/zero_finding.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"IP","HOSTNAME","LAST SCAN","QID","VULN TITLE","TYPE","SEVERITY","PORT","PROTOCOL","OPERATING SYSTEM","IS_PCI","FALSE POSITIVE STATUS","CVSS_BASE","Q_SEVERITY","THREAT","IMPACT","SOLUTION","CVSS_TEMPORAL","CATEGORY","RESULT","BUGTRAQID","CVEID"
46 changes: 46 additions & 0 deletions unittests/tools/test_qualys_hacker_guardian_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from os import path

from dojo.models import Test
from dojo.tools.qualys_hacker_guardian.parser import QualysHackerGuardianParser
from unittests.dojo_test_case import DojoTestCase


class TestQualysHackerGuardianParser(DojoTestCase):

def test_qualys_hacker_guardian_parser_with_no_findings(self):
with open(path.join(path.dirname(__file__), "../scans/qualys_hacker_guardian/zero_finding.csv"), encoding="utf-8") as testfile:
parser = QualysHackerGuardianParser()
findings = parser.get_findings(testfile, Test())
self.assertEqual(0, len(findings))

def test_qualys_hacker_guardian_parser_with_one_findings(self):
with open(path.join(path.dirname(__file__), "../scans/qualys_hacker_guardian/one_finding.csv"), encoding="utf-8") as testfile:
parser = QualysHackerGuardianParser()
findings = parser.get_findings(testfile, Test())
self.assertEqual(1, len(findings))
finding = findings[0]
self.assertEqual("Low", finding.severity)
self.assertEqual("Reference to Windows file path is present in HTML", finding.title)
self.assertIsNotNone(finding.description)
self.assertEqual(len(finding.unsaved_endpoints), 2)

def test_qualys_hacker_guardian_parser_with_many_findings(self):
with open(path.join(path.dirname(__file__), "../scans/qualys_hacker_guardian/many_finding.csv"), encoding="utf-8") as testfile:
parser = QualysHackerGuardianParser()
findings = parser.get_findings(testfile, Test())
self.assertEqual(3, len(findings))
finding = findings[0]
self.assertEqual("Low", finding.severity)
self.assertEqual("Reference to Windows file path is present in HTML", finding.title)
self.assertIsNotNone(finding.description)
self.assertEqual(len(finding.unsaved_endpoints), 2)
finding = findings[1]
self.assertEqual("HTTP Security Header Not Detected", finding.title)
self.assertEqual("Low", finding.severity)
self.assertIsNotNone(finding.description)
self.assertEqual(len(finding.unsaved_endpoints), 1)
finding = findings[2]
self.assertEqual("Predictable Resource Location Via Forced Browsing", finding.title)
self.assertEqual("Low", finding.severity)
self.assertIsNotNone(finding.description)
self.assertEqual(len(finding.unsaved_endpoints), 1)

0 comments on commit 7c1807a

Please sign in to comment.