25 Commits

Author SHA1 Message Date
AccentuSoft
7507990ca5 Push commits:
Move saved Resolutions parameters to global scope.

Move all Program settings to global scope.

Update & Refactor Settings Object.

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

Update README.md

Update version number

Finalize importing tabs from TOR.

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

Fix bug with deselected entities when zooming out.

Transition to pyqtdarktheme for theme & styling.

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

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

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

Make the properties editors use Global Variables for hidden fields.

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

Update build scripts.

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

Add logging for LinkScope version.

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

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

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

View File

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

View File

@@ -54,8 +54,8 @@ class CommunicationsHandler(QtCore.QObject):
receive_start_collector_signal = QtCore.Signal(str, str, str, list, dict)
receive_collector_result_signal = QtCore.Signal(str, str, str, list)
receive_resolutions_signal = QtCore.Signal(dict)
receive_completed_resolution_result_signal = QtCore.Signal(str, list)
receive_completed_resolution_string_result_signal = QtCore.Signal(str, str)
receive_completed_resolution_result_signal = QtCore.Signal(str, list, str)
receive_completed_resolution_string_result_signal = QtCore.Signal(str, str, str)
receive_document_summary_signal = QtCore.Signal(str, str)
remove_server_resolution_from_running_signal = QtCore.Signal(str)
receive_projects_list_signal = QtCore.Signal(list)
@@ -379,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:

View File

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

View File

@@ -20,8 +20,9 @@ from PySide6.QtWidgets import QGraphicsPixmapItem
from PySide6.QtSvgWidgets import QGraphicsSvgItem
from PySide6.QtWebEngineWidgets import QWebEngineView
from Core.Interface import Entity, Stylesheets
from Core.Interface import Entity
from Core.ResourceHandler import RichNotesEditor
from Core.GlobalVariables import hidden_fields
class WorkspaceWidget(QtWidgets.QWidget):
@@ -90,7 +91,7 @@ class TabBar(QtWidgets.QTabBar):
self.setMovable(True)
def mouseDoubleClickEvent(self, event) -> None:
if event.button() != QtGui.Qt.LeftButton:
if event.button() != QtGui.Qt.MouseButton.LeftButton:
return
currIndex = self.currentIndex()
currName = self.tabText(currIndex)
@@ -123,7 +124,6 @@ class RenameOrDeleteTabDialog(QtWidgets.QDialog):
def __init__(self, isSynced: bool, currName: str) -> None:
super(RenameOrDeleteTabDialog, self).__init__()
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
self.setModal(True)
self.setWindowTitle('Rename Or Delete Tab')
@@ -193,6 +193,9 @@ class TabbedPane(QtWidgets.QTabWidget):
self.tabsNotesDict = {}
self.previousTab = None
self.allBanners = {bannerID: bannerPath
for bannerID, bannerPath in self.mainWindow.RESOURCEHANDLER.banners.items()}
self.currentChanged.connect(self.currentTabChangedListener)
def getCanvasDBPath(self):
@@ -352,7 +355,7 @@ class TabbedPane(QtWidgets.QTabWidget):
progress = QtWidgets.QProgressDialog(f'Resolving new nodes for resolution: {resolution_name}, please wait...',
'Abort Resolving Nodes', 0, steps, self)
progress.setWindowModality(QtCore.Qt.WindowModal)
progress.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
progress.setMinimumDuration(1500)
# In case we have no entities in the database when the resolution finishes, i.e. the user deletes the origin
@@ -600,8 +603,9 @@ class TabbedPane(QtWidgets.QTabWidget):
# Save canvases
with open(canvasDBPathTmp, "wb") as canvasDBFile:
saveJson = {canvasName: [self.resourceHandler.deconstructGraphForFileDump(self.canvasTabs[canvasName].scene().sceneGraph),
self.canvasTabs[canvasName].scene().scenePos] for canvasName in self.canvasTabs}
saveJson = {canvasName: [self.resourceHandler.deconstructGraphForFileDump(
self.canvasTabs[canvasName].scene().sceneGraph),
self.canvasTabs[canvasName].scene().scenePos] for canvasName in self.canvasTabs}
dump(saveJson, canvasDBFile)
move(canvasDBPathTmp, canvasDBPath)
@@ -746,31 +750,26 @@ class CanvasView(QtWidgets.QGraphicsView):
self.name = name
self.urlManager = urlManager
self.setRenderHint(QtGui.QPainter.Antialiasing)
self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
self.setViewportUpdateMode(QtWidgets.QGraphicsView.FullViewportUpdate)
self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
# self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
# self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing)
self.setTransformationAnchor(QtWidgets.QGraphicsView.ViewportAnchor.AnchorUnderMouse)
self.setViewportUpdateMode(QtWidgets.QGraphicsView.ViewportUpdateMode.FullViewportUpdate)
self.setResizeAnchor(QtWidgets.QGraphicsView.ViewportAnchor.AnchorUnderMouse)
self.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(54, 69, 79)))
self.setFrameShape(QtWidgets.QFrame.NoFrame)
self.setFrameShape(QtWidgets.QFrame.Shape.NoFrame)
self.setAcceptDrops(True)
self.setDragMode(QtWidgets.QGraphicsView.NoDrag)
self.setDragMode(QtWidgets.QGraphicsView.DragMode.NoDrag)
self.setSizePolicy(QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding))
QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding))
self.dragOver = False
self.synced = False
self.menu = QtWidgets.QMenu()
self.menu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
selectMenu = self.menu.addMenu("Select...")
selectMenu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
viewMenu = self.menu.addMenu("Hide / Delete...")
viewMenu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
groupingMenu = self.menu.addMenu("Grouping...")
groupingMenu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
bannersMenu = self.menu.addMenu("Banners...")
actionSelectChildren = QtGui.QAction('Select Child Nodes',
selectMenu,
@@ -835,6 +834,18 @@ class CanvasView(QtWidgets.QGraphicsView):
triggered=self.importConnectedEntities)
self.menu.addAction(importConnectedEntitiesAction)
self.clearBannerMenu = QtGui.QAction('Clear Banners',
bannersMenu,
statusTip="Remove banners from the selected entities.",
triggered=self.clearBanners)
bannersMenu.addAction(self.clearBannerMenu)
self.setBannerIconMenu = QtGui.QAction('Set Banner Icon',
bannersMenu,
statusTip="Set a banner icon for the selected entities.",
triggered=self.setBanners)
bannersMenu.addAction(self.setBannerIconMenu)
def deleteItemsFromDatabase(self) -> None:
items = self.scene().selectedItems()
for item in items:
@@ -915,8 +926,7 @@ class CanvasView(QtWidgets.QGraphicsView):
if entityUID in self.scene().sceneGraph.nodes():
wasGrouped = False
for groupNode in [node for node in self.items() if isinstance(node, Entity.GroupNode)]:
wasGrouped = \
groupNode.removeSpecificItemFromGroupIfExists(entityUID)
wasGrouped = groupNode.removeSpecificItemFromGroupIfExists(entityUID)
if wasGrouped:
self.removeGroupNodeLinksForUID(groupNode.uid, entityUID)
groupNodeJson = self.tabbedPane.entityDB.getEntity(groupNode.uid)
@@ -998,7 +1008,7 @@ class CanvasView(QtWidgets.QGraphicsView):
def mouseReleaseEvent(self, event) -> None:
QtWidgets.QGraphicsView.mouseReleaseEvent(self, event)
self.setDragMode(QtWidgets.QGraphicsView.NoDrag)
self.setDragMode(QtWidgets.QGraphicsView.DragMode.NoDrag)
itemsMoved = [item for item in self.scene().selectedItems()
if isinstance(item, Entity.BaseNode)]
for item in itemsMoved:
@@ -1027,10 +1037,11 @@ class CanvasView(QtWidgets.QGraphicsView):
if event.button() == QtCore.Qt.MouseButton.RightButton and \
not self.scene().linking and not self.scene().appendingToGroup:
if len(self.scene().selectedItems()) == 0:
self.setDragMode(QtWidgets.QGraphicsView.RubberBandDrag)
self.setDragMode(QtWidgets.QGraphicsView.DragMode.RubberBandDrag)
else:
items = self.scene().selectedItems()
groupItems = [groupItem for groupItem in items if isinstance(groupItem, Entity.GroupNode)]
entityItems = [entityItem for entityItem in items if isinstance(entityItem, Entity.BaseNode)]
groupItems = [groupItem for groupItem in entityItems if isinstance(groupItem, Entity.GroupNode)]
linkItems = [linkItem for linkItem in items if isinstance(linkItem, Entity.BaseConnector)]
if groupItems:
self.actionUngroup.setDisabled(False)
@@ -1038,12 +1049,23 @@ class CanvasView(QtWidgets.QGraphicsView):
else:
self.actionUngroup.setDisabled(True)
self.actionUngroup.setEnabled(False)
if len(items) > 1:
self.actionGroup.setDisabled(False)
self.actionGroup.setEnabled(True)
if entityItems:
self.clearBannerMenu.setDisabled(False)
self.clearBannerMenu.setEnabled(True)
self.setBannerIconMenu.setDisabled(False)
self.setBannerIconMenu.setEnabled(True)
if len(entityItems) > 1:
self.actionGroup.setDisabled(False)
self.actionGroup.setEnabled(True)
else:
self.actionGroup.setDisabled(True)
self.actionGroup.setEnabled(False)
else:
self.actionGroup.setDisabled(True)
self.actionGroup.setEnabled(False)
self.clearBannerMenu.setDisabled(True)
self.clearBannerMenu.setEnabled(False)
self.setBannerIconMenu.setDisabled(True)
self.setBannerIconMenu.setEnabled(False)
if linkItems:
self.actionLinkDelete.setDisabled(False)
self.actionLinkDelete.setEnabled(True)
@@ -1055,7 +1077,7 @@ class CanvasView(QtWidgets.QGraphicsView):
elif event.button() == QtCore.Qt.MouseButton.RightButton and self.scene().appendingToGroup:
self.scene().appendSelectedItemsToGroupToggle()
elif event.button() == QtCore.Qt.MouseButton.LeftButton:
self.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag)
self.setDragMode(QtWidgets.QGraphicsView.DragMode.ScrollHandDrag)
super(CanvasView, self).mousePressEvent(event)
def deleteSelectedLinks(self) -> None:
@@ -1135,6 +1157,33 @@ class CanvasView(QtWidgets.QGraphicsView):
newNode.setSelected(True)
self.scene().rearrangeGraph()
def clearBanners(self) -> None:
selectedEntities = [item for item in self.scene().selectedItems() if isinstance(item, Entity.BaseNode)]
for entity in selectedEntities:
entityJson = self.tabbedPane.mainWindow.LENTDB.getEntity(entity.uid)
entityJson['Canvas Banner'] = ''
entity.updateBanner(True, None)
self.tabbedPane.mainWindow.LENTDB.addEntity(entityJson, updateTimeline=False)
def setBanners(self) -> None:
selectedEntities = [item for item in self.scene().selectedItems() if isinstance(item, Entity.BaseNode)]
if len(selectedEntities) < 1:
self.tabbedPane.mainWindow.MESSAGEHANDLER.warning('Need to select at least one Entity to set its banner.',
popUp=True)
return
bannerDialog = BannerSelector(self.tabbedPane.allBanners)
if bannerDialog.exec():
try:
# This following line will throw IndexError if no banner is selected.
selectedBannerItem = bannerDialog.bannerIconContainer.selectedItems()[0].text()
self.scene().bannerDrawHelper(selectedEntities, selectedBannerItem)
for entity in selectedEntities:
entityJson = self.tabbedPane.mainWindow.LENTDB.getEntity(entity.uid)
entityJson['Canvas Banner'] = selectedBannerItem
self.tabbedPane.mainWindow.LENTDB.addEntity(entityJson, updateTimeline=False)
except IndexError:
self.tabbedPane.mainWindow.MESSAGEHANDLER.warning('No Banner selected.', popUp=True)
def takePictureOfView(self, justViewport: bool = True, transparentBackground: bool = False) -> QtGui.QImage:
# Need to set size and format of pic before using it.
# Ref: https://qtcentre.org/threads/10975-Help-Export-QGraphicsView-to-Image-File
@@ -1144,7 +1193,7 @@ class CanvasView(QtWidgets.QGraphicsView):
for item in selectedItems:
item.setSelected(False)
if justViewport:
picture = QtGui.QImage(self.viewport().size(), QtGui.QImage.Format_ARGB32_Premultiplied)
picture = QtGui.QImage(self.viewport().size(), QtGui.QImage.Format.Format_ARGB32_Premultiplied)
# Pictures are initialised with junk data - need to clear it out before painting
# to avoid visual artifacts.
picture.fill(QtGui.QColor(0, 0, 0, 0))
@@ -1157,7 +1206,7 @@ class CanvasView(QtWidgets.QGraphicsView):
else:
# Convert QRectF to QRect - can't have floats when it comes to picture size.
rectToPrint = self.scene().sceneRect().toRect()
picture = QtGui.QImage(rectToPrint.size(), QtGui.QImage.Format_ARGB32_Premultiplied)
picture = QtGui.QImage(rectToPrint.size(), QtGui.QImage.Format.Format_ARGB32_Premultiplied)
# Pictures are initialised with junk data - need to clear it out before painting
# to avoid visual artifacts.
picture.fill(QtGui.QColor(0, 0, 0, 0))
@@ -1224,6 +1273,48 @@ class CanvasScene(QtWidgets.QGraphicsScene):
# Re-Center the Label
item.updateLabel(item.labelItem.text())
def bannerDrawHelper(self, entities: list, bannerName: str = None) -> None:
"""
If we are given a banner name, try to set each canvas entity banner to the banner with the given name.
If not, then instead we get the banner that each entity is already assigned, and make sure it's drawn.
"""
if bannerName:
try:
bannerPathStr = self.parent().allBanners[bannerName]
with open(bannerPathStr, 'rb') as bannerFile:
bannerByteArray = QtCore.QByteArray(bannerFile.read())
for entity in entities:
entity.updateBanner(False, bannerByteArray)
except FileNotFoundError:
self.parent().mainWindow.MESSAGEHANDLER.error(f'Banner Icon not found in filesystem: {bannerName}',
popUp=True,
exc_info=False)
except KeyError:
self.parent().mainWindow.MESSAGEHANDLER.warning(f'Invalid Banner: {bannerName}', popUp=True)
else:
notFoundBanners = set()
for entity in entities:
try:
entityJson = self.parent().mainWindow.LENTDB.getEntity(entity.uid)
bannerPathStr = self.parent().allBanners.get(entityJson.get('Canvas Banner', ''), '')
if not bannerPathStr:
entity.updateBanner(True, None)
else:
with open(bannerPathStr, 'rb') as bannerFile:
bannerByteArray = QtCore.QByteArray(bannerFile.read())
entity.updateBanner(False, bannerByteArray)
except FileNotFoundError:
if bannerName not in notFoundBanners:
self.parent().mainWindow.MESSAGEHANDLER.error(f'Banner Icon not found in filesystem: '
f'{bannerName}',
popUp=True,
exc_info=False)
notFoundBanners.add(bannerName)
except KeyError:
if bannerName not in notFoundBanners:
self.parent().mainWindow.MESSAGEHANDLER.warning(f'Invalid Banner: {bannerName}', popUp=True)
notFoundBanners.add(bannerName)
# Redefined so that the BaseConnector items are not considered.
def itemsBoundingRect(self) -> QtCore.QRectF:
try:
@@ -1246,6 +1337,7 @@ class CanvasScene(QtWidgets.QGraphicsScene):
def addNodeToScene(self, item, x=0, y=0) -> None:
self.nodesDict[item.uid] = item
self.addItem(item)
self.bannerDrawHelper([item])
item.setPos(QtCore.QPointF(x, y))
self.parent().mainWindow.MESSAGEHANDLER.info(f'Added node: {str(item.uid)} | {item.labelItem.toPlainText()}')
@@ -1365,7 +1457,7 @@ class CanvasScene(QtWidgets.QGraphicsScene):
# Remove Cancel button from progress bar (user should not be able to stop canvas from loading).
progress.setMinimumDuration(1500)
progress.setCancelButton(None)
progress.setWindowModality(QtCore.Qt.WindowModal)
progress.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
progressValue = 0
for node in sceneGraphNodes:
@@ -1515,7 +1607,7 @@ class CanvasScene(QtWidgets.QGraphicsScene):
# No triangulation library on Windows, so sfdp can't be used there.
if graphAlgorithm is None or graphAlgorithm not in ('sfdp', 'neato', 'dot', 'circular'):
graphAlgorithm = self.parent().mainWindow.SETTINGS.value("Program/GraphLayout", 'dot')
graphAlgorithm = self.parent().mainWindow.SETTINGS.value("Program/Graph Layout", 'dot')
# No real 'links' to group nodes by default (links to internal nodes don't count). This means that the
# graph algorithms can create odd graphs where group nodes are concerned.
@@ -1795,13 +1887,13 @@ class CanvasScene(QtWidgets.QGraphicsScene):
self.removeItem(item.iconItem)
pictureByteArray = pEditor.objectJson['Icon']
item.pixmapItem = QtGui.QPixmap()
item.pixmapItem.loadFromData(pictureByteArray)
if pictureByteArray.data().startswith(b'<svg '):
item.iconItem = QGraphicsSvgItem()
item.iconItem.renderer().load(pictureByteArray)
item.iconItem.setElementId("") # Force recalculation of geometry, else this looks like 1 pixel.
else:
item.pixmapItem = QtGui.QPixmap()
item.pixmapItem.loadFromData(pictureByteArray)
item.iconItem = QGraphicsPixmapItem(item.pixmapItem)
item.iconItem.setPos(item.pos())
@@ -1971,14 +2063,13 @@ class PropertiesEditor(QtWidgets.QDialog):
self.isEditingNode = isNode
self.setWindowTitle("Properties Editor")
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
self.setMinimumSize(500, 300)
self.objectJson = objectJson
self.canvas = canvas
self.itemProperties = QtWidgets.QFormLayout()
for key in objectJson:
if key in ('uid', 'Entity Type', 'Date Last Edited', 'Child UIDs'):
if key in hidden_fields:
continue
keyField = QtWidgets.QLabel(key)
if key == "Notes":
@@ -1991,9 +2082,7 @@ class PropertiesEditor(QtWidgets.QDialog):
valueField = QtWidgets.QLineEdit(str(objectJson[key]))
self.itemProperties.addRow(keyField, valueField)
acceptButton = QtWidgets.QPushButton("Confirm")
acceptButton.setStyleSheet(Stylesheets.BUTTON_STYLESHEET)
cancelButton = QtWidgets.QPushButton("Cancel")
cancelButton.setStyleSheet(Stylesheets.BUTTON_STYLESHEET)
acceptButton.setAutoDefault(True)
acceptButton.setDefault(True)
acceptButton.clicked.connect(self.accept)
@@ -2005,16 +2094,16 @@ class PropertiesEditor(QtWidgets.QDialog):
def accept(self):
for row in range(self.itemProperties.rowCount()):
key = self.itemProperties.itemAt(
row, self.itemProperties.LabelRole).widget().text()
row, self.itemProperties.ItemRole.LabelRole).widget().text()
if key == "Notes":
value = self.itemProperties.itemAt(
row, self.itemProperties.FieldRole).widget().toMarkdown()
row, self.itemProperties.ItemRole.FieldRole).widget().toMarkdown()
elif key == 'Icon':
value = self.itemProperties.itemAt(
row, self.itemProperties.FieldRole).widget().pictureByteArray
row, self.itemProperties.ItemRole.FieldRole).widget().pictureByteArray
elif key == 'File Path':
value = self.itemProperties.itemAt(
row, self.itemProperties.FieldRole).widget().text()
row, self.itemProperties.ItemRole.FieldRole).widget().text()
projectFilesPath = Path(self.canvas.parent().mainWindow.SETTINGS.value("Project/FilesDir"))
newPath = projectFilesPath / value
if not newPath.is_relative_to(projectFilesPath):
@@ -2024,7 +2113,7 @@ class PropertiesEditor(QtWidgets.QDialog):
value = 'None'
else:
value = self.itemProperties.itemAt(
row, self.itemProperties.FieldRole).widget().text()
row, self.itemProperties.ItemRole.FieldRole).widget().text()
# The last row is the Cancel / Accept buttons.
if key != "Cancel":
self.objectJson[key] = value
@@ -2053,8 +2142,10 @@ class PropertiesEditorFilePathField(QtWidgets.QLineEdit):
self.setText(str(value))
def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
selectedPath = QtWidgets.QFileDialog().getOpenFileName(parent=self, caption='Select File Path',
options=QtWidgets.QFileDialog.DontUseNativeDialog)[0]
selectedPath = QtWidgets.QFileDialog().getOpenFileName(
parent=self,
caption='Select File Path',
options=QtWidgets.QFileDialog.Option.DontUseNativeDialog)[0]
if selectedPath != '':
self.setText(str(Path(selectedPath).absolute()))
@@ -2075,7 +2166,7 @@ class PropertiesEditorIconField(QtWidgets.QLabel):
def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
selectedPath = QtWidgets.QFileDialog().getOpenFileName(parent=self, caption='Select New Icon',
options=QtWidgets.QFileDialog.DontUseNativeDialog,
options=QtWidgets.QFileDialog.Option.DontUseNativeDialog,
filter="Image Files (*.png *.jpg *.bmp *.svg)")[0]
if selectedPath != '':
try:
@@ -2099,7 +2190,7 @@ class PropertiesEditorIconField(QtWidgets.QLabel):
self.pictureByteArray = QtCore.QByteArray()
imageBuffer = QtCore.QBuffer(self.pictureByteArray)
imageBuffer.open(QtCore.QIODevice.WriteOnly)
imageBuffer.open(QtCore.QIODevice.OpenModeFlag.WriteOnly)
thumbnail.save(imageBuffer, "PNG")
imageBuffer.close()
@@ -2119,7 +2210,6 @@ class SendToOtherTabCanvasSelector(QtWidgets.QDialog):
def __init__(self, canvasNames: list):
super(SendToOtherTabCanvasSelector, self).__init__()
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
self.setModal(True)
self.setWindowTitle('Move Selected Entities to New Canvas')
@@ -2143,3 +2233,48 @@ class SendToOtherTabCanvasSelector(QtWidgets.QDialog):
sendToOtherCanvasLayout.addWidget(self.canvasNameSelector, 1, 1, 1, 2)
sendToOtherCanvasLayout.addWidget(cancelButton, 2, 0, 1, 1)
sendToOtherCanvasLayout.addWidget(confirmButton, 2, 1, 1, 2)
class BannerSelector(QtWidgets.QDialog):
def __init__(self, bannerDict: dict):
super(BannerSelector, self).__init__()
self.setModal(True)
self.setWindowTitle('Select Banner')
bannerLayout = QtWidgets.QVBoxLayout()
self.setLayout(bannerLayout)
descriptionLabel = QtWidgets.QLabel("Select the Banner that you want to apply to the selected Entities:")
descriptionLabel.setWordWrap(True)
bannerLayout.addWidget(descriptionLabel)
self.bannerIconContainer = QtWidgets.QListWidget()
self.bannerIconContainer.setFlow(self.bannerIconContainer.Flow.LeftToRight)
self.bannerIconContainer.setMovement(self.bannerIconContainer.Movement.Static)
self.bannerIconContainer.setViewMode(self.bannerIconContainer.ViewMode.IconMode)
self.bannerIconContainer.setLayoutMode(self.bannerIconContainer.LayoutMode.SinglePass)
self.bannerIconContainer.setSelectionMode(self.bannerIconContainer.SelectionMode.SingleSelection)
self.bannerIconContainer.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.bannerIconContainer.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.bannerIconContainer.setSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum,
QtWidgets.QSizePolicy.Policy.Minimum)
for bannerID, bannerPathStr in bannerDict.items():
bannerPixmap = QtGui.QIcon(bannerPathStr)
QtWidgets.QListWidgetItem(bannerPixmap, bannerID, self.bannerIconContainer)
bannerLayout.addWidget(self.bannerIconContainer)
buttonsWidget = QtWidgets.QWidget()
buttonsWidgetLayout = QtWidgets.QHBoxLayout()
buttonsWidget.setLayout(buttonsWidgetLayout)
cancelButton = QtWidgets.QPushButton('Cancel')
cancelButton.clicked.connect(self.reject)
acceptButton = QtWidgets.QPushButton('Confirm')
acceptButton.clicked.connect(self.accept)
acceptButton.setAutoDefault(True)
acceptButton.setDefault(True)
buttonsWidgetLayout.addWidget(cancelButton)
buttonsWidgetLayout.addWidget(acceptButton)
bannerLayout.addWidget(buttonsWidget)

