LitterBox v1.6.0

This commit is contained in:
BlackSnufkin
2025-01-26 12:56:03 -08:00
parent 23dd88339d
commit 11ee5ddde3
9 changed files with 399 additions and 146 deletions
+8 -1
View File
@@ -2,6 +2,13 @@
All notable changes to this project will be documented in this file.
## [v1.6.0] - 2025-01-26
### Added
- New Static analyzer Stringnalyzer implementation
### Fixed
- Refactoring health check implementation to use configuration file settings
## [v1.5.1] - 2025-01-25
### Added
- Support for executing payloads with custom command-line arguments
@@ -15,7 +22,7 @@ All notable changes to this project will be documented in this file.
## [v1.5.0] - 2025-01-11
### Added
- LitterBox RedEdr Scanner implementation
- New Dynamic analyzer RedEdr Scanner implementation
- Added LICENSE file
### Changed
+6
View File
@@ -36,6 +36,12 @@ analysis:
command: "{tool_path} -m -r -f {file_path}"
timeout: 120
stringnalyzer:
enabled: true
tool_path: ".\\Scanners\\Stringnalyzer\\Stringnalyzer.exe "
command: "{tool_path} --wide -f {file_path}"
timeout: 120
dynamic:
yara:
enabled: true
+2 -2
View File
@@ -58,20 +58,20 @@ Features include:
- Injection technique analysis
- Sleep pattern monitoring
- Collect Windows telemetry via ETW
- PE integrity verification
## Integrated Tools
### Static Analysis Suite
- [YARA](https://github.com/elastic/protections-artifacts/tree/main/yara) - Pattern matching and signature detection
- [CheckPlz](https://github.com/BlackSnufkin/CheckPlz) - AV detection testing
- [Stringnalyzer](https://github.com/BlackSnufkin/Rusty-Playground/Stringnalyzer) - Payload Strings analyzer (Note: May produce false positives)
### Dynamic Analysis Suite
- [YARA](https://github.com/elastic/protections-artifacts/tree/main/yara) (memory scanning) - Runtime pattern detection
- [PE-Sieve](https://github.com/hasherezade/pe-sieve) - Detecting and dumping in-memory malware implants and advanced process injection techniques
- [Moneta](https://github.com/forrest-orr/moneta) - Usermode memory analysis tool to detect malware IOCs
- [Patriot](https://github.com/BlackSnufkin/patriot) - Detecting various kinds of in-memory stealth techniques
- [RedEdr](https://github.com/dobin/RedEdr) - Collect Windows telemetry via ETW providers
- [RedEdr](https://github.com/dobin/RedEdr) - Collect Windows telemetry via ETW providers (Note: See issue #6)
- [Hunt-Sleeping-Beacons](https://github.com/thefLink/Hunt-Sleeping-Beacons) - Beacon behavior analysis
## Web Endpoint Reference
Binary file not shown.
+3 -2
View File
@@ -1,6 +1,5 @@
# app/analyzers/manager.py
import logging
import subprocess
import time
@@ -12,6 +11,7 @@ from abc import ABC, abstractmethod
# Import analyzers
from .static.yara_analyzer import YaraStaticAnalyzer
from .static.checkplz_analyzer import CheckPlzAnalyzer
from .static.stringnalyzer_analyzer import StringsAnalyzer
from .dynamic.yara_analyzer import YaraDynamicAnalyzer
from .dynamic.pe_sieve_analyzer import PESieveAnalyzer
from .dynamic.moneta_analyzer import MonetaAnalyzer
@@ -32,7 +32,8 @@ class AnalysisManager:
# Define analyzer mappings
STATIC_ANALYZERS = {
'yara': YaraStaticAnalyzer,
'checkplz': CheckPlzAnalyzer
'checkplz': CheckPlzAnalyzer,
'stringnalyzer': StringsAnalyzer
}
DYNAMIC_ANALYZERS = {
@@ -0,0 +1,102 @@
import subprocess
import json
import os
from .base import StaticAnalyzer
class StringsAnalyzer(StaticAnalyzer):
def analyze(self, file_path):
"""
Analyzes a file using strings analysis tool specified in the config.
"""
try:
tool_config = self.config['analysis']['static']['stringnalyzer']
command = tool_config['command'].format(
tool_path=os.path.abspath(tool_config['tool_path']),
file_path=os.path.abspath(file_path)
)
process = subprocess.Popen(
command,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
cwd=os.path.dirname(os.path.abspath(tool_config['tool_path']))
)
stdout, stderr = process.communicate(timeout=tool_config.get('timeout', 300))
# Parse the JSON output
results = self._parse_output(stdout)
self.results = {
'status': 'completed' if process.returncode == 0 else 'failed',
'scan_info': {
'target': file_path,
'tool': 'Stringnalyzer'
},
'findings': results,
'errors': stderr if stderr else None
}
except Exception as e:
self.results = {
'status': 'error',
'error': str(e)
}
def _parse_output(self, output):
"""
Parse the strings tool JSON output into structured data.
Returns a dictionary containing the parsed results.
"""
try:
# Try to parse the JSON output
results = json.loads(output)
# Ensure all expected fields are present, initialize if missing
default_fields = {
'file_path': None,
'total_strings': 0,
'all_strings': [],
'found_error_messages': [],
'found_functions': [],
'found_url': [],
'found_dll': [],
'found_ip': [],
'found_path': [],
'found_file': [],
'found_commands': [],
'found_suspicious_strings': [],
'found_suspicious_functions': [],
'found_network_indicators': [],
'found_registry_keys': [],
'found_interesting_strings': [],
'found_file_operations': [],
'found_emails': [],
'found_domains': []
}
# Update default fields with actual results
for key in default_fields:
if key not in results:
results[key] = default_fields[key]
return results
except json.JSONDecodeError as e:
return {
'error': f'Failed to parse JSON output: {str(e)}',
**default_fields
}
except Exception as e:
return {
'error': f'Unexpected error parsing output: {str(e)}',
**default_fields
}
def cleanup(self):
"""
Cleanup any temporary files or processes.
"""
pass
+27 -14
View File
@@ -724,7 +724,7 @@ def register_routes(app):
analysis_config = config.get('analysis', {})
issues = []
# Check upload folder accessibility
# Simple upload folder check
upload_folder = upload_config.get('upload_folder')
if not upload_folder:
app.logger.warning("Upload folder path is not configured.")
@@ -732,9 +732,6 @@ def register_routes(app):
elif not os.path.isdir(upload_folder):
app.logger.warning(f"Upload folder does not exist: {upload_folder}")
issues.append(f"Upload folder does not exist: {upload_folder}")
elif not os.access(upload_folder, os.W_OK):
app.logger.warning(f"Upload folder is not writable: {upload_folder}")
issues.append(f"Upload folder is not writable: {upload_folder}")
# Check static and dynamic analysis tools
def check_analysis_tool(section, tool_name):
@@ -746,29 +743,45 @@ def register_routes(app):
issues.append(f"{tool_name}: tool path not configured")
elif not os.path.isfile(tool_path):
issues.append(f"{tool_name}: tool not found at {tool_path}")
elif not os.access(tool_path, os.X_OK):
issues.append(f"{tool_name}: tool not executable at {tool_path}")
rules_path = tool_config.get('rules_path')
if rules_path and not os.path.isfile(rules_path):
issues.append(f"{tool_name}: rules not found at {rules_path}")
for tool in ['yara', 'checkplz']:
check_analysis_tool(analysis_config.get('static', {}), tool)
# Get tools from config instead of hardcoding
static_section = analysis_config.get('static', {})
dynamic_section = analysis_config.get('dynamic', {})
for tool in ['yara', 'pe_sieve', 'moneta', 'patriot', 'hsb', 'rededr']:
check_analysis_tool(analysis_config.get('dynamic', {}), tool)
# Check all configured static tools
for tool_name in static_section.keys():
check_analysis_tool(static_section, tool_name)
# Check all configured dynamic tools
for tool_name in dynamic_section.keys():
check_analysis_tool(dynamic_section, tool_name)
# Get all enabled tools for configuration response
static_tools = {
tool: static_section.get(tool, {}).get('enabled', False)
for tool in static_section.keys()
}
dynamic_tools = {
tool: dynamic_section.get(tool, {}).get('enabled', False)
for tool in dynamic_section.keys()
}
status = 'ok' if not issues else 'degraded'
app.logger.debug(f"Health check completed. Status: {status}")
return jsonify({
'status': status,
'timestamp': datetime.datetime.now().isoformat(),
'upload_folder_accessible': os.path.isdir(upload_folder) and os.access(upload_folder, os.W_OK) if upload_folder else False,
'upload_folder_accessible': os.path.isdir(upload_folder) if upload_folder else False,
'issues': issues,
'configuration': {
'static_analysis': {tool: analysis_config.get('static', {}).get(tool, {}).get('enabled', False) for tool in ['yara', 'threatcheck']},
'dynamic_analysis': {tool: analysis_config.get('dynamic', {}).get(tool, {}).get('enabled', False) for tool in ['yara', 'pe_sieve', 'moneta']}
'static_analysis': static_tools,
'dynamic_analysis': dynamic_tools
}
}), 200 if status == 'ok' else 503
+234 -126
View File
@@ -250,6 +250,32 @@ function formatBytes(bytes) {
}
function renderSection(title, items) {
if (!items || items.length === 0) return '';
const displayItems = items.slice(0, 25);
const remainingCount = items.length - 25;
return `
<div class="bg-gray-900/30 rounded-lg border border-gray-800 p-4">
<div class="text-sm font-medium text-gray-300 mb-3">
${title} (${items.length})
</div>
<div class="space-y-2">
${displayItems.map(item => `
<div class="text-sm text-gray-400 font-mono break-all bg-gray-900/50 p-2 rounded">
${item}
</div>
`).join('')}
${remainingCount > 0 ? `
<div class="text-sm text-gray-500 mt-2 p-2">
... and ${remainingCount} more items
</div>
` : ''}
</div>
</div>`;
}
// Tools Registry Object (keeping reference to tools)
const tools = {
yara: {
@@ -421,6 +447,214 @@ const tools = {
},
},
checkplz: {
element: document.getElementById('threatCheckResults'),
statsElement: document.getElementById('threatCheckStats'),
render: (results) => {
if (results.status === 'error') {
tools.checkplz.element.innerHTML = `
<div class="bg-red-500/10 border border-red-900/20 rounded-lg p-4">
<div class="flex items-center space-x-2 text-red-500">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>${results.error}</span>
</div>
</div>`;
return;
}
const findings = results.findings || {};
const scanResults = findings.scan_results || {};
const isClean = !findings.initial_threat && !scanResults.detection_offset;
// Stats Section
tools.checkplz.statsElement.innerHTML = `
<div class="grid grid-cols-3 gap-4 mb-6">
<div class="bg-gray-900/30 rounded-lg border ${isClean ? 'border-green-500/30' : 'border-red-500/30'} p-4">
<div class="text-sm text-gray-500">Status</div>
<div class="text-base font-semibold ${isClean ? 'text-green-500' : 'text-red-500'}">
${isClean ? 'Clean' : (findings.initial_threat || 'Unknown Threat')}
</div>
</div>
<div class="bg-gray-900/30 rounded-lg border border-gray-800 p-4">
<div class="text-sm text-gray-500">Scan Duration</div>
<div class="text-xl font-semibold text-gray-400">
${typeof scanResults.scan_duration === 'number' ? scanResults.scan_duration.toFixed(3) + 's' : 'N/A'}
</div>
</div>
<div class="bg-gray-900/30 rounded-lg border border-gray-800 p-4">
<div class="text-sm text-gray-500">Search Iterations</div>
<div class="text-xl font-semibold text-gray-400">
${scanResults.search_iterations || 'N/A'}
</div>
</div>
</div>`;
let html = '';
// File Information Section
html += `
<div class="bg-gray-900/30 rounded-lg border border-gray-800 p-4 mb-6">
<div class="text-sm font-medium text-gray-300 mb-3">File Information</div>
<div class="grid grid-cols-2 gap-4">
<div>
<div class="text-sm text-gray-500">File Path</div>
<div class="text-sm text-gray-300 font-mono break-all">
${scanResults.file_path || 'N/A'}
</div>
</div>
<div>
<div class="text-sm text-gray-500">File Size</div>
<div class="text-sm text-gray-300">
${scanResults.file_size || 'N/A'}
</div>
</div>
</div>
</div>`;
// Clean State Message
if (isClean) {
html += `
<div class="flex flex-col items-center justify-center py-8 bg-green-500/10 rounded-lg border border-green-500/20">
<svg class="w-12 h-12 text-green-500 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span class="text-green-500 font-medium">No threats detected - File is clean</span>
<span class="text-green-400 text-sm mt-1">Security scan completed successfully</span>
</div>`;
} else {
// Detection Details Section
if (scanResults.detection_offset) {
html += `
<div class="bg-red-500/10 rounded-lg border border-red-900/20 p-4 mb-6">
<div class="flex items-center space-x-2 mb-3">
<svg class="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
<span class="text-sm font-medium text-red-500">Threat Detection Details</span>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<div class="text-sm text-gray-500">Detection Offset</div>
<div class="text-sm text-red-500 font-mono">${scanResults.detection_offset}</div>
</div>
<div>
<div class="text-sm text-gray-500">Relative Location</div>
<div class="text-sm text-red-500">${scanResults.relative_location}</div>
</div>
<div class="col-span-2">
<div class="text-sm text-gray-500">Final Threat Detection</div>
<div class="text-sm text-red-500">${scanResults.final_threat_detection}</div>
</div>
</div>
</div>`;
}
// Hex Dump Section (only shown when threats are detected)
if (scanResults.hex_dump) {
html += `
<div class="bg-gray-900/30 rounded-lg border border-gray-800 p-4">
<div class="flex items-center justify-between mb-3">
<span class="text-sm font-medium text-gray-300">Showing ±128 bytes around detection point</span>
<button
onclick="navigator.clipboard.writeText(this.parentElement.nextElementSibling.textContent)"
class="px-2 py-1 text-xs text-gray-400 hover:text-white border border-gray-700 rounded hover:border-gray-600 transition-colors">
Copy
</button>
</div>
<pre class="text-base font-mono text-gray-400 whitespace-pre-wrap overflow-x-auto p-4 bg-gray-900/50 rounded-lg leading-relaxed">${scanResults.hex_dump}</pre>
</div>`;
}
}
tools.checkplz.element.innerHTML = html;
}
},
stringnalyzer: {
element: document.getElementById('StringnalyzerResults'),
statsElement: document.getElementById('StringnalyzerStats'),
render: (results) => {
if (results.status === 'error') {
tools.stringnalyzer.element.innerHTML = `
<div class="bg-red-500/10 border border-red-900/20 rounded-lg p-4">
<div class="flex items-center space-x-2 text-red-500">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>${results.error}</span>
</div>
</div>`;
return;
}
const findings = results.findings || {};
// Stats Section with Download Button
tools.stringnalyzer.statsElement.innerHTML = `
<div class="grid grid-cols-2 gap-4 mb-6">
<div class="bg-gray-900/30 rounded-lg border border-gray-800 p-4">
<div class="text-sm text-gray-500">File Path</div>
<div class="text-sm text-gray-300 font-mono break-all">
${findings.file_path || 'N/A'}
</div>
</div>
<div class="bg-gray-900/30 rounded-lg border border-gray-800 p-4">
<div class="text-sm text-gray-500">Total Strings</div>
<div class="text-xl font-semibold text-gray-400">
${findings.total_strings || 0}
</div>
</div>
</div>`;
// Attach click event to the Download Button
const downloadButton = document.getElementById('downloadResultsBtn');
if (downloadButton) {
downloadButton.addEventListener('click', () => {
const filePath = results.findings.file_path || 'unknown';
const fullFileName = filePath.split('\\').pop().split('/').pop(); // Get file name from path
const actualFileName = fullFileName.split('_').pop(); // Get everything after last underscore
const downloadName = `stringnalyzer_${actualFileName.replace('.exe', '')}.json`;
const blob = new Blob([JSON.stringify(results, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = downloadName;
a.click();
URL.revokeObjectURL(url);
});
}
let html = '';
// All Findings Sections
html += `
<div class="space-y-4">
${renderSection('Suspicious Strings', findings.found_suspicious_strings)}
${renderSection('Functions Referenced', findings.found_suspicious_functions)}
${renderSection('URLs Found', findings.found_url)}
${renderSection('DLLs Referenced', findings.found_dll)}
${renderSection('IP Addresses', findings.found_ip)}
${renderSection('Paths Found', findings.found_path)}
${renderSection('Files Referenced', findings.found_file)}
${renderSection('Commands Found', findings.found_commands)}
${renderSection('Functions', findings.found_functions)}
${renderSection('Error Messages', findings.found_error_messages)}
${renderSection('Network Indicators', findings.found_network_indicators)}
${renderSection('Registry Keys', findings.found_registry_keys)}
${renderSection('File Operations', findings.found_file_operations)}
${renderSection('Email Addresses', findings.found_emails)}
${renderSection('Domains', findings.found_domains)}
${renderSection('Interesting Strings', findings.found_interesting_strings)}
</div>`;
tools.stringnalyzer.element.innerHTML = html;
}
},
pe_sieve: {
element: document.getElementById('peSieveResults'),
render: (results) => {
@@ -686,132 +920,6 @@ const tools = {
}
},
checkplz: {
element: document.getElementById('threatCheckResults'),
statsElement: document.getElementById('threatCheckStats'),
render: (results) => {
if (results.status === 'error') {
tools.checkplz.element.innerHTML = `
<div class="bg-red-500/10 border border-red-900/20 rounded-lg p-4">
<div class="flex items-center space-x-2 text-red-500">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>${results.error}</span>
</div>
</div>`;
return;
}
const findings = results.findings || {};
const scanResults = findings.scan_results || {};
const isClean = !findings.initial_threat && !scanResults.detection_offset;
// Stats Section
tools.checkplz.statsElement.innerHTML = `
<div class="grid grid-cols-3 gap-4 mb-6">
<div class="bg-gray-900/30 rounded-lg border ${isClean ? 'border-green-500/30' : 'border-red-500/30'} p-4">
<div class="text-sm text-gray-500">Status</div>
<div class="text-base font-semibold ${isClean ? 'text-green-500' : 'text-red-500'}">
${isClean ? 'Clean' : (findings.initial_threat || 'Unknown Threat')}
</div>
</div>
<div class="bg-gray-900/30 rounded-lg border border-gray-800 p-4">
<div class="text-sm text-gray-500">Scan Duration</div>
<div class="text-xl font-semibold text-gray-400">
${typeof scanResults.scan_duration === 'number' ? scanResults.scan_duration.toFixed(3) + 's' : 'N/A'}
</div>
</div>
<div class="bg-gray-900/30 rounded-lg border border-gray-800 p-4">
<div class="text-sm text-gray-500">Search Iterations</div>
<div class="text-xl font-semibold text-gray-400">
${scanResults.search_iterations || 'N/A'}
</div>
</div>
</div>`;
let html = '';
// File Information Section
html += `
<div class="bg-gray-900/30 rounded-lg border border-gray-800 p-4 mb-6">
<div class="text-sm font-medium text-gray-300 mb-3">File Information</div>
<div class="grid grid-cols-2 gap-4">
<div>
<div class="text-sm text-gray-500">File Path</div>
<div class="text-sm text-gray-300 font-mono break-all">
${scanResults.file_path || 'N/A'}
</div>
</div>
<div>
<div class="text-sm text-gray-500">File Size</div>
<div class="text-sm text-gray-300">
${scanResults.file_size || 'N/A'}
</div>
</div>
</div>
</div>`;
// Clean State Message
if (isClean) {
html += `
<div class="flex flex-col items-center justify-center py-8 bg-green-500/10 rounded-lg border border-green-500/20">
<svg class="w-12 h-12 text-green-500 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span class="text-green-500 font-medium">No threats detected - File is clean</span>
<span class="text-green-400 text-sm mt-1">Security scan completed successfully</span>
</div>`;
} else {
// Detection Details Section
if (scanResults.detection_offset) {
html += `
<div class="bg-red-500/10 rounded-lg border border-red-900/20 p-4 mb-6">
<div class="flex items-center space-x-2 mb-3">
<svg class="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
<span class="text-sm font-medium text-red-500">Threat Detection Details</span>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<div class="text-sm text-gray-500">Detection Offset</div>
<div class="text-sm text-red-500 font-mono">${scanResults.detection_offset}</div>
</div>
<div>
<div class="text-sm text-gray-500">Relative Location</div>
<div class="text-sm text-red-500">${scanResults.relative_location}</div>
</div>
<div class="col-span-2">
<div class="text-sm text-gray-500">Final Threat Detection</div>
<div class="text-sm text-red-500">${scanResults.final_threat_detection}</div>
</div>
</div>
</div>`;
}
// Hex Dump Section (only shown when threats are detected)
if (scanResults.hex_dump) {
html += `
<div class="bg-gray-900/30 rounded-lg border border-gray-800 p-4">
<div class="flex items-center justify-between mb-3">
<span class="text-sm font-medium text-gray-300">Showing ±128 bytes around detection point</span>
<button
onclick="navigator.clipboard.writeText(this.parentElement.nextElementSibling.textContent)"
class="px-2 py-1 text-xs text-gray-400 hover:text-white border border-gray-700 rounded hover:border-gray-600 transition-colors">
Copy
</button>
</div>
<pre class="text-base font-mono text-gray-400 whitespace-pre-wrap overflow-x-auto p-4 bg-gray-900/50 rounded-lg leading-relaxed">${scanResults.hex_dump}</pre>
</div>`;
}
}
tools.checkplz.element.innerHTML = html;
}
},
patriot: {
element: document.getElementById('patriotResults'),
statsElement: document.getElementById('patriotStats'),
+17 -1
View File
@@ -121,7 +121,6 @@
</div>
</div>
<!-- Results Section -->
<!-- Results Section -->
<div class="bg-black/60 backdrop-blur-sm rounded-xl border border-gray-800 shadow-lg">
<div class="p-6">
@@ -137,6 +136,9 @@
<button class="tab-button text-base px-4 py-2 text-gray-300 hover:text-white border-b-2" data-tab="threatCheckResultsTab">
CheckPlz
</button>
<button class="tab-button text-base px-4 py-2 text-gray-300 hover:text-white border-b-2" data-tab="StringnalyzerResultsTab">
Stringnalyzer
</button>
{% else %}
<button class="tab-button text-base px-4 py-2 text-gray-300 hover:text-white border-b-2" data-tab="summaryTab">
Summary
@@ -220,6 +222,20 @@
<div id="threatCheckStats" class="flex space-x-4 mb-6"></div>
<div id="threatCheckResults" class="space-y-4"></div>
</div>
<div id="StringnalyzerResultsTab" class="tab-content">
<div class="flex justify-between items-center">
<h3 class="text-xl font-medium text-gray-100">Stringnalyzer Scan Results</h3>
<button id="downloadResultsBtn" class="px-3 py-1 text-sm text-gray-300 hover:text-white border border-gray-700 rounded hover:border-gray-600 transition-colors flex items-center space-x-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
</svg>
<span>Download Results</span>
</button>
</div>
<p class="text-base text-gray-500 mb-6">Payload String Analyzer</p>
<div id="StringnalyzerStats" class="flex space-x-4 mb-6"></div>
<div id="StringnalyzerResults" class="space-y-4"></div>
</div>
{% else %}
<!-- Summary Tab for Dynamic Analysis -->
<div id="summaryTab" class="tab-content">