diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..f9789ad --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,26 @@ +default_stages: [commit] +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.1.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-json + - id: check-yaml + args: [ --unsafe ] + - id: debug-statements +- repo: https://gitlab.com/pycqa/flake8 + rev: 3.8.3 + hooks: + - id: flake8 + additional_dependencies: [flake8-typing-imports==1.9.0] +- repo: https://github.com/ikamensh/flynt/ + rev: '0.56' + hooks: + - id: flynt + args: [ -df ] +- repo: https://github.com/asottile/pyupgrade + rev: v2.29.0 + hooks: + - id: pyupgrade + args: [ --py3-plus , --py36-plus] diff --git a/CHANGELOG/1.8.0/add_invicti.md b/CHANGELOG/1.8.0/add_invicti.md new file mode 100644 index 0000000..8af2ce4 --- /dev/null +++ b/CHANGELOG/1.8.0/add_invicti.md @@ -0,0 +1 @@ +[Add] Add invicti plugin diff --git a/CHANGELOG/1.8.0/add_nessus_sc_plugin.md b/CHANGELOG/1.8.0/add_nessus_sc_plugin.md new file mode 100644 index 0000000..dc993c2 --- /dev/null +++ b/CHANGELOG/1.8.0/add_nessus_sc_plugin.md @@ -0,0 +1 @@ +[Add] Add nessus_sc plugin diff --git a/CHANGELOG/1.8.0/fix_nexpose_full.md b/CHANGELOG/1.8.0/fix_nexpose_full.md new file mode 100644 index 0000000..717e566 --- /dev/null +++ b/CHANGELOG/1.8.0/fix_nexpose_full.md @@ -0,0 +1 @@ +[FIX] Remove cvss_vector from refs in nexpose_full diff --git a/CHANGELOG/1.8.0/fix_nikto.md b/CHANGELOG/1.8.0/fix_nikto.md new file mode 100644 index 0000000..c818fe6 --- /dev/null +++ b/CHANGELOG/1.8.0/fix_nikto.md @@ -0,0 +1 @@ +Add new identifier_tag to nikto plugin diff --git a/CHANGELOG/1.8.0/fix_plugins.md b/CHANGELOG/1.8.0/fix_plugins.md new file mode 100644 index 0000000..1374707 --- /dev/null +++ b/CHANGELOG/1.8.0/fix_plugins.md @@ -0,0 +1 @@ +[FIX] Now plugins check if ref field is already a dictionary diff --git a/CHANGELOG/1.8.0/mod_grype.md b/CHANGELOG/1.8.0/mod_grype.md new file mode 100644 index 0000000..59b26c7 --- /dev/null +++ b/CHANGELOG/1.8.0/mod_grype.md @@ -0,0 +1,3 @@ +[MOD] Improve grype plugin for dockers images and change report_belong_to method for +json plugins to check if json_keys is a list, in that case iterate the list and try if +any of them create a match. diff --git a/CHANGELOG/current/date.md b/CHANGELOG/current/date.md new file mode 100644 index 0000000..7dc3755 --- /dev/null +++ b/CHANGELOG/current/date.md @@ -0,0 +1 @@ +Oct 26th, 2022 diff --git a/RELEASE.md b/RELEASE.md index e883eea..22cf2a3 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,3 +1,14 @@ +1.8.0 [Oct 26th, 2022]: +--- + * [Add] Add invicti plugin + * [Add] Add nessus_sc plugin + * [FIX] Remove cvss_vector from refs in nexpose_full + * Add new identifier_tag to nikto plugin + * [FIX] Now plugins check if ref field is already a dictionary + * [MOD] Improve grype plugin for dockers images and change report_belong_to method for +json plugins to check if json_keys is a list, in that case iterate the list and try if +any of them create a match. + 1.7.0 [Sep 5th, 2022]: --- * Add CWE to PluginBase. The plugins that have this implemented are the following: diff --git a/faraday_plugins/__init__.py b/faraday_plugins/__init__.py index 0e1a38d..b280975 100644 --- a/faraday_plugins/__init__.py +++ b/faraday_plugins/__init__.py @@ -1 +1 @@ -__version__ = '1.7.0' +__version__ = '1.8.0' diff --git a/faraday_plugins/plugins/plugin.py b/faraday_plugins/plugins/plugin.py index 163b596..4867777 100644 --- a/faraday_plugins/plugins/plugin.py +++ b/faraday_plugins/plugins/plugin.py @@ -428,7 +428,11 @@ """ refs = [] if ref: - refs = [{'name': url, 'type': 'other'} for url in ref] + for r in ref: + if isinstance(r, dict): + refs.append(r) + else: + refs.append({'name': r, 'type': 'other'}) return refs def createAndAddVulnToHost(self, host_id, name, desc="", ref=None, @@ -740,8 +744,15 @@ if super().report_belongs_to(**kwargs): if file_json_keys is None: file_json_keys = set() - match = self.json_keys.issubset(file_json_keys) - self.logger.debug(f"Json Keys Match: [{file_json_keys} =/in {self.json_keys}] -> {match}") + if isinstance(self.json_keys, list): + for jk in self.json_keys: + match = jk.issubset(file_json_keys) + self.logger.debug(f"Json Keys Match: [{file_json_keys} =/in {jk}] -> {match}") + if match: + break + else: + match = self.json_keys.issubset(file_json_keys) + self.logger.debug(f"Json Keys Match: [{file_json_keys} =/in {self.json_keys}] -> {match}") return match diff --git a/faraday_plugins/plugins/repo/grype/plugin.py b/faraday_plugins/plugins/repo/grype/plugin.py index 33fec6f..0b29083 100644 --- a/faraday_plugins/plugins/repo/grype/plugin.py +++ b/faraday_plugins/plugins/repo/grype/plugin.py @@ -25,31 +25,44 @@ self._command_regex = re.compile(r'^grype\s+.*') self._use_temp_file = True self._temp_file_extension = "json" - self.json_keys = {"source", "matches", "descriptor"} + self.json_keys = [{"source", "matches", "descriptor"}, {"matches", "image"}] def parseOutputString(self, output, debug=True): grype_json = json.loads(output) - if "userInput" in grype_json["source"]["target"]: + if "userInput" in grype_json.get("source", {"target": ""}).get("target"): name = grype_json["source"]["target"]["userInput"] + host_type = grype_json['source']['type'] + elif "tags" in grype_json.get("image", {}): + name = " ".join(grype_json["image"]["tags"]) + host_type = "Docker Image" else: name = grype_json["source"]["target"] - host_id = self.createAndAddHost(name, description=f"Type: {grype_json['source']['type']}") + host_type = grype_json['source']['type'] + host_id = self.createAndAddHost(name, description=f"Type: {host_type}") for match in grype_json['matches']: name = match.get('vulnerability').get('id') cve = name references = [] - if match["relatedVulnerabilities"]: + if match.get("relatedVulnerabilities"): description = match["relatedVulnerabilities"][0].get('description') references.append(match["relatedVulnerabilities"][0]["dataSource"]) related_vuln = match["relatedVulnerabilities"][0] severity = related_vuln["severity"].lower().replace("negligible", "info") - for url in related_vuln["urls"]: - references.append(url) + if related_vuln.get("links"): + for url in related_vuln["links"]: + references.append(url) + else: + for url in related_vuln["urls"]: + references.append(url) else: - description = match.get('vulnerability').get('description') + description = match.get('vulnerability').get('description', "Issues provided no description") severity = match.get('vulnerability').get('severity').lower().replace("negligible", "info") - for url in match.get('vulnerability').get('urls'): - references.append(url) + if match.get('vulnerability').get("links"): + for url in match.get('vulnerability')["links"]: + references.append(url) + else: + for url in match.get('vulnerability')["urls"]: + references.append(url) if not match['artifact'].get('metadata'): data = f"Artifact: {match['artifact']['name']}" \ f"Version: {match['artifact']['version']} " \ @@ -61,6 +74,10 @@ f"Type: {match['artifact']['type']}" elif "VirtualPath" in match['artifact']['metadata']: data = f"Artifact: {match['artifact']['name']} [{match['artifact']['metadata']['VirtualPath']}] " \ + f"Version: {match['artifact']['version']} " \ + f"Type: {match['artifact']['type']}" + else: + data = f"Artifact: {match['artifact']['name']}" \ f"Version: {match['artifact']['version']} " \ f"Type: {match['artifact']['type']}" self.createAndAddVulnToHost(host_id, diff --git a/faraday_plugins/plugins/repo/invicti/DTO.py b/faraday_plugins/plugins/repo/invicti/DTO.py new file mode 100644 index 0000000..450f7ba --- /dev/null +++ b/faraday_plugins/plugins/repo/invicti/DTO.py @@ -0,0 +1,148 @@ +from typing import List + + +class Cvss3: + def __init__(self, node): + self.node = node + + @property + def vector(self) -> str: + return self.node.find('vector').text + + +class Reference: + def __init__(self, node): + self.node = node + + @property + def owasp(self) -> str: + return self.node.find('owasp').text + + @property + def wasc(self) -> str: + return self.node.find('wasc').text + + @property + def cwe(self) -> str: + return self.node.find('cwe').text + + @property + def capec(self) -> str: + return self.node.find('capec').text + + @property + def pci32(self) -> str: + return self.node.find('pci32').text + + @property + def hipaa(self) -> str: + return self.node.find('hipaa').text + + @property + def owasppc(self) -> str: + return self.node.find('owasppc').text + + @property + def cvss3(self) -> Cvss3: + return Cvss3(self.node.find("cvss31")) + + +class Request: + def __init__(self, node): + self.node = node + + @property + def method(self) -> str: + return self.node.find("method").text + + @property + def content(self) -> str: + return self.node.find("content").text + + +class Response: + def __init__(self, node): + self.node = node + + @property + def content(self) -> str: + return self.node.find("content").text + + +class Vulnerability: + def __init__(self, node): + self.node = node + + @property + def look_id(self) -> str: + return self.node.find('LookupId').text + + @property + def url(self) -> str: + return self.node.find("url").text + + @property + def name(self) -> str: + return self.node.find('name').text + + @property + def severity(self) -> str: + return self.node.find('severity').text + + @property + def confirmed(self) -> str: + return self.node.find('confirmed').text + + @property + def description(self) -> str: + return self.node.find('description').text + + @property + def http_request(self) -> Request: + return Request(self.node.find("http-request")) + + @property + def http_response(self) -> Response: + return Response(self.node.find("http-response")) + + @property + def impact(self) -> str: + return self.node.find("impact").text + + @property + def remedial_actions(self) -> str: + return self.node.find("remedial-actions").text + + @property + def remedial_procedure(self) -> str: + return self.node.find("remedial-procedure").text + + @property + def classification(self) -> Reference: + return Reference(self.node.find("classification")) + + +class Target: + def __init__(self, node): + self.node = node + + @property + def scan_id(self) -> str: + return self.node.find("scan-id").text + + @property + def url(self) -> str: + return self.node.find("url").text + + +class Invicti: + def __init__(self, node): + self.node = node + + @property + def target(self) -> Target: + return Target(self.node.find('target')) + + @property + def vulnerabilities(self) -> List[Vulnerability]: + return [Vulnerability(i) for i in self.node.findall('vulnerabilities/vulnerability')] diff --git a/faraday_plugins/plugins/repo/invicti/__init__.py b/faraday_plugins/plugins/repo/invicti/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/faraday_plugins/plugins/repo/invicti/plugin.py b/faraday_plugins/plugins/repo/invicti/plugin.py new file mode 100644 index 0000000..e88a261 --- /dev/null +++ b/faraday_plugins/plugins/repo/invicti/plugin.py @@ -0,0 +1,121 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information + +""" +from urllib.parse import urlsplit +from bs4 import BeautifulSoup +from lxml import etree + +from faraday_plugins.plugins.plugin import PluginXMLFormat +from faraday_plugins.plugins.repo.invicti.DTO import Invicti + +__author__ = "Gonzalo Martinez" +__copyright__ = "Copyright (c) 2013, Infobyte LLC" +__credits__ = ["Gonzalo Martinez"] +__version__ = "1.0.0" +__maintainer__ = "Gonzalo Martinez" +__email__ = "gmartinez@infobytesec.com" +__status__ = "Development" + + +class InvictiXmlParser: + """ + The objective of this class is to parse a xml file generated by + the acunetix tool. + + @param invicti_xml_filepath A proper xml generated by acunetix + """ + + def __init__(self, xml_output): + + tree = self.parse_xml(xml_output) + self.invicti = Invicti(tree) + + @staticmethod + def parse_xml(xml_output): + """ + Open and parse an xml file. + + TODO: Write custom parser to just read the nodes that we need instead + of reading the whole file. + + @return xml_tree An xml tree instance. None if error. + """ + + try: + parser = etree.XMLParser(recover=True) + tree = etree.fromstring(xml_output, parser=parser) + except SyntaxError as err: + print(f"SyntaxError: {err}. {xml_output}") + return None + + return tree + + +class InvictiPlugin(PluginXMLFormat): + """ + Example plugin to parse invicti output. + """ + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.identifier_tag = "invicti-enterprise" + self.id = "Invicti" + self.name = "Invicti XML Output Plugin" + self.plugin_version = "1.0.0" + self.version = "9" + self.framework_version = "1.0.0" + self.options = None + self._current_output = None + self.target = None + + def parseOutputString(self, output): + """ + This method will discard the output the shell sends, it will read it + from the xml where it expects it to be present. + + NOTE: if 'debug' is true then it is being run from a test case and the + output being sent is valid. + """ + parser = InvictiXmlParser(output) + url = urlsplit(parser.invicti.target.url) + ip = self.resolve_hostname(url.netloc) + h_id = self.createAndAddHost(ip) + s_id = self.createAndAddServiceToHost(h_id, url.scheme, ports=433) + for vulnerability in parser.invicti.vulnerabilities: + vuln = {"name": vulnerability.name, "severity": vulnerability.severity, + "confirmed": vulnerability.confirmed, + "desc": BeautifulSoup(vulnerability.description, features="lxml").text, + "path": vulnerability.url.replace(parser.invicti.target.url, ""), + "external_id": vulnerability.look_id, + "resolution": BeautifulSoup(vulnerability.remedial_procedure, features="lxml").text} + if vulnerability.classification: + references = [] + if vulnerability.classification.owasp: + references.append("OWASP" + vulnerability.classification.owasp) + if vulnerability.classification.wasc: + references.append("WASC" + vulnerability.classification.wasc) + if vulnerability.classification.cwe: + vuln["cwe"] = "CWE-" + vulnerability.classification.cwe + if vulnerability.classification.capec: + references.append("CAPEC" + vulnerability.classification.capec) + if vulnerability.classification.pci32: + references.append("PCI32" + vulnerability.classification.pci32) + if vulnerability.classification.hipaa: + references.append("HIPAA" + vulnerability.classification.hipaa) + if vulnerability.classification.owasppc: + references.append("OWASPPC" + vulnerability.classification.owasppc) + if vulnerability.classification.cvss3.node is not None: + vuln["cvss3"] = {"vector_string": vulnerability.classification.cvss3.vector} + vuln["ref"] = references + if vulnerability.http_response.node is not None: + vuln["response"] = vulnerability.http_response.content + if vulnerability.http_request.node is not None: + vuln["request"] = vulnerability.http_request.content + self.createAndAddVulnWebToService(h_id, s_id, **vuln) + + +def createPlugin(*args, **kwargs): + return InvictiPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/nessus_sc/__init__.py b/faraday_plugins/plugins/repo/nessus_sc/__init__.py new file mode 100644 index 0000000..f70f230 --- /dev/null +++ b/faraday_plugins/plugins/repo/nessus_sc/__init__.py @@ -0,0 +1,6 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2017 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information + +""" diff --git a/faraday_plugins/plugins/repo/nessus_sc/plugin.py b/faraday_plugins/plugins/repo/nessus_sc/plugin.py new file mode 100644 index 0000000..d3938d2 --- /dev/null +++ b/faraday_plugins/plugins/repo/nessus_sc/plugin.py @@ -0,0 +1,81 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information + +""" + +from faraday_plugins.plugins.plugin import PluginCSVFormat +import csv +import io + + +__author__ = "Gonzalo Martinez" +__copyright__ = "Copyright (c) 2019, Infobyte LLC" +__credits__ = ["Gonzalo Martinez"] +__license__ = "" +__version__ = "1.0.0" +__maintainer__ = "Gonzalo Martinez" +__email__ = "gmartinez@infobytesec.com" +__status__ = "Development" + + +class NessusScPlugin(PluginCSVFormat): + """ + Example plugin to parse Nessus Sc output. + """ + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.csv_headers = [{'Plugin', 'Plugin Name'}] + self.id = "Nessus_sc" + self.name = "Nessus Sc Output Plugin" + self.plugin_version = "1.0.0" + self.version = "1.0.0" + self.framework_version = "1.0.0" + + def parseOutputString(self, output): + try: + reader = csv.DictReader(io.StringIO(output)) + except: + print("Error parser output") + return None + + for row in reader: + ip = row['IP Address'] + hostname = row['DNS Name'] + h_id = self.createAndAddHost(name=ip, hostnames=hostname) + protocol = row['Protocol'] + port = row['Port'] + s_id = self.createAndAddServiceToHost(h_id, name=port, protocol=protocol, ports=port, status="open") + name = row['Plugin Name'] + severity = row['Severity'] + description = row['Description'] + vuln = {"name": name, "severity": severity, "desc": description} + solution = row['Solution'] + if solution: + vuln["resolution"] = solution + cvss3_vector = row['CVSS V3 Vector'] + if cvss3_vector: + if not cvss3_vector.startswith("CVSS:3.0/"): + cvss3_vector = "CVSS:3.0/"+cvss3_vector + vuln["cvss3"] = {"vector_string": cvss3_vector} + cvss2_vector = row['CVSS V2 Vector'] + if cvss2_vector: + vuln["cvss2"] = {"vector_string": cvss2_vector} + external_ref = row["See Also"] + cross_ref = row["Cross References"] + references = [] + if external_ref: + references.append(external_ref) + if cross_ref: + references.append(cross_ref) + vuln["ref"] = references + cve = row['CVE'] + if cve: + vuln["cve"] = cve + self.createAndAddVulnToService(h_id, s_id, **vuln) + + +def createPlugin(*args, **kwargs): + return NessusScPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/nexpose_full/plugin.py b/faraday_plugins/plugins/repo/nexpose_full/plugin.py index cf7bf6a..18ba074 100644 --- a/faraday_plugins/plugins/repo/nexpose_full/plugin.py +++ b/faraday_plugins/plugins/repo/nexpose_full/plugin.py @@ -141,7 +141,7 @@ vuln = { 'desc': "", 'name': vulnDef.get('title'), - 'refs': ["vector: " + vector, vid], + 'refs': [], 'resolution': "", 'severity': "", 'tags': list(), @@ -172,13 +172,19 @@ vuln['refs'].append(nameMalware) if item.tag == 'references': for ref in list(item): - if ref.text: - rf = ref.text.strip() - check = CVE_regex.search(rf.upper()) - if check: - vuln["CVE"].append(check.group()) - else: - vuln['refs'].append(rf) + if not ref.text: + continue + source = "" + if "source" in ref.attrib: + source = ref.attrib['source'] + ": " + rf = ref.text.strip() + check = CVE_regex.search(rf.upper()) + if check: + vuln["CVE"].append(check.group()) + else: + if rf.isnumeric(): + rf = source + rf + vuln['refs'].append(rf) if item.tag == 'solution': for htmlType in list(item): vuln['resolution'] += self.parse_html_type(htmlType) diff --git a/faraday_plugins/plugins/repo/nikto/plugin.py b/faraday_plugins/plugins/repo/nikto/plugin.py index ed17b73..cdc4b52 100644 --- a/faraday_plugins/plugins/repo/nikto/plugin.py +++ b/faraday_plugins/plugins/repo/nikto/plugin.py @@ -100,7 +100,7 @@ self.node = item_node self.osvdbid = [ - "OSVDB-ID: " + self.node.get('osvdbid')] if self.node.get('osvdbid') != "0" else [] + "OSVDB-ID: " + self.node.get('osvdbid')] if self.node.get('osvdbid', "0") != "0" else [] self.namelink = self.get_text_from_subnode('namelink') self.iplink = self.get_text_from_subnode('iplink') @@ -207,7 +207,7 @@ def __init__(self, *arg, **kwargs): super().__init__(*arg, **kwargs) - self.identifier_tag = "niktoscan" + self.identifier_tag = ["niktoscan", "niktoscans"] self.id = "Nikto" self.name = "Nikto XML Output Plugin" self.plugin_version = "0.0.2"