Skip to content

Commit 1c64327

Browse files
authored
Update to 1.5 🗺️ (#12)
* Add visualization option with python and javascript using the Cytoscape.js framework * Add logic to callmapper.py that hosts an existing data.json file and hosts its content. Add README.md for CallMapper. * Update callmapper to filter out processes without network activity and design. Update badges. Update README for callmapper. Update .gitignore. * Change logic for adding APIlookups. Add change to design of nodes depending on the node if they're malicious or not as they're bigger and have the color red. * Split API lookup classes to separate file. Add file for custom API lookup classes. Update index.html page. Update README.md * Add more logical structuring of files for CallMapper. Update version and the respective badges. Update main README.md and CallMapper README.md * Update README.mds * Update badges and README.md * Fix issue from resolving build warnings where an empty string was passed rather than null. Fix issue by correctly adding process name to started process. Update CallMapper. Add walkthrough gif of CallMapper. Refactor and clean code. Extend GitHub actions pipelines * Attempt fix for GitHub actions job * Replace video for new at later time * Attempt fix GitHub Actions file * Attempt fix to GitHub actions. Add gif for CallMapper. * Attempt fix to GitHub actions. Im blaming the lack of sleep. * Change logic to not retrieve the executable name when supplying a PID to listen to. This better manages protected processes as it's not always possible to retrieve their executbale path, causing issue * Fix issue with Listen mode where the retrieved process name of the PID wasn't successfully added as a monitored process * Update GitHub actions to include privileged and unprivileged executions * Add "Buy me a coffe" for support". Update github actions since password for new low priv user doesnt match pw policy * Fix github actions attempt - shorter password * Attempt fix of github actions
1 parent 8fd03ad commit 1c64327

30 files changed

Lines changed: 2051 additions & 105 deletions

.github/FUNDING.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
buy_me_a_coffee: H4NM
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
name: Compile and run WhoYouCalling
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- dev
8+
9+
jobs:
10+
build:
11+
runs-on: windows-latest
12+
13+
steps:
14+
- name: Checkout Code
15+
uses: actions/checkout@v4
16+
17+
- name: Setup .NET
18+
uses: actions/setup-dotnet@v4
19+
with:
20+
dotnet-version: '8.0'
21+
22+
- name: Restore dependencies
23+
run: dotnet restore
24+
25+
- name: Publish application
26+
run: dotnet publish -c Release -r win-x64 --self-contained false -o output
27+
28+
- name: Upload compiled binary
29+
uses: actions/upload-artifact@v4
30+
with:
31+
name: compiled-binary
32+
path: output
33+
34+
test-illuminate:
35+
needs: build
36+
runs-on: windows-latest
37+
38+
steps:
39+
- name: Download compiled binary
40+
uses: actions/download-artifact@v4
41+
with:
42+
name: compiled-binary
43+
path: output
44+
45+
- name: Run with Illuminate mode
46+
run: output\wyc.exe --illuminate --nopcap --timer 20 -d
47+
48+
test-executable-privileged:
49+
needs: build
50+
runs-on: windows-latest
51+
52+
steps:
53+
- name: Download compiled binary
54+
uses: actions/download-artifact@v4
55+
with:
56+
name: compiled-binary
57+
path: output
58+
59+
- name: Run with Execute mode with cmd.exe as privileged user
60+
run: |
61+
output\wyc.exe --executable "C:\Windows\System32\cmd.exe" --arguments "/c whoami" --nopcap --killprocesses --timer 10 --privileged -d
62+
63+
test-executable-unprivileged:
64+
needs: build
65+
runs-on: windows-latest
66+
67+
steps:
68+
- name: Download compiled binary
69+
uses: actions/download-artifact@v4
70+
with:
71+
name: compiled-binary
72+
path: output
73+
74+
- name: Run with Execute mode with cmd.exe as low privileged user
75+
run: |
76+
net user ga ETphon3H0me@_1 /add
77+
output\wyc.exe --executable "C:\Windows\System32\cmd.exe" --user ga --password ETphon3H0me@_1 --arguments "/c whoami" --nopcap --killprocesses --timer 10 -d
78+
79+
test-pid:
80+
needs: build
81+
runs-on: windows-latest
82+
83+
steps:
84+
- name: Download compiled binary
85+
uses: actions/download-artifact@v4
86+
with:
87+
name: compiled-binary
88+
path: output
89+
90+
- name: Run with Listen mode against System proces
91+
run: output\wyc.exe --PID 4 --nopcap --timer 10 -d
92+

.github/workflows/build.yml

Lines changed: 0 additions & 26 deletions
This file was deleted.

.gitignore

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@ Caps/
66
TestApplication*
77
args.txt
88
requirements.txt
9-
*.py
9+
autobadge.py
10+
requesttest.py
11+
TestApplication.py
12+
13+
Result.json
14+
Summary.txt
15+
data.json
16+
Events.txt
1017
*.spec
1118
*.sh
1219
build/

CallMapper/README.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# CallMapper
2+
CallMapper offers a network graph with analytics of looking up domains and IP addresses against APIs, or via static links to websites such as [ipinfo.io](https://ipinfo.io/), [whois.com](https://www.whois.com/), [abuseipdb.com](https://www.abuseipdb.com/) and [virustotal.com](https://www.virustotal.com/).
3+
4+
## How it works:
5+
1. `callmapper.py` parses the JSON results file from WhoYouCalling and creates a `data.json` file in the same directory as the script. If the flag for API lookups is provided, the data in the `data.json` files are enriched with stored HTML.
6+
2. `callmapper.py` hosts a HTTP server in the same directory as the script at localhost port 8080 that serves the `data.json` and the `index.html` with other related resources (css, js and icon).
7+
3. You can now view the visualization via http://127.0.0.1:8080 in a web browser
8+
9+
## Usage:
10+
11+
**Visualize the output from WhoYouCalling**:
12+
```
13+
python callmapper.py -r ./Result.json
14+
```
15+
> **Note:** You can visualize an already existing data.json file by not providing a Result.json and if that data.json file exists in the same directory as callmapper.py.
16+
17+
**Visualize the output from WhoYouCalling and enrich the data with API lookups**:
18+
```
19+
python callmapper.py --results-file ./Result.json --api-lookup
20+
```
21+
22+
## Dependencies
23+
**CallMapper** has been tested and works with Python version 3.11 or later. The packages that are used:
24+
- Visualization:
25+
- [Cytoscape.js](https://github.com/cytoscape/cytoscape)
26+
- API Lookups:
27+
- [requests](https://pypi.org/project/requests/) (*Optional - if API lookup of IPs and domains is wanted.*)
28+
29+
In order to run **CallMapper**, all you really need is Python.
30+
31+
## Using API lookups
32+
When running `callmapper.py` with the flag `--api-lookup` or `-a` for short, you will be prompted to choose which processes with network activity you want to lookup.
33+
Thereafter, you will be asked which API's you want to use to perform the lookups against. Both of the prompts accept an empty answer for selecting everything.
34+
35+
The list of available API's can be found in `callmapper.py` in the variable `AVAILABLE_APIS`.
36+
`AVAILABLE_APIS` is a dict with the title of the API as a key, with two subkeys; `api_key` and `api`.
37+
38+
```python
39+
AVAILABLE_APIS = {
40+
'VirusTotal': {
41+
'api_key': '',
42+
'api': VirusTotal,
43+
},
44+
'AbuseIPDB': {
45+
'api_key': '',
46+
'api': AbuseIPDB,
47+
}
48+
}
49+
```
50+
51+
The included APIs, `VirusTotal` and `AbuseIPDB`, both require an API key. Their defined class, found in `/lib/api_lookups.py`, specifiy if the API source requires an API key or not. The API key is added in their respective respective `api_key` field in `AVAILABLE_APIS`.
52+
If the field is empty and the API source requires an API key, and you as a user specified you want to use that api during the prompt, it will simply be skipped.
53+
54+
## Add you own API integration
55+
> **Note:** Only REST APIs are supported.
56+
57+
To create your own API integration, there's a template in `/custom/custom_api_lookups.py`.
58+
Any API integration must have the following structure:
59+
60+
```python
61+
62+
class MyCustomAPILookupClass(APILookup):
63+
64+
def __init__(self, api_source:str, api_key:str = ""):
65+
super().__init__(api_source, api_key)
66+
self.headers = {"x-api-key": self.api_key}
67+
self.api_key_required = True
68+
self.lookup_types = [LookupType.IP, LookupType.DOMAIN]
69+
70+
def get_data(self, endpoint: str, lookup_type) -> dict:
71+
url = f"https://my.own.api/api/v2/check?{endpoint}"
72+
response = self.requests.get(url, headers=self.headers)
73+
#...
74+
json_response = response.json()
75+
76+
def get_presentable_data_for_ip(self, returned_data: dict) -> Tuple[dict, bool]:
77+
presentable_data: dict = {}
78+
is_potentially_malicious: bool = False
79+
#...
80+
return presentable_data, is_potentially_malicious
81+
82+
def get_presentable_data_for_domain(self, returned_data: dict) -> Tuple[dict, bool]:
83+
presentable_data: dict = {}
84+
is_potentially_malicious: bool = False
85+
#....
86+
return presentable_data, is_potentially_malicious
87+
```
88+
The function `__init__` is invoked when the object of the class is initiated. In there, you need to define:
89+
1. If the API-key is required or not: `self.api_key_required = True`
90+
2. Should you lookup IPs, domains or both: `self.lookup_types = [LookupType.IP, LookupType.DOMAIN]`
91+
You can also define the requests header if needed, e.g. `self.headers = {"x-api-key": self.api_key}`. Otherwise you can define it in `get_data`.
92+
93+
The function `get_data` is the one conducting the actual HTTP REST API lookup. It will simply query the endpoint, using `self.requests` (yes, that's an object inherited requests). The reason behind assigning the library `requests` to an object variable was to ensure that CallMapper doesn't require the library `requests` to run - this also why there's no `requirements.txt` file here :-). The `get_data` function processes the request to the extent of validating if successful data was returned or not. Thereafter it's only returned as a JSON object. Worth noting is that `get_data` may have a different URL depending on the endpoint type, in which is needs to be able to process both types. It is possible to return, as of now, three different API error types. If `MAJOR_ERROR`, `QUOTA_EXCEEDED`, or `WRONG_CREDENTIALS` are returned, the remaining types of endpoints will be skipped. If any other type of error is returned, it will simply attempt to lookup the next endpoint.
94+
95+
```python
96+
class APIErrorType:
97+
NO_RESULTS = "NO_RESULTS"
98+
INVALID_FORMAT = "INVALID_FORMAT"
99+
ERROR = "ERROR"
100+
WRONG_CREDENTIALS = "WRONG_CREDENTIALS"
101+
QUOTA_EXCEEDED = "QUOTA_EXCEEDED"
102+
MAJOR_ERROR = "MAJOR_ERROR"
103+
```
104+
105+
The function `get_presentable_data_for_ip` and `get_presentable_data_for_domain` simply takes the returned JSON object retrieves the fields that are of value and places them within a flat dict (not nested). The keys in the dict will be the titles presented in the visualization and the data with be the corresponding values. The functions will return the dict and a bool wether the retrieved data indicates that the endpoint may be malicious. If the bool variable is returned `True` (potentially malicious), the nodes take a red star shape in the network graph, clearly indicating that they're worth investigating.
106+
107+
When it's done and ready, import the custom API you have defined in `/custom/` (e.g. `from custom.MyCustomAPILookupClass import *`) in `callmapper.py`, then simply add it in the same fashion as `VirusTotal` and `AbuseIPDB` are in `AVAILABLE_APIS`.

CallMapper/callmapper.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
2+
import os
3+
import sys
4+
import argparse
5+
from pathlib import Path
6+
7+
#=====================================
8+
# CUSTOM LIBRARIES
9+
#======== FUNCTIONS & CLASSES =======
10+
from lib.functions import *
11+
from lib.static import SCRIPT_BANNER
12+
from lib.output import *
13+
14+
#========== API LOOKUPS ============
15+
from lib.lookups import *
16+
#from custom.MyCustomAPILookupClass import *
17+
18+
#=====================================
19+
# CHANGABLE VARIABLES AND FUNCTIONS
20+
#=====================================
21+
HTTP_HOST_ADRESS:str = "127.0.0.1"
22+
HTTP_HOST_PORT:int = 8080
23+
AVAILABLE_APIS = {
24+
'VirusTotal': {
25+
'api_key': '',
26+
'api': VirusTotal,
27+
},
28+
'AbuseIPDB': {
29+
'api_key': '',
30+
'api': AbuseIPDB,
31+
}
32+
}
33+
34+
35+
#==================================================
36+
# Dont touch anything below :-)
37+
#==================================================
38+
39+
def main() -> None:
40+
SCRIPT_DIRECTORY: Path.parent = Path(__file__).parent
41+
DATA_FILE: str = SCRIPT_DIRECTORY / "data.json"
42+
43+
parser = argparse.ArgumentParser(description="A script demonstrating argparse with flags.")
44+
parser.add_argument("-r", "--results-file", type=str, help="Results file")
45+
parser.add_argument("-a", "--api-lookup", action="store_true", help="Lookup endpoints against defined APIs")
46+
args = parser.parse_args()
47+
print(SCRIPT_BANNER)
48+
49+
if not file_exists_in_same_script_folder(SCRIPT_DIRECTORY, "index.html"):
50+
ConsoleOutputPrint(msg=f"Unable to find index.html in the same directory as the script", print_type="fatal")
51+
sys.exit(1)
52+
53+
if not args.results_file:
54+
if not file_exists_in_same_script_folder(SCRIPT_DIRECTORY, "data.json"):
55+
ConsoleOutputPrint(msg=f"Unable to find data.json in the same directory as the script. Please supply a Results.json file or move data.json to the same path as the script", print_type="fatal")
56+
sys.exit(1)
57+
if not valid_data_file_exists(DATA_FILE):
58+
ConsoleOutputPrint(msg=f"{DATA_FILE} has an invalid JSON structure", print_type="fatal")
59+
sys.exit(1)
60+
61+
if args.api_lookup and not requests_is_installed():
62+
ConsoleOutputPrint(REQUESTS_LIBRARY_MISSING_MSG, print_type="fatal")
63+
sys.exit(1)
64+
65+
if args.results_file:
66+
ConsoleOutputPrint(msg=f"Retrieving data from results file", print_type="info")
67+
monitored_processes: list = get_results_file_data(args.results_file)
68+
69+
if args.api_lookup:
70+
unique_process_names: set = get_unique_process_names_with_external_network_activity(monitored_processes)
71+
processes_to_lookup_with_network_activity: list = prompt_user_for_processes_to_lookup(unique_process_names)
72+
endpoints: dict = get_unique_endpoints_to_lookup(monitored_processes, processes_to_lookup_with_network_activity)
73+
apis_to_use: list = prompt_user_for_apis_to_use(AVAILABLE_APIS)
74+
lookup_endpoints(AVAILABLE_APIS, endpoints, apis_to_use)
75+
76+
ConsoleOutputPrint(msg=f"Creating visualization data", print_type="info")
77+
visualization_data = get_visualization_data(monitored_processes)
78+
if os.path.isfile(DATA_FILE):
79+
if prompt_user_for_overwrite_of_data_file():
80+
ConsoleOutputPrint(msg=f"Overwriting existing data.json.", print_type="info")
81+
output_visualization_data(DATA_FILE, visualization_data)
82+
else:
83+
ConsoleOutputPrint(msg=f"Keeping existing data.json", print_type="info")
84+
else:
85+
output_visualization_data(DATA_FILE, visualization_data)
86+
else:
87+
ConsoleOutputPrint(msg=f"Visualizing from existing results file", print_type="info")
88+
ConsoleOutputPrint(msg=f"Hosting visualization via http://{HTTP_HOST_ADRESS}:{HTTP_HOST_PORT}", print_type="info")
89+
try:
90+
start_http_server(directory=SCRIPT_DIRECTORY, host=HTTP_HOST_ADRESS, port=HTTP_HOST_PORT)
91+
except KeyboardInterrupt:
92+
ConsoleOutputPrint(msg=f"Keyboard interuppt. Goodbye!", print_type="info")
93+
94+
if __name__ == "__main__":
95+
main()

0 commit comments

Comments
 (0)