Compare commits
95 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
680830c51f | ||
|
|
87e510a48f | ||
|
|
ce42a77162 | ||
|
|
f8b262cb1a | ||
|
|
924c208835 | ||
|
|
7c46ffa499 | ||
|
|
d9fe58de79 | ||
|
|
c8ad8ae383 | ||
|
|
83c1ffdcdc | ||
|
|
756972aa75 | ||
|
|
3603f714fb | ||
|
|
7507990ca5 | ||
|
|
1c8452be08 | ||
|
|
f535f5f9aa | ||
|
|
118b9c2c57 | ||
|
|
efd7bf4f1f | ||
|
|
8e0e7115c5 | ||
|
|
9abeb5e0ec | ||
|
|
fc15db46a3 | ||
|
|
435f4ed97e | ||
|
|
202f8e0ceb | ||
|
|
e16b6f16ff | ||
|
|
3b9e431d12 | ||
|
|
d49c884752 | ||
|
|
ac5ec3d6db | ||
|
|
93d921daa2 | ||
|
|
c44409fab8 | ||
|
|
d8035bdb12 | ||
|
|
d0b643c633 | ||
|
|
a0864cdca5 | ||
|
|
3c864ba511 | ||
|
|
1fe87263dc | ||
|
|
16228612c3 | ||
|
|
fbeb393963 | ||
|
|
0503c64f5f | ||
|
|
194208f772 | ||
|
|
3bcc6c112b | ||
|
|
b879a245b7 | ||
|
|
804b976bb4 | ||
|
|
92fb7c729e | ||
|
|
5f7ceeb2f8 | ||
|
|
f41de445bc | ||
|
|
c2b3f6e710 | ||
|
|
d06acd5aba | ||
|
|
f1dfdb75ec | ||
|
|
f40a13e1a6 | ||
|
|
025b27d191 | ||
|
|
cbe98f652d | ||
|
|
4c1cf569e1 | ||
|
|
0025a18e17 | ||
|
|
e5fa7b35d8 | ||
|
|
09db975d88 | ||
|
|
9cb80f296a | ||
|
|
b9a1149698 | ||
|
|
dd28481792 | ||
|
|
1833304c4a | ||
|
|
6bb39357d9 | ||
|
|
7e9ab2ddd7 | ||
|
|
dab232c750 | ||
|
|
3cda14c487 | ||
|
|
3ffe8efe8d | ||
|
|
e00d052e32 | ||
|
|
9f501119e4 | ||
|
|
eb0c32cc06 | ||
|
|
9863f597b7 | ||
|
|
cee6bb84a2 | ||
|
|
1cdad0decf | ||
|
|
75bb7fea47 | ||
|
|
9c3edee49f | ||
|
|
17b5a08c31 | ||
|
|
7cd1f42267 | ||
|
|
c0dd01f8f0 | ||
|
|
9ffc8e05ad | ||
|
|
fe020ba383 | ||
|
|
3adef55fd2 | ||
|
|
9c60322d86 | ||
|
|
b22f6ee770 | ||
|
|
f65fd1db90 | ||
|
|
9cb48d28a7 | ||
|
|
26d05f4317 | ||
|
|
bd9cd3d0a4 | ||
|
|
1e6ddae957 | ||
|
|
dfbd04b7a6 | ||
|
|
e35100fb7d | ||
|
|
d01e810b7a | ||
|
|
69123ad662 | ||
|
|
361c5f4b96 | ||
|
|
e101ed60bb | ||
|
|
5b2212952e | ||
|
|
6dde75baa1 | ||
|
|
62e65aa2a3 | ||
|
|
6f820c84ae | ||
|
|
20a0f1d9f8 | ||
|
|
58fa19fdfd | ||
|
|
312c6a87c6 |
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal 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
|
||||
@@ -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>
|
||||
|
||||
470
Core/EntityDB.py
470
Core/EntityDB.py
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
|
||||
@@ -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
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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
@@ -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);"
|
||||
@@ -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,
|
||||
|
||||
225
Core/LQL.py
225
Core/LQL.py
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
259
Core/Resolutions/Core/CryptoAddressExtractor.py
Normal file
259
Core/Resolutions/Core/CryptoAddressExtractor.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
76
Core/Resolutions/Core/DeleteColumn.py
Normal file
76
Core/Resolutions/Core/DeleteColumn.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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': ''}}])
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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': ''}}])
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
108
Core/Resolutions/Core/LongANStringExtractor.py
Normal file
108
Core/Resolutions/Core/LongANStringExtractor.py
Normal 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
|
||||
37
Core/Resolutions/Core/NPMJSSearch.py
Normal file
37
Core/Resolutions/Core/NPMJSSearch.py
Normal 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
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
73
Core/Resolutions/Core/ReplacePhrase.py
Normal file
73
Core/Resolutions/Core/ReplacePhrase.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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()
|
||||
|
||||
1671
LinkScope.py
1671
LinkScope.py
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,3 @@
|
||||
requests
|
||||
pandas
|
||||
pandas
|
||||
requests-futures
|
||||
@@ -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'])
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
requests
|
||||
pycountry
|
||||
pycountry
|
||||
requests-futures
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
beautifulsoup4
|
||||
bs4
|
||||
playwright
|
||||
1
Modules/BinaryEdge/requirements.txt
Normal file
1
Modules/BinaryEdge/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
requests
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
1
Modules/BreachData/requirements.txt
Normal file
1
Modules/BreachData/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
requests
|
||||
@@ -1 +1,2 @@
|
||||
requests
|
||||
requests
|
||||
datetime
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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 = ''
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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'},
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
requests
|
||||
beautifulsoup4
|
||||
xmltodict
|
||||
bs4
|
||||
xmltodict
|
||||
playwright
|
||||
datetime
|
||||
@@ -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'}
|
||||
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
docker
|
||||
beautifulsoup4
|
||||
bs4
|
||||
playwright
|
||||
@@ -1 +1,2 @@
|
||||
requests
|
||||
requests
|
||||
requests-futures
|
||||
@@ -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
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
requests
|
||||
requests
|
||||
requests-futures
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
pycountry
|
||||
requests
|
||||
requests_futures
|
||||
requests-futures
|
||||
71
Modules/InternetDB/InternetDB.py
Normal file
71
Modules/InternetDB/InternetDB.py
Normal 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
|
||||
1
Modules/InternetDB/requirements.txt
Normal file
1
Modules/InternetDB/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
requests
|
||||
@@ -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])
|
||||
|
||||
@@ -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])
|
||||
|
||||
1
Modules/Interpol/requirements.txt
Normal file
1
Modules/Interpol/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
requests
|
||||
@@ -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 '@'.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
aiohttp[speedups]
|
||||
aiosmtplib
|
||||
requests_html
|
||||
aiohttp_socks
|
||||
requests-html
|
||||
aiohttp-socks
|
||||
dnspython
|
||||
47
Modules/MailDomainReputation/EvaPingUtil.py
Normal file
47
Modules/MailDomainReputation/EvaPingUtil.py
Normal 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
Reference in New Issue
Block a user