This article will cover writing local transforms in Python, using our Maltego TRX library.


Is has been possible to write local transforms using the Maltego TRX library v1.3+ (release on the 12th of November 2018).

Read how to Setup: Python Transform Server using TRX + Gunicorn

Read how to Setup: Python Transform Server using TRX + Apache2

The advantage of using the Maltego-TRX library is that the exact same code can be used for writing both local and TDS transforms. Local transforms written with the TRX library can be deployed to a remote transform server without any code changes.


The code mentioned in this document, as well as the CSV file is contained in our Maltego TRX Examples project. Please reference this project if you are unsure about anything mentioned in this article.


Create a project folder

Your transform code will need to be placed in a project folder that follows our recommended project structure.


First install the maltego-trx library by running the following commands:


pip install maltego-trx


After the maltego-trx library has been installed, you can use the following command to create a new project folder with the recommended layout:


maltego-trx start new_project


This will create a directory named "new_project" which contains the "project.py" file used to run your transforms, and the "transforms" directory that contains your transform code.


New Transform Code

Use Case

The default project created by the "maltego-trx start" command will already contain two transforms in the "transforms" folder. These are included as examples of what a transform looks like.


In this tutorial we are going to read data from a local CSV file and return it into Maltego.


Create "phone_to_names.csv" file and store it in the root of the project directory. This file will act as a data source containing data in the following format:


1-541-754-3010, John Smith
1-541-800-6987, Jane Doe
1-336-854-1155, Alex Walker


The first column contains a phone number and the second column contains the name associated with that phone number.


We would like to be able to select a phone number in Maltego and to retrieve the names of any people associated with that number.


Adding the transform class

By writing our transforms as classes that inherit from the parent class "DiscoverableTransform", our transform can be automatically detected by the library as an available transform.


In order for a new transform to be discovered, we must create a new file that:

  • Is inside the "transforms" folder
  • Contains a class with the same name as the filename
  • The class inherits from the DiscoverableTransform class


We can go ahead and create a new file "transforms/NameFromCSV.py" with the following contents:


from maltego_trx.entities import Person
from maltego_trx.transform import DiscoverableTransform


class NameFromCSV(DiscoverableTransform):
    """
    Lookup the name associated with a phone number.
    """

    @classmethod
    def create_entities(cls, request, response):
        pass


This transform will not return anything to the client, but we can check that the transform has been discovered by the library using the following command in "new_project/" directory:


python project.py list


The above command should output the following:

= Transform Server URLs =
/run/dnstoip/: DNSToIP
/run/greetperson/: GreetPerson
/run/namefromcsv/: NameFromCSV


= Local Transform Names =
dnstoip: DNSToIP
greetperson: GreetPerson
namefromcsv: NameFromCSV


Whilst we haven't implemented any of the logic for our transform, we can at least see that the new "NameFromCSV" transform has been discovered by the library.


Implementing the logic

There is very little Maltego specific code required to write a transform. As a first step I will write a generic Python function that accepts a phone number and returns a list of any associated names or [].


I will add the following static method to my "NameFromCSV" class:


@staticmethod
def get_names(search_phone):
    matching_names = []
    with open("phone_to_names.csv") as f:
        for ln in f.readlines():
            phone, name = ln.split(",", 1)
            if phone.strip() == search_phone.strip():
                matching_names.append(name.strip())
    return matching_names


The above code implements the logic required for scanning through our CSV file to find the associated name.


We can now add the Maltego specific code that formats the result of this function so that it can be returned to Maltego.


I will update my "create_entities" method of my transform class to the following code:


@classmethod
def create_entities(cls, request, response):
    phone = request.Value

    try:
        names = cls.get_names(phone)
        if names:
            for name in names:
                response.addEntity(Person, name)
        else:
            response.addUIMessage("The phone number given did not match any numbers in the CSV file")
    except IOError:
        response.addUIMessage("An error occurred reading the CSV file.", messageType=UIM_PARTIAL)


We first access the phone number selected in the graph, using the "request.Value" property.


We then search for the phone number using the "get_names" function that we created. For every matching name returned, we create a new Person entity on the graph.


If we cannot find a match, then we return a UI message informing the user that we found no results. 


Finally, when reading files from disk you can often get an IOError if the file is missing or doesn't have the correct permissions. Our transform should handle this exception and return a sensible error message to Maltego.


Final Structure of code and data:


new_project/transforms/NameFromCSV.py
new_project/phone_to_names.csv


The Final Code

After working through the above steps, our code should be the following:


from maltego_trx.entities import Person
from maltego_trx.maltego import UIM_PARTIAL
from maltego_trx.transform import DiscoverableTransform


class NameFromCSV(DiscoverableTransform):
    """
    Lookup the name associated with a phone number.
    """

    @classmethod
    def create_entities(cls, request, response):
        phone = request.Value

        try:
            names = cls.get_names(phone)
            if names:
                for name in names:
                    response.addEntity(Person, name)
            else:
                response.addUIMessage("The phone number given did not match any numbers in the CSV file")
        except IOError:
            response.addUIMessage("An error occurred reading the CSV file.", messageType=UIM_PARTIAL)

    @staticmethod
    def get_names(search_phone):
        matching_names = []
        with open("phone_to_names.csv") as f:
            for ln in f.readlines():
                phone, name = ln.split(",", 1)
                if phone.strip() == search_phone.strip():
                    matching_names.append(name.strip())
        return matching_names


if __name__ == "__main__":
    print(NameFromCSV.get_names("1-541-754-3010"))


We can now add the local transform to the Maltego client. 


Two additional lines are added at the end of the transform:


if __name__ == "__main__":
    print(NameFromCSV.get_names("1-541-754-3010"))


The statement "if __name__ == "__main__"" allows us to specify code that only runs if the script is run directly, rather than imported.


If we import our transform class, the code will not execute. However, if we execute the script directly using "python transforms/NameFromCSV.py" then the method "get_names" will be run. This allows us to quickly test the "get_names" method without worrying about running a transform server, or using Maltego.


Adding the transform to Maltego

You can add the local transform to Maltego by opening Maltego desktop client, clicking on the "Transforms" tab in the ribbon bar and clicking "New Local Transform".



This will open a new wizard that guide you through the process of adding a new local transform.


The first page allows you to describe the new transform. The most important details are the "Transform ID" (which must be unique) and Input Entity Type.



The next page allows you to specify the settings used to execute the transform. 



The "command" field should be the absolute path of the interpreter that will execute your transform. In this case I will use the path for my Python installation, which is "C:\Users\<username>\AppData\Local\Programs\Python\Python36\python.exe".


The "parameters" field will need to include "project.py" to tell Python that we want to execute our project file. We will also need to include the parameter "local" to tell our project it's being run locally and the transform name to run. If you are unsure what transform name to use, you can use the following command:


python project.py list


This will output the following:


= Transform Server URLs =
/run/dnstoip/: DNSToIP
/run/greetperson/: GreetPerson
/run/namefromcsv/: NameFromCSV


= Local Transform Names =
dnstoip: DNSToIP
greetperson: GreetPerson
namefromcsv: NameFromCSV


From the above output we can see that we can use the transform name "namefromcsv" to run the transform class called NameFromCSV.


The "Working Directory" filed should be set to your project folder. 


The should be all that is required, and you can now click "Finish" to save the local transform.


Running the Transform

If we paste the phone number "1-541-754-3010" into Maltego, right-click and run our local transform, we should then get the name associated with the phone number from our CSV file.