From cd9c99c49975e872744f78572a54816f670d2ead Mon Sep 17 00:00:00 2001 From: tisf Date: Fri, 26 Jul 2024 10:18:05 +0700 Subject: [PATCH] init 0.0.1 --- README.md | 58 ++++++ dataview_examples.md | 52 +++++ main.js | 444 +++++++++++++++++++++++++++++++++++++++++++ manifest.json | 10 + package.json | 13 ++ 5 files changed, 577 insertions(+) create mode 100644 README.md create mode 100644 dataview_examples.md create mode 100644 main.js create mode 100644 manifest.json create mode 100644 package.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..2127334 --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# VirusTotal Enrichment Plugin for Obsidian + +## About +The VirusTotal Enrichment Plugin for Obsidian allows you to enhance your notes by querying VirusTotal directly within Obsidian. You can enrich notes with detailed properties of files, URLs, IP addresses, or domains, which can then be utilized within the note's body or as metadata for advanced querying capabilities, particularly with plugins like [dataview](https://blacksmithgu.github.io/obsidian-dataview/). + +## How to Use +To use the VirusTotal Enrichment Plugin, follow these steps: +1. Install the plugin via Obsidian’s third-party plugin settings. +2. Obtain an API key from [VirusTotal](https://www.virustotal.com/gui/join-us). +3. Enter your API key in the plugin settings within Obsidian. +4. Use the command palette (`Ctrl/Cmd + P`) to run VirusTotal queries: + - `Enrich Current Note`: Enriches the open note with data fetched from VirusTotal based on content detected in the note. + +## How to - Settings +Navigate to the plugin settings in Obsidian to configure the following options: +- **API Key**: Securely store your VirusTotal API key. +- **Include Page Type**: Toggle to include the type of page (e.g., indicator, IP, URL) as a note property. +- **Custom Fields**: Define which properties from VirusTotal you wish to include in your notes, such as `md5`, `sha256`, `filetype`, etc. + +## How to - Dataview Examples +Once your notes are enriched, you can utilize Dataview to create dynamic lists and tables based on the enriched data. Here are a few example queries: +```` +```dataview +table md5, sha256, creation_date +from "notes-folder" +where contains(filetype, "executable") +``` +```` + +```` +```dataview +list +from "notes-folder" +where sha256 = "specific-hash-value" +``` +```` + +See more examples in this repo in [this link](https://github.com/ytisf/virustotal-enrich/dataview_examples.md). + +## License +This project is licensed under the GNU License - see the LICENSE file for details. + +## Thanks +Special thanks to: +- The developers of [Obsidian](https://github.com/obsidianmd) for creating such a versatile tool. +- The [VirusTotal](https://github.com/VirusTotal/vt-py) team for providing the API. +- Thanks to a good friend (namesless for now) who provided the push i needed to tackle NodeJS and overcome my laziness for a plugin i was hoping someone else would develop... + +## How to Contribute +Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated. + +### Fork the Project +- Create your Feature Branch (`git checkout -b feature/AmazingFeature`) +- Commit your Changes (`git commit -m 'Add some AmazingFeature'`) +- Push to the Branch (`git push origin feature/AmazingFeature`) +- Open a Pull Request + +For more detailed instructions, you may follow the standard GitHub documentation on creating a pull request. \ No newline at end of file diff --git a/dataview_examples.md b/dataview_examples.md new file mode 100644 index 0000000..ebd9b57 --- /dev/null +++ b/dataview_examples.md @@ -0,0 +1,52 @@ + +# Dataview Examples for VirusTotal Enrichment Plugin + +The following examples demonstrate how you can use the Dataview plugin to leverage the data enriched via the VirusTotal Enrichment Plugin in Obsidian. + +## Example 1: List All Executables +Generate a list of all notes containing executable files with their MD5 and SHA256 hashes. + +\`\`\`dataview +table md5, sha256, creation_date +from "notes-folder" +where filetype = "executable" +\`\`\` + +## Example 2: Find Specific Hash +Display notes where the SHA256 hash matches a specific value. Useful for tracking specific threats. + +\`\`\`dataview +list +from "notes-folder" +where sha256 = "specific-hash-value" +\`\`\` + +## Example 3: Group By File Type +Group notes by file type and list their corresponding hashes. + +\`\`\`dataview +table filetype, md5, sha256 +from "notes-folder" +group by filetype +\`\`\` + +## Example 4: Recent Submissions +List recent submissions by creation date. + +\`\`\`dataview +list creation_date, name +from "notes-folder" +sort creation_date desc +\`\`\` + +## Example 5: Large Files +Identify large files in your notes, which could be potential high-risk items. + +\`\`\`dataview +table name, size, md5 +from "notes-folder" +where size > 1000000 +sort size desc +\`\`\` + +These examples provide a starting point for querying and organizing your enriched data effectively. diff --git a/main.js b/main.js new file mode 100644 index 0000000..a36dd0d --- /dev/null +++ b/main.js @@ -0,0 +1,444 @@ +const { Plugin, PluginSettingTab, Setting, Notice, Modal } = require('obsidian'); +const { spawn } = require('child_process'); +const path = require('path'); +const fs = require('fs'); +const { exec } = require('child_process'); +const os = require('os'); +const platform = process.platform; + + +function curlRequest({ url, method = 'GET', headers = {} }) { + // Construct the header part of the curl command + let headerStr = ''; + for (const [key, value] of Object.entries(headers)) { + headerStr += `-H "${key}: ${value}" `; + } + + // Create the curl command + const curlCmd = `curl -X ${method} ${headerStr} "${url}"`; + + // Execute the curl command + return new Promise((resolve, reject) => { + exec(curlCmd, (error, stdout, stderr) => { + if (error) { + console.error(`exec error: ${error}`); + reject({ code: error.code, error: stderr }); + } else { + console.log(`stdout: ${stdout}`); + resolve({ code: 0, content: stdout }); + } + }); + }); +} + +function powershellRequest({ url, method = 'GET', headers = {} }) { + // Construct the header part of the PowerShell command + let headerStr = ''; + for (const [key, value] of Object.entries(headers)) { + headerStr += `-Headers @{${key}='${value}'} `; + } + + // Create the PowerShell command + const psCmd = `powershell -Command "(Invoke-WebRequest -Uri '${url}' -Method ${method} ${headerStr} -UseBasicParsing).Content"`; + + // Execute the PowerShell command + return new Promise((resolve, reject) => { + exec(psCmd, { shell: 'powershell.exe' }, (error, stdout, stderr) => { + if (error) { + console.error(`exec error: ${error}`); + reject({ code: error.code, error: stderr }); + } else { + console.log(`stdout: ${stdout}`); + resolve({ code: 0, content: stdout }); + } + }); + }); +} + +function identifyInput(input) { + // Regular expression for MD5, SHA1, SHA256 hashes + const hashRegex = /^[a-f0-9]{32}$|^[a-f0-9]{40}$|^[a-f0-9]{64}$/i; + + // Regular expression for IPv4 addresses + const ipv4Regex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){2}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; + + // Regular expression for domains + const domainRegex = /^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\.[a-zA-Z]{2,}$/; + + // Check if input matches hash patterns (MD5, SHA1, SHA256) + if (hashRegex.test(input)) { + return "files"; + } + + // Check if input matches IPv4 pattern + if (ipv4Regex.test(input)) { + return "ip_addresses"; + } + + // Check if input matches domain pattern + if (domainRegex.test(input)) { + return "domains"; + } + + // If none of the above, return unknown + return "unknown"; +} + +function convertEpochToISO(obj) { + function isEpoch(num) { + // Check if the number is in the range of common epoch timestamps (specifically targeting recent timestamps) + // This range covers dates from around 2001 to 2030 + return num > 1000000000 && num < 2000000000; + } + + function convert(obj) { + for (let key in obj) { + if (obj.hasOwnProperty(key)) { + if (typeof obj[key] === 'object') { + convert(obj[key]); // Recursively search for nested objects + } else if (typeof obj[key] === 'number' && isEpoch(obj[key])) { + var newDate = new Date(obj[key] * 1000).toISOString(); // Convert epoch to ISO date-time string + obj[key] = newDate.replace(/_/g, '-').replace('T', ' ').replace(/(\d{2})_(\d{2})_(\d{2})/, '$1:$2:$3').split('.')[0]; + + } + } + } + } + + const clonedObj = JSON.parse(JSON.stringify(obj)); // Clone the original object to avoid mutating it + convert(clonedObj); + return clonedObj; +} + +class VirusTotalEnrichPlugin extends Plugin { + settings = { + apiKey: '', + includePageType: true, + pageType: 'indicator', + customFields: { + 'name': 'attributes.meaningful_name', + 'first_submission_date': 'attributes.first_submission_date', + 'creation_date': 'attributes.creation_date', + 'filetype': 'attributes.type_description', + 'size': 'attributes.size', + 'md5': 'attributes.md5', + 'sha1': 'attributes.sha1', + 'sha256': 'attributes.sha256', + 'magic': 'attributes.magic', + 'tlsh': 'attributes.tlsh', + 'ssdeep': 'attributes.ssdeep', + } + }; + + onload() { + console.log('Loading VirusTotal Enrichment plugin'); + this.loadSettings(); + + // Register the settings tab + this.addSettingTab(new SettingTab(this.app, this)); + + // Register the about modal command + this.addCommand({ + id: 'open-about-modal', + name: 'Open About Page', + callback: () => this.openAboutModal() + }); + + // Register the enrich command + this.addCommand({ + id: 'enrich-current-note', + name: 'Enrich Current Note', + callback: () => { + const enricher = new EnrichIndicator(this.app, this); + enricher.enrichCurrentNote(); + } + }); + } + + onunload() { + console.log('Unloading plugin'); + } + async loadSettings() { + try { + this.settings = Object.assign({}, this.settings, await this.loadData()); + } catch (error) { + console.error('Failed to load settings:', error); + new Notice('Error loading settings.'); + } + } + async saveSettings() { + try { + await this.saveData(this.settings); + new Notice('Settings saved successfully!'); + } catch (error) { + console.error('Failed to save settings:', error); + new Notice('Error saving settings.'); + } + } + openAboutModal() { + // Simple modal to show about information + new AboutModal(this.app).open(); + } +} + +class SettingTab extends PluginSettingTab { + plugin; + + constructor(app, plugin) { + super(app, plugin); + this.plugin = plugin; + } + + display() { + const {containerEl} = this; + containerEl.empty(); + containerEl.createEl('h2', {text: 'Settings for Virus Total Enrichment'}); + + new Setting(containerEl) + .setName('API Key') + .setDesc('Enter your API key here.') + .addText(text => text + .setValue(this.plugin.settings.apiKey) + .onChange(async (value) => { + this.plugin.settings.apiKey = value; + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName('Include Page Type') + .setDesc('Toggle whether to include the Page Type property.') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.includePageType) + .onChange(async (value) => { + this.plugin.settings.includePageType = value; + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName('Page Type Value') + .setDesc('Set the value for the Page Type property.') + .addText(text => text + .setValue(this.plugin.settings.pageType) + .onChange(async (value) => { + this.plugin.settings.pageType = value; + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName('Custom Fields') + .setDesc('Add custom key-value pairs for enrichment.') + .addTextArea(text => text + .setValue(JSON.stringify(this.plugin.settings.customFields, null, 2)) + .onChange(async (value) => { + try { + this.plugin.settings.customFields = JSON.parse(value); + await this.plugin.saveSettings(); + } catch (error) { + new Notice('Invalid JSON format for custom fields.'); + } + })); + } +} + +class AboutModal extends Modal { + constructor(app) { + super(app); + } + + onOpen() { + const {contentEl} = this; + contentEl.empty(); + + contentEl.createEl('h1', { text: 'VirusTotal Enrichment Plugin' }); + + contentEl.createEl('img', { + attr: { + src: 'path_to_your_image.png', + alt: 'Plugin Icon' + }, + cls: 'modal-icon' + }); + + contentEl.createEl('p', { text: 'This plugin enhances your Obsidian notes by fetching and displaying data from VirusTotal based on the content of your notes.\nNotes will be enriched with properties as well as entire JSON digest as an appendix to the note. This plugin was desgined to, hopefully, be easy to use with dataview for cool queries.' }); + + contentEl.createEl('h3', { text: 'Developed by:' }); + contentEl.createEl('p', { text: 'tisf' }); + + contentEl.createEl('h3', { text: 'GitHub Repository:' }); + contentEl.createEl('a', { + text: 'View on GitHub', + href: 'https://github.com/ytisf/virustotal-enrich' + }); + + contentEl.createEl('h3', { text: 'Dataview Examples:' }); + contentEl.createEl('a', { + text: 'View on GitHub', + href: 'https://github.com/ytisf/virustotal-enrich/dataview_examples.md' + }); + + contentEl.createEl('h3', { text: 'End-User License Agreement (EULA):' }); + contentEl.createEl('a', { + text: 'Read EULA', + href: 'https://github.com/ytisf/virustotal-enrich/blob/main/LICENSE' + }); + + contentEl.createEl('p', { text: 'For more information and updates, follow the repository on GitHub.' }); + + contentEl.createEl('button', { + text: 'Close', + cls: 'mod-cta', + type: 'button', + onclick: () => { + this.close(); + } + }); + } + + onClose() { + let {contentEl} = this; + contentEl.empty(); + } +} + +class EnrichIndicator { + constructor(app, plugin) { + this.app = app; + this.plugin = plugin; + } + + createYamlPreamble(jsonData) { + const escapeYamlString = (str) => { + // Replace problematic characters with underscore and escape double quotes + let sanitized = str.replace(/[:\{\}\[\],&*#?|\-<>=!%@\\]/g, '_').replace(/"/g, '\\"'); + // Wrap the sanitized string in double quotes + return `"${sanitized}"`; + }; + + const recurseObject = (obj, indent = '') => { + let yamlContent = ''; + for (const key in obj) { + if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) { + yamlContent += `${indent}${key}:\n`; + yamlContent += recurseObject(obj[key], indent + ' '); + } else if (Array.isArray(obj[key])) { + yamlContent += `${indent}${key}:\n`; + obj[key].forEach((item) => { + if (typeof item === 'object' && item !== null) { + yamlContent += `${indent} - `; + yamlContent += recurseObject(item, indent + ' ').trim(); + yamlContent += '\n'; + } else { + const itemStr = String(item); + if (itemStr.length <= 513) { + yamlContent += `${indent} - ${escapeYamlString(itemStr)}\n`; + } + } + }); + } else { + const valueStr = String(obj[key]); + if (valueStr.length <= 513) { + yamlContent += `${indent}${key}: ${escapeYamlString(valueStr)}\n`; + } + } + } + return yamlContent; + }; + + return `\n${recurseObject(jsonData)}---`; + } + + enrichCurrentNote() { + + let httpFunction; + if (platform === 'win32') { + httpFunction = powershellRequest; + } else { + httpFunction = curlRequest; + } + + + const activeLeaf = this.app.workspace.activeLeaf; + if (activeLeaf) { + const editor = activeLeaf.view.sourceMode.cmEditor; + const content = editor.getValue(); + const noteTitle = this.app.workspace.getActiveFile().basename; + + var search_type = identifyInput(noteTitle); + var url_to_get = `https://www.virustotal.com/api/v3/${search_type}/${noteTitle}`; + + // Use the curlRequest function to send data to an API + httpFunction({ + url: url_to_get, + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'x-apikey': `${this.plugin.settings.apiKey}` + } + }).then(response => { + // Check if response has content and try to parse it + if (response && response.content) { + try { + // Attempt to parse the content string into JSON + // var parsedContent = JSON.parse(response.content)["data"]; + var fixed_content = convertEpochToISO(JSON.parse(response.content)["data"]) + var data = JSON.stringify(fixed_content, null, 2); + } catch (error) { + console.error('Error parsing JSON from content:', error); + var data = '{"error": "Failed to parse content"}'; + new Notice(`Request failed: ${response.content}`); + return; + } + } else { + console.error('Invalid or missing data in response:', response); + var data = '{"error": "No content found"}'; + new Notice(`Request failed: ${response.content}`); + return; + } + + // Got Response - Now process it: + var bottom_appendix = '\n\n\n\n\n#### Appendix - VirusTotal Output\n```json\n' + data + '\n```\n\n'; + + // Ensure the cursor is at the bottom and append the data + editor.setCursor(editor.lineCount(), 0); + editor.replaceSelection(bottom_appendix); + + // Parse the content JSON and create YAML preamble + var now = new Date().toISOString().replace('T', ' ').substring(0, 19); + var new_content = editor.getValue(); + var yamlPreamble = "---\n"; + if (this.plugin.settings.includePageType) { + yamlPreamble += `pageType: ${this.plugin.settings.pageType}\n`; + } + // Handle custom fields + Object.entries(this.plugin.settings.customFields).forEach(([key, path]) => { + const value = _.get(JSON.parse(data), path, 'Not available'); + yamlPreamble += `${key}: ${value}\n`; + }); + yamlPreamble = `${yamlPreamble}main_value: ${noteTitle}\n`; + yamlPreamble = `${yamlPreamble}enrichment_date: ${now}\n`; + yamlPreamble = `${yamlPreamble}adversary: \n`; + yamlPreamble = `${yamlPreamble}nation-state: \n`; + yamlPreamble = `${yamlPreamble}variant: \n`; + yamlPreamble = `${yamlPreamble}campaign: \n`; + yamlPreamble = `${yamlPreamble}main_comment: \n`; + yamlPreamble = `${yamlPreamble}toolset: \n`; + + yamlPreamble += this.createYamlPreamble(fixed_content); + + var updatedContent = yamlPreamble + "\n" + new_content; + editor.setValue(updatedContent); + + new Notice('Note enriched successfully.'); + + }).catch(error => { + console.error('Failed to enrich note:', error); + new Notice('Failed to enrich note.'); + }); + } else { + new Notice('No active note found.'); + } + } +} + + +module.exports = VirusTotalEnrichPlugin; diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..62edfe1 --- /dev/null +++ b/manifest.json @@ -0,0 +1,10 @@ +{ + "id": "virustotal-enrich", + "name": "Virus Total Enrichment Plugin", + "version": "0.0.1", + "minAppVersion": "1.5.12", + "description": "Enrich your Obsidian notes with VirusTotal information.", + "author": "Yuval tisf Nativ", + "authorUrl": "https://www.github.com/ytisf/", + "isDesktopOnly": true +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..9509639 --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "name": "vt_enrich", + "version": "1.0.0", + "description": "Enrich your Obsidian notes with VirusTotal information.", + "main": "main.js", + "keywords": ["virustotal", "threat hunting", "enrichment"], + "author": "", + "license": "GNU", + "dependencies": { + "node-fetch": "^3.3.2" + } + } + \ No newline at end of file