Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
77 changes: 73 additions & 4 deletions web/explorer/src/composables/useRdocLoader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -32,6 +35,58 @@ 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) => {
const isInvalidObject = (v) => !v || typeof v !== "object" || Array.isArray(v);

if (isInvalidObject(rdoc)) {
return { valid: false, message: "Invalid JSON: expected an 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 (isInvalidObject(rdoc.meta.analysis)) {
return { valid: false, message: "Invalid result document: missing or invalid 'meta.analysis'." };
}
if (isInvalidObject(rdoc.meta.analysis.layout)) {
return { valid: false, message: "Invalid result document: missing or invalid 'meta.analysis.layout'." };
}
Comment thread
mike-hunhoff marked this conversation as resolved.
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;
// 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.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)) {
return { valid: false, message: "Invalid result document: missing or invalid 'rules' field." };
}
return { valid: true };
};
Comment thread
mike-hunhoff marked this conversation as resolved.

/**
* Checks if the version of the loaded data is supported.
* @param {Object} rdoc - The loaded JSON data containing version information.
Expand Down Expand Up @@ -81,27 +136,41 @@ export function useRdocLoader() {
* @returns {Promise<Object|null>} 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 += VT_REANALYZE_SUGGESTION;
}
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 += VT_REANALYZE_SUGGESTION;
}
showToast("error", "Failed to process the file", detail);
}
return null;
};
Comment thread
mike-hunhoff marked this conversation as resolved.
Expand Down
4 changes: 2 additions & 2 deletions web/explorer/src/utils/rdocParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading