Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 57 additions & 2 deletions api_app/visualizers_manager/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,6 @@ def type(self) -> str:


class VisualizableDownload(VisualizableObject):

def __init__(
self,
value: str,
Expand Down Expand Up @@ -189,6 +188,63 @@ def attributes(self) -> List[str]:
]


class VisualizableImage(VisualizableObject):
"""
A visualizable component for displaying images in the visualizer.

Supports two image sources:
- url: A URL to fetch the image from (e.g., URLscan screenshot)
- base64: Base64 encoded image data

At least one of url or base64 must be provided.
If both are provided, url takes precedence.
"""

def __init__(
self,
value: str = "",
url: str = "",
base64: str = "",
title: str = "",
description: str = "",
max_width: int = 500,
max_height: int = 400,
allow_expand: bool = True,
size: VisualizableSize = VisualizableSize.S_AUTO,
alignment: VisualizableAlignment = VisualizableAlignment.CENTER,
disable: bool = False,
):
if not url and not base64:
raise ValueError("VisualizableImage requires either 'url' or 'base64'")

super().__init__(size, alignment, disable)
self.value = value or title # value is used as alt text
self.url = url
self.base64 = base64
self.title = title
self.description = description
self.max_width = max_width
self.max_height = max_height
self.allow_expand = allow_expand

@property
def type(self) -> str:
return "image"

@property
def attributes(self) -> List[str]:
return super().attributes + [
"value",
"url",
"base64",
"title",
"description",
"max_width",
"max_height",
"allow_expand",
]


