├── LICENSE ├── style.css ├── README.md ├── index.html └── script.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Varshil Desai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-color: #6366f1; 3 | --secondary-color: #a78bfa; 4 | --accent-color: #f472b6; 5 | --bg-color-start: #1e1b4b; 6 | --bg-color-end: #312e81; 7 | --text-color: #e5e7eb; 8 | --text-color-dark: #9ca3af; 9 | --card-bg-color: rgba(255, 255, 255, 0.05); 10 | --card-border-color: rgba(255, 255, 255, 0.1); 11 | --input-bg-color: rgba(0, 0, 0, 0.2); 12 | --kql-bg-color: #0f172a; 13 | --kql-text-color: #94a3b8; 14 | --header-text-color: white; 15 | --header-subtext-color: #e5e7eb; 16 | } 17 | 18 | body[data-theme="light"] { 19 | --primary-color: #4f46e5; 20 | --secondary-color: #7c3aed; 21 | --accent-color: #ec4899; 22 | --bg-color-start: #f3f4f6; 23 | --bg-color-end: #e5e7eb; 24 | --text-color: #1f2937; 25 | --text-color-dark: #6b7280; 26 | --card-bg-color: rgba(255, 255, 255, 0.7); 27 | --card-border-color: rgba(0, 0, 0, 0.1); 28 | --input-bg-color: rgba(0, 0, 0, 0.05); 29 | --kql-bg-color: #f8fafc; 30 | --kql-text-color: #334155; 31 | --header-text-color: #111827; 32 | --header-subtext-color: #374151; 33 | } 34 | 35 | body { 36 | font-family: 'Inter', sans-serif; 37 | background-color: var(--bg-color-start); 38 | color: var(--text-color); 39 | margin: 0; 40 | overflow-x: hidden; 41 | transition: background-color 0.3s, color 0.3s; 42 | } 43 | 44 | .background-aurora { 45 | position: fixed; 46 | top: 0; 47 | left: 0; 48 | width: 100%; 49 | height: 100%; 50 | background: radial-gradient(ellipse at top, var(--primary-color), transparent), 51 | radial-gradient(ellipse at bottom, var(--accent-color), transparent); 52 | opacity: 0.3; 53 | z-index: -1; 54 | animation: aurora 20s infinite linear; 55 | transition: opacity 0.3s; 56 | } 57 | 58 | body[data-theme="light"] .background-aurora { 59 | opacity: 0.15; 60 | } 61 | 62 | @keyframes aurora { 63 | from { transform: rotate(0deg); } 64 | to { transform: rotate(360deg); } 65 | } 66 | 67 | /* Custom styles to complement Tailwind */ 68 | .card { 69 | background-color: var(--card-bg-color); 70 | border: 1px solid var(--card-border-color); 71 | backdrop-filter: blur(10px); 72 | -webkit-backdrop-filter: blur(10px); 73 | transition: background-color 0.3s, border-color 0.3s; 74 | } 75 | 76 | .card-title { 77 | color: var(--text-color); 78 | } 79 | 80 | .card-subtitle { 81 | color: var(--text-color-dark); 82 | } 83 | 84 | .btn-primary { 85 | background: linear-gradient(to right, var(--primary-color), var(--secondary-color)); 86 | color: white; 87 | } 88 | .btn-primary:hover:not(:disabled) { 89 | transform: translateY(-2px); 90 | box-shadow: 0 4px 15px rgba(0,0,0,0.2); 91 | } 92 | 93 | .btn-secondary { 94 | background-color: rgba(255, 255, 255, 0.1); 95 | color: var(--text-color); 96 | border: 1px solid var(--card-border-color); 97 | } 98 | 99 | body[data-theme="dark"] .btn-secondary { 100 | color: white; 101 | } 102 | 103 | .btn-secondary:hover:not(:disabled) { 104 | background-color: rgba(255, 255, 255, 0.2); 105 | } 106 | 107 | body[data-theme="light"] .btn-secondary:hover:not(:disabled) { 108 | background-color: rgba(0, 0, 0, 0.05); 109 | } 110 | 111 | .toggle-container { 112 | background-color: var(--input-bg-color); 113 | } 114 | .toggle-btn { 115 | color: var(--text-color-dark); 116 | } 117 | .toggle-btn.active { 118 | background-color: var(--primary-color); 119 | color: white; 120 | box-shadow: 0 2px 5px rgba(0,0,0,0.2); 121 | } 122 | 123 | .input-field { 124 | background-color: var(--input-bg-color); 125 | border: 1px solid var(--card-border-color); 126 | color: var(--text-color); 127 | } 128 | .input-field:focus { 129 | outline: none; 130 | border-color: var(--primary-color); 131 | box-shadow: 0 0 0 2px var(--primary-color); 132 | } 133 | 134 | .kql-query { 135 | background-color: var(--kql-bg-color); 136 | color: var(--kql-text-color); 137 | border: 1px solid var(--card-border-color); 138 | transition: background-color 0.3s, color 0.3s, border-color 0.3s; 139 | } 140 | .kql-query .comment { 141 | color: #64748b; 142 | } 143 | .kql-query .copy-btn { 144 | background-color: #374151; 145 | color: #e5e7eb; 146 | } 147 | 148 | .modal-overlay { 149 | background-color: rgba(0, 0, 0, 0.5); 150 | backdrop-filter: blur(5px); 151 | -webkit-backdrop-filter: blur(5px); 152 | z-index: 100; /* Ensure modal is on top */ 153 | } 154 | 155 | .message-box { 156 | transition: opacity 0.3s, transform 0.3s; 157 | z-index: 101; 158 | } 159 | .message-box:not(.hidden) { 160 | opacity: 1; 161 | transform: translateY(0); 162 | } 163 | .message-box.success { background-color: #10b981; } 164 | .message-box.error { background-color: #ef4444; } 165 | 166 | /* Theme-aware text colors */ 167 | header h1 { 168 | color: var(--header-text-color); 169 | } 170 | header p { 171 | color: var(--header-subtext-color); 172 | } 173 | .input-label { 174 | color: var(--text-color-dark); 175 | } 176 | .modal-content h2, .modal-content h3 { 177 | color: var(--text-color); 178 | } 179 | .prose { 180 | color: var(--text-color); 181 | } 182 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # KQLIntel 🚀 4 | 5 | ### _From Unstructured Threat Intel to Actionable KQL Queries in Seconds_ 6 | 7 |
8 | 9 |

