This article will cover the writing of local Transforms in Python using our Maltego TRX library.
It has been possible to write local Transforms using the Maltego TRX library v1.3+ (release on the 12th of November 2018).
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, 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 a "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:
- Lies 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 the 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 will 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 which 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.