95 Commits

Author SHA1 Message Date
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
AccentuSoft
7cd1f42267 Merge changes in README 2022-08-29 16:14:15 +03:00
AccentuSoft
c0dd01f8f0 Small change in WayBack link text. 2022-08-29 04:41:45 +03:00
AccentuSoft
9ffc8e05ad Update VirusTotal usage limits & small bux fixes. 2022-08-29 03:47:28 +03:00
AccentuSoft
fe020ba383 Small improvements to Aleph resolutions. 2022-08-29 03:30:40 +03:00
AccentuSoft
3adef55fd2 Refactor offshore leaks resolutions. 2022-08-29 03:29:43 +03:00
AccentuSoft
9c60322d86 Fix link label bug with zooming in and out. 2022-08-29 00:17:03 +03:00
AccentuSoft
b22f6ee770 Fix holehe on compiled builds. 2022-08-29 00:16:35 +03:00
AccentuSoft
f65fd1db90 Hide labels after zooming out a bit. Allow user to tweak this value in Graphics Settings.
Small bug fix for previous commits included.
2022-08-28 15:31:43 +03:00
AccentuSoft
9cb48d28a7 Fix bug with hash ID module in build scripts not being found. 2022-08-28 03:54:02 +03:00
AccentuSoft
26d05f4317 Remove unused resolution parameter check. 2022-08-28 03:35:52 +03:00
AccentuSoft
bd9cd3d0a4 Sort entity list & resolution list. 2022-08-28 01:49:34 +03:00
AccentuSoft
1e6ddae957 Don't show resolutions that were stopped in the Running Resolutions menu. 2022-08-28 00:59:51 +03:00
AccentuSoft
dfbd04b7a6 Update buildWin.bat 2022-08-28 00:46:02 +03:00
AccentuSoft
e35100fb7d Terrible solution to ThreadPoolExecutor threads not stopping when QApplication quits. 2022-08-28 00:43:03 +03:00
AccentuSoft
d01e810b7a Update Social Analyzer Detector 2022-08-27 22:56:39 +03:00
AccentuSoft
69123ad662 Update Whats_My_Name.py to the new format, and add new sites to check. 2022-08-26 21:09:57 +03:00
AccentuSoft
361c5f4b96 Update README.md with learning resources. 2022-08-17 20:32:50 +03:00
AccentuSoft
e101ed60bb Add PayPal to js web accounts check. 2022-08-17 00:16:59 +03:00
AccentuSoft
5b2212952e Use proper syntax when copying graphs. 2022-08-13 15:53:02 +03:00
AccentuSoft
6dde75baa1 Merge branch '1.4-Macros' into main 2022-08-09 06:19:24 +03:00
AccentuSoft
62e65aa2a3 Merge branch 'resolutions' into main 2022-08-09 06:18:02 +03:00
AccentuSoft
6f820c84ae Small fix on Shodan resolutions - 'globals' parameter attribute should be 'global'.
Make bs4 use lxml to parse html in GetWebsiteText.py