View File

@@ -1,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,12 @@ class EntityList(QtWidgets.QTreeWidget):
"""
Handle dragging of entities onto canvas.
"""
itemDragged = self.itemAt(event.pos())
super().mouseMoveEvent(event)
if event.buttons() == QtCore.Qt.MouseButton.LeftButton:
itemDragged = self.itemAt(event.pos())
else:
return
# Categories & entity names don't have uids.
try:
@@ -201,7 +202,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 +244,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 +300,6 @@ class ResolutionList(QtWidgets.QTreeWidget):
super(ResolutionList, self).__init__(parent=parent)
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
self.resolutionManager = resolutionManager
self.lentDB = entityDatabase
self.mainWindow = mainWindow
@@ -310,7 +308,7 @@ class ResolutionList(QtWidgets.QTreeWidget):
self.setAlternatingRowColors(False)
self.setMinimumWidth(200)
self.setSortingEnabled(True)
self.sortByColumn(0, QtCore.Qt.AscendingOrder)
self.sortByColumn(0, QtCore.Qt.SortOrder.AscendingOrder)
self.loadAllResolutions()
@@ -368,13 +366,12 @@ class NodeList(QtWidgets.QTreeWidget):
def __init__(self, resourceHandler, parent=None) -> None:
super(NodeList, self).__init__(parent=parent)
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
self.resourceHandler = resourceHandler
self.setDragEnabled(True)
self.setHeaderLabels(['Entities'])
self.setAlternatingRowColors(False)
self.setSortingEnabled(True)
self.sortByColumn(0, QtCore.Qt.AscendingOrder)
self.sortByColumn(0, QtCore.Qt.SortOrder.AscendingOrder)
self.allEntities = []
self.loadEntities()
@@ -398,8 +395,7 @@ class NodeList(QtWidgets.QTreeWidget):
"""
Handle dragging of entities onto canvas.
"""
# No, I have no idea why this is the case: v
if event.button() == QtGui.Qt.MouseButton.NoButton:
if event.buttons() == QtCore.Qt.MouseButton.LeftButton:
itemDragged = self.itemAt(event.pos())
if itemDragged is None or \
itemDragged.text(0) not in self.allEntities:
@@ -419,9 +415,6 @@ class NodeList(QtWidgets.QTreeWidget):
drag.setPixmap(pixmap)
drag.setHotSpot(QtCore.QPoint(pixmap.rect().width() // 2, pixmap.rect().height() // 2))
drag.exec_()
else:
# This should never happen.
super(NodeList, self).mousePressEvent(event)
class NodeWidget(QtWidgets.QTreeWidgetItem):

View File

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

View File

@@ -3,8 +3,8 @@
from pathlib import Path
import magic
from PySide6 import QtWidgets, QtCore, QtGui
from Core.Interface import Stylesheets
from Core.ResourceHandler import MinSizeStackedLayout, RichNotesEditor
from Core.GlobalVariables import hidden_fields_dockbars
class DockBarTwo(QtWidgets.QDockWidget):
@@ -15,9 +15,9 @@ class DockBarTwo(QtWidgets.QDockWidget):
scrollAreaWidget = QtWidgets.QScrollArea()
scrollAreaWidget.setWidget(self.entDetails)
scrollAreaWidget.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
scrollAreaWidget.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAsNeeded)
scrollAreaWidget.setWidgetResizable(True)
scrollAreaWidget.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
scrollAreaWidget.setSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum)
childWidget.addTab(scrollAreaWidget, 'Entity Details')
childWidget.addTab(self.oracle, 'Oracle')
@@ -30,11 +30,11 @@ class DockBarTwo(QtWidgets.QDockWidget):
title="DockBar Two"):
super(DockBarTwo, self).__init__(parent=mainWindow)
self.setAllowedAreas(QtCore.Qt.LeftDockWidgetArea |
QtCore.Qt.RightDockWidgetArea)
self.setFeatures(QtWidgets.QDockWidget.DockWidgetMovable |
QtWidgets.QDockWidget.DockWidgetFloatable |
QtWidgets.QDockWidget.DockWidgetClosable)
self.setAllowedAreas(QtCore.Qt.DockWidgetArea.LeftDockWidgetArea |
QtCore.Qt.DockWidgetArea.RightDockWidgetArea)
self.setFeatures(QtWidgets.QDockWidget.DockWidgetFeature.DockWidgetMovable |
QtWidgets.QDockWidget.DockWidgetFeature.DockWidgetFloatable |
QtWidgets.QDockWidget.DockWidgetFeature.DockWidgetClosable)
self.setWindowTitle(title)
self.resourceHandler = resourceHandler
self.entityDB = entityDB
@@ -98,7 +98,7 @@ class EntityDetails(QtWidgets.QWidget):
self.entityDB = entityDB
self.detailsLayout = MinSizeStackedLayout()
self.setLayout(self.detailsLayout)
self.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
self.setSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum)
layoutNothingSelected = QtWidgets.QVBoxLayout()
widgetNothing = QtWidgets.QWidget()
@@ -118,7 +118,7 @@ class EntityDetails(QtWidgets.QWidget):
# Need to keep track of how many nodes are selected.
# ~ Nothing Selected/Hovered Layout
nothingLabel = QtWidgets.QLabel("Nothing is Selected.")
nothingLabel.setAlignment(QtCore.Qt.AlignCenter)
nothingLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
layoutNothingSelected.addWidget(nothingLabel)
###
@@ -128,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("")
@@ -164,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)
@@ -276,7 +273,7 @@ class EntityDetails(QtWidgets.QWidget):
return
rowCount = 0
for key in jsonDict:
if key in ["uid", "Child UIDs", "Icon"]:
if key in hidden_fields_dockbars:
continue
elif key == "Notes":
notesTextArea = RichNotesEditor(self, jsonDict[key], False)
@@ -304,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)
@@ -415,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)
@@ -441,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
@@ -474,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):
@@ -542,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)
@@ -554,9 +549,7 @@ class Oracle(QtWidgets.QWidget):
oracleLayout.addWidget(self.answerSection, 2, 0, 1, 2)
self.questionLabel = QtWidgets.QLabel("Ask a Question")
self.questionLabel.setStyleSheet(Stylesheets.DOCK_BAR_LABEL)
self.questionLabel.setAlignment(QtCore.Qt.AlignHCenter |
QtCore.Qt.AlignVCenter)
self.questionLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
oracleLayout.addWidget(self.questionLabel, 3, 0, 1, 2)
self.questionSection = QtWidgets.QLineEdit()
self.questionSection.setPlaceholderText("Ask a Question here.")

View File

