132 Commits
v1.3.7 ... 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
AccentuSoft
3bcc6c112b Update .md files to indicate transition to support Windows 11. 2022-10-06 15:38:45 +03:00
AccentuSoft
b879a245b7 Minor bug fixes & Improvements 2022-10-06 15:38:45 +03:00
AccentuSoft
804b976bb4 Update LinkScope version on window. 2022-10-02 23:04:40 +03:00
AccentuSoft
92fb7c729e Update Installer - make it work better.
BREAKING CHANGE - Naming for Windows binary changed.
2022-10-02 23:04:40 +03:00
AccentuSoft
5f7ceeb2f8 Update WhatsMyName data 2022-10-02 23:04:40 +03:00
AccentuSoft
f41de445bc Major refactoring & Minor bug fixes 2022-10-02 23:04:40 +03:00
AccentuSoft
c2b3f6e710 Finish adding Macros functionality. 2022-09-26 22:07:39 +03:00
AccentuSoft
d06acd5aba Refactor resolutions to make everything work a bit more smoothly. 2022-09-26 22:01:24 +03:00
AccentuSoft
f1dfdb75ec Remove unnecessary Macro setting. 2022-09-24 02:31:44 +03:00
AccentuSoft
f40a13e1a6 Refactor saving to reduce code duplication. 2022-09-24 02:30:48 +03:00
AccentuSoft
025b27d191 Set notes field to empty string instead of "None" when creating links with notes field value of None.
Small grammar improvements for comments.
2022-09-21 02:24:49 +03:00
AccentuSoft
cbe98f652d Fix error when renaming project. 2022-09-20 18:14:50 +03:00
AccentuSoft
4c1cf569e1 Add Macros menu option in MenuBar.py 2022-09-17 22:07:09 +03:00
AccentuSoft
0025a18e17 Re-organize menus to make them more intuitive. 2022-09-17 20:24:35 +03:00
AccentuSoft
e5fa7b35d8 Update WhatsMyName resolution. 2022-09-17 15:20:27 +03:00
AccentuSoft
09db975d88 Increase margins on Canvases for better visibility. 2022-09-17 15:20:27 +03:00
AccentuSoft
9cb80f296a Don't allow people to withdraw consent in the middle of the installation. 2022-09-17 15:20:27 +03:00
AccentuSoft
b9a1149698 Formatting changes to Installer.py 2022-09-17 15:20:27 +03:00
AccentuSoft
dd28481792 Add InternetDB module. 2022-09-17 15:20:27 +03:00
AccentuSoft
1833304c4a Fix bugs with GetInternalURLs.py resolution. 2022-09-17 15:20:27 +03:00
AccentuSoft
6bb39357d9 Update Installer.py - improvements, comments, & switch to a compiled version. 2022-09-14 15:31:05 +03:00
AccentuSoft
7e9ab2ddd7 Update README instructions for running from source for Windows. 2022-09-14 12:23:06 +03:00
AccentuSoft
dab232c750 Explicitly specify to not put any of the magic binaries in git. 2022-09-14 12:23:06 +03:00
AccentuSoft
3cda14c487 Update build requirements, and add script to patch magic library for Windows. 2022-09-14 12:23:06 +03:00
AccentuSoft
3ffe8efe8d Update EDGAR resolutions. 2022-09-14 12:23:06 +03:00
AccentuSoft
e00d052e32 Make Holehe work with compiled functions. The optimization also works even if the code is not compiled. 2022-09-14 12:23:06 +03:00
AccentuSoft
9f501119e4 Update Git-All-Secrets to explicitly state that the user needs to have Docker installed. 2022-09-14 12:23:06 +03:00
AccentuSoft
eb0c32cc06 Update build scripts to use nuikta instead of PyInstaller. 2022-09-14 12:23:06 +03:00
AccentuSoft
9863f597b7 Update snscrape resolutions. 2022-08-31 16:42:45 +03:00
AccentuSoft
cee6bb84a2 Update Amass to explicitly state that the user needs to have Docker installed. 2022-08-31 16:42:10 +03:00
AccentuSoft
1cdad0decf Update some resolutions. 2022-08-30 22:52:20 +03:00
AccentuSoft
75bb7fea47 Update import statement in ResolutionManager.py 2022-08-30 19:05:16 +03:00
AccentuSoft
9c3edee49f Fix bug with LinkedIn account detection 2022-08-30 19:03:50 +03:00
AccentuSoft
17b5a08c31 Update requirements.txt for modules & software. 2022-08-30 19:03:22 +03:00
234 changed files with 7062 additions and 24028 deletions

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
/magic/COPYING.file
/magic/COPYING.libgnurx
/magic/file.exe
/magic/libgnurx-0.dll
/magic/libmagic-1.dll
/magic/magic.mgc

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
@@ -29,42 +27,38 @@ class EntitiesDB:
"""
Load DiGraph from LinkScope Database file - msgpack dumped object.
"""
self.dbLock.acquire()
if self.database is not None:
self.save()
databaseFile = Path(self.mainWindow.SETTINGS.value("Project/FilesDir")).joinpath("LocalEntitiesDB.lsdb")
self.messageHandler.debug('Opening Database at: ' + str(databaseFile))
try:
dbFile = open(databaseFile, "rb")
self.database = self.mainWindow.RESOURCEHANDLER.reconstructGraphFullFromFile(load(dbFile))
dbFile.close()
self.messageHandler.info('Loaded Local Entities Database.')
except FileNotFoundError:
self.messageHandler.info('Creating new Local Entities Database.')
self.database = nx.DiGraph()
except Exception as exc:
self.messageHandler.error('Cannot parse Database: ' + str(exc) + "\nCreating new Local Entities Database.",
popUp=True)
self.database = nx.DiGraph()
finally:
self.dbLock.release()
with self.dbLock:
if self.database is not None:
self.save()
databaseFile = Path(self.mainWindow.SETTINGS.value("Project/FilesDir")).joinpath("LocalEntitiesDB.lsdb")
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.mainWindow.MESSAGEHANDLER.info('Loaded Local Entities Database.')
except FileNotFoundError:
self.mainWindow.MESSAGEHANDLER.info('Creating new Local Entities Database.')
self.database = nx.DiGraph()
except Exception as exc:
self.mainWindow.MESSAGEHANDLER.error(
f'Cannot parse Database: {exc}\nCreating new Local Entities Database.',
popUp=True)
self.database = nx.DiGraph()
def resetTimeline(self) -> None:
"""
Reset the timeline on dockBarThree to reflect the current state of the database.
"""
self.dbLock.acquire()
if self.database is not None:
self.mainWindow.resetTimeline(self.database)
self.dbLock.release()
with self.dbLock:
if self.database is not None:
self.mainWindow.resetTimeline(self.database)
def updateTimeline(self, node, added: bool, updateGraph: bool = True) -> None:
"""
Update the timeline on dockBarThree to reflect the newest change of the database.
"""
self.dbLock.acquire()
self.mainWindow.updateTimeline(node, added, updateGraph)
self.dbLock.release()
with self.dbLock:
self.mainWindow.updateTimeline(node, added, updateGraph)
def save(self) -> None:
"""
@@ -75,41 +69,38 @@ class EntitiesDB:
databaseFile = Path(self.mainWindow.SETTINGS.value("Project/FilesDir")).joinpath("LocalEntitiesDB.lsdb")
if databaseFile is None:
raise ValueError('Database File is None, cannot save database.')
self.dbLock.acquire()
tmpSavePath = databaseFile.with_suffix(databaseFile.suffix + '.tmp')
dbFile = open(tmpSavePath, "wb")
dump(self.mainWindow.RESOURCEHANDLER.deconstructGraphForFileDump(self.database), dbFile)
dbFile.close()
move(tmpSavePath, databaseFile)
self.messageHandler.info('Database Saved.')
self.dbLock.release()
with self.dbLock:
tmpSavePath = databaseFile.with_suffix(f'{databaseFile.suffix}.tmp')
with open(tmpSavePath, "wb") as dbFile:
dump(self.mainWindow.RESOURCEHANDLER.deconstructGraphForFileDump(self.database), dbFile)
move(tmpSavePath, databaseFile)
self.mainWindow.MESSAGEHANDLER.info('Database Saved.')
def addEntity(self, entJson: dict, fromServer: bool = False, updateTimeline: bool = True) -> Union[dict, None]:
"""
Adds the entity represented by the json dictionary to the database.
"""
self.dbLock.acquire()
returnValue = None
with self.dbLock:
returnValue = None
# Check if we're overwriting an existing entity
exists = None
if entJson.get('uid') is not None:
exists = self.getEntityNoLock(entJson.get('uid'))
# Check if we're overwriting an existing entity
exists = None
if entJson.get('uid') is not None:
exists = self.getEntityNoLock(entJson.get('uid'))
entity = self.resourceHandler.getEntityJson(
entJson.get('Entity Type'),
entJson)
entity = self.mainWindow.RESOURCEHANDLER.getEntityJson(
entJson.get('Entity Type'),
entJson)
if entity is None:
return returnValue
# Use uid as key. Code is holdover from the time when primary field == uid.
self.database.add_node(entity['uid'], **entity)
returnValue = entity
if exists:
# Update canvases if the node already exists.
self.mainWindow.updateEntityNodeLabelsOnCanvases(entity['uid'], entity[list(entity)[1]])
if entity is None:
self.dbLock.release()
return returnValue
# Use uid as key. Code is holdover from the time when primary field == uid.
self.database.add_node(entity['uid'], **entity)
returnValue = entity
if exists:
# Update canvases if the node already exists.
self.mainWindow.updateEntityNodeLabelsOnCanvases(entity['uid'], entity[list(entity)[1]])
self.dbLock.release()
if not fromServer:
self.mainWindow.sendLocalDatabaseUpdateToServer(entity, 1)
self.mainWindow.populateEntitiesWidget(returnValue, add=True)
@@ -123,32 +114,31 @@ class EntitiesDB:
return returnValue
def addEntities(self, entitiesJsonList: Union[list, set, tuple], fromServer: bool = False) -> list:
self.dbLock.acquire()
returnValue = []
with self.dbLock:
returnValue = []
for entJson in entitiesJsonList:
# Check if we're overwriting an existing entity
exists = None
if entJson.get('uid') is not None:
exists = self.getEntityNoLock(entJson.get('uid'))
for entJson in entitiesJsonList:
# Check if we're overwriting an existing entity
exists = None
if entJson.get('uid') is not None:
exists = self.getEntityNoLock(entJson.get('uid'))
entity = self.resourceHandler.getEntityJson(
entJson.get('Entity Type'),
entJson)
entity = self.mainWindow.RESOURCEHANDLER.getEntityJson(
entJson.get('Entity Type'),
entJson)
if entity is None:
continue
# Use uid as key. Code is holdover from the time when primary field == uid.
self.database.add_node(entity['uid'], **entity)
returnValue.append(entity)
if exists:
# Update canvases if the node already exists.
self.mainWindow.updateEntityNodeLabelsOnCanvases(entity['uid'], entity[list(entity)[1]])
if not fromServer:
self.mainWindow.sendLocalDatabaseUpdateToServer(entity, 1)
self.mainWindow.populateEntitiesWidget(entity, add=True)
if entity is None:
continue
# Use uid as key. Code is holdover from the time when primary field == uid.
self.database.add_node(entity['uid'], **entity)
returnValue.append(entity)
if exists:
# Update canvases if the node already exists.
self.mainWindow.updateEntityNodeLabelsOnCanvases(entity['uid'], entity[list(entity)[1]])
if not fromServer:
self.mainWindow.sendLocalDatabaseUpdateToServer(entity, 1)
self.mainWindow.populateEntitiesWidget(entity, add=True)
self.dbLock.release()
self.resetTimeline()
return returnValue
@@ -162,42 +152,39 @@ class EntitiesDB:
:param fromServer:
:return:
"""
self.dbLock.acquire()
exists = self.isLinkNoLock(linkJson['uid'])
link = self.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.dbLock.release()
return None
else:
linkUID = link['uid']
if exists:
newRes = link.get('Resolution')
newNotes = link.get('Notes')
if newRes and newRes != exists['Resolution']:
if overwrite:
link['Resolution'] = newRes
else:
link['Resolution'] = exists['Resolution'] + ' | ' + newRes
if newNotes and newNotes != exists['Notes'] and newNotes != 'None':
if overwrite:
link['Notes'] = str(newNotes)
else:
link['Notes'] = exists['Notes'] + '\n\n' + str(newNotes)
exists.update(link)
link.update(exists)
# Update canvases if the link already exists.
# We can do this before updating the database here because the GUI will be updated only after this
# function returns. If we ever execute this function outside the main event loop, we will need
# to alter the execution flow.
self.mainWindow.updateLinkLabelsOnCanvases(linkUID[0] + linkUID[1], link['Resolution'])
self.database.add_edge(linkUID[0], linkUID[1], **link)
with self.dbLock:
exists = self.isLinkNoLock(linkJson['uid'])
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.mainWindow.MESSAGEHANDLER.error("Attempted to add Link with no uid to database.", popUp=True)
return None
else:
linkUID = link['uid']
if exists:
newRes = link.get('Resolution')
newNotes = link.get('Notes')
if newRes and newRes != exists['Resolution']:
if overwrite:
link['Resolution'] = newRes
else:
link['Resolution'] = f"{exists['Resolution']} | {newRes}"
if newNotes and newNotes != exists['Notes'] and newNotes != 'None':
if overwrite:
link['Notes'] = str(newNotes)
else:
link['Notes'] = f"{exists['Notes']}\n\n{str(newNotes)}"
exists.update(link)
link.update(exists)
# Update canvases if the link already exists.
# We can do this before updating the database here because the GUI will be updated only after this
# function returns. If we ever execute this function outside the main event loop, we will need
# to alter the execution flow.
self.mainWindow.updateLinkLabelsOnCanvases(f"{linkUID[0]}{linkUID[1]}", link['Resolution'])
self.database.add_edge(linkUID[0], linkUID[1], **link)
self.dbLock.release()
if not fromServer:
if overwrite:
self.mainWindow.sendLocalDatabaseUpdateToServer(link, 3)
@@ -209,51 +196,41 @@ class EntitiesDB:
"""
Returns the attributes of the given entity uid as a dict.
"""
self.dbLock.acquire()
returnValue = None
try:
returnValue = self.database.nodes[uid]
except KeyError:
self.messageHandler.warning(
"Tried to get entity with nonexistent UID: " + uid)
finally:
self.dbLock.release()
return returnValue
with self.dbLock:
returnValue = None
try:
returnValue = self.database.nodes[uid]
except KeyError:
self.mainWindow.MESSAGEHANDLER.warning(f"Tried to get entity with nonexistent UID: {uid}")
finally:
return returnValue
def getAllEntities(self) -> Union[None, list]:
"""
Returns a list containing the Json representation of every entity in the database.
"""
self.dbLock.acquire()
returnValue = None
try:
returnValue = []
for node in self.database.nodes():
returnValue += [self.database.nodes[node]]
except KeyError:
self.messageHandler.error(
"Tried to get entity with nonexistent UID.")
finally:
self.dbLock.release()
return returnValue
with self.dbLock:
returnValue = None
try:
returnValue = [self.database.nodes[node] for node in self.database.nodes()]
except KeyError as keyError:
self.mainWindow.MESSAGEHANDLER.error(f"Tried to get entity with nonexistent UID. Error: {keyError}")
finally:
return returnValue
def getAllLinks(self) -> Union[None, list]:
"""
Returns a list containing the Json representation of every link in the database.
:return:
"""
self.dbLock.acquire()
returnValue = None
try:
returnValue = []
for edge in self.database.edges():
returnValue += [self.database.edges[edge]]
except KeyError:
self.messageHandler.error(
"Tried to get link with nonexistent UID.")
finally:
self.dbLock.release()
return returnValue
with self.dbLock:
returnValue = None
try:
returnValue = [self.database.edges[edge] for edge in self.database.edges()]
except KeyError:
self.mainWindow.MESSAGEHANDLER.error("Tried to get link with nonexistent UID.")
finally:
return returnValue
def getEntityNoLock(self, uid: str) -> Union[None, dict]:
"""
@@ -274,28 +251,26 @@ class EntitiesDB:
"""
Returns the attributes of the given link uid as a dict.
"""
self.dbLock.acquire()
returnValue = None
try:
returnValue = self.database.edges[uid]
except KeyError:
self.messageHandler.error(
"Tried to get link with nonexistent UID.")
finally:
self.dbLock.release()
return returnValue
with self.dbLock:
returnValue = None
try:
returnValue = self.database.edges[uid]
except KeyError:
self.mainWindow.MESSAGEHANDLER.error(f"Tried to get link with nonexistent UID: {uid}")
finally:
return returnValue
def removeEntity(self, uid: str, fromServer=False, updateTimeLine=True) -> None:
"""
Removes the entity with the given uid, if it exists.
"""
self.dbLock.acquire()
ent = None
if self.isNodeNoLock(uid):
ent = self.getEntityNoLock(uid)
self.mainWindow.populateEntitiesWidget(ent, add=False)
self.database.remove_node(uid)
self.dbLock.release()
with self.dbLock:
ent = None
if self.isNodeNoLock(uid):
ent = self.getEntityNoLock(uid)
self.mainWindow.populateEntitiesWidget(ent, add=False)
self.database.remove_node(uid)
if ent is not None:
self.mainWindow.handleGroupNodeUpdateAfterEntityDeletion(uid) # Blocking - locks the db.
if not fromServer:
@@ -308,10 +283,9 @@ class EntitiesDB:
Removes the link with the given uid (in string or tuple form),
if it exists.
"""
self.dbLock.acquire()
if self.isLinkNoLock(uid):
self.database.remove_edge(uid[0], uid[1])
self.dbLock.release()
with self.dbLock:
if self.isLinkNoLock(uid):
self.database.remove_edge(uid[0], uid[1])
if not fromServer:
self.mainWindow.sendLocalDatabaseUpdateToServer({"uid": uid}, 2)
@@ -319,14 +293,13 @@ class EntitiesDB:
"""
Checks if an entity with the specified primary attribute exists.
"""
self.dbLock.acquire()
result = False
for node in self.database.nodes():
details = self.database.nodes[node]
if details[list(details)[1]] == primaryAttr:
result = True
break
self.dbLock.release()
with self.dbLock:
result = False
for node in self.database.nodes():
details = self.database.nodes[node]
if details[list(details)[1]] == primaryAttr:
result = True
break
return result
def getEntityOfType(self, primaryAttr: str, entityType: str) -> Union[dict, None]:
@@ -334,16 +307,15 @@ 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
self.dbLock.acquire()
for node in self.database.nodes():
details = self.database.nodes[node]
if details['Entity Type'] == entityType and details[primaryField] == primaryAttr:
result = dict(details)
break
self.dbLock.release()
with self.dbLock:
for node in self.database.nodes():
details = self.database.nodes[node]
if details['Entity Type'] == entityType and details[primaryField] == primaryAttr:
result = dict(details)
break
return result
def getLinkIfExists(self, uid) -> Union[None, dict]:
@@ -351,36 +323,29 @@ class EntitiesDB:
Returns the attributes of the given link uid as a dict.
Does not create an error if the link does not exist.
"""
self.dbLock.acquire()
returnValue = None
try:
returnValue = self.database.edges[uid]
except KeyError:
pass
finally:
self.dbLock.release()
return returnValue
with self.dbLock:
returnValue = None
try:
returnValue = self.database.edges[uid]
except KeyError:
pass
finally:
return returnValue
def getIncomingLinks(self, uid: str):
"""
Get all incoming edges for the given entity uid (primary attribute).
"""
self.dbLock.acquire()
returnValue = None
if self.isNodeNoLock(uid):
returnValue = self.database.in_edges(uid)
self.dbLock.release()
with self.dbLock:
returnValue = self.database.in_edges(uid) if self.isNodeNoLock(uid) else None
return returnValue
def getOutgoingLinks(self, uid: str):
"""
Get all outgoing edges for the given entity uid (primary attribute).
"""
self.dbLock.acquire()
returnValue = None
if self.isNodeNoLock(uid):
returnValue = self.database.out_edges(uid)
self.dbLock.release()
with self.dbLock:
returnValue = self.database.out_edges(uid) if self.isNodeNoLock(uid) else None
return returnValue
def isNode(self, uid: Union[str, list, tuple]) -> bool:
@@ -388,11 +353,8 @@ class EntitiesDB:
Returns True if the uid (primary attribute) given exists as
an entity, and False otherwise.
"""
self.dbLock.acquire()
returnValue = False
if isinstance(uid, str) and self.database.nodes.get(uid) is not None:
returnValue = True
self.dbLock.release()
with self.dbLock:
returnValue = isinstance(uid, str) and self.database.nodes.get(uid) is not None
return returnValue
def isNodeNoLock(self, uid: str) -> bool:
@@ -402,19 +364,14 @@ class EntitiesDB:
Used only in this class, as it does not lock.
"""
if self.database.nodes.get(uid) is not None:
return True
return False
return self.database.nodes.get(uid) is not None
def isLink(self, uid: Union[str, list, tuple]) -> bool:
"""
Returns True if the uid given exists as a link, and False otherwise.
"""
self.dbLock.acquire()
returnValue = False
if isinstance(uid, tuple) and self.database.edges.get(uid) is not None:
returnValue = True
self.dbLock.release()
with self.dbLock:
returnValue = isinstance(uid, tuple) and self.database.edges.get(uid) is not None
return returnValue
def isLinkNoLock(self, uid: tuple) -> Union[bool, dict]:
@@ -428,60 +385,43 @@ class EntitiesDB:
return False
def getEntityType(self, uid: str) -> Union[None, dict]:
self.dbLock.acquire()
returnValue = None
try:
returnValue = self.getEntityNoLock(uid)['Entity Type']
except KeyError:
pass
finally:
self.dbLock.release()
return returnValue
with self.dbLock:
returnValue = None
try:
returnValue = self.getEntityNoLock(uid)['Entity Type']
except KeyError:
pass
finally:
return returnValue
def mergeDatabases(self, newDB_nodes: dict, newDB_edges: dict, fromServer=True) -> None:
"""
Merges the existing database with the one provided.
Overwrites older attributes with newer ones.
Overwrites older attributes with newer ones based on date last edited.
"""
self.dbLock.acquire()
differenceGraph = nx.DiGraph()
# Note: If we ever receive a node without a 'Date Last Edited' field, ignore it.
differenceGraph.add_nodes_from([(x, newDB_nodes[x])
for x in newDB_nodes if (x not in self.database.nodes()) or
(
x in self.database.nodes() and
newDB_nodes[x].get('Date Last Edited') and
newDB_nodes[x]['Date Last Edited'] >
self.database.nodes[x]['Date Last Edited']
)
])
differenceGraph.add_edges_from([(x, y, newDB_edges[(x, y)])
for x, y in newDB_edges if ((x, y) not in self.database.edges()) or
(
(x, y) in self.database.edges() and
newDB_edges[(x, y)].get('Date Last Edited') and
newDB_edges[(x, y)]['Date Last Edited'] >
self.database.edges[(x, y)]['Date Last Edited']
)
])
if differenceGraph.number_of_nodes() > 0:
self.database = nx.compose(self.database, differenceGraph)
# Some nodes given by differenceGraph may be empty dicts, with an existing node's uid as the key.
for node in differenceGraph.nodes:
self.mainWindow.populateEntitiesWidget(self.database.nodes[node], add=True)
if not fromServer:
if self.mainWindow.FCOM.isConnected():
# diffNew = nx.DiGraph()
# diffNew.add_nodes_from([(x, self.database.nodes[x])
# for x in self.database.nodes() if x not in differenceGraph.nodes()
# ])
# diffNew.add_edges_from([(x, y, self.database.edges[(x, y)])
# for x, y in self.database.edges() if (x, y) not in differenceGraph.edges()
# ])
with self.dbLock:
differenceGraph = nx.DiGraph()
differenceGraph.add_nodes_from([(n, nDict)
for n, nDict in newDB_nodes.items() if (n not in self.database.nodes()) or
(
nDict.get('Date Last Edited', '') >
self.database.nodes[n].get('Date Last Edited', '')
)
])
differenceGraph.add_edges_from([(e[0], e[1], eDict)
for e, eDict in newDB_edges.items() if (e not in self.database.edges()) or
(
eDict.get('Date Last Edited', '') >
self.database.edges[e].get('Date Last Edited', '')
)
])
if differenceGraph.number_of_nodes():
self.database = nx.compose(self.database, differenceGraph)
# Some nodes given by differenceGraph may be empty dicts, with an existing node's uid as the key.
for node in differenceGraph.nodes:
self.mainWindow.populateEntitiesWidget(self.database.nodes[node], add=True)
if not fromServer and self.mainWindow.FCOM.isConnected():
# Assume we are already synced with server, so just send the difference.
self.mainWindow.FCOM.syncDatabase(self.mainWindow.SETTINGS.value("Project/Server/Project"),
differenceGraph)
self.dbLock.release()

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env python3
import contextlib
from ast import literal_eval
from typing import Union
from pathlib import Path
@@ -26,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
@@ -53,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)
@@ -148,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
@@ -165,7 +164,7 @@ class CommunicationsHandler(QtCore.QObject):
except ConnectionRefusedError:
self.mainWindow.MESSAGEHANDLER.error("Did not connect: Server not running.", popUp=True, exc_info=False)
except Exception as exception:
self.mainWindow.MESSAGEHANDLER.error("Did not connect: " + str(exception))
self.mainWindow.MESSAGEHANDLER.error(f"Did not connect: {str(exception)}")
try:
if self.sock is not None:
@@ -184,9 +183,7 @@ class CommunicationsHandler(QtCore.QObject):
"""
Check if socket is in a working state.
"""
if self.sock is not None and self.sock.fileno() != -1:
return True
return False
return self.sock is not None and self.sock.fileno() != -1
def close(self) -> None:
global closeSoftwareLock
@@ -214,14 +211,13 @@ class CommunicationsHandler(QtCore.QObject):
def decryptTransmission(self, bytesObject) -> Union[bytes, None]:
decrypter = self.cipher.decryptor()
try:
message = decrypter.update(bytesObject) + decrypter.finalize()
return message
return decrypter.update(bytesObject) + decrypter.finalize()
except InvalidTag:
# If the ciphertext cannot be decrypted to a valid message, return None.
return None
def transmitMessage(self, messageJson: dict, showErrorOnBrokenPipe: bool = True) -> None:
self.mainWindow.MESSAGEHANDLER.debug('Sending Message: ' + str(messageJson))
self.mainWindow.MESSAGEHANDLER.debug(f'Sending Message: {messageJson}')
argEncoded = str(messageJson)
largeMessageUUID = str(uuid4())
try:
@@ -244,19 +240,15 @@ class CommunicationsHandler(QtCore.QObject):
"""
try:
self.transmitMessage({"Operation": "Close Socket", "Arguments": {}}, showErrorOnBrokenPipe=False)
except OSError:
# Typically this is due to bad file descriptor, i.e. server is closed.
pass
except AttributeError:
# This happens if no connection was established while the software was running
except (OSError, AttributeError):
# OSError thrown due to bad file descriptor, i.e. server is closed.
# AttributeError thrown if no connection was established while the software was running
pass
finally:
try:
with contextlib.suppress(OSError):
# OSError thrown if the socket is already closed.
if self.sock is not None:
self.sock.shutdown(socket.SHUT_RDWR)
except OSError:
# This would typically occur if the socket is already closed.
pass
try:
self.sock.close()
finally:
@@ -276,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]
@@ -299,15 +291,12 @@ class CommunicationsHandler(QtCore.QObject):
self.inbox.put(preInbox.pop(messageID).get("message"))
except socket.error as socketError:
self.mainWindow.MESSAGEHANDLER.error('Socket Error: ' + str(socketError))
self.mainWindow.MESSAGEHANDLER.error(f'Socket Error: {str(socketError)}')
# If something happens, wait 2 seconds then try again.
closeSoftwareLock.acquire()
if not closeSoftware:
closeSoftwareLock.release()
time.sleep(2)
else:
closeSoftwareLock.release()
break
with closeSoftwareLock:
if closeSoftware:
break
time.sleep(2)
except ValueError:
# E.g.: Unpack failed: incomplete input
# In this case, we are being sent fragmented messages.
@@ -333,14 +322,12 @@ class CommunicationsHandler(QtCore.QObject):
continueTimestamp: int = 0) -> None:
collector_entities_to_send = []
for entity in collector_entities:
try:
with contextlib.suppress(KeyError):
dereferenced_entity = dict(entity)
collector_entities_to_send.append(dereferenced_entity)
# Icon is not necessary for any collector as of now: 2022/4/3.
# Cutting it out saves data.
dereferenced_entity['Icon'] = ''
except KeyError:
pass
message = {'Operation': 'Start Server Collector',
'Arguments': {
'collector_name': collector_name,
@@ -372,14 +359,10 @@ class CommunicationsHandler(QtCore.QObject):
resolution_uid: str) -> None:
resolution_entities_to_send = []
for entity in resolution_entities:
try:
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'] = ''
except KeyError:
pass
dereferenced_entity['Icon'] = dereferenced_entity['Icon'].toBase64().data()
message = {'Operation': 'Run Resolution',
'Arguments': {
'resolution_name': resolution_name,
@@ -392,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:
@@ -512,17 +500,13 @@ class CommunicationsHandler(QtCore.QObject):
# Being verbose is better than prematurely optimizing for a few kbps of
# network traffic.
def receiveDatabaseUpdateEvent(self, entity_json: dict, add: int) -> None:
try:
with contextlib.suppress(KeyError):
entity_json['Icon'] = QtCore.QByteArray(b64decode(entity_json['Icon']))
except KeyError:
pass
self.receive_project_database_update.emit(entity_json, add)
def sendDatabaseUpdateEvent(self, project_name: str, entity_json: dict, add: int) -> None:
try:
with contextlib.suppress(KeyError):
entity_json['Icon'] = entity_json['Icon'].toBase64().data()
except KeyError:
pass
message = {"Operation": "Update Project Entities",
"Arguments": {
'project_name': project_name,
@@ -557,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()
@@ -573,30 +557,28 @@ class CommunicationsHandler(QtCore.QObject):
"""
if not filePath.exists() or not filePath.is_file():
return
fileHandler = open(filePath, 'rb')
currThread = threading.currentThread()
while getattr(currThread, "continue_running", True):
filePart = fileHandler.read(512)
if not filePart:
messageJson = {"Operation": "File Upload Done",
with open(filePath, 'rb') as fileHandler:
currThread = threading.current_thread()
while getattr(currThread, "continue_running", True):
filePart = fileHandler.read(512)
if not filePart:
messageJson = {"Operation": "File Upload Done",
"Arguments": {
'project_name': project_name,
'file_name': file_name
}}
self.transmitMessage(messageJson)
break
messageJson = {"Operation": "File Upload",
"Arguments": {
'project_name': project_name,
'file_name': file_name
'file_name': file_name,
'file_contents': filePart
}}
self.transmitMessage(messageJson)
break
messageJson = {"Operation": "File Upload",
"Arguments": {
'project_name': project_name,
'file_name': file_name,
'file_contents': filePart
}}
self.transmitMessage(messageJson)
fileHandler.close()
def sendFileAbort(self, project_name: str, file_name: str) -> None:
try:
with contextlib.suppress(KeyError):
uploadToAbort = self.uploadingFiles.pop(file_name)
uploadToAbort.continue_running = False
messageJson = {"Operation": "File Upload Abort",
@@ -605,8 +587,6 @@ class CommunicationsHandler(QtCore.QObject):
'file_name': file_name
}}
self.transmitMessage(messageJson)
except KeyError:
pass
def scanInbox(self) -> None:
"""
@@ -619,15 +599,14 @@ class CommunicationsHandler(QtCore.QObject):
message = self.inbox.get(timeout=0.2)
except Empty:
with closeSoftwareLock:
if not closeSoftware:
time.sleep(0.2)
continue
else:
if closeSoftware:
return
time.sleep(0.2)
continue
if prevMesg == message:
# Same message, do not waste time handling.
continue
self.mainWindow.MESSAGEHANDLER.debug('Message to handle: ' + str(message))
self.mainWindow.MESSAGEHANDLER.debug(f'Message to handle: {str(message)}')
operation = message['Operation']
arguments = message['Arguments']
if operation == 'Get Server Resolutions':
@@ -667,8 +646,8 @@ class CommunicationsHandler(QtCore.QObject):
elif operation == "Start Collector":
self.receiveStartCollector(**arguments)
else:
self.mainWindow.MESSAGEHANDLER.warning('Unhandled message: ' + str(message) +
' On Operation: ' + str(operation))
self.mainWindow.MESSAGEHANDLER.warning(f'Unhandled message: {str(message)} On Operation: '
f'{str(operation)}')
prevMesg = message
def handleStatusMessage(self, operation: str, message: str, status_code: int) -> None:
@@ -686,61 +665,53 @@ class CommunicationsHandler(QtCore.QObject):
:param status_code:
:return:
"""
if status_code != 200:
if status_code == 404 and message == 'No project with the specified name exists!':
self.close_project_signal.emit()
else:
self.status_message_signal.emit('Operation ' + operation + ' failed with status code ' +
str(status_code) + ': ' + message, True)
else:
if operation == 'Create Project':
# No need to do anything here. Creating a new project also opens it.
pass
elif operation == 'Open Project':
projectName = message.split(': ', 1)[1]
self.open_project_signal.emit(projectName)
# Show the user that the server is doing something.
elif operation == 'Opening Project':
self.status_message_signal.emit(message, True)
elif operation == 'Close Project':
self.close_project_signal.emit()
elif operation == 'Create Canvas':
# No need to do anything here. Creating a new canvas also opens it.
pass
elif operation == 'Open Canvas':
canvas_name = message.split(': ', 1)[1]
self.open_project_canvas_signal.emit(canvas_name)
elif operation == 'Close Canvas':
canvas_name = message.split(': ', 1)[1]
self.close_project_canvas_signal.emit(canvas_name)
elif operation == 'Connect To Server':
server_name = message.split(': ', 1)[1]
self.connected_to_server_listener.emit(server_name)
elif operation == 'File Upload':
file_name = message.split(': ', 1)[1]
self.file_upload_finished_signal.emit(file_name)
elif operation == 'File Download Done':
file_name = message.split(': ', 1)[1]
self.receiveFileDoneListener(file_name)
elif operation == 'Abort Resolution':
if status_code == 200:
if operation == 'Abort Resolution':
# Remove resolution from resolutions list.
resolution_uid = message.split(': ', 1)[1]
self.remove_server_resolution_from_running_signal.emit(resolution_uid)
elif operation == 'Close Canvas':
canvas_name = message.split(': ', 1)[1]
self.close_project_canvas_signal.emit(canvas_name)
elif operation == 'Close Project':
self.close_project_signal.emit()
elif operation == 'Connect To Server':
server_name = message.split(': ', 1)[1]
self.connected_to_server_listener.emit(server_name)
elif operation in {'Create Project', 'Create Canvas', 'Stop Collector'}:
# No need to do anything for these.
pass
elif operation == 'Delete Project':
# Remove project from server projects list.
project_name = message.split(': ', 1)[1]
self.delete_server_project_signal.emit(project_name)
elif operation == 'File Download Done':
file_name = message.split(': ', 1)[1]
self.receiveFileDoneListener(file_name)
elif operation == 'File Upload Abort':
file_name = message.split(': ', 1)[1]
# Remove file from uploading files list.
self.file_upload_abort_signal.emit(file_name)
elif operation == 'Stop Collector':
# No need to do anything here - stopping collectors is only done by the client.
pass
elif operation == 'File Upload':
file_name = message.split(': ', 1)[1]
self.file_upload_finished_signal.emit(file_name)
elif operation == 'Open Canvas':
canvas_name = message.split(': ', 1)[1]
self.open_project_canvas_signal.emit(canvas_name)
elif operation == 'Open Project':
projectName = message.split(': ', 1)[1]
self.open_project_signal.emit(projectName)
elif operation == 'Opening Project':
self.status_message_signal.emit(message, True)
else:
self.mainWindow.MESSAGEHANDLER.warning('Unhandled status message: ' + message +
' Code: ' + str(status_code) +
' On Operation: ' + str(operation))
self.mainWindow.MESSAGEHANDLER.warning(f'Unhandled status message: {message} Code: {status_code} '
f'On Operation: {operation}')
elif status_code == 404 and message == 'No project with the specified name exists!':
self.close_project_signal.emit()
else:
self.status_message_signal.emit(f'Operation {operation} failed with status code {status_code}: {message}',
True)
def receiveFile(self, project_name: str, file_name: str, saveDir: Path) -> None:
# Do not download files already being downloaded.
@@ -761,18 +732,19 @@ class CommunicationsHandler(QtCore.QObject):
fileHandler.write(file_contents)
except Exception:
# In case something goes wrong in the middle of writing.
self.mainWindow.MESSAGEHANDLER.warning('Received data for file: ' + file_name +
' but no valid file handler exists for this file.')
self.mainWindow.MESSAGEHANDLER.warning(f'Received data for file: {file_name} but no valid file handler '
f'exists for this file.')
def receiveFileDoneListener(self, file_name: str) -> None:
fileHandler = self.downloadingFiles.pop(file_name)
if fileHandler is None:
self.mainWindow.MESSAGEHANDLER.warning('Received file: ' + file_name +
' but no file handler exists for this file.')
self.mainWindow.MESSAGEHANDLER.warning(f'Received file: {file_name} but no file handler exists for this '
f'file.')
return
fileHandler.close()
self.status_message_signal.emit('Finished downloading file from server: ' + file_name, True)
self.status_message_signal.emit(f'Finished downloading file from server: {file_name}', True)
def receiveFileAbort(self, project_name: str, file_name: str) -> None:
messageJson = {"Operation": "File Download Abort",
@@ -813,12 +785,10 @@ class CommunicationsHandler(QtCore.QObject):
:param file_name:
:return:
"""
try:
with contextlib.suppress(KeyError):
uploadToAbort = self.uploadingFiles.pop(file_name)
uploadToAbort.continue_running = False
self.file_upload_abort_signal.emit(file_name)
except KeyError:
pass
def sendFileAbortAll(self, project_name: str) -> None:
"""
@@ -828,7 +798,7 @@ class CommunicationsHandler(QtCore.QObject):
:return:
"""
for file_name in dict(self.uploadingFiles):
try:
with contextlib.suppress(KeyError):
uploadToAbort = self.uploadingFiles.pop(file_name)
uploadToAbort.continue_running = False
messageJson = {"Operation": "File Upload Abort",
@@ -837,8 +807,6 @@ class CommunicationsHandler(QtCore.QObject):
'file_name': file_name
}}
self.transmitMessage(messageJson)
except KeyError:
pass
def receiveFileAbortAll(self, project_name: str) -> None:
"""

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 '

File diff suppressed because it is too large Load Diff

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()
@@ -352,7 +352,7 @@ class ResolutionList(QtWidgets.QTreeWidget):
if resolution not in self.resolutionManager.getResolutionsInCategory(category):
return
self.mainWindow.runResolution(category + '/' + resolution)
self.mainWindow.runResolution(f'{category}/{resolution}')
class ResolutionWidget(QtWidgets.QTreeWidgetItem):
@@ -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

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
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
@@ -14,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)
@@ -29,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()
@@ -50,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.
@@ -76,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()
@@ -95,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))
@@ -129,16 +125,12 @@ class TimeWidget(QtWidgets.QWidget):
if nodeMinute not in self.timeDetails[nodeYear][nodeMonth][nodeDay][nodeHour]:
# Sanity check. Should not be able to remove nodes that do not exist, but you never know.
if added:
self.timeDetails[nodeYear][nodeMonth][nodeDay][nodeHour][nodeMinute] = 1
else:
self.timeDetails[nodeYear][nodeMonth][nodeDay][nodeHour][nodeMinute] = 0
else:
if added:
self.timeDetails[nodeYear][nodeMonth][nodeDay][nodeHour][nodeMinute] += 1
else:
if self.timeDetails[nodeYear][nodeMonth][nodeDay][nodeHour][nodeMinute] > 0:
self.timeDetails[nodeYear][nodeMonth][nodeDay][nodeHour][nodeMinute] -= 1
self.timeDetails[nodeYear][nodeMonth][nodeDay][nodeHour][nodeMinute] = 1 if added else 0
elif added:
self.timeDetails[nodeYear][nodeMonth][nodeDay][nodeHour][nodeMinute] += 1
elif self.timeDetails[nodeYear][nodeMonth][nodeDay][nodeHour][nodeMinute] > 0:
self.timeDetails[nodeYear][nodeMonth][nodeDay][nodeHour][nodeMinute] -= 1
if updateGraph:
self.drawChart([])
@@ -177,15 +169,11 @@ class TimeWidget(QtWidgets.QWidget):
if minute is not None:
barsDict = {minute: self.timeDetails[year][month][day][hour][minute]}
self.currentTimeStep = [year, month, day, hour, minute]
self.drawChartHelper(barsDict, self.currentTimeStep)
elif hour is not None:
barsDict = {}
for minute in self.timeDetails[year][month][day][hour]:
barsDict[minute] = self.timeDetails[year][month][day][hour][minute]
self.currentTimeStep = [year, month, day, hour]
self.drawChartHelper(barsDict, self.currentTimeStep)
barsDict = {minute: self.timeDetails[year][month][day][hour][minute]
for minute in self.timeDetails[year][month][day][hour]}
self.currentTimeStep = [year, month, day, hour]
elif day is not None:
barsDict = {}
for hour in self.timeDetails[year][month][day]:
@@ -193,8 +181,6 @@ class TimeWidget(QtWidgets.QWidget):
for minute in self.timeDetails[year][month][day][hour]:
barsDict[hour] += self.timeDetails[year][month][day][hour][minute]
self.currentTimeStep = [year, month, day]
self.drawChartHelper(barsDict, self.currentTimeStep)
elif month is not None:
barsDict = {}
for day in self.timeDetails[year][month]:
@@ -203,8 +189,6 @@ class TimeWidget(QtWidgets.QWidget):
for minute in self.timeDetails[year][month][day][hour]:
barsDict[day] += self.timeDetails[year][month][day][hour][minute]
self.currentTimeStep = [year, month]
self.drawChartHelper(barsDict, self.currentTimeStep)
elif year is not None:
barsDict = {}
for month in self.timeDetails[year]:
@@ -214,8 +198,6 @@ class TimeWidget(QtWidgets.QWidget):
for minute in self.timeDetails[year][month][day][hour]:
barsDict[month] += self.timeDetails[year][month][day][hour][minute]
self.currentTimeStep = [year]
self.drawChartHelper(barsDict, self.currentTimeStep)
else:
barsDict = {}
for year in self.timeDetails:
@@ -226,7 +208,8 @@ class TimeWidget(QtWidgets.QWidget):
for minute in self.timeDetails[year][month][day][hour]:
barsDict[year] += self.timeDetails[year][month][day][hour][minute]
self.currentTimeStep = []
self.drawChartHelper(barsDict, self.currentTimeStep)
self.drawChartHelper(barsDict, self.currentTimeStep)
def drawChartHelper(self, barsDict: dict, timestep: list):
timelineSeries = QtCharts.QBarSeries(self.timelineChart)
@@ -234,21 +217,20 @@ 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)
if barsDict[bar] > maxEntityNum:
maxEntityNum = barsDict[bar]
value = ""
for step in range(len(timestep)):
for step, stepValue in enumerate(timestep):
if step <= 2:
value += str(timestep[step]) + '/'
value += f'{str(stepValue)}/'
if step == 2:
value = value[:-1]
value += " "
value = f"{value[:-1]} "
else:
value += str(timestep[step]) + ':'
value += f'{str(stepValue)}:'
value += str(bar)
xAxisValues.append(value)
self.timelineChart.removeAllSeries()
@@ -276,10 +258,7 @@ class TimeWidget(QtWidgets.QWidget):
self.timescaleSelector.adjustLabelsToHour()
elif timesteps == 4:
self.timescaleSelector.adjustLabelsToMinute()
elif timesteps == 5:
self.timescaleSelector.adjustLabelsToSecond()
else:
# Just in case.
self.timescaleSelector.adjustLabelsToSecond()
yAxis.applyNiceNumbers()
@@ -305,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)
@@ -478,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):
@@ -501,14 +478,13 @@ class ChatBox(QtWidgets.QWidget):
self.setMinimumWidth(500)
self.setMaximumWidth(500)
self.chatName = getuser() + ": "
self.chatName = f"{getuser()}: "
chatLayout = QtWidgets.QGridLayout()
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()
@@ -536,14 +512,10 @@ class LoggingUpdateThread(QtCore.QThread):
self.messageHandler = messageHandler
def run(self):
while True:
if self.endLogging:
break
while not self.endLogging:
if not self.messageHandler.logQueue.empty():
try:
with contextlib.suppress(queue.Empty):
logMsg = self.messageHandler.logQueue.get().getMessage()
self.loggingSignal.emit(logMsg)
except queue.Empty:
pass
else:
self.msleep(100)

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
@@ -50,13 +50,10 @@ class DockBarTwo(QtWidgets.QDockWidget):
self.initialiseLayout()
def setNotesText(self, newText: str) -> None:
if self.tabNotes.textEditor.isReadOnly():
self.tabNotes.textEditor.contents = newText
self.tabNotes.textEditor.setMarkdown(newText)
else:
if not self.tabNotes.textEditor.isReadOnly():
self.tabNotes.textEditor.setReadOnly(True)
self.tabNotes.textEditor.contents = newText
self.tabNotes.textEditor.setMarkdown(newText)
self.tabNotes.textEditor.contents = newText
self.tabNotes.textEditor.setMarkdown(newText)
def getNotesText(self) -> str:
"""
@@ -101,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()
@@ -121,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)
###
@@ -131,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)
@@ -167,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)
@@ -248,15 +244,15 @@ class EntityDetails(QtWidgets.QWidget):
else:
self.populateMultiRelationshipHelperLink(item)
linksNumber += 1
self.multiNodesTableLabelOne.setText('Selected Nodes: ' + str(nodesNumber))
self.multiNodesTableLabelTwo.setText('Selected Links: ' + str(linksNumber))
self.multiNodesTableLabelOne.setText(f'Selected Nodes: {str(nodesNumber)}')
self.multiNodesTableLabelTwo.setText(f'Selected Links: {str(linksNumber)}')
self.switchLayoutHelper(numberOfItems, isNode)
except Exception as exc:
# If an error is thrown at some point during the process, show the default nothing selected screen.
self.detailsLayout.setCurrentIndex(0)
self.mainWindow.MESSAGEHANDLER.error('Error occurred while trying to display the details of the selected '
'nodes: ' + str(exc), popUp=False, exc_info=False)
self.mainWindow.MESSAGEHANDLER.error(f'Error occurred while trying to display the details of the selected '
f'nodes: {str(exc)}', popUp=False, exc_info=False)
# Display helper functions
def clearDetailsHelper(self) -> None:
@@ -279,7 +275,7 @@ class EntityDetails(QtWidgets.QWidget):
return
rowCount = 0
for key in jsonDict:
if key == "uid" or key == "Child UIDs" or key == "Icon":
if key in hidden_fields_dockbars:
continue
elif key == "Notes":
notesTextArea = RichNotesEditor(self, jsonDict[key], False)
@@ -287,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)
@@ -307,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)
@@ -317,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']
@@ -334,28 +335,28 @@ 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]],
uid)
self.relationshipsIncomingTable.setHeaderLabel('Incoming Links: ' + str(len(inc)))
self.relationshipsIncomingTable.setHeaderLabel(f'Incoming Links: {len(inc)}')
for edge in out:
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]],
uid)
self.relationshipsOutgoingTable.setHeaderLabel('Outgoing Links: ' + str(len(out)))
self.relationshipsOutgoingTable.setHeaderLabel(f'Outgoing Links: {len(out)}')
def populateMultiRelationshipHelperNode(self, nodeJson) -> None:
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]],
@@ -380,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("--")
@@ -418,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)
@@ -444,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
@@ -477,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):
@@ -545,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)
@@ -557,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