Create PinterestUsersSearch.py
Create DoTheyExist.py
2022-08-09 06:11:28 +03:00
AccentuSoft
20a0f1d9f8 Fix playwright bug in build scripts & fix bug with file dragging. 2022-08-03 16:34:32 +03:00
AccentuSoft
58fa19fdfd Continue working on macros. 2022-07-31 23:54:14 +03:00
AccentuSoft
312c6a87c6 Update Whats_My_Name.py resolution - optimize reading of json file & add new sites. 2022-07-31 11:00:56 +03:00
155 changed files with 12651 additions and 8897 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
@@ -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)
@@ -165,7 +166,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 +185,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 +213,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 +242,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:
@@ -299,15 +293,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 +324,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 +361,12 @@ 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
message = {'Operation': 'Run Resolution',
'Arguments': {
'resolution_name': resolution_name,
@@ -392,9 +379,11 @@ 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)
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 +501,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,
@@ -573,30 +558,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.currentThread()
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 +588,6 @@ class CommunicationsHandler(QtCore.QObject):
'file_name': file_name
}}
self.transmitMessage(messageJson)
except KeyError:
pass
def scanInbox(self) -> None:
"""
@@ -619,15 +600,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 +647,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 +666,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 +733,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 +786,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 +799,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 +808,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,7 +1,6 @@
#!/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
@@ -36,11 +35,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 +72,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 +93,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()
@@ -178,7 +174,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 +200,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 +242,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 +298,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
@@ -309,6 +305,8 @@ class ResolutionList(QtWidgets.QTreeWidget):
self.setHeaderLabels(['Resolutions'])
self.setAlternatingRowColors(False)
self.setMinimumWidth(200)
self.setSortingEnabled(True)
self.sortByColumn(0, QtCore.Qt.SortOrder.AscendingOrder)
self.loadAllResolutions()
@@ -350,7 +348,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):
@@ -366,11 +364,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.SortOrder.AscendingOrder)
self.allEntities = []
self.loadEntities()
@@ -394,8 +393,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:
@@ -415,9 +413,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.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,7 +128,7 @@ 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("")
@@ -167,13 +164,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 +242,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 +273,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)
@@ -307,7 +301,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)
@@ -339,7 +335,7 @@ class EntityDetails(QtWidgets.QWidget):
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)
@@ -349,7 +345,7 @@ class EntityDetails(QtWidgets.QWidget):
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']))
@@ -418,10 +414,10 @@ class SingleLinkItem(QtWidgets.QWidget):
self.linkItemPic = QtWidgets.QLabel()
self.linkItemPic.setAlignment(QtCore.Qt.AlignCenter)
self.linkItemPic.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.linkItemName = QtWidgets.QLabel()
self.linkItemName.setAlignment(QtCore.Qt.AlignCenter)
self.linkItemName.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.linkItemUid = ""
self.setMaximumHeight(90)
@@ -444,7 +440,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 +472,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 +539,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 +549,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
@@ -19,10 +20,7 @@ class BaseNode(QGraphicsItemGroup):
brush: QtGui.QBrush) -> None:
super(BaseNode, self).__init__()
self.setCacheMode(self.DeviceCoordinateCache)
self.pixmapItem = QtGui.QPixmap()
self.pixmapItem.loadFromData(pictureByteArray)
self.setCacheMode(QGraphicsItemGroup.CacheMode.DeviceCoordinateCache)
if pictureByteArray.data().startswith(b'<svg '):
self.iconItem = QGraphicsSvgItem()
@@ -31,20 +29,26 @@ class BaseNode(QGraphicsItemGroup):
# https://stackoverflow.com/a/68182093
self.iconItem.setElementId("")
else:
self.iconItem = QGraphicsPixmapItem(self.pixmapItem)
pixmapItem = QtGui.QPixmap()
pixmapItem.loadFromData(pictureByteArray)
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 +56,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 +69,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,7 +129,16 @@ 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:
# 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():
centerPoint = QtCore.QPointF(self.iconItem.x() + 20, self.iconItem.y() + 20)
selectionBackgroundGradient = QtGui.QRadialGradient(centerPoint, 80, centerPoint)
@@ -142,7 +162,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:
@@ -182,7 +202,7 @@ 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)
def addItemToGroup(self, uid: str) -> None:
self.groupedNodesUid.add(uid)
@@ -231,10 +251,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)}
@@ -246,23 +263,19 @@ class BaseConnector(QGraphicsItemGroup):
self.myStartItem.addConnector(self)
self.myEndItem.addConnector(self)
# Set as the wrong positions to force drawing.
self.oldStartPos = QtCore.QPointF(self.myStartItem.pos().x() + 1, 0)
self.oldEndPos = QtCore.QPointF(self.myEndItem.pos().x() + 1, 0)
self.colorSelected = QtGui.QColor(0, 173, 238)
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()
@@ -310,30 +323,28 @@ class BaseConnector(QGraphicsItemGroup):
self.myColor = self.colorSelected if self.isSelected() else self.colorDefault
if currentEndPos == self.oldEndPos and currentStartPos == self.oldStartPos:
myPen = QtGui.QPen(self.myColor)
painter.setPen(myPen)
painter.setBrush(self.myColor)
painter.drawLine(self.line)
painter.drawPolygon(self.arrowHead)
return
self.oldStartPos = currentStartPos
self.oldEndPos = currentEndPos
p1 = QtCore.QPointF(self.myStartItem.pos().x() + 20, self.myStartItem.pos().y() + 20)
p2 = QtCore.QPointF(self.myEndItem.pos().x() + 20, self.myEndItem.pos().y() + 20)
p1 = QtCore.QPointF(currentStartPos.x() + 20, currentStartPos.y() + 20)
p2 = QtCore.QPointF(currentEndPos.x() + 20, currentEndPos.y() + 20)
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:
self.labelItem.hide()
if (line.length() < 50 + len(self.labelItem.text()) * 15) or \
self.scene().views()[0].zoom < self.scene().hideZoom:
if self.isSelected():
self.labelItem.hide()
self.setSelected(True)
else:
self.labelItem.hide()
else:
self.labelItem.show()
angle2 = math.degrees(math.pi - angle)
@@ -385,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()
@@ -400,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
@@ -419,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,

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env python3
import contextlib
from typing import Union, Optional, Any
from uuid import uuid4
import re
@@ -27,10 +28,9 @@ class LQLQueryBuilder:
self.mainWindow = mainWindow
def takeSnapshot(self):
self.mainWindow.LENTDB.dbLock.acquire()
# Create a copy
self.databaseSnapshot = nx.DiGraph(self.mainWindow.LENTDB.database)
self.mainWindow.LENTDB.dbLock.release()
with self.mainWindow.LENTDB.dbLock:
# Create a copy
self.databaseSnapshot = self.mainWindow.LENTDB.database.copy()
self.databaseEntities = set(self.databaseSnapshot.nodes)
@@ -82,16 +82,14 @@ class LQLQueryBuilder:
def parseSelect(self, selectClause: str, selectValue: Union[str, list]):
if selectClause == 'SELECT':
if '*' in selectValue:
# No need to remove the '*'. Could cause errors if that's a field name (even though it is bad practice).
return self.allEntityFields
return set([entityField for entityField in selectValue if entityField in self.allEntityFields])
else:
try:
clauseValue = re.compile(selectValue)
return set([entityField for entityField in self.allEntityFields if clauseValue.match(entityField)])
except re.error:
return set()
return self.allEntityFields if '*' in selectValue else \
{entityField for entityField in selectValue if entityField in self.allEntityFields}
try:
clauseValue = re.compile(selectValue)
return {entityField for entityField in self.allEntityFields if clauseValue.match(entityField)}
except re.error:
return set()
def parseSource(self, sourceClause: str, sourceValues: Union[None, list], fieldsToSelect: set) -> set:
"""
@@ -118,32 +116,24 @@ class LQLQueryBuilder:
except (ValueError, re.error):
continue
# Not the most efficient way of phrasing this, but by far the most compact and legible.
for matchingCanvas in matchingCanvases:
if sourceValue[0] == 'AND':
if sourceValue[2] is True:
resultEntitySet = self.canvasAndNot(resultEntitySet,
self.canvasesEntitiesDict[matchingCanvas])
else:
resultEntitySet = self.canvasAnd(resultEntitySet,
self.canvasesEntitiesDict[matchingCanvas])
resultEntitySet = self.canvasAndNot(resultEntitySet, self.canvasesEntitiesDict[matchingCanvas])\
if sourceValue[2] is True else\
self.canvasAnd(resultEntitySet, self.canvasesEntitiesDict[matchingCanvas])
elif sourceValue[2] is True:
resultEntitySet = self.canvasOrNot(resultEntitySet,
self.canvasesEntitiesDict[matchingCanvas],
self.databaseEntities)
else:
# If this is the first clause, or'ing the empty initial resultEntitySet is what we want.
if sourceValue[2] is True:
resultEntitySet = self.canvasOrNot(resultEntitySet,
self.canvasesEntitiesDict[matchingCanvas],
self.databaseEntities)
else:
resultEntitySet = self.canvasOr(resultEntitySet,
self.canvasesEntitiesDict[matchingCanvas])
resultEntitySet = self.canvasOr(resultEntitySet,
self.canvasesEntitiesDict[matchingCanvas])
# Filter out all entities that do not contain at least one of the selected fields.
for entity in list(resultEntitySet):
validEntity = False
for field in fieldsToSelect:
if field in self.allEntities[entity].keys():
validEntity = True
break
validEntity = any(field in self.allEntities[entity].keys() for field in fieldsToSelect)
if not validEntity:
resultEntitySet.remove(entity)
self.allEntities.pop(entity)
@@ -190,30 +180,26 @@ class LQLQueryBuilder:
attributeRegex = re.compile(userInput1)
except re.error:
continue
for field in self.allEntityFields:
if attributeRegex.match(field):
matchingFields.append(field)
matchingFields.extend(field for field in self.allEntityFields if attributeRegex.match(field))
for matchingField in matchingFields:
entitiesToRemove = []
for entity in self.allEntities:
attributeKeyValue = str(self.allEntities[entity].get(matchingField))
if not self.checkVCHelper(conditionValue[2], isNot, attributeKeyValue, userInput2):
if conditionClause[0] == "AND":
entitiesToRemove.append(entity)
else:
if self.checkVCHelper(conditionValue[2], isNot, attributeKeyValue, userInput2):
uidsToSelect.add(entity)
elif conditionClause[0] == "AND":
entitiesToRemove.append(entity)
for entityToRemove in entitiesToRemove:
uidsToSelect.remove(entityToRemove)
elif conditionClause[1] == "Graph Condition":
entitiesToRemove = []
for entity in self.allEntities:
if not self.checkGCHelper(firstArgument, isNot, [entity] + conditionValue[1:]):
if conditionClause[0] == "AND":
entitiesToRemove.append(entity)
else:
if self.checkGCHelper(firstArgument, isNot, [entity] + conditionValue[1:]):
uidsToSelect.add(entity)
elif conditionClause[0] == "AND":
entitiesToRemove.append(entity)
for entityToRemove in entitiesToRemove:
uidsToSelect.remove(entityToRemove)
@@ -236,169 +222,119 @@ class LQLQueryBuilder:
return canvasSetA.union(allEntitiesSet.difference(canvasSetB))
def checkEQ(self, valueA: str, valueB: str):
if valueA == valueB:
return True
return False
return valueA == valueB
def checkContains(self, valueA: str, valueB: str):
if valueB in valueA:
return True
return False
return valueB in valueA
def checkStartsWith(self, valueA: str, valueB: str):
if valueA.startswith(valueB):
return True
return False
return valueA.startswith(valueB)
def checkEndsWith(self, valueA: str, valueB: str):
if valueA.endswith(valueB):
return True
return False
return valueA.endswith(valueB)
def checkRMatch(self, valueA: str, valueB: str):
try:
with contextlib.suppress(re.error):
valueMatch = re.compile(valueB)
if valueMatch.match(valueA):
return True
except re.error:
pass
return False
def checkVCHelper(self, checkType: str, isNot: bool, valueA: str, valueB: str):
returnVal = False
if checkType == "EQ":
returnVal = self.checkEQ(valueA, valueB)
elif checkType == "CONTAINS":
if checkType == "CONTAINS":
returnVal = self.checkContains(valueA, valueB)
elif checkType == "STARTSWITH":
returnVal = self.checkStartsWith(valueA, valueB)
elif checkType == "ENDSWITH":
returnVal = self.checkEndsWith(valueA, valueB)
elif checkType == "EQ":
returnVal = self.checkEQ(valueA, valueB)
elif checkType == "RMATCH":
returnVal = self.checkRMatch(valueA, valueB)
if isNot:
return not returnVal
return returnVal
elif checkType == "STARTSWITH":
returnVal = self.checkStartsWith(valueA, valueB)
return not returnVal if isNot else returnVal
def checkParentOf(self, valueA: str, valueB: str):
return self.databaseSnapshot.has_successor(valueA, valueB)
def checkAncestorOf(self, valueA: str, valueB: str):
try:
with contextlib.suppress(nx.NetworkXError):
if valueB in nx.descendants(self.databaseSnapshot, valueA):
return True
except nx.NetworkXError:
pass
return False
def checkChildOf(self, valueA: str, valueB: str):
return self.databaseSnapshot.has_predecessor(valueA, valueB)
def checkDescendantOf(self, valueA: str, valueB: str):
try:
with contextlib.suppress(nx.NetworkXError):
if valueB in nx.ancestors(self.databaseSnapshot, valueA):
return True
except nx.NetworkXError:
pass
return False
def checkNumChildren(self, valueA: str, valueB: str, valueC: int):
numChildren = len(list(self.databaseSnapshot.successors(valueA)))
returnValue = False
if valueB == "<":
if numChildren < valueC:
returnValue = True
elif valueB == "<=":
if numChildren <= valueC:
returnValue = True
elif valueB == ">":
if numChildren > valueC:
returnValue = True
elif valueB == ">=":
if numChildren >= valueC:
returnValue = True
elif valueB == "==":
if numChildren == valueC:
returnValue = True
return returnValue
return (valueB == "<" and numChildren < valueC) or \
(valueB == "<=" and numChildren <= valueC) or \
(valueB == ">" and numChildren > valueC) or \
(valueB == ">=" and numChildren >= valueC) or \
(valueB == "==" and numChildren == valueC)
def checkNumParents(self, valueA: str, valueB: str, valueC: int):
numParents = len(list(self.databaseSnapshot.predecessors(valueA)))
returnValue = False
if valueB == "<":
if numParents < valueC:
returnValue = True
elif valueB == "<=":
if numParents <= valueC:
returnValue = True
elif valueB == ">":
if numParents > valueC:
returnValue = True
elif valueB == ">=":
if numParents >= valueC:
returnValue = True
elif valueB == "==":
if numParents == valueC:
returnValue = True
return returnValue
return (valueB == "<" and numParents < valueC) or \
(valueB == "<=" and numParents <= valueC) or \
(valueB == ">" and numParents > valueC) or \
(valueB == ">=" and numParents >= valueC) or \
(valueB == "==" and numParents == valueC)
def checkConnectedTo(self, valueA: str, valueB: str):
try:
with contextlib.suppress(nx.NetworkXError):
if nx.has_path(self.databaseSnapshot, valueA, valueB):
return True
except nx.NetworkXError:
pass
return False
def checkIsolated(self, valueA: str):
try:
with contextlib.suppress(nx.NetworkXError):
if valueA in self.databaseSnapshot.nodes and nx.is_isolate(self.databaseSnapshot, valueA):
return True
except nx.NetworkXError:
pass
return False
def checkIsRoot(self, valueA: str):
try:
with contextlib.suppress(nx.NetworkXError):
if len(self.databaseSnapshot.in_edges(valueA)) == 0:
return True
except nx.NetworkXError:
pass
return False
def checkIsLeaf(self, valueA: str):
try:
with contextlib.suppress(nx.NetworkXError):
if len(self.databaseSnapshot.out_edges(valueA)) == 0:
return True
except nx.NetworkXError:
pass
return False
def checkGCHelper(self, checkType: str, isNot: bool, args: list):
returnVal = False
if checkType == "CHILDOF":
returnVal = self.checkChildOf(*args)
elif checkType == "DESCENDANTOF":
returnVal = self.checkDescendantOf(*args)
elif checkType == "PARENTOF":
returnVal = self.checkParentOf(*args)
elif checkType == "ANCESTOROF":
if checkType == "ANCESTOROF":
returnVal = self.checkAncestorOf(*args)
elif checkType == "NUMCHILDREN":
returnVal = self.checkNumChildren(*args)
elif checkType == "NUMPARENTS":
returnVal = self.checkNumParents(*args)
elif checkType == "CHILDOF":
returnVal = self.checkChildOf(*args)
elif checkType == "CONNECTEDTO":
returnVal = self.checkConnectedTo(*args)
elif checkType == "DESCENDANTOF":
returnVal = self.checkDescendantOf(*args)
elif checkType == "ISLEAF":
returnVal = self.checkIsLeaf(*args)
elif checkType == "ISOLATED":
returnVal = self.checkIsolated(*args)
elif checkType == "ISROOT":
returnVal = self.checkIsRoot(*args)
elif checkType == "ISLEAF":
returnVal = self.checkIsLeaf(*args)
if isNot:
return not returnVal
return returnVal
elif checkType == "NUMCHILDREN":
returnVal = self.checkNumChildren(*args)
elif checkType == "NUMPARENTS":
returnVal = self.checkNumParents(*args)
elif checkType == "PARENTOF":
returnVal = self.checkParentOf(*args)
return not returnVal if isNot else returnVal
def modifyNumify(self, valueA: str):
# Get the first number that shows up.
@@ -457,17 +393,15 @@ class LQLQueryBuilder:
for entity in self.allEntities:
for modifyField in modifyFields:
entityFieldValue = self.allEntities[entity].get(modifyField)
if entityFieldValue is None:
if entityFieldValue is None or modificationType not in ["UPPERCASE", "LOWERCASE", "NUMIFY"]:
newFieldValue = None
elif modificationType == "UPPERCASE":
newFieldValue = self.modifyUpperCase(entityFieldValue)
elif modificationType == "LOWERCASE":
newFieldValue = self.modifyLowerCase(entityFieldValue)
elif modificationType == "NUMIFY":
else:
newFieldValue = self.modifyNumify(entityFieldValue)
numifiedFields.add(modifyField)
else:
newFieldValue = None
if newFieldValue is not None:
modifiedUIDs.add(entity)
self.allEntities[entity][modifyField] = newFieldValue
@@ -476,19 +410,16 @@ class LQLQueryBuilder:
def parseQuery(self, selectClause: str, selectValue: Union[str, list], sourceClause: str,
sourceValues: Union[None, list], conditionClauses: Union[None, list],
modifyQueries: Union[list, None] = None) -> \
Optional[tuple[Optional[tuple[set, Union[set[Any], set[Union[str, Any]]]]],
Optional[tuple[set[Any], set[Any]]]]]:
modifyQueries: Union[list, None] = None) -> Optional[tuple[Optional[tuple[set, Union[set[Any], set[Union[str, Any]]]]],
Optional[tuple[set[Any], set[Any]]]]]:
if self.databaseSnapshot is None:
return None
returnValue = None
modifications = None
fieldsToSelect = self.parseSelect(selectClause, selectValue)
if fieldsToSelect:
entitiesToConsider = self.parseSource(sourceClause, sourceValues, fieldsToSelect)
if entitiesToConsider:
if fieldsToSelect := self.parseSelect(selectClause, selectValue):
if entitiesToConsider := self.parseSource(sourceClause, sourceValues, fieldsToSelect):
if conditionClauses:
entitiesToConsider = self.parseConditions(conditionClauses, entitiesToConsider)
returnValue = (entitiesToConsider, fieldsToSelect)

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)