10 | GitHub language count 11 | GitHub top language 12 | GitHub 13 |

14 | 15 | --- 16 | 17 | **KQLIntel** is a powerful, browser-based tool designed to bridge the gap between unstructured threat intelligence reports and actionable Kusto Query Language (KQL) queries. It leverages the power of modern Large Language Models (LLMs) to analyze threat reports from URLs or raw text, intelligently extract Indicators of Compromise (IOCs), and automatically generate KQL queries ready for threat hunting in Microsoft Sentinel and other security platforms. 18 | 19 | The tool is designed for security analysts, threat hunters, and incident responders who need to quickly operationalize threat intelligence without manual parsing and query creation. 20 | 21 | ## ✨ Key Features 22 | 23 | - **Intelligent IOC Extraction:** Automatically identifies and extracts key IOCs, including IPs, domains, file hashes (MD5, SHA1, SHA256), filenames, and URLs. 24 | - **Automatic KQL Generation:** Instantly generates "guaranteed" KQL queries based on the extracted IOCs, ready to be used in your security tools. 25 | - **AI-Powered Enhancements:** 26 | - **Threat Summaries:** Get a concise, AI-generated summary of the threat report. 27 | - **Advanced Hunting Queries:** Discover deeper threats with experimental, AI-suggested hunting queries. 28 | - **Mitigation Suggestions:** Receive actionable mitigation steps to respond to the identified threats. 29 | - **Flexible Input:** Analyze intelligence by providing a URL to a public report or by pasting in raw, unstructured text. 30 | - **Multi-Provider LLM Support:** Integrates with a wide range of LLM providers, including Google Gemini, OpenAI, Azure OpenAI, Anthropic, and any provider compatible with the OpenRouter API. 31 | - **Modern UI:** Features a sleek, user-friendly interface with both dark and light modes to suit your preference. 32 | - **Secure by Design:** API keys are stored securely in your browser's local storage and are never exposed or transmitted anywhere except to the respective AI provider's endpoint. 33 | 34 | ## 🚀 Live Demo & Screenshots 35 | 36 | 1. URL / Threat Report Page 37 | 38 | image 39 |

40 | 2. KQL Queries 41 |

42 | 43 | image 44 |

45 | 3. AI Generated Recommendations 46 |

47 | image 48 |

49 | 4. API Configurations 50 |

51 | image 52 | 53 | 54 | ## 🛠️ Getting Started 55 | 56 | KQLIntel is a pure client-side application and requires no backend. You can run it locally or host it on any static web hosting service like GitHub Pages, Vercel, or Netlify. 57 | 58 | ### Prerequisites 59 | 60 | - A modern web browser (Chrome, Firefox, Edge, Safari). 61 | - An API key from a supported LLM provider (e.g., [Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service) OR [Google AI Studio](https://aistudio.google.com/)), etc. 62 | - 63 | 64 | ### Installation 65 | 66 | 1. **Clone the repository:** 67 | ```bash 68 | git clone [https://github.com/Var5h1l/KQLIntel.git](https://github.com/Var5h1l/KQLIntel.git) 69 | ``` 70 | 2. **Navigate to the directory:** 71 | ```bash 72 | cd KQLIntel 73 | ``` 74 | 3. **Open `index.html`:** 75 | Simply open the `index.html` file in your local web browser to start using the application. 76 | 77 | ## ⚙️ How to Use 78 | 79 | 1. **Set Your API Keys:** 80 | - On first launch, click the **"Set API Keys"** button in the top-right corner. 81 | - Enter the API key for the AI provider you wish to use. 82 | - Click **"Save"**. Your key is saved securely in your browser's local storage for future use. 83 | 84 | 2. **Choose an Input Method:** 85 | - **URL:** Paste the URL of a public threat intelligence report. The tool will fetch and parse the content. 86 | - **Raw Text:** Paste any unstructured text containing threat intelligence or IOCs directly into the text area. 87 | 88 | 3. **Analyze the Intel:** 89 | - Select your preferred AI Provider and Model from the dropdown menus. 90 | - Click the **"Analyze & Generate KQL"** button. 91 | 92 | 4. **Review the Results:** 93 | - **Guaranteed KQL Queries:** The application will immediately display KQL queries based on the IOCs it confidently extracted. 94 | - **AI Threat Summary:** Click "Generate Summary" to get a high-level overview of the threat. 95 | - **AI-Assisted Hunting Queries:** Click "Suggest Advanced Queries" for more complex, behavior-based KQL queries. 96 | - **Mitigation Suggestions:** After generating a summary, click "Suggest Mitigations" for recommended response actions. 97 | 98 | ## 🤝 Contributing & Future Enhancement Roadmap 99 | 100 | Contributions, issues, and feature requests are welcome! Please feel free to check the [issues page](https://github.com/Var5h1l/KQLIntel/issues) to see if your idea has already been discussed. 101 | 102 | KQLIntel is actively evolving! Planned enhancements include: 103 | 104 | - **KQL Query Enhancer & Optimizer:** 105 | Improve generated KQL queries for performance, best practices, and reduced resource consumption. 106 | 107 | - **Multiple URL Report Support:** 108 | Enable users to analyze and generate KQL from several threat intelligence report URLs simultaneously. 109 | 110 | - **API-Free Report Generation:** 111 | Allow users to generate basic reports and queries without requiring an API key, making the tool more accessible. 112 | 113 | - **Integration of New AI Models:** 114 | Expand compatibility by adding support for additional AI models and providers, enhancing flexibility and accuracy. 115 | 116 | --- 117 | 118 | 119 | ## 👤 Author 120 | 121 | - **Varshil Desai** 122 | - **Connect:** [LinkedIn](https://www.linkedin.com/in/varshil01/) 123 | 124 | ## 📄 License 125 | 126 | This project is open-source and available under the MIT License. See the [LICENSE](LICENSE) file for more info. 127 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | KQLIntel 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 |
15 | 16 |
17 |