@@ -20,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()
@@ -32,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:
@@ -53,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)
@@ -64,17 +69,24 @@ class BaseNode(QGraphicsItemGroup):
self.setAcceptHoverEvents(True)
self.connectors = []
self.bookmarked = False
self.isBeingResolved = False
self.parentGroup = None
def updateLabel(self, newText: str = '') -> None:
if not isinstance(newText, str):
newText = newText
if newText != '':
if len(newText) > 50:
newText = f"{newText[:47]}..."
self.labelItem.setPlainText(newText)
self.labelItem.document().adjustSize()
self.labelItem.setPos(self.iconItem.x() + 20 - (self.labelItem.textWidth() / 2), self.iconItem.y() + 45)
def updateBanner(self, bannerHidden: bool = True, bannerGraphic: QtCore.QByteArray = None) -> None:
if bannerHidden: # No icon visible
self.bannerIconItem.hide()
self.bannerIconItem.setVisible(False)
return
self.bannerIconItem.renderer().load(bannerGraphic)
self.bannerIconItem.show()
self.bannerIconItem.setVisible(True)
self.bannerIconItem.setElementId("")
def removeConnector(self, connector) -> None:
# Exception could be thrown if the connector is already deleted.
@@ -117,9 +129,14 @@ class BaseNode(QGraphicsItemGroup):
def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionGraphicsItem,
widget: Optional[QtWidgets.QWidget] = ...) -> None:
painter.setPen(QtCore.Qt.NoPen)
painter.setPen(QtCore.Qt.PenStyle.NoPen)
if self.scene().views()[0].zoom < self.scene().hideZoom:
self.labelItem.hide()
# Looks stupid, but fixes bug where entities are deselected when zooming out past hideZoom level.
if self.isSelected():
self.labelItem.hide()
self.setSelected(True)
else:
self.labelItem.hide()
else:
self.labelItem.show()
if self.isSelected():
@@ -145,7 +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:
@@ -185,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)
@@ -250,8 +267,8 @@ class BaseConnector(QGraphicsItemGroup):
self.colorDefault = QtGui.QColor(200, 200, 200)
self.myColor = self.colorDefault
self.pen = QtGui.QPen(self.myColor, 2, QtCore.Qt.SolidLine,
QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
self.pen = QtGui.QPen(self.myColor, 2, QtCore.Qt.PenStyle.SolidLine,
QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin)
self.arrowHead = QtGui.QPolygonF()
self.line = QtCore.QLineF()
@@ -312,14 +329,22 @@ class BaseConnector(QGraphicsItemGroup):
line = QtCore.QLineF(p1, p2)
if line.length() < 45:
self.labelItem.hide()
if self.isSelected():
self.labelItem.hide()
self.setSelected(True)
else:
self.labelItem.hide()
return
angle = math.atan2(line.dy(), - line.dx())
if (line.length() < 50 + len(self.labelItem.text()) * 15) or \
self.scene().views()[0].zoom < self.scene().hideZoom:
self.labelItem.hide()
if self.isSelected():
self.labelItem.hide()
self.setSelected(True)
else:
self.labelItem.hide()
else:
self.labelItem.show()
angle2 = math.degrees(math.pi - angle)
@@ -371,7 +396,7 @@ class GroupNodeChildList(QtWidgets.QWidget):
self.setLayout(QtWidgets.QVBoxLayout())
titleLabel = QtWidgets.QLabel('Child Items')
titleLabel.setAlignment(QtCore.Qt.AlignVCenter | QtCore.Qt.AlignHCenter)
titleLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.layout().addWidget(titleLabel)
self.itemList = ChildListWidget()
@@ -386,7 +411,11 @@ class ChildListWidget(QtWidgets.QListWidget):
self.setSortingEnabled(True)
def mouseMoveEvent(self, event: QtGui.QMouseEvent) -> None:
itemDragged = self.itemAt(event.pos())
super().mouseMoveEvent(event)
itemDragged = None
if event.buttons() == QtCore.Qt.MouseButton.LeftButton:
itemDragged = self.itemAt(event.pos())
if itemDragged is None:
return
@@ -405,5 +434,3 @@ class ChildListWidget(QtWidgets.QListWidget):
drag.setPixmap(pixmap)
drag.setHotSpot(QtCore.QPoint(pixmap.rect().width() / 2, pixmap.rect().height() / 2))
drag.exec_()
super().mousePressEvent(event)

View File

@@ -27,14 +27,13 @@ from playwright.sync_api import sync_playwright, Error, TimeoutError
from PySide6 import QtWidgets, QtGui, QtCore
from Core.GlobalVariables import user_agents
from Core.Interface import Stylesheets
from Core.Interface.Entity import BaseNode
from Core.ResourceHandler import StringPropertyInput, FilePropertyInput, SingleChoicePropertyInput, \
MultiChoicePropertyInput
class MenuBar(QtWidgets.QMenuBar):
browserTabsImportDoneSignalListener = QtCore.Signal(list, bool, str)
browserTabsImportDoneSignalListener = QtCore.Signal(list, str)
def __init__(self, parent):
super().__init__(parent=parent)
@@ -42,7 +41,6 @@ class MenuBar(QtWidgets.QMenuBar):
self.browserTabsImportDoneSignalListener.connect(self.importBrowserTabsFindings)
fileMenu = self.addMenu("File")
fileMenu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
saveAction = QtGui.QAction("&Save",
self,
@@ -64,13 +62,17 @@ class MenuBar(QtWidgets.QMenuBar):
fileMenu.addAction(renameAction)
importMenu = self.addMenu("Import")
importMenu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
fromBrowserAction = QtGui.QAction("From Browser",
self,
statusTip="Import open tabs as Website and materials entities.",
triggered=self.importFromBrowser)
fromTorBrowserAction = QtGui.QAction("From TOR Browser",
self,
statusTip="Import open TOR tabs as Website and materials entities.",
triggered=self.importFromTORBrowser)
fromFileAction = QtGui.QAction("From File",
self,
statusTip="Import entities from a file.",
@@ -86,12 +88,12 @@ class MenuBar(QtWidgets.QMenuBar):
statusTip="Import database from a GraphML file.",
triggered=self.parent().importDatabaseFromGraphML)
importMenu.addAction(fromBrowserAction)
importMenu.addAction(fromTorBrowserAction)
importMenu.addAction(fromFileAction)
importMenu.addAction(graphMLCanvasAction)
importMenu.addAction(graphMLDatabaseAction)
exportMenu = self.addMenu("Export")
exportMenu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
canvasPictureAction = QtGui.QAction("Save Picture of Canvas", self,
statusTip="Save a picture of your canvas",
@@ -148,7 +150,6 @@ class MenuBar(QtWidgets.QMenuBar):
fileMenu.addAction(exitAction)
viewMenu = self.addMenu("View")
viewMenu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
findAction = QtGui.QAction("&Find",
self,
@@ -237,7 +238,6 @@ class MenuBar(QtWidgets.QMenuBar):
toolbarVisibilityMenu.addAction(self.primaryToolbarVisibilityAction)
nodeOperationsMenu = self.addMenu("Node Operations")
nodeOperationsMenu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
actionSelectAllNodes = QtGui.QAction('Select All Nodes',
self,
@@ -339,7 +339,6 @@ class MenuBar(QtWidgets.QMenuBar):
nodeOperationsMenu.addAction(detectCyclesAction)
projectMenu = self.addMenu("Project Operations")
projectMenu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
generateReportAction = QtGui.QAction("Generate Report",
self,
statusTip="Generate a report from the set of currently selected nodes.",
@@ -353,7 +352,6 @@ class MenuBar(QtWidgets.QMenuBar):
projectMenu.addAction(queryAction)
modulesMenu = self.addMenu("Modules")
modulesMenu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
reloadModulesAction = QtGui.QAction("Reload Modules", self,
statusTip="Reload all Entities and Transforms from Modules",
@@ -361,7 +359,6 @@ class MenuBar(QtWidgets.QMenuBar):
modulesMenu.addAction(reloadModulesAction)
serverMenu = self.addMenu("&Server")
serverMenu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
connectAction = QtGui.QAction("Connect", self,
statusTip="Connect to a Server",
@@ -541,22 +538,26 @@ class MenuBar(QtWidgets.QMenuBar):
self.parent().LENTDB.addEntity(existingEntity, updateTimeline=False)
elif importDialog.CSVFileChoice.isChecked():
csvDF = pd.read_csv(fileDirectory)
try:
csvDF = pd.read_excel(fileDirectory)
except ValueError:
csvDF = pd.read_csv(fileDirectory)
# If we have no rows or columns, we cannot import anything.
# This is essentially a sanity check.
if len(csvDF.index) < 1:
raise ValueError("Invalid import file data - Not enough rows.")
if len(csvDF.columns) < 1:
raise ValueError("Invalid import file data - Not enough columns.")
# Remove duplicate column names
# When importing, all columns should be given unique values,
# so this should be just another sanity check.
csvDF = csvDF.loc[:, ~csvDF.columns.duplicated()]
# Fill NaN values with an empty string
csvDF.fillna('')
# If we have less than 2 rows, we cannot import
rowNumber = len(csvDF.index)
if rowNumber < 2:
raise ValueError("Invalid CSV file data - Not enough rows.")
if len(csvDF.columns) < 1:
raise ValueError("Invalid CSV file data - Not enough columns.")
importEntityCSVDialog = ImportEntityFromCSVFile(self, csvDF)
if importEntityCSVDialog.exec_():
attributeRows = [comboBox.currentText()
@@ -588,22 +589,26 @@ class MenuBar(QtWidgets.QMenuBar):
self.parent().LENTDB.addEntity(existingEntity, updateTimeline=False)
elif importDialog.CSVFileChoiceLinks.isChecked():
csvDF = pd.read_csv(fileDirectory)
try:
csvDF = pd.read_excel(fileDirectory)
except ValueError:
csvDF = pd.read_csv(fileDirectory)
# If we have no rows or columns, we cannot import anything.
# This is essentially a sanity check.
if len(csvDF.index) < 1:
raise ValueError("Invalid import file data - Not enough rows.")
if len(csvDF.columns) < 1:
raise ValueError("Invalid import file data - Not enough columns.")
# Remove duplicate column names
# When importing, all columns should be given unique values,
# so this should be just another sanity check.
csvDF = csvDF.loc[:, ~csvDF.columns.duplicated()]
# Fill NaN values with an empty string
csvDF.fillna('')
# If we have less than 2 rows, we cannot import
rowNumber = len(csvDF.index)
if rowNumber < 2:
raise ValueError("Invalid CSV file data - Not enough rows.")
if len(csvDF.columns) < 1:
raise ValueError("Invalid CSV file data - Not enough columns.")
importLinksCSVDialog = ImportLinksFromCSVFile(self, csvDF)
if importLinksCSVDialog.exec_():
unmapped = []
@@ -1167,12 +1172,33 @@ class MenuBar(QtWidgets.QMenuBar):
steps = 3
progress = QtWidgets.QProgressDialog('Importing tabs, please wait...',
'Abort Import', 0, steps, self)
progress.setWindowModality(QtCore.Qt.WindowModal)
progress.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
progress.setMinimumDuration(0)
importTabsThread.progressSignal.connect(progress.setValue)
progress.canceled.connect(lambda: importTabsThread.cancelOperation())
importTabsThread.start()
def importFromTORBrowser(self) -> None:
"""
Import TOR session tabs to canvas.
:return:
"""
importDialog = TORBrowserImportDialog(self)
if importDialog.exec_():
importTorTabsThread = ImportTorBrowserTabsThread(importDialog, self.parent(), self)
steps = 4
progress = QtWidgets.QProgressDialog('Importing tabs, please wait...',
'Abort Import', 0, steps, self)
progress.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
progress.setMinimumDuration(0)
importTorTabsThread.progressSignal.connect(progress.setValue)
progress.canceled.connect(lambda: importTorTabsThread.cancelOperation())
importTorTabsThread.start()
def firefoxCookiesHelper(self, cookiesDatabasePath: Path) -> list:
"""
Used by threads to get firefox's cookies.
@@ -1225,107 +1251,25 @@ class MenuBar(QtWidgets.QMenuBar):
cookieHash.update(chunk)
return cookieHash.digest()
def importBrowserTabsFindings(self, resolution_result: list, importToCanvas: Union[bool, None] = None,
canvasToImportTo: Union[str, None] = None) -> None:
# See the function 'facilitateResolution' in CentralPane for guidance.
# Get all the entities, then split it into several lists, to make searching & iterating through them faster.
allEntities = [(entity['uid'], (entity[list(entity)[1]], entity['Entity Type']))
for entity in self.parent().LENTDB.getAllEntities()]
if allEntities:
allEntityUIDs, allEntityPrimaryFieldsAndTypes = map(list, zip(*allEntities))
else:
allEntityUIDs = []
allEntityPrimaryFieldsAndTypes = []
allLinks = [linkUID['uid'] for linkUID in self.parent().LENTDB.getAllLinks()]
links = []
newNodeUIDs = []
for resultList in resolution_result:
newNodeJSON = resultList[0]
newNodeEntityType = newNodeJSON['Entity Type']
# Cannot assume proper order of dicts sent over the net.
newNodePrimaryFieldKey = self.parent().RESOURCEHANDLER.getPrimaryFieldForEntityType(newNodeEntityType)
newNodePrimaryField = newNodeJSON[newNodePrimaryFieldKey]
try:
# Attempt to get the index of an existing entity that shares primary field and type with the new
# entity. Those two entities are considered to be referring to the same thing.
newNodeExistsIndex = allEntityPrimaryFieldsAndTypes.index((newNodePrimaryField, newNodeEntityType))
# If entity already exists, update the fields and re-add
newNodeExistingUID = allEntityUIDs[newNodeExistsIndex]
existingEntityJSON = self.parent().LENTDB.getEntity(newNodeExistingUID)
# Remove primary field and entity type, since those are duplicates. Primary field is the first element.
del newNodeJSON['Entity Type']
del newNodeJSON[newNodePrimaryFieldKey]
try:
notesField = newNodeJSON.pop('Notes')
existingEntityJSON['Notes'] += f'\n{notesField}'
except KeyError:
# If no new field was actually added to the entity, don't re-add to the database
if len(newNodeJSON) == 0:
newNodeUIDs.append(newNodeExistingUID)
continue
# Remove any 'None' values from new nodes - we want to keep all collected info.
for potentiallyNoneKey, potentiallyNoneValue in dict(newNodeJSON).items():
if potentiallyNoneValue is None or potentiallyNoneValue == 'None':
del newNodeJSON[potentiallyNoneKey]
# Update old values to new ones, and add new ones where applicable.
existingEntityJSON.update(newNodeJSON)
self.parent().LENTDB.addEntity(existingEntityJSON, fromServer=True, updateTimeline=False)
newNodeUIDs.append(newNodeExistingUID)
except ValueError:
# If there is no index for which the primary field and entity type of the new node match one of the
# existing ones, the node must indeed be new. We add it here.
entityJson = self.parent().LENTDB.addEntity(newNodeJSON, fromServer=True, updateTimeline=False)
newNodeUIDs.append(entityJson['uid'])
# Ensure that different entities involved in the resolution can't independently
# create the same new entities.
allEntityUIDs.append(entityJson['uid'])
allEntityPrimaryFieldsAndTypes.append((newNodePrimaryField, newNodeEntityType))
for parentsDictHolder, outputEntityUID in zip(resolution_result, newNodeUIDs):
if len(parentsDictHolder) > 1:
parentsDict = parentsDictHolder[1]
for parentID in parentsDict:
parentUID = parentID
if isinstance(parentUID, int):
parentUID = newNodeUIDs[parentUID]
# Sanity check: Check that the node that was used for this resolution still exists.
if parentUID in allEntityUIDs:
resolutionName = parentsDict[parentID]['Resolution']
newLinkUID = (parentUID, outputEntityUID)
# Avoid creating more links between the same two entities.
if newLinkUID in allLinks:
linkJson = self.parent().LENTDB.getLinkIfExists(newLinkUID)
if resolutionName not in linkJson['Notes']:
linkJson['Notes'] += f'\nConnection also produced by Resolution: {resolutionName}'
self.parent().LENTDB.addLink(linkJson, fromServer=True)
else:
self.parent().LENTDB.addLink({'uid': newLinkUID, 'Resolution': resolutionName,
'Notes': parentsDict[parentID].get('Notes', '')},
fromServer=True)
links.append((parentUID, outputEntityUID, resolutionName))
allLinks.append(newLinkUID)
self.parent().syncDatabase()
if importToCanvas:
def importBrowserTabsFindings(self, resolution_result: list, canvasToImportTo: Union[str, None] = None) -> None:
for finding in resolution_result:
if len(finding) < 2:
# Add dummy parents
finding.append({'@^@^@^@': {'Resolution': 'Browser Import', 'Notes': ''}})
newNodeUIDs = self.parent().centralWidget().tabbedPane.facilitateResolution('Importing Entities from Browser',
resolution_result)
if canvasToImportTo:
sceneToAddTo = self.parent().centralWidget().tabbedPane.getSceneByName(canvasToImportTo)
for newNodeUID in newNodeUIDs:
if newNodeUID is not None and newNodeUID not in sceneToAddTo.sceneGraph.nodes:
sceneToAddTo.addNodeProgrammatic(newNodeUID)
sceneToAddTo.rearrangeGraph()
self.parent().centralWidget().tabbedPane.addLinksToTabs(links, "Browser Import")
self.parent().LENTDB.resetTimeline()
self.parent().saveProject()
self.parent().MESSAGEHANDLER.info('Imported tabs from browser successfully.')
class DeleteProjectConfirmationDialog(QtWidgets.QDialog):
def __init__(self, mainWindowObject, currentServerProject: str):
super(DeleteProjectConfirmationDialog, self).__init__()
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
self.setModal(True)
self.setLayout(QtWidgets.QVBoxLayout())
self.mainWindowObject = mainWindowObject
@@ -1334,7 +1278,7 @@ class DeleteProjectConfirmationDialog(QtWidgets.QDialog):
resolutionsLabel = QtWidgets.QLabel(f'Delete Project: "{currentServerProject}" ?')
resolutionsLabel.setWordWrap(True)
resolutionsLabel.setAlignment(QtCore.Qt.AlignCenter)
resolutionsLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.layout().addWidget(resolutionsLabel)
buttonsWidget = QtWidgets.QWidget()
@@ -1371,9 +1315,7 @@ class BrowserImportDialog(QtWidgets.QDialog):
firefoxGroupLayout = QtWidgets.QVBoxLayout()
firefoxGroup.setLayout(firefoxGroupLayout)
self.firefoxChoice = QtWidgets.QCheckBox('Get tabs from Firefox')
self.firefoxChoice.setStyleSheet(Stylesheets.CHECK_BOX_STYLESHEET)
self.firefoxSessionChoice = QtWidgets.QCheckBox('Get entire session instead of latest tabs')
self.firefoxSessionChoice.setStyleSheet(Stylesheets.CHECK_BOX_STYLESHEET)
firefoxGroupLayout.addWidget(self.firefoxChoice)
firefoxGroupLayout.addWidget(self.firefoxSessionChoice)
@@ -1381,18 +1323,15 @@ class BrowserImportDialog(QtWidgets.QDialog):
chromeGroupLayout = QtWidgets.QVBoxLayout()
chromeGroup.setLayout(chromeGroupLayout)
self.chromeChoice = QtWidgets.QCheckBox('Get tabs from Chrome / Chromium (Experimental)')
self.chromeChoice.setStyleSheet(Stylesheets.CHECK_BOX_STYLESHEET)
chromeGroupLayout.addWidget(self.chromeChoice)
dialogLayout.addWidget(firefoxGroup, 1, 0, 1, 2)
dialogLayout.addWidget(chromeGroup, 2, 0, 1, 2)
self.importScreenshotsCheckbox = QtWidgets.QCheckBox('Take screenshots of sites')
self.importScreenshotsCheckbox.setStyleSheet(Stylesheets.CHECK_BOX_STYLESHEET)
dialogLayout.addWidget(self.importScreenshotsCheckbox, 3, 0, 1, 2)
self.importToCanvasCheckbox = QtWidgets.QCheckBox('Import To Canvas:')
self.importToCanvasCheckbox.setStyleSheet(Stylesheets.CHECK_BOX_STYLESHEET)
self.importToCanvasDropdown = QtWidgets.QComboBox()
self.importToCanvasDropdown.addItems(list(parent.parent().centralWidget().tabbedPane.canvasTabs))
self.importToCanvasDropdown.setEditable(False)
@@ -1419,6 +1358,39 @@ class BrowserImportDialog(QtWidgets.QDialog):
dialogLayout.addWidget(acceptButton, 5, 1, 1, 1)
class TORBrowserImportDialog(QtWidgets.QDialog):
def __init__(self, parent):
super(TORBrowserImportDialog, self).__init__(parent=parent)
self.setWindowTitle('Import TOR Browser Tabs')
self.setModal(True)
dialogLayout = QtWidgets.QGridLayout()
self.setLayout(dialogLayout)
self.entireSessionChoice = QtWidgets.QCheckBox('Get entire session instead of latest tabs')
dialogLayout.addWidget(self.entireSessionChoice, 0, 0, 1, 2)
self.importToCanvasCheckbox = QtWidgets.QCheckBox('Import To Canvas:')
self.importToCanvasDropdown = QtWidgets.QComboBox()
self.importToCanvasDropdown.addItems(list(parent.parent().centralWidget().tabbedPane.canvasTabs))
self.importToCanvasDropdown.setEditable(False)
self.importToCanvasDropdown.setDisabled(True)
self.importToCanvasCheckbox.toggled.connect(lambda: self.importToCanvasDropdown.setDisabled(
self.importToCanvasDropdown.isEnabled()))
dialogLayout.addWidget(self.importToCanvasCheckbox, 1, 0, 1, 2)
dialogLayout.addWidget(self.importToCanvasDropdown, 2, 0, 1, 2)
acceptButton = QtWidgets.QPushButton('Accept')
acceptButton.setAutoDefault(True)
acceptButton.setDefault(True)
cancelButton = QtWidgets.QPushButton('Cancel')
acceptButton.clicked.connect(self.accept)
cancelButton.clicked.connect(self.reject)
acceptButton.setFocus()
dialogLayout.addWidget(cancelButton, 4, 0, 1, 1)
dialogLayout.addWidget(acceptButton, 4, 1, 1, 1)
class ServerConnectWizard(QtWidgets.QDialog):
"""
Dialog Window that lets the user input the details of the server that
@@ -1441,7 +1413,7 @@ class ServerConnectWizard(QtWidgets.QDialog):
serverPasswordLabel = QtWidgets.QLabel("Server Password:")
self.serverPasswordTextbox = QtWidgets.QLineEdit()
self.serverPasswordTextbox.setEchoMode(QtWidgets.QLineEdit.Password)
self.serverPasswordTextbox.setEchoMode(QtWidgets.QLineEdit.EchoMode.Password)
self.layout().addRow(serverPasswordLabel, self.serverPasswordTextbox)
self.confirmConnect = False
@@ -1480,7 +1452,7 @@ class ServerCreateOrOpenProject(QtWidgets.QDialog):
self.openProjectDropdown.addItems(serverProjects)
self.openProjectPassword = QtWidgets.QLineEdit('')
self.openProjectPassword.setToolTip('Enter the password of the selected server project.')
self.openProjectPassword.setEchoMode(QtWidgets.QLineEdit.Password)
self.openProjectPassword.setEchoMode(QtWidgets.QLineEdit.EchoMode.Password)
openProjectLayout.addRow('Open Project:', self.openProjectDropdown)
openProjectLayout.addRow('Password:', self.openProjectPassword)
openProjectButton = QtWidgets.QPushButton('Open Project')
@@ -1494,7 +1466,7 @@ class ServerCreateOrOpenProject(QtWidgets.QDialog):
self.createProjectNameTextbox = QtWidgets.QLineEdit('')
self.createProjectNameTextbox.setToolTip('Specify the name of the server project. Must be unique.')
self.createProjectPasswordTextbox = QtWidgets.QLineEdit('')
self.createProjectPasswordTextbox.setEchoMode(QtWidgets.QLineEdit.Password)
self.createProjectPasswordTextbox.setEchoMode(QtWidgets.QLineEdit.EchoMode.Password)
self.createProjectPasswordTextbox.setToolTip('Specify a password to be entered'
' in order to access this project.')
@@ -1531,14 +1503,13 @@ class ViewAndStopResolutionsDialog(QtWidgets.QDialog):
def __init__(self, mainWindowObject):
super(ViewAndStopResolutionsDialog, self).__init__()
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
self.setModal(True)
self.setLayout(QtWidgets.QVBoxLayout())
self.mainWindowObject = mainWindowObject
resolutionsLabel = QtWidgets.QLabel('Running Resolutions:')
resolutionsLabel.setAlignment(QtCore.Qt.AlignCenter)
resolutionsLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.layout().addWidget(resolutionsLabel)
scrollArea = QtWidgets.QScrollArea()
@@ -1566,7 +1537,6 @@ class ViewAndStopResolutionsDialogOption(QtWidgets.QPushButton):
def __init__(self, resolutionThread, fromServer, mainWindowObject):
super(ViewAndStopResolutionsDialogOption, self).__init__()
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
self.mainWindowObject = mainWindowObject
self.resolutionThread = resolutionThread
self.fromServer = fromServer
@@ -1588,14 +1558,13 @@ class CollectorsDialog(QtWidgets.QDialog):
def __init__(self, mainWindow, collectorsDict: dict = None, runningCollectors: dict = None):
super(CollectorsDialog, self).__init__()
self.mainWindow = mainWindow
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
self.baseLayout = QtWidgets.QVBoxLayout()
self.setLayout(self.baseLayout)
self.setModal(True)
self.runningCollectorTreeItems = {}
collectorsLabel = QtWidgets.QLabel("Collectors")
collectorsLabel.setAlignment(QtCore.Qt.AlignCenter)
collectorsLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.baseLayout.addWidget(collectorsLabel)
if collectorsDict is None:
@@ -1625,9 +1594,10 @@ class CollectorsDialog(QtWidgets.QDialog):
newCollectorInstanceTree = QtWidgets.QTreeWidget()
newCollectorInstanceTree.setColumnCount(2)
newCollectorInstanceTree.setHeaderLabels(['UID', 'Stop Button'])
newCollectorInstanceTree.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
newCollectorInstanceTree.setSelectionBehavior(
QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows)
newCollectorInstanceTree.header().setStretchLastSection(False)
newCollectorInstanceTree.header().setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch)
newCollectorInstanceTree.header().setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeMode.Stretch)
newCollectorWidgetLayout.addWidget(newCollectorInstanceTree, 1, 0, 2, 2)
if runningCollectors is not None:
@@ -1686,7 +1656,6 @@ class CollectorStartDialog(QtWidgets.QDialog):
def __init__(self, entityDB, collectorDict: dict):
super(CollectorStartDialog, self).__init__()
self.entityDB = entityDB
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
self.setModal(True)
self.setWindowTitle('Collector Wizard')
self.parametersList = []
@@ -1710,16 +1679,16 @@ class CollectorStartDialog(QtWidgets.QDialog):
entitySelectTabLabel.setWordWrap(True)
entitySelectTabLabel.setMaximumWidth(600)
entitySelectTabLabel.setAlignment(QtCore.Qt.AlignCenter)
entitySelectTabLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
entitySelectTab.layout().addWidget(entitySelectTabLabel)
self.entitySelector = QtWidgets.QTreeWidget()
self.entitySelector.setHeaderLabels(['Primary Field', 'Entity Type', 'Icon'])
self.entitySelector.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
self.entitySelector.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows)
self.entitySelector.setSortingEnabled(True)
# Stretch the first column, since it contains the primary field.
self.entitySelector.header().setStretchLastSection(False)
self.entitySelector.header().setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch)
self.entitySelector.header().setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeMode.Stretch)
relevantEntityFields = [(entity['uid'], entity[list(entity)[1]], entity['Entity Type'], entity['Icon'])
for entity in entityDB.getAllEntities()
if entity['Entity Type'] in originTypes or '*' in originTypes]
@@ -1733,7 +1702,7 @@ class CollectorStartDialog(QtWidgets.QDialog):
# Hidden, so we can pull the UID later.
newTreeWidgetItem.setText(3, eligibleEntity[0])
self.entitySelector.setSelectionMode(self.entitySelector.MultiSelection)
self.entitySelector.setSelectionMode(self.entitySelector.SelectionMode.MultiSelection)
entitySelectTab.layout().addWidget(self.entitySelector)
self.childWidget.addTab(entitySelectTab, 'Entities')
@@ -1748,7 +1717,7 @@ class CollectorStartDialog(QtWidgets.QDialog):
propertyLabel.setWordWrap(True)
propertyLabel.setMaximumWidth(600)
propertyLabel.setAlignment(QtCore.Qt.AlignCenter)
propertyLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
propertyKeyLayout.addWidget(propertyLabel)
propertyType = parameters[key].get('type')
@@ -1769,7 +1738,6 @@ class CollectorStartDialog(QtWidgets.QDialog):
if propertyInputField is not None:
propertyKeyLayout.addWidget(propertyInputField)
propertyInputField.setStyleSheet(Stylesheets.CHECK_BOX_STYLESHEET)
propertyKeyLayout.setStretch(1, 1)
@@ -1777,18 +1745,14 @@ class CollectorStartDialog(QtWidgets.QDialog):
self.parametersList.append((key, propertyInputField))
nextButton = QtWidgets.QPushButton('Next')
nextButton.setStyleSheet(Stylesheets.BUTTON_STYLESHEET_2)
nextButton.clicked.connect(self.nextTab)
previousButton = QtWidgets.QPushButton('Previous')
previousButton.setStyleSheet(Stylesheets.BUTTON_STYLESHEET_2)
previousButton.clicked.connect(self.previousTab)
acceptButton = QtWidgets.QPushButton('Accept')
acceptButton.setAutoDefault(True)
acceptButton.setDefault(True)
acceptButton.setStyleSheet(Stylesheets.BUTTON_STYLESHEET_2)
acceptButton.clicked.connect(self.accept)
cancelButton = QtWidgets.QPushButton('Cancel')
cancelButton.setStyleSheet(Stylesheets.BUTTON_STYLESHEET_2)
cancelButton.clicked.connect(self.reject)
dialogLayout.addWidget(previousButton, 4, 0, 1, 1)
@@ -1829,7 +1793,6 @@ class ImportLinksFromCSVFile(QtWidgets.QDialog):
def __init__(self, parent, csvTableContents: pd.DataFrame):
super(ImportLinksFromCSVFile, self).__init__(parent=parent)
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
self.setModal(True)
importLayout = QtWidgets.QVBoxLayout()
self.setLayout(importLayout)
@@ -1838,7 +1801,7 @@ class ImportLinksFromCSVFile(QtWidgets.QDialog):
columnNumber = len(csvTableContents.columns)
titleLabel = QtWidgets.QLabel("Import Links from CSV")
titleLabel.setAlignment(QtCore.Qt.AlignCenter)
titleLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
descLabel = QtWidgets.QLabel("Select the Entity Type of the Parent entity (Entity One) and the Entity Type of "
"the Child entity (Entity Two). Then, map these entities to columns in the CSV "
@@ -1894,7 +1857,7 @@ class ImportLinksFromCSVFile(QtWidgets.QDialog):
rowValues = list(row)
for column in range(columnNumber):
columnItem = QtWidgets.QTableWidgetItem(str(rowValues[column + 1]))
columnItem.setFlags(columnItem.flags() & ~QtCore.Qt.ItemIsEditable)
columnItem.setFlags(columnItem.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable)
csvTable.setItem(rowValues[0], column, columnItem)
randomizationLabel = QtWidgets.QLabel("If the resolution identifiers (i.e. 'Resolution ID') are not guaranteed "
@@ -1903,7 +1866,7 @@ class ImportLinksFromCSVFile(QtWidgets.QDialog):
"have random tokens as the Resolution IDs.\nPlease select what you would "
"like to do:")
randomizationLabel.setWordWrap(True)
randomizationLabel.setAlignment(QtCore.Qt.AlignCenter)
randomizationLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.randAsIs = QtWidgets.QRadioButton("Use 'Resolution ID' as-is.")
self.randMerge = QtWidgets.QRadioButton("Append a random token to the values mapped to the 'Resolution ID' "
"field.")
@@ -1969,7 +1932,6 @@ class ImportLinkEntitiesFromCSVFile(QtWidgets.QDialog):
def __init__(self, parent, csvTableContents: pd.DataFrame):
super(ImportLinkEntitiesFromCSVFile, self).__init__(parent=parent)
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
self.setModal(True)
importLayout = QtWidgets.QVBoxLayout()
self.setLayout(importLayout)
@@ -1978,7 +1940,7 @@ class ImportLinkEntitiesFromCSVFile(QtWidgets.QDialog):
columnNumber = len(csvTableContents.columns)
titleLabel = QtWidgets.QLabel("Create Entities from Link Fields")
titleLabel.setAlignment(QtCore.Qt.AlignCenter)
titleLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
descLabel = QtWidgets.QLabel("Select the entity type to import the remaining fields as. "
"Then, configure the mapping between the CSV fields and entity attributes. To do "
@@ -2028,7 +1990,7 @@ class ImportLinkEntitiesFromCSVFile(QtWidgets.QDialog):
rowValues = list(row)
for column in range(columnNumber):
columnItem = QtWidgets.QTableWidgetItem(str(rowValues[column + 1]))
columnItem.setFlags(columnItem.flags() & ~QtCore.Qt.ItemIsEditable)
columnItem.setFlags(columnItem.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable)
csvTable.setItem(rowValues[0], column, columnItem)
buttonsWidget = QtWidgets.QWidget()
@@ -2082,7 +2044,6 @@ class ImportEntityFromCSVFile(QtWidgets.QDialog):
def __init__(self, parent, csvTableContents: pd.DataFrame):
super(ImportEntityFromCSVFile, self).__init__(parent=parent)
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
self.setModal(True)
importLayout = QtWidgets.QVBoxLayout()
self.setLayout(importLayout)
@@ -2091,7 +2052,7 @@ class ImportEntityFromCSVFile(QtWidgets.QDialog):
columnNumber = len(csvTableContents.columns)
titleLabel = QtWidgets.QLabel("Import Entities from CSV")
titleLabel.setAlignment(QtCore.Qt.AlignCenter)
titleLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
descLabel = QtWidgets.QLabel("Select the entity type to import the entities from the CSV file as. "
"Then, configure the mapping between the CSV fields and entity attributes. To do "
@@ -2131,14 +2092,13 @@ class ImportEntityFromCSVFile(QtWidgets.QDialog):
rowValues = list(row)
for column in range(columnNumber):
columnItem = QtWidgets.QTableWidgetItem(str(rowValues[column + 1]))
columnItem.setFlags(columnItem.flags() & ~QtCore.Qt.ItemIsEditable)
columnItem.setFlags(columnItem.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable)
csvTable.setItem(rowValues[0], column, columnItem)
importToCanvasChoiceWidget = QtWidgets.QWidget()
importToCanvasChoiceLayout = QtWidgets.QHBoxLayout()
importToCanvasChoiceWidget.setLayout(importToCanvasChoiceLayout)
self.importToCanvasCheckbox = QtWidgets.QCheckBox('Import To Canvas:')
self.importToCanvasCheckbox.setStyleSheet(Stylesheets.CHECK_BOX_STYLESHEET)
self.importToCanvasDropdown = QtWidgets.QComboBox()
self.importToCanvasDropdown.addItems(list(parent.parent().centralWidget().tabbedPane.canvasTabs))
self.importToCanvasDropdown.setEditable(False)
@@ -2201,10 +2161,11 @@ class ImportEntityFromCSVFile(QtWidgets.QDialog):
class ImportFromFileDialog(QtWidgets.QDialog):
def popupFileDialog(self):
self.fileDirectory = QtWidgets.QFileDialog().getOpenFileName(parent=self, caption='Select File to Import From',
dir=str(Path.home()),
options=QtWidgets.QFileDialog.DontUseNativeDialog,
filter="CSV or txt (*.csv *.txt)")[0]
self.fileDirectory = QtWidgets.QFileDialog().getOpenFileName(
parent=self, caption='Select File to Import From',
dir=str(Path.home()),
options=QtWidgets.QFileDialog.Option.DontUseNativeDialog,
filter="Import File (*.csv *.txt *.xls *.xlsx *.ods)")[0]
if self.fileDirectory != '':
self.fileDirectoryLine.setText(self.fileDirectory)
@@ -2217,7 +2178,7 @@ class ImportFromFileDialog(QtWidgets.QDialog):
dialogLayout = QtWidgets.QGridLayout()
self.setLayout(dialogLayout)
descriptionLabel = QtWidgets.QLabel('Select the file to import Entities or Links from:')
descriptionLabel.setAlignment(QtCore.Qt.AlignCenter)
descriptionLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
descriptionLabel.setWordWrap(True)
dialogLayout.addWidget(descriptionLabel, 0, 0, 1, 2)
@@ -2226,16 +2187,13 @@ class ImportFromFileDialog(QtWidgets.QDialog):
self.fileDirectoryLine.setReadOnly(True)
fileChoiceLabel = QtWidgets.QLabel('Specify the type of the chosen file and what to import:')
fileChoiceLabel.setAlignment(QtCore.Qt.AlignCenter)
fileChoiceLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
fileChoiceLabel.setWordWrap(True)
self.textFileChoice = QtWidgets.QRadioButton('Text file - Entities Import')
self.textFileChoice.setStyleSheet(Stylesheets.RADIO_BUTTON_STYLESHEET)
self.textFileChoice.setChecked(True)
self.CSVFileChoice = QtWidgets.QRadioButton('CSV - Entities Import')
self.CSVFileChoice.setStyleSheet(Stylesheets.RADIO_BUTTON_STYLESHEET)
self.CSVFileChoiceLinks = QtWidgets.QRadioButton('CSV - Links Import')
self.CSVFileChoiceLinks.setStyleSheet(Stylesheets.RADIO_BUTTON_STYLESHEET)
self.CSVFileChoice = QtWidgets.QRadioButton('Spreadsheet / CSV - Entities Import')
self.CSVFileChoiceLinks = QtWidgets.QRadioButton('Spreadsheet / CSV - Links Import')
dialogLayout.addWidget(self.fileDirectoryLine, 1, 0, 1, 2)
dialogLayout.addWidget(self.fileDirectoryButton, 2, 0, 1, 2)
@@ -2272,7 +2230,7 @@ class ImportFromTextFileDialog(QtWidgets.QDialog):
dialogLayout = QtWidgets.QGridLayout()
self.setLayout(dialogLayout)
descriptionLabel = QtWidgets.QLabel('Importing entities from text file, one entity per line.')
descriptionLabel.setAlignment(QtCore.Qt.AlignCenter)
descriptionLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
descriptionLabel.setWordWrap(True)
dialogLayout.addWidget(descriptionLabel, 0, 0, 1, 2)
@@ -2299,7 +2257,7 @@ class ImportFromTextFileDialog(QtWidgets.QDialog):
textTable.setFixedWidth(450)
for lineIndex, lineValue in enumerate(fileContents):
columnItem = QtWidgets.QTableWidgetItem(lineValue)
columnItem.setFlags(columnItem.flags() & ~QtCore.Qt.ItemIsEditable)
columnItem.setFlags(columnItem.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable)
textTable.setItem(lineIndex, 0, columnItem)
textTable.setColumnWidth(0, 450)
textTable.setHorizontalHeaderLabels(['File Entities Preview'])
@@ -2307,7 +2265,6 @@ class ImportFromTextFileDialog(QtWidgets.QDialog):
dialogLayout.addWidget(textTable, 3, 0, 1, 2)
self.importToCanvasCheckbox = QtWidgets.QCheckBox('Import To Canvas:')
self.importToCanvasCheckbox.setStyleSheet(Stylesheets.CHECK_BOX_STYLESHEET)
self.importToCanvasDropdown = QtWidgets.QComboBox()
self.importToCanvasDropdown.addItems(list(parent.parent().centralWidget().tabbedPane.canvasTabs))
self.importToCanvasDropdown.setEditable(False)
@@ -2347,7 +2304,6 @@ class CanvasPictureDialog(QtWidgets.QDialog):
self.fileDirectory = ""
self.setWindowTitle('Save Canvas Picture')
self.setModal(True)
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
dialogLayout = QtWidgets.QGridLayout()
self.setLayout(dialogLayout)
@@ -2411,12 +2367,11 @@ class CanvasPictureDialog(QtWidgets.QDialog):
def popupFileDialog(self):
saveAsDialog = QtWidgets.QFileDialog()
saveAsDialog.setOption(QtWidgets.QFileDialog.DontUseNativeDialog, True)
saveAsDialog.setViewMode(QtWidgets.QFileDialog.List)
saveAsDialog.setOption(QtWidgets.QFileDialog.Option.DontUseNativeDialog, True)
saveAsDialog.setViewMode(QtWidgets.QFileDialog.ViewMode.List)
saveAsDialog.setNameFilter("Image (*.png)")
saveAsDialog.setAcceptMode(QtWidgets.QFileDialog.AcceptSave)
saveAsDialog.setAcceptMode(QtWidgets.QFileDialog.AcceptMode.AcceptSave)
saveAsDialog.setDirectory(str(Path.home()))
saveAsDialog.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
saveAsDialog.exec()
self.fileDirectory = saveAsDialog.selectedFiles()[0]
if self.fileDirectory != '':
@@ -2436,7 +2391,6 @@ class SearchEngineDialog(QtWidgets.QDialog):
super(SearchEngineDialog, self).__init__(parent=parent)
self.setWindowTitle('Search Engine Lookup')
self.setModal(True)
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
layout = QtWidgets.QVBoxLayout()
self.setLayout(layout)
@@ -2473,7 +2427,6 @@ class SearchImageEngineDialog(QtWidgets.QDialog):
super(SearchImageEngineDialog, self).__init__(parent=parent)
self.setWindowTitle('Image Search Engine Lookup')
self.setModal(True)
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
layout = QtWidgets.QVBoxLayout()
self.setLayout(layout)
@@ -2530,6 +2483,8 @@ class ScreenshotWebsiteThread(QtCore.QThread):
user_agent=user_agents['Firefox']['Linux'][0]
)
urlPath = Path.home() / '.mozilla' / 'firefox'
if not (urlPath / 'profiles.ini').exists():
urlPath = Path.home() / 'snap' / 'firefox' / 'common' / '.mozilla' / 'firefox'
else: # We already checked before that the platform is either 'Linux' or 'Windows'.
context = browser.new_context(
viewport={'width': 1920, 'height': 1080},
@@ -2621,6 +2576,8 @@ class SaveWebsiteThread(QtCore.QThread):
user_agent=user_agents['Firefox']['Linux'][0]
)
urlPath = Path.home() / '.mozilla' / 'firefox'
if not (urlPath / 'profiles.ini').exists():
urlPath = Path.home() / 'snap' / 'firefox' / 'common' / '.mozilla' / 'firefox'
else: # We already checked before that the platform is either 'Linux' or 'Windows'.
context = browser.new_context(
viewport={'width': 1920, 'height': 1080},
@@ -2697,6 +2654,8 @@ class ImportBrowserTabsThread(QtCore.QThread):
user_agent=user_agents['Firefox']['Linux'][0]
)
urlPath = Path.home() / '.mozilla' / 'firefox'
if not (urlPath / 'profiles.ini').exists():
urlPath = Path.home() / 'snap' / 'firefox' / 'common' / '.mozilla' / 'firefox'
else: # We already checked before that the platform is either 'Linux' or 'Windows'.
context = browser.new_context(
viewport={'width': 1920, 'height': 1080},
@@ -2722,12 +2681,13 @@ class ImportBrowserTabsThread(QtCore.QThread):
first = True
for browserEntry in browserTab['entries']:
url = browserEntry['url']
title = browserEntry.get('title', '')
if not url.startswith('about:'):
if first:
tabsToOpen.append((url, browserEntry['title'], True))
tabsToOpen.append((url, title, True))
first = False
else:
tabsToOpen.append((url, browserEntry['title'], False))
tabsToOpen.append((url, title, False))
else:
browserEntry = browserTab['entries'][browserTab['index'] - 1]
url = browserEntry['url']
@@ -2823,7 +2783,9 @@ class ImportBrowserTabsThread(QtCore.QThread):
[screenshotEntity,
{len(returnResults) + 1: {'Resolution': 'Screenshot of Tab',
'Notes': ''}}])
else:
newEntity = [{'Phrase': actualURL,
'Entity Type': 'Phrase'}]
if len(tabToOpen) == 3:
if historyMark != -1 and not tabToOpen[2]:
newEntity.append({historyMark: {'Resolution': 'Next Page'}})
@@ -2999,5 +2961,151 @@ class ImportBrowserTabsThread(QtCore.QThread):
progressValue = 3
self.progressSignal.emit(progressValue)
self.menuObject.browserTabsImportDoneSignalListener.emit(
returnResults, self.importDialog.importToCanvasCheckbox.isChecked(),
self.importDialog.importToCanvasDropdown.currentText())
returnResults,
self.importDialog.importToCanvasDropdown.currentText() if
self.importDialog.importToCanvasCheckbox.isChecked() else '')
class ImportTorBrowserTabsThread(QtCore.QThread):
progressSignal = QtCore.Signal(int)
cancelled = False
def __init__(self, importDialog: TORBrowserImportDialog, mainWindowObject, menuObject):
super(ImportTorBrowserTabsThread, self).__init__(parent=mainWindowObject)
self.importDialog = importDialog
self.mainWindow = mainWindowObject
self.menuObject = menuObject
def cancelOperation(self):
self.cancelled = True
def run(self) -> None:
progressValue = 1
self.progressSignal.emit(progressValue)
returnResults = []
torBrowserProfilePath = Path(self.mainWindow.SETTINGS.value("Program/TOR Profile Location", "/"))
tabsFilePath = torBrowserProfilePath / 'sessionstore-backups' / 'recovery.jsonlz4'
if not tabsFilePath.exists():
progressValue = 4
self.progressSignal.emit(progressValue)
self.menuObject.browserTabsImportDoneSignalListener.emit(
returnResults,
'')
return
if self.importDialog.entireSessionChoice.isChecked():
tabsToOpen = []
else:
tabsToOpen = set()
tabsBytes = tabsFilePath.read_bytes()
if tabsBytes[:8] == b'mozLz40\0':
tabsBytes = lz4.block.decompress(tabsBytes[8:])
tabsJson = json.loads(tabsBytes)
for browserWindow in tabsJson['windows']:
for browserTab in browserWindow['tabs']:
if self.importDialog.entireSessionChoice.isChecked():
first = True
for browserEntry in browserTab['entries']:
url = browserEntry['url']
title = browserEntry.get('title', '')
if not url.startswith('about:'):
if first:
tabsToOpen.append((url, title, True))
first = False
else:
tabsToOpen.append((url, title, False))
else:
browserEntry = browserTab['entries'][browserTab['index'] - 1]
url = browserEntry['url']
if not url.startswith('about:'):
tabsToOpen.add((url, browserEntry['title']))
cookiesDatabasePath = torBrowserProfilePath / 'cookies.sqlite'
browserCookies = self.menuObject.firefoxCookiesHelper(cookiesDatabasePath)
historyMark = -1
for tabToOpen in tabsToOpen:
if self.cancelled:
break
urlTitle = tabToOpen[1]
actualURL = tabToOpen[0]
decodedPath = parse.unquote(actualURL)
parsedURL = parse.urlparse(decodedPath)
urlPath = parsedURL.path
if parsedURL.scheme == 'file':
try:
mime = magic.Magic(mime=True)
pathType = mime.from_file(urlPath)
except FileNotFoundError:
continue
if 'application' in pathType:
newEntity = [{'Document Name': urlTitle,
'File Path': urlPath,
'Entity Type': 'Document'}]
elif 'image' in pathType:
newEntity = [{'Image Name': urlTitle,
'File Path': urlPath,
'Entity Type': 'Image'}]
elif 'video' in pathType:
newEntity = [{'Video Name': urlTitle,
'File Path': urlPath,
'Entity Type': 'Video'}]
elif 'archive' in pathType:
newEntity = [{'Archive Name': urlTitle,
'File Path': urlPath,
'Entity Type': 'Archive'}]
elif parsedURL.scheme.startswith('http'):
if '.onion' in actualURL:
newEntity = [{'Onion URL': actualURL,
'Entity Type': 'Onion Website'}]
else:
newEntity = [{'URL': actualURL,
'Entity Type': 'Website'}]
else:
newEntity = [{'Phrase': actualURL,
'Entity Type': 'Phrase'}]
if len(tabToOpen) == 3:
if historyMark != -1 and not tabToOpen[2]:
newEntity.append({historyMark: {'Resolution': 'Next Page'}})
historyMark = len(returnResults)
returnResults.append(newEntity)
progressValue = 3
self.progressSignal.emit(progressValue)
if self.cancelled:
self.mainWindow.statusBarSignalListener.emit('Cancelled importing entities from Browser.')
else:
newTabEntities = len(returnResults)
for cookie in browserCookies:
cookieParents = []
cookieURI = cookie['domain'] + cookie['path']
if cookieURI.startswith('.'):
cookieURI = cookieURI[1:]
for index, tabEntity in enumerate(returnResults[:newTabEntities]):
for key, value in tabEntity[0].items():
if key != 'Entity Type' and cookieURI in value:
cookieParents.append(index)
cookieEntity = cookie
cookieEntity['Phrase'] = f'Cookie {uuid4()}'
cookieEntity['Entity Type'] = 'Phrase'
cookieParentDict: dict = {}
for cookieParent in cookieParents:
cookieParentDict[cookieParent] = {'Resolution': 'Site Cookie', 'Notes': ''}
cookieResult = [cookieEntity, cookieParentDict]
returnResults.append(cookieResult)
progressValue = 4
self.progressSignal.emit(progressValue)
self.menuObject.browserTabsImportDoneSignalListener.emit(
returnResults,
self.importDialog.importToCanvasDropdown.currentText() if
self.importDialog.importToCanvasCheckbox.isChecked() else '')

View File

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

View File

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

View File

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

View File

@@ -7,8 +7,6 @@ from os import listdir
from pathlib import Path
from uuid import uuid4
from typing import Union
from shutil import move
from msgpack import dump, load
class ResolutionManager:
@@ -25,7 +23,6 @@ class ResolutionManager:
exceptionsCount = 0
for resolution in listdir(directory):
resolution = str(resolution)
resolutionCategory = "Uncategorized"
try:
if resolution.endswith('.py'):
resolutionName = resolution[:-3]
@@ -44,7 +41,7 @@ class ResolutionManager:
with contextlib.suppress(AttributeError):
resolutionCategory = resClassInst.category
if not isinstance(resolutionCategory, str):
raise AttributeError()
resolutionCategory = "Uncategorized"
if self.resolutions.get(resolutionCategory) is None:
self.resolutions[resolutionCategory] = {}
self.resolutions[resolutionCategory][resNameString] = {'name': resNameString,
@@ -153,6 +150,21 @@ class ResolutionManager:
return macroUID
def renameMacro(self, oldName: str, newName: str) -> bool:
if oldName != newName:
with self.mainWindow.macrosLock:
if newName in self.macros:
self.mainWindow.MESSAGEHANDLER.warning('The specified name already exists. '
'Macro names must be unique.',
popUp=True)
return False
if oldName not in self.macros:
self.mainWindow.MESSAGEHANDLER.error('Attempting to rename a nonexistent macro.', popUp=True)
return False
oldMacro = self.macros.pop(oldName)
self.macros[newName] = oldMacro
return True
def deleteMacro(self, macroUID: str) -> bool:
# We don't have to worry about running macros, because the details of the macro are saved in memory.
# We do however want to get the thread lock because of potential race conditions.
@@ -163,31 +175,9 @@ class ResolutionManager:
except KeyError:
return False
def getMacroFilePath(self):
return Path(self.mainWindow.SETTINGS.value("Project/BaseDir")) / "Project Files" / "Project Macros.lsmacros"
def loadMacros(self):
def loadMacros(self) -> None:
# Load AFTER we load resolutions.
macroFilePath = self.getMacroFilePath()
self.macros = self.mainWindow.SETTINGS.value("Program/Macros", {})
try:
with open(macroFilePath, "rb") as macroFile:
self.macros = load(macroFile)
except ValueError:
# If the Macros file is empty or contains invalid input, ignore it.
pass
except FileNotFoundError:
# Create new placeholder notes file if it doesn't exist.
try:
macroFilePath.touch(0o700, exist_ok=False)
self.mainWindow.MESSAGEHANDLER.info('Created new Macros file.')
except FileExistsError:
self.mainWindow.MESSAGEHANDLER.error('Race condition occurred while trying to create Macros file.')
def save(self):
macroFilePath = self.getMacroFilePath()
macroFilePathTmp = macroFilePath.with_suffix(f'{macroFilePath.suffix}.tmp')
with open(macroFilePathTmp, "wb") as macroFile:
dump(self.macros, macroFile)
move(macroFilePathTmp, macroFilePath)
def save(self) -> None:
self.mainWindow.SETTINGS.setGlobalValue("Program/Macros", self.macros)

View File

@@ -46,18 +46,13 @@ class ASNToCIDR:
cidrWithOutPrefix = split_string[0]
prefix = split_string[1]
index_of_child = len(returnResult)
returnResult.append([{'IP Address': cidrWithOutPrefix,
'Range': prefix,
'Entity Type': 'Network'},
{uid: {'Resolution': 'ASN to CIDR', 'Notes': ''}}])
returnResult.append(
[{'Phrase': network['description'], 'Entity Type': 'Phrase'},
{index_of_child: {'Resolution': 'CIDR Description', 'Notes': ''}}])
returnResult.append(
[{'Organization Name': network['source'], 'Entity Type': 'Organization'},
{index_of_child: {'Resolution': 'ASN Registry', 'Notes': ''}}])
returnResult.append(
[{'Company Name': network['maintainer'], 'Entity Type': 'Company'},
{index_of_child: {'Resolution': 'Company Name', 'Notes': ''}}])
returnResult.extend(([{'IP Address': cidrWithOutPrefix, 'Range': prefix, 'Entity Type': 'Network'},
{uid: {'Resolution': 'ASN to CIDR', 'Notes': ''}}],
[{'Phrase': network['description'], 'Entity Type': 'Phrase'},
{index_of_child: {'Resolution': 'CIDR Description', 'Notes': ''}}],
[{'Organization Name': network['source'], 'Entity Type': 'Organization'},
{index_of_child: {'Resolution': 'ASN Registry', 'Notes': ''}}],
[{'Company Name': network['maintainer'], 'Entity Type': 'Company'},
{index_of_child: {'Resolution': 'Company Name', 'Notes': ''}}]))
return returnResult

View File

@@ -49,7 +49,7 @@ class AffiliateCodesExtractor:
import re
returnResults = []
visitExternal = True if parameters['Visit External Links'] == 'Yes' else False
visitExternal = parameters['Visit External Links'] == 'Yes'
# Numbers less than zero are the same as zero, but we should try to prevent overflows.
try:
@@ -133,57 +133,55 @@ class AffiliateCodesExtractor:
linksInLinkHref = soupContents.find_all('link')
for tag in linksInLinkHref:
newLink = tag.get('href', None)
if newLink is not None:
if newLink.startswith('http'):
newLink = newLink.split('#')[0]
newDepth = depth - 1
if domain in newLink and newLink not in exploredDepth and newDepth > 0:
exploredDepth.add(newLink)
extractCodes(currentUID, newLink, newDepth)
if newLink is not None and newLink.startswith('http'):
newLink = newLink.split('#')[0]
newDepth = depth - 1
if domain in newLink and newLink not in exploredDepth and newDepth > 0:
exploredDepth.add(newLink)
extractCodes(currentUID, newLink, newDepth)
linksInAHref = soupContents.find_all('a')
for tag in linksInAHref:
newLink = tag.get('href', None)
if newLink is not None:
if newLink.startswith('http'):
newLink = newLink.split('#')[0]
newDepth = depth - 1
if domain in newLink:
redirLinks = redirectRegex.findall(newLink)
if 'redirect' in newLink and len(redirLinks) > 0:
newLink = str(urllib.parse.unquote(redirLinks[0]))[2:]
if newLink not in exploredForeign:
exploredForeign.add(newLink)
if visitExternal:
for _ in range(3):
try:
page.goto(newLink, wait_until="networkidle", timeout=10000)
GetAffiliateCodes(currentUID, page.url)
break
except TimeoutError:
pass
except Error:
break
else:
GetAffiliateCodes(currentUID, newLink)
else:
if newLink not in exploredDepth and newDepth > 0:
exploredDepth.add(newLink)
extractCodes(currentUID, newLink, newDepth)
elif newLink not in exploredForeign:
exploredForeign.add(newLink)
if visitExternal:
for _ in range(3):
try:
page.goto(newLink, wait_until="networkidle", timeout=10000)
GetAffiliateCodes(currentUID, page.url)
break
except TimeoutError:
pass
except Error:
break
if newLink is not None and newLink.startswith('http'):
newLink = newLink.split('#')[0]
newDepth = depth - 1
if domain in newLink:
redirLinks = redirectRegex.findall(newLink)
if 'redirect' in newLink and len(redirLinks) > 0:
newLink = str(urllib.parse.unquote(redirLinks[0]))[2:]
if newLink not in exploredForeign:
exploredForeign.add(newLink)
if visitExternal:
for _ in range(3):
try:
page.goto(newLink, wait_until="networkidle", timeout=10000)
GetAffiliateCodes(currentUID, page.url)
break
except TimeoutError:
pass
except Error:
break
else:
GetAffiliateCodes(currentUID, newLink)
else:
if newLink not in exploredDepth and newDepth > 0:
exploredDepth.add(newLink)
extractCodes(currentUID, newLink, newDepth)
elif newLink not in exploredForeign:
exploredForeign.add(newLink)
if visitExternal:
for _ in range(3):
try:
page.goto(newLink, wait_until="networkidle", timeout=10000)
GetAffiliateCodes(currentUID, page.url)
break
except TimeoutError:
pass
except Error:
break
else:
GetAffiliateCodes(currentUID, newLink)
with sync_playwright() as p:
browser = p.firefox.launch()
@@ -197,7 +195,7 @@ class AffiliateCodesExtractor:
if url is None:
continue
if not url.startswith('http://') and not url.startswith('https://'):
url = 'http://' + url
url = f'http://{url}'
domain = tldextract.extract(url).fqdn
extractCodes(uid, url, maxDepth)
browser.close()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -38,17 +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 = []
# 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()
@@ -92,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)
@@ -100,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()
@@ -133,7 +130,7 @@ class EmailExtractor:
if url is None:
continue
if not url.startswith('http://') and not url.startswith('https://'):
url = 'http://' + url
url = f'http://{url}'
extractEmails(uid, url)
browser.close()

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ class FileHasher:
name = "Get File Hash"
category = "File Operations"
description = "Get the Hash of a file."
originTypes = {"Image", "Document", "Video", "Archive", "Disk"}
originTypes = {"Image", "Document", "Spreadsheet", "Video", "Archive", "Disk"}
resultTypes = {'Hash'}
parameters = {'hashing_algorithms': {'description': 'The type of hash/es that will be returned',
'type': 'MultiChoice',
@@ -25,10 +25,10 @@ class FileHasher:
continue
block_size = 65536 # The size of each read from the file
for hashing_algorithm in hashing_algorithms:
if hashing_algorithm == "SHA256":
file_hash = hashlib.sha256() # nosec
elif hashing_algorithm == "SHA1":
if hashing_algorithm == "SHA1":
file_hash = hashlib.sha1() # nosec
elif hashing_algorithm == "SHA256":
file_hash = hashlib.sha256() # nosec
else:
file_hash = hashlib.md5() # nosec
with open(file_path, 'rb') as f:
@@ -40,5 +40,5 @@ class FileHasher:
return_result.append([{'Hash Value': resulting_hash,
'Hash Algorithm': hashing_algorithm,
'Entity Type': 'Hash'},
{uid: {'Resolution': hashing_algorithm + ' Hash', 'Notes': ''}}])
{uid: {'Resolution': f'{hashing_algorithm} Hash', 'Notes': ''}}])
return return_result

View File

@@ -83,13 +83,7 @@ class GetExternalURLs:
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}
else:
if domain in link:
redirectLinks = redirectRegex.findall(link)
if 'redirect' in link and len(redirectLinks) > 0:
try:
@@ -106,31 +100,35 @@ class GetExternalURLs:
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:
else:
newLink = link.split('#')[0].split('?')[0]
try:
externalUrls[newLink].add(uid)
except KeyError:
externalUrls[newLink] = {uid}
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]) 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}
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 +141,11 @@ class GetExternalURLs:
for externalUrl in externalUrls:
onionCheck = onionRegex.findall(externalUrl)
if len(onionCheck) == 1:
for urlUid in externalUrls[externalUrl]:
for urlUid in externalUrls[externalUrl]:
if len(onionCheck) == 1:
returnResult.append([{'Onion URL': externalUrl, 'Entity Type': 'Onion Website'},
{urlUid: {'Resolution': 'External Link', 'Notes': ''}}])
else:
for urlUid in externalUrls[externalUrl]:
else:
returnResult.append([{'URL': externalUrl, 'Entity Type': 'Website'},
{urlUid: {'Resolution': 'External Link', 'Notes': ''}}])

View File

@@ -27,9 +27,7 @@ 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, 'lxml')
@@ -59,7 +57,7 @@ class GetWebsiteText:
try:
page.goto(url, wait_until="networkidle", timeout=10000)
textContent = text_from_html(page.content())
returnResults.append([{'Phrase': 'Website Body of: ' + url,
returnResults.append([{'Phrase': f'Website Body of: {url}',
'Notes': textContent,
'Entity Type': 'Phrase'},
{uid: {'Resolution': 'Website Body', 'Notes': ''}}])

View File

@@ -17,7 +17,7 @@ class HostnameToDomain:
uid = entity['uid']
primary_field = entity[list(entity)[1]].strip()
tsd, td, tsu = extract(primary_field)
domain = td + '.' + tsu
domain = f'{td}.{tsu}'
if domain == primary_field:
continue
return_result.append([{'Domain Name': domain,

View File

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

View File

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

View File

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

View File

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

View File

@@ -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()
@@ -48,126 +50,127 @@ class JSCodeExtractor:
brightcoveRegex = re.compile(r'metrics\.brightcove\.com/.*/tracker\?.*&account=[^&]*')
def GetTrackingCodes(pageUid, requestUrl) -> None:
if requestUrl not in requestUrlsParsed:
requestUrlsParsed.add(requestUrl)
for uaCode in uaRegex.findall(requestUrl):
returnResults.append([{'Phrase': uaCode,
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Google UA Tracking Code',
'Notes': ''}}])
for pubCode in pubRegex.findall(requestUrl):
returnResults.append([{'Phrase': pubCode,
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Google AdSense ca-pub Tracking Code',
'Notes': ''}}])
for gtmCode in gtmRegex.findall(requestUrl):
returnResults.append([{'Phrase': gtmCode,
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Google GTM Tracking Code',
'Notes': ''}}])
for gCode in gRegex.findall(requestUrl):
returnResults.append([{'Phrase': gCode,
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Google G Tracking Code',
'Notes': ''}}])
for qCode in qualtricsRegex.findall(requestUrl):
returnResults.append([{'Phrase': qCode[6:],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Qualtrics Tracking Code',
'Notes': ''}}])
for pCode in pingdomRegex.findall(requestUrl):
returnResults.append([{'Phrase': pCode[:-3],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Pingdom Tracking Code',
'Notes': ''}}])
for mCode in mPulseRegex.findall(requestUrl):
returnResults.append([{'Phrase': mCode.split('/')[-1],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'mPulse Tracking Code',
'Notes': ''}}])
for cCode in contextWebRegex.findall(requestUrl):
returnResults.append([{'Phrase': cCode.split('token=')[1].split('&')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'ContextWeb Tracking Code',
'Notes': ''}}])
for fCode in facebookRegex.findall(requestUrl):
returnResults.append([{'Phrase': fCode.split('id=')[1].split('&')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Facebook Tracking Pixel Code',
'Notes': ''}}])
for mapsCode in googleMapsRegex.findall(requestUrl):
returnResults.append([{'Phrase': mapsCode.split('client=', 1)[1].split('&')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Google Maps Client Code',
'Notes': ''}}])
for marketoCode in marketoRegex.findall(requestUrl):
returnResults.append([{'Phrase': marketoCode.split('aid=')[1].split('&')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Marketo Tracking Code',
'Notes': ''}}])
for vwoCode in visualWebsiteOptimizerRegex.findall(requestUrl):
returnResults.append([{'Phrase': vwoCode.split('a=')[1].split('&')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Visual Website Optimizer Tracking User ID',
'Notes': ''}}])
for oCode in optimizeRegex.findall(requestUrl):
returnResults.append([{'Phrase': oCode.split('id=')[1].split('&')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Google Optimize ID',
'Notes': ''}}])
for mmCode in markMonitorRegex.findall(requestUrl):
returnResults.append([{'Phrase': mmCode.split('adv=')[1].split('&')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Mark Monitor Tracking ID',
'Notes': ''}}])
for zCode in zendeskRegex.findall(requestUrl):
returnResults.append([{'Phrase': zCode.split('key=')[1].split('&')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Zendesk ID',
'Notes': ''}}])
for qsCode in quantServeRegex.findall(requestUrl):
returnResults.append([{'Phrase': qsCode.split('/pixel/')[1].split('.gif')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'QuantServe Tracking Pixel ID',
'Notes': ''}}])
for clCode in cookieLawRegex.findall(requestUrl):
returnResults.append([{'Phrase': clCode.split('/')[2],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'CookieLaw Website ID',
'Notes': ''}}])
for otCode in oneTagRegex.findall(requestUrl):
returnResults.append([{'Phrase': otCode.split('/')[1],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'OneTag Tracking ID',
'Notes': ''}}])
for beCode in bounceExchangeRegex.findall(requestUrl):
returnResults.append([{'Phrase': beCode.split('/')[1],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'BounceExchange Tracking ID',
'Notes': ''}}])
for pushlyCode in pushlyRegex.findall(requestUrl):
returnResults.append([{'Phrase': pushlyCode.split('domain_key=')[1],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Pushly Website ID',
'Notes': ''}}])
for aCode in akamaiRegex.findall(requestUrl):
encodedTracking = aCode.split('a=', 1)[1]
encodedTracking = encodedTracking.replace('%3D', '=')
decodedTracking = b64decode(encodedTracking).decode('utf-8').split('t=')[1].split('&')[0]
returnResults.append([{'Phrase': decodedTracking,
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Akamai Website ID',
'Notes': 'SHA-1 Sum'}}])
for dCode in demdexRegex.findall(requestUrl):
returnResults.append([{'Phrase': dCode.split('d_orgid=')[1],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'DemDex (Adobe) Website ID',
'Notes': ''}}])
for bCode in brightcoveRegex.findall(requestUrl):
returnResults.append([{'Phrase': bCode.split('account=')[1],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'BrightCove Website ID',
'Notes': ''}}])
if requestUrl in requestUrlsParsed:
return
requestUrlsParsed.add(requestUrl)
for uaCode in uaRegex.findall(requestUrl):
returnResults.append([{'Phrase': uaCode,
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Google UA Tracking Code',
'Notes': ''}}])
for pubCode in pubRegex.findall(requestUrl):
returnResults.append([{'Phrase': pubCode,
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Google AdSense ca-pub Tracking Code',
'Notes': ''}}])
for gtmCode in gtmRegex.findall(requestUrl):
returnResults.append([{'Phrase': gtmCode,
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Google GTM Tracking Code',
'Notes': ''}}])
for gCode in gRegex.findall(requestUrl):
returnResults.append([{'Phrase': gCode,
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Google G Tracking Code',
'Notes': ''}}])
for qCode in qualtricsRegex.findall(requestUrl):
returnResults.append([{'Phrase': qCode[6:],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Qualtrics Tracking Code',
'Notes': ''}}])
for pCode in pingdomRegex.findall(requestUrl):
returnResults.append([{'Phrase': pCode[:-3],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Pingdom Tracking Code',
'Notes': ''}}])
for mCode in mPulseRegex.findall(requestUrl):
returnResults.append([{'Phrase': mCode.split('/')[-1],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'mPulse Tracking Code',
'Notes': ''}}])
for cCode in contextWebRegex.findall(requestUrl):
returnResults.append([{'Phrase': cCode.split('token=')[1].split('&')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'ContextWeb Tracking Code',
'Notes': ''}}])
for fCode in facebookRegex.findall(requestUrl):
returnResults.append([{'Phrase': fCode.split('id=')[1].split('&')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Facebook Tracking Pixel Code',
'Notes': ''}}])
for mapsCode in googleMapsRegex.findall(requestUrl):
returnResults.append([{'Phrase': mapsCode.split('client=', 1)[1].split('&')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Google Maps Client Code',
'Notes': ''}}])
for marketoCode in marketoRegex.findall(requestUrl):
returnResults.append([{'Phrase': marketoCode.split('aid=')[1].split('&')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Marketo Tracking Code',
'Notes': ''}}])
for vwoCode in visualWebsiteOptimizerRegex.findall(requestUrl):
returnResults.append([{'Phrase': vwoCode.split('a=')[1].split('&')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Visual Website Optimizer Tracking User ID',
'Notes': ''}}])
for oCode in optimizeRegex.findall(requestUrl):
returnResults.append([{'Phrase': oCode.split('id=')[1].split('&')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Google Optimize ID',
'Notes': ''}}])
for mmCode in markMonitorRegex.findall(requestUrl):
returnResults.append([{'Phrase': mmCode.split('adv=')[1].split('&')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Mark Monitor Tracking ID',
'Notes': ''}}])
for zCode in zendeskRegex.findall(requestUrl):
returnResults.append([{'Phrase': zCode.split('key=')[1].split('&')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Zendesk ID',
'Notes': ''}}])
for qsCode in quantServeRegex.findall(requestUrl):
returnResults.append([{'Phrase': qsCode.split('/pixel/')[1].split('.gif')[0],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'QuantServe Tracking Pixel ID',
'Notes': ''}}])
for clCode in cookieLawRegex.findall(requestUrl):
returnResults.append([{'Phrase': clCode.split('/')[2],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'CookieLaw Website ID',
'Notes': ''}}])
for otCode in oneTagRegex.findall(requestUrl):
returnResults.append([{'Phrase': otCode.split('/')[1],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'OneTag Tracking ID',
'Notes': ''}}])
for beCode in bounceExchangeRegex.findall(requestUrl):
returnResults.append([{'Phrase': beCode.split('/')[1],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'BounceExchange Tracking ID',
'Notes': ''}}])
for pushlyCode in pushlyRegex.findall(requestUrl):
returnResults.append([{'Phrase': pushlyCode.split('domain_key=')[1],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Pushly Website ID',
'Notes': ''}}])
for aCode in akamaiRegex.findall(requestUrl):
encodedTracking = aCode.split('a=', 1)[1]
encodedTracking = encodedTracking.replace('%3D', '=')
decodedTracking = b64decode(encodedTracking).decode('utf-8').split('t=')[1].split('&')[0]
returnResults.append([{'Phrase': decodedTracking,
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'Akamai Website ID',
'Notes': 'SHA-1 Sum'}}])
for dCode in demdexRegex.findall(requestUrl):
returnResults.append([{'Phrase': dCode.split('d_orgid=')[1],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'DemDex (Adobe) Website ID',
'Notes': ''}}])
for bCode in brightcoveRegex.findall(requestUrl):
returnResults.append([{'Phrase': bCode.split('account=')[1],
'Entity Type': 'Phrase'},
{pageUid: {'Resolution': 'BrightCove Website ID',
'Notes': ''}}])
with sync_playwright() as p:
browser = p.firefox.launch()
@@ -194,16 +197,12 @@ class JSCodeExtractor:
if url is None:
continue
if not url.startswith('http://') and not url.startswith('https://'):
url = 'http://' + url
url = f'http://{url}'
try:
with contextlib.suppress(Error):
pageJS.goto(url, wait_until="networkidle")
except Error:
pass
try:
with contextlib.suppress(Error):
pageNoJS.goto(url, wait_until="networkidle")
except Error:
pass
pageJS.close()
pageNoJS.close()
browser.close()

