diff --git a/README.md b/README.md index 67c5f21..bc34832 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,76 @@ $ faraday-cli host list 575 127.0.0.9 unknown 1 3 590 58.76.184.4 unknown www.googlec.com 0 - ``` +### Update Hosts + +```shell script +$ faraday-cli host update 12 -d '[{"ip":"127.0.0.1", "hostnames":["test.test.es"]}]' +Updated host: +{ + "services": 11, + "owner": "faraday", + "description": "", + "ip": "127.0.0.1", + "_rev": "", + "workspace_name": "TEST", + "hostnames": [ + "127.0.0.1", + "feeds.cti.pers.eus" + ], + "name": "127.0.0.1", + "vulns": 14, + "metadata": { + "update_user": null, + "owner": "faraday", + "update_controller_action": "", + "update_action": 0, + "command_id": null, + "creator": "", + "update_time": "2025-04-03T11:42:02.417316+00:00", + "create_time": "2025-03-31T14:13:22.684505+00:00" + }, + "_id": 12, + "mac": "00:00:00:00:00:00", + "type": "Host", + "versions": [ + "OpenSSH 8.9p1 Ubuntu 3ubuntu0.11", + "nginx 1.18.0", + "", + "Nagios NSCA", + "", + "Golang net/http server", + "MinIO S3-compatible object store", + "Elasticsearch REST API 8.17.3", + "Elasticsearch binary API", + "Golang net/http server" + ], + "id": 12, + "service_summaries": [ + "(22/tcp) ssh (OpenSSH 8.9p1 Ubuntu 3ubuntu0.11)", + "(80/tcp) http (nginx 1.18.0)", + "(2377/tcp) grpc", + "(7946/tcp) unknown", + "(8000/tcp) nagios-nsca (Nagios NSCA)", + "(8080/tcp) http-proxy", + "(9000/tcp) http (Golang net/http server)", + "(9001/tcp) http (MinIO S3-compatible object store)", + "(9200/tcp) http (Elasticsearch REST API 8.17.3)", + "(9300/tcp) elasticsearch (Elasticsearch binary API)", + "(9443/tcp) https (Golang net/http server)" + ], + "owned": false, + "default_gateway": "", + "os": "Linux", + "credentials": 0, + "importance": 0 +} +``` +### Create Vuln + +```shell script +$ faraday-cli vuln create -d '{"name": "process injection", "description": "inyeccion de procesos", "exploitation": "medium", "type": ["vulnerability_template"]}' +Vulnerability created with ID: 89 +``` ### Get host diff --git a/faraday_cli/api_client/exceptions.py b/faraday_cli/api_client/exceptions.py index d4dd92f..f02fcfe 100644 --- a/faraday_cli/api_client/exceptions.py +++ b/faraday_cli/api_client/exceptions.py @@ -29,3 +29,8 @@ class RequestError(ErrorWithResponse): class ExpiredLicense(Exception): pass + +class HostNotFoundError(Exception): + def __init__(self, message): + self.message = message + super().__init__(self.message) diff --git a/faraday_cli/api_client/faraday_api.py b/faraday_cli/api_client/faraday_api.py index a66b609..b8db468 100644 --- a/faraday_cli/api_client/faraday_api.py +++ b/faraday_cli/api_client/faraday_api.py @@ -7,6 +7,7 @@ InvalidCredentials, Invalid2FA, MissingConfig, + HostNotFoundError, ExpiredLicense, NotFound, RequestError, @@ -68,6 +69,8 @@ def hanlde(self, *args, **kwargs): raise Exception(f"{e}") except NotFoundError: raise NotFound("Element not found") + except HostNotFoundError as e: + raise Exception(f"Host not found: {e}") except ClientError as e: if e.response.status_code == 402: raise ExpiredLicense("Your Faraday license is expired") @@ -286,6 +289,17 @@ def get_vulns(self, workspace_name: str, query_filter: dict = None): workspace_name, params={"q": json.dumps(query_filter)} ) return response.body + @handle_errors + def create_vuln(self, workspace_name: str, vuln_params): + try: + response = self.faraday_api.vuln.create(workspace_name, body=vuln_params) + except ClientError as e: + if e.response.status_code == 409: + raise exceptions.DuplicatedError("Vulnerability already exists") + else: + raise + else: + return response.body @handle_errors def get_workspace_credentials(self, workspace_name: str): @@ -354,6 +368,28 @@ def create_host(self, workspace_name: str, host_params): raise else: return response.body + + @handle_errors + def update_host(self, workspace_name: str, host_id, host_params): + try: + response = self.faraday_api.host.update(workspace_name, host_id, body=host_params) + current_host = response.body + if "hostnames" in host_params: + current_host["hostnames"] = host_params["hostnames"] + if "description" in host_params: + current_host["description"] = host_params["description"] + + try: + update_response = self.faraday_api.host.update(workspace_name, host_id, body=current_host) + except ClientError as e: + raise Exception(f"Error while updating host: {e}") + else: + return update_response.body + except ClientError as e: + if e.response.status_code == 404: + raise exceptions.HostNotFoundError("Host not found") + else: + raise Exception(f"Error while checking host existence: {e}") @handle_errors def get_host_services(self, workspace_name: str, host_id): diff --git a/faraday_cli/api_client/resources.py b/faraday_cli/api_client/resources.py index 6296750..d2c7e6a 100644 --- a/faraday_cli/api_client/resources.py +++ b/faraday_cli/api_client/resources.py @@ -41,12 +41,14 @@ class HostResource(Resource): }, # workaround for api bug "get": {"method": "GET", "url": "v3/ws/{}/hosts/{}"}, "create": {"method": "POST", "url": "v3/ws/{}/hosts"}, + "update": {"method": "PATCH", "url": "v3/ws/{}/hosts/{}"}, "delete": {"method": "DELETE", "url": "v3/ws/{}/hosts/{}"}, "get_services": { "method": "GET", "url": "v3/ws/{}/hosts/{}/services", }, "get_vulns": {"method": "GET", "url": "v3/ws/{}/vulns"}, + } @@ -60,6 +62,7 @@ class ServiceResource(Resource): class VulnResource(Resource): actions = { "get": {"method": "GET", "url": "v3/ws/{}/vulns/{}"}, + "create": {"method": "POST", "url": "v3/vulnerability_template"}, "patch": {"method": "PATCH", "url": "v3/ws/{}/vulns/{}"}, "list": {"method": "GET", "url": "v3/ws/{}/vulns"}, "filter": {"method": "GET", "url": "v3/ws/{}/vulns/filter"}, diff --git a/faraday_cli/shell/modules/host.py b/faraday_cli/shell/modules/host.py index f9950f7..1b06410 100644 --- a/faraday_cli/shell/modules/host.py +++ b/faraday_cli/shell/modules/host.py @@ -25,6 +25,24 @@ }, } +HOST_UPDATE_JSON_SCHEMA = { + "type": "array", + "items": { + "type": "object", + "properties": { + "ip": {"type": "string"}, + "description": {"type": "string"}, + "hostnames": { + "type": "array", + "items": {"type": "string"} + }, + }, + "required": [], + "additionalProperties": False + }, +} + + class HostCommands(cmd2.CommandSet): def __init__(self): @@ -386,3 +404,79 @@ def create_hosts(self, args: argparse.Namespace): self._cmd.poutput( f"Created host\n{json.dumps(host, indent=4)}" ) + # Update hosts + update_host_parser = argparse.ArgumentParser() + update_host_parser.add_argument( + "-d", + "--host-data", + type=str, + help=f"Host data in json format: {HOST_UPDATE_JSON_SCHEMA}", + ) + update_host_parser.add_argument( + "--stdin", action="store_true", help="Read host-data from stdin" + ) + update_host_parser.add_argument("host_id", type=int, help="ID of the host") + update_host_parser.add_argument( + "-w", + "--workspace-name", + type=str, + help="Workspace name", + required=False, + ) + update_host_parser.add_argument( + "--resolve-hostname", + action="store_true", + help="Doesn't resolve hostname", + ) + + @cmd2.as_subcommand_to( + "host", "update", update_host_parser, help="update hosts" + ) + def update_hosts(self, args: argparse.Namespace): + """Update Hosts""" + if not args.workspace_name: + if active_config.workspace: + workspace_name = active_config.workspace + else: + self._cmd.perror("No active Workspace") + return + else: + workspace_name = args.workspace_name + if args.stdin: + host_data = sys.stdin.read() + else: + if not args.host_data: + self._cmd.perror("Missing host data") + return + else: + host_data = args.host_data + + try: + json_data = utils.json_schema_validator(HOST_UPDATE_JSON_SCHEMA)(host_data) + except Exception as e: + self._cmd.perror(f"JSON validation error: {e}") + return + for _host_data in json_data: + if args.resolve_hostname: + ip, hostname = utils.get_ip_and_hostname(_host_data.get("ip", "")) + else: + ip = _host_data.get("ip", "") + hostname = ip + _host_data["ip"] = ip + if hostname: + if "hostnames" in _host_data: + _host_data["hostnames"].append(hostname) + else: + _host_data["hostnames"] = [hostname] + try: + host = self._cmd.api_client.get_host(workspace_name, args.host_id) + if not host: + self._cmd.perror(f"Host with ID {args.host_id} not found") + return + updated_host = self._cmd.api_client.update_host(workspace_name, args.host_id, _host_data) + + except Exception as e: + self._cmd.perror(f"Error updating host: {e}") + else: + self._cmd.poutput(f"Updated host:\n{json.dumps(updated_host, indent=4)}") + \ No newline at end of file diff --git a/faraday_cli/shell/modules/vulnerability.py b/faraday_cli/shell/modules/vulnerability.py index 4432897..b4630fb 100644 --- a/faraday_cli/shell/modules/vulnerability.py +++ b/faraday_cli/shell/modules/vulnerability.py @@ -23,6 +23,104 @@ colorama.init(autoreset=True) +VULN_CREATE_JSON_SCHEMA = { + "type": "object", + "properties": { + "cve": { + "type": "array", + "items": { + "type": "string" + } + }, + "cwe": { + "type": "string" + }, + "data": { + "type": "string" + }, + "desc": { + "type": "string" + }, + "description": { + "type": "string" + }, + "easeofresolution": { + "type": "string" + }, + "exploitation": { + "type": "string" + }, + "external_id": { + "type": "string" + }, + "impact": { + "type": "object", + "properties": { + "accountability": { + "type": "boolean" + }, + "availability": { + "type": "boolean" + }, + "integrity": { + "type": "boolean" + }, + "confidentiality": { + "type": "boolean" + } + }, + "required": ["accountability", "availability", "integrity", "confidentiality"], + "additionalProperties": False + }, + "accountability": { + "type": "boolean" + }, + "availability": { + "type": "boolean" + }, + "confidentiality": { + "type": "boolean" + }, + "integrity": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "policyviolations": { + "type": "array", + "items": { + "type": "string" + } + }, + "references": { + "type": "array", + "items": { + "type": "string" + } + }, + "refs": { + "type": "array", + "items": { + "type": "string" + } + }, + "resolution": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["vulnerability_template"] + } + }, + "required": [ + "exploitation", + "description", + "name" + ], + "additionalProperties": False +} + VULN_UPDATE_JSON_SCHEMA = { "properties": { @@ -415,3 +513,74 @@ def delete_vuln(self, args: argparse.Namespace): self._cmd.poutput( cmd2.style("Vulnerability deleted", fg=COLORS.GREEN) ) + + create_vuln_parser = cmd2.Cmd2ArgumentParser() + create_vuln_parser.add_argument( + "-w", "--workspace-name", type=str, help="Workspace" + ) + create_vuln_parser.add_argument( + "-d", + "--host-data", + type=str, + help=f"Host data in JSON format: {VULN_CREATE_JSON_SCHEMA}", + ) + create_vuln_parser.add_argument( + "--stdin", + action="store_true", + help="Read host-data from stdin instead of the -d argument", + ) + + @cmd2.as_subcommand_to( + "vuln", + "create", + create_vuln_parser, + help="Create a new vulnerability from the provided data" + ) + def create_vuln(self, args: argparse.Namespace): + """Create a new Vulnerability from JSON data""" + + if not args.workspace_name: + if active_config.workspace: + workspace_name = active_config.workspace + else: + self._cmd.perror("No active Workspace") + return + else: + workspace_name = args.workspace_name + if args.stdin: + try: + input_data = sys.stdin.read() + body = json.loads(input_data) + except json.JSONDecodeError: + self._cmd.perror("Invalid JSON data provided via stdin") + return + elif args.host_data: + try: + body = json.loads(args.host_data) + except json.JSONDecodeError: + self._cmd.perror("Invalid JSON data provided in -d argument") + return + else: + self._cmd.perror("No data provided. Use -d or --stdin to provide vulnerability data.") + return + required_fields = ["exploitation", "description", "name"] + missing_fields = [field for field in required_fields if field not in body] + + if missing_fields: + self._cmd.perror(f"Missing required fields: {', '.join(missing_fields)}") + return + + body["impact"] = body.get("impact", { + "accountability": False, + "availability": False, + "integrity": False, + "confidentiality": False + }) + + try: + response = self._cmd.api_client.create_vuln(workspace_name, body) + self._cmd.poutput(f"Vulnerability created with ID: {response['id']}") + except RequestError as e: + self._cmd.perror(e.message) + + \ No newline at end of file