KQLIntel

18 |

Go from unstructured report to actionable hunt in seconds.

19 |
20 | 21 | 22 |
23 | 31 | 32 |
33 | 34 |
35 | 36 |
37 |
38 |
39 | 40 | 41 |
42 |
43 | 44 |
45 | 46 | 47 |
48 | 49 | 53 | 54 | 55 |
56 | 57 | 64 |
65 |
66 | 67 | 68 | 72 |
73 | 74 |
75 | 78 | 81 |
82 |
83 | 84 | 85 | 129 |
130 |
131 | 132 | 133 | 180 | 181 | 182 | 195 | 196 | 197 | 198 | 199 | 200 | 205 | 206 | 207 | 208 | 209 | -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | // --- DOM Element References --- 2 | const dom = { 3 | // Input 4 | toggleUrlBtn: document.getElementById('toggle-url'), 5 | toggleTextBtn: document.getElementById('toggle-text'), 6 | urlContainer: document.getElementById('input-url-container'), 7 | textContainer: document.getElementById('input-text-container'), 8 | urlInput: document.getElementById('url-input'), 9 | textInput: document.getElementById('text-input'), 10 | analyzeBtn: document.getElementById('analyze-btn'), 11 | spinner: document.getElementById('spinner'), 12 | 13 | // AI Selection 14 | providerSelect: document.getElementById('provider-select'), 15 | modelSelect: document.getElementById('model-select'), 16 | modelSelectLabel: document.getElementById('model-select-label'), 17 | azureSettingsPrompt: document.getElementById('azure-settings-prompt'), 18 | azureConfigureBtn: document.getElementById('azure-configure-btn'), 19 | 20 | // Results 21 | resultsSection: document.getElementById('results-section'), 22 | guaranteedQueriesContainer: document.getElementById('guaranteed-queries-container'), 23 | guaranteedQueriesList: document.getElementById('guaranteed-queries-list'), 24 | 25 | // Threat Summary 26 | threatSummaryContainer: document.getElementById('threat-summary-container'), 27 | generateSummaryBtn: document.getElementById('generate-summary-btn'), 28 | summarySpinner: document.getElementById('summary-spinner'), 29 | threatSummaryContent: document.getElementById('threat-summary-content'), 30 | summaryControls: document.getElementById('summary-controls'), 31 | 32 | // AI Assisted 33 | aiAssistedContainer: document.getElementById('ai-assisted-container'), 34 | suggestQueriesBtn: document.getElementById('suggest-queries-btn'), 35 | aiSpinner: document.getElementById('ai-spinner'), 36 | aiAssistedQueriesList: document.getElementById('ai-assisted-queries-list'), 37 | 38 | // Mitigation Suggestions 39 | mitigationContainer: document.getElementById('mitigation-container'), 40 | suggestMitigationsBtn: document.getElementById('suggest-mitigations-btn'), 41 | mitigationSpinner: document.getElementById('mitigation-spinner'), 42 | mitigationContent: document.getElementById('mitigation-content'), 43 | mitigationControls: document.getElementById('mitigation-controls'), 44 | 45 | // Settings Modal & Theme 46 | settingsBtn: document.getElementById('settings-btn'), 47 | settingsModal: document.getElementById('settings-modal'), 48 | settingsSaveBtn: document.getElementById('settings-save-btn'), 49 | settingsCancelBtn: document.getElementById('settings-cancel-btn'), 50 | themeToggleBtn: document.getElementById('theme-toggle-btn'), 51 | themeSunIcon: document.getElementById('theme-sun-icon'), 52 | themeMoonIcon: document.getElementById('theme-moon-icon'), 53 | googleKeyInput: document.getElementById('google-key-input'), 54 | openaiKeyInput: document.getElementById('openai-key-input'), 55 | anthropicKeyInput: document.getElementById('anthropic-key-input'), 56 | openrouterKeyInput: document.getElementById('openrouter-key-input'), 57 | azureKeyInput: document.getElementById('azure-key-input'), 58 | azureEndpointInput: document.getElementById('azure-endpoint-input'), 59 | azureDeploymentInput: document.getElementById('azure-deployment-input'), 60 | 61 | // Info Modal 62 | infoBtn: document.getElementById('info-btn'), 63 | infoModal: document.getElementById('info-modal'), 64 | infoCloseBtn: document.getElementById('info-close-btn'), 65 | 66 | // Global 67 | messageBox: document.getElementById('message-box'), 68 | }; 69 | 70 | // --- State Variables --- 71 | let state = { 72 | extractedIOCs: {}, 73 | lastAnalyzedText: '', 74 | lastSummary: '', 75 | apiKeys: {}, 76 | }; 77 | 78 | // --- Configuration --- 79 | const config = { 80 | kqlTemplates: { 81 | ipv4: 'DeviceNetworkEvents | where RemoteIP == "{ioc}"', 82 | domain: 'DeviceNetworkEvents | where RemoteUrl has "{ioc}" or RemoteIP in ((find where DomainName == "{ioc}" | project IPAddress))', 83 | md5: 'DeviceFileEvents | where MD5 == "{ioc}"', 84 | sha1: 'DeviceFileEvents | where SHA1 == "{ioc}"', 85 | sha256: 'DeviceFileEvents | where SHA256 == "{ioc}"', 86 | filename: 'DeviceFileEvents | where FileName == "{ioc}"', 87 | url: 'DeviceNetworkEvents | where RemoteUrl == "{ioc}"' 88 | }, 89 | modelsByProvider: { 90 | google: ['gemini-1.5-flash-latest', 'gemini-1.5-pro-latest', 'gemini-pro'], 91 | openai: ['gpt-4o', 'gpt-4-turbo', 'gpt-3.5-turbo'], 92 | anthropic: ['claude-3-opus-20240229', 'claude-3-sonnet-20240229', 'claude-3-haiku-20240307'], 93 | openrouter: [ 94 | 'nousresearch/nous-hermes-2-mixtral-8x7b-dpo', 95 | 'mistralai/mistral-7b-instruct-v0.2', 96 | 'google/gemma-7b-it', 97 | 'openchat/openchat-7b' 98 | ], 99 | azure: ['custom (defined by deployment name)'] 100 | } 101 | }; 102 | 103 | // --- Initialization --- 104 | function initialize() { 105 | loadApiKeys(); 106 | applyInitialTheme(); 107 | setupEventListeners(); 108 | updateModelSelector(); 109 | dom.toggleUrlBtn.classList.add('active'); 110 | } 111 | 112 | // --- Event Listeners Setup --- 113 | function setupEventListeners() { 114 | dom.toggleUrlBtn.addEventListener('click', () => switchInputType('url')); 115 | dom.toggleTextBtn.addEventListener('click', () => switchInputType('text')); 116 | dom.analyzeBtn.addEventListener('click', handleAnalysis); 117 | dom.generateSummaryBtn.addEventListener('click', handleThreatSummary); 118 | dom.suggestQueriesBtn.addEventListener('click', handleAiSuggestions); 119 | dom.suggestMitigationsBtn.addEventListener('click', handleMitigationSuggestions); 120 | 121 | dom.settingsBtn.addEventListener('click', () => dom.settingsModal.classList.remove('hidden')); 122 | dom.settingsCancelBtn.addEventListener('click', () => dom.settingsModal.classList.add('hidden')); 123 | dom.settingsSaveBtn.addEventListener('click', saveApiKeys); 124 | 125 | dom.themeToggleBtn.addEventListener('click', handleThemeToggle); 126 | dom.providerSelect.addEventListener('change', updateModelSelector); 127 | dom.azureConfigureBtn.addEventListener('click', () => dom.settingsModal.classList.remove('hidden')); 128 | 129 | // Info Modal Listeners 130 | dom.infoBtn.addEventListener('click', () => dom.infoModal.classList.remove('hidden')); 131 | dom.infoCloseBtn.addEventListener('click', () => dom.infoModal.classList.add('hidden')); 132 | dom.infoModal.addEventListener('click', (e) => { 133 | if (e.target === dom.infoModal) { 134 | dom.infoModal.classList.add('hidden'); 135 | } 136 | }); 137 | } 138 | 139 | // --- UI Functions --- 140 | function showMessage(text, type = 'success') { 141 | dom.messageBox.textContent = text; 142 | dom.messageBox.className = 'message-box fixed bottom-5 right-20 text-white px-5 py-3 rounded-lg shadow-lg'; 143 | dom.messageBox.classList.add(type); 144 | dom.messageBox.classList.remove('hidden'); 145 | setTimeout(() => { dom.messageBox.classList.add('hidden'); }, 4000); 146 | } 147 | 148 | function switchInputType(type) { 149 | dom.urlContainer.classList.toggle('hidden', type !== 'url'); 150 | dom.textContainer.classList.toggle('hidden', type === 'url'); 151 | dom.toggleUrlBtn.classList.toggle('active', type === 'url'); 152 | dom.toggleTextBtn.classList.toggle('active', type !== 'url'); 153 | } 154 | 155 | function updateModelSelector() { 156 | const provider = dom.providerSelect.value; 157 | const models = config.modelsByProvider[provider] || []; 158 | const isAzure = provider === 'azure'; 159 | 160 | dom.modelSelect.innerHTML = models.map(m => ``).join(''); 161 | dom.modelSelect.classList.toggle('hidden', isAzure); 162 | dom.modelSelectLabel.classList.toggle('hidden', isAzure); 163 | dom.azureSettingsPrompt.classList.toggle('hidden', !isAzure); 164 | } 165 | 166 | function createKqlQueryElement(title, query) { 167 | const div = document.createElement('div'); 168 | div.className = 'kql-query relative font-mono whitespace-pre-wrap break-all p-4 rounded-md'; 169 | const sanitizedQuery = query.replace(//g, ">"); 170 | div.innerHTML = ` 171 | 172 |