View File

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

View File

@@ -0,0 +1,38 @@
#!/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 = set()
string_checks.add(''.join(primaryField.split(' ')))
string_checks.add('_'.join(primaryField.split(' ')))
string_checks.add('-'.join(primaryField.split(' ')))
for check in string_checks:
check_url = url_base + check
request = requests.head(check_url, headers=headers)
if request.status_code == 200:
returnResults.append([{'URL': check_url,
'Entity Type': 'Website'},
{entity['uid']: {'Resolution': 'NPMJS Org',
'Notes': ''}}])
return returnResults

View File

@@ -55,12 +55,11 @@ class PhoneNumbersExtractor:
linksInAHref = soupContents.find_all('a')
for tag in linksInAHref:
newLink = tag.get('href', None)
if newLink is not None:
if newLink.startswith('tel:'):
returnResults.append([{'Phone Number': newLink[4:],
'Entity Type': 'Phone Number'},
{currentUID: {'Resolution': 'Phone Number Found',
'Notes': ''}}])
if newLink is not None and newLink.startswith('tel:'):
returnResults.append([{'Phone Number': newLink[4:],
'Entity Type': 'Phone Number'},
{currentUID: {'Resolution': 'Phone Number Found',
'Notes': ''}}])
textTags = soupContents.find_all('p')
for tag in textTags:
@@ -88,7 +87,7 @@ class PhoneNumbersExtractor:
if url is None:
continue
if not url.startswith('http://') and not url.startswith('https://'):
url = 'http://' + url
url = f'http://{url}'
extractTels(uid, url)
browser.close()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,10 +2,11 @@
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
@@ -19,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:
@@ -31,62 +30,53 @@ class ResourceHandler:
def __init__(self, mainWindow, messageHandler) -> None:
self.mainWindow = mainWindow
self.messageHandler = messageHandler
self.programBaseDirPath = Path(self.mainWindow.SETTINGS.value("Program/BaseDir"))
self.entityCategoryList = {}
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"""^(?:(?: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.loadCoreEntities()
@@ -208,7 +198,7 @@ class ResourceHandler:
self.entityCategoryList[category] = {}
self.entityCategoryList[category][entityName] = {
'Attributes': attributesDict,
'Icon': str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) / "Resources" / "Icons" / icon)}
'Icon': str(self.programBaseDirPath / "Resources" / "Icons" / icon)}
except (KeyError, AttributeError) as err:
# Ignore malformed entities
self.messageHandler.error(f'Error: {str(err)}', popUp=False)
@@ -216,13 +206,13 @@ class ResourceHandler:
return True
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'):
@@ -325,7 +315,7 @@ class ResourceHandler:
return linkJson
def getEntityDefaultPicture(self, entityType) -> QByteArray:
picture = Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) / "Resources" / "Icons" / "Default.svg"
picture = self.programBaseDirPath / "Resources" / "Icons" / "Default.svg"
try:
for category in self.entityCategoryList:
if entityType in self.entityCategoryList[category]:
@@ -342,11 +332,11 @@ class ResourceHandler:
return QByteArray(pictureContents)
def getLinkPicture(self):
picture = Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) / "Resources" / "Icons" / "Resolution.png"
picture = self.programBaseDirPath / "Resources" / "Icons" / "Resolution.png"
return QtGui.QIcon(str(picture)).pixmap(40, 40)
def getLinkArrowPicture(self):
picture = Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) / "Resources" / "Icons" / "Right-Arrow.svg"
picture = self.programBaseDirPath / "Resources" / "Icons" / "Right-Arrow.svg"
return QtGui.QIcon(str(picture)).pixmap(40, 40)
def deconstructGraph(self, graph: nx.DiGraph) -> tuple:
@@ -421,7 +411,7 @@ class FilePropertyInput(QtWidgets.QLineEdit):
fileChosen = self.fileDialog.getOpenFileName(self,
"Open File",
str(Path.home()),
options=QtWidgets.QFileDialog.DontUseNativeDialog)
options=QtWidgets.QFileDialog.Option.DontUseNativeDialog)
self.setText(fileChosen[0])
@@ -440,7 +430,6 @@ class SingleChoicePropertyInput(QtWidgets.QGroupBox):
for option in enforceOptionsSet:
radioButton = QtWidgets.QRadioButton(option)
radioButton.setStyleSheet(Stylesheets.RADIO_BUTTON_STYLESHEET)
if option == defaultOption:
radioButton.setChecked(True)
else:
@@ -467,7 +456,6 @@ class MultiChoicePropertyInput(QtWidgets.QGroupBox):
for option in enforceOptionsSet:
checkBox = QtWidgets.QCheckBox(option)
checkBox.setStyleSheet(Stylesheets.CHECK_BOX_STYLESHEET)
if option in defaultOptions:
checkBox.setChecked(True)
else:
@@ -488,6 +476,7 @@ class MinSizeStackedLayout(QtWidgets.QStackedLayout):
https://stackoverflow.com/a/34300567
"""
def sizeHint(self) -> QSize:
return self.currentWidget().sizeHint()
@@ -536,7 +525,7 @@ class RichNotesEditor(QtWidgets.QTextBrowser):
def mousePressEvent(self, ev: QtGui.QMouseEvent) -> None:
potentialLink = self.anchorAt(ev.pos())
if not potentialLink and ev.button() == QtGui.Qt.LeftButton:
if not potentialLink and ev.button() == QtGui.Qt.MouseButton.LeftButton:
self.startEditing()
super(RichNotesEditor, self).mousePressEvent(ev)

