From 8074cbc1442fdfad9e33749c4657a6d20d1f89c6 Mon Sep 17 00:00:00 2001 From: devs6186 Date: Fri, 20 Feb 2026 00:47:00 +0530 Subject: [PATCH 1/5] webui: show error when JSON does not follow expected schema Validate result document has required fields (meta, meta.version, meta.analysis, meta.analysis.layout, rules) after parse. Show user-friendly error; for URL loads suggest reanalyzing (e.g. VT). Fixes #2363 --- CHANGELOG.md | 2 + web/explorer/src/composables/useRdocLoader.js | 48 +++++++++++++++++-- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3169082671..b84cf251b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,8 @@ ### capa Explorer Web - webui: fix 404 for "View rule in capa-rules" by using encodeURIComponent for rule name in URL @devs6186 #2482 +- webui: show error when JSON does not follow expected result document schema; suggest reanalyzing for VT URLs @devs6186 #2363 + ### capa Explorer IDA Pro plugin ### Development diff --git a/web/explorer/src/composables/useRdocLoader.js b/web/explorer/src/composables/useRdocLoader.js index 8253183071..b8eef51971 100644 --- a/web/explorer/src/composables/useRdocLoader.js +++ b/web/explorer/src/composables/useRdocLoader.js @@ -32,6 +32,33 @@ export function useRdocLoader() { toast.add({ severity, summary, detail, life: 3000, group: "bc" }); // bc: bottom-center }; + /** + * Validates that the parsed object has the expected result document schema. + * @param {Object} rdoc - The parsed JSON data. + * @returns {{ valid: boolean, message?: string }} Validation result with an optional error message. + */ + const validateRdocSchema = (rdoc) => { + if (!rdoc || typeof rdoc !== "object") { + return { valid: false, message: "Invalid JSON: expected an object." }; + } + if (!rdoc.meta || typeof rdoc.meta !== "object") { + return { valid: false, message: "Invalid result document: missing or invalid 'meta' field." }; + } + if (rdoc.meta.version === undefined) { + return { valid: false, message: "Invalid result document: missing 'meta.version'." }; + } + if (!rdoc.meta.analysis || typeof rdoc.meta.analysis !== "object") { + return { valid: false, message: "Invalid result document: missing or invalid 'meta.analysis'." }; + } + if (!rdoc.meta.analysis.layout || typeof rdoc.meta.analysis.layout !== "object") { + return { valid: false, message: "Invalid result document: missing or invalid 'meta.analysis.layout'." }; + } + if (!rdoc.rules || typeof rdoc.rules !== "object") { + return { valid: false, message: "Invalid result document: missing or invalid 'rules' field." }; + } + return { valid: true }; + }; + /** * Checks if the version of the loaded data is supported. * @param {Object} rdoc - The loaded JSON data containing version information. @@ -81,27 +108,40 @@ export function useRdocLoader() { * @returns {Promise} A promise that resolves to the processed RDOC data, or null if processing fails. */ const loadRdoc = async (source) => { + const isUrl = typeof source === "string"; try { let data; - if (typeof source === "string") { - // Load from URL + if (isUrl) { const blob = await fetchFromUrl(source); data = await processBlob(blob); } else if (source instanceof File) { - // Load from local data = await processBlob(source); } else { throw new Error("Invalid source type"); } + const validation = validateRdocSchema(data); + if (!validation.valid) { + let detail = validation.message; + if (isUrl) { + detail += " If this is a VirusTotal or similar link, the file may need to be reanalyzed. Try again later."; + } + showToast("error", "Invalid result document", detail); + return null; + } + if (checkVersion(data)) { showToast("success", "Success", "JSON data loaded successfully"); return data; } } catch (error) { console.error("Error loading JSON:", error); - showToast("error", "Failed to process the file", error.message); + let detail = error.message; + if (isUrl && (error instanceof SyntaxError || error.message.includes("JSON"))) { + detail += " If this is a VirusTotal or similar link, the file may need to be reanalyzed. Try again later."; + } + showToast("error", "Failed to process the file", detail); } return null; }; From bfc235589b0ef00f996db3998b76bf9ce48cee69 Mon Sep 17 00:00:00 2001 From: devs6186 Date: Fri, 20 Feb 2026 02:07:07 +0530 Subject: [PATCH 2/5] webui: fix array validation bug and deduplicate VT suggestion string - introduce isInvalidObject() helper (checks !v || typeof !== "object" || Array.isArray) so that arrays are correctly rejected in schema validation - extract VT_REANALYZE_SUGGESTION constant to eliminate the duplicated string in loadRdoc() Addresses review feedback on #2871 --- web/explorer/src/composables/useRdocLoader.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/web/explorer/src/composables/useRdocLoader.js b/web/explorer/src/composables/useRdocLoader.js index b8eef51971..2768036752 100644 --- a/web/explorer/src/composables/useRdocLoader.js +++ b/web/explorer/src/composables/useRdocLoader.js @@ -38,22 +38,24 @@ export function useRdocLoader() { * @returns {{ valid: boolean, message?: string }} Validation result with an optional error message. */ const validateRdocSchema = (rdoc) => { - if (!rdoc || typeof rdoc !== "object") { + const isInvalidObject = (v) => !v || typeof v !== "object" || Array.isArray(v); + + if (isInvalidObject(rdoc)) { return { valid: false, message: "Invalid JSON: expected an object." }; } - if (!rdoc.meta || typeof rdoc.meta !== "object") { + if (isInvalidObject(rdoc.meta)) { return { valid: false, message: "Invalid result document: missing or invalid 'meta' field." }; } if (rdoc.meta.version === undefined) { return { valid: false, message: "Invalid result document: missing 'meta.version'." }; } - if (!rdoc.meta.analysis || typeof rdoc.meta.analysis !== "object") { + if (isInvalidObject(rdoc.meta.analysis)) { return { valid: false, message: "Invalid result document: missing or invalid 'meta.analysis'." }; } - if (!rdoc.meta.analysis.layout || typeof rdoc.meta.analysis.layout !== "object") { + if (isInvalidObject(rdoc.meta.analysis.layout)) { return { valid: false, message: "Invalid result document: missing or invalid 'meta.analysis.layout'." }; } - if (!rdoc.rules || typeof rdoc.rules !== "object") { + if (isInvalidObject(rdoc.rules)) { return { valid: false, message: "Invalid result document: missing or invalid 'rules' field." }; } return { valid: true }; @@ -109,6 +111,9 @@ export function useRdocLoader() { */ const loadRdoc = async (source) => { const isUrl = typeof source === "string"; + const VT_REANALYZE_SUGGESTION = + " If this is a VirusTotal or similar link, the file may need to be reanalyzed. Try again later."; + try { let data; @@ -125,7 +130,7 @@ export function useRdocLoader() { if (!validation.valid) { let detail = validation.message; if (isUrl) { - detail += " If this is a VirusTotal or similar link, the file may need to be reanalyzed. Try again later."; + detail += VT_REANALYZE_SUGGESTION; } showToast("error", "Invalid result document", detail); return null; @@ -139,7 +144,7 @@ export function useRdocLoader() { console.error("Error loading JSON:", error); let detail = error.message; if (isUrl && (error instanceof SyntaxError || error.message.includes("JSON"))) { - detail += " If this is a VirusTotal or similar link, the file may need to be reanalyzed. Try again later."; + detail += VT_REANALYZE_SUGGESTION; } showToast("error", "Failed to process the file", detail); } From 9591c4ed51b1c14ac94e5085cf3195cd61b83207 Mon Sep 17 00:00:00 2001 From: devs6186 Date: Fri, 20 Feb 2026 05:02:07 +0530 Subject: [PATCH 3/5] webui: address review - validate feature_counts, hoist VT_REANALYZE_SUGGESTION - Add validation for meta.analysis.feature_counts in validateRdocSchema() so parseFunctionCapabilities and other consumers do not hit missing/invalid feature_counts at runtime. - Require feature_counts to have either 'functions' or 'processes' array (static vs dynamic result documents). - Move VT_REANALYZE_SUGGESTION to module top level to avoid redefining on every loadRdoc call. --- web/explorer/src/composables/useRdocLoader.js | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/web/explorer/src/composables/useRdocLoader.js b/web/explorer/src/composables/useRdocLoader.js index 2768036752..fbfdc1c5b4 100644 --- a/web/explorer/src/composables/useRdocLoader.js +++ b/web/explorer/src/composables/useRdocLoader.js @@ -17,6 +17,9 @@ import { useToast } from "primevue/usetoast"; import { isGzipped, decompressGzip, readFileAsText } from "@/utils/fileUtils"; +const VT_REANALYZE_SUGGESTION = + " If this is a VirusTotal or similar link, the file may need to be reanalyzed. Try again later."; + export function useRdocLoader() { const toast = useToast(); const MIN_SUPPORTED_VERSION = "7.0.0"; @@ -55,6 +58,22 @@ export function useRdocLoader() { if (isInvalidObject(rdoc.meta.analysis.layout)) { return { valid: false, message: "Invalid result document: missing or invalid 'meta.analysis.layout'." }; } + if (isInvalidObject(rdoc.meta.analysis.feature_counts)) { + return { + valid: false, + message: "Invalid result document: missing or invalid 'meta.analysis.feature_counts'." + }; + } + const fc = rdoc.meta.analysis.feature_counts; + const hasFunctions = Array.isArray(fc.functions); + const hasProcesses = Array.isArray(fc.processes); + if (!hasFunctions && !hasProcesses) { + return { + valid: false, + message: + "Invalid result document: 'meta.analysis.feature_counts' must contain 'functions' or 'processes' array." + }; + } if (isInvalidObject(rdoc.rules)) { return { valid: false, message: "Invalid result document: missing or invalid 'rules' field." }; } @@ -111,8 +130,6 @@ export function useRdocLoader() { */ const loadRdoc = async (source) => { const isUrl = typeof source === "string"; - const VT_REANALYZE_SUGGESTION = - " If this is a VirusTotal or similar link, the file may need to be reanalyzed. Try again later."; try { let data; From b1ff199861e24888057fea4da4fef033470fac06 Mon Sep 17 00:00:00 2001 From: devs6186 Date: Tue, 24 Feb 2026 08:44:02 +0530 Subject: [PATCH 4/5] webui: allow file-scoped-only result documents in schema validation - Validation: allow feature_counts without functions/processes arrays; if present they must be arrays. - rdocParser: default feature_counts.functions to [] when missing so file-scoped-only docs do not throw. --- web/explorer/src/composables/useRdocLoader.js | 15 +++++++++++---- web/explorer/src/utils/rdocParser.js | 4 ++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/web/explorer/src/composables/useRdocLoader.js b/web/explorer/src/composables/useRdocLoader.js index fbfdc1c5b4..540becbf09 100644 --- a/web/explorer/src/composables/useRdocLoader.js +++ b/web/explorer/src/composables/useRdocLoader.js @@ -65,13 +65,20 @@ export function useRdocLoader() { }; } const fc = rdoc.meta.analysis.feature_counts; - const hasFunctions = Array.isArray(fc.functions); - const hasProcesses = Array.isArray(fc.processes); - if (!hasFunctions && !hasProcesses) { + // Allow file-scoped-only documents (no functions/processes arrays). + // If present, functions and processes must be arrays. + if (fc.functions !== undefined && !Array.isArray(fc.functions)) { return { valid: false, message: - "Invalid result document: 'meta.analysis.feature_counts' must contain 'functions' or 'processes' array." + "Invalid result document: 'meta.analysis.feature_counts.functions' must be an array when present." + }; + } + if (fc.processes !== undefined && !Array.isArray(fc.processes)) { + return { + valid: false, + message: + "Invalid result document: 'meta.analysis.feature_counts.processes' must be an array when present." }; } if (isInvalidObject(rdoc.rules)) { diff --git a/web/explorer/src/utils/rdocParser.js b/web/explorer/src/utils/rdocParser.js index 4a43634488..ad313becfb 100644 --- a/web/explorer/src/utils/rdocParser.js +++ b/web/explorer/src/utils/rdocParser.js @@ -322,8 +322,8 @@ export function parseFunctionCapabilities(doc) { }); } - // Iterate through all functions in the document - for (const f of doc.meta.analysis.feature_counts.functions) { + // Iterate through all functions in the document (empty for file-scoped-only) + for (const f of doc.meta.analysis.feature_counts.functions ?? []) { const addr = formatAddress(f.address); const matches = matchesByFunction.get(addr); // Skip functions with no matches (unlikely) From 42f1574e143dc5035caf95545edb9f05287d18ff Mon Sep 17 00:00:00 2001 From: devs6186 Date: Tue, 24 Feb 2026 09:08:05 +0530 Subject: [PATCH 5/5] webui: remove leading space from VT_REANALYZE_SUGGESTION constant Per review feedback: the concatenation at call sites handles spacing, so the constant should not carry a leading space. --- web/explorer/src/composables/useRdocLoader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/explorer/src/composables/useRdocLoader.js b/web/explorer/src/composables/useRdocLoader.js index 540becbf09..a02480958b 100644 --- a/web/explorer/src/composables/useRdocLoader.js +++ b/web/explorer/src/composables/useRdocLoader.js @@ -18,7 +18,7 @@ import { useToast } from "primevue/usetoast"; import { isGzipped, decompressGzip, readFileAsText } from "@/utils/fileUtils"; const VT_REANALYZE_SUGGESTION = - " If this is a VirusTotal or similar link, the file may need to be reanalyzed. Try again later."; + "If this is a VirusTotal or similar link, the file may need to be reanalyzed. Try again later."; export function useRdocLoader() { const toast = useToast();