// ${title}

173 |

${sanitizedQuery}

`; 174 | 175 | const copyBtn = div.querySelector('.copy-btn'); 176 | copyBtn.addEventListener('click', (e) => copyToClipboard(query, e.target)); 177 | 178 | div.addEventListener('mouseover', () => copyBtn.style.opacity = '1'); 179 | div.addEventListener('mouseout', () => copyBtn.style.opacity = '0'); 180 | 181 | return div; 182 | } 183 | 184 | function copyToClipboard(text, buttonElement) { 185 | navigator.clipboard.writeText(text).then(() => { 186 | buttonElement.textContent = 'Copied!'; 187 | showMessage('Copied to clipboard!', 'success'); 188 | setTimeout(() => { buttonElement.textContent = 'Copy'; }, 2000); 189 | }).catch(err => { 190 | console.error('Failed to copy text: ', err); 191 | showMessage('Failed to copy.', 'error'); 192 | }); 193 | } 194 | 195 | function setLoadingState(isLoading, type) { 196 | const elements = { 197 | analyze: { spinner: dom.spinner, btn: dom.analyzeBtn }, 198 | summary: { spinner: dom.summarySpinner, btn: dom.generateSummaryBtn }, 199 | ai: { spinner: dom.aiSpinner, btn: dom.suggestQueriesBtn }, 200 | mitigation: { spinner: dom.mitigationSpinner, btn: dom.suggestMitigationsBtn } 201 | }; 202 | const el = elements[type]; 203 | if (el) { 204 | el.spinner.classList.toggle('hidden', !isLoading); 205 | el.btn.disabled = isLoading; 206 | } 207 | } 208 | 209 | function resetResults() { 210 | dom.resultsSection.classList.add('hidden'); 211 | state.extractedIOCs = {}; 212 | state.lastAnalyzedText = ''; 213 | state.lastSummary = ''; 214 | 215 | [dom.guaranteedQueriesContainer, dom.threatSummaryContainer, dom.aiAssistedContainer, dom.mitigationContainer].forEach(c => c.classList.add('hidden')); 216 | dom.guaranteedQueriesList.innerHTML = ''; 217 | dom.threatSummaryContent.innerHTML = ''; 218 | dom.aiAssistedQueriesList.innerHTML = ''; 219 | dom.mitigationContent.innerHTML = ''; 220 | 221 | dom.summaryControls.classList.remove('hidden'); 222 | dom.mitigationControls.classList.remove('hidden'); 223 | } 224 | 225 | // --- Theme Management --- 226 | function applyInitialTheme() { 227 | const savedTheme = localStorage.getItem('kqlintel-theme') || 'dark'; 228 | setTheme(savedTheme); 229 | } 230 | 231 | function handleThemeToggle() { 232 | const currentTheme = document.body.dataset.theme; 233 | const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; 234 | setTheme(newTheme); 235 | } 236 | 237 | function setTheme(theme) { 238 | document.body.dataset.theme = theme; 239 | localStorage.setItem('kqlintel-theme', theme); 240 | if (theme === 'light') { 241 | dom.themeSunIcon.classList.add('hidden'); 242 | dom.themeMoonIcon.classList.remove('hidden'); 243 | } else { 244 | dom.themeSunIcon.classList.remove('hidden'); 245 | dom.themeMoonIcon.classList.add('hidden'); 246 | } 247 | } 248 | 249 | // --- API Key Management --- 250 | function saveApiKeys() { 251 | state.apiKeys = { 252 | google: dom.googleKeyInput.value, 253 | openai: dom.openaiKeyInput.value, 254 | anthropic: dom.anthropicKeyInput.value, 255 | openrouter: dom.openrouterKeyInput.value, 256 | azure: { 257 | key: dom.azureKeyInput.value, 258 | endpoint: dom.azureEndpointInput.value, 259 | deployment: dom.azureDeploymentInput.value, 260 | } 261 | }; 262 | localStorage.setItem('kqlintel-apikeys', JSON.stringify(state.apiKeys)); 263 | showMessage('API Keys saved!', 'success'); 264 | dom.settingsModal.classList.add('hidden'); 265 | } 266 | 267 | function loadApiKeys() { 268 | const savedKeys = localStorage.getItem('kqlintel-apikeys'); 269 | if (savedKeys) { 270 | state.apiKeys = JSON.parse(savedKeys); 271 | dom.googleKeyInput.value = state.apiKeys.google || ''; 272 | dom.openaiKeyInput.value = state.apiKeys.openai || ''; 273 | dom.anthropicKeyInput.value = state.apiKeys.anthropic || ''; 274 | dom.openrouterKeyInput.value = state.apiKeys.openrouter || ''; 275 | if (state.apiKeys.azure) { 276 | dom.azureKeyInput.value = state.apiKeys.azure.key || ''; 277 | dom.azureEndpointInput.value = state.apiKeys.azure.endpoint || ''; 278 | dom.azureDeploymentInput.value = state.apiKeys.azure.deployment || ''; 279 | } 280 | } 281 | } 282 | 283 | function checkApiKey(provider) { 284 | if (provider === 'azure') { 285 | if (!state.apiKeys.azure || !state.apiKeys.azure.key || !state.apiKeys.azure.endpoint || !state.apiKeys.azure.deployment) { 286 | showMessage(`Azure credentials are incomplete. Please add Key, Endpoint, and Deployment Name in Settings.`, 'error'); 287 | dom.settingsModal.classList.remove('hidden'); 288 | return false; 289 | } 290 | } else if (!state.apiKeys[provider]) { 291 | showMessage(`API Key for ${provider} is missing. Please add it in Settings.`, 'error'); 292 | dom.settingsModal.classList.remove('hidden'); 293 | return false; 294 | } 295 | return true; 296 | } 297 | 298 | // --- Core Logic --- 299 | async function handleAnalysis() { 300 | if (!checkApiKey(dom.providerSelect.value)) return; 301 | 302 | setLoadingState(true, 'analyze'); 303 | resetResults(); 304 | 305 | try { 306 | const isUrlMode = dom.toggleUrlBtn.classList.contains('active'); 307 | let textToAnalyze; 308 | let sourceDomain = null; 309 | 310 | if (isUrlMode) { 311 | const urlValue = dom.urlInput.value.trim(); 312 | if (!urlValue) { 313 | showMessage('Please provide a URL to analyze.', 'error'); 314 | setLoadingState(false, 'analyze'); 315 | return; 316 | } 317 | try { 318 | const url = new URL(urlValue); 319 | sourceDomain = url.hostname; 320 | } catch (e) { 321 | showMessage('Invalid URL provided.', 'error'); 322 | setLoadingState(false, 'analyze'); 323 | return; 324 | } 325 | textToAnalyze = await fetchUrlContent(urlValue); 326 | if (!textToAnalyze) { 327 | setLoadingState(false, 'analyze'); 328 | return; 329 | } 330 | } else { 331 | textToAnalyze = dom.textInput.value.trim(); 332 | if (!textToAnalyze) { 333 | showMessage('Please provide some text to analyze.', 'error'); 334 | setLoadingState(false, 'analyze'); 335 | return; 336 | } 337 | } 338 | 339 | state.lastAnalyzedText = textToAnalyze; 340 | showMessage('Analyzing text with AI...', 'success'); 341 | 342 | let iocPrompt = `You are an expert cybersecurity threat intelligence analyst with a specialization in parsing unstructured reports. Your primary task is to extract ONLY malicious Indicators of Compromise (IOCs) from the provided text. 343 | 344 | **CRITICAL INSTRUCTIONS:** 345 | 1. **Focus on Explicit IOCs:** Give the highest priority to items listed under explicit headings like "Indicators of Compromise", "IOCs", "Malicious Hashes", "C2 Domains", etc. If no such section exists, be extremely cautious. 346 | 2. **Context is Key:** Do NOT extract every domain, URL, or IP address you find. Analyze the surrounding text. An IOC is typically presented as a threat artifact. A URL in a reference link at the bottom of a page is NOT an IOC. 347 | 3. **Ignore Legitimate & Reference Domains:** You MUST ignore common, legitimate domains and URLs unless they are explicitly identified as malicious. This includes: 348 | * The source domain of the report itself (${sourceDomain || 'the source website'}). 349 | * Major tech and security company domains (e.g., microsoft.com, google.com, apple.com, virustotal.com, github.com). 350 | * Social media links (e.g., twitter.com, linkedin.com). 351 | * URLs in footnotes, references, or "further reading" sections. 352 | 4. **Identify True IOCs:** Look for file hashes (MD5, SHA1, SHA256), suspicious IP addresses, command-and-control (C2) domains, malicious filenames (e.g., malware.exe, payload.dll), and URLs pointing directly to malicious content. 353 | 5. **Be Exhaustive:** You MUST extract ALL indicators of each type from the entire text. Do not stop after finding just one. Scrutinize the entire provided text, especially tables and lists, to ensure every single IOC is included in your final JSON response. 354 | 355 | From the text below, extract the IOCs and return them as a valid JSON object with the following keys: "ipv4", "domain", "md5", "sha1", "sha256", "filename", "url". If you find absolutely no items that you can confidently identify as malicious IOCs based on these rules, return a JSON object with empty arrays for each key. 356 | 357 | Text to analyze: 358 | --- 359 | ${textToAnalyze} 360 | ---`; 361 | 362 | const iocSchema = { type: "OBJECT", properties: { "ipv4": { "type": "ARRAY", "items": { "type": "STRING" } }, "domain": { "type": "ARRAY", "items": { "type": "STRING" } }, "md5": { "type": "ARRAY", "items": { "type": "STRING" } }, "sha1": { "type": "ARRAY", "items": { "type": "STRING" } }, "sha256": { "type": "ARRAY", "items": { "type": "STRING" } }, "filename": { "type": "ARRAY", "items": { "type": "STRING" } }, "url": { "type": "ARRAY", "items": { "type": "STRING" } } } }; 363 | 364 | const iocsResult = await callLLM(iocPrompt, iocSchema); 365 | const iocs = JSON.parse(iocsResult); 366 | 367 | const iocsFound = iocs && Object.values(iocs).some(arr => arr.length > 0); 368 | 369 | dom.resultsSection.classList.remove('hidden'); 370 | dom.guaranteedQueriesContainer.classList.remove('hidden'); 371 | displayGuaranteedQueries(iocs); 372 | 373 | if (iocsFound) { 374 | state.extractedIOCs = iocs; 375 | dom.threatSummaryContainer.classList.remove('hidden'); 376 | dom.aiAssistedContainer.classList.remove('hidden'); 377 | dom.mitigationContainer.classList.remove('hidden'); 378 | showMessage("AI analysis successful! Found IOCs.", "success"); 379 | } else { 380 | showMessage("AI analysis complete. No standard IOCs found.", "success"); 381 | } 382 | } catch (error) { 383 | console.error("Analysis failed:", error); 384 | showMessage(`Analysis failed: ${error.message}`, 'error'); 385 | } finally { 386 | setLoadingState(false, 'analyze'); 387 | } 388 | } 389 | 390 | async function handleThreatSummary() { 391 | if (!checkApiKey(dom.providerSelect.value)) return; 392 | if (!state.lastAnalyzedText) { 393 | showMessage("Please run an analysis first.", 'error'); 394 | return; 395 | } 396 | setLoadingState(true, 'summary'); 397 | try { 398 | const prompt = `You are a senior cybersecurity analyst. Based on the following threat intelligence text and its extracted IOCs, provide a concise summary (2-3 paragraphs) for a security operations team. Explain the threat's nature, behavior, and impact. Return only the summary text itself, without any titles, JSON formatting, or other conversational text.\n\nIOCs:\n${JSON.stringify(state.extractedIOCs)}\n\nOriginal Text:\n---\n${state.lastAnalyzedText}\n---`; 399 | 400 | const summary = await callLLM(prompt, null); 401 | state.lastSummary = summary; // Save summary for mitigation step 402 | 403 | dom.threatSummaryContent.innerHTML = `