View File

@@ -4,6 +4,7 @@ from msgpack import dump
from shutil import move
from pathlib import Path
from Core.PathHelper import is_path_exists_or_creatable_portable
from PySide6.QtCore import QSettings
class SettingsObject(dict):
@@ -21,22 +22,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/Graphics/EntityTextFontType", "Mono")
self.setValue("Program/Graphics/EntityTextFontSize", "11")
self.setValue("Program/Graphics/EntityTextFontBoldness", "700")
self.setValue("Program/Graphics/LinkTextFontType", "Mono")
self.setValue("Program/Graphics/LinkTextFontSize", "11")
self.setValue("Program/Graphics/LinkTextFontBoldness", "700")
self.setValue("Program/Graphics/EntityTextColor", "#000000") # RGB
self.setValue("Program/Graphics/LinkTextColor", "#000000") # RGB
self.setValue("Program/Graphics/LabelFade", "3")
self.globalSettings = QSettings()
self.globalSettings.setValue("Program/Version", "v1.5.0")
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,20 +73,45 @@ 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):
@@ -67,7 +119,12 @@ class SettingsObject(dict):
with open(tempSavePath, "wb") as projectFile:
dump(self, projectFile)
move(tempSavePath, actualSavePath)
self.globalSettings.sync()
globalSettingsSavingError = self.globalSettings.status()
if globalSettingsSavingError != self.globalSettings.Status.NoError:
raise Exception(f'Could not save global settings: {globalSettingsSavingError}')
def load(self, savedDict: dict):
def load(self, savedDict: dict) -> None:
# No need to do anything with global settings.
for key in savedDict:
self[key] = savedDict[key]

View File

@@ -55,17 +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"}
elif fileType == "image":
entityJson = {"Image Name": urlName, "File Path": savePathString, "Entity Type": "Image"}
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):

View File

