@remnux/mcp-server 0.1.73 → 0.1.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +14 -0
  2. package/dist/analysis/summarizer.d.ts +25 -1
  3. package/dist/analysis/summarizer.d.ts.map +1 -1
  4. package/dist/analysis/summarizer.js +40 -12
  5. package/dist/analysis/summarizer.js.map +1 -1
  6. package/dist/handlers/analyze-file.d.ts +15 -0
  7. package/dist/handlers/analyze-file.d.ts.map +1 -1
  8. package/dist/handlers/analyze-file.js +113 -83
  9. package/dist/handlers/analyze-file.js.map +1 -1
  10. package/dist/ioc/extractor.d.ts +4 -5
  11. package/dist/ioc/extractor.d.ts.map +1 -1
  12. package/dist/ioc/extractor.js +1 -1
  13. package/dist/ioc/extractor.js.map +1 -1
  14. package/dist/parsers/1768.d.ts +19 -0
  15. package/dist/parsers/1768.d.ts.map +1 -0
  16. package/dist/parsers/1768.js +120 -0
  17. package/dist/parsers/1768.js.map +1 -0
  18. package/dist/parsers/capa.d.ts.map +1 -1
  19. package/dist/parsers/capa.js +2 -1
  20. package/dist/parsers/capa.js.map +1 -1
  21. package/dist/parsers/diec.d.ts.map +1 -1
  22. package/dist/parsers/diec.js +2 -1
  23. package/dist/parsers/diec.js.map +1 -1
  24. package/dist/parsers/floss.d.ts.map +1 -1
  25. package/dist/parsers/floss.js +3 -1
  26. package/dist/parsers/floss.js.map +1 -1
  27. package/dist/parsers/index.d.ts +3 -3
  28. package/dist/parsers/index.d.ts.map +1 -1
  29. package/dist/parsers/index.js +8 -4
  30. package/dist/parsers/index.js.map +1 -1
  31. package/dist/parsers/types.d.ts +8 -1
  32. package/dist/parsers/types.d.ts.map +1 -1
  33. package/dist/parsers/yara.d.ts +18 -4
  34. package/dist/parsers/yara.d.ts.map +1 -1
  35. package/dist/parsers/yara.js +89 -48
  36. package/dist/parsers/yara.js.map +1 -1
  37. package/dist/tools/advisories.d.ts +3 -0
  38. package/dist/tools/advisories.d.ts.map +1 -1
  39. package/dist/tools/advisories.js +3 -12
  40. package/dist/tools/advisories.js.map +1 -1
  41. package/dist/tools/definitions.d.ts.map +1 -1
  42. package/dist/tools/definitions.js +13 -0
  43. package/dist/tools/definitions.js.map +1 -1
  44. package/dist/tools/registry.d.ts +8 -0
  45. package/dist/tools/registry.d.ts.map +1 -1
  46. package/dist/tools/registry.js +7 -0
  47. package/dist/tools/registry.js.map +1 -1
  48. package/package.json +1 -1
package/README.md CHANGED
@@ -284,6 +284,20 @@ claude mcp add remnux --transport http http://REMNUX_IP:3000/mcp \
284
284
 
285
285
  **Auto-summarization:** When total tool output exceeds ~32KB, `analyze_file` automatically switches to summary mode to prevent LLM context overflow — key findings per tool, full IOC extraction, and paths to saved full outputs for drill-down via `download_file`.
286
286
 
287
+ **IOC list:** Hash-shaped values in tool output (MD5, SHA-1, SHA-256, SHA-512, ssdeep) are not counted in `analyze_file`'s `iocs` list. A hash in tool output is almost always one a tool computed, such as a section hash or an imphash. Those hashes stay in each tool's output, and `extract_iocs` still reports hashes in the text it is given.
288
+
289
+ **Tool status (summary mode):** each tool entry in the summary has a `status`.
290
+
291
+ | Status | Meaning |
292
+ |---|---|
293
+ | `findings` | The tool's parser resolved at least one finding |
294
+ | `clean` | The tool's parser read the output and resolved nothing. A benign verdict needs other evidence |
295
+ | `not_assessed` | No parser reads this tool's output, so the server did not interpret it. Its `key_lines` are raw excerpts |
296
+ | `error` | The tool exited non-zero or reported its own failure (`tool_reported_error`), or its parser could not read the output (`parse_failed`) |
297
+ | `timeout` | The tool exited non-zero and its output mentions a timeout |
298
+
299
+ *Compatibility note:* `not_assessed` is new. Tools without a parser used to report `clean`. A client that switches exhaustively over the status values needs a branch for it.
300
+
287
301
  **Preprocessing:** Before analysis, `analyze_file` checks for conditions that prevent effective analysis (encrypted Office docs, bloated PEs, PyInstaller bundles) and applies automatic fixes. Results appear in the `preprocessing` field.
288
302
 
289
303
  ### Example: run_tool
@@ -14,6 +14,12 @@ interface ToolRun {
14
14
  full_output_length?: number;
15
15
  findings?: Finding[];
16
16
  metadata?: Record<string, unknown>;
17
+ /** Output file the server saved for this tool. */
18
+ saved_output_file?: string;
19
+ /** The tool's parser could not read its output. */
20
+ parse_failed?: boolean;
21
+ /** The tool exited 0 but reported a failure of its own. */
22
+ tool_reported_error?: boolean;
17
23
  }
18
24
  interface IOC {
19
25
  type: string;
@@ -44,9 +50,21 @@ interface PreprocessResult {
44
50
  outputPath?: string;
45
51
  error?: string;
46
52
  }
53
+ /**
54
+ * `clean` means the tool's parser read its output and resolved nothing.
55
+ * `not_assessed` means no parser reads this tool's output, so nothing interpreted it.
56
+ */
57
+ export type ToolStatus = "findings" | "clean" | "not_assessed" | "error" | "timeout";
47
58
  export interface ToolSummary {
48
59
  name: string;
49
- status: "findings" | "clean" | "error" | "timeout";
60
+ status: ToolStatus;
61
+ /** Present when the tool's parser could not read its output; status is then "error". */
62
+ parse_failed?: true;
63
+ /**
64
+ * Present when the tool exited 0 but reported a failure of its own. Status is
65
+ * "error", or "findings" when the tool also recovered something.
66
+ */
67
+ tool_reported_error?: true;
50
68
  key_lines: string[];
51
69
  finding_count?: number;
52
70
  output_size: number;
@@ -120,6 +138,12 @@ export declare function shouldSummarize(toolsRun: ToolRun[]): boolean;
120
138
  * - Repetitive lines
121
139
  */
122
140
  export declare function extractKeyLines(output: string, limit?: number): string[];
141
+ /**
142
+ * The lines that represent a tool in summary mode. A parsed tool's findings come
143
+ * first, because the generic scorer can miss them (a YARA match line scores zero),
144
+ * followed by the scorer's picks from the raw output.
145
+ */
146
+ export declare function deriveKeyLines(tool: ToolRun): string[];
123
147
  /**
124
148
  * Generate a summarized analysis response.
125
149
  */
@@ -1 +1 @@
1
- {"version":3,"file":"summarizer.d.ts","sourceRoot":"","sources":["../../src/analysis/summarizer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"summarizer.d.ts","sourceRoot":"","sources":["../../src/analysis/summarizer.ts"],"names":[],"mappings":"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"}
@@ -4,6 +4,8 @@
4
4
  * When total output exceeds the budget, generates a summary with key findings
5
5
  * per tool while preserving IOCs and providing paths to full outputs.
6
6
  */
7
+ import { hasParser } from "../parsers/index.js";
8
+ import { exitCodeIsFailure } from "../tools/registry.js";
7
9
  /**
8
10
  * Separate capability findings (e.g. capa) by the kind of evidence they matched
9
11
  * on, using each finding's evidence_types tag. This is a pure reorganization of
@@ -130,11 +132,37 @@ export function extractKeyLines(output, limit = MAX_KEY_LINES_PER_TOOL) {
130
132
  scored.sort((a, b) => b.score - a.score);
131
133
  return scored.slice(0, limit).map((s) => s.line);
132
134
  }
135
+ /** Findings shown as key lines for a parsed tool. Overflow is counted, never silent. */
136
+ const MAX_FINDING_KEY_LINES = 20;
137
+ /** Per-line cap, since a finding description can quote sample text. */
138
+ const MAX_KEY_LINE_CHARS = 300;
139
+ const capLine = (line) => line.length > MAX_KEY_LINE_CHARS ? `${line.slice(0, MAX_KEY_LINE_CHARS)}…` : line;
140
+ /**
141
+ * The lines that represent a tool in summary mode. A parsed tool's findings come
142
+ * first, because the generic scorer can miss them (a YARA match line scores zero),
143
+ * followed by the scorer's picks from the raw output.
144
+ */
145
+ export function deriveKeyLines(tool) {
146
+ const findings = tool.findings ?? [];
147
+ if (findings.length === 0)
148
+ return extractKeyLines(tool.output);
149
+ const lines = findings.slice(0, MAX_FINDING_KEY_LINES).map((f) => capLine(f.description));
150
+ if (findings.length > MAX_FINDING_KEY_LINES) {
151
+ lines.push(`… and ${findings.length - MAX_FINDING_KEY_LINES} more finding(s)`);
152
+ }
153
+ const seen = new Set(lines.map((l) => l.toLowerCase()));
154
+ for (const line of extractKeyLines(tool.output)) {
155
+ const capped = capLine(line);
156
+ if (!seen.has(capped.toLowerCase()))
157
+ lines.push(capped);
158
+ }
159
+ return lines;
160
+ }
133
161
  /**
134
162
  * Determine the status of a tool run.
135
163
  */
136
164
  function getToolStatus(tool) {
137
- if (tool.exit_code !== 0) {
165
+ if (exitCodeIsFailure(tool.name, tool.exit_code)) {
138
166
  if (tool.output?.toLowerCase().includes("timeout"))
139
167
  return "timeout";
140
168
  // Some tools exit non-zero but still produce findings
@@ -144,6 +172,11 @@ function getToolStatus(tool) {
144
172
  }
145
173
  if (tool.findings && tool.findings.length > 0)
146
174
  return "findings";
175
+ if (tool.parse_failed || tool.tool_reported_error)
176
+ return "error";
177
+ // "clean" would claim a reading of output that nothing interpreted.
178
+ if (!hasParser(tool.name))
179
+ return "not_assessed";
147
180
  return "clean";
148
181
  }
149
182
  /**
@@ -154,22 +187,17 @@ export function generateSummary(file, detectedType, category, depth, triageSumma
154
187
  const savedFiles = [];
155
188
  for (const tool of toolsRun) {
156
189
  const status = getToolStatus(tool);
157
- const keyLines = extractKeyLines(tool.output);
158
- // Track which files were saved for full output retrieval. The marker is
159
- // written by analyze_file's truncation path as
160
- // "Saved in full as %OUTPUT%/<file> (...)" (legacy form: "Full output: output/<file>").
161
- // Only trust the marker when the server actually truncated this tool's
162
- // output: sample-derived text could otherwise forge a saved_to entry.
163
- const savedMatch = tool.truncated
164
- ? (tool.output?.match(/Saved in full as %OUTPUT%\/([^\s\](]+)/) ??
165
- tool.output?.match(/Full output: output\/([^\s\]]+)/))
166
- : null;
167
- const savedTo = savedMatch ? savedMatch[1] : undefined;
190
+ const keyLines = deriveKeyLines(tool);
191
+ // Set by analyze_file whenever it saved this tool's output, never read from
192
+ // tool text, so sample content cannot forge it.
193
+ const savedTo = tool.saved_output_file;
168
194
  if (savedTo)
169
195
  savedFiles.push(savedTo);
170
196
  toolSummaries.push({
171
197
  name: tool.name,
172
198
  status,
199
+ ...(tool.parse_failed && { parse_failed: true }),
200
+ ...(tool.tool_reported_error && { tool_reported_error: true }),
173
201
  key_lines: keyLines,
174
202
  ...(tool.findings && tool.findings.length > 0 && { finding_count: tool.findings.length }),
175
203
  output_size: tool.output?.length || 0,
@@ -1 +1 @@
1
- {"version":3,"file":"summarizer.js","sourceRoot":"","sources":["../../src/analysis/summarizer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"summarizer.js","sourceRoot":"","sources":["../../src/analysis/summarizer.ts"],"names":[],"mappings":"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"}
@@ -11,6 +11,12 @@ interface ToolRun {
11
11
  full_output_length?: number;
12
12
  findings?: Finding[];
13
13
  metadata?: Record<string, unknown>;
14
+ /** Output file the server saved for this tool. */
15
+ saved_output_file?: string;
16
+ /** The tool's parser could not read its output. */
17
+ parse_failed?: boolean;
18
+ /** The tool exited 0 but reported a failure of its own. */
19
+ tool_reported_error?: boolean;
14
20
  }
15
21
  interface ToolSkipped {
16
22
  name: string;
@@ -20,8 +26,17 @@ interface ToolSkipped {
20
26
  /** Categorizes why the tool was skipped for clearer UX */
21
27
  skip_type: "not_installed" | "not_applicable" | "requires_user_args";
22
28
  }
29
+ /**
30
+ * Name for a tool-output file the server writes into the output directory.
31
+ * Sanitizing the sample name is lossy (every non-Latin letter becomes "_"), so a
32
+ * short hash of the original name keeps two samples from overwriting each other's
33
+ * output. The whole name stays within one 255-byte path component.
34
+ */
35
+ export declare function spillFilename(toolName: string, sampleName: string): string;
23
36
  /** Generate suggested next steps based on file category and analysis results */
24
37
  export declare function generateNextSteps(category: string, depth: DepthTier, toolsRun: ToolRun[], toolsSkipped: ToolSkipped[], iocCount: number): string[];
38
+ /** Generate a brief triage summary from analysis results */
39
+ export declare function generateTriageSummary(category: string, toolsRun: ToolRun[], iocCount: number): string;
25
40
  /**
26
41
  * Parsing hints for querying saved (oversized) tool output with jq/grep.
27
42
  * Every hint is a runnable run_tool command: `%OUTPUT%/<file>` is the sentinel
@@ -1 +1 @@
1
- {"version":3,"file":"analyze-file.d.ts","sourceRoot":"","sources":["../../src/handlers/analyze-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAG3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAI1D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAgBnD,UAAU,OAAO;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,UAAU,WAAW;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,0DAA0D;IAC1D,SAAS,EAAE,eAAe,GAAG,gBAAgB,GAAG,oBAAoB,CAAC;CACtE;AAED,gFAAgF;AAChF,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,SAAS,EAChB,QAAQ,EAAE,OAAO,EAAE,EACnB,YAAY,EAAE,WAAW,EAAE,EAC3B,QAAQ,EAAE,MAAM,GACf,MAAM,EAAE,CAyFV;AA+ID;;;;;GAKG;AACH,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CA0BlD,CAAC;AAEF,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,WAAW,EACjB,IAAI,EAAE,eAAe;;;;;;GAqctB"}
1
+ {"version":3,"file":"analyze-file.d.ts","sourceRoot":"","sources":["../../src/handlers/analyze-file.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAG3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAI1D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAgBnD,UAAU,OAAO;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,kDAAkD;IAClD,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,mDAAmD;IACnD,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,2DAA2D;IAC3D,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,UAAU,WAAW;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,0DAA0D;IAC1D,SAAS,EAAE,eAAe,GAAG,gBAAgB,GAAG,oBAAoB,CAAC;CACtE;AAeD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAK1E;AAED,gFAAgF;AAChF,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,SAAS,EAChB,QAAQ,EAAE,OAAO,EAAE,EACnB,YAAY,EAAE,WAAW,EAAE,EAC3B,QAAQ,EAAE,MAAM,GACf,MAAM,EAAE,CAwFV;AAED,4DAA4D;AAC5D,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,OAAO,EAAE,EACnB,QAAQ,EAAE,MAAM,GACf,MAAM,CAsFR;AAmCD;;;;;GAKG;AACH,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CA0BlD,CAAC;AAEF,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,WAAW,EACjB,IAAI,EAAE,eAAe;;;;;;GA6ctB"}
@@ -1,7 +1,8 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { saveOversizedOutput } from "./output-spill.js";
2
3
  import { validateFilePath } from "../security/blocklist.js";
3
4
  import { matchFileType, CATEGORY_TAG_MAP } from "../file-type-mappings.js";
4
- import { toolRegistry } from "../tools/registry.js";
5
+ import { toolRegistry, exitCodeIsFailure } from "../tools/registry.js";
5
6
  import { buildCommandFromDefinition, resolveOutputPath, buildInvocationTemplate } from "../tools/invoker.js";
6
7
  import { parseToolOutput } from "../parsers/index.js";
7
8
  import { formatResponse, formatError } from "../response.js";
@@ -15,9 +16,34 @@ import { checkFileExists } from "../utils/check-file-exists.js";
15
16
  import { getPreprocessors } from "../tools/preprocessors.js";
16
17
  import { shouldSummarize, generateSummary, summarizeCapabilityEvidence } from "../analysis/index.js";
17
18
  import { evaluateAdvisories, } from "../tools/advisories.js";
19
+ const YARA_TOOLS = new Set(["yara-forge", "yara-rules"]);
20
+ /** diec detections (its "type: name" findings) that earn a container or packer label. */
21
+ const CONTAINER_LABELS = [
22
+ [/^sfx:.*Microsoft Cabinet/i, "Microsoft Cabinet SFX"],
23
+ [/Nullsoft|NSIS/i, "NSIS installer"],
24
+ [/Inno Setup/i, "Inno Setup"],
25
+ [/PyInstaller/i, "PyInstaller"],
26
+ [/AutoIt/i, "AutoIt compiled"],
27
+ [/Themida|VMProtect|Enigma/i, "protected"],
28
+ [/\bUPX\b/i, "UPX packed"],
29
+ ];
30
+ /**
31
+ * Name for a tool-output file the server writes into the output directory.
32
+ * Sanitizing the sample name is lossy (every non-Latin letter becomes "_"), so a
33
+ * short hash of the original name keeps two samples from overwriting each other's
34
+ * output. The whole name stays within one 255-byte path component.
35
+ */
36
+ export function spillFilename(toolName, sampleName) {
37
+ const suffix = createHash("sha256").update(sampleName).digest("hex").slice(0, 8);
38
+ const room = 255 - toolName.length - suffix.length - "--.txt".length;
39
+ const safe = sampleName.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, Math.max(1, room));
40
+ return `${toolName}-${safe}-${suffix}.txt`;
41
+ }
18
42
  /** Generate suggested next steps based on file category and analysis results */
19
43
  export function generateNextSteps(category, depth, toolsRun, toolsSkipped, iocCount) {
20
44
  const steps = [];
45
+ // A generic step that repeats a command this run already executed adds nothing.
46
+ const ran = (name) => toolsRun.some(t => t.name === name);
21
47
  // Depth-based suggestions
22
48
  if (depth === "quick") {
23
49
  steps.push("Run with depth='standard' for more thorough analysis (capa, YARA rules, etc.)");
@@ -29,11 +55,13 @@ export function generateNextSteps(category, depth, toolsRun, toolsSkipped, iocCo
29
55
  switch (category) {
30
56
  case "PE":
31
57
  case "DOTNET":
32
- if (!toolsRun.some(t => t.name === "capa" && t.findings && t.findings.length > 0)) {
58
+ if (!ran("upx-decompress") && !toolsRun.some(t => t.name === "capa" && t.findings && t.findings.length > 0)) {
33
59
  steps.push("File may be packed — try unpacking with 'upx -d' or specialized unpackers before re-analysis");
34
60
  }
35
- steps.push("For dynamic analysis: use run_tool with 'speakeasy -t <file>' to emulate execution");
36
- steps.push("Extract strings: run_tool command='pestr <file>' (extracts both ASCII and Unicode with section info)");
61
+ if (!ran("speakeasy"))
62
+ steps.push("For dynamic analysis: use run_tool with 'speakeasy -t <file>' to emulate execution");
63
+ if (!ran("pestr"))
64
+ steps.push("Extract strings: run_tool command='pestr <file>' (extracts both ASCII and Unicode with section info)");
37
65
  steps.push("If this is a self-extracting archive (SFX/dropper): run_tool command='7z l <file>' to list embedded contents, then 'run_tool command=7z x <file> -o%OUTPUT%/sfx -y' to unpack them");
38
66
  break;
39
67
  case "PDF":
@@ -43,15 +71,21 @@ export function generateNextSteps(category, depth, toolsRun, toolsSkipped, iocCo
43
71
  case "OLE2":
44
72
  case "OOXML":
45
73
  steps.push("Extract specific macro streams: run_tool command='oledump.py -s <stream_num> -v <file>'");
46
- steps.push("Decode VBA p-code for stomped macros: run_tool command='pcodedmp <file>'");
74
+ if (!ran("pcodedmp"))
75
+ steps.push("Decode VBA p-code for stomped macros: run_tool command='pcodedmp <file>'");
47
76
  break;
48
77
  case "Shellcode":
49
- steps.push("Emulate 32-bit shellcode: run_tool command='speakeasy -t <file> -r -a x86'");
50
- steps.push("Emulate 64-bit shellcode: run_tool command='speakeasy -t <file> -r -a amd64'");
78
+ if (!ran("speakeasy-sc-x86"))
79
+ steps.push("Emulate 32-bit shellcode: run_tool command='speakeasy -t <file> -r -a x86'");
80
+ if (!ran("speakeasy-sc-x64"))
81
+ steps.push("Emulate 64-bit shellcode: run_tool command='speakeasy -t <file> -r -a amd64'");
51
82
  break;
52
83
  case "DataWithPEExtension":
53
- steps.push("Emulate as shellcode: run_tool command='speakeasy -t <file> -r -a x86' (try amd64 if no output)");
54
- steps.push("Check for Cobalt Strike beacon: run_tool command='1768.py <file>'");
84
+ // The step covers both architectures, so it is stale only when both ran.
85
+ if (!(ran("speakeasy-sc-x86") && ran("speakeasy-sc-x64")))
86
+ steps.push("Emulate as shellcode: run_tool command='speakeasy -t <file> -r -a x86' (try amd64 if no output)");
87
+ if (!ran("1768"))
88
+ steps.push("Check for Cobalt Strike beacon: run_tool command='1768.py <file>'");
55
89
  steps.push("Extract encoded content: run_tool command='base64dump.py -n 20 <file>'");
56
90
  break;
57
91
  case "PCAP":
@@ -77,37 +111,37 @@ export function generateNextSteps(category, depth, toolsRun, toolsSkipped, iocCo
77
111
  // NOTE: Cross-tool conditions like "autoit-ripper failed + diec detected AutoIt"
78
112
  // are now handled by the advisory framework (see advisories.ts).
79
113
  // This keeps generateNextSteps focused on category/depth suggestions.
80
- // IOC-based suggestions
114
+ // Pointers to this run's own results come after the cap on generic suggestions,
115
+ // so a full generic list cannot evict them.
116
+ const runSpecific = [];
81
117
  if (iocCount > 0) {
82
- steps.push("Extracted IOCs are in the 'iocs' field — consider threat intel lookup for network indicators");
118
+ runSpecific.push("Extracted IOCs are in the 'iocs' field — consider threat intel lookup for network indicators");
83
119
  }
84
- // Tool-not-installed suggestions
85
120
  const notInstalled = toolsSkipped.filter(t => t.skip_type === "not_installed");
86
121
  if (notInstalled.length > 0) {
87
- steps.push(`${notInstalled.length} tool(s) not installed — run check_tools to see installation status`);
122
+ runSpecific.push(`${notInstalled.length} tool(s) not installed — run check_tools to see installation status`);
88
123
  }
89
- // Report-drafting pointer — only when the analysis produced something worth reporting,
90
- // so empty triage runs stay quiet. Cap the base steps to 5 FIRST, then append the
91
- // pointer, so it can never be truncated away in the very case it's meant to fire
92
- // (a busy PE run can otherwise accumulate 6 base steps and evict the pointer).
124
+ // Report-drafting pointer, only when the analysis produced something worth reporting.
93
125
  const hasSubstantiveResults = iocCount > 0 || toolsRun.some(t => t.findings && t.findings.length > 0);
94
126
  if (hasSubstantiveResults) {
95
- const reportPointer = "Draft a report: get_report_template and get_report_guidance provide a bundled report template and writing " +
96
- "guidelines (offline); treat (Optional) section markers as conditional, not literal headings.";
97
- return [...steps.slice(0, 5), reportPointer];
127
+ runSpecific.push("Draft a report: get_report_template and get_report_guidance provide a bundled report template and writing " +
128
+ "guidelines (offline); treat (Optional) section markers as conditional, not literal headings.");
98
129
  }
99
- return steps.slice(0, 5); // Limit to 5 most relevant suggestions
130
+ return [...steps.slice(0, 5), ...runSpecific];
100
131
  }
101
132
  /** Generate a brief triage summary from analysis results */
102
- function generateTriageSummary(category, toolsRun, iocCount) {
133
+ export function generateTriageSummary(category, toolsRun, iocCount) {
103
134
  const findings = [];
104
135
  // Count findings by severity
136
+ let criticalCount = 0;
105
137
  let highCount = 0;
106
138
  let mediumCount = 0;
107
139
  for (const tool of toolsRun) {
108
140
  if (tool.findings) {
109
141
  for (const f of tool.findings) {
110
- if (f.severity === "high")
142
+ if (f.severity === "critical")
143
+ criticalCount++;
144
+ else if (f.severity === "high")
111
145
  highCount++;
112
146
  else if (f.severity === "medium")
113
147
  mediumCount++;
@@ -119,19 +153,13 @@ function generateTriageSummary(category, toolsRun, iocCount) {
119
153
  const hasMacros = toolsRun.some(t => t.name === "olevba" && t.output && !t.output.includes("No VBA macros found"));
120
154
  const hasPackerDetection = toolsRun.some(t => t.name === "diec" && t.output && /packer|protector|crypter/i.test(t.output));
121
155
  const hasAnomaly = toolsRun.some(t => (t.name === "portex" || t.name === "pescan") && t.findings && t.findings.length > 0);
122
- const hasYaraMatches = toolsRun.some(t => t.name === "yara-rules" && t.output && t.output.trim().length > 0 && !t.output.includes("No matches"));
123
- const hasFamilyDetection = toolsRun.some(t => {
124
- if (t.name !== "yara-forge" || !t.output)
125
- return false;
126
- // Check for actual YARA matches: lines that aren't warnings/errors
127
- const lines = t.output.trim().split("\n");
128
- return lines.some(line => {
129
- const trimmed = line.trim();
130
- return trimmed.length > 0 &&
131
- !trimmed.startsWith("warning:") &&
132
- !trimmed.startsWith("error:");
133
- });
134
- });
156
+ // Read the rules each YARA parser resolved. Testing the output text instead read
157
+ // the server's own "(no output)" placeholder as a match on every unmatched scan.
158
+ const yaraMatched = (name) => toolsRun.some(t => t.name === name && (t.findings?.length ?? 0) > 0);
159
+ const hasYaraMatches = yaraMatched("yara-rules");
160
+ const hasFamilyDetection = yaraMatched("yara-forge");
161
+ // Only a configuration that passed 1768's own sanity check earns the token.
162
+ const hasCobaltStrikeConfig = toolsRun.some(t => t.name === "1768" && (t.findings ?? []).some(f => f.category === "cobalt-strike-config" && f.severity === "high"));
135
163
  // Detect shellcode loader/stub pattern: no imports + W+X section + low entropy
136
164
  const hasNoImports = toolsRun.some(t => t.name === "yara-rules" && t.output?.includes("ImportTableIsBad"));
137
165
  const hasWriteExecute = toolsRun.some(t => (t.name === "portex" || t.name === "pescan") &&
@@ -140,28 +168,15 @@ function generateTriageSummary(category, toolsRun, iocCount) {
140
168
  const isShellcodeLoaderPattern = hasNoImports && hasWriteExecute && hasLowEntropy;
141
169
  // Build summary
142
170
  findings.push(`File type: ${category}`);
143
- // Surface key container/packer detections from ALL tool outputs
144
- const allOutput = toolsRun.map(t => t.output || "").join(" ");
145
- if (/IExpress|WEXTRACT|Cabinet Self-Extractor/i.test(allOutput)) {
146
- findings.push("IExpress SFX");
147
- }
148
- if (/NSIS|Nullsoft/i.test(allOutput)) {
149
- findings.push("NSIS installer");
150
- }
151
- if (/Inno\s*Setup/i.test(allOutput)) {
152
- findings.push("Inno Setup");
153
- }
154
- if (/PyInstaller/i.test(allOutput)) {
155
- findings.push("PyInstaller");
156
- }
157
- if (/AutoIt|AU3!/i.test(allOutput)) {
158
- findings.push("AutoIt compiled");
159
- }
160
- if (/Themida|VMProtect|Enigma/i.test(allOutput)) {
161
- findings.push("protected");
162
- }
163
- if (/\bUPX\b/i.test(allOutput)) {
164
- findings.push("UPX packed");
171
+ // Container and packer labels come from diec, the tool whose job is identifying
172
+ // them. Matching every tool's text instead picked up unpackers' own failure
173
+ // messages ("not packed by UPX") and echoes of the sample's name.
174
+ const diecDetections = toolsRun
175
+ .filter(t => t.name === "diec")
176
+ .flatMap(t => (t.findings ?? []).map(f => f.description));
177
+ for (const [pattern, label] of CONTAINER_LABELS) {
178
+ if (diecDetections.some(d => pattern.test(d)))
179
+ findings.push(label);
165
180
  }
166
181
  if (isShellcodeLoaderPattern)
167
182
  findings.push("⚠️ Shellcode loader pattern (no imports + W+X section + low entropy)");
@@ -173,18 +188,22 @@ function generateTriageSummary(category, toolsRun, iocCount) {
173
188
  findings.push("Notable capabilities identified");
174
189
  if (hasMacros)
175
190
  findings.push("VBA macros present");
191
+ if (hasCobaltStrikeConfig)
192
+ findings.push("Cobalt Strike configuration recovered (1768)");
176
193
  if (hasFamilyDetection)
177
194
  findings.push("YARA family signature matched");
178
195
  if (hasYaraMatches)
179
196
  findings.push("YARA rules matched");
180
- if (highCount > 0)
197
+ if (criticalCount > 0)
198
+ findings.push(`${criticalCount} critical-severity finding(s)`);
199
+ else if (highCount > 0)
181
200
  findings.push(`${highCount} high-severity finding(s)`);
182
201
  else if (mediumCount > 0)
183
202
  findings.push(`${mediumCount} medium-severity finding(s)`);
184
203
  if (iocCount > 0)
185
204
  findings.push(`${iocCount} IOC(s) extracted`);
186
- const toolsSucceeded = toolsRun.length;
187
- findings.push(`${toolsSucceeded} tool(s) completed`);
205
+ // Every tool that returned, including those that failed, so "ran" rather than "completed".
206
+ findings.push(`${toolsRun.length} tool(s) ran`);
188
207
  return findings.join(" | ");
189
208
  }
190
209
  const DEFAULT_OUTPUT_BUDGET = 40 * 1024; // 40KB default
@@ -286,20 +305,6 @@ export async function handleAnalyzeFile(deps, args) {
286
305
  const msg = `Error running file command: ${error instanceof Error ? error.message : "Unknown error"}`;
287
306
  return formatError("analyze_file", new REMnuxError(msg, "EMPTY_OUTPUT", "tool_failure", "Check that the file exists and is readable"), startTime);
288
307
  }
289
- // Compute the file's own hashes so we can filter them from IOC results
290
- const ownHashes = new Set();
291
- try {
292
- const hashResult = await connector.execute(["sh", "-c", `md5sum '${filePath.replace(/'/g, "'\\''")}' && sha1sum '${filePath.replace(/'/g, "'\\''")}' && sha256sum '${filePath.replace(/'/g, "'\\''")}'`], { timeout: 30000 });
293
- if (hashResult.exitCode === 0) {
294
- for (const line of hashResult.stdout.split("\n")) {
295
- const hash = line.trim().split(/\s+/)[0];
296
- if (hash && /^[a-fA-F0-9]{32,128}$/.test(hash)) {
297
- ownHashes.add(hash.toLowerCase());
298
- }
299
- }
300
- }
301
- }
302
- catch { /* best effort — if hashing fails, we just skip filtering */ }
303
308
  // Step 2: Match to category and get tools from registry by tag + depth
304
309
  const category = matchFileType(fileOutput, normalizedFile);
305
310
  const tag = CATEGORY_TAG_MAP[category.name] ?? "fallback";
@@ -419,8 +424,9 @@ export async function handleAnalyzeFile(deps, args) {
419
424
  continue;
420
425
  }
421
426
  // Detect missing tools — only match shell "command not found" or exit code 127,
422
- // not tool output that happens to contain "not found" (e.g., pescan "section not found")
423
- const isNotInstalled = result.exitCode === 127 ||
427
+ // not tool output that happens to contain "not found" (e.g., pescan "section not found").
428
+ // A tool whose exit code carries a result can exit 127 by chance, so ask its definition.
429
+ const isNotInstalled = (result.exitCode === 127 && exitCodeIsFailure(tool.name, 127)) ||
424
430
  /command not found/i.test(stderr) ||
425
431
  (result.exitCode !== 0 && /^.*: No such file or directory$/m.test(stderr) && stderr.includes(tool.command));
426
432
  if (isNotInstalled) {
@@ -448,8 +454,7 @@ export async function handleAnalyzeFile(deps, args) {
448
454
  let savedOutputFile;
449
455
  if (outputTruncated) {
450
456
  // Save full output to output dir for later retrieval (if under size limit)
451
- const safeFile = args.file.replace(/[^a-zA-Z0-9._-]/g, "_");
452
- const outFilename = `${tool.name}-${safeFile}.txt`;
457
+ const outFilename = spillFilename(tool.name, args.file);
453
458
  // Non-fatal: on any failure the truncation hint simply carries no file reference
454
459
  const spill = await saveOversizedOutput(connector, config.outputDir, outFilename, output);
455
460
  if (spill.saved)
@@ -472,7 +477,17 @@ export async function handleAnalyzeFile(deps, args) {
472
477
  output = output.slice(0, budget) + truncationMsg;
473
478
  }
474
479
  totalOutputSize += output.length;
475
- const parsed = parseToolOutput(tool.name, output);
480
+ // Parse the tool's complete output, never the display copy: the cut loses every
481
+ // finding past the budget, and cut JSON (capa -j) does not parse at all.
482
+ const parsed = parseToolOutput(tool.name, result.stdout || stderr, { targetPath: analysisPath, stderr });
483
+ // A YARA match list is the run's attribution evidence and too small to reach
484
+ // the truncation spill, so save it explicitly: summary mode shows only key lines.
485
+ if (!savedOutputFile && YARA_TOOLS.has(tool.name) && parsed.findings.length > 0) {
486
+ const outFilename = spillFilename(tool.name, args.file);
487
+ const spill = await saveOversizedOutput(connector, config.outputDir, outFilename, fullOutput);
488
+ if (spill.saved)
489
+ savedOutputFile = outFilename;
490
+ }
476
491
  // Check for tool-specific exit code hints
477
492
  const extraMetadata = {};
478
493
  const hint = tool.exitCodeHints?.[result.exitCode];
@@ -510,6 +525,10 @@ export async function handleAnalyzeFile(deps, args) {
510
525
  output,
511
526
  exit_code: result.exitCode,
512
527
  ...(outputTruncated && { truncated: true, full_output_length: fullLen }),
528
+ ...(savedOutputFile && { saved_output_file: savedOutputFile }),
529
+ // Only at exit 0: a tool that exited non-zero refused the file, which is not a parser failure.
530
+ ...(parsed.metadata?.parse_error === true && result.exitCode === 0 && { parse_failed: true }),
531
+ ...(parsed.metadata?.tool_reported_error === true && { tool_reported_error: true }),
513
532
  ...(parsed.parsed && {
514
533
  findings: parsed.findings,
515
534
  metadata: { ...parsed.metadata, ...extraMetadata },
@@ -529,9 +548,13 @@ export async function handleAnalyzeFile(deps, args) {
529
548
  }
530
549
  const combinedOutput = iocScanOutputs.join("\n\n")
531
550
  .replace(/^\s*"command":\s*".*"$/gm, "");
532
- // The file's own hashes are excluded during extraction rather than after it, so an excluded
533
- // hash cannot take a slot under the per-type cap and hide a real indicator behind it.
534
- const iocResult = extractIOCs(combinedOutput, ownHashes.size > 0 ? { exclude: ownHashes } : undefined);
551
+ // A hash-shaped value in tool output is almost always one a tool computed (a section hash, the
552
+ // imphash, the file's ssdeep, a decoder's per-chunk MD5), not an indicator found in the sample.
553
+ // Those hashes stay visible in each tool's output, and extract_iocs still reports hashes in text.
554
+ // This also covers the file's own hashes.
555
+ const iocResult = extractIOCs(combinedOutput, {
556
+ excludeTypes: new Set(["md5", "sha1", "sha256", "sha512", "ssdeep"]),
557
+ });
535
558
  // Generate triage summary and next steps
536
559
  const triageSummary = generateTriageSummary(category.name, toolsRun, iocResult.iocs.length);
537
560
  const suggestedNextSteps = generateNextSteps(category.name, depth, toolsRun, toolsSkipped, iocResult.iocs.length);
@@ -549,6 +572,7 @@ export async function handleAnalyzeFile(deps, args) {
549
572
  name: t.name,
550
573
  exit_code: t.exit_code,
551
574
  output: fullOutputs[i] ?? t.output,
575
+ ...(t.findings && { findings: t.findings }),
552
576
  })),
553
577
  toolsFailed: toolsFailed.map((t) => ({
554
578
  name: t.name,
@@ -574,6 +598,12 @@ export async function handleAnalyzeFile(deps, args) {
574
598
  "explainable as normal development practices? " +
575
599
  "(3) What concrete evidence distinguishes this from a benign program? " +
576
600
  "State your confidence level (low/medium/high) and what evidence supports or contradicts a malicious verdict. " +
601
+ "TOOL STATUS (summary mode): 'clean' means the tool's parser read its output and found nothing " +
602
+ "notable. A benign verdict needs other evidence. 'not_assessed' means no parser reads that tool's output, " +
603
+ "so the server did not interpret it. Its key_lines are raw excerpts. 'error' without either flag " +
604
+ "below means the tool exited non-zero. 'error' with parse_failed " +
605
+ "means the parser could not read the output. 'error' with tool_reported_error means the tool " +
606
+ "exited 0 but reported a failure of its own. " +
577
607
  "ATTRIBUTION AND CLASSIFICATION: " +
578
608
  "YARA family signatures (yara-forge) indicate resemblance to known families, not confirmed identity — " +
579
609
  "signatures can match shared code, libraries, or techniques reused across unrelated families. " +