Skip to content

Commit

Permalink
Merge pull request #73 from target/tlsh-update
Browse files Browse the repository at this point in the history
Adding TLSH Card
  • Loading branch information
phutelmyer authored Mar 4, 2024
2 parents ef640e3 + b50442b commit f0f6cec
Show file tree
Hide file tree
Showing 8 changed files with 377 additions and 21 deletions.
2 changes: 1 addition & 1 deletion app/blueprints/strelka.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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(
Expand Down
161 changes: 161 additions & 0 deletions ui/src/components/FileComponents/TlshOverviewCard.js
Original file line number Diff line number Diff line change
@@ -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 = (
<Text style={{fontSize: "11px"}}>
This file has a TLSH match against the family{" "}
<Text style={{fontSize: "11px"}} code copyable>
{family}
</Text>{" "}
with the TLSH{" "}
<Text style={{fontSize: "11px"}} code copyable>
{matchedTlsh}
</Text>
<br></br>
The TLSH for this file was given a comparison score of{" "}
<Tag style={{fontSize: "11px"}} color={getColorForDescription(getSimilarityDescription(score))}>
{score}
</Tag>
which indicates the two files may be{" "}
<Tag style={{fontSize: "11px"}} color={getColorForDescription(getSimilarityDescription(score))}>
{getSimilarityDescription(score)}
</Tag>
<br></br>
<div style={{ paddingTop: "20px" }}>
<Text style={{fontSize: "11px"}} type="secondary">
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.
</Text>
</div>
</Text>
);

// 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 (
<div className="tlsh-overview-card">
<div style={{ textAlign: "center" }}>
<Text strong className="score-heading">
TLSH Comparison Scale
</Text>
<Text style={{ fontSize: "10px" }}>(Lower = More Similar)</Text>
</div>
<div
className="slider-boxes"
style={{ paddingLeft: "15px", paddingRight: "15px" }}
>
{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 (
<div
key={index}
className={boxClass}
style={{
backgroundColor,
"--box-glow-color": isActive ? backgroundColor : "transparent",
}}
>
{/* Render the actual score inside the box where it resides */}
{index === scoreIndex && (
<Text className="score-text">{score}</Text>
)}
{/* Render the tick values underneath the box its aligned to */}
{scoreKeys.includes(index) && (
<div className="tick-text">
<span>{scorePositions[index][0]}</span>
<br />
<span>{scorePositions[index][1]}</span>
</div>
)}
</div>
);
})}
</div>
<div style={{ textAlign: "center", paddingTop: "60px" }}>
<div style={{ borderTop: "1px solid #eee", paddingTop: "20px" }}>
{tlshDescriptionSentence}
</div>
</div>
</div>
);
};

export default TlshOverviewCard;
24 changes: 12 additions & 12 deletions ui/src/components/FileFlow/NodeTypes/EventNode.js
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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%;
Expand Down Expand Up @@ -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",
Expand All @@ -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;

Expand Down Expand Up @@ -321,6 +314,13 @@ const EventNode = memo(({ data, selected }) => {
<Tag color="default">
{data.nodeMetric} {data.nodeMetricLabel}
</Tag>
{tlshResponse && (
<Tag style={{
margin: "2px",
fontWeight: "500",
fontSize: "11px",
}} color="red">TLSH Related Match: {tlshResponse}</Tag>
)}
</div>
</RightWrapper>
<Handle
Expand Down
2 changes: 1 addition & 1 deletion ui/src/components/SubmissionTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -655,7 +655,7 @@ const SubmissionTable = () => {
Search Filter
</Text>
<Input.Search
placeholder="Search by File Name or Submission Description..."
placeholder="Search by File Name, Submission Description, Uploader, or YARA Matches..."
onChange={(e) => debouncedSearchChange(e)}
style={{ fontSize: "12px" }}
/>
Expand Down
62 changes: 58 additions & 4 deletions ui/src/pages/SubmissionView.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import VbOverviewCard from "../components/FileComponents/VbOverviewCard";
import JavascriptOverviewCard from "../components/FileComponents/JavascriptOverviewCard";
import EmailOverviewCard from "../components/FileComponents/EmailOverviewCard";
import QrOverviewCard from "../components/FileComponents/QrOverviewCard";
import TlshOverviewCard from "../components/FileComponents/TlshOverviewCard";


import { getIconConfig } from "../utils/iconMappingTable";

Expand Down Expand Up @@ -146,6 +148,20 @@ const SubmissionsPage = (props) => {
};
};

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) {
Expand Down Expand Up @@ -194,7 +210,7 @@ const SubmissionsPage = (props) => {

useEffect(() => {
let mounted = true;

fetchWithTimeout(`${APP_CONFIG.BACKEND_URL}/strelka/scans/${id}`, {
method: "GET",
mode: "cors",
Expand Down Expand Up @@ -225,12 +241,12 @@ const SubmissionsPage = (props) => {
setFileDepthView(res.strelka_response[0]?.file?.depth || "");
}
});

return function cleanup() {
mounted = false;
};
}, [handle401, id]);


return isLoading ? (
<div
Expand Down Expand Up @@ -419,7 +435,45 @@ const SubmissionsPage = (props) => {
</Collapse.Panel>
</Collapse>
)}

{selectedNodeData && selectedNodeData.scan?.tlsh?.match && (
<Collapse
defaultActiveKey={[]}
style={{ width: "100%", marginBottom: "10px" }}
>
<Collapse.Panel
header={
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<div>
<Text strong>TLSH Related Match</Text>
<div style={{ fontSize: "smaller", color: "#888" }}>
Match Family:{" "}
{selectedNodeData.scan.tlsh.match?.family}
</div>
</div>
{(() => {
const { label, color } = getTlshRating(
selectedNodeData.scan.tlsh.match.score
);
return (
<Tag color={color}>
<b>{label}</b>
</Tag>
);
})()}
</div>
}
key="1"
>
<TlshOverviewCard data={selectedNodeData} />
</Collapse.Panel>
</Collapse>
)}
{selectedNodeData &&
selectedNodeData.scan.header &&
selectedNodeData.scan.footer && (
Expand Down
Loading

0 comments on commit f0f6cec

Please sign in to comment.