@@ -783,7 +783,7 @@ class InstallWizard(QtWidgets.QWizard):
self.graphvizExists = graphvizPath.exists()
self.baseSoftwarePath = Path(os.path.abspath(os.sep)) / 'Program Files' / 'LinkScope'
self.executablePath = self.baseSoftwarePath / 'LinkScope.exe'
self.downloadURL = downloadURLBase + "LinkScope-Windows-x64.7z"
self.downloadURL = f"{downloadURLBase}LinkScope-Windows-x64.7z"
newArgs = ['"' + str(self.desktopShortcutPath) + '"', str(self.graphvizExists),
'"' + str(self.baseSoftwarePath) + '"', '"' + str(self.executablePath) + '"',
@@ -804,7 +804,7 @@ class InstallWizard(QtWidgets.QWizard):
self.appPath = Path(
os.path.abspath(os.sep)) / 'usr' / 'share' / 'applications' / 'LinkScope.desktop'
self.executablePath = self.baseSoftwarePath / 'LinkScope'
self.downloadURL = downloadURLBase + "LinkScope-Ubuntu-x64.7z"
self.downloadURL = f"{downloadURLBase}LinkScope-Ubuntu-x64.7z"
# No need to wrap these in quotes
newArgs = [str(self.desktopShortcutPath), str(self.graphvizExists), str(self.baseSoftwarePath),
@@ -815,7 +815,7 @@ class InstallWizard(QtWidgets.QWizard):
sudoPassword = QtWidgets.QInputDialog.getText(self, 'Sudo Password',
'Installation requires elevated privileges. '
'Please enter your password: ',
QtWidgets.QLineEdit.Password)
QtWidgets.QLineEdit.EchoMode.Password)
if sudoPassword[1] and sudoPassword[0] != '':
sudoPrivs = subprocess.Popen(['sudo', '-S', '-H', '-k', sys.executable, *newArgs],
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
@@ -865,7 +865,7 @@ class InstallWizard(QtWidgets.QWizard):
sys.exit(-2)
self.appPath = Path(sys.argv[6])
self.setWizardStyle(self.ModernStyle)
self.setWizardStyle(self.WizardStyle.ModernStyle)
self.setWindowTitle('LinkScope Installer')
# Normally one would use enums to keep track of pages, but the installer crashes if we try, so we
@@ -888,8 +888,8 @@ class InstallWizard(QtWidgets.QWizard):
self.addPage(licensePage)
self.addPage(installUpgradePage)
self.addPage(DonePage())
self.setOptions(self.NoBackButtonOnStartPage | self.NoBackButtonOnLastPage | self.CancelButtonOnLeft |
self.NoCancelButtonOnLastPage)
self.setOptions(self.WizardOption.NoBackButtonOnStartPage | self.WizardOption.NoBackButtonOnLastPage |
self.WizardOption.CancelButtonOnLeft | self.WizardOption.NoCancelButtonOnLastPage)
self.show()
@@ -1000,7 +1000,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')
@@ -1034,7 +1034,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)
@@ -1082,7 +1082,7 @@ class LinkScopeInstallLatestPage(QtWidgets.QWizardPage):
self.installThread = None
self.installLabel.setWordWrap(True)
self.installLabel.setAlignment(QtCore.Qt.AlignCenter)
self.installLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
installLayout.addWidget(self.installLabel)
self.installProgressWidget = QtWidgets.QWidget()
@@ -1097,7 +1097,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)
@@ -1135,7 +1135,7 @@ class LinkScopeUninstallPage(QtWidgets.QWizardPage):
self.uninstallLabel = QtWidgets.QLabel('The installer will now uninstall LinkScope from this computer. Click '
'"Commit" to begin the removal process.')
self.uninstallLabel.setWordWrap(True)
self.uninstallLabel.setAlignment(QtCore.Qt.AlignCenter)
self.uninstallLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
uninstallLayout.addWidget(self.uninstallLabel)
self.uninstallProgressWidget = QtWidgets.QWidget()
@@ -1183,7 +1183,7 @@ class CreateDesktopShortcutPage(QtWidgets.QWizardPage):
desktopShortcutLayout = QtWidgets.QVBoxLayout()
self.setLayout(desktopShortcutLayout)
desktopShortcutLabel = QtWidgets.QLabel('Create a Shortcut for LinkScope on the Desktop?')
desktopShortcutLabel.setAlignment(QtCore.Qt.AlignCenter)
desktopShortcutLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.shortcutCheckbox = QtWidgets.QCheckBox('Create a Desktop Shortcut?')
self.shortcutCheckbox.setChecked(True)
@@ -1202,7 +1202,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)
@@ -1235,7 +1235,7 @@ class DonePage(QtWidgets.QWizardPage):
self.setLayout(doneLayout)
self.doneLabel = QtWidgets.QLabel('Thank you for using LinkScope!\nClick "Finish" to exit the installer.')
self.doneLabel.setWordWrap(True)
self.doneLabel.setAlignment(QtCore.Qt.AlignCenter)
self.doneLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
doneLayout.addWidget(self.doneLabel)

File diff suppressed because it is too large Load Diff

View File

@@ -48,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:

View File

@@ -51,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:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -44,7 +44,7 @@ class InterpolYellowNotices:
thumbnailIconImageScaled = thumbnailIconImageOriginal.scaled(QSize(40, 40))
thumbnailByteArrayFin = QByteArray()
thumbnailImageBuffer = QBuffer(thumbnailByteArrayFin)
thumbnailImageBuffer.open(QIODevice.WriteOnly)
thumbnailImageBuffer.open(QIODevice.OpenModeFlag.WriteOnly)
thumbnailIconImageScaled.save(thumbnailImageBuffer, "PNG")
thumbnailImageBuffer.close()
except Exception:

View File

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

View File