@@ -1,5 +1,6 @@
#!/usr/bin/env python3
import contextlib
from json import dumps
import math
from typing import Any, Optional
@@ -9,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)
@@ -19,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:
@@ -52,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)
@@ -63,24 +72,29 @@ 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 = str(newText)
if newText != '':
if len(newText) > 50:
newText = newText[:47] + "..."
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.
try:
with contextlib.suppress(ValueError):
self.connectors.remove(connector)
except ValueError:
pass
def addConnector(self, connector) -> None:
self.connectors.append(connector)
@@ -118,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():
@@ -146,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:
@@ -164,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)
@@ -186,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)
@@ -235,10 +255,7 @@ class BaseConnector(QGraphicsItemGroup):
self.updateLabel(name)
if uid is not None:
if isinstance(uid, list) or isinstance(uid, set):
self.uid = set(uid)
else:
self.uid = {uid}
self.uid = set(uid) if isinstance(uid, (list, set)) else {uid}
else:
self.uid = {(origin.uid, destination.uid)}
@@ -254,15 +271,15 @@ 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()
def updateLabel(self, newText: str = '') -> None:
if len(newText) > 50:
newText = newText[:47] + "..."
newText = f"{newText[:47]}..."
self.labelItem.setText(newText)
self.update()
@@ -285,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()
@@ -305,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
@@ -316,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)
@@ -375,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()
@@ -390,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
@@ -409,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,14 +52,13 @@ 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))
return message
# Set the severity level
def setSeverityLevel(self, level: int):
def setSeverityLevel(self, level):
currentLogLevel = self.linkScopeLogger.level
try:
level = int(level)

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
@@ -10,15 +15,374 @@ from reportlab.platypus.tableofcontents import TableOfContents
from reportlab.lib.units import cm
from reportlab.pdfgen import canvas
from reportlab.platypus import Paragraph, PageBreak, Image, Spacer, Table, LongTable, ParagraphAndImage
from reportlab.platypus import Paragraph, PageBreak, Image, Spacer, Table, ParagraphAndImage
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
@@ -33,7 +397,7 @@ class MyDocTemplate(BaseDocTemplate):
style = flowable.style.name
if style == 'Heading1':
self.notify('TOCEntry', (0, text, self.page))
if style == 'Heading2':
elif style == 'Heading2':
self.notify('TOCEntry', (1, text, self.page))
@@ -58,7 +422,7 @@ class ReportBuilder(canvas.Canvas):
canvas.Canvas.save(self)
def drawHeaderAndFooter(self, pageCount):
pageCountString = "Page %s of %s" % (self._pageNumber, pageCount)
pageCountString = f"Page {self._pageNumber} of {pageCount}"
self.saveState()
self.setStrokeColorRGB(0, 0, 0)
self.setLineWidth(0.5)
@@ -96,7 +460,7 @@ class PDFReport:
self.elements.append(spacer)
self.elements.append(report_subtitle)
spacer = Spacer(100, 200)
spacer = Spacer(100, 425)
self.elements.append(spacer)
date = datetime.now().strftime('%Y-%m-%d %H:%m:%S')
@@ -165,14 +529,16 @@ class PDFReport:
"""
text = Paragraph(notes, notesParagraph)
if type(entityImagePath) == Drawing:
if isinstance(entityImagePath, Drawing):
tbl = ReportImageAndParagraph(text, entityImagePath, side='left', xpad=10, ypad=0)
elif entityImagePath.endswith('.png') or entityImagePath.endswith('.jpg'):
entityImage = Image(entityImagePath, kind='proportional')
entityImage = Image(entityImagePath)
entityImage.preserveAspectRatio = True
entityImage.drawHeight = 2 * inch
entityImage.drawWidth = 2 * inch
tbl = ReportImageAndParagraph(text, entityImage, side='left', xpad=10, ypad=0)
else:
raise ValueError('Invalid Image Type')
links_subHeader = Paragraph("Entity Links", psSubHeaderText)
@@ -188,60 +554,56 @@ class PDFReport:
outgoing_data = [
['Outgoing Links'],
['Resolution Name', 'Child Entity', 'Date Created', 'Notes']]
index = 0
for link in outgoingLinks:
for index, link in enumerate(outgoingLinks):
resolutionText = link['Resolution']
childNodeText = outgoingNames[index]
linkNotesText = link['Notes']
linkName = "".join([resolutionText[counter:counter+24] + "\n"
dateCreatedText = link['Date Created']
linkName = "".join([resolutionText[counter:counter + 24] + "\n"
for counter in range(0, len(resolutionText), 24)])
childNode = "".join([childNodeText[counter:counter+24] + "\n"
for counter in range(0, len(childNodeText), 24)])
linkNotes = "".join([linkNotesText[counter:counter+24] + "\n"
for counter in range(0, len(linkNotesText), 24)])
dateCreated = link['Date Created']
childNode = "".join([childNodeText[counter:counter + 24] + "\n"
for counter in range(0, len(childNodeText), 24)])
linkNotes = "".join([linkNotesText[counter:counter + 24] + "\n"
for counter in range(0, len(linkNotesText), 24)])
dateCreated = "".join([dateCreatedText[counter:counter + 24] + "\n"
for counter in range(0, len(dateCreatedText), 24)])
outgoing_data.append([linkName, childNode, dateCreated, Paragraph(linkNotes)])
index += 1
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:
incoming_data = [
['Incoming Links'],
['Resolution Name', 'Parent Entity', 'Date Created', 'Notes']]
index = 0
for link in incomingLinks:
for index, link in enumerate(incomingLinks):
resolutionText = link['Resolution']
parentNodeText = incomingNames[index]
linkNotesText = link['Notes']
linkName = "".join([resolutionText[counter:counter+24] + "\n"
dateCreatedText = link['Date Created']
linkName = "".join([resolutionText[counter:counter + 24] + "\n"
for counter in range(0, len(resolutionText), 24)])
parentNode = "".join([parentNodeText[counter:counter+24] + "\n"
parentNode = "".join([parentNodeText[counter:counter + 24] + "\n"
for counter in range(0, len(parentNodeText), 24)])
linkNotes = "".join([linkNotesText[counter:counter+24] + "\n"
for counter in range(0, len(linkNotesText), 24)])
dateCreated = link['Date Created']
linkNotes = "".join([linkNotesText[counter:counter + 24] + "\n"
for counter in range(0, len(linkNotesText), 24)])
dateCreated = "".join([dateCreatedText[counter:counter + 24] + "\n"
for counter in range(0, len(dateCreatedText), 24)])
incoming_data.append([linkName, parentNode, dateCreated, Paragraph(linkNotes)])
index += 1
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)
@@ -260,7 +622,7 @@ class PDFReport:
for key in list(entity):
if key not in avoid_parsing_fields and key != 'Notes':
valueText = entity[key]
value = "".join([valueText[counter:counter+80] + "\n"
value = "".join([valueText[counter:counter + 80] + "\n"
for counter in range(0, len(valueText), 80)])
entity_data.append([Paragraph(key, tableParagraph), Paragraph(value, tableParagraph)])
elif key == 'Notes':
@@ -268,28 +630,6 @@ class PDFReport:
entity_notes = Paragraph(entity[key], notesParagraph)
entity_table = Table(data=entity_data, style=entity_table_style, hAlign="CENTER")
appendix_header = Paragraph(f"Appendix {appendixNumber}", notesHeader)
text = []
images = []
imangeNParagraph = []
for appendixDict in appendixDicts:
if appendixDict['AppendixEntityImage'] == '':
text.append(Paragraph(appendixDict['AppendixEntityNotes'], notesParagraph))
elif appendixDict['AppendixEntityNotes'] == '' and appendixDict['AppendixEntityImage'] != '':
img = Image(Path(appendixDict['AppendixEntityImage']), kind='proportional')
# img.preserveAspectRatio=True
img.drawHeight = 2 * inch
img.drawWidth = 2 * inch
img.hAlign = 'LEFT'
images.append(img)
elif appendixDict['AppendixEntityNotes'] != '' and appendixDict['AppendixEntityImage'] != '':
paragraph = appendixDict['AppendixEntityNotes']
img = Image(Path(appendixDict['AppendixEntityImage']), kind='proportional')
img.drawHeight = 2 * inch
img.drawWidth = 2 * inch
imangeNParagraph.append(
ReportImageAndParagraph(Paragraph(paragraph), img, side='left', xpad=10, ypad=0))
self.elements.append(entityTitle)
spacer = Spacer(20, 20)
self.elements.append(spacer)
@@ -314,11 +654,11 @@ class PDFReport:
pie = Pie()
pie.x = 150
pie.y = 65
pie.data = [int(len(incomingLinks)), int(len(outgoingLinks))]
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 int(len(incomingLinks)) > int(len(outgoingLinks)):
if len(incomingLinks) > len(outgoingLinks):
pie.slices[0].popout = 5
else:
pie.slices[1].popout = 5
@@ -333,17 +673,29 @@ class PDFReport:
self.elements.append(entity_notes_header)
self.elements.append(entity_notes)
self.elements.append(spacer)
self.elements.append(appendix_header)
spacer = Spacer(20, 20)
self.elements.append(spacer)
for elementText in text:
self.elements.append(elementText)
for appendixIndex, appendixDict in enumerate(appendixDicts):
appendix_header = Paragraph(f"Entity Appendix {appendixIndex}", notesHeader)
self.elements.append(appendix_header)
self.elements.append(spacer)
for elementImage in images:
self.elements.append(elementImage)
self.elements.append(spacer)
for elementBoth in imangeNParagraph:
self.elements.append(elementBoth)
appendixImage = appendixDict['AppendixEntityImage']
appendixNotes = appendixDict['AppendixEntityNotes']
if appendixImage == '':
self.elements.append(Paragraph(appendixNotes, notesParagraph))
elif appendixNotes == '' and appendixImage != '':
img = Image(Path(appendixImage))
# img.preserveAspectRatio = True
img.drawHeight = 3 * inch
img.drawWidth = 3 * inch
self.elements.append(img)
elif appendixNotes != '' and appendixImage != '':
paragraph = appendixNotes
img = Image(Path(appendixImage))
img.drawHeight = 3 * inch
img.drawWidth = 3 * inch
self.elements.append(ReportImageAndParagraph(Paragraph(paragraph), img, side='left', xpad=10, ypad=0))
self.elements.append(spacer)
self.elements.append(PageBreak())
@@ -356,7 +708,7 @@ class PDFReport:
ParagraphStyle('Report', fontSize=9, justifyBreaks=1, alignment=TA_LEFT,
justifyLastLine=1)
img = Image(timeLineImage, kind='proportional')
img = Image(timeLineImage)
img.drawHeight = 1.3 * inch
img.drawWidth = 6 * inch
img.hAlign = 'LEFT'
@@ -429,11 +781,15 @@ class PDFReport:
head = f'Entity Report: {entityPrimaryField[i - 3]}'
self.nextPagesHeader(True, head)
self.entityPage(title=entityPrimaryField[i - 3], userNotes=entityListData[i][0].get('EntityNotes'),
self.entityPage(title=entityPrimaryField[i - 3],
userNotes=entityListData[i][0].get('EntityNotes'),
entityImagePath=imagePath,
appendixDicts=entityListData[i][1], outgoingLinks=outgoingLinks[i - 3],
appendixDicts=entityListData[i][1],
outgoingLinks=outgoingLinks[i - 3],
incomingLinks=incomingLinks[i - 3],
entity=entity[i - 3], incomingNames=incomingNames[i - 3], outgoingNames=outgoingNames[i - 3],
entity=entity[i - 3],
incomingNames=incomingNames[i - 3],
outgoingNames=outgoingNames[i - 3],
appendixNumber=i - 3)
# Graph report stuff disabled, at least for now.

View File

@@ -1,28 +1,35 @@
#!/usr/bin/env python3
import importlib
import contextlib
import importlib.util
import sys
from os import listdir
from pathlib import Path
from uuid import uuid4
from typing import Union
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 (category, resolution name)
# 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]
@@ -38,12 +45,10 @@ class ResolutionManager:
originTypes = resClassInst.originTypes
resultTypes = resClassInst.resultTypes
resolutionParameters = resClassInst.parameters
try:
with contextlib.suppress(AttributeError):
resolutionCategory = resClassInst.category
if not isinstance(resolutionCategory, str):
raise AttributeError()
except AttributeError:
pass
resolutionCategory = "Uncategorized"
if self.resolutions.get(resolutionCategory) is None:
self.resolutions[resolutionCategory] = {}
self.resolutions[resolutionCategory][resNameString] = {'name': resNameString,
@@ -54,36 +59,39 @@ class ResolutionManager:
'category': resolutionCategory,
'resolution': resClass
}
self.messageHandler.info("Loaded Resolution: " + resNameString)
self.mainWindow.MESSAGEHANDLER.debug(f"Loaded Resolution: {resNameString}")
resolutionsLoaded.append(f'{resolutionCategory}/{resNameString}')
except Exception as e:
self.messageHandler.error("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:
parameters = self.resolutions[resolutionCategory][resolutionNameString]['parameters']
return parameters
return dict(self.resolutions[resolutionCategory][resolutionNameString]['parameters'])
return None
def getResolutionOriginTypes(self, resolutionNameString: str) -> Union[list, None]:
for category in self.resolutions:
if resolutionNameString in self.resolutions[category]:
originTypes = self.resolutions[category][resolutionNameString]['originTypes']
def getResolutionOriginTypes(self, resolutionCategoryNameString: str) -> Union[list, None]:
resolutionCategory, resolutionName = resolutionCategoryNameString.split('/', 1)
with contextlib.suppress(TypeError):
if resolutionName in self.resolutions.get(resolutionCategory):
originTypes = self.resolutions[resolutionCategory][resolutionName]['originTypes']
if '*' in originTypes:
originTypes = self.mainWindow.RESOURCEHANDLER.getAllEntities()
return originTypes
return None
def getResolutionDescription(self, resolutionNameString: str) -> Union[str, None]:
for category in self.resolutions:
if resolutionNameString in self.resolutions[category]:
resolutionDescription = self.resolutions[category][resolutionNameString].get('description', '')
return resolutionDescription
def getResolutionDescription(self, resolutionCategoryNameString: str) -> Union[str, None]:
resolutionCategory, resolutionName = resolutionCategoryNameString.split('/', 1)
with contextlib.suppress(TypeError):
if resolutionName in self.resolutions.get(resolutionCategory):
return self.resolutions[resolutionCategory][resolutionName].get('description', '')
return None
def loadResolutionsFromServer(self, serverRes) -> None:
@@ -122,9 +130,7 @@ class ResolutionManager:
return result
def getResolutionsInCategory(self, category) -> list:
if category in self.resolutions:
return list(self.resolutions[category])
return []
return list(self.resolutions[category]) if category in self.resolutions else []
def getAllResolutions(self) -> list:
categories = self.getResolutionCategories()
@@ -133,24 +139,565 @@ class ResolutionManager:
result += self.getResolutionsInCategory(category)
return result
def executeResolution(self, resolutionName: str, resolutionEntitiesInput: list, parameters: dict,
def executeResolution(self, resolutionCategoryNameString: str, resolutionEntitiesInput: list, parameters: dict,
resolutionUID: str):
for category in self.resolutions:
for resolution in self.resolutions[category]:
if self.resolutions[category][resolution]['name'] == resolutionName:
resolutionCategory, resolutionName = resolutionCategoryNameString.split('/', 1)
with contextlib.suppress(TypeError):
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.
if self.resolutions[category][resolution]['resolution'] == '':
self.mainWindow.executeRemoteResolution(resolutionName, resolutionEntitiesInput, parameters,
resolutionUID)
# Returning a bool, so we know that the resolution is running on the server.
return True
resolutionClass = self.resolutions[category][resolution]['resolution']()
result = resolutionClass.resolution(resolutionEntitiesInput, parameters)
return result
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']()
return resolutionClass.resolution(resolutionEntitiesInput, parameters)
return None
def createMacro(self, resolutionList: list) -> str:
macroUID = str(uuid4())
self.macros[macroUID] = resolutionList
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.
try:
with self.mainWindow.macrosLock:
self.macros.pop(macroUID)
return True
except KeyError:
return False
def loadMacros(self) -> None:
# Load AFTER we load resolutions.
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:
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
# 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)

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'
}
}
@@ -61,13 +62,12 @@ class ContainsPhrase:
counter += 1
if counter > 0:
returnResults.append([{'Phrase': primaryField + ' Contains Phrase: "' + searchPhrase +
f'" {counter} times',
returnResults.append([{'Phrase': f'{primaryField} Contains Phrase: "{searchPhrase}" {counter} times',
'Entity Type': 'Phrase',
'Notes': f'"{searchPhrase}" was found {counter} time(s)\n'
f'Offsets: Matches at character indices: '
f'{(", ".join(map(str, offsets)))}'},
{uid: {'Resolution': 'Contains Phrase ' + searchPhrase,
{uid: {'Resolution': f'Contains Phrase {searchPhrase}',
'Notes': ''}}])
return returnResults

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,31 +19,37 @@ 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:
return None
if currentLink.startswith('//'):
if currentUrl.endswith('/'):
currentUrl = currentUrl[:-1]
currentLink = currentUrl + currentLink[1:]
elif currentLink.startswith('/'):
urlParts = urllib.parse.urlparse(currentUrl)
currentLink = urlParts.scheme + '://' + urlParts.netloc + currentLink
parsedCurrentURL = urllib.parse.urlparse(link)
if all([parsedCurrentURL.scheme, parsedCurrentURL.netloc]):
if currentDomain in currentLink:
newLink = currentLink.split('#')[0].split('?')[0]
if newLink.endswith('/'):
newLink = newLink[:-1]
return newLink
currentLink = f'{urlParts.scheme}://{urlParts.netloc}{currentLink}'
parsedCurrentURL = urllib.parse.urlparse(currentLink)
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,12 @@ 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']).name
return_result.append([{'Country Name': country,
if country := pycountry.countries.get(alpha_2=net['country']):
country_name = country.name
else:
# May not always be an actual Country.
country_name = net['country']
return_result.append([{'Country Name': country_name,
'Entity Type': 'Country'},
{uid: {'Resolution': 'IPWhois', 'Notes': ''}}])
if net['name'] is not None:
@@ -40,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

@@ -1,10 +1,13 @@
#!/usr/bin/env python3
from typing import Union
import contextlib
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
from os import listdir
@@ -14,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:
@@ -26,81 +61,105 @@ 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:
eList = []
for category in self.entityCategoryList:
eList.append(category)
return eList
return list(self.entityCategoryList)
def getAllEntityDetailsWithIconsInCategory(self, category) -> list:
eList = []
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]:
@@ -108,12 +167,11 @@ class ResourceHandler:
try:
for category in self.entityCategoryList:
if entityType in self.entityCategoryList[category]:
for attribute in self.entityCategoryList[category][entityType]['Attributes']:
aList.append(attribute)
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
@@ -121,16 +179,12 @@ class ResourceHandler:
"""
Get all Entity Types in the specified category.
"""
eList = []
for entity in self.entityCategoryList[category]:
eList.append(entity)
return eList
return list(self.entityCategoryList[category])
def getCategoryOfEntityType(self, entityType: Union[str, None]):
for category in self.entityCategoryList:
if entityType in self.entityCategoryList[category]:
return category
return None
return next((category for category in self.entityCategoryList
if entityType in self.entityCategoryList[category]),
None)
def getAllEntities(self) -> list:
"""
@@ -138,8 +192,7 @@ class ResourceHandler:
"""
eList = []
for category in self.getEntityCategories():
for entity in self.getAllEntitiesInCategory(category):
eList.append(entity)
eList.extend(iter(self.getAllEntitiesInCategory(category)))
return eList
def validateAttributesOfEntity(self, entityJSON: dict) -> (bool, str):
@@ -156,7 +209,7 @@ class ResourceHandler:
if attrValue is None or not self.runCheckOnAttribute(
attrValue,
self.entityCategoryList[entityCategory][entityType]['Attributes'][attribute][1]):
return 'Bad value: ' + str(attrValue)
return f'Bad value: {str(attrValue)}'
except Exception:
return False
return True
@@ -172,17 +225,26 @@ class ResourceHandler:
if attrCheck is None:
return False
result = attrCheck.findall(attribute)
if len(result) == 1:
return True
return False
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()
@@ -191,56 +253,54 @@ class ResourceHandler:
try:
entityName = entity.tag.replace('_', ' ')
attributes = entity.find('Attributes')
attributesDict = {}
primaryCount = 0
attributesDict = {}
for attribute in list(attributes):
attributeName = attribute.text
defaultValue = attribute.attrib['default']
valueCheck = attribute.attrib['check']
isPrimary = True if attribute.attrib['primary'] == 'True' else False
isPrimary = attribute.attrib['primary'] == 'True'
if isPrimary:
if primaryCount > 0:
raise AttributeError('Malformed Entity: ' + entityName + ' - too many primary fields')
raise AttributeError(f'Malformed Entity: {entityName} - too many primary fields')
else:
primaryCount += 1
if self.runCheckOnAttribute(defaultValue, valueCheck):
attributesDict[attributeName] = [attribute.attrib['default'], attribute.attrib['check'],
isPrimary]
else:
raise AttributeError('Malformed Entity: ' + entityName + ' - default values do not conform to '
'their corresponding checks.')
if not self.runCheckOnAttribute(defaultValue, valueCheck):
raise AttributeError(f'Malformed Entity: {entityName} - default values do not pass their '
f'corresponding checks.')
attributeName = attribute.text
attributesDict[attributeName] = [attribute.attrib['default'], attribute.attrib['check'], isPrimary]
if primaryCount != 1:
raise AttributeError('Malformed Entity: ' + entityName + ' - invalid number of primary fields '
'specified.')
raise AttributeError(f'Malformed Entity: {entityName} - invalid number of primary fields '
f'specified.')
icon = entity.find('Icon')
if icon is not None:
icon = icon.text.strip()
elif icon is None or icon == '':
icon = 'Default.svg'
icon = icon.text.strip() if icon is not None else 'Default.svg'
if self.entityCategoryList.get(category) is None:
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('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())}
@@ -253,8 +313,8 @@ class ResourceHandler:
eJson[attribute] = self.entityCategoryList[category][entityType]['Attributes'][attribute][0]
break
except KeyError:
self.messageHandler.error("Attempted to get attributes for "
"malformed entity type: " + str(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
@@ -269,7 +329,7 @@ class ResourceHandler:
if value is not None and value != '':
eJson[key] = value
utcNow = datetime.isoformat(datetime.utcnow())
utcNow = datetime.isoformat(datetime.now(timezone.utc))
if eJson['Date Created'] is None:
eJson['Date Created'] = utcNow
else:
@@ -291,8 +351,8 @@ class ResourceHandler:
if self.entityCategoryList[category][entityType]['Attributes'][attribute][2]:
return attribute
except KeyError:
self.messageHandler.error("Attempted to get primary attribute for "
"malformed entity type: " + str(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]:
@@ -304,8 +364,8 @@ class ResourceHandler:
eJson[attribute] = self.entityCategoryList[category][entityType]['Attributes'][attribute][0]
break
except KeyError:
self.messageHandler.error("Attempted to get attributes for "
"malformed entity type: " + str(entityType), True)
self.mainWindow.MESSAGEHANDLER.error(
f"Attempted to get attributes for malformed entity type: {entityType}", True)
return None
eJson['Entity Type'] = entityType
@@ -318,7 +378,7 @@ class ResourceHandler:
except KeyError:
return None
utcNow = datetime.isoformat(datetime.utcnow())
utcNow = datetime.isoformat(datetime.now(timezone.utc))
linkJson['Resolution'] = str(jsonData.get('Resolution')) # This way, if it is None, it is cast to a string.
linkJson['Date Created'] = jsonData.get('Date Created')
# Make sure that dates are always in ISO format.
@@ -330,17 +390,17 @@ class ResourceHandler:
except (TypeError, ValueError):
linkJson['Date Created'] = utcNow
linkJson['Date Last Edited'] = utcNow
linkJson['Notes'] = str(jsonData.get('Notes'))
linkJson['Notes'] = str(jsonData.get('Notes', ""))
# Transfer all values from jsonData to linkJson, but preserve the values and order of linkJson for existing
# keys.
jsonData.update(linkJson)
linkJson.update(jsonData)
jsonData |= linkJson
linkJson |= jsonData
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]:
@@ -349,56 +409,46 @@ 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()
pictureByteArray = QByteArray(pictureContents)
return pictureByteArray
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:
nodes = {}
for nodeKey in graph.nodes:
# Dereference the original dict so we don't actually convert its icon to data.
# Dereference the original dict, so we don't actually convert its icon to data.
nodes[nodeKey] = dict(graph.nodes.get(nodeKey))
try:
with contextlib.suppress(KeyError):
nodes[nodeKey]['Icon'] = nodes[nodeKey]['Icon'].toBase64().data()
except KeyError:
pass
edges = {edgeKey: graph.edges.get(edgeKey) for edgeKey in graph.edges}
return nodes, edges
def deconstructGraphForFileDump(self, graph: nx.DiGraph) -> tuple:
nodes = {}
for nodeKey in graph.nodes:
# Dereference the original dict so we don't actually convert its icon to data.
# Dereference the original dict, so we don't actually convert its icon to data.
nodes[nodeKey] = dict(graph.nodes.get(nodeKey))
try:
with contextlib.suppress(KeyError):
nodes[nodeKey]['Icon'] = nodes[nodeKey]['Icon'].toBase64().data()
except KeyError:
pass
edges = {str(edgeKey): graph.edges.get(edgeKey) for edgeKey in graph.edges}
return nodes, edges
def reconstructGraphFromString(self, graphString: str) -> tuple:
nodes, edges = literal_eval(graphString)
for node in nodes:
try:
with contextlib.suppress(KeyError):
nodes[node]['Icon'] = QByteArray(b64decode(nodes[node]['Icon']))
except KeyError:
pass
return nodes, edges
def reconstructGraphFullFromFile(self, graphNodesAndEdges: Union[tuple, list]) -> nx.DiGraph:
@@ -406,10 +456,8 @@ class ResourceHandler:
graphNodes = graphNodesAndEdges[0]
graphEdges = graphNodesAndEdges[1]
for node in graphNodes:
try:
with contextlib.suppress(KeyError):
graphNodes[node]['Icon'] = QByteArray(b64decode(graphNodes[node]['Icon']))
except KeyError:
pass
returnGraph.add_node(node, **graphNodes[node])
for edge in graphEdges:
@@ -448,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])
@@ -467,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:
@@ -476,11 +523,7 @@ class SingleChoicePropertyInput(QtWidgets.QGroupBox):
vboxLayout.addWidget(radioButton)
def getValue(self):
for option in self.options:
if option.isChecked():
return option.text()
return ''
return next((option.text() for option in self.options if option.isChecked()), '')
class MultiChoicePropertyInput(QtWidgets.QGroupBox):
@@ -498,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:
@@ -507,12 +549,7 @@ class MultiChoicePropertyInput(QtWidgets.QGroupBox):
vboxLayout.addWidget(checkBox)
def getValue(self):
valuesSelected = []
for option in self.options:
if option.isChecked():
valuesSelected.append(option.text())
return valuesSelected
return [option.text() for option in self.options if option.isChecked()]
class MinSizeStackedLayout(QtWidgets.QStackedLayout):
@@ -524,6 +561,7 @@ class MinSizeStackedLayout(QtWidgets.QStackedLayout):
https://stackoverflow.com/a/34300567
"""
def sizeHint(self) -> QSize:
return self.currentWidget().sizeHint()
@@ -550,12 +588,11 @@ class RichNotesEditor(QtWidgets.QTextBrowser):
self.textFormat = self.currentCharFormat()
def startEditing(self) -> None:
if self.allowEditing:
if self.isReadOnly():
# Reset char format to plain text.
self.setCurrentCharFormat(self.textFormat)
self.setPlainText(self.contents)
self.setReadOnly(False)
if self.allowEditing and self.isReadOnly():
# Reset char format to plain text.
self.setCurrentCharFormat(self.textFormat)
self.setPlainText(self.contents)
self.setReadOnly(False)
def stopEditing(self) -> None:
if not self.isReadOnly():
@@ -573,15 +610,13 @@ class RichNotesEditor(QtWidgets.QTextBrowser):
def mousePressEvent(self, ev: QtGui.QMouseEvent) -> None:
potentialLink = self.anchorAt(ev.pos())
if not potentialLink:
if ev.button() == QtGui.Qt.LeftButton:
self.startEditing()
if not potentialLink and ev.button() == QtGui.Qt.MouseButton.LeftButton:
self.startEditing()
super(RichNotesEditor, self).mousePressEvent(ev)
def focusOutEvent(self, ev: QtGui.QFocusEvent) -> None:
if not self.underMouse():
if self.isActiveWindow():
self.stopEditing()
if not self.underMouse() and self.isActiveWindow():
self.stopEditing()
super(RichNotesEditor, self).focusOutEvent(ev)
def doSetSource(self, name: Union[QUrl, str], resourceType: QtGui.QTextDocument.ResourceType = ...) -> None:

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,23 +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/Internal/Macros", "")
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")
@@ -47,29 +92,59 @@ 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):
tempSavePath = actualSavePath + '.tmp'
projectFile = open(tempSavePath, "wb")
dump(self, projectFile)
projectFile.close()
tempSavePath = f'{actualSavePath}.tmp'
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
@@ -31,11 +30,7 @@ class URLManager:
Takes a list of QUrls and returns a list of entities that correspond
to them.
"""
returnValue = []
for url in urls:
returnValue.append(self.handleURL(url))
return returnValue
return [self.handleURL(url) for url in urls]
def handleURL(self, url):
parsedURL = urlparse(url.toString())
@@ -59,20 +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"}
pass
elif fileType == "image":
entityJson = {"Image Name": urlName, "File Path": savePathString, "Entity Type": "Image"}
pass
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()
@@ -85,12 +85,12 @@ class URLManager:
savePath = valuePath.relative_to(projectFilesPath)
except ValueError:
# The file selected is not in Project Files
createSymlink = True if self.mainWindow.SETTINGS.value("Project/Symlink or Copy Materials") == "Symlink" \
else False
createSymlink = self.mainWindow.SETTINGS.value("Project/Symlink or Copy Materials") == "Symlink"
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 / (saveHash + '|' + urlPath.name)
saveHash = sha3_512(str(urlPath).encode()).hexdigest()[:16] # nosec
savePath = projectFilesPath / f'{saveHash}_{urlPath.name}'
if createSymlink:
symlink(urlPath, savePath)
@@ -103,8 +103,6 @@ class URLManager:
def handleRemoteURL(self, url):
stringURL = url.toString()
if self.mainWindow.RESOURCEHANDLER.runCheckOnAttribute(stringURL, 'Onion'):
entity = {'Entity Type': 'Onion Website', 'Onion URL': stringURL}
else:
entity = {'Entity Type': 'Website', 'URL': stringURL}
return entity
return {'Entity Type': 'Onion Website', 'Onion URL': stringURL} \
if self.mainWindow.RESOURCEHANDLER.runCheckOnAttribute(stringURL, 'Onion') \
else {'Entity Type': 'Website', 'URL': stringURL}

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

@@ -1,17 +1,5 @@
#!/usr/bin/env python3
# Requirements:
# requests
# PySide6
# py7zr
#
# Compile with:
# Linux:
# pyinstaller --clean --noconsole --noconfirm --onefile --icon='../Icon.ico' Installer.py
# Windows:
# pyinstaller --clean --noconsole --noconfirm --onefile --icon='..\Icon.ico' Installer.py
import shutil
import sys
import ctypes
@@ -19,11 +7,37 @@ import os
import platform
import subprocess
import tempfile
import stat
from pathlib import Path
import requests
import py7zr
from PySide6 import QtCore, QtWidgets
from PySide6 import QtCore, QtWidgets, QtGui
# Requirements:
# requests PySide6 py7zr wheel pip nuitka ordered-set zstandard
#
# 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 --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
"""
# Linux:
"""
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 --warn-unusual-code --show-modules \
--include-data-files="Icon.ico=Icon.ico" --linux-icon="Icon.ico" \
Installer.py
"""
LINUX_DESKTOP_FILE_ENTRY = """[Desktop Entry]
Name=LinkScope Client
@@ -702,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:
@@ -712,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:
@@ -732,34 +848,32 @@ class InstallWizard(QtWidgets.QWizard):
if self.currentId() == 3:
return 6
if self.currentId() == 4:
if self.currentOS == 'Windows' and not self.graphvizExists:
return 2
else:
return 5
return 2 if self.currentOS == 'Windows' and not self.graphvizExists else 5
if self.currentId() == 5:
return 6
if self.currentId() == 6:
return -1
# If we lose the user somehow, return them to the last page.
return 6
return -1 if self.currentId() == 6 else 6
def __init__(self):
super(InstallWizard, self).__init__()
self.currentOS = platform.system()
appIcon = QtGui.QIcon('Icon.ico')
self.setWindowIcon(appIcon)
self.trayIcon = QtWidgets.QSystemTrayIcon(appIcon, self)
# Whether the icon is shown or not depends on the Desktop environment.
self.trayIcon.show()
if len(sys.argv) < 5:
releasesPage = requests.get('https://github.com/AccentuSoft/LinkScope_Client/releases/latest')
releasesParts = releasesPage.text.split('\n')
downloadURLBase = "https://github.com/AccentuSoft/LinkScope_Client/releases/latest/download/"
if self.currentOS == 'Windows':
try:
if ctypes.windll.shell32.IsUserAnAdmin() == 0:
raise ValueError('Not an admin')
else:
QtWidgets.QMessageBox.critical(self, 'Elevated Privileges Detected',
'The Installer must be ran as a normal user, not as an '
'Administrator. Please run the Installer normally.')
sys.exit(-2)
QtWidgets.QMessageBox.critical(self, 'Elevated Privileges Detected',
'The Installer must be ran as a normal user, not as an '
'Administrator. Please run the Installer normally.')
sys.exit(-2)
except Exception:
self.desktopShortcutPath = Path.home() / 'Desktop' / 'LinkScope.lnk'
@@ -767,15 +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'
for textPart in releasesParts:
if 'Windows10-x64.7z' in textPart:
urlPart = textPart.split('"')[1].strip()
self.downloadURL = 'https://github.com' + urlPart
break
self.downloadURL = f"{downloadURLBase}LinkScope-Windows-x64.7z"
newArgs = ['"' + str(self.desktopShortcutPath) + '"', str(self.graphvizExists),
'"' + str(self.baseSoftwarePath) + '"', '"' + str(self.executablePath) + '"',
'"' + str(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':
@@ -789,41 +903,34 @@ class InstallWizard(QtWidgets.QWizard):
# No harm in re-installing graphviz if it exists.
self.graphvizExists = False
self.baseSoftwarePath = Path(os.path.abspath(os.sep)) / 'usr' / 'local' / 'sbin' / 'LinkScope'
self.appPath = Path(os.path.abspath(os.sep)) / 'usr' / 'share' / 'applications' / 'LinkScope.desktop'
self.appPath = Path(
os.path.abspath(os.sep)) / 'usr' / 'share' / 'applications' / 'LinkScope.desktop'
self.executablePath = self.baseSoftwarePath / 'LinkScope'
for textPart in releasesParts:
if 'Ubuntu-x64.7z' in textPart:
urlPart = textPart.split('"')[1].strip()
self.downloadURL = 'https://github.com' + urlPart
break
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),
str(self.executablePath), str(self.downloadURL), str(self.appPath)]
str(self.executablePath), self.downloadURL, str(self.appPath)]
shortcutExistsBefore = self.desktopShortcutPath.exists()
for _ in range(3):
sudoPassword = QtWidgets.QInputDialog.getText(None, 'Sudo Password',
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,
stderr=subprocess.PIPE)
stdOut, stdErr = sudoPrivs.communicate(input=(sudoPassword[0] + "\n").encode())
if b'\nsudo: 1 incorrect password attempt\n' in stdErr:
QtWidgets.QMessageBox.warning(None, 'Incorrect Password', 'Incorrect password entered.')
QtWidgets.QMessageBox.warning(self, 'Incorrect Password', 'Incorrect password entered.')
continue
if not shortcutExistsBefore and self.desktopShortcutPath.exists():
subprocess.run(
['dbus-launch', 'gio', 'set', str(self.desktopShortcutPath), "metadata::trusted",
'true'])
subprocess.run(
["dbus-send --type=method_call --dest=org.gnome.Shell /org/gnome/Shell "
"org.gnome.Shell.Eval string:'global.reexec_self()'"],
shell=True)
sys.exit(0)
else:
sys.exit(-1)
@@ -834,7 +941,7 @@ class InstallWizard(QtWidgets.QWizard):
sys.exit(-5)
else:
self.desktopShortcutPath = Path(sys.argv[1])
self.graphvizExists = True if sys.argv[2] == 'True' else False
self.graphvizExists = sys.argv[2] == 'True'
self.baseSoftwarePath = Path(sys.argv[3])
self.executablePath = Path(sys.argv[4])
self.downloadURL = sys.argv[5]
@@ -860,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
@@ -873,45 +980,33 @@ class InstallWizard(QtWidgets.QWizard):
uninstallPage.setCommitPage(True)
installUpgradePage = LinkScopeInstallLatestPage()
installUpgradePage.setCommitPage(True)
licensePage = LicensePage()
licensePage.setCommitPage(True)
self.addPage(introPage)
self.addPage(shortcutPage)
self.addPage(WindowsGraphVizPage())
self.addPage(uninstallPage)
self.addPage(LicensePage())
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 pathToRemove.exists():
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 == "":
@@ -929,61 +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 = ""
for chunk in graphVizParts:
if '(64-bit) EXE installer' in chunk:
graphVizDownloadLink = chunk.split('"')[1]
break
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':
# 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)
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)
elif self.currentOS == 'Windows':
# 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?
pass
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):
@@ -995,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'
@@ -1013,12 +1082,11 @@ class IntroInstallUninstallPage(QtWidgets.QWizardPage):
'on them.')
installUninstallLayout.addWidget(self.installRadio)
installUninstallLayout.addWidget(self.updateRadio)
installUninstallLayout.addWidget(self.uninstallRadio)
class WindowsGraphVizPage(QtWidgets.QWizardPage):
def __init__(self):
super(WindowsGraphVizPage, self).__init__()
self.setTitle('Graphviz')
@@ -1029,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)
@@ -1037,34 +1105,28 @@ class WindowsGraphVizPage(QtWidgets.QWizardPage):
class LinkScopeInstallLatestPage(QtWidgets.QWizardPage):
def doStuff(self):
# Graphviz is installed by default as we install the software on linux.
try:
self.installProgressWidget.setEnabled(True)
self.installProgressWidget.setDisabled(False)
if not self.wizard().graphvizExists and self.wizard().currentOS == 'Windows':
self.progressBar.setValue(1)
self.wizard().downloadGraphviz()
self.progressBar.setValue(2)
self.wizard().install()
self.downloadingLabel.setVisible(True)
self.downloadingLabel.setHidden(False)
self.progressBar.setValue(4)
self.wizard().downloadClient()
self.progressBar.setValue(8)
shortcutExists = self.wizard().desktopShortcutPath.exists()
self.progressBar.setValue(9)
if self.createShortcut or (self.updateSelected and shortcutExists):
self.wizard().createShortcut()
self.progressBar.setValue(10)
except Exception as e:
self.wizard().page(6).doneLabel.setText('Error occurred during installation: ' +
str(e) + '\nThe installation cannot continue.')
self.progressBar.setValue(10)
self.processStarted = True
self.installProgressWidget.setEnabled(True)
self.installProgressWidget.setDisabled(False)
self.installThread = InstallThread(self, self.wizard())
self.installThread.progressSignal.connect(self.progressBar.setValue)
self.installThread.doneSignal.connect(self.installationFinished)
self.installThread.start()
def installationFinished(self, success: bool):
# No real need to do anything here, since we update the final page if something goes wrong.
self.downloadingLabel.setVisible(False)
if success:
self.installLabel.setText('Installation complete, click "Commit" to proceed.')
else:
self.installLabel.setText('Installation failed, click "Commit" to proceed.')
def validatePage(self) -> bool:
if self.progressBar.value() != 10:
if self.progressBar.value() == 10:
return True
if not self.processStarted:
self.doStuff()
return True
return False
def __init__(self):
super(LinkScopeInstallLatestPage, self).__init__()
@@ -1078,10 +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()
@@ -1096,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)
@@ -1108,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: ' +
@@ -1134,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()
@@ -1162,8 +1226,9 @@ class CreateDesktopShortcutPage(QtWidgets.QWizardPage):
self.wizard().page(5).createShortcut = True
if self.wizard().currentOS == 'Linux':
self.wizard().page(6).doneLabel.setText('Thank you for using LinkScope!\nClick "Finish" to exit the '
'installer.\nNOTE: On some Desktops, you may see the display '
'refresh. This is done to "activate" the desktop shortcut.')
'installer.\nNOTE: On some Desktops, you may need to manually '
'mark the Desktop shortcut as executable. You can do that by '
'right-clicking it and selecting "Allow Launching".')
else:
self.wizard().page(5).installLabel.setText('The installer will now download and install the latest version '
'of LinkScope. Click "Commit" to start the installation.')
@@ -1181,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)
@@ -1200,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)
@@ -1213,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)
@@ -1223,7 +1286,7 @@ class LicensePage(QtWidgets.QWizardPage):
class DonePage(QtWidgets.QWizardPage):
def __init__(self):
super(DonePage, self).__init__()
self.setTitle('Done')
@@ -1233,11 +1296,43 @@ 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)
class InstallThread(QtCore.QThread):
progressSignal = QtCore.Signal(int)
doneSignal = QtCore.Signal(bool)
def __init__(self, installPage, wizard) -> None:
super().__init__()
self.installPage = installPage
self.wizard = wizard
def run(self) -> None:
try:
self.progressSignal.emit(2)
self.wizard.install()
self.installPage.downloadingLabel.setVisible(True)
self.installPage.downloadingLabel.setHidden(False)
self.progressSignal.emit(4)
self.wizard.downloadClient()
self.progressSignal.emit(8)
self.progressSignal.emit(9)
if self.installPage.createShortcut:
self.wizard.createShortcut()
self.progressSignal.emit(10)
self.doneSignal.emit(True)
except Exception as e:
self.wizard.page(6).doneLabel.setText('Error occurred during installation: ' +
str(e) + '\nThe installation cannot continue.')
self.progressSignal.emit(10)
self.doneSignal.emit(False)
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,2 +0,0 @@
requests
pandas

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,2 +0,0 @@
requests
pycountry

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"
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': {'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': {'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': {'description': 'Enter your api key under your profile after'
' signing up on https://c99.nl.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Censys': {'description': 'Enter your api key under your profile after'
' signing up on https://censys.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Chaos': {'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': {'description': 'Enter your api key under your profile after'
' signing up on https://cloudflare.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'DNSDB': {'description': 'Enter your api key under your profile after'
' signing up on https://dnsdb.info.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'GitHub': {'description': 'Enter your api key under your profile after'
' signing up on https://github.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Hunter': {'description': 'Enter your api key under your profile after'
' signing up on https://hunter.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'IPinfo': {'description': 'Enter your api key under your profile after'
' signing up on https://ipinfo.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'NetworksDB': {'description': 'Enter your api key under your profile after'
' signing up on https://networksdb.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'PassiveTotal': {'description': 'Enter your api key under your profile after'
' signing up on https://passivetotal.com .',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'ReconDev': {'description': 'Enter your api key under your profile after'
' signing up on https://recon.dev.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'SecurityTrails': {'description': 'Enter your api key under your profile after'
' signing up on https://securitytrails.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Shodan': {'description': 'Enter your api key under your profile after'
' signing up on https://shodan.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Spyse': {'description': 'Enter your api key under your profile after'
' signing up on https://spyse.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'ThreatBook': {'description': 'Enter your api key under your profile after'
' signing up on https://threatbook.cn.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Umbrella': {'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': {'description': 'Enter your api key under your profile after'
' signing up on https://urlscan.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'WhoisXMLAPI': {'description': 'Enter your api key under your profile after'
' signing up on https://whoisxmlapi.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'ZETAlytics': {'description': 'Enter your api key under your profile after'
' signing up on https://zetalytics.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'ZoomEye': {'description': 'Please Enter the Username and password with a space '
'in between',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'FacebookCT': {'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': {'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': {
'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': {
'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,231 +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"
originTypes = {'Domain', 'IP Address', 'Autonomous System'}
resultTypes = {'Domain'}
parameters = {'VirusTotal': {'description': 'Enter your api key under your profile after'
' signing up on https://virustotal.com.',
'type': 'String',
'value': 'None'},
'AlienVault': {'description': 'Enter your api key under your profile after'
' signing up on https://otx.alienvault.com.',
'type': 'String',
'value': 'None'},
'BinaryEdge': {'description': 'Enter your api key under your profile after'
' signing up on https://app.binaryedge.com.',
'type': 'String',
'value': 'None'},
'C99': {'description': 'Enter your api key under your profile after'
' signing up on https://c99.nl.',
'type': 'String',
'value': 'None'},
'Censys': {'description': 'Enter your api key under your profile after'
' signing up on https://censys.io.',
'type': 'String',
'value': 'None'},
'Chaos': {'description': 'Enter your api key under your profile after'
' signing up on https://chaos.projectdiscovery.io.',
'type': 'String',
'value': 'None'},
'Cloudflare': {'description': 'Enter your api key under your profile after'
' signing up on https://cloudflare.com.',
'type': 'String',
'value': 'None'},
'DNSDB': {'description': 'Enter your api key under your profile after'
' signing up on https://dnsdb.info.',
'type': 'String',
'value': 'None'},
'GitHub': {'description': 'Enter your api key under your profile after'
' signing up on https://github.com.',
'type': 'String',
'value': 'None'},
'Hunter': {'description': 'Enter your api key under your profile after'
' signing up on https://hunter.io.',
'type': 'String',
'value': 'None'},
'IPinfo': {'description': 'Enter your api key under your profile after'
' signing up on https://ipinfo.io.',
'type': 'String',
'value': 'None'},
'NetworksDB': {'description': 'Enter your api key under your profile after'
' signing up on https://networksdb.io.',
'type': 'String',
'value': 'None'},
'PassiveTotal': {'description': 'Enter your api key under your profile after'
' signing up on https://passivetotal.com .',
'type': 'String',
'value': 'None'},
'ReconDev': {'description': 'Enter your api key under your profile after'
' signing up on https://recon.dev.',
'type': 'String',
'value': 'None'},
'SecurityTrails': {'description': 'Enter your api key under your profile after'
' signing up on https://securitytrails.com.',
'type': 'String',
'value': 'None'},
'Shodan': {'description': 'Enter your api key under your profile after'
' signing up on https://shodan.io.',
'type': 'String',
'value': 'None'},
'Spyse': {'description': 'Enter your api key under your profile after'
' signing up on https://spyse.com.',
'type': 'String',
'value': 'None'},
'ThreatBook': {'description': 'Enter your api key under your profile after'
' signing up on https://threatbook.cn.',
'type': 'String',
'value': 'None'},
'Umbrella': {'description': 'Enter your api key under your profile after'
' signing up on https://umbrella.cisco.com.',
'type': 'String',
'value': 'None'},
'URLScan': {'description': 'Enter your api key under your profile after'
' signing up on https://urlscan.io.',
'type': 'String',
'value': 'None'},
'WhoisXMLAPI': {'description': 'Enter your api key under your profile after'
' signing up on https://whoisxmlapi.com.',
'type': 'String',
'value': 'None'},
'ZETAlytics': {'description': 'Enter your api key under your profile after'
' signing up on https://zetalytics.com.',
'type': 'String',
'value': 'None'},
'ZoomEye': {'description': 'Please Enter the Username and password with a space '
'in between',
'type': 'String',
'value': 'None'},
'FacebookCT': {'description': 'Please Enter the api key and secret with a space '
'in between. Obtain them at https://developer.facebook.com',
'type': 'String',
'value': 'None'},
'Twitter': {'description': 'Please Enter the api key and secret with a space '
'in between. Obtain them at https://developer.twitter.com',
'type': 'String',
'value': 'None'},
'ReconDev.free': {
'description': 'Please Enter the api key under your"\
"profile after signing up on https://recon.dev',
'type': 'String',
'value': 'None'},
'ReconDev.paid': {
'description': 'Please Enter the api key under your profile"\
"after signing up on https://recon.dev',
'type': 'String',
'value': '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 +0,0 @@
beautifulsoup4

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,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 == 401:
return "The HIBP API Key provided is invalid."
elif statusCode == 429:
sleep(2)
continue
elif statusCode == 503:
return "The HIBP Service is unavailable."
elif 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': ''}}])
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 == 401:
return "The HIBP API Key provided is invalid."
elif statusCode == 429:
sleep(2)
continue
elif statusCode == 503:
return "The HIBP Service is unavailable."
elif 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': ''}}])
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 == 401:
return "The HIBP API Key provided is invalid."
elif statusCode == 429:
sleep(2)
continue
elif statusCode == 503:
return "The HIBP Service is unavailable."
elif statusCode == 200:
pwnedPasswordContent = breachInfoRequest.content.decode('utf-8').split('\r\n')
for password in pwnedPasswordContent:
if hashSuffix in password:
returnResults.append([{'Phrase': 'Password Hash Found ' + password.split(':')[1] +
' times in breach data.',
'Entity Type': 'Phrase'},
{entity['uid']: {'Resolution': 'Pwned Password',
'Notes': ''}}])
break
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 == 401:
return "The HIBP API Key provided is invalid."
elif statusCode == 429:
sleep(2)
continue
elif statusCode == 503:
return "The HIBP Service is unavailable."
elif statusCode == 200:
pwnedPasswordContent = breachInfoRequest.content.decode('utf-8').split('\r\n')
for password in pwnedPasswordContent:
if hashSuffix in password:
returnResults.append([{'Phrase': 'Password Hash Found ' + password.split(':')[1] +
' times in breach data.',
'Entity Type': 'Phrase'},
{entity['uid']: {'Resolution': 'Pwned Password',
'Notes': ''}}])
break
sleep(1.7)
count += 1
return returnResults

View File

@@ -1,60 +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 == 401:
return "The HIBP API Key provided is invalid."
elif statusCode == 429:
sleep(2)
continue
elif statusCode == 503:
return "The HIBP Service is unavailable."
elif 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': 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': ''}}])
sleep(1.7)
count += 1
return returnResults

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 +0,0 @@
requests

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,80 +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):
import re
from bs4 import BeautifulSoup
from playwright.sync_api import sync_playwright, TimeoutError, Error
returnResults = []
index_of_child = []
cikRegex = re.compile(r'CIK=\d{4,10}', re.IGNORECASE)
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:
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')
cikIDs = cikRegex.findall(soup.get_text())
links_with_text = []
for td_element in soup.find_all('td'):
if td_element.text:
try:
text = td_element.text
split = text.split('SIC')[0]
links_with_text.append(split)
except IndexError:
links_with_text.append(td_element.text)
for link in links_with_text:
if search_term.lower() in link.lower():
index_of_child.append(len(returnResults))
returnResults.append([{'Company Name': link,
'Entity Type': 'Company'},
{uid: {'Resolution': 'Edgar Company',
'Notes': ''}}])
for code in cikIDs:
returnResults.append([{'CIK': code.split('=')[1],
'Entity Type': 'Edgar ID'},
{index_of_child[cikIDs.index(code)]: {'Resolution': 'CIK Edgar ID',
'Notes': ''}}])
page.close()
browser.close()
return returnResults

View File

@@ -1,198 +0,0 @@
#!/usr/bin/env python3
class EFDByFromDate:
name = 'Get EFD Reports From Date'
category = "US Senate Financial Info"
description = 'Get EFD reports starting from the date specified by the input entities.'
originTypes = {'Date'}
resultTypes = {'Politically Exposed Person', 'Website'}
parameters = {'Max Results': {'description': 'Please enter the maximum number of results to return. '
'Returns the 5 most recent by default.',
'type': 'String',
'default': '5'},
'To Date': {'description': 'Records will be collected from the Start Date provided by the input '
'entities. NOTE: The Start Date is assumed to be in ISO format.\n'
'An End Date is required to complete the Date constraints. '
'Please input the End Date for the search in the format mm/dd/yyyy',
'type': 'String',
'value': ''},
'Filer Type': {'description': 'Please select the Office you wish to search records for.',
'type': 'MultiChoice',
'value': {'Senator',
'Candidate',
'Former Senator',
}},
'Report Type': {'description': 'Please select the Report Type you want to search for.',
'type': 'MultiChoice',
'value': {'Annual',
'Periodic Transactions',
'Due Date Extension',
'Blind Trusts',
'Other Documents',
}}}
def resolution(self, entityJsonList, parameters):
from datetime import datetime
from playwright.sync_api import sync_playwright, TimeoutError, Error
from bs4 import BeautifulSoup, SoupStrainer, Doctype, Tag
returnResults = []
try:
maxResults = int(parameters['Max Results'])
except ValueError:
return "Invalid integer provided in 'Max Results' parameter."
if maxResults <= 0:
return []
try:
toDate = datetime.strptime(parameters['To Date'], '%m/%d/%Y')
except ValueError:
return "Invalid End Date specified."
url = 'https://efdsearch.senate.gov/search/'
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()
pageResolved = False
for _ in range(5):
try:
page.goto(url, wait_until="networkidle", timeout=10000)
pageResolved = True
break
except TimeoutError:
pass
except Error:
break
if not pageResolved:
return "Could not access EFD Search website."
try:
page.click("text=I understand the prohibitions on obtaining and use of financial disclosure repor")
except TimeoutError:
return "The EFD search website is unresponsive."
except Error:
return "Connection Error."
page.wait_for_timeout(1000)
for entity in entityJsonList:
try:
# Assume ISO format - guessing
date = datetime.fromisoformat(entity['Date'])
except ValueError:
continue
if toDate < date:
continue
date = date.strftime('%m/%d/%Y')
toDate = toDate.strftime('%m/%d/%Y')
uid = entity['uid']
page.wait_for_timeout(1000)
pageResolved = False
for _ in range(3):
try:
page.goto(url, wait_until="networkidle", timeout=10000)
pageResolved = True
break
except TimeoutError:
pass
except Error:
break
if not pageResolved:
continue
try:
page.fill("input[name=\"submitted_end_date\"]", toDate)
page.fill("input[name=\"submitted_start_date\"]", date)
if 'Senator' in parameters['Filer Type']:
page.click("label:has-text(\"Senator\")")
if 'Candidate' in parameters['Filer Type']:
page.click("label:has-text(\"Candidate\")")
if 'Former Senator' in parameters['Filer Type']:
page.click("label:has-text(\"Former Senator\")")
if 'Annual' in parameters['Report Type']:
page.click("text=Annual")
if 'Periodic Transactions' in parameters['Report Type']:
page.click("text=Periodic Transactions")
if 'Due Date Extension' in parameters['Report Type']:
page.click("text=Due Date Extension")
if 'Blind Trusts' in parameters['Report Type']:
page.click("text=Blind Trusts")
if 'Other Documents' in parameters['Report Type']:
page.click("text=Other Documents")
page.click("text=Search Reports")
entriesInfo = page.locator('#filedReports_info')
entriesInfo.wait_for(state='visible')
currentFirstIndex = 1
currentLastIndex = int(entriesInfo.inner_text().split(" ")[3])
lastIndex = int(entriesInfo.inner_text().split(" ")[5])
resultCount = 0
if lastIndex == 0:
continue
# Need to click twice to sort by most recent.
page.click("text=Date Received/Filed")
page.wait_for_timeout(500)
page.click("text=Date Received/Filed")
page.wait_for_timeout(500)
while True:
soup = BeautifulSoup(page.content(), 'lxml', parse_only=SoupStrainer('tr'))
for record in soup:
if isinstance(record, Tag) and record.get('class'):
recordFields = record.childGenerator()
senateName = next(recordFields).text
senateName += " " + next(recordFields).text
office = next(recordFields).text
report = next(recordFields)
reportType = report.text
reportLink = next(report.children).get('href')
dateCreated = datetime.strptime(next(recordFields).text, '%m/%d/%Y').isoformat()
resultCount += 1
childIndex = len(returnResults)
returnResults.append([{'Full Name': senateName,
'Office': office,
'Entity Type': 'Politically Exposed Person'},
{uid: {'Resolution': 'EFD Reports', 'Notes': ''}}])
returnResults.append([{'URL': 'https://efdsearch.senate.gov' + reportLink,
'Report Type': reportType,
'Entity Type': 'Website'},
{childIndex: {'Resolution': 'Filed Disclosure Report',
'Notes': '',
'Date Created': dateCreated}}])
if resultCount == maxResults:
break
# Break if we've read enough records, or we ran out of records on this page.
if resultCount == maxResults or currentLastIndex == lastIndex:
break
# We've read all the available records, so we click next.
page.click("text=Next")
entriesInfo.wait_for(state='visible')
while currentFirstIndex == int(entriesInfo.inner_text().split(" ")[1]):
page.wait_for_timeout(1000)
currentFirstIndex = int(entriesInfo.inner_text().split(" ")[1])
currentLastIndex = int(entriesInfo.inner_text().split(" ")[3])
lastIndex = int(entriesInfo.inner_text().split(" ")[5])
except TimeoutError:
continue
except Exception as e:
return "Resolution '" + self.name + "' encountered an error: " + str(e)
page.close()
browser.close()
return returnResults

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