19package org.sleuthkit.autopsy.mcp;
21import com.fasterxml.jackson.databind.JsonNode;
22import org.joda.time.DateTimeZone;
23import org.joda.time.Interval;
24import org.sleuthkit.datamodel.*;
26import java.nio.ByteBuffer;
27import java.nio.CharBuffer;
28import java.nio.charset.CharsetDecoder;
29import java.nio.charset.CodingErrorAction;
30import java.nio.charset.StandardCharsets;
31import java.time.Instant;
32import java.time.ZoneOffset;
33import java.time.format.DateTimeFormatter;
34import java.util.ArrayList;
35import java.util.Collections;
36import java.util.LinkedHashMap;
45class TskQueryService {
47 private final SleuthkitCase skCase;
48 private final String caseName;
50 TskQueryService(SleuthkitCase skCase, String caseName) {
52 this.caseName = caseName;
55 String getCaseName() {
66 List<Map<String, Object>> listTools() {
68 tool(
"get_server_status",
69 "Returns the status of the Autopsy MCP server and whether a case is currently open. " +
70 "Call this tool first: it confirms that the Autopsy MCP server is running and tells " +
71 "you whether a case is open. If caseOpen is false, no other tools will work until the " +
72 "examiner opens a case in Autopsy. If caseOpen is true, caseName contains the name of " +
73 "the open case and all other tools are available.",
76 toolWithNote(
"get_case_summary",
77 "Get a summary of the currently open case including name, " +
78 "data sources, file count, and artifact count. Call this first " +
79 "for any general question about the case. Returns a message " +
80 "instead of an error if no case is currently open.",
83 toolWithNote(
"query_files",
84 "Search for files in the current case. All parameters optional. " +
85 "Returns full file metadata matching Autopsy's UI columns: name, path, " +
86 "timestamps (modified/changed/accessed/created), size, flags, known status, " +
87 "MIME type, extension, hashes (MD5/SHA-256/SHA-1), and file attributes. " +
88 "Limit defaults to 50.",
90 Map.entry(
"nameContains", param(
"string",
"Filter by filename substring (case-insensitive)")),
91 Map.entry(
"extension", param(
"string",
"Filter by file extension without dot, e.g. exe, jpg, pdf (case-insensitive)")),
92 Map.entry(
"mimeType", param(
"string",
"Filter by MIME type e.g. image/jpeg or image/*")),
93 Map.entry(
"minSize", param(
"integer",
"Minimum file size in bytes")),
94 Map.entry(
"maxSize", param(
"integer",
"Maximum file size in bytes")),
95 Map.entry(
"modifiedAfter", param(
"string",
"ISO 8601 date — files with mtime after this date")),
96 Map.entry(
"modifiedBefore", param(
"string",
"ISO 8601 date — files with mtime before this date")),
97 Map.entry(
"createdAfter", param(
"string",
"ISO 8601 date — files with crtime (birth time) after this date")),
98 Map.entry(
"createdBefore", param(
"string",
"ISO 8601 date — files with crtime (birth time) before this date")),
99 Map.entry(
"pathContains", param(
"string",
"Filter by parent path substring (case-insensitive)")),
100 Map.entry(
"isDirectory", param(
"boolean",
"true to return only directories, false for regular files only")),
101 Map.entry(
"allocated", param(
"boolean",
"true for allocated files only, false for unallocated only")),
102 Map.entry(
"knownState", param(
"string",
"Filter by known status: UNKNOWN, KNOWN, or NOTABLE")),
103 Map.entry(
"md5", param(
"string",
"Filter by exact MD5 hash (hex string)")),
104 Map.entry(
"sha256", param(
"string",
"Filter by exact SHA-256 hash (hex string)")),
105 Map.entry(
"limit", param(
"integer",
"Max results, default 50, max 500")),
106 Map.entry(
"orderBy", param(
"string",
"Sort order: size_desc, size_asc, name_asc, name_desc, modified_desc, modified_asc, created_desc, created_asc"))
109 toolWithNote(
"query_data_artifacts",
110 "Search for data artifacts in the current case. Data artifacts represent facts " +
111 "extracted from the data — browser history, messages, contacts, installed programs, " +
112 "GPS locations, etc. Use artifactType to filter by type. " +
113 "Available types: " +
114 "TSK_ACCOUNT (credit card or communications account; attr: TSK_ACCOUNT_TYPE, TSK_ID, TSK_CARD_NUMBER), " +
115 "TSK_ASSOCIATED_OBJECT (back-link to referencing artifact; attr: TSK_ASSOCIATED_ARTIFACT), " +
116 "TSK_BACKUP_EVENT (system/app/file backup; attr: TSK_DATETIME_START, TSK_DATETIME_END), " +
117 "TSK_BLUETOOTH_ADAPTER (BT adapter; attr: TSK_MAC_ADDRESS, TSK_NAME, TSK_DATETIME, TSK_DEVICE_ID), " +
118 "TSK_BLUETOOTH_PAIRING (BT pairing; attr: TSK_DEVICE_NAME, TSK_DATETIME, TSK_MAC_ADDRESS, TSK_DEVICE_ID, TSK_DATETIME_ACCESSED), " +
119 "TSK_CALENDAR_ENTRY (calendar event; attr: TSK_CALENDAR_ENTRY_TYPE, TSK_DATETIME_START, TSK_DESCRIPTION, TSK_LOCATION, TSK_DATETIME_END), " +
120 "TSK_CALLLOG (call record; attr: TSK_PHONE_NUMBER, TSK_PHONE_NUMBER_FROM, TSK_PHONE_NUMBER_TO, TSK_DATETIME_START, TSK_DATETIME_END, TSK_DIRECTION, TSK_NAME), " +
121 "TSK_CLIPBOARD_CONTENT (clipboard data; attr: TSK_TEXT), " +
122 "TSK_CONTACT (contact book entry; attr: TSK_NAME, TSK_EMAIL, TSK_PHONE_NUMBER, TSK_ORGANIZATION, TSK_URL), " +
123 "TSK_DELETED_PROG (deleted program; attr: TSK_DATETIME, TSK_PROG_NAME, TSK_PATH), " +
124 "TSK_DEVICE_ATTACHED (physically attached device e.g. USB; attr: TSK_DEVICE_ID, TSK_DATETIME, TSK_DEVICE_MAKE, TSK_DEVICE_MODEL, TSK_MAC_ADDRESS), " +
125 "TSK_DEVICE_INFO (device identifiers; attr: TSK_IMEI, TSK_ICCID, TSK_IMSI), " +
126 "TSK_EMAIL_MSG (email message; attr: TSK_EMAIL_FROM, TSK_EMAIL_TO, TSK_EMAIL_CC, TSK_EMAIL_BCC, TSK_SUBJECT, TSK_DATETIME_SENT, TSK_DATETIME_RCVD, TSK_EMAIL_CONTENT_PLAIN, TSK_HEADERS, TSK_MSG_ID, TSK_THREAD_ID), " +
127 "TSK_EXTRACTED_TEXT (text extracted from content; attr: TSK_TEXT), " +
128 "TSK_GEN_INFO (generic per-file info; attr: TSK_HASH_PHOTODNA), " +
129 "TSK_GPS_AREA (GPS area outline; attr: TSK_GEO_WAYPOINTS, TSK_LOCATION, TSK_NAME, TSK_PROG_NAME), " +
130 "TSK_GPS_BOOKMARK (saved GPS waypoint; attr: TSK_GEO_LATITUDE, TSK_GEO_LONGITUDE, TSK_GEO_ALTITUDE, TSK_DATETIME, TSK_LOCATION, TSK_NAME, TSK_PROG_NAME), " +
131 "TSK_GPS_LAST_KNOWN_LOCATION (last known GPS location; attr: TSK_GEO_LATITUDE, TSK_GEO_LONGITUDE, TSK_GEO_ALTITUDE, TSK_DATETIME, TSK_LOCATION, TSK_NAME), " +
132 "TSK_GPS_ROUTE (GPS route; attr: TSK_GEO_WAYPOINTS, TSK_DATETIME, TSK_LOCATION, TSK_NAME, TSK_PROG_NAME), " +
133 "TSK_GPS_SEARCH (GPS location that was searched; attr: TSK_GEO_LATITUDE, TSK_GEO_LONGITUDE, TSK_GEO_ALTITUDE, TSK_DATETIME, TSK_LOCATION, TSK_NAME), " +
134 "TSK_GPS_TRACK (GPS track path; attr: TSK_GEO_TRACKPOINTS, TSK_NAME, TSK_PROG_NAME), " +
135 "TSK_INSTALLED_PROG (installed program; attr: TSK_PROG_NAME, TSK_DATETIME, TSK_PATH, TSK_VERSION, TSK_PERMISSIONS), " +
136 "TSK_MESSAGE (chat/SMS message; attr: TSK_TEXT, TSK_MESSAGE_TYPE, TSK_DATETIME, TSK_DIRECTION, TSK_PHONE_NUMBER_FROM, TSK_PHONE_NUMBER_TO, TSK_READ_STATUS, TSK_SUBJECT, TSK_THREAD_ID), " +
137 "TSK_METADATA (document metadata; attr: TSK_DATETIME_CREATED, TSK_DATETIME_MODIFIED, TSK_DESCRIPTION, TSK_OWNER, TSK_ORGANIZATION, TSK_PROG_NAME, TSK_VERSION, TSK_LAST_PRINTED_DATETIME, TSK_USER_ID), " +
138 "TSK_OS_INFO (OS details; attr: TSK_PROG_NAME, TSK_VERSION, TSK_DATETIME, TSK_DOMAIN, TSK_OWNER, TSK_ORGANIZATION, TSK_PATH, TSK_PROCESSOR_ARCHITECTURE, TSK_NAME, TSK_PRODUCT_ID, TSK_TEMP_DIR), " +
139 "TSK_PROG_NOTIFICATIONS (app notifications; attr: TSK_DATETIME, TSK_PROG_NAME, TSK_TITLE, TSK_VALUE), " +
140 "TSK_PROG_RUN (program execution; attr: TSK_PROG_NAME, TSK_DATETIME, TSK_COUNT, TSK_USER_NAME, TSK_PATH), " +
141 "TSK_RECENT_OBJECT (recently accessed item; attr: TSK_PATH, TSK_DATETIME_ACCESSED, TSK_PROG_NAME, TSK_NAME, TSK_VALUE), " +
142 "TSK_REMOTE_DRIVE (remote/mapped drive; attr: TSK_REMOTE_PATH, TSK_LOCAL_PATH), " +
143 "TSK_SCREEN_SHOTS (screenshot; attr: TSK_DATETIME, TSK_PROG_NAME, TSK_PATH), " +
144 "TSK_SERVICE_ACCOUNT (app/web account; attr: TSK_PROG_NAME, TSK_USER_ID, TSK_USER_NAME, TSK_DOMAIN, TSK_URL, TSK_DATETIME_CREATED, TSK_CATEGORY, TSK_PASSWORD), " +
145 "TSK_SIM_ATTACHED (SIM card; attr: TSK_ICCID, TSK_IMSI), " +
146 "TSK_SPEED_DIAL_ENTRY (speed dial; attr: TSK_PHONE_NUMBER, TSK_NAME_PERSON, TSK_SHORTCUT), " +
147 "TSK_TL_EVENT (timeline event; attr: TSK_TL_EVENT_TYPE, TSK_DATETIME, TSK_DESCRIPTION), " +
148 "TSK_USER_DEVICE_EVENT (device activity e.g. lock/unlock; attr: TSK_DATETIME_START, TSK_ACTIVITY_TYPE, TSK_DATETIME_END, TSK_PROG_NAME), " +
149 "TSK_WEB_BOOKMARK (browser bookmark; attr: TSK_URL, TSK_DOMAIN, TSK_PROG_NAME, TSK_NAME, TSK_TITLE, TSK_DATETIME_CREATED, TSK_USER_NAME, TSK_COMMENT), " +
150 "TSK_WEB_CACHE (web cache entry; attr: TSK_URL, TSK_PATH, TSK_DOMAIN, TSK_DATETIME_CREATED, TSK_HEADERS), " +
151 "TSK_WEB_COOKIE (web cookie; attr: TSK_URL, TSK_NAME, TSK_VALUE, TSK_DOMAIN, TSK_DATETIME_CREATED, TSK_DATETIME_ACCESSED, TSK_DATETIME_END, TSK_PROG_NAME, TSK_USER_NAME), " +
152 "TSK_WEB_DOWNLOAD (web download; attr: TSK_URL, TSK_DOMAIN, TSK_PATH, TSK_DATETIME_ACCESSED, TSK_PROG_NAME), " +
153 "TSK_WEB_FORM_ADDRESS (browser autofill address; attr: TSK_LOCATION, TSK_NAME_PERSON, TSK_EMAIL, TSK_PHONE_NUMBER, TSK_DATETIME_ACCESSED), " +
154 "TSK_WEB_FORM_AUTOFILL (browser autofill field; attr: TSK_NAME, TSK_VALUE, TSK_DATETIME_CREATED, TSK_DATETIME_ACCESSED, TSK_PROG_NAME), " +
155 "TSK_WEB_HISTORY (browser history; attr: TSK_URL, TSK_DOMAIN, TSK_TITLE, TSK_DATETIME_ACCESSED, TSK_REFERRER, TSK_URL_DECODED, TSK_USER_NAME, TSK_PROG_NAME), " +
156 "TSK_WEB_SEARCH_QUERY (web search; attr: TSK_TEXT, TSK_DOMAIN, TSK_DATETIME_ACCESSED, TSK_PROG_NAME), " +
157 "TSK_WIFI_NETWORK (WiFi network; attr: TSK_SSID, TSK_DATETIME, TSK_DEVICE_ID, TSK_MAC_ADDRESS), " +
158 "TSK_WIFI_NETWORK_ADAPTER (WiFi adapter; attr: TSK_MAC_ADDRESS).",
160 "artifactType", param(
"string",
"TSK artifact type name e.g. TSK_WEB_HISTORY"),
161 "attributeType", param(
"string",
"Filter by attribute type name e.g. TSK_URL"),
162 "attributeValue", param(
"string",
"Filter by attribute value substring"),
163 "dataSourceId", param(
"integer",
"Object ID of the data source to limit results to (from query_data_sources objectId field)"),
164 "limit", param(
"integer",
"Max results, default 50, max 500")
167 toolWithNote(
"query_analysis_results",
168 "Search for analysis results in the current case. Analysis results are conclusions " +
169 "drawn by ingest modules — hash hits, keyword hits, encryption detection, EXIF data, " +
170 "etc. Each result includes a score (significance + priority) indicating how notable " +
171 "the finding is. Significance values: NOTABLE, LIKELY_NOTABLE, LIKELY_NONE, NONE, UNKNOWN. " +
172 "Use artifactType to filter by type. Available types: " +
173 "TSK_DATA_SOURCE_USAGE (how data source was used e.g. OS Drive; attr: TSK_DESCRIPTION), " +
174 "TSK_ENCRYPTION_DETECTED (encrypted content; attr: TSK_COMMENT), " +
175 "TSK_ENCRYPTION_SUSPECTED (likely encrypted content; attr: TSK_COMMENT), " +
176 "TSK_EXT_MISMATCH_DETECTED (file extension does not match MIME type), " +
177 "TSK_FACE_DETECTED (human face detected in media), " +
178 "TSK_HASHSET_HIT (MD5 matches a known hashset; attr: TSK_SET_NAME, TSK_COMMENT), " +
179 "TSK_INTERESTING_ITEM (matches an interesting items rule; attr: TSK_SET_NAME, TSK_COMMENT, TSK_CATEGORY), " +
180 "TSK_INTERESTING_ARTIFACT_HIT (artifact matches an interesting items rule; attr: TSK_SET_NAME, TSK_COMMENT), " +
181 "TSK_INTERESTING_FILE_HIT (file matches an interesting items rule; attr: TSK_SET_NAME, TSK_COMMENT), " +
182 "TSK_KEYWORD_HIT (keyword search match; attr: TSK_KEYWORD, TSK_KEYWORD_SEARCH_TYPE, TSK_SET_NAME, TSK_KEYWORD_PREVIEW), " +
183 "TSK_MALWARE (malware detected), " +
184 "TSK_METADATA_EXIF (EXIF metadata from image; attr: TSK_DATETIME_CREATED, TSK_DEVICE_MAKE, TSK_DEVICE_MODEL, TSK_GEO_LATITUDE, TSK_GEO_LONGITUDE, TSK_GEO_ALTITUDE), " +
185 "TSK_OBJECT_DETECTED (object detected in media by CV; attr: TSK_COMMENT), " +
186 "TSK_PREVIOUSLY_NOTABLE (previously tagged Notable in another case; attr: TSK_CORRELATION_TYPE, TSK_CORRELATION_VALUE, TSK_OTHER_CASES), " +
187 "TSK_PREVIOUSLY_SEEN (seen in another case; attr: TSK_CORRELATION_TYPE, TSK_CORRELATION_VALUE, TSK_OTHER_CASES), " +
188 "TSK_PREVIOUSLY_UNSEEN (not seen before; attr: TSK_CORRELATION_TYPE, TSK_CORRELATION_VALUE), " +
189 "TSK_USER_CONTENT_SUSPECTED (likely user-generated content; attr: TSK_COMMENT), " +
190 "TSK_VERIFICATION_FAILED (hash/integrity verification failed; attr: TSK_COMMENT), " +
191 "TSK_WEB_ACCOUNT_TYPE (web account type classification; attr: TSK_DOMAIN, TSK_TEXT, TSK_URL), " +
192 "TSK_WEB_CATEGORIZATION (web host category e.g. Web Email; attr: TSK_NAME, TSK_DOMAIN, TSK_HOST), " +
193 "TSK_YARA_HIT (YARA rule match; attr: TSK_RULE, TSK_SET_NAME).",
195 "artifactType", param(
"string",
"TSK artifact type name e.g. TSK_KEYWORD_HIT"),
196 "attributeType", param(
"string",
"Filter by attribute type name e.g. TSK_KEYWORD"),
197 "attributeValue", param(
"string",
"Filter by attribute value substring"),
198 "dataSourceId", param(
"integer",
"Object ID of the data source to limit results to (from query_data_sources objectId field)"),
199 "limit", param(
"integer",
"Max results, default 50, max 500")
202 toolWithNote(
"query_data_sources",
203 "List all data sources (disk images, logical file sets) in the current case. " +
204 "Returns objectId, name, type, size, timezone, and for disk images: image type, " +
205 "sector size, file paths, and acquisition hashes (MD5/SHA-1/SHA-256).",
208 toolWithNote(
"get_hosts",
209 "List all hosts in the case. Each host groups one or more data sources " +
210 "that belong to the same device or machine. Use this as the top of the " +
211 "storage hierarchy before drilling into data sources.",
214 toolWithNote(
"get_data_source_tree",
215 "Returns the full storage hierarchy for one or all data sources: " +
216 "Image → VolumeSystem → Volume → FileSystem. " +
217 "Use this to understand how a disk image is partitioned and what file systems it contains. " +
218 "Logical file set data sources (no partitions) appear as leaf nodes with no children. " +
219 "Each FileSystem entry includes type (NTFS, FAT32, ext4, etc.), offset, block size, " +
220 "block count, and inode range.",
222 "dataSourceId", param(
"integer",
"Object ID of the data source to inspect. Omit to return all data sources.")
225 toolWithNote(
"query_tags",
226 "Find files or artifacts that have been tagged by the examiner.",
228 "tagName", param(
"string",
"Filter by tag name e.g. \"Notable Item\"")
231 toolWithNote(
"query_timeline",
232 "Return timeline events in a time range, sorted by time. " +
233 "Covers all event types in a single query: file system timestamps (modified, accessed, " +
234 "changed, created) and artifact events (web history, downloads, searches, cookies, " +
235 "bookmarks, installed programs, USB devices, etc.). " +
236 "Much more efficient than querying individual artifact types when the question is " +
237 "time-driven. Use summarize_timeline first to understand what categories of activity " +
238 "exist before fetching detail. Omit startTime/endTime to span the entire case.",
240 Map.entry(
"startTime", param(
"string",
"ISO 8601 start of time range (inclusive)")),
241 Map.entry(
"endTime", param(
"string",
"ISO 8601 end of time range (inclusive)")),
242 Map.entry(
"dataSourceId", param(
"integer",
"Object ID of the data source to limit results to (from query_data_sources objectId field)")),
243 Map.entry(
"textFilter", param(
"string",
"Filter events whose description contains this substring")),
244 Map.entry(
"limit", param(
"integer",
"Max results, default 100, max 1000"))
247 toolWithNote(
"summarize_timeline",
248 "Return counts of timeline events grouped by category for a time range. " +
249 "Categories are: File System (file timestamps), Web Activity (history, downloads, " +
250 "cookies, bookmarks, searches), and Misc (installed programs, USB devices, etc.). " +
251 "Use this before query_timeline to understand the shape of activity in a period " +
252 "without fetching every event. Omit startTime/endTime to summarize the entire case.",
254 "startTime", param(
"string",
"ISO 8601 start of time range (inclusive)"),
255 "endTime", param(
"string",
"ISO 8601 end of time range (inclusive)"),
256 "dataSourceId", param(
"integer",
"Object ID of the data source to limit results to (from query_data_sources objectId field)")
259 toolWithNote(
"get_os_accounts",
260 "List OS user accounts discovered in the case. Returns SID/UID, login name, " +
261 "full name, account type, status, creation time, extended attributes " +
262 "(e.g., home directory, login script, last login), and which data sources " +
263 "the account appeared on with instance type (LAUNCHED, ACCESSED, or REFERENCED). " +
264 "Useful for identifying users, admins, and service accounts on examined systems.",
267 toolWithNote(
"get_communications_accounts",
268 "List accounts found in communications data: email addresses, phone numbers, " +
269 "Skype/Facebook/WhatsApp/Twitter/Instagram usernames, etc. Optionally filter " +
270 "by account type. Available types: CREDIT_CARD, DEVICE, EMAIL, FACEBOOK, " +
271 "IMO, INSTAGRAM, LINE, MESSAGING_APP, PHONE, SHAREIT, SKYPE, TANGO, TEXTNOW, " +
272 "THREEMA, TWITTER, VIBER, WEBSITE, WHATSAPP, XENDER, ZAPYA. " +
273 "Returns account type, identifier, device ID, and relationship count.",
275 "accountType", param(
"string",
"Filter by account type e.g. EMAIL, PHONE, SKYPE"),
276 "limit", param(
"integer",
"Max results, default 100")
279 toolWithNote(
"get_file_content",
280 "Read the text content of a file by its object ID. Returns UTF-8 text with " +
281 "undecodable bytes replaced by '?'. " +
282 "Files larger than 65536 bytes require an explicit maxBytes parameter up to 1048576 (1 MB); " +
283 "requests beyond that are rejected — use offset+maxBytes to page through larger files. " +
284 "Returns the content string, actual bytes read, file size, and whether the content was truncated.",
286 "objectId", param(
"integer",
"Object ID of the file (from query_files objectId field)"),
287 "offset", param(
"integer",
"Byte offset to start reading from, default 0"),
288 "maxBytes", param(
"integer",
"Maximum bytes to read, default 65536, max 1048576")
290 List.of(
"objectId")),
292 toolWithNote(
"get_account_relationships",
293 "Get communications relationships for a specific account — who it communicated " +
294 "with and how many messages/calls. Supply the accountType (e.g. EMAIL) and " +
295 "accountId (the identifier, e.g. user@example.com or +15551234567). " +
296 "Returns the matched account plus all related accounts with relationship counts.",
298 "accountType", param(
"string",
"Account type e.g. EMAIL, PHONE (required)"),
299 "accountId", param(
"string",
"Type-specific identifier e.g. user@example.com or +15551234567 (required)")
301 List.of(
"accountType",
"accountId")),
303 toolWithNote(
"get_object_children",
304 "Return the parent and children of any object in the case database by its object ID. " +
305 "All TSK objects (files, directories, artifacts, images, volume systems, volumes, " +
306 "file systems) share a common parent-child hierarchy. A file's children may include " +
307 "both derived files and blackboard artifacts. An image's children include volume systems " +
308 "and file systems. Use this to navigate the object tree starting from any known ID. " +
309 "Each child entry includes its objectId, objectType, and type-specific summary fields " +
310 "(name/path/size for files; artifactType/attributes for artifacts; fsType for file " +
313 "objectId", param(
"integer",
"Object ID of the item whose children you want (required)")
315 List.of(
"objectId")),
317 toolWithNote(
"list_reports",
318 "List all reports that have been generated for the current case. " +
319 "Returns objectId, reportName, sourceModule, path, createdTime, size, and contentType " +
320 "(text/plain or text/html) for each report. Use this before get_report_content " +
321 "to discover available report IDs and names.",
324 toolWithNote(
"get_report_content",
325 "Read the content of a case report by its objectId (from list_reports). " +
326 "Reports may be plain text or HTML — check the contentType field. " +
327 "Files larger than 65536 bytes require an explicit maxBytes parameter up to 1048576 (1 MB); " +
328 "requests beyond that are rejected — use offset+maxBytes to page through larger reports. " +
329 "Returns objectId, reportName, path, fileSize, contentType, offset, bytesRead, " +
330 "truncated, eof, and content.",
332 "objectId", param(
"integer",
"Object ID of the report (from list_reports objectId field)"),
333 "offset", param(
"integer",
"Byte offset to start reading from, default 0"),
334 "maxBytes", param(
"integer",
"Maximum bytes to read, default 65536, max 1048576")
344 Map<String, Object> getCaseSummary() throws TskCoreException {
345 long totalFiles = skCase.countFilesWhere(
"1=1");
347 final long[] artifactCount = {0};
348 skCase.getCaseDbAccessManager().select(
349 "COUNT(*) AS cnt FROM blackboard_artifacts",
353 artifactCount[0] = rs.getLong(
"cnt");
355 }
catch (java.sql.SQLException ex) {
360 Map<String, Object> summary =
new LinkedHashMap<>();
361 summary.put(
"caseName", caseName);
362 summary.put(
"dataSources", skCase.getDataSources().size());
363 summary.put(
"totalFiles", totalFiles);
364 summary.put(
"totalArtifacts", artifactCount[0]);
379 List<Map<String, Object>> queryFiles(JsonNode args)
throws TskCoreException {
380 int limit = args.path(
"limit").asInt(50);
381 if (limit <= 0 || limit > 500) {
385 List<String> conditions =
new ArrayList<>();
388 String nameContains = textOrNull(args,
"nameContains");
389 if (nameContains !=
null) {
390 conditions.add(
"LOWER(name) LIKE '%" + escapeSql(nameContains.toLowerCase()) +
"%'");
394 String extension = textOrNull(args,
"extension");
395 if (extension !=
null) {
396 conditions.add(
"LOWER(extension) = '" + escapeSql(extension.toLowerCase()) +
"'");
400 String mimeType = textOrNull(args,
"mimeType");
401 if (mimeType !=
null) {
402 if (mimeType.endsWith(
"*")) {
403 String prefix = mimeType.substring(0, mimeType.length() - 1);
404 conditions.add(
"mime_type LIKE '" + escapeSql(prefix) +
"%'");
406 conditions.add(
"mime_type = '" + escapeSql(mimeType) +
"'");
411 if (!args.path(
"minSize").isMissingNode()) {
412 conditions.add(
"size >= " + args.path(
"minSize").asLong());
414 if (!args.path(
"maxSize").isMissingNode()) {
415 conditions.add(
"size <= " + args.path(
"maxSize").asLong());
419 if (!args.path(
"modifiedAfter").isMissingNode()) {
420 conditions.add(
"mtime >= " + parseTimeArg(args,
"modifiedAfter", 0L));
422 if (!args.path(
"modifiedBefore").isMissingNode()) {
423 conditions.add(
"mtime <= " + parseTimeArg(args,
"modifiedBefore", 0L));
427 if (!args.path(
"createdAfter").isMissingNode()) {
428 conditions.add(
"crtime >= " + parseTimeArg(args,
"createdAfter", 0L));
430 if (!args.path(
"createdBefore").isMissingNode()) {
431 conditions.add(
"crtime <= " + parseTimeArg(args,
"createdBefore", 0L));
435 String pathContains = textOrNull(args,
"pathContains");
436 if (pathContains !=
null) {
437 conditions.add(
"LOWER(parent_path) LIKE '%" + escapeSql(pathContains.toLowerCase()) +
"%'");
441 if (!args.path(
"isDirectory").isMissingNode()) {
442 boolean isDir = args.path(
"isDirectory").asBoolean();
443 conditions.add(
"dir_type = " + (isDir ? TskData.TSK_FS_NAME_TYPE_ENUM.DIR.getValue()
444 : TskData.TSK_FS_NAME_TYPE_ENUM.REG.getValue()));
448 if (!args.path(
"allocated").isMissingNode()) {
449 boolean alloc = args.path(
"allocated").asBoolean();
450 conditions.add(
"dir_flags = " + (alloc ? TskData.TSK_FS_NAME_FLAG_ENUM.ALLOC.getValue()
451 : TskData.TSK_FS_NAME_FLAG_ENUM.UNALLOC.getValue()));
455 String knownStateStr = textOrNull(args,
"knownState");
456 if (knownStateStr !=
null) {
457 TskData.FileKnown knownVal = parseKnownState(knownStateStr);
458 if (knownVal !=
null) {
459 conditions.add(
"known = " + knownVal.getFileKnownValue());
464 String md5 = textOrNull(args,
"md5");
466 conditions.add(
"LOWER(md5) = '" + escapeSql(md5.toLowerCase()) +
"'");
470 String sha256 = textOrNull(args,
"sha256");
471 if (sha256 !=
null) {
472 conditions.add(
"LOWER(sha256) = '" + escapeSql(sha256.toLowerCase()) +
"'");
476 String orderBy = textOrNull(args,
"orderBy");
477 String orderClause =
switch (orderBy ==
null ?
"" : orderBy.toLowerCase()) {
478 case "size_desc" ->
" ORDER BY size DESC";
479 case "size_asc" ->
" ORDER BY size ASC";
480 case "name_asc" ->
" ORDER BY name ASC";
481 case "name_desc" ->
" ORDER BY name DESC";
482 case "modified_desc" ->
" ORDER BY mtime DESC";
483 case "modified_asc" ->
" ORDER BY mtime ASC";
484 case "created_desc" ->
" ORDER BY crtime DESC";
485 case "created_asc" ->
" ORDER BY crtime ASC";
489 String whereClause = (conditions.isEmpty() ?
"1=1" : String.join(
" AND ", conditions))
490 + orderClause +
" LIMIT " + limit;
492 List<AbstractFile> files = skCase.findAllFilesWhere(whereClause);
494 List<Map<String, Object>> result =
new ArrayList<>(files.size());
495 for (AbstractFile f : files) {
496 result.add(buildFileItem(f));
505 private Map<String, Object> buildFileItem(AbstractFile f) {
506 Map<String, Object> item =
new LinkedHashMap<>();
509 item.put(
"objectId", f.getId());
510 item.put(
"name", f.getName());
512 item.put(
"path", f.getUniquePath());
513 }
catch (TskCoreException ex) {
514 item.put(
"path", f.getParentPath() + f.getName());
516 item.put(
"parentPath", f.getParentPath());
517 item.put(
"extension", f.getNameExtension());
518 item.put(
"dataSourceId", f.getDataSourceObjectId());
521 item.put(
"size", f.getSize());
524 item.put(
"modifiedTime", epochToIso(f.getMtime()));
525 item.put(
"accessedTime", epochToIso(f.getAtime()));
526 item.put(
"changeTime", epochToIso(f.getCtime()));
527 item.put(
"createdTime", epochToIso(f.getCrtime()));
530 item.put(
"fileType", f.getType().name());
531 item.put(
"dirType", f.getDirTypeAsString());
532 item.put(
"metaType", f.getMetaTypeAsString());
533 item.put(
"dirFlag", f.getDirFlagAsString());
534 item.put(
"metaFlags", f.getMetaFlagsAsString());
535 item.put(
"modes", f.getModesAsString());
538 item.put(
"knownState", f.getKnown().name());
541 item.put(
"mimeType", f.getMIMEType());
542 item.put(
"md5Hash", f.getMd5Hash());
543 item.put(
"sha256Hash", f.getSha256Hash());
544 item.put(
"sha1Hash", f.getSha1Hash());
547 item.put(
"uid", f.getUid());
548 item.put(
"gid", f.getGid());
549 item.put(
"ownerUid", f.getOwnerUid().orElse(
null));
552 item.put(
"metaAddr", f.getMetaAddr());
553 item.put(
"metaSeq", f.getMetaSeq());
557 List<Attribute> attrs = f.getAttributes();
558 if (!attrs.isEmpty()) {
559 List<Map<String, Object>> attrList =
new ArrayList<>(attrs.size());
560 for (Attribute attr : attrs) {
561 Map<String, Object> attrMap =
new LinkedHashMap<>();
562 attrMap.put(
"type", attr.getAttributeType().getTypeName());
563 attrMap.put(
"value", fileAttrValueAsObject(attr));
564 attrList.add(attrMap);
566 item.put(
"attributes", attrList);
568 item.put(
"attributes", Collections.emptyList());
570 }
catch (TskCoreException ex) {
571 item.put(
"attributes", Collections.emptyList());
581 List<Map<String, Object>> queryDataArtifacts(JsonNode args)
throws TskCoreException {
582 return queryArtifactsInternal(args,
false);
589 List<Map<String, Object>> queryAnalysisResults(JsonNode args)
throws TskCoreException {
590 return queryArtifactsInternal(args,
true);
601 private List<Map<String, Object>> queryArtifactsInternal(JsonNode args,
boolean isAnalysis)
602 throws TskCoreException {
603 int limit = args.path(
"limit").asInt(50);
604 if (limit <= 0 || limit > 500) limit = 50;
606 String artifactTypeName = textOrNull(args,
"artifactType");
607 String attrTypeFilter = textOrNull(args,
"attributeType");
608 String attrValueFilter = textOrNull(args,
"attributeValue");
609 long dataSourceId = args.path(
"dataSourceId").isMissingNode() ? -1
610 : args.path(
"dataSourceId").asLong();
613 StringBuilder where =
new StringBuilder(
"1=1");
614 if (artifactTypeName !=
null) {
615 BlackboardArtifact.Type type;
617 type = skCase.getBlackboard().getArtifactType(artifactTypeName);
618 }
catch (TskCoreException ex) {
619 return Collections.emptyList();
621 where.append(
" AND artifacts.artifact_type_id = ").append(type.getTypeID());
623 if (dataSourceId >= 0) {
624 where.append(
" AND artifacts.data_source_obj_id = ").append(dataSourceId);
629 if (attrTypeFilter ==
null && attrValueFilter ==
null) {
630 where.append(
" LIMIT ").append(limit);
634 List<? extends BlackboardArtifact> artifacts = isAnalysis
635 ? skCase.getBlackboard().getAnalysisResultsWhere(where.toString())
636 : skCase.getBlackboard().getDataArtifactsWhere(where.toString());
639 List<Map<String, Object>> result =
new ArrayList<>();
640 for (BlackboardArtifact artifact : artifacts) {
641 if (result.size() >= limit)
break;
643 List<BlackboardAttribute> attrs = artifact.getAttributes();
644 if (attrTypeFilter !=
null || attrValueFilter !=
null) {
645 boolean matches =
false;
646 for (BlackboardAttribute attr : attrs) {
647 boolean typeMatch = attrTypeFilter ==
null
648 || attr.getAttributeType().getTypeName().equalsIgnoreCase(attrTypeFilter);
649 boolean valueMatch = attrValueFilter ==
null
650 || attrValueAsString(attr).toLowerCase().contains(attrValueFilter.toLowerCase());
651 if (typeMatch && valueMatch) { matches =
true;
break; }
653 if (!matches)
continue;
656 Map<String, Object> item =
new LinkedHashMap<>();
657 item.put(
"objectId", artifact.getId());
658 item.put(
"sourceFileId", artifact.getObjectID());
659 item.put(
"artifactType", artifact.getArtifactTypeName());
660 item.put(
"dataSourceId", artifact.getDataSourceObjectID());
663 Score score = ((AnalysisResult) artifact).getScore();
664 item.put(
"score", Map.of(
665 "significance", score.getSignificance().getName(),
666 "priority", score.getPriority().getName()
670 List<Map<String, Object>> attrList =
new ArrayList<>();
671 for (BlackboardAttribute attr : attrs) {
672 Map<String, Object> attrMap =
new LinkedHashMap<>();
673 attrMap.put(
"type", attr.getAttributeType().getTypeName());
674 attrMap.put(
"value", attrValueAsObject(attr));
675 attrList.add(attrMap);
677 item.put(
"attributes", attrList);
691 List<Map<String, Object>> getHosts() throws TskCoreException {
692 List<Map<String, Object>> result =
new ArrayList<>();
693 for (Host host : skCase.getHostManager().getAllHosts()) {
694 Map<String, Object> item =
new LinkedHashMap<>();
695 item.put(
"id", host.getHostId());
696 item.put(
"name", host.getName());
698 List<Map<String, Object>> dataSources =
new ArrayList<>();
699 for (DataSource ds : skCase.getHostManager().getDataSourcesForHost(host)) {
700 dataSources.add(buildDataSourceItem((Content) ds));
702 item.put(
"dataSources", dataSources);
712 List<Map<String, Object>> queryDataSources() throws TskCoreException {
713 List<Map<String, Object>> result =
new ArrayList<>();
714 for (Content ds : skCase.getDataSources()) {
715 result.add(buildDataSourceItem(ds));
724 private Map<String, Object> buildDataSourceItem(Content ds)
throws TskCoreException {
725 Map<String, Object> item =
new LinkedHashMap<>();
726 item.put(
"objectId", ds.getId());
727 item.put(
"name", ds.getName());
728 item.put(
"type", ds.getClass().getSimpleName());
729 item.put(
"size", ds.getSize());
731 if (ds instanceof DataSource) {
732 DataSource dataSource = (DataSource) ds;
733 item.put(
"timezone", dataSource.getTimeZone());
734 item.put(
"deviceId", dataSource.getDeviceId());
738 Long addedMs = dataSource.getDateAdded();
739 item.put(
"addedDate", addedMs !=
null && addedMs > 0 ? epochToIso(addedMs / 1000) :
null);
740 }
catch (TskCoreException ex) {
741 item.put(
"addedDate",
null);
745 if (ds instanceof Image) {
746 Image image = (Image) ds;
747 item.put(
"imageType", image.getType().name());
748 item.put(
"sectorSize", image.getSsize());
749 item.put(
"paths", List.of(image.getPaths()));
750 try { item.put(
"md5", image.getMd5()); }
751 catch (TskCoreException ex) { item.put(
"md5",
null); }
752 try { item.put(
"sha1", image.getSha1()); }
753 catch (TskCoreException ex) { item.put(
"sha1",
null); }
754 try { item.put(
"sha256", image.getSha256()); }
755 catch (TskCoreException ex) { item.put(
"sha256",
null); }
756 try { item.put(
"acquisitionDetails", image.getAcquisitionDetails()); }
757 catch (TskCoreException ex) { item.put(
"acquisitionDetails",
null); }
758 try { item.put(
"acquisitionToolName", image.getAcquisitionToolName()); }
759 catch (TskCoreException ex) { item.put(
"acquisitionToolName",
null); }
760 try { item.put(
"acquisitionToolVersion", image.getAcquisitionToolVersion()); }
761 catch (TskCoreException ex) { item.put(
"acquisitionToolVersion",
null); }
776 Object getDataSourceTree(JsonNode args)
throws TskCoreException {
777 long filterDsId = args.path(
"dataSourceId").isMissingNode() ? -1
778 : args.path(
"dataSourceId").asLong();
780 List<Map<String, Object>> result =
new ArrayList<>();
781 for (Content ds : skCase.getDataSources()) {
782 if (filterDsId >= 0 && ds.getId() != filterDsId) {
785 result.add(buildDataSourceTreeNode(ds));
789 if (filterDsId >= 0 && result.size() == 1) {
790 return result.get(0);
795 private Map<String, Object> buildDataSourceTreeNode(Content ds)
throws TskCoreException {
796 Map<String, Object> node = buildDataSourceItem(ds);
798 if (ds instanceof Image) {
799 Image image = (Image) ds;
800 List<Map<String, Object>> vsNodes =
new ArrayList<>();
802 for (VolumeSystem vs : image.getVolumeSystems()) {
803 vsNodes.add(buildVolumeSystemNode(vs));
807 List<Map<String, Object>> directFsNodes =
new ArrayList<>();
808 for (FileSystem fs : image.getFileSystems()) {
809 directFsNodes.add(buildFileSystemNode(fs));
812 node.put(
"volumeSystems", vsNodes);
815 node.put(
"fileSystems", vsNodes.isEmpty() ? directFsNodes : Collections.emptyList());
818 node.put(
"volumeSystems", Collections.emptyList());
819 node.put(
"fileSystems", Collections.emptyList());
825 private Map<String, Object> buildVolumeSystemNode(VolumeSystem vs)
throws TskCoreException {
826 Map<String, Object> node =
new LinkedHashMap<>();
827 node.put(
"objectId", vs.getId());
828 node.put(
"type",
"VolumeSystem");
829 node.put(
"vsType", vs.getType().getName());
830 node.put(
"offset", vs.getOffset());
831 node.put(
"blockSize", vs.getBlockSize());
833 List<Map<String, Object>> volNodes =
new ArrayList<>();
834 for (Volume vol : vs.getVolumes()) {
835 volNodes.add(buildVolumeNode(vol));
837 node.put(
"volumes", volNodes);
841 private Map<String, Object> buildVolumeNode(Volume vol)
throws TskCoreException {
842 Map<String, Object> node =
new LinkedHashMap<>();
843 node.put(
"objectId", vol.getId());
844 node.put(
"type",
"Volume");
845 node.put(
"addr", vol.getAddr());
846 node.put(
"description", vol.getDescription());
847 node.put(
"startSector", vol.getStart());
848 node.put(
"lengthSectors", vol.getLength());
849 node.put(
"size", vol.getSize());
850 node.put(
"flags", vol.getFlagsAsString());
852 List<Map<String, Object>> fsNodes =
new ArrayList<>();
853 for (FileSystem fs : vol.getFileSystems()) {
854 fsNodes.add(buildFileSystemNode(fs));
856 node.put(
"fileSystems", fsNodes);
860 private Map<String, Object> buildFileSystemNode(FileSystem fs)
throws TskCoreException {
861 Map<String, Object> node =
new LinkedHashMap<>();
862 node.put(
"objectId", fs.getId());
863 node.put(
"type",
"FileSystem");
864 node.put(
"fsType", fs.getFsType().getDisplayName());
865 node.put(
"imageOffset", fs.getImageOffset());
866 node.put(
"blockSize", fs.getBlock_size());
867 node.put(
"blockCount", fs.getBlock_count());
868 node.put(
"rootInum", fs.getRoot_inum());
869 node.put(
"firstInum", fs.getFirst_inum());
870 node.put(
"lastInum", fs.getLastInum());
878 List<Map<String, Object>> queryTags(JsonNode args)
throws TskCoreException {
879 String tagNameFilter = textOrNull(args,
"tagName");
881 List<TagName> tagNames =
new ArrayList<>();
882 for (TagName tn : skCase.getTagNamesInUse()) {
883 if (tagNameFilter ==
null || tn.getDisplayName().equalsIgnoreCase(tagNameFilter)) {
888 List<Map<String, Object>> result =
new ArrayList<>();
889 for (TagName tagName : tagNames) {
890 for (ContentTag tag : skCase.getContentTagsByTagName(tagName)) {
891 Map<String, Object> item =
new LinkedHashMap<>();
892 item.put(
"tagId", tag.getId());
893 item.put(
"tagName", tagName.getDisplayName());
894 item.put(
"comment", tag.getComment());
895 item.put(
"itemType",
"file");
896 Content content = tag.getContent();
897 item.put(
"objectId", content.getId());
898 item.put(
"itemName", content.getName());
901 for (BlackboardArtifactTag tag : skCase.getBlackboardArtifactTagsByTagName(tagName)) {
902 Map<String, Object> item =
new LinkedHashMap<>();
903 item.put(
"tagId", tag.getId());
904 item.put(
"tagName", tagName.getDisplayName());
905 item.put(
"comment", tag.getComment());
906 item.put(
"itemType",
"artifact");
907 BlackboardArtifact artifact = tag.getArtifact();
908 item.put(
"objectId", artifact.getId());
909 item.put(
"itemName", artifact.getArtifactTypeName());
920 Map<String, Object> getFileContent(JsonNode args)
throws TskCoreException {
921 long fileId = args.path(
"objectId").asLong(-1);
923 throw new TskCoreException(
"objectId is required");
926 long offset = args.path(
"offset").asLong(0);
927 int maxBytes = args.path(
"maxBytes").asInt(65536);
930 if (maxBytes <= 0 || maxBytes > 1_048_576) {
934 AbstractFile file = skCase.getAbstractFileById(fileId);
935 long fileSize = file.getSize();
940 if (offset >= fileSize) {
941 return Map.of(
"objectId", fileId,
"fileName", file.getName(),
"fileSize", fileSize,
942 "offset", offset,
"bytesRead", 0,
"truncated",
false,
"eof",
true,
"content",
"");
945 long available = fileSize - offset;
946 int toRead = (int) Math.min(maxBytes, available);
947 boolean truncated = available > maxBytes;
949 byte[] buf =
new byte[toRead];
950 int bytesRead = file.read(buf, offset, toRead);
951 if (bytesRead < toRead) {
952 buf = java.util.Arrays.copyOf(buf, Math.max(bytesRead, 0));
957 CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder()
958 .onMalformedInput(CodingErrorAction.REPLACE)
959 .onUnmappableCharacter(CodingErrorAction.REPLACE);
962 chars = decoder.decode(ByteBuffer.wrap(buf));
963 }
catch (java.nio.charset.CharacterCodingException ex) {
965 chars = CharBuffer.wrap(
new String(buf, StandardCharsets.ISO_8859_1));
968 Map<String, Object> result =
new LinkedHashMap<>();
969 result.put(
"objectId", fileId);
970 result.put(
"fileName", file.getName());
971 result.put(
"fileSize", fileSize);
972 result.put(
"offset", offset);
973 result.put(
"bytesRead", buf.length);
974 result.put(
"truncated", truncated);
975 result.put(
"content", chars.toString());
983 List<Map<String, Object>> queryTimeline(JsonNode args)
throws TskCoreException {
984 int limit = args.path(
"limit").asInt(100);
985 if (limit <= 0 || limit > 1000) {
989 TimelineManager tm = skCase.getTimelineManager();
990 long startEpoch = parseTimeArg(args,
"startTime", tm.getMinEventTime());
991 long endEpoch = parseTimeArg(args,
"endTime", tm.getMaxEventTime());
993 Interval interval =
new Interval(startEpoch * 1000L, (endEpoch + 1) * 1000L, DateTimeZone.UTC);
994 TimelineFilter.RootFilter filter = buildTimelineFilter(args);
999 List<TimelineEvent> events = tm.getEvents(interval, filter);
1001 List<Map<String, Object>> result =
new ArrayList<>(Math.min(events.size(), limit));
1002 for (TimelineEvent e : events) {
1003 if (result.size() >= limit)
break;
1004 Map<String, Object> item =
new LinkedHashMap<>();
1005 item.put(
"time", epochToIso(e.getTime()));
1006 item.put(
"eventType", e.getEventType().getDisplayName());
1007 item.put(
"description", e.getDescription(TimelineLevelOfDetail.HIGH));
1008 item.put(
"sourceFileId", e.getContentObjID());
1009 item.put(
"artifactId", e.getArtifactID().orElse(
null));
1010 item.put(
"dataSourceId", e.getDataSourceObjID());
1011 item.put(
"tagged", e.eventSourceIsTagged());
1012 item.put(
"hashHit", e.eventSourceHasHashHits());
1022 Map<String, Object> summarizeTimeline(JsonNode args)
throws TskCoreException {
1023 TimelineManager tm = skCase.getTimelineManager();
1024 long startEpoch = parseTimeArg(args,
"startTime", tm.getMinEventTime());
1025 long endEpoch = parseTimeArg(args,
"endTime", tm.getMaxEventTime());
1027 TimelineFilter.RootFilter filter = buildTimelineFilter(args);
1029 Map<TimelineEventType, Long> counts = tm.countEventsByType(
1030 startEpoch, endEpoch, filter, TimelineEventType.HierarchyLevel.CATEGORY);
1033 Map<String, Long> byCategory =
new LinkedHashMap<>();
1034 for (Map.Entry<TimelineEventType, Long> entry : counts.entrySet()) {
1035 byCategory.put(entry.getKey().getDisplayName(), entry.getValue());
1036 total += entry.getValue();
1039 Map<String, Object> result =
new LinkedHashMap<>();
1040 result.put(
"startTime", epochToIso(startEpoch));
1041 result.put(
"endTime", epochToIso(endEpoch));
1042 result.put(
"totalEvents", total);
1043 result.put(
"byCategory", byCategory);
1051 List<Map<String, Object>> getOsAccounts() throws TskCoreException {
1052 List<Map<String, Object>> result =
new ArrayList<>();
1053 OsAccountManager oam = skCase.getOsAccountManager();
1054 for (OsAccount account : oam.getOsAccounts()) {
1055 Map<String, Object> item =
new LinkedHashMap<>();
1056 item.put(
"objectId", account.getId());
1057 item.put(
"addr", account.getAddr().orElse(
null));
1058 item.put(
"loginName", account.getLoginName().orElse(
null));
1059 item.put(
"fullName", account.getFullName().orElse(
null));
1060 item.put(
"type", account.getOsAccountType().map(t -> t.getName()).orElse(
null));
1061 item.put(
"status", account.getOsAccountStatus().map(s -> s.getName()).orElse(
null));
1062 Long createdEpoch = account.getCreationTime().orElse(
null);
1063 item.put(
"creationTime", createdEpoch !=
null && createdEpoch > 0
1064 ? epochToIso(createdEpoch) :
null);
1067 List<Map<String, String>> extAttrs =
new ArrayList<>();
1068 for (OsAccount.OsAccountAttribute attr : account.getExtendedOsAccountAttributes()) {
1069 Map<String, String> a =
new LinkedHashMap<>();
1070 a.put(
"type", attr.getAttributeType().getTypeName());
1071 a.put(
"value", attr.getDisplayString());
1074 item.put(
"extendedAttributes", extAttrs);
1077 List<Map<String, Object>> instances =
new ArrayList<>();
1078 for (OsAccountInstance inst : account.getOsAccountInstances()) {
1079 Map<String, Object> instMap =
new LinkedHashMap<>();
1080 instMap.put(
"dataSourceId", inst.getDataSource().getId());
1081 instMap.put(
"instanceType", inst.getInstanceType().getName());
1082 instances.add(instMap);
1084 item.put(
"instances", instances);
1094 List<Map<String, Object>> getCommunicationsAccounts(JsonNode args)
throws TskCoreException {
1095 int limit = args.path(
"limit").asInt(100);
1096 if (limit <= 0 || limit > 500) limit = 100;
1098 String accountTypeStr = textOrNull(args,
"accountType");
1100 CommunicationsFilter filter =
new CommunicationsFilter();
1101 if (accountTypeStr !=
null) {
1102 Account.Type type = lookupAccountType(accountTypeStr);
1104 filter.addAndFilter(
new CommunicationsFilter.AccountTypeFilter(
1105 Collections.singleton(type)));
1109 CommunicationsManager cm = skCase.getCommunicationsManager();
1110 List<AccountDeviceInstance> adis = cm.getAccountDeviceInstancesWithRelationships(filter);
1112 List<Map<String, Object>> result =
new ArrayList<>();
1113 for (AccountDeviceInstance adi : adis) {
1114 if (result.size() >= limit)
break;
1115 Map<String, Object> item =
new LinkedHashMap<>();
1116 Account account = adi.getAccount();
1117 item.put(
"accountType", account.getAccountType().getDisplayName());
1118 item.put(
"accountId", account.getTypeSpecificID());
1119 item.put(
"deviceId", adi.getDeviceId());
1120 item.put(
"relationshipCount", cm.getRelationshipSourcesCount(adi,
new CommunicationsFilter()));
1130 Map<String, Object> getAccountRelationships(JsonNode args)
throws TskCoreException, McpException {
1131 String accountTypeStr = textOrNull(args,
"accountType");
1132 String accountId = textOrNull(args,
"accountId");
1134 if (accountId ==
null) {
1135 throw new McpException(
"accountId is required");
1137 if (accountTypeStr ==
null) {
1138 throw new McpException(
"accountType is required");
1141 CommunicationsManager cm = skCase.getCommunicationsManager();
1142 CommunicationsFilter filter =
new CommunicationsFilter();
1145 AccountDeviceInstance targetAdi =
null;
1146 for (AccountDeviceInstance adi : cm.getAccountDeviceInstancesWithRelationships(filter)) {
1147 Account account = adi.getAccount();
1148 if (account.getTypeSpecificID().equalsIgnoreCase(accountId)
1149 && account.getAccountType().getTypeName().equalsIgnoreCase(accountTypeStr)) {
1155 Map<String, Object> result =
new LinkedHashMap<>();
1156 if (targetAdi ==
null) {
1157 result.put(
"error",
"Account not found: " + accountId);
1162 List<AccountDeviceInstance> related = cm.getRelatedAccountDeviceInstances(targetAdi, filter);
1164 List<Map<String, Object>> relationships =
new ArrayList<>();
1165 long totalRelationships = 0;
1166 for (AccountDeviceInstance relAdi : related) {
1168 long pairCount = cm.getRelationshipSources(targetAdi, relAdi, filter).size();
1169 totalRelationships += pairCount;
1170 Map<String, Object> item =
new LinkedHashMap<>();
1171 Account relAccount = relAdi.getAccount();
1172 item.put(
"accountType", relAccount.getAccountType().getDisplayName());
1173 item.put(
"accountId", relAccount.getTypeSpecificID());
1174 item.put(
"deviceId", relAdi.getDeviceId());
1175 item.put(
"relationshipCount", pairCount);
1176 relationships.add(item);
1179 result.put(
"account", targetAdi.getAccount().getTypeSpecificID());
1180 result.put(
"accountType", targetAdi.getAccount().getAccountType().getDisplayName());
1181 result.put(
"deviceId", targetAdi.getDeviceId());
1182 result.put(
"totalRelationships", totalRelationships);
1183 result.put(
"relatedAccounts", relationships);
1196 Map<String, Object> getObjectChildren(JsonNode args)
throws TskCoreException, McpException {
1197 if (args.path(
"objectId").isMissingNode()) {
1198 throw new McpException(
"objectId is required");
1200 long objectId = args.path(
"objectId").asLong();
1202 Content content = skCase.getContentById(objectId);
1203 if (content ==
null) {
1204 throw new McpException(
"No object found with id " + objectId);
1207 List<Content> children = content.getChildren();
1208 List<Map<String, Object>> childList =
new ArrayList<>(children.size());
1209 for (Content child : children) {
1210 childList.add(buildContentSummary(child));
1213 Content parent = content.getParent();
1215 Map<String, Object> result =
new LinkedHashMap<>();
1216 result.put(
"object", buildContentSummary(content));
1217 result.put(
"parent", parent !=
null ? buildContentSummary(parent) :
null);
1218 result.put(
"childCount", childList.size());
1219 result.put(
"children", childList);
1227 private Map<String, Object> buildContentSummary(Content c)
throws TskCoreException {
1228 Map<String, Object> item =
new LinkedHashMap<>();
1229 item.put(
"objectId", c.getId());
1231 if (c instanceof BlackboardArtifact) {
1232 BlackboardArtifact artifact = (BlackboardArtifact) c;
1233 item.put(
"objectType",
"artifact");
1234 item.put(
"artifactType", artifact.getArtifactTypeName());
1235 item.put(
"sourceObjectId", artifact.getObjectID());
1236 item.put(
"dataSourceId", artifact.getDataSourceObjectID());
1237 List<Map<String, Object>> attrList =
new ArrayList<>();
1238 for (BlackboardAttribute attr : artifact.getAttributes()) {
1239 Map<String, Object> attrMap =
new LinkedHashMap<>();
1240 attrMap.put(
"type", attr.getAttributeType().getTypeName());
1241 attrMap.put(
"value", attrValueAsObject(attr));
1242 attrList.add(attrMap);
1244 item.put(
"attributes", attrList);
1246 }
else if (c instanceof AbstractFile) {
1247 AbstractFile f = (AbstractFile) c;
1248 item.put(
"objectType",
"file");
1249 item.put(
"name", f.getName());
1251 item.put(
"path", f.getUniquePath());
1252 }
catch (TskCoreException ex) {
1253 item.put(
"path", f.getParentPath() + f.getName());
1255 item.put(
"size", f.getSize());
1256 item.put(
"mimeType", f.getMIMEType());
1257 item.put(
"fileType", f.getType().name());
1258 item.put(
"dirType", f.getDirTypeAsString());
1259 item.put(
"isDirectory", f.isDir());
1260 item.put(
"modifiedTime", epochToIso(f.getMtime()));
1261 item.put(
"createdTime", epochToIso(f.getCrtime()));
1263 }
else if (c instanceof Image) {
1264 Image img = (Image) c;
1265 item.put(
"objectType",
"image");
1266 item.put(
"name", img.getName());
1267 item.put(
"imageType", img.getType().getName());
1268 item.put(
"size", img.getSize());
1270 }
else if (c instanceof VolumeSystem) {
1271 VolumeSystem vs = (VolumeSystem) c;
1272 item.put(
"objectType",
"volumeSystem");
1273 item.put(
"vsType", vs.getType().getName());
1274 item.put(
"offset", vs.getOffset());
1275 item.put(
"blockSize", vs.getBlockSize());
1277 }
else if (c instanceof Volume) {
1278 Volume vol = (Volume) c;
1279 item.put(
"objectType",
"volume");
1280 item.put(
"addr", vol.getAddr());
1281 item.put(
"description", vol.getDescription());
1282 item.put(
"startSector", vol.getStart());
1283 item.put(
"lengthSectors", vol.getLength());
1284 item.put(
"flags", vol.getFlagsAsString());
1286 }
else if (c instanceof FileSystem) {
1287 FileSystem fs = (FileSystem) c;
1288 item.put(
"objectType",
"fileSystem");
1289 item.put(
"fsType", fs.getFsType().getDisplayName());
1290 item.put(
"imageOffset", fs.getImageOffset());
1291 item.put(
"blockSize", fs.getBlock_size());
1292 item.put(
"blockCount", fs.getBlock_count());
1295 item.put(
"objectType", c.getClass().getSimpleName());
1296 item.put(
"name", c.getName());
1306 List<Map<String, Object>> listReports() throws TskCoreException {
1307 List<Map<String, Object>> result =
new ArrayList<>();
1308 for (Report report : skCase.getAllReports()) {
1309 Map<String, Object> item =
new LinkedHashMap<>();
1310 item.put(
"objectId", report.getId());
1311 item.put(
"reportName", report.getReportName());
1312 item.put(
"sourceModule", report.getSourceModuleName());
1313 item.put(
"path", report.getPath());
1314 item.put(
"createdTime", epochToIso(report.getCreatedTime()));
1315 item.put(
"size", report.getSize());
1316 item.put(
"contentType", guessReportTypeByExtension(report.getPath()));
1326 Map<String, Object> getReportContent(JsonNode args)
throws TskCoreException, McpException {
1327 if (args.path(
"objectId").isMissingNode()) {
1328 throw new McpException(
"objectId is required");
1330 long reportId = args.path(
"objectId").asLong();
1331 long offset = args.path(
"offset").asLong(0);
1332 int maxBytes = args.path(
"maxBytes").asInt(65536);
1333 if (maxBytes <= 0 || maxBytes > 1_048_576) {
1337 Report report = skCase.getReportById(reportId);
1338 if (report ==
null) {
1339 throw new McpException(
"No report found with id " + reportId);
1342 String contentType = guessReportTypeByExtension(report.getPath());
1343 long fileSize = report.getSize();
1348 if (offset >= fileSize) {
1349 Map<String, Object> empty =
new LinkedHashMap<>();
1350 empty.put(
"objectId", reportId);
1351 empty.put(
"reportName", report.getReportName());
1352 empty.put(
"path", report.getPath());
1353 empty.put(
"fileSize", fileSize);
1354 empty.put(
"contentType", contentType);
1355 empty.put(
"offset", offset);
1356 empty.put(
"bytesRead", 0);
1357 empty.put(
"truncated",
false);
1358 empty.put(
"eof",
true);
1359 empty.put(
"content",
"");
1363 long available = fileSize - offset;
1364 int toRead = (int) Math.min(maxBytes, available);
1365 boolean truncated = available > maxBytes;
1367 byte[] buf =
new byte[toRead];
1368 int bytesRead = report.read(buf, offset, toRead);
1369 if (bytesRead < toRead) {
1370 buf = java.util.Arrays.copyOf(buf, Math.max(bytesRead, 0));
1373 CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder()
1374 .onMalformedInput(CodingErrorAction.REPLACE)
1375 .onUnmappableCharacter(CodingErrorAction.REPLACE);
1378 chars = decoder.decode(ByteBuffer.wrap(buf));
1379 }
catch (java.nio.charset.CharacterCodingException ex) {
1380 chars = CharBuffer.wrap(
new String(buf, StandardCharsets.ISO_8859_1));
1383 Map<String, Object> result =
new LinkedHashMap<>();
1384 result.put(
"objectId", reportId);
1385 result.put(
"reportName", report.getReportName());
1386 result.put(
"path", report.getPath());
1387 result.put(
"fileSize", fileSize);
1388 result.put(
"contentType", contentType);
1389 result.put(
"offset", offset);
1390 result.put(
"bytesRead", buf.length);
1391 result.put(
"truncated", truncated);
1392 result.put(
"eof", !truncated);
1393 result.put(
"content", chars.toString());
1405 private TimelineFilter.RootFilter buildTimelineFilter(JsonNode args)
throws TskCoreException {
1407 TimelineFilter.DataSourcesFilter dsFilter =
null;
1408 if (!args.path(
"dataSourceId").isMissingNode()) {
1409 long dsId = args.path(
"dataSourceId").asLong();
1410 dsFilter =
new TimelineFilter.DataSourcesFilter();
1411 dsFilter.addSubFilter(
new TimelineFilter.DataSourceFilter(
"", dsId));
1415 String text = textOrNull(args,
"textFilter");
1416 TimelineFilter.TextFilter textFilter = (text !=
null)
1417 ?
new TimelineFilter.TextFilter(text) :
null;
1420 TimelineFilter.EventTypeFilter typeFilter =
1421 new TimelineFilter.EventTypeFilter(TimelineEventType.ROOT_EVENT_TYPE);
1423 return new TimelineFilter.RootFilter(
1431 Collections.emptyList()
1438 private static long parseTimeArg(JsonNode args, String field, Long fallback) {
1439 String s = textOrNull(args, field);
1441 return fallback !=
null ? fallback : 0L;
1446 if (s.length() == 10) {
1447 boolean isEndBoundary =
"endTime".equals(field) || field.endsWith(
"Before");
1448 s = s + (isEndBoundary ?
"T23:59:59Z" :
"T00:00:00Z");
1450 return Instant.parse(s).getEpochSecond();
1453 private static final String CASE_ID_NOTE =
1454 "Note: caseId in the response reflects the currently open case — " +
1455 "if this changes between calls, alert the user.";
1457 private Map<String, Object> toolWithNote(String name, String description, Map<String, Object> properties) {
1458 return tool(name, description +
" " + CASE_ID_NOTE, properties);
1461 private Map<String, Object> toolWithNote(String name, String description, Map<String, Object> properties, List<String> required) {
1462 return tool(name, description +
" " + CASE_ID_NOTE, properties, required);
1465 private Map<String, Object> tool(String name, String description, Map<String, Object> properties) {
1468 "description", description,
1469 "inputSchema", Map.of(
1471 "properties", properties
1476 private Map<String, Object> tool(String name, String description, Map<String, Object> properties, List<String> required) {
1479 "description", description,
1480 "inputSchema", Map.of(
1482 "properties", properties,
1483 "required", required
1488 private Map<String, Object> param(String type, String description) {
1489 return Map.of(
"type", type,
"description", description);
1492 private static String textOrNull(JsonNode node, String field) {
1493 JsonNode child = node.path(field);
1494 if (child.isMissingNode() || child.isNull()) {
1497 String text = child.asText().trim();
1498 return text.isEmpty() ? null : text;
1501 private static String guessReportTypeByExtension(String path) {
1502 String p = path.toLowerCase();
1503 if (p.endsWith(
".html") || p.endsWith(
".htm") || p.endsWith(
".xhtml")) {
1506 return "text/plain";
1509 private static String escapeSql(String value) {
1510 return value.replace(
"'",
"''");
1513 private static String epochToIso(
long epochSeconds) {
1514 if (epochSeconds <= 0) {
1517 return Instant.ofEpochSecond(epochSeconds)
1518 .atOffset(ZoneOffset.UTC)
1519 .format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
1525 private static Account.Type lookupAccountType(String name) {
1526 switch (name.toUpperCase()) {
1527 case "CREDIT_CARD":
return Account.Type.CREDIT_CARD;
1528 case "DEVICE":
return Account.Type.DEVICE;
1529 case "EMAIL":
return Account.Type.EMAIL;
1530 case "FACEBOOK":
return Account.Type.FACEBOOK;
1531 case "IMO":
return Account.Type.IMO;
1532 case "INSTAGRAM":
return Account.Type.INSTAGRAM;
1533 case "LINE":
return Account.Type.LINE;
1534 case "MESSAGING_APP":
return Account.Type.MESSAGING_APP;
1535 case "PHONE":
return Account.Type.PHONE;
1536 case "SHAREIT":
return Account.Type.SHAREIT;
1537 case "SKYPE":
return Account.Type.SKYPE;
1538 case "TANGO":
return Account.Type.TANGO;
1539 case "TEXTNOW":
return Account.Type.TEXTNOW;
1540 case "THREEMA":
return Account.Type.THREEMA;
1541 case "TWITTER":
return Account.Type.TWITTER;
1542 case "VIBER":
return Account.Type.VIBER;
1543 case "WEBSITE":
return Account.Type.WEBSITE;
1544 case "WHATSAPP":
return Account.Type.WHATSAPP;
1545 case "XENDER":
return Account.Type.XENDER;
1546 case "ZAPYA":
return Account.Type.ZAPYA;
1547 default:
return null;
1554 private static TskData.FileKnown parseKnownState(String value) {
1555 switch (value.toUpperCase()) {
1556 case "KNOWN":
return TskData.FileKnown.KNOWN;
1558 case "BAD":
return TskData.FileKnown.BAD;
1559 case "UNKNOWN":
return TskData.FileKnown.UNKNOWN;
1560 default:
return null;
1568 private static Object attrValueAsObject(BlackboardAttribute attr) {
1569 switch (attr.getValueType()) {
1572 return attr.getValueString();
1574 return epochToIso(attr.getValueLong());
1576 return attr.getValueInt();
1578 return attr.getValueLong();
1580 return attr.getValueDouble();
1590 private static Object fileAttrValueAsObject(Attribute attr) {
1591 BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE valueType =
1592 attr.getAttributeType().getValueType();
1593 switch (valueType) {
1596 return attr.getValueString();
1598 return epochToIso(attr.getValueLong());
1600 return attr.getValueInt();
1602 return attr.getValueLong();
1604 return attr.getValueDouble();
1614 private static String attrValueAsString(BlackboardAttribute attr) {
1615 switch (attr.getValueType()) {
1618 String s = attr.getValueString();
1619 return s !=
null ? s :
"";
1621 String iso = epochToIso(attr.getValueLong());
1622 return iso !=
null ? iso :
"";
1624 return Integer.toString(attr.getValueInt());
1626 return Long.toString(attr.getValueLong());
1628 return Double.toString(attr.getValueDouble());