View File

@@ -10,7 +10,7 @@ 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
@@ -33,7 +33,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 +58,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 +96,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 +165,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,20 +190,20 @@ 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)
@@ -218,20 +220,20 @@ class PDFReport:
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)
@@ -260,7 +262,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 +270,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 +294,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.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 +313,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 +348,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 +421,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,25 +1,31 @@
#!/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
class ResolutionManager:
# Load all resources needed.
def __init__(self, mainWindow, messageHandler):
self.messageHandler = messageHandler
def __init__(self, mainWindow):
self.mainWindow = mainWindow
self.resolutions = {}
# Macro dict item contents: tuple of (resolution, parameter values)
self.macros = {}
def loadResolutionsFromDir(self, directory: Path) -> None:
self.loadResolutionsFromDir(
Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) / "Core" / "Resolutions" / "Core")
def loadResolutionsFromDir(self, directory: Path) -> list:
resolutionsLoaded = []
exceptionsCount = 0
for resolution in listdir(directory):
resolution = str(resolution)
resolutionCategory = "Uncategorized"
try:
if resolution.endswith('.py'):
resolutionName = resolution[:-3]
@@ -35,12 +41,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,
@@ -51,36 +55,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 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:
@@ -119,9 +126,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()
@@ -130,18 +135,55 @@ 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
self.mainWindow.executeRemoteResolution(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
resolutionClass = self.resolutions[category][resolution]['resolution']()
result = resolutionClass.resolution(resolutionEntitiesInput, parameters)
return result
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)

View File

@@ -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

@@ -49,7 +49,7 @@ class AffiliateCodesExtractor:
import re
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,57 +133,55 @@ 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()
@@ -197,7 +195,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

@@ -61,13 +61,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,259 @@
#!/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
import re
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()
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,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

@@ -38,23 +38,19 @@ class EmailExtractor:
from playwright.sync_api import sync_playwright, TimeoutError, Error
from bs4 import BeautifulSoup
import re
import contextlib
from email_validator import validate_email, caching_resolver, EmailNotValidError
returnResults = []
# Numbers less than zero are the same as zero, but we should try to prevent overflows.
try:
maxDepth = max(int(parameters['Max Depth']), 0)
except ValueError:
return "Invalid value provided for Max Webpages to follow."
# 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()
@@ -98,7 +94,7 @@ class EmailExtractor:
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)
@@ -106,24 +102,19 @@ class EmailExtractor:
'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.email not in allEmails:
allEmails.add(valid.email)
returnResults.append([{'Email Address': valid.email,
'Entity Type': 'Email Address'},
{currentUID: {'Resolution': 'Email Address Found',
'Notes': ''}}])
with sync_playwright() as p:
browser = p.chromium.launch()
@@ -139,7 +130,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
@@ -29,41 +30,57 @@ class ExtractPDFMeta:
if not (filePath.exists() and 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:
if metadataKey.startswith('/'):
attrValue = metadataKey[1:]
else:
attrValue = 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]),
'Entity Type': 'Phrase'},
{uid: {'Resolution': metadataKey, 'Notes': ''}}])
# Clean some misshapen strings
value = str(info[metadataKey])
if value.startswith('/'):
value = value[1:]
returnResults.append([{'Phrase': 'Number of Pages: ' + str(number_of_pages),
'Entity Type': 'Phrase'},
returnResults.append([{'Phrase': f'{attrValue}: {value}',
'Entity Type': 'Phrase'},
{uid: {'Resolution': attrValue, 'Notes': ''}}])
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,6 +41,7 @@ class FileExtractor:
'default': '0'}}
def resolution(self, entityJsonList, parameters):
import contextlib
import tldextract
import requests
from hashlib import md5
@@ -54,14 +55,15 @@ class FileExtractor:
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 +89,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 +105,8 @@ class FileExtractor:
fileTypeIdentified = 'Image'
elif link.endswith(archiveTypes):
fileTypeIdentified = 'Archive'
elif link.endswith(spreadsheetTypes):
fileTypeIdentified = 'Spreadsheet'
if fileTypeIdentified:
childIndex = len(returnResults)
@@ -113,10 +117,10 @@ class FileExtractor:
'Notes': ''}}])
docProperName = link.split('/')[-1]
docFileName = hexlify(md5(link.encode()).digest()).decode() + ' | ' + docProperName # nosec
docFileName = f'{hexlify(md5(link.encode()).digest()).decode()} | {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 +130,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 +144,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 +156,10 @@ class FileExtractor:
{uid: {'Resolution': 'File URL',
'Notes': ''}}])
docProperName = link.split('/')[-1]
docFileName = hexlify(md5(link.encode()).digest()).decode() + ' | ' + docProperName # nosec
docFileName = f'{hexlify(md5(link.encode()).digest()).decode()} | {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,9 +174,6 @@ class FileExtractor:
'Entity Type': 'Image'},
{childIndex: {'Resolution': 'Downloaded File',
'Notes': ''}}])
except Exception:
pass
if currentDepth > 0:
newDepth = currentDepth - 1
for newURL in urlsToExplore:
@@ -194,7 +192,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,7 +5,7 @@ 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',
@@ -25,10 +25,10 @@ class FileHasher:
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,7 +39,11 @@ 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
onionRegex = re.compile(r"""^https?://\w{56}\.onion/?(\S(?<!\.))*(\.(\S(?<!\.))*)?$""")
returnResult = []
@@ -48,16 +52,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()
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 +76,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 +86,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 +149,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

@@ -24,23 +24,27 @@ class GetInternalURLs:
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()

View File

@@ -27,12 +27,10 @@ class GetWebsiteText:
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, 'html.parser')
soup = BeautifulSoup(body, 'lxml')
texts = soup.findAll(text=True)
visible_texts = filter(tag_visible, texts)
return u" ".join(t.strip() for t in visible_texts if t.strip() != '')
@@ -59,7 +57,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

@@ -17,7 +17,7 @@ class HostnameToDomain:
uid = entity['uid']
primary_field = entity[list(entity)[1]].strip()
tsd, td, tsu = extract(primary_field)
domain = td + '.' + tsu
domain = f'{td}.{tsu}'
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

@@ -20,6 +20,8 @@ class JSCodeExtractor:
from playwright.sync_api import sync_playwright, Error
from base64 import b64decode
import re
import contextlib
returnResults = []
requestUrlsParsed = set()
@@ -27,7 +29,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,126 +50,127 @@ 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()
@@ -194,16 +197,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,108 @@
#!/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
import re
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()
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

@@ -55,12 +55,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:
@@ -88,7 +87,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