${summary.replace(/\n\n/g, '

').replace(/\n/g, '
')}

`; 404 | dom.summaryControls.classList.add('hidden'); 405 | showMessage("Threat summary generated!", "success"); 406 | } catch (error) { 407 | console.error("Failed to get threat summary:", error); 408 | showMessage(`Error generating summary: ${error.message}`, 'error'); 409 | } finally { 410 | setLoadingState(false, 'summary'); 411 | } 412 | } 413 | 414 | async function handleAiSuggestions() { 415 | if (!checkApiKey(dom.providerSelect.value)) return; 416 | if (Object.keys(state.extractedIOCs).length === 0) { 417 | showMessage("No IOCs found to generate advanced queries from.", 'error'); 418 | return; 419 | } 420 | setLoadingState(true, 'ai'); 421 | dom.aiAssistedQueriesList.innerHTML = ''; 422 | try { 423 | const prompt = `You are a senior threat hunter specializing in KQL. Given these IOCs, generate 3-5 advanced KQL queries to find related suspicious activity. Use operators like 'join', 'summarize', and look for behavioral patterns. Return ONLY a valid JSON array of objects, each with a "title" and a "query" key.\n\nIOCs:\n${JSON.stringify(state.extractedIOCs)}`; 424 | const schema = { type: "ARRAY", items: { type: "OBJECT", properties: { "title": { "type": "STRING" }, "query": { "type": "STRING" } }, required: ["title", "query"] } }; 425 | const result = await callLLM(prompt, schema); 426 | const queries = JSON.parse(result); 427 | displayAdvancedQueries(queries); 428 | showMessage("Advanced queries generated!", "success"); 429 | } catch (error) { 430 | console.error("Failed to get advanced queries:", error); 431 | showMessage(`Error generating advanced queries: ${error.message}`, 'error'); 432 | } finally { 433 | setLoadingState(false, 'ai'); 434 | } 435 | } 436 | 437 | async function handleMitigationSuggestions() { 438 | if (!checkApiKey(dom.providerSelect.value)) return; 439 | if (Object.keys(state.extractedIOCs).length === 0) { 440 | showMessage("No IOCs found to suggest mitigations from.", 'error'); 441 | return; 442 | } 443 | if (!state.lastSummary) { 444 | showMessage("Please generate a threat summary first.", 'error'); 445 | return; 446 | } 447 | 448 | setLoadingState(true, 'mitigation'); 449 | try { 450 | const prompt = `You are a principal security engineer. Based on the following threat summary and IOCs, provide a prioritized list of actionable mitigation steps for a Security Operations Center (SOC) and IT administrators. Group suggestions by theme (e.g., Network, Endpoint, Identity). Be specific and practical. Return only the mitigation steps as a well-formatted text response.\n\nThreat Summary:\n${state.lastSummary}\n\nIOCs:\n${JSON.stringify(state.extractedIOCs)}`; 451 | 452 | const mitigations = await callLLM(prompt, null); 453 | 454 | dom.mitigationContent.innerHTML = `

${mitigations.replace(/\n\n/g, '

').replace(/\n/g, '
')}

`; 455 | dom.mitigationControls.classList.add('hidden'); 456 | showMessage("Mitigation steps generated!", "success"); 457 | } catch (error) { 458 | console.error("Failed to get mitigation suggestions:", error); 459 | showMessage(`Error generating mitigations: ${error.message}`, 'error'); 460 | } finally { 461 | setLoadingState(false, 'mitigation'); 462 | } 463 | } 464 | 465 | function displayGuaranteedQueries(iocs) { 466 | dom.guaranteedQueriesList.innerHTML = ''; 467 | let count = 0; 468 | for (const type in iocs) { 469 | if (config.kqlTemplates[type] && iocs[type]?.length > 0) { 470 | iocs[type].forEach(ioc => { 471 | const query = config.kqlTemplates[type].replace(/{ioc}/g, ioc); 472 | dom.guaranteedQueriesList.appendChild(createKqlQueryElement(`Hunt for ${type.toUpperCase()} - ${ioc}`, query)); 473 | count++; 474 | }); 475 | } 476 | } 477 | if (count === 0) { 478 | dom.guaranteedQueriesList.innerHTML = '

No standard IOCs were found.

'; 479 | } 480 | } 481 | 482 | function displayAdvancedQueries(queries) { 483 | dom.aiAssistedQueriesList.innerHTML = ''; 484 | if (queries?.length > 0) { 485 | queries.forEach(q => dom.aiAssistedQueriesList.appendChild(createKqlQueryElement(q.title, q.query))); 486 | } else { 487 | dom.aiAssistedQueriesList.innerHTML = '

The AI could not suggest any advanced queries.

'; 488 | } 489 | } 490 | 491 | async function fetchUrlContent(url) { 492 | const readerApiUrl = `https://r.jina.ai/${encodeURIComponent(url)}`; 493 | showMessage('Fetching and parsing content with reader API...', 'success'); 494 | try { 495 | const controller = new AbortController(); 496 | const timeoutId = setTimeout(() => controller.abort(), 25000); 497 | 498 | const response = await fetch(readerApiUrl, { 499 | signal: controller.signal, 500 | headers: { 'Accept': 'text/plain' } 501 | }); 502 | 503 | clearTimeout(timeoutId); 504 | 505 | if (!response.ok) { 506 | throw new Error(`Reader API failed with status ${response.status}: ${await response.text()}`); 507 | } 508 | 509 | const textContent = await response.text(); 510 | 511 | if (!textContent || textContent.trim() === '') { 512 | throw new Error("Reader API returned empty content."); 513 | } 514 | 515 | return textContent; 516 | 517 | } catch (error) { 518 | let errorMsg = error.message; 519 | if (error.name === 'AbortError' || error.name === 'TimeoutError') { 520 | errorMsg = "Request to the reader API timed out after 25 seconds."; 521 | } 522 | console.error("URL content fetching failed:", error); 523 | showMessage(`Failed to fetch content: ${errorMsg}`, 'error'); 524 | return null; 525 | } 526 | } 527 | 528 | // --- LLM API Abstraction Layer --- 529 | async function callLLM(prompt, schema) { 530 | const provider = dom.providerSelect.value; 531 | const model = dom.modelSelect.value; 532 | const apiKey = state.apiKeys[provider]; 533 | 534 | const apiHandlers = { 535 | google: callGoogleAPI, 536 | openai: callOpenAIAPI, 537 | anthropic: callAnthropicAPI, 538 | openrouter: callOpenRouterAPI, 539 | azure: callAzureOpenAIAPI, 540 | }; 541 | 542 | if (!apiHandlers[provider]) { 543 | throw new Error(`Unsupported provider: ${provider}`); 544 | } 545 | 546 | return apiHandlers[provider](apiKey, model, prompt, schema); 547 | } 548 | 549 | // --- Remote APIs (Key Required) --- 550 | async function callGoogleAPI(apiKey, model, prompt, schema) { 551 | const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`; 552 | 553 | let finalPrompt = prompt; 554 | if (schema) { 555 | finalPrompt = `${prompt}\n\nImportant: Respond with ONLY a valid JSON object that conforms to the following schema. Do not include any other text, comments, or markdown formatting.\n\nSchema: ${JSON.stringify(schema)}`; 556 | } 557 | 558 | const payload = { 559 | contents: [{ role: "user", parts: [{ text: finalPrompt }] }], 560 | generationConfig: { temperature: 0.1 } 561 | }; 562 | 563 | const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); 564 | if (!response.ok) { 565 | const errorBody = await response.json(); 566 | throw new Error(`Google API Error: ${response.statusText} - ${errorBody.error?.message || 'Unknown error'}`); 567 | } 568 | const result = await response.json(); 569 | const textResponse = result.candidates?.[0]?.content?.parts?.[0]?.text; 570 | 571 | if (!textResponse) { 572 | const reason = result.promptFeedback?.blockReason || 'Unknown reason'; 573 | if(reason !== 'Unknown reason') throw new Error(`Request blocked by Google API. Reason: ${reason}`); 574 | throw new Error("Invalid or empty response from Google API."); 575 | } 576 | 577 | if (!schema) { 578 | return textResponse; 579 | } 580 | 581 | const jsonMatch = textResponse.match(/\{[\s\S]*\}|\[[\s\S]*\]/); 582 | if (!jsonMatch) throw new Error("Google API did not return valid JSON."); 583 | return jsonMatch[0]; 584 | } 585 | 586 | async function callOpenAIAPI(apiKey, model, prompt, schema) { 587 | const url = 'https://api.openai.com/v1/chat/completions'; 588 | const payload = { 589 | model: model, 590 | messages: [{ role: "user", content: prompt }], 591 | temperature: 0.1 592 | }; 593 | 594 | if (schema) { 595 | payload.response_format = { "type": "json_object" }; 596 | } 597 | 598 | const response = await fetch(url, { 599 | method: 'POST', 600 | headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, 601 | body: JSON.stringify(payload) 602 | }); 603 | 604 | if (!response.ok) { 605 | const errorBody = await response.json(); 606 | throw new Error(`OpenAI API Error: ${errorBody.error?.message || response.statusText}`); 607 | } 608 | const result = await response.json(); 609 | const textResponse = result.choices?.[0]?.message?.content; 610 | 611 | if (!textResponse) throw new Error("Invalid or empty response from OpenAI API."); 612 | 613 | if (!schema) { 614 | return textResponse; 615 | } 616 | 617 | return textResponse; 618 | } 619 | 620 | async function callAnthropicAPI(apiKey, model, prompt, schema) { 621 | const url = 'https://api.anthropic.com/v1/messages'; 622 | let systemPrompt = "You are a helpful assistant."; 623 | 624 | if (schema) { 625 | systemPrompt = `You are a helpful assistant designed to output JSON. Respond with a valid JSON object that conforms to this schema. Do not add any other text. Schema: ${JSON.stringify(schema)}`; 626 | } 627 | 628 | const payload = { 629 | model: model, 630 | max_tokens: 4096, 631 | system: systemPrompt, 632 | messages: [{ role: "user", content: prompt }], 633 | temperature: 0.2 634 | }; 635 | const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' }, body: JSON.stringify(payload) }); 636 | if (!response.ok) { 637 | const errorBody = await response.json(); 638 | throw new Error(`Anthropic API Error: ${errorBody.error?.message || response.statusText}`); 639 | } 640 | const result = await response.json(); 641 | const textResponse = result.content?.[0]?.text; 642 | 643 | if (!textResponse) throw new Error("Invalid or empty response from Anthropic API."); 644 | 645 | if (!schema) { 646 | return textResponse; 647 | } 648 | 649 | const jsonMatch = textResponse.match(/\{[\s\S]*\}|\[[\s\S]*\]/); 650 | if (!jsonMatch) throw new Error("Anthropic API did not return valid JSON."); 651 | return jsonMatch[0]; 652 | } 653 | 654 | async function callOpenRouterAPI(apiKey, model, prompt, schema) { 655 | const url = 'https://openrouter.ai/api/v1/chat/completions'; 656 | const payload = { 657 | model: model, 658 | messages: [{ role: "user", content: prompt }], 659 | temperature: 0.1 660 | }; 661 | 662 | if (schema) { 663 | payload.response_format = { "type": "json_object" }; 664 | } 665 | 666 | const response = await fetch(url, { 667 | method: 'POST', 668 | headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, 'HTTP-Referer': `${location.protocol}//${location.host}`, 'X-Title': 'KQLIntel' }, 669 | body: JSON.stringify(payload) 670 | }); 671 | if (!response.ok) { 672 | const errorBody = await response.json(); 673 | throw new Error(`OpenRouter API Error: ${errorBody.error?.message || response.statusText}`); 674 | } 675 | const result = await response.json(); 676 | const textResponse = result.choices?.[0]?.message?.content; 677 | 678 | if (!textResponse) throw new Error("Invalid or empty response from OpenRouter API."); 679 | 680 | if (!schema) { 681 | return textResponse; 682 | } 683 | 684 | return textResponse; 685 | } 686 | 687 | async function callAzureOpenAIAPI(apiCredentials, model, prompt, schema) { 688 | const { key, endpoint, deployment } = apiCredentials; 689 | const sanitizedEndpoint = endpoint.replace(/\/+$/, ""); 690 | const url = `${sanitizedEndpoint}/openai/deployments/${deployment}/chat/completions?api-version=2024-02-01`; 691 | 692 | const payload = { 693 | messages: [{ role: "user", content: prompt }], 694 | temperature: 0.1 695 | }; 696 | 697 | if (schema) { 698 | payload.response_format = { "type": "json_object" }; 699 | } 700 | 701 | const response = await fetch(url, { 702 | method: 'POST', 703 | headers: { 'Content-Type': 'application/json', 'api-key': key }, 704 | body: JSON.stringify(payload) 705 | }); 706 | 707 | if (!response.ok) { 708 | const errorBody = await response.json(); 709 | throw new Error(`Azure API Error: ${errorBody.error?.message || response.statusText}`); 710 | } 711 | const result = await response.json(); 712 | const textResponse = result.choices?.[0]?.message?.content; 713 | 714 | if (!textResponse) throw new Error("Invalid response from Azure API."); 715 | 716 | if (!schema) { 717 | return textResponse; 718 | } 719 | 720 | return textResponse; 721 | } 722 | 723 | 724 | // --- Run Initialization --- 725 | initialize(); 726 | --------------------------------------------------------------------------------