From 6cb843af3d35ba01db0f286918683bc88594084a Mon Sep 17 00:00:00 2001 From: Paul Hutelmyer Date: Mon, 4 Mar 2024 12:57:16 -0500 Subject: [PATCH 1/3] Adding TLSH and QR Fix --- .../FileComponents/TlshOverviewCard.js | 161 ++++++++++++++++++ .../FileFlow/NodeTypes/EventNode.js | 24 +-- ui/src/pages/SubmissionView.js | 62 ++++++- ui/src/styles/TlshOverviewCard.css | 126 ++++++++++++++ ui/src/utils/indexDataUtils.js | 8 + ui/src/utils/layoutUtils.js | 13 +- 6 files changed, 375 insertions(+), 19 deletions(-) create mode 100644 ui/src/components/FileComponents/TlshOverviewCard.js create mode 100644 ui/src/styles/TlshOverviewCard.css diff --git a/ui/src/components/FileComponents/TlshOverviewCard.js b/ui/src/components/FileComponents/TlshOverviewCard.js new file mode 100644 index 0000000..74a008f --- /dev/null +++ b/ui/src/components/FileComponents/TlshOverviewCard.js @@ -0,0 +1,161 @@ +import React from "react"; +import { Typography, Tag } from "antd"; +import { antdColors } from "../../utils/colors"; +import "../../styles/TlshOverviewCard.css"; + +const { Text } = Typography; + +const TlshOverviewCard = ({ data }) => { + const { match } = data.scan.tlsh; + const score = match.score; + const family = match.family; + const matchedTlsh = match.tlsh; + + // Define the indexes where we want to show the scores + const scorePositions = { + 0: ["0", "Very Similar"], + 6: ["75", "Somewhat Similar"], + 14: ["150", "Moderately Different"], + 22: ["225", "Quite Different"], + 29: ["300+", "Very Different"], + }; + + // Logic to determine the similarity description based on the score + const getSimilarityDescription = (score) => { + if (score < 30) return "Very Similar"; + if (score < 75) return "Somewhat Similar"; + if (score < 150) return "Moderately Different"; + if (score < 225) return "Quite Different"; + return "Very Different"; // Assuming scores higher than 225 are 'Very Different' + }; + + // Function to get color based on similarity description + const getColorForDescription = (description) => { + const colorMapping = { + "Very Similar": "red", + "Somewhat Similar": "volcano", + "Moderately Different": "orange", + "Quite Different": "gold", + "Very Different": "lime", + }; + return colorMapping[description] || antdColors.gray; + }; + + // Construct the sentence with the provided values + const tlshDescriptionSentence = ( + + This file has a TLSH match against the family{" "} + + {family} + {" "} + with the TLSH{" "} + + {matchedTlsh} + +

+ The TLSH for this file was given a comparison score of{" "} + + {score} + + which indicates the two files may be{" "} + + {getSimilarityDescription(score)} + +

+
+ + The results provided are an estimation of similarity and may not be completely accurate. The accuracy of the TLSH comparison can vary significantly between different types of files, typically providing more reliable results for executable files than for text files. + +
+
+ ); + + // antd color gradient + const colorGradient = [ + antdColors.red, // Very Similar + antdColors.volcano, + antdColors.orange, + antdColors.gold, + antdColors.lime, + antdColors.green, // Very Different + ]; + + // Assume 300 is the highest score for TLSH comparison (It's not, but let's pretend) + const maxScore = 300; + // Divide the slider into 'n' parts + const numberOfParts = 30; + + // Calculate the appropriate index for the actual score to be displayed + const scoreIndex = + score >= maxScore + ? numberOfParts - 1 + : Math.floor((score / maxScore) * numberOfParts); + // Define the keys where we want to show the scores + const scoreKeys = Object.keys(scorePositions).map(Number); + + // Update the color gradient logic to handle scores of 300 or higher + const getColorForIndex = (index) => { + if (index === numberOfParts - 1 && score >= maxScore) { + return antdColors.green; + } + return index <= scoreIndex + ? colorGradient[ + Math.floor((colorGradient.length - 1) * (index / (numberOfParts - 1))) + ] + : antdColors.lightGray; + }; + + return ( +
+
+ + TLSH Comparison Scale + + (Lower = More Similar) +
+
+ {Array.from({ length: numberOfParts }).map((_, index) => { + const isActive = index <= scoreIndex; + const backgroundColor = getColorForIndex(index); + const boxClass = `slider-box ${isActive ? "active" : ""} ${ + index === scoreIndex ? "score-box" : "" + }`; + + return ( +
+ {/* Render the actual score inside the box where it resides */} + {index === scoreIndex && ( + {score} + )} + {/* Render the tick values underneath the box its aligned to */} + {scoreKeys.includes(index) && ( +
+ {scorePositions[index][0]} +
+ {scorePositions[index][1]} +
+ )} +
+ ); + })} +
+
+
+ {tlshDescriptionSentence} +
+
+
+ ); +}; + +export default TlshOverviewCard; diff --git a/ui/src/components/FileFlow/NodeTypes/EventNode.js b/ui/src/components/FileFlow/NodeTypes/EventNode.js index 91f8ef2..d92da84 100644 --- a/ui/src/components/FileFlow/NodeTypes/EventNode.js +++ b/ui/src/components/FileFlow/NodeTypes/EventNode.js @@ -1,7 +1,7 @@ import { useState, memo } from "react"; import styled, { createGlobalStyle } from "styled-components"; import { CameraOutlined, QrcodeOutlined } from "@ant-design/icons"; -import { Button, Tag, Tooltip } from "antd"; +import { Tag, Tooltip } from "antd"; import { Handle, Position } from "reactflow"; import { getIconConfig } from "../../../utils/iconMappingTable"; @@ -107,21 +107,13 @@ const ImageTooltip = styled(Tooltip)` align-items: center; justify-content: center; padding: 0 !important; - overflow: hidden; + overflow: hidden; } .ant-tooltip-inner img { pointer-events: auto; } `; -const UnblurButton = styled(Button)` - position: absolute; - top: 0; - right: 0; - transform: translate(50%, -50%); - z-index: 20; // Ensure it's above the image/QR code preview -`; - const PreviewImage = styled.img` max-width: 100%; max-height: 100%; @@ -254,9 +246,8 @@ const EventNode = memo(({ data, selected }) => { // Initialize isBlurred based on the presence of data.nodeQrData const [isBlurred, setIsBlurred] = useState(!!data.nodeQrData); - // Example of conditional styling for blur effect - const previewStyle = isBlurred ? { filter: 'blur(4px)' } : {}; + const previewStyle = isBlurred ? { filter: "blur(4px)" } : {}; const handleStyle = { backgroundColor: "#aaa", @@ -271,6 +262,8 @@ const EventNode = memo(({ data, selected }) => { const color = mappingEntry?.color || data.color; const hasImage = Boolean(data.nodeImage); const virusTotalResponse = data.nodeVirustotal; + const tlshResponse = data.nodeTlshData.family; + data.nodeAlert = typeof virusTotalResponse === "number" && virusTotalResponse > 5; @@ -321,6 +314,13 @@ const EventNode = memo(({ data, selected }) => { {data.nodeMetric} {data.nodeMetricLabel} + {tlshResponse && ( + TLSH Related Match: {tlshResponse} + )} { }; }; + const getTlshRating = (score) => { + const scoreRanges = [ + { max: 30, label: "Very Similar", color: "error" }, + { max: 60, label: "Somewhat Similar", color: "orange" }, + { max: 120, label: "Moderately Different", color: "gold" }, + { max: 180, label: "Quite Different", color: "lime" }, + { max: 300, label: "Very Different", color: "green" }, + ]; + + // Find the first range where the score is less than the max + const range = scoreRanges.find((r) => score <= r.max); + return range || scoreRanges[scoreRanges.length - 1]; // default to the last range if not found + }; + const getFilteredData = () => { let filteredData = ""; if (selectedNodeData) { @@ -194,7 +210,7 @@ const SubmissionsPage = (props) => { useEffect(() => { let mounted = true; - + fetchWithTimeout(`${APP_CONFIG.BACKEND_URL}/strelka/scans/${id}`, { method: "GET", mode: "cors", @@ -225,12 +241,12 @@ const SubmissionsPage = (props) => { setFileDepthView(res.strelka_response[0]?.file?.depth || ""); } }); - + return function cleanup() { mounted = false; }; }, [handle401, id]); - + return isLoading ? (
{ )} - + {selectedNodeData && selectedNodeData.scan?.tlsh?.match && ( + + +
+ TLSH Related Match +
+ Match Family:{" "} + {selectedNodeData.scan.tlsh.match?.family} +
+
+ {(() => { + const { label, color } = getTlshRating( + selectedNodeData.scan.tlsh.match.score + ); + return ( + + {label} + + ); + })()} +
+ } + key="1" + > + + + + )} {selectedNodeData && selectedNodeData.scan.header && selectedNodeData.scan.footer && ( diff --git a/ui/src/styles/TlshOverviewCard.css b/ui/src/styles/TlshOverviewCard.css new file mode 100644 index 0000000..d2e0d37 --- /dev/null +++ b/ui/src/styles/TlshOverviewCard.css @@ -0,0 +1,126 @@ +/* TlshOverviewCard.css */ + +.tlsh-overview-card { + padding-left: 20px; + padding-bottom: 10px; + background: #fff; + border-radius: 8px; +} + + +.score-value { + font-size: 1.5em; + color: #333; + font-weight: bold; +} +.card-header { + align-items: center; + margin-bottom: 10px; +} + +.card-header > :not(:last-child) { + margin-right: 8px; +} + +.slider-boxes { + display: flex; + height: 20px; + margin-top: 10px; + position: relative; +} + +.slider-box { + flex-grow: 1; + margin: 0 2px; + background-color: #eee; + position: relative; + display: flex; + justify-content: center; + align-items: flex-end; +} + +.score-value { + font-size: 1.5em; + color: #333; + font-weight: bold; +} + +.score-heading { + display: block; + +} + +/* If the score is 0, the first box should be red */ +.slider-box.first-box { + background-color: "#ff4d4f"; /* Use the red color from antdColors */ + animation: pulseGlow 2s infinite; +} + +/* Style for the box that contains the score */ +.slider-box.score-box { + animation: pulseGlow 2s infinite; +} + +/* Keyframes for the pulsing glow effect */ +@keyframes pulseGlow { + 0%, 100% { + box-shadow: 0 0 8px var(--box-glow-color); + } + 50% { + box-shadow: 0 0 15px var(--box-glow-color); + } +} + + +.score-text { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 11px; + font-weight: 800; + color: white; + word-break: keep-all; +} + +.tick-text { + position: absolute; + bottom: -40px; + width: 120px; + text-align: center; + font-weight:600; + font-size: 11px; + word-break: keep-all; +} + +.score-markers { + display: flex; + position: absolute; + width: 100%; + bottom: -20px; +} + +.score-marker { + position: absolute; + text-align: center; + transform: translateX(-50%); +} + +.label-text { + position: absolute; + top: 20px; + width: 100%; + text-align: center; + font-size: 12px; +} + +.active-marker:before { + content: ""; + position: absolute; + left: 50%; + top: -15px; + transform: translateX(-50%); + width: 2px; + height: 10px; + background-color: black; +} diff --git a/ui/src/utils/indexDataUtils.js b/ui/src/utils/indexDataUtils.js index 5533795..3a2fcc3 100644 --- a/ui/src/utils/indexDataUtils.js +++ b/ui/src/utils/indexDataUtils.js @@ -38,6 +38,7 @@ export const indexDataType = (index, data) => { nodeDisposition: "", nodeMain: "", nodeSub: "", + nodeTlsh: "", nodeLabel: "", nodeYaraList: "", nodeMetric: "", @@ -53,6 +54,12 @@ export const indexDataType = (index, data) => { qrData = data.scan.qr.data; } + // Check if TLSH data exists and add to nodeData + let tlshData = ""; + if (data.scan?.tlsh?.match) { + tlshData = data.scan.tlsh.match; + } + // Extracting the base64_thumbnail from _any_ scanner, if present let base64Thumbnail = ""; if (data["scan"]) { @@ -80,6 +87,7 @@ export const indexDataType = (index, data) => { data["enrichment"]?.["virustotal"] !== undefined ? data["enrichment"]["virustotal"] : "Not Found", + nodeTlshData: tlshData, nodeInsights: data?.insights?.length, nodeIocs: data?.iocs?.length, nodeImage: base64Thumbnail, diff --git a/ui/src/utils/layoutUtils.js b/ui/src/utils/layoutUtils.js index de1e451..8b08836 100644 --- a/ui/src/utils/layoutUtils.js +++ b/ui/src/utils/layoutUtils.js @@ -84,7 +84,13 @@ export function transformElasticSearchDataToElements(results) { const edges = []; let rootNodes = new Set(); let nodeIdsToIndices = new Map(); - + let qrDataPresent = false; + + results.forEach((result) => { + if (result.scan?.qr?.data){ + qrDataPresent = true + } + }) results.forEach((result) => { result.index = "strelka" @@ -104,11 +110,12 @@ export function transformElasticSearchDataToElements(results) { nodeInsights: nodeData.nodeInsights, nodeIocs: nodeData.nodeIocs, nodeImage: nodeData.nodeImage, - nodeQrData: nodeData.nodeQrData, + nodeQrData: qrDataPresent || nodeData.nodeQrData, nodeMain: nodeData.nodeMain, nodeSub: nodeData.nodeSub, nodeLabel: nodeData.nodeLabel, nodeMetric: nodeData.nodeMetric, + nodeTlshData: nodeData.nodeTlshData, nodeMetricLabel: nodeData.nodeMetricLabel, nodeYaraList: nodeData.nodeYaraList, nodeParentId: nodeData.nodeParentId, @@ -187,4 +194,4 @@ export function transformElasticSearchDataToElements(results) { nodes, edges, }; -} \ No newline at end of file +} From 749487efb361c468d62668ccf87a31ea7e789b2b Mon Sep 17 00:00:00 2001 From: Paul Hutelmyer Date: Mon, 4 Mar 2024 12:57:32 -0500 Subject: [PATCH 2/3] Updating placeholder due to more search capability --- ui/src/components/SubmissionTable.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/components/SubmissionTable.js b/ui/src/components/SubmissionTable.js index 95ecfdf..2cd83a3 100644 --- a/ui/src/components/SubmissionTable.js +++ b/ui/src/components/SubmissionTable.js @@ -655,7 +655,7 @@ const SubmissionTable = () => { Search Filter debouncedSearchChange(e)} style={{ fontSize: "12px" }} /> From c3e26406e53918c6138be7a9288269edfa0204cb Mon Sep 17 00:00:00 2001 From: Paul Hutelmyer Date: Mon, 4 Mar 2024 12:58:11 -0500 Subject: [PATCH 3/3] Bug fix for missing key --- app/blueprints/strelka.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/blueprints/strelka.py b/app/blueprints/strelka.py index 1aa94a1..ea6bba2 100644 --- a/app/blueprints/strelka.py +++ b/app/blueprints/strelka.py @@ -77,6 +77,7 @@ def submit_file( None. """ submitted_hash = "" + total_scanned_with_hits = [] if "file" not in request.files and b"hash" not in request.data: return ( @@ -114,7 +115,6 @@ def submit_file( submitted_description = submission["description"] submitted_hash = submission["hash"] submitted_type = "virustotal" - total_scanned_with_hits = [] if os.environ.get("VIRUSTOTAL_API_KEY"): file = create_vt_zip_and_download(