@@ -1,10 +1,13 @@
#!/usr/bin/env python3
import contextlib
import re
from typing import Union
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
@@ -17,8 +20,6 @@ from dateutil import parser
from PySide6.QtCore import QByteArray, QSize, QUrl, Qt
from PySide6 import QtWidgets, QtGui
from Core.Interface import Stylesheets
class ResourceHandler:
@@ -26,81 +27,68 @@ 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()
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 +96,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 +108,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 +121,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 +138,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 +154,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,51 +182,48 @@ 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"
entDir = self.programBaseDirPath / "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"
entDir = self.programBaseDirPath / "Modules"
for module in listdir(entDir):
for entFile in listdir(entDir / module):
if entFile.endswith('.xml'):
@@ -253,8 +241,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 +257,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 +279,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 +292,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 +306,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 +318,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 +337,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 +384,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 +424,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 +443,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 +451,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 +469,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 +477,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 +489,7 @@ class MinSizeStackedLayout(QtWidgets.QStackedLayout):
https://stackoverflow.com/a/34300567
"""
def sizeHint(self) -> QSize:
return self.currentWidget().sizeHint()
@@ -550,12 +516,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 +538,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,22 +22,48 @@ 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.globalSettings = QSettings()
self.globalSettings.setValue("Program/Version", "v1.5.1")
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"))
# 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.setValue("Project/Name", "Untitled")
self.setValue("Project/BaseDir", "")
self.setValue("Project/FilesDir", "")
# For any entity with a Path variable, this dictates whether a copy of the original is made or whether a
# symlink is created. Symlinks require special permissions or developer mode in Windows however.
# symlink is created. Symlinks however require special permissions or developer mode in Windows.
# To ensure that the software works out-of-the-box on all platforms, the default is set to 'Copy'.
self.setValue("Project/Symlink or Copy Materials", "Copy") # Values are 'Copy' or 'Symlink'.
self.setValue("Project/Resolution Result Grouping Threshold", "15")
@@ -46,29 +73,58 @@ 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 = {}
for setting in self.globalSettings.allKeys():
if setting.startswith(settingsGroup):
settingsDict[setting] = self.globalSettings.value(setting)
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 Exception(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

@@ -31,15 +31,11 @@ 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())
if not url.isValid() or not all([parsedURL.scheme, parsedURL.netloc]):
if not url.isValid() or (not all([parsedURL.scheme, parsedURL.netloc]) and parsedURL.scheme != 'file'):
return None
if url.isLocalFile():
return self.handleLocalURL(url)
@@ -59,19 +55,26 @@ 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',
'vnd.openxmlformats-officedocument.spreadsheetml.sheet'):
entityJson = {"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):
elif zipfile.is_zipfile(urlPathString):
entityJson = {"Archive Name": urlName, "File Path": savePathString, "Entity Type": "Archive"}
elif fileTypeSplit1 == "video":
entityJson = {"Video Name": urlName, "File Path": savePathString, "Entity Type": "Video"}
elif fileTypeSplit1 == "image":
entityJson = {"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"}
entityJson = {"Document Name": urlName, "File Path": savePathString, "Entity Type": "Document"}
return entityJson
def moveURLToProjectFilesHelperIfNeeded(self, urlPath: Path):
@@ -85,12 +88,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)
savePath = projectFilesPath / f'{saveHash}|{urlPath.name}'
if createSymlink:
symlink(urlPath, savePath)
@@ -103,8 +106,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}

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
@@ -23,7 +11,34 @@ from pathlib import Path
import requests
import py7zr
from PySide6 import QtCore, QtWidgets
from PySide6 import QtCore, QtWidgets, QtGui
# Requirements:
# requests
# PySide6
# py7zr
#
# Compile with (requires zstandard, benefits from orderedset):
#
# Windows:
"""
python -m nuitka --follow-imports --onefile --noinclude-pytest-mode=nofollow --noinclude-setuptools-mode=nofollow ^
--noinclude-custom-mode=setuptools:error --noinclude-IPython-mode=nofollow --enable-plugin=pyside6 ^
--assume-yes-for-downloads --remove-output --disable-console --warn-unusual-code --show-modules ^
--windows-company-name="AccentuSoft" --windows-product-name="LinkScope Installer" --windows-product-version=1.5.1.0 ^
--include-data-files="Icon.ico=Icon.ico" --linux-icon="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 --disable-console --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
@@ -732,34 +747,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 = f"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 +780,11 @@ 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) + '"']
'"' + self.downloadURL + '"']
ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, " ".join(newArgs), None, 1)
sys.exit(0)
elif self.currentOS == 'Linux':
@@ -789,41 +798,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 +836,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 +862,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,32 +875,35 @@ 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)
if not pathToRemove.exists():
return
pathToRemove.chmod(0o777)
if pathToRemove.is_dir():
for dirpath, dirnames, filenames in os.walk(pathToRemove):
Path(dirpath).chmod(0o777)
filenames.extend(dirnames)
for filename in filenames:
filePath = Path(dirpath) / filename
filePath.chmod(0o777)
shutil.rmtree(pathToRemove)
else:
pathToRemove.unlink(missing_ok=True)
def createShortcut(self):
self.removeFileHelper(self.desktopShortcutPath)
@@ -932,11 +937,9 @@ class InstallWizard(QtWidgets.QWizard):
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
graphVizDownloadLink = next((chunk.split('"')[1] for chunk in graphVizParts
if '(64-bit) EXE installer' in chunk), "")
if not isinstance(graphVizDownloadLink, str) or graphVizDownloadLink == "":
raise ValueError('Cannot install GraphViz: Failed to locate the latest version of the GraphViz installer.')
graphVizInstallerTemp = tempfile.mkstemp()
@@ -952,25 +955,24 @@ class InstallWizard(QtWidgets.QWizard):
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.')
self.removeFileHelper(self.appPath)
with open(self.appPath, 'w') as desktopApplicationFile:
desktopApplicationFile.write(LINUX_DESKTOP_FILE_ENTRY)
# Mark desktop file as executable
self.appPath.chmod(self.appPath.stat().st_mode | 0o111)
elif self.currentOS == 'Windows':
if self.currentOS != 'Linux':
# Assume the user has installed / will install Graphviz.
# We don't actually need to do anything here. Maybe in the future, register application in registry?
pass
return
# Redundant since we always try to update and install, but it's good coding practice.
if not self.graphvizExists:
# No need to check if this succeeds - if there are any issues with installation, we will throw
# an error on the install command.
subprocess.run(['apt', 'update'])
command = subprocess.run(['apt', 'install', 'p7zip-full', 'libopengl0', 'graphviz', 'libmagic1', '-y'])
if command.returncode != 0:
raise ValueError('Installing new packages failed, cannot continue installation.')
self.removeFileHelper(self.appPath)
with open(self.appPath, 'w') as desktopApplicationFile:
desktopApplicationFile.write(LINUX_DESKTOP_FILE_ENTRY)
# Mark desktop file as executable
self.appPath.chmod(self.appPath.stat().st_mode | 0o111)
class IntroInstallUninstallPage(QtWidgets.QWizardPage):
@@ -995,7 +997,7 @@ 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')
@@ -1018,7 +1020,7 @@ class IntroInstallUninstallPage(QtWidgets.QWizardPage):
class WindowsGraphVizPage(QtWidgets.QWizardPage):
def __init__(self):
super(WindowsGraphVizPage, self).__init__()
self.setTitle('Graphviz')
@@ -1029,7 +1031,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 +1039,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__()
@@ -1079,9 +1075,11 @@ class LinkScopeInstallLatestPage(QtWidgets.QWizardPage):
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 +1094,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)
@@ -1134,7 +1132,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 +1160,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 +1180,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 +1199,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)
@@ -1223,7 +1222,7 @@ class LicensePage(QtWidgets.QWizardPage):
class DonePage(QtWidgets.QWizardPage):
def __init__(self):
super(DonePage, self).__init__()
self.setTitle('Done')
@@ -1233,10 +1232,47 @@ 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:
# We have already installed Graphviz on Linux at this point.
if not self.wizard.graphvizExists and self.wizard.currentOS == 'Windows':
self.progressSignal.emit(1)
self.wizard.downloadGraphviz()
self.progressSignal.emit(2)
if self.installPage.updateSelected:
self.wizard.uninstall()
self.wizard.install()
self.installPage.downloadingLabel.setVisible(True)
self.installPage.downloadingLabel.setHidden(False)
self.progressSignal.emit(4)
self.wizard.downloadClient()
self.progressSignal.emit(8)
shortcutExists = self.wizard.desktopShortcutPath.exists()
self.progressSignal.emit(9)
if self.installPage.createShortcut or (self.installPage.updateSelected and shortcutExists):
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)
installWizard = InstallWizard()

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -7,11 +7,10 @@ class Aleph_Entity_Search:
description = "Find information about a given search parameter"
originTypes = {'Phrase', 'Person', 'Politically Exposed Person'}
resultTypes = {'Phrase'}
parameters = {'Number of results': {'description': 'Creating a lot of nodes could slow down the software. Please '
'be mindful of the value you enter.',
'type': 'String',
'value': 'Enter the number of results you want returned',
'default': '1'},
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'
@@ -43,9 +42,13 @@ class Aleph_Entity_Search:
return "Please Accept the Terms for Aleph."
try:
max_results = int(parameters['Number of results'])
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'])

View File

@@ -7,7 +7,7 @@ class GetCollectionsInfo:
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. ',
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'
@@ -38,10 +38,10 @@ class GetCollectionsInfo:
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
try:
linkNumbers = int(parameters['Max Results'])
maxResults = int(parameters['Max Results'])
except ValueError:
return "The value for parameter 'Max Results' is not a valid integer."
if linkNumbers <= 0:
if maxResults <= 0:
return []
with FuturesSession(max_workers=15) as session:
for entity in entityJsonList:
@@ -56,11 +56,7 @@ class GetCollectionsInfo:
except requests.exceptions.ConnectionError:
return "Please check your internet connection"
max_results = int(len(response['results']))
if linkNumbers >= max_results:
collections = response['results']
else:
collections = response['results'][0: linkNumbers]
collections = response['results'][:maxResults]
for collection in collections:
index_of_child = len(returnResults)

View File

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

View File

@@ -4,7 +4,7 @@
class Amass_Domain:
name = "Amass Domain Scan"
category = "Network Infrastructure"
description = "Find information about a particular domain"
description = "Find information about a particular domain. Requires Docker to be installed."
originTypes = {'Domain'}
resultTypes = {'IP Address', 'Phrase', 'Autonomous System', 'Domain', 'IPv6 Address'}
parameters = {'VirusTotal API Key': {'description': 'Enter your api key under your profile after'
@@ -13,158 +13,158 @@ class Amass_Domain:
'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.',
'AlienVault API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://otx.alienvault.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'BinaryEdge API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://app.binaryedge.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'C99 API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://c99.nl.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'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': {
'Censys API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://censys.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Chaos API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://chaos.projectdiscovery.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Cloudflare API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://cloudflare.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'DNSDB API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://dnsdb.info.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'GitHub API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://github.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Hunter API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://hunter.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'IPInfo Access Token': {'description': 'Enter your api key under your profile after'
' signing up on https://ipinfo.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'NetworksDB API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://networksdb.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'PassiveTotal API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://passivetotal.com .',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'ReconDev API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://recon.dev.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'SecurityTrails API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://securitytrails.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Shodan API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://shodan.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Spyse API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://spyse.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'ThreatBook API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://threatbook.cn.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Umbrella API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://umbrella.cisco.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'URLScan API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://urlscan.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'WhoisXMLAPI API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://whoisxmlapi.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'ZETAlytics API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://zetalytics.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'ZoomEye API Key': {'description': 'Please Enter the Username and password with a space '
'in between',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'FacebookCT API Key': {'description': 'Please Enter the api key and secret with a space '
'in between. Obtain them at https://developer.facebook.com',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Twitter API Key': {'description': 'Please Enter the api key and secret with a space '
'in between. Obtain them at https://developer.twitter.com',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'ReconDev.free API Key': {
'description':
'Please Enter the api key under your profile after signing up on https://recon.dev',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'ReconDev.paid': {
'ReconDev.paid API Key': {
'description':
'Please Enter the api key under your profile after signing up on https://recon.dev',
'type': 'String',

View File

@@ -4,119 +4,173 @@
class Amass_Intel:
name = "Amass Intel Scan"
category = "Network Infrastructure"
description = "Find information about a particular domain"
description = "Find information about a particular domain. Requires Docker to be installed."
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.',
parameters = {'VirusTotal API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://virustotal.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'AlienVault API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://otx.alienvault.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'BinaryEdge API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://app.binaryedge.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'C99 API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://c99.nl.',
'type': 'String',
'value': 'None'},
'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',
'value': 'None',
'global': True,
'default': 'None'},
'Censys API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://censys.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Chaos API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://chaos.projectdiscovery.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Cloudflare API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://cloudflare.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'DNSDB API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://dnsdb.info.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'GitHub API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://github.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Hunter API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://hunter.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'IPInfo Access Token': {'description': 'Enter your api key under your profile after'
' signing up on https://ipinfo.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'NetworksDB API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://networksdb.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'PassiveTotal API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://passivetotal.com .',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'ReconDev API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://recon.dev.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'SecurityTrails API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://securitytrails.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Shodan API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://shodan.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Spyse API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://spyse.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'ThreatBook API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://threatbook.cn.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Umbrella API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://umbrella.cisco.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'URLScan API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://urlscan.io.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'WhoisXMLAPI API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://whoisxmlapi.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'ZETAlytics API Key': {'description': 'Enter your api key under your profile after'
' signing up on https://zetalytics.com.',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'ZoomEye API Key': {'description': 'Please Enter the Username and password with a space '
'in between',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'FacebookCT API Key': {'description': 'Please Enter the api key and secret with a space '
'in between. Obtain them at https://developer.facebook.com',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'Twitter API Key': {'description': 'Please Enter the api key and secret with a space '
'in between. Obtain them at https://developer.twitter.com',
'type': 'String',
'value': 'None',
'global': True,
'default': 'None'},
'ReconDev.free API Key': {
'description':
'Please Enter the api key under your profile after signing up on https://recon.dev',
'type': 'String',
'value': 'None'},
'ReconDev.paid': {
'description': 'Please Enter the api key under your profile"\
"after signing up on https://recon.dev',
'value': 'None',
'global': True,
'default': 'None'},
'ReconDev.paid API Key': {
'description':
'Please Enter the api key under your profile after signing up on https://recon.dev',
'type': 'String',
'value': 'None'}}
'value': 'None',
'global': True,
'default': 'None'}}
def resolution(self, entityJsonList, parameters):
from pathlib import Path

View File

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

View File

@@ -0,0 +1 @@
requests

View File

@@ -33,14 +33,7 @@ class HaveIBeenPwnedBreachDomains:
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:
if statusCode == 200:
breachContent = json.loads(breachInfoRequest.content)
for breach in breachContent:
@@ -55,7 +48,7 @@ class HaveIBeenPwnedBreachDomains:
breachIconByteArrayFin = QByteArray()
breachImageBuffer = QBuffer(breachIconByteArrayFin)
breachImageBuffer.open(QIODevice.WriteOnly)
breachImageBuffer.open(QIODevice.OpenModeFlag.WriteOnly)
breachIconImageRotated.save(breachImageBuffer, "PNG")
breachImageBuffer.close()
except Exception:
@@ -80,6 +73,13 @@ class HaveIBeenPwnedBreachDomains:
'Date Created': breach['BreachDate']},
{entity['uid']: {'Resolution': 'Contained in Breach',
'Notes': ''}}])
elif statusCode == 401:
return "The HIBP API Key provided is invalid."
elif statusCode == 429:
sleep(2)
continue
elif statusCode == 503:
return "The HIBP Service is unavailable."
sleep(1.7)
count += 1
return returnResults

View File

@@ -36,14 +36,7 @@ class HaveIBeenPwnedBreaches:
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:
if statusCode == 200:
breachContent = json.loads(breachInfoRequest.content)
for breach in breachContent:
@@ -58,7 +51,7 @@ class HaveIBeenPwnedBreaches:
breachIconByteArrayFin = QByteArray()
breachImageBuffer = QBuffer(breachIconByteArrayFin)
breachImageBuffer.open(QIODevice.WriteOnly)
breachImageBuffer.open(QIODevice.OpenModeFlag.WriteOnly)
breachIconImageRotated.save(breachImageBuffer, "PNG")
breachImageBuffer.close()
except Exception:
@@ -83,6 +76,13 @@ class HaveIBeenPwnedBreaches:
'Date Created': breach['BreachDate']},
{entity['uid']: {'Resolution': 'Contained in Breach',
'Notes': ''}}])
elif statusCode == 401:
return "The HIBP API Key provided is invalid."
elif statusCode == 429:
sleep(2)
continue
elif statusCode == 503:
return "The HIBP Service is unavailable."
sleep(1.7)
count += 1
return returnResults

