UNPKG

obsidian-cyber-utils

Version:

Utility functions for developing Obsidian plugins, focused on cyber security note-taking.

1,512 lines (1,492 loc) 74.6 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { BaseOcrProcessor: () => BaseOcrProcessor, CENSYS_SEARCH: () => CENSYS_SEARCH, CodeListModal: () => CodeListModal, CodeModal: () => CodeModal, CyberPlugin: () => CyberPlugin, DDG_SEARCH: () => DDG_SEARCH, DOMAIN_EXCLUSIONS: () => DOMAIN_EXCLUSIONS, DOMAIN_REGEX: () => DOMAIN_REGEX, ErrorModal: () => ErrorModal, FILE_REGEX: () => FILE_REGEX, GOOGLE_SEARCH: () => GOOGLE_SEARCH, GeminiClient: () => GeminiClient, GeminiOcrProcessor: () => GeminiOcrProcessor, HASH_REGEX: () => HASH_REGEX, IPDB_SEARCH: () => IPDB_SEARCH, IP_EXCLUSIONS: () => IP_EXCLUSIONS, IP_REGEX: () => IP_REGEX, IPv4_REGEX: () => IP_REGEX, IPv6_REGEX: () => IPv6_REGEX, IndicatorSource: () => IndicatorSource, IndicatorType: () => IndicatorType, InputModal: () => InputModal, LOCAL_IP_REGEX: () => LOCAL_IP_REGEX, MACRO_REGEX: () => MACRO_REGEX, MD5_REGEX: () => MD5_REGEX, Matcher: () => Matcher, OcrProvider: () => OcrProvider, OllamaClient: () => OllamaClient, OpenAICompatibleClient: () => OpenAICompatibleClient, PATTERN_KEYS: () => PATTERN_KEYS, SHA1_REGEX: () => SHA1_REGEX, SHA256_REGEX: () => SHA256_REGEX, SHODAN_SEARCH: () => SHODAN_SEARCH, SPUR_SEARCH: () => SPUR_SEARCH, ScriptObject: () => ScriptObject, TLD_URL: () => TLD_URL, TesseractOcrProcessor: () => TesseractOcrProcessor, URLSCAN_SEARCH: () => URLSCAN_SEARCH, VT_SEARCH: () => VT_SEARCH, addButtonContainer: () => addButtonContainer, addButtonToContainer: () => addButtonToContainer, addUniqueValuesToArray: () => addUniqueValuesToArray, appendToEnd: () => appendToEnd, censysSearch: () => censysSearch, checkFolderExistsRecursive: () => checkFolderExistsRecursive, constructMacroRegex: () => constructMacroRegex, convertTime: () => convertTime, createFolderIfNotExists: () => createFolderIfNotExists, createNote: () => createNote, datePickerSettingEl: () => datePickerSettingEl, dateTimeRegex: () => dateTimeRegex, ddgSearch: () => ddgSearch, decryptString: () => decryptString, defangDomain: () => defangDomain, defangEmail: () => defangEmail, defangIp: () => defangIp, defaultSites: () => defaultSites, encodeImageFile: () => encodeImageFile, encryptString: () => encryptString, extractMacros: () => extractMacros, extractMatches: () => extractMatches, filterExclusions: () => filterExclusions, findFirstByRegex: () => findFirstByRegex, friendlyDatetime: () => friendlyDatetime, getActiveNoteContent: () => getActiveNoteContent, getAttachmentFiles: () => getAttachmentFiles, getAttachments: () => getAttachments, getBacklinks: () => getBacklinks, getIndicatorMatches: () => getIndicatorMatches, getIocType: () => getIocType, getMatches: () => getMatches, getValidTld: () => getValidTld, googleSearch: () => googleSearch, groupIndicators: () => groupIndicators, initializeWorker: () => initializeWorker, ipdbSearch: () => ipdbSearch, isLocalIpv4: () => isLocalIpv4, loadPrivateData: () => loadPrivateData, localDateTime: () => localDateTime, lowerMd5: () => lowerMd5, lowerSha256: () => lowerSha256, macroSeparator: () => macroSeparator, macroValue: () => macroValue, noteAppend: () => noteAppend, noteReplace: () => noteReplace, ocr: () => ocr, ocrMultiple: () => ocrMultiple, openDetails: () => openDetails, openNote: () => openNote, parseCodeBlocks: () => parseCodeBlocks, processExclusions: () => processExclusions, readImageFile: () => readImageFile, refangIoc: () => refangIoc, removeArrayDuplicates: () => removeArrayDuplicates, removeDotObsidian: () => removeDotObsidian, removeElements: () => removeElements, replaceMacros: () => replaceMacros, replaceTemplateText: () => replaceTemplateText, savePrivateData: () => savePrivateData, shodanSearch: () => shodanSearch, spurSearch: () => spurSearch, supportedMacros: () => supportedMacros, todayFolderStructure: () => todayFolderStructure, todayLocalDate: () => todayLocalDate, transformSelectedText: () => transformSelectedText, urlscanSearch: () => urlscanSearch, validateDomain: () => validateDomain, validateDomains: () => validateDomains, virusTotalRequest: () => virusTotalRequest, vtSearch: () => vtSearch }); module.exports = __toCommonJS(index_exports); // src/cyberPlugin.ts var import_obsidian3 = require("obsidian"); var import_events2 = require("events"); // src/vaultUtils.ts var import_obsidian = require("obsidian"); async function checkFolderExistsRecursive(vault, folderName) { async function searchFolder(rootPath) { const checkVal = rootPath + "/" + folderName; const folderExists = await vault.adapter.exists(checkVal, true); if (folderExists) return folderName; const subFolders = (await vault.adapter.list(rootPath)).folders; const i = subFolders.indexOf(".obsidian"); if (i > -1) { subFolders.splice(i, 1); } for (const subFolder of subFolders) { const isSubFolder = await vault.adapter.exists(subFolder, true); if (isSubFolder) { const found = await searchFolder(subFolder); if (found && !found.startsWith(subFolder)) { return `${subFolder}/${found}`; } else if (found) return found; } } return ""; } return await searchFolder(""); } function removeDotObsidian(files) { const removals = [".obsidian", ".DS_Store"]; removals.forEach((value) => { const i = files.indexOf(value); if (i > -1) { files.splice(i, 1); } }); return files; } async function createFolderIfNotExists(vault, folderName) { const folder = await checkFolderExistsRecursive(vault, folderName); if (!folder) { await vault.createFolder(folderName); } } async function createNote(vault, folderName, noteTitle) { return await vault.create(`${folderName}/${noteTitle}.md`, ""); } function getBacklinks(notePath, app, resolved = false) { let backlinks = null; if (resolved) { backlinks = app.metadataCache.resolvedLinks[notePath]; } else { backlinks = app.metadataCache.unresolvedLinks[notePath]; } const retval = []; for (const i in backlinks) { retval.push(i); } return retval; } function getAttachments(notePath, app) { const links = getBacklinks(notePath, app, true); const attachments = /* @__PURE__ */ new Set(); links.forEach((link) => { const file = app.vault.getAbstractFileByPath(link); if (file && file instanceof import_obsidian.TFile && file.extension !== "md") { attachments.add(file); } }); return Array.from(attachments); } function getAttachmentFiles(note, app) { const links = getBacklinks(note.path, app, true); const attachments = /* @__PURE__ */ new Set(); links.forEach((link) => { const file = app.vault.getAbstractFileByPath(link); if (file && file instanceof import_obsidian.TFile && file.extension !== "md") { attachments.add(file); } }); return Array.from(attachments); } function noteAppend(vault, note, content) { return vault.process(note, (data) => { return data + content; }); } function noteReplace(vault, note, regex, content) { return vault.process(note, (data) => { return data.replace(regex, content); }); } function openNote(app, note) { if (!note || !app) return; const view = app.workspace.getLeaf(); view.openFile(note); } // src/regex.ts var ipv4Octet = "(?:25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])"; var ipv6Octet = "[0-9a-fA-F]{1,4}"; var IP_REGEX = new RegExp( // match a possibly url-encoded character preceding, or // a word boundary String.raw`(?:%[0-9a-fA-F]{2})?(?=\b|^)(` + `(?:${ipv4Octet + possiblyDefangedVal(String.raw`\.`)}){3}` + ipv4Octet + ")", "g" // flags ); var IPv6_REGEX = new RegExp( `((?:${ipv6Octet}${possiblyDefangedVal(":")}){7}${ipv6Octet}|(?:(?:${ipv6Octet}${possiblyDefangedVal(":")})*${ipv6Octet})?${possiblyDefangedVal("::")}(?:(?:${ipv6Octet}${possiblyDefangedVal(":")})*${ipv6Octet})?)`, // zero or more segments "gi" // flags ); var LOCAL_IP_REGEX = /^((127\.)|(10\.)|(172\.1[6-9]\.)|(172\.2[0-9]\.)|(172\.3[0-1]\.)|(192\.168\.))/g; var MACRO_REGEX = /({{([^}]+)}})/g; var DOMAIN_REGEX = new RegExp( String.raw`(?:%[0-9a-f]{2})?((?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?` + possiblyDefangedVal(String.raw`\.`) + `)+` + String.raw`[a-z][a-z0-9-]{0,61}[a-z](?=\.?)\b)`, "gi" // flags ); var hash_start = "(?:%[0-9a-f]{2})?(?<=^|[^a-f0-9]+)"; var hash_end = "(?=$|[^a-f0-9]+)"; var HASH_REGEX = new RegExp( hash_start + "([a-f0-9]{64}|[a-f0-9]{40}|[a-f0-9]{32})" + // standard hash length (SHA256, MD5, SHA1) hash_end, "gi" // flags ); var SHA256_REGEX = new RegExp( hash_start + "([a-f0-9]{64})" + // SHA256 hash length hash_end, "gi" // flags ); var MD5_REGEX = new RegExp( hash_start + "([a-f0-9]{32})" + // SHA256 hash length hash_end, "gi" // flags ); var SHA1_REGEX = new RegExp( hash_start + "([a-f0-9]{40})" + // SHA256 hash length hash_end, "gi" // flags ); var FILE_REGEX = new RegExp( String.raw`(?:%[0-9a-f]{2})?(?<=^|\s|")` + // beginning of string, space, or open quote "(" + String.raw`(?:\w:\\|[\\/])` + // drive letter or leading slash String.raw`(?:[^\\/:][\\/]?)+` + // String.raw`[^\\/\n"|]+\.\w+` + // filename with extension ")", "gi" ); function possiblyDefangedVal(val) { return String.raw`[\[\(\\]?${val}[\]\)]?`; } // src/iocParser.ts var IndicatorType = /* @__PURE__ */ ((IndicatorType2) => { IndicatorType2["IPv4"] = "ipv4"; IndicatorType2["PRIVATE_IPv4"] = "private_ipv4"; IndicatorType2["IPv6"] = "ipv6"; IndicatorType2["PRIVATE_IPv6"] = "private_ipv6"; IndicatorType2["DOMAIN"] = "domain"; IndicatorType2["HASH"] = "hash"; IndicatorType2["EMAIL"] = "email"; return IndicatorType2; })(IndicatorType || {}); var IndicatorSource = /* @__PURE__ */ ((IndicatorSource2) => { IndicatorSource2["TEXT"] = "text"; IndicatorSource2["OCR"] = "ocr"; return IndicatorSource2; })(IndicatorSource || {}); function filterExclusions(items, exclusions) { return items.filter( (item) => !exclusions.some( (exclusion) => typeof exclusion === "string" ? item === exclusion : exclusion.test(item) ) ); } function groupIndicators(indicators) { const grouped = { ["text" /* TEXT */]: { ["ipv4" /* IPv4 */]: [], ["private_ipv4" /* PRIVATE_IPv4 */]: [], ["ipv6" /* IPv6 */]: [], ["private_ipv6" /* PRIVATE_IPv6 */]: [], ["domain" /* DOMAIN */]: [], ["hash" /* HASH */]: [], ["email" /* EMAIL */]: [] }, ["ocr" /* OCR */]: { ["ipv4" /* IPv4 */]: [], ["private_ipv4" /* PRIVATE_IPv4 */]: [], ["ipv6" /* IPv6 */]: [], ["private_ipv6" /* PRIVATE_IPv6 */]: [], ["domain" /* DOMAIN */]: [], ["hash" /* HASH */]: [], ["email" /* EMAIL */]: [] } }; const seenIndicators = /* @__PURE__ */ new Set(); for (const indicator of indicators) { const source = indicator.source || "text" /* TEXT */; const uniqueKey = `${source}:${indicator.value}`; if (seenIndicators.has(uniqueKey)) { continue; } seenIndicators.add(uniqueKey); grouped[source][indicator.type].push(indicator); } return grouped; } // src/textUtils.ts var import_obsidian2 = require("obsidian"); var TLD_URL = "https://data.iana.org/TLD/tlds-alpha-by-domain.txt"; async function getValidTld() { const tldParams = { url: "https://data.iana.org/TLD/tlds-alpha-by-domain.txt", throw: true }; try { const data = await (0, import_obsidian2.request)(tldParams); const tlds = data.split("\n"); if (tlds[0].startsWith("#")) tlds.shift(); if (!tlds.slice(-1)[0]) tlds.pop(); return tlds; } catch (e) { console.error("failed to get valid TLDs"); console.error(e); return null; } } function todayLocalDate() { const tzoffset = (/* @__PURE__ */ new Date()).getTimezoneOffset() * 6e4; const date = new Date(Date.now() - tzoffset).toISOString().slice(0, 10); return date; } function localDateTime() { return `${todayLocalDate()} ${new Date(Date.now()).toString().slice(16, 21)}`; } function todayFolderStructure(prefs) { const date = todayLocalDate(); const year = date.slice(0, 4); const month = Number(date.slice(5, 7)); const yearMonth = date.slice(0, 7); const currentQuarter = Math.floor((month + 2) / 3); const folderArray = []; if (prefs.year) folderArray.push(year); if (prefs.quarter) folderArray.push(`${year}-Q${currentQuarter}`); if (prefs.month) folderArray.push(yearMonth); if (prefs.day) folderArray.push(date); return folderArray; } function defangIp(text) { return text.replaceAll(/(\d{1,3}\.\d{1,3}\.\d{1,3})\.(\d{1,3})/g, "$1[.]$2"); } function defangDomain(text) { const httpString = /http(s?):\/\//gi; const anyDomain = /(([\w-]\.?)+)\.((xn--)?([a-z][a-z0-9-]{1,60}|[a-z][a-z0-9-]{1,29}\.[a-z]{2,}))/gi; let retval = text.replaceAll(httpString, "hxxp$1[://]"); retval = retval.replaceAll(anyDomain, "$1[.]$3"); return retval; } function defangEmail(text) { const emailString = /([^\s]+)@([^\s]+)\.([^\s]+)/gi; const retval = text.replaceAll(emailString, "$1[@]$2[.]$3"); return retval; } function refangIoc(text) { let retval = text.replaceAll("[.]", "."); retval = retval.replaceAll("(.)", "."); retval = retval.replaceAll(String.raw`\.`, "."); retval = retval.replaceAll("[/]", "/"); retval = retval.replaceAll("[//]", "/"); retval = retval.replaceAll("[@]", "@"); retval = retval.replaceAll("[at]", "@"); retval = retval.replaceAll("hxxp", "http"); retval = retval.replaceAll("[:]", ":"); retval = retval.replaceAll("[::]", "::"); retval = retval.replaceAll("[://]", "://"); retval = retval.toLowerCase(); return retval; } function lowerSha256(hash) { return hash.replace(/([0-9a-fA-F]{64})/g, function(match) { return match.toLowerCase(); }); } function lowerMd5(text) { return text.replace(/([0-9a-fA-F]{32})/g, function(match) { return match.toLowerCase(); }); } var dateTimeRegex = /(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2}\s+UTC)/g; function friendlyDatetime(text) { return text.replace(dateTimeRegex, "$1 at $2"); } function findFirstByRegex(text, regex) { const result = regex.exec(text); if (!result) { return null; } else { return result[1]; } } function replaceTemplateText(template, content, note, contentMacro = "{{content}}") { let template_replaced = template.replaceAll("{{title}}", note.name.slice(0, -3)); const dateTime = localDateTime().split(" "); template_replaced = template_replaced.replaceAll("{{date}}", dateTime[0]); template_replaced = template_replaced.replaceAll("{{time}}", dateTime[1]); template_replaced = template_replaced.replaceAll(contentMacro, content); return template_replaced; } function extractMacros(text) { const regexTest = new RegExp(MACRO_REGEX.source, MACRO_REGEX.flags); const matches = text.matchAll(regexTest); return addUniqueValuesToArray([], matches); } function extractMatches(text, pattern) { if (Array.isArray(pattern)) { const matches = []; pattern.forEach((value) => { addUniqueValuesToArray(matches, text.matchAll(value)); }); return matches; } else { const matches = text.matchAll(pattern); return addUniqueValuesToArray([], matches); } } function replaceMacros(text, replacements) { let retval = text; replacements.forEach((value, key) => { retval = retval.replaceAll(key, value); }); return retval; } function addUniqueValuesToArray(array, values) { const valueArray = [...values]; valueArray.forEach((match) => { if (!array.includes(match[1])) { array.push(match[1]); } }); return array; } function parseCodeBlocks(content) { const retval = /* @__PURE__ */ new Map(); const codeBlockRegex = /#+\s+(.+)$\n+```([\w-_\s]*)\n(((?!^```\n).|\n)*)\n^```$/gm; const matches = content.matchAll(codeBlockRegex); const matchArray = [...matches]; matchArray.forEach((match) => { if (!retval.has(match[1])) { const code = { content: match[3], lang: match[2] }; retval.set(match[1], code); } }); return retval; } var macroSeparator = "(?:\\s*[:=]\\s*|\\s+)"; var macroValue = "(((?:[^}\\s]*\\w[^}\\s]*)+))"; function constructMacroRegex(macroRegex) { if (macroRegex instanceof RegExp) macroRegex = macroRegex.source; const retval = new RegExp(macroRegex + macroSeparator + macroValue, "gi"); return retval; } function validateDomain(domain, validTld) { let tld = domain.split(".").pop()?.toUpperCase(); if (tld && validTld.includes(tld)) return true; tld = domain.split("[.]").pop()?.toUpperCase(); if (tld && validTld.includes(tld)) return true; return false; } function removeArrayDuplicates(array) { return array.filter((item, index) => { return array.indexOf(item) === index; }); } function convertTime(timeString) { return Date.parse(timeString); } function validateDomains(domains, validTld) { let index = domains.length - 1; while (index >= 0) { const domain = domains[index]; if (!validateDomain(domain, validTld)) { domains.splice(index, 1); } index -= 1; } return domains; } function isLocalIpv4(ip) { const localIpTest = new RegExp(LOCAL_IP_REGEX.source, LOCAL_IP_REGEX.flags); if (localIpTest.exec(ip)) return true; else return false; } function getIocType(val) { val = val.trim().toLowerCase(); const ipTest = new RegExp(IP_REGEX.source, IP_REGEX.flags); if (ipTest.exec(val)) return "ip"; const ipv6Test = new RegExp(IPv6_REGEX.source, IPv6_REGEX.flags); if (ipv6Test.exec(val)) return "ip"; const domainTest = new RegExp(DOMAIN_REGEX.source, DOMAIN_REGEX.flags); if (domainTest.exec(val)) return "domain"; const hashTest = new RegExp(HASH_REGEX.source, HASH_REGEX.flags); if (hashTest.exec(val)) return "hash"; return null; } // src/matcher.ts var PATTERN_KEYS = ["IPv6", "IP", "IPv4", "LocalIP", "Domain", "SHA256", "MD5", "SHA1", "File"]; var Matcher = class _Matcher { static Patterns = { IPv6: IPv6_REGEX.source, IPv4: IP_REGEX.source, IP: IP_REGEX.source, LocalIP: LOCAL_IP_REGEX.source, LocalIPv4: LOCAL_IP_REGEX.source, Domain: DOMAIN_REGEX.source, SHA256: SHA256_REGEX.source, MD5: MD5_REGEX.source, SHA1: SHA1_REGEX.source, File: FILE_REGEX.source, Macro: MACRO_REGEX.source }; static getAvailablePatterns() { return PATTERN_KEYS; } static getExactMatcher(pattern) { return new RegExp(`^${_Matcher.Patterns[pattern]}$`, "i"); } static getGlobalMatcher(pattern) { return new RegExp(_Matcher.Patterns[pattern], "gi"); } static findAll(text, pattern) { return Array.from(text.matchAll(this.getGlobalMatcher(pattern)), (m) => m[0]); } static isMatch(text, pattern) { return this.getExactMatcher(pattern).test(text); } static findFirst(text, pattern) { const match = text.match(_Matcher.Patterns[pattern]); return match ? match[0] : null; } }; function getMatches(fileContent) { if (!fileContent) return []; const ips = { title: "IPs", items: Matcher.findAll(fileContent, "IPv4") }; const domains = { title: "Domains", items: Matcher.findAll(fileContent, "Domain") }; const hashes = { title: "Hashes", items: Matcher.findAll(fileContent, "SHA256") }; const privateIps = { title: "IPs (Private)", items: [] }; const ipv6 = { title: "IPv6", items: Matcher.findAll(fileContent, "IPv6") }; ips.title = "IPs (Public)"; for (let i = 0; i < ips.items.length; i++) { const item = ips.items[i]; if (isLocalIpv4(item)) { ips.items.splice(i, 1); i--; privateIps.items.push(item); } } const retval = [ips, privateIps, domains, hashes, ipv6]; retval.forEach((iocList, index, array) => { const refangedItems = iocList.items.map((x) => refangIoc(x)); iocList.items = [...new Set(refangedItems)]; array[index] = iocList; }); return retval; } function getIndicatorMatches(fileContent, source = "text" /* TEXT */, metadata) { const indicators = []; if (!fileContent) return indicators; const ipv4Addresses = Matcher.findAll(fileContent, "IPv4"); for (const ip of ipv4Addresses) { const refangedIp = refangIoc(ip); if (isLocalIpv4(refangedIp)) { indicators.push({ value: refangedIp, type: "private_ipv4" /* PRIVATE_IPv4 */, source, metadata }); } else { indicators.push({ value: refangedIp, type: "ipv4" /* IPv4 */, source, metadata }); } } const ipv6Addresses = Matcher.findAll(fileContent, "IPv6"); for (const ip of ipv6Addresses) { indicators.push({ value: refangIoc(ip), type: "ipv6" /* IPv6 */, source, metadata }); } const domains = Matcher.findAll(fileContent, "Domain"); for (const domain of domains) { indicators.push({ value: refangIoc(domain), type: "domain" /* DOMAIN */, source, metadata }); } const sha256Hashes = Matcher.findAll(fileContent, "SHA256"); for (const hash of sha256Hashes) { indicators.push({ value: hash, type: "hash" /* HASH */, source, metadata }); } return indicators; } function processExclusions(iocs, plugin) { if (!iocs || !plugin) return iocs; const exclusions = plugin.getIocExclusions(); return iocs.map((indicatorList) => { const processed = { ...indicatorList }; switch (processed.title) { case "IPs": case "IPs (Public)": case "IPs (Private)": processed.exclusions = exclusions.ipv4Exclusions || []; break; case "IPv6": processed.exclusions = exclusions.ipv6Exclusions || []; break; case "Domains": processed.exclusions = exclusions.domainExclusions || []; break; case "Hashes": processed.exclusions = exclusions.hashExclusions || []; break; default: processed.exclusions = []; break; } if (processed.exclusions && processed.exclusions.length > 0) { processed.items = filterExclusions(processed.items, processed.exclusions); } return processed; }); } // src/ocr.ts var import_tesseract = require("tesseract.js"); // src/ocr/utils.ts async function readImageFile(app, file) { const arrBuff = await app.vault.readBinary(file); const buffer = Buffer.from(arrBuff); return buffer; } async function encodeImageFile(app, file) { return (await readImageFile(app, file)).toString("base64"); } // src/ocr.ts var OcrQueue = class { worker; queue; processing; processingLock; constructor(worker) { this.worker = worker; this.queue = []; this.processing = false; this.processingLock = null; } async addToQueue(image) { return new Promise((resolve, reject) => { this.queue.push({ image, resolve, reject }); if (!this.processing) { this.processQueue(); } }); } async processQueue() { if (this.processing) { return; } this.processing = true; while (this.queue.length > 0) { const queueItem = this.queue[0]; if (!queueItem) continue; const { image, resolve, reject } = queueItem; try { const { data: { text } } = await this.worker.recognize(image); resolve(text); this.queue.shift(); } catch (error) { reject(error); this.queue.shift(); } } this.processing = false; } }; async function ocr(app, file, worker) { if (!worker) { console.error("OCR worker is not initialized"); return null; } if (!file) return null; const buffer = await readImageFile(app, file); if (!buffer) return null; const ret = await worker.recognize(buffer); return ret.data.text; } async function initializeWorker() { const worker = await (0, import_tesseract.createWorker)(); await worker.loadLanguage("eng"); await worker.initialize("eng"); return worker; } async function ocrMultiple(app, files, worker) { const resultsMap = /* @__PURE__ */ new Map(); if (!worker) { console.error("No worker to complete OCR."); return null; } if (!files) { console.error("No files passed to OCR function"); return null; } const ocrQueue = new OcrQueue(worker); for (let file of files) { if (typeof file === "string") { resultsMap.set(file, ""); const fileObj = app.vault.getFileByPath(file); if (fileObj) file = fileObj; else { console.error(`couldn't find file ${file}`); continue; } ; } else { resultsMap.set(file.path, ""); } const arrBuff = await readImageFile(app, file); if (!arrBuff) { console.error(`failed to convert ${file.path} to buffer for OCR`); continue; } const buffer = Buffer.from(arrBuff); try { const text = await ocrQueue.addToQueue(buffer); resultsMap.set(file.path, text); } catch (error) { let message = error instanceof Error ? error.message : String(error); console.error(message); resultsMap.set(file.path, message); } } return resultsMap; } // src/ocr/ocrProvider.ts var import_events = require("events"); var OcrProvider = class { emitter; activeJobs = /* @__PURE__ */ new Set(); isDebugging = false; processors; // Shared emitter instance for communication between processors and this provider processorEventEmitter; cacheChecker; constructor(processors, cacheChecker, enableDebug = false) { this.processors = processors; this.isDebugging = enableDebug; this.emitter = new import_events.EventEmitter(); this.processorEventEmitter = new import_events.EventEmitter(); this.cacheChecker = cacheChecker; this.processors.forEach((processor) => { if (typeof processor.setEventEmitter === "function") { processor.setEventEmitter(this.processorEventEmitter); } }); this.setupEventListeners(); } setupEventListeners() { this.processorEventEmitter.on("ocr-progress", (payload) => { this.emitter.emit("ocr-progress", payload); }); this.processorEventEmitter.on("ocr-complete", (payload) => { const jobId = this.getJobId(payload.fileId, payload.processorId); this.activeJobs.delete(jobId); this.debug(`Completed ${jobId}. Active jobs: ${this.activeJobs.size}`); this.emitter.emit("ocr-complete", payload); }); this.processorEventEmitter.on("ocr-error", (payload) => { const jobId = this.getJobId(payload.fileId, payload.processorId); this.activeJobs.delete(jobId); this.debug(`Errored ${jobId}. Active jobs: ${this.activeJobs.size}`); this.emitter.emit("ocr-error", payload); }); } async processAttachments(parentNoteFileId, attachments) { this.debug(`Starting processing for ${attachments.length} attachments for note '${parentNoteFileId}'.`); const allProcessingPromises = []; for (const attachmentJob of attachments) { this.debug(`Processing attachment: ${attachmentJob.fileId}`); for (const processor of this.processors) { const jobId = this.getJobId(attachmentJob.fileId, processor.id); if (this.cacheChecker(attachmentJob.fileId, processor.id)) { this.debug(`Skipping ${jobId} due to cache hit.`); continue; } if (this.activeJobs.has(jobId)) { this.debug(`Skipping ${jobId} - already active/queued.`); continue; } this.activeJobs.add(jobId); this.debug(`Queuing ${jobId}. Active jobs: ${this.activeJobs.size}`); this.processorEventEmitter.emit("ocr-progress", { fileId: attachmentJob.fileId, processorId: processor.id, status: "queued", message: `Queued for ${processor.id}` }); allProcessingPromises.push( processor.processImage(attachmentJob).catch((err) => { console.error(`Error starting ${processor.id} for ${attachmentJob.fileId}:`, err); this.processorEventEmitter.emit("ocr-error", { fileId: attachmentJob.fileId, processorId: processor.id, error: `Failed to initiate processing: ${err.message}`, canRetry: false }); }) ); this.debug(`queued ${allProcessingPromises.length}`); } } if (this.activeJobs.size > 0) { this.debug(`Finished initiating all jobs for note ${parentNoteFileId}. Active jobs: ${this.activeJobs.size}. Waiting for results via events.`); } else { this.debug(`No active jobs for ${parentNoteFileId}.`); } } addProcessor(processor) { this.processors.push(processor); if (typeof processor.setEventEmitter === "function") { processor.setEventEmitter(this.processorEventEmitter); } } setDebugging(enabled) { const changed = this.isDebugging !== enabled; this.isDebugging = enabled; if (changed) { this.debug(`Debugging ${enabled ? "enabled" : "disabled"}`); } } debug(...args) { if (this.isDebugging) { console.log(`[OcrProvider]`, ...args); } } getJobId(fileId, processorId) { return `${fileId}-${processorId}`; } }; // src/ocr/baseProcessor.ts var BaseOcrProcessor = class { id; isDebugging = false; emitter = null; jobQueue = []; activeTasks = 0; maxConcurrency; constructor(id, maxConcurrency = 4, enableDebug = false) { this.id = id; this.maxConcurrency = Math.max(1, maxConcurrency); this.isDebugging = enableDebug; } setEventEmitter(emitter) { this.emitter = emitter; this.debug(`Event emitter set.`); } async processImage(job) { if (!this.emitter) { console.error(`[${this.id}] Cannot queue job for ${job.fileId} - event emitter not configured.`); throw new Error(`[${this.id}] Event emitter not configured.`); } this.debug(`Queuing job for ${job.fileId}`); this.jobQueue.push(job); this.emitProgress(job.fileId, "queued", void 0, `Queued (${this.jobQueue.length} in queue)`); this._tryProcessNext(); } _tryProcessNext() { if (!this.emitter) { console.warn(`[${this.id}] Cannot process queue, emitter not set.`); return; } while (this.activeTasks < this.maxConcurrency && this.jobQueue.length > 0) { const job = this.jobQueue.shift(); if (!job) continue; this.activeTasks++; this.debug(`Starting task for ${job.fileId}. Active: ${this.activeTasks}/${this.maxConcurrency}`); this.emitProgress(job.fileId, "processing", 0, "Starting OCR task"); (async () => { try { const extractedText = await this._performOcr(job); this.debug(`Successfully processed ${job.fileId}`); this.emitter?.emit("ocr-complete", { fileId: job.fileId, processorId: this.id, extractedText: extractedText ?? "" }); } catch (error) { console.error(`[${this.id}] Error processing ${job.fileId}`, error); this.emitter?.emit("ocr-error", { fileId: job.fileId, processorId: this.id, error: error.message || "OCR processing failed", canRetry: false }); } finally { this.activeTasks--; this.debug(`Finished task for ${job.fileId}. Active: ${this.activeTasks}/${this.maxConcurrency}`); this._tryProcessNext(); } })(); } } emitProgress(fileId, status, progressPercent, message) { if (this.emitter) { this.emitter.emit("ocr-progress", { fileId, processorId: this.id, status, progressPercent, message }); } else { console.warn(`[${this.id}] Cannot emit progress for ${fileId}, emitter not initialized.`); } } setDebugging(enabled) { const changed = this.isDebugging !== enabled; this.isDebugging = enabled; if (changed) { this.debug(`Debugging ${enabled ? "enabled" : "disabled"}.`); } } debug(...args) { if (this.isDebugging) { console.log(`[${this.id}]`, ...args); } } async shutdown() { this.debug(`Base shutdown called.`); } }; // src/ocr/tesseractProcessor.ts var TesseractOcrProcessor = class extends BaseOcrProcessor { worker; constructor(worker, enableDebug = false) { super("tesseract", 1, enableDebug); this.worker = worker; this.debug(`[${this.id}] Processor initialized (waiting for emitter).`); } async _performOcr(job) { const { fileId, imageData } = job; this.debug(`[${this.id}] Worker starting recognize for ${fileId}`); const result = await this.worker.recognize(imageData); return result.data.text; } async shutdown() { await super.shutdown(); if (this.worker) { this.debug(`[${this.id}] Terminating tesseract worker.`); await this.worker.terminate(); } } }; // src/cyberPlugin.ts var CyberPlugin = class extends import_obsidian3.Plugin { isDebugging = false; settings; validTld; sidebarContainers; emitter; indicators; ocrIndicators = null; ocrCache = /* @__PURE__ */ new Map(); exclusions; fileOpenRef = null; fileModifyRef = null; vaultCacheRef = null; activeFile = null; activeFileContent = null; activeFileAttachments = null; ocrProvider = null; ocrProcessingEmitter; uiNotifier; ocrProgressRef; ocrCompleteRef; ocrErrorRef; worker = null; // private fileProgressRef: () => void; // private fileCompleteRef: () => void; constructor(app, manifest, enableDebug = false) { super(app, manifest); this.isDebugging = enableDebug; this.emitter = new import_events2.EventEmitter(); this.indicators = []; this.exclusions = { ipv4Exclusions: [], ipv6Exclusions: [], hashExclusions: [], domainExclusions: [] }; this.uiNotifier = new import_events2.EventEmitter(); this.ocrProcessingEmitter = new import_events2.EventEmitter(); const cacheChecker = this.hasCachedOcrResult.bind(this); this.ocrProvider = new OcrProvider([], cacheChecker); this.registerObsidianListeners(); } async onload() { if (!this.ocrProvider) { await this.initializeOcrSystem(); } this.worker = await initializeWorker(); this.ocrProvider?.addProcessor(new TesseractOcrProcessor(this.worker)); this.setupOcrEventListeners(); } async refreshIndicators() { if (this.activeFile && this.activeFileContent) { this.indicators = getIndicatorMatches(this.activeFileContent, "text" /* TEXT */); this.debug(`Refreshed ${this.indicators.length} indicators from Markdown text.`); } else { this.indicators = []; } const collectedOcrIndicators = []; if (this.activeFileAttachments && this.activeFileAttachments.length > 0) { this.debug(`Checking OCR cache for ${this.activeFileAttachments.length} attachments.`); for (const att of this.activeFileAttachments) { const fileCache = this.ocrCache.get(att.path); if (fileCache) { for (const ind of fileCache.values()) { collectedOcrIndicators.push(...ind); } this.debug(`Found cached OCR indicators for ${att.path}`); } else { this.debug(`No cached OCR indicators for ${att.path}`); } } } this.ocrIndicators = collectedOcrIndicators; this.emitter.emit("indicators-changed"); } getFileContent() { return this.activeFileContent; } getFileAttachments() { return this.activeFileAttachments ?? []; } getIocExclusions() { return this.exclusions; } /** * Compare attachments for the current file against the plugin's attachment list. * @param file the file to evaluate * @returns true if attachments are unchanged, false if attachments have changed */ compareAttachments(file) { const currentAttachments = getAttachments(file.path, this.app); const existingAttachments = this.activeFileAttachments ?? []; const currentPaths = new Set(currentAttachments.map((f) => f.path)); const existingPaths = new Set(existingAttachments.map((f) => f.path)); const unchanged = currentPaths.size === existingPaths.size && [...currentPaths].every((path) => existingPaths.has(path)); if (!unchanged) { this.debug(`Attachments changed for ${file.path}. New count: ${currentPaths.size}, Old count: ${existingPaths.size}`); this.activeFileAttachments = currentAttachments; this.emitter.emit("attachments-changed"); } return unchanged; } registerObsidianListeners() { this.fileOpenRef = this.app.workspace.on("file-open", this.handleFileOpen.bind(this)); this.fileModifyRef = this.app.vault.on("modify", this.handleFileModify.bind(this)); this.vaultCacheRef = this.app.metadataCache.on("resolve", this.handleMetadataResolve.bind(this)); } async handleFileOpen(file) { if (file && file instanceof import_obsidian3.TFile) { this.activeFile = file; this.activeFileContent = await this.app.vault.cachedRead(file); this.compareAttachments(file); this.emitter.emit("file-opened"); await this.refreshIndicators(); this.triggerOcrProcessing(); } else { this.activeFile = null; this.activeFileContent = null; this.activeFileAttachments = null; this.indicators = []; this.ocrIndicators = []; await this.refreshIndicators(); this.debug("Active file closed or is not a TFile, indicators cleared."); } } async handleFileModify(file) { if (file === this.activeFile && file instanceof import_obsidian3.TFile) { this.activeFileContent = await this.app.vault.cachedRead(file); const attachmentsChanged = this.compareAttachments(file); this.emitter.emit("file-modified"); await this.refreshIndicators(); if (attachmentsChanged) { this.debug("Attachments changed during file modify, re-triggering OCR processing."); this.triggerOcrProcessing(); } } } async handleMetadataResolve(file) { if (file === this.activeFile) { const attachmentsChanged = this.compareAttachments(file); await this.refreshIndicators(); if (attachmentsChanged) { this.debug("Attachments changed during metadata resolve, re-triggering OCR processing."); this.triggerOcrProcessing(); } } } async triggerOcrProcessing() { if (!this.ocrProvider || !this.activeFileAttachments || this.activeFileAttachments.length === 0) { return; } this.debug(`Triggering OCR for ${this.activeFileAttachments.length} new attachment(s)...`); const ocrJobs = []; for (const att of this.activeFileAttachments) { try { const content = await readImageFile(this.app, att); ocrJobs.push({ fileId: att.path, imageData: content }); this.debug(`Added Job for ${att.path}`); } catch (error) { console.error(`Failed to read or encode attachment ${att.path}`, error); this.handleOcrError({ fileId: att.path, processorId: "plugin", error: `Failed to read file: ${error}`, canRetry: false }); } } if (ocrJobs.length > 0) { this.ocrProvider.processAttachments(this.activeFile?.path ?? "unknown", ocrJobs).catch((error) => { console.error(`Error occurred during OCR Provider processAttachments call:`, error); }); } } /** * Add search sites to a set of ParsedIndicators. */ applySearchSites(indicators) { if (!this.settings?.searchSites) return indicators; return indicators.map((indicator) => { const indicatorCopy = { ...indicator }; switch (indicator.title) { case "IPs (Public)": case "IPs (Private)": case "IPv6": indicatorCopy.sites = this.settings?.searchSites.filter( (x) => x.enabled && x.ip ); break; case "Domains": indicatorCopy.sites = this.settings?.searchSites.filter( (x) => x.enabled && x.domain ); break; case "Hashes": indicatorCopy.sites = this.settings?.searchSites.filter( (x) => x.enabled && x.hash ); default: indicatorCopy.sites = []; } return indicatorCopy; }); } /** * Validate that domains end with a valid TLD */ validateDomains(indicators) { if (!this.validTld) return indicators; return indicators.map((indicator) => { const indicatorCopy = { ...indicator }; if (this.validTld && indicator.title === "Domain" && indicator.items.length > 0) { indicatorCopy.items = validateDomains(indicator.items, this.validTld); } return indicatorCopy; }); } processExclusions(indicators) { return indicators.map((iocs) => { const processed = { ...iocs }; switch (processed.title) { case "IPs": case "IPs (Public)": case "IPs (Private)": processed.exclusions = this.exclusions.ipv4Exclusions; break; case "IPv6": processed.exclusions = this.exclusions.ipv6Exclusions; break; case "Domains": processed.exclusions = this.exclusions.domainExclusions; break; case "Hashes": processed.exclusions = this.exclusions.hashExclusions; break; default: processed.exclusions = []; break; } if (processed.exclusions.length > 0) { processed.items = filterExclusions(processed.items, processed.exclusions); } return processed; }); } async initializeOcrSystem() { const processors = []; if (processors.length === 0) { console.warn("No OCR processors were successfully initialized."); return; } const cacheChecker = this.hasCachedOcrResult.bind(this); this.ocrProvider = new OcrProvider(processors, cacheChecker); this.debug(`OCR Provider initialized with ${processors.length} processors`); } hasCachedOcrResult(fileId, processorId) { const hasResult = this.ocrCache.get(fileId)?.has(processorId) ?? false; if (hasResult) { this.debug(`[Cache Check] Cache hit for ${fileId} using ${processorId}.`); } return hasResult; } setupOcrEventListeners() { if (!this.ocrProvider) return; this.ocrCompleteRef = this.handleOcrComplete.bind(this); this.ocrErrorRef = this.handleOcrError.bind(this); this.ocrProgressRef = this.handleOcrProgress.bind(this); this.ocrProvider.emitter.on("ocr-complete", this.ocrCompleteRef); this.ocrProvider.emitter.on("ocr-error", this.ocrErrorRef); this.ocrProvider.emitter.on("ocr-progress", this.ocrProgressRef); } async handleOcrComplete(payload) { this.debug(`OCR Complete: ${payload.fileId} by ${payload.processorId}`); const isRelevantAttachment = this.activeFileAttachments?.some((att) => att.path === payload.fileId); if (!isRelevantAttachment) { this.debug(`Received OCR result for ${payload.fileId} which is not an attachment of the active file`); } const indicators = getIndicatorMatches(payload.extractedText, "ocr" /* OCR */, { "processor": payload.processorId }); this.debug(`Extracted ${indicators.length} indicators via OCR from ${payload.fileId}.`); if (!this.ocrCache.has(payload.fileId)) { this.ocrCache.set(payload.fileId, /* @__PURE__ */ new Map()); } const fileCache = this.ocrCache.get(payload.fileId); if (fileCache) { fileCache.set(payload.processorId, indicators); this.debug(`Cached OCR results from ${payload.processorId} for ${payload.fileId}`); } if (isRelevantAttachment) { this.debug(`OCR result is for a relevant attachment (${payload.fileId}), refreshing indicators.`); await this.refreshIndicators(); } } handleOcrError(payload) { console.error(`OCR error: ${payload.fileId} by ${payload.processorId}:`, payload.error); } handleOcrProgress(payload) { this.debug(`OCR Progress: ${payload.fileId} by ${payload.processorId}: ${payload.status} ${payload.progressPercent ?? ""}% ${payload.message ?? ""}`); } /** * Activate a view of the given type in the right sidebar. * @param type a view type */ async activateView(type) { const { workspace } = this.app; let leaf = await workspace.ensureSideLeaf(type, "right", { active: true }); this.sidebarContainers?.set(type, leaf); } on(event, callback) { this.emitter.on(event, callback); return () => this.emitter.off(event, callback); } async saveSettings() { await this.saveData(this.settings); this.emitter.emit("settings-change"); } async onunload() { if (this.fileOpenRef) this.app.workspace.offref(this.fileOpenRef); if (this.fileModifyRef) this.app.vault.offref(this.fileModifyRef); if (this.vaultCacheRef) this.app.metadataCache.offref(this.vaultCacheRef); this.worker?.terminate(); } setDebugging(enabled) { const changed = this.isDebugging !== enabled; this.isDebugging = enabled; if (changed) { this.debug(`Debugging ${enabled ? "enabled" : "disabled"}.`); } } debug(...args) { if (this.isDebugging) { console.log(`[${this.manifest.name}]`, ...args); } } }; // src/domUtils.ts var import_obsidian4 = require("obsidian"); function removeElements(els) { if (els && els.length > 0) { Array.from(els).forEach((el, index, array) => { try { el.parentNode?.removeChild(el); } catch { } }); } } function openDetails(els) { if (els && els.length > 0) { Array.from(els).forEach((el) => { try { el.open = true; } catch { } }); } } function datePickerSettingEl(parentEl, value, name) { value = value || new Date(Date.now()).toISOString(); name = name || "Date Picker"; const fromDate = new import_obsidian4.Setting(parentEl).setName(name).settingEl; const fromDateEl = document.createElement("input"); fromDateEl.setAttribute("type", "datetime-local"); fromDateEl.setAttribute("value", value); fromDate.appendChild(fromDateEl); return fromDateEl; } // src/editorUtils.ts var import_obsidian5 = require("obsidian"); function transformSelectedText(editor, func) { const selection = editor.getSelection(); const transformed = func(selection); editor.replaceSelection(transformed); return transformed; } function appendToEnd(app, file, text) { if (!app) return; const vault = app.vault; if (!vault || !file) return; vault.append(file, ` ${text}`); const view = app.workspace.getActiveViewOfType(import_obsidian5.MarkdownView); if (!view) return; const editor = view.editor; if (!editor) return; let lastLine = editor.lastLine(); if (!lastLine) return; lastLine = lastLine - 1; const lastLineLen = editor.getLine(lastLine).length; if (!lastLineLen) return; const lastLinePos = { line: lastLine, ch: lastLineLen }; editor.setCursor(lastLinePos); editor.scrollIntoView({ from: lastLinePos, to: lastLinePos }, true); editor.focus(); } // src/modal.ts var import_obsidian7 = require("obsidian"); // src/workspaceUtils.ts var import_obsidian6 = require("obsidian"); function addButtonContainer(workspace, file, className, rootFolder) { const view = workspace.getActiveViewOfType(import_obsidian6.MarkdownView); if (!view) return; const container = view.containerEl; if (!container) return; const els = container.getElementsByClassName(className); if (els && els.length > 0) { Array.from(els).forEach((el, index, array) => { container.removeChild(el); }); } if (rootFolder && !file.path.includes(rootFolder)) { return; } const header = container.querySelector(".view-header"); if (!header) return; const newDiv = document.createElement("div"); newDiv.className = className; container.insertAfter(newDiv, header); return newDiv; } function addButtonToContainer(el, buttonText) { const button = new import_obsidian6.ButtonComponent(el).setButtonText(buttonText); return button; } async function getActiveNoteContent(app) { const file = app.workspace.getActiveFile(); if (!file) return null; return await app.vault.cachedRead(file); } // src/modal.ts var userMacro = { name: "User", macroRegex: [constructMacroRegex(/user\.?(?:\s*named?)?/)], valueRegex: /user(name)?/gi }; var hostNameMacro = { name: "Hostname", macroRegex: [constructMacroRegex(/(?:host|computer|comp)\.?\s*(?:named?)?/)], valueRegex: /(host|computer|comp)(name)?/gi }; var hashMacro = { name: "Hash", macroRegex: [constructMacroRegex(/(?:hash|sha\s*256|sha)/)], valueRegex: /(hash|sha256|sha)/gi }; var fileMacro = { name: "Filename", macroRegex: [FILE_REGEX, constructMacroRegex(/(?:(?:file\s*(?:path)?|path|attachments?)\.?\s*(?:name)?)/)], valueRegex: /(file(path)?|path)(name)?/gi }; var supportedMacros = [userMacro, hostNameMacro, hashMacro, fileMacro]; var ScriptObject = class { code; dated; baseTimestamp; fromTime; toTime; macros; replacedCode; constructor(code, dated) { this.code = code;