From e1d48c9d0f6b0c27265a9766e693cbc6de217188 Mon Sep 17 00:00:00 2001 From: bobbysto Date: Tue, 7 Feb 2017 03:29:03 +0000 Subject: [PATCH 01/32] Correct syntax error (#2) Previous error: >>> api.pages.get() Traceback (most recent call last): File "", line 1, in File "build/bdist.linux-x86_64/egg/gophish/api/pages.py", line 11, in get AttributeError: 'API' object has no attribute 'super' --- gophish/api/pages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gophish/api/pages.py b/gophish/api/pages.py index 83574cb..406c638 100644 --- a/gophish/api/pages.py +++ b/gophish/api/pages.py @@ -8,7 +8,7 @@ def __init__(self, api, endpoint='/api/pages/'): def get(self, page_id=None): """ Gets one or more pages """ - return self.super(API, self).get(resource_id=page_id) + return super(API, self).get(resource_id=page_id) def post(self, page): """ Creates a new page """ From 714207fe730c48db30ebd53b30d1ab727ce26702 Mon Sep 17 00:00:00 2001 From: Mart Date: Mon, 6 Feb 2017 22:30:35 -0500 Subject: [PATCH 02/32] Added campaign completion feature. (#3) --- gophish/api/api.py | 6 +++++- gophish/api/campaigns.py | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/gophish/api/api.py b/gophish/api/api.py index 20bb28d..dfb5afb 100644 --- a/gophish/api/api.py +++ b/gophish/api/api.py @@ -25,12 +25,13 @@ def __init__(self, api, endpoint=None, cls=None): self.endpoint = endpoint self._cls = cls - def get(self, resource_id=None): + def get(self, resource_id=None, resource_action=None): """ Gets the details for one or more resources by ID Args: cls - gophish.models.Model - The resource class resource_id - str - The endpoint (URL path) for the resource + resource_action - str - An action to perform on the resource Returns: One or more instances of cls parsed from the returned JSON @@ -41,6 +42,9 @@ def get(self, resource_id=None): if resource_id: endpoint = '{}/{}'.format(endpoint, resource_id) + if resource_action: + endpoint = '{}/{}'.format(endpoint, resource_action) + response = self.api.execute("GET", endpoint) if not response.ok: return Error.parse(response.json()) diff --git a/gophish/api/campaigns.py b/gophish/api/campaigns.py index d928d4c..31abc9e 100644 --- a/gophish/api/campaigns.py +++ b/gophish/api/campaigns.py @@ -30,6 +30,12 @@ def delete(self, campaign_id): return super(API, self).delete(campaign_id) + def complete(self, campaign_id): + """ Complete an existing campaign (Stop processing events) """ + + return super(API, self).get(resource_id=campaign_id, + resource_action='complete') + def summary(campaign_id=None): """ Returns the summary of one or more campaigns. """ raise NotImplementedError From 2d3ccfcc17be67e3368cae59ce9b96f945e690bb Mon Sep 17 00:00:00 2001 From: Mart Date: Thu, 23 Feb 2017 00:12:34 -0500 Subject: [PATCH 03/32] Fixed AttributeErrors due to small errors (#4) --- gophish/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gophish/models.py b/gophish/models.py index 4bc1784..1445e7a 100644 --- a/gophish/models.py +++ b/gophish/models.py @@ -20,7 +20,7 @@ def as_dict(self): if isinstance(val, datetime): val = val.isoformat() # Parse custom classes - elif val and not isinstance(val, (str, list, dict)): + elif val and not isinstance(val, (int, float, str, list, dict)): val = val.as_dict() # Parse lists of objects elif isinstance(val, list): @@ -91,7 +91,7 @@ def parse(cls, json): return result -class TimelineEntry(object): +class TimelineEntry(Model): _valid_properties = {'email': None, 'time': None, 'message': None, 'details': None} @classmethod From 30f838b87e1c4df9906cc32b8a29a7e55e756d60 Mon Sep 17 00:00:00 2001 From: Jordan Wright Date: Fri, 26 May 2017 21:04:13 -0500 Subject: [PATCH 04/32] Bumping version number to 0.1.2 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4fa6e03..38fc5df 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name = 'gophish', packages = ['gophish', 'gophish.api'], - version = '0.1.1', + version = '0.1.2', description = 'Python API Client for Gophish', author = 'Jordan Wright', author_email = 'python@getgophish.com', From 98d05eba471f436052db407e0189bb0f4bbc4728 Mon Sep 17 00:00:00 2001 From: Jordan Wright Date: Fri, 26 May 2017 21:30:58 -0500 Subject: [PATCH 05/32] Bumping version --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 38fc5df..2385e9d 100644 --- a/setup.py +++ b/setup.py @@ -3,13 +3,13 @@ setup( name = 'gophish', packages = ['gophish', 'gophish.api'], - version = '0.1.2', + version = '0.1.3', description = 'Python API Client for Gophish', author = 'Jordan Wright', author_email = 'python@getgophish.com', url = 'https://github.com/gophish/api-client-python', license='MIT', - download_url = 'https://github.com/gophish/api-client-python/tarball/0.1.1', + download_url = 'https://github.com/gophish/api-client-python/tarball/0.1.3', keywords = ['gophish'], classifiers = [ 'Development Status :: 3 - Alpha', From ad4229c9c753be38caa69a5346aeb869bd42ab8e Mon Sep 17 00:00:00 2001 From: Jordan Wright Date: Wed, 20 Sep 2017 21:35:22 -0500 Subject: [PATCH 06/32] Added /campaigns/summary and /campaigns/:id/results endpoints. --- gophish/api/api.py | 28 ++++-- gophish/api/campaigns.py | 30 ++++-- gophish/client.py | 21 ++-- gophish/models.py | 210 ++++++++++++++++++++++++++++++++++----- 4 files changed, 240 insertions(+), 49 deletions(-) diff --git a/gophish/api/api.py b/gophish/api/api.py index dfb5afb..201420a 100644 --- a/gophish/api/api.py +++ b/gophish/api/api.py @@ -1,18 +1,19 @@ import requests from gophish.models import Error - ''' api.py Base API endpoint class that abstracts basic CRUD operations. ''' + class APIEndpoint(object): """ Represents an API endpoint for Gophish, containing common patterns for CRUD operations. """ + def __init__(self, api, endpoint=None, cls=None): """ Creates an instance of the APIEndpoint class. @@ -25,13 +26,20 @@ def __init__(self, api, endpoint=None, cls=None): self.endpoint = endpoint self._cls = cls - def get(self, resource_id=None, resource_action=None): + def get(self, + resource_id=None, + resource_action=None, + resource_cls=None, + single_resource=False): """ Gets the details for one or more resources by ID Args: cls - gophish.models.Model - The resource class resource_id - str - The endpoint (URL path) for the resource resource_action - str - An action to perform on the resource + resource_cls - cls - A class to use for parsing, if different than the base resource + single_resource - bool - An override to tell Gophish that even + though we aren't requesting a single resource, we expect a single response object Returns: One or more instances of cls parsed from the returned JSON @@ -39,6 +47,9 @@ def get(self, resource_id=None, resource_action=None): endpoint = self.endpoint + if not resource_cls: + resource_cls = self._cls + if resource_id: endpoint = '{}/{}'.format(endpoint, resource_id) @@ -49,10 +60,10 @@ def get(self, resource_id=None, resource_action=None): if not response.ok: return Error.parse(response.json()) - if resource_id: - return self._cls.parse(response.json()) + if resource_id or single_resource: + return resource_cls.parse(response.json()) - return [self._cls.parse(resource) for resource in response.json()] + return [resource_cls.parse(resource) for resource in response.json()] def post(self, resource): """ Creates a new instance of the resource. @@ -61,8 +72,9 @@ def post(self, resource): resource - gophish.models.Model - The resource instance """ - response = self.api.execute("POST", self.endpoint, json=(resource.as_dict())) - + response = self.api.execute( + "POST", self.endpoint, json=(resource.as_dict())) + if not response.ok: return Error.parse(response.json()) @@ -74,7 +86,7 @@ def put(self, resource): Args: resource - gophish.models.Model - The resource instance """ - + endpoint = self.endpoint if resource.id: diff --git a/gophish/api/campaigns.py b/gophish/api/campaigns.py index 31abc9e..5b1d401 100644 --- a/gophish/api/campaigns.py +++ b/gophish/api/campaigns.py @@ -1,6 +1,6 @@ import json -from gophish.models import Campaign, Error +from gophish.models import Campaign, CampaignSummary, CampaignSummaries, CampaignResults, Error from gophish.api import APIEndpoint @@ -33,13 +33,27 @@ def delete(self, campaign_id): def complete(self, campaign_id): """ Complete an existing campaign (Stop processing events) """ - return super(API, self).get(resource_id=campaign_id, - resource_action='complete') + return super(API, self).get( + resource_id=campaign_id, resource_action='complete') - def summary(campaign_id=None): - """ Returns the summary of one or more campaigns. """ - raise NotImplementedError + def summary(self, campaign_id=None): + """ Returns the campaign summary """ + resource_cls = CampaignSummary + single_resource = False - def results(campaign_id): + if not campaign_id: + resource_cls = CampaignSummaries + single_resource = True + + return super(API, self).get( + resource_id=campaign_id, + resource_action='summary', + resource_cls=resource_cls, + single_resource=single_resource) + + def results(self, campaign_id): """ Returns just the results for a given campaign """ - raise NotImplementedError + return super(API, self).get( + resource_id=campaign_id, + resource_action='results', + resource_cls=CampaignResults) diff --git a/gophish/client.py b/gophish/client.py index d5d92d6..586c6b9 100644 --- a/gophish/client.py +++ b/gophish/client.py @@ -1,31 +1,34 @@ import requests -from gophish.api import ( - campaigns, - groups, - pages, - smtp, - templates) +from gophish.api import (campaigns, groups, pages, smtp, templates) DEFAULT_URL = 'http://localhost:3333' + class GophishClient(object): """ A standard HTTP REST client used by Gophish """ + def __init__(self, api_key, host=DEFAULT_URL, **kwargs): self.api_key = api_key self.host = host self._client_kwargs = kwargs - + def execute(self, method, path, **kwargs): """ Executes a request to a given endpoint, returning the result """ url = "{}{}".format(self.host, path) kwargs.update(self._client_kwargs) - response = requests.request(method, url, params={"api_key": self.api_key}, **kwargs) + response = requests.request( + method, url, params={"api_key": self.api_key}, **kwargs) return response + class Gophish(object): - def __init__(self, api_key, host=DEFAULT_URL, client=GophishClient, **kwargs): + def __init__(self, + api_key, + host=DEFAULT_URL, + client=GophishClient, + **kwargs): self.client = client(api_key, host=host, **kwargs) self.campaigns = campaigns.API(self.client) self.groups = groups.API(self.client) diff --git a/gophish/models.py b/gophish/models.py index 1445e7a..8a56edf 100644 --- a/gophish/models.py +++ b/gophish/models.py @@ -4,10 +4,12 @@ import json as _json import dateutil.parser + def parse_date(datestr): """ Parses an ISO 8601 formatted date from Gophish """ return dateutil.parser.parse(datestr) + class Model(object): def __init__(self): self._valid_properties = {} @@ -20,7 +22,7 @@ def as_dict(self): if isinstance(val, datetime): val = val.isoformat() # Parse custom classes - elif val and not isinstance(val, (int, float, str, list, dict)): + elif val and not isinstance(val, (int, float, str, list, dict, unicode)): val = val.as_dict() # Parse lists of objects elif isinstance(val, list): @@ -28,7 +30,7 @@ def as_dict(self): # Add it if it's not None if val: - result[key] = val + result[key] = val return result @classmethod @@ -39,16 +41,27 @@ def parse(cls, json): class Campaign(Model): _valid_properties = { - 'id': None, 'name': None, 'created_date': datetime.now(tzlocal()), - 'launch_date': datetime.now(tzlocal()), 'completed_date': None, 'template': None, - 'page': None, 'results': [], 'status': None, 'timeline': [], - 'smtp': None, 'url': None, 'groups': [], 'profile': None} + 'id': None, + 'name': None, + 'created_date': datetime.now(tzlocal()), + 'launch_date': datetime.now(tzlocal()), + 'completed_date': None, + 'template': None, + 'page': None, + 'results': [], + 'status': None, + 'timeline': [], + 'smtp': None, + 'url': None, + 'groups': [], + 'profile': None + } def __init__(self, **kwargs): """ Creates a new campaign instance """ for key, default in Campaign._valid_properties.items(): setattr(self, key, kwargs.get(key, default)) - + @classmethod def parse(cls, json): campaign = cls() @@ -72,11 +85,124 @@ def parse(cls, json): return campaign +class CampaignSummaries(Model): + ''' Represents a list of campaign summary objects ''' + _valid_properties = {'total': None, 'campaigns': None} + + def __init__(self): + """ Creates a new instance of the campaign summaries""" + for key, default in CampaignSummaries._valid_properties.items(): + setattr(self, key, default) + + @classmethod + def parse(cls, json): + campaign_summaries = cls() + for key, val in json.items(): + # TODO Add date parsing + if key == 'campaigns': + summaries = [CampaignSummary.parse(summary) for summary in val] + setattr(campaign_summaries, key, summaries) + elif key in cls._valid_properties: + setattr(campaign_summaries, key, val) + return campaign_summaries + + +class CampaignSummary(Model): + ''' Represents a campaign summary object ''' + _valid_properties = { + 'id': None, + 'name': None, + 'status': None, + 'created_date': None, + 'launch_date': None, + 'completed_date': None, + 'stats': None + } + + def __init__(self): + for key, default in CampaignSummary._valid_properties.items(): + setattr(self, key, default) + + @classmethod + def parse(cls, json): + summary = cls() + for key, val in json.items(): + # TODO Add date parsing + if key == 'stats': + stats = Stat.parse(val) + setattr(summary, key, stats) + elif key in cls._valid_properties: + setattr(summary, key, val) + return summary + + +class Stat(Model): + _valid_properties = { + 'total': None, + 'sent': None, + 'opened': None, + 'clicked': None, + 'submitted_data': None, + 'error': None + } + + def __init__(self): + for key, default in Stat._valid_properties.items(): + setattr(self, key, default) + + @classmethod + def parse(cls, json): + stat = cls() + for key, val in json.items(): + if key in cls._valid_properties: + setattr(stat, key, val) + return stat + + +class CampaignResults(Model): + ''' Represents a succinct view of campaign results ''' + _valid_properties = { + 'id': None, + 'name': None, + 'results': [], + 'status': None, + 'timeline': [], + } + + def __init__(self, **kwargs): + """ Creates a new instance of the campaign results object""" + for key, default in CampaignResults._valid_properties.items(): + setattr(self, key, kwargs.get(key, default)) + + @classmethod + def parse(cls, json): + campaign_results = cls() + for key, val in json.items(): + # TODO Add date parsing + if key == 'results': + results = [Result.parse(result) for result in val] + setattr(campaign_results, key, results) + elif key == 'timeline': + if val is not None: + timeline = [TimelineEntry.parse(entry) for entry in val] + setattr(campaign_results, key, timeline) + elif key in cls._valid_properties: + setattr(campaign_results, key, val) + return campaign_results + + class Result(Model): _valid_properties = { - 'id': None, 'first_name': None, 'last_name': None, 'email': None, - 'position': None, 'ip': None, 'latitude': None, 'longitude': None, - 'status': None} + 'id': None, + 'first_name': None, + 'last_name': None, + 'email': None, + 'position': None, + 'ip': None, + 'latitude': None, + 'longitude': None, + 'status': None + } def __init__(self, **kwargs): for key, default in Result._valid_properties.items(): @@ -92,7 +218,17 @@ def parse(cls, json): class TimelineEntry(Model): - _valid_properties = {'email': None, 'time': None, 'message': None, 'details': None} + _valid_properties = { + 'email': None, + 'time': None, + 'message': None, + 'details': None + } + + def __init__(self): + ''' Creates a new instance of a timeline entry ''' + for key, default in TimelineEntry._valid_properties.items(): + setattr(self, key, default) @classmethod def parse(cls, json): @@ -110,8 +246,12 @@ class User(Model): """ User contains the attributes for a member of a group used in Gophish """ _valid_properties = { - 'id': None, 'first_name': None, 'last_name': None, 'email': None, - 'position': None} + 'id': None, + 'first_name': None, + 'last_name': None, + 'email': None, + 'position': None + } def __init__(self, **kwargs): for key, default in User._valid_properties.items(): @@ -129,8 +269,11 @@ def parse(cls, json): class Group(Model): """ Groups contain one or more users """ _valid_properties = { - 'id': None, 'name': None, 'modified_date': datetime.now(tzlocal()), - 'targets': []} + 'id': None, + 'name': None, + 'modified_date': datetime.now(tzlocal()), + 'targets': [] + } def __init__(self, **kwargs): for key, default in Group._valid_properties.items(): @@ -152,9 +295,14 @@ def parse(cls, json): class SMTP(Model): _valid_properties = { - 'id': None, 'interface_type': 'SMTP', 'name': None, 'host': None, - 'from_address': None, 'ignore_cert_errors' : False, - 'modified_date': datetime.now(tzlocal())} + 'id': None, + 'interface_type': 'SMTP', + 'name': None, + 'host': None, + 'from_address': None, + 'ignore_cert_errors': False, + 'modified_date': datetime.now(tzlocal()) + } def __init__(self, **kwargs): for key, default in SMTP._valid_properties.items(): @@ -173,8 +321,14 @@ def parse(cls, json): class Template(Model): _valid_properties = { - 'id': None, 'name': None, 'text': None, 'html': None, - 'modified_date': datetime.now(tzlocal()), 'subject': None, 'attachments': []} + 'id': None, + 'name': None, + 'text': None, + 'html': None, + 'modified_date': datetime.now(tzlocal()), + 'subject': None, + 'attachments': [] + } def __init__(self, **kwargs): for key, default in Template._valid_properties.items(): @@ -188,7 +342,8 @@ def parse(cls, json): setattr(template, key, parse_date(val)) elif key == 'attachments': attachments = [ - Attachment.parse(attachment) for attachment in val] + Attachment.parse(attachment) for attachment in val + ] setattr(template, key, attachments) elif key in cls._valid_properties: setattr(template, key, val) @@ -197,9 +352,14 @@ def parse(cls, json): class Page(Model): _valid_properties = { - 'id': None, 'name': None, 'html': None, 'modified_date': datetime.now(tzlocal()), - 'capture_credentials': False, 'capture_passwords': False, - 'redirect_url': None} + 'id': None, + 'name': None, + 'html': None, + 'modified_date': datetime.now(tzlocal()), + 'capture_credentials': False, + 'capture_passwords': False, + 'redirect_url': None + } def __init__(self, **kwargs): for key, default in Page._valid_properties.items(): @@ -215,6 +375,7 @@ def parse(cls, json): setattr(page, key, val) return page + class Attachment(Model): _valid_properties = {'content': None, 'type': None, 'name': None} @@ -226,6 +387,7 @@ def parse(cls, json): setattr(attachment, key, val) return attachment + class Error(Model): _valid_properties = {'message', 'success', 'data'} From 7e0389790f76e7bde8cdd14f2c84410e19d6f91a Mon Sep 17 00:00:00 2001 From: Mart Date: Sat, 8 Sep 2018 19:43:54 -0400 Subject: [PATCH 07/32] Fix for typo and unicode error (#11) --- gophish/api/api.py | 2 +- gophish/models.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gophish/api/api.py b/gophish/api/api.py index 201420a..8c39821 100644 --- a/gophish/api/api.py +++ b/gophish/api/api.py @@ -94,7 +94,7 @@ def put(self, resource): response = self.api.execute("PUT", endpoint, json=resource.as_json()) - if not respose.ok: + if not response.ok: return Error.parse(response.json()) return self._cls.parse(response.json()) diff --git a/gophish/models.py b/gophish/models.py index 8a56edf..87b2297 100644 --- a/gophish/models.py +++ b/gophish/models.py @@ -22,7 +22,7 @@ def as_dict(self): if isinstance(val, datetime): val = val.isoformat() # Parse custom classes - elif val and not isinstance(val, (int, float, str, list, dict, unicode)): + elif val and not isinstance(val, (int, float, str, list, dict)): val = val.as_dict() # Parse lists of objects elif isinstance(val, list): From 48456fc77cdea237cc4b38224e88f3ba456e6a2b Mon Sep 17 00:00:00 2001 From: Jordan Wright Date: Sat, 8 Sep 2018 18:45:38 -0500 Subject: [PATCH 08/32] Bumping release to 0.1.4 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2385e9d..0c580b6 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name = 'gophish', packages = ['gophish', 'gophish.api'], - version = '0.1.3', + version = '0.1.4', description = 'Python API Client for Gophish', author = 'Jordan Wright', author_email = 'python@getgophish.com', From d50435df50ecea9489c211c269e1644392d209b5 Mon Sep 17 00:00:00 2001 From: Jordan Wright Date: Sat, 8 Sep 2018 18:46:24 -0500 Subject: [PATCH 09/32] Bumping revision to 0.1.5 to include previous fix --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0c580b6..eda6128 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name = 'gophish', packages = ['gophish', 'gophish.api'], - version = '0.1.4', + version = '0.1.5', description = 'Python API Client for Gophish', author = 'Jordan Wright', author_email = 'python@getgophish.com', From 7603dc5f7b01cc4337460436830e310719cec3a1 Mon Sep 17 00:00:00 2001 From: Jordan Wright Date: Mon, 8 Oct 2018 23:31:11 -0500 Subject: [PATCH 10/32] Cleaning up formatting --- gophish/api/api.py | 14 +++++++------- gophish/api/campaigns.py | 5 ++--- gophish/api/groups.py | 1 + gophish/api/pages.py | 3 ++- gophish/api/smtp.py | 1 + gophish/api/templates.py | 3 ++- setup.py | 38 +++++++++++++++++++------------------- 7 files changed, 34 insertions(+), 31 deletions(-) diff --git a/gophish/api/api.py b/gophish/api/api.py index 8c39821..b266b7e 100644 --- a/gophish/api/api.py +++ b/gophish/api/api.py @@ -1,8 +1,6 @@ -import requests - from gophish.models import Error ''' -api.py +api.py Base API endpoint class that abstracts basic CRUD operations. ''' @@ -32,14 +30,16 @@ def get(self, resource_cls=None, single_resource=False): """ Gets the details for one or more resources by ID - + Args: cls - gophish.models.Model - The resource class resource_id - str - The endpoint (URL path) for the resource resource_action - str - An action to perform on the resource - resource_cls - cls - A class to use for parsing, if different than the base resource - single_resource - bool - An override to tell Gophish that even - though we aren't requesting a single resource, we expect a single response object + resource_cls - cls - A class to use for parsing, if different than + the base resource + single_resource - bool - An override to tell Gophish that even + though we aren't requesting a single resource, we expect a + single response object Returns: One or more instances of cls parsed from the returned JSON diff --git a/gophish/api/campaigns.py b/gophish/api/campaigns.py index 5b1d401..2401647 100644 --- a/gophish/api/campaigns.py +++ b/gophish/api/campaigns.py @@ -1,6 +1,5 @@ -import json - -from gophish.models import Campaign, CampaignSummary, CampaignSummaries, CampaignResults, Error +from gophish.models import (Campaign, CampaignSummary, CampaignSummaries, + CampaignResults) from gophish.api import APIEndpoint diff --git a/gophish/api/groups.py b/gophish/api/groups.py index 780d9b7..63ecf76 100644 --- a/gophish/api/groups.py +++ b/gophish/api/groups.py @@ -1,6 +1,7 @@ from gophish.models import Group from gophish.api import APIEndpoint + class API(APIEndpoint): def __init__(self, api, endpoint='/api/groups/'): super(API, self).__init__(api, endpoint=endpoint, cls=Group) diff --git a/gophish/api/pages.py b/gophish/api/pages.py index 406c638..46ec3fb 100644 --- a/gophish/api/pages.py +++ b/gophish/api/pages.py @@ -1,13 +1,14 @@ from gophish.models import Page from gophish.api import APIEndpoint + class API(APIEndpoint): def __init__(self, api, endpoint='/api/pages/'): super(API, self).__init__(api, endpoint=endpoint, cls=Page) def get(self, page_id=None): """ Gets one or more pages """ - + return super(API, self).get(resource_id=page_id) def post(self, page): diff --git a/gophish/api/smtp.py b/gophish/api/smtp.py index 11f96a3..13cdf67 100644 --- a/gophish/api/smtp.py +++ b/gophish/api/smtp.py @@ -1,6 +1,7 @@ from gophish.models import SMTP from gophish.api import APIEndpoint + class API(APIEndpoint): def __init__(self, api, endpoint='/api/smtp/'): super(API, self).__init__(api, endpoint=endpoint, cls=SMTP) diff --git a/gophish/api/templates.py b/gophish/api/templates.py index e98cff6..6acd355 100644 --- a/gophish/api/templates.py +++ b/gophish/api/templates.py @@ -1,13 +1,14 @@ from gophish.models import Template from gophish.api import APIEndpoint + class API(APIEndpoint): def __init__(self, api, endpoint='/api/templates/'): super(API, self).__init__(api, endpoint=endpoint, cls=Template) def get(self, template_id=None): """ Gets one or more templates """ - + return super(API, self).get(resource_id=template_id) def post(self, template): diff --git a/setup.py b/setup.py index eda6128..7a7cac6 100644 --- a/setup.py +++ b/setup.py @@ -1,23 +1,23 @@ from setuptools import setup setup( - name = 'gophish', - packages = ['gophish', 'gophish.api'], - version = '0.1.5', - description = 'Python API Client for Gophish', - author = 'Jordan Wright', - author_email = 'python@getgophish.com', - url = 'https://github.com/gophish/api-client-python', - license='MIT', - download_url = 'https://github.com/gophish/api-client-python/tarball/0.1.3', - keywords = ['gophish'], - classifiers = [ - 'Development Status :: 3 - Alpha', - 'Intended Audience :: Developers', - 'Natural Language :: English', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - ], + name='gophish', + packages=['gophish', 'gophish.api'], + version='0.1.5', + description='Python API Client for Gophish', + author='Jordan Wright', + author_email='python@getgophish.com', + url='https://github.com/gophish/api-client-python', + license='MIT', + download_url='https://github.com/gophish/api-client-python/tarball/0.1.3', + keywords=['gophish'], + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Intended Audience :: Developers', + 'Natural Language :: English', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + ], ) From 03f02d52d1e04ce30cabdffa4563eee79fe0c83a Mon Sep 17 00:00:00 2001 From: Christian Schwartz Date: Sat, 3 Nov 2018 10:12:40 +0100 Subject: [PATCH 11/32] Correctly parses Templates without Attachments. Fixes #13 (#14) --- gophish/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gophish/models.py b/gophish/models.py index 87b2297..6e2649f 100644 --- a/gophish/models.py +++ b/gophish/models.py @@ -340,7 +340,7 @@ def parse(cls, json): for key, val in json.items(): if key == 'modified_date': setattr(template, key, parse_date(val)) - elif key == 'attachments': + elif key == 'attachments' and val: attachments = [ Attachment.parse(attachment) for attachment in val ] From 38046ca68119885f42b48f6276a82859cd3949bc Mon Sep 17 00:00:00 2001 From: Jordan Wright Date: Sun, 4 Nov 2018 19:56:56 -0600 Subject: [PATCH 12/32] Bumped version to 0.1.6 --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 7a7cac6..db9aab8 100644 --- a/setup.py +++ b/setup.py @@ -3,13 +3,13 @@ setup( name='gophish', packages=['gophish', 'gophish.api'], - version='0.1.5', + version='0.1.6', description='Python API Client for Gophish', author='Jordan Wright', author_email='python@getgophish.com', url='https://github.com/gophish/api-client-python', license='MIT', - download_url='https://github.com/gophish/api-client-python/tarball/0.1.3', + download_url='https://github.com/gophish/api-client-python/tarball/0.1.6', keywords=['gophish'], classifiers=[ 'Development Status :: 3 - Alpha', From 6780eb65e3823244197caa56dab6686ef6219faf Mon Sep 17 00:00:00 2001 From: Jordan Wright Date: Mon, 5 Nov 2018 14:50:22 -0600 Subject: [PATCH 13/32] Raising errors instead of returning them as objects. Bumped version to 0.2.0 --- gophish/api/api.py | 8 ++++---- gophish/models.py | 20 +++++++++++++++++--- setup.py | 4 ++-- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/gophish/api/api.py b/gophish/api/api.py index b266b7e..db88cad 100644 --- a/gophish/api/api.py +++ b/gophish/api/api.py @@ -58,7 +58,7 @@ def get(self, response = self.api.execute("GET", endpoint) if not response.ok: - return Error.parse(response.json()) + raise Error.parse(response.json()) if resource_id or single_resource: return resource_cls.parse(response.json()) @@ -76,7 +76,7 @@ def post(self, resource): "POST", self.endpoint, json=(resource.as_dict())) if not response.ok: - return Error.parse(response.json()) + raise Error.parse(response.json()) return self._cls.parse(response.json()) @@ -95,7 +95,7 @@ def put(self, resource): response = self.api.execute("PUT", endpoint, json=resource.as_json()) if not response.ok: - return Error.parse(response.json()) + raise Error.parse(response.json()) return self._cls.parse(response.json()) @@ -111,6 +111,6 @@ def delete(self, resource_id): response = self.api.execute("DELETE", endpoint) if not response.ok: - return Error.parse(response.json()) + raise Error.parse(response.json()) return self._cls.parse(response.json()) diff --git a/gophish/models.py b/gophish/models.py index 6e2649f..adf283e 100644 --- a/gophish/models.py +++ b/gophish/models.py @@ -17,16 +17,21 @@ def __init__(self): def as_dict(self): """ Returns a dict representation of the resource """ result = {} + print(self._valid_properties) for key in self._valid_properties: val = getattr(self, key) if isinstance(val, datetime): val = val.isoformat() # Parse custom classes - elif val and not isinstance(val, (int, float, str, list, dict)): + elif val and not isinstance(val, + (int, float, str, list, dict, bool)): val = val.as_dict() # Parse lists of objects elif isinstance(val, list): val = [e.as_dict() for e in val] + # If it's a boolean, add it regardless of the value + elif isinstance(val, bool): + result[key] = val # Add it if it's not None if val: @@ -388,8 +393,17 @@ def parse(cls, json): return attachment -class Error(Model): - _valid_properties = {'message', 'success', 'data'} +class Error(Exception, Model): + _valid_properties = {'message': None, 'success': None, 'data': None} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def __str__(self): + return self.message + + def __repr__(self): + return _json.dumps(self.as_dict()) @classmethod def parse(cls, json): diff --git a/setup.py b/setup.py index db9aab8..88cc071 100644 --- a/setup.py +++ b/setup.py @@ -3,13 +3,13 @@ setup( name='gophish', packages=['gophish', 'gophish.api'], - version='0.1.6', + version='0.2.0', description='Python API Client for Gophish', author='Jordan Wright', author_email='python@getgophish.com', url='https://github.com/gophish/api-client-python', license='MIT', - download_url='https://github.com/gophish/api-client-python/tarball/0.1.6', + download_url='https://github.com/gophish/api-client-python/tarball/0.2.0', keywords=['gophish'], classifiers=[ 'Development Status :: 3 - Alpha', From 67707513e8c25c2a0cd291a139d79dae8b73c8bc Mon Sep 17 00:00:00 2001 From: Jordan Wright Date: Mon, 5 Nov 2018 14:58:14 -0600 Subject: [PATCH 14/32] Removed print statement - bumped version to 0.2.1 --- gophish/models.py | 1 - setup.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/gophish/models.py b/gophish/models.py index adf283e..e5eadd3 100644 --- a/gophish/models.py +++ b/gophish/models.py @@ -17,7 +17,6 @@ def __init__(self): def as_dict(self): """ Returns a dict representation of the resource """ result = {} - print(self._valid_properties) for key in self._valid_properties: val = getattr(self, key) if isinstance(val, datetime): diff --git a/setup.py b/setup.py index 88cc071..b701c2e 100644 --- a/setup.py +++ b/setup.py @@ -3,13 +3,13 @@ setup( name='gophish', packages=['gophish', 'gophish.api'], - version='0.2.0', + version='0.2.1', description='Python API Client for Gophish', author='Jordan Wright', author_email='python@getgophish.com', url='https://github.com/gophish/api-client-python', license='MIT', - download_url='https://github.com/gophish/api-client-python/tarball/0.2.0', + download_url='https://github.com/gophish/api-client-python/tarball/0.2.1', keywords=['gophish'], classifiers=[ 'Development Status :: 3 - Alpha', From 890aee69f1fb801f77461dc00526927bee02ee8f Mon Sep 17 00:00:00 2001 From: Jordan Wright Date: Tue, 6 Nov 2018 15:03:03 -0600 Subject: [PATCH 15/32] Added send_by_date parameter. Bumped version to 0.2.2 --- gophish/models.py | 3 ++- setup.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/gophish/models.py b/gophish/models.py index e5eadd3..487027d 100644 --- a/gophish/models.py +++ b/gophish/models.py @@ -49,6 +49,7 @@ class Campaign(Model): 'name': None, 'created_date': datetime.now(tzlocal()), 'launch_date': datetime.now(tzlocal()), + 'send_by_date': None, 'completed_date': None, 'template': None, 'page': None, @@ -58,7 +59,6 @@ class Campaign(Model): 'smtp': None, 'url': None, 'groups': [], - 'profile': None } def __init__(self, **kwargs): @@ -118,6 +118,7 @@ class CampaignSummary(Model): 'name': None, 'status': None, 'created_date': None, + 'send_by_date': None, 'launch_date': None, 'completed_date': None, 'stats': None diff --git a/setup.py b/setup.py index b701c2e..2e6c3b2 100644 --- a/setup.py +++ b/setup.py @@ -3,13 +3,13 @@ setup( name='gophish', packages=['gophish', 'gophish.api'], - version='0.2.1', + version='0.2.2', description='Python API Client for Gophish', author='Jordan Wright', author_email='python@getgophish.com', url='https://github.com/gophish/api-client-python', license='MIT', - download_url='https://github.com/gophish/api-client-python/tarball/0.2.1', + download_url='https://github.com/gophish/api-client-python/tarball/0.2.2', keywords=['gophish'], classifiers=[ 'Development Status :: 3 - Alpha', From 05eed67a7f961a79b5622dc48439e7196f4460b6 Mon Sep 17 00:00:00 2001 From: Jordan Wright Date: Tue, 6 Nov 2018 15:04:08 -0600 Subject: [PATCH 16/32] Bumped requests dependency version --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f3261e4..a1d8a8e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,5 +2,5 @@ appdirs==1.4.0 packaging==16.8 pyparsing==2.1.10 python-dateutil==2.6.0 -requests==2.12.5 +requests>=2.20.0 six==1.10.0 From 378dc26ea191cbed68ec1d33fd301ee219d30c36 Mon Sep 17 00:00:00 2001 From: Jordan Wright Date: Thu, 20 Dec 2018 22:37:09 -0600 Subject: [PATCH 17/32] Fixing syntax error for PUT requests. Updating the way URLs are built to be more reliable --- gophish/api/api.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/gophish/api/api.py b/gophish/api/api.py index db88cad..757290e 100644 --- a/gophish/api/api.py +++ b/gophish/api/api.py @@ -24,6 +24,19 @@ def __init__(self, api, endpoint=None, cls=None): self.endpoint = endpoint self._cls = cls + def _build_url(self, *parts): + """Builds a path to an API resource by joining the individual parts + with a slash (/). + + This is used instead of urljoin since we're given relative URL parts + which need to be chained together. + + Returns: + str -- The parts joined with a slash + """ + + return '/'.join(str(part).rstrip('/') for part in parts) + def get(self, resource_id=None, resource_action=None, @@ -51,10 +64,10 @@ def get(self, resource_cls = self._cls if resource_id: - endpoint = '{}/{}'.format(endpoint, resource_id) + endpoint = self._build_url(endpoint, resource_id) if resource_action: - endpoint = '{}/{}'.format(endpoint, resource_action) + endpoint = self._build_url(endpoint, resource_action) response = self.api.execute("GET", endpoint) if not response.ok: @@ -90,9 +103,9 @@ def put(self, resource): endpoint = self.endpoint if resource.id: - endpoint = '{}/{}'.format(endpoint, resource.id) + endpoint = self._build_url(endpoint, resource.id) - response = self.api.execute("PUT", endpoint, json=resource.as_json()) + response = self.api.execute("PUT", endpoint, json=resource.as_dict()) if not response.ok: raise Error.parse(response.json()) From 825bea12e16e1e1fcf36ab6649252e321d8860d2 Mon Sep 17 00:00:00 2001 From: Jordan Wright Date: Sat, 29 Dec 2018 23:29:58 -0600 Subject: [PATCH 18/32] Added SMTP auth parameters and SMTP headers. Fixed PUT page/:id. Bumped version to 0.2.4. --- gophish/api/pages.py | 2 +- gophish/models.py | 19 +++++++++++++++---- setup.py | 2 +- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/gophish/api/pages.py b/gophish/api/pages.py index 46ec3fb..0f69157 100644 --- a/gophish/api/pages.py +++ b/gophish/api/pages.py @@ -19,7 +19,7 @@ def post(self, page): def put(self, page): """ Edits a page """ - return super(API, self).put(put) + return super(API, self).put(page) def delete(self, page_id): """ Deletes a page by ID """ diff --git a/gophish/models.py b/gophish/models.py index 487027d..c0f3d8e 100644 --- a/gophish/models.py +++ b/gophish/models.py @@ -14,6 +14,10 @@ class Model(object): def __init__(self): self._valid_properties = {} + @classmethod + def _is_builtin(cls, obj): + return isinstance(obj, (int, float, str, list, dict, bool)) + def as_dict(self): """ Returns a dict representation of the resource """ result = {} @@ -22,12 +26,16 @@ def as_dict(self): if isinstance(val, datetime): val = val.isoformat() # Parse custom classes - elif val and not isinstance(val, - (int, float, str, list, dict, bool)): + elif val and not Model._is_builtin(val): val = val.as_dict() # Parse lists of objects elif isinstance(val, list): - val = [e.as_dict() for e in val] + # We only want to call as_dict in the case where the item + # isn't a builtin type. + for i in range(len(val)): + if Model._is_builtin(val[i]): + continue + val[i] = val[i].as_dict() # If it's a boolean, add it regardless of the value elif isinstance(val, bool): result[key] = val @@ -304,9 +312,12 @@ class SMTP(Model): 'interface_type': 'SMTP', 'name': None, 'host': None, + 'username': None, + 'password': None, 'from_address': None, 'ignore_cert_errors': False, - 'modified_date': datetime.now(tzlocal()) + 'modified_date': datetime.now(tzlocal()), + 'headers': [] } def __init__(self, **kwargs): diff --git a/setup.py b/setup.py index 2e6c3b2..7762ba8 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='gophish', packages=['gophish', 'gophish.api'], - version='0.2.2', + version='0.2.4', description='Python API Client for Gophish', author='Jordan Wright', author_email='python@getgophish.com', From 28a7790f19e13c92ef0fb7bde8cd89389df5c155 Mon Sep 17 00:00:00 2001 From: Jordan Wright Date: Sun, 30 Dec 2018 12:32:25 -0600 Subject: [PATCH 19/32] Changed client to use Authorization header instead of GET parameter. --- gophish/client.py | 5 ++++- setup.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/gophish/client.py b/gophish/client.py index 586c6b9..61e0562 100644 --- a/gophish/client.py +++ b/gophish/client.py @@ -19,7 +19,10 @@ def execute(self, method, path, **kwargs): url = "{}{}".format(self.host, path) kwargs.update(self._client_kwargs) response = requests.request( - method, url, params={"api_key": self.api_key}, **kwargs) + method, + url, + headers={"Authorization": "Bearer {}".format(self.api_key)}, + **kwargs) return response diff --git a/setup.py b/setup.py index 7762ba8..1666fc0 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='gophish', packages=['gophish', 'gophish.api'], - version='0.2.4', + version='0.2.5', description='Python API Client for Gophish', author='Jordan Wright', author_email='python@getgophish.com', From b91511257dee32bfe04accee9b7975ffd8412943 Mon Sep 17 00:00:00 2001 From: quelsan <52572277+quelsan@users.noreply.github.com> Date: Sat, 20 Jul 2019 02:49:41 +0200 Subject: [PATCH 20/32] Fixed issue #17 (#18) The API client was appending an extra forward slash infront of all request paths, which caused the CSRF protection to trigger on state changing request. This commit fixes the issue. Related issue in Gophish: https://github.com/gophish/gophish/issues/1506 --- gophish/api/campaigns.py | 2 +- gophish/api/groups.py | 2 +- gophish/api/pages.py | 2 +- gophish/api/smtp.py | 2 +- gophish/api/templates.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gophish/api/campaigns.py b/gophish/api/campaigns.py index 2401647..3827ec8 100644 --- a/gophish/api/campaigns.py +++ b/gophish/api/campaigns.py @@ -4,7 +4,7 @@ class API(APIEndpoint): - def __init__(self, api, endpoint='/api/campaigns/'): + def __init__(self, api, endpoint='api/campaigns/'): """ Creates a new instance of the campaigns API """ super(API, self).__init__(api, endpoint=endpoint, cls=Campaign) diff --git a/gophish/api/groups.py b/gophish/api/groups.py index 63ecf76..fb3fea9 100644 --- a/gophish/api/groups.py +++ b/gophish/api/groups.py @@ -3,7 +3,7 @@ class API(APIEndpoint): - def __init__(self, api, endpoint='/api/groups/'): + def __init__(self, api, endpoint='api/groups/'): super(API, self).__init__(api, endpoint=endpoint, cls=Group) def get(self, group_id=None): diff --git a/gophish/api/pages.py b/gophish/api/pages.py index 0f69157..00e3bad 100644 --- a/gophish/api/pages.py +++ b/gophish/api/pages.py @@ -3,7 +3,7 @@ class API(APIEndpoint): - def __init__(self, api, endpoint='/api/pages/'): + def __init__(self, api, endpoint='api/pages/'): super(API, self).__init__(api, endpoint=endpoint, cls=Page) def get(self, page_id=None): diff --git a/gophish/api/smtp.py b/gophish/api/smtp.py index 13cdf67..2abf509 100644 --- a/gophish/api/smtp.py +++ b/gophish/api/smtp.py @@ -3,7 +3,7 @@ class API(APIEndpoint): - def __init__(self, api, endpoint='/api/smtp/'): + def __init__(self, api, endpoint='api/smtp/'): super(API, self).__init__(api, endpoint=endpoint, cls=SMTP) def get(self, smtp_id=None): diff --git a/gophish/api/templates.py b/gophish/api/templates.py index 6acd355..a2a4863 100644 --- a/gophish/api/templates.py +++ b/gophish/api/templates.py @@ -3,7 +3,7 @@ class API(APIEndpoint): - def __init__(self, api, endpoint='/api/templates/'): + def __init__(self, api, endpoint='api/templates/'): super(API, self).__init__(api, endpoint=endpoint, cls=Template) def get(self, template_id=None): From 9a86be5908c8759f4e9cf8fdf29e4b9203753cbf Mon Sep 17 00:00:00 2001 From: Mark Feldhousen Date: Tue, 30 Jul 2019 23:13:43 -0400 Subject: [PATCH 21/32] Include required dependencies in setup.py (#16) * Move package dependencies into setup.py install_requires * Add dev requirements * Remove requirements files per @jordan-wright's request. --- requirements.txt | 6 ------ setup.py | 43 ++++++++++++++++++++++++++----------------- 2 files changed, 26 insertions(+), 23 deletions(-) delete mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index a1d8a8e..0000000 --- a/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -appdirs==1.4.0 -packaging==16.8 -pyparsing==2.1.10 -python-dateutil==2.6.0 -requests>=2.20.0 -six==1.10.0 diff --git a/setup.py b/setup.py index 1666fc0..90580be 100644 --- a/setup.py +++ b/setup.py @@ -1,23 +1,32 @@ +"""This is the setup module for the Python Gophish API client.""" from setuptools import setup setup( - name='gophish', - packages=['gophish', 'gophish.api'], - version='0.2.5', - description='Python API Client for Gophish', - author='Jordan Wright', - author_email='python@getgophish.com', - url='https://github.com/gophish/api-client-python', - license='MIT', - download_url='https://github.com/gophish/api-client-python/tarball/0.2.2', - keywords=['gophish'], + name="gophish", + packages=["gophish", "gophish.api"], + version="0.2.5", + description="Python API Client for Gophish", + author="Jordan Wright", + author_email="python@getgophish.com", + url="https://github.com/gophish/api-client-python", + license="MIT", + download_url="https://github.com/gophish/api-client-python/tarball/0.2.2", + keywords=["gophish"], classifiers=[ - 'Development Status :: 3 - Alpha', - 'Intended Audience :: Developers', - 'Natural Language :: English', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Natural Language :: English", + "Programming Language :: Python", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.3", + ], + install_requires=[ + "appdirs==1.4.0", + "packaging==16.8", + "pyparsing==2.1.10", + "python-dateutil==2.6.0", + "requests>=2.20.0", + "six==1.10.0", ], ) From 416d24b7147fbba8852f7cca5d24b6b361d3dde3 Mon Sep 17 00:00:00 2001 From: quelsan <52572277+quelsan@users.noreply.github.com> Date: Fri, 2 Aug 2019 03:07:22 +0200 Subject: [PATCH 22/32] Added validation of "host" attribute (#20) Modified the init functionality to add a trailing forward slash to the host attribute if none is provided, which is required. This commit complements https://github.com/gophish/api-client-python/pull/18 , which caused an issue if a trailing slash was missing. --- gophish/client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gophish/client.py b/gophish/client.py index 61e0562..a945a24 100644 --- a/gophish/client.py +++ b/gophish/client.py @@ -10,7 +10,10 @@ class GophishClient(object): def __init__(self, api_key, host=DEFAULT_URL, **kwargs): self.api_key = api_key - self.host = host + if host.endswith('/'): + self.host = host + else: + self.host = host + '/' self._client_kwargs = kwargs def execute(self, method, path, **kwargs): From 356196006b71a98e7d50793ecf7caa3260b5b92d Mon Sep 17 00:00:00 2001 From: Jordan Wright Date: Sun, 2 Feb 2020 11:50:33 -0600 Subject: [PATCH 23/32] Create pythonpublish.yml Initial commit of automatic Python package release management via GitHub Actions. --- .github/workflows/pythonpublish.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/pythonpublish.yml diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml new file mode 100644 index 0000000..b143a53 --- /dev/null +++ b/.github/workflows/pythonpublish.yml @@ -0,0 +1,26 @@ +name: Upload Python Package + +on: + release: + types: [created] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Build and publish + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + python setup.py sdist bdist_wheel + twine upload dist/* From 69f00cae3e7a3d165481153b16f94564690bfd02 Mon Sep 17 00:00:00 2001 From: Jordan Wright Date: Thu, 1 Aug 2019 20:13:57 -0500 Subject: [PATCH 24/32] Bumping version to 0.3.0 --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 90580be..2f4d452 100644 --- a/setup.py +++ b/setup.py @@ -4,13 +4,13 @@ setup( name="gophish", packages=["gophish", "gophish.api"], - version="0.2.5", + version="0.3.0", description="Python API Client for Gophish", author="Jordan Wright", author_email="python@getgophish.com", url="https://github.com/gophish/api-client-python", license="MIT", - download_url="https://github.com/gophish/api-client-python/tarball/0.2.2", + download_url="https://github.com/gophish/api-client-python/tarball/0.3.0", keywords=["gophish"], classifiers=[ "Development Status :: 3 - Alpha", From f5c4cccec7c9fd4005e72f53f654b384442da494 Mon Sep 17 00:00:00 2001 From: Jordan Wright Date: Sun, 2 Feb 2020 20:25:46 -0600 Subject: [PATCH 25/32] Adding webhook support --- gophish/api/api.py | 57 +++++++++++++++++++++++++---------------- gophish/api/webhooks.py | 54 ++++++++++++++++++++++++++++++++++++++ gophish/client.py | 4 +-- gophish/models.py | 22 ++++++++++++++++ 4 files changed, 113 insertions(+), 24 deletions(-) create mode 100644 gophish/api/webhooks.py diff --git a/gophish/api/api.py b/gophish/api/api.py index 757290e..3c10074 100644 --- a/gophish/api/api.py +++ b/gophish/api/api.py @@ -11,7 +11,6 @@ class APIEndpoint(object): Represents an API endpoint for Gophish, containing common patterns for CRUD operations. """ - def __init__(self, api, endpoint=None, cls=None): """ Creates an instance of the APIEndpoint class. @@ -37,6 +36,33 @@ def _build_url(self, *parts): return '/'.join(str(part).rstrip('/') for part in parts) + def request(self, + method, + resource_id=None, + resource_action=None, + resource_cls=None, + single_resource=False): + + endpoint = self.endpoint + + if not resource_cls: + resource_cls = self._cls + + if resource_id: + endpoint = self._build_url(endpoint, resource_id) + + if resource_action: + endpoint = self._build_url(endpoint, resource_action) + + response = self.api.execute(method, endpoint) + if not response.ok: + raise Error.parse(response.json()) + + if resource_id or single_resource: + return resource_cls.parse(response.json()) + + return [resource_cls.parse(resource) for resource in response.json()] + def get(self, resource_id=None, resource_action=None, @@ -58,25 +84,11 @@ def get(self, One or more instances of cls parsed from the returned JSON """ - endpoint = self.endpoint - - if not resource_cls: - resource_cls = self._cls - - if resource_id: - endpoint = self._build_url(endpoint, resource_id) - - if resource_action: - endpoint = self._build_url(endpoint, resource_action) - - response = self.api.execute("GET", endpoint) - if not response.ok: - raise Error.parse(response.json()) - - if resource_id or single_resource: - return resource_cls.parse(response.json()) - - return [resource_cls.parse(resource) for resource in response.json()] + return self.request("GET", + resource_id=resource_id, + resource_action=resource_action, + resource_cls=resource_cls, + single_resource=single_resource) def post(self, resource): """ Creates a new instance of the resource. @@ -85,8 +97,9 @@ def post(self, resource): resource - gophish.models.Model - The resource instance """ - response = self.api.execute( - "POST", self.endpoint, json=(resource.as_dict())) + response = self.api.execute("POST", + self.endpoint, + json=(resource.as_dict())) if not response.ok: raise Error.parse(response.json()) diff --git a/gophish/api/webhooks.py b/gophish/api/webhooks.py new file mode 100644 index 0000000..84c067d --- /dev/null +++ b/gophish/api/webhooks.py @@ -0,0 +1,54 @@ +from gophish.models import Webhook +from gophish.api import APIEndpoint + + +class API(APIEndpoint): + def __init__(self, api, endpoint='api/webhooks/'): + super(API, self).__init__(api, endpoint=endpoint, cls=Webhook) + + def get(self, webhook_id=None): + """Gets one or more webhooks + + Keyword Arguments: + webhook_id {int} -- The ID of the Webhook (optional, default: {None}) + """ + + return super(API, self).get(resource_id=webhook_id) + + def post(self, webhook): + """Creates a new webhook + + Arguments: + webhook {gophish.models.Webhook} -- The webhook to create + """ + + return super(API, self).post(webhook) + + def put(self, webhook): + """Edits a webhook + + Arguments: + webhook {gophish.models.Webhook} -- The updated webhook details + """ + + return super(API, self).put(webhook) + + def delete(self, webhook_id): + """Deletes a webhook by ID + + Arguments: + webhook_id {int} -- The ID of the webhook to delete + """ + return super(API, self).delete(webhook_id) + + def validate(self, webhook_id): + """Sends a validation payload to the webhook specified by the given ID + + Arguments: + webhook_id {int} -- The ID of the webhook to validate + """ + return self.request("POST", + resource_id=webhook_id, + resource_action='validate', + resource_cls=Webhook, + single_resource=True) diff --git a/gophish/client.py b/gophish/client.py index a945a24..4baffb3 100644 --- a/gophish/client.py +++ b/gophish/client.py @@ -1,13 +1,12 @@ import requests -from gophish.api import (campaigns, groups, pages, smtp, templates) +from gophish.api import (campaigns, groups, pages, smtp, templates, webhooks) DEFAULT_URL = 'http://localhost:3333' class GophishClient(object): """ A standard HTTP REST client used by Gophish """ - def __init__(self, api_key, host=DEFAULT_URL, **kwargs): self.api_key = api_key if host.endswith('/'): @@ -41,3 +40,4 @@ def __init__(self, self.pages = pages.API(self.client) self.smtp = smtp.API(self.client) self.templates = templates.API(self.client) + self.webhooks = webhooks.API(self.client) diff --git a/gophish/models.py b/gophish/models.py index c0f3d8e..2241dc0 100644 --- a/gophish/models.py +++ b/gophish/models.py @@ -404,6 +404,28 @@ def parse(cls, json): return attachment +class Webhook(Model): + _valid_properties = { + 'id': None, + 'name': None, + 'url': None, + 'secret': None, + 'is_active': None + } + + def __init__(self, **kwargs): + for key, default in Webhook._valid_properties.items(): + setattr(self, key, kwargs.get(key, default)) + + @classmethod + def parse(cls, json): + webhook = cls() + for key, val in json.items(): + if key in cls._valid_properties: + setattr(webhook, key, val) + return webhook + + class Error(Exception, Model): _valid_properties = {'message': None, 'success': None, 'data': None} From cd2b18f0a0a798d7d0dc9fe046ae53c998c6fdea Mon Sep 17 00:00:00 2001 From: Jordan Wright Date: Sun, 2 Feb 2020 21:22:45 -0600 Subject: [PATCH 26/32] Added IMAP support --- gophish/api/api.py | 3 ++- gophish/api/imap.py | 34 ++++++++++++++++++++++++++++++++ gophish/client.py | 4 +++- gophish/models.py | 47 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 gophish/api/imap.py diff --git a/gophish/api/api.py b/gophish/api/api.py index 3c10074..3dbea14 100644 --- a/gophish/api/api.py +++ b/gophish/api/api.py @@ -38,6 +38,7 @@ def _build_url(self, *parts): def request(self, method, + body=None, resource_id=None, resource_action=None, resource_cls=None, @@ -54,7 +55,7 @@ def request(self, if resource_action: endpoint = self._build_url(endpoint, resource_action) - response = self.api.execute(method, endpoint) + response = self.api.execute(method, endpoint, json=body) if not response.ok: raise Error.parse(response.json()) diff --git a/gophish/api/imap.py b/gophish/api/imap.py new file mode 100644 index 0000000..a7a8e08 --- /dev/null +++ b/gophish/api/imap.py @@ -0,0 +1,34 @@ +from gophish.models import IMAP, Success +from gophish.api import APIEndpoint + + +class API(APIEndpoint): + def __init__(self, api, endpoint='api/imap/'): + super(API, self).__init__(api, endpoint=endpoint, cls=IMAP) + + def get(self): + """Gets the configured IMAP settings + """ + + return super(API, self).get() + + def post(self, imap): + """Updates the IMAP settings + + Arguments: + imap {gophish.models.IMAP} -- The IMAP settings to configure + """ + + return super(API, self).post(imap) + + def validate(self, imap): + """Sends a validation payload to the webhook specified by the given ID + + Arguments: + webhook_id {int} -- The ID of the webhook to validate + """ + return self.request("POST", + body=imap.as_dict(), + resource_action='validate', + resource_cls=Success, + single_resource=True) diff --git a/gophish/client.py b/gophish/client.py index 4baffb3..944ab68 100644 --- a/gophish/client.py +++ b/gophish/client.py @@ -1,6 +1,7 @@ import requests -from gophish.api import (campaigns, groups, pages, smtp, templates, webhooks) +from gophish.api import (campaigns, groups, imap, pages, smtp, templates, + webhooks) DEFAULT_URL = 'http://localhost:3333' @@ -37,6 +38,7 @@ def __init__(self, self.client = client(api_key, host=host, **kwargs) self.campaigns = campaigns.API(self.client) self.groups = groups.API(self.client) + self.imap = imap.API(self.client) self.pages = pages.API(self.client) self.smtp = smtp.API(self.client) self.templates = templates.API(self.client) diff --git a/gophish/models.py b/gophish/models.py index 2241dc0..6e7c3f0 100644 --- a/gophish/models.py +++ b/gophish/models.py @@ -426,6 +426,53 @@ def parse(cls, json): return webhook +class IMAP(Model): + _valid_properties = { + 'enabled': None, + 'host': None, + 'port': None, + 'username': None, + 'password': None, + 'tls': None, + 'folder': None, + 'restrict_domain': None, + 'delete_reported_campaign_email': None, + 'last_login': None, + 'modified_date': None, + 'imap_freq': None + } + + def __init__(self, **kwargs): + for key, default in IMAP._valid_properties.items(): + setattr(self, key, kwargs.get(key, default)) + + @classmethod + def parse(cls, json): + imap = cls() + for key, val in json.items(): + if key in cls._valid_properties: + setattr(imap, key, val) + return imap + + +class Success(Exception, Model): + _valid_properties = {'message': None, 'success': None, 'data': None} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def __str__(self): + return self.message + + @classmethod + def parse(cls, json): + success = cls() + for key, val in json.items(): + if key in cls._valid_properties: + setattr(success, key, val) + return success + + class Error(Exception, Model): _valid_properties = {'message': None, 'success': None, 'data': None} From 0978ef5f9533c0d47305c96ab52d361b74108fc5 Mon Sep 17 00:00:00 2001 From: Jordan Wright Date: Sun, 2 Feb 2020 21:33:42 -0600 Subject: [PATCH 27/32] Bumped version to 0.4.0 --- LICENSE | 2 +- setup.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index e561eb0..75560dc 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017 gophish +Copyright (c) 2020 gophish Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/setup.py b/setup.py index 2f4d452..ac1b5d7 100644 --- a/setup.py +++ b/setup.py @@ -4,13 +4,13 @@ setup( name="gophish", packages=["gophish", "gophish.api"], - version="0.3.0", + version="0.4.0", description="Python API Client for Gophish", author="Jordan Wright", author_email="python@getgophish.com", url="https://github.com/gophish/api-client-python", license="MIT", - download_url="https://github.com/gophish/api-client-python/tarball/0.3.0", + download_url="https://github.com/gophish/api-client-python/tarball/0.4.0", keywords=["gophish"], classifiers=[ "Development Status :: 3 - Alpha", From 1ddcc746c37b4638eed76bc76ecf202740d26518 Mon Sep 17 00:00:00 2001 From: Hitesh Patel <35237459+HiteshPatel0101@users.noreply.github.com> Date: Mon, 24 Aug 2020 21:13:26 +0530 Subject: [PATCH 28/32] Update README.md (#28) ## Full Documentation Link was broken, removed gitbook URL and added proper link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4a0ca2e..e19d735 100644 --- a/README.md +++ b/README.md @@ -29,4 +29,4 @@ Now you're ready to start using the API! ## Full Documentation -You can find the full Python client documentation [here.](https://gophish.gitbooks.io/python-api-client/content/) +You can find the full Python client documentation [here.](https://docs.getgophish.com/python-api-client/) From f02819cac8d23a1aa24eef72e9ac04ad7bec6036 Mon Sep 17 00:00:00 2001 From: Jordan Wright Date: Thu, 17 Sep 2020 20:56:01 -0500 Subject: [PATCH 29/32] Updated dependencies --- setup.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index ac1b5d7..4b8aacc 100644 --- a/setup.py +++ b/setup.py @@ -22,11 +22,15 @@ "Programming Language :: Python :: 3.3", ], install_requires=[ - "appdirs==1.4.0", - "packaging==16.8", - "pyparsing==2.1.10", - "python-dateutil==2.6.0", - "requests>=2.20.0", - "six==1.10.0", + "appdirs==1.4.4", + "certifi==2020.6.20", + "chardet==3.0.4", + "idna==2.10", + "packaging==20.4", + "pyparsing==2.4.7", + "python-dateutil==2.8.1", + "requests==2.24.0", + "six==1.15.0", + "urllib3==1.25.10" ], ) From b5d82330d88072021a8f54f9fb8818beee986beb Mon Sep 17 00:00:00 2001 From: Jordan Wright Date: Thu, 17 Sep 2020 20:56:12 -0500 Subject: [PATCH 30/32] Changed default host to use HTTPS --- gophish/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gophish/client.py b/gophish/client.py index 944ab68..64d9c9f 100644 --- a/gophish/client.py +++ b/gophish/client.py @@ -3,7 +3,7 @@ from gophish.api import (campaigns, groups, imap, pages, smtp, templates, webhooks) -DEFAULT_URL = 'http://localhost:3333' +DEFAULT_URL = 'https://localhost:3333' class GophishClient(object): From 1a8a6be31d43925e8cec903c4834869951b68ea6 Mon Sep 17 00:00:00 2001 From: Jordan Wright Date: Thu, 17 Sep 2020 21:05:47 -0500 Subject: [PATCH 31/32] Bumping version to 0.5.1 --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 4b8aacc..9fc8a07 100644 --- a/setup.py +++ b/setup.py @@ -4,13 +4,13 @@ setup( name="gophish", packages=["gophish", "gophish.api"], - version="0.4.0", + version="0.5.1", description="Python API Client for Gophish", author="Jordan Wright", author_email="python@getgophish.com", url="https://github.com/gophish/api-client-python", license="MIT", - download_url="https://github.com/gophish/api-client-python/tarball/0.4.0", + download_url="https://github.com/gophish/api-client-python/tarball/0.5.1", keywords=["gophish"], classifiers=[ "Development Status :: 3 - Alpha", From a4094b866293ae25c5425981d9e7fb405a7beebe Mon Sep 17 00:00:00 2001 From: Glenn Wilkinson Date: Wed, 30 Nov 2022 12:06:38 +0000 Subject: [PATCH 32/32] Added email_reported to models Stat --- gophish/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gophish/models.py b/gophish/models.py index 6e7c3f0..f1a5044 100644 --- a/gophish/models.py +++ b/gophish/models.py @@ -156,6 +156,7 @@ class Stat(Model): 'opened': None, 'clicked': None, 'submitted_data': None, + 'email_reported': None, 'error': None }