View File

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

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

@@ -31,14 +31,7 @@ class HaveIBeenPwnedPastes:
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:
if statusCode == 200:
pasteContent = json.loads(pasteInfoRequest.content)
for paste in pasteContent:
@@ -46,7 +39,7 @@ class HaveIBeenPwnedPastes:
pasteSource = paste['Source']
# If Paste Date is None, then default to entity creation date.
returnResults.append([{'Paste Identifier': pasteSource + ' | ' + pasteID,
returnResults.append([{'Paste Identifier': f'{pasteSource} | {pasteID}',
'Paste Title': paste['Title'],
'Paste Source': pasteSource,
'Paste ID': pasteID,
@@ -55,6 +48,14 @@ class HaveIBeenPwnedPastes:
'Date Created': paste['Date']},
{entity['uid']: {'Resolution': 'Contained in Paste',
'Notes': ''}}])
elif statusCode == 401:
return "The HIBP API Key provided is invalid."
elif statusCode == 429:
sleep(2)
continue
elif statusCode == 503:
return "The HIBP Service is unavailable."
sleep(1.7)
count += 1
return returnResults

View File

@@ -0,0 +1 @@
requests

View File

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

View File

@@ -20,7 +20,7 @@ class CompanyInfo:
import time
headers = {
'User-Agent': 'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0',
}
returnResults = []

View File

@@ -17,19 +17,15 @@ class CompanyToCIK:
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'
viewport={'width': 1920, 'height': 1080}
)
page = context.new_page()
for entity in entityJsonList:
@@ -51,30 +47,31 @@ class CompanyToCIK:
continue
soup = BeautifulSoup(page.content(), 'lxml')
cikIDs = cikRegex.findall(soup.get_text())
links_with_text = []
count = 0
temp = None
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)
text = td_element.text
splitText = text.split('SIC')[0]
if count == 0:
temp = [{'CIK': splitText,
'Entity Type': 'Edgar ID'},
{len(returnResults): {'Resolution': 'CIK Edgar ID',
'Notes': ''}}]
elif count == 1:
returnResults.append([{'Company Name': splitText,
'Entity Type': 'Company'},
{uid: {'Resolution': 'Edgar Company',
'Notes': ''}}])
returnResults.append(temp)
elif count == 2:
returnResults.append([{'Phrase': "State: " + splitText,
'Entity Type': 'Phrase'},
{len(returnResults) - 1: {'Resolution': 'Edgar Company State',
'Notes': ''}}])
count = (count + 1) % 3
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

@@ -32,7 +32,7 @@ class FramesLookUp:
import time
headers = {
'User-Agent': 'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0',
}
try:

View File

@@ -23,7 +23,7 @@ class Get10KForms:
import time
headers = {
'User-Agent': 'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0',
}
try:
@@ -61,7 +61,7 @@ class Get10KForms:
for j in range(linkNumbers):
if '10-K' in data['facts'][form][i]['units']['USD'][j]['form']:
value = data['facts'][form][i]['units']['USD'][::-1][j]
returnResults.append([{'Field Name': '10-K: ' + i + ' ' + value['filed'],
returnResults.append([{'Field Name': cik + ' 10-K: ' + i + ' ' + value['filed'],
'Account Number': value['accn'],
'Fiscal Year': value['fy'],
'Fiscal Period': value['fp'],
@@ -80,7 +80,7 @@ class Get10KForms:
for j in range(linkNumbers):
if '10-K' in data['facts'][form][i]['units']['shares'][j]['form']:
value = data['facts'][form][i]['units']['shares'][::-1][j]
returnResults.append([{'Field Name': '10-K: ' + i + ' ' + value['filed'],
returnResults.append([{'Field Name': cik + ' 10-K: ' + i + ' ' + value['filed'],
'Account Number': value['accn'],
'Fiscal Year': value['fy'],
'Fiscal Period': value['fp'],

View File

@@ -23,7 +23,7 @@ class Get10QForms:
import time
headers = {
'User-Agent': 'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0',
}
try:
@@ -61,7 +61,7 @@ class Get10QForms:
for j in range(linkNumbers):
if '10-Q' in data['facts'][form][i]['units']['USD'][j]['form']:
value = data['facts'][form][i]['units']['USD'][::-1][j]
returnResults.append([{'Field Name': '10-Q: ' + i + ' ' + value['filed'],
returnResults.append([{'Field Name': cik + ' 10-Q: ' + i + ' ' + value['filed'],
'Account Number': value['accn'],
'Fiscal Year': value['fy'],
'Fiscal Period': value['fp'],
@@ -79,7 +79,7 @@ class Get10QForms:
for j in range(linkNumbers):
if '10-Q' in data['facts'][form][i]['units']['shares'][j]['form']:
value = data['facts'][form][i]['units']['shares'][::-1][j]
returnResults.append([{'Field Name': '10-Q: ' + i + ' ' + value['filed'],
returnResults.append([{'Field Name': cik + ' 10-Q: ' + i + ' ' + value['filed'],
'Account Number': value['accn'],
'Fiscal Year': value['fy'],
'Fiscal Period': value['fp'],

View File

@@ -27,7 +27,7 @@ class Get13FForms:
from ast import literal_eval
headers = {
'User-Agent': 'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0',
}
name = ''

View File

@@ -23,7 +23,7 @@ class Get20FForms:
import time
headers = {
'User-Agent': 'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0',
}
try:
@@ -60,7 +60,7 @@ class Get20FForms:
for j in range(linkNumbers):
if '20-F' in data['facts'][form][i]['units']['USD'][j]['form']:
value = data['facts'][form][i]['units']['USD'][::-1][j]
returnResults.append([{'Field Name': '20-F: ' + i + ' ' + value['filed'],
returnResults.append([{'Field Name': cik + ' 20-F: ' + i + ' ' + value['filed'],
'Account Number': value['accn'],
'Fiscal Year': value['fy'],
'Fiscal Period': value['fp'],
@@ -79,7 +79,7 @@ class Get20FForms:
for j in range(linkNumbers):
if '20-F' in data['facts'][form][i]['units']['shares'][j]['form']:
value = data['facts'][form][i]['units']['shares'][::-1][j]
returnResults.append([{'Field Name': '20-F: ' + i + ' ' + value['filed'],
returnResults.append([{'Field Name': cik + ' 20-F: ' + i + ' ' + value['filed'],
'Account Number': value['accn'],
'Fiscal Year': value['fy'],
'Fiscal Period': value['fp'],

View File

@@ -27,7 +27,7 @@ class Get3Forms:
from ast import literal_eval
headers = {
'User-Agent': 'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0',
}
try:

View File

@@ -23,7 +23,7 @@ class Get40FForms:
import time
headers = {
'User-Agent': 'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0',
}
try:
@@ -61,7 +61,7 @@ class Get40FForms:
for j in range(linkNumbers):
if '40-F' in data['facts'][form][i]['units']['USD'][j]['form']:
value = data['facts'][form][i]['units']['USD'][::-1][j]
returnResults.append([{'Field Name': '40-F: ' + i + ' ' + value['filed'],
returnResults.append([{'Field Name': cik + ' 40-F: ' + i + ' ' + value['filed'],
'Account Number': value['accn'],
'Fiscal Year': value['fy'],
'Fiscal Period': value['fp'],
@@ -80,7 +80,7 @@ class Get40FForms:
for j in range(linkNumbers):
if '40-F' in data['facts'][form][i]['units']['shares'][j]['form']:
value = data['facts'][form][i]['units']['shares'][::-1][j]
returnResults.append([{'Field Name': '40-F: ' + i + ' ' + value['filed'],
returnResults.append([{'Field Name': cik + ' 40-F: ' + i + ' ' + value['filed'],
'Account Number': value['accn'],
'Fiscal Year': value['fy'],
'Fiscal Period': value['fp'],

View File

@@ -27,7 +27,7 @@ class Get4Forms:
from ast import literal_eval
headers = {
'User-Agent': 'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0',
}
try:

View File

@@ -23,7 +23,7 @@ class Get6KForms:
import time
headers = {
'User-Agent': 'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0',
}
try:
@@ -63,7 +63,7 @@ class Get6KForms:
for j in range(linkNumbers):
if '6-K' in data['facts'][form][i]['units']['USD'][j]['form']:
value = data['facts'][form][i]['units']['USD'][::-1][j]
returnResults.append([{'Field Name': '6-K: ' + i + ' ' + value['filed'],
returnResults.append([{'Field Name': cik + ' 6-K: ' + i + ' ' + value['filed'],
'Account Number': value['accn'],
'Fiscal Year': value['fy'],
'Fiscal Period': value['fp'],
@@ -82,7 +82,7 @@ class Get6KForms:
for j in range(linkNumbers):
if '6-K' in data['facts'][form][i]['units']['shares'][j]['form']:
value = data['facts'][form][i]['units']['shares'][::-1][j]
returnResults.append([{'Field Name': '6-K: ' + i + ' ' + value['filed'],
returnResults.append([{'Field Name': cik + ' 6-K: ' + i + ' ' + value['filed'],
'Account Number': value['accn'],
'Fiscal Year': value['fy'],
'Fiscal Period': value['fp'],

View File

@@ -23,7 +23,7 @@ class Get8KForms:
import time
headers = {
'User-Agent': 'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0',
}
try:
@@ -63,7 +63,7 @@ class Get8KForms:
for j in range(linkNumbers):
if '8-K' in data['facts'][form][i]['units']['USD'][j]['form']:
value = data['facts'][form][i]['units']['USD'][::-1][j]
returnResults.append([{'Field Name': '8-K: ' + i + ' ' + value['filed'],
returnResults.append([{'Field Name': cik + ' 8-K: ' + i + ' ' + value['filed'],
'Account Number': value['accn'],
'Fiscal Year': value['fy'],
'Fiscal Period': value['fp'],
@@ -82,7 +82,7 @@ class Get8KForms:
for j in range(linkNumbers):
if '8-K' in data['facts'][form][i]['units']['shares'][j]['form']:
value = data['facts'][form][i]['units']['shares'][::-1][j]
returnResults.append([{'Field Name': '8-K: ' + i + ' ' + value['filed'],
returnResults.append([{'Field Name': cik + ' 8-K: ' + i + ' ' + value['filed'],
'Account Number': value['accn'],
'Fiscal Year': value['fy'],
'Fiscal Period': value['fp'],

View File

@@ -27,7 +27,7 @@ class GetDForms:
from ast import literal_eval
headers = {
'User-Agent': 'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0',
}
try:

View File

@@ -25,7 +25,7 @@ class GetN8FForms:
from bs4 import BeautifulSoup
headers = {
'User-Agent': 'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0',
}
try:

View File

@@ -139,7 +139,7 @@ class GetNMFP2Forms:
for field in liquidAssets:
if 'Daily' in field:
returnResults.append([{'Field Name': f'N-MFP2:({field})' + ' '
returnResults.append([{'Field Name': cik + f' N-MFP2:({field})' + ' '
+ f'ID: {seriesId} Date: {date}',
'Friday 1': fieldPath[field][
'ns3:fridayDay1'],
@@ -154,7 +154,7 @@ class GetNMFP2Forms:
{uid: {'Resolution': field,
'Notes': ''}}])
else:
returnResults.append([{'Field Name': f'N-MFP2:({field})' + ' '
returnResults.append([{'Field Name': cik + f' N-MFP2:({field})' + ' '
+ f'ID: {seriesId} Date: {date}',
'Friday 1': fieldPath[field][
'ns3:fridayWeek1'],
@@ -177,7 +177,7 @@ class GetNMFP2Forms:
[{'Phrase': classInfo['classesId'],
'Entity Type': 'Phrase'},
{uid: {'Resolution': 'Classes Id', 'Notes': ''}}])
returnResults.append([{'Field Name': f'N-MFP2:(Net Asset Per Share)' + ' '
returnResults.append([{'Field Name': cik + f' N-MFP2:(Net Asset Per Share)' + ' '
+ f'ID: {seriesId} Date: {date}',
'Friday 1': classInfo['netAssetPerShare'][
'ns3:fridayWeek1'],
@@ -211,7 +211,8 @@ class GetNMFP2Forms:
{index_of_child: {'Resolution': value,
'Notes': ''}}])
scheduleOfPortfolioSecurities = data['edgarSubmission']['formData']['scheduleOfPortfolioSecuritiesInfo']
scheduleOfPortfolioSecurities = data['edgarSubmission']['formData'][
'scheduleOfPortfolioSecuritiesInfo']
instance = 0
for securitiesInfo in scheduleOfPortfolioSecurities:
index_of_child = len(returnResults)
@@ -259,7 +260,8 @@ class GetNMFP2Forms:
returnResults.append([{'Name': issuer['nameOfCollateralIssuer'],
'Coupon or Yield': issuer['couponOrYield'],
'Principal Amount': issuer['principalAmountToTheNearestCent'],
'Value of Collateral': issuer['valueOfCollateralToTheNearestCent'],
'Value of Collateral': issuer[
'valueOfCollateralToTheNearestCent'],
'Ctgry Investments Rprsnts Collateral':
issuer['ctgryInvestmentsRprsntsCollateral'],
'Entity Type': 'Collateral Issuer'},

View File

@@ -1,3 +1,5 @@
requests
beautifulsoup4
xmltodict
bs4
xmltodict
playwright
datetime

View File

@@ -8,7 +8,7 @@ class OrgSearch_GitAllSecrets:
category = "Secrets & Leaks"
# A string that describes this resolution.
description = "Returns Nodes of Relationship Info"
description = "Returns Nodes of Relationship Info. Requires Docker to be installed."
originTypes = {'GitHub Organisation'}

View File

@@ -1,2 +1,3 @@
docker
beautifulsoup4
bs4
playwright

View File

@@ -1 +1,2 @@
requests
requests
requests-futures

View File

@@ -7,14 +7,14 @@ class IPInfo:
description = "Find information about the location of a given IP Address"
originTypes = {'IP Address'}
resultTypes = {'Geocordinates', 'Organization', 'City'}
parameters = {'IPInfo Access Token': {'description': 'Enter your access token key under your profile after'
parameters = {'IPInfo Access Token': {'description': 'Enter your access token key under your profile after '
'signing up on https://ipinfo.io. Free usage of the API is '
'limited to 50,000 requests per month. '
'For any requests beyond that limit, no results will be '
'returned.',
'type': 'String',
'value': '',
'global': True}}
'global': True}}
def resolution(self, entityJsonList, parameters):
import requests

View File

@@ -1 +1,2 @@
requests
requests
requests-futures

View File

@@ -43,37 +43,36 @@ class IPQualityScore_Email:
"query. Please contact IPQualityScore " \
" support if this error persists.":
return "Your account doesn't have sufficient credits to complete this operation."
else:
valid = f"valid: {response['valid']}\n"
disposable = f"disposable: {response['disposable']}\n"
smtp_score = f"smtp_score: {response['smtp_score']}\n"
overall_score = f"overall_score: {response['overall_score']}\n"
generic = f"generic: {response['generic']}\n"
common = f"common: {response['common']}\n"
dns_valid = f"dns_valid: {response['dns_valid']}\n"
honeypot = f"honeypot: {response['honeypot']}\n"
deliverability = f"deliverability: {response['deliverability']}\n"
frequent_complainer = f"frequent_complainer: {response['frequent_complainer']}\n"
spam_trap_score = f"spam_trap_score: {response['spam_trap_score']}\n"
catch_all = f"catch_all: {response['catch_all']}\n"
suspect = f"suspect: {response['suspect']}\n"
recent_abuse = f"recent_abuse: {response['recent_abuse']}\n"
fraud_score = f"fraud_score: {response['fraud_score']}\n"
suggested_domain = f"suggested_domain: {response['suggested_domain']}\n"
leaked = f"leaked: {response['leaked']}\n"
return_result.append([{'Phrase': response['request_id'],
'Notes': valid + disposable + smtp_score + overall_score + generic + common +
dns_valid + honeypot + deliverability + frequent_complainer +
spam_trap_score + catch_all + suspect + recent_abuse + fraud_score +
suggested_domain + leaked,
'Entity Type': 'Phrase'},
{uid: {'Resolution': 'IPQualityScore Scan ID', 'Notes': ''}}])
if response['first_name'] != "":
return_result.append([{'Full Name': response['first_name'],
'Entity Type': 'Person'},
{uid: {'Resolution': 'IPQualityScore First Name', 'Notes': ''}}])
if response['sanitized_email'] != primary_field:
return_result.append([{'Email Address': response['sanitized_email'],
'Entity Type': 'Email Address'},
{uid: {'Resolution': 'IPQualityScore Sanitized Email', 'Notes': ''}}])
valid = f"valid: {response['valid']}\n"
disposable = f"disposable: {response['disposable']}\n"
smtp_score = f"smtp_score: {response['smtp_score']}\n"
overall_score = f"overall_score: {response['overall_score']}\n"
generic = f"generic: {response['generic']}\n"
common = f"common: {response['common']}\n"
dns_valid = f"dns_valid: {response['dns_valid']}\n"
honeypot = f"honeypot: {response['honeypot']}\n"
deliverability = f"deliverability: {response['deliverability']}\n"
frequent_complainer = f"frequent_complainer: {response['frequent_complainer']}\n"
spam_trap_score = f"spam_trap_score: {response['spam_trap_score']}\n"
catch_all = f"catch_all: {response['catch_all']}\n"
suspect = f"suspect: {response['suspect']}\n"
recent_abuse = f"recent_abuse: {response['recent_abuse']}\n"
fraud_score = f"fraud_score: {response['fraud_score']}\n"
suggested_domain = f"suggested_domain: {response['suggested_domain']}\n"
leaked = f"leaked: {response['leaked']}\n"
return_result.append([{'Phrase': response['request_id'],
'Notes': valid + disposable + smtp_score + overall_score + generic + common +
dns_valid + honeypot + deliverability + frequent_complainer +
spam_trap_score + catch_all + suspect + recent_abuse + fraud_score +
suggested_domain + leaked,
'Entity Type': 'Phrase'},
{uid: {'Resolution': 'IPQualityScore Scan ID', 'Notes': ''}}])
if response['first_name'] != "":
return_result.append([{'Full Name': response['first_name'],
'Entity Type': 'Person'},
{uid: {'Resolution': 'IPQualityScore First Name', 'Notes': ''}}])
if response['sanitized_email'] != primary_field:
return_result.append([{'Email Address': response['sanitized_email'],
'Entity Type': 'Email Address'},
{uid: {'Resolution': 'IPQualityScore Sanitized Email', 'Notes': ''}}])
return return_result

View File

@@ -44,45 +44,44 @@ class IPQualityScore_IP:
"query. Please contact IPQualityScore " \
" support if this error persists.":
return "Your account doesn't have sufficient credits to complete this operation."
else:
Country_Code = response['country_code']
# Region = response['region']
City = response['city']
# ISP = response['ISP']
ASN = response['ASN']
Organization = response['organization']
latitude = response['latitude']
longitude = response['longitude']
fraud_score = f"fraud_score: {response['fraud_score']}\n"
proxy = f"proxy: {response['proxy']}\n"
vpn = f"vpn: {response['vpn']}\n"
tor = f"tor: {response['tor']}\n"
is_crawler = f"iscrawler {response['is_crawler']}\n"
active_vpn = f"active vpn: {response['active_vpn']}\n"
active_tor = f"active tor: {response['active_tor']}\n"
recent_abuse = f"recent abuse: {response['recent_abuse']}\n"
bot_status = f"bot status: {response['bot_status']}\n"
Country_Code = response['country_code']
# Region = response['region']
City = response['city']
# ISP = response['ISP']
ASN = response['ASN']
Organization = response['organization']
latitude = response['latitude']
longitude = response['longitude']
fraud_score = f"fraud_score: {response['fraud_score']}\n"
proxy = f"proxy: {response['proxy']}\n"
vpn = f"vpn: {response['vpn']}\n"
tor = f"tor: {response['tor']}\n"
is_crawler = f"iscrawler {response['is_crawler']}\n"
active_vpn = f"active vpn: {response['active_vpn']}\n"
active_tor = f"active tor: {response['active_tor']}\n"
recent_abuse = f"recent abuse: {response['recent_abuse']}\n"
bot_status = f"bot status: {response['bot_status']}\n"
return_result.append([{'AS Number': f"AS{str(ASN)}",
'Entity Type': 'Autonomous System'},
{uid: {'Resolution': 'IPQualityScore AS Number', 'Notes': ''}}])
return_result.append([{'Organization Name': Organization,
'Entity Type': 'Organization'},
{uid: {'Resolution': 'IPQualityScore IP Organization', 'Notes': ''}}])
return_result.append([{'Country Name': pycountry.countries.get(alpha_2=Country_Code).name,
'Entity Type': 'Country'},
{uid: {'Resolution': 'IPQualityScore Scan', 'Notes': ''}}])
return_result.append([{'City Name': City,
'Entity Type': 'City'},
{uid: {'Resolution': 'IPQualityScore IP City Name', 'Notes': ''}}])
return_result.append([{'Label': str(primary_field) + " Location",
'Latitude': latitude,
'Longitude': longitude,
'Entity Type': 'GeoCoordinates'},
{uid: {'Resolution': 'IPQualityScore IP Geolocation', 'Notes': ''}}])
return_result.append([{'Phrase': response['request_id'],
'Notes': fraud_score + is_crawler + proxy + vpn + tor + active_vpn + active_tor +
recent_abuse + bot_status,
'Entity Type': 'Phrase'},
{uid: {'Resolution': 'IPQualityScore Scan ID', 'Notes': ''}}])
return_result.append([{'AS Number': f"AS{str(ASN)}",
'Entity Type': 'Autonomous System'},
{uid: {'Resolution': 'IPQualityScore AS Number', 'Notes': ''}}])
return_result.append([{'Organization Name': Organization,
'Entity Type': 'Organization'},
{uid: {'Resolution': 'IPQualityScore IP Organization', 'Notes': ''}}])
return_result.append([{'Country Name': pycountry.countries.get(alpha_2=Country_Code).name,
'Entity Type': 'Country'},
{uid: {'Resolution': 'IPQualityScore Scan', 'Notes': ''}}])
return_result.append([{'City Name': City,
'Entity Type': 'City'},
{uid: {'Resolution': 'IPQualityScore IP City Name', 'Notes': ''}}])
return_result.append([{'Label': f"{primary_field} Location",
'Latitude': latitude,
'Longitude': longitude,
'Entity Type': 'GeoCoordinates'},
{uid: {'Resolution': 'IPQualityScore IP Geolocation', 'Notes': ''}}])
return_result.append([{'Phrase': response['request_id'],
'Notes': fraud_score + is_crawler + proxy + vpn + tor + active_vpn + active_tor +
recent_abuse + bot_status,
'Entity Type': 'Phrase'},
{uid: {'Resolution': 'IPQualityScore Scan ID', 'Notes': ''}}])
return return_result

View File

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

View File

@@ -0,0 +1,71 @@
#!/usr/bin/env python3
class InternetDB:
name = "InternetDB IP lookup"
category = "Network Infrastructure"
description = "Convert the primary field of selected entities to a Phrase entity."
originTypes = {'IP Address'}
resultTypes = {'Domain', 'Phrase', 'Port'}
parameters = {'InternetDB Disclaimer': {'description': 'InternetDB access is free for non-commercial use. '
'If you are using this service for commercial purposes, '
'you need an enterprise license. You can get one at '
'https://enterprise.shodan.io/.\n'
'Type "Accept" (without quotes) to confirm your '
'understanding.',
'type': 'String',
'value': 'Type "Accept" (without quotes) to confirm your understanding.',
'global': True}}
def resolution(self, entityJsonList, parameters):
import requests
if parameters['InternetDB Disclaimer'].strip() != 'Accept':
return []
returnResults = []
for entity in entityJsonList:
primaryField = entity['IP Address']
entityUID = entity['uid']
requestResult = requests.get("https://internetdb.shodan.io/" + primaryField).json()
if "detail" in requestResult:
returnResults.append([{'Phrase': requestResult['detail'],
'Entity Type': 'Phrase'},
{entityUID: {'Resolution': 'InternetDB Lookup Result',
'Notes': ''}}])
elif "msg" in requestResult:
returnResults.append([{'Phrase': requestResult['msg'],
'Entity Type': 'Phrase'},
{entityUID: {'Resolution': 'InternetDB Lookup Result',
'Notes': ''}}])
else:
for cpe in requestResult['cpes']:
returnResults.append([{'Phrase': cpe,
'Entity Type': 'Phrase'},
{entityUID: {'Resolution': 'InternetDB IP CPE',
'Notes': ''}}])
for hostname in requestResult['hostnames']:
returnResults.append([{'Domain Name': hostname,
'Entity Type': 'Domain'},
{entityUID: {'Resolution': 'InternetDB IP Domain',
'Notes': ''}}])
for port in requestResult['ports']:
returnResults.append([{'Port': requestResult['ip'] + ":" + str(port),
'Entity Type': 'Port'},
{entityUID: {'Resolution': 'InternetDB IP Open Port',
'Notes': ''}}])
for tag in requestResult['tags']:
returnResults.append([{'Phrase': tag,
'Entity Type': 'Phrase'},
{entityUID: {'Resolution': 'InternetDB IP Tag',
'Notes': ''}}])
for vuln in requestResult['vulns']:
returnResults.append([{'Phrase': vuln,
'Entity Type': 'Phrase'},
{entityUID: {'Resolution': 'InternetDB IP Vuln',
'Notes': ''}}])
return returnResults

View File

@@ -0,0 +1 @@
requests

View File

@@ -38,8 +38,8 @@ class InterpolRedNotices:
noticeNotes = ""
for warrant in noticeContents['arrest_warrants']:
noticeNotes += 'CHARGE: ' + warrant.get('charge') + '\nFROM COUNTRY: ' + \
warrant.get('issuing_country_id') + '\n\n'
noticeNotes += f"CHARGE: {warrant.get('charge')}\nFROM COUNTRY: " \
f"{warrant.get('issuing_country_id')}\n\n"
try:
thumbnailPictureURL = noticeContents['_links']['thumbnail']['href']
@@ -49,7 +49,7 @@ class InterpolRedNotices:
thumbnailIconImageScaled = thumbnailIconImageOriginal.scaled(QSize(40, 40))
thumbnailByteArrayFin = QByteArray()
thumbnailImageBuffer = QBuffer(thumbnailByteArrayFin)
thumbnailImageBuffer.open(QIODevice.WriteOnly)
thumbnailImageBuffer.open(QIODevice.OpenModeFlag.WriteOnly)
thumbnailIconImageScaled.save(thumbnailImageBuffer, "PNG")
thumbnailImageBuffer.close()
except Exception:
@@ -84,7 +84,7 @@ class InterpolRedNotices:
for entity in entityJsonList:
uid = entity['uid']
entityType = entity['Entity Type']
if entityType == 'Person' or entityType == 'Politically Exposed Person':
if entityType in ['Person', 'Politically Exposed Person']:
primaryField = entity['Full Name'].strip()
elif entityType == 'Phrase':
primaryField = entity['Phrase'].strip()
@@ -95,17 +95,13 @@ class InterpolRedNotices:
# This ensures that we will not miss any matches.
nameFragments = primaryField.split(' ')
firstName = nameFragments[0].upper()
if len(nameFragments) == 1:
lastName = None
else:
lastName = nameFragments[-1].upper()
lastName = None if len(nameFragments) == 1 else nameFragments[-1].upper()
firstRequestURL = firstRequestPart1 + firstName
if lastName is not None:
firstRequestURL += "&name=" + lastName
firstRequestURL += f"&name={lastName}"
firstRequestURL += firstRequestPart2
firstRequest = requests.get(firstRequestURL + "1" + firstRequestPart3)
firstRequest = requests.get(f"{firstRequestURL}1{firstRequestPart3}")
pageContents = firstRequest.json()
lastPage = int(pageContents['_links']['last']['href'].split('&page=')[1].split('&')[0])

View File

@@ -44,7 +44,7 @@ class InterpolYellowNotices:
thumbnailIconImageScaled = thumbnailIconImageOriginal.scaled(QSize(40, 40))
thumbnailByteArrayFin = QByteArray()
thumbnailImageBuffer = QBuffer(thumbnailByteArrayFin)
thumbnailImageBuffer.open(QIODevice.WriteOnly)
thumbnailImageBuffer.open(QIODevice.OpenModeFlag.WriteOnly)
thumbnailIconImageScaled.save(thumbnailImageBuffer, "PNG")
thumbnailImageBuffer.close()
except Exception:
@@ -84,7 +84,7 @@ class InterpolYellowNotices:
for entity in entityJsonList:
uid = entity['uid']
entityType = entity['Entity Type']
if entityType == 'Person' or entityType == 'Politically Exposed Person':
if entityType in ['Person', 'Politically Exposed Person']:
primaryField = entity['Full Name'].strip()
elif entityType == 'Phrase':
primaryField = entity['Phrase'].strip()
@@ -95,17 +95,13 @@ class InterpolYellowNotices:
# This ensures that we will not miss any matches.
nameFragments = primaryField.split(' ')
firstName = nameFragments[0].upper()
if len(nameFragments) == 1:
lastName = None
else:
lastName = nameFragments[-1].upper()
lastName = None if len(nameFragments) == 1 else nameFragments[-1].upper()
firstRequestURL = firstRequestPart1 + firstName
if lastName is not None:
firstRequestURL += "&name=" + lastName
firstRequestURL += f"&name={lastName}"
firstRequestURL += firstRequestPart2
firstRequest = requests.get(firstRequestURL + "1" + firstRequestPart3)
firstRequest = requests.get(f"{firstRequestURL}1{firstRequestPart3}")
pageContents = firstRequest.json()
lastPage = int(pageContents['_links']['last']['href'].split('&page=')[1].split('&')[0])

View File

@@ -0,0 +1 @@
requests

View File

@@ -852,14 +852,11 @@ class Mailcat:
if chkKolab.status == 200:
kolabJSON = chkKolab.json()
if kolabJSON["errors"]["login"] == kolabsuc:
# print("[+] Success with {}@{}".format(target, kolabdomain))
if (
kolabJSON["errors"]["login"] != kolabsuc
and kolabJSON["errors"]
):
pass
else:
if kolabJSON["errors"]:
pass
# print(kolabJSON["errors"])
except Exception as e:
logger.error(e, exc_info=True)
@@ -1566,8 +1563,7 @@ class Mailcat:
async def print_results(checker, stringToCheck: str, req_session_function, entityUID):
originalString = stringToCheck
if stringToCheck.startswith('@'):
stringToCheck = stringToCheck[1:]
stringToCheck = stringToCheck.removeprefix('@')
if '@' in stringToCheck:
stringToCheck = stringToCheck.split('@', 1)[0] # The first part of an email address won't have a '@'.

View File

@@ -1,5 +1,5 @@
aiohttp[speedups]
aiosmtplib
requests_html
aiohttp_socks
requests-html
aiohttp-socks
dnspython

View File

@@ -0,0 +1,47 @@
#!/usr/bin/env python3
class EvaPingUtil:
name = "Eva PingUtil Email Check"
category = "Reputation Check"
description = "Check if an email address is disposable, spam, or gibberish."
originTypes = {'Email Address', 'Domain'}
resultTypes = {'Phrase'}
parameters = {}
def resolution(self, entityJsonList, parameters):
import requests
returnResults = []
for entity in entityJsonList:
entityType = entity['Entity Type']
if entityType == 'Email Address':
email = entity['Email Address']
elif entityType == 'Domain':
email = f"{entity['Domain Name'].split('.')[0]}@{entity['Domain Name']}"
else:
continue
result = requests.get('https://api.eva.pingutil.com/email?email=' + email).json()
disposable = result['data']['disposable']
spam = result['data']['spam']
gibberish = result['data']['gibberish']
if disposable or spam or gibberish:
returnResults.append([{'Phrase': 'Poor Reputation: ' + email,
'Disposable': str(disposable),
'Spam': str(spam),
'Gibberish': str(gibberish),
'Entity Type': 'Phrase'},
{entity['uid']: {'Resolution': 'Eva PingUtil Email Check',
'Notes': ''}}])
else:
returnResults.append([{'Phrase': 'Good Reputation: ' + email,
'Disposable': str(disposable),
'Spam': str(spam),
'Gibberish': str(gibberish),
'Entity Type': 'Phrase'},
{entity['uid']: {'Resolution': 'Eva PingUtil Email Check',
'Notes': ''}}])
return returnResults

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