@@ -45,7 +45,7 @@ class OpenCorporateCompaniesAddress:
for entity in entityJsonList:
uid = entity['uid']
if parameters['OpenCorporates API Key'] == 'No Key':
if parameters['OpenCorporates API Key'].strip() == 'No Key':
# Set up parameters
data_params = parse.urlencode({
'q': entity['Company Name']
@@ -70,7 +70,7 @@ class OpenCorporateCompaniesAddress:
)
# Rate limited to the Starter API rate.
time.sleep(0.02)
data = r.json
data = r.json()
if r.status_code == 401:
return 'Invalid API Key'

View File

@@ -52,7 +52,7 @@ class OpenCorporateCompanyOfficers:
companyCode = entity['Company Number']
uid = entity['uid']
if parameters['OpenCorporates API Key'] == 'No Key':
if parameters['OpenCorporates API Key'].strip() == 'No Key':
# Perform and process get request
try:
r = requests.get(f"https://api.opencorporates.com/v0.4/companies/{jurisdictionCode}/{companyCode}")
@@ -71,7 +71,7 @@ class OpenCorporateCompanyOfficers:
return "Please check your internet connection"
# Rate limited to the Starter API rate.
time.sleep(80)
data = r.json
data = r.json()
if r.status_code == 401:
return 'Invalid API Key'

View File

@@ -47,7 +47,7 @@ class OpenCorporateCompanyResults:
for entity in entityJsonList:
uid = entity['uid']
if parameters['OpenCorporates API Key'] == 'No Key':
if parameters['OpenCorporates API Key'].strip() == 'No Key':
# Set up parameters
data_params = parse.urlencode({
'q': entity[list(entity)[1]]
@@ -75,7 +75,7 @@ class OpenCorporateCompanyResults:
return "Please check your internet connection"
# Rate limited to the Starter API rate.
time.sleep(0.02)
data = r.json
data = r.json()
if r.status_code == 401:
return 'Invalid API Key'

View File

@@ -50,7 +50,7 @@ class OpenCorporateOfficerCompanies:
if entity['Entity Type'] == 'Open Corporates Officer':
primaryField = primaryField.split(' | ')[0] # Nobody has ' | ' in their name - 2022/7/10
if parameters['OpenCorporates API Key'] == 'No Key':
if parameters['OpenCorporates API Key'].strip() == 'No Key':
# Set up parameters
data_params = parse.urlencode({
'q': primaryField
@@ -79,7 +79,7 @@ class OpenCorporateOfficerCompanies:
# Rate limited to the Starter API rate.
time.sleep(0.02)
# print(r)
data = r.json
data = r.json()
if r.status_code == 401:
return 'Invalid API Key'

View File

@@ -106,7 +106,7 @@ class SteamGroupChecker:
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:

View File

@@ -41,7 +41,7 @@ class SteamGroupMembersChecker:
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:

View File

@@ -107,7 +107,7 @@ class SteamUsernameChecker:
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:

View File

@@ -157,7 +157,7 @@ class TwitterUser:
childIconImageScaled = childIconImageOriginal.scaled(QSize(40, 40))
childIconByteArrayFin = QByteArray()
childImageBuffer = QBuffer(childIconByteArrayFin)
childImageBuffer.open(QIODevice.WriteOnly)
childImageBuffer.open(QIODevice.OpenModeFlag.WriteOnly)
childIconImageScaled.save(childImageBuffer, "PNG")
childImageBuffer.close()
except Exception:
@@ -216,7 +216,7 @@ class TwitterUser:
iconImageScaled = iconImageOriginal.scaled(QSize(40, 40))
iconByteArrayFin = QByteArray()
imageBuffer = QBuffer(iconByteArrayFin)
imageBuffer.open(QIODevice.WriteOnly)
imageBuffer.open(QIODevice.OpenModeFlag.WriteOnly)
iconImageScaled.save(imageBuffer, "PNG")
imageBuffer.close()
except Exception:

View File

@@ -63,7 +63,7 @@ class PinterestUsersSearch:
childIconImageScaled = childIconImageOriginal.scaled(QSize(40, 40))
childIconByteArrayFin = QByteArray()
childImageBuffer = QBuffer(childIconByteArrayFin)
childImageBuffer.open(QIODevice.WriteOnly)
childImageBuffer.open(QIODevice.OpenModeFlag.WriteOnly)
childIconImageScaled.save(childImageBuffer, "PNG")
childImageBuffer.close()
except Exception:

View File

@@ -18,6 +18,7 @@ class Whats_My_Name:
from concurrent.futures import as_completed
from pathlib import Path
import json
import contextlib
from playwright.sync_api import sync_playwright, TimeoutError
import re
@@ -38,10 +39,9 @@ class Whats_My_Name:
file = json.load(web_accounts_list)
with sync_playwright() as p:
browser = p.firefox.launch()
browser = p.chromium.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()
@@ -58,51 +58,62 @@ class Whats_My_Name:
original_uri = site['uri_check'].replace('{account}', social_field)
account_existence_code = site['e_code']
account_existence_string = site['e_string']
account_missing_string = site['m_string']
account_missing_code = site['m_code']
requires_javascript = site.get('requires_javascript', False)
post_body = site['post_body']
if site['valid']:
account_existence_string = re.escape(account_existence_string)
account_existence_string = re.compile(account_existence_string)
account_missing_string = re.escape(account_missing_string)
account_missing_string = re.compile(account_missing_string)
if requires_javascript:
for _ in range(3):
try:
with contextlib.suppress(TimeoutError):
response = page.goto(original_uri, wait_until="networkidle", timeout=10000)
status_code = response.status
page_source = page.content()
if status_code == account_existence_code and \
account_existence_string != "" and \
len(account_existence_string.findall(page_source)) > 0:
return_result.append([{'URL': original_uri,
'Entity Type': 'Website'},
{uid: {'Resolution': 'Whats My Name Account Match',
'Notes': ''}}])
len(account_existence_string.findall(page_source)) > 0 and \
(len(account_missing_string.findall(page_source)) == 0
if account_missing_code == account_existence_code else True):
return_result.append(
[{'URL': original_uri,
'Entity Type': 'Website'},
{uid: {'Resolution': 'Whats My Name Account Match',
'Notes': ''}}])
break
except TimeoutError:
pass
else:
post_body = site['post_body']
if post_body != "":
futures[session.post(original_uri, data=post_body, headers=headers,
timeout=10, allow_redirects=False)] = \
(account_existence_code, account_existence_string)
(uid, account_existence_code, account_existence_string,
account_missing_string, account_missing_code)
else:
futures[session.get(original_uri, headers=headers,
timeout=10, allow_redirects=False)] = \
(account_existence_code, account_existence_string)
(uid, account_existence_code, account_existence_string,
account_missing_string, account_missing_code)
for future in as_completed(futures):
account_existence_code = futures[future][0]
account_existence_string = futures[future][1]
parent_uid = futures[future][0]
account_existence_code = futures[future][1]
account_existence_string = futures[future][2]
account_missing_string = futures[future][3]
account_missing_code = futures[future][4]
try:
first_response = future.result()
except RequestException:
continue
page_source = first_response.text
if first_response.status_code == account_existence_code and \
account_existence_string != "" and \
len(account_existence_string.findall(page_source)) > 0:
if first_response.status_code == account_existence_code and account_existence_string != "" and \
len(account_existence_string.findall(page_source)) > 0 and \
(len(account_missing_string.findall(page_source)) == 0
if account_missing_code == account_existence_code else True):
return_result.append([{'URL': first_response.url,
'Entity Type': 'Website'},
{uid: {'Resolution': 'Whats My Name Account Match', 'Notes': ''}}])
{parent_uid: {'Resolution': 'Whats My Name Account Match',
'Notes': ''}}])
page.close()
browser.close()
return return_result

File diff suppressed because it is too large Load Diff

View File

@@ -42,9 +42,9 @@ Note that the SFDP graph layout does not function on Windows, as an essential gr
Since Version 1.0.0, installers are provided for Windows 11 and Linux (Ubuntu) platforms.
Download the latest installer for your platform from the Releases page, and run it to install the software:
https://github.com/AccentuSoft/LinkScope_Client/releases/tag/v1.0.0
https://github.com/AccentuSoft/LinkScope_Client/releases/tag/v1.4.0
Note: On Ubuntu, you may need to mark the downloaded installer as executable before you can run it. To do this, right-click the installer, and from the drop-down menu, select 'properties'. On the dialog window that pops up, navigate to the 'Permissions' tab, and make sure that 'Allow executing file as program' is checked. You should at this point be able to run the installer by double-clicking it.
Note: On Ubuntu, you may need to mark the downloaded installer as executable before you can run it. To do this, right-click the installer, and from the drop-down menu, select 'properties'. On the dialog window that pops up, navigate to the 'Permissions' tab, and make sure that 'Allow executing file as program' is checked. You should at this point be able to run the installer by double-clicking it. If double-clicking the installer does not start it, you can also launch the installer through a terminal.
### Running from source
One could also clone the repository and run the software as-is.
@@ -71,6 +71,8 @@ The Wiki page contains a manual on using the tool, available here: https://githu
We have an introductory blog post that explains the basics of using the tool, available here: https://accentusoft.com/tutorials/first-steps-with-linkscope-client/
We also have some videos demonstrating how to use the software on our YouTube channel: https://www.youtube.com/channel/UC8h9Vde1OdezdC2cJ1nEUcw
## Extending the software
LinkScope was built from the ground up to be modular! In this repository's wiki, there are instructions on how to create your own modules, which can contain custom Entities and Resolutions. There is also an example module in the Modules directory that can act as a template, and has a verbose description of most things that a module creator should need to consider.
@@ -78,7 +80,7 @@ LinkScope was built from the ground up to be modular! In this repository's wiki,
Warnings and best practices on using the software:
### Do NOT use Resolutions obtained by untrusted sources.
Resolutions are essentially Python code that ingests information from various data sources. Make sure that the people providing you with code to run are trustworthy, and that you inspect all the modules you use before installing them. Pre-compiled binaries on systems without Python installed are not able to install new resolutions, so compiling custom versions of the client to suit your investigators' needs should effectively mitigate this risk.
Resolutions are essentially Python code that ingests information from various data sources. Make sure that the people providing you with code to run are trustworthy, and that you inspect all the modules you use before installing them.
### Do NOT interact with unsafe resources without sufficient protection measures.
Some resources that an investigator may access through the software may pose a risk to the investigator and/or the assets used during the investigation. Some examples include, but are not limited to: Sites that host malicious code, torrents for malware, or forums that contain illegal materials. Handling the risk of investigating targets and materials that may be harmful to the investigator and/or the equipment used during the investigation is completely up to the investigator.

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="40" height="40" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<defs>
<clipPath id="b">
<path d="m777 483h200.86v83h-200.86z"/>
</clipPath>
<clipPath id="a">
<path d="m222.14 552h605.86v403h-605.86z"/>
</clipPath>
</defs>
<path d="m1200 600c0 214.36-114.36 412.44-300 519.62-185.64 107.18-414.36 107.18-600 0-185.64-107.18-300-305.26-300-519.62s114.36-412.44 300-519.62c185.64-107.18 414.36-107.18 600 0 185.64 107.18 300 305.26 300 519.62" fill="#758bcc"/>
<path d="m380.3 671.18 111.94-74.859-82.703-168.71c-5.6641-11.551-5.1055-25.043 1.4844-36.09l9.6016-16.086c3.4102-5.7188 1.9922-13.074-3.2969-17.113-5.4336-4.1484-13.234-3.3789-17.758 1.75l-23.266 26.379c-11.484 13.027-17.246 30.426-15.797 47.734z" fill="#fff"/>
<path d="m706.25 560.55-12.074 5.8906c-3.0078 1.4688-4.5586 4.8789-3.6797 8.1133l11.312 41.762c0.54688 2.0195 1.9531 3.6719 3.8594 4.5391s4.0742 0.83203 5.957-0.082031l12.07-5.8906c3.0078-1.4688 4.5586-4.8789 3.6836-8.1133l-11.312-41.762c-1.4141-4.4844-4.793-6.0547-9.8164-4.457z" fill="#fff"/>
<g clip-path="url(#b)">
<path d="m977.7 531.55c-0.21094-0.86719-0.97266-2.9805-3.5117-3.8672l-127.82-44.242c-1.3398 0-2.6562 0.44141-3.7461 1.2852l-63.176 48.922c-2.8945 2.2422-2.4297 5.3555-2.2188 6.2578 0.21094 0.89844 1.1797 3.8945 4.7656 4.6094 71.266 14.219 123.37 21.676 142.96 20.449 21.539-1.3477 39.828-16.137 51.379-28.305 1.875-1.9805 1.582-4.2344 1.3711-5.1094z" fill="#fff"/>
</g>
<path d="m345.03 828.57c4.7539-2.2266 8.3242-6.3984 9.793-11.445 1.4766-5.0703 0.69531-10.535-2.1406-14.988-4.9336-7.7539-15.016-10.609-23.445-6.6484-13.547 6.3633-25.719 9.5859-36.184 9.5859-7.1836 0-13.344-1.4805-18.832-4.5195-4.3828-2.4297-8.1445-5.5-11.281-9.1523-7.207 10.82-13.523 21.809-18.879 32.703 3.7891 3.1406 7.9492 5.9766 12.426 8.4609 23.973 13.293 54.492 11.938 88.543-3.9961z" fill="#fff"/>
<path d="m717.69 439.84c4.5391-4.7266 8.4883-9.207 11.852-13.391l-45.363-24.973c-5.5039-3.0273-12.016-2.9297-17.422 0.26562-5.4062 3.1953-8.6367 8.8555-8.6367 15.137v68.512l54.879-36.703c0.66406-3.2344 2.2422-6.2969 4.6914-8.8477z" fill="#fff"/>
<path d="m725.51 454.49c0.34375 0.80078 1.7227 3.418 5.168 3.418h17.645c11.328 0 22.48-3.0234 32.262-8.7461l47.414-27.758c5.4492-3.1914 9.0781-8.6094 9.9531-14.863l19.949-142.66c0.79687-5.6953-1.3867-11.262-5.8398-14.895-2.9727-2.4297-6.5898-3.6914-10.281-3.6914-1.8398 0-3.6992 0.3125-5.5078 0.95312-28.91 10.258-48.09 21.965-55.453 33.848-10 16.137-14.871 51.27-19.168 82.27-3.4023 24.539-6.3398 45.73-11.551 55.766-4.4023 8.4766-12.297 18.668-23.477 30.297-2.375 2.4766-1.4531 5.2695-1.1133 6.0664z" fill="#fff"/>
<path d="m337.29 873.02 106.24-82.207-7.1133-26.258c-2.4258-8.957 1.8672-18.414 10.207-22.484l12.07-5.8906c5.2109-2.5469 11.23-2.6289 16.512-0.23047 5.2812 2.3984 9.1797 6.9844 10.695 12.578l2.125 7.8516 15.699-12.148c-1.332-4.8008-2.0742-10.129-2.0742-15.777 0-21.809 10.953-38.891 24.938-38.891 9.5664 0 17.711 7.9922 21.906 20.023l15.148-11.723c-1.3164-4.7773-2.0469-10.07-2.0469-15.684 0-21.809 10.953-38.891 24.938-38.891 9.5391 0 17.668 7.9531 21.871 19.934l15.156-11.727c-1.3008-4.75-2.0195-10.012-2.0195-15.59 0-21.809 10.953-38.891 24.938-38.891 9.5156 0 17.625 7.918 21.84 19.844l17.195-13.305-6.9766-25.758c-2.4258-8.957 1.8672-18.414 10.207-22.484l12.074-5.8906c5.2109-2.5469 11.23-2.6289 16.512-0.23047 5.2812 2.3984 9.1797 6.9844 10.695 12.582l1.9922 7.3477 35.012-27.09c-1.3555-6.918 1.2148-13.828 6.832-18.176l63.172-48.922c4.9805-3.8594 11.5-4.918 17.445-2.8438l31.234 10.902c5.375-13.211 9.8086-27.273 13.188-41.879 0.82422-3.5586-0.015625-7.25-2.3008-10.125-2.3125-2.9102-5.7695-4.5781-9.4844-4.5781h-43.656c-2.0938 2.1914-4.5078 4.1094-7.2031 5.6875l-47.414 27.758c-11.676 6.832-24.992 10.445-38.52 10.445h-17.645c-6.3477 0-11.945-3.1406-15.207-8.3516l-375.02 250.79c-28.027 18.742-51.57 42.641-70.102 68.016 2.5078 3.7266 5.75 6.6875 9.8633 8.9648 3.6055 1.9961 7.8047 2.9688 12.828 2.9688 8.6289 0 19.031-2.8281 30.914-8.4102 14.055-6.6055 30.891-1.7852 39.164 11.207 4.75 7.4609 6.0547 16.609 3.5859 25.102-2.4648 8.4727-8.4531 15.469-16.434 19.207-20.23 9.4688-39.516 14.27-57.316 14.27-15.434 0-29.73-3.582-42.488-10.652-4.1719-2.3125-8.082-4.9258-11.754-7.7539-3.7617 8.6719-6.9023 17.215-9.3984 25.492l13.328 10.805c27.609 22.391 66.531 22.828 94.645 1.0703z" fill="#fff"/>
<path d="m633.94 635.91c0 15.168 6.625 26.504 12.551 26.504 5.9219 0 12.551-11.336 12.551-26.504s-6.625-26.504-12.551-26.504c-5.9219 0-12.551 11.336-12.551 26.504z" fill="#fff"/>
<path d="m470.09 747.23c-0.92188-0.41797-1.9062-0.62891-2.8906-0.62891-1.0469 0-2.0977 0.23828-3.0664 0.71094l-12.074 5.8906c-3.0078 1.4688-4.5586 4.8789-3.6797 8.1133l11.312 41.762c0.54688 2.0195 1.9531 3.6719 3.8594 4.5391 1.9062 0.86719 4.0742 0.83594 5.957-0.082032l12.07-5.8945c3.0078-1.4688 4.5547-4.8789 3.6797-8.1094l-11.312-41.762c-0.54688-2.0195-1.9531-3.6719-3.8555-4.5391z" fill="#fff"/>
<path d="m956.19 749.45-41.23 7.582c-12.195 2.2422-24.531 2.0898-36.668-0.46484l-211.45-44.414-137.91 111.36 327.45 8.7773c15.086 0.41016 29.898-3.5547 42.785-11.449l68.336-41.871c7.2109-4.418 9.7695-13.734 5.8203-21.215-3.2969-6.2422-10.176-9.5781-17.129-8.3047z" fill="#fff"/>
<g clip-path="url(#a)">
<path d="m827.35 565.7c-17.535-3.1094-34.316-6.3477-47.781-9.0312-3.1094-0.62109-5.8984-1.9688-8.207-3.875l-37.84 29.277 5.8164 21.465c2.4258 8.957-1.8672 18.414-10.207 22.484l-12.074 5.8906c-5.6445 1.7422-11.16 1.9102-16.512 0.23047-5.2812-2.3984-9.1797-6.9844-10.695-12.578l-0.82812-3.0586-17.852 13.812c0.16016 1.8242 0.25 3.6875 0.25 5.5898 0 21.809-10.953 38.891-24.938 38.891-7.082 0-13.383-4.3867-17.879-11.539l-17.367 13.438c0.15625 1.7891 0.24219 3.6172 0.24219 5.4805 0 21.809-10.953 38.891-24.938 38.891-7.0547 0-13.336-4.3516-17.832-11.461l-17.406 13.469c0.14844 1.7539 0.23047 3.543 0.23047 5.3711 0 21.809-10.953 38.891-24.938 38.891-7.0312 0-13.293-4.3203-17.785-11.387l-17.277 13.367 5.6797 20.965c2.4258 8.957-1.8672 18.414-10.207 22.484l-12.07 5.8945c-5.9961 1.5938-11.547 1.8164-16.508 0.23047-5.2812-2.3984-9.1797-6.9844-10.699-12.582l-0.69141-2.5586-102.16 79.055c-16.059 12.43-35.156 18.625-54.234 18.625-19.746 0-39.469-6.6367-55.797-19.871l-8.9961-7.293c-1.4492 6.3945-2.4961 12.582-3.0859 18.453-2.293 22.902 1.8398 40.398 11.961 50.59 34.668 34.93 145.45-15.59 228.2-82.422l350.77-283.26c4.7148-3.793 9.2656-7.8008 13.652-11.93z" fill="#fff"/>
</g>
<path d="m573.99 682.18c0 15.168 6.625 26.504 12.551 26.504 5.9219 0 12.551-11.336 12.551-26.504 0-15.168-6.625-26.504-12.551-26.504s-12.551 11.336-12.551 26.504z" fill="#fff"/>
<path d="m514.04 728.45c0 15.168 6.625 26.504 12.551 26.504 5.9219 0 12.551-11.336 12.551-26.504s-6.625-26.504-12.551-26.504c-5.9219 0-12.551 11.336-12.551 26.504z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="37" height="37" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<path d="m797.97 905.21c4.3125-1.7852 8-4.8125 10.594-8.6953 2.5977-3.8867 3.9805-8.4492 3.9805-13.121v-566.79c0-6.2656-2.4883-12.27-6.918-16.699-4.4297-4.4297-10.434-6.918-16.699-6.918h-377.86c-6.2656 0-12.27 2.4883-16.699 6.918-4.4297 4.4297-6.918 10.434-6.918 16.699v566.79c0 6.2617 2.4883 12.27 6.918 16.699 4.4258 4.4297 10.434 6.918 16.699 6.918 6.2617-0.003907 12.27-2.4922 16.695-6.918l172.23-172.23 172.23 172.23c3.3008 3.3008 7.5078 5.5508 12.09 6.4648 4.582 0.91016 9.332 0.44141 13.648-1.3477z" fill="#12b0fb"/>
</svg>

After

Width:  |  Height:  |  Size: 673 B

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="37" height="37" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<path d="m797.97 905.21c4.3125-1.7852 8-4.8125 10.594-8.6953 2.5977-3.8867 3.9805-8.4492 3.9805-13.121v-566.79c0-6.2656-2.4883-12.27-6.918-16.699-4.4297-4.4297-10.434-6.918-16.699-6.918h-377.86c-6.2656 0-12.27 2.4883-16.699 6.918-4.4297 4.4297-6.918 10.434-6.918 16.699v566.79c0 6.2617 2.4883 12.27 6.918 16.699 4.4258 4.4297 10.434 6.918 16.699 6.918 6.2617-0.003907 12.27-2.4922 16.695-6.918l172.23-172.23 172.23 172.23c3.3008 3.3008 7.5078 5.5508 12.09 6.4648 4.582 0.91016 9.332 0.44141 13.648-1.3477z" fill="#ff814a"/>
</svg>

After

Width:  |  Height:  |  Size: 673 B

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="37" height="37" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<path d="m797.97 905.21c4.3125-1.7852 8-4.8125 10.594-8.6953 2.5977-3.8867 3.9805-8.4492 3.9805-13.121v-566.79c0-6.2656-2.4883-12.27-6.918-16.699-4.4297-4.4297-10.434-6.918-16.699-6.918h-377.86c-6.2656 0-12.27 2.4883-16.699 6.918-4.4297 4.4297-6.918 10.434-6.918 16.699v566.79c0 6.2617 2.4883 12.27 6.918 16.699 4.4258 4.4297 10.434 6.918 16.699 6.918 6.2617-0.003907 12.27-2.4922 16.695-6.918l172.23-172.23 172.23 172.23c3.3008 3.3008 7.5078 5.5508 12.09 6.4648 4.582 0.91016 9.332 0.44141 13.648-1.3477z" fill="#ff001b"/>
</svg>

After

Width:  |  Height:  |  Size: 673 B

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="40" height="40" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<defs>
<clipPath id="b">
<path d="m777 483h200.86v83h-200.86z"/>
</clipPath>
<clipPath id="a">
<path d="m222.14 552h605.86v403h-605.86z"/>
</clipPath>
</defs>
<path d="m1200 600c0 214.36-114.36 412.44-300 519.62-185.64 107.18-414.36 107.18-600 0-185.64-107.18-300-305.26-300-519.62s114.36-412.44 300-519.62c185.64-107.18 414.36-107.18 600 0 185.64 107.18 300 305.26 300 519.62"/>
<path d="m380.3 671.18 111.94-74.859-82.703-168.71c-5.6641-11.551-5.1055-25.043 1.4844-36.09l9.6016-16.086c3.4102-5.7188 1.9922-13.074-3.2969-17.113-5.4336-4.1484-13.234-3.3789-17.758 1.75l-23.266 26.379c-11.484 13.027-17.246 30.426-15.797 47.734z" fill="#fff"/>
<path d="m706.25 560.55-12.074 5.8906c-3.0078 1.4688-4.5586 4.8789-3.6797 8.1133l11.312 41.762c0.54688 2.0195 1.9531 3.6719 3.8594 4.5391s4.0742 0.83203 5.957-0.082031l12.07-5.8906c3.0078-1.4688 4.5586-4.8789 3.6836-8.1133l-11.312-41.762c-1.4141-4.4844-4.793-6.0547-9.8164-4.457z" fill="#fff"/>
<g clip-path="url(#b)">
<path d="m977.7 531.55c-0.21094-0.86719-0.97266-2.9805-3.5117-3.8672l-127.82-44.242c-1.3398 0-2.6562 0.44141-3.7461 1.2852l-63.176 48.922c-2.8945 2.2422-2.4297 5.3555-2.2188 6.2578 0.21094 0.89844 1.1797 3.8945 4.7656 4.6094 71.266 14.219 123.37 21.676 142.96 20.449 21.539-1.3477 39.828-16.137 51.379-28.305 1.875-1.9805 1.582-4.2344 1.3711-5.1094z" fill="#fff"/>
</g>
<path d="m345.03 828.57c4.7539-2.2266 8.3242-6.3984 9.793-11.445 1.4766-5.0703 0.69531-10.535-2.1406-14.988-4.9336-7.7539-15.016-10.609-23.445-6.6484-13.547 6.3633-25.719 9.5859-36.184 9.5859-7.1836 0-13.344-1.4805-18.832-4.5195-4.3828-2.4297-8.1445-5.5-11.281-9.1523-7.207 10.82-13.523 21.809-18.879 32.703 3.7891 3.1406 7.9492 5.9766 12.426 8.4609 23.973 13.293 54.492 11.938 88.543-3.9961z" fill="#fff"/>
<path d="m717.69 439.84c4.5391-4.7266 8.4883-9.207 11.852-13.391l-45.363-24.973c-5.5039-3.0273-12.016-2.9297-17.422 0.26562-5.4062 3.1953-8.6367 8.8555-8.6367 15.137v68.512l54.879-36.703c0.66406-3.2344 2.2422-6.2969 4.6914-8.8477z" fill="#fff"/>
<path d="m725.51 454.49c0.34375 0.80078 1.7227 3.418 5.168 3.418h17.645c11.328 0 22.48-3.0234 32.262-8.7461l47.414-27.758c5.4492-3.1914 9.0781-8.6094 9.9531-14.863l19.949-142.66c0.79687-5.6953-1.3867-11.262-5.8398-14.895-2.9727-2.4297-6.5898-3.6914-10.281-3.6914-1.8398 0-3.6992 0.3125-5.5078 0.95312-28.91 10.258-48.09 21.965-55.453 33.848-10 16.137-14.871 51.27-19.168 82.27-3.4023 24.539-6.3398 45.73-11.551 55.766-4.4023 8.4766-12.297 18.668-23.477 30.297-2.375 2.4766-1.4531 5.2695-1.1133 6.0664z" fill="#fff"/>
<path d="m337.29 873.02 106.24-82.207-7.1133-26.258c-2.4258-8.957 1.8672-18.414 10.207-22.484l12.07-5.8906c5.2109-2.5469 11.23-2.6289 16.512-0.23047 5.2812 2.3984 9.1797 6.9844 10.695 12.578l2.125 7.8516 15.699-12.148c-1.332-4.8008-2.0742-10.129-2.0742-15.777 0-21.809 10.953-38.891 24.938-38.891 9.5664 0 17.711 7.9922 21.906 20.023l15.148-11.723c-1.3164-4.7773-2.0469-10.07-2.0469-15.684 0-21.809 10.953-38.891 24.938-38.891 9.5391 0 17.668 7.9531 21.871 19.934l15.156-11.727c-1.3008-4.75-2.0195-10.012-2.0195-15.59 0-21.809 10.953-38.891 24.938-38.891 9.5156 0 17.625 7.918 21.84 19.844l17.195-13.305-6.9766-25.758c-2.4258-8.957 1.8672-18.414 10.207-22.484l12.074-5.8906c5.2109-2.5469 11.23-2.6289 16.512-0.23047 5.2812 2.3984 9.1797 6.9844 10.695 12.582l1.9922 7.3477 35.012-27.09c-1.3555-6.918 1.2148-13.828 6.832-18.176l63.172-48.922c4.9805-3.8594 11.5-4.918 17.445-2.8438l31.234 10.902c5.375-13.211 9.8086-27.273 13.188-41.879 0.82422-3.5586-0.015625-7.25-2.3008-10.125-2.3125-2.9102-5.7695-4.5781-9.4844-4.5781h-43.656c-2.0938 2.1914-4.5078 4.1094-7.2031 5.6875l-47.414 27.758c-11.676 6.832-24.992 10.445-38.52 10.445h-17.645c-6.3477 0-11.945-3.1406-15.207-8.3516l-375.02 250.79c-28.027 18.742-51.57 42.641-70.102 68.016 2.5078 3.7266 5.75 6.6875 9.8633 8.9648 3.6055 1.9961 7.8047 2.9688 12.828 2.9688 8.6289 0 19.031-2.8281 30.914-8.4102 14.055-6.6055 30.891-1.7852 39.164 11.207 4.75 7.4609 6.0547 16.609 3.5859 25.102-2.4648 8.4727-8.4531 15.469-16.434 19.207-20.23 9.4688-39.516 14.27-57.316 14.27-15.434 0-29.73-3.582-42.488-10.652-4.1719-2.3125-8.082-4.9258-11.754-7.7539-3.7617 8.6719-6.9023 17.215-9.3984 25.492l13.328 10.805c27.609 22.391 66.531 22.828 94.645 1.0703z" fill="#fff"/>
<path d="m633.94 635.91c0 15.168 6.625 26.504 12.551 26.504 5.9219 0 12.551-11.336 12.551-26.504s-6.625-26.504-12.551-26.504c-5.9219 0-12.551 11.336-12.551 26.504z" fill="#fff"/>
<path d="m470.09 747.23c-0.92188-0.41797-1.9062-0.62891-2.8906-0.62891-1.0469 0-2.0977 0.23828-3.0664 0.71094l-12.074 5.8906c-3.0078 1.4688-4.5586 4.8789-3.6797 8.1133l11.312 41.762c0.54688 2.0195 1.9531 3.6719 3.8594 4.5391 1.9062 0.86719 4.0742 0.83594 5.957-0.082032l12.07-5.8945c3.0078-1.4688 4.5547-4.8789 3.6797-8.1094l-11.312-41.762c-0.54688-2.0195-1.9531-3.6719-3.8555-4.5391z" fill="#fff"/>
<path d="m956.19 749.45-41.23 7.582c-12.195 2.2422-24.531 2.0898-36.668-0.46484l-211.45-44.414-137.91 111.36 327.45 8.7773c15.086 0.41016 29.898-3.5547 42.785-11.449l68.336-41.871c7.2109-4.418 9.7695-13.734 5.8203-21.215-3.2969-6.2422-10.176-9.5781-17.129-8.3047z" fill="#fff"/>
<g clip-path="url(#a)">
<path d="m827.35 565.7c-17.535-3.1094-34.316-6.3477-47.781-9.0312-3.1094-0.62109-5.8984-1.9688-8.207-3.875l-37.84 29.277 5.8164 21.465c2.4258 8.957-1.8672 18.414-10.207 22.484l-12.074 5.8906c-5.6445 1.7422-11.16 1.9102-16.512 0.23047-5.2812-2.3984-9.1797-6.9844-10.695-12.578l-0.82812-3.0586-17.852 13.812c0.16016 1.8242 0.25 3.6875 0.25 5.5898 0 21.809-10.953 38.891-24.938 38.891-7.082 0-13.383-4.3867-17.879-11.539l-17.367 13.438c0.15625 1.7891 0.24219 3.6172 0.24219 5.4805 0 21.809-10.953 38.891-24.938 38.891-7.0547 0-13.336-4.3516-17.832-11.461l-17.406 13.469c0.14844 1.7539 0.23047 3.543 0.23047 5.3711 0 21.809-10.953 38.891-24.938 38.891-7.0312 0-13.293-4.3203-17.785-11.387l-17.277 13.367 5.6797 20.965c2.4258 8.957-1.8672 18.414-10.207 22.484l-12.07 5.8945c-5.9961 1.5938-11.547 1.8164-16.508 0.23047-5.2812-2.3984-9.1797-6.9844-10.699-12.582l-0.69141-2.5586-102.16 79.055c-16.059 12.43-35.156 18.625-54.234 18.625-19.746 0-39.469-6.6367-55.797-19.871l-8.9961-7.293c-1.4492 6.3945-2.4961 12.582-3.0859 18.453-2.293 22.902 1.8398 40.398 11.961 50.59 34.668 34.93 145.45-15.59 228.2-82.422l350.77-283.26c4.7148-3.793 9.2656-7.8008 13.652-11.93z" fill="#fff"/>
</g>
<path d="m573.99 682.18c0 15.168 6.625 26.504 12.551 26.504 5.9219 0 12.551-11.336 12.551-26.504 0-15.168-6.625-26.504-12.551-26.504s-12.551 11.336-12.551 26.504z" fill="#fff"/>
<path d="m514.04 728.45c0 15.168 6.625 26.504 12.551 26.504 5.9219 0 12.551-11.336 12.551-26.504s-6.625-26.504-12.551-26.504c-5.9219 0-12.551 11.336-12.551 26.504z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="40" height="40" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<defs>
<clipPath id="d">
<path d="m222.14 765h755.71v107h-755.71z"/>
</clipPath>
<clipPath id="c">
<path d="m222.14 847h755.71v130.86h-755.71z"/>
</clipPath>
<clipPath id="b">
<path d="m222.14 670h755.71v119h-755.71z"/>
</clipPath>
<clipPath id="a">
<path d="m416 222.14h368v311.86h-368z"/>
</clipPath>
</defs>
<path d="m1200 600c0 214.36-114.36 412.44-300 519.62-185.64 107.18-414.36 107.18-600 0-185.64-107.18-300-305.26-300-519.62s114.36-412.44 300-519.62c185.64-107.18 414.36-107.18 600 0 185.64 107.18 300 305.26 300 519.62"/>
<g clip-path="url(#d)">
<path d="m966.05 777.12c-19.594-0.50391-38.855 5.1328-55.09 16.117-20.195 12.207-43.344 18.656-66.945 18.656-23.598 0-46.746-6.4531-66.941-18.656-16.578-10.156-35.641-15.531-55.082-15.531-19.441 0-38.504 5.375-55.082 15.531-20.25 12.043-43.371 18.402-66.934 18.398-23.559 0-46.684-6.3555-66.934-18.398-16.691-9.8281-35.707-15.008-55.074-15.008-19.371 0-38.387 5.1836-55.078 15.008-20.258 12.02-43.375 18.363-66.93 18.363-23.555 0-46.676-6.3438-66.93-18.363-16.23-10.988-35.488-16.621-55.078-16.117-6.5234 0-11.809-5.2852-11.809-11.809v82.656c0-6.5195 5.2852-11.805 11.809-11.805 23.75-0.47266 47.082 6.2617 66.934 19.309 16.227 10.984 35.484 16.617 55.074 16.113 19.59 0.50391 38.848-5.1289 55.078-16.113 19.848-13.047 43.184-19.781 66.934-19.309 23.746-0.46875 47.078 6.2617 66.926 19.309 16.23 10.984 35.488 16.617 55.082 16.113 19.59 0.50391 38.848-5.1289 55.078-16.113 20.047-12.617 43.25-19.309 66.934-19.309 23.688 0 46.891 6.6914 66.938 19.309 16.445 10.523 35.562 16.113 55.09 16.113 19.523 0 38.641-5.5898 55.086-16.113 19.855-13.047 43.191-19.781 66.945-19.309 6.5195 0 11.809 5.2852 11.809 11.805v-82.656c0 6.5234-5.2891 11.809-11.809 11.809z" fill="#fff"/>
</g>
<g clip-path="url(#c)">
<path d="m966.05 859.78c-19.594-0.50391-38.855 5.1328-55.09 16.117-20.051 12.613-43.254 19.309-66.941 19.309s-46.895-6.6953-66.945-19.309c-16.441-10.523-35.559-16.117-55.082-16.117-19.52 0-38.637 5.5938-55.082 16.117-19.848 13.047-43.184 19.777-66.93 19.309-23.75 0.46875-47.086-6.2617-66.934-19.309-16.23-10.984-35.484-16.621-55.074-16.117-19.594-0.50391-38.852 5.1328-55.078 16.117-19.852 13.047-43.188 19.777-66.934 19.309-23.75 0.46875-47.082-6.2656-66.93-19.309-16.23-10.984-35.488-16.621-55.078-16.117-6.5234 0-11.809-5.2852-11.809-11.809v82.656c0 6.5234 5.2852 11.809 11.809 11.809 19.59-0.50391 38.848 5.1328 55.078 16.117 19.848 13.043 43.18 19.777 66.93 19.309 23.746 0.46875 47.082-6.2617 66.934-19.309 16.227-10.984 35.484-16.621 55.078-16.117 19.59-0.50391 38.844 5.1328 55.074 16.117 19.848 13.047 43.184 19.777 66.934 19.309 23.746 0.46875 47.082-6.2617 66.93-19.309 16.445-10.523 35.562-16.117 55.082-16.117 19.523 0 38.641 5.5938 55.082 16.117 20.051 12.613 43.258 19.309 66.945 19.309s46.891-6.6953 66.941-19.309c16.234-10.984 35.496-16.621 55.09-16.117 6.5195 0 11.809-5.2852 11.809-11.809v-82.656c0 6.5234-5.2891 11.809-11.809 11.809z" fill="#fff"/>
</g>
<g clip-path="url(#b)">
<path d="m966.05 670.85c-23.75-0.47266-47.09 6.2578-66.941 19.305-16.609 10.074-35.664 15.398-55.09 15.398s-38.477-5.3242-55.086-15.395c-20.254-12.039-43.379-18.391-66.938-18.391s-46.684 6.3516-66.938 18.387c-16.609 10.062-35.656 15.383-55.078 15.383-19.418 0-38.469-5.3164-55.078-15.379-20.18-12.238-43.332-18.711-66.93-18.711-23.602 0-46.75 6.4727-66.93 18.707-16.625 10.023-35.668 15.32-55.078 15.32-19.414 0-38.457-5.2969-55.078-15.316-19.852-13.047-43.184-19.781-66.934-19.309-6.5234 0-11.809 5.2852-11.809 11.805v82.656c0-6.5195 5.2852-11.805 11.809-11.805 23.75-0.47266 47.082 6.2617 66.934 19.309 16.566 10.176 35.633 15.566 55.078 15.566 19.441 0 38.508-5.3906 55.074-15.57 20.242-12.066 43.367-18.438 66.934-18.438 23.562 0 46.688 6.3711 66.93 18.441 16.566 10.18 35.633 15.57 55.078 15.57 19.445 0 38.512-5.3906 55.078-15.574 20.246-12.055 43.375-18.422 66.938-18.422 23.562 0.003906 46.691 6.3672 66.938 18.426 16.617 10.039 35.668 15.348 55.086 15.348s38.469-5.3086 55.09-15.352c19.852-13.047 43.191-19.777 66.941-19.305 6.5195 0 11.809 5.2852 11.809 11.805v-82.656c0-6.5195-5.2891-11.805-11.809-11.805z" fill="#fff"/>
</g>
<g clip-path="url(#a)">
<path d="m602.84 493.32 180.19 39.852v-104.38c0-6.2656-2.4883-12.273-6.918-16.699-4.4258-4.4297-10.434-6.918-16.699-6.918h-11.809v-35.422c0.003907-6.2656-2.4844-12.273-6.9141-16.703-4.4297-4.4297-10.438-6.9141-16.703-6.9141h-5.9023v-23.617c0-6.5195-5.2852-11.809-11.809-11.809-6.5195 0-11.809 5.2891-11.809 11.809v23.617h-47.23v-64.945c-0.019531-6.5117-5.2969-11.789-11.809-11.809h-23.617v-23.613h11.809c6.5234 0 11.809-5.2891 11.809-11.809 0-6.5234-5.2852-11.809-11.809-11.809h-47.23c-6.5234 0-11.809 5.2852-11.809 11.809 0 6.5195 5.2852 11.809 11.809 11.809h11.809v23.613h-23.617c-6.5156 0.019531-11.789 5.2969-11.809 11.809v64.945h-47.23v-23.617c0-6.5195-5.2891-11.809-11.809-11.809-6.5234 0-11.809 5.2891-11.809 11.809v23.617h-5.9062c-6.2617 0-12.27 2.4844-16.699 6.9141-4.4297 4.4297-6.918 10.438-6.9141 16.703v35.426l-11.809-0.003906c-6.2656 0-12.273 2.4883-16.703 6.918-4.4258 4.4258-6.9141 10.434-6.9141 16.699v104.5l180.78-39.969c1.6758-0.35547 3.4023-0.35547 5.0781 0zm79.82-29.105h-35.422v-35.426h35.426zm-206.64-94.465h247.97v35.426l-247.97-0.003906zm76.754 94.465h-35.426v-35.426h35.426zm23.617-35.426h47.23v35.426h-47.23z" fill="#fff"/>
</g>
<path d="m403.57 667.37c23.461-13.203 50.008-19.941 76.926-19.523 26.918 0.41406 53.242 7.9648 76.285 21.883 12.996 7.9922 27.957 12.223 43.215 12.223 15.262 0 30.219-4.2305 43.219-12.223 46.984-27.844 105.19-28.762 153.03-2.418 0.22266 0.035156 0.44141 0.09375 0.65234 0.17578 14.207-28.809 21.582-60.508 21.547-92.633-0.003907-5.5508-3.8516-10.359-9.2695-11.574l-208.88-46.344-209.47 46.344c-5.418 1.2148-9.2656 6.0234-9.2695 11.574-0.027344 32.121 7.3438 63.82 21.547 92.633 0.14453-0.082031 0.30859-0.12109 0.47266-0.11719zm256.47-70.73c1.4023-6.3633 7.6797-10.398 14.051-9.0352l43.336 9.3867v0.003907c5.8906 1.2891 9.8672 6.8008 9.2305 12.801-0.63281 5.9961-5.6797 10.555-11.711 10.578-0.85547-0.011719-1.707-0.10938-2.5391-0.29688l-43.336-9.3867c-6.3633-1.4023-10.398-7.6797-9.0312-14.051zm-179.36 0.35547 43.336-9.3867-0.003906-0.003907c3.0664-0.67969 6.2812-0.10937 8.9258 1.5859 2.6445 1.6914 4.5078 4.3672 5.1758 7.4375 0.66797 3.0703 0.085937 6.2812-1.6211 8.918-1.7031 2.6406-4.3867 4.4922-7.4609 5.1445l-43.336 9.3867c-0.83203 0.1875-1.6836 0.28516-2.5391 0.29688-6.0312-0.023438-11.074-4.582-11.711-10.578-0.63672-6 3.3398-11.512 9.2305-12.801z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="40" height="40" version="1.1" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<g>
<path d="m100 50c0 27.613-22.387 50-50 50s-50-22.387-50-50 22.387-50 50-50 50 22.387 50 50" fill="#007335"/>
<path d="m30.992 74.109v-48.789c0-1.1367 1.1367-1.1367 1.1367-1.1367h26.098l10.211 10.215v39.715c0 1.1328-1.1328 1.1328-1.1328 1.1328h-35.176s-1.1367 0-1.1367-1.1367zm1.7031-1.6328v-45.523c0-1.0664 1.0352-1.0664 1.0352-1.0664h22.793v9.0781c0 1.1367 1.1367 1.1367 1.1367 1.1367h9.0781v36.375c0 1.0703-1.0352 1.0703-1.0352 1.0703h-31.973s-1.0352 0-1.0352-1.0703zm2.2695-31.84v29.504h29.504v-29.504zm1.7031 5.1094v2.2695h5.6719v-2.2695zm6.8086 0v2.2695h5.6719v-2.2695zm6.8086 0v2.2695h5.6719v-2.2695zm6.8047 0v2.2695h5.6758v-2.2695zm-20.422 3.4023v2.2695h5.6719v-2.2695zm6.8086 0v2.2695h5.6719v-2.2695zm6.8086 0v2.2695h5.6719v-2.2695zm6.8047 0v2.2695h5.6758v-2.2695zm-20.422 3.4062v2.2695h5.6719v-2.2695zm6.8086 0v2.2695h5.6719v-2.2695zm6.8086 0v2.2695h5.6719v-2.2695zm6.8047 0v2.2695h5.6758v-2.2695zm-20.422 3.4023v2.2695h5.6719v-2.2695zm6.8086 0v2.2695h5.6719v-2.2695zm6.8086 0v2.2695h5.6719v-2.2695zm6.8047 0v2.2695h5.6758v-2.2695zm-20.422 3.4023v2.2695h5.6719v-2.2695zm6.8086 0v2.2695h5.6719v-2.2695zm6.8086 0v2.2695h5.6719v-2.2695zm6.8047 0v2.2695h5.6758v-2.2695zm-20.422 3.4062v2.2695h5.6719v-2.2695zm6.8086 0v2.2695h5.6719v-2.2695zm6.8086 0v2.2695h5.6719v-2.2695zm6.8047 0v2.2695h5.6758v-2.2695zm-20.422 3.4023v2.2695h5.6719v-2.2695zm6.8086 0v2.2695h5.6719v-2.2695zm6.8086 0v2.2695h5.6719v-2.2695zm6.8047 0v2.2695h5.6758v-2.2695z" fill="#fff" fill-rule="evenodd"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="40" height="40" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<defs>
<clipPath id="d">
<path d="m222.14 765h755.71v107h-755.71z"/>
</clipPath>
<clipPath id="c">
<path d="m222.14 847h755.71v130.86h-755.71z"/>
</clipPath>
<clipPath id="b">
<path d="m222.14 670h755.71v119h-755.71z"/>
</clipPath>
<clipPath id="a">
<path d="m416 222.14h368v311.86h-368z"/>
</clipPath>
</defs>
<path d="m1200 600c0 214.36-114.36 412.44-300 519.62-185.64 107.18-414.36 107.18-600 0-185.64-107.18-300-305.26-300-519.62s114.36-412.44 300-519.62c185.64-107.18 414.36-107.18 600 0 185.64 107.18 300 305.26 300 519.62" fill="#042fad"/>
<g clip-path="url(#d)">
<path d="m966.05 777.12c-19.594-0.50391-38.855 5.1328-55.09 16.117-20.195 12.207-43.344 18.656-66.945 18.656-23.598 0-46.746-6.4531-66.941-18.656-16.578-10.156-35.641-15.531-55.082-15.531-19.441 0-38.504 5.375-55.082 15.531-20.25 12.043-43.371 18.402-66.934 18.398-23.559 0-46.684-6.3555-66.934-18.398-16.691-9.8281-35.707-15.008-55.074-15.008-19.371 0-38.387 5.1836-55.078 15.008-20.258 12.02-43.375 18.363-66.93 18.363-23.555 0-46.676-6.3438-66.93-18.363-16.23-10.988-35.488-16.621-55.078-16.117-6.5234 0-11.809-5.2852-11.809-11.809v82.656c0-6.5195 5.2852-11.805 11.809-11.805 23.75-0.47266 47.082 6.2617 66.934 19.309 16.227 10.984 35.484 16.617 55.074 16.113 19.59 0.50391 38.848-5.1289 55.078-16.113 19.848-13.047 43.184-19.781 66.934-19.309 23.746-0.46875 47.078 6.2617 66.926 19.309 16.23 10.984 35.488 16.617 55.082 16.113 19.59 0.50391 38.848-5.1289 55.078-16.113 20.047-12.617 43.25-19.309 66.934-19.309 23.688 0 46.891 6.6914 66.938 19.309 16.445 10.523 35.562 16.113 55.09 16.113 19.523 0 38.641-5.5898 55.086-16.113 19.855-13.047 43.191-19.781 66.945-19.309 6.5195 0 11.809 5.2852 11.809 11.805v-82.656c0 6.5234-5.2891 11.809-11.809 11.809z" fill="#fff"/>
</g>
<g clip-path="url(#c)">
<path d="m966.05 859.78c-19.594-0.50391-38.855 5.1328-55.09 16.117-20.051 12.613-43.254 19.309-66.941 19.309s-46.895-6.6953-66.945-19.309c-16.441-10.523-35.559-16.117-55.082-16.117-19.52 0-38.637 5.5938-55.082 16.117-19.848 13.047-43.184 19.777-66.93 19.309-23.75 0.46875-47.086-6.2617-66.934-19.309-16.23-10.984-35.484-16.621-55.074-16.117-19.594-0.50391-38.852 5.1328-55.078 16.117-19.852 13.047-43.188 19.777-66.934 19.309-23.75 0.46875-47.082-6.2656-66.93-19.309-16.23-10.984-35.488-16.621-55.078-16.117-6.5234 0-11.809-5.2852-11.809-11.809v82.656c0 6.5234 5.2852 11.809 11.809 11.809 19.59-0.50391 38.848 5.1328 55.078 16.117 19.848 13.043 43.18 19.777 66.93 19.309 23.746 0.46875 47.082-6.2617 66.934-19.309 16.227-10.984 35.484-16.621 55.078-16.117 19.59-0.50391 38.844 5.1328 55.074 16.117 19.848 13.047 43.184 19.777 66.934 19.309 23.746 0.46875 47.082-6.2617 66.93-19.309 16.445-10.523 35.562-16.117 55.082-16.117 19.523 0 38.641 5.5938 55.082 16.117 20.051 12.613 43.258 19.309 66.945 19.309s46.891-6.6953 66.941-19.309c16.234-10.984 35.496-16.621 55.09-16.117 6.5195 0 11.809-5.2852 11.809-11.809v-82.656c0 6.5234-5.2891 11.809-11.809 11.809z" fill="#fff"/>
</g>
<g clip-path="url(#b)">
<path d="m966.05 670.85c-23.75-0.47266-47.09 6.2578-66.941 19.305-16.609 10.074-35.664 15.398-55.09 15.398s-38.477-5.3242-55.086-15.395c-20.254-12.039-43.379-18.391-66.938-18.391s-46.684 6.3516-66.938 18.387c-16.609 10.062-35.656 15.383-55.078 15.383-19.418 0-38.469-5.3164-55.078-15.379-20.18-12.238-43.332-18.711-66.93-18.711-23.602 0-46.75 6.4727-66.93 18.707-16.625 10.023-35.668 15.32-55.078 15.32-19.414 0-38.457-5.2969-55.078-15.316-19.852-13.047-43.184-19.781-66.934-19.309-6.5234 0-11.809 5.2852-11.809 11.805v82.656c0-6.5195 5.2852-11.805 11.809-11.805 23.75-0.47266 47.082 6.2617 66.934 19.309 16.566 10.176 35.633 15.566 55.078 15.566 19.441 0 38.508-5.3906 55.074-15.57 20.242-12.066 43.367-18.438 66.934-18.438 23.562 0 46.688 6.3711 66.93 18.441 16.566 10.18 35.633 15.57 55.078 15.57 19.445 0 38.512-5.3906 55.078-15.574 20.246-12.055 43.375-18.422 66.938-18.422 23.562 0.003906 46.691 6.3672 66.938 18.426 16.617 10.039 35.668 15.348 55.086 15.348s38.469-5.3086 55.09-15.352c19.852-13.047 43.191-19.777 66.941-19.305 6.5195 0 11.809 5.2852 11.809 11.805v-82.656c0-6.5195-5.2891-11.805-11.809-11.805z" fill="#fff"/>
</g>
<g clip-path="url(#a)">
<path d="m602.84 493.32 180.19 39.852v-104.38c0-6.2656-2.4883-12.273-6.918-16.699-4.4258-4.4297-10.434-6.918-16.699-6.918h-11.809v-35.422c0.003907-6.2656-2.4844-12.273-6.9141-16.703-4.4297-4.4297-10.438-6.9141-16.703-6.9141h-5.9023v-23.617c0-6.5195-5.2852-11.809-11.809-11.809-6.5195 0-11.809 5.2891-11.809 11.809v23.617h-47.23v-64.945c-0.019531-6.5117-5.2969-11.789-11.809-11.809h-23.617v-23.613h11.809c6.5234 0 11.809-5.2891 11.809-11.809 0-6.5234-5.2852-11.809-11.809-11.809h-47.23c-6.5234 0-11.809 5.2852-11.809 11.809 0 6.5195 5.2852 11.809 11.809 11.809h11.809v23.613h-23.617c-6.5156 0.019531-11.789 5.2969-11.809 11.809v64.945h-47.23v-23.617c0-6.5195-5.2891-11.809-11.809-11.809-6.5234 0-11.809 5.2891-11.809 11.809v23.617h-5.9062c-6.2617 0-12.27 2.4844-16.699 6.9141-4.4297 4.4297-6.918 10.438-6.9141 16.703v35.426l-11.809-0.003906c-6.2656 0-12.273 2.4883-16.703 6.918-4.4258 4.4258-6.9141 10.434-6.9141 16.699v104.5l180.78-39.969c1.6758-0.35547 3.4023-0.35547 5.0781 0zm79.82-29.105h-35.422v-35.426h35.426zm-206.64-94.465h247.97v35.426l-247.97-0.003906zm76.754 94.465h-35.426v-35.426h35.426zm23.617-35.426h47.23v35.426h-47.23z" fill="#fff"/>
</g>
<path d="m403.57 667.37c23.461-13.203 50.008-19.941 76.926-19.523 26.918 0.41406 53.242 7.9648 76.285 21.883 12.996 7.9922 27.957 12.223 43.215 12.223 15.262 0 30.219-4.2305 43.219-12.223 46.984-27.844 105.19-28.762 153.03-2.418 0.22266 0.035156 0.44141 0.09375 0.65234 0.17578 14.207-28.809 21.582-60.508 21.547-92.633-0.003907-5.5508-3.8516-10.359-9.2695-11.574l-208.88-46.344-209.47 46.344c-5.418 1.2148-9.2656 6.0234-9.2695 11.574-0.027344 32.121 7.3438 63.82 21.547 92.633 0.14453-0.082031 0.30859-0.12109 0.47266-0.11719zm256.47-70.73c1.4023-6.3633 7.6797-10.398 14.051-9.0352l43.336 9.3867v0.003907c5.8906 1.2891 9.8672 6.8008 9.2305 12.801-0.63281 5.9961-5.6797 10.555-11.711 10.578-0.85547-0.011719-1.707-0.10938-2.5391-0.29688l-43.336-9.3867c-6.3633-1.4023-10.398-7.6797-9.0312-14.051zm-179.36 0.35547 43.336-9.3867-0.003906-0.003907c3.0664-0.67969 6.2812-0.10937 8.9258 1.5859 2.6445 1.6914 4.5078 4.3672 5.1758 7.4375 0.66797 3.0703 0.085937 6.2812-1.6211 8.918-1.7031 2.6406-4.3867 4.4922-7.4609 5.1445l-43.336 9.3867c-0.83203 0.1875-1.6836 0.28516-2.5391 0.29688-6.0312-0.023438-11.074-4.582-11.711-10.578-0.63672-6 3.3398-11.512 9.2305-12.801z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@@ -4,18 +4,18 @@
# to a new directory along with this file.
# Then, run the script.
PYTHON_VER=3.9
PYTHON_VER=3.10
python${PYTHON_VER} -m venv buildEnv
source buildEnv/bin/activate
# orderedset package installed for compile time performance.
python${PYTHON_VER} -m pip install --upgrade wheel pip nuitka orderedset
python${PYTHON_VER} -m pip install --upgrade wheel pip nuitka ordered-set
python${PYTHON_VER} -m pip cache purge
python${PYTHON_VER} -m pip install -r requirements.txt
python${PYTHON_VER} -m pip install --upgrade -r requirements.txt
# Stop snscrape from messing with directories that will not exist in the final build.
echo "" > "buildEnv/lib/python${PYTHON_VER}/site-packages/snscrape/modules/__init__.py"
@@ -26,10 +26,10 @@ FIREFOX_VER=$(python -c "from pathlib import Path;x=Path(\"buildEnv/lib/python${
python${PYTHON_VER} -m nuitka --follow-imports --standalone --noinclude-pytest-mode=nofollow \
--noinclude-setuptools-mode=nofollow --noinclude-custom-mode=setuptools:error --noinclude-IPython-mode=nofollow \
--enable-plugin=pyside6 --enable-plugin=numpy --enable-plugin=trio --assume-yes-for-downloads --remove-output \
--noinclude-unittest-mode=nofollow --enable-plugin=pyside6 --enable-plugin=trio --assume-yes-for-downloads --remove-output \
--disable-console --include-data-dir="Resources=Resources" --include-plugin-directory=Modules --include-package=Core \
--include-data-dir="Core/Entities=Core/Entities" --include-data-dir="Core/Resolutions/Core=Core/Resolutions/Core" \
--include-data-dir="Modules=Modules" --warn-unusual-code --show-modules --include-data-files="Icon.ico=Icon.ico" \
--include-data-dir="Core/Entities=Core/Entities" \
--warn-unusual-code --show-modules --include-data-files="Icon.ico=Icon.ico" \
--linux-icon="Icon.ico" \
--include-package-data=playwright \
--include-package-data=folium --include-package-data=branca \
@@ -43,7 +43,11 @@ python${PYTHON_VER} -m nuitka --follow-imports --standalone --noinclude-pytest-m
--include-package=jellyfish \
--include-package=ipwhois \
--include-package=tweepy \
--include-data-dir="buildEnv/lib/python${PYTHON_VER}/site-packages/playwright/driver/package/.local-browsers/firefox-${FIREFOX_VER}/firefox=playwright/driver/package/.local-browsers/firefox-${FIREFOX_VER}/firefox" \
LinkScope.py
# We need the code, dll & etc files to be present.
cp -r "buildEnv/lib/python${PYTHON_VER}/site-packages/playwright/driver/package/.local-browsers/firefox-${FIREFOX_VER}/firefox" "LinkScope.dist/playwright/driver/package/.local-browsers/firefox-${FIREFOX_VER}"
cp -r Modules/ LinkScope.dist
cp -r Core/Resolutions LinkScope.dist/Core
deactivate

View File

@@ -8,17 +8,17 @@
:: PatchMagicWin.py to a new directory along with this file.
:: Then, run the script.
SET PYTHON_VER=3.9
SET PYTHON_VER=3.10
python -m venv buildEnv
call buildEnv\Scripts\activate.bat
python -m pip install --upgrade wheel pip nuitka orderedset
python -m pip install --upgrade wheel pip nuitka ordered-set
python -m pip cache purge
python -m pip install -r requirements.txt
python -m pip install --upgrade -r requirements.txt
:: Patch magic library with our own binaries
python PatchMagicWin.py
@@ -34,10 +34,10 @@ FOR /F "usebackq" %%L in (`python -c "from pathlib import Path;x=Path('buildEnv\
python -m nuitka --follow-imports --standalone --noinclude-pytest-mode=nofollow --noinclude-setuptools-mode=nofollow ^
--noinclude-custom-mode=setuptools:error --noinclude-IPython-mode=nofollow --enable-plugin=pyside6 ^
--enable-plugin=numpy --enable-plugin=trio --assume-yes-for-downloads --remove-output --disable-console ^
--noinclude-unittest-mode=nofollow --enable-plugin=trio --assume-yes-for-downloads --remove-output --disable-console ^
--include-data-dir="Resources=Resources" --include-plugin-directory=Modules --include-package=Core ^
--include-data-dir="Core\Entities=Core\Entities" --include-data-dir="Core\Resolutions\Core=Core\Resolutions\Core" ^
--include-data-dir="Modules=Modules" --warn-unusual-code --show-modules --include-data-files="Icon.ico=Icon.ico" ^
--include-data-dir="Core\Entities=Core\Entities" ^
--warn-unusual-code --show-modules --include-data-files="Icon.ico=Icon.ico" ^
--windows-icon-from-ico=".\Icon.ico" --include-data-dir="magic=magic" --windows-company-name=AccentuSoft ^
--windows-product-name="LinkScope Client" --windows-product-version="1.3.8.0" ^
--windows-file-description="LinkScope Client Software" ^
@@ -53,7 +53,11 @@ python -m nuitka --follow-imports --standalone --noinclude-pytest-mode=nofollow
--include-package=jellyfish ^
--include-package=ipwhois ^
--include-package=tweepy ^
--include-data-dir="buildEnv\Lib\site-packages\playwright\driver\package\.local-browsers\firefox-%FIREFOX_VER%\firefox=playwright\driver\package\.local-browsers\firefox-%FIREFOX_VER%\firefox" ^
".\LinkScope.py"
:: We need the code, dll & etc files to be present.
xcopy "buildEnv\Lib\site-packages\playwright\driver\package\.local-browsers\firefox-%FIREFOX_VER%\firefox" "LinkScope.dist\playwright\driver\package\.local-browsers\firefox-%FIREFOX_VER%" /S /E /Y
xcopy Modules\ LinkScope.dist /S /E /Y
xcopy Core\Resolutions LinkScope.dist\Core /S /E /Y
call buildEnv\Scripts\deactivate.bat

View File

@@ -1,4 +1,5 @@
PySide6
pyqtdarktheme
# For python-magic, the following packages may be required:
@@ -28,6 +29,10 @@ reportlab
pandas
svglib
lxml
# For opening spreadsheets
odfpy
openpyxl
xlrd # For old Excel formats
# Speeds up parsing of webpages by bs4
cchardet
bs4
@@ -42,7 +47,7 @@ playwright
requests
shodan
docx2python
PyPDF2
pypdf
beautifulsoup4
python-Wappalyzer
vtapi3