diff --git a/README.md b/README.md index 47b9981..91d7c1d 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,9 @@ maltego-trx start new_project This will create a folder new_project with the recommend project structure. +Alternatively, you can copy either the `gunicorn` or `apache` example projects from the `demo` directory. +These also include Dockerfile and corresponding docker-compose configuration files for production deployment. + **Adding a Transform:** Add a new transform by creating a new python file in the "transforms" folder of your directory. @@ -201,6 +204,25 @@ The following constants can be imported from `maltego_trx.maltego`. - `LINK_STYLE_DOTTED` - `LINK_STYLE_DASHDOT` +### Enums + +**Overlays:** + +Overlays Enums are imported from `maltego_trx.overlays` + +*Overlay OverlayPosition:* +- `NORTH = "N"` +- `SOUTH = "S"` +- `WEST = "W"` +- `NORTH_WEST = "NW"` +- `SOUTH_WEST = "SW"` +- `CENTER = "C"` + +*Overlay Type* +- `IMAGE = "image"` +- `COLOUR = "colour"` +- `TEXT = "text"` + ### Request/MaltegoMsg The request/maltego msg object given to the transform contains the information about the input entity. @@ -213,18 +235,28 @@ The request/maltego msg object given to the transform contains the information a - `Type: str`: The input entity type - `Properties: dict(str: str)`: A key-value dictionary of the input entity properties - `TransformSettings: dict(str: str)`: A key-value dictionary of the transform settings +- `Genealogy: list(dict(str: str))`: A key-value dictionary of the Entity genealogy, + this is only applicable for extended entities e.g. Website Entity **Methods:** -- `getProperty(name: str)`: get a property value of the input entity -- `getTransformSetting(name: str)`: get a transform setting value +- `getProperty(name: str)`: Get a property value of the input entity +- `getTransformSetting(name: str)`: Get a transform setting value +- `clearLegacyProperties()`: Delete (duplicate) legacy properties from the input entity. This will not result in +property information being lost, it will simply clear out some properties that the TRX library duplicates on all +incoming Transform requests. In older versions of TRX, these Entity properties would have a different internal ID when +sent the server than what the Maltego client would advertise in the Entity Manager UI. For a list of Entities with such +properties and their corresponding legacy and actual IDs, see `entity_property_map` in `maltego_trx/entities.py`. For +the majority of projects this distinction can be safely ignored. ### Response/MaltegoTransform **Methods:** -- `addEntity(type: str, value: str) -> Entity`: Add an entity to the transform response. Returns an Entity object created by the method. -- `addUIMessage(message: str, messageType='Inform')`: Return a UI message to the user. For message type, use a message type constant. +- `addEntity(type: str, value: str) -> Entity`: Add an entity to the transform response. Returns an Entity object +created by the method. +- `addUIMessage(message: str, messageType='Inform')`: Return a UI message to the user. For message type, use a message +type constant. ### Entity @@ -234,10 +266,51 @@ The request/maltego msg object given to the transform contains the information a - `setValue(value: str)`: Set the entity value - `setWeight(weight: int)`: Set the entity weight - `addDisplayInformation(content: str, title: str)`: Add display information for the entity. -- `addProperty(fieldName: str, displayName: str, matchingRule: str, value: str)`: Add a property to the entity. Matching rule can be `strict` or `loose`. +- `addProperty(fieldName: str, displayName: str, matchingRule: str, value: str)`: Add a property to the entity. +Matching rule can be `strict` or `loose`. +- `addOverlay(propertyName: str, position: OverlayPosition, overlay_type: OverlayType)`: Add an overlay to the entity. +`OverlayPosition` and `OverlayType` are defined in the `maltego_tx.overlays` + +Overlay can be added as Text, Image or Color + +```python + + person_name = request.Value + entity = response.addEntity(Phrase, "Hi %s, nice to meet you!" % person_name) + + # Normally, when we create an overlay, we would reference a property name so that Maltego can then use the + # value of that property to create the overlay. Sometimes that means creating a dynamic property, but usually + # it's better to either use an existing property, or, if you created the Entity yourself, and only need the + # property for the overlay, to use a hidden property. Here's an example of using a dynamic property: + entity.addProperty( + 'dynamic_overlay_icon_name', + displayName="Name for overlay image", + value="Champion" # references an icon in the Maltego client + ) + entity.addOverlay('dynamic_overlay_icon_name', OverlayPosition.WEST, OverlayType.IMAGE) + + # DISCOURAGED: + # You *can* also directly supply the string value of the property, however this is not recommended. Why? If + # the entity already has a property of the same ID (in this case, "DE"), then you would in fact be assigning the + # value of that property, not the string "DE", which is not the intention. Nevertheless, here's an example: + entity.addOverlay( + 'DE', # name of an icon, however, could also accidentally be a property name + OverlayPosition.SOUTH_WEST, + OverlayType.IMAGE + ) + + # Overlays can also be used to display extra text on an entity: + entity.addProperty("exampleDynamicPropertyName", "Example Dynamic Property", "loose", "Maltego Overlay Testing") + entity.addOverlay('exampleDynamicPropertyName', OverlayPosition.NORTH, OverlayType.TEXT) + + # Or a small color indicator: + entity.addOverlay('#45e06f', OverlayPosition.NORTH_WEST, OverlayType.COLOUR) +``` + - `setIconURL(url: str)`: Set the entity icon URL - `setBookmark(bookmark: int)`: Set bookmark color index (e.g. -1 for BOOKMARK_COLOR_NONE, 3 for BOOKMARK_COLOR_PURPLE) - `setNote(note: str)`: Set note content +- `setGenealogy(genealogy: dict)`: Set genealogy **Link Methods:** diff --git a/demo/apache/transforms/OverlayExample.py b/demo/apache/transforms/OverlayExample.py new file mode 100644 index 0000000..7bd8ecc --- /dev/null +++ b/demo/apache/transforms/OverlayExample.py @@ -0,0 +1,36 @@ +from maltego_trx.entities import Phrase +from maltego_trx.overlays import OverlayPosition, OverlayType + +from maltego_trx.transform import DiscoverableTransform + + +class OverlayExample(DiscoverableTransform): + """ + Returns a phrase with overlays on the graph. + """ + + @classmethod + def create_entities(cls, request, response): + person_name = request.Value + entity = response.addEntity(Phrase, "Hi %s, nice to meet you!" % person_name) + + # Normally, when we create an overlay, we would reference a property name so that Maltego can then use the + # value of that property to create the overlay. Sometimes that means creating a dynamic property, but usually + # it's better to either use an existing property, or, if you created the Entity yourself, and only need the + # property for the overlay, to use a hidden property. Here's an example of using a dynamic property: + entity.addProperty('dynamic_overlay_icon_name', displayName="Name for overlay image", value="Champion") + entity.addOverlay('dynamic_overlay_icon_name', OverlayPosition.WEST, OverlayType.IMAGE) + + # DISCOURAGED: + # You *can* also directly supply the string value of the property, however this is not recommended. Why? If + # the entity already has a property of the same ID (in this case, "DE"), then you would in fact be assigning the + # value of that property, not the string "DE", which is not the intention. Nevertheless, here's an example: + entity.addOverlay('DE', OverlayPosition.SOUTH_WEST, OverlayType.IMAGE) + + # Overlays can also be used to display extra text on an entity: + entity.addProperty("exampleDynamicPropertyName", "Example Dynamic Property", "loose", "Maltego Overlay Testing") + entity.addOverlay('exampleDynamicPropertyName', OverlayPosition.NORTH, OverlayType.TEXT) + + # Or a small color indicator: + entity.addOverlay('#45e06f', OverlayPosition.NORTH_WEST, OverlayType.COLOUR) + diff --git a/demo/gunicorn/Dockerfile b/demo/gunicorn/Dockerfile index 66f56f7..42edd11 100644 --- a/demo/gunicorn/Dockerfile +++ b/demo/gunicorn/Dockerfile @@ -5,7 +5,7 @@ RUN mkdir /var/www/TRX/ WORKDIR /var/www/TRX/ # System dependencies -RUN apt-get update +RUN apt-get update -y RUN apt-get install python3-pip -y COPY requirements.txt requirements.txt @@ -18,4 +18,5 @@ COPY . /var/www/TRX/ RUN chown -R www-data:www-data /var/www/TRX/ +# for running a production server, use docker-compose with prod.yml or prod-ssl.yml CMD ["python3", "project.py", "runserver"] diff --git a/demo/gunicorn/prod-ssl.yml b/demo/gunicorn/prod-ssl.yml new file mode 100644 index 0000000..554c9a7 --- /dev/null +++ b/demo/gunicorn/prod-ssl.yml @@ -0,0 +1,7 @@ +version: '3' +services: + python: + build: . + command: "gunicorn --certfile=server.crt --keyfile=server.key --bind=0.0.0.0:8443 --threads=25 --workers=2 project:app" + ports: + - "8443:8443" diff --git a/demo/gunicorn/prod.yml b/demo/gunicorn/prod.yml index c243cc3..0a4c6dc 100644 --- a/demo/gunicorn/prod.yml +++ b/demo/gunicorn/prod.yml @@ -1,7 +1,7 @@ version: '3' services: python: - build: .. + build: . command: "gunicorn --bind=0.0.0.0:8080 --threads=25 --workers=2 project:app" ports: - "8080:8080" diff --git a/demo/gunicorn/requirements.txt b/demo/gunicorn/requirements.txt index c58373f..e9f1c7c 100644 --- a/demo/gunicorn/requirements.txt +++ b/demo/gunicorn/requirements.txt @@ -1 +1 @@ -maltego-trx \ No newline at end of file +maltego-trx>=1.3.8 diff --git a/demo/gunicorn/transforms/OverlayExample.py b/demo/gunicorn/transforms/OverlayExample.py new file mode 100644 index 0000000..7bd8ecc --- /dev/null +++ b/demo/gunicorn/transforms/OverlayExample.py @@ -0,0 +1,36 @@ +from maltego_trx.entities import Phrase +from maltego_trx.overlays import OverlayPosition, OverlayType + +from maltego_trx.transform import DiscoverableTransform + + +class OverlayExample(DiscoverableTransform): + """ + Returns a phrase with overlays on the graph. + """ + + @classmethod + def create_entities(cls, request, response): + person_name = request.Value + entity = response.addEntity(Phrase, "Hi %s, nice to meet you!" % person_name) + + # Normally, when we create an overlay, we would reference a property name so that Maltego can then use the + # value of that property to create the overlay. Sometimes that means creating a dynamic property, but usually + # it's better to either use an existing property, or, if you created the Entity yourself, and only need the + # property for the overlay, to use a hidden property. Here's an example of using a dynamic property: + entity.addProperty('dynamic_overlay_icon_name', displayName="Name for overlay image", value="Champion") + entity.addOverlay('dynamic_overlay_icon_name', OverlayPosition.WEST, OverlayType.IMAGE) + + # DISCOURAGED: + # You *can* also directly supply the string value of the property, however this is not recommended. Why? If + # the entity already has a property of the same ID (in this case, "DE"), then you would in fact be assigning the + # value of that property, not the string "DE", which is not the intention. Nevertheless, here's an example: + entity.addOverlay('DE', OverlayPosition.SOUTH_WEST, OverlayType.IMAGE) + + # Overlays can also be used to display extra text on an entity: + entity.addProperty("exampleDynamicPropertyName", "Example Dynamic Property", "loose", "Maltego Overlay Testing") + entity.addOverlay('exampleDynamicPropertyName', OverlayPosition.NORTH, OverlayType.TEXT) + + # Or a small color indicator: + entity.addOverlay('#45e06f', OverlayPosition.NORTH_WEST, OverlayType.COLOUR) + diff --git a/maltego_trx/__init__.py b/maltego_trx/__init__.py index bf730e4..3ba22d0 100644 --- a/maltego_trx/__init__.py +++ b/maltego_trx/__init__.py @@ -1 +1 @@ -VERSION = "1.3.7" +VERSION = "1.3.8" diff --git a/maltego_trx/entities.py b/maltego_trx/entities.py index e129248..0f48e06 100644 --- a/maltego_trx/entities.py +++ b/maltego_trx/entities.py @@ -35,3 +35,38 @@ URL = "maltego.URL" Website = "maltego.Website" WebTitle = "maltego.WebTitle" + +# {entityName: {version2PropertyName: version3PropertyName,...}} +entity_property_map = { + "maltego.Person": {"firstname": "person.firstnames", "lastname": "person.lastname"}, + "maltego.Domain": {"whois": "whois-info"}, + "maltego.IPv4Address": {"whois": "whois-info"}, + "maltego.URL": {"maltego.v2.value.property": "short-title", "theurl": "url", "fulltitle": "title"}, + "maltego.Document": {"maltego.v2.value.property": "title", "link": "url", "metainfo": "document.meta-data"}, + "maltego.Location": {"area": "location.area", "countrysc": "url", "long": "longitude", "lat": "latitude"}, + "maltego.PhoneNumber": { + "countrycode": "phonenumber.countrycode", "citycode": "phonenumber.citycode", + "areacode": "phonenumber.areacode", "lastnumbers": "phonenumber.lastnumbers" + }, + "maltego.affiliation.Spock": { + "network": "affiliation.network", "uid": "affiliation.uid", "profile_url": "affiliation.profile-url", + "spock_websites": "spock.websites" + }, + "maltego.affiliation": { + "network": "affiliation.network", "uid": "affiliation.uid", "profile_url": "affiliation.profile-url" + }, + "maltego.Service": {"banner": "banner.text", "port": "port.number"}, + "maltego.Alias": {"properties.alias": "alias"}, + "maltego.Device": {"properties.device": "device"}, + "maltego.GPS": {"properties.gps": "gps.coordinate"}, + "maltego.CircularArea": {"area": "radius"}, + "maltego.Image": {"properties.image": "description", "fullImage": "url"}, + "maltego.NominatimLocation": {"properties.nominatimlocation": "nominatimlocation"}, + "maltego.BuiltWithTechnology": {"properties.builtwithtechnology": "builtwith.technology"}, + "maltego.FacebookObject": {"properties.facebookobject": "facebook.object"} +} + + +def translate_legacy_property_name(entity_type, v2_property): + """Function maps a legacy version 2 entity property name to version 3 entity property name""" + return entity_property_map .get(entity_type, {}).get(v2_property) diff --git a/maltego_trx/maltego.py b/maltego_trx/maltego.py index 4383b83..c5dc7b0 100644 --- a/maltego_trx/maltego.py +++ b/maltego_trx/maltego.py @@ -1,7 +1,9 @@ import uuid; + from xml.dom import minidom -from .entities import Phrase +from .entities import Phrase, translate_legacy_property_name, entity_property_map +from .overlays import OverlayPosition, OverlayType from .utils import remove_invalid_xml_chars BOOKMARK_COLOR_NONE = "-1" @@ -44,6 +46,7 @@ ADD_FIELD_TEMPLATE = "" DISP_INFO_TEMPLATE = "" UIM_TEMPLATE = "%(text)s" +OVERLAY_TEMPLATE = "" class MaltegoEntity(object): @@ -55,6 +58,7 @@ def __init__(self, type=None, value=None): self.additionalFields = [] self.displayInformation = [] self.iconURL = "" + self.overlays = [] def setType(self, type=None): if type: @@ -93,7 +97,7 @@ def setLinkLabel(self, label): def reverseLink(self): self.addProperty('link#maltego.link.direction', 'link#maltego.link.direction', 'loose', 'output-to-input') - + def addCustomLinkProperty(self, fieldName=None, displayName=None, value=None): self.addProperty('link#' + fieldName, displayName, '', value) @@ -103,6 +107,11 @@ def setBookmark(self, bookmark): def setNote(self, note): self.addProperty('notes#', 'Notes', '', note) + def addOverlay( + self, propertyName, position: OverlayPosition, overlayType: OverlayType + ): + self.overlays.append([propertyName, position.value, overlayType.value]) + def add_field_to_xml(self, additional_field): name, display, matching, value = additional_field matching = "strict" if matching.lower().strip() == "strict" else "loose" @@ -139,6 +148,16 @@ def returnEntity(self): lines.append(self.add_field_to_xml(additional_field)) lines.append("") + if self.overlays: + lines.append("") + for overlay in self.overlays: + overlay_tag = OVERLAY_TEMPLATE % { + "property_name": overlay[0], + "position": overlay[1], + "type": overlay[2], + } + lines.append(overlay_tag) + lines.append("") if self.iconURL: lines.append("%s" % self.iconURL) @@ -226,6 +245,15 @@ def __init__(self, MaltegoXML="", LocalArgs=[]): self.Weight = self._get_int(entity, "Weight") self.Slider = self._get_int(maltego_msg, "Limits", attr_name="SoftLimit") + self.Genealogy = [] + genealogy_tag = maltego_msg.getElementsByTagName("Genealogy") + genealogy_types = genealogy_tag[0].getElementsByTagName("Type") if genealogy_tag else [] + for genealogy_type_tag in genealogy_types: + entity_type_name = genealogy_type_tag.getAttribute("Name") + entity_type_old_name = genealogy_type_tag.getAttribute("OldName") + entity_type = {"Name": entity_type_name, + "OldName": entity_type_old_name if entity_type_old_name else None} + self.Genealogy.append(entity_type) # Additional Fields self.Properties = {} @@ -235,6 +263,10 @@ def __init__(self, MaltegoXML="", LocalArgs=[]): name = field.getAttribute("Name") value = self._get_text(field) self.Properties[name] = value + for entity_type in self.Genealogy: + v3_property_name = translate_legacy_property_name(entity_type["Name"], name) + if v3_property_name is not None: + self.Properties[v3_property_name] = value # Transform Settings self.TransformSettings = {} @@ -247,6 +279,7 @@ def __init__(self, MaltegoXML="", LocalArgs=[]): elif LocalArgs: self.Value = LocalArgs[0] self.Type = "local.Unknown" + self.Genealogy = None self.Weight = 100 self.Slider = 100 @@ -263,6 +296,16 @@ def __init__(self, MaltegoXML="", LocalArgs=[]): self.buildProperties(text.split("#"), hash_rnd, equals_rnd, bslash_rnd) self.TransformSettings = {} + def clearLegacyProperties(self): + to_clear = set() + for entity_type in self.Genealogy or []: + for prop_name in entity_property_map.get(entity_type["Name"], []): + to_clear.add(prop_name) + + for field_name in to_clear: + if field_name in self.Properties: + del self.Properties[field_name] + def buildProperties(self, key_value_array, hash_rnd, equals_rnd, bslash_rnd): self.Properties = {} for property_section in key_value_array: diff --git a/maltego_trx/overlays.py b/maltego_trx/overlays.py new file mode 100644 index 0000000..1c897b0 --- /dev/null +++ b/maltego_trx/overlays.py @@ -0,0 +1,16 @@ +from enum import Enum + + +class OverlayPosition(Enum): + NORTH = "N" + SOUTH = "S" + WEST = "W" + NORTH_WEST = "NW" + SOUTH_WEST = "SW" + CENTER = "C" + + +class OverlayType(Enum): + IMAGE = "image" + COLOUR = "colour" + TEXT = "text" diff --git a/maltego_trx/template_dir/transforms/OverlayExample.py b/maltego_trx/template_dir/transforms/OverlayExample.py new file mode 100644 index 0000000..e4478d2 --- /dev/null +++ b/maltego_trx/template_dir/transforms/OverlayExample.py @@ -0,0 +1,35 @@ +from maltego_trx.entities import Phrase +from maltego_trx.overlays import OverlayPosition, OverlayType + +from maltego_trx.transform import DiscoverableTransform + + +class OverlayExample(DiscoverableTransform): + """ + Returns a phrase with overlays on the graph. + """ + + @classmethod + def create_entities(cls, request, response): + person_name = request.Value + entity = response.addEntity(Phrase, "Hi %s, nice to meet you!" % person_name) + + # Normally, when we create an overlay, we would reference a property name so that Maltego can then use the + # value of that property to create the overlay. Sometimes that means creating a dynamic property, but usually + # it's better to either use an existing property, or, if you created the Entity yourself, and only need the + # property for the overlay, to use a hidden property. Here's an example of using a dynamic property: + entity.addProperty('dynamic_overlay_icon_name', displayName="Name for overlay image", value="Champion") + entity.addOverlay('dynamic_overlay_icon_name', OverlayPosition.WEST, OverlayType.IMAGE) + + # You *can* also directly supply the string value of the property, however this is not recommended. Why? If + # the entity already has a property of the same ID (in this case, "DE"), then you would in fact be assigning the + # value of that property, not the string "DE", which is not the intention. Nevertheless, here's an example: + entity.addOverlay('DE', OverlayPosition.SOUTH_WEST, OverlayType.IMAGE) + + # Overlays can also be an additional field of text displayed on the entity: + entity.addProperty("exampleDynamicPropertyName", "Example Dynamic Property", "loose", "Maltego Overlay Testing") + entity.addOverlay('exampleDynamicPropertyName', OverlayPosition.NORTH, OverlayType.TEXT) + + # Or a small color indicator + entity.addOverlay('#45e06f', OverlayPosition.NORTH_WEST, OverlayType.COLOUR) + diff --git a/maltego_trx/test_hierarchical_entity.xml b/maltego_trx/test_hierarchical_entity.xml new file mode 100644 index 0000000..5b828fe --- /dev/null +++ b/maltego_trx/test_hierarchical_entity.xml @@ -0,0 +1,13 @@ + + + + + + + www.paterva.com + false + 80 + + www.paterva.com + 0 + diff --git a/setup.py b/setup.py index edc23fc..be1ee8b 100644 --- a/setup.py +++ b/setup.py @@ -1,25 +1,28 @@ from setuptools import setup from maltego_trx import VERSION -setup(name='maltego-trx', - version=VERSION, - description='Python library used to develop Maltego transforms', - url='https://github.com/paterva/maltego-trx/', - author='Maltego Staff', - author_email='support@maltego.com', - license='MIT', - install_requires=[ - 'flask>=1', - 'six>=1', - 'cryptography>=3.3.1' - ], - packages=[ - 'maltego_trx', - 'maltego_trx/template_dir', - 'maltego_trx/template_dir/transforms' - ], - entry_points={'console_scripts': [ - 'maltego-trx = maltego_trx.commands:execute_from_command_line', - ]}, - zip_safe=False - ) +setup( + name='maltego-trx', + version=VERSION, + description='Python library used to develop Maltego transforms', + url='https://github.com/paterva/maltego-trx/', + author='Maltego Staff', + author_email='support@maltego.com', + license='MIT', + install_requires=[ + 'flask>=1', + 'six>=1', + 'cryptography==3.3.2' # pinned for now as newer versions require setuptools_rust + ], + packages=[ + 'maltego_trx', + 'maltego_trx/template_dir', + 'maltego_trx/template_dir/transforms' + ], + entry_points={ + 'console_scripts': [ + 'maltego-trx = maltego_trx.commands:execute_from_command_line', + ] + }, + zip_safe=False +) diff --git a/tests/test_property_mapping.py b/tests/test_property_mapping.py new file mode 100644 index 0000000..12af79e --- /dev/null +++ b/tests/test_property_mapping.py @@ -0,0 +1,20 @@ +from maltego_trx.registry import register_transform_classes +from maltego_trx.server import app +from tests import transforms + + +def test_request_property_mapping(): + register_transform_classes(transforms) + app.testing = True + + with app.test_client() as test_app: + response = make_transform_call(test_app, "/run/testrequestpropertymapping/") + assert response.status_code == 200 + data = response.data.decode('utf8') + assert "whois-info found" in data + + +def make_transform_call(test_app=None, run_endpoint=""): + with open('test_request.xml') as requestMsg: + response = test_app.post(run_endpoint, data=requestMsg.read()) + return response diff --git a/tests/test_request.xml b/tests/test_request.xml new file mode 100644 index 0000000..77c5f40 --- /dev/null +++ b/tests/test_request.xml @@ -0,0 +1,18 @@ + + + + + + + + + paterva.com + whois-info found + + paterva.com + 0 + + + + + diff --git a/tests/transforms/TestRequestPropertyMapping.py b/tests/transforms/TestRequestPropertyMapping.py new file mode 100644 index 0000000..c82426a --- /dev/null +++ b/tests/transforms/TestRequestPropertyMapping.py @@ -0,0 +1,15 @@ +from maltego_trx.entities import Phrase + +from maltego_trx.transform import DiscoverableTransform + + +class TestRequestPropertyMapping(DiscoverableTransform): + """ + Test if the automatic mapping of v2 propertyname `whois` -> `whois-info` has been done by the library. Original input + contains only whois property name. see test_request.xml + """ + + @classmethod + def create_entities(cls, request, response): + v3_property_value = request.Properties['whois-info'] + response.addEntity(Phrase, "%s" % v3_property_value) diff --git a/tests/transforms/__init__.py b/tests/transforms/__init__.py new file mode 100644 index 0000000..e69de29