class VisualizableBool(VisualizableBase):
def __init__(
self,
Expand Down Expand Up @@ -553,7 +609,6 @@ def get_pivots_reports(self) -> QuerySet:
return PivotReport.objects.filter(job=self._job)

def get_data_models(self) -> QuerySet:

data_model_class = self._job.analyzable.get_data_model_class()
analyzer_reports_pk = [report.pk for report in self.get_analyzer_reports()]
return data_model_class.objects.filter(analyzers_report__in=analyzer_reports_pk)
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export const VisualizerComponentType = Object.freeze({
TITLE: "title",
TABLE: "table",
DOWNLOAD: "download",
IMAGE: "image",
});
212 changes: 212 additions & 0 deletions frontend/src/components/common/visualizer/elements/image.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
// This file is a part of IntelOwl https://github.com/intelowlproject/IntelOwl
// See the file 'LICENSE' for copying permission.

import React from "react";
import PropTypes from "prop-types";
import { Modal, ModalBody, ModalHeader, UncontrolledTooltip } from "reactstrap";
import { MdBrokenImage, MdZoomIn } from "react-icons/md";

/**
* ImageVisualizer component for displaying images in the visualizer.
* Supports both URL and base64 encoded images.
* Includes click-to-zoom functionality with a modal preview.
*/
export function ImageVisualizer({
id,
url,
base64,
title,
description,
maxWidth,
maxHeight,
allowExpand,
disable,
size,
}) {
const [isModalOpen, setIsModalOpen] = React.useState(false);
const [hasError, setHasError] = React.useState(false);
const [isLoading, setIsLoading] = React.useState(true);

// Determine the image source - URL takes precedence over base64
const imageSrc = React.useMemo(() => {
if (url) return url;
if (base64) {
// Handle base64 with or without data URI prefix
if (base64.startsWith("data:")) {
return base64;
}
// Default to PNG if no prefix provided
return `data:image/png;base64,${base64}`;
}
return null;
}, [url, base64]);

const toggleModal = () => {
if (!disable && allowExpand && !hasError) {
setIsModalOpen(!isModalOpen);
}
};

const handleImageLoad = () => {
setIsLoading(false);
setHasError(false);
};

const handleImageError = () => {
setIsLoading(false);
setHasError(true);
};

// Reset states when image source changes
React.useEffect(() => {
setIsLoading(true);
setHasError(false);
}, [imageSrc]);

const containerStyle = {
maxWidth: maxWidth || "500px",
display: "flex",
flexDirection: "column",
alignItems: "center",
};

const imageStyle = {
maxWidth: "100%",
maxHeight: maxHeight || "400px",
objectFit: "contain",
cursor: !disable && allowExpand && !hasError ? "pointer" : "default",
opacity: disable ? 0.5 : 1,
};

const renderImage = () => {
if (hasError) {
return (
<div
className="d-flex flex-column align-items-center justify-content-center text-muted p-3"
style={{ minHeight: 100 }}
>
<MdBrokenImage size={48} />
<span className="mt-2">Failed to load image</span>
</div>
);
}

const imageElement = (
<img
src={imageSrc}
alt={title || "Visualizer Image"}
style={{ ...imageStyle, display: isLoading ? "none" : "block" }}
onLoad={handleImageLoad}
onError={handleImageError}
/>
);

return (
<>
{isLoading && (
<div
className="d-flex align-items-center justify-content-center text-muted p-3"
style={{ minHeight: 100 }}
>
<span className="spinner-border spinner-border-sm me-2" />
Loading...
</div>
)}
{allowExpand && !disable ? (
<button
type="button"
onClick={toggleModal}
style={{
border: "none",
background: "transparent",
padding: 0,
cursor: "pointer",
}}
>
{imageElement}
</button>
) : (
imageElement
)}
</>
);
};

if (!imageSrc) {
return (
<div className={`${size} text-muted text-center p-3`}>
No image source provided
</div>
);
}

return (
<div id={id} className={`${size} p-2`} style={containerStyle}>
{title && <div className="fw-bold mb-2 text-center">{title}</div>}
<div className="position-relative">
{renderImage()}
{!hasError && !isLoading && allowExpand && !disable && (
<>
<MdZoomIn
id={`${id}-zoom-icon`}
size={24}
className="position-absolute text-white bg-dark bg-opacity-50 rounded p-1"
style={{ bottom: 8, right: 8, cursor: "pointer" }}
onClick={toggleModal}
/>
<UncontrolledTooltip target={`${id}-zoom-icon`} placement="top">
Click to enlarge
</UncontrolledTooltip>
</>
)}
</div>
{description && (
<div className="text-muted small mt-2 text-center">{description}</div>
)}

{/* Modal for expanded view */}
<Modal isOpen={isModalOpen} toggle={toggleModal} size="xl" centered>
<ModalHeader toggle={toggleModal}>
{title || "Image Preview"}
</ModalHeader>
<ModalBody className="text-center p-4">
<img
src={imageSrc}
alt={title || "Visualizer Image"}
style={{
maxWidth: "100%",
maxHeight: "80vh",
objectFit: "contain",
}}
/>
{description && <p className="text-muted mt-3 mb-0">{description}</p>}
</ModalBody>
</Modal>
</div>
);
}

ImageVisualizer.propTypes = {
id: PropTypes.string.isRequired,
url: PropTypes.string,
base64: PropTypes.string,
title: PropTypes.string,
description: PropTypes.string,
maxWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
maxHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
allowExpand: PropTypes.bool,
disable: PropTypes.bool,
size: PropTypes.string,
};

ImageVisualizer.defaultProps = {
url: "",
base64: "",
title: "",
description: "",
maxWidth: "500px",
maxHeight: "400px",
allowExpand: true,
disable: false,
size: "col-auto",
};
10 changes: 10 additions & 0 deletions frontend/src/components/common/visualizer/validators.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,16 @@ function parseElementFields(rawElement) {

// validation for the elements
switch (validatedFields.type) {
case VisualizerComponentType.IMAGE: {
validatedFields.url = parseString(rawElement.url);
validatedFields.base64 = parseString(rawElement.base64);
validatedFields.title = parseString(rawElement.title);
validatedFields.description = parseString(rawElement.description);
validatedFields.maxWidth = parseString(rawElement.max_width || "500px");
validatedFields.maxHeight = parseString(rawElement.max_height || "400px");
validatedFields.allowExpand = parseBool(rawElement.allow_expand ?? true);
break;
}
case VisualizerComponentType.DOWNLOAD: {
validatedFields.value = parseString(rawElement.value);
validatedFields.mimetype = parseMimetype(rawElement.mimetype);
Expand Down
19 changes: 19 additions & 0 deletions frontend/src/components/common/visualizer/visualizer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { BooleanVisualizer } from "./elements/bool";
import { BaseVisualizer } from "./elements/base";
import { VerticalListVisualizer } from "./elements/verticalList";
import { TitleVisualizer } from "./elements/title";
import { ImageVisualizer } from "./elements/image";

import { VisualizerComponentType } from "./elements/const";
import { getIcon } from "../icon/icons";
Expand All @@ -27,6 +28,24 @@ import { DownloadVisualizer } from "./elements/download";
function convertToElement(element, idElement, isChild = false) {
let visualizerElement;
switch (element.type) {
case VisualizerComponentType.IMAGE: {
visualizerElement = (
<ImageVisualizer
key={idElement}
id={idElement}
size={element.size}
disable={element.disable}
url={element.url}
base64={element.base64}
title={element.title}
description={element.description}
maxWidth={element.maxWidth}
maxHeight={element.maxHeight}
allowExpand={element.allowExpand}
/>
);
break;
}
case VisualizerComponentType.DOWNLOAD: {
visualizerElement = (
<DownloadVisualizer
Expand Down
Loading
Loading