98 Commits
v1.4.0 ... main

Author SHA1 Message Date
AccentuSoft
16be1c5ee4 Bump python to 3.13, and app version 2025-02-06 14:43:30 +02:00
AccentuSoft
7346a42227 Move to using Fusion theme for app 2025-02-06 14:40:01 +02:00
AccentuSoft
85ed2e7227 Update syntax for EmailExtractor
Convert | to _

Another bug fix & bump version
2024-07-30 12:50:36 +03:00
AccentuSoft
f9f315da33 Bump version 2024-07-18 11:49:34 +03:00
AccentuSoft
741aa78ff4 Fix bug with SVG images lacking both 'width' and 'height' parameters 2024-07-18 11:46:16 +03:00
AccentuSoft
8e8838e6e9 Fix bugs & add more stuff to PATH 2024-07-04 19:37:30 +03:00
AccentuSoft
889c09ad14 Merge remote-tracking branch 'origin/main' 2024-06-28 21:25:29 +03:00
AccentuSoft
d11a7723f6 Small Cleanup 2024-06-26 18:14:58 +03:00
AccentuSoft
47051a66c2 Remove/Adjust console build option, and update version 2024-06-26 18:10:52 +03:00
AccentuSoft
61912960e2 Update README.md 2023-10-29 03:10:16 +02:00
AccentuSoft
9c5dd03473 Fix bugs & various optimizations:
- Adjust for installations where venv was not configured.
- Change update strategy to ensure update goes through on all platforms after compilation.
- Make sure that paths work on both Windows & Linux.
- Adjust update strategy to make it work on both Windows & Linux.
2023-10-24 19:04:53 +03:00
AccentuSoft
9c6fee0fa6 Fix bugs, add checks and enable configuration of default module packs source on first time start. 2023-10-24 19:04:53 +03:00
AccentuSoft
8d55e8027a Fix bugs and adjust approach of handing playwright versions 2023-10-23 23:30:08 +03:00
AccentuSoft
4583ce3be2 Use semver to compare versions - more robust, if we ever have something like 1.10.2 in the future. 2023-10-23 23:30:08 +03:00
AccentuSoft
e8ed1db4d7 Add functions to cleanup & maintain playwright browser versions as updates happen. 2023-10-23 23:30:08 +03:00
AccentuSoft
1b7f8ae9e4 Some light refactoring 2023-10-23 23:30:08 +03:00
AccentuSoft
b14160ec1f Add path to playwright to module arguments, and pass it to Core modules. 2023-10-23 23:30:08 +03:00
AccentuSoft
79e8ad3684 Work on UpdateManager.py 2023-10-23 23:30:08 +03:00
AccentuSoft
ff988d69a6 Shuffle code around to more appropriate places. 2023-10-23 23:30:08 +03:00
AccentuSoft
a49e2baa32 Add File menu option to open Project Files dir for current Project. 2023-10-23 23:30:08 +03:00
AccentuSoft
8161e6f2e6 Start work on UpdateManager 2023-10-23 23:30:08 +03:00
AccentuSoft
9c861c238a Update build scripts and Installer for 1.6 2023-10-23 23:30:08 +03:00
AccentuSoft
53597f838c Update requirements 2023-09-29 18:56:38 +03:00
AccentuSoft
df39374970 TabbedPane gets banners dynamically - now modules can load banners. 2023-09-29 18:56:38 +03:00
AccentuSoft
0797190d41 Improve Core resolutions.
Add DecodeRedirectUrlParameter.py Resolution
2023-09-29 18:56:38 +03:00
AccentuSoft
7f58986f6d More work on ModuleManager. 2023-09-29 18:56:38 +03:00
AccentuSoft
a4aa1076f0 Change depreciated function to non-depreciated function. 2023-09-29 18:56:38 +03:00
AccentuSoft
c367e2b2b2 Increase message size & ending bytestring 2023-09-29 18:56:38 +03:00
AccentuSoft
f742ad750e Fix bug with resizing SVGs 2023-09-29 18:56:38 +03:00
AccentuSoft
90c648ad56 Create signal listener for errors from spawned threads. 2023-09-29 18:56:38 +03:00
AccentuSoft
39d8632827 Give Icons to remote resolutions. 2023-09-29 18:56:38 +03:00
AccentuSoft
e6c9b132e6 Pass the Main Window instead of the entity database to the Collector dialog. 2023-09-29 18:56:38 +03:00
AccentuSoft
429853fea8 Make the ResolutionManager.py handle launching remote resolutions all by itself. 2023-09-29 18:56:38 +03:00
AccentuSoft
accbab69f1 Make entities have the full size image stored, and resize it where appropriate. 2023-09-29 18:56:38 +03:00
AccentuSoft
20b1161782 Work on ModuleManager.py 2023-09-29 18:56:38 +03:00
AccentuSoft
88581689a1 Set Daemon attribute during Thread initialization instead of afterwards. 2023-09-29 18:56:38 +03:00
AccentuSoft
f075ca17cb Add error handling & adjust saving for module stuff
Work on ModuleManager.py
2023-09-29 18:56:38 +03:00
AccentuSoft
98371620f0 Fix bug with selection 2023-09-29 18:56:38 +03:00
AccentuSoft
3e3ee37f74 Add easy way to expand selection to include parents & children while keeping the currently selected nodes. 2023-09-29 18:56:38 +03:00
AccentuSoft
1c24f91b32 Work on MenuBar.py - modules & minor optimization 2023-09-29 18:56:38 +03:00
AccentuSoft
0cfb8b53dc Detect first time start & add welcome message for first time users. 2023-09-29 18:56:38 +03:00
AccentuSoft
77fd696626 Work on ModuleManager.py 2023-09-29 18:56:38 +03:00
AccentuSoft
a69980a3f9 Made the group node list widget be on top of other drawn stuff on the canvas. 2023-09-29 18:56:38 +03:00
AccentuSoft
5fe072b2a9 Add ability to select LQL Query result nodes on current canvas.
Add query conditions for the sum of the values of the primary fields of parents & children.

Add query conditions for total number of ancestors & successors.

Made it so 'Run Query' button is preselected, and that you can run multiple queries in the current context without needing to reset the wizard each time.
2023-09-29 18:56:38 +03:00
AccentuSoft
379e81548d Remove Modules from Core application. 2023-09-29 18:56:38 +03:00
AccentuSoft
7c3cb92a34 Work on ModuleManager & slight refactoring. 2023-09-29 18:56:37 +03:00
AccentuSoft
a79ef66d90 Fix bugs, trim fat & add logging. 2023-04-28 10:52:06 +03:00
AccentuSoft
c4e393e4e4 Start implementing Module Manager 2023-04-28 10:52:06 +03:00
AccentuSoft
cdcf83ea27 Update Demo Pic 2023-04-28 10:52:06 +03:00
AccentuSoft
758e2f7a03 Update Resolutions 2023-04-28 10:52:06 +03:00
AccentuSoft
1abc1377ea Small optimizations & improvements 2023-04-28 10:52:06 +03:00
AccentuSoft
f49deb8509 Small improvements to LinkScope.py 2023-04-28 10:52:06 +03:00
AccentuSoft
ed9494f604 Small improvements to SettingsObject.py 2023-04-28 10:52:06 +03:00
AccentuSoft
83b39fd8b7 Fix bug where rearranging as timezone was not working if the Date Created format was changed to be timezone-naive. 2023-04-28 10:52:06 +03:00
AccentuSoft
e7d1d8a075 Using signals properly for resolutions - necessary for PySide6.5+ 2023-04-28 10:52:06 +03:00
AccentuSoft
d9e83bfeec Change labels to line edit widgets to allow for easy copying & pasting. 2023-04-28 10:52:06 +03:00
AccentuSoft
8e632775e6 Fix bug with saved parameters - give new dict instead of existing, so when popping saved parameters, we don't mess with the original.
Refactor Virustotal stuff

Slight rewrite of FileHasher resolution.

Resolution fixes & Remove resolutions that no longer work.
2023-04-28 10:52:06 +03:00
AccentuSoft
270eb8c410 Add error handling for trying to find nodes on empty canvas. 2023-04-28 10:52:06 +03:00
AccentuSoft
ac1aeaf554 Add name of resolution to parameter selector dialog as standard. 2023-04-28 10:52:06 +03:00
AccentuSoft
b83eb15ffd Change startup message to make it more accurate as to where in the project open process we are. 2023-04-28 10:52:06 +03:00
AccentuSoft
1d1e4c28b2 Update Version 2023-04-28 10:52:06 +03:00
AccentuSoft
09cb4f4aa1 Update README.md
Add note about required Ubuntu version, and remove line about Windows visual bugs, since those were removed when we transitioned to the new UI.
2023-04-06 10:33:24 +03:00
AccentuSoft
680830c51f Update version 2023-03-22 10:20:51 +02:00
AccentuSoft
87e510a48f Update WhatsMyName 2023-03-21 22:36:42 +02:00
AccentuSoft
ce42a77162 Reduce resolution load message severity to 'debug' from 'info'. 2023-03-18 15:44:46 +02:00
AccentuSoft
f8b262cb1a Refactor EntityDB 2023-03-17 22:41:16 +02:00
AccentuSoft
924c208835 Refactoring & Update WhatsMyName and make it use Firefox 2023-03-17 18:07:12 +02:00
AccentuSoft
7c46ffa499 Fix bug with generating report with entities with custom (i.e. non-default) icons. 2023-02-24 15:59:58 +02:00
AccentuSoft
d9fe58de79 Update TwitterUser.py - make it compatible with the latest version of snscrape. 2023-02-24 14:56:59 +02:00
AccentuSoft
c8ad8ae383 Update RedditSearch.py - make resultTypes actually reflect the entities that could be generated. 2023-02-22 02:19:33 +02:00
AccentuSoft
83c1ffdcdc Update Installer. 2023-02-22 02:18:54 +02:00
AccentuSoft
756972aa75 Small bug fix. 2023-02-17 01:12:56 +02:00
AccentuSoft
3603f714fb Update README.md 2023-02-08 22:35:17 +02:00
AccentuSoft
7507990ca5 Push commits:
Move saved Resolutions parameters to global scope.

Move all Program settings to global scope.

Update & Refactor Settings Object.

Update Whats My Name - make detection more precise & add sites.

Update README.md

Update version number

Finalize importing tabs from TOR.

Change settings labels so they look better in menus. Add Tor browser path setting.

Fix bug with deselected entities when zooming out.

Transition to pyqtdarktheme for theme & styling.

Check if firefox snap exists if firefox package is not present.

Make sure that every file from the directories holding resolutions is present.

Refactor importing of tabs.
2023-02-07 18:04:19 +02:00
AccentuSoft
1c8452be08 Push commits through:
Minor optimizations.

Make the properties editors use Global Variables for hidden fields.

Finalize banners - they should now be persistent & work properly.

Update build scripts.

Make DockBarTwo.py use Global Variables for the dockbars' hidden fields.

Add logging for LinkScope version.

Update GlobalVariables.py - adjust the hidden fields & the fields to avoid parsing.

Update Settings Object to have version & TOR file location parameters.

Add npmjs search.
2023-01-26 21:28:06 +02:00
AccentuSoft
f535f5f9aa Adjust ExtractPDFMeta.py resolution to use new library & improve logic. 2023-01-04 10:10:05 +02:00
AccentuSoft
118b9c2c57 Context-dependent enabling of banner stuff & some minor reformatting. 2022-12-20 13:30:27 +02:00
AccentuSoft
efd7bf4f1f Started adding support for Banners. 2022-12-20 01:56:53 +02:00
AccentuSoft
8e0e7115c5 Add resolutions to extract wallet strings & long alphanumeric strings from websites. 2022-12-19 20:41:55 +02:00
AccentuSoft
9abeb5e0ec Allow renaming of Macros. 2022-12-19 15:43:56 +02:00
AccentuSoft
fc15db46a3 Add Banner resources. 2022-12-12 19:31:31 +02:00
AccentuSoft
435f4ed97e Update to python 3.10 & Improve ResourceHandler.py formatting. 2022-12-12 19:30:56 +02:00
AccentuSoft
202f8e0ceb Add a few more recognized file formats for FileExtractor.py 2022-11-17 11:52:54 +02:00
AccentuSoft
e16b6f16ff Add icons for entities 2022-11-17 11:52:19 +02:00
AccentuSoft
3b9e431d12 Add Resolution support for Spreadsheet documents specifically. 2022-11-11 18:58:38 +02:00
AccentuSoft
d49c884752 Add new module for email reputation check. 2022-11-11 15:55:39 +02:00
AccentuSoft
ac5ec3d6db Make macros program-wide. 2022-10-31 06:48:17 +02:00
AccentuSoft
93d921daa2 Merge branch 'main' of https://github.com/AccentuSoft/LinkScope_Client into main 2022-10-29 20:45:48 +03:00
AccentuSoft
c44409fab8 Update to PySide 6.4 2022-10-29 20:42:09 +03:00
AccentuSoft
d8035bdb12 Update WhatsMyName json 2022-10-29 20:42:09 +03:00
AccentuSoft
d0b643c633 Small update to Installer.py 2022-10-29 20:42:09 +03:00
AccentuSoft
a0864cdca5 Add ReplacePhrase resolution & small update to ResolutionManager.py 2022-10-29 20:42:09 +03:00
AccentuSoft
3c864ba511 Refactor core resolutions. 2022-10-29 20:42:09 +03:00
AccentuSoft
1fe87263dc Add support for importing spreadsheets seamlessly. 2022-10-29 20:42:09 +03:00
AccentuSoft
16228612c3 Fix a few bugs with resolutions 2022-10-29 20:42:09 +03:00
AccentuSoft
fbeb393963 Update README.md with YouTube channel details. 2022-10-17 18:38:33 +03:00
AccentuSoft
0503c64f5f Update README.md
Add some clarifying detail for potential installation issues on Linux.
2022-10-14 18:12:12 +03:00
AccentuSoft
194208f772 Update link in README.md 2022-10-14 18:00:25 +03:00
241 changed files with 5885 additions and 23670 deletions

View File

@@ -8,6 +8,15 @@
Document.svg
</Icon>
</Document>
<Spreadsheet>
<Attributes>
<Attribute default="Spreadsheet" check="String" primary="True">Spreadsheet Name</Attribute>
<Attribute default="DefaultFilePath" check="String" primary="False">File Path</Attribute>
</Attributes>
<Icon>
Spreadsheet.svg
</Icon>
</Spreadsheet>
<Image>
<Attributes>
<Attribute default="Image" check="String" primary="True">Image Name</Attribute>

View File

@@ -15,9 +15,7 @@ class EntitiesDB:
links on a project-wide scale.
"""
def __init__(self, mainWindow, messageHandler, resourceHandler) -> None:
self.messageHandler = messageHandler
self.resourceHandler = resourceHandler
def __init__(self, mainWindow) -> None:
self.mainWindow = mainWindow
self.dbLock = Lock()
self.database = None
@@ -33,17 +31,18 @@ class EntitiesDB:
if self.database is not None:
self.save()
databaseFile = Path(self.mainWindow.SETTINGS.value("Project/FilesDir")).joinpath("LocalEntitiesDB.lsdb")
self.messageHandler.debug(f'Opening Database at: {str(databaseFile)}')
self.mainWindow.MESSAGEHANDLER.debug(f'Opening Database at: {databaseFile}')
try:
with open(databaseFile, "rb") as dbFile:
self.database = self.mainWindow.RESOURCEHANDLER.reconstructGraphFullFromFile(load(dbFile))
self.messageHandler.info('Loaded Local Entities Database.')
self.mainWindow.MESSAGEHANDLER.info('Loaded Local Entities Database.')
except FileNotFoundError:
self.messageHandler.info('Creating new Local Entities Database.')
self.mainWindow.MESSAGEHANDLER.info('Creating new Local Entities Database.')
self.database = nx.DiGraph()
except Exception as exc:
self.messageHandler.error(f'Cannot parse Database: {str(exc)}\nCreating new Local Entities Database.',
popUp=True)
self.mainWindow.MESSAGEHANDLER.error(
f'Cannot parse Database: {exc}\nCreating new Local Entities Database.',
popUp=True)
self.database = nx.DiGraph()
def resetTimeline(self) -> None:
@@ -75,7 +74,7 @@ class EntitiesDB:
with open(tmpSavePath, "wb") as dbFile:
dump(self.mainWindow.RESOURCEHANDLER.deconstructGraphForFileDump(self.database), dbFile)
move(tmpSavePath, databaseFile)
self.messageHandler.info('Database Saved.')
self.mainWindow.MESSAGEHANDLER.info('Database Saved.')
def addEntity(self, entJson: dict, fromServer: bool = False, updateTimeline: bool = True) -> Union[dict, None]:
"""
@@ -89,7 +88,7 @@ class EntitiesDB:
if entJson.get('uid') is not None:
exists = self.getEntityNoLock(entJson.get('uid'))
entity = self.resourceHandler.getEntityJson(
entity = self.mainWindow.RESOURCEHANDLER.getEntityJson(
entJson.get('Entity Type'),
entJson)
@@ -124,7 +123,7 @@ class EntitiesDB:
if entJson.get('uid') is not None:
exists = self.getEntityNoLock(entJson.get('uid'))
entity = self.resourceHandler.getEntityJson(
entity = self.mainWindow.RESOURCEHANDLER.getEntityJson(
entJson.get('Entity Type'),
entJson)
@@ -155,13 +154,12 @@ class EntitiesDB:
"""
with self.dbLock:
exists = self.isLinkNoLock(linkJson['uid'])
link = self.resourceHandler.getLinkJson(linkJson)
link = self.mainWindow.RESOURCEHANDLER.getLinkJson(linkJson)
if link is None:
# This can technically be caused by a race condition if the user
# either tries really hard or gets really unlucky.
# Caused by deleting a node faster than the link can be created.
self.messageHandler.error("Attempted to add Link with "
"no uid to database.", popUp=True)
self.mainWindow.MESSAGEHANDLER.error("Attempted to add Link with no uid to database.", popUp=True)
return None
else:
linkUID = link['uid']
@@ -203,7 +201,7 @@ class EntitiesDB:
try:
returnValue = self.database.nodes[uid]
except KeyError:
self.messageHandler.warning(f"Tried to get entity with nonexistent UID: {uid}")
self.mainWindow.MESSAGEHANDLER.warning(f"Tried to get entity with nonexistent UID: {uid}")
finally:
return returnValue
@@ -215,8 +213,8 @@ class EntitiesDB:
returnValue = None
try:
returnValue = [self.database.nodes[node] for node in self.database.nodes()]
except KeyError:
self.messageHandler.error("Tried to get entity with nonexistent UID.")
except KeyError as keyError:
self.mainWindow.MESSAGEHANDLER.error(f"Tried to get entity with nonexistent UID. Error: {keyError}")
finally:
return returnValue
@@ -230,7 +228,7 @@ class EntitiesDB:
try:
returnValue = [self.database.edges[edge] for edge in self.database.edges()]
except KeyError:
self.messageHandler.error("Tried to get link with nonexistent UID.")
self.mainWindow.MESSAGEHANDLER.error("Tried to get link with nonexistent UID.")
finally:
return returnValue
@@ -258,8 +256,7 @@ class EntitiesDB:
try:
returnValue = self.database.edges[uid]
except KeyError:
self.messageHandler.error(
"Tried to get link with nonexistent UID.")
self.mainWindow.MESSAGEHANDLER.error(f"Tried to get link with nonexistent UID: {uid}")
finally:
return returnValue
@@ -310,7 +307,7 @@ class EntitiesDB:
Checks if an entity with the specified primary attribute exists, and if it does, return it.
"""
result = None
primaryField = self.resourceHandler.getPrimaryFieldForEntityType(entityType)
primaryField = self.mainWindow.RESOURCEHANDLER.getPrimaryFieldForEntityType(entityType)
if primaryField is None:
return result
with self.dbLock:

View File

@@ -27,7 +27,7 @@ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.exceptions import InvalidTag
# Amount of data to place in each message
MESSAGE_DATA_SIZE = 8192
MESSAGE_DATA_SIZE = 8192 * 5
# Needs to be a bit bigger than MESSAGE_DATA_SIZE
RECV_SIZE = MESSAGE_DATA_SIZE + 1024
@@ -54,8 +54,8 @@ class CommunicationsHandler(QtCore.QObject):
receive_start_collector_signal = QtCore.Signal(str, str, str, list, dict)
receive_collector_result_signal = QtCore.Signal(str, str, str, list)
receive_resolutions_signal = QtCore.Signal(dict)
receive_completed_resolution_result_signal = QtCore.Signal(str, list)
receive_completed_resolution_string_result_signal = QtCore.Signal(str, str)
receive_completed_resolution_result_signal = QtCore.Signal(str, list, str)
receive_completed_resolution_string_result_signal = QtCore.Signal(str, str, str)
receive_document_summary_signal = QtCore.Signal(str, str)
remove_server_resolution_from_running_signal = QtCore.Signal(str)
receive_projects_list_signal = QtCore.Signal(list)
@@ -149,12 +149,10 @@ class CommunicationsHandler(QtCore.QObject):
self.sock.send(passMessage)
messageReceived = decrypter.update(self.sock.recv(RECV_SIZE)) + decrypter.finalize()
if messageReceived == b"Passphrase is OK":
self.threadInc = threading.Thread(target=self.scanIncoming)
self.threadInc.setDaemon(True)
self.threadInc = threading.Thread(target=self.scanIncoming, daemon=True)
self.threadInc.start()
self.threadInb = threading.Thread(target=self.scanInbox)
self.threadInb.setDaemon(True)
self.threadInb = threading.Thread(target=self.scanInbox, daemon=True)
self.threadInb.start()
return True
@@ -270,7 +268,7 @@ class CommunicationsHandler(QtCore.QObject):
# Socket closed.
break
receivedInfo = oldData + receivedInfo
messages = receivedInfo.split(b'\x03\x03\x03\x03\x03')
messages = receivedInfo.split(b'\x03\x03\x03\x03\x03\x03\x03\x03')
# Last message is either blank (i.e. '') or incomplete data, so we ignore it.
oldData = messages[-1]
messages = messages[:-1]
@@ -364,9 +362,7 @@ class CommunicationsHandler(QtCore.QObject):
with contextlib.suppress(KeyError):
dereferenced_entity = dict(entity)
resolution_entities_to_send.append(dereferenced_entity)
# Icon is not necessary for any resolution as of now: 2022/1/2.
# Cutting it out saves data.
dereferenced_entity['Icon'] = ''
dereferenced_entity['Icon'] = dereferenced_entity['Icon'].toBase64().data()
message = {'Operation': 'Run Resolution',
'Arguments': {
'resolution_name': resolution_name,
@@ -379,9 +375,14 @@ class CommunicationsHandler(QtCore.QObject):
def receiveResolutionResult(self, resolution_name: str, resolution_result: Union[list, str],
resolution_uid: str) -> None:
if isinstance(resolution_result, str):
self.receive_completed_resolution_string_result_signal.emit(resolution_name, resolution_result)
self.receive_completed_resolution_string_result_signal.emit(resolution_name, resolution_result,
resolution_uid)
else:
self.receive_completed_resolution_result_signal.emit(resolution_name, resolution_result)
for res_result in resolution_result:
if res_icon := res_result[0].get('Icon'):
res_result[0]['Icon'] = QtCore.QByteArray(b64decode(res_icon))
self.receive_completed_resolution_result_signal.emit(resolution_name, resolution_result,
resolution_uid)
self.remove_server_resolution_from_running_signal.emit(resolution_uid)
def abortResolution(self, resolution_name: str, resolution_uid: str) -> None:
@@ -540,8 +541,8 @@ class CommunicationsHandler(QtCore.QObject):
:param filePath:
:return:
"""
sendHelperThread = threading.Thread(target=self.sendFileHelper, args=(project_name, file_name, filePath))
sendHelperThread.setDaemon(True)
sendHelperThread = threading.Thread(target=self.sendFileHelper, daemon=True,
args=(project_name, file_name, filePath))
self.uploadingFiles[file_name] = sendHelperThread
sendHelperThread.start()
@@ -557,7 +558,7 @@ class CommunicationsHandler(QtCore.QObject):
if not filePath.exists() or not filePath.is_file():
return
with open(filePath, 'rb') as fileHandler:
currThread = threading.currentThread()
currThread = threading.current_thread()
while getattr(currThread, "continue_running", True):
filePart = fileHandler.read(512)
if not filePart:

View File

@@ -3,9 +3,10 @@
import random
non_string_fields = ('Icon', 'Child UIDs')
hidden_fields = ('uid', 'Date Last Edited', 'Child UIDs')
hidden_fields = ('uid', 'Date Last Edited', 'Child UIDs', 'Canvas Banner', 'Entity Type')
hidden_fields_dockbars = ('uid', 'Child UIDs', 'Canvas Banner', 'Icon')
meta_fields = ('Child UIDs',)
avoid_parsing_fields = ('uid', 'Date Last Edited', 'Child UIDs', 'Icon')
avoid_parsing_fields = ('uid', 'Date Last Edited', 'Child UIDs', 'Icon', 'Canvas Banner')
# Closer to the top means more recent.
user_agents = {'Chrome': {'Windows': ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '

View File

@@ -1,7 +1,6 @@
#!/usr/bin/env python3
import contextlib
import re
import json
import sys
import threading
@@ -12,16 +11,15 @@ import folium
import networkx as nx
from shutil import move
from msgpack import dump, load
from PIL import Image
from PIL.ImageQt import ImageQt
from pathlib import Path
from PySide6 import QtWidgets, QtGui, QtCore
from PySide6.QtWidgets import QGraphicsPixmapItem
from PySide6.QtSvgWidgets import QGraphicsSvgItem
from PySide6.QtWebEngineWidgets import QWebEngineView
from Core.Interface import Entity, Stylesheets
from Core.ResourceHandler import RichNotesEditor
from Core.Interface import Entity
from Core.ResourceHandler import RichNotesEditor, resizePictureFromBuffer
from Core.GlobalVariables import hidden_fields
class WorkspaceWidget(QtWidgets.QWidget):
@@ -90,7 +88,7 @@ class TabBar(QtWidgets.QTabBar):
self.setMovable(True)
def mouseDoubleClickEvent(self, event) -> None:
if event.button() != QtGui.Qt.LeftButton:
if event.button() != QtGui.Qt.MouseButton.LeftButton:
return
currIndex = self.currentIndex()
currName = self.tabText(currIndex)
@@ -123,7 +121,6 @@ class RenameOrDeleteTabDialog(QtWidgets.QDialog):
def __init__(self, isSynced: bool, currName: str) -> None:
super(RenameOrDeleteTabDialog, self).__init__()
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
self.setModal(True)
self.setWindowTitle('Rename Or Delete Tab')
@@ -195,6 +192,9 @@ class TabbedPane(QtWidgets.QTabWidget):
self.currentChanged.connect(self.currentTabChangedListener)
def getAllBanners(self) -> dict:
return dict(self.mainWindow.RESOURCEHANDLER.banners)
def getCanvasDBPath(self):
return Path(self.mainWindow.SETTINGS.value("Project/BaseDir")) / "Project Files" / "CanvasTabs.lscanvas"
@@ -352,7 +352,7 @@ class TabbedPane(QtWidgets.QTabWidget):
progress = QtWidgets.QProgressDialog(f'Resolving new nodes for resolution: {resolution_name}, please wait...',
'Abort Resolving Nodes', 0, steps, self)
progress.setWindowModality(QtCore.Qt.WindowModal)
progress.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
progress.setMinimumDuration(1500)
# In case we have no entities in the database when the resolution finishes, i.e. the user deletes the origin
@@ -600,8 +600,9 @@ class TabbedPane(QtWidgets.QTabWidget):
# Save canvases
with open(canvasDBPathTmp, "wb") as canvasDBFile:
saveJson = {canvasName: [self.resourceHandler.deconstructGraphForFileDump(self.canvasTabs[canvasName].scene().sceneGraph),
self.canvasTabs[canvasName].scene().scenePos] for canvasName in self.canvasTabs}
saveJson = {canvasName: [self.resourceHandler.deconstructGraphForFileDump(
self.canvasTabs[canvasName].scene().sceneGraph),
self.canvasTabs[canvasName].scene().scenePos] for canvasName in self.canvasTabs}
dump(saveJson, canvasDBFile)
move(canvasDBPathTmp, canvasDBPath)
@@ -746,31 +747,25 @@ class CanvasView(QtWidgets.QGraphicsView):
self.name = name
self.urlManager = urlManager
self.setRenderHint(QtGui.QPainter.Antialiasing)
self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
self.setViewportUpdateMode(QtWidgets.QGraphicsView.FullViewportUpdate)
self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
# self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
# self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing)
self.setTransformationAnchor(QtWidgets.QGraphicsView.ViewportAnchor.AnchorUnderMouse)
self.setViewportUpdateMode(QtWidgets.QGraphicsView.ViewportUpdateMode.FullViewportUpdate)
self.setResizeAnchor(QtWidgets.QGraphicsView.ViewportAnchor.AnchorUnderMouse)
self.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(54, 69, 79)))
self.setFrameShape(QtWidgets.QFrame.NoFrame)
self.setFrameShape(QtWidgets.QFrame.Shape.NoFrame)
self.setAcceptDrops(True)
self.setDragMode(QtWidgets.QGraphicsView.NoDrag)
self.setDragMode(QtWidgets.QGraphicsView.DragMode.NoDrag)
self.setSizePolicy(QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding))
QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding))
self.dragOver = False
self.synced = False
self.menu = QtWidgets.QMenu()
self.menu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
selectMenu = self.menu.addMenu("Select...")
selectMenu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
viewMenu = self.menu.addMenu("Hide / Delete...")
viewMenu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
groupingMenu = self.menu.addMenu("Grouping...")
groupingMenu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
bannersMenu = self.menu.addMenu("Banners...")
actionSelectChildren = QtGui.QAction('Select Child Nodes',
selectMenu,
@@ -835,6 +830,18 @@ class CanvasView(QtWidgets.QGraphicsView):
triggered=self.importConnectedEntities)
self.menu.addAction(importConnectedEntitiesAction)
self.clearBannerMenu = QtGui.QAction('Clear Banners',
bannersMenu,
statusTip="Remove banners from the selected entities.",
triggered=self.clearBanners)
bannersMenu.addAction(self.clearBannerMenu)
self.setBannerIconMenu = QtGui.QAction('Set Banner Icon',
bannersMenu,
statusTip="Set a banner icon for the selected entities.",
triggered=self.setBanners)
bannersMenu.addAction(self.setBannerIconMenu)
def deleteItemsFromDatabase(self) -> None:
items = self.scene().selectedItems()
for item in items:
@@ -853,7 +860,7 @@ class CanvasView(QtWidgets.QGraphicsView):
def drawBackground(self, painter: QtGui.QPainter, rect: Union[QtCore.QRectF, QtCore.QRect]) -> None:
super(CanvasView, self).drawBackground(painter, rect)
# Ensure that all links will always be drawn.
[link.paint(painter, None, None) for link in list(self.scene().linksDict.values())]
[link.paint(painter, None, None) for link in self.scene().linksDict.values()]
def centerViewportOnNode(self, uid: str) -> None:
node = self.scene().getVisibleNodeForUID(uid)
@@ -867,7 +874,6 @@ class CanvasView(QtWidgets.QGraphicsView):
def dragEnterEvent(self, event) -> None:
if event.mimeData().hasText() or event.mimeData().hasImage():
event.setAccepted(True)
self.dragOver = True
self.update()
def dragLeaveEvent(self, event) -> None:
@@ -875,7 +881,6 @@ class CanvasView(QtWidgets.QGraphicsView):
def dropEvent(self, event) -> None:
pos = self.mapToScene(event.pos())
self.dragOver = False
mimeData = event.mimeData()
jsonData = None
@@ -915,8 +920,7 @@ class CanvasView(QtWidgets.QGraphicsView):
if entityUID in self.scene().sceneGraph.nodes():
wasGrouped = False
for groupNode in [node for node in self.items() if isinstance(node, Entity.GroupNode)]:
wasGrouped = \
groupNode.removeSpecificItemFromGroupIfExists(entityUID)
wasGrouped = groupNode.removeSpecificItemFromGroupIfExists(entityUID)
if wasGrouped:
self.removeGroupNodeLinksForUID(groupNode.uid, entityUID)
groupNodeJson = self.tabbedPane.entityDB.getEntity(groupNode.uid)
@@ -998,7 +1002,7 @@ class CanvasView(QtWidgets.QGraphicsView):
def mouseReleaseEvent(self, event) -> None:
QtWidgets.QGraphicsView.mouseReleaseEvent(self, event)
self.setDragMode(QtWidgets.QGraphicsView.NoDrag)
self.setDragMode(QtWidgets.QGraphicsView.DragMode.NoDrag)
itemsMoved = [item for item in self.scene().selectedItems()
if isinstance(item, Entity.BaseNode)]
for item in itemsMoved:
@@ -1027,10 +1031,11 @@ class CanvasView(QtWidgets.QGraphicsView):
if event.button() == QtCore.Qt.MouseButton.RightButton and \
not self.scene().linking and not self.scene().appendingToGroup:
if len(self.scene().selectedItems()) == 0:
self.setDragMode(QtWidgets.QGraphicsView.RubberBandDrag)
self.setDragMode(QtWidgets.QGraphicsView.DragMode.RubberBandDrag)
else:
items = self.scene().selectedItems()
groupItems = [groupItem for groupItem in items if isinstance(groupItem, Entity.GroupNode)]
entityItems = [entityItem for entityItem in items if isinstance(entityItem, Entity.BaseNode)]
groupItems = [groupItem for groupItem in entityItems if isinstance(groupItem, Entity.GroupNode)]
linkItems = [linkItem for linkItem in items if isinstance(linkItem, Entity.BaseConnector)]
if groupItems:
self.actionUngroup.setDisabled(False)
@@ -1038,12 +1043,23 @@ class CanvasView(QtWidgets.QGraphicsView):
else:
self.actionUngroup.setDisabled(True)
self.actionUngroup.setEnabled(False)
if len(items) > 1:
self.actionGroup.setDisabled(False)
self.actionGroup.setEnabled(True)
if entityItems:
self.clearBannerMenu.setDisabled(False)
self.clearBannerMenu.setEnabled(True)
self.setBannerIconMenu.setDisabled(False)
self.setBannerIconMenu.setEnabled(True)
if len(entityItems) > 1:
self.actionGroup.setDisabled(False)
self.actionGroup.setEnabled(True)
else:
self.actionGroup.setDisabled(True)
self.actionGroup.setEnabled(False)
else:
self.actionGroup.setDisabled(True)
self.actionGroup.setEnabled(False)
self.clearBannerMenu.setDisabled(True)
self.clearBannerMenu.setEnabled(False)
self.setBannerIconMenu.setDisabled(True)
self.setBannerIconMenu.setEnabled(False)
if linkItems:
self.actionLinkDelete.setDisabled(False)
self.actionLinkDelete.setEnabled(True)
@@ -1055,7 +1071,7 @@ class CanvasView(QtWidgets.QGraphicsView):
elif event.button() == QtCore.Qt.MouseButton.RightButton and self.scene().appendingToGroup:
self.scene().appendSelectedItemsToGroupToggle()
elif event.button() == QtCore.Qt.MouseButton.LeftButton:
self.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag)
self.setDragMode(QtWidgets.QGraphicsView.DragMode.ScrollHandDrag)
super(CanvasView, self).mousePressEvent(event)
def deleteSelectedLinks(self) -> None:
@@ -1135,6 +1151,33 @@ class CanvasView(QtWidgets.QGraphicsView):
newNode.setSelected(True)
self.scene().rearrangeGraph()
def clearBanners(self) -> None:
selectedEntities = [item for item in self.scene().selectedItems() if isinstance(item, Entity.BaseNode)]
for entity in selectedEntities:
entityJson = self.tabbedPane.mainWindow.LENTDB.getEntity(entity.uid)
entityJson['Canvas Banner'] = ''
entity.updateBanner(True, None)
self.tabbedPane.mainWindow.LENTDB.addEntity(entityJson, updateTimeline=False)
def setBanners(self) -> None:
selectedEntities = [item for item in self.scene().selectedItems() if isinstance(item, Entity.BaseNode)]
if not selectedEntities:
self.tabbedPane.mainWindow.MESSAGEHANDLER.warning('Need to select at least one Entity to set its banner.',
popUp=True)
return
bannerDialog = BannerSelector(self.tabbedPane.getAllBanners())
if bannerDialog.exec():
try:
# This following line will throw IndexError if no banner is selected.
selectedBannerItem = bannerDialog.bannerIconContainer.selectedItems()[0].text()
self.scene().bannerDrawHelper(selectedEntities, selectedBannerItem)
for entity in selectedEntities:
entityJson = self.tabbedPane.mainWindow.LENTDB.getEntity(entity.uid)
entityJson['Canvas Banner'] = selectedBannerItem
self.tabbedPane.mainWindow.LENTDB.addEntity(entityJson, updateTimeline=False)
except IndexError:
self.tabbedPane.mainWindow.MESSAGEHANDLER.warning('No Banner selected.', popUp=True)
def takePictureOfView(self, justViewport: bool = True, transparentBackground: bool = False) -> QtGui.QImage:
# Need to set size and format of pic before using it.
# Ref: https://qtcentre.org/threads/10975-Help-Export-QGraphicsView-to-Image-File
@@ -1144,7 +1187,7 @@ class CanvasView(QtWidgets.QGraphicsView):
for item in selectedItems:
item.setSelected(False)
if justViewport:
picture = QtGui.QImage(self.viewport().size(), QtGui.QImage.Format_ARGB32_Premultiplied)
picture = QtGui.QImage(self.viewport().size(), QtGui.QImage.Format.Format_ARGB32_Premultiplied)
# Pictures are initialised with junk data - need to clear it out before painting
# to avoid visual artifacts.
picture.fill(QtGui.QColor(0, 0, 0, 0))
@@ -1157,7 +1200,7 @@ class CanvasView(QtWidgets.QGraphicsView):
else:
# Convert QRectF to QRect - can't have floats when it comes to picture size.
rectToPrint = self.scene().sceneRect().toRect()
picture = QtGui.QImage(rectToPrint.size(), QtGui.QImage.Format_ARGB32_Premultiplied)
picture = QtGui.QImage(rectToPrint.size(), QtGui.QImage.Format.Format_ARGB32_Premultiplied)
# Pictures are initialised with junk data - need to clear it out before painting
# to avoid visual artifacts.
picture.fill(QtGui.QColor(0, 0, 0, 0))
@@ -1224,6 +1267,49 @@ class CanvasScene(QtWidgets.QGraphicsScene):
# Re-Center the Label
item.updateLabel(item.labelItem.text())
def bannerDrawHelper(self, entities: list, bannerName: str = None) -> None:
"""
If we are given a banner name, try to set each canvas entity banner to the banner with the given name.
If not, then instead we get the banner that each entity is already assigned, and make sure it's drawn.
"""
if bannerName:
try:
bannerPathStr = self.parent().getAllBanners()[bannerName]
with open(bannerPathStr, 'rb') as bannerFile:
bannerByteArray = QtCore.QByteArray(bannerFile.read())
for entity in entities:
entity.updateBanner(False, bannerByteArray)
except FileNotFoundError:
self.parent().mainWindow.MESSAGEHANDLER.error(f'Banner Icon not found in filesystem: {bannerName}',
popUp=True,
exc_info=False)
except KeyError:
self.parent().mainWindow.MESSAGEHANDLER.warning(f'Invalid Banner: {bannerName}', popUp=True)
else:
notFoundBanners = set()
for entity in entities:
try:
entityJson = self.parent().mainWindow.LENTDB.getEntity(entity.uid)
if bannerPathStr := self.parent().getAllBanners().get(
entityJson.get('Canvas Banner', ''), ''
):
with open(bannerPathStr, 'rb') as bannerFile:
bannerByteArray = QtCore.QByteArray(bannerFile.read())
entity.updateBanner(False, bannerByteArray)
else:
entity.updateBanner(True, None)
except FileNotFoundError:
if bannerName not in notFoundBanners:
self.parent().mainWindow.MESSAGEHANDLER.error(f'Banner Icon not found in filesystem: '
f'{bannerName}',
popUp=True,
exc_info=False)
notFoundBanners.add(bannerName)
except KeyError:
if bannerName not in notFoundBanners:
self.parent().mainWindow.MESSAGEHANDLER.warning(f'Invalid Banner: {bannerName}', popUp=True)
notFoundBanners.add(bannerName)
# Redefined so that the BaseConnector items are not considered.
def itemsBoundingRect(self) -> QtCore.QRectF:
try:
@@ -1246,6 +1332,7 @@ class CanvasScene(QtWidgets.QGraphicsScene):
def addNodeToScene(self, item, x=0, y=0) -> None:
self.nodesDict[item.uid] = item
self.addItem(item)
self.bannerDrawHelper([item])
item.setPos(QtCore.QPointF(x, y))
self.parent().mainWindow.MESSAGEHANDLER.info(f'Added node: {str(item.uid)} | {item.labelItem.toPlainText()}')
@@ -1365,7 +1452,7 @@ class CanvasScene(QtWidgets.QGraphicsScene):
# Remove Cancel button from progress bar (user should not be able to stop canvas from loading).
progress.setMinimumDuration(1500)
progress.setCancelButton(None)
progress.setWindowModality(QtCore.Qt.WindowModal)
progress.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
progressValue = 0
for node in sceneGraphNodes:
@@ -1390,11 +1477,11 @@ class CanvasScene(QtWidgets.QGraphicsScene):
if groupItems is None:
newNode = Entity.BaseNode(picture, node, nodePrimaryAttribute, self.entityTextFont,
self.entityTextBrush)
self.addNodeToScene(newNode)
self.addNodeToScene(newNode, positions[node][0], positions[node][1])
else:
newNode = Entity.GroupNode(picture, node, nodePrimaryAttribute, self.entityTextFont,
self.entityTextBrush)
self.addNodeToScene(newNode)
self.addNodeToScene(newNode, positions[node][0], positions[node][1])
newGroupList = newNode.listWidget
newGroupListGraphic = self.addWidget(newGroupList)
@@ -1403,8 +1490,6 @@ class CanvasScene(QtWidgets.QGraphicsScene):
for item in groupItems:
self.sceneGraph.add_node(item, groupID=newNode.uid)
newNode.setPos(QtCore.QPointF(positions[node][0], positions[node][1]))
progressValue += 1
progress.setValue(progressValue)
@@ -1515,7 +1600,7 @@ class CanvasScene(QtWidgets.QGraphicsScene):
# No triangulation library on Windows, so sfdp can't be used there.
if graphAlgorithm is None or graphAlgorithm not in ('sfdp', 'neato', 'dot', 'circular'):
graphAlgorithm = self.parent().mainWindow.SETTINGS.value("Program/GraphLayout", 'dot')
graphAlgorithm = self.parent().mainWindow.SETTINGS.value("Program/Graph Layout", 'dot')
# No real 'links' to group nodes by default (links to internal nodes don't count). This means that the
# graph algorithms can create odd graphs where group nodes are concerned.
@@ -1605,6 +1690,9 @@ class CanvasScene(QtWidgets.QGraphicsScene):
# Should never happen, but we will handle it if it does.
self.parent().mainWindow.MESSAGEHANDLER.warning(f'Entity without valid Date Created: {str(node)}')
entityDate = datetime.now().replace(microsecond=0, second=0)
if entityDate.tzinfo is None or entityDate.tzinfo.utcoffset(entityDate) is None:
# Make timezone-naive objects into timezone-aware, with the user's current timezone.
entityDate = entityDate.replace(tzinfo=datetime.now().astimezone().tzinfo)
if entityDate not in nodesOnCanvas:
nodesOnCanvas[entityDate] = [node]
else:
@@ -1795,13 +1883,14 @@ class CanvasScene(QtWidgets.QGraphicsScene):
self.removeItem(item.iconItem)
pictureByteArray = pEditor.objectJson['Icon']
item.pixmapItem = QtGui.QPixmap()
item.pixmapItem.loadFromData(pictureByteArray)
pictureByteArray = resizePictureFromBuffer(pictureByteArray, (40, 40))
if pictureByteArray.data().startswith(b'<svg '):
item.iconItem = QGraphicsSvgItem()
item.iconItem.renderer().load(pictureByteArray)
item.iconItem.setElementId("") # Force recalculation of geometry, else this looks like 1 pixel.
else:
item.pixmapItem = QtGui.QPixmap()
item.pixmapItem.loadFromData(pictureByteArray)
item.iconItem = QGraphicsPixmapItem(item.pixmapItem)
item.iconItem.setPos(item.pos())
@@ -1832,9 +1921,16 @@ class CanvasScene(QtWidgets.QGraphicsScene):
for node in self.nodesDict:
self.nodesDict[node].setSelected(True)
def selectChildNodes(self) -> None:
items = [item.uid for item in self.selectedItems() if isinstance(item, Entity.BaseNode)]
def selectNodesFromList(self, nodesList: list):
self.clearSelection()
for node in nodesList:
if node in self.nodesDict:
self.nodesDict[node].setSelected(True)
def selectChildNodes(self, clearSelection: bool = True) -> None:
items = [item.uid for item in self.selectedItems() if isinstance(item, Entity.BaseNode)]
if clearSelection:
self.clearSelection()
for item in items:
childLinks = self.parent().entityDB.getOutgoingLinks(item)
for childLink in childLinks:
@@ -1842,9 +1938,10 @@ class CanvasScene(QtWidgets.QGraphicsScene):
with contextlib.suppress(KeyError):
self.nodesDict[childLink[1]].setSelected(True)
def selectParentNodes(self) -> None:
def selectParentNodes(self, clearSelection: bool = True) -> None:
items = [item.uid for item in self.selectedItems() if isinstance(item, Entity.BaseNode)]
self.clearSelection()
if clearSelection:
self.clearSelection()
for item in items:
parentLinks = self.parent().entityDB.getIncomingLinks(item)
for parentLink in parentLinks:
@@ -1971,14 +2068,13 @@ class PropertiesEditor(QtWidgets.QDialog):
self.isEditingNode = isNode
self.setWindowTitle("Properties Editor")
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
self.setMinimumSize(500, 300)
self.objectJson = objectJson
self.canvas = canvas
self.itemProperties = QtWidgets.QFormLayout()
for key in objectJson:
if key in ('uid', 'Entity Type', 'Date Last Edited', 'Child UIDs'):
if key in hidden_fields:
continue
keyField = QtWidgets.QLabel(key)
if key == "Notes":
@@ -1991,9 +2087,7 @@ class PropertiesEditor(QtWidgets.QDialog):
valueField = QtWidgets.QLineEdit(str(objectJson[key]))
self.itemProperties.addRow(keyField, valueField)
acceptButton = QtWidgets.QPushButton("Confirm")
acceptButton.setStyleSheet(Stylesheets.BUTTON_STYLESHEET)
cancelButton = QtWidgets.QPushButton("Cancel")
cancelButton.setStyleSheet(Stylesheets.BUTTON_STYLESHEET)
acceptButton.setAutoDefault(True)
acceptButton.setDefault(True)
acceptButton.clicked.connect(self.accept)
@@ -2005,16 +2099,16 @@ class PropertiesEditor(QtWidgets.QDialog):
def accept(self):
for row in range(self.itemProperties.rowCount()):
key = self.itemProperties.itemAt(
row, self.itemProperties.LabelRole).widget().text()
row, self.itemProperties.ItemRole.LabelRole).widget().text()
if key == "Notes":
value = self.itemProperties.itemAt(
row, self.itemProperties.FieldRole).widget().toMarkdown()
row, self.itemProperties.ItemRole.FieldRole).widget().toMarkdown()
elif key == 'Icon':
value = self.itemProperties.itemAt(
row, self.itemProperties.FieldRole).widget().pictureByteArray
row, self.itemProperties.ItemRole.FieldRole).widget().pictureByteArray
elif key == 'File Path':
value = self.itemProperties.itemAt(
row, self.itemProperties.FieldRole).widget().text()
row, self.itemProperties.ItemRole.FieldRole).widget().text()
projectFilesPath = Path(self.canvas.parent().mainWindow.SETTINGS.value("Project/FilesDir"))
newPath = projectFilesPath / value
if not newPath.is_relative_to(projectFilesPath):
@@ -2024,7 +2118,7 @@ class PropertiesEditor(QtWidgets.QDialog):
value = 'None'
else:
value = self.itemProperties.itemAt(
row, self.itemProperties.FieldRole).widget().text()
row, self.itemProperties.ItemRole.FieldRole).widget().text()
# The last row is the Cancel / Accept buttons.
if key != "Cancel":
self.objectJson[key] = value
@@ -2053,8 +2147,10 @@ class PropertiesEditorFilePathField(QtWidgets.QLineEdit):
self.setText(str(value))
def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
selectedPath = QtWidgets.QFileDialog().getOpenFileName(parent=self, caption='Select File Path',
options=QtWidgets.QFileDialog.DontUseNativeDialog)[0]
selectedPath = QtWidgets.QFileDialog().getOpenFileName(
parent=self,
caption='Select File Path',
options=QtWidgets.QFileDialog.Option.DontUseNativeDialog)[0]
if selectedPath != '':
self.setText(str(Path(selectedPath).absolute()))
@@ -2067,50 +2163,25 @@ class PropertiesEditorIconField(QtWidgets.QLabel):
super(PropertiesEditorIconField, self).__init__()
self.pictureByteArray = pictureByteArray
pixmapToSet = QtGui.QPixmap()
pixmapToSet.loadFromData(pictureByteArray)
pixmapToSet.loadFromData(resizePictureFromBuffer(pictureByteArray, (40, 40)))
self.setPixmap(pixmapToSet)
self.uid = uid
self.canvas = canvas
self.mainWindow = self.canvas.parent().mainWindow
def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
selectedPath = QtWidgets.QFileDialog().getOpenFileName(parent=self, caption='Select New Icon',
options=QtWidgets.QFileDialog.DontUseNativeDialog,
options=QtWidgets.QFileDialog.Option.DontUseNativeDialog,
filter="Image Files (*.png *.jpg *.bmp *.svg)")[0]
if selectedPath != '':
try:
filePath = Path(selectedPath)
with open(filePath, 'rb') as newIconFile:
fileContents = newIconFile.read()
if fileContents.startswith(b'<svg '):
widthRegex = re.compile(b' width="\d*" ')
for widthMatches in widthRegex.findall(fileContents):
fileContents = fileContents.replace(widthMatches, b' ')
heightRegex = re.compile(b' height="\d*" ')
for heightMatches in heightRegex.findall(fileContents):
fileContents = fileContents.replace(heightMatches, b' ')
fileContents = fileContents.replace(b'<svg ', b'<svg height="40" width="40" ', 1)
self.pictureByteArray = QtCore.QByteArray(fileContents)
else:
image = Image.open(selectedPath)
thumbSize = 40, 40
thumbnail = ImageQt(image.resize(thumbSize))
self.pictureByteArray = QtCore.QByteArray()
imageBuffer = QtCore.QBuffer(self.pictureByteArray)
imageBuffer.open(QtCore.QIODevice.WriteOnly)
thumbnail.save(imageBuffer, "PNG")
imageBuffer.close()
filePath = Path(selectedPath)
newPic = self.mainWindow.RESOURCEHANDLER.getPictureFromFile(filePath)
if newPic is not None:
self.pictureByteArray = newPic
pixmapToSet = QtGui.QPixmap()
pixmapToSet.loadFromData(self.pictureByteArray)
pixmapToSet.loadFromData(resizePictureFromBuffer(newPic, (40, 40)))
self.setPixmap(pixmapToSet)
except ValueError as ve:
# Image type is unsupported (for ImageQt)
# Supported types: 1, L, P, RGB, RGBA
self.canvas.parent().mainWindow.MESSAGEHANDLER.warning(f'Invalid Image selected: {str(ve)}', popUp=True)
super(PropertiesEditorIconField, self).mousePressEvent(event)
@@ -2119,7 +2190,6 @@ class SendToOtherTabCanvasSelector(QtWidgets.QDialog):
def __init__(self, canvasNames: list):
super(SendToOtherTabCanvasSelector, self).__init__()
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
self.setModal(True)
self.setWindowTitle('Move Selected Entities to New Canvas')
@@ -2143,3 +2213,48 @@ class SendToOtherTabCanvasSelector(QtWidgets.QDialog):
sendToOtherCanvasLayout.addWidget(self.canvasNameSelector, 1, 1, 1, 2)
sendToOtherCanvasLayout.addWidget(cancelButton, 2, 0, 1, 1)
sendToOtherCanvasLayout.addWidget(confirmButton, 2, 1, 1, 2)
class BannerSelector(QtWidgets.QDialog):
def __init__(self, bannerDict: dict):
super(BannerSelector, self).__init__()
self.setModal(True)
self.setWindowTitle('Select Banner')
bannerLayout = QtWidgets.QVBoxLayout()
self.setLayout(bannerLayout)
descriptionLabel = QtWidgets.QLabel("Select the Banner that you want to apply to the selected Entities:")
descriptionLabel.setWordWrap(True)
bannerLayout.addWidget(descriptionLabel)
self.bannerIconContainer = QtWidgets.QListWidget()
self.bannerIconContainer.setFlow(self.bannerIconContainer.Flow.LeftToRight)
self.bannerIconContainer.setMovement(self.bannerIconContainer.Movement.Static)
self.bannerIconContainer.setViewMode(self.bannerIconContainer.ViewMode.IconMode)
self.bannerIconContainer.setLayoutMode(self.bannerIconContainer.LayoutMode.SinglePass)
self.bannerIconContainer.setSelectionMode(self.bannerIconContainer.SelectionMode.SingleSelection)
self.bannerIconContainer.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.bannerIconContainer.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.bannerIconContainer.setSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum,
QtWidgets.QSizePolicy.Policy.Minimum)
for bannerID, bannerPathStr in bannerDict.items():
bannerPixmap = QtGui.QIcon(bannerPathStr)
QtWidgets.QListWidgetItem(bannerPixmap, bannerID, self.bannerIconContainer)
bannerLayout.addWidget(self.bannerIconContainer)
buttonsWidget = QtWidgets.QWidget()
buttonsWidgetLayout = QtWidgets.QHBoxLayout()
buttonsWidget.setLayout(buttonsWidgetLayout)
cancelButton = QtWidgets.QPushButton('Cancel')
cancelButton.clicked.connect(self.reject)
acceptButton = QtWidgets.QPushButton('Confirm')
acceptButton.clicked.connect(self.accept)
acceptButton.setAutoDefault(True)
acceptButton.setDefault(True)
buttonsWidgetLayout.addWidget(cancelButton)
buttonsWidgetLayout.addWidget(acceptButton)
bannerLayout.addWidget(buttonsWidget)

View File

@@ -1,9 +1,10 @@
#!/usr/bin/env python3
from json import dumps
from Core.Interface.Entity import BaseNode
from Core.Interface import Stylesheets
from PySide6 import QtWidgets, QtCore, QtGui
from Core.ResourceHandler import resizePictureFromBuffer
class DockBarOne(QtWidgets.QDockWidget):
@@ -36,11 +37,11 @@ class DockBarOne(QtWidgets.QDockWidget):
self.resolutionManager = resolutionManager
self.resourceHandler = resourceHandler
self.lentDB = entityDatabase
self.setAllowedAreas(QtCore.Qt.LeftDockWidgetArea |
QtCore.Qt.RightDockWidgetArea)
self.setFeatures(QtWidgets.QDockWidget.DockWidgetMovable |
QtWidgets.QDockWidget.DockWidgetFloatable |
QtWidgets.QDockWidget.DockWidgetClosable)
self.setAllowedAreas(QtCore.Qt.DockWidgetArea.LeftDockWidgetArea |
QtCore.Qt.DockWidgetArea.RightDockWidgetArea)
self.setFeatures(QtWidgets.QDockWidget.DockWidgetFeature.DockWidgetMovable |
QtWidgets.QDockWidget.DockWidgetFeature.DockWidgetFloatable |
QtWidgets.QDockWidget.DockWidgetFeature.DockWidgetClosable)
self.setWindowTitle(title)
self.setObjectName(title)
@@ -73,14 +74,13 @@ class EntityList(QtWidgets.QTreeWidget):
def __init__(self, entityDB, mainWindow, parent=None):
super(EntityList, self).__init__(parent=parent)
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
self.entityDB = entityDB
self.mainWindow = mainWindow
self.setDragEnabled(True)
self.setHeaderLabels(['Entity List'])
self.setAlternatingRowColors(False)
self.setMinimumWidth(200)
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.ExtendedSelection)
self.menu = QtWidgets.QMenu()
actionDelete = QtGui.QAction('Delete Selected Items',
@@ -95,8 +95,6 @@ class EntityList(QtWidgets.QTreeWidget):
triggered=self.addItemsToCurrentCanvas)
self.menu.addAction(actionAddToCurrentCanvas)
self.menu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
self.entityCategories: dict = {}
self.entityTypes: dict = {}
self.loadEntities()
@@ -120,7 +118,8 @@ class EntityList(QtWidgets.QTreeWidget):
topItem = self.entityTypes[entity['Entity Type']]
primaryAttr = entity[list(entity)[1]]
pixmapIcon = QtGui.QPixmap()
pixmapIcon.loadFromData(entity['Icon'])
resizedIcon = resizePictureFromBuffer(entity['Icon'], (40, 40))
pixmapIcon.loadFromData(resizedIcon)
EntityWidget(topItem,
entity['uid'],
QtGui.QIcon(pixmapIcon),
@@ -146,7 +145,8 @@ class EntityList(QtWidgets.QTreeWidget):
child.setText(0, primaryAttr)
return
pixmapIcon = QtGui.QPixmap()
pixmapIcon.loadFromData(entityJson['Icon'])
resizedIcon = resizePictureFromBuffer(entityJson['Icon'], (40, 40))
pixmapIcon.loadFromData(resizedIcon)
EntityWidget(entityTypeItem,
entityJson['uid'],
QtGui.QIcon(pixmapIcon),
@@ -178,7 +178,10 @@ class EntityList(QtWidgets.QTreeWidget):
"""
Handle dragging of entities onto canvas.
"""
itemDragged = self.itemAt(event.pos())
if event.buttons() == QtCore.Qt.MouseButton.LeftButton:
itemDragged = self.itemAt(event.pos())
else:
return
# Categories & entity names don't have uids.
try:
@@ -201,7 +204,6 @@ class EntityList(QtWidgets.QTreeWidget):
drag.setPixmap(pixmap)
drag.setHotSpot(QtCore.QPoint(pixmap.rect().width() // 2, pixmap.rect().height() // 2))
drag.exec_()
super().mouseMoveEvent(event)
def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
super(EntityList, self).mousePressEvent(event)
@@ -244,13 +246,12 @@ class DocList(QtWidgets.QTreeWidget):
def __init__(self, resourceHandler, parent=None) -> None:
super(DocList, self).__init__(parent=parent)
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
self.resourceHandler = resourceHandler
self.setAlternatingRowColors(False)
self.setHeaderLabels(['Files Loaded'])
self.uploadingFileWidgets = []
self.uploadedFileWidgets = []
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.ExtendedSelection)
def addUploadingFileToList(self, fileName: str) -> None:
newWidget = DocWidget(self,
@@ -301,7 +302,6 @@ class ResolutionList(QtWidgets.QTreeWidget):
super(ResolutionList, self).__init__(parent=parent)
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
self.resolutionManager = resolutionManager
self.lentDB = entityDatabase
self.mainWindow = mainWindow
@@ -310,7 +310,7 @@ class ResolutionList(QtWidgets.QTreeWidget):
self.setAlternatingRowColors(False)
self.setMinimumWidth(200)
self.setSortingEnabled(True)
self.sortByColumn(0, QtCore.Qt.AscendingOrder)
self.sortByColumn(0, QtCore.Qt.SortOrder.AscendingOrder)
self.loadAllResolutions()
@@ -368,13 +368,12 @@ class NodeList(QtWidgets.QTreeWidget):
def __init__(self, resourceHandler, parent=None) -> None:
super(NodeList, self).__init__(parent=parent)
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
self.resourceHandler = resourceHandler
self.setDragEnabled(True)
self.setHeaderLabels(['Entities'])
self.setAlternatingRowColors(False)
self.setSortingEnabled(True)
self.sortByColumn(0, QtCore.Qt.AscendingOrder)
self.sortByColumn(0, QtCore.Qt.SortOrder.AscendingOrder)
self.allEntities = []
self.loadEntities()
@@ -398,8 +397,7 @@ class NodeList(QtWidgets.QTreeWidget):
"""
Handle dragging of entities onto canvas.
"""
# No, I have no idea why this is the case: v
if event.button() == QtGui.Qt.MouseButton.NoButton:
if event.buttons() == QtCore.Qt.MouseButton.LeftButton:
itemDragged = self.itemAt(event.pos())
if itemDragged is None or \
itemDragged.text(0) not in self.allEntities:
@@ -419,9 +417,6 @@ class NodeList(QtWidgets.QTreeWidget):
drag.setPixmap(pixmap)
drag.setHotSpot(QtCore.QPoint(pixmap.rect().width() // 2, pixmap.rect().height() // 2))
drag.exec_()
else:
# This should never happen.
super(NodeList, self).mousePressEvent(event)
class NodeWidget(QtWidgets.QTreeWidgetItem):

View File

@@ -2,7 +2,6 @@
import contextlib
from PySide6 import QtWidgets, QtCore, QtCharts, QtGui
from Core.Interface import Stylesheets
from datetime import datetime
from getpass import getuser
import networkx as nx
@@ -15,7 +14,6 @@ class DockBarThree(QtWidgets.QDockWidget):
"""
def initialiseLayout(self):
# self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
childWidget = QtWidgets.QWidget()
childWidget.setLayout(QtWidgets.QVBoxLayout())
childWidget.setContentsMargins(0, 0, 0, 0)
@@ -30,20 +28,18 @@ class DockBarThree(QtWidgets.QDockWidget):
self.tabPane.addTab(self.logViewer, 'Program Log')
self.tabPane.addTab(self.timeWidget, 'Timeline')
childWidget2.layout().addWidget(self.chatBox)
self.serverStatus.setStyleSheet(Stylesheets.DOCK_BAR_LABEL)
childWidget.layout().addWidget(self.serverStatus)
def __init__(self, mainWindow, title="Dockbar Three"):
super(DockBarThree, self).__init__(parent=mainWindow)
self.setAllowedAreas(QtCore.Qt.TopDockWidgetArea |
QtCore.Qt.BottomDockWidgetArea)
self.setFeatures(QtWidgets.QDockWidget.DockWidgetMovable |
QtWidgets.QDockWidget.DockWidgetFloatable |
QtWidgets.QDockWidget.DockWidgetClosable)
self.setAllowedAreas(QtCore.Qt.DockWidgetArea.TopDockWidgetArea |
QtCore.Qt.DockWidgetArea.BottomDockWidgetArea)
self.setFeatures(QtWidgets.QDockWidget.DockWidgetFeature.DockWidgetMovable |
QtWidgets.QDockWidget.DockWidgetFeature.DockWidgetFloatable |
QtWidgets.QDockWidget.DockWidgetFeature.DockWidgetClosable)
self.setWindowTitle(title)
self.setObjectName(title)
self.setMaximumHeight(275)
self.setMinimumHeight(275)
self.setMinimumHeight(300)
self.tabPane = QtWidgets.QTabWidget()
@@ -51,7 +47,6 @@ class DockBarThree(QtWidgets.QDockWidget):
self.chatBox = ChatBox(self, self.parent())
self.timeWidget = TimeWidget(self, self.parent())
self.logViewer = QtWidgets.QPlainTextEdit()
self.logViewer.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
# Because we're not going to stop the thread before closing, an error will be thrown by Qt.
# That error can be safely ignored.
@@ -77,11 +72,11 @@ class TimeWidget(QtWidgets.QWidget):
self.timelineChart = QtCharts.QChart()
self.timelineChart.setTitle("Timeline")
self.timelineChart.setTheme(QtCharts.QChart.ChartThemeBlueCerulean)
self.timelineChart.setTheme(QtCharts.QChart.ChartTheme.ChartThemeBlueCerulean)
self.timelineChart.setMargins(QtCore.QMargins(0, 0, 0, 0))
self.chartView = QtCharts.QChartView(self.timelineChart)
self.chartView.setRubberBand(QtCharts.QChartView.NoRubberBand)
self.timelineChart.setAnimationOptions(QtCharts.QChart.AllAnimations)
self.chartView.setRubberBand(QtCharts.QChartView.RubberBand.NoRubberBand)
self.timelineChart.setAnimationOptions(QtCharts.QChart.AnimationOption.AllAnimations)
self.timelineChart.setAnimationDuration(250)
self.timelineChart.legend().hide()
@@ -96,7 +91,7 @@ class TimeWidget(QtWidgets.QWidget):
# Ref: https://qtcentre.org/threads/10975-Help-Export-QGraphicsView-to-Image-File
# Rendering best optimized to rgb32 and argb32_premultiplied.
# Ref: https://doc.qt.io/qtforpython/PySide6/QtGui/QImage.html?highlight=qimage#image-formats
picture = QtGui.QImage(self.chartView.size(), QtGui.QImage.Format_ARGB32_Premultiplied)
picture = QtGui.QImage(self.chartView.size(), QtGui.QImage.Format.Format_ARGB32_Premultiplied)
# Pictures are initialised with junk data - need to clear it out before painting
# to avoid visual artifacts.
picture.fill(QtGui.QColor(0, 0, 0, 0))
@@ -222,7 +217,7 @@ class TimeWidget(QtWidgets.QWidget):
xAxisValues = []
barSet = TimelineBarSet('Entities', self, timestep, list(barsDict))
barSet.setColor(QtGui.Qt.darkCyan)
barSet.setColor(QtGui.Qt.GlobalColor.darkCyan)
for bar in barsDict:
barSet.append(barsDict[bar])
timelineSeries.append(barSet)
@@ -289,35 +284,33 @@ class TimelineTimescaleSelector(QtWidgets.QLabel):
def __init__(self, timeWidget: TimeWidget):
super(TimelineTimescaleSelector, self).__init__(parent=timeWidget)
self.setStyleSheet("""border: 2px solid rgb(44, 49, 58);""")
self.timeWidget = timeWidget
self.setMinimumWidth(150)
self.setMaximumHeight(150)
self.setLayout(QtWidgets.QFormLayout())
self.setFrameStyle(QtWidgets.QFrame.Sunken)
self.setFrameStyle(QtWidgets.QFrame.Shadow.Sunken)
self.yearButton = QtWidgets.QPushButton(' Year: ')
self.yearButton.clicked.connect(self.yearButtonPressed)
self.yearText = QtWidgets.QLabel('-')
self.yearText.setFrameStyle(QtWidgets.QFrame.Sunken)
self.yearText.setFrameStyle(QtWidgets.QFrame.Shadow.Sunken)
self.monthButton = QtWidgets.QPushButton(' Month: ')
self.monthButton.clicked.connect(self.monthButtonPressed)
self.monthText = QtWidgets.QLabel('X')
self.monthText.setFrameStyle(QtWidgets.QFrame.Sunken)
self.monthText.setFrameStyle(QtWidgets.QFrame.Shadow.Sunken)
self.dayButton = QtWidgets.QPushButton(' Day: ')
self.dayButton.clicked.connect(self.dayButtonPressed)
self.dayText = QtWidgets.QLabel('X')
self.dayText.setFrameStyle(QtWidgets.QFrame.Sunken)
self.dayText.setFrameStyle(QtWidgets.QFrame.Shadow.Sunken)
self.hourButton = QtWidgets.QPushButton(' Hour: ')
self.hourButton.clicked.connect(self.hourButtonPressed)
self.hourText = QtWidgets.QLabel('X')
self.hourText.setFrameStyle(QtWidgets.QFrame.Sunken)
self.hourText.setFrameStyle(QtWidgets.QFrame.Shadow.Sunken)
self.minuteButton = QtWidgets.QPushButton(' Minute: ')
self.minuteButton.clicked.connect(self.minuteButtonPressed)
self.minuteText = QtWidgets.QLabel('X')
self.minuteText.setFrameStyle(QtWidgets.QFrame.Sunken)
self.minuteText.setFrameStyle(QtWidgets.QFrame.Shadow.Sunken)
self.layout().addRow(self.yearButton, self.yearText)
self.layout().addRow(self.monthButton, self.monthText)
@@ -462,8 +455,8 @@ class ServerStatusBox(QtWidgets.QLabel):
def __init__(self, parent):
super(ServerStatusBox, self).__init__(parent=parent)
self.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter)
self.setFrameStyle(QtWidgets.QFrame.Sunken | QtWidgets.QFrame.StyledPanel)
self.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.setFrameStyle(QtWidgets.QFrame.Shadow.Sunken | QtWidgets.QFrame.Shape.StyledPanel)
self.setText("Not connected to a server")
def updateStatus(self, status: str):
@@ -491,8 +484,7 @@ class ChatBox(QtWidgets.QWidget):
self.setLayout(chatLayout)
chatLabel = QtWidgets.QLabel('Project Collaboration Chat')
chatLabel.setStyleSheet(Stylesheets.DOCK_BAR_LABEL)
chatLabel.setAlignment(QtCore.Qt.AlignCenter)
chatLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.textView = QtWidgets.QPlainTextEdit()
self.textView.setReadOnly(True)
self.textSendBox = QtWidgets.QLineEdit()

View File

@@ -3,8 +3,8 @@
from pathlib import Path
import magic
from PySide6 import QtWidgets, QtCore, QtGui
from Core.Interface import Stylesheets
from Core.ResourceHandler import MinSizeStackedLayout, RichNotesEditor
from Core.ResourceHandler import MinSizeStackedLayout, RichNotesEditor, resizePictureFromBuffer
from Core.GlobalVariables import hidden_fields_dockbars
class DockBarTwo(QtWidgets.QDockWidget):
@@ -15,9 +15,9 @@ class DockBarTwo(QtWidgets.QDockWidget):
scrollAreaWidget = QtWidgets.QScrollArea()
scrollAreaWidget.setWidget(self.entDetails)
scrollAreaWidget.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
scrollAreaWidget.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAsNeeded)
scrollAreaWidget.setWidgetResizable(True)
scrollAreaWidget.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
scrollAreaWidget.setSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum)
childWidget.addTab(scrollAreaWidget, 'Entity Details')
childWidget.addTab(self.oracle, 'Oracle')
@@ -30,11 +30,11 @@ class DockBarTwo(QtWidgets.QDockWidget):
title="DockBar Two"):
super(DockBarTwo, self).__init__(parent=mainWindow)
self.setAllowedAreas(QtCore.Qt.LeftDockWidgetArea |
QtCore.Qt.RightDockWidgetArea)
self.setFeatures(QtWidgets.QDockWidget.DockWidgetMovable |
QtWidgets.QDockWidget.DockWidgetFloatable |
QtWidgets.QDockWidget.DockWidgetClosable)
self.setAllowedAreas(QtCore.Qt.DockWidgetArea.LeftDockWidgetArea |
QtCore.Qt.DockWidgetArea.RightDockWidgetArea)
self.setFeatures(QtWidgets.QDockWidget.DockWidgetFeature.DockWidgetMovable |
QtWidgets.QDockWidget.DockWidgetFeature.DockWidgetFloatable |
QtWidgets.QDockWidget.DockWidgetFeature.DockWidgetClosable)
self.setWindowTitle(title)
self.resourceHandler = resourceHandler
self.entityDB = entityDB
@@ -98,7 +98,7 @@ class EntityDetails(QtWidgets.QWidget):
self.entityDB = entityDB
self.detailsLayout = MinSizeStackedLayout()
self.setLayout(self.detailsLayout)
self.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
self.setSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum)
layoutNothingSelected = QtWidgets.QVBoxLayout()
widgetNothing = QtWidgets.QWidget()
@@ -118,7 +118,7 @@ class EntityDetails(QtWidgets.QWidget):
# Need to keep track of how many nodes are selected.
# ~ Nothing Selected/Hovered Layout
nothingLabel = QtWidgets.QLabel("Nothing is Selected.")
nothingLabel.setAlignment(QtCore.Qt.AlignCenter)
nothingLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
layoutNothingSelected.addWidget(nothingLabel)
###
@@ -128,10 +128,12 @@ class EntityDetails(QtWidgets.QWidget):
summaryPanel = QtWidgets.QWidget()
summaryPanel.setLayout(summaryLayout)
self.summaryIcon = QtWidgets.QLabel("")
self.summaryIcon.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
self.summaryIcon.setSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum)
self.entityTypeLabel = QtWidgets.QLabel("")
self.entityUIDLabel = QtWidgets.QLabel("")
self.entityPrimaryLabel = QtWidgets.QLabel("")
self.entityUIDLabel = QtWidgets.QLineEdit("")
self.entityUIDLabel.setReadOnly(True)
self.entityPrimaryLabel = QtWidgets.QLineEdit("")
self.entityPrimaryLabel.setReadOnly(True)
summaryLayout.addWidget(self.summaryIcon, 0, 0)
summaryLayout.addWidget(self.entityTypeLabel, 0, 1, 1, 3)
summaryLayout.addWidget(self.entityPrimaryLabel, 1, 1)
@@ -164,13 +166,10 @@ class EntityDetails(QtWidgets.QWidget):
oneLinkRelPanel.setMaximumHeight(150)
oneLinkRelPanel.setLayout(oneLinkRelLayout)
self.linkParent = SingleLinkItem(self, mainWindow)
self.linkParent.setStyleSheet(Stylesheets.DOCK_BAR_TWO_LINK)
self.linkIcon = QtWidgets.QLabel("")
self.linkIcon.setMaximumHeight(90)
self.linkIcon.setStyleSheet(Stylesheets.DOCK_BAR_TWO_LINK)
self.linkIcon.setAlignment(QtCore.Qt.AlignCenter)
self.linkIcon.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.linkChild = SingleLinkItem(self, mainWindow)
self.linkChild.setStyleSheet(Stylesheets.DOCK_BAR_TWO_LINK)
oneLinkRelLayout.addWidget(self.linkParent)
oneLinkRelLayout.addWidget(self.linkIcon)
oneLinkRelLayout.addWidget(self.linkChild)
@@ -276,7 +275,7 @@ class EntityDetails(QtWidgets.QWidget):
return
rowCount = 0
for key in jsonDict:
if key in ["uid", "Child UIDs", "Icon"]:
if key in hidden_fields_dockbars:
continue
elif key == "Notes":
notesTextArea = RichNotesEditor(self, jsonDict[key], False)
@@ -284,13 +283,16 @@ class EntityDetails(QtWidgets.QWidget):
self.detailsLayoutOneNode.addWidget(notesTextArea, rowCount, 1, 10, 1)
rowCount += 9
else:
valueLabel = QtWidgets.QLineEdit(str(jsonDict[key]))
valueLabel.setReadOnly(True)
self.detailsLayoutOneNode.addWidget(QtWidgets.QLabel(key), rowCount, 0)
self.detailsLayoutOneNode.addWidget(QtWidgets.QLabel(str(jsonDict[key])), rowCount, 1)
self.detailsLayoutOneNode.addWidget(valueLabel, rowCount, 1)
rowCount += 1
filePath = jsonDict.get('File Path')
if filePath is not None:
fullFilePath = Path(self.mainWindow.SETTINGS.value('Project/FilesDir')) / filePath
if fullFilePath.exists() and fullFilePath.is_file():
if fullFilePath.is_file():
magicType = magic.from_file(str(fullFilePath), mime=True)
if magicType.split('/')[0] == 'image':
previewImage = QtGui.QImage(fullFilePath)
@@ -304,7 +306,9 @@ class EntityDetails(QtWidgets.QWidget):
else:
previewPixmap = QtGui.QPixmap(previewImage)
previewLabel = QtWidgets.QLabel()
previewLabel.setPixmap(previewPixmap.scaled(250, 250, QtCore.Qt.KeepAspectRatio))
previewLabel.setPixmap(previewPixmap.scaled(250,
250,
QtCore.Qt.AspectRatioMode.KeepAspectRatio))
self.detailsLayoutOneNode.addWidget(QtWidgets.QLabel('Preview:'), rowCount, 0)
self.detailsLayoutOneNode.addWidget(previewLabel, rowCount, 1, 10, 1)
@@ -314,9 +318,9 @@ class EntityDetails(QtWidgets.QWidget):
second = uid[1]
secondJson = self.entityDB.getEntity(second)
firstPixmap = QtGui.QPixmap()
firstPixmap.loadFromData(firstJson.get('Icon'))
firstPixmap.loadFromData(resizePictureFromBuffer(firstJson.get('Icon'), (40, 40)))
secondPixmap = QtGui.QPixmap()
secondPixmap.loadFromData(secondJson.get('Icon'))
secondPixmap.loadFromData(resizePictureFromBuffer(secondJson.get('Icon'), (40, 40)))
self.linkParent.linkItemPic.setPixmap(firstPixmap)
self.linkParent.linkItemName.setText(firstJson[list(firstJson)[1]])
self.linkParent.linkItemUid = firstJson['uid']
@@ -331,7 +335,7 @@ class EntityDetails(QtWidgets.QWidget):
uid = edge[0]
edgeJson = self.entityDB.getEntity(uid)
nodePixmap = QtGui.QPixmap()
nodePixmap.loadFromData(edgeJson.get('Icon'))
nodePixmap.loadFromData(resizePictureFromBuffer(edgeJson.get('Icon'), (40, 40)))
ResolutionTreeWidgetEntity(self.relationshipsIncomingTable,
nodePixmap,
edgeJson[list(edgeJson)[1]],
@@ -341,7 +345,7 @@ class EntityDetails(QtWidgets.QWidget):
uid = edge[1]
edgeJson = self.entityDB.getEntity(uid)
nodePixmap = QtGui.QPixmap()
nodePixmap.loadFromData(edgeJson.get('Icon'))
nodePixmap.loadFromData(resizePictureFromBuffer(edgeJson.get('Icon'), (40, 40)))
ResolutionTreeWidgetEntity(self.relationshipsOutgoingTable,
nodePixmap,
edgeJson[list(edgeJson)[1]],
@@ -352,7 +356,7 @@ class EntityDetails(QtWidgets.QWidget):
inc = len(self.entityDB.getIncomingLinks(nodeJson['uid']))
out = len(self.entityDB.getOutgoingLinks(nodeJson['uid']))
nodePixmap = QtGui.QPixmap()
nodePixmap.loadFromData(nodeJson.get('Icon'))
nodePixmap.loadFromData(resizePictureFromBuffer(nodeJson.get('Icon'), (40, 40)))
ResolutionTreeWidgetEntity(self.nodesTable,
nodePixmap,
nodeJson[list(nodeJson)[1]],
@@ -377,7 +381,7 @@ class EntityDetails(QtWidgets.QWidget):
self.entityPrimaryLabel.setText(jsonDict[list(jsonDict)[0]])
self.entityTypeLabel.setText(jsonDict['Entity Type'])
summaryPixmap = QtGui.QPixmap()
summaryPixmap.loadFromData(jsonDict.get('Icon'))
summaryPixmap.loadFromData(resizePictureFromBuffer(jsonDict.get('Icon'), (40, 40)))
self.summaryIcon.setPixmap(summaryPixmap)
else:
self.entityUIDLabel.setText("--")
@@ -415,10 +419,11 @@ class SingleLinkItem(QtWidgets.QWidget):
self.linkItemPic = QtWidgets.QLabel()
self.linkItemPic.setAlignment(QtCore.Qt.AlignCenter)
self.linkItemName = QtWidgets.QLabel()
self.linkItemPic.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.linkItemName = QtWidgets.QLineEdit()
self.linkItemName.setReadOnly(True)
self.linkItemName.setAlignment(QtCore.Qt.AlignCenter)
self.linkItemName.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.linkItemUid = ""
self.setMaximumHeight(90)
@@ -441,7 +446,6 @@ class RelationshipsTable(QtWidgets.QTreeWidget):
def __init__(self, parent, mainWindow, uidLabel: QtWidgets.QLabel = None, incomingOrOutgoing: int = None):
super().__init__(parent=parent)
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
self.mainWindow = mainWindow
self.incomingOrOutgoing = incomingOrOutgoing
self.uidLabel = uidLabel
@@ -474,7 +478,6 @@ class LinksTable(QtWidgets.QTreeWidget):
def __init__(self, parent, mainWindow):
super().__init__(parent=parent)
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
self.mainWindow = mainWindow
def mousePressEvent(self, event):
@@ -542,9 +545,7 @@ class Oracle(QtWidgets.QWidget):
self.setLayout(oracleLayout)
self.answerLabel = QtWidgets.QLabel("Answer Section")
self.answerLabel.setStyleSheet(Stylesheets.DOCK_BAR_LABEL)
self.answerLabel.setAlignment(QtCore.Qt.AlignHCenter |
QtCore.Qt.AlignVCenter)
self.answerLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
oracleLayout.addWidget(self.answerLabel, 1, 0, 1, 2)
self.answerSection = QtWidgets.QPlainTextEdit()
self.answerSection.setReadOnly(True)
@@ -554,9 +555,7 @@ class Oracle(QtWidgets.QWidget):
oracleLayout.addWidget(self.answerSection, 2, 0, 1, 2)
self.questionLabel = QtWidgets.QLabel("Ask a Question")
self.questionLabel.setStyleSheet(Stylesheets.DOCK_BAR_LABEL)
self.questionLabel.setAlignment(QtCore.Qt.AlignHCenter |
QtCore.Qt.AlignVCenter)
self.questionLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
oracleLayout.addWidget(self.questionLabel, 3, 0, 1, 2)
self.questionSection = QtWidgets.QLineEdit()
self.questionSection.setPlaceholderText("Ask a Question here.")

View File

@@ -10,6 +10,8 @@ from PySide6.QtWidgets import QGraphicsItem
from PySide6.QtWidgets import QGraphicsItemGroup, QGraphicsSimpleTextItem, QGraphicsPixmapItem, QGraphicsTextItem
from PySide6.QtSvgWidgets import QGraphicsSvgItem
from Core.ResourceHandler import resizePictureFromBuffer
ENTITY_TEXT_FONT = QtGui.QFont("Mono", 11, 700)
LINK_TEXT_FONT = QtGui.QFont("Mono", 11, 700)
@@ -20,32 +22,36 @@ class BaseNode(QGraphicsItemGroup):
brush: QtGui.QBrush) -> None:
super(BaseNode, self).__init__()
self.setCacheMode(self.DeviceCoordinateCache)
self.setCacheMode(QGraphicsItemGroup.CacheMode.DeviceCoordinateCache)
resizedByteArray = resizePictureFromBuffer(pictureByteArray, (40, 40))
self.pixmapItem = QtGui.QPixmap()
self.pixmapItem.loadFromData(pictureByteArray)
if pictureByteArray.data().startswith(b'<svg '):
if pictureByteArray.data().startswith(b'<svg ') or pictureByteArray.data().startswith(b'<?xml'):
self.iconItem = QGraphicsSvgItem()
self.iconItem.renderer().load(pictureByteArray)
self.iconItem.renderer().load(resizedByteArray)
# Force recalculation of geometry, else this looks like 1 pixel.
# https://stackoverflow.com/a/68182093
self.iconItem.setElementId("")
else:
self.iconItem = QGraphicsPixmapItem(self.pixmapItem)
pixmapItem = QtGui.QPixmap()
pixmapItem.loadFromData(resizedByteArray)
self.iconItem = QGraphicsPixmapItem(pixmapItem)
self.labelItem = QGraphicsTextItem('')
# Have to do it this way; directly assigning stuff does not work due to how PySide6 works.
labelDocument = self.labelItem.document()
labelDocument.setTextWidth(280)
textOption = labelDocument.defaultTextOption()
textOption.setWrapMode(QtGui.QTextOption.WrapAtWordBoundaryOrAnywhere)
textOption.setAlignment(QtCore.Qt.AlignHCenter)
textOption.setWrapMode(QtGui.QTextOption.WrapMode.WrapAtWordBoundaryOrAnywhere)
textOption.setAlignment(QtCore.Qt.AlignmentFlag.AlignHCenter)
labelDocument.setDefaultTextOption(textOption)
self.labelItem.setDocument(labelDocument)
self.bannerIconItem = QGraphicsSvgItem()
self.bannerIconItem.setElementId("")
self.addToGroup(self.iconItem)
self.addToGroup(self.labelItem)
self.addToGroup(self.bannerIconItem)
if font is not None:
self.labelItem.setFont(font)
else:
@@ -53,9 +59,11 @@ class BaseNode(QGraphicsItemGroup):
if brush is not None:
self.labelItem.setDefaultTextColor(brush.color())
self.labelItem.setPos(self.iconItem.x() - 120, self.iconItem.y() + 45)
self.updateLabel(primaryAttribute)
self.bannerIconItem.setPos(self.iconItem.x() + 15, self.iconItem.y() - 9)
self.bannerIconItem.setZValue(10)
self.uid = uid
self.setFlag(QGraphicsItem.ItemIsMovable, True)
self.setFlag(QGraphicsItem.ItemIsSelectable, True)
@@ -64,17 +72,24 @@ class BaseNode(QGraphicsItemGroup):
self.setAcceptHoverEvents(True)
self.connectors = []
self.bookmarked = False
self.isBeingResolved = False
self.parentGroup = None
def updateLabel(self, newText: str = '') -> None:
if not isinstance(newText, str):
newText = newText
if newText != '':
if len(newText) > 50:
newText = f"{newText[:47]}..."
self.labelItem.setPlainText(newText)
self.labelItem.document().adjustSize()
self.labelItem.setPos(self.iconItem.x() + 20 - (self.labelItem.textWidth() / 2), self.iconItem.y() + 45)
def updateBanner(self, bannerHidden: bool = True, bannerGraphic: QtCore.QByteArray = None) -> None:
if bannerHidden: # No icon visible
self.bannerIconItem.hide()
self.bannerIconItem.setVisible(False)
return
self.bannerIconItem.renderer().load(bannerGraphic)
self.bannerIconItem.show()
self.bannerIconItem.setVisible(True)
self.bannerIconItem.setElementId("")
def removeConnector(self, connector) -> None:
# Exception could be thrown if the connector is already deleted.
@@ -117,9 +132,14 @@ class BaseNode(QGraphicsItemGroup):
def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionGraphicsItem,
widget: Optional[QtWidgets.QWidget] = ...) -> None:
painter.setPen(QtCore.Qt.NoPen)
painter.setPen(QtCore.Qt.PenStyle.NoPen)
if self.scene().views()[0].zoom < self.scene().hideZoom:
self.labelItem.hide()
# Looks stupid, but fixes bug where entities are deselected when zooming out past hideZoom level.
if self.isSelected():
self.labelItem.hide()
self.setSelected(True)
else:
self.labelItem.hide()
else:
self.labelItem.show()
if self.isSelected():
@@ -145,7 +165,7 @@ class GroupNode(BaseNode):
self.listProxyWidget = None
def itemChange(self, change: QtWidgets.QGraphicsItem.GraphicsItemChange, value: Any) -> Any:
if change == QtWidgets.QGraphicsItem.ItemSelectedChange:
if change == QtWidgets.QGraphicsItem.GraphicsItemChange.ItemSelectedChange:
if value:
self.showList(None)
else:
@@ -163,7 +183,7 @@ class GroupNode(BaseNode):
except IndexError:
primaryField = ''
iconPixmap = QtGui.QPixmap()
iconPixmap.loadFromData(entityJson['Icon'])
iconPixmap.loadFromData(resizePictureFromBuffer(entityJson['Icon'], (40, 40)))
GroupNodeListItem(icon=iconPixmap, text=primaryField, uid=uid,
listview=self.listWidget.itemList)
@@ -185,7 +205,8 @@ class GroupNode(BaseNode):
def formGroup(self, childNodeUIDs, listProxyWidget: QtWidgets.QGraphicsProxyWidget) -> None:
[self.addItemToGroup(uid) for uid in childNodeUIDs] # Should be faster than just a for loop
self.listProxyWidget = listProxyWidget
self.listProxyWidget.setCacheMode(self.DeviceCoordinateCache)
self.listProxyWidget.setCacheMode(QGraphicsItemGroup.CacheMode.DeviceCoordinateCache)
self.listProxyWidget.setZValue(100)
def addItemToGroup(self, uid: str) -> None:
self.groupedNodesUid.add(uid)
@@ -250,8 +271,8 @@ class BaseConnector(QGraphicsItemGroup):
self.colorDefault = QtGui.QColor(200, 200, 200)
self.myColor = self.colorDefault
self.pen = QtGui.QPen(self.myColor, 2, QtCore.Qt.SolidLine,
QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
self.pen = QtGui.QPen(self.myColor, 2, QtCore.Qt.PenStyle.SolidLine,
QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin)
self.arrowHead = QtGui.QPolygonF()
self.line = QtCore.QLineF()
@@ -281,10 +302,6 @@ class BaseConnector(QGraphicsItemGroup):
def endItem(self) -> BaseNode:
return self.myEndItem
def updatePosition(self) -> None:
self.line = QtCore.QLineF(self.mapFromItem(self.myStartItem, 0, 0), self.mapFromItem(self.myEndItem, 0, 0))
self.update()
def boundingRect(self) -> QtCore.QRectF:
extra = self.pen.width() + 20
p1 = self.line.p1()
@@ -301,8 +318,8 @@ class BaseConnector(QGraphicsItemGroup):
def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionGraphicsItem,
widget: Optional[QtWidgets.QWidget] = ...) -> None:
currentStartPos = self.myStartItem.pos()
currentEndPos = self.myEndItem.pos()
currentStartPos = self.myStartItem.pos() - self.pos()
currentEndPos = self.myEndItem.pos() - self.pos()
self.myColor = self.colorSelected if self.isSelected() else self.colorDefault
@@ -312,14 +329,22 @@ class BaseConnector(QGraphicsItemGroup):
line = QtCore.QLineF(p1, p2)
if line.length() < 45:
self.labelItem.hide()
if self.isSelected():
self.labelItem.hide()
self.setSelected(True)
else:
self.labelItem.hide()
return
angle = math.atan2(line.dy(), - line.dx())
if (line.length() < 50 + len(self.labelItem.text()) * 15) or \
self.scene().views()[0].zoom < self.scene().hideZoom:
self.labelItem.hide()
if self.isSelected():
self.labelItem.hide()
self.setSelected(True)
else:
self.labelItem.hide()
else:
self.labelItem.show()
angle2 = math.degrees(math.pi - angle)
@@ -371,7 +396,7 @@ class GroupNodeChildList(QtWidgets.QWidget):
self.setLayout(QtWidgets.QVBoxLayout())
titleLabel = QtWidgets.QLabel('Child Items')
titleLabel.setAlignment(QtCore.Qt.AlignVCenter | QtCore.Qt.AlignHCenter)
titleLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.layout().addWidget(titleLabel)
self.itemList = ChildListWidget()
@@ -386,7 +411,11 @@ class ChildListWidget(QtWidgets.QListWidget):
self.setSortingEnabled(True)
def mouseMoveEvent(self, event: QtGui.QMouseEvent) -> None:
itemDragged = self.itemAt(event.pos())
super().mouseMoveEvent(event)
itemDragged = None
if event.buttons() == QtCore.Qt.MouseButton.LeftButton:
itemDragged = self.itemAt(event.pos())
if itemDragged is None:
return
@@ -405,5 +434,3 @@ class ChildListWidget(QtWidgets.QListWidget):
drag.setPixmap(pixmap)
drag.setHotSpot(QtCore.QPoint(pixmap.rect().width() / 2, pixmap.rect().height() / 2))
drag.exec_()
super().mousePressEvent(event)

File diff suppressed because it is too large Load Diff

View File

@@ -1,273 +0,0 @@
#!/usr/bin/env python3
TOOLBAR_STYLESHEET = """QToolBar {background-color: rgb(41, 45, 62);
color: rgba(248, 248, 242, 1) !important;
font-family: Segoe UI;
font-size: 13px;
text-align: left;}
QToolBar::separator {
background-color: rgb(0, 173, 238);
width: 3px;
height: 3px;
}
"""
MAIN_WINDOW_STYLESHEET = """
QWidget{
background-color: rgb(41, 45, 62);
color: rgba(248, 248, 242, 1) !important;
font-size: 15px;
}
QScrollBar:vertical {
background:rgb(44, 49, 58);
width:7px;
margin: 0px 0px 0px 0px;
}
QScrollBar::handle:vertical {
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop: 0 rgb(103, 110, 149), stop: 0.5 rgb(103, 110, 149), stop:1 rgb(103, 110, 149));
min-height: 0px;
}
QScrollBar:horizontal {
background:rgb(44, 49, 58);
height:7px;
margin: 0px 0px 0px 0px;
}
QScrollBar::handle:horizontal {
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop: 0 rgb(103, 110, 149),
stop: 0.5 rgb(103, 110, 149),
stop:1 rgb(103, 110, 149));
}
QMenuBar {
color: #ffffff;
background-color: rgb(33, 37, 43);
font-size: 15px;
}
QPushButton {
border: 2px solid rgb(0, 173, 238);
border-radius: 6px;
background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 1 rgb(0, 173, 238), stop: 0.7 rgb(44, 49, 58));
min-width: 50px;
}
QLabel {
border: 2px solid rgb(41, 45, 62);
padding-left: 7px;
border-left-color: rgb(0, 173, 238);
}
QLineEdit, QPlainTextEdit {
border: 0.5px solid rgb(0, 173, 238);
border-radius: 6px;
}
QTabBar::tab {
background: rgb(68, 66, 103);
border: 2px solid rgb(41, 45, 62);
border-radius: 3px;
min-height: 3ex;
}
QTabWidget {
border-style: outset;
border-color: rgba(248, 248, 242, 1);
border-width: 0.5px;
}
QToolBar {
border-style: outset;
border-color: rgba(75, 75, 75, 1);
border-width: 1px;
border-left-width: 0px;
border-right-width: 0px;
}
QTabBar::tab:selected {
background: rgb(51, 55, 95);
border: 2px solid rgb(41, 45, 62);
min-height: 2.5ex;
border-radius: 3px;
border-top-color: rgb(0, 173, 238);
}
QComboBox { combobox-popup: 0; }
QHeaderView::section {
background-color: rgb(41, 45, 62);
color: rgba(248, 248, 242, 1);
}
QMenu::item {
background-color: rgb(41, 45, 62);
color: rgba(248, 248, 242, 1) !important;
border-left: 1px solid rgb(0, 173, 238);
padding-right: 4px;
padding-bottom: 4px;
padding-top: 4px;
font-size: 15px;
text-align: left;
padding-left: 4px;
}
QMenu::item:selected {
background-color: rgb(0, 85, 127);
color: rgb(255, 255, 255);
}
QMenu::item:disabled {
background-color:rgb(81, 87, 114);
}
QTextBrowser {
background-color:rgb(60, 60, 80);
}
"""
DOCK_BAR_TWO_LINK = """
QLabel {
border: 1px solid rgb(41, 45, 62);
}
"""
DOCK_BAR_LABEL = """
QLabel {
border: 1px solid rgb(41, 45, 62);
border-radius: 2px;
border-bottom-color: rgb(0, 173, 238);
background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 1 rgb(0, 173, 238), stop: 0.85 rgb(41, 45, 62));
}
"""
TEXT_BOX_STYLESHEET = "QLineEdit, QLabel{" \
"border-bottom: 1px solid rgb(0, 173, 238);" \
"}"
CHECK_BOX_STYLESHEET = "QCheckBox::indicator:unchecked" \
"{" \
"border: 0.5px solid rgb(0, 173, 238);" \
"background: none;" \
"}"
RADIO_BUTTON_STYLESHEET = "QRadioButton::indicator:unchecked" \
"{" \
"border: 0.5px solid rgb(0, 173, 238);" \
"background: none;" \
"border-radius: 7px;" \
"}"
SETTINGS_WIDGET_STYLESHEET = "#settingsWidget {background-color:rgb(41, 45, 62);}"
MENUS_STYLESHEET = "background-color: rgb(41, 45, 62);" \
"color: rgba(248, 248, 242, 1) !important;" \
"border: 1px solid rgb(44, 49, 58);" \
"border-bottom: 1px solid rgb(0, 173, 238);" \
"font-family: Segoe UI;" \
"font-size: 13px;" \
"text-align: left;"
MENUS_STYLESHEET_2 = """QMenu::item{
background-color: rgb(41, 45, 62);
color: rgba(248, 248, 242, 1) !important;
border-left: 1px solid rgb(0, 173, 238);
padding-right: 4px;
padding-bottom: 4px;
padding-top: 4px;
font-size: 15px;
text-align: left;
padding-left: 4px;
}
QMenu::item:selected{
background-color: rgb(0, 85, 127);
color: rgb(255, 255, 255);
}
QMenu::item:disabled {
background-color:rgb(81, 87, 114);
}"""
MERGE_STYLESHEET = """
QWidget, QDialog{
background-color: rgb(41, 45, 62);
color: rgba(248, 248, 242, 1) !important;
font-family: Segoe UI;
font-size: 13px;}
QScrollBar:vertical {
background:rgb(44, 49, 58);
width:7px;
margin: 0px 0px 0px 0px;
}
QScrollBar::handle:vertical {
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop: 0 rgb(103, 110, 149), stop: 0.5 rgb(103, 110, 149), stop:1 rgb(103, 110, 149));
min-height: 0px;
}
QScrollBar:horizontal {
background:rgb(44, 49, 58);
height:7px;
margin: 0px 0px 0px 0px;
}
QScrollBar::handle:horizontal {
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop: 0 rgb(103, 110, 149), stop: 0.5 rgb(103, 110, 149), stop:1 rgb(103, 110, 149));
}"""
RESOLUTION_WIZARD_STYLESHEET = "background-color: rgb(41, 45, 62);" \
"color: rgba(248, 248, 242, 1) !important;" \
"padding-bottom: 5px;" \
"font-family: Segoe UI;" \
"font-size: 13px;"
PATH_INPUT_STYLESHEET = """border: 2px solid rgb(44, 49, 58);
border-radius: 25px;
padding: 4px;
background-color: rgb(129, 133, 137);
color: rgba(248, 248, 242, 1) !important;
"""
BUTTON_STYLESHEET = """
QPushButton {
border: 2px solid rgb(0, 173, 238);
border-radius: 6px;
color: rgba(248, 248, 242, 1) !important;
background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 1 rgb(0, 173, 238), stop: 0.7 rgb(44, 49, 58));
min-width: 80px;
}
QPushButton:pressed {
background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #dadbde, stop: 1 #f6f7fa);
}
"""
BUTTON_STYLESHEET_2 = """
QPushButton {
border: 2px solid rgb(0, 173, 238);
border-radius: 6px;
color: rgba(248, 248, 242, 1) !important;
background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 1 rgb(0, 173, 238), stop: 0.7 rgb(44, 49, 58));
min-width: 300px;
}
QPushButton:pressed {
background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #dadbde, stop: 1 #f6f7fa);
}
"""
SELECT_PROJECT_STYLESHEET = "background-color: rgb(41, 45, 62);" \
"color: rgba(248, 248, 242, 1) !important;" \
"border-color: rgb(0, 173, 238);"

View File

@@ -1,7 +1,6 @@
#!/usr/bin/env python3
from PySide6 import QtWidgets, QtGui
from Core.Interface import Stylesheets
class ToolBarOne(QtWidgets.QToolBar):
@@ -10,8 +9,7 @@ class ToolBarOne(QtWidgets.QToolBar):
# Parent is (expected to be) mainWindow.
super().__init__(title, parent=parent)
self.setObjectName(title)
self.setToolButtonStyle(QtGui.Qt.ToolButtonTextUnderIcon)
self.setStyleSheet(Stylesheets.TOOLBAR_STYLESHEET)
self.setToolButtonStyle(QtGui.Qt.ToolButtonStyle.ToolButtonTextUnderIcon)
newCanvas = QtGui.QAction("Add Canvas",
self,

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,6 @@ from logging import handlers
from multiprocessing import Queue
from pathlib import Path
from PySide6 import QtWidgets
from Core.Interface import Stylesheets
class MessageHandler:
@@ -26,7 +25,6 @@ class MessageHandler:
self.linkScopeLogger.info(message, exc_info=exc_info)
if popUp:
msgBox = QtWidgets.QMessageBox()
msgBox.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
QtWidgets.QMessageBox.information(msgBox,
self.mainWindow.tr("Info"),
self.mainWindow.tr(message))
@@ -36,7 +34,6 @@ class MessageHandler:
self.linkScopeLogger.warning(message, exc_info=exc_info)
if popUp:
msgBox = QtWidgets.QMessageBox()
msgBox.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
QtWidgets.QMessageBox.warning(msgBox,
self.mainWindow.tr("Warning"),
self.mainWindow.tr(message))
@@ -46,7 +43,6 @@ class MessageHandler:
self.linkScopeLogger.error(message, exc_info=exc_info)
if popUp:
msgBox = QtWidgets.QMessageBox()
msgBox.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
QtWidgets.QMessageBox.critical(msgBox,
self.mainWindow.tr("Error"),
self.mainWindow.tr(message))
@@ -56,7 +52,6 @@ class MessageHandler:
self.linkScopeLogger.critical(message, exc_info=exc_info)
if popUp:
msgBox = QtWidgets.QMessageBox()
msgBox.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
QtWidgets.QMessageBox.critical(msgBox,
self.mainWindow.tr("Critical"),
self.mainWindow.tr(message))

1000
Core/ModuleManager.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,10 @@
#!/usr/bin/env python3
import tempfile
import re
from shutil import rmtree
from svglib.svglib import svg2rlg
from uuid import uuid4
from datetime import datetime
from pathlib import Path
from reportlab.lib import colors
@@ -15,10 +20,369 @@ from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_JUSTIFY
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.pagesizes import LETTER, inch
from reportlab.graphics.shapes import Line, Drawing
from PySide6 import QtWidgets
from Core.Interface.Entity import BaseNode
from Core.GlobalVariables import avoid_parsing_fields
class ReportWizard(QtWidgets.QWizard):
def __init__(self, parent):
super(ReportWizard, self).__init__(parent=parent)
self.reportTempFolder = tempfile.mkdtemp()
self.primaryFieldsList = []
self.addPage(InitialConfigPage(self))
self.addPage(TitlePage(self))
self.addPage(SummaryPage(self))
self.selectedNodes = [entity for entity in
self.parent().centralWidget().tabbedPane.getCurrentScene().selectedItems()
if isinstance(entity, BaseNode)]
for selectedNode in self.selectedNodes:
# used in wizard
self.primaryField = selectedNode.labelItem.toPlainText()
self.uid = selectedNode.uid
# used in report generation
self.primaryFieldsList.append(self.primaryField)
self.addPage(EntityPage(self))
self.setWizardStyle(QtWidgets.QWizard.WizardStyle.ModernStyle)
self.setWindowTitle("Generate Report Wizard")
self.button(QtWidgets.QWizard.WizardButton.FinishButton).clicked.connect(self.onFinish)
def onFinish(self):
outgoingEntitiesForEachEntity = []
incomingEntitiesForEachEntity = []
outgoingEntityPrimaryFieldsForEachEntity = []
incomingEntityPrimaryFieldsForEachEntity = []
entityList = []
reportData = []
for pageID in self.pageIds():
pageObject = self.page(pageID)
reportData.append(pageObject.getData())
for selectedNode in self.selectedNodes:
uid = selectedNode.uid
entityList.append(self.parent().LENTDB.getEntity(uid))
outgoing = self.parent().LENTDB.getOutgoingLinks(uid)
incoming = self.parent().LENTDB.getIncomingLinks(uid)
outgoingEntities = []
incomingEntities = []
outgoingNames = []
incomingNames = []
for out in outgoing:
outLink = self.parent().LENTDB.getLink(out)
outgoingEntities.append(outLink)
outgoingEntityJson = self.parent().LENTDB.getEntity(outLink['uid'][1])
outgoingNames.append(outgoingEntityJson[list(outgoingEntityJson)[1]])
for inc in incoming:
inLink = self.parent().LENTDB.getLink(inc)
incomingEntities.append(inLink)
incomingEntityJson = self.parent().LENTDB.getEntity(inLink['uid'][0])
incomingNames.append(incomingEntityJson[list(incomingEntityJson)[1]])
outgoingEntityPrimaryFieldsForEachEntity.append(outgoingNames)
incomingEntityPrimaryFieldsForEachEntity.append(incomingNames)
outgoingEntitiesForEachEntity.append(outgoingEntities)
incomingEntitiesForEachEntity.append(incomingEntities)
path = Path(reportData[0].get('SavePath'))
canvasName = reportData[2].get('CanvasName')
viewPortBool = reportData[2].get('ViewPort')
canvasPicture = self.parent().getPictureOfCanvas(canvasName, viewPortBool, True)
canvasImagePath = Path(self.reportTempFolder) / 'canvas.png'
canvasPicture.save(str(canvasImagePath), "PNG")
# timelinePicture = self.parent().dockbarThree.timeWidget.takePictureOfView(False)
# timelineImagePath = Path(temp_dir.name) / 'timeline.png'
# timelinePicture.save(str(timelineImagePath), "PNG")
savePath = Path(reportData[0]['SavePath']).absolute()
try:
PDFReport(str(path), reportData, outgoingEntitiesForEachEntity,
incomingEntitiesForEachEntity, entityList, canvasImagePath, None, # <timelinePic
self.primaryFieldsList, incomingEntityPrimaryFieldsForEachEntity,
outgoingEntityPrimaryFieldsForEachEntity)
self.parent().MESSAGEHANDLER.debug(reportData)
self.parent().MESSAGEHANDLER.info(
f"Saved Report at: {str(savePath)}", popUp=True
)
except PermissionError:
self.parent().MESSAGEHANDLER.error(
f"Could not generate report. No permission to save at the chosen location: {str(savePath)}",
popUp=True,
exc_info=False,
)
except Exception as exc:
self.parent().MESSAGEHANDLER.error(
f"Could not generate report: {str(exc)}", popUp=True, exc_info=True
)
finally:
rmtree(self.reportTempFolder)
class InitialConfigPage(QtWidgets.QWizardPage):
def __init__(self, parent=None):
super(InitialConfigPage, self).__init__(parent)
self.subtitleLabel = QtWidgets.QLabel("Path to save the report at: ")
self.savePathEdit = QtWidgets.QLineEdit()
self.setTitle("Initial Configuration Wizard")
pDirButton = QtWidgets.QPushButton("Save Report As...")
pDirButton.clicked.connect(self.editPath)
hLayout = QtWidgets.QVBoxLayout()
hLayout.addWidget(self.subtitleLabel)
hLayout.addWidget(self.savePathEdit)
hLayout.addWidget(pDirButton)
layout = QtWidgets.QVBoxLayout()
layout.addLayout(hLayout)
self.setLayout(layout)
def editPath(self):
selectedPath = QtWidgets.QFileDialog.getSaveFileName(self,
"File Name to Save As",
str(Path.home()),
filter="PDF Files (*.pdf)",
options=QtWidgets.QFileDialog.Option.DontUseNativeDialog)
selectedPath = selectedPath[0]
if selectedPath != '':
savePath = Path(selectedPath).absolute()
if savePath.suffix != '.pdf':
savePath = savePath.with_suffix(f"{savePath.suffix}.pdf")
self.savePathEdit.setText(str(savePath))
def getData(self):
return {'SavePath': self.savePathEdit.text()}
class TitlePage(QtWidgets.QWizardPage):
def __init__(self, parent=None):
super(TitlePage, self).__init__(parent)
self.inputTitleEdit = QtWidgets.QLineEdit()
self.inputSubtitleEdit = QtWidgets.QLineEdit()
self.inputAuthorsEdit = QtWidgets.QLineEdit()
self.setTitle("Title Page Wizard")
titleLabel = QtWidgets.QLabel("Title: ")
subtitleLabel = QtWidgets.QLabel("Subtitle: ")
authorsLabel = QtWidgets.QLabel("Authors: ")
hLayout = QtWidgets.QVBoxLayout()
hLayout.addWidget(titleLabel)
hLayout.addWidget(self.inputTitleEdit)
hLayout.addWidget(subtitleLabel)
hLayout.addWidget(self.inputSubtitleEdit)
hLayout.addWidget(authorsLabel)
hLayout.addWidget(self.inputAuthorsEdit)
layout = QtWidgets.QVBoxLayout()
layout.addLayout(hLayout)
self.setLayout(layout)
def getData(self):
return {
'Title': self.inputTitleEdit.text(),
'Subtitle': self.inputSubtitleEdit.text(),
'Authors': self.inputAuthorsEdit.text(),
}
class SummaryPage(QtWidgets.QWizardPage):
def __init__(self, parent):
super(SummaryPage, self).__init__(parent=parent.parent())
self.setTitle("Summary Page Wizard")
self.inputNotesEdit = QtWidgets.QPlainTextEdit()
self.canvasDropDownMenu = QtWidgets.QComboBox()
self.viewPortCheckBox = QtWidgets.QCheckBox('ViewPort Only')
self.viewPortCheckBox.setChecked(False)
self.canvasNames = list(self.parent().centralWidget().tabbedPane.canvasTabs.keys())
summaryLabel = QtWidgets.QLabel("Summary Notes: ")
canvasLabel = QtWidgets.QLabel("Select canvas to be displayed: ")
for canvasName in self.canvasNames:
self.canvasDropDownMenu.addItem(canvasName)
hLayout = QtWidgets.QVBoxLayout()
hLayout.addWidget(summaryLabel)
hLayout.addWidget(self.inputNotesEdit)
hLayout.addWidget(canvasLabel)
hLayout.addWidget(self.viewPortCheckBox)
hLayout.addWidget(self.canvasDropDownMenu)
layout = QtWidgets.QVBoxLayout()
layout.addLayout(hLayout)
self.setLayout(layout)
def getData(self):
return {
'SummaryNotes': self.inputNotesEdit.toPlainText(),
'CanvasName': self.canvasDropDownMenu.currentText(),
'ViewPort': self.viewPortCheckBox.isChecked(),
}
class EntityPage(QtWidgets.QWizardPage):
def __init__(self, parent: ReportWizard):
super(EntityPage, self).__init__(parent=parent.parent())
self.reportWizard = parent
self.setTitle("Entity Page Wizard")
self.setMinimumSize(300, 700)
self.entityName = parent.primaryField
self.entityUID = parent.uid
self.inputNotesEdit = QtWidgets.QPlainTextEdit()
self.inputImageEdit = QtWidgets.QLineEdit()
self.inputImageEdit.setReadOnly(True)
self.addAppendixButton = QtWidgets.QPushButton("Add New Appendix Section")
self.removeAppendixButton = QtWidgets.QPushButton("Remove Last Appendix Section")
self.scrolllayout = QtWidgets.QVBoxLayout()
self.scrollwidget = QtWidgets.QWidget()
self.defaultpic = self.parent().LENTDB.getEntity(self.entityUID).get('Icon')
summaryLabel = QtWidgets.QLabel(f"Entity {self.entityName} Notes: ")
imageLabel = QtWidgets.QLabel("Image Path: ")
pDirButton = QtWidgets.QPushButton("Select Image...")
pDirButton.clicked.connect(self.editPath)
pDirButton.setDisabled(True)
self.imageCheckBox = QtWidgets.QCheckBox('Add Custom Entity Image')
self.imageCheckBox.setChecked(False)
self.imageCheckBox.toggled.connect(pDirButton.setEnabled)
self.addAppendixButton.clicked.connect(self.addSection)
self.removeAppendixButton.clicked.connect(self.removeSection)
self.scrollwidget.setLayout(self.scrolllayout)
scroll = QtWidgets.QScrollArea()
scroll.setWidgetResizable(True)
scroll.setWidget(self.scrollwidget)
hLayout = QtWidgets.QVBoxLayout()
hLayout.addWidget(summaryLabel)
hLayout.addWidget(self.inputNotesEdit)
hLayout.addWidget(self.imageCheckBox)
hLayout.addWidget(imageLabel)
hLayout.addWidget(self.inputImageEdit)
hLayout.addWidget(pDirButton)
hLayout.addItem(QtWidgets.QSpacerItem(10, 30))
hLayout.addWidget(self.addAppendixButton)
hLayout.addWidget(self.removeAppendixButton)
layout = QtWidgets.QVBoxLayout()
layout.addLayout(hLayout)
layout.addWidget(scroll)
self.setLayout(layout)
def editPath(self) -> None:
selectedPath = QtWidgets.QFileDialog().getOpenFileName(parent=self, caption='Select New Icon',
dir=str(Path.home()),
options=QtWidgets.QFileDialog.Option.DontUseNativeDialog,
filter="Image Files (*.png *.jpg)")[0]
if selectedPath != '':
self.inputImageEdit.setText(str(Path(selectedPath).absolute()))
def addSection(self) -> None:
appendixWidget = AppendixWidget()
self.scrolllayout.addWidget(appendixWidget)
def removeSection(self) -> None:
if numChildren := self.scrolllayout.count():
appendixItem = self.scrolllayout.takeAt(numChildren - 1)
appendixItem.widget().deleteLater()
def getData(self):
appendixNotes = []
if self.inputImageEdit.text() != '' and self.imageCheckBox.isChecked():
data = {'EntityNotes': self.inputNotesEdit.toPlainText(), 'EntityImage': self.inputImageEdit.text()}
elif 'PNG' in str(self.defaultpic):
imagePath = Path(self.reportWizard.reportTempFolder) / f'{str(uuid4())}.png'
with open(imagePath, 'wb') as tempFile:
tempFile.write(bytearray(self.defaultpic.data()))
data = {'EntityNotes': self.inputNotesEdit.toPlainText(), 'EntityImage': str(imagePath)}
else:
if 'svg' not in str(self.defaultpic):
# Default picture is an SVG.
self.defaultpic = self.reportWizard.parent().RESOURCEHANDLER.getEntityDefaultPicture(
self.reportWizard.parent().LENTDB.getEntity(self.entityUID)['Entity Type'])
contents = bytearray(self.defaultpic)
widthRegex = re.compile(b' width="\d*" ')
for widthMatches in widthRegex.findall(self.defaultpic):
contents = contents.replace(widthMatches, b' ')
heightRegex = re.compile(b' height="\d*" ')
for heightMatches in heightRegex.findall(self.defaultpic):
contents = contents.replace(heightMatches, b' ')
contents = contents.replace(b'<svg ', b'<svg height="150" width="150" ')
imagePath = Path(self.reportWizard.reportTempFolder) / f'{str(uuid4())}.svg'
with open(imagePath, 'wb') as tempFile:
tempFile.write(contents)
image = svg2rlg(imagePath)
data = {'EntityNotes': self.inputNotesEdit.toPlainText(), 'EntityImage': image}
for index in range(self.scrolllayout.count()):
childWidget = self.scrolllayout.itemAt(index).widget()
appendixDict = {'AppendixEntityNotes': childWidget.inputAppendixNotesEdit.toPlainText(),
'AppendixEntityImage': childWidget.inputAppendixImageEdit.text()}
appendixNotes.append(appendixDict)
return data, appendixNotes
class AppendixWidget(QtWidgets.QWidget):
def __init__(self) -> None:
super(AppendixWidget, self).__init__()
appendixWidgetLayout = QtWidgets.QGridLayout()
appendixLabelNotes = QtWidgets.QLabel("Entity Notes: ")
self.inputAppendixNotesEdit = QtWidgets.QPlainTextEdit()
imageAppendixLabel = QtWidgets.QLabel("Image Path: ")
appendixButton = QtWidgets.QPushButton("Select Image...")
appendixButton.clicked.connect(self.editAppendixPath)
self.inputAppendixImageEdit = QtWidgets.QLineEdit()
self.inputAppendixImageEdit.setReadOnly(True)
appendixWidgetLayout.addWidget(appendixLabelNotes, 0, 0, 1, 1)
appendixWidgetLayout.addWidget(self.inputAppendixNotesEdit, 2, 0, 4, 1)
appendixWidgetLayout.addWidget(imageAppendixLabel, 7, 0, 1, 1)
appendixWidgetLayout.addWidget(self.inputAppendixImageEdit, 9, 0, 1, 1)
appendixWidgetLayout.addWidget(appendixButton, 11, 0, 1, 1)
self.setLayout(appendixWidgetLayout)
self.inputAppendixNotesEdit.setFixedHeight(100)
def editAppendixPath(self) -> None:
selectedPath = QtWidgets.QFileDialog().getOpenFileName(parent=self, caption='Select New Icon',
dir=str(Path.home()),
options=QtWidgets.QFileDialog.Option.DontUseNativeDialog,
filter="Image Files (*.png *.jpg)")[0]
if selectedPath != '':
self.inputAppendixImageEdit.setText(str(Path(selectedPath).absolute()))
class MyDocTemplate(BaseDocTemplate):
def __init__(self, filename, **kw):
self.allowSplitting = 0
@@ -206,14 +570,12 @@ class PDFReport:
outgoing_data.append([linkName, childNode, dateCreated, Paragraph(linkNotes)])
outgoing_table = Table(data=outgoing_data, style=links_table_style, hAlign="CENTER",
colWidths=[140, 140, 140, 140])
spacer = Spacer(10, 10)
self.elements.append(spacer)
else:
outgoing_data = [
['No Outgoing Links']]
outgoing_table = Table(data=outgoing_data, hAlign="CENTER")
spacer = Spacer(10, 10)
self.elements.append(spacer)
spacer = Spacer(10, 10)
self.elements.append(spacer)
# parent UID
if incomingLinks:
@@ -236,14 +598,12 @@ class PDFReport:
incoming_data.append([linkName, parentNode, dateCreated, Paragraph(linkNotes)])
incoming_table = Table(data=incoming_data, style=links_table_style, hAlign="CENTER",
colWidths=[140, 140, 140, 140])
spacer = Spacer(10, 10)
self.elements.append(spacer)
else:
incoming_data = [
['No Incoming Links']]
incoming_table = Table(data=incoming_data, hAlign="CENTER")
spacer = Spacer(10, 10)
self.elements.append(spacer)
spacer = Spacer(10, 10)
self.elements.append(spacer)
tableParagraph = ParagraphStyle('Report', fontSize=9, justifyBreaks=1, alignment=TA_CENTER,
justifyLastLine=0)
@@ -296,7 +656,7 @@ class PDFReport:
pie.y = 65
pie.data = [len(incomingLinks), len(outgoingLinks)]
pie.sideLabels = 1
pie.labels = ['Incoming: ' + str(len(incomingLinks)), 'Outgoing: ' + str(len(outgoingLinks))]
pie.labels = [f'Incoming: {len(incomingLinks)}', f'Outgoing: {len(outgoingLinks)}']
pie.slices.strokeWidth = 1
if len(incomingLinks) > len(outgoingLinks):
pie.slices[0].popout = 5

View File

@@ -6,26 +6,30 @@ import sys
from os import listdir
from pathlib import Path
from uuid import uuid4
from typing import Union
from shutil import move
from msgpack import dump, load
from typing import Union, Any
from PySide6 import QtCore, QtWidgets, QtGui
from Core.ResourceHandler import StringPropertyInput, FilePropertyInput, SingleChoicePropertyInput, \
MultiChoicePropertyInput
class ResolutionManager:
# Load all resources needed.
def __init__(self, mainWindow, messageHandler):
self.messageHandler = messageHandler
def __init__(self, mainWindow):
self.mainWindow = mainWindow
self.resolutions = {}
# Macro dict item contents: tuple of (resolution, parameter values)
self.macros = {}
def loadResolutionsFromDir(self, directory: Path) -> None:
self.loadResolutionsFromDir(
Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) / "Core" / "Resolutions" / "Core")
def loadResolutionsFromDir(self, directory: Path) -> list:
resolutionsLoaded = []
exceptionsCount = 0
for resolution in listdir(directory):
resolution = str(resolution)
resolutionCategory = "Uncategorized"
try:
if resolution.endswith('.py'):
resolutionName = resolution[:-3]
@@ -44,7 +48,7 @@ class ResolutionManager:
with contextlib.suppress(AttributeError):
resolutionCategory = resClassInst.category
if not isinstance(resolutionCategory, str):
raise AttributeError()
resolutionCategory = "Uncategorized"
if self.resolutions.get(resolutionCategory) is None:
self.resolutions[resolutionCategory] = {}
self.resolutions[resolutionCategory][resNameString] = {'name': resNameString,
@@ -55,19 +59,22 @@ class ResolutionManager:
'category': resolutionCategory,
'resolution': resClass
}
self.messageHandler.info(f"Loaded Resolution: {resNameString}")
self.mainWindow.MESSAGEHANDLER.debug(f"Loaded Resolution: {resNameString}")
resolutionsLoaded.append(f'{resolutionCategory}/{resNameString}')
except Exception as e:
self.messageHandler.error(f"Cannot load resolutions from {str(directory)}" + "\n Info: " + repr(e))
self.mainWindow.MESSAGEHANDLER.error(
f"Cannot load resolutions from {str(directory)}\n Info: {repr(e)}")
exceptionsCount += 1
if exceptionsCount > 3:
# Will not occur when loading modules with 3 or fewer resolutions, but that should be fine.
self.messageHandler.critical("Failed loading too many resolutions to proceed.")
self.mainWindow.MESSAGEHANDLER.critical("Failed loading too many resolutions to proceed.")
sys.exit(5)
return resolutionsLoaded
def getResolutionParameters(self, resolutionCategory, resolutionNameString):
resolutionsList = self.resolutions.get(resolutionCategory)
if resolutionsList is not None and resolutionNameString in resolutionsList:
return self.resolutions[resolutionCategory][resolutionNameString]['parameters']
return dict(self.resolutions[resolutionCategory][resolutionNameString]['parameters'])
return None
def getResolutionOriginTypes(self, resolutionCategoryNameString: str) -> Union[list, None]:
@@ -139,8 +146,8 @@ class ResolutionManager:
if resolutionName in self.resolutions.get(resolutionCategory):
if self.resolutions[resolutionCategory][resolutionName].get('resolution') == '':
# If resolution class does not exist locally, then assume it exists on the server.
self.mainWindow.executeRemoteResolution(resolutionCategoryNameString, resolutionEntitiesInput,
parameters, resolutionUID)
self.mainWindow.FCOM.runRemoteResolution(
resolutionCategoryNameString, resolutionEntitiesInput, parameters, resolutionUID)
# Returning a bool, so we know that the resolution is running on the server.
return True
resolutionClass = self.resolutions[resolutionCategory][resolutionName]['resolution']()
@@ -153,6 +160,21 @@ class ResolutionManager:
return macroUID
def renameMacro(self, oldName: str, newName: str) -> bool:
if oldName != newName:
with self.mainWindow.macrosLock:
if newName in self.macros:
self.mainWindow.MESSAGEHANDLER.warning('The specified name already exists. '
'Macro names must be unique.',
popUp=True)
return False
if oldName not in self.macros:
self.mainWindow.MESSAGEHANDLER.error('Attempting to rename a nonexistent macro.', popUp=True)
return False
oldMacro = self.macros.pop(oldName)
self.macros[newName] = oldMacro
return True
def deleteMacro(self, macroUID: str) -> bool:
# We don't have to worry about running macros, because the details of the macro are saved in memory.
# We do however want to get the thread lock because of potential race conditions.
@@ -163,31 +185,519 @@ class ResolutionManager:
except KeyError:
return False
def getMacroFilePath(self):
return Path(self.mainWindow.SETTINGS.value("Project/BaseDir")) / "Project Files" / "Project Macros.lsmacros"
def loadMacros(self):
def loadMacros(self) -> None:
# Load AFTER we load resolutions.
macroFilePath = self.getMacroFilePath()
self.macros = self.mainWindow.SETTINGS.value("Program/Macros", {})
def save(self) -> None:
self.mainWindow.SETTINGS.setGlobalValue("Program/Macros", self.macros)
class ResolutionParametersSelector(QtWidgets.QDialog):
def __init__(self, mainWindowObject, resolutionName, properties: dict, includeEntitySelector: list = None,
originTypes: list = None, resolutionDescription: str = None,
windowTitle: str = None) -> None:
super(ResolutionParametersSelector, self).__init__()
self.setModal(True)
if windowTitle is None:
windowTitle = f'Resolution Parameter Selector: {resolutionName}'
self.setWindowTitle(windowTitle)
self.parametersList = []
# Have two separate dicts for readability.
self.chosenParameters = {}
self.properties = properties
self.mainWindowObject = mainWindowObject
self.resolutionName = resolutionName
dialogLayout = QtWidgets.QGridLayout()
self.setLayout(dialogLayout)
self.childWidget = QtWidgets.QTabWidget()
dialogLayout.addWidget(self.childWidget, 0, 0, 4, 2)
dialogLayout.setRowStretch(0, 1)
dialogLayout.setColumnStretch(0, 1)
if includeEntitySelector is not None and originTypes is not None:
entitySelectTab = QtWidgets.QWidget()
entitySelectTab.setLayout(QtWidgets.QVBoxLayout())
labelText = ""
if resolutionDescription is not None:
labelText += resolutionDescription + "\n\n"
labelText += 'Select the entities to use for this resolution.\nAccepted Origin Types: ' + \
', '.join(originTypes)
entitySelectTabLabel = QtWidgets.QLabel(labelText)
entitySelectTabLabel.setWordWrap(True)
entitySelectTabLabel.setMaximumWidth(600)
entitySelectTabLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
entitySelectTab.layout().addWidget(entitySelectTabLabel)
self.entitySelector = QtWidgets.QListWidget()
self.entitySelector.setSortingEnabled(True)
self.entitySelector.addItems(includeEntitySelector)
self.entitySelector.setSelectionMode(self.entitySelector.SelectionMode.MultiSelection)
entitySelectTab.layout().addWidget(self.entitySelector)
self.childWidget.addTab(entitySelectTab, 'Entities')
for key in properties:
propertyWidget = QtWidgets.QWidget()
propertyKeyLayout = QtWidgets.QVBoxLayout()
propertyWidget.setLayout(propertyKeyLayout)
propertyLabel = QtWidgets.QLabel(properties[key].get('description'))
propertyLabel.setWordWrap(True)
propertyLabel.setMaximumWidth(600)
propertyLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
propertyKeyLayout.addWidget(propertyLabel)
propertyType = properties[key].get('type')
propertyValue = properties[key].get('value')
propertyDefaultValue = properties[key].get('default')
if propertyType == 'String':
propertyInputField = StringPropertyInput(propertyValue, propertyDefaultValue)
elif propertyType == 'File':
propertyInputField = FilePropertyInput(propertyValue, propertyDefaultValue)
elif propertyType == 'SingleChoice':
propertyInputField = SingleChoicePropertyInput(propertyValue, propertyDefaultValue)
elif propertyType == 'MultiChoice':
propertyInputField = MultiChoicePropertyInput(propertyValue, propertyDefaultValue)
else:
# If value has invalid type, skip to the next property.
propertyInputField = None
if propertyInputField is not None:
propertyKeyLayout.addWidget(propertyInputField)
rememberChoiceCheckbox = QtWidgets.QCheckBox('Remember Choice')
rememberChoiceCheckbox.setChecked(False)
propertyKeyLayout.addWidget(rememberChoiceCheckbox)
propertyKeyLayout.setStretch(1, 1)
self.childWidget.addTab(propertyWidget, key)
self.parametersList.append((key, propertyInputField, rememberChoiceCheckbox))
nextButton = QtWidgets.QPushButton('Next')
nextButton.clicked.connect(self.nextTab)
previousButton = QtWidgets.QPushButton('Previous')
previousButton.clicked.connect(self.previousTab)
acceptButton = QtWidgets.QPushButton('Accept')
acceptButton.setAutoDefault(True)
acceptButton.setDefault(True)
acceptButton.clicked.connect(self.accept)
cancelButton = QtWidgets.QPushButton('Cancel')
cancelButton.clicked.connect(self.reject)
dialogLayout.addWidget(previousButton, 4, 0, 1, 1)
dialogLayout.addWidget(nextButton, 4, 1, 1, 1)
dialogLayout.addWidget(cancelButton, 5, 0, 1, 1)
dialogLayout.addWidget(acceptButton, 5, 1, 1, 1)
def nextTab(self):
currentIndex = self.childWidget.currentIndex()
if currentIndex < self.childWidget.count():
self.childWidget.setCurrentIndex(currentIndex + 1)
def previousTab(self):
currentIndex = self.childWidget.currentIndex()
if currentIndex > 0:
self.childWidget.setCurrentIndex(currentIndex - 1)
def accept(self) -> None:
savedParameters = {}
for resolutionParameterName, resolutionParameterInput, resolutionParameterRemember in self.parametersList:
value = resolutionParameterInput.getValue()
if value == '':
msgBox = QtWidgets.QMessageBox()
msgBox.setModal(True)
QtWidgets.QMessageBox.warning(msgBox,
"Not all parameters were filled in",
"Some of the required parameters for the resolution have been left blank."
" Please fill them in before proceeding.")
return
self.chosenParameters[resolutionParameterName] = value
if resolutionParameterRemember.isChecked():
savedParameters[resolutionParameterName] = value
# Only save parameters after we verify that everything is filled in properly.
for savedParameter in savedParameters:
if self.properties[savedParameter].get('global') is True:
self.mainWindowObject.SETTINGS.setGlobalValue(
f'Resolutions/Global/Parameters/{savedParameter}',
savedParameters[savedParameter],
)
else:
self.mainWindowObject.SETTINGS.setGlobalValue(
f'Resolutions/{self.resolutionName}/{savedParameter}',
savedParameters[savedParameter],
)
super(ResolutionParametersSelector, self).accept()
class ResolutionSearchResultsList(QtWidgets.QListWidget):
def __init__(self, mainWindowObject):
super(ResolutionSearchResultsList, self).__init__()
self.mainWindow = mainWindowObject
self.setSortingEnabled(True)
def mouseDoubleClickEvent(self, event: QtGui.QMouseEvent) -> None:
super(ResolutionSearchResultsList, self).mouseDoubleClickEvent(event)
resItem = self.itemAt(event.pos())
if resItem is None or '/' not in resItem.text():
return
self.mainWindow.centralWidget().tabbedPane.getCurrentScene().clearSelection()
self.mainWindow.runResolution(resItem.text())
class FindResolutionDialog(QtWidgets.QDialog):
def __init__(self, parent, entityList: list, resolutionDict: dict):
super(FindResolutionDialog, self).__init__()
self.entities = entityList
self.resolutions = resolutionDict
self.setModal(True)
self.setWindowTitle('Find Resolutions')
dialogLayout = QtWidgets.QGridLayout()
self.setLayout(dialogLayout)
descriptionLabel = QtWidgets.QLabel("Find Resolutions based on their parameters.")
descriptionLabel.setWordWrap(True)
dialogLayout.addWidget(descriptionLabel, 0, 0, 1, 2)
originLabel = QtWidgets.QLabel("Origin Entity:")
self.originDropDown = QtWidgets.QComboBox()
self.originDropDown.addItem('Any')
self.originDropDown.addItems(entityList)
self.originDropDown.addItem('*')
dialogLayout.addWidget(originLabel, 1, 0, 1, 1)
dialogLayout.addWidget(self.originDropDown, 1, 1, 1, 1)
targetLabel = QtWidgets.QLabel("Target Entity:")
self.targetDropDown = QtWidgets.QComboBox()
self.targetDropDown.addItem('Any')
self.targetDropDown.addItems(entityList)
self.targetDropDown.addItem('*')
dialogLayout.addWidget(targetLabel, 2, 0, 1, 1)
dialogLayout.addWidget(self.targetDropDown, 2, 1, 1, 1)
keywordsLabel = QtWidgets.QLabel("Keywords:")
self.keywordsWidget = QtWidgets.QLineEdit()
self.keywordsWidget.setToolTip("Add keywords separated by spaces.\nKeywords are checked against the "
"resolutions' titles and descriptions.")
dialogLayout.addWidget(keywordsLabel, 3, 0, 1, 2)
dialogLayout.addWidget(self.keywordsWidget, 4, 0, 1, 2)
resultsLabel = QtWidgets.QLabel("Matches:")
self.resultsWidget = ResolutionSearchResultsList(parent)
self.resultsWidget.addItem('Click "Search" to display results')
dialogLayout.addWidget(resultsLabel, 5, 0, 1, 2)
dialogLayout.addWidget(self.resultsWidget, 6, 0, 2, 2)
self.searchButton = QtWidgets.QPushButton("Search")
self.searchButton.clicked.connect(self.search)
self.closeButton = QtWidgets.QPushButton("Close")
self.closeButton.clicked.connect(self.accept)
dialogLayout.addWidget(self.closeButton, 8, 0, 1, 1)
dialogLayout.addWidget(self.searchButton, 8, 1, 1, 1)
def search(self):
self.resultsWidget.clear()
target = self.targetDropDown.currentText()
validResolutions = []
for category in self.resolutions:
for resolution in self.resolutions[category]:
if target == 'Any':
validResolutions.append(f'{category}/{resolution}')
elif target in self.resolutions[category][resolution]['originTypes']:
validResolutions.append(f'{category}/{resolution}')
origin = self.originDropDown.currentText()
if origin != 'Any':
for category in self.resolutions:
for resolution in self.resolutions[category]:
if origin not in self.resolutions[category][resolution]['originTypes']:
with contextlib.suppress(KeyError):
validResolutions.remove(f'{str(category)}/{str(resolution)}')
# Try to see if any of the keywords are a substring of the name or description of any resolution.
keywordFilter = self.keywordsWidget.text().strip()
if keywordFilter != '':
wordsToFind = keywordFilter.split(' ')
for category in self.resolutions:
for resolution in self.resolutions[category]:
titleText = self.resolutions[category][resolution]['name']
descriptionText = self.resolutions[category][resolution]['description']
for keyword in wordsToFind:
if keyword not in titleText and keyword not in descriptionText:
with contextlib.suppress(KeyError):
validResolutions.remove(f'{str(category)}/{str(resolution)}')
for result in validResolutions:
self.resultsWidget.addItem(result)
class ResolutionExecutorThread(QtCore.QThread):
sig = QtCore.Signal(str, list, str)
sigStr = QtCore.Signal(str, str, str)
sigError = QtCore.Signal(str)
def __init__(self, resolution: str, resolutionArgument: list, resolutionParameters: dict,
mainWindowObject, uid: str):
super().__init__()
self.resolution = resolution
self.resolutionArgument = resolutionArgument
self.resolutionParameters = resolutionParameters
self.mainWindow = mainWindowObject
self.return_results = True
self.uid = uid
self.done = False
def run(self) -> None:
try:
with open(macroFilePath, "rb") as macroFile:
self.macros = load(macroFile)
except ValueError:
# If the Macros file is empty or contains invalid input, ignore it.
pass
except FileNotFoundError:
# Create new placeholder notes file if it doesn't exist.
try:
macroFilePath.touch(0o700, exist_ok=False)
self.mainWindow.MESSAGEHANDLER.info('Created new Macros file.')
except FileExistsError:
self.mainWindow.MESSAGEHANDLER.error('Race condition occurred while trying to create Macros file.')
ret = self.mainWindow.RESOLUTIONMANAGER.executeResolution(self.resolution,
self.resolutionArgument,
self.resolutionParameters,
self.uid)
if ret is None:
self.sigError.emit(f'Resolution {self.resolution} failed during run.')
elif isinstance(ret, bool):
# Resolution is running on the server, we do not have results right now.
ret = None
except Exception as e:
self.sigError.emit(f'Resolution {self.resolution} failed during run: {str(e)}')
ret = None
def save(self):
macroFilePath = self.getMacroFilePath()
macroFilePathTmp = macroFilePath.with_suffix(f'{macroFilePath.suffix}.tmp')
# If the resolution is ran on the server or there is a problem, don't emit signal.
if ret is not None and self.return_results:
if isinstance(ret, str):
self.sigStr.emit(self.resolution, ret, self.uid)
else:
self.sig.emit(self.resolution, ret, self.uid)
self.done = True
class MacroDialog(QtWidgets.QDialog):
def __init__(self, mainWindowObject):
super(MacroDialog, self).__init__()
self.mainWindowObject = mainWindowObject
self.setModal(True)
self.resolutionList = []
for category in mainWindowObject.RESOLUTIONMANAGER.getResolutionCategories():
self.resolutionList.extend(
f'{category}/{resolution}'
for resolution in mainWindowObject.RESOLUTIONMANAGER.getResolutionsInCategory(category)
)
self.resolutionList.sort()
layout = QtWidgets.QVBoxLayout()
self.setLayout(layout)
macroLabel = QtWidgets.QLabel("This is a list of all currently configured Macros.\n"
"Click on a Macro to view the Resolutions included in it.")
macroLabel.setWordWrap(True)
self.macroTree = MacroTree(self, mainWindowObject)
self.macroTree.setSelectionMode(self.macroTree.SelectionMode.ExtendedSelection)
self.macroTree.setSelectionBehavior(self.macroTree.SelectionBehavior.SelectRows)
self.macroTree.setHeaderLabels(['Macro UID', 'Delete'])
self.macroTree.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows)
self.macroTree.setSortingEnabled(False)
# Stretch the first column, since it contains the primary field.
self.macroTree.header().setStretchLastSection(False)
self.macroTree.header().setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeMode.Stretch)
buttonsWidget = QtWidgets.QWidget()
buttonsWidgetLayout = QtWidgets.QHBoxLayout()
buttonsWidget.setLayout(buttonsWidgetLayout)
closeButton = QtWidgets.QPushButton('Close')
closeButton.clicked.connect(self.reject)
addMacroButton = QtWidgets.QPushButton('Create New Macro')
addMacroButton.clicked.connect(self.createMacro)
runSelectedButton = QtWidgets.QPushButton('Run Selected Macros')
runSelectedButton.clicked.connect(self.accept)
buttonsWidgetLayout.addWidget(closeButton)
buttonsWidgetLayout.addWidget(addMacroButton)
buttonsWidgetLayout.addWidget(runSelectedButton)
layout.addWidget(macroLabel)
layout.addWidget(self.macroTree)
layout.addWidget(buttonsWidget)
self.updateMacroTree()
self.setBaseSize(1000, 1000)
def updateMacroTree(self) -> None:
self.macroTree.clear()
with self.mainWindowObject.macrosLock:
allMacros = self.mainWindowObject.RESOLUTIONMANAGER.macros
for macro in allMacros:
newMacro = MacroTreeItem(macro, allMacros[macro])
self.macroTree.addTopLevelItem(newMacro)
self.macroTree.setItemWidget(newMacro, 1, newMacro.deleteButton)
def createMacro(self) -> None:
createMacroDialog = MacroCreatorDialog(self.resolutionList)
if createMacroDialog.exec():
macroResolutionsList = []
numberOfResolutionsSelected = createMacroDialog.createList.count()
for itemIndex in range(numberOfResolutionsSelected):
itemText = createMacroDialog.createList.item(itemIndex).text()
resolutionCategory, resolutionName = itemText.split('/', 1)
rParameters = self.mainWindowObject.RESOLUTIONMANAGER.getResolutionParameters(resolutionCategory,
resolutionName)
if rParameters is None:
message = f'Resolution parameters not found for resolution: {resolutionName}'
self.mainWindowObject.MESSAGEHANDLER.error(message, popUp=True, exc_info=False)
self.mainWindowObject.setStatus(f'{message}, Macro creation aborted.')
return
resolutionParameterValues = self.mainWindowObject.popParameterValuesAndReturnSpecified(resolutionName,
rParameters)
if rParameters:
parameterSelector = ResolutionParametersSelector(
self.mainWindowObject, resolutionName, rParameters,
windowTitle=f'[{str(itemIndex + 1)}/{str(numberOfResolutionsSelected)}] Select Parameter '
f'values for Resolution: {resolutionName}')
if parameterSelector.exec():
resolutionParameterValues.update(parameterSelector.chosenParameters)
else:
self.mainWindowObject.MESSAGEHANDLER.info('Macro creation aborted.')
self.mainWindowObject.setStatus('Macro creation aborted.')
return
macroResolutionsList.append((itemText, resolutionParameterValues))
self.mainWindowObject.RESOLUTIONMANAGER.createMacro(macroResolutionsList)
self.updateMacroTree()
self.mainWindowObject.setStatus('New Macro Created.')
self.mainWindowObject.MESSAGEHANDLER.info('New Macro Created.')
def accept(self) -> None:
super(MacroDialog, self).accept()
class MacroTree(QtWidgets.QTreeWidget):
def __init__(self, parent, mainWindowObject):
super(MacroTree, self).__init__(parent=parent)
self.mainWindowObject = mainWindowObject
def deleteMacro(self, treeEntry: QtWidgets.QTreeWidgetItem):
index = self.indexOfTopLevelItem(treeEntry)
uid = treeEntry.text(0)
self.mainWindowObject.RESOLUTIONMANAGER.deleteMacro(uid)
self.takeTopLevelItem(index)
class MacroTreeItem(QtWidgets.QTreeWidgetItem):
def __init__(self, uid: str, resolutionList: list):
super(MacroTreeItem, self).__init__()
self.setText(0, uid)
self.uid = uid
self.setFlags(QtCore.Qt.ItemFlag.ItemIsEditable | self.flags())
self.deleteButton = QtWidgets.QPushButton('X')
self.deleteButton.clicked.connect(self.removeSelf)
for resolution in resolutionList:
resolutionItem = QtWidgets.QTreeWidgetItem()
resolutionItem.setText(0, f'Resolution: {resolution[0]}')
self.addChild(resolutionItem)
def setData(self, column: int, role: int, value: Any):
if self.treeWidget() is None:
# Happens during initialization
super().setData(column, role, value)
elif self.treeWidget().mainWindowObject.RESOLUTIONMANAGER.renameMacro(self.uid, value):
super().setData(column, role, value)
self.uid = value
def removeSelf(self):
self.treeWidget().deleteMacro(self)
class MacroCreatorDialog(QtWidgets.QDialog):
def __init__(self, resolutionsWithCategoriesList: list):
super(MacroCreatorDialog, self).__init__()
self.setWindowTitle('Create new Macro')
self.viewList = QtWidgets.QListWidget()
for resolution in resolutionsWithCategoriesList:
viewItem = QtWidgets.QListWidgetItem(resolution)
self.viewList.addItem(viewItem)
self.viewList.sortItems()
self.viewList.setSelectionMode(self.viewList.SelectionMode.ExtendedSelection)
buttonsAddRemoveWidget = QtWidgets.QWidget()
buttonsAddRemoveWidgetLayout = QtWidgets.QVBoxLayout()
buttonAdd = QtWidgets.QPushButton('>')
buttonAdd.clicked.connect(self.addSelectedToMacro)
buttonRemove = QtWidgets.QPushButton('<')
buttonRemove.clicked.connect(self.removeSelectedFromMacro)
buttonsAddRemoveWidget.setLayout(buttonsAddRemoveWidgetLayout)
buttonsAddRemoveWidgetLayout.addWidget(buttonAdd)
buttonsAddRemoveWidgetLayout.addWidget(buttonRemove)
self.createList = QtWidgets.QListWidget()
buttonsRearrangeWidget = QtWidgets.QWidget()
buttonsRearrangeWidgetLayout = QtWidgets.QVBoxLayout()
buttonsRearrangeWidget.setLayout(buttonsRearrangeWidgetLayout)
buttonMoveUp = QtWidgets.QPushButton('^')
buttonMoveUp.clicked.connect(self.shiftSelectedUp)
buttonMoveDown = QtWidgets.QPushButton('v')
buttonMoveDown.clicked.connect(self.shiftSelectedDown)
buttonsRearrangeWidgetLayout.addWidget(buttonMoveUp)
buttonsRearrangeWidgetLayout.addWidget(buttonMoveDown)
allResolutionsLabel = QtWidgets.QLabel('All Resolutions')
allResolutionsLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
selectedResolutionsLabel = QtWidgets.QLabel('Selected Resolutions')
selectedResolutionsLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
confirmButton = QtWidgets.QPushButton('Confirm')
confirmButton.clicked.connect(self.accept)
cancelButton = QtWidgets.QPushButton('Cancel')
cancelButton.clicked.connect(self.reject)
macroCreateLayout = QtWidgets.QGridLayout()
self.setLayout(macroCreateLayout)
macroCreateLayout.addWidget(self.viewList, 1, 0, 1, 5)
macroCreateLayout.addWidget(buttonsAddRemoveWidget, 1, 5, 1, 1)
macroCreateLayout.addWidget(self.createList, 1, 6, 1, 5)
macroCreateLayout.addWidget(buttonsRearrangeWidget, 1, 11, 1, 1)
macroCreateLayout.addWidget(allResolutionsLabel, 0, 0, 1, 5)
macroCreateLayout.addWidget(selectedResolutionsLabel, 0, 6, 1, 5)
macroCreateLayout.addWidget(cancelButton, 2, 1, 1, 3)
macroCreateLayout.addWidget(confirmButton, 2, 7, 1, 3)
self.setBaseSize(1000, 700)
def addSelectedToMacro(self):
for selectedItem in self.viewList.selectedItems():
self.createList.addItem(QtWidgets.QListWidgetItem(selectedItem.text()))
def removeSelectedFromMacro(self):
for selectedItem in self.createList.selectedItems():
self.createList.takeItem(self.createList.row(selectedItem))
def shiftSelectedUp(self):
with contextlib.suppress(IndexError):
selectedItemIndex = self.createList.row(self.createList.selectedItems()[0])
if selectedItemIndex != 0:
currentItem = self.createList.takeItem(selectedItemIndex)
self.createList.insertItem(selectedItemIndex - 1, currentItem)
currentItem.setSelected(True)
def shiftSelectedDown(self):
with contextlib.suppress(IndexError):
selectedItemIndex = self.createList.row(self.createList.selectedItems()[0])
currentItem = self.createList.takeItem(selectedItemIndex)
self.createList.insertItem(selectedItemIndex + 1, currentItem)
currentItem.setSelected(True)
with open(macroFilePathTmp, "wb") as macroFile:
dump(self.macros, macroFile)
move(macroFilePathTmp, macroFilePath)

View File

@@ -10,7 +10,7 @@ class ASNToCIDR:
originTypes = {'Autonomous System'}
# A set of entities that could be the result of this resolution.
resultTypes = {'Network'}
resultTypes = {'Network', 'Company', 'Organization', 'Phrase'}
# A dictionary of properties for this resolution. The key is the property name,
# the value is the property attributes. The type of input expected from the user is determined by the
@@ -46,18 +46,13 @@ class ASNToCIDR:
cidrWithOutPrefix = split_string[0]
prefix = split_string[1]
index_of_child = len(returnResult)
returnResult.append([{'IP Address': cidrWithOutPrefix,
'Range': prefix,
'Entity Type': 'Network'},
{uid: {'Resolution': 'ASN to CIDR', 'Notes': ''}}])
returnResult.append(
[{'Phrase': network['description'], 'Entity Type': 'Phrase'},
{index_of_child: {'Resolution': 'CIDR Description', 'Notes': ''}}])
returnResult.append(
[{'Organization Name': network['source'], 'Entity Type': 'Organization'},
{index_of_child: {'Resolution': 'ASN Registry', 'Notes': ''}}])
returnResult.append(
[{'Company Name': network['maintainer'], 'Entity Type': 'Company'},
{index_of_child: {'Resolution': 'Company Name', 'Notes': ''}}])
returnResult.extend(([{'IP Address': cidrWithOutPrefix, 'Range': prefix, 'Entity Type': 'Network'},
{uid: {'Resolution': 'ASN to CIDR', 'Notes': ''}}],
[{'Phrase': network['description'], 'Entity Type': 'Phrase'},
{index_of_child: {'Resolution': 'CIDR Description', 'Notes': ''}}],
[{'Organization Name': network['source'], 'Entity Type': 'Organization'},
{index_of_child: {'Resolution': 'ASN Registry', 'Notes': ''}}],
[{'Company Name': network['maintainer'], 'Entity Type': 'Company'},
{index_of_child: {'Resolution': 'Company Name', 'Notes': ''}}]))
return returnResult

View File

@@ -44,12 +44,14 @@ class AffiliateCodesExtractor:
def resolution(self, entityJsonList, parameters):
from playwright.sync_api import sync_playwright, TimeoutError, Error
from bs4 import BeautifulSoup
from pathlib import Path
import urllib
import tldextract
import re
playwrightPath = Path(parameters['Playwright Firefox'])
returnResults = []
visitExternal = True if parameters['Visit External Links'] == 'Yes' else False
visitExternal = parameters['Visit External Links'] == 'Yes'
# Numbers less than zero are the same as zero, but we should try to prevent overflows.
try:
@@ -133,60 +135,58 @@ class AffiliateCodesExtractor:
linksInLinkHref = soupContents.find_all('link')
for tag in linksInLinkHref:
newLink = tag.get('href', None)
if newLink is not None:
if newLink.startswith('http'):
newLink = newLink.split('#')[0]
newDepth = depth - 1
if domain in newLink and newLink not in exploredDepth and newDepth > 0:
exploredDepth.add(newLink)
extractCodes(currentUID, newLink, newDepth)
if newLink is not None and newLink.startswith('http'):
newLink = newLink.split('#')[0]
newDepth = depth - 1
if domain in newLink and newLink not in exploredDepth and newDepth > 0:
exploredDepth.add(newLink)
extractCodes(currentUID, newLink, newDepth)
linksInAHref = soupContents.find_all('a')
for tag in linksInAHref:
newLink = tag.get('href', None)
if newLink is not None:
if newLink.startswith('http'):
newLink = newLink.split('#')[0]
newDepth = depth - 1
if domain in newLink:
redirLinks = redirectRegex.findall(newLink)
if 'redirect' in newLink and len(redirLinks) > 0:
newLink = str(urllib.parse.unquote(redirLinks[0]))[2:]
if newLink not in exploredForeign:
exploredForeign.add(newLink)
if visitExternal:
for _ in range(3):
try:
page.goto(newLink, wait_until="networkidle", timeout=10000)
GetAffiliateCodes(currentUID, page.url)
break
except TimeoutError:
pass
except Error:
break
else:
GetAffiliateCodes(currentUID, newLink)
else:
if newLink not in exploredDepth and newDepth > 0:
exploredDepth.add(newLink)
extractCodes(currentUID, newLink, newDepth)
elif newLink not in exploredForeign:
exploredForeign.add(newLink)
if visitExternal:
for _ in range(3):
try:
page.goto(newLink, wait_until="networkidle", timeout=10000)
GetAffiliateCodes(currentUID, page.url)
break
except TimeoutError:
pass
except Error:
break
if newLink is not None and newLink.startswith('http'):
newLink = newLink.split('#')[0]
newDepth = depth - 1
if domain in newLink:
redirLinks = redirectRegex.findall(newLink)
if 'redirect' in newLink and len(redirLinks) > 0:
newLink = str(urllib.parse.unquote(redirLinks[0]))[2:]
if newLink not in exploredForeign:
exploredForeign.add(newLink)
if visitExternal:
for _ in range(3):
try:
page.goto(newLink, wait_until="networkidle", timeout=10000)
GetAffiliateCodes(currentUID, page.url)
break
except TimeoutError:
pass
except Error:
break
else:
GetAffiliateCodes(currentUID, newLink)
else:
if newLink not in exploredDepth and newDepth > 0:
exploredDepth.add(newLink)
extractCodes(currentUID, newLink, newDepth)
elif newLink not in exploredForeign:
exploredForeign.add(newLink)
if visitExternal:
for _ in range(3):
try:
page.goto(newLink, wait_until="networkidle", timeout=10000)
GetAffiliateCodes(currentUID, page.url)
break
except TimeoutError:
pass
except Error:
break
else:
GetAffiliateCodes(currentUID, newLink)
with sync_playwright() as p:
browser = p.firefox.launch()
browser = p.firefox.launch(executable_path=playwrightPath)
context = browser.new_context(
viewport={'width': 1920, 'height': 1080},
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0'
@@ -197,7 +197,7 @@ class AffiliateCodesExtractor:
if url is None:
continue
if not url.startswith('http://') and not url.startswith('https://'):
url = 'http://' + url
url = f'http://{url}'
domain = tldextract.extract(url).fqdn
extractCodes(uid, url, maxDepth)
browser.close()

View File

@@ -11,6 +11,7 @@ class CertificateInfo:
parameters = {}
def resolution(self, entityJsonList, parameters):
import contextlib
import ssl
import socket
@@ -54,21 +55,18 @@ class CertificateInfo:
'Entity Type': 'Domain'},
{uid: {'Resolution': 'Certificate Subject Common Name',
'Notes': ''}}])
elif subjectAttributeInnerKey == 'streetAddress':
streetAddr = subjectAttributeInnerValue
elif subjectAttributeInnerKey == 'countryName':
subjectCountry = subjectAttributeInnerValue
elif subjectAttributeInnerKey == 'postalCode':
postalCode = subjectAttributeInnerValue
elif subjectAttributeInnerKey == 'localityName':
locality = subjectAttributeInnerValue
elif subjectAttributeInnerKey == 'serialNumber':
subjectSerial = subjectAttributeInnerValue
elif subjectAttributeInnerKey == 'organizationName':
subjectName = subjectAttributeInnerValue
elif subjectAttributeInnerKey == 'postalCode':
postalCode = subjectAttributeInnerValue
elif subjectAttributeInnerKey == 'serialNumber':
subjectSerial = subjectAttributeInnerValue
elif subjectAttributeInnerKey == 'streetAddress':
streetAddr = subjectAttributeInnerValue
subjectIndex = None
if subjectName is not None:
subjectIndex = len(returnResults)
@@ -141,45 +139,33 @@ class CertificateInfo:
'Notes': ''}}])
# Domain names included in the certificate.
try:
with contextlib.suppress(KeyError):
for altNameAttribute in websiteCertificate['subjectAltName']:
returnResults.append([{'Domain Name': altNameAttribute[1],
'Entity Type': 'Domain'},
{uid: {'Resolution': 'Certificate Subject Alternate Name',
'Notes': ''}}])
except KeyError:
pass
# OCSP URLs. Often just one.
try:
with contextlib.suppress(KeyError):
for ocsp in websiteCertificate['OCSP']:
returnResults.append([{'URL': ocsp,
'Entity Type': 'Website'},
{uid: {'Resolution': 'Certificate OCSP URL',
'Notes': ''}}])
except KeyError:
pass
# CA Issuer URL
try:
with contextlib.suppress(KeyError):
for caIssuer in websiteCertificate['caIssuers']:
returnResults.append([{'URL': caIssuer,
'Entity Type': 'Website'},
{uid: {'Resolution': 'Certificate Authority Issuer URL',
'Notes': ''}}])
except KeyError:
pass
# CRL URLs
try:
with contextlib.suppress(KeyError):
for crlDistributionPoint in websiteCertificate['crlDistributionPoints']:
returnResults.append([{'URL': crlDistributionPoint,
'Entity Type': 'Website'},
{uid: {'Resolution': 'Certificate Authority Revocation List URL',
'Notes': ''}}])
except KeyError:
pass
# Issuer information
orgName = None
orgCommonName = None
@@ -192,21 +178,20 @@ class CertificateInfo:
for issuerAttributeInner in issuerAttributeOuter:
issuerAttributeInnerKey = issuerAttributeInner[0]
issuerAttributeInnerValue = issuerAttributeInner[1]
if issuerAttributeInnerKey == 'organizationName':
orgName = issuerAttributeInnerValue
elif issuerAttributeInnerKey == 'commonName':
if issuerAttributeInnerKey == 'commonName':
orgCommonName = issuerAttributeInnerValue
elif issuerAttributeInnerKey == 'countryName':
orgCountry = issuerAttributeInnerValue
elif issuerAttributeInnerKey == 'postalCode':
orgPostal = issuerAttributeInnerValue
elif issuerAttributeInnerKey == 'localityName':
orgLocality = issuerAttributeInnerValue
elif issuerAttributeInnerKey == 'stateOrProvinceName':
orgStateOrProvince = issuerAttributeInnerValue
elif issuerAttributeInnerKey == 'orgUnitName':
orgUnitName = issuerAttributeInnerValue
elif issuerAttributeInnerKey == 'organizationName':
orgName = issuerAttributeInnerValue
elif issuerAttributeInnerKey == 'postalCode':
orgPostal = issuerAttributeInnerValue
elif issuerAttributeInnerKey == 'stateOrProvinceName':
orgStateOrProvince = issuerAttributeInnerValue
issuerIndex = None
if orgName is not None:
issuerIndex = len(returnResults)

View File

@@ -22,7 +22,8 @@ class ContainsPhrase:
'value': ''},
'Case Sensitive': {'description': 'Do you want the phrase to be case sensitive?',
'type': 'SingleChoice',
'value': {'Yes', 'No'}
'value': {'Yes', 'No'},
'default': 'Yes'
}
}

View File

@@ -0,0 +1,261 @@
#!/usr/bin/env python3
"""
Credit to @cyb_detective:
https://medium.com/@cyb_detective/20-regular-expressions-examples-to-search-for-data-related-to-cryptocurrencies-43e31dd4a5dc
"""
class CryptoAddressExtractor:
# A string that is treated as the name of this resolution.
name = "Extract Cryptocurrency Addresses"
category = "Website Information"
# A string that describes this resolution.
description = "Returns patterns matching common cryptocurrency address formats on a website."
originTypes = {'Domain', 'Website'}
resultTypes = {'Crypto Wallet'}
parameters = {}
def resolution(self, entityJsonList, parameters):
from playwright.sync_api import sync_playwright, TimeoutError, Error
from bs4 import BeautifulSoup
from pathlib import Path
import re
playwrightPath = Path(parameters['Playwright Chromium'])
returnResults = []
ethRegex = re.compile(r"\b0[xX][a-fA-F0-9]{40}\b")
btcRegex = re.compile(
r"\b(?:bc(?:0(?:[ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59})|1[ac-hj-np-z02-9]{8,87})|[13][a-km-zA-HJ-NP-Z1-9]{25,34})\b")
bchRegex = re.compile(r"\b(?:(?:bitcoincash|bchreg|bchtest):)?[qp][a-z0-9]{41}\b")
moneroRegex = re.compile(r"\b[48][0-9AB][1-9A-HJ-NP-Za-km-z]{93}\b")
dogeRegex = re.compile(r"\bD[5-9A-HJ-NP-U][1-9A-HJ-NP-Za-km-z]{32}\b")
dashRegex = re.compile(r"\bX[1-9A-HJ-NP-Za-km-z]{33}\b")
rippleRegex = re.compile(r"\br[1-9A-HJ-NP-Za-km-z]{24,34}\b")
neoRegex = re.compile(r"\bN[0-9a-zA-Z]{33}\b")
litecoinRegex = re.compile(r"\b[LM3][a-km-zA-HJ-NP-Z1-9]{26,33}\b")
cosmosRegex = re.compile(r"\bcosmos[a-zA-Z0-9_.-]{10,}\b")
cardanoRegex = re.compile(r"\baddr1[a-z0-9]{10,}\b")
iotaRegex = re.compile(r"\biota[a-z0-9]{10,}\b")
liskRegex = re.compile(r"\b[0-9]{19}L\b")
nemRegex = re.compile(
r"\bN[A-Za-z0-9]{4,7}-[A-Za-z0-9]{4,7}-[A-Za-z0-9]{4,7}-[A-Za-z0-9]{4,7}-[A-Za-z0-9]{4,7}-[A-Za-z0-9]{4,7}-[A-Za-z0-9]{4,7}\b")
ontologyRegex = re.compile(r"\bA[0-9a-zA-Z]{33}\b")
polkadotRegex = re.compile(r"\b1[0-9a-zA-Z]{47}\b")
stellarRegex = re.compile(r"\bG[0-9A-Z]{55}\b") # Stellar addresses are always 56 characters long.
# The software can deduplicate, but handling it here is better.
allWallets = set()
def extractCryptoAddresses(currentUID: str, site: str):
page = context.new_page()
pageResolved = False
for _ in range(3):
try:
page.goto(site, wait_until="networkidle", timeout=10000)
pageResolved = True
break
except TimeoutError:
pass
except Error:
break
if not pageResolved:
# Last chance for this to work; some pages have issues with the "networkidle" trigger.
try:
page.goto(site, wait_until="load", timeout=10000)
except Error:
return
soupContents = BeautifulSoup(page.content(), 'lxml')
# Remove <span> and <noscript> tags.
while True:
try:
soupContents.noscript.extract()
except AttributeError:
break
while True:
try:
soupContents.span.extract()
except AttributeError:
break
siteContent = soupContents.get_text()
ethMatch = ethRegex.findall(siteContent)
for potentialMatch in ethMatch:
if potentialMatch not in allWallets:
allWallets.add(potentialMatch)
returnResults.append([{'Wallet Address': potentialMatch,
'Currency Name': 'Etherium',
'Entity Type': 'Crypto Wallet'},
{currentUID: {'Resolution': 'Potential Etherium Wallet Address',
'Notes': ''}}])
btcMatch = btcRegex.findall(siteContent)
for potentialMatch in btcMatch:
if potentialMatch not in allWallets:
allWallets.add(potentialMatch)
returnResults.append([{'Wallet Address': potentialMatch,
'Currency Name': 'Bitcoin',
'Entity Type': 'Crypto Wallet'},
{currentUID: {'Resolution': 'Potential Bitcoin or Bitcoin Cash Wallet Address',
'Notes': ''}}])
bchMatch = bchRegex.findall(siteContent)
for potentialMatch in bchMatch:
if potentialMatch not in allWallets:
allWallets.add(potentialMatch)
returnResults.append([{'Wallet Address': potentialMatch,
'Currency Name': 'Bitcoin Cash',
'Entity Type': 'Crypto Wallet'},
{currentUID: {'Resolution': 'Potential Bitcoin Cash Wallet Address',
'Notes': ''}}])
xmrMatch = moneroRegex.findall(siteContent)
for potentialMatch in xmrMatch:
if potentialMatch not in allWallets:
allWallets.add(potentialMatch)
returnResults.append([{'Wallet Address': potentialMatch,
'Currency Name': 'Monero',
'Entity Type': 'Crypto Wallet'},
{currentUID: {'Resolution': 'Potential Monero Wallet Address',
'Notes': ''}}])
dogeMatch = dogeRegex.findall(siteContent)
for potentialMatch in dogeMatch:
if potentialMatch not in allWallets:
allWallets.add(potentialMatch)
returnResults.append([{'Wallet Address': potentialMatch,
'Currency Name': 'Dogecoin',
'Entity Type': 'Crypto Wallet'},
{currentUID: {'Resolution': 'Potential Dogecoin Wallet Address',
'Notes': ''}}])
dashMatch = dashRegex.findall(siteContent)
for potentialMatch in dashMatch:
if potentialMatch not in allWallets:
allWallets.add(potentialMatch)
returnResults.append([{'Wallet Address': potentialMatch,
'Currency Name': 'Dash',
'Entity Type': 'Crypto Wallet'},
{currentUID: {'Resolution': 'Potential Dash Wallet Address',
'Notes': ''}}])
rippleMatch = rippleRegex.findall(siteContent)
for potentialMatch in rippleMatch:
if potentialMatch not in allWallets:
allWallets.add(potentialMatch)
returnResults.append([{'Wallet Address': potentialMatch,
'Currency Name': 'Ripple',
'Entity Type': 'Crypto Wallet'},
{currentUID: {'Resolution': 'Potential Ripple Wallet Address',
'Notes': ''}}])
neoMatch = neoRegex.findall(siteContent)
for potentialMatch in neoMatch:
if potentialMatch not in allWallets:
allWallets.add(potentialMatch)
returnResults.append([{'Wallet Address': potentialMatch,
'Currency Name': 'Neo',
'Entity Type': 'Crypto Wallet'},
{currentUID: {'Resolution': 'Potential Neo Wallet Address',
'Notes': ''}}])
litecoinMatch = litecoinRegex.findall(siteContent)
for potentialMatch in litecoinMatch:
if potentialMatch not in allWallets:
allWallets.add(potentialMatch)
returnResults.append([{'Wallet Address': potentialMatch,
'Currency Name': 'Litecoin',
'Entity Type': 'Crypto Wallet'},
{currentUID: {'Resolution': 'Potential Litecoin Wallet Address',
'Notes': ''}}])
cosmosMatch = cosmosRegex.findall(siteContent)
for potentialMatch in cosmosMatch:
if potentialMatch not in allWallets:
allWallets.add(potentialMatch)
returnResults.append([{'Wallet Address': potentialMatch,
'Currency Name': 'Cosmos',
'Entity Type': 'Crypto Wallet'},
{currentUID: {'Resolution': 'Potential Cosmos Wallet Address',
'Notes': ''}}])
cardanoMatch = cardanoRegex.findall(siteContent)
for potentialMatch in cardanoMatch:
if potentialMatch not in allWallets:
allWallets.add(potentialMatch)
returnResults.append([{'Wallet Address': potentialMatch,
'Currency Name': 'Cardano',
'Entity Type': 'Crypto Wallet'},
{currentUID: {'Resolution': 'Potential Cardano Wallet Address',
'Notes': ''}}])
iotaMatch = iotaRegex.findall(siteContent)
for potentialMatch in iotaMatch:
if potentialMatch not in allWallets:
allWallets.add(potentialMatch)
returnResults.append([{'Wallet Address': potentialMatch,
'Currency Name': 'Iota',
'Entity Type': 'Crypto Wallet'},
{currentUID: {'Resolution': 'Potential Iota Wallet Address',
'Notes': ''}}])
liskMatch = liskRegex.findall(siteContent)
for potentialMatch in liskMatch:
if potentialMatch not in allWallets:
allWallets.add(potentialMatch)
returnResults.append([{'Wallet Address': potentialMatch,
'Currency Name': 'Lisk',
'Entity Type': 'Crypto Wallet'},
{currentUID: {'Resolution': 'Potential Lisk Wallet Address',
'Notes': ''}}])
nemMatch = nemRegex.findall(siteContent)
for potentialMatch in nemMatch:
if potentialMatch not in allWallets:
allWallets.add(potentialMatch)
returnResults.append([{'Wallet Address': potentialMatch,
'Currency Name': 'Nem',
'Entity Type': 'Crypto Wallet'},
{currentUID: {'Resolution': 'Potential Nem Wallet Address',
'Notes': ''}}])
ontologyMatch = ontologyRegex.findall(siteContent)
for potentialMatch in ontologyMatch:
if potentialMatch not in allWallets:
allWallets.add(potentialMatch)
returnResults.append([{'Wallet Address': potentialMatch,
'Currency Name': 'Ontology',
'Entity Type': 'Crypto Wallet'},
{currentUID: {'Resolution': 'Potential Ontology Wallet Address',
'Notes': ''}}])
polkadotMatch = polkadotRegex.findall(siteContent)
for potentialMatch in polkadotMatch:
if potentialMatch not in allWallets:
allWallets.add(potentialMatch)
returnResults.append([{'Wallet Address': potentialMatch,
'Currency Name': 'Polkadot',
'Entity Type': 'Crypto Wallet'},
{currentUID: {'Resolution': 'Potential Polkadot Wallet Address',
'Notes': ''}}])
stellarMatch = stellarRegex.findall(siteContent)
for potentialMatch in stellarMatch:
if potentialMatch not in allWallets:
allWallets.add(potentialMatch)
returnResults.append([{'Wallet Address': potentialMatch,
'Currency Name': 'Stellar',
'Entity Type': 'Crypto Wallet'},
{currentUID: {'Resolution': 'Potential Stellar Wallet Address',
'Notes': ''}}])
with sync_playwright() as p:
browser = p.chromium.launch(executable_path=playwrightPath)
context = browser.new_context(
viewport={'width': 1920, 'height': 1080},
user_agent='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/101.0.4951.54 Safari/537.36'
)
for entity in entityJsonList:
uid = entity['uid']
url = entity.get('URL') if entity.get('Entity Type', '') == 'Website' else \
entity.get('Domain Name', None)
if url is None:
continue
if not url.startswith('http://') and not url.startswith('https://'):
url = f'http://{url}'
extractCryptoAddresses(uid, url)
browser.close()
return returnResults

View File

@@ -47,11 +47,10 @@ class DecodePhrase:
if len(text) % 8 != 0:
return "Malformed format not in Octaves"
ascii_string = ''
for binaryIndex in range(0, len(text), 8):
ascii_string += chr(int(text[binaryIndex:binaryIndex + 8], 2))
returnResult.append([{'Phrase': str(ascii_string),
'Entity Type': 'Phrase'},
ascii_string = ''.join(chr(int(text[binaryIndex: binaryIndex + 8], 2))
for binaryIndex in range(0, len(text), 8))
returnResult.append([{'Phrase': ascii_string, 'Entity Type': 'Phrase'},
{uid: {'Resolution': 'Binary Decoded Phrase', 'Notes': ''}}])
return returnResult

View File

@@ -0,0 +1,51 @@
#!/usr/bin/env python3
class DecodeRedirectUrlParameter:
name = "Decode Redirect URL"
category = "Website Information"
description = "Search the URL's parameters to see where you'll be redirected."
originTypes = {'Website'}
resultTypes = {'Website'}
parameters = {}
def resolution(self, entityJsonList, parameters):
import contextlib
from urllib.parse import urlparse
from urllib.parse import parse_qs
from urllib.parse import unquote
from base64 import b64decode
def get_redirect_value(parameter_arg: str) -> str:
with contextlib.suppress(Exception):
clean_val = unquote(parameter_arg)
if urlparse(clean_val).scheme:
return clean_val
clean_val = unquote(b64decode(parameter_arg).decode('UTF-8'))
if urlparse(clean_val).scheme:
return clean_val
return ''
returnResults = []
for entity in entityJsonList:
primaryField = entity['URL'].strip()
parsed_url = urlparse(primaryField, allow_fragments=False)
parsed_url_params = parse_qs(parsed_url.query)
for param, param_value in parsed_url_params.items():
param_potential_url_value = get_redirect_value(', '.join(param_value))
if param_potential_url_value:
parsed_url_params_copy = dict(parsed_url_params)
parsed_url_params_copy.pop(param)
new_entity = {'URL': param_potential_url_value,
'Entity Type': 'Website'}
for param_copy, param_value_copy in parsed_url_params_copy.items():
new_entity[param_copy] = ', '.join(param_value_copy)
returnResults.append([new_entity,
{entity['uid']: {'Resolution': 'Redirect To',
'Notes': ''}}])
break
return returnResults

View File

@@ -0,0 +1,76 @@
#!/usr/bin/env python3
class DeleteColumn:
# A string that is treated as the name of this resolution.
name = "Rename or Delete Column"
category = "Spreadsheet Operations"
# A string that describes this resolution.
description = "Deletes the column with the specified index from a Spreadsheet document."
originTypes = {'Spreadsheet'}
resultTypes = {'Spreadsheet'}
parameters = {'Working Sheet': {'description': 'The name or index of the Sheet to read in the Spreadsheet '
'file. By default, the first Sheet is used.',
'type': 'String',
'value': '0',
'default': '0'},
'Column Name to Rename': {'description': 'Please enter the name of the column that you wish to '
'rename or delete.',
'type': 'String',
'value': ''},
'New Column Name': {'description': 'Please enter the new name for the column.\nEnter the same name '
'to delete the column instead.',
'type': 'String',
'value': ''}
}
def resolution(self, entityJsonList, parameters):
from pathlib import Path
import pandas as pd
import contextlib
workingSheet = parameters['Working Sheet']
with contextlib.suppress(ValueError):
workingSheet = int(workingSheet)
renameColumn = parameters['Column Name to Rename']
targetColumn = parameters['New Column Name']
returnResults = []
for entity in entityJsonList:
uid = entity['uid']
filePath = Path(parameters['Project Files Directory']) / entity['File Path']
if not filePath.exists() or not filePath.is_file():
continue
try:
csvDF = pd.read_excel(filePath, sheet_name=workingSheet)
except ValueError:
continue
if renameColumn == targetColumn:
csvDF.drop(renameColumn, inplace=True)
else:
csvDF.rename(columns={renameColumn: targetColumn}, inplace=True)
count = 0
while True:
newFileName = f"{filePath.name.split(filePath.suffix, 1)[0]}-c{count}{filePath.suffix}"
newFilePath = filePath.parent / newFileName
if not newFilePath.exists():
break
csvDF.to_excel(newFilePath, index=False)
returnResults.append([{'Spreadsheet Name': newFileName,
'File Path': newFileName,
'Entity Type': 'Spreadsheet'},
{uid: {'Resolution': 'Rename/Delete Column',
'Notes': ''}}])
return returnResults

View File

@@ -12,6 +12,7 @@ class DomainFromPhrase:
def resolution(self, entityJsonList, parameters):
import re
import contextlib
import tldextract
domainRegex = re.compile(
@@ -30,14 +31,11 @@ class DomainFromPhrase:
while wordChar.match(entityChunk[-1]) is None:
entityChunk = entityChunk[:-1]
if domainRegex.match(entityChunk):
try:
with contextlib.suppress(Exception):
tldObject = tldextract.extract(entityChunk)
if tldObject.suffix != '':
returnResults.append([{'Domain Name': entityChunk,
'Entity Type': 'Domain'},
{entity['uid']: {'Resolution': 'Phrase To Domain',
'Notes': ''}}])
except Exception:
pass
return returnResults

View File

@@ -37,18 +37,22 @@ class EmailExtractor:
def resolution(self, entityJsonList, parameters):
from playwright.sync_api import sync_playwright, TimeoutError, Error
from bs4 import BeautifulSoup
from pathlib import Path
import re
import contextlib
from email_validator import validate_email, caching_resolver, EmailNotValidError
playwrightPath = Path(parameters['Playwright Chromium'])
returnResults = []
# Source: https://emailregex.com/
# Alt: (?:[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+(\.([a-zA-Z0-9-])+)+)
emailRegex = re.compile(r"""(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])""")
useRegex = True if parameters['Use Regex'] == 'Yes' else False
emailRegex = re.compile(
r"""(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])""")
useRegex = parameters['Use Regex'] == 'Yes'
resolver = caching_resolver(timeout=10)
verifyDomain = True if parameters['Verify Email Domain Validity'] == 'Yes' else False
verifyDomain = parameters['Verify Email Domain Validity'] == 'Yes'
# The software can deduplicate, but handling it here is better.
allEmails = set()
@@ -89,42 +93,38 @@ class EmailExtractor:
siteContent = re.sub(r'\s*(\[|\<|\()+\s*at\s*(\]|\>|\))+\s*', '@', siteContent)
siteContent = re.sub(r'\s*(\[|\<|\()+\s*dot\s*(\]|\>|\))+\s*', '.', siteContent)
siteContent = re.sub(r'\s*(\[|\<|\()+\s*\.\s*(\]|\>|\))+\s*', '.', siteContent)
siteContent = re.sub(r'\s*@\s*', '@', siteContent)
siteContent = re.sub(r'\s*\.', '.', siteContent)
siteContent = re.sub(r'\.\s*([^A-Z])', r'.\1', siteContent)
potentialEmails = emailRegex.findall(siteContent)
for potentialEmail in potentialEmails:
try:
with contextlib.suppress(EmailNotValidError):
valid = validate_email(potentialEmail, dns_resolver=resolver, check_deliverability=verifyDomain)
if valid.email not in allEmails:
allEmails.add(valid.email)
returnResults.append([{'Email Address': valid.email,
if valid.normalized not in allEmails:
allEmails.add(valid.normalized)
returnResults.append([{'Email Address': valid.normalized,
'Entity Type': 'Email Address'},
{currentUID: {'Resolution': 'Email Address Found',
'Notes': ''}}])
except EmailNotValidError:
pass
linksInAHref = soupContents.find_all('a')
for tag in linksInAHref:
newLink = tag.get('href', None)
if newLink is not None:
if newLink.startswith('mailto:'):
try:
valid = validate_email(newLink[7:], dns_resolver=resolver,
check_deliverability=verifyDomain)
if valid.email not in allEmails:
allEmails.add(valid.email)
returnResults.append([{'Email Address': valid.email,
'Entity Type': 'Email Address'},
{currentUID: {'Resolution': 'Email Address Found',
'Notes': ''}}])
except EmailNotValidError:
pass
if newLink is not None and newLink.startswith('mailto:'):
with contextlib.suppress(EmailNotValidError):
valid = validate_email(newLink[7:], dns_resolver=resolver,
check_deliverability=verifyDomain)
if valid.normalized not in allEmails:
allEmails.add(valid.normalized)
returnResults.append([{'Email Address': valid.normalized,
'Entity Type': 'Email Address'},
{currentUID: {'Resolution': 'Email Address Found',
'Notes': ''}}])
with sync_playwright() as p:
browser = p.chromium.launch()
browser = p.chromium.launch(executable_path=playwrightPath)
context = browser.new_context(
viewport={'width': 1920, 'height': 1080},
user_agent='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/101.0.4951.54 Safari/537.36'
viewport={'width': 1920, 'height': 1080}
)
for entity in entityJsonList:
uid = entity['uid']
@@ -133,7 +133,7 @@ class EmailExtractor:
if url is None:
continue
if not url.startswith('http://') and not url.startswith('https://'):
url = 'http://' + url
url = f'http://{url}'
extractEmails(uid, url)
browser.close()

View File

@@ -11,18 +11,16 @@ class EmailToDomain:
parameters = {}
def resolution(self, entityJsonList, parameters):
import contextlib
returnResults = []
for entity in entityJsonList:
primaryField = entity['Email Address']
# There is no provider that I am aware of that allows '@' signs in the user part of the email.
try:
with contextlib.suppress(Exception):
returnResults.append([{'Domain Name': primaryField.split('@')[1].strip(),
'Entity Type': 'Domain'},
{entity['uid']: {'Resolution': 'Email To Domain',
'Notes': ''}}])
except Exception:
pass
return returnResults

View File

@@ -26,7 +26,7 @@ class ExtractDOCXMeta:
uid = entity['uid']
filePath = Path(parameters['Project Files Directory']) / entity['File Path']
if not (filePath.exists() and filePath.is_file()):
if not filePath.exists() or not filePath.is_file():
continue
if magic.from_file(str(filePath), mime=True) != \
@@ -49,9 +49,8 @@ class ExtractDOCXMeta:
'Entity Type': 'Date'},
{uid: {'Resolution': 'created', 'Notes': ''}}])
for metadataKey in [dataKey for dataKey in data if dataKey not in defaultDateProperties]:
returnResults.append([{'Phrase': metadataKey + ': ' + str(data.get(metadataKey)),
'Entity Type': 'Phrase'},
{uid: {'Resolution': metadataKey, 'Notes': ''}}])
returnResults.extend([{'Phrase': f'{metadataKey}: {str(data.get(metadataKey))}', 'Entity Type': 'Phrase'},
{uid: {'Resolution': metadataKey, 'Notes': ''}}] for metadataKey in
[dataKey for dataKey in data if dataKey not in defaultDateProperties])
return returnResults

View File

@@ -7,7 +7,7 @@ class ExtractPDFMeta:
category = "File Operations"
# A string that describes this resolution.
description = "Returns a set of nodes that contain all the metadata info of pdf files."
description = "Returns a set of nodes that contain notable metadata info of pdf files."
originTypes = {'Document'}
@@ -16,7 +16,8 @@ class ExtractPDFMeta:
parameters = {}
def resolution(self, entityJsonList, parameters):
from PyPDF2 import PdfFileReader
from pypdf import PdfReader
from datetime import datetime, timedelta
import magic
from pathlib import Path
@@ -26,44 +27,55 @@ class ExtractPDFMeta:
uid = entity['uid']
filePath = Path(parameters['Project Files Directory']) / entity['File Path']
if not (filePath.exists() and filePath.is_file()):
if not (filePath.is_file()):
continue
if magic.from_file(str(filePath), mime=True) != \
'application/pdf':
if magic.from_file(str(filePath), mime=True) != 'application/pdf':
continue
with open(filePath, 'rb') as f:
pdf = PdfFileReader(f)
info = pdf.getDocumentInfo()
number_of_pages = pdf.getNumPages()
pdf = PdfReader(f)
info = pdf.metadata
number_of_pages = len(pdf.pages)
for metadataKey in info:
attrValue = metadataKey[1:] if metadataKey.startswith('/') else metadataKey
if 'Date' in metadataKey:
try:
strDate = info[metadataKey]
strDate = strDate.split(':')[1].split('-')[0]
strDate1 = strDate[:-6]
strDate2 = strDate[-6:]
strDate2 = ':'.join(strDate2[i:i+2] for i in range(0, 6, 2))
strDate1 = strDate1[:-4] + '-' + '-'.join(strDate1[::-1][i:i+2] for i in range(0, 4, 2))[::-1]
strDate = strDate1 + 'T' + strDate2
returnResults.append([{'Date': strDate,
strDate = info[metadataKey].split(':', 1)[1]
if strDate.endswith('Z'):
dateString = datetime.strptime(strDate, "%Y%m%d%H%M%SZ").isoformat()
elif '+' in strDate:
datePart1, datePart2 = strDate.split('+', 1)
date1 = datetime.strptime(datePart1, "%Y%m%d%H%M%S")
date2 = timedelta(hours=int(datePart2.split("'")[0]), minutes=int(datePart2.split("'")[1]))
dateString = (date1 + date2).isoformat()
elif '-' in strDate:
datePart1, datePart2 = strDate.split('-', 1)
date1 = datetime.strptime(datePart1, "%Y%m%d%H%M%S")
date2 = timedelta(hours=int(datePart2.split("'")[0]), minutes=int(datePart2.split("'")[1]))
dateString = (date1 - date2).isoformat()
else:
raise ValueError('Cannot parse Date format.')
returnResults.append([{'Date': dateString,
'Entity Type': 'Date'},
{uid: {'Resolution': metadataKey, 'Notes': ''}}])
{uid: {'Resolution': attrValue, 'Notes': ''}}])
except Exception:
# Reset strDate to default value
strDate = info[metadataKey]
returnResults.append([{'Date': strDate,
'Entity Type': 'Date'},
{uid: {'Resolution': metadataKey, 'Notes': ''}}])
{uid: {'Resolution': attrValue, 'Notes': ''}}])
else:
returnResults.append([{'Phrase': metadataKey + ': ' + str(info[metadataKey]),
# Clean some misshapen strings
value = str(info[metadataKey])
value = value.removeprefix('/')
returnResults.append([{'Phrase': f'{attrValue}: {value}',
'Entity Type': 'Phrase'},
{uid: {'Resolution': metadataKey, 'Notes': ''}}])
{uid: {'Resolution': attrValue, 'Notes': ''}}])
returnResults.append([{'Phrase': 'Number of Pages: ' + str(number_of_pages),
'Entity Type': 'Phrase'},
returnResults.append([{'Phrase': f'Number of Pages: {number_of_pages}', 'Entity Type': 'Phrase'},
{uid: {'Resolution': 'Number of Pages', 'Notes': ''}}])
return returnResults

View File

@@ -26,7 +26,7 @@ class FileExtractor:
originTypes = {'Domain', 'Website'}
resultTypes = {'Website', 'Document', 'Image', 'Video', 'Archive'}
resultTypes = {'Website', 'Document', 'Spreadsheet', 'Image', 'Video', 'Archive'}
parameters = {'Max Depth': {'description': 'Each link leading to another website in the same domain can be '
'explored to discover more entities. Each entity discovered after '
@@ -41,27 +41,30 @@ class FileExtractor:
'default': '0'}}
def resolution(self, entityJsonList, parameters):
import contextlib
import tldextract
import requests
from hashlib import md5
from binascii import hexlify
from pathlib import Path
from bs4 import BeautifulSoup
from playwright.sync_api import sync_playwright, TimeoutError, Error
playwrightPath = Path(parameters['Playwright Firefox'])
try:
maxDepth = max(int(parameters['Max Depth']), 0)
except ValueError:
return "Invalid value provided for Max Webpages to follow."
fileTypes = (".sxw", ".odt", ".ods", ".odg", ".odp", ".docx", ".xlsx", ".pptx", ".ppsx", ".doc", ".xls",
fileTypes = (".sxw", ".odt", ".odg", ".odp", ".docx", ".pptx", ".ppsx", ".doc", ".csv",
".ppt", ".pps", ".pdf", ".wpd", ".raw", ".cr2", ".crw", ".indd", ".rdp", ".ica", ".ico", ".txt",
".text", ".bak", ".log", ".env", ".pub", ".docm", ".xlsm", ".old", ".csv", ".apk", ".sql", ".cfg",
".text", ".bak", ".log", ".env", ".pub", ".docm", ".old", ".apk", ".sql", ".cfg",
".key", ".reg", ".yml", ".yaml", ".mail", ".eml", ".mbox", ".mbx", ".url", ".csr", ".config",
".mdb", ".user", ".adr", ".ini", ".plist", ".conf", ".dat", ".pcf", ".bok", ".properties", ".json",
".backup", ".sh", ".py", ".md", ".inc")
videoTypes = (".mp3", ".mp4")
imageTypes = (".jpg", ".jpeg", ".png", ".svg", ".svgz")
".backup", ".sh", ".py", ".md", ".inc", '.ovpn', '.bat')
spreadsheetTypes = (".xlsx", ".xls", ".ods", ".xlsm")
videoTypes = (".mp3", ".mp4", ".mov", ".webm", ".amv")
imageTypes = (".jpg", ".jpeg", ".png", ".svg", ".svgz", ".bmp")
archiveTypes = (".zip", ".rar", ".7z", ".gz")
returnResults = []
@@ -87,7 +90,7 @@ class FileExtractor:
if link is not None:
if not link.startswith('http'):
# We assume that we will be redirected to https if available.
link = 'http://' + domain + link
link = f'http://{domain}{link}'
link = link.split('#')[0]
if link not in urlsExplored:
urlsExplored.add(link)
@@ -103,6 +106,8 @@ class FileExtractor:
fileTypeIdentified = 'Image'
elif link.endswith(archiveTypes):
fileTypeIdentified = 'Archive'
elif link.endswith(spreadsheetTypes):
fileTypeIdentified = 'Spreadsheet'
if fileTypeIdentified:
childIndex = len(returnResults)
@@ -113,10 +118,10 @@ class FileExtractor:
'Notes': ''}}])
docProperName = link.split('/')[-1]
docFileName = hexlify(md5(link.encode()).digest()).decode() + ' | ' + docProperName # nosec
docFileName = f'{md5(link.encode("UTF-8")).hexdigest()}_{docProperName}'
docFullPath = Path(parameters['Project Files Directory']) / docFileName
try:
with contextlib.suppress(Exception):
response = requests.get(link,
headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; '
'x64; rv:94.0) Gecko/20100101 '
@@ -126,13 +131,10 @@ class FileExtractor:
for chunk in response.iter_content(4096):
fileToWrite.write(chunk)
returnResults.append([{fileTypeIdentified + ' Name': docProperName,
returnResults.append([{f'{fileTypeIdentified} Name': docProperName,
'File Path': docFileName,
'Entity Type': fileTypeIdentified},
{childIndex: {'Resolution': 'Downloaded File',
'Notes': ''}}])
except Exception:
pass
{childIndex: {'Resolution': 'Downloaded File', 'Notes': ''}}])
elif domain in link:
urlsToExplore.add(link)
@@ -143,7 +145,7 @@ class FileExtractor:
if link is not None:
if not link.startswith('http'):
# We assume that we will be redirected to https if available.
link = 'http://' + domain + link
link = f'http://{domain}{link}'
link = link.split('#')[0]
if link not in urlsExplored:
urlsExplored.add(link)
@@ -155,10 +157,10 @@ class FileExtractor:
{uid: {'Resolution': 'File URL',
'Notes': ''}}])
docProperName = link.split('/')[-1]
docFileName = hexlify(md5(link.encode()).digest()).decode() + ' | ' + docProperName # nosec
docFileName = f'{md5(link.encode()).hexdigest()}_{docProperName}'
docFullPath = Path(parameters['Project Files Directory']) / docFileName
try:
with contextlib.suppress(Exception):
response = requests.get(link,
headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; '
'x64; rv:94.0) Gecko/20100101 '
@@ -173,16 +175,13 @@ class FileExtractor:
'Entity Type': 'Image'},
{childIndex: {'Resolution': 'Downloaded File',
'Notes': ''}}])
except Exception:
pass
if currentDepth > 0:
newDepth = currentDepth - 1
for newURL in urlsToExplore:
iterateOnDepth(newURL, newDepth)
with sync_playwright() as p:
browser = p.firefox.launch()
browser = p.firefox.launch(executable_path=playwrightPath)
context = browser.new_context(
viewport={'width': 1920, 'height': 1080},
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0'
@@ -194,7 +193,7 @@ class FileExtractor:
if url is None:
continue
if not url.startswith('http://') and not url.startswith('https://'):
url = 'http://' + url
url = f'http://{url}'
domain = tldextract.extract(url).fqdn
# Because these do not persist across entities, it is possible to explore a URL multiple times.

View File

@@ -5,30 +5,30 @@ class FileHasher:
name = "Get File Hash"
category = "File Operations"
description = "Get the Hash of a file."
originTypes = {"Image", "Document", "Video", "Archive", "Disk"}
originTypes = {"Image", "Document", "Spreadsheet", "Video", "Archive", "Disk"}
resultTypes = {'Hash'}
parameters = {'hashing_algorithms': {'description': 'The type of hash/es that will be returned',
'type': 'MultiChoice',
'value': {'SHA1', 'SHA256', 'MD5'}
}}
parameters = {'Hashing Algorithm': {'description': 'Choose the type of hash(es) that you want to be returned:',
'type': 'MultiChoice',
'value': {'SHA1', 'SHA256', 'MD5'}
}}
def resolution(self, entityJsonList, parameters):
import hashlib
from pathlib import Path
return_result = []
hashing_algorithms = parameters['hashing_algorithms']
hashing_algorithms = parameters['Hashing Algorithm']
for entity in entityJsonList:
uid = entity['uid']
file_path = Path(parameters['Project Files Directory']) / entity['File Path']
if not (file_path.exists() and file_path.is_file()):
if not file_path.is_file():
continue
block_size = 65536 # The size of each read from the file
for hashing_algorithm in hashing_algorithms:
if hashing_algorithm == "SHA256":
file_hash = hashlib.sha256() # nosec
elif hashing_algorithm == "SHA1":
if hashing_algorithm == "SHA1":
file_hash = hashlib.sha1() # nosec
elif hashing_algorithm == "SHA256":
file_hash = hashlib.sha256() # nosec
else:
file_hash = hashlib.md5() # nosec
with open(file_path, 'rb') as f:
@@ -40,5 +40,5 @@ class FileHasher:
return_result.append([{'Hash Value': resulting_hash,
'Hash Algorithm': hashing_algorithm,
'Entity Type': 'Hash'},
{uid: {'Resolution': hashing_algorithm + ' Hash', 'Notes': ''}}])
{uid: {'Resolution': f'{hashing_algorithm} Hash', 'Notes': ''}}])
return return_result

View File

@@ -39,8 +39,14 @@ class GetExternalURLs:
from playwright.sync_api import sync_playwright, TimeoutError, Error
from bs4 import BeautifulSoup
import re
import urllib.parse
import contextlib
from urllib.parse import urlparse
from urllib.parse import parse_qs
from urllib.parse import unquote
from base64 import b64decode
from pathlib import Path
playwrightPath = Path(parameters['Playwright Chromium'])
onionRegex = re.compile(r"""^https?://\w{56}\.onion/?(\S(?<!\.))*(\.(\S(?<!\.))*)?$""")
returnResult = []
@@ -48,16 +54,23 @@ class GetExternalURLs:
extract_img = '<img> elements' in parameters['Element types to check']
extract_link = '<link> elements' in parameters['Element types to check']
# Sites like youtube replace external links with a redirect link originating
# from the site itself. This sort of gets around that.
redirectRegex = re.compile(r'\?.*(q|url)=\S[^&#?]+', re.IGNORECASE)
def get_potential_redirect_value(potential_redirect_url: str) -> str:
parsed_url = urlparse(potential_redirect_url, allow_fragments=False)
parsed_url_params = parse_qs(parsed_url.query)
for param, param_value in parsed_url_params.items():
with contextlib.suppress(Exception):
clean_val = unquote(', '.join(param_value))
if urlparse(clean_val).scheme:
return clean_val
clean_val = unquote(b64decode(', '.join(param_value)).decode('UTF-8'))
if urlparse(clean_val).scheme:
return clean_val
return ''
with sync_playwright() as p:
browser = p.chromium.launch()
browser = p.chromium.launch(executable_path=playwrightPath)
context = browser.new_context(
viewport={'width': 1920, 'height': 1080},
user_agent='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/101.0.4951.64 Safari/537.36'
viewport={'width': 1920, 'height': 1080}
)
page = context.new_page()
externalUrls = {}
@@ -65,7 +78,7 @@ class GetExternalURLs:
for site in entityJsonList:
uid = site['uid']
url = site['URL']
parsedURL = urllib.parse.urlparse(url)
parsedURL = urlparse(url)
if not all([parsedURL.scheme, parsedURL.netloc]):
continue
domain = tldextract.extract(url).fqdn
@@ -75,62 +88,57 @@ class GetExternalURLs:
for _ in range(3):
try:
page.goto(url, wait_until="networkidle", timeout=10000)
### Youtube
with contextlib.suppress(Exception):
page.get_by_role("button",
name="Reject the use of cookies and other data for the purposes described").click()
with contextlib.suppress(Exception):
page.get_by_role("button", name="Show more").click()
soupContents = BeautifulSoup(page.content(), 'lxml')
if extract_a:
linksInAHref = soupContents.find_all('a')
for tag in linksInAHref:
link = tag.get('href', None)
parsedURL = urllib.parse.urlparse(link)
parsedURL = urlparse(link)
if all([parsedURL.scheme, parsedURL.netloc]):
if domain not in link:
if domain in link:
redirectLink = get_potential_redirect_value(link)
if redirectLink:
try:
externalUrls[redirectLink].add(uid)
except KeyError:
externalUrls[redirectLink] = {uid}
else:
newLink = link.split('#')[0].split('?')[0]
try:
externalUrls[newLink].add(uid)
except KeyError:
externalUrls[newLink] = {uid}
else:
redirectLinks = redirectRegex.findall(link)
if 'redirect' in link and len(redirectLinks) > 0:
try:
newLink = str(urllib.parse.unquote(redirectLinks[0]))[2:]
try:
externalUrls[newLink].add(uid)
except KeyError:
externalUrls[newLink] = {uid}
except IndexError:
try:
externalUrls[redirectLinks[0]].add(uid)
except KeyError:
externalUrls[redirectLinks[0]] = {uid}
except Exception:
pass
if extract_img:
linksInImgSrc = soupContents.find_all('img')
for tag in linksInImgSrc:
link = tag.get('src', None)
parsedURL = urllib.parse.urlparse(link)
if all([parsedURL.scheme, parsedURL.netloc]):
if domain not in link:
newLink = link.split('#')[0].split('?')[0]
try:
externalUrls[newLink].add(uid)
except KeyError:
externalUrls[newLink] = {uid}
parsedURL = urlparse(link)
if all([parsedURL.scheme, parsedURL.netloc]) and domain not in link:
newLink = link.split('#')[0].split('?')[0]
try:
externalUrls[newLink].add(uid)
except KeyError:
externalUrls[newLink] = {uid}
if extract_link:
linksInLinkHref = soupContents.find_all('link')
for tag in linksInLinkHref:
link = tag.get('href', None)
parsedURL = urllib.parse.urlparse(link)
if all([parsedURL.scheme, parsedURL.netloc]):
if domain not in link:
newLink = link.split('#')[0].split('?')[0]
try:
externalUrls[newLink].add(uid)
except KeyError:
externalUrls[newLink] = {uid}
parsedURL = urlparse(link)
if all([parsedURL.scheme, parsedURL.netloc]) and domain not in link:
newLink = link.split('#')[0].split('?')[0]
try:
externalUrls[newLink].add(uid)
except KeyError:
externalUrls[newLink] = {uid}
break
except TimeoutError:
pass
@@ -143,12 +151,11 @@ class GetExternalURLs:
for externalUrl in externalUrls:
onionCheck = onionRegex.findall(externalUrl)
if len(onionCheck) == 1:
for urlUid in externalUrls[externalUrl]:
for urlUid in externalUrls[externalUrl]:
if len(onionCheck) == 1:
returnResult.append([{'Onion URL': externalUrl, 'Entity Type': 'Onion Website'},
{urlUid: {'Resolution': 'External Link', 'Notes': ''}}])
else:
for urlUid in externalUrls[externalUrl]:
else:
returnResult.append([{'URL': externalUrl, 'Entity Type': 'Website'},
{urlUid: {'Resolution': 'External Link', 'Notes': ''}}])

View File

@@ -19,12 +19,14 @@ class GetInternalURLs:
import tldextract
from playwright.sync_api import sync_playwright, TimeoutError, Error
from bs4 import BeautifulSoup
from pathlib import Path
import urllib.parse
playwrightPath = Path(parameters['Playwright Chromium'])
returnResult = []
internalUrls = {}
considerResources = False if parameters['Include Resources'] == 'Only consider links to pages' else True
considerResources = parameters['Include Resources'] != 'Only consider links to pages'
def handleLink(currentLink, currentUrl, currentDomain):
if currentLink is None:
@@ -35,17 +37,19 @@ class GetInternalURLs:
currentLink = currentUrl + currentLink[1:]
elif currentLink.startswith('/'):
urlParts = urllib.parse.urlparse(currentUrl)
currentLink = urlParts.scheme + '://' + urlParts.netloc + currentLink
currentLink = f'{urlParts.scheme}://{urlParts.netloc}{currentLink}'
parsedCurrentURL = urllib.parse.urlparse(currentLink)
if all([parsedCurrentURL.scheme, parsedCurrentURL.netloc]):
if currentDomain in currentLink:
newLink = currentLink.split('#')[0].split('?')[0]
if newLink.endswith('/'):
newLink = newLink[:-1]
return newLink
if (
all([parsedCurrentURL.scheme, parsedCurrentURL.netloc])
and currentDomain in currentLink
):
newLink = currentLink.split('#')[0].split('?')[0]
if newLink.endswith('/'):
newLink = newLink[:-1]
return newLink
with sync_playwright() as p:
browser = p.chromium.launch()
browser = p.chromium.launch(executable_path=playwrightPath)
context = browser.new_context(
viewport={'width': 1920, 'height': 1080},
user_agent='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) '

View File

@@ -21,15 +21,15 @@ class GetWebsiteText:
from bs4.element import Comment
from playwright.sync_api import sync_playwright, TimeoutError, Error
from urllib.parse import urlparse
from pathlib import Path
returnResults = []
playwrightPath = Path(parameters['Playwright Chromium'])
def tag_visible(element):
if element.parent.name in ['style', 'script', 'head', 'title', 'meta', '[document]']:
return False
if isinstance(element, Comment):
return False
return True
return not isinstance(element, Comment)
def text_from_html(body):
soup = BeautifulSoup(body, 'lxml')
@@ -38,7 +38,7 @@ class GetWebsiteText:
return u" ".join(t.strip() for t in visible_texts if t.strip() != '')
with sync_playwright() as p:
browser = p.chromium.launch()
browser = p.chromium.launch(executable_path=playwrightPath)
context = browser.new_context(
viewport={'width': 1920, 'height': 1080},
user_agent='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) '
@@ -59,7 +59,7 @@ class GetWebsiteText:
try:
page.goto(url, wait_until="networkidle", timeout=10000)
textContent = text_from_html(page.content())
returnResults.append([{'Phrase': 'Website Body of: ' + url,
returnResults.append([{'Phrase': f'Website Body of: {url}',
'Notes': textContent,
'Entity Type': 'Phrase'},
{uid: {'Resolution': 'Website Body', 'Notes': ''}}])

View File

@@ -16,8 +16,7 @@ class HostnameToDomain:
for entity in entityJsonList:
uid = entity['uid']
primary_field = entity[list(entity)[1]].strip()
tsd, td, tsu = extract(primary_field)
domain = td + '.' + tsu
domain = extract(primary_field).fqdn
if domain == primary_field:
continue
return_result.append([{'Domain Name': domain,

View File

@@ -35,19 +35,19 @@ class IPToASN:
index_of_child = len(returnResult)
countryCode = results['asn_country_code']
country = pycountry.countries.get(alpha_2=countryCode).name
returnResult.append([{'AS Number': "AS" + results['asn'],
'ASN Cidr': results['asn_cidr'],
'Date Created': results['asn_date'],
'Entity Type': 'Autonomous System'},
{uid: {'Resolution': 'Autonomous System of IP', 'Notes': ''}}])
returnResult.append(
[{'Organization Name': results['asn_registry'], 'Entity Type': 'Organization'},
{index_of_child: {'Resolution': 'ASN Registry', 'Notes': ''}}])
returnResult.append(
[{'Country Name': country, 'Entity Type': 'Country'},
{index_of_child: {'Resolution': 'Country of Registry for ASN', 'Notes': ''}}])
returnResult.append(
[{'Phrase': results['asn_description'], 'Entity Type': 'Phrase'},
{index_of_child: {'Resolution': 'ASN Description', 'Notes': ''}}])
returnResult.extend(([{'AS Number': "AS" + results['asn'],
'ASN Cidr': results['asn_cidr'],
'Date Created': results['asn_date'],
'Entity Type': 'Autonomous System'},
{uid: {'Resolution': 'Autonomous System of IP', 'Notes': ''}}],
[{'Organization Name': results['asn_registry'],
'Entity Type': 'Organization'},
{index_of_child: {'Resolution': 'ASN Registry', 'Notes': ''}}],
[{'Country Name': country,
'Entity Type': 'Country'},
{index_of_child: {'Resolution': 'Country of Registry for ASN', 'Notes': ''}}],
[{'Phrase': results['asn_description'],
'Entity Type': 'Phrase'},
{index_of_child: {'Resolution': 'ASN Description', 'Notes': ''}}]))
return returnResult

View File

@@ -31,8 +31,7 @@ class IPWhois:
{uid: {'Resolution': 'IPWhois', 'Notes': ''}}])
for net in response['nets']:
if net['country'] is not None:
country = pycountry.countries.get(alpha_2=net['country'])
if country:
if country := pycountry.countries.get(alpha_2=net['country']):
country_name = country.name
else:
# May not always be an actual Country.
@@ -45,8 +44,6 @@ class IPWhois:
'Entity Type': 'Company'},
{uid: {'Resolution': 'IPWhois', 'Notes': ''}}])
if net['emails'] is not None:
for email in net['emails']:
return_result.append([{'Email Address': email,
'Entity Type': 'Email Address'},
{uid: {'Resolution': 'IPWhois', 'Notes': ''}}])
return_result.extend([{'Email Address': email, 'Entity Type': 'Email Address'},
{uid: {'Resolution': 'IPWhois', 'Notes': ''}}] for email in net['emails'])
return return_result

View File

@@ -18,22 +18,21 @@ class ImageToDevice:
uid = entity['uid']
index_of_child = len(return_result)
image_path = Path(parameters['Project Files Directory']) / entity['File Path']
if not (image_path.exists() and image_path.is_file()):
if not image_path.exists() or not image_path.is_file():
continue
with open(image_path, 'rb') as image_file:
my_image = Image(image_file)
if my_image.has_exif is False:
continue
else:
for tag in my_image.list_all():
if tag == "make":
return_result.append([{'Phrase': my_image.make,
'Entity Type': 'Phrase'},
{uid: {'Resolution': 'ExifMetadata Device Manufacturer',
'Notes': ''}}])
if tag == "model":
return_result.append([{'Phrase': my_image.model,
'Entity Type': 'Phrase'},
{index_of_child: {'Resolution': 'ExifMetadata Device Model',
'Notes': ''}}])
for tag in my_image.list_all():
if tag == "make":
return_result.append([{'Phrase': my_image.make,
'Entity Type': 'Phrase'},
{uid: {'Resolution': 'ExifMetadata Device Manufacturer',
'Notes': ''}}])
elif tag == "model":
return_result.append([{'Phrase': my_image.model,
'Entity Type': 'Phrase'},
{index_of_child: {'Resolution': 'ExifMetadata Device Model',
'Notes': ''}}])
return return_result

View File

@@ -17,18 +17,17 @@ class ImageToGeoLocation:
for entity in entityJsonList:
uid = entity['uid']
image_path = Path(parameters['Project Files Directory']) / entity['File Path']
if not (image_path.exists() and image_path.is_file()):
if not image_path.exists() or not image_path.is_file():
continue
with open(image_path, 'rb') as image_file:
my_image = Image(image_file)
if my_image.has_exif is False:
continue
else:
for tag in my_image.list_all():
if tag == "gps_latitude":
return_result.append([{'Label': "Location of"+str(entity[list(entity)[1]].strip()),
'Latitude': my_image.gps_latitude,
'Longitude': my_image.gps_longitude,
'Entity Type': 'GeoCoordinates'},
{uid: {'Resolution': 'GeoCoordinates', 'Notes': ''}}])
return_result.extend([{'Label': f"Location of {str(entity[list(entity)[1]].strip())}",
'Latitude': my_image.gps_latitude,
'Longitude': my_image.gps_longitude,
'Entity Type': 'GeoCoordinates'},
{uid: {'Resolution': 'GeoCoordinates', 'Notes': ''}}]
for tag in my_image.list_all() if tag == "gps_latitude")
return return_result

View File

@@ -19,7 +19,11 @@ class JSCodeExtractor:
def resolution(self, entityJsonList, parameters):
from playwright.sync_api import sync_playwright, Error
from base64 import b64decode
from pathlib import Path
import re
import contextlib
playwrightPath = Path(parameters['Playwright Firefox'])
returnResults = []
requestUrlsParsed = set()
@@ -27,7 +31,7 @@ class JSCodeExtractor:
pubRegex = re.compile(r'\bca-pub-\d{1,16}\b', re.IGNORECASE)
gtmRegex = re.compile(r'\bGTM-[A-Z\d]{1,7}\b')
gRegex = re.compile(r'\bG-[A-Z\d]{1,15}\b', re.IGNORECASE)
qualtricsRegex = re.compile(r'\bQ_(?:Z|S)ID=\w*\b')
qualtricsRegex = re.compile(r'\bQ_[ZS]ID=\w*\b')
pingdomRegex = re.compile(r'\bpa-[a-fA-F\d]{24}.js\b$')
mPulseRegex = re.compile(r'go-mpulse.net/boomerang/[A-Z\d]{5}(?:-[A-Z\d]{5}){4}\b')
contextWebRegex = re.compile(r'\.contextweb\.com.*token=.*')
@@ -48,129 +52,130 @@ class JSCodeExtractor:
brightcoveRegex = re.compile(r'metrics\.brightcove\.com/.*/tracker\?.*&account=[^&]*')
def GetTrackingCodes(pageUid, requestUrl) -> None:
if requestUrl not in requestUrlsParsed:
requestUrlsParsed.add(requestUrl)
for uaCode in uaRegex.findall(requestUrl):
returnResults.append([{'Phrase': uaCode,
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Google UA Tracking Code',
'Notes': ''}}])
for pubCode in pubRegex.findall(requestUrl):
returnResults.append([{'Phrase': pubCode,
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Google AdSense ca-pub Tracking Code',
'Notes': ''}}])
for gtmCode in gtmRegex.findall(requestUrl):
returnResults.append([{'Phrase': gtmCode,
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Google GTM Tracking Code',
'Notes': ''}}])
for gCode in gRegex.findall(requestUrl):
returnResults.append([{'Phrase': gCode,
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Google G Tracking Code',
'Notes': ''}}])
for qCode in qualtricsRegex.findall(requestUrl):
returnResults.append([{'Phrase': qCode[6:],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Qualtrics Tracking Code',
'Notes': ''}}])
for pCode in pingdomRegex.findall(requestUrl):
returnResults.append([{'Phrase': pCode[:-3],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Pingdom Tracking Code',
'Notes': ''}}])
for mCode in mPulseRegex.findall(requestUrl):
returnResults.append([{'Phrase': mCode.split('/')[-1],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'mPulse Tracking Code',
'Notes': ''}}])
for cCode in contextWebRegex.findall(requestUrl):
returnResults.append([{'Phrase': cCode.split('token=')[1].split('&')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'ContextWeb Tracking Code',
'Notes': ''}}])
for fCode in facebookRegex.findall(requestUrl):
returnResults.append([{'Phrase': fCode.split('id=')[1].split('&')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Facebook Tracking Pixel Code',
'Notes': ''}}])
for mapsCode in googleMapsRegex.findall(requestUrl):
returnResults.append([{'Phrase': mapsCode.split('client=', 1)[1].split('&')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Google Maps Client Code',
'Notes': ''}}])
for marketoCode in marketoRegex.findall(requestUrl):
returnResults.append([{'Phrase': marketoCode.split('aid=')[1].split('&')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Marketo Tracking Code',
'Notes': ''}}])
for vwoCode in visualWebsiteOptimizerRegex.findall(requestUrl):
returnResults.append([{'Phrase': vwoCode.split('a=')[1].split('&')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Visual Website Optimizer Tracking User ID',
'Notes': ''}}])
for oCode in optimizeRegex.findall(requestUrl):
returnResults.append([{'Phrase': oCode.split('id=')[1].split('&')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Google Optimize ID',
'Notes': ''}}])
for mmCode in markMonitorRegex.findall(requestUrl):
returnResults.append([{'Phrase': mmCode.split('adv=')[1].split('&')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Mark Monitor Tracking ID',
'Notes': ''}}])
for zCode in zendeskRegex.findall(requestUrl):
returnResults.append([{'Phrase': zCode.split('key=')[1].split('&')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Zendesk ID',
'Notes': ''}}])
for qsCode in quantServeRegex.findall(requestUrl):
returnResults.append([{'Phrase': qsCode.split('/pixel/')[1].split('.gif')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'QuantServe Tracking Pixel ID',
'Notes': ''}}])
for clCode in cookieLawRegex.findall(requestUrl):
returnResults.append([{'Phrase': clCode.split('/')[2],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'CookieLaw Website ID',
'Notes': ''}}])
for otCode in oneTagRegex.findall(requestUrl):
returnResults.append([{'Phrase': otCode.split('/')[1],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'OneTag Tracking ID',
'Notes': ''}}])
for beCode in bounceExchangeRegex.findall(requestUrl):
returnResults.append([{'Phrase': beCode.split('/')[1],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'BounceExchange Tracking ID',
'Notes': ''}}])
for pushlyCode in pushlyRegex.findall(requestUrl):
returnResults.append([{'Phrase': pushlyCode.split('domain_key=')[1],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Pushly Website ID',
'Notes': ''}}])
for aCode in akamaiRegex.findall(requestUrl):
encodedTracking = aCode.split('a=', 1)[1]
encodedTracking = encodedTracking.replace('%3D', '=')
decodedTracking = b64decode(encodedTracking).decode('utf-8').split('t=')[1].split('&')[0]
returnResults.append([{'Phrase': decodedTracking,
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Akamai Website ID',
'Notes': 'SHA-1 Sum'}}])
for dCode in demdexRegex.findall(requestUrl):
returnResults.append([{'Phrase': dCode.split('d_orgid=')[1],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'DemDex (Adobe) Website ID',
'Notes': ''}}])
for bCode in brightcoveRegex.findall(requestUrl):
returnResults.append([{'Phrase': bCode.split('account=')[1],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'BrightCove Website ID',
'Notes': ''}}])
if requestUrl in requestUrlsParsed:
return
requestUrlsParsed.add(requestUrl)
for uaCode in uaRegex.findall(requestUrl):
returnResults.append([{'Phrase': uaCode,
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Google UA Tracking Code',
'Notes': ''}}])
for pubCode in pubRegex.findall(requestUrl):
returnResults.append([{'Phrase': pubCode,
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Google AdSense ca-pub Tracking Code',
'Notes': ''}}])
for gtmCode in gtmRegex.findall(requestUrl):
returnResults.append([{'Phrase': gtmCode,
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Google GTM Tracking Code',
'Notes': ''}}])
for gCode in gRegex.findall(requestUrl):
returnResults.append([{'Phrase': gCode,
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Google G Tracking Code',
'Notes': ''}}])
for qCode in qualtricsRegex.findall(requestUrl):
returnResults.append([{'Phrase': qCode[6:],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Qualtrics Tracking Code',
'Notes': ''}}])
for pCode in pingdomRegex.findall(requestUrl):
returnResults.append([{'Phrase': pCode[:-3],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Pingdom Tracking Code',
'Notes': ''}}])
for mCode in mPulseRegex.findall(requestUrl):
returnResults.append([{'Phrase': mCode.split('/')[-1],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'mPulse Tracking Code',
'Notes': ''}}])
for cCode in contextWebRegex.findall(requestUrl):
returnResults.append([{'Phrase': cCode.split('token=')[1].split('&')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'ContextWeb Tracking Code',
'Notes': ''}}])
for fCode in facebookRegex.findall(requestUrl):
returnResults.append([{'Phrase': fCode.split('id=')[1].split('&')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Facebook Tracking Pixel Code',
'Notes': ''}}])
for mapsCode in googleMapsRegex.findall(requestUrl):
returnResults.append([{'Phrase': mapsCode.split('client=', 1)[1].split('&')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Google Maps Client Code',
'Notes': ''}}])
for marketoCode in marketoRegex.findall(requestUrl):
returnResults.append([{'Phrase': marketoCode.split('aid=')[1].split('&')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Marketo Tracking Code',
'Notes': ''}}])
for vwoCode in visualWebsiteOptimizerRegex.findall(requestUrl):
returnResults.append([{'Phrase': vwoCode.split('a=')[1].split('&')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Visual Website Optimizer Tracking User ID',
'Notes': ''}}])
for oCode in optimizeRegex.findall(requestUrl):
returnResults.append([{'Phrase': oCode.split('id=')[1].split('&')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Google Optimize ID',
'Notes': ''}}])
for mmCode in markMonitorRegex.findall(requestUrl):
returnResults.append([{'Phrase': mmCode.split('adv=')[1].split('&')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Mark Monitor Tracking ID',
'Notes': ''}}])
for zCode in zendeskRegex.findall(requestUrl):
returnResults.append([{'Phrase': zCode.split('key=')[1].split('&')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Zendesk ID',
'Notes': ''}}])
for qsCode in quantServeRegex.findall(requestUrl):
returnResults.append([{'Phrase': qsCode.split('/pixel/')[1].split('.gif')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'QuantServe Tracking Pixel ID',
'Notes': ''}}])
for clCode in cookieLawRegex.findall(requestUrl):
returnResults.append([{'Phrase': clCode.split('/')[2],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'CookieLaw Website ID',
'Notes': ''}}])
for otCode in oneTagRegex.findall(requestUrl):
returnResults.append([{'Phrase': otCode.split('/')[1],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'OneTag Tracking ID',
'Notes': ''}}])
for beCode in bounceExchangeRegex.findall(requestUrl):
returnResults.append([{'Phrase': beCode.split('/')[1],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'BounceExchange Tracking ID',
'Notes': ''}}])
for pushlyCode in pushlyRegex.findall(requestUrl):
returnResults.append([{'Phrase': pushlyCode.split('domain_key=')[1],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Pushly Website ID',
'Notes': ''}}])
for aCode in akamaiRegex.findall(requestUrl):
encodedTracking = aCode.split('a=', 1)[1]
encodedTracking = encodedTracking.replace('%3D', '=')
decodedTracking = b64decode(encodedTracking).decode('utf-8').split('t=')[1].split('&')[0]
returnResults.append([{'Phrase': decodedTracking,
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Akamai Website ID',
'Notes': 'SHA-1 Sum'}}])
for dCode in demdexRegex.findall(requestUrl):
returnResults.append([{'Phrase': dCode.split('d_orgid=')[1],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'DemDex (Adobe) Website ID',
'Notes': ''}}])
for bCode in brightcoveRegex.findall(requestUrl):
returnResults.append([{'Phrase': bCode.split('account=')[1],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'BrightCove Website ID',
'Notes': ''}}])
with sync_playwright() as p:
browser = p.firefox.launch()
browser = p.firefox.launch(executable_path=playwrightPath)
context = browser.new_context(
viewport={'width': 1920, 'height': 1080},
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:99.0) Gecko/20100101 Firefox/99.0'
@@ -194,16 +199,12 @@ class JSCodeExtractor:
if url is None:
continue
if not url.startswith('http://') and not url.startswith('https://'):
url = 'http://' + url
url = f'http://{url}'
try:
with contextlib.suppress(Error):
pageJS.goto(url, wait_until="networkidle")
except Error:
pass
try:
with contextlib.suppress(Error):
pageNoJS.goto(url, wait_until="networkidle")
except Error:
pass
pageJS.close()
pageNoJS.close()
browser.close()

View File

@@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""
Credit to @cyb_detective:
https://medium.com/@cyb_detective/20-regular-expressions-examples-to-search-for-data-related-to-cryptocurrencies-43e31dd4a5dc
"""
class LongANStringExtractor:
# A string that is treated as the name of this resolution.
name = "Extract Long Alphanumeric Strings"
category = "Website Information"
# A string that describes this resolution.
description = "Returns patterns matching common cryptocurrency address formats on a website."
originTypes = {'Domain', 'Website'}
resultTypes = {'Phrase'}
parameters = {'Minimum Length': {'description': 'Specify the minimum length an alphanumeric string has to have '
'to be extracted.',
'type': 'String',
'value': '',
'default': '20'
}}
def resolution(self, entityJsonList, parameters):
from playwright.sync_api import sync_playwright, TimeoutError, Error
from bs4 import BeautifulSoup
from pathlib import Path
import re
playwrightPath = Path(parameters['Playwright Chromium'])
try:
minLength = int(parameters['Minimum Length'])
if minLength < 1:
raise ValueError('Invalid min length specified.')
except ValueError:
return "Invalid Minimum Length specified."
returnResults = []
matchPattern = re.compile(r"\b[a-zA-Z0-9_.-]{" + str(minLength) + r",}\b")
# The software can deduplicate, but handling it here is better.
allPatterns = set()
def extractStrings(currentUID: str, site: str):
page = context.new_page()
pageResolved = False
for _ in range(3):
try:
page.goto(site, wait_until="networkidle", timeout=10000)
pageResolved = True
break
except TimeoutError:
pass
except Error:
break
if not pageResolved:
# Last chance for this to work; some pages have issues with the "networkidle" trigger.
try:
page.goto(site, wait_until="load", timeout=10000)
except Error:
return
soupContents = BeautifulSoup(page.content(), 'lxml')
# Remove <span> and <noscript> tags.
while True:
try:
soupContents.noscript.extract()
except AttributeError:
break
while True:
try:
soupContents.span.extract()
except AttributeError:
break
siteContent = soupContents.get_text()
stringMatches = matchPattern.findall(siteContent)
for potentialMatch in stringMatches:
if potentialMatch not in allPatterns:
allPatterns.add(potentialMatch)
returnResults.append([{'Phrase': potentialMatch,
'Entity Type': 'Phrase'},
{currentUID: {'Resolution': 'Long alphanumeric string',
'Notes': ''}}])
with sync_playwright() as p:
browser = p.chromium.launch(executable_path=playwrightPath)
context = browser.new_context(
viewport={'width': 1920, 'height': 1080},
user_agent='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/101.0.4951.54 Safari/537.36'
)
for entity in entityJsonList:
uid = entity['uid']
url = entity.get('URL') if entity.get('Entity Type', '') == 'Website' else \
entity.get('Domain Name', None)
if url is None:
continue
if not url.startswith('http://') and not url.startswith('https://'):
url = f'http://{url}'
extractStrings(uid, url)
browser.close()
return returnResults

View File

@@ -0,0 +1,37 @@
#!/usr/bin/env python3
class NPMJSSearch:
name = "Find NPM organization"
category = "Online Identity"
description = "Find a collective's npmjs organization page."
originTypes = {'Phrase', 'Company', 'Organization'}
resultTypes = {'Website'}
parameters = {}
def resolution(self, entityJsonList, parameters):
import requests
headers = {'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:108.0) Gecko/20100101 Firefox/108.0'}
url_base = 'https://www.npmjs.com/org/'
returnResults = []
for entity in entityJsonList:
primaryField = entity[list(entity)[1]].lower()
string_checks = {''.join(primaryField.split(' '))}
string_checks.add('_'.join(primaryField.split(' ')))
string_checks.add('-'.join(primaryField.split(' ')))
for check in string_checks:
check_url = url_base + check
request = requests.head(check_url, headers=headers)
if request.status_code == 200:
returnResults.append([{'URL': check_url,
'Entity Type': 'Website'},
{entity['uid']: {'Resolution': 'NPMJS Org',
'Notes': ''}}])
return returnResults

View File

@@ -24,8 +24,11 @@ class PhoneNumbersExtractor:
def resolution(self, entityJsonList, parameters):
from playwright.sync_api import sync_playwright, TimeoutError, Error
from bs4 import BeautifulSoup
from pathlib import Path
import re
playwrightPath = Path(parameters['Playwright Chromium'])
cleanTagsRegex = re.compile(r'<.*?>')
phoneNumCharsExclusion = re.compile(r'[^ -+()\[\]\d]')
@@ -55,12 +58,11 @@ class PhoneNumbersExtractor:
linksInAHref = soupContents.find_all('a')
for tag in linksInAHref:
newLink = tag.get('href', None)
if newLink is not None:
if newLink.startswith('tel:'):
returnResults.append([{'Phone Number': newLink[4:],
'Entity Type': 'Phone Number'},
{currentUID: {'Resolution': 'Phone Number Found',
'Notes': ''}}])
if newLink is not None and newLink.startswith('tel:'):
returnResults.append([{'Phone Number': newLink[4:],
'Entity Type': 'Phone Number'},
{currentUID: {'Resolution': 'Phone Number Found',
'Notes': ''}}])
textTags = soupContents.find_all('p')
for tag in textTags:
@@ -75,7 +77,7 @@ class PhoneNumbersExtractor:
'Notes': ''}}])
with sync_playwright() as p:
browser = p.chromium.launch()
browser = p.chromium.launch(executable_path=playwrightPath)
context = browser.new_context(
viewport={'width': 1920, 'height': 1080},
user_agent='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) '
@@ -88,7 +90,7 @@ class PhoneNumbersExtractor:
if url is None:
continue
if not url.startswith('http://') and not url.startswith('https://'):
url = 'http://' + url
url = f'http://{url}'
extractTels(uid, url)
browser.close()

View File

@@ -23,11 +23,10 @@ class PhraseSimilarity:
entity_fields = []
selection = parameters['Primary field or Notes']
algorithm = parameters['Algorithm'].replace(" ", "_")
if selection == 'Primary Field':
for entity in entityJsonList:
for entity in entityJsonList:
if selection == 'Primary Field':
entity_fields.append((entity['uid'], entity[list(entity)[1]].strip()))
elif selection == 'Notes':
for entity in entityJsonList:
elif selection == 'Notes':
entity_fields.append((entity['uid'], entity.get('Notes')))
if len(entity_fields) < 2:

View File

@@ -59,10 +59,9 @@ class RegexMatch:
search_re = re.findall(search_param, text, flags=flagsToUse)
for regexMatch in search_re[:maxResults]:
returnResults.append([{'Phrase': 'Regex Match: ' + regexMatch,
'Entity Type': 'Phrase',
'Notes': ''},
{uid: {'Resolution': 'Regex String Match',
'Notes': ''}}])
returnResults.extend([{'Phrase': f'Regex Match: {regexMatch}',
'Entity Type': 'Phrase',
'Notes': ''},
{uid: {'Resolution': 'Regex String Match', 'Notes': ''}}]
for regexMatch in search_re[:maxResults])
return returnResults

View File

@@ -0,0 +1,73 @@
#!/usr/bin/env python3
class ReplacePhrase:
name = "Replace String in Phrase"
category = "String Operations"
description = "Replace a character, or sequence of characters in a Phrase with another character or sequence."
originTypes = {'Phrase'}
resultTypes = {'Phrase'}
parameters = {'Sequence to Remove': {'description': 'Specify the character or sequence of characters to replace.',
'type': 'String',
'value': ''
},
'Sequence to Insert': {'description': 'Specify the character or sequence of characters to replace '
'the old character or sequence with. Enter the same character '
'or sequence to delete the character or sequence instead.',
'type': 'String',
'value': ''
},
'Match Type': {'description': 'Specify whether the matching of characters to replace '
'should be plain (as in, match characters as they were typed), '
'case insensitive, or regex.',
'type': 'SingleChoice',
'value': {'Plain', 'Case Insensitive', 'Regex'},
'default': 'Plain'
},
'Match Count': {'description': 'Specify the number of times to replace the specified character or '
'sequence with the new sequence. Zero is unlimited times.',
'type': 'String',
'value': '0',
'default': '0'
}
}
def resolution(self, entityJsonList, parameters):
import re
returnResults = []
remove = parameters['Sequence to Remove']
insert = parameters['Sequence to Insert']
if insert == remove:
insert = ''
matchType = parameters['Match Type']
try:
matchCount = int(parameters['Match Count'])
if matchCount < 0:
return []
except ValueError:
return "Invalid Match Count specified."
for entity in entityJsonList:
primaryField = entity['Phrase']
if matchType == 'Plain':
if matchCount == 0:
matchCount = -1
primaryField = primaryField.replace(remove, insert, matchCount)
elif matchType == 'Case Insensitive':
pattern = re.compile(re.escape(remove), re.IGNORECASE)
primaryField = pattern.sub(insert, primaryField, matchCount)
else:
pattern = re.compile(remove)
primaryField = pattern.sub(insert, primaryField, matchCount)
returnResults.append([{'Phrase': primaryField,
'Entity Type': 'Phrase'},
{entity['uid']: {'Resolution': 'Replace characters',
'Notes': ''}}])
return returnResults

View File

@@ -34,16 +34,14 @@ class TikTokVideoPublishDetails:
binString = "{0:b}".format(videoID)
if len(binString) == 63:
binString = '0' + binString
binString = f'0{binString}'
binString = int(binString[:32], 2)
UTCTimestamp = datetime.utcfromtimestamp(binString).isoformat() + '+00:00'
UTCTimestamp = f'{datetime.utcfromtimestamp(binString).isoformat()}+00:00'
returnResults.append([{'Date': UTCTimestamp,
'Entity Type': 'Date'},
{uid: {'Resolution': 'Video Publish Date', 'Notes': ''}}])
returnResults.append([{'User Name': username,
'Entity Type': 'Social Media Handle'},
{uid: {'Resolution': 'Published By', 'Notes': ''}}])
returnResults.extend(([{'Date': UTCTimestamp, 'Entity Type': 'Date'},
{uid: {'Resolution': 'Video Publish Date', 'Notes': ''}}],
[{'User Name': username, 'Entity Type': 'Social Media Handle'},
{uid: {'Resolution': 'Published By', 'Notes': ''}}]))
return returnResults

View File

@@ -11,10 +11,11 @@ class WebsiteFromPhrase:
parameters = {}
def resolution(self, entityJsonList, parameters):
import contextlib
import re
import tldextract
websiteRegex = re.compile(r"""^https?://(\S(?<!\.)){1,63}(\.(\S(?<!\.)){1,63})+$""")
websiteRegex = re.compile(r"""^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$""")
wordChar = re.compile(r'\w')
returnResults = []
@@ -25,14 +26,11 @@ class WebsiteFromPhrase:
while wordChar.match(entityChunk[-1]) is None:
entityChunk = entityChunk[:-1]
if websiteRegex.match(entityChunk):
try:
with contextlib.suppress(Exception):
tldObject = tldextract.extract(entityChunk)
if tldObject.suffix != '':
returnResults.append([{'URL': entityChunk,
'Entity Type': 'Website'},
{entity['uid']: {'Resolution': 'Phrase To Website',
'Notes': ''}}])
except Exception:
pass
return returnResults

View File

@@ -9,7 +9,8 @@ class WordCounter:
resultTypes = {'Phrase'}
parameters = {'Primary field or Notes': {'description': 'Choose Either Primary field or Notes',
'type': 'SingleChoice',
'value': {'Notes', 'Primary Field'}}}
'value': {'Notes', 'Primary Field'},
'default': 'Notes'}}
def resolution(self, entityJsonList, parameters):
import re

View File

@@ -2,10 +2,11 @@
import contextlib
from typing import Union
import re
from typing import Union, Optional
from glob import glob
import networkx as nx
import re
from datetime import timezone
from defusedxml.ElementTree import parse
from datetime import datetime
@@ -16,10 +17,42 @@ from ast import literal_eval
from base64 import b64decode
from dateutil import parser
from PySide6.QtCore import QByteArray, QSize, QUrl, Qt
from PIL import Image
from PIL.ImageQt import ImageQt
from PySide6.QtCore import QByteArray, QBuffer, QIODevice, QSize, QUrl, Qt
from PySide6 import QtWidgets, QtGui
from Core.Interface import Stylesheets
def resizePictureFromBuffer(picBuffer: QByteArray, newSize: tuple) -> QByteArray:
"""
newSize: First is width, second is height.
"""
picBufferData = picBuffer.data()
if picBufferData.startswith(b'<svg ') or picBufferData.startswith(b'<?xml '):
return QByteArray(resizeSVG(picBufferData, newSize))
originalImage = QtGui.QImage()
originalImage.loadFromData(picBuffer)
newImage = originalImage.scaled(newSize[0], newSize[1])
pictureByteArray = QByteArray()
imageBuffer = QBuffer(pictureByteArray)
imageBuffer.open(QIODevice.OpenModeFlag.WriteOnly)
newImage.save(imageBuffer, "PNG")
imageBuffer.close()
return pictureByteArray
def resizeSVG(byteString: bytes, resize: tuple):
bytesWidth = str(resize[0]).encode('UTF-8')
bytesHeight = str(resize[1]).encode('UTF-8')
widthRegex = re.compile(b' width="\d*" ')
for widthMatches in widthRegex.findall(byteString):
byteString = byteString.replace(widthMatches, b' ')
heightRegex = re.compile(b' height="\d*" ')
for heightMatches in heightRegex.findall(byteString):
byteString = byteString.replace(heightMatches, b' ')
return byteString.replace(b'<svg ', b'<svg height="%b" width="%b" ' % (bytesHeight, bytesWidth), 1)
class ResourceHandler:
@@ -28,67 +61,95 @@ class ResourceHandler:
return self.icons[iconName]
# Load all resources needed.
def __init__(self, mainWindow, messageHandler) -> None:
def __init__(self, mainWindow) -> None:
self.mainWindow = mainWindow
self.messageHandler = messageHandler
self.programBaseDirPath = Path(self.mainWindow.SETTINGS.value("Program/BaseDir"))
self.entityCategoryList = {}
self.moduleAssetPaths = []
self.icons = {"uploading": str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) /
"Resources" / "Icons" / "Uploading.png"),
"uploaded": str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) /
"Resources" / "Icons" / "Uploaded.png"),
"upArrow": str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) /
"Resources" / "Icons" / "UpArrow.png"),
"downArrow": str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) /
"Resources" / "Icons" / "DownArrow.png"),
"isolatedNodes": str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) /
"Resources" / "Icons" / "SelectIsolated.png"),
"addCanvas": str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) /
"Resources" / "Icons" / "Add_Canvas.png"),
"generateReport": str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) /
"Resources" / "Icons" / "Generate_Report.png"),
"leafNodes": str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) /
"Resources" / "Icons" / "SelectLeaf.png"),
"nonIsolatedNodes": str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) /
"Resources" / "Icons" / "SelectNonIsolated.png"),
"rootNodes": str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) /
"Resources" / "Icons" / "SelectRoot.png"),
"split": str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) /
"Resources" / "Icons" / "Split.png"),
"merge": str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) /
"Resources" / "Icons" / "Merge.png"),
"shortestPath": str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) /
"Resources" / "Icons" / "ShortestPath.png"),
"drawLink": str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) /
"Resources" / "Icons" / "DrawLink.png"),
"rearrange": str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) /
"Resources" / "Icons" / "RearrangeGraph.png"),
"colorPicker": str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) /
"Resources" / "Icons" / "ColorPicker.png"),
self.icons = {"uploading": str(self.programBaseDirPath / "Resources" / "Icons" / "Uploading.png"),
"uploaded": str(self.programBaseDirPath / "Resources" / "Icons" / "Uploaded.png"),
"upArrow": str(self.programBaseDirPath / "Resources" / "Icons" / "UpArrow.png"),
"downArrow": str(self.programBaseDirPath / "Resources" / "Icons" / "DownArrow.png"),
"isolatedNodes": str(self.programBaseDirPath / "Resources" / "Icons" / "SelectIsolated.png"),
"addCanvas": str(self.programBaseDirPath / "Resources" / "Icons" / "Add_Canvas.png"),
"generateReport": str(self.programBaseDirPath / "Resources" / "Icons" / "Generate_Report.png"),
"leafNodes": str(self.programBaseDirPath / "Resources" / "Icons" / "SelectLeaf.png"),
"nonIsolatedNodes": str(self.programBaseDirPath / "Resources" / "Icons" /
"SelectNonIsolated.png"),
"rootNodes": str(self.programBaseDirPath / "Resources" / "Icons" / "SelectRoot.png"),
"split": str(self.programBaseDirPath / "Resources" / "Icons" / "Split.png"),
"merge": str(self.programBaseDirPath / "Resources" / "Icons" / "Merge.png"),
"shortestPath": str(self.programBaseDirPath / "Resources" / "Icons" / "ShortestPath.png"),
"drawLink": str(self.programBaseDirPath / "Resources" / "Icons" / "DrawLink.png"),
"rearrange": str(self.programBaseDirPath / "Resources" / "Icons" / "RearrangeGraph.png"),
"colorPicker": str(self.programBaseDirPath / "Resources" / "Icons" / "ColorPicker.png"),
}
self.banners = {f"{bannerPath.split('Banner_')[-1].split('.')[0]}": str(bannerPath)
for bannerPath in glob(str(self.programBaseDirPath / "Resources" / "Icons" / "Banner_*.svg"))}
# These are not meant to be strict - just restrictive enough such that users don't put in utter nonsense.
# Note that regex isn't always the best way of validating fields, but it should be good enough for our
# purposes.
self.checks = {'Email': re.compile(r"""(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])"""),
'Phonenumber': re.compile(r"""^(\+|00)?[0-9\(\) \-]{3,32}$"""),
'String': re.compile(r""".+"""),
'URL': re.compile(r"""^(?:(?:http|ftp)s?|file)://(\S(?<!\.)){1,63}(\.(\S(?<!\.)){1,63})+$"""),
'Onion': re.compile(r"""^https?://\w{56}\.onion/?(\S(?<!\.))*(\.(\S(?<!\.))*)?$"""),
'Domain': re.compile(r"""^(\S(?<!\.)(?!/)(?<!/)){1,63}(\.(\S(?<!\.)(?!/)(?<!/)){1,63})+$"""),
'Float': re.compile(r"""^([-+])?(\d|\.(?=\d))+$"""),
'WordString': re.compile(r"""^\D+$"""),
'Numbers': re.compile(r"""^\d+$"""),
'IPv4': re.compile(r"""^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$"""),
'IPv6': re.compile(r"""^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))$"""),
'MAC': re.compile(r"""^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$"""),
'ASN': re.compile(r"""^(AS)?\d+$"""),
'CUSIP': re.compile(r"""^[a-zA-Z0-9]{9}$"""),
'EIN': re.compile(r"""^\d{2}-?\d{7}$"""),
'LEIID': re.compile(r"""^[a-zA-Z0-9]{20}$"""),
'ISINID': re.compile(r"""^[a-zA-Z0-9]{2}-?[a-zA-Z0-9]{9}-?[a-zA-Z0-9]$"""),
'SIC/NAICS': re.compile(r"""^[0-9]{4,6}$""")}
self.checks = {'Email': re.compile(
r"""(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)])"""),
'Phonenumber': re.compile(r"""^(\+|00)?[0-9() \-]{3,32}$"""),
'String': re.compile(r""".+"""),
'URL': re.compile(r"""[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)"""),
'Onion': re.compile(r"""^https?://\w{56}\.onion/?(\S(?<!\.))*(\.(\S(?<!\.))*)?$"""),
'Domain': re.compile(r"""^(\S(?<!\.)(?!/)(?<!/)){1,63}(\.(\S(?<!\.)(?!/)(?<!/)){1,63})+$"""),
'Float': re.compile(r"""^([-+])?(\d|\.(?=\d))+$"""),
'WordString': re.compile(r"""^\D+$"""),
'Numbers': re.compile(r"""^\d+$"""),
'IPv4': re.compile(r"""^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$"""),
'IPv6': re.compile(
r"""^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))$"""),
'MAC': re.compile(r"""^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$"""),
'ASN': re.compile(r"""^(AS)?\d+$"""),
'CUSIP': re.compile(r"""^[a-zA-Z0-9]{9}$"""),
'EIN': re.compile(r"""^\d{2}-?\d{7}$"""),
'LEIID': re.compile(r"""^[a-zA-Z0-9]{20}$"""),
'ISINID': re.compile(r"""^[a-zA-Z0-9]{2}-?[a-zA-Z0-9]{9}-?[a-zA-Z0-9]$"""),
'SIC/NAICS': re.compile(r"""^[0-9]{4,6}$""")}
self.loadCoreEntities()
self.loadModuleEntities(self.programBaseDirPath / "Core")
def getPictureFromFile(self, filePath: Path, resize: tuple = (0, 0)) -> Optional[QByteArray]:
"""
resize: tuple, first is width, second is height.
"""
try:
with open(filePath, 'rb') as newIconFile:
fileContents = newIconFile.read()
if fileContents.startswith(b'<svg ') or fileContents.startswith(b'<?xml '):
if resize != (0, 0):
fileContents = resizeSVG(fileContents, resize)
pictureByteArray = QByteArray(fileContents)
else:
image = Image.open(str(filePath))
if resize != (0, 0):
thumbnail = ImageQt(image.resize(resize))
else:
thumbnail = ImageQt(image)
pictureByteArray = QByteArray()
imageBuffer = QBuffer(pictureByteArray)
imageBuffer.open(QIODevice.OpenModeFlag.WriteOnly)
thumbnail.save(imageBuffer, "PNG")
imageBuffer.close()
except ValueError as ve:
# Image type is unsupported (for ImageQt)
# Supported types: 1, L, P, RGB, RGBA
self.mainWindow.MESSAGEHANDLER.warning(f'Invalid Image selected: {str(ve)}', popUp=True)
pictureByteArray = None
return pictureByteArray
def loadModuleBanners(self, modulePath: Path):
assetsPath = modulePath / "Assets"
self.banners.update({f"{bannerPath.split('Banner_')[-1].split('.')[0]}": str(bannerPath)
for bannerPath in glob(str(assetsPath / "Banner_*.svg"))})
def getEntityCategories(self) -> list:
return list(self.entityCategoryList)
@@ -98,8 +159,7 @@ class ResourceHandler:
for entity in self.entityCategoryList[category]:
entityValue = self.entityCategoryList[category][entity]
eList.append((self.getBareBonesEntityJson(entity),
entityValue['Icon']
))
entityValue['Icon']))
return eList
def getEntityAttributes(self, entityType) -> Union[None, list]:
@@ -110,8 +170,8 @@ class ResourceHandler:
aList.extend(iter(self.entityCategoryList[category][entityType]['Attributes']))
break
except KeyError:
self.messageHandler.error("Attempted to get attributes for "
"nonexistent entity type: " + str(entityType), True)
self.mainWindow.MESSAGEHANDLER.error(
f"Attempted to get attributes for nonexistent entity type: {entityType}", True)
return None
return aList
@@ -167,13 +227,24 @@ class ResourceHandler:
result = attrCheck.findall(attribute)
return len(result) == 1
def addRecognisedEntityTypes(self, entityFile: Path) -> bool:
def getIconPathForIconFile(self, iconFile: str) -> Union[None, Path]:
iconPath = self.programBaseDirPath / "Resources" / "Icons" / iconFile
if iconPath.exists():
return iconPath
for assetPath in self.moduleAssetPaths:
iconPath = assetPath / iconFile
if iconPath.exists():
return iconPath
return self.programBaseDirPath / "Resources" / "Icons" / "Default.svg"
def addRecognisedEntityTypes(self, entityFile: Path) -> list:
entityTypesAdded = []
try:
tree = parse(entityFile, forbid_dtd=True, forbid_entities=True, forbid_external=True)
except Exception as exc:
self.mainWindow.MESSAGEHANDLER.warning('Error occurred when loading entities from '
+ str(entityFile) + ': ' + str(exc) + ', skipping.')
return False
self.mainWindow.MESSAGEHANDLER.warning(
f'Error occurred when loading entities from {entityFile}: {exc}, skipping.')
return []
root = tree.getroot()
@@ -208,26 +279,28 @@ class ResourceHandler:
self.entityCategoryList[category] = {}
self.entityCategoryList[category][entityName] = {
'Attributes': attributesDict,
'Icon': str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) / "Resources" / "Icons" / icon)}
'Icon': str(self.getIconPathForIconFile(icon))}
entityTypesAdded.append(f'{category}/{entityName}')
except (KeyError, AttributeError) as err:
# Ignore malformed entities
self.messageHandler.error(f'Error: {str(err)}', popUp=False)
self.mainWindow.MESSAGEHANDLER.error(f'Error: {str(err)}', popUp=False)
continue
return True
return entityTypesAdded
def loadCoreEntities(self) -> None:
entDir = Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) / "Core" / "Entities"
for entFile in listdir(entDir):
if entFile.endswith('.xml'):
self.addRecognisedEntityTypes(entDir / entFile)
def loadModuleEntities(self) -> None:
entDir = Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) / "Modules"
for module in listdir(entDir):
for entFile in listdir(entDir / module):
def loadModuleEntities(self, modulePath: Path) -> list:
entitiesPath = modulePath / 'Entities'
allModuleEntitiesAdded = []
if entitiesPath.exists():
for entFile in listdir(entitiesPath):
if entFile.endswith('.xml'):
self.addRecognisedEntityTypes(
entDir / module / entFile)
allModuleEntitiesAdded += self.addRecognisedEntityTypes(entitiesPath / entFile)
return allModuleEntitiesAdded
def loadModuleAssets(self, modulePath: Path):
moduleAssetsPath = modulePath / "Assets"
if moduleAssetsPath.exists():
self.moduleAssetPaths.append(moduleAssetsPath)
self.loadModuleBanners(modulePath)
def getEntityJson(self, entityType: str, jsonData=None) -> Union[dict, None]:
eJson = {'uid': str(uuid4())}
@@ -240,7 +313,8 @@ class ResourceHandler:
eJson[attribute] = self.entityCategoryList[category][entityType]['Attributes'][attribute][0]
break
except KeyError:
self.messageHandler.error(f"Attempted to get attributes for malformed entity type: {entityType}", True)
self.mainWindow.MESSAGEHANDLER.error(
f"Attempted to get attributes for malformed entity type: {entityType}", True)
return None
eJson['Entity Type'] = entityType
eJson['Date Created'] = None
@@ -277,8 +351,8 @@ class ResourceHandler:
if self.entityCategoryList[category][entityType]['Attributes'][attribute][2]:
return attribute
except KeyError:
self.messageHandler.error(f"Attempted to get primary attribute for malformed entity type: {entityType}",
True)
self.mainWindow.MESSAGEHANDLER.error(
f"Attempted to get primary attribute for malformed entity type: {entityType}", True)
return None
def getBareBonesEntityJson(self, entityType: str) -> Union[dict, None]:
@@ -290,7 +364,8 @@ class ResourceHandler:
eJson[attribute] = self.entityCategoryList[category][entityType]['Attributes'][attribute][0]
break
except KeyError:
self.messageHandler.error(f"Attempted to get attributes for malformed entity type: {entityType}", True)
self.mainWindow.MESSAGEHANDLER.error(
f"Attempted to get attributes for malformed entity type: {entityType}", True)
return None
eJson['Entity Type'] = entityType
@@ -324,8 +399,8 @@ class ResourceHandler:
return linkJson
def getEntityDefaultPicture(self, entityType) -> QByteArray:
picture = Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) / "Resources" / "Icons" / "Default.svg"
def getEntityDefaultPicture(self, entityType: str) -> QByteArray:
picture = self.programBaseDirPath / "Resources" / "Icons" / "Default.svg"
try:
for category in self.entityCategoryList:
if entityType in self.entityCategoryList[category]:
@@ -334,19 +409,19 @@ class ResourceHandler:
picture = entityPicture
break
except KeyError:
self.messageHandler.warning("Attempted to get icon for "
"nonexistent entity type: " + str(entityType), popUp=False)
self.mainWindow.MESSAGEHANDLER.warning(
f"Attempted to get icon for nonexistent entity type: {entityType}", popUp=False)
finally:
with open(picture, 'rb') as pictureFile:
pictureContents = pictureFile.read()
return QByteArray(pictureContents)
def getLinkPicture(self):
picture = Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) / "Resources" / "Icons" / "Resolution.png"
picture = self.programBaseDirPath / "Resources" / "Icons" / "Resolution.png"
return QtGui.QIcon(str(picture)).pixmap(40, 40)
def getLinkArrowPicture(self):
picture = Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) / "Resources" / "Icons" / "Right-Arrow.svg"
picture = self.programBaseDirPath / "Resources" / "Icons" / "Right-Arrow.svg"
return QtGui.QIcon(str(picture)).pixmap(40, 40)
def deconstructGraph(self, graph: nx.DiGraph) -> tuple:
@@ -421,7 +496,7 @@ class FilePropertyInput(QtWidgets.QLineEdit):
fileChosen = self.fileDialog.getOpenFileName(self,
"Open File",
str(Path.home()),
options=QtWidgets.QFileDialog.DontUseNativeDialog)
options=QtWidgets.QFileDialog.Option.DontUseNativeDialog)
self.setText(fileChosen[0])
@@ -440,7 +515,6 @@ class SingleChoicePropertyInput(QtWidgets.QGroupBox):
for option in enforceOptionsSet:
radioButton = QtWidgets.QRadioButton(option)
radioButton.setStyleSheet(Stylesheets.RADIO_BUTTON_STYLESHEET)
if option == defaultOption:
radioButton.setChecked(True)
else:
@@ -467,7 +541,6 @@ class MultiChoicePropertyInput(QtWidgets.QGroupBox):
for option in enforceOptionsSet:
checkBox = QtWidgets.QCheckBox(option)
checkBox.setStyleSheet(Stylesheets.CHECK_BOX_STYLESHEET)
if option in defaultOptions:
checkBox.setChecked(True)
else:
@@ -488,6 +561,7 @@ class MinSizeStackedLayout(QtWidgets.QStackedLayout):
https://stackoverflow.com/a/34300567
"""
def sizeHint(self) -> QSize:
return self.currentWidget().sizeHint()
@@ -536,7 +610,7 @@ class RichNotesEditor(QtWidgets.QTextBrowser):
def mousePressEvent(self, ev: QtGui.QMouseEvent) -> None:
potentialLink = self.anchorAt(ev.pos())
if not potentialLink and ev.button() == QtGui.Qt.LeftButton:
if not potentialLink and ev.button() == QtGui.Qt.MouseButton.LeftButton:
self.startEditing()
super(RichNotesEditor, self).mousePressEvent(ev)

View File

@@ -4,6 +4,7 @@ from msgpack import dump
from shutil import move
from pathlib import Path
from Core.PathHelper import is_path_exists_or_creatable_portable
from PySide6.QtCore import QSettings
class SettingsObject(dict):
@@ -21,22 +22,67 @@ class SettingsObject(dict):
def __init__(self):
super().__init__()
self.setValue("Program/BaseDir", "Unset") # dirname(abspath(getsourcefile(lambda:0))) + "/../" )
self.setValue("Program/GraphLayout", "dot")
self.setValue("Program/Graphics/EntityTextFontType", "Mono")
self.setValue("Program/Graphics/EntityTextFontSize", "11")
self.setValue("Program/Graphics/EntityTextFontBoldness", "700")
self.setValue("Program/Graphics/LinkTextFontType", "Mono")
self.setValue("Program/Graphics/LinkTextFontSize", "11")
self.setValue("Program/Graphics/LinkTextFontBoldness", "700")
self.setValue("Program/Graphics/EntityTextColor", "#000000") # RGB
self.setValue("Program/Graphics/LinkTextColor", "#000000") # RGB
self.setValue("Program/Graphics/LabelFade", "3")
self.globalSettings = QSettings()
self.globalSettings.setValue("Program/Version", "v1.6.5")
self.globalSettings.setValue("Program/TOR Profile Location",
self.globalSettings.value("Program/TOR Profile Location", ""))
self.globalSettings.setValue("Program/BaseDir",
self.globalSettings.value("Program/BaseDir", "Unset"))
self.globalSettings.setValue("Program/Version Check Source",
self.globalSettings.value("Program/Version Check Source",
"https://api.github.com/repos/AccentuSoft/LinkScope_Client/releases/latest"))
self.globalSettings.setValue("Program/Update Source",
self.globalSettings.value("Program/Update Source",
"https://github.com/AccentuSoft/LinkScope_Client/releases/latest/download/"))
# The value '20' equates to logging.INFO
# It's not necessary to set this, but we will for
# the sake of completeness
self.globalSettings.setValue("Logging/Severity",
self.globalSettings.value("Logging/Severity", "20"))
self.globalSettings.setValue("Logging/Logfile",
self.globalSettings.value("Logging/Logfile",
str(Path.home() / 'LinkScope_logfile.log')))
self.globalSettings.setValue("Program/Graph Layout",
self.globalSettings.value("Program/Graph Layout", "dot"))
self.globalSettings.setValue("Program/Graphics/Entity Text Font Type",
self.globalSettings.value("Program/Graphics/Entity Text Font Type", "Mono"))
self.globalSettings.setValue("Program/Graphics/Entity Text Font Size",
self.globalSettings.value("Program/Graphics/Entity Text Font Size", "11"))
self.globalSettings.setValue("Program/Graphics/Entity Text Font Boldness",
self.globalSettings.value("Program/Graphics/Entity Text Font Boldness", "700"))
self.globalSettings.setValue("Program/Graphics/Link Text Font Type",
self.globalSettings.value("Program/Graphics/Link Text Font Type", "Mono"))
self.globalSettings.setValue("Program/Graphics/Link Text Font Size",
self.globalSettings.value("Program/Graphics/Link Text Font Size", "11"))
self.globalSettings.setValue("Program/Graphics/Link Text Font Boldness",
self.globalSettings.value("Program/Graphics/Link Text Font Boldness", "700"))
self.globalSettings.setValue("Program/Graphics/Entity Text Color",
self.globalSettings.value("Program/Graphics/Entity Text Color", "#000000"))
self.globalSettings.setValue("Program/Graphics/Link Text Color",
self.globalSettings.value("Program/Graphics/Link Text Color", "#000000"))
self.globalSettings.setValue("Program/Graphics/Label Fade Scroll Distance",
self.globalSettings.value("Program/Graphics/Label Fade Scroll Distance", "3"))
self.globalSettings.setValue("Program/Usage/First Time Start",
self.globalSettings.value("Program/Usage/First Time Start", 'true'))
self.globalSettings.setValue("Program/Sources/Sources List",
self.globalSettings.value(
"Program/Sources/Sources List",
{}))
self.globalSettings.setValue("Program/Sources/Module Packs List",
self.globalSettings.value(
"Program/Sources/Module Packs List",
{}))
self.setValue("Project/Name", "Untitled")
self.setValue("Project/BaseDir", "")
self.setValue("Project/FilesDir", "")
# For any entity with a Path variable, this dictates whether a copy of the original is made or whether a
# symlink is created. Symlinks require special permissions or developer mode in Windows however.
# symlink is created. Symlinks however require special permissions or developer mode in Windows.
# To ensure that the software works out-of-the-box on all platforms, the default is set to 'Copy'.
self.setValue("Project/Symlink or Copy Materials", "Copy") # Values are 'Copy' or 'Symlink'.
self.setValue("Project/Resolution Result Grouping Threshold", "15")
@@ -46,20 +92,46 @@ class SettingsObject(dict):
self.setValue("Project/Server/Project", "")
self.setValue("Project/Server/Collectors", "{}")
# The value '20' equates to logging.INFO
# It's not necessary to set this, but we will for
# the sake of completeness
self.setValue("Logging/Severity", "20")
self.setValue("Logging/Logfile", str(Path.home() / 'LinkScope_logfile.log'))
def getGroupSettings(self, settingsGroup: str) -> dict:
if not settingsGroup.endswith('/'):
settingsGroup += '/'
settingsDict = {
setting: self.globalSettings.value(setting)
for setting in self.globalSettings.allKeys()
if setting.startswith(settingsGroup)
}
for setting in self:
if setting.startswith(settingsGroup):
settingsDict[setting] = self[setting]
return dict(sorted(settingsDict.items()))
# Usability Alias
def setValue(self, key, value):
def setValue(self, key, value) -> None:
if self.globalSettings.contains(key):
self.globalSettings.setValue(key, value)
self[key] = value
def setGlobalValue(self, key, value) -> None:
"""
Helper in the case we want to be explicit in setting a value globally.
"""
self.globalSettings.setValue(key, value)
def value(self, key, alt=None):
if self.globalSettings.contains(key):
return self.globalSettings.value(key)
return self.get(key, alt)
def save(self):
def removeKey(self, key) -> bool:
try:
if self.globalSettings.contains(key):
self.globalSettings.remove(key)
else:
self.pop(key)
return True
except KeyError:
return False
def save(self) -> None:
# Save and then move to prevent corruption if the application closes unexpectedly.
actualSavePath = str(Path(self.value("Project/BaseDir")).joinpath(self.value("Project/Name") + ".linkscope"))
if is_path_exists_or_creatable_portable(actualSavePath):
@@ -67,7 +139,12 @@ class SettingsObject(dict):
with open(tempSavePath, "wb") as projectFile:
dump(self, projectFile)
move(tempSavePath, actualSavePath)
self.globalSettings.sync()
globalSettingsSavingError = self.globalSettings.status()
if globalSettingsSavingError != self.globalSettings.Status.NoError:
raise ValueError(f'Could not save global settings: {globalSettingsSavingError}')
def load(self, savedDict: dict):
def load(self, savedDict: dict) -> None:
# No need to do anything with global settings.
for key in savedDict:
self[key] = savedDict[key]

View File

@@ -6,7 +6,6 @@ from pathlib import Path
from os import symlink
from shutil import copy2
from hashlib import sha3_512
from binascii import hexlify
from urllib.parse import urlparse
from PySide6 import QtCore
@@ -55,18 +54,25 @@ class URLManager:
if savePathString == 'None':
return None
fileType = magic.from_file(urlPathString, mime=True)
fileTypeSplit1, fileTypeSplit2 = fileType.split('/', 1)
# CSV files not considered - may have any dialect, hard to accommodate.
if urlPath.suffix in {'.ods', '.xls', '.xlsm', '.xlsx'} and \
fileTypeSplit2 in {'vnd.oasis.opendocument.spreadsheet',
'vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'vnd.ms-excel'}:
return {"Spreadsheet Name": urlName,
"File Path": savePathString,
"Entity Type": "Spreadsheet"}
# Only support zip files for archives (for now) 10/Jul/2021).
if zipfile.is_zipfile(urlPathString):
entityJson = {"Archive Name": urlName, "File Path": savePathString, "Entity Type": "Archive"}
elif zipfile.is_zipfile(urlPathString):
return {"Archive Name": urlName, "File Path": savePathString, "Entity Type": "Archive"}
elif fileTypeSplit1 == "video":
return {"Video Name": urlName, "File Path": savePathString, "Entity Type": "Video"}
elif fileTypeSplit1 == "image":
return {"Image Name": urlName, "File Path": savePathString, "Entity Type": "Image"}
else:
fileType = magic.from_file(urlPathString, mime=True).split('/')[0]
if fileType == "video":
entityJson = {"Video Name": urlName, "File Path": savePathString, "Entity Type": "Video"}
elif fileType == "image":
entityJson = {"Image Name": urlName, "File Path": savePathString, "Entity Type": "Image"}
else:
entityJson = {"Document Name": urlName, "File Path": savePathString, "Entity Type": "Document"}
return entityJson
return {"Document Name": urlName, "File Path": savePathString, "Entity Type": "Document"}
def moveURLToProjectFilesHelperIfNeeded(self, urlPath: Path):
valuePath = Path(urlPath).absolute()
@@ -83,8 +89,8 @@ class URLManager:
projectFilesPath = Path(self.mainWindow.SETTINGS.value("Project/FilesDir"))
# Create a unique path in Project Files
saveHash = hexlify(sha3_512(str(urlPath).encode()).digest()).decode()[:16] # nosec
savePath = projectFilesPath / f'{saveHash}|{urlPath.name}'
saveHash = sha3_512(str(urlPath).encode()).hexdigest()[:16] # nosec
savePath = projectFilesPath / f'{saveHash}_{urlPath.name}'
if createSymlink:
symlink(urlPath, savePath)

193
Core/UpdateManager.py Normal file
View File

@@ -0,0 +1,193 @@
#!/usr/bin/env python3
import contextlib
import platform
import subprocess
import requests
import tempfile
import shutil
import os
import ctypes
from pathlib import Path
from semver import compare
from PySide6 import QtCore, QtWidgets
class UpdateManager:
def __init__(self, mainWindow):
self.updateThread = None
self.mainWindow = mainWindow
self.system = platform.system()
if self.system == 'Windows':
self.baseSoftwarePath = Path(os.path.abspath(os.sep)) / 'Program Files' / 'LinkScope'
else:
self.baseSoftwarePath = Path(os.path.abspath(os.sep)) / 'usr' / 'local' / 'sbin' / 'LinkScope'
def getDownloadURL(self):
downloadURLBase = self.mainWindow.SETTINGS.value("Program/Update Source")
if self.system == 'Windows':
return f"{downloadURLBase}LinkScope-Windows-x64.7z"
else:
return f"{downloadURLBase}LinkScope-Ubuntu-x64.7z"
def getLatestVersion(self):
with contextlib.suppress(Exception):
version_req = requests.get(self.mainWindow.SETTINGS.value("Program/Version Check Source"),
headers={'User-Agent': 'LinkScope Update Checker'})
if version_req.status_code == 200:
return version_req.json()['tag_name']
return None
def isUpdateAvailable(self):
latest_version = self.getLatestVersion()
return (
latest_version is not None
and compare(self.mainWindow.SETTINGS.value("Program/Version").lstrip('v'),
latest_version.lstrip('v')) < 0
)
def doUpdate(self) -> None:
if self.updateThread is not None:
self.mainWindow.MESSAGEHANDLER.error('Update already in progress.')
return
self.mainWindow.MESSAGEHANDLER.info('Starting update...')
self.updateThread = UpdaterThread(self, self.mainWindow)
self.updateThread.updateDoneSignal.connect(self.finalizeUpdate)
self.updateThread.start()
self.mainWindow.MESSAGEHANDLER.info('Update in progress')
def finalizeUpdate(self, updateTempPath: str) -> None:
if updateTempPath != '':
latest_version = self.getLatestVersion()
self.mainWindow.SETTINGS.setValue("Program/Version", latest_version)
self.mainWindow.MESSAGEHANDLER.info('Updating done, please restart for the changes to take effect.')
self.mainWindow.MESSAGEHANDLER.info("The application will now save and close to apply the updates. "
"Please wait for a few minutes before reopening LinkScope.",
popUp=True)
uncompressNewVersion(
self.system,
self.mainWindow.SETTINGS.value("Program/BaseDir"),
str(self.baseSoftwarePath),
updateTempPath)
self.mainWindow.close()
else:
self.mainWindow.MESSAGEHANDLER.info('Updating failed.')
class UpdaterWindow(QtWidgets.QDialog):
def __init__(self, mainWindow, updateManager: UpdateManager, updateAvailableOverride: bool = False):
super().__init__()
self.mainWindow = mainWindow
self.updateManager = updateManager
self.setWindowTitle('Update Manager')
layout = QtWidgets.QGridLayout()
self.setLayout(layout)
doUpdateButton = QtWidgets.QPushButton('Update')
doUpdateButton.clicked.connect(self.initiateUpdate)
cancelButton = QtWidgets.QPushButton('Close')
cancelButton.clicked.connect(self.reject)
updateAvailableLabel = QtWidgets.QLabel()
updateAvailableLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
if updateAvailableOverride or updateManager.isUpdateAvailable():
updateAvailableLabel.setText('An update for LinkScope is available.')
else:
updateAvailableLabel.setText('LinkScope is up to date.')
doUpdateButton.setDisabled(True)
doUpdateButton.setEnabled(False)
layout.addWidget(updateAvailableLabel, 1, 1, 1, 2)
layout.addWidget(cancelButton, 2, 1)
layout.addWidget(doUpdateButton, 2, 2)
def initiateUpdate(self) -> None:
self.updateManager.doUpdate()
self.accept()
class UpdaterThread(QtCore.QThread):
updateDoneSignal = QtCore.Signal(str)
def __init__(self, updateManager, mainWindow):
super().__init__()
self.mainWindow = mainWindow
self.updateManager = updateManager
def run(self) -> None:
try:
downloadUrl = self.updateManager.getDownloadURL()
clientTempCompressedArchive = tempfile.mkstemp(suffix='.7z')
tempPath = clientTempCompressedArchive[1]
with os.fdopen(clientTempCompressedArchive[0], 'wb') as tempArchive:
with requests.get(downloadUrl, stream=True) as fileStream:
for chunk in fileStream.iter_content(chunk_size=5 * 1024 * 1024):
tempArchive.write(chunk)
self.updateDoneSignal.emit(tempPath)
except Exception:
self.updateDoneSignal.emit('')
def uncompressNewVersion(system: str, baseDir: str, baseSoftwarePath: str, updateTempPath: str):
tempDir = tempfile.mkdtemp(prefix='LinkScope_Updater_TMP_')
if system == 'Windows':
updaterPath = Path(baseDir) / "UpdaterUtil.exe"
tempUpdaterPath = Path(tempDir) / updaterPath.name
shutil.copy(updaterPath, tempUpdaterPath)
# This is done so that Windows spawns the updater as a detached process.
ShellExecuteEx = ctypes.windll.shell32.ShellExecuteEx
SEE_MASK_NO_CONSOLE = 0x00008000
class SHELLEXECUTEINFO(ctypes.Structure):
_fields_ = [
("cbSize", ctypes.c_ulong),
("fMask", ctypes.c_ulong),
("hwnd", ctypes.c_void_p),
("lpVerb", ctypes.c_char_p),
("lpFile", ctypes.c_char_p),
("lpParameters", ctypes.c_char_p),
("lpDirectory", ctypes.c_char_p),
("nShow", ctypes.c_int),
("hInstApp", ctypes.c_void_p),
("lpIDList", ctypes.c_void_p),
("lpClass", ctypes.c_char_p),
("hkeyClass", ctypes.c_void_p),
("dwHotKey", ctypes.c_ulong),
("hIconOrMonitor", ctypes.c_void_p),
("hProcess", ctypes.c_void_p),
]
sei = SHELLEXECUTEINFO()
sei.cbSize = ctypes.sizeof(sei)
sei.fMask = SEE_MASK_NO_CONSOLE
sei.lpVerb = b"runas"
sei.lpFile = bytes(tempUpdaterPath)
sei.lpParameters = f'"{updateTempPath}" "{baseSoftwarePath}"'.encode('utf-8')
sei.nShow = 1
if not ShellExecuteEx(ctypes.byref(sei)):
raise ctypes.WinError()
elif system == 'Linux':
updaterPath = Path(baseDir) / "UpdaterUtil"
tempUpdaterPath = Path(tempDir) / updaterPath.name
shutil.copy(updaterPath, tempUpdaterPath)
subprocess.Popen(
f'pkexec "{tempUpdaterPath}" "{updateTempPath}" "{baseSoftwarePath}"',
start_new_session=True,
close_fds=True,
shell=True,
)

View File

@@ -7,6 +7,7 @@ import os
import platform
import subprocess
import tempfile
import stat
from pathlib import Path
import requests
@@ -14,19 +15,17 @@ import py7zr
from PySide6 import QtCore, QtWidgets, QtGui
# Requirements:
# requests
# PySide6
# py7zr
# requests PySide6 py7zr wheel pip nuitka ordered-set zstandard
#
# Compile with (requires zstandard, benefits from orderedset):
# Compile with:
#
# Windows:
"""
python -m nuitka --follow-imports --onefile --noinclude-pytest-mode=nofollow --noinclude-setuptools-mode=nofollow ^
--noinclude-custom-mode=setuptools:error --noinclude-IPython-mode=nofollow --enable-plugin=pyside6 ^
--assume-yes-for-downloads --remove-output --disable-console --warn-unusual-code --show-modules ^
--windows-company-name="AccentuSoft" --windows-product-name="LinkScope Installer" --windows-product-version=2.0.0.0 ^
--include-data-files="Icon.ico=Icon.ico" --linux-icon="Icon.ico" --windows-icon-from-ico=".\Icon.ico" ^
--assume-yes-for-downloads --remove-output --windows-console-mode=disable --warn-unusual-code --show-modules ^
--windows-company-name="AccentuSoft" --windows-product-name="LinkScope Installer" --windows-product-version=1.6.5.0 ^
--include-data-files="Icon.ico=Icon.ico" --windows-icon-from-ico=".\Icon.ico" ^
--windows-file-description="LinkScope Installer" ^
Installer.py
"""
@@ -35,7 +34,7 @@ Installer.py
"""
python -m nuitka --follow-imports --onefile --noinclude-pytest-mode=nofollow --noinclude-setuptools-mode=nofollow \
--noinclude-custom-mode=setuptools:error --noinclude-IPython-mode=nofollow --enable-plugin=pyside6 \
--assume-yes-for-downloads --remove-output --disable-console --warn-unusual-code --show-modules \
--assume-yes-for-downloads --remove-output --warn-unusual-code --show-modules \
--include-data-files="Icon.ico=Icon.ico" --linux-icon="Icon.ico" \
Installer.py
"""
@@ -717,6 +716,117 @@ For more information on this, and how to apply and follow the GNU AGPL, see
"""
def recursiveMakeWriteableHelper(targetPath: Path):
for root, dirs, files in os.walk(targetPath):
for dir_name in dirs:
dir_path = os.path.join(root, dir_name)
os.chmod(dir_path, stat.S_IWRITE)
if not os.access(dir_path, os.W_OK):
os.chmod(dir_path, stat.S_IWRITE | stat.S_IREAD | stat.S_IEXEC)
for file_name in files:
file_path = os.path.join(root, file_name)
os.chmod(file_path, stat.S_IWRITE)
if not os.access(file_path, os.W_OK):
os.chmod(file_path, stat.S_IWRITE | stat.S_IREAD)
def deleteVenvStuff() -> None:
baseAppStoragePath = Path(
QtCore.QStandardPaths.standardLocations(
QtCore.QStandardPaths.StandardLocation.AppDataLocation)[0])
recursiveMakeWriteableHelper(baseAppStoragePath)
shutil.rmtree(baseAppStoragePath)
def installGraphviz() -> None:
if platform.system() == 'Linux':
installGraphvizLinuxHelper()
else:
installGraphvizWindowsHelper()
def installGraphvizLinuxHelper():
subprocess.run(['apt', 'update'])
# https://doc.qt.io/qt-6/linux-requirements.html
# https://github.com/Nuitka/Nuitka/issues/2138
# No need to check if this succeeds - if there are any issues with installation, we will throw
# an error on the install command.
command = subprocess.run(['apt', 'install', 'p7zip-full', 'libopengl0', 'graphviz', 'libmagic1',
'libfontconfig1-dev', 'libfreetype6-dev', 'libatspi2.0-dev',
'libcairo2-dev', 'python3-dev', 'pkg-config', '-y'])
if command.returncode != 0:
raise ValueError('Installing new packages failed, cannot continue installation.')
def installGraphvizWindowsHelper():
graphVizPage = requests.get('https://graphviz.org/download/')
graphVizParts = graphVizPage.text.split('\n')
graphVizDownloadLink = next((chunk.split('"')[1] for chunk in graphVizParts
if '(64-bit) EXE installer' in chunk), "")
if not isinstance(graphVizDownloadLink, str) or graphVizDownloadLink == "":
raise ValueError('Cannot install GraphViz: Failed to locate the latest version of the GraphViz installer.')
graphVizInstallerFileHandler, graphVizInstallerTemp = tempfile.mkstemp()
tempPath = Path(graphVizInstallerTemp)
with os.fdopen(graphVizInstallerFileHandler, 'wb') as tempInstallerFile:
with requests.get(graphVizDownloadLink, stream=True) as fileStream:
for chunk in fileStream.iter_content(chunk_size=5 * 1024 * 1024):
tempInstallerFile.write(chunk)
tempPath.chmod(tempPath.stat().st_mode | 0o111)
subprocess.run([str(tempPath)])
tempPath.unlink(missing_ok=True)
def installPythonLinuxHelper():
subprocess.run('echo "y" | add-apt-repository ppa:deadsnakes/ppa', shell=True)
subprocess.run('apt-get update && apt-get install python3.13 python3.13-venv -y', shell=True)
subprocess.run('wget -O - https://bootstrap.pypa.io/get-pip.py | python3.13', shell=True)
def installPythonWindowsHelper():
win_downloads = requests.get('https://www.python.org/downloads/windows/')
if win_downloads.status_code != 200:
return False
latest_release_path = win_downloads.text.split(
'Python 3.13.', 1)[0].split('href="')[-1].split('"', 1)[0]
latest_release_full_path = f'https://www.python.org{latest_release_path}'
latest_release_page = requests.get(latest_release_full_path)
if latest_release_page.status_code != 200:
return False
latest_release_installer_path = latest_release_page.text.split(
'Windows installer (64-bit)', 1)[0].split('href="')[-1].split('"', 1)[0]
latest_release_binary = requests.get(latest_release_installer_path, allow_redirects=True)
if latest_release_binary.status_code != 200:
return False
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir_path = Path(tmpdir)
installer_file_path = tmpdir_path / 'python.exe'
installer_file_path.touch(mode=0o777)
with open(installer_file_path, 'wb') as file:
file.write(latest_release_binary.content)
ctypes.windll.shell32.ShellExecuteW(None, "runas", installer_file_path, "/quiet", None, 1)
def removeFileHelper(pathToRemove: Path):
if not pathToRemove.exists():
return
if pathToRemove.is_dir():
recursiveMakeWriteableHelper(pathToRemove)
shutil.rmtree(pathToRemove)
else:
os.chmod(pathToRemove, stat.S_IWRITE)
pathToRemove.unlink(missing_ok=True)
class InstallWizard(QtWidgets.QWizard):
# INSTALLER PATHS:
@@ -727,19 +837,10 @@ class InstallWizard(QtWidgets.QWizard):
# 0 -> 1 -> 4 -> 2 -> 5 -> 6 -> -1
# Install, Graphviz exists
# 0 -> 1 -> 4 -> 5 -> 6 -> -1
# Update
# 0 -> 4 -> 2 -> 5 -> 6 -> -1
# -> 5 -> 6 -> -1
def nextId(self) -> int:
if self.currentId() == 0:
if self.currentPage().uninstallRadio.isChecked():
return 3
elif self.currentPage().updateRadio.isChecked():
return 4
else:
# Default action is install.
return 1
# Default action is install.
return 3 if self.currentPage().uninstallRadio.isChecked() else 1
if self.currentId() == 1:
return 4
if self.currentId() == 2:
@@ -763,10 +864,7 @@ class InstallWizard(QtWidgets.QWizard):
self.trayIcon.show()
if len(sys.argv) < 5:
latestReleaseDetails = requests.get('https://github.com/AccentuSoft/LinkScope_Client/releases/latest',
headers={'Accept': 'application/json'})
latestVersion = latestReleaseDetails.json()['tag_name']
downloadURLBase = f"https://github.com/AccentuSoft/LinkScope_Client/releases/download/{latestVersion}/"
downloadURLBase = "https://github.com/AccentuSoft/LinkScope_Client/releases/latest/download/"
if self.currentOS == 'Windows':
try:
@@ -783,11 +881,15 @@ class InstallWizard(QtWidgets.QWizard):
self.graphvizExists = graphvizPath.exists()
self.baseSoftwarePath = Path(os.path.abspath(os.sep)) / 'Program Files' / 'LinkScope'
self.executablePath = self.baseSoftwarePath / 'LinkScope.exe'
self.downloadURL = downloadURLBase + "LinkScope-Windows-x64.7z"
self.downloadURL = f"{downloadURLBase}LinkScope-Windows-x64.7z"
newArgs = ['"' + str(self.desktopShortcutPath) + '"', str(self.graphvizExists),
'"' + str(self.baseSoftwarePath) + '"', '"' + str(self.executablePath) + '"',
'"' + self.downloadURL + '"']
newArgs = [
f'"{str(self.desktopShortcutPath)}"',
str(self.graphvizExists),
f'"{str(self.baseSoftwarePath)}"',
f'"{str(self.executablePath)}"',
f'"{self.downloadURL}"',
]
ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, " ".join(newArgs), None, 1)
sys.exit(0)
elif self.currentOS == 'Linux':
@@ -804,7 +906,7 @@ class InstallWizard(QtWidgets.QWizard):
self.appPath = Path(
os.path.abspath(os.sep)) / 'usr' / 'share' / 'applications' / 'LinkScope.desktop'
self.executablePath = self.baseSoftwarePath / 'LinkScope'
self.downloadURL = downloadURLBase + "LinkScope-Ubuntu-x64.7z"
self.downloadURL = f"{downloadURLBase}LinkScope-Ubuntu-x64.7z"
# No need to wrap these in quotes
newArgs = [str(self.desktopShortcutPath), str(self.graphvizExists), str(self.baseSoftwarePath),
@@ -815,7 +917,7 @@ class InstallWizard(QtWidgets.QWizard):
sudoPassword = QtWidgets.QInputDialog.getText(self, 'Sudo Password',
'Installation requires elevated privileges. '
'Please enter your password: ',
QtWidgets.QLineEdit.Password)
QtWidgets.QLineEdit.EchoMode.Password)
if sudoPassword[1] and sudoPassword[0] != '':
sudoPrivs = subprocess.Popen(['sudo', '-S', '-H', '-k', sys.executable, *newArgs],
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
@@ -865,7 +967,7 @@ class InstallWizard(QtWidgets.QWizard):
sys.exit(-2)
self.appPath = Path(sys.argv[6])
self.setWizardStyle(self.ModernStyle)
self.setWizardStyle(self.WizardStyle.ModernStyle)
self.setWindowTitle('LinkScope Installer')
# Normally one would use enums to keep track of pages, but the installer crashes if we try, so we
@@ -888,38 +990,23 @@ class InstallWizard(QtWidgets.QWizard):
self.addPage(licensePage)
self.addPage(installUpgradePage)
self.addPage(DonePage())
self.setOptions(self.NoBackButtonOnStartPage | self.NoBackButtonOnLastPage | self.CancelButtonOnLeft |
self.NoCancelButtonOnLastPage)
self.setOptions(self.WizardOption.NoBackButtonOnStartPage | self.WizardOption.NoBackButtonOnLastPage |
self.WizardOption.CancelButtonOnLeft | self.WizardOption.NoCancelButtonOnLastPage)
self.show()
def removeFileHelper(self, pathToRemove: Path):
if not pathToRemove.exists():
return
pathToRemove.chmod(0o777)
if pathToRemove.is_dir():
for dirpath, dirnames, filenames in os.walk(pathToRemove):
Path(dirpath).chmod(0o777)
filenames.extend(dirnames)
for filename in filenames:
filePath = Path(dirpath) / filename
filePath.chmod(0o777)
shutil.rmtree(pathToRemove)
else:
pathToRemove.unlink(missing_ok=True)
def createShortcut(self):
self.removeFileHelper(self.desktopShortcutPath)
removeFileHelper(self.desktopShortcutPath)
if self.currentOS == 'Linux':
QtCore.QFile.link(str(self.appPath), str(self.desktopShortcutPath))
elif self.currentOS == 'Windows':
QtCore.QFile.link(str(self.executablePath), str(self.desktopShortcutPath))
def uninstall(self):
self.removeFileHelper(self.desktopShortcutPath)
removeFileHelper(self.desktopShortcutPath)
if self.currentOS == 'Linux':
self.removeFileHelper(self.appPath)
self.removeFileHelper(self.baseSoftwarePath)
removeFileHelper(self.appPath)
removeFileHelper(self.baseSoftwarePath)
def downloadClient(self):
if not isinstance(self.downloadURL, str) or self.downloadURL == "":
@@ -937,58 +1024,39 @@ class InstallWizard(QtWidgets.QWizard):
tempPath.unlink(missing_ok=True)
def downloadGraphviz(self):
graphVizPage = requests.get('https://graphviz.org/download/')
graphVizParts = graphVizPage.text.split('\n')
graphVizDownloadLink = next((chunk.split('"')[1] for chunk in graphVizParts
if '(64-bit) EXE installer' in chunk), "")
if not isinstance(graphVizDownloadLink, str) or graphVizDownloadLink == "":
raise ValueError('Cannot install GraphViz: Failed to locate the latest version of the GraphViz installer.')
graphVizInstallerTemp = tempfile.mkstemp()
tempPath = Path(graphVizInstallerTemp[1])
with os.fdopen(graphVizInstallerTemp[0], 'wb') as tempInstallerFile:
with requests.get(graphVizDownloadLink, stream=True) as fileStream:
for chunk in fileStream.iter_content(chunk_size=5 * 1024 * 1024):
tempInstallerFile.write(chunk)
tempPath.chmod(tempPath.stat().st_mode | 0o111)
subprocess.run([str(tempPath)])
tempPath.unlink(missing_ok=True)
def install(self):
# Assumes we have superuser privileges.
if self.currentOS != 'Linux':
# Assume the user has installed / will install Graphviz.
# We don't actually need to do anything here. Maybe in the future, register application in registry?
return
# Redundant since we always try to update and install, but it's good coding practice.
if not self.graphvizExists:
# No need to check if this succeeds - if there are any issues with installation, we will throw
# an error on the install command.
subprocess.run(['apt', 'update'])
command = subprocess.run(['apt', 'install', 'p7zip-full', 'libopengl0', 'graphviz', 'libmagic1', '-y'])
if command.returncode != 0:
raise ValueError('Installing new packages failed, cannot continue installation.')
# We do not install/configure anything on the virtual env, the first time boot will take longer, but
# it's safer to do it like this since we don't know how the user's system is configured.
self.removeFileHelper(self.appPath)
with open(self.appPath, 'w') as desktopApplicationFile:
desktopApplicationFile.write(LINUX_DESKTOP_FILE_ENTRY)
# Mark desktop file as executable
self.appPath.chmod(self.appPath.stat().st_mode | 0o111)
if self.currentOS == 'Linux':
try:
if subprocess.run(["python3.13", "--version"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True).returncode != 0:
raise ValueError()
except Exception:
installPythonLinuxHelper()
elif not subprocess.check_output(['where', 'python']):
installPythonWindowsHelper()
if not self.graphvizExists or self.currentOS == 'Linux':
installGraphviz()
if self.currentOS == 'Linux':
removeFileHelper(self.appPath)
with open(self.appPath, 'w') as desktopApplicationFile:
desktopApplicationFile.write(LINUX_DESKTOP_FILE_ENTRY)
# Mark desktop file as executable
self.appPath.chmod(self.appPath.stat().st_mode | 0o111)
class IntroInstallUninstallPage(QtWidgets.QWizardPage):
def validatePage(self) -> bool:
if self.updateRadio.isChecked():
self.wizard().page(5).setTitle('Updating LinkScope Installation')
self.wizard().page(5).installProgressLabel.setText('Update Process: ')
self.wizard().page(5).updateSelected = True
else:
self.wizard().page(5).setTitle('Install LinkScope')
self.wizard().page(5).installProgressLabel.setText('Installation Process: ')
self.wizard().page(5).updateSelected = False
self.wizard().page(5).setTitle('Install LinkScope')
self.wizard().page(5).installProgressLabel.setText('Installation Process: ')
return True
def __init__(self):
@@ -1000,17 +1068,13 @@ class IntroInstallUninstallPage(QtWidgets.QWizardPage):
self.setLayout(installUninstallLayout)
actionLabel = QtWidgets.QLabel("Please select the action that you wish to carry out:")
actionLabel.setAlignment(QtCore.Qt.AlignCenter)
actionLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
installUninstallLayout.addWidget(actionLabel)
self.installRadio = QtWidgets.QRadioButton('Install LinkScope')
self.installRadio.setToolTip('Install the LinkScope Client software.')
self.installRadio.setChecked(True)
self.updateRadio = QtWidgets.QRadioButton('Update / Repair LinkScope')
self.updateRadio.setToolTip('Update the existing installation of LinkScope Client, or repair any issues with '
'the existing installation.')
self.uninstallRadio = QtWidgets.QRadioButton('Uninstall LinkScope')
self.uninstallRadio.setToolTip('Uninstall the LinkScope Client software.\nNote that on Linux, any packages '
'that were installed during the installation of LinkScope will not be removed.\n'
@@ -1018,7 +1082,6 @@ class IntroInstallUninstallPage(QtWidgets.QWizardPage):
'on them.')
installUninstallLayout.addWidget(self.installRadio)
installUninstallLayout.addWidget(self.updateRadio)
installUninstallLayout.addWidget(self.uninstallRadio)
@@ -1034,7 +1097,7 @@ class WindowsGraphVizPage(QtWidgets.QWizardPage):
'function, and will be installed along with the software. The licensing terms '
'for GraphViz can be found at: https://graphviz.org/license/.')
graphVizLabel.setWordWrap(True)
graphVizLabel.setAlignment(QtCore.Qt.AlignCenter)
graphVizLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.setLayout(graphVizLayout)
graphVizLayout.addWidget(graphVizLabel)
@@ -1077,12 +1140,11 @@ class LinkScopeInstallLatestPage(QtWidgets.QWizardPage):
'LinkScope. Click "Commit" to start the installation.')
self.createShortcut = False
self.updateSelected = False
self.processStarted = False
self.installThread = None
self.installLabel.setWordWrap(True)
self.installLabel.setAlignment(QtCore.Qt.AlignCenter)
self.installLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
installLayout.addWidget(self.installLabel)
self.installProgressWidget = QtWidgets.QWidget()
@@ -1097,7 +1159,7 @@ class LinkScopeInstallLatestPage(QtWidgets.QWizardPage):
self.downloadingLabel = QtWidgets.QLabel('Downloading files. This may take some time...')
self.downloadingLabel.setWordWrap(True)
self.downloadingLabel.setAlignment(QtCore.Qt.AlignCenter)
self.downloadingLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.downloadingLabel.setVisible(False)
installLayout.addWidget(self.installProgressWidget)
@@ -1109,8 +1171,9 @@ class LinkScopeInstallLatestPage(QtWidgets.QWizardPage):
class LinkScopeUninstallPage(QtWidgets.QWizardPage):
def doStuff(self):
self.progressBar.setValue(3)
try:
deleteVenvStuff()
self.progressBar.setValue(3)
self.wizard().uninstall()
except Exception as e:
self.wizard().page(6).doneLabel.setText('Error occurred during uninstallation: ' +
@@ -1135,7 +1198,7 @@ class LinkScopeUninstallPage(QtWidgets.QWizardPage):
self.uninstallLabel = QtWidgets.QLabel('The installer will now uninstall LinkScope from this computer. Click '
'"Commit" to begin the removal process.')
self.uninstallLabel.setWordWrap(True)
self.uninstallLabel.setAlignment(QtCore.Qt.AlignCenter)
self.uninstallLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
uninstallLayout.addWidget(self.uninstallLabel)
self.uninstallProgressWidget = QtWidgets.QWidget()
@@ -1183,7 +1246,7 @@ class CreateDesktopShortcutPage(QtWidgets.QWizardPage):
desktopShortcutLayout = QtWidgets.QVBoxLayout()
self.setLayout(desktopShortcutLayout)
desktopShortcutLabel = QtWidgets.QLabel('Create a Shortcut for LinkScope on the Desktop?')
desktopShortcutLabel.setAlignment(QtCore.Qt.AlignCenter)
desktopShortcutLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.shortcutCheckbox = QtWidgets.QCheckBox('Create a Desktop Shortcut?')
self.shortcutCheckbox.setChecked(True)
@@ -1202,7 +1265,7 @@ class LicensePage(QtWidgets.QWizardPage):
self.setLayout(licenseLayout)
licenseLabel = QtWidgets.QLabel('Please review carefully the license terms for the LinkScope Client software.')
licenseLabel.setAlignment(QtCore.Qt.AlignCenter)
licenseLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
licenseLabel.setWordWrap(True)
licenseText = QtWidgets.QPlainTextEdit(AGPL_LICENSE)
@@ -1215,8 +1278,6 @@ class LicensePage(QtWidgets.QWizardPage):
rejectLicense.setChecked(True)
self.registerField('Accept Terms*', self.acceptLicense)
self.setMinimumWidth(550)
self.setMinimumHeight(600)
licenseLayout.addWidget(licenseLabel)
licenseLayout.addWidget(licenseText)
@@ -1235,7 +1296,7 @@ class DonePage(QtWidgets.QWizardPage):
self.setLayout(doneLayout)
self.doneLabel = QtWidgets.QLabel('Thank you for using LinkScope!\nClick "Finish" to exit the installer.')
self.doneLabel.setWordWrap(True)
self.doneLabel.setAlignment(QtCore.Qt.AlignCenter)
self.doneLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
doneLayout.addWidget(self.doneLabel)
@@ -1250,22 +1311,15 @@ class InstallThread(QtCore.QThread):
def run(self) -> None:
try:
# We have already installed Graphviz on Linux at this point.
if not self.wizard.graphvizExists and self.wizard.currentOS == 'Windows':
self.progressSignal.emit(1)
self.wizard.downloadGraphviz()
self.progressSignal.emit(2)
if self.installPage.updateSelected:
self.wizard.uninstall()
self.wizard.install()
self.installPage.downloadingLabel.setVisible(True)
self.installPage.downloadingLabel.setHidden(False)
self.progressSignal.emit(4)
self.wizard.downloadClient()
self.progressSignal.emit(8)
shortcutExists = self.wizard.desktopShortcutPath.exists()
self.progressSignal.emit(9)
if self.installPage.createShortcut or (self.installPage.updateSelected and shortcutExists):
if self.installPage.createShortcut:
self.wizard.createShortcut()
self.progressSignal.emit(10)
self.doneSignal.emit(True)
@@ -1278,5 +1332,7 @@ class InstallThread(QtCore.QThread):
if __name__ == '__main__':
application = QtWidgets.QApplication(sys.argv)
application.setOrganizationName("AccentuSoft")
application.setApplicationName("LinkScope Client")
installWizard = InstallWizard()
sys.exit(application.exec())

File diff suppressed because it is too large Load Diff

View File

@@ -1,46 +0,0 @@
#!/usr/bin/env python3
class AircraftInquiryByDealer:
name = "Aircraft Inquiry By Dealer"
category = "Aircraft"
description = "Find information about aircraft identifications from https://registry.faa.gov/aircraftinquiry/"
originTypes = {"Company"}
resultTypes = {'Phrase', 'Company'}
parameters = {}
def resolution(self, entityJsonList, parameters):
import requests
import pandas as pd
from requests_futures.sessions import FuturesSession
from concurrent.futures import as_completed
futures = []
uidList = []
return_result = []
submit_url = "https://registry.faa.gov/aircraftinquiry/Search/"
crafted_url = f"{submit_url}DealerResult"
with FuturesSession(max_workers=15) as session:
for entity in entityJsonList:
uidList.append(entity['uid'])
futures.append(session.post(crafted_url, data={"Dealertxt": entity['Company Name']}))
for future in as_completed(futures):
uid = uidList[futures.index(future)]
try:
df_list = pd.read_html(future.result().text)
except requests.exceptions.ConnectionError:
return "Please check your internet connection"
except ValueError:
return_result = []
return return_result
df = df_list[0]
for certificate_index in range(len(df["Certificate Number"])):
index_of_child = len(return_result)
return_result.append([{'Company Name': df["Name"][certificate_index],
'Entity Type': 'Company'},
{uid: {'Resolution': 'Aircraft Dealer', 'Notes': ''}}])
return_result.append([{'Phrase': df["Certificate Number"][certificate_index],
'Entity Type': 'Phrase'},
{index_of_child: {'Resolution': 'Aircraft Certificate Number', 'Notes': ''}}])
return return_result

View File

@@ -1,51 +0,0 @@
#!/usr/bin/env python3
class AircraftInquiryByEngine:
name = "Aircraft Inquiry By Engine"
category = "Aircraft"
description = "Find information about aircraft identifications from https://registry.faa.gov/aircraftinquiry/"
originTypes = {"Phrase"}
resultTypes = {"Phrase"}
parameters = {'Manufacturer': {'description': "Enter the Manufacturer of the Engine Model",
'type': 'String',
'value': 'None'}}
def resolution(self, entityJsonList, parameters):
import requests
from requests_futures.sessions import FuturesSession
from concurrent.futures import as_completed
import pandas as pd
Manufacturer = parameters['Manufacturer']
futures = []
uidList = []
return_result = []
submit_url = "https://registry.faa.gov/aircraftinquiry/Search/"
crafted_url = f"{submit_url}EngineReferenceResult"
with FuturesSession(max_workers=15) as session:
for entity in entityJsonList:
uidList.append(entity['uid'])
futures.append(session.post(crafted_url, data={"Modeltxt": entity['Phrase'],
"MfrNametxt": Manufacturer}))
for future in as_completed(futures):
uid = uidList[futures.index(future)]
try:
df_list = pd.read_html(future.result().text)
except requests.exceptions.ConnectionError:
return "Please check your internet connection"
except ValueError:
return "No results retrieved"
df = df_list[0]
return_result.append([{'Phrase': f"Model Code:str({df['Mfr/Mdl Code']})",
'Entity Type': 'Phrase'},
{uid: {'Resolution': 'Aircraft Model Code', 'Notes': ''}}])
return_result.append([{'Phrase': f"Engine Type:{df['Type Engine']}",
'Entity Type': 'Phrase'},
{uid: {'Resolution': 'Aircraft Engine Type', 'Notes': ''}}])
return_result.append([{'Phrase': f"Horse Power:str({df['Horsepower']})",
'Entity Type': 'Phrase'},
{uid: {'Resolution': 'Aircraft Engine Horsepower', 'Notes': ''}}])
return return_result

View File

@@ -1,60 +0,0 @@
#!/usr/bin/env python3
class AircraftInquiryByNNumber:
name = "Aircraft Inquiry By N-Number"
category = "Aircraft"
description = "Find information about aircraft identifications from https://registry.faa.gov/aircraftinquiry/"
originTypes = {"Phrase"}
resultTypes = {'Phrase', 'Person', 'Identification Number', 'Company', 'Country', 'City'}
parameters = {}
def resolution(self, entityJsonList, parameters):
import requests
from requests_futures.sessions import FuturesSession
from concurrent.futures import as_completed
import pandas as pd
futures = []
uidList = []
return_result = []
submit_url = "https://registry.faa.gov/aircraftinquiry/Search/"
crafted_url = f"{submit_url}NNumberResult"
with FuturesSession(max_workers=15) as session:
for entity in entityJsonList:
uidList.append(entity['uid'])
futures.append(session.post(crafted_url, data={"NNumbertxt": entity['Phrase']}))
for future in as_completed(futures):
uid = uidList[futures.index(future)]
try:
df_list = pd.read_html(future.result().text)
except requests.exceptions.ConnectionError:
return "Please check your internet connection"
except ValueError:
return "No results retrieved"
df1 = df_list[0]
df2 = df_list[1]
df3 = df_list[2]
return_result.append([{'ID Number': df1[1][0],
'Entity Type': 'Identification Number'},
{uid: {'Resolution': 'Aircraft Identification Number', 'Notes': ''}}])
return_result.append([{'Company Name': df1[1][1],
'Entity Type': 'Company'},
{uid: {'Resolution': 'Aircraft Company', 'Notes': ''}}])
return_result.append([{'Full Name': df2[1][0],
'Entity Type': 'Person'},
{uid: {'Resolution': 'Aircraft Owner', 'Notes': ''}}])
return_result.append([{'City Name': df2[1][2],
'Entity Type': 'City'},
{uid: {'Resolution': "Aircraft Owner's City", 'Notes': ''}}])
return_result.append([{'Country Name': df2[1][4],
'Entity Type': 'Country'},
{uid: {'Resolution': "Aircraft Owner's Country", 'Notes': ''}}])
return_result.append([{'Phrase': df3[1][1],
'Entity Type': 'Phrase'},
{uid: {'Resolution': "Aircraft Engine Series", 'Notes': ''}}])
return_result.append([{'Phrase': df3[1][2],
'Entity Type': 'Phrase'},
{uid: {'Resolution': "Aircraft Engine Motor", 'Notes': ''}}])
return return_result

View File

@@ -1,47 +0,0 @@
#!/usr/bin/env python3
class AircraftInquiryByPersonName:
name = "Aircraft Inquiry By Person Name"
category = "Aircraft"
description = "Find information about aircraft identifications from https://registry.faa.gov/aircraftinquiry/"
originTypes = {"Person"}
resultTypes = {'Phrase', 'Person', 'Identification Number', 'Company'}
parameters = {}
def resolution(self, entityJsonList, parameters):
import requests
from requests_futures.sessions import FuturesSession
from concurrent.futures import as_completed
import pandas as pd
futures = []
uidList = []
return_result = []
submit_url = "https://registry.faa.gov/aircraftinquiry/Search/"
crafted_url = f"{submit_url}NameResult"
with FuturesSession(max_workers=15) as session:
for entity in entityJsonList:
uidList.append(entity['uid'])
futures.append(session.post(crafted_url, data={"nametxt": entity['Full Name'], "sort_option": "1"}))
for future in as_completed(futures):
uid = uidList[futures.index(future)]
try:
df_list = pd.read_html(future.result().text)
except requests.exceptions.ConnectionError:
return "Please check your internet connection"
except ValueError:
return "No results retrieved"
df = df_list[0]
for i in range(len(df["N-Number"])):
return_result.append([{'Phrase': df["N-Number"][0],
'Entity Type': 'Phrase'},
{uid: {'Resolution': 'Aircraft N-Number', 'Notes': ''}}])
return_result.append([{'ID Number': str(df['Serial Number'][0]),
'Entity Type': 'Identification Number'},
{uid: {'Resolution': 'Aircraft Identification Number', 'Notes': ''}}])
return_result.append([{'Company Name': df['Manufacturer Name Model'][0],
'Entity Type': 'Company'},
{uid: {'Resolution': 'Aircraft Manufacturer Name', 'Notes': ''}}])
return return_result

View File

@@ -1,47 +0,0 @@
#!/usr/bin/env python3
class AircraftInquiryBySerialNumber:
name = "Aircraft Inquiry By Serial Number"
category = "Aircraft"
description = "Find information about aircraft identifications from https://registry.faa.gov/aircraftinquiry/"
originTypes = {"Identification Number"}
resultTypes = {'Phrase', 'Person', 'Identification Number', 'Company'}
parameters = {}
def resolution(self, entityJsonList, parameters):
import requests
from requests_futures.sessions import FuturesSession
from concurrent.futures import as_completed
import pandas as pd
futures = []
uidList = []
return_result = []
submit_url = "https://registry.faa.gov/aircraftinquiry/Search/"
crafted_url = f"{submit_url}SerialResult"
with FuturesSession(max_workers=15) as session:
for entity in entityJsonList:
uidList.append(entity['uid'])
futures.append(session.post(crafted_url, data={"Serialtxt": entity['ID Number'], "sort_option": "1"}))
for future in as_completed(futures):
uid = uidList[futures.index(future)]
try:
df_list = pd.read_html(future.result().text)
except requests.exceptions.ConnectionError:
return "Please check your internet connection"
except ValueError:
return "No results retrieved"
df = df_list[0]
for i in range(len(df["N-Number"])):
return_result.append([{'Company Name': df["Manufacturer Name"][i],
'Entity Type': 'Company'},
{uid: {'Resolution': 'Aircraft Manufacturer', 'Notes': ''}}])
return_result.append([{'Phrase': df["N-Number"][i],
'Entity Type': 'Phrase'},
{uid: {'Resolution': 'Aircraft N-Number', 'Notes': ''}}])
return_result.append([{'Phrase': df["Model"][i],
'Entity Type': 'Phrase'},
{uid: {'Resolution': 'Aircraft Model', 'Notes': ''}}])
return return_result

View File

@@ -1,3 +0,0 @@
requests
pandas
requests-futures

View File

@@ -1,18 +0,0 @@
<Aleph>
<Aleph_ID>
<Attributes>
<Attribute default="Aleph Default ID" check="String" primary="True">ID</Attribute>
</Attributes>
<Icon>
Default.svg
</Icon>
</Aleph_ID>
<Aleph_Collection_ID>
<Attributes>
<Attribute default="Aleph Default Collection ID" check="String" primary="True">ID</Attribute>
</Attributes>
<Icon>
Default.svg
</Icon>
</Aleph_Collection_ID>
</Aleph>

View File

@@ -1,273 +0,0 @@
#!/usr/bin/env python3
class Aleph_Entity_Search:
name = "Aleph Entity Search"
category = "Aleph OCCRP"
description = "Find information about a given search parameter"
originTypes = {'Phrase', 'Person', 'Politically Exposed Person'}
resultTypes = {'Phrase'}
parameters = {'Max Results': {'description': 'The maximum number of results to return.',
'type': 'String',
'value': 'Enter the number of results you want returned',
'default': '1'},
'Aleph Disclaimer': {'description': 'The content on Aleph is provided for general information only.\n'
'It is not intended to amount to advice on which you should place'
'sole and entire reliance.\n'
'We recommend that you conduct your own independent fact checking'
'against the data and materials that you access on Aleph.\n'
'Aleph API is not a replacement for traditional due diligence '
'checks and know-your-customer background checks.',
'type': 'String',
'value': 'Type "Accept" (without quotes) to confirm your understanding.',
'global': True}
}
def resolution(self, entityJsonList, parameters):
import requests
import pycountry
import time
from requests_futures.sessions import FuturesSession
from concurrent.futures import as_completed
return_result = []
uidList = []
futures = []
url = "https://aleph.occrp.org/api/2/entities"
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
gender = "None"
if parameters['Aleph Disclaimer'] != 'Accept':
return "Please Accept the Terms for Aleph."
try:
max_results = int(parameters['Max Results'])
except ValueError:
return "The value for parameter 'Max Results' is not a valid integer."
if max_results <= 0:
return []
with FuturesSession(max_workers=15) as session:
for entity in entityJsonList:
uidList.append(entity['uid'])
primary_field = entity[list(entity)[1]].strip()
crafted_url = url + f"?q={primary_field}&filter:schemata=Thing&limit={max_results}"
time.sleep(1)
futures.append(session.get(crafted_url, headers=headers))
for future in as_completed(futures):
uid = uidList[futures.index(future)]
try:
response = future.result().json()
except requests.exceptions.ConnectionError:
return "Please check your internet connection"
# print(response)
for schema in response['results']:
index_of_child = len(return_result)
try:
if schema['schema'] == "Person":
if schema['properties'].get('gender') is not None \
and schema['properties'].get('gender')[0] == "F":
gender = "Female"
elif schema['properties'].get('gender') is not None \
and schema['properties'].get('gender')[0] == "M":
gender = "Male"
if schema['properties'].get('legalForm') is not None:
return_result.append(
[{'Full Name': schema['properties']['name'][0],
'Gender': gender,
'Date Of Birth': str(schema['properties'].get('birthDate')),
'Notes': f"{schema['links']['self']}\nLegal Form: {schema['properties']['legalForm'][0]}",
'Entity Type': 'Person'},
{uid: {'Resolution': 'Person Entity', 'Notes': ''}}])
else:
return_result.append(
[{'Full Name': str(schema['properties']['name'][0]),
'Gender': gender,
'Date Of Birth': str(schema['properties']['birthDate'][0]),
'Notes': schema['links']['self'],
'Entity Type': 'Person'},
{uid: {'Resolution': 'Person Entity', 'Notes': ''}}])
if schema['properties'].get('registrationNumber') is not None:
return_result.append(
[{'Registration Number': str(schema['properties']['registrationNumber'][0]),
'Notes': '',
'Entity Type': 'Company'},
{index_of_child: {'Resolution': 'Aleph Registration Number', 'Notes': ''}}])
if schema['properties'].get('country') is not None:
return_result.append(
[{'Country Name': str(
pycountry.countries.get(alpha_2=schema['properties']['country'][0]).name),
'Notes': '',
'Entity Type': 'Country'},
{index_of_child: {'Resolution': 'Country of Origin', 'Notes': ''}}])
if schema['properties'].get('addressEntity'):
return_result.append(
[{'Street Address': str(
schema['properties']['addressEntity'][0]['properties']['full'][0]),
'Notes': '',
'Entity Type': 'Address'},
{index_of_child: {'Resolution': 'Address Entity', 'Notes': ''}}])
else:
return_result.append(
[{'Street Address': str(schema['properties']['address'][0]),
'Notes': '',
'Entity Type': 'Address'},
{index_of_child: {'Resolution': 'Address Entity', 'Notes': ''}}])
return_result.append(
[{'ID': str(schema['id']),
'Notes': '',
'Entity Type': 'Aleph ID'},
{index_of_child: {'Resolution': 'Aleph ID', 'Notes': ''}}])
return_result.append(
[{'Phrase': str(schema['collection']['label']),
'Notes': str(schema['collection']['summary']),
'Entity Type': 'Phrase'},
{index_of_child: {'Resolution': 'Aleph Collection', 'Notes': ''}}])
return_result.append(
[{'ID': str(schema['collection']['collection_id']),
'Notes': '',
'Entity Type': 'Aleph Collection ID'},
{index_of_child: {'Resolution': 'Aleph Collection ID', 'Notes': ''}}])
elif schema['schema'] == "Organization":
return_result.append(
[{'Organization Name': str(schema['properties']['name'][0]),
'Registration Number': str(schema['properties']['registrationNumber'][0]),
'Notes': f"{schema['links']['self']}\nLegal Form: {schema['properties']['legalForm'][0]}\n"
f"Source URL: {schema['properties']['sourceUrl'][0]}",
'Entity Type': 'Organization'},
{uid: {'Resolution': 'Aleph Organisation Entity', 'Notes': ''}}])
return_result.append(
[{'Country Name': str(
pycountry.countries.get(alpha_2=schema['properties']['country'][0]).name),
'Notes': '',
'Entity Type': 'Country'},
{index_of_child: {'Resolution': "Aleph Organisation Country", 'Notes': ''}}])
return_result.append(
[{'Street Address': str(schema['properties']['address'][0]),
'Notes': '',
'Entity Type': 'Address'},
{index_of_child: {'Resolution': "Aleph Organisation Address", 'Notes': ''}}])
return_result.append(
[{'ID': str(schema['id']),
'Notes': '',
'Entity Type': 'Aleph ID'},
{index_of_child: {'Resolution': "Aleph Organisation ID", 'Notes': ''}}])
return_result.append(
[{'Phrase': str(schema['collection']['label']),
'Notes': str(schema['collection']['summary']),
'Entity Type': 'Phrase'},
{index_of_child: {'Resolution': 'Aleph Collection', 'Notes': ''}}])
return_result.append(
[{'Phone Number': str(schema['properties']['phone'][0]),
'Notes': '',
'Entity Type': 'Phone Number'},
{index_of_child: {'Resolution': 'Phone Number', 'Notes': ''}}])
return_result.append(
[{'Phrase': str(schema['properties']['classification'][0]),
'Notes': '',
'Entity Type': 'Phrase'},
{index_of_child: {'Resolution': 'Organisation Classification', 'Notes': ''}}])
return_result.append(
[{'Phrase': str(schema['collection']['collection_id']),
'Notes': '',
'Entity Type': 'Phrase'},
{index_of_child: {'Resolution': 'Aleph Collection ID', 'Notes': ''}}])
elif schema['schema'] == "Pages":
if 'updated_at' in schema:
date_created = schema['updated_at']
else:
date_created = schema['created_at']
doc_name = 'Document: ' + schema['properties']['title'][0]
entity_link = schema['links']['self']
file_link = schema['links']['file']
source_url = schema['properties']['sourceUrl'][0]
return_result.append(
[{'Phrase': doc_name,
'Source': source_url,
'Notes': 'Link to Aleph Entity: ' + entity_link + '\n\n' +
'Link to document: ' + file_link,
'Entity Type': 'Phrase',
'Date Created': date_created},
{uid: {'Resolution': 'Aleph Document', 'Notes': ''}}])
elif schema['properties']['parent'][0]['schema'] == "Person":
gender = str(schema['properties']['parent'][0]['properties'].get('gender')[0])
if schema['properties']['parent'][0]['properties'].get('legalForm') is not None:
return_result.append(
[{'Full Name': schema['properties']['parent'][0]['properties']['name'][0],
'Gender': gender,
'Date Of Birth': str(schema['properties']['parent'][0]['properties']['birthDate'][0]),
'Notes': f"{schema['properties']['parent'][0]['links']['self']}\nLegal Form: "
f"{schema['properties']['parent'][0]['properties']['legalForm'][0]}",
'Entity Type': 'Person'},
{uid: {'Resolution': 'Aleph Person Entity', 'Notes': ''}}])
else:
return_result.append(
[{'Full Name': schema['properties']['parent'][0]['properties']['name'][0],
'Gender': gender,
'Date Of Birth': str(schema['properties']['parent'][0]['properties']['birthDate'][0]),
'Notes': schema['properties']['parent'][0]['links']['self'],
'Entity Type': 'Person'},
{uid: {'Resolution': 'Aleph Person Entity', 'Notes': ''}}])
if schema['properties']['parent'][0]['properties'].get('registrationNumber') is not None:
return_result.append(
[{'Registration Number': str(
schema['properties']['parent'][0]['properties']['registrationNumber'][0]),
'Notes': '',
'Entity Type': 'Company'},
{index_of_child: {'Resolution': 'Company Registration Number', 'Notes': ''}}])
if schema['properties']['parent'][0]['properties'].get('country') is not None:
return_result.append(
[{'Country Name': str(
pycountry.countries.get(
alpha_2=schema['properties']['parent'][0]['properties']['country'][0]).name),
'Notes': '',
'Entity Type': 'Country'},
{index_of_child: {'Resolution': 'Country', 'Notes': ''}}])
return_result.append(
[{'ID': str(schema['properties']['parent'][0]['id']),
'Notes': '',
'Entity Type': 'Aleph ID'},
{index_of_child: {'Resolution': 'Aleph ID', 'Notes': ''}}])
return_result.append(
[{'Phrase': str(schema['properties']['parent'][0]['collection']['label']),
'Notes': str(schema['properties']['parent'][0]['collection']['summary']),
'Entity Type': 'Phrase'},
{index_of_child: {'Resolution': 'Aleph Collection Entity', 'Notes': ''}}])
return_result.append(
[{'Phrase': str(schema['properties']['parent'][0]['collection']['collection_id']),
'Notes': '',
'Entity Type': 'Phrase'},
{index_of_child: {'Resolution': 'Aleph Collection ID', 'Notes': ''}}])
index_of_child_of_child = len(return_result)
return_result.append(
[{'Company Name': str(schema['properties']['name'][0]),
'Notes': schema['links']['self'],
'Entity Type': 'Company'},
{index_of_child: {'Resolution': 'Aleph Company Entity', 'Notes': ''}}])
return_result.append(
[{'Street Address': str(schema['properties']['addressEntity'][0]['properties']['full'][0]),
'Notes': '',
'Entity Type': 'Address'},
{index_of_child_of_child: {'Resolution': 'Aleph Person Address', 'Notes': ''}}])
for country_code in schema['collection']['countries']:
return_result.append(
[{'Country Name': str(pycountry.countries.get(alpha_2=country_code).name),
'Notes': '',
'Entity Type': 'Country'},
{index_of_child_of_child: {'Resolution': 'Country', 'Notes': ''}}])
return_result.append(
[{'Phrase': str(schema['collection']['label']),
'Notes': str(schema['collection']['summary']),
'Entity Type': 'Phrase'},
{index_of_child_of_child: {'Resolution': 'Aleph Collection Entity', 'Notes': ''}}])
return_result.append(
[{'ID': str(schema['collection']['collection_id']),
'Notes': '',
'Entity Type': 'Aleph Collection ID'},
{index_of_child_of_child: {'Resolution': 'Aleph Entity Search', 'Notes': ''}}])
except (TypeError, KeyError):
# print(repr(e))
continue
return return_result

View File

@@ -1,98 +0,0 @@
#!/usr/bin/env python3
class GetCollectionByID:
name = "Get Collections By ID"
category = "Aleph OCCRP"
description = "Find information about Collections and their IDs"
originTypes = {'Phrase'}
resultTypes = {'Phrase. Person, Address, Phone Number, Email Address, Country, Bank Account'}
parameters = {'Aleph Disclaimer': {'description': 'The content on Aleph is provided for general information only.\n'
'It is not intended to amount to advice on which you should place'
'sole and entire reliance.\n'
'We recommend that you conduct your own independent fact checking'
'against the data and materials that you access on Aleph.\n'
'Aleph API is not a replacement for traditional due diligence '
'checks and know-your-customer background checks.',
'type': 'String',
'value': 'Type "Accept" (without quotes) to confirm your understanding.',
'global': True}}
def resolution(self, entityJsonList, parameters):
import time
import requests
from requests_futures.sessions import FuturesSession
from concurrent.futures import as_completed
returnResults = []
futures = []
uidList = []
if parameters['Aleph Disclaimer'] != 'Accept':
return "Please Accept the Terms for Aleph."
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
with FuturesSession(max_workers=15) as session:
for entity in entityJsonList:
uidList.append(entity['uid'])
primary_field = entity[list(entity)[1]].strip()
url = f"https://aleph.occrp.org/api/2/collections/{primary_field}"
time.sleep(1)
futures.append(session.get(url, headers=headers))
for future in as_completed(futures):
uid = uidList[futures.index(future)]
try:
response = future.result().json()
except requests.exceptions.ConnectionError:
return "Please check your internet connection"
if response['statistics']['names'].get('values') is not None:
nameKeys = list(response['statistics']['names'].get('values').keys())
for nameKey in nameKeys:
returnResults.append([{'Full Name': str(nameKey),
'Entity Type': 'Person'},
{uid: {'Resolution': 'Person Entity',
'Notes': ''}}])
if response['statistics']['addresses'].get('values') is not None:
addressKeys = list(response['statistics']['addresses'].get('values').keys())
for addressKey in addressKeys:
returnResults.append([{'Street Address': str(addressKey),
'Entity Type': 'Address'},
{uid: {'Resolution': 'Address Entity',
'Notes': ''}}])
if response['statistics']['phones'].get('values') is not None:
phoneKeys = list(response['statistics']['phones'].get('values').keys())
for phoneKey in phoneKeys:
returnResults.append([{'Phone Number': str(phoneKey),
'Entity Type': 'Phone Number'},
{uid: {'Resolution': 'Phone Number Entity',
'Notes': ''}}])
if response['statistics']['emails'].get('values') is not None:
emailKeys = list(response['statistics']['emails'].get('values').keys())
for emailKey in emailKeys:
returnResults.append([{'Email Address': str(emailKey),
'Entity Type': 'Email Address'},
{uid: {'Resolution': 'Email Address Entity',
'Notes': ''}}])
if response['statistics']['countries'].get('values') is not None:
countriesKeys = list(response['statistics']['countries'].get('values').keys())
for countriesKey in countriesKeys:
returnResults.append([{'Country Name': str(countriesKey),
'Entity Type': 'Country'},
{uid: {'Resolution': 'Country Entity',
'Notes': ''}}])
if response['statistics']['languages'].get('values') is not None:
languagesKeys = list(response['statistics']['languages'].get('values').keys())
for languagesKey in languagesKeys:
returnResults.append([{'Phrase': str(languagesKey),
'Entity Type': 'Phrase'},
{uid: {'Resolution': 'Language Entity',
'Notes': ''}}])
if response['statistics']['ibans'].get('values') is not None:
ibansKeys = list(response['statistics']['ibans'].get('values').keys())
for ibansKey in ibansKeys:
returnResults.append([{'Account Number': str(ibansKey),
'Entity Type': 'Bank Account'},
{uid: {'Resolution': 'IBAN Entity',
'Notes': ''}}])
return returnResults

View File

@@ -1,73 +0,0 @@
#!/usr/bin/env python3
class GetCollectionsInfo:
name = "Get Collections Info"
category = "Aleph OCCRP"
description = "Find information about Collections and their IDs"
originTypes = {'Phrase'}
resultTypes = {'Phrase, Aleph ID'}
parameters = {'Max Results': {'description': 'Please enter the maximum number of results to return.',
'type': 'String',
'default': '1'},
'Aleph Disclaimer': {'description': 'The content on Aleph is provided for general information only.\n'
'It is not intended to amount to advice on which you should place'
'sole and entire reliance.\n'
'We recommend that you conduct your own independent fact checking'
'against the data and materials that you access on Aleph.\n'
'Aleph API is not a replacement for traditional due diligence '
'checks and know-your-customer background checks.',
'type': 'String',
'value': 'Type "Accept" (without quotes) to confirm your understanding.',
'global': True}
}
def resolution(self, entityJsonList, parameters):
import time
import requests
from requests_futures.sessions import FuturesSession
from concurrent.futures import as_completed
returnResults = []
futures = []
uidList = []
if parameters['Aleph Disclaimer'] != 'Accept':
return "Please Accept the Terms for Aleph."
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
try:
maxResults = int(parameters['Max Results'])
except ValueError:
return "The value for parameter 'Max Results' is not a valid integer."
if maxResults <= 0:
return []
with FuturesSession(max_workers=15) as session:
for entity in entityJsonList:
uidList.append(entity['uid'])
url = f"https://aleph.occrp.org/api/2/collections?offset=0&limit=300&page"
time.sleep(1)
futures.append(session.get(url, headers=headers))
for future in as_completed(futures):
uid = uidList[futures.index(future)]
try:
response = future.result().json()
except requests.exceptions.ConnectionError:
return "Please check your internet connection"
collections = response['results'][:maxResults]
for collection in collections:
index_of_child = len(returnResults)
returnResults.append([{'Phrase': collection['label'],
'Notes': str(collection.get('summary')),
'Entity Type': 'Phrase'},
{uid: {'Resolution': 'Aleph Collection Name',
'Notes': ''}}])
returnResults.append([{'ID': collection['id'],
'Entity Type': 'Aleph ID'},
{index_of_child: {'Resolution': 'Aleph Collection ID',
'Notes': ''}}])
return returnResults

View File

@@ -1,102 +0,0 @@
#!/usr/bin/env python3
class GetSimilarEntities:
name = "Get Similar Entities"
category = "Aleph OCCRP"
description = "Find information about similar entities"
originTypes = {'Phrase', 'Person', 'Politically Exposed Person'}
resultTypes = {'Phrase', 'Person', 'Address', 'Aleph ID'}
parameters = {'Aleph Disclaimer': {'description': 'The content on Aleph is provided for general information only.\n'
'It is not intended to amount to advice on which you should place'
'sole and entire reliance.\n'
'We recommend that you conduct your own independent fact checking'
'against the data and materials that you access on Aleph.\n'
'Aleph API is not a replacement for traditional due diligence '
'checks and know-your-customer background checks.',
'type': 'String',
'value': 'Type "Accept" (without quotes) to confirm your understanding.',
'global': True}}
def resolution(self, entityJsonList, parameters):
import time
import requests
import pycountry
from requests_futures.sessions import FuturesSession
from concurrent.futures import as_completed
returnResults = []
futures = []
uidList = []
if parameters['Aleph Disclaimer'] != 'Accept':
return "Please Accept the Terms for Aleph."
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
with FuturesSession(max_workers=15) as session:
for entity in entityJsonList:
uidList.append(entity['uid'])
primary_field = entity[list(entity)[1]].strip()
url = f"https://aleph.occrp.org/api/2/entities/{primary_field}/similar"
time.sleep(1)
futures.append(session.get(url, headers=headers))
for future in as_completed(futures):
uid = uidList[futures.index(future)]
try:
response = future.result().json()
except requests.exceptions.ConnectionError:
return "Please check your internet connection"
entities = response['results']
for schema in entities:
if schema['entity']['schema'] == 'Person':
index_of_child = len(returnResults)
returnResults.append([{'Full Name': ' '.join(map(str, schema['entity']['properties']['name'])),
'Gender': ' '.join(map(str, schema['entity']['properties']['gender'])),
'Notes': ' '.join(map(str, schema['entity']['properties']['legalForm'])),
'Entity Type': 'Person'},
{uid: {'Resolution': 'Person Entity',
'Notes': ''}}])
country = pycountry.countries.get(alpha_2=schema['entity']['properties']['country'][0]).name
returnResults.append([{'Street Address': schema['entity']['properties']['addressEntity'][0]
['properties']['full'][0],
'Postal Code': schema['entity']['properties']['addressEntity'][0]
['properties']['postalCode'][0],
'Country': country,
'Entity Type': 'Address'},
{index_of_child: {'Resolution': 'Address',
'Notes': ''}}])
returnResults.append([{'Phrase': schema['entity']['collection']['label'],
'Notes': schema['entity']['collection']['summary'],
'Entity Type': 'Phrase'},
{index_of_child: {'Resolution': 'Location in Database',
'Notes': ''}}])
returnResults.append([{'ID': schema['entity']['id'],
'Entity Type': 'Aleph ID'},
{index_of_child: {'Resolution': 'ID in Database',
'Notes': ''}}])
elif schema['entity']['schema'] == 'Company':
index_of_child = len(returnResults)
returnResults.append([{'Company Name': ' '.join(map(str, schema['entity']['properties']['name'])),
'Notes': str(schema['entity']['properties']['status']),
'Entity Type': 'Company'},
{uid: {'Resolution': 'Company Entity',
'Notes': ''}}])
country = pycountry.countries.get(alpha_2=schema['entity']['properties']['country'][0]).name
returnResults.append([{'Street Address': str(schema['entity']['properties'].get('address')),
'Country': country,
'Entity Type': 'Address'},
{index_of_child: {'Resolution': 'Address',
'Notes': ''}}])
returnResults.append([{'Phrase': schema['entity']['collection']['label'],
'Notes': schema['entity']['collection']['summary'],
'Entity Type': 'Phrase'},
{index_of_child: {'Resolution': 'Location in Database',
'Notes': ''}}])
returnResults.append([{'ID': schema['entity']['id'],
'Entity Type': 'Aleph ID'},
{index_of_child: {'Resolution': 'ID in Database',
'Notes': ''}}])
return returnResults

View File

@@ -1,3 +0,0 @@
requests
pycountry
requests-futures

View File

@@ -1,280 +0,0 @@
#!/usr/bin/env python3
# User needs to be in docker group or to have root privileges
class Amass_Domain:
name = "Amass Domain Scan"
category = "Network Infrastructure"
description = "Find information about a particular domain. Requires Docker to be installed."
originTypes = {'Domain'}
resultTypes = {'IP Address', 'Phrase', 'Autonomous System', 'Domain', 'IPv6 Address'}
parameters = {'VirusTotal API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://virustotal.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'AlienVault API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://otx.alienvault.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'BinaryEdge API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://app.binaryedge.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'C99 API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://c99.nl.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Censys API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://censys.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Chaos API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://chaos.projectdiscovery.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Cloudflare API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://cloudflare.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'DNSDB API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://dnsdb.info.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'GitHub API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://github.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Hunter API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://hunter.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'IPInfo Access Token': {'description': 'Enter your api key under your profile after'
' signing up on https://ipinfo.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'NetworksDB API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://networksdb.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'PassiveTotal API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://passivetotal.com .',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'ReconDev API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://recon.dev.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'SecurityTrails API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://securitytrails.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Shodan API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://shodan.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Spyse API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://spyse.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'ThreatBook API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://threatbook.cn.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Umbrella API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://umbrella.cisco.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'URLScan API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://urlscan.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'WhoisXMLAPI API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://whoisxmlapi.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'ZETAlytics API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://zetalytics.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'ZoomEye API Key': {'description': 'Please Enter the Username and password with a space '
'in between',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'FacebookCT API Key': {'description': 'Please Enter the api key and secret with a space '
'in between. Obtain them at https://developer.facebook.com',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Twitter API Key': {'description': 'Please Enter the api key and secret with a space '
'in between. Obtain them at https://developer.twitter.com',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'ReconDev.free API Key': {
'description':
'Please Enter the api key under your profile after signing up on https://recon.dev',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'ReconDev.paid API Key': {
'description':
'Please Enter the api key under your profile after signing up on https://recon.dev',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'}}
def resolution(self, entityJsonList, parameters):
from pathlib import Path
import json
from ipaddress import ip_address, IPv4Address, IPv6Address
import docker
import tempfile
from docker.errors import APIError
return_result = []
# Generate Config as a temporary file:
with tempfile.TemporaryDirectory() as tempDir:
tempPath = Path(tempDir).absolute()
config = tempfile.NamedTemporaryFile(mode='w+t', prefix='Amass',
suffix='Config',
dir=tempPath)
config.write("share = true\n")
config.write("[scope]\n")
config.write("port = 80\n")
config.write("port = 443\n")
config.write("[data_sources]\n")
config.write("minimum_ttl = 1440\n")
for parameter in self.parameters:
if parameters[f'{parameter}'] != 'None':
field1 = f"[data_sources.{parameter}]"
field2 = f"[data_sources.{parameter}.Credentials]"
if parameter == "ZoomEye":
username, password = parameters[parameter].split(' ', 1)
config.write(f"{field1}\n")
config.write(f"{field2}\n")
config.write(f"username = {username}\n")
config.write(f"password = {password}\n")
elif parameter == "FacebookCT":
field3, secret = parameters[parameter].split(' ', 1)
config.write(f"{field1}\n")
config.write(f"[data_sources.{parameter}.app1\n")
config.write(f"apikey = \"{field3}\"\n")
config.write(f"secret = {secret}\n")
elif parameter == "Twitter":
field3, secret = parameters[parameter].split(' ', 1)
config.write(f"{field1}\n")
config.write(f"[data_sources.{parameter}.account1\n")
config.write(f"apikey = \"{field3}\"\n")
config.write(f"secret = {secret}\n")
elif parameter == "ReconDev.paid":
field3 = parameters[f'{parameter}']
config.write(f"{field1}\n")
config.write(f"[data_sources.{parameter}.paid\n")
config.write(f"apikey = \"{field3}\"\n")
elif parameter == "ReconDev.free":
field3 = parameters[f'{parameter}']
config.write(f"{field1}\n")
config.write(f"[data_sources.{parameter}.free\n")
config.write(f"apikey = \"{field3}\"\n")
else:
field3 = parameters[f'{parameter}']
config.write(f"{field1}\n")
config.write(f"{field2}\n")
config.write(f"apikey = \"{field3}\"\n")
path_to_config = Path(config.name).name
config.seek(0)
for entity in entityJsonList:
primary_field = entity["Domain Name"].strip()
try:
client = docker.from_env()
container = client.containers.run("caffix/amass:latest",
f"enum -src -d {primary_field} "
f"-config /.config/amass/{path_to_config}",
volumes={
str(tempPath): {'bind': '/.config/amass',
'mode': 'rw'}},
remove=True)
jsonFile = tempPath / 'amass.json'
jsonContents = ""
if jsonFile.exists():
with open(jsonFile, 'r') as jsonFileHandler:
jsonContents = jsonFileHandler.read()
client.close()
except (APIError, docker.errors.ContainerError) as error:
return "Something happened to the docker container - Cannot continue: " + str(error)
uid = entity['uid']
for dictionary in jsonContents.splitlines():
index_of_child = len(return_result)
line_dictionary = json.loads(dictionary)
size = len(line_dictionary['addresses'])
return_result.append([{'Domain Name': str(line_dictionary['name']),
'Entity Type': 'Domain'},
{uid: {'Resolution': 'Amass Domain Scan', 'Notes': ''}}])
for ip in range(size):
if type(ip_address(line_dictionary['addresses'][ip]['ip'])) is IPv4Address:
return_result.append([{'IP Address': str(line_dictionary['addresses'][ip]['ip']),
'Entity Type': 'IP Address'},
{index_of_child: {'Resolution': 'Amass IP Address', 'Notes': ''}}])
elif type(ip_address(line_dictionary['addresses'][ip]['ip'])) is IPv6Address:
return_result.append([{'IPv6 Address': str(line_dictionary['addresses'][ip]['ip']),
'Entity Type': 'IPv6 Address'},
{index_of_child: {'Resolution': 'Amass IPv6 Address', 'Notes': ''}}])
return_result.append([{'AS Number': "AS" + str(line_dictionary['addresses'][ip]['asn']),
'ASN Cidr': str(line_dictionary['addresses'][ip]['cidr']),
'Entity Type': 'Autonomous System'},
{index_of_child: {'Resolution': 'Amass Autonomous System', 'Notes': ''}}])
return_result.append([{'Phrase': str(line_dictionary['addresses'][ip]['desc']),
'Entity Type': 'Phrase'},
{index_of_child: {'Resolution': 'Amass Domain Scan Description',
'Notes': ''}}])
config.close()
return return_result

View File

@@ -1,285 +0,0 @@
#!/usr/bin/env python3
# User needs to be in docker group or have root privileges
class Amass_Intel:
name = "Amass Intel Scan"
category = "Network Infrastructure"
description = "Find information about a particular domain. Requires Docker to be installed."
originTypes = {'Domain', 'IP Address', 'Autonomous System'}
resultTypes = {'Domain'}
parameters = {'VirusTotal API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://virustotal.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'AlienVault API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://otx.alienvault.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'BinaryEdge API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://app.binaryedge.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'C99 API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://c99.nl.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Censys API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://censys.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Chaos API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://chaos.projectdiscovery.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Cloudflare API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://cloudflare.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'DNSDB API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://dnsdb.info.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'GitHub API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://github.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Hunter API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://hunter.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'IPInfo Access Token': {'description': 'Enter your api key under your profile after'
' signing up on https://ipinfo.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'NetworksDB API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://networksdb.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'PassiveTotal API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://passivetotal.com .',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'ReconDev API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://recon.dev.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'SecurityTrails API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://securitytrails.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Shodan API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://shodan.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Spyse API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://spyse.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'ThreatBook API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://threatbook.cn.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Umbrella API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://umbrella.cisco.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'URLScan API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://urlscan.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'WhoisXMLAPI API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://whoisxmlapi.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'ZETAlytics API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://zetalytics.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'ZoomEye API Key': {'description': 'Please Enter the Username and password with a space '
'in between',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'FacebookCT API Key': {'description': 'Please Enter the api key and secret with a space '
'in between. Obtain them at https://developer.facebook.com',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Twitter API Key': {'description': 'Please Enter the api key and secret with a space '
'in between. Obtain them at https://developer.twitter.com',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'ReconDev.free API Key': {
'description':
'Please Enter the api key under your profile after signing up on https://recon.dev',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'ReconDev.paid API Key': {
'description':
'Please Enter the api key under your profile after signing up on https://recon.dev',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'}}
def resolution(self, entityJsonList, parameters):
from pathlib import Path
import json
from ipaddress import ip_address, IPv4Address, IPv6Address
import docker
import tempfile
from docker.errors import APIError
return_result = []
# Generate Config as a temporary file:
with tempfile.TemporaryDirectory() as tempDir:
tempPath = Path(tempDir).absolute()
config = tempfile.NamedTemporaryFile(mode='w+t', prefix='Amass',
suffix='Config',
dir=tempPath)
config.write("share = true\n")
config.write("[scope]\n")
config.write("port = 80\n")
config.write("port = 443\n")
config.write("[data_sources]\n")
config.write("minimum_ttl = 1440\n")
for parameter in parameters:
if parameters[f'{parameter}'] != 'None':
field1 = f"[data_sources.{parameter}]"
field2 = f"[data_sources.{parameter}.Credentials]"
if parameter == "ZoomEye":
username, password = parameters[parameter].split(' ', 1)
config.write(f"{field1}\n")
config.write(f"{field2}\n")
config.write(f"username = {username}\n")
config.write(f"password = {password}\n")
elif parameter == "FacebookCT":
field3, secret = parameters[parameter].split(' ', 1)
config.write(f"{field1}\n")
config.write(f"[data_sources.{parameter}.app1\n")
config.write(f"apikey = \"{field3}\"\n")
config.write(f"secret = {secret}\n")
elif parameter == "Twitter":
field3, secret = parameters[parameter].split(' ', 1)
config.write(f"{field1}\n")
config.write(f"[data_sources.{parameter}.account1\n")
config.write(f"apikey = \"{field3}\"\n")
config.write(f"secret = {secret}\n")
elif parameter == "ReconDev.paid":
field3 = parameters[f'{parameter}']
config.write(f"{field1}\n")
config.write(f"[data_sources.{parameter}.paid\n")
config.write(f"apikey = \"{field3}\"\n")
elif parameter == "ReconDev.free":
field3 = parameters[f'{parameter}']
config.write(f"{field1}\n")
config.write(f"[data_sources.{parameter}.free\n")
config.write(f"apikey = \"{field3}\"\n")
else:
field3 = parameters[f'{parameter}']
config.write(f"{field1}\n")
config.write(f"{field2}\n")
config.write(f"apikey = \"{field3}\"\n")
path_to_config = "/" + Path(config.name).name
config.seek(0)
for entity in entityJsonList:
primary_field = entity[list(entity)[1]].strip()
try:
client = docker.from_env()
if entity['Entity Type'] == "Domain":
container = client.containers.run("caffix/amass:latest",
f"intel -whois -d {primary_field} -config /.config/amass"
f"{path_to_config}",
volumes={
str(tempPath): {'bind': '/.config/amass',
'mode': 'rw'}},
remove=True)
elif entity['Entity Type'] == "IP Address":
try:
ip_address(primary_field)
except ValueError:
return "The Entity Provided isn't a valid IP Address"
container = client.containers.run("caffix/amass:latest",
f"intel -addr {primary_field} -config "
f"/.config/amass{path_to_config}",
volumes={
str(tempPath): {'bind': '/.config/amass',
'mode': 'rw'}},
remove=True)
elif entity['Entity Type'] == "Autonomous System":
if primary_field.startswith('AS'):
primary_field = primary_field[2:]
container = client.containers.run("caffix/amass:latest",
f"intel -asn {primary_field}"
f" -config /.config/amass{path_to_config}",
volumes={
str(tempPath): {'bind': '/.config/amass',
'mode': 'rw'}},
remove=True)
textFile = tempPath / 'amass.txt'
textContents = ""
if textFile.exists():
with open(textFile, 'r') as textFileHandler:
textContents = textFileHandler.read()
client.close()
except (APIError, docker.errors.ContainerError) as error:
return "Something happened to the docker container - Cannot continue: " + str(error)
uid = entity['uid']
for newDomain in textContents.splitlines():
return_result.append([{'Domain Name': newDomain.strip(),
'Entity Type': 'Domain'},
{uid: {'Resolution': 'Amass Intel Scan', 'Notes': ''}}])
config.close()
return return_result

View File

@@ -1 +0,0 @@
docker

View File

@@ -1,67 +0,0 @@
#!/usr/bin/env python3
class BigMatch:
name = "BigMatch Search"
category = "Secrets & Leaks"
description = "Find information about a file using https://bigmatch.rev.ng/static/index.html"
originTypes = {"Image", "Document", "Archive"}
resultTypes = {'Website'}
parameters = {}
def resolution(self, entityJsonList, parameters):
from pathlib import Path
from playwright.sync_api import sync_playwright, TimeoutError, Error
from bs4 import BeautifulSoup
return_result = []
url = "https://bigmatch.rev.ng/static/index.html"
failString = 'Too many strings in binary?'
successString = 'Results:'
with sync_playwright() as p:
browser = p.firefox.launch()
context = browser.new_context(
viewport={'width': 1920, 'height': 1080},
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0'
)
page = context.new_page()
for entity in entityJsonList:
uid = entity['uid']
file_path = Path(parameters['Project Files Directory']) / entity["File Path"]
file_path = file_path.absolute()
if not (file_path.exists() and file_path.is_file()):
continue
page.wait_for_timeout(3000)
for _ in range(3):
try:
page.goto(url, wait_until="networkidle", timeout=10000)
inputLocator = page.locator("input")
inputLocator.set_input_files([str(file_path)])
page.wait_for_timeout(3000)
soup = BeautifulSoup(page.content(), 'lxml')
soupText = soup.get_text()
while (failString not in soupText) and (successString not in soupText):
page.wait_for_timeout(1000)
soup = BeautifulSoup(page.content(), 'lxml')
soupText = soup.get_text()
if failString in soupText:
return []
for link in soup.find_all('a'):
potentialLink = link.get('href', None)
if potentialLink is not None:
if 'github' in potentialLink:
return_result.append([{'URL': potentialLink, 'Entity Type': 'Website'},
{uid: {'Resolution': 'BigMatch Github Link', 'Notes': ''}}])
break
except TimeoutError:
pass
except Error:
break
page.close()
browser.close()
return return_result

View File

@@ -1,2 +0,0 @@
bs4
playwright

View File

@@ -1,64 +0,0 @@
#!/usr/bin/env python3
class BinaryEdgeHost:
name = "BinaryEdge Host Query"
category = "Network Infrastructure"
description = "Get information about a host from BinaryEdge."
originTypes = {"IP Address", "IPv6 Address"}
resultTypes = {'Port'}
parameters = {'BinaryEdge API Key': {'description': "Enter your BinaryEdge API key. Sign up for one at "
"https://www.binaryedge.io/",
'type': 'String',
'value': '',
'global': True}}
def resolution(self, entityJsonList, parameters):
import requests
import json
baseURL = 'https://api.binaryedge.io/v2/query/ip/'
requestHeaders = {'X-Key': parameters['BinaryEdge API Key'].strip()}
returnResults = []
for entity in entityJsonList:
uid = entity['uid']
if entity['Entity Type'] == 'IP Address':
primaryField = entity['IP Address']
elif entity['Entity Type'] == 'IPv6 Address':
primaryField = entity['IPv6 Address']
else:
continue
infoRequest = requests.get(baseURL + primaryField, headers=requestHeaders)
statusCode = infoRequest.status_code
if statusCode == 401:
return "The BinaryEdge API key provided is not valid."
elif statusCode == 403:
return "The BinaryEdge API key provided does not have permission to access this resource."
elif statusCode != 200:
continue
requestContent = json.loads(infoRequest.content)
for event in requestContent['events']:
for result in event['results']:
originDetails = result['origin']
targetDetails = result['target']
resultDetails = result['result']
if 'state' not in resultDetails['data']:
# Discard return result if it doesn't actually give us useful info about the state of the port.
# This happens in cases where the API returns stuff like the ciphers used in an SSH service.
# There seems to always be a result with the simple port info, so we will use that one.
continue
returnResults.append([{'Port': targetDetails['ip'] + ':' + str(targetDetails['port']) + ':' +
targetDetails['protocol'],
'State': resultDetails['data']['state']['state'],
'Banner': resultDetails['data']['service'].get('banner', 'N/A'),
'Product': resultDetails['data']['service'].get('product', 'Unknown'),
'Entity Type': 'Port'},
{uid: {'Resolution': 'BinaryEdge Scan Timestamp: ' + str(originDetails['ts']),
'Notes': ''}}])
return returnResults

View File

@@ -1 +0,0 @@
requests

View File

@@ -1,34 +0,0 @@
<HIBP>
<Data_Breach>
<Attributes>
<Attribute default="Breach Name" check="String" primary="True">Breach Name</Attribute>
<Attribute default="Breach Title" check="String" primary="False">Breach Title</Attribute>
<Attribute default="Breach Domain" check="String" primary="False">Breach Domain</Attribute>
<Attribute default="0" check="Numbers" primary="False">Breach Pwn Count</Attribute>
<Attribute default="Breach Description" check="String" primary="False">Breach Description</Attribute>
<Attribute default="False" check="String" primary="False">Breach Is Sensitive</Attribute>
<Attribute default="False" check="String" primary="False">Breach Is Verified</Attribute>
<Attribute default="False" check="String" primary="False">Breach Is Fabricated</Attribute>
<Attribute default="False" check="String" primary="False">Breach Is Retired</Attribute>
<Attribute default="False" check="String" primary="False">Breach Is Spam List</Attribute>
<Attribute default="False" check="String" primary="False">Breach Is Malware</Attribute>
<Attribute default="Unknown" check="String" primary="False">Breach Added Date</Attribute>
<Attribute default="Unknown" check="String" primary="False">Breach Modified Date</Attribute>
</Attributes>
<Icon>
Default.svg
</Icon>
</Data_Breach>
<Paste_Data_Leak>
<Attributes>
<Attribute default="Paste Identifier" check="String" primary="True">Paste Identifier</Attribute>
<Attribute default="Paste Title" check="String" primary="False">Paste Title</Attribute>
<Attribute default="Paste Source" check="String" primary="False">Paste Source</Attribute>
<Attribute default="Paste ID" check="String" primary="False">Paste ID</Attribute>
<Attribute default="0" check="Numbers" primary="False">Paste Email Count</Attribute>
</Attributes>
<Icon>
Default.svg
</Icon>
</Paste_Data_Leak>
</HIBP>

View File

@@ -1,26 +0,0 @@
#!/usr/bin/env python3
class HIBPBreachToDomain:
name = "HIBP Breach To Domain"
category = "Leaked Data"
description = "Get the domain of the primary website that a data breach occurred on."
originTypes = {'Data Breach'}
resultTypes = {'Domain'}
parameters = {}
def resolution(self, entityJsonList, parameters):
returnResults = []
for entity in entityJsonList:
domainMaybe = entity.get('Breach Domain')
if isinstance(domainMaybe, str):
if domainMaybe.strip() != '':
returnResults.append([{'Domain Name': domainMaybe,
'Entity Type': 'Domain'},
{entity['uid']: {'Resolution': 'Data Breach to Domain',
'Notes': ''}}])
return returnResults

View File

@@ -1,85 +0,0 @@
#!/usr/bin/env python3
class HaveIBeenPwnedBreachDomains:
name = "HIBP Breach Domain Lookup"
category = "Leaked Data"
description = "Find breaches associated with a specified domain."
originTypes = {'Domain'}
resultTypes = {'Data Breach'}
parameters = {'HIBP API Key': {'description': 'Enter your "Have I Been Pwned" API key. '
'You can get a key here: https://haveibeenpwned.com/API/Key',
'type': 'String',
'value': '',
'global': True,
'default': 'None'}}
def resolution(self, entityJsonList, parameters):
import requests
import json
from time import sleep
from PySide6.QtCore import QByteArray, QBuffer, QIODevice, QSize
from PySide6.QtGui import QImage
baseURL = "https://haveibeenpwned.com/api/v3/breaches?domain="
requestHeaders = {'hibp-api-key': parameters['HIBP API Key'].strip(), 'user-agent': 'LinkScope Client'}
returnResults = []
count = 0
while count < len(entityJsonList):
entity = entityJsonList[count]
primaryField = entity['Domain Name']
breachInfoRequest = requests.get(baseURL + primaryField, headers=requestHeaders)
statusCode = breachInfoRequest.status_code
if statusCode == 200:
breachContent = json.loads(breachInfoRequest.content)
for breach in breachContent:
try:
breachLogoIconRequest = requests.get(breach['LogoPath'])
breachIconByteArray = QByteArray(breachLogoIconRequest.content)
breachIconImageOriginal = QImage().fromData(breachIconByteArray)
breachIconImageScaled = breachIconImageOriginal.scaled(QSize(40, 40))
# Rotate the breach domain logo upside down
breachIconImageRotated = breachIconImageScaled.mirrored()
breachIconByteArrayFin = QByteArray()
breachImageBuffer = QBuffer(breachIconByteArrayFin)
breachImageBuffer.open(QIODevice.WriteOnly)
breachIconImageRotated.save(breachImageBuffer, "PNG")
breachImageBuffer.close()
except Exception:
breachIconByteArrayFin = None
# If Breach Date is None, then default to entity creation date.
returnResults.append([{'Breach Name': breach['Name'],
'Breach Title': breach['Title'],
'Breach Domain': breach['Domain'],
'Breach Pwn Count': str(breach['PwnCount']),
'Breach Description': breach['Description'],
'Breach Is Sensitive': str(breach['IsSensitive']),
'Breach Is Verified': str(breach['IsVerified']),
'Breach Is Fabricated': str(breach['IsFabricated']),
'Breach Is Retired': str(breach['IsRetired']),
'Breach Is Spam List': str(breach['IsSpamList']),
'Breach Is Malware': str(breach['IsMalware']),
'Breach Added Date': breach['AddedDate'],
'Breach Modified Date': breach['ModifiedDate'],
'Entity Type': 'Data Breach',
'Icon': breachIconByteArrayFin, # If None -> Default breach icon.
'Date Created': breach['BreachDate']},
{entity['uid']: {'Resolution': 'Contained in Breach',
'Notes': ''}}])
elif statusCode == 401:
return "The HIBP API Key provided is invalid."
elif statusCode == 429:
sleep(2)
continue
elif statusCode == 503:
return "The HIBP Service is unavailable."
sleep(1.7)
count += 1
return returnResults

View File

@@ -1,88 +0,0 @@
#!/usr/bin/env python3
class HaveIBeenPwnedBreaches:
name = "HIBP Breach Lookup"
category = "Leaked Data"
description = "Find all breaches that an account has been involved in. Note that Date Created for breaches is an " \
"estimate."
originTypes = {'Email Address', 'Phone Number'}
resultTypes = {'Data Breach'}
parameters = {'HIBP API Key': {'description': 'Enter your "Have I Been Pwned" API key. '
'You can get a key here: https://haveibeenpwned.com/API/Key',
'type': 'String',
'value': '',
'global': True,
'default': 'None'}}
def resolution(self, entityJsonList, parameters):
import requests
import json
from time import sleep
from urllib.parse import quote_plus
from PySide6.QtCore import QByteArray, QBuffer, QIODevice, QSize
from PySide6.QtGui import QImage
baseURL = "https://haveibeenpwned.com/api/v3/breachedaccount/"
requestHeaders = {'hibp-api-key': parameters['HIBP API Key'].strip(), 'user-agent': 'LinkScope Client'}
returnResults = []
count = 0
while count < len(entityJsonList):
entity = entityJsonList[count]
primaryField = entity[list(entity)[1]]
breachInfoRequest = requests.get(baseURL + quote_plus(primaryField) + '?truncateResponse=false',
headers=requestHeaders)
statusCode = breachInfoRequest.status_code
if statusCode == 200:
breachContent = json.loads(breachInfoRequest.content)
for breach in breachContent:
try:
breachLogoIconRequest = requests.get(breach['LogoPath'])
breachIconByteArray = QByteArray(breachLogoIconRequest.content)
breachIconImageOriginal = QImage().fromData(breachIconByteArray)
breachIconImageScaled = breachIconImageOriginal.scaled(QSize(40, 40))
# Rotate the breach domain logo upside down
breachIconImageRotated = breachIconImageScaled.mirrored()
breachIconByteArrayFin = QByteArray()
breachImageBuffer = QBuffer(breachIconByteArrayFin)
breachImageBuffer.open(QIODevice.WriteOnly)
breachIconImageRotated.save(breachImageBuffer, "PNG")
breachImageBuffer.close()
except Exception:
breachIconByteArrayFin = None
# If Breach Date is None, then default to entity creation date.
returnResults.append([{'Breach Name': breach['Name'],
'Breach Title': breach['Title'],
'Breach Domain': breach['Domain'],
'Breach Pwn Count': str(breach['PwnCount']),
'Breach Description': breach['Description'],
'Breach Is Sensitive': str(breach['IsSensitive']),
'Breach Is Verified': str(breach['IsVerified']),
'Breach Is Fabricated': str(breach['IsFabricated']),
'Breach Is Retired': str(breach['IsRetired']),
'Breach Is Spam List': str(breach['IsSpamList']),
'Breach Is Malware': str(breach['IsMalware']),
'Breach Added Date': breach['AddedDate'],
'Breach Modified Date': breach['ModifiedDate'],
'Entity Type': 'Data Breach',
'Icon': breachIconByteArrayFin, # If None -> Default breach icon.
'Date Created': breach['BreachDate']},
{entity['uid']: {'Resolution': 'Contained in Breach',
'Notes': ''}}])
elif statusCode == 401:
return "The HIBP API Key provided is invalid."
elif statusCode == 429:
sleep(2)
continue
elif statusCode == 503:
return "The HIBP Service is unavailable."
sleep(1.7)
count += 1
return returnResults

View File

@@ -1,57 +0,0 @@
#!/usr/bin/env python3
class HaveIBeenPwnedPassword:
name = "HIBP Password Lookup"
category = "Leaked Data"
description = "Check whether the given password was found in breaches."
originTypes = {'Phrase'}
resultTypes = {'Phrase'}
parameters = {'HIBP API Key': {'description': 'Enter your "Have I Been Pwned" API key. '
'You can get a key here: https://haveibeenpwned.com/API/Key',
'type': 'String',
'value': '',
'global': True,
'default': 'None'}}
def resolution(self, entityJsonList, parameters):
import requests
from time import sleep
from hashlib import sha1
baseURL = "https://api.pwnedpasswords.com/range/"
requestHeaders = {'hibp-api-key': parameters['HIBP API Key'].strip(), 'user-agent': 'LinkScope Client'}
returnResults = []
count = 0
while count < len(entityJsonList):
entity = entityJsonList[count]
primaryField = sha1(entity[list(entity)[1]].encode('utf-8')).hexdigest().upper()
hashPrefix = primaryField[:5]
hashSuffix = primaryField[5:]
breachInfoRequest = requests.get(baseURL + hashPrefix, headers=requestHeaders)
statusCode = breachInfoRequest.status_code
if statusCode == 200:
pwnedPasswordContent = breachInfoRequest.content.decode('utf-8').split('\r\n')
for password in pwnedPasswordContent:
if hashSuffix in password:
returnResults.append([{'Phrase': f"Password Hash Found {password.split(':')[1]} times "
f"in breach data.",
'Entity Type': 'Phrase'},
{entity['uid']: {'Resolution': 'Pwned Password',
'Notes': ''}}])
break
elif statusCode == 401:
return "The HIBP API Key provided is invalid."
elif statusCode == 429:
sleep(2)
continue
elif statusCode == 503:
return "The HIBP Service is unavailable."
sleep(1.7)
count += 1
return returnResults

View File

@@ -1,58 +0,0 @@
#!/usr/bin/env python3
class HaveIBeenPwnedPasswordHash:
name = "HIBP Password Hash Lookup"
category = "Leaked Data"
description = "Check whether the given password hash was found in breaches."
originTypes = {'Hash', 'Phrase'}
resultTypes = {'Phrase'}
parameters = {'HIBP API Key': {'description': 'Enter your "Have I Been Pwned" API key. '
'You can get a key here: https://haveibeenpwned.com/API/Key',
'type': 'String',
'value': '',
'global': True,
'default': 'None'}}
def resolution(self, entityJsonList, parameters):
import requests
from time import sleep
baseURL = "https://api.pwnedpasswords.com/range/"
requestHeaders = {'hibp-api-key': parameters['HIBP API Key'].strip(), 'user-agent': 'LinkScope Client'}
returnResults = []
count = 0
while count < len(entityJsonList):
entity = entityJsonList[count]
primaryField = entity[list(entity)[1]].upper()
if len(primaryField) != 40:
continue
hashPrefix = primaryField[:5]
hashSuffix = primaryField[5:]
breachInfoRequest = requests.get(baseURL + hashPrefix, headers=requestHeaders)
statusCode = breachInfoRequest.status_code
if statusCode == 200:
pwnedPasswordContent = breachInfoRequest.content.decode('utf-8').split('\r\n')
for password in pwnedPasswordContent:
if hashSuffix in password:
returnResults.append([{'Phrase': f"Password Hash Found {password.split(':')[1]} times "
f"in breach data.",
'Entity Type': 'Phrase'},
{entity['uid']: {'Resolution': 'Pwned Password',
'Notes': ''}}])
break
elif statusCode == 401:
return "The HIBP API Key provided is invalid."
elif statusCode == 429:
sleep(2)
continue
elif statusCode == 503:
return "The HIBP Service is unavailable."
sleep(1.7)
count += 1
return returnResults

View File

@@ -1,61 +0,0 @@
#!/usr/bin/env python3
class HaveIBeenPwnedPastes:
name = "HIBP Paste Lookup"
category = "Leaked Data"
description = "Find all pastes that an account has been involved in."
originTypes = {'Email Address'}
resultTypes = {'Paste Data Leak'}
parameters = {'HIBP API Key': {'description': 'Enter your "Have I Been Pwned" API key. '
'You can get a key here: https://haveibeenpwned.com/API/Key',
'type': 'String',
'value': '',
'global': True,
'default': 'None'}}
def resolution(self, entityJsonList, parameters):
import requests
import json
from time import sleep
from urllib.parse import quote_plus
baseURL = "https://haveibeenpwned.com/api/v3/pasteaccount/"
requestHeaders = {'hibp-api-key': parameters['HIBP API Key'].strip(), 'user-agent': 'LinkScope Client'}
returnResults = []
count = 0
while count < len(entityJsonList):
entity = entityJsonList[count]
emailAddress = entity['Email Address']
pasteInfoRequest = requests.get(baseURL + quote_plus(emailAddress), headers=requestHeaders)
statusCode = pasteInfoRequest.status_code
if statusCode == 200:
pasteContent = json.loads(pasteInfoRequest.content)
for paste in pasteContent:
pasteID = paste['Id']
pasteSource = paste['Source']
# If Paste Date is None, then default to entity creation date.
returnResults.append([{'Paste Identifier': f'{pasteSource} | {pasteID}',
'Paste Title': paste['Title'],
'Paste Source': pasteSource,
'Paste ID': pasteID,
'Paste Email Count': str(paste['EmailCount']),
'Entity Type': 'Paste Data Leak',
'Date Created': paste['Date']},
{entity['uid']: {'Resolution': 'Contained in Paste',
'Notes': ''}}])
elif statusCode == 401:
return "The HIBP API Key provided is invalid."
elif statusCode == 429:
sleep(2)
continue
elif statusCode == 503:
return "The HIBP Service is unavailable."
sleep(1.7)
count += 1
return returnResults

View File

@@ -1 +0,0 @@
requests

View File

@@ -1,69 +0,0 @@
#!/usr/bin/env python3
class BlockChainAddressDestinations:
name = "Get Outbound Transactions for Bitcoin Address"
category = "CryptoCurrency"
description = "Returns the Bitcoin transactions where cryptocurrency was sent from this address."
originTypes = {'BTC Address'}
resultTypes = {'BTC Transaction'}
parameters = {}
def resolution(self, entityJsonList, parameters):
import requests
import time
from datetime import datetime
returnResults = []
apiEndpointAddress = 'https://blockchain.info/rawaddr/'
for entity in entityJsonList:
uid = entity['uid']
primaryField = entity['BTC Address']
try:
addressDetails = requests.get(apiEndpointAddress + primaryField).json()
if addressDetails.get('error') is not None:
continue
except requests.exceptions.ConnectionError:
return "Please check your internet connection"
blockTransactions = addressDetails.get('txs', [])
for transaction in blockTransactions:
inputValue = 0
isInAddr = False
for transactionInput in transaction.get('inputs', []):
inputValue += (int(transactionInput['prev_out']['value']) / 100000000)
if transactionInput['prev_out']['addr'] == primaryField:
isInAddr = True
if not isInAddr:
continue
outputValue = 0
for transactionOutput in transaction.get('out', []):
outputValue += (int(transactionOutput['value']) / 100000000)
timestamp = datetime.utcfromtimestamp(transaction.get('time')).isoformat()
returnResults.append(
[{'Transaction Hash': transaction['hash'],
'Input Value (BTC)': str(inputValue),
'Output Value (BTC)': str(outputValue),
'Fee': str(transaction['fee']),
'Number of Inputs': str(transaction['vin_sz']),
'Number of Outputs': str(transaction['vout_sz']),
'Transaction Index': str(transaction['tx_index']),
'Size': str(transaction['size']),
'Height': str(transaction['block_height']),
'Entity Type': 'BTC Transaction',
'Date Created': timestamp},
{uid: {'Resolution': 'BTC Transaction',
'Notes': ''}}])
time.sleep(5)
return returnResults

View File

@@ -1,73 +0,0 @@
#!/usr/bin/env python3
class BlockChainAddressSources:
name = "Get Inbound Transactions for Bitcoin Address"
category = "CryptoCurrency"
description = "Returns the Bitcoin transactions where cryptocurrency was sent to this address."
originTypes = {'BTC Address'}
resultTypes = {'BTC Transaction'}
parameters = {}
def resolution(self, entityJsonList, parameters):
import requests
import time
from datetime import datetime
returnResults = []
returnResultResolutions = {}
apiEndpointAddress = 'https://blockchain.info/rawaddr/'
for entity in entityJsonList:
primaryField = entity['BTC Address']
try:
addressDetails = requests.get(apiEndpointAddress + primaryField).json()
if addressDetails.get('error') is not None:
continue
except requests.exceptions.ConnectionError:
return "Please check your internet connection"
blockTransactions = addressDetails.get('txs', [])
for transaction in blockTransactions:
outputValue = 0
isOutAddr = False
for transactionOutput in transaction.get('out', []):
outputValue += (int(transactionOutput['value']) / 100000000)
if transactionOutput['addr'] == primaryField:
isOutAddr = True
if not isOutAddr:
continue
inputValue = 0
for transactionInput in transaction.get('inputs', []):
inputValue += (int(transactionInput['prev_out']['value']) / 100000000)
timestamp = datetime.utcfromtimestamp(transaction.get('time')).isoformat()
returnResultResolutions[len(returnResults)] = {'Resolution': 'BTC Transaction'}
returnResults.append(
[{'Transaction Hash': transaction['hash'],
'Input Value (BTC)': str(inputValue),
'Output Value (BTC)': str(outputValue),
'Fee': str(transaction['fee']),
'Number of Inputs': str(transaction['vin_sz']),
'Number of Outputs': str(transaction['vout_sz']),
'Transaction Index': str(transaction['tx_index']),
'Size': str(transaction['size']),
'Height': str(transaction['block_height']),
'Entity Type': 'BTC Transaction',
'Date Created': timestamp},
{'^^^': {'Resolution': 'NULL',
'Notes': ''}}])
time.sleep(5)
returnResults.append([{'BTC Address': primaryField,
'Entity Type': 'BTC Address'},
returnResultResolutions])
returnResultResolutions = {}
return returnResults

View File

@@ -1,51 +0,0 @@
#!/usr/bin/env python3
class BlockChainBlock:
name = "Get Bitcoin Block Details"
category = "CryptoCurrency"
description = "Returns the details of a particular bitcoin block."
originTypes = {'Hash', 'Phrase', 'BTC Block'}
resultTypes = {'BTC Block'}
parameters = {}
def resolution(self, entityJsonList, parameters):
import requests
import time
from datetime import datetime
returnResults = []
apiEndpoint = 'https://blockchain.info/rawblock/'
for entity in entityJsonList:
uid = entity['uid']
primaryField = entity[list(entity)[1]]
try:
details = requests.get(apiEndpoint + primaryField).json()
if details.get('error') is not None:
continue
except requests.exceptions.ConnectionError:
return "Please check your internet connection"
timestamp = datetime.utcfromtimestamp(details.get('time')).isoformat()
returnResults.append(
[{'Block Address': details['hash'],
'Previous Block': details['prev_block'],
'Merkle Root': details['mrkl_root'],
'Relayed By': details['relayed_by'],
'Nonce': str(details['nonce']),
'Bits': str(details['bits']),
'Size': str(details['size']),
'Block Index': str(details['block_index']),
'Height': str(details['height']),
'Main Chain': str(details['main_chain']),
'Entity Type': 'BTC Block',
'Date Created': timestamp},
{uid: {'Resolution': 'Bitcoin Block Details',
'Notes': ''}}])
time.sleep(5)
return returnResults

View File

@@ -1,55 +0,0 @@
#!/usr/bin/env python3
class BlockChainBlockHeight:
name = "Get Bitcoin Blocks At Height"
category = "CryptoCurrency"
description = "Returns the details of all bitcoin blocks at the specified height."
originTypes = {'Phrase'}
resultTypes = {'BTC Block'}
parameters = {}
def resolution(self, entityJsonList, parameters):
import requests
import time
from datetime import datetime
returnResults = []
apiEndpoint = 'https://blockchain.info/block-height/'
for entity in entityJsonList:
uid = entity['uid']
try:
primaryField = int(entity['Phrase'])
except ValueError:
continue
try:
heightDetails = requests.get(apiEndpoint + str(primaryField)).json()
if heightDetails.get('error') is not None:
continue
except requests.exceptions.ConnectionError:
return "Please check your internet connection"
for details in heightDetails['blocks']:
timestamp = datetime.utcfromtimestamp(details.get('time')).isoformat()
returnResults.append(
[{'Block Address': details['hash'],
'Previous Block': details['prev_block'],
'Merkle Root': details['mrkl_root'],
'Relayed By': details['relayed_by'],
'Nonce': str(details['nonce']),
'Bits': str(details['bits']),
'Size': str(details['size']),
'Block Index': str(details['block_index']),
'Height': str(details['height']),
'Main Chain': str(details['main_chain']),
'Entity Type': 'BTC Block',
'Date Created': timestamp},
{uid: {'Resolution': 'Bitcoin Block Address',
'Notes': ''}}])
time.sleep(5)
return returnResults

View File

@@ -1,57 +0,0 @@
#!/usr/bin/env python3
class BlockChainBlockNext:
name = "Get Next Bitcoin Block"
category = "CryptoCurrency"
description = "Returns the details of the next bitcoin block."
originTypes = {'BTC Block'}
resultTypes = {'BTC Block'}
parameters = {}
def resolution(self, entityJsonList, parameters):
import requests
import time
from datetime import datetime
returnResults = []
apiEndpoint = 'https://blockchain.info/rawblock/'
for entity in entityJsonList:
uid = entity['uid']
primaryField = entity['Block Address']
try:
currDetails = requests.get(apiEndpoint + primaryField).json()
if currDetails.get('error') is not None:
continue
nextBlockHashList = currDetails.get('next_block')
# Ignore nonexistent or indeterminate 'next' blocks.
if nextBlockHashList is None or len(nextBlockHashList) > 1:
continue
time.sleep(5)
details = requests.get(apiEndpoint + nextBlockHashList[0]).json()
except requests.exceptions.ConnectionError:
return "Please check your internet connection"
timestamp = datetime.utcfromtimestamp(details.get('time')).isoformat()
returnResults.append(
[{'Block Address': details['hash'],
'Previous Block': details['prev_block'],
'Merkle Root': details['mrkl_root'],
'Relayed By': details['relayed_by'],
'Nonce': str(details['nonce']),
'Bits': str(details['bits']),
'Size': str(details['size']),
'Block Index': str(details['block_index']),
'Height': str(details['height']),
'Main Chain': str(details['main_chain']),
'Entity Type': 'BTC Block',
'Date Created': timestamp},
{uid: {'Resolution': 'Next BTC Block',
'Notes': ''}}])
time.sleep(5)
return returnResults

View File

@@ -1,59 +0,0 @@
#!/usr/bin/env python3
class BlockChainBlockPrev:
name = "Get Previous Bitcoin Block"
category = "CryptoCurrency"
description = "Returns the details of the previous bitcoin block."
originTypes = {'BTC Block'}
resultTypes = {'BTC Block'}
parameters = {}
def resolution(self, entityJsonList, parameters):
import requests
import time
from datetime import datetime
returnResults = []
apiEndpoint = 'https://blockchain.info/rawblock/'
for entity in entityJsonList:
primaryField = entity['Block Address']
try:
currDetails = requests.get(apiEndpoint + primaryField).json()
if currDetails.get('error') is not None:
continue
prevBlockHash = currDetails.get('prev_block')
# Ignore first block.
if prevBlockHash == "0000000000000000000000000000000000000000000000000000000000000000":
continue
time.sleep(5)
details = requests.get(apiEndpoint + prevBlockHash).json()
except requests.exceptions.ConnectionError:
return "Please check your internet connection"
timestamp = datetime.utcfromtimestamp(details.get('time')).isoformat()
returnResults.append(
[{'Block Address': details['hash'],
'Previous Block': details['prev_block'],
'Merkle Root': details['mrkl_root'],
'Relayed By': details['relayed_by'],
'Nonce': str(details['nonce']),
'Bits': str(details['bits']),
'Size': str(details['size']),
'Block Index': str(details['block_index']),
'Height': str(details['height']),
'Main Chain': str(details['main_chain']),
'Entity Type': 'BTC Block',
'Date Created': timestamp},
{'^^^': {'Resolution': 'NULL'}}])
returnResults.append([{'Block Address': primaryField,
'Entity Type': 'BTC Block'},
{len(returnResults) - 1: {'Resolution': 'Next BTC Block'}}])
time.sleep(5)
return returnResults

View File

@@ -1,61 +0,0 @@
#!/usr/bin/env python3
class BlockChainBlockTransactions:
name = "Get Bitcoin Block Transactions"
category = "CryptoCurrency"
description = "Returns the transactions that happened in a particular bitcoin block."
originTypes = {'BTC Block'}
resultTypes = {'BTC Transaction'}
parameters = {}
def resolution(self, entityJsonList, parameters):
import requests
import time
from datetime import datetime
returnResults = []
apiEndpoint = 'https://blockchain.info/rawblock/'
for entity in entityJsonList:
uid = entity['uid']
primaryField = entity['Block Address']
try:
details = requests.get(apiEndpoint + primaryField).json()
if details.get('error') is not None:
continue
except requests.exceptions.ConnectionError:
return "Please check your internet connection"
blockTransactions = details.get('tx', [])
for transaction in blockTransactions:
inputValue = 0
for transactionInput in transaction.get('inputs', []):
inputValue += (int(transactionInput['prev_out']['value']) / 100000000)
outputValue = 0
for transactionOutput in transaction.get('out', []):
outputValue += (int(transactionOutput['value']) / 100000000)
timestamp = datetime.utcfromtimestamp(transaction.get('time')).isoformat()
returnResults.append(
[{'Transaction Hash': transaction['hash'],
'Input Value (BTC)': str(inputValue),
'Output Value (BTC)': str(outputValue),
'Fee': str(transaction['fee']),
'Number of Inputs': str(transaction['vin_sz']),
'Number of Outputs': str(transaction['vout_sz']),
'Transaction Index': str(transaction['tx_index']),
'Size': str(transaction['size']),
'Height': str(transaction['block_height']),
'Entity Type': 'BTC Transaction',
'Date Created': timestamp},
{uid: {'Resolution': 'Bitcoin Block Address',
'Notes': ''}}])
time.sleep(5)
return returnResults

View File

@@ -1,58 +0,0 @@
#!/usr/bin/env python3
class BlockChainTransaction:
name = "Get Bitcoin Transaction"
category = "CryptoCurrency"
description = "Returns the details of the specified Bitcoin transaction."
originTypes = {'BTC Transaction', 'Hash', 'Phrase'}
resultTypes = {'BTC Transaction'}
parameters = {}
def resolution(self, entityJsonList, parameters):
import requests
import time
from datetime import datetime
returnResults = []
apiEndpoint = 'https://blockchain.info/rawtx/'
for entity in entityJsonList:
uid = entity['uid']
primaryField = entity[list(entity)[1]]
try:
transaction = requests.get(apiEndpoint + primaryField).json()
if transaction.get('error') is not None:
continue
except requests.exceptions.ConnectionError:
return "Please check your internet connection"
timestamp = datetime.utcfromtimestamp(transaction.get('time')).isoformat()
inputValue = 0
for transactionInput in transaction.get('inputs', []):
inputValue += (int(transactionInput['prev_out']['value']) / 100000000)
outputValue = 0
for transactionOutput in transaction.get('out', []):
outputValue += (int(transactionOutput['value']) / 100000000)
returnResults.append(
[{'Transaction Hash': transaction['hash'],
'Input Value (BTC)': str(inputValue),
'Output Value (BTC)': str(outputValue),
'Fee': str(transaction['fee']),
'Number of Inputs': str(transaction['vin_sz']),
'Number of Outputs': str(transaction['vout_sz']),
'Transaction Index': str(transaction['tx_index']),
'Size': str(transaction['size']),
'Height': str(transaction['block_height']),
'Entity Type': 'BTC Transaction',
'Date Created': timestamp},
{uid: {'Resolution': 'Bitcoin Transaction Information',
'Notes': ''}}])
time.sleep(5)
return returnResults

View File

@@ -1,55 +0,0 @@
#!/usr/bin/env python3
class BlockChainTransactionDestinations:
name = "Get Bitcoin Transaction Destinations"
category = "CryptoCurrency"
description = "Returns the Bitcoin addresses that received cryptocurrency in the specified transaction."
originTypes = {'BTC Transaction'}
resultTypes = {'BTC Address'}
parameters = {}
def resolution(self, entityJsonList, parameters):
import requests
import time
returnResults = []
apiEndpointTransaction = 'https://blockchain.info/rawtx/'
apiEndpointAddress = 'https://blockchain.info/rawaddr/'
for entity in entityJsonList:
uid = entity['uid']
primaryField = entity['Transaction Hash']
try:
transaction = requests.get(apiEndpointTransaction + primaryField).json()
if transaction.get('error') is not None:
continue
except requests.exceptions.ConnectionError:
return "Please check your internet connection"
for transactionInput in transaction.get('out', []):
time.sleep(5)
inputAddress = transactionInput['addr']
try:
details = requests.get(apiEndpointAddress + inputAddress).json()
if details.get('error') is not None:
continue
except requests.exceptions.ConnectionError:
return "Please check your internet connection"
returnResults.append(
[{'BTC Address': details['address'],
'Total Transactions': str(details['n_tx']),
'Unredeemed Transactions': str(details['n_unredeemed']),
'Total BTC Received': str(details['total_received'] / 100000000),
'Total BTC Sent': str(details['total_sent'] / 100000000),
'Current Balance': str(details['final_balance'] / 100000000),
'Entity Type': 'BTC Address'},
{uid: {'Resolution': 'Bitcoin Transaction',
'Notes': ''}}])
time.sleep(5)
return returnResults

View File

@@ -1,62 +0,0 @@
#!/usr/bin/env python3
class BlockChainTransactionSources:
name = "Get Bitcoin Transaction Sources"
category = "CryptoCurrency"
description = "Returns the Bitcoin addresses that sent cryptocurrency in the specified transaction."
originTypes = {'BTC Transaction'}
resultTypes = {'BTC Address'}
parameters = {}
def resolution(self, entityJsonList, parameters):
import requests
import time
returnResults = []
returnResultResolutions = {}
apiEndpointTransaction = 'https://blockchain.info/rawtx/'
apiEndpointAddress = 'https://blockchain.info/rawaddr/'
for entity in entityJsonList:
primaryField = entity['Transaction Hash']
try:
transaction = requests.get(apiEndpointTransaction + primaryField).json()
if transaction.get('error') is not None:
continue
except requests.exceptions.ConnectionError:
return "Please check your internet connection"
for transactionInput in transaction.get('inputs', []):
time.sleep(5)
inputAddress = transactionInput['prev_out']['addr']
try:
details = requests.get(apiEndpointAddress + inputAddress).json()
if details.get('error') is not None:
continue
except requests.exceptions.ConnectionError:
return "Please check your internet connection"
returnResultResolutions[len(returnResults)] = {'Resolution': 'BTC Transaction'}
returnResults.append(
[{'BTC Address': details['address'],
'Total Transactions': str(details['n_tx']),
'Unredeemed Transactions': str(details['n_unredeemed']),
'Total BTC Received': str(details['total_received'] / 100000000),
'Total BTC Sent': str(details['total_sent'] / 100000000),
'Current Balance': str(details['final_balance'] / 100000000),
'Entity Type': 'BTC Address'},
{'^^^': {'Resolution': 'NULL',
'Notes': ''}}])
time.sleep(5)
# Re-add the source entity so that we can point to it.
# Only include the primary field, in case the rest of the fields were updated in the meantime.
returnResults.append([{'Transaction Hash': primaryField,
'Entity Type': 'BTC Transaction'},
returnResultResolutions])
returnResultResolutions = {}
return returnResults

View File

@@ -1,26 +0,0 @@
<CryptoCurrency>
<BTC_Block>
<Attributes>
<Attribute default="0000000000000000000000000000000000000000000000000000000000000000" check="String" primary="True">Block Address</Attribute>
</Attributes>
<Icon>
Bitcoin.svg
</Icon>
</BTC_Block>
<BTC_Transaction>
<Attributes>
<Attribute default="0000000000000000000000000000000000000000000000000000000000000000" check="String" primary="True">Transaction Hash</Attribute>
</Attributes>
<Icon>
Transaction.svg
</Icon>
</BTC_Transaction>
<BTC_Address>
<Attributes>
<Attribute default="1AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" check="String" primary="True">BTC Address</Attribute>
</Attributes>
<Icon>
CryptoWallet.svg
</Icon>
</BTC_Address>
</CryptoCurrency>

View File

@@ -1,40 +0,0 @@
#!/usr/bin/env python3
class EtherScanGetBalance:
name = "EtherScan.io Get Balance"
category = "CryptoCurrency"
description = "EtherScan get the balance of the selected account"
originTypes = {"Crypto Wallet"}
resultTypes = {'Crypto Wallet'}
parameters = {'EtherScan API Key': {'description': "Enter the api key under your profile after signing up at "
"https://etherscan.io.",
'type': 'String',
'value': '',
'global': True}}
def resolution(self, entityJsonList, parameters):
import requests
import time
return_result = []
api_key = parameters['EtherScan API Key']
for entity in entityJsonList:
uid = entity['uid']
primary_field = entity['Wallet Address']
crafted_url = f"https://api.etherscan.io/api?module=account&action=balance" \
f"&address={primary_field}&tag=latest&apikey={api_key}"
try:
response = requests.get(crafted_url)
except requests.exceptions.ConnectionError:
return "Please check your internet connection"
response = response.json()
return_result.append([{'Amount': response['result'],
'Currency': 'Ethereum',
'Entity Type': 'Currency'},
{uid: {'Resolution': 'EtherScan.io Account Balance', 'Notes': ''}}])
time.sleep(0.2)
return return_result

View File

@@ -1,39 +0,0 @@
#!/usr/bin/env python3
class EtherScanGetBlocksMined:
name = "EtherScan.io Get Blocks Mined"
category = "CryptoCurrency"
description = "EtherScan Blocks Mined from the selected account"
originTypes = {"Crypto Wallet"}
resultTypes = {'Crypto Wallet'}
parameters = {'EtherScan API Key': {'description': "Enter the api key under your profile after signing up at "
"https://etherscan.io.",
'type': 'String',
'value': '',
'global': True}}
def resolution(self, entityJsonList, parameters):
import requests
import time
return_result = []
api_key = parameters['EtherScan API Key']
for entity in entityJsonList:
uid = entity['uid']
primary_field = entity['Wallet Address']
crafted_url = f"https://api.etherscan.io/api?module=account&action=getminedblocks" \
f"&address={primary_field}&tag=latest&apikey={api_key}"
try:
response = requests.get(crafted_url)
except requests.exceptions.ConnectionError:
return "Please check your internet connection"
response = response.json()
return_result.append([{'Phrase': response['result'],
'Entity Type': 'Phrase'},
{uid: {'Resolution': 'EtherScan.io Blocks Mined', 'Notes': ''}}])
time.sleep(0.2)
return return_result

View File

@@ -1,25 +0,0 @@
#!/usr/bin/env python3
class ToCryptoWallet:
name = "BTC Address To Crypto Wallet"
category = "CryptoCurrency"
description = "Convert BTC Address entities to Crypto Wallet entities."
originTypes = {'BTC Address'}
resultTypes = {'Crypto Wallet'}
parameters = {}
def resolution(self, entityJsonList, parameters):
returnResults = []
for entity in entityJsonList:
primaryField = entity['BTC Address']
returnResults.append([{'Wallet Address': primaryField,
'Currency Name': 'Bitcoin',
'Entity Type': 'Crypto Wallet'},
{entity['uid']: {'Resolution': 'To Crypto Wallet',
'Notes': ''}}])
return returnResults

View File

@@ -1,2 +0,0 @@
requests
datetime

View File

@@ -1,107 +0,0 @@
#!/usr/bin/env python3
class CompanyInfo:
# A string that is treated as the name of this resolution.
name = "Get Company Info"
category = "EDGAR Info"
# A string that describes this resolution.
description = "Returns Nodes containing Company Information"
originTypes = {'Edgar ID'}
resultTypes = {'Phrase, SIC, EIN, Address'}
parameters = {}
def resolution(self, entityJsonList, parameters):
import requests
import time
headers = {
'User-Agent': 'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
}
returnResults = []
for entity in entityJsonList:
uid = entity['uid']
cik = entity['CIK']
if cik.lower().startswith('cik'):
cik = cik.split('cik')[1]
if len(cik) != 10:
cik = cik.zfill(1)
search_url = f'https://data.sec.gov/submissions/CIK{cik}.json'
time.sleep(1)
r = requests.get(search_url, headers=headers)
time.sleep(1)
if r.status_code != 200:
return []
data = r.json()
exchanges = data['exchanges']
for exchange in exchanges:
returnResults.append([{'Exchange Name': exchange,
'Entity Type': 'Exchange'},
{uid: {'Resolution': 'Exchange',
'Notes': ''}}])
tickers = data['tickers']
for ticker in tickers:
returnResults.append([{'Ticker ID': ticker,
'Entity Type': 'Ticker'},
{uid: {'Resolution': 'Ticker',
'Notes': ''}}])
if data['insiderTransactionForOwnerExists'] == 1:
returnResults.append([{'Phrase': 'Insider Transaction For Owner Exists',
'Entity Type': 'Phrase'},
{uid: {'Resolution': '',
'Notes': ''}}])
else:
returnResults.append([{'Phrase': 'Insider Transaction For Owner Does Not Exists',
'Entity Type': 'Phrase'},
{uid: {'Resolution': '',
'Notes': ''}}])
if data['insiderTransactionForIssuerExists'] == 1:
returnResults.append([{'Phrase': 'Insider Transaction For Issuer Exists',
'Entity Type': 'Phrase'},
{uid: {'Resolution': '',
'Notes': ''}}])
else:
returnResults.append([{'Phrase': 'Insider Transaction For Issuer Does Not Exists',
'Entity Type': 'Phrase'},
{uid: {'Resolution': '',
'Notes': ''}}])
if data['sic'] is not None:
returnResults.append([{'SIC': str(data['sic']),
'Description': data['sicDescription'],
'Entity Type': 'SIC'},
{uid: {'Resolution': '',
'Notes': ''}}])
if data['ein'] is not None:
returnResults.append([{'EIN': str(data['ein']),
'Entity Type': 'EIN'},
{uid: {'Resolution': '',
'Notes': ''}}])
if data['addresses'] is not None:
returnResults.append([{'Street Address': data['addresses']['mailing']['street1'],
'Postal Code': data['addresses']['mailing']['zipCode'],
'Country': data['addresses']['mailing']['stateOrCountry'],
'Locality': data['addresses']['mailing']['city'],
'Entity Type': 'Address'},
{uid: {'Resolution': '',
'Notes': ''}}])
if data['addresses']['mailing']['street1'] != data['addresses']['business']['street1']:
returnResults.append([{'Street Address': data['addresses']['business']['street1'],
'Postal Code': data['addresses']['business']['zipCode'],
'Country': data['addresses']['business']['stateOrCountry'],
'Locality': data['addresses']['business']['city'],
'Entity Type': 'Address'},
{uid: {'Resolution': '',
'Notes': ''}}])
return returnResults

View File

@@ -1,77 +0,0 @@
#!/usr/bin/env python3
class CompanyToCIK:
# A string that is treated as the name of this resolution.
name = "Get CIK ID From Company"
category = "EDGAR Info"
# A string that describes this resolution.
description = "Returns Nodes of contact info for websites"
originTypes = {'Phrase', 'Company'}
resultTypes = {'Phrase'}
parameters = {}
def resolution(self, entityJsonList, parameters):
from bs4 import BeautifulSoup
from playwright.sync_api import sync_playwright, TimeoutError, Error
returnResults = []
with sync_playwright() as p:
browser = p.firefox.launch()
context = browser.new_context(
viewport={'width': 1920, 'height': 1080}
)
page = context.new_page()
for entity in entityJsonList:
page.wait_for_timeout(1000)
uid = entity['uid']
search_term = entity[list(entity)[1]]
pageResolved = False
for _ in range(3):
try:
page.goto(f'https://www.sec.gov/cgi-bin/browse-edgar?company={search_term}',
wait_until="networkidle", timeout=10000)
pageResolved = True
break
except TimeoutError:
pass
except Error:
break
if not pageResolved:
continue
soup = BeautifulSoup(page.content(), 'lxml')
count = 0
temp = None
for td_element in soup.find_all('td'):
if td_element.text:
text = td_element.text
splitText = text.split('SIC')[0]
if count == 0:
temp = [{'CIK': splitText,
'Entity Type': 'Edgar ID'},
{len(returnResults): {'Resolution': 'CIK Edgar ID',
'Notes': ''}}]
elif count == 1:
returnResults.append([{'Company Name': splitText,
'Entity Type': 'Company'},
{uid: {'Resolution': 'Edgar Company',
'Notes': ''}}])
returnResults.append(temp)
elif count == 2:
returnResults.append([{'Phrase': "State: " + splitText,
'Entity Type': 'Phrase'},
{len(returnResults) - 1: {'Resolution': 'Edgar Company State',
'Notes': ''}}])
count = (count + 1) % 3
page.close()
browser.close()
return returnResults

Some files were not shown because too many files have changed in this diff Show More