@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.
- package/README.md +14 -0
- package/dist/analysis/summarizer.d.ts +25 -1
- package/dist/analysis/summarizer.d.ts.map +1 -1
- package/dist/analysis/summarizer.js +40 -12
- package/dist/analysis/summarizer.js.map +1 -1
- package/dist/handlers/analyze-file.d.ts +15 -0
- package/dist/handlers/analyze-file.d.ts.map +1 -1
- package/dist/handlers/analyze-file.js +113 -83
- package/dist/handlers/analyze-file.js.map +1 -1
- package/dist/ioc/extractor.d.ts +4 -5
- package/dist/ioc/extractor.d.ts.map +1 -1
- package/dist/ioc/extractor.js +1 -1
- package/dist/ioc/extractor.js.map +1 -1
- package/dist/parsers/1768.d.ts +19 -0
- package/dist/parsers/1768.d.ts.map +1 -0
- package/dist/parsers/1768.js +120 -0
- package/dist/parsers/1768.js.map +1 -0
- package/dist/parsers/capa.d.ts.map +1 -1
- package/dist/parsers/capa.js +2 -1
- package/dist/parsers/capa.js.map +1 -1
- package/dist/parsers/diec.d.ts.map +1 -1
- package/dist/parsers/diec.js +2 -1
- package/dist/parsers/diec.js.map +1 -1
- package/dist/parsers/floss.d.ts.map +1 -1
- package/dist/parsers/floss.js +3 -1
- package/dist/parsers/floss.js.map +1 -1
- package/dist/parsers/index.d.ts +3 -3
- package/dist/parsers/index.d.ts.map +1 -1
- package/dist/parsers/index.js +8 -4
- package/dist/parsers/index.js.map +1 -1
- package/dist/parsers/types.d.ts +8 -1
- package/dist/parsers/types.d.ts.map +1 -1
- package/dist/parsers/yara.d.ts +18 -4
- package/dist/parsers/yara.d.ts.map +1 -1
- package/dist/parsers/yara.js +89 -48
- package/dist/parsers/yara.js.map +1 -1
- package/dist/tools/advisories.d.ts +3 -0
- package/dist/tools/advisories.d.ts.map +1 -1
- package/dist/tools/advisories.js +3 -12
- package/dist/tools/advisories.js.map +1 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +13 -0
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/registry.d.ts +8 -0
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js +7 -0
- package/dist/tools/registry.js.map +1 -1
- 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:
|
|
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":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;
|
|
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
|
|
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 =
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
|
|
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":"AAAA;;;;;GAKG;
|
|
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":"
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
122
|
+
runSpecific.push(`${notInstalled.length} tool(s) not installed — run check_tools to see installation status`);
|
|
88
123
|
}
|
|
89
|
-
// Report-drafting pointer
|
|
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
|
-
|
|
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);
|
|
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 === "
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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 (
|
|
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
|
-
|
|
187
|
-
findings.push(`${
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
533
|
-
//
|
|
534
|
-
|
